本人大二电气工程在读,写篇文章总结一下寒假所学蓝桥杯嵌入式,由于是临时起意,还正在学习,所以就从我目前学习到的地方开始,本文也可用于速成,内容不全是还在完善。(本文我会坚持更新的,学到哪里更新到哪里,并且永远不会加入vip计划)
串口相关
在蓝桥杯里,可以借助cubemax快速开发stm32g431rbt6,所以主要掌握cubemax的配置和代码修改即可
首先打开cubemax,选择对应芯片,接着进入这个界面,时钟树配置就省略了
主要核对圈起来的配置,mode选择异步,第二个圈为波特率,这里我改为了9600,另外需要特别注意的是需要手动配置PA9和PA10为串口收发引脚,另外再去将nvic的串口中断打开。
接下来生成代码即可。
串口发送
然后我们测试一下串口助手是否能正常接受,首先我们先去hal库底层关于uart的.h文件里,滑到最下面往上翻,找到函数列表,就可以找到一个和串口发送相关的函数
接下来写一个简单的发送函数测试一下功能
#include "string.h" #include "main.h" UART_HandleTypeDef huart1; void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_USART1_UART_Init(void); int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); char text_uart[20]="Hello\r"; while (1) { HAL_UART_Transmit(&huart1,(uint8_t*)text_uart,strlen(text_uart),500); HAL_Delay(500); } }由于发送函数有四个参数,我们用的串口1,所以第一个传入&huart1,第二个参数是发送的数据,这里定义一个字符数组,存放要发送的数据,第三个是数据长度,这里引入string.h的头文件,用于测量字符长度,最后一个参数是超时时间,由于这个函数是阻塞式的,所以要设置一个超时时间,防止程序卡死在这里,然后用hal库自带的延时函数,延时500ms发送一次,其中字符数组里的\r可以让接收数据自动换行,最后在串口助手里就是这样的效果
串口中断
用同样在hal库底层中关于uart的.h文件的函数列表里找到uart相关的中断回调函数,主要不要找错了,这里有很多相似的函数,只有这一个才是我们要用的
利用串口中断做一个测试,让他返回我们发送的数据,其中回调函数里的\n的意思是检测到换行键就认为数据传完了,所以发数据的时候需要手动在数据后面加一个回车键,传完了之后就给他加个\0作为停止位,然后在通过串口返回函数发回来,需要注意的是,hal库的串口发送函数是以\0作为停止标志,如果发送的字符数组中有一个\0,那么其后的所有数据都不会发送,但他仍存在数组中,另外需要注意的地方就是,每次触发中断后,仍然需要再次开启中断,建议开启函数写在中断回调函数的最后。
#include "string.h" #include "main.h" UART_HandleTypeDef huart1; void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_USART1_UART_Init(void); uint8_t rxdat; char receive_dat[30]; uint8_t pointer=0; int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); HAL_UART_Receive_IT(&huart1,&rxdat,1); while (1) { } } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance==USART1) { receive_dat[pointer++]=rxdat; if(rxdat=='\n') { receive_dat[pointer-2]='\0'; HAL_UART_Transmit(&huart1,(uint8_t*)receive_dat,strlen(receive_dat),500); pointer=0; } HAL_UART_Receive_IT(&huart1,&rxdat,1); } }定时器相关
定时中断
定时器中有几个重要的名次,分别是预分频值(psc)和自动重装值(arr),预分频值的意思是,时钟信号来定时器计数之前先进行分频,比如进入的时钟频率为80Mhz(8千万hz),经过40000的预分频值就变成了2khz,需要注意的是,单片机中预分频值从零开始记,零表示不分频,那么要设置40000psc,程序中就要写40000-1,另一个自动重装值的意思是,时钟信号经过预分频之后,要震荡多少次才会触发一次中断,比如现在预分频之后是2khz的信号,arr就设为200,那么就是一秒触发10次中断,以为预分频之后的信号一秒震荡2k次,而震荡两百次就触发一次中断,那么一秒当然触发十次中断,预分频值的设置也和psc一致,都要减一。
如上图这样设置,时钟源选择内部时钟,统一配置成80Mhz,这样就是每秒触发两次中断。
接下来依旧老方法,去hal库底层关于time的底层.h文件的最底下函数列表里寻找,中断开启函数
中断回调函数在最下面
这样我们不需要记住函数名字,我们只需要能在底层中找到对应函数,并且会用就可以了。
接下来编写一个简单的测试函数,让串口每隔一段时间发送一个数据
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim==&htim1) { i++; if(i==9) {time++;i=0; if(time==10)time=0; uint8_t send=time+'0'; HAL_UART_Transmit(&huart1,&send,1,500); } } }需要注意的是,在使用这个回调函数之前需要在main函数里先开启定时器中断才可以。
遇到的一些问题
首先就是,我是用cubemax生成代码然后移植过去的,少移植了一些东西,导致我在debug的时候发现程序直接卡死在了中断向量表,下面总结一下需要移植的东西,main.h文件里的那些初始化函数就跳过了,首先就是msp.h里面得相关mspInit和DeInit函数(似乎不要也可以),然后是在这个文件里还要声明一个外部可用的句柄,然后就是再去main.h里面,找hal库的宏定义,将time相关的文件包含进去,(步骤就是main.h->stm32g4xx_hal.h->stm32g4xx_hal_conf.h)这里面会找到很多宏定义被注释掉了,只需要再把需要的添加进去即可,另外如果在cubemax里面更新过某个外设的时钟也要去时钟配置函数里看一下修改的部分。(剩下的以后学到更多再添加)
PWM相关
PWM输出
在这块嵌入式板子中,为了观察实验现象,我们将led的相关引脚作为pwm的输出通道,以便于观察代码是否顺畅运行。
需要格外注意的是,这里我们使用的是PC8作为pwm的输出引脚,那么和lcd相关的所有函数就不要再使用了,因为会和PC8引脚冲突,目前我还没找到解决办法,如果是简单操作这个引脚的高低电平,可以在调用lcd相关函数的时候保存一下相关寄存器的状态,等函数结束后再改回去。
首先打开原理图,这里使用PC8作为pwm的输出通道按照如下图所示配置
在引脚配置里我们选择了ch3,那么这里就配置通道3,PSC和ARR前面已经说过了,不再赘述,这里还有一项需要配置,就是比较值,即output compare preload(CCR),顾名思义就是和某个值进行比较使用的,这里是和arr比较,arr会随着预分频后的信号逐渐增加,当arr小与crr时,就输出高电平,反之输出低电平,这里我们可以让CCR为950(图片中的500不用管,懒得换了),为什么设为950,因为原理图里面,led要想亮必须对应引脚是低电平才可以,也就是说高电平led会熄灭,这里把高电平时间设置高一些,这样led比较暗,容易观察到是否有现象,然后再去开启nvic中断即可。
进入keil之后依旧去time相关的.h文件里寻找相关函数,下图函数是pwm定时器开启函数,由于我们已经设置过CCR的值了,所以不需要更改,但是更改相关函数是这个(__HAL_TIM_SetCompare(&htim3,TIM_CHANNEL_3,990);)第三个参数就是CCR的值了。
接下来编写一下代码即可,这里要让锁存器使能,所以要让PD2引脚输出高电平。
#include "main.h" TIM_HandleTypeDef htim3; void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_TIM3_Init(void); int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM3_Init(); HAL_GPIO_WritePin(GPIOD,GPIO_PIN_2,GPIO_PIN_SET); __HAL_TIM_SetCompare(&htim3,TIM_CHANNEL_3,990); while (1) { } }PWM输入捕获
从硬件里看到这两个信号发生器,我们测量PB4引脚的输出信号即可,我在网上搜集到的资料显示,这个xl555芯片组成的信号放生电路和模拟电子技术中的ne555组成的信号发生电路类似,占空比基本只能控制在百分之五十,所以测量占空比时显示百分之50左右是正常现象
cubemax里面按照这种模式配置,通道一设为直接模式,通道二设为间接模式。
通道一设为上升沿触发,通道二设为下降沿触发,接下来开启中断,当然不开启中断也可以使用函数读取,不过需要在while循环里一直扫描,比较占用资源,所以使用中断的方式。
和这部分相关的函数还是可以去.h文件里找,这里直接罗列
//输入捕获中断回调函数 void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) //读取计数值 HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1) //开启定时器通道输入捕获 HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_1);接下来只需要写一下中断回调函数里的逻辑,逻辑比较简单不再赘述。
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if(htim->Instance==TIM3) { if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) { PWM1_T_Count = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1)+1; PWM1_Duty = (float)PWM1_D_Count/PWM1_T_Count; } else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2) { PWM1_D_Count = HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_2)+1; } } }ADC电压采集
单通道
只需要如此配置即可,模式中Single-ended是对地模式,即相对0v,另一个顾名思义是差分模式,用于测量两个引脚的电压差,需要注意的是差分模式仅限于同一个adc外设。
在这个芯片中adc都是12位通道的,因此adc的映射是将3.3v分为4096份,因此我们获得的adc的值是介于0到4095之间,因此我们在程序中要注意映射关系,我们可以编写如下所示简单封装一下adc,让他自动开启自动关闭,我们只需调用即可,电压映射关系我们只需要用这个简单关系映射即可,330*get_adc(&hadc1))/4095,这个映射出来1个就代表0.01v,可以根据精度调整。
uint32_t get_adc(ADC_HandleTypeDef *hadc) { HAL_ADC_Start(hadc); uint32_t adc=HAL_ADC_GetValue(hadc); HAL_ADC_Stop(hadc); return adc; }这样我们便可以采集相应数据。
按键
按键扫描
按键扫描这一部分,比较简单,只需要在正确配置gpio口为读取模式即可,使用如下代码来循环扫描按键的值。
unsigned char Key_Scan(void) { unsigned char unKey_Val = 0; if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET) unKey_Val = 1; if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET) unKey_Val = 2; if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_2) == GPIO_PIN_RESET) unKey_Val = 3; if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) unKey_Val = 4; return unKey_Val; }按键检测
按键检测主要考察按下触发或者松开触发以及长按等,我们可以使用如下代码块来实现
void Key_Proc(void) { if((uwTick - uwTick_Key_Set_Point)<100) return; uwTick_Key_Set_Point = uwTick; ucKey_Val = Key_Scan(); unKey_Down = ucKey_Val & (ucKey_Old ^ ucKey_Val); ucKey_Up = ~ucKey_Val & (ucKey_Old ^ ucKey_Val); ucKey_Old = ucKey_Val; /* 此处将注释删除写逻辑 */ if(unKey_Down) uwTick_Key_LongStart = uwTick; if(ucKey_Val && (uwTick - uwTick_Key_LongStart)>=2000) ucKey_LongPress = 1; if(ucKey_Up) {uwTick_Key_LongStart = uwTick;ucKey_LongPress=0;} }先解释一下各个变量的意思:
1.uwtick变量是hal库自带的计时变量,用它来为按键扫描计时。
2.uwTick_Key_Set_Point变量用来为按键按下时间计时。
3.unKey_Down,ucKey_Up分别用来检测下降沿和上升沿,只在触发瞬间取值
4.ucKey_Old变量为上次按键的值
unKey_Down = ucKey_Val & (ucKey_Old ^ ucKey_Val)这一句的意思是先将旧值和新值按位异或,这样就得到按键的变化情况,再和按键值与,这样就得到按下的按键,同理得到松开的按键
读懂上半部分下半部分便无需讲解。
RTC定时器
只需要在cubemax中这样配置即可开启时钟和日历功能即可异步预分频值和同步预分频值分别设为124和5999,这样乘出来正好是750khz,rtc的计时频率就是1hz,前提是给rtc的时钟是750khz。
另外此处编码设为BCD码,这里不清楚是否是hal库的bug,读取的时候需要以二进制读取才可以正确读取,设置完成之后,只需要调用以下两个函数
HAL_RTC_GetTime(&hrtc, &H_M_S_Time, RTC_FORMAT_BIN); HAL_RTC_GetDate(&hrtc, &Y_M_D_Date, RTC_FORMAT_BIN);这样时间信息就会自动存入H_M_S_Time和H_M_S_Date这两个结构体中,需要注意的是,即使不需要日期也应该读取日期,不然时间不会正常读取,似乎也是bug。
剩下两个模块懒得介绍了,大概率考不到
AT24C02
void iic_24c02_write(uint8_t *pucBuf, uint8_t ucAddr, uint8_t ucNum) { I2CStart(); I2CSendByte(0xa0); I2CWaitAck(); I2CSendByte(ucAddr); I2CWaitAck(); while(ucNum--) { I2CSendByte(*pucBuf++); I2CWaitAck(); } I2CStop(); delay1(500); } void iic_24c02_read(uint8_t *pucBuf, uint8_t ucAddr, uint8_t ucNum) { I2CStart(); I2CSendByte(0xa0); I2CWaitAck(); I2CSendByte(ucAddr); I2CWaitAck(); I2CStart(); I2CSendByte(0xa1); I2CWaitAck(); while(ucNum--) { *pucBuf++ = I2CReceiveByte(); if(ucNum) I2CSendAck(); else I2CSendNotAck(); } I2CStop(); }MCP4017
void write_resistor(uint8_t value) { I2CStart(); I2CSendByte(0x5E); I2CWaitAck(); I2CSendByte(value); I2CWaitAck(); I2CStop(); } uint8_t read_resistor(void) { uint8_t value; I2CStart(); I2CSendByte(0x5F); I2CWaitAck(); value = I2CReceiveByte(); I2CSendNotAck(); I2CStop(); return value; }