单片机十五届省赛第一次

:memo: 第十五届蓝桥杯单片机省赛(大学组)

1. 题目核心考点梳理

1.1 硬件配置

  • 平台:CT107D (IAP15F2K61S2)

  • 频率:12MHz

  • 关键外设

    • NE555:产生频率信号(P3.4/T0)。

    • DS1302:读取实时时间(BCD码)。

    • PCF8591:DAC 输出电压。

    • 数码管 & LED:显示与报警。

    • 矩阵键盘:参数设置与界面切换。

1.2 核心功能模块

  1. 频率测量:使用定时器 T0 计数,T1 计时(1秒闸门)。

  2. 频率校准最终频率 = 测量频率 + 校准参数

    • 难点:校准值可能为负,导致结果为负。

    • 要求:结果为负时,数码管显示 LL,LED2 常亮。

  3. 参数设置

    • 超限参数:决定报警阈值和 DAC 输出斜率。

    • 校准参数:修正测量频率。

    • 要求:具备“生效机制”(退出设置界面时才生效)。

  4. 数据回显:记录最大频率及其发生时间。


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 个关键逻辑错误,务必背诵,防止考场再犯。

:cross_mark: 错误 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); // 记录时刻
    }
    

:cross_mark: 错误 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); 
    

:cross_mark: 错误 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;
    }
    

:cross_mark: 错误 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. 最终检查清单(上机前必看)

  1. [ ] 数码管消隐:所有判断是不是都用了 == 10

  2. [ ] BCD 码:底层到底转没转十进制?(转了用 /10,没转用 /16)。

  3. [ ] 变量区分:计算和报警时,是用 Mode1_0(真值)还是 Mode1_0_Ctrl(草稿)?

  4. [ ] 中断逻辑:L2 报警逻辑是不是放在了最外层?(不能被包在 if(Mode==0) 里)。

  5. [ ] 负数处理:校准减法前,是不是先判断了大小,防止 unsigned int 溢出?

  6. [ ] 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查找的

:cross_mark: 错误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);
}


:cross_mark: 错误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控制值


:cross_mark: 错误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;
}


:cross_mark: 错误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;
}