ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

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

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 接 PA3T_IRQ 接 PB0逻辑完全一样。1.3 CubeMX 配置要点工程我是在 STM32CubeMX 里生成的工具链用 STM32CubeIDE 或者 Keil 都能编译关键配置就几处RCC选择 HSE 外部晶振我板子上是 8MHz 晶振时钟树配置 PLL 到 72MHz。SYSDebug 选 Serial Wire否则烧录一次后 SWD 口容易被占用。SPI1配置为 Full-Duplex Master波特率预分频选 4。因为 APB2 总线时钟是 72MHz72/418MHz这是 F103 的 SPI 极限。SPI1 参数CPOLLowCPHA1Edge模式 08 位数据MSB First关闭硬件 NSS。GPIO 输出脚 PA0-PA4 配成 Push-Pull Output初始电平建议CS 脚高电平、RST 脚高电平、BL 脚高电平。PB0 配成 GPIO_Input内部上拉。这样不按下时读到高电平按下时被拉低。这里解释一下为什么不用硬件 NSSSTM32 的硬件 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); // RGB56516bit 像素格式 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.c、lcd.h、touch.c、touch.h加进去主循环里按顺序调用ILI9486_Init()和触摸读取函数就行。注意一件事CubeMX 生成后默认 SPI 初始化顺序在 GPIO 之前你不需要手动改main 函数里先调用MX_GPIO_Init()再调用MX_SPI1_Init()的顺序已经是正确的。如果你想把显示内容做得更丰富比如显示汉字需要加字库和取模工具。我用的是常见的“PCtoLCD2002”取模取模方向选“逐行式”字体大小 16x16然后把取模后的数组按同样方式喂给 ILI9486。取模方向如果和屏幕扫描方向不一致汉字会“躺倒”或者乱码这个不是驱动逻辑问题而是方向匹配问题。4.2 实测性能可以接受但别当高刷屏用SPI 工作在 18MHz 时理论速率 2.25MB/s。RGB565 全屏一帧数据是 320x480x2307200 字节理论全屏填色时间约 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 这两个芯片的时序吃透。本文还有配套的精品资源点击获取
返回列表