news 2026/10/26 18:50:00

F103驱动ILI9486并口屏:FSMC/GPIO与XPT2046校准

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 没有 FSMC,16 位并口根本铺不开;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-D15,LCD_CS 接 FSMC_NE1,LCD_RS 接 FSMC_A16,LCD_WR 接 FSMC_NWE,LCD_RD 接 FSMC_NOE,LCD_RST 单独接一个普通 GPIO,背光 BL 接 3.3V 或 PWM。RS 接 A16 的意思是:访问 0x60000000 时 A16 为低,写命令;访问 0x60020000 时 A16 为高,写数据。地址偏移不是随便写的,16 位总线下一个地址对应两个字节,A16 对应偏移 1<<16 再乘 2,得到 0x20000。

/* FSMC 地址映射:A16 接 RS,NE1 接 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; /* A16=0,命令 */ } static inline void LCD_WriteData(uint16_t data) { *LCD_DATA_ADDR = data; /* A16=1,数据 */ }

上面三个宏和两个函数是整份代码的地基。volatile不能省,否则编译器可能把连续写数据优化掉;uint16_t指针保证一次写 16 位,和 FSMC 数据宽度对齐。如果实际接线把 RS 接到了 A17 或其他地址线,LCD_DATA_ADDR的偏移要跟着改,不能照抄。写命令和写数据之间不需要手动翻转 CS,NE1 由 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; /* RS=0 */ LCD_CS_PORT->BRR = LCD_CS_PIN; /* CS=0 */ 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; /* RS=1 */ 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 接普通 GPIO,T_CLK 接 SPI_SCK,T_MOSI 接 SPI_MOSI,T_MISO 接 SPI_MISO,T_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 读 X,0x90 读 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 配置

新建工程选 F103VET6,RCC 里 HSE 选晶振,Clock Configuration 把 SYSCLK 拉到 72MHz,HCLK 72MHz,APB1 36MHz,APB2 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_TransmitReceive,HAL 库内部会处理时钟相位。SPI 模式 0 是 CPOL=0、CPHA=0,XPT2046 在时钟上升沿采样。如果读出来数值在 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 以外的片选,地址映射全错,写命令写数据都落到别的 Bank;RS 如果接到 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 只有几十 KB,320×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

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

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

高效纯净文本编辑器:提升编程与写作效率的利器

1. 为什么我们需要一款纯粹的文本编辑器&#xff1f;在这个信息爆炸的时代&#xff0c;我们每天都要处理大量的文字工作。从程序员编写代码&#xff0c;到作家创作内容&#xff0c;再到学生整理笔记&#xff0c;一个高效、纯粹的文本编辑器能极大提升工作效率。但市面上大多数编…

作者头像 李华
网站建设 2026/10/26 18:46:58

STM32+HX711电子秤设计:信号链隔离与分段校准实战

简介&#xff1a;本资源是一套基于STM32单片机的电子秤完整嵌入式开发项目&#xff0c;面向嵌入式初学者、电子设计爱好者及高校课程实践者&#xff0c;解决高精度称重系统从硬件搭建到软件实现的一站式学习需求。压缩包共225个文件&#xff0c;涵盖35个C源码&#xff08;含HX7…

作者头像 李华
网站建设 2026/10/26 18:46:48

HTML语义化标签与Web品质优化实践

1. 为什么HTML元素对Web品质至关重要 HTML作为Web的基石语言&#xff0c;其元素选择与使用直接影响着网页的品质表现。一个常见的误解是认为HTML只是简单的标签组合&#xff0c;实际上每个元素都承载着特定的语义和功能。比如 <section> 和 <div> 虽然视觉表现…

作者头像 李华
网站建设 2026/10/26 18:44:06

LabVIEW视觉对位系统:三网通信、九点标定与节拍优化

简介&#xff1a;这份资料围绕一套LabVIEW上位机控制汇川H5U PLC与EtherCAT伺服&#xff0c;并结合海康威视相机完成视觉对位的项目资料展开&#xff0c;面向已有LabVIEW基础、希望切入非标自动化与运动控制方向的工程师及学生。内容涉及上位机通信、PLC下位机逻辑、伺服运动控…

作者头像 李华
网站建设 2026/10/26 18:43:50

HappyPlanet元宇宙平台核心技术解析与应用实践

1. HappyPlanet元宇宙平台初探&#xff1a;数字世界的新大陆 第一次登录HappyPlanet时&#xff0c;那种震撼感至今难忘——眼前突然展开的3D城市里&#xff0c;数百个数字分身正沿着发光街道流动&#xff0c;远处悬浮的广告牌展示着实时更新的虚拟演唱会信息。这个2023年最新推…

作者头像 李华