蓝桥杯嵌入式赛道模板分享和解读

蓝桥杯嵌入式赛道模板分享。萌新第一次做分享,如有不足,恳请多多指教。

这里可能不会写完,完整版请见:bilibili: 蓝桥杯单片机思路和模板分享|蓝桥杯国一

前言

首先声明,关于比赛,除驱动代码外,本人没有参考或看过任何人的代码,因此如有雷同,纯属巧合。关于驱动代码的参考来源,本人已在博客中详细给出。

这个模板同时适用于省赛和国赛。本人非与嵌入式强相关的专业,下面的讲解,或许并不严谨。

假设你已经熟悉驱动代码,但你仍然希望得到一种较为【清晰】的思路,可以稳定地逐步实现赛题中的各种要求,那么这个模板的结构也许能帮到你。不过没关系,我们还是会过一遍驱动代码的,只不过没有那么多的细节。

使用的资源:
字体:得意黑、JetBrains Mono等
软件&库:礦ision、STC-ISP、Windows10+的计算器、CodeSnap等

绪论

标识符取名仅供参考,希望不要在这上面浪费太多精力。

typedef简化

我比较习惯u8i32这样的写法。

1
2
3
4
5
6
typedef char i8;
typedef unsigned char u8;
typedef int i16;
typedef unsigned int u16;
typedef long i32;
typedef unsigned long u32;

注意:多提一嘴,众所周知,51上的int是16位的。

我的标识符取名办法

类似于linux,标志服采用下划线命名方式,且尽量缩短标识符长度。

常见的(似乎都是前缀)有:

  • timer:时间计数器
  • para:题目中要求设定的参数
  • freq:频率有关的
    • freq_counter:给timer0用的计数器
    • freq(直接叫这个):频率值(我一直采用Hz作为其单位)
    • timer_freq:频率的计数时长。我的timer1一般以1ms为周期,所以每当timer_freq=1000时,将freq_counter赋值给freq并清零。
  • draw:界面绘制有关的函数前缀
  • nt:Number Tubes,数码管(当然你也可以用Seg之类的,按你的来)。

本模板中的,value(或dat)都写作val。按你习惯来。

例如:

1
2
3
4
u16 freq;
u16 freq_counter;
u16 timer_freq;
u8 para_distance;

快速过一遍驱动代码

LED、数码管、继电器的操控,我们会在后面展开说。

  • 按键扫描(BTN、矩阵键盘、特定区域、多点按键)
  • PCF
  • 温度传感器
  • E2PROM
  • 超声波*
  • 串口通信
  • 时钟初始化
  • 时钟中断的写法
  • Timer0的外部中断的写法

注意:我的建议是,在驱动和模板上少动一些脑子,减少出错的可能性,或者是降低查错难度,保留精力,精力留给后面,用于实现功能。

主函数

在本模板中,main函数里只有以下内容:

1
2
3
4
5
6
7
8
void main(void) {
init();
while (1) {
draw(); // display也行,但是draw更短
update(); // 资源更新
key_proc(); // 按键处理
}
}

LED、数码管和杂项

如你所愿,按你的来就好。仅供参考。

三大模块的基础

1
2
3
4
#define attach(y, x) P27 = 0; P25 = (x); P26 = (y); P27 = 1;
#define detach() P27 = 0; // P25 = 0; P26 = 0;
#define write_0x00(y, x, val) P0 = 0x00; attach(y, x); P0 = (val); detach();
#define write_0xff(y, x, val) P0 = 0xFF; attach(y, x); P0 = (val); detach();

LED

1
2
3
4
5
u8 led_statue = 0xFF;       // 一定不要忘记写,不然灯全亮
#define led_show() write_0xff(0, 0, led_statue);
#define led_on(index) led_statue &= 0xFF ^ (1 << (index));
#define led_off(index) led_statue |= 1 << (index);
#define led_inv(index) led_statue ^= 1 << (index);

数码管

数码管缓冲区,以及任意位置上的数码管显示。

1
2
3
4
5
6
7
8
9
10
11
#define nt_blank 16
code nt_code[] = {
// 数码管代码
0xFF // nt_blank
};
u8 nt_index;
u8 nt_buf[] = {nt_blank, nt_blank, nt_blank, nt_blank,
nt_blank, nt_blank, nt_blank, nt_blank}; // 缓冲区本体,
// 一般来说,存储的是需要显示的字符在nt_code中对应的下标。
#define nt_show(pos, nt_val) write_0x00(1, 0, 1 << (pos)); write_0xff(1, 1, nt_code[nt_val]);
#define nt_show_dot(pos, nt_val) write_0x00(1, 0, 1 << (pos)); write_0xff(1, 1, nt_code[nt_val] & 0x7F);

如何记录那个数码管需要点亮小数点?我们不是再额外开一个数组去记录,或者是再加一个u8,而是直接在nt_buf[]的对应位置上进去一个0x80——给最高位置一。在输出前判断一下最高位是否为1(也就是& 0x80),如果,那么就用nt_show_dot()输出,否则就用nt_show()
再多提一嘴:nt_show_dot()的基本逻辑是,将段码最高位直接置一(& 0x7F)。为什么这样可以置一?请你改变一下对与运算的看法,把&右边的数字当成一个筛子,按位筛选,左边的是被筛的数字。右边的数字,如果位上为1,那么左边对应位上的值就可以通过;为0,就不可以通过,结果上对应的位也就直接是0了。

数码管时钟中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void timer1_int(void) interrupt 3 {
// 数码管
nt_index %= 8;
if (nt_buf[nt_index] & 0x80) {
nt_show_dot(nt_index, nt_buf[nt_index] & 0x80);
}
else {
nt_show(nt_index, nt_buf[nt_index]);
}
nt_index++;

// led
led_show();

// 继电器控制
// write_0x00(1, 0, relay_statue);
// relay_statue是u8,保存着输出时的整个P0的值
}

注意:请把所有会用到write_xx()的代码,都放到timer1里,集中输出。
为什么:数码管显示一定会放在某timer里,timer0一般用于频率计数,那么数码管一定在timer1里;如果在timer外用到了write_xx(),在timer1中断的影像下,不出意外的话一般不会有事,但总有某个时刻会有事,特别是加上继电器控制之后。所以你应该将write_xx()相关的代码都放到timer1里。

向缓冲区数字:

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
33
34
35
36
37
38
39
40
41
42
// 补0
void nt_buf_draw_len(u8 pos, u32 dat, u8 len) {
do {
nt_buf[pos] = dat % 10;
dat /= 10;
pos--;
} while(--len);
}

// 补空白、补负号
void nt_buf_draw_blank(u8 pos, i32 dat, u8 len) {
bit is_negative = dat < 0;
if (is_negative) {
dat = -dat;
}
do {
nt_buf[pos] = dat % 10;
dat /= 10;
pos--;
} while(--len && dat);
if (is_negative) {
nt_buf[pos] = nt_interval;
pos--;
len--;
}
while (len--) {
nt_buf[pos] = nt_blank;
pos--;
}
}

// 带小数点,补0
void nt_buf_draw_dot(u8 pos, u32 dat, u8 len, u8 pos_dot) {
nt_buf_draw_len(pos, dat, len);
nt_buf[pos_dot] |= 0x80; // 为小数点的存在,打标记
}

// 带小数点,补空白,补负号
void nt_buf_draw_dot_blank(u8 pos, i32 dat, u8 len, u8 pos_dot) {
nt_buf_draw_blank(pos, dat, len);
nt_buf[pos_dot] |= 0x80; // 为小数点的存在,打标记
}

显示整数为什么从右往左写入:因为蓝桥杯嵌入式显示数字,历·年·来都是右侧必定有数字,而左边可能是因为数位不足而补的0或空白。
为什么关于小数点的代码这样写:因为蓝桥杯只有8位数码管,资源逼仄,通常都是定长显示数字(当然不排除以后改变),小数点的位置历·年·来都是固定的,变化的只有小数点往左是补0还是补空白。
如你所愿,按你的来。

从赛题解读出发

首先让我们从赛题出发,熟悉或回顾一下蓝桥杯会要求我们做什么。然后再组织我们的框架。

注意:不建议跳过LED、数码管和杂项部分。

数据显示

知识储备:按键扫描、数码管显示、时钟初始化和时钟的中断

蓝桥杯每年都会要求我们使用数码管来显示一些数据。赛题里通常把它叫做“界面”,因为“界面”一定同时与两个事物关联:按键、显示。如何做好界面的切换呢?

以十四届的省赛和国赛真题为例,让我们这么做:

draw_xxx()

对每个需要显示的界面,单独封装成函数。当然你写#define也行。
但是我建议:

  1. 不要传任何形参,有关变量只有全局变量
  2. 不要在里面写判断、循环等逻辑。遇到同一个变量的不同显示形式(例如用cm或m来显示距离),应该拆成两个函数并写上不同后缀(_cm和_m)
  3. 只写绘制相关的代码
    这是由框架下的分工决定的。一定要分好工,不然到后面,容易一头雾水。
    如果你有着良好的习惯,按你的来!

例如,我们要做如下格式的显示:

P 1 空白 空白 空白 2. 3 3
P 1 空白 空白 para_example1 小数点 小数部分 小数部分
要求从第五位往右开始显示,保留两位小数,不足补零
P 2 空白 空白 空白 6 6 6
P 2 空白 para_example2 数字 数字 数字 数字

除此以外,还要求你以cm为单位显示距离,或者是以m为单位显示。

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
float para_example1;
float para_example2;

void draw_distance_cm(void) {
// ...
}

void draw_distance_m(void) {
// 是的,分开写!判断逻辑在后面的draw()里。
// ...
}

void draw_para_example1(void) {
nt_buf[0] = nt_p; // 在nt_code里实现一下显示P的段码,记录其下标值然后#define一个nt_p
nt_buf[1] = 0x01; // 官方会给出0-0x0F的段码,直接复制进nt_code里就可以实现这种对应了。
nt_buf[2] = nt_buf[3] = nt_buf[4] = nt_blank;
nt_show_dot_blank(7, para_example1, 4, 5); // pos val len dot_pos
}

void draw_para_example2(void) {
nt_buf[0] = nt_p;
nt_buf[1] = 0x02;
nt_buf[2] = nt_buf[3] = nt_blank;
nt_show_blank(7, para_example2, 7, 5); // pos val len
}

draw()

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
u8 interface;
u8 interface_para;
void draw(void) {
switch (interface) {
case 0:
// draw_xxx();
break;
case 1:
switch (interface_para) {
case 0:
draw_para_example1();
break;
case 1:
draw_para_example2();
break;
}
break;
case 2:
switch (interface_distance) {
case 0:
draw_distance_cm();
break;
case 1:
draw_distance_m();
break;
}
break;
}
// 下面实现一些关于LED的功能
// 巧用led_on()、led_off()、led_inv()
// 你可能需要结合timer_xxx来使用。后面的综合部分会讲。
}

key_proc()

按键处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void key_proc(void) {
static bit key_pressed = 0; // 防止按键按下之后的重复处理
// 当然了,如果想要实现类似于
// 按下按键后数字隔一段时间增长一下 的效果
// 可以考虑用计时器来处理。
switch (key_scan()) {
case 0xBB: // S8
// 每个case后紧跟这个。
if (key_pressed) { // 有按键被按下,那么后面的功能都不触发
return;
}
key_pressed = 1; // 记录有按键被按下
/////////////////////////////////////////////////////////
interface++;
interface %= 3; // 假设S8控制的是三个界面,那你就模3
// 根据历·年·题,直接将其他的界面变量置零即可
interface_para = 0;
interface_distance = 0;
break;
default:
key_pressed = 0; // 在default中,清除标记
break;
}
}

这是一个经验结论:按键和界面的处理,应该写成switch。写if怎么会有问题呢?写if不会有任何问题的。写成switch是为了满足我们前面说的,在这个部分上“少动脑子”“保留精力”的原则。缩进break,与代码对齐,这样能方便区分功能。

更新数据

1
2
3
4
void update(void) {
// 除了与时间增长(也就是timer_xxx++)有关的代码
// 上面没实现的功能都放这儿
}

灵活的timer

片上只有两个时钟可供我们挥霍,其中一个与频率计数深深绑定,我们只能最大化利用timer1了。一般来说,timer1都是以1ms为周期。

现在假设你要实现一个功能:当“湿度”(一般是让你读取频率,通过他们给出的式子转化得来)高于80%时,实现L3以0.1s为周期闪烁;低于20%时熄灭;在20%~80%(包含20%和80%)之间时常亮。

这里我分享一个压缩空间复杂度的方法,先看代码,特别注意timer_l3 == 0timer_l3 = 1;的有关部分。

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
33
34
35
36
37
38
39
40
41
42
43

u8 timer_l3; // ms
u8 humidity; // 用u8即可,一般不会要求到小数位。

void draw(void) {
// switch(interface)和其它代码
if (humidity > 80) {
if (timer_l3 == 0) {
timer_l3 = 1; // 置非零的数,表示该时钟开启
}
if (timer_l3 > 100) {
timer_l3 = 1;
led_inv(2); // l3翻转,但因为通过位移实现,所以这里应该写3-1=2
}
}
else if (humidity >= 20 && humidity <= 80) {
led_on(2);
timer_l3 = 0; // 关闭该时钟,手动写一下
}
else {
led_off(2);
timer_l3 = 0; // 关闭该时钟,手动写一下
}
// 上面的代码,如果你不想重复写timer_l3 = 0;你可以修改一下判断逻辑。随你的习惯!
}

void update(void) {
// 根据频率值计算湿度值
if (freq ...) {
// ...
}
}

void timer1_int(void) interrupt 3 {
// 数码管显示
// ...
led_show();
// 如果时钟开启,就timer_l3++
if (timer_l3 > 0) {
timer_l3++;
}
// ...
}

这里提供的节省空间开支的小技巧是,由于1ms这样短的时间不会被察觉,规定当timer_l3为0时表示控制l3闪烁的时钟关闭,大于0时为开启。在timer1中检测到timer_l3大于0就让它自增,当timer_l3增大到超过周期长度时,将它置1以表示重置。
实际上,这样写出来的timer(以100ms为周期控制某个东西),它的周期变成了99ms,但这不重要!因为根本看不出来差别。

这种方法不太适用于频率计数和超声波,除了二者以外,适用范围非常广阔。

涉及到超声波和频率计数的情况,我会在后面简单写一写。

作者

勇敢梧桐树

发布于

2024-06-05

更新于

2025-01-14

许可协议

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×