简介这份资源面向嵌入式初学者与单片机开发者提供STM32F103驱动3.5寸ILI9486触摸屏的完整HAL库工程可直接烧录运行帮助读者跳过繁琐的底层配置快速搭建TFT显示与触摸交互原型。压缩包共153个文件约4.77MB以c与h源码为主配合o、d、crf等编译中间文件及uvprojx、ioc工程配置另含hex烧录文件与map、lst等链接信息覆盖从源码到固件的完整链路。资源围绕STM32F103的HAL库编程、ILI9486控制器驱动、SPI与GPIO配置、触摸中断处理及STM32CubeMX初始化等关键知识点展开移植了中景园驱动代码包含初始化、分辨率设置、颜色模式、清屏、画点画线、文本图像显示等功能并涉及XPT2046触摸坐标读取。目前已有1702人学习下载适合作为图形界面与人机交互设计的实践起点。1. 一块 3.5 寸屏点亮之后我才发现 STM32F103 的 GPIO 模拟总线才是真门槛很多人第一次拿到 3.5 寸 ILI9486 触摸屏模块插上 STM32F103 最小系统板烧进去一个现成的例程屏幕要么全白、要么花屏、要么干脆没反应。我当初也踩过这个坑后来才发现问题根本不在屏幕本身而在于 ILI9486 这个控制器对时序的要求比常见的 ILI9341 更挑剔尤其是 16 位并口模式下STM32F103 的 GPIO 翻转速度如果没算清楚写进去的像素数据就是乱的。这份资源给的就是一套完整的 HAL 库版本驱动代码覆盖了 ILI9486 的初始化、GRAM 写入、触摸屏 XPT2046 读取以及基本的绘图接口拿到手可以直接烧录验证。适合正在用 STM32F103 做嵌入式 HMI 界面、想快速把屏幕跑起来再二次开发的人也适合想搞明白 FSMC 和 GPIO 模拟总线到底差在哪的开发者。2. 硬件接线与工程结构先把屏幕和板子接对再谈代码2.1 模块引脚定义与 STM32F103 的对应关系ILI9486 模块通常引出 20 个左右的排针核心信号分三组数据总线 DB0~DB15、控制信号 CS/WR/RD/RS/RST、以及触摸屏的 T_CS/T_IRQ/T_DO/T_DIN/T_CLK。STM32F103 这边常见做法是用 FSMC 的 NOR/SRAM 模式来驱动 8080 并口因为 FSMC 能自动产生 WR/RD 时序CPU 只需要往固定地址写数据就行。但这份代码走的是 GPIO 模拟总线方案原因是 F103 的 FSMC 引脚分布比较固定很多最小系统板没有全部引出而 GPIO 模拟更灵活任何引脚都能用。接线时最容易翻车的地方是数据总线的高低字节顺序。ILI9486 在 16 位模式下DB0~DB15 对应一个像素的 RGB565 值但模块排针上往往标的是 LCD_D0~LCD_D15实际连接时如果 D0 接到了 PA0、D1 接到了 PA1那写 0xF800红色进去屏幕可能显示成蓝色或者绿色。我一般会先用一个简单的测试全屏写 0xF800看是不是红色如果不是就把数据线的高 8 位和低 8 位对调试试。// lcd_gpio.h 中定义的数据总线引脚映射 // 这里假设使用 GPIOB 的低 16 位作为数据总线 #define LCD_DATA_PORT GPIOB #define LCD_DATA_PIN_0 GPIO_PIN_0 #define LCD_DATA_PIN_1 GPIO_PIN_1 // ... 省略中间引脚定义 #define LCD_DATA_PIN_15 GPIO_PIN_15 // 控制信号引脚定义 #define LCD_CS_PORT GPIOA #define LCD_CS_PIN GPIO_PIN_4 #define LCD_RS_PORT GPIOA #define LCD_RS_PIN GPIO_PIN_5 #define LCD_WR_PORT GPIOA #define LCD_WR_PIN GPIO_PIN_6 #define LCD_RD_PORT GPIOA #define LCD_RD_PIN GPIO_PIN_7 #define LCD_RST_PORT GPIOA #define LCD_RST_PIN GPIO_PIN_8这段代码把数据总线和控制信号分开定义好处是后面写时序函数时可以直接操作 BSRR 寄存器不用一个个引脚去调 HAL_GPIO_WritePin。参数上数据总线选 GPIOB 是因为 F103 的 B 口在 APB2 总线上翻转速度比 A 口稍快而且 B 口低 16 位刚好能一次写半个字。控制信号放在 GPIOA 是为了布线方便实际项目中可以根据 PCB 走线调整。2.2 工程目录与 HAL 库配置要点这份代码的工程结构比较典型基于 STM32CubeMX 生成后手动添加了 LCD 驱动层。目录大致是这样的Project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── lcd_ili9486.h // LCD 驱动头文件 │ │ ├── lcd_gpio.h // 引脚宏定义 │ │ └── touch_xpt2046.h // 触摸屏驱动 │ └── Src/ │ ├── main.c │ ├── lcd_ili9486.c // 核心驱动实现 │ ├── lcd_gpio.c // 总线读写时序 │ └── touch_xpt2046.c ├── Drivers/ │ └── STM32F1xx_HAL_Driver/ └── MDK-ARM/ └── Project.uvprojxCubeMX 配置时关键是把用到的 GPIO 全部设为 Output Push-Pull、High Speed不要开 Pull-up/Pull-down因为并口总线上有外部上拉电阻。时钟树方面F103 通常跑 72MHzAPB2 也是 72MHz这样 GPIO 翻转一次大约 14ns写一个像素需要两次翻转WR 低然后高加上数据建立时间实际一个像素大约 100ns 左右刷满 320x480 的屏幕需要 15ms 左右肉眼看起来刷新率勉强够用。提示如果编译时提示 HAL_GPIO_WritePin 未定义检查 Drivers 目录下是否包含了 stm32f1xx_hal_gpio.cCubeMX 生成时有时会漏掉。3. 核心驱动拆解从写命令到刷屏的完整链路3.1 写命令与写数据的时序实现ILI9486 的 8080 并口协议里RS 引脚决定当前传的是命令还是数据RS0 写命令RS1 写数据。WR 上升沿锁存数据RD 下降沿读数据。GPIO 模拟的关键就是精确控制这几个信号的先后顺序。// lcd_gpio.c 中的底层写函数 static inline void LCD_WriteBus(uint16_t data) { // 直接操作 BSRR 寄存器避免读-改-写 GPIOB-BSRR (uint32_t)data; GPIOB-BRR (uint32_t)(~data 0xFFFF); } void LCD_WriteCmd(uint16_t cmd) { LCD_CS_LOW(); // 片选拉低 LCD_RS_LOW(); // 命令模式 LCD_WR_LOW(); // WR 拉低 LCD_WriteBus(cmd); // 数据放到总线上 LCD_WR_HIGH(); // WR 上升沿锁存 LCD_CS_HIGH(); // 片选拉高 } void LCD_WriteData(uint16_t data) { LCD_CS_LOW(); LCD_RS_HIGH(); // 数据模式 LCD_WR_LOW(); LCD_WriteBus(data); LCD_WR_HIGH(); LCD_CS_HIGH(); }这里用 BSRR 和 BRR 寄存器代替 HAL_GPIO_WritePin 是血泪经验HAL 库的函数有参数检查、有断言一次调用几十个时钟周期就没了刷屏时根本扛不住。BSRR 写 1 置位、BRR 写 1 复位两条指令搞定 16 位数据。参数上data 是 16 位像素值或命令码注意 BRR 那里要取反再与 0xFFFF否则会把高 16 位也清零。3.2 ILI9486 初始化序列与 GRAM 写入ILI9486 的初始化寄存器比 ILI9341 多不少尤其是电源控制和 Gamma 校正部分如果照搬 ILI9341 的初始化代码屏幕大概率偏色或者亮度不对。这份代码里的初始化序列是针对 3.5 寸 320x480 模块调过的。// lcd_ili9486.c 中的初始化函数片段 void LCD_Init(void) { LCD_RST_LOW(); HAL_Delay(20); LCD_RST_HIGH(); HAL_Delay(120); LCD_WriteCmd(0xE0); // Positive Gamma Control LCD_WriteData(0x00); LCD_WriteData(0x03); LCD_WriteData(0x09); LCD_WriteData(0x08); // ... 省略后续 Gamma 参数 LCD_WriteCmd(0xE1); // Negative Gamma Control LCD_WriteData(0x00); LCD_WriteData(0x16); // ... 省略 LCD_WriteCmd(0x36); // Memory Access Control LCD_WriteData(0x48); // 横屏模式BGR 顺序 LCD_WriteCmd(0x3A); // Pixel Format LCD_WriteData(0x55); // 16 位 RGB565 LCD_WriteCmd(0x11); // Sleep Out HAL_Delay(120); LCD_WriteCmd(0x29); // Display ON }初始化里 0x36 寄存器的值决定屏幕方向0x48 是横屏、BGR 顺序如果显示红蓝反了改成 0x40 试试。0x3A 写 0x55 表示 16 位色写 0x66 是 18 位色但 F103 的 GPIO 模拟总线传 18 位需要三次写一般不用。GRAM 写入时先发 0x2C 命令然后连续写像素数据地址会自动递增。void LCD_Fill(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { LCD_SetWindow(x1, y1, x2, y2); LCD_WriteCmd(0x2C); uint32_t total (uint32_t)(x2 - x1 1) * (y2 - y1 1); for (uint32_t i 0; i total; i) { LCD_WriteData(color); } }LCD_SetWindow 负责发 0x2A 和 0x2B 设置行列地址然后 0x2C 开始写 GRAM。参数 color 是 RGB565 格式比如红色是 0xF800。这个循环里每次 LCD_WriteData 都有函数调用开销如果追求更快可以把 CS 和 RS 的操作提到循环外面只翻转 WR。3.3 触摸屏 XPT2046 的 SPI 读取触摸屏部分用的是 XPT2046 控制器它本质是一个 SPI 接口的 ADC。STM32F103 这边可以用硬件 SPI也可以用 GPIO 模拟。这份代码用的是软件 SPI因为触摸屏的采样率要求不高软件 SPI 更省引脚。// touch_xpt2046.c 中的读取函数 uint16_t XPT2046_ReadADC(uint8_t cmd) { uint16_t value 0; T_CS_LOW(); T_CLK_LOW(); for (int i 0; i 8; i) { if (cmd 0x80) T_DIN_HIGH(); else T_DIN_LOW(); cmd 1; T_CLK_HIGH(); T_CLK_LOW(); } T_CLK_HIGH(); // 跳过忙标志 T_CLK_LOW(); for (int i 0; i 12; i) { T_CLK_HIGH(); value 1; if (T_DO_READ()) value | 0x01; T_CLK_LOW(); } T_CS_HIGH(); return value; }cmd 的 bit7 是起始位bit6~4 是通道选择bit3 是 12/8 位模式bit2 是单端/差分。读出来的 12 位值需要做校准通常取触摸屏四个角读两组值算出 X 和 Y 的缩放系数和偏移量。常见做法是在屏幕上画四个十字让用户依次点击把原始 ADC 值存到 Flash 里。4. 避坑与常见问题屏幕不亮、花屏、触摸跳点的排查4.1 屏幕全白或全黑背光亮但没图像现象上电后背光正常但屏幕没有任何显示或者全白。原因通常是初始化序列没跑完就发了显示开启命令或者 RST 复位时间不够。ILI9486 要求 RST 拉低至少 10us拉高后等 120ms 才能发命令。解决把 HAL_Delay(120) 加到 LCD_RST_HIGH() 之后并且确认 0x11 命令后也等了 120ms 再发 0x29。4.2 花屏、颜色错乱、像素偏移现象屏幕能亮但显示的内容像雪花或者颜色明显不对。原因一般是数据总线某一位虚焊或接错或者 0x36 寄存器的 BGR 位设反了。解决先写全屏 0xF800如果显示蓝色说明 BGR 位要改如果显示条纹用万用表逐个测数据线的通断重点查 D8~D15 有没有接反。4.3 触摸坐标跳变严重按不准现象触摸笔点一个位置读出来的坐标跳动几十个像素。原因有两个一是 XPT2046 的参考电压不稳模块上一般有 104 电容如果没焊或者容量不够读数就会抖二是软件 SPI 的时钟太快XPT2046 最高支持 2MHzGPIO 模拟时如果没加延时时钟可能跑到 5MHz 以上。解决在 T_CLK_HIGH() 和 T_CLK_LOW() 之间加一个 __NOP()或者把 SPI 时钟分频调大。4.4 刷屏速度慢动画卡顿现象填充整个屏幕要几百毫秒刷新页面时肉眼可见撕裂。原因每次写数据都调用 LCD_WriteData 函数函数调用和 CS/RS 操作占了大量时间。解决把 CS 拉低后连续写多个像素只在换页时拉高或者把 LCD_WriteData 改成宏定义让编译器内联展开。4.5 编译报错 region RAM overflowed现象Keil 编译时提示 RAM 不够。原因F103 只有 20KB RAM如果开了大数组做显存或者字库很容易超。解决不要开全屏显存直接写 GRAM字库用外部 Flash 或者只取需要的几个汉字把栈大小从默认的 0x400 改成 0x200。5. 二次开发改造从能跑到好用的几个关键改动5.1 把 GPIO 模拟总线换成 FSMC刷屏速度翻三倍如果 PCB 上 FSMC 的引脚都引出来了强烈建议换成 FSMC 驱动。FSMC 的 NOR/SRAM 模式可以自动产生 8080 时序CPU 只需要往地址写数据不用手动翻转 WR。改造步骤在 CubeMX 里使能 FSMC选择 LCD Interface数据宽度 16 位然后定义两个地址指针一个指向命令地址一个指向数据地址。// 使用 FSMC 后的写命令和写数据 #define LCD_CMD_ADDR ((uint32_t)0x60000000) #define LCD_DATA_ADDR ((uint32_t)0x60020000) void LCD_WriteCmd_FSMC(uint16_t cmd) { *(volatile uint16_t *)LCD_CMD_ADDR cmd; } void LCD_WriteData_FSMC(uint16_t data) { *(volatile uint16_t *)LCD_DATA_ADDR data; }地址的偏移量由 FSMC 的 A 地址线决定A0 接 RS 时命令地址是基地址数据地址是基地址加 2。这样写一个像素只需要一条存储指令刷屏时间能从 15ms 降到 5ms 以内。5.2 加一层简单的 GUI 缓冲减少闪烁直接写 GRAM 时如果先清屏再画图会看到明显的闪烁。常见做法是在 RAM 里开一个小的行缓冲比如 320x10 的数组画完一行再一次性写进 GRAM。或者用脏矩形算法只刷新变化区域。F103 的 RAM 有限全屏缓冲不现实但 320x20 的缓冲只占 12.8KB勉强够用。5.3 触摸校准参数的存储与读取触摸校准值不能每次上电都重新算我一般会把它存到 STM32F103 内部的 Flash 最后一页。写之前先解锁 Flash擦除页然后按半字写入。读取时直接读地址。注意 Flash 擦写寿命只有 1 万次左右不要频繁写只在用户主动校准时才存一次。// 校准参数结构体 typedef struct { int32_t x_scale; int32_t y_scale; int32_t x_offset; int32_t y_offset; } TouchCalib_t; // 存到 Flash 地址 0x0801FC00最后一页 #define CALIB_ADDR ((uint32_t)0x0801FC00)5.4 用 DMA 加速数据搬运如果不想换 FSMC还有一个折中方案用 DMA 把内存里的像素数据搬到 GPIO 的 BSRR 寄存器。F103 的 DMA 支持外设到存储器、存储器到外设可以把 GPIOB-BSRR 设为目标地址DMA 每次传输一个半字。这样 CPU 只需要准备数据搬运交给 DMA刷屏时 CPU 可以去做触摸检测。不过 DMA 传输到 GPIO 的时序不好控制WR 信号还得用定时器或者 PWM 来产生实现起来比 FSMC 麻烦适合对成本敏感但引脚又不够用 FSMC 的场景。从那以后我每次拿到新的屏幕模块都强制先跑一遍全屏三原色测试和触摸四点校准确认硬件没问题再动上层代码。这套流程帮我省下了至少两天反复查线的时间。希望帮到你。本文还有配套的精品资源点击获取