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
22н.1 Бумага, модули и выводы
Как устроена бумага (по описаниям производителей, не проверено). Поле — слой крошечных капсул с прозрачной жидкостью, в которой плавают заряженные частицы: белые одного знака, чёрные — другого (в трёхцветных — ещё красные). Напряжение между верхним и нижним электродом тянет к зрителю одни частицы и прячет другие. Сняли напряжение — частицы стоят где стояли: картинка держится без питания, и ток идёт только пока она меняется. Отсюда три свойства, с которыми имеет дело программа:
- кадр меняется медленно: частицы плывут в жидкости, и контроллер ведёт каждую точку по таблице напряжений (LUT) — серией импульсов разного знака, а не одним переключением;
- полное обновление мигает: чтобы точка пришла в точный цвет независимо от прошлого, её прогоняют через чёрное и белое — весь экран вспыхивает;
- на холоде жидкость гуще, частицы медленнее, и та же таблица идёт дольше; поэтому контроллер выбирает таблицу по температуре.
Модель всё это не считает — только следствия: обновление держит 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 Итоги
- Электронная бумага держит картинку без питания; ток идёт только при обновлении. SSD1680: SPI на передачу, DC, CS, RST и BUSY (1 — занят).
- Инициализация: RST,
12, ждать BUSY;01 F9 00 00,11 03,44 00 0F,45 00 00 F9 00,3C 05,18 80,4E/4F— 9 команд и 24 байта у 2,13″; у 2,9″ —01 27 01 00и 296 строк. - Память строками: байт — 8 точек, старший бит — левая; в памяти чёрного 1 — белая. Точка (8, 0) — байт 1, 0x7F. Буфер — 4000 байт на цвет у 2,13″ и 4736 у 2,9″.
- Полное:
24и26,22 F7,20, BUSY — в модели 2,0 с; по SPI 8017 байт за 16,0 мс. Частичное:22 FF, 0,40 с, двигаются только изменившиеся точки (1332 из 30 500); после него24ещё раз. - «Призраки»: в модели — на 6-м частичном подряд; полное после 5 частичных — сутки без единой серой точки (24 полных, 120 частичных).
- Сон
10 01, выход — только RST; без RST команды теряются, а драйвер без проверки BUSY решит, что всё прошло. - Метеостанции раз в 10 минут батарейки 220 мА·ч хватает на 459 суток; без сна модуля — на 187, с МК в Standby — на 1001.
Полные исходники
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.