第八周新旧模板对比

旧版底层 vs 新版底层

一、核心差异总览(一眼定乾坤)

对比维度 旧版底层 新版底层 为什么要改
初始化写法 直接写 P2 = P2 & 0x1f | 0x80,一行搞定 temp 临时变量分三步操作,注释详细 旧版是“一步到位”但可读性差,新版是“分步操作”更安全,防止P2口其他位被意外修改
LED控制 Led_Disp(addr, enable) 控制单个LED Led_Disp(unsigned char *ucLed) 控制8个LED数组 新版支持批量控制,一次调用刷新全部LED,效率更高
按键扫描 简单粗暴,直接读P3口 增加了串口冲突检测 + 组合键检测 P3.0同时是串口RXD引脚,新版防止把串口数据误判成按键,这是大坑!
蜂鸣器/继电器 代码被注释掉了,没启用 独立成函数 Beep()Relay()Motor()状态缓存防重复刷新 旧版“留了坑没填”,新版填好了直接用
数码管 段码表在RAM里,没优化 pdata 关键字存段码表,消隐步骤更严谨 新版显示更稳定,减少鬼影

二、硬件初始化篇——从“黑盒”到“说明书”

:yellow_circle: 旧版 Init.c:一行流,简洁但吓人

void System_Init()
{
    // LED
    P0 = 0xff;
    P2 = P2 & 0x1f | 0x80;   // ← 这一行小白直接懵
    P2 &= 0X1f;
    // 蜂鸣器、继电器
    P0 = 0x00;
    P2 = P2 & 0x1f | 0xa0;   // ← 为什么是0xa0?
    P2 &= 0X1f;
}

问题

  • P2 = P2 & 0x1f | 0x80 这行代码,没有注释,小白看到 0x1f0x80 直接原地去世。
  • 没有用临时变量,直接操作P2口,容易把P2口的低5位状态搞乱(虽然实际影响不大,但代码风格不严谨)。

:green_circle: 新版 init.c:分步走,注释拉满

void System_Init()
{
    unsigned char temp;  // ← 多了个临时变量

    // 关闭所有LED
    P0 = 0xff;
    temp = P2 & 0x1f;    // 1. 先读出P2低5位
    temp = temp | 0x80;  // 2. 再设置高3位为100(选中Y4)
    P2 = temp;           // 3. 最后写回去
    temp = P2 & 0x1f;    // 4. 取消选中,锁存数据
    P2 = temp;

    // 关闭继电器、蜂鸣器(同理,用0xa0选中Y5)
    P0 = 0x00;
    temp = P2 & 0x1f;
    temp = temp | 0xa0;
    P2 = temp;
    temp = P2 & 0x1f;
    P2 = temp;
}

比喻

  • 旧版:像蒙着眼睛换灯泡——手伸进去一通操作,亮了就行,但不知道摸到了什么。
  • 新版:像按操作手册换灯泡——先断电、再拆旧、再装新、再通电,每一步都有记录。

:light_bulb: 为什么要改?

旧版问题 新版改进
直接修改P2口,代码“一步到位”但不可读 temp 分步操作,逻辑清晰,一看就懂
注释基本没有 每一行都有注释,连为什么要 &0x1f 都解释了
新手容易把 0x800xa0 记反,导致LED和蜂鸣器串扰 注释里明确写了 Y4控制LED,Y5控制蜂鸣器/继电器

三、按键扫描篇——从“莽夫”到“带脑子扫描”

:yellow_circle: 旧版 Key.c:四个按键的简单扫描

unsigned char Key_Read()
{
    unsigned char temp = 0;
    P44 = 0; P42 = 1; P35 = 1; P34 = 1;
    if(P33 == 0) temp = 4;
    if(P32 == 0) temp = 5;
    if(P31 == 0) temp = 6;
    if(P30 == 0) temp = 7;
    // ... 后面还有三行扫描
    return temp;
}

问题

  • 完全没有防抖,按键按下去可能读到乱码。
  • 没有串口冲突检测——P3.0是串口RXD引脚,如果串口正在接收数据,P3.0被拉低,会被误判成按键S7按下

:green_circle: 新版 key.c:加了“智能检测”

unsigned char Key_Read()
{
    unsigned char temp=0;

    // 扫描第一行
    P44=1;P42=1;P35=1;  // 先全部置高

    // *** 关键:串口冲突检测 ***
    if(P30==0) return 0;  // ← P3.0被拉低?说明串口在忙,直接退出!

    P44=0;P42=1;P35=1;
    if(P30==0) temp=7;
    if(P31==0) temp=6;
    if(P32==0) temp=5;
    if(P33==0) temp=4;

    // ... 后续行扫描同理

    // 特殊功能:组合键检测
    if((P32==0)&&(P33==0)) return 89;  // ← S9和S8同时按下返回89

    return temp;
}

比喻

  • 旧版:像超市收银员——不管顾客有没有准备好,上来就扫码,扫错也不管。
  • 新版:像机场安检——先确认通道没人(串口不忙),再放人通过(扫描按键),还支持“双人同时过”(组合键检测)。

:light_bulb: 为什么要改?

旧版问题 新版改进
P3.0和串口共用,串口数据会被当成按键 每次扫描前检查 if(P30==0) return 0;,串口忙就主动退出
没有防抖,按键容易误触 虽然没有软件防抖,但配合定时器扫描(后续在main里处理)可以做到
只返回单一按键值 支持 组合键检测,比如 S9+S8 同时按下返回 89
P3.4被占用用于频率测量,但代码没注释说明 注释明确写了 P3.4用于频率测量,所以第四行扫描被注释掉

四、LED控制篇——从“单个控制”到“批量刷新”

:yellow_circle: 旧版 Led.c:控制单个LED

void Led_Disp(unsigned char addr, unsigned char enable)
{
    static unsigned char temp = 0x00;
    static unsigned char temp_old = 0xff;
    if(enable)
        temp |= 0x01 << addr;   // 点亮第addr个LED
    else
        temp &= ~(0x01 << addr); // 熄灭
    if(temp != temp_old)        // 状态变化才刷新硬件
    {
        P0 = ~temp;
        P2 = P2 & 0x1f | 0x80;
        P2 &= 0x1f;
        temp_old = temp;
    }
}

用法:想控制LED1亮,调用 Led_Disp(0, 1);想控制LED3灭,调用 Led_Disp(2, 0)

问题:每次只能控制一个LED,要同时控制多个LED就得调用多次,效率低。

:green_circle: 新版 led.c:控制8个LED数组

void Led_Disp(unsigned char *ucLed)  // ucLed是8个元素的数组
{
    // 把8个0/1合并成一个字节
    temp_1 = (ucLed[0] << 0) | (ucLed[1] << 1) | ... | (ucLed[7] << 7);

    if (temp_1 != temp_1_old)  // 状态变化才刷新
    {
        P0 = ~temp_1;
        // 选中Y4锁存器...
        temp_1_old = temp_1;
    }
}

用法

unsigned char led_status[8] = {1, 0, 1, 0, 0, 0, 0, 0};  // LED1亮、LED3亮
Led_Disp(led_status);  // 一次调用全部刷新

:light_bulb: 为什么要改?

旧版问题 新版改进
一次只能控制1个LED,控制8个要调8次 一次传入8个状态,一次刷新全部
每个LED状态变化都要操作硬件 数组整体变化才操作硬件,减少IO操作次数
没有“全部关闭”函数 新增 Led_Off() 函数,一键全灭

比喻

  • 旧版:像单个开关——要关8个灯得来回跑8趟。
  • 新版:像总控面板——一拨开关全部搞定。

五、数码管篇——旧版少了什么?

:yellow_circle: 旧版 Seg.c

unsigned char Seg_Dula[] = {0xc0,0xf9,...};  // 段码表
unsigned char Seg_Wela[] = {0x01,0x02,...};  // 位选表

void Seg_Disp(unsigned char wela, dula, point)
{
    P0 = 0xff;              // 消隐
    P2 = P2 & 0x1f | 0xe0;  // 选中段选锁存器
    P2 &= 0x1f;

    P0 = Seg_Wela[wela];    // 位选
    P2 = P2 & 0x1f | 0xc0;
    P2 &= 0x1f;

    P0 = Seg_Dula[dula];    // 段选
    if(point) P0 &= 0x7f;
    P2 = P2 & 0x1f | 0xe0;
    P2 &= 0x1f;
}

问题:段码表存在 RAM 里,没做存储优化。

:green_circle: 新版 seg.c

pdata unsigned char seg_dula[]={0xc0,0xf9,...};  // ← 加了 pdata 关键字

改进

  • pdata 关键字把段码表存到 分页外部RAM,不占用宝贵的内部RAM空间。
  • 消隐步骤更严谨:先消隐、再位选、再段选,减少鬼影。

比喻

  • 旧版:把菜谱放在厨房台面(内部RAM)上,占地方。
  • 新版:把菜谱放在储物柜(外部RAM)里,要用再拿,台面更宽敞。

六、蜂鸣器/继电器篇——从“注释”到“成品”

:green_circle: 新版 led.c

void Beep(bit enable)
{
    if (enable) temp_2 |= 0x40;
    else temp_2 &= (~0x40);
    if (temp_2 != temp_2_old)
    {
        P0 = temp_2;
        // 选中Y5锁存器...
        temp_2_old = temp_2;
    }
}

void Relay(bit enable) { /* 同理,bit 4控制 */ }
void Motor(bit enable) { /* 同理,bit 5控制 */ }

改进

  • 状态缓存机制:只有状态变化才刷新硬件,减少IO操作。

比喻

  • 旧版:给你一套零件
  • 新版:直接给你成品,插电即用。

七、实战避坑指南(针对小白)

:cross_mark: 新手用新底层最容易犯的3个错误

错误1:在 Led_Disp() 里传错数组大小

// ❌ 错误写法
unsigned char led[5] = {1,1,1,1,1};
Led_Disp(led);  // 只传了5个,但函数要读8个!读到垃圾数据!

:white_check_mark: 正确做法:数组必须是8个元素,不够就补0:

unsigned char led[8] = {1,1,1,1,1,0,0,0};  // 前5个亮,后3个灭
Led_Disp(led);

错误2:在按键扫描里加延时防抖

// ❌ 错误写法——把延时写在Key_Read()里
unsigned char Key_Read()
{
    // ...
    if(P30==0) {
        Delay_ms(10);   // ← 千万不能在这里延时!
        temp=7;
    }
    // ...
}

后果:按键扫描通常放在定时器中断主循环快速调用,在里面加延时会导致整个程序卡住,数码管闪烁。

:white_check_mark: 正确做法:在 main.c 里用状态机计数变量做防抖,Key_Read() 只负责读取当前状态,不负责延时。

错误3:数码管显示时忘了“动态扫描”

// ❌ 错误写法——只调用一次Seg_Disp()
void main()
{
    System_Init();
    Seg_Disp(0, 5, 0);  // 只显示第0位为5
    while(1);
}

后果:只有一位数码管亮,其他全灭——因为数码管是动态扫描的,需要定时刷新每一位

:white_check_mark: 正确做法:在定时器中断里轮流刷新8位数码管:

// 定时器中断(1ms一次)
void Timer0_Isr() interrupt 1
{
    static unsigned char pos = 0;
    Seg_Disp(pos, Seg_Buf[pos], 0);  // Seg_Buf里存着要显示的数字
    pos = (pos + 1) % 8;
}

:pushpin: 一句话总结

新底层 = 旧底层的“完全体”

  • :white_check_mark: 解决了 P3.0串口冲突 这个大坑
  • :white_check_mark: LED支持批量控制,效率更高
  • :white_check_mark: 数码管段码表优化存储,省内部RAM

旧版主函数 vs 新版主函数

一、核心差异总览(一眼定乾坤)

对比维度 旧版 main.c 新版 main.c 为什么要改
定时器选择 使用 定时器0(中断号1) 使用 定时器1(中断号3) 定时器0留给更高级的功能(如红外遥控、PWM输入捕获),定时器1专门做系统心跳
中断服务函数命名 Timer0Server非标准命名 Timer1_Isr标准命名 新版命名更规范,一看就知道是定时器1的中断服务函数
LED刷新方式 Led_Disp(Seg_Pos, ucLed[Seg_Pos]) 每次只刷新1个LED Led_Disp(ucLed) 一次刷新全部8个LED 新版批量刷新更高效,而且配合PWM调光
数码管显示逻辑 直接用 Seg_Buf[Seg_Pos] 显示 增加了小数点判断技巧:数字>20时点亮小数点 新版封装了小数点自动处理,更智能
变量存储类型 普通变量(默认在data区) 加了 pdataidata 关键字 新版优化内存分配,把大数组放外部RAM,关键变量放内部RAM
PWM调光功能 没有 ——软件PWM控制LED亮度 国赛常考PWM调光,新版提前把框架搭好了
减速计数器 按键10ms、数码管500ms 按键10ms、数码管20ms 新版数码管刷新更快更流畅,500ms太慢了会闪烁

二、变量声明篇——从“随便放”到“精打细算”

:yellow_circle: 旧版:变量全堆在data区

unsigned char Key_Slow_Down;      // 默认存在内部RAM(data区)
unsigned char Key_Val,Key_Old,Key_Down,Key_Up;
unsigned int Seg_Slow_Down;       // ← unsigned int 占2字节
unsigned char Seg_Pos;
unsigned char Seg_Buf[8] = {...}; // 8字节,占data区
unsigned char Seg_Point[8] = {...}; // 又8字节
unsigned char ucLed[8] = {...};   // 再8字节

问题:STC15的内部RAM(data区)只有128字节,这么多个数组+变量一放,快爆了!而且 Seg_Slow_Downunsigned int 纯粹浪费(计数到500就够了,unsigned char 足矣)。

:green_circle: 新版:变量按需分配存储位置

pdata unsigned char ucLed[8] = {0,...};   // 存到分页外部RAM(pdata区)
pdata unsigned char Seg_Buf[8] = {10,...}; // 存到分页外部RAM
idata unsigned char Seg_Pos = 0;           // 存到内部RAM(idata区),访问速度最快
idata unsigned char Seg_Slow_Down;         // 内部RAM
idata unsigned char Key_Val, Key_Old, Key_Up, Key_Down;
idata unsigned char Key_Slow_Down;
idata unsigned int AD_1_Data_100x;         // AD值确实需要unsigned int

比喻

  • 旧版:把所有东西都塞进随身口袋(data区),口袋快撑破了。
  • 新版:把大件行李托运舱(pdata区),随身只带贵重物品(idata区),空间分配合理。

:light_bulb: 存储类型速查表(小白必存)

关键字 存储位置 容量 访问速度 适合放什么
data(默认) 内部RAM低128字节 128B 最快 频繁使用的变量(循环变量、标志位)
idata 内部RAM 256字节 256B 需要快速访问的变量
pdata 外部RAM分页(一页256B) 256B 较慢 大数组(LED状态、数码管缓冲)
xdata 外部RAM 全部 最大 最慢 超大数组(不常用)

三、定时器篇——从“定时器0”到“定时器1”

:yellow_circle: 旧版:定时器0做系统心跳

void Timer0_Init(void)  // 1毫秒@12.000MHz
{
    AUXR &= 0x7F;   // 12T模式
    TMOD &= 0xF0;   // 模式1(16位定时器)
    TL0 = 0x18; TH0 = 0xFC;
    TR0 = 1;        // 启动定时器0
    ET0 = 1;        // 使能定时器0中断
    EA = 1;
}

void Timer0Server() interrupt 1  // ← 中断号1
{
    // 刷新数码管、LED
}

:green_circle: 新版:定时器1做系统心跳

void Timer1_Init(void)  // 1毫秒@12.000MHz
{
    AUXR &= 0xBF;   // 12T模式(注意:0xBF清的是T1的位)
    TMOD &= 0x0F;   // 清空T1的模式位
    TL1 = 0x18; TH1 = 0xFC;
    TR1 = 1;        // 启动定时器1
    ET1 = 1;        // 使能定时器1中断
    EA = 1;
}

void Timer1_Isr(void) interrupt 3  // ← 中断号3
{
    // 刷新数码管、LED、PWM
}

:light_bulb: 为什么要从定时器0换到定时器1?

原因 解释
预留定时器0 定时器0可以用于红外遥控解码频率测量PWM输入捕获等高级功能,这些功能在国赛省赛里经常考。把定时器0空出来,以后扩展更方便
中断优先级管理 定时器1的中断优先级默认比定时器0低,但系统心跳不需要最高优先级,把高优先级留给更紧急的中断(如串口接收)
代码规范 Timer1_Isr 这个命名一看就知道是定时器1的中断,而 Timer0Server 命名不规范(Server是服务端的意思,跟中断没关系)

比喻

  • 旧版:让总经理(定时器0)每天打卡签到(做系统心跳),大材小用。
  • 新版:让前台(定时器1)负责打卡签到,总经理(定时器0)留着处理紧急事务(频率测量/红外解码)。

四、中断服务函数篇——从“大杂烩”到“精工细作”

:yellow_circle: 旧版中断:什么活都干

void Timer0Server() interrupt 1
{
    if(++Key_Slow_Down == 10) Key_Slow_Down = 0;      // 按键减速
    if(++Seg_Slow_Down == 500) Seg_Slow_Down = 0;      // 数码管减速(500ms!太慢了)
    if(++Seg_Pos == 8) Seg_Pos = 0;                    // 位置循环
    Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], Seg_Point[Seg_Pos]); // 刷新数码管
    Led_Disp(Seg_Pos, ucLed[Seg_Pos]);                 // 刷新单个LED
}

问题

  • Seg_Slow_Down 计数到500才清零——也就是每500ms才处理一次数码管显示数据,但数码管刷新是每1ms都在刷新的!这意味着你改了 Seg_Buf 里的数据,最多要等500ms才能看到变化
  • Led_Disp(Seg_Pos, ucLed[Seg_Pos]) 每次只刷新1个LED,要8ms才能刷完一轮,PWM调光?不存在的

:green_circle: 新版中断:分工明确,功能丰富

void Timer1_Isr(void) interrupt 3
{
    // 1. 减速计数器自增(只做++,不判断清零,由各Proc函数判断)
    Seg_Slow_Down++;
    Key_Slow_Down++;

    // 2. 数码管动态扫描(每1ms刷一位)
    Seg_Pos = (++Seg_Pos) % 8;  // 更简洁的写法
    if (Seg_Buf[Seg_Pos] > 20)
        Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos] - ',', 1);  // 显示数字+小数点
    else
        Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], 0);

    // 3. LED软件PWM调光(10级亮度)
    pwm_period = (++pwm_period) % 10;
    if (pwm_period < pwm_compare)
        Led_Disp(ucLed);   // 点亮
    else
        Led_Off();         // 熄灭
}

改进点

改进 解释
减速计数器只++不判断 把“是否达到阈值”的判断放到 Key_Proc()Seg_Proc() 里,逻辑更清晰
数码管刷新极快 每1ms刷新一位,8ms一轮,显示丝滑无闪烁
小数点自动处理 Seg_Buf[Seg_Pos] > 20 这个技巧很骚——存数字时加个逗号的ASCII码(44),用来标记“这个位要点亮小数点”
软件PWM调光 在中断里做10级PWM,LED亮度可调,国赛必考

:fire: 新版的“小数点技巧”详解(小白必看)

// 在 Seg_Proc() 里设置数据时:
Seg_Buf[0] = 5;      // 正常显示数字5,不带小数点
Seg_Buf[1] = 5 + 44;  // 显示数字5,带小数点(44是逗号','的ASCII码)

// 在中断里显示时:
if (Seg_Buf[Seg_Pos] > 20)   // 如果大于20,说明这个数字被加了44
    Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos] - ',', 1);  // 减掉44得到真实数字,点亮小数点
else
    Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], 0);        // 正常显示

比喻:就像在快递盒子上贴个红色标签(加44),分拣员(中断)看到红色标签就知道这单要加急(点亮小数点)——不用额外开个数组记录小数点状态,省内存又高效

五、主循环篇——结构几乎一样,但细节有差

:yellow_circle: 旧版 main():

void main()
{
    System_Init();
    Timer0_Init();
    while(1)
    {
        Key_Proc();
        Seg_Proc();
        Led_Proc();
    }
}

:green_circle: 新版 main():

void main()
{
    System_Init();
    Timer1_Init();    // 换成定时器1
    while(1)
    {
        Key_Proc();
        Seg_Proc();
        Led_Proc();
    }
}

结构上一模一样,只是定时器换成了1号。这说明新版完全向下兼容,你以前写的业务逻辑(Key_Proc、Seg_Proc)不用改,只是底层驱动更强大了。

六、减速机制篇——从“中断判断”到“函数判断”

:yellow_circle: 旧版:在中断里判断减速阈值

// 中断里
if(++Key_Slow_Down == 10) Key_Slow_Down = 0;

// 主循环里
void Key_Proc()
{
    if(Key_Slow_Down) return;  // 不为0就退出
    Key_Slow_Down = 1;         // 手动置1
    // ... 执行按键逻辑
}

流程:中断计数到10→清零→主循环检测到0→执行→手动置1→中断继续计数…

:green_circle: 新版:中断只负责++,主循环判断阈值

// 中断里
Key_Slow_Down++;   // 只++,不清零

// 主循环里
void Key_Proc()
{
    if (Key_Slow_Down < 10) return;  // 没到10ms就退出
    Key_Slow_Down = 0;               // 到了10ms,清零重新计数
    // ... 执行按键逻辑
}

比喻

  • 旧版闹钟响了(中断清零)→ 你醒来干活(执行Key_Proc)→ 干完关灯继续睡(手动置1)——流程复杂,容易搞混。
  • 新版你看着秒表(Key_Slow_Down++)→ 到了10秒就干活(判断>=10)→ 干完秒表归零(=0)——逻辑清晰,一眼就懂。

七、PWM调光篇(新版独有)

新版在中断里加了一段软件PWM代码,这是旧版完全没有的:

// PWM周期10ms,占空比由 pwm_compare 控制(0-9)
pwm_period = (++pwm_period) % 10;
if (pwm_period < pwm_compare)
    Led_Disp(ucLed);   // 亮
else
    Led_Off();         // 灭

原理

  • pwm_period 从0数到9,一共10ms一个周期。
  • pwm_compare 是亮度阈值,范围0-9。
  • 比如 pwm_compare = 6:前6ms亮,后4ms灭,亮度60%
  • 改变 pwm_compare 就能调光——国赛经常考“呼吸灯”或“PWM调光”。

比喻:就像开关灯——1秒内开0.6秒、关0.4秒,人眼看到的就是60%亮度

八、实战避坑指南(针对小白)

:cross_mark: 用新模板最容易犯的3个错误

错误1:Seg_Buf里直接放大于20的数,忘记减44

// ❌ 错误写法
Seg_Buf[0] = 44;  // 想显示数字0带小数点,但44 = ',' 逗号!

:white_check_mark: 正确写法

Seg_Buf[0] = 0 + 44;   // 显示0带小数点
Seg_Buf[1] = 5 + 44;   // 显示5带小数点
Seg_Buf[2] = 10;       // 显示全灭(10是'熄灭',不是小数点)

错误2:在Led_Proc里调用Led_Disp,跟中断抢资源

// ❌ 错误写法
void Led_Proc()
{
    ucLed[0] = 1;
    Led_Disp(ucLed);   // ← 中断里已经在调用Led_Disp和Led_Off了,你再调一次会打架!
}

后果:LED显示乱闪,PWM调光失效。

:white_check_mark: 正确做法Led_Proc() 只修改 ucLed[] 数组,不调用Led_Disp,让中断自动刷新。

// ✅ 正确写法
void Led_Proc()
{
    ucLed[0] = 1;
    ucLed[1] = 0;
    // 只修改数组,让中断去刷新硬件
}

错误3:忘记初始化 pwm_compare,导致LED不亮

新版默认 pwm_compare = 6,但如果你在 Led_Proc 里不小心把它改成0,LED就全灭了

// ❌ 错误写法
void Led_Proc()
{
    pwm_compare = 0;   // 占空比0%,LED全灭!
    ucLed[0] = 1;      // 设置了也没用
}

:white_check_mark: 正确做法pwm_compare 在变量声明时初始化好,不要在Led_Proc里改它(除非题目要求调光)。

九、总结:以后怎么“抄作业”最省力?

:pushpin: 旧版→新版迁移对照表

旧版写法 新版写法 改完有什么好处
Timer0_Init() Timer1_Init() 把定时器0留给更高级的功能
Timer0Server() interrupt 1 Timer1_Isr() interrupt 3 命名规范,一看就懂
Seg_Point[8] 数组存小数点 Seg_Buf[x] + 44 标记小数点 省8字节RAM,代码更简洁
Led_Disp(addr, enable) 单灯控制 Led_Disp(ucLed) 批量控制 一次刷新全部,效率更高
中断里 if(++x==10)x=0 中断里 x++,主循环 if(x>=10) 逻辑分离,更清晰
无PWM功能 中断里自带软件PWM 国赛常考功能,框架已搭好

:white_check_mark: 新模板“填空式”代码框架

┌─────────────────────────────────────────────────┐
│  需要你手动添加的内容(你的业务逻辑)           │
├─────────────────────────────────────────────────┤
│  Seg_Proc() {                                   │
│      Seg_Buf[0] = 1;        // 第0位显示1      │
│      Seg_Buf[1] = 2 + 44;   // 第1位显示2.     │
│      Seg_Buf[7] = 10;       // 第7位熄灭       │
│  }                                              │
│                                                  │
│  Led_Proc() {                                   │
│      ucLed[0] = 1;          // LED1亮          │
│      ucLed[1] = 0;          // LED2灭          │
│      // 改 pwm_compare 可以调亮度(0-9)       │
│      pwm_compare = 6;       // 60%亮度         │
│  }                                              │
│                                                  │
│  Key_Proc() {                                   │
│      if (Key_Down == 7) {   // S7被按下        │
│          // 写你的按键逻辑                       │
│      }                                          │
│  }                                              │
├─────────────────────────────────────────────────┤
│  绝对不能动的地方(底层框架)                    │
├─────────────────────────────────────────────────┤
│  ✅ Timer1_Init() 初始化                        │
│  ✅ Timer1_Isr() interrupt 3 中断服务           │
│  ✅ main() 的 while(1) 调用顺序                 │
│  ✅ Key_Read() 底层驱动                         │
│  ✅ Seg_Disp() 底层驱动                         │
└─────────────────────────────────────────────────┘

:loudspeaker: 一句话终极建议

放弃旧模板,拥抱新模板! 新模板:

  • :white_check_mark: 解决了P3.0串口冲突
  • :white_check_mark: 优化了内存分配
  • :white_check_mark: 自带PWM调光框架
  • :white_check_mark: 数码管显示更稳定

调度器代码深度分析——从“手工减速”到“自动调度”

一、调度器本质:把“人工计时”变成“闹钟自动提醒”

:yellow_circle: 旧方式(手工减速)

// 中断里
if(++Key_Slow_Down == 10) Key_Slow_Down = 0;

// 主循环里
void Key_Proc()
{
    if(Key_Slow_Down) return;   // ← 每次进函数都要手动检查
    Key_Slow_Down = 1;
    // 干活...
}

问题:每个任务都要自己管自己的计时器,代码重复、容易写漏、维护麻烦。

:green_circle: 新方式(调度器统一管理)

// 任务列表——把所有任务的周期统一登记在册
task_t Scheduler_Task[] = {
    {Led_Proc, 1, 0},      // LED任务,每1ms执行一次
    {Key_Proc, 10, 0},     // 按键任务,每10ms执行一次
    {Seg_Proc, 20, 0},     // 数码管任务,每20ms执行一次
    // ... 想加任务就加一行
};

// 调度器统一检查:到时间了就自动调用
void Scheduler_Run()
{
    for (i = 0; i < task_num; i++) {
        if (now_time >= Scheduler_Task[i].rate_ms + Scheduler_Task[i].last_ms) {
            Scheduler_Task[i].task_func();  // ← 自动执行!
        }
    }
}

比喻

  • 旧方式:每个员工自己看表——“到10点了吗?到了我就干活”——容易有人偷懒或看错时间。
  • 新方式:公司统一打卡系统——“滴,到点了,你该干活了”——公平、准确、省心

:light_bulb: 核心优势总结

对比维度 手工减速 调度器
代码重复度 每个任务都要写减速变量+判断 统一管理,一行数组搞定
添加新任务 要加变量、加中断代码、加判断 数组里加一行 {新函数, 周期, 0}
修改周期 全局搜索改代码 数组里直接改数字
可读性 逻辑分散在中断和主循环 一看任务列表就知道所有任务的执行频率

二、你的调度器代码逐行审查

:white_check_mark: 结构体定义——规范

typedef struct
{
    void (*task_func)(void);   // 任务函数指针
    unsigned long int rate_ms; // 任务执行周期(毫秒)
    unsigned long int last_ms; // 上次执行的时间戳
} task_t;

评价:标准写法,没毛病。unsigned long int 能计到49天(2^32 / 1000 / 3600 / 24 ≈ 49.7天),比赛绰绰有余。

:white_check_mark: 任务列表——清晰

idata task_t Scheduler_Task[] = {
    {Led_Proc, 1, 0},        // 1ms   ——最快
    {Key_Proc, 10, 0},       // 10ms  ——按键消抖
    {Seg_Proc, 20, 0},       // 20ms  ——显示刷新
    {Get_Time, 100, 0},      // 100ms ——RTC读取
    {Get_Temperature, 300, 0},// 300ms ——温度读取(DS18B20比较慢)
    {AD_DA, 150, 0},         // 150ms ——AD/DA转换
    {Get_Distance, 120, 0},  // 120ms ——超声波测距
    {Uart_Proc, 10, 0},      // 10ms  ——串口处理
};

评价:周期分配非常合理!

任务 周期 为什么合理
Led_Proc 1ms 配合PWM调光,响应快,亮度变化丝滑
Key_Proc 10ms 标准按键消抖周期,10ms检测一次刚刚好
Seg_Proc 20ms 数码管显示内容变化不需要太快,20ms够用
Get_Temperature 300ms DS18B20转换一次最长750ms,300ms读一次不阻塞系统
Get_Distance 120ms 超声波测距一次几十ms,120ms周期给足时间

:white_check_mark: Scheduler_Run 核心逻辑——正确

void Scheduler_Run()
{
    unsigned char i;
    for (i = 0; i < task_num; i++)
    {
        unsigned long int now_time = uwTick;  // ← 获取系统心跳
        if (now_time >= (Scheduler_Task[i].rate_ms + Scheduler_Task[i].last_ms))
        {
            Scheduler_Task[i].last_ms = now_time;
            Scheduler_Task[i].task_func();
        }
    }
}

三、:warning: 潜在坑点排查(90%的人会踩)

:fire: 坑点1:uwTick 在哪里更新???

你的调度器依赖 uwTick 这个全局变量来获取当前时间。代码里没看到它的定义和更新逻辑

你必须确保:在1ms定时器中断里,有 uwTick++ 这行代码!

// 在 Timer1_Isr() 里必须加上这一行!!!
void Timer1_Isr(void) interrupt 3
{
    uwTick++;   // ← 没有这一行,调度器就是死的!
    // ... 其他代码
}

如果没有now_time 永远是0,now_time >= rate + last 永远为真,所有任务每1ms全部执行一遍——CPU直接跑满,比裸奔还差!

:fire: 坑点2:Led_Proc 周期是1ms,但它里面做了什么?

如果你的 Led_Proc 里面直接操作硬件(调用 Led_DispP0 = ...),那问题大了:

// ❌ 危险写法——如果 Led_Proc 是这样
void Led_Proc()
{
    Led_Disp(ucLed);   // ← 直接在1ms任务里操作硬件!
}

后果:定时器中断里已经在做 Led_DispLed_Off 的PWM交替了,你再在任务里调用 Led_Disp两个地方同时抢硬件,LED显示会乱闪

:white_check_mark: 正确写法

// ✅ 安全写法——Led_Proc只改数组,不动硬件
void Led_Proc()
{
    // 只修改 ucLed 数组,让中断里的PWM去刷新硬件
    ucLed[0] = 1;
    ucLed[1] = 0;
    // 不调用 Led_Disp()!!!
}

:fire: 坑点3:Seg_Proc 周期20ms,但数码管刷新在中断里

你的数码管硬件刷新Timer1_Isr 里(每1ms刷一位),而 Seg_Proc 只是往 Seg_Buf填数据

// ✅ 正确分工
// 中断里(1ms):硬件刷新
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], ...);  // 读 Seg_Buf

// Seg_Proc(20ms):填数据
Seg_Buf[0] = 5;   // 写 Seg_Buf

只要 Seg_Proc 不直接调用 Seg_Disp,就没问题。

:fire: 坑点4:任务数量计算

task_num = sizeof(Scheduler_Task) / sizeof(task_t);

没问题,这是标准写法。但注意 sizeof 在编译时计算,不会在运行时额外开销。

:fire: 坑点5:时间戳溢出问题

uwTickunsigned long,溢出时间约49天。比赛连续运行最多几小时,不用担心。

但如果想做到绝对完美,可以改成:

// 安全写法——防止时间戳溢出导致任务跳过
if ((now_time - Scheduler_Task[i].last_ms) >= Scheduler_Task[i].rate_ms)
{
    Scheduler_Task[i].last_ms = now_time;
    Scheduler_Task[i].task_func();
}

差值比较而不是 now_time >= last + rate,可以避免 last + rate 溢出。(不过你跑的时长不会溢出,这属于锦上添花)

:fire: 坑点6:调度器轮询会不会太慢?

你的任务数 = 8个Scheduler_Run 每次主循环遍历8次。

  • 每次遍历 = 几次比较 + 可能调用函数
  • 主循环频率 ≈ 几万次/秒(无延时)
  • 8个任务的遍历开销 ≈ 微秒级

结论:完全没问题,51单片机轻松驾驭。

四、调度器 + 中断 = 完美的“双引擎”架构

你的代码现在是这个结构:

┌─────────────────────────────────────────────────────────────┐
│                     定时器1中断(1ms)                      │
│  ┌──────────────────────────────────────────────────────┐   │
│  │ 1. uwTick++(给调度器提供时间基准)                  │   │
│  │ 2. 数码管动态扫描(每1ms刷一位)                    │   │
│  │ 3. LED PWM调光(10ms周期,软件PWM)                 │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘
                              ↑
                         提供时间基准
                              │
┌─────────────────────────────────────────────────────────────┐
│                      主循环 while(1)                        │
│  ┌──────────────────────────────────────────────────────┐   │
│  │ Scheduler_Run() {                                    │   │
│  │    遍历8个任务,判断 uwTick 是否到达执行时间        │   │
│  │    ├─ Led_Proc(1ms)    → 改 ucLed[]              │   │
│  │    ├─ Key_Proc(10ms)   → 读按键,更新Key_Down    │   │
│  │    ├─ Seg_Proc(20ms)   → 改 Seg_Buf[]            │   │
│  │    ├─ Get_Time(100ms)  → 读RTC                   │   │
│  │    ├─ Get_Temperature(300ms)→ 读DS18B20          │   │
│  │    ├─ AD_DA(150ms)     → 读AD/写DA               │   │
│  │    ├─ Get_Distance(120ms)→ 读超声波              │   │
│  │    └─ Uart_Proc(10ms)  → 处理串口收发数据        │   │
│  │ }                                                  │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

这个架构的牛逼之处

  1. 中断负责高频、硬实时的工作(显示刷新、PWM)——不能有任何延迟
  2. 主循环负责低频、软实时的工作(按键、传感器读取、串口)——偶尔延迟几十ms没关系
  3. 调度器作为桥梁,把时间基准(uwTick)转换成各个任务的执行节奏

这其实就是一个小型RTOS(实时操作系统)的雏形!