蓝桥杯第九届国赛代码错误总结

蓝桥杯第九届国赛代码错误总结

错误列表

1. :warning:PCF8591控制字节理解不清导致使用不规范

题目要求:

测量竞赛板上 RB2 输出的电压信号

错误代码:

void AD_DA()
{
    Ad_Read(0x43);  // 第一次读取(丢弃,用于稳定)
    AD_3_Data_10x = (float)Ad_Read(0x43) / 51.0f * 10;
}

PCF8591控制字节详解:

PCF8591是8位A/D和D/A转换芯片,控制字节格式如下:

Bit7  Bit6  Bit5  Bit4  Bit3  Bit2  Bit1  Bit0
  0   DAOE  AIN2  AIN1  AIN0   0     CH1   CH0

各位含义:

  • Bit7: 保留位,固定为0

  • Bit6 (DAOE): D/A输出使能位

    • 1 = 使能DAC输出(AOUT引脚输出模拟电压)

    • 0 = 禁用DAC输出

  • Bit5-Bit3 (AIN2-AIN0): A/D输入模式选择(通常用默认值000)

  • Bit2: 保留位,固定为0

  • Bit1-Bit0 (CH1-CH0): A/D通道选择

    • 00 = 通道0 (AIN0)

    • 01 = 通道1 (AIN1) - 光敏电阻

    • 10 = 通道2 (AIN2)

    • 11 = 通道3 (AIN3) - RB2电压(本题使用)

常用控制字节对比:

控制字节 二进制 DAC使能 AD通道 适用场景 是否合适本题
0x43 0100 0011 :white_check_mark: 使能 CH3 (RB2) AD读取 + DA输出 :warning: 能用但多余
0x41 0100 0001 :white_check_mark: 使能 CH1 (光敏) AD读取 + DA输出 :cross_mark: 通道错误
0x03 0000 0011 :cross_mark: 禁用 CH3 (RB2) 仅AD读取RB2 :white_check_mark: 最佳
0x01 0000 0001 :cross_mark: 禁用 CH1 (光敏) 仅AD读取光敏 :cross_mark: 通道错误

详细分析:

// 0x43 = 0100 0011b
Ad_Read(0x43);
// ├─ Bit6 = 1 → DAC使能 ← ⚠️ 本题不需要DA输出!
// └─ Bit[1:0] = 11 → 选择CH3 (RB2) ← ✓ 通道正确
​
// 0x03 = 0000 0011b
Ad_Read(0x03);
// ├─ Bit6 = 0 → DAC禁用 ← ✓ 不需要DA时应禁用
// └─ Bit[1:0] = 11 → 选择CH3 (RB2) ← ✓ 通道正确

问题分析:

使用0x43的影响:

  • 功能层面: ✓ 能正确读取RB2电压(通道选择正确)

  • 规范层面: :cross_mark: 不必要地开启了DAC输出功能

  • 硬件层面: AOUT引脚可能输出未定义的电压(没有写DA值)

  • 竞赛评分: 一般不扣分,但不够专业

正确代码:

void AD_DA()
{
    Ad_Read(0x03);  // ✓ 仅读取AIN3 (RB2),不使能DAC
    AD_3_Data_10x = (float)Ad_Read(0x03) / 51.0f * 10;
}

如果题目要求同时使用DA功能:

void AD_DA()
{
    // 读取AD
    Ad_Read(0x43);
    AD_3_Data_10x = (float)Ad_Read(0x43) / 51.0f * 10;
​
    // 输出DA
    Da_Write(some_value);  // Da_Write内部会发送0x41控制字节
}

关键点:

  • 控制字节Bit6决定是否使能DAC输出

  • Bit[1:0]决定读取哪个AD通道

  • 仅读AD时应使用0x0X格式(Bit6=0)

  • 需要DA输出时使用0x4X格式(Bit6=1)

  • 功能正确 ≠ 代码规范,应该只开启需要的功能


2. :warning:EEPROM校验标志写入后未同步内存变量导致逻辑判断错误

题目要求 (3.4 存储功能):

通过 E2PROM 实现电压、频率、温度数据记录和电压阈值参数的存储功能,设备重新上电后,能够自动从 E2PROM 中载入全部参数。

错误代码:

void main()
{
    System_Init();
    while(rd_temperature() == 85);
​
    EEPROM_Read(&EEPROM_Temp, 22, 1);
    if(EEPROM_Temp == EEPROM_Lock)  // 校验通过(==9)
    {
        // 读取保存的电压阈值
        EEPROM_Read(&Param, 9, 1);
    }
    else  // 首次上电
    {
        EEPROM_Write(&EEPROM_Lock, 22, 1);  // 写了标志
        // ❌ 但EEPROM_Temp还是旧值(0xFF),没有更新!
    }
​
    // ... 其他初始化 ...
}
​
// S7按键处理
case 7://设置按键
    if(Seg_Mode!=2)
        Seg_Mode=2;
    else if(Seg_Mode==2)
    {
        Seg_Mode=0;
        if(EEPROM_Temp == EEPROM_Lock)  // ❌ 用的是内存中的旧值!
        {
            EEPROM_Write(&Param,9,1);
        }
        else
        {
            EEPROM_Write(&EEPROM_Lock, 22, 1);  // 只写标志,Param没存!
        }
    }
    Led_Proc();
break;

错误原因:

核心问题: EEPROM和内存是两个独立的存储空间

EEPROM (非易失性)           内存变量 (易失性)
┌──────────────┐           ┌──────────────┐
│ 地址22: 0xFF │           │ EEPROM_Temp  │
│   (空芯片)    │           │   = 0xFF     │
└──────────────┘           └──────────────┘
        │                          │
        │ EEPROM_Write(&EEPROM_Lock, 22, 1)
        ↓                          │
┌──────────────┐                  │
│ 地址22: 0x09 │ ← ✓ EEPROM更新了  │
└──────────────┘                  │
                                  ↓
                         ┌──────────────┐
                         │ EEPROM_Temp  │
                         │   = 0xFF     │ ← ❌ 内存没更新!
                         └──────────────┘

问题场景演示:

首次上电操作流程:
1. main()中读取EEPROM地址22
   → EEPROM_Temp = 0xFF (空EEPROM默认值)
​
2. EEPROM_Temp (0xFF) != EEPROM_Lock (9)
   → 走else分支
​
3. EEPROM_Write(&EEPROM_Lock, 22, 1)
   → EEPROM地址22现在有标志了 (值为9)
   → ❌ 但内存中EEPROM_Temp = 0xFF,没有同步!
​
4. 用户进入设置界面,调整阈值到3.5V
​
5. 按S7退出
   → 判断EEPROM_Temp (0xFF) != EEPROM_Lock (9)
   → 走else分支
   → 只写标志,Param (3.5V) 没保存!💥
​
6. 断电重启
   → 读取Param地址9 → 0xFF (255)
   → 阈值变成25.5V,显示和报警全乱!

根本原因:

  • EEPROM_Write()只修改了EEPROM芯片中的值

  • 内存变量EEPROM_Temp并不会自动更新

  • 后续代码判断if(EEPROM_Temp == EEPROM_Lock)时用的是内存中的旧值

  • 导致逻辑判断错误


:wrench: 修复方案

修复思想:

  • 问题本质: EEPROM和内存不同步

  • 解决方向1: 写EEPROM后立即同步内存变量(修复A)

修复A:main() else 分支补一行同步,其余部分再把多余的else分支去掉

适用场景: 保留原有逻辑结构,最小化修改

修改位置: main.c:489-493

else  // 首次上电
{
    EEPROM_Write(&EEPROM_Lock, 22, 1);  // 写EEPROM
    EEPROM_Temp = EEPROM_Lock;  // ✓ 同步内存变量!
}

修复效果:

首次上电操作流程(修复后):
1. main()中读取EEPROM地址22
   → EEPROM_Temp = 0xFF

2. EEPROM_Temp (0xFF) != EEPROM_Lock (9)
   → 走else分支

3. EEPROM_Write(&EEPROM_Lock, 22, 1)
   → EEPROM地址22 = 9 ✓

4. EEPROM_Temp = EEPROM_Lock
   → EEPROM_Temp = 9 ✓ 内存已同步!

5. 用户调整阈值到3.5V,按S7退出
   → 判断EEPROM_Temp (9) == EEPROM_Lock (9) ✓
   → 走if分支 → Param保存成功!✓

关键点总结:

  • EEPROM和内存是独立的,写EEPROM后必须同步内存变量

  • 简化逻辑比复杂判断更可靠

  • 无条件执行消除边界情况

  • 防御性编程:宁可重复写入,不要遗漏保存


3. :light_bulb: 本题按键长按检测的实现思路

题目要求 (3.3 按键功能):

S6 "回显"按键在阈值设置界面下定义为阈值调整功能,每次按下 S6,电压阈值增加 0.1V,长按 0.8 秒以上,可实现快速增加功能

设计目标:

  • 检测S6是否长按超过0.8秒

  • 短按:单次+0.1V

  • 长按:连续快速+0.1V

核心思路: 双计时器机制

                    按下S6               按住800ms              松开S6
                      ↓                     ↓                    ↓
Key_Down=6 ─────→ Led6_Flag=1 ─────→ Count_800ms≥800 ─────→ Key_Up=6
                      │                     │                    │
                      │                     │                    │
                  启动计时           进入快速递增模式          判断类型

实现代码:

// 全局变量
idata unsigned char Led6_Flag = 0;   // S6按键计时使能标志
idata unsigned int Count_800ms = 0;  // 0.8s计时器

// Key_Proc() - 按键处理函数 (每20ms调度一次)
void Key_Proc()
{
    Key_Val = Key_Read();
    Key_Down = Key_Val & (Key_Val ^ Key_Old);
    Key_Up = ~Key_Val & (Key_Val ^ Key_Old);
    Key_Old = Key_Val;

    // 1. 检测S6按下 → 启动计时
    if(Key_Down == 6)
    {
        Led6_Flag = 1;  // 使能计时标志
    }

    // 2. 检测长按超过0.8s → 进入快速递增模式
    if(Count_800ms >= 800)  // 达到800ms
    {
        if(Key_Old == 6)  // 按键还在按着
        {
            if(Seg_Mode == 2)  // 在阈值设置界面
            {
                Param++;  // 每20ms递增一次
                if(Param == 51)
                    Param = 1;
            }
        }
    }

    // 3. 检测S6松开 → 判断是短按还是长按
    if(Key_Up == 6)
    {
        if(Count_800ms < 800)  // 短按(按下时间<0.8s)
        {
            if(Seg_Mode == 2)  // 阈值设置界面 → 单次递增
            {
                Param++;
                if(Param == 51)
                    Param = 1;
            }
            else  // 非阈值设置界面 → 进入回显界面
            {
                Seg_Mode = 1;
                // 读取EEPROM回显数据...
            }
        }
        // 长按(按下时间≥0.8s)→ 已经在步骤2中连续递增了,不需要额外操作

        // 重置计时器和标志
        Led6_Flag = 0;
        Count_800ms = 0;
    }
}

// Timer1中断 (每1ms执行一次)
void Timer1_Isr(void) interrupt 3
{
    uwTick++;

    // ... 数码管扫描 ...

    // 按键长按计时
    if(Led6_Flag == 1)  // 只有S6按下时才计时
    {
        Count_800ms++;
        if(Count_800ms >= 800)
            Count_800ms = 800;  // 限幅,防止溢出
    }
}

时序分析:

场景1: 短按(按下300ms后松开)

时间轴:  0ms    300ms
         │      │
Key_Down=6      Key_Up=6
         │      │
         ↓      ↓
Led6_Flag=1    Count_800ms=300 < 800
Count_800ms开始累加
                ↓
              判定为短按
              Param += 1 (单次)
              Led6_Flag=0, Count_800ms=0

场景2: 长按(按下1200ms后松开)

时间轴:  0ms    800ms  820ms  840ms  1200ms
         │       │      │      │      │
Key_Down=6      ≥800   ≥800   ≥800   Key_Up=6
         │       │      │      │      │
         ↓       ↓      ↓      ↓      ↓
Led6_Flag=1   Param++  Param++ Param++ 判定为长按
Count_800ms累加                       Led6_Flag=0
                                      Count_800ms=0

总共递增次数: (1200-800)/20 = 20次

设计要点:

  1. 双标志位设计:

    • Led6_Flag: 控制是否进行计时

    • Count_800ms: 累计按下时长

  2. 中断中计时:

    • Timer1中断每1ms检查Led6_Flag

    • 如果为1,则累加Count_800ms

    • 限幅在800ms,防止溢出

  3. 按键处理中判断:

    • Key_Down: 启动计时

    • Count_800ms >= 800 && Key_Old == 6: 长按快速递增

    • Key_Up: 判断短按/长按,重置标志

  4. 短按和长按的区分:

    • 短按: Count_800ms < 800 → 单次递增

    • 长按: Count_800ms >= 800 → 已经连续递增过了,松开时不需要再加

  5. 递增速率控制:

    • Key_Proc()每20ms调度一次

    • 长按时每20ms递增一次Param

    • 速率 = 1000ms / 20ms = 50次/秒

  6. 重置时机:

    • 必须在Key_Up时重置Led6_FlagCount_800ms

    • 确保下次按键重新开始计时

优化建议:

如果觉得50次/秒太快,可以降低递增频率:

idata unsigned int Count_Fast_Inc = 0;  // 快速递增间隔计数器

if(Count_800ms >= 800)
{
    if(Key_Old == 6)
    {
        if(Seg_Mode == 2)
        {
            Count_Fast_Inc++;
            if(Count_Fast_Inc >= 100)  // 每100ms递增一次
            {
                Count_Fast_Inc = 0;
                Param++;
                if(Param == 51)
                    Param = 1;
            }
        }
    }
}

关键点总结:

  • 双计时器机制: 标志位使能 + 时长累计

  • 中断计时: 精确到1ms

  • 短按/长按区分: 根据松开时的Count_800ms判断

  • 重置时机: 松开时清零,确保下次正确

  • 限幅保护: 防止计时器溢出


总结

本次审查发现的问题类型:

  1. 硬件控制不规范 - PCF8591控制字节bit6多余使能DAC

  2. 内存同步问题 - EEPROM写入后未同步内存变量

  3. 逻辑过于复杂 - 过多的if-else判断导致边界情况遗漏


学到的经验

1. 硬件芯片控制字节要精确理解

核心原则: 只开启需要的功能

// 错误: 盲目照抄,开启不需要的功能
Ad_Read(0x43);  // bit6=1, DAC使能了但不需要

// 正确: 理解每一位的作用
Ad_Read(0x03);  // bit6=0, 只读AD不开DAC

芯片手册阅读重点:

  • 控制字节每一位的含义

  • 不同功能组合的推荐值

  • 默认值和复位值

常见芯片控制字节:

  • PCF8591: bit6=DAC使能, bit[1:0]=通道选择

  • DS1302: bit0=读(1)/写(0), bit[7:1]=寄存器地址

  • AT24C02: 设备地址 + 读写位

2. 按键长按检测的通用模式

核心机制: 标志位使能 + 中断计时

// 按下 → 启动计时
if(Key_Down == target)
    timer_flag = 1;

// 中断 → 累计时长
if(timer_flag == 1)
    count_ms++;

// 超时 → 执行长按动作
if(count_ms >= threshold)
    long_press_action();

// 松开 → 判断类型并重置
if(Key_Up == target)
{
    if(count_ms < threshold)
        short_press_action();
    timer_flag = 0;
    count_ms = 0;
}

适用场景:

  • 长按进入设置

  • 长按快速调节

  • 双击检测

  • 组合按键


复盘检查清单

在蓝桥杯单片机比赛中,务必检查:

硬件控制规范:

  • PCF8591控制字节bit6根据是否需要DA输出决定

  • 仅读AD时使用0x0X格式,需要DA时使用0x4X格式

  • 通道选择bit[1:0]与实际接线对应

EEPROM逻辑:

  • 写入EEPROM后立即同步对应的内存变量

  • 优先使用简化的无条件保存逻辑

  • 避免过多依赖内存变量判断的if-else分支

按键检测:

  • 长按检测使用标志位使能 + 中断计时模式

  • 松开时重置计时器和标志位

  • 短按/长按根据计时器值区分

代码规范:

  • 简化逻辑,减少if-else嵌套

  • 能无条件执行的不要加判断

  • 防御性编程,宁可重复也不要遗漏


生成时间: 2026-02-20
蓝桥杯第九届国赛代码错误总结