F103驱动ILI9486并口屏:FSMC/GPIO与XPT2046校准 简介这份完整工程面向STM32嵌入式初学者、单片机开发者及需要为小型设备加装显示屏的工程人员基于STM32F103的HAL库驱动3.5英寸ILI9486 TFT触摸屏代码可直接烧录验证。内容覆盖SPI与GPIO引脚配置、LCD初始化、清屏、画点画线、文本与图像显示以及配合XPT2046触摸控制器读取并转换坐标能够帮助读者理解硬件抽象层的外设交互与中景园驱动移植方法。压缩包共153个文件、约4.77MB内含51个头文件、21个源文件以及可烧录的十六进制文件、CubeMX配置文件、Keil工程文件与编译映射文件目录结构便于按模块查阅和二次开发。已有1705人学习适合用作屏幕驱动调试与工程配置的对照样本。读者可据此掌握显示控制、触摸事件处理和底层通信排错思路并以此为起点扩展图形界面与人机交互设计。1. 3.5 寸 ILI9486 屏接到 F103先判断是 FSMC 并口还是 GPIO 模拟把一块 3.5 寸 ILI9486 并口屏接到 STM32F103 上最先撞到的不是驱动命令而是引脚和总线F103C8T6 没有 FSMC16 位并口根本铺不开F103VET6 有 FSMC但 A16 当地址线、NE1 当片选、时序配错一个参数就白屏。这套 HAL 库代码解决的是“屏幕能亮、触摸能用、工程能直接烧录”三件事适合做嵌入式硬件原型的单片机开发者也适合拿 ARM 内核练外设的学生。它不是屏幕厂家的通用手册而是一份把 ILI9486 初始化、FSMC 并口刷屏、XPT2046 触摸校准串起来的可复现工程。拿到代码包先别急着编译先看手头板子的型号和屏幕排针定义再决定走 16 位 FSMC 还是 8 位 GPIO 模拟这一步选错后面调一周都点不亮。2. 硬件接线与并口选型16 位 FSMC 和 8 位模拟的引脚账2.1 3.5 寸 ILI9486 的接口形态与 F103 的引脚预算3.5 寸 ILI9486 模块常见形态是 320×480 分辨率、16 位并口数据线、带 XPT2046 电阻触摸、自带 3.3V 稳压和背光升压。排针上一般有 D0-D15、CS、RS、WR、RD、RST、BL触摸侧单独引出 T_CS、T_CLK、T_MOSI、T_MISO、T_IRQ。别小看这些线16 位并口要占 16 个数据 IO加上 5 根控制线和 5 根触摸线至少 26 个可用 IO还得尽量落在同一个 GPIO 端口或 FSMC 专用引脚上否则写总线时要在多个端口之间拼字节时序会碎。F103C8T6 这类 48 脚芯片可用 IO 看起来有三十多个但真正能连续拼成 16 位数据口的没有且没有 FSMC 外设。用 GPIO 模拟 16 位并口不是不能写问题是每个像素要翻转 WR 两次、拼两次端口320×480 全屏刷下来接近秒级做静态界面勉强做触摸画线会明显拖影。F103VET6、F103ZET6 这类 100 脚以上的型号才有 FSMC数据线、NE、NWE、NOE 都是专用引脚写命令和写数据变成一次内存访问刷屏速度完全不是一个量级。芯片/板型FSMC可用 IO 余量建议接法实际体验F103C8T6 等 48 脚无紧张8 位 GPIO 模拟并口静态 UI 可用刷新慢F103RCT6 等 64 脚无一般8 位模拟或 SPI 屏不建议硬上 16 位F103VET6 等 100 脚有充足16 位 FSMC 并口推荐刷屏稳定F103ZET6 等 144 脚有很充足16 位 FSMC 并口接线余量大这张表不是绝对边界但能挡掉大部分“为什么我的 C8T6 接 16 位屏不亮”的问题。代码包默认按 16 位 FSMC 并口组织因为 ILI9486 在 16 位模式下写一个像素就是一次 16 位数据写窗口设好后地址自增连续写颜色最顺。手里的板子如果是 C8T6不要直接烧 16 位配置先把数据口改成 8 位模拟分支或者换一块带 FSMC 的板子再验证屏幕本身。2.2 FSMC 并口接法与时序参数以 F103VET6 为例16 位 FSMC 的典型接线是LCD_D0-D15 接 FSMC_D0-D15LCD_CS 接 FSMC_NE1LCD_RS 接 FSMC_A16LCD_WR 接 FSMC_NWELCD_RD 接 FSMC_NOELCD_RST 单独接一个普通 GPIO背光 BL 接 3.3V 或 PWM。RS 接 A16 的意思是访问 0x60000000 时 A16 为低写命令访问 0x60020000 时 A16 为高写数据。地址偏移不是随便写的16 位总线下一个地址对应两个字节A16 对应偏移 116 再乘 2得到 0x20000。/* FSMC 地址映射A16 接 RSNE1 接 CS */ #define LCD_CMD_ADDR ((volatile uint16_t *)0x60000000U) #define LCD_DATA_ADDR ((volatile uint16_t *)0x60020000U) static inline void LCD_WriteCmd(uint16_t cmd) { *LCD_CMD_ADDR cmd; /* A160命令 */ } static inline void LCD_WriteData(uint16_t data) { *LCD_DATA_ADDR data; /* A161数据 */ }上面三个宏和两个函数是整份代码的地基。volatile不能省否则编译器可能把连续写数据优化掉uint16_t指针保证一次写 16 位和 FSMC 数据宽度对齐。如果实际接线把 RS 接到了 A17 或其他地址线LCD_DATA_ADDR的偏移要跟着改不能照抄。写命令和写数据之间不需要手动翻转 CSNE1 由 FSMC 自动拉低这也是并口比 GPIO 模拟省心的地方。FSMC 时序先给一组保守值地址建立时间 2 个 HCLK地址保持 1 个 HCLK数据建立 3 个 HCLK总线恢复 0时钟分频 0数据延迟 0。72MHz 下 1 个 HCLK 约 13.9ns这组值对应几十纳秒级ILI9486 一般能吃住。如果屏幕白屏或偶发花屏先把数据建立时间加到 5地址建立加到 3确认能亮后再往下压。血泪经验是时序不是越快越好杜邦线一长建立时间不够就是随机花屏查半天以为是初始化命令错。2.3 GPIO 模拟并口的接线与速率取舍没有 FSMC 的板子只能走 8 位模拟并口。ILI9486 支持 8 位 MCU 接口但写一个 16 位颜色要拆成高字节和低字节两次每次都要拉低 WR 再拉高。数据口最好选同一个 GPIO 端口的低 8 位比如 GPIOB 的 PB0-PB7这样一条ODR赋值就能出 8 位数据不用逐位HAL_GPIO_WritePin。控制线单独分配RS、WR、RD、CS、RST 各占一个脚RD 在只写场景可以直接接 3.3V 省一个 IO但读 ID 时就没法用。/* 8 位模拟并口示例数据口 PB0-PB7控制线按实际板子改 */ #define LCD_DATA_PORT GPIOB #define LCD_DATA_MASK 0x00FFU #define LCD_RS_PIN GPIO_PIN_8 #define LCD_WR_PIN GPIO_PIN_9 #define LCD_CS_PIN GPIO_PIN_10 #define LCD_RST_PIN GPIO_PIN_11 #define LCD_RS_PORT GPIOA #define LCD_WR_PORT GPIOA #define LCD_CS_PORT GPIOA #define LCD_RST_PORT GPIOA static inline void LCD_BusWrite(uint8_t dat) { LCD_DATA_PORT-ODR (LCD_DATA_PORT-ODR ~LCD_DATA_MASK) | dat; LCD_WR_PORT-BRR LCD_WR_PIN; /* WR 低 */ __NOP(); __NOP(); /* 短延时按主频和线长调 */ LCD_WR_PORT-BSRR LCD_WR_PIN; /* WR 高 */ } static inline void LCD_WriteCmd8(uint8_t cmd) { LCD_RS_PORT-BRR LCD_RS_PIN; /* RS0 */ LCD_CS_PORT-BRR LCD_CS_PIN; /* CS0 */ LCD_BusWrite(cmd); LCD_CS_PORT-BSRR LCD_CS_PIN; } static inline void LCD_WriteData8(uint8_t dat) { LCD_RS_PORT-BSRR LCD_RS_PIN; /* RS1 */ LCD_CS_PORT-BRR LCD_CS_PIN; LCD_BusWrite(dat); LCD_CS_PORT-BSRR LCD_CS_PIN; } static inline void LCD_WriteData16(uint16_t dat) { LCD_WriteData8(dat 8); /* 高字节先写 */ LCD_WriteData8(dat 0xFF); /* 低字节后写 */ }这段代码的关键在LCD_BusWrite先把 8 位数据整体写到ODR再拉 WR 低、短延时、拉高。__NOP()的数量取决于主频和排线长度72MHz 下两三个 NOP 往往够用线长超过 10cm 就加到五个。LCD_WriteData16的高字节在前还是低字节在前要看 ILI9486 在 8 位模式下的像素格式设置常见是0x3A写0x55后按高字节、低字节顺序。写反了颜色会整体偏色不是屏幕坏。8 位模拟的刷屏时间可以粗算一个像素两次字节写每次写约 1-2 微秒320×480 全屏 150-300ms所以它适合菜单、参数页不适合视频和高速波形。2.4 触摸 XPT2046 的 SPI 接线与电气注意触摸芯片 XPT2046 是电阻屏常用方案四线 SPI支持 12 位采样。接线一般是 T_CS 接普通 GPIOT_CLK 接 SPI_SCKT_MOSI 接 SPI_MOSIT_MISO 接 SPI_MISOT_IRQ 可以不接用轮询判断按下。XPT2046 供电 3.3V通信电平也是 3.3V直接接 F103 的 SPI 引脚没问题。SPI 模式用模式 0时钟先降到 1MHz 到 2MHz别一上来就 18MHz电阻屏的采样保持时间不够读出来会跳。触摸引脚接到 F103说明T_CS任意 GPIO软件片选低有效T_CLKSPI_SCK模式 0空闲低T_MOSISPI_MOSI主机输出T_MISOSPI_MISO主机输入T_IRQ可不接轮询时不需要/* XPT2046 读 12 位原始值命令字节0xD0 读 X0x90 读 Y */ static uint16_t TP_ReadRaw(uint8_t cmd) { uint8_t tx[3] {cmd, 0x00, 0x00}; uint8_t rx[3] {0}; HAL_GPIO_WritePin(TP_CS_PORT, TP_CS_PIN, GPIO_PIN_RESET); HAL_SPI_TransmitReceive(hspi1, tx, rx, 3, 20); HAL_GPIO_WritePin(TP_CS_PORT, TP_CS_PIN, GPIO_PIN_SET); return (uint16_t)(((rx[1] 8) | rx[2]) 3); /* 12 位有效 */ }TP_ReadRaw发三个字节第一个字节是命令后两个字节时钟把转换结果移出来。rx[1]和rx[2]拼成 16 位后右移 3 位得到 12 位采样值。命令字节里0xD0对应 X 通道、12 位、差分模式0x90对应 Y 通道具体通道定义以 XPT2046 手册为准。片选拉低后不要立刻发数据有些模块需要几个微秒建立HAL 的 SPI 函数本身有开销通常够。如果读出来一直是 0 或 4095先查 CS 和 MISO 有没有接反再查 SPI 模式是不是 0。3. HAL 工程搭建从 CubeMX 时钟到第一帧 320×480 刷屏3.1 时钟、FSMC 与 GPIO 的 CubeMX 配置新建工程选 F103VET6RCC 里 HSE 选晶振Clock Configuration 把 SYSCLK 拉到 72MHzHCLK 72MHzAPB1 36MHzAPB2 72MHz。FSMC 挂在 AHB 上时钟就是 72MHz。SYS 里 Debug 选 Serial Wire别把 PA13、PA14 占掉。FSMC 选 NOR/SRAM Bank1片选 NE1数据宽度 16 位地址线只开 A16其他地址线不用开。时序参数按上一章的保守值填先能亮再优化。GPIO 里把 LCD_RST 和背光 BL 配成输出触摸 CS 配成输出SPI1 配成主机、模式 0、8 位、软件片选。初始化顺序有讲究先时钟再 GPIO再 FSMC最后 SPI。屏幕复位要在 FSMC 配置完成后做否则复位期间总线状态不确定。下面这段是主循环前的骨架MX_xxx_Init的名字按 CubeMX 生成的实际函数名替换。int main(void) { HAL_Init(); SystemClock_Config(); /* 72MHz */ MX_GPIO_Init(); MX_FSMC_Init(); /* 16 位并口 */ MX_SPI1_Init(); /* XPT2046 */ LCD_RST_Low(); HAL_Delay(20); LCD_RST_High(); HAL_Delay(120); /* 复位后等稳定 */ LCD_Init(); LCD_Fill(0, 0, 319, 479, 0xFFFF); HAL_Delay(200); LCD_Fill(0, 0, 319, 479, 0xF800); while (1) { /* 触摸和界面逻辑 */ } }HAL_Delay(120)不是摆设ILI9486 复位后内部电源和振荡器需要时间短了会出现初始化命令写进去但显示异常。LCD_Fill先刷白再刷红是为了看清窗口设置和颜色格式是不是对。如果白屏但背光亮先查复位和 CS如果刷红显示成蓝色查0x36的 BGR 位和0x3A的像素格式。3.2 ILI9486 初始化命令序列ILI9486 的初始化序列比小尺寸屏长电源控制、伽马、帧率、显示功能都要配。下面这组是常见能点亮 320×480 模块的序列不同排针和屏厂可能有小差异但大框架一致。写命令用LCD_WriteCmd写参数用LCD_WriteData每个命令后的延时按注释保留。static void LCD_Init(void) { LCD_WriteCmd(0x01); /* 软复位 */ HAL_Delay(120); LCD_WriteCmd(0x28); /* 显示关 */ LCD_WriteCmd(0x3A); /* 像素格式 */ LCD_WriteData(0x55); /* 16 位 RGB565 */ LCD_WriteCmd(0xC0); /* 电源控制 1 */ LCD_WriteData(0x0F); LCD_WriteData(0x0F); LCD_WriteCmd(0xC1); /* 电源控制 2 */ LCD_WriteData(0x41); LCD_WriteCmd(0xC5); /* VCOM 控制 */ LCD_WriteData(0x00); LCD_WriteData(0x3C); LCD_WriteData(0x00); LCD_WriteCmd(0x36); /* 存储访问控制 */ LCD_WriteData(0x48); /* 横屏BGR 位按实际屏调 */ LCD_WriteCmd(0xB0); /* 接口模式 */ LCD_WriteData(0x00); LCD_WriteCmd(0xB1); /* 帧率 */ LCD_WriteData(0xA0); LCD_WriteCmd(0xB4); /* 显示反转 */ LCD_WriteData(0x00); LCD_WriteCmd(0xB6); /* 显示功能 */ LCD_WriteData(0x02); LCD_WriteData(0x22); LCD_WriteData(0x3B); LCD_WriteCmd(0xB7); /* 入口模式 */ LCD_WriteData(0x06); LCD_WriteCmd(0xE0); /* 正伽马 */ { const uint8_t gp[15] {0x0F,0x1F,0x1C,0x0C,0x0F,0x08,0x48,0x98, 0x37,0x0A,0x13,0x04,0x11,0x0D,0x00}; for (uint8_t i 0; i 15; i) LCD_WriteData(gp[i]); } LCD_WriteCmd(0xE1); /* 负伽马 */ { const uint8_t gn[15] {0x0F,0x32,0x2E,0x0B,0x0D,0x05,0x47,0x75, 0x37,0x06,0x10,0x03,0x24,0x20,0x00}; for (uint8_t i 0; i 15; i) LCD_WriteData(gn[i]); } LCD_WriteCmd(0x11); /* 退出睡眠 */ HAL_Delay(120); LCD_WriteCmd(0x29); /* 开显示 */ HAL_Delay(50); }0x3A写0x55是 RGB565写0x66是 RGB666前者一个像素 16 位后者 18 位并口接线和刷屏函数都要跟着改。0x36的0x48是横屏加 BGR 位如果显示红蓝反了把0x48改成0x40或0x08试别急着重焊数据线。伽马数组影响灰阶过渡不是点不亮的主因但颜色发灰、暗部糊成一片时值得换一组。0x11后的 120ms 和0x29后的 50ms 建议保留尤其是模块电源上升慢的时候。3.3 窗口设置与像素写入ILI9486 显示靠窗口加连续写。0x2A设列地址0x2B设行地址0x2C开始写显存。窗口设好后每写一个 16 位数据列地址自动加到右边界后回左边界、行加一。刷矩形时只用设一次窗口然后连续写w*h个颜色速度最快。下面三个函数是后面所有界面的基础。static 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); } static void LCD_Fill(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { uint32_t n (uint32_t)(x1 - x0 1) * (y1 - y0 1); LCD_SetWindow(x0, y0, x1, y1); while (n--) { LCD_WriteData(color); } } static void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color) { LCD_SetWindow(x, y, x, y); LCD_WriteData(color); }LCD_SetWindow的坐标是闭区间x1、y1要传右下角实际坐标不是宽高。LCD_Fill里先算像素总数再循环避免在循环里反复算乘法。LCD_DrawPixel每次都设窗口画单点没问题画线时不要逐点调用否则每点三次命令加四次数据速度会崩。画线应该先算包围盒设一次窗口再用 Bresenham 在窗口内写点或者直接写显存偏移。LCD_WriteData在 FSMC 下是内存写循环展开后能到十几纳秒一次GPIO 模拟下每次都要拉 WR差距明显。3.4 刷屏验证三色测试与坐标方向确认第一帧不要急着上界面先做三色测试和边框。全屏刷红、绿、蓝每次停 500ms看颜色纯不纯、有没有纵向条纹、边缘有没有缺列。然后刷白底画一个 1 像素边框和十字线确认 0,0 在左上角还是右下角。0x36的横屏、竖屏、镜像位不同坐标原点会跑边框能最快暴露方向问题。static void LCD_TestPattern(void) { LCD_Fill(0, 0, 319, 479, 0xF800); HAL_Delay(500); /* 红 */ LCD_Fill(0, 0, 319, 479, 0x07E0); HAL_Delay(500); /* 绿 */ LCD_Fill(0, 0, 319, 479, 0x001F); HAL_Delay(500); /* 蓝 */ LCD_Fill(0, 0, 319, 479, 0xFFFF); /* 边框 */ LCD_Fill(0, 0, 319, 1, 0x0000); LCD_Fill(0, 478, 319, 479, 0x0000); LCD_Fill(0, 0, 1, 479, 0x0000); LCD_Fill(318, 0, 319, 479, 0x0000); /* 中心十字 */ LCD_Fill(158, 0, 161, 479, 0x0000); LCD_Fill(0, 238, 319, 241, 0x0000); }如果红色显示成蓝色先改0x36的 BGR 位不要先怀疑数据线。如果边框左右各缺一列或上下各缺一行查窗口坐标是不是写成了开区间x1传了 320 而不是 319。如果整屏有规律斜纹多半是 FSMC 数据建立时间不够或者杜邦线太长。三色测试通过后再跑触摸屏幕和触摸分开验证出问题才能定位到是哪一边。4. 触摸接入XPT2046 采样、滤波与四点校准的落地参数4.1 XPT2046 SPI 时序与通道选择XPT2046 的命令字节决定通道和模式。常见读 X 用0xD0读 Y 用0x90读压力用0xB0。这三个命令都是 12 位、差分、掉电模式关闭。SPI 每次发三个字节第一个字节命令后两个字节收数据。片选拉低后先发命令再收两个字节最后拼成 12 位。读压力不是为了算牛顿而是判断按得重不重用来过滤轻触抖动。#define TP_CMD_X 0xD0 #define TP_CMD_Y 0x90 #define TP_CMD_Z 0xB0 static uint16_t TP_ReadX(void) { return TP_ReadRaw(TP_CMD_X); } static uint16_t TP_ReadY(void) { return TP_ReadRaw(TP_CMD_Y); } static uint16_t TP_ReadZ(void) { return TP_ReadRaw(TP_CMD_Z); }TP_ReadRaw里片选拉低后立刻调用HAL_SPI_TransmitReceiveHAL 库内部会处理时钟相位。SPI 模式 0 是 CPOL0、CPHA0XPT2046 在时钟上升沿采样。如果读出来数值在 0 和 4095 之间随机跳先把 SPI 时钟从 18MHz 降到 2MHz再查 MISO 有没有上拉。有些模块的触摸排线很长MISO 悬空时会读到 0xFF看起来像 4095。4.2 原始坐标滤波与按下判定电阻屏原始值抖动是常态直接拿单次采样去映射画线会像锯齿。常见做法是连采 5 次去掉最大最小剩下的取平均。按下判定不要只看 X 是否在范围内还要看 Z 压力是否超过阈值。轻触时 Z 值可能接近 0松手瞬间 X/Y 会跳到边界值这就是“松手跳点”的来源。滤波窗口别开太大5 次采样在 2MHz SPI 下大约几百微秒开 15 次会明显拖慢触摸响应。typedef struct { uint16_t x; uint16_t y; uint8_t pressed; } TouchPoint; static uint16_t TP_Median5(uint16_t v[5]) { /* 简单冒泡排序5 个数开销可接受 */ for (int i 0; i 4; i) { for (int j 0; j 4 - i; j) { if (v[j] v[j 1]) { uint16_t t v[j]; v[j] v[j 1]; v[j 1] t; } } } return v[2]; } static TouchPoint TP_Scan(void) { TouchPoint p {0, 0, 0}; uint16_t xs[5], ys[5], zs[5]; for (int i 0; i 5; i) { xs[i] TP_ReadX(); ys[i] TP_ReadY(); zs[i] TP_ReadZ(); } uint16_t z TP_Median5(zs); if (z 200) { /* 压力阈值按模块调 */ return p; } p.x TP_Median5(xs); p.y TP_Median5(ys); p.pressed 1; return p; }TP_Median5取中位数比平均值更能扛单次野值。压力阈值200不是固定值不同模块的 Z 通道范围差很多实际调法是手指轻按屏幕串口打印 Z记录按下和松开的数值取中间偏上的值。如果阈值太低松手后还会持续报点太高轻按不响应。TP_Scan每次读 5 组响应时间大约几毫秒做按钮和画线够用。4.3 四点校准与坐标映射原始坐标和屏幕坐标不是简单比例关系XPT2046 的安装方向、屏幕旋转、排线顺序都会让轴反向。四点校准取屏幕四角对应的原始值分别算 X 轴和 Y 轴的斜率、截距。如果 X 轴反向斜率会是负数公式照样成立。校准数据存到结构体里上电从 Flash 读或每次开机重新采。typedef struct { float kx, bx; float ky, by; uint8_t swap_xy; } TouchCal; static void Touch_Calibrate(TouchCal *cal, uint16_t raw_x0, uint16_t raw_y0, uint16_t raw_x1, uint16_t raw_y1, uint16_t lcd_x0, uint16_t lcd_y0, uint16_t lcd_x1, uint16_t lcd_y1) { cal-kx (float)(lcd_x1 - lcd_x0) / (float)(raw_x1 - raw_x0); cal-bx (float)lcd_x0 - cal-kx * raw_x0; cal-ky (float)(lcd_y1 - lcd_y0) / (float)(raw_y1 - raw_y0); cal-by (float)lcd_y0 - cal-ky * raw_y0; cal-swap_xy 0; } static void Touch_Map(const TouchCal *cal, uint16_t raw_x, uint16_t raw_y, uint16_t *lcd_x, uint16_t *lcd_y) { float x cal-kx * raw_x cal-bx; float y cal-ky * raw_y cal-by; if (x 0) x 0; if (x 319) x 319; if (y 0) y 0; if (y 479) y 479; *lcd_x (uint16_t)x; *lcd_y (uint16_t)y; }校准取点建议用屏幕四角向内缩 20 像素避免边缘死区。raw_x0、raw_y0是左上角对应的原始值raw_x1、raw_y1是右下角对应的原始值。如果发现 X 和 Y 对调把swap_xy置 1在Touch_Map里交换 raw_x 和 raw_y 再算。kx、ky算出来是小数F103 没有 FPU浮点运算靠软件库一次映射几微秒画线时能接受如果嫌慢可以改成定点数把斜率乘 1024 存成整数。4.4 触摸与显示联调画线、按钮联调先做画点触摸哪里屏幕哪里画一个 3×3 的红点。点能跟手说明校准和方向对了。再做画线记录上一个点两点之间用 Bresenham 连线线宽 2 像素。最后做按钮在屏幕上画矩形判断触摸点是否落在矩形内按下变色。注意画线时不要每个点都LCD_SetWindow先把线段的包围盒设成窗口再在窗口内写点速度会好很多。static void Touch_DrawDemo(void) { static uint16_t last_x 0, last_y 0; static uint8_t has_last 0; TouchPoint p TP_Scan(); if (!p.pressed) { has_last 0; return; } uint16_t x, y; Touch_Map(g_touch_cal, p.x, p.y, x, y); if (has_last) { /* 简化两点间画粗线实际可换 Bresenham */ int16_t x0 last_x, y0 last_y; int16_t dx (x x0) ? (x - x0) : (x0 - x); int16_t dy (y y0) ? (y - y0) : (y0 - y); int16_t steps (dx dy) ? dx : dy; for (int16_t i 0; i steps; i) { int16_t px x0 (x - x0) * i / (steps ? steps : 1); int16_t py y0 (y - y0) * i / (steps ? steps : 1); LCD_Fill(px - 1, py - 1, px 1, py 1, 0xF800); } } last_x x; last_y y; has_last 1; }has_last用来处理松手再按下的断笔不加会把上一次的点连到新位置。steps为 0 时除法要保护否则单点会除零。LCD_Fill每次画 3×3 小块频繁设窗口仍然慢真正画线应该把整条线的包围盒设一次窗口再用显存指针按行写。这个 Demo 的目的是验证触摸坐标不是做绘图板。按钮判断更简单按下时取 x、y和矩形x0,y0,x1,y1比较落在里面就把按钮区域刷成按下色松手刷回。5. 避坑与排查白屏、花屏、触摸漂移的 5 个现场记录5.1 现象上电白屏背光亮但没有任何图像现象很典型背光升压工作屏幕白得均匀但三色测试不出颜色串口打印初始化已经跑完。原因通常在三类复位时序不对、CS/RS 接错、FSMC 时序太快。ILI9486 复位低电平至少保持 10ms拉高后等 120ms 再发命令有些模块电源上升慢120ms 还不够加到 200ms 也不亏。CS 如果接到 NE1 以外的片选地址映射全错写命令写数据都落到别的 BankRS 如果接到 A16 以外的地址线命令和数据会串。排查顺序先用万用表查 CS 是否在初始化时被拉低RS 在写命令时是否为低再把 FSMC 数据建立时间从 3 加到 6地址建立从 2 加到 4最后把LCD_WriteCmd(0x01)后的延时加到 200ms。如果读 ID 能读到0x94、0x86之类的值说明总线通了问题在初始化序列读 ID 全 0 或全 FF先查数据线和片选。血泪经验是白屏不一定是屏坏九成是复位或片选。5.2 现象花屏、颜色偏蓝、红蓝反花屏分两种随机雪花点和规律斜纹。随机雪花多半是数据线接触不良或 FSMC 时序临界杜邦线越长越明显规律斜纹是时序参数固定偏快降数据建立时间能好。颜色偏蓝、红蓝反先看0x36的 BGR 位0x48和0x40差一位显示效果完全反过来。如果红色显示成蓝色但边框位置正确基本就是 BGR 位不用动数据线。还有一种花屏是 16 位数据线高低字节接反写0xF800红色屏幕显示成0x00F8的蓝色调。查法刷纯红、纯绿、纯蓝看哪个通道错位。解决要么改接线要么在LCD_WriteData里交换高低字节但交换字节会拖慢刷屏能改线就改线。0x3A写成0x55才是 16 位写成0x66后按 16 位发数据会颜色错乱这个坑在换初始化序列时容易带进来。5.3 现象触摸点漂移、松手跳点触摸漂移先看 SPI 时钟。XPT2046 在 18MHz 下不是所有模块都能稳定采样降到 2MHz 往往立刻变稳。再看滤波单次采样直接用画线必然毛刺5 次中值加平均能压住大部分抖动。松手跳点是因为按下判定只看坐标范围松手瞬间 X/Y 会掉到 0 或 4095加 Z 压力阈值后基本消失。校准也有关系。只做两点校准屏幕边缘会偏四点校准取四角向内 20 像素斜率更准。如果 X 轴越往右越偏检查kx是不是负数被截断成 0浮点转整型时先判断范围。还有一点触摸排线和 LCD 排线不要捆在一起SPI 时钟串到并口数据线上读出来的原始值会周期性跳分开走线或加地线隔离。5.4 现象F103C8T6 接 16 位并口不亮C8T6 没有 FSMC代码里如果直接编译 16 位 FSMC 版本地址写 0x60000000 不会产生总线动作屏幕当然不亮。有些开发者用 GPIO 模拟 16 位把 D0-D15 拆到 GPIOA 和 GPIOB 两个端口写一个像素要拼两次端口、翻转两次 WR不仅慢还容易因为端口间写时序不一致出现花屏。C8T6 的 IO 也不够同时接 16 位数据、5 根控制线和触摸 SPI硬挤会占用调试口和晶振脚。解决要么换 F103VET6 这类带 FSMC 的板子要么改 8 位模拟并口数据口选同一个端口的低 8 位控制线单独放。8 位模式下初始化要发0x3A 0x55写颜色分高低字节刷屏慢但能显示。不要相信“C8T6 超频到 128MHz 就能模拟 16 位”的说法IO 翻转极限摆在那超频只会让时序更离散。5.5 现象刷屏慢到 1 秒以上全屏刷新超过 1 秒先看是不是走了 GPIO 模拟逐位写。逐位HAL_GPIO_WritePin写 16 位数据要 16 次函数调用再加 WR 翻转一个像素几十微秒全屏自然秒级。改成端口ODR整体写、WR 用BSRR/BRR翻转能快一个数量级。还是慢就看是不是每画一个点都LCD_SetWindow设窗口要写 10 个命令/数据比写一个像素还贵。第三个常见原因是循环里加了HAL_Delay或串口打印。刷屏函数里不要打日志串口 115200 下打印一行就几毫秒。第四个原因是 FSMC 时序过于保守数据建立时间加到 15每个像素多等一百多纳秒全屏多出几十毫秒。先把时序降到能稳定显示的最低值再用连续写窗口的方式刷320×480 全屏在 FSMC 下可以做到几十毫秒级。6. 进阶压榨把 320×480 全屏刷新压到 20 ms 的脏矩形做法6.1 脏矩形与连续写F103 的 RAM 只有几十 KB320×480×2 字节需要 300KB全屏双缓冲不现实。能做的就是脏矩形界面只刷新变化区域比如按钮按下只重绘按钮矩形数值变化只重绘数字区域。实现上维护一个dirty_x0/y0/x1/y1所有绘图函数不直接写屏而是把坐标并入脏矩形主循环每 16ms 取一次脏矩形设一次窗口连续写颜色。这样全屏刷新只在开机时发生平时只刷小块20ms 以内很轻松。做法320×480 全屏耗时适用场景GPIO 逐位模拟800ms 以上不推荐GPIO 8 位端口模拟150-300ms静态页FSMC 16 位连续写20-50ms推荐脏矩形局部刷新1-10ms交互界面验证方法很直接在LCD_Fill前后翻转一个空闲 GPIO用逻辑分析仪看高电平宽度或者用HAL_GetTick打时间戳串口打印一次全屏耗时。别用示波器看单次 WR本文还有配套的精品资源点击获取