STM32F103驱动GC9A01圆屏:从初始化到圆弧绘制的完整方案
2026/9/3 22:56:20 网站建设 项目流程

简介:面向使用STM32F103驱动1.28寸TFT液晶屏的嵌入式开发者,基于GC9A01驱动芯片,屏幕分辨率240×240像素,采用硬件SPI通信。压缩包内为完整Keil MDK工程,共148个文件,压缩后约2.81MB。文件以h头文件和c源文件为主,覆盖STM32标准外设库、系统初始化及液晶显示驱动代码;同时包含uvprojx工程、hex烧录文件、axf调试文件、map映射文件等,拿到手即可编译、烧录与调试。资料中明确给出了电源和SPI数据线的接线说明,引脚定义清晰,便于对照实物快速连接;启动文件、链接脚本与编译批处理等一应俱全,目录结构完整。已有2328人学习浏览,适合正在学习STM32裸机驱动TFT屏幕、需要参考GC9A01时序和硬件SPI配置的开发者,也适合需要快速完成1.28寸圆屏显示项目或进行二次开发的工程师。 这个1.28寸小圆屏我前后玩了两三块,从最早裸屏加杜邦线飞线,到后来画好板子做小手表,踩过的坑还挺多。这个尺寸配GC9A01驱动、240x240分辨率,加上STM32F103这颗经典M3内核芯片,组合起来做桌面小摆件、温湿度计、迷你游戏机、智能手环原型都特别合适。网上关于GC9A01的资料大多集中在ESP32和Arduino平台,专门讲STM32F103配合库函数开发的相对零散,很多新手一上来就被初始化序列和取模方式卡住。这篇就把我实际跑通过的一套方案完整拆开讲,从接线、SPI配置到画点画圆、显示图片,再到掉电保存和低功耗停机模式,一次性说清楚。

1. 为什么选这套组合

1.1 GC9A01这颗驱动芯片的特殊之处

GC9A01跟常见的ILI9341、ST7789有个本质区别:它天生就是为圆形屏幕设计的。传统方屏的驱动IC在扫描方式上倾向于矩形坐标系,而GC9A01内部虽然也是240x240的像素矩阵,但它在行列扫描、显示窗口设置上针对圆屏做了适配,配合物理上切成圆形的液晶面板,就能做到四角完全无显示区域浪费,边缘像素也不会出现奇怪的拉伸或偏色。

另外这颗IC的SPI接口速度上限很高,理论上可以跑到10MHz以上甚至更高。对STM32F103来说,它的硬件SPI外设最高是18MHz,刚好接得住。我实测把SPI时钟配到9MHz左右,用DMA搬运一整帧240x240的RGB565数据(大约115KB),刷新一屏可以压到大概60到70毫秒,肉眼看基本没有撕裂感。如果不用DMA纯靠CPU一个像素一个像素推,刷新时间会翻好几倍,这也是很多人在小圆屏上感觉“卡”的根本原因。

1.2 STM32F103作为主控的性价比

很多人会问为什么不用ESP32,ESP32驱动这类屏幕资料更多、性能也更强。但STM32F103的优势在于:一是价格足够低,最小系统板几块钱就能拿到,裸片更是便宜到可以随便焊;二是它的SPI、I2C、定时器、ADC、DMA这些外设足够齐全,做小型交互设备完全够用;三是它的生态太成熟了,标准外设库、HAL库、寄存器版代码都能找到大量参考,遇到问题好排查。

更实际的一点是:GC9A01需要3.3V逻辑电平,STM32F103的GPIO正好是3.3V,不需要电平转换,电源直接共3.3V就行,外围电路极简。如果用5V单片机比如Arduino Uno,还得加电平转换芯片或者电阻分压,麻烦不少。F103还支持Stop模式,电流能降到微安级别,配合RTC唤醒做低功耗表盘项目也很顺手。

1.3 这套方案能做什么

说白了,这个组合可以覆盖三类典型场景:

  • 桌面小摆件:显示时钟、倒计时、温湿度曲线、CPU占用率。
  • 可穿戴原型:圆形表盘天然契合手表形态,240x240的分辨率显示卡通表盘够用。
  • 入门学习板:SPI屏驱动、DMA传输、图形算法、低功耗管理,一块屏一次全练到。

这篇文章会以标准库(SPL)为背景写代码,因为网上F103的库函数资料最多,很多人的板子也在用这个。HAL库的思路完全一样,SPI时序、初始化命令都是通用的,移植过去只改底层接口就行。

2. 硬件接线与引脚分配

2.1 标准接线表

GC9A01这个屏幕模组一般引出8个引脚,也有带触摸的版本会多出几个。我们只用不带触摸的基础版本,引脚定义很固定:

屏幕引脚功能接STM32F103引脚
VCC电源正3.3V
GND电源地GND
SCL / SCKSPI时钟PA5(SPI1_SCK)
SDA / MOSISPI数据PA7(SPI1_MOSI)
RES / RST复位PA3,任意GPIO
DC / RS数据/命令选择PA2,任意GPIO
CS片选PA4,任意GPIO
BLK / BL背光PA1,任意GPIO或直接3.3V

这里需要特别说明两个引脚:DC和CS用普通GPIO控制即可,不一定要挂到SPI的硬件NSS上,因为手动拉低拉高更灵活,也不需要担心NSS自动模式下的各种诡异行为。BLK背光引脚如果直接接3.3V,就是常亮,我习惯用一个GPIO控制,方便做息屏省电。

2.2 为什么选SPI1而不是SPI2

STM32F103有SPI1和SPI2,SPI1挂载在APB2总线上,时钟最高72MHz;SPI2挂载在APB1上,最高36MHz。虽然对9MHz的SPI时钟来说两者都够用,但SPI1的引脚PA5、PA6、PA7是默认复用功能脚,配置起来更顺手,而且很多开发板的排针布局就是把SPI1引出来的,插上就能用。

另外要注意:GC9A01只支持写操作,不需要读数据(除非你要读ID或者做回读校验),所以SPI的MISO引脚根本不用接。这意味着我们可以用“半双工写”模式,也可以干脆用软件SPI。但我的建议是:尽量用硬件SPI+DMA,因为软件SPI在240x240这种分辨率下性能太吃力,后期画曲线、刷图片都会卡。

2.3 供电与电平匹配的实际经验

这块屏的背光LED电流不小,全亮时整个模组大概能到40到60毫安。如果直接用STM32最小板上的3.3V LDO供电,板上其他外设(比如无线模块、传感器)一起工作的时候电压容易掉到3.2V以下,屏幕就会出现闪烁、颜色发虚的问题。我做过一个比较稳的方案:用单独的3.3V稳压芯片(比如AMS1117-3.3或RT9013)给屏幕供电,主控板和屏幕共地,但电源路径分开,能明显改善显示稳定性。

注意:GC9A01的逻辑电平虽然是3.3V,但它的IO引脚耐压并不一定支持5V。如果你的主控是5V逻辑,一定要加电平转换,否则长期运行有烧毁驱动IC的风险。这个项目里STM32F103是3.3V IO,天然安全。

3. 驱动初始化与底层显示接口

3.1 初始化序列背后的逻辑

GC9A01的上电初始化顺序网上各种版本都有,但核心步骤是固定的:硬件复位 -> 软件复位 -> 关显示 -> 设置电源参数 -> 设置显示窗口 -> 开显示。我整理了一份实测稳定的序列,直接用就行:

void GC9A01_Init(void) { // 硬件复位:RES拉低至少10ms再拉高 GC9A01_RST_LOW(); delay_ms(20); GC9A01_RST_HIGH(); delay_ms(20); // 软件复位 GC9A01_WriteCmd(0x01); // SWRESET delay_ms(120); GC9A01_WriteCmd(0x28); // DISPLAY OFF GC9A01_WriteCmd(0xEF); // 解锁内部命令 GC9A01_WriteCmd(0xEB); GC9A01_WriteData(0x14); GC9A01_WriteCmd(0xFE); GC9A01_WriteCmd(0xEF); // ... 中间是一长串厂商寄存器设置 GC9A01_WriteCmd(0x20); // INVOFF,不反色 GC9A01_WriteCmd(0x21); // INVON,某些批次需要开反色才正常 GC9A01_WriteCmd(0x36); // MADCTL 扫描方向 GC9A01_WriteData(0x48); // 根据安装方向调整 GC9A01_WriteCmd(0x3A); // COLMOD 像素格式 GC9A01_WriteData(0x05); // 16位RGB565 GC9A01_WriteCmd(0x2A); // 列地址设置 GC9A01_WriteData(0x00); GC9A01_WriteData(0x00); GC9A01_WriteData(0x00); GC9A01_WriteData(0xEF); // 239 GC9A01_WriteCmd(0x2B); // 行地址设置 GC9A01_WriteData(0x00); GC9A01_WriteData(0x00); GC9A01_WriteData(0x00); GC9A01_WriteData(0xEF); // 239 GC9A01_WriteCmd(0x29); // DISPLAY ON delay_ms(30); }

中间那一大串EF、FE、EB之类的寄存器命令,不同批次屏幕可能略有差异,有的屏幕不开反色显示是反的,有的色调偏蓝偏红,这些都是因为厂商在驱动IC内部预设了不同的Gamma曲线。如果你发现屏幕颜色不对,优先调整0x20/0x21、0x36以及Gamma相关的寄存器,不要一上来就怀疑硬件坏了。

3.2 SPI底层读写与DC引脚时序

GC9A01的数据格式很简单:DC引脚为低时,SPI发送的是命令字节;DC引脚为高时,SPI发送的是数据。很多新手容易在这里犯错——初始化序列里明明发的是命令,但因为DC没拉低,屏幕就完全没反应。

我习惯把底层封装成两个函数:

void GC9A01_WriteCmd(uint8_t cmd) { GC9A01_DC_LOW(); GC9A01_CS_LOW(); SPI1_SendByte(cmd); GC9A01_CS_HIGH(); } void GC9A01_WriteData(uint8_t dat) { GC9A01_DC_HIGH(); GC9A01_CS_LOW(); SPI1_SendByte(dat); GC9A01_CS_HIGH(); }

这里有个细节:CS引脚在每次传输前后都拉低拉高,虽然会有点耗时,但能保证多设备SPI总线上不会冲突。如果你的屏独占SPI总线,可以把CS直接拉低不操作,速度还能快一点。不过为了通用性和稳定性,我还是建议保留CS控制。

SPI配置上,我用的是SPI1,主模式,8位数据,CPOL=0、CPHA=0(Mode 0),9MHz分频。F103的SPI时钟分频选择是2、4、8、16等,系统时钟72MHz时选8分频正好9MHz。

3.3 画点函数与颜色格式

画点是所有显示功能的地基。GC9A01的显示窗口机制和ILI9341类似,先通过0x2A和0x2B命令设置画布区域,再用0x2C命令连续写入像素数据。最简单的画点流程:

void GC9A01_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { GC9A01_WriteCmd(0x2A); GC9A01_WriteData(x0 >> 8); GC9A01_WriteData(x0 & 0xFF); GC9A01_WriteData(x1 >> 8); GC9A01_WriteData(x1 & 0xFF); GC9A01_WriteCmd(0x2B); GC9A01_WriteData(y0 >> 8); GC9A01_WriteData(y0 & 0xFF); GC9A01_WriteData(y1 >> 8); GC9A01_WriteData(y1 & 0xFF); GC9A01_WriteCmd(0x2C); // RAM写命令 } void GC9A01_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { if (x >= 240 || y >= 240) return; GC9A01_SetWindow(x, y, x, y); GC9A01_WriteData(color >> 8); GC9A01_WriteData(color & 0xFF); }

注意GC9A01是RGB565格式,但SPI发送时高位在前,也就是说一个像素需要两个字节:高字节在前、低字节在后。很多人直接把一个16位数用memcpy发过去,结果颜色错乱,就是因为字节序对不上。另外,GC9A01内部的RGB排列并不一定是标准RGB,有的批次是BGR,如果发现红蓝对调,就在初始化里改0x36寄存器的值,或者在写像素时交换R和B通道。

4. 圆屏绘制的核心难点:圆弧与图形算法

4.1 圆屏显示的特殊性问题

240x240的方形矩阵套上一个圆形的物理屏幕,最尴尬的问题就是:四角区域会被裁掉。如果你用普通的画矩形求交集逻辑,在四角画一个矩形UI,边缘会硬生生被切掉,非常难看。所以圆屏UI的设计思路要反过来:以圆心为中心,所有元素尽量沿径向排布,表盘指针、弧形进度条、圆形菜单都是圆屏的标配元素。

也因此,画圆弧成了这个项目里绕不开的算法需求。网上搜索“tft 屏幕绘制圆弧代码 .c”能找到很多版本,但大多数是针对方屏的,直接搬过来在圆屏上画弧,经常出现端点对不齐、线条粗细不均的问题。

4.2 中点画圆法与圆弧裁剪

画整圆最经典的是Bresenham或者中点画圆算法,用整数运算加移位,避免浮点运算带来的性能损耗。但在圆屏上,更常用的是只画一段圆弧,比如表盘上的刻度、弧线进度条。

我的做法是:先用参数方程计算圆弧上的点,再配合屏幕中心做平移,最后判断点是否落在300像素半径的圆形裁切区域内。伪代码如下:

void GC9A01_DrawArc(uint16_t cx, uint16_t cy, uint16_t radius, int16_t start_angle, int16_t end_angle, uint16_t color) { // 角度范围归一化 if (end_angle < start_angle) end_angle += 360; for (int16_t angle = start_angle; angle <= end_angle; angle += 2) { // 角度转弧度,用查表法优化 float rad = angle * PI / 180.0f; int16_t x = cx + (int16_t)(radius * cosf(rad)); int16_t y = cy + (int16_t)(radius * sinf(rad)); // 圆形裁切:只画落在物理圆内的像素 int dx = x - 120; int dy = y - 120; if (dx*dx + dy*dy <= 120*120) { GC9A01_DrawPoint(x, y, color); } } }

这里angle步长取2是为了在240x240分辨率下相邻点不会太稀疏,如果半径更大、对精度要求高,可以改步长为1,但速度会慢一倍。这个函数在F103主频72MHz下,画一条半径100像素的90度圆弧,大约耗时几十毫秒,用来做表盘刻度完全够用。

要想画粗弧线,一个简单有效的办法是画多个半径偏移的细弧线叠加:

for (int r = radius - 2; r <= radius + 2; r++) { GC9A01_DrawArc(cx, cy, r, start_angle, end_angle, color); }

这个方法比计算每点的矩形填充要简单得多,实测圆弧边缘比较均匀,不会出现明显的锯齿感。

4.3 双缓冲、DMA与刷新性能优化

在240x240分辨率下,RGB565全屏数据是115KB,而STM32F103的RAM通常在20KB到64KB之间。普通型号(比如C8T6只有20KB RAM)根本无法容纳双缓冲。所以小内存芯片上不要想着开全屏双缓冲,而是要区域双缓冲:每次只更新UI的一部分,比如表盘的时针区域、数字区域。

我的实际方案是做一个128x128的区域缓冲区,先在这个小缓冲区里绘制图形,再一次性DMA发送给屏幕。配合DMA传输,CPU可以在SPI搬运的间隙去做别的事情。SPI DMA配置注意事项:

void GC9A01_DMA_SendBuffer(uint16_t *buf, uint32_t size) { // size为半字数 DMA_Cmd(DMA1_Channel3, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel3, size); DMA_Cmd(DMA1_Channel3, ENABLE); // SPI发送8位数据,但这里我们按16位传输 SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE); while (DMA_GetFlagStatus(DMA1_FLAG_TC3) == RESET); SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, DISABLE); }

注意:GC9A01在0x2C命令之后,连续写入的像素数据是按设置窗口的行列顺序自动填充的。要高效刷屏,最好把屏幕方向调成MADCTL的0x48,这样行扫描顺序和内存数组顺序能对应起来,免得每次显示前还要做坐标变换。

5. 常见问题与排查实录

5.1 白屏、黑屏、花屏的排查顺序

遇到屏幕完全不显示的时候,我习惯按下面这个顺序查,能解决九成问题:

现象可能原因排查方式
完全不亮背光电源没接、BLK引脚悬空万用表量背光两端电压,正常3.3V左右
白屏复位时序不对、初始化失败检查RES引脚,确认复位低电平时间够10ms
花屏/颜色错乱SPI速率过高、MADCTL方向不对、RGB/BGR反了SPI分频降到8或16试,改0x36寄存器
显示内容偏左或偏移初始化地址设置错误检查0x2A和0x2B写入的边界值
闪烁电源供电不足、SPI速度过高单独供电、降低SPI时钟、加滤波电容

最常见的坑其实是复位引脚。有些屏幕模组的RES引脚上拉了RC复位电路,主控GPIO如果配置成开漏输出,拉低的时候可能拉不到地,导致复位不完全。我一般把RES引脚配成推挽输出,并且上电后先拉低20ms再拉高,给内部电路充足的复位时间。

5.2 颜色偏差问题

很多人反映屏幕显示红色变成蓝色,或者整体偏绿,这基本上是两个原因:一个是RGB565字节序发送错误,另一个是GC9A01的RGB通道映射和预期不同。字节序问题我已经在上面提到了,而通道映射问题可以通过修改0x36寄存器的BGR位来解决。

如果屏幕偏暗,也要检查一下0x3A寄存器是不是被设成了12位RGB444模式,如果初始化序列里误写了0x03,显示会明显偏色且暗。正确的16位RGB565设置是0x05。

5.3 掉电保存与低功耗停机的实用经验

很多做桌面时钟或者手表的朋友都会遇到两个问题:掉电后时间被重置、电池供电时功耗太高。F103的掉电保存可以用内部备份寄存器或者Flash写入来实现。备份寄存器最简单:

// 先开启PWR和BKP时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); // 写入参数 BKP_WriteBackupRegister(BKP_DR1, year); BKP_WriteBackupRegister(BKP_DR2, month); // 掉电后重新上电直接读取 uint16_t saved_temp = BKP_ReadBackupRegister(BKP_DR1);

备份寄存器在VDD断掉但VBAT有电池时会保持数据,这个特性很适合做时间保存和小参数存储。如果连VBAT都没有,就只能用Flash写入了,但要注意Flash擦写寿命约1万次,不能频繁写。

低功耗停机模式也是F103的经典用法。进入Stop之前,要把SPI外设关掉、把所有GPIO设成低功耗状态,然后:

PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); // 唤醒后需要重新配置系统时钟,因为Stop模式会关闭HSI/PLL SystemInit();

实测在外部晶振+Stop模式下,整机电流能做到几十微安,配一个100mAh的纽扣电池,做静态表盘能撑很久。但要注意唤醒后SPI外设需要重新初始化,直接唤醒就开始刷屏的话,容易花屏。

5.4 内部晶振运行的风险

有几次我图省事,用F103的内部HSI时钟跑屏幕,结果发现SPI波特率偏差比较大,屏幕有时候会出现横条纹干扰。这是因为HSI的精度只有±1%左右,而GC9A01对SPI时序要求其实没有那么苛刻,但时钟偏移太大会导致数据采样点错位。如果必须用内部晶振,SPI分频最好再降一挡,用4MHz或者2MHz,留足裕量。不过做低功耗项目时,进入Stop模式再唤醒后时钟会切回HSI,这时候也要这么处理。

6. 工具链与取模操作

6.1 图片取模的正确姿势

要在圆屏上显示非文字图形,最常见的方式是取模工具生成C语言数组。我用过Image2Lcd和PCtoLCD2002,两者都能输出C数组,但有三个坑要注意:

  • 输出格式:选16位真彩色(RGB565),不是24位真彩色,否则一个像素占3字节,屏幕显示错乱。
  • 字节序:Image2Lcd里有个“高位在前/低位在前”的选项,一定要和高位在前的SPI匹配,或者生成后手动交换高低字节。
  • 扫描方向:取模的扫描方向要和MADCTL设置一致,否则图片是镜像或者旋转90度。

图片分辨率建议直接处理成240x240,因为如果先取大图再在MCU端缩放,F103的运算量不小,显示速度会很慢。

6.2 图片显示的内存管理

一张240x240的RGB565图片数组大小是115200字节,而F103C8T6的Flash只有64KB,放不下一张图。所以要做图片显示的项目,建议用F103ZET6(512KB Flash)或者改用外部Flash,再配合部分窗口更新的方式,一次只刷一部分图片数据。

我之前做过一个方案:把图片切成16个60x60的小块,存到外部SPI Flash里,需要显示某个区域时只读取对应的块,这样可以大幅减少内存占用。缺点是读取和传输时间会累积,但做静态页面完全没问题。

6.3 第三方工具:Pillow与调试辅助

热词里提到的“pillow tft lcd”让我想到一个很实用的工作流:先在PC端用Python的Pillow库生成测试图片、字体位图,再转成C数组烧到MCU里。这样能快速预览效果,不用反复烧录固件。具体做法是:

from PIL import Image, ImageDraw, ImageFont img = Image.new("RGB", (240, 240), (0, 0, 0)) draw = ImageDraw.Draw(img) draw.text((20, 100), "Hello", fill=(255, 255, 255)) # 转成RGB565格式的C数组 pixels = img.load() for y in range(240): for x in range(240): r, g, b = pixels[x, y][:3] rgb565 = ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)

这个方法的优势是Pillow的绘图API非常成熟,画圆弧、画多边形、做抗锯齿都比在MCU端简单得多。对于UI原型验证阶段,用Python生成好效果图,确认没问题后再用C在MCU上实现,开发效率能提高好几倍。

7. 调试工具与扩展玩法

7.1 用逻辑分析仪抓SPI波形

调试SPI屏最有效的工具不是示波器,而是逻辑分析仪。我习惯在SCK和SDA引脚上夹两个探头,抓一段初始化序列,然后用Saleae或者PulseView查看数据。重点看三点:

  • 时钟极性是否正确(空闲时SCK是低还是高)。
  • 命令字节发送时DC引脚是否确实为低。
  • SPI速率是不是太高导致数据建立时间不足。

很多时候屏幕莫名奇妙地初始化失败,用逻辑分析仪一看就能发现问题,比反复改代码快太多了。

7.2 屏幕方向与坐标系变换

GC9A01的0x36寄存器(MADCTL)控制扫描方向,不同值对应不同的旋转方向:

MADCTL值效果
0x00正常方向,左上角起点
0x48常见手表方向
0x80左右镜像
0xC0水平+垂直翻转

我实际使用发现,如果要把屏幕竖着安装在手表壳里,0x48或者0xC8比较顺手,具体要看你的安装方向。改完之后,记得画点函数的x、y边界逻辑也要跟着调整,因为屏幕的行列方向和显示坐标并不总是一一对应的。

7.3 后续扩展:FreeRTOS、WS2812B、多串口

这套硬件平台跑FreeRTOS很合适,因为屏幕刷新可以通过DMA在后台搬运,CPU可以同时处理传感器数据、按键扫描和无线通信任务。我试过在F103上跑一个简单的任务:一个任务读温度传感器,一个任务刷新表盘UI,一个任务处理按键事件,整体很流畅。

如果要做氛围灯或者RGB灯带,F103的PWM+DMA可以实现WS2812B的控制,PA1、PA3这些引脚都可以接灯带的数据线。屏幕和灯带一起配合,做出来的小摆件视觉效果会提升一个档次。

另外一个常见需求是多串口通信。F103有USART1、USART2、USART3,可以一个串口接调试、一个接传感器、一个接上位机,用HAL库的CD C多串口或者标准库直接配置都很方便。屏幕显示和串口并行处理不冲突,只要别在中断里做SPI通信就行。

8. 个人版完整初始化代码参考

最后把我实际在项目里用过的底层驱动文件核心部分贴出来,包含SPI初始化、引脚配置和屏幕初始化,可以直接抄作业:

void SPI1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); // SCK: PA5, MOSI: PA7, 复用推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // CS: PA4, DC: PA2, RES: PA3, 推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx; // 只发送 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; // 9MHz SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }

这里的SPI_Direction选的是单线发送模式,因为不需要读数据,可以简化DMA配置。如果你的驱动库版本不支持这个宏,用SPI_Direction_2Lines_FullDuplex也可以,只是MISO会多占一个引脚,不影响使用。

uint8_t SPI1_SendByte(uint8_t byte) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, byte); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); return SPI_I2S_ReceiveData(SPI1); }

注意如果SPI配置成半双工模式,发送后也要读一下RXNE标志来清缓冲,否则会卡死在发送循环里。

说实话,屏幕驱动这块踩坑最多的不是代码本身,而是初始化序列和屏幕批次之间的差异。我手上三块GC9A01屏幕,有两块需要开启反色命令才能正常显示,有一块不需要,最后只能分别在初始化里做条件编译处理。如果你买的屏幕显示颜色是反的,别怀疑硬件,先把0x20改成0x21试试。这套驱动方案我跑了小半年,大部分问题都集中在供电和初始化时序上,真正把这两块理顺之后,这个小圆屏能玩出来的东西比你想象得多。

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

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

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

立即咨询