Introduction
Alright, let’s start with today’s topic. In the third article, we introduced the multi-tasking method of driving digital tubes. However, digital tubes can only display numbers. If we want to display English and Chinese characters, we must use a liquid crystal display (LCD). LCDs can be divided into character type and dot matrix type, among which character type LCDs, due to their low cost and ease of use, have become ideal substitutes for digital tubes. Today, we will discuss the driver program for character LCD1602.
LCD1602 Pin Definition
Before discussing the driver, let’s first look at the pin definition of LCD1602 as shown below.
From the figure, we know that the LCD has 16 pins, where pins 1 and 2 are the power supply for the LCD, pin 1 is GND, and pin 2 is VCC.
The last two pins are for the LCD backlight, where BLA connects to the positive power supply and BLK connects to the negative power supply to light it up, as shown in the figure below.
Of course, you can also use a transistor to control the backlight switch. Here, we directly power it on to turn on the backlight, without needing program control.
Pin 3 is the liquid crystal display bias (used to adjust the display contrast), which is generally connected to a variable resistor to adjust the value of the resistor to adjust the contrast of the LCD. This may be a bit difficult to understand, so please look at the gif below.
Note:Sometimes, if the contrast is not adjusted properly, the screen will not display characters.
The next pin, pin 4 (RS), is for register selection. 1 is for data register, and 0 is for command register.
Pin 5 (RW) is for read/write operation selection (1 means read, 0 means write).
Pin 6 (E) is the LCD enable signal, which is valid for reading/writing when it changes from high to low (i.e., falling edge).
Pins 7 to 14 are the 8-bit data bus, directly connected to the P1 port of the microcontroller. As mentioned above, one reason why character type LCD1602 is easy to use is that its pins can be directly connected to the microcontroller.
LCD1602 Control Commands
Now that we know the pin definitions, let’s look at the control commands of LCD1602, as shown in the table below.
There are a total of 11 commands, and we will briefly introduce them.
-
Command 1: 0x01 is the clear screen command, which returns the cursor to the top left corner of the screen.
-
Command 2: 0x02 returns the cursor to the top left corner of the screen.
Summary: These two commands both return the cursor to the top left corner of the screen. The difference is that 0x02 does not clear the screen, as shown in the figure below.
-
Command 3: Cursor and display mode settings (generally set to 0x06).
Where I/D is the address pointer increment or decrement selection bit. I/D=1 means increment the pointer by 1; I/D=0 means decrement the pointer by 1.
S is whether the character movement direction on the screen is valid. S=0 means no movement for the entire screen display; S=1 means movement for the entire screen display (I/D=1 means left move, I/D=0 means right move).
-
Command 4: Display on/off and cursor settings (generally set to 0x0c).
D is the overall display control bit. D=0 means turn off display; D=1 means turn on display. C is the cursor presence control bit. C=0 means no cursor; C=1 means there is a cursor. B is the cursor blinking control bit. B=0 means no blinking; B=1 means blinking. -
Command 5: Cursor or character shifting (left shift is 0x18, right shift is 0x1c).
S/C is the cursor or character shifting selection control bit. S/C=0 means move the cursor; S/C=1 means move the displayed character. R/L is the shift direction selection control bit. R/L=0 means left shift; R/L=1 means right shift. -
Command 6: Function settings (generally set to 0x38).
DL is the effective length selection control bit for data transmission. DL=1 means 8-bit data line; DL=0 means 4-bit data line. N is the line number selection control bit for the display. N=0 means single line display; N=1 means two-line display. F is the point matrix control bit for character display. F=0 means display 5*7 point matrix characters; F=1 means display 5*10 point matrix characters. -
Command 7: CGRAM address setting (address range is 0~63, a total of 64 bytes).
-
Command 8: DDRAM address setting. The LCD has an internal data address pointer that can be accessed to display all 80 bytes of data in RAM. As shown in the figure below.
The data format for command 8 is 0x80 + address code.
-
Command 9: Read busy flag. BF=1 indicates that the LCD is busy, and at this time, the LCD cannot accept commands or data; BF=0 indicates that the LCD is not busy.
-
Command 10: Write data.
-
Command 11: Read data.
LCD1602 Driver Program
With the commands, how do we make the LCD display characters?
This also requires looking at the timing diagram in the LCD data manual, as shown below.
In the figure, the double-headed arrow points to the time, and the longest tc is also at the ns level, so we can use _nop_() for simple delays. _nop_() in the “intrins.h” header file takes one mechanical cycle, which is 12 clock cycles (the oscillation period of the crystal, 12M crystal is 1us).
Alright, let’s start writing the driver, the function for writing commands is as follows.
void delay_50us(){ unsigned int i; for(i=50;i>0;i--){ _nop_(); } }void lcd1602_write_cmd(unsigned char cmd){ LCD1602_E=0; LCD1602_RS=0;//Select command LCD1602_RW=0;//Select write LCD1602_DATAPORT=cmd;//Prepare command LCD1602_E=1;//Rising edge _nop_(); //Delay LCD1602_E=0;//Falling edge to write delay_50us(); }
-
Pitfall Guide: Note that a 50us delay must be added at the end of the function; otherwise, some screens may display garbled characters.
The function for writing data is as follows:
void lcd1602_write_data(unsigned char dat) { LCD1602_E=0; LCD1602_RS=1;//Select data LCD1602_RW=0;//Select write LCD1602_DATAPORT=dat;//Prepare data LCD1602_E=1;//Rising edge _nop_();//Delay LCD1602_E=0;//Falling edge to write delay_50us(); }
-
Writing commands and writing data functions only differ in the value of LCD1602_RS in the second line.
Do you think this is enough?
Of course not, we still need to check if the LCD is busy; if it is busy, we cannot write data and commands.
The LCD busy check program is as follows:
void lcd_check_busy(){ u8 dt = 0xff; do{ LCD1602_E=0; LCD1602_RS=0;//Select command LCD1602_RW=1;//Select read LCD1602_E=1; dt = LCD1602_DATAPORT; }while(dt & 0x80); LCD1602_E=0;}
-
This program uses a do{}while() loop. If the LCD is busy (for example, if it is not connected or has a fault), it will cause an infinite loop, causing the program to hang.
At this point, we need to define a counting variable. If the LCD is still busy after reaching a certain value, we exit the loop, as shown below:
void lcd_check_busy(){ u8 dt = 0xff; u8 i = 0;//Counting variable do{ LCD1602_E=0; LCD1602_RS=0;//Select command LCD1602_RW=1;//Select read LCD1602_E=1; dt = LCD1602_DATAPORT; i++;//Increment count if(i > 100){//Exit the loop after reaching a certain value break; } }while(dt & 0x80); LCD1602_E=0;}
-
By adding the counting variable i, we prevent the occurrence of infinite loops. Isn’t it simple? (* ̄︶ ̄)
Next, we can add the busy check in the write data or command function, as shown below:
void lcd1602_write_cmd(unsigned char cmd){ lcd_check_busy(); ... }void lcd1602_write_data(unsigned char dat) { lcd_check_busy(); ... }
Displaying Characters on LCD
With the driver functions, we can start displaying characters on the LCD1602.
First, we need an LCD initialization function as follows:
void lcd1602_init(void){ lcd1602_write_cmd(0x38);//8-bit data bus, 2 lines display, 5*7 point matrix character lcd1602_write_cmd(0x0c);//Display function on, no cursor lcd1602_write_cmd(0x06);//Cursor right move, display no move lcd1602_write_cmd(0x01);//Clear screen}
-
It’s also very simple, only 4 commands are used.
Next, we will write a function to display a string as follows:
void lcd1602_show_string(u8 x,u8 y,u8 *str){ u8 i=0; if(y>1||x>15)return;//Exit if row and column parameters are incorrect if(y == 0){ lcd1602_write_cmd(0x80+i+x);//Display on the first line }else{ lcd1602_write_cmd(0x80+0x40+i+x);//Display on the second line } while(*str){ //Print string lcd1602_write_data(*str);//Display content str++;//Pointer increment, display the next character }}
-
This function is also very simple. The parameter y indicates which row, x indicates which column, and str is the pointer to the string to be displayed.
Finally, in the main function, we will display two lines of strings as follows:
void main(){ lcd1602_init(); timer2_init(); tasks_init(); lcd1602_show_string(0,0,"qian ru shi"); lcd1602_show_string(0,1,"xiao shu chong"); while(1){ task_progress(); }}
We will define a task to move the string to the left every 500ms as follows:
void lcd_task(){ lcd1602_write_cmd(0x18);}void tasks_init(){ //Add LCD left move task add_task(500,&lcd_task);}
-
We can use the 0x18 command for left move (the right move command is 0x1c). The program effect is as follows:
Displaying Chinese Characters
Characters can be displayed, but how to display Chinese characters?
Haha, this requires the use of the CGRAM of the LCD1602.
CGRAM is the internal custom byte space of the LCD1602 (size is 64 bytes), and in the LCD1602, displaying a character (5*8 point matrix) requires 8 bytes. Therefore, the 64-byte CGRAM can only save 8 custom characters. We will utilize the custom byte space to achieve the effect of displaying Chinese characters.
So how does this 8-byte CGRAM save a character? This requires taking the modulus of the Chinese characters, as shown in the figure below.
Have you also noticed that the 5*8 point matrix can only display some simple Chinese characters? More complex ones won’t work, such as “嵌” character?
Is it really impossible?
Actually, there is a way to display it. We can split it into two 5*8 point matrices, as shown in the figure below.
Then we can take the modulus of this “嵌” character into two 5*8 point matrices.
Next, we will take the Chinese character “嵌入式小书虫” modulus and save it in an array as follows:
#define CHNESE_NUM 7char code chnese_data[8*CHNESE_NUM]={0x11,0x1f,0x0a,0x1f,0x0a,0x0e,0x0a,0x0e,//嵌0x02,0x1e,0x04,0x0f,0x15,0x04,0x0a,0x11,0x00,0x08,0x04,0x06,0x0a,0x11,0x11,0x00,//入0x09,0x04,0x1f,0x02,0x1d,0x09,0x1d,0x01,//式0x00,0x04,0x04,0x1d,0x15,0x05,0x0c,0x04,//小0x09,0x1f,0x0a,0x1f,0x09,0x09,0x0b,0x08,//书0x04,0x1f,0x15,0x1f,0x04,0x05,0x1f,0x01 //虫};
With this array, how do we write it to CGRAM?
We use command 7 to set the CGRAM address, and then write data into it, as follows:
void write_chnese_data(){ char i; for(i = 0; i < 56; i++){ lcd1602_write_cmd(0x40+i); lcd1602_write_data(chnese_data[i]); }}
-
Since we have 7 5*8 point matrix character models, we need to write 56 bytes (one 5*8 point matrix requires 8 bytes).
Finally, we need to display the character models from CGRAM.
Don’t worry, this is mentioned in the LCD1602 manual. Since CGRAM can only save 8 custom character models, we can only write 0~7 to display the custom characters. The program for displaying “嵌入式小书虫” is as follows:
void show_chnese(){ lcd1602_write_cmd(0x80+2); lcd1602_write_data(0); lcd1602_write_data(1); lcd1602_write_data(2); lcd1602_write_data(3); lcd1602_write_data(4); lcd1602_write_data(5); lcd1602_write_data(6); }
The program is very simple, and the display effect of the Chinese characters is as shown in the figure below.
The displayed character “嵌” doesn’t look very good. If it were a left-right structured character, it might look better (* ̄︶ ̄). You can also think about why the second line of characters does not move when using the full-screen left move command (0x18) (how is it implemented? Feel free to leave a message).
Next is today’s surprise segment.
Today’s surprise segment is to optimize the code of the previous time-slicing scheduler to make it simpler and more elegant.
First, let’s talk about the problems we encountered. Although the LCD task only has one left move command, as shown below:
void lcd_task(){ lcd1602_write_cmd(0x18);}
However, this task, although simple, calls the busy check function in the lcd1602_write_cmd function, and the running time of this function is uncertain (we can only guarantee that it is less than 1ms because we exit the dead wait using a counting variable)
Let’s take a look at the task initialization function as follows:
void tasks_init(){ task_manage.taskNum = 0; add_task(2,&smg_display); add_task(1000,&smg_clock); add_task(500,&lcd_task);}
There are a total of 3 tasks: the first runs every 2ms, the second every 1000ms, and the third every 500ms. In this way, every 1000ms (the least common multiple of 2, 1000, and 500 is 1000), these 3 tasks will run simultaneously. Note that when we say simultaneously, we mean that after running this task, we immediately run the next task (not sure if I explained it clearly), just like the year of the tiger is followed by the year of the rabbit.
Assuming each task runs for less than 1ms, but when all three tasks run together, it may exceed 1ms (the timer interrupts every 1ms). Of course, this won’t cause any major problems, but at least there will be some safety hazards. Can we eliminate it?
The answer is definitely yes.
Let’s take a look at the task adding function and task structure as follows:
typedef void fun_t(void);typedef struct { char statue;//Running status int cycle;//Running cycle int count;//Counting variable fun_t *run;//Task function}task_t;void add_task(int cycle,fun_t *run){ if(task_manage.taskNum < TASK_NUM_MAX){ task_manage.tasks[task_manage.taskNum].statue = 0; task_manage.tasks[task_manage.taskNum].cycle = cycle; task_manage.tasks[task_manage.taskNum].run = run; task_manage.taskNum++; }}
We find that the count in the task structure is not assigned in the task adding function add_task. We can use this variable to separate multiple tasks.
For example, we assign the count of the smg_display task to 1. Since its running interval is 2ms (i.e., it executes every 2ms), it will execute this task at 1ms, 3ms, 5ms, 7ms, etc. Therefore, the smg_display task executes at odd times, while the other two tasks execute at even times, thus separating the smg_display task, which will never execute at the same time as the other tasks.
Great, but what about the remaining two tasks?
Similarly, we assign the count of the smg_clock task to 2, and the lcd_task task to 4. This way, it works as shown in the table below:
The smg_clock task adds 1000ms each time, while the lcd_task task adds 500ms each time. So how does 998 come about? That’s because we assign the count of the smg_clock task an initial value of 2, meaning it starts counting from 2, adding to 1000 to execute the smg_clock task, which results in adding from 2 to 1000, giving us exactly 998.
Now that we know this, let’s look at the table above. Pay attention to the unit digits; smg_clock and lcd_task have 8 and 6 respectively, so they won’t execute together.
Now let’s modify the task adding function as follows:
void add_task(int cycle,int count,fun_t *run){ if(task_manage.taskNum < TASK_NUM_MAX){ task_manage.tasks[task_manage.taskNum].statue = 0; task_manage.tasks[task_manage.taskNum].cycle = cycle; task_manage.tasks[task_manage.taskNum].count = count; task_manage.tasks[task_manage.taskNum].run = run; task_manage.taskNum++; }}
-
Just add a parameter count.
The task initialization function can be written as follows:
void tasks_init(){ task_manage.taskNum = 0; add_task(2,1,&smg_display); add_task(1000,2,&smg_clock); add_task(500,4,&lcd_task);}
This way, we have eliminated the risk of multiple tasks running at the same time. Isn’t it simple? (* ̄︶ ̄)
Alright, today’s article ends here. Of course, there are still some shortcomings, so please point them out to me. I have also created a microcontroller learning group. Everyone is welcome to join and learn together (* ̄︶ ̄)
Original works are not easy. If you like my public account and find my articles inspiring,
please be sure to “like, collect, and forward.” This is very important to me. Thank you!
Welcome to subscribe to Embedded Little Bookworm
Leave a Comment
Your email address will not be published. Required fields are marked *