在之前的一篇博文中,我讲解了常用的通信协议,在这一篇以及接下来的几篇里,我会给出实际使用的代码以及配置方式。(程序会放在Q群)
交流Q群:659512171
目录
一,基础知识:
关于串口通信协议,我在之前的博文讲过了 传送门:,所以这里只说一下STM32的串口功能。
STM32具有同步串口USART和异步串口UART,这两种串口的区别我也在之前的博文里说过了,在实际项目中,一般只使用异步串口就可以满足需求了(因为一般是与PC端的数据传输,主从机一般使用IIC和SPI),由于实验中使用的是USB转串口模块,没有同步通信所要求的CLK时钟线,所以这里只做异步串口通信的实验。
STM32接收方式:这里指的不是STM32中的外设,指的是在程序设计中所设计的接收方式,一般常用的有轮询式,中断式(又可分为多种方式,如字节中断,DMA中断,空闲中断等),其中使用空闲中断可以方便的接收不定长数据,DMA中断可以有效的减小CPU负载,轮询式简单但是容易与其他程序冲突导致丢失数据,所以在使用中应结合实际选择合适的方式
二,程序设计:
设计思路如下:使用STM32CubeMX生成基础配置文件,先通过轮询式验证单片机是否正常,然后编写中断模式下的不定长数据接收,以及空闲中断,DMA接收等方式接收数据。
三,STM32CubeMX配置:
1,新建工程:
打开STM32CubeMX,新建一个工程:
2,配置项目:
配置RCC时钟:
3,配置调试:
回到前面的页面打开SYS,配置调试方式为Serial Wire:
4,配置串口:
配置DMA:
配置中断:
也可以在如下区域内调整其它串口配置,不过一般使用默认配置
5,配置项目:
6,生成项目:
点击右上角的 GENERATE CODE 生成项目,在弹出的对话框中选择 Open Project 打开工程
四,Keil编程:
1,基本程序:
打开 main.c 和 usart.c 文件
接下来要添加串口重定向函数,需要在 usart.c 文件中先包含 stdio.h 头文件,为了方便以后使用,直接再usart.h文件中引用
右击上面的 #include "usart.h" 打开 usart.h文件
然后回到usart.c文件,下滑到文件末尾,找到 /* USER CODE BEGIN 1 */ 这里,这两段注释是用户代码区,在这里添加串口重定向
/* USER CODE BEGIN 1 */
//发送的重定向,重定向以后可以使用printf等函数
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
//接收的重定向,重定向以后可以使用scanf等函数
int fgetc(FILE *f)
{
int ch;
HAL_UART_Receive(&huart1, (uint8_t *)&ch, 1, 1000);
return (ch);
}
/* USER CODE END 1 */
重定向函数无需函数声明,直接写好就可以了
然后我们再转到main.c中,编写测试程序(由于 stdio.h 是在usart.h中引用的,所以无需再次引用stdio.h)
别忘了连接USB转串口,连接方法:
“然后编译,下载,连接好USB转串口,打开串口监视器,观察实验现象……为什么没有现象?”
因为如果使用 printf 函数,需要使用 Micro LIB 库,可以在魔术棒里面开启:
然后重新编译,下载,观察现象
可以看到,串口监视器已经可以成功接收到STM32发送的数据了
然后对程序做一些修改,测试接收是否正常
然后编译运行,在串口监视器发送 Hello STM32!观察现象,发现STM32可以成功回复,但是无法停止,这是因为没有加条件判断,导致一直发送
此方法只适用于定长数据接收,不定长就会接收出错,所以一般不采用这种方式接收,使用中断方式接收数据更加完整,资源占用更少,另外,如果是自定义的协议,还可以在帧尾加入自定义的字符作为结束标志。
2,中断接收:
(1),普通中断接收:
普通中断模式也就是接收到一个字节就进入中断处理,直到接收到结束符后再将数据输出,这种模式的优点是没有数据量的限制,但缺点就是对CPU的占用较大,对于CPU占用敏感的场合不建议使用。
先在 main.c 中定义几个变量
/* USER CODE BEGIN 0 */
uint8_t RX_buff[512]={0}; //定义数据存放数组
uint8_t ch; //定义接收缓冲
int flag = 1; //定义标志位
int i = 0; //定义计数变量
/* USER CODE END 0 */
接着在 main.c 中下翻找到 /* USER CODE BEGIN 4 */ 和 /* USER CODE END 4 */ 两个注释段,在这里编写如下的中断处理函数(理论上中断可以在任何一个包含串口程序的文件中编写,不过为了方便,这里在 main.c 中写中断)
/* USER CODE BEGIN 4 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
if(huart == &huart1){ //判断是否是串口一的中断
RX_buff[i] = ch; //把写入串口接收的数据
i++;
if(ch == 0x0d){ //判断结束符(0x0d 回车,0x0a 换行符)
for(int t=i;t<512;t++){
RX_buff[t] = 0;
} //清空除接收数据外的数据,防止接收长数据后再次接收短数据出现问题
i=0;
flag = 1; //接收完成标志
}
HAL_UART_Receive_IT(&huart1,&ch,1); //再次开启中断
}
}
/* USER CODE END 4 */
然后在主循环中编写处理程序
/* USER CODE BEGIN 2 */
HAL_UART_Receive_IT(&huart1,&ch,1); //开启中断
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(flag != 0){
printf("%s",RX_buff); //串口打印接收到的数据
flag = 0; //清空标志位
}
}
/* USER CODE END 3 */
编译,下载,运行,然后再串口监视器中发送几个数据,观察现象
可以看到数据被完整的显示出来了,即使变换长度也没有问题(发送的数据末尾要加一个回车),同时只要发送的数据不超过数据数组的大小,都是可以完整接收的。
(2),空闲中断接收:
空闲中断顾名思义就是在串口超出一定时间没有新数据(即空闲)时产生中断,优点是对CPU占用减小,同时可以在没有结束符的情况下接收不定长数据。
空闲中断就不需要那么多的变量了,只需要保留两个变量即可
/* USER CODE BEGIN 0 */
uint8_t RX_buff[512]={0}; //定义数据存放数组
int flag = 1; //定义标志位
/* USER CODE END 0 */
空闲中断的中断回调名称与处理逻辑不一样,都要改一下
/* USER CODE BEGIN 4 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart,uint16_t Size){
if(huart == &huart1){ //判断是否是串口一的中断
__HAL_UNLOCK(huart); //解锁串口状态
flag = 1; //接收完成标志
HAL_UARTEx_ReceiveToIdle_IT(&huart1,RX_buff,511); //再次开启空闲中断
}
}
/* USER CODE END 4 */
然后只需在主循环中做一些小的改动,改一下开启的中断,加个清空数组的循环,同时在输出时加一个换行符
/* USER CODE BEGIN 2 */
HAL_UARTEx_ReceiveToIdle_IT(&huart1,RX_buff,511);//开启空闲中断
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(flag != 0){
printf("%s\r",RX_buff); //串口打印接收到的数据
flag = 0; //清空标志位
for(int t=0;t<512;t++){
RX_buff[t] = 0;
} //清空数组,防止接收长数据后再次接收短数据出现问题
}
}
/* USER CODE END 3 */
再次编译,下载,运行……直接上效果图
可以看到无论怎么发只要不超过数据数组的大小都可以稳定的接收到,同时数据末尾不要求有回车,但是此方法也有一个问题,就是如果发送端不是打包好发送,而是发送一个后再次发送下一个,有可能因为中间间隔时间过长导致将一个数据分割成两个,或者是两个数据间的间隔时间过短导致把两个数据合为一个,不过在实际项目中很少会遇到这种极端情况,放心用就好。
(3),DMA空闲中断接收:
前面的空闲中断接收已经大幅降低了CPU的使用率,但是DMA模式下的空闲中断接收更进一步,接收时根本无需CPU参与,关于DMA在我之前ADC+DMA实现模拟量读取的那一篇博文里有简单的介绍,这里不再重复 传送门
DMA空闲中断的配置也很简单,只需在空闲中断接收的基础上略微改动
/* USER CODE BEGIN 0 */
uint8_t RX_buff[255]={0}; //定义数据存放数组
int flag = 1; //定义标志位
/* USER CODE END 0 */
中断处理函数名称没有变,还是空闲中断的名称,只不过使用DMA方式将数据搬到数组里,但是要增加一个停止DMA接收的函数,并且把再次开启中断的语句改为开启DMA模式下的中断
/* USER CODE BEGIN 4 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart,uint16_t Size){
if(huart == &huart1){ //判断是否是串口一的中断
HAL_UART_DMAStop(huart); //暂停DMA接收
__HAL_UNLOCK(huart); //解锁串口状态
flag = 1; //接收完成标志
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,RX_buff,255); //再次开启DMA空闲中断
}
}
/* USER CODE END 4 */
主循环里只需要把开启中断的语句改为开启DMA中断即可
/* USER CODE BEGIN 2 */
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,RX_buff,255);//开启DMA空闲中断
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(flag != 0){
printf("%s\r",RX_buff); //串口打印接收到的数据
flag = 0; //清空标志位
for(int t=0;t<512;t++){
RX_buff[t] = 0;
} //清空数组,防止接收长数据后再次接收短数据出现问题
}
}
/* USER CODE END 3 */
然后还是编译,下载……
可以看到,与空闲中断一样,都可以十分稳定的接收数据(当然空闲中断的问题它也有),不过对CPU的占用少了许多,不过依然有一个问题,可能在刚才改代码是就已经发现一些端倪了,当一次性接收的数据长度大于 255 时,会丢失后面的数据,用常规空闲中断就不会出现这样的情况,可以通过做双缓冲区解决,具体原因我推测应该是某个寄存器是8位寄存器,最大只能储存255长度的数据,有哪位大佬知道的可以在评论区留言
总结:
串口是比较常用的外设,掌握好串口的使用,以后在学习串口通信的模块(如ESP01,HC05)时会很简单,本篇给出了常用的几种串口接收方式,其中以空闲中断方式泛用性最强,DMA中断式资源占用最少,轮询式最简单,使用时根据自己的需要选择即可。
---------------------------------------------------------- 完 ----------------------------------------------------------
都看到这里了不给个关注吗?
完整程序已经放在Q群了
交流Q群:659512171