下降沿检测Bug调试记录

下降沿检测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 :cross_mark:用的是初始值
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 :cross_mark:用的是上次值
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 ✓检测到,但慢一拍

问题总结

  1. t1时刻:应该记录3.57V,但实际用的是0
  2. t3时刻:应该检测到3.57V→2.84V的下降沿(穿越3.00V),但因为用的是上次的3.57V和初始的0,未检测到
  3. 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 关键改进点

:white_check_mark: 改进1:保存顺序调整

// 原代码:先计算再保存(错误)
Voltage_Real = ...;
Voltage_Old = Voltage_Real;  // 保存的是本次值

// 新代码:先保存再计算(正确)
Voltage_Old = Voltage_Real;  // 保存上次值
Voltage_Real = ...;          // 计算本次值

:white_check_mark: 改进2:立即计算四舍五入

不等待Seg_Proc更新,在确认输入时立即处理:

// 复制Seg_Proc的四舍五入逻辑
Voltage_Real_Data[0-2] = Voltage_Data[0-2];
if(Voltage_Data[3] >= 5) { 进位处理... }

:white_check_mark: 改进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 :white_check_mark:立即计算
t2 输入2.844V确认 357→保存 [2,8,4,4] 4<5→不进位 284 (284<300)&&(357≥300)=True Count=1 :white_check_mark:检测到下降沿
t3 输入1.234V确认 284→保存 [1,2,3,4] 4<5→不进位 123 (123<300)&&(284≥300)=False Count=1 :white_check_mark:已在阈值下
t4 输入3.456V确认 123→保存 [3,4,5,6] 6≥5→进位 346 (346<300)&&(123≥300)=False Count=1 :white_check_mark:上升不计数
t5 输入2.789V确认 346→保存 [2,7,8,9] 9≥5→进位 279 (279<300)&&(346≥300)=True Count=2 :white_check_mark:再次检测到

验证结果

:white_check_mark: t1:立即计算出3.57V,不再依赖Seg_Proc
:white_check_mark: t2:正确检测到3.57V→2.84V的下降沿(穿越3.00V)
:white_check_mark: t3:已在阈值下,不计数
:white_check_mark: t4:上升穿越不计数(只检测下降沿)
:white_check_mark: 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 时序问题的识别

症状

  • 功能延迟响应(慢一拍)
  • 首次执行结果异常
  • 数据更新和使用不同步

排查方法

  1. 绘制数据流程图
  2. 标记每个变量的更新时机
  3. 跟踪主循环的执行顺序
  4. 用具体数据模拟执行流程

7.2 解决原则

原则1:数据一致性

  • 使用的数据必须是最新计算的
  • 避免跨函数、跨循环的依赖

原则2:原子操作

  • 相关的操作应该在同一个代码块内完成
  • 保存旧值→计算新值→比较判断,应该连续执行

原则3:代码复用 vs 时序要求

  • 本例中Seg_Proc的四舍五入逻辑被复制到Key_Proc
  • 虽然有代码重复,但保证了时序正确
  • 这是时序优先于复用的典型案例

7.3 调试技巧

  1. 添加详细注释:标记每步的作用
  2. 绘制时序图:可视化执行流程
  3. 表格化测试:用表格记录每个变量的变化
  4. 边界测试:测试等于阈值的情况
  5. 首次执行测试:检查初始值处理

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行)