下降沿检测Bug调试记录
1. 问题背景
需求描述
实现电压采集器的下降沿计数功能:
- 用户输入电压值(4位,如3.256V)
- 设定阈值(如3.00V)
- 每次确认输入时,比较本次输入和上次输入
- 检测是否穿越阈值的下降沿(从≥阈值变为<阈值)
- 若检测到下降沿,计数器+1
核心逻辑
下降沿检测 = (当前电压 < 阈值) && (上次电压 >= 阈值)
2. 原代码问题
2.1 原代码实现(修复前)
// 按键11确认时(第63-74行)
if((Key_Down == 11) && (Voltage_Input_Index == 4) )
{
for(i = 0;i<4;i++)
{
Voltage_Data[i] = Voltage_Input[i]; // 复制输入数据
}
Disp_Mode = 1; // 切换到显示模式
// ❌ 问题代码
Voltage_Setting = Voltage_Setting_Data[2] + 10*Voltage_Setting_Data[1] + 100*Voltage_Setting_Data[0];
Voltage_Real = Voltage_Real_Data[2] + 10*Voltage_Real_Data[1] + 100*Voltage_Real_Data[0];
if((Voltage_Real < Voltage_Setting) && (Voltage_Old > Voltage_Setting))
Voltage_Count++;
Voltage_Old = Voltage_Real;
}
2.2 数据流程分析
程序中的数据流向:
用户输入
↓
Voltage_Input[4] // 原始输入(4位)
↓ (确认时复制)
Voltage_Data[4] // 确认的数据
↓ (Seg_Proc模式1处理)
Voltage_Real_Data[3] // 四舍五入后(3位)
↓ (计算整数值)
Voltage_Real // 用于比较的整数
↓ (下降沿检测)
Voltage_Count++
2.3 问题1:数据时序错误(严重)
关键问题:Voltage_Real_Data的更新时机不对!
Voltage_Real_Data只在Seg_Proc的case 1中更新:
// Seg_Proc函数(原188-190行)
case 1:
// ...四舍五入处理...
Voltage_Real_Data[0] = Seg_Buf[3];
Voltage_Real_Data[1] = Seg_Buf[4];
Voltage_Real_Data[2] = Seg_Buf[5];
break;
主循环执行顺序:
while (1)
{
Key_Proc(); // ← 先执行,此时Voltage_Real_Data还是旧值!
Seg_Proc(); // ← 后执行,才更新Voltage_Real_Data
Led_Proc();
}
时序问题示意图:
t0: 用户输入3.256V,按S7确认
├─ Voltage_Data = [3,2,5,6]
├─ Voltage_Real_Data = [0,0,0] ← 还是初始值!
├─ Voltage_Real = 0 + 0 + 0 = 0 ← 错误!
└─ Voltage_Old = 0
t1: 主循环执行Seg_Proc
└─ Voltage_Real_Data = [3,2,6] ← 3.26V(四舍五入)
t2: 用户输入2.844V,按S7确认
├─ Voltage_Data = [2,8,4,4]
├─ Voltage_Real_Data = [3,2,6] ← 用的是上一次的值!
├─ Voltage_Real = 6 + 20 + 300 = 326 ← 这是上次的3.26V
└─ Voltage_Old = 326
t3: 主循环执行Seg_Proc
└─ Voltage_Real_Data = [2,8,4] ← 2.84V
t4: 用户输入1.234V,按S7确认
├─ Voltage_Real_Data = [2,8,4] ← 上一次的2.84V
├─ Voltage_Real = 4 + 80 + 200 = 284
├─ 判断:(284 < 300) && (326 > 300) = True
└─ Voltage_Count++ ← 终于计数了,但比较的是2.84V和3.26V!
结论:下降沿检测总是滞后一拍,比较的是"上次输入"和"上上次输入"!
2.4 问题2:首次输入无效
unsigned int Voltage_Real_Data[3] = {0,0,0}; // 初始值
unsigned int Voltage_Old; // ❌ 未初始化!
- 第一次确认输入时,
Voltage_Real = 0(使用初始值) Voltage_Old未初始化,值不确定- 第一次比较结果不可靠
2.5 问题3:边界条件不准确
if((Voltage_Real < Voltage_Setting) && (Voltage_Old > Voltage_Setting))
使用>而非>=:
- 如果上次电压正好等于阈值,不会被判定为"高于阈值"
- 应该使用
>=,因为等于阈值时也算"在阈值上"
3. 错误示例演示
测试场景
- 阈值设定:3.00V
- 输入序列:3.567V → 2.844V → 1.234V
原代码执行流程
| 时刻 | 操作 | Voltage_Data | Voltage_Real_Data | Voltage_Real | Voltage_Old | 判断条件 | 结果 | 说明 |
|---|---|---|---|---|---|---|---|---|
| t0 | 启动 | [0,0,0,0] | [0,0,0] | - | 未初始化 | - | Count=0 | 初始状态 |
| t1 | 输入3.567V确认 | [3,5,6,7] | [0,0,0] | 0 | 0 | (0<300)&&(0>300)=False | Count=0 | |
| t2 | Seg_Proc执行 | [3,5,6,7] | [3,5,7] | - | - | - | - | 四舍五入:7≥5→3.57V |
| t3 | 输入2.844V确认 | [2,8,4,4] | [3,5,7] | 357 | 0 | (357<300)&&(0>300)=False | Count=0 | |
| t4 | Seg_Proc执行 | [2,8,4,4] | [2,8,4] | - | - | - | - | 四舍五入:4<5→2.84V |
| t5 | 输入1.234V确认 | [1,2,3,4] | [2,8,4] | 284 | 357 | (284<300)&&(357>300)=True | Count=1 | ✓检测到,但慢一拍 |
问题总结
- t1时刻:应该记录3.57V,但实际用的是0
- t3时刻:应该检测到3.57V→2.84V的下降沿(穿越3.00V),但因为用的是上次的3.57V和初始的0,未检测到
- t5时刻:检测到的是2.84V→1.23V,但这不是本次应该检测的对比
4. 解决方案
4.1 核心思路
在确认输入时立即计算四舍五入值,不依赖Seg_Proc的延迟更新。
4.2 修复代码
// 按键11确认时(第63-101行)
if((Key_Down == 11) && (Voltage_Input_Index == 4) )
{
// 1. 保存旧电压值用于下降沿检测
Voltage_Old = Voltage_Real;
// 2. 复制新输入数据
for(i = 0;i<4;i++)
{
Voltage_Data[i] = Voltage_Input[i];
}
// 3. 立即计算四舍五入值(复制Seg_Proc模式1的逻辑)
Voltage_Real_Data[0] = Voltage_Data[0];
Voltage_Real_Data[1] = Voltage_Data[1];
Voltage_Real_Data[2] = Voltage_Data[2];
// 第4位四舍五入处理,带进位
if(Voltage_Data[3] >= 5)
{
Voltage_Real_Data[2]++;
if(Voltage_Real_Data[2] >= 10) // 个位进位
{
Voltage_Real_Data[2] = 0;
Voltage_Real_Data[1]++;
}
if(Voltage_Real_Data[1] >= 10) // 十分位进位
{
Voltage_Real_Data[1] = 0;
Voltage_Real_Data[0]++;
}
}
// 4. 计算整数值
Voltage_Real = Voltage_Real_Data[2] + 10*Voltage_Real_Data[1] + 100*Voltage_Real_Data[0];
// 5. 下降沿检测:上次>=阈值 且 当前<阈值
Voltage_Setting = Voltage_Setting_Data[2] + 10*Voltage_Setting_Data[1] + 100*Voltage_Setting_Data[0];
if((Voltage_Real < Voltage_Setting) && (Voltage_Old >= Voltage_Setting))
{
Voltage_Count++;
}
Disp_Mode = 1;
}
4.3 关键改进点
改进1:保存顺序调整
// 原代码:先计算再保存(错误)
Voltage_Real = ...;
Voltage_Old = Voltage_Real; // 保存的是本次值
// 新代码:先保存再计算(正确)
Voltage_Old = Voltage_Real; // 保存上次值
Voltage_Real = ...; // 计算本次值
改进2:立即计算四舍五入
不等待Seg_Proc更新,在确认输入时立即处理:
// 复制Seg_Proc的四舍五入逻辑
Voltage_Real_Data[0-2] = Voltage_Data[0-2];
if(Voltage_Data[3] >= 5) { 进位处理... }
改进3:边界条件修正
// 原代码
if((Voltage_Real < Voltage_Setting) && (Voltage_Old > Voltage_Setting))
// 新代码(包含等于情况)
if((Voltage_Real < Voltage_Setting) && (Voltage_Old >= Voltage_Setting))
5. 修复后验证
同样的测试场景
- 阈值设定:3.00V
- 输入序列:3.567V → 2.844V → 1.234V
新代码执行流程
| 时刻 | 操作 | Voltage_Old | 输入 | 四舍五入 | Voltage_Real | 判断条件 | 结果 | 说明 |
|---|---|---|---|---|---|---|---|---|
| t0 | 启动 | 0 | - | - | 0 | - | Count=0 | 初始状态 |
| t1 | 输入3.567V确认 | 0→保存 | [3,5,6,7] | 7≥5→进位 | 357 | (357<300)&&(0≥300)=False | Count=0 | |
| t2 | 输入2.844V确认 | 357→保存 | [2,8,4,4] | 4<5→不进位 | 284 | (284<300)&&(357≥300)=True | Count=1 | |
| t3 | 输入1.234V确认 | 284→保存 | [1,2,3,4] | 4<5→不进位 | 123 | (123<300)&&(284≥300)=False | Count=1 | |
| t4 | 输入3.456V确认 | 123→保存 | [3,4,5,6] | 6≥5→进位 | 346 | (346<300)&&(123≥300)=False | Count=1 | |
| t5 | 输入2.789V确认 | 346→保存 | [2,7,8,9] | 9≥5→进位 | 279 | (279<300)&&(346≥300)=True | Count=2 |
验证结果
t1:立即计算出3.57V,不再依赖Seg_Proc
t2:正确检测到3.57V→2.84V的下降沿(穿越3.00V)
t3:已在阈值下,不计数
t4:上升穿越不计数(只检测下降沿)
t5:再次检测到3.46V→2.79V的下降沿
6. 修复前后对比
数据流程对比
修复前:
确认输入 → Voltage_Data更新
↓
主循环 → Key_Proc → 使用旧的Voltage_Real_Data(❌滞后)
↓
主循环 → Seg_Proc → 更新Voltage_Real_Data
↓
下次确认 → Key_Proc → 使用上次的Voltage_Real_Data(❌慢一拍)
修复后:
确认输入 → Voltage_Data更新
↓
立即计算 → Voltage_Real_Data更新(✅实时)
↓
立即检测 → 下降沿判断(✅准确)
↓
主循环 → Seg_Proc → 使用已更新的Voltage_Real_Data(✅同步)
代码逻辑对比
| 方面 | 修复前 | 修复后 |
|---|---|---|
| 数据更新 | 依赖Seg_Proc延迟更新 | 确认时立即计算 |
| 执行顺序 | 先计算后保存(错误) | 先保存后计算(正确) |
| 时序关系 | 滞后一拍 | 实时准确 |
| 边界条件 | > 不含等于 |
>= 包含等于 |
| 首次输入 | 使用未初始化值 | 正确处理 |
7. 经验总结
7.1 时序问题的识别
症状:
- 功能延迟响应(慢一拍)
- 首次执行结果异常
- 数据更新和使用不同步
排查方法:
- 绘制数据流程图
- 标记每个变量的更新时机
- 跟踪主循环的执行顺序
- 用具体数据模拟执行流程
7.2 解决原则
原则1:数据一致性
- 使用的数据必须是最新计算的
- 避免跨函数、跨循环的依赖
原则2:原子操作
- 相关的操作应该在同一个代码块内完成
- 保存旧值→计算新值→比较判断,应该连续执行
原则3:代码复用 vs 时序要求
- 本例中
Seg_Proc的四舍五入逻辑被复制到Key_Proc - 虽然有代码重复,但保证了时序正确
- 这是时序优先于复用的典型案例
7.3 调试技巧
- 添加详细注释:标记每步的作用
- 绘制时序图:可视化执行流程
- 表格化测试:用表格记录每个变量的变化
- 边界测试:测试等于阈值的情况
- 首次执行测试:检查初始值处理
7.4 类似场景
这类时序问题常见于:
- 传感器数据采集
- 状态机切换
- 中断与主循环的数据交互
- 多任务间的数据共享
核心原则:数据的生产和消费应该在同一个时间片内完成
8. 附加修复
在调试过程中,还发现并修复了其他问题:
8.1 返回模式0的Bug
// 修复前:条件过于严格
}else if((Key_Down == 11) && (Voltage_Input_Index == 4))
// 修复后:任何非模式0情况都能返回
}else if(Key_Down == 11)
8.2 循环内重复赋值
// 修复前
for(i = 0; i<4; i++)
{
Voltage_Input_Index = 0; // ❌ 循环4次
Voltage_Input[i] = 13;
}
// 修复后
Voltage_Input_Index = 0; // ✅ 移到循环外
for(i = 0; i<4; i++)
{
Voltage_Input[i] = 13;
}
9. 总结
本次调试的核心教训:
在实时系统中,数据的计算和使用必须同步。依赖异步更新的数据会导致时序错误。
修复方法:
- 将四舍五入逻辑从
Seg_Proc复制到确认代码块 - 虽然有代码重复,但保证了逻辑正确性
- 正确性优先于代码优雅性
调试日期:2026-01-27
文件:main.c
关键函数:Key_Proc (第63-101行)