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

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:

Формы напряжений при 1/4 duty и 1/3 bias за нечётный и чётный кадр: выводы COM0, COM1, SEG0 и разность на горящем пикселе COM0 и SEG0 и на погашенном COM1 и SEG0 со среднеквадратичными 0,577 и 0,333 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 Итоги

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

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, питание и уровни».

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