☰
MCU驱动SPI屏幕实战:桌面状态卡片硬件落地全记录
2026/10/18 1:01:10 网站建设 项目流程

Status Deck 这个桌面状态卡片项目折腾到第四篇,我今年给自己定的规矩是软件能写、硬件也得上手。前几篇聊了整体架构、后端数据聚合和前端布局设计,但一直没碰实物。这周硬件终于到了,这篇就是一次完整的硬件落地实录:从拆快递开始,到屏幕点亮,再到跑通第一个真正显示状态数据的 Demo。如果你也在做类似的桌面小屏信息面板,或者刚接触 MCU 驱动屏幕,这篇可以当一份避坑指南看。

1. 硬件方案回顾与物料清单

1.1 为什么最终选了这套搭配

Status Deck 要显示的东西其实不复杂:时间、天气、CPU 占用率、网络上下行速度,偶尔切几个页面看看订阅信息。这些内容基本都是文字和简单的柱状图,所以屏幕分辨率不需要太高,但响应速度要跟手,不能像手机那样动不动就刷新闪烁。系统本身是一个长期放在桌面上待机的小面板,需要安静、发热低、功耗不能太高。

基于这些需求,我最终放弃了“HDMI 小屏 + Linux 小主机”的组合。那个方案性能是强,但体积大、开机慢、还要维护图形桌面环境,对一块桌面状态牌来说,多少有点杀鸡用牛刀。最后改成了“单片机 + SPI 屏”的方案。MCU 直接操作屏幕像素,没有桌面系统,功耗只有一两瓦,开机基本是秒亮,稳定性也更容易控制。

屏幕最终选了一块 2.8 寸的 IPS 屏,分辨率 320x240,四线 SPI 接口。驱动 IC 是市面上非常常见的一种,指令集兼容通用 MIPI-DBI 类型,初始化序列在售卖页面上能找到。为什么选 SPI 不选 I2C?I2C 速度太慢,刷一帧画面时能明显看到扫描线,体验很差。SPI 用线少、带宽够,320x240 的 16 位色数据量对 SPI 来说完全不在话下。开发板则选了一块带 WiFi 的双核 MCU 板子,主频 240MHz,板载 USB 转串口,方便烧录和看日志。这套组合总成本不高,坏了也不心疼,非常符合自造玩具的定位。

1.2 到货清单与选型心得

这次到货的东西不多,列个表记一下,也算给后面想复刻的人一个参考:

物料数量作用说明
双核 MCU 开发板1 块主控,带 WiFi,负责数据和显示控制
2.8 寸 SPI IPS 屏模块1 块320x240 分辨率,带背光、排线
杜邦线/排线若干前期原型验证用,后期会换软排线
面包板1 块方便快速接线,不用焊
5V/2A USB 电源1 个给开发板供电
3D 打印外壳(半成品)1 套还没打印,下一步的事

选型时有几个心得值得说。第一,不要只看屏幕分辨率,还要看是否带触摸、排线方向、模块是否自带电平转换。触摸屏对 Status Deck 这种固定显示面板没什么用,我就选了不带触摸的版本,便宜不少。第二,屏幕模块的排线很娇气,第一次买回来就发现排线压弯了,建议同时买一根备用排线,花不了几个钱,关键时候能救命。第三,MCU 板子上的引脚数量和引出方式也很重要,优先选把所有 GPIO 都引出的款,否则后面接屏幕时可能被某个引脚复用问题卡住。

2. 拆箱检查与首次上电

2.1 拆箱后别急着插电

快递到手,我第一件事不是插 USB,而是先拆出来做静态检查。先用肉眼过一遍屏幕排线有没有折痕、主控板引脚有没有歪针。然后拿万用表量了一下屏幕模块电源正极和地之间的阻值,确认没有短路。这一下真的能避开不少问题,之前见过有人拿到板子直接上电,结果焊点连锡,一冒烟就废了。

屏幕这类模块要特别注意逻辑电压。我的主控板输出是 3.3V 逻辑电平,屏幕模块也是 3.3V 供电逻辑,两者直接接就行。如果主控板是 5V IO 并且模块不支持 5V 输入,那就必须加电平转换,否则芯片会过热甚至击穿。用万用表量一下主控板的 VCC 引脚电压,是最靠谱的做法。

上电之前还要检查 USB 供电线。劣质 USB 线线阻大,插上去电压会掉到 4.6V 甚至更低,开发板就会反复重启,显示也容易花屏。我换了一根标称 5V/2A 的电源线,后面就再没出现过供电导致的随机重启。

2.2 首次上电的三种现场现象

插上 USB 线后,我遇到了三种典型现象,写出来给各位一个预期:

  • 电源灯亮,但串口没有任何输出:大概率是 USB 转串口驱动没装,或者 TX/RX 接反。这个板子之前我在别的项目用过,串口正常。
  • 屏幕背光亮,但屏幕没有内容:说明背光供电没问题,但控制器的命令和数据没通,后面初始化解决。
  • 完全没反应:先查供电,再看 USB 线,最后看板子有没有焊错。

我这次的情况是第二种:屏幕上白光一闪,然后就一直亮着,像一个小夜灯。这其实是个好消息,说明主控板、屏幕背光、电源都没问题,问题出在数据通道。白屏不可怕,怕的是那种连背光都不亮的暗屏,那往往意味着硬件接线错了。

3. 点亮屏幕:接线、初始化和背光

3.1 八根线的连接方式

这块屏幕模块引出 8 个脚,除了电源和地,剩下 6 根都是控制信号。我用的接线方式如下,引脚编号以我手里这块开发板的 GPIO 标号为准,接口名是通用的:

屏幕引脚开发板引脚说明
VCC3V3逻辑电源
GNDGND共地
SCLKGP5SPI 时钟
MOSI/SDAGP6SPI 数据输出
CSGP7片选,低电平有效
DCGP8数据/命令选择
RSTGP9复位,低电平有效
BLGP4背光控制,接 PWM

这里有几个容易踩坑的地方。MOSI 必须接到屏幕的 MOSI/SDA 引脚,别接到 MISO,否则数据发不出去,屏幕一直白屏。DC 信号在 SPI 协议里没有对应概念,它是在每次传输之前决定当前字节是命令还是数据。如果 DC 接错或者不接,屏幕会把命令字节当成像素数据,画面就会变成一堆乱码。

RST 我特意接到 GPIO 而不是直接接一个电阻电容做上电复位。原因很简单:软件可以随时硬复位屏幕,让它在初始化前处于干净状态。有些模块的 RST 引脚是单独的,不接会导致初始化时序不稳定,偶尔白屏。如果你用的是那种 RST 已经和芯片内部上拉电阻短接的模块,可以不接,但为了统一,我还是接了 GP9。

BL 是背光控制脚。一开始我图省事,把 BL 直接接 3V3,结果是背光常亮、亮度不可调,晚上放在桌面上像个小探照灯。后来改成 GP4 并配置成 PWM,才解决了亮度问题。如果你完全不在乎调光,接 3V3 也行,但后续想用传感器自动调光就麻烦了。

3.2 驱动 IC 初始化序列(含 SPI 速率选择)

接线没问题后,接下来就是初始化屏幕。SPI 屏幕不像串口屏那样上电自动显示字母,它内部是一片需要配置的显示控制器,必须先发一串命令让它进入正常显示模式。如果你写过 MCU 驱动 OLED 屏,这套流程会很眼熟。

先定义两个最底层函数:发命令和发数据。区别就在 DC 引脚的电平:

static void lcd_send_cmd(uint8_t cmd) { gpio_write(DC_PIN, 0); // 低电平表示命令 spi_write_bytes(&cmd, 1); } static void lcd_send_data(uint8_t *data, size_t len) { gpio_write(DC_PIN, 1); // 高电平表示数据 spi_write_bytes(data, len); }

然后是复位和初始化:

static void lcd_reset(void) { gpio_write(RST_PIN, 0); delay_ms(20); gpio_write(RST_PIN, 1); delay_ms(120); // 给控制器留出启动时间 } static void lcd_init(void) { lcd_reset(); lcd_send_cmd(0x01); // Soft Reset delay_ms(120); lcd_send_cmd(0x11); // Sleep Out delay_ms(120); lcd_send_cmd(0x3A); // 像素格式 uint8_t p1 = 0x55; // 16 位色 lcd_send_data(&p1, 1); lcd_send_cmd(0x36); // 扫描方向 uint8_t p2 = 0x00; lcd_send_data(&p2, 1); lcd_send_cmd(0x29); // Display On }

这套序列是最基础的,能让屏幕亮起来并显示内容。如果你拿到的是其他驱动 IC 的屏幕,序列可能略有不同,但结构都一样:复位、Sleep Out、设置像素格式、设置扫描方向、Display On。真正花时间的是后面调对齐和颜色,那要从 0x36 寄存器的方向位去试。

SPI 速率也要注意。主控板的 SPI 外设可能跑到 40MHz,但屏幕模块的排线、杜邦线以及芯片实际能力不一定跟得上。我把速率从 40MHz 往下调,最后稳定在 20MHz。测试方法很简单:如果刷全屏会出现随机杂点、横向偏移或颜色错位,就先降速。我见过有人用 8MHz 才正常,这种时候别觉得是主控不行,先检查线材和接触。

3.3 背光控制和 PWM 频率选择

背光控制听起来简单,其实也有参数要选。直接用 GPIO 拉高拉低只能控制开关,不能调亮度。要调节亮度,必须用 PWM 输出。我把 BL 引脚接到支持 PWM 的 GP4,然后配置成频率 5kHz、占空比可调。

PWM 频率选多少合适?太低了,比如 100Hz,虽然频率高于人眼闪烁阈值,但在屏幕切换时会产生可见的扫描条纹,尤其在拍照或斜视时更明显。太高了,比如 50kHz,又会给电源部分引入噪声,可能干扰 SPI 数据线。我试了一圈,在 1kHz 到 10kHz 之间选,5kHz 是一个比较稳的点。晚上调到 30% 占空比,既省电又不刺眼;白天用 80%,内容仍然可读。

调背光时还有一个容易忽略的点:很多 MCU 的 PWM 引脚和 SPI 引脚是复用的,如果你把 BL 接到了 SPI 时钟或者片选脚上,那初始化 PWM 的同时很可能把 SPI 配置改了,结果屏幕开始花屏。所以接线前一定要查清楚引脚复用表,尽量选一个独立的 GPIO 来做 PWM。

4. 跑通第一个状态 Demo

4.1 Demo 目标与信息流设计

屏幕亮起来只是第一步,真正让 Status Deck 有意义的,是显示真实状态数据。第一个 Demo 我没打算做花活,只显示四块内容:一个大标题 “Status Deck v0.1”、当前时间、CPU 占用率、内存使用量。刷新频率 1 秒一次。这些数据从哪来?由局域网里的一台电脑或者服务器提供一个 JSON 接口,MCU 通过 WiFi 去拉。

这个设计的好处是前后端解耦。MCU 不用自己去抓系统负载,那太复杂了,它只需要维护一个 HTTP 客户端,反复读取同一个 JSON 就行。数据源那边,无论 Python、Node 还是 Shell 脚本都能提供这个接口,后续想增加宿主机温度、磁盘占用率、网络速度,都只需要改服务端,MCU 侧只加解析字段。

JSON 结构定义得越短越好:

{ "time": "2025-03-20 14:22:33", "cpu": 23, "mem": 512 }

返回体小,MCU 的解析压力就小,缓冲区也只需要开 1KB 左右。这个信息流设计看起来简单,却是整个项目最核心的骨架:显示终端只做展示,数据统一从服务端来。

4.2 WiFi 拉取 JSON 数据

MCU 连 WiFi 的过程就不详细贴代码了,不同开发板的 API 差异比较大。关键是在建立 WiFi 连接后,不要立刻发 HTTP 请求。我试过,开机能显示 IP,但直接 GET 经常超时,原因可能是 DHCP 还没完成、路由器没有立刻建立路由表。解决办法是在连接成功后延迟 2 到 3 秒再开始第一轮请求,并且设置一个超时时间。

HTTP GET 部分,我用的是底层 IP 协议栈配合一个轻量 HTTP 客户端接口。核心请求逻辑很简单:

char url[128]; snprintf(url, sizeof(url), "http://192.168.1.10:8080/status.json"); char response_buf[1024]; int len = http_get(url, response_buf, sizeof(response_buf)); if (len > 0) { response_buf[len] = '\0'; char time_str[32]; int cpu = 0; int mem = 0; json_get_string(response_buf, "time", time_str, sizeof(time_str)); json_get_int(response_buf, "cpu", &cpu); json_get_int(response_buf, "mem", &mem); display_status(time_str, cpu, mem); }

我特意保留了最简单的 JSON 解析方式:只查短 key,不用嵌套结构。实际项目里我写了一个 60 行的极简解析函数,逐段扫描 response_buf,找到匹配 key 后把 value 拷贝出来。真没必要上完整的 JSON 库,那会让固件体积多出几十 KB,还可能占用内存导致崩溃。

为什么不用 HTTPS?因为这是局域网内部部署,数据不经过公网,HTTP 明文传输已经够用。等到 Status Deck 以后要访问外部服务或者远程部署时,再考虑加一层加密代理,主控端保持简单。

4.3 文本绘制与局部刷新

有了数据以后,就是把它画到屏幕上。屏幕驱动肯定是自己封装一层,不直接写在主循环里。我内置了一个 8x16 的 ASCII 字符字体,用来绘制文本。绘制流程就是先定位坐标,然后把背景色填充到指定矩形,最后逐字模像素写进显存。

刷新策略我选择了“局部刷新”而不是“全屏刷新”。全屏刷新 320x240x2 字节,大概 153KB 数据,按 20MHz SPI 算也需要 60 多毫秒,再加上 1 秒一次的周期,会导致明显的闪烁和 CPU 占用。而实际变化区域只有那一小条文字,所以只重绘变化行就够了。

第一次写 Demo 时我犯了个错误:每次都写一整块矩形再写字,结果信息闪烁。后来调整成下面这种模式:先只更新数值区域的背景,再写新字符串,最后才调一次屏幕刷新函数。刷新函数只把显存中的脏矩形区域发送到屏幕,而不是所有像素。这样重绘一行的耗时从几十毫秒降到几毫秒,肉眼看起来非常丝滑。

核心绘制函数长这个样子,思路可以直接复用:

void display_status(const char *time_str, int cpu, int mem) { char buf[32]; // 清掉时间区域 lcd_fill_rect(20, 20, 280, 16, BG_COLOR); lcd_draw_string(20, 20, time_str, FG_COLOR, BG_COLOR); snprintf(buf, sizeof(buf), "CPU: %d%%", cpu); lcd_fill_rect(20, 60, 280, 16, BG_COLOR); lcd_draw_string(20, 60, buf, FG_COLOR, BG_COLOR); snprintf(buf, sizeof(buf), "MEM: %dMB", mem); lcd_fill_rect(20, 96, 280, 16, BG_COLOR); lcd_draw_string(20, 96, buf, FG_COLOR, BG_COLOR); lcd_update_dirty(); }

第一次跑通时,屏幕上时间一秒一秒跳,CPU 数值跟着本机负载上下波动,那一刻的成就感确实不小。虽然只是个极其简陋的原型,但已经能看出 Status Deck 最终形态的雏形:一块低功耗、低干扰、常驻桌面的信息面板。

5. 常见问题与排查技巧实录

5.1 白屏、花屏这类显示问题怎么查

白屏或花屏是 SPI 屏最常见的问题,问题可能出在接线、初始化序列、SPI 速率三个环节。建议先按下面这个顺序排查,能省很多时间:

  • 白屏 + 背光亮:先检查 DC 接线,再检查 RST 时序。如果 RST 没接对,控制器一直没有退出复位状态。
  • 花屏 + 内容有规律性错位:大概率是 0x36 寄存器里的扫描方向不对,或者 RGB/BGR 颜色顺序反了,需要把参数改成 0xC0 或 0x08 逐个试。
  • 随机杂点 + 偶发性花屏:先降 SPI 速率,再看杜邦线是不是太长。超过 10 厘米的杜邦线在 20MHz 下很容易串扰。
  • 画面偏移到屏幕边缘:初始化命令里缺少 Set Address Mode 或窗口设置,很多驱动 IC 默认扫描顺序和屏幕模块的排线方向不一致。

我这次遇到的问题是内容整体向右偏移了大概 20 像素。查了半小时,发现是初始化序列里没发“列地址/行地址”命令,导致控制器默认窗口起始坐标和屏幕物理原点不匹配。加了一组设置显示窗口的命令后,画面就正常了。这类问题用“逐条加命令、随时看屏幕变化”的方式排查,比盲猜高效得多。

5.2 WiFi 连接不稳定

跑 Demo 的过程中,MCU 偶尔会连不上 WiFi,或者连着连着就断线。我发现有几个原因需要逐一排除。

第一,天线位置。开发板的天线如果贴在金属桌腿或者机箱后面,信号强度会明显下降。我把板子竖起来,天线朝外,信号从 -65dBm 改善到 -48dBm,断线次数立刻减少。第二,电源电流。MCU 的 WiFi 开启瞬态电流很大,如果 USB 口供电不足,电压跌到 4.3V 就会直接掉线重启。换成单独 5V/2A 电源后,问题消失。第三,重试逻辑。没写断线自动重连时,我遇到过连不上只能拔插电源的尴尬。后来增加了一个简单的状态机:每 5 秒检查一次连接状态,断开就自动重连,重连成功后延迟 2 秒再发 HTTP 请求。

还有一个细节:路由器信道拥挤。我所在环境里 2.4G 信道堆了很多信号,我把路由器固定到信道 1,状态明显稳定。如果你的路由器没有固定信道选项,可以考虑在 MCU 固件里配置静态 IP,减少 DHCP 交互失败的概率。

5.3 内存不足和 JSON 解析崩溃

MCU 虽然有 240MHz 双核,但它毕竟还是嵌入式设备,内存比电脑小得多。我用的这块板子可用内存大概 300KB 左右,看着不小,但如果每个模块都开大缓冲区,很快就会不够用。

第一次写 JSON 解析时,我豪爽地开了一个 4KB 的 response_buf,然后又在绘字时申请了一块 8KB 的临时画布,结果启动后没几分钟就死机。后来分析日志,发现是动态内存碎片化,再加上 JSON 解析函数对边界条件没有判断,一个超长字符串直接越界覆盖了别的数据。

解决办法有三条,都在实际项目中验证过有效:

  • 所有 HTTP 响应缓冲区大小不超过 1024 字节,服务端 JSON 结构严格控制字段数量和长度。
  • JSON 解析用固定字符串搜索而不是完整解析,不使用动态分配。
  • 所有字符串拷贝操作全部用带长度上限的安全函数,像snprintf、strlcpy,不使用裸的sprintf和strcpy。

改完这些之后,固件连续运行了 48 小时没有死机。记住,在 MCU 上写代码,内存上限意识是刻到骨子里的习惯。

5.4 逻辑电平不匹配的隐藏坑

最后分享一个特别隐蔽的问题:逻辑电平不匹配。我一开始把屏幕模块的 VCC 接到了开发板的 5V 引脚上,因为屏幕模块丝印上面就写的 5V。结果屏幕是亮了,但颜色明显偏色,而且偶尔出现完全没有规律的横线。我以为是 SPI 速率问题,降速也没用。

后来用万用表量了一遍,才发现 MCU 的 SPI 输出是高电平 3.3V,屏幕模块的输入引脚被拉到了 5V 上,导致逻辑电平判断出错,数据时序不稳。把 VCC 改回 3V3,并把所有信号线确认都是 3.3V 电平后,偏色和横线问题同时消失。

所以拆箱后第一步真的别急着接线,先搞清楚板上哪几个引脚是 3V3、哪几个是 5V,再用万用表确认一下屏幕模块的实际供电电压。这块屏幕模块虽然丝印上有多种供电标注,但实际驱动 IC 的逻辑部分大多按 3.3V 工作。接错一次不一定烧,但一定会让你在排查显示异常时多花好几个小时。这个坑我记在小本上了,分享出来,希望大家别再踩。

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

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

立即咨询