第八章-PID-速度控制
8.1-速度控制探索
前面我們已經能夠通過編碼器測量出速度值,下面我們來控制速度
我們先編寫一個簡單的控制方法
要求:講轉速控制再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:%drn",Motor1Speed,Motor1Pwm);
printf("Motor2Speed:%.2f Motor2Pwm:%drn",Motor2Speed,Motor2Pwm);
HAL_Delay(100);
開始實驗
現(xiàn)象就開始電機沒有到達3轉每秒,PWM占空比逐漸增大,電機逐漸達到要求轉速、到達要求轉速后我們增加阻力,電機變慢,阻力大小不邊PWM占空比逐漸更大轉速逐漸更大。
這樣我們就把轉速控制到我們想要的范圍,但是我們并不滿意、能夠看出來控制的速度很慢,給電機一些阻力電機至少要2-3秒能夠調整過來,這在一些場景是不允許的。
我們理想的控制效果是:在電機轉速很慢的是時候能快速調整,在電機一直轉的不能達到要求時候能夠更快速度調整
8.2-準備工作-匿名上位機曲線顯示速度波形方便觀察數(shù)據
為了方便觀察電機速度數(shù)據,我們通過上位機曲線顯示一下。
這里我們使用的上位機是匿名上位機-大佬寫的非常穩(wěn)定功能也很多
我使用的版本是:匿名上位機V7.2.2.8版本推薦大家和我使用一樣
匿名上位機官方下載鏈接:https://www.anotc.com/wiki/匿名產品資料/資料下載鏈接匯總
我們要把STM32數(shù)據發(fā)送到匿名上位機,就要滿足匿名上位機的數(shù)據協(xié)議要求
在匿名上位機資料下載鏈接,可以下載到協(xié)議介紹
- 匿名上位機V7通信協(xié)議,20210528發(fā)布:https://pan.baidu.com/s/1nGrIGWj6qr9DWOcGpKR51g 提取碼:z8d1
- CSDN 慕羽★大佬寫的協(xié)議解析教程博客:https://blog.csdn.net/qq_44339029/article/details/106004997
1.先補充一下大小端模式
這是因為在計算機系統(tǒng)中,我們是以字節(jié)為單位的,每個地址單元都對應著一個字節(jié),一個字節(jié)為 8bit。但是在C語言中除了8bit的char之外,還有16bit的short型,32bit的long型(要看具體的編譯器),另外,對于位數(shù)大于8位的處理器,例如16位或者32位的處理器,由于寄存器寬度大于一個字節(jié),那么必然存在著一個如和將多個字節(jié)安排的問題。因此就導致了大端存儲模式和小端存儲模式。例如一個16bit的short型x,在內存中的地址為0x0010,x的值為0x1122,那么0x11為高字節(jié),0x22為低字節(jié)。對于大端模式,就將0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。 - 所謂的大端模式(BE big-endian),是指數(shù)據的低位保存在內存的高地址中,而數(shù)據的高位,保存在內存的低地址中(低對高,高對低);
- 所謂的小端模式(LE little-endian),是指數(shù)據的低位保存在內存的低地址中,而數(shù)據的高位保存在內存的高地址中(低對低,高對高)。
常見的單片機大小端模式:(1)KEIL C51中,變量都是大端模式的,而KEIL MDK中,變量是小端模式的。(2)SDCC-C51是小端尋址,AVRGCC 小端尋址.(3)PC小端,大部分ARM是小端 (4)總起來說51單片機一般是大端模式,32單片機一般是小端模式.
2.看一下上位機要求的協(xié)議
靈活格式幀(用戶自定義幀)
前面我們好理解
0xAA:一個字節(jié)表示開始
0xFF:一個字節(jié)表示目標地址
0xF1:一個字節(jié)表示發(fā)送功能碼
1-40:一個字節(jié)表示數(shù)據長度
數(shù)據內容有多個字節(jié)如何發(fā)送
因為串口每次發(fā)送一個字節(jié),但是數(shù)據可能是int16_t 16位的數(shù)據,或者int32_t 32位數(shù)據,每次發(fā)送16位數(shù)據,先發(fā)送數(shù)據低八位,還是先發(fā)送數(shù)據高八位那?
匿名協(xié)議通信介紹給出:DATA 數(shù)據內容中的數(shù)據,采用小端模式傳送,低字節(jié)在前,高字節(jié)在后。
那么就要求,比如我們在發(fā)送16位數(shù)據0x2314我們要先發(fā)送低字節(jié)0x14,然后發(fā)送高字節(jié)0x23
那么如何解析出低字節(jié)或者高字節(jié),就需要知道多字節(jié)數(shù)據在單片機里面是怎么存的,因為STM32是小端存儲,所以低字節(jié)就在低位地址中,高字節(jié)高位地址中。
如果使用32單片機 小端模式,0x23高地址,0x14在低地址,所以我們要先發(fā)低地址,再發(fā)高地址。
下面就是對16位數(shù)據,或者32位數(shù)據的拆分
//需要發(fā)送16位,32位數(shù)據,對數(shù)據拆分,之后每次發(fā)送單個字節(jié)
//拆分過程:對變量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))
拆分后我們按照協(xié)議要求發(fā)送數(shù)據就可以了
niming.c
#include "niming.h"
#include "main.h"
#include "usart.h"
uint8_t data_to_send[100];
//通過F1幀發(fā)送4個uint16類型的數(shù)據
void ANO_DT_Send_F1(uint16_t _a, uint16_t _b, uint16_t _c, uint16_t _d)
{
uint8_t _cnt = 0; //計數(shù)值
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; //數(shù)據長度
//單片機為小端模式-低地址存放低位數(shù)據,匿名上位機要求先發(fā)低位數(shù)據,所以先發(fā)低地址
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);//這里是串口發(fā)送函數(shù)
}
//,通過F2幀發(fā)送4個int16類型的數(shù)據
void ANO_DT_Send_F2(int16_t _a, int16_t _b, int16_t _c, int16_t _d) //F2幀 4個 int16 參數(shù)
{
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; //數(shù)據長度
//單片機為小端模式-低地址存放低位數(shù)據,匿名上位機要求先發(fā)低位數(shù)據,所以先發(fā)低地址
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);//這里是串口發(fā)送函數(shù)
}
//通過F3幀發(fā)送2個int16類型和1個int32類型的數(shù)據
void ANO_DT_Send_F3(int16_t _a, int16_t _b, int32_t _c ) //F3幀 2個 int16 參數(shù) 1個 int32 參數(shù)
{
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; //數(shù)據長度
//單片機為小端模式-低地址存放低位數(shù)據,匿名上位機要求先發(fā)低位數(shù)據,所以先發(fā)低地址
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);//這里是串口發(fā)送函數(shù)
}
niming.h
#ifndef NIMING_H
#define NIMING_H
#include "main.h"
//需要發(fā)送16位,32位數(shù)據,對數(shù)據拆分,之后每次發(fā)送單個字節(jié)
//拆分過程:對變量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
添加測試代碼
//電機速度等信息發(fā)送到上位機
//注意上位機不支持浮點數(shù),所以要乘100
ANO_DT_Send_F2(Motor1Speed*100, 3.0*100,Motor2Speed*100,3.0*100);
下面設置上位機-數(shù)據解析
這個是控制效果,并不理想,后面我們介紹PID控制
8.3-P I D 逐個參數(shù)理解
加入的現(xiàn)在 過去 未來概念
p:現(xiàn)在
i:過去
d:未來
那么我們就開始寫PID
PID的結構體類型變量、里面成員都是浮點類型
先在pid.h聲明一個結構體類型、聲明.c中的函數(shù)
#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;//比例,積分,微分系數(shù)
} tPid;
//聲明函數(shù)
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調節(jié)控制函數(shù)
float P_realize(tPid * pid,float actual_val)
{
pid->actual_val = actual_val;//傳遞真實值
pid->err = pid->target_val - pid->actual_val;//當前誤差=目標值-真實值
//比例控制調節(jié) 輸出=Kp*當前誤差
pid->actual_val = pid->Kp*pid->err;
return pid->actual_val;
}
//比例P 積分I 控制函數(shù)
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控制函數(shù)
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();函數(shù)
PID_init();
p調節(jié)函數(shù)函數(shù)只根據當前誤差進行控制
//比例p調節(jié)控制函數(shù)
float P_realize(tPid * pid,float actual_val)
{
pid->actual_val = actual_val;//傳遞真實值
pid->err = pid->target_val - pid->actual_val;//目標值減去實際值等于誤差值
//比例控制調節(jié)
pid->actual_val = pid->Kp*pid->err;
return pid->actual_val;
}
主函數(shù)-可以估算當p=10 就有較好的響應速度
先看根據p比例控制的效果
p調節(jié) 電機穩(wěn)態(tài)后還是存在誤差。
下面加入i 調節(jié)也就是加入歷史誤差
pi的控制函數(shù)
//比例P 積分I 控制函數(shù)
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 調節(jié)的控制效果
下面是PID調節(jié)的
// PID控制函數(shù)
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;
}
8.4-加入cJSON方便上位機調參
調大堆棧
軟件開啟中斷
開啟接收中斷
__HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE); //開啟串口1接收中斷
中斷回調函數(shù)
uint8_t Usart1_ReadBuf[256]; //串口1 緩沖數(shù)組
uint8_t Usart1_ReadCount = 0; //串口1 接收字節(jié)計數(shù)
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_RXNE))//判斷huart1 是否讀到字節(jié)
{
if(Usart1_ReadCount >= 255) Usart1_ReadCount = 0;
HAL_UART_Receive(&huart1,&Usart1_ReadBuf[Usart1_ReadCount++],1,1000);
}
編寫函數(shù)用于判斷串口是否發(fā)送完一幀數(shù)據
extern uint8_t Usart1_ReadBuf[255]; //串口1 緩沖數(shù)組
extern uint8_t Usart1_ReadCount; //串口1 接收字節(jié)計數(shù)
//判斷否接收完一幀數(shù)據
uint8_t Usart_WaitReasFinish(void)
{
static uint16_t Usart_LastReadCount = 0;//記錄上次的計數(shù)值
if(Usart1_ReadCount == 0)
{
Usart_LastReadCount = 0;
return 1;//表示沒有在接收數(shù)據
}
if(Usart1_ReadCount == Usart_LastReadCount)//如果這次計數(shù)值等于上次計數(shù)值
{
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
和添加其他文件一樣,加入工程,然后指定路徑
編寫解析指令的函數(shù)
#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不然會 出現(xiàn)異常錯誤
}
memset(Usart1_ReadBuf,0,255);//清空接收buf,注意這里不能使用strlen
}
printf("P:%.3f I:%.3f D:%.3f A:%.3frn",p,i,d,a);
測試發(fā)送cJSON數(shù)據就會解析收到數(shù)據
然后我們賦值改變一個電機的PID參數(shù)和目標轉速
然后我們通過串口發(fā)送命令,就會改變PID的參數(shù)了
這么我們的第八章就弄好了,下篇我們進行第九章-PID整定
聯(lián)系:Q,1930299709