22м. OLED-дисплей SSD1306 128×64 по I2C: страницы, буфер кадра и вывод через DMA
Цветной TFT на ST7735 разобран в 22з: 20 480 точек по два байта, кадр в ОЗУ F103 не помещается, и картинку рисуют окнами прямо в память дисплея. Общий взгляд на дисплеи — в 22а. Здесь другой край: маленький монохромный OLED 128×64 на контроллере SSD1306, тот самый «0,96 дюйма» с четырьмя выводами. Точка — один бит, весь кадр — 1024 байта, и его можно держать в ОЗУ целиком. Значит, рисует программа у себя в буфере, а на дисплей уходит готовый кадр — одной транзакцией I2C через DMA. Какой у модуля адрес, что за байт идёт после адреса, как включить экран (и почему он остаётся чёрным), как память разбита на страницы и как бит становится точкой, сколько стоит кадр на 100 и 400 кГц, как слать только изменившиеся страницы и чем от SSD1306 отличается похожий SH1106 — и что ломается на каждом шаге.
Опоры: RM0008 (DocID13902 Rev 17): запрос I2C1_TX — канал 6 DMA1,
с. 281; CNDTR — до 65 535, с. 286; I2C до 400 кГц, с. 752; заголовок
10-битного адреса 11110xx0, с. 755 и 759; STOP — после BTF, с. 760;
передача через DMA: DMAEN до события ADDR, после конца DMA снять
запросы, дождаться BTF и только потом STOP, с. 769; бит DMAEN в
I2C_CR2, с. 775; I2C_CCR и I2C_TRISE, с. 783–784. DocID13587 Rev 17
(STM32F103x8/xB): 20 Кбайт ОЗУ, с. 1; PB6 — I2C1_SCL, PB7 — I2C1_SDA,
оба FT, с. 32; фронт SCL и SDA в быстром режиме — до 300 нс, в
стандартном — до 1000 нс, ёмкость линии — до 400 пФ, с. 69. HAL F4
(STM32F4xx DFP 2.17.1): адрес из документа на микросхему перед вызовом
надо сдвинуть влево. Даташитов SSD1306 и SH1106 в проекте нет. Коды
команд и их параметры, байт управления (0x00, 0x40, 0x80), накачка
0x8D 0x14 и 0x10, режимы адресации, раскладка COM 0x12 и 0x02, значение
0x80 для частоты, 0xF1 для заряда при своей накачке, контраст по
умолчанию 128, «младший бит — верхняя точка», адреса 0x3C и 0x3D, у SH1106
— 132 столбца, сдвиг 2, только страничный режим и преобразователь
0xAD 0x8B — сверены с драйверами на диске: Zephyr 4.4.2, Klipper, QMK,
openmv, IronOS и Betaflight. Сброс в известное состояние после включения,
поведение указателя на краях окна, мусор в памяти после включения,
реакция SH1106 на чужие команды и разводка стекла модуля — по
документации производителя, не сверено: в модели это допущения. Все
числа ниже — прогон драйвера _verify/oled22m/oled22m.c, собранного
arm-none-eabi-gcc 12.2.1 под Cortex-M3 и запущенного в QEMU 7.2
(stm32vldiscovery), против своей модели SSD1306 и SH1106 на Python 3.11;
I2C и DMA в стенде подменены строками по USART1, картинку стекла модель
сравнивает с эталоном точка в точку. Прошивка для F103 собрана
armclang 6.24.
Как это называется в документации
Оригинал Перевод GDDRAM (graphic display data RAM) память кадра в контроллере: 8 страниц × 128 байт page, PAGE0…PAGE7 страница — полоса из 8 строк SEG, COM выводы столбцов и строк стекла control byte, Co, D/C# байт управления: «дальше один байт» и «команда или данные» charge pump накачка: своё высокое напряжение для стекла multiplex ratio сколько строк стекла опрашивать display start line, display offset с какой строки памяти начинать; сдвиг строк segment re-map, COM scan direction переворот столбцов и строк COM pins hardware configuration раскладка выводов строк под стекло horizontal / vertical / page addressing mode горизонтальный, вертикальный и страничный режимы адресации contrast control контраст: ток через точку pre-charge period, VCOMH заряд перед точкой; напряжение невыбранной строки burn-in выгорание: точки, горящие дольше, тускнеют dirty page «грязная» страница — менялась после отправки
22м.1 Модуль, адрес и байт управления
Модуль — стекло 128×64 с контроллером SSD1306 и четыре вывода: питание,
земля, SCL и SDA. Адрес на шине — 0x3C; бывает и 0x3D (в Zephyr 4.4.2
так подключён SSD1306 на одной из плат, ssd1306@3d). По документации
производителя адрес выбирает вывод SA0 — на модулях это перемычка или
резистор (не сверено).
| Модуль | STM32F103 | Что |
|---|---|---|
| SCL, SDA | PB6, PB7 | I2C1, 400 кГц; адрес 0x3C |
| VCC, GND | 3,3 В и земля | общая земля с микроконтроллером |
PB6 и PB7 — FT (DocID13587, с. 32): если подтяжки модуля идут к 5 В,
выводы это выдержат. I2C1 — как в 22и.2: 36 МГц / (3 × 30) = 400 кГц,
TRISE = 11 (RM0008, с. 783–784). Здесь же — адрес регистра данных для
DMA: запрос I2C1_TX приходит на канал 6 DMA1 (с. 281):
static void i2c1_init(void)
{
I2C1->CR1 = 0; /* настраивать при PE = 0 */
I2C1->CR2 = APB1_MHZ;
I2C1->CCR = I2C_CCR_FS | 30u; /* DUTY = 0: 36 МГц / (3 × 30) = 400 кГц */
I2C1->TRISE = APB1_MHZ * 300u / 1000u + 1u; /* 300 нс × 36 МГц = 10,8 → 10 + 1 = 11 */
I2C1->CR1 = I2C_CR1_PE;
DMA1_Channel6->CPAR = (uint32_t)&I2C1->DR; /* I2C1_TX — канал 6 DMA1 */
NVIC_EnableIRQ(DMA1_Channel6_IRQn);
}
Байт управления. После адреса контроллер ждёт не команду, а байт
управления. Бит 7 — Co, бит 6 — D/C#. При Co = 0 этот байт один на всю
транзакцию: 0x00 — дальше до STOP только команды, 0x40 — только данные в
память. При Co = 1 после одного байта снова идёт байт управления: 0x80 —
одна команда, 0xC0 — один байт данных. Так эти четыре значения описаны в
Zephyr; openmv шлёт команды по одной с 0x80 и данные — с 0x40. Модель
разбирает оба вида: транзакция 80 8D 80 14 80 AF C0 FF 00 20 02 дала
команды 8D:14, AF, один байт данных 0xFF в столбец 0 и команду
20:02. Драйвер раздела пользуется только 0x00 и 0x40: одна транзакция
— одна команда за другой или один кадр подряд; 0x80 перед каждым байтом
удвоил бы поток.
Адрес на проводе. На проводе первый байт — адрес, сдвинутый на бит влево, и бит направления: 0x3C даёт 0x78. Отсюда два вида ошибки. Взять 0x78 из описания модуля за 7-битный адрес — первым байтом уйдёт 0xF0, а это заголовок 10-битного адреса 11110xx0 (RM0008, с. 755 и 759): ответа нет. Отдать 0x3C функции, которая ждёт уже сдвинутый адрес (так устроен HAL: «адрес надо сдвинуть влево перед вызовом»), — на провод уйдёт 0x3C, то есть 7-битный 0x1E. Драйвер раздела берёт 7 бит и сдвигает сам:
| Вызов | Первый байт на проводе | Итог в модели |
|---|---|---|
oled_init(…, 0x3C, …) |
0x78 | OLED_OK, 4 транзакции |
oled_init(…, 0x78, …) — 8 бит вместо 7 |
0xF0 | OLED_E_NACK |
| 0x3C в функцию, которая ждёт 8 бит | 0x3C | OLED_E_NACK |
| модуль на 0x3D, драйвер — на 0x3C | 0x78 | OLED_E_NACK |
22м.2 Инициализация: от «выключить» до «включить»
Порядок команд — как в Klipper: выключить показ, частота, мультиплекс, смещение, стартовая строка, накачка, режим адресации, переворот столбцов и строк, раскладка COM, контраст, заряд и VCOMH, показ памяти без инверсии. Всё это — одна транзакция: байт 0x00 и 24 байта команд с параметрами.
/* Инициализация SSD1306 — одной транзакцией команд: выключить, частота, мультиплекс, смещение, стартовая
* строка, накачка, горизонтальный режим, переворот сегментов и строк, раскладка COM, контраст, заряд,
* VCOMH, показ памяти, без инверсии. Включение (0xAF) — отдельно, когда память уже очищена. */
static const uint8_t init_ssd1306[] = {
SSD_DISPLAY_OFF, SSD_CLOCK_DIV, 0x80, SSD_MULTIPLEX, OLED_H - 1u, SSD_OFFSET, 0x00, SSD_START_LINE | 0u,
SSD_CHARGE_PUMP, SSD_PUMP_ON, SSD_MEM_MODE, SSD_MODE_HORIZ, SSD_SEG_REMAP, SSD_COM_DEC, SSD_COM_PINS, 0x12,
SSD_CONTRAST, 0x80, SSD_PRECHARGE, 0xF1, SSD_VCOM, 0x20, SSD_RAM_ON, SSD_NORMAL,
};
Что значит каждая пара:
| Команда | Параметр | Что |
|---|---|---|
AE |
— | показ выключен, пока всё не настроено |
D5 |
80 |
частота генератора и делитель (так у Klipper, openmv и Zephyr) |
A8 |
3F |
64 строки: высота минус 1 |
D3 |
00 |
сдвига строк нет |
40 |
— | стартовая строка 0 |
8D |
14 |
накачка включена; 10 — выключена, стекло питают снаружи |
20 |
00 |
горизонтальный режим адресации (22м.3) |
A1, C8 |
— | столбец 127 — на SEG0, строки снизу вверх: так модуль показывает прямо |
DA |
12 |
раскладка COM для 128×64; у 128×32 — 02 |
81 |
80 |
контраст 128 — по умолчанию в Zephyr |
D9, DB |
F1, 20 |
заряд при своей накачке (openmv, Betaflight) и VCOMH (Zephyr) |
A4, A6 |
— | показывать память, без инверсии |
Включения (AF) в списке нет. Память после подачи питания — мусор (по
документации производителя; в модели 4122 горящих точки из 8192), и
включить показ до очистки — значит на мгновение показать этот мусор.
Поэтому oled_init сначала шлёт нули во весь кадр и только потом AF:
int oled_init(oled_t *o, uint8_t addr7, uint8_t kind, uint8_t col0)
{
static const uint8_t on = SSD_DISPLAY_ON;
int rc;
o->addr = addr7;
o->kind = kind;
o->col0 = col0;
o->hold = 0;
o->xfers = o->bytes = o->errors = 0;
o->tx[0] = OLED_CTL_DATA;
if (kind == OLED_SH1106)
rc = cmds(o, init_sh1106, sizeof init_sh1106);
else
rc = cmds(o, init_ssd1306, sizeof init_ssd1306);
if (rc != OLED_OK)
return rc;
oled_clear(o); /* память после включения — мусор: сначала ноль, потом показ */
if ((rc = oled_flush_all(o)) != OLED_OK)
return rc;
return cmds(o, &on, 1);
}
В модели oled_init прошла четырьмя транзакциями — команды (25 байт),
окно (7), кадр нулей (1025) и AF (2), всего 1059 байт и 24,0 мс на
400 кГц. В момент AF на стекле горело 0 точек; если включить показ
сразу после 8D 14, горят все 4122 точки мусора.
Накачка. Стеклу OLED нужно своё напряжение, выше питания логики (по
документации производителя, не сверено); у SSD1306 его делает своя
накачка, и включает её пара 8D 14 (Betaflight пишет прямо:
0x10 — внешнее питание, 0x14 — свой преобразователь). Без неё всё
остальное работает: команды проходят, память пишется, AF принят — а
стекло чёрное. В модели инициализация без 8D 14: память совпала с
эталоном (1484 точки), на стекле — 0; одна транзакция 00 8D 14 после
этого зажгла все 1484, отличий от эталона 0.
22м.3 Память: 8 страниц, бит — точка, режимы адресации
Память кадра — 8 страниц по 128 байт. Страница — полоса из 8 строк, байт — столбец этой полосы, младший бит — верхняя точка (в Zephyr — «8 точек по вертикали в байте», и старший бит не первый). Точка (x, y) живёт в странице y / 8, столбце x, бите y % 8:
| Точка | Страница | Столбец | Байт |
|---|---|---|---|
| (0, 0) | 0 | 0 | 0x01 |
| (5, 13) | 1 | 5 | 0x20 |
| (64, 33) | 4 | 64 | 0x02 |
| (127, 63) | 7 | 127 | 0x80 |
Каждую точку драйвер зажёг одну на чистом экране; в памяти модели оказался ровно один ненулевой байт — тот, что в таблице, а на стекле загорелась ровно эта точка.
Режимы адресации решают, куда пойдёт следующий байт данных. В
горизонтальном (20 00) указатель идёт по столбцам окна, в конце строки
окна — на следующую страницу, после последней — на первую. Окно задают
21 (столбцы от и до) и 22 (страницы от и до). В вертикальном (20 01)
— наоборот: по страницам, потом следующий столбец. В страничном (20 02)
указатель идёт по столбцам одной страницы, а страницу выбирает команда
B0…B7; столбец ставят двумя командами по тетраде, 0x и 1x. Модель
послала одни и те же 1024 байта кадра проверки в трёх режимах:
| Режим | Отличий от эталона | Горит точек |
|---|---|---|
| горизонтальный, окно 0…127 × 0…7 | 0 | 1484 |
| вертикальный, то же окно | 2294 | 1484 |
| страничный, страница 0 | 3902 | 3794 |
В вертикальном режиме байты те же, но легли не туда. В страничном все
1024 байта ушли в страницу 0 по кругу, а остальные семь страниц остались с
мусором после включения. Буфер кадра раздела лежит страница за страницей
— ровно как его читает горизонтальный режим, поэтому драйвер ставит
20 00, и кадр уходит одним куском. Страничный режим нужен там, где
другого нет, — у SH1106 (22м.6).
22м.4 Буфер кадра, точка и шрифт 5×7
Буфер — 1024 байта в ОЗУ: 5 % из 20 Кбайт F103C8 (DocID13587, с. 1). В 22з кадр был 40 960 байт и в ОЗУ не помещался; здесь он помещается целиком, и рисовать можно как угодно: дисплей увидит только готовый кадр. Перед буфером драйвер держит ещё один байт — 0x40, чтобы кадр с байтом управления лежал подряд (22м.5).
/* Точка (x, y): страница y / 8, столбец x, бит y % 8 — младший бит сверху */
void oled_pixel(oled_t *o, int x, int y, int on)
{
uint8_t *b, m;
if (x < 0 || y < 0 || x >= (int)OLED_W || y >= (int)OLED_H)
return;
(void)oled_wait(o);
b = &OLED_FBP(o)[(uint32_t)(y >> 3) * OLED_W + (uint32_t)x];
m = (uint8_t)(1u << (y & 7));
if (!!(*b & m) == !!on)
return; /* не изменилась — страница не «грязная» */
*b ^= m;
o->dirty |= (uint8_t)(1u << (y >> 3));
}
Точка меняет страницу только если бит правда изменился: зажечь горящую точку — не повод слать страницу.
Шрифт — тот же массив 5×7, что в 22з: знак — 5 байт-столбцов, бит 0 — верхняя строка, от пробела до «Z». Столбец знака — это готовый байт страницы. Если строка текста стоит на границе страниц (y кратно 8), столбец ложится одним байтом. Если нет — он делится на хвосты двух соседних страниц:
/* 8 точек столбца x начиная со строки y: при y % 8 = 0 — один байт, иначе — хвосты двух соседних страниц */
static void put_col(oled_t *o, int x, int y, uint8_t bits)
{
int p = y >> 3, sh = y & 7;
uint8_t *fb = OLED_FBP(o);
if (x < 0 || x >= (int)OLED_W || y < 0 || y >= (int)OLED_H)
return;
for (int k = 0; k < 2 && p + k < (int)OLED_PAGES; k++) {
uint8_t m = (uint8_t)(k ? 0xFFu >> (8 - sh) : 0xFFu << sh);
uint8_t v = (uint8_t)(k ? bits >> (8 - sh) : bits << sh);
uint8_t *b = &fb[(uint32_t)(p + k) * OLED_W + (uint32_t)x];
if (!m)
break;
if ((*b & m) != v) {
*b = (uint8_t)((*b & ~m) | v);
o->dirty |= (uint8_t)(1u << (p + k));
}
}
}
/* Строка шрифтом 5×7 в клетках 6×8: клетка пишется целиком вместе с фоном — старый знак стирать не надо */
int oled_text(oled_t *o, int x, int y, const char *s)
{
(void)oled_wait(o);
for (; *s; s++, x += 6) {
char c = *s >= ' ' && *s <= 'Z' ? *s : ' ';
for (int i = 0; i < 6; i++)
put_col(o, x + i, y, i < 5 ? font5x7[c - ' '][i] : 0);
}
return x;
}
Клетка знака — 6×8 точек и пишется целиком вместе с фоном, так что старый
знак стирать не надо. Вот кусок экрана проверки — начало надписи «Y 27: PAGES 3-4»
с y = 27 (столбцы 0…39, # — точка горит; столбец 0 — рамка):
y=24 стр.3 |#.......................................|
y=25 стр.3 |#.......................................|
y=26 стр.3 |#.......................................|
y=27 стр.3 |#...#...#........###..#####.............|
y=28 стр.3 |#...#...#.......#...#.....#..##.........|
y=29 стр.3 |#....#.#............#....#...##.........|
y=30 стр.3 |#.....#............#....#...............|
y=31 стр.3 |#.....#...........#....#.....##.........|
y=32 стр.4 |#.....#..........#.....#.....##.........|
y=33 стр.4 |#.....#.........#####..#................|
y=34 стр.4 |#.......................................|
y=35 стр.4 |#.......................................|
y=36 стр.4 |#.......................................|
y=37 стр.4 |#.......................................|
y=38 стр.4 |#.......................................|
y=39 стр.4 |#.......................................|
Строки 27…31 лежат в странице 3, строки 32 и 33 — в странице 4. Для такой строки текста «грязными» становятся обе страницы.
Картинка проверки — рамка, четыре строки текста (три из них не на границе страниц), полоса в рамке — дала на стекле модели 1484 горящих точки, отличий от эталона на Python — 0, память контроллера совпала с буфером байт в байт.
22м.5 Кадр одной транзакцией через DMA: сколько это времени
Байт на I2C — 9 тактов SCL: 8 бит и подтверждение. Кадр — байт
управления и 1024 байта, 1025 × 9 = 9225 тактов: на 100 кГц — 92,25 мс
(10,8 кадра в секунду), на 400 кГц — 23,06 мс (43,4). Модель считает
точнее: ещё START, адрес и STOP, а перед кадром — окно 21 00 7F 22 00 07
отдельной транзакцией:
/* Окно для страниц p…q: SSD1306 — горизонтальный режим, столбцы и страницы от и до (7 байт);
* SH1106 — только страничный: страница и столбец col0 тетрадами (4 байта), на каждую страницу своё окно */
static int window(oled_t *o, uint8_t p, uint8_t q)
{
uint8_t c[6];
if (o->kind == OLED_SH1106) {
c[0] = (uint8_t)(SSD_PAGE_START | p);
c[1] = (uint8_t)(SSD_COL_LOW | (o->col0 & 0x0Fu));
c[2] = (uint8_t)(SSD_COL_HIGH | (o->col0 >> 4));
return cmds(o, c, 3);
}
c[0] = SSD_COL_ADDR;
c[1] = o->col0;
c[2] = (uint8_t)(o->col0 + OLED_W - 1u);
c[3] = SSD_PAGE_ADDR;
c[4] = p;
c[5] = q;
return cmds(o, c, 6);
}
| Что | Транзакций | Байт | Тактов SCL | 400 кГц | 100 кГц |
|---|---|---|---|---|---|
| весь кадр: окно и 1025 байт | 2 | 1032 | 9310 | 23,295 мс, 42,9 кадра/с | 93,120 мс, 10,7 кадра/с |
| весь кадр по байту: окно и байт на каждый | 2048 | 9216 | 105 472 | 304,620 мс, 3,3 кадра/с | 1095,660 мс, 0,9 кадра/с |
| одна страница: окно и 129 байт | 2 | 136 | 1246 | 3,135 мс | 12,480 мс |
| две соседние страницы | 2 | 264 | 2398 | 6,015 мс | 24,000 мс |
| страницы 0 и 7 | 4 | 272 | 2492 | 6,290 мс | 24,980 мс |
Время — от первого START до последнего STOP, с 20 мкс программы между транзакциями (допущение модели). По байту выходит в 9 раз больше байт и в 13 раз дольше: на каждый байт — свои START, адрес, байт управления и окно.
Один кусок памяти. Чтобы DMA отдал кадр одной передачей, байт
управления и кадр должны лежать подряд. Поэтому буфер в oled_t — это
tx[1 + 1024]: tx[0] = 0x40, дальше страницы 0…7. Для страниц с 1-й
перед ними стоит последний байт предыдущей страницы; на время передачи
драйвер кладёт туда 0x40, а oled_wait после конца DMA возвращает его
на место:
/* Страницы p…q одной транзакцией через DMA. Перед данными нужен байт 0x40: для страницы 0 это tx[0], для
* остальных — последний байт предыдущей страницы; на время передачи в нём 0x40, oled_wait вернёт его. */
static int send_pages(oled_t *o, uint8_t p, uint8_t q)
{
uint8_t *start = o->tx + (uint32_t)p * OLED_W;
uint32_t n = 1u + (uint32_t)(q - p + 1u) * OLED_W;
int rc;
o->held = *start;
*start = OLED_CTL_DATA;
o->hold = start;
rc = oled_port_dma(o->addr, start, n);
o->xfers++;
o->bytes += n;
if (rc != OLED_OK) { /* не запустилась: ждать нечего */
*start = o->held;
o->hold = 0;
o->errors++;
}
return rc;
}
Проверка в модели: столбец 127 страницы 2 горел целиком (байт 0xFF), потом
изменилась только страница 3 — на шину ушли 2 транзакции и 136 байт, и
передача шла с этого самого байта. После неё oled_get(127, 23) дал 1,
следующий полный кадр совпал с эталоном: отличий 0.
DMA на F103. Канал 6, память → I2C_DR, шаг по памяти, байтами;
CNDTR вмещает до 65 535 (RM0008, с. 286), кадру хватает 1025. По
RM0008 (с. 769) канал и бит DMAEN (с. 775) готовят до события ADDR;
после конца передачи снимают запросы DMA, ждут BTF и только потом
STOP:
/* Передача через DMA (RM0008, с. 769): канал готов и DMAEN стоит до события ADDR; дальше на каждый TXE
* DMA сам кладёт байт в I2C_DR. Конец ловит прерывание канала. */
int oled_port_dma(uint8_t addr7, const uint8_t *tx, uint32_t n)
{
int rc;
DMA1_Channel6->CCR = 0;
DMA1_Channel6->CMAR = (uint32_t)tx;
DMA1_Channel6->CNDTR = n; /* до 65 535; кадр — 1025 */
DMA1_Channel6->CCR = DMA_CCR6_MINC | DMA_CCR6_DIR | DMA_CCR6_TCIE | DMA_CCR6_PL_1; /* память → I2C_DR */
I2C1->CR2 |= I2C_CR2_DMAEN;
dma_rc = OLED_OK;
dma_busy = 1;
DMA1_Channel6->CCR |= DMA_CCR6_EN;
if ((rc = start_addr(addr7)) != OLED_OK) {
DMA1_Channel6->CCR = 0;
I2C1->CR2 &= (uint16_t)~I2C_CR2_DMAEN;
dma_busy = 0;
return fail(rc);
}
return OLED_OK;
}
/* DMA отдал последний байт: снять запросы DMA, дождаться BTF и только потом STOP (RM0008, с. 769) */
void DMA1_Channel6_IRQHandler(void)
{
DMA1->IFCR = DMA_IFCR_CTCIF6;
DMA1_Channel6->CCR &= (uint16_t)~DMA_CCR6_EN;
I2C1->CR2 &= (uint16_t)~I2C_CR2_DMAEN;
while (!(I2C1->SR1 & (I2C_SR1_BTF | I2C_SR1_AF))) {
}
if (I2C1->SR1 & I2C_SR1_AF) {
dma_rc = OLED_E_NACK;
I2C1->SR1 = (uint16_t)~I2C_SR1_AF;
}
I2C1->CR1 |= I2C_CR1_STOP;
dma_busy = 0;
}
Пока уходит кадр на 400 кГц, у ядра на 72 МГц есть около 1 660 500
тактов, на 100 кГц — 6 642 000: с опросом TXE всё это время ушло бы на
ожидание. oled_flush возвращается сразу после запуска DMA; следующая
правка буфера (oled_pixel, oled_text) сама зовёт oled_wait и
дождётся конца. Ждать BTF в прерывании приходится, пока уходят последние
байты, — порядка двух байт на шине, 45 мкс на 400 кГц.
22м.6 «Грязные» страницы, контраст и выгорание, SH1106
Слать только изменившееся. В oled_t — байт dirty, бит на страницу.
oled_flush идёт по страницам; подряд идущие «грязные» уходят одним
окном и одной транзакцией:
/* «Грязные» страницы: у SSD1306 подряд идущие — одним окном и одной транзакцией, у SH1106 — по одной */
int oled_flush(oled_t *o)
{
int rc = OLED_OK;
uint8_t p = 0;
while (p < OLED_PAGES) {
uint8_t q = p;
if (!(o->dirty & (1u << p))) {
p++;
continue;
}
if (o->kind != OLED_SH1106)
while (q + 1u < OLED_PAGES && (o->dirty & (1u << (q + 1u))))
q++;
if ((rc = window(o, p, q)) != OLED_OK || (rc = send_pages(o, p, q)) != OLED_OK)
return rc;
o->dirty &= (uint8_t)~(((1u << (q + 1u)) - 1u) & ~((1u << p) - 1u));
p = (uint8_t)(q + 1u);
}
return rc;
}
Счётчик «N:0000» на экране проверки стоит на y = 52 — это страницы 6 и 7.
Каждый новый номер — 2 транзакции и 264 байта вместо 1032, на 400 кГц —
6,015 мс вместо 23,295; картинка после каждого шага совпала с эталоном.
Повторная запись того же текста не пачкает страниц вовсе: oled_flush
не шлёт ничего. Счётчик в одной странице (y = 48) — 136 байт и 3,135 мс,
в 7,4 раза быстрее полного кадра.
Контраст — 81 и байт 0…255, одна транзакция из 3 байт. Zephyr
ставит по умолчанию 128, Klipper — 239. AE гасит показ, память и
настройки остаются: в модели после AE на стекле 0 точек, после AF —
снова картинка без единого отличия.
Выгорание. Точка OLED, которая горит дольше соседей, со временем тускнеет (по документации производителей, не проверено; скорость выгорания модель не считает). Модель считает другое — часы свечения каждой точки за сутки. Часы «12:34» обновлялись раз в 10 мин:
| Как показывать | Горело точек | Больше всех, ч | Точек дольше 20 ч | Точек дольше 12 ч |
|---|---|---|---|---|
| на месте сутки | 60 | 24,0 | 60 | 60 |
| сдвиг по кругу 3×3 на точку каждые 10 мин | 219 | 16,0 | 0 | 14 |
на месте, ночью 8 ч выключено (AE) |
60 | 16,0 | 0 | 60 |
Сдвиг на одну-две точки глазу почти не виден, а самая загруженная точка горит 16 ч вместо 24, и свечение размазано по 219 точкам. Меньший контраст и выключение на ночь — второй и третий способы.
SH1106 — похожий, но другой. SH1106 — другой контроллер, который
можно встретить на таком же с виду модуле; Zephyr, Klipper и QMK
поддерживают его рядом с SSD1306. Он понимает большинство тех же команд, но (по Zephyr, Klipper и QMK) у
него 132 столбца, и стекло 128 точек показывает столбцы со 2-го (в Zephyr
— segment-offset = <2>), горизонтального режима нет — только
страничный, а вместо накачки 8D 14 — преобразователь AD 8B. Драйвер
раздела для SH1106 шлёт каждую страницу отдельно: B0+p, столбец 2
тетрадами (02 10) и 129 байт. Кадр — 16 транзакций, 1064 байта, 24,680 мс
на 400 кГц (40,5 кадра/с) и 97,820 мс на 100 кГц (10,2).
| Модуль на SH1106, драйвер | Отличий от эталона | Что на стекле |
|---|---|---|
| для SH1106, столбец 2 | 0 | картинка |
| для SSD1306 | 3865 | мусор, наверху обрывок кадра со сдвигом строк |
| для SH1106, столбец 0 | 998 | картинка сдвинута на 2 точки влево, справа 2 столбца мусора |
Драйвер SSD1306 на SH1106 — ловушка. SH1106 не знает 20, 21, 22 и
8D и (в модели) читает их параметры как новые команды: 7F из окна
стала стартовой строкой 63, 14 из накачки — старшей тетрадой столбца.
Кадр в 1025 байт ушёл по кругу в одну страницу, остальные семь остались
с мусором после включения.
22м.7 Грабли
Экран чёрный, а обмен идёт без ошибок. Не включена накачка: нет
8D 14. Все ACK на месте, память пишется, AF принят — а светить
нечем. В модели: в памяти 1484 точки, на стекле 0.
Нет ответа ни на один байт. Адрес 0x78 (8 бит из описания модуля)
отдан как 7-битный — на проводе 0xF0, заголовок 10-битного адреса. Или
наоборот: 0x3C отдан функции, которая ждёт сдвинутый адрес, — на проводе
7-битный 0x1E. Или модуль переставлен на 0x3D. Во всех трёх случаях
oled_init вернул OLED_E_NACK на первой транзакции.
На 100 кГц работает, на 400 кГц — сбои. Подтяжки слишком слабые для быстрого режима. Фронт от 0,3 до 0,7 питания длится R · C · ln(0,7 / 0,3) = 0,8473 · R · C, а в быстром режиме он не длиннее 300 нс, в стандартном — 1000 нс (DocID13587, с. 69):
| Подтяжка | 50 пФ | 100 пФ | 200 пФ |
|---|---|---|---|
| 2,2 кОм | 93 нс | 186 нс | 373 нс — только 100 кГц |
| 4,7 кОм | 199 нс | 398 нс — только 100 кГц | 796 нс — только 100 кГц |
| 10 кОм | 424 нс — только 100 кГц | 847 нс — только 100 кГц | 1695 нс — никакой |
При 100 пФ на линии для 400 кГц подтяжка нужна не больше 3541 Ом. Ёмкость линии — провода, модуль и входы; на модуле подтяжки часто уже стоят, и вместе со своими они включаются параллельно.
Картинка вверх ногами или зеркально. A0/C0 вместо A1/C8 — или
модуль собран иначе. В модели одна A0 дала зеркало (отличий от
зеркального эталона 0, от прямого — 1376), A0 и C0 вместе —
поворот на 180° (1988 отличий от прямого, 0 от повёрнутого). Пару
меняют под модуль, буфер не трогают.
Картинка через строку, растянута по высоте. Раскладка COM не под
стекло: на модуле 128×64 записали DA 02 (от модуля 128×32). В модели —
1724 отличия от эталона; DA 12 вернул картинку.
При включении — вспышка мусора. AF раньше очистки памяти: в модели
горит 4122 точки из 8192. Драйвер шлёт AF последним, после кадра нулей.
На вид такой же модуль, а на экране мусор и обрывок картинки сверху. Это SH1106, а драйвер — для SSD1306: 3865 отличий. Для SH1106 — постранично и со столбца 2; со столбца 0 картинка уезжает на 2 точки влево, а справа остаются 2 столбца мусора.
Картинка на миг портится, если рисовать во время DMA. Буфер читает
DMA, и в байте перед окном лежит 0x40. Каждая функция рисования начинает
с oled_wait — правка ждёт конца передачи.
Помехи от накачки. Накачка дисплея работает импульсами и тянет ток из 3,3 В рывками; на той же линии питания это видно как пульсации. АЦП и чувствительные датчики лучше питать отдельно или развязать по питанию Модель этого не считает, и чисел здесь нет.
Модуль на 5 В. Если модуль питают от 5 В, а подтяжки стоят на нём, SCL и SDA поднимаются до 5 В. PB6 и PB7 — FT, им это не страшно (DocID13587, с. 32); вывод без FT на той же шине — другое дело. Проще питать модуль от 3,3 В.
22м.8 Итоги
- SSD1306 по I2C: адрес 0x3C (или 0x3D), на проводе 0x78; после адреса
— байт управления: 0x00 — команды, 0x40 — данные, 0x80 — один байт.
0x78 как 7-битный — заголовок 10-битного адреса и
OLED_E_NACK. - Инициализация — одна транзакция из 25 байт; накачка
8D 14обязательна: без неё в памяти 1484 точки, на стекле 0.AF— после очистки, иначе 4122 точки мусора. - Память — 8 страниц × 128 байт, байт — столбец, бит 0 сверху: точка (5, 13) — страница 1, столбец 5, байт 0x20. Горизонтальный режим читает буфер так, как он лежит: 0 отличий; вертикальный — 2294, страничный — 3902.
- Буфер кадра — 1024 байта (5 % ОЗУ F103C8) плюс байт 0x40 перед ним: кадр уходит одной передачей DMA, канал 6, 1025 байт.
- Кадр: 1025 × 9 = 9225 тактов — 23,06 мс (43,4 кадра/с) на 400 кГц и 92,25 мс (10,8) на 100 кГц; с окном, адресом и паузами — 23,295 мс (42,9) и 93,120 мс (10,7). По байту — 304,620 мс.
- «Грязные» страницы: одна — 136 байт и 3,135 мс, в 7,4 раза быстрее кадра; две соседние — 264 байта, одной транзакцией.
- Выгорание: сутки на месте — 60 точек по 24 ч; сдвиг 3×3 — самая загруженная точка 16,0 ч.
- SH1106: 132 столбца, сдвиг 2, только страничный режим — 16 транзакций на кадр; драйвер SSD1306 на нём — 3865 отличий.
Полные исходники
oled22m.h — команды SSD1306 и SH1106, байт управления, тип oled_t с буфером кадра, порт и прототипы драйвера.
/* oled22m.h — раздел 22м: монохромный OLED 128×64 на SSD1306 (и SH1106) по I2C без регистров микроконтроллера.
* Драйвер говорит с дисплеем через oled_port_write / oled_port_dma / oled_port_wait: в прошивке fw22m.c их
* делают I2C1 и DMA1 канал 6 STM32F103, в стенде qhost22m.c — строки по USART1 к модели model22m.py (QEMU).
* Коды команд, байт управления 0x00 / 0x40 / 0x80, значения 0x14 и 0x10 для накачки, 0x12 для раскладки COM,
* 0x80 для частоты, смещение столбца SH1106 и его «только страничный режим» сверены с драйверами Zephyr 4.4.2,
* Klipper, QMK, openmv и IronOS на диске; даташитов SSD1306 и SH1106 в проекте нет. */
#ifndef OLED22M_H
#define OLED22M_H
#include <stdint.h>
#define OLED_ADDR 0x3Cu /* 7 бит; второй адрес модуля — 0x3D */
#define OLED_W 128u
#define OLED_H 64u
#define OLED_PAGES 8u /* страница — 8 строк: байт = столбец из 8 точек, бит 0 — верхняя */
#define OLED_FB (OLED_W * OLED_PAGES) /* 1024 байта на кадр */
/* ── байт управления: первый байт после адреса ─────────────────────────── */
#define OLED_CTL_CMD 0x00u /* Co = 0, D/C# = 0: до STOP — только команды */
#define OLED_CTL_DATA 0x40u /* Co = 0, D/C# = 1: до STOP — только данные в память */
#define OLED_CTL_CO 0x80u /* Co = 1: после одного байта — снова байт управления */
/* ── команды SSD1306 ───────────────────────────────────────────────────── */
#define SSD_DISPLAY_OFF 0xAEu
#define SSD_DISPLAY_ON 0xAFu
#define SSD_CLOCK_DIV 0xD5u /* + байт: старшая тетрада — частота генератора, младшая — делитель */
#define SSD_MULTIPLEX 0xA8u /* + байт: строк минус 1 — 0x3F для 64 */
#define SSD_OFFSET 0xD3u /* + байт: сдвиг строк */
#define SSD_START_LINE 0x40u /* | 0…63: с какой строки памяти начинать */
#define SSD_CHARGE_PUMP 0x8Du /* + байт: 0x14 — накачка включена, 0x10 — выключена */
#define SSD_PUMP_ON 0x14u
#define SSD_MEM_MODE 0x20u /* + байт: 0x00 — горизонтальный, 0x01 — вертикальный, 0x02 — страничный */
#define SSD_MODE_HORIZ 0x00u
#define SSD_SEG_REMAP 0xA1u /* столбец 127 — на SEG0; 0xA0 — без переворота */
#define SSD_COM_DEC 0xC8u /* строки снизу вверх; 0xC0 — сверху вниз */
#define SSD_COM_PINS 0xDAu /* + байт: 0x12 — 128×64, 0x02 — 128×32 */
#define SSD_CONTRAST 0x81u /* + байт 0…255 */
#define SSD_PRECHARGE 0xD9u /* + байт */
#define SSD_VCOM 0xDBu /* + байт */
#define SSD_RAM_ON 0xA4u /* показывать память; 0xA5 — все точки горят */
#define SSD_NORMAL 0xA6u /* 0xA7 — инверсия */
#define SSD_COL_ADDR 0x21u /* + 2 байта: окно столбцов от и до (горизонтальный и вертикальный режимы) */
#define SSD_PAGE_ADDR 0x22u /* + 2 байта: окно страниц от и до */
#define SSD_PAGE_START 0xB0u /* | 0…7: страница — страничный режим */
#define SSD_COL_LOW 0x00u /* | младшая тетрада столбца — страничный режим */
#define SSD_COL_HIGH 0x10u /* | старшая тетрада столбца */
#define SH_DCDC 0xADu /* SH1106: + байт 0x8B — преобразователь включён (вместо 0x8D 0x14) */
#define SH_DCDC_ON 0x8Bu
#define SH_PUMP_90 0x33u /* SH1106: напряжение накачки — так шлёт Zephyr */
enum { OLED_SSD1306 = 0, OLED_SH1106 = 1 };
enum { OLED_OK = 0, OLED_E_NACK = -1, OLED_E_BUS = -2, OLED_E_ARG = -3 };
typedef struct {
uint8_t addr; /* 7 бит */
uint8_t kind; /* OLED_SSD1306 или OLED_SH1106 */
uint8_t col0; /* первый столбец стекла в памяти: 0 у SSD1306, 2 у SH1106 (132 столбца) */
uint8_t dirty; /* бит p — страница p менялась после отправки */
uint8_t *hold; /* байт перед окном DMA: на время передачи в нём 0x40 */
uint8_t held; /* что в нём было */
uint32_t xfers, bytes, errors; /* транзакций, байт после адреса, ошибок шины */
uint8_t tx[1 + OLED_FB]; /* tx[0] = 0x40 — байт управления, tx[1…1024] — буфер кадра */
} oled_t;
#define OLED_FBP(o) ((o)->tx + 1)
/* ── порт: в прошивке — I2C1 и DMA1, в стенде — строки к модели ──────────── */
int oled_port_write(uint8_t addr7, const uint8_t *tx, uint32_t n); /* START, адрес, n байт, STOP — и ждать */
int oled_port_dma(uint8_t addr7, const uint8_t *tx, uint32_t n); /* то же через DMA: запустить и вернуться */
int oled_port_wait(void); /* дождаться конца DMA; код ошибки */
/* ── драйвер ─────────────────────────────────────────────────────────── */
int oled_init(oled_t *o, uint8_t addr7, uint8_t kind, uint8_t col0);
int oled_wait(oled_t *o); /* перед правкой буфера: DMA ещё может читать его */
void oled_clear(oled_t *o);
void oled_pixel(oled_t *o, int x, int y, int on);
int oled_get(oled_t *o, int x, int y);
void oled_rect(oled_t *o, int x, int y, int w, int h, int fill);
int oled_text(oled_t *o, int x, int y, const char *s); /* шрифт 5×7, клетка 6×8; y — любой; вернёт x после текста */
int oled_flush(oled_t *o); /* отправить «грязные» страницы */
int oled_flush_all(oled_t *o);
int oled_contrast(oled_t *o, uint8_t v);
int oled_power(oled_t *o, int on); /* 0xAE / 0xAF: память и настройки сохраняются */
#endif
oled22m.c — драйвер: инициализация, точка, шрифт 5×7, окно, «грязные» страницы и кадр через DMA — общий для прошивки и стенда.
/* oled22m.c — раздел 22м: драйвер OLED 128×64 (SSD1306, SH1106): инициализация, буфер кадра, точка, шрифт 5×7,
* «грязные» страницы и кадр одной транзакцией через DMA. Один и тот же файл собирается в прошивку (armclang,
* Cortex-M3) и в стенд (arm-none-eabi-gcc, QEMU): микроконтроллера он не знает, только порт oled_port_*. */
#include "oled22m.h"
/* Шрифт 5×7 — тот же, что в 22з: знак — 5 столбцов, бит 0 — верхняя строка; подряд от пробела до «Z».
* Столбец знака ложится в страницу SSD1306 как есть: бит 0 — верхняя точка страницы. */
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 */
};
/* Инициализация SSD1306 — одной транзакцией команд: выключить, частота, мультиплекс, смещение, стартовая
* строка, накачка, горизонтальный режим, переворот сегментов и строк, раскладка COM, контраст, заряд,
* VCOMH, показ памяти, без инверсии. Включение (0xAF) — отдельно, когда память уже очищена. */
static const uint8_t init_ssd1306[] = {
SSD_DISPLAY_OFF, SSD_CLOCK_DIV, 0x80, SSD_MULTIPLEX, OLED_H - 1u, SSD_OFFSET, 0x00, SSD_START_LINE | 0u,
SSD_CHARGE_PUMP, SSD_PUMP_ON, SSD_MEM_MODE, SSD_MODE_HORIZ, SSD_SEG_REMAP, SSD_COM_DEC, SSD_COM_PINS, 0x12,
SSD_CONTRAST, 0x80, SSD_PRECHARGE, 0xF1, SSD_VCOM, 0x20, SSD_RAM_ON, SSD_NORMAL,
};
/* SH1106: вместо 0x8D 0x14 — преобразователь 0xAD 0x8B, команды 0x20 нет — только страничный режим */
static const uint8_t init_sh1106[] = {
SSD_DISPLAY_OFF, SSD_CLOCK_DIV, 0x80, SSD_MULTIPLEX, OLED_H - 1u, SSD_OFFSET, 0x00, SSD_START_LINE | 0u,
SH_DCDC, SH_DCDC_ON, SH_PUMP_90, SSD_SEG_REMAP, SSD_COM_DEC, SSD_COM_PINS, 0x12,
SSD_CONTRAST, 0x80, SSD_PRECHARGE, 0x22, SSD_VCOM, 0x20, SSD_RAM_ON, SSD_NORMAL,
};
int oled_wait(oled_t *o)
{
int rc = OLED_OK;
if (o->hold) {
rc = oled_port_wait();
*o->hold = o->held; /* DMA дочитал — вернуть байт буфера на место */
o->hold = 0;
if (rc != OLED_OK)
o->errors++;
}
return rc;
}
static int wr(oled_t *o, const uint8_t *tx, uint32_t n)
{
int rc = oled_wait(o);
if (rc == OLED_OK)
rc = oled_port_write(o->addr, tx, n);
o->xfers++;
o->bytes += n;
if (rc != OLED_OK)
o->errors++;
return rc;
}
/* n команд одной транзакцией: байт управления 0x00, дальше все байты — команды и их параметры */
static int cmds(oled_t *o, const uint8_t *c, uint32_t n)
{
uint8_t tx[1 + sizeof init_ssd1306];
if (n >= sizeof tx)
return OLED_E_ARG;
tx[0] = OLED_CTL_CMD;
for (uint32_t i = 0; i < n; i++)
tx[1 + i] = c[i];
return wr(o, tx, n + 1u);
}
int oled_init(oled_t *o, uint8_t addr7, uint8_t kind, uint8_t col0)
{
static const uint8_t on = SSD_DISPLAY_ON;
int rc;
o->addr = addr7;
o->kind = kind;
o->col0 = col0;
o->hold = 0;
o->xfers = o->bytes = o->errors = 0;
o->tx[0] = OLED_CTL_DATA;
if (kind == OLED_SH1106)
rc = cmds(o, init_sh1106, sizeof init_sh1106);
else
rc = cmds(o, init_ssd1306, sizeof init_ssd1306);
if (rc != OLED_OK)
return rc;
oled_clear(o); /* память после включения — мусор: сначала ноль, потом показ */
if ((rc = oled_flush_all(o)) != OLED_OK)
return rc;
return cmds(o, &on, 1);
}
void oled_clear(oled_t *o)
{
uint8_t *fb = OLED_FBP(o);
(void)oled_wait(o);
for (uint32_t i = 0; i < OLED_FB; i++)
fb[i] = 0;
o->dirty = 0xFF;
}
/* Точка (x, y): страница y / 8, столбец x, бит y % 8 — младший бит сверху */
void oled_pixel(oled_t *o, int x, int y, int on)
{
uint8_t *b, m;
if (x < 0 || y < 0 || x >= (int)OLED_W || y >= (int)OLED_H)
return;
(void)oled_wait(o);
b = &OLED_FBP(o)[(uint32_t)(y >> 3) * OLED_W + (uint32_t)x];
m = (uint8_t)(1u << (y & 7));
if (!!(*b & m) == !!on)
return; /* не изменилась — страница не «грязная» */
*b ^= m;
o->dirty |= (uint8_t)(1u << (y >> 3));
}
int oled_get(oled_t *o, int x, int y)
{
(void)oled_wait(o);
if (x < 0 || y < 0 || x >= (int)OLED_W || y >= (int)OLED_H)
return 0;
return OLED_FBP(o)[(uint32_t)(y >> 3) * OLED_W + (uint32_t)x] >> (y & 7) & 1;
}
void oled_rect(oled_t *o, int x, int y, int w, int h, int fill)
{
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)
oled_pixel(o, i, j, 1);
}
/* 8 точек столбца x начиная со строки y: при y % 8 = 0 — один байт, иначе — хвосты двух соседних страниц */
static void put_col(oled_t *o, int x, int y, uint8_t bits)
{
int p = y >> 3, sh = y & 7;
uint8_t *fb = OLED_FBP(o);
if (x < 0 || x >= (int)OLED_W || y < 0 || y >= (int)OLED_H)
return;
for (int k = 0; k < 2 && p + k < (int)OLED_PAGES; k++) {
uint8_t m = (uint8_t)(k ? 0xFFu >> (8 - sh) : 0xFFu << sh);
uint8_t v = (uint8_t)(k ? bits >> (8 - sh) : bits << sh);
uint8_t *b = &fb[(uint32_t)(p + k) * OLED_W + (uint32_t)x];
if (!m)
break;
if ((*b & m) != v) {
*b = (uint8_t)((*b & ~m) | v);
o->dirty |= (uint8_t)(1u << (p + k));
}
}
}
/* Строка шрифтом 5×7 в клетках 6×8: клетка пишется целиком вместе с фоном — старый знак стирать не надо */
int oled_text(oled_t *o, int x, int y, const char *s)
{
(void)oled_wait(o);
for (; *s; s++, x += 6) {
char c = *s >= ' ' && *s <= 'Z' ? *s : ' ';
for (int i = 0; i < 6; i++)
put_col(o, x + i, y, i < 5 ? font5x7[c - ' '][i] : 0);
}
return x;
}
/* Окно для страниц p…q: SSD1306 — горизонтальный режим, столбцы и страницы от и до (7 байт);
* SH1106 — только страничный: страница и столбец col0 тетрадами (4 байта), на каждую страницу своё окно */
static int window(oled_t *o, uint8_t p, uint8_t q)
{
uint8_t c[6];
if (o->kind == OLED_SH1106) {
c[0] = (uint8_t)(SSD_PAGE_START | p);
c[1] = (uint8_t)(SSD_COL_LOW | (o->col0 & 0x0Fu));
c[2] = (uint8_t)(SSD_COL_HIGH | (o->col0 >> 4));
return cmds(o, c, 3);
}
c[0] = SSD_COL_ADDR;
c[1] = o->col0;
c[2] = (uint8_t)(o->col0 + OLED_W - 1u);
c[3] = SSD_PAGE_ADDR;
c[4] = p;
c[5] = q;
return cmds(o, c, 6);
}
/* Страницы p…q одной транзакцией через DMA. Перед данными нужен байт 0x40: для страницы 0 это tx[0], для
* остальных — последний байт предыдущей страницы; на время передачи в нём 0x40, oled_wait вернёт его. */
static int send_pages(oled_t *o, uint8_t p, uint8_t q)
{
uint8_t *start = o->tx + (uint32_t)p * OLED_W;
uint32_t n = 1u + (uint32_t)(q - p + 1u) * OLED_W;
int rc;
o->held = *start;
*start = OLED_CTL_DATA;
o->hold = start;
rc = oled_port_dma(o->addr, start, n);
o->xfers++;
o->bytes += n;
if (rc != OLED_OK) { /* не запустилась: ждать нечего */
*start = o->held;
o->hold = 0;
o->errors++;
}
return rc;
}
/* «Грязные» страницы: у SSD1306 подряд идущие — одним окном и одной транзакцией, у SH1106 — по одной */
int oled_flush(oled_t *o)
{
int rc = OLED_OK;
uint8_t p = 0;
while (p < OLED_PAGES) {
uint8_t q = p;
if (!(o->dirty & (1u << p))) {
p++;
continue;
}
if (o->kind != OLED_SH1106)
while (q + 1u < OLED_PAGES && (o->dirty & (1u << (q + 1u))))
q++;
if ((rc = window(o, p, q)) != OLED_OK || (rc = send_pages(o, p, q)) != OLED_OK)
return rc;
o->dirty &= (uint8_t)~(((1u << (q + 1u)) - 1u) & ~((1u << p) - 1u));
p = (uint8_t)(q + 1u);
}
return rc;
}
int oled_flush_all(oled_t *o)
{
o->dirty = 0xFF;
return oled_flush(o);
}
int oled_contrast(oled_t *o, uint8_t v)
{
uint8_t c[2] = {SSD_CONTRAST, v};
return cmds(o, c, 2);
}
int oled_power(oled_t *o, int on)
{
uint8_t c = on ? SSD_DISPLAY_ON : SSD_DISPLAY_OFF;
return cmds(o, &c, 1);
}
fw22m.c — прошивка STM32F103: I2C1 на 400 кГц, DMA1 канал 6, счётчик на экране раз в 100 мс.
/* fw22m.c — раздел 22м: OLED 128×64 на SSD1306 у STM32F103 (BluePill), 72 МГц, APB1 = 36 МГц.
* I2C1: PB6 — SCL, PB7 — SDA (открытый сток, 400 кГц), адрес дисплея 0x3C.
* Команды идут опросом флагов, кадр и «грязные» страницы — через DMA1 канал 6 (запрос I2C1_TX):
* пока байты уходят по шине, процессор свободен. На экране — заголовок и счётчик; раз в 100 мс
* меняется счётчик, и на шину уходит только его страница. Тактирование до 72 МГц — как в разделе 6. */
#include "stm32f10x.h"
#include "oled22m.h"
#define CPU_HZ 72000000u
#define APB1_MHZ 36u
#define I2C_TIMEOUT_US 1000u /* на START, адрес и один байт команды */
#define DMA_TIMEOUT_US 50000u /* весь кадр — 9310 тактов SCL: 23,3 мс на 400 кГц, 93,1 мс на 100 кГц */
static oled_t oled;
static volatile uint8_t dma_busy;
static volatile int dma_rc;
static volatile uint32_t ticks;
volatile uint32_t frames, bus_errors, idle_loops;
void SysTick_Handler(void)
{
ticks++;
}
static int expired(uint32_t t0, uint32_t us)
{
return DWT->CYCCNT - t0 >= us * (CPU_HZ / 1000000u);
}
static void clock_72mhz(void)
{
RCC->CR |= RCC_CR_HSEON;
while (!(RCC->CR & RCC_CR_HSERDY)) {
}
FLASH->ACR = FLASH_ACR_PRFTBE | FLASH_ACR_LATENCY_2; /* 72 МГц — два такта ожидания Flash */
RCC->CFGR = RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9 | RCC_CFGR_PPRE1_DIV2;
RCC->CR |= RCC_CR_PLLON;
while (!(RCC->CR & RCC_CR_PLLRDY)) {
}
RCC->CFGR |= RCC_CFGR_SW_PLL;
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL) {
}
}
static void pins_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
/* PB6, PB7 — альтернативная функция, открытый сток, 50 МГц (1111); подтяжки — на модуле или свои */
GPIOB->CRL = (GPIOB->CRL & 0x00FFFFFFu) | 0xFF000000u;
}
static void i2c1_init(void)
{
I2C1->CR1 = 0; /* настраивать при PE = 0 */
I2C1->CR2 = APB1_MHZ;
I2C1->CCR = I2C_CCR_FS | 30u; /* DUTY = 0: 36 МГц / (3 × 30) = 400 кГц */
I2C1->TRISE = APB1_MHZ * 300u / 1000u + 1u; /* 300 нс × 36 МГц = 10,8 → 10 + 1 = 11 */
I2C1->CR1 = I2C_CR1_PE;
DMA1_Channel6->CPAR = (uint32_t)&I2C1->DR; /* I2C1_TX — канал 6 DMA1 */
NVIC_EnableIRQ(DMA1_Channel6_IRQn);
}
static int sr1_wait(uint16_t mask)
{
uint32_t t0 = DWT->CYCCNT;
while (!(I2C1->SR1 & mask)) {
if (I2C1->SR1 & I2C_SR1_AF)
return OLED_E_NACK; /* адрес или байт не подтверждён */
if (expired(t0, I2C_TIMEOUT_US))
return OLED_E_BUS;
}
return OLED_OK;
}
static int fail(int rc)
{
I2C1->CR1 |= I2C_CR1_STOP;
I2C1->SR1 = (uint16_t)~I2C_SR1_AF; /* AF снимается записью нуля */
return rc;
}
/* START и адрес на запись: после чтения SR2 (ADDR снят) шина ждёт первый байт */
static int start_addr(uint8_t addr7)
{
int rc;
uint32_t t0 = DWT->CYCCNT;
while (I2C1->SR2 & I2C_SR2_BUSY) { /* SDA держит ведомый — раздел 17.6 */
if (expired(t0, I2C_TIMEOUT_US))
return OLED_E_BUS;
}
I2C1->CR1 |= I2C_CR1_START;
if ((rc = sr1_wait(I2C_SR1_SB)) != OLED_OK)
return rc;
I2C1->DR = (uint8_t)(addr7 << 1); /* 7-битный 0x3C на проводе — 0x78 */
if ((rc = sr1_wait(I2C_SR1_ADDR)) != OLED_OK)
return rc;
(void)I2C1->SR2;
return OLED_OK;
}
int oled_port_write(uint8_t addr7, const uint8_t *tx, uint32_t n)
{
int rc = start_addr(addr7);
for (uint32_t i = 0; rc == OLED_OK && i < n; i++)
if ((rc = sr1_wait(I2C_SR1_TXE)) == OLED_OK)
I2C1->DR = tx[i];
if (rc == OLED_OK)
rc = sr1_wait(I2C_SR1_BTF);
if (rc != OLED_OK)
return fail(rc);
I2C1->CR1 |= I2C_CR1_STOP;
return OLED_OK;
}
/* Передача через DMA (RM0008, с. 769): канал готов и DMAEN стоит до события ADDR; дальше на каждый TXE
* DMA сам кладёт байт в I2C_DR. Конец ловит прерывание канала. */
int oled_port_dma(uint8_t addr7, const uint8_t *tx, uint32_t n)
{
int rc;
DMA1_Channel6->CCR = 0;
DMA1_Channel6->CMAR = (uint32_t)tx;
DMA1_Channel6->CNDTR = n; /* до 65 535; кадр — 1025 */
DMA1_Channel6->CCR = DMA_CCR6_MINC | DMA_CCR6_DIR | DMA_CCR6_TCIE | DMA_CCR6_PL_1; /* память → I2C_DR */
I2C1->CR2 |= I2C_CR2_DMAEN;
dma_rc = OLED_OK;
dma_busy = 1;
DMA1_Channel6->CCR |= DMA_CCR6_EN;
if ((rc = start_addr(addr7)) != OLED_OK) {
DMA1_Channel6->CCR = 0;
I2C1->CR2 &= (uint16_t)~I2C_CR2_DMAEN;
dma_busy = 0;
return fail(rc);
}
return OLED_OK;
}
/* DMA отдал последний байт: снять запросы DMA, дождаться BTF и только потом STOP (RM0008, с. 769) */
void DMA1_Channel6_IRQHandler(void)
{
DMA1->IFCR = DMA_IFCR_CTCIF6;
DMA1_Channel6->CCR &= (uint16_t)~DMA_CCR6_EN;
I2C1->CR2 &= (uint16_t)~I2C_CR2_DMAEN;
while (!(I2C1->SR1 & (I2C_SR1_BTF | I2C_SR1_AF))) {
}
if (I2C1->SR1 & I2C_SR1_AF) {
dma_rc = OLED_E_NACK;
I2C1->SR1 = (uint16_t)~I2C_SR1_AF;
}
I2C1->CR1 |= I2C_CR1_STOP;
dma_busy = 0;
}
int oled_port_wait(void)
{
uint32_t t0 = DWT->CYCCNT;
while (dma_busy) {
if ((I2C1->SR1 & I2C_SR1_AF) || expired(t0, DMA_TIMEOUT_US)) {
int rc = I2C1->SR1 & I2C_SR1_AF ? OLED_E_NACK : OLED_E_BUS; /* дисплей замолчал посреди кадра */
DMA1_Channel6->CCR = 0;
I2C1->CR2 &= (uint16_t)~I2C_CR2_DMAEN;
dma_busy = 0;
bus_errors++;
return fail(rc);
}
}
return dma_rc;
}
static void counter_text(char *s, uint32_t n)
{
s[0] = 'N';
s[1] = ':';
for (int i = 5; i >= 2; i--, n /= 10u)
s[i] = (char)('0' + n % 10u);
s[6] = 0;
}
int main(void)
{
char s[8];
uint32_t last = 0, n = 0;
clock_72mhz();
SysTick_Config(CPU_HZ / 1000u);
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
pins_init();
i2c1_init();
while (oled_init(&oled, OLED_ADDR, OLED_SSD1306, 0) != OLED_OK) {
uint32_t t0 = ticks; /* нет ответа: провод, подтяжки, адрес, питание */
bus_errors++;
while (ticks - t0 < 100u) {
}
}
oled_rect(&oled, 0, 0, 128, 64, 0);
oled_text(&oled, 4, 2, "SSD1306 128X64");
oled_text(&oled, 4, 16, "I2C 400 KHZ DMA");
(void)oled_flush(&oled); /* весь кадр: 1025 байт одной транзакцией */
for (;;) {
if (ticks - last < 100u) {
idle_loops++; /* шина занята DMA, а процессор свободен */
continue;
}
last += 100u;
counter_text(s, n++);
oled_text(&oled, 4, 48, s); /* oled_text сам дождётся конца прошлого DMA */
if (oled_flush(&oled) != OLED_OK)
bus_errors++;
frames++;
}
}
Дальше: раздел 43 «Аппаратная криптография: CRC, RNG, HASH, CRYP».