VFD 荧光管是那种「第一眼惊艳、第二周想砸」的显示器:辉光确实漂亮,但用 51 单片机直驱,十个人有九个会遇到同一个问题——闪。不是那种明显的狂闪,是凑近看才发现的呼吸感、扫描纹,暗环境下尤其明显。我用 CH6800 带一块 VFD 模块调了很久,最后沉淀出一套稳定基线,这篇文章把参数、代码和取舍完整写出来。
VFD 为什么比数码管更容易闪
先懂原理,才知道该调什么。VFD 的显示结构和数码管类似:灯丝加热阴极发射电子,栅极控制哪一位亮,阳极控制这一位里哪几段亮。多位显示时只能一位一位来:第 1 位点亮一小会儿,切到第 2 位,再切到第 3 位……靠人眼视觉暂留把「轮流亮」看成「同时亮」。这就是扫描显示。
关键差别在余晖:VFD 的荧光粉余晖比 LED 短得多,「熄灭期」占的比例稍大,肉眼就能看出断续。LED 数码管你 60Hz 刷新可能也凑合,VFD 想看不出来,帧率得往 100Hz 走。同时 VFD 是真空器件,驱动电压高(栅阳极几十伏),大多要配高压驱动芯片,切换有建立时间,时序不干净还会出残影——又是「闪」的另一种形态。
结论先放这:VFD 扫描显示的帧率目标不是「能用」,是 100Hz。整个基线设计都是围着这个数转的。
闪烁的三个根因,逐个对账
| 根因 | 现场表现 | 机制 |
|---|---|---|
| 帧率不够 | 整屏轻微呼吸感,暗环境明显 | 每帧周期太长,视觉暂留补不上熄灭期 |
| 扫描被阻塞 | 时快时慢地闪,伴随功能卡顿 | 主循环某个长任务把扫描时序拖歪,时隙忽长忽短 |
| 中断里干活太多 | 稳定地闪,且主程序越忙闪得越狠 | 定时器中断执行太久,主循环被挤占,缓冲更新跟不上 |
大部分人的代码死在第二种:把「取显示数据 + 查表 + 输出」全放在定时器中断里,数据一多中断执行时间就不可控;或者反过来,主循环里直接驱动扫描,循环里一次 delay 就把时序拖垮。这两个坑的共同解法是下面这套基线。
稳定基线:830µs 时隙 × 12 路扫描 = 100Hz
我最后稳定的参数(CH6800 / HC6800,11.0592MHz 晶振,Keil 限制版亲测能跑):
| 参数 | 值 | 说明 |
|---|---|---|
| 定时器 | T0 模式 1(16 位) | 11.0592M / 12 = 921600Hz,一个机器周期 ≈ 1.085µs |
| 单时隙 | 830µs | 921600 × 0.00083 ≈ 765 个计数 |
| 扫描路数 | 12 位 | 12 位一帧,覆盖常用 VFD 模块 |
| 帧周期 | 830µs × 12 ≈ 9.96ms | ≈ 100.4Hz,压过肉眼可辨下限 |
| 中断预算 | 每次中断 < 30µs | 只查表输出,占比 3.6%,主循环有充足时间 |
定时器初值算出来是 65536 − 765 = 64771 = 0xFCE3,即 TH0 = 0xFC; TL0 = 0xE3;。这个 830µs 不是拍的——再短,高压驱动芯片建立时间不够,出残影;再长,12 路下来帧率掉出 100Hz。12 路 × 830µs 刚好卡在两头的最优点上。
代码骨架:中断只翻缓冲区,双缓冲防撕裂
核心原则一句话:中断里只做查表和输出,主循环只改缓冲,两者永不相见。显示内容更新用双缓冲,避免画面更新到一半被扫描读到的撕裂感:
// vfd_scan.c —— 100Hz 扫描基线
#define T0_H 0xFC
#define T0_L 0xE3 // 830us @ 11.0592M/12
unsigned char idata disp_buf[12]; // 扫描读的缓冲(中断侧)
unsigned char idata shadow[12]; // 主循环写的缓冲(应用侧)
unsigned char slot = 0;
void t0_isr(void) interrupt 1 {
TR0 = 0; // 重装期间停表,防累计误差
TH0 = T0_H;
TL0 = T0_L;
TR0 = 1;
// 只做三件事:取这一位的段码、选这一位、灭一下消残影
SEG = code SEG_TAB[disp_buf[slot]];
GRID = code GRID_TAB[slot];
BLANK = 1; // 切位瞬间全灭,防串影
BLANK = 0;
slot = (slot + 1) % 12;
}
// 主循环:任何地方都直接写 shadow,不碰 disp_buf
void show_char(unsigned char pos, unsigned char ch) {
shadow[pos] = ch;
}
// 帧边界一次性交换:在 slot==0 的那一瞬间拷贝
void commit(void) {
unsigned char i;
for (i = 0; i < 12; i++) disp_buf[i] = shadow[i];
}
几个细节都是踩出来的:
- 重装定时器要停表:TH0/TL0 重装那几个周期不停表,一帧积累几个周期误差,几十帧之后就看得出来了。
- 切位瞬间全灭:
BLANK那两行是消残影的关键,高压驱动切换有建立时间,不遮一下,前一位的段会「拖」到后一位上。 - SEG_TAB / GRID_TAB 放 code 区:查表放 ROM,不占宝贵的 RAM,中断里取指也最快。
- commit 的时机:主循环改完 shadow 后,等到
slot == 0再整体拷贝,扫描永远读不到半新半旧的一帧。
Keil 0800H 超限下,怎么保显示的命
用 Keil 评估版的人都懂那个痛:代码上限 0800H(2KB),功能一多就爆。我的项目最后到了 0A76H,超了 130%,详细瘦身过程写在《Keil C51 评估版 2KB 代码限制怎么破》那篇里。这里只强调一条排序原则:
代码超限时,先砍功能,后砍显示。显示中断是整套系统里唯一「用户直接看见」的部分,它抖一下全家都像坏的;而功能可以慢慢加、分期上。
具体做法:显示驱动(中断 + 两张表 + 双缓冲)控制在 600 字节以内,用 small 模式编译减少栈开销,reentrant 只给必须的函数加。业务逻辑超限了,把 sprintf 换成自己写的十六进制转换,把printf 家族整个剔掉——这两个在 8051 上是体积大户,剔掉通常能省 300~500 字节,足够再撑一个功能。
怎么证明真的不闪了:示波器与 240fps 慢动作
调显示最怕「我觉得不闪了」。两个客观验证手段,都不用猜:
- 示波器量帧周期:探头搭在栅极驱动上,测相邻两帧同一位的间隔,
9.96ms ± 0.1ms才算稳。超过这个带宽说明主循环有长任务在挤中断,回去查。 - 手机 240fps 慢动作:暗光环境拍屏幕,慢放。100Hz 稳定扫描在慢动作下是均匀的逐位点亮,有任何一路忽明忽暗、节奏跳变,当场现形。这个办法零成本,推荐先用它粗筛。
这套基线后来也用在自制 8 位计算机上做状态显示,寄存器、总线、时钟分频的实时值全跑在同一个扫描框架里,加了功能没动显示一行代码——双缓冲的价值就在这:应用长多大,显示都不用重写。