第十五届蓝桥杯单片机省赛(大学组)
1. 题目核心考点梳理
1.1 硬件配置
-
平台:CT107D (IAP15F2K61S2)
-
频率:12MHz
-
关键外设:
-
NE555:产生频率信号(P3.4/T0)。
-
DS1302:读取实时时间(BCD码)。
-
PCF8591:DAC 输出电压。
-
数码管 & LED:显示与报警。
-
矩阵键盘:参数设置与界面切换。
-
1.2 核心功能模块
-
频率测量:使用定时器 T0 计数,T1 计时(1秒闸门)。
-
频率校准:
最终频率 = 测量频率 + 校准参数。-
难点:校准值可能为负,导致结果为负。
-
要求:结果为负时,数码管显示
LL,LED2 常亮。
-
-
参数设置:
-
超限参数:决定报警阈值和 DAC 输出斜率。
-
校准参数:修正测量频率。
-
要求:具备“生效机制”(退出设置界面时才生效)。
-
-
数据回显:记录最大频率及其发生时间。
2. 核心逻辑与代码实现
2.1 频率测量与校准(核心难点)
逻辑图解:钱包与罚款
为了解决 unsigned int 无法存储负数的问题,我们将校准逻辑分为两类:
-
正数校准(发奖金):直接相加。
-
负数校准(交罚款):先判断“钱包(测量值)”够不够“罚款(校准值绝对值)”。
-
够扣 → 正常减法。
-
不够扣 → 破产(负数),显示
LL。
-
代码实现(Timer1Isr 中):
C
// 1. 将校准参数转换为正数(绝对值),必须用 unsigned int 防止溢出
unsigned int Para_Temp = -Seg_Show_Mode1_1;
if(Seg_Show_Mode1_1 < 0) // 情况:校准值为负
{
if(Para_Temp > Freq_Real) // 绝对值比较:罚款 > 钱包
{
Freq_Error = 1; // 标记错误 -> 显示 LL
Freq_Cal = 0; // 归零防止溢出
Freq_Wring = 0; // 错误状态下不报超限
}
else // 钱包够扣
{
Freq_Error = 0;
Freq_Cal = Freq_Real - Para_Temp; // 正常减法
Freq_Wring = (Freq_Cal > Seg_Show_Mode1_0); // 判断是否超限
}
}
else // 情况:校准值为正
{
Freq_Error = 0;
Freq_Cal = Freq_Real + Seg_Show_Mode1_1; // 直接加
Freq_Wring = (Freq_Cal > Seg_Show_Mode1_0); // 判断是否超限
}
2.2 DS1302 时间读取与 BCD 码
底层原理:
-
DS1302 芯片内部寄存器存储的是 BCD 码(十六进制形式的十进制数,如
0x59代表 59秒)。 -
情况 A(常见):底层驱动只读不转。显示时需用
/16和%16分离。 -
情况 B(本题驱动):底层驱动已包含
temp / 16 * 10 + temp % 16转换公式。-
结论:读取到的
ucRtc已经是十进制。 -
显示:必须使用
/10和%10。
-
数据回显逻辑(修复版):
C
// 只有在频率正常且创新高时,才更新最大值
// ❌ 错误写法:if(Freq_Cal > Freq_Max) ... (导致错误值被记录)
// ✅ 正确写法:
if(!Freq_Error && Freq_Cal > Freq_Max)
{
Freq_Max = Freq_Cal;
// 直接复制时间数组,不要在显示函数里调 Read_Rtc()!
Freq_Max_Rtc[0] = ucRtc[0];
Freq_Max_Rtc[1] = ucRtc[1];
Freq_Max_Rtc[2] = ucRtc[2];
}
2.3 数码管消隐逻辑(易错点)
规则:
-
数码管数组
Seg_Buf中,10代表熄灭,0代表显示数字0。 -
高位消隐必须从高到低连锁判断。
代码模板:
C
// 万位:是0就灭(10)
Seg_Buf[3] = (Freq_Cal/10000%10 == 0) ? 10 : Freq_Cal/10000%10;
// 千位:如果万位灭了(10) 且 千位也是0,则灭
// ❌ 错误写法:(Seg_Buf[3] == 0) ...
// ✅ 正确写法:(Seg_Buf[3] == 10) ...
Seg_Buf[4] = ((Freq_Cal/1000%10 == 0) && (Seg_Buf[3] == 10)) ? 10 : Freq_Cal/1000%10;
3. 实战纠错记录(自我修正)
以下是在调试过程中发现的 4 个关键逻辑错误,务必背诵,防止考场再犯。
错误 1:更新最大值未检查错误标志
-
位置:
main.c:114-118 -
现象:数码管显示错误的中间值(如
HF 152),时间回显乱码(如HA 000045)。 -
原因:当
Freq_Error为真(LL状态)时,Freq_Cal可能为 0 或计算过程中的无效值,此时不应更新最大记录。 -
修正:
C
if(!Freq_Error && Freq_Cal > Freq_Max) // 增加 !Freq_Error 检查 { Freq_Max = Freq_Cal; Read_Rtc(Freq_Max_Rtc); // 记录时刻 }
错误 2:使用了错误的限制参数变量
-
位置:
main.c:316 -
现象:DAC 输出电压不对,或者报警逻辑混乱。
-
原因:在运行逻辑(中断、DA输出)中使用了
_Ctrl结尾的变量(这是参数设置时的“草稿”),导致参数还没确认保存就生效了,或者混用了旧值。 -
修正:
C
// ❌ Freq_Wring = (Freq_Cal > Seg_Show_Mode1_0_Ctrl); // ✅ 必须使用生效后的“正式参数” Freq_Wring = (Freq_Cal > Seg_Show_Mode1_0);
错误 3:多余的 else 破坏 LED 闪烁
-
位置:
main.c:327-331 -
现象:指示灯 L1 不闪烁,或者闪烁频率不对。
-
原因:
else分支写错位置,导致计数器Time_200ms在不等于 200 时被强制清零,永远无法累加到 200。 -
修正:
C
if(Seg_Show_Mode == 0) { if(++Time_200ms_Freq_Show == 200) // 只有到了200才处理 { Time_200ms_Freq_Show = 0; Led_Freq_Show ^= 1; } // ❌ 不要在这里写 else { Time... = 0; } } else // 只有切出频率界面,才强制灭灯 { Time_200ms_Freq_Show = 0; Led_Freq_Show = 0; }
错误 4:else 缩进与配对错误
-
位置:
main.c:341-345 -
现象:L2 报警灯逻辑异常(该亮不亮)。
-
原因:代码缩进误导视觉,实际
else可能与错误的if配对了。 -
修正:
C
if(Freq_Wring) { if(++Time_200ms_Freq_Wring == 200) { // ...闪烁逻辑 } } else // ✅ 严格对齐,确保它是 Freq_Wring 的 else { Time_200ms_Freq_Wring = 0; Led_Freq_Wring = 0; }
4. 关键变量定义备忘
在写代码前,先把变量分清楚,防止混淆:
C
// 1. 真实生效值(用于 Timer1Isr 和 AD_DA)
idata unsigned int Seg_Show_Mode1_0 = 2000; // 超限参数
idata int Seg_Show_Mode1_1 = 0; // 校准参数
// 2. 调节控制值(用于 Key_Proc 和 Seg_Proc 显示调节界面)
idata unsigned int Seg_Show_Mode1_0_Ctrl = 2000;
idata int Seg_Show_Mode1_1_Ctrl = 0;
// 3. 状态标志位
idata bit Freq_Error; // 频率错误 (LL)
idata bit Freq_Wring; // 频率超限 (报警)
idata bit Led_Freq_Show; // L1 闪烁状态位
idata bit Led_Freq_Wring; // L2 闪烁状态位
5. 最终检查清单(上机前必看)
-
[ ] 数码管消隐:所有判断是不是都用了
== 10? -
[ ] BCD 码:底层到底转没转十进制?(转了用
/10,没转用/16)。 -
[ ] 变量区分:计算和报警时,是用
Mode1_0(真值)还是Mode1_0_Ctrl(草稿)? -
[ ] 中断逻辑:L2 报警逻辑是不是放在了最外层?(不能被包在
if(Mode==0)里)。 -
[ ] 负数处理:校准减法前,是不是先判断了大小,防止
unsigned int溢出? -
[ ] LL 显示:
Freq_Error显示LL后,是不是加了else防止被后续数字覆盖?
#
//数据
idata unsigned int Seg_Show_Mode1_0=2000;//超限参数 1000~9000
idata int Seg_Show_Mode1_1=0//校准参数 -900~900
idata unsigned int Seg_Show_Mode1_0_Ctrl=2000;//超限参数控制值 1000~9000
idata int Seg_Show_Mode1_1_Ctrl=0//校准参数控制值 -900~900
设置参数要把真实值和控制值都定义出来
cc查找的
错误1:第114-118行 - 未检查频率错误标志
位置:main.c:114-118
原代码:
if(Freq_Cal>Freq_Max)
{
Freq_Max=Freq_Cal;
Read_Rtc(Freq_Max_Rtc);
}
问题:当频率检测出错时,Freq_Cal可能为0或旧值,此时不应该更新最大频率。这导致了显示HF 152(错误的中间值)而不是HF 250,以及时间显示HA 000045而不是HA 133445。
已修正为:
if(!Freq_Error && Freq_Cal>Freq_Max) // 增加!Freq_Error检查
{
Freq_Max=Freq_Cal;
Read_Rtc(Freq_Max_Rtc);
}
错误2:第316行 - 使用了错误的限制参数
位置:main.c:316
原代码:
Freq_Wring=(Freq_Cal>Seg_Show_Mode1_0);
问题:当校准值为正数时,应该使用当前生效的控制值Seg_Show_Mode1_0_Ctrl(可调整的参数),而不是已保存的Seg_Show_Mode1_0,这会导致判断超限时使用了错误的阈值。
已修正为:
Freq_Wring=(Freq_Cal>Seg_Show_Mode1_0_Ctrl); // 使用Ctrl控制值
错误3:第327-331行 - 多余的else块破坏LED闪烁
位置:main.c:327-331
原代码:
if(Seg_Show_Mode==0)
{
if(++Time_200ms_Freq_Show==200)
{
Time_200ms_Freq_Show=0;
Led_Freq_Show^=1;
}
else // ← 这个else是错误的
{
Time_200ms_Freq_Show=0;
Led_Freq_Show=0;
}
}
问题:这个else块会在计数器不等于200时立即清零,导致计数器永远无法累加到200,LED无法闪烁。这就是测试用例中"指示灯L1闪烁间隔error"的根本原因。
已修正为:
if(Seg_Show_Mode==0)
{
if(++Time_200ms_Freq_Show==200)
{
Time_200ms_Freq_Show=0;
Led_Freq_Show^=1;
}
}
else // ← 改为与Seg_Show_Mode==0配对
{
Time_200ms_Freq_Show=0;
Led_Freq_Show=0;
}
错误4:第341行 - else缩进位置错误
位置:main.c:341-345
原代码:
if(Freq_Wring)
{
if(++Time_200ms_Freq_Wring==200)
{
…
}
}
else // ← 缩进错位
{
…
}
问题:缩进错误导致else块与if语句不匹配,编译器可能将其与上一个if配对,导致逻辑错误。
已修正为:
if(Freq_Wring)
{
if(++Time_200ms_Freq_Wring==200)
{
…
}
}
else // ← 正确对齐
{
Time_200ms_Freq_Wring=0;
Led_Freq_Wring=0;
}