之前在学习FOC时,发现了一款名叫VOFA+的串口调试软件。最让人眼前一亮的是它丰富的控件库,包括仪表盘,波形显示,按键滑条等等。体验下来感觉不错,记录下心得。
人到25后记性也开始不好了,也从这第一篇博客开始记录下自己以前学的一些杂七杂八的小玩意。如有错误,还请指教。

一. VOFA+介绍

如果刚接触或者还不了解。这里先丢个官网链接:VOFA+ (゜-゜)つロ干杯
附上我参考的Up主视频,详细介绍了数据协议,主要控件使用及代码编写,十分推荐: STM32F4使用DMA串口通信连接VOFA软件进行电机调试和波形显示

二. 数据协议与收发操作

1. RawData

让我们看看官网是怎么描述的。

RawData协议适用于不需要解析数据,仅仅查看字节流的需求。
如果您只把VOFA+当成串口调试助手,不做任何采样数据和图片解析,请务必使用本协议。

也就是说,在这种数据格式下,VOFA+就是个常见的串口助手,不多赘述。
在这里插入图片描述
值得一提得是上图中的New Tab就是我们用来放控件的地方,旁边的数据栏是我们根据数据协议发送数据时自动读取的。接下来看看官方的两种数据协议。

2. FireWater

编程像printf简单。但由于字符串解析消耗更多的运算资源,建议仅在通道数量不多、发送频率不高的时候使用。
重点: FireWater遇到换行才会打印数据,很多新用户在这里产生疑惑。

要调用它,就如官网所言,只需要在串口重定向后,直接打印即可。

	printf("鼠标悬停在上面官网链接,有大恐怖\n");

记得选择好数据引擎,再点击左侧栏最上方小圆点打开串口,下方串口接收栏成功显示。(请无视右侧数据栏的2000)
在这里插入图片描述
说起来这个单词是烈酒的意思,很符合软件名称。

3. JustFloat

既然FireWater因为字符串会消耗更多资源,如果我们想要更快的传输速度,可以试试 换上主频更高的芯片。 我是说换上JustFloat这种纯浮点数协议。

本协议是小端浮点数组形式的字节流协议,纯十六进制浮点传输,节省带宽。此协议非常适合用在通道数量多、发送频率高的时候。
重点:

  • 在51单片机中,浮点为大端,使用JustFloat需要调换一下字节序;
  • 字节接收区请勾选十六进制,以十六进制方式打印字符,否则只能打印乱码。

这个协议需是依靠数据格式(浮点数)和帧尾0x00, 0x00, 0x80, 0x7f作为识别。常规思路是用memcpy()将帧尾数据复制给用户数据最后输出。不过我从B站Up主那偷学了一手,可以自定义一个union,一个地址可以存储不同类型的变量,在这里我定义了一个浮点数和一个4成员的数组。这在后面使用按键控件配置时也有妙用。

	typedef union USART_Control													//联合体,同时具备uint_8和浮点数属性
	{
		float float_data;
		uint8_t char_table[4];
	}USARTControl_Typedef;

	USARTControl_Typedef Tx_Packet[4];				//定义发送数据包数组,联合体,包含4个浮点数,根据自己需求来,预留一位给帧尾。
	Tx_Packet[3].char_table[0] = 0x00;						//vofa中justfloat解析数据格式,帧尾
	Tx_Packet[3].char_table[1] = 0x00;
	Tx_Packet[3].char_table[2] = 0x80;
	Tx_Packet[3].char_table[3] = 0x7f;
	//在main函数中直接输出。	
	u_test[0] = 1.414;
	u_test[1] = 1.732;
	u_test[2] = 1.14514;
	USART_DMA_Send(u_test,4);

如图,下方串口打印栏和右侧数据栏中也显示出了我要的数据,显示三位是因为我设置的全局3位。
在这里插入图片描述

三. 控件使用

接下来就是VOFA+最让人亮眼的控件功能。用VOFA+不体验它的图形控件,就像玩游戏不买通行证,虽然也能用,却少了许多体验。与游戏通行证不同的是,它免费又护肝。

1. 波形图

在JustFloat那已经出现了波形图控件,怎么操作的呢?
如图示:点击控件栏,波形图控件映入眼帘。长按拖动到New Tab窗口。右键之后可以看到各种选项。首先选择填充之后第一个选项将波形图摆好位置。其他参数可以自行摸索,并不复杂。
在这里插入图片描述在这里插入图片描述
插播一下,如果数据没有成功发送,先用RawData测试是否正常发送,再检验数据发送格式是否正确,能否正常打印数据。这里先附上我的代码,可以自行测试下。
在数据成功发送之后,只要你是JustFloat或者FireWater类型,右侧数据栏会按顺序自动显示数据。单击数据,可以修改各种参数。而在波形图中选择Y轴就可以选择自己想要显示的数据。

我以生成马鞍波为例,看看显示效果。

在这里插入图片描述
Perfect,可以鼠标左键拖拽波形,滚轮缩放Y轴,Auto下方滑条拉动控制X轴缩放。至于Auto键,和学校示波器的包浆Auto键一样,没事就用但不堪大用 。望着这样直观的图形,验算法的软件男,没钱的硬件男,掉头发的调参狗都在掉小珍珠😭
在一个窗口可以拖拽多个组件,所以我们可以同时显示多个波形图独立显示数据,也可以结合其他组件。当然也可以在多建窗口,形成自己的风格化工作区。

2. 按键与滑条

如果说波形图是接收数据的图形化显示,那么按键与滑条就是发送数据的图形化操作。想一想,有着花里胡哨交互的上位机,底层不也是这般吗?
先拖动一个Button控件,注意三个Button的区别,我以切换按键为例(ExtraButton Toggle)并进行了事件与参数设置00和01,用于后续替代命令中的占位符:
在这里插入图片描述
注意参数不要多输少输,软件会完全照搬,以防后续判断出现问题

然后可以新建一个命令用于发送给下位机,包括了名称Butt on,数据类型Hex,发送内容AA FF 00 01 %% 00 00 00。简单分析下这么做的意图?

  • 名称不必多言,言简意赅,结合分组工具有助于我们的工作。
  • 数据类型可以是Hex或者字符串,使用字符串一目了然指令意义,发送各种类型的数据。不过我还是倾向于用Hex,代码也会清爽很多。懒得处理字符串
  • 发送内容是我们自定义的数据协议,你可以随意定义。比如我在这里就使用AAFF作为帧头判断,01作为模式选择1,00作为占位符,凑满四个字节。因为我是用串口+DMA,DMA数据接受为定长。后面四位就是我们的发送内容。

这个命令就是我们会向单片机发送的内容,如果不绑定指令,浮点数模式会发送名称:参数的字符串,十六进制会直接发送参数内容,点一点就知道了。
在这里插入图片描述在这里插入图片描述

最后别忘了绑定命令。
在这里插入图片描述
点击按键后串口栏也发送了我们定义的数据协议。Keil中可以看到Rx_Packet成员的变换。
在这里插入图片描述

还记得我们之前定义的union吗?你可以定义一个这样类型的RxPacket接受你发送的整数和浮点数。来测试一下用整形发送,浮点数会怎么显示。
直接在发送区发送AA FF 00 01 23 3B 37 41,看看会发生什么?
Rx_Packet的float_data显示了11.4519377。和我想要的11.4514还是有误差的,很遗憾,这是HEX格式的局限,你可以根据精度需求决定是否这样做,或者采用字符串的判断方式。

在这里插入图片描述

滑条的操作和按键的很相似,选好触发方式和模式,再绑定好命令就可以了。

在这里插入图片描述
效果不错
在这里插入图片描述

四. 代码

可能有人想直接用代码测试下,我丢一个我写的便宜代码。我用的是STM32F103的USART3+DMA收发数据,波特率460800,可以通过宏修改引脚定义和波特率。文件基于江科大的Serial文件,记得勾选MicroLIB库以启用printf函数。
收发数据包皆为自定义的union,内部定义了一个浮点数和一个四字节数组。指令判断均依靠十六进制,没有使用字符串相关的功能与函数, ,点子王可以添加新功能。

#ifndef __SERIAL_H
#define __SERIAL_H

#define USART_NUMBER				USART3									//串口号宏定义	USART1			PA9 TX			PA10 RX
#define USART_DR					((uint32_t)&(USART_NUMBER->DR))			//串口号宏定义					DMA1_Channel4	DMA1_Channel5
#define USART_IRQn					USART3_IRQn								//串口中断号宏定义,未用上

#define USART_DMA_RX_CHANNEL		DMA1_Channel3							//DMA接收通道号,根据串口号决定
#define USART_DMA_TX_CHANNEL		DMA1_Channel2							//DMA发送通道号,根据串口号决定

#define USART_DMA_RX_IRQn			DMA1_Channel3_IRQn						//DMA接收中断号,根据串口号决定
#define USART_DMA_TX_IRQn			DMA1_Channel2_IRQn						//DMA发送中断号,根据串口号决定

#define DMA_Channel_RX_IRQHandler	DMA1_Channel3_IRQHandler				//DMA接收中断函数,根据串口号决定
#define DMA_Channel_TX_IRQHandler	DMA1_Channel2_IRQHandler				//DMA发送中断函数,根据串口号决定

#define USART_DMA_RX_FLG			DMA1_IT_TC3								//DMA接收完成标志位
#define USART_DMA_TX_FLG			DMA1_IT_TC2								//DMA发送完成标志位

#define USART_RX_PIN				GPIO_Pin_11								//串口发送引脚号,根据串口号决定
#define USART_RX_GPIO_PORT			GPIOB									//串口发送引脚号,根据串口号决定

#define USART_TX_PIN				GPIO_Pin_10								//串口接收引脚号,根据串口号决定
#define USART_TX_GPIO_PORT			GPIOB									//串口接收端口,根据串口号决定

#define BAUDRATE					460800									//波特率,STM32F103最高支持4.5M

#include "stm32f10x.h"                  // Device header
#include <stdio.h>

typedef union USART_Control													//联合体,同时具备uint_8和浮点数属性
{
	float float_data;
	uint8_t char_table[4];
}USARTControl_Typedef;

extern USARTControl_Typedef Tx_Packet[4];

void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void USART_DMA_Send(float *buffer, uint16_t len);

#endif

#include "Serial.h"

USARTControl_Typedef Tx_Packet[4];				//定义发送数据包数组,联合体,包含4个浮点数
USARTControl_Typedef Rx_Packet[2];				//定义接收数据包数组
uint8_t Tx_Flag;								//DMA发送数据完成标志位,同步,防止CPU覆盖缓冲区

void Serial_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);	//开启USART3的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	//开启GPIOB的时钟

	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);		//开启DMA1的时钟
	
	GPIO_InitTypeDef GPIO_InitStructure;					//定义结构体变量
	USART_InitTypeDef USART_InitStructure;					
	NVIC_InitTypeDef NVIC_InitStructure;				
	DMA_InitTypeDef DMA_InitStructure;
	
	Tx_Packet[3].char_table[0] = 0x00;						//vofa中justfloat解析数据格式
	Tx_Packet[3].char_table[1] = 0x00;
	Tx_Packet[3].char_table[2] = 0x80;
	Tx_Packet[3].char_table[3] = 0x7f;
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = USART_TX_PIN;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(USART_TX_GPIO_PORT, &GPIO_InitStructure);					//将PA9引脚初始化为复用推挽输出
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = USART_RX_PIN;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(USART_RX_GPIO_PORT, &GPIO_InitStructure);					//将PA10引脚初始化为上拉输入
	
	USART_InitStructure.USART_BaudRate = BAUDRATE;									//波特率
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;	//硬件流控制,不需要
	USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;					//模式,发送模式和接收模式均选择
	USART_InitStructure.USART_Parity = USART_Parity_No;								//奇偶校验,不需要
	USART_InitStructure.USART_StopBits = USART_StopBits_1;							//停止位,选择1位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;						//字长,选择8位
	USART_Init(USART_NUMBER, &USART_InitStructure);									//将结构体变量交给USART_Init,配置USART1
	
	USART_Cmd(USART_NUMBER,ENABLE);

	DMA_DeInit(USART_DMA_TX_CHANNEL);											//DMA发送通道配置  
	DMA_InitStructure.DMA_PeripheralBaseAddr = USART_DR;						//外设基地址,给定形参USART->DR
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;		//外设数据宽度,选择字节
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;			//外设地址自增,选择失能,始终以USART DR寄存器为地址
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Tx_Packet;					//存储器基地址,给定Tx_Send
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;				//存储器数据宽度,选择字节,与源数据宽度对应
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;						//存储器地址自增,选择使能,每次转运后,数组移到下一个位置
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;							//数据传输方向,选择由内存到外设
	DMA_InitStructure.DMA_BufferSize = 16;										//转运的数据大小(转运次数),4个浮点数为16字节
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;								//模式,选择普通模式,由软件进行重复转运
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;								//存储器到存储器,选择失能,数据由内存到串口
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;						//优先级,选择中等
	DMA_Init(USART_DMA_TX_CHANNEL, &DMA_InitStructure);							//将结构体变量交给DMA_Init,配置DMA1的相应通道

	DMA_ITConfig(USART_DMA_TX_CHANNEL,DMA_IT_TC,ENABLE);						//使能DMA发送中断,清除发送完成标志位并开启中断
	DMA_ClearFlag(USART_DMA_TX_FLG);
	DMA_Cmd(USART_DMA_TX_CHANNEL,ENABLE);

	DMA_DeInit(USART_DMA_RX_CHANNEL);											//DMA发送通道配置  
	DMA_InitStructure.DMA_PeripheralBaseAddr = USART_DR;						//外设基地址,给定形参USARc->DR
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;		//外设数据宽度,选择字节
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;			//外设地址自增,选择失能,始终以USART DR寄存器为地址
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)Rx_Packet;					//存储器基地址,给定Rx_Send
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;				//存储器数据宽度,选择字节,与源数据宽度对应
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;						//存储器地址自增,选择使能,每次转运后,数组移到下一个位置
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;							//数据传输方向,选择由串口到内存
	DMA_InitStructure.DMA_BufferSize = 8;										//转运的数据大小(转运次数),2个浮点数为8字节,表示相关指令
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;								//模式,选择普通模式,由软件进行重复转运
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;								//存储器到存储器,选择失能,数据由内存到串口
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;						//优先级,选择中等
	DMA_Init(USART_DMA_RX_CHANNEL, &DMA_InitStructure);							//将结构体变量交给DMA_Init,配置DMA1的相应通道
	
	DMA_ITConfig(USART_DMA_RX_CHANNEL,DMA_IT_TC,ENABLE);						//使能DMA接收中断,清除接收完成标志位并开启中断
	DMA_ClearFlag(USART_DMA_RX_FLG);
	DMA_Cmd(USART_DMA_RX_CHANNEL,ENABLE);
	

	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);					//配置NVIC为分组2	
	NVIC_InitStructure.NVIC_IRQChannel = USART_DMA_TX_IRQn;			//选择配置NVIC的USART_DMA_TX_IRQn线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;					//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;		//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;				//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);									//将结构体变量交给NVIC_Init,配置NVIC外设
	
	NVIC_InitStructure.NVIC_IRQChannel = USART_DMA_RX_IRQn;			//选择配置NVIC的USART_DMA_RX_IRQn线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;					//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;		//指定NVIC线路的抢占优先级为1
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;				//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);									//将结构体变量交给NVIC_Init,配置NVIC外设

	/*USART DMA使能*/
	USART_DMACmd(USART_NUMBER,USART_DMAReq_Tx | USART_DMAReq_Rx,ENABLE);
}

void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART_NUMBER, Byte);									//将字节数据写入数据寄存器,写入后USART自动生成时序波形
	while (USART_GetFlagStatus(USART_NUMBER, USART_FLAG_TXE) == RESET);	//等待发送完成
	/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}

int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);			//将printf的底层重定向到自己的发送字节函数
	return ch;
}

void USART_DMA_Send(float *buffer, uint16_t len)
{
	if(Tx_Flag==1)
	{
		Tx_Packet[0].float_data = buffer[0];						//DMA数据转运函数,将相关计算函数所得值传递进DMA缓冲区再发送至串口
		Tx_Packet[1].float_data = buffer[1];
		Tx_Packet[2].float_data = buffer[2];
		Tx_Flag=0;
		DMA_Cmd(USART_DMA_TX_CHANNEL,DISABLE);						//每次发送完成需要关闭DMA才能修改参数
		DMA_SetCurrDataCounter(USART_DMA_TX_CHANNEL, len*4);		//重新设置发送起始地址及长度,普通模式下,如此才能继续发送数据
		DMA_Cmd(USART_DMA_TX_CHANNEL,ENABLE);	
	}
}

void DMA_Channel_TX_IRQHandler(void)								//DMA发送完成中断
{
	if(DMA_GetITStatus(USART_DMA_TX_FLG) != RESET)					//检测发送完成标志位是否挂起,并清除相关中断标志位和同步位
	{
		DMA_ClearITPendingBit(USART_DMA_TX_FLG);
		Tx_Flag=1;
	}
}

void DMA_Channel_RX_IRQHandler(void)								//DMA接收完成中断
{
	if(DMA_GetITStatus(USART_DMA_RX_FLG) != RESET)
	{
		DMA_ClearITPendingBit(USART_DMA_RX_FLG);					
		DMA_Cmd(USART_DMA_RX_CHANNEL,DISABLE);						//每次发送完成需要关闭DMA才能修改参数
		DMA_SetCurrDataCounter(USART_DMA_RX_CHANNEL, 8);			//重新设置发送起始地址及长度,普通模式下,如此才能继续接收数据
		DMA_Cmd(USART_DMA_RX_CHANNEL,ENABLE);
		if((Rx_Packet[0].char_table[0] == 0xAA) && (Rx_Packet[0].char_table[1] == 0xFF))		//判断接收指令,指令为8字节,帧头为AA FF 在vofa中需输入00占位第三个字节位置 
		{
			if(Rx_Packet[0].char_table[3] == 0x01)												//第四个字节位为指令位,区分启停,PID调参等功能
			{
				//空		
			}
		}				
	}
}

在main函数中进行测试

#include "stm32f10x.h"                  // Device header
#include "Serial.h"
#include "Delay.h"

float u_test[3];

int main(void)
{
	Serial_Init();	
	u_test[0] = 1.414;
	u_test[1] = 1.732;
	u_test[2] = 1.14514;
	while (1)
	{
//		printf("鼠标悬停在上面官网链接,有大恐怖\n");
		USART_DMA_Send(u_test,4);
		Delay_ms(1000);
	}
}

至此VOFA+的简单应用和使用心得就到这里了。由于学得不算深入,加上时间久远,难免会有疏漏错误。如有错误和疑问,也请提出来,多多交流,共同进步。


碎碎念:第一次写MD格式的文档,看着丰富的语法说明,有一种知道音效库限时免费的感觉。😄

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐