蓝桥杯嵌入式赛道模板分享。萌新第一次做分享,如有不足,恳请多多指教。
这里可能不会写完,完整版请见:bilibili: 蓝桥杯单片机思路和模板分享|蓝桥杯国一
前言
首先声明,关于比赛,除驱动代码外,本人没有参考或看过任何人的代码,因此如有雷同,纯属巧合。关于驱动代码的参考来源,本人已在博客中详细给出。
这个模板同时适用于省赛和国赛。本人非与嵌入式强相关的专业,下面的讲解,或许并不严谨。
假设你已经熟悉驱动代码,但你仍然希望得到一种较为【清晰】的思路,可以稳定地逐步实现赛题中的各种要求,那么这个模板的结构也许能帮到你。不过没关系,我们还是会过一遍驱动代码的,只不过没有那么多的细节。
使用的资源:
字体:得意黑、JetBrains Mono等
软件&库:礦ision、STC-ISP、Windows10+的计算器、CodeSnap等
绪论
标识符取名仅供参考,希望不要在这上面浪费太多精力。
typedef简化
我比较习惯u8、i32这样的写法。
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(); update(); key_proc(); } }
|
LED、数码管和杂项
如你所愿,按你的来就好。仅供参考。
三大模块的基础
1 2 3 4
| #define attach(y, x) P27 = 0; P25 = (x); P26 = (y); P27 = 1; #define detach() P27 = 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 }; u8 nt_index; u8 nt_buf[] = {nt_blank, nt_blank, nt_blank, nt_blank, nt_blank, nt_blank, nt_blank, nt_blank}; #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_show();
}
|
注意:请把所有会用到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
| 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--; } }
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也行。
但是我建议:
- 不要传任何形参,有关变量只有全局变量
- 不要在里面写判断、循环等逻辑。遇到同一个变量的不同显示形式(例如用cm或m来显示距离),应该拆成两个函数并写上不同后缀(_cm和_m)
- 只写绘制相关的代码
这是由框架下的分工决定的。一定要分好工,不然到后面,容易一头雾水。
如果你有着良好的习惯,按你的来!
例如,我们要做如下格式的显示:
| 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) { }
void draw_para_example1(void) { nt_buf[0] = nt_p; nt_buf[1] = 0x01; nt_buf[2] = nt_buf[3] = nt_buf[4] = nt_blank; nt_show_dot_blank(7, para_example1, 4, 5); }
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); }
|
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: 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; } }
|
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: if (key_pressed) { return; } key_pressed = 1; interface++; interface %= 3; interface_para = 0; interface_distance = 0; break; default: key_pressed = 0; break; } }
|
这是一个经验结论:按键和界面的处理,应该写成switch。写if怎么会有问题呢?写if不会有任何问题的。写成switch是为了满足我们前面说的,在这个部分上“少动脑子”“保留精力”的原则。缩进break,与代码对齐,这样能方便区分功能。
更新数据
1 2 3 4
| void update(void) { }
|
灵活的timer
片上只有两个时钟可供我们挥霍,其中一个与频率计数深深绑定,我们只能最大化利用timer1了。一般来说,timer1都是以1ms为周期。
现在假设你要实现一个功能:当“湿度”(一般是让你读取频率,通过他们给出的式子转化得来)高于80%时,实现L3以0.1s为周期闪烁;低于20%时熄灭;在20%~80%(包含20%和80%)之间时常亮。
这里我分享一个压缩空间复杂度的方法,先看代码,特别注意timer_l3 == 0和timer_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; u8 humidity;
void draw(void) { if (humidity > 80) { if (timer_l3 == 0) { timer_l3 = 1; } if (timer_l3 > 100) { timer_l3 = 1; led_inv(2); } } else if (humidity >= 20 && humidity <= 80) { led_on(2); timer_l3 = 0; } else { led_off(2); timer_l3 = 0; } }
void update(void) { if (freq ...) { } }
void timer1_int(void) interrupt 3 { led_show(); if (timer_l3 > 0) { timer_l3++; } }
|
这里提供的节省空间开支的小技巧是,由于1ms这样短的时间不会被察觉,规定当timer_l3为0时表示控制l3闪烁的时钟关闭,大于0时为开启。在timer1中检测到timer_l3大于0就让它自增,当timer_l3增大到超过周期长度时,将它置1以表示重置。
实际上,这样写出来的timer(以100ms为周期控制某个东西),它的周期变成了99ms,但这不重要!因为根本看不出来差别。
这种方法不太适用于频率计数和超声波,除了二者以外,适用范围非常广阔。
涉及到超声波和频率计数的情况,我会在后面简单写一写。