CubeMX移植LCD驱动
电子显示屏想必对于能看到这篇文章的你而言肯定不会陌生,无论是你平常使用的手机、电脑,还是电视、电子手表,可以说当今消费级的电子设备十种有九种都会带个屏。由于本文为实操性质的单片机技术文章,就不过多详细介绍基础的显示原理和一些技术细节了。简单来说,当今的消费级电子产品的屏幕主要分LED、LCD、OLED三类,各具特色,有关资料在任何搜索引擎都能搜到相当多的内容。就以当今手机使用的显示屏为例简单了解一下区别,这位UP主的视频做的很不错值得一看:
引脚资源
大部分单片机开发板配备的显示屏多为LCD,我所使用的这块正点原子的战舰V2开发板的显示屏用的是ALIENTEK3.5寸TFTLCD,本文以这块开发板为例记录一下在CubeMX上的LCD驱动移植方法。
首先,LCD的驱动在单片机上主要有两种实现方式:
- 通过GPIO模拟时序
- 通过FSMC模拟时序
GPIO大家都很熟了,通过I/O口的输入输出控制信号量的变化,无论是按钮信号还是点亮LED灯珠用的都是GPIO。
FSMC(Flexible Static Memory Controller,可变静态存储控制器)是STM32系列采用的一种新型的存储器扩展技术,可用于驱动SRAM,NOR FLASH,NAND FLASH,PC Card等诸多静态内存。
大容量,且引脚数在 100 脚以上的 STM32F103 芯片都带有 FSMC 接口,其操作时序和 SRAM 的控制完全类似,唯一不同就是 TFTLCD 有 RS 信号,但是没有地址信号。因此,可以通过模拟显示屏时序的方式实现对LCD的驱动。
而相比于普通的GPIO,FSMC由于操作外部存储设备因此速度会快非常多,因此通常在条件允许的情况下会选择使用FSMC驱动LCD,例如正点原子的mini板没有FSMC接口所以只能用GPIO,而战舰板则会使用FSMC驱动LCD,这在提供的例程源码中的LCD_Init()函数初始化液晶管脚时就能发现两者LCD驱动的明显不同之处。
根据原理图与例程源码,我们可以发现实际上LCD使用了如下的管脚资源:
管脚编号 | 对应功能 | 作用 |
---|---|---|
PD14-15 | FSMC_D0-1 | 数据传输 |
PD0-1 | FSMC_D2-3 | 同上 |
PE7-15 | FSMC_D4-12 | 同上 |
PD8-10 | FSMC_D13-15 | 同上 |
PG12 | FSMC_NE4 LCD_CS | 分区片选,固定访问地址 |
PG0 | FSMC_A10 LCD _RS | 指令/数据选择信号 |
PD4 | FSMC_NOE LCD _RD | 读数据/指令 |
PD5 | FSMC_NWE LCD _WR | 写数据/指令 |
PB0 | LCD_BL | 背光控制 |
需要注意的是,除了FSMC与背光GPIO的配置,由于正点原子的LCD驱动初始化中休要通过printf函数打印屏幕id,需要开启USART串口
并重写printf的底层函数fputc。
CubeMX配置
了解了所利用的引脚资源后,接着要做的自然是通过CubeMX进行初始的管脚配置了。
CubeMX的安装相关网上资料很多我就不重复造轮子了,没有经验的同学这里推荐一篇不错的文章:
【STM32】STM32 CubeMx使用教程一--安装教程
首先,选择我们使用开发板的芯片型号:
例如我使用的战舰V2开发板芯片型号为F103ZET6,那么直接CubeMX左上角搜索就可以找到对应型号的芯片:
选择好我们的芯片后,接着要做的自然是配置管脚与时钟了。
根据上文的引脚资源使用情况分析,我们可知我们需要配置的主要有三大块内容:
-
配置时钟树
-
配置FSMC
-
配置串口
-
配置GPIO
配置时钟树
在不需要考虑低功耗、省电的情况下,时钟树的配置通常比较简单,不进行详细需求分析直接使用系统自动配置就行,多耗点电。
首先,是开启HSE,选择外部晶振作为时钟源。
不上操作系统的情况下,时基可以简单的选择系统时钟就行。Debug可以开个SW方便后续开发调试。
然后,我们需要配置一下具体的时钟树。
在时钟树配置里将HCLK调到常用的72MHz,按下回车,后续的时钟配置CubeMX会自动帮你配好。
简单的几步,我们的最小系统配置其实就已经完成了。后续的配置基本都是外设内容相关了。
对于时钟细节概念与了解,可以学习参考一下这篇博文:
配置FSMC
FSMC的相关配置在通讯配置选栏中,通常控制LCD用的都是SRAM的模式A或NOR FLASH的模式B时序,FSMC控制LCD屏的原理会在后文进行讲述,这里我们先了解一下FSMC的地址映射是如何控制LCD的。
FSMC 把整个 External RAM 存储区域分成了 4 个 Bank 区域,并分配了地址范围及适 用的存储器类型,如 NOR 及 SRAM 存储器只能使用 Bank1 的地址。 在每个 Bank 的内部 又分成了 4 个小块,每个小块有相应的控制引脚用于连接片选信号,如 FSMC_NE[4:1]信 号线可用于选择 BANK1 内部的 4 小块地址区域,见图 26-17,当 STM32 访问 0x6C000000-0x6FFFFFFF 地址空间时,会访问到 Bank1 的第 1 小块区域,相应的 FSMC_NE1 信号线会输出控制信号。
野火教程里的资料其实讲的挺清楚了,有兴趣的比较推荐详细学习一下。简单来说就是拓展RAM提供了四个区块用于控制不同的外部存储,其中Bank1用于控制NOR/PSRAM类存储。每个Bank又分别划分了四个区间,每个区间有64M的空间大小,映射对应的外部存储地址,这样我们就可以通过FSMC控制外部存储设备了。
因此,我们需要配置FSMC位置为Bank1
,区间可以和正点原子例程中一样选NE4
,存储类型选LCD接口
,命令/数据位找个没用到的FSMC地址管脚,例如和例程一样选A10
。由于我使用的ILI9341数据存储宽度为16位,根据自己的LCD型号数据手册选相应的DATA bit
就行。
做好FSMC基础配置后,我们可以看到在CubeMX的界面右侧管脚视图可以看到所利用到的管脚资源变成了绿色,且已经自动进行了管脚功能昵称的编辑。这就是CubeMX最吸引人的地方之一,可视化的方式清楚的了解自己所做的工作与使用了的资源,极大的简化了标准库开发的繁琐工序,明确自己已完成的工作内容。
读写时序参数配置
开启extend mode,即分别控制FSMC的读写时序。
Bus turn around time 用于NorFLASH,SRAM无用,设0
通常来说,控制SRAM采用的是A模式,因此 Access mode 选Mode A
。
对于Address setup time
与Data setup time
参数,根据读取寄存器中lcd的id可以得知为ILI9341,由数据手册写周期为最小 twc = 66ns,而读周期最小为 trdl+trod=45+20=65ns。(对于读周期表中有参数要一个 要求为 trcfm 和 trc 分别为 450ns 及 160ns,但据火哥测试并不需要遵照它们的指标要求)
首先,HCLK我们设置值为1/72M,即13.8ns,这就是我们液晶驱动的最小单位时间了。
写时序
由于twrl
写控制低电平信号最小时间为15ns,故可以设置FSMC模式B的地址建立周期DATAST要大于15ns, 用来保证写控制低电平信号有效。
DATASTHCLK > 15ns, 2 \ 13.8 > 15 ,所以设置ADDSET=2
保证写低电平有效。
tdht
数据保持时间,与 twrh写控制高电平的最小时间相同,是10ns,在这个周期内WRX线处于高电平。
观察时序图,当NWE变成高电平后,会持续1HCLK=13.8ns,默认满足tdht,不需要考虑tdht数据保持时间。
tdst
数据设置时间最小是10ns,由于1HCLK=13.8ns>10ns,因此DATAST≥1即可满足。但同时,我们需要满足完整的写入周期twc
也得满足最小值,
((ADDSET+1) + (DATAST+1))HCLK > 66ns ,((1+1)+(2+1))*13.8 = 5x13.8 = 69ns>66ns
故可以设置为ADDSET=1,DATxAST=2
读时序
由于trdl
读控制低电平信号最小时间为45ns,因此()(DATAST+1)+2)HCLK>45ns,即((1+1)+2)13.8=55.2>45,DATAST=1
可满足该条件。
trdh
读控制高电平信号最小时间为90ns,但由于F103的FSMC性能问题,实际上就算设置 ADDSET 为 0,RD 的高电 平持续时间也达到了 190ns 以上,所以可设ADDSET=1
实际 RD高电平大于 200ns。
但因为trcfm
最小帧缓存周期为450ns,需满足((ADDSET+1)+(DATAST+1)+2)*HCLK>450ns.
考虑到对更多屏的兼容性,通常可设DATAST=15
。验证可得203.8+((15+1)+2)*HCLK=452.2>450ns.
因此,我的FSMC相关配置如下:
配置USART串口
前文提到,由于正点原子的lcd驱动的初始化过程涉及到了printf打印串口信息识别型号,需要开启USART串口并后续进行重写fputc函数。
在CubeMX中配置串口十分简单:
模式选择异步通讯,为后续开发需要可配置NVIC开启中断。
GPIO配置
LCD不同于OLED能够单像素点自发光,需要开启背光后通过液晶偏振生成颜色。因此,管脚用到了GPIO进行背光信号的开关。为便于安装LCD屏,直接选择正点原子默认的PB0
管脚,左键点击管脚视图选择GPIO_Output
,右键添加一下用户标签方便确认管脚功能:
CubeMX对于GPIO 输出模式的默认配置就是推挽输出,满足控制LCD背光的需要,在默认基础上可以调一下最大输出速度模式为高速,不过通常不会有快速开关背光的需要,影响不大。
至此,我们在CubeMX的基本管脚配置就已经完成了,生成相应平台的代码后我们就可以进行后续的功能开发了。
代码生成
我们到项目管理(Project Manager)里选择一下我们代码的生成方式与平台配置:
例如,我给项目取名为ZET6,项目地址为桌面的Warship文件夹。我的后续代码在Keil5中开发,因此工具链选MDK-ARM,版本V5。最小堆与栈的大小无特殊需要可先暂且不管,固件库通常选择安装默认的版本就行。
然后,再于代码生成(Code Generator)处配置代码生成的方式:
打包方式一般选择复制必要的库文件方便移植且不会太大,生成文件我更喜欢生成.c和.h文件的方式更加美观清晰。然后就可以回到我们熟悉的Keil中添加我们的具体项目代码了。
英语不好或者还是觉得很多细节不清楚有困难的同学,推荐阅读一下下面这篇非常优质的博文,相当喂饭式的学习一下以点灯为基础的CubeMX配置开发流程:
【STM32】STM32CubeMX教程二--基本使用(新建工程点亮LED灯)
代码配置
通常STM32的开发应该无非是用MDK\IAR\VScode了。我更习惯于使用Keil MDK开发,因此就以该环境为例简单提一下代码配置过程。
来到Keil中,首先要做的自然是LCD驱动的移植了。使用开发板学习的同学推荐到板子设计商例如正点原子、野火、安富莱的论坛资源帖找相应的例程便于开发,这也是最为高效与优质的学习方法,也是开发板学习的必经之路。
CubeMX使用的是HAL库,相比于标准库其实最直接的不同也就是资源调用函数的名字不一样而已。但是CubeMX的一个特点是为保证生成代码效果与质量,用户只能在指定的区域内写入自己的代码,这虽然可能算一种限制但同样也是一项优点,能够更好的了解自己代码需要用啥,该放哪里,在寻找Bug时也更容易避免与发现不少低级错误。
借用一下Z小旋博主的图:
我们在文件中相应位置添加代码,这样再调整CubeMX重新生成时我们自己写的代码也就不会被删除了。
移植的第一步,首先自然是添加驱动文件并链接了。我们首先找到例程中的LCD.c和.h驱动文件移动到自己想放的位置,我习惯于放在Drivers文件夹里,只要是在工程文件中在哪都行。
左上方的项目文件夹右键,选择Add Group添加新组,随后改为便于自己理解管理的名字。一般其实可以直接在上方项目管理中创建并改名,但由于CubeMX生成文件存在一定的小bug,这样添加可能会卡死。解决方法如下:
CubeMX生成的文件直接添加组存在Bug导致未响应的情况,有两种解决方法:
1.直接工程中新建组后再重命名、添加文件
2.在包管理中将 “Manage Run-Time Environment” 中 “CMSIS” 的 “CORE” 的对勾去除
对于连链接新文件都还不熟悉的同学,我更推荐先跟着开发板提供商的视频或文档学一学基本的前三章左右的内容,了解一下基本的开发流程。这里就简单提下两个关键步骤:
1.项目管理中添加新组链接相应.c文件
2.Target中包含路径(Include Paths)里添加.h文件所在文件夹路径。
lcd.c
移植完文件后,我们需要修改一下驱动文件的配置。正点原子的例程讲管脚配置都写在了驱动中,而我们在CubeMX中都已经配置好了,因此注释掉这一部分。
以战舰V3的例程为例,fsmc相关配置和GPIO配置写在了lcd.c文件中。我们注释掉整个HAL_SRAM_MspInit()
函数,以及LCD_Init()
中前半部的FSMC相关配置,直到LCD型号判断部分前的代码。这样,LCD部分的主要代码就移植完成了。
原本正点原子还有一个串口配置文件usart.c与.h文件,我们在CubeMX中已配置因此不移植。把LCD.c中引用该文件的#include “usart.h”
注释掉。
sys.c
但是,移植正点原子的驱动时,由于lcd驱动延时使用的他家自己的阻塞延时函数,考虑到修改优化工作量巨大,因此最好还是顺带移植一下delay的.c和.h文件。这两个文件还用到了sys.h中定义的操作系统判断量:
//0,不支持ucos
//1,支持ucos
#define SYSTEM_SUPPORT_OS 0 //定义系统文件夹是否支持UCOS
而且光移动这个定义也不能根本性的解决问题,正点原子的诸多驱动都用到了这个判断。。。
那能怎么办呢,吃人家的嘴短,用的人家板子只能硬着头皮啃屎山了。按相同方法移植一下sys.c和sys.h文件,注释掉sys.c中初始化系统时钟的函数Stm32_Clock_Init()
,凑合着用吧。
usart.c
前文提到,我们需要改写printf
的底层函数fputc
以使原本输出到终端,但单片机中不可视的部分写到我们可以观测的串口中以便打印输出,因此我们需在usart.c的头文件中添加printf函数的头文件stdio.h
。
/* USER CODE BEGIN 0 */
#include <stdio.h>
/* USER CODE END 0 */
然后,再在用户代码区添加我们重写的函数:
/* USER CODE BEGIN 1 */
/**
* 函数功能: 重定向c库函数printf到DEBUG_USARTx
* 输入参数: 无
* 返 回 值: 无
* 说 明:无
*/
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
/**
* 函数功能: 重定向c库函数getchar,scanf到DEBUG_USARTx
* 输入参数: 无
* 返 回 值: 无
* 说 明:无
*/
int fgetc(FILE *f)
{
uint8_t ch = 0;
HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
return ch;
}
/* USER CODE END 1 */
这样,我们就可以通过串口打印printf的信息了。
main.c
前期的移植工作做完,编译不报错Error就可以去main函数中测试移植效果了。
先在用户自定义头文件区添加使用到的.c与.h文件:
/* USER CODE BEGIN Includes */
#include "sys.h"
#include "delay.h"
#include "lcd.h"
/* USER CODE END Includes */
然后,在主函数中添加相应的调试代码:
/* USER CODE BEGIN 1 */
u8 x=0;
u8 lcd_id[12]; //存放LCD ID字符串
/* USER CODE END 1 */
/* USER CODE BEGIN 2 */
delay_init(72); //初始化延时函数
LCD_Init(); //初始化LCD FSMC接口
POINT_COLOR=RED; //画笔颜色:红色
sprintf((char*)lcd_id,"LCD ID:%04X",lcddev.id);//将LCD ID打印到lcd_id数组。
/* USER CODE END 2 */
注意lcd初始化函数要放在生成的串口初始化函数之后,否则会无法打印lcd的型号id导致程序停止在lcd初始化中。
/* USER CODE BEGIN WHILE */
while (1)
{
switch(x)
{
case 0:LCD_Clear(WHITE);break;
case 1:LCD_Clear(BLACK);break;
case 2:LCD_Clear(BLUE);break;
case 3:LCD_Clear(RED);break;
case 4:LCD_Clear(MAGENTA);break;
case 5:LCD_Clear(GREEN);break;
case 6:LCD_Clear(CYAN);break;
case 7:LCD_Clear(YELLOW);break;
case 8:LCD_Clear(BRRED);break;
case 9:LCD_Clear(GRAY);break;
case 10:LCD_Clear(LGRAY);break;
case 11:LCD_Clear(BROWN);break;
}
POINT_COLOR=RED;
LCD_ShowString(30,40,210,24,24,"WarShip STM32 ^_^");
LCD_ShowString(30,70,200,16,16,"TFTLCD TEST");
LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(30,110,200,16,16,lcd_id); //显示LCD ID
LCD_ShowString(30,130,200,12,12,"2017/5/27");
x++;
if(x==12)x=0;
delay_ms(1000);
/* USER CODE END WHILE */
这样,我们的代码配置也就完成了。编译程序,下载到开发板,根据自己的仿真器选择相应的烧录模式与Flash大小,选择上电重启,就能看到液晶屏的变色与信息打印结果了。
以上,我们便完成了LCD驱动的移植过程。然而身在无处不屏的时代,有了显示自然少不了触摸控制了。有关触摸控制的移植,将在之后的触摸控制移植文章中进行讲解学习。
本文为原创个人开发学习过程的记录总结,难免存在不少错误与疏忽,欢迎留言或通过社交媒体联系我指出问题,也欢迎在评论中留下您的建议与见解,或是贴上中意的优质文章链接。
更好的车轮,更远的里程。
参考资料
FSMC驱动TFT显示屏(和驱动触摸屏)_oyzy_Sean的篝火-CSDN博客_tft屏幕驱动
SRAM Controller - IAmAProgrammer - 博客园 (cnblogs.com)
STM32CubeMX系列 | TFTLCD显示 - 知乎 (zhihu.com)
STM32篇七:TFTLCD(ATK-4.3‘)_OnlyLove_的博客-CSDN博客
stm32学习笔记 -根据外接存储器时序初始化FSMC结构体
主要参考资料下载链接:
STM32F1 开发指南 V1.0 - HAL 库版本 −ALIENTEK 战舰 STM32F103 V3 开发板教程
STM32 HAL 库开发实战指南 —基于野火 F103 霸道开发板
大神,我移植完移植黑屏,debug调试的时候卡在LCD清屏for循环里找不到哪里出问题,可以把工程发给我研究一下吗?非常感谢
for(index=0;indexLCD_RAM=color;
}
不好意思没有打理博客没看到您的评论,这个教程当时开发毕业设计项目时边开发边写的,开源在了我的github上。地址:https://github.com/th1matic/Warship/commit/bc20617ca5f947354d5e4262508bcae571b314ca 我会给您发邮件,有问题后续QQ聊吧
补上发往邮件的回答。
不知道您是否已经寻到了解答,由于项目有一段时间没有接触印象有点淡了,不一定保证能解答您的困惑,下面的回答还请仅作参考。
在学习裸机开发时,一般分三步骤:1.直接使用例程确保硬件、例程函数无误 2.基于模板移植函数 3.利用函数自由开发
由于我使用的板子型号较老,使用的例程是以前V2的老例程,所以我的工程文件适用开发板可能与您的不一样没法直接使用,不过正点原子的LCD驱动逻辑基本没动,因此如果设备能够正常运行例程的话那么驱动文件就可以直接用了。
关于您提到的移植黑屏卡在Debug的情况,由我自己的学习开发过程经验可能存在以下问题:
1.头文件引用有误 注意调用文件的大小写,以及移植时和CubeMX的配置有无重复。
2.串口配置未完善 由于正点原子的LCD驱动为保证各型号显示屏都能通用,初始配置函数 TFTLCD_Init(void) 中进行了大量嵌套循环判断显示屏的型号,使用了串口来打印LCD ID,因此这一步不能跳过。
3.调用的函数是否在主函数main(void)的正确位置 初始化的一系列配置函数顺序是会有影响的,通常如定时器、中断、串口等基本驱动xxx_Init()函数在前,显示屏、模块等驱动在后。同时,使用CubeMX配置时一定注意函数是否放在了有效区域 (如 /* USER CODE BEGIN 2 */与/* USER CODE END 2 */内)。
4.确认例程函数本身有无问题 例如例程中TFT_Init() 最后配置完成后会会点亮背光再清屏,可以通过自己添加一个LED指示灯在 LCD_LED=1;LCD_Clear(WHITE);前面,根据LED灯是否正常点亮来判断程序是否能正常运行到此处。如果正常点亮却依旧不亮,那么就能判断是否是LCD_Clear()可能存在问题。
以上便是我认为容易遇到的一些问题,希望能对您有所帮助。