蓝桥杯第九届国赛代码错误总结
错误列表
1.
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 |
CH3 (RB2) | AD读取 + DA输出 | ||
| 0x41 | 0100 0001 |
CH1 (光敏) | AD读取 + DA输出 | ||
| 0x03 | 0000 0011 |
CH3 (RB2) | 仅AD读取RB2 | ||
| 0x01 | 0000 0001 |
CH1 (光敏) | 仅AD读取光敏 |
详细分析:
// 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电压(通道选择正确)
-
规范层面:
不必要地开启了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.
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)时用的是内存中的旧值 -
导致逻辑判断错误
修复方案
修复思想:
-
问题本质: 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.
本题按键长按检测的实现思路
题目要求 (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次
设计要点:
-
双标志位设计:
-
Led6_Flag: 控制是否进行计时 -
Count_800ms: 累计按下时长
-
-
中断中计时:
-
Timer1中断每1ms检查
Led6_Flag -
如果为1,则累加
Count_800ms -
限幅在800ms,防止溢出
-
-
按键处理中判断:
-
Key_Down: 启动计时 -
Count_800ms >= 800 && Key_Old == 6: 长按快速递增 -
Key_Up: 判断短按/长按,重置标志
-
-
短按和长按的区分:
-
短按:
Count_800ms < 800→ 单次递增 -
长按:
Count_800ms >= 800→ 已经连续递增过了,松开时不需要再加
-
-
递增速率控制:
-
Key_Proc()每20ms调度一次 -
长按时每20ms递增一次Param
-
速率 = 1000ms / 20ms = 50次/秒
-
-
重置时机:
-
必须在
Key_Up时重置Led6_Flag和Count_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判断 -
重置时机: 松开时清零,确保下次正确
-
限幅保护: 防止计时器溢出
总结
本次审查发现的问题类型:
-
硬件控制不规范 - PCF8591控制字节bit6多余使能DAC
-
内存同步问题 - EEPROM写入后未同步内存变量
-
逻辑过于复杂 - 过多的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
蓝桥杯第九届国赛代码错误总结