资讯动态

STM32 HAL库驱动3.5寸SPI屏:ILI9486显示与XPT2046触摸校准实战

发布时间:2026/10/17 11:22:17 来源:尧图企业网站定制
简介这是一份基于STM32F103和HAL库驱动3.5寸ILI9486触摸屏的完整工程代码面向物联网、工控等嵌入式场景开发者尤其适合需要快速点亮LCD并加入触摸交互、但又不想从零编写寄存器级驱动的项目。压缩包为RAR格式共153个文件约4.77MB核心是HAL_ILI9486L驱动文件另外包括51个.h头文件、21个.c源文件以及o/d/crf等编译中间文件、uvprojx工程文件、ioc配置、hex/axf可执行文件等拿到后可直接编译烧录到STM32F103开发板。工程完整覆盖ILI9486初始化和清屏、画点、画线、文本显示以及XPT2046触摸坐标读取等关键功能SPI接口、GPIO使能信号、中断引脚均通过STM32CubeMX图形化配置生成方便查看和修改。内容预览中可见stm32f1xx_hal_tim、rcc、gpio等底层文件便于排查移植问题。目前已有1702人浏览学习既可作为HAL库工程规范模板也能作为后续开发简易GUI或人机交互界面的基础。1. 手里这块 3.5 寸屏能干什么HAL 库驱动一次说透做嵌入式设备最绕不开的一件事就是人机交互而 3.5 寸 320×480 的电阻触摸屏几乎是平衡成本与显示效果的常见选择。我第一次拿到这种屏时以为接上 SPI 就能点亮结果白屏、颜色错乱、触摸坐标乱跳轮着来最后耗了三个晚上才把整套流程理顺。这套 STM32F103 ILI9486 XPT2046 的 HAL 库工程就是把当初踩过的坑全部填平之后的完整版本从初始化序列到画点画线再到触摸校准代码直接烧录就能跑。它适合手头有 F103 核心板、想快速把屏幕点亮而不是从寄存器逐行啃 Datasheet 的开发者也适合需要做菜单、波形显示、参数设置界面的实际项目。2. HAL 库工程搭建与 SPI 参数选型先让屏幕被系统认识2.1 时钟树与 SPI 预分频为什么选 18Mbps 而不是最高档F103 的主频上限是 72MHz这一点决定了 SPI 外设能跑多快。注意 SPI1 挂在 APB2 上SPI2/SPI3 挂在 APB1 上APB1 外设时钟默认是 36MHz所以同样的预分频系数SPI2 能跑到的绝对速率只有 SPI1 的一半。我一般把按键和触摸芯片挂 SPI2把屏幕挂 SPI1这样刷屏带宽不受 APB1 限制。时钟树里把 PCLK2 设为 72MHzSPI1 预分频选 2就能得到 36MHz 的线时钟但对 ILI9486 来说 36MHz 在 SPI 四线模式下太激进官方手册里的 AC 时序最高也只保证到 20MHz 上下所以我实测跑 18Mbps——也就是预分频取 4。不用怀疑这个数值很多现成工程里写 36Mbps 反而不稳定长线传输时波形振铃严重。hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; hspi1.Init.CLKPhase SPI_PHASE_1EDGE; hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_4; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi1);这段是 CubeMX 生成后的标准配置核心参数集中在几个地方模式选主机、两线全双工、数据位 8bit、CPOL0 CPHA1 即 SPI Mode 0。ILI9486 支持 Mode 0 和 Mode 3Mode 0 是通用默认值如果手里的屏模组在别的工程里用的是 Mode 3改这两项即可其余不用动。预分频 4 意味着 SPI1 实际波特率为 72MHz/418Mbps如果你发现屏幕初始化偶尔失败或者花屏优先把预分频改成 8降到 9Mbps 排查是不是速率问题。2.2 GPIO 分配与时序角色DC、RST、CS 谁说了算HAL 库工程里 GPIO 的分配直接决定了后面写驱动时能不能少折腾。屏幕这边需要 5 根控制线加 2 根数据线SCK、MOSI、CS、DC、RST外加背光 BL 可选。SCK 和 MOSI 复用 SPI1 功能CS、DC、RST 全部用普通推挽输出 GPIO 手动控制不要依赖硬件 NSSF103 的硬件 NSS 在 SPI 主机模式下行为比较绕不如直接用软件拉低拉高来得干净。DC 引脚尤其重要它决定当前 SPI 总线上的字节是命令还是数据ILI9486 的所有寄存器操作都依赖这个电平切换选一个普通 PA 口即可。功能推荐引脚模式说明SCKPA5AF_PPSPI1_SCKMOSIPA7AF_PPSPI1_MOSICSPB6OUT_PP软件控制低有效DCPB7OUT_PP低为命令高为数据RSTPB8OUT_PP低有效复位BLPB9OUT_PP高电平点亮触摸芯片 XPT2046 挂在另一个 SPI 总线 SPI2 上时要有独立的 CS 引脚。我常用的触摸 CS 是 PB12PENIRQ 接 PB13 并配成外部中断输入。在配置 GPIO 时要留意F103 的 SPI2 复用引脚是 PB13 的 SCK 和 PB15 的 MOSI别把 MISO 和 MOSI 接反这种错我犯过一次触摸数据读出来全是 0xFFFF 或 0x0000。2.3 工程骨架main.c 里的初始化顺序有讲究CubeMX 生成的 main.c 结构基本固定能够直接用的部分是HAL_Init()、SystemClock_Config()和 GPIO/SPI 的初始化MX_GPIO_Init()、MX_SPI1_Init()。屏幕上电时序一般是先拉低 RST 至少 10ms再拉高等待 120ms之后才能发初始化序列。注意不要把复位延时写在MX_GPIO_Init()前面GPIO 外设时钟没打开时操作引脚是无效的这也是屏幕第一天怎么都不亮的原因。#define LCD_RST_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET) #define LCD_RST_HIGH() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET) int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); MX_SPI2_Init(); LCD_RST_LOW(); HAL_Delay(20); LCD_RST_HIGH(); HAL_Delay(150); lcd_init(); touch_init(); lcd_fill_screen(0xFFFF); while (1) { // 主循环里跑触摸轮询或刷帧 } }注意这里MX_SPI2_Init()是给触摸用的假如你的屏模组触摸和显示共用一条 SPI 总线只需要一个 SPI 外设和两根 CS 线这在我的另一块模组上碰到过确认你的连接方式属于哪种再决定用几个 SPI。模块划分上我把 LCD 驱动、触摸驱动、GUI 基础绘制分别放三个文件main.c 只保留调度逻辑工程大了之后维护起来不会变成一坨代码。3. ILI9486 初始化序列与绘图驱动从寄存器到画点画线3.1 初始化序列背后的设计逻辑Gamma 与电源控制不能照抄很多教程的初始化数组是直接从卖家源码里拷的照抄能亮但不知道为什么。ILI9486 的初始化本质是两件事设置电源电压和设置显示扫描方式。0xC0 寄存器控制电源的 GVDD 电平常见值是 0x1B对应的是一组内部电荷泵的输出电压改大会导致对比度异常甚至出现白屏残影。0xE0 和 0xE1 分别是正负 Gamma 校正一组 15 个字节直接决定灰阶过渡是否平滑。对大多数模组来说这套参数是原厂调好的你只需在初始化时原样写入。static const uint8_t lcd_init_cmd[] { 0xE0, 15, 0x00, 0x03, 0x09, 0x0B, 0x1E, 0x28, 0x30, 0x3B, 0x3D, 0x2E, 0x14, 0x15, 0x0E, 0x07, 0x0A, 0xE1, 15, 0x00, 0x03, 0x09, 0x0B, 0x1E, 0x28, 0x30, 0x3B, 0x3D, 0x2E, 0x14, 0x15, 0x0E, 0x07, 0x0A, 0xC0, 2, 0x1B, 0x0B, 0xC1, 1, 0x10, 0xC5, 2, 0x30, 0x30, 0xC7, 1, 0xB7, 0x36, 1, 0x48, 0x3A, 1, 0x55, 0xB1, 2, 0x00, 0x1B, 0xB4, 1, 0x02, 0xB6, 3, 0x02, 0x02, 0x3B, 0x11, 0, 0x29, 0, };这段数组的格式是“命令号 参数个数 参数列表”参数个数为 0 表示只有命令没有数据。0x36 的值 0x48 做了两件事打开 MX 镜像位同时把 RGB 顺序改成 BGR如果你的屏幕显示红色和蓝色对调就把 0x48 改成 0x08。0x3A 写入 0x55 表示像素格式为 16bit RGB565这是 SPI 四线模式下最常见的配置如果你把初始化改成 0x66 则进入 18bit 模式颜色更准确但刷屏带宽大增不建议效仿。0x11 是 Sleep Out之后必须延时 120ms 再发 0x29 打开显示否则屏幕会一直黑着。3.2 画点函数set_window 是唯一入口ILI9486 的坐标管理方式是窗口制你先告诉芯片要往哪个矩形区域写像素再连续往这个区域填充数据。一次完整的画点操作包含设置列地址 0x2A、设置行地址 0x2B、然后写 0x2C 进入写像素状态。HAL 库里最直接的做法是每次画点都做一遍窗口设置但这样做 SPI 事务开销极大。更合理的做法是画点函数保留低层能力实际绘制场景尽量用批量填充。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); } void lcd_draw_pixel(uint16_t x, uint16_t y, uint16_t color) { lcd_set_window(x, y, x, y); uint8_t buf[2]; buf[0] color 8; buf[1] color 0xFF; HAL_SPI_Transmit(hspi1, buf, 2, 100); }lcd_write_cmd和lcd_write_data的区别只在 DC 引脚的电平上。写命令时 DC 拉低写数据时 DC 拉高。这里有个细节是所有地址参数都要拆成高低字节分别发送先发高位后发低位顺序反了窗口设置就会错乱。画点的颜色参数用 uint16_t 承载 RGB565红色是 0xF800绿色是 0x07E0蓝色是 0x001F如果需要从 R8G8B8 转成 RGB565用((r 0xF8) 8) | ((g 0xFC) 3) | (b 3)。3.3 批量绘制与矩形填充把 SPI 事务从两次降到一次UI 界面大部分元素都是矩形或渐变条逐点调用画点函数会慢到肉眼可见。正确的批量做法是先设置目标窗口然后把要显示区域的像素数据拼成一个大数组一次发送。全屏 320×480 的 RGB565 数据是 307200 字节堆内存根本放不下。常规方案有两种一是申请一块矩形区域大小的缓冲比如 8×8 的字体字模缓冲二是按行刷每行 640 字节分 480 次发送。第二种方案不占用大内存速度也能接受。void lcd_fill_rect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { lcd_set_window(x0, y0, x1, y1); uint32_t pixel_cnt (uint32_t)(x1 - x0 1) * (y1 - y0 1); uint32_t bytes pixel_cnt * 2; uint8_t *buf (uint8_t *)malloc(bytes); if (buf NULL) return; for (uint32_t i 0; i pixel_cnt; i) { buf[i * 2] color 8; buf[i * 2 1] color 0xFF; } HAL_SPI_Transmit(hspi1, buf, bytes, HAL_MAX_DELAY); free(buf); }这段代码用了 malloc在小内存设备上有风险不建议在实际工程里照搬。更稳妥的方法是改成一个固定 640 字节的行缓冲外层循环 480 次每次只发送一行像素数据。画线算法方面Bresenham 在整数运算上是最优解但如果你把画线实现成“计算出一系列点坐标再逐点调用 draw_pixel”每一次调用都涉及窗口设置和 SPI 传输性能会非常差。另一个常见优化是绘制水平或垂直线时先调用lcd_set_window把宽度缩成 1 像素的窄矩形再按批量填充方式发送这样一条线只要两次 SPI 事务。4. XPT2046 触摸读取与校准坐标映射不是简单的比例换算4.1 触摸 SPI 时序命令字节与 12 位转换结果XPT2046 是一颗 4 线电阻触摸屏的典型控制芯片它在 SPI 总线上扮演从机主机发送一个字节的命令然后读取 3 个字节转换结果取中间 12 位有效数据。命令字节的高四位决定了要测量的是 X 方向还是 Y 方向0x90 表示差分模式测量 X 坐标0xD0 表示测量 Y 坐标。别搞反X 和 Y 接反的话屏幕上点横坐标会变成纵坐标界面逻辑会全部错乱。uint16_t touch_read_raw(uint8_t cmd) { uint8_t tx cmd; uint8_t rx[3] {0}; HAL_GPIO_WritePin(TOUCH_CS_GPIO_Port, TOUCH_CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi2, tx, 1, 100); HAL_SPI_Receive(hspi2, rx, 3, 100); HAL_GPIO_WritePin(TOUCH_CS_GPIO_Port, TOUCH_CS_Pin, GPIO_PIN_SET); return ((rx[1] 8) | rx[2]) 3; }注意我在发送命令和读取转换结果之间没有做延时原因是 XPT2046 的转换期间 SCLK 会继续驱动数据线主机连续发送 3 字节的读时钟就能拿到完整结果。这三个字节中第一个字节是前次转换的残留所以要舍掉只取第 2 和第 3 字节组合后的高 12 位。另一个关键点是触摸 CS 必须在传输期间全程拉低有些工程只在发命令时拉低、读数据时拉高结果坐标要么是 0要么是随机的毛刺数字。4.2 一次校准实站四角取点还是三线拟合电阻屏的坐标系统存在两套逻辑ADC 原始值和 LCD 像素坐标二者之间受屏幕材质、贴合工艺、供电电压影响不同模组的换算比例并不一致。最简单实用的校准方法是取四个角或边缘中点采样算出 X 方向的比例因子和偏移量x_screen (raw_x - x_min) * 320 / (x_max - x_min)Y 方向同理。这个方法在屏幕装配没有明显旋转时已经够用。如果屏幕贴歪了或者你要支持不同批次的模组就要用三点校准解一个仿射变换矩阵。typedef struct { int16_t x_min, x_max; int16_t y_min, y_max; uint8_t swap_xy; } touch_cal_t; static touch_cal_t cal {0}; void touch_map(uint16_t raw_x, uint16_t raw_y, uint16_t *sx, uint16_t *sy) { int16_t x raw_x, y raw_y; if (cal.swap_xy) { int16_t t x; x y; y t; } *sx (uint16_t)((x - cal.x_min) * 320L / (cal.x_max - cal.x_min)); *sy (uint16_t)((y - cal.y_min) * 480L / (cal.y_max - cal.y_min)); }这段代码里的swap_xy标志是旋转适配的关键。ILI9486 的 3.5 寸屏物理尺寸接近正方形很多时候为了排版方便会把显示方向做成横屏这时触摸 ADC 的 X 轴对应的是屏幕的 Y 方向不交换坐标的话触摸轨迹会和显示内容完全垂直。初始化时在四个角分别采样并把结果存入 cal 结构体画一个十字光标让用户点击效果比纯后台自动校准更直观。如果懒一点也可以直接把 x_min 设为 0、x_max 设为 4095再乘以一个 0.98 的修正系数但不同批次的屏误差会明显放大看重用户体验就别省这个步骤。4.3 滤波与抗抖动滑动平均在触摸场景的局限性拿到原始坐标直接映射到屏幕坐标手感会很差。电阻屏本质是一个分压电阻网络按下时接触电阻会波动表现为坐标在目标点周围跳动。对于这类噪声我试过滑动平均效果不理想——它只能平滑高频抖动对偶尔出现的尖峰毛刺毫无办法。真正稳的方案是先做限幅再做中值滤波连续采样 5 次去掉最大值和最小值对中间 3 个值取平均。uint16_t touch_filter(uint16_t raw) { static uint16_t buf[5]; static uint8_t idx 0; buf[idx] raw; if (idx 5) idx 0; uint8_t i, j; uint16_t tmp[5], t; for (i 0; i 5; i) tmp[i] buf[i]; for (i 0; i 4; i) { for (j i 1; j 5; j) { if (tmp[j] tmp[i]) { t tmp[i]; tmp[i] tmp[j]; tmp[j] t; } } } return (tmp[1] tmp[2] tmp[3]) / 3; }注意这个函数每次touch_read_raw返回后的调用顺序先读 X 再读 Y各过一次滤波然后才做坐标映射。不要在一次采样里先把 X 和 Y 都读回来再统一滤波那样引入的是不同时间点的采样坐标会出现明显的拉扯感。除了算法校验位置还会影响结果PENIRQ 引脚检测到按下时芯片内部参考电压还没稳定所以最好在 PENIRQ 有效后延时 1ms 再发起读取这个时序细节比任何滤波算法都重要。5. 避坑清单这个工程里最容易翻车的五个地方5.1 白屏但背光亮问题出在复位时序而非驱动代码现象是上电后屏幕背光正常但画面全白发送任何初始化命令都没有反应。我排查了 SPI 波形和初始化数组最后发现是 RST 引脚复位脉宽太短。F103 上电过程中 GPIO 状态不定如果 RST 引脚被瞬间拉低又被迅速释放ILI9486 可能只完成了部分复位内部状态机停在异常位置。解决方法是复位时序强制拉低 RST 50ms 以上然后拉高再等 150ms。注意复位后不能立刻发初始化命令睡眠退出指令 0x11 之后的延时也不能省否则同样会出现白屏。5.2 颜色整体偏蓝或红蓝对调0x36 的 BGR 位和 0x3A 的数据格式现象是明明写入 0xF800 红色屏幕显示的却是蓝色或者颜色整体偏冷色。原因有两个ILI9486 的 MADCTL 寄存器里有一个 BGR 位默认是按 RGB 顺序排列的如果你的模组物理线路是 BGR 走线就需要在 0x36 的初始值里加上 0x08。第二个原因是像素格式配置0x3A 写入 0x55 才是 16bit如果你从某些自制的工程里拷贝了 0x6618bit那 SPI 发送的 16bit 数据会被当成 18bit 解析颜色自然不对。排查顺序先确认 0x3A再改 0x36 的低字节。5.3 触摸坐标乱跳且无规律片选引脚没有独立控制现象是触摸采样值在按下时不稳定放开后还能读到接近 4095 的残留数据。第一次遇到时以为是滤波不够实际上是我把触摸芯片和屏幕的 CS 挂在同一个引脚上——屏幕操作和触摸操作互相干扰SCLK 线上混着屏幕的数据包。解决方法是给触摸芯片分配独立 CS 引脚且在触摸采样时先延时 1ms 等内部上电稳定再开始 SPI 事务。另一个容易忽略的点是 XPT2046 的 VREF 电压如果模组没有独立 VREF 而直接使用 AVDD供电纹波会直接反应到坐标上这种噪声再多滤波算法都压不住只能改善电源质量。5.4 用 DMA 刷屏后触摸失灵SPI 总线占用冲突现象是 DMA 方式刷屏时触摸完全无响应刷屏完成后触摸能恢复但坐标偶尔偏移。原因是触摸和显示共用一条 SPI 总线时DMA 传输占据着总线触摸读取命令发不出去。我的做法是触摸读取之前检查 SPI 外设状态用HAL_SPI_GetState或者直接读取 SR 寄存器的 BSY 位确认 DMA 传输完全结束后再片选触摸。另一个细节HAL_SPI_Transmit_DMA发起后要等传输完成回调里的标志位置位不要在中断里立刻切换 CSDMA 可能还在把数据从内存搬到移位寄存器。5.5 编译后 Flash 超出HAL 库的默认配置吃掉了大量空间现象是 F103C8T6 编译报错Flash 占用超过 64KB。原因包括 SPI 的 HAL 驱动默认使能了中断处理、assert 宏全量展开、调试接口没关等因素。解决思路是三步工程选项里把优化等级从 -O0 改成 -O2用 CubeMX 配置时只勾选实际用的外设不用的 SPI/USART 不要开启在 lcd 驱动里把 HAL_SPI_Transmit 的 timeout 改成固定 100不要用 HAL_MAX_DELAY 占住堆栈。还有一个实际技巧F103C8T6 只有 20KB SRAM做全屏缓冲就不可能所以 fi的方案里我刻意不用大数组。如果项目确实需要全屏缓冲建议换 F103ZET6 或者给 LCD 单独配一块外部 SRAM 芯片。6. 把刷屏速度逼到极限的进阶技巧DMA、方向切换与验证6.1 用 SPI DMA 刷帧让 CPU 从长传输里解放出来批量填充矩形时把HAL_SPI_Transmit换成HAL_SPI_Transmit_DMA能显著降低 CPU 占用。核心逻辑是初始化一个全局像素缓冲在 UI 绘制完成后调用 DMA 传输传输完成中断里把 DMA 状态复位。使用 DMA 前一定要先关掉对应的 DMA 通道再重新配置否则第二次启动传输会失败。另外结合上一章说的DMA 传输期间任何触摸读取都必须等待 BSY 位清零。6.2 屏幕方向旋转后触摸坐标跟着翻转方向0x36 值触摸 X 映射到触摸 Y 映射到竖屏0x48屏幕 X屏幕 Y横屏0x28屏幕 Y屏幕 X竖屏镜像0x88320-X480-Y横屏镜像0xE8480-Y320-X表格里的映射方式只要在touch_map()里按旋转换算即可不用改触摸采样代码。旋转换算最常犯的错是忘记像素坐标范围也随之变化竖屏 320×480 变横屏就是 480×320。6.3 用一个彩色渐变页面验证整套工程稳定性拿到一套新模组时我不会直接写 UI而是先跑一个纯色渐变页左半屏红色、右半屏蓝色中间一列绿色竖线然后在触摸区画一圈十字刻度并实时显示坐标点。如果颜色渐变上没有竖条纹说明像素格式正确如果触摸光标能准确跟到十字刻度中心说明校准矩阵可用。这套验证方法让我在三块不同批次屏幕上都能在十分钟内确认驱动是否适配。从那以后我每次换屏模组都强制走一遍这个流程颜色和触摸的隐藏问题就不会攒到最后一起爆炸。这套工程是我从实际项目里提取出来的希望能帮你在自己的板子上少走几晚弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑