The registers driver should be used for registers that may be accessed by multiple drivers.
Concepts
Drivers often communicate with hardware via register-based interfaces. Conceptually, registers are groups of 8/16/32/64 bits that can be read or written via an address space. MMIO registers are accessed via the CPU's memory address space, while I2C registers are accessed via an I2C bus.
Most hardware only supports atomic register writes. So, drivers that want
to change a few bits in a register must do so via read/modify/write
operations. For example, for a 32-bit register, if you were to change the
0-1st bits both to 0
, you would need to read all 32 bits of that register,
say 0xFFFFFFFF
, then write back to that register 0xFFFFFFFC
.
Synchronization
Read/modify/write operations introduce the potential for data races. The classic transaction example "withdraw $10 from a bank account" is effectively a read/modify/write operation. See Data Race in the Examples section for a concrete illustration.
When a register is "exclusively owned" by a driver, the driver is expected to synchronize its accesses to the register to avoid data races. (The potential for races still exists, but it's outside the scope of the register driver).
When a register is "shared" (may be accessed) by multiple drivers, we need a global coordinator to avoid races. The register driver can act as this global coordinator, when each driver only needs to access a disjoint subset of the register's bits. The global coordinator will provide the needed synchronization between reads/writes of a register.
Isolation
The registers driver not only prevents races by providing synchronization, but also provides isolation of bit fields. The registers driver splits a register up into multiple resources and ensures that each driver only has access to those bits. A driver may not accidentally read or write into bits that it does not own.
Theory of operation
In the board driver, a register which is accessed by multiple drivers declares the different bit fields that it wants to expose. The registers driver creates devices for each of the bit fields. A driver that wants to access those bit fields needs to bind to the corresponding device.
Within the registers driver, a lock for each register is used to ensure that only one read/write can access the register at once. The interface of the registers driver is defined by registers-util.fidl.
How to use
Currently the Reset Registers are the only registers that are migrated to use the registers driver. This section will use them as an example on how to use the registers driver.
Board driver changes
Metadata format is declared in metadata.fidl.
In the board driver (i.e. nelson-registers), make the following changes.
a. MMIO
Ensure that the registers driver has access to the MMIO. If not, add MMIO and corresponding MMIO index:
enum MmioMetadataIdx { kResetMmio, kMmioCount, }; static const std::vector<fpbus::Mmio> registers_mmios{ { { .base = A311D_RESET_BASE, .length = A311D_RESET_LENGTH, }, }, };
Note that the
MmioMetadataIndex
must correspond to the MMIO's index inregisters_mmios
.b. Declare bitfields
The
RegistersMetadataToFidl
helper function can be used to declare bitfields:auto metadata_bytes = fidl_metadata::registers::RegistersMetadataToFidl<uint32_t>(kRegisters); if (metadata_bytes.is_error()) { zxlogf(ERROR, "%s: Failed to FIDL encode registers metadata %s\n", __func__, metadata_bytes.status_string()); return metadata_bytes.error_value(); } const std::vector<fpbus::Metadata> registers_metadata{ { { .type = DEVICE_METADATA_REGISTERS, .data = metadata_bytes.value(), }, }, };
where the bitfields are defined in the
kRegisters
field:static const fidl_metadata::registers::Register<uint32_t> kRegisters[]{ { .bind_id = aml_registers::REGISTER_USB_PHY_V2_RESET, .mmio_id = kResetMmio, .masks = { { .value = aml_registers::USB_RESET1_REGISTER_UNKNOWN_1_MASK | aml_registers::USB_RESET1_REGISTER_UNKNOWN_2_MASK, .mmio_offset = A311D_RESET1_REGISTER, }, { .value = aml_registers::USB_RESET1_LEVEL_MASK, .mmio_offset = A311D_RESET1_LEVEL, }, }, }, ... };
bind_id
: The unique ID for each bitfield definition that is used to identify it during binding.mmio_id
: The ID corresponding to the MMIO that this bitfield is in reference to.masks
: A list of masks describing the specific bitfields that should be accessible through this register device.value
: The bitmask that is available to access, with1
meaning accessible and0
inaccessible. For example for a 32-bit register, a bitmask may be0xFFFF0000
, which means the higher 16 bits of the register are accessible through this register device and the lower 16 are not.mmio_offset
: The starting offset from the start of the MMIO identified bymmio_id
that these bitfields define.count
: The number of register address this bitfield mask applies to followingmmio_offset
. Defaults to1
.overlap_check_on
: Iftrue
, the registers driver will verify that these bitfields do not overlap with any other defined bitfields. Otherwise, the check is skipped. Defaults totrue
.
The registers driver will create a device that serves registers-util.fidl with bind properties:
{:.devsite-disable-click-to-copy} bind_fuchsia_register::NAME == bind_id
Binding to the registers driver
The driver that wishes to access these bitfields must then bind to the device created above.
Using the registers driver interface
After successfully connecting to the registers device, say through
fidl::WireSyncClient<fuchsia_hardware_registers::Device> register
, you may call the corresponding FIDL read/write according to the FIDL interface defined in registers-util.fidl. E.g., for 32-bit registers,auto result = reset_register_->WriteRegister32(RESET1_LEVEL_OFFSET, aml_registers::USB_RESET1_LEVEL_MASK, aml_registers::USB_RESET1_LEVEL_MASK); if ((result.status() != ZX_OK) || result->is_error()) { zxlogf(ERROR, "Write failed\n"); return ZX_ERR_INTERNAL; }
Enjoy isolated synchronized registers!
Examples
Data Race
Say we have a register that is accessed by both driver A and driver B.
Now driver A wants to write b01
to bits 0-1 of the register and driver
B wants to write b001
to bits 8-10 of the register. Depending on timing
driver A may read the original value of the register, say 0xFFFFFFFF
and
driver B may also read that same value. Driver A then writes to bits 0-1
and puts into memory 0xFFFFFFFD
. Driver B then writes to memory
0xFFFFF9FF
. In this sequence of events, the register now holds the value
of 0xFFFFF9FF
, which driver B had written last. However, the next time
driver A comes to read bits 0-1 of this register, it will not get the
value it expects as it was previously written.
AMLogic SoCs
This section gives some examples of where the registers driver should and should not be used. The registers driver definitely should be used when it is needed for synchronization between multiple drivers, and may or may not be used only for isolation of resources. Note that the following is not a complete list of shared registers in the AMLogic SoCs.
Reset Registers
AMLogic SoCs have the reset functionality for various hardware units
concentrated in a few 32-bit registers. This is exemplified by
RESET1_REGISTER
in S905D3 Table 6-186. For example, USB reset is
controlled by bit 2 and SD_EMMC by bits 12-14. The hardware design forces
us to share RESET1_REGISTER
among multiple drivers including the EMMC
driver and the USB driver.
See nelson-registers for the board file changes made for reset
registers and nelson-usb - aml_usb_phy
device for adding reset
register fragments. AmlUsbPhy::InitPhy()
of aml-usb-phy uses
the FIDL client to write to the reset register.
Power Registers
Similar to the reset registers described above, AO_RTI_GEN_PWR_SLEEP0
(S905D3 Table 6-17) and AO_RTI_GEN_PWR_ISO0
(S905D3 Table
6-18) are shared by multiple drivers, including display, USB, and ML, and should
be managed by the registers driver because they are writable and need
coordination. This migration is currently in progress.
On the other hand, although AO_RTI_GEN_PWR_ACK0
(S905D3 Table 6-19)
will be shared by multiple drivers (PCIE, USB, display, etc.), it is readonly.
Data from this register can be accessed concurrently without any risk of races,
so it is not required to use the registers driver for synchronization. If
desired, we may still use the registers driver for isolation of resources.