Skip to content

Porting

Run the driver on a new MCU by writing one class: the SPI interface.

The driver never calls your SDK directly. It calls eight methods on a class you derive from isf15acp4::SpiInterface<Self> (in inc/isf15acp4_spi_interface.hpp). The base class uses CRTP: the calls are resolved at compile time, so there are no virtual functions, no vtable and nothing to allocate.

Layer diagram: the application draws into a graphics context and calls the hardware-agnostic SmartDisplay driver, which uses the CRTP SPI interface, which calls the platform port you write, which drives the SPI bus, the D/C, SS, RES and VCC enable lines and reads the switch of the ISF15ACP4.Layer diagram: the application draws into a graphics context and calls the hardware-agnostic SmartDisplay driver, which uses the CRTP SPI interface, which calls the platform port you write, which drives the SPI bus, the D/C, SS, RES and VCC enable lines and reads the switch of the ISF15ACP4.
Three layers, one of them yours
Figure description and sources

From top to bottom. Your application draws into a GraphicsContext and calls one SmartDisplay object, whose public calls (listed from inc/isf15acp4.hpp) initialise the SSD1331 controller, upload frames, run its hardware drawing commands and read the switch. Each call becomes command and data transactions. The communication interface (SpiInterface in inc/isf15acp4_spi_interface.hpp) is a CRTP base that forwards them to your class. The platform port is the only code you write: a class that writes SPI bytes with D/C and SS, sets the control pins, reads the switch and waits. Below it is the hardware.

Drawn from: inc/isf15acp4.hpp SmartDisplay public calls; inc/isf15acp4_spi_interface.hpp SpiInterface; figure module docs/diagrams/figures/driver-layers.py.

What you implement

Method Contract
EnsureInitialized() Bring SPI and GPIOs up; true when ready, also on later calls
BeginCommand() D/C low, then SS low: a command and its parameters follow
BeginData() D/C high, then SS low: display data follows
EndTransaction() SS high, after the last bit has left
WriteBytes(data, len) Send len bytes (up to 12,288), blocking until sent
GpioSet(pin, signal) Drive SS, D/C, RES or VCC enable Active / Inactive
GpioRead(pin) true when Switch is pressed, else false
DelayUs(us) Wait at least that long (the driver uses 3, 10 and 1000)

All return bool except EndTransaction(), GpioSet() and DelayUs(), which return void.

SPI port

my_spi.hpp
class MySpi : public isf15acp4::SpiInterface<MySpi> {
public:
  // Your board's GPIO numbers.
  static constexpr int kCs = 10, kDc = 13, kRes = 14, kVccEn = 9, kSwitch = 8;

  bool EnsureInitialized() noexcept {
    platform_gpio_write(kCs, true);       // SS idle high
    platform_gpio_write(kVccEn, false);   // 15 V drive supply off until after reset
    return platform_spi_init();
  }

  // D/C low = command, high = data; SS low for the whole transaction.
  bool BeginCommand() noexcept { return begin(false); }
  bool BeginData() noexcept { return begin(true); }
  void EndTransaction() noexcept { platform_gpio_write(kCs, true); }

  // Must not return before the last bit is on the wire: SS rises right after.
  bool WriteBytes(const uint8_t* data, std::size_t len) noexcept {
    return platform_spi_write(data, len);
  }

  void GpioSet(isf15acp4::CtrlPin pin, isf15acp4::GpioSignal signal) noexcept {
    const bool active = signal == isf15acp4::GpioSignal::Active;
    switch (pin) {
      case isf15acp4::CtrlPin::ChipSelect:  platform_gpio_write(kCs, !active); break;   // SS active low
      case isf15acp4::CtrlPin::DataCommand: platform_gpio_write(kDc, active); break;    // high = data
      case isf15acp4::CtrlPin::Reset:       platform_gpio_write(kRes, !active); break;  // RES# active low
      case isf15acp4::CtrlPin::VccEnable:   platform_gpio_write(kVccEn, active); break;
      case isf15acp4::CtrlPin::Switch:      break;                                      // input only
    }
  }

  // The switch closes pins 1-2; wired to GND with a pull-up, pressed reads low.
  bool GpioRead(isf15acp4::CtrlPin pin) noexcept {
    return pin == isf15acp4::CtrlPin::Switch && !platform_gpio_read(kSwitch);
  }

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

private:
  bool begin(bool data) noexcept {
    platform_gpio_write(kDc, data);
    platform_gpio_write(kCs, false);
    return true;
  }
};

Rules the transfer must follow:

  • SPI mode 0 (mode 3 also meets the datasheet), MSB first, at most 6.6 MHz.
  • One SS-low transaction per Begin / End pair. The driver calls WriteBytes() one or more times inside it (a command's opcode, then its parameters; a frame in chunks of up to 128 bytes); keep SS low across all of them.
  • Blocking writes. WriteBytes() must not return while bytes are still queued: EndTransaction() raises SS right after it, and the display drops a byte cut short. With DMA, wait for completion.
  • No MISO. The display has no data output; leave MISO unconnected or free for other devices.
  • Return false only for a bus failure. The driver turns it into Error::SpiWrite.

Control pins

The driver addresses pins by name and abstract level. Your port decides the physical level:

Pin Driver use Active is
ChipSelect Inactive at start Low
DataCommand Active at start High (data)
Reset Active 3 µs, then Inactive Low (RES# held in reset)
VccEnable Active after reset Supply on
Switch Read by IsPressed() Pressed

Reset follows the other pins: Active is the asserted level, which for RES# is low. Initialize() drives it Active for at least 3 µs, then Inactive (high), and waits 10 µs before the first command (SSD1331 power-on sequence, reset completion 2 µs maximum).

Reference implementation

The ESP32 examples use esp32_isf15acp4_spi_adapter.hpp: an ESP-IDF spi_master device in mode 0 with SS as a GPIO, spi_device_polling_transmit() for blocking writes, an input with pull-up for the switch and esp_rom_delay_us() / vTaskDelay() for 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. With a logic analyser on SS, D/C and SCK, call Initialize(): you should see 21 command transactions (the init table) and two window commands, each with SS low around its bytes, and RES high at the end.
  3. Call FillScreen(isf15acp4::colors::White), then FillScreen(isf15acp4::colors::Black): the whole panel turns white, then dark. (White and black do not depend on byte order; for colours see Troubleshooting.)
  4. Read IsPressed() with the button held and released.

Threads

The driver is not thread-safe. Give each display one owner task, or guard every call on a driver object (and its port) with one mutex. Several displays on one SPI bus need a separate SS per display, and the bus must not be used by another device between a Begin*() and its EndTransaction().