第十五届蓝桥杯单片机省赛 — 刷题总结
一、题目概要
- 芯片:STC IAP15F2K61S2,12MHz 晶振
- 外设:NE555方波输入、数码管×8、LED×8、矩阵键盘、DS1302实时时钟、PCF8591 DAC
- 核心功能:频率测量 + 参数调节 + DAC线性输出 + 历史最大值记录
二、系统架构
Timer0(计数模式)←── NE555方波 (P3.4)
Timer1(1ms中断)
├── 系统节拍 uwTick
├── 数码管动态扫描
├── L1/L2 LED控制(每200ms)
├── 1秒门控:读取Timer0 → Freq
└── freq_last / 历史最大值更新
主循环(时间片调度器)
├── Key_Proc 10ms
├── Seg_Proc 20ms
├── Get_Time 100ms(读DS1302)
└── AD_DA 150ms(DAC输出)
三、数码管显示模式(S4切换)
| Seg_Show_Mod | 界面 | 格式示例 |
|---|---|---|
| 0 | F(频率) | F _ _ _ 2 3 5 0(右对齐,前置空) |
| 1 | P(参数) | P1: P 1 _ _ 2 0 0 0 / P2: P 2 _ _ - 3 0 0 |
| 2 | 时钟 | 1 3 - 4 5 - 0 5 |
| 3 | H(历史) | HF: H F _ 2 3 5 0 / HT: H A 1 3 4 5 0 4 |
- S5:在 P界面切换 P1/P2;在 H界面切换 HF/HT
- S8/S9:参数界面调节数值(P1步进1000,P2步进100)
四、DAC 输出规则
| freq_last 范围 | DAC输出 | da_val |
|---|---|---|
| < 0(校准后频率为负) | 0V | 0 |
| 0 ~ 500 Hz | 1V | 51 |
| 500 ~ pf Hz(线性) | 1V ~ 5V | 51 + 4×51×(f-500)/(pf-500) |
| ≥ pf Hz | 5V | 255 |
五、LED 行为规则
| 条件 | L1 | L2 |
|---|---|---|
| Seg_Show_Mod == 0(F界面) | 每200ms翻转 | — |
| 其他界面 | 熄灭 | — |
| freq_last > pf | — | 每200ms翻转 |
| freq_last < 0 | — | 常亮 |
| 正常范围 | — | 熄灭 |
六、本题 Bug 汇总与修正
Bug 1 — 中间量类型溢出
位置:全局变量声明区
| 错误代码 | 修正代码 | |
|---|---|---|
| 问题 | idata unsigned char freq_delta_pf; |
idata unsigned int freq_delta_pf; |
| 问题 | idata unsigned char freq_delta; |
idata unsigned int freq_delta; |
原因:pf 最大 9000,pf - 500 = 8500,超出 unsigned char(0~255)范围,溢出后 DAC 计算完全错误。
Bug 2 — DAC 线性段公式错误
位置:AD_DA() 函数
// 错误:分子分母相同,结果永远是 4×51+51 = 255(5V)
da_val = 4*51*(freq_delta)/(freq_delta) + 51;
// 修正:除以 pf 与 500 的差值
da_val = 4*51*(freq_delta)/(freq_delta_pf) + 51;
原因:复制变量名时笔误,/freq_delta 应为 /freq_delta_pf。
Bug 3 — DAC 0Hz 条件判断遗漏
位置:AD_DA() 函数
// 错误:freq_last > 0 排除了 0Hz,导致 0Hz 时 da_val 不更新
}else if((freq_last <= 500) && (freq_last > 0)){
// 修正:包含 0Hz,0Hz 也输出 1V(题目要求 0~500Hz 均为 1V)
}else if((freq_last <= 500) && (freq_last >= 0)){
Bug 4 — S4 退出参数界面只保存一个参数
位置:Key_Proc() 函数
// 错误:只保存当前显示的那个参数
if(Seg_Show_Mod == 2)
{
if(!parameter_show_mod) pf = pf_temp;
else sta_val = sta_val_temp;
}
// 修正:同时保存两个参数
if(Seg_Show_Mod == 2)
{
pf = pf_temp;
sta_val = sta_val_temp;
}
原因:若在 P1 界面修改了 pf,切到 P2 后按 S4 退出,pf 的修改会丢失。
Bug 5 — HT 时间显示个位写成了十位
位置:Seg_Proc() case 3,back_show_mod == 1 分支
// 错误:/10 取的是十位,导致分钟、秒的个位显示错误
Seg_Buf[5] = uchft[1]/10;
Seg_Buf[7] = uchft[2]/10;
// 修正:%10 取个位
Seg_Buf[5] = uchft[1]%10;
Seg_Buf[7] = uchft[2]%10;
Bug 6 — DAC 线性段乘法溢出
位置:AD_DA() 函数
// 错误:204 × freq_delta,freq_delta 最大 9499
// 204 × 9499 = 1,937,796 > 65535,unsigned int 溢出,DAC 输出完全错误
da_val = 4*51*(freq_delta)/(freq_delta_pf) + 51;
// 修正:强制转 unsigned long 再做乘法,避免中间值溢出
da_val = (unsigned int)((unsigned long)204 * freq_delta / freq_delta_pf) + 51;
验证(freq_last=1000, pf=2000):
| 计算过程 | 结果 | |
|---|---|---|
| 错误版 | 204×500=102000 → 溢出为 36464 → 36464/1500+51=75 |
约 1.47V(错误) |
| 修正版 | (ulong)204×500=102000 → 102000/1500+51=119 |
约 2.33V(正确) |
原因:C51 中 unsigned int 为 16 位,乘法结果超过 65535 时高位被截断,必须在乘法前先强制转为 32 位 unsigned long。
Bug 7 — 中断 ISR 中全局变量 i 未初始化
位置:Timer1_Isr() 中断服务函数
// 错误:i 是全局变量,进入中断时值不确定(可能 >= 3,循环体永不执行)
for(i;i<3;i++)
{
uchft[i] = ucRtc[i];
}
// 修正:循环前显式初始化
for(i=0;i<3;i++)
{
uchft[i] = ucRtc[i];
}
原因:i 定义为全局变量(idata unsigned char i),在中断 ISR 里复用时必须手动赋初值,否则历史最大频率出现时间永远不会被记录。
七、完整代码
main.c
#include <STC15F2K60S2.H>
#include "LED.h"
#include "init.h"
#include "Seg.h"
#include "Key.h"
#include "ds1302.h"
#include "iic.h"
/* ==================== 全局变量定义 ==================== */
idata unsigned long int uwTick;
pdata unsigned char ucLed[8] = {0,0,0,0,0,0,0,0};
pdata unsigned char Seg_Buf[8] = {10,10,10,10,10,10,10,10};
idata unsigned char Seg_Pos = 0;
idata unsigned char Key_Val, Key_Old, Key_Up, Key_Down;
idata unsigned char ucRtc[3] = {12,23,45};
idata unsigned int Temperature_10x;
idata unsigned int Freq;
idata unsigned int Time_1s;
idata unsigned char Seg_Show_Mod;
bit parameter_show_mod;
idata int pf = 2000;
idata int pf_temp = 2000;
idata int sta_val = 0;
idata int sta_val_temp = 0;
idata unsigned int sta_val_abs = 0;
idata unsigned int sta_val_temp_abs = 0;
idata unsigned int hf;
bit back_show_mod;
idata unsigned char uchft[3] = {0,0,0};
idata int freq_last;
idata int freq_last_max;
idata unsigned int da_val;
idata unsigned int freq_delta_pf; // [修正Bug1] unsigned int,防止溢出
idata unsigned char da_val_uc;
idata unsigned int freq_delta; // [修正Bug1] unsigned int,防止溢出
idata unsigned int time200;
idata unsigned char i = 0;
/* ==================== 按键处理 ==================== */
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;
if(Key_Down == 4)
{
Seg_Show_Mod++;
if(Seg_Show_Mod == 4) Seg_Show_Mod = 0;
if(Seg_Show_Mod == 2) // [修正Bug4] S4退出参数界面同时保存两个参数
{
pf = pf_temp;
sta_val = sta_val_temp;
}
if(Seg_Show_Mod == 1) parameter_show_mod = 0;
if(Seg_Show_Mod == 3) back_show_mod = 0;
}
if(Seg_Show_Mod == 1)
{
if(Key_Down == 5) parameter_show_mod ^= 1;
if(!parameter_show_mod)
{
if(Key_Down == 8) { pf_temp += 1000; if(pf_temp > 9000) pf_temp = 9000; }
if(Key_Down == 9) { pf_temp -= 1000; if(pf_temp < 1000) pf_temp = 1000; }
}
else
{
if(Key_Down == 8) { sta_val_temp += 100; if(sta_val_temp > 900) sta_val_temp = 900; }
if(Key_Down == 9) { sta_val_temp -= 100; if(sta_val_temp < -900) sta_val_temp = -900; }
if(sta_val_temp < 0) sta_val_temp_abs = (-1) * sta_val_temp;
else sta_val_temp_abs = sta_val_temp;
}
}
if(Seg_Show_Mod == 3)
{
if(Key_Down == 5) back_show_mod ^= 1;
}
}
/* ==================== 数码管显示 ==================== */
void Seg_Proc()
{
switch(Seg_Show_Mod)
{
case 0: // F界面:频率显示
if(freq_last < 0)
{
Seg_Buf[0] = 11; // F
Seg_Buf[1] = 10; Seg_Buf[2] = 10; Seg_Buf[3] = 10;
Seg_Buf[4] = 10; Seg_Buf[5] = 10;
Seg_Buf[6] = 16; Seg_Buf[7] = 16; // 无效显示
}
else
{
Seg_Buf[0] = 11;
Seg_Buf[1] = 10;
Seg_Buf[2] = 10;
Seg_Buf[3] = (freq_last/10000) ? (freq_last/10000%10) : 10;
Seg_Buf[4] = (freq_last/1000) ? (freq_last/1000%10) : 10;
Seg_Buf[5] = (freq_last/100) ? (freq_last/100%10) : 10;
Seg_Buf[6] = (freq_last/10) ? (freq_last/10%10) : 10;
Seg_Buf[7] = freq_last % 10;
}
break;
case 1: // P界面:参数调节
if(!parameter_show_mod)
{
Seg_Buf[0] = 12; Seg_Buf[1] = 1; // P1
Seg_Buf[2] = 10; Seg_Buf[3] = 10;
Seg_Buf[4] = (pf_temp/1000) ? (pf_temp/1000%10) : 10;
Seg_Buf[5] = (pf_temp/100) ? (pf_temp/100%10) : 10;
Seg_Buf[6] = (pf_temp/10) ? (pf_temp/10%10) : 10;
Seg_Buf[7] = pf_temp % 10;
}
else
{
Seg_Buf[0] = 12; Seg_Buf[1] = 2; // P2
Seg_Buf[2] = 10; Seg_Buf[3] = 10;
Seg_Buf[4] = (sta_val_temp >= 0) ? 10 : 15; // 负号
Seg_Buf[5] = (sta_val_temp_abs/100) ? (sta_val_temp_abs/100%10) : 10;
Seg_Buf[6] = (sta_val_temp_abs/10) ? (sta_val_temp_abs/10%10) : 10;
Seg_Buf[7] = sta_val_temp_abs % 10;
}
break;
case 2: // 时钟界面
Seg_Buf[0] = ucRtc[0]/10%10;
Seg_Buf[1] = ucRtc[0]%10;
Seg_Buf[2] = 15; // -
Seg_Buf[3] = ucRtc[1]/10%10;
Seg_Buf[4] = ucRtc[1]%10;
Seg_Buf[5] = 15; // -
Seg_Buf[6] = ucRtc[2]/10%10;
Seg_Buf[7] = ucRtc[2]%10;
break;
case 3: // H界面:历史最大值
if(!back_show_mod)
{
Seg_Buf[0] = 13; Seg_Buf[1] = 11; // HF
Seg_Buf[2] = 10;
Seg_Buf[3] = (freq_last_max/10000) ? (freq_last_max/10000%10) : 10;
Seg_Buf[4] = (freq_last_max/1000) ? (freq_last_max/1000%10) : 10;
Seg_Buf[5] = (freq_last_max/100) ? (freq_last_max/100%10) : 10;
Seg_Buf[6] = (freq_last_max/10) ? (freq_last_max/10%10) : 10;
Seg_Buf[7] = freq_last_max % 10;
}
else
{
Seg_Buf[0] = 13; Seg_Buf[1] = 14; // HA(历史时刻)
Seg_Buf[2] = uchft[0]/10%10;
Seg_Buf[3] = uchft[0]%10;
Seg_Buf[4] = uchft[1]/10%10;
Seg_Buf[5] = uchft[1]%10; // [修正Bug5] %10 取个位
Seg_Buf[6] = uchft[2]/10%10;
Seg_Buf[7] = uchft[2]%10; // [修正Bug5] %10 取个位
}
break;
}
}
void Led_Proc() { }
void Get_Time() { Read_Rtc(ucRtc); }
/* ==================== DAC 输出 ==================== */
void AD_DA()
{
if(freq_last < 0)
{
da_val = 0;
}
else if((freq_last <= 500) && (freq_last >= 0)) // [修正Bug3] >= 0 包含0Hz
{
da_val = 51;
}
else if((freq_last > 500) && (freq_last < pf))
{
freq_delta_pf = pf - 500;
freq_delta = freq_last - 500;
da_val = (unsigned int)((unsigned long)204 * freq_delta / freq_delta_pf) + 51; // [修正Bug2+Bug6] 防溢出
}
else if(freq_last >= pf)
{
da_val = 5*51;
}
da_val_uc = (unsigned char)da_val;
Da_Write(da_val_uc);
}
/* ==================== 定时器初始化 ==================== */
void Timer1_Init(void)
{
AUXR &= 0xBF;
TMOD &= 0x0F;
TL1 = 0x18; TH1 = 0xFC;
TF1 = 0; TR1 = 1; ET1 = 1; EA = 1;
}
void Timer0_Init(void)
{
AUXR &= 0x7F;
TMOD &= 0xF0;
TMOD |= 0x05;
TL0 = 0x00; TH0 = 0x00;
TF0 = 0; TR0 = 1;
}
/* ==================== 中断服务函数 ==================== */
void Timer1_Isr(void) interrupt 3
{
uwTick++;
Seg_Pos = (++Seg_Pos) % 8;
if(Seg_Buf[Seg_Pos] > 20)
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos] - ',', 1);
else
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], 0);
if(++time200 == 200)
{
time200 = 0;
if(Seg_Show_Mod == 0) ucLed[0] ^= 1;
else ucLed[0] = 0;
if(freq_last > pf) ucLed[1] ^= 1;
else if(freq_last < 0) ucLed[1] = 1;
else ucLed[1] = 0;
}
if(++Time_1s == 1000)
{
Time_1s = 0;
Freq = TH0 << 8 | TL0;
TH0 = TL0 = 0;
}
freq_last = Freq + sta_val;
if(freq_last_max < freq_last) freq_last_max = freq_last;
if(hf ^ freq_last_max)
{
for(i=0; i<3; i++) // [修正Bug6] i=0 显式初始化
{
uchft[i] = ucRtc[i];
}
}
hf = freq_last_max;
Led_Disp(ucLed);
}
/* ==================== 调度器 ==================== */
typedef struct
{
void(*task_func)(void);
unsigned long int rate_ms;
unsigned long int last_ms;
} task_t;
idata task_t Scheduler_Task[] = {
{Led_Proc, 1, 0},
{Key_Proc, 10, 0},
{Seg_Proc, 20, 0},
{Get_Time, 100, 0},
{AD_DA, 150, 0}
};
idata unsigned char task_num;
void Scheduler_Init()
{
task_num = sizeof(Scheduler_Task) / sizeof(task_t);
}
void Scheduler_Run()
{
unsigned char i;
for(i=0; i<task_num; i++)
{
unsigned long int now_time = uwTick;
if(now_time >= (Scheduler_Task[i].rate_ms + Scheduler_Task[i].last_ms))
{
Scheduler_Task[i].last_ms = now_time;
Scheduler_Task[i].task_func();
}
}
}
/* ==================== 主函数 ==================== */
void main()
{
System_Init();
Scheduler_Init();
Set_Rtc(ucRtc);
Timer0_Init();
Timer1_Init();
while(1)
{
Scheduler_Run();
}
}
八、考点总结
| 考点 | 关键点 |
|---|---|
| 类型溢出 | 中间计算量若超过 255,必须用 unsigned int,不能用 unsigned char |
| 全局变量在 ISR 中复用 | 中断里使用全局循环变量,循环前必须 i=0 显式赋初值 |
| DAC 线性映射公式 | da_val = 基准 + 斜率×(x-x0)/(x1-x0),分母变量名不能写成分子 |
| 乘法中间值溢出 | C51 中 unsigned int 仅 16 位,乘法前需强制转 (unsigned long) 再做运算,最后转回结果类型 |
| 参数保存时机 | 参数界面有多个子页时,退出时应保存所有参数,不只保存当前页 |
| 数位提取 | 十位用 /10%10,个位用 %10,两者不可混用 |
| 频率测量原理 | Timer0 计数模式(TMOD|=0x05),Timer1 提供1s门控,读完立即清零 |
hf ^ freq_last_max |
利用 XOR 检测值是否发生变化,变化则记录时间戳 |