VOFA+使用心得_串口显示波形及可视化调节参数【附代码】
如果刚接触或者还不了解。VOFA+ (゜-゜)つロ干杯附上我参考的Up主视频,详细介绍了数据协议,主要控件使用及代码编写,十分推荐:STM32F4使用DMA串口通信连接VOFA软件进行电机调试和波形显示。
之前在学习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格式的文档,看着丰富的语法说明,有一种知道音效库限时免费的感觉。😄
更多推荐



所有评论(0)