把 VFD-GP1211AI(GP1211AI / Noritake MN12864K 128×64 VFD 的 STM32F103 Arduino 驱动)移植到 RP2040 / Raspberry Pi Pico(Pico SDK),并顺手修掉了原驱动的一批 规格合规与健壮性问题。仓库内附带可在 PC 上运行的测试(不需要 Pico SDK)。
128×64 VFD(GP1211AI,无显示控制器)
│ 384bit 阳极链 + 48bit 栅极链 + 45V VDD2 + 2.9Vac 灯丝
RP2040(Pico SDK, C++17, CMake)—— 两种扫描引擎,编译期二选一
├─ tick(默认): SPI0 4.5 MHz(实测 4.464 MHz = 125 MHz/28)MODE3 → CLKa/SIa + DMA + repeating_timer 189 µs
└─ pio : PIO 状态机产生 CLKa/SIa/CLKg/LAT/SIg + 整帧 DMA(CPU 只在帧边界介入)
公共: PWM(一周期 = 一扫描周期)→ BK 消隐/调光;双缓冲发布;扫描心跳看护
可选: 双核分工(-DVFD_DUAL_CORE=ON,默认关)—— core0 = 扫描时序 + 帧发布 + 看护,
core1 = SSD1306 数据面(排水/解码/渲染);核间 2 槽 SPSC 双缓冲 + 序号握手,宁丢帧不撕裂
另: 可把本机当"一片 SSD1306 OLED"用(4 线 SPI 从机 + 命令/GDDRAM 行为模拟)
- 帧缓冲 1 KB + 双缓冲发送缓冲 2×2064 B;tick 引擎中断 CPU ≈2%,PIO 引擎 ≈0.06%
(原驱动 ≈24% 忙等)。双核可把 SSD1306 数据面整个搬到 core1,core0 只剩时序与发布;
占用率可用诊断里的 4 个子项(
core0_irq/core0_work|core1_drain/core1_render)直接量出来 - 完全符合手册关键条款:fCLK ≤ 5 MHz、CLK 空闲高(Note 7①)、 传输期间 BK 不变(Note 7②③④)、桁间消隐 ≥5 µs(Note 16)
- 自带扫描心跳看护:手册 Note 14 明确"栅极扫描停止可能永久损坏屏", 停摆会自动恢复,连续失败则先关高压再停扫描
- SSD1306 行为模拟:对端 MCU 用现成 SSD1306 库(Adafruit_SSD1306 / u8g2…)
经 4 线 SPI 直接把画面发过来即可;支持显示/反显/全亮/对比度(→亮度)、页/水平/垂直寻址、
滚动(
0x26/0x27/0x29/0x2A+0x2E/0x2F);内置测试图像默认不渲染,只有把VFD_PIN_TEST_MODE(默认 GP16)在上电时拉低才进入测试模式
| 项 | 结论(已固化进实现,不再是编译开关) | 代码位置 |
|---|---|---|
| LAT 极性 | 空闲低、锁存打正脉冲(驱动电路无反向器;手册 Figure 3 画的就是正脉冲) | .pio 的 set 组 / tick 的 vfdLatPulse() |
| CLKg 极性 | 空闲高;低→高脉冲的上升沿 = 栅极链前进一位 | 同上 |
| 扫描边界 | 四步:① CLKg 拉低 → ② CLKg↑ 前进 → ③ LAT↑ 锁存 → ④ LAT↓ 释放(先前进再锁存,否则画面整体平移 3 列) | src/vfd_scan.pio / tick 引擎 |
| 帧首播种 | 由 CPU 在帧末 DMA 完成中断里做(SIg = 1,1,0,0,0 五个 CLKg 脉冲),全程保持 LAT 低(否则链搬运可见 = 边缘闪烁/线框抖动),落点等 BK 点亮窗口以保证确定 | Rp2040Platform::seedGrid() / onFrameDmaDone() |
| 带内 3 列槽序 | 原驱动约定:t 偶 → off 0,2,4、t 奇 → off 5,3,1 |
packScan() |
| 扫描相位 | −1(边界锁存的是上一扫描移入的数据 ⇒ 数据侧提前一个扫描发) | wirePrepareFrame() |
| T43 | 手册 Note 12:该时序只能用 a,b ⇒ off 2 / off 4、k 整体 −2、只写两个槽(c,d,e,f 全 OFF) |
packScan() 的 t43 分支 |
| 数据段 | 逐 bit 消费(set y,2 + set x,15 + jmp x-- + jmp y--,程序 25/32);不要再用"每扫描一个控制字 + out x,32 按字读 OSR"(OSR 初始 32 bit 会让流错位) |
src/vfd_scan.pio |
两条 wait |
不可省(一次扫描只有 ≈88 µs,结束时 BK 仍为消隐高电平) | 同上 |
├─ CMakeLists.txt # Pico SDK 目标工程(-DVFD_SCAN_ENGINE=tick|pio)
├─ src/
│ ├─ vfd_scanpack.{h,cpp} # 平台无关:帧缓冲 -> 43×48 字节位重排(纯函数)
│ ├─ vfd_gp1211ai.{h,cpp} # 驱动层(继承 Adafruit_GFX):绘图原语 + 双缓冲发布 + 看护
│ ├─ vfd_platform.h # 硬件抽象接口(换 MCU 只实现它)
│ ├─ vfd_platform_rp2040.{h,cpp} # 两引擎共用:引脚/上电/亮度 PWM/发布/看护
│ ├─ vfd_platform_rp2040_tick.cpp # tick 引擎:repeating_timer + SPI + DMA
│ ├─ vfd_platform_rp2040_pio.cpp # pio 引擎:PIO 状态机 + 整帧 DMA
│ ├─ vfd_scan.pio / .pio.h # 扫描 PIO 程序(25 条指令 / SM 上限 32)与已生成头文件
│ ├─ ssd1306_emulator.{h,cpp} # SSD1306 行为模拟核心(命令/GDDRAM/寻址/渲染,可宿主测试)
│ ├─ ssd1306_slave_rp2040.{h,cpp} # 4 线 SPI 从机传输层(PIO + DMA 环形缓冲)
│ ├─ ssd1306_spi_slave.pio / .pio.h # 从机 PIO 程序(15 条指令)与已生成头文件
│ ├─ demo_screens.{h,cpp} # 测试模式用的内置图像(宿主机测试也会用)
│ ├─ main.cpp # 示例程序:按测试引脚选择"测试图像"或"SSD1306 模拟"
│ └─ compat/ # Arduino 兼容层(让 Adafruit_GFX 在裸 Pico SDK 上编译)
├─ lib/Adafruit_GFX/ # vendor 的 Adafruit GFX v1.2.3(BSD,仅必需 4 文件)
├─ tests/ # 宿主机测试(g++,无需嵌入式工具链)
│ ├─ run_host_tests.ps1 # 一键跑:6 个测试程序(289 项 + 核间握手 79102 项)+ SDK 语法桩 + 6 个 .ino 语法
│ ├─ reference_pack.* # 原驱动 display() 的 1:1 转录(位重排对照用)
│ ├─ host_syntax/stub/ # Pico SDK 形状桩(让目标机源码能在 PC 上做 -fsyntax-only)
│ ├─ host_syntax/stub_arduino/ # Arduino/Adafruit/u8g2 形状桩(给 examples/ 的 .ino 做语法检查)
│ └─ tools/ # 逻辑分析仪抓包判读脚本(node:渲染/合规/位序反解)
├─ examples/ # 主机侧样例 sketch(Pico 2 / Due;u8g2、Adafruit、接线探针)
├─ hardware/ # 接线与电平注意事项(wiring.md)+ 预留(原理图/实物照片)
└─ docs/综合技术报告.md # 精简技术报告(纯 Markdown)
powershell -File tests\run_host_tests.ps1 # 需要 g++/clang++(6 个程序:44+121+28+53+43 = 289 项,另加核间握手回归 79102 项)跑的是目标机同一份驱动源码(src/),用一个模拟扫描引擎的 MockPlatform:
位重排与原驱动 1:1 转录逐位对比、双射校验、双缓冲无撕裂回归、看护逻辑、示例画面 ASCII 预览;
ssd1306_host_tests.exe(121 项)则覆盖 SSD1306 模拟核心(标准初始化+整屏写入 ⇒ 渲染与主机图像逐位一致、
段/COM 镜像、三种寻址模式与窗口回绕、显示开关/全亮/反显/对比度、未知命令与跨调用拆分的参数、滚动:
参数个数按命令不同、垂直只上移、0xA3 滚动区、0x2E 停止后垂直复位而水平保留,以及
Adafruit_SSD1306 等主机库的真实滚动字节流回放);
另有 pio_seed_timing(28 项:PIO 程序结构 + 边界四步 + 扫描相位模型)、
ssd1306_pio_host_tests(53 项:从机 PIO 线上格式)、master_phases_host_tests(43 项:主控相位序列回放)
与 dualcore_handoff(79102 项:双核核间 2 槽 SPSC 协议的全状态组合 —— 丢帧守卫、槽位轮换、最新帧选择)。
脚本还会用Pico SDK API 形状桩(tests/host_syntax/stub/)对目标机源码
(vfd_platform_rp2040.cpp / _tick.cpp / _pio.cpp、ssd1306_emulator.cpp、
ssd1306_slave_rp2040.cpp、main.cpp,分别以 -DVFD_SCAN_ENGINE_PIO=0/1、
-DVFD_DEBUG_DIAG=0/1、-DVFD_DUAL_CORE=0/1 的组合编译多遍,共 17 个 SDK 目标 + 6 个 .ino)
做 -fsyntax-only 语法/类型检查 —— 在没装 pico-sdk 与 arm-none-eabi-gcc 的机器上
也能抓出语法、类型、成员名错误(它不能替代真实固件构建)。
export PICO_SDK_PATH=/path/to/pico-sdk # Windows: $env:PICO_SDK_PATH="C:\pico\pico-sdk"
# 默认:tick 引擎(定时器 + SPI + DMA)+ SSD1306 行为模拟
cmake -S . -B build -G Ninja -DPICO_BOARD=pico
cmake --build build -j
# build/vfd_gp1211ai_demo.uf2 -> 按住 BOOTSEL 插入 USB,拖入即可
# 可选:PIO 扫描引擎(扫描时序完全由硬件状态机产生)
cmake -S . -B build-pio -G Ninja -DPICO_BOARD=pico -DVFD_SCAN_ENGINE=pio
cmake --build build-pio -j
# 可选:双核分工(core0 = 时序 + 帧发布 + 看护;core1 = SSD1306 数据面)
cmake -S . -B build-dual -G Ninja -DPICO_BOARD=pico -DVFD_DUAL_CORE=ON
cmake --build build-dual -j本机已验证过的完整命令(工具不在 PATH 时把这两条路径显式带上即可;当前固件就是这样构建的):
$tools = '<你的工具链目录>' # 例如 pico-sdk / xpack gcc / picotool 所在目录 $env:PATH = "$tools\xpack-arm-none-eabi-gcc-13.2.1-1.1\bin;$env:PATH" # 把 ninja 所在目录也加上 cmake -S . -B build-rp2040-pio -G Ninja "-DCMAKE_MAKE_PROGRAM=C:\Program Files\Meson\ninja.EXE" ` "-DPICO_SDK_PATH=$tools/pico-sdk" -DPICO_BOARD=pico -DPICO_NO_PICOTOOL=1 ` -DCMAKE_BUILD_TYPE=Release -DVFD_SCAN_ENGINE=pio cmake --build build-rp2040-pio # 生成 uf2(SDK 内建 picotool 被关掉时用独立 picotool) & "$tools\picotool\picotool\picotool.exe" uf2 convert ` build-rp2040-pio\vfd_gp1211ai_demo.elf build-rp2040-pio\vfd_gp1211ai_demo.uf2产物(Release;诊断关 / 诊断开):
build-rp2040/vfd_gp1211ai_demo.uf2(tick 单核,129.5 / 146.5 KB,text 66148 / 74868 B)与build-rp2040-pio/vfd_gp1211ai_demo.uf2(pio 单核,129.5 / 146.5 KB,text 66164 / 74756 B); 双核(-DVFD_DUAL_CORE=ON,诊断开)tick 140.0 KB、pio 139.5 KB;干净重建一律 0 告警 0 错误。
脚本会自动定位工具链(arm-none-eabi-gcc / ninja / Pico SDK / picotool / pioasm,也认 PICO_SDK_PATH 等环境变量),
设好环境变量后逐项问你要编哪个模式,再配置、编译、转出 .uf2,最后打印体积、产物路径和"板上启动横幅应为"的指纹。
powershell -ExecutionPolicy Bypass -File build.ps1 # 交互式:引擎 / 诊断 / 类型 / 双核 / 目录名
powershell -ExecutionPolicy Bypass -File build.ps1 -Yes # 全默认:pio + 诊断 off + Release + 单核
powershell -ExecutionPolicy Bypass -File build.ps1 -Engine both -Diag on -Yes # 两引擎各出一个 uf2(诊断只走 UART)
powershell -ExecutionPolicy Bypass -File build.ps1 -DualCore -Diag on -DiagUsb -Yes # 双核 + 诊断同时走 USB 与 UART
powershell -ExecutionPolicy Bypass -File build.ps1 -Engine pio -Dir build-x -Yes # 指定构建目录
powershell -ExecutionPolicy Bypass -File build.ps1 -Test -Clean -Yes # 先跑宿主测试、删目录重来
powershell -ExecutionPolicy Bypass -File build.ps1 -Clean -Yes -CMakeVar VFD_TICK_RX_TOPUP=0 # 额外透传 CMake 变量(可逗号分隔多个)
powershell -ExecutionPolicy Bypass -File build.ps1 -CleanOnly # 只清理本次会用到的构建目录(不编译、不提问)
powershell -ExecutionPolicy Bypass -File build.ps1 -CleanAll # 只清理所有 build-* 与 tests\build(不编译、不提问)| 参数 | 取值 | 含义 |
|---|---|---|
-Engine |
pio / tick / both |
扫描引擎 |
-Diag |
on / off |
逐秒诊断。默认 off;on 时诊断只走 UART(GP0/GP1),uf2 大 ~17 KB(实测 129.5 → 146.5 KB) |
-DiagUsb |
开关 | 诊断是否也走 USB(默认只走 UART)。USB stdio 的写会阻塞主循环,排故时不想额外接 UART 再开它 |
-DualCore |
开关 | 双核分工:core0 = 扫描时序 + 帧发布 + 看护,core1 = SSD1306 数据面。默认关 = 与单核逐行为一致 |
-Type |
Release / Debug |
构建类型(只影响优化级别/调试信息,与诊断开关无关) |
-Dir |
目录名 | 构建目录(默认 build-rp2040-pio / build-rp2040;后缀见下) |
-Test |
开关 | 编译前先跑宿主测试 |
-Clean |
开关 | 先删本次的构建目录,再编译(清理阶段不提问) |
-CleanOnly |
开关 | 只清理:删掉本次会用到的构建目录后退出(不编译、不提问) |
-CleanAll |
开关 | 只清理:删掉仓库内所有 build-* 与 tests\build 后退出(不编译、不提问) |
-CMakeVar |
NAME=VALUE[,NAME=VALUE…] |
额外透传 CMake 变量(自动补 -D;省略 -Dir 时构建目录名带 -custom 后缀) |
-PioRegen / -Yes / -Help |
开关 | 用 pioasm 重新生成 .pio.h / 不提问 / 打印详细帮助 |
构建目录后缀:-diagon(诊断开)/ 无后缀(诊断关,默认)/ -diagusb(诊断也走 USB)/ -dual(双核)/ -debug(Debug 类型)。
屏幕时序相关的调参开关已全部移除(2026-10-08 实机定标后固化进实现,见下方"已实机验证"): LAT 极性、带内 3 列槽序、扫描相位都不是编译选项了,改它们要动源码(
src/vfd_scanpack.h顶部有完整说明)。
.pio比生成的.pio.h新时脚本会提醒(这类"改了源却编了旧头"的坑踩过),需要时加-PioRegen。
| CMake 选项 | 默认 | 说明 |
|---|---|---|
VFD_SCAN_ENGINE |
tick |
tick = 定时器+SPI+DMA;pio = PIO 状态机+DMA |
VFD_DUAL_CORE |
OFF | 双核分工(core0 时序+发布,core1 SSD1306 数据面);关 = 单核行为 |
VFD_DEBUG_DIAG |
off |
逐秒诊断输出(on/off)。与 CMAKE_BUILD_TYPE=Debug 无关 |
VFD_DEBUG_DIAG_USB |
off |
诊断是否也走 USB;默认只走 UART(uart_write_blocking),不碰 USB |
VFD_PIO_CLK_HZ |
4500000 | PIO 引擎的 CLKa 频率(须 ≤5 MHz;SM 时钟 = 2×) |
VFD_PIN_TEST_MODE |
16 | 测试模式选择:上电该脚为低 → 渲染内置测试图像;默认高 → 只做 SSD1306 从机 |
VFD_EMU_PIN_SCK/MOSI/DC/CS/RESET |
11/12/13/14/15 | SSD1306 从机引脚(DC 必须 = MOSI+1;RESET=255 表示不接) |
VFD_EMU_PIO_INST / VFD_EMU_PIO_SM |
1 / 0 | 模拟器用的 PIO 块/状态机(须与扫描引擎不同块) |
VFD_EMU_DMA_CH |
2 | 模拟器 DMA 通道(勿与扫描引擎冲突) |
VFD_TICK_RX_DMA_COUNT |
0xFFFFFFFF |
tick:RX 排空 DMA 的计数(跑满即补计数 rearm;按 48 B/189 µs 折算默认约 2.35 h 一轮) |
VFD_TICK_RX_TOPUP |
1 | tick:RX DMA 补计数开关(0 = 关,A/B 实验用) |
VFD_TICK_SERVICE_DRAIN |
1 | tick:service() 里用 CPU 顺带排空 SPI RX FIFO(0 = 关,A/B 实验用) |
VFD_PIO_REGENERATE |
OFF | 用 pioasm 重新生成 src/*.pio.h(改了 .pio 才需要) |
VFD_PIN_* / VFD_DMA_* |
见下表 | 覆盖默认接线/通道 |
VFD_STDIO_USB / VFD_STDIO_UART |
ON | stdio 开关(无 tinyusb 子模块时关掉 USB) |
引脚覆盖:-DVFD_PIN_BK=15 -DVFD_PIN_LAT=4 ...(默认接线见下表)。
PIO 引擎额外要求 LAT/CLKg/SIg 三者引脚连续(默认 GP4/5/6),否则 engineError() 会置位。
| 项 | tick 引擎(默认) | pio 引擎 |
|---|---|---|
| pico-sdk / 工具链 | 2.1.0(含 lib/tinyusb)/ xPack GNU Arm Embedded GCC 13.2.1 + Ninja |
同 |
| 配置 | -DPICO_BOARD=pico -DCMAKE_BUILD_TYPE=Release |
同上 + -DVFD_SCAN_ENGINE=pio |
| 诊断输出 | -DVFD_DEBUG_DIAG=off(默认)。on = 逐秒打印 SSD1306 状态 / CPU 占用 / 帧抖动 / pinmon / slave-dbg / pc_hist / waits / gapmax(排水间隔) / SCAN(RX DMA),默认只走 UART(-DVFD_DEBUG_DIAG_USB=on 才同时走 USB);off 只留启动横幅与告警 |
同左(两引擎共用同一份 main.cpp) |
| Flash / RAM | 诊断 off text 66148 B / bss 33564 B;诊断 on text 74868 B / bss 33616 B |
诊断 off text 66164 B / bss 35604 B;诊断 on text 74756 B / bss 35656 B |
| 中断放置 | scanTimerThunk(含内联的 scanTick)位于 RAM 0x200000d4,292 B |
dmaIrqThunk(含内联的 onFrameDmaDone/排程)位于 RAM 0x20000150,276 B |
| PIO 程序 | 从机 15/32 条指令(ssd1306_spi_slave.pio,含滚动) |
扫描 25/32(vfd_scan.pio)+ 从机 15/32,运行时均经 pio_can_add_program() 校验 |
| 产物 | build-rp2040/…uf2(诊断关 129.5 KB / 诊断开 146.5 KB) |
build-rp2040-pio/…uf2(诊断关 129.5 KB / 诊断开 146.5 KB) |
双核(-DVFD_DUAL_CORE=ON,诊断开) |
build-rp2040-diagon-dual/…uf2 140.0 KB(text 71500 B / bss 36688 B) |
build-rp2040-pio-diagon-dual/…uf2 139.5 KB(text 71228 B / bss 38728 B) |
如果本机没有安装 picotool,可先 -DPICO_NO_PICOTOOL=1 配置(此时不产出 uf2),
再用任意独立 picotool 转换:picotool uf2 convert <elf> <uf2>。
构建过程中发现并修掉的几个真实陷阱:
- SDK 里没有
pwm_channel_t,它是enum pwm_chan { PWM_CHAN_A, PWM_CHAN_B }, 而pwm_gpio_to_channel()返回uint; - PIO 相关头文件一旦启用
-O3,scanTick()会被内联进调用它的 thunk —— 若只给scanTick()加__not_in_flash_func,中断代码仍会留在 Flash;两个 thunk 也要一起标注; - pioasm 不支持
.equ/|运算符,且set立即数上限 31(48 字节要拆成 3×16 循环); - SSD1306 从机的 DC 必须紧邻 MOSI(
in pins,2一次采 {MOSI,DC}),代码会校验。
| Pico | GP2 | GP3 | GP4 | GP5 | GP6 | GP7 | GP8 | GP9 | GP10 | GP11 | GP12 | GP13 | GP14 | GP15 | GP16 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 用途 | CLKA | SIA | LAT | CLKG | SIG | BK | HVEN | FLEN | —(保留) | 主机 SCK | 主机 MOSI | 主机 DC | 主机 CS | 主机 RESET | 测试模式¹ |
¹ 上电为低 → 渲染内置测试图像;高/悬空 → 只做 SSD1306 从机(默认)。 (GP10 早期用作 PIO 的"帧末播种请求"握手脚,现播种由 CPU 在帧末直接翻引脚,该脚已不再使用。)
⚠️ 驱动板有 45 V(VDD2) 与 2.9 Vac 灯丝;FLEN高电平是关断灯丝驱动。 接线、上电前请确认驱动板状态,调试时建议先断开高压只验证逻辑。
#include "vfd_gp1211ai.h"
#include "vfd_platform_rp2040.h"
vfd::Rp2040Platform platform(vfd::defaultRp2040Config());
VFD_GP1211AI display(platform);
display.begin(); // 平台 init(含扫描引擎)+ 灯丝预热 + 上高压
display.setBrightness(200); // 0..255
display.clearDisplay();
display.setTextColor(WHITE);
display.setCursor(0, 0);
display.print(F("hello, VFD"));
display.display(); // 重排 + 发布
for (;;) {
display.task(); // 必须周期调用:扫描看护 + 杂务
// ... 画下一帧 ...
display.display();
}| 项 | 原驱动(STM32F103) | 本移植(RP2040) |
|---|---|---|
| 阳极时钟 | setClock(12e6) → 实跑 9 MHz(超手册 5 MHz) |
目标 4.5 MHz(实测 4.464 MHz),MODE3(空闲高) |
| 阳极数据搬运 | ISR 阻塞式 SPI.write,≈24% CPU |
DMA,ISR 只翻转引脚 + 触发,≈1~2% CPU |
| 调光 | BK 94 kHz 自由 PWM(违反 Note 7②) | "消隐窗口":一 PWM 周期 = 一扫描周期 |
| 桁间消隐 | 无显式实现(Note 16) | ≈100 µs(≫ 5 µs) |
| 帧缓冲/撕裂 | display() 与 ISR 共用一块发送缓冲 |
双缓冲 + 帧边界握手,任意调用节奏不撕裂 |
| 灯丝预热 | 100 ms | 400 ms(可配) |
| 扫描保护 | 无 | 心跳看护 → 自动恢复 → 连续失败关高压 |
| 颜色 ≥3 | 静默丢弃(默认文字色画不出字) | 归一化为 WHITE |
invertDisplay() |
无效(基类空实现) | 生效(重排阶段按像素取反) |
fillScreen(INVERSE) |
变成白色 | 整体取反(与 fillRect(INVERSE) 一致) |
setRotation() |
会越界写坏 _sendBuffer |
拒绝非 0 值 |
| 列 128 越界读 | 依赖对象内存布局的 UB | 显式跳过(该位本就不会点亮) |
| 可移植性 | 驱动里写死 Arduino/STM32 | 算法层 + 驱动层 + 平台层分离 |
docs/综合技术报告.md—— 精简技术报告(纯 Markdown,约 1/6 篇幅): 项目概述、硬件与手册要点、软件架构、扫描时序的实机定标结论、SSD1306 模拟支持面、 与原驱动的差异、验证方法、已知取舍。
源码注释里出现的
docs/NN-*.md(如docs/13)指向开发仓库的完整分册文档集(原驱动不足分析、 移植说明、PIO 引擎、实测定标、SSD1306 行为模拟、排故报告等)。本仓库只随附上面这份精简报告, 分册不在本仓库内 —— 需要细节时请以源码注释 + 本报告为准,或向维护者索取分册。
默认模式下本机不渲染任何内置测试图像,而是等主机通过 4 线 SPI 送画面 (主机侧代码与驱动真实 OLED 完全一样,无需改动):
// 主机(例如另一块 Arduino):SCK→GP11, MOSI→GP12, DC→GP13, CS→GP14, 共地
// ⚠️ bitrate 必须显式给 4 MHz:Adafruit_SSD1306 的默认值是 8 MHz(见下方要点)
Adafruit_SSD1306 oled(128, 64, &SPI, /*dc=*/9, /*res=*/-1, /*cs=*/10, /*bitrate=*/4000000);
oled.begin(SSD1306_SWITCHCAPVCC, 0);
oled.clearDisplay();
oled.setTextSize(2);
oled.setTextColor(SSD1306_WHITE);
oled.setCursor(0, 0);
oled.println(F("VFD as SSD1306"));
oled.display(); // 调一次即可,本机持续显示要点:
- 主机 SPI 用模式 0(CPOL=0, CPHA=0);
DC必须接在 MOSI 的下一个 GPIO(GP12→GP13); - 支持页/水平/垂直寻址、列页窗口、显示开关、全亮、反显、
0x81对比度(→ VFD 亮度)、RESET、 滚动(0x26/0x27水平右/左 = 6 个参数、0x29/0x2A垂直+水平 = 5 个参数、0x2E/0x2F停止/启动、0xA3垂直滚动区域;间隔按规格书帧数表,1 帧折算 8 ms); 水平滚动改写 GDDRAM(0x2E停止后位移保留),垂直滚动是显示级上移(0x2E停止后上下自动复位、 只能向上)——详见docs/综合技术报告.md§5; 未知命令忽略并计数; - 段/COM 重映射按"标准模块"约定:库默认的
0xA1+0xC8与主机缓冲同向(逐位一致); - 内置测试图像只在
VFD_PIN_TEST_MODE(GP16)上电为低时渲染,用于点亮/时序自检; - 上电即抓取:主机接收通路在
main()一开始就启动(早于 440 ms 灯丝预热与启动横幅), 预热也不阻塞 CPU ⇒ 复位后主机发来的初始化序列与首批画面不会丢;横幅里的boot capture: N B received before this loop (overrun=… drop=…)可核对上电窗口接收量。 主机侧仍建议上电 ≥150 ms 后再发初始化(或先把 RESET 拉低 ≥150 ms 再释放)—— RP2040 自身启动的那几十毫秒无法接收任何字节,详见docs/综合技术报告.md§5。 - 失步看护:从机检测到硬件 RXSTALL(时钟/数据受扰导致 PIO 停摆)时立刻停 SM 与 DMA、
丢弃本帧剩余字节并重新对齐(最多损失一帧,不会整屏错位),CS 抬起等待有 20 ms 兜底;
诊断计数
stallfix/stallforced正常恒 0。 - 主机时钟用 4 MHz(
examples/下有 u8g2 与 Adafruit 两个可烧样例 +wiring_probe_due_r4探针):⚠️ Adafruit_SSD1306的spiSettings默认 8 MHz,请显式给 4 MHz(构造函数最后的 bitrate 参数, 或SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0)))。理由有三条: · 8 MHz 下 PIO 从机的字节边界余量只剩 ≈85 ns(4 MHz 是 210 ns),CPU 排水占用 >70%(4 MHz ≈35%); · 更关键的是帧结构:Adafruit_SSD1306::display()把"6 条窗口命令 + 1024 B 数据"压在一次 CS 拉低里 (单事务最大 1030 B),而 u8g2 是每页一个事务(≤132 B)。本机唯一的失步恢复点是 CS 边界 (endTransaction(),500 µs 去抖) ⇒ 同样一次位相位错位,Adafruit 毁整帧、u8g2 最多毁一页; · 4 MHz 对 83 fps 刷新绰绰有余。要试 8 MHz 请先读docs/综合技术报告.md§8 的余量说明。 - 接线:Arduino Due 的硬件 SPI 只在 2×3 ICSP 排针(ICSP-3=SCK/GPIO76、ICSP-4=MOSI/GPIO75;
数字口 11/12/13 不是 SPI);Uno R4 Minima 是 5 V,直连超规格(需电平转换或先降速短时试);
DC必须接在 MOSI 的下一个 GPIO。 - 诊断怎么读(
-Diag on,每秒一行):启动横幅的fw: <编译时间> …唯一标识固件版本;gapmax=<µs>(起点段→终点段,rx+NB,big=N)是"本窗口最长多久没把主机数据取走",以及当时主机是否在发 (rx+0⇒ 没发,无害)。危险线 = 4094 B ÷ 主机字节率:4 MHz 8.19 ms | 8 MHz 4.09 ms; 实测 4.464 MHz 下修复后为 2.79 ms(= 一个 32 B 诊断分块的串口时间,-Diag on的地板),overrun/drop全程 0。
| 场景 | 建议 |
|---|---|
| 一般使用、想省一个 PIO 资源、习惯用 SPI | tick(默认) |
| 系统里有高优先级中断/长临界区,希望扫描时序不被软件拖累 | pio |
| 需要 CLKa 频率精确可调、或以后要接第二块屏(PIO 可多 SM 并行) | pio |
两种引擎输出到屏上的时序效果一致(同样的 189 µs 扫描周期、同样的消隐窗口与亮度映射), 差别只在"谁产生时序":tick 靠 5.29 kHz 中断,pio 靠状态机 + 123 Hz 的换帧中断。
双核分工(
-DualCore)与引擎选择正交:它只决定"SSD1306 数据面跑在哪个核",扫描引擎照旧二选一。 打开后 core0 = 扫描时序 + 帧发布 + 看护,core1 = SSD1306 排水/解码/渲染(核间 2 槽 SPSC 双缓冲 + 序号握手, 宁丢帧不撕裂;默认关 = 与单核逐行为一致,可一键回滚)。 占用率用诊断的 4 个子项量:单核core0: irq/drain/render/work,双核core0: irq/work | core1: drain/render。
- 峰值亮度 ≈47%:为满足 Note 7②,阳极数据在消隐期间搬运,点亮窗口最大 ≈89/189。
tick 引擎受限于 48 字节移位时间(≈86 µs),PIO 引擎受限于 802 周期的数据阶段(≈89.1 µs);
两者都会在
init()里自检,必要时抬高blank_guard_us,余量可用guardMarginUs()读出。 - 仍是"整帧重排 + 双缓冲"模型(约 4 KB RAM),未改成"每次扫描现场生成 48 字节"。
- PIO 引擎的数据通路仍依赖 CPU 换帧:CPU 长时间不响应帧中断时 DMA 会停供,
PIO 卡在
out(engineStalled()可查)。要做到"CPU 完全脱离也不停扫", 下一步可把整帧 DMA 做成自循环控制块链(间接寻址 + 自触发)。 - PIO 引擎靠 BK 引脚的电平沿同步,因此亮度 0 时仍保留 1 µs 点亮窗口 (占空 0.5%,视觉上等同全黑)。
- SSD1306 模拟:只支持 SPI 模式 0;
0xA8多路复用比只记录;渲染有 12 ms 节流(最高 83 fps, 足够呈现最快档滚动 16 ms/步,主机的多次局部刷新会合并显示);滚动时间基准是折算值 (1 帧 = 8 ms,非逐晶振复刻);从机环形缓冲 16 KB(4096 字节容量),异常突发时会计数overrunCount()/droppedCount()、保留最新数据并自动重新对齐。
| 工作流 | 触发 | 做什么 |
|---|---|---|
ci.yml |
push 到 main / PR / 手动 |
跑宿主机测试(289 项 + 核间握手 79102 项 + SDK 语法桩 + 6 个 .ino 语法),并编译 4 种固件组合(tick / pio × 单核 / 双核),uf2 作为构建产物上传 |
release.yml |
推 v* 标签 / 手动 |
先跑测试,再编译同样 4 种组合,然后自动创建 GitHub Release 并把 4 个 uf2 附上(-tick / -pio / -tick-dual / -pio-dual,发布说明自动生成) |
发版只需打标签:
git tag -a v1.0.0 -m "v1.0.0" && git push origin v1.0.0徽章在本页顶部;点进去可看每次运行的分步日志与 uf2 产物。
MIT(见 LICENSE):驱动算法与位映射源自原项目(_VIFEXTech / Trigger-CN),本仓库为其 RP2040 移植。
lib/Adafruit_GFX/ 为 Adafruit GFX v1.2.3(BSD,见 lib/Adafruit_GFX/license.txt)。
tests/reference_pack.* 是原驱动 display() 的 1:1 转录,仅用于测试对照。