第六章 大模板

51单片机模块化开发框架笔记

1. 核心架构:时间片轮询 (Time-Slice Polling)

这套代码的核心在于抛弃传统的软件延时(如 Delayms(10)),改用硬件定时器作为系统的“心脏”。

工作原理

  • 定时器 (Timer0): 设置为 1ms 中断一次。

  • 中断服务函数 (Timer0Server): 仅仅负责让不同的变量(计数器)自增。

  • 主循环 (main): 查询这些计数器是否到达设定值。如果到了,就执行任务;没到,就跳过。

### 代码解析

C

 void timer0server() interrupt 1 {
     // 按键减速:每10ms (1ms * 10) 归零一次
     if(++key_slow_down == 10) key_slow_down = 0;
     
     // 数码管减速:每500ms 归零一次(注:这里通常配合逻辑处理,不仅仅是刷新)
     if(++seg_slow_down == 500) seg_slow_down = 0;
     
     // 数码管扫描:每1ms切换一位,8ms扫完一轮 (125Hz刷新率,人眼无闪烁)
     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]);
 }

2. 模块详解与举例

2.1 按键处理模块 (key.c & key_proc)

该模块实现了矩阵键盘扫描软件消抖以及按键边沿检测(按下瞬间/抬起瞬间)。

  • 消抖逻辑:

    在 key_proc 中,首先判断 key_slow_down。如果它不为0,说明距离上次扫描还没过10ms,直接 return。

    C

     if(key_slow_down) return; // 时间没到,回去干别的
     key_slow_down = 1;        // 时间到了,重置,等待中断把它减回0
    
  • 三行代码定乾坤(边沿检测):

    这是最精华的部分,用于判断按键的状态变化。

    C

     key_val = key_read();                 // 读取当前状态
     key_down = key_val & (key_old ^ key_val); // 检测【按下瞬间】
     key_up = ~key_val & (key_old ^ key_val);  // 检测【抬起瞬间】
     key_old = key_val;                    // 更新旧状态
    

    举例说明:

    假设 key_old 为 0 (没按),key_val 变为 1 (按了)。

    1. key_old ^ key_val = 0 ^ 1 = 1 (表示状态发生了变化)。

    2. key_val & 1 = 1 & 1 = 1

    3. 结果:key_down 为 1,系统捕捉到了这次“按下”。

2.2 LED 控制模块 (led.c)

该模块实现了对8个LED的独立控制,改变其中一个灯的状态不会影响其他灯。

  • 原理: 利用静态变量 temp 记住当前所有灯的状态,然后通过位运算修改特定位。

  • 代码:

    C

     if(enable)
         temp |= 0x01 << addr;  // 置1:开启第 addr 号灯,其他位不变
     else
         temp &= ~(0x01 << addr); // 置0:关闭第 addr 号灯,其他位不变
    

    举例:

    想要点亮第3个灯(索引为2),调用 led_disp(2, 1)。

    想要关闭第3个灯,调用 led_disp(2, 0)。

2.3 数码管显示模块 (seg.c)

该模块负责底层的段选(显示什么字)和位选(在哪个位置显示)。

  • 消影处理:

    在切换下一个数码管之前,先执行 P0 = 0xff; (或者关闭位选),防止上一个数字的残影出现在下一个位置。

  • 缓存机制:

    主函数只需要修改 seg_buf[] 数组的内容,中断函数会自动把这个数组的内容显示出来。

    举例:

    想要在第1个数码管显示数字 ‘5’,只需要在主逻辑里写:seg_buf[0] = 5;。不需要在主循环里一直调用显示函数。


3. 实际应用场景示例

假设你要实现一个功能:按下 S4 键,数码管第0位数字加1,且 LED0 翻转状态。

只需要在 main.c 中修改 key_proc 部分(或者在主循环调用新的逻辑函数):

C

 // 在 main.c 或逻辑处理文件中
 void Logic_Proc()
 {
     // 利用之前计算好的 key_down 变量
     if(key_down == 4) // 这里的4对应 key_read() 返回的键值
     {
         // 1. 处理数码管数据
         seg_buf[0]++; 
         if(seg_buf[0] > 9) seg_buf[0] = 0;
 ​
         // 2. 处理LED数据 (通过修改 ucled 数组)
         if(ucled[0] == 0)
             ucled[0] = 1;
         else
             ucled[0] = 0;
     }
 }
 ​
 void main()
 {
     system_init();
     Timer0Init();
     while (1)
     {
         key_proc();   // 负责获取按键状态
         Logic_Proc(); // 负责业务逻辑(处理上面的需求)
         // seg_proc(); // 你的代码里这个函数目前是空的,可以用来处理时钟计数等
     }
 }

4. :warning: 代码中的注意事项(Debug指南)

在你提供的代码中,有几个细节需要注意:

  1. 大小写敏感:

    • C语言区分大小写。

    • main 函数中调用了 seg_Proc();led_Proc();(大写P),但你的定义是 void seg_proc()(小写p)。这会导致编译报错,请统一改为小写。

  2. Latch(锁存器)操作规范:

    • seg.c 中:

      C

       P2 = P2 & 0x1f | 0xc0; // 打开位选锁存器
       P2 = P2 & 0x1f;        // 这一句非常重要!关闭锁存器
      
    • 你的代码中 led.cinit.c 都写得很规范,记得保持这种“打开-赋值-关闭”的操作顺序,防止数据串扰(比如由于锁存器没关,导致改LED的时候数码管乱跳)。

  3. 头文件引用:

    • 引用自定义头文件用双引号 "",如 #include "key.h"

    • 你的代码已经遵循了这个规则,做得很好。

5. 总结

这份代码是一个非常标准的工程化模板

  • 解耦: 硬件驱动(按键读取、显示驱动)和业务逻辑(按了键干什么)完全分离。

  • 高效: 没有 delay,CPU利用率高。

  • 易扩展: 如果想加一个“跑马灯”功能,只需要在中断或 seg_proc 里增加计数逻辑,不需要推翻重写。