Skip to content

Porting

Run the driver on a new MCU by writing two small classes: one for SPI, one for CONVST, DRDY and delays.

The driver never calls your SDK directly. It calls six methods on classes you derive from ads9324::SpiInterface<Self> and ads9324::HostInterface<Self> (in inc/ads9324_spi_interface.hpp). The base classes use CRTP: the calls are resolved at compile time, so there are no virtual functions, no vtable and nothing to allocate.

Layer diagram: the application calls the hardware-agnostic ADS9324 driver and its methods, which use the SpiInterface and HostInterface contracts, which call the platform port you write, which drives the SPI bus, CONVST and DRDY of the ADS9324.Layer diagram: the application calls the hardware-agnostic ADS9324 driver and its methods, which use the SpiInterface and HostInterface contracts, which call the platform port you write, which drives the SPI bus, CONVST and DRDY of the ADS9324.
Three layers, one of them yours
Figure description and sources

From top to bottom. Your application talks to one ADS9324 object, whose public methods are listed from inc/ads9324.hpp. The core packs 24-bit register frames, selects register banks, merges the PGA nibbles and unpacks conversion bursts. It reaches the hardware only through two CRTP contracts in inc/ads9324_spi_interface.hpp: SpiInterface (one transfer method) and HostInterface (CONVST, DRDY and two delays). The platform port is the only code you write: two small classes, or one class implementing both. Below it is the hardware.

Drawn from: inc/ads9324.hpp ADS9324 public methods; inc/ads9324_spi_interface.hpp SpiInterface, HostInterface; figure module docs/diagrams/figures/driver-layers.py.

What you implement

Method Class Contract
transfer(tx, rx, len) SpiInterface One full-duplex transfer of len bytes with CS low for all of them. tx == nullptr: send zeros. rx == nullptr: discard the received bytes. Return true when the bytes went out, false on a bus error
ConvstWrite(high) HostInterface Drive CONVST high (true) or low (false)
DrdyRead() HostInterface Return true when DRDY/ALARM is high (conversion ready)
HasDrdy() const HostInterface true if DRDY is wired; false makes the driver wait CONV_FALLBACK_US instead
DelayUs(us) HostInterface Block for at least us microseconds (1 µs steps during the DRDY poll)
DelayMs(ms) HostInterface Block for at least ms milliseconds (30 ms power-up, 1 ms after reset)
ResetWrite(high) your host class, optional Drive the RESET pin: false = low (chip held in reset), true = released. Only needed for HardwareReset()

The port

my_port.hpp
// SPI: one CS-low window per call, SPI mode 0, MSB first.
class MySpi : public ads9324::SpiInterface<MySpi> {
public:
  // tx == nullptr: clock out zeros. rx == nullptr: discard what arrives on SDOUT.
  // Return false on a bus error: the driver then fails the call that sent the frame.
  bool transfer(const uint8_t* tx, uint8_t* rx, std::size_t len) {
    return platform_spi_transfer(tx, rx, len);
  }
};

// Host: the CONVST output, the optional DRDY input and two delays.
class MyHost : public ads9324::HostInterface<MyHost> {
public:
  static constexpr int kConvst = 3;   // GPIO wired to CONVST (pin 50)
  static constexpr int kDrdy = 11;    // GPIO wired to DRDY/ALARM (pin 49)
  static constexpr int kReset = 12;   // GPIO wired to RESET (pin 32); optional

  void ConvstWrite(bool high) { platform_gpio_write(kConvst, high); }
  bool DrdyRead() { return platform_gpio_read(kDrdy); }   // high = conversion done
  bool HasDrdy() const { return true; }                   // false: fixed wait instead
  void DelayUs(uint32_t us) { platform_delay_us(us); }
  void DelayMs(uint32_t ms) { platform_delay_us(ms * 1000U); }
  void ResetWrite(bool high) {
    platform_gpio_write(kReset, high);
  } // optional: HardwareReset()
};

One class may implement both contracts; pass the same object twice: ADS9324<Bus, Bus> adc(bus, bus).

Rules for the transfer

  • SPI mode 0, MSB first, up to 60 MHz. Start at 1 to 8 MHz on a new board.
  • One CS window per call. CS goes low before the first byte and high after the last. Register frames are 3 bytes; a conversion burst is 32 bytes (16 channels, 16-bit words) or 48 bytes (24-bit words). Never split one call into several CS windows: the chip decodes a register write on the CS rising edge.
  • Handle nullptr. The driver passes real buffers today, but the contract allows either pointer to be null.
  • Own the bus. Do not share this CS with another device, and do not let another task use the bus in the middle of a register read (two frames).

transfer() returns bool: false tells the driver the transfer failed. The call that sent the frame stops there: configuration calls return false, ReadDeviceId() returns 0 and the reads return Error::SpiError. The driver's cached settings change only after the frames that set them went out, and after a failed BANK_SEL frame the next access selects the bank again.

Return the SDK's status as a bool (== ESP_OK, == HAL_OK): any other return type is rejected at compile time, so an esp_err_t (0 = success) cannot be read as false. A port written for the earlier contract, with a void transfer(), still compiles; the driver then treats every transfer as successful.

CONVST and DRDY

The driver holds CONVST high for 1 µs and then drives it low; the falling edge samples every channel. The chip needs only 50 ns high and 50 ns low, so any GPIO is fast enough. DRDY is active high by default; if your board inverts it, invert it in DrdyRead(), not with SetAlarmPin(..., true).

Without DRDY (HasDrdy() returns false) the driver waits CONV_FALLBACK_US (10 µs) after the falling edge. The conversion takes 1.25 µs without oversampling, so the default leaves margin.

Optional reset pin

If RESET (pin 32) is wired to a GPIO, add void ResetWrite(bool high) to the host class, as the port above does. The driver detects it at compile time (ADS9324<...>::HasHardwareReset()); it is not part of HostInterface, so a port without it compiles unchanged and HardwareReset() returns false. Hold RESET high from power-up, for example with a pull-up, so the chip is not held in reset before your code runs.

Reference implementation

The ESP32 examples use esp32_ads9324_bus.hpp: one class that implements both contracts with the ESP-IDF spi_master driver (hardware CS, one transaction per call), GPIO for CONVST and DRDY, esp_rom_delay_us() and FreeRTOS delays. Start from it for any ESP-IDF target.

Check your port

  1. Build it: the port on this page is compiled by the host CI from tests/docs/, with the driver instantiated over it.
  2. Call EnsureInitialized(). If it fails, print ReadDeviceId(): 0x0004 proves SDI, SCLK, CS and SDOUT end to end.
  3. Short one input and read a snapshot: the count should be within a few tens of LSB of zero.
  4. Watch GetDiagnostics().errors: it should stay at zero on a healthy link.

Threads

The driver is not thread-safe. Give each ADS9324 one owner task, or guard every call on a driver object (and its port) with one mutex. Several chips on separate buses can run in parallel.