基于GC9A01圆形显示屏与XIAO开发板的嵌入式GUI开发实战
2026/8/3 0:25:56 网站建设 项目流程

1. 项目缘起:为什么需要一块圆形显示屏扩展板?

最近在捣鼓一个桌面小摆件项目,想做一个既有科技感又不失设计感的桌面时钟。市面上常见的方形显示屏看腻了,总想搞点不一样的。正好手头有个树莓派Pico,就想给它配个屏幕。在翻看Seeed Studio的配件库时,一块名为“Grove - Round Display for XIAO”的圆形显示屏扩展板一下子抓住了我的眼球。它的外观非常独特,一块1.28英寸的圆形LCD,分辨率是240x240,像素密度不低,显示效果很细腻。最关键的是,它通过Grove接口与Seeed自家的XIAO系列开发板(比如XIAO RP2040, XIAO ESP32C3等)连接,号称即插即用,这大大降低了硬件连接和驱动的复杂度。

对于很多嵌入式开发者和创客来说,选择圆形显示屏往往不只是为了“好看”。在很多应用场景下,圆形是更自然、更高效的信息载体。比如智能手表表盘、汽车仪表盘、家电的旋钮控制界面、甚至是复古风格的收音机界面,圆形显示区域能更好地聚焦视觉中心,信息排布也更符合人机交互直觉。相比于在方形屏幕上画一个圆形的UI,直接使用物理圆形屏幕,能实现更高的屏占比和更沉浸的视觉体验,省去了处理方形屏幕四个角空白区域的麻烦。

这块扩展板的核心是一块GC9A01驱动芯片的LCD,它通过SPI接口与主控通信。Seeed Studio很贴心地将其做成了Grove接口的扩展板形式,这意味着我们不需要再去费力地焊接七八根线(电源、地线、SPI的MOSI, MISO, SCK,还有DC、RST、CS等控制线),只需要一根Grove线,同时解决了供电和通信问题。对于快速原型开发来说,这种“一体化”的设计能让我们把精力更集中在软件和创意实现上,而不是纠缠于硬件连线。接下来,我就结合自己把这个圆形屏用起来的全过程,从硬件准备、环境搭建、基础显示到高级应用,一步步拆解,希望能帮你绕过我踩过的那些坑。

2. 硬件开箱与连接:认识你的“圆屏伙伴”

拿到这块Grove圆形显示屏扩展板,第一印象是做工相当精致。板子主体是黑色的,中间的圆形显示屏被一圈金属边框包裹,很有质感。板载了一个Grove接口(4针,支持I2C和UART,但这里用于SPI),一个复位按钮,以及一个给XIAO系列开发板准备的母座。翻到背面,可以看到主要的芯片:GC9A01显示驱动IC和一颗Flash芯片(用于存储字库或图形资源)。

核心硬件解析:

  1. 显示屏:1.28英寸圆形TFT LCD,驱动芯片为GC9A01。分辨率240x240,262K色,采用SPI接口通信。圆形直径约为32mm,实际显示区域就是完美的正圆。
  2. Grove接口:这是Seeed Studio的生态系统接口。对于这块屏,Grove接口的四根线被定义为:
    • 黄色线 (SIG): 充当SPI的MOSI(主设备输出,从设备输入)数据线。
    • 白色线 (NC): 通常不连接,但在这块板上可能被用作SPI的DC(数据/命令)选择线。
    • 红色线 (VCC): 电源正极(3.3V或5V,具体看主控板)。
    • 黑色线 (GND): 电源地。 需要注意的是,Grove标准接口本身不直接映射SPI的所有引脚,所以Seeed通过板载电路和软件库,在内部完成了引脚的重映射和功能分配,对我们用户而言是透明的。
  3. 兼容的主控板:官方主要适配Seeed Studio XIAO系列,如XIAO RP2040、XIAO ESP32C3/S3、XIAO nRF52840等。因为这些板子的Grove接口引脚定义是统一的,并且有丰富的软件库支持。

连接步骤:我的选择是XIAO RP2040,因为它基于树莓派Pico的同款芯片,性能足够且社区资源丰富。

  1. 将圆形显示屏扩展板背面的母座,与XIAO RP2040的引脚对齐,轻轻按压,使两者牢固结合。它们就像乐高积木一样严丝合缝。
  2. 此时,显示屏扩展板已经通过排针从XIAO RP2040获得了电力和通信能力。你不需要连接任何额外的杜邦线。
  3. 通过USB-C数据线将XIAO RP2040连接到电脑。连接成功后,XIAO RP2040上的电源指示灯会亮起。

注意:确保按压方向正确。XIAO RP2040的USB口和显示屏扩展板的Grove接口在同一侧为正确方向。如果插反了,虽然物理上可能插得进去,但会导致短路风险,务必小心。

硬件连接就是这么简单。相比于传统SPI屏需要连接7-8根线,这种集成度高的模块大大减少了出错的概率,也使得项目外观更加整洁。接下来,我们进入软件环境配置环节。

3. 软件环境搭建与基础库驱动

硬件连接好了,下一步就是让代码跑起来。我选择的开发环境是Arduino IDE,因为它对XIAO系列和Grove生态的支持比较友好。当然,你也可以使用PlatformIO。

3.1 安装开发板支持与库文件

首先,需要在Arduino IDE中添加Seeed Studio的板卡支持。

  1. 打开Arduino IDE,点击文件->首选项
  2. 在“附加开发板管理器网址”中,填入以下URL(如果已有其他,用逗号隔开):https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json
  3. 点击工具->开发板->开发板管理器
  4. 在搜索框中输入“Seeed XIAO”,找到“Seeed XIAO RP2040”(或你对应的型号),点击安装。

接着,安装驱动圆形屏所需的库。Seeed官方为这块屏提供了高度封装的库,让绘图变得非常简单。

  1. 点击项目->加载库->管理库...
  2. 在库管理器中搜索“Round Display for XIAO”或“Seeed Studio Round Display”。
  3. 找到由Seeed Studio发布的库,点击安装。这个库通常名为Seeed_Arduino_RoundDisplay或类似。它会自动处理GC9A01的底层驱动,并提供高级的图形API。

3.2 第一个测试程序:画个同心圆

库安装好后,我们来写一个最简单的测试程序,验证屏幕是否正常工作。这个程序将在屏幕中心画一组彩色的同心圆。

#include "RoundDisplay.h" // 包含圆形显示屏库 RoundDisplay display; // 创建显示屏对象 void setup() { display.begin(); // 初始化显示屏 display.setBackgroundColor(COLOR_BLACK); // 设置背景色为黑色 display.clear(); // 清屏 // 定义一组颜色 uint16_t colors[] = {COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_YELLOW, COLOR_MAGENTA}; int centerX = display.width() / 2; // 屏幕中心X坐标 (120) int centerY = display.height() / 2; // 屏幕中心Y坐标 (120) int maxRadius = min(centerX, centerY); // 最大半径 (120) // 绘制5个同心圆 for (int i = 0; i < 5; i++) { int radius = maxRadius * (i + 1) / 6; // 半径逐步增大 display.setForegroundColor(colors[i]); // 设置当前绘图颜色 display.drawCircle(centerX, centerY, radius); // 画圆 } display.refresh(); // 将绘制内容刷新到屏幕上 } void loop() { // 主循环为空,图形绘制一次后保持显示 }

代码逻辑解读:

  • display.begin(): 这个函数至关重要,它内部完成了对GC9A01芯片的初始化序列配置,包括电源、伽马校正、像素格式等。如果屏幕白屏或花屏,首先检查这里是否执行成功。
  • setBackgroundColorclear: 设置背景色并清屏,相当于准备一块画布。
  • drawCircle: 库提供的高级绘图函数。你只需要关心圆心坐标和半径,底层库会自动计算圆上的每个点并进行绘制,避免了手动进行复杂的Bresenham画圆算法。
  • display.refresh(): 在完成所有绘制命令后,必须调用此函数才能将帧缓冲区的内容真正更新到物理屏幕上。在此之前的所有draw操作都只是在内存中进行的。

将代码上传到XIAO RP2040,如果一切顺利,你应该能看到屏幕上出现五个彩色的同心圆。这个简单的测试验证了硬件连接、库安装和基础绘图功能都是正常的。如果屏幕没有反应,请检查USB连接、开发板型号选择(工具->开发板->Seeed XIAO RP2040)和端口选择(工具->端口)。

4. 核心功能实战:打造一个圆形时钟界面

基础测试通过后,我们来实现一个更实用的功能:一个圆形时钟。这涉及到图形绘制、文本显示和实时时间的获取。

4.1 绘制静态表盘元素

首先,我们绘制时钟的静态背景,包括表盘、刻度和数字。

#include "RoundDisplay.h" #include "TFT_eSPI_Graphics.h" // 该库可能包含在RoundDisplay库中,用于更高级的图形和字体 RoundDisplay display; void drawClockFace() { display.setBackgroundColor(COLOR_BLACK); display.clear(); int centerX = 120; int centerY = 120; int faceRadius = 110; // 表盘半径 // 1. 绘制表盘外圆 display.setForegroundColor(COLOR_WHITE); display.drawCircle(centerX, centerY, faceRadius); // 2. 绘制小时刻度(12个粗刻度) for (int hour = 0; hour < 12; hour++) { float angle = hour * PI / 6.0; // 将小时转换为弧度 (360°/12 = 30° = PI/6) int innerX = centerX + (int)((faceRadius - 15) * sin(angle)); int innerY = centerY - (int)((faceRadius - 15) * cos(angle)); // 注意屏幕Y轴向下,所以用减号 int outerX = centerX + (int)(faceRadius * sin(angle)); int outerY = centerY - (int)(faceRadius * cos(angle)); display.drawLine(innerX, innerY, outerX, outerY); } // 3. 绘制分钟刻度(60个细刻度) display.setForegroundColor(COLOR_LIGHT_GRAY); for (int minute = 0; minute < 60; minute++) { if (minute % 5 != 0) { // 跳过已经是小时刻度的位置 float angle = minute * PI / 30.0; // (360°/60 = 6° = PI/30) int innerX = centerX + (int)((faceRadius - 8) * sin(angle)); int innerY = centerY - (int)((faceRadius - 8) * cos(angle)); int outerX = centerX + (int)(faceRadius * sin(angle)); int outerY = centerY - (int)(faceRadius * cos(angle)); display.drawLine(innerX, innerY, outerX, outerY); } } // 4. 绘制小时数字 (1-12) display.setForegroundColor(COLOR_WHITE); display.setTextSize(2); // 设置文本大小 display.setTextAlignment(TEXT_ALIGN_CENTER); // 文本居中对齐 for (int hour = 1; hour <= 12; hour++) { float angle = hour * PI / 6.0; int textX = centerX + (int)((faceRadius - 30) * sin(angle)); int textY = centerY - (int)((faceRadius - 30) * cos(angle)) - 8; // 微调Y坐标使数字居中 display.drawNumber(textX, textY, hour); } }

这部分代码在setup()中调用一次即可。它利用了三角函数sincos来计算圆上每个刻度的坐标,这是圆形界面编程的通用技巧。注意屏幕坐标系的原点(0,0)在左上角,Y轴向下为正,所以在计算Y坐标时用了减法来适应数学上的标准坐标系。

4.2 动态指针与时间获取

静态表盘画好了,现在需要让指针动起来。我们需要获取实时时间,并计算时针、分针、秒针的角度。

对于XIAO RP2040这类没有实时时钟(RTC)模块的板子,通常有两种方式获取时间:

  1. 网络授时(NTP):如果板子有网络功能(如XIAO ESP32C3),可以通过Wi-Fi连接互联网,使用NTP协议获取精确时间。
  2. 编译时间 + 软件计时:在代码编译时记录一个起始时间戳,然后依靠MCU内部的毫秒级计时器进行累加。这种方法简单,但存在累积误差,且断电后时间丢失。

为了演示的通用性,这里采用第二种方法。我们将使用Arduino的millis()函数来模拟时间的流逝。

#include "RoundDisplay.h" RoundDisplay display; unsigned long lastUpdateTime = 0; int simulatedSecond = 0; int simulatedMinute = 0; int simulatedHour = 0; void updateClockHands() { int centerX = 120; int centerY = 120; // 先清除上一帧的指针(用背景色重画,或采用局部刷新策略) // 简单起见,这里每次重画整个表盘。实际优化可以考虑局部刷新。 drawClockFace(); // 计算指针角度(将时间转换为弧度) float secondAngle = simulatedSecond * PI / 30.0 - PI / 2.0; // -PI/2让0秒指向12点方向 float minuteAngle = simulatedMinute * PI / 30.0 - PI / 2.0; float hourAngle = (simulatedHour % 12 + simulatedMinute / 60.0) * PI / 6.0 - PI / 2.0; // 绘制秒针(红色,细长) display.setForegroundColor(COLOR_RED); int secondHandX = centerX + (int)(90 * cos(secondAngle)); int secondHandY = centerY + (int)(90 * sin(secondAngle)); display.drawLine(centerX, centerY, secondHandX, secondHandY); // 绘制分针(白色,中等) display.setForegroundColor(COLOR_WHITE); int minuteHandX = centerX + (int)(70 * cos(minuteAngle)); int minuteHandY = centerY + (int)(70 * sin(minuteAngle)); display.drawLine(centerX, centerY, minuteHandX, minuteHandY); // 绘制时针(白色,粗短) display.setForegroundColor(COLOR_WHITE); int hourHandX = centerX + (int)(50 * cos(hourAngle)); int hourHandY = centerY + (int)(50 * sin(hourAngle)); display.drawLine(centerX, centerY, hourHandX, hourHandY); // 绘制指针中心的圆点 display.fillCircle(centerX, centerY, 4, COLOR_WHITE); display.refresh(); // 刷新屏幕 } void setup() { display.begin(); // 初始化一个模拟时间,例如 10:08:30 simulatedHour = 10; simulatedMinute = 8; simulatedSecond = 30; drawClockFace(); updateClockHands(); lastUpdateTime = millis(); } void loop() { unsigned long currentMillis = millis(); if (currentMillis - lastUpdateTime >= 1000) { // 每1000毫秒(1秒)更新一次 lastUpdateTime = currentMillis; simulatedSecond++; if (simulatedSecond >= 60) { simulatedSecond = 0; simulatedMinute++; if (simulatedMinute >= 60) { simulatedMinute = 0; simulatedHour = (simulatedHour + 1) % 24; } } updateClockHands(); } }

关键点与优化思考:

  • 角度计算:代码中- PI / 2.0是一个关键调整。因为数学上0弧度对应的是3点钟方向,而钟表的12点钟方向对应的是负90度(即 -π/2 弧度)。这个偏移量是必须的。
  • 时针的精确性:时针的角度不是整点乘以30度,而是(小时 + 分钟/60.0) * 30度,这样时针才会随着分钟数缓慢移动,更符合真实时钟。
  • 性能问题updateClockHands()函数中每次调用drawClockFace()重绘整个背景,在240x240的分辨率下,通过SPI传输的数据量不小,可能会导致屏幕刷新有肉眼可见的延迟或闪烁。这在秒针跳动时尤其明显。
  • 闪烁问题:直接全屏刷新(先清背景再画所有元素)会导致屏幕在每一帧都有短暂的全黑或全白过程,产生闪烁。

5. 性能优化与高级技巧:解决闪烁与提升帧率

上一节的时钟程序功能完整,但体验不佳,主要是闪烁和卡顿。这对于动态显示来说是致命的。下面介绍几种优化策略。

5.1 双缓冲与局部刷新

GC9A01这类驱动芯片通常支持设置一个“窗口”,只更新这个矩形区域内的像素,而不是整个屏幕。这被称为局部刷新(Partial Update)。同时,在内存中维护一个“帧缓冲区”(Frame Buffer),所有绘图操作先在缓冲区完成,然后一次性将变化的区域发送到屏幕,这被称为双缓冲(Double Buffering)。

幸运的是,Seeed_Arduino_RoundDisplay库的底层可能已经使用了帧缓冲区。但我们需要优化绘图逻辑,避免不必要的全屏重绘。

优化策略:只重绘变化的指针

  1. 记录上一帧指针位置:在全局变量中保存上一秒时针、分针、秒针的终点坐标。
  2. 擦除旧指针:在绘制新指针前,用背景色(黑色)在旧指针的位置上重新画线,相当于“擦除”。
  3. 绘制新指针:在新位置绘制当前时间的指针。
  4. 更新记录:将新指针位置保存为“旧位置”。

这样,我们只需要更新屏幕上很少的像素(几条线),而不是整个240x240的区域,性能提升巨大,且完全消除了闪烁。

// 全局变量记录旧指针位置 int oldSecondHandX, oldSecondHandY; int oldMinuteHandX, oldMinuteHandY; int oldHourHandX, oldHourHandY; void updateClockHandsOptimized() { int centerX = 120; int centerY = 120; // 1. 用背景色擦除旧指针 display.setForegroundColor(COLOR_BLACK); display.drawLine(centerX, centerY, oldSecondHandX, oldSecondHandY); display.drawLine(centerX, centerY, oldMinuteHandX, oldMinuteHandY); display.drawLine(centerX, centerY, oldHourHandX, oldHourHandY); // 注意:也需要擦除旧的中心圆点,或者选择不擦除,因为新点会覆盖 // 2. 计算新指针位置(角度计算同上文) float secondAngle = simulatedSecond * PI / 30.0 - PI / 2.0; float minuteAngle = simulatedMinute * PI / 30.0 - PI / 2.0; float hourAngle = (simulatedHour % 12 + simulatedMinute / 60.0) * PI / 6.0 - PI / 2.0; int newSecondHandX = centerX + (int)(90 * cos(secondAngle)); int newSecondHandY = centerY + (int)(90 * sin(secondAngle)); int newMinuteHandX = centerX + (int)(70 * cos(minuteAngle)); int newMinuteHandY = centerY + (int)(70 * sin(minuteAngle)); int newHourHandX = centerX + (int)(50 * cos(hourAngle)); int newHourHandY = centerY + (int)(50 * sin(hourAngle)); // 3. 绘制新指针 display.setForegroundColor(COLOR_RED); display.drawLine(centerX, centerY, newSecondHandX, newSecondHandY); display.setForegroundColor(COLOR_WHITE); display.drawLine(centerX, centerY, newMinuteHandX, newMinuteHandY); display.drawLine(centerX, centerY, newHourHandX, newHourHandY); display.fillCircle(centerX, centerY, 4, COLOR_WHITE); // 4. 更新旧指针位置记录 oldSecondHandX = newSecondHandX; oldSecondHandY = newSecondHandY; oldMinuteHandX = newMinuteHandX; oldMinuteHandY = newMinuteHandY; oldHourHandX = newHourHandX; oldHourHandY = newHourHandY; // 5. 刷新屏幕(库可能支持局部刷新,这里调用全局刷新) display.refresh(); }

setup()中,你需要初始化旧指针的位置(例如指向12点方向),并在第一次绘制表盘后调用一次updateClockHandsOptimized来设置初始的旧位置。

5.2 利用硬件SPI与DMA

对于RP2040或ESP32这类性能较强的MCU,进一步优化可以从底层通信入手。

  • 硬件SPI:确保库使用的是硬件SPI接口,而不是软件模拟的(Software SPI)。硬件SPI由专用外设处理,速度远快于软件模拟。通常库的begin()函数内部会配置好。
  • DMA(直接存储器访问):这是终极武器。DMA允许数据在存储器和外设(如SPI)之间直接传输,而无需CPU介入。在传输大量屏幕数据时,CPU可以解放出来处理其他任务(如网络、传感器读数)。XIAO RP2040的SPI接口支持DMA。你需要查阅底层驱动库(如Adafruit_GC9A01TFT_eSPI)的文档,看是否支持以及如何启用DMA。启用后,display.refresh()的调用将几乎不阻塞CPU。

5.3 图形素材的存储与显示

如果你想显示复杂的图标、图片或中文字体,需要处理图形资源。240x240的RGB565全屏位图需要约115KB内存(2402402 bytes),这很可能超过MCU的RAM。通常做法是:

  1. 压缩存储:将图片转换为索引色格式(如16色、256色)或使用RLE等压缩算法,存储在Flash或外置SD卡中。
  2. 流式解码与显示:在显示时,动态地从存储介质中读取并解码一小部分数据,通过SPI发送到屏幕。TFT_eSPI库对从SD卡显示BMP、PNG图片有很好的支持。
  3. 使用LVGL等GUI库:对于复杂的用户界面,强烈推荐使用LVGL、U8g2等嵌入式图形库。它们提供了按钮、标签、图表等控件,并且自带高效的渲染引擎和内存管理。Seeed_Arduino_RoundDisplay库可能已经集成了对LVGL的适配。使用LVGL后,绘制时钟可能只需要几行代码来创建“弧”对象和“标签”对象,并设置其属性,库会处理所有的优化和刷新。

6. 项目扩展与创意应用场景

圆形显示屏的魅力在于其独特的形式感,非常适合打造具有设计感的嵌入式项目。以下是一些可以尝试的扩展方向:

6.1 环境信息监视器结合XIAO ESP32C3的Wi-Fi功能和各类Grove传感器,可以制作一个桌面环境信息站。

  • 硬件:XIAO ESP32C3 + 圆形显示屏 + Grove温湿度传感器(如DHT20) + Grove大气压传感器(如BMP280)。
  • 软件
    1. 连接Wi-Fi,通过NTP获取精确时间。
    2. 定时读取传感器数据。
    3. 在圆形屏幕上设计UI:中心显示实时时间,周围用环形进度条或仪表盘显示温度、湿度、气压数值,顶部区域可以显示天气图标(从网络API获取)。
  • 效果:一个极具科技感的桌面摆件,信息呈现方式比方形屏幕更聚焦。

6.2 智能家居控制旋钮利用圆形屏幕和旋转编码器,制作一个实体控制旋钮。

  • 硬件:XIAO RP2040 + 圆形显示屏 + Grove旋转编码器。
  • 软件
    1. 屏幕显示一个环形菜单,例如“灯光亮度”、“空调温度”、“音箱音量”。
    2. 旋转编码器用于切换菜单项和调整数值。
    3. 按下编码器确认选择。
    4. XIAO通过Wi-Fi(如果使用ESP32型号)或蓝牙,将控制指令发送给智能家居中枢(如Home Assistant)。
  • 优势:提供了触觉反馈和视觉反馈结合的良好交互体验,比手机App控制更有仪式感和便捷性。

6.3 简易示波器或音频频谱显示器对于RP2040,其ADC和计算能力可以胜任一些简单的信号可视化。

  • 硬件:XIAO RP2040 + 圆形显示屏 + 音频输入模块(或直接使用ADC引脚采集信号)。
  • 软件
    1. 使用RP2040的ADC以较高频率(如10kHz)采样模拟信号。
    2. 对采样数据进行FFT(快速傅里叶变换)计算,得到各频率分量的强度。
    3. 在圆形屏幕上,将0-180度的范围映射为频率范围,用从圆心向外发散的条形长度来表示该频率的强度,形成一个半圆或全圆的频谱图。
  • 挑战:需要较高的计算性能和优化好的FFT算法。RP2040的双核和PIO(可编程IO)特性在这里能派上用场。

6.4 复古游戏机界面圆形屏幕非常适合制作一些复古风格的像素游戏,比如吃豆人(Pac-Man)的迷宫本身就是圆形的概念延伸,或者一个简单的打砖块游戏,挡板和球在圆形边界内反弹会带来新的玩法。

在实现这些创意项目时,你可能会遇到内存不足、刷新率不够、图形复杂度过高等问题。我的经验是:优先考虑使用LVGL这样的专业GUI库。它虽然有一定学习成本,但提供了文本框、按钮、图表、动画等现成控件,以及基于事件驱动的编程模型,能极大地提高开发效率和最终效果的流畅度。Seeed的Wiki和示例代码通常会有如何在该圆形屏上使用LVGL的指南,这是深入玩转这块屏的必经之路。

从点亮第一个同心圆,到实现一个流畅的时钟,再到构思更复杂的项目,这块小小的圆形显示屏扩展板打开了一扇通往创意嵌入式开发的新大门。它的价值不仅在于“能显示”,更在于其独特的形态激发了不同于方形屏幕的交互和视觉设计思路。在实际操作中,耐心调试绘图坐标、优化刷新逻辑是必不可少的环节,而成功点亮并流畅运行自己设计的界面时,那种成就感正是硬件开发的乐趣所在。

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

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

立即咨询