前言
本人在(2024年)第十五届蓝桥杯竞赛单片机赛道中取得了全国一等奖的成绩。本文我将会和读者们分享蓝桥杯单片机竞赛中可能会涉及到的所有模块的代码。
官方资料获取(已整合):
蓝桥杯单片机资源包(含指导手册和考点大纲)
优质教程推荐:
蚂蚁工厂科技蓝桥杯单片机教程
小蜜蜂蓝桥杯单片机基础技能与进阶强化教程
其他相关文章:
【提分必看!】蓝桥杯单片机提分技巧(国一经验分享)
本届蓝桥杯单片机国赛赛题被参赛选手们评价为办赛以来最难的赛题,我也非常赞同这个观点。我个人认为赛题难在其非常创新,涉及的考点很多,完成题目需要考虑的问题也很多,需要编写的代码量也很大。
要想在难度如此大的赛题中做到条理清晰地编程,就需要对赛题可能涉及到的模块的代码有足够的了解,且能做到熟练编写和使用。下面我就来和大家分享蓝桥杯单片机比赛中可能会用到的所有的模块代码。
(本文不会详细解释每一行代码是如何编写的,读者可搭配开发手册和单片机开发板的原理图来理解代码。)
基础模块
初始化
bsp_init.c
#include "bsp_init.h"
void Cls_Waishe(void)
{
// 初始化关闭LED灯
P0=0xff;
P2=P2&0x1F|0x80;
P2=P2&0x1F;
// 初始化关闭蜂鸣器、继电器等外设
P0=0x00;
P2=P2&0x1F|0xA0;
P2=P2&0x1F;
}
bsp_init.h
#include "STC15F2K60S2.H"
void Cls_Waishe(void);
初始化函数要在主函数最开始的地方调用,用以消除开发板重新上电后LED灯不规则亮灭和蜂鸣器异常发声的情况。
LED灯
bsp_led.c
#include "bsp_led.h"
// 1对应灯亮,0对应灯灭
void Led_Disp(unsigned char ucLed)
{
// 这里做了按位取反
P0=~ucLed;
P2=P2&0x1F|0x80;
P2=P2&0x1F;
}
bsp_led.h
#include "STC15F2K60S2.H"
void Led_Disp(unsigned char ucLed);
Key按键
bsp_key.c
#include "bsp_key.h"
// 独立键盘
unsigned char Read_BTN(void)
{
unsigned char Key_Val;
if(P30==0) Key_Val=7;
else if(P31==0) Key_Val=6;
else if(P32==0) Key_Val=5;
else if(P33==0) Key_Val=4;
else Key_Val=0;
return Key_Val;
}
// 矩阵键盘
unsigned char Read_KBD(void)
{
unsigned char Key_Val;
unsigned int Key_New;
// 这里的P34需要特别注意,如果与555接收频率冲突的话,这里的P34要去掉才能正常读取555的频率!
P44=0;P42=1;P35=1;P34=1;
Key_New=P3&0x0F;
P44=1;P42=0;P35=1;P34=1;
Key_New=(Key_New<<4)|(P3&0x0F);
P44=1;P42=1;P35=0;P34=1;
Key_New=(Key_New<<4)|(P3&0x0F);
P44=1;P42=1;P35=1;P34=0;
Key_New=(Key_New<<4)|(P3&0x0F);
switch(~Key_New)
{
case 0x8000:Key_Val=4;break;
case 0x4000:Key_Val=5;break;
case 0x2000:Key_Val=6;break;
case 0x1000:Key_Val=7;break;
case 0x0800:Key_Val=8;break;
case 0x0400:Key_Val=9;break;
case 0x0200:Key_Val=10;break;
case 0x0100:Key_Val=11;break;
case 0x0080:Key_Val=12;break;
case 0x0040:Key_Val=13;break;
case 0x0020:Key_Val=14;break;
case 0x0010:Key_Val=15;break;
case 0x0008:Key_Val=16;break;
case 0x0004:Key_Val=17;break;
case 0x0002:Key_Val=18;break;
case 0x0001:Key_Val=19;break;
default : Key_Val=0;break;
}
return Key_Val;
}
bsp_key.h
#include "STC15F2K60S2.H"
unsigned char Read_BTN(void);
unsigned char Read_KBD(void);
比赛时独立键盘和矩阵键盘只会用到其中一种,这取决于J5跳帽是接“BTN”还是“KBD”,比赛时写题目要求的那种就行。独立键盘是最简单的,矩阵键盘较复杂,要写扫描逻辑,而且得注意和555频率测量模块冲突的问题。
Seg数码管
bsp_seg.c
#include "bsp_seg.h"
void Seg_Tran(unsigned char* seg_string,unsigned char* seg_buf)
{
unsigned char i,j=0;
unsigned char temp;
for(i=0;i<=7;i++,j++)
{
switch(seg_string[j])
{
case '0':temp=0xc0;break;
case '1':temp=0xf9;break;
case '2':temp=0xa4;break;
case '3':temp=0xb0;break;
case '4':temp=0x99;break;
case '5':temp=0x92;break;
case '6':temp=0x82;break;
case '7':temp=0xf8;break;
case '8':temp=0x80;break;
case '9':temp=0x90;break;
case 'A':temp=0x88;break;
case 'B':temp=0x83;break;
case 'C':temp=0xc6;break;
case 'D':temp=0xa1;break;
case 'E':temp=0x86;break;
case 'F':temp=0x8e;break;
// 下面这几个需要单独背(现场推算也行)
case 'H':temp=0x89;break;
case 'L':temp=0xC7;break;
case 'N':temp=0xC8;break;
case 'P':temp=0x8C;break;
case 'U':temp=0xC1;break;
case '-':temp=0xBF;break;
case ' ':temp=0xFF;break;
default :temp=0xFF;break;
}
if(seg_string[j+1]=='.')
{
temp&=0x7F;
j++;
}
seg_buf[i]=temp;
}
}
void Seg_Disp(unsigned char* seg_buf,unsigned char pos)
{
P0=1<<pos;
P2=P2&0x1F|0xC0;
P2=P2&0x1F;
// 数码管消隐操作
P0=0xff;
P2=P2&0x1F|0xE0;
P2=P2&0x1F;
P0=seg_buf[pos];
P2=P2&0x1F|0xE0;
P2=P2&0x1F;
}
bsp_seg.h
#include "STC15F2K60S2.H"
void Seg_Tran(unsigned char* seg_string,unsigned char* seg_buf);
void Seg_Disp(unsigned char* seg_buf,unsigned char pos);
每个8位数码管对应“DP G F E D C B A”,DP表示小数点。开发板上的数码管是共阳数码管,所以0(低电平)对应亮,1(高电平)对应灭。除了以上列举的字符,往届比赛真题中还出现过“上划线”和“下划线”字符,这里留给读者自己推理。
DS18B20温度测量模块
bsp_onewire.c
#include "bsp_onewire.h"
//
void Delay_OneWire(unsigned int t)
{
unsigned char i;
while(t--){
for(i=0;i<12;i++);
}
}
//
void Write_DS18B20(unsigned char dat)
{
unsigned char i;
for(i=0;i<8;i++)
{
DQ = 0;
DQ = dat&0x01;
Delay_OneWire(5);
DQ = 1;
dat >>= 1;
}
Delay_OneWire(5);
}
//
unsigned char Read_DS18B20(void)
{
unsigned char i;
unsigned char dat;
for(i=0;i<8;i++)
{
DQ = 0;
dat >>= 1;
DQ = 1;
if(DQ)
{
dat |= 0x80;
}
Delay_OneWire(5);
}
return dat;
}
//
bit init_ds18b20(void)
{
bit initflag = 0;
DQ = 1;
Delay_OneWire(12);
DQ = 0;
Delay_OneWire(80);
DQ = 1;
Delay_OneWire(10);
initflag = DQ;
Delay_OneWire(5);
return initflag;
}
// 以下需要自己编写,上面的代码比赛时会提供
unsigned int re_temp(void)
{
unsigned char low,high;
init_ds18b20();
Write_DS18B20(0xCC);//跳过ROM
Write_DS18B20(0x44);//进行温度转换,结果存放在暂存器
init_ds18b20();
Write_DS18B20(0xCC);//跳过ROM
Write_DS18B20(0xBE);//读取温度(读取暂存器)
low=Read_DS18B20();
high=Read_DS18B20();
return (high<<8)|low;
}
bsp_onewire.h
#include "STC15F2K60S2.H"
sbit DQ=P1^4;
unsigned int re_temp(void);
比赛时会给出大部分的ds18b20底层代码,我们只需要在其基础上加入头文件和re_temp
函数。
DS1302时钟模块
bsp_ds1302.c
#include "bsp_ds1302.h"
//
void Write_Ds1302(unsigned char temp)
{
unsigned char i;
for (i=0;i<8;i++)
{
SCK = 0;
SDA = temp&0x01;
temp>>=1;
SCK=1;
}
}
//
void Write_Ds1302_Byte( unsigned char address,unsigned char dat )
{
RST=0; _nop_();
SCK=0; _nop_();
RST=1; _nop_();
Write_Ds1302(address);
Write_Ds1302(dat);
RST=0;
}
//
unsigned char Read_Ds1302_Byte ( unsigned char address )
{
unsigned char i,temp=0x00;
RST=0; _nop_();
SCK=0; _nop_();
RST=1; _nop_();
Write_Ds1302(address);
for (i=0;i<8;i++)
{
SCK=0;
temp>>=1;
if(SDA)
temp|=0x80;
SCK=1;
}
RST=0; _nop_();
SCK=0; _nop_();
SCK=1; _nop_();
SDA=0; _nop_();
SDA=1; _nop_();
return (temp);
}
// 以下需要自己编写,上面的代码比赛时会提供
// 设置时钟时间
void Set_Rtc(unsigned char* ucRtc)
{
unsigned char temp;
Write_Ds1302_Byte(0x8e,0);
temp=((ucRtc[0]/10)<<4)|(ucRtc[0]%10);
Write_Ds1302_Byte(0x84,temp); // 设置时
temp=((ucRtc[1]/10)<<4)|(ucRtc[1]%10);
Write_Ds1302_Byte(0x82,temp); // 设置分
temp=((ucRtc[2]/10)<<4)|(ucRtc[2]%10);
Write_Ds1302_Byte(0x80,temp); // 设置秒
Write_Ds1302_Byte(0x8e,0x80);
}
// 读取时钟时间
void Read_Rtc(unsigned char* ucRtc)
{
unsigned char temp;
temp=Read_Ds1302_Byte(0x85);
ucRtc[0]=((temp>>4)*10)+(temp&0x0F);
temp=Read_Ds1302_Byte(0x83);
ucRtc[1]=((temp>>4)*10)+(temp&0x0F);
temp=Read_Ds1302_Byte(0x81);
ucRtc[2]=((temp>>4)*10)+(temp&0x0F);
}
bsp_ds1302.h
#include "STC15F2K60S2.H"
#include "intrins.h"
sbit SCK=P1^7;
sbit SDA=P2^3;
sbit RST=P1^3;
void Set_Rtc(unsigned char* ucRtc);
void Read_Rtc(unsigned char* ucRtc);
这里头文件的引脚配置需要严格按照原理图来操作。
AT24C02存储模块
bsp_iic.c
#include "bsp_iic.h"
#define DELAY_TIME 5
//
static void I2C_Delay(unsigned char n)
{
do
{
_nop_();_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();_nop_();
}
while(n--);
}
//
void I2CStart(void)
{
sda = 1;
scl = 1;
I2C_Delay(DELAY_TIME);
sda = 0;
I2C_Delay(DELAY_TIME);
scl = 0;
}
//
void I2CStop(void)
{
sda = 0;
scl = 1;
I2C_Delay(DELAY_TIME);
sda = 1;
I2C_Delay(DELAY_TIME);
}
//
void I2CSendByte(unsigned char byt)
{
unsigned char i;
for(i=0; i<8; i++){
scl = 0;
I2C_Delay(DELAY_TIME);
if(byt & 0x80){
sda = 1;
}
else{
sda = 0;
}
I2C_Delay(DELAY_TIME);
scl = 1;
byt <<= 1;
I2C_Delay(DELAY_TIME);
}
scl = 0;
}
//
unsigned char I2CReceiveByte(void)
{
unsigned char da;
unsigned char i;
for(i=0;i<8;i++){
scl = 1;
I2C_Delay(DELAY_TIME);
da <<= 1;
if(sda)
da |= 0x01;
scl = 0;
I2C_Delay(DELAY_TIME);
}
return da;
}
//
unsigned char I2CWaitAck(void)
{
unsigned char ackbit;
scl = 1;
I2C_Delay(DELAY_TIME);
ackbit = sda;
scl = 0;
I2C_Delay(DELAY_TIME);
return ackbit;
}
//
void I2CSendAck(unsigned char ackbit)
{
scl = 0;
sda = ackbit;
I2C_Delay(DELAY_TIME);
scl = 1;
I2C_Delay(DELAY_TIME);
scl = 0;
sda = 1;
I2C_Delay(DELAY_TIME);
}
// 以下四个函数需要自己编写
unsigned char Pcf_ADC(unsigned char channel)
{
unsigned char temp;
I2CStart();
I2CSendByte(0x90);
I2CWaitAck();
I2CSendByte(channel);
I2CWaitAck();
I2CStart();
I2CSendByte(0x91);
I2CWaitAck();
temp=I2CReceiveByte();
I2CSendAck(1);// 不应答
I2CStop();
return temp;
}
void Pcf_DAC(unsigned char tran_data)
{
I2CStart();
I2CSendByte(0x90);
I2CWaitAck();
I2CSendByte(0x41);//随便选,主要是激活通道
I2CWaitAck();
I2CSendByte(tran_data);
I2CWaitAck();
I2CStop();
}
void EEPROM_Write(unsigned char* string,unsigned char addr,unsigned char num)
{
I2CStart();
I2CSendByte(0xA0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
while(num--)
{
I2CSendByte(*string++);
I2CWaitAck();
I2C_Delay(200);
}
I2CStop();
}
void EEPROM_Read(unsigned char* string,unsigned char addr,unsigned char num)
{
I2CStart();
I2CSendByte(0xA0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
I2CStart();
I2CSendByte(0xA1);
I2CWaitAck();
while(num--)
{
*string++=I2CReceiveByte();
if(num) I2CSendAck(0);
else I2CSendAck(1);
}
I2CStop();
}
bsp_iic.h
#include "STC15F2K60S2.H"
#include "intrins.h"
#define GUANG 0x41 // 光敏电阻地址
#define ADJUST 0x43 // 可调电阻地址
sbit scl=P2^0;
sbit sda=P2^1;
unsigned char Pcf_ADC(unsigned char channel);
void Pcf_DAC(unsigned char tran_data);
void EEPROM_Write(unsigned char* string,unsigned char addr,unsigned char num);
void EEPROM_Read(unsigned char* string,unsigned char addr,unsigned char num);
我认为在所有模块里,IIC这一块是最难的,涉及到的代码比较多,而且也很容易写错,大家在备赛时一定要反复多次地练习这一段代码。
Timer定时器模块
Timer.c
#include "Timer.h"
//如果用定时器2(注意要使用对应的回调函数:void t2int() interrupt 12)
void Timer2Init(void) //1毫秒@12.000MHz
{
AUXR &= 0xFB; //定时器时钟12T模式
T2L = 0x18; //设置定时初值
T2H = 0xFC; //设置定时初值
AUXR |= 0x10; //定时器2开始计时
IE2 |= 0x04; // 这是IE2不要写错了!!!
}
//如果用定时器1(注意要使用对应的回调函数:void tm1_isr() interrupt 3)
void Timer1Init(void) //1毫秒@12.000MHz
{
AUXR &= 0xBF; //定时器时钟12T模式
TMOD &= 0x0F; //设置定时器模式
TL1 = 0x18; //设置定时初值
TH1 = 0xFC; //设置定时初值
TF1 = 0; //清除TF1标志
TR1 = 1; //定时器1开始计时
ET1 = 1;
}
如果用定时器0(注意要使用对应的回调函数:void tm0_isr() interrupt 1)
void Timer0Init(void) //1毫秒@12.000MHz
{
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
ET0 = 1;
}
Timer.h
#include "STC15F2K60S2.H"
void Timer2Init(void);
//或者
void Timer1Init(void);
//或者
void Timer0Init(void);
竞赛开发板IAP15F2K61S2只有三个定时器:Timer0、Timer1、Timer2。这里均演示定时器16位自动重装载模式下的使用。(可以简单理解为周期计时,周期中断的使用。)
NE555频率测量模块
bsp_555.c
#include "bsp_555.h"
void Freq_Timer0Init(void)
{
AUXR &= 0x7F; //定时器时钟12T模式
TMOD &= 0xF0; //设置定时器模式
// 下面这行是要自己加上的,stc工具配置没有这个!
TMOD |= 0x04; //这里参考4T的答案来设置,蚂蚁工厂科技的是0x05
TL0 = 0; //设置定时初值
TH0 = 0; //设置定时初值
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
}
// 下面是main函数部分关于555的测试
if(++ms_tick==1000)// 1毫秒
{
ms_tick=0;
TR0 = 0;
freq=((TH0<<8)|TL0);
TL0 = 0;
TH0 = 0;
TR0 = 1;
}
bsp_555.h
#include "STC15F2K60S2.H"
void Freq_Timer0Init(void);
通过查看芯片开发手册可以知道,555测量频率,只能通过将定时器 0 初始化为计数模式,对输入到 P34(T0)的脉冲进行计数。如果在赛题中出现了频率测量的题目,就只能用定时器0来处理,此时不能再将定时器0用于周期计数。
另外,由于定时器通过读取P34引脚的脉冲进行计数,而上文中在矩阵键盘编写里也提到了引脚冲突的问题,这时就需要将矩阵键盘里P34引脚检测给去掉。如下所示:
// 矩阵键盘
unsigned char Read_KBD(void)
{
unsigned char Key_Val;
unsigned char Key_New;
// 这里的P34需要特别注意,如果与555接收频率冲突的话,这里的P34要去掉才能正常读取555的频率!
P44=0;P42=1;
Key_New=P3&0x0F;
P44=1;P42=0;
Key_New=(Key_New<<4)|(P3&0x0F);
switch(~Key_New)
{
case 0x80:Key_Val=4;break;
case 0x40:Key_Val=5;break;
case 0x20:Key_Val=6;break;
case 0x10:Key_Val=7;break;
case 0x08:Key_Val=8;break;
case 0x04:Key_Val=9;break;
case 0x02:Key_Val=10;break;
case 0x01:Key_Val=11;break;
default : Key_Val=0;break;
}
return Key_Val;
}
看到这里读者可能会问,如果题目考了555频率测量,又考了矩阵键盘P34相关的按键检测怎么办?我在备赛时也考虑过这个问题,但事实是看了近几年来的省国赛考题,都没有出现过这种刁钻的题目,所以这种情况出现在考题的概率不大。如果真的出了,那读者可以尝试通过设置标志位来分隔开两种操作(状态),从而解决这个问题。(题目不可能要求你测量555频率的同时又让你检测P34相关的按键状态。)
UART串口模块
Uart.c
#include "Uart.h"
//考试时要求的波特率一般为9600
// IAP15F2K61S2单片机没有独立波特率发生器,只能选择定时器1或者定时器2为波特率发生器
void UartInit(void) //9600bps@12.000MHz
{
SCON = 0x50; //8位数据,可变波特率
AUXR |= 0x01; //串口1选择定时器2为波特率发生器
AUXR |= 0x04; //定时器2时钟为Fosc,即1T
T2L = 0xC7; //设定定时初值
T2H = 0xFE; //设定定时初值
AUXR |= 0x10; //启动定时器2
ES = 1;
}
// 发送逻辑
void Uart_Send(unsigned char* uart_string)
{
while(*uart_string! ='\0')
{
SBUF=*uart_string;
while(TI==0);
TI=0;
uart_string++;
}
}
// 以下是main.c里串口中断的写法:
/*----------------------------
UART 中断服务程序 (接收逻辑)
-----------------------------*/
void Uart() interrupt 4
{
if (RI)
{
RI = 0; //清除RI位
uart_rec_string[uart_index++]=SBUF;
}
}
Uart.h
#include "STC15F2K60S2.H"
void UartInit(void);
void Uart_Send(unsigned char* uart_string);
近几年省国赛都没考到串口通信,今年国赛终于考了串口,而且一考就考的特别难,读者看看原题就知道了:第十五届蓝桥杯单片机国赛真题·程序设计题。
比赛中串口的底层代码本身并不是很难,收发逻辑也是比较简单的,但是对应的使用场景可以设计的很难(就比如第十五届的国赛真题)。
超声波模块
bsp_sonic.c
#include "bsp_sonic.c"
void Sonic_Timer1Init(void) //12微秒@12.000MHz
{
AUXR &= 0xBF; //定时器时钟12T模式
TMOD &= 0x0F; //设置定时器模式
TL1 = 0xF4; //设置定时初值
TH1 = 0xFF; //设置定时初值
TF1 = 0; //清除TF1标志
TR1 = 0; //定时器1不计时
}
unsigned char Wave_Rec(void)
{
unsigned char num=2;
unsigned char dist;
Tx=0;
TL1 = 0xF4; //这里不要置零!
TH1 = 0xFF; //这里不要置零!
TR1 = 1;
while(num--)
{
while(!TF1);
TF1=0;
Tx^=1;
}
TR1 = 0;
TL1 = 0; //设置定时初值
TH1 = 0; //设置定时初值
TR1 = 1;
while(Rx && (~TF1));
TR1 = 0;
if(TF1==1)
{
TF1=0;
dist=250;
}
else
{
dist=((TH1<<8)|TL1)*0.017;
}
return dist;
}
bsp_sonic.h
#include "STC15F2K60S2.H"
sbit Tx=P1^0;
sbit Rx=P1^1;
void Sonic_Timer1Init(void);
unsigned char Wave_Rec(void);
当题目涉及到超声波模块时,难度就上来了。对于超声波模块的代码,我看过很多种形式的,不同教程教的都不一样,不过底层逻辑都是相同的,都是通过延时或定时器计数产生40kHz的方波信号驱动超声波发送探头,接收到反射脉冲后根据时间间隔计算距离。
上面这段代码需要用到一个定时器来计时/计数,优点是准确性高,缺点是占用了一个定时器,如果赛题涉及到的模块比较多,定时器资源就可能不够用(很多参赛选手在第十五届国赛时碰到了这个问题便无从下手,导致最后和国一无缘)。
读者看到这里可能就会想,如果定时器资源这么紧缺,为什么不用延时或者其他方法来处理超声波测距模块呢?是的,延时同样可以处理这个模块,但我这里为了保证测距准确性,依然是使用定时器来处理,对于定时器资源紧缺的问题,我有一个“秘密武器”来解决。限于篇幅问题,我就不在这里叙述,请参考我的另一篇文章:【提分必看!】蓝桥杯单片机提分技巧(国一经验分享)
写在最后
非常感谢“蚂蚁工厂科技”的竞赛教程,让我在备赛过程中少走了很多弯路,也非常感谢实验室的伙伴们在备赛过程中给予了我很大的帮助。祝接下来参加蓝桥杯大赛的选手们都能取得理想的成绩!
(欢迎大家在评论区指导交流~)