温度采集器项目对比分析报告
对比概览
本报告对比分析了两个项目:
- 项目A(你的项目):
demo5/User/main.c(451行) - 项目B(范例程序):
过渡训练 - 范例程序/User/main.c(293行)
相同点总结
1. 整体架构
| 特性 | 项目A | 项目B | 一致性 |
|---|---|---|---|
| 分层设计 | Driver + User | Driver + User | |
| 三种工作模式 | 输入/显示/设置 | 输入/显示/设置 | |
| 单片机型号 | AT89C52 | AT89C52 | |
| 晶振频率 | 12MHz | 12MHz |
2. 驱动层代码
重要发现:三个驱动文件完全相同!
| 文件 | 代码行数 | 功能 | 对比结果 |
|---|---|---|---|
| Key.c | 27行 | 4×4矩阵键盘扫描 | |
| Led.c | 17行 | 8位LED驱动 | |
| Seg.c | 22行 | 6位数码管驱动 |
代码对比示例:
// Key.c - 两个项目完全一致
unsigned char Key_Read()
{
unsigned char temp = 0;
P3_0 = 0; P3_1 = 1; P3_2 = 1; P3_3 = 1;
if(P3_4 == 0) temp = 1;
// ... 完全相同
}
3. 定时器配置
// Timer0初始化 - 两个项目完全一致
void Timer0Init(void) // @12.000MHz
{
TMOD &= 0xF0;
TMOD |= 0x01;
TL0 = 0x18; // ✅ 相同
TH0 = 0xFC; // ✅ 相同
// ... 其他配置完全相同
}
4. 按键防抖机制
// 两个项目都采用边沿检测
Key_Val = Key_Read();
Key_Down = Key_Val & (Key_Old ^ Key_Val); // 下降沿
Key_Up = ~Key_Val & (Key_Old ^ Key_Val); // 上升沿(范例)
// 或
Key_Up = Key_Old & (Key_Val ^ Key_Old); // 上升沿(你的)
5. 功能模块
| 功能 | 两个项目都有 |
|---|---|
| 温度输入(0-85℃) | |
| 小数点支持 | |
| 四舍五入处理 | |
| 参数设置(上下限) | |
| LED状态指示 | |
| PWM亮度控制 | |
| 闪烁效果 | |
| 长按加速 |
差异点详解
【差异1】温度数据类型与处理方式
项目B(范例):使用浮点数
// 范例程序 - 使用float类型
float Temperature; // 实际温度变量
// 按键16确认时的计算逻辑
Temperature = Seg_Input[0] * 100 + Seg_Input[1] * 10 + Seg_Input[2] + 5;
while(3 - Point_Wela)
{
Temperature /= 10.0; // 浮点除法
Point_Wela++;
}
// 示例:输入36.8
// 计算:(3*100 + 6*10 + 8 + 5) = 373
// Point_Wela=1,除1次:373/10 = 37.3
// 四舍五入后显示:37
// 显示时直接取整
Seg_Buf[4] = (unsigned char)Temperature / 10; // 十位
Seg_Buf[5] = (unsigned char)Temperature % 10; // 个位
优点:
- 代码简洁
- 直观易懂
- 自动四舍五入
缺点:
- 占用更多RAM(4字节)
- 需要浮点运算库(增加代码体积)
- 运行速度较慢
项目A(你的):使用整数数组
// 你的项目 - 使用unsigned char数组
unsigned char Temperature_Real[3] = {0,0,0}; // 百、十、个位
unsigned int Temperature_Input_Data; // 临时计算变量
// 按键16确认时的四舍五入逻辑
switch(Point_Wei)
{
case 0: // 格式:x.xx → 保留整数
Temperature_Real[1] = 0;
Temperature_Real[2] = Temperature_Input[0];
if(Temperature_Input[1] >= 5) // 十分位≥5进位
{
Temperature_Real[2]++;
if(Temperature_Real[2] == 10)
{
Temperature_Real[2] = 0;
Temperature_Real[1]++;
}
}
break;
case 1: // 格式:xx.x → 保留一位小数
Temperature_Real[1] = Temperature_Input[0];
Temperature_Real[2] = Temperature_Input[1];
if(Temperature_Input[2] >= 5) // 百分位≥5进位
{
Temperature_Real[2]++;
// ... 进位处理
}
break;
case 2: // 无小数点 → 清除数据
// 重置逻辑
break;
}
// 最终转换为整数
Temperature_Input_Data = Temperature_Real[0]*100 +
Temperature_Real[1]*10 +
Temperature_Real[2];
优点:
- 不需要浮点库(代码体积小)
- 运行速度快
- RAM占用少
- 精确控制四舍五入
缺点:
- 代码较长
- 逻辑复杂
- 需要手动处理进位
【差异2】参数设置的数据结构
项目B(范例):紧凑型设计
// 范例程序 - 单维数组
unsigned char Parameter[2] = {30, 20}; // 上限30℃,下限20℃
unsigned char Parameter_Ctrol[2] = {30, 20}; // 实际生效参数
bit Parameter_Index; // 索引:0或1
// 参数切换(按键13)
Parameter_Index ^= 1; // 0↔1切换
// 显示逻辑(模式2)
Seg_Buf[2] = Parameter[0] / 10; // 上限十位
Seg_Buf[3] = Parameter[0] % 10; // 上限个位
Seg_Buf[4] = Parameter[1] / 10; // 下限十位
Seg_Buf[5] = Parameter[1] % 10; // 下限个位
// 闪烁效果:通过Parameter_Index计算偏移
Seg_Buf[2 + 2*Parameter_Index] = Seg_Star_Flag ? Parameter[Parameter_Index]/10 : 10;
Seg_Buf[3 + 2*Parameter_Index] = Seg_Star_Flag ? Parameter[Parameter_Index]%10 : 10;
优点:
- 数据结构简洁
- 索引计算巧妙(2*Parameter_Index)
- 内存占用少
项目A(你的):四位数组设计
// 你的项目 - 四位数组
unsigned char Temperature_Setting[4] = {3,0,2,0}; // [上限十位][上限个位][下限十位][下限个位]
unsigned char Temperature_Setting_Old[4] = {3,0,2,0}; // 备份数组
unsigned char Temperature_Setting_Index; // 索引:0或2
// 参数切换(按键13)
Temperature_Setting_Index += 2;
if(Temperature_Setting_Index == 4)
Temperature_Setting_Index = 0;
// 显示逻辑(模式2)
Seg_Buf[2] = Temperature_Setting[0]; // 上限十位
Seg_Buf[3] = Temperature_Setting[1]; // 上限个位
Seg_Buf[4] = Temperature_Setting[2]; // 下限十位
Seg_Buf[5] = Temperature_Setting[3]; // 下限个位
// 闪烁效果:直接判断索引
if(Temperature_Setting_Index == 0) // 修改上限
{
Seg_Buf[2] = 10;
Seg_Buf[3] = 10;
}
if(Temperature_Setting_Index == 2) // 修改下限
{
Seg_Buf[4] = 10;
Seg_Buf[5] = 10;
}
优点:
- 数据结构直观
- 直接存储各位数字
- 易于理解和维护
缺点:
- 内存占用稍多
- 需要额外转换(合并为完整温度值)
【差异3】长按检测机制
项目B(范例):基于标志位的计时
// 范例程序 - 使用计时标志
bit Time_Flag; // 计时启动标志
unsigned int Count_500Ms; // 计时累加器
// 按键处理
if(Key_Down == 14)
Time_Flag = 1; // 启动计时
if(Count_500Ms < 500) // 短按判断
{
if(Key_Up == 14)
{
Time_Flag = Count_500Ms = 0; // 复位
Parameter[Parameter_Index]++; // 单次加1
}
}
else // 长按判断(≥500ms)
{
if(Key_Old == 14)
{
Parameter[Parameter_Index]++; // 连续加1
}
if(Key_Up == 14)
Time_Flag = Count_500Ms = 0;
}
// 定时器中断
if(Time_Flag == 1)
{
if(++Count_500Ms == 600) // 限制上限
Count_500Ms = 600;
}
特点:
- 按下时启动计时
- 500ms为阈值
- 上限保护600ms
- 支持短按和长按两种行为
项目A(你的):独立计数器
// 你的项目 - 每个按键独立计数
unsigned int Key_Long0; // 按键14长按计数
unsigned int Key_Long1; // 按键15长按计数
// 按键处理
if(Key_Down == 14) // 短按
{
Temperature_Setting[Temperature_Setting_Index + 1]++;
// ... 进位处理
}
if(Key_Old == 14) // 长按判断
{
if(Key_Long0 >= 500) // 阈值500ms
{
Temperature_Setting[Temperature_Setting_Index + 1]++;
// ... 进位处理
}
}
// 定时器中断
if(Key_Old == 14)
Key_Long0++;
else
Key_Long0 = 0;
if(Key_Old == 15)
Key_Long1++;
else
Key_Long1 = 0;
特点:
- 每个按键独立计数器
- 始终累加,不需要启动标志
- 500ms阈值
- 短按和长按都会执行相同操作
【差异4】数码管刷新周期
| 项目 | 按键扫描 | 数码管刷新 | 闪烁周期 |
|---|---|---|---|
| 项目B(范例) | 10ms | 10ms | 500ms |
| 项目A(你的) | 10ms | 50ms | 250ms |
// 范例程序
if(++Seg_Slow_Down == 10) Seg_Slow_Down = 0; // 10ms刷新
if(++Timer_500Ms == 500) { ... } // 500ms闪烁
// 你的项目
if(++Seg_Slow_Down == 50) Seg_Slow_Down = 0; // 50ms刷新
if(++Time250 == 250) { ... } // 250ms闪烁
分析:
-
数码管刷新:
- 范例10ms:响应更快,但主循环压力较大
- 你的50ms:减少CPU占用,响应稍慢但足够
-
闪烁周期:
- 范例500ms:节奏舒缓
- 你的250ms:节奏更快,视觉反馈更明显
【差异5】LED控制逻辑的实现方式
项目B(范例):位运算表达式
// 范例程序 - 简洁但难懂的位运算
void Led_Proc()
{
// PWM等级计算(注释掉的版本)
// Led_Pwm = 3 * (1*((int)Temperature/Parameter_Ctrol[0]) +
// 2*((!((int)Temperature/Parameter_Ctrol[0])) & ((int)Temperature/Parameter_Ctrol[1])) +
// 3*(!((int)Temperature/Parameter_Ctrol[1])));
// 当前使用的if-else版本
if(Temperature > Parameter_Ctrol[0])
Led_Pwm = 3;
else if(Temperature < Parameter_Ctrol[0] && Temperature > Parameter_Ctrol[1])
Led_Pwm = 6;
else
Led_Pwm = 9;
// LED状态计算 - 使用位运算
ucLed[0] = (int)Temperature / Parameter_Ctrol[0]; // 温度>上限 → 1
ucLed[1] = (!((int)Temperature / Parameter_Ctrol[0])) &
((int)Temperature / Parameter_Ctrol[1]); // 在范围内 → 1
ucLed[2] = !((int)Temperature / Parameter_Ctrol[1]); // 温度<下限 → 1
ucLed[3] = Error_Flag;
}
解析:
- 利用整数除法:
Temperature/Parameter_Ctrol[0]- 若Temperature > Parameter_Ctrol[0],结果≥1(真)
- 若Temperature < Parameter_Ctrol[0],结果=0(假)
- 使用逻辑非和位与组合判断范围
优点:代码简洁(4行)
缺点:可读性差,难以维护
项目A(你的):清晰的条件判断
// 你的项目 - 直观的if-else
void Led_Proc()
{
// LED0:温度超过上限
if(Temperature_Data > TMAX)
{
Led_Level = 3;
ucLed[0] = 1;
}
else
{
ucLed[0] = 0;
}
// LED1:温度在范围内
if((Temperature_Data >= TMIN) && (Temperature_Data <= TMAX))
{
Led_Level = 6;
ucLed[1] = 1;
}
else
{
ucLed[1] = 0;
}
// LED2:温度低于下限
if(Temperature_Data < TMIN)
{
Led_Level = 9;
ucLed[2] = 1;
}
else
{
ucLed[2] = 0;
}
// LED3:数据错误标志
if(Data_Error == 1)
{
ucLed[3] = 1;
}
else
{
ucLed[3] = 0;
}
}
优点:逻辑清晰,易于理解和调试
缺点:代码较长(34行)
【差异6】PWM亮度等级的变量命名
| 项目 | PWM计数器 | PWM阈值 |
|---|---|---|
| 项目B(范例) | Led_Num |
Led_Pwm |
| 项目A(你的) | Led_PWM |
Led_Level |
// 范例程序
if(++Led_Num == 10) Led_Num = 0;
if(Led_Num < Led_Pwm)
Led_Disp(Led_Pos, ucLed[Led_Pos]);
else
Led_Disp(Led_Pos, 0);
// 你的项目
if(++Led_PWM == 10) Led_PWM = 0;
if(Led_PWM < Led_Level)
Led_Disp(Led_Pos, ucLed[Led_Pos]);
else
Led_Disp(Led_Pos, 0);
语义分析:
- 范例:
Led_Num(计数器),Led_Pwm(阈值) - 你的:
Led_PWM(计数器),Led_Level(阈值) - 你的命名更合理:
Led_Level明确表示"亮度等级"
【差异7】温度输入验证规则
项目B(范例):强制小数点
if(Point_Flag == 0 || Seg_Input_Index < 3)
{
// 小数点未使能 或 输入不足3位 → 无效数据
// 清除输入,重新开始
}
else
{
// 小数点已使能 且 输入3位 → 有效数据
Temperature = Seg_Input[0]*100 + Seg_Input[1]*10 + Seg_Input[2] + 5;
// ... 四舍五入处理
}
规则:
必须输入小数点(如:3.25、3.2)
必须输入3位数字
不能输入整数(如:32)
示例:
输入:3 → 2 → . → 5 → 16 → 成功(32.5℃ → 显示33℃)
输入:3 → 2 → 16 → 失败(清除重输)
项目A(你的):灵活处理
switch(Point_Wei)
{
case 0: // 格式:x.xx
// 四舍五入为整数
break;
case 1: // 格式:xx.x
// 保留一位小数
break;
case 2: // 无小数点
// 按16第一次:清除数据
// 按16第二次:返回模式0
break;
}
规则:
可以输入小数点(如:3.25)
可以输入整数(按16两次清除后重输)
支持不同小数位格式
示例:
输入:3 → 2 → . → 5 → 16 → 成功(32.5℃ → 显示33℃)
输入:3 → 2 → 16 → 清除 → 重新输入
【差异8】段码表索引定义
// 范例程序
// Seg_Dula[] = {0-9, 空, -, A, C, P}
// 0-9 10 11 12 13 14
Seg_Buf[0] = 12; // 模式0显示 "A"
Seg_Buf[0] = 13; // 模式1显示 "C"
Seg_Buf[0] = 14; // 模式2显示 "P"
// 你的项目
// Seg_Dula[] = {0-9, 空, G, A(T), -, C(d), P(S)}
// 0-9 10 11 12 13 14 15
Seg_Buf[0] = 12; // 模式0显示 "T" (实际编码是A)
Seg_Buf[0] = 14; // 模式1显示 "d" (实际编码是C)
Seg_Buf[0] = 15; // 模式2显示 "S" (实际编码是P)
分析:
- 范例:直接使用A、C、P字母
- 你的:使用T、d、S作为界面标识(更直观)
- T = Temperature(温度输入)
- d = display(数据显示)
- S = Setting(参数设置)
【差异9】代码注释风格
项目B(范例):简洁注释
unsigned char Key_Val,Key_Down,Key_Old,Key_Up;//按键专用变量
unsigned char Seg_Buf[6] = {10,10,10,10,10,10};//数码管显示数据存放数组
项目A(你的):详细注释
unsigned char Key_Val,Key_Down,Key_Old,Key_Up;//按键专用变量
unsigned char Key_Slow_Down;//按键减速专用变量
unsigned char Seg_Buf[6] = {10,10,10,10,10,10};//数码管显示数据存放数组
unsigned char Seg_Point[6] = {0,0,0,0,0,0};//数码管小数点数据存放数组
unsigned char Seg_Pos;//数码管扫描专用变量
对比:
- 范例:注释简洁,关注重点
- 你的:每个变量都有注释,更详细
【差异10】参数调整的进位逻辑
项目B(范例):循环边界
if(++Parameter[Parameter_Index] > 70)
Parameter[Parameter_Index] = 10; // 70 → 10 循环
if(--Parameter[Parameter_Index] == 255) // unsigned char下溢
Parameter[Parameter_Index] = 70; // 10 → 70 循环
特点:
- 参数范围:10-70℃
- 超出边界时循环(70→10,10→70)
项目A(你的):分位调整
Temperature_Setting[Temperature_Setting_Index + 1]++;
if(Temperature_Setting[Temperature_Setting_Index + 1] == 10)
{
Temperature_Setting[Temperature_Setting_Index + 1] = 0;
Temperature_Setting[Temperature_Setting_Index]++; // 个位进位
}
// 上限保护:70℃
if((Temperature_Setting[Temperature_Setting_Index] == 7) &&
(Temperature_Setting[Temperature_Setting_Index + 1] == 1))
{
Temperature_Setting[Temperature_Setting_Index] = 7;
Temperature_Setting[Temperature_Setting_Index + 1] = 0;
}
特点:
- 分别处理十位和个位
- 个位满10进位
- 边界保护(上限70℃,下限10℃)
综合对比表
| 对比维度 | 项目A(你的) | 项目B(范例) | 推荐 |
|---|---|---|---|
| 代码行数 | 451行 | 293行 | 范例更简洁 |
| 温度数据类型 | unsigned char数组 | float浮点数 | 看需求 |
| 代码体积 | 小(无浮点库) | 大(需浮点库) | |
| 运行速度 | 快(整数运算) | 慢(浮点运算) | |
| 可读性 | 高(清晰条件) | 中(位运算) | |
| 可维护性 | 高(注释详细) | 中(注释简洁) | |
| 代码简洁度 | 中(逻辑展开) | 高(表达式简洁) | |
| 长按机制 | 独立计数器 | 标志位计时 | 各有优势 |
| 输入验证 | 灵活(可整数) | 严格(必须小数点) | |
| 数码管刷新 | 50ms | 10ms | 看需求 |
| 闪烁周期 | 250ms | 500ms | 看需求 |
| 变量命名 | Led_Level(清晰) | Led_Pwm(简洁) |
优化建议
对项目A(你的)的建议
1. 简化LED控制逻辑
// 当前代码(34行)
if(Temperature_Data > TMAX)
{
Led_Level = 3;
ucLed[0] = 1;
}
else
{
ucLed[0] = 0;
}
// ... 重复逻辑
// 优化建议(借鉴范例)
ucLed[0] = (Temperature_Data > TMAX);
ucLed[1] = (Temperature_Data >= TMIN) && (Temperature_Data <= TMAX);
ucLed[2] = (Temperature_Data < TMIN);
ucLed[3] = Data_Error;
// PWM等级
if(Temperature_Data > TMAX)
Led_Level = 3;
else if(Temperature_Data >= TMIN)
Led_Level = 6;
else
Led_Level = 9;
2. 统一长按机制
// 当前:两个按键独立计数
unsigned int Key_Long0;
unsigned int Key_Long1;
// 建议:借鉴范例的统一标志位
bit Time_Flag;
unsigned int Long_Press_Counter;
3. 优化参数数组结构
// 当前:4位数组
unsigned char Temperature_Setting[4] = {3,0,2,0};
// 建议:改为2位数组(借鉴范例)
unsigned char TMAX_Setting = 30;
unsigned char TMIN_Setting = 20;
// 或
unsigned char Temp_Setting[2] = {30, 20};
对项目B(范例)的建议
1. 避免浮点运算(如有代码体积限制)
// 当前:使用float
float Temperature;
// 建议:改为整数(如你的项目)
unsigned char Temperature_Real[3];
2. 增加注释详细度
// 当前:简洁注释
unsigned char Parameter[2] = {30,20};
// 建议:详细注释
unsigned char Parameter[2] = {30,20}; // [0]=上限(TMAX), [1]=下限(TMIN)
3. 放宽输入验证规则
// 当前:必须输入小数点
if(Point_Flag == 0 || Seg_Input_Index < 3)
{
// 清除数据
}
// 建议:支持整数输入(如你的项目)
最佳实践总结
从范例程序学习的优点
代码简洁性:使用表达式和位运算减少代码行数
数据结构:Parameter[2]比四位数组更紧凑
长按机制:基于标志位的设计更通用
你的项目的优势
无浮点运算:减少代码体积,提高运行效率
可读性强:清晰的if-else结构,易于理解
注释详细:每个变量和逻辑都有说明
变量命名:Led_Level比Led_Pwm更语义化
灵活输入:支持整数和小数两种输入方式
学习要点
1. 嵌入式系统设计权衡
- 代码体积 vs 可读性:范例更简洁,你的更清晰
- 运行效率 vs 开发效率:整数运算更快,浮点运算更直观
- 灵活性 vs 严格性:你的输入更灵活,范例更严格
2. 代码风格
- 范例风格:适合追求代码简洁的场景
- 你的风格:适合团队协作和长期维护
3. 算法选择
- 四舍五入:
- 浮点:简单直观,但需要库支持
- 整数:复杂但高效,适合资源受限场景
4. 状态机设计
- 两个项目都采用了清晰的状态机设计(模式0/1/2)
- 状态转移逻辑清晰,易于扩展
结论
两个项目都是优秀的实现!
- 范例程序:代码简洁,适合学习嵌入式编程技巧
- 你的项目:逻辑清晰,适合实际工程应用
核心相同点
驱动层代码100%相同
整体架构和功能完全一致
按键防抖、定时器、PWM等机制相同
主要差异
温度数据处理:浮点 vs 整数
代码风格:简洁 vs 详细
参数结构:紧凑 vs 直观
推荐学习路径
- 理解范例的简洁性:学习位运算和表达式优化
- 保持你的可读性:清晰的代码更易维护
- 融合两者优点:根据实际需求选择最佳方案
报告生成时间:2026-01-31
对比项目:demo5 vs 过渡训练范例程序
分析深度:驱动层、应用层、算法实现、代码风格