LED,按键
配置LED点亮,按键采集按键值
前期准备:通过Cubemx生成一个源文件方便后续直接使用。
源文件准备完毕以后开始进行按键和LED的配置
LED
对比芯片引脚连接图可以知道8个LED分别连接在GPIOC的如下8个引脚中
Cubemx中对该8个引脚进行配置,分别配置为推挽输出模式,初始电平设置为高电平(保证初始化时LED是熄灭的),LE作为锁存器(由于部分引脚与LCD共用而引入的设计)
将gpio.c中生成的代码拷贝到LED的初始化函数中
void BSP_LED_Init() {GPIO_InitTypeDef GPIO_InitStruct = {0};/* GPIO Ports Clock Enable */__HAL_RCC_GPIOC_CLK_ENABLE();__HAL_RCC_GPIOF_CLK_ENABLE();__HAL_RCC_GPIOD_CLK_ENABLE();/*Configure GPIO pin Output Level */HAL_GPIO_WritePin(GPIOC, LED6_Pin|LED7_Pin|LED8_Pin|LED1_Pin|LED2_Pin|LED3_Pin|LED4_Pin|LED5_Pin, GPIO_PIN_SET);/*Configure GPIO pin Output Level */HAL_GPIO_WritePin(LED_Cmd_GPIO_Port, LED_Cmd_Pin, GPIO_PIN_RESET);/*Configure GPIO pins : PCPin PCPin PCPin PCPinPCPin PCPin PCPin PCPin */GPIO_InitStruct.Pin = LED6_Pin|LED7_Pin|LED8_Pin|LED1_Pin|LED2_Pin|LED3_Pin|LED4_Pin|LED5_Pin;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pull = GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);/*Configure GPIO pin : PtPin */GPIO_InitStruct.Pin = LED_Cmd_Pin;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pull = GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(LED_Cmd_GPIO_Port, &GPIO_InitStruct);}
接下来写一个LED点亮函数来点亮LED(这里以点亮LED1,LED2为例子) - 蠢方法,能用就行
HAL_GPIO_WritePin(GPIOC, LED6_Pin|LED7_Pin|LED8_Pin|LED4_Pin|LED5_Pin, GPIO_PIN_SET);HAL_GPIO_WritePin(LED_Cmd_GPIO_Port, LED_Cmd_Pin, GPIO_PIN_SET);HAL_GPIO_WritePin(LED_Cmd_GPIO_Port, LED_Cmd_Pin, GPIO_PIN_RESET);if(LED == 1){if(Mode){HAL_GPIO_WritePin(LED1_GPIO_Port,LED1_Pin,GPIO_PIN_RESET);}else{HAL_GPIO_WritePin(LED1_GPIO_Port,LED1_Pin,GPIO_PIN_SET);}}else if(LED == 2){if(Mode){HAL_GPIO_WritePin(LED2_GPIO_Port,LED2_Pin,GPIO_PIN_RESET);}else{HAL_GPIO_WritePin(LED2_GPIO_Port,LED2_Pin,GPIO_PIN_SET);}}else if(LED == 3){if(Mode){HAL_GPIO_WritePin(LED3_GPIO_Port,LED3_Pin,GPIO_PIN_RESET);}else{HAL_GPIO_WritePin(LED3_GPIO_Port,LED3_Pin,GPIO_PIN_SET);}}HAL_GPIO_WritePin(LED_Cmd_GPIO_Port, LED_Cmd_Pin, GPIO_PIN_SET);HAL_GPIO_WritePin(LED_Cmd_GPIO_Port, LED_Cmd_Pin, GPIO_PIN_RESET);
按键KEY
同LED去对比引脚情况得到Key的引脚如下
利用Cubemx将对应引脚配置为输入模式
生成gpio中的代码,保留其中和按键有关的初始化配置到按键的初始化函数中
void BSP_Key_Init() {GPIO_InitTypeDef GPIO_InitStruct = {0};__HAL_RCC_GPIOA_CLK_ENABLE();__HAL_RCC_GPIOB_CLK_ENABLE();/*Configure GPIO pin : PtPin */GPIO_InitStruct.Pin = Key4_Pin;GPIO_InitStruct.Mode = GPIO_MODE_INPUT;GPIO_InitStruct.Pull = GPIO_NOPULL;HAL_GPIO_Init(Key4_GPIO_Port, &GPIO_InitStruct);/*Configure GPIO pins : PBPin PBPin PBPin */GPIO_InitStruct.Pin = Key1_Pin|Key2_Pin|Key3_Pin;GPIO_InitStruct.Mode = GPIO_MODE_INPUT;GPIO_InitStruct.Pull = GPIO_NOPULL;HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);}
再写一个按键获取的函数(利用延时来消抖是一个贪图方便的做法,有更好的办法则代替它,顺便告诉我.......)
enum {Key1 = 1,Key2,Key3,Key4}; uint8_t Key_Get() {uint8_t Temp = 0; //声明一个临时变量用来获取后返回if(HAL_GPIO_ReadPin(GPIOB,Key1_Pin) == 0 ){Temp = Key1;HAL_Delay(100);}if(HAL_GPIO_ReadPin(GPIOB,Key2_Pin) == 0 ){Temp = Key2;HAL_Delay(100);}if(HAL_GPIO_ReadPin(GPIOB,Key3_Pin) == 0 ){Temp = Key3;HAL_Delay(100);}if(HAL_GPIO_ReadPin(Key4_GPIO_Port,Key4_Pin) == 0 ){Temp = Key4;HAL_Delay(100);}return Temp;}
在h文件里声明以后去main函数中测试
int main(void) {HAL_Init();SystemClock_Config();// MX_GPIO_Init();BSP_LED_Init(); //初始化LED BSP_Key_Init();#ifdef LED_DEBUGLED_On(1,1); //点亮LED1LED_On(2,1); //点亮LED2#endif while (1){#ifdef KEY_DEBUGuint8_t temp = Key_Get();if(temp == 1)LED_On(1,1); //点亮LED1else if(temp == 2)LED_On(2,1); //点亮LED2else if(temp == 0){LED_On(1,0); LED_On(2,0); }#endif}/* USER CODE END 3 */ }
现象应该是按下按键1之后点亮,松开则会熄灭,按键2同理。
2024.3.1更新 非阻塞延时以及按键消抖
按键消抖: --在Key_Get()中使用了HAL_DELAY()函数来作为按键消抖,但事实上这种效果并不好...,在此基础上新添加一个函数通过判断按键按下和按键松开两个状态解决问题
先定义变量值Old_Key和Cur_Key来接收按键的状态。再定义Key_Down,Key_Up作为返回变量。
1. Cur_Key 采集按键按下的值
CKey_Value = BSP_KEY_Getnum();
2.将Cur_Key 和 (Cur_Key 与Old_Key异或后的结果)相与后将结果赋值给Key_Down
3将Cur_Key取反后 和 (Cur_Key 与Old_Key异或后的结果)相与后将结果赋值给Key_Up
Key_Down = CKey_Value & (CKey_Value ^ OKey_Value);Key_Up = ~CKey_Value & (CKey_Value ^ OKey_Value);
4最后将Cur_Key 的值赋值给Old_Key
OKey_Value = CKey_Value;
5返回Key_Down/Key_Up作为判断依据
return Key_Up;
非阻塞延时 - 为了实现非阻塞延时,我们利用uwTick和某个变量相减的值判断该轮回是否执行函数
1.先定义一个32位的变量来接收uwTick的值
uint32_t Key_Tick;
2判断(uwTick - 变量值)< 100(某个时间)是否成立 。
3如果成立,则直接返回,本论不执行代码
4如果不成立,则将uwTick的值赋给变量以供下一个轮回使用
if( (uwTick - Key_Tick)<100 ) return 0;elseKey_Tick = uwTick;
5结果就是前99ms不阻塞直接返回,不执行代码,直到第100ms条件不成立时后续代码才会执行。好处就是不会因为延时干扰到其它模块的运行
完整代码如下
uint8_t _Key_Get() {//延时执行函数 每3s才能执行一次//如何利用uwTick实现部分函数的延时//首先定义一个32位的变量接收值//用(uwTick-变量值)<某个时间 成立则直接返回,本轮回不执行该代码。不成立则本次执行该代码并将uwTick的值赋给变量值if( (uwTick - Key_Tick)<100 ) return 0;elseKey_Tick = uwTick;//如何实现按键消抖//设立一个旧值和新值。首先,新值采集采集到的按键值//然后为了得到按下的命令,将新值和(新值与旧值异或处理后的结果)相与得到结果。//为了得到松开的命令,将新值取反以后和旧值和新值异或后的结果相与得到结果。 CKey_Value = BSP_KEY_Getnum();Key_Down = CKey_Value & (CKey_Value ^ OKey_Value);Key_Up = ~CKey_Value & (CKey_Value ^ OKey_Value);OKey_Value = CKey_Value;return Key_Up;}
/************************************************************************************************************************/
3.3日更新 关于长短按 ------这个破方法没法解决长按和短按分开的问题..............
由于在第14届中需要在DATA界面处理长按的需求,所以加入了一些代码
uint32_t Key_tick;uint8_t __Key_Get()
{if( (uwTick - Key_tick)<100 ) return 0;elseKey_tick = uwTick ;uint8_t Temp= 0;if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == 0){Temp = KEY1;} if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == 0){Temp = KEY2;} if(HAL_GPIO_ReadPin(KEY3_GPIO_Port,KEY3_Pin) == 0){Temp = KEY3;} if(HAL_GPIO_ReadPin(KEY4_GPIO_Port,KEY4_Pin) == 0){Temp = KEY4;if(LCD_Mode == DATA){uint16_t Key_Tick=0;while(HAL_GPIO_ReadPin(KEY4_GPIO_Port,KEY4_Pin) == 0) {Key_Tick++;} //判断长按时间if(Key_Tick >= 2000) //时间超过2s{Temp = KEY4_1; Key_Tick = 0; } //确定为长按}} return Temp;}
综上,如果有长短按的需求/其它需求,就用第一种方法。如果没有的话两种方法都可以
/*********************************************************************************************************************/
接下来是串口配置
模式我们选择异步(Asyno....)串口时钟模式,它适用于大多数情况,即UART。同步时钟(syno....)要求发送和接收处于同一个时钟下即时间同步,条件比较苛刻但传输速率似乎也更快
配置好需要的各项参数如波特率,字长,校验位,停止位...........(其实这里根本不需要修改)
打开串口中断,并在NVIC(中断管理)中设置优先级为3
当然,如果此时对应引脚没有选择正确模式的话还需要自己去设置(这一点经常被忘记)
一切安排妥当后生成代码
这时串口的初始化已经配置完毕了,剩下的就是串口接收中断的实现(针对不同的需要选择不同的中断方式,如接收中断,空闲中断等等)
在红线标识都文件中找到串口的中断函数
在红线标识都文件中找到串口的中断函数
void USART1_IRQHandler(void) {/* USER CODE BEGIN USART1_IRQn 0 *//* USER CODE END USART1_IRQn 0 */HAL_UART_IRQHandler(&huart1);/* USER CODE BEGIN USART1_IRQn 1 *//* USER CODE END USART1_IRQn 1 */ }
在串口中断函数中我们去寻找中断回调函数HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart),我们将重写回调函数(注意不要找错了)
首先在usart.c中先声明4个全局变量来保存接收的数据和表示串口状态并将变量声明为外部变量(方便后续其它模块和串口数据联动)
uint8_t Rx_Str[255]={0}; //接收区 uint8_t Rx_Char=0; //接收缓存区 uint8_t Rx_Count=0; //接收数量 uint8_t Rx_Flag=0; //接收完成标志位
然后在main.c中初始化串口之后打开串口接收中断
MX_USART1_UART_Init(); //串口初始化 HAL_UART_Receive_IT(&huart1,&Rx_Char,1); //开启串口中断,接收数据保留在缓冲区中
在中断函数中处理接收到的串口数据
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {if(huart->Instance==USART1){Rx_Str[Rx_Count] = Rx_Char; //如果Rx_Count在这里累加的话,到后面你会神奇的发现1234567890会输出0123456789,(因为最后一个字符没有接收到Rx_Count就已经在这里自增到10了。导致它成为了下一次的第一个数据)if(Rx_Count++ == 10) //根据接收的不同选择不同的结束方式 ---这里接收10个字符后结束 //数据在判断前最好不要被更改,记住这条编程规范 {Rx_Flag = 1;Rx_Count = 0;}}if(Rx_Flag == 1){ Rx_Flag = 0;printf("接收到的数据为%s\r\n",Rx_Str); //这里对Printf进行了重定向,使其通向串口,后面会说明memset(Rx_Str,0,sizeof(Rx_Str)); }HAL_UART_Receive_IT(&huart1,&Rx_Char,1); //再次开启接收中断 }//不定量接收void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){if(huart->Instance == USART1){Rec_Str[Rec_Count] = Rec_char; if( Rec_Str[Rec_Count] == '\n') //利用\r\n判断结束Rec_Flag = 1; elseRec_Count++;HAL_UART_Receive_IT(&huart1,&Rec_char,1);}if(Rec_Flag == 1){Rec_Flag = 0;Rec_Count = 0;printf("%s\r\n",Rec_Str);}}
关于printf的重定向
在usart.c中补充该函数,记住要在配置中勾选如下选项才能使用
//printf重定向
//关于该函数:我推测该函数应该是printf用来向标准输出流输送字符的一个子函数。---没有看过源码.............
int fputc(int c,FILE* stream) {HAL_UART_Transmit(&huart1,(uint8_t *)&c,1,10);return c; }
再来看IIC
由于蓝桥杯比赛中IIC是用在驱动24c02闪存芯片的,且已经提供了IIC文件,所以这里不讲如何写IIC,只讲怎么在24c02中用。
将赛方提供的IIC文件移植进来
24c02的初始化直接用给的IIC_Init即可
void BSP_24c02_Init() {I2CInit(); }
24c02有写入/读取字节的操作,对应的函数如下
//向指定地址中读取字节 /* Data_Addr:接收缓冲区 Target_Addr:要读取的目标地址 Count:读取总字节数 */ void BSP_24c02_Read(uint8_t* Data_Addr,uint8_t Target_Addr,uint8_t Count) {I2CStart(); //iic通信的基本操作,开始和结束....非常容易忘,后果也非常严重....(上次整整一个下午没找出这个DEBUG) I2CSendByte(0xa0); //24c02写入操作码I2CWaitAck(); //这里我贪方便没有测试应答是否成功,正确做法应该是用状态机来执行这个过程I2CSendByte(Target_Addr); //为了实现地址偏移 I2CWaitAck(); I2CStart(); //重新开始I2CSendByte(0xa1); //24c02读取操作码 I2CWaitAck();while(Count){
//先接收数据再应答*Data_Addr++ =I2CReceiveByte(); //Count不为0接收字节if(--Count){ I2CSendAck();}elseI2CSendNotAck(); //非应答 }I2CStop();}
写入操作
void BSP_24c02_Write(uint8_t* Data_Addr,uint8_t Target_Addr,uint8_t Count) {I2CStart(); //iic通信的基本操作,开始和结束....非常容易忘,后果也非常严重....(上次整整一个下午没找出这个DEBUG) I2CSendByte(0xa0); //24c02写入操作码I2CWaitAck(); //这里我贪方便没有测试应答是否成功,正确做法应该是用状态机来执行这个过程I2CSendByte(Target_Addr); //发送要写入的地址 I2CWaitAck();while(--Count){
//先接收/发送数据再应答I2CSendByte(*Data_Addr++);I2CWaitAck();}I2CStop();HAL_Delay(500);}
//测试代码,测试结果
void _24c02_Test() {BSP_24c02_Write(W_str_Test,0,5);BSP_24c02_Read(R_str_Test,0,5);//后面用LCD测试uint8_t LCD_str[255];sprintf((char *)LCD_str,"W_Str %x:%x:%x:%x:%x",W_str_Test[0],W_str_Test[1],W_str_Test[2],W_str_Test[3],W_str_Test[4]); //%x:显示16进制数 LCD_DisplayStringLine(Line2,LCD_str);sprintf((char *)LCD_str,"R_Str %x:%x:%x:%x:%x",R_str_Test[0],R_str_Test[1],R_str_Test[2],R_str_Test[3],R_str_Test[4]);LCD_DisplayStringLine(Line3,LCD_str);}
LCD塞方也已经提供了需要的HAL文件,也只需要知道如何用就行
将赛方提供的HAL文件移植进来
初始化,并配置黑底白字
LCD_Init();LCD_Clear(Black);LCD_SetBackColor(Black);LCD_SetTextColor(White);
测试显示效果
#ifdef LCD_DEBUGLCD_Clear(Black);uint8_t LCD_STR[255]="LCD test";LCD_DisplayStringLine(Line1,LCD_STR);#endif
定时器相关
RTC配置
勾选这两个选项开启时钟源和日历
选择二进制形式保存并将时间写入
生成代码文件rtc.c/h
然后在rtc.c中定义两个结构体变量来保存时间和日期
RTC_DateTypeDef Date01; //日期结构体
RTC_TimeTypeDef Time01; //时间结构体
调用库函数来获取时间并显示在LCD上效果如下
void MyRTC_GetTime() {HAL_RTC_GetTime(&hrtc,&Time01,RTC_FORMAT_BIN);HAL_RTC_GetDate(&hrtc,&Date01,RTC_FORMAT_BIN);uint8_t RTC_STR[255];sprintf((char *)RTC_STR,"cur Time:%02d:%02d:%02d",Time01.Hours,Time01.Minutes,Time01.Seconds);LCD_DisplayStringLine(Line4,RTC_STR); }
TIM - 脉冲捕获
根据蓝桥杯板子的引脚设计,信号发生器通过PA15输出脉冲信号。我们选用TIM2(PA15)来对输入脉冲进行捕获
先设置从模式为复位模式,触发信号为TF1FP1(即每次捕获到该通道信号时计数器会复位),时钟源选择内部时钟。两个通道一个直接捕获,一个间接捕获(....我不清楚是什么意思,求解答)
关于TI1FP1 ---从我练习的情况来看这里是选择通道的意思,就比如上面,如果主要通道是通道1,就选择TI1FP1,如果主要通道是通道2,就选择TI2FP2。
如果你只是需要一个通道来单独测量频率,那么选择对应的通道即可
这里我选择了通道2来捕获,所以选择TI2FP2
下面通道配置,一个通道捕获上升沿,一个通道捕获下降沿
开启中断并更改中断优先级
注意!!!一定要确保GPIO口已经配置好了,不然搞半天跟没搞一样。
生成tim.c/h文件后,去定时中断函数中找到输入捕获的回调函数
在中断函数中找输入捕获中断函数
重写输入捕获回调函数
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {if(htim->Instance == TIM2){if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1){Cap_T_count = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1)+1;Duty = (float)Cap_R_count/Cap_T_count;}if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2){Cap_R_count = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2)+1;}}}
main函数中执行初始化,开启时基单元,打开中断
MX_TIM2_Init();HAL_TIM_Base_Start(&htim2);HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_1);HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_2);
写测试代码
#ifdef TIM_IC_DEBUG sprintf(IC_STR,"Cap count is:%d",Cap_T_count);LCD_DisplayStringLine(Line8,(uint8_t *)IC_STR);sprintf(IC_STR,"PWM1:%05dHz , %4.1f%%",(1000000/Cap_T_count),Duty * 100); //频率和占空比LCD_DisplayStringLine(Line5,(uint8_t *)IC_STR);#endif
TIM - PWM输出
我这里选择PA7作为输出通道,即TIM17的通道1输出PWM(频率为2000HZ,占空比50%) //80M/80/500 = 2khz
生成tim.c后执行tim17初始化后开启PWM输出即可
MX_TIM17_Init();HAL_TIM_PWM_Start(&htim17,TIM_CHANNEL_1);
这里利用了前面的脉冲捕获来确认波形
输出比较
首先,先选择对应的引脚,设置为定时器输出比较
选择内部时钟,输出模式选择输出比较模式
设置预分频值和比较值,模式选择Toggle on match 到达比较值时翻转电平来输出方波
开启中断
生成代码tim.c后前往中断函数里寻找TIM3输出比较的回调函数
void TIM3_IRQHandler(void) { HAL_TIM_IRQHandler(&htim3); }
在输出回调函数里重新设置新的比较值
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef *htim) {if(htim->Instance == TIM3 ){if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1){__HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_1, __HAL_TIM_GET_COMPARE(htim, TIM_CHANNEL_1)+200); //1000000/(200*2)=2500hz的方波}} }
main函数中初始化定时器并开启定时器的输出比较
MX_TIM3_Init();HAL_TIM_OC_Start_IT(&htim3,TIM_CHANNEL_1);
ADC采集
根据开发板的引脚图选择PB15作为ADC采集的端口
对应ADC2的通道15
选择独立模式,异步时钟2分频
生成代码adc.c/h
定义一个ADC采集函数,调用库函数启动ADC,然后获取ADC的值并显示在LCD屏幕上
void MyADC_Get() {HAL_ADC_Start(&hadc2);ADC_Value = HAL_ADC_GetValue(&hadc2);uint8_t ADC_STR[255];sprintf((char *)ADC_STR,"CUR ADC:%d",ADC_Value);LCD_DisplayStringLine(Line6,ADC_STR);}