So you've decided to bring up a new board. Before you dive into coding, ensure that you have everything you need by answering these questions:
How will I understand how the device and its registers work?
- This is usually called a "theory of operation". Manufacturers often provide datasheets with register definitions, but those references may not explain how the device is used in practice.
Is there an existing driver for a similar board?
- Where practical, reuse code for similar boards by refactoring that code and amending the bind rules for the driver.
Does the device have a fixed display?
- Some display controllers and panels (output screens) are tightly coupled. If this is the case for a new board, you'll need to add support for the GPIO, I2C, and other controls as part of the display driver.
Prerequisites
This guide assumes that you are familiar with driver development for one or more operating systems. It also assumes that you are familiar with the Fuchsia DDK-TL.
Programming Languages
New drivers must be written in C++. Rust support is planned, but is still highly experimental.
If an appropriately licensed driver already exists and is written in C, it may be acceptable to port it to Fuchsia rather than implementing a new version in C++. Please contact graphics-dev@fuchsia.dev before making this decision.
Getting Started
For platforms without ACPI or a PCI bus, Modifying board
drivers is the first step. This guide assumes that the
board driver is ready and that the display driver is codenamed fancy
. All code
for the new driver will live in src/graphics/display/drivers/fancy-display/
.
To begin, create:
- A minimal implementation of DisplayEngine
- A set of bind rules
- A build recipe for the
DisplayEngine
and the bind rules
Add the driver to the build
- Create the build recipe in a file named
BUILD.gn
# Copyright 2021 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import("//build/bind/bind.gni")
import("//build/drivers.gni")
driver_bind_rules("fancy-display-bind") {
rules = "meta/fancy-display.bind"
bind_output = "fancy-display.bindbc"
tests = "meta/bind_tests.json"
deps = [
"//src/devices/bind/board_maker_company.platform",
]
}
# Factored out so that it can be used in tests.
source_set("common") {
public_deps = [
":fancy-display-bind",
]
sources = [
"fancy-display.cc",
]
}
fuchsia_cc_driver("fancy-display") {
sources = []
deps = [
":common",
"//src/devices/lib/driver",
]
}
- Add
//src/graphics/display/drivers/fancy-display
as a dependency for the board(s) that you are using as test products. For example, if your device is part of a Khadas VIM3 board, modify//boards/vim3.gni
by adding your driver to the_common_bootfs_deps
list.
Choose devices to drive
Now that you have a build recipe, you can move on to creating the bind rules, which the driver manager uses to decide whether a driver can be used with a device.
- In
src/graphics/display/drivers/fancy-display
, createmeta/fancy-display.bind
:
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
using fuchsia.pci;
fuchsia.BIND_PROTOCOL == fuchsia.pci.BIND_PROTOCOL.DEVICE;
fuchsia.BIND_PCI_VID == fuchsia.pci.BIND_PCI_VID.PLANK_HW_INC;
accept fuchsia.BIND_PCI_DID {
// Fancy
0x0100,
// Fancy+ series
0x0120,
0x0121,
}
For PC devices, the intel-display bind rules are a good example. For fixed-hardware SoCs, see the Amlogic display rules.
Minimal driver
Finally, add a bare bones driver that simply constructs a new object every time it successfully binds to a device. Later, you can use the datasheet to get the device to actually do something.
In src/graphics/display/drivers/fancy-display
, create fancy-display.cc
:
#include <ddktl/device.h>
#include <fuchsia/hardware/display/controller/cpp/banjo.h>
namespace fancy_display {
class Device;
using DeviceType = ddk::Device<Device>
// A Device exposes a single display controller for use by the display
// coordinator driver in src/graphics/display/drivers/coordinator.
//
// This object is constructed once for each device that matches this
// driver's bind rules.
class Device : public DeviceType {
public:
explicit Device(zx_device_t* parent) : DeviceType(parent) {}
// If Bind() returns an error, the driver won't claim the device.
zx_status_t Bind() { return ZX_OK };
// Functionality needed by the common display driver core.
void DisplayEngineRegisterDisplayEngineListener(
const display_engine_listener_protocol* interface) {}
zx_status_t DisplayEngineImportBufferCollection(
uint64_t collection_id, zx::channel collection_token) {
return ZX_ERR_NOT_SUPPORTED;
}
zx_status_t DisplayEngineReleaseBufferCollection(
uint64_t collection_id) {
return ZX_ERR_NOT_SUPPORTED;
}
zx_status_t DisplayEngineImportImage(const image_metadata_t* image_metadata,
uint64_t collection_id, uint32_t index,
uint64_t* out_image_handle) {
return ZX_ERR_NOT_SUPPORTED;
}
void DisplayEngineReleaseImage(image_t* image) {}
config_check_result_t DisplayEngineCheckConfiguration(
const display_config_t** display_configs, size_t display_count,
client_composition_opcode_t* out_client_composition_opcodes_list, size_t client_composition_opcodes_count,
size_t* out_client_composition_opcodes_actual);
void DisplayEngineApplyConfiguration(
const display_config_t** display_config, size_t display_count) {}
zx_status_t DisplayEngineSetBufferCollectionConstraints(
const image_buffer_usage_t* usage, uint64_t collection_id) {
return ZX_ERR_NOT_SUPPORTED;
}
};
} // namespace fancy_display
// Main bind function called from dev manager.
zx_status_t fancy_display_bind(void* ctx, zx_device_t* parent) {
fbl::AllocChecker alloc_checker;
auto dev = fbl::make_unique_checked<fancy_display::Device>(
&alloc_checker, parent);
if (!alloc_checker.check()) {
return ZX_ERR_NO_MEMORY;
}
auto status = dev->Bind();
if (status == ZX_OK) {
// The driver/device manager now owns this memory.
[[maybe_unused]] auto ptr = dev.release();
}
return status;
}
// zx_driver_ops_t is the ABI between driver modules and the device manager.
// This lambda is used so that drivers can be rebuilt without compiler
// warnings if/when new fields are added to the struct.
static zx_driver_ops_t fancy_display_ops = [](){
zx_driver_ops_t ops;
ops.version = DRIVER_OPS_VERSION;
ops.bind = fancy_display_bind;
return ops;
}();
// ZIRCON_DRIVER marks the compiled driver as compatible with the zircon
// 0.1 driver ABI.
ZIRCON_DRIVER(fancy_display, fancy_display_ops, "zircon", "0.1");
Display drivers are required to implement the DisplayEngine
protocol, which exposes hardware layers and implements vsync
notifications. A display-coordinator driver multiplexes
between all the device-specific drivers on the system and the display driver
stack clients, which are the system compositor and Virtcon.
Implementation tips
The driver decides when and how a configuration passed to ApplyConfiguration
takes effect. In order to avoid tearing, drivers should
apply new settings just after vsync.
Most devices generate interrupts for vsync events. The easiest way to
ensure timely vsync notifications is to spawn a separate thread just for
servicing that interrupt. Even if no images are displayed, your driver must
call OnDisplayVsync
for every vsync.
Controllers with bootloader support
If the display is active on boot, e.g. a panel is turned on and an image is displayed, then you can get basic functionality in your driver quickly. Read bootloader logs and/or source to find:
- The physical address of the framebuffer
- The registers used to program that address
- The pixel dimensions of the image, e.g. 800x600
- The pixel format of the image, e.g. RGB888, NV12, or BGRA8888
Then:
- Modify the driver to report a display with the format constraints.
- Record the physical address of any imported image in
image->handle
. - When
ApplyConfig
is called, re-program the registers.
If you do not yet know how to observe vsyncs, you can fake it with a thread that
calls OnDisplayVsync
at 60Hz.
Controllers that boot "dark"
There is no one right way to bring up a display controller that lacks even a basic bootloader driver. In most cases, your roadmap will be:
- Power up the device.
- Initialize clocks.
- Discover attached displays.
- Program PHYs for a compatible mode.
- Program layouts (framebuffer addrs, etc.) on vsync to avoid tearing.
- Integrate with Sysmem.