Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103 · уровень средний · 38 мин чтения
Открыть в интерактивном справочнике

22н. Дисплей на электронной бумаге (e-paper) с контроллером SSD1680 по SPI: BUSY, полное и частичное обновление, LUT, буферы чёрного и красного, сон и питание от батарейки

В 22з цветной TFT рисовал окнами прямо в свою память, в 22м монохромный OLED получал готовый кадр из ОЗУ — и оба светились, только пока есть питание. Общий взгляд на дисплеи — в 22а. Здесь экран, который картинку не показывает, а хранит: электронная бумага. Обесточили — надпись осталась. Зато кадр меняется секундами, экран при этом мигает, а частые быстрые обновления оставляют следы. Модули 2,13″ 250×122 и 2,9″ 296×128 на контроллере SSD1680, четыре провода SPI и линия BUSY; инициализация, две памяти — чёрного и красного, — полное и частичное обновление, байты и поворот, глубокий сон и расчёт батарейки для метеостанции, которая обновляет экран раз в 10 минут. И грабли: не дождаться BUSY, разбудить без сброса, частичное без базового кадра, перевёрнутые биты, мороз.

Опоры: RM0008 (DocID13902 Rev 17): ожидание TXE, потом BSY, с. 719; делитель BR — от /2, с. 743. DocID13587 Rev 17 (STM32F103x8/xB): 20 Кбайт ОЗУ, с. 1; PA1–PA7 без допуска 5 В, PA5 — SPI1_SCK, PA7 — SPI1_MOSI, с. 29; ток в работе на 8 МГц — до 8,6 мА, с. 42; Stop с экономичным регулятором — 14 мкА, Standby с LSI без сторожа — 3,2 мкА при 3,3 В, с. 45. Даташита SSD1680 в проекте нет. Коды команд и число их байтов, биты команды 0x22 и коды 0xF7 и 0xFF, режим ввода 0x11, глубокий сон 0x10 0x01, «SW reset не трогает память», «BUSY = 1 — занят», «после частичного памяти меняются местами», «старший бит — первая точка», 1 — белая в памяти чёрного, у SSD1680 до 296×176 точек, модуль 2,13″ 250×122 со своим датчиком 0x80, рамкой 0x05 и 0x3C и SPI на 4 МГц — сверены с драйвером SSD16xx и платами Zephyr 4.4.2 на диске. Как устроена бумага физически, длительность обновления, токи модуля, поведение во сне и при сбросе, мусор в памяти после включения и накопление «призраков» — по документации производителя, не проверено: в модели это допущения, и числа о них говорят о модели, а не о стекле. Все остальные числа ниже — прогон драйвера _verify/epd22n/epd22n.c, собранного arm-none-eabi-gcc 12.2.1 под Cortex-M3 и запущенного в QEMU 7.2 (mps2-an385), против своей модели SSD1680 на Python 3.11; SPI, RST и BUSY в стенде подменены строками через semihosting, стекло модель сравнивает с эталоном точка в точку. Прошивка для F103 собрана armclang 6.24.

Как это называется в документации

Оригинал Перевод
electrophoretic display (EPD), e-paper электрофоретический дисплей, электронная бумага
microcapsule, charged pigment particles микрокапсула с заряженными частицами краски
bistable бистабильный: картинка держится без питания
Driver Output Control (0x01) сколько строк у стекла и как их обходить
Data Entry Mode (0x11) куда идёт указатель памяти после байта
BW RAM (0x24), RED RAM (0x26) память чёрного и память красного
Display Update Control 2 (0x22), Master Activation (0x20) что делать при обновлении и «пуск»
Display Mode 1 / Display Mode 2 полное и частичное обновление
LUT (waveform look-up table) таблица формы напряжений: что подавать на точку и сколько
TSSV (0x18), internal sensor 0x80 какой датчик температуры брать
Border Waveform Control (0x3C) что делать с рамкой вокруг поля
image retention (residual image) остаточное изображение — «призрак»
DSM (0x10 0x01) глубокий сон: выход только через RST

Вверху — подключение модуля 2,13″ на SSD1680 к STM32F103: SPI1 на 4 МГц, режим 0, только передача; PA5 — SCK, PA7 — MOSI (DIN), PA4 — CS, PA3 — DC, 0 — команда, 1 — данные, PA2 — RST, сброс и выход из сна, PA1 — вход BUSY, 1 — контроллер занят; модуль 250 × 122 от 3,3 В. Посередине — обновление во времени: RST и 12, настройка 01 11 44 45 3C 18, запись 24 — 4000 байт и 26 — 4000 байт, 0x22 F7 и 20, потом BUSY = 1 — 2,0 с полное и 0,40 с частичное, затем сон 10 01; полный кадр — 8017 байт по SPI за 16,0 мс; частичное — 0x22 FF и второй 24. Внизу — строка памяти: 16 байт, байт — 8 точек, старший бит — левая, в памяти чёрного 1 — белая, точка (8, 0) чёрная — байт 1 строки 0 равен 0x7F; две памяти частичного обновления: 0x24 — новый кадр, 0x26 — прошлый, двигаются только разные точки; без базового кадра — мусор, на 6-м частичном подряд в модели появляются серые «призраки», поэтому раз в несколько частичных — полное


22н.1 Бумага, модули и выводы

Как устроена бумага (по описаниям производителей, не проверено). Поле — слой крошечных капсул с прозрачной жидкостью, в которой плавают заряженные частицы: белые одного знака, чёрные — другого (в трёхцветных — ещё красные). Напряжение между верхним и нижним электродом тянет к зрителю одни частицы и прячет другие. Сняли напряжение — частицы стоят где стояли: картинка держится без питания, и ток идёт только пока она меняется. Отсюда три свойства, с которыми имеет дело программа:

Модель всё это не считает — только следствия: обновление держит BUSY заданное время, частичное копит «недожим» (22н.5), холод растягивает время (22н.7).

Модули. SSD1680 в Zephyr 4.4.2 описан как контроллер до 296×176 точек; рядом в том же драйвере — родственники SSD1608, SSD1673, SSD1675A и SSD1681 со своими пределами. У старшего поколения (SSD1608, SSD1673, SSD1675A) другие биты команды 0x22, так что «совместимый» — не значит «тот же код». В разделе два модуля на SSD1680:

Модуль Точек Память X × Y Байт на цвет
2,13″ 250 × 122 16 байт × 250 строк 4000
2,9″ 296 × 128 16 байт × 296 строк 4736

Память контроллера лежит строками: X — по 8 точек в байте, Y — номер строки. Короткая сторона стекла — это X: 122 и 128 точек дают по 16 байт. Длинная — строки: 250 и 296. Модуль 2,13″ так и описан в Zephyr: 250 по ширине, 122 по высоте, датчик 0x80, рамка 0x05 и 0x3C, SPI — 4 МГц.

Выводы. Шина — SPI только на передачу (Zephyr подключает такие модули как «4-проводный SPI»: такт, данные, CS и линия DC), плюс сброс и BUSY:

Модуль STM32F103 Что
CLK (SCK) PA5 такт SPI1
DIN (MOSI) PA7 данные SPI1
CS PA4 0 — идёт посылка
DC PA3 0 — байт команды, 1 — её данные
RST PA2 0 — сброс; только он выводит из глубокого сна
BUSY PA1, вход 1 — контроллер занят, команды не слать
VCC, GND 3,3 В и земля

PA1–PA7 без допуска 5 В (DocID13587, с. 29): модуль — от 3,3 В. Ядро в прошивке остаётся на HSI 8 МГц, как после сброса: для батарейки 72 МГц не нужны. SPI1 — делитель /2 (RM0008, с. 743), 4 МГц, как у модуля в Zephyr; режим 0. Посылка — команда при DC = 0, потом её данные при DC = 1, всё под одним CS = 0. DC менять, только когда байт ушёл целиком: TXE, потом BSY (с. 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 Инициализация: сброс, SW reset, BUSY и окно памяти

Порядок — как в драйвере Zephyr: аппаратный сброс, ждать BUSY, SW reset 0x12, снова ждать BUSY, потом настройка. SW reset возвращает регистры к значениям по умолчанию и не трогает память (так пишет Zephyr) — поэтому программа не обязана заново слать кадр после каждого сброса.

/* Аппаратный сброс, 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);
}

Что ушло на модуль 2,13″ (журнал модели):

Команда Данные Что
RST — вывод в 0 на 10 мс, потом ждать BUSY
12 — SW reset; BUSY = 1, пока он идёт
01 F9 00 00 строк 250: 249 = 0x00F9 младшим байтом вперёд, и 0x00
11 03 X растёт, Y растёт, сначала X — как лежит буфер
44 00 0F окно X: байты 0…15
45 00 00 F9 00 окно Y: строки 0…249
3C 05 рамка — как у этого модуля в Zephyr для полного
18 80 свой датчик температуры
4E, 4F 00, 00 00 указатель — в начало окна

После сброса — 9 команд, 24 байта, 48 мкс на 4 МГц; у модуля 2,9″ отличаются две: 01 27 01 00 и 45 00 00 27 01 — 296 строк. Ждать BUSY — функция в несколько строк, но с тайм-аутом: если модуль не подключён или сломан, BUSY может висеть в 1 вечно.

/* 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;
}

В прошивке epd_port_delay_ms — сон в WFI до тика SysTick: пока BUSY стоит, ядро спит. Шаг опроса — 10 мс; на полное обновление модель насчитала 202 опроса BUSY.


22н.3 Две памяти, байты и поворот

У контроллера две памяти одного размера: 24 — чёрного, 26 — красного. В памяти чёрного бит 1 — белая точка, 0 — чёрная (Zephyr заливает её 0xFF, чтобы экран стал белым). В памяти красного у трёхцветного модуля 1 — красная точка. У чёрно-белого модуля красного нет, и вторая память работает как «прошлый кадр» для частичного обновления (22н.5).

Байт — 8 соседних точек строки, старший бит — левая (в Zephyr — SCREEN_INFO_MONO_MSB_FIRST). Это не как у SSD1306 в 22м, где байт — столбец из 8 точек и младший бит сверху:

/* Байт — 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;
}

Драйвер зажигал одну точку на белом экране, модель находила единственный изменившийся байт и единственную чёрную точку стекла:

Точка Поворот Байт Значение Строка, столбец памяти
(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

Строка — 16 байт, но у модуля 2,13″ точек 122 = 15 × 8 + 2: у последнего байта строки заняты два старших бита, шесть младших стекло не показывает. Буфер — 16 × 250 = 4000 байт на цвет, у 2,9″ — 16 × 296 = 4736. Тип epd_t держит оба цвета под больший модуль: armclang показал у прошивки 9516 байт статической памяти — 46 % из 20 Кбайт F103C8 (DocID13587, с. 1). Чёрно-белому модулю хватило бы одного буфера.

Поворот. Память — портрет: 122 точки по X и 250 строк. Метеостанции нужен альбом 250×122. Драйвер не трогает режим ввода, а пересчитывает точку в 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;
}

При повороте 1 точка (0, 0) экрана — это столбец 121 строки 0, байт 15, 0xBF; точка (249, 121) — столбец 0 строки 249. Буфер остаётся в осях памяти и уходит на модуль как есть.

Режим ввода и указатель — вместе. Повернуть картинку на 180° по Y можно и самим контроллером: 11 01 — Y убывает. Но указатель 4F должен тогда стоять на последней строке. В модели 11 01 с указателем на строке 0 дал картинку вверх ногами со сдвигом на строку: 2010 точек отличия от перевёрнутого эталона; с указателем на строке 249 — 0 отличий. Zephyr делает так же: перед заливкой ставит режим с убывающим Y и указатель на последнюю строку.


22н.4 Полное обновление: 0x24, 0x26, 0x22 и 0x20

Запись кадра — указатель в начало и одна посылка: команда и 16 × строк байт данных.

/* Весь цвет в память контроллера: указатель в начало окна, потом 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));
}

Сам показ — две команды: 22 говорит, что делать, 20 запускает. Биты 22 — из ssd16xx_regs.h Zephyr:

Бит Что
0x80, 0x40 включить генератор и аналоговую часть
0x20 взять температуру с датчика
0x10 загрузить таблицу формы напряжений (LUT) из OTP
0x08 второй режим — частичное обновление
0x04 показать
0x02, 0x01 выключить аналоговую часть и генератор

Полное — всё, кроме 0x08: 0xF7; частичное — всё: 0xFF. Так их собирает Zephyr для SSD1680 со своим датчиком. Код 0xFC — то же частичное без последних двух бит: генератор и аналоговая часть остаются включены после обновления. Драйвер раздела его не шлёт; что это даёт по времени и току — по документации производителя, не проверено.

/* 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);
}

После 20 BUSY обязан подняться. Если через 1 мс он всё ещё 0 — команды не дошли: контроллер спит или модуль не подключён (22н.7), и драйвер возвращает EPD_E_NOBUSY, а не «готово».

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;
}

Полное обновление кладёт кадр в обе памяти: в 24 — как новый, в 26 — как будущую базу для частичного. Кадр метеостанции в модели:

Что Значение
по SPI 8017 байт, 16,0 мс на 4 МГц
BUSY = 1 (модель, 25 °C) 2,0 с, драйвер намерил 2000 мс шагами по 10
отличий стекла от эталона 0 из 30 500 точек, чёрных 5075

Посылка — 0,8 % от времени обновления: ускорять SPI или ставить DMA здесь почти незачем, всё время уходит на частицы.

LUT. Форму напряжений контроллер берёт из своей памяти OTP — бит 0x10 в 22, — а нужную под температуру выбирает по датчику — бит 0x20 и команда 18 80. Свою таблицу можно положить командой 32 (в Zephyr — у профилей full и partial), но её содержимое под конкретное стекло даёт только его производитель: таблица от другого модуля может испортить картинку. Драйвер раздела своей LUT не шлёт.


22н.5 Частичное обновление и «призраки»

Частичное обновление не гонит весь экран через чёрное и белое. Контроллер сравнивает две памяти: в 24 — новый кадр, в 26 — прошлый, и двигает только точки, где они разные. Мигания нет, и в модели обновление идёт 0,40 с вместо 2,0 (по документации производителя, не проверено). После обновления памяти меняются местами — так пишет Zephyr, — и в 24 оказывается прошлый кадр. Поэтому драйвер после частичного шлёт новый кадр в 24 ещё раз: теперь в обеих памятях то, что на стекле, и следующее частичное начнётся с верной базы. Журнал модели: 24 3C:3C 22:FF 20 24.

От кадра «00:00» к «00:10» в модели должны были смениться 1332 точки — ровно столько и сдвинулось; отличий стекла от эталона 0, обе памяти после обновления совпали. По SPI — те же 8017 байт: два раза 24.

«Призраки». Частичное ведёт точку короче полного, и точка приходит не совсем в свой цвет — остаётся «недожим». От обновления к обновлению он копится, и там, где цифры менялись чаще всего, проступают серые следы. В модели это допущение: каждое частичное добавляет перекрашенной точке 0,02, остальным — 0,002, точка с недожимом больше 0,10 видна серой. Полное обновление стирает всё. Метеостанция шла частичными подряд:

Частичных подряд Серых точек Наибольший недожим
5 0 0,100
6 79 —
10 684 0,200
20 2636 0,400
30 2778 0,600

Первые серые точки — на 6-м частичном, в цифрах температуры и влажности. Отсюда правило драйвера: после 5 частичных — полное (full_every = 5, EPD_AUTO). Сутки метеостанции — 144 обновления раз в 10 минут — дали 24 полных и 120 частичных, серых точек — 0, отличий от эталона за все сутки — 0. Число 5 — следствие допущений модели; на настоящем стекле его подбирают глазами.

Рамка. Поле окружено рамкой, и у неё своя форма напряжений — команда 3C. Для полного у модуля 2,13″ в Zephyr — 0x05, для частичного — 0x3C; драйвер ставит её перед каждым обновлением.

Трёхцветный модуль. У модуля с красным в 26 — красный цвет, и «прошлого кадра» нет. Частичного драйвер для него не делает: epd_show с EPD_PARTIAL вернул EPD_E_NOBASE (−3). Полное в модели — 304 красных точек надписи «ALERT», отличий 0.


22н.6 Сон и батарейка: метеостанция раз в 10 минут

Между обновлениями модулю не нужно ничего: картинка держится сама. Значит, после обновления — глубокий сон 10 01. Выход из него — только аппаратный сброс; SPI в это время контроллер не слушает.

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 помнит, что контроллер спит, и начинает с controller_init: RST, 12, настройка. Память во сне в модели сохраняется (по документации производителя, не проверено), и следующее частичное идёт с той же базой.

Энергия одного обновления. Токи модуля в модели — допущения: при обновлении в среднем 4,0 мА, без сна 30 мкА, во сне 1 мкА. При 3,3 В:

Что Время (модель) Энергия Заряд
полное 2,0 с 26,40 мДж 2,222 мкА·ч
частичное 0,40 с 5,28 мДж 0,444 мкА·ч
посылка по SPI, ядро в работе (8,6 мА) 16,0 мс 456,4 мкДж

Сутки. Метеостанция просыпается раз в 10 минут, рисует кадр (работа программы — допущение: 20 мс на 8 МГц, 8,6 мА по DocID13587, с. 42), шлёт его, ждёт BUSY во сне и засыпает до следующего раза в Stop — 14 мкА (с. 45). Батарейка — та же, что в 79.3: 220 мА·ч. Число полных и частичных — из суточного прогона (24 и 120):

Как Модуль, мА·ч в сутки МК, мА·ч в сутки Средний ток Хватит, дней (лет)
частичное, полное после 5, модуль спит, МК в Stop 0,131 0,348 19,96 мкА 459 (1,26)
только полные 0,344 0,348 28,85 мкА 318 (0,87)
без сна модуля 0,826 0,348 48,93 мкА 187 (0,51)
МК ждёт BUSY в работе 0,131 0,577 29,50 мкА 311 (0,85)
МК спит в Standby 0,131 0,089 9,16 мкА 1001 (2,74)

Видно, кто съедает батарейку. Модуль в правильном режиме тратит меньше половины от МК: дорогие только обновления, а их 144 в сутки. Сон модуля — не мелочь: без 10 01 его 30 мкА покоя (допущение модели) за сутки набирают больше всех обновлений вместе. Ждать BUSY в работе, а не во сне, — средний ток 29,50 мкА вместо 19,96. А больше всего даёт сам МК: Stop у F103 — 14 мкА, и если спать в Standby (3,2 мкА с LSI для будильника, с. 45), батарейки хватит более чем вдвое дольше. Standby — это сброс на выходе (раздел 10.4): epd_t и счётчик частичных надо хранить в резервных регистрах, а кадр рисовать заново — он всё равно рисуется каждый раз.


22н.7 Грабли

Команды не ждали BUSY — часть пропала. Пока BUSY = 1, контроллер не слушает (в модели — так; по документации производителя, не проверено). Частичное и сразу, не дожидаясь BUSY, второй 24 и сон: в модели пропали 4 команды, стекло показало новый кадр, но в памяти чёрного остался прошлый, а модуль не уснул — те самые 30 мкА вместо 1. Ждать BUSY перед каждой командой после 12 и 20.

После сна экран не меняется, а ошибок нет. Контроллер в глубоком сне не слышит SPI, а BUSY у него 0 — наивный драйвер решит, что обновление прошло мгновенно. В модели 9 команд ушли в никуда, на стекле прошлый кадр (отличий от него 0). Драйвер раздела ловит это сам: после 20 BUSY не поднялся — EPD_E_NOBUSY (−2). Будит только RST: после сброса и controller_init следующее частичное прошло, отличий 0.

Частичное без базового кадра — каша. После включения питания в памяти контроллера мусор (допущение модели), а стекло хранит прошлую картинку. Частичное сравнит новый кадр с мусором и не сдвинет точки, где мусор случайно совпал с новым кадром. В модели из 1332 точек, которые надо было сменить, не на месте оказались 673. Первый показ после включения — только полный; epd_init сбрасывает base_ok, и EPD_AUTO делает полный сам: отличий 0.

Негатив, зеркальные восьмёрки, всё красное. В памяти чёрного 1 — белая. Буфер «1 — чёрная» даёт негатив: в модели отличаются все 30 500 точек. Младший бит слева — каждая восьмёрка точек зеркальна: 5874 отличий. У трёхцветного память красного, залитая 0xFF вместо 0x00, — весь экран красный: 30 500 точек из 30 500.

Картинка вверх ногами и на строку съехала. Режим ввода с убывающим Y, а указатель оставлен на строке 0 — 22н.3: 2010 отличий от перевёрнутого эталона. Режим и указатель меняют вместе.

На морозе — тайм-аут. Таблица под холод идёт дольше. В модели (допущение) время обновления растёт так, а драйвер с тайм-аутом 3 с ломается уже при 5 °C:

Температура Полное Частичное Тайм-аут 3 с Тайм-аут 10 с
25 °C 2,0 с 0,40 с ок ок
15 °C 2,6 с 0,52 с ок ок
5 °C 3,6 с 0,72 с EPD_E_TIMEOUT ок
−5 °C 5,2 с 1,04 с EPD_E_TIMEOUT ок
−15 °C 8,0 с 1,60 с EPD_E_TIMEOUT ок

Тайм-аут брать с запасом под самый холодный случай: в драйвере — 10 с. Время обновления на холоде растёт — и растут заряд и энергия.

Модуль на 5 В. PA1–PA7 у F103 без допуска 5 В (DocID13587, с. 29): модуль с логикой 5 В к ним напрямую не подключать.


22н.8 Итоги

Полные исходники

epd22n.h — команды и биты SSD1680, тип epd_t с буферами чёрного и красного, порт и прототипы драйвера.

/* 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 — драйвер: инициализация, BUSY, точка с поворотом, шрифт 5×7, полное и частичное обновление, сон — общий для прошивки и стенда.

/* 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 — прошивка STM32F103: SPI1 на 4 МГц, выводы CS, DC, RST и BUSY, метеостанция раз в 10 минут.

/* 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);
    }
}

Дальше: раздел 22о «Дисплей 320×240 на ILI9341: SPI с DMA против параллельной шины через FSMC на F407 — скорость заливки, окна, шрифты и разрыв кадра» — от бумаги, которая держит картинку без питания, к быстрому цветному экрану: 320×240 по SPI с DMA и по параллельной шине FSMC.

Открыть в интерактивном справочнике