第四周 过渡模拟学习笔记

LED彩灯程序错误点分析

本文对比了原始代码与范例程序,总结了三处关键差异及错误所在,并给出润色后的修改建议,以帮助理解正确逻辑。


1. 数据显示逻辑

1.1 原始代码

if(Seg_Mode == 0 && Led_Flag == 1) // 处于运行状态界面且系统暂停
{
    if(Key_Old == 4)
        Seg_Mode = 2;              // 长按4进入数据显示界面
}
if(Seg_Mode == 2)                  // 处于数据显示界面
{
    if(Key_Up == 4)
        Seg_Mode = 0;              // 松开4返回运行界面
}

case 0: // 运行状态界面
    Seg_Buf[0] = Led_Flag ? 11 : 12;        // 运行状态
    Seg_Buf[1] = Led_Mode + 1;              // 运行模式
    Seg_Buf[2] = Led_Time[Led_Mode] / 1000 % 10;
    Seg_Buf[3] = Led_Time[Led_Mode] / 100 % 10;
    Seg_Buf[4] = Led_Time[Led_Mode] / 10 % 10;
    Seg_Buf[5] = Led_Time[Led_Mode] % 10;
    if(Seg_Buf[2] == 0) Seg_Buf[2] = 10;    // 高位管熄灭
    break;

case 2: // 数据显示界面
    Seg_Buf[0] = 0;                         // D
    Seg_Buf[1] = 12;                        // A
    Seg_Buf[2] = 13;                       // -
    Seg_Buf[3] = Led_Mode + 1;              // 运行模式
    Seg_Buf[4] = Led_Data / 10 % 10;
    Seg_Buf[5] = Led_Data % 10;
    break;

1.2 范例程序

if(System_Flag == 0 && Key_Old == 4)   // 在系统停止状态下长按S4跳转到数据显示界面
    Data_Disp_Flag = 1;                // 显示数据
else
    Data_Disp_Flag = 0;                // 不显示数据

case 0: // 运行状态界面
    if(Data_Disp_Flag == 0)
    {
        // 运行状态
        Seg_Buf[0] = System_Flag ? 11 : 12; // 11-S启动 12-R停止
        // 运行模式
        Seg_Buf[1] = Led_Mode + 1;
        // 流转间隔
        Seg_Buf[2] = Led_Time_Dat[Led_Mode] / 1000 % 10;
        Seg_Buf[3] = Led_Time_Dat[Led_Mode] / 100 % 10;
        Seg_Buf[4] = Led_Time_Dat[Led_Mode] / 10 % 10;
        Seg_Buf[5] = Led_Time_Dat[Led_Mode] % 10;
        if(Seg_Buf[2] == 0) Seg_Buf[2] = 10; // 高位管熄灭
    }
    else
    {
        // 模式标识符
        Seg_Buf[0] = 0;
        Seg_Buf[1] = 12;
        // 运行模式编号
        Seg_Buf[2] = 13;                    // -
        Seg_Buf[3] = Led_Mode + 1;
        // Led 灯数据
        Seg_Buf[4] = Led_Disp_Data / 10 % 10;
        Seg_Buf[5] = Led_Disp_Data % 10;
    }
    break;

1.3 差异与问题分析

对比项 原始代码 范例程序
模式切换 使用 Seg_Mode 状态机,分为 0(运行)、2(数据) 使用独立标志位 Data_Disp_Flag,在运行界面内分支
切换条件 长按进入数据显示,松开返回运行 长按进入数据显示,否则一直显示运行界面
潜在问题 两个独立 if 可能导致逻辑混乱,尤其当 Seg_Mode 被意外修改时 结构清晰,一个 if-else 完成状态切换

主要问题:原始代码将数据显示界面作为独立 case,需要额外管理 Seg_Mode 的切换,容易引入状态不一致。范例则仅用一个标志位控制显示内容,所有显示逻辑都集中在 case 0 中,更简洁可靠。


2. LED 数据读取

2.1 原始代码

while((~ucLed & (0x01 << i)) != 0)
    i++;
if(Led_Mode < 2) // 处于前两种模式
    Led_Data = i + 1;
else
    Led_Data = (i+1)*10 + (8 - (i+1));

2.2 范例程序

i = 0;
while((~ucLed & (0x01 << i)) == 0)  // 判断0x01左移i位是否等于ucLed,==优先级大于&
    i++;                            // 不等于则+1
if(Led_Mode < 2)                    // 处于前两种运行模式
    Led_Data = i + 1;
else
    Led_Data = (8 - i) + (i + 1)*10; // 高位为i+1,低位为8-i

2.3 差异与问题分析

循环条件完全相反:

  • 原始:(~ucLed & (0x01 << i)) != 0 表示当位为 1(即 ucLed 对应位为 0,LED 亮)时继续循环,直到遇到 0(灭)停止,找到的是第一个灭的位。
  • 范例:(~ucLed & (0x01 << i)) == 0 表示当位为 0(即 ucLed 对应位为 1,灭)时循环,直到遇到 1(亮)停止,正确找到第一个亮的位。

后两种模式计算错误:

  • 原始:(i+1)*10 + (8 - (i+1)) = 9i + 17
  • 范例:(8 - i) + (i + 1)*10 = 9i + 18

两者相差 1,原始计算了错误的数据。

建议:直接采用范例的逻辑,确保寻找第一个亮灯位置,并正确计算显示值。


3. LED 流转处理逻辑

3.1 原始代码

if(Timer == Led_Time[Led_Mode])  // 当计时到达当前模式的流转间隔
{
    Timer = 0;                   // 复位
    switch(Led_Mode)
    {
        case 0: // 模式1
            if(ucLed == 0x7e)    // 从模式4跳转到模式1
                ucLed = 0xfe;    // 复位
            else
            {
                ucLed = _crol_(ucLed, 1);
                P1 = ucLed;
                if(ucLed == 0x7f)       // 流转到最后一个灯
                    Led_Mode = 1;        // 跳转到模式2
            }
            break;
        case 1: // 模式2
            ucLed = _cror_(ucLed, 1);
            P1 = ucLed;
            if(ucLed == 0xfe)            // 流转到最后一个灯
                Led_Mode = 2;            // 跳转到模式3
            break;
        case 2: // 模式3
            ucLed = Led_Disp[Led_Time_Index];
            P1 = ucLed;
            if(++Led_Time_Index == 4)
            {
                Led_Mode = 3;           // 跳转到模式4
                Led_Time_Index = 3;
            }
            break;
        case 3: // 模式4
            ucLed = Led_Disp[Led_Time_Index];
            P1 = ucLed;
            if(--Led_Time_Index == 255)
            {
                Led_Mode = 1;           // 跳转到模式1(错误,应为0)
                Led_Time_Index = 0;
            }
            break;
    }
}

3.2 范例程序

P1 = ucLed;  // 每次更新前先输出当前状态
if(Sys_Tick == Led_Time_Dat[Led_Mode])  // 当计时器走到当前模式的流转时间间隔时
{
    Sys_Tick = 0;                       // 计时器复位
    switch(Led_Mode)
    {
        case 0: // 模式1
            if(ucLed == 0x7e)           // 从模式4进入模式1
                ucLed = 0xfe;           // 复位至模式1初始情况
            else
            {
                ucLed = _crol_(ucLed, 1); // 左移
                if(ucLed == 0x7f)
                    Led_Mode = 1;       // 当灯8亮时,进入模式2
            }
            break;
        case 1: // 模式2
            ucLed = _cror_(ucLed, 1);   // 右移
            if(ucLed == 0xfe)
                Led_Mode = 2;           // 当灯1亮时,进入模式3
            break;
        case 2: // 模式3
            ucLed = ucLed_Dat[ucLed_Dat_Index]; // 从外往里
            if(++ucLed_Dat_Index == 4)
            {
                Led_Mode = 3;           // 进入模式4
                ucLed_Dat_Index = 3;    // 指针复位至模式4初始值
            }
            break;
        case 3: // 模式4
            ucLed = ucLed_Dat[ucLed_Dat_Index]; // 从里往外
            if(--ucLed_Dat_Index == 255)
            {
                Led_Mode = 0;           // 进入模式1
                ucLed_Dat_Index = 0;    // 指针复位至模式3初始值
            }
            break;
    }
}

3.3 差异与问题分析

对比项 原始代码 范例程序
输出时机 在每个 case 内部单独赋值 P1 = ucLed,重复且易遗漏 在 switch 之前统一执行 P1 = ucLed,保证每次更新后立即输出
模式4跳转 Led_Mode = 1(跳转到模式2,错误) Led_Mode = 0(跳转到模式1,正确)
变量命名 Led_Time, Led_Data, Led_Time_Index 等 Led_Time_Dat, Led_Disp_Data, ucLed_Dat_Index 等
可读性与维护 重复代码多,状态转移易错 结构清晰,状态转移明确

主要问题:

  1. 输出位置:原始代码每处修改 ucLed 后都要手动写 P1 = ucLed,容易忘记;范例统一在每次进入前输出上一次的 ucLed,修改 ucLed 后在下一次循环输出,减少了重复和遗漏。
  2. 模式4跳转错误:原始代码 Led_Mode = 1 会进入模式2(case 1),而正确应回到模式1(case 0),范例修正了这一点。
  3. 变量命名规范:范例使用更描述性的名称,如 Sys_Tick 替代 Timer,ucLed_Dat_Index 替代 Led_Time_Index,增强了可读性。

4. 总结

本文通过对比原始代码与范例程序,发现原始代码主要存在以下错误:

  • 数据显示:状态管理混乱,建议使用独立标志位代替多状态机。
  • LED 数据读取:循环条件写反,导致找到了错误的位置;后两种模式数据计算有误。
  • LED 流转处理:模式4跳转目标错误,且输出逻辑分散,容易遗漏。

电压采集器学习笔记

1. 上电后数码管显示异常

现象:
上电后,后三个数码管仅短暂显示“-”,随后全部熄灭。


2. C 语言类型转换写法错误

2.1 原始代码

#define u8 unsigned char
#define u16 unsigned int
​
// 电压显示界面
case 1:
    Seg_Buf[1] = Seg_Buf[2] = 10; // 固定熄灭
    Seg_Buf[0] = 12;              // U
    Seg_Buf[3] = u8(Voltage);     // 个位(错误写法)
    Seg_Buf[4] = u16(Voltage*1000) / 100 % 10;
    Seg_Buf[5] = u16(Voltage*1000) / 10 % 10;
    break;

2.2 错误分析

C 语言中类型转换必须使用 (类型)表达式 的格式,即括号包裹类型名。写成 u8(Voltage) 不符合标准 C 语法,部分编译器可能扩展支持,但不建议使用。

2.3 修正代码

Seg_Buf[3] = (u8)(Voltage); // 个位
Seg_Buf[4] = (u16)(Voltage*1000) / 100 % 10;
Seg_Buf[5] = (u16)(Voltage*1000) / 10 % 10;

3. S11 按键逻辑冲突

3.1 原始代码

case 11: // 电压采集按键
    if (Seg_Mode == 0 && Seg_Input_Index >= 4) // 处于采集界面且输入完成
    {
        Voltage = (Seg_Input[0]*1000 + Seg_Input[1]*100 + Seg_Input[2]*10 + Seg_Input[3] + 5) / 1000.0;
        if (Voltage >= 0 && Voltage < 11) // 电压输入符合规范
            Seg_Mode = 1;                 // 跳转电压显示界面
        else
        {
            for (i = 0; i < 4; i++)
                Seg_Input[i] = 11;
            Seg_Input_Index = 0;
        }
    }
​
    if (Seg_Mode > 0)
    {
        for (i = 0; i < 4; i++)
            Seg_Input[i] = 11;
        Seg_Input_Index = 0;
        Seg_Mode = 0;
    }
    break;

3.2 问题现象c

输入完成并合规后,按下 S11 会立即重新进入输入状态,无法停留在显示界面。

3.3 逻辑分析

第一次按下 S11(假设初始 Seg_Mode = 0):

  • 第一个 if 条件 Seg_Mode == 0 && Seg_Input_Index >= 4 可能不成立(因为尚未输入完成),跳过。

  • 第二个 if 条件 Seg_Mode > 0 也不成立,跳过。

  • 因此第一次按下按键时可能无任何效果。

输入完成后再次按下 S11:

  • 此时 Seg_Mode == 0 且 Seg_Input_Index >= 4,进入第一个 if。

  • 计算 Voltage,若合法则将 Seg_Mode 设为 1(显示界面)。

  • 紧接着执行第二个 if,因为 Seg_Mode > 0 成立,执行清空输入、将 Seg_Input_Index 清零,并将 Seg_Mode 重新置为 0。

  • 最终效果:电压虽然保存,但显示模式被立即取消,回到采集界面,表现为“又进行重新输入”。

3.4 解决思路

将两个 if 改为 if-else 或使用状态标志,避免连续执行:

if (Seg_Mode == 0 && Seg_Input_Index >= 4)
{
    // 处理输入并切换模式
}
else if (Seg_Mode > 0)
{
    // 退出显示模式
}

也可以在第一个 if 内设置 Seg_Mode 后直接 break 或使用 return,但需注意 break 通常用于跳出 switch,逻辑需谨慎。


4. 高位管熄灭逻辑

4.1 原始代码

i = 0;
while (Seg_Buf[i] == 0 && i < 5)
{
    Seg_Buf[i] = 10;
    i++;
}

范例:

u8 j = 1;
while (Seg_Buf[j] == 0)
{
    Seg_Buf[j] = 10;
    if (++j == 5) break;
}

4.2 功能说明

用于熄灭高位无效的 0,例如将 0 0 3 5 显示为 3 5。


小结

本文记录了电压采集器调试中遇到的四个问题:上电显示异常、类型转换错误、按键逻辑冲突以及高位熄灭处理。重点在于 C 语言类型转换格式 和 状态机逻辑顺序,需在编程时特别注意。