软件SPI驱动1.8寸TFT-LCD:STM32从时序到字符显示全解析
2026/9/13 13:58:18 网站建设 项目流程

最近在调STM32驱动一块1.8寸TFT-LCD,分辨率128x160,主控是STM32F103C8T6。本来想直接走硬件SPI,结果引脚被串口和按键占得七七八八,最后干脆用软件SPI,用GPIO一点一点模拟时序,反而把协议吃透了。这篇文章是这次实践的完整记录,从接线、时序、初始化、画点、字符显示到常见坑,全部摊开讲。手头正好有这块屏、或者想把SPI通信原理搞明白的同学,可以跟着过一遍。

软件SPI最大的优势就是灵活:不挑芯片、不挑引脚,只要GPIO能输出,就能驱动TFT-LCD。代价是刷新慢,但做静态界面、仪表显示完全够用。下面从头开始拆解。

1. 为什么选择软件SPI来驱动1.8寸TFT-LCD

1.1 1.8寸TFT-LCD和ST7735是怎么回事

1.8寸TFT-LCD模块大多采用ST7735作为驱动IC,分辨率128x160,内部有一块GRAM(显存),每个像素用RGB565格式存放,也就是16位色。内部GRAM大小是128 x 160 x 2 = 40960字节。MCU需要做的只是把像素数据通过SPI接口写进去,屏幕自己会把GRAM内容刷新到面板上。

这种屏的接口是4线SPI,一般引出6到8个引脚:SCK、SDA(MOSI)、CS、DC(A0)、RST、BLK背光,有些模块还会引出MISO,但实际用不到,因为驱动IC一般不回读。对MCU来说,整个驱动过程可以抽象成两件事:写命令和写数据。区别在于DC引脚电平:DC为低时写命令,DC为高时写数据。

ST7735的SPI是单向写的,所以不需要MISO,这让软件模拟更加方便。只要SCK和SDA两根线能做时钟和数据翻转,CS做片选,DC区分命令和参数,RST做复位,屏幕就能工作。

1.2 软件SPI与硬件SPI的取舍

很多人在开发时第一反应是用硬件SPI外设,这当然是对的,但软件SPI在一些场景下反而是更好的选择。我整理了一个简单对比:

维度硬件SPI软件SPI
速度最高可达18MHz甚至更高通常1-4MHz,取决于主频和循环代码
CPU占用低,外设自动移位高,每个bit都靠CPU翻转GPIO
引脚约束必须使用固定的MOSI/SCK引脚,还要配置复用功能任意GPIO都能用,配置简单
调试成本出错要看状态寄存器、错误标志逻辑直接,定位问题很直观
跨平台能力换芯片后SPI外设不同,代码要改只要改GPIO宏,几乎能通用

硬件SPI适合大数据量、高刷新率场景,比如播放动画、摄像头预览。但如果只是显示一个温度、电压、时间,软件SPI完全够用。尤其当硬件SPI引脚被其他外设占用时,软件SPI可以绕开引脚冲突,不需要改PCB,也不需要引线抢夺复用脚。

另外,对初学者而言,软件SPI会强迫你理解“SPI到底怎么传数据”——什么时候放数据、什么时候采样、CPOL和CPHA是什么关系。这些理解了,后面再用硬件SPI就是配置寄存器的事,不会被黑盒卡住。

1.3 什么时候该用软件SPI

我遇到的情况通常有三种:

  • 硬件SPI引脚被SD卡、Flash、CAN控制器等占用,重新分配会导致现有板子无法工作。
  • 换了一个不带硬件SPI的小型MCU,比如某些8位单片机,或者只是想做快速功能验证。
  • 想彻底搞懂SPI协议,为一款新屏写驱动。

如果要做全屏刷新动画,1.8寸屏一帧数据是40960字节,软件SPI跑1MHz时一个字节要8us,一帧纯数据就要0.33秒,加上命令和循环开销,体感大概0.4到0.5秒,刷新率只有2帧左右。这种场景就别逞强了,老老实实用硬件SPI,或者换FSMC接口的屏。

2. 硬件连接与引脚规划

2.1 引脚定义

1.8寸TFT-LCD模块的引脚顺序不同卖家会有差异,但功能都一样。通常有:

引脚功能说明
VCC屏供电,一般接3.3V,有些模块兼容5V,但最好看丝印
GND地线
CS片选,低电平有效,拉低后才能和屏通信
RST复位,低电平复位,用完后拉高
DC / A0数据/命令选择,低表示命令,高表示数据
SDA / MOSI串行数据线
SCK串行时钟
BL / BLK背光控制,高电平点亮

接线前先查屏的丝印或者卖家资料,因为SDA和SCK容易标成SDA/SDL、SCL/SCK,这是最寻常的乱源。确认之后先别急着焊,用杜邦线接出来测,正确再固定。

2.2 推荐接线表

以STM32F103C8T6为例,我建议这样接:

STM32引脚屏引脚说明
PA5SCKSPI时钟
PA7SDASPI数据
PA3CS片选
PA2DC数据/命令选择
PA1RST复位
PA0BL背光
3.3VVCC供电
GNDGND地线

这里用了PA5和PA7,正好是硬件SPI1的SCK和MOSI,好处是如果哪一天想切回硬件SPI,板子不用改线。如果不想用这两个引脚,软件SPI可以任意选择其他GPIO,只要避开JTAG占用的PA15/PB3/PB4就行,这三个引脚默认复用为调试口,用GPIO前要禁用JTAG,否则输出状态不可控。

2.3 供电和电平要注意的几个点

这块要单独提醒:ST7735的逻辑供电一般是3.3V,意思是VCC不应该接5V。虽然有些模块板上自带稳压和电平转换,但廉价模块经常没有,直接接5V很容易把驱动IC烧掉。如果只有5V供电,可以加一个LDO或者AMS1117-3.3。

STM32F103的GPIO输出电压是3.3V,对ST7735来说逻辑高电平足够,不需要额外电平转换。背光引脚BL也不能直接接5V,很多模块的BL引脚只是控制一个三极管,5V会灌入损坏。我用限流电阻串在背光正极,或者直接用GPIO控制,拉高点亮、拉低熄灭,亮度不够就调整电阻值,但不能小于10Ω,不然背光电流可能超过GPIO能力。

3. 软件SPI时序:先搞清楚时钟极性和相位

3.1 SPI模式0的写字节时序

SPI通信有一组参数叫CPOL(时钟极性)和CPHA(时钟相位)。对软件模拟来说,最常用的是模式0:CPOL=0,CPHA=0。意思是SCK空闲时为低电平,数据在SCK上升沿被采样。发送方在SCK为低的时候改变SDA数据,等SCK变成高电平,接收方锁存当前的电平,一个bit就传完了。

一个字节是8个bit,从最高位MSB开始发。所以软件SPI写一个字节的逻辑是:

  1. 根据当前最高位的值,把SDA引脚拉高或拉低。
  2. 将数据左移一位,准备处理下一位。
  3. 把SCK拉高,让接收方采样。
  4. 把SCK拉低,完成一个时钟周期。
  5. 重复8次。

画出时序就是:SDA在SCK低区间变化,SCK上升沿采样数据,下降沿后SDA可以继续变,如此循环。

3.2 STM32 GPIO初始化

无论用标准外设库还是HAL库,都得先把接屏的GPIO配置成推挽输出模式。标准外设库的初始化代码这样写:

void LCD_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); LCD_SCK_H; LCD_CS_H; LCD_RST_H; LCD_BL_H; }

HAL库也类似,只是把GPIO_Mode_Out_PP换成GPIO_MODE_OUTPUT_PP,然后调用HAL_GPIO_Init。引脚宏建议用#define定义,方便后面换引脚。

#define LCD_SCK_GPIO GPIOA #define LCD_SCK_PIN GPIO_Pin_5 #define LCD_SCK_H() GPIO_SetBits(LCD_SCK_GPIO, LCD_SCK_PIN) #define LCD_SCK_L() GPIO_ResetBits(LCD_SCK_GPIO, LCD_SCK_PIN)

建议把CS、DC、RST、BL都这样定义,写代码逻辑会很清爽。

3.3 写字节、写命令、写数据的代码

核心函数就是一个SPI写字节:

void SPI_WriteByte(uint8_t dat) { uint8_t i; for (i = 0; i < 8; i++) { if (dat & 0x80) LCD_SDA_H; else LCD_SDA_L; dat <<= 1; LCD_SCK_H; LCD_SCK_L; } }

这里没有额外延时,因为STM32F103在72MHz下执行几条赋值和GPIO操作本身就有几十纳秒,加上循环判断,实际SCK频率大概在2到4MHz,ST7735识别无压力。如果发现屏幕不稳定,可以在SCK拉高后加几个空循环:

for (volatile uint8_t j = 0; j < 3; j++);

再定义写命令和写数据函数:

void LCD_WriteCmd(uint8_t cmd) { LCD_DC_L; LCD_CS_L; SPI_WriteByte(cmd); LCD_CS_H; } void LCD_WriteData(uint8_t dat) { LCD_DC_H; LCD_CS_L; SPI_WriteByte(dat); LCD_CS_H; }

注意每次操作前都要先拉低CS,操作完成后拉高CS。DC引脚必须根据当前发的是命令还是数据来设置,顺序不能反,否则屏会把命令当数据、数据当命令,画面直接乱掉。

3.4 关于延时的讨论

软件模拟SPI时,最容易被扔进去的是各种delay_us。如果像我在F103C8T6上这样,GPIO翻转足够慢,不加延时反而刚刚好。如果换到主频很高的芯片,比如STM32H7,GPIO翻转快到几十MHz,可能就需要加轻微延时,否则屏跟不上。

但延时不能用得太随意。在初始化阶段,需要的是毫秒级延时,因为ST7735内部上电过程中有等待时间。可以写一个简易毫秒延时:

void delay_ms(uint32_t ms) { volatile uint32_t i; while (ms--) { for (i = 0; i < 7200; i++); } }

这个循环次数是按72MHz主频估算的,移植到别的频率要重新调。更可靠的办法是用SysTick,或者直接使用HAL的HAL_Delay。写驱动到这里,后面所有的命令间隔都用delay_ms,而不是delay_us

4. LCD初始化配置:白屏和初始化顺序的因果关系

4.1 ST7735初始化命令全景

ST7735的初始化并不复杂,但很敏感。命令大概分几类:

  • 软件复位和睡眠控制:0x01软件复位,0x11 Sleep Out,0x29 Display ON。
  • 显示控制:0x36 MADCTL(行列方向、RGB/BGR顺序),0x3A像素格式,0x20/0x21反色控制。
  • 电源控制:0xC0、0xC1、0xC5、0xD0等,设置不同电压。
  • 帧率设置:0xB1、0xB2、0xB3。
  • Gamma设置:0xE0、0xE1,影响色彩和对比度。

很多网上代码喜欢把一大段初始化序列直接怼进去,但不解释原因。其实初始化时最关键的是复位、Sleep Out、颜色格式和Display ON,其他很多寄存器有默认值,某些屏即使不写也能显示。但每个模块的品控和出厂配置不同,完整初始化序列能最大程度保证画面正常。

4.2 典型的初始化序列

下面是我验证过可用的一套ST7735初始化代码,适用于大多数1.8寸模块:

void LCD_Init(void) { LCD_GPIO_Init(); LCD_RST_L; delay_ms(50); LCD_RST_H; delay_ms(120); LCD_WriteCmd(0x11); // Sleep Out delay_ms(120); LCD_WriteCmd(0xB1); LCD_WriteData(0x05); LCD_WriteData(0x3A); LCD_WriteData(0x3A); LCD_WriteCmd(0xB2); LCD_WriteData(0x05); LCD_WriteData(0x3A); LCD_WriteData(0x3A); LCD_WriteCmd(0xB3); LCD_WriteData(0x05); LCD_WriteData(0x3A); LCD_WriteData(0x3A); LCD_WriteData(0x05); LCD_WriteData(0x3A); LCD_WriteData(0x3A); LCD_WriteCmd(0xC0); LCD_WriteData(0x02); LCD_WriteData(0x02); LCD_WriteCmd(0xC1); LCD_WriteData(0x02); LCD_WriteCmd(0xC2); LCD_WriteData(0x02); LCD_WriteCmd(0xC5); LCD_WriteData(0x50); LCD_WriteData(0x50); LCD_WriteCmd(0xD0); LCD_WriteData(0x02); LCD_WriteCmd(0x36); LCD_WriteData(0x08); // MADCTL,可根据方向调整 LCD_WriteCmd(0x3A); LCD_WriteData(0x05); // 16bit RGB565 LCD_WriteCmd(0xE0); LCD_WriteData(0x02); LCD_WriteData(0x1C); LCD_WriteData(0x07); LCD_WriteData(0x12); LCD_WriteData(0x37); LCD_WriteData(0x32); LCD_WriteData(0x29); LCD_WriteData(0x2D); LCD_WriteData(0x29); LCD_WriteData(0x25); LCD_WriteData(0x2B); LCD_WriteData(0x39); LCD_WriteData(0x00); LCD_WriteData(0x01); LCD_WriteData(0x03); LCD_WriteData(0x10); LCD_WriteCmd(0xE1); LCD_WriteData(0x03); LCD_WriteData(0x1D); LCD_WriteData(0x07); LCD_WriteData(0x06); LCD_WriteData(0x2E); LCD_WriteData(0x2C); LCD_WriteData(0x29); LCD_WriteData(0x2D); LCD_WriteData(0x2E); LCD_WriteData(0x2E); LCD_WriteData(0x37); LCD_WriteData(0x3F); LCD_WriteData(0x00); LCD_WriteData(0x00); LCD_WriteData(0x02); LCD_WriteData(0x10); LCD_WriteCmd(0x29); // Display ON delay_ms(50); LCD_Clear(0x0000); delay_ms(20); LCD_Clear(0xFFFF); delay_ms(20); }

这套序列里,0x36的值直接决定了扫描方向,0x08通常是没有翻转、RGB顺序的状态。如果显示方向不对,或者颜色变成反色,需要改这个寄存器。

4.3 为什么Wakeup后要等那么久

Sleep Out后等待120ms不是随便拍的。ST7735内部电压要从睡眠模式切换到工作模式,升压电路和GVDD电压建立需要时间,命令发得太快,后面设置直接无效,表现出来就是白屏或颜色错乱。

软件复位后等待120ms同理。上电后第一次拉低RST再释放,屏内部会重新加载校准参数,这时候立刻发命令容易失败。

4.4 初始化失败的典型表现

如果初始化代码有问题,画面通常不会报错,而是直接白屏、黑屏或花屏。最常见的几个现象:

  • 白屏:复位没生效、Sleep Out后没等待、SCK/SDA接线反了、CS没拉低。
  • 花屏但有点亮痕:初始化序列不完整,或者0x36方向设置不对,GRAM数据写入错位。
  • 颜色反色:0x36的RGB/BGR位不对,或者0x21反色命令被意外发送。
  • 画面偏移:0x2A和0x2B的列行地址偏移没设置,显示区域可能整体偏几行。

这些不是玄学,后面在第七节展开排查。

5. 显示基础:窗口模式与批量写内存

5.1 ST7735的GRAM和窗口机制

ST7735内部有一块128x160的GRAM,地址对应屏幕坐标。写入数据时有两种方式:一是每次写一个坐标的点,二是先设置一个显示窗口,然后把数据连续写给整个窗口。窗口机制相当于告诉屏幕“我现在要写这个矩形区域”,之后连续发来的像素数据会按从左到右、从上到下的顺序自动填进窗口,不需要每次重新发坐标。

每次画单个像素的时候,其实都有一组写窗口命令:0x2A设置列范围,0x2B设置行范围,0x2C启动写GRAM。这种调用很贵,因为命令开销远大于像素数据本身。批量写时才真正发挥SPI效率。

5.2 设置显示窗口的实现

用代码写就是:

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); }

之后连续发送的每两个字节会被当作一个RGB565像素写入窗口。窗口设置之后,MCU不需要再重复发命令,直到写满窗口或者收到其他命令,ST7735内部地址指针会自动继续移动到下一个像素。

5.3 画点、填充和清屏

最直观的画点函数可以这样写:

void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color) { if (x >= 128 || y >= 160) return; LCD_SetWindow(x, y, x, y); LCD_DC_H; LCD_CS_L; SPI_WriteByte(color >> 8); SPI_WriteByte(color & 0xFF); LCD_CS_H; }

清屏就不该用画点循环,否则要调用128*160次画点,每次画点都要发一遍0x2A、0x2B、0x2C命令,效率极低。正确做法是开一个全屏窗口,然后连续吐40960个字节:

void LCD_Clear(uint16_t color) { uint32_t i; LCD_SetWindow(0, 0, 127, 159); LCD_DC_H; LCD_CS_L; for (i = 0; i < 128 * 160; i++) { SPI_WriteByte(color >> 8); SPI_WriteByte(color & 0xFF); } LCD_CS_H; }

这样同样清一屏,命令开销只有一次,整体速度能差几十倍。

5.4 为什么批量写比逐个画点快得多

逐个画点的问题在于,每个点都要重复执行设置列地址、设置行地址和启动写GRAM。哪怕软件SPI每字节只要8us,加上这些命令,每个点可能需要多出30到50us。128x160个点,额外开销就是0.6到1秒,清个屏肉眼都看得到刷新过程。

批量写把命令开销摊薄到几万个像素上,实际速度瓶颈只剩SPI位循环本身。比如显示一张图片、绘制进度条、区块填充,都应该先LCD_SetWindow,再循环发送像素数据。这也是TFT驱动的一个通用思路,不只是ST7735适用。

6. 字符显示:从点阵字模到字符串API

6.1 字模是什么,怎么生成

屏幕上显示字符,本质上是把字符拆成像素点阵。最常见的8x16 ASCII字模,就是一个字符用16个字节表示,每字节表示一行8个像素,bit位为1的点画前景色,为0的点画背景色。

可以用PCtoLCD2002这类取模软件生成,也可以直接下载标准的ASCII字库数组。取模时选择“横向取模、高位在前”,生成结果和下面的代码匹配。

6.2 8x16 ASCII字模的使用方法

下面是字符'A'的8x16字模示例:

const uint8_t font8x16[][16] = { // 'A' { 0x00, 0x00, 0x00, 0x10, 0x28, 0x28, 0x44, 0x44, 0x7C, 0x82, 0x82, 0x82, 0x82, 0x00, 0x00, 0x00 }, // 'B' { 0x00, 0x00, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x42, 0x42, 0x42, 0x42, 0x7E, 0x00, 0x00, 0x00 }, };

实际工程中需要放整张ASCII表,从0x20空格到0x7E波浪号,一共95个字符。数组大小是95*16=1520字节,对STM32来说内存不是问题,放在Flash常量区即可。

6.3 实现ShowChar和ShowString

先写一个显示单个字符的函数。如果直接调用LCD_DrawPixel,每个像素都会触发一次窗口设置,15个像素的字符要调用15次,非常慢。优化版先设置一个8x16窗口,然后连续输出每个像素颜色:

void LCD_ShowChar(uint16_t x, uint16_t y, char ch, uint16_t color, uint16_t bg) { uint8_t i, j; uint8_t line; uint16_t pixel; if (ch < 0x20 || ch > 0x7E) ch = '?'; LCD_SetWindow(x, y, x + 7, y + 15); for (i = 0; i < 16; i++) { line = font8x16[ch - 0x20][i]; for (j = 0; j < 8; j++) { if (line & (0x80 >> j)) pixel = color; else pixel = bg; LCD_DC_H; LCD_CS_L; SPI_WriteByte(pixel >> 8); SPI_WriteByte(pixel & 0xFF); LCD_CS_H; } } }

这里SPI_WriteByte被调用了8*16=128次,但窗口只设置一次,比逐个画点还是快很多。如果还想更快,可以在两次字节发送之间不反复拉CS,但注意DC方向和CS时序要维持。

显示字符串就变得很简单:

void LCD_ShowString(uint16_t x, uint16_t y, const char *str, uint16_t color, uint16_t bg) { while (*str) { LCD_ShowChar(x, y, *str, color, bg); x += 8; str++; } }

每显示完一个字符,x坐标向右移动8个像素。8x16字模的字符间距是固定的,如果要显示中文字符,需要单独的汉字字库和更宽的窗口,这里不展开,思路是一样的。

6.4 扩展:显示整数、进度条

显示整数可以写一个简单的数字转字符函数:

void LCD_ShowNum(uint16_t x, uint16_t y, uint32_t num, uint16_t color, uint16_t bg) { char buf[12]; uint8_t i = 0; do { buf[i++] = '0' + num % 10; num /= 10; } while (num); while (i > 0) { i--; LCD_ShowChar(x, y, buf[i], color, bg); x += 8; } }

进度条本质是矩形填充。背景部分用窗口填充灰底色,进度部分再设置一个小窗口,按百分比计算填充长度:

void LCD_ShowProgress(uint16_t x, uint16_t y, uint16_t width, uint8_t percent) { uint16_t fill_w = width * percent / 100; // 先画边框和背景,再画填充块 LCD_SetWindow(x, y, x + fill_w - 1, y + 3); // 连续输出前景色像素 }

这类函数能组合出很实用的仪表界面。

7. 实测中的坑:白屏、反色花屏和逻辑分析仪

7.1 白屏排障链路

白屏是TFT驱动里最常见的问题。遇到白屏,我一般按下面的顺序排查:

  • 先看背光亮不亮。背光不亮,先查BL引脚电平和模块供电,再看背光电阻和LED有没有接反。
  • 背光亮但无任何内容,大概率是通信问题。检查SCK和SDA是否接反、CS有没有正确拉低、DC命令数据逻辑是否正确。
  • 复位是否成功。用示波器或者逻辑分析仪看RST引脚,上电后有没有先低后高的脉冲,低电平持续时间不能太短,至少要几个毫秒。
  • Sleep Out等待时间是否足够。不少白屏都是因为Sleep Out后只延时了1ms就继续初始化,ST7735内部电压还没稳定。
  • 初始化序列是否完整。有些模块用的ST7735S,寄存器版本不同,完整序列会更稳妥。

如果用逻辑分析仪抓SCK和SDA,能看到屏幕有没有收到0x11、0x29这些关键命令。如果在命令阶段SDA已经有波形,但屏幕一直白屏,多半是初始化配置和模块不匹配。

7.2 颜色不对怎么办

颜色异常也很常见。比如红色显示成蓝色,背景变成反色,通常不是硬件问题,而是颜色格式和扫描方向配置不正确。

  • RGB565字节顺序反了:显示颜色会整体偏色,比如红蓝互换。检查发送像素数据时是不是先发高字节。color >> 8后发送高字节,这是ST7735默认方式。
  • 0x36寄存器里的RGB/BGR位不对:屏幕会变成红蓝反色。很多1.8寸模块需要把0x36设为0xC8而不是0x08,也可以用0xA80x68调整方向。
  • 0x21反色命令:如果初始化序列里误发了INVOFFINVON,画面会出现颜色反转。白底蓝字可能变成蓝底白字。

碰到颜色问题时,可以先写一个红色矩形测试屏,然后切换0x36寄存器值,观察复位后颜色是否修正。这里最忌讳瞎试,记录每次改动,一次只改一个参数。

7.3 片选和DC引脚的微型时序问题

软件SPI虽然不用关心SPI外设的状态机,但CS和DC的时序还是要注意。有些人在初始化的时候把DC和CS状态搞反:

  • 先拉高了DC再拉低CS,理论上没问题,但每次写字节都要保持DC正确。
  • 如果初始化过程中DC一直保持高电平,所有命令都会被当成数据写入GRAM,屏幕看起来就是花屏或全白。
  • CS在发送字节过程中拉高,会导致这次传输被截断。SPI_WriteByte必须在CS低电平期间完成整个字节,不能在字节中间抬起来。

我在代码里写注释时都会标注“这个函数执行完后CS为高”,就是为了防止后面调用的函数忘记拉低CS。

7.4 逻辑分析仪实测波形

软件模拟SPI调试起来比硬件SPI还方便,因为只要几根杜邦线,把逻辑分析仪接到SCK和SDA上,就能看到每位翻转。标准8通道逻辑分析仪,采样率50MHz以上完全够用。

我第一次调的时候,发现屏幕一直白屏,抓波形后发现SDA数据在SCK上升沿之后才变化。原因是我在SPI_WriteByte里先拉高了SCK再设置SDA。解决办法就是先把SDA设置好,再拉SCK,SCK拉低后允许SDA变化。这个顺序从肉眼看代码没感觉,波形一抓就露馅。

所以遇到任何软件SPI问题,第一反应不应该是换初始化序列,而是先抓时序。

7.5 性能优化和易踩的坑

软件SPI的性能优化主要在两个方向:

  • 减少函数调用层级,把SPI_WriteByte声明成static inline,或者直接在循环里展开。
  • 减少无效CS操作,在批量填充时只拉一次CS,不要每发一个字节都拉高拉低。前面LCD_Clear已经示范过这个方法。

另外,不要在主循环里频繁调用软件SPI驱动大量刷新。像显示一个计数器,每秒刷新一次数字就够了。如果屏幕内容一直在变化,考虑把刷新频率降低,或者切到硬件SPI。

还有一个常被忽略的坑:STM32F103的GPIO如果没使能对应时钟,操作GPIO引脚无效。有些代码里只配置了GPIOA、GPIOB的时钟,漏了某个引脚所在端口,表现就是某一根线没波形。检查RCC时钟使能代码,确保SCK、SDA所在端口都开了时钟。

最后,初始化序列里如果用了0x200x21这类反色命令,建议先注释掉,用纯色矩形测试屏。稳定后再加反色命令,避免把反色效果当故障分析老半天。这块屏的软件SPI驱动做到这一步,静态显示已经非常稳定。项目如果后续要做动画,再花十几分钟把GPIO模拟改成硬件SPI也不迟,啊对了,当初选PA5和PA7的好处就在这里,改配置不用动杜邦线。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询