教程 
蓝桥杯单片机国赛模板详解 | 各类功能实现详解

写在前面

这个模板是我在Claude老师与Gemini老师的指导下,在刷题的过程中总结出来的,如您觉得有改进之处或者错误欢迎指出. 另外,主播的英语不好,函数名都是乱取的(

总的来说,模板由两部分组成. 一部分使用一个定时器负责当作时钟,可以定时调用函数, 而另一部分就是实现功能的函数.

时钟部分

以下均以Timer0​为例.

作为计时的部分,我们首先需要确认定时器的最小时间. 一般我选择1ms. 这样,1ms整数倍的时间都能够在中断函数中被表示.

1
2
3
4
5
6
7
8
9
10
11
void Timer0_Init(void)		//1毫秒@12.000MHz
{
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
TL0 = 0x18; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1; //开启Timer0中断
EA = 1; //开启总中断
}

那如何隔一段时间调用函数呢?我们需要一个计数器来记录时间过去了多久.

若定义一个cnt, 每次调用中断函数都过去1ms的时间, cnt都会计一次数,这样当过去timeLimit×1ms\text{timeLimit}\times \text{1ms}的时候,if条件成立,调用函数. 这样就实现了每过一段任意的时间调用一次函数.

1
2
3
4
5
6
7
8
void Timer0_Routine(void) interrupt 1 {
static unsigned char cnt=0;
cnt++;
if(cnt >= timeLimit){ //<<---
//do something...
cnt = 0;
}
}

接下来要解决的问题就是:在这个中断函数中应该干什么呢?

输入与输出

按键扫描

这部分 分为扫描矩阵键盘与处理扫描函数返回值两个部分.

第一部分,我们定义一个函数用来扫描矩阵键盘,返回当前键码值. 函数原型为:

unsigned char ScanMatrix(void);

因为蓝桥杯要用到的按键不多,所以直接对一个引脚进行赋值/读取即可.

例如我要看S7按键是否被按下,我只需要使得COL中只有COL1为0(防止其他按键干扰),然后去判断ROW1是否为0就行了(若按下,则强制ROW1和COL1电位相等,为低电平).

同样,如果要判定是否有两个按键同时被按下的话,可以把他们对应的引脚值暂存起来,最后一起判定是否都为低电平.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//existing code..
sfr P4 = 0xC0;
sbit COL1 = P4^4;sbit COL2 = P4^2;
sbit ROW3 = P3^2;sbit ROW4 = P3^3;//引脚定义

unsigned char ScanMatrix(){
unsigned char key = 0;//若无按键按下,则会返回0
bit s5; bit s8;

COL1 = 0;//判定COL1列
COL2 = 1;//COL3 = 1; COL4 = 1;
s5=ROW3;//暂存一下
if(ROW3 == 0) key=5;
else if(ROW4 == 0) key=4;

COL1 = 1;//判定COL2列
COL2 = 0;//COL3 = 1; COL4 = 1;
s8=ROW4;//暂存一下
if(ROW3 == 0) key=9;
else if(ROW4 == 0) key=8;

if(!s5 && !s8) key=58;//两个按键按下!
return key;
}

这样,ScanMatrix的功能就是,如果有按键被按下返回键码值,否则返回0.

那么,怎么处理返回值呢?

一般是在按键松开时触发,所以只需要定义一个static的变量,存上一次扫描的键码值. 如果这次的键码值为0,而上一次的键码值不为0,就说明按键松开了.

但这里处理双键有一个小细节需要注意. 松手时可能会出现双键不同时松开的情况,例如一起按下s5s8,最后先松开s5,这样preKey58,而key8不是0,与单键不一样,需要特判. 此外,还需要保证后松开的s8不会触发单键. 所以需要在触发双键时用一个变量标记状态,使得后一次单键不触发.

ps: 因为这里每50ms才扫描一次按键,所以不用消抖.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void ProcessKey(){
// preKey: 上一次扫描到的按键值
// comboLock: 组合键已处理标志,防止松开过程中残余单键误触发
static unsigned char preKey=0, comboLock=0;
unsigned char key = ScanMatrix();

if(key != preKey) { // 按键状态发生变化
if(key == 0) { // 所有键已全部松开
if(!comboLock){ // 非组合键结尾,正常处理松开事件
if(preKey == 4) {} // S4 松开
if(preKey == 58){} // S5+S8 同时松开
}
comboLock = 0; // 全部松开后复位组合锁
}
else { // 有键被按下或正在变化(处理组合键中途松开的情形)
if(preKey == 58){ // S5+S8 组合键已触发,其中一键开始松开
comboLock = 1; // 加锁,防止剩余单键松开时被误识别为单键事件
}
}
// do something...
}
preKey = key; // 更新状态
}
void Timer0_Routine(void) interrupt 1 {
static unsigned char cnt=0;
//existing code...
cnt++;
if(cnt >= 50){
ProcessKey();//50ms扫描一次键盘
cnt = 0;
}
}

此外,长按也可以在这里处理. 不同的是,它不用在按键松开的条件下处理,所以在if外写. 同样需要定义一个计数变量用来记录长按了多久,在大于一定值时触发.

1
2
3
4
5
6
7
8
9
10
11
12
void ProcessKey(){
static unsigned char preKey=0, s4Cnt=0;
unsigned char key=ScanMatrix();
//existing code...

if(key==4) s4Cnt++;
else s4Cnt=0;
if(s4Cnt>=10){//长按了10*100ms=1s
//do something...
}
preKey = key;
}

数码管显示

模板中,时钟最小有1ms,这已经很小了,没必要再在中断里调用循环了,直接把中断当循环用就行了.

so在中断里面需要调用一个函数,它的功能是更新指定位置的数码管. 与之配套的,需要一个数组存储显示的数据.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
unsigned char segData[8] = {0, 1, 2, 3, 4, 5, 6, 7};
unsigned char code segTable[18] = {0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90, 0xFF,
0xBF, /*11 -*/
0xC6, /*12 C*/
0x89, /*13 H*/
0x8E, /*14 F*/
0x8C, /*15 P*/
0x86, /*16 E*/
0x88, /*17 A*/
};
void DisplaySingleDigit(unsigned char addr) {
//消隐
P2 = (P2 & 0x1F) | 0xE0;
P0 = 0xFF;
P2 = P2 & 0x1F; //Lock
//位选
P2 = (P2 & 0x1F) | 0xC0; //
P0 = (1 << addr);
P2 = P2 & 0x1F; //Lock
//段选
P2 = (P2 & 0x1F) | 0xE0;
P0 = segTable[segData[addr]];
P2 = P2 & 0x1F;
}

而在中断之中,需要用一个变量指明该更新哪一位数码管

1
2
3
4
5
6
7
8
9
10
11
12
13
void Timer0_Routine() interrupt 1 {
static unsigned char segCnt=0;
static unsigned char cnt=0;
//existing code...
DisplaySingleDigit(segCnt);
if(cnt&0x01) segCnt++;//与if(cnt%2)等价,速度快一些
cnt++;
if(segCnt>=8) segCnt=0;
if(cnt >= timeLimit){
//do something...
cnt = 0;
}
}

这样就写完了.

主函数如何写?

比如说我们有一个函数Funtion()用来获取外设数据与进行逻辑计算. 那是在中断中调用好呢,还是在主循环调用好呢?后者更好. 因为前者会导致函数阻塞中断,会导致中断时间不准确. 比如OneWire的通讯时间就需要几十毫秒.

所以我们在中断中需要做的是操控一个标志变量,让主函数里的循环去读取这个变量观察是否运行.

下面实现了每100ms运行一次Funtion.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
volatile bit runFlag=0;
void main() {
while(1){
if(runFlag) {
runFlag=0;//先置0,防止运行时间超过100ms的话,会导致下一次运行失效的情况.
Function();
}
}
}
void Timer0_Routine() interrupt 1 {//1ms per time
static unsigned char cnt=0;
cnt++;
if(cnt >= 100){
//do something...
runFlag = 1;
cnt = 0;
}
}

到这里,基本模板的部分结束了. 在这个模板之上调用封装好的外设函数与数据处理等函数完全可以应付蓝桥杯了. 下面的外设部分本意是我个人复习之用,所以可能解析写的不是很详细.

外设

串口

E2PROM 持久化存储

DS1302 时间存储

超声波

DS18B20 温度传感器

PCF8591 ADC/DAC

NE555

本文还没写完,先发上来看看效果 /doge

本文作者: Genkaim

本文链接: https://www.genkaim.top/posts/eabfbc74

打赏博主😘

bilibili发电⚡
Alipay (移动端)