news 2026/9/9 22:21:14

STM32F103 HAL库驱动ILI9486 SPI触摸屏完整方案与接线配置

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103 HAL库驱动ILI9486 SPI触摸屏完整方案与接线配置

简介:这套基于STM32F103的3.5英寸ILI9486触摸屏HAL库工程,面向嵌入式和单片机开发者,解决屏幕驱动与触摸交互的快速落地问题。工程使用STM32CubeMX生成底层初始化,结合中景园ILI9486驱动代码,通过SPI接口完成显示和触摸读取,适合需要彩屏界面又希望降低底层开发门槛的中级开发者。压缩包共153个文件,以51个.h、21个.c源文件为主体,同时包含.o、.d、.hex、.axf、.map及uvprojx/uvoptx等工程与编译产物,整体仅4.77MB,体积紧凑但内容完整。项目已围绕HAL库封装好LCD控制和触摸读取逻辑,附有.ioc配置,可直接打开、重新生成或直接烧录hex验证。目前已有1689人学习下载,对理解STM32外设交互、HAL库SPI/GPIO编程和简单人机界面设计都有明显参考价值。 前阵子手头项目需要快速验证一块 3.5 寸 SPI 触摸屏,不想再翻标准库老代码,就直接用 STM32F103C8T6 最小系统 + HAL 库从头整理了一套 ILI9486 驱动。折腾完发现网上能找到的资料绝大多数还是标准库的写法,HAL 库版本要么残缺、要么只是个大杂烩,新手抄完直接编译都过不了。所以我把这套完整能跑的方案整理出来,接线、CubeMX 配置、LCM 初始化、触摸读取都放在下面,烧录即可用,适合手里有 STM32F103 最小系统和 3.5 寸 ILI9486 触摸屏的嵌入式爱好者直接抄作业。

1. 项目思路与整体设计

1.1 这一块“3.5寸屏”用到什么芯片

很多新手拿到 3.5 寸屏幕,第一反应是“这就是一块屏幕,一个驱动芯片搞定”。实际上这类 SPI 触摸屏模块里面至少有两个独立芯片:

  • ILI9486:负责显示面板,分辨率 320x480,支持 RGB 接口/SPI 接口,我们通过 SPI 四线方式操作它。
  • XPT2046:负责触摸采样,是一个 12 位 ADC 型四线电阻触摸控制器,和显示驱动完全独立,只是做在同一个 PCB 上。

这两个芯片挂在同一个 SPI 总线上,靠各自的片选脚(CS)区分。LCD_CS 拉低时操作屏幕,TP_CS 拉低时读触摸,互不干扰。搞懂这一层,后续接线和代码就不会乱。

另外注意,模块供电一般标 3.3V 或 5V,但 SPI 信号电平千万要接 3.3V。STM32F103 的 GPIO 是 5V 容忍(大多数引脚),可这不代表你可以无脑灌 5V 逻辑电平到屏幕的 SCLK/MOSI,尤其是长时间运行,发热和花屏的隐患很大。我这次直接全部用 3.3V。

1.2 引脚分配:能直接抄的接线表

我手头是最常见的 STM32F103C8T6 蓝色 pill 板,SPI 用 SPI1,硬件引脚固定是 PA5/PA6/PA7,其余控制脚全是普通 GPIO。完整接线如下:

屏幕/触摸引脚STM32F103C8T6说明
VCC3.3V供电,不要用 5V 给逻辑脚
GNDGND共地
LCD_SCLKPA5SPI1_SCK,时钟
LCD_MOSIPA7SPI1_MOSI,主发从收
LCD_MISOPA6SPI1_MISO,触摸 XPT2046 数据返回
LCD_CSPA4显示片选,软件控制
LCD_DC/RSPA2命令/数据选择
LCD_RSTPA1复位,低有效
LCD_BLPA0背光控制,GPIO 拉高点亮
TP_CSPA3触摸芯片片选
TP_IRQPB0触摸中断信号,低电平表示有按下

如果你的模块上触摸引脚名字写的是 T_CLK、T_DIN、T_DOUT,不要慌,它们就对应 SCLK、MOSI、MISO。T_CS 接 PA3,T_IRQ 接 PB0,逻辑完全一样。

1.3 CubeMX 配置要点

工程我是在 STM32CubeMX 里生成的,工具链用 STM32CubeIDE 或者 Keil 都能编译,关键配置就几处:

  • RCC:选择 HSE 外部晶振,我板子上是 8MHz 晶振,时钟树配置 PLL 到 72MHz。
  • SYS:Debug 选 Serial Wire,否则烧录一次后 SWD 口容易被占用。
  • SPI1:配置为 Full-Duplex Master,波特率预分频选 4。因为 APB2 总线时钟是 72MHz,72/4=18MHz,这是 F103 的 SPI 极限。
  • SPI1 参数:CPOL=Low,CPHA=1Edge(模式 0),8 位数据,MSB First,关闭硬件 NSS。
  • GPIO 输出脚 PA0-PA4 配成 Push-Pull Output,初始电平建议:CS 脚高电平、RST 脚高电平、BL 脚高电平。
  • PB0 配成 GPIO_Input,内部上拉。这样不按下时读到高电平,按下时被拉低。

这里解释一下为什么不用硬件 NSS:STM32 的硬件 NSS 自动管理经常会和软件操作打架,控制不好就会出现偶发花屏。LCD 这类设备我们对时序要求是“发完当前数据再拉高片选”,软件 GPIO 最直接可控。

2. ILI9486 显示驱动:从初始化到画点

2.1 命令/数据切换和 SPI 总线操作

ILI9486 通过 DC(也叫 RS)引脚区分当前发的是命令还是数据。DC 为低时写入的是命令字节,DC 为高时写入的是数据。理论上写命令和写数据都要先拉低 CS,写完再拉高,但有一个特殊情况:当你在发一长串像素数据时,CS 要一直保持低,不能每发一个字节就抖一下,否则屏幕的“内存写模式”会被打断。

我封装的最底层发送函数如下:

void LCD_SendBytes(uint8_t *data, uint16_t len) { HAL_SPI_Transmit(&hspi1, data, len, HAL_MAX_DELAY); } void LCD_WriteCmd(uint8_t cmd) { LCD_CS_0(); LCD_DC_0(); LCD_SendBytes(&cmd, 1); LCD_CS_1(); } void LCD_WriteData(uint8_t data) { LCD_CS_0(); LCD_DC_1(); LCD_SendBytes(&data, 1); LCD_CS_1(); }

很多新手会问,为什么感觉比标准库的代码多包了一层 HAL 函数?因为 HAL_SPI_Transmit 内部会检查 SPI 状态、忙标志,还会带超时,比标准库那种直接操作寄存器更稳。代价就是函数调用多一点,但对于初始化、设置窗口这种低频操作,完全无所谓。

2.2 初始化到底做了什么

ILI9486 上电后并不是直接能显示的,要先经历复位、退出睡眠、设置像素格式、设置扫描方向、开显示这几个步骤。下面是精简后可稳定工作的初始化序列:

void ILI9486_Init(void) { LCD_RST_0(); HAL_Delay(10); LCD_RST_1(); HAL_Delay(120); LCD_WriteCmd(0x11); // Sleep Out HAL_Delay(120); LCD_WriteCmd(0x36); LCD_WriteData(0x48); // 扫描方向 + BGR LCD_WriteCmd(0x3A); LCD_WriteData(0x55); // RGB565,16bit 像素格式 LCD_WriteCmd(0x20); // Inversion Off LCD_WriteCmd(0x29); // Display ON HAL_Delay(10); LCD_Clear(0xFFFF); // 清屏为白色 }

这里有几个容易踩的坑。0x36 是“Memory Access Control”,里面每一位控制扫描方向、行列交换、RGB/BGR 顺序。我给的是 0x48,适合竖屏 320x480,但如果你的模块方向和我相反,或者镜面效果不对,就试 0x08、0x28、0x88 三组值,总有一组正常。

0x3A 设成 0x55 表示 16 位 RGB565 色彩,每个像素 2 字节。ILI9486 本身能支持 18 位色彩,如果设成 0x66,每个像素要发 3 字节,速度更慢而且颜色并不一定更好。我这次按 2 字节像素处理,数据量小,刷新速度快。如果你的屏幕偏色严重,优先检查 0x36 的 BGR 位,而不是急着把 0x3A 改成 0x66。

2.3 填充函数为什么快了很多

最直观的画点函数是“设置窗口,写一个像素”,但画矩形或清屏时如果每个像素都单独调一次,光 SPI 片选翻转的时间就够你等半天。正确做法是先用 0x2A 设置列地址、0x2B 设置行地址,再发 0x2C 进入连续写内存模式,之后一路发像素数据,发完再把 CS 拉高。

void LCD_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { LCD_WriteCmd(0x2A); LCD_WriteData(x1 >> 8); LCD_WriteData(x1); LCD_WriteData(x2 >> 8); LCD_WriteData(x2); LCD_WriteCmd(0x2B); LCD_WriteData(y1 >> 8); LCD_WriteData(y1); LCD_WriteData(y2 >> 8); LCD_WriteData(y2); LCD_WriteCmd(0x2C); }

清屏函数可以这样写:准备 1KB 的纯色缓冲,然后反复把这段缓冲区喂给 SPI。这里必须注意,HAL_SPI_Transmit 的第三个参数是 uint16_t 类型,最大只能传 65535 字节。320x480 的 RGB565 全屏数据是 307200 字节,直接塞给 HAL_SPI_Transmit 会溢出,所以一定要分块发送:

void LCD_Clear(uint16_t color) { uint8_t buf[1024]; uint32_t total = 320 * 480; for (uint32_t i = 0; i < 1024; i += 2) { buf[i] = color >> 8; buf[i + 1] = color & 0xFF; } LCD_SetWindow(0, 0, 319, 479); LCD_CS_0(); LCD_DC_1(); for (uint32_t sent = 0; sent < total; sent += 512) { uint16_t len = 1024; if (sent + 512 > total) { len = (total - sent) * 2; } HAL_SPI_Transmit(&hspi1, buf, len, HAL_MAX_DELAY); } LCD_CS_1(); }

第一次写代码的人特别容易在len和像素数量之间把自己绕晕。记住一个原则:收发的都是“字节数”,一个 RGB565 像素占 2 字节,缓冲区 1024 字节等于 512 个像素。

3. 触摸屏 XPT2046:读坐标、滤波、校准

3.1 SPI 读坐标的基本姿势

XPT2046 的读取方式和显示器完全不同,它相当于一个挂在 SPI 上的 12 位 ADC。操作流程是:先拉低 TP_CS,发送一个控制字节,再读两个字节回来,拼成一个 12 位坐标值。

我用的控制字节是 0xD0 读 X、0x90 读 Y。如果你的屏读出来 X/Y 是反的,把这两个命令互换即可,这不是芯片坏了,只是模块布线定义不同。

uint16_t TP_Read(uint8_t cmd) { uint8_t tx[2] = {0x00, 0x00}; uint8_t rx[2] = {0x00, 0x00}; TP_CS_0(); HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY); HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, HAL_MAX_DELAY); TP_CS_1(); return ((uint16_t)(rx[0] << 8 | rx[1])) >> 3; }

注意返回值的>> 3。XPT2046 是 12 位 ADC,但 SPI 输出时数据一般是对齐在 15..4 位或者 11..0 位,不同资料写法不一样。我碰到的模块实测是“右移 3 位后落在 0-4095”,如果你的坐标范围明显不对,可以试试右移 4 位或者直接& 0x0FFF,以实际量程为准。

3.2 软件滤波与两点校准

直接读回来的一次原始值会很抖,尤其是手指轻轻搭在上面的时候,坐标值能跳动几十甚至上百。用中值滤波最省事:连续读 5 次,去掉最大和最小,剩下 3 次取平均值。

uint16_t TP_ReadAverage(uint8_t cmd) { uint16_t samples[5]; for (uint8_t i = 0; i < 5; i++) { samples[i] = TP_Read(cmd); } for (uint8_t i = 0; i < 4; i++) { for (uint8_t j = i + 1; j < 5; j++) { if (samples[j] < samples[i]) { uint16_t tmp = samples[i]; samples[i] = samples[j]; samples[j] = tmp; } } } return (samples[1] + samples[2] + samples[3]) / 3; }

原始 ADC 值还不能直接用,因为屏幕的物理坐标和 ADC 读数之间存在旋转、镜像和缩放关系。最简单的校准是两点校准:显示左上角十字,记录 raw_x1、raw_y1;显示右下角十字,记录 raw_x2、raw_y2。然后计算比例:

float kx = (float)(RIGHT_X - LEFT_X) / (raw_x2 - raw_x1); float offset_x = LEFT_X - raw_x1 * kx; float ky = (float)(BOTTOM_Y - TOP_Y) / (raw_y2 - raw_y1); float offset_y = TOP_Y - raw_y1 * ky;

使用时直接screen_x = raw_x * kx + offset_x。这套校准参数建议算完后存到 STM32F103 内部 Flash,否则每次断电重新校准很烦。STM32F103 的 Flash 操作本身不复杂,但要注意按页擦除,别把程序区擦没了。

3.3 判断触摸按下,避免一直刷屏

TP_IRQ 引脚在手指按下时会拉低,所以主循环里先判断这个引脚,再决定要不要读坐标。不要傻乎乎地每轮循环都去读 ADC,那样不仅 CPU 浪费,还会因为触摸芯片不断采样导致数字噪声。

while (1) { if (HAL_GPIO_ReadPin(TP_IRQ_GPIO_Port, TP_IRQ_Pin) == GPIO_PIN_RESET) { uint16_t rx = TP_ReadAverage(TP_CMD_X); uint16_t ry = TP_ReadAverage(TP_CMD_Y); uint16_t sx = (uint16_t)(rx * kx + offset_x); uint16_t sy = (uint16_t)(ry * ky + offset_y); LCD_DrawPixel(sx, sy, 0xF800); // 画红色点 HAL_Delay(10); } }

我自己实际测试下来,10ms 的延时不会影响画线连贯性,反而能避免一次按下读出很多重复点。如果想要更顺滑的手绘效果,可以把延时改成 5ms,但滤波还是要保留。

4. 工程整合、性能实测与常见问题排查

4.1 主循环与现有代码整合

整个工程不复杂,只要你用 CubeMX 生成的代码,再把lcd.clcd.htouch.ctouch.h加进去,主循环里按顺序调用ILI9486_Init()和触摸读取函数就行。注意一件事:CubeMX 生成后默认 SPI 初始化顺序在 GPIO 之前,你不需要手动改,main 函数里先调用MX_GPIO_Init()再调用MX_SPI1_Init()的顺序已经是正确的。

如果你想把显示内容做得更丰富,比如显示汉字,需要加字库和取模工具。我用的是常见的“PCtoLCD2002”取模,取模方向选“逐行式”,字体大小 16x16,然后把取模后的数组按同样方式喂给 ILI9486。取模方向如果和屏幕扫描方向不一致,汉字会“躺倒”或者乱码,这个不是驱动逻辑问题,而是方向匹配问题。

4.2 实测性能:可以接受,但别当高刷屏用

SPI 工作在 18MHz 时,理论速率 2.25MB/s。RGB565 全屏一帧数据是 320x480x2=307200 字节,理论全屏填色时间约 136ms,加上命令开销和函数调用,实测大概 150ms 一帧,相当于每秒 6-7 帧。

这个速度显示静态界面、按钮、仪表盘完全够用,但如果你要做动画、跑马灯这类对帧率敏感的效果,建议上 DMA 或者直接选带 FSMC 并口的高分屏。DMA 方案并不复杂,核心就是把HAL_SPI_Transmit换成HAL_SPI_Transmit_DMA,但一定要在 DMA 传输完成回调里拉高 CS,不能在启动 DMA 后立刻拉高,否则最后一段数据会被截掉。

4.3 常见问题速查表

现象大概率原因排查/解决办法
白屏或黑屏无反应RST 时序不对 / 背光没亮检查 PA0 是否输出高电平;RST 拉低至少 10ms 再拉高
花屏、有明显横条纹SPI 速率过高或杜邦线太长分频从 4 改成 8,即 9MHz;线长尽量控制在 10cm 内
画面上下颠倒/左右镜像0x36 扫描方向不对依次试 0x08、0x28、0x48、0x88
颜色偏红偏蓝或反色BGR 位不对0x36 的最后一位,也就是 bit3,取反重试
显示正常但触摸没反应TP_CS/IRQ 接错或 MISO 没接确认 PA6 接了触摸输出,PB0 是否有下拉变化
触摸坐标只在一个角落跳控制字节或右移位数不对换 0xD0/0x90,右移试试 3/4 位
HAL_SPI_Transmit 卡死SPI 通信异常或 timeout 太短timeout 直接用 HAL_MAX_DELAY,先排除硬件接线

最后再分享一个我自己的教训:一开始我把触摸检测接到了外部中断里,想在按下瞬间立刻读坐标,结果 SPI 中断和触摸中断相互干扰,偶尔读回 0xFFFF 或者坐标漂移。后来改成主循环轮询 TP_IRQ,不但代码更短,稳定性反而上来了。对于这种低速触摸屏,真的没必要强行用中断。

这个项目目前驱动部分已经很完整,后面如果你想扩展,可以往“多页面菜单 + 触摸按键”方向走,或者把 ESP8266/ESP32 串口接进来做远程数据显示。硬件基础都是同一套,核心就是先把 ILI9486 和 XPT2046 这两个芯片的时序吃透。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 22:18:17

Missile Datcom气动估算实战:从半经验公式到导弹设计应用

简介&#xff1a;这是一份用于导弹气动特性快速估算的软件工具资源&#xff0c;基于DATCOM方法&#xff0c;面向导弹设计、飞行性能分析相关工程师与学习者。压缩包内为MD_GUI_Ver_3.6.0_Portable便携版&#xff0c;共28个文件&#xff0c;包含可执行程序、数据文件&#xff08…

作者头像 李华
网站建设 2026/9/9 22:17:28

电力网格化运营指标体系与考核模型全解析

1. 电力网格化运营&#xff1a;一套指标体系解决的管理难题1.1 网格化管理为什么在电力行业火起来网格化运营这个词&#xff0c;在电力行业其实已经不算新鲜了&#xff0c;但真正把它做扎实、做出成效的&#xff0c;却远比想象中少。电网企业从过去的“按专业条线管设备”转向“…

作者头像 李华
网站建设 2026/9/9 22:17:26

大数据可视化实战:从渲染性能到数据链路与工程化落地

上个月帮一家公司排查数据可视化大屏卡顿的问题&#xff0c;打开浏览器控制台一看&#xff0c;三百多兆的JSON数据被直接塞进了ECharts的series数组里&#xff0c;页面白屏&#xff0c;浏览器直接崩溃。现场负责人还一脸无辜地跟我说&#xff1a;"后端已经把数据查出来了&…

作者头像 李华
网站建设 2026/9/9 22:17:22

布谷鸟算法结合电导增量法的光伏MPPT全局搜索与精调仿真

做光伏MPPT仿真的人&#xff0c;多少都遇到过这种尴尬&#xff1a;上午十点光照正好&#xff0c;系统却突然卡在一个低功率点不动了&#xff0c;示波器上功率曲线平得像心电图&#xff0c;明明旁边就有一个更高的峰值。传统电导增量法&#xff08;INC&#xff09;在均匀光照下很…

作者头像 李华
网站建设 2026/9/9 22:17:15

OLAP引擎演进与选型实战:从离线批处理到实时数仓

最近大数据圈子里&#xff0c;OLAP 绝对算得上高频词。不管是在社招面试里被问“你们公司的分析系统用的什么引擎”“离线数仓和实时数仓怎么衔接”&#xff0c;还是公司内部那堆每天要跑的报表、管理层要看的数据大屏、运营那边随时甩过来的多维分析需求&#xff0c;背后其实都…

作者头像 李华