Skip to content

API guide

How to do the common jobs with the driver: initialise, draw, use the controller's drawing commands, set brightness, read the switch and handle errors.

Every snippet on this page is compiled by the host CI from tests/docs/api_tasks.cpp, with Display standing for isf15acp4::SmartDisplay<MySpi> and colors for isf15acp4::colors. The API reference lists every member.

The shape of the API

One SmartDisplay<Port> object per button. You draw into a GraphicsContext and hand it to the display; or you call the display's own drawing and window functions, which talk to the controller directly.

Task Calls
Start Initialize(), Reset(), IsInitialized()
Draw a frame CreateGraphicsContext(), then GraphicsContext drawing, then Present()
Draw on the controller DrawLine(), DrawRectangle(), ClearWindow(), DrawPixel(), FillScreen()
Write pixels yourself SetWindow(), SetFullWindow(), WritePixels(), DrawCanvas()
Brightness and power DisplayOn(), DisplayOff(), DisplayDim(), SetMasterCurrent()
Scroll ConfigureScroll()
Switch IsPressed(), and ButtonUI for events
Errors GetLastError(), GetErrorFlags(), HasError(), ClearErrorFlags()

Every call that talks to the display returns bool and never throws. Check it: false means a parameter was out of range or the port reported a failure, and an Error flag says which.

Initialise

Pick the product variant and graphics backend in SmartDisplayConfig, then call Initialize():

const isf15acp4::SmartDisplayConfig config{
    .variant = isf15acp4::ProductVariant::Isf15acp4,
    .graphics = isf15acp4::GraphicsBackend::BuiltinCanvas,
    .enable_vcc_on_init = true,
};
isf15acp4::SmartDisplay<MySpi> display(&spi, config);
return display.Initialize(config.enable_vcc_on_init);

Initialize() brings the port up, pulses RES, enables VCC (unless you pass false), sends the variant's initialisation table (which ends with display on) and selects the full window. Reset() runs the same sequence again, for example after a supply brown-out. Configuration lists every field.

Draw a frame

auto gfx = display.CreateGraphicsContext();  // holds a 12,288-byte frame: keep it alive
gfx.Clear(colors::Black);
gfx.FillRect({0, 0, 95, 10}, colors::Blue);
gfx.DrawString(2, 2, "SPINDLE", colors::White, colors::Blue);

char text[16];
std::snprintf(text, sizeof(text), "%lu RPM", static_cast<unsigned long>(rpm));
gfx.DrawString(2, 24, text, colors::Cyan);

return display.Present(gfx);                 // full-window upload

Drawing into the context changes RAM on your MCU only; Present() sends the frame as one 12,288-byte data transaction. At 6 MHz that is about 16 ms of SPI time, so a full frame costs at most about 60 frames a second before your drawing time. Text uses the 5 x 7 font, 6 pixels per character: 16 characters across. Graphics covers the backends, colours and images.

Draw with the controller

The SSD1331 draws lines and rectangles itself from a few command bytes, which is much less SPI traffic than a frame:

if (!display.ClearWindow({0, 0, 95, 63})) {                          // 25h
  return false;
}
if (!display.DrawRectangle({10, 10, 85, 53}, colors::Orange, true)) { // 26h fill on, 22h, fill off
  return false;
}
return display.DrawLine({0, 63}, {95, 0}, colors::White);            // 21h
Call Command Bytes
DrawLine() 21h 8
DrawRectangle() 22h 11
filled 26h, 22h, 26h 17
ClearWindow() 25h 5
DrawPixel() 15h, 75h, data 8
FillScreen() A full frame 12,294

These draw straight into the display RAM. A later Present() overwrites them with the canvas contents.

Write pixels yourself

// An 8 x 8 red square at (40, 28): window, then 64 pixels.
isf15acp4::Color565 block[8 * 8];
for (auto& px : block) {
  px = colors::Red;
}
if (!display.SetWindow({40, 28, 47, 35})) {   // 15h / 75h, inclusive bounds
  return false;
}
if (!display.WriteColors(block, 8 * 8)) {     // high byte first on any CPU
  return false;
}
return display.SetFullWindow();               // back to 96 x 64 for Present()

SetWindow() takes inclusive bounds in display coordinates and returns false with Error::OutOfRange outside the visible area (0..95, 0..63 on the ISF15ACP4); on the smaller variants the driver adds the RAM column offset. WriteColors() needs Initialize() first and sends each Color565 pixel high byte first, the SSD1331's 65k-colour order. WritePixels() sends raw bytes unchanged, two per pixel already in that format, and needs an even byte count. Restore the full window before the next Present(); DrawCanvas() and Present() set it themselves.

Brightness and power

if (!display.DisplayOff()) {                 // AEh: sleep, GRAM kept
  return false;
}
if (!display.SetMasterCurrent(0x08)) {       // 87h: 0x00 (dimmest) .. 0x0F (brightest)
  return false;
}
if (!display.DisplayOn()) {                  // AFh: normal display
  return false;
}
return display.DisplayDim();                 // ACh: dim mode, for an idle screen
Call Command Effect
DisplayOn() AFh Normal display
DisplayOff() AEh Sleep; the RAM keeps its contents
DisplayDim() ACh Dim mode, levels from the init table
SetMasterCurrent() 87h Drive current scale, 0 to 15

The master current scales all three colours from 0 (dimmest) to 15 (brightest, the reset value).

Lower brightness also slows the OLED's ageing. SetMasterCurrent() rejects values above 0x0F with Error::OutOfRange.

Scroll

// Horizontal scroll of all 64 rows by 1 column, no vertical offset, fastest interval (code 0).
if (!display.ConfigureScroll(1, 0, 64, 0, 0, true)) {  // 2Eh, 27h, then 2Fh
  return false;
}
// ... later: stop, and rewrite the display RAM from the frame.
return display.StopScroll(gfx);                        // 2Eh, then Present(gfx)

ConfigureScroll(h_offset, start_row, rows, v_offset, interval, activate) stops any running scroll (2Eh), sends the SSD1331 scroll setup (27h) and, with activate, starts it (2Fh): the datasheet forbids changing the parameters while scrolling runs. The interval code is 0 to 3: a step every 6, 10, 100 or 200 frames.

Stopping leaves the display RAM shifted, and the datasheet says to rewrite it: StopScroll(gfx) sends 2Eh and then Present(gfx). StopScroll() without a context only sends 2Eh; redraw yourself afterwards.

Read the switch

IsPressed() returns your port's GpioRead(CtrlPin::Switch): the raw contact, with bounce. ButtonUI turns it into events:

// Call every 10-20 ms from your loop; elapsed_ms is the time since the previous call.
switch (ui.Update(display.IsPressed(), elapsed_ms)) {
  case isf15acp4::ButtonEvent::Pressed:   std::printf("down, press #%lu\n",
                                                      static_cast<unsigned long>(ui.PressCount())); break;
  case isf15acp4::ButtonEvent::Click:     std::printf("click\n"); break;
  case isf15acp4::ButtonEvent::LongPress: std::printf("held 800 ms\n"); break;
  case isf15acp4::ButtonEvent::Released:  std::printf("released after a long press\n"); break;
  case isf15acp4::ButtonEvent::None:      break;
}
Event When
Pressed The contact closed (counted in PressCount())
Click Released before the long-press time
LongPress Held for the long-press time (default 800 ms); reported once per press
Released Released after a long press

ButtonUI(debounce_ms, long_press_ms) defaults to 25 ms and 800 ms. It samples the contact once per debounce period, so call Update() more often than that.

Handle errors

if (!display.Initialize()) {
  switch (display.GetLastError()) {
    case isf15acp4::Error::NotInitialized: std::printf("port: EnsureInitialized() failed\n"); break;
    case isf15acp4::Error::SpiWrite:       std::printf("port: WriteBytes() failed\n"); break;
    case isf15acp4::Error::InvalidParam:   std::printf("no port (null bus pointer)\n"); break;
    default:                               std::printf("error 0x%04x\n", display.GetErrorFlags()); break;
  }
}
if (display.HasError(isf15acp4::Error::OutOfRange)) {   // flags accumulate until cleared
  display.ClearErrorFlags(static_cast<uint16_t>(isf15acp4::Error::OutOfRange));
}
std::printf("driver %s\n", Display::GetDriverVersion());
Error:: Set when
SpiWrite Your Begin*() or WriteBytes() returned false
NotInitialized EnsureInitialized() failed, or pixels before Initialize()
InvalidParam No port, an odd pixel byte count, a wrong-size canvas
OutOfRange A window off the panel, a master current above 15

GetLastError() is the most recent error; GetErrorFlags() is every error since the last ClearErrorFlags(). Error::GpioControl and Error::PowerSequence exist in the enum but nothing sets them yet. The display sends nothing back, so the driver cannot tell a missing or unpowered module from a working one: Troubleshooting starts there.