蓝桥杯备赛笔记(嵌入式)
蓝桥杯备赛笔记
- 新建工程
- 芯片选择
- 配置时钟
- 调试器配置
- 工程配置
- Keil5优化
- LED
- LCD
- KEY
- 按键单击
- 按键单击、双击判断
- 按键单击、长按后松手判断
- 单击、长按一定时间执行操作
- PWM输出
- 输入捕获
- ADC
- eeprom读写
- 串口通信
- RTC时钟
- 进阶
- LED闪烁
- LCD高亮显示
- LCD界面切换
- 输入捕获占空比
- 串口发送字符串
- BUG发现
- DMA串口收发
- 串口数据解析
- 判断是否是数字并解析打印
- 用串口实现按键功能
- 修改PWM输出频率
以下是本人备赛第十六届蓝桥杯嵌入式赛道的备赛笔记,方便复习,备赛时间为一周
新建工程
芯片选择
芯片为 STM32G431RBT6
配置时钟
题目要求主频是 80MHz
使用外部高速晶振
板子上的高速晶振是 24MHz
调试器配置
选择调试器
工程配置
工程命最好不要用中文,也不要有空格
Toolchain/IDE 选择 MDK-ARM,版本默认
第一个是打开项目文件夹,第二个是打开项目工程,即MDK工程
在文件夹中的 MDK-ARM 子文件夹中存放MDK工程文件
双击打开并编译,看有没有问题
配置调试器,选择DAP
勾上下载后自动复位
插上板子有下图所示信息说明能够正常烧录程序
比赛要求最后上传自己修改过的.c和.h文件
为了方便管理自己修改过的文件,先在工程文件夹中创建一个子文件夹
我命名为bsp
将该文件夹包含进来
同时在MDK中新加一个组,也命名为bsp
Keil5优化
LED
根据LED引脚原理图配置CubeMX,设置为输出模式,其中PD2用于控制锁存器的使能状态,高电平工作,反之不工作
将PC8-PC15和PD2都设为高电平,即LED默认都是熄灭状态
添加led.c和led.h文件专门用来存放与LED相关的代码
先点击左上角创建新文件,然后按Ctrl+s保存,分别命名为led.c 和led.h
右击刚刚创建的 bsp 组,将刚刚创建的两个文件添加到组中
led.c程序
#include"led.h"void led_dis(uchar dsled)//LED初始化
{HAL_GPIO_WritePin(GPIOC,GPIO_PIN_All,GPIO_PIN_SET);//高电平熄灭HAL_GPIO_WritePin(GPIOC,dsled<<8,GPIO_PIN_RESET);//写入控制锁存器HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET);HAL_GPIO_WritePin(GPIOC,GPIO_PIN_2,GPIO_PIN_RESET);//使能
}
led.h
#ifndef _LED_H_
#define _LED_H_#include"main.h"void led_dis(uchar dsled);#endif
为了方便可以在 main.h 文件中定义两个宏
现象就是每隔500ms,LED交替闪烁
上面是一种LED的控制代码,下面是第二种
LCD
因为LCD的驱动官方都写好了,我们只需要移植官方的驱动代码,然后调用相应函数就可以了
在官方提供的文件中找到LCD_Driver/MDK5_LCD_AHL目录
将上面三个文件复制到我们自己创建的bsp文件夹中
添加这三个文件
将LCD的引脚全部配置为输出模式
在主函数包含LCD的头文件 lcd.h
和stdio.h
头文件,LCD的输出要用到sprintf()函数,这个函数包含于stdio.h
文件中
初始化
LCD_Init();
LCD_Clear(Black);
LCD_SetBackColor(Black);
LCD_SetTextColor(White);char buf[20] = {0};
显示
while (1){/* USER CODE END WHILE */sprintf(buf, " OKay ");LCD_DisplayStringLine(Line1, (unsigned char *)buf);sprintf(buf, " One:%d ", 1);LCD_DisplayStringLine(Line3, (unsigned char *)buf);sprintf(buf, " Two:%d ", 2);LCD_DisplayStringLine(Line5, (unsigned char *)buf);sprintf(buf, " Two:%d ", i);LCD_DisplayStringLine(Line7, (unsigned char *)buf);/* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}
效果
KEY
按键控制要将IO口配置为输出模式
设置为上拉模式,因为原理图中按键按下后相当于IO口接地,即低电平,因为按键未触发时应该是处于高电平,不然无法检测按键的按下与否
使用定时器来进行按键判断,使用通用定时器4
开启NVIC
创建专门用于存放中断回调函数的头文件和源文件
按键单击
思路:定义两个变量,一个用于存放上一次按键的状态,一个用于存放现在按键的状态,上一次状态为1,现在的状态为0,则按键按下
用按键1判断单击
//按键状态
uint8_t key_stat[4] = {0};
uint8_t key_last_stat[4] = {1};void key_scan(void)
{key_stat[0] = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);key_stat[1] = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);key_stat[2] = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);key_stat[3] = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);//按键1实现单击判断if(key_stat[0] == 0 && key_last_stat[0] == 1){//单击后对应的操作}//将现态保存,以便下一次判断key_last_stat[0] = key_stat[0];key_last_stat[1] = key_stat[1];key_last_stat[2] = key_stat[2];key_last_stat[3] = key_stat[3];
试一下把key_stat[4] 和 key_last_stat[4] 这两个变量放在key_scan()函数里面定义,效果是不一样的
按键单击、双击判断
思路:使用定时器和系统时钟进行判断,定义两个时间变量,一个用于存放上一次按键按下的时间戳,另一个存放现在按键按下的时间戳,比较两次按键按下的时间间隔,如果间隔小于某个值,例如300ms,则说明是双击,则执行双击的操作,同时要用一个变量存储第一次按下按键后运行的时间,如果时间超过300ms没有再次按下按键,则说明是单击,则执行单击的操作,判断完单击和双击后要将时间和按下的标志位清零,用于下一次按键的判断
//按键2单击、双击判断
uint16_t k2_currunt_time = 0;//当前按键按下的时间戳
uint16_t k2_last_time = 0;//上一次按键按下的时间戳
uint16_t k2_press_time = 0;//按键按下的时间
uint16_t k2_press_flag = 0;//按键按下的标志位
uint16_t k2_press_count = 0;//按键按下的次数void key_scan(void)
{key_stat[0] = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);key_stat[1] = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);key_stat[2] = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);key_stat[3] = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);//按键2实现单击双击判断if(key_stat[1] == 0 && key_last_stat[1] == 1){k2_currunt_time = HAL_GetTick();//系统开始运行就会计时if(k2_press_count && (k2_currunt_time - k2_last_time < 300)){//双击操作k2_press_flag = 0;//清除标志位k2_press_count = 0;//清除按键按下次数}else{k2_last_time = k2_currunt_time;k2_press_count = 1;//按键按下一次k2_press_flag = 1;//按键按下标志位k2_press_time = 0;//开始计时}} key_last_stat[0] = key_stat[0];key_last_stat[1] = key_stat[1];key_last_stat[2] = key_stat[2];key_last_stat[3] = key_stat[3];
}//按键2单击操作
void k2_single_press(void)
{if(k2_press_flag && k2_press_time > 30)//如果第一次按下后300ms没有按下第二次,则执行单击操作{//单击操作k2_press_flag = 0;//清除标志位k2_press_count = 0;//清除按下次数}
}
写回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM4)//判断是否来自定时器4{if(k2_press_flag)//按键第一次按下{k2_press_time++;//按键第一次按下后开始计时k2_single_press();//在定时器中执行单击的判断}}
}
还要在主函数初始化中断
HAL_TIM_Base_Start_IT(&htim4);
按键单击、长按后松手判断
思路:设置一个按键按下的标志位,和一个用于记录按键按下的时间,如果按键按下,标志位置1,计时时间清零,开始计时,当检测到按键松开后,根据按键按下的时间判断是短按还是长按,然后执行相应的操作
uint8_t k3_press_flag;//按键3按下标志位
uint8_t k3_press_time;//按键3按下时间void key_scan(void)
{key_stat[0] = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);key_stat[1] = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);key_stat[2] = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);key_stat[3] = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);//按键3实现单击、长按松手操作if(key_stat[2] == 0 && key_last_stat[2] == 1){k3_press_time = 0;k3_press_flag = 1;} else if(key_stat[2] == 1 && key_last_stat[2] == 0){if(k3_press_flag){if(k3_press_time > 100){//长按操作}else{//短按操作} }k3_press_flag = 0;}key_last_stat[0] = key_stat[0];key_last_stat[1] = key_stat[1];key_last_stat[2] = key_stat[2];key_last_stat[3] = key_stat[3];
}
当按下标志位置1,定时器开始累加k3_press_flag
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM4)//判断是否来自定时器4{if(k3_press_flag){k3_press_flag ++;}}
}
单击、长按一定时间执行操作
思路:与长按松手思路差不多,当按键按下时,置标志位,计时时间清零,开始计时,当上一次的状态是按下,现在还是按下时,而且时间大于某一个值,如1s,则执行长按操作,如果上一次是按下,现在是松开,则是单击操作
uint8_t k4_press_time;//按键4按下时间void key_scan(void)
{key_stat[0] = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_0);key_stat[1] = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_1);key_stat[2] = HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2);key_stat[3] = HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0);//按键4实现单击、长按持续时间操作if(key_stat[3] == 0 && key_last_stat[3] == 1){k4_press_time = 0;} else if(key_stat[3] == 0 && key_last_stat[3] == 0)//如果按键一直按下{if(k4_press_time > 100){//长按操作 }}else if(key_stat[3] == 1 && key_last_stat[3] == 0)//如果按键松开{if(k4_press_time < 100)//这个时间用于区分长按松开还是短按松开{//短按操作}}key_last_stat[0] = key_stat[0];key_last_stat[1] = key_stat[1];key_last_stat[2] = key_stat[2];key_last_stat[3] = key_stat[3];
}
定时器直接对按键4的按下事件进行累加
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM4)//判断是否来自定时器4{k4_press_time ++;}
}
要把所有变量放在 key_scan()函数外面!!!!
PWM输出
用板子上的PA6和PA7口进行PWM输出
设置定时器16通道1为PWM产生模式,设置频率为100hz
频率 = 主频 / (PSC + 1) / (AutoReload + 1)
即要想输出频率为 100Hz ,80M /(8000-1+1)/(100 - 1 + 1)= 100
随便设置一个占空比
定时器17同理,设置频率为200hz
随便设置一个占空比
开启定时器通道的PWM
HAL_TIM_PWM_Start(&htim16,TIM_CHANNEL_1);//开启定时器16通道1的PWMHAL_TIM_PWM_Start(&htim17,TIM_CHANNEL_1);//开启定时器17通道1的PWM
定义占空比变量
uint8_t p16duty = 10;//定时器16占空比uint8_t p17duty = 10;//定时器17占空比
设置占空比
__HAL_TIM_SetCompare(&htim16,TIM_CHANNEL_1,p16duty);
输入捕获
板子上有两个脉冲发生芯片 XL555,分别连接到PA15和PB4
设置为直接模式,将预分频系数设为 80 - 1,自动重装载值默认即可
开启NVIC
定时器3同理
interrupt.c
uint16_t ccl_value1,ccl_value2;//频率
uint16_t frq1,frq2;void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)//回调函数
{if(htim->Instance == TIM2)//判断是否是定时器2的{ccl_value1 = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);//从输入捕获通道1 读取当前捕获值,ccl_value1 记录的是两个上升沿(或下降沿)之间的计数值,即一个周期的时钟计数数__HAL_TIM_SetCounter(htim,0);//将定时器计数器清零,为下一次测量做准备,避免旧值影响新周期//TIM2->CNT = 0//也可以用这一句替代上面一句frq1 = (80000000/80)/ccl_value1;//系统主频为 80 MHz,定时器预分频设置为 80-1,所以定时器时钟为 80MHz ÷ 80 = 1MHz,所以 ccl_value1 对应的时间是 多少微秒一个周期,frq1 = 1,000,000 / ccl_value1,即得到频率(单位 Hz)HAL_TIM_IC_Start(htim,TIM_CHANNEL_1);//重新启动输入捕获功能,为下一个周期的捕获做准备}if(htim->Instance == TIM3)//判断是否是定时器2的{ccl_value2 = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);//从输入捕获通道1 读取当前捕获值,ccl_value2 记录的是两个上升沿(或下降沿)之间的计数值,即一个周期的时钟计数数__HAL_TIM_SetCounter(htim,0);//将定时器计数器清零,为下一次测量做准备,避免旧值影响新周期//TIM3->CNT = 0//也可以用这一句替代上面一句frq2 = (80000000/80)/ccl_value2;//系统主频为 80 MHz,定时器预分频设置为 80-1,所以定时器时钟为 80MHz ÷ 80 = 1MHz,所以 ccl_value2 对应的时间是 多少微秒一个周期,frq2 = 1,000,000 / ccl_value2,即得到频率(单位 Hz)HAL_TIM_IC_Start(htim,TIM_CHANNEL_1);//重新启动输入捕获功能,为下一个周期的捕获做准备}
}
捕获定时器开启
HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_1);//捕获定时器开启HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1);
声明变量
打印捕获的信息
sprintf(buf," FRQ1:%d ",frq1);LCD_DisplayStringLine(Line2,(unsigned char *)buf);sprintf(buf," FRQ2:%d ",frq2);LCD_DisplayStringLine(Line3,(unsigned char *)buf);
555芯片
PWM输出频率
ADC
板子上两个电位器接到芯片的ADC输入引脚
添加.c和.h文件,不能命名为adc.c或adc.h,因为会与系统的冲突
myadc.c
#include "myadc.h"double getADC(ADC_HandleTypeDef *pin)
{uint16_t adc;HAL_ADC_Start(pin);adc = HAL_ADC_GetValue(pin);return adc*3.3/4096;//ADC是12位的,2^12=4096
}
myadc.h
#ifndef _MYADC_H_
#define _MYADC_H_#include"main.h"double getADC(ADC_HandleTypeDef *pin);#endif
输出采样得到的值
sprintf(buf," V1:%.2f ",getADC(&hadc1));
LCD_DisplayStringLine(Line5,(unsigned char *)buf);sprintf(buf," V2:%.2f ",getADC(&hadc2));
LCD_DisplayStringLine(Line6,(unsigned char *)buf);
eeprom读写
开发板上的eeprom芯片使用 软件I2C
将PB6和PB7配置成输出模式
I2C与LCD一样,驱动不需要自己去写,只需要移植官方写好的文件进行添加修改即可
可以打开官方提供的 AT24C02 芯片手册
最重要的是下图的内容,最左边为芯片的存储容量,右边表示每一位的控制功能,A2、A1、A0的不同组合,可以表示不同地址的从机,多个从机的通信
最右边的 R/W
位是控制对芯片进行读还是写,该位为1,则为读操作,为0则为写操作
板子上的是 24C02 ,对应 2K 的
E0、E1、E2相当于A0、A1、A2,原理图上都是接地,所以A0、A1、A2都是0
使用软件I2C,先将PB6和PB7配置为输出模式
重要函数
void I2CStart(void)//I2C开始信号
void I2CStop(void)//I2C结束信号
unsigned char I2CWaitAck(void)//I2C主机等待从机应答
void I2CSendNotAck(void)//I2C主机不给从机发送应答
I2C 读取数据
uint8_t i2cRead(uint8_t addr)
{uint8_t data;I2CStart();//开始I2C信号I2CSendByte(0xa0); //先写,即要选择的从机地址,就是eeprom的地址I2CWaitAck();//等待应答I2CSendByte(addr);//告诉从机需要读取片内的哪个地址I2CWaitAck();I2CStop();I2CStart();I2CSendByte(0xa1);//开始读I2CWaitAck();//等待应答data = I2CReceiveByte();//读取数据I2CSendNotAck();//不应答,即只需要从机发送一次数据 I2CStop();//I2C信号停止return data;//返回读取到的数据
}
使用完I2CReceiveByte()读取数据后,使用I2CSendNotAck()函数,即主机不给从机应答,使从机只发送一次数据给主机,如果使用I2CSendAck()函数的话从机会一直发数据给主机,这样收到的数据是错误的
I2C写数据
void i2cWrite(uint8_t addr,uint8_t data)
{I2CStart();//开始I2C信号I2CSendByte(0xa0); //先写,即要选择的从机地址,就是eeprom的地址I2CWaitAck();//等待应答I2CSendByte(addr);//数据要写入eeprom的哪个片内地址I2CWaitAck();I2CSendByte(data);I2CWaitAck();I2CStop();
}
将数据写入eeprom前要先对数据进行处理,如果写入的是16位的数据,要先将数据拆分成高8位和低8位,因为eeprom内部的存储空间是8位的
//eeprom只能存储8位,而频率是16位的uint8_t frq1_h = frq1 >> 8;//高八位移到第八位uint8_t frq1_l = frq1 & 0xff;//不变
通过按键操作eeprom的数据写入,高低位写入之间要加点延时,保证上一次数据写入完整
if(HAL_GPIO_ReadPin(GPIOB,GPIO_PIN_2) == 0)//写入eeprom{i2cWrite(1,frq1_h);HAL_Delay(10);//保证上一次写入完成i2cWrite(2,frq1_l); }
通过lcd读取显示16位数据时,要将存入eeprom中的高低位取出然后相加,存储在一个16位的变量中再显示
uint16_t eep = (i2cRead(1) << 8) + i2cRead(2); sprintf(buf," eeprom:%d ",eep);LCD_DisplayStringLine(Line8,(unsigned char *)buf);
串口通信
配置为异步模式
开启NVIC
设置波特率为9600
发送需要用到的函数
发送函数
char text[20];//用于存放需要发送的数据
sprintf(text,"OKay\r\n");//与LCD一样,需要先将数据写入数组if(uart_Tfalg)//使用定时器来控制发送数据的时间间隔,不然使用延时函数会阻塞程序进行
{HAL_UART_Transmit(&huart1,(uint8_t *)text,sizeof(text),50);//发送数据uart_Tfalg = 0;//标志位清零
}
定时器函数,10ms进一次中断,即1s发送一次数据
uint8_t uart_time;
bool uart_Tfalg = 0;uart_time ++;
if(uart_time == 100)
{uart_Tfalg = 1;uart_time = 0;
}
接收需要用到的函数
在main.c函数初始化先调用一次接收函数,每次接收的数据只有1位
使用串口接收回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)//接收回调函数
{if(huart->Instance == USART1)//判断是否来自USART1{HAL_UART_Transmit(huart,&rec_data,1,50);//先调用一次发送函数,发给上位机,因为在主函数初始化时接收了一次//这里要先发送,然后再执行下面的接收,保证数据不会错乱,必须是这样的顺序//主函数的第一次接收也必须要有HAL_UART_Receive_IT(huart,&rec_data,1);//下一次接收}
}
用led验证是否收到数据
if(rec_data == 1){led_dis(4,1); }else{led_dis(4,0); }
用定时器接收数据
串口回调函数
uint8_t rec_data;//接收到的一个数据bool rec_flag;//接收标志位
uint8_t rec_buf[20];//用于存放接收的数据
uint8_t rec_count = 0;//接收的数据的个数
char send_data[20];//存放发送的数据void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{if(huart->Instance == USART1){TIM8 -> CNT = 0;//接收到数据后,先将计数器清零,开始计时rec_flag = 1;//表示有数据来标志位rec_buf[rec_count] = rec_data;//将接收到的数据存入数组rec_count ++;//接收下一个数据HAL_UART_Receive_IT(huart,&rec_data,1);}
}
发送函数,发送给上位机,同时也是对数据进行处理的函数
void uart1_rec(void)
{if(rec_flag)//有数据来{if(TIM8 -> CNT > 15)//数据接收完整,即时间大于1.5ms{//对数据进行处理if(rec_buf[0] == 'O' && rec_buf[1] == 'K' && rec_buf[2] == 'a' && rec_buf[3] == 'y') //判断数据是否是OKay{sprintf(send_data,"OKay\r\n");//如果是则将数据存储HAL_UART_Transmit(&huart1,(uint8_t *)send_data,sizeof(send_data),50);//打印出数据内容}else if(rec_buf[0] == 'l' && rec_buf[1] == 'q' && rec_buf[2] == 'b')//判断数据是否是lqb{sprintf(send_data,"lqb\r\n");//如果是则将数据存储HAL_UART_Transmit(&huart1,(uint8_t *)send_data,sizeof(send_data),50);//将数据发送}else//如果不是我们想要的数据,则打印error{sprintf(send_data,"error\r\n");HAL_UART_Transmit(&huart1,(uint8_t *)send_data,sizeof(send_data),50);}for(int i=0;i<rec_count;i++)//把数据缓存区清零,用于下一次数据的接收{rec_buf[i] = 0;}rec_count = 0;//将数据个数清零rec_flag = 0;//数据接收标志位清零} }
}
将接收函数放到主循环
效果如下,但是有点小Bug,例如 lqb 是我们想要的数据,但是 lqbxxx ,即 lqb 后面加了其他的,也会判断为是我们想要的数据,并发送 lqb 给上位机
解决方法:
在这一句判断字符的逻辑后加上最后是否是字符串的结束即可
RTC时钟
配置时钟和闹钟
开启闹钟的中断
生成代码后编译,打开 rtc.c 文件,可以看到上面配置的信息
获取时间、日期,设置时间、日期等的函数如下图
获取并显示时间和日期
//首先定义两个结构体
RTC_TimeTypeDef sTime = {0};//用于存放时间
RTC_DateTypeDef sDate = {0};//用于存放日期//RTC获取时间
void RTC_Gettime()
{HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);//获取时间,函数使用可在rtc.c文件中参考HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);//获取日期
}
用LCD显示
sprintf(buf, " TIME ");LCD_DisplayStringLine(Line2, (unsigned char *)buf);sprintf(buf, " %2d:%2d:%2d",sTime.Hours,sTime.Minutes,sTime.Seconds);LCD_DisplayStringLine(Line4, (unsigned char *)buf);sprintf(buf, " DATE ");LCD_DisplayStringLine(Line6, (unsigned char *)buf);sprintf(buf, " %2d-%2d-%2d",sDate.Year,sDate.Month,sDate.Date);LCD_DisplayStringLine(Line8, (unsigned char *)buf);
将RTC获取时间函数放在LCD显示函数前
设置闹钟
闹钟回调函数
#include "stdbool.h"bool alarm_falg = 0;//闹钟中断回调函数
void HAL_RTC_AlarmAEventCallback(RTC_HandleTypeDef *hrtc)//当到达设定的闹钟时间时,会调用这个回调函数
//在里面写需要执行的事件
{alarm_falg = 1;//让闹钟标志位至1
}
extern bool alarm_falg;//放在主函数循环中,到达闹钟时间就让led4点亮
while(1)
{led_dis(4,alarm_falg);
}
我设定的初始化时间时 10:20:00,闹钟时间时10:20:30,即在30秒时led4点亮
进阶
LED闪烁
使用定时器,不然用延时函数会阻塞程序运行
在定时器中断函数中
uint8_t led_time;//led闪烁的时间
uint8_t led_shine;//led闪烁标志位void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if(htim->Instance == TIM4){led_time++;//每10ms加一次if(led_time == 50)//时间为500ms时{led_shine ++;//标志位置1led_shine %= 2;//等于2时变为0led_time = 0;//重新开始计时}
}
while(1)//循环执行
{led_dis(2,led_shine);
}
LCD高亮显示
在需要高亮的行数前设置背景颜色,其他的行数都设置为黑色,然后设置一个变量,例如count,不同的count显示不同的界面就可以
switch(count){case(0):{LCD_SetBackColor(Yellow);sprintf(buf, " count:%d ",count);LCD_DisplayStringLine(Line2, (unsigned char *)buf);LCD_SetBackColor(Black);sprintf(buf, " FRQ1:%d ",frq1);LCD_DisplayStringLine(Line4, (unsigned char *)buf);sprintf(buf, " FRQ2:%d ",frq2);LCD_DisplayStringLine(Line6, (unsigned char *)buf);sprintf(buf, " EEP_FRQ:%d ",(i2c_read(1)<<8) + i2c_read(2));LCD_DisplayStringLine(Line8, (unsigned char *)buf);sprintf(buf, " receive:%d ",rec_data);LCD_DisplayStringLine(Line1, (unsigned char *)buf);}break;case(1):{sprintf(buf, " count:%d ",count);LCD_DisplayStringLine(Line2, (unsigned char *)buf);LCD_SetBackColor(Yellow);sprintf(buf, " FRQ1:%d ",frq1);LCD_DisplayStringLine(Line4, (unsigned char *)buf);LCD_SetBackColor(Black);sprintf(buf, " FRQ2:%d ",frq2);LCD_DisplayStringLine(Line6, (unsigned char *)buf);sprintf(buf, " EEP_FRQ:%d ",(i2c_read(1)<<8) + i2c_read(2));LCD_DisplayStringLine(Line8, (unsigned char *)buf);}break;case(2):{sprintf(buf, " count:%d ",count);LCD_DisplayStringLine(Line2, (unsigned char *)buf);sprintf(buf, " FRQ1:%d ",frq1);LCD_DisplayStringLine(Line4, (unsigned char *)buf);LCD_SetBackColor(Yellow);sprintf(buf, " FRQ2:%d ",frq2);LCD_DisplayStringLine(Line6, (unsigned char *)buf);LCD_SetBackColor(Black);sprintf(buf, " EEP_FRQ:%d ",(i2c_read(1)<<8) + i2c_read(2));LCD_DisplayStringLine(Line8, (unsigned char *)buf);}break;case(3):{LCD_SetBackColor(Black);sprintf(buf, " count:%d ",count);LCD_DisplayStringLine(Line2, (unsigned char *)buf);sprintf(buf, " FRQ1:%d ",frq1);LCD_DisplayStringLine(Line4, (unsigned char *)buf);sprintf(buf, " FRQ2:%d ",frq2);LCD_DisplayStringLine(Line6, (unsigned char *)buf);LCD_SetBackColor(Yellow);sprintf(buf, " EEP_FRQ:%d ",(i2c_read(1)<<8) + i2c_read(2));LCD_DisplayStringLine(Line8, (unsigned char *)buf);}break;}}
LCD界面切换
设置一个用于切换界面的参数,与高亮显示大同小异,通过按键改变view的值
uint8_t view= 0;void lcd_show(void)
{if(view == 0){switch(count){case(0):{LCD_SetBackColor(Yellow);sprintf(buf, " count:%d ",count);LCD_DisplayStringLine(Line2, (unsigned char *)buf);LCD_SetBackColor(Black);sprintf(buf, " FRQ1:%d ",frq1);LCD_DisplayStringLine(Line4, (unsigned char *)buf);sprintf(buf, " FRQ2:%d ",frq2);LCD_DisplayStringLine(Line6, (unsigned char *)buf);sprintf(buf, " EEP_FRQ:%d ",(i2c_read(1)<<8) + i2c_read(2));LCD_DisplayStringLine(Line8, (unsigned char *)buf);sprintf(buf, " receive:%d ",rec_data);LCD_DisplayStringLine(Line1, (unsigned char *)buf);}break;case(1):{sprintf(buf, " count:%d ",count);LCD_DisplayStringLine(Line2, (unsigned char *)buf);LCD_SetBackColor(Yellow);sprintf(buf, " FRQ1:%d ",frq1);LCD_DisplayStringLine(Line4, (unsigned char *)buf);LCD_SetBackColor(Black);sprintf(buf, " FRQ2:%d ",frq2);LCD_DisplayStringLine(Line6, (unsigned char *)buf);sprintf(buf, " EEP_FRQ:%d ",(i2c_read(1)<<8) + i2c_read(2));LCD_DisplayStringLine(Line8, (unsigned char *)buf);}break;case(2):{sprintf(buf, " count:%d ",count);LCD_DisplayStringLine(Line2, (unsigned char *)buf);sprintf(buf, " FRQ1:%d ",frq1);LCD_DisplayStringLine(Line4, (unsigned char *)buf);LCD_SetBackColor(Yellow);sprintf(buf, " FRQ2:%d ",frq2);LCD_DisplayStringLine(Line6, (unsigned char *)buf);LCD_SetBackColor(Black);sprintf(buf, " EEP_FRQ:%d ",(i2c_read(1)<<8) + i2c_read(2));LCD_DisplayStringLine(Line8, (unsigned char *)buf);}break;case(3):{LCD_SetBackColor(Black);sprintf(buf, " count:%d ",count);LCD_DisplayStringLine(Line2, (unsigned char *)buf);sprintf(buf, " FRQ1:%d ",frq1);LCD_DisplayStringLine(Line4, (unsigned char *)buf);sprintf(buf, " FRQ2:%d ",frq2);LCD_DisplayStringLine(Line6, (unsigned char *)buf);LCD_SetBackColor(Yellow);sprintf(buf, " EEP_FRQ:%d ",(i2c_read(1)<<8) + i2c_read(2));LCD_DisplayStringLine(Line8, (unsigned char *)buf);}break;}}else{LCD_SetBackColor(Black);sprintf(buf, " TIME ");LCD_DisplayStringLine(Line2, (unsigned char *)buf);sprintf(buf, " %2d:%2d:%2d",sTime.Hours,sTime.Minutes,sTime.Seconds);LCD_DisplayStringLine(Line4, (unsigned char *)buf);sprintf(buf, " DATE ");LCD_DisplayStringLine(Line6, (unsigned char *)buf);sprintf(buf, " %2d-%2d-%2d",sDate.Year,sDate.Month,sDate.Date);LCD_DisplayStringLine(Line8, (unsigned char *)buf);}
}
例如使用按键3的单击切换界面
切换界面之前要先调用 LCD_Clear(Black);
,将LCD清屏,不然上一个界面的内容会保留
输入捕获占空比
要用到一个定时器的两个通道
uint16_t ccl2b,ccl2a;
uint16_t frq2;
float duty_val;void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{ if(htim->Instance == TIM3){if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1){ccl2a = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);//直接ccl2b = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2);//间接TIM3->CNT = 0;frq2 = (80000000/80)/ccl2a;duty_val = ((ccl2b*1.0)/(ccl2a*1.0))*100;//因为上面定义的ccl2a、ccl2b是uint16_t类型//而占空比是float,要*1.0进行类型转换,不然输出是0HAL_TIM_IC_Start(htim,TIM_CHANNEL_1);HAL_TIM_IC_Start(htim,TIM_CHANNEL_2);}}
}
//主函数开启输入捕获中断
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_2);
//显示函数记得类型是f
sprintf(buf," Duty:%.3f ",duty_val);
串口发送字符串
可以任意设定需要发送的字符串
//发送字符函数定义
void uart1_send_char(char data[20])
{//要用strlen,不用sizeof,要包含头文件“string.h” HAL_UART_Transmit(&huart1,(uint8_t *)data,strlen(data),50);
}
发送演示
if(key_stat[2] == 0 && key_last_stat[2] == 1)
{uart1_send_char("OKay\r\n");
}
BUG发现
在LCD显示函数中,将字符存入数组的sprintf函数中,在打印ADC的值时,如果后面的空格大于3格,串口的接收数据就会不正确,当空格数小于等于3格时,串口数据接收又变正常了,怀疑是因为内存溢出问题,但是没有时间去深入研究
DMA串口收发
串口DMA发送
使用的函数是HAL_UART_Transmit_DMA()
void uart1_send_DMA(char data[20])
{HAL_UART_Transmit_DMA(&huart1,(uint8_t *)data,strlen(data));
}
串口DMA接收
使用的回调函数是 HAL_UARTEx_RxEventCallback()
DMA接收函数HAL_UARTEx_ReceiveToIdle_DMA()
回调函数
//USART
bool rx_flag;//数据接收标志位
char rx_buf[100] = {0};//存储接收到的数据
char rx_send[100] = {0};//用于存储发送给上位机的数据
uint8_t rx_size = 0;//接收到的数据的大小void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{if(huart->Instance == USART1){rx_flag = 1;//接收标志位置1rx_size = Size;//将数据大小存储HAL_UARTEx_ReceiveToIdle_DMA(huart,(uint8_t *)rx_buf,100);//准备下一次数据的接收}
}
与定时器的方法一样,要先在主函数调用一次串口DMA接收的函数,初始化的时候就接收一次数据
处理函数
void uart_rx_handler(void)
{if(rx_flag == 1)//当接收到数据{rx_flag = 0;//标志位清零,为下一次接收做准备 if(rx_size == 1)//数据处理,判断接收到的数据是不是自己想要的{sprintf(rx_send,"%s\r\n",rx_buf);//将接收到的数据发回给上位机HAL_UART_Transmit_DMA(&huart1,(uint8_t *)rx_send,strlen(rx_send));}else//如果不是自己想要的数据,则打印error到上位机{sprintf(rx_send,"error\r\n");HAL_UART_Transmit_DMA(&huart1,(uint8_t *)rx_send,strlen(rx_send));}memset(rx_buf,0,100);//将缓存清零,为下一次接收做准备}
}
将处理函数放在主函数循环调用
串口数据解析
例如第十二届省赛真题,要求用串口发送一段车辆出入停车场信息,要求用LCD屏将信息按照要求显示出来
使用sscanf函数,将接收到的数据按格式存储到相应的数据变量中
//车辆信息:VNBR:D583:200202120000
char type[5] = {0};
char num[5] = {0};
int year;
int mon;
int day;
int hour;
int min;
int sec;void uart_dma_handler(void)
{if(uart_rec_flag == 1){uart_rec_flag = 0;if(uart_rec_size == 22)//判断接收到的数据长度是否为22,如果是,说明信息正确,则将信息存储{sscanf(uart_rec_buf,"%4s:%4s:%2d%2d%2d%2d%2d%2d",type,num,&year,&mon,&day,&hour,&min,&sec);} else//如果不是自己想要的数据,则打印error到上位机{sprintf(rx_send,"error\r\n");HAL_UART_Transmit_DMA(&huart1,(uint8_t *)rx_send,strlen(rx_send));}memset(uart_rec_buf,0,100);}
}
使用LCD显示
void lcd_dis(void)
{sprintf(buf," DATA ");LCD_DisplayStringLine(Line1,(uint8_t *)buf);sprintf(buf," %s:%s",type,num);LCD_DisplayStringLine(Line3,(uint8_t *)buf);sprintf(buf," %2d-%2d-%2d-%2d-%d ",year,mon,day,min,sec);LCD_DisplayStringLine(Line5,(uint8_t *)buf);
}
判断是否是数字并解析打印
因为用串口发送数字时,都是以字符的形式发送的,也就是发送数字对应的ASCII码,要先对字符进行判断,即判断该数字是否时在 ‘0’ 和 ‘9’ 之间,即 if( a >= ‘0’ && a <= ‘9’) ,如果为真,则是数字,并将 a - ‘0’ ,即可得到该数字
先写一个函数用于判断接收到的字符是否是数字
uint8_t is_num(char a)
{return (a >= '0' && a <= '9');
}
以1位数字为例
void uart_dma_handler(void)
{if(uart_rec_flag == 1){uart_rec_flag = 0;if(uart_rec_size == 1)//判断是否是1个字符{if(is_num(uart_rec_buf[0]))//判断该字符是否是数字{result = uart_rec_buf[0] - '0';//将该字符减去'0'后的的结果返回,即可得到该数字}}else//如果不是自己想要的数据,则打印error到上位机{sprintf(rx_send,"error\r\n");HAL_UART_Transmit_DMA(&huart1,(uint8_t *)rx_send,strlen(rx_send));}memset(uart_rec_buf,0,100);}
}
然后用LCD将数字显示出来
用串口实现按键功能
设接收到B1代表按键1按下,判断接收到的字符是否是 ‘B1’ ,如果是,则执行相应的动作
void uart_dma_handler(void)
{if(uart_rec_flag == 1){uart_rec_flag = 0;if(uart_rec_size == 2 && uart_rec_buf[0] == 'B' && uart_rec_buf[1] == '1'){count ++;}memset(uart_rec_buf,0,100);}
}
修改PWM输出频率
定时器的输出频率与 预分频置值 PRESCALER 和自动重装载值 Autoreload 有关,要修改PWM输出频率,修改其中一个或者两个同时修改就可以了
例如修改定时器16的输出频率
__HAL_TIM_SetAutoreload(&htim16, 10 -1);
或者直接找到定时器16的初始化函数,修改对应的变量的值
理论上是上面的说法,但是实际测试发现直接调用函数或者修改变量的值无法修改频率实际输出值