Skip to content

Quickstart

Pull in a solenoid from your MCU: wire seven signals, write a small port class, call four driver functions.

Before you power the load

Use a current-limited VM supply for the first run. The example turns channel 0 on at 400 mA as soon as the chip is up, and ChannelConfig defaults to low-side drive: the coil goes from OUT0 to VM.

1. Wire it

MCU MAX22200 Notes
SPI SCLK, MOSI, CS SCK, SDI, CSB SPI mode 0; start at 1 MHz
SPI MISO SDO Open drain: pull it up to the MCU's I/O voltage
GPIO out ENABLE High enables the chip; internal pull-down
GPIO out CMD High while the command byte is sent
GPIO in FAULT Open drain, low on a fault: pull it up
GND GND, EP Common ground between the MCU and VM

Tie TRIGA and TRIGB low (or leave them to their internal pull-downs) unless you trigger channels from pins. Put a resistor of 15 kΩ to 100 kΩ from IREF to GND: it sets the full-scale current for CDR. The Hardware page has the full pin list, the external parts and the timing.

2. Add the driver

Add the repository as a submodule and link the hf::max22200 CMake target (or add inc/ to your include path and build with C++20). Installation covers CMake and ESP-IDF.

git submodule add https://github.com/hardfoc/hf-max22200-driver.git external/hf-max22200-driver

3. Write the port

The port is the only platform code. It runs one blocking SPI transaction per call, drives ENABLE and CMD, reads FAULT and waits. Replace the platform_* calls with your SDK's.

my_spi.hpp
class MySpi : public max22200::SpiInterface<MySpi> {
public:
  // Board wiring: ENABLE and CMD are MCU outputs, FAULT is an input with a pull-up.
  static constexpr int kEnablePin = 4;
  static constexpr int kCmdPin = 5;
  static constexpr int kFaultPin = 6;

  // Cap the clock: 1 MHz for bring-up, at most 10 MHz alone or 5 MHz in a daisy chain.
  explicit MySpi(uint32_t max_clock_hz = 10000000) : max_clock_hz_(max_clock_hz) {}

  bool Initialize() { return true; }

  // The driver calls this once from Initialize(): 10 MHz, mode 0, MSB first.
  bool Configure(uint32_t speed_hz, uint8_t mode, bool msb_first) {
    const uint32_t hz = speed_hz < max_clock_hz_ ? speed_hz : max_clock_hz_;
    return mode == 0 && msb_first && platform_spi_init(hz, mode);
  }

  bool IsReady() const { return true; }

  // One blocking, full-duplex transaction: CS low, `length` bytes, CS high, then return.
  bool Transfer(const uint8_t* tx, uint8_t* rx, std::size_t length) {
    return platform_spi_transfer(tx, rx, length);
  }

  void SetChipSelect(bool) {}   // Transfer() owns CS; the driver never calls this

  void DelayUs(uint32_t us) { platform_delay_us(us); }

  // ENABLE and CMD are active high, FAULT is active low (open drain).
  void GpioSet(max22200::CtrlPin pin, max22200::GpioSignal signal) {
    const bool high = signal == max22200::GpioSignal::ACTIVE;
    if (pin == max22200::CtrlPin::ENABLE) platform_gpio_write(kEnablePin, high);
    if (pin == max22200::CtrlPin::CMD) platform_gpio_write(kCmdPin, high);
  }

  bool GpioRead(max22200::CtrlPin pin, max22200::GpioSignal& signal) {
    if (pin != max22200::CtrlPin::FAULT) return false;
    signal = platform_gpio_read(kFaultPin) ? max22200::GpioSignal::INACTIVE
                                           : max22200::GpioSignal::ACTIVE;
    return true;
  }

private:
  uint32_t max_clock_hz_;
};

Transfer() must hold CS low for the whole buffer and return only after CS is high again: the driver drops CMD right after it returns. Porting explains each method.

4. Pull in the solenoid

main.cpp
#include "max22200.hpp"

int pull_in_solenoid_on_out0(MySpi& spi) {
  // 1. Board: RREF = 30 kOhm on IREF, so IFS = 15k x 1 V / 30 kOhm = 500 mA.
  max22200::MAX22200<MySpi> driver(spi, max22200::BoardConfig(30.0f, false));

  if (driver.Initialize() != max22200::DriverStatus::OK) {   // 2. ENABLE, STATUS, ACTIVE = 1
    return -1;                                               // wiring, SPI mode, VM
  }

  // 3. Channel 0: low-side CDR, 400 mA for 20 ms to pull in, then 150 mA to hold.
  const auto valve = max22200::ChannelConfig::makeSolenoidCdr(400.0f, 150.0f, 20.0f);
  if (driver.ConfigureChannel(0, valve) != max22200::DriverStatus::OK) {
    return -2;
  }
  return driver.EnableChannel(0) == max22200::DriverStatus::OK ? 0 : -3;   // 4. on
}

What each step does:

  1. BoardConfig(30.0f, false) gives the driver the full-scale current from your RREF: IFS = 15 000 × 1 V / RREF, so 30 kΩ is 500 mA. Every milliamp value is converted against it.
  2. Initialize() raises ENABLE, waits 0.5 ms, reads STATUS (clearing the power-up UVM flag) and writes STATUS with ACTIVE = 1 and every channel off.
  3. makeSolenoidCdr() builds a low-side, current-regulated channel: 400 mA until the HIT time ends, then 150 mA. ConfigureChannel() encodes it into CFG_CH0.
  4. EnableChannel() sets ONCH0 with one 8-bit write. The HIT phase starts, then the chip drops to HOLD by itself.

This example is built by the host CI, so it compiles against the current headers.

If it does not work

Symptom First thing to check
Initialize() returns COMMUNICATION_ERROR CMD wiring and level, SPI mode 0, Transfer() blocking
Every read is 0xFF SDO pull-up missing or too weak, ENABLE low, VM off
Initialises, but the output never switches Load wiring (low side: coil to VM), VM above 4.5 V, ACTIVE in STATUS
FAULT stays low Read STATUS and FAULT (Troubleshooting)

Next

  • API guide: CDR and VDR channels, switching, bridges, faults and plunger-movement detection.
  • Drive modes: how HIT, HOLD, the HIT time and the chopping frequency map to registers.
  • Examples: ESP32 applications for valves and diagnostics.