Embedded HandbookApps
All sections › III. Peripherals
STM32F103 · level intermediate · 38 min read
Open in the interactive handbook

22н. E-paper display with the SSD1680 controller over SPI: BUSY, full and partial update, LUT, black and red buffers, sleep and battery power

In 22з the colour TFT drew through windows straight into its own memory, in 22м the monochrome OLED received a finished frame from RAM — and both shone only while powered. The general view of displays is in 22а. Here is a screen that does not show a picture but keeps it: electronic paper. Cut the power — the text stays. In return a frame takes seconds to change, the screen flashes while doing so, and frequent quick updates leave traces. The 2.13″ 250×122 and 2.9″ 296×128 modules on the SSD1680 controller, four SPI wires and a BUSY line; initialisation, the two memories — black and red — full and partial update, bytes and rotation, deep sleep and a battery estimate for a weather station that updates its screen every 10 minutes. And the pitfalls: not waiting for BUSY, waking without a reset, a partial update without a base frame, inverted bits, frost.

Sources: RM0008 (DocID13902 Rev 17): wait for TXE, then BSY, p. 719; the BR divider — from /2, p. 743. DocID13587 Rev 17 (STM32F103x8/xB): 20 Kbytes of RAM, p. 1; PA1–PA7 not 5 V tolerant, PA5 is SPI1_SCK, PA7 is SPI1_MOSI, p. 29; run current at 8 MHz up to 8.6 mA, p. 42; Stop with the low-power regulator 14 µA, Standby with LSI and no watchdog 3.2 µA at 3.3 V, p. 45. The project has no SSD1680 datasheet. The command codes and their byte counts, the bits of command 0x22 and the codes 0xF7 and 0xFF, data entry mode 0x11, deep sleep 0x10 0x01, "SW reset leaves the RAM intact", "BUSY = 1 means busy", "after a partial update the memories are swapped", "the most significant bit is the first pixel", 1 is white in the black memory, the SSD1680 handles up to 296×176 pixels, the 2.13″ module is 250×122 with its internal sensor 0x80, border 0x05 and 0x3C and SPI at 4 MHz — were checked against the SSD16xx driver and boards of Zephyr 4.4.2 on disk. How the paper works physically, the update duration, the module currents, behaviour in sleep and on reset, garbage in memory after power-up and the accumulation of ghosts follow the manufacturer's documentation and were not checked: in the model they are assumptions, and the numbers about them describe the model, not the glass. All other numbers below come from running the driver _verify/epd22n/epd22n.c, built with arm-none-eabi-gcc 12.2.1 for the Cortex-M3 and run in QEMU 7.2 (mps2-an385), against its own SSD1680 model in Python 3.11; in the bench SPI, RST and BUSY are replaced by text lines over semihosting, and the model compares the glass with a reference pixel by pixel. The F103 firmware is built with armclang 6.24.

What it is called in the documentation

Original Meaning
electrophoretic display (EPD), e-paper electrophoretic display, electronic paper
microcapsule, charged pigment particles a capsule with charged ink particles
bistable the picture holds without power
Driver Output Control (0x01) how many rows the glass has and how to scan them
Data Entry Mode (0x11) where the memory pointer goes after a byte
BW RAM (0x24), RED RAM (0x26) the black memory and the red memory
Display Update Control 2 (0x22), Master Activation (0x20) what to do on update, and "go"
Display Mode 1 / Display Mode 2 full and partial update
LUT (waveform look-up table) the voltage waveform table: what to apply to a pixel and for how long
TSSV (0x18), internal sensor 0x80 which temperature sensor to use
Border Waveform Control (0x3C) what to do with the border around the field
image retention (residual image) a residual image — a "ghost"
DSM (0x10 0x01) deep sleep: exit only through RST

Top — connecting the 2.13″ SSD1680 module to the STM32F103: SPI1 at 4 MHz, mode 0, transmit only; PA5 — SCK, PA7 — MOSI (DIN), PA4 — CS, PA3 — DC, 0 — command, 1 — data, PA2 — RST, reset and wake from sleep, PA1 — the BUSY input, 1 — the controller is busy; the module is 250 × 122 at 3.3 V. Middle — an update in time: RST and 12, setup 01 11 44 45 3C 18, writing 24 — 4000 bytes and 26 — 4000 bytes, 0x22 F7 and 20, then BUSY = 1 — 2.0 s for a full and 0.40 s for a partial update, then sleep 10 01; a full frame is 8017 bytes over SPI in 16.0 ms; a partial one is 0x22 FF and a second 24. Bottom — a memory row: 16 bytes, a byte is 8 pixels, the most significant bit is the leftmost, in the black memory 1 is white, pixel (8, 0) black — byte 1 of row 0 equals 0x7F; the two memories of a partial update: 0x24 — the new frame, 0x26 — the previous one, only differing pixels move; without a base frame — garbage, on the 6th partial in a row grey ghosts appear in the model, so every few partial updates — a full one


22н.1 The paper, the modules and the pins

How the paper works (according to manufacturers' descriptions, not checked). The field is a layer of tiny capsules with clear liquid in which charged particles float: white of one sign, black of the other (in three-colour panels also red). A voltage between the top and bottom electrodes pulls one kind of particle towards the viewer and hides the other. Remove the voltage and the particles stay where they are: the picture holds without power, and current flows only while it changes. Hence three properties the program has to deal with:

The model does not compute any of this — only the consequences: an update holds BUSY for a set time, a partial update accumulates "underdrive" (22н.5), cold stretches the time (22н.7).

Modules. In Zephyr 4.4.2 the SSD1680 is described as a controller of up to 296×176 pixels; next to it in the same driver are its relatives SSD1608, SSD1673, SSD1675A and SSD1681 with their own limits. The older generation (SSD1608, SSD1673, SSD1675A) has different bits in command 0x22, so "compatible" does not mean "the same code". This section uses two SSD1680 modules:

Module Pixels Memory X × Y Bytes per colour
2.13″ 250 × 122 16 bytes × 250 rows 4000
2.9″ 296 × 128 16 bytes × 296 rows 4736

The controller memory is organised in rows: X is 8 pixels per byte, Y is the row number. The short side of the glass is X: 122 and 128 pixels give 16 bytes each. The long side is rows: 250 and 296. That is how Zephyr describes the 2.13″ module: 250 wide, 122 high, sensor 0x80, border 0x05 and 0x3C, SPI at 4 MHz.

Pins. The bus is transmit-only SPI (Zephyr connects such modules as "4-wire SPI": clock, data, CS and the DC line), plus reset and BUSY:

Module STM32F103 What
CLK (SCK) PA5 SPI1 clock
DIN (MOSI) PA7 SPI1 data
CS PA4 0 — a transfer is in progress
DC PA3 0 — command byte, 1 — its data
RST PA2 0 — reset; only it wakes from deep sleep
BUSY PA1, input 1 — the controller is busy, send no commands
VCC, GND 3.3 V and ground

PA1–PA7 are not 5 V tolerant (DocID13587, p. 29): power the module from 3.3 V. In the firmware the core stays on HSI 8 MHz, as after reset: a battery device does not need 72 MHz. SPI1 uses the /2 divider (RM0008, p. 743), 4 MHz, as for the module in Zephyr; mode 0. A transfer is a command with DC = 0, then its data with DC = 1, all under one CS = 0. Change DC only when the byte has gone completely: TXE, then BSY (p. 719):

/* CS = 0 на всю посылку; DC = 0 — байт команды, DC = 1 — её данные. DC менять только когда SPI пуст */
void epd_port_cmd(uint8_t cmd, const uint8_t *data, uint32_t n)
{
    CLR(PIN_CS);
    CLR(PIN_DC);
    spi_byte(cmd);
    spi_idle();
    SET(PIN_DC);
    while (n--)
        spi_byte(*data++);
    spi_idle();
    SET(PIN_CS);
}

22н.2 Initialisation: reset, SW reset, BUSY and the memory window

The order is as in the Zephyr driver: a hardware reset, wait for BUSY, SW reset 0x12, wait for BUSY again, then the setup. SW reset returns the registers to their defaults and leaves the memory intact (Zephyr says so) — so the program does not have to resend the frame after every reset.

/* Аппаратный сброс, SW reset 0x12 и настройка: строки, режим ввода, окно памяти, рамка, датчик, указатель.
 * Память контроллера сброс не трогает: что было в памяти красного, то и осталось. */
static int controller_init(epd_t *e)
{
    const uint16_t last = (uint16_t)(e->p->gates - 1u);
    const uint8_t gdo[3] = {(uint8_t)last, (uint8_t)(last >> 8), 0x00};
    const uint8_t wx[2] = {0x00, (uint8_t)(EPD_STRIDE - 1u)};
    const uint8_t wy[4] = {0x00, 0x00, (uint8_t)last, (uint8_t)(last >> 8)};
    const uint8_t y0[2] = {0x00, 0x00};
    int rc;
    epd_port_rst(0);
    epd_port_delay_ms(10);
    epd_port_rst(1);
    epd_port_delay_ms(10);
    if ((rc = epd_wait(e)) != EPD_OK)
        return rc;
    cmd(e, EPD_SW_RESET, 0, 0);
    if ((rc = epd_wait(e)) != EPD_OK)
        return rc;
    cmd(e, EPD_DRIVER_OUT, gdo, 3);
    cmd1(e, EPD_ENTRY_MODE, EPD_ENTRY_XINC_YINC);
    cmd(e, EPD_RAM_X, wx, 2);
    cmd(e, EPD_RAM_Y, wy, 4);
    cmd1(e, EPD_BORDER, EPD_BORDER_FULL);
    cmd1(e, EPD_TEMP_SENSOR, EPD_TSENSOR_INTERNAL);
    cmd1(e, EPD_RAM_XCNT, 0x00);
    cmd(e, EPD_RAM_YCNT, y0, 2);
    e->asleep = 0;
    return epd_wait(e);
}

What went to the 2.13″ module (model log):

Command Data What
RST — pin to 0 for 10 ms, then wait for BUSY
12 — SW reset; BUSY = 1 while it runs
01 F9 00 00 250 rows: 249 = 0x00F9 low byte first, and 0x00
11 03 X increments, Y increments, X first — as the buffer is laid out
44 00 0F X window: bytes 0…15
45 00 00 F9 00 Y window: rows 0…249
3C 05 border — as for this module in Zephyr for a full update
18 80 internal temperature sensor
4E, 4F 00, 00 00 pointer to the start of the window

After the reset — 9 commands, 24 bytes, 48 µs at 4 MHz; the 2.9″ module differs in two: 01 27 01 00 and 45 00 00 27 01 — 296 rows. Waiting for BUSY is a function of a few lines, but with a timeout: if the module is disconnected or broken, BUSY may stay at 1 forever.

/* BUSY = 1 — контроллер занят: команды сейчас не слать. Ждём с шагом EPD_POLL_MS, но не дольше timeout_ms */
int epd_wait(epd_t *e)
{
    uint32_t t = 0;
    while (epd_port_busy()) {
        if (t >= e->timeout_ms)
            return EPD_E_TIMEOUT;
        epd_port_delay_ms(EPD_POLL_MS);
        t += EPD_POLL_MS;
    }
    e->last_busy_ms = t;
    return EPD_OK;
}

In the firmware epd_port_delay_ms sleeps in WFI until the SysTick tick: while BUSY is high, the core sleeps. The polling step is 10 ms; for a full update the model counted 202 BUSY polls.


22н.3 Two memories, bytes and rotation

The controller has two memories of the same size: 24 — black, 26 — red. In the black memory bit 1 is a white pixel, 0 is black (Zephyr fills it with 0xFF to make the screen white). In the red memory of a three-colour module 1 is a red pixel. A black-and-white module has no red, and the second memory serves as the "previous frame" for a partial update (22н.5).

A byte is 8 neighbouring pixels of a row, the most significant bit is the leftmost (in Zephyr — SCREEN_INFO_MONO_MSB_FIRST). This is not like the SSD1306 in 22м, where a byte is a column of 8 pixels and the least significant bit is on top:

/* Байт — 8 точек строки, старший бит — левая: точка sx строки gy — байт gy × 16 + sx / 8, бит 7 − sx % 8 */
void epd_pixel(epd_t *e, int x, int y, uint8_t color)
{
    uint32_t sx, gy, i;
    uint8_t m;
    if (!map_xy(e, x, y, &sx, &gy))
        return;
    i = gy * EPD_STRIDE + (sx >> 3);
    m = (uint8_t)(0x80u >> (sx & 7u));
    if (color == EPD_BLACK)
        e->bw[i] &= (uint8_t)~m;        /* 0 — чёрная */
    else
        e->bw[i] |= m;                  /* 1 — белая; под красной точкой тоже белая */
    if (color == EPD_RED)
        e->red[i] |= m;
    else
        e->red[i] &= (uint8_t)~m;
}

The driver set one pixel on a white screen, and the model found the single changed byte and the single black pixel on the glass:

Pixel Rotation Byte Value Memory row, column
(0, 0) 0 0 0x7F 0, 0
(7, 0) 0 0 0xFE 0, 7
(8, 0) 0 1 0x7F 0, 8
(121, 0) 0 15 0xBF 0, 121
(0, 249) 0 3984 0x7F 249, 0
(0, 0) 1 15 0xBF 0, 121
(249, 121) 1 3984 0x7F 249, 0

A row is 16 bytes, but the 2.13″ module has 122 = 15 × 8 + 2 pixels: the last byte of a row uses its two top bits, and the glass does not show the six low ones. The buffer is 16 × 250 = 4000 bytes per colour, for the 2.9″ — 16 × 296 = 4736. The epd_t type holds both colours for the larger module: armclang showed 9516 bytes of static memory in the firmware — 46 % of the 20 Kbytes of the F103C8 (DocID13587, p. 1). A black-and-white module would need one buffer.

Rotation. The memory is portrait: 122 pixels in X and 250 rows. A weather station needs landscape 250×122. The driver does not touch the data entry mode; it recalculates the pixel in map_xy:

/* Точка (x, y) экрана → столбец памяти sx и строка gy с учётом поворота */
static int map_xy(const epd_t *e, int x, int y, uint32_t *sx, uint32_t *gy)
{
    const int s = (int)e->p->sources, g = (int)e->p->gates;
    int a, b;
    if (x < 0 || y < 0 || x >= epd_width(e) || y >= epd_height(e))
        return 0;
    switch (e->rot) {
    case 1:  a = s - 1 - y; b = x;          break;
    case 2:  a = s - 1 - x; b = g - 1 - y;  break;
    case 3:  a = y;         b = g - 1 - x;  break;
    default: a = x;         b = y;          break;
    }
    *sx = (uint32_t)a;
    *gy = (uint32_t)b;
    return 1;
}

With rotation 1, screen pixel (0, 0) is column 121 of row 0, byte 15, 0xBF; pixel (249, 121) is column 0 of row 249. The buffer stays in memory axes and goes to the module as is.

Entry mode and pointer — together. The controller can turn the picture 180° in Y by itself: 11 01 — Y decrements. But then the 4F pointer must sit on the last row. In the model 11 01 with the pointer on row 0 gave an upside-down picture shifted by a row: 2010 pixels different from the flipped reference; with the pointer on row 249 — 0 differences. Zephyr does the same: before clearing it sets a mode with decrementing Y and the pointer on the last row.


22н.4 Full update: 0x24, 0x26, 0x22 and 0x20

Writing a frame is the pointer to the start and one transfer: the command and 16 × rows data bytes.

/* Весь цвет в память контроллера: указатель в начало окна, потом 0x24 или 0x26 и 16 × строк байт */
static void write_plane(epd_t *e, uint8_t ram, const uint8_t *buf)
{
    static const uint8_t y0[2] = {0x00, 0x00};
    cmd1(e, EPD_RAM_XCNT, 0x00);
    cmd(e, EPD_RAM_YCNT, y0, 2);
    cmd(e, ram, buf, epd_bytes(e));
}

The display itself is two commands: 22 says what to do, 20 starts it. The bits of 22 come from Zephyr's ssd16xx_regs.h:

Bit What
0x80, 0x40 enable the oscillator and the analog part
0x20 read the temperature from the sensor
0x10 load the voltage waveform table (LUT) from OTP
0x08 the second mode — partial update
0x04 display
0x02, 0x01 disable the analog part and the oscillator

Full is everything except 0x08: 0xF7; partial is everything: 0xFF. That is how Zephyr builds them for the SSD1680 with the internal sensor. Code 0xFC is the same partial without the last two bits: the oscillator and the analog part stay on after the update. The section's driver does not send it; what it gives in time and current follows the manufacturer's documentation and was not checked.

/* 0x22 и 0x20, потом BUSY: через 1 мс он обязан стоять в 1 — иначе контроллер спит или не подключён */
static int activate(epd_t *e, uint8_t ctrl2)
{
    cmd1(e, EPD_UPDATE_CTRL2, ctrl2);
    cmd(e, EPD_MASTER_ACT, 0, 0);
    epd_port_delay_ms(1);
    if (!epd_port_busy())
        return EPD_E_NOBUSY;
    return epd_wait(e);
}

After 20 BUSY must rise. If it is still 0 after 1 ms, the commands did not arrive: the controller is asleep or the module is disconnected (22н.7), and the driver returns EPD_E_NOBUSY instead of "done".

int epd_show(epd_t *e, int mode)
{
    int rc;
    if (e->asleep && (rc = controller_init(e)) != EPD_OK)     /* из сна выводит только аппаратный сброс */
        return rc;
    if (mode == EPD_AUTO)
        mode = e->kind == EPD_KIND_BWR || !e->base_ok || (e->full_every && e->partials >= e->full_every)
                   ? EPD_FULL : EPD_PARTIAL;
    if (mode == EPD_PARTIAL && (!e->base_ok || e->kind == EPD_KIND_BWR))
        return EPD_E_NOBASE;
    if (mode == EPD_FULL) {
        /* у чёрно-белого память красного — «прошлый кадр»: кладём туда тот же кадр, он станет базой */
        write_plane(e, EPD_WRITE_BW, e->bw);
        write_plane(e, EPD_WRITE_RED, e->kind == EPD_KIND_BWR ? e->red : e->bw);
        cmd1(e, EPD_BORDER, EPD_BORDER_FULL);
        if ((rc = activate(e, EPD_UPD_FULL)) != EPD_OK)
            return rc;
        e->base_ok = e->kind == EPD_KIND_BW;
        e->partials = 0;
        e->fulls++;
        return EPD_OK;
    }
    /* частичное: двигаются только точки, где новый кадр (0x24) отличается от прошлого (0x26) */
    write_plane(e, EPD_WRITE_BW, e->bw);
    cmd1(e, EPD_BORDER, EPD_BORDER_PART);
    if ((rc = activate(e, EPD_UPD_PART)) != EPD_OK)
        return rc;
    write_plane(e, EPD_WRITE_BW, e->bw);   /* после частичного память поменялась местами (так в Zephyr): ещё раз */
    e->partials++;
    e->parts++;
    return EPD_OK;
}

A full update puts the frame into both memories: into 24 as the new one, into 26 as the future base for a partial update. The weather station frame in the model:

What Value
over SPI 8017 bytes, 16.0 ms at 4 MHz
BUSY = 1 (model, 25 °C) 2.0 s, the driver measured 2000 ms in steps of 10
glass differences from the reference 0 of 30500 pixels, 5075 black

The transfer is 0.8 % of the update time: there is little point in speeding up SPI or adding DMA here, all the time goes to the particles.

LUT. The controller takes the voltage waveform from its OTP memory — bit 0x10 in 22 — and chooses the one for the temperature by the sensor — bit 0x20 and command 18 80. You can load your own table with command 32 (in Zephyr — in the full and partial profiles), but its contents for a particular glass can only come from its manufacturer: a table from another module can spoil the picture. The section's driver sends no LUT of its own.


22н.5 Partial update and ghosts

A partial update does not drive the whole screen through black and white. The controller compares the two memories: 24 holds the new frame, 26 the previous one, and moves only the pixels where they differ. There is no flashing, and in the model the update takes 0.40 s instead of 2.0 (per the manufacturer's documentation, not checked). After the update the memories are swapped — Zephyr says so — and 24 ends up with the previous frame. So after a partial update the driver sends the new frame to 24 once more: now both memories hold what is on the glass, and the next partial update starts from the right base. Model log: 24 3C:3C 22:FF 20 24.

From frame "00:00" to "00:10" 1332 pixels had to change in the model — exactly that many moved; glass differences from the reference 0, both memories matched after the update. Over SPI — the same 8017 bytes: 24 twice.

Ghosts. A partial update drives a pixel for less time than a full one, and the pixel does not quite reach its colour — some "underdrive" remains. It accumulates from update to update, and where the digits changed most often grey traces appear. In the model this is an assumption: each partial update adds 0.02 to a repainted pixel and 0.002 to the others, and a pixel with underdrive above 0.10 looks grey. A full update erases everything. The weather station ran partial updates in a row:

Partial updates in a row Grey pixels Largest underdrive
5 0 0.100
6 79 —
10 684 0.200
20 2636 0.400
30 2778 0.600

The first grey pixels appear on partial update 6, in the temperature and humidity digits. Hence the driver's rule: after 5 partial updates — a full one (full_every = 5, EPD_AUTO). A day of the weather station — 144 updates every 10 minutes — gave 24 full and 120 partial updates, 0 grey pixels, 0 differences from the reference over the whole day. The number 5 follows from the model's assumptions; on real glass it is chosen by eye.

Border. The field is surrounded by a border with its own waveform — command 3C. For a full update of the 2.13″ module Zephyr uses 0x05, for a partial one 0x3C; the driver sets it before every update.

Three-colour module. On a module with red, 26 is the red colour, and there is no "previous frame". The driver does no partial updates for it: epd_show with EPD_PARTIAL returned EPD_E_NOBASE (−3). A full update in the model — 304 red pixels of the word "ALERT", 0 differences.


22н.6 Sleep and the battery: a weather station every 10 minutes

Between updates the module needs nothing: the picture holds by itself. So after an update — deep sleep 10 01. The only way out is a hardware reset; the controller does not listen to SPI meanwhile.

int epd_sleep(epd_t *e)
{
    int rc = epd_wait(e);
    if (rc != EPD_OK)
        return rc;
    cmd1(e, EPD_DEEP_SLEEP, EPD_SLEEP_MODE1);
    e->asleep = 1;
    return EPD_OK;
}

epd_show remembers that the controller is asleep and starts with controller_init: RST, 12, setup. In the model the memory survives sleep (per the manufacturer's documentation, not checked), and the next partial update goes from the same base.

Energy of one update. The module currents in the model are assumptions: 4.0 mA on average during an update, 30 µA without sleep, 1 µA in sleep. At 3.3 V:

What Time (model) Energy Charge
full 2.0 s 26.40 mJ 2.222 µAh
partial 0.40 s 5.28 mJ 0.444 µAh
SPI transfer, core running (8.6 mA) 16.0 ms 456.4 µJ

A day. The weather station wakes every 10 minutes, draws a frame (program work is an assumption: 20 ms at 8 MHz, 8.6 mA per DocID13587, p. 42), sends it, waits for BUSY asleep and sleeps until next time in Stop — 14 µA (p. 45). The battery is the same as in 79.3: 220 mAh. The numbers of full and partial updates come from the daily run (24 and 120):

How Module, mAh per day MCU, mAh per day Average current Lasts, days (years)
partial, full after 5, module asleep, MCU in Stop 0.131 0.348 19.96 µA 459 (1.26)
full updates only 0.344 0.348 28.85 µA 318 (0.87)
module not asleep 0.826 0.348 48.93 µA 187 (0.51)
MCU waits for BUSY running 0.131 0.577 29.50 µA 311 (0.85)
MCU sleeps in Standby 0.131 0.089 9.16 µA 1001 (2.74)

You can see who eats the battery. In the right mode the module spends less than half of what the MCU does: only updates are expensive, and there are 144 of them a day. Module sleep matters: without 10 01 its 30 µA of idle current (a model assumption) add up over a day to more than all the updates together. Waiting for BUSY running rather than asleep gives an average current of 29.50 µA instead of 19.96. And the MCU itself gives the most: Stop on the F103 is 14 µA, and sleeping in Standby (3.2 µA with LSI for the alarm, p. 45) makes the battery last more than twice as long. Standby means a reset on exit (section 10.4): epd_t and the partial counter must be kept in the backup registers, and the frame redrawn — it is redrawn every time anyway.


22н.7 Pitfalls

Commands did not wait for BUSY — some were lost. While BUSY = 1 the controller does not listen (so in the model; per the manufacturer's documentation, not checked). A partial update followed immediately, without waiting for BUSY, by a second 24 and sleep: in the model 4 commands were lost, the glass showed the new frame, but the black memory kept the previous one, and the module did not go to sleep — those 30 µA instead of 1. Wait for BUSY before every command after 12 and 20.

After sleep the screen does not change, and there are no errors. A controller in deep sleep does not hear SPI, and its BUSY is 0 — a naive driver concludes the update finished instantly. In the model 9 commands went nowhere, the glass shows the previous frame (0 differences from it). The section's driver catches this itself: BUSY did not rise after 20 — EPD_E_NOBUSY (−2). Only RST wakes it: after a reset and controller_init the next partial update went through, 0 differences.

A partial update without a base frame is a mess. After power-up the controller memory holds garbage (a model assumption), while the glass keeps the previous picture. A partial update compares the new frame with garbage and does not move pixels where the garbage happens to match the new frame. In the model, of the 1332 pixels that had to change, 673 ended up wrong. The first display after power-up must be full; epd_init clears base_ok, and EPD_AUTO does a full one itself: 0 differences.

A negative, mirrored groups of eight, all red. In the black memory 1 is white. A buffer with "1 is black" gives a negative: in the model all 30500 pixels differ. Least significant bit on the left — every group of eight pixels is mirrored: 5874 differences. On a three-colour module a red memory filled with 0xFF instead of 0x00 makes the whole screen red: 30500 pixels of 30500.

Picture upside down and shifted by a row. An entry mode with decrementing Y but the pointer left on row 0 — 22н.3: 2010 differences from the flipped reference. Change the mode and the pointer together.

A timeout in the frost. The cold table takes longer. In the model (an assumption) the update time grows like this, and a driver with a 3 s timeout breaks already at 5 °C:

Temperature Full Partial 3 s timeout 10 s timeout
25 °C 2.0 s 0.40 s ok ok
15 °C 2.6 s 0.52 s ok ok
5 °C 3.6 s 0.72 s EPD_E_TIMEOUT ok
−5 °C 5.2 s 1.04 s EPD_E_TIMEOUT ok
−15 °C 8.0 s 1.60 s EPD_E_TIMEOUT ok

Choose the timeout with a margin for the coldest case: the driver uses 10 s. The update time grows in the cold — and so do the charge and energy.

A 5 V module. PA1–PA7 on the F103 are not 5 V tolerant (DocID13587, p. 29): do not connect a module with 5 V logic to them directly.


22н.8 Summary

Full sources

epd22n.h — SSD1680 commands and bits, the epd_t type with the black and red buffers, the port and the driver prototypes.

/* epd22n.h — раздел 22н: дисплей на электронной бумаге с контроллером SSD1680 по SPI без регистров микроконтроллера.
 * Драйвер говорит с модулем через epd_port_*: в прошивке fw22n.c их делают SPI1 и выводы STM32F103, в стенде
 * host22n.c — строки через semihosting к модели model22n.py (QEMU). Коды команд, биты 0x22, режим 0x11, 0x10 0x01,
 * датчик 0x18 0x80, рамка 0x05 / 0x3C, «старший бит — левая точка», размеры модулей и 4 МГц сверены с драйвером
 * SSD16xx и платами Zephyr 4.4.2 на диске; даташита SSD1680 в проекте нет. */
#ifndef EPD22N_H
#define EPD22N_H
#include <stdint.h>

/* ── команды SSD1680 ───────────────────────────────────────────────────── */
#define EPD_DRIVER_OUT    0x01u         /* + 3 байта: (строк − 1) младшим байтом вперёд и 0x00 */
#define EPD_DEEP_SLEEP    0x10u         /* + 1 байт: 0x01 — глубокий сон, выход только аппаратным сбросом */
#define EPD_ENTRY_MODE    0x11u         /* + 1 байт: куда идёт указатель после байта */
#define EPD_SW_RESET      0x12u         /* программный сброс: регистры по умолчанию, память не трогает */
#define EPD_TEMP_SENSOR   0x18u         /* + 1 байт: 0x80 — свой датчик температуры */
#define EPD_MASTER_ACT    0x20u         /* запустить последовательность, заданную 0x22 */
#define EPD_UPDATE_CTRL2  0x22u         /* + 1 байт: что делать при 0x20 (биты ниже) */
#define EPD_WRITE_BW      0x24u         /* + данные: память чёрного (новый кадр) */
#define EPD_WRITE_RED     0x26u         /* + данные: память красного (у чёрно-белого — прошлый кадр) */
#define EPD_WRITE_LUT     0x32u         /* + данные: своя таблица формы напряжений вместо OTP */
#define EPD_BORDER        0x3Cu         /* + 1 байт: что делать с рамкой вокруг поля */
#define EPD_RAM_X         0x44u         /* + 2 байта: окно по X в байтах, от и до */
#define EPD_RAM_Y         0x45u         /* + 4 байта: окно по Y в строках, от и до, младшим вперёд */
#define EPD_RAM_XCNT      0x4Eu         /* + 1 байт: указатель X */
#define EPD_RAM_YCNT      0x4Fu         /* + 2 байта: указатель Y */

/* ── биты 0x22 (как в ssd16xx_regs.h Zephyr) ───────────────────────────── */
#define EPD_U_CLK_ON      0x80u
#define EPD_U_ANALOG_ON   0x40u
#define EPD_U_LOAD_TEMP   0x20u
#define EPD_U_LOAD_LUT    0x10u
#define EPD_U_MODE2       0x08u         /* второй режим — частичное обновление */
#define EPD_U_DISPLAY     0x04u
#define EPD_U_ANALOG_OFF  0x02u
#define EPD_U_CLK_OFF     0x01u
#define EPD_UPD_FULL  (EPD_U_CLK_ON | EPD_U_ANALOG_ON | EPD_U_LOAD_TEMP | EPD_U_LOAD_LUT | EPD_U_DISPLAY | \
                       EPD_U_ANALOG_OFF | EPD_U_CLK_OFF)                   /* 0xF7 */
#define EPD_UPD_PART  (EPD_UPD_FULL | EPD_U_MODE2)                         /* 0xFF */

#define EPD_ENTRY_XINC_YINC  0x03u      /* X растёт, Y растёт, сначала X — как лежит буфер */
#define EPD_ENTRY_XINC_YDEC  0x01u
#define EPD_SLEEP_MODE1      0x01u
#define EPD_TSENSOR_INTERNAL 0x80u
#define EPD_BORDER_FULL      0x05u      /* так у модуля 2,13″ в Zephyr: полное */
#define EPD_BORDER_PART      0x3Cu      /* и частичное */

#define EPD_STRIDE      16u             /* байт на строку: 122 и 128 точек → 16 байт, старший бит — левая точка */
#define EPD_BUF_MAX     (EPD_STRIDE * 296u)

enum { EPD_WHITE = 0, EPD_BLACK = 1, EPD_RED = 2 };
enum { EPD_KIND_BW = 0, EPD_KIND_BWR = 1 };                 /* чёрно-белый или с красным */
enum { EPD_AUTO = 0, EPD_FULL = 1, EPD_PARTIAL = 2 };
enum { EPD_OK = 0, EPD_E_TIMEOUT = -1, EPD_E_NOBUSY = -2, EPD_E_NOBASE = -3, EPD_E_ARG = -4 };

typedef struct {
    uint16_t sources;                   /* точек в строке памяти (по X): 122 или 128 */
    uint16_t gates;                     /* строк (по Y): 250 или 296 */
} epd_panel_t;

extern const epd_panel_t EPD_213;       /* 2,13″ 122 × 250 — 4000 байт на цвет */
extern const epd_panel_t EPD_29;        /* 2,9″ 128 × 296 — 4736 байт */

typedef struct {
    const epd_panel_t *p;
    uint8_t kind, rot;                  /* rot: 0 — как память (портрет), 1 — 90°, 2 — 180°, 3 — 270° */
    uint8_t base_ok;                    /* в памяти красного лежит то, что сейчас на стекле */
    uint8_t asleep;
    uint8_t full_every;                 /* после стольких частичных — полное; 0 — только по запросу */
    uint8_t partials;                   /* частичных подряд после последнего полного */
    uint32_t timeout_ms;                /* сколько ждать BUSY */
    uint32_t last_busy_ms;              /* сколько BUSY держался в последний раз (шаг опроса) */
    uint32_t fulls, parts, bytes, cmds;
    uint8_t bw[EPD_BUF_MAX];            /* 1 — белая точка, 0 — чёрная */
    uint8_t red[EPD_BUF_MAX];           /* 1 — красная точка (только у EPD_KIND_BWR) */
} epd_t;

/* ── порт: в прошивке — SPI1 и выводы, в стенде — строки к модели ──────── */
void epd_port_rst(int level);                                          /* вывод RST: 0 — сброс */
void epd_port_cmd(uint8_t cmd, const uint8_t *data, uint32_t n);       /* CS = 0; DC = 0 — команда; DC = 1 — данные */
int epd_port_busy(void);                                               /* 1 — контроллер занят */
void epd_port_delay_ms(uint32_t ms);                                   /* пауза; в прошивке — сон до тика */

/* ── драйвер ─────────────────────────────────────────────────────────── */
#define EPD_POLL_MS     10u
#define EPD_TIMEOUT_MS  10000u
uint32_t epd_bytes(const epd_t *e);     /* байт на один цвет: 16 × строк */
int epd_width(const epd_t *e);
int epd_height(const epd_t *e);
int epd_init(epd_t *e, const epd_panel_t *p, uint8_t kind, uint8_t rot);
int epd_wait(epd_t *e);
void epd_clear(epd_t *e, uint8_t color);
void epd_pixel(epd_t *e, int x, int y, uint8_t color);
int epd_get(const epd_t *e, int x, int y);
void epd_rect(epd_t *e, int x, int y, int w, int h, int fill, uint8_t color);
int epd_text(epd_t *e, int x, int y, const char *s, int scale, uint8_t fg, uint8_t bg);
int epd_show(epd_t *e, int mode);       /* EPD_AUTO: полное, если нет базового кадра или пора, иначе частичное */
int epd_sleep(epd_t *e);                /* 0x10 0x01; выход — только аппаратный сброс, его делает epd_show */

#endif

epd22n.c — the driver: initialisation, BUSY, a rotated pixel, the 5×7 font, full and partial refresh, sleep — shared by the firmware and the bench.

/* epd22n.c — раздел 22н: драйвер дисплея на электронной бумаге (SSD1680): инициализация, буферы чёрного и
 * красного, точка и шрифт с поворотом, полное и частичное обновление с ожиданием BUSY, глубокий сон.
 * Один и тот же файл собирается в прошивку (armclang, Cortex-M3) и в стенд (arm-none-eabi-gcc, QEMU):
 * микроконтроллера он не знает, только порт epd_port_*. */
#include "epd22n.h"

const epd_panel_t EPD_213 = {122u, 250u};
const epd_panel_t EPD_29 = {128u, 296u};

/* Шрифт 5×7 — тот же, что в 22з и 22м: знак — 5 столбцов, бит 0 — верхняя строка; подряд от пробела до «Z» */
static const uint8_t font5x7['Z' - ' ' + 1][5] = {
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x43, 0x33, 0x08, 0x66, 0x61},   /* % */
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x08, 0x08, 0x08, 0x00},   /* - */
    {0x00, 0x60, 0x60, 0x00, 0x00},   /* . */
    {0x40, 0x30, 0x08, 0x06, 0x01},   /* / */
    {0x3E, 0x51, 0x49, 0x45, 0x3E},   /* 0 */
    {0x00, 0x42, 0x7F, 0x40, 0x00},   /* 1 */
    {0x42, 0x61, 0x51, 0x49, 0x46},   /* 2 */
    {0x21, 0x41, 0x45, 0x4B, 0x31},   /* 3 */
    {0x18, 0x14, 0x12, 0x7F, 0x10},   /* 4 */
    {0x27, 0x45, 0x45, 0x45, 0x39},   /* 5 */
    {0x3C, 0x4A, 0x49, 0x49, 0x30},   /* 6 */
    {0x01, 0x71, 0x09, 0x05, 0x03},   /* 7 */
    {0x36, 0x49, 0x49, 0x49, 0x36},   /* 8 */
    {0x06, 0x49, 0x49, 0x29, 0x1E},   /* 9 */
    {0x00, 0x36, 0x36, 0x00, 0x00},   /* : */
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00},
    {0x7E, 0x09, 0x09, 0x09, 0x7E},   /* A */
    {0x7F, 0x49, 0x49, 0x49, 0x36},   /* B */
    {0x3E, 0x41, 0x41, 0x41, 0x22},   /* C */
    {0x7F, 0x41, 0x41, 0x22, 0x1C},   /* D */
    {0x7F, 0x49, 0x49, 0x49, 0x41},   /* E */
    {0x7F, 0x09, 0x09, 0x09, 0x01},   /* F */
    {0x3E, 0x41, 0x49, 0x49, 0x7A},   /* G */
    {0x7F, 0x08, 0x08, 0x08, 0x7F},   /* H */
    {0x00, 0x41, 0x7F, 0x41, 0x00},   /* I */
    {0x20, 0x40, 0x41, 0x3F, 0x01},   /* J */
    {0x7F, 0x08, 0x14, 0x22, 0x41},   /* K */
    {0x7F, 0x40, 0x40, 0x40, 0x40},   /* L */
    {0x7F, 0x02, 0x0C, 0x02, 0x7F},   /* M */
    {0x7F, 0x04, 0x08, 0x10, 0x7F},   /* N */
    {0x3E, 0x41, 0x41, 0x41, 0x3E},   /* O */
    {0x7F, 0x09, 0x09, 0x09, 0x06},   /* P */
    {0x3E, 0x41, 0x51, 0x21, 0x5E},   /* Q */
    {0x7F, 0x09, 0x19, 0x29, 0x46},   /* R */
    {0x46, 0x49, 0x49, 0x49, 0x31},   /* S */
    {0x01, 0x01, 0x7F, 0x01, 0x01},   /* T */
    {0x3F, 0x40, 0x40, 0x40, 0x3F},   /* U */
    {0x1F, 0x20, 0x40, 0x20, 0x1F},   /* V */
    {0x3F, 0x40, 0x38, 0x40, 0x3F},   /* W */
    {0x63, 0x14, 0x08, 0x14, 0x63},   /* X */
    {0x03, 0x04, 0x78, 0x04, 0x03},   /* Y */
    {0x61, 0x51, 0x49, 0x45, 0x43},   /* Z */
};

static void cmd(epd_t *e, uint8_t c, const uint8_t *d, uint32_t n)
{
    epd_port_cmd(c, d, n);
    e->cmds++;
    e->bytes += 1u + n;
}

static void cmd1(epd_t *e, uint8_t c, uint8_t v)
{
    cmd(e, c, &v, 1);
}

uint32_t epd_bytes(const epd_t *e)
{
    return EPD_STRIDE * e->p->gates;
}

int epd_width(const epd_t *e)
{
    return (int)(e->rot & 1u ? e->p->gates : e->p->sources);
}

int epd_height(const epd_t *e)
{
    return (int)(e->rot & 1u ? e->p->sources : e->p->gates);
}

/* BUSY = 1 — контроллер занят: команды сейчас не слать. Ждём с шагом EPD_POLL_MS, но не дольше timeout_ms */
int epd_wait(epd_t *e)
{
    uint32_t t = 0;
    while (epd_port_busy()) {
        if (t >= e->timeout_ms)
            return EPD_E_TIMEOUT;
        epd_port_delay_ms(EPD_POLL_MS);
        t += EPD_POLL_MS;
    }
    e->last_busy_ms = t;
    return EPD_OK;
}

/* Аппаратный сброс, SW reset 0x12 и настройка: строки, режим ввода, окно памяти, рамка, датчик, указатель.
 * Память контроллера сброс не трогает: что было в памяти красного, то и осталось. */
static int controller_init(epd_t *e)
{
    const uint16_t last = (uint16_t)(e->p->gates - 1u);
    const uint8_t gdo[3] = {(uint8_t)last, (uint8_t)(last >> 8), 0x00};
    const uint8_t wx[2] = {0x00, (uint8_t)(EPD_STRIDE - 1u)};
    const uint8_t wy[4] = {0x00, 0x00, (uint8_t)last, (uint8_t)(last >> 8)};
    const uint8_t y0[2] = {0x00, 0x00};
    int rc;
    epd_port_rst(0);
    epd_port_delay_ms(10);
    epd_port_rst(1);
    epd_port_delay_ms(10);
    if ((rc = epd_wait(e)) != EPD_OK)
        return rc;
    cmd(e, EPD_SW_RESET, 0, 0);
    if ((rc = epd_wait(e)) != EPD_OK)
        return rc;
    cmd(e, EPD_DRIVER_OUT, gdo, 3);
    cmd1(e, EPD_ENTRY_MODE, EPD_ENTRY_XINC_YINC);
    cmd(e, EPD_RAM_X, wx, 2);
    cmd(e, EPD_RAM_Y, wy, 4);
    cmd1(e, EPD_BORDER, EPD_BORDER_FULL);
    cmd1(e, EPD_TEMP_SENSOR, EPD_TSENSOR_INTERNAL);
    cmd1(e, EPD_RAM_XCNT, 0x00);
    cmd(e, EPD_RAM_YCNT, y0, 2);
    e->asleep = 0;
    return epd_wait(e);
}

int epd_init(epd_t *e, const epd_panel_t *p, uint8_t kind, uint8_t rot)
{
    e->p = p;
    e->kind = kind;
    e->rot = (uint8_t)(rot & 3u);
    e->base_ok = 0;                     /* после включения память — мусор: первый показ только полный */
    e->full_every = 5;
    e->partials = 0;
    e->timeout_ms = EPD_TIMEOUT_MS;
    e->fulls = e->parts = e->bytes = e->cmds = 0;
    epd_clear(e, EPD_WHITE);
    return controller_init(e);
}

void epd_clear(epd_t *e, uint8_t color)
{
    for (uint32_t i = 0; i < epd_bytes(e); i++) {
        e->bw[i] = color == EPD_BLACK ? 0x00u : 0xFFu;
        e->red[i] = color == EPD_RED ? 0xFFu : 0x00u;
    }
}

/* Точка (x, y) экрана → столбец памяти sx и строка gy с учётом поворота */
static int map_xy(const epd_t *e, int x, int y, uint32_t *sx, uint32_t *gy)
{
    const int s = (int)e->p->sources, g = (int)e->p->gates;
    int a, b;
    if (x < 0 || y < 0 || x >= epd_width(e) || y >= epd_height(e))
        return 0;
    switch (e->rot) {
    case 1:  a = s - 1 - y; b = x;          break;
    case 2:  a = s - 1 - x; b = g - 1 - y;  break;
    case 3:  a = y;         b = g - 1 - x;  break;
    default: a = x;         b = y;          break;
    }
    *sx = (uint32_t)a;
    *gy = (uint32_t)b;
    return 1;
}

/* Байт — 8 точек строки, старший бит — левая: точка sx строки gy — байт gy × 16 + sx / 8, бит 7 − sx % 8 */
void epd_pixel(epd_t *e, int x, int y, uint8_t color)
{
    uint32_t sx, gy, i;
    uint8_t m;
    if (!map_xy(e, x, y, &sx, &gy))
        return;
    i = gy * EPD_STRIDE + (sx >> 3);
    m = (uint8_t)(0x80u >> (sx & 7u));
    if (color == EPD_BLACK)
        e->bw[i] &= (uint8_t)~m;        /* 0 — чёрная */
    else
        e->bw[i] |= m;                  /* 1 — белая; под красной точкой тоже белая */
    if (color == EPD_RED)
        e->red[i] |= m;
    else
        e->red[i] &= (uint8_t)~m;
}

int epd_get(const epd_t *e, int x, int y)
{
    uint32_t sx, gy, i;
    uint8_t m;
    if (!map_xy(e, x, y, &sx, &gy))
        return -1;
    i = gy * EPD_STRIDE + (sx >> 3);
    m = (uint8_t)(0x80u >> (sx & 7u));
    if (e->kind == EPD_KIND_BWR && (e->red[i] & m))
        return EPD_RED;
    return e->bw[i] & m ? EPD_WHITE : EPD_BLACK;
}

void epd_rect(epd_t *e, int x, int y, int w, int h, int fill, uint8_t color)
{
    for (int j = y; j < y + h; j++)
        for (int i = x; i < x + w; i++)
            if (fill || j == y || j == y + h - 1 || i == x || i == x + w - 1)
                epd_pixel(e, i, j, color);
}

/* Строка шрифтом 5×7 с увеличением scale: клетка 6×8 знака пишется целиком, фон — цветом bg */
int epd_text(epd_t *e, int x, int y, const char *s, int scale, uint8_t fg, uint8_t bg)
{
    for (; *s; s++, x += 6 * scale) {
        char c = *s >= ' ' && *s <= 'Z' ? *s : ' ';
        for (int i = 0; i < 6; i++) {
            uint8_t bits = i < 5 ? font5x7[c - ' '][i] : 0;
            for (int r = 0; r < 8; r++)
                epd_rect(e, x + i * scale, y + r * scale, scale, scale, 1, bits >> r & 1u ? fg : bg);
        }
    }
    return x;
}

/* Весь цвет в память контроллера: указатель в начало окна, потом 0x24 или 0x26 и 16 × строк байт */
static void write_plane(epd_t *e, uint8_t ram, const uint8_t *buf)
{
    static const uint8_t y0[2] = {0x00, 0x00};
    cmd1(e, EPD_RAM_XCNT, 0x00);
    cmd(e, EPD_RAM_YCNT, y0, 2);
    cmd(e, ram, buf, epd_bytes(e));
}

/* 0x22 и 0x20, потом BUSY: через 1 мс он обязан стоять в 1 — иначе контроллер спит или не подключён */
static int activate(epd_t *e, uint8_t ctrl2)
{
    cmd1(e, EPD_UPDATE_CTRL2, ctrl2);
    cmd(e, EPD_MASTER_ACT, 0, 0);
    epd_port_delay_ms(1);
    if (!epd_port_busy())
        return EPD_E_NOBUSY;
    return epd_wait(e);
}

int epd_show(epd_t *e, int mode)
{
    int rc;
    if (e->asleep && (rc = controller_init(e)) != EPD_OK)     /* из сна выводит только аппаратный сброс */
        return rc;
    if (mode == EPD_AUTO)
        mode = e->kind == EPD_KIND_BWR || !e->base_ok || (e->full_every && e->partials >= e->full_every)
                   ? EPD_FULL : EPD_PARTIAL;
    if (mode == EPD_PARTIAL && (!e->base_ok || e->kind == EPD_KIND_BWR))
        return EPD_E_NOBASE;
    if (mode == EPD_FULL) {
        /* у чёрно-белого память красного — «прошлый кадр»: кладём туда тот же кадр, он станет базой */
        write_plane(e, EPD_WRITE_BW, e->bw);
        write_plane(e, EPD_WRITE_RED, e->kind == EPD_KIND_BWR ? e->red : e->bw);
        cmd1(e, EPD_BORDER, EPD_BORDER_FULL);
        if ((rc = activate(e, EPD_UPD_FULL)) != EPD_OK)
            return rc;
        e->base_ok = e->kind == EPD_KIND_BW;
        e->partials = 0;
        e->fulls++;
        return EPD_OK;
    }
    /* частичное: двигаются только точки, где новый кадр (0x24) отличается от прошлого (0x26) */
    write_plane(e, EPD_WRITE_BW, e->bw);
    cmd1(e, EPD_BORDER, EPD_BORDER_PART);
    if ((rc = activate(e, EPD_UPD_PART)) != EPD_OK)
        return rc;
    write_plane(e, EPD_WRITE_BW, e->bw);   /* после частичного память поменялась местами (так в Zephyr): ещё раз */
    e->partials++;
    e->parts++;
    return EPD_OK;
}

int epd_sleep(epd_t *e)
{
    int rc = epd_wait(e);
    if (rc != EPD_OK)
        return rc;
    cmd1(e, EPD_DEEP_SLEEP, EPD_SLEEP_MODE1);
    e->asleep = 1;
    return EPD_OK;
}

fw22n.c — the STM32F103 firmware: SPI1 at 4 MHz, the CS, DC, RST and BUSY pins, a weather station every 10 minutes.

/* fw22n.c — раздел 22н: электронная бумага 2,13″ 250 × 122 на SSD1680 у STM32F103 (BluePill), метеостанция.
 * Ядро — на HSI 8 МГц, как после сброса: для батарейки 72 МГц не нужны. SPI1 ведущим только на передачу, режим 0,
 * 8 МГц / 2 = 4 МГц: SCK — PA5, MOSI — PA7; CS — PA4, DC — PA3, RST — PA2, BUSY — PA1 (вход).
 * Раз в 10 минут: новый кадр в буфер, epd_show (полное после 5 частичных), epd_sleep. Пока BUSY = 1 и между
 * кадрами процессор спит в WFI до тика SysTick; настоящий сон Stop или Standby с будильником RTC — раздел 10. */
#include "stm32f10x.h"
#include "epd22n.h"

#define PIN_BUSY  1u
#define PIN_RST   2u
#define PIN_DC    3u
#define PIN_CS    4u
#define SET(p)    (GPIOA->BSRR = 1u << (p))
#define CLR(p)    (GPIOA->BSRR = 1u << ((p) + 16u))
#define PERIOD_MS 600000u               /* 10 минут */

static epd_t epd;
static volatile uint32_t ticks;
volatile int last_rc;

void SysTick_Handler(void)
{
    ticks++;
}

void epd_port_delay_ms(uint32_t ms)
{
    uint32_t t0 = ticks;
    while (ticks - t0 < ms)
        __WFI();                        /* спать до следующего тика */
}

void epd_port_rst(int level)
{
    if (level)
        SET(PIN_RST);
    else
        CLR(PIN_RST);
}

int epd_port_busy(void)
{
    return (GPIOA->IDR >> PIN_BUSY) & 1u;
}

static void spi_byte(uint8_t b)
{
    while (!(SPI1->SR & SPI_SR_TXE)) {
    }
    SPI1->DR = b;
}

static void spi_idle(void)              /* последний байт ушёл целиком: TXE = 1, потом BSY = 0 */
{
    while (!(SPI1->SR & SPI_SR_TXE)) {
    }
    while (SPI1->SR & SPI_SR_BSY) {
    }
    (void)SPI1->DR;                     /* приёма нет: снять RXNE и OVR */
    (void)SPI1->SR;
}

/* CS = 0 на всю посылку; DC = 0 — байт команды, DC = 1 — её данные. DC менять только когда SPI пуст */
void epd_port_cmd(uint8_t cmd, const uint8_t *data, uint32_t n)
{
    CLR(PIN_CS);
    CLR(PIN_DC);
    spi_byte(cmd);
    spi_idle();
    SET(PIN_DC);
    while (n--)
        spi_byte(*data++);
    spi_idle();
    SET(PIN_CS);
}

static void put2(char *p, unsigned v)
{
    p[0] = (char)('0' + v / 10u % 10u);
    p[1] = (char)('0' + v % 10u);
}

int main(void)
{
    uint32_t n = 0;
    char hm[] = "00:00", nn[] = "N:000";
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_SPI1EN;
    GPIOA->BSRR = 1u << PIN_RST | 1u << PIN_CS | 1u << PIN_DC;          /* RST, CS, DC — в 1 */
    GPIOA->CRL = (GPIOA->CRL & 0x0F00000Fu)            /* PA0 и PA6 не трогаем */
               | 0x4u << 4                             /* PA1 — BUSY, вход без подтяжки */
               | 0x2u << 8 | 0x2u << 12 | 0x2u << 16   /* PA2–PA4 — выходы, 2 МГц */
               | 0xBu << 20 | 0xBu << 28;              /* PA5, PA7 — SPI1, 50 МГц */
    SPI1->CR1 = SPI_CR1_MSTR | SPI_CR1_SSM | SPI_CR1_SSI;   /* BR = 000: 8 МГц / 2 = 4 МГц; режим 0 */
    SPI1->CR1 |= SPI_CR1_SPE;
    SysTick_Config(8000u);                              /* 1 мс на 8 МГц */

    last_rc = epd_init(&epd, &EPD_213, EPD_KIND_BW, 1); /* альбомная: 250 × 122 */
    for (;;) {
        uint32_t mins = n * 10u;
        put2(hm, mins / 60u % 24u);
        put2(hm + 3, mins % 60u);
        nn[2] = (char)('0' + n / 100u % 10u);
        put2(nn + 3, n % 100u);
        epd_clear(&epd, EPD_WHITE);
        epd_rect(&epd, 0, 0, 250, 122, 0, EPD_BLACK);
        epd_rect(&epd, 2, 2, 246, 118, 0, EPD_BLACK);
        epd_text(&epd, 8, 8, "METEO", 2, EPD_BLACK, EPD_WHITE);
        epd_text(&epd, 150, 8, hm, 3, EPD_BLACK, EPD_WHITE);
        epd_text(&epd, 8, 40, "21.5C", 5, EPD_BLACK, EPD_WHITE);   /* здесь — показания датчика */
        epd_rect(&epd, 8, 86, 234, 2, 1, EPD_BLACK);
        epd_text(&epd, 8, 96, "H 41%", 2, EPD_BLACK, EPD_WHITE);
        epd_text(&epd, 130, 96, nn, 2, EPD_BLACK, EPD_WHITE);
        last_rc = epd_show(&epd, EPD_AUTO);             /* полное после 5 частичных, первое — полное */
        if (last_rc == EPD_OK)
            last_rc = epd_sleep(&epd);
        n++;
        epd_port_delay_ms(PERIOD_MS);
    }
}

Next: section 22о "A 320×240 display on the ILI9341: SPI with DMA versus a parallel bus through FSMC on the F407 — fill speed, windows, fonts and tearing" — from paper that keeps a picture without power to a fast colour screen: 320×240 over SPI with DMA and over the parallel FSMC bus.

Open in the interactive handbook