74人参与 • 2024-08-06 • stm32
前面我们已经能够通过编码器测量出速度值,下面我们来控制速度
我们先编写一个简单的控制方法
要求:讲转速控制再2.9-3.1转每秒
可以把中断里面不重要的输出注释掉
if(motor1speed>3.1) motor1pwm--;
if(motor1speed<2.9) motor1pwm++;
if(motor2speed>3.1) motor2pwm--;
if(motor2speed<2.9) motor2pwm++;
motor_set(motor1pwm,motor2pwm);
printf("motor1speed:%.2f motor1pwm:%d\r\n",motor1speed,motor1pwm);
printf("motor2speed:%.2f motor2pwm:%d\r\n",motor2speed,motor2pwm);
hal_delay(100);
开始实验
现象就开始电机没有到达3转每秒,pwm占空比逐渐增大,电机逐渐达到要求转速、到达要求转速后我们增加阻力,电机变慢,阻力大小不边pwm占空比逐渐更大转速逐渐更大。
这样我们就把转速控制到我们想要的范围,但是我们并不满意、能够看出来控制的速度很慢,给电机一些阻力电机至少要2-3秒能够调整过来,这在一些场景是不允许的。
我们理想的控制效果是:在电机转速很慢的是时候能快速调整,在电机一直转的不能达到要求时候能够更快速度调整
为了方便观察电机速度数据,我们通过上位机曲线显示一下。
这里我们使用的上位机是匿名上位机-大佬写的非常稳定功能也很多
我使用的版本是:匿名上位机v7.2.2.8版本推荐大家和我使用一样
匿名上位机官方下载链接:https://www.anotc.com/wiki/%e5%8c%bf%e5%90%8d%e4%ba%a7%e5%93%81%e8%b5%84%e6%96%99/%e8%b5%84%e6%96%99%e4%b8%8b%e8%bd%bd%e9%93%be%e6%8e%a5%e6%b1%87%e6%80%bb
我们要把stm32数据发送到匿名上位机,就要满足匿名上位机的数据协议要求
在匿名上位机资料下载链接,可以下载到协议介绍
匿名上位机v7通信协议,20210528发布:https://pan.baidu.com/s/1ngrigwj6qr9dwocgpkr51g 提取码:z8d1
csdn 大佬写的协议解析教程博客:
1.先补充一下大小端模式
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8bit。但是在c语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如和将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。例如一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。
所谓的大端模式(be big-endian),是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中(低对高,高对低);
所谓的小端模式(le little-endian),是指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中(低对低,高对高)。
常见的单片机大小端模式:(1)keil c51中,变量都是大端模式的,而keil mdk中,变量是小端模式的。(2)sdcc-c51是小端寻址,avrgcc 小端寻址.(3)pc小端,大部分arm是小端 (4)总起来说51单片机一般是大端模式,32单片机一般是小端模式.
2.看一下上位机要求的协议
灵活格式帧(用户自定义帧)
前面我们好理解
0xaa:一个字节表示开始
0xff:一个字节表示目标地址
0xf1:一个字节表示发送功能码
1-40:一个字节表示数据长度
数据内容有多个字节如何发送
因为串口每次发送一个字节,但是数据可能是int16_t 16位的数据,或者int32_t 32位数据,每次发送16位数据,先发送数据低八位,还是先发送数据高八位那?
匿名协议通信介绍给出:data 数据内容中的数据,采用小端模式传送,低字节在前,高字节在后。
那么就要求,比如我们在发送16位数据0x2314我们要先发送低字节0x14,然后发送高字节0x23
那么如何解析出低字节或者高字节,就需要知道多字节数据在单片机里面是怎么存的,因为stm32是小端存储,所以低字节就在低位地址中,高字节高位地址中。
如果使用32单片机 小端模式,0x23高地址,0x14在低地址,所以我们要先发低地址,再发高地址。
下面就是对16位数据,或者32位数据的拆分
//需要发送16位,32位数据,对数据拆分,之后每次发送单个字节
//拆分过程:对变量dwtemp 去地址然后将其转化成char类型指针,最后再取出指针所指向的内容
#define byte0(dwtemp) (*(char *)(&dwtemp))
#define byte1(dwtemp) (*((char *)(&dwtemp) + 1))
#define byte2(dwtemp) (*((char *)(&dwtemp) + 2))
#define byte3(dwtemp) (*((char *)(&dwtemp) + 3))
拆分后我们按照协议要求发送数据就可以了
niming.c
#include "niming.h"
#include "main.h"
#include "usart.h"
uint8_t data_to_send[100];
//通过f1帧发送4个uint16类型的数据
void ano_dt_send_f1(uint16_t _a, uint16_t _b, uint16_t _c, uint16_t _d)
{
uint8_t _cnt = 0; //计数值
uint8_t sumcheck = 0; //和校验
uint8_t addcheck = 0; //附加和校验
uint8_t i = 0;
data_to_send[_cnt++] = 0xaa;//帧头
data_to_send[_cnt++] = 0xff;//目标地址
data_to_send[_cnt++] = 0xf1;//功能码
data_to_send[_cnt++] = 8; //数据长度
//单片机为小端模式-低地址存放低位数据,匿名上位机要求先发低位数据,所以先发低地址
data_to_send[_cnt++] = byte0(_a);
data_to_send[_cnt++] = byte1(_a);
data_to_send[_cnt++] = byte0(_b);
data_to_send[_cnt++] = byte1(_b);
data_to_send[_cnt++] = byte0(_c);
data_to_send[_cnt++] = byte1(_c);
data_to_send[_cnt++] = byte0(_d);
data_to_send[_cnt++] = byte1(_d);
for ( i = 0; i < data_to_send[3]+4; i++)
{
sumcheck += data_to_send[i];//和校验
addcheck += sumcheck;//附加校验
}
data_to_send[_cnt++] = sumcheck;
data_to_send[_cnt++] = addcheck;
hal_uart_transmit(&huart1,data_to_send,_cnt,0xffff);//这里是串口发送函数
}
//,通过f2帧发送4个int16类型的数据
void ano_dt_send_f2(int16_t _a, int16_t _b, int16_t _c, int16_t _d) //f2帧 4个 int16 参数
{
uint8_t _cnt = 0;
uint8_t sumcheck = 0; //和校验
uint8_t addcheck = 0; //附加和校验
uint8_t i=0;
data_to_send[_cnt++] = 0xaa;
data_to_send[_cnt++] = 0xff;
data_to_send[_cnt++] = 0xf2;
data_to_send[_cnt++] = 8; //数据长度
//单片机为小端模式-低地址存放低位数据,匿名上位机要求先发低位数据,所以先发低地址
data_to_send[_cnt++] = byte0(_a);
data_to_send[_cnt++] = byte1(_a);
data_to_send[_cnt++] = byte0(_b);
data_to_send[_cnt++] = byte1(_b);
data_to_send[_cnt++] = byte0(_c);
data_to_send[_cnt++] = byte1(_c);
data_to_send[_cnt++] = byte0(_d);
data_to_send[_cnt++] = byte1(_d);
for ( i = 0; i < data_to_send[3]+4; i++)
{
sumcheck += data_to_send[i];
addcheck += sumcheck;
}
data_to_send[_cnt++] = sumcheck;
data_to_send[_cnt++] = addcheck;
hal_uart_transmit(&huart1,data_to_send,_cnt,0xffff);//这里是串口发送函数
}
//通过f3帧发送2个int16类型和1个int32类型的数据
void ano_dt_send_f3(int16_t _a, int16_t _b, int32_t _c ) //f3帧 2个 int16 参数 1个 int32 参数
{
uint8_t _cnt = 0;
uint8_t sumcheck = 0; //和校验
uint8_t addcheck = 0; //附加和校验
uint8_t i=0;
data_to_send[_cnt++] = 0xaa;
data_to_send[_cnt++] = 0xff;
data_to_send[_cnt++] = 0xf3;
data_to_send[_cnt++] = 8; //数据长度
//单片机为小端模式-低地址存放低位数据,匿名上位机要求先发低位数据,所以先发低地址
data_to_send[_cnt++] = byte0(_a);
data_to_send[_cnt++] = byte1(_a);
data_to_send[_cnt++] = byte0(_b);
data_to_send[_cnt++] = byte1(_b);
data_to_send[_cnt++] = byte0(_c);
data_to_send[_cnt++] = byte1(_c);
data_to_send[_cnt++] = byte2(_c);
data_to_send[_cnt++] = byte3(_c);
for ( i = 0; i < data_to_send[3]+4; i++)
{
sumcheck += data_to_send[i];
addcheck += sumcheck;
}
data_to_send[_cnt++] = sumcheck;
data_to_send[_cnt++] = addcheck;
hal_uart_transmit(&huart1,data_to_send,_cnt,0xffff);//这里是串口发送函数
}
niming.h
#ifndef niming_h
#define niming_h
#include "main.h"
//需要发送16位,32位数据,对数据拆分,之后每次发送单个字节
//拆分过程:对变量dwtemp 去地址然后将其转化成char类型指针,最后再取出指针所指向的内容
#define byte0(dwtemp) (*(char *)(&dwtemp))
#define byte1(dwtemp) (*((char *)(&dwtemp) + 1))
#define byte2(dwtemp) (*((char *)(&dwtemp) + 2))
#define byte3(dwtemp) (*((char *)(&dwtemp) + 3))
void ano_dt_send_f1(uint16_t, uint16_t _b, uint16_t _c, uint16_t _d);
void ano_dt_send_f2(int16_t _a, int16_t _b, int16_t _c, int16_t _d);
void ano_dt_send_f3(int16_t _a, int16_t _b, int32_t _c );
#endif
添加测试代码
//电机速度等信息发送到上位机
//注意上位机不支持浮点数,所以要乘100
ano_dt_send_f2(motor1speed*100, 3.0*100,motor2speed*100,3.0*100);
下面设置上位机-数据解析
这个是控制效果,并不理想,后面我们介绍pid控制
加入的现在 过去 未来概念
p:现在
i:过去
d:未来
那么我们就开始写pid
pid的结构体类型变量、里面成员都是浮点类型
先在pid.h声明一个结构体类型、声明.c中的函数
#ifndef __pid_h
#define __pid_h
//声明一个结构体类型
typedef struct
{
float target_val;//目标值
float actual_val;//实际值
float err;//当前偏差
float err_last;//上次偏差
float err_sum;//误差累计值
float kp,ki,kd;//比例,积分,微分系数
} tpid;
//声明函数
float p_realize(tpid * pid,float actual_val);
void pid_init(void);
float pi_realize(tpid * pid,float actual_val);
float pid_realize(tpid * pid,float actual_val);
#endif
然后在pid.c中定义结构体类型变量
#include "pid.h"
//定义一个结构体类型变量
tpid pidmotor1speed;
//给结构体类型变量赋初值
void pid_init()
{
pidmotor1speed.actual_val=0.0;
pidmotor1speed.target_val=0.00;
pidmotor1speed.err=0.0;
pidmotor1speed.err_last=0.0;
pidmotor1speed.err_sum=0.0;
pidmotor1speed.kp=0;
pidmotor1speed.ki=0;
pidmotor1speed.kd=0;
}
//比例p调节控制函数
float p_realize(tpid * pid,float actual_val)
{
pid->actual_val = actual_val;//传递真实值
pid->err = pid->target_val - pid->actual_val;//当前误差=目标值-真实值
//比例控制调节 输出=kp*当前误差
pid->actual_val = pid->kp*pid->err;
return pid->actual_val;
}
//比例p 积分i 控制函数
float pi_realize(tpid * pid,float actual_val)
{
pid->actual_val = actual_val;//传递真实值
pid->err = pid->target_val - pid->actual_val;//当前误差=目标值-真实值
pid->err_sum += pid->err;//误差累计值 = 当前误差累计和
//使用pi控制 输出=kp*当前误差+ki*误差累计值
pid->actual_val = pid->kp*pid->err + pid->ki*pid->err_sum;
return pid->actual_val;
}
// pid控制函数
float pid_realize(tpid * pid,float actual_val)
{
pid->actual_val = actual_val;//传递真实值
pid->err = pid->target_val - pid->actual_val;当前误差=目标值-真实值
pid->err_sum += pid->err;//误差累计值 = 当前误差累计和
//使用pid控制 输出 = kp*当前误差 + ki*误差累计值 + kd*(当前误差-上次误差)
pid->actual_val = pid->kp*pid->err + pid->ki*pid->err_sum + pid->kd*(pid->err - pid->err_last);
//保存上次误差: 这次误差赋值给上次误差
pid->err_last = pid->err;
return pid->actual_val;
}
然后在main中要调用pid_init();函数
pid_init();
p调节函数函数只根据当前误差进行控制
//比例p调节控制函数
float p_realize(tpid * pid,float actual_val)
{
pid->actual_val = actual_val;//传递真实值
pid->err = pid->target_val - pid->actual_val;//目标值减去实际值等于误差值
//比例控制调节
pid->actual_val = pid->kp*pid->err;
return pid->actual_val;
}
主函数-可以估算当p=10 就有较好的响应速度
先看根据p比例控制的效果
p调节 电机稳态后还是存在误差。
下面加入i 调节也就是加入历史误差
pi的控制函数
//比例p 积分i 控制函数
float pi_realize(tpid * pid,float actual_val)
{
pid->actual_val = actual_val;//传递真实值
pid->err = pid->target_val - pid->actual_val;//目标值减去实际值等于误差值
pid->err_sum += pid->err;//误差累计求和
//使用pi控制
pid->actual_val = pid->kp*pid->err + pid->ki*pid->err_sum;
return pid->actual_val;
}
因为实际值1.6的时候误差为1.4 上次偏差1.4和这次偏差1.4相加2.8 我们乘5 等于10点多就会有较好控制效果
这是pi 调节的控制效果
下面是pid调节的
// pid控制函数
float pid_realize(tpid * pid,float actual_val)
{
pid->actual_val = actual_val;//传递真实值
pid->err = pid->target_val - pid->actual_val;//目标值减去实际值等于误差值
pid->err_sum += pid->err;//误差累计求和
//使用pid控制
pid->actual_val = pid->kp*pid->err + pid->ki*pid->err_sum + pid->kd*(pid->err - pid->err_last);
//保存上次误差:最近一次 赋值给上次
pid->err_last = pid->err;
return pid->actual_val;
}
调大堆栈
软件开启中断
开启接收中断
__hal_uart_enable_it(&huart1,uart_it_rxne); //开启串口1接收中断
中断回调函数
uint8_t usart1_readbuf[256]; //串口1 缓冲数组
uint8_t usart1_readcount = 0; //串口1 接收字节计数
if(__hal_uart_get_flag(&huart1,uart_flag_rxne))//判断huart1 是否读到字节
{
if(usart1_readcount >= 255) usart1_readcount = 0;
hal_uart_receive(&huart1,&usart1_readbuf[usart1_readcount++],1,1000);
}
编写函数用于判断串口是否发送完一帧数据
extern uint8_t usart1_readbuf[255]; //串口1 缓冲数组
extern uint8_t usart1_readcount; //串口1 接收字节计数
//判断否接收完一帧数据
uint8_t usart_waitreasfinish(void)
{
static uint16_t usart_lastreadcount = 0;//记录上次的计数值
if(usart1_readcount == 0)
{
usart_lastreadcount = 0;
return 1;//表示没有在接收数据
}
if(usart1_readcount == usart_lastreadcount)//如果这次计数值等于上次计数值
{
usart1_readcount = 0;
usart_lastreadcount = 0;
return 0;//已经接收完成了
}
usart_lastreadcount = usart1_readcount;
return 2;//表示正在接受中
}
然后我们把cjson库放入工程里面
下载cjson新版
gtihub链接:https://github.com/davegamble/cjson
百度网盘链接:https://pan.baidu.com/s/1acnhtzuv5bokmq2f6qog7q
提取码:a422
和添加其他文件一样,加入工程,然后指定路径
编写解析指令的函数
#include "cjson.h"
#include <string.h>
cjson *cjsondata ,*cjsonvlaue;
if(usart_waitreasfinish() == 0)//是否接收完毕
{
cjsondata = cjson_parse((const char *)usart1_readbuf);
if(cjson_getobjectitem(cjsondata,"p") !=null)
{
cjsonvlaue = cjson_getobjectitem(cjsondata,"p");
p = cjsonvlaue->valuedouble;
pidmotor1speed.kp = p;
}
if(cjson_getobjectitem(cjsondata,"i") !=null)
{
cjsonvlaue = cjson_getobjectitem(cjsondata,"i");
i = cjsonvlaue->valuedouble;
pidmotor1speed.ki = i;
}
if(cjson_getobjectitem(cjsondata,"d") !=null)
{
cjsonvlaue = cjson_getobjectitem(cjsondata,"d");
d = cjsonvlaue->valuedouble;
pidmotor1speed.kd = d;
}
if(cjson_getobjectitem(cjsondata,"a") !=null)
{
cjsonvlaue = cjson_getobjectitem(cjsondata,"a");
a = cjsonvlaue->valuedouble;
pidmotor1speed.target_val =a;
}
if(cjsondata != null){
cjson_delete(cjsondata);//释放空间、但是不能删除cjsonvlaue不然会 出现异常错误
}
memset(usart1_readbuf,0,255);//清空接收buf,注意这里不能使用strlen
}
printf("p:%.3f i:%.3f d:%.3f a:%.3f\r\n",p,i,d,a);
测试发送cjson数据就会解析收到数据
然后我们赋值改变一个电机的pid参数和目标转速
然后我们通过串口发送命令,就会改变pid的参数了
这么我们的第八章就弄好了,下篇我们进行第九章-pid整定
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论