一、vofa+上位机介绍

         vofa+是一款面向嵌入式开发领域的上位机软件,它支持Windows/Linux/macOS平台,提供串口(超高波特率稳定支持)、网口(TCP/UDP)等通信接口,核心功能是通过图形化交互实现传感器数据实时可视化与分析。

        viofa+采用插件驱动架构,协议与控件开源均可自定义,内置三大协议:

  1. FireWater:类似printf的CSV字符串协议(如下所示)
    "1.23,4.56\n"
    编程简单但是资源消耗较高,适合低速场景。
  2. JustWater:小端浮点数组+帧尾的二进制协议(如0x00,0x00,0x80,0x7F),高效低开销,推荐用于MCU多通道高速数据传输
  3. RawData:原始字节流模式,兼容传统串口助手功能。

        其图形控件库支持拖拽式添加波形图、3D模型(可导入STL文件)、按钮、滑块等。在侵入式调试中,尤其适合PID调参、飞控姿态监控、实时数据监控等场景,大幅度提升开发效率。本次重点内容便是通过滑块的模式改变PID的参数,是PID调参更加快捷。

        vofa+完全开源免费,下载的官网为:VOFA-Plus上位机 | VOFA-Plus上位机

二、使用vofa+调整PID参数

2.1vofa+界面

       vofa+的初始化界面如下所示:

        vofa+的界面大致可以分为三部分:协议与连接,图形显示界面和串口调试界面。可以根据自己的需要选择对应数据引擎,数据接口一般选择串口模式,串口参数配置根据自己实际情况选择,波特率自己根据需要配置,本人选用的是115200bit/s,常使用的还有9600bit/s。图形显示界面默认为空的,串口调试界面与常用的串口调试助手使用方式一样。

        串口调试界面常用的命令如下图所示:

        左侧最下边的图表为控件,点开它,可以将控件图形移动到图形显示界面,本次需要的是波形图和最下变的slider,移动完后的图形显示界面如下所示:

        可以根据自己的需要更改对应的名称,本次将其修改为Kp,Ki,Kd。

2.2命令配置

        拖动滑条实现PID参数的调整,其本质就是每滑动一次,由上位机发送一个自定义的串口数据包,下位机进行数据包的解析,得到实时的Kp,Ki和Kd值,因此需要将这三个控件绑定好对应的命令格式,使其能够发送数据包。

        命令的绑定在左侧第三个绿色图表,点开后点击添加新的命令。

        双击命令可进入编辑模式,更改命令的名称为Kp,并且可以自定义发送的内容,连续发送参数选择默认的命令即可。下面详细讲述一下命令的发送指令数据包编写。

        串口的数据接收是按位接收,比如发送的是字符串“abc”,则串口接收的形式是按位接收字符‘a’,‘b’,‘c’,因此我们利用串口接收的特性,自定义数据包的格式如下:KP%fM,KI%fM,KD%fM。具体分析如下:

  • K是数据包的包头;
  • P,I,D则是标志位,用来判断具体是PID的哪一个参数;
  • %f则是对应的浮点型数据,也是真正需要赋值接收的参数值;
  • M是数据包的包尾,用来判断数据接收完毕

         

        将发送的内容写好之后,变可以将命令与对应的控件进行绑定,Kp绑定Kp,Ki绑定Ki,Kd绑定Kd。

        操作完成后,注意将自己所配置的控件内容进行保存,否则下次再次打开仍需重新配置。

2.3编写代码思路

        整体编写思路如下所示:

  • 对上位机发送的数据包进行接收;
  • 对数据包进行解析,除去包头和包尾,得到需要的数据内容;
  • 将字符串格式的数据通过库函数转换为浮点型数据;
  • 将得到的数据分别赋值给Kp,Ki和Kd。

        数据包的接收采用的是状态机的变成思路,定义两个状态,分别是接收的状态(用来指示接收数据到哪一步)和接收标识的状态(用来指示接收的是Kp,Ki还是Kd)

static enum{
	Wait_Head,//等待包头
	Wait_Flag,//等待接收标识
	Wait_Data //等待接收数据
}RxState=Wait_Head;//初始状态为等待包头

static enum{
    CMD_NONE, //空状态
	CMD_Kp,   //Kp
	CMD_Ki,   //Ki
	CMD_Kd    //Kd
}CurrentCmd=CMD_NONE;//初始为空状态

        状态机的实现流程图如下所示:   

        在代码编写中,需要关注一个C语言函数strtof,将字符串转换为单精度浮点数float,该函数的解析规则如下:

  • 跳过前导空白字符(空格、制表符等);
  • 识别符号​:支持 +正数,默认)或 -(负数)​;
  • 解析数字格式​:
  1. 十进制格式(如 123.456);
  2. 科学计数法(如1.23e4);
  3. 十六进制格式(如0x1F);
  • 遇到非法字符停止​:例如字母、非数字符号等;
uint8_t *endptr;//二级指针,用于返回转换结束位置(指向首个未转换字符的地址)
float NewValue = strtof((char*)RxPacket, (char**)&endptr);
//(char*)RxPacket:将uint8_t数组(原始数据缓冲区)转换为 char* 类型,符合nptr参数要求
//(char**)&endptr:将uint8_t*的地址转换为char**类型,使endptr能正确存储未转换字符的位置。

        (这一处函数如果不太清楚,直接拿过来使用即可)

          基于STM32F407VET6的串口中断回调函数编写:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{

  if(huart==&huart1)
  {
	static uint8_t RxData;            //定义接收的数据
	static uint8_t RxIndex;           //储存数据数组的索引值
	static uint8_t RxPacket[128];     //储存数据的数组
	static enum{
		Wait_Head, //等待包头
		Wait_Flag, //等待接收标识
		Wait_Data  //等待接收数据
	}RxState=Wait_Head;//初始状态为等待包头
    static enum{
		CMD_NONE,  //空状态
		CMD_Kp,    //Kp
		CMD_Ki,    //Ki
		CMD_Kd     //Kd
	}CurrentCmd=CMD_NONE;//初始为空状态
	switch(RxState)//使用switch语句使得结构更加清晰
	{
		case Wait_Head:
			if(RxData=='K')//接收到包头‘K’
			{
				RxState=Wait_Flag;//跳转转态至等待接收标识
			}
			break;
		case Wait_Flag:
			if(RxData=='P')//接收到标识‘P’
			{
				CurrentCmd=CMD_Kp;
				RxState=Wait_Data;
				RxIndex=0;//数组清零,便于下一次开始接受实际数据内容
			}
			else if(RxData=='I')//接收到标识‘I’
			{
				CurrentCmd=CMD_Ki;
				RxState=Wait_Data;
				RxIndex=0;
			}
			else if(RxData=='D')//接收到标识‘D’
			{
				CurrentCmd=CMD_Kd;
				RxState=Wait_Data;
				RxIndex=0;
			}
			else//均为接收到,则重置状态
				RxState=Wait_Head;
			break;
		case Wait_Data:
			if(RxData=='M')//接收到包尾‘M’
		  {
				RxPacket[RxIndex]='\0';//为接收的数组添加上结束符‘\0’
				uint8_t *endptr;
				float NewValue = strtof((char*)RxPacket, (char**)&endptr);//双重转换
				// 验证转换有效性
                if(endptr!=RxPacket && *endptr == '\0') 
				{
					switch(CurrentCmd)//判断接收标识符
					{
						case CMD_Kp:
							PID_K[0]=NewValue;//PID_K[0]=Kp
							printf("Kp updated:%.2f\n",PID_K[0]);
                            break;
						case CMD_Ki:
							PID_K[1]=NewValue;//PID_K[0]=Ki
						    printf("Ki updated:%.2f\n",PID_K[1]);
						    break;
						case CMD_Kd:
							PID_K[2]=NewValue;//PID_K[0]=Kd
							printf("Kd updated:%.2f\n",PID_K[2]);
						    break;
						case CMD_NONE:
							break;
					}
				}
				else
				    printf("Error:%s\n",RxPacket);//接收错误
				RxState=Wait_Head;
				CurrentCmd=CMD_NONE;//状态重置
			}
			else
			{
				if(RxIndex<sizeof(RxPacket)-1)//判断数组是否越界
				{
					RxPacket[RxIndex++]=RxData;//将接收到的数据存储至数组中
				}
				else
				{
					RxState=Wait_Head;//溢出复位
				}
			}
			break;
		default:
			RxState=Wait_Head;
			break;
		    
	}
    HAL_UART_Receive_IT(&huart1,&RxData,1);//重启串口接收
  }
}

        注意事项:

        1、RxData,RxIndex,RxPacket设置为静态变量,使其仍可保存之前存储的数据。

        2、本代码在串口中断中使用printf函数,可能会占用内存资源,可以在设置输出标志位在主函数中进行输出。

        3、在实际调整参数中,调到合适的值之后,需要对原程序里的Kp,Ki,Kd初始值赋给自己所调好的值,否则每次烧录初始化,Kp,Ki和Kd的值还是之前设置的初始化未调好的值。

        在USART函数中需要对printf函数进行重定向:

int fputc(int ch, FILE *f)
{
    while ((USART1->SR & 0X40) == 0);                                       

    USART1->DR = (uint8_t)ch;                                               
    return ch;
}

        完整代码:

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2025 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "adc.h"
#include "dma.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */

#include "stm32f4xx_it.h"
#include "tim.h"
#include "stdio.h"
#include "OLED.h"
#include "pid.h"
#include "stdlib.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

uint8_t RxData;
float PID_K[3]={1.0,1.0,1.0};//Kp,Ki,Kd
/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_TIM1_Init();
  MX_ADC1_Init();
  MX_TIM2_Init();
  MX_TIM3_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  OLED_Init();
  HAL_UART_Receive_IT(&huart1,&RxData,1);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
	  OLED_ShowFloatNum(0,0,PID_K[0],2,3,OLED_8X16);
	  OLED_ShowFloatNum(0,16,PID_K[1],2,3,OLED_8X16);
	  OLED_ShowFloatNum(0,32,PID_K[2],2,3,OLED_8X16);
	  OLED_Update();
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Configure the main internal regulator output voltage
  */
  __HAL_RCC_PWR_CLK_ENABLE();
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI;
  RCC_OscInitStruct.PLL.PLLM = 8;
  RCC_OscInitStruct.PLL.PLLN = 168;
  RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
  RCC_OscInitStruct.PLL.PLLQ = 4;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */






void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{

  if(huart==&huart1)
  {
	static uint8_t RxData;
	static uint8_t RxIndex;
	static uint8_t RxPacket[128];
	static enum{
		Wait_Head, //等待包头
		Wait_Flag, //等待接收标识
		Wait_Data  //等待接收数据
	}RxState=Wait_Head;//初始状态为等待包头
    static enum{
		CMD_NONE,  //空状态
		CMD_Kp,    //Kp
		CMD_Ki,    //Ki
		CMD_Kd     //Kd
	}CurrentCmd=CMD_NONE; //初始为空状态
	switch(RxState)
	{
		case Wait_Head:
			if(RxData=='K')
			{
				RxState=Wait_Flag;
			}
			break;
		case Wait_Flag:
			if(RxData=='P')
			{
				CurrentCmd=CMD_Kp;
				RxState=Wait_Data;
				RxIndex=0;
			}
			else if(RxData=='I')
			{
				CurrentCmd=CMD_Ki;
				RxState=Wait_Data;
				RxIndex=0;
			}
			else if(RxData=='D')
			{
				CurrentCmd=CMD_Kd;
				RxState=Wait_Data;
				RxIndex=0;
			}
			else
				RxState=Wait_Head;
			break;
		case Wait_Data:
			if(RxData=='M')
		  {
				RxPacket[RxIndex]='\0';
				uint8_t *endptr;
				float NewValue = strtof((char*)RxPacket, (char**)&endptr);  // 双重转换
				// 验证转换有效性
                if(endptr!=RxPacket && *endptr == '\0') 
				{
					switch(CurrentCmd)
					{
						case CMD_Kp:
							PID_K[0]=NewValue;
							printf("Kp updated:%.2f\n",PID_K[0]);
                            break;
						case CMD_Ki:
							PID_K[1]=NewValue;
						    printf("Ki updated:%.2f\n",PID_K[1]);
						    break;
						case CMD_Kd:
							PID_K[2]=NewValue;
							printf("Kd updated:%.2f\n",PID_K[2]);
						    break;
						case CMD_NONE:
							break;
					}
				}
				else
					printf("Error:%s\n",RxPacket);
				RxState=Wait_Head;
				CurrentCmd=CMD_NONE;
			}
			else
			{
				if(RxIndex<sizeof(RxPacket)-1)
				{
					RxPacket[RxIndex++]=RxData;
				}
				else
				{
					RxState=Wait_Head;
				}
			}
			break;
		default:
			RxState=Wait_Head;
			break;
		    
	}
    HAL_UART_Receive_IT(&huart1,&RxData,1);
  }
}


/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

        

三、功能展示

        

        OLED屏实时显示

        

        演示视频

vofa+上位机调PID

        四、总结

             如果搭配上无线link,便可实现无线模式调参。尤其对于小车这种运动系统而言,可以实时调整PID参数,方便快捷。这也是本人第一次写博客,里面有些内容理解可能不足,请批评指正,谢谢!

Logo

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

更多推荐