STM32F103C8T6驱动ILI9341 TFT LCD完整教程与调试指南
简介面向STM32入门者与嵌入式开发者的ILI9341液晶显示工程基于STM32F103C8T6最小系统板和2.8寸TFT LCD模块重点演示如何在无FSMC的48脚芯片上用普通GPIO模拟总线实现16位并口TFT驱动可直接迁移到类似MCU平台也常见于高校课程设计与毕业设计等场景。压缩包共182个文件总大小3.16MB内容覆盖C/H源码、Keil工程配置、链接脚本、编译后的hex/axf/map等输出在Keil中打开即可查看或烧录验证。已有5078人学习/下载尤其适合正在学习STM32裸机驱动、LCD接口移植以及并口时序的开发者参考。资源按模块化硬件电路搭建代码中包含LCD驱动与标准外设库的定时器、Flash、ADC等模块有助于理解显示初始化流程和底层寄存器操作也便于二次开发需要注意的是触屏输入功能暂未实现当前使用场景以显示屏驱动为主。整体结构清晰明了适合逐步阅读和功能裁剪。 手头这块板子是我入门STM32用的C8T6最小系统板配上2.8寸TFT LCD很经典的一套显控组合。STM32F103C8T6是64引脚、Cortex-M3内核、主频72MHz、64KB Flash、20KB RAM虽然不算大但跑一个小型GUI、做波形显示或者菜单交互完全够用。屏的驱动芯片是ILI9341分辨率240x320颜色深度最高支持16位RGB565刷新流畅度和色彩表现对这个尺寸来说足够。这套组合适合谁如果你刚学完点亮LED想搞一个“能看又能点的屏幕”或者想做一个天气站、温湿度计、示波器、平衡小车界面再或者准备用带触摸的版本做电子相册、游戏机这条路都能打通。因为C8T6的资料最多ILI9341的参考代码也最多两者放在一起遇到问题基本都能在网上找到答案不会卡死你。下面我把这次从接线、CubeMX配置到代码移植的完整过程整理出来顺便把调试时踩过的坑都标出来。1. 项目思路与选型分析1.1 为什么是STM32F103C8T6 ILI9341这个组合C8T6虽然属于F103系列里的“小容量”型号但外设非常齐全3个USART、2个SPI、2个I2C、1个USB、多个定时器和ADC对于驱动一块屏来说绰绰有余。64KB的Flash如果只是放显示驱动、字库和几个界面空间非常宽裕20KB的RAM跑不带操作系统的小工程也够用。更关键的是这块板的国产替代方案非常多价格便宜坏了也不心疼特别适合做实验。ILI9341则是当前小尺寸TFT屏里最常见的一款驱动芯片几乎所有2.8寸、2.4寸的SPI屏都基于它。它内部自带GRAM显存MCU只需要通过SPI接口把像素数据送进去屏就会自动刷新显示。这样主控不需要频繁刷新整屏能省出大量CPU时间去处理其他逻辑。所以这一对组合是“低成本、低引脚占用、资料海量”的典型代表作为LCD入门项目很合适。1.2 SPI接口和并行接口的取舍2.8寸的ILI9341模组通常不止一种接口常见的有4线SPI、8位并行、16位并行。SPI版只需要CS、SCK、MOSI、DC、RST、BL这6根线再加上电源和地C8T6引脚的余量就很宽裕并行版速度最快一次能刷整行像素但需要至少十几根数据线和好几根控制线C8T6这种小封装引脚马上就紧张了而且C8T6内部没有FSMC控制器FSMC是103系列高容量型号才带的外设做不了“并口屏FSMC无缝映射”那种硬方案所以小板上最合理的还是SPI。顺便说个经验同样是2.8寸有些模组上默认是16位并口需要你手动改电阻或飞线才能切到SPI模式买之前一定看清店铺标注或者模块丝印别等焊完才发现引脚不对。如果你手头只有并口屏又想用C8T6驱动也不是不行用GPIO模拟并口时序也能点亮但代码量和调试难度都会上一个台阶新手不建议一上来就这么玩。1.3 硬件SPI比软件SPI强在哪这次我选择的是硬件SPI也就是直接把SCK、MOSI接到STM32的SPI1引脚上PA5、PA7数据都由外设自己移位发出CPU只需要往数据寄存器里写一个字节。相比软件模拟SPI好处有三个一是速度快SPI1最大可以跑到18MHz软模拟一般也就2~4MHz二是时序稳定不像GPIO翻转那样受中断和分支指令影响三是后续可以用DMA刷大图时CPU可以腾出来干别的。当然软件SPI也不是没用当你接到非复用引脚、或者屏和主控不在同一块板上需要自定义接线软SPI更灵活。实际项目里你可以把底层函数抽象成同一个接口先用软SPI调通逻辑再切到硬件SPI提速度排查问题时这种切换能力特别有用。尤其是遇到“硬件SPI点不亮但软件SPI能亮”的怪问题时基本就能确定是引脚复用或模式配置的问题。2. 硬件准备与接线2.1 需要的材料清单STM32F103C8T6最小系统板蓝色板、红板都行2.8寸ILI9341 SPI TFT LCD模组建议带电平转换的版本或确认逻辑电压是3.3VST-Link或USB转串口CH340、CP2102下载器杜邦线若干能短则短逻辑分析仪调试时可选项建议备一个下载器多说一句如果是从串口下载C8T6板载的CH340或CP2102驱动装不上是个高频问题。Win10以上一般免驱老系统记得去芯片厂商官网下载对应驱动不要乱装那种“万能驱动”。用ST-Link的话驱动同样去官网装装好之后在Keil里选择ST-Link下载器即可速度比串口快不少。2.2 接线表与引脚分配我在CubeMX里把SPI1配置为主模式只发送不接收所以MISO引脚可以不接。控制引脚分配是这样LCD引脚功能STM32引脚VCC电源正3.3VGND电源地GNDCS片选PA4RST复位PA1DC/RS数据/命令选择PA2MOSI/SDISPI数据输入PA7SCKSPI时钟PA5BL/LED背光PA3软件控制SDO/MISO数据输出不接注意PA5、PA7是SPI1的默认复用引脚PA4是SPI1的NSS引脚但因为我是用GPIO软件控制片选PA4必须配置成普通推挽输出不能复用成NSS自动模式否则CS可能会被外设自动拉来拉去导致收发数据错乱。PA6是SPI1的MISO引脚我这边没有接屏的SDO所以PA6可以留作他用如果后续想读屏的ID或者做回读校验再把PA6复用成MISO接过去。RST接法有讲究。很多模组把RST拉高就能用但ST官方和屏厂建议上电后对RST做一个低脉冲复位时间至少20us然后拉高。如果你板上RC复位电路影响时序最稳的是用一个GPIO控制RST不要直接并接在主控的NRST脚上否则屏可能会在MCU复位后仍处于复位状态等MCU开始初始化了屏还没醒过来。2.3 CubeMX配置要点时钟树这里提一嘴STM32F103C8T6最高主频72MHz系统时钟建议配到72MHz否则SPI时钟分频之后速率会偏低。SPI1挂载在APB2总线上APB2默认是72MHz。SPI1的波特率预分频有2、4、8、16、32、64、128、256分频可选。我用的8分频实际SPI时钟是9MHz。为什么不用4分频的18MHz理论上SPI1可以跑18MHz但那是在PCB走线非常短、器件都在同一块板上的理想情况。用杜邦线连接的时候18MHz下波形边沿变差读取回来容易出花屏9MHz既快又稳是2.8寸屏的开销和稳定性的平衡点。SPI配置参数ModeTransmit Only Master只发送主模式Clock Polarity (CPOL)LowClock Phase (CPHA)1 Edge即SPI模式0第一个边沿采样NSSDisable软件管理帧格式8bitMSB First关于模式0和模式3ILI9341其实两种都支持。很多现成代码里用的是模式0但如果你拿到的屏花屏或第一行错位可以试着把SPI模式改成模式3CPOLHighCPHA2 Edge不少人都在这上面栽过代码逻辑没毛病就是模式选错了。这个切换不涉及驱动代码改动改CubeMX重新生成或者直接改寄存器就行。3. 驱动代码核心实现3.1 底层命令/数据读写函数先写底层。LCD作为外设最关键的是区分“命令”和“数据”这个由DC引脚决定。DC拉低时SPI总线上的一个字节被屏当成命令DC拉高时被当成数据。两者间的时序关系用代码表达就是#define LCD_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define LCD_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) #define LCD_DC_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET) #define LCD_DC_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET) void LCD_WriteCmd(uint8_t cmd) { LCD_CS_LOW(); LCD_DC_LOW(); HAL_SPI_Transmit(hspi1, cmd, 1, 10); LCD_CS_HIGH(); } void LCD_WriteData(uint8_t data) { LCD_CS_LOW(); LCD_DC_HIGH(); HAL_SPI_Transmit(hspi1, data, 1, 10); LCD_CS_HIGH(); }这里有个细节有些驱动代码会在拉高CS之前把DC也恢复成默认这个问题不大但建议每次都按“CS拉低 - DC切到对应电平 - 发送 - CS拉高”这个顺序做不要反过来否则第一个字节容易因为建立时间不够而丢失。HAL_SPI_Transmit的最后一个参数是超时时间我写的10ms正常情况下单字节发送是微秒级但如果SPI没有正确初始化这个函数会一直等待直到超时可以作为简单的故障判断。3.2 初始化序列逐条拆解ILI9341上电后不是直接就能用的它内部有一个复杂的寄存器组需要通过初始化命令设置好电源、时序、显示方向和颜色格式。以下是我整理的完整初始化序列static const uint16_t lcd_init_seq[] { 0x01, /* 软件复位 */ 0x11, /* 退出睡眠模式 */ 0x36, 0xC8, /* 内存访问控制BGR、行列扫描方向 */ 0x3A, 0x55, /* 像素格式16位 RGB565 */ 0xB0, 0x00, 0xB1, 0x00, 0xB6, 0x0A, 0x82, 0x27, 0xC0, 0x23, 0xC1, 0x10, 0xC5, 0x3E, 0x28, 0xC7, 0x86, 0xE0, 0x00, 0x0D, 0x18, 0x0E, 0x0A, 0x09, 0x26, 0x43, 0x53, 0x0F, 0x13, 0x0F, 0x0D, 0x2B, 0x33, 0xE1, 0x00, 0x0D, 0x19, 0x0C, 0x09, 0x09, 0x26, 0x42, 0x52, 0x0F, 0x12, 0x0E, 0x0D, 0x2A, 0x33, 0x29, /* 开启显示 */ };逐条说一下关键命令0x01是软件复位执行后要延时至少5ms再继续。这步不是必须的但如果你的屏是从别的状态切换过来的最好别省。0x11是退出睡眠。屏幕上电默认处于睡眠模式不退出的话就算你写了像素也不显示。这步之后需要延时120ms左右让内部稳压器稳定。0x36是内存访问控制这个寄存器的bit7、bit6、bit5分别是MY、MX、MV控制扫描方向bit3是BGR顺序控制。0xC8对应的二进制是11001000也就是MY和MX都被置位同时BGR为1。如果你发现屏幕上下左右颠倒或者颜色偏红偏蓝改的就是这个寄存器。0x3A设置像素格式0x55表示16位色RGB565也就是每个像素用2个字节表示。如果你用0x6618位或者0x7724位后面写像素数据的格式都要跟着变刷屏速度也会更慢。后面的0xB0、0xB1、0xB6、0xC0、0xC1、0xC5、0xC7是电源和显示时序控制一般照搬参考值就行。E0/E1分别是正负电压的Gamma校正曲线用来调节灰阶过渡不同屏幕模组可能会有差异出现颜色断层时可以微调。最后0x29开启显示前面所有配置才会真正生效。初始化函数里加延时的方式我用的是HAL_Delay注意HAL_Delay依赖SysTick中断如果项目里关了SysTick延时就会失效。还有一种写法是用裸延时循环但建议直接保持SysTick开启。3.3 设置显示窗口与写GRAM设置窗口是ILI9341用起来和串口屏最大的区别。你想往屏上任何区域画东西都要先告诉屏幕“我要写哪个矩形区域”然后连续写入像素数据屏幕会自动按行填充写满一个矩形后再回到起点。void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCmd(0x2A); LCD_WriteData(x0 8); LCD_WriteData(x0 0xFF); LCD_WriteData(x1 8); LCD_WriteData(x1 0xFF); LCD_WriteCmd(0x2B); LCD_WriteData(y0 8); LCD_WriteData(y0 0xFF); LCD_WriteData(y1 8); LCD_WriteData(y1 0xFF); LCD_WriteCmd(0x2C); }这里两个命令分别是列地址设置0x2A和页地址设置0x2B参数都是16位的起始值和结束值。写完这两个命令后连续发过来的像素数据就会写到这个窗口内。画点函数就是设一个1x1的窗口然后写入RGB565数据void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { if (x LCD_WIDTH || y LCD_HEIGHT) return; LCD_SetWindow(x, y, x, y); LCD_WriteData(color 8); LCD_WriteData(color 0xFF); }这里有一个很关键的坑ILI9341的GRAM坐标范围是240x320而2.8寸屏在竖屏模式下显示区域是240宽、320高。如果你的初始化里0x36配置了交换行列MV1那x和y的映射关系就和硬件坐标相反很多人画矩形时边缘总有一条线错位多半是这个原因。我的做法是统一用“应用层坐标”即x范围0~239y范围0~319然后根据0x36的方向设置决定是否需要交换。3.4 矩形填充和字符显示有了窗口和画点函数填充矩形就很自然了void LCD_Fill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint32_t pixels (uint32_t)(x1 - x0 1) * (y1 - y0 1); LCD_SetWindow(x0, y0, x1, y1); for (uint32_t i 0; i pixels; i) { LCD_WriteData(color 8); LCD_WriteData(color 0xFF); } LCD_SetWindow(0, 0, LCD_WIDTH - 1, LCD_HEIGHT - 1); }注意最后把窗口复位到全屏否则后面画其他东西时窗口还停留在上次的位置显示就会错乱这个细节能帮你省掉很多调试时间。字符显示的原理是字模。以8x16字体为例一个字符用16个字节表示每个字节对应8列像素字节的每一位对应一行的黑白。显示的时候先画背景色再根据字模每一位决定用前景色还是背景色。用PCtoLCD2002取模时参数要选“阴码、逐列式、逆向”这样取出来的数组正好是按列从左到右排列的配合画点逻辑最顺手。汉字因为要用UTF-8编码直接放在const数组里没问题但要注意编码格式工程文件里如果是GBK而代码里写UTF-8取模后显示出来的字会乱。3.5 提高刷新速度的几种方式刷屏速度是很多人做完第一版后才开始关心的。2.8寸SPI屏在9MHz下全屏清一次大概需要几十毫秒短时间连续刷新会看到明显的闪烁和撕裂。优化手段按优先级排序连续写GRAM时不要反复拉CS和设置窗口一次窗口写到底。上面的LCD_Fill就是这么做的。用DMA传输。HAL_SPI_Transmit_DMA可以一次把一整个缓冲区的数据发给屏CPU不阻塞。适合做动态波形、视频流这种场景。降低像素格式16位RGB565已经是2字节一个像素了没有更低的常规选择了做界面一般不用调。把数据线改成8位或16位并口。前提是MCU和屏都支持C8T6没有FSMC但GPIO模拟并口也能跑到几兆Hz对于240x320的分辨率提升明显。4. 常见问题与排查技巧4.1 白屏先从硬件和基础时序查起白屏是驱动ILI9341时遇到最多的现象我把它排在第一位。排查顺序如下背光亮不亮。如果背光不亮先看BL引脚电平很多模组是低电平无效我直接用GPIO拉高如果亮了但屏幕无内容跳过这条。复位是否执行。用GPIO控制RST时上电后要拉低至少20us再拉高。如果你把RST接到了3.3V上没有用GPIO控制也大概率能亮但部分模组由于内部RC复位时序问题会出现“上电概率性白屏”解决办法就是GPIO控制RST。初始化序列是否执行完整。特别是0x11退出睡眠和0x29开启显示。我见过有人把初始化代码从参考工程里抄过来但把0x29漏了结果屏是什么都不显示。SPI模式是否匹配。在CubeMX里把SPI配置从模式0改成模式3试试。注意改完要重新初始化显示否则时序对不上。用逻辑分析仪抓波形。看SCK上有没有脉冲、MOSI电平是否变化这是定位“主控根本没发数据”和“屏没收到数据”的最快方法。4.2 花屏和颜色异常多为接线、速率、字节序花屏的典型场景是在点亮成功之后画面出现彩色噪点、横纹、错位。第一种可能是接线太长杜邦线超过15厘米SPI信号边沿劣化。把线缩短或者把SPI时钟从9MHz降到4.5MHz试试。稳定后如果还想提速可以在MOSI、SCK上串33欧电阻改善振铃。第二种可能是颜色顺序反了。如果你显示红色却变成蓝色或者人脸发青十有八九是0x36寄存器里的BGR位没有设对。0x36 0xC8是“BGR1”如果你用0x08或者0x48色彩就会颠倒把BGR位反过来即可。第三种可能是字节序问题。写RGB565时大多数驱动采用高字节在前color 8再发送。如果你用的是现成库跟另一份代码的字节序不一致就会看到色块错位、边缘有杂色统一成高字节在前就好。4.3 画面偏移、镜像和部分区域不显示画面整体镜像、上下颠倒去调0x36的MY和MX位就行。比如你希望竖屏但显示成了横屏或者反过来这就是MV位在起作用0xC8的MYMX组合会让屏幕按从左到右、从上到下的顺序扫描具体哪个组合适配你的模组需要实测两三次。部分区域不显示或显示到别的地方则基本可以断定是窗口设置函数有问题。检查0x2A和0x2B传的是不是合法坐标范围注意x1、y1是包含边界值的比如画一个从(0,0)到(239,319)的整屏窗口坐标最大值就是239和319。如果写了240屏幕GRAM地址就溢出到下一行了。4.4 触摸和背光干扰显示如果模组带触摸功能常见XPT2046触摸控制器会复用部分SPI引脚。你需要确认屏和触摸是否共用SPI总线和片选别在初始化屏幕的时候把触摸片选也拉低了否则触摸控制器会在SPI总线上抢数据轻则花屏重则初始化失败。触摸芯片一般单独一个CS引脚同时在读触摸坐标时会占用一定时间可以用轮询方式读取时序上注意和屏幕刷新错开。背光PWM调节时如果把PWM频率设置得太低比如几百Hz仔细看屏幕能察觉到亮度闪烁。一般把PWM频率提到1kHz以上就基本看不出来了如果做摄影灯那种高刷新场合背光干脆不要PWM直接拉满电压。5. 后续可以怎么玩显示打通以后这个组合能扩展的方向很多。一是加触摸在2.8寸屏上做按键菜单、滑动手势配合C8T6的20KB RAM跑一个轻量级状态机完全没问题。二是显示传感器数据接DHT11、BMP280、MPU6050把温度、气压、姿态实时画在屏上这就是一个小型仪表板。三是上RTOS比如FreeRTOS里开一个显示任务、一个采集任务让屏的刷新和数据处理解耦这是嵌入式多任务开发很好的练手项目。如果对性能不满意可以试试“画中画”局部刷新只在数据变化时更新相关区域而不是整个屏幕重画实战里很多界面卡顿都是因为“全屏重绘”惹的祸。这个优化思路比单纯追求SPI频率更实用。最后再分享一个小技巧做界面之前先把常用的“画矩形、填充、显示字符串”这几个函数封装好后面所有界面代码都会轻松很多我第二次做项目时就直接复用这一套底层节省了大量时间。本文还有配套的精品资源点击获取
