做那台 VFD 荧光管 8 位计算机的时候,功能一路加得挺顺,直到某次编译,Keil 直接不让过了:

*** RESTRICTED VERSION WITH 0800H BYTE CODE SIZE LIMIT;
    CODE SIZE: 0A76H BYTES

这段报错的每一个字都在说人话,但第一次见的人基本都会先去改源码找语法错误 —— 那里什么都没有。

先看懂那条报错

报错里的词含义
RESTRICTED VERSION你用的是评估版 / 限制版,不是正版授权
0800H BYTE代码段上限 2048 字节(0x0800 = 2048)
CODE SIZE: 0A76H你的程序实际 2678 字节(0x0A76 = 2678)

算一下:2678 − 2048 = 630 字节超标,超出上限 31%。这个数字很重要,因为后面所有瘦身手段都要围着它算账:你得把 2678 压到 2048 以下,缺口是 630 字节,不是「稍微优化一下」能糊过去的量。

报错出现在链接阶段(BL51/LX51 那一步),不是编译阶段。所以你在源码里怎么找都找不到问题,这不是语法错,是体积错。

0800H 到底限制了什么

这一点很多人搞错,直接影响你该往哪个方向省:2KB 限制只管 CODE 段,不管你 RAM 里的数据。

所以「我把大数组搬到 xdata 里去」这种操作对解除 2KB 限制没有用(除非它是带初值的数组 —— 那种初值表是存在 code 区的,搬走才有效)。真正要砍的是被链接进来的函数和常量表。

只有三条路,先选路再动手

路线成本适合谁
① 拿授权钱项目要交付、要长期做,或者有学校/公司授权可用
② 抠体积时间学习项目、一次性的小东西,代码本身也不大
③ 换工具链改代码代码量注定超 2KB 很多,又不想花钱

我三条都走过一遍,顺序建议是:先估缺口。缺口在 500 字节以内,走 ②;缺口超过一倍,直接考虑 ① 或 ③。我这次缺 630 字节,属于「②能干但很累」的区间。

路一:拿到合法授权

先说清楚一件事:评估版的 2KB 限制没有任何设置项能关掉。网上那些「改注册表 / 换 license.dat」的教程,说到底就是找破解,我不建议碰,理由不是道德洁癖:

正规途径就几条:Keil(现属 Arm)官网买 PK51 / CA51 授权;学校的实验室授权;国内代理商的正版渠道。价格会随版本和授权类型变,自己问代理商,别信贴吧的旧报价。

学生党还有一种情况:学校机房装的可能是正版,但你手上没有。那就去机房编译,或者干脆用下面的第三条路。

路二:按收益排序的瘦身清单

下面这些我按「收益 / 工作量」从高到低排。不要跳着做,从上往下一条条试,每改一条重新编译看体积变化,这样你才知道到底是哪个改动救了你。

1. 存储模式改 SMALL(收益最大的一刀)

Options for Target → Target → Memory Model,从 Large 改成 Small。

为什么这一刀最狠:指针宽度变了。small 模式下指针是 1 字节(只指向片内 128 字节 data),large 模式下是 3 字节(段 + 16 位偏移)。你代码里每一次指针传递、每一个数组下标、每一个函数参数列表,都会跟着缩水。

⚠️ 代价:small 模式下所有变量默认挤进片内 128 字节(idata 够大就有 256),大数组会直接撑爆 data 区,链接报 DATA SEGMENT TOO LARGE。所以这一步通常要配合手动给大数组加 xdata 修饰。

// 改 small 之后,大缓冲要自己标 xdata 挪出去
unsigned char xdata vfd_buf[64];
unsigned int  xdata adc_samples[32];

2. 优化等级 + Favor size

Options for Target → C51 页,Level 拉满(8),Emphasis 选 Favor size。命令行等效:

OPTIMIZE(8, SIZE)

默认强调速度,生成的代码会长一些。改成体积优先,一般能省下可观的一截,代价是执行变慢 —— 对你那个 830µs 的显示时隙来说,只要 ISR 里没有复杂运算,完全扛得住。

3. 干掉浮点(第二大的一刀)

8051 没有 FPU,所有浮点运算都是软件模拟。你写一句 float v = adc * 5.0 / 1024.0;,链接器就会把浮点加减乘除、格式转换一整套库拖进来,轻轻松松吃掉几百字节到 1KB 以上。

// ❌ 引入浮点库
float v = adc * 5.0f / 1024.0f;

// ✅ 定点:单位换成 mV,全程整数
unsigned int mv = (unsigned long)adc * 5000UL / 1024UL;

显示的时候也不需要浮点:你要显示「2.44V」,就用 mv/1000 和 mv%1000 分别取整数位和小数位,中间补个点号而已。

4. 干掉 printf / sprintf

格式化引擎是另一个体积黑洞,为了打一行调试信息把整个 printf 拉进来,非常不值。自己写个最小实现只要几十字节:

void u16_to_str(unsigned int v, char *out)
{
    char t[6];
    unsigned char i = 0, j = 0;
    if (v == 0) { out[0] = '0'; out[1] = '\0'; return; }
    while (v) { t[i++] = (char)('0' + v % 10); v /= 10; }
    while (i) out[j++] = t[--i];
    out[j] = '\0';
}

注意 % 和 / 本身也是库例程(16 位除法 ?C?UIDIV、32 位 ?C?ULDIV,各占几百字节)。上面这行 v /= 10 会拉进 16 位除法库 —— 但如果别处已经用了除法,这个库本来就在,不额外收费。真正要避免的是「只为一处运算把 32 位除法库拉进来」。

5. 常量表确定放 code 区

段码表、字库、字符串,一定要加 code 修饰,否则默认落在 RAM 里(RAM 不够时链接器会挪走,但初值表仍然占 code 区,等于双份)。

// ❌ 占 RAM,初值表还占 code
unsigned char seg_table[10] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};

// ✅ 只占 code,不占 RAM
unsigned char code seg_table[10] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};

6. 能算出来的表就别存

七段码里 a~g 各段是固定的位模式,与其存 10 字节表,不如一次位运算算出来。这条收益不大,但在临界点上刚好能救你一命:

// 只对「段顺序规整」的接法有效,接得乱就用表
unsigned char seg_of(unsigned char d) {
    static const unsigned char s[10] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};
    return s[d];
}

说实话:这段没省,反倒多一层函数调用。我想表达的是先量再优化 —— 一条看着聪明的优化,可能因为它自己引入的代码反而更费。这也是下一节要讲 .M51 的原因。

7. 别用 reentrant 函数

Keil C51 默认函数不可重入,参数和局部变量放在固定的覆盖区,很省。reentrant 关键字会给每个调用建栈帧,体积和执行开销都上去。除非真的在中断和主循环里同时调同一个函数,否则不要加。

8. 中断里只做最必要的事

中断服务程序里塞太多逻辑,除了拖垮节拍,还会让编译器没法把这段代码和其它函数共享覆盖区(因为 ISR 是随时可能进来的)。把运算挪回主循环,用标志位传数据,通常是既省代码又稳的双赢。

动手之前先看 .M51,别瞎改

这是我最想给的一个建议。Keil 链接完会在输出目录生成一个 .M51 文件(Memory Map),里面有每一段的位置和长度。打开它,搜 CODE 段明细,你就能看到是谁把 2678 字节吃掉的,而不是凭感觉猜。

LINK MAP OF MODULE:  ...
TYPE    BASE      LENGTH    RELOCATION   SEGMENT NAME
CODE    0000H     00D3H     ABSOLUTE     ?C_STARTUP
CODE    0100H     0416H     UNIT         ?PR?MAIN?MAIN
CODE    0516H     0233H     UNIT         ?PR?T0_ISR?MAIN
CODE    ...       ...       ...          ?C?FPADD      ← 浮点库,几百字节
CODE    ...       ...       ...          ?C?ULDIV      ← 32 位除法

看到 ?C?FPADD / ?C?ULDIV / PRINTF 这几类名字出现,基本就定案了 —— 那不是你写的逻辑占地方,是你顺手用的库占地方。

方法比技巧重要:改一条 → 重编译 → 看 .M51 里 CODE 总长变了多少。三轮之后你就知道这个工程的钱花在哪了。

路三:换 SDCC,语法对照表

SDCC 是开源的 8051 编译器,没有 2KB 限制,也没有代码段上限。代价是它不吃 Keil 的扩展语法,得改一遍。常用对照:

用途Keil C51SDCC
中断函数void t0() interrupt 1 using 1void t0() __interrupt(1) __using(1)
程序区常量code__code
片外数据xdata__xdata
位变量bit__bit
位寻址 SFRsbit LED = P1^0;__sbit __at (0x90) LED;
绝对地址xdata unsigned char buf _at_ 0x1000;__xdata __at (0x1000) unsigned char buf;
输出格式直接出 .hex先出 .ihx,再 packihx 转 .hex

几个实操提醒:

几个我踩过的坑


我的结论是:2KB 限制不是技术难题,是一道选择题 —— 花钱、花时间、还是花一次重构。想清楚缺口有多大,再挑一条走,比东改一句西改一句省得多。

这台机器的完整制作过程写在 自制 8 位计算机:CH6800 + VFD 荧光管显示的折腾记录,视频在 B站搜 codevtool。