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

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 «грязная» страница — менялась после отправки

Вверху — подключение модуля SSD1306 128 × 64 к STM32F103: I2C1 на 400 кГц, PB6 — SCL, PB7 — SDA, обе линии с подтяжками, PB6 и PB7 терпят 5 В; адрес модуля 0x3C, на проводе первый байт 0x78; питание 3,3 В, накачка включается командой 8D 14; байты кадра в I2C_DR кладёт DMA1 канал 6 — запрос I2C1_TX. Посередине — кадр одной транзакцией: START, адрес 0x78, байт управления 0x40 и 1024 байта страниц 0…7, STOP — 1025 байт после адреса, 9310 тактов SCL вместе с окном 21 00 7F 22 00 07, 23,3 мс и 42,9 кадра в секунду на 400 кГц, 93,1 мс и 10,7 на 100 кГц. Внизу — память: 8 страниц по 8 строк, 128 столбцов; байт — столбец из 8 точек, бит 0 сверху; точка (5, 13) — страница 1, столбец 5, бит 5, байт 0x20; сменился счётчик в странице 6 — на шину идут 136 байт, 3,1 мс вместо 23,3


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 Итоги

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

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».

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