# psoc-rs architectural overview

This document describes the structure and organization of the psoc-rs project and its approach to driver reuse and versioning.

The main goal is to support most devices in the PSOC family without code duplication or a lot of tedious manual work to define devices. Some key definitions:

- **Die**: A family of devices that share a common register map and maximum supported feature set.
- **Feature**: A configuration parameter for a die. A feature can be a boolean indicating the presence of some specific IP (with an optional version), or a key-value pair.
- **Device**: A chip, identified by its part number (e.g. `PSC3M5FDS2AFQ1`). A device is based on a die and exposes some subset of its features (depending on variant and pin package).

## Project structure

The project is divided into the following crates:

- `pacs/`: Peripheral Access Crates, generated by [svd2pac](https://github.com/Infineon/svd2pac). Each PAC defines the register map of one die.
- `drivers/`: Drivers for various IP blocks. The drivers crate can be used with any PAC (selected by a feature flag) and expose all IP supported by the die (regardless of whether it's usable on a specific device).
- The root crate `psoc` provides feature flags allowing the user to select their device, and re-exports the drivers available on that device. It also defines device-specific types and traits, such as GPIO pin routings. Most of this crate is auto-generated by the build system.

The following crates are used for compile-time code generation:
- `devices/`: Infrastructure for determining features based on the selected device and die. Also contains JSON data files listing information about all supported devices.
- `macros/`: Procedural macros used by drivers, including:
  - `#[optional_interrupt]`/`require_interrupt!` for allowing drivers to optionally provide interrupt handlers
  - `hsiom!` for defining GPIO pin routing configurations based on the selected device

The following directories contain supplemental code and data:
- `boot/`: Boot images for multi-core devices (see below).
- `docs/`: Developer documentation.
- `ld/`: Linker script templates.
- `scripts/`: Utility scripts for assisting with development.
  - `check_all.rs`: Runs `cargo check` on all supported dies (or all devices, if the `--all` flag is used), and builds all the examples on all supported BSPs.
- `utils/`: Device-agnostic utility types and data structures

## Driver versioning and reuse

Drivers should conditionalize based on feature version information rather than on platform, die, part number, or any other product-specific information. Drivers should present a compatible interface across all supported versions of the IP as long as the underlying functionality or capabilities match.

Drivers implement the maximum capabilities supported by the die; the root `psoc` crate will re-export only the drivers that are actually usable on a given device. For example, a die may define 16 serial communication blocks, but a specific device may not have pins connected to all of them. This is not the concern of the driver, which works with any SCB instance number; rather, the `psoc` crate will only re-export the usable SCB instances for the selected device.

For this reason, the `drivers` crate does not provide a safe API to instantiate a driver. Every driver struct has a `steal` function with the following signature:

```rust
/// Unsafely creates an instance of XYZ driver.
///
/// # Safety
///
/// The caller is responsible for ensuring the hardware is present on the device, configured
/// correctly, and not accessed concurrently.
pub const unsafe fn steal() -> Self {
    ...
}
```

The `psoc` crate then re-exports a `Device` struct containing instances of all supported drivers on a given device.

## Driver ownership and typestates

Ownership of a driver instance represents ownership of the underlying hardware block so that an application does not have accidental race conditions, resource conflicts, or hidden shared state. All driver functions that modify the state of the hardware and/or are not safe to execute concurrently must take an `&mut self` parameter, while functions that are safe to execute concurrently should take an `&self` parameter. User applications can use interior mutability such as `RefCell` if they want shared access to a driver.

All drivers must be interrupt-safe. Interrupt handlers do not change the ownership rules -- a `&mut` reference is exclusive even across interrupt handlers. However, if a driver function accesses shared memory or hardware registers that could potentially race with interrupt contexts, that function must either:
- Use the `critical_section` crate to disable interrupts and restore them afterwards,
- Require the caller to call the function in a critical section, by accepting a `CriticalSection` parameter, or
- Be marked unsafe, with the safety condition clearly documented (e.g. disabling interrupts).

Driver types must not be constructible from user code except via the `steal` function, or by obtaining an instance from the `Device` struct. This means driver structs must contain private fields or be marked `#[non_exhaustive]` to prevent construction outside of the crate.

If a driver requires configuration before it can be used, then it must provide an `Unconfigured` typestate representing a driver instance before configuration, and a configuration function allowing a user to transform an unconfigured instance into a configured one by following the hardware's configuration and initialization sequence. The configured state must also provide a deinitialization function allowing the user to return to the unconfigured state. For example:

```rust
/// An unconfigured instance of a Widget hardware block.
#[non_exhaustive]
struct UnconfiguredWidget<...>;

impl<...> UnconfiguredWidget<...> {
	pub unsafe fn steal() -> Self { UnconfiguredWidget }

	pub fn configure(self) -> Widget<...> { ... }
}

/// A driver for an instance of the Widget hardware block.
struct Widget<...> { ... }

impl<...> Widget<...> {
	pub fn destroy(self) -> UnconfiguredWidget { ... }
}
```

An unconfigured driver instance must be a zero-sized type, using generic parameters to track configuration (such as an instance number). A configured driver instance should be zero-sized where practical, but may contain fields if necessary to track runtime state.

Drivers should also provide type-erased variants allowing parameters such as instance number to be tracked at runtime. Type-erased drivers should take a lifetime parameter, allowing the user to temporarily type-erase a driver instance; this parameter may be set to `'static` if the user wants to permanently type-erase a driver. For example:

```rust
impl<const N: u8> Widget<N> {
    fn into_erased(self) -> AnyWidget<'static> { ... }
	fn as_erased(&mut self) -> AnyWidget<'_> { ... }
}

/// A type-erased Widget.
struct<'a> AnyWidget<'a> {
    instance: u8,
}
```

Typestates can also be used to track driver state like a GPIO pin's mode or a watchdog timer that requires an unlock sequence to access configuration registers; however, it should not be used for state that is expected to vary dynamically at runtime.

For TrustZone support, all drivers should have a generic parameter indicating whether the drivers should be accesed via a secure or non-secure alias; see `drivers/src/security.rs` for details.

Configuration structures for drivers should include a `pub _ne: NonExhaustive` field instead of using `#[non_exhaustive]` to allow for the use of struct update syntax.

## Code generation and conditional compilation

The `psoc` and `drivers` crates both have build scripts that use device features to generate code or configuration:
- The `psoc` build script generates the `Device` struct and linker scripts for the selected device, based on the supported peripherals and memories.
- The `drivers` build script generates `cfg` flags for each supported feature. The supported features are defined in `devices/src/die.rs`.
  - For a boolean feature, the `cfg` flag is just the name of the feature: `#[cfg(mcwdt0)]`.
  - For a key-value feature, the `cfg` flag is the name of the feature and its value: `#[cfg(clock_path_fll = "0")]`.
  - For a versioned feature, two `cfg` flags are generated: one for the presence of the feature, and one for the version: `#[cfg(mxs40sioss)]` and `#[cfg(mxs40sioss_version = "v1.3")]`.

## Other notes

Drivers must never import PAC crates directly; they should use the `crate::regs` module, which re-exports the PAC for the current device.

`portable_atomic` should be used instead of `core::sync::atomic` to allow for atomic operations on armv6 devices which don't have atomic read-modify-write operations.
