一份项目记录。主角是一块二十块钱左右的 STC8 单片机做的掌上示波器: 把固件从原版移植到 45 MHz 的 56DZ 核心板,并补上了信号源、串口数据流、 电脑上位机、双光标测量、开机自检,以及一整套可校准的时基系统。
仓库:WuSiTech/mini-dso(固件、上位机、README 都在里面)
一、缘起:原版、板子和 45 MHz
一切的起点是 CreativeLau 的开源项目 Mini-DSO v0.3:STC8 单片机 + 128×64 OLED + EC11 旋钮,做成一个能塞进口袋的示波器。后来有玩家把它适配到了 56DZ 版硬件 (对应 Elektor 的 reverse-engineering 项目),我们拿到的就是这块板子。
原版固件跑的不是 45 MHz,而 56DZ 板载的 STC8A8K64D4 我们习惯让它跑 45 MHz 内部 IRC。 主频一改,所有和"时间"有关的东西全部要重算:
- 采样间隔(时基)
- ADC 的时钟分频
- 串口波特率(Timer2)
- 波形发生器与方波的定时器
这一轮改造的成果是:21 档时基、从 500 ms/div 一直到 80 µs/div, 每档都用实测值重新标定,而不是照搬原版表格。
二、第一场硬仗:时基校准
一开始时基误差高达 ±60%,因为"延时函数"的实际耗时和原版假设完全不一样。 解决思路不是拍脑袋,而是测:
- 让固件从 P1.1 输出一个已知分频的测试方波(
0x15命令) - 电脑端采集波形、数一个周期占多少个采样点
- 反推出真实的采样间隔,逐档修正延时表
- 把 21 档修正值写进 EEPROM
这套流程后来被做成上位机的**「一键校准」**:自动驱动设备输出参考方波、 逐档测量、把校准表回写,全程 1~2 分钟。
- 校准表格式:21 ×
uint16,存 EEPROM(带 magic'CL'+ 校验和) - 串口命令:
0x16写表 /0x17读表
最终误差:慢档 ≤ ±1%,常用档 ±2% 以内。
三、让示波器会说"串口话"
原版只有屏幕,没有电脑端。我们做的第二件事是给固件加一条完整的串口数据流, 并配套写了一个 Python 上位机。
帧格式:A5 5A TYPE LENH LENL PAYLOAD SUM
| 类型 | 方向 | 内容 |
|---|---|---|
0x01 | 设备→PC | 采样帧:28 B 头 + 100 × uint16 原始 ADC |
0x02 | 设备→PC | ACK |
0x03 | 设备→PC | 参数块(22 B) |
0x10/0x11/0x12 | PC→设备 | 下发参数 / 存 EEPROM / 读参数 |
0x14 | PC→设备 | 软复位进 ISP(为自动烧录服务) |
0x15 | PC→设备 | 输出测试方波 |
0x16/0x17 | PC→设备 | 写 / 读时基校准表 |
0x19 | 设备→PC | 固件版本 / 参数块长度 / 能力位 |
0x1A/0x1B | 双向 | 读 / 设波形滚动位置 |
0x1C | PC→设备 | 运行 / 暂停 |
采样帧头里塞了:运行标志、时基档、绘图模式、触发方式/沿、VMax/VMin、实测频率、 屏幕电压上下限、ADC 上下限、BGV、分压系数…… 这些让上位机可以精确地把原始 ADC 换算成电压刻度,而不是自己猜。
上位机本身是 Tkinter 写的(py/dso_gui.py):实时波形、FFT、自动测量
(Vpp/Vmax/Vmin/Vrms/Vavg/频率/周期/占空比/上升时间)、双光标 Δt/ΔV、
CSV/PNG 导出、无硬件时的 --demo、协议自检 --selftest。
四、自动烧录:让固件自己进 ISP
STC8 烧录需要"冷启动"进 ISP bootloader。反复拔插电源太麻烦,于是:
上位机发 0x14 → 固件回 ACK → 延时约 1 秒 → IAP_CONTR = 0x60(软复位进 ISP)
→ stcgal 立刻接上烧录。配合 py/flash.py,一次点击就能"改代码→烧录"。
期间踩过的坑:这块板的 DTR/RTS 并不能复位 MCU,所以必须靠固件自己跳。 另外 stcgal 成功时也可能返回非零退出码,脚本里改成解析
Disconnected!来判断。
五、功能扩展(从"能看"到"好用")
1. 可调信号源
- P1.1 方波:15 档频率(约 1 Hz ~ 42 kHz)+ 占空比 1~99 %
- P1.0 波形:PCA 8-bit PWM 输出 正弦 / 三角 / 锯齿,外部 RC 滤波后就是模拟波形
这里有个有意思的细节:最初周期表是"抄"出来的经验值,后来越补越乱。
最后改成运行期计算——所有频率都从一个常量 GEN_TICK_US10
(1 tick = 12.0 µs,实测值)算出来,同时顺手修掉了一个占空比被 uint8 截断的老 bug
(低频时占空比完全不对)。
2. 中英双语与中文点阵
设备只有 128×64 点阵屏,没有现成中文字库。我们的做法:
- 用 Pillow + 宋体渲染 12×12 像素汉字
- 生成
cnfont.h(字模 + 字符串索引表) - 加一个"按像素精确贴字"的绘制函数
第 3 步是重点:原来的 OLED_DrawBMP() 只支持 8 像素对齐的起点,
而行距是 12 像素时汉字会被切边、错位。我们写了一个
OLED_DrawGlyph12(),把 12×12 字形按位搬到任意 y(跨 2~3 个页),
并且支持"反色模式"——设置页的白色高亮条就是这样来的。
3. 双光标测量(设备端)
在设备上新增一页光标测量:两条竖直光标、可切换 A/B、可左右移动,
底部直接显示 dT / dV。这一页和上位机的光标功能是各自独立的实现,
但读的是同一套采样数据。
4. 开机自检 + 启动页
上电先显示 LOGO + C.L./W.S.&D.S,底部一行自检结果:
EEP:OK CAL:OK 1.18V —— 分别检查 EEPROM 是否有效、校准表是否合法、电池电压是否正常。
5. 设备端校准表页面
以前校准只能靠电脑。现在设置里多了「校准表」一页,可以直接在设备上:
- 按住 + 旋转:选择要调的档位(0~20)
- 旋转:微调该档校准值(慢转 ±1,快转 ±8)
- 长按:立刻写 EEPROM 并提示保存成功
六、交互重构:从"中断里忙等"到状态机
原版固件的按键处理全在 INT1 中断里:
Delay50ms(); // 消抖
if (Delay800ms_Long_Press()) // 判断长按
if (Delay300ms_Double_Click())// 判断双击
一次按键最多阻塞 1.15 秒,期间 72 kHz 的波形输出中断、编码器采样、串口全部停摆。 更糟的是,这套逻辑让按键行为变得不可预测。
我们把它重构成:
- INT1 只记录"按下时刻"(外加一个
ADCInterrupt=1打断当前扫描) - Timer0 每 1 ms 跑一次
Key_Tick(),负责计时、消抖、判定短按/长按/双击 - 主循环
Key_Poll()只负责执行排队的动作
改完以后:长按不再打断信号源输出;所有"看起来卡死"的界面里也插入了 Key_Poll(),
不会再出现某个页面按键失灵。
顺手修掉一个隐蔽 bug:机械按键按下时触点会在 A/B 线上抖动, 之前每次抖动都被当成"按住旋转",导致正常的短按被丢弃——于是有了"时好时坏"的手感。 现在只有真正转出一格才算"按住旋转"。
七、暂停与滚动:一个被反复推翻的设计
“波形滚动"这个功能我们推翻重做了三次:
- 原版:双击切换全局滚动模式,旋转 = 左右滚波形
- 我们第一版:把双击去掉(和 Run/Stop 冲突),改成设置页里的开关
- 最终版:滚动只在暂停时有意义,所以做成"暂停时多出来的第 6 个选中项”: 按住旋钮把光标切到左下角那条"触发位置标尺",标尺整条反色, 此时旋转就是左右滚动;切到别的项就恢复调参数
配套地:
- 暂停时按一下 = 继续(恢复运行不清屏,先保留旧波形,等新扫描完成再刷新)
- 上位机暂停按钮会同时暂停单片机(
0x1C),并且在波形下方出现一条 与设备实时同步的滚动条(0x1A读 /0x1B写)
这里也踩了个坑:上位机一开始每 250 ms 轮询一次滚动位置, 结果把采样帧挤掉了——30 ms 以上的时基看起来"画面完全不动"。 后来改成只在暂停时轮询,问题消失。
八、工程化的坑
1. 8051 的 128 字节
功能越加越多,某次编译突然链接失败:
?ASlink-Error-Could not get 66 consecutive bytes in internal RAM for area DSEG
8051 的直接寻址 RAM 只有 128 B,而我们把它塞满了。
最后发现解法不是"把全局变量搬进 xdata"(试过,没用),
而是给整个工程加 --stack-auto——让函数局部变量走堆栈而不是固定 overlay 区。
加上之后链接一次通过,还给栈多挤出 80 字节。
2. 编译脚本与版本号
原仓库的 Makefile 在 Windows 上跑不起来,我们写了 build.ps1:
一条命令编译 src/*.c + user/main.c、打包库、链接,并输出内存占用。
固件里加了 FW_VER_MAJOR/MINOR/PATCH 和 0x19 查询命令,
上位机连接时会自动读出来显示在状态栏——排查问题时非常有用
(“你现在跑的到底是哪版固件”)。
3. 编码
所有 C 源文件是 GBK,中文注释在 UTF-8 编辑器里会乱码;
cnfont.h 是生成文件,改字模要改生成脚本,不能手改。这些都写进了 README。
4. 看门狗:加了又删
我们试过加软件看门狗(2 秒超时,超时 IAP_CONTR=0x20 软复位)。
结果在 500 ms/档时误复位——慢时基跑一轮就要 4.4 秒,
触发等待、长扫描这些状态在软件看来和"卡死"无法区分。
权衡之后删掉看门狗,换来的是长时基下不会莫名重启。
九、现在的样子
| 项 | 数值 |
|---|---|
| 固件版本 | v1.4.1 |
| ROM | 38,693 / 64,512 B(60%) |
| XRAM | 3,558 / 8,192 B |
| 栈剩余 | 217 B |
| 时基 | 21 档,500 ms/div ~ 80 µs/div |
| 串口命令 | 0x01 ~ 0x1C |
| 上位机 | py/dso_gui.py(Tkinter + pyserial + pillow) |
十、还能做什么
- 双通道:硬件上 ADC CH1 是现成的,固件目前只用一个通道
- 设备端 FFT:RAM 和算力都很紧,需要定点 + 抽取
- 真正的硬件看门狗:需要给所有长等待点(ADC、EEPROM 写、长扫描)都设计喂狗点
- 绝对时间基准:现在的时基是"自洽"校准的,绝对精度还缺一个外部参考
- 预设保存:2~3 组常用设置一键切换
致谢
- 原作:CreativeLau/Mini-DSO(MIT)
- 56DZ 版硬件适配:Elektor reverse project
- 本移植与增强:WuSi & DeepSeek — WuSiTech/mini-dso
本文由项目记录整理,代码与文档都在仓库里,欢迎提 issue / PR。
