蓝桥杯第十二届省赛单片机真题(第一场) — 刷题总结
一、题目概述
基于 STC15F2K60S2 单片机(12MHz),实现一个 DS18B20温度采集与DAC输出系统,包含:
-
DS18B20 温度采集与显示
-
温度参数设置(整数,默认25°C)
-
两种 DAC 输出模式(S5切换)
-
PCF8591 AD回读DAC输出电压
-
LED 指示当前模式/界面
二、功能模块实现
2.1 数码管显示(3个界面,S4 切换)
| 界面 | 首位标识 | 显示内容 | 格式 |
|---|---|---|---|
| 0 | C (11) |
实时温度 | C XX.XX |
| 1 | P (12) |
温度参数设置 | P XX |
| 2 | A (13) |
DAC输出电压回读 | A X.XX |
2.2 按键功能
| 按键 | 功能 | 有效界面 |
|---|---|---|
| S4 | 切换界面(0→1→2→0) | 全局 |
| S5 | 切换DAC模式(模式1↔模式2) | 全局 |
| S8 | 温度参数-1 | 界面1(参数设置) |
| S9 | 温度参数+1 | 界面1(参数设置) |
关键逻辑:退出参数设置界面时温度参数才生效。
2.3 DAC输出模式
| 模式 | 规则 |
|---|---|
| 模式1 | T < 参数 → 0V;T >= 参数 → 5V |
| 模式2 | T ≤ 20°C → 1.0V;20~40°C线性1.0~4.0V;T ≥ 40°C → 4.0V |
2.4 LED 指示
| LED | 条件 |
|---|---|
| L1 | 模式1 |
| L2 | 温度显示界面 |
| L3 | 参数设置界面 |
| L4 | DAC输出界面 |
三、踩坑记录(重点)
踩坑1:模式2线性插值 — 整数除法导致精度丢失
错误代码:
Da_1000x = 1000 + 3*(Temperature_100x - 2000) / 2; // 电压×1000
Da_Write(Da_1000x / 1000 % 10 * 51); // 转DAC值
问题: Da_1000x / 1000 是整数除法,直接截断小数部分。
例如 T=30°C:Da_1000x = 2500,2500 / 1000 = 2(丢掉了0.5),2 * 51 = 102(≈2.0V),实际应输出2.5V(DAC=127)。
修复: 不要先算电压再转DAC,而是直接计算DAC值,把乘法放在除法前面:
DAC_data = 51 + (unsigned long)(Temperature_100x - 2000) * 153 / 2000;
Da_Write(DAC_data);
公式推导:
V = 1.0 + (T - 20) × 3.0 / 20.0
DAC = V × 51 = 51 + (T - 20) × 153 / 20
用 Temperature_100x 替换 T(放大100倍):
DAC = 51 + (Temperature_100x - 2000) × 153 / 2000
教训:整数运算中,乘法放前面、除法放后面,避免中间过程被截断丢失精度。
踩坑2:模式2线性插值 — 中间乘法溢出
问题代码:
DAC_data = 51 + (Temperature_100x - 2000) * 153 / 2000;
问题: (Temperature_100x - 2000) * 153 最大值为 1999 × 153 = 305,847,而 unsigned int 最大值为 65,535,溢出。
修复: 在乘法之前将其中一个操作数强转为 unsigned long(32位):
DAC_data = 51 + (unsigned long)(Temperature_100x - 2000) * 153 / 2000;
原理: C语言规则 — 两个操作数类型不同时,窄类型自动提升为宽类型。unsigned long × int → 整个乘法在32位下进行。
注意转换位置:
// 正确:先转再乘,乘法在32位下进行
(unsigned long)(Temperature_100x - 2000) * 153
// 错误:先乘后转,括号内乘法仍在16位下溢出,转了也救不回来
(unsigned long)((Temperature_100x - 2000) * 153)
教训:中间产物的类型由参与运算的操作数中最宽的类型决定。类型转换必须放在溢出发生之前(乘法之前),而不是之后。
踩坑3:模式2分支不完整 — 缺少 T ≥ 40°C 和边界条件
错误代码:
if(Temperature_100x < 2000) Da_Write(1 * 51);
if((Temperature_100x > 2000) && (Temperature_100x < 4000))
{
// 线性区间...
}
// 缺少 T >= 40°C 的分支!
问题:
- T ≥ 40°C 时没有任何输出,DAC保持上次的值
- T = 20°C(恰好2000)时,
< 2000和> 2000都不匹配,落入空白区
修复: 用 if-else if-else 三段互斥结构,边界用 <= 和 >=:
if(Temperature_100x <= 2000)
{
Da_Write(51); // T ≤ 20°C:1.0V
}
else if(Temperature_100x >= 4000)
{
Da_Write(204); // T ≥ 40°C:4.0V
}
else
{
// 20°C < T < 40°C:线性插值
DAC_data = 51 + (unsigned long)(Temperature_100x - 2000) * 153 / 2000;
Da_Write((unsigned char)DAC_data);
}
教训:分段函数必须用
if-else if-else保证互斥且无遗漏,边界值要明确归属。
四、核心知识点:线性插值在单片机中的实现
4.1 数学公式
已知两端点 (T_A, V_A) 和 (T_B, V_B),求中间任意点:
V = V_A + (T - T_A) × (V_B - V_A) / (T_B - T_A)
本题:(20°C, 1.0V) → (40°C, 4.0V)
V = 1.0 + (T - 20) × 3.0 / 20.0
4.2 单片机整数化三步法
第一步:目标量直接化
不要先算电压再转DAC,直接算最终需要的DAC值:
DAC = V × 51 = 51 + (T - 20) × 153 / 20
第二步:变量放大对齐
代码中温度放大了100倍,分母同步放大:
DAC = 51 + (T_100x - 2000) × 153 / 2000
第三步:防溢出类型提升
中间产物最大 305,847 > 65,535,在乘法前强转32位:
DAC = 51 + (unsigned long)(T_100x - 2000) * 153 / 2000;
4.3 验证表
| 温度 | T_100x | DAC计算 | DAC值 | 电压 |
|---|---|---|---|---|
| 20°C | 2000 | ≤2000 → 51 | 51 | 1.0V |
| 25°C | 2500 | 51+500×153/2000 | 89 | 1.75V |
| 30°C | 3000 | 51+1000×153/2000 | 127 | 2.49V |
| 35°C | 3500 | 51+1500×153/2000 | 165 | 3.24V |
| 40°C | 4000 | ≥4000 → 204 | 204 | 4.0V |
五、通用经验总结
| 编号 | 经验 | 适用场景 |
|---|---|---|
| 1 | 整数运算中先乘后除,避免中间截断丢精度 | 所有需要保留精度的整数计算 |
| 2 | 中间产物可能溢出时,在乘法之前强转宽类型 | unsigned int 乘积 > 65535 的场景 |
| 3 | 类型转换位置决定一切:转在括号外 vs 括号内效果完全不同 | 所有涉及类型提升的表达式 |
| 4 | 分段函数用 if-else if-else,边界用 <=/>= 明确归属 |
DAC分段输出、阈值判断 |
| 5 | 线性插值单片机实现:直接算目标量 → 变量放大对齐 → 防溢出提升 | PCF8591 DAC、PWM占空比等线性映射 |
void AD_DA()
{
unsigned char dac_val = 0;
if(DACMODE == 0)
{
if(Temperature_100x < Temperature_parameter*100) dac_val = 0;
else dac_val = 255;
}else
{
if(Temperature_100x <= 2000) dac_val = 51;
else if(Temperature_100x >= 4000) dac_val = 204;
else dac_val = 51 + (unsigned long)(Temperature_100x - 2000) * 153 / 2000;
}
Da_Write(dac_val);
AD_0_Data_100x = (unsigned int)dac_val * 100 / 51; // 直接从DAC值计算电压×100
}```
