Qurak

Make Code Talk

0%

STM32单片机HAL库学习MacOS(4)——LCD显示

——Dec.21.2020

目录

本次实验采用正点原子STM32F407探索者开发版和STM32cubeIDE软件开发

  • 单片机上LCD屏幕显示原理
  • 引脚相关配置
  • 调用相关函数画个人

开头说两句

最近学习单片机LCD屏幕的显示,一开始遇到了一些困难和坑,面对复杂的LCD的控制原理、寄存器读写操作以及LCD初始化配置指令着实有点让人不知从何下手。不过都在网上以及正点原子资料的帮助下成功实现LCD的显示,在这里和大家分享一下。

对于一个小萌新而言,我认为应该先学会用,然后再实践中发现问题来寻找原理。如果一开始就卡在复杂的寄存器指令配置,那么学习还是会变得很无聊的(泪

一、单片机上LCD屏幕的显示原理

(1)LCD显示原理

LCD(Liquid Crystal Display)也就是我们平时所说的液晶屏,液晶屏是如何显示的呢?液晶屏大致可以分为三层,最底层是一个背光板——负责发白光;背光板的上面会有一层偏光片,将白光滤成偏光片;再上面是液晶,我们通过电信号来控制液晶和彩色滤光片来控制光的颜色。我们知道光的三原色RGB,通过控制每个像素位置上的RGB三色滤光片来实现像素点彩色的显示。

(2)单片机通信LCD

  • 那么单片机是如何与LCD通信的呢?

    我们首先介绍一下FSMC(Flexible Static Memory Controller,可变静态存储控制器)FSMC,即灵活的静态存储控制器,能够与同步或异步存储器和16位PC存储器卡连接,STM32的FSMC接口支持包括SRAM、NAND FLASH、NOR FLASH和PSRAM等存储器。可以理解成一个比较万能的存储连接模块,可以用来控制各种存储外设。而TFTLCD在这里相当于一个SRAM,我们通过FSMC调用这个SRAM。通过读写寄存器使LCD显示出我们需要的东西。

    所以我们可以把LCD显示的过程理解成用FSMC这个工具往这个LCD的盒子里装东西。因此我们学会根据LCD的数据手册进行读写寄存器的配置。

引脚相关配置

  • 首先是经典时钟树配置(参考前面的时钟树配置,结果差不多)

  • Connectivity -> FSMC -> NOR Flash/PSRAM/SRAM/ROM/LCD 1

    Chip Select: NE4 (片选信号,根据原理图我们可以知道LCD片选选择NE4)

    Memory type:LCD Interface (选择为LCD)

    LCD Register Select: A6 (原理图中我们可以看到RS对应A6)

    Data: 16bits (我们选择16位数据,传输速率会快一倍)

    Attention: 在上一个文章中矩阵键盘的引脚设置中包含了A6,若要同时使用这两者,需要更改一下矩阵键盘的引脚设置,由于矩阵键盘代码大多数采用宏定义和标签来写,因此修改引脚时要记得加上Label以及函数HAL_GPIO_ReadPin(),HAL_GPIO_WritePin()中需要检查对应GPIO_Port是否正确。

    LCD原理图

  • 然后我们设置NOR/PSRAM 1的内容

    • 首先是控制部分(control):

      主要设置拓展模式(Extended mode):Enable

    • 时序设置(timing):(这里数字为高频时钟脉冲数,如果按照之前设置的系统时钟为168M,一个脉冲就约为6ns,根据数据手册中我们可以去查询相应的时间,然后换算成相近的脉冲数注意,这里计数是从0开始的 )

      「以下是根据正点原子资料所设置」

      Address setup time:15 (大约为16x6=96ns)

      Data setup time: 60 (对于我们这个TFTLCD而言并不需要这么久,考虑到代码兼容性设置为60)

      Bus turn around time: 1 (这个随意设置吧,我查了半天没弄明白这个具体是什么,网上有设置为0,也有设置为十几的,应该问题不大)

      Access mode: A (这里选择访问模式A)

    • 读时序设置:(由于我们选择拓展模式,读写是分开的寄存器,即读写分离。因此我们就需要设置这里的读时序,若extended mode unable,则不需要设置这里)

      Extended address setup time: 2

      Extended data setup time: 2

      Extended bus turn around time: 1

      Extended access mode: A

    NOR:PSRAM设置

    • 最后,我们需要打开背光源(PB15)若没有打开,LCD是看不到显示的

      Maximun output speed: High (好像Low和High没太大差别)

      GPIO Pull-up/Pull-down: NPND (选择推挽输出)

      User Label: LCD_BL(这里可以自定义,单纯方便说明这个引脚的用途)

      PB15_set

都设置好了就可以点击Generate Code,进入代码部分

Here we go~ ⛵️

驱动代码的引入以及代码修改~~(缝合)~~

作为一个萌新,自己写驱动不仅费时费力消耗精力而且还容易出错,因此这里我采用了正点原子的驱动代码,但是由于我的文件体系和正点原子给的体系有点不同,因此还需要做一点改变~~(缝合)~~。一下简单介绍一下。

1、导入文件

我们找到正点原子相关的工程文件夹,HARDWARE -> LCD中找到对应的lcd.hlcd.c以及font.h(字库文件)并拷贝到我们自己的工程文件夹中,这里我设置一个MyFunction文件夹存放外设文件。因此我将这两个文件拷进去。另外我们还需要System -> sys文件夹中找到sys.c以及sys.h,同样复制到MyFunction文件夹中,并导入到工程中。

注:如果导入成功后,在工程中打开文件后中文出现乱码大概是因为IDE的文本编码和我们添加的文本编码不大一样,我们随便打开一个文本输入界面将中文复制粘贴到IDE中就好了。

2、修改代码

  • 修改代码适配我们的文件体系,我们需要注意几个地方

    (1)引入的文件中,是否有原来的工程中没有的宏定义和变量;

    (2)引入的代码函数中,是否有没有明确声明过的函数;

    (3)引入的文件是否和原工程有冲突(相同的函数,相同的变量,相同功能的模块等)。

(1)头文件包含,宏定义和变量

点开lcd.h观察头文件包含:

1
2
3
4
5
6
// Include head files
#include "lcd.h" // The head file of LCD.c
#include "stdlib.h" // It is not necessary if you do not use "printf()" in project
#include "font.h" // This is a font of characters. It makes us show characters easily
#include "usart.h" // It is used in UART or USART and It not necessary because we can set up in STM32cubeIDE
#include "delay.h" // It contains the delay functions. We can use HAL_Delay() instead

因为我暂时没有对与printf()重定向(在HAL库中若要使用**printf()**函数,除了添加头文件,还需要重定向处理。)所以不需要"stdlib.h"。而最后两个是正点原子编写的串口通信代码以及延迟函数的代码,这里我没有采用因此我也把他们删掉了,最后只保留下lcd.h以及font.h

这里注意的是,点开lcd.h后,我们可以看到:

1
2
3
4
5
6
//lcd.h
#ifndef __LCD_H
#define __LCD_H
#include "sys.h" //"sys.h" is also included in this project
#include "stdlib.h"
...

我们可以打开sys.h,文件中主要是宏定义了一些变量.sys.c中主要是系统初始化的配置代码,我们点开sys.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//定义一些常用的数据类型短关键字 
typedef int32_t s32;
typedef int16_t s16;
typedef int8_t s8;

typedef const int32_t sc32;
typedef const int16_t sc16;
typedef const int8_t sc8;

typedef __IO int32_t vs32;
typedef __IO int16_t vs16;
typedef __IO int8_t vs8;

typedef __I int32_t vsc32;
typedef __I int16_t vsc16;
typedef __I int8_t vsc8;

typedef uint32_t u32;
typedef uint16_t u16;
typedef uint8_t u8;

typedef const uint32_t uc32;
typedef const uint16_t uc16;
typedef const uint8_t uc8;

typedef __IO uint32_t vu32;
typedef __IO uint16_t vu16;
typedef __IO uint8_t vu8;

typedef __I uint32_t vuc32;
typedef __I uint16_t vuc16;
typedef __I uint8_t vuc8;

由于我之前没有考虑拿正点原子的代码,所以没有设置简化后的变量类型。但是驱动文件中许多变量都是定义为简化后的名称,因此我们需要把这些简化的变量命名添加在lcd.h头文件中。

然后我们继续检查,在大约60行的地方,我们可以看到这里有一个宏定义:

1
2
//-----------------MCU屏 LCD端口定义----------------
#define LCD_LED PBout(15) //LCD背光 PB15

其中PBout(n)函数很显然并不出现在我们的HAL库中(读者可以右键打开声明或者对F4xx等配置文件搜索PBout函数,会发现不存在这个函数)我们点开sys.h文件可以发现:

1
2
3
4
5
6
7
//PBout() function declaration
// IO interface mapping
#define GPIOB_ODR_Addr (GPIOB_BASE+20) //0x40020414
#define GPIOB_IDR_Addr (GPIOB_BASE+16) //0x40020410
// IO interface operation
#define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) // output
#define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) // input

这是通过寄存器控制PB15的输出——是否点亮LCD背光。如果要直接用的话可以直接将对应的宏定义放入,其中GPIOB_BASE``GPIOB_ODR_Addr``GPIO_IDR_Addr均可以在配置文件中找到。但是BIT_ADDR()这个函数并没有找到,猜测可能是换了名字,但是我还没找到对应的函数。因此这里我将PBout的宏定义注释掉,采用HAL_GPIO_WritePin()函数来开启LCD的背光。(不过还没尝试用PWM输出来控制LCD背光的亮度,以后用到再拿出来说说吧(挖坑)😂 )

然后这个部分算是修改好了,我们接下来第二步。

(2)函数

搞定宏定义和变量后,我们还需要检查有没有我们之前没有声明过的函数出现在下面。

不过一开始看这个庞大的代码大家或多或少会有点害怕,但是如果阅读过lcd文件中相关的代码,我们可以发现大部分代码都是通过读写寄存器来控制的,而这些代码都是底层配置文件中已经有的,所以我们需要改动的地方很少。我们可以通过卡头包含的头文件来猜测。在我们已经删掉了这三个函数:

1
2
3
#include "stdlib.h"    // It is not necessary if you do not use "printf()" in project
#include "usart.h" // It is used in UART or USART and It not necessary because we can set up in STM32cubeIDE
#include "delay.h" // It contains the delay functions. We can use HAL_Delay() instead

其中stdilb.h以及usart.h估计是用来定义printf()的,我这里并没有采用这个函数,因此删掉头文件后,我们还需要在这个文件中查找printf()函数,看lcd.c中是否调用了printf()函数。我们点开搜索,啪的一下很快啊,我们就可以看到在函数LCD_Init()函数中调用了:

1
2
3
4
5
6
void LCD_Init(void)
{
...
printf(" LCD ID: %x\r\n", lcddev.id); //printf the LCD ID
...
}

我们直接注释掉就可以了。

然后我们还有一个头文件delay.h,根据该文件我们可以知道这个文件主要是一些延迟函数的设置。我们点开这个文件可以看到

1
2
3
4
//delay.h
void delay_init(u8 SYSCLK);
void delay_ms(u16 nms); //延迟毫秒
void delay_us(u32 nus); //延迟纳秒

我们对这个文件进行检索,可以发现该文件中有delay_ms()delay_us()两个函数的调用。这里有两种处理方法,第一种是讲这些函数的原型拷贝到我们的lcd.c文件中。第二种方法是替代这些函数,用HAL库自带的HAL_Delay()函数替代,不过注意这个函数延迟时间的单位是毫秒。但我还是厚着脸皮四舍五入全改成HAL_Delay()函数。发现好像没多大差别。读者可以自己选择用哪一种方式进行修改。

(3)检查模块冲突

我们已经检查了变量、宏定义以及函数调用,按道理来说应该可以直接用了。但是如果你按下编译,IDE仍然会报错。这是由于STM32cubeIDE自带配置引脚的功能,因此我们最开始的引脚初始化在我们Generate Code以后就已经生成好了代码。而正点原子的文件中,有一部分函数初始化时候就自带了引脚的配置,这样会产生配置函数之间的冲突。因此我们只需要在IDE中正确配置引脚然后将lcd.c中的相关引脚配置删掉或者注释掉即可。

观察lcd.c文件,我们可以发现函数HAL_SRAM_MspInit()内置了引脚的配置,因此我们将这一段注释掉即可。然后我们就可以点击编译了,看到编译成功,说明我们缝合已经可以了。下面开始写点测试代码试试吧!

编译成功

三、画个小人写点字

上面我们应把相应的驱动代码缝合好了,我们可以写点代码测试一下。这里我打算画个小人然后让小人说两句话。为了实现这个小目标🚩我们需要简单一下驱动文件中自带的绘图函数。读者若之前有用过类似的图形函数,可以直接点开lcd.c或者lcd.h看一看绘图函数都有啥,这里简单列几个:(找到需要的函数点开声明可以查看原理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void LCD_Init(void);													   	//初始化
void LCD_DisplayOn(void); //开显示
void LCD_DisplayOff(void); //关显示
void LCD_Clear(u32 Color); //清屏
void LCD_SetCursor(u16 Xpos, u16 Ypos); //设置光标
void LCD_DrawPoint(u16 x,u16 y); //画点
void LCD_Fast_DrawPoint(u16 x,u16 y,u32 color); //快速画点
u32 LCD_ReadPoint(u16 x,u16 y); //读点
void LCD_Draw_Circle(u16 x0,u16 y0,u8 r); //画圆
void LCD_DrawLine(u16 x1, u16 y1, u16 x2, u16 y2); //画线
void LCD_DrawRectangle(u16 x1, u16 y1, u16 x2, u16 y2); //画矩形
void LCD_Fill(u16 sx,u16 sy,u16 ex,u16 ey,u32 color); //填充单色
void LCD_Color_Fill(u16 sx,u16 sy,u16 ex,u16 ey,u16 *color); //填充指定颜色
void LCD_ShowChar(u16 x,u16 y,u8 num,u8 size,u8 mode); //显示一个字符
void LCD_ShowNum(u16 x,u16 y,u32 num,u8 len,u8 size); //显示一个数字
void LCD_ShowxNum(u16 x,u16 y,u32 num,u8 len,u8 size,u8 mode); //显示 数字
void LCD_ShowString(u16 x,u16 y,u16 width,u16 height,u8 size,u8 *p); //显示一个字符串,12/16字体

我们在main函数中写到:

1
2
3
4
5
6
7
8
9
10
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_15,GPIO_PIN_SET);
LCD_Clear(WHITE); //背景颜色为白色
LCD_ShowString(30,40,200,24,24,"My name is LCD!"); //字符串显示
LCD_ShowString(30,80,200,24,24,"Nice to meet you!"); //字符串显示
LCD_Draw_Circle(200,200,50); //画了个圆当头
LCD_DrawLine(200, 250, 125, 320); //这是左手
LCD_DrawLine(200,250,320,280); //这是右手
LCD_DrawLine(200,250,200,370); //身体
LCD_DrawLine(200,370,150,550); //左脚
LCD_DrawLine(200,370,250,550); //右脚

然后我们点击编译,0error。如果这时候出现LCD_ShowString()警告的话是因为显示内容的指针类型为u8,而我们"…"的为字符串类型,因此会报错。可以尝试添加强制修改(uint8_t)

我们来看看最后的效果:

IMG_4527

nice!


END