拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32驱动5寸TFT屏:FSMC配置、DMA刷新与GT911触摸校准全解析

简介针对STM32驱动5寸液晶屏的嵌入式开发需求这份C语言源文件提供了完整可参考的驱动实现面向使用HAL或LL库的开发者解决LCD接口初始化、时序参数配置、像素数据写入及触摸事件处理等关键问题。资源共1个文件为单个.c源文件压缩包体积仅4KB代码精简聚焦便于快速阅读和移植到800×480分辨率、16位色深的液晶屏项目中。目前已有502人学习下载适合正在调试同类型屏幕或希望理解STM32与液晶屏底层通信逻辑的工程师。源码中可看到初始化函数、数据发送与触摸处理等模块结合描述中的六步驱动流程能帮助读者理清GPIO配置、DMA传输和图形库调用之间的关系节省排查硬件时序和初始化序列的时间为后续扩展图形界面打下基础。1. 碰到5寸TFT白屏先别量线查FSMC配置第一次驱动5寸TFT液晶屏的人遇到白屏时的第一反应通常是查接线但实际出问题的地方往往在GPIO复用配置和初始化序列顺序上。一块800×480、16位色深的屏一帧裸数据就是800×480×2768,000字节约750KBSTM32内部SRAM根本放不下整帧所以从第一批代码开始就得把DMA和显存方案想清楚否则画面刷新时会出现撕裂和闪烁。接下来把FSMC/LTDC接口选型、初始化序列、RGB565排布、触摸坐标校准和双缓冲整条链路串起来适合正在用STM32F407、F429或类似带FSMC外设的型号驱动5寸TFT的人参考。2. 800x480这种5寸屏先确定接口再决定用FSMC还是LTDC2.1 MCU接口屏和RGB接口屏驱动逻辑完全不同5寸屏并不是只有一种接口。带液晶控制器的模组内部有GRAM只要用8080并口往GRAM里写像素屏自己会刷新这种屏用STM32的FSMC外设去模拟时序F103和F407都能跑。另一种是RGB接口裸屏没有控制器需要STM32的LTDC外设按固定帧率把整帧数据不断推向屏通常要F429或F746并外挂SDRAM做帧缓冲。同样是800x480的5寸模组两种版本并存所以拿到项目先看原理图数排线信号就能区分MCU接口有D0~D15、WR、RD、RS、CSRGB接口则是DE、HSYNC、VSYNC、DCLK加数据线。判断错了后面所有代码都是白写。F407没有LTDC如果板上是RGB裸屏带不动只能换带控制器的模组或换MCU反过来F429有LTDC但驱动MCU接口屏时一样用FSMC互不冲突。2.2 从模组数据手册倒推FSMC时序参数MCU接口屏的数据手册里都有一份8080时序表FSMC里配的寄存器值要按HCLK周期倒推。HCLK为168MHz时一个周期约5.95nS用需求值除以周期向上取整得到AddressSetupTime和DataSetupTime。下面是一份常见5寸模组的时序基准不同主控差别不大参数含义典型值PWR完整写周期时间320nS地址建立时间CS/RS有效到WR上升沿50nS数据建立时间数据有效到WR上升沿80nS数据保持时间WR结束后数据保持15nSPFRD完整读周期时间320nS模板工程里常见的FSMC配置48基本能点亮多数屏但碰到对时序敏感的模组配置太快会出现花屏或偶发错色。我一般会先按410跑起来确认稳定后再把DataSetupTime逐档往下降每次降2个周期并长时间跑刷屏测试降到花屏就退回上一档。边界值附近必须留余量尤其是高温环境下时序会劣化余量太少现场会随机出问题。2.2.1 余量按一个HCLK周期取整320nS折算下来是54个HCLK周期但我通常不会把这个数值直接搬进FSMC配置因为FSMC内部还有地址建立和数据建立的固定开销实际总线周期比AddressSetupTimeDataSetupTime要大。用手册给的最小值减去固定开销后再折算才是有效做法。留10%15%的余量能省下很多现场排查时间。2.3 用HAL库把FSMC配置填进去CubeMX里先把FSMC外设打开模式选NOR/PSRAM数据宽度选16bit然后代码里按之前算好的周期配置结构体FSMC_NORSRAM_TimingTypeDef fsmc_timing {0}; fsmc_timing.AddressSetupTime 4; // 地址建立时间约23.8nS fsmc_timing.DataSetupTime 10; // 数据建立时间约59.5nS fsmc_timing.AccessMode FSMC_ACCESS_MODE_B; FSMC_NORSRAM_InitTypeDef fsmc_control {0}; fsmc_control.FSMC_Bank FSMC_Bank1_NORSRAM1; fsmc_control.FSMC_MemoryType FSMC_MEMORY_TYPE_SRAM; fsmc_control.FSMC_MemoryDataWidth FSMC_NORSRAM_MEM_BUS_WIDTH_16; fsmc_control.FSMC_WriteOperation FSMC_WriteOperation_Enable; fsmc_control.FSMC_ExtendedMode FSMC_ExtendedMode_Disable; HAL_SRAM_Init(hsram, fsmc_control, fsmc_timing);AccessMode FSMC_ACCESS_MODE_B对应的是SRAM/PSRAM的时序模型8080并口屏通常按这个模式走。FSMC_Bank1_NORSRAM1对应NE1片选地址从0x60000000开始。没有使能扩展模式意味着读和写共用一套时序普通应用够用如果模组读时序特别慢也可以打开扩展模式单独给读操作配一套更宽的时序。注意CubeMX自动生成的GPIO初始化已经处理了引脚复用但屏的RS信号接在哪条地址线上仍然由板子决定软件里要根据实际接线换算地址。2.4 RS引脚挂在A6上命令和数据就有了地址差MCU接口屏的RS也叫D/CX区分命令和数据。RS接在FSMC的A6上时命令口地址是0x60000000数据口地址是0x60000040因为A6对应位权是0x40。C代码里最常用的是这两个宏#define LCD_REG_ADDR (*(__IO uint16_t *)0x60000000) #define LCD_DATA_ADDR (*(__IO uint16_t *)0x60000040)这样写的原因是FSMC访问地址时会按地址线输出对应的电平组合CS、WR、RD这些控制信号都由FSMC硬件完成不需要用GPIO手动翻转。(__IO uint16_t *)告诉编译器这是16位宽度映射FSMC按16bit总线往外部发数据。如果板子上RS接的是A16数据口地址就变成0x60020000差值是0x20000这个值一定要和原理图对起来。8bit并口屏的地址差算法也会不同RS挂A6时数据口偏移同样是0x40但寄存器访问宽度要改成8bit。3. 拆开STM32驱动液晶屏.c初始化序列、RGB565与DMA3.1 初始化序列用结构体数组管理液晶屏控制器上电后默认状态不是直接可显示需要发一串命令配置显示方向、像素格式、伽马和背光。几十条命令散放在main里很难维护调屏时要来回改我用结构体数组保存一条命令一个条目typedef struct { uint16_t cmd; uint8_t params[8]; uint8_t len; } lcd_seq_t; static const lcd_seq_t lcd_init_list[] { {0x01, {0x00}, 0}, // 软件复位 {0x11, {0x00}, 0}, // 退出睡眠模式 {0x36, {0x60}, 1}, // MADCTL扫描方向按模组接线调 {0x3A, {0x55}, 1}, // 像素格式0x55表示16位RGB565 {0x29, {0x00}, 0}, // 打开显示 }; void lcd_send_init_sequence(void) { for (uint32_t i 0; i sizeof(lcd_init_list) / sizeof(lcd_init_list[0]); i) { lcd_write_cmd(lcd_init_list[i].cmd); for (uint8_t j 0; j lcd_init_list[i].len; j) { lcd_write_data(lcd_init_list[i].params[j]); } } }len为0表示该命令不带参数比如0x01、0x11、0x29都是无参命令0x36和0x3A各带一个参数。const关键字一定不要省这样数组放在Flash里不占SRAM。STM32F407内部SRAM一共才128KB整帧750KB本来就放不下SRAM留给行缓冲和DMA描述符更划算。还需要注意不同厂商模组即使主控芯片相同伽马设置也会不一样命令集必须按模组手册来不能拿其它屏的初始化代码直接套。3.2 命令和数据通过RS地址口分开前面地址差为0x40的设计到写入环节才真正发挥作用。写命令时把数据送到LCD_REG_ADDR写数据时送到LCD_DATA_ADDRFSMC自动驱动RS脚屏控制器才知道总线上内容应该当命令还是参数static void lcd_write_cmd(uint16_t cmd) { LCD_REG_ADDR cmd; } static void lcd_write_data(uint16_t data) { LCD_DATA_ADDR data; }这两个函数写法几乎一样区别只在目标地址。新手最容易犯的错误是两个都发到同一个地址结果屏收到的是连续的「命令命令命令」显示完全错乱。调试这种问题可以抓RS和WR波形确认RS在WR有效期间是否确实在命令和数据间切换。3.3 RGB565在800x480上的排布16位色深在这块屏上是RGB565高5位红、中间6位绿、低5位蓝24位彩色转成16位时按位截取#define RGB565(r, g, b) \ (((r 0xF8) 8) | ((g 0xFC) 3) | (b 3))r、g、b三通道各8位转换时红和蓝丢掉低3位绿丢掉低2位肉眼很难看出差别。像素写进GRAM前要先设窗口否则会顺着上次写入命令的地址继续排出现错位static void lcd_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { lcd_write_cmd(0x2A); lcd_write_data(x0 8); lcd_write_data(x0 0xFF); lcd_write_data(x1 8); lcd_write_data(x1 0xFF); lcd_write_cmd(0x2B); lcd_write_data(y0 8); lcd_write_data(y0 0xFF); lcd_write_data(y1 8); lcd_write_data(y1 0xFF); lcd_write_cmd(0x2C); }0x2A是列地址0x2B是行地址0x2C表示开始写显存。坐标参数要拆成高字节和低字节分两次发因为这条屏的命令参数是8bit宽度。窗口设好后连续写入的数据按行填充。这块屏一行800个像素就是1600字节480行合计768,000字节区域像素数占用字节整屏 800x480384,000768,000一行扫描线8001,6008行行缓冲6,40012,800这也是为什么不能像小屏那样开768KB数组当帧缓冲只能按行或按小块区域搬运。3.4 用DMA搬像素CPU只负责切行带控制器的MCU接口屏一次搬一行最稳妥。先把一行像素准备好让DMA把这1600字节送到LCD_DATA_ADDRCPU可以趁机处理下一行数据void lcd_dma_send(uint16_t *buf, uint16_t len) { while (__HAL_DMA_GET_FLAG(hdma, DMA_FLAG_TC5) RESET) ; __HAL_DMA_CLEAR_FLAG(hdma, DMA_FLAG_TC5); HAL_DMA_Start_IT(hdma, (uint32_t)buf, (uint32_t)LCD_DATA_ADDR, len); }LCD_DATA_ADDR就是对FSMC数据端口地址取地址展开后就是0x60000040DMA把像素按16bit宽度写到这个地址CPU全程不参与传输。len在DMA配置为半字宽度时表示像素个数不是字节数写错头像会变成一行拉长或缩短。DMA外设宽度和内存宽度必须都配成16bit如果某个通道配成8bitFSMC侧会看到两个连续8bit访问性能直接掉一半。while等待上一次传输结束再启动下一次是为了同一个DMA通道不会同时跑两段传输。3.4.1 DMA完成中断里不要做重活行传输结束的中断里只适合交换缓冲指针和置一个标志位不要在中断里调用lcd_set_window或画像素函数。FSMC写寄存器有等待周期在中断里做这些会把中断服务时间拉长反而把DMA的并行优势抵消掉。常见做法是置位标志主循环看到标志后设置下一行窗口并启动下一次DMA。对于RGB接口裸屏软件DMA这套思路就不适用了。LTDC内部有专门的DMA会不断从SDRAM帧缓冲读数据再推给屏软件只需要把帧缓冲首地址写进LTDC图层控制寄存器剩下的刷新是硬件自动完成。所以同样是DMAMCU接口屏和RGB屏是完全两个概念。4. STM32触摸驱动GT911的I2C读时序和坐标校准4.1 先读设备ID确认I2C地址和通信链路5寸电容触摸模组里最常见的控制器是GT911走I2C。GT911的7位地址由引脚电平决定常见0x5D和0x29两组。上手第一件事不是读坐标而是读设备ID把链路先确认下来#define GT911_ADDR (0x5D 1) static void gt911_read(uint16_t reg, uint8_t *buf, uint8_t len) { HAL_I2C_Mem_Read(hi2c1, GT911_ADDR, reg, I2C_MEMADD_SIZE_16BIT, buf, len, 100); } static void gt911_write(uint16_t reg, uint8_t data) { HAL_I2C_Mem_Write(hi2c1, GT911_ADDR, reg, I2C_MEMADD_SIZE_16BIT, data, 1, 100); } uint8_t gt911_check(void) { uint8_t id[3] {0}; gt911_read(0x8140, id, 3); return (id[0] 0x39) (id[1] 0x31) (id[2] 0x31); }GT911内部寄存器是16位地址所以I2C_MEMADD_SIZE_16BIT必须带上0x5D 1是把7位地址左移成8位地址再交给HAL。ID读回0x39 0x31 0x31基本能确认器件在线。读不到时优先查中断和复位引脚的时序GT911要求RST拉低后延时10ms以上再拉高INT引脚在复位期间要保持特定电平顺序反了设备根本起不来。4.2 坐标读取要分两个寄存器事务GT911把触点信息放在0x814E开始的缓冲区。常规顺序是先读0x814E一个字节判断最高位是否为1有新触点再读坐标缓冲读完把0x814E写0清除标志uint8_t status 0; gt911_read(0x814E, status, 1); if (status 0x80) { uint8_t touch[7]; gt911_read(0x8150, touch, 7); uint16_t raw_x (touch[1] 8) | touch[2]; uint16_t raw_y (touch[3] 8) | touch[4]; } gt911_write(0x814E, 0);status 0x80是buffer status标志不判断直接读0x8150会一直读到上一次的旧坐标。坐标数据在寄存器里是高字节在前(touch[1] 8) | touch[2]就是拼回整数如果某个固件高低字节顺序相反触摸轨迹会沿对角线跳动对调一下即可。写完0x814E清标志后GT911才会产生下一次中断并更新坐标缓冲。4.2.1 多点触摸的数据布局status低4位是本次触点个数每个触点占7字节连续排在0x8150之后。多个点时不需要把缓冲区全部读回来按for (i 0; i points; i)从0x8150 i * 7处循环读取即可。单点应用只看第一个触点的7字节就够如果要实现双指缩放还是要按这个布局把每个点的坐标都解析出来。4.3 触摸坐标到屏幕坐标的线性校准触摸感应区的RAW范围通常和显示分辨率不一致比如这块5寸模组的触摸区可能是1024x768甚至更高直接拿RAW坐标当显示坐标肯定对不上。标准做法是左上角和右下角各采一次RAW值换算比例和偏移采样点raw_xraw_y目标显示坐标左上角10587(0, 0)右下角920590(799, 479)x方向scale是800 / (920 - 105)y方向scale是479 / (590 - 87)offset分别为105和87。运行时按下面公式换算typedef struct { int16_t offset_x; int16_t offset_y; float scale_x; float scale_y; } touch_cal_t; uint16_t calibrate_x(touch_cal_t *cal, uint16_t raw_x) { return (uint16_t)(((int32_t)raw_x - cal-offset_x) * cal-scale_x); }中间用int32_t是因为当raw_x小于offset_x时差值是负数uint16_t直接相减会溢出成很大的正数。scale用float保留小数左下角或右上角做第二采样点还能顺便判断是否需要X方向翻转。校准参数实测一次后可以存在片内Flash里下次开机直接读不用每次校准。5. STM32显示优化双缓冲、背光PWM与白屏定位手段5.1 行缓冲双份画图和DMA搬运重叠750KB整帧放不进内部SRAM行缓冲可以。准备两份800像素的缓冲区一份给DMA搬一份给CPU画DMA完成中断里互换角色static uint16_t line_buffer[2][800]; static uint8_t active_buf; void lcd_dma_irq_callback(void) { active_buf ^ 1; } void lcd_draw_line(uint16_t y) { fill_line(line_buffer[active_buf], y); lcd_set_window(0, y, 799, y); lcd_dma_send(line_buffer[active_buf], 800); }fill_line画到的是上一次DMA已经搬完、当前空闲的那块和DMA正在读的缓冲区分开。如果只有一块行缓冲每行都要等DMA跑完才能画下一行帧率直接减半。屏带TE脚的话在TE中断里启动DMA还能避免整幅撕裂。5.2 背光亮度用gamma曲线映射PWM背光频率放在1kHz到10kHz之间太低闪烁太高增加开关损耗。占空比不要线性调人眼对低亮度更敏感static uint16_t level_to_ccr(uint8_t level) { if (level 0) return 0; if (level 100) level 100; return (uint16_t)(1000.0f * powf(level / 100.0f, 2.2f)); }2.2的gamma指数补偿人眼感知返回值0到1000对应PWM比较值记得引入math.h。固定背光没有调光脚的模组这段不适用。5.3 白屏时先读一次控制器ID初始化命令发完还白屏先读屏控制器ID判断数据通路通不通uint16_t lcd_read_id(void) { lcd_write_cmd(0x04); return lcd_read_data(); }读到非0xFFFF、非0的值说明FSMC地址映射和命令写入正常问题多半在初始化序列。一直读回0xFFFF优先查RS地址差是否和板子一致、RD信号有没有被FSMC复用。少数模组只写不读读回全1也正常这时写一屏纯色看颜色有没有变化即可。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门