蓝桥杯单片机学习笔记(十二):V2026 大模板构建(上)

蓝桥杯单片机学习笔记(十二):V2026 大模板构建(上)

温馨提示:本文内容源自米醋电子工作室培训课,由本人学习、整理并补充说明。

本文面向已经学过"调度器大模板"的同学,也尽量照顾刚入门的小白。你可以把这篇笔记理解成一次"模板升级":原来的模板像一辆能跑的自行车,V2026 模板则是在保留车架的基础上,换上了更适合国赛复杂路况的变速器和刹车系统。

这里附上调度器模板的链接:---调度器模板教学

目录

  • 一、前言:为什么要升级模板
  • 二、LED、蜂鸣器、继电器、电机
  • 三、数码管显示
  • [四、初始化底层 Init](#四、初始化底层 Init)
  • [五、按键底层 Key:从"单键值"升级为"16 位状态图"](#五、按键底层 Key:从“单键值”升级为“16 位状态图”)
  • [六、NE555 频率测量:屏蔽 P34 与缩短测量周期](#六、NE555 频率测量:屏蔽 P34 与缩短测量周期)
  • [七、PWM 调光:把 LED 处理合并到显示处理思路中](#七、PWM 调光:把 LED 处理合并到显示处理思路中)
  • [八、温度底层 onewire:DS18B20 读取与分辨率设置](#八、温度底层 onewire:DS18B20 读取与分辨率设置)
  • [九、本篇小结:V2026 模板的核心变化](#九、本篇小结:V2026 模板的核心变化)

一、前言:为什么要升级模板

这次的 V2026 大模板 是在之前"调度器模板"的基础上做的进一步优化,主要目标是应对蓝桥杯国赛中更复杂、更容易"卡边界"的题目。

如果说省赛模板更像一套"标准厨房工具"------锅、铲、刀都能用,做一般菜完全够用;那么国赛模板更像"专业后厨工作台"------不仅工具更全,还要考虑多人同时操作、火候更细、出菜更快。

换到单片机模板里,就是:

  • 按键不再只考虑"一个键按下",而是要能表示 多个按键同时按下
  • NE555 测频不再只追求"能测出来",还要考虑测评系统的时间窗口;
  • 温度读取不再只写基础函数,还要考虑上电初值、分辨率和稳定性;
  • LED、数码管、按键、外设控制之间的调度关系要更加清晰。

本文会重点标注相较旧模板的变化。如果你已经学过之前的调度器大模板,可以优先看 "改动" 部分;如果你是小白,也可以从头阅读,把每个模块当成一块积木来理解。


二、LED、蜂鸣器、继电器、电机

本部分较之前模板基本无变化,仍然使用锁存器控制外设。

可以把 P0 想象成"数据传送带",P2 的高三位则像"仓库门牌号":

我们先把数据放到 P0 上,再通过 P2 选择对应的锁存器,把数据送进正确的"仓库"。

蓝桥杯单片机开发板常用的几个锁存器地址大致可以这样记:

外设 常见锁存器选择 作用理解
LED 0x80 把 P0 上的数据锁存到 LED 控制端
蜂鸣器/继电器/电机等 0xa0 把 P0 上的数据锁存到 ULN2003 等驱动控制端
数码管位选 0xc0 选择第几个数码管亮
数码管段选 0xe0 选择数码管显示什么内容

小白提示
P2 = P2 & 0x1f | 0x80; 的意思是:保留 P2 低 5 位,把高 3 位改成锁存器地址。

因为 C 语言中 & 的优先级高于 |,所以它等价于:
P2 = (P2 & 0x1f) | 0x80;

代码

c 复制代码
//Led.c
#include "Led.h"

idata unsigned char temp_1 = 0x00;
idata unsigned char temp_1_old = 0xff;

void Led_Disp(unsigned char *ucLed)
{
	temp_1 = 0x00;

	// 将 Led_Buf[0] ~ Led_Buf[7] 这 8 个开关状态压缩成一个字节
	// ucLed[0] 对应最低位,ucLed[7] 对应最高位
	temp_1 = ucLed[0] << 0 | ucLed[1] << 1 | ucLed[2] << 2 | ucLed[3] << 3 |
					 ucLed[4] << 4 | ucLed[5] << 5 | ucLed[6] << 6 | ucLed[7] << 7;

	// 只有当 LED 状态发生变化时才刷新锁存器,减少无意义写入
	if(temp_1 != temp_1_old)
	{
		P0 = ~temp_1;              // LED 通常为低电平点亮,所以这里取反
		P2 = P2 & 0x1f | 0x80;    // 选择 LED 锁存器
		P2 &= 0x1f;               // 关闭锁存器选择
		temp_1_old = temp_1;      // 保存本次状态
	}
}

void Led_Off()
{
	P0 = 0xff;                    // 全部熄灭
	P2 = P2 & 0x1f | 0x80;
	P2 &= 0x1f;
	temp_1_old = 0x00;
}

idata unsigned char temp_2 = 0x00;
idata unsigned char temp_2_old = 0x00;
// 注意:此处的 temp_2_old 由 0xff 修改为 0x00
// 这样上电后 temp_2 与 temp_2_old 初值一致,避免无必要的第一次刷新

//蜂鸣器
void Beep(bit enable)
{
	if(enable) 
		temp_2 |= 0x40;       // 需要蜂鸣器时,将对应位置 1
	else
		temp_2 &= ~0x40;      // 不需要蜂鸣器时,将对应位清 0
	//需要某一位,把某一位或1,不需要某一位,把某一位与0

	if(temp_2 != temp_2_old)
	{
		P0 = temp_2;
		P2 = P2 & 0x1f | 0xa0;
		P2 &= 0x1f;
		temp_2_old = temp_2;
	}
}

//继电器
void Relay(bit enable)
{
	if(enable) 
		temp_2 |= 0x10;
	else
		temp_2 &= ~0x10;
	//需要某一位,把某一位或1,不需要某一位,把某一位与0

	if(temp_2 != temp_2_old)
	{
		P0 = temp_2;
		P2 = P2 & 0x1f | 0xa0;
		P2 &= 0x1f;
		temp_2_old = temp_2;
	}
}

//电机
void MOTOR(bit enable)
{
	if(enable) 
		temp_2 |= 0x20;
	else
		temp_2 &= ~0x20;
	//需要某一位,把某一位或1,不需要某一位,把某一位与0

	if(temp_2 != temp_2_old)
	{
		P0 = temp_2;
		P2 = P2 & 0x1f | 0xa0;
		P2 &= 0x1f;
		temp_2_old = temp_2;
	}
}

//Led.h
#include <STC15F2K60S2.H>

void Led_Disp(unsigned char *ucLed);
void Led_Off();
void Beep(bit enable);
void Relay(bit enable);
void MOTOR(bit enable);

本模块理解重点

1. temp_1temp_1_old 的作用

这两个变量像"当前照片"和"上一张照片"。

  • temp_1:本次准备显示的 LED 状态;
  • temp_1_old:上一次已经显示出去的 LED 状态。

如果两张照片一样,就没必要重新拍一遍、传一遍;如果不一样,才重新刷新 LED。

这样写的好处是:减少对锁存器的重复操作,让程序更稳定、更省时间。

2. 蜂鸣器、继电器、电机共用 temp_2

蜂鸣器、继电器和电机都挂在同一组驱动控制位上,所以不能每次只管自己,否则容易出现"开蜂鸣器时把继电器状态冲掉"的问题。
temp_2 就像一个总开关面板,每一位控制一个设备:

位值 含义
0x10 继电器
0x20 电机
0x40 蜂鸣器

使用 |= 是"只打开某一个开关,不影响其他开关";使用 &= ~ 是"只关闭某一个开关,不影响其他开关"。


三、数码管显示

本部分较之前模板无变化。

数码管显示可以理解成"快速轮流点亮":人的眼睛有视觉暂留,只要刷新足够快,看起来就像 8 个数码管同时亮着。

数码管显示通常分为两步:

  1. 位选:选择第几个数码管亮;
  2. 段选:选择这个数码管显示什么数字或符号。

就像在教室里点名:先叫"第 3 排第 2 个同学",再告诉他"你举数字 8 的牌子"。

代码

c 复制代码
//Seg.c
#include "Seg.h"

// 共阳数码管段码表
// 0~9 分别显示数字 0~9,最后一个 0xff 表示空白
pdata unsigned char Seg_Dula[] = {0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0xff
																 };

void Seg_Disp(unsigned char wela,unsigned char dula,bit point)
{
	//消影
	// 切换数码管之前,先让段码全灭,避免上一位的残影带到下一位
	P0 = 0xff;
	P2 = P2 & 0x1f | 0xe0;
	P2 &= 0x1f;

	// 位选:选择第 wela 个数码管
	P0 = 0x01 << wela;
	P2 = P2 & 0x1f | 0xc0;
	P2 &= 0x1f;

	// 段选:决定当前数码管显示的内容
	P0 = Seg_Dula[dula];
	if(point)
		P0 &= 0x7f;             // 小数点通常对应最高位,清 0 点亮小数点
	P2 = P2 & 0x1f | 0xe0;
	P2 &= 0x1f;
}

//Seg.h
#include <STC15F2K60S2.H>

void Seg_Disp(unsigned char wela,unsigned char dula,bit point);         

本模块理解重点

1. 为什么要"消影"

如果不先消影,数码管从一位切到下一位时,上一位的段码可能还残留在总线上。

这就像投影仪换 PPT 时上一页还没完全消失,下一页已经出现,会看到一瞬间的重影。

所以模板里先执行:

c 复制代码
P0 = 0xff;
P2 = P2 & 0x1f | 0xe0;
P2 &= 0x1f;

这一步相当于先把黑板擦干净,再写下一位的内容。

2. Seg_Dula[10] = 0xff 的意义

0xff 表示所有段都不亮,也就是空白。

在实际题目中,如果某一位不想显示内容,可以把 Seg_Buf[i] 设置为 10,让该位熄灭。


四、初始化底层 Init

初始化函数的任务很简单:
上电后先把所有外设恢复到安全状态。

小白可以把它理解成开机前的"整理桌面":

  • LED 全灭;
  • 蜂鸣器不响;
  • 继电器不吸合;
  • 电机不转。

这样做可以避免单片机刚上电时,因为锁存器状态不确定导致外设乱动。

代码

c 复制代码
//Init.c
#include "Init.h"

void System_Init()
{
	// 关闭 LED
	P0 = 0xff;
	P2 = P2 & 0x1f | 0x80;
	P2 &= 0x1f;

	// 关闭蜂鸣器、继电器、电机等外设
	P0 = 0x00;
	P2 = P2 & 0x1f | 0xa0;
	P2 &= 0x1f;
}

//Init.h
#include <STC15F2K60S2.H>

void System_Init();

本模块理解重点

初始化函数建议放在 main() 的最开始执行。

一般结构可以理解为:

c 复制代码
void main()
{
	System_Init();     // 先关掉不该乱动的外设
	Timer_Init();      // 再打开定时器等核心功能

	while(1)
	{
		Key_Proc();
		Seg_Proc();
		// 其他任务函数
	}
}

它就像比赛开始前检查工具箱:

如果一开始工具就是乱的,后面再精妙的逻辑也容易出问题。


五、按键底层 Key:从"单键值"升级为"16 位状态图"

本部分是 V2026 模板的重要改动之一。

1. 为什么要改按键底层

旧模板中,按键通常返回一个单独的键值,例如:

  • S4 返回 4;
  • S5 返回 5;
  • S15 返回 15。

这种写法适合"每次只按一个键"的场景。

但是国赛题目可能出现更复杂的要求,例如:

  • 检测两个键同时按下;
  • 判断不同行、不同列按键组合;
  • 用组合键进入设置界面;
  • 用长按 + 短按实现不同功能。

这时候,如果仍然只返回一个键值,就像只给你一张"单人照片",无法表达"合影"。

V2026 模板改成返回 unsigned int,本质上就是给 16 个按键拍一张"全家福":哪个键按下,哪个位置就标 1。

2. 16 位按键状态的理解

按键一共有 16 个,而 C51 中 unsigned int 通常为 16 位,所以刚好可以用 16 个二进制位表示 16 个按键:

text 复制代码
0000 0000 0000 0000

在新模板中,这串数字 从右往左 对应:

text 复制代码
S4  S5  S6  S7  S8  S9  S10  S11  S12  S13  S14  S15  S16  S17  S18  S19

也就是说:

按键 对应位 二进制权值 十进制值
S4 bit0 1 << 0 1
S5 bit1 1 << 1 2
S6 bit2 1 << 2 4
S7 bit3 1 << 3 8
S8 bit4 1 << 4 16
S9 bit5 1 << 5 32
S10 bit6 1 << 6 64
S11 bit7 1 << 7 128
S12 bit8 1 << 8 256
S13 bit9 1 << 9 512
S14 bit10 1 << 10 1024
S15 bit11 1 << 11 2048
S16 bit12 1 << 12 4096
S17 bit13 1 << 13 8192
S18 bit14 1 << 14 16384
S19 bit15 1 << 15 32768

例如:

text 复制代码
0000 1000 0000 0000

代表 bit11 为 1,也就是 S15 被按下 ,换算成十进制就是 2048

如果 S15 和 S4 同时按下:

text 复制代码
0000 1000 0000 0001

换算成十进制就是:

text 复制代码
2048 + 1 = 2049

这就是多键同按的核心思想。

重要提醒

如果键码值可能大于 255,那么 Key_ValKey_DownKey_UpKey_Old 这几个按键专用变量都应该使用 unsigned int

如果仍然使用 unsigned char,高 8 位会丢失,S12~S19 这部分按键就可能判断错误。

3. 示例写法说明

如果想判断 S15 是否刚刚按下,可以写:

c 复制代码
if(Key_Down == 2048) // 此行代表按下 S15
{
    // 写你按键需要操作的语句
}

如果想判断 S15 和 S4 是否同时刚刚按下,可以写:

c 复制代码
if(Key_Down == 2049) // S15 + S4 同时按下
{
    // 写组合键对应的操作
}

如果想让代码更直观,也可以使用位移写法:

c 复制代码
if(Key_Down == (1 << 11)) // S15
{
    // S15 按下
}

if(Key_Down == ((1 << 11) | (1 << 0))) // S15 + S4
{
    // S15 和 S4 同时按下
}

这种写法不需要自己手算 2048、2049,可读性更好。


4. 底层代码

c 复制代码
//Key.c
#include "Key.h"

unsigned int Key_Read()
{
	unsigned int Key_State = 0;

	// 第一列扫描:拉低 P44,其余列拉高
	P44 = 0;P42 = 1;P35 = 1;P34 = 1;
	if(P33 == 0) Key_State |= (1 << 0);
	if(P32 == 0) Key_State |= (1 << 1);
	if(P31 == 0) Key_State |= (1 << 2);
	if(P30 == 0) Key_State |= (1 << 3);

	// 第二列扫描:拉低 P42
	P44 = 1;P42 = 0;P35 = 1;P34 = 1;
	if(P33 == 0) Key_State |= (1 << 4);
	if(P32 == 0) Key_State |= (1 << 5);
	if(P31 == 0) Key_State |= (1 << 6);
	if(P30 == 0) Key_State |= (1 << 7);

	// 第三列扫描:拉低 P35
	P44 = 1;P42 = 1;P35 = 0;P34 = 1;
	if(P33 == 0) Key_State |= (1 << 8);
	if(P32 == 0) Key_State |= (1 << 9);
	if(P31 == 0) Key_State |= (1 << 10);
	if(P30 == 0) Key_State |= (1 << 11);

	// 第四列扫描:拉低 P34
	P44 = 1;P42 = 1;P35 = 1;P34 = 0;
	if(P33 == 0) Key_State |= (1 << 12);
	if(P32 == 0) Key_State |= (1 << 13);
	if(P31 == 0) Key_State |= (1 << 14);
	if(P30 == 0) Key_State |= (1 << 15);

	// 扫描结束后恢复空闲状态
	P44 = 1;P42 = 1;P35 = 1;P34 = 1;

	return Key_State;
}

5. Key_Proc() 中的按键边沿判断

c 复制代码
//Main中的Key_Proc:
/* 按键处理函数 */
void Key_Proc()
{
	if(Key_Slow_Down < 10) return;
	Key_Slow_Down = 0;

	if(P30 == 0) return;
	Key_Val = Key_Read();
	if(P30 == 0) return;

	Key_Down = Key_Val & (Key_Val ^ Key_Old);
	Key_Up = ~Key_Val & (Key_Val ^ Key_Old);
	Key_Old = Key_Val;  

	if(Key_Down == 1)//按键4
		Seg_Buf[0] = 4;
	if(Key_Down == 32768)
		Seg_Buf[0] = 10;

}

6. Key_DownKey_UpKey_Old 怎么理解

这三行是按键处理中非常经典的"边沿检测":

c 复制代码
Key_Down = Key_Val & (Key_Val ^ Key_Old);
Key_Up = ~Key_Val & (Key_Val ^ Key_Old);
Key_Old = Key_Val;

可以把它理解成比较"这一帧照片"和"上一帧照片"。

变量 含义 类比
Key_Val 当前按键状态 现在拍到的照片
Key_Old 上一次按键状态 上一张照片
Key_Down 新按下的键 照片里新出现的人
Key_Up 新松开的键 照片里刚离开的人

Key_Val ^ Key_Old 用来找变化的位;

再与 Key_Val 相与,就能找到"从 0 变成 1"的位,也就是新按下的按键;

再与 ~Key_Val 相与,就能找到"从 1 变成 0"的位,也就是刚松开的按键。


六、NE555 频率测量:屏蔽 P34 与缩短测量周期

本部分也是 V2026 模板中的重点改动。

1. 为什么 NE555 要屏蔽 P34

在蓝桥杯单片机开发板中,NE555 测频通常会用到 T0 计数输入,而相关信号会接到 P34。

如果矩阵键盘扫描时仍然反复操作 P34,就会干扰 NE555 的计数。

这就像你正在数门口进来了几个人,旁边有人不断开关门、拍门、挡门,那么你数出来的人数就很容易不准。

因此,在使用 NE555 测频时,需要把按键扫描中涉及 P34 的部分屏蔽掉。

2. 屏蔽 P34 后的按键扫描代码

c 复制代码
#include "Key.h"

unsigned int Key_Read()
{
	unsigned int Key_State = 0;

	// 使用 NE555 测频时,P34 要留给 T0 计数输入,因此不再参与矩阵键盘扫描
	P44 = 0;P42 = 1;P35 = 1;//P34 = 1;
	if(P33 == 0) Key_State |= (1 << 0);
	if(P32 == 0) Key_State |= (1 << 1);
	if(P31 == 0) Key_State |= (1 << 2);
	if(P30 == 0) Key_State |= (1 << 3);

	P44 = 1;P42 = 0;P35 = 1;//P34 = 1;
	if(P33 == 0) Key_State |= (1 << 4);
	if(P32 == 0) Key_State |= (1 << 5);
	if(P31 == 0) Key_State |= (1 << 6);
	if(P30 == 0) Key_State |= (1 << 7);

	P44 = 1;P42 = 1;P35 = 0;//P34 = 1;
	if(P33 == 0) Key_State |= (1 << 8);
	if(P32 == 0) Key_State |= (1 << 9);
	if(P31 == 0) Key_State |= (1 << 10);
	if(P30 == 0) Key_State |= (1 << 11);
//	
//	P44 = 1;P42 = 1;P35 = 1;//P34 = 0;
//	if(P33 == 0) Key_State |= (1 << 12);
//	if(P32 == 0) Key_State |= (1 << 13);
//	if(P31 == 0) Key_State |= (1 << 14);
//	if(P30 == 0) Key_State |= (1 << 15);

	P44 = 1;P42 = 1;P35 = 1;//P34 = 1;

	return Key_State;
}

3. 屏蔽 P34 后有什么影响

屏蔽 P34 后,矩阵键盘中与 P34 相关的那一列按键就不再扫描。

也就是说,原本 16 个按键中有一列会暂时无法使用。

在比赛中这并不一定是问题,因为题目通常不会要求同时使用所有矩阵按键和 NE555 频率测量。

但写程序时要有这个意识:

使用 NE555 测频时,P34 更像"专用跑道",不要再让按键扫描也来占用这条跑道。


4. 为什么从 1s 测频改成约 200ms 测频

旧模板可能使用 1s 测一次频率。

这样做的优点是思路简单:1 秒内数到多少个脉冲,频率就是多少 Hz。

但是在 4T 测评环境下,测评系统对响应时间要求更严格。

如果你 1s 才更新一次频率,可能出现这种情况:

  • 测评系统已经开始检查答案;
  • 你的程序还在等 1s 计数周期结束;
  • 结果显示值还没刷新出来;
  • 测评就判定错误。

因此,新模板将测量窗口缩短到约 200ms。

200ms 内数到的脉冲数乘以 5,就近似得到 1s 内的脉冲数,也就是频率值。

5. 变量声明

c 复制代码
//变量声明区域
pdata unsigned int Freq = 0;
pdata unsigned char Timer_200ms = 0;

这里需要两个变量:

变量 作用
Freq 存放最终频率值
Timer_200ms 用于累计定时器中断次数,形成约 200ms 的时间窗口

6. 定时器 1 中断中的频率测量

c 复制代码
/* 定时器1中断服务函数 */
void Timer1_Isr(void) interrupt 3
{
    //频率测量相关内容
	if(++Timer_200ms > 198)
	{
		Freq = (TH0 << 8) | TL0;
		Timer_200ms = 0;
		Freq *= 5;
		TH0 = TL0 = 0;
	}

}

7. 这段代码的两个关键点

关键点一:为什么是 > 198,不是 >= 200

如果定时器 1 是 1ms 进入一次中断,那么理论上 200 次就是 200ms。

但是中断函数内部不可能只有这一段频率测量代码,前后可能还有数码管刷新、PWM、计时变量更新等内容。

也就是说,程序走到这段测频代码时,可能已经消耗了一点时间。

把判断条件写成 > 198,是为了给这部分执行时间留一点余量。

这就像赶火车:

如果车票写着 10:00 发车,你最好 9:58 就到站台,而不是 10:00:00 才冲进检票口。

关键点二:为什么先赋给 Freq,再 Freq *= 5

代码写成:

c 复制代码
Freq = (TH0 << 8) | TL0;
Freq *= 5;

而不是直接写成:

c 复制代码
Freq = ((TH0 << 8) | TL0) * 5;

这样写的目的,是让计数值先进入 unsigned int 类型的 Freq 中,再进行乘法。

在 C51 里,类型宽度比较敏感,如果表达式在较小类型中运算,可能有溢出风险。

补充提醒

如果频率特别高,200ms 内的计数值乘以 5 后仍然可能超过 unsigned int 的范围。

不过蓝桥杯常见 NE555 测频范围通常不会无限大,模板这样写主要是为了兼顾速度和够用性。

如果以后做更高频率测量,可以考虑使用更大范围的数据类型,或者缩短测量窗口。

8. 硬件连接别忘了

测量 NE555 频率时,一定要记住:

把 P34 和 SIGNAL 用跳线帽接在一起。

如果没有接跳线帽,程序再正确也读不到信号。

这就像你写好了收音机程序,但天线没接上,自然收不到电台。


七、PWM 调光:把 LED 处理合并到显示处理思路中

在新模板中,Led_Proc() 合并到了 Seg_Proc() 中。

这样做的原因是:Seg_Proc() 通常负责获取和整理显示相关的信息,而 LED 的显示状态往往依赖这些信息。

既然 LED 状态要在信息处理之后才能确定,就没有必要再单独开一个函数,合并后结构更紧凑。

可以这样理解:

  • Seg_Proc() 像"总调度员",负责整理当前界面、数据显示、状态变量;
  • LED 显示像"指示灯",通常根据这些状态变量亮灭;
  • 因此把 LED 状态更新放在 Seg_Proc() 里,是比较自然的。

PWM 的基本思想

PWM 调光的核心不是改变 LED 的电压,而是改变"亮的时间占比"。

比如一个周期分成 10 份:

Pwm_Compare 亮的份数 灭的份数 亮度感觉
0 0 10 全灭
2 2 8 较暗
5 5 5 中等
8 8 2 较亮
10 10 0 全亮

这就像水龙头开关:

不是水压变了,而是一段时间内"打开"的比例变了。打开时间越长,平均流出来的水越多;LED 亮的时间越长,人眼感觉就越亮。

代码

c 复制代码
//定时器1中断处理函数部分
if(++Pwm_Count == 10) Pwm_Count = 0;
	if(Pwm_Compare > Pwm_Count)
		Led_Disp(Led_Buf);
	else
		Led_Off();

本模块理解重点

1. Pwm_Count

Pwm_Count 是周期内的计数器。

它从 0 数到 9,一共 10 个小格子,然后重新回到 0。

2. Pwm_Compare

Pwm_Compare 是比较值,也就是亮度等级。

当:

c 复制代码
Pwm_Compare > Pwm_Count

成立时,LED 点亮;否则熄灭。

如果 Pwm_Compare = 7,那么在 0~9 这 10 个计数中,有 7 个计数时间 LED 是亮的,亮度大约就是 70%。

3. 为什么 PWM 放在定时器中断里

PWM 需要稳定的时间节拍。

如果放在 while(1) 主循环里,主循环执行快慢会受到按键、显示、传感器读取等影响,亮度就容易抖。

放在定时器中断里,相当于让节拍器来控制亮灭,节奏会更稳。


八、温度底层 onewire:DS18B20 读取与分辨率设置

本部分是新模板另一个重点改动。

DS18B20 是蓝桥杯中常见的 1-Wire 温度传感器。

它的通信方式可以理解成"单线传纸条":主机和传感器只通过一根数据线交流,所以时序要求比较严格。

1. 温度读取底层改动

c 复制代码
float Temperature_Read()
{
	unsigned char high1,low1,high2,low2;

	init_ds18b20();
	Write_DS18B20(0xcc);
	Write_DS18B20(0x44);

	do{
		init_ds18b20();
		Write_DS18B20(0xcc);
		Write_DS18B20(0xbe);
		low1 = Read_DS18B20();
		high1 = Read_DS18B20();

		init_ds18b20();
		Write_DS18B20(0xcc);
		Write_DS18B20(0xbe);
		low2 = Read_DS18B20();
		high2 = Read_DS18B20();
	}while(high1 != high2 || low1 != low2);

	return (float)(high1 << 8 | low1) * 0.0625f;
}

2. 为什么旧模板可能第一次显示 85°C

很多同学第一次调 DS18B20 时,会遇到上电后第一次读取显示 85°C ,第二次才正常。

更准确地说,85°C 常见于 DS18B20 上电后温度寄存器的默认值。如果程序发起温度转换后没有等转换完成,就立刻读取温度寄存器,就可能读到这个默认值或上一次的旧值。

这就像你刚让同学去量体温,结果他温度计还没拿回来,你就先翻了他的旧记录本,自然可能读到旧数据。

本模板的思路是连续读取两次 scratchpad 中的温度低字节和高字节,只有两次读数一致时才返回结果。

这样做可以在一定程度上规避读数跳变或通信不稳定问题。

重要补充

这段代码"连续两次读到相同值再返回",可以提高读数稳定性,但它并不完全等价于"等待本次温度转换完成"。

如果想写得更加严格,可以把"启动转换"和"读取结果"拆成两个步骤,或者按照当前分辨率等待足够的转换时间。

3. 0xcc0x440xbe 分别是什么意思

指令 名称 作用
0xcc Skip ROM 跳过 ROM 匹配,适合总线上只有一个 DS18B20 的情况
0x44 Convert T 启动温度转换
0xbe Read Scratchpad 读取暂存器数据

通俗理解:

text 复制代码
0xcc:这里就你一个传感器,我不点名了。
0x44:你现在去测温。
0xbe:把你暂存器里的结果发给我。

4. 温度换算为什么乘 0.0625f

在 12 位分辨率下,DS18B20 的最低有效位对应 0.0625°C

所以读取到的原始值需要乘以 0.0625f,才能得到摄氏温度。

例如原始值为 0x0191,十进制是 401:

text 复制代码
401 × 0.0625 = 25.0625°C

这也是代码最后一行的含义:

c 复制代码
return (float)(high1 << 8 | low1) * 0.0625f;

小提醒

如果读取负温度,DS18B20 的温度数据采用二进制补码表示。普通比赛题目多数测常温,影响不大;如果题目明确涉及负温度,建议对符号位和补码转换再做专门处理。


5. 温度分辨率设置

新模板新增了温度分辨率设置函数。

DS18B20 支持 9~12 位分辨率。分辨率越高,温度变化看起来越细腻,但转换时间也越长。

这就像拍照片:

  • 低分辨率:拍得快,但细节少;
  • 高分辨率:细节多,但需要更长时间。
位数 分辨率 理论最大转换时间 适用场景
9 位 0.5°C 约 93.75ms 对速度要求高,精度要求低
10 位 0.25°C 约 187.5ms 折中
11 位 0.125°C 约 375ms 较高精度
12 位 0.0625°C 约 750ms 最高精度,转换最慢

纠错说明

常见说法里有人会写"DS18B20 出厂默认 9 位",但更严谨的说法是:DS18B20 支持 9~12 位可编程分辨率,官方资料中默认上电分辨率为 12 位

不过,因为分辨率配置可以写入 EEPROM,不同模块或被别人改写过的芯片可能表现不同。比赛和练习中,最好在初始化时主动设置自己需要的分辨率,减少不确定性。

6. 分辨率设置代码

c 复制代码
//温度精度设置
void ds18b20_setResoluation(unsigned char res)
{
	unsigned char config;

	switch(res)
	{
		case 0://9位
			config = 0x1f;
		break;
		case 1://10位
			config = 0x3f;
		break;
		case 2://11位
			config = 0x5f;
		break;
		default://12位
			config = 0x7f;
		break;	
	}

	init_ds18b20();
	Write_DS18B20(0xcc);
	Write_DS18B20(0x4e);
	Write_DS18B20(0x00);
	Write_DS18B20(0x00);
	Write_DS18B20(config);

	init_ds18b20();
	Write_DS18B20(0xcc);
	Write_DS18B20(0x48);
}

7. 这段代码在做什么

第一步:根据 res 生成配置字节
c 复制代码
switch(res)
{
	case 0://9位
		config = 0x1f;
	break;
	case 1://10位
		config = 0x3f;
	break;
	case 2://11位
		config = 0x5f;
	break;
	default://12位
		config = 0x7f;
	break;	
}

config 中的关键是 R1、R0 两个位。

它们决定分辨率:

R1 R0 分辨率
0 0 9 位
0 1 10 位
1 0 11 位
1 1 12 位

因此:

config 含义
0x1f 9 位
0x3f 10 位
0x5f 11 位
0x7f 12 位
第二步:写入 Scratchpad
c 复制代码
init_ds18b20();
Write_DS18B20(0xcc);
Write_DS18B20(0x4e);
Write_DS18B20(0x00);
Write_DS18B20(0x00);
Write_DS18B20(config);

这里的 0x4e 是 Write Scratchpad 指令。

它后面通常要写 3 个字节:

  1. TH 高温报警值;
  2. TL 低温报警值;
  3. 配置寄存器。

模板中 TH 和 TL 都写成 0x00,重点是写入第三个字节 config

第三步:复制到 EEPROM
c 复制代码
init_ds18b20();
Write_DS18B20(0xcc);
Write_DS18B20(0x48);

0x48 是 Copy Scratchpad 指令。

它会把 Scratchpad 中的配置复制到 EEPROM 中。

EEPROM 掉电不丢失,所以设置一次后,下次上电仍然可以保持该配置。

命名小提示

函数名 ds18b20_setResoluation 中的 Resoluation 疑似是 Resolution 的拼写笔误。

为了保持原工程一致,本文不改函数名。实际写工程时,只要 .c.h 中声明、定义、调用保持一致,函数名拼写不会影响编译;但为了可读性,后续新工程建议统一成 ds18b20_setResolution


九、本篇小结:V2026 模板的核心变化

本篇主要完成了 V2026 大模板中若干底层模块的整理。

整体来看,模板升级的方向可以概括为一句话:

从"能用"升级到"更适合复杂题、更适合测评环境、更适合组合逻辑"。

1. 没有明显变化的模块

模块 状态 说明
LED 基本不变 仍使用锁存器控制,增加状态缓存减少刷新
蜂鸣器 基本不变 与继电器、电机共用 temp_2
继电器 基本不变 通过位操作独立控制
电机 基本不变 通过位操作独立控制
数码管 基本不变 位选 + 段选 + 消影
Init 基本不变 上电关闭外设,保证初始安全

2. 重点变化的模块

模块 改动点 目的
按键 Key 返回值改为 unsigned int 支持 16 位按键状态与多键同按
NE555 屏蔽 P34 避免按键扫描干扰频率计数
NE555 约 200ms 测一次并乘 5 降低 4T 测评等待风险
PWM LED 处理合并进显示/信息处理流程 减少函数分散,让逻辑更集中
DS18B20 两次读取一致再输出 降低上电初值或通信不稳造成的异常显示
DS18B20 增加分辨率设置函数 主动控制精度与转换时间

3. 学习建议

如果你是小白,建议按下面顺序理解:

  1. 先理解锁存器:知道 P0 是数据,P2 是选择;
  2. 再理解数码管动态扫描:位选和段选分开;
  3. 再理解按键位图:16 个按键对应 16 个二进制位;
  4. 再理解定时器中断:定时器是整个模板的节拍器;
  5. 最后理解 NE555、PWM、DS18B20:它们都是建立在定时器和底层 IO 思路之上的应用模块。

模板不是死记硬背的代码堆,而是一套"模块化工具箱"。

比赛时真正重要的不是把每一行都背下来,而是知道:

  • 哪个模块负责什么;
  • 哪些变量必须用 unsigned int
  • 使用 NE555 时为什么要让出 P34;
  • PWM 为什么要放在定时器中断里;
  • DS18B20 为什么不能刚启动转换就急着读结果;
  • 出现异常显示时应该从硬件连接、跳线帽、定时器、变量类型、刷新周期这些方向排查。

只要把这些逻辑想清楚,模板就不再是一大坨代码,而是一张清晰的地图。

遇到题目时,你只需要在地图上找到对应模块,再根据题目要求拼装即可。

相关推荐
森利威尔电子-5 小时前
森利威尔 SL3037B 替换HT7463A/HT7463B 5.5-60V宽压 峰值 0.6A
单片机·嵌入式硬件·物联网·集成电路·芯片
Bechamz5 小时前
大数据开发学习Day37
大数据·学习
zxd0203115 小时前
Zabbix7 监控系统学习总结
学习
z200509305 小时前
【linux学习】在linux下使用git提交到gitee
git·学习·gitee
叶~小兮5 小时前
Zabbix 7.0学习笔记
笔记·学习·zabbix
一条泥憨鱼5 小时前
【Java 进阶】LinkedHashMap 与 TreeMap
java·开发语言·数据结构·笔记·后端·学习
ゆづき5 小时前
假如编程语言们有外号
java·c语言·c++·python·学习·c#·生活
xuhaoyu_cpp_java5 小时前
Linux学习(一)
linux·经验分享·笔记·学习
red_redemption5 小时前
自由学习记录(189)
学习