1. 项目概述:当STM32F103遇上GC9306
搞嵌入式开发的朋友,对STM32F103这颗“国民MCU”肯定不陌生。它价格亲民、资源丰富,是无数学生、工程师入门和做项目的首选。而做项目总离不开人机交互,一块好用的屏幕往往是点睛之笔。这次我们要折腾的,就是通过SPI接口来驱动一块GC9306控制器的TFT液晶屏。
GC9306是一款常见的驱动芯片,广泛用于中小尺寸的TFT屏幕,比如1.8寸、2.0寸这些在开发板上很常见的尺寸。它支持多种接口,其中SPI模式因其接线简单、占用IO口少而备受青睐,特别适合在STM32F103这种引脚资源不算特别阔绰的芯片上使用。你可能会想,市面上不是有现成的库吗?直接用不就好了?这话没错,但知其然更要知其所以然。直接调用HAL_SPI_Transmit把一堆初始化命令丢过去,屏幕是亮了,可一旦遇到花屏、颜色不对、刷新慢的问题,就只能干瞪眼。自己动手从底层捋一遍SPI时序,搞清楚GC9306的寄存器配置,这个过程不仅能解决问题,更能让你对SPI通信和屏幕驱动有肌肉记忆般的理解。
这篇文章,我就以一个实际项目为背景,带你从零开始,用STM32F103的SPI外设点亮并驱动GC9306屏幕。我会详细拆解硬件连接、SPI配置、GC9306的初始化序列、打点画线算法的实现,以及那些调试过程中必然会踩的坑和解决技巧。无论你是刚接触STM32的新手,还是想优化现有驱动性能的老鸟,相信都能找到有用的干货。
2. 硬件连接与SPI外设解析
2.1 核心硬件接口定义
GC9306在SPI模式下,与我们交互的引脚并不多,通常只需要4根线(不含背光和控制电源)。这是它相比并行接口的巨大优势。
- SCK (Serial Clock): SPI时钟线,由主设备(STM32)产生,用于同步数据位传输。
- SDA (MOSI): 主设备输出,从设备输入。对于屏幕来说,这就是STM32发送命令和数据的通道。注意,在纯SPI写模式下,GC9306的MISO引脚通常不需要连接。
- DC (Data/Command): 这是一个非常关键的引脚,它不是一个SPI标准信号,而是GC9306用来区分当前SPI线上传来的是“命令”还是“数据”的。拉低时,后续字节被解释为命令(如设置地址、功能开关);拉高时,后续字节被解释为要写入显存的数据(如像素颜色)。
- CS (Chip Select): 片选信号,低电平有效。当CS为低时,GC9306才会监听SPI总线上的数据。这个引脚可以用硬件SPI的NSS引脚,但更常见的做法是使用一个普通的GPIO进行软件控制,这样更灵活。
- RESET: 复位引脚,低电平有效。用于对GC9306进行硬件复位,通常在初始化开始时使用。
- BACKLIGHT: 背光控制,通常接一个GPIO通过PWM控制其亮度。
在STM32F103C8T6(常见的“蓝色药丸”开发板)上,我们可以选择SPI1或SPI2。SPI1的引脚是PA5(SCK), PA6(MISO), PA7(MOSI), SP2的引脚是PB13(SCK), PB14(MISO), PB15(MOSI)。考虑到PA口可能用于其他功能(如USART),我通常习惯使用SPI2,并把DC、CS、RESET接到PB12、PB10、PB11这类空闲的GPIO上。接线一定要对照你的具体屏幕模块和开发板原理图,用万用表通断档确认一遍,这是避免后续“玄学”问题的第一步。
2.2 STM32F103 SPI外设配置要点
STM32的SPI配置参数众多,驱动GC9306需要特别注意以下几点:
时钟极性(CPOL)与时钟相位(CPHA): 这是SPI时序的核心。GC9306的数据手册通常会指明其SPI模式。绝大多数情况下,它工作在Mode 0,即CPOL=0(时钟空闲时为低电平),CPHA=0(数据在时钟的第一个边沿(上升沿)采样)。在CubeMX中配置时,就选择“Motorola”帧格式下的“CPOL Low, CPHA 1 Edge”。这里有个容易混淆的点:CubeMX的“CPHA 1 Edge”对应的是数据在第一个边沿采样,也就是CPHA=0。务必以数据手册和实际波形为准。
数据大小(Data Size): GC9306的SPI接口通常一次传输8位数据。因此需要设置为8 bits。
波特率预分频器(Baud Rate Prescaler): 这决定了SPI的通信速度。STM32F103的主频通常是72MHz,SPI2挂在APB1总线上(最高36MHz)。初始调试时,建议选择一个较低的分频,如256分频(~140kHz),确保通信稳定。待驱动稳定后,可以逐步提高分频系数,如8分频(4.5MHz),以提升刷屏速度。GC9306的最大SPI时钟频率需要查手册,常见规格在10MHz到30MHz之间,留有余地更稳妥。
软件片选管理: 如前所述,我强烈建议将CS引脚配置为普通GPIO输出模式,上拉或初始化为高电平(无效状态)。在发送数据前,手动拉低CS;发送完毕后,再拉高。这样你可以精确控制每次传输的帧边界,避免因硬件NSS管理带来的意外。
代码示例:SPI初始化(基于HAL库)
SPI_HandleTypeDef hspi2; void SPI2_Init(void) { hspi2.Instance = SPI2; hspi2.Init.Mode = SPI_MODE_MASTER; // 主模式 hspi2.Init.Direction = SPI_DIRECTION_2LINES; // 全双工,但只发不收 hspi2.Init.DataSize = SPI_DATASIZE_8BIT; hspi2.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL = 0 hspi2.Init.CLKPhase = SPI_PHASE_1EDGE; // CPHA = 0 (注意HAL库的命名) hspi2.Init.NSS = SPI_NSS_SOFT; // 软件管理NSS hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; // 初始低速 hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB; // 高位先传 hspi2.Init.TIMode = SPI_TIMODE_DISABLE; hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; hspi2.Init.CRCPolynomial = 10; if (HAL_SPI_Init(&hspi2) != HAL_OK) { Error_Handler(); } // 使能SPI外设时钟等初始化代码... }注意:HAL库的
SPI_PHASE_1EDGE实际上对应的是时钟的第一个边沿采样,即标准SPI模式中的CPHA=0。这是很多初学者配置不对导致屏幕无反应的一个关键点。
3. GC9306驱动芯片深度解析
3.1 命令与数据写入机制
理解了硬件连接和SPI配置,我们就要和GC9306芯片“对话”了。所有的对话都通过“命令”和“数据”两种“语言”进行,而DC引脚就是切换这两种语言的开关。
- 写命令 (Write Command): 当需要设置屏幕的工作模式、扫描方向、亮度等参数时,需要写命令。操作流程是:拉低
DC引脚 -> 拉低CS引脚 -> 通过SPI发送一个字节的命令码 -> 拉高CS引脚。 - 写数据 (Write Data): 当命令需要参数,或者要向显存写入像素颜色时,需要写数据。操作流程是:拉高
DC引脚 -> 拉低CS引脚 -> 通过SPI发送一个或多个字节的数据 -> 拉高CS引脚。
很多命令后面需要跟一个或多个数据字节作为参数。例如,设置X坐标范围的命令0x2A,后面就需要跟4个字节的数据(起始地址高8位、低8位,结束地址高8位、低8位)。因此,我们通常需要封装两个基础函数:LCD_Write_Cmd和LCD_Write_Data,以及一个复合函数LCD_Write_Cmd_Data用于发送命令和其参数序列。
// 示例:基础写入函数 void LCD_Write_Cmd(uint8_t cmd) { LCD_DC(0); // DC引脚置低,表示命令 LCD_CS(0); // 片选有效 HAL_SPI_Transmit(&hspi2, &cmd, 1, 100); // 发送命令字节 LCD_CS(1); // 片选无效 } void LCD_Write_Data(uint8_t dat) { LCD_DC(1); // DC引脚置高,表示数据 LCD_CS(0); HAL_SPI_Transmit(&hspi2, &dat, 1, 100); LCD_CS(1); } void LCD_Write_Cmd_Data(uint8_t cmd, uint8_t *dat, uint32_t len) { LCD_Write_Cmd(cmd); for(uint32_t i=0; i<len; i++) { LCD_Write_Data(dat[i]); } }3.2 关键初始化命令序列详解
屏幕上电后,GC9306处于一个未知状态,必须通过一系列命令进行初始化,才能正常显示。这个序列通常由屏幕模组厂商提供,但理解其中关键命令的意义,能让你在移植或调试时游刃有余。
- 硬件复位: 首先拉低RESET引脚至少10ms,然后拉高,并延时120ms以上,等待内部电路稳定。这是最彻底的复位方式。
- 退出睡眠模式: 发送命令
0x11(Sleep Out)。屏幕从低功耗睡眠模式唤醒。 - 设置颜色格式: 发送命令
0x3A(COLMOD: Interface Pixel Format)。GC9306支持多种颜色格式,如16位RGB565、18位RGB666等。对于SPI接口,为了平衡速度和色彩,最常用的是RGB565格式,即一个像素用2个字节(16位)表示。此时需要向该命令写入数据0x55(代表16bits/pixel)。这个设置至关重要,它决定了后续你发送的像素数据应该如何被解析。 - 设置显示方向(横屏/竖屏): 发送命令
0x36(MX, MY, RGB Mode)。通过设置这个寄存器的位,可以控制屏幕的扫描方向和颜色顺序。例如,0x00可能表示横屏模式,RGB顺序;0xC0可能表示横屏模式,但进行了MY和MX的翻转,实现了180度旋转。具体值需要查阅你屏幕的数据手册或示例代码。 - 设置列地址和页地址: 命令
0x2A(Column Address Set) 和0x2B(Page Address Set) 用于设置你接下来要写入的显存区域。GC9306的显存可以看作一个二维矩阵,0x2A设置X方向(列)的起始和结束地址,0x2B设置Y方向(页/行)的起始和结束地址。设置好后,后续的像素数据就会按顺序填充到这个矩形窗口内。全屏填充就是设置X从0到屏宽-1,Y从0到屏高-1。 - 打开显示: 最后发送命令
0x29(Display On),屏幕才会开始显示显存中的内容。
一个典型的初始化代码段如下:
void GC9306_Init(void) { // 1. 硬件复位 LCD_RST(0); HAL_Delay(20); LCD_RST(1); HAL_Delay(120); // 等待稳定 // 2. 退出睡眠 LCD_Write_Cmd(0x11); HAL_Delay(120); // 等待退出睡眠 // 3. 设置颜色格式为RGB565 LCD_Write_Cmd(0x3A); LCD_Write_Data(0x55); // 16位色 // 4. 设置横屏模式 (具体值需根据屏幕调整) LCD_Write_Cmd(0x36); LCD_Write_Data(0x00); // 示例值,可能为0x00, 0xC0, 0xA0等 // 5. 其他调整,如伽马校正、亮度等(参考屏幕资料) // ... // 6. 打开显示 LCD_Write_Cmd(0x29); HAL_Delay(50); }实操心得:初始化序列中的延时
HAL_Delay非常重要,尤其是退出睡眠后的延时。如果省略或太短,后续命令可能无法被正确执行,导致屏幕初始化失败。这是新手最容易忽略的一点。
4. 图形显示核心算法实现
初始化完成后,屏幕是亮了,但通常是一片随机噪点或纯色。要让屏幕显示我们想要的图形,核心就是向显存写入像素数据。最基础的操作是“画点”,所有复杂的图形(线、矩形、圆、图片、文字)都可以由它衍生。
4.1 画点函数与坐标系统
画点函数的本质是:告诉GC9306“我要在(X,Y)坐标处写颜色”,然后发送该颜色对应的两个字节(RGB565格式)。
流程如下:
- 设置操作窗口为单个像素点。通过
0x2A和0x2B命令,将列地址和页地址的起始、结束都设置为目标坐标(X, Y)。 - 发送写显存命令
0x2C(Memory Write)。 - 通过SPI连续发送2个字节的颜色数据(高字节在前还是低字节在前,需要根据屏幕的字节序测试,通常是高位字节先发,即R[4:0]G[5:3]组成第一个字节,G[2:0]B[4:0]组成第二个字节)。
为了提高连续画多个点的效率(比如画线、填充矩形),我们可以优化:先设置一个大的窗口,然后连续发送颜色数据,GC9306的地址指针会自动递增。这就是“打点”和“填充”的区别。
// 设置窗口函数 void LCD_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2) { LCD_Write_Cmd(0x2A); LCD_Write_Data(x1 >> 8); // 起始列高8位 LCD_Write_Data(x1 & 0xFF); // 起始列低8位 LCD_Write_Data(x2 >> 8); // 结束列高8位 LCD_Write_Data(x2 & 0xFF); // 结束列低8位 LCD_Write_Cmd(0x2B); LCD_Write_Data(y1 >> 8); LCD_Write_Data(y1 & 0xFF); LCD_Write_Data(y2 >> 8); LCD_Write_Data(y2 & 0xFF); } // 基础画点函数(效率较低,用于理解原理) void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { if(x >= LCD_WIDTH || y >= LCD_HEIGHT) return; // 边界检查 LCD_SetWindow(x, y, x, y); // 窗口设为1个像素 LCD_Write_Cmd(0x2C); // 发送写内存命令 // 发送颜色数据(16位,两字节) LCD_Write_Data(color >> 8); // 发送高8位 LCD_Write_Data(color & 0xFF); // 发送低8位 } // 优化的连续填充函数(用于画矩形、清屏等) void LCD_Fill(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { uint32_t total_pixels = (uint32_t)(x2 - x1 + 1) * (y2 - y1 + 1); LCD_SetWindow(x1, y1, x2, y2); LCD_Write_Cmd(0x2C); LCD_DC(1); LCD_CS(0); // 使用HAL_SPI_Transmit的DMA模式或快速轮询模式连续发送数据 for(uint32_t i=0; i<total_pixels; i++) { uint8_t buf[2] = {color >> 8, color & 0xFF}; HAL_SPI_Transmit(&hspi2, buf, 2, 100); // 这里可以优化为批量发送 } LCD_CS(1); }4.2 高级图形功能:线、矩形、圆与位图
基于画点函数,我们可以实现更高级的图形功能。这里以 Bresenham 画线算法为例,它是一种高效的整数算法,避免了浮点运算。
画线算法实现思路: 计算两点之间的差值dx和dy。根据斜率,决定每次是步进x还是步进y,并计算一个误差项e来决定何时需要在另一个方向上也步进。算法只涉及整数加减和比较,速度很快。
void LCD_DrawLine(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { int16_t dx = abs(x2 - x1); int16_t dy = abs(y2 - y1); int16_t sx = (x1 < x2) ? 1 : -1; int16_t sy = (y1 < y2) ? 1 : -1; int16_t err = dx - dy; int16_t e2; while(1) { LCD_DrawPoint(x1, y1, color); if (x1 == x2 && y1 == y2) break; e2 = 2 * err; if (e2 > -dy) { err -= dy; x1 += sx; } if (e2 < dx) { err += dx; y1 += sy; } } }矩形和圆形:矩形可以直接用四条线组成,或者用更快的填充函数LCD_Fill来实现实心矩形。圆形则可以使用中点圆算法,原理与Bresenham画线类似。
显示位图(图片):将图片数据(如从SD卡读取,或编译到代码中的数组)显示到屏幕上,本质是连续写入一大片像素数据。关键是要知道图片数据的格式(必须是RGB565格式),并且确保设置的正确窗口与图片尺寸匹配。通常我们会使用工具(如Img2Lcd)将JPG或PNG图片转换为RGB565格式的C语言数组。
// 显示一张RGB565格式的位图 void LCD_ShowPicture(uint16_t x, uint16_t y, uint16_t width, uint16_t height, const uint16_t *pic) { LCD_SetWindow(x, y, x+width-1, y+height-1); LCD_Write_Cmd(0x2C); LCD_DC(1); LCD_CS(0); // 假设pic是uint16_t类型的数组,每个元素就是一个像素的RGB565值 // 注意:直接传输uint16_t指针需要处理字节序。更稳妥的方式是按字节发送。 for(uint32_t i=0; i<width*height; i++) { uint16_t color = pic[i]; uint8_t buf[2] = {color >> 8, color & 0xFF}; HAL_SPI_Transmit(&hspi2, buf, 2, 100); } LCD_CS(1); }注意事项:当需要刷新大量像素(如全屏刷图)时,使用
HAL_SPI_Transmit单次发送2字节的效率极低,因为每次调用都有函数开销和CS控制开销。此时应该将CS控制放在循环外,并使用HAL_SPI_Transmit的DMA模式,或者将多个像素数据打包到一个大的缓冲区后一次性发送,可以极大提升刷新率。
5. 性能优化与高级技巧
5.1 SPI通信速度优化策略
驱动屏幕,尤其是动画或视频播放,刷新率(FPS)是关键。而SPI的传输速度是瓶颈。以下是一些行之有效的优化手段:
- 提升SPI时钟频率: 在确保屏幕能稳定接收的前提下,逐步提高
BaudRatePrescaler。从256分频尝试到8、4甚至2分频。同时,确保STM32的APB总线时钟已配置到最高(对于SPI2是36MHz)。 - 使用DMA传输: 这是提升性能最有效的方法。将需要发送的像素数据缓冲区地址交给DMA,SPI外设会在后台自动发送,CPU在此期间可以处理其他任务(如准备下一帧数据)。HAL库提供了
HAL_SPI_Transmit_DMA函数。使用DMA时,需要注意数据缓冲区的内存对齐和缓存一致性(如果用了D-Cache)。 - 优化GPIO操作: 对于软件控制的
DC和CS引脚,使用位带操作或直接寄存器操作(GPIOx->BSRR)来替代HAL_GPIO_WritePin函数,可以减少函数调用开销。 - 减少传输开销: 对于连续像素数据写入,只在开始设置一次窗口和
0x2C命令,然后保持DC=1和CS=0,连续发送所有像素数据,最后再拉高CS。避免每两个字节就切换一次CS。 - 使用硬件SPI的FIFO: 确保SPI的TXFIFO阈值设置合理,避免频繁中断。
示例:使用DMA进行全屏填充
uint16_t screen_buffer[LCD_WIDTH * LCD_HEIGHT]; // 定义一个全屏缓冲区 void LCD_Refresh_DMA(void) { // 1. 将需要显示的内容填充到screen_buffer // ... (例如,将UI图形绘制到buffer中) // 2. 设置全屏窗口 LCD_SetWindow(0, 0, LCD_WIDTH-1, LCD_HEIGHT-1); LCD_Write_Cmd(0x2C); LCD_DC(1); LCD_CS(0); // 3. 启动DMA传输 // 注意:因为SPI是8位传输,而我们的buffer是16位的,需要转换为8位流。 // 一种方法是定义uint8_t的buffer,或者使用DMA的半字/字节传输模式配合数据寄存器。 // 这里假设我们使用uint8_t buffer[LCD_WIDTH*LCD_HEIGHT*2] HAL_SPI_Transmit_DMA(&hspi2, (uint8_t*)screen_buffer, LCD_WIDTH*LCD_HEIGHT*2); // 传输完成会在SPI_TxCpltCallback中断中通知 }5.2 双缓冲与局部刷新机制
对于动态UI,直接刷全屏(即使用了DMA)也可能导致可见的撕裂现象(上一帧和下一帧内容混合)。此时可以引入双缓冲机制。
- 原理:在内存中开辟两个与屏幕大小相同的缓冲区(Frame Buffer)。UI渲染线程只向“后台缓冲区”绘制。当一帧绘制完成后,通过一个“垂直同步”信号(可以是定时器中断),将“后台缓冲区”的内容通过DMA快速搬运到屏幕上,同时交换前后台缓冲区指针。这样,屏幕始终显示完整的一帧。
- 内存开销:对于一块240x320的16位色屏幕,一个缓冲区需要 240 * 320 * 2 = 150KB。STM32F103C8T6只有64KB RAM,显然不够。但对于STM32F103RC(256KB RAM)或更高型号是可行的。如果内存紧张,可以考虑局部刷新。
- 局部刷新:只更新屏幕上发生变化的部分区域。例如,一个数字时钟,只有秒数字在变,那就只刷新秒数字所在的那一小块矩形区域。这需要UI框架记录每个控件的“脏矩形”区域,并在刷新时合并这些区域进行更新,能极大减少数据传输量。
5.3 字体显示与GUI基础
在屏幕上显示文字是基本需求。通常我们使用点阵字体,将每个字符的笔画信息存储在一个数组中(字库)。
- 字库获取与存储: 可以使用PC工具生成特定字体、大小的字库数组,直接编译到代码中(占用Flash)。对于中文字库,体积庞大,需要外置Flash或SD卡存储,并按需读取。
- 显示一个字符: 根据字符的ASCII码或Unicode码找到对应的点阵数据。点阵数据通常按行存储,每个位代表一个像素(1点亮,0点不亮或为背景色)。我们遍历这些位,在屏幕上相应的位置画前景色或背景色。
- 基础GUI元素: 基于画点、画线、填充矩形和显示字符的函数,就可以构建按钮、标签、进度条等简单GUI元素。核心是维护每个元素的坐标、状态(如按下、释放),并在相应事件(如触摸、定时)发生时重绘它们。
// 显示一个ASCII字符的简单示例(8x16字体) void LCD_ShowChar(uint16_t x, uint16_t y, char chr, uint16_t color, uint16_t bgcolor) { uint8_t i, j; uint8_t *pFont = &asc2_8x16[(chr - ' ') * 16]; // 假设字库数组是asc2_8x16 for(i=0; i<16; i++) { // 16行 uint8_t line = pFont[i]; for(j=0; j<8; j++) { // 8列 if(line & (0x80 >> j)) { LCD_DrawPoint(x+j, y+i, color); } else if(bgcolor != color) { // 如果背景色与前景色不同,则画背景 LCD_DrawPoint(x+j, y+i, bgcolor); } } } }6. 调试实战与常见问题排查
驱动屏幕的过程很少一帆风顺,以下是我在多个项目中总结的常见问题及排查方法,堪称“避坑指南”。
6.1 屏幕无任何显示(白屏、黑屏、花屏)
这是最常见的问题。请按以下顺序排查:
- 电源与背光: 首先确认屏幕的VCC和GND连接正确且电压稳定(通常是3.3V或5V)。用万用表测量。然后检查背光引脚(BL/BLK)是否接高电平或PWM信号。有些屏幕背光需要串联限流电阻。
- 复位时序: 确保复位信号(RESET)的时序正确。上电后,拉低至少10ms,然后拉高,并等待足够长的时间(>100ms)再进行初始化。可以在复位后加一个长延时(如200ms)再初始化。
- SPI波形: 使用逻辑分析仪或示波器抓取SCK、MOSI、DC、CS的波形。这是最直接的调试手段。
- 检查SCK是否有时钟输出: 如果没有,检查SPI外设时钟是否使能,GPIO是否配置为复用推挽输出。
- 检查MOSI数据: 对照初始化命令序列,看发送的数据是否正确。特别注意第一个命令(如
0x11)是否成功发出。 - 检查DC电平: 发送命令时,DC应为低;发送数据时,DC应为高。如果DC引脚一直为低,屏幕会把所有数据当命令,导致初始化失败。
- 检查CS片选: 确保在发送数据期间CS保持为低。
- SPI模式与相位:这是重中之重!用逻辑分析仪看波形。在SCK空闲时,电平应该是低的(CPOL=0)。数据应该在SCK的上升沿被采样(CPHA=0)。如果你的波形不符合,请调整
CPOL和CPHA配置。我遇到过屏幕要求CPHA=1(下降沿采样)的情况。 - 初始化序列与延时: 确认你的初始化命令序列完全正确,特别是
0x36(方向)、0x3A(颜色格式)和0x29(开显示)。每个命令后的延时是否足够?尤其是0x11(退出睡眠)之后,建议延时120ms以上。 - 颜色格式: 如果你发送了像素数据但颜色完全不对或花屏,检查
0x3A命令设置的颜色格式(如0x55代表RGB565)与你程序中颜色数据的组织方式是否匹配。例如,你定义的颜色0xF800是红色,但屏幕可能以BGR顺序解析,导致显示蓝色。
6.2 显示错位、镜像或颜色异常
- 显示方向错乱: 修改
0x36命令的参数。尝试0x00,0xC0,0xA0,0x60等常见值,观察屏幕旋转和镜像效果。具体值需要查屏规。 - 窗口设置错误: 画点或图片时,如果显示位置偏移,检查
LCD_SetWindow函数中坐标的计算和发送顺序(先高8位还是低8位)。GC9306的坐标原点通常在左上角。 - 颜色反相或错乱: 除了检查
0x3A格式,还要检查SPI的字节序(MSB/LSB First)。STM32的SPI通常配置为MSB First。如果颜色还是不对,尝试交换颜色数据的高低字节。即原本发送{color>>8, color&0xFF},改为发送{color&0xFF, color>>8}。
6.3 刷新速度慢,动画卡顿
- SPI时钟太低: 提高SPI波特率分频系数。
- 函数调用开销大: 避免在循环内频繁调用
HAL_SPI_Transmit发送单个字节或双字节。改用批量发送。 - 未使用DMA: 对于全屏或大块区域更新,务必使用DMA。
- 不必要的全屏刷新: 实现局部刷新逻辑,只更新变化的区域。
- CPU主频未最大化: 确保STM32F103运行在72MHz。
6.4 内存不足问题
在STM32F103C8T6(20K RAM)上驱动大屏幕时,内存非常紧张。
- 避免大数组: 不要定义全屏的帧缓冲区。使用局部刷新。
- 使用
const将常量存到Flash: 将字库、图片数据等用const修饰,编译器会将其放入Flash,节省RAM。 - 优化变量类型: 在满足范围的前提下,使用
uint8_t、int16_t等精确宽度的类型。 - 使用内存管理: 如果必须使用大缓冲区,考虑使用外部SRAM(如通过FSMC接口)。
调试记录表示例:
| 现象 | 可能原因 | 排查步骤 | 解决方法 |
|---|---|---|---|
| 白屏,背光亮 | 初始化失败 | 1. 查电源/复位 2. 逻辑分析仪看SPI波形,第一个命令 0x11是否发出?DC电平是否正确? | 检查接线、SPI配置、初始化序列延时 |
| 花屏,有彩色条纹 | 颜色格式或数据位序错误 | 1. 检查0x3A命令参数2. 单色填充测试(如全红 0xF800),看显示是否正确 | 调整0x3A值,或交换颜色字节发送顺序 |
| 显示内容镜像或旋转 | 扫描方向设置错误 | 修改0x36命令的参数,尝试不同值 | 查阅屏幕规格书,找到正确的0x36参数 |
| 刷新极慢,刷屏有拖影 | SPI速度慢,或软件开销大 | 1. 提高SPI波特率 2. 使用逻辑分析仪测量两次刷新的时间间隔 | 优化代码,使用DMA,提高SPI时钟 |
最后,驱动屏幕是一个软硬件结合非常紧密的工作。耐心和细致的观察(尤其是借助逻辑分析仪)是成功的关键。从最基础的打点开始,逐步实现线、面、文字,最终构建起整个显示功能,这个过程本身就是对嵌入式系统通信和图形处理的一次深刻学习。当你点亮的第一个像素在屏幕上如期而至时,那种成就感就是对我们工程师最好的回报。