<PID调参>VOFA+实现实时PID调参 (附源码)

在该篇文章中,本人只做对VOFA+和stm32之间如何实现数据互传,通过VOFA+如何实时修改stm32内的参数,具体如何调PID的过程,日后有空会进行更新,也请关注等待。以下全是本人的个人见解,有更好的想法或者不懂的问题,也可在评论区提问,谢谢大家。

一、什么是VOFA+

VOFA+就是一款串口助手,它不仅能够实现基础的串口数据收发,还能实现数据绘图(包括直方图、FFT图),控件编辑,图像显示等功能。使用VOFA+,可以给我们平常的PID调参等调试带来方便。这是VOFA+官网链接,可以在点击超链接进行下载。

二、如何使用VOFA+

在进入讲解前,可以在VOFA+官方文档中先过一遍基本的界面操作。

1.打开VOFA+的界面

什么都没有,那个密钥也不用去激活,叉掉就行
在这里插入图片描述

2.串口协议配置

这里是串口协议和串口一些配置,配置那些跟着代码来就行,波特率9600,无流控,无校验位,8位数据位,1位停止位,端口号是根据自己stm32与电脑相连的端口号一致,端口号可以在设备管理器中查看到。
在这里插入图片描述
在这里插入图片描述

3.放置控件

a.放置波形图控件

将控件中的第一个拖出来拖到那个大大的窗口中,然后再双击边缘,即我花黑点的地方,就能够放大波形图控件了。当然你不想太大也可以直接就拖动边缘,任意大小。
在这里插入图片描述

b.放置参数控件

这个控件在控件里面的最下面,直接拉三个出来,我这里对应着PID的Kp,Ki,Kd。右击控件对控件进行修改名称和最大值最小值和步长。

在这里插入图片描述

调整完就类似于这样

在这里插入图片描述
这里的绑定命令需要我们对应的在命令的窗口书写命令,这个命令也就是这个控件在做改变数值或者其他事件时会触发的,这个事件对应上图的事件与参数里的几个。

3.命令配置

那命令是什么呢,这里我们可以理解成触发了事件(改变了参数),就执行了命令(发送更改后的参数给stm32),以下就是我对命令的配置。名称即为命令的名称,可以跟对应的控件相同名称。如我这里是Kp,即当我的Kp发生数值改变时,命令就会自动发送一个内容为 #P1=%f! 这里的%f即为你Kp的数值。对应的可以设置Ki,Kd的命令,发送内容分别为 #P2=%f! , #P3=%f! 。这里为什么有123,是因为后面要在stm32中提取该数,然后就可以分别VOFA+发送给stm32是对应哪个需要改变的变量了。而这里的一些#,P,=,! 即为数据包的帧头帧尾,不理解这个概念的可以去看看江科大的串口收发数据包那节网课,本人也是看完那节然后自己码出stm32的代码的。
在这里插入图片描述

命令配置完毕,我们就需要在参数控件右键绑定各个命令,Kp绑定Kp,Ki绑定Ki,Kd绑定Kd。
以上就是VOFA+相关的配置了,在书写代码之前,先保存好相关配置,不然下次打开VOFA+就得重新配置了。
在这里插入图片描述

三、编写代码思路

  • 1.通过数据包对VOFA+传来的数据进行接收。
  • 2.除去帧头帧尾将数据包内的传回的数据提取出来。
  • 3.将提取出来的数据进行换算,把几个十六进制数换成一个float类型的数。
  • 4.将换算后的数赋值给对应的变量。

如:前面设置的命令中的 #P1=%f!,分为了几部分:帧头 #P、变量的辨识id= 1、数据开始提取表识位 =、还有数据本身 %f、和提取结束标志位即帧尾

看完江科大串口数据包收发 就能够理解以上的东西了。

以下关于我对数值转换的想法:
假设VOFA+所改变后的数值为12.134,那VOFA+会以ASCII码的形式发送给stm32,所以如果发送了一个感叹号“!”,此时STM32接收到的将会是感叹号的ASCII码,十六进制就是0x21,当我们发送“#”,对应的就是0x23。当我们发送0时,其十六进制就是0x30,对应十进制就是48,所以在对获取到的数值进行计算时,需要将数据减去48,得到其真正的数。
所以,当VOFA+发送#P1=12.134时,stm32接收到的 十六进制数据包就是

 0x23 0x50 0x31 0x3D 0x31 0x32 0x2E 0x31 0x33 0x34 0x21
  #    P     1   =    1    2    .    1     3    4   !

那我们只需要当接收到 0x23,0x50后就知道数据来了,然后将下一位收起来,即0x31,方便后面对该位进行判断,才可知是哪个参数的接收值。判断等号,进入收集数据的状态,将数据存在数组直到受到0x21即感叹号,我们就能退出保存数据

那保存的数据如何处理?
比如上面的12.134
由以上数据包处理后的得到的数据本身={0x31,0x32,0x2E,0x31,0x33,0x34};
当我们读第一位,即高位时。
我们可以写出
遇到1时: Data =1
遇到2时: Data = 1*10+2
遇到小数点时 不做处理
遇到1时, Data = 12 + 1/10
遇到3时, Data = 12.1 + 3/100
遇到4时, Data = 12.13 + 4/1000

可以发现规律 当遇到小数点前,我们可以用Data = Data*10 + 数据本身[i] 这里可以做判断当没有遇到小数点前,就一直这样i++,循环出小数点前的数。

    if(dot_Flag==0)
    {
      if(Usart_RxPacket[i]==0x2E)//如果识别到小数点,则将dot_Flag置1
      {
        dot_Flag=1;
      }
      else//还没遇到小数点前的运算
      {
        Data = Data*10 + Usart_RxPacket[i]-48;
      }
    }

当遇到小数点后 Data = Data + 数据本身[i]/10的-n次方

    else//遇到小数点后的运算
    {
      Data = Data + Pow_invert(Usart_RxPacket[i]-48,dot_after_num);
      dot_after_num++;
    }

pow_invert函数其实就是一个以10为底的指数函数而已,即X*10的负n次方

float Pow_invert(uint8_t X,uint8_t n)//x除以n次10
{
  float result=X;
	while(n--)
	{
		result/=10;
	}
	return result;
}

这样得到的数据,我们只需要返回一开始得到的 标志 即 P后面的数字,和 换算下来的数据本身Data就好了,返回以上的参数可以封装成函数,方便main函数调用。

在pid那边参数获取时,可以在先判断P后面的数字是什么,对应的将Data赋值给p或i或d 。具体对P后面数字的获取我在以下程序源码已给出,有写注解。详情可以在源码中查看。代码内还有对负数的判断方法,其实无非就是判断数据包第一位是否是负号,如果是,就在最后返回的Data乘以-1即可。

四、程序源码

全部复制粘贴就能直接运行了,本人使用的是stm32f103C8T6。

1.串口Usart的c文件

#include "Usart.h"
#if 1
#pragma import(__use_no_semihosting)             
//标准库需要的支持函数                 
struct __FILE 
{ 
	int handle; 

}; 

FILE __stdout;       
//定义_sys_exit()以避免使用半主机模式    
void _sys_exit(int x) 
{ 
	x = x; 
} 
//重定义fputc函数 
int fputc(int ch, FILE *f)
{      
	while((USART1->SR&0X40)==0); 
	USART1->DR=(u8)ch;      
	return ch;
}
#endif 

uint8_t Usart_RxData;
uint8_t Usart_RxFlag;

uint8_t Usart_RxPacket[100];//接受数据包
uint8_t Usart_RxPacket_Len=100;//接受数据包长度

void Usart_Init(void)
{
	RCC_APB2PeriphClockCmd(USART_RCC,ENABLE);
	RCC_APB2PeriphClockCmd(USART_GPIO_RCC,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;//推挽输出,也就是发送口
	GPIO_InitStructure.GPIO_Pin=USART_TX;
	GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(USART_GPIO,&GPIO_InitStructure);
  
	GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;//上拉输入,接收端
	GPIO_InitStructure.GPIO_Pin=USART_RX;
	GPIO_Init(USART_GPIO,&GPIO_InitStructure);
  
	USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate=9600;//波特率
	USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None;//流控
	USART_InitStructure.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;
	USART_InitStructure.USART_Parity=USART_Parity_No;//校验位
	USART_InitStructure.USART_StopBits=USART_StopBits_1;
	USART_InitStructure.USART_WordLength=USART_WordLength_8b;
	USART_Init(USART_X,&USART_InitStructure);
	
  //开启串口中断
	USART_ITConfig(USART_X,USART_IT_RXNE,ENABLE);//打开接受中断
  //配置中断组
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	//配置中断
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel=USART_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
	NVIC_Init(&NVIC_InitStructure);
  
  USART_Cmd(USART_X,ENABLE);
	USART_ClearFlag(USART_X,USART_FLAG_TC);//清楚串口发送标志位

}

void Send_Byte(uint8_t Byte)//发送一个字节
{
	USART_SendData(USART_X,Byte);
	while(USART_GetFlagStatus(USART_X,USART_FLAG_TXE)==RESET);
}

void Send_Array(uint8_t *Array,uint16_t Length)//发送一个数组
{
	uint16_t i;
	for(i=0;i<Length;i++)
	{
		Send_Byte(Array[i]);
	}
}

void Send_String(char*String)//发送一个字符串
{
	while(*String)
	{
		Send_Byte(*String++);
	}
}

uint32_t Pow(uint32_t X,uint32_t Y)
{
	uint32_t result=1;
	while(Y--)
	{
		result*=X;
	}
	return result;
}

void Send_Number(uint32_t Num,uint8_t Length)//发送数字
{
	uint8_t i;
	for(i=0;i<Length;i++)
	{
		Send_Byte(Num/Pow(10,Length-i-1)%10+'0');  
	}
}

void Usart_Printf(char *format,...)
{
	char String[100];
	va_list arg;
	va_start(arg,format);
	vsprintf(String,format,arg);
	va_end(arg);
	Send_String(String);
}

uint8_t Usart_GetRxFlag(void)
{
	if(Usart_RxFlag==1)
	{
		Usart_RxFlag=0;
		return 1;
	}
	return 0;
}

uint8_t Usart_GetRxData(void)
{
	return Usart_RxData;
}

uint8_t id_Flag;//1为Kp 2为Ki 3为Kd
uint8_t Data_BitNum=0;//数据的位数,即12.123 有6位 -12.123有7为

//串口中断,用于接受vofa的参数的   #P1=12.123!   #P为帧头,1为是改变谁的标志位, =是数据收集标志位
void USART1_IRQHandler(void)    //  12.123是数据本身  !是帧尾
{
	static uint8_t RxState=0;
	static uint8_t pRxPacket=0;
	if(USART_GetFlagStatus(USART_X,USART_IT_RXNE)==SET)
	{
		Usart_RxData=USART_ReceiveData(USART_X);
		if(RxState==0&&Usart_RxData==0x23) //第一个帧头  "#"==0x23
		{
			RxState=1;
		}
		else if(RxState==1&&Usart_RxData==0x50) //第二个帧头  "P"==0x50
		{
			RxState=2;
		}
		else if(RxState==2)//确认传参的对象 即修改id_Flag
		{	
			id_Flag=Usart_RxData-48;
			RxState=3;
		}
		else if(RxState==3&&Usart_RxData==0x3D) //判断等号,也可以类比为数据开始的帧头
		{
			RxState=4;
		}
    else if(RxState==4)//开始接收传输的数据
		{	
      if(Usart_RxData==0x21)//结束的帧尾   如果没有接收到!即还有数据来,就一直接收
			{
        Data_BitNum=pRxPacket;//获取位数
        pRxPacket=0;//清除索引方便下次进行接收数据
        RxState=0;
        Usart_RxFlag=1;
			}
      else
      {
        Usart_RxPacket[pRxPacket++]=Usart_RxData;//把数据放在数据包内
      }
		}
		USART_ClearITPendingBit(USART_X,USART_IT_RXNE);
	}
}

uint8_t Get_id_Flag(void)//将获取id_Flag封装成函数
{
  uint8_t id_temp;
  id_temp=id_Flag;
  id_Flag=0;
  return id_temp;
}

float Pow_invert(uint8_t X,uint8_t n)//x除以n次10
{
  float result=X;
	while(n--)
	{
		result/=10;
	}
	return result;
}

//uint8_t Usart_RxPacket[5]={0x31,0x32,0x2E,0x31,0x33};//可以给数据包直接赋值直接调用一下换算程序,看是否输出为12.13
//Data_BitNum = 5//别忘记数据的长度也要设置
//然后直接在主程序就放  Usart_Printf("%f\n",RxPacket_Data_Handle());  Delay_ms(1000);就ok了
float RxPacket_Data_Handle(void)//数据包换算处理
{
  float Data=0.0;
  uint8_t dot_Flag=0;//小数点标志位,能区分小数点后或小数点前 0为小数点前,1为小数点后
  uint8_t dot_after_num=1;//小数点后的第几位
  int8_t minus_Flag=1;// 负号标志位 -1为是负号 1为正号
  for(uint8_t i=0;i<Data_BitNum;i++)
  {
    if(Usart_RxPacket[i]==0x2D)//如果第一位为负号
    {
      minus_Flag=-1;
      continue;//跳过本次循环 
    }
    if(dot_Flag==0)
    {
      if(Usart_RxPacket[i]==0x2E)//如果识别到小数点,则将dot_Flag置1
      {
        dot_Flag=1;
      }
      else//还没遇到小数点前的运算
      {
        Data = Data*10 + Usart_RxPacket[i]-48;
      }
    }
    else//遇到小数点后的运算
    {
      Data = Data + Pow_invert(Usart_RxPacket[i]-48,dot_after_num);
      dot_after_num++;
    }
  }
  return Data*minus_Flag;//将换算后的数据返回出来 这里乘上负号标志位
}

2.串口Usart的h文件

能够直接改变头文件内的宏定义,更换使用的Usart。注意: Usart1是挂载在APB2上的外设,Usart2和Usart3均在APB1上,在修改代码时注意更换。

#ifndef __USART_H
#define __USART_H
#include "stm32f10x.h"                  // Device header
#include <stdarg.h>
#include <stdio.h>

#define USART_RCC  RCC_APB2Periph_USART1
#define USART_GPIO_RCC RCC_APB2Periph_GPIOA
#define USART_GPIO GPIOA
#define USART_X    USART1
#define USART_IRQn USART1_IRQn
#define USART_IRQHandler USART1_IRQHandler
#define USART_TX   GPIO_Pin_9
#define USART_RX   GPIO_Pin_10

void Usart_Init(void);
void Send_Byte(uint8_t Byte);
void Send_Array(uint8_t *Array,uint16_t Length);
void Send_String(char*String);
void Send_Number(uint32_t Num,uint8_t Length);
void Usart_Printf(char *format,...);
uint8_t Usart_GetRxFlag(void);
uint8_t Usart_GetRxData(void);

uint8_t Get_id_Flag(void);//将获取id_Flag封装成函数
float RxPacket_Data_Handle(void);//数据包换算处理
#endif

3.main.c文件

这里仅仅简单的实现修改数值,具体要进行pid调参就根据代码修改一下即可,大差不差的。

#include "stm32f10x.h"                  // Device header
#include "Usart.h"
#include "OLED.h"
#include "Delay.h"


float PID_K[3]={1.0,1.0,1.0};//Kp,Ki,Kd
uint8_t PID_index=0;
int main(void)
{
  OLED_Init();
  Usart_Init();
	while(1)
	{
    if(Usart_GetRxFlag())
    {
      PID_index=Get_id_Flag();
      PID_K[PID_index-1]=RxPacket_Data_Handle();
    }
    Usart_Printf("%.3f,%.3f,%.3f\n",PID_K[0],PID_K[1],PID_K[2]);
    OLED_ShowNum(1,1,PID_index,1);
    Delay_ms(2);
  }
}

五、结语

以上是我对PID可视化调参的一些小技巧,也是希望能够帮助到 像我一样对于边改pid参数,边烧录程序的繁杂过程而有心无力的人,如果这篇文章有帮助到你,也请您能够给我个关注,谢谢!

  • 92
    点赞
  • 299
    收藏
    觉得还不错? 一键收藏
  • 40
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 40
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值