赞
踩
在该篇文章中,本人只做对VOFA+和stm32之间如何实现数据互传,通过VOFA+如何实时修改stm32内的参数,具体如何调PID的过程,日后有空会进行更新,也请关注等待。以下全是本人的个人见解,有更好的想法或者不懂的问题,也可在评论区提问,谢谢大家。
VOFA+就是一款串口助手,它不仅能够实现基础的串口数据收发,还能实现数据绘图(包括直方图、FFT图),控件编辑,图像显示等功能。使用VOFA+,可以给我们平常的PID调参等调试带来方便。这是VOFA+官网链接,可以在点击超链接进行下载。
在进入讲解前,可以在VOFA+官方文档中先过一遍基本的界面操作。
什么都没有,那个密钥也不用去激活,叉掉就行
这里是串口协议和串口一些配置,配置那些跟着代码来就行,波特率9600,无流控,无校验位,8位数据位,1位停止位,端口号是根据自己stm32与电脑相连的端口号一致,端口号可以在设备管理器中查看到。
将控件中的第一个拖出来拖到那个大大的窗口中,然后再双击边缘,即我花黑点的地方,就能够放大波形图控件了。当然你不想太大也可以直接就拖动边缘,任意大小。
这个控件在控件里面的最下面,直接拉三个出来,我这里对应着PID的Kp,Ki,Kd。右击控件对控件进行修改名称和最大值最小值和步长。
调整完就类似于这样
这里的绑定命令需要我们对应的在命令的窗口书写命令,这个命令也就是这个控件在做改变数值或者其他事件时会触发的,这个事件对应上图的事件与参数里的几个。
那命令是什么呢,这里我们可以理解成触发了事件(改变了参数),就执行了命令(发送更改后的参数给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+就得重新配置了。
如:前面设置的命令中的 #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。
#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;//将换算后的数据返回出来 这里乘上负号标志位 }
能够直接改变头文件内的宏定义,更换使用的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
这里仅仅简单的实现修改数值,具体要进行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参数,边烧录程序的繁杂过程而有心无力的人,如果这篇文章有帮助到你,也请您能够给我个关注,谢谢!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。