Pull to refresh

Праздник жизни по гиковски: с ARM и TFT LCD

Reading time 8 min
Views 25K

Введение


Результат Увидев посты от dlinyj, goodic и Hoshi я в очередной раз ощутил, что Хабр — торт.

Первый пост касался написания драйвера символьного дисплея на базе HD44780 для Linux (Создание собственных драйверов под Linux от dlinyj); отличными ответами на него послужили посты хабраюзеров goodic (Поздравление по гиковски, без написания дров) и Hoshi (Новогодняя малина — прикручиваем экран HD44780 к Raspberry Pi).

Мне тоже захотелось поучаствовать в этом празднике жизни и реализовать свой аппаратный vt52-like терминал. Символьного дисплея у меня не оказалось, но был китайский dev-board на базе ARM Cortex-M3 с полноценным TFT-дисплеем 240х320, частичной документацией.

Запас энтузиазма в наличии имелся, поэтому, проснувщись в воскресенье днем (~17 MSK) я приступил к написанию embedded драйвера для данного LCD.

Если вам интересно embedded-программирование по ARM, электроника или просто результат — прошу под кат.

Железо


В моем распоряжении была простая отладочная плата из Поднебесной (стоимостью около $20) на базе микроконтроллера ST STM32F103RB с аппаратным мостом USB-to-UART Prolific PL-2303HX, кучей мелкой периферии и TFT LCD с контроллером Ilitek ILI9320 с неведомой схемой подключения.

В качестве внутрисхемного отладчика и программатора использовался Olimex JTAG ARM-TINY-USB-H. Хороший девайс, нормально работает с OpenOCD.

devboard
devboard


Точнее сказать, изначально даже не было известно, что за контроллер стоит на LCD. Все, что можно было узнать из дисплейного модуля, что он подключен по 16-bit шине, имеет сигналы nCS, nWR, nRD, BL_EN и RS,
назначение которых было угадать не сложно:
  • nCS — активация шины дисплея (здесь и далее префикс n означает, что активный уровень сигнала — 0)
  • BL_EN — управление подсветкой
  • nWR — записи
  • nRD — чтение
  • RS — выбор регистра


В одном из архивов с документацией, найденных на просторах Китайского сегмента интернета была схожая плата с
модулем Ilitek 932x.

Программные интерфейсы


Низкоуровневый интерфейс

Так как в рунете описаний работы с этим LCD-контроллером не много, я, пожалуй, опишу низкоуровневый интерфейс.

Их у данного контроллера по сути 4: i80-system (параллельный интерфейс, a-la обычная память, похожий на интерфейс HD44780), SPI, VSYNC (system + VSYNC, с внутренним тактированием) и RGB (VSYNC, HSYNC, ENABLE, с внешним тактированием DOTCLK). В моём случае доступен i80-system и, возможно, SPI (не проверял).

Т. к. я использовал только system, то его описание и займемся. Дабы сильно не загружать в статью — будет в спойлере.

Электрический интерфейс ILI9320
На электрическом уровне работа с цифровой техникой обычно описывается timing-диаграммами. В нашем случае есть пять управляющих сигналов и 16-ти битная шина данных.

Перед передачей контроллеру какой-либо информации следует активировать интерфейс сигналом nCS, выставив его в 0.

Далее, при выставленном в 0 RS записывается адрес регистра в который будет записываться информация (фактическая запись осуществляется активацией сигнала nWR. Сигнал RS выставляется обратно в 1.

После этого выполняется фактическая операция чтения или записи (с помощью nRD и nWR соответственно).

Диаграммы этих процессов выглядят следующим образом:
op
read LCD read op
write LCD write op


При записи/чтении из GRAM используется специальный регистр 0x22. Кроме того, контроллер может делать автоинкремент
адреса GRAM, что позволяет читать/писать её содержимое последовательно.

Диаграммы:
op
GRAM read LCD GRAM read op
GRAM write LCD GRAM write op


После выполнения операций nCS выставляется обратно в 1.

Для рисования timing-диаграмм нашел прекрасный проект wavedrom, работающий в браузере. Тестировать тут (здесь же были подготовлены схемы выше).


На основе электрического интерфейса были написаны низкоуровневые функции:

lcd_ll_funcs
void _lcd_select(void) { GPIO_ResetBits(GPIOC, GPIO_Pin_9); }
void _lcd_deselect(void) { GPIO_SetBits(GPIOC, GPIO_Pin_9); }
void _lcd_rs_set(void) { GPIO_SetBits(GPIOC, GPIO_Pin_8); }
void _lcd_rs_reset(void) { GPIO_ResetBits(GPIOC, GPIO_Pin_8); }
void _lcd_rd_en(void) { GPIO_ResetBits(GPIOC, GPIO_Pin_11); }
void _lcd_rd_dis(void) { GPIO_SetBits(GPIOC, GPIO_Pin_11); }
void _lcd_wr_en(void) { GPIO_ResetBits(GPIOC, GPIO_Pin_10); }
void _lcd_wr_dis(void) { GPIO_SetBits(GPIOC, GPIO_Pin_10); }

void _lcd_bl_en(void) { GPIO_SetBits(GPIOC, GPIO_Pin_12); }
void _lcd_bl_dis(void) { GPIO_ResetBits(GPIOC, GPIO_Pin_12); }

// changes DB[15:0] GPIO pins mode
void lcd_gpio_conf(GPIOMode_TypeDef mode);

void _lcd_put_data(u16 data) {
    // data[0-7] -> GPIOC[0-7], data[8-15] -> GPIOB[8-15]
    GPIOB->ODR = (GPIOB->ODR&0x00ff)|(data&0xff00);
    GPIOC->ODR = (GPIOC->ODR&0xff00)|(data&0x00ff);
}

u16 _lcd_read_data(void) {
    lcd_gpio_conf(GPIO_Mode_IN_FLOATING);
    u16 result = (GPIOB->IDR&0xff00)|(GPIOC->IDR&0x00ff);
    lcd_gpio_conf(GPIO_Mode_Out_PP);
    return result;
}

// assume that lcd_select() was done before it
void _lcd_tx_reg(u8 addr) {
    _lcd_put_data(addr);
    _lcd_rs_reset();
    _lcd_wr_en();
    _lcd_wr_dis();
    _lcd_rs_set();
}

// assume that _lcd_tx_reg(u8) was done before it
void _lcd_tx_data(u16 data) {
    _lcd_put_data(data);
    _lcd_wr_en();
    _lcd_wr_dis();
}

// assume that _lcd_tx_reg(u8) was done before it
u16 _lcd_rx_data(void) {
    _lcd_rd_en();
    u16 result = _lcd_read_data();
    _lcd_rd_dis();
    return result;
}


Для ускорения можно заинлайнить эти функции и преобразовать в макросы (с которыми Eclipse не очень дружит, к сожалению).

На основе этих функций реализованы функции записи в регистр, чтения из регистра, блиттинг изображения.

Высокоуровневый интерфейс

Функции LCD-дисплея для основной части программы доступны через следующее API:

u16 lcd_init(void);

void lcd_set_cursor(u16 x, u16 y);
void lcd_set_window(u16 left, u16 top, u16 right, u16 bottom);

void lcd_fill(u32 color);
void lcd_rect(u16 left, u16 top, u16 right, u16 bottom);
void lcd_put_char_at(u32 data, u16 x, u16 y);

u32 lcd_get_fg(void);
u32 lcd_get_bg(void);
void lcd_set_fg(u32 color);
void lcd_set_bg(u32 color);


Функции терминала используют этот интерфейс для всех своих операций.

Наиболее интересной частью является функция рисования символа, т. к. за ней скрывается вся работа со шрифтами. Выглядит она следующим образом:

lcd_put_char_at
void lcd_put_char_at(u32 data, u16 x, u16 y) {
    u8 xsize, ysize;
    u8 *char_img;
    lcd_get_char(data, &xsize, &ysize, &char_img);

    lcd_set_cursor(x, y);
    lcd_set_window(x, y, x + xsize, y + ysize);

    _lcd_select();
    _lcd_tx_reg(0x22);
    // works only for 8xN fonts
    for(u8 i = 0; i < ysize; i++) {
        u8 str = char_img[i];
        for(u8 j = 0; j < xsize; j++) {
            _lcd_tx_data((str&(1<<(xsize-j-1)))?fg_color:bg_color);
        }
    }
    _lcd_deselect();
}


Как можно увидеть, ссылка на битмап символа и его размеры приходит из функции lcd_get_char по коду символа (он 32-х битный, чтобы дополнительными символами не трограть ASCII-часть).

В текущий момент используется шрифт, содержащий нижнюю часть ASCII-таблицы, плюс «ёлочка». Желающие могут попробовать её найти ,)

debug
debug


Наименее интересной и наиболее затратной (в смысле времени написания) явлется функция инициализации дисплея:
lcd_init: для тех, кто хочет испугаться
u16 lcd_init(void) {
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC, ENABLE);

    GPIO_InitTypeDef gpio_conf;

    gpio_conf.GPIO_Speed = GPIO_Speed_50MHz;
    gpio_conf.GPIO_Mode = GPIO_Mode_Out_PP;
    gpio_conf.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12;
    GPIO_Init(GPIOC, &gpio_conf);

    lcd_gpio_conf(GPIO_Mode_Out_PP);

    // to init state (0xffff on db0-15, backlit is disabled, nCS, nWR, nRD and RS are high)
    _lcd_bl_dis();
    _lcd_put_data(0xffff);
    _lcd_deselect();
    _lcd_wr_dis();
    _lcd_rd_dis();
    _lcd_rs_set();

    // osc enable
    _lcd_bl_dis();
    lcd_write_reg(0x00, 0x0001);
    delay_ms(100);
    u16 lcd_code = lcd_read_reg(0x00);
    delay_ms(100);

    // driver output control (S720-S1)
    lcd_write_reg(0x01, 0x0100);

    // driving wave control (line inv)
    lcd_write_reg(0x02, 0x0700);

    // entry mode (horiz, dir(h+,v+), hwm-, bgr+)
    lcd_write_reg(0x03, 0x1030);

    // resize (off)
    lcd_write_reg(0x04, 0x0000);

    // display control 2 (skip 2 lines on front porch and on back porch)
    lcd_write_reg(0x08, 0x0202);

    // display control 3-4 (scan mode normal, fmark off)
    lcd_write_reg(0x09, 0x0000);
    lcd_write_reg(0x0a, 0x0000);

    // RGB disp iface control (int clock, sys int, 16bit)
    lcd_write_reg(0x0c, 0x0001);

    // frame marker  position (isn't used)
    lcd_write_reg(0x0d, 0x0000);

    // RGB disp iface control 2 (all def, we don't use rgb)
    lcd_write_reg(0x0f, 0x0000);

    // power on seq
    lcd_write_reg(0x07, 0x0021);
    delay_ms(10);

    // turn on power supply and configure it (enable sources, set contrast, power supply on)
    lcd_write_reg(0x10, 0x16b0);
    // set normal voltage and max dcdc freq
    lcd_write_reg(0x11, 0x0007);
    // internal vcomh (see 0x29), pon, gray level (0x08)
    lcd_write_reg(0x12, 0x0118);
    // set vcom to 0.92 * vreg1out
    lcd_write_reg(0x13, 0x0b00);
    // vcomh = 0.69 * vreg1out
    lcd_write_reg(0x29, 0x0000);

    // set x and y range
    lcd_write_reg(0x50, 0);
    lcd_write_reg(0x51, LCD_WIDTH-1);
    lcd_write_reg(0x52, 0);
    lcd_write_reg(0x53, LCD_HEIGHT-1);

    // gate scan control (scan direction, display size)
    lcd_write_reg(0x60, 0x2700);
    lcd_write_reg(0x61, 0x0001);
    lcd_write_reg(0x6a, 0x0000);

    // partial displays off
    for(u8 addr = 0x80; addr < 0x86; addr++) {
        lcd_write_reg(addr, 0x0000);
    }

    // panel iface control (19 clock/line)
    lcd_write_reg(0x90, 0x0013);

    // lcd timings
    lcd_write_reg(0x92, 0x0000);
    lcd_write_reg(0x93, 0x0001);
    lcd_write_reg(0x95, 0x0110);
    lcd_write_reg(0x97, 0x0000);
    lcd_write_reg(0x98, 0x0000);

    lcd_write_reg(0x07, 0x0133);

    // turn on backlit after init done
    _lcd_bl_en();

    return lcd_code;
}


Реализация терминала


Эта часть ничем особым не примечательна. Реализован unbuffered-терминал, с частью кодов из предыдущих статей.

escape sequences
Escape-последовательности:
  • \033[A = Переместить курсор на одну строку вверх
  • \033[B = Переместить курсор на одну строку вниз
  • \033[C = Сдвинуть курсор на одну позицию вправо
  • \033[D = Сдвинуть курсор на одну позицию влево
  • \033[H = Переместить курсор в левый верхний угол — домой (позиция 0,0)
  • \033[J = Очистить всё, НЕ возвращает курсор домой!
  • \033[K = Стирает до конца строки, НЕ возвращает курсор домой!
  • \033[M = Новая карта символов — не реализована
  • \033[Y = Позиция, принимает Y-X
  • \033[X = Позиция, принимает X-Y
  • \033[R = CGRAM Выбор ячейки памяти — не реализована, т. к. нет CGRAM
  • \033[V = Прокрутка включена — не реализована
  • \033[W = Прокрутка вылючена — не реализована
  • \033[b = Подсветка включена-выключена — не реализована


Другие полезные коды:
  • \r = Возврат каретки (возвращают курсор в позицию 0 на текущей линии!)
  • \n = Новая линия
  • \t = Табуляция (по умолчанию 3 символа)



Коммуникации


Для взаимодействия с внешним миром используется USART1 в асинхронном режиме через преобразователь USB-to-UART PL-2303HX.

С точки зрения хоста с Linux на борту это /dev/ttyUSBx. К сожалению, драйвера для pl2303 оказались довольно нестабильными. Но, как только подцепятся, работают прилично.

Чтобы не опрашивать UART в основном цикле (который пустой), работа с ним реализована на прерываниях.

С программной точки зрения это значит, что после инициализации USART1 необходимо настроить соответствующий вектор прерывания в NVIC.

Выглядит это следующим образом:
NVIC_InitTypeDef nvic_conf;
nvic_conf.NVIC_IRQChannel = USART1_IRQn;
nvic_conf.NVIC_IRQChannelPreemptionPriority = 0;
nvic_conf.NVIC_IRQChannelSubPriority = 2;
nvic_conf.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&nvic_conf);

USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);


Последней коммандой разрешаем событие заполнения приемного регистра USART1.

Соответственно, обработка выглядит так:
void USART1_IRQHandler(void) {
    u8 data = USART1->DR;
    uart_write_byte(data);
    handle_byte(data);
}


Отправляем байт обратно (echo) и вызываем обработчик, который является простым конечным автоматом.
handle_byte(u8)
// escape sequence handling vars
u8 escape_seq = 0;
u8 buf[10];

void handle_byte(u8 data) {
    if((!escape_seq) && (data == 0x1b)) {
        escape_seq = 1;
    } else if (escape_seq == 1) {
        buf[escape_seq] = data;
        escape_seq++;
        if(data != '[') {
            escape_seq = 0;
        }
    } else if (escape_seq == 2) {
        switch(data) {
        case 'A':
            lcd_term_set_cursor(lcd_term_row()-1, lcd_term_col());
            break;
        case 'B':
            lcd_term_set_cursor(lcd_term_row()+1, lcd_term_col());
            break;
        case 'C':
            lcd_term_set_cursor(lcd_term_row(), lcd_term_col()+1);
            break;
        case 'D':
            lcd_term_set_cursor(lcd_term_row(), lcd_term_col()-1);
            break;
        case 'H':
            lcd_term_set_cursor(0, 0);
            break;
        case 'J':
            lcd_term_clear();
            break;
        case 'K':
            lcd_term_flush_str();
            break;
        case 'X':
        case 'Y':
            buf[escape_seq] = data;
            escape_seq++;
            return;
        }
        escape_seq = 0;
    } else if(escape_seq == 3) {
        buf[escape_seq] = data;
        escape_seq++;
    } else if(escape_seq == 4) {
        u8 row = (buf[2] == 'Y') ? buf[3] - 037 : data - 037;
        u8 col = (buf[2] == 'Y') ? data - 037 : buf[3] - 037;
        lcd_term_set_cursor(row, col);
        escape_seq = 0;
    } else {
        lcd_term_put_str(&data, 1);
    }
}


Весь код опубликован в репозитории на гитхабе.

P. S.


Написание этого поста заняло почти 6 часов. Написание и отладка железячно-софтовой части — около 13 часов.

Спасибо всем, кто дочитал. О всяких очепятках и прочих насекомых пишите в личку.
Tags:
Hubs:
+27
Comments 16
Comments Comments 16

Articles