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 (按了)。
-
key_old ^ key_val=0 ^ 1=1(表示状态发生了变化)。 -
key_val & 1=1 & 1=1。 -
结果:
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.
代码中的注意事项(Debug指南)
在你提供的代码中,有几个细节需要注意:
-
大小写敏感:
-
C语言区分大小写。
-
main函数中调用了seg_Proc();和led_Proc();(大写P),但你的定义是void seg_proc()(小写p)。这会导致编译报错,请统一改为小写。
-
-
Latch(锁存器)操作规范:
-
在
seg.c中:C
P2 = P2 & 0x1f | 0xc0; // 打开位选锁存器 P2 = P2 & 0x1f; // 这一句非常重要!关闭锁存器 -
你的代码中
led.c和init.c都写得很规范,记得保持这种“打开-赋值-关闭”的操作顺序,防止数据串扰(比如由于锁存器没关,导致改LED的时候数码管乱跳)。
-
-
头文件引用:
-
引用自定义头文件用双引号
"",如#include "key.h"。 -
你的代码已经遵循了这个规则,做得很好。
-
5. 总结
这份代码是一个非常标准的工程化模板。
-
解耦: 硬件驱动(按键读取、显示驱动)和业务逻辑(按了键干什么)完全分离。
-
高效: 没有
delay,CPU利用率高。 -
易扩展: 如果想加一个“跑马灯”功能,只需要在中断或
seg_proc里增加计数逻辑,不需要推翻重写。