蓝桥杯第十五届省赛(第二次)错误总结(附满分代码)

第十五届蓝桥杯省赛(第二次)错误总结

题目信息

  • 赛题:第十五届省赛(第二次)程序题
  • 涉及外设:数码管、LED、按键、DS18B20、PCF8591 DAC、继电器

错误清单

Bug #1:uwTick 类型错误(调度器65秒后全面瘫痪)

项目 内容
严重程度 致命
文件位置 main.c:14
影响范围 65秒后所有功能停止工作,约10+个测试失败

错误代码:

idata unsigned int uwTick = 0;  // 16位,最大值65535

正确代码:

idata unsigned long int uwTick = 0;  // 32位,最大值约42亿

原因分析:

uwTick 每毫秒加1,unsigned int 是16位,约65秒后从65535溢出归零。但调度器的 last_ms 字段是 unsigned long int(32位),存储的是溢出前的大值(如60000)。溢出后:

now_time = 0          (uwTick归零后提升为32位)
last_ms  = 60000      (之前存的值)
判断条件: 0 >= 60000 + 200  →  永远为false
→ 所有任务永远不会再被调度

后果:

  • 按键扫描停止 → 继电器、模式切换全部无响应
  • AD_DA 停止 → DAC输出冻结在最后一次写入的值
  • 温度采集停止 → LED报警逻辑停滞
  • 前40个测试(约65秒内)大部分通过,第41个测试开始全部失败

教训:

调度器的时间戳变量类型必须与 last_ms 类型一致。类型不匹配导致溢出后比较逻辑永久失效。这种bug极其隐蔽——短时间测试完全正常,只有长时间运行才会暴露。


Bug #2:继电器参数写反(S16/S17 对调)

项目 内容
严重程度 严重
文件位置 main.c:74-77
影响范围 所有继电器相关测试结果相反

错误代码:

if(Key_Down==16) Relay(0);  // 本意"打开",实际是断开
if(Key_Down==17) Relay(1);  // 本意"关闭",实际是吸合

正确代码:

if(Key_Down==16) Relay(1);  // S16 → 继电器吸合(ON)
if(Key_Down==17) Relay(0);  // S17 → 继电器断开(OFF)

原因分析:

Relay(bit enable) 函数中 enable=1 表示吸合(ON),enable=0 表示断开(OFF)。写代码时把0当成了"打开",1当成了"关闭",语义理解反了。

教训:

调用硬件控制函数前,先确认参数含义:1=ON/吸合 还是 1=OFF/断开。可以在调用处写上注释 Relay(1); // ON-吸合 来自检。


Bug #3++Seg_Show_Mode 未定义行为

项目 内容
严重程度 严重(潜在)
文件位置 main.c:67
影响范围 界面切换可能异常

错误代码:

Seg_Show_Mode = (++Seg_Show_Mode) % 3;
// ++修改一次,=赋值又修改一次 → C语言未定义行为

正确代码:

Seg_Show_Mode = (Seg_Show_Mode + 1) % 3;
// Seg_Show_Mode+1 只读取不修改,= 只修改一次 → 安全

原因分析:

C标准规定:在两个序列点之间,对同一个变量最多只能修改一次。++x 修改一次,x = ... 再修改一次,属于未定义行为。Keil C51可能碰巧给出正确结果,但换编译器或优化等级就可能出错。

教训:

永远不要在同一个表达式中对同一个变量既做自增/自减,又做赋值。用 x+1 替代 ++x 就能避免问题。


Bug #4:DAC温控计算精度丢失

项目 内容
严重程度 中等
文件位置 main.c:177
影响范围 DAC输出电压在部分温度点偏差

错误代码:

Dac_Digit_Temperature = (Temperature_Calibrate_Value10x - 100) * 51 / 100 + 102;
// 纯整数运算,中间结果被截断

正确代码:

idata signed char temp_celsius = (signed char)(Temperature_Calibrate_Value10x / 10);
Dac_Digit_Temperature = (5.1 * (temp_celsius - 10) + 102);
// 浮点运算,精度更高;signed char 处理负温度

原因分析:

整数除法 * 51 / 100 会截断小数部分。例如温度25.5°C时,整数计算得181,浮点计算得178。虽然大部分情况偏差很小(约0.02V),但边界值可能导致平台判定不通过。

教训:

涉及模拟量输出(DAC/ADC)的计算,优先使用浮点运算避免精度问题。8051的浮点运算虽然慢,但在低频任务(如150ms周期)中完全可接受。


Bug #5:DS18B20初始化方式不稳健

项目 内容
严重程度 中等
文件位置 main.c:355-356
影响范围 上电后第一次温度可能是85°C

错误代码:

rd_temperature();
Delay750ms();  // 固定延时,不确定转换是否完成

正确代码:

while(rd_temperature() == 85);  // 循环读取直到不是上电默认值

原因分析:

DS18B20上电后寄存器默认值是85°C。Delay750ms() 的实际延时可能因晶振精度、编译器优化等略有偏差。如果第一次读取仍是85°C,会导致后续温度突变判定(85→真实温度),意外触发LED报警。

教训:

传感器初始化时,用条件循环(while(读取值 == 默认值))比固定延时更可靠。不要假设延时"刚好够"。


Bug #6:调度器任务周期不合理

项目 内容
严重程度
文件位置 main.c:331-335
影响范围 CPU浪费、显示刷新慢
任务 我的周期 满分周期 问题
Led_Proc 1ms 10ms 过快,浪费CPU
Seg_Proc 80ms 20ms 过慢,显示反应迟钝

教训:

Led_Proc 每1ms执行一次意味着每毫秒都在操作P0/P2端口,与中断中的数码管扫描产生不必要的竞争。Seg_Proc 80ms更新一次,按键后显示变化有明显延迟。参考经验值:LED 10ms、数码管数据 20ms、按键 10ms。


错误影响追溯图

uwTick 16位溢出 (Bug #1)
  │
  └──→ 65秒后调度器全面停止
        ├──→ Key_Proc 停止 → 按键无响应
        │     ├──→ 继电器无法控制 → 测试41,44,45,46,48,49,50 ❌
        │     └──→ DA模式无法切换 → DAC相关测试异常
        ├──→ AD_DA 停止 → DAC输出冻结
        │     └──→ 测试37,38,39,43,47 ❌
        ├──→ Get_Temperature 停止 → 温度不更新
        │     └──→ LED报警逻辑停滞
        └──→ Led_Proc 停止 → LED状态不刷新
              └──→ 测试18 ❌

继电器参数反转 (Bug #2)
  │
  └──→ 即使在65秒内,继电器操作也是反的
        └──→ 早期继电器测试也会失败(如果有的话)

满分代码

/头文件/
#include <STC15F2K60S2.H>
#include “iic.h”
#include “led.h”
#include “key.h”
#include “onewire.h”
#include “seg.h”
#include “init.h”
#include “intrins.h”
#include “filtering.h”

/变量/
//定时器
idata unsigned long int uwTick=0;
//按键
idata unsigned char Key_Val,Key_Old, Key_Up, Key_Down;
//led
pdata unsigned char ucLed[8]={0,0,0,0,0,0,0,0};
//数码管
idata unsigned char Seg_Pos=0;
pdata unsigned char Seg_Buf[8]={10,10,10,10,10,10,10,10};

//模式
idata unsigned char Seg_Show_Mode=0;//0-温度,1-DA,2-校准值界面
idata bit DA_Mode=0;//0-温度控制,1-手动控制 DA输出模式控制
idata bit Relay_Lock=0;//0-解锁,1-锁定 继电器解锁固定控制

//数据
// 存储放大10倍的温度值,以处理一位小数
idata unsigned int Temperature_Value10x=0;//实时采集温度的10倍
idata unsigned int Temperature_Value10x_Old=0;//上一次采集的温度的10倍
idata unsigned int Temperature_Calibrate_Value10x = 0; // 温度校准后的值的10倍
idata char Para_Calibrate=0;//校准值 -9~9有负值就不能再用unsigned了
idata char Para_Calibrate_Ctrl=0;//校准值控制值 -9~9 题目里明确要求 校准值在退出校准值界面时生效所以一定要再定义一个控制值

idata unsigned char Dac_Digit_Hand =100; // 手动控制电压数字值
idata unsigned char Dac_Digit_Temperature = 0; // 温度控制电压数字值

//标志位
idata bit Long_Press_Flag=0;//0-没按下 1-按下 长按检测标志位
idata unsigned int Time_1500ms_Press=0;//长按1.5s计时器
idata unsigned char Temp_Count=0;//温度采样稳定计数器,连续采样过滤掉刚上电的值
idata bit Temperature_Alarm_Flag=0;//温度突变报警标志位
idata unsigned int L2_2000ms=0;//L2 2秒计时
idata bit L2_Flag=0;//L2标志位
idata unsigned int L3_2000ms=0;//L3 2秒计时
idata bit L3_Flag=0;//L3标志位
idata unsigned int L4_200ms=0;//L4 0.2秒计时
idata bit L4_Flag=0;//L4标志位
idata unsigned int L4_3000ms = 0;
idata unsigned char L4_Blinking=0;//L4闪烁进行中标志

/按键/
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==12)
{
if(Seg_Show_Mode==1)//进入校准值界面前,把参数值给我们的控制值
Para_Calibrate_Ctrl=Para_Calibrate;
if(Seg_Show_Mode==2)//退出校准值界面,将控制值给参数值
Para_Calibrate=Para_Calibrate_Ctrl;
Seg_Show_Mode=(Seg_Show_Mode+1)%3;
}
switch(Seg_Show_Mode)
{
case 0://在温度界面
if(Relay_Lock==0)//解锁状态下
{
if(Key_Down==16)
Relay(1);//打开继电器

			if(Key_Down==17)
				Relay(0);//关闭继电器
			
		}
		break;
	
	case 1://DA输出控制界面下
		if(Key_Down==16)
			Dac_Digit_Hand=(Dac_Digit_Hand==255)? 255:Dac_Digit_Hand+5;//定死在上限255
	  
		if(Key_Down==17)
			Dac_Digit_Hand=(Dac_Digit_Hand==0)? 0:Dac_Digit_Hand-5;//定死在下限0
		
		break;
	case 2://校准值界面下
		if(Key_Down==16)
			Para_Calibrate_Ctrl=(Para_Calibrate_Ctrl==9)? 9:Para_Calibrate_Ctrl+1;//要用控制值,定死在上限
		
		if(Key_Down==17)
			Para_Calibrate_Ctrl=(Para_Calibrate_Ctrl==-9)? -9:Para_Calibrate_Ctrl-1;//定死在下限
		
		break;	
}
//长短按
if(Key_Down==13)
	Long_Press_Flag=1;
  
if(Key_Up==13)//检测抬起
{
	if(Time_1500ms_Press>1500)//长按
		Relay_Lock^=1;
	else//短按
		DA_Mode^=1;
Long_Press_Flag=0;
	Time_1500ms_Press=0;
	
}	

}

/数码管/
void Seg_Proc()
{
unsigned char Temp_Para_Calibrate_Ctrl=-Para_Calibrate_Ctrl;//定义一个临时变量取负数的相反数
Seg_Buf[1]=10;
Seg_Buf[2]=10;
Seg_Buf[3]=10;
Seg_Buf[4]=10;
switch(Seg_Show_Mode)
{
case 0://温度界面
Seg_Buf[0]=11;//C
Seg_Buf[5]=10;
Seg_Buf[6]=Temperature_Calibrate_Value10x/100%10;//没要求高位熄灭就不要擅自高位熄灭
Seg_Buf[7]=Temperature_Calibrate_Value10x/10%10;
break;
case 1://DA输出控制界面
Seg_Buf[0]=12;//R
if(DA_Mode==0)//温度控制
{
Seg_Buf[5]=(Dac_Digit_Hand/100%10==0)? 10:Dac_Digit_Hand/100%10;
Seg_Buf[6]=((Seg_Buf[5]==10)&&(Dac_Digit_Hand/10%10==0))? 10:Dac_Digit_Hand/10%10;
Seg_Buf[7]=Dac_Digit_Hand%10;
}
else//手动控制模式
{
Seg_Buf[5]=(Dac_Digit_Hand/100%10==0)? 10:Dac_Digit_Hand/100%10;
Seg_Buf[6]=((Seg_Buf[5]==10)&&(Dac_Digit_Hand/10%10==0))? 10:Dac_Digit_Hand/10%10;
Seg_Buf[7]=Dac_Digit_Hand%10;
}
break;
case 2://校准值界面,怎么处理负数
Seg_Buf[0]=13;//P
Seg_Buf[5]=10;
if(Para_Calibrate_Ctrl<0)
{
Seg_Buf[6]=14;//-
Seg_Buf[7]=Temp_Para_Calibrate_Ctrl;
}
else
{
Seg_Buf[6]=10;
Seg_Buf[7]=Para_Calibrate_Ctrl;
}
break;
}
}

/Led/
void Led_Proc()
{
ucLed[0]=(!DA_Mode);
ucLed[1]=L2_Flag;
ucLed[2]=L3_Flag;
ucLed[3] = L4_Flag;
ucLed[7]=(!Relay_Lock);
Led_Disp(ucLed);
}

/AD_DA/
void AD_DA()
{
if(DA_Mode==0)//处于温度控制模式下
{
idata signed char temp_celsius = (signed char)(Temperature_Calibrate_Value10x / 10);
if(temp_celsius<=10)
Dac_Digit_Temperature=251;
else if(temp_celsius>=40)
Dac_Digit_Temperature=5
51;
else//待验证
Dac_Digit_Temperature=(5.1 * (temp_celsius - 10) + 102);
Da_Write(Dac_Digit_Temperature);
}
else//处于手动控制模式下
Da_Write(Dac_Digit_Hand);

}

/温度/
void Get_Temperature()
{
//用单总线温度读取函数读取温度
Temperature_Value10x_Old=Temperature_Value10x;//先保存旧值
Temperature_Value10x=rd_temperature()10;//再读取新值
Temperature_Calibrate_Value10x=Temperature_Value10x+Para_Calibrate
10;//这个*10不要忘记了

//采样稳定控制
Temp_Count++;
if(Temp_Count==3)
	Temp_Count=2;//这部分的作用实质上就是跳过刚上电时的温度
//只有采样稳定后才判断温度变化
if(Temp_Count==2)
{
	if((Temperature_Value10x>=(Temperature_Value10x_Old+10))||(Temperature_Value10x_Old>=(Temperature_Value10x+10)))
	{	
		Temperature_Alarm_Flag=1;//上升或下降幅度超过 1℃,立即触发温度突变报警功能
		if(L4_Blinking==0)//判断是否在闪烁进行中
		{
			L4_Blinking=1;
			L4_200ms=0;
			L4_3000ms=0;
			L4_Flag=1;//报警一瞬间让灯亮起
		}
	}
	else
		Temperature_Alarm_Flag=0;//有if最好配套else
	if(Temperature_Value10x>Temperature_Value10x_Old)
	{
		L2_Flag=1;//温度上升
		L2_2000ms=0;//归零开始计时
	}
	if(Temperature_Value10x<Temperature_Value10x_Old)
	{
		L3_Flag=1;//温度上升
		L3_2000ms=0;//归零开始计时
	}
}

}

/定时器/
void Timer1_Init(void) //1毫秒@12.000MHz
{
AUXR &= 0xBF; //定时器时钟12T模式
TMOD &= 0x0F; //设置定时器模式
TL1 = 0x18; //设置定时初始值
TH1 = 0xFC; //设置定时初始值
TF1 = 0; //清除TF1标志
TR1 = 1; //定时器1开始计时
ET1 = 1; //使能定时器1中断
EA=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(Long_Press_Flag==1)
{
if(++Time_1500ms_Press>1500)
Time_1500ms_Press=1501;
}
else//这个else一般不要丢
{
Time_1500ms_Press=0;
}
if(L4_Blinking==1)//闪烁中请勿打扰
{
L4_200ms++;
L4_3000ms++;
//处理0.2m闪烁
if(L4_200ms==200)
{
L4_200ms=0;
L4_Flag^=1;
}
//处理3m延时
if(L4_3000ms==3000)
{
//四大金刚都要清零
L4_3000ms=0;
L4_Blinking=0;//可以打扰了
L4_200ms=0;
L4_Flag=0;
}
}
/if(L2_2000ms<3000)
L2_2000ms++;
if(L2_2000ms>=2000)
{
ucLed[1]=0;
L2_Flag=0;
}
if(L3_2000ms<3000)
L3_2000ms++;
if(L3_2000ms>=2000)
{
ucLed[2]=0;
L3_Flag=0;
}
/
if(L2_Flag==1)
{
L2_2000ms++;
if(L2_2000ms==2000)
{
L2_2000ms=0;
L2_Flag=0;
}
}
if(L3_Flag==1)
{
L3_2000ms++;
if(L3_2000ms==2000)
{
L3_2000ms=0;
L3_Flag=0;
}
}

}

/调度器/
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, 80, 0},
{AD_DA, 160, 0},
{Get_Temperature, 200, 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].last_ms + Scheduler_Task[i].rate_ms)
{
Scheduler_Task[i].last_ms = now_time;
Scheduler_Task[i].task_func();
}
}
}

void main()
{
System_Init();//系统初始化
while(rd_temperature() == 85);
Scheduler_Init();
Timer1_Init();//定时器1的初始化一定要放在最后
while(1)
{
Scheduler_Run();
}
}