蓝桥杯嵌入式赛道模板分享和解读
蓝桥杯嵌入式赛道模板分享。萌新第一次做分享,如有不足,恳请多多指教。
这里可能不会写完,完整版请见:bilibili: 蓝桥杯单片机思路和模板分享|蓝桥杯国一
前言
首先声明,关于比赛,除驱动代码外,本人没有参考或看过任何人的代码,因此如有雷同,纯属巧合。关于驱动代码的参考来源,本人已在博客中详细给出。
这个模板同时适用于省赛和国赛。本人非与嵌入式强相关的专业,下面的讲解,或许并不严谨。
假设你已经熟悉驱动代码,但你仍然希望得到一种较为【清晰】的思路,可以稳定地逐步实现赛题中的各种要求,那么这个模板的结构也许能帮到你。不过没关系,我们还是会过一遍驱动代码的,只不过没有那么多的细节。
使用的资源:
字体:得意黑、JetBrains Mono等
软件&库:礦ision、STC-ISP、Windows10+的计算器、CodeSnap等
绪论
标识符取名仅供参考,希望不要在这上面浪费太多精力。
typedef简化
我比较习惯u8
、i32
这样的写法。
1 | typedef char i8; |
注意:多提一嘴,众所周知,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 | u16 freq; |
快速过一遍驱动代码
LED、数码管、继电器的操控,我们会在后面展开说。
- 按键扫描(BTN、矩阵键盘、特定区域、多点按键)
- PCF
- 温度传感器
- E2PROM
- 超声波*
- 串口通信
- 时钟初始化
- 时钟中断的写法
- Timer0的外部中断的写法
注意:我的建议是,在驱动和模板上少动一些脑子,减少出错的可能性,或者是降低查错难度,保留精力,精力留给后面,用于实现功能。
主函数
在本模板中,main函数里只有以下内容:
1 | void main(void) { |
LED、数码管和杂项
如你所愿,按你的来就好。仅供参考。
三大模块的基础
1 |
LED
1 | u8 led_statue = 0xFF; // 一定不要忘记写,不然灯全亮 |
数码管
数码管缓冲区,以及任意位置上的数码管显示。
1 |
|
如何记录那个数码管需要点亮小数点?我们不是再额外开一个数组去记录,或者是再加一个u8,而是直接在nt_buf[]的对应位置上或
进去一个0x80——给最高位置一。在输出前判断一下最高位是否为1(也就是& 0x80
),如果有
,那么就用nt_show_dot()
输出,否则
就用nt_show()
。
再多提一嘴:nt_show_dot()
的基本逻辑是,将段码最高位直接置一(& 0x7F)。为什么这样可以置一?请你改变一下对与运算的看法,把&右边的数字当成一个筛子,按位筛选,左边的是被筛的数字。右边的数字,如果位上为1,那么左边对应位上的值就可以通过;为0,就不可以通过,结果上对应的位也就直接是0了。
数码管时钟中断
1 | void timer1_int(void) interrupt 3 { |
注意:请把所有会用到write_xx()的代码,都放到timer1里,集中输出。
为什么:数码管显示一定会放在某timer里,timer0一般用于频率计数,那么数码管一定在timer1里;如果在timer外用到了write_xx(),在timer1中断的影像下,不出意外的话一般不会有事,但总有某个时刻会有事,特别是加上继电器控制之后。所以你应该将write_xx()相关的代码都放到timer1里。
向缓冲区数字:
1 | // 补0 |
显示整数为什么从右往左写入:因为蓝桥杯嵌入式显示数字,历·年·来都是右侧必定有数字,而左边可能是因为数位不足而补的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 | float para_example1; |
draw()
1 | u8 interface; |
key_proc()
按键处理:
1 | void key_proc(void) { |
这是一个经验结论:按键和界面的处理,应该写成switch。写if怎么会有问题呢?写if不会有任何问题的。写成switch是为了满足我们前面说的,在这个部分上“少动脑子”“保留精力”的原则。缩进break,与代码对齐,这样能方便区分功能。
更新数据
1 | void update(void) { |
灵活的timer
片上只有两个时钟可供我们挥霍,其中一个与频率计数深深绑定,我们只能最大化利用timer1了。一般来说,timer1都是以1ms为周期。
现在假设你要实现一个功能:当“湿度”(一般是让你读取频率,通过他们给出的式子转化得来)高于80%时,实现L3以0.1s为周期闪烁;低于20%时熄灭;在20%~80%(包含20%和80%)之间时常亮。
这里我分享一个压缩空间复杂度的方法,先看代码,特别注意timer_l3 == 0
和timer_l3 = 1;
的有关部分。
1 |
|
这里提供的节省空间开支的小技巧是,由于1ms这样短的时间不会被察觉,规定当timer_l3为0时表示控制l3闪烁的时钟关闭,大于0时为开启。在timer1中检测到timer_l3大于0就让它自增,当timer_l3增大到超过周期长度时,将它置1以表示重置。
实际上,这样写出来的timer(以100ms为周期控制某个东西),它的周期变成了99ms,但这不重要!因为根本看不出来差别。
这种方法不太适用于频率计数和超声波,除了二者以外,适用范围非常广阔。
涉及到超声波和频率计数的情况,我会在后面简单写一写。
蓝桥杯嵌入式赛道模板分享和解读