Введение
Увидев посты от 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
Точнее сказать, изначально даже не было известно, что за контроллер стоит на 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-ти битная шина данных.
Перед передачей контроллеру какой-либо информации следует активировать интерфейс сигналом
Далее, при выставленном в 0
После этого выполняется фактическая операция чтения или записи (с помощью
Диаграммы этих процессов выглядят следующим образом:
При записи/чтении из GRAM используется специальный регистр
адреса GRAM, что позволяет читать/писать её содержимое последовательно.
Диаграммы:
После выполнения операций
Для рисования timing-диаграмм нашел прекрасный проект wavedrom, работающий в браузере. Тестировать тут (здесь же были подготовлены схемы выше).
Перед передачей контроллеру какой-либо информации следует активировать интерфейс сигналом
nCS
, выставив его в 0.Далее, при выставленном в 0
RS
записывается адрес регистра в который будет записываться информация (фактическая запись осуществляется активацией сигнала nWR
. Сигнал RS
выставляется обратно в 1.После этого выполняется фактическая операция чтения или записи (с помощью
nRD
и nWR
соответственно).Диаграммы этих процессов выглядят следующим образом:
op | |
---|---|
read | |
write |
При записи/чтении из GRAM используется специальный регистр
0x22
. Кроме того, контроллер может делать автоинкрементадреса GRAM, что позволяет читать/писать её содержимое последовательно.
Диаграммы:
op | |
---|---|
GRAM read | |
GRAM write |
После выполнения операций
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
Наименее интересной и наиболее затратной (в смысле времени написания) явлется функция инициализации дисплея:
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 часов.
Спасибо всем, кто дочитал. О всяких очепятках и прочих насекомых пишите в личку.