今年的蓝桥杯嵌入式组是以stm32G431RBT6作为主控,与前年不同与去年相同。
比赛使用的下载器是DAP调试器,使用CubeMX快速创建工程,Keil5作为编译器,第一次使用CunbeMX创建stm32G431RBTx的Keil工程后,Keil会自动下载关于此型号设备的资源,需要等待一段时间。工程创建好后,点击魔术棒,找到Debug将下载器改为CMSIS-DAP,然后点击settings,进入默认界面找到端口port,将类型改为sw,这与CunbeMX配置所选一致,使用Serial Wire方式下载。然后再Download中勾选Reset and Run。由于是第一次创建工程,这里要做一个添加,如图将上面的配置做好后我们就可以开始学习了。
一.点灯
在初学单片机时,上来第一个实际操作一般都以点灯为主,且是蓝桥杯比赛的考点。
这是官方给的产品手册,相比于常规的学习板来说它都是以LED作为点亮的器件,当导线接地,电流导通LED被点亮,但是与其他学习板不同的是。这里面的线路不是单连接一个开关,而是使用了一个锁存器。学过数电或者类似专业课的同学可以知道,锁存器中当输出端状态改变使能端LE为高电平时,锁存器会正常输出前面输入的信号,也就是同步改变。但当LE为低电平时,状态会被锁住,输出不会因为输入的改变而改变。这里要与触发器做好区分。所以当LE为高电平时,我们才可以改变LED的状态,这就意味着,在后面我们要根据题目对LED进行状态改变时,一定要记得将LE端电平拉高。
二.LCD
LCD这里不谈如何写清屏等函数如何设计,由于比赛直接提供LCD有关的库文件,这里我们需要学如何把官方给的库函数移植到工程里,在工程文件夹中加入BSP文件夹,我们之后所有的c和h文件都可以放在这里。将官方提供的lcd.c和lcd.h以及fonts.h都放在BSP文件夹里。接着,在KEIL工程里添加一个新的文件夹,也叫BSP,然后添加已存在的文件,将c文件放进去,最后一步,找到魔术棒里的C/C++选项,将BSP头文件路径添加进去,这样就大功告成了。将官方给的几个库函数熟练运用,非常简单。别忘了LCD初始化。
三.定时器
定时器对于开发来说非常重要且是比赛考点,它的功能不仅仅局限于设定时间,更可以在定时的基础上完成其他高级功能。比如输出PWM等。F103系列有8个定时器,定时器6、7为基本定时器,2-5为通用定时器,1、8为高级定时器,而G4系列是多了三个通用定时器13、14、15。学习中常用的是通用定时器。所以在这里我们使用一个TIM4来完成一个定时器中断+定时器按键消抖的例子。由于常规的软件延时函数会导致阻塞、占用资源、定时不准确,所以使用定时器,硬件定时是最好的。
在CubeMX配置一个定时器,我们使用TIM2,在TIM2中打开Clock Source:Internal Clock,然后根据定时器计时配置分频数和重装载数。这里有个公式
注意我们使用的这款STM32官方例程中使用的Tclk是80MHz,只要不大于170MHz就好,那么这里以80MHz为例,计算一下想要设置定时时间为10ms,则T=0.01s,Tclk=80000000Hz。将这两个数带到公式里,然后找到合适的PSC和ARR即可。
进入工程,程序设计思路:
可以直接将回调函数写在main.c文件里,最好是可以单独创建两个文件,h文件主要是创建结构体,声明函数,c文件里就包括回调函数里的具体逻辑实现。在c文件中新建一个四位结构体对象,分别代表四个按键,将结构体里面的参数全部默认为0。然后使用EDA中状态机的方法开始模拟按键的状态,最后输出不同的结果,要知道,使用状态机的过程中,各个过程之间的转化是需要时间的,而这个时间就是设置的定时器时间,当按键按下进入第一个状态,等10ms过后依然是这个状态,则进入第二个状态,最后按键松开进入第三个状态,输出结果为主函数所利用。
不要忘了主函数中定义"extern struct 结构体名 结构体对象名[];(作用是使用其他文件中的变量)"以及定时器的启动HAL_TIM_Base_Start_IT(&htim4);
四、按键控制显示
此部分概括一下如何根据不同按键以及长按短按,将不同的内容显示到LCD上。前面说过,结构体里有多个参数。为了完成上述的要求,这里至少要设置五个元素(包含消抖):按键状态检测a、状态机状态序号b、按下标志c、长按键时间d、长按键标志e。
循环开始。初始b=0,当a==0,此时b=1,d=0;进入下一个状态b=1,若a==0,则b=1,否则b=0即回到初始状态;进入最后一个状态即b=2,此时定时器不断以10ms的时间间隔执行htim->Instance==TIM4内的语句,当a持续为0,每检测一次就将d+1.当按键松开即a=1,检测d的数值,根据题目要求时长看是否满足长按键条件,若满足则e=1(长按键),否则在松开按键时c=1(短按键),并将b、d清零以便下一次检测。此时根据四个按键的e或c的值在屏幕上输出不同内容,且执行显示语句后,都要将e或c清零。
五、pwm输出
有时题目中会要求输出一个**HZ频率的脉冲信号,那么这里所说的脉冲信号就是PWM波。比如我们这里要输出一个100HZ的脉冲信号,则我们需要满足HCLK/(PSC+1)*(ARR+1)=100,PSC和ARR要在CubeMX里配置好,并且要配置PWM Gerneration里的Pulse。进入工程,首先要打开PWM通道HAL_TIM_PWM_Start(&htima ,TIM_CHANNEL_b );//打开PWM定时器a 通道**b。**然后题目中若是要求占空比的变化我们可以使用__HAL_TIM_SetCompare(&htima,TIM_CHANNEL_b,number);这里number的范围在0-100。然后根据题目的逻辑以及前面完成的按键功能进行编程。
六. 信号发生器(555计数器)产生的信号测量
可以看到在比赛的开发板中是有两个旋钮控制频率输出的。并且根据原理图可以看到,在555计数器的THRES引脚连接着一个电位器,这个电位器就是我们的旋钮,通过这一旋钮改变电流电压值,使进入阈值引脚的信号发生变化,进而影响OUT引脚的输出(频率)。在OUT引脚后面可以看到有一个J10,这个可以通过一个跳线帽直接将信号输入到PA15或PB4,便于直接测量555定时器的信号频率。那么了解了原理之后插上J10和J9的跳线帽我们开始配置CubeMX以及程序的编写。
打开CubeMX找到PA15和PB4这两个引脚,点开发现分别由TIM2_CH1和TIM3_CH1,这两个就是我们测量时所需要的通道。将二引脚点亮之后将其通道一配置为直接输入(input capture direct mode)并改为上升沿捕获(Rising Edge)。生成代码。 首先在主函数中打开这两个定时器的通道一: HAL_TIM_IC_Start_IT(&htim2,TIM_CHANNEL_1);//频率测量捕获定时器开启
HAL_TIM_IC_Start_IT(&htim3,TIM_CHANNEL_1); 然后在之前的按键中断里写如下代码:
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)//中断捕获回调函数
{
if(htim->Instance==TIM2)
{
count=HAL_TIM_ReadCapturedValue(htim,TIM_CHANNEL_1);//读取计时值
__HAL_TIM_SetCounter(htim,0);//把计时值清零
frq=(80000000/80)/count;(80000000为HCLK,80为PSC+1)
HAL_TIM_IC_Start(htim,TIM_CHANNEL_1);//重新打开一下定时器
}
}
在主函数中调用frq这个变量就好了。 如果说想要测量脉冲信号的占空比,我们可以在CubeMX做如下配置:
这里加了个间接通道Channel2,且在两个通道(直接通道、间接通道)分别配置为上升沿计数和下降沿计数。上面我们测量了直接通道的计数值,那么这里同样测量间接通道值,然后将直接和间接的计数值带入公式:(间接/直接)*100。注:中断消息来源要选择直接输入通道。htim->Channel==HAL_TIM_ACTIVE_CHANNEL_1
七 ADC模数转换
模拟信号一般在实验室使用示波器能看到,而使用单片机进而显示电压状态需要的是数字信号。当然连续的模拟信号需要转化为一个个的数字信号,这里就需要使用ADC不断采集。 打开CubeMX,并根据原理图找到两个电压采集旋钮的IO口。
然后打开两个ADC的single-end。 进入程序,HAL库给我们提供了非常大的方便内置了许多函数(记得当时使用51单片机+adc0809,中间要根据时序图采集,并且设置采集频率很麻烦) HAL_ADC_Start(&hadc1);//打开ADC1 adc = HAL_ADC_GetValue(&hadc1);//获取ADC的值 我们的ADC配置时是12位,我们最后返回的值应该是(adc*3.3/2的十二次方)
八.UART串口通信
串口通信简单来说就是两个设备之间的数据传输,数据传输通过TXD和RXD,一般使用CH340用来USB转串口,输入/输出数据缓冲器(SBUF)等,关于串口有很多知识点,蓝桥杯嵌入式组的话学会怎么用就可以,具体知识点可以看我的另一篇博文串口通信编程_串口编程-CSDN博客。
找到之前做过的频率实验,我们这次的目标是把频率发送到上位机。打开之前配置好的CubeMX,我们要在里面加点东西,找到PA9和PA10引脚。根据官方的原理图可以看到,PA9和PA10分别对应TX和RX,打开USART1,将Mode选为异步,然后根据题目设置波特率,最后打开串口中断,然后生成代码。进入到工程里,由于需要不断的向上位机发送数据,我们的处理应该在while循环里,下面是一个发送频率的案例
char temp[20];
sprintf(temp,"frq=%d\r\n",frq);
HAL_UART_Transmit(&huart1,(uint8_t *)temp,strlen(temp),50);
这里用到了一个HAL库内置函数HAL_UART_Transmit(&huart1,(uint8_t *)temp,strlen(temp),50);第一个参数是串口的句柄,选择使用哪个串口,第二个参数是发送的内容,第三个参数是发送字符串的长度,第四个参数Timeout是超时时间,代表某次执行函数,最多占用串口的时间。至此数据发送就已经完成了,很简单。下面说说串口通信接收。
还记得上面我们开启了串口中断吗。与定时器中断类似,这里也需要使用一个串口中断回调函数,并且每使用一次中断回调函数就应该接收一次数据,因此,在中断回调函数内需要使用接收函数HAL_UART_Receive_IT(&huart1,&rxdat,1);//这里的1是因为中断接收只能接收一个字符。由于每次只能接收一个字符,所以需要一个变量来接收单个字符,还需要一个字符串数组来存储字符串,当然还需要一个下标。这样基本的逻辑就对了。参考代码如下:
//串口接收的回调函数
char text[50];
uint8_t tex;
unsigned char index;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
text[index++]=tex;
HAL_UART_Receive_IT(&huart1,&tex,1);//这里的1是因为中断接收只能接收一个字符
}
剩下的就需要看题目要求了,在主函数进行对应的处理就好,这里由于是使用串口调试助手,就一定会使用到输入数据给单片机,这里还是要注意输入的格式,根据题目输入的格式在代码中提前准备好。使用sscanf进行输入,这个知识点初学c语言时应该都学过,就不过多赘述了。