前言
我也是2024年蓝桥杯嵌入式的考生之一,在我备战蓝桥杯的过程中,将我总结到的难点以及经验分享给大家,其中包含基础模块的使用,和每个模块所对应的难点,以及各个模块配置的注意事项,无论你是以后的参赛者或是今年的参赛者,这个教程都对你有所帮助,希望大家可以耐心看完,因为肯定会有所收获,希望大家都能猛猛拿奖,上岸上岸!!!
模块一:led模块控制
CubeMX初始化配置:
led相关引脚有九个,其中八个为led驱动引脚,另外一个为led锁存引脚。
由板子的原理图中可以看到,因为led驱动引脚接了led灯的负级,所以CubeMX中将每个引脚全部设为high,让灯初始化状态全为灭。锁存引脚也默认为高电平,表示不锁存。
实现代码:
void led_disp(uint16_t led)
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_All,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOC,led<<8,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
}
注意事项:
不知道大家会不会遇到灯在加了lcd之后,灯会不受控制的乱跳,这是因为led的引脚和lcd的引脚发生了冲突,为了解决这个问题,只需要在执行lcd函数前面加上一句:
HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_RESET);
这句代码是为了让led锁存引脚有效,由于CubeMX初始化led为全灭的状态,只需要在lcd与led冲突之前,将锁存引脚锁住,即使lcd会改变led的引脚,但是锁存引脚会让led始终保持原状态。
led一大难点(如何让他不断闪烁):
目前,很多题都会设置某个led在0.2s之内不断的闪烁,其实这个也是有套路的。
前面可以知道,我们的led的控制函数为:
void led_disp(uint16_t led);
我们控制特定的特定的灯进行点亮的步骤为:
1、设置一个全局变量,我这里用的是led_mark。
2、通过对全局变量led_mark不断与或来控制某个灯的点亮和熄灭。
3、之后只需要将全局变量放入函数中即可。
led_disp(led_mark);
知道了如何点亮一个特定的灯之后,接下来就是如何让他进行闪烁:
这里可以举一个例子:led1每过0.2s闪烁一次。
1、让led1点亮的话,此时要将全局变量进行如下操作:
led_mark=led_mark|0x01;
2、让led2点亮的话,此时要将全局变量进行如下操作:
led_mark=led_mark&0xfe;
3、要让他不断闪烁,只需要每过0.2s就改变一次led_mark.
这个时候,问题来了,该如何让他每过0.s就改变一次led_mark呢?方法很简单,只需要采用定时器中断。
使用cubemx将定时器4初始化,切记要开NVIC全局中断,不然定时器不会进入中断回调函数。
代码如下:
uint32_t time;
uint8_t cnt;
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM4)
{
time++;
if(time&20==0)
{
cnt++;
if(cnt%2==0)
{
led_mark=led_mark|0x01;
}
else
{
led_mark=led_mark&0xfe;
}
}
}
代码解析:
1、设置两个变量,一个为time一个为cnt;
2、由定时器初始化配置可以知道,定时器4为每0.01s进一次中断,为了让0.2s就改变一次led,这个时候需要进20次定时器中断,对time进行%20的操作,相当于每0.2s改变一次led。
3、cnt的作用是为了让每0.2s翻转一次led,因为我们每0.2s改变一次led。设置个cnt,就可以让每次改变led的值不一样。
只需要这样,就能让led1进行周期为0.2s的闪烁。
在提醒一次,在使用定时器的基本中断功能的时候,要在main函数中调用;
HAL_TIM_Base_Start_IT(&htim4);
来开启定时器和中断。
模块二:按键(key)控制
CubeMX初始化配置:
按键可以说是每年必考的,这个模块十分重要,大家一定要理解透彻和熟练。
因为按键有4个,分别对应4个引脚,只需将4个对应的GPIO引脚设置为输入模式。
实现代码:
struct keys
{
char key_sta;
char judge_sta;
char single_sta;
};
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM4)
{
key[0].key_sta=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);
key[1].key_sta=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);
key[2].key_sta=HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);
key[3].key_sta=HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);
for(uint8_t i=0;i<4;i++)
{
switch(key[i].judge_sta)
{
case 0:
if(key[i].key_sta==0)
{
key[i].judge_sta=1;
}
break;
case 1:
if(key[i].key_sta==0)
{
key[i].judge_sta=2;
key[i].single_sta=1;
}
else
{
key[i].judge_sta=0;
}
break;
case 2:
if(key[i].key_sta==1)
{
key[i].judge_sta=0;
}
break;
}
}
}
}
代码解析:
1、以定时器4为基础,每过10ms对按键各个引脚进行一次判断。
2、如果检测到特定引脚变为低电平(按键被按下),key[i].single_sta=1;
3、接下来只需要不断在主函数检测key[i].single_sta是否为1,就可以知道哪个按键被按下。
void key_scan(void)
{
if(key[0].single_sta==1)
{
key[0].single_sta=0;
}
if(key[1].single_sta==1)
{
key[1].single_sta=0;
}
if(key[2].single_sta==1)
{
key[2].single_sta=0;
}
if(key[3].single_sta==1)
{
key[3].single_sta=0;
}
}
如发现key[i].single_sta==1,记得将它变回0,不然单片机会认为一直被按下。
注意事项:
1、在main函数,要手动打开定时器,调用函数为:HAL_TIM_Base_Start_IT(&htim4);(以定时器4为例子)
2、按键在没按下的时候,引脚电平为高,按下后,引脚电平为低。
模块三:液晶显示屏使用(lcd):
lcd初始化配置:
由于lcd驱动程序是由官方给予我们的,我们只需要将官方给的文件复制到我们的工程下面。
将lcd.c和lcd.h以及fonts.h复制到我们的工程目录下。千万别忘记将它同时添加到我们的工程下面。
实现代码:
1、对屏幕初始化以及设置背景色和文本色。(一般为黑色背景,白色字体)
LCD_Init();
LCD_Clear(Black);
LCD_SetBackColor(Black);
LCD_SetTextColor(White);
2、实现屏幕显示
void lcd_disp(void)
{
char text[20];
sprintf(text," DATA");
LCD_DisplayStringLine(Line1,(uint8_t *)text);
sprintf(text,"Volt:%.2f",adc_value);
LCD_DisplayStringLine(Line3,(uint8_t *)text);
sprintf(text,"D:%d%%",duty);
LCD_DisplayStringLine(Line4,(uint8_t *)text);
sprintf(text,"F:%5dHz",frq);
LCD_DisplayStringLine(Line5,(uint8_t *)text);
}
3、效果演示
代码解析:
1、设置一个数组来存储一行所显示的字符串。
2、通过sprintf()函数,将字符串写入该数组。
3、最后调用void LCD_DisplayStringLine(u8 Line, u8 *ptr),参数1为Linex(需要第几行显示),参数2为(uint8_t *)text,由于函数里面定义的参数2是uint8_t类型的指针变量,我们的数值是char类型的,所以需要把他强转为uint8_t类型的指针变量。
4、还可以将变量一起显示。
float adc_value;
sprintf(text,"Volt:%.2f",adc_value);
这样就可以显示变量的值了。
lcd如何高亮一行?
这是第十届的真题,需要对特定的一行进行高亮,看起来很高级,实际实现的方法很简单。
实现代码:
char text[20];
sprintf(text," Setting ");
LCD_DisplayStringLine(Line1,(uint8_t *)text);
if(para_choose==0) LCD_SetBackColor(Green);
sprintf(text," Max Volt:%.1fV ",Max_Volt);
LCD_DisplayStringLine(Line3,(uint8_t *)text);
LCD_SetBackColor(Black);
if(para_choose==1) LCD_SetBackColor(Green);
sprintf(text," Min Volt:%.1fV ",Min_Volt);
LCD_DisplayStringLine(Line5,(uint8_t *)text);
LCD_SetBackColor(Black);
if(para_choose==2) LCD_SetBackColor(Green);
sprintf(text," Upper:LD%d ",Upper_led);
LCD_DisplayStringLine(Line7,(uint8_t *)text);
LCD_SetBackColor(Black);
if(para_choose==3) LCD_SetBackColor(Green);
sprintf(text," Lower:LD%d ",Lower_led);
LCD_DisplayStringLine(Line9,(uint8_t *)text);
LCD_SetBackColor(Black);
代码解析:
实际上就是不断的改变背景色,在写入行直接,将背景色改为绿色,写入之后,在将背景色改回为黑色,最后就会呈现出特定的一行高亮了。
效果图:
注意事项:
1、要将特定文件添加到工程里面。
2、要记得调用LCD_Init(),来对lcd初始化。
3、lcd一共有10行,每行有20列,切记不要写超,不然显示会发生错误。
模块四:ADC电压采集
CubeMX初始化配置:
这里以PB15举例。
1、勾选IN15 Single_ended选项。
2、将模式选为independent mode。
3、其他全为默认。
实现代码:
double ADC_Getvalue(ADC_HandleTypeDef *hadc)
{
double adc;
adc=HAL_ADC_GetValue(hadc);
HAL_ADC_Start(hadc);
return adc*3.3/4096;
}
代码解析:
1、ADC转换的代码较为简单,只有短短几行,但是同样不能疏忽。
2、两个关键函数,HAL_ADC_GetValue(hadc);HAL_ADC_Start(hadc);采集电压值和开启ADC转换。
3、由于ADC的精度是12位的,和参考电压为3.3V,所以return adc*3.3/4096;
模块五:pwm输出(控制占空比和频率)
pwm输出原理:
该图片由江科大stm32入门教程提供,相关知识点可以跳转至江科大pwm章节教程学习
CubeMX初始化配置:
根据题目需要输出pwm的引脚来进行相应引脚的配置,我这里要求为PA1输出可调PWM波。
相应定时器配置为PWM Generation CHX,时基单元设置psc的值和counter值,在pwm配置里设置Pulse的值。
实现代码:
HAL_TIM_PWM_Start(&htim2,TIM_CHANNEL_2);
这样就开启了pwm输出,这个时候输出的频率和占空比为CubeMX配置时所设置的
如何改变pwm的频率和占空比呢?
我们这里就需要两条公式了:
输出波频率:Freq=CK_PSC/(PSC+1)/(ARR+1);
输出波占空比:Duty=CCR/(ARR+1);
1、我们配置时钟树时将系统时钟配置为80MHz,所以CK_PSC=80000000。
2、因为占空比设置为0%-100%,那么,我们一开始就将ARR设置为100,这个时候,CCR等于多少就意味着输出占空比为多少。
3、既然ARR已经固定,这个时候,若要改变Freq,只需要改变PSC的值。
4、改变需要调用的函数为:__HAL_TIM_SetCompare();__HAL_TIM_SET_PRESCALER();
这里以输出百分之50占空比,频率为1KHz的pwm举例。
只需要让ARR=100,CCR=50,PSC=800。
__HAL_TIM_SetCounter(&htim2,50);
__HAL_TIM_SET_PRESCALER(&htim2,800);
不需要再去设置ARR的值,因为在CubeMX初始化配置的时候,已经把它设置为100了。这样,无论题目要求你设置多少占空比和频率,你只需要将两条公式代入,就可以得到CCR和PSC的值,这样在将他们配置,就能输出对应的pwm了。
模块六:测量外部pwm的频率和占空比
测量原理:
该图片由江科大stm32入门教程提供,相关知识点可以跳转至江科大pwm章节教程学习
CubeMx初始化配置:
题目会要求对应的测量引脚,这里教程采用的是PB4引脚测量外部输入的频率和占空比。
1、因为是PB4引脚,在引脚中找到定时器3,通道1选项,进行勾选。
2、因为要同时测量频率和占空比,通道配置的时候要对通道1和通道2同时进行配置。
3、通道1为Input Capture direct mode,通道2为nput Capture indirect mode。
4、参数设置中,将通道1触发沿设置为Rising Edge,通道2触发沿设置为Falling Edge。
5、最后,记得打开NVIC中断。
实现代码:
double compare1,compare2;
uint frq1=0;//频率
float duty1=0//占空比
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM3)
{
if(htim->Channel==HAL_TIM_ACTIVE_CHANNEL_1)//中断消息来源 选择直接输入的通道
{
compare1=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);
compare2=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);
__HAL_TIM_SetCounter(htim,0);
frq1=(80000000/80)/ccr1_val2a;
duty1=(compare2/compare1)*100;
HAL_TIM_IC_Start(htim,TIM_CHANNEL_1);
HAL_TIM_IC_Start(htim,TIM_CHANNEL_2);
}
}
}
1、调用输入捕获中断回调函数。
2、判断是否为定时器3,且为通道1发生的中断。
3、由公式可以求出外部输入的频率和占空比。
4、求出频率和占空比有两条重要的公式:
Freq=CK_PSC/(PSC+1)/(CCR+1)
Duty=(CCR2/CCR1)*100
注意事项:
1、需要在主函数中开启捕获中断和相应的定时器。
HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_1);//频率测量捕获定时器开启
2、设置的两个捕获比较寄存器的值要用double类型,如果为两个整形,相除就会为0,这样占空比测量有误。
3、占空比的值是compare2/compare1,别写反。
模块七:Uart串口收发
CubeMX初始化配置:
1、将PA9和PA10设置为USART1_TX和USART1_RX。
2、将USART1的模式(mode)设为异步通信模式(Asynchronous)。
3、将波特率设置为9600Bit/s,其他为默认值。
4、打开串口1的NVIC中断。
HAL_UART_Receive_IT(&huart1,&rxdat,1);
实现代码:
uint8_t rxdat;//接收到的数据
char receive_dat[30];//将接收到的字符放在该字符串里面
uint8_t pointer;//接收数据指针
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance==USART1)
{
receive_dat[pointer++]=rxdat;
HAL_UART_Receive_IT(huart,&rxdat,1);
}
}
HAL_UART_Receive_IT(&huart1,&rxdat,1);
代码解析:
1、串口每接收到一个字符就进入串口接收中断回调函数。
2、设置一个接收数据的数组以及接收单个字符的变量(rxdat)。
3、通过指针pointer(这个pointer不是真正意义上的的指针),来将收到的字符不断的写入接收数据的数组。
4、退出中断之前,要加上HAL_UART_Receive_IT(&huart1,&rxdat,1);再次开启串口接收中断。
如何接收到对应长度的字符串以及如何判断串口输入的格式是否正确并向串口发送数据?
接收到对应长度的字符串:
拿12届省赛题来举例:
实现代码:
char car_type[5];
char car_data[5];
char car_time[13];
void uart_disp(void)
{
if(pointer==22)
{
sscanf(receive,"%4s:%4s:%12s",car_type,car_data,car_time);
}
else
{
char temp[30];
sprintf(temp,"Error");
HAL_UART_Transmit(&huart1,(uint8_t *)temp,strlen(temp),0xff);
}
pointer=0;
memset(receive,0,30);
}
}
代码解析:
1、定义三个字符数组存储串口发送来的数据,由于本教程为实例,只完成基础功能,在真题中,需要用到结构体数组。
2、当接收到的数据为22位的时候,就将串口接收到的数据用sscanf()函数,将它分割开,依次存入三个字符数组中。sscanf()函数的使用方法,可以点开下面的链接进行学习。https://blog.csdn.net/faihung/article/details/119325390
3、如果接收的不是22位数据,单片机就会认为数据接收错误,这个时候,就会将error通过
char temp[30];
sprintf(temp,"Error");
HAL_UART_Transmit(&huart1,(uint8_t *)temp,strlen(temp),0xff);
发送出去。
注意事项:
在调用void uart_disp(void)函数之前,要在while循环里面加上一个判断:
if(pointer!=0)
{
uint8_t temp=pointer;
HAL_Delay(1);
if(temp==pointer)
{
uart_disp();
}
}
这样子就确保了单片机收到全部字符之后才去调用void uart_disp(void)函数。
模块八:EEPROM模块
EERPROM初始化配置:
由于EEPROM的驱动程序是官方提供的,我们只需要将官方给的文件复制到我们的工程下面。
实现代码:
EEPROM写入:
void eeprom_write(unsigned char addr,unsigned char dat)
{
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
I2CSendByte(dat);
I2CWaitAck();
I2CStop();
}
EEPROM读出:
unsigned char eeprom_read(unsigned char addr)
{
unsigned char dat;
I2CStart();
I2CSendByte(0xa0);
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
I2CStart();
I2CSendByte(0xa1);
I2CWaitAck();
dat=I2CReceiveByte();
I2CSendNotAck();
I2CStop();
return dat;
}
代码解析:
1、官方中的i2.c中给我们写好了开始函数、结束函数、接收一个字节函数、发送一个字节函数、接收请求函数、发送请求函数。
2、我们只需要将官方给的函数,根据时序,封装成两个函数。
3、一个为eeprom特定地址读字节函数,另一个为eeprom特定地址写字节函数。
EEPROM难点:
我们知道,写入和读出一个uint8_t、char等占用八个字节类型的数据,只需要直接调用上面的
void eeprom_write(unsigned char addr,unsigned char dat)’
unsigned char eeprom_read(unsigned char addr);
因为他们都是一个字节的内存。
那么问题来了,如何将一个double类型的写入和读出呢?
1、我们知道,计算机系统中,不论何种类型的变量,在计算机中,都是0、1储存,类型的区别取决于计算机如何看待这些特定的0和1。
2、既然这样,我们只需要直接传0和1给EEPROM即可。
3、double类型占据8个字节,那么可以得出,写入一个double类型进入EEPROM中,需要连续写入八次,一共八个字节。
代码实现:
void I2C_EE_BufferWrite_Max_Volt(double *dat)
{
unsigned char *addr=(unsigned char *)dat;
for(uint8_t i=0;i<8;i++)
{
eeprom_write(i+2,*(addr+i));
HAL_Delay(5);
}
}
void I2C_EE_BufferWrite_Min_Volt(double *dat)
{
unsigned char *addr=(unsigned char *)dat;
for(uint8_t i=0;i<8;i++)
{
eeprom_write(i+10,*(addr+i));
HAL_Delay(5);
}
}
void I2C_EE_BufferRead_Max_Volt(void)
{
unsigned char *addr=(unsigned char *)&Max_Volt;
for(uint8_t i=0;i<8;i++)
{
*(addr+i)=eeprom_read(i+2);
}
}
void I2C_EE_BufferRead_Min_Volt(void)
{
unsigned char *addr=(unsigned char *)&Min_Volt;
for(uint8_t i=0;i<8;i++)
{
*(addr+i)=eeprom_read(i+10);
}
}
代码解析:
1、功能是将两个double类型的Max_Volt以及Min_Volt写入eeprom里面,并将他们读出来。
2、Max_Volt的八个字节存储在eeprom的地址2到地址10之间,Min_Volt的八个字节存储在eeprom的地址11到地址18之间。
3、将double类型的指针转为uint8_t类型的指针,传输到eeprom的写入和读出函数里面,并用for循环,循环八次,一次写入或者读出八个字节。
4、这样就是实现了eeprom对double类型变量的写入与读出了。
注意事项:
在连续写入EEPROM的时候,要对其进行适当的延迟,不然EEPROM会发生写入错误。
结束:
上述教程中包含所有基础模块的使用,和每个模块所对应的难点,以及各个模块配置的注意事项,如果你认真的看完,且都能理解,那么你蓝桥杯嵌入式学的以及不错了,但是想要冲击更高的奖项,这些只能说是基础,如果将各个模块结合在一起展现,这才是难点,为此需要多做几道真题,在真题中寻找相应的规律,并且锻炼自己的思维能力,只有真正做到这个,我相信一定可以取得一个不错的成绩,最后,忠心的希望各位都能拿到自己心仪的成绩,我们一起努力,加油!
我是第一次写这个,如果我有哪里写的不好的话,请多多包含,我接收大家的批评,如果觉得我写的不错的话,可以给我关注一下,这是对我最大的鼓励,谢谢大家。