简介:本资源是一套基于STM32F103RCT6单片机驱动2.0寸SPI接口ILI9225 TFT显示屏的完整嵌入式源码工程,面向嵌入式初学者、单片机开发工程师及高校电子类课程实践者,解决SPI模拟并行时序驱动非原生SPI屏的核心难点。压缩包共274个文件,以74个.h头文件和62个.c源文件为主体,涵盖SPI底层驱动、ILI9225寄存器配置、图形绘制函数(点/线/矩形/字符)、屏幕初始化与方向控制等模块;另有26个.o目标文件、24个.crf编译中间文件及多个Keil工程文件(.uvproj、.uvopt)和可执行镜像(.hex、.axf),总大小3.77MB,结构完整,支持开箱即编译调试。目前已有47人学习下载。读者可直接复用该工程的硬件抽象层设计、SPI时序适配逻辑及寄存器操作范式,快速实现TFT显示功能,并参考其模块化组织方式优化自身项目架构。
1. 2.0寸SPI_ILI9225显示屏在STM32F103RCT6上跑通,不是调个库就完事——它卡在SPI时序、GPIO复用和显存映射三道坎上
你拿到一个标着“2.0寸SPI_ILI9225显示屏_ART20F140 Code for STM32 (STM32F103RCT6)”的压缩包,解压后发现是标准HAL库结构:Core、Drivers、Inc、Src,还有几个.c文件里写着ILI9225_Init()、ILI9225_SetCursor()、ILI9225_WriteData()。但烧录进去屏幕全黑,或者只闪一下白光就熄灭——这不是代码没编译成功,而是SPI物理层没对齐、LCD控制器寄存器没按手册逐字节配置、显存地址映射没匹配ILI9225的GRAM起始偏移。这个标题指向的是一类典型嵌入式显示落地场景:资源受限(STM32F103RCT6仅72MHz主频、64KB Flash、20KB RAM)、接口简陋(仅支持8位并口或3线/4线SPI)、驱动芯片老旧(ILI9225已停产多年,但因成本低仍在工控小屏中大量使用)。它不适用于LVGL或emWin这类GUI框架,而必须手撕底层时序、重写写命令/写数据函数、手动管理176×220像素的显存搬运。适合需要快速验证硬件连接、做状态指示或简易参数界面的嵌入式工程师,尤其当你手头只有ST-Link V2和一块最小系统板时。
2. SPI通信必须绕开HAL_SPI_Transmit的默认行为:ILI9225要求DCX引脚电平切换与SPI帧严格同步
ILI9225不是标准SPI外设,它没有MISO引脚,也不支持自动片选;它的通信协议本质是“伪SPI”:通过DCX(Data/Command)引脚电平区分发送的是命令还是数据,且每次发送前必须拉低CS(片选),发送后拉高,而DCX必须在CS拉低后、第一个SCK边沿前稳定。HAL库默认的HAL_SPI_Transmit()会直接发完一整段数据,中间无法插入DCX翻转——这会导致ILI9225把命令当数据、把数据当命令,初始化失败或显示错乱。
2.1 手动控制SPI+GPIO组合,实现DCX与SCK边沿对齐
不能依赖HAL_SPI_Transmit,必须拆解为单字节发送,并在每次发送前精确控制DCX。以STM32F103RCT6为例,假设SPI1用于通信,PA4为CS,PA5为DCX,PA7为SCK,PA6为MOSI:
// 在stm32f103rct6_gpio.h中定义 #define LCD_CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) #define LCD_CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define LCD_DCX_CMD() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET) // 0 = command #define LCD_DCX_DATA() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET) // 1 = data // 单字节SPI发送(阻塞式,无DMA) static void ILI9225_SPI_WriteByte(uint8_t byte) { uint8_t tx_buf = byte; uint8_t rx_buf; HAL_SPI_TransmitReceive(&hspi1, &tx_buf, &rx_buf, 1, HAL_MAX_DELAY); }提示:这里必须用
HAL_SPI_TransmitReceive()而非HAL_SPI_Transmit(),因为ILI9225虽无MISO,但SPI硬件要求MISO引脚悬空或接上拉,否则SCK可能失锁。TransmitReceive能确保SPI状态机完整运行,避免后续帧错位。
2.2 初始化序列必须严格按Datasheet第12页执行,漏掉任一寄存器将导致黑屏
ILI9225上电后需按固定顺序写入14个关键寄存器,顺序错、值错、延时不达标均会失败。常见错误是把0x0022(GRAM写使能)写在0x0007(Display Control)之前,或忽略0x0001(Driver Output Control)中VS(Vertical Scroll)位的设置。以下是精简但必填的初始化片段(含毫秒级延时):
void ILI9225_Init(void) { LCD_CS_LOW(); LCD_DCX_CMD(); ILI9225_SPI_WriteByte(0x0001); // Driver Output Control HAL_Delay(5); LCD_DCX_CMD(); ILI9225_SPI_WriteByte(0x0002); // LCD Driving AC Control LCD_DCX_DATA(); ILI9225_SPI_WriteByte(0x0000); LCD_DCX_CMD(); ILI9225_SPI_WriteByte(0x0003); // Entry Mode LCD_DCX_DATA(); ILI9225_SPI_WriteByte(0x1030); // AM=1, ID=10, DL=0, DFM=0 LCD_DCX_CMD(); ILI9225_SPI_WriteByte(0x0007); // Display Control LCD_DCX_DATA(); ILI9225_SPI_WriteByte(0x0133); // BT=1, SC=1, DT=1, GE=1, PW=3 LCD_DCX_CMD(); ILI9225_SPI_WriteByte(0x0010); // Power Control 1 LCD_DCX_DATA(); ILI9225_SPI_WriteByte(0x0000); LCD_DCX_DATA(); ILI9225_SPI_WriteByte(0x0000); LCD_DCX_DATA(); ILI9225_SPI_WriteByte(0x0000); LCD_DCX_DATA(); ILI9225_SPI_WriteByte(0x0000); LCD_DCX_CMD(); ILI9225_SPI_WriteByte(0x0011); // Power Control 2 LCD_DCX_DATA(); ILI9225_SPI_WriteByte(0x0000); LCD_DCX_CMD(); ILI9225_SPI_WriteByte(0x0012); // Power Control 3 LCD_DCX_DATA(); ILI9225_SPI_WriteByte(0x0000); LCD_DCX_CMD(); ILI9225_SPI_WriteByte(0x0013); // Power Control 4 LCD_DCX_DATA(); ILI9225_SPI_WriteByte(0x0000); LCD_DCX_CMD(); ILI9225_SPI_WriteByte(0x0020); // GRAM Horizontal Address Set LCD_DCX_DATA(); ILI9225_SPI_WriteByte(0x0000); LCD_DCX_DATA(); ILI9225_SPI_WriteByte(0x0000); LCD_DCX_CMD(); ILI9225_SPI_WriteByte(0x0021); // GRAM Vertical Address Set LCD_DCX_DATA(); ILI9225_SPI_WriteByte(0x0000); LCD_DCX_DATA(); ILI9225_SPI_WriteByte(0x0000); LCD_DCX_CMD(); ILI9225_SPI_WriteByte(0x0022); // GRAM Write LCD_CS_HIGH(); HAL_Delay(50); }2.2.1 关键寄存器参数说明表
| 寄存器地址 | 名称 | 推荐值 | 作用说明 |
|---|---|---|---|
0x0001 | Driver Output Control | 0x0000 | 设置扫描方向:GS=0(Gate Scan from G1 to G320),VLE=0(No Vertical Line Enable) |
0x0003 | Entry Mode | 0x1030 | AM=1(Address Counter Mode: horizontal then vertical),ID=10(Increment direction: column→row) |
0x0007 | Display Control | 0x0133 | 控制背光、休眠、显示开/关;0x0133表示正常显示(D=1)、非休眠(S=1)、BT=1(Boost Voltage) |
0x0020/0x0021 | GRAM Address Set | (0,0)→(175,219) | ILI9225分辨率为176×220,GRAM起始地址为(0,0),结束地址为(175,219),注意索引从0开始 |
0x0022 | GRAM Write | — | 此命令后所有后续字节均写入GRAM,无需再发命令 |
注意:ILI9225的GRAM地址空间为176×220×2 = 77440字节(16位色),但实际可用显存需按
0x0020/0x0021设定的窗口裁剪。若未正确设置地址窗口,0x0022写入的数据会覆盖到屏幕外区域,表现为局部花屏或偏移。
3. 显存管理不能靠malloc:用静态数组+坐标映射实现176×220像素的高效刷屏
STM32F103RCT6的20KB RAM中,仅剩约12KB可用(扣除栈、堆、全局变量),而ILI9225全屏显存需77440字节(≈75.6KB),远超RAM容量。因此不能开辟全屏缓冲区,必须采用“窗口刷屏”策略:只维护一个16×16像素的小缓存(512字节),配合SetCursor(x,y,w,h)函数动态定位写入区域,再用WriteGRAM()逐行发送。
3.1 定义GRAM写入窗口与坐标转换逻辑
ILI9225的GRAM地址不是线性排列,而是按列优先(Column-major)存储:先填满一列(220行),再下一列。因此(x,y)像素对应GRAM地址为y * 176 + x(注意不是x * 220 + y)。该转换必须硬编码进写函数,否则图像会90°旋转。
// 设置GRAM写入窗口:x0~x1列,y0~y1行(含端点) void ILI9225_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_CS_LOW(); LCD_DCX_CMD(); ILI9225_SPI_WriteByte(0x0020); // Column Address Set LCD_DCX_DATA(); ILI9225_SPI_WriteByte(x0 >> 8); // X start MSB LCD_DCX_DATA(); ILI9225_SPI_WriteByte(x0 & 0xFF); // X start LSB LCD_DCX_DATA(); ILI9225_SPI_WriteByte(x1 >> 8); // X end MSB LCD_DCX_DATA(); ILI9225_SPI_WriteByte(x1 & 0xFF); // X end LSB LCD_DCX_CMD(); ILI9225_SPI_WriteByte(0x0021); // Row Address Set LCD_DCX_DATA(); ILI9225_SPI_WriteByte(y0 >> 8); // Y start MSB LCD_DCX_DATA(); ILI9225_SPI_WriteByte(y0 & 0xFF); // Y start LSB LCD_DCX_DATA(); ILI9225_SPI_WriteByte(y1 >> 8); // Y end MSB LCD_DCX_DATA(); ILI9225_SPI_WriteByte(y1 & 0xFF); // Y end LSB LCD_DCX_CMD(); ILI9225_SPI_WriteByte(0x0022); // GRAM write } // 向当前窗口写入一行像素(16位RGB565格式) void ILI9225_WriteLine(uint16_t *line, uint16_t len) { LCD_CS_LOW(); for (uint16_t i = 0; i < len; i++) { LCD_DCX_DATA(); ILI9225_SPI_WriteByte(line[i] >> 8); // R5G6B5高位字节 ILI9225_SPI_WriteByte(line[i] & 0xFF); // 低位字节 } LCD_CS_HIGH(); }3.2 实现字符渲染:用8×16点阵字模+逐像素写入
不依赖外部字体文件,直接在代码中定义ASCII字符集(0x20–0x7E),每个字符占16字节(8列×16行),每字节表示一列的8个像素(MSB在上)。渲染时按列扫描,将点阵映射到GRAM坐标:
const uint8_t font8x16[96][16] = { // 空格 0x20 {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, // '0' 0x30 {0x00,0x00,0x00,0xF8,0xCC,0xCC,0xCC,0xCC,0xCC,0xCC,0xCC,0xF8,0x00,0x00,0x00,0x00}, // ... 其他字符省略 }; void ILI9225_PutChar(uint16_t x, uint16_t y, char c, uint16_t color) { if (c < 0x20 || c > 0x7E) return; const uint8_t *p = font8x16[c - 0x20]; uint16_t line_buf[8]; // 一行8像素,每像素16位 for (uint8_t row = 0; row < 16; row++) { for (uint8_t col = 0; col < 8; col++) { uint8_t bit = (p[row] & (0x80 >> col)) ? color : 0x0000; line_buf[col] = bit; } ILI9225_SetWindow(x, y + row, x + 7, y + row); ILI9225_WriteLine(line_buf, 8); } }3.2.1 字模数据生成方法说明
网络搜索“ILI9225 字模生成工具”可得在线生成器,输入字体(如Courier New)、大小(8×16)、输出格式(C数组),勾选“MSB first”和“Column major”。生成后需手动校验:第一行应为顶部像素,0x80表示最顶点亮,0x01表示最底点亮。错误的字模会导致文字倒置或镜像。
4. STM32F103RCT6硬件适配要点:时钟配置、引脚复用与Boot引脚实测验证
STM32F103RCT6的SPI1位于APB2总线,最高支持36MHz,但ILI9225最大SPI时钟为10MHz(Datasheet Section 6.3),因此必须降频。同时,PA4/PA5/PA6/PA7需配置为推挽输出(非复用功能),因为ILI9225不响应SPI硬件片选,CS和DCX必须由GPIO软件控制。
4.1 SPI1时钟与GPIO模式配置(CubeMX不可信,必须手改)
CubeMX生成的代码常将PA6/PA7设为AF_PP(复用推挽),但ILI9225不需要MISO,且PA6若设为复用会强制启用内部上拉,导致SPI空闲时MOSI电平异常。正确做法是:
// 在MX_GPIO_Init()中修改 __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 非复用! GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // SPI1初始化(在MX_SPI1_Init中) hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; // 实际只用MOSI,但硬件要求2线模式 hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0 hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA=0,采样在第一个边沿 hspi1.Init.NSS = SPI_NSS_SOFT; // 必须软NSS,禁用硬件CS hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // APB2=72MHz → SCK=9MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;提示:
SPI_BAUDRATEPRESCALER_8对应72MHz/8=9MHz,在ILI9225允许的10MHz内;若设为_4(18MHz)则屏幕闪烁或乱码。实测中,超过9.5MHz即不稳定。
4.2 Boot引脚电平确认:避免程序不启动的隐形故障
STM32F103RCT6的启动模式由BOOT0和BOOT1引脚决定。常见错误是BOOT0悬空(未接10kΩ下拉),导致上电时随机进入系统存储器启动模式(USB DFU),Keil下载后无法运行。必须实测:
| BOOT1 | BOOT0 | 启动模式 | 推荐接法 |
|---|---|---|---|
| 0 | 0 | 主闪存(Flash) | BOOT0接地,BOOT1接地(默认) |
| 0 | 1 | 系统存储器(System Memory) | BOOT0接3.3V(调试用) |
| 1 | 0 | 内置SRAM | 不常用 |
用万用表测量PA14(BOOT0)对地电压,应为0V;若为1.8V以上,检查原理图是否漏接下拉电阻。此问题与ILI9225无关,但会导致“代码烧录成功却无任何反应”,浪费数小时排查SPI。
5. 验证与排错:用逻辑分析仪抓SPI波形,定位DCX/CSPIN时序偏差
当屏幕全黑或显示残影时,最有效手段是用Saleae Logic或类似设备抓取CS、DCX、SCK、MOSI四路信号,验证三个关键时间点:
① CS下降沿到DCX稳定的时间 ≥ 10ns(Datasheet tCSX);
② DCX稳定到第一个SCK上升沿的时间 ≥ 20ns(tDCX);
③ 每次发送命令/数据前CS必须重新拉低(ILI9225无自动片选保持)。
5.1 典型故障波形与修复方案
| 故障现象 | 波形特征 | 根本原因 | 修复代码位置 |
|---|---|---|---|
| 屏幕全黑,无任何反应 | CS始终高电平,或仅在初始化时拉低一次 | LCD_CS_LOW()未被调用,或HAL_GPIO_WritePin()参数错误 | 检查ILI9225_Init()开头是否漏写LCD_CS_LOW() |
| 显示内容上下颠倒 | DCX在CS拉高后才翻转,或GRAM地址窗口Y轴设反 | ILI9225_SetWindow()中y0/y1传参颠倒,或字模扫描方向错 | 修改ILI9225_SetWindow(y0,y1)为ILI9225_SetWindow(y0,y1),确保y0<y1 |
| 文字有重影、颜色发虚 | SPI时钟过快(>10MHz)或SCK空闲电平错误 | hspi1.Init.CLKPolarity设为SPI_POLARITY_HIGH,导致采样相位错 | 改为SPI_POLARITY_LOW,并确认CLKPhase=SPI_PHASE_1EDGE |
5.2 最小可验证程序:只点亮一个像素
不要一上来就跑完整UI,先验证硬件链路:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); ILI9225_Init(); // 点亮坐标(0,0)处一个红色像素(RGB565: 0xF800) ILI9225_SetWindow(0, 0, 0, 0); uint16_t red = 0xF800; LCD_CS_LOW(); LCD_DCX_CMD(); ILI9225_SPI_WriteByte(0x0022); LCD_DCX_DATA(); ILI9225_SPI_WriteByte(red >> 8); LCD_DCX_DATA(); ILI9225_SPI_WriteByte(red & 0xFF); LCD_CS_HIGH(); while (1) { } }烧录后若左上角出现一个红点,证明SPI通信、DCX控制、GRAM写入全部正确;若无反应,则问题必在硬件连接(检查VCC/GND/LCD_VCC是否3.3V)或Boot引脚配置。
本文还有配套的精品资源,点击获取