diff --git a/acacia_sddf/__init__.py b/acacia_sddf/__init__.py new file mode 100644 index 000000000..d8477dd19 --- /dev/null +++ b/acacia_sddf/__init__.py @@ -0,0 +1,9 @@ +# Copyright 2026, UNSW +# SPDX-License-Identifier: BSD-2-Clause + +from .i2c import sDDFI2C +from .timer import sDDFTimer +from .serial import sDDFSerial +from .sddf import sDDFDriverClass, sDDFDriverConfig, sDDFDriverManifest +from .board import BOARDS, Board +from .pmic import sDDFPMIC diff --git a/acacia_sddf/board.py b/acacia_sddf/board.py new file mode 100644 index 000000000..f78377a87 --- /dev/null +++ b/acacia_sddf/board.py @@ -0,0 +1,192 @@ +# Copyright 2025, UNSW +# SPDX-License-Identifier: BSD-2-Clause +from dataclasses import dataclass +from typing import List, Optional, Tuple +from acacia import System, ProtectionDomain, aarch64, riscv64, x86_64, Arch +from importlib.metadata import version + + +@dataclass(frozen=True) +class DriverDouble: + compatible: str + node_path: str + + +@dataclass +class Board: + name: str + arch: Arch + paddr_top: int + # Driver mappings -> (compatible, preferred_node) tuples + serial: Optional[DriverDouble] = DriverDouble(None, None) + ethernet: Optional[DriverDouble] = DriverDouble(None, None) + timer: Optional[DriverDouble] = DriverDouble(None, None) + i2c: Optional[DriverDouble] = DriverDouble(None, None) + blk: Optional[DriverDouble] = DriverDouble(None, None) + pmic: Optional[DriverDouble] = DriverDouble(None, None) + partition: int = 0 + baud_rate: Optional[int] = None + + +# Keep this list in alphabetical order by board name +# TODO: convert to Dictionary +BOARDS: List[Board] = [ + Board( + name="cheshire", + arch=riscv64, + paddr_top=0x90000000, + serial=DriverDouble("ns16550a", "soc/serial@3002000"), + i2c=DriverDouble("eth,i2c", "soc/i2c@3003000"), + ), + Board( + name="hifive_p550", + arch=riscv64, + paddr_top=0xA0000000, + serial=DriverDouble("snps,dw-apb-uart", "soc/serial@0x50900000"), + ), + Board( + name="imx8mm_evk", + arch=aarch64, + paddr_top=0x70000000, + serial=DriverDouble( + "fsl,imx8mm-uart", "soc@0/bus@30800000/spba-bus@30800000/serial@30890000" + ), + timer=DriverDouble("fsl,imx8mm-gpt", "soc@0/bus@30000000/timer@302d0000"), + ethernet=DriverDouble("", "soc@0/bus@30800000/ethernet@30be0000"), + ), + Board( + name="imx8mp_evk", + arch=aarch64, + paddr_top=0x70000000, + serial=DriverDouble( + "fsl,imx8mp-uart", "soc@0/bus@30800000/spba-bus@30800000/serial@30890000" + ), + timer=DriverDouble("fsl,imx8mp-gpt", "soc@0/bus@30000000/timer@302d0000"), + ethernet=DriverDouble("", "soc@0/bus@30800000/ethernet@30bf0000"), + ), + Board( + name="imx8mp_iotgate", + arch=aarch64, + paddr_top=0x70000000, + serial=DriverDouble("fsl,imx8mp-uart", "soc@0/bus@30800000/serial@30890000"), + timer=DriverDouble("fsl,imx8mp-gpt", "soc@0/bus@30000000/timer@302d0000"), + ethernet=DriverDouble("", "soc@0/bus@30800000/ethernet@30bf0000"), + ), + Board( + name="imx8mq_evk", + arch=aarch64, + paddr_top=0x70000000, + serial=DriverDouble("fsl,imx8mq-uart", "soc@0/bus@30800000/serial@30860000"), + timer=DriverDouble("fsl,imx8mq-gpt", "soc@0/bus@30000000/timer@302d0000"), + ethernet=DriverDouble("", "soc@0/bus@30800000/ethernet@30be0000"), + ), + Board( + name="kria_k26", + arch=aarch64, + paddr_top=0x70000000, + timer=DriverDouble("cdns,ttc", "axi/timer@ff140000"), + serial=DriverDouble("xlnx,zynqmp-uart", "axi/serial@ff010000"), + ), + Board( + name="maaxboard", + arch=aarch64, + paddr_top=0x70000000, + serial=DriverDouble("fsl,imx8mq-uart", "soc@0/bus@30800000/serial@30860000"), + timer=DriverDouble("fsl,imx8mq-gpt", "soc@0/bus@30000000/timer@302d0000"), + ethernet=DriverDouble("", "soc@0/bus@30800000/ethernet@30be0000"), + blk=DriverDouble("", "soc@0/bus@30800000/mmc@30b40000"), + i2c=DriverDouble("fsl,imx8mq-i2c", "soc@0/bus@30800000/i2c@30a20000"), + pmic=DriverDouble("rohm,bd71837", "soc@0/bus@30800000/i2c@30a20000/pmic@4b"), + partition=2, + ), + Board( + name="odroidc2", + arch=aarch64, + paddr_top=0x60000000, + serial=DriverDouble("amlogic,meson-gx-uart", "soc/bus@c8100000/serial@4c0"), + timer=DriverDouble("amlogic,meson-gxbb-wdt", "soc/bus@c1100000/watchdog@98d0"), + ethernet=DriverDouble("", "soc/ethernet@c9410000"), + baud_rate=115200, + ), + Board( + name="odroidc4", + arch=aarch64, + paddr_top=0x60000000, + i2c=DriverDouble("amlogic,meson-axg-i2c", "soc/bus@ffd00000/i2c@1d000"), + serial=DriverDouble("amlogic,meson-gx-uart", "soc/bus@ff800000/serial@3000"), + timer=DriverDouble("amlogic,meson-gxbb-wdt", "soc/bus@ffd00000/watchdog@f0d0"), + ethernet=DriverDouble("amlogic,meson-gx-uart", "soc/ethernet@ff3f0000"), + baud_rate=115200, + ), + Board( + name="qemu_virt_aarch64", + arch=aarch64, + paddr_top=0x6_0000_000, + serial=DriverDouble("arm,pl011", "pl011@9000000"), + timer=DriverDouble("arm,armv8-timer", "timer"), + blk=DriverDouble("", "virtio_mmio@a000200"), + ethernet=DriverDouble("", "virtio_mmio@a000000"), + i2c=None, + ), + Board( + name="qemu_virt_riscv64", + arch=riscv64, + paddr_top=0xA_0000_000, + serial=DriverDouble("ns16550a", "soc/serial@10000000"), + timer=DriverDouble("google,goldfish-rtc", "soc/rtc@101000"), + ethernet=DriverDouble("", "soc/virtio_mmio@10001000"), + blk=DriverDouble("", "soc/virtio_mmio@10002000"), + partition=0, + i2c=None, + ), + Board( + name="rock3b", + arch=aarch64, + paddr_top=0xEC000000, + serial=DriverDouble("snps,dw-apb-uart", "serial@fe660000"), + timer=DriverDouble("rockchip,rk3568-timer", "rktimer@fe5f0000"), + ethernet=DriverDouble("", "ethernet@fe2a0000"), + baud_rate=1500000, + ), + Board( + name="rpi4b_1gb", + arch=aarch64, + paddr_top=0x2_000_000, + serial=DriverDouble("brcm,bcm2835-aux-uart", "soc/serial@7e215040"), + timer=DriverDouble("brcm,bcm2835-system-timer", "soc/timer@7e003000"), + ethernet=DriverDouble("", "scb/ethernet@7d580000"), + ), + Board( + name="serengeti", + arch=riscv64, + paddr_top=0x90000000, + serial=DriverDouble("ns16550a", "soc/serial@3002000"), + i2c=DriverDouble("eth,i2c", "soc/i2c@3003000"), + timer=DriverDouble("pulp,apb_timer", "soc/timer@300B000"), + ), + Board( + name="star64", + arch=riscv64, + paddr_top=0x100000000, + serial=DriverDouble("starfive,jh7110-uart", "soc/serial@10000000"), + timer=DriverDouble("starfive,jh7110-timer", "soc/timer@13050000"), + ethernet=DriverDouble("", "soc/ethernet@16030000"), + ), + Board( + name="zcu102", + arch=aarch64, + paddr_top=0x80000000, + timer=DriverDouble("cdns,ttc", "axi/timer@ff140000"), + serial=DriverDouble("xlnx,zynqmp-uart", "axi/serial@ff000000"), + ), + Board( + name="x86_64_generic", + arch=x86_64, + paddr_top=0x7FFDF000, + ), + Board( + name="x86_64_generic_vtx", + arch=x86_64, + paddr_top=0x7FFDF000, + ), +] diff --git a/acacia_sddf/driver_manifest.py b/acacia_sddf/driver_manifest.py new file mode 100644 index 000000000..aecab9e30 --- /dev/null +++ b/acacia_sddf/driver_manifest.py @@ -0,0 +1,92 @@ +# Copyright 2026, UNSW +# SPDX-License-Identifier: BSD-2-Clause + +from dataclasses import dataclass +from typing import List, Dict, Type, Union, Optional +from collections import defaultdict + + +@dataclass +class DTSRegion: + name: str + perms: str = None + size: int = None + dt_idx: int = None + + +@dataclass +class DTSIRQ: + dt_index: int + + +@dataclass +class sDDFDriverConfig: + """ + Encapsulation of device tree fields describing an instance + of a driver. + + WARNING: the order of regions and irqs affects the order they are + mapped into the driver in config structs! We REALLY shouldn't have + this be the case. This is a hangover from `config.json` and sdfgen. + + TODO: make this better in future + """ + + compatible: Union[List[str], str] + regions: List[DTSRegion] + irqs: List[DTSIRQ] + + def __post_init__(self): + if type(self.compatible) is str: + self.compatible = [self.compatible] + assert type(self.regions) is list + assert type(self.irqs) is list + + +class __sDDFDriverManifest: + """ + Wrapper class encapsulating sDDF driver manifest. This is a + mapping of driver subsystem type -> list of driver names -> + DTS fields. I.e. this encodes: + * Which drivers are compatible with what devices, according to the + device tree, + * What drivers are available in each driver class, + * Which driver subsystem types in sdfgen map to which drivers. + + You should NOT make a new instance of this class! Use the + `sDDFDriverManifest()` function to get the global instance. + """ + + def __init__(self): + self.map: Dict[Type[sDDFDeviceClass], Dict[str, sDDFDriverConfig]] = ( + defaultdict(dict) + ) + + def add_driver_config( + self, + subsystem_type: Type[sDDFDriverConfig], + driver_name: str, + config: sDDFDriverConfig, + ): + # Refuse namespace collisions + if driver_name in self.map[subsystem_type]: + raise ValueError( + f"Driver named {driver_name} already exists for " f"{subsystem_type}!" + ) + self.map[subsystem_type][driver_name] = config + + def __getitem__(self, item): + # Allow array syntax for indexing into dict of driver names per class type + return self.map[item] + + def get_configs_matching_compatible( + self, subsystem_type: Type[sDDFDriverConfig], compat: str + ) -> List[sDDFDriverConfig]: + return [c for c in self.map[subsystem_type].values() if compat in c.compatible] + + +module_manifest = __sDDFDriverManifest() + + +def sDDFDriverManifest(): + return module_manifest diff --git a/acacia_sddf/i2c.py b/acacia_sddf/i2c.py new file mode 100644 index 000000000..74da991ca --- /dev/null +++ b/acacia_sddf/i2c.py @@ -0,0 +1,384 @@ +# Copyright 2026, UNSW +# SPDX-License-Identifier: BSD-2-Clause + +from acacia import ( + System, + Subsystem, + ProtectionDomain, + Channel, + Map, + MemoryRegion, + DTBNode, + DeviceTreeBlob, + SchedulingProperties, + ConfigStruct, +) +import sys, os +from collections import defaultdict +from dataclasses import dataclass +from typing import List, Dict, Type, Union, Optional +from .driver_manifest import sDDFDriverManifest, sDDFDriverConfig, DTSIRQ, DTSRegion +from .sddf import sDDFDriverClass, DeviceResourcesFactory, RegionResourceFactory + +I2C_DATA_SZ = 0x1000 +I2C_NUM_BUFS = 128 # TODO: add support for dynamically sized queues +I2C_PROTOCOL_MAGIC = "sDDF" + chr(0x4) + + +@dataclass +class I2CAddress: + addr: int + + def __post_init__(self): + if self.addr & (1 << 31): + # This bit is set in DTS I2C addresses to mark 10 bit addresses. + # If found, we strip it out and store a bool. + self.is_ten_bit = True + self.addr &= ~(1 << 31) + else: + self.is_ten_bit = False + + +class sDDFI2C(sDDFDriverClass): + def __init__( + self, + sdf: System, + dev_compatible: str, + dev_dt_path: str, + driver_prio: int, + virt_prio: int, + cpu: Optional[int] = None, + virt_elf: str = "i2c_virt.elf", + driver_elf: str = "i2c_driver.elf", + ): + super().__init__( + sdf, "i2c", dev_compatible, dev_dt_path, magic="sDDF" + chr(0x1) + ) + self.sdf = sdf + self.cpu = cpu + + # Internal bookkeeping + self.driver = None + self.virt = None + + # ELF names. This is required because acacia is tragically detached + # from the build system itself and cannot guarantee the names of sDDF + # compiled objects itself. This will not be required once we start using + # the sDDF with an SDK model, but that is for the future. + self.virt_elf = virt_elf + self.driver = ProtectionDomain( + self.sdf, + "i2c_driver", + driver_elf, + scheduling=SchedulingProperties(driver_prio), + cpu=self.cpu, + ) + + # We must make the driver BEFORE we get here + self.driver_dev_resources = self.create_dtb_resources(self.driver) + + # Stubs of config structs that we need to collect in construct_infrastructure and connect_clients + self.virt_config = None + self.driver_config = None + self.virt_driver_config = None + self.client_configs = [] + self.channels = [] + + # Special cases for boards. These should be removed once we add infrastructure to support this better. + # meson (odroidc4/5) + if "amlogic,meson" in dev_compatible: + # Odroid-C4 I2C requires clocks/GPIO setup, for now we give the I2C driver + # direct access. + clk_mr = MemoryRegion( + self.sdf, "clk", 0x1000, paddr=0xFF63C000, cached=False + ) + gpio_mr = MemoryRegion( + self.sdf, "gpio", 0x1000, paddr=0xFF634000, cached=False + ) + self.driver.add_map(Map(clk_mr, 0x30_000_000, "rw")) + self.driver.add_map(Map(gpio_mr, 0x30_100_000, "rw")) + + # We create queues etc. AFTER setting up the device resources to ensure that IRQ channels + # have a lower value than any other channels. This is necessary because Microkit will + # deliver notifications in ascending channel_id order, which can end up mattering in certain + # cases. + self.construct_infrastructure(virt_prio) + + def construct_infrastructure(self, virt_prio): + self.virt = ProtectionDomain( + self.sdf, + "i2c_virt", + self.virt_elf, + scheduling=SchedulingProperties(virt_prio), + cpu=self.cpu, + ) + + # Make queues + driver_req_q_mr = MemoryRegion(self.sdf, "i2c_driver_request", 0x1000) + driver_resp_q_mr = MemoryRegion(self.sdf, "i2c_driver_response", 0x1000) + driver_req_map = self.driver.create_automap( + driver_req_q_mr, Map.Permissions(r=True, w=True) + ) + driver_resp_map = self.driver.create_automap( + driver_resp_q_mr, Map.Permissions(r=True, w=True) + ) + virt_req_map = self.virt.create_automap( + driver_req_q_mr, Map.Permissions(r=True, w=True) + ) + virt_resp_map = self.virt.create_automap( + driver_resp_q_mr, Map.Permissions(r=True, w=True) + ) + + # Create channels + driver_virt_ch = Channel( + self.sdf, + Channel.End(self.driver, can_notify=True, can_pp=False), + Channel.End(self.virt, can_notify=True, can_pp=False), + ) + self.channels.append(driver_virt_ch) + + # Create config structs + self.virt_driver_config = self.i2c_connection_resource_factory( + virt_req_map, + virt_resp_map, + I2C_NUM_BUFS, + driver_virt_ch.id_for_pd(self.virt), + ) + driver_virt_connection = self.i2c_connection_resource_factory( + driver_req_map, + driver_resp_map, + I2C_NUM_BUFS, + driver_virt_ch.id_for_pd(self.driver), + ) + self.driver_config = self.i2c_driver_config_factory( + self.driver, I2C_PROTOCOL_MAGIC, driver_virt_connection + ) + + def connect_clients(self): + assert self.virt is not None + assert self.driver is not None + + # Clients are connected with: + # a. request queue + # b. response queue + # c. data region shared with driver + # c. channel for notifications and PPCs + virt_client_configs = [] + for c in self.clients: + if c.priority >= self.virt.priority: + raise SubsystemBuildError( + f"Client {c} has a priority higher " + f"than virt's ({self.driver.priority})!" + ) + # Make channel + ch = Channel( + self.sdf, + Channel.End(c, can_notify=True, can_pp=True), + Channel.End(self.virt, can_notify=True, can_pp=False), + ) + self.channels.append(ch) + + # Add request and response queue + c_req_q_mr = MemoryRegion(self.sdf, f"i2c_client_request_{c.name}", 0x1000) + c_resp_q_mr = MemoryRegion( + self.sdf, f"i2c_client_response_{c.name}", 0x1000 + ) + c_data_mr = MemoryRegion(self.sdf, f"i2c_client_data_{c.name}", I2C_DATA_SZ) + + # Create maps for clients + c_req_map = c.create_automap(c_req_q_mr, Map.Permissions(r=True, w=True)) + c_resp_map = c.create_automap(c_resp_q_mr, Map.Permissions(r=True, w=True)) + c_data_map = c.create_automap(c_data_mr, Map.Permissions(r=True, w=True)) + + # Maps for virt / driver + req_map = self.virt.create_automap( + c_req_q_mr, Map.Permissions(r=True, w=True) + ) + resp_map = self.virt.create_automap( + c_resp_q_mr, Map.Permissions(r=True, w=True) + ) + data_map = self.driver.create_automap( + c_data_mr, Map.Permissions(r=True, w=True) + ) + + # Prep config structs + virt_connection = self.i2c_connection_resource_factory( + req_map, resp_map, I2C_NUM_BUFS, ch.id_for_pd(self.virt) + ) + client_connection = self.i2c_connection_resource_factory( + c_req_map, c_resp_map, I2C_NUM_BUFS, ch.id_for_pd(c) + ) + client_data = RegionResourceFactory(c_data_map) + + self.client_configs.append( + self.i2c_client_config_factory(c, client_connection, client_data) + ) + virt_client_configs.append( + self.i2c_virt_client_config_factory( + virt_connection, I2C_DATA_SZ, data_map.vaddr, c_data_map.vaddr + ) + ) + # Clients added. Finally, create virt config + self.virt_config = self.i2c_virt_config_factory( + self.virt, + I2C_PROTOCOL_MAGIC, + len(self.clients), + self.virt_driver_config, + virt_client_configs, + ) + + def generate_config_structs(self): + # We've already made our structs, just return them as a list for the serialiser + driver_resources = [self.driver_dev_resources, self.driver_config] + virt_resources = [self.virt_config] + return driver_resources + virt_resources + self.client_configs + + # ### dtb utility functions for drivers that depend on i2c ### + def get_i2c_addresses_from_dtb(self, device_node: DTBNode) -> List[I2CAddress]: + """ + Try and get the i2c addresses of a device on the bus controlled by this driver. + + If the path given is not for this I2C bus or the device is not present, a + ValueError will be raised. + Args: + device_node: DTBNode - peripheral node + Returns: + List[I2CAddress] + """ + # first: check that this path belongs to us + if self.dtb_node.path not in device_node.path: + raise ValueError( + f"{device_node} doesn't belong to this I2C bus ({self.dtb_node.path})!" + ) + + # reg property contains addresses. + reg = [x[0] for x in self.dtb.get_node_regs(device_node)] + return [I2CAddress(int(x)) for x in reg] + + # ### connection config struct factory functions ### + def i2c_connection_resource_factory( + self, req_q: Map, resp_q: Map, num_bufs: int, id: int + ) -> ConfigStruct: + fields = { + "req_queue": RegionResourceFactory(req_q), + "resp_queue": RegionResourceFactory(resp_q), + "num_buffers": num_bufs, + "id": id, + } + return ConfigStruct("i2c_connection_resource_t", fields=fields) + + def i2c_client_config_factory( + self, + client_pd: ProtectionDomain, + virt_connection: ConfigStruct, + data_region: ConfigStruct, + ) -> ConfigStruct: + """ + Create i2c_client_config for client_pd with serial id n + """ + # invariant: this PD only is a client to i2c one time. + end = next(x.end_a for x in self.channels if x.end_a.pd is client_pd) + ch_id = end.ch_id + fields = { + "magic": I2C_PROTOCOL_MAGIC, + "virt": virt_connection, + "data": data_region, + } + return ConfigStruct( + "i2c_client_config_t", + target_file=client_pd.prog_image, + section_name="i2c_client_config", + fields=fields, + ) + + def i2c_virt_client_config_factory( + self, + client_connection: ConfigStruct, + data_size: int, + driver_d_vaddr: int, + client_d_vaddr: int, + ) -> ConfigStruct: + """ + Create a i2c_virt_client_config for some client. + """ + fields = { + "conn": client_connection, + "data_size": data_size, + "driver_data_vaddr": driver_d_vaddr, + "client_data_vaddr": client_d_vaddr, + } + return ConfigStruct("i2c_virt_client_config_t", fields=fields) + + def i2c_virt_config_factory( + self, + virt_pd: ProtectionDomain, + magic: str, + num_clients: int, + driver_connection: ConfigStruct, + client_connections: List[ConfigStruct], + ) -> ConfigStruct: + fields = { + "magic": magic, + "num_clients": num_clients, + "driver": driver_connection, + "clients": client_connections, + } + return ConfigStruct( + "i2c_virt_config_t", + target_file=virt_pd.prog_image, + section_name="i2c_virt_config", + fields=fields, + ) + + def i2c_driver_config_factory( + self, driver_pd: ProtectionDomain, magic: str, virt_connection: ConfigStruct + ) -> ConfigStruct: + fields = { + "magic": magic, + "virt": virt_connection, + } + return ConfigStruct( + "i2c_driver_config_t", + target_file=driver_pd.prog_image, + section_name="i2c_driver_config", + fields=fields, + ) + + +# Driver configs +i2c_driver_configs: Dict[str, List[sDDFDriverConfig]] = defaultdict(list) + + +def add_driver_config(driver_name: str, config: sDDFDriverConfig): + sDDFDriverManifest().add_driver_config(sDDFI2C, driver_name, config) + + +# meson +add_driver_config( + "meson", + sDDFDriverConfig( + compatible="amlogic,meson-axg-i2c", + regions=[DTSRegion("regs", "rw", 4096, 0)], + irqs=[DTSIRQ(0), DTSIRQ(1)], + ), +) + +# opentitan +add_driver_config( + "opentitan", + sDDFDriverConfig( + compatible="eth,i2c", + regions=[DTSRegion("regs", "rw", 4096, 0)], + irqs=[DTSIRQ(4), DTSIRQ(0), DTSIRQ(1), DTSIRQ(7), DTSIRQ(9)], + ), +) + +# imx (8mq only for now? untested on others) +add_driver_config( + "imx", + sDDFDriverConfig( + compatible=["fsl,imx8mq-i2c", "fsl,imx21-i2c"], + regions=[DTSRegion("regs", "rw", 4096, 0)], + irqs=[DTSIRQ(0)], + ), +) diff --git a/acacia_sddf/pmic.py b/acacia_sddf/pmic.py new file mode 100644 index 000000000..9c24f589f --- /dev/null +++ b/acacia_sddf/pmic.py @@ -0,0 +1,150 @@ +# Copyright 2026, UNSW +# SPDX-License-Identifier: BSD-2-Clause + +from acacia import ( + System, + Subsystem, + ProtectionDomain, + Channel, + Map, + MemoryRegion, + DTBNode, + DeviceTreeBlob, + SchedulingProperties, + ConfigStruct, + IRQ, + SubsystemBuildError, +) +import sys, os +from .driver_manifest import sDDFDriverManifest, sDDFDriverConfig, DTSIRQ, DTSRegion +from .sddf import sDDFDriverClass, DeviceResourcesFactory, RegionResourceFactory +from .i2c import sDDFI2C, I2CAddress +from collections import defaultdict +from typing import List, Dict, Type, Union, Optional + + +class sDDFPMIC(sDDFDriverClass): + def __init__( + self, + sdf: System, + dev_compatible: str, + dev_dt_path: str, + i2c: sDDFI2C, + i2c_addr: Optional[I2CAddress] = None, + driver_prio: Optional[int] = None, + cpu: Optional[int] = None, + driver_elf: str = "pmic_driver.elf", + ): + self.i2c = i2c + self.cpu = cpu + assert not i2c.built # We need to be a client! + super().__init__( + sdf, "pmic", dev_compatible, dev_dt_path, magic="sDDF" + chr(1) + ) + if driver_prio: + assert driver_prio < i2c.virt.priority + else: + # Default to just below i2c virt + driver_prio = i2c.virt.priority - 1 + self.driver = ProtectionDomain( + self.sdf, + "pmic_driver", + driver_elf, + scheduling=SchedulingProperties(driver_prio), + cpu=self.cpu, + ) + i2c.add_client(self.driver) + + # PMIC doesn't need device resources (currently)! Just an I2C client for imx. + # self.driver_dev_resources = self.create_dtb_resources(self.driver) + + # If no address is provided, we should try find one matching our dev_dt_path. + dtb_i2c_addrs = self.i2c.get_i2c_addresses_from_dtb(self.dtb_node) + if i2c_addr: + if i2c_addr not in dtb_i2c_addrs: + raise ValueError( + f"PMIC @ addr={i2c_addr} not present in DTB! Eligible: {dtb_i2c_addrs}" + ) + self.i2c_addr = i2c_addr + else: + if len(dtb_i2c_addrs) != 1: + # Perhaps we can have better default behaviour? + raise RuntimeWarning( + f"PMIC doesn't have a single I2C address in DTB (found `{dtb_i2c_addrs}`)! Please specify using i2c_addr=(preferred)" + ) + self.i2c_addr = dtb_i2c_addrs[0] # Should only be one! + + if self.i2c_addr.is_ten_bit: + raise RuntimeError( + "The sDDF does not currently support ten-bit I2C addresses!" + ) + + # TODO: claim i2c address from i2c driver once support is added to do so + self.client_configs = [] + + def connect_clients(self): + # Clients are connected with: + # a. channel allowing PPCs -> driver + for c in self.clients: + if c.priority > self.driver.priority: + raise SubsystemBuildError( + f"Client {c} has higher priority than pmic driver!" + ) + ch = Channel( + self.sdf, + Channel.End(c, can_notify=False, can_pp=True), + Channel.End(self.driver, can_notify=False, can_pp=False), + ) + self.client_configs.append( + self.pmic_client_config_factory(c, ch.id_for_pd(c)) + ) + + def x86_resources(self): + self.add_x86_hpet() + + def generate_config_structs(self): + # We've already made our structs + return [self.pmic_driver_config_factory()] + self.client_configs + + def pmic_driver_config_factory(self) -> ConfigStruct: + """ + create pmic_driver_config_t for driver. + """ + fields = {"magic": "sDDF" + chr(7), "i2c_addr": self.i2c_addr.addr} + return ConfigStruct( + "pmic_driver_config_t", + target_file=self.driver.prog_image, + section_name="pmic_driver_config", + fields=fields, + ) + + def pmic_client_config_factory( + self, client_pd: ProtectionDomain, driver_id: int + ) -> ConfigStruct: + """ + create pmic_client_config for client_pd + """ + # invariant: this PD only is a client to pmic one time. + fields = {"magic": "sDDF" + chr(7), "driver_id": driver_id} + return ConfigStruct( + "pmic_client_config_t", + target_file=client_pd.prog_image, + section_name="pmic_client_config", + fields=fields, + ) + + +# Driver configs +def add_driver_config(driver_name: str, config: sDDFDriverConfig): + sDDFDriverManifest().add_driver_config(sDDFPMIC, driver_name, config) + + +# imx +add_driver_config( + "bd71837", + sDDFDriverConfig( + "rohm,bd71837", + regions=[], + irqs=[], + ), +) diff --git a/acacia_sddf/sddf.py b/acacia_sddf/sddf.py new file mode 100644 index 000000000..d0234d5d9 --- /dev/null +++ b/acacia_sddf/sddf.py @@ -0,0 +1,241 @@ +# Copyright 2026, UNSW +# SPDX-License-Identifier: BSD-2-Clause +import sys, os +from typing import List, Optional, Tuple +from abc import abstractmethod +from acacia import ( + Subsystem, + ProtectionDomain, + Channel, + Map, + MemoryRegion, + DTBNode, + DeviceTreeBlob, + SchedulingProperties, + ConfigStruct, + IRQ, + System, +) +from .driver_manifest import sDDFDriverManifest, sDDFDriverConfig, DTSIRQ, DTSRegion + + +class sDDFDriverClass(Subsystem): + """ + This abstract class is inherited by all sDDF driver class implementations. + It handles: + a) Mapping of sDDF drivers to their corresponding device tree blobs + b) Parsing the device tree to set up device resources + c) Providing some common utility functions for generating config structs, etc. + """ + + def __init__( + self, + system: System, + class_name: str, + dev_compatible: str, + dev_dt_path: str, + magic: str, + ): + super().__init__(system, class_name) + + self.sdf = system + self.dtb = system.dtb + self.driver_magic = magic + if system.dtb is None: + print( + f"Initialising {class_name} driver with no DTB. Assuming this is x86 and no DTB is needed" + ) + return + + # Find real DTB node + print( + f"Finding {class_name} compatible for {dev_compatible} -- {dev_dt_path} from {self.dtb.file_path}" + ) + target_node = self.dtb.get_node_by_path(dev_dt_path) + + # make sure compatible matches! + if dev_compatible not in (a_c := self.dtb.get_compatible(target_node)): + raise IOError( + f"Target node {dev_dt_path} has compatible {a_c}... " + f"doesn't match expected {dev_compatible}!" + ) + + # check if DTB node is "okay" if it has a status + ok = self.dtb.get_node_prop(target_node, "status") + if ok is not None and ok.as_str() != "okay": + raise RuntimeError(f"{target_node} has bad status {ok.as_str()}!") + + self.dtb_node = target_node + + # Find sDDF driver matching this node + matching_configs = sDDFDriverManifest().get_configs_matching_compatible( + type(self), dev_compatible + ) + + if len(matching_configs) == 0: + raise RuntimeError( + f"No driver config matches {dev_compatible} -> {dev_dt_path}!" + ) + elif len(matching_configs) != 1: + raise RuntimeError( + f"Multiple sDDF drivers satisfy {dev_compatible}! " + f"There whould be only one.\n{matching_configs}" + ) + self.driver_config = matching_configs[0] + + def create_dtb_resources(self, driver_pd: ProtectionDomain) -> ConfigStruct: + """ + Given the driver PD and the DTB+driver_config we were initialised with, + create all regions, maps, and IRQs required. Creates a DeviceResources ConfigStruct + for the subsystem to use upon calling `create_config_structs`. + + This method will not run twice; it will return the existing ConfigStruct. I.e. + there is no risk of creating duplicate MRs, maps or IRQs from this method. + + Args: + driver_pd: ProtectionDomain + Returns: + ConfigStruct -> DeviceResources. + """ + # Cache DTB regions. Don't want to accidentally make these multiple times. + if hasattr(self, "__device_resources"): + return self.__device_resources + + if self.dtb is None: + print(f"sddf.py: no DTB! Creating dummy device resources.") + # x86 or otherwise no DTB! + print("sddf.py: no DTB! Assuming x86") + self.x86_resources() + # We generate an empty deviceresources despite it being useless, as our build system expects it. + self.__device_resources = DeviceResourcesFactory( + self.driver_magic, [], [], target_file=driver_pd.prog_image + ) + return self.__device_resources + + region_maps = ( + [] + ) # track fields to store in DeviceResources. tuples of vaddr, offset + for region in self.driver_config.regions: + mr = None + # We name regions as [region_name]_[node_path] to avoid collisions with + # duplicate driver classes on different nodes + region_name = region.name + "_" + self.dtb_node.path + + # First: create or find matching MR + if region.dt_idx is not None: + # First: find reg property + regs = self.dtb.get_node_regs(self.dtb_node) + r_addr, r_sz = regs[region.dt_idx] + r_sz = self.sdf.arch.roundup_to_page(r_sz) + + # Check we can turn this into a region + if region.size is not None: + if r_sz < region.size: + raise RuntimeError() # todo + + if (region.size & (self.sdf.arch.default_page_size() - 1)) != 0: + raise RuntimeError( + f"Region {region} with size={region.size} is not aligned to" + f"system page size!" + ) + mr_sz = region.size if region.size is not None else r_sz + d_paddr = self.dtb.get_reg_paddr(self.sdf.arch, self.dtb_node, r_addr) + d_reg_offset = r_addr % self.sdf.arch.default_page_size() + + # Check if this page is shared (i.e. a matching region is already existing). + # If regions overlap but don't have the same start, we do nothing and let microkit + # reject this. Should we reject here? TODO + existing_mr = [mr for mr in self.sdf.mrs if mr.paddr == d_paddr] + if len(existing_mr) == 1: + mr = existing_mr[0] + elif len(existing_mr) > 1: + raise RuntimeError( + f"Multiple MRs with paddr={d_paddr}! -> {existing_mr}" + ) + else: + # This is new (or overlapping with a different start) + mr = MemoryRegion( + self.sdf, region_name, mr_sz, paddr=d_paddr, cached=False + ) + else: + # This is a MR that doesn't correspond to physical memory + # mr = MemoryRegion(region_name, region.size) + # d_reg_offset = 0 + + # If you've run into this, open an issue. Old sdfgen supports this but it seems like a bug. + raise NotImplementedError("Config region doesn't correspond to DTB!") + + # Second: set up map + # Assumes permission string is correctly formatted. Non r/w/x chars are ignored + d_map = driver_pd.create_automap(mr, region.perms if region.perms else "rw") + region_maps.append((d_map, d_reg_offset)) + + # Next: set up IRQs + irqs_from_prop = self.dtb.get_parsed_irqs(self.dtb_node, self.sdf.arch) + if len(irqs_from_prop) == 0 and (t := len(self.driver_config.irqs)) != 0: + raise RuntimeError( + f"Driver config expects {t} irqs but none found in node!" + ) + + irq_ids = [] + for irq in self.driver_config.irqs: + dt_irq = irqs_from_prop[irq.dt_index] + irq_ids.append(driver_pd.add_irq(dt_irq)) + + # Finally: make config struct + self.__device_resources = DeviceResourcesFactory( + self.driver_magic, region_maps, irq_ids, target_file=driver_pd.prog_image + ) + return self.__device_resources + + def x86_resources(self): + """ + Create any resources needed if running on an x86 platform. Automatically + called in the event that no DTB is present. Subclasses should override this + method to do whatever they might need. + + By default nothing will happen. + """ + ... + + +def RegionResourceFactory(map: Map, section_name: Optional[str] = None, offset=0): + fields = {"vaddr": map.vaddr + offset, "size": map.mr.size} + return ConfigStruct("region_resource_t", section_name=section_name, fields=fields) + + +def DeviceRegionResourceFactory(region: ConfigStruct, io_addr: int): + fields = {"region": region, "io_addr": io_addr} + return ConfigStruct("device_region_resource_t", fields=fields) + + +def DeviceIRQResourceFactory(id: int): + fields = {"id": id} + return ConfigStruct("device_irq_resource_t", fields=fields) + + +def DeviceResourcesFactory( + magic_str: str, + maps_offsets: List[Tuple[Map, int]], + irq_ids: List[int], + target_file: str, + section_name="device_resources", +): + region_structs = [ + DeviceRegionResourceFactory(RegionResourceFactory(m, offset=o), m.mr.paddr) + for m, o in maps_offsets + ] + irq_structs = [DeviceIRQResourceFactory(i) for i in irq_ids] + fields = { + "magic": magic_str, + "num_regions": len(region_structs), + "num_irqs": len(irq_structs), + "regions": region_structs, + "irqs": irq_structs, + } + return ConfigStruct( + "device_resources_t", + section_name=section_name, + fields=fields, + target_file=target_file, + ) diff --git a/acacia_sddf/serial.py b/acacia_sddf/serial.py new file mode 100644 index 000000000..d63826717 --- /dev/null +++ b/acacia_sddf/serial.py @@ -0,0 +1,537 @@ +# Copyright 2026, UNSW +# SPDX-License-Identifier: BSD-2-Clause + +from acacia import ( + System, + Subsystem, + ProtectionDomain, + Channel, + Map, + MemoryRegion, + DTBNode, + DeviceTreeBlob, + SchedulingProperties, + ConfigStruct, + SubsystemBuildError, +) +from acacia.x86 import IOPort +from acacia.irq import IrqIoapic +import sys, os +from .driver_manifest import sDDFDriverManifest, sDDFDriverConfig, DTSIRQ, DTSRegion +from .sddf import sDDFDriverClass, DeviceResourcesFactory, RegionResourceFactory +from collections import defaultdict +from typing import List, Dict, Type, Union, Optional + +SERIAL_DEFAULT_BEGIN_STR = "Begin input\r\n" +SERIAL_MAX_BEGIN_STR_LEN = 128 +SERIAL_PROTOCOL_MAGIC = "sDDF" + chr(0x3) + + +class sDDFSerial(sDDFDriverClass): + def __init__( + self, + sdf: System, + dev_compatible: str, + dev_dt_path: str, + driver_prio: int, + virt_tx_prio: int, + allow_rx: bool = False, + virt_rx_prio: Optional[int] = None, + cpu: Optional[int] = None, + enable_color: bool = True, + baud_rate: int = 115200, + begin_str: str = SERIAL_DEFAULT_BEGIN_STR, + # We leave this as configurable just in case... + data_size: int = 0x10000, + queue_size: int = 0x1000, + virt_rx_elf: str = "serial_virt_rx.elf", + virt_tx_elf: str = "serial_virt_tx.elf", + driver_elf: str = "serial_driver.elf", + ): + super().__init__( + sdf, "serial", dev_compatible, dev_dt_path, magic="sDDF" + chr(0x1) + ) + assert driver_prio > virt_tx_prio > 0 + if allow_rx: + # Default RX prio == TX prio + if virt_rx_prio is None: + virt_rx_prio = virt_tx_prio + assert driver_prio > virt_rx_prio > 0 + + self.cpu = cpu + self.allow_rx = allow_rx + self.data_size = data_size + self.queue_size = queue_size + self.enable_color = enable_color + self.baud_rate = baud_rate + if len(begin_str) > SERIAL_MAX_BEGIN_STR_LEN: + raise SubsystemBuildError( + f"begin_str length {len(begin_str)} exceeds max {SERIAL_MAX_BEGIN_STR_LEN}" + ) + self.begin_str = begin_str + + self.virt_tx = None + self.virt_rx = None + self.virt_rx_elf = virt_rx_elf + self.virt_tx_elf = virt_tx_elf + self.driver = ProtectionDomain( + self.sdf, + "serial_driver", + driver_elf, + scheduling=SchedulingProperties(driver_prio), + cpu=self.cpu, + ) + + # We must make the driver BEFORE we get here + self.driver_dev_resources = self.create_dtb_resources(self.driver) + + # Stubs of config structs that we need to collect in construct_infrastructure and connect_clients + self.virt_tx_config = None + self.virt_rx_config = None + self.driver_config = None + self.virt_tx_driver_conn = None + self.virt_rx_driver_conn = None + self.client_configs = [] + self.construct_infrastructure( + virt_rx_prio if virt_rx_prio else -1, virt_tx_prio + ) + + def construct_infrastructure(self, virt_rx_prio: int, virt_tx_prio: int): + self.virt_tx = ProtectionDomain( + self.sdf, + "serial_virt_tx", + self.virt_tx_elf, + scheduling=SchedulingProperties(virt_tx_prio), + cpu=self.cpu, + ) + if self.allow_rx and virt_rx_prio > 0: + self.virt_rx = ProtectionDomain( + self.sdf, + "serial_virt_rx", + self.virt_rx_elf, + scheduling=SchedulingProperties(virt_rx_prio), + cpu=self.cpu, + ) + + driver_tx_queue_mr = MemoryRegion( + self.sdf, "serial_driver_tx_queue", self.queue_size + ) + driver_tx_data_mr = MemoryRegion( + self.sdf, + "serial_driver_tx_data", + self.data_size * 2 if self.enable_color else self.data_size, + cached=True, + ) + + driver_tx_queue_map = self.driver.create_automap( + driver_tx_queue_mr, Map.Permissions(r=True, w=True) + ) + driver_tx_data_map = self.driver.create_automap( + driver_tx_data_mr, Map.Permissions(r=True, w=True) + ) + virt_tx_queue_map = self.virt_tx.create_automap( + driver_tx_queue_mr, Map.Permissions(r=True, w=True) + ) + virt_tx_data_map = self.virt_tx.create_automap( + driver_tx_data_mr, Map.Permissions(r=True, w=True) + ) + + driver_virt_tx_ch = Channel( + self.sdf, + Channel.End(self.driver, can_notify=True, can_pp=False), + Channel.End(self.virt_tx, can_notify=True, can_pp=False), + ) + + driver_tx_conn = self.serial_connection_resource_factory( + driver_tx_queue_map, + driver_tx_data_map, + driver_virt_tx_ch.id_for_pd(self.driver), + ) + self.virt_tx_driver_conn = self.serial_connection_resource_factory( + virt_tx_queue_map, + virt_tx_data_map, + driver_virt_tx_ch.id_for_pd(self.virt_tx), + ) + + driver_rx_conn = None + if self.virt_rx: + driver_rx_queue_mr = MemoryRegion( + self.sdf, "serial_driver_rx_queue", self.queue_size + ) + driver_rx_data_mr = MemoryRegion( + self.sdf, "serial_driver_rx_data", self.data_size + ) + + driver_rx_queue_map = self.driver.create_automap( + driver_rx_queue_mr, Map.Permissions(r=True, w=True) + ) + driver_rx_data_map = self.driver.create_automap( + driver_rx_data_mr, Map.Permissions(r=True, w=True) + ) + virt_rx_queue_map = self.virt_rx.create_automap( + driver_rx_queue_mr, Map.Permissions(r=True, w=True) + ) + virt_rx_data_map = self.virt_rx.create_automap( + driver_rx_data_mr, Map.Permissions(r=True, w=True) + ) + + driver_virt_rx_ch = Channel( + self.sdf, + Channel.End(self.driver, can_notify=True, can_pp=False), + Channel.End(self.virt_rx, can_notify=True, can_pp=False), + ) + + driver_rx_conn = self.serial_connection_resource_factory( + driver_rx_queue_map, + driver_rx_data_map, + driver_virt_rx_ch.id_for_pd(self.driver), + ) + self.virt_rx_driver_conn = self.serial_connection_resource_factory( + virt_rx_queue_map, + virt_rx_data_map, + driver_virt_rx_ch.id_for_pd(self.virt_rx), + ) + + self.driver_config = self.serial_driver_config_factory( + self.driver, + SERIAL_PROTOCOL_MAGIC, + self.baud_rate, + 1 if self.virt_rx else 0, + driver_tx_conn, + driver_rx_conn, + ) + + def connect_clients(self): + assert self.virt_tx is not None + assert self.driver is not None + + virt_tx_client_structs = [] + virt_rx_client_conns = [] + client_configs = [] + + for c in self.clients: + if c.priority >= self.virt_tx.priority: + raise SubsystemBuildError( + f"Client {c} has a priority higher than virt_tx's " + f"({self.virt_tx.priority})!" + ) + if self.virt_rx and c.priority >= self.virt_rx.priority: + raise SubsystemBuildError( + f"Client {c} has a priority higher than virt_rx's " + f"({self.virt_rx.priority})!" + ) + + # TX connection: virt_tx -> client + tx_queue_mr = MemoryRegion( + self.sdf, f"serial_tx_queue_{c.name}", self.queue_size + ) + tx_data_mr = MemoryRegion( + self.sdf, f"serial_tx_data_{c.name}", self.data_size + ) + + virt_tx_tx_queue_map = self.virt_tx.create_automap( + tx_queue_mr, Map.Permissions(r=True, w=True) + ) + virt_tx_tx_data_map = self.virt_tx.create_automap( + tx_data_mr, Map.Permissions(r=True, w=True) + ) + c_tx_queue_map = c.create_automap( + tx_queue_mr, Map.Permissions(r=True, w=True) + ) + c_tx_data_map = c.create_automap( + tx_data_mr, Map.Permissions(r=True, w=True) + ) + + tx_ch = Channel( + self.sdf, + Channel.End(self.virt_tx, can_notify=True, can_pp=False), + Channel.End(c, can_notify=True, can_pp=False), + ) + + virt_tx_conn = self.serial_connection_resource_factory( + virt_tx_tx_queue_map, virt_tx_tx_data_map, tx_ch.id_for_pd(self.virt_tx) + ) + client_tx_conn = self.serial_connection_resource_factory( + c_tx_queue_map, c_tx_data_map, tx_ch.id_for_pd(c) + ) + + virt_tx_client_structs.append( + self.serial_virt_tx_client_config_factory(c.name, virt_tx_conn) + ) + + # RX connection (if enabled): virt_rx -> client + client_rx_conn = None + if self.virt_rx: + rx_queue_mr = MemoryRegion( + self.sdf, f"serial_rx_queue_{c.name}", self.queue_size + ) + rx_data_mr = MemoryRegion( + self.sdf, f"serial_rx_data_{c.name}", self.data_size + ) + + virt_rx_rx_queue_map = self.virt_rx.create_automap( + rx_queue_mr, Map.Permissions(r=True, w=True) + ) + virt_rx_rx_data_map = self.virt_rx.create_automap( + rx_data_mr, Map.Permissions(r=True, w=True) + ) + c_rx_queue_map = c.create_automap( + rx_queue_mr, Map.Permissions(r=True, w=True) + ) + c_rx_data_map = c.create_automap( + rx_data_mr, Map.Permissions(r=True, w=True) + ) + + rx_ch = Channel( + self.sdf, + Channel.End(self.virt_rx, can_notify=True, can_pp=False), + Channel.End(c, can_notify=True, can_pp=False), + ) + + virt_rx_conn = self.serial_connection_resource_factory( + virt_rx_rx_queue_map, + virt_rx_rx_data_map, + rx_ch.id_for_pd(self.virt_rx), + ) + client_rx_conn = self.serial_connection_resource_factory( + c_rx_queue_map, c_rx_data_map, rx_ch.id_for_pd(c) + ) + virt_rx_client_conns.append(virt_rx_conn) + + client_configs.append( + self.serial_client_config_factory( + c, SERIAL_PROTOCOL_MAGIC, client_tx_conn, client_rx_conn + ) + ) + + self.virt_tx_config = self.serial_virt_tx_config_factory( + self.virt_tx, + SERIAL_PROTOCOL_MAGIC, + len(self.clients), + self.virt_tx_driver_conn, + virt_tx_client_structs, + 1 if self.enable_color else 0, + 1 if self.virt_rx else 0, + self.begin_str, + ) + + if self.virt_rx: + self.virt_rx_config = self.serial_virt_rx_config_factory( + self.virt_rx, + SERIAL_PROTOCOL_MAGIC, + len(self.clients), + self.virt_rx_driver_conn, + virt_rx_client_conns, + ) + + self.client_configs = client_configs + + def x86_resources(self): + self.add_x86_serial_port() + + def generate_config_structs(self): + # We've already made our structs, just return them as a list for the serialiser + driver_resources = [self.driver_dev_resources, self.driver_config] + virt_resources = [] + if self.virt_tx_config: + virt_resources.append(self.virt_tx_config) + if self.virt_rx_config: + virt_resources.append(self.virt_rx_config) + return driver_resources + virt_resources + self.client_configs + + # ### connection config struct factory functions ### + + def serial_connection_resource_factory( + self, queue_map: Map, data_map: Map, ch_id: int + ) -> ConfigStruct: + fields = { + "queue": RegionResourceFactory(queue_map), + "data": RegionResourceFactory(data_map), + "id": ch_id, + } + return ConfigStruct("serial_connection_resource_t", fields=fields) + + def serial_driver_config_factory( + self, + driver_pd: ProtectionDomain, + magic: str, + baud_rate: int, + rx_enabled: int, + tx_connection: ConfigStruct, + rx_connection: Optional[ConfigStruct] = None, + ) -> ConfigStruct: + fields = { + "magic": magic, + "default_baud": baud_rate, + "rx_enabled": rx_enabled, + "tx": tx_connection, + "rx": rx_connection if rx_connection else 0, + } + return ConfigStruct( + "serial_driver_config_t", + target_file=driver_pd.prog_image, + section_name="serial_driver_config", + fields=fields, + ) + + def serial_virt_rx_config_factory( + self, + virt_rx_pd: ProtectionDomain, + magic: str, + num_clients: int, + driver_connection: ConfigStruct, + client_connections: List[ConfigStruct], + ) -> ConfigStruct: + fields = { + "magic": magic, + "num_clients": num_clients, + "driver": driver_connection, + "clients": client_connections, + "switch_char": chr(28), + "terminate_num_char": "\r", + } + return ConfigStruct( + "serial_virt_rx_config_t", + target_file=virt_rx_pd.prog_image, + section_name="serial_virt_rx_config", + fields=fields, + ) + + def serial_virt_tx_client_config_factory( + self, name: str, conn: ConfigStruct + ) -> ConfigStruct: + fields = { + "conn": conn, + "name": name, + } + return ConfigStruct("serial_virt_tx_client_t", fields=fields) + + def serial_virt_tx_config_factory( + self, + virt_tx_pd: ProtectionDomain, + magic: str, + num_clients: int, + driver_connection: ConfigStruct, + client_connections: List[ConfigStruct], + enable_colour: int, + enable_rx: int, + begin_str: str, + ) -> ConfigStruct: + fields = { + "magic": magic, + "driver": driver_connection, + "clients": client_connections, + "num_clients": num_clients, + "begin_str": begin_str, + "enable_colour": enable_colour, + "enable_rx": enable_rx, + } + return ConfigStruct( + "serial_virt_tx_config_t", + target_file=virt_tx_pd.prog_image, + section_name="serial_virt_tx_config", + fields=fields, + ) + + def serial_client_config_factory( + self, + client_pd: ProtectionDomain, + magic, + tx_connection: ConfigStruct, + rx_connection: Optional[ConfigStruct] = None, + ) -> ConfigStruct: + fields = { + "magic": magic, + "tx": tx_connection, + "rx": rx_connection if rx_connection else 0, + } + return ConfigStruct( + "serial_client_config_t", + target_file=client_pd.prog_image, + section_name="serial_client_config", + fields=fields, + ) + + # x86 Util + def add_x86_serial_port(self): + # The serial device does not located on PCIe and the interrupts are + # conventionally configured by BIOS. The IRQ number can be read from + # Linux or APCI tables. + self.driver.add_ioport(IOPort(0x3F8, 8, 0)) + self.driver.add_irq(IrqIoapic(0, 4, 0, id=1)) + + +# Driver configs +serial_driver_configs: Dict[str, List[sDDFDriverConfig]] = defaultdict(list) + + +def add_driver_config(driver_name: str, config: sDDFDriverConfig): + sDDFDriverManifest().add_driver_config(sDDFSerial, driver_name, config) + + +add_driver_config( + "meson", + sDDFDriverConfig( + ["amlogic,meson-gx-uart", "amlogic,meson-ao-uart"], + [DTSRegion("regs", "rw", 4096, 0)], + [DTSIRQ(0)], + ), +) + +add_driver_config( + "pl011", + sDDFDriverConfig( + compatible="arm,pl011", + regions=[DTSRegion("regs", "rw", 4096, 0)], + irqs=[DTSIRQ(0)], + ), +) + +add_driver_config( + "imx", + sDDFDriverConfig( + compatible=["fsl,imx8mq-uart", "fsl,imx8mm-uart", "fsl,imx8mp-uart"], + regions=[DTSRegion("regs", "rw", 4096, 0)], + irqs=[DTSIRQ(0)], + ), +) + +# ns16550a +add_driver_config( + "ns16550a", + sDDFDriverConfig( + compatible=[ + "starfive,jh7110-uart", + "ns16550a", + "brcm,bcm2835-aux-uart", + "snps,dw-apb-uart", + ], + regions=[DTSRegion("regs", "rw", 4096, 0)], + irqs=[DTSIRQ(0)], + ), +) + +# virtio +add_driver_config( + "virtio", + sDDFDriverConfig( + compatible="virtio,mmio", + regions=[ + DTSRegion("regs", "rw", 4096, 0), + DTSRegion("hw_ring_buffer", size=65536), + DTSRegion("virtio_rx_buf", size=4096), + DTSRegion("virtio_tx_buf", size=4096), + ], + irqs=[DTSIRQ(0)], + ), +) + +# xlnx +add_driver_config( + "xlnx", + sDDFDriverConfig( + compatible="xlnx,zynqmp-uart", + regions=[DTSRegion("regs", dt_idx=0)], + irqs=[DTSIRQ(0)], + ), +) diff --git a/acacia_sddf/timer.py b/acacia_sddf/timer.py new file mode 100644 index 000000000..f4c381bb6 --- /dev/null +++ b/acacia_sddf/timer.py @@ -0,0 +1,198 @@ +# Copyright 2026, UNSW +# SPDX-License-Identifier: BSD-2-Clause + +from acacia import ( + System, + Subsystem, + ProtectionDomain, + Channel, + Map, + MemoryRegion, + DTBNode, + DeviceTreeBlob, + SchedulingProperties, + ConfigStruct, + IRQ, + SubsystemBuildError, +) +import sys, os +from .driver_manifest import sDDFDriverManifest, sDDFDriverConfig, DTSIRQ, DTSRegion +from .sddf import sDDFDriverClass, DeviceResourcesFactory, RegionResourceFactory +from collections import defaultdict +from typing import List, Dict, Type, Union, Optional + + +class sDDFTimer(sDDFDriverClass): + def __init__( + self, + sdf: System, + dev_compatible: str, + dev_dt_path: str, + driver_prio: int = 254, + cpu: Optional[int] = None, + driver_elf: str = "timer_driver.elf", + ): + super().__init__( + sdf, "timer", dev_compatible, dev_dt_path, magic="sDDF" + chr(1) + ) + self.driver = ProtectionDomain( + self.sdf, + "timer_driver", + driver_elf, + scheduling=SchedulingProperties(driver_prio, passive=True), + ) + self.cpu = cpu + + # Create driver resources before doing anything else + self.driver_dev_resources = self.create_dtb_resources(self.driver) + self.client_configs = [] + + def connect_clients(self): + # Clients are connected with: + # a. channel allowing PPCs -> driver, notifications -> clienet + # ... that's it! + for c in self.clients: + if c.priority > self.driver.priority: + raise SubsystemBuildError( + f"Client {c} has higher priority than timer driver!" + ) + ch = Channel( + self.sdf, + Channel.End(c, can_notify=False, can_pp=True), + Channel.End(self.driver, can_notify=True, can_pp=False), + ) + self.client_configs.append( + self.timer_client_config_factory(c, ch.id_for_pd(c)) + ) + + def x86_resources(self): + self.add_x86_hpet() + + def generate_config_structs(self): + # We've already made our structs + return [self.driver_dev_resources] + self.client_configs + + def timer_client_config_factory( + self, client_pd: ProtectionDomain, driver_id: int + ) -> ConfigStruct: + """ + create timer_client_config for client_pd with serial id n + """ + # invariant: this PD only is a client to timer one time. + fields = {"magic": "sDDF" + chr(6), "driver_id": driver_id} + return ConfigStruct( + "timer_client_config_t", + target_file=client_pd.prog_image, + section_name="timer_client_config", + fields=fields, + ) + + # x86 utility + # NOTE: is this safe to call automatically? I currently am assuming we want manual + # control over this since we didn't bake it into sdfgen before. + def add_x86_hpet(self): + # Timer IRQ must be the highest priority (highest vector) to ensure they are delivered + # as close as possible to the timer expiry. The highest vector is defined by (irq_user_max - irq_user_min) in seL4 source + # Since our HPET driver uses legacy IRQ routing, comparator 0's IRQ will always arrives at + # I/O APIC 0's pin 2. + from acacia.irq import IrqIoapic + + hpet_irq = IrqIoapic( + ioapic_id=0, pin=2, vector=107, id=0, trigger=IRQ.Trigger.EDGE + ) + self.driver.add_irq(hpet_irq) + # paddr=0xFED00000 is a x86 convention for HPET, though it may be different on some machines depending on their BIOS. + hpet_regs = MemoryRegion("hpet_regs", 0x1000, paddr=0xFED00000) + hpet_regs_map = Map(hpet_regs, 0x5000_0000, "rw") + self.driver.add_map(hpet_regs_map) + sdf.add_memory_region(hpet_regs) + + +# Driver configs +def add_driver_config(driver_name: str, config: sDDFDriverConfig): + sDDFDriverManifest().add_driver_config(sDDFTimer, driver_name, config) + + +# pulp +add_driver_config( + "apb_timer", + sDDFDriverConfig( + compatible="pulp,apb_timer", + regions=[DTSRegion("regs", "rw", 4096, 0)], + irqs=[DTSIRQ(0), DTSIRQ(1), DTSIRQ(2), DTSIRQ(3)], + ), +) +# armv8 +add_driver_config( + "arm", sDDFDriverConfig(compatible="arm,armv8-timer", regions=[], irqs=[DTSIRQ(1)]) +) + +# bcm2835 +add_driver_config( + "bcm2835", + sDDFDriverConfig( + compatible="brcm,bcm2835-system-timer", + regions=[DTSRegion("regs", dt_idx=0)], + irqs=[DTSIRQ(1)], + ), +) + +# cdns +add_driver_config( + "cdns", + sDDFDriverConfig( + compatible="cdns,ttc", + regions=[DTSRegion("regs", dt_idx=0)], + irqs=[DTSIRQ(0), DTSIRQ(1)], + ), +) + +# goldfish +add_driver_config( + "goldfish", + sDDFDriverConfig( + compatible="google,goldfish-rtc", + regions=[DTSRegion("regs", dt_idx=0)], + irqs=[DTSIRQ(0)], + ), +) + +# imx8 +add_driver_config( + "imx", + sDDFDriverConfig( + compatible=["fsl,imx8mm-gpt", "fsl,imx8mq-gpt", "fsl,imx8mp-gpt"], + regions=[DTSRegion("regs", "rw", 65536, 0)], + irqs=[DTSIRQ(0)], + ), +) + +# jh7110 +add_driver_config( + "jh7110", + sDDFDriverConfig( + compatible="starfive,jh7110-timer", + regions=[DTSRegion("regs", "rw", 4096, 0)], + irqs=[DTSIRQ(0), DTSIRQ(1)], + ), +) + +# meson_gxbb +add_driver_config( + "meson", + sDDFDriverConfig( + compatible="amlogic,meson-gxbb-wdt", + regions=[DTSRegion("regs", "rw", 4096, 0)], + irqs=[DTSIRQ(0)], + ), +) + +# rk3568 +add_driver_config( + "rk3568", + sDDFDriverConfig( + compatible="rockchip,rk3568-timer", + regions=[DTSRegion("regs", dt_idx=0)], + irqs=[DTSIRQ(0), DTSIRQ(1)], + ), +) diff --git a/ci/matrix.py b/ci/matrix.py index 7bd443ca3..d9263c95f 100644 --- a/ci/matrix.py +++ b/ci/matrix.py @@ -82,7 +82,7 @@ def listify(s: str | Sequence[str]) -> Sequence[str]: "i2c_bus_scan": { "configs": ["debug", "release"], "build_systems": ["make"], - "boards": ["serengeti"], + "boards": ["serengeti", "maaxboard"], "tests_exclude": [], }, "ina219": { diff --git a/docs/design/design.tex b/docs/design/design.tex index eadbc14d7..e9328792d 100644 --- a/docs/design/design.tex +++ b/docs/design/design.tex @@ -2643,12 +2643,73 @@ \subsection{PWM}\label{s:pwm} For the Pulse-Width-Modulation (PWM) class there is a single call that sets period and duty cycle. - \subsection{Status} These device-class specifications are \textbf{subject to change}; code implementing them has not yet been merged. + +\section{Power, thermal and clock devices}\label{s:sensors} + +This section describes drivers which are used for system management. All of +these drivers use a simple, low-throughput interface. + + +\subsection{PMIC}\label{s:pmic} + +Many SoCs have an external \emph{Power Management IC} +(PMIC) controlled via I2C. This is a chip that is responsible for feeding in +several power lines to the chip and controlling their output with an array of +regulators. + +The PMIC +contains 15 programmable regulators, each generating a voltage rail for +various board functions. +Each regulator has different: +\begin{enumerate} + \item voltage output ranges (different minima and maxima), + \item ability to set a current limit, with different maxima and minima, + \item ability to be turned on or off, and + \item step sizes for configuring voltages and currents. +\end{enumerate} + +The PMIC driver must be able to deal with manipulating these heterogeneous +regulators programmatically because clients may generate requests +that are not possible to implement, or that conflict with the +requirements for other clients. + +The core of the PMIC driver is a table of regulator descriptions, parameterised +by regulator IDs that are stored in a shared PMIC bindings header file for the +specific PMIC. The bindings header file is available to clients of the PMIC +driver as a way to identify specific regulators. The regulator description +table encodes all capabilities of every register such that our driver can +return descriptive errors for incorrect requests and avoid unnecessary \gls{i2c} +operations. + +The PMIC protocol provides a \emph{Protected Procedure Call} (PPC) interface to +clients, with the following methods available: +\begin{enumerate} +\item Enable or disable a target regulator, +\item Set the output voltage of a regulator, +\item Set the current limit of a regulator, +\item Get info about the state of a regulator. +\end{enumerate} + +In the current version, we have implemented only the ability to +set the output voltage for now. + +Upon receipt of a PPC from a client, the driver will validate that any +arguments supplied are valid before attempting an \gls{i2c} transaction. If the +arguments supplied are not within the bounds of the regulator description +table, the driver returns an error to the client. Otherwise, it will +generate an \gls{i2c} request to read or write appropriate state to the PMIC. + +Unlike most other classes, the PMIC driver is itself a client to the \gls{i2c} +driver class. +The PMIC driver issues requests to the \gls{i2c} virtualiser via a queue +in a shared +memory interface, abstracted using \emph{libi2c}. + \chapter{Hotplugging}\label{s:hotplugging} \section{Overview} diff --git a/docs/drivers.md b/docs/drivers.md index 41aeb44f7..a62818134 100644 --- a/docs/drivers.md +++ b/docs/drivers.md @@ -87,3 +87,8 @@ Device Tree compatible strings/platforms it is known to work with. * `brcm,bcm2835-system-timer` * x86-64 TSC & HPET: * `tsc_hpet` + +## PMIC + +* Rohm BD71837 (iMX boards) + * `rohm,bd71837` diff --git a/docs/pmic/pmic.md b/docs/pmic/pmic.md new file mode 100644 index 000000000..a72f38c45 --- /dev/null +++ b/docs/pmic/pmic.md @@ -0,0 +1,59 @@ + +# sDDF PMIC Subsystem + +The sDDF has support for I²C-based PMIC devices, using the sDDF I²C driver as +supporting infrastructure. + +## Power Management ICs + +Many SoCs have an external **Power Management IC** +(PMIC) controlled via I2C. This is a chip that is responsible for feeding in +several power lines to the chip and controlling their output with an array of +regulators. + +## PMIC driver class + +**CURRENT STATUS**: the PMIC class is a WIP and only supports controlling the output voltage +of regulators in the `bd71837` driver. The protocol supports all operations, but they are +not implemented there as of the time of writing. This is sufficient for implementing +basic system management features like DVFS (dynamic voltage and frequency scaling). + +A PMIC contains many programmable regulators, each generating a voltage rail for +various board functions. +Each regulator has different: + - voltage output ranges (different minima and maxima), + - ability to set a current limit, with different maxima and minima, + - ability to be turned on or off, and + - step sizes for configuring voltages and currents. + +The PMIC driver must be able to deal with manipulating these heterogeneous +regulators programmatically because clients may generate requests +that are not possible to implement, or that conflict with the +requirements for other clients. + +The core of the PMIC driver is a table of regulator descriptions, parameterised +by regulator IDs that are stored in a shared PMIC bindings header file for the +specific PMIC. The bindings header file is available to clients of the PMIC +driver as a way to identify specific regulators. The regulator description +table encodes all capabilities of every register such that our driver can +return descriptive errors for incorrect requests and avoid unnecessary I²C +operations. + +The PMIC protocol provides a \emph{Protected Procedure Call} (PPC) interface to +clients, with the following methods available: +- Enable or disable a target regulator, +- Set the output voltage of a regulator, +- Set the current limit of a regulator, +- Get info about the state of a regulator. + +Upon receipt of a PPC from a client, the driver will validate that any +arguments supplied are valid before attempting an I²C transaction. If the +arguments supplied are not within the bounds of the regulator description +table, the driver returns an error to the client. Otherwise, it will +generate an I²C request to read or write appropriate state to the PMIC. + + diff --git a/drivers/i2c/imx/config.json b/drivers/i2c/imx/config.json new file mode 100644 index 000000000..cb4f54493 --- /dev/null +++ b/drivers/i2c/imx/config.json @@ -0,0 +1,21 @@ +{ + "compatible": [ + "fsl,imx8mq-i2c", + "fsl,imx21-i2c" + ], + "resources": { + "regions": [ + { + "name": "regs", + "perms": "rw", + "size": 4096, + "dt_index": 0 + } + ], + "irqs": [ + { + "dt_index": 0 + } + ] + } +} diff --git a/drivers/i2c/imx/driver.h b/drivers/i2c/imx/driver.h new file mode 100644 index 000000000..e0811c928 --- /dev/null +++ b/drivers/i2c/imx/driver.h @@ -0,0 +1,42 @@ +/* + * Copyright 2026, UNSW + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include + +// Base: 0x30A20000 for I2C1 +struct imx_i2c_regs { + uint16_t iadr; // 0x00 - I2C Address Register + uint16_t _pad0; + uint16_t ifdr; // 0x04 - I2C Frequency Divider Register + uint16_t _pad1; + uint16_t i2cr; // 0x08 - I2C Control Register + uint16_t _pad2; + uint16_t i2sr; // 0x0C - I2C Status Register + uint16_t _pad3; + uint16_t i2dr; // 0x10 - I2C Data I/O Register + uint16_t _pad4; +}; + +// I2CR Control Register bits +#define REG_CR_IEN (1 << 7) // I2C Enable +#define REG_CR_IIEN (1 << 6) // I2C Interrupt Enable +#define REG_CR_MSTA (1 << 5) // Master/Slave mode (1=Master, generates START when 0->1) +#define REG_CR_MTX (1 << 4) // Transmit/Receive (1=Transmit, 0=Receive) +#define REG_CR_TXAK (1 << 3) // Transmit Acknowledge (1=No ACK sent) +#define REG_CR_RSTA (1 << 2) // Repeat Start (1=Generate repeated START) + +// I2SR Status Register bits +#define REG_SR_ICF (1 << 7) // Transfer Complete (set at 9th clock falling edge) +#define REG_SR_IAAS (1 << 6) // Addressed As Slave +#define REG_SR_IBB (1 << 5) // Bus Busy (1=Busy, set by START, cleared by STOP) +#define REG_SR_IAL (1 << 4) // Arbitration Lost +#define REG_SR_SRW (1 << 2) // Slave Read/Write (1=Slave transmit) +#define REG_SR_IIF (1 << 1) // I2C Interrupt (set when byte transfer complete) +#define REG_SR_RXAK (1 << 0) // Received Acknowledge (1=No ACK received, 0=ACK received) + diff --git a/drivers/i2c/imx/i2c.c b/drivers/i2c/imx/i2c.c new file mode 100644 index 000000000..5581c3b39 --- /dev/null +++ b/drivers/i2c/imx/i2c.c @@ -0,0 +1,404 @@ +/* + * Copyright 2026, UNSW + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include "driver.h" + + +__attribute__((__section__(".i2c_driver_config"))) i2c_driver_config_t config; +__attribute__((__section__(".device_resources"))) device_resources_t device_resources; + +// NOTE: i2c registers on the imx8mq are 16 bit! + +// Base addresses: I2C1=0x30A20000, I2C2=0x30A30000, I2C3=0x30A40000, I2C4=0x30A50000 +volatile struct imx_i2c_regs *regs; + +i2c_driver_data_t driver_data; +fsm_data_t fsm_data = { 0 }; +i2c_queue_handle_t queue_handle; + +bool dummy_read = false; + +i2c_state_func_t *i2c_state_table[NUM_STATES] = { + state_idle, state_req, state_sel_cmd, state_cmd, state_cmd_ret, state_resp +}; + +/** + * Initialise the i2c master interface for 400KHz fast mode. + */ +static inline void i2c_setup(void) +{ + LOG_I2C_DRIVER("initialising i2c master interface...\n"); + + // disable initially + regs->i2cr = 0; + LOG_I2C_DRIVER("a\n"); + + // clear interrupt flag + regs->i2sr &= ~REG_SR_IIF; + LOG_I2C_DRIVER("b\n"); + + // Set frequency divider for 400 KHz (IC=0x06, divider=60 for 24MHz clock) + regs->ifdr = 0x06; + LOG_I2C_DRIVER("c\n"); + + // enable, and turn on interrupts + regs->i2cr = REG_CR_IEN | REG_CR_IIEN; + LOG_I2C_DRIVER("done\n"); +} + +/** + * Aborts the current operation by generating a STOP condition and disabling master mode. + */ +int i2c_halt(void) +{ + LOG_I2C_DRIVER("I2C HALT\n"); + + // Clear MSTA bit to generate STOP + regs->i2cr &= ~(REG_CR_MSTA | REG_CR_MTX); + + // busy wait for ibb to clear ... + int timeout = 10000; + while ((regs->i2sr & REG_SR_IBB) && timeout-- > 0) {} + + if (timeout <= 0) { + LOG_I2C_DRIVER_ERR("failed to halt - bus busy timeout\n"); + return -1; + } + + // Reset to idle state (keep IEN and IIEN enabled) + regs->i2cr = REG_CR_IEN | REG_CR_IIEN; + + return 0; +} + +void init(void) +{ + assert(i2c_config_check_magic(&config)); + assert(device_resources_check_magic(&device_resources)); + assert(device_resources.num_irqs == 1 || device_resources.num_irqs == 2); + assert(device_resources.num_regions == 1); + + regs = (volatile struct imx_i2c_regs *)device_resources.regions[0].region.vaddr; + i2c_setup(); + + i2c_reset_state(&driver_data); + queue_handle = i2c_queue_init(config.virt.req_queue.vaddr, config.virt.resp_queue.vaddr); + + LOG_I2C_DRIVER("i.MX8M I2C driver initialised.\n"); +} + +/** + * Send a start condition. This method should short circuit other writes to the CR register. + */ +static inline void imx_i2c_start(bool repeat) { + // Update dummy read flag upon switch to MTX + dummy_read = false; + if (!repeat) { + regs->i2cr |= REG_CR_MSTA; + // wait until bus is clear + for (uint32_t i = 0; (i < 10000) && (regs->i2sr & REG_SR_IBB); i++) { + } + + // if our short busy wait failed, die + if (regs->i2sr & REG_SR_IBB) { + LOG_I2C_DRIVER_ERR("Failed to claim bus on start! Is there another master?\n"); + assert(0); + } + + // write other start flags + regs->i2cr |= REG_CR_MTX | REG_CR_IIEN; + } else { + regs->i2cr |= REG_CR_RSTA; + } + + // Always enable TXAK at start of a transaction. + regs->i2cr &= ~REG_CR_TXAK; +} + +/** + * After sending an address+start OR a data byte, call this function + * to check if an acknowledgement is received. + * + * This is required as the hardware will only generate an interrupt on + * the successful completion of a transfer. + */ +static inline bool imx_i2c_ackd(void) { + // TODO: figure out if a busy wait is required + return ((regs->i2sr & REG_SR_RXAK) == 0); +} + +/** + * S_CMD (command) + * Initiate a single bus operation (START, data byte, or STOP) then yield to await IRQ completion. + * The imx8mq I2C controller operates byte-by-byte, not as a list processor. + */ +void state_cmd(fsm_data_t *fsm, i2c_driver_data_t *data, i2c_queue_handle_t *queue_handle) +{ + LOG_I2C_DRIVER("S_CMD: rw_idx=%u, len=%u, await_start=%u, await_stop=%u\n", + data->rw_idx, data->active_cmd.data_len, data->await_start, data->await_stop); + + // Sanity: if IAL (arbitration lost) is ever asserted, die. This should never happen for now, + // as our protocol currently doesn't support arbitration. + if (regs->i2sr & REG_SR_IAL) { + LOG_I2C_DRIVER_ERR("Arbitration lost! This should never happen as we do not support multi master!\n"); + LOG_I2C_DRIVER_ERR("Dying now.\n"); + // clear IAL + regs->i2sr &= ~REG_SR_IAL; + data->err = I2C_ERR_OTHER; + fsm->next_state = S_RESPONSE; + return; + } + + // invariant: these tmp registers are unused if we imx_i2c_start() as the start + // op needs to directly touch the registers. + uint32_t i2cr_tmp = regs->i2cr; + uint32_t i2dr_tmp = regs->i2dr; + bool do_i2dr_write = false; + bool do_i2cr_write = false; + if (data->await_start) { + LOG_I2C_DRIVER("Sending start condition...\n"); + // This is a repeated start if await_addr is not set. + bool is_repeat = (data->await_addr == 0); + + // Sanity: if a write-read is pending, this repeated start is invalid. + // The virt should handle this, but on the imx this condition is completely + // unhandleable so we check again. + assert(!(is_repeat && data->await_wrrd)); + + imx_i2c_start(is_repeat); + data->await_start = false; + fsm->yield = false; + // If we're here, i2c_start has already waited until ibb=0. + // Return to re-enter this state and check if arbitration loss occured. + return; + } + + // Addressing stage + if (data->await_wrrd) { + // first: handle sending initial address. + // invariant: start condition already set + LOG_I2C_DRIVER("Selected WRRD\n"); + + if (data->await_wrrd == WRRD_WRADDR) { + // Write address to data register. read bit = 0 + i2dr_tmp = i2c_curr_addr(data) << 1; + do_i2dr_write = true; + // invariant: mtx already set from await_start + } else if (data->await_wrrd == WRRD_SUBADDR) { + // Write sub address byte + uint8_t payload_byte = data->active_cmd.payload.data[0]; + i2dr_tmp = payload_byte; + do_i2dr_write = true; + LOG_I2C_DRIVER("WRRD: sending address byte %u\n", payload_byte); + } else { + LOG_I2C_DRIVER("WRRD: sending repeat start and looping\n"); + // Send a new start condition and leave await_addr to set up the read + // for us. + imx_i2c_start(true); + LOG_I2C_DRIVER("WRRD: ... done!\n"); + fsm->yield = false; + data->await_wrrd = 0; + // Re-enter state to check for arb. loss and continue. + return; + } + data->await_wrrd--; + + } else if (data->await_addr) { + LOG_I2C_DRIVER("Primary addressing stage...\n"); + // Calculate address byte with R/W bit + uint8_t addr_byte = (i2c_curr_addr(data) << 1); + bool is_read = cmd_is_read(data->active_cmd); + + if (is_read) { + addr_byte |= 0x1; // Read operation (R/W bit = 1) + } else { + addr_byte &= ~0x1; // Write operation (R/W bit = 0) + } + + LOG_I2C_DRIVER("Sending ADDR 0x%02x (read=%u)\n", i2c_curr_addr(data), is_read); + + // Write address to data register - this initiates the transfer + i2dr_tmp = addr_byte; + do_i2dr_write = true; + data->await_addr = false; + + // Data transmission and end + } else { + // read case + if (cmd_is_read(data->active_cmd)) { + // For the first data byte of a read, we need to switch to receive mode + // and do a dummy read to initiate the first byte reception + if (dummy_read == false) { + // Clear MTX to enter receive mode + i2cr_tmp &= ~REG_CR_MTX; + do_i2cr_write = true; + + // If this is a single byte read, prepare NACK for it + if (data->active_cmd.data_len == 1) { + i2cr_tmp |= REG_CR_TXAK; // No ACK for single byte + } + + // Dummy read to initiate first byte reception + // The byte we read here is garbage/invalid for the first read. + // Store dummy byte in this buffer to avoid optimising out... + // TODO: check for fencepost error here. + data->active_cmd.payload.data[0] = (uint8_t) regs->i2dr; + LOG_I2C_DRIVER("MTX->RX dummy read completed...\n"); + dummy_read = true; + } + + // We must send a stop just before reading the very last byte + if (data->rw_idx >= data->active_cmd.data_len - 1) { + if (data->await_stop) { + LOG_I2C_DRIVER("Generating STOP (read)\n"); + // Clear MSTA and MTX to generate STOP condition + // Use real register here as this must happen strictly + // BEFORE doing the read. + do_i2cr_write = true; + i2cr_tmp &= ~REG_CR_MSTA; + i2cr_tmp &= ~REG_CR_MTX; + data->await_stop = false; + } else { + LOG_I2C_DRIVER("Command complete, skipping back to S_SEL_CMD\n"); + // Command complete without STOP (repeated start follows) + fsm->next_state = S_SEL_CMD; + return; + } + } + + // Set up NACK for last byte when reading second-to-last byte + if (data->rw_idx == data->active_cmd.data_len - 2) { + // This is the second-to-last byte, set TXAK for NACK on last byte + i2cr_tmp |= REG_CR_TXAK; + do_i2cr_write = true; + } + + // Real read: simply store result directly. We are effectively saving + // the result of the previous read data kick. + data->active_cmd.payload.data[data->bytes_read] = (uint8_t) regs->i2dr; + LOG_I2C_DRIVER("Reading byte 0x%02x at idx %u\n", + data->active_cmd.payload.data[data->bytes_read], data->rw_idx); + data->bytes_read++; + data->rw_idx++; + + } else { + // --- WRITE OPERATION --- + uint8_t write_byte; + write_byte = data->active_cmd.payload.data[data->rw_idx]; + + // Generate stop just after sending last byte. + if (data->rw_idx >= data->active_cmd.data_len-1) { + if (data->await_stop) { + LOG_I2C_DRIVER("Generating STOP (read)\n"); + // Clear MSTA and MTX to generate STOP condition + i2cr_tmp &= ~REG_CR_MSTA; + i2cr_tmp &= ~REG_CR_MTX; + do_i2cr_write = true; + data->await_stop = false; + } else { + // TODO: check if this is needed, we probably can't get here. + LOG_I2C_DRIVER("Command complete, skipping back to S_SEL_CMD\n"); + // Command complete without STOP (repeated start follows) + fsm->next_state = S_SEL_CMD; + return; + } + } + + LOG_I2C_DRIVER("Writing data byte 0x%02x at idx %u\n", write_byte, data->rw_idx); + regs->i2dr = write_byte; + do_i2dr_write = true; + data->rw_idx++; + } + } + + // Only write if required. Writing to the DR in read mode is UB, and unneeded writes to CR + // may have side effects. + // Important: write DR FIRST. + if (do_i2dr_write) { + regs->i2dr = i2dr_tmp; + } + if (do_i2cr_write) { + regs->i2cr = i2cr_tmp; + } + // We should always sleep and go to CMD_RET unless we decided to return earlier to a + // different state. + fsm->next_state = S_CMD_RET; + fsm->yield = true; + return; +} + +/** + * S_CMD_RET + * Handle completion interrupt from hardware. Check status, read data if applicable, + * and determine next state (continue command, select next command, or error). + */ +void state_cmd_ret(fsm_data_t *fsm, i2c_driver_data_t *data, i2c_queue_handle_t *queue_handle) +{ + // Clear interrupt + regs->i2sr &= ~REG_SR_IIF; + uint16_t status = regs->i2sr; + + LOG_I2C_DRIVER("S_CMD_RET: status=0x%04x\n", status); + + // check arbitration, just in case. + if (status & REG_SR_IAL) { + LOG_I2C_DRIVER_ERR("Arbitration lost!\n"); + data->err = I2C_ERR_OTHER; + // clear IAL + regs->i2sr &= ~REG_SR_IAL; + fsm->next_state = S_RESPONSE; + return; + } + + // Check for NACK (RXAK=1 means no ACK received) + // This applies to address phase and write phases + if (status & REG_SR_RXAK) { + // Don't print this as an error, not useful usually. + LOG_I2C_DRIVER("NACK!\n"); + data->err = I2C_ERR_NACK; + + // Generate STOP and return error + regs->i2cr &= ~(REG_CR_MSTA | REG_CR_MTX); + fsm->next_state = S_RESPONSE; + return; + } + + // Check if command is complete + if (data->rw_idx >= data->active_cmd.data_len - 1) { + // We just sent the STOP condition in state_cmd, wait for completion + // IIF will be set when STOP completes + LOG_I2C_DRIVER("STOP complete\n"); + fsm->next_state = S_SEL_CMD; + + // Clean up + i2c_setup(); + } else { + // More data to transfer + fsm->next_state = S_CMD; + } +} + +void notified(microkit_channel ch) +{ + LOG_I2C_DRIVER("Notified on channel %d\n", ch); + + if (ch == config.virt.id) { + LOG_I2C_DRIVER("Notified by virt\n"); + fsm_virt_notified(&fsm_data); + } else if (ch == device_resources.irqs[0].id) { + LOG_I2C_DRIVER("I2C IRQ\n"); + fsm_cmd_done(&fsm_data); + microkit_irq_ack(ch); + } else { + LOG_I2C_DRIVER_ERR("unexpected notification on channel %d\n", ch); + } +} + diff --git a/drivers/i2c/imx/i2c_driver.mk b/drivers/i2c/imx/i2c_driver.mk new file mode 100644 index 000000000..48aa4d606 --- /dev/null +++ b/drivers/i2c/imx/i2c_driver.mk @@ -0,0 +1,35 @@ +# +# Copyright 2026, UNSW +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Include this snippet in your project Makefile to build +# the Meson i2c driver +# +# NOTES +# Generates i2c_driver.elf +# Requires libsddf_util_debug.a in ${LIBS} + +I2C_DRIVER_DIR := $(dir $(lastword $(MAKEFILE_LIST))) + +i2c_driver.elf: i2c/i2c_driver.o i2c/i2c_common.o + $(LD) $(LDFLAGS) $^ $(LIBS) -o $@ + +i2c/i2c_driver.o: CFLAGS+=-I${I2C_DRIVER_DIR} +i2c/i2c_driver.o: ${I2C_DRIVER_DIR}/i2c.c |i2c $(SDDF_LIBC_INCLUDE) + ${CC} ${CFLAGS} -c -o $@ $< + +i2c/i2c_common.o: CFLAGS+=-I${I2C_DRIVER_DIR} +i2c/i2c_common.o: ${I2C_DRIVER_DIR}/../i2c_common.c |i2c $(SDDF_LIBC_INCLUDE) + ${CC} ${CFLAGS} -c -o $@ $< + +i2c: + mkdir -p $@ + +clean:: + rm -rf i2c + +clobber:: + rm -f i2c_driver.elf + +-include i2c_driver.d diff --git a/drivers/pmic/bd71837amwv/bd71837.c b/drivers/pmic/bd71837amwv/bd71837.c new file mode 100644 index 000000000..df54aa620 --- /dev/null +++ b/drivers/pmic/bd71837amwv/bd71837.c @@ -0,0 +1,267 @@ +/* + * Copyright 2026, UNSW + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "bd71837.h" + +__attribute__((__section__(".i2c_client_config"))) i2c_client_config_t i2c_config; +__attribute__((__section__(".pmic_driver_config"))) pmic_driver_config_t pmic_config; + +libi2c_conf_t libi2c_conf; +i2c_queue_handle_t queue; +uint8_t *i2c_data_region; + +#define PMIC_I2C_ADDR ((i2c_addr_t)pmic_config.i2c_addr) + +#ifndef I2C_DATA_REGION +#define I2C_DATA_REGION ((uint8_t *)i2c_config.data.vaddr) +#endif + +// HACK: this driver skips around a limitation in microkit ... we return a success for ops +// before sending an i2c transaction as we cannot wait for a notification in the middle of +// protected(). This means that if the i2c transaction fails, the client will not know! +// There is no way to fix this other than hacking the microkit event loop locally, which isn't +// suitable for a driver. We will handle this in the future by changing microkit. + +pmic_driver_state_t state; + +static inline sddf_pmic_err_t pmic_not_implemented() +{ + LOG_PMIC_DRIVER_ERR("This PPC not implemented for this platform!\n"); + return SDDF_PMIC_ERR_NOT_IMPLEMENTED; +} + +sddf_pmic_err_t pmic_drv_enable_reg(uint64_t reg_id) +{ + return pmic_not_implemented(); +} + +sddf_pmic_err_t pmic_drv_disable_reg(uint64_t reg_id) +{ + return pmic_not_implemented(); +} + +sddf_pmic_err_t pmic_drv_set_vout(uint64_t reg_id, uint64_t voltage_uv) +{ + // Look up regulator info + bd71837_reg_t *regulator = &bd71837_regulator_table[reg_id]; + + // sanity checks + if (!regulator->reg.enabled) { + LOG_PMIC_DRIVER_ERR("Cannot set voltage for a disabled regulator\n"); + return SDDF_PMIC_ERR_FAIL_REG; + } + if (!regulator->reg.capabilities.voltage.adjustable) { + LOG_PMIC_DRIVER_ERR("Tried to set voltage for incompatible regulator %zu!\n", reg_id); + return SDDF_PMIC_ERR_BAD_SETTING; + } + + // check new voltage is valid + pmic_unit_t max = regulator->reg.capabilities.voltage.max_value; + pmic_unit_t min = regulator->reg.capabilities.voltage.min_value; + if (voltage_uv >= max || voltage_uv <= min) { + LOG_PMIC_DRIVER_ERR("Incompatible voltage requested!\n"); + return SDDF_PMIC_ERR_BAD_SETTING; + } + // prepare regulator register write + // most registers simply require us to write to bits [quantisation:0], but some + // are special cases + uint8_t i2c_reg_val; + if (reg_id == BD718XX_BUCK5) { + return pmic_not_implemented(); + } else if (reg_id == BD718XX_LDO1) { + return pmic_not_implemented(); + } else if (reg_id == BD718XX_LDO2) { + return pmic_not_implemented(); + } else { + // all other regulators + uint64_t uv_range = max - min; + uint64_t uv_per_step = uv_range / (1 << regulator->reg.capabilities.voltage.quantisation); + + uint64_t target_val = (voltage_uv - min) / uv_per_step; + + // If our target can't fit in the register, we've screwed up. + LOG_PMIC_DRIVER("Setting voltage to %zu - target value = %zu\n", voltage_uv, target_val); + assert(target_val <= (1 << regulator->reg.capabilities.voltage.quantisation)); + + i2c_reg_val = (uint8_t)(target_val & 0xff); + } + + // prepare i2c transaction + i2c_data_region[0] = regulator->i2c_reg_addr; + i2c_data_region[1] = i2c_reg_val; + sddf_i2c_nb_write(&libi2c_conf, PMIC_I2C_ADDR, i2c_data_region, 2); + // We set the state machine up in protected() + return SDDF_PMIC_ERR_OK; +} + +sddf_pmic_err_t pmic_drv_set_climit(uint64_t reg_id, uint64_t current_ua) +{ + return pmic_not_implemented(); +} + +sddf_pmic_err_t pmic_drv_get_info(uint64_t reg_id, sddf_pmic_reg_info_t *info) +{ + return pmic_not_implemented(); +} + +void init(void) +{ + // Setup i2c client connection + assert(i2c_config_check_magic((void *)&i2c_config)); + assert(pmic_config_check_magic((void *)&pmic_config)); + i2c_data_region = (uint8_t *)i2c_config.data.vaddr; + queue = i2c_queue_init(i2c_config.virt.req_queue.vaddr, i2c_config.virt.resp_queue.vaddr); + + bool claimed = i2c_bus_claim(i2c_config.virt.id, PMIC_I2C_ADDR); + if (!claimed) { + LOG_PMIC_DRIVER_ERR("Failed to claim PMIC bus address! Dying now!\n"); + assert(false); + } + + /* Initialise libi2c */ + libi2c_init(&libi2c_conf, &queue); + + pmic_reset_state(&state); + LOG_PMIC_DRIVER("Initialised.\n"); +} + +void notified(microkit_channel ch) +{ + if (ch == i2c_config.virt.id) { + if (state.curr_ppc_op == SDDF_PMIC_PPC_INVALID) { + LOG_PMIC_DRIVER_ERR("Spurious I2C interrupt!\n"); + return; + } + // Simply report if we failed to complete. + i2c_addr_t returned_addr; + size_t err_cmd_idx = 0; + int ret = sddf_i2c_nb_return(&libi2c_conf, &returned_addr, &err_cmd_idx); + + if (ret == I2C_ERR_OK) { + LOG_PMIC_DRIVER("Completed request for client %u. Opcode = %zu\n", state.curr_client, state.curr_ppc_op); + } else { + LOG_PMIC_DRIVER_ERR("I2C request failed for client %u! Opcode = %zu\n", state.curr_client, + state.curr_ppc_op); + } + + // Clean up state for next request. + pmic_reset_state(&state); + } else { + LOG_PMIC_DRIVER_ERR("Unknown channel 0x%x!\n", ch); + } +} + +microkit_msginfo protected(microkit_channel ch, microkit_msginfo msginfo) +{ + sddf_pmic_err_t err = 0; + uint64_t ret_num = 0; + uint32_t argc = microkit_msginfo_get_count(msginfo); + // Due to our HACK to support I2C, there exists a race condition on repeated calls to + // this driver. If we're currently waiting for an I2C response, simply reject any PPC. + if (state.curr_ppc_op != SDDF_PMIC_PPC_INVALID) { + LOG_PMIC_DRIVER_ERR("Rejecting incoming PPC due to outstanding request! Try again soon...\n"); + err = SDDF_PMIC_ERR_BUSY; + return microkit_msginfo_new(err, 1); + } + switch (microkit_msginfo_get_label(msginfo)) { + case SDDF_PMIC_ENABLE_REG: { + if (argc != 1) { + LOG_PMIC_DRIVER_ERR("Incorrect number of arguments %u != 1\n", argc); + err = SDDF_PMIC_ERR_BAD_PPC_CALL; + break; + } + uint32_t reg_id = (uint32_t)microkit_mr_get(SDDF_PMIC_ENABLE_REG_REG_ID); + LOG_PMIC_DRIVER("get request pmic_enable_reg(%d)\n", reg_id); + state.err = pmic_drv_enable_reg(reg_id); + break; + } + case SDDF_PMIC_DISABLE_REG: { + if (argc != 1) { + LOG_PMIC_DRIVER_ERR("Incorrect number of arguments %u != 1\n", argc); + err = SDDF_PMIC_ERR_BAD_PPC_CALL; + break; + } + uint32_t reg_id = (uint32_t)microkit_mr_get(SDDF_PMIC_DISABLE_REG_REG_ID); + LOG_PMIC_DRIVER("get request pmic_disable_reg(%d)\n", reg_id); + state.err = pmic_drv_disable_reg(reg_id); + break; + } + case SDDF_PMIC_SET_VOUT: { + if (argc != 3) { + LOG_PMIC_DRIVER_ERR("Incorrect number of arguments %u != 3\n", argc); + err = SDDF_PMIC_ERR_BAD_PPC_CALL; + break; + } + uint32_t reg_id = (uint32_t)microkit_mr_get(SDDF_PMIC_SET_VOUT_REG_ID); + uint64_t voltage_uv = microkit_mr_get(SDDF_PMIC_SET_VOUT_VOLTAGE_UV); + uint32_t op_mode_id = (uint32_t)microkit_mr_get(SDDF_PMIC_SET_VOUT_OP_MODE_ID); + LOG_PMIC_DRIVER("get request pmic_set_vout(%d, %lu, %d)\n", reg_id, voltage_uv, op_mode_id); + state.err = pmic_drv_set_vout(reg_id, voltage_uv); + break; + } + case SDDF_PMIC_SET_CLIMIT: { + if (argc != 3) { + LOG_PMIC_DRIVER_ERR("Incorrect number of arguments %u != 3\n", argc); + err = SDDF_PMIC_ERR_BAD_PPC_CALL; + break; + } + uint32_t reg_id = (uint32_t)microkit_mr_get(SDDF_PMIC_SET_CLIMIT_REG_ID); + uint64_t current_ua = microkit_mr_get(SDDF_PMIC_SET_CLIMIT_CURRENT_UA); + uint32_t op_mode_id = (uint32_t)microkit_mr_get(SDDF_PMIC_SET_CLIMIT_OP_MODE_ID); + LOG_PMIC_DRIVER("get request pmic_set_climit(%d, %lu, %d)\n", reg_id, current_ua, op_mode_id); + state.err = pmic_drv_set_climit(reg_id, current_ua); + break; + } + case SDDF_PMIC_GET_REG_INFO: { + if (argc != 1) { + LOG_PMIC_DRIVER_ERR("Incorrect number of arguments %u != 1\n", argc); + err = SDDF_PMIC_ERR_BAD_PPC_CALL; + break; + } + uint32_t reg_id = (uint32_t)microkit_mr_get(SDDF_PMIC_GET_REG_INFO_REG_ID); + sddf_pmic_reg_info_t info = { 0 }; + LOG_PMIC_DRIVER("get request pmic_get_reg_info(%d)\n", reg_id); + state.err = pmic_drv_get_info(reg_id, &info); + if (err == SDDF_PMIC_GET_REG_INFO_SUCCESS) { + microkit_mr_set(SDDF_PMIC_GET_REG_INFO_ENABLED, info.enabled); + microkit_mr_set(SDDF_PMIC_GET_REG_INFO_VOLTAGE_UV, info.voltage_uv); + microkit_mr_set(SDDF_PMIC_GET_REG_INFO_CURRENT_UA, info.current_ua); + microkit_mr_set(SDDF_PMIC_GET_REG_INFO_MIN_VOLTAGE_UV, info.min_voltage_uv); + microkit_mr_set(SDDF_PMIC_GET_REG_INFO_MAX_VOLTAGE_UV, info.max_voltage_uv); + microkit_mr_set(SDDF_PMIC_GET_REG_INFO_MIN_CURRENT_UA, info.min_current_ua); + microkit_mr_set(SDDF_PMIC_GET_REG_INFO_MAX_CURRENT_UA, info.max_current_ua); + microkit_mr_set(SDDF_PMIC_GET_REG_INFO_RAMPRATE, info.ramprate); + ret_num = 9; /* MR0-MR8 */ + } + break; + } + default: + LOG_PMIC_DRIVER_ERR("Unknown request %lu to PMIC driver from channel %u\n", microkit_msginfo_get_label(msginfo), + ch); + err = SDDF_PMIC_ERR_BAD_PPC_CALL; + } + if (state.err != SDDF_PMIC_ERR_OK) { + // Give up and die if we failed to set up a valid op + err = state.err; + } else { + // We didn't explode, proceed to sleep for i2c operation. + // Per the HACK label earlier, we pre-emptively return a success + // to clients here! + LOG_PMIC_DRIVER("Preparing to sleep for i2c return...\n"); + } + + return microkit_msginfo_new(err, ret_num); +} diff --git a/drivers/pmic/bd71837amwv/bd71837.h b/drivers/pmic/bd71837amwv/bd71837.h new file mode 100644 index 000000000..eaed7d71d --- /dev/null +++ b/drivers/pmic/bd71837amwv/bd71837.h @@ -0,0 +1,425 @@ +/* + * Copyright 2026, UNSW + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once +#include + +typedef struct bd71837_reg { + const char *name; + sddf_regulator_t reg; + uint8_t i2c_reg_addr; +} bd71837_reg_t; + +// IMPORTANT: this driver currently ONLY supports setting "run" mode parameters. +// We don't support setting idle or suspend mode parameters as these features +// cannot be harmoniously touched in LionsOS at the time of writing. +// TODO: sanity check the below, i am rushing and tired +#define BD71837_NUM_REGULATORS (15) +static bd71837_reg_t bd71837_regulator_table[BD71837_NUM_REGULATORS] = { + /* BUCK1: VDD_SOC, 3.6A, 0.7-1.3V @ 10mV step (6-bit DVS) */ + { + .name = "BUCK1", + .reg = { + .capabilities = { + .voltage = { .adjustable = true, .min_value = 700000, .max_value = 1300000, .quantisation = 6 }, + .current = { .adjustable = false, .min_value = 3600000, .max_value = 3600000, .quantisation = 0 }, + .toggleable = true, + }, + .voltage_uv = 0, + .current_ua = 3600000, + .enabled = true, + }, + .i2c_reg_addr = 0x0D, /* BUCK1_VOLT_RUN */ + }, + /* BUCK2: VDD_ARM, 4.0A, 0.7-1.3V @ 10mV step (6-bit DVS) */ + { + .name = "BUCK2", + .reg = { + .capabilities = { + .voltage = { .adjustable = true, .min_value = 700000, .max_value = 1300000, .quantisation = 6 }, + .current = { .adjustable = false, .min_value = 4000000, .max_value = 4000000, .quantisation = 0 }, + .toggleable = true, + }, + .voltage_uv = 0, + .current_ua = 4000000, + .enabled = true, + }, + .i2c_reg_addr = 0x10, /* BUCK2_VOLT_RUN */ + }, + /* BUCK3: VDD_GPU, 2.1A, 0.7-1.3V @ 10mV step (6-bit DVS) */ + { + .name = "BUCK3", + .reg = { + .capabilities = { + .voltage = { .adjustable = true, .min_value = 700000, .max_value = 1300000, .quantisation = 6 }, + .current = { .adjustable = false, .min_value = 2100000, .max_value = 2100000, .quantisation = 0 }, + .toggleable = true, + }, + .voltage_uv = 0, + .current_ua = 2100000, + .enabled = true, + }, + .i2c_reg_addr = 0x12, /* BUCK3_VOLT_RUN */ + }, + /* BUCK4: VDD_VPU, 1.0A, 0.7-1.3V @ 10mV step (6-bit DVS) */ + { + .name = "BUCK4", + .reg = { + .capabilities = { + .voltage = { .adjustable = true, .min_value = 700000, .max_value = 1300000, .quantisation = 6 }, + .current = { .adjustable = false, .min_value = 1000000, .max_value = 1000000, .quantisation = 0 }, + .toggleable = true, + }, + .voltage_uv = 0, + .current_ua = 1000000, + .enabled = true, + }, + .i2c_reg_addr = 0x13, /* BUCK4_VOLT_RUN */ + }, + /* BUCK5: VDD_DRAM, 2.5A, 0.7-1.35V 8-step (3-bit, non-linear) */ + { + .name = "BUCK5", + .reg = { + .capabilities = { + .voltage = { .adjustable = true, .min_value = 700000, .max_value = 1350000, .quantisation = 3 }, + .current = { .adjustable = false, .min_value = 2500000, .max_value = 2500000, .quantisation = 0 }, + .toggleable = true, + }, + .voltage_uv = 0, + .current_ua = 2500000, + .enabled = true, + }, + .i2c_reg_addr = 0x14, /* BUCK5_VOLT */ + }, + /* BUCK6: NVCC_3P3, 3.0A, 3.0-3.3V @ 100mV step (2-bit, 4 values) */ + { + .name = "BUCK6", + .reg = { + .capabilities = { + .voltage = { .adjustable = true, .min_value = 3000000, .max_value = 3300000, .quantisation = 2 }, + .current = { .adjustable = false, .min_value = 3000000, .max_value = 3000000, .quantisation = 0 }, + .toggleable = true, + }, + .voltage_uv = 0, + .current_ua = 3000000, + .enabled = true, + }, + .i2c_reg_addr = 0x15, /* BUCK6_VOLT */ + }, + /* BUCK7: NVCC_1P8, 1.5A, 1.605-1.995V 8-step (3-bit, discrete steps) */ + { + .name = "BUCK7", + .reg = { + .capabilities = { + .voltage = { .adjustable = true, .min_value = 1605000, .max_value = 1995000, .quantisation = 3 }, + .current = { .adjustable = false, .min_value = 1500000, .max_value = 1500000, .quantisation = 0 }, + .toggleable = true, + }, + .voltage_uv = 0, + .current_ua = 1500000, + .enabled = true, + }, + .i2c_reg_addr = 0x16, /* BUCK7_VOLT */ + }, + /* BUCK8: NVCC_DRAM, 3.0A, 0.8-1.4V @ 10mV step (6-bit) */ + { + .name = "BUCK8", + .reg = { + .capabilities = { + .voltage = { .adjustable = true, .min_value = 800000, .max_value = 1400000, .quantisation = 6 }, + .current = { .adjustable = false, .min_value = 3000000, .max_value = 3000000, .quantisation = 0 }, + .toggleable = true, + }, + .voltage_uv = 0, + .current_ua = 3000000, + .enabled = true, + }, + .i2c_reg_addr = 0x17, /* BUCK8_VOLT */ + }, + /* LDO1: NVCC_SNVS, 10mA, 3.0-3.3V or 1.6-1.9V (2-bit + range sel) */ + { + .name = "LDO1", + .reg = { + .capabilities = { + .voltage = { .adjustable = true, .min_value = 1600000, .max_value = 3300000, .quantisation = 2 }, + .current = { .adjustable = false, .min_value = 10000, .max_value = 10000, .quantisation = 0 }, + .toggleable = true, + }, + .voltage_uv = 0, + .current_ua = 10000, + .enabled = true, + }, + .i2c_reg_addr = 0x18, /* LDO1_VOLT */ + }, + /* LDO2: VDD_SNVS, 10mA, 0.8V or 0.9V (1-bit) */ + { + .name = "LDO2", + .reg = { + .capabilities = { + .voltage = { .adjustable = true, .min_value = 800000, .max_value = 900000, .quantisation = 1 }, + .current = { .adjustable = false, .min_value = 10000, .max_value = 10000, .quantisation = 0 }, + .toggleable = true, + }, + .voltage_uv = 0, + .current_ua = 10000, + .enabled = true, + }, + .i2c_reg_addr = 0x19, /* LDO2_VOLT */ + }, + /* LDO3: VDDA_1P8/DRAM, 300mA, 1.8-3.3V @ 100mV step (4-bit) */ + { + .name = "LDO3", + .reg = { + .capabilities = { + .voltage = { .adjustable = true, .min_value = 1800000, .max_value = 3300000, .quantisation = 4 }, + .current = { .adjustable = false, .min_value = 300000, .max_value = 300000, .quantisation = 0 }, + .toggleable = true, + }, + .voltage_uv = 0, + .current_ua = 300000, + .enabled = true, + }, + .i2c_reg_addr = 0x1A, /* LDO3_VOLT */ + }, + /* LDO4: VDDA_0P9, 250mA, 0.9-1.8V @ 100mV step (4-bit) */ + { + .name = "LDO4", + .reg = { + .capabilities = { + .voltage = { .adjustable = true, .min_value = 900000, .max_value = 1800000, .quantisation = 4 }, + .current = { .adjustable = false, .min_value = 250000, .max_value = 250000, .quantisation = 0 }, + .toggleable = true, + }, + .voltage_uv = 0, + .current_ua = 250000, + .enabled = true, + }, + .i2c_reg_addr = 0x1B, /* LDO4_VOLT */ + }, + /* LDO5: PHY_1P8, 300mA, 1.8-3.3V @ 100mV step (4-bit) */ + { + .name = "LDO5", + .reg = { + .capabilities = { + .voltage = { .adjustable = true, .min_value = 1800000, .max_value = 3300000, .quantisation = 4 }, + .current = { .adjustable = false, .min_value = 300000, .max_value = 300000, .quantisation = 0 }, + .toggleable = true, + }, + .voltage_uv = 0, + .current_ua = 300000, + .enabled = true, + }, + .i2c_reg_addr = 0x1C, /* LDO5_VOLT */ + }, + /* LDO6: PHY_0P9, 300mA, 0.9-1.8V @ 100mV step (4-bit) */ + { + .name = "LDO6", + .reg = { + .capabilities = { + .voltage = { .adjustable = true, .min_value = 900000, .max_value = 1800000, .quantisation = 4 }, + .current = { .adjustable = false, .min_value = 300000, .max_value = 300000, .quantisation = 0 }, + .toggleable = true, + }, + .voltage_uv = 0, + .current_ua = 300000, + .enabled = true, + }, + .i2c_reg_addr = 0x1D, /* LDO6_VOLT */ + }, + /* LDO7: PHY_3P3, 150mA, 1.8-3.3V @ 100mV step (4-bit) */ + { + .name = "LDO7", + .reg = { + .capabilities = { + .voltage = { .adjustable = true, .min_value = 1800000, .max_value = 3300000, .quantisation = 4 }, + .current = { .adjustable = false, .min_value = 150000, .max_value = 150000, .quantisation = 0 }, + .toggleable = true, + }, + .voltage_uv = 0, + .current_ua = 150000, + .enabled = true, + }, + .i2c_reg_addr = 0x1E, /* LDO7_VOLT */ + }, +}; + +/* Registers specific to BD71837 */ +enum { + BD71837_REG_BUCK3_CTRL = 0x07, + BD71837_REG_BUCK4_CTRL = 0x08, + BD71837_REG_BUCK3_VOLT_RUN = 0x12, + BD71837_REG_BUCK4_VOLT_RUN = 0x13, + BD71837_REG_LDO7_VOLT = 0x1E, +}; + +/* Registers common for BD71837 and BD71847 */ +enum { + BD718XX_REG_REV = 0x00, + BD718XX_REG_SWRESET = 0x01, + BD718XX_REG_I2C_DEV = 0x02, + BD718XX_REG_PWRCTRL0 = 0x03, + BD718XX_REG_PWRCTRL1 = 0x04, + BD718XX_REG_BUCK1_CTRL = 0x05, + BD718XX_REG_BUCK2_CTRL = 0x06, + BD718XX_REG_1ST_NODVS_BUCK_CTRL = 0x09, + BD718XX_REG_2ND_NODVS_BUCK_CTRL = 0x0A, + BD718XX_REG_3RD_NODVS_BUCK_CTRL = 0x0B, + BD718XX_REG_4TH_NODVS_BUCK_CTRL = 0x0C, + BD718XX_REG_BUCK1_VOLT_RUN = 0x0D, + BD718XX_REG_BUCK1_VOLT_IDLE = 0x0E, + BD718XX_REG_BUCK1_VOLT_SUSP = 0x0F, + BD718XX_REG_BUCK2_VOLT_RUN = 0x10, + BD718XX_REG_BUCK2_VOLT_IDLE = 0x11, + BD718XX_REG_1ST_NODVS_BUCK_VOLT = 0x14, + BD718XX_REG_2ND_NODVS_BUCK_VOLT = 0x15, + BD718XX_REG_3RD_NODVS_BUCK_VOLT = 0x16, + BD718XX_REG_4TH_NODVS_BUCK_VOLT = 0x17, + BD718XX_REG_LDO1_VOLT = 0x18, + BD718XX_REG_LDO2_VOLT = 0x19, + BD718XX_REG_LDO3_VOLT = 0x1A, + BD718XX_REG_LDO4_VOLT = 0x1B, + BD718XX_REG_LDO5_VOLT = 0x1C, + BD718XX_REG_LDO6_VOLT = 0x1D, + BD718XX_REG_TRANS_COND0 = 0x1F, + BD718XX_REG_TRANS_COND1 = 0x20, + BD718XX_REG_VRFAULTEN = 0x21, + BD718XX_REG_MVRFLTMASK0 = 0x22, + BD718XX_REG_MVRFLTMASK1 = 0x23, + BD718XX_REG_MVRFLTMASK2 = 0x24, + BD718XX_REG_RCVCFG = 0x25, + BD718XX_REG_RCVNUM = 0x26, + BD718XX_REG_PWRONCONFIG0 = 0x27, + BD718XX_REG_PWRONCONFIG1 = 0x28, + BD718XX_REG_RESETSRC = 0x29, + BD718XX_REG_MIRQ = 0x2A, + BD718XX_REG_IRQ = 0x2B, + BD718XX_REG_IN_MON = 0x2C, + BD718XX_REG_POW_STATE = 0x2D, + BD718XX_REG_OUT32K = 0x2E, + BD718XX_REG_REGLOCK = 0x2F, + BD718XX_REG_OTPVER = 0xFF, + BD718XX_MAX_REGISTER = 0x100, +}; + +#define REGLOCK_PWRSEQ 0x1 +#define REGLOCK_VREG 0x10 + +/* Generic BUCK control masks */ +#define BD718XX_BUCK_SEL 0x02 +#define BD718XX_BUCK_EN 0x01 +#define BD718XX_BUCK_RUN_ON 0x04 + +/* Generic LDO masks */ +#define BD718XX_LDO_SEL 0x80 +#define BD718XX_LDO_EN 0x40 + +/* BD71837 BUCK ramp rate CTRL reg bits */ +#define BUCK_RAMPRATE_MASK 0xC0 +#define BUCK_RAMPRATE_10P00MV 0x0 +#define BUCK_RAMPRATE_5P00MV 0x1 +#define BUCK_RAMPRATE_2P50MV 0x2 +#define BUCK_RAMPRATE_1P25MV 0x3 + +#define DVS_BUCK_RUN_MASK 0x3F +#define DVS_BUCK_SUSP_MASK 0x3F +#define DVS_BUCK_IDLE_MASK 0x3F + +#define BD718XX_1ST_NODVS_BUCK_MASK 0x07 +#define BD718XX_3RD_NODVS_BUCK_MASK 0x07 +#define BD718XX_4TH_NODVS_BUCK_MASK 0x3F + +#define BD71847_BUCK3_MASK 0x07 +#define BD71847_BUCK3_RANGE_MASK 0xC0 +#define BD71847_BUCK4_MASK 0x03 +#define BD71847_BUCK4_RANGE_MASK 0x40 + +#define BD71837_BUCK5_MASK 0x07 +#define BD71837_BUCK5_RANGE_MASK 0x80 +#define BD71837_BUCK6_MASK 0x03 + +#define BD718XX_LDO1_MASK 0x03 +#define BD718XX_LDO1_RANGE_MASK 0x20 +#define BD718XX_LDO2_MASK 0x20 +#define BD718XX_LDO3_MASK 0x0F +#define BD718XX_LDO4_MASK 0x0F +#define BD718XX_LDO6_MASK 0x0F + +#define BD71837_LDO5_MASK 0x0F +#define BD71847_LDO5_MASK 0x0F +#define BD71847_LDO5_RANGE_MASK 0x20 + +#define BD71837_LDO7_MASK 0x0F + +/* BD718XX Voltage monitoring masks */ +#define BD718XX_BUCK1_VRMON80 0x1 +#define BD718XX_BUCK1_VRMON130 0x2 +#define BD718XX_BUCK2_VRMON80 0x4 +#define BD718XX_BUCK2_VRMON130 0x8 +#define BD718XX_1ST_NODVS_BUCK_VRMON80 0x1 +#define BD718XX_1ST_NODVS_BUCK_VRMON130 0x2 +#define BD718XX_2ND_NODVS_BUCK_VRMON80 0x4 +#define BD718XX_2ND_NODVS_BUCK_VRMON130 0x8 +#define BD718XX_3RD_NODVS_BUCK_VRMON80 0x10 +#define BD718XX_3RD_NODVS_BUCK_VRMON130 0x20 +#define BD718XX_4TH_NODVS_BUCK_VRMON80 0x40 +#define BD718XX_4TH_NODVS_BUCK_VRMON130 0x80 +#define BD718XX_LDO1_VRMON80 0x1 +#define BD718XX_LDO2_VRMON80 0x2 +#define BD718XX_LDO3_VRMON80 0x4 +#define BD718XX_LDO4_VRMON80 0x8 +#define BD718XX_LDO5_VRMON80 0x10 +#define BD718XX_LDO6_VRMON80 0x20 + +/* BD71837 specific voltage monitoring masks */ +#define BD71837_BUCK3_VRMON80 0x10 +#define BD71837_BUCK3_VRMON130 0x20 +#define BD71837_BUCK4_VRMON80 0x40 +#define BD71837_BUCK4_VRMON130 0x80 +#define BD71837_LDO7_VRMON80 0x40 + +/* BD718XX_REG_IRQ bits */ +#define IRQ_SWRST 0x40 +#define IRQ_PWRON_S 0x20 +#define IRQ_PWRON_L 0x10 +#define IRQ_PWRON 0x08 +#define IRQ_WDOG 0x04 +#define IRQ_ON_REQ 0x02 +#define IRQ_STBY_REQ 0x01 + +/* ROHM BD718XX irqs */ +enum { + BD718XX_INT_STBY_REQ, + BD718XX_INT_ON_REQ, + BD718XX_INT_WDOG, + BD718XX_INT_PWRBTN, + BD718XX_INT_PWRBTN_L, + BD718XX_INT_PWRBTN_S, + BD718XX_INT_SWRST +}; + +/* ROHM BD718XX interrupt masks */ +#define BD718XX_INT_SWRST_MASK 0x40 +#define BD718XX_INT_PWRBTN_S_MASK 0x20 +#define BD718XX_INT_PWRBTN_L_MASK 0x10 +#define BD718XX_INT_PWRBTN_MASK 0x8 +#define BD718XX_INT_WDOG_MASK 0x4 +#define BD718XX_INT_ON_REQ_MASK 0x2 +#define BD718XX_INT_STBY_REQ_MASK 0x1 + +/* Register write induced reset settings */ + +/* + * Even though the bit zero is not SWRESET type we still want to write zero + * to it when changing type. Bit zero is 'SWRESET' trigger bit and if we + * write 1 to it we will trigger the action. So always write 0 to it when + * changning SWRESET action - no matter what we read from it. + */ +#define BD718XX_SWRESET_TYPE_MASK 7 +#define BD718XX_SWRESET_TYPE_DISABLED 0 +#define BD718XX_SWRESET_TYPE_COLD 4 +#define BD718XX_SWRESET_TYPE_WARM 6 + +#define BD718XX_SWRESET_RESET_MASK 1 +#define BD718XX_SWRESET_RESET 1 diff --git a/drivers/pmic/bd71837amwv/pmic_driver.mk b/drivers/pmic/bd71837amwv/pmic_driver.mk new file mode 100644 index 000000000..24d9eec42 --- /dev/null +++ b/drivers/pmic/bd71837amwv/pmic_driver.mk @@ -0,0 +1,32 @@ +# +# Copyright 2026, UNSW +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Include this snippet in your project Makefile to build +# the Meson pmic driver +# +# NOTES +# Generates pmic_driver.elf +# Requires libsddf_util_debug.a in ${LIBS} +# Requires libi2c.a + +PMIC_DRIVER_DIR := $(dir $(lastword $(MAKEFILE_LIST))) + +pmic_driver.elf: pmic/pmic_driver.o libi2c.a + $(LD) $(LDFLAGS) $^ $(LIBS) -o $@ + +pmic/pmic_driver.o: CFLAGS+=-I${PMIC_DRIVER_DIR} +pmic/pmic_driver.o: ${PMIC_DRIVER_DIR}/bd71837.c |pmic $(SDDF_LIBC_INCLUDE) + ${CC} ${CFLAGS} -c -o $@ $< + +pmic: + mkdir -p $@ + +clean:: + rm -rf pmic + +clobber:: + rm -f pmic_driver.elf + +-include pmic_driver.d diff --git a/drivers/timer/meson/timer.c b/drivers/timer/meson/timer.c index d133fa99c..c5c2bf6e2 100644 --- a/drivers/timer/meson/timer.c +++ b/drivers/timer/meson/timer.c @@ -120,8 +120,8 @@ seL4_MessageInfo_t protected(sddf_channel ch, seL4_MessageInfo_t msginfo) } case SDDF_TIMER_SET_TIMEOUT: { uint64_t curr_time = get_ticks(); - uint64_t offset_ticks = ns_to_ticks(sddf_get_mr(0), MESON_TIMER_CLK_FREQ); - timeouts[ch] = curr_time + offset_ticks; + uint64_t offset_us = sddf_get_mr(0) / NS_IN_US; + timeouts[ch] = curr_time + offset_us; process_timeouts(curr_time); break; } diff --git a/examples/i2c/i2c.mk b/examples/i2c/i2c.mk index 693f2b037..283af67a9 100644 --- a/examples/i2c/i2c.mk +++ b/examples/i2c/i2c.mk @@ -18,7 +18,9 @@ endif PYTHONPATH := ${SDDF}/tools/meta:${PYTHONPATH} export PYTHONPATH -SUPPORTED_BOARDS := odroidc4 +SUPPORTED_BOARDS := \ + odroidc4 \ + maaxboard include ${SDDF}/tools/make/board/common.mk @@ -80,17 +82,17 @@ $(SYSTEM_FILE): $(METAPROGRAM) $(IMAGES) $(DTB) $(PYTHON) $(METAPROGRAM) --sddf $(SDDF) --board $(MICROKIT_BOARD) --dtb $(DTB) --output . --sdf $(SYSTEM_FILE) $(OBJCOPY) --update-section .device_resources=timer_driver_device_resources.data timer_driver.elf $(OBJCOPY) --update-section .device_resources=i2c_driver_device_resources.data i2c_driver.elf - $(OBJCOPY) --update-section .i2c_driver_config=i2c_driver.data i2c_driver.elf - $(OBJCOPY) --update-section .i2c_virt_config=i2c_virt.data i2c_virt.elf - $(OBJCOPY) --update-section .i2c_client_config=i2c_client_client_ds3231.data client_ds3231.elf - $(OBJCOPY) --update-section .timer_client_config=timer_client_client_ds3231.data client_ds3231.elf - $(OBJCOPY) --update-section .i2c_client_config=i2c_client_client_pn532.data client_pn532.elf - $(OBJCOPY) --update-section .timer_client_config=timer_client_client_pn532.data client_pn532.elf + $(OBJCOPY) --update-section .i2c_driver_config=i2c_driver_i2c_driver_config.data i2c_driver.elf + $(OBJCOPY) --update-section .i2c_virt_config=i2c_virt_i2c_virt_config.data i2c_virt.elf + $(OBJCOPY) --update-section .i2c_client_config=client_ds3231_i2c_client_config.data client_ds3231.elf + $(OBJCOPY) --update-section .timer_client_config=client_ds3231_timer_client_config.data client_ds3231.elf + $(OBJCOPY) --update-section .i2c_client_config=client_pn532_i2c_client_config.data client_pn532.elf + $(OBJCOPY) --update-section .timer_client_config=client_pn532_timer_client_config.data client_pn532.elf $(OBJCOPY) --update-section .device_resources=serial_driver_device_resources.data serial_driver.elf - $(OBJCOPY) --update-section .serial_driver_config=serial_driver_config.data serial_driver.elf - $(OBJCOPY) --update-section .serial_virt_tx_config=serial_virt_tx.data serial_virt_tx.elf - $(OBJCOPY) --update-section .serial_client_config=serial_client_client_pn532.data client_pn532.elf - $(OBJCOPY) --update-section .serial_client_config=serial_client_client_ds3231.data client_ds3231.elf + $(OBJCOPY) --update-section .serial_driver_config=serial_driver_serial_driver_config.data serial_driver.elf + $(OBJCOPY) --update-section .serial_virt_tx_config=serial_virt_tx_serial_virt_tx_config.data serial_virt_tx.elf + $(OBJCOPY) --update-section .serial_client_config=client_pn532_serial_client_config.data client_pn532.elf + $(OBJCOPY) --update-section .serial_client_config=client_ds3231_serial_client_config.data client_ds3231.elf touch $@ $(IMAGE_FILE) $(REPORT_FILE): $(IMAGES) $(SYSTEM_FILE) diff --git a/examples/i2c/meta.py b/examples/i2c/meta.py index 750e085d6..ebb1d654f 100644 --- a/examples/i2c/meta.py +++ b/examples/i2c/meta.py @@ -4,82 +4,45 @@ import argparse from typing import List from dataclasses import dataclass -from sdfgen import SystemDescription, Sddf, DeviceTree +from acacia import System, ProtectionDomain, MemoryRegion, Channel, DeviceTreeBlob, Map -sys.path.append( - os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../tools/meta") -) -from board import BOARDS +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../")) +from acacia_sddf import BOARDS, sDDFI2C, sDDFSerial, sDDFTimer -ProtectionDomain = SystemDescription.ProtectionDomain -MemoryRegion = SystemDescription.MemoryRegion -Map = SystemDescription.Map - -def generate(sdf_file: str, output_dir: str, dtb: DeviceTree): - serial_driver = ProtectionDomain("serial_driver", "serial_driver.elf", priority=200) - serial_virt_tx = ProtectionDomain( - "serial_virt_tx", "serial_virt_tx.elf", priority=199 +def generate(sdf_file: str, output_dir: str, dtb: DeviceTreeBlob): + client_pn532 = ProtectionDomain(sdf, "client_pn532", "client_pn532.elf", priority=1) + client_ds3231 = ProtectionDomain( + sdf, "client_ds3231", "client_ds3231.elf", priority=1 ) - timer_driver = ProtectionDomain("timer_driver", "timer_driver.elf", priority=4) - i2c_driver = ProtectionDomain("i2c_driver", "i2c_driver.elf", priority=3) - i2c_virt = ProtectionDomain("i2c_virt", "i2c_virt.elf", priority=2) - client_pn532 = ProtectionDomain("client_pn532", "client_pn532.elf", priority=1) - client_ds3231 = ProtectionDomain("client_ds3231", "client_ds3231.elf", priority=1) - - if board.name == "odroidc4": - # Odroid-C4 I2C requires clocks/GPIO setup, for now we give the I2C driver - # direct access. - clk_mr = MemoryRegion(sdf, "clk", 0x1000, paddr=0xFF63C000) - gpio_mr = MemoryRegion(sdf, "gpio", 0x1000, paddr=0xFF634000) - sdf.add_mr(clk_mr) - sdf.add_mr(gpio_mr) - i2c_driver.add_map(Map(clk_mr, 0x30_000_000, "rw", cached=False)) - i2c_driver.add_map(Map(gpio_mr, 0x30_100_000, "rw", cached=False)) - - i2c_node = dtb.node(board.i2c) - assert i2c_node is not None - timer_node = dtb.node(board.timer) - assert timer_node is not None - serial_node = dtb.node(board.serial) - assert serial_node is not None - - i2c_system = Sddf.I2c(sdf, i2c_node, i2c_driver, i2c_virt) - i2c_system.add_client(client_ds3231) - i2c_system.add_client(client_pn532) - - timer_system = Sddf.Timer(sdf, timer_node, timer_driver) - timer_system.add_client(client_pn532) - timer_system.add_client(client_ds3231) - - serial_system = Sddf.Serial( - sdf, serial_node, serial_driver, serial_virt_tx, enable_color=False + i2c = sDDFI2C( + sdf, board.i2c.compatible, board.i2c.node_path, driver_prio=200, virt_prio=199 ) - serial_system.add_client(client_pn532) - serial_system.add_client(client_ds3231) - - pds = [ - serial_driver, - serial_virt_tx, - timer_driver, - i2c_driver, - i2c_virt, - client_pn532, - client_ds3231, - ] - for pd in pds: - sdf.add_pd(pd) - - assert i2c_system.connect() - assert i2c_system.serialise_config(output_dir) - assert serial_system.connect() - assert serial_system.serialise_config(output_dir) - assert timer_system.connect() - assert timer_system.serialise_config(output_dir) + i2c.add_client(client_ds3231) + i2c.add_client(client_pn532) + + timer = sDDFTimer(sdf, board.timer.compatible, board.timer.node_path) + timer.add_client(client_ds3231) + timer.add_client(client_pn532) + + serial = sDDFSerial( + sdf, + board.serial.compatible, + board.serial.node_path, + driver_prio=201, + virt_tx_prio=200, + allow_rx=False, + enable_color=False, + baud_rate=board.baud_rate if board.baud_rate else 115200, + ) + serial.add_client(client_ds3231) + serial.add_client(client_pn532) - with open(f"{output_dir}/{sdf_file}", "w+") as f: - f.write(sdf.render()) + out_file = f"{output_dir}/{sdf_file}" + sdf.make_config_structs() + print(f"Saving to {out_file}") + sdf.write_xml_file(out_file) if __name__ == "__main__": @@ -94,10 +57,7 @@ def generate(sdf_file: str, output_dir: str, dtb: DeviceTree): board = next(filter(lambda b: b.name == args.board, BOARDS)) - sdf = SystemDescription(board.arch, board.paddr_top) - sddf = Sddf(args.sddf) - - with open(args.dtb, "rb") as f: - dtb = DeviceTree(f.read()) + dtb = DeviceTreeBlob(args.dtb) + sdf = System(board.arch, board.paddr_top, dtb) generate(args.sdf, args.output, dtb) diff --git a/examples/i2c_bus_scan/i2cscan.mk b/examples/i2c_bus_scan/i2cscan.mk index 4c5f44fa8..b3056d7a2 100644 --- a/examples/i2c_bus_scan/i2cscan.mk +++ b/examples/i2c_bus_scan/i2cscan.mk @@ -20,6 +20,7 @@ export PYTHONPATH SUPPORTED_BOARDS := \ odroidc4 \ + maaxboard \ serengeti include ${SDDF}/tools/make/board/common.mk @@ -75,14 +76,15 @@ $(SYSTEM_FILE): $(METAPROGRAM) $(IMAGES) $(DTB) $(PYTHON) $(METAPROGRAM) --sddf $(SDDF) --board $(MICROKIT_BOARD) --dtb $(DTB) --output . --sdf $(SYSTEM_FILE) $(OBJCOPY) --update-section .device_resources=timer_driver_device_resources.data timer_driver.elf $(OBJCOPY) --update-section .device_resources=i2c_driver_device_resources.data i2c_driver.elf - $(OBJCOPY) --update-section .i2c_driver_config=i2c_driver.data i2c_driver.elf - $(OBJCOPY) --update-section .i2c_virt_config=i2c_virt.data i2c_virt.elf - $(OBJCOPY) --update-section .i2c_client_config=i2c_client_client_scan.data client_scan.elf - $(OBJCOPY) --update-section .timer_client_config=timer_client_client_scan.data client_scan.elf $(OBJCOPY) --update-section .device_resources=serial_driver_device_resources.data serial_driver.elf - $(OBJCOPY) --update-section .serial_driver_config=serial_driver_config.data serial_driver.elf - $(OBJCOPY) --update-section .serial_virt_tx_config=serial_virt_tx.data serial_virt_tx.elf - $(OBJCOPY) --update-section .serial_client_config=serial_client_client_scan.data client_scan.elf + $(OBJCOPY) --update-section .i2c_driver_config=i2c_driver_i2c_driver_config.data i2c_driver.elf + $(OBJCOPY) --update-section .i2c_virt_config=i2c_virt_i2c_virt_config.data i2c_virt.elf + $(OBJCOPY) --update-section .i2c_client_config=client_scan_i2c_client_config.data client_scan.elf + $(OBJCOPY) --update-section .timer_client_config=client_scan_timer_client_config.data client_scan.elf + $(OBJCOPY) --update-section .serial_driver_config=serial_driver_serial_driver_config.data serial_driver.elf + $(OBJCOPY) --update-section .serial_virt_tx_config=serial_virt_tx_serial_virt_tx_config.data serial_virt_tx.elf + $(OBJCOPY) --update-section .serial_client_config=client_scan_serial_client_config.data client_scan.elf + touch $@ $(IMAGE_FILE) $(REPORT_FILE): $(IMAGES) $(SYSTEM_FILE) diff --git a/examples/i2c_bus_scan/meta.py b/examples/i2c_bus_scan/meta.py index 74c4a95ea..fc3231de6 100644 --- a/examples/i2c_bus_scan/meta.py +++ b/examples/i2c_bus_scan/meta.py @@ -4,78 +4,40 @@ import argparse from typing import List from dataclasses import dataclass -from sdfgen import SystemDescription, Sddf, DeviceTree +from acacia import System, MemoryRegion, Map, Channel, DeviceTreeBlob, ProtectionDomain -sys.path.append( - os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../tools/meta") -) -from board import BOARDS +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../")) -ProtectionDomain = SystemDescription.ProtectionDomain -MemoryRegion = SystemDescription.MemoryRegion -Map = SystemDescription.Map +from acacia_sddf import BOARDS, sDDFI2C, sDDFSerial, sDDFTimer -def generate(sdf_file: str, output_dir: str, dtb: DeviceTree): - serial_driver = ProtectionDomain("serial_driver", "serial_driver.elf", priority=200) - # Increase the stack size as running with UBSAN uses more stack space than normal. - serial_virt_tx = ProtectionDomain( - "serial_virt_tx", "serial_virt_tx.elf", priority=199, stack_size=0x2000 - ) - - timer_driver = ProtectionDomain("timer_driver", "timer_driver.elf", priority=4) - i2c_driver = ProtectionDomain("i2c_driver", "i2c_driver.elf", priority=3) - i2c_virt = ProtectionDomain("i2c_virt", "i2c_virt.elf", priority=2) - client_scan = ProtectionDomain("client_scan", "client_scan.elf", priority=1) - - if board.name == "odroidc4": - # Odroid-C4 I2C requires clocks/GPIO setup, for now we give the I2C driver - # direct access. - clk_mr = MemoryRegion(sdf, "clk", 0x1000, paddr=0xFF63C000) - gpio_mr = MemoryRegion(sdf, "gpio", 0x1000, paddr=0xFF634000) - sdf.add_mr(clk_mr) - sdf.add_mr(gpio_mr) - i2c_driver.add_map(Map(clk_mr, 0x30_000_000, "rw", cached=False)) - i2c_driver.add_map(Map(gpio_mr, 0x30_100_000, "rw", cached=False)) - - i2c_node = dtb.node(board.i2c) - assert i2c_node is not None - timer_node = dtb.node(board.timer) - assert timer_node is not None - serial_node = dtb.node(board.serial) - assert serial_node is not None - - i2c_system = Sddf.I2c(sdf, i2c_node, i2c_driver, i2c_virt) - i2c_system.add_client(client_scan) +def generate(sdf_file: str, output_dir: str, dtb: DeviceTreeBlob): + client_scan = ProtectionDomain(sdf, "client_scan", "client_scan.elf", priority=1) - timer_system = Sddf.Timer(sdf, timer_node, timer_driver) - timer_system.add_client(client_scan) - - serial_system = Sddf.Serial( - sdf, serial_node, serial_driver, serial_virt_tx, enable_color=False + i2c = sDDFI2C( + sdf, board.i2c.compatible, board.i2c.node_path, driver_prio=200, virt_prio=199 ) - serial_system.add_client(client_scan) - - pds = [ - serial_driver, - serial_virt_tx, - timer_driver, - i2c_driver, - i2c_virt, - client_scan, - ] - for pd in pds: - sdf.add_pd(pd) - - assert i2c_system.connect() - assert i2c_system.serialise_config(output_dir) - assert serial_system.connect() - assert serial_system.serialise_config(output_dir) - assert timer_system.connect() - assert timer_system.serialise_config(output_dir) + i2c.add_client(client_scan) + + timer = sDDFTimer(sdf, board.timer.compatible, board.timer.node_path) + timer.add_client(client_scan) + + serial = sDDFSerial( + sdf, + board.serial.compatible, + board.serial.node_path, + driver_prio=201, + virt_tx_prio=200, + allow_rx=False, + enable_color=False, + baud_rate=board.baud_rate if board.baud_rate else 115200, + ) + serial.add_client(client_scan) - with open(f"{output_dir}/{sdf_file}", "w+") as f: - f.write(sdf.render()) + out_file = f"{output_dir}/{sdf_file}" + sdf.make_config_structs() + print(f"Saving to {out_file}") + sdf.write_xml_file(out_file) if __name__ == "__main__": @@ -90,10 +52,7 @@ def generate(sdf_file: str, output_dir: str, dtb: DeviceTree): board = next(filter(lambda b: b.name == args.board, BOARDS)) - sdf = SystemDescription(board.arch, board.paddr_top) - sddf = Sddf(args.sddf) - - with open(args.dtb, "rb") as f: - dtb = DeviceTree(f.read()) + dtb = DeviceTreeBlob(args.dtb) + sdf = System(board.arch, board.paddr_top, dtb) generate(args.sdf, args.output, dtb) diff --git a/examples/ina219/ina219.mk b/examples/ina219/ina219.mk index ce8deb4a0..43e98809a 100644 --- a/examples/ina219/ina219.mk +++ b/examples/ina219/ina219.mk @@ -74,14 +74,15 @@ $(SYSTEM_FILE): $(METAPROGRAM) $(IMAGES) $(DTB) $(PYTHON) $(METAPROGRAM) --sddf $(SDDF) --board $(MICROKIT_BOARD) --dtb $(DTB) --output . --sdf $(SYSTEM_FILE) $(OBJCOPY) --update-section .device_resources=timer_driver_device_resources.data timer_driver.elf $(OBJCOPY) --update-section .device_resources=i2c_driver_device_resources.data i2c_driver.elf - $(OBJCOPY) --update-section .i2c_driver_config=i2c_driver.data i2c_driver.elf - $(OBJCOPY) --update-section .i2c_virt_config=i2c_virt.data i2c_virt.elf - $(OBJCOPY) --update-section .i2c_client_config=i2c_client_client_ina.data client_ina.elf - $(OBJCOPY) --update-section .timer_client_config=timer_client_client_ina.data client_ina.elf $(OBJCOPY) --update-section .device_resources=serial_driver_device_resources.data serial_driver.elf - $(OBJCOPY) --update-section .serial_driver_config=serial_driver_config.data serial_driver.elf - $(OBJCOPY) --update-section .serial_virt_tx_config=serial_virt_tx.data serial_virt_tx.elf - $(OBJCOPY) --update-section .serial_client_config=serial_client_client_ina.data client_ina.elf + $(OBJCOPY) --update-section .i2c_driver_config=i2c_driver_i2c_driver_config.data i2c_driver.elf + $(OBJCOPY) --update-section .i2c_virt_config=i2c_virt_i2c_virt_config.data i2c_virt.elf + $(OBJCOPY) --update-section .i2c_client_config=client_ina_i2c_client_config.data client_ina.elf + $(OBJCOPY) --update-section .timer_client_config=client_ina_timer_client_config.data client_ina.elf + $(OBJCOPY) --update-section .serial_driver_config=serial_driver_serial_driver_config.data serial_driver.elf + $(OBJCOPY) --update-section .serial_virt_tx_config=serial_virt_tx_serial_virt_tx_config.data serial_virt_tx.elf + $(OBJCOPY) --update-section .serial_client_config=client_ina_serial_client_config.data client_ina.elf + touch $@ $(IMAGE_FILE) $(REPORT_FILE): $(IMAGES) $(SYSTEM_FILE) diff --git a/examples/ina219/meta.py b/examples/ina219/meta.py index f0b3b9c73..ce1e25cc1 100644 --- a/examples/ina219/meta.py +++ b/examples/ina219/meta.py @@ -4,68 +4,42 @@ import argparse from typing import List from dataclasses import dataclass -from sdfgen import SystemDescription, Sddf, DeviceTree from importlib.metadata import version -sys.path.append( - os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../tools/meta") -) -from board import BOARDS +from acacia import System, ProtectionDomain, MemoryRegion, Channel, DeviceTreeBlob, Map -ProtectionDomain = SystemDescription.ProtectionDomain -MemoryRegion = SystemDescription.MemoryRegion -Map = SystemDescription.Map +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../")) +from acacia_sddf import BOARDS, sDDFI2C, sDDFSerial, sDDFTimer -def generate(sdf_file: str, output_dir: str, dtb: DeviceTree): - serial_driver = ProtectionDomain("serial_driver", "serial_driver.elf", priority=200) - serial_virt_tx = ProtectionDomain( - "serial_virt_tx", "serial_virt_tx.elf", priority=199 - ) - - timer_driver = ProtectionDomain("timer_driver", "timer_driver.elf", priority=4) - i2c_driver = ProtectionDomain("i2c_driver", "i2c_driver.elf", priority=3) - i2c_virt = ProtectionDomain("i2c_virt", "i2c_virt.elf", priority=2) - client_ina = ProtectionDomain("client_ina", "client_ina.elf", priority=1) - - i2c_node = dtb.node(board.i2c) - assert i2c_node is not None - timer_node = dtb.node(board.timer) - assert timer_node is not None - serial_node = dtb.node(board.serial) - assert serial_node is not None - - i2c_system = Sddf.I2c(sdf, i2c_node, i2c_driver, i2c_virt) - i2c_system.add_client(client_ina) - timer_system = Sddf.Timer(sdf, timer_node, timer_driver) - timer_system.add_client(client_ina) +def generate(sdf_file: str, output_dir: str, dtb: DeviceTreeBlob): + client_ina = ProtectionDomain(sdf, "client_ina", "client_ina.elf", priority=1) - serial_system = Sddf.Serial( - sdf, serial_node, serial_driver, serial_virt_tx, enable_color=False + i2c = sDDFI2C( + sdf, board.i2c.compatible, board.i2c.node_path, driver_prio=200, virt_prio=199 ) - serial_system.add_client(client_ina) - - pds = [ - serial_driver, - serial_virt_tx, - timer_driver, - i2c_driver, - i2c_virt, - client_ina, - ] - for pd in pds: - sdf.add_pd(pd) - - assert i2c_system.connect() - assert i2c_system.serialise_config(output_dir) - assert serial_system.connect() - assert serial_system.serialise_config(output_dir) - assert timer_system.connect() - assert timer_system.serialise_config(output_dir) + i2c.add_client(client_ina) + + timer = sDDFTimer(sdf, board.timer.compatible, board.timer.node_path) + timer.add_client(client_ina) + + serial = sDDFSerial( + sdf, + board.serial.compatible, + board.serial.node_path, + driver_prio=201, + virt_tx_prio=200, + allow_rx=False, + enable_color=False, + baud_rate=board.baud_rate if board.baud_rate else 115200, + ) + serial.add_client(client_ina) - with open(f"{output_dir}/{sdf_file}", "w+") as f: - f.write(sdf.render()) + out_file = f"{output_dir}/{sdf_file}" + sdf.make_config_structs() + print(f"Saving to {out_file}") + sdf.write_xml_file(out_file) if __name__ == "__main__": @@ -80,10 +54,7 @@ def generate(sdf_file: str, output_dir: str, dtb: DeviceTree): board = next(filter(lambda b: b.name == args.board, BOARDS)) - sdf = SystemDescription(board.arch, board.paddr_top) - sddf = Sddf(args.sddf) - - with open(args.dtb, "rb") as f: - dtb = DeviceTree(f.read()) + dtb = DeviceTreeBlob(args.dtb) + sdf = System(board.arch, board.paddr_top, dtb) generate(args.sdf, args.output, dtb) diff --git a/examples/pmic/Makefile b/examples/pmic/Makefile new file mode 100644 index 000000000..19369ba43 --- /dev/null +++ b/examples/pmic/Makefile @@ -0,0 +1,31 @@ +# +# Copyright 2026, UNSW +# +# SPDX-License-Identifier: BSD-2-Clause +# + +ifeq ($(strip $(MICROKIT_SDK)),) +$(error MICROKIT_SDK must be specified) +endif +override MICROKIT_SDK := $(abspath ${MICROKIT_SDK}) + +BUILD_DIR ?= build +override BUILD_DIR := $(abspath ${BUILD_DIR}) +export BUILD_DIR +export MICROKIT_CONFIG ?= debug +export MICROKIT_BOARD ?= maaxboard + +export SDDF := $(abspath ../../) + +IMAGE_FILE := $(BUILD_DIR)/loader.img +REPORT_FILE := $(BUILD_DIR)/report.txt + +all: ${IMAGE_FILE} + +${IMAGE_FILE} ${REPORT_FILE} clean clobber: ${BUILD_DIR}/Makefile FORCE + ${MAKE} -C ${BUILD_DIR} MICROKIT_SDK=${MICROKIT_SDK} $(notdir $@) + +${BUILD_DIR}/Makefile: pmic.mk + mkdir -p ${BUILD_DIR} + cp pmic.mk $@ +FORCE: diff --git a/examples/pmic/client.c b/examples/pmic/client.c new file mode 100644 index 000000000..1d3d190ba --- /dev/null +++ b/examples/pmic/client.c @@ -0,0 +1,113 @@ +/* + * Copyright 2026, UNSW + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef CONFIG_PLAT_MAAXBOARD +#include +#define TARGET_REGULATOR (BD718XX_BUCK2) // VDD_ARM +#define VOLTAGE_A (900000) // 0.9V +#define VOLTAGE_B (1000000) // 1V +#else +#error "Unsupported board!" +#endif + +__attribute__((__section__(".timer_client_config"))) timer_client_config_t timer_config; +__attribute__((__section__(".serial_client_config"))) serial_client_config_t serial_config; +__attribute__((__section__(".pmic_client_config"))) pmic_client_config_t pmic_config; + +cothread_t t_event; +cothread_t t_main; + +static serial_queue_handle_t serial_tx_queue_handle; + +#define PMIC_CHANNEL (pmic_config.driver_id) + +#define STACK_SIZE (4096) +static char t_client_main_stack[STACK_SIZE]; + +#define DEBUG_CLIENT + +#ifdef DEBUG_CLIENT +#define LOG_CLIENT(...) do{ sddf_dprintf("SCAN|INFO: "); sddf_printf(__VA_ARGS__); }while(0) +#else +#define LOG_CLIENT(...) do{}while(0) +#endif +#define LOG_CLIENT_ERR(...) do{ sddf_printf("SCAN|ERROR: "); sddf_printf(__VA_ARGS__); }while(0) + +static inline bool delay_ms(size_t milliseconds) +{ + size_t time_ns = milliseconds * NS_IN_MS; + + /* Detect potential overflow */ + if (milliseconds != 0 && time_ns / milliseconds != NS_IN_MS) { + LOG_CLIENT_ERR("overflow detected in delay_ms"); + return false; + } + + sddf_timer_set_timeout(timer_config.driver_id, time_ns); + co_switch(t_event); + + return true; +} + +sddf_channel timer_channel; + +void notified(sddf_channel ch) +{ + if (ch == timer_config.driver_id) { + co_switch(t_main); + } else if (ch == serial_config.tx.id) { + // nothing to do + } else { + LOG_CLIENT_ERR("Unknown channel 0x%x!\n", ch); + } +} + +void client_main(void) +{ + LOG_CLIENT("Entered main loop.\n"); + for (uint32_t i = 0;; i++) { + // Alternate between setting voltage rail to 0.9 or 1V + if (i % 2) { + sddf_pmic_set_vout(PMIC_CHANNEL, TARGET_REGULATOR, VOLTAGE_A); + LOG_CLIENT("Set voltage of regulator %d to %zu\n", TARGET_REGULATOR, VOLTAGE_A); + } else { + sddf_pmic_set_vout(PMIC_CHANNEL, TARGET_REGULATOR, VOLTAGE_B); + LOG_CLIENT("Set voltage of regulator %d to %zu\n", TARGET_REGULATOR, VOLTAGE_B); + } + delay_ms(5000); + } +} + +void init(void) +{ + assert(serial_config_check_magic(&serial_config)); + serial_queue_init(&serial_tx_queue_handle, serial_config.tx.queue.vaddr, serial_config.tx.data.size, + serial_config.tx.data.vaddr); + serial_putchar_init(serial_config.tx.id, &serial_tx_queue_handle); + + assert(timer_config_check_magic(&timer_config)); + assert(pmic_config_check_magic(&pmic_config)); + sddf_printf("CLIENT|INFO: starting\n"); + + timer_channel = timer_config.driver_id; + + /* Define the event loop/notified thread as the active co-routine */ + t_event = co_active(); + + /* derive main entry point */ + t_main = co_derive((void *)t_client_main_stack, STACK_SIZE, client_main); + + co_switch(t_main); +} diff --git a/examples/pmic/meta.py b/examples/pmic/meta.py new file mode 100644 index 000000000..0ed411561 --- /dev/null +++ b/examples/pmic/meta.py @@ -0,0 +1,60 @@ +# Copyright 2026, UNSW +# SPDX-License-Identifier: BSD-2-Clause +import os, sys +import argparse +from typing import List +from dataclasses import dataclass + +from acacia import System, ProtectionDomain, MemoryRegion, Channel, DeviceTreeBlob, Map + +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../")) +from acacia_sddf import BOARDS, sDDFI2C, sDDFSerial, sDDFTimer, sDDFPMIC + + +def generate(sdf_file: str, output_dir: str, dtb: DeviceTreeBlob): + client = ProtectionDomain(sdf, "client", "client.elf", priority=1) + + timer = sDDFTimer(sdf, board.timer.compatible, board.timer.node_path) + timer.add_client(client) + + serial = sDDFSerial( + sdf, + board.serial.compatible, + board.serial.node_path, + driver_prio=201, + virt_tx_prio=200, + allow_rx=False, + enable_color=False, + baud_rate=board.baud_rate if board.baud_rate else 115200, + ) + serial.add_client(client) + + i2c = sDDFI2C( + sdf, board.i2c.compatible, board.i2c.node_path, driver_prio=200, virt_prio=199 + ) + + pmic = sDDFPMIC(sdf, board.pmic.compatible, board.pmic.node_path, i2c) + pmic.add_client(client) + + out_file = f"{output_dir}/{sdf_file}" + sdf.make_config_structs() + print(f"Saving to {out_file}") + sdf.write_xml_file(out_file) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--dtb", required=True) + parser.add_argument("--sddf", required=True) + parser.add_argument("--board", required=True, choices=[b.name for b in BOARDS]) + parser.add_argument("--output", required=True) + parser.add_argument("--sdf", required=True) + + args = parser.parse_args() + + board = next(filter(lambda b: b.name == args.board, BOARDS)) + + dtb = DeviceTreeBlob(args.dtb) + sdf = System(board.arch, board.paddr_top, dtb) + + generate(args.sdf, args.output, dtb) diff --git a/examples/pmic/pmic.mk b/examples/pmic/pmic.mk new file mode 100644 index 000000000..5ad333d43 --- /dev/null +++ b/examples/pmic/pmic.mk @@ -0,0 +1,113 @@ +# +# Copyright 2026, UNSW +# +# SPDX-License-Identifier: BSD-2-Clause +# +# This Makefile is copied into the build directory +# and operated on from there. +# + +ifeq ($(strip $(MICROKIT_SDK)),) +$(error MICROKIT_SDK must be specified) +endif + +ifeq ($(strip $(TOOLCHAIN)),) + TOOLCHAIN := clang +endif + +PYTHONPATH := ${SDDF}/tools/meta:${PYTHONPATH} +export PYTHONPATH + +SUPPORTED_BOARDS := \ + maaxboard + +include ${SDDF}/tools/make/board/common.mk + +SDDF_CUSTOM_LIBC := 1 +UTIL := $(SDDF)/util +LIBCO := $(SDDF)/libco +TOP := ${SDDF}/examples/pmic +I2C := $(SDDF)/i2c +SERIAL := $(SDDF)/serial +I2C_DRIVER := $(SDDF)/drivers/i2c/${I2C_DRIV_DIR} +TIMER_DRIVER := $(SDDF)/drivers/timer/${TIMER_DRIV_DIR} +SERIAL_DRIVER := $(SDDF)/drivers/serial/${UART_DRIV_DIR} +PMIC_DRIVER := $(SDDF)/drivers/pmic/${PMIC_DRIV_DIR} + +IMAGES := i2c_virt.elf \ + i2c_driver.elf \ + client.elf \ + timer_driver.elf \ + serial_driver.elf \ + serial_virt_tx.elf \ + pmic_driver.elf + +LDFLAGS := -L$(BOARD_DIR)/lib +LIBS := --start-group -lmicrokit -Tmicrokit.ld libsddf_util_debug.a --end-group +CFLAGS += -Wno-unused-function -I${TOP} + +IMAGE_FILE = loader.img +REPORT_FILE = report.txt +SYSTEM_FILE = pmic.system + +DTS := $(SDDF)/dts/$(MICROKIT_BOARD).dts +DTB := $(MICROKIT_BOARD).dtb +METAPROGRAM := $(TOP)/meta.py + +CFLAGS += -I$(BOARD_DIR)/include \ + -I$(SDDF)/include \ + -I$(SDDF)/include/microkit \ + -I$(LIBCO) \ + -DLIBI2C_NOCO \ + -MD \ + -MP + +CLIENT_OBJS := client.o + +VPATH := ${TOP} +all: $(IMAGE_FILE) + +client.o: client.c + +client.elf: $(CLIENT_OBJS) libco.a libsddf_util.a libi2c.a + $(LD) $(LDFLAGS) $^ $(LIBS) -o $@ + +$(SYSTEM_FILE): $(METAPROGRAM) $(IMAGES) $(DTB) + $(PYTHON) $(METAPROGRAM) --sddf $(SDDF) --board $(MICROKIT_BOARD) --dtb $(DTB) --output . --sdf $(SYSTEM_FILE) + $(OBJCOPY) --update-section .device_resources=timer_driver_device_resources.data timer_driver.elf + $(OBJCOPY) --update-section .device_resources=i2c_driver_device_resources.data i2c_driver.elf + $(OBJCOPY) --update-section .device_resources=serial_driver_device_resources.data serial_driver.elf + $(OBJCOPY) --update-section .i2c_driver_config=i2c_driver_i2c_driver_config.data i2c_driver.elf + $(OBJCOPY) --update-section .i2c_virt_config=i2c_virt_i2c_virt_config.data i2c_virt.elf + $(OBJCOPY) --update-section .i2c_client_config=pmic_driver_i2c_client_config.data pmic_driver.elf + $(OBJCOPY) --update-section .timer_client_config=client_timer_client_config.data client.elf + $(OBJCOPY) --update-section .serial_driver_config=serial_driver_serial_driver_config.data serial_driver.elf + $(OBJCOPY) --update-section .serial_virt_tx_config=serial_virt_tx_serial_virt_tx_config.data serial_virt_tx.elf + $(OBJCOPY) --update-section .serial_client_config=client_serial_client_config.data client.elf + $(OBJCOPY) --update-section .pmic_client_config=client_pmic_client_config.data client.elf + $(OBJCOPY) --update-section .pmic_driver_config=pmic_driver_pmic_driver_config.data pmic_driver.elf + + touch $@ + +$(IMAGE_FILE) $(REPORT_FILE): $(IMAGES) $(SYSTEM_FILE) + $(MICROKIT_TOOL) $(SYSTEM_FILE) --search-path $(BUILD_DIR) --board $(MICROKIT_BOARD) --config $(MICROKIT_CONFIG) -o $(IMAGE_FILE) -r $(REPORT_FILE) + +${IMAGES}: libsddf_util_debug.a +.PHONY: all compile clean + +clean:: + rm -f *.elf + find . -name '*.[do]' |xargs --no-run-if-empty rm + +clobber:: clean + rm -f ${REPORT_FILE} ${IMAGE_FILE} *.a .*cflags* + +include ${SDDF}/util/util.mk +include ${I2C}/components/i2c_virt.mk +include ${SERIAL}/components/serial_components.mk +include ${SERIAL_DRIVER}/serial_driver.mk +include ${TIMER_DRIVER}/timer_driver.mk +include ${LIBCO}/libco.mk +include ${I2C_DRIVER}/i2c_driver.mk +include ${PMIC_DRIVER}/pmic_driver.mk +include ${I2C}/libi2c.mk diff --git a/examples/serial/meta.py b/examples/serial/meta.py index 183fd0d40..93b89e152 100644 --- a/examples/serial/meta.py +++ b/examples/serial/meta.py @@ -4,72 +4,35 @@ import argparse from typing import List from dataclasses import dataclass -from sdfgen import SystemDescription, Sddf, DeviceTree +from acacia import System, ProtectionDomain, MemoryRegion, Channel, DeviceTreeBlob +from acacia.arch import x86_64 -sys.path.append( - os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../tools/meta") -) -from board import BOARDS +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../")) +from acacia_sddf import sDDFSerial, BOARDS -ProtectionDomain = SystemDescription.ProtectionDomain +def generate(sdf_file: str, output_dir: str): + client0 = ProtectionDomain(sdf, "client0", "client0.elf", priority=1) + client1 = ProtectionDomain(sdf, "client1", "client1.elf", priority=1) -def generate(sdf_file: str, output_dir: str, dtb: DeviceTree): - serial_driver = ProtectionDomain("serial_driver", "serial_driver.elf", priority=200) - serial_virt_tx = ProtectionDomain( - "serial_virt_tx", "serial_virt_tx.elf", priority=199 - ) - serial_virt_rx = ProtectionDomain( - "serial_virt_rx", "serial_virt_rx.elf", priority=199 - ) - - if board.arch == SystemDescription.Arch.X86_64: - serial_port = SystemDescription.IoPort(0x3F8, 8, 0) - serial_driver.add_ioport(serial_port) - - # The serial device does not located on PCIe and the interrupts are - # conventionally configured by BIOS. The IRQ number can be read from - # Linux or APCI tables. - serial_irq = SystemDescription.IrqIoapic(0, 4, 0, id=1) - serial_driver.add_irq(serial_irq) - - client0 = ProtectionDomain("client0", "client0.elf", priority=1) - client1 = ProtectionDomain("client1", "client1.elf", priority=1) - - serial_node = None - if dtb is not None: - serial_node = dtb.node(board.serial) - assert serial_node is not None - - baud_rate = board.baud_rate - - serial_system = Sddf.Serial( + serial = sDDFSerial( sdf, - serial_node, - serial_driver, - serial_virt_tx, - virt_rx=serial_virt_rx, + board.serial.compatible, + board.serial.node_path, + driver_prio=200, + virt_tx_prio=199, + allow_rx=True, enable_color=True, - baud_rate=baud_rate, + baud_rate=board.baud_rate if board.baud_rate else 115200, ) - serial_system.add_client(client0) - serial_system.add_client(client1) - - pds = [ - serial_driver, - serial_virt_tx, - serial_virt_rx, - client0, - client1, - ] - for pd in pds: - sdf.add_pd(pd) - assert serial_system.connect() - assert serial_system.serialise_config(output_dir) + for pd in [client0, client1]: + serial.add_client(pd) - with open(f"{output_dir}/{sdf_file}", "w+") as f: - f.write(sdf.render()) + sdf.make_config_structs() + out_file = f"{output_dir}/{sdf_file}" + print(f"Saving to {out_file}") + sdf.write_xml_file(out_file) if __name__ == "__main__": @@ -83,13 +46,10 @@ def generate(sdf_file: str, output_dir: str, dtb: DeviceTree): args = parser.parse_args() board = next(filter(lambda b: b.name == args.board, BOARDS)) + if board.arch != x86_64: + dtb = DeviceTreeBlob(args.dtb) + else: + dtb = None + sdf = System(board.arch, board.paddr_top, dtb) - sdf = SystemDescription(board.arch, board.paddr_top) - sddf = Sddf(args.sddf) - - dtb = None - if board.arch != SystemDescription.Arch.X86_64: - with open(args.dtb, "rb") as f: - dtb = DeviceTree(f.read()) - - generate(args.sdf, args.output, dtb) + generate(args.sdf, args.output) diff --git a/examples/serial/serial.mk b/examples/serial/serial.mk index bdf307155..a1e896a60 100644 --- a/examples/serial/serial.mk +++ b/examples/serial/serial.mk @@ -85,11 +85,11 @@ else $(PYTHON) $(METAPROGRAM) --sddf $(SDDF) --board $(MICROKIT_BOARD) --output . --sdf $(SYSTEM_FILE) endif $(OBJCOPY) --update-section .device_resources=serial_driver_device_resources.data serial_driver.elf - $(OBJCOPY) --update-section .serial_driver_config=serial_driver_config.data serial_driver.elf - $(OBJCOPY) --update-section .serial_virt_rx_config=serial_virt_rx.data serial_virt_rx.elf - $(OBJCOPY) --update-section .serial_virt_tx_config=serial_virt_tx.data serial_virt_tx.elf - $(OBJCOPY) --update-section .serial_client_config=serial_client_client0.data client0.elf - $(OBJCOPY) --update-section .serial_client_config=serial_client_client1.data client1.elf + $(OBJCOPY) --update-section .serial_driver_config=serial_driver_serial_driver_config.data serial_driver.elf + $(OBJCOPY) --update-section .serial_virt_rx_config=serial_virt_rx_serial_virt_rx_config.data serial_virt_rx.elf + $(OBJCOPY) --update-section .serial_virt_tx_config=serial_virt_tx_serial_virt_tx_config.data serial_virt_tx.elf + $(OBJCOPY) --update-section .serial_client_config=client0_serial_client_config.data client0.elf + $(OBJCOPY) --update-section .serial_client_config=client1_serial_client_config.data client1.elf touch $@ $(IMAGE_FILE) $(REPORT_FILE): $(IMAGES) $(SYSTEM_FILE) diff --git a/examples/timer/meta.py b/examples/timer/meta.py index a823628b6..73880ac97 100644 --- a/examples/timer/meta.py +++ b/examples/timer/meta.py @@ -3,43 +3,30 @@ import os import sys import argparse -from sdfgen import SystemDescription, Sddf, DeviceTree import importlib - -sys.path.append( - os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../tools/meta") -) +from acacia import System, ProtectionDomain, MemoryRegion, Channel, DeviceTreeBlob +from acacia.arch import x86_64 # Use importlib to dynamically load. Using `from` import below other code is bad style. -board_module = importlib.import_module("board") -BOARDS = board_module.BOARDS - -ProtectionDomain = SystemDescription.ProtectionDomain +# board_module = importlib.import_module("board") +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../..")) +from acacia_sddf import BOARDS, sDDFTimer -def generate(sdf_file: str, output_dir: str, dtb: DeviceTree): - timer_node = None - timer_driver = ProtectionDomain("timer_driver", "timer_driver.elf", priority=253) - client = ProtectionDomain("client", "client.elf", priority=1) - - if board.arch == SystemDescription.Arch.X86_64: - board_module.add_x86_hpet(sdf, timer_driver) - else: - timer_node = dtb.node(board.timer) - assert timer_node is not None +def generate(sdf_file: str, output_dir: str): + client = ProtectionDomain(sdf, "client", "client.elf", priority=1) - timer_system = Sddf.Timer(sdf, timer_node, timer_driver) - timer_system.add_client(client) + timer = sDDFTimer(sdf, board.timer.compatible, board.timer.node_path) + timer.add_client(client) - pds = [timer_driver, client] - for pd in pds: - sdf.add_pd(pd) + # Add HPET if x86 + if board.arch == x86_64: + timer.add_x86_hpet(sdf) - assert timer_system.connect() - assert timer_system.serialise_config(output_dir) - - with open(f"{output_dir}/{sdf_file}", "w+") as f: - f.write(sdf.render()) + sdf.make_config_structs() + out_file = f"{output_dir}/{sdf_file}" + print(f"Saving to {out_file}") + sdf.write_xml_file(out_file) if __name__ == "__main__": @@ -54,12 +41,10 @@ def generate(sdf_file: str, output_dir: str, dtb: DeviceTree): board = next(filter(lambda b: b.name == args.board, BOARDS)) - sdf = SystemDescription(board.arch, board.paddr_top) - sddf = Sddf(args.sddf) - - dtb = None - if board.arch != SystemDescription.Arch.X86_64: - with open(args.dtb, "rb") as f: - dtb = DeviceTree(f.read()) + if board.arch != x86_64: + dtb = DeviceTreeBlob(args.dtb) + else: + dtb = None + sdf = System(board.arch, board.paddr_top, dtb) - generate(args.sdf, args.output, dtb) + generate(args.sdf, args.output) diff --git a/examples/timer/timer.mk b/examples/timer/timer.mk index 42a4c9523..33e682665 100644 --- a/examples/timer/timer.mk +++ b/examples/timer/timer.mk @@ -80,7 +80,7 @@ else $(PYTHON) $(METAPROGRAM) --sddf $(SDDF) --board $(MICROKIT_BOARD) --output . --sdf $(SYSTEM_FILE) endif $(OBJCOPY) --update-section .device_resources=timer_driver_device_resources.data timer_driver.elf - $(OBJCOPY) --update-section .timer_client_config=timer_client_client.data client.elf + $(OBJCOPY) --update-section .timer_client_config=client_timer_client_config.data client.elf touch $@ $(IMAGE_FILE) $(REPORT_FILE): $(SYSTEM_FILE) diff --git a/i2c/libi2c.c b/i2c/libi2c.c index f6713c847..4d61b0a62 100644 --- a/i2c/libi2c.c +++ b/i2c/libi2c.c @@ -33,6 +33,7 @@ static inline int check_data_buf(void *data_buf) return 0; } +#ifndef LIBI2C_NOCO /** * Block on a notification using libco or libmicrokitco for blocking calls. */ @@ -48,6 +49,7 @@ static void __i2c_block(libi2c_conf_t *conf) microkit_cothread_wait_on_channel(i2c_config.virt.id); #endif } +#endif /** * Given a buffer pointer from the DATA region, create an I2C op, dispatch and return when @@ -130,6 +132,7 @@ static i2c_err_t __i2c_handle_response(libi2c_conf_t *conf, i2c_addr_t *returned // #### Blocking API using libco/libmicrokitco #### +#ifndef LIBI2C_NOCO /** * Perform a simple I2C write given a DATA region buffer containing data. * To perform a write to a device register, ensure the FIRST byte of write_buf contains @@ -215,6 +218,7 @@ int sddf_i2c_dispatch(libi2c_conf_t *conf, i2c_addr_t address, void *buf, uint16 assert(returned_addr == address); return err; } +#endif // #### Non-blocking API #### /** diff --git a/include/sddf/i2c/libi2c.h b/include/sddf/i2c/libi2c.h index 9baccb0eb..6bc054c8a 100644 --- a/include/sddf/i2c/libi2c.h +++ b/include/sddf/i2c/libi2c.h @@ -12,6 +12,11 @@ // necessary in most cases! If your usage requires more commands per request, do not use // this library and instead implement direct calls to the protocol in // +// Three operating modes are supplied based on coroutine support: +// a. libmicrokitco: use libmicrokitco.h for blocking API +// b. libi2c_raw: use raw libco for blocking API +// c. libi2c_noco: blocking API fully disabled for use in drivers +// // See i2c/queue.h for details about the I2C transport layer. #pragma once @@ -19,6 +24,7 @@ #include #include #include +#ifndef LIBI2C_NOCO #ifdef LIBI2C_RAW #include // Client must define and set up these cothreads for this interface to function. @@ -27,6 +33,7 @@ extern cothread_t t_main; #else #include #endif +#endif // Client must define this. E.g. // __attribute__((__section__(".i2c_client_config"))) i2c_client_config_t i2c_config; @@ -56,11 +63,13 @@ typedef struct libi2c_conf { int libi2c_init(libi2c_conf_t *conf_struct, i2c_queue_handle_t *queue_handle); +#ifndef LIBI2C_NOCO // Blocking interface int sddf_i2c_write(libi2c_conf_t *conf, i2c_addr_t address, void *write_buf, uint16_t len); int sddf_i2c_read(libi2c_conf_t *conf, i2c_addr_t address, void *read_buf, uint16_t len); int sddf_i2c_writeread(libi2c_conf_t *conf, i2c_addr_t address, i2c_addr_t reg_address, void *read_buf, uint16_t len); int sddf_i2c_dispatch(libi2c_conf_t *conf, i2c_addr_t address, void *buf, uint16_t len, uint8_t flag_mask); +#endif // Non-blocking interface. Separates dispatch and completion for applications // with custom concurrency models instead of libco/libmicrokitco (e.g. micropython) diff --git a/include/sddf/pmic/bd71837amwv-bindings.h b/include/sddf/pmic/bd71837amwv-bindings.h new file mode 100644 index 000000000..be4cd20b1 --- /dev/null +++ b/include/sddf/pmic/bd71837amwv-bindings.h @@ -0,0 +1,44 @@ +/* + * Copyright 2026, UNSW + * SPDX-License-Identifier: BSD-2-Clause + */ + +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* Copyright (C) 2018 ROHM Semiconductors */ +// (for snippets derived from rohm-bd718x7.h in Linux kernel) + +typedef enum { + BD718XX_BUCK1 = 0, + BD718XX_BUCK2, + BD718XX_BUCK3, + BD718XX_BUCK4, + BD718XX_BUCK5, + BD718XX_BUCK6, + BD718XX_BUCK7, + BD718XX_BUCK8, + BD718XX_LDO1, + BD718XX_LDO2, + BD718XX_LDO3, + BD718XX_LDO4, + BD718XX_LDO5, + BD718XX_LDO6, + BD718XX_LDO7, + BD718XX_REGULATOR_AMOUNT, +} bd718xx_regulators; + +/* Common voltage configurations */ +#define BD718XX_DVS_BUCK_VOLTAGE_NUM 0x3D +#define BD718XX_4TH_NODVS_BUCK_VOLTAGE_NUM 0x3D + +#define BD718XX_LDO1_VOLTAGE_NUM 0x08 +#define BD718XX_LDO2_VOLTAGE_NUM 0x02 +#define BD718XX_LDO3_VOLTAGE_NUM 0x10 +#define BD718XX_LDO4_VOLTAGE_NUM 0x0A +#define BD718XX_LDO6_VOLTAGE_NUM 0x0A + +/* BD71837 specific voltage configurations */ +#define BD71837_BUCK5_VOLTAGE_NUM 0x10 +#define BD71837_BUCK6_VOLTAGE_NUM 0x04 +#define BD71837_BUCK7_VOLTAGE_NUM 0x08 +#define BD71837_LDO5_VOLTAGE_NUM 0x10 +#define BD71837_LDO7_VOLTAGE_NUM 0x10 diff --git a/include/sddf/pmic/client.h b/include/sddf/pmic/client.h new file mode 100644 index 000000000..fee93a041 --- /dev/null +++ b/include/sddf/pmic/client.h @@ -0,0 +1,110 @@ +/* + * Copyright 2026, UNSW + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +/** + * Enable a given regulator via PPC to the passive PMIC driver. + * @param channel of PMIC driver. + * @param reg_id identifier of target regulator. + * @return 0 on success, nonzero on failure. + */ +static inline int sddf_pmic_enable_reg(microkit_channel channel, uint32_t reg_id) +{ + microkit_msginfo msginfo = microkit_msginfo_new(SDDF_PMIC_ENABLE_REG, 1); + microkit_mr_set(SDDF_PMIC_ENABLE_REG_REG_ID, reg_id); + + msginfo = microkit_ppcall(channel, msginfo); + + return (int)microkit_msginfo_get_label(msginfo); +} + +/** + * Disable a given regulator via PPC to the passive PMIC driver. + * @param channel of PMIC driver. + * @param reg_id identifier of target regulator. + * @return 0 on success, nonzero on failure. + */ +static inline int sddf_pmic_disable_reg(microkit_channel channel, uint32_t reg_id) +{ + microkit_msginfo msginfo = microkit_msginfo_new(SDDF_PMIC_DISABLE_REG, 1); + microkit_mr_set(SDDF_PMIC_DISABLE_REG_REG_ID, reg_id); + + msginfo = microkit_ppcall(channel, msginfo); + + return (int)microkit_msginfo_get_label(msginfo); +} + +/** + * Set the voltage output level for a regulator via PPC to the passive PMIC driver. + * @param channel of PMIC driver. + * @param reg_id identifier of target regulator. + * @param voltage_uv target voltage in microvolts. + * @return 0 on success, 1 if regulator invalid, 2 if voltage setting invalid. + */ +static inline int sddf_pmic_set_vout(microkit_channel channel, uint32_t reg_id, uint64_t voltage_uv) +{ + microkit_msginfo msginfo = microkit_msginfo_new(SDDF_PMIC_SET_VOUT, 3); + microkit_mr_set(SDDF_PMIC_SET_VOUT_REG_ID, reg_id); + microkit_mr_set(SDDF_PMIC_SET_VOUT_VOLTAGE_UV, voltage_uv); + // microkit_mr_set(SDDF_PMIC_SET_VOUT_OP_MODE_ID, op_mode_id); + + msginfo = microkit_ppcall(channel, msginfo); + + return (int)microkit_msginfo_get_label(msginfo); +} + +/** + * Set the current limit for a regulator via PPC to the passive PMIC driver. + * @param channel of PMIC driver. + * @param reg_id identifier of target regulator. + * @param current_ua target current limit in microamps. + * @return 0 on success, 1 if regulator invalid, 2 if current setting invalid. + */ +static inline int sddf_pmic_set_climit(microkit_channel channel, uint32_t reg_id, uint64_t current_ua) +{ + microkit_msginfo msginfo = microkit_msginfo_new(SDDF_PMIC_SET_CLIMIT, 3); + microkit_mr_set(SDDF_PMIC_SET_CLIMIT_REG_ID, reg_id); + microkit_mr_set(SDDF_PMIC_SET_CLIMIT_CURRENT_UA, current_ua); + // microkit_mr_set(SDDF_PMIC_SET_CLIMIT_OP_MODE_ID, op_mode_id); + + msginfo = microkit_ppcall(channel, msginfo); + + return (int)microkit_msginfo_get_label(msginfo); +} + +/** + * Get information about a regulator via PPC to the passive PMIC driver. + * @param channel of PMIC driver. + * @param reg_id identifier of target regulator. + * @param info pointer to structure to populate with regulator information. + * @return 0 on success, 1 if regulator invalid. + */ +static inline int sddf_pmic_get_reg_info(microkit_channel channel, uint32_t reg_id, sddf_pmic_reg_info_t *info) +{ + microkit_msginfo msginfo = microkit_msginfo_new(SDDF_PMIC_GET_REG_INFO, 1); + microkit_mr_set(SDDF_PMIC_GET_REG_INFO_REG_ID, reg_id); + + msginfo = microkit_ppcall(channel, msginfo); + + int ret = (int)microkit_msginfo_get_label(msginfo); + if (ret == SDDF_PMIC_GET_REG_INFO_SUCCESS) { + info->enabled = microkit_mr_get(SDDF_PMIC_GET_REG_INFO_ENABLED); + info->voltage_uv = microkit_mr_get(SDDF_PMIC_GET_REG_INFO_VOLTAGE_UV); + info->current_ua = microkit_mr_get(SDDF_PMIC_GET_REG_INFO_CURRENT_UA); + info->min_voltage_uv = microkit_mr_get(SDDF_PMIC_GET_REG_INFO_MIN_VOLTAGE_UV); + info->max_voltage_uv = microkit_mr_get(SDDF_PMIC_GET_REG_INFO_MAX_VOLTAGE_UV); + info->min_current_ua = microkit_mr_get(SDDF_PMIC_GET_REG_INFO_MIN_CURRENT_UA); + info->max_current_ua = microkit_mr_get(SDDF_PMIC_GET_REG_INFO_MAX_CURRENT_UA); + info->ramprate = microkit_mr_get(SDDF_PMIC_GET_REG_INFO_RAMPRATE); + } + + return ret; +} diff --git a/include/sddf/pmic/driver.h b/include/sddf/pmic/driver.h new file mode 100644 index 000000000..3cd669b8a --- /dev/null +++ b/include/sddf/pmic/driver.h @@ -0,0 +1,95 @@ +/* + * Copyright 2026, UNSW + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +// This header file defines the state machine used by the driver to function. +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG_PMIC_DRIVER +#ifdef DEBUG_PMIC_DRIVER +#define LOG_PMIC_DRIVER(...) do{ sddf_dprintf("PMIC DRIVER|INFO: "); sddf_dprintf(__VA_ARGS__); }while(0) +#else +#define LOG_PMIC_DRIVER(...) do{}while(0) +#endif + +#define LOG_PMIC_DRIVER_ERR(...) do{ sddf_dprintf("PMIC DRIVER|ERROR: "); sddf_dprintf(__VA_ARGS__); }while(0) + +// Utility types +typedef uint32_t pmic_unit_t; +typedef uint32_t pmic_reg_id_t; + +typedef struct unit_capability { + bool adjustable; + pmic_unit_t min_value; + pmic_unit_t max_value; + pmic_unit_t quantisation; // Number of bits used to parameterise value in PMIC - i.e. conf reg size +} unit_capability_t; + +typedef struct reg_capabilities { + unit_capability_t voltage; + unit_capability_t current; + bool toggleable; // Can this regulator be turned on and off? +} reg_capabilities_t; + +typedef struct sddf_regulator { + reg_capabilities_t capabilities; + pmic_unit_t voltage_uv; + pmic_unit_t current_ua; + bool enabled; +} sddf_regulator_t; + +typedef struct pmic_driver_state { + microkit_channel curr_client; + uint64_t curr_ppc_op; + uint64_t err; + uint64_t get_info_cnt; // Progress through get info call. Easier to count than add N states! +} pmic_driver_state_t; + +// typedef enum { S_IDLE, S_REQ_RET, NUM_STATES } pmic_state_t; + +// State machine: +// Idle: sleeping +// Request: a PPC has arrived and we've dispatched an I2C command to handle it +// This state is IMPLICIT. We are here by merit of a PPC arriving. +// // Request return: notification has arrived from I2C, finish PPC and go to sleep. +// // Invariant: one I2C request per PPC only, allowing state machine to be simple. +// +// typedef struct fsm { +// pmic_state_t curr_state; +// pmic_state_t next_state; +// bool yield; // fsm funcs can set this to tell the FSM loop to allow the PD to sleep +// } fsm_data_t; +// +// // Each state implements a single state function which is called by the FSM. +// typedef void pmic_state_func_t(fsm_data_t *fsm, pmic_driver_state_t *state); +// +// Prototype for FSM function +// void fsm(fsm_data_t *f); +// +// void state_idle(fsm_data_t *fsm, pmic_driver_state_t *state); +// void state_req(fsm_data_t *fsm, pmic_driver_state_t *state); +// void state_req_return(fsm_data_t *fsm, pmic_driver_state_t *state); + +// PPC handlers +sddf_pmic_err_t pmic_drv_enable_reg(uint64_t reg_id); +sddf_pmic_err_t pmic_drv_disable_reg(uint64_t reg_id); +sddf_pmic_err_t pmic_drv_set_vout(uint64_t reg_id, uint64_t voltage_uv); +sddf_pmic_err_t pmic_drv_set_climit(uint64_t reg_id, uint64_t current_ua); +sddf_pmic_err_t pmic_drv_get_info(uint64_t reg_id, sddf_pmic_reg_info_t *info); + +static void pmic_reset_state(pmic_driver_state_t *s) +{ + memset(s, 0, sizeof(pmic_driver_state_t)); + s->curr_ppc_op = SDDF_PMIC_PPC_INVALID; + // HACK: sentinel value due to HACK required for i2c-based pmics. +} diff --git a/include/sddf/pmic/protocol.h b/include/sddf/pmic/protocol.h new file mode 100644 index 000000000..3c779a6bb --- /dev/null +++ b/include/sddf/pmic/protocol.h @@ -0,0 +1,120 @@ +/* + * Copyright 2026, UNSW + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once +#include + +// PPC interface for PMIC device class +// DANGER: current PMIC class is DUMB. It will not automatically solve constraints, +// each operation here is a best-effort direct operation on hardware. + +typedef enum { + SDDF_PMIC_ENABLE_REG = 0U, + SDDF_PMIC_DISABLE_REG, + SDDF_PMIC_SET_VOUT, + SDDF_PMIC_SET_CLIMIT, + SDDF_PMIC_GET_REG_INFO, + SDDF_PMIC_PPC_INVALID +} sddf_pmic_ppc_codes_t; + +typedef enum { + SDDF_PMIC_ERR_OK = 0U, + SDDF_PMIC_ERR_FAIL_REG, + SDDF_PMIC_ERR_BAD_SETTING, + SDDF_PMIC_ERR_NOT_IMPLEMENTED, + SDDF_PMIC_ERR_BAD_PPC_CALL, + SDDF_PMIC_ERR_BUSY, + SDDF_PMIC_ERR_OTHER, +} sddf_pmic_err_t; + +typedef struct { + uint64_t enabled; + uint64_t voltage_uv; + uint64_t current_ua; + uint64_t min_voltage_uv; + uint64_t max_voltage_uv; + uint64_t min_current_ua; + uint64_t max_current_ua; + uint64_t ramprate; +} sddf_pmic_reg_info_t; + +// ## PPCs ## +// SDDF_PMIC_ENABLE_REG +// Enable a given regulator +// Args: +// MR0: reg_id (defined per device, in bindings file) +// Returns: +// MR0: 0 on success, nonzero on failure +#define SDDF_PMIC_ENABLE_REG_REG_ID (0) +#define SDDF_PMIC_ENABLE_REG_SUCCESS (0) +#define SDDF_PMIC_ENABLE_REG_FAIL (1) + +// SDDF_PMIC_DISABLE_REG +// Disable a given regulator +// Args: +// MR0: reg_id +// Returns: +// MR0: 0 on success, nonzero on failure +#define SDDF_PMIC_DISABLE_REG_REG_ID (0) +#define SDDF_PMIC_DISABLE_REG_SUCCESS (0) +#define SDDF_PMIC_DISABLE_REG_FAIL (1) + +// SDDF_PMIC_SET_VOUT +// Set the voltage output level for a regulator, if that regulator has adjustable voltage. +// Args: +// MR0: reg_id +// MR1: voltage (microvolts) +// MR2: operating mode id (defined per regulator, per device, in bindings file) +// Returns: +// MR0: 0 on success, 1 if regulator invalid, 2 if voltage setting invalid for valid reg. +// NOTE: if a regulator doesn't support voltage setting, error 1 is returned. +#define SDDF_PMIC_SET_VOUT_REG_ID (0) +#define SDDF_PMIC_SET_VOUT_VOLTAGE_UV (1) +#define SDDF_PMIC_SET_VOUT_OP_MODE_ID (2) +#define SDDF_PMIC_SET_VOUT_SUCCESS (0) +#define SDDF_PMIC_SET_VOUT_FAIL_BADREG (1) +#define SDDF_PMIC_SET_VOUT_FAIL_BADVOLTAGE (2) + +// SDDF_PMIC_SET_CLIMIT +// Set the current limit of a regulator, if that regulator has a current limit setting. +// Args: +// MR0: reg_id +// MR1: current (uA) +// MR2: operating mode id (defined per regulator, per device, in bindings file) +// Returns: +// MR0: 0 on success, 1 if regulator invalid, 2 if current setting invalid for valid reg. +// NOTE: if a regulator doesn't support current setting, error 1 is returned. +#define SDDF_PMIC_SET_CLIMIT_REG_ID (0) +#define SDDF_PMIC_SET_CLIMIT_CURRENT_UA (1) +#define SDDF_PMIC_SET_CLIMIT_OP_MODE_ID (2) +#define SDDF_PMIC_SET_CLIMIT_SUCCESS (0) +#define SDDF_PMIC_SET_CLIMIT_FAIL_BADREG (1) +#define SDDF_PMIC_SET_CLIMIT_FAIL_BADCURRENT (2) + +// SDDF_PMIC_GET_REG_INFO +// Get information about a regulator, including the current state as well as capabilities. +// Args: +// MR0: reg_id +// Returns: +// MR0: 0 on success, 1 if invalid. +// MR1: enabled +// MR2: current voltage (microvolts) +// MR3: current current (uA) +// MR4: min voltage (uV) +// MR5: max voltage (uV) +// MR6: min current (uA) +// MR7: max current (uA) +// MR8: ramprate (NOT IMPLEMENTED) +#define SDDF_PMIC_GET_REG_INFO_REG_ID (0) +#define SDDF_PMIC_GET_REG_INFO_SUCCESS (0) +#define SDDF_PMIC_GET_REG_INFO_FAIL_INVALID (1) +#define SDDF_PMIC_GET_REG_INFO_ENABLED (1) +#define SDDF_PMIC_GET_REG_INFO_VOLTAGE_UV (2) +#define SDDF_PMIC_GET_REG_INFO_CURRENT_UA (3) +#define SDDF_PMIC_GET_REG_INFO_MIN_VOLTAGE_UV (4) +#define SDDF_PMIC_GET_REG_INFO_MAX_VOLTAGE_UV (5) +#define SDDF_PMIC_GET_REG_INFO_MIN_CURRENT_UA (6) +#define SDDF_PMIC_GET_REG_INFO_MAX_CURRENT_UA (7) +#define SDDF_PMIC_GET_REG_INFO_RAMPRATE (8) diff --git a/tools/make/board/maaxboard.mk b/tools/make/board/maaxboard.mk index 5c9372dfb..863f5efb8 100644 --- a/tools/make/board/maaxboard.mk +++ b/tools/make/board/maaxboard.mk @@ -13,5 +13,6 @@ NET_DRIV_DIR := ${PLATFORM} ETH_DRIV := eth_driver_${PLATFORM}.elf TIMER_DRIV_DIR := ${PLATFORM} UART_DRIV_DIR := ${PLATFORM} +PMIC_DRIV_DIR := bd71837amwv CPU := cortex-a53 diff --git a/tools/meta/board.py b/tools/meta/board.py deleted file mode 100644 index fba22a67a..000000000 --- a/tools/meta/board.py +++ /dev/null @@ -1,211 +0,0 @@ -# Copyright 2025, UNSW -# SPDX-License-Identifier: BSD-2-Clause -from dataclasses import dataclass -from typing import List, Optional -from sdfgen import SystemDescription -from importlib.metadata import version - -ProtectionDomain = SystemDescription.ProtectionDomain - -# This file is imported by most of our meta.py scripts, so add this check -# here so that we can catch this error consistently. -assert version("sdfgen").split(".")[1] == "33", "Unexpected sdfgen version" - - -def add_x86_hpet(sdf: SystemDescription, timer_driver: ProtectionDomain): - # Timer IRQ must be the highest priority (highest vector) to ensure they are delivered - # as close as possible to the timer expiry. The highest vector is defined by (irq_user_max - irq_user_min) in seL4 source - # Since our HPET driver uses legacy IRQ routing, comparator 0's IRQ will always arrives at - # I/O APIC 0's pin 2. - hpet_irq = SystemDescription.IrqIoapic( - ioapic_id=0, - pin=2, - vector=107, - id=0, - trigger=SystemDescription.IrqIoapic.Trigger.EDGE, - ) - timer_driver.add_irq(hpet_irq) - - # paddr=0xFED00000 is a x86 convention for HPET, though it may be different on some machines depending on their BIOS. - hpet_regs = SystemDescription.MemoryRegion( - sdf, "hpet_regs", 0x1000, paddr=0xFED00000 - ) - hpet_regs_map = SystemDescription.Map(hpet_regs, 0x5000_0000, "rw", cached=False) - timer_driver.add_map(hpet_regs_map) - sdf.add_mr(hpet_regs) - - -@dataclass -class Board: - name: str - arch: SystemDescription.Arch - paddr_top: int - serial: Optional[str] = None - ethernet: Optional[str] = None - timer: Optional[str] = None - i2c: Optional[str] = None - partition: int = 0 - blk: Optional[str] = None - baud_rate: Optional[int] = None - - -# Keep this list in alphabetical order by board name -# TODO: convert to Dictionary -BOARDS: List[Board] = [ - Board( - name="cheshire", - arch=SystemDescription.Arch.RISCV64, - paddr_top=0x90000000, - serial="soc/serial@3002000", - i2c="soc/i2c@3003000", - ), - Board( - name="hifive_p550", - arch=SystemDescription.Arch.RISCV64, - paddr_top=0xA0000000, - serial="soc/serial@0x50900000", - ), - Board( - name="imx8mm_evk", - arch=SystemDescription.Arch.AARCH64, - paddr_top=0x70000000, - serial="soc@0/bus@30800000/spba-bus@30800000/serial@30890000", - timer="soc@0/bus@30000000/timer@302d0000", - ethernet="soc@0/bus@30800000/ethernet@30be0000", - ), - Board( - name="imx8mp_evk", - arch=SystemDescription.Arch.AARCH64, - paddr_top=0x70000000, - serial="soc@0/bus@30800000/spba-bus@30800000/serial@30890000", - timer="soc@0/bus@30000000/timer@302d0000", - ethernet="soc@0/bus@30800000/ethernet@30bf0000", - ), - Board( - name="imx8mp_iotgate", - arch=SystemDescription.Arch.AARCH64, - paddr_top=0x70000000, - serial="soc@0/bus@30800000/serial@30890000", - timer="soc@0/bus@30000000/timer@302d0000", - ethernet="soc@0/bus@30800000/ethernet@30bf0000", - ), - Board( - name="imx8mq_evk", - arch=SystemDescription.Arch.AARCH64, - paddr_top=0x70000000, - serial="soc@0/bus@30800000/serial@30860000", - timer="soc@0/bus@30000000/timer@302d0000", - ethernet="soc@0/bus@30800000/ethernet@30be0000", - ), - Board( - name="kria_k26", - arch=SystemDescription.Arch.AARCH64, - paddr_top=0x70000000, - timer="axi/timer@ff140000", - serial="axi/serial@ff010000", - ethernet="axi/ethernet@ff0e0000", - ), - Board( - name="maaxboard", - arch=SystemDescription.Arch.AARCH64, - paddr_top=0x70000000, - serial="soc@0/bus@30800000/serial@30860000", - timer="soc@0/bus@30000000/timer@302d0000", - ethernet="soc@0/bus@30800000/ethernet@30be0000", - blk="soc@0/bus@30800000/mmc@30b40000", - partition=2, - ), - Board( - name="odroidc2", - arch=SystemDescription.Arch.AARCH64, - paddr_top=0x60000000, - serial="soc/bus@c8100000/serial@4c0", - timer="soc/bus@c1100000/watchdog@98d0", - ethernet="soc/ethernet@c9410000", - ), - Board( - name="odroidc4", - arch=SystemDescription.Arch.AARCH64, - paddr_top=0x60000000, - i2c="soc/bus@ffd00000/i2c@1d000", - serial="soc/bus@ff800000/serial@3000", - timer="soc/bus@ffd00000/watchdog@f0d0", - ethernet="soc/ethernet@ff3f0000", - ), - Board( - name="qemu_virt_aarch64", - arch=SystemDescription.Arch.AARCH64, - paddr_top=0x6_0000_000, - serial="pl011@9000000", - timer="timer", - blk="virtio_mmio@a000200", - ethernet="virtio_mmio@a000000", - i2c=None, - ), - Board( - name="qemu_virt_riscv64", - arch=SystemDescription.Arch.RISCV64, - paddr_top=0xA_0000_000, - serial="soc/serial@10000000", - timer="soc/rtc@101000", - ethernet="soc/virtio_mmio@10001000", - blk="soc/virtio_mmio@10002000", - partition=0, - i2c=None, - ), - Board( - name="rock3b", - arch=SystemDescription.Arch.AARCH64, - paddr_top=0xEC000000, - serial="serial@fe660000", - timer="rktimer@fe5f0000", - ethernet="ethernet@fe2a0000", - baud_rate=1500000, - ), - Board( - name="rpi4b_1gb", - arch=SystemDescription.Arch.AARCH64, - paddr_top=0x2_000_000, - serial="soc/serial@7e215040", - timer="soc/timer@7e003000", - ethernet="scb/ethernet@7d580000", - ), - Board( - name="serengeti", - arch=SystemDescription.Arch.RISCV64, - paddr_top=0x90000000, - serial="soc/serial@3002000", - timer="soc/timer@300B000", - i2c="soc/i2c@3003000", - ), - Board( - name="star64", - arch=SystemDescription.Arch.RISCV64, - paddr_top=0x100000000, - serial="soc/serial@10000000", - timer="soc/timer@13050000", - ethernet="soc/ethernet@16030000", - ), - Board( - name="zcu102", - arch=SystemDescription.Arch.AARCH64, - paddr_top=0x80000000, - timer="axi/timer@ff140000", - serial="axi/serial@ff000000", - ethernet="axi/ethernet@ff0e0000", - ), - Board( - name="x86_64_generic", - arch=SystemDescription.Arch.X86_64, - paddr_top=0x70000000, - timer=None, - serial=None, - ), - Board( - name="x86_64_generic_vtx", - arch=SystemDescription.Arch.X86_64, - paddr_top=0x7FFDF000, - timer=None, - serial=None, - ), -]