基于GC9A01圆形显示屏与XIAO开发板的嵌入式GUI开发实战 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.28英寸圆形TFT LCD驱动芯片为GC9A01。分辨率240x240262K色采用SPI接口通信。圆形直径约为32mm实际显示区域就是完美的正圆。Grove接口这是Seeed Studio的生态系统接口。对于这块屏Grove接口的四根线被定义为黄色线 (SIG) 充当SPI的MOSI主设备输出从设备输入数据线。白色线 (NC) 通常不连接但在这块板上可能被用作SPI的DC数据/命令选择线。红色线 (VCC) 电源正极3.3V或5V具体看主控板。黑色线 (GND) 电源地。 需要注意的是Grove标准接口本身不直接映射SPI的所有引脚所以Seeed通过板载电路和软件库在内部完成了引脚的重映射和功能分配对我们用户而言是透明的。兼容的主控板官方主要适配Seeed Studio XIAO系列如XIAO RP2040、XIAO ESP32C3/S3、XIAO nRF52840等。因为这些板子的Grove接口引脚定义是统一的并且有丰富的软件库支持。连接步骤我的选择是XIAO RP2040因为它基于树莓派Pico的同款芯片性能足够且社区资源丰富。将圆形显示屏扩展板背面的母座与XIAO RP2040的引脚对齐轻轻按压使两者牢固结合。它们就像乐高积木一样严丝合缝。此时显示屏扩展板已经通过排针从XIAO RP2040获得了电力和通信能力。你不需要连接任何额外的杜邦线。通过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的板卡支持。打开Arduino IDE点击文件-首选项。在“附加开发板管理器网址”中填入以下URL如果已有其他用逗号隔开https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json点击工具-开发板-开发板管理器。在搜索框中输入“Seeed XIAO”找到“Seeed XIAO RP2040”或你对应的型号点击安装。接着安装驱动圆形屏所需的库。Seeed官方为这块屏提供了高度封装的库让绘图变得非常简单。点击项目-加载库-管理库...。在库管理器中搜索“Round Display for XIAO”或“Seeed Studio Round Display”。找到由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芯片的初始化序列配置包括电源、伽马校正、像素格式等。如果屏幕白屏或花屏首先检查这里是否执行成功。setBackgroundColor和clear: 设置背景色并清屏相当于准备一块画布。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()中调用一次即可。它利用了三角函数sin和cos来计算圆上每个刻度的坐标这是圆形界面编程的通用技巧。注意屏幕坐标系的原点(0,0)在左上角Y轴向下为正所以在计算Y坐标时用了减法来适应数学上的标准坐标系。4.2 动态指针与时间获取静态表盘画好了现在需要让指针动起来。我们需要获取实时时间并计算时针、分针、秒针的角度。对于XIAO RP2040这类没有实时时钟RTC模块的板子通常有两种方式获取时间网络授时NTP如果板子有网络功能如XIAO ESP32C3可以通过Wi-Fi连接互联网使用NTP协议获取精确时间。编译时间 软件计时在代码编译时记录一个起始时间戳然后依靠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库的底层可能已经使用了帧缓冲区。但我们需要优化绘图逻辑避免不必要的全屏重绘。优化策略只重绘变化的指针记录上一帧指针位置在全局变量中保存上一秒时针、分针、秒针的终点坐标。擦除旧指针在绘制新指针前用背景色黑色在旧指针的位置上重新画线相当于“擦除”。绘制新指针在新位置绘制当前时间的指针。更新记录将新指针位置保存为“旧位置”。这样我们只需要更新屏幕上很少的像素几条线而不是整个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_GC9A01或TFT_eSPI的文档看是否支持以及如何启用DMA。启用后display.refresh()的调用将几乎不阻塞CPU。5.3 图形素材的存储与显示如果你想显示复杂的图标、图片或中文字体需要处理图形资源。240x240的RGB565全屏位图需要约115KB内存2402402 bytes这很可能超过MCU的RAM。通常做法是压缩存储将图片转换为索引色格式如16色、256色或使用RLE等压缩算法存储在Flash或外置SD卡中。流式解码与显示在显示时动态地从存储介质中读取并解码一小部分数据通过SPI发送到屏幕。TFT_eSPI库对从SD卡显示BMP、PNG图片有很好的支持。使用LVGL等GUI库对于复杂的用户界面强烈推荐使用LVGL、U8g2等嵌入式图形库。它们提供了按钮、标签、图表等控件并且自带高效的渲染引擎和内存管理。Seeed_Arduino_RoundDisplay库可能已经集成了对LVGL的适配。使用LVGL后绘制时钟可能只需要几行代码来创建“弧”对象和“标签”对象并设置其属性库会处理所有的优化和刷新。6. 项目扩展与创意应用场景圆形显示屏的魅力在于其独特的形式感非常适合打造具有设计感的嵌入式项目。以下是一些可以尝试的扩展方向6.1 环境信息监视器结合XIAO ESP32C3的Wi-Fi功能和各类Grove传感器可以制作一个桌面环境信息站。硬件XIAO ESP32C3 圆形显示屏 Grove温湿度传感器如DHT20 Grove大气压传感器如BMP280。软件连接Wi-Fi通过NTP获取精确时间。定时读取传感器数据。在圆形屏幕上设计UI中心显示实时时间周围用环形进度条或仪表盘显示温度、湿度、气压数值顶部区域可以显示天气图标从网络API获取。效果一个极具科技感的桌面摆件信息呈现方式比方形屏幕更聚焦。6.2 智能家居控制旋钮利用圆形屏幕和旋转编码器制作一个实体控制旋钮。硬件XIAO RP2040 圆形显示屏 Grove旋转编码器。软件屏幕显示一个环形菜单例如“灯光亮度”、“空调温度”、“音箱音量”。旋转编码器用于切换菜单项和调整数值。按下编码器确认选择。XIAO通过Wi-Fi如果使用ESP32型号或蓝牙将控制指令发送给智能家居中枢如Home Assistant。优势提供了触觉反馈和视觉反馈结合的良好交互体验比手机App控制更有仪式感和便捷性。6.3 简易示波器或音频频谱显示器对于RP2040其ADC和计算能力可以胜任一些简单的信号可视化。硬件XIAO RP2040 圆形显示屏 音频输入模块或直接使用ADC引脚采集信号。软件使用RP2040的ADC以较高频率如10kHz采样模拟信号。对采样数据进行FFT快速傅里叶变换计算得到各频率分量的强度。在圆形屏幕上将0-180度的范围映射为频率范围用从圆心向外发散的条形长度来表示该频率的强度形成一个半圆或全圆的频谱图。挑战需要较高的计算性能和优化好的FFT算法。RP2040的双核和PIO可编程IO特性在这里能派上用场。6.4 复古游戏机界面圆形屏幕非常适合制作一些复古风格的像素游戏比如吃豆人Pac-Man的迷宫本身就是圆形的概念延伸或者一个简单的打砖块游戏挡板和球在圆形边界内反弹会带来新的玩法。在实现这些创意项目时你可能会遇到内存不足、刷新率不够、图形复杂度过高等问题。我的经验是优先考虑使用LVGL这样的专业GUI库。它虽然有一定学习成本但提供了文本框、按钮、图表、动画等现成控件以及基于事件驱动的编程模型能极大地提高开发效率和最终效果的流畅度。Seeed的Wiki和示例代码通常会有如何在该圆形屏上使用LVGL的指南这是深入玩转这块屏的必经之路。从点亮第一个同心圆到实现一个流畅的时钟再到构思更复杂的项目这块小小的圆形显示屏扩展板打开了一扇通往创意嵌入式开发的新大门。它的价值不仅在于“能显示”更在于其独特的形态激发了不同于方形屏幕的交互和视觉设计思路。在实际操作中耐心调试绘图坐标、优化刷新逻辑是必不可少的环节而成功点亮并流畅运行自己设计的界面时那种成就感正是硬件开发的乐趣所在。