22в. Сегментный ЖКИ: стекло, мультиплекс и контроллер LCD
В разделе 22 цифры показывал светодиодный индикатор: сегмент горит, пока через него течёт ток, разряды зажигаются по очереди. Жидкокристаллическое стекло устроено иначе — оно не светится, а меняет прозрачность от напряжения, и почти не потребляет тока. Поэтому часы, счётчики и градусники на батарейке делают на ЖКИ. Здесь — почему стекло нельзя кормить постоянным напряжением, как по четырём общим выводам управляют сотнями сегментов, как это делает встроенный контроллер LCD у серий L0, L1 и L4 и сколько он берёт в режиме Stop.
Опоры: справочное руководство RM0367 Rev 8 (STM32L0x3, глава 17 «Liquid crystal display controller») и даташит DS10152 Rev 9 на STM32L053 — оба из пака Keil; заголовки CMSIS L053, L152 и L476 из паков. Формы напряжений и среднеквадратичные значения посчитаны своей моделью на ПК по правилам из RM0367; код собран armclang для трёх кристаллов.
Как это называется в документации
Оригинал Перевод liquid crystal display, LCD жидкокристаллический индикатор, ЖКИ glass стекло (сам индикатор без контроллера) common, COM общий вывод segment, SEG вывод сегмента duty ratio, 1/4 duty мультиплекс, 1/4 — четыре общих вывода bias, 1/3 bias число уровней напряжения: 1/3 — уровни 0, 1/3, 2/3, 1 frame, frame rate кадр, частота кадров step-up converter повышающий преобразователь contrast control регулировка контраста double buffer двойной буфер
22в.1 Почему не постоянным током
Сегмент — это слой жидкого кристалла между двумя прозрачными электродами. Когда напряжение на слое выше порога, молекулы поворачиваются и сегмент темнеет. Но постоянное напряжение стекло портит: RM0367 прямо требует чередовать полярность, иначе начинается электрофорез — кристалл деградирует. Значит, на каждом сегменте должно быть переменное напряжение со средним ровно нулём.
Самый простой способ — статика: у стекла один общий вывод COM на все сегменты, на каждый сегмент — свой вывод SEG. На COM идёт меандр 0 / VLCD. Если сегмент должен гореть, на его SEG подают тот же меандр в противофазе: на слое ±VLCD, среднеквадратичное — VLCD. Если не должен — в фазе: на слое ноль. Контраст максимальный, но выводов нужно столько, сколько сегментов: у L053 до 51 в статике.
Хорошо, что темнота зависит не от мгновенного, а от среднеквадратичного напряжения: стекло медленное. Это и открывает дорогу мультиплексу.
22в.2 Мультиплекс: duty и bias
Возьмём четыре общих вывода COM0…COM3 (1/4 duty). Каждый вывод SEG теперь идёт к четырём сегментам — по одному на каждый COM. Кадр делится на четыре фазы; в фазе n «активен» COMn, и сегменты этого COM зажигаются или гасятся уровнем на SEG. Чтобы сегменты других COM в это время не загорались, лишнее напряжение гасят промежуточными уровнями — это и есть bias. При 1/3 bias по RM0367:
- активный COM в нечётном кадре — VLCD, в чётном — 0; неактивный — 1/3 и 2/3 VLCD;
- SEG горящего сегмента — противоположная шина (0 в нечётном, VLCD в чётном); SEG негорящего — 2/3 и 1/3 VLCD.
Модель строит эти формы для каждого пикселя и перебирает все комбинации соседних пикселей на том же SEG. Результат от соседей не зависит, а среднее за пару кадров ровно ноль (за один кадр — нет, до 0,5 VLCD: поэтому кадры и идут парами). Отношение «горит / погашен» — главная мера контраста:
| Мультиплекс | Bias | Горит, доля VLCD | Погашен | Отношение |
|---|---|---|---|---|
| статика | — | 1 | 0 | бесконечность |
| 1/2 | 1/2 | 0,7906 | 0,3536 | 2,236 |
| 1/3 | 1/3 | 0,6383 | 0,3333 | 1,915 |
| 1/4 | 1/3 | 0,5774 | 0,3333 | 1,732 |
| 1/4 | 1/2 | 0,6614 | 0,4330 | 1,528 |
| 1/4 | 1/4 | 0,5449 | 0,3307 | 1,648 |
| 1/8 | 1/4 | 0,4239 | 0,2932 | 1,446 |
Модель совпадает с классической формулой (1/a)·√((a² + N − 1)/N) для
горящего и (1/a)·√(((a − 2)² + N − 1)/N) для погашенного (N — число COM,
a — знаменатель bias). Видно две вещи. Чем больше COM, тем хуже различие —
поэтому 1/8 duty годится только для стекла с резким порогом. И bias
подбирают под мультиплекс: при четырёх COM 1/3 даёт 1,732, а 1/2 и 1/4 —
меньше. Какой мультиплекс и bias нужны, пишут в паспорте стекла.
Абсолютные числа зависят от VLCD. У L053 внутренний повышающий преобразователь выдаёт от 2,6 до 3,55 В (восемь ступеней CC, DS10152); на ступени VLCD3 = 2,98 В при 1/4 duty и 1/3 bias горящий сегмент получает 1,721 В, погашенный — 0,993 В. Порог стекла должен лечь между ними — его берут из паспорта стекла и подбирают ступенью CC.
22в.3 Контроллер LCD: где есть и как устроен
Встроенный контроллер есть у L0 (L053, L063, L073 и др.), L1, L4 (L476 и соседи) и U0 (RM0503, глава 19); у U5 его нет — в оглавлении RM0456 такого блока нет. У L0, L1 и L4 он сидит по одному адресу 0x4000 2400 и имеет одинаковые поля — один и тот же код собирается для всех трёх:
| Регистр | Смещение | Что в нём |
|---|---|---|
| LCD_CR | 0x00 | DUTY (биты 4:2), BIAS (6:5), VSEL (1), LCDEN (0), MUX_SEG (7) |
| LCD_FCR | 0x04 | PS (25:22), DIV (21:18), BLINK, BLINKF, CC (12:10), DEAD, PON (6:4), HD |
| LCD_SR | 0x08 | FCRSF (5), RDY (4), UDD (3), UDR (2), SOF (1), ENS (0) |
| LCD_CLR | 0x0C | сброс флагов UDD и SOF |
| LCD_RAM | 0x14…0x50 | 16 слов: для COMn — слово 2n (SEG0…31) и 2n+1 (SEG32…51) |
Коды полей по RM0367: DUTY 000 — статика, 001 — 1/2, 010 — 1/3, 011 — 1/4, 100 — 1/8; BIAS 00 — 1/4, 01 — 1/2, 10 — 1/3. Контроллер ведёт до 8 COM и до 52 SEG. Память двойная: программа пишет LCD_RAM, ставит UDR, и в начале следующего кадра данные уходят в буфер показа; пока UDR стоит, LCD_RAM закрыта для записи. Выводы COM и SEG настраивают как альтернативную функцию.
22в.4 Частота кадра
Тактирование LCDCLK — то же, что у часов реального времени: LSE, LSI или HSE с делителем. Дальше два делителя из LCD_FCR:
f_ck_div = f_LCDCLK / (2^PS · (16 + DIV))
f_кадр = f_ck_div · duty
Проверка пересчитала этой формулой все 15 строк таблицы 74 из RM0367 —
совпадают до сотых: например, от LSE 32 768 Гц при PS = 4, DIV = 1 и 1/4
duty выходит 30,12 Гц. RM советует держать кадр примерно от 30 до 100 Гц:
ниже — мерцание, выше — лишний ток. Возьмём PS = 3, DIV = 1: ck_div =
240,94 Гц, кадр при 1/4 duty — 60,24 Гц.
Той же частотой ck_div тактируется мигание: f_ck_div / 2^(BLINKF + 3),
при BLINKF = 5 — 0,941 Гц. Длина импульса PON (сколько держать включённым
низкоомный делитель после смены уровня) считается в тактах ck_ps: при
PS = 3 один такт — 244,1 мкс.
22в.5 Шрифт и таблица сегментов
Шрифт тот же, что в разделе 22: бит 0 — сегмент a, бит 6 — g, бит 7 — точка. Новое — таблица стекла: какой сегмент сидит на пересечении какого COM и какого SEG. Она своя у каждого стекла и берётся из его паспорта. Для примера — условное стекло на четыре цифры, по два вывода SEG на цифру:
| Вывод 0 разряда | Вывод 1 разряда | |
|---|---|---|
| COM0 | f | a |
| COM1 | g | b |
| COM2 | e | c |
| COM3 | d | точка |
void lcd_encode(uint32_t ram[16], const uint8_t pat[4])
{
for (int i = 0; i < 16; i++)
ram[i] = 0;
for (int d = 0; d < 4; d++)
for (int com = 0; com < 4; com++)
for (int k = 0; k < 2; k++)
if (pat[d] >> GLASS[com][k] & 1u) {
unsigned seg = PIN[d][k];
ram[2 * com + seg / 32] |= 1UL << (seg % 32);
}
}
GLASS — таблица выше, PIN — какие выводы SEG разведены к разряду.
Смена стекла или разводки меняет только эти две таблицы. Модель на ПК
раскодирует образ LCD_RAM обратно в цифры: все 10 000 чисел с точкой в
любом из четырёх разрядов и без неё возвращаются без ошибок, а gcc-сборка
этого же файла даёт на ПК те же 50 000 образов, что модель. Число 12.34
(точка после второго разряда) — слова 00000068 00000000 000000FE 00000000
000000A6 00000000 0000001C 00000000.
22в.6 Запуск и вывод
void lcd_init(void)
{
LCD->FCR = (3UL << LCD_FCR_PS_Pos) | (1UL << LCD_FCR_DIV_Pos) | (3UL << LCD_FCR_CC_Pos) | (2UL << LCD_FCR_PON_Pos);
while (!(LCD->SR & LCD_SR_FCRSR)) {} /* флаг FCRSF по RM: FCR перешёл в домен LCDCLK */
LCD->CR = (2UL << LCD_CR_BIAS_Pos) | (3UL << LCD_CR_DUTY_Pos); /* BIAS 10 = 1/3, DUTY 011 = 1/4, VSEL 0 */
LCD->CR |= LCD_CR_LCDEN;
while (!(LCD->SR & LCD_SR_ENS)) {}
while (!(LCD->SR & LCD_SR_RDY)) {} /* повышающий преобразователь вышел на напряжение */
}
Порядок как у HAL из пака: сначала LCD_FCR и ожидание синхронизации, затем
LCD_CR, включение, ожидание ENS и RDY. Поля DUTY, BIAS, VSEL и MUX_SEG после
включения защищены от записи — выбирают их до LCDEN. armclang собирает
этот файл для L053, L152 и L476 без предупреждений; _Static_assert
сверяют с заголовками каждого кристалла адрес 0x4000 2400, LCD_RAM с 0x14
на 16 слов и позиции всех полей. Дизассемблер подтверждает константы:
LCD_FCR = 0x00C4 0C20, LCD_CR = 0x4C. Вместе с кодированием — 360 байт кода
на L053 и 414 на L152 и L476.
Вывод числа: дождаться, пока UDR сбросится, записать слова 0…7 (для 1/4 duty нужны только COM0…COM3) и поставить UDR — перенос случится в начале следующего кадра, без мерцания и без прерываний.
Ток в Stop. Контроллер работает в Stop (в Standby — нет). По таблице 39 DS10152 он добавляет в Stop при 25 °C 0,15 мкА в статике и 1,6 мкА (1,8 В) или 2,6 мкА (3,0 В) при 1/8 duty. Ток самого контроллера с внутренним повышающим преобразователем по таблице 79 — 3,1 мкА при 3,0 В (1/8 duty, 1/4 bias, без стекла). Для сравнения, весь кристалл в Stop с RTC — 0,8 мкА. Сам делитель на высокоомных резисторах (6,6 МОм) при 2,98 В проводит 0,452 мкА; ток перезаряда ёмкости стекла зависит от стекла и в даташите не указан — его меряют на своей плате.
22в.7 Грабли
Стекло выцветает или «залипает» через месяцы. Постоянная составляющая: вывод COM или SEG дёргают программой без чередования кадров или выключают посреди кадра. После сброса LCDEN контроллер сам доводит до конца чётный кадр и только потом отключает делитель — не обходить его, дёргая выводы программой.
Все сегменты полусерые. Не тот bias или мультиплекс для стекла, либо VLCD слишком высок: погашенный сегмент при 1/4 duty получает 0,577 от горящего, и порог стекла должен лечь между ними. Понижают CC или включают паузу DEAD.
Флаг FCRSF не находится. В RM0367 он FCRSF, а в заголовках CMSIS L0, L1
и L4 — LCD_SR_FCRSR. А ждать его после записи LCD_FCR нужно: чтение
LCD_FCR возвращает последнее записанное, а не то, с чем идёт текущий кадр.
Новые цифры не появились. Запись в LCD_RAM, пока UDR = 1, игнорируется, а перенос вообще не случается, пока LCDEN = 0. UDR сбрасывает только аппаратура.
LCD_CR не меняется. После включения поля DUTY, BIAS, VSEL и MUX_SEG защищены. Сначала LCDEN = 0 и ожидание ENS = 0.
Контраст «по RM» не совпал с даташитом. RM0367 называет диапазон CC 2,60…3,51 В, а DS10152 для L053 даёт VLCD7 = 3,55 В. Числа — только из даташита своего кристалла. И в таблице PON у RM0367 для 011 стоит 782 мкс, хотя три такта по 244,1 мкс — это 732,4 мкс.
Прерывание UDD молчит в Stop. RM0367: в Stop нет тактов шины, и UDD прерывания не даёт даже при UDDIE = 1. Для сна обновляют LCD_RAM до входа в Stop.
Вывод VLCD висит в воздухе. При внутреннем преобразователе на VLCD нужен конденсатор на землю (у L053 — от 0,1 до 2 мкФ), без LCD вывод соединяют с VDDA.
22в.8 Итоги
- Среднее напряжение на сегменте — ровно ноль за пару кадров; иначе стекло деградирует.
- Статика: 1 и 0 VLCD; 1/4 duty с 1/3 bias: 0,577 и 0,333 VLCD, отношение 1,732 — лучшее для четырёх COM; 1/8 duty с 1/4 bias — уже 1,446.
- L053: VLCD от 2,6 до 3,55 В; на 2,98 В — 1,721 В на горящем и 0,993 В на погашенном сегменте.
- Контроллер LCD: L0, L1, L4, U0 (у U5 нет); адрес 0x4000 2400, 16 слов LCD_RAM, двойной буфер через UDR.
- Кадр
LCDCLK / (2^PS · (16 + DIV)) · duty: от LSE при PS = 3, DIV = 1, 1/4 duty — 60,24 Гц; таблица 74 RM0367 сходится во всех 15 строках. - В Stop контроллер добавляет 0,15 мкА (статика) … 2,6 мкА (1/8 duty, 3 В); с повышающим преобразователем его ток по даташиту — 3,1 мкА.
- Код для трёх кристаллов — 360…414 байт, шрифт и стекло — две таблицы.
Полные исходники
seglcd22d.c — четыре цифры на сегментном ЖКИ через контроллер LCD.
/* seglcd22d.c — раздел 22в: четыре цифры на сегментном ЖКИ через контроллер LCD.
*
* Стекло условное: 4 разряда, 1/4 duty, на разряд два вывода SEG (у настоящего стекла таблица — из его паспорта).
* Кодирование (lcd_digits, lcd_encode) — чистые функции: их гоняет и gcc на ПК, и armclang для кристалла.
* Работа с регистрами — только когда задан кристалл (-DSTM32L053xx, -DSTM32L152xB, -DSTM32L476xx):
* у всех трёх LCD по адресу 0x4000 2400 и одинаковые поля CR, FCR, SR (RM0367 гл. 17, RM0038, RM0351).
*/
#include <stddef.h>
#include <stdint.h>
/* Шрифт как в разделе 22: бит 0 — сегмент a, …, бит 6 — g, бит 7 — точка. */
static const uint8_t FONT[10] = { 0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F };
/* GLASS[com][k] — какой сегмент разряда зажигает пара (COMcom, k-й вывод разряда). */
static const uint8_t GLASS[4][2] = {
{ 5, 0 }, /* COM0: f, a */
{ 6, 1 }, /* COM1: g, b */
{ 4, 2 }, /* COM2: e, c */
{ 3, 7 }, /* COM3: d, точка */
};
/* PIN[d][k] — номер вывода SEG у k-го вывода разряда d (как разведена плата). */
static const uint8_t PIN[4][2] = { { 0, 1 }, { 2, 3 }, { 4, 5 }, { 6, 7 } };
/* Число 0…9999 в сегменты; ведущие нули гасятся; dp — разряд с точкой (0…3) или −1. */
void lcd_digits(uint8_t pat[4], unsigned v, int dp)
{
for (int d = 3; d >= 0; d--) {
pat[d] = (v != 0 || d == 3) ? FONT[v % 10] : 0;
v /= 10;
}
if (dp >= 0 && dp < 4)
pat[dp] |= 0x80;
}
/* Сегменты в образ LCD_RAM: для COMn — слова 2n (SEG0…31) и 2n+1 (SEG32…51). */
void lcd_encode(uint32_t ram[16], const uint8_t pat[4])
{
for (int i = 0; i < 16; i++)
ram[i] = 0;
for (int d = 0; d < 4; d++)
for (int com = 0; com < 4; com++)
for (int k = 0; k < 2; k++)
if (pat[d] >> GLASS[com][k] & 1u) {
unsigned seg = PIN[d][k];
ram[2 * com + seg / 32] |= 1UL << (seg % 32);
}
}
#if defined(LCD_BASE)
_Static_assert(LCD_BASE == 0x40002400UL, "LCD на APB1: 0x4000 2400");
_Static_assert(offsetof(LCD_TypeDef, RAM) == 0x14 && sizeof(((LCD_TypeDef *)0)->RAM) == 16 * 4, "LCD_RAM: 16 слов с 0x14");
_Static_assert(LCD_CR_DUTY_Pos == 2 && LCD_CR_BIAS_Pos == 5, "DUTY 4:2, BIAS 6:5");
_Static_assert(LCD_FCR_PS_Pos == 22 && LCD_FCR_DIV_Pos == 18 && LCD_FCR_CC_Pos == 10 && LCD_FCR_PON_Pos == 4, "PS, DIV, CC, PON");
_Static_assert(LCD_SR_FCRSR == 0x20 && LCD_SR_RDY == 0x10 && LCD_SR_UDR == 0x04 && LCD_SR_ENS == 0x01, "FCRSF (в заголовке FCRSR), RDY, UDR, ENS");
/* LCDCLK = LSE 32 768 Гц (выбор источника в RCC и выводы как альтернативная функция — раздел 12, здесь нет).
* Кадр: 32768 / (2^3 · (16 + 1)) / 4 = 60,24 Гц; 1/4 duty, 1/3 bias, внутренний повышающий преобразователь. */
void lcd_init(void)
{
LCD->FCR = (3UL << LCD_FCR_PS_Pos) | (1UL << LCD_FCR_DIV_Pos) | (3UL << LCD_FCR_CC_Pos) | (2UL << LCD_FCR_PON_Pos);
while (!(LCD->SR & LCD_SR_FCRSR)) {} /* флаг FCRSF по RM: FCR перешёл в домен LCDCLK */
LCD->CR = (2UL << LCD_CR_BIAS_Pos) | (3UL << LCD_CR_DUTY_Pos); /* BIAS 10 = 1/3, DUTY 011 = 1/4, VSEL 0 */
LCD->CR |= LCD_CR_LCDEN;
while (!(LCD->SR & LCD_SR_ENS)) {}
while (!(LCD->SR & LCD_SR_RDY)) {} /* повышающий преобразователь вышел на напряжение */
}
void lcd_show(unsigned v, int dp)
{
uint8_t pat[4];
uint32_t ram[16];
lcd_digits(pat, v, dp);
lcd_encode(ram, pat);
while (LCD->SR & LCD_SR_UDR) {} /* пока идёт перенос, LCD_RAM закрыта для записи */
for (int i = 0; i < 8; i++) /* 1/4 duty: COM0…COM3 — слова 0…7 */
LCD->RAM[i] = ram[i];
LCD->SR = LCD_SR_UDR; /* перенос в буфер показа в начале следующего кадра */
}
#endif
Дальше: раздел 22г «Адресные светодиоды: WS2812B, SK6812 RGBW и SK6812WWA — протокол, тайминги, SPI, таймер с DMA, питание и уровни».