当前位置:   article > 正文

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

vofa

在该篇文章中,本人只做对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   !
  • 1
  • 2

那我们只需要当接收到 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;
      }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

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

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

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

这样得到的数据,我们只需要返回一开始得到的 标志 即 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;//将换算后的数据返回出来 这里乘上负号标志位
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239

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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

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);
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

五、结语

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/236607
推荐阅读
相关标签
  

闽ICP备14008679号