Введение
Увидев посты от 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.
Точнее сказать, изначально даже не было известно, что за контроллер стоит на LCD. Все, что можно было узнать из дисплейного модуля, что он подключен по 16-bit шине, имеет сигналы nCS
, nWR
, nRD
, BL_EN
и RS
,
назначение которых было угадать не сложно:
nCS
— активация шины дисплея (здесь и далее префиксn
означает, что активный уровень сигнала — 0)BL_EN
— управление подсветкойnWR
— записиnRD
— чтениеRS
— выбор регистра
В одном из архивов с документацией, найденных на просторах Китайского сегмента интернета была схожая плата с
модулем Ilitek 932x.
Программные интерфейсы
Низкоуровневый интерфейс
Так как в рунете описаний работы с этим LCD-контроллером не много, я, пожалуй, опишу низкоуровневый интерфейс.
Их у данного контроллера по сути 3: i80-system (параллельный интерфейс, a-la обычная память, похожий на интерфейс HD44780), SPI и RGB (с внешней синхронизацией). В моём случае доступен i80-system и, возможно, SPI (не проверял).
Т. к. я использовал только system, то его описание и займемся. Дабы сильно не загружать в статью — будет в спойлере.
Перед передачей контроллеру какой-либо информации следует активировать интерфейс сигналом nCS
, выставив его в 0.
Далее, при выставленном в 0 RS
записывается адрес регистра в который будет записываться информация (фактическая запись осуществляется активацией сигнала nWR
. Сигнал RS
выставляется обратно в 1.
После этого выполняется фактическая операция чтения или записи (с помощью nRD
и nWR
соответственно).
Диаграммы этих процессов выглядят следующим образом:
read | |
write |
При записи/чтении из GRAM используется специальный регистр 0x22
. Кроме того, контроллер может делать автоинкремент
адреса GRAM, что позволяет читать/писать её содержимое последовательно.
Диаграммы:
GRAM read | |
GRAM write |
После выполнения операций nCS
выставляется обратно в 1.
Для рисования timing-диаграмм нашел прекрасный проект wavedrom, работающий в браузере. Тестировать тут (здесь же были подготовлены схемы выше).
На основе электрического интерфейса были написаны низкоуровневые функции:
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);
Функции терминала используют этот интерфейс для всех своих операций.
Наиболее интересной частью является функция рисования символа, т. к. за ней скрывается вся работа со шрифтами. Выглядит она следующим образом:
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-таблицы, плюс «ёлочка». Желающие могут попробовать её найти ,)
Наименее интересной и наиболее затратной (в смысле времени написания) явлется функция инициализации дисплея:
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-терминал, с частью кодов из предыдущих статей.
- \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) и вызываем обработчик, который является простым конечным автоматом.
// 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 часов.
Спасибо всем, кто дочитал. О всяких очепятках и прочих насекомых пишите в личку.
ссылка на оригинал статьи http://habrahabr.ru/post/207136/
Добавить комментарий