目录
第十一届省赛还是比较简单的,不论使用的外设模块还是数码管LED界面显示的复杂度,都不难,但设计上仍然有一些注意点,文末附上自己写的整个工程压缩文件
指导书上的十一届预赛原题
1.基础模块编写
我们可以先不看题,直接先进行基础模块的编写,如数码管,LED,矩阵按键,75HC573片选函数等等,在参加蓝桥杯单片机组比赛的时候也可以先这样写底层代码来缓解紧张情绪。
(1)最先写的是74HC573片选函数,数据类型重定义
因为没它很多基础模块编写不起来
void inint_74HC573(u8 select) //74HC573片选函数
{
switch(select)
{
case 4: P2 = (P2 & 0X1F) | 0X80; break;//开启 Y4 LED端口 装填
case 5: P2 = (P2 & 0X1F) | 0XA0; break;//开启 Y5 ULN2003驱动端口 装填
case 6: P2 = (P2 & 0X1F) | 0XC0; break;//开启 Y6 数码管位选端口 装填
case 7: P2 = (P2 & 0X1F) | 0XE0; break;//开启 Y7 数码管段选端口 装填
}
}
这个片选函数没别的好介绍的,基本大家都是这么写的,
这个函数可以通过传入参数select的值来决定打开哪一片74HC573芯片,
从而进行相应的端口数据的锁存输出,从而控制数码管,LED的亮灭情况。
同时为了方便之后的码字,可以对一些数据类型进行重定义,给他们一个更简短的名字:
typedef unsigned int u16;
typedef unsigned char u8;
这样以后想写unsigned int 时可以用 u16 代替了。
如果你把所有函数放在主函数中,不会有什么问题,
但你要是和我一样喜欢封装很多“xxx.h”文件
(一种封装自己写的函数库的编程手法,可以通过头文件调用自己写的库函数)
一定不能忘记在别的函数用到u16时,声明这个库头文件。
比如我封装这俩句重定义在include “public.h”中,
我在include “smg.h”中如果要用到u16了,
那我必须声明一下include “public.h”这个头文件,否则报错。
(2)然后可以写关闭外设和数码管扫描的函数:
关于数码管函数的探索设计解释在这里:
关闭所有外设代码:
void cls_led_buzz()
{
inint_74HC573(4);
P0=0xff; //关led
inint_74HC573(5);
P0=0X00; //关蜂鸣器
inint_74HC573(6);
P0=0XFF; //关数码管位选
inint_74HC573(7);
P0=0XFF; //关数码管段选
}
数码管刷新函数,一次性地会打印八个数码管:
#include "smg.h"
//0 1 2 3 4 5 6 7 8 9 0. 1. 2. 3. 4. 5. 6. 7. 8. 9. 20空 21根线 22U 23P 24 N
u8 code smg_code[25]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,
0x40,0x79,0x24,0x30,0x19,0x12,0x02,0x78,0x00,0x10,
0xff,0xbf,0xc1,0x8c,0xc8
};
void Delay250us() //@12.000MHz
{
unsigned char i, j;
i = 3;
j = 232;
do
{
while (--j);
} while (--i);
}
void smg_display(u8 nr1,nr2,nr3,nr4,nr5,nr6,nr7,nr8)
{
u8 i;
i=0;
for(i=1;i<=8;i++)
{
inint_74HC573(6);
switch(i)
{
case 1:
P0=0x01;
inint_74HC573(7);
P0=smg_code[nr1];
Delay250us();
P0=0xff;
break;
case 2:
P0=0x02;
inint_74HC573(7);
P0=smg_code[nr2];
Delay250us();
P0=0xff;
break;
case 3:
P0=0x04;
inint_74HC573(7);
P0=smg_code[nr3];
Delay250us();
P0=0xff;
break;
case 4:
P0=0x08;
inint_74HC573(7);
P0=smg_code[nr4];
Delay250us();
P0=0xff;
break;
case 5:
P0=0x10;
inint_74HC573(7);
P0=smg_code[nr5];
Delay250us();
P0=0xff;
break;
case 6:
P0=0x20;
inint_74HC573(7);
P0=smg_code[nr6];
Delay250us();
P0=0xff;
break;
case 7:
P0=0x40;
inint_74HC573(7);
P0=smg_code[nr7];
Delay250us();
P0=0xff;
break;
case 8:
P0=0x80;
inint_74HC573(7);
P0=smg_code[nr8];
Delay250us();
P0=0xff;
break;
}
}
}
一次打印八个数码管,
不需要亮的数码管让其位选对应的段码=0xff即可,
例如,我想让最左边的数码管灭了,那传入参数nr1就等于20即可
(因为smg_code[20]=0xff,表示全灭);
这样的数码管打印方式是我本人创新且一直运用的,
算是一种比较稳定的显示打印方式了。
(3)然后可以写对LED操作的数组:
为何要写数组来操作LED呢?
因为很多时候题目中的LED,在同一时间不一定亮一个,有时可能要几个同时亮,
甚至在本题中存在,一个灯亮,另一个灯在闪烁的情况。
如果把每种情况排列组合的方式枚举出来将会很浪费时间。
此处我列出完整版亮灭操作的数组来解释:
(在工程中我只根据需要用到的灯 对这个完整版的数组进行了相应的缩减):
//与之进行相与运算,以打开对应led
//1_L1 2_L2 3_L3 4_L4 5_L5 6_L6 7_L7 8_L8
u8 code LED_codeON[10]={0xff,
0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f};
//与之进行相或运算,以关闭对应led
//1_L1 2_L2 3_L3 4_L4 5_L5 6_L6 7_L7 8_L8
u8 code LED_codeOFF[10]={0xff,
0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};
以上俩数组能够方便我们随时 叠加开关 LED1~LED8中的任何一个。
使用步骤如下:
1.在题目中,我们先定义一个unsigned char led_tmp; 可以给它 初始化赋值为0xff。
2.在不断刷新点亮LED时只需要这俩句:
if(i==15) //15ms刷新LED
{
inint_74HC573(4);
P0=led_tmp;
}
这样P0 就会每15ms 给LED的端口输出 led_tmp 的值。
3.我们在需要改变LED输出亮灭情况的 if语句下 对led_tmp进行赋值
就可以改变LED亮灭情况。
比如,我想让板子最左边的LED1亮或灭,我可以这样:
led_temp&=LED_codeON[1]; //让LED1亮
led_temp|=LED_codeOFF[1];//让LED1灭
这样进行与或运算得到的值,不会抹去上一位置的LED的亮灭状态,
而是让俩个位置LED状态叠加出现,
比直接进行让 P0=0xfe 之类的覆盖式赋值要实用;
(4)矩阵按键函数:
矩阵按键在蓝桥省赛经常考察,松手检测、短按、与定时器结合的长按等都是十分考验基础理解的操作。此处就不展示按键扫描写法了,可以自己下了工程在库函数“key.c”“key.h”翻看
2.主函数结构
主函数结构需要尽量简洁,我的主函数有以下几个部分:
1.whie(1)循环之外的初始化部分:
一般进行一些模块,外设关闭,定时器初始化,变量初始化的操作
有少量变量只有主函数需要用到来辅助。
(比如key_value用来接收返回的矩阵按键键值,帮助进行调用函数之类的操作)
2.循环内的标志位响应调用函数:
这是一种严格安排时间来调用模块函数的思想:
简单地介绍就是:
在定时器里用计数标志来定时刷新模块定义对应的标志位,
主函数if语句判断接收到标志位,就处理它对应的函数过程
标志位其实就是一个unsigned char 类型的变量。
比如,我想30ms扫描一次按键,那我的步骤大致如下:
1.定义标志位:u8 key_flag; //按键扫描标志
2.定义与之绑定的计数变量 比如 u8 key_count;用来计数定时器中断次数,来达到计时的目的。
3.在定时器服务函数里用计数变量计时,比如30ms刷新一次标志位让 key_flag=1;
(以下代码在“Timer.c”中)
仔细看此处为何是30ms:
因为定时器频率为1ms,计数变量i每1ms加一,
计数变量i每加到10就置零 并 让key_count加一,
key_count=3时才置零, 然后让key_flag=1;
完成了一次标志位的刷新;
(置1就是刷新了标志,所以主函数接收到if(key_flag==1),就执行操作了)
标志位刷新写法有注意点 :
1.到达刷新的时间,切勿忘记将计数的变量置0;
2.标志位置1被刷新,切勿忘记在主函数if中及时置回0;
3.有时可能程序一直执行不了,检查自己计数是否达到超过255 级别
就是计数变量的类型定义是否溢出了,unsigned char 不能计数超过255,
当超过溢出时就该把这个计数变量改为unsigned int 类型(不能超过65535)。
3.其他状态的判断函数:
比如这个题目中提到的误触判断,电压采集后的实时比较,有即时性,
因此我选择直接放在while(1)里即时进行判断。
这个写入EEPROM的函数我也直接放此处while(1),实现断电保护功能。
以下为电压比较函数本体,也是本题许多注意点存在的地方:
电压比较函数代码中没注释的变量:
Vp: 标准电压参数,float类型
value_read_AIN3: 读取ADC电压值,float类型
SMALL_FLAG: 电压小于Vp标准值
void dianya_cmp() //比较电压
{
static int bd_flag; //等于5V时,变化过程符合图像标志
static int bx_flag; //小于5V时,变化过程符合图像标志
// 因为浮点型数据不精确,所以判断相等 == 和 != 这俩种情况不会准确
// 所以下面 要强制转化为 int类型
if(vp==5) //标准电压等于5V时
{
if((unsigned int)(value_read_AIN3*100)<500) //第一次采集发现adc_value电压小于5V这标准
{bd_flag= 1;}
if((bd_flag==1)&&((unsigned int)(value_read_AIN3*100)==(unsigned int)(vp*100)))
{
bd_flag = 0; count++; //计数值加1
if(count==100){count= 99;} //count计数不会超过99
}
}
else //标准电压小于5V时
{
if((unsigned int)(value_read_AIN3*100)>(unsigned int)(vp*100))//第一次采集发现adc_value电压大于vp这标准
{bx_flag = 1;}
if((bx_flag == 1)&&((unsigned int)(value_read_AIN3*100)<=(unsigned int)(vp*100)))
{
bx_flag = 0; count++; //计数值加1
if(count==100){count = 99;} //count计数不会超过99
}
}
if(value_read_AIN3<vp) SMALL_FLAG = 1; //满足条件 让定时器1 开始为 L1 计时 5ms
else if(value_read_AIN3>vp) SMALL_FLAG = 0; //不满足条件就不计时
if(count%2!=0) {led_tmp=led_tmp&LED_mode[2];} //计数奇数触发L2亮
if(count%2==0) {led_tmp=led_tmp|LED_mode[5];}
}
注意点罗列:
1.因为ADC转换值接收的变量是定义的浮点型,所以注意数值比较是并不精确的
在float类型数据需要比较 等于(==) 的情况时,要强制类型转换。
2.注意分析计数触发图像,说明要我们进行俩次采集
而且只有第一次大于等于标准参数Vp,第二次小于Vp后,
才能让计数界面的变量 count 加一,其他情况是不计数的。
3.定时器程序设计结构
我用了俩个定时器,他们就是整个单片机工程运转时序的核心:
先说定时器0:
它是1ms中断一次,12T模式,
承担了以下任务:
1.刷新数码管
2.刷新按键标志位
3.刷新ADC采样标志位
void Timer0Init(void) //1毫秒@12.000MHz
{
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0x01; //设置定时器模式
TL0 = 0x18; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
EA=1;
ET0=1;
}
void Timer0_server() interrupt 1
{
u8 i;
u16 j;
TL0 = 0x18; //设置定时初始值
TH0 = 0xFC; //设置定时初始值
i++; //第一次测试忘了这个
j++;
if(i==10) //10ms刷新数码管
{
i=0;
key_count++;
smg_display_mode(smg_mode);
smg_display(nr1,nr2,nr3,nr4,nr5,nr6,nr7,nr8);
}
if(key_count==3) //30ms刷新按键标志
{
key_count=0;
key_flag=1;
}
if(j==78) //78ms刷新一次 adc 电位器 标志
{
j=0;
adc_flag=1;
}
}
定时器1承担以下任务:
1.刷新LED
2.接收判断SMALL_FLAG(测量电压小于标准超过5s点亮L1,超过后就灭掉重新计数计时)
void Timer1Init(void) //1毫秒@12.000MHz
{
AUXR &= 0xBF; //定时器时钟12T模式
TMOD &= 0x01; //设置定时器模式
TL1 = 0x18; //设置定时初始值
TH1 = 0xFC; //设置定时初始值
TF1 = 0; //清除TF1标志
TR1 = 1; //定时器1开始计时
EA=1;
ET1=1;
}
void timer1_server() interrupt 3
{
u8 i;
u16 kk;
TL1 = 0x18; //设置定时初始值
TH1 = 0xFC; //设置定时初始值
i++;
if(i==12) {inint_74HC573(4);P0=led_tmp;} //12ms刷新LED
if(SMALL_FLAG==1) {kk++;} //长时间V<Vp L1亮
if(SMALL_FLAG==0) {kk=0;}
if(kk>=5000) {led_tmp&=LED_mode[1];}
else if(kk<=5000){led_tmp|=LED_mode[4];}
}
4.最后的不完美程序说明
本程序是参加省赛前的练习,最近一次测试发现有些许不完美,ADC采样超过3.00V会开始数据大跳抖动,在0~2.5V是完全正常的,这我以后会仔细研究;
推荐将Vp参数用按键设置为0.5~1.5V来测试我程序的各项功能。
如果有看了我程序有不错改进思路的大佬,跪求私信联系解惑。