DMA串口通信&解决数据错位&收发不定长数据&vofa的使用
最近发现了一个非常好用的数据观察工具, 这个软件十分强大, 可以实时接收串口的数据, 并且将对应的数据的形式显示出来, 来看下图:真的可以说非常方便, 可以很直观的,同时也可以通过这个上位机, 也可以看到数据的波动情况, 很适合调节PID, FOC之类的算法.发现了这个之后我一时兴起, 写了个串口发送的程序, 可能是由于脱离技术太久迟迟, 即便是写这样的一个程序也不是那么的手到擒来了, 不过在这其
最近发现了一个非常好用的数据观察工具vofa, 这个软件十分强大, 可以实时接收串口的数据, 并且将对应的数据以图形化的形式显示出来, 来看下图:

真的可以说非常方便, 可以很直观的看到数据的走向,同时也可以通过这个上位机软件发送数据, 也可以看到数据的波动情况, 很适合调节PID, FOC之类的算法.
发现了这个之后我一时兴起, 写了个串口发送的程序, 可能是由于脱离技术太久迟迟, 即便是写这样的一个程序也不是那么的手到擒来了, 不过在这其中我也踩到了不少以前没有踩过的坑, 同时针对数据的发送和接收我做了一定的优化, 比如说接收不定长的数据, 以及DMA搭配串口的时候出现的奇怪问题, 以及串口重定向printf函数等等, 下面就让我来一一介绍吧.
注: (以下都是基于HAL库实现)
硬件平台: STM32F103zet6
软件平台: STM32CubeMX + CLion
DMA搭配串口发送数据
其实想到数据的观测我第一个想到的就是DMA, 因为观测讲究实时性, 自然是速度越快越好, 这里我第一个想起来的就是DMA搭配串口.
下面我来说明一下DMA搭配串口发送数据的基本流程:
配置stm32cubemx
对于stm32cubemx的配置, 这里我直接贴图了:
时钟频率改为72MHz

打开串口一, 相关参数默认就行

勾选全局中断, 如果想用DMA的话, 必须要打开全局中断
一般来说如果是大工程的话最好分配一下中断优先级, 不过这里我们仅仅是测试这一个功能, 也就无所谓了

来到DMA配置栏目, 点击下面的Add选项, 添加RX通道和TX通道

Rx和Tx通道的配置如下,这里的RX一定是Circular:


大差不差, 配置好之后, 生成代码, 接下来我们来到编译器中.
代码改写
HAL库仅仅是给我们完成了初始化的流程, 有些东西我们还得自己添加, 根据配置的顺序
这里我定义了几个数据存储的数组, 用来存储发送和接收的数据
#define RE_MAX_LEN 100 //todo 最大数据长度
extern uint8_t ReBuff[RE_MAX_LEN]; //todo 数据区
随后是手动开启串口中断, 这个其实初始化一次就行了
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
然后是DMA的手动配置, 这个需要反复调用, 因为每次发送数据之后, DMA都会自动关闭, 所以需要手动调用此函数开启, 所以这个函数我们主要添加在两个地方:
- main初始化
HAL_UART_Receive_DMA(&huart1, (uint8_t *)ReBuff,10);//启动DMA接收,UART1_RX_BUF:数据接收缓冲
- 中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance==USART1)
{
HAL_UART_Transmit_DMA(&huart1,(uint8_t *)ReBuff,10);
HAL_UART_Receive_DMA(&huart1,(uint8_t *)ReBuff,10);
}
}
一般来说这样就可以了, 但是当我用串口助手调试的时候出了问题, 比如说我发送字符串"123456"

上图我其实点了一次, 但是并没有回传任何数据, 于是我点了第二次

可以看到这次的确回传了数据, 但这并不是我想要的, 注意看这里的数据长度是10, 而我发送的数据长度是6, 同时在'6'的后面
其实这个原因也并不难找出,我们设置DMA的传输字节数量是10Byte但是我们一次性只发了6个,剩下的4个被留到了下一次填充,下次又仅仅填充后两个,中间是被截断了,也可以视为数据发生了错位,这个问题相对棘手,发送的内容被限定死了,这是十分不方便的,如何解决呢?
空闲中断IDLE
我之前并没有听说过这个中断类型,来看看官方手册的解释

也就是说如果RX并不是处于接收数据的状态的话,那就是空闲状态,也就是说接收完数据一段时间后如果还没有接收到其他的数据这个IDLE就会被置为1,你看,这不就是天然的不定长数据响应器吗?而且这个标志位只会置定一次,也就是说不管你空闲多长时间这个IDLE的改变都只会发生在最开始的那一次。
那空闲状态能反映出什么呢?我们又能根据这个做出什么操作呢?
既然知道了什么时候算RX空闲, 那么在其空闲的时候我们可以通过这个状态清除相关的标志位,以及得到原本完整的数据。那具体该怎么做呢?可以看以下步骤(这里我们以接收定长数据帧且带帧头帧尾)
- 关闭DMA传输通道。在DMA的实际内存未达到指定内存时是不会转运的,这是我们需要手动停止让他主动转递数据
- 清除相关标志位。/一个是USART_SR和 USART_DR 当然还有/ 空闲状态标志这两个寄存器内部的数据如果不及时清楚下次DMA传输时就会被带走,我们最好在空闲时就清除
- 判断是否错位。如果发生了错位,那么一次传输是一定不会同时包含固定长度间隔的帧头帧尾的,同时DMA剩余的长度也一定与目标长度不符合,这里我们就可以通过这两个条件判断是否错位。
- 如果错位了,我们需要重新设定DMA传输的目标长度,同时清除寄存器的内容(也可以仅清除帧头帧尾, 这两个是判断的必要条件)
来看看代码
首先在main函数中需要完成以下初始化:
__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE); //使能空闲中断
HAL_UART_Receive_DMA(&huart1, (uint8_t *)ReBuff,RE_MAX_LEN);//启动DMA接收,UART1_RX_BUF:数据接收缓冲
uint32_t IDLEIRQ = 0;
uint32_t OkRX = 0;
uint32_t ErrorRX = 0;
uint32_t DMA_Residual_length = 0;
void USART2_IRQHandler(void)
{
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))
{
IDLEIRQ++;
__HAL_DMA_DISABLE(&hdma_usart1_rx); // Disable DMA
// 清除IDLE标志位
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
//清除 DMA传输完成标志位
__HAL_DMA_CLEAR_FLAG(&hdma_usart1_rx, DMA_FLAG_TC6);
// 获取长度
DMA_Residual_length = HAL_DMA_GetCurrDataCounter(&hdma_usart1_rx);
if ((DMA_Residual_length == ReLen) && (ReBuff[0] == 0x0f && ReBuff[24] == 0x00))
{
OkRX++;
UnSbusPack(ChannelRx); // 有效帧,解析并在 ChannelRx 中存储数据
}
else
{
ErrorRX++;
HAL_DMA_SetCurrDataCounter(&hdma_usart1_rx, ReLen);
ReBuff[0] = 0; //清除帧头帧尾即可
ReBuff[24] = 0;
}
HAL_UART_Receive_DMA(&huart1, (uint8_t *)ReBuff,ReLen); // 重新打开DMA通道
}
}
来看看宏定义
#define RC_BAUD 100000
#define ReLen 25 // Sbus Length TX and RX
u8 ReBuff[SbusLength];
u16 ChannelRx[16];
void UnSbusPack(u16 *Array);
接收不定长数据
有了空闲中断,对于不定长数据的接收我们采取以下思路(这里以收发不定长数据为例):
1. 预留足够大的DMA传输空间。这个很熟悉了,我确实不知道如何动态为接收的数据分配存储的空间,我们只能在DMA中预留足够大的传输空间。
2. 在串口接收完后关闭DMA通道。
3. 关闭空闲中断标志位。
4. 获取已接收数据的长度。这个很好得到,我把公式贴在下面
已接收的长度 = 总长度 - 剩余长度
5. 将以接收的对应长度的数据发送出去。发送数据需要指定数据的传输长度,根据3 我们可以很方便的发送出去。不过在下面的演示代码中我把存储接收数据的数组内的元素使用memcpy拷贝到了另一个数组当中使用
6. 重新打开DMA通道
下面来看代码:
宏定义
#define RE_MAX_LEN 100 //todo 最大数据长度
extern uint8_t ReLen; //todo 数据长度
extern uint8_t ReBuff[RE_MAX_LEN]; //todo 数据区
extern uint8_t ReData[RE_MAX_LEN]; //todo 数据缓冲区
中断函数
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 */
if((__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE) != RESET))
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1); //清楚空闲状态标志
HAL_UART_DMAStop(&huart1); //关闭DMA传输
ReLen = RE_MAX_LEN - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); //计算接收到的数据长度
USAR_UART_IDLECallback(&huart1,ReLen ); //调用回调函数
}
/* USER CODE END USART1_IRQn 1 */
}
USAR_UART_IDLECallback自定义回调函数
void USAR_UART_IDLECallback(UART_HandleTypeDef *huart,uint8_t rxlen)
{
if(huart->Instance==USART1)
{
memcpy(ReData,ReBuff,rxlen); //使用memcpy把ReBuff拷贝到了另一个数组ReData当中
HAL_UART_Transmit_DMA(&huart1,(uint8_t *)ReBuff,ReLen); //回传指定长度的数据
rxlen=0; //长度清零
HAL_UART_Receive_DMA(&huart1, (uint8_t *)ReBuff,RE_MAX_LEN);//重新打开DMA中断
}//别忘了在头文件声明
}
printf重定向
关于重定向的问题网上大多都是重写fputc函数实现,如下:
int fputc(int ch,FILE *p) // 函数默认的,在使用printf函数时自动调用
{
USART_SendDate(USART1,(u8)ch);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
return ch;
}
// 此函数定义在stdio.h中,调用此函数需先导入stdio.h头文件
或者是这样的
#if 1
#include <stdio.h>
/* 告知连接器不从C库链接使用半主机的函数 */
#pragma import(__use_no_semihosting)
/* 定义 _sys_exit() 以避免使用半主机模式 */
void _sys_exit(int x)
{
x = x;
}
/* 标准库需要的支持类型 */
struct __FILE
{
int handle;
};
FILE __stdout;
/* */
int fputc(int ch, FILE *stream)
{
/* 堵塞判断串口是否发送完成 */
while((USART1->ISR & 0X40) == 0);
/* 串口发送完成,将该字符发送 */
USART1->TDR = (uint8_t) ch;
return ch;
}
#endif
但是我在我自己的编译器上(CLion)不知为何是无效的,我原本以为是因为我的fputc改写错了于是查找了许多,但好在最后还是跳出了思维,在CLion编译器上重定向printf用的不是fputc 而是 __io_putchar(int), 看下面代码
int __io_putchar(int ch)
{
while ((USART1->SR & 0X40) == 0); // 等待上一次发送完
USART1->DR = (uint8_t)ch; //串口发送字符
return 1;
}
也算是一份收获吧,对于fputc 在keil上确实是可用的,但是如果是其他的编译器比如说我用的CLion可能有所不同。
vofa的使用
发送对应格式的数据
这个官方的文档里有,如果仅仅是接收数据,协议其实很简单

我的就以发送三角函数为例了

设定串口,这个和串口助手差不多这里就贴一下

添加网格图
如果是实时检测数据,肯定是需要一个坐标系的,当然这个也不难找。在控件一栏找到然后拖进来填充全屏就行就行

设置网格图
右键鼠标, 针对这里传进来的数据,我们可以设定其x轴y轴分别是什么, x轴我们一般是设定时间轴, y轴用于显示传进来的数据, 我们可以指定观测哪个数据, 当然也可以把多个数据显示在一个图上选择ALL即可

打开串口
也就是那个看起来像按钮一样的东西

一般来说打开这个开关就可以显示出图像了,但是我在这里又踩了一个坑,可能是因为没有仔细查看纵坐标,我第一次尝试是没有显示出图像的,我反复对照网上的
教程一步一步来但都是无济于事,最后我瞎操作一通无意间解决了。
注意看右下角这里的Auto,这个点一下就会自动校准,折线也是立马就显示出来了。

好了以上就是本期的全部内容了, 希望对你有所帮助, 如果有哪里不对的欢迎指出并联系我.
非常感谢这些资料:
【STM32】DMA+串口空闲中断接收定长数据(解决接收错位问题)
STM32CubeMX配置串口DMA传输实现不定长数据收发
使用clion配合STM32CubeMX开发stm32(包含断点调试,查看寄存器值,printf重定向)
Printf重定向
FirWater协议
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)