Introduction

Having seen posts from
dlinyj ,
goodic and
Hoshi, I once again felt that Habr is a cake.
The first post concerned writing a character driver based on the HD44780 for Linux (
Creating your own Linux drivers from
dlinyj ); The excellent answers to it were the posts of goodic
habrauers (
Congratulations on geeks, without writing firewood ) and
Hoshi (
New Year raspberry - fastening the HD44780 screen to Raspberry Pi ).
I also wanted to participate in this celebration of life and implement my hardware
vt52-like
terminal. I didn't have a character display, but there was a Chinese dev-board based on ARM Cortex-M3 with a full TFT-display 240x320, partial documentation.
')
There was a lot of enthusiasm, so, waking up on Sunday afternoon (~ 17 MSK), I began to write the embedded driver for this LCD.
If you are interested in embedded ARM programming, electronics or just the result, I ask for cat.
Iron
I had a simple debug board from the Middle Kingdom (costing about $ 20) based on an
ST STM32F103RB microcontroller with a USB-to-UART
Prolific PL-2303HX hardware bridge, a bunch of small peripherals and a TFT LCD with an
Ilitek ILI9320 controller with an unknown wiring diagram.
Olimex JTAG ARM-TINY-USB-H was used as an in-circuit debugger and programmer. Good device, it works fine with
OpenOCD .
More precisely, initially it was not even known what the controller is on the LCD. All that could be learned from the display module, that it is connected via a 16-bit bus, has
nCS
,
nWR
,
nRD
,
BL_EN
and
RS
signals,
the purpose of which was not difficult to guess:
nCS
- bus display activation (hereinafter the prefix n
means that the active signal level is 0)BL_EN
- backlight controlnWR
- recordsnRD
- readingRS
- register selection
In one of the archives with the documentation found on the open spaces of the Chinese segment of the Internet there was a similar fee to
module Ilitek 932x.
Software interfaces
Low-level interface
Since there are not many descriptions of work with this LCD controller in Runet, I will probably describe a low-level interface.
In essence, this controller has 4: i80-system (parallel interface, a-la regular memory, similar to HD44780 interface), SPI, VSYNC (system +
VSYNC
, with internal clock) and RGB (
VSYNC
,
HSYNC
,
ENABLE
, with external
DOTCLK
clocking). In my case, i80-system is available and, possibly, SPI (did not check).
Since I used only the system, we’ll do its description. In order not to load much into the article - it will be in the spoiler.
Electrical interface ILI9320At the electrical level, working with digital technology is usually described by timing charts. In our case, there are five control signals and a 16-bit data bus.
Before transmitting any information to the
nCS
, the interface should be activated by the
nCS
signal, setting it to 0.
Further, with the
RS
set to 0, the register address is written into which information will be written (the actual recording is performed by activating the
nWR
signal. The
RS
signal is set back to 1.
After that, the actual read or write operation is performed (using
nRD
and
nWR
respectively).
Diagrams of these processes are as follows:
op | |
---|
read |  |
write |  |
When writing / reading from GRAM, a special register
0x22
. In addition, the controller can auto-increment
addresses GRAM, which allows you to read / write its contents sequentially.
Charts:
op | |
---|
GRAM read |  |
GRAM write |  |
After performing operations,
nCS
set back to 1.
For drawing timing charts, I found a wonderful
wavedrom project running in a browser. Test
here (here the schemes were prepared above).
Low-level functions were written based on the electrical interface:
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); }
To speed up, you can zainlaynit these functions and convert to macros (with which Eclipse is not very friendly, unfortunately).
Based on these functions, the functions of writing to the register, reading from the register, blitting the image are implemented.
High-level interface
The functions of the LCD display for the main part of the program are available through the following 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);
Terminal functions use this interface for all their operations.
The most interesting part is the function of drawing a symbol, since it hides all the work with fonts. It looks like this:
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);
As you can see, the link to the character bitmap and its size comes from the
lcd_get_char
function by the character code (it is 32-bit, so that the additional characters do not rub the ASCII part).
The font currently in use is the font that contains the bottom of the ASCII table plus the herringbone. Those interested can try to find it,)
The least interesting and most costly (in terms of writing time) is the function of display initialization:
lcd_init: for those who want to get scared 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);
Terminal implementation
This part is not particularly remarkable. Implemented unbuffered terminal, with part of the codes from previous articles.
escape sequencesEscape sequences:
- \ 033 [A = Move cursor one line up
- \ 033 [B = Move cursor one line down
- \ 033 [C = Move the cursor one position to the right
- \ 033 [D = Move the cursor one position to the left
- \ 033 [H = Move the cursor to the upper left corner - home (position 0,0)
- \ 033 [J = Clear all, does NOT return the cursor home!
- \ 033 [K = Erases to the end of the line, does NOT return the cursor home!
- \ 033 [M = New Character Map - Not Implemented
- \ 033 [Y = Position, accepts YX
- \ 033 [X = Position, takes XY
- \ 033 [R = CGRAM Memory selection is not implemented, since there is no CGRAM
- \ 033 [V = Scroll Enabled - Not Implemented
- \ 033 [W = Scrolling disabled - not implemented
- \ 033 [b = Backlight on / off - not implemented
Other useful codes:
- \ r = Carriage return (return the cursor to position 0 on the current line!)
- \ n = New line
- \ t = Tab (default 3 characters)
Communications
To interact with the outside world,
USART1
used in asynchronous mode via the
PL-2303HX
USB-to-UART
PL-2303HX
.
From the point of view of the Linux host on board, this is
/dev/ttyUSBx
. Unfortunately, the drivers for
pl2303
were rather unstable. But, as soon as picked up, work well.
In order not to poll the UART in the main loop (which is empty), work with it is implemented on interrupts.
From a software point of view, this means that after initializing USART1, you must configure the corresponding interrupt vector in
NVIC .
It looks like this:
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);
The last command allows the event to fill the receiving register USART1.
Accordingly, the processing looks like this:
void USART1_IRQHandler(void) { u8 data = USART1->DR; uart_write_byte(data); handle_byte(data); }
We send the byte back (echo) and call the handler, which is a simple state machine.
All code is published in the
github repository.
PS
Writing this post took almost 6 hours. Writing and debugging of the hardware-software part is about 13 hours.
Thanks to everyone who read it. About any ochepyatkah and other insects, write in a personal.