尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32F103 FSMC驱动TFTLCD实战:时序配置与代码详解

STM32F103 FSMC驱动TFTLCD实战:时序配置与代码详解 简介这是一套面向 STM32F103 嵌入式开发者的 TFTLCD 显示驱动源码配套完整 Keil 工程示例通过 FSMC 接口完成 LCD 初始化、分辨率配置、命令与数据发送、屏幕刷新以及基础图形绘制适合学习驱动移植或直接用于显示界面二次开发。RAR 压缩包共 206 个文件整体约 3.87MB以 .c/.h 源码文件为主同时包含 .uvprojx 工程文件、编译链接生成的 .axf/.hex/.map 文件以及 .bat 辅助脚本既能查看完整代码也能直接打开工程编译烧录。目前已有 1464 人学习/下载特别适合刚接触 STM32 显示驱动或希望理解 FSMC 控制 TFTLCD 时序的开发者。压缩包内除 LCD 底层驱动和寄存器配置外还带有 stm32f10x 标准外设库的定时器、Flash、RCC、ADC、I2C 等基础模块文件方便在已有框架上继续扩展功能。整体代码结构清晰接口划分明确对照硬件手册阅读可以较快掌握 FSMC 驱动 LCD 的关键流程对入门与二次开发都有实用参考价值。1. FSMC接口为什么是STM32F103驱动TFTLCD的默认答案如果你在STM32F103上点亮过TFTLCD屏大概率用过FSMC接口。这个外设最初是给NOR Flash和SRAM设计的但8080并口液晶屏的时序和SRAM几乎一致于是它顺理成章地成了驱动LCD的高效方案。用GPIO模拟时序当然也能点亮屏幕但当分辨率从2.4寸爬到3.5寸、像素格式切到RGB565时逐位翻转IO的方式会吃掉大量CPU时间——刷一帧图片的时间够FSMC写完十帧。FSMC真正的价值在于CPU向某个内存地址执行写入操作时总线控制器会自动产生CS、WR、RS这些控制信号整个过程不需要CPU干预。也就是说写LCD寄存器变成了写内存变量这在代码层面极大简化了驱动逻辑。这篇文章从FSMC的映射机制讲起到标准外设库v3.5环境下完整的驱动代码组织最后落到时序参数调节和倒挂屏幕这类实际项目中一定会遇到的问题。适合正在调屏、想把手头的ILI9341或ST7789V跑起来的嵌入式开发者也适合复习FSMC工作细节的老手。2. FSMC总线访问TFTLCD的核心机制与Bank1分区2.1 从8080时序到FSMC存储映射的对照关系TFTLCD液晶屏控制器无论是ILI9341、ILI9488还是ST7789V对外暴露的接口本质上是一个16位或8位的并行总线。以最常见的16位8080接口为例操作屏只需要四类信号片选CS、寄存器选择RS也叫D/CX、读使能RD和写使能WR。当RS为低电平时总线上传输的是命令字节RS为高电平时总线上传输的是显存数据或寄存器数据。这种“地址决定访问对象”的模式刚好能映射到FSMC的存储地址空间上。FSMC把外部存储器分成四个Bank其中Bank1专门用于NOR Flash、PSRAM和LCD这类并行设备。Bank1内部又划分为四个片区每个片区对应一个独立的片选信号NE1到NE4片区的起始地址分别为0x60000000、0x64000000、0x68000000和0x6C000000。地址线FSMC_A[25:0]用来索引外部地址而操作方式可以从代码上直观理解访问地址的最高位数值决定了RS引脚状态。如果连线时将FSMC_A6接到LCD的RS引脚那么地址最低位的第7比特决定访问的是命令还是数据。这里给出一个最小系统下的FSMC初始化结构体配置注意存储器类型选择SRAM数据宽度16位时序参数使用扩展模式FSMC_NORSRAMInitTypeDef fsmc_norsram_init; FSMC_NORSRAMTimingInitTypeDef fsmc_read_timing; FSMC_NORSRAMTimingInitTypeDef fsmc_write_timing; fsmc_read_timing.FSMC_AddressSetupTime 0x0F; // 地址建立时间15个HCLK fsmc_read_timing.FSMC_AddressHoldTime 0x00; // 地址保持时间 fsmc_read_timing.FSMC_DataSetupTime 0x1E; // 数据建立时间30个HCLK fsmc_read_timing.FSMC_BusTurnAroundDuration 0x00; fsmc_read_timing.FSMC_CLKDivision 0x00; fsmc_read_timing.FSMC_DataLatency 0x00; fsmc_read_timing.FSMC_AccessMode FSMC_AccessMode_B; fsmc_write_timing.FSMC_AddressSetupTime 0x03; // 写时序通常比读时序快 fsmc_write_timing.FSMC_AddressHoldTime 0x00; fsmc_write_timing.FSMC_DataSetupTime 0x05; fsmc_write_timing.FSMC_BusTurnAroundDuration 0x00; fsmc_write_timing.FSMC_CLKDivision 0x00; fsmc_write_timing.FSMC_DataLatency 0x00; fsmc_write_timing.FSMC_AccessMode FSMC_AccessMode_B; fsmc_norsram_init.FSMC_Bank FSMC_Bank1_NORSRAM4; fsmc_norsram_init.FSMC_DataAddressMux FSMC_DataAddressMux_Disable; fsmc_norsram_init.FSMC_MemoryType FSMC_MemoryType_SRAM; fsmc_norsram_init.FSMC_MemoryDataWidth FSMC_MemoryDataWidth_16b; fsmc_norsram_init.FSMC_BurstAccessMode FSMC_BurstAccessMode_Disable; fsmc_norsram_init.FSMC_AsynchronousWait FSMC_AsynchronousWait_Disable; fsmc_norsram_init.FSMC_WaitSignalPolarity FSMC_WaitSignalPolarity_Low; fsmc_norsram_init.FSMC_WrapMode FSMC_WrapMode_Disable; fsmc_norsram_init.FSMC_WaitSignalActive FSMC_WaitSignalActive_BeforeWaitState; fsmc_norsram_init.FSMC_WriteOperation FSMC_WriteOperation_Enable; fsmc_norsram_init.FSMC_MemoryWriteOperation FSMC_MemoryWriteOperation_Enable; fsmc_norsram_init.FSMC_WriteBurst FSMC_WriteBurst_Disable; fsmc_norsram_init.FSMC_ReadWriteTimingStruct fsmc_read_timing; fsmc_norsram_init.FSMC_WriteTimingStruct fsmc_write_timing; FSMC_NORSRAMInit(fsmc_norsram_init); FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM4, ENABLE);这段代码配置的是NE4片区即访问0x6C000000地址时片选信号FSMC_NE4会被拉低。读时序和写时序分开配置的关键在于LCD控制器的读周期和写周期不一定相同——ILI9341的读周期要求通常比写周期更宽松所以读时序里DataSetupTime给到30个HCLK而写时序只给5个HCLK这样在保证稳定的前提下尽可能提升写速度。2.2 地址线偏移与RS引脚的选择逻辑在BDEMO和正点原子等常见开发板的FSMC原理图里看到RS接FSMC_A6不是巧合。原因在于Bank1的片区内地址线是从FSMC_A0开始排列的16位模式时FSMC_A0对应内部HADDR[1]FSMC_A1对应HADDR[2]依此类推。当连接LCD的RS到FSMC_A6时访问地址中控制RS的位对应HADDR的第7位。定义读写命令和数据地址如下#define LCD_BASE ((uint32_t)0x6C000000) #define LCD_CMD_ADDR ((uint32_t)(LCD_BASE (1 7))) // RS为高电平命令地址 #define LCD_DATA_ADDR ((uint32_t)(LCD_BASE (1 7) 1)) // 实际中常用两倍偏移第7位为1时FSMC_A6输出高电平RS引脚为高此时总线传输数据第7位为0时输出低电平对应命令。代码里用移位运算确保偏移量一目了然。有人把RS接到FSMC_A0上那RS位就对应最低地址位偏移量变成1同样能工作。还有一点容易被忽略LCD的数据引脚是FSMC_D0到FSMC_D15复位引脚通常由普通GPIO控制背光控制引脚也可以用GPIO或定时器PWM。FSMC_NORSRAMInit结构体里的FSMC_MemoryType选择SRAM而不是NOR Flash因为LCD的读写时序最接近SRAM——没有地址锁存周期不需要等待状态机去解析Flash命令。2.3 写命令与写数据的地址分离艺术FSMC驱动LCD的代码核心就是三个基础函数写命令、写数据、读数据。一切图形绘制、字符显示、颜色填充都建立在这三个函数之上。标准外设库时代惯用的写法是定义几个宏把地址强转成指针再解引用#define LCD_CMD *((volatile uint16_t *)0x6C000000) #define LCD_DATA *((volatile uint16_t *)0x6C000008) void LCD_WriteCmd(uint16_t cmd) { LCD_CMD cmd; } void LCD_WriteData(uint16_t data) { LCD_DATA data; } uint16_t LCD_ReadData(void) { return LCD_DATA; }这里地址偏移选8而不是7是因为ILI9341这类控制器的命令和数据寄存器各占一个地址周期RS引脚在FSMC_A6上时内部HADDR[7]控制RS。偏移8意味着HADDR[7]为1同时HADDR[3]也为1但如果FSMC_A6确实对应HADDR[7]那偏移8和偏移7的差别在于额外拉高了FSMC_A3。实际使用中只要保证RS引脚电平正确即可多出来的地址线状态不影响LCD控制器的判断因为LCD只关心RS和总线上的数据。这个细节经常让初学者困惑其实记录下当前板卡的接线算出正确的偏移量写个函数专门打印一下映射值就能验证清楚。3. STM32F103标准库v3.5环境下LCD驱动代码的分层实现3.1 从GPIO初始化到FSMC外设时钟的使能顺序在STM32F103上使能FSMC外设之前必须先使能GPIO引脚的时钟再把相关引脚配置为复用推挽输出。FSMC的引脚不是固定的某个GPIO端口而是分散在GPIOD、GPIOE和GPIOB上——NE4片选在GPIOE的PE7A6在GPIOD的PD3D0到D15分布在GPIOD和GPIOE的低16位。启用这些引脚的复用功能时注意不能简单地把整个端口设为AFIO推挽要精确到每个引脚void FSMC_GPIO_Config(void) { GPIO_InitTypeDef gpio_init; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE | RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_FSMC, ENABLE); gpio_init.GPIO_Speed GPIO_Speed_50MHz; gpio_init.GPIO_Mode GPIO_Mode_AF_PP; // PD0-PD15 中FSMC_D2到D7、D11到D14 gpio_init.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_Init(GPIOD, gpio_init); gpio_init.GPIO_Pin GPIO_Pin_7 | GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11 | GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_Init(GPIOE, gpio_init); gpio_init.GPIO_Pin GPIO_Pin_3; // FSMC_A6 - PD3 GPIO_Init(GPIOD, gpio_init); gpio_init.GPIO_Pin GPIO_Pin_7; // FSMC_NE4 - PE7 GPIO_Init(GPIOE, gpio_init); gpio_init.GPIO_Pin GPIO_Pin_0; // LCD复位和背光如果接在PB0/PB1 gpio_init.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOB, gpio_init); }上面出现了两个RCC时钟使能语句——APB2外设时钟负责GPIO和AFIOAHB外设时钟负责FSMC本身。这个顺序不能颠倒。AFIO时钟必须打开虽然FSMC不算标准的复用功能但NE片选信号和部分地址线的重映射逻辑涉及AFIO的配置。还有一点容易踩坑如果板子上PD2被其他外设占用就得确认FSMC_A3走的是PD2还是其它引脚不同封装和不同板卡设计会不一样。3.2 LCD初始化序列中命令参数的时序依赖LCD控制器上电后必须执行一段初始化序列设置像素格式、显示方向、伽马曲线和背光控制等寄存器。这一段代码看起来是在“写寄存器”但其正确性完全依赖于FSMC总线时序是否满足控制器的要求。若总线时序过快LCD控制器可能采样到错误的数据现象是初始化后白屏或者颜色错乱。以ILI9341为例RGB565格式、扫描方向从左到右从上到下的典型初始化开头如下static void LCD_InitSequence(void) { LCD_WriteCmd(0x01); // 软件复位 delay_ms(120); LCD_WriteCmd(0x11); // 退出睡眠模式 delay_ms(120); LCD_WriteCmd(0x36); // 内存访问控制 LCD_WriteData(0x48); // BGR顺序扫描方向由上到下 LCD_WriteCmd(0x3A); // 像素格式 LCD_WriteData(0x05); // 16位色 LCD_WriteCmd(0x29); // 开启显示 LCD_WriteCmd(0x2C); // 开始写显存 }0x36寄存器的值是整个驱动里最需要关注的地方。0x48二进制是01001000其中第6位和第3位分别控制RGB/BGR顺序和行扫描方向。如果屏幕出现颜色反相就把第6位取反如果图像上下颠倒或左右镜像就要调整第7位和第4位。后面会专门细说方向与坐标的配合问题。这段代码放在FSMC初始化完成后执行复位引脚需要先拉低再拉高——通常拉低至少10毫秒再释放。注意复位后要延时120毫秒再发命令这是ILI9341数据手册明确规定的上电时序要求不少白屏问题就出在这个延时上。仔细检查LCD模组厂家给的初始化序列很多屏厂会给出对应的初始化数组配合FSMC的写命令和写数据函数直接填充即可。3.3 画点与区域填充的显存窗口机制TFTLCD显示图像的核心是“显存窗口”机制。任何绘图操作前需要先通过命令0x2A设置列地址范围通过命令0x2B设置行地址范围然后写0x2C开启数据写入。这两个命令让LCD控制器知道后续进入的像素数据应该落在屏幕的哪个矩形区域内驱动代码中对应的是一个四个形参的接口void LCD_SetWindow(uint16_t xStart, uint16_t yStart, uint16_t xEnd, uint16_t yEnd) { LCD_WriteCmd(0x2A); LCD_WriteData(xStart 8); LCD_WriteData(xStart 0xFF); LCD_WriteData(xEnd 8); LCD_WriteData(xEnd 0xFF); LCD_WriteCmd(0x2B); LCD_WriteData(yStart 8); LCD_WriteData(yStart 0xFF); LCD_WriteData(yEnd 8); LCD_WriteData(yEnd 0xFF); LCD_WriteCmd(0x2C); } void LCD_Fill(uint16_t xStart, uint16_t yStart, uint16_t xEnd, uint16_t yEnd, uint16_t color) { uint32_t pixelCount (xEnd - xStart 1) * (yEnd - yStart 1); uint32_t i; LCD_SetWindow(xStart, yStart, xEnd, yEnd); for (i 0; i pixelCount; i) { LCD_WriteData(color); } LCD_SetWindow(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1); }画点的函数也是这样先设置窗口设置一个像素的矩形区域然后写一个像素数据最后把窗口恢复为全屏。如果每次都恢复窗口会造成额外开销所以代码里把恢复窗口放在批量操作之后单个画点调用时也可以不恢复——只要接下来其他绘图函数会重新设置窗口就行。这里有一个容易被忽略的性能点循环连续写数据时FSMC总线上的地址保持不变数据总线连续传输这部分性能只受FSMC写入周期的限制不受CPU指令周期的限制因为FSMC的写缓冲可以连续接受写入请求。4. 驱动代码中FSMC时序参数的调节方法与实践4.1 读时序和写时序分离配置的必要性很多LCD控制器的数据手册中读周期和写周期的参数是分开给出的。以ILI9341为例读周期的典型值是180纳秒写周期典型值是60纳秒。如果使用同一组时序参数必须取更慢的读周期这会拖慢写性能。FSMC恰好支持独立配置——结构体FSMC_NORSRAMTimingInitTypeDef可以分别指定读时序和写时序结构。有人误以为FSMC_AddressSetupTime与FSMC_DataSetupTime的值越大越稳定事实上它们过大会导致LCD控制器采样数据时错过了有效窗口。FSMC在地址建立阶段输出的地址信号和片选信号有效时LCD控制器的RS引脚电平必须在WR信号下降沿之前稳定下来。从实践来看2到15之间的AddressSetupTime和5到30之间的DataSetupTime覆盖了绝大多数LCD控制器的需求。如果屏出现竖条纹或者显示内容随机错乱先把两个时序参数都设成最大值——0x0F和0x3F——确认能够点亮后再逐步减小直到找到保证稳定运行的最小值。4.2 用逻辑分析仪校准FSMC总线时序的具体步骤将示波器或逻辑分析仪的通道接到FSMC_NE4、FSMC_WR、FSMC_A6和FSMC_D0到D3上触发条件设为NE4下降沿。运行LCD_WriteCmd和LCD_WriteData各一次捕获波形后检查三个时间点参数测量位置参考值地址建立时间RS电平变化到WR下降沿大于LCD最小要求数据建立时间D0-D15数据有效到WR上升沿大于LCD最小要求写周期两次WR下降沿之间的间隔大于LCD最小写周期若数据建立时间不足首先增大FSMC_DataSetupTime而不是加大HCLK频率。STM32F103的HCLK可达72MHz一个HCLK约13.9纳秒。有些LCD控制器要求数据保持时间这时候把FSMC_AddressHoldTime从0改到1或2就能延长WR上升沿后的保持窗口。4.3 关于FSMC_AccessMode四种模式的选择边界FSMC的NOR Flash控制器支持四种访问模式——模式A、B、C、D。模式A提供独立的地址建立和地址保持时间模式B在模式A基础上增加了总线周转周期模式C取消了地址保持时间模式D类似SRAM的OE持续周期。驱动TFTLCD时大多数代码选模式B或模式A。模式B更适合读写不对称的场景因为FSMC_DataSetupTime在模式B下对读和写是分别计数的模式A则强制读写使用同一套数值。如果在模式A下读正常但写异常把模式改成B再分别调整时序是一个常见的调试路径。特别注意等待信号FSMC_WaitSignalActive在模式B下可以配置为BeforeWaitState或DuringWaitStateLCD如果不接等待信号引脚直接保持默认值。用一个简单的性能测试函数来检验时序是否过慢连续往整个屏幕填充单色用定时器计时。计时结果对比修改FSMC_DataSetupTime前后的差异能直观看出优化的空间。注意填充操作受限于LCD控制器本身的像素时钟FSMC写得再快LCD控制器也来不及接收——此时瓶颈在屏端FSMC时序参数再调小也不会提速。5. LCD初始化后图像颠倒、偏色的工程修正5.1 扫描方向寄存器对坐标映射的影响屏幕装进外壳之后发现图像是倒的这个问题几乎人人都遇到过。除了旋转机身修改LCD控制器的扫描方向是最常见的做法。以ILI9341的0x36寄存器为例其第7位和第6位控制行和列的扫描方向第6位同时影响RGB/BGR。修改前先记录当前屏的安装角度再对照方向表计算新的寄存器值。比如当前代码初始化里写0x48表示从左到右从上到下扫描BGR顺序。屏幕旋转90度后水平和垂直方向对调需要设置0x28或0x88具体值取决于旋转方向。碰到镜像显示——文字左右相反——则调整第4位。调完0x36后一定要同步修改坐标系的宽高常量否则后续的触摸校准和绘图抠图会全部错位。5.2 颜色反相问题的两种根源识别颜色反相分两种情况红蓝对调和全色反相。红蓝对调是0x36寄存器第6位控制RGB与BGR顺序翻转这1比特就能纠正。全色反相则往往出在初始化序列的像素格式命令0x3A上——如果LCD控制器工作在18位色模式而驱动按16位色发送数据每个像素的低两位会被丢弃或错位。检查0x3A的值是否为0x0516位色如果是0x06就处于18位模式。还有一类隐蔽的颜色异常源于FSMC写命令函数的返回值类型。LCD_WriteData的参数类型必须是无符号16位如果误传了8位数据色彩会整体偏色且不均匀。在代码里强制类型转换或者把函数声明放在编译器能检查到的地方是排查这类问题最节省时间的做法。5.3 分层驱动代码中调试宏的应用很长一段时间里我调试LCD驱动时会加一套调试宏方便随时切换读回校验和串口打印#define LCD_DEBUG_ENABLE #ifdef LCD_DEBUG_ENABLE #define LCD_DEBUG(fmt, args...) printf([LCD] fmt \r\n, ##args) #else #define LCD_DEBUG(fmt, args...) #endif uint16_t LCD_ReadID(void) { uint16_t id; LCD_WriteCmd(0xD3); // ILI9341读ID命令 LCD_ReadData(); LCD_ReadData(); id LCD_ReadData(); LCD_DEBUG(LCD ID 0x%04X, id); return id; }ILI9341读ID的流程是发命令0xD3之后先空读两个字节再读到的才是真正ID。不同控制器命令不同——ILI9488读ID用的命令是0xD3ST7789V是0x04。如果读回ID异常但屏幕能显示说明FSMC时序基本可用但可能有边缘问题如果ID读回0x0000或0xFFFF优先检查连线、复位时序和FSMC配置。5.4 刷屏速度提升方向不要只盯着FSMC参数FSMC的写入时序调到极限之后刷屏速度还能靠减少无效操作继续提升。例如清屏函数中不需要恢复全屏窗口——接下来会被SetWindow覆盖。再比如连续填充大片相同颜色时可以用while循环代替for循环编译器生成的指令更精简。还有SPI方式驱动LCD的板子换到FSMC之后原来在SPI时代为了省指令而做的各种小技巧可以直接删除FSMC写几个像素的开销已经接近于零反而是函数调用开销占了大头。把热点函数加上__inline修饰或者直接在头文件里定义成static inline可以让FSMC总线充分利用——连续写数据时避免在调用和返回之间插入无用的指令周期。不过要留意编译器的优化级别Keil的-O0下inline的效果非常有限至少开到-O2才看得出明显差异。帧率的另一个瓶颈是LCD控制器内部的行填充时间。写一行像素到屏端之后控制器需要一段时间把这些数据锁存到面板上这个时间通常在几十微秒到几百微秒之间。如果代码连续写入大量数据而不做任何延时屏端可能会丢掉部分行数据表现为横向随机出现亮线。遇到这种情况检查代码是否在写完一行之后加了适当延时或者在行结束和下一行开始之间插入了DCB数据继续命令。手头有逻辑分析仪时抓取D0到D15的活动情况就能直接判断是否是行锁存导致的花屏。本文还有配套的精品资源点击获取
返回列表