当前位置: 首页 > news >正文

PID控制算法

施工中

:::danger
本文依据江协科大的视频和PPT所写,图片也都来自视频截图或PPT。

本文项目框架大体上沿用正点原子,并根据个人习惯做出适当修改。如串口驱动分开在usart.c和stm32f1xx_it.c里;并且printf函数抽象成int my_USART_printf(uint8_t x, char *format, ... ) ,可以指定串口发送数据。

本文使用视频配套套件。若你只是想要了解 PID 控制原理及流程,可以不使用套件,否则建议搭配套件食用。

:::

:::color2
⚠️建议在深色配色下阅读。

:::

视频地址:

PID入门教程-电机控制 倒立摆 全程手把手打代码调试_哔哩哔哩_bilibili

music163

PID理论基础

PID是什么

> + PID是比例(Proportional)、积分(Integral)、微分(Differential)的缩写。 > + PID是一种闭环控制算法,它动态改变施加到被控对象的输出值(Out),使得被控对象某一物理量的实际值(Actual),能够快速、准确、稳定地跟踪到指定的目标值(Target)。 > + PID是一种基于误差(Error)调控的算法,其中规定:误差=目标值-实际值,PID的任务是使误差始终为0。 > + PID对被控对象模型要求低,无需建模,即使被控对象内部运作规律不明确,PID也能进行调控。 >

简单来说,PID是通过对误差的运算来控制较为简单的系统或无法明确知晓内部工作原理的系统的算法。

查看:开环与闭环

开环(Open Loop)是指控制器单向输出值给被控对象,不获取被控对象的反馈,控制器对被控对象的执行状态不清楚;

闭环(Closed Loop)是指控制器输出值给被控对象,同时获取被控对象的反馈,控制器知道被控对象的执行状态,可以根据反馈修改输出值以优化控制。

换言之,开环控制只能控制对象的一个"趋向",而闭环可以精确控制对象的物理值。

PID是怎样工作的

PID公式与系统框图

$ 误差:𝑒𝑟𝑟𝑜𝑟(𝑡)=𝑡𝑎𝑟𝑔𝑒𝑡(𝑡)−𝑎𝑐𝑡𝑢𝑎𝑙(𝑡)

$

$ PID输出值: 𝑜𝑢𝑡(𝑡)=𝐾_𝑝 (𝑒𝑟𝑟𝑜𝑟(𝑡)+\frac{1}{𝑇_𝑖} ∫_0^𝑡𝑒𝑟𝑟𝑜𝑟(𝑡)𝑑𝑡+\frac{𝑇_𝑑 𝑑𝑒𝑟𝑟𝑜𝑟(𝑡)}{𝑑𝑡}) $

$ PID输出值: 𝑜𝑢𝑡(𝑡)=𝐾_𝑝∗𝑒𝑟𝑟𝑜𝑟(𝑡)+𝐾_𝑖∗∫_0^𝑡𝑒𝑟𝑟𝑜𝑟(𝑡)𝑑𝑡+𝐾_𝑑∗\frac{𝑑𝑒𝑟𝑟𝑜𝑟(𝑡)}{𝑑𝑡} $

PID系统框图,使用第3条公式。

其中,第二条公式使用较少且调参麻烦,一般使用第三条公式。

第三条公式是由第二条把Kp乘进去得到的。

公式为“经验公式”,不用细究其中深意(

:::info
简单拆解公式可以得到:

OUT = Kp当前值 + Ki历史值 + Kd*变化趋势

:::

详解PID公式

P:比例项

+ 只含有比例项的输出:$ 𝑜𝑢𝑡(𝑡)=𝐾_𝑝∗𝑒𝑟𝑟𝑜𝑟(𝑡)$。 + 比例项的输出值仅取决于当前时刻的误差,与历史时刻无关。当前存在误差时,比例项输出一个与误差呈正比的值,当前不存在误差时,比例项输出0。 + K_p越大,比例项权重越大,系统响应越快,但超调也会随之增加。 + 纯比例项控制时,系统一般会存在稳态误差,Kp越大,稳态误差越小。

:::info
超调:指明明只有一点误差,PID却反馈一个很大的输出,导致系统越过目标值朝另一个方向靠拢;即用力过猛。

稳态误差:指系统达到稳定状态后仍和目标值有一定误差。

稳态误差产生原因:纯比例项控制时,若误差为0,则比例项结果也为0。被控对象输入0时,一般会自发地向一个方向偏移,产生误差。产生误差后,误差非0,比例项负反馈调控输出,当调控输出力度和自发偏移力度相同时,系统达到稳态。

判断是否会产生稳态误差:给被控对象输入0,判断被控对象会不会自发偏移。

判断稳态误差的方向:给被控对象输入0,自发偏移方向即为稳态误差方向。

举例:当PID输出只有比例项,且目标值为10,Kp=0.25时,当系统达到3时, out=7*0.25=1.75,与系统自偏差相同,两者相互抵消。这就造成系统仍和目标有差距却达到稳态的情况。这里的系统自偏差可能是由于电机有摩擦力。

:::

Kp=0.25

Kp=0.75

Kp=0.5

Kp=1

从上图可以看到,Kp越大,稳态误差越小,但始终存在。

I:积分项

+ 含有比例项和积分项的输出:$ 𝑜𝑢𝑡(𝑡)=𝐾_𝑝∗𝑒𝑟𝑟𝑜𝑟(𝑡) + 𝐾_𝑖∗∫_0^𝑡𝑒𝑟𝑟𝑜𝑟(𝑡)𝑑𝑡$ + 积分项的输出值取决于0~t所有时刻误差的积分,与历史时刻有关。积分项将历史所有时刻的误差累积,乘上积分项系数Ki后作为积分项输出值。 + 积分项用于弥补纯比例项产生的稳态误差,若系统持续产生误差,则积分项会不断累积误差,直到控制器产生动作,让稳态误差消失。 + Ki越大,积分项权重越大,稳态误差消失越快,但系统滞后性也会随之增加。

:::info
在程序中,积分即为每次测得的error值累加到一个总error值里;0~t表示从程序刚开始运行的时候就开始进行累加。

由于积分项进行的是累加操作,所以当误差为0时积分项不会清零,而是维持在一个值左右,可以和系统自偏差相抵消。

基于同样的原因,从误差产生到积分发挥作用需要一定的时间,当输出只有积分时,系统的滞后性会大大增加,这点在电机正转反转之间切换时尤为明显。

:::

Kp=0,Ki=0.05

Kp=0.25,Ki=0.2

Kp=0.25,Ki=0.5

Kp=0.25,Ki=0.05

Kp=0.5,Ki=0.2

Kp=0.5,Ki=0.5

D:微分项

+ 含有比例项、积分项和微分项的PID输出:$ 𝑜𝑢𝑡(𝑡)=𝐾_𝑝∗𝑒𝑟𝑟𝑜𝑟(𝑡)+𝐾_𝑖∗∫_0^𝑡𝑒𝑟𝑟𝑜𝑟(𝑡)𝑑𝑡+𝐾_𝑑∗\frac{𝑑𝑒𝑟𝑟𝑜𝑟(𝑡)}{𝑑𝑡} $ + 微分项的输出值取决于当前时刻误差变化的斜率,与当前时刻附近误差变化的趋势有关。当误差急剧变化时,微分项会负反馈输出相反的作用力,阻碍误差急剧变化。 + 斜率一定程度上反映了误差未来的变化趋势,这使得微分项具有 “预测未来,提前调控”的特性。 + 微分项给系统增加阻尼,可以有效防止系统超调,尤其是惯性比较大的系统。 + Kd越大,微分项权重越大,系统阻尼越大,但系统卡顿现象也会随之增加。

:::info
在程序中,微分就是指上一次和这一次误差的差值,如果你的程序是每隔一段时间测量一下,也可以除以取均值,或是减小Kd相应的值。

微分项一般配合PI或P使用。

微分项可以有效防止系统震荡,使震荡不断衰减;同时微分项的阻尼作用对PI也有效,会削弱PI项的调控力度。

:::

Kd值不同的变化。从左到右四段Kd值分别是0、0、2、4

图片解释:

第一段:系统稳定,没有误差。

第二段:误差开始出现,并且导致系统震荡,越来越大。

第三段:Kd为2,开始削减震荡;但震荡减小到一定程度后削减作用不明显。

第四段:Kd为4,削减力度加强,波形趋于平稳。

单片机中的PID

离散形式PID

在上文中,我们了解的error(t)是一个连续的函数,但在单片机中两次测量往往会间隔一定的时间;也就是说,我们实际上得到的是一个个离散值。

在离散形式的PID中:$ 𝑜𝑢𝑡(𝑡)=𝐾_𝑝∗𝑒𝑟𝑟𝑜𝑟(k)+𝐾_𝑖∗ \textstyle \sum_{j=0}^k 𝑒𝑟𝑟𝑜𝑟(j)+𝐾_𝑑∗(error(k)-error(k-1)) $

实际上应该是$ 𝑜𝑢𝑡(𝑡)=𝐾_𝑝∗𝑒𝑟𝑟𝑜𝑟(k)+𝐾_𝑖∗T* \textstyle \sum_{j=0}^k 𝑒𝑟𝑟𝑜𝑟(j)+𝐾_𝑑∗\frac{error(k)-error(k-1)}{T} $

但可以把T看作是参数的一部分,化简成上面的形式。

上面的公式也可以叫“位置式PID”,与之相对的还有“增量式PID”:

$ \color{OrangeRed}{
\begin{align}
\Delta out(t) =
&K_p * (error(k) - error(k-1))\
&+ K_ierror(k)\
&+K_d
(error(k)-2error(k-1)+error(k-2))
\end{align}
}
$

:::info
位置式和增量式更像是通项公式和递推公式的区别。

增量式的好处是公式中没有求和这样的大型运算,所有运算都是加减乘除基础运算。

增量式PID适合被控对象有积分功能或有积分性质,如步进电机和阀门控制等。

:::

离散PID实现

确定调控周期T

T取决于:
  • 被控对象变化的速度:如平衡车、飞行器等就需要较高的调控频率(5ms甚至1ms),电机就不宜过高的调控频率(一般20~100ms);像锅炉这类变化及其缓慢的设备,T会更长。
  • 硬件限制:如姿态传感器更新间隔是5ms,那么小于这个间隔的调控都是无效的。

实现方案

1. delay延时:不推荐。 2. 定时器:推荐,但需注意涉及硬件的操作,尽量避免中断访问某硬件、主函数也访问这个硬件,可能会造成资源冲突。 3. 定时器置标志位:在定时器中断中置标志位,主函数判断标志位然后执行调控。这种方法避免了资源冲突,但和方法1有一样的问题。

位置式PID实现

> 下面代码使用的是类似C的伪代码,采用方法2实现T。 >
/*定义变量*/
float Target,Actual,Out;		//目标值,实际值,输出值
float Kp =值,Ki =值,Kd=值;		//比例项,积分项,微分项的权重
float Error0, Error1, ErrorInt;	//本次误差,上次误差,误差积分int main()
{Timer_Init();while(1){Target = 用户在此处指定目标值;}
}
void TIM2_IRQHandler(void)
{if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET){/*每隔时间T,程序执行到这里一次*//**********执行PID调控**********//*获取实际值*/Actual=读取传感器();/*获取本次误差和上次误差*/Error1 = ErrorO;Error0 = Target - Actual;/*误差积分(累加)*/ErrorInt += ErrorO;/*PID计算*/Out = Kp * Error0 + Ki * ErrorInt + Kd * (Error0 - Error1);/*输岀限幅*/if(Out > 上限){Out = 上限;}if(Out < 下限){Out = 下限;}/*执行控制*/输出至被控对象(Out);/********************/TIM_ClearITPendingBit(TIM2, TIM_IT_Update);}
}

:::info
值得注意的是输出限幅积分限幅。比如电机控制函数参数接收范围为+-100,那么就需要对输出进行限幅防止参数不合法。如果不想误差积分影响过大,也可以设置积分限幅(#即ErrorInt的上下限)使得积分项可以兼具消除稳态偏差快和系统响应快的特点。

:::

增量式PID实现

```c /*定义变量*/ float Target,Actual,Out; //目标值,实际值,输出值 float Kp =值,Ki =值,Kd=值; //比例项,积分项,微分项的权重 float Error0, Error1, Error2; //本次误差,上次误差,上上次误差

int main()
{
Timer_Init();

while(1){Target = 用户在此处指定目标值;
}

}


```c
void TIM2_IRQHandler(void)
{if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET){/*每隔时间T,程序执行到这里一次*//**********执行PID调控**********//*获取实际值*/Actual=读取传感器();/*获取本次误差和上次误差*/Error2 = Error1;Error1 = Error0;Error0 = Target - Actual;/*误差积分(累加)*///ErrorInt += Error0;/*PID计算*/Out += Kp * Error0 + Ki * Error0 + Kd * (Error0 - 2*Error1 + Error2);/*输岀限幅*/if(Out > 上限){Out = 上限;}if(Out < 下限){Out = 下限;}/*执行控制*/输出至被控对象(Out);/********************/TIM_ClearITPendingBit(TIM2, TIM_IT_Update);}
}

:::info
注意,这里的out值是对增量的累加,因此上面的代码可以看作“用增量式PID实现位置式PID”的算法。虽说如此,但该算法仍有位置式不具备的功能:当PID暂停控制时,out会维持不变,而不是变为0 。

这种特性导致它特别适合那种需要切换自动控制和手动控制的场景。(位置式也可以,但由于误差积分需要一定时间进行变化,从手动切换自动会导致系统刚开始时出现抖动)

如果想要原本的增量式,把“out += Kp……”的“+=”改成“=”即可。

:::

PID编程实战

那些与PID无关的知识:基础驱动代码&编码电机原理

:::warning 附件如下,请下载后将后缀名改为zip解压缩。

00SimpleDriver.docx

:::

在开始编写PID之前,得先完成底层外设驱动的编写,如此才能读取传感器的值、控制被控对象。

:::danger
本文使用江协科技设计的PID学习套件,如果没有可以购买或根据官网原理图自行设计。

PID电机驱动及控制板-V1.0.pdf

PID入门套件用户手册-V1.0.pdf

编码电机工作原理.pdf

:::

外设表格一览

| 外设名 | 引脚接线 | | --- | --- | | OLED显示屏 | SCL PB8
SDA PB9 | | 电机驱动模块TB6612FNG | AIN1 PB12
AIM2 PB13
BIN1 PB14
BIN2 PB15
PWMA PA0
PWMB PA1 | | 稳压模块MP1584EN | 无 | | 编码电机接口(两组) | 组一:
EA PA6
EB PA7
组二:
EA PB6
EB PB7 | | 角度传感器接口(2个) | OUT1 PB0
OUT2 PB1 | | 串口 | RX PA10
TX PA9 | | 电位器(4个) | PA2~PA5 | | 按键(4个) | K1 PB10
K2 PB11
K3 PA11
K4 PA12 |

定时器(TIM1)

定时器1用于按键检测、编码器读取速度等需要定时完成的工作。设置成1ms中断一次。
TIM_HandleTypeDef g_tim1_handle;     /* 定时器1句柄 */void Timer_Init(){LED_OFF();__HAL_RCC_TIM1_CLK_ENABLE();                                 /* 使能TIMx时钟 */g_tim1_handle.Instance = TIM1;                              /* 通用定时器x */g_tim1_handle.Init.Prescaler = 72-1;                         /* 预分频系数 */g_tim1_handle.Init.CounterMode = TIM_COUNTERMODE_UP;        /* 递增计数模式 */g_tim1_handle.Init.Period = 1000-1;                            /* 自动装载值 */HAL_TIM_Base_Init(&g_tim1_handle);HAL_NVIC_SetPriority(TIM1_UP_IRQn, 1, 2);                      /* 设置中断优先级,抢占优先级1,子优先级3 */HAL_NVIC_EnableIRQ(TIM1_UP_IRQn);                              /* 开启ITMx中断 */HAL_TIM_Base_Start_IT(&g_tim1_handle);                      /* 使能定时器x和定时器x更新中断 */
}int16_t speed=0,location=0;
void TIM1_UP_IRQHandler()
{if(__HAL_TIM_GET_FLAG(&g_tim1_handle, TIM_FLAG_UPDATE) == SET){Key_Tick();static int8_t count=0;count++;if(count>20){count=0;OLED_Update();speed = Encoder_Get();location+=speed;}__HAL_TIM_CLEAR_IT(&g_tim1_handle, TIM_IT_UPDATE);  /* 清除定时器溢出中断标志位 */}
}

KEY

:::warning ⚠️注意:本文件是在作者以前项目上修改而成,因此宏定义不代表实际意义,也可能会出现一些魔法数字。

:::

#ifndef __KEY_H
#define __KEY_H#include "./SYSTEM/sys/sys.h"/******************************************************************************************/
/* 引脚 定义 */#define KEY0_GPIO_PORT                  GPIOB
#define KEY0_GPIO_PIN                   GPIO_PIN_10
#define KEY0_GPIO_CLK_ENABLE()          do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PE口时钟使能 */#define KEY1_GPIO_PORT                  GPIOB
#define KEY1_GPIO_PIN                   GPIO_PIN_11
#define KEY1_GPIO_CLK_ENABLE()          do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PE口时钟使能 */#define WKUP_GPIO_PORT                  GPIOA
#define WKUP_GPIO_PIN                   GPIO_PIN_11
#define WKUP_GPIO_CLK_ENABLE()          do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)   /* PA口时钟使能 *//******************************************************************************************/#define KEY0        HAL_GPIO_ReadPin(KEY0_GPIO_PORT, KEY0_GPIO_PIN)     /* 读取KEY0引脚 */
#define KEY1        HAL_GPIO_ReadPin(KEY1_GPIO_PORT, KEY1_GPIO_PIN)     /* 读取KEY1引脚 */
#define WK_UP       HAL_GPIO_ReadPin(WKUP_GPIO_PORT, WKUP_GPIO_PIN)     /* 读取WKUP引脚 */#define KEY0_PRES    1              /* KEY0按下 */
#define KEY1_PRES    2              /* KEY1按下 */
#define WKUP_PRES    3              /* KEY_UP按下(即WK_UP) */void key_init(void);                /* 按键初始化函数 */
uint8_t Key_GetNum(void);
void Key_Tick(void);//uint8_t key_scan(uint8_t mode);     /* 按键扫描函数 */#endif
#include "./BSP/KEY/key.h"
#include "./SYSTEM/delay/delay.h"uint8_t Key_Num;
/*** @brief       按键初始化函数* @param       无* @retval      无*/
void key_init(void)
{GPIO_InitTypeDef gpio_init_struct;KEY0_GPIO_CLK_ENABLE();                                     /* KEY0时钟使能 */KEY1_GPIO_CLK_ENABLE();                                     /* KEY1时钟使能 */WKUP_GPIO_CLK_ENABLE();                                     /* WKUP时钟使能 */gpio_init_struct.Pin = KEY0_GPIO_PIN;                       /* K1引脚 */gpio_init_struct.Mode = GPIO_MODE_INPUT;                    /* 输入 */gpio_init_struct.Pull = GPIO_PULLUP;                        /* 上拉 */gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;              /* 高速 */HAL_GPIO_Init(KEY0_GPIO_PORT, &gpio_init_struct);           /* KEY0引脚模式设置,上拉输入 */gpio_init_struct.Pin = KEY1_GPIO_PIN;                       /* K2引脚 */HAL_GPIO_Init(KEY1_GPIO_PORT, &gpio_init_struct);           /* KEY1引脚模式设置,上拉输入 */gpio_init_struct.Pin = WKUP_GPIO_PIN;                       /* K3引脚 */HAL_GPIO_Init(WKUP_GPIO_PORT, &gpio_init_struct);           /* WKUP引脚模式设置,下拉输入 */gpio_init_struct.Pin = GPIO_PIN_12;                         /* K4引脚 */HAL_GPIO_Init(WKUP_GPIO_PORT, &gpio_init_struct);           /* WKUP引脚模式设置,下拉输入 */}uint8_t Key_GetState(void)
{if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_10) == 0){return 1;}if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_11) == 0){return 2;}if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_11) == 0){return 3;}if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_12) == 0){return 4;}return 0;
}uint8_t Key_GetNum(void)
{uint8_t Temp;if (Key_Num){Temp = Key_Num;Key_Num = 0;return Temp;}return 0;
}void Key_Tick(void)
{static uint8_t Count;static uint8_t CurrState, PrevState;Count ++;if (Count >= 20){Count = 0;PrevState = CurrState;CurrState = Key_GetState();if (CurrState == 0 && PrevState != 0){Key_Num = PrevState;}}
}

LED

```c #ifndef _LED_H #define _LED_H #include "./SYSTEM/sys/sys.h"

/*****************************************************************************************/
/
引脚 定义 */

define LED0_GPIO_PORT GPIOC

define LED0_GPIO_PIN GPIO_PIN_13

define LED0_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* PC口时钟使能 */

/*****************************************************************************************/
/
LED端口定义 */

define LED0(x) do{ x ? \

                  HAL_GPIO_WritePin(LED0_GPIO_PORT, LED0_GPIO_PIN, GPIO_PIN_SET) : \HAL_GPIO_WritePin(LED0_GPIO_PORT, LED0_GPIO_PIN, GPIO_PIN_RESET); \}while(0)      /* LED0翻转 */

define LED_ON() LED0(0)

define LED_OFF() LED0(1)

/* LED取反定义 */

define LED0_TOGGLE() do{ HAL_GPIO_TogglePin(LED0_GPIO_PORT, LED0_GPIO_PIN); }while(0) /* 翻转LED0 */

define LED_TOGGLE() do{ HAL_GPIO_TogglePin(LED0_GPIO_PORT, LED0_GPIO_PIN); }while(0)

/***************************************************************************************/
/
外部接口函数
/
void led_init(void); /
初始化 */

endif


```c#include "./BSP/LED/led.h"void led_init(void)
{GPIO_InitTypeDef gpio_init_struct;LED0_GPIO_CLK_ENABLE();                                 /* LED0时钟使能 */gpio_init_struct.Pin = LED0_GPIO_PIN;                   /* LED0引脚 */gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;            /* 推挽输出 */gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;          /* 高速 */HAL_GPIO_Init(LED0_GPIO_PORT, &gpio_init_struct);       /* 初始化LED0引脚 */LED0(1);                                                /* 关闭 LED0 */
}

ADC(用于电位器)

```c /* #define RP_Init ADC2_Init #define RP_GetValue(x) ADC2_get_result(x+1) void ADC2_Init(); uint32_t ADC2_get_result(uint8_t ch); 这四个是电位器用到的 */ #ifndef __ADC_H #define __ADC_H

include "./SYSTEM/sys/sys.h"

define ADC_DMA_BUF_SIZE 1 * 4 /* ADC DMA采集 BUF大小, 应等于ADC通道数的整数倍 */

/*****************************************************************************************/
/
ADC及引脚 定义 */

define ADC_ADCX_CHY_GPIO_PORT GPIOA

define ADC_ADCX_CHY_GPIO_PIN GPIO_PIN_1

define ADC_ADCX_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0) /* PA口时钟使能 */

define ADC_ADCX ADC1

define ADC_ADCX_CHY ADC_CHANNEL_1 /* 通道Y, 0 <= Y <= 17 */

define ADC_ADCX_CHY_CLK_ENABLE() do{ __HAL_RCC_ADC1_CLK_ENABLE(); }while(0) /* ADC1 时钟使能 */

/* ADC单通道/多通道 DMA采集 DMA及通道 定义

  • 注意: ADC1的DMA通道只能是: DMA1_Channel1, 因此只要是ADC1, 这里是不能改动的
  •   ADC2不支持DMA采集
    
  •   ADC3的DMA通道只能是: DMA2_Channel5, 因此如果使用 ADC3 则需要修改
    

*/

define ADC_ADCX_DMACx DMA1_Channel1

define ADC_ADCX_DMACx_IRQn DMA1_Channel1_IRQn

define ADC_ADCX_DMACx_IRQHandler DMA1_Channel1_IRQHandler

define ADC_ADCX_DMACx_IS_TC() ( DMA1->ISR & (1 << 1) ) /* 判断 DMA1_Channel1 传输完成标志, 这是一个假函数形式,

                                                                     * 不能当函数使用, 只能用在if等语句里面 */

define ADC_ADCX_DMACx_CLR_TC() do{ DMA1->IFCR |= 1 << 1; }while(0) /* 清除 DMA1_Channel1 传输完成标志 */

define RP_Init ADC2_Init

define RP_GetValue(x) ADC2_get_result(x+1)

/******************************************************************************************/

void adc_init(void); /* ADC初始化 */
void adc_channel_set(ADC_HandleTypeDef adc_handle, uint32_t ch,uint32_t rank, uint32_t stime); / ADC通道设置 /
uint32_t adc_get_result(uint32_t ch); /
获得某个通道值 /
uint32_t adc_get_result_average(uint32_t ch, uint8_t times); /
得到某个通道给定次数采样的平均值 */

void adc_dma_init(uint32_t mar); /* ADC DMA采集初始化 /
void adc_dma_enable( uint16_t cndtr); /
使能一次ADC DMA采集传输 */

void adc_nch_dma_init(uint32_t mar); /* ADC多通道 DMA采集初始化 */

void ADC2_Init();
uint32_t ADC2_get_result(uint8_t ch);

endif


```c
/******************************************************************************************************* @file        adc.c* @author      正点原子团队(ALIENTEK)* @version     V1.2* @date        2020-04-23* @brief       ADC 驱动代码* @license     Copyright (c) 2020-2032, 广州市星翼电子科技有限公司***************************************************************************************************** @attention** 实验平台:正点原子 STM32F103开发板* 在线视频:www.yuanzige.com* 技术论坛:www.openedv.com* 公司网址:www.alientek.com* 购买地址:openedv.taobao.com** 修改说明* V1.0 20200423* 第一次发布* V1.1 20200423* 1,支持ADC单通道DMA采集 * 2,新增adc_dma_init和adc_dma_enable函数.* V1.2 20200423* 1,支持ADC多通道DMA采集 * 2,新增adc_nch_dma_init函数.******************************************************************************************************/#include "./BSP/ADC/adc.h"
#include "./SYSTEM/delay/delay.h"ADC_HandleTypeDef g_adc_handle;   /* ADC句柄 *//*** @brief       ADC初始化函数*   @note      本函数支持ADC1/ADC2任意通道, 但是不支持ADC3*              我们使用12位精度, ADC采样时钟=12M, 转换时间为: 采样周期 + 12.5个ADC周期*              设置最大采样周期: 239.5, 则转换时间 = 252 个ADC周期 = 21us* @param       无* @retval      无*/
void adc_init(void)
{g_adc_handle.Instance = ADC_ADCX;                        /* 选择哪个ADC */g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;       /* 数据对齐方式:右对齐 */g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;       /* 非扫描模式,仅用到一个通道 */g_adc_handle.Init.ContinuousConvMode = DISABLE;          /* 关闭连续转换模式 */g_adc_handle.Init.NbrOfConversion = 1;                   /* 赋值范围是1~16,本实验用到1个规则通道序列 */g_adc_handle.Init.DiscontinuousConvMode = DISABLE;       /* 禁止规则通道组间断模式 */g_adc_handle.Init.NbrOfDiscConversion = 0;               /* 配置间断模式的规则通道个数,禁止规则通道组间断模式后,此参数忽略 */g_adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 触发转换方式:软件触发 */HAL_ADC_Init(&g_adc_handle);                             /* 初始化 */HAL_ADCEx_Calibration_Start(&g_adc_handle);              /* 校准ADC */
}/*** @brief       ADC底层驱动,引脚配置,时钟使能此函数会被HAL_ADC_Init()调用* @param       hadc:ADC句柄* @retval      无*/
void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
{if(hadc->Instance == ADC_ADCX){GPIO_InitTypeDef gpio_init_struct;RCC_PeriphCLKInitTypeDef adc_clk_init = {0};ADC_ADCX_CHY_CLK_ENABLE();                                /* 使能ADCx时钟 */ADC_ADCX_CHY_GPIO_CLK_ENABLE();                           /* 开启GPIO时钟 *//* 设置ADC时钟 */adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;    /* ADC外设时钟 */adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;       /* 分频因子6时钟为72M/6=12MHz */HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);                 /* 设置ADC时钟 *//* 设置AD采集通道对应IO引脚工作模式 */gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN;             /* ADC通道IO引脚 */gpio_init_struct.Mode = GPIO_MODE_ANALOG;                 /* 模拟 */HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);}
}/*** @brief       设置ADC通道采样时间* @param       adcx : adc句柄指针,ADC_HandleTypeDef* @param       ch   : 通道号, ADC_CHANNEL_0~ADC_CHANNEL_17* @param       stime: 采样时间  0~7, 对应关系为:*   @arg       ADC_SAMPLETIME_1CYCLE_5, 1.5个ADC时钟周期        ADC_SAMPLETIME_7CYCLES_5, 7.5个ADC时钟周期*   @arg       ADC_SAMPLETIME_13CYCLES_5, 13.5个ADC时钟周期     ADC_SAMPLETIME_28CYCLES_5, 28.5个ADC时钟周期*   @arg       ADC_SAMPLETIME_41CYCLES_5, 41.5个ADC时钟周期     ADC_SAMPLETIME_55CYCLES_5, 55.5个ADC时钟周期*   @arg       ADC_SAMPLETIME_71CYCLES_5, 71.5个ADC时钟周期     ADC_SAMPLETIME_239CYCLES_5, 239.5个ADC时钟周期* @param       rank: 多通道采集时需要设置的采集编号,假设你定义channle1的rank=1,channle2 的rank=2,那么对应你在DMA缓存空间的变量数组AdcDMA[0] 就i是channle1的转换结果,AdcDMA[1]就是通道2的转换结果。 单通道DMA设置为 ADC_REGULAR_RANK_1*   @arg       编号1~16:ADC_REGULAR_RANK_1~ADC_REGULAR_RANK_16* @retval      无*/
void adc_channel_set(ADC_HandleTypeDef *adc_handle, uint32_t ch, uint32_t rank, uint32_t stime)
{ADC_ChannelConfTypeDef adc_ch_conf;adc_ch_conf.Channel = ch;                            /* 通道 */adc_ch_conf.Rank = rank;                             /* 序列 */adc_ch_conf.SamplingTime = stime;                    /* 采样时间 */HAL_ADC_ConfigChannel(adc_handle, &adc_ch_conf);     /* 通道配置 */
}/*** @brief       获得ADC转换后的结果* @param       ch: 通道值 0~17,取值范围为:ADC_CHANNEL_0~ADC_CHANNEL_17* @retval      无*/
uint32_t adc_get_result(uint32_t ch)
{adc_channel_set(&g_adc_handle , ch, ADC_REGULAR_RANK_1, ADC_SAMPLETIME_239CYCLES_5);    /* 设置通道,序列和采样时间 */HAL_ADC_Start(&g_adc_handle);                            /* 开启ADC */HAL_ADC_PollForConversion(&g_adc_handle, 10);            /* 轮询转换 */return (uint16_t)HAL_ADC_GetValue(&g_adc_handle);        /* 返回最近一次ADC1规则组的转换结果 */
}/*** @brief       获取通道ch的转换值,取times次,然后平均* @param       ch      : 通道号, 0~17* @param       times   : 获取次数* @retval      通道ch的times次转换结果平均值*/
uint32_t adc_get_result_average(uint32_t ch, uint8_t times)
{uint32_t temp_val = 0;uint8_t t;for (t = 0; t < times; t++)     /* 获取times次数据 */{temp_val += adc_get_result(ch);delay_ms(5);}return temp_val / times;        /* 返回平均值 */
}/***************************************单通道ADC采集(DMA读取)实验代码*****************************************/DMA_HandleTypeDef g_dma_adc_handle = {0};                                   /* 定义要搬运ADC数据的DMA句柄 */
ADC_HandleTypeDef g_adc_dma_handle = {0};                                   /* 定义ADC(DMA读取)句柄 */
uint8_t g_adc_dma_sta = 0;                                                  /* DMA传输状态标志, 0,未完成; 1, 已完成 *//*** @brief       ADC DMA读取 初始化函数*   @note      本函数还是使用adc_init对ADC进行大部分配置,有差异的地方再单独配置* @param       par         : 外设地址* @param       mar         : 存储器地址* @retval      无*/
void adc_dma_init(uint32_t mar)
{GPIO_InitTypeDef gpio_init_struct;RCC_PeriphCLKInitTypeDef adc_clk_init = {0};ADC_ChannelConfTypeDef adc_ch_conf = {0};ADC_ADCX_CHY_CLK_ENABLE();                                              /* 使能ADCx时钟 */ADC_ADCX_CHY_GPIO_CLK_ENABLE();                                         /* 开启GPIO时钟 */if ((uint32_t)ADC_ADCX_DMACx > (uint32_t)DMA1_Channel7)                 /* 大于DMA1_Channel7, 则为DMA2的通道了 */{__HAL_RCC_DMA2_CLK_ENABLE();                                        /* DMA2时钟使能 */}else{__HAL_RCC_DMA1_CLK_ENABLE();                                        /* DMA1时钟使能 */}/* 设置ADC时钟 */adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;                  /* ADC外设时钟 */adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;                     /* 分频因子6时钟为72M/6=12MHz */HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);                               /* 设置ADC时钟 *//* 设置AD采集通道对应IO引脚工作模式 */gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN;                           /* ADC通道对应的IO引脚 */gpio_init_struct.Mode = GPIO_MODE_ANALOG;                               /* 模拟 */HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);/* 初始化DMA */g_dma_adc_handle.Instance = ADC_ADCX_DMACx;                             /* 设置DMA通道 */g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;                 /* 从外设到存储器模式 */g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE;                     /* 外设非增量模式 */g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE;                         /* 存储器增量模式 */g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;    /* 外设数据长度:16位 */g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;       /* 存储器数据长度:16位 */g_dma_adc_handle.Init.Mode = DMA_NORMAL;                                /* 外设流控模式 */g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM;                   /* 中等优先级 */HAL_DMA_Init(&g_dma_adc_handle);__HAL_LINKDMA(&g_adc_dma_handle, DMA_Handle, g_dma_adc_handle);         /* 将DMA与adc联系起来 */g_adc_dma_handle.Instance = ADC_ADCX;                                   /* 选择哪个ADC */g_adc_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;                  /* 数据对齐方式:右对齐 */g_adc_dma_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;                  /* 非扫描模式,仅用到一个通道 */g_adc_dma_handle.Init.ContinuousConvMode = ENABLE;                      /* 使能连续转换模式 */g_adc_dma_handle.Init.NbrOfConversion = 1;                              /* 赋值范围是1~16,本实验用到1个规则通道序列 */g_adc_dma_handle.Init.DiscontinuousConvMode = DISABLE;                  /* 禁止规则通道组间断模式 */g_adc_dma_handle.Init.NbrOfDiscConversion = 0;                          /* 配置间断模式的规则通道个数,禁止规则通道组间断模式后,此参数忽略 */g_adc_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;            /* 触发转换方式:软件触发 */HAL_ADC_Init(&g_adc_dma_handle);                                        /* 初始化 */HAL_ADCEx_Calibration_Start(&g_adc_dma_handle);                         /* 校准ADC *//* 配置ADC通道 */adc_ch_conf.Channel = ADC_ADCX_CHY;                                     /* 通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                                  /* 序列 */adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;                  /* 采样时间,设置最大采样周期:239.5个ADC周期 */HAL_ADC_ConfigChannel(&g_adc_dma_handle, &adc_ch_conf);                 /* 通道配置 *//* 配置DMA数据流请求中断优先级 */HAL_NVIC_SetPriority(ADC_ADCX_DMACx_IRQn, 3, 3);HAL_NVIC_EnableIRQ(ADC_ADCX_DMACx_IRQn);HAL_DMA_Start_IT(&g_dma_adc_handle, (uint32_t)&ADC1->DR, mar, 0);       /* 启动DMA,并开启中断 */HAL_ADC_Start_DMA(&g_adc_dma_handle, &mar, 0);                          /* 开启ADC,通过DMA传输结果 */
}/*************************单通道ADC采集(DMA读取)实验和多通道ADC采集(DMA读取)实验公用代码*******************************/DMA_HandleTypeDef g_dma_nch_adc_handle = {0};                               /* 定义要搬运ADC多通道数据的DMA句柄 */
ADC_HandleTypeDef g_adc_nch_dma_handle = {0};                               /* 定义ADC(多通道DMA读取)句柄 *//*** @brief       ADC N通道(6通道) DMA读取 初始化函数*   @note      本函数还是使用adc_init对ADC进行大部分配置,有差异的地方再单独配置*              另外,由于本函数用到了6个通道, 宏定义会比较多内容, 因此,本函数就不采用宏定义的方式来修改通道了,*              直接在本函数里面修改, 这里我们默认使用PA0~PA5这6个通道.**              注意: 本函数还是使用 ADC_ADCX(默认=ADC1) 和 ADC_ADCX_DMACx( DMA1_Channel1 ) 及其相关定义*              不要乱修改adc.h里面的这两部分内容, 必须在理解原理的基础上进行修改, 否则可能导致无法正常使用.** @param       mar         : 存储器地址 * @retval      无*/
void adc_nch_dma_init(uint32_t mar)
{GPIO_InitTypeDef gpio_init_struct;RCC_PeriphCLKInitTypeDef adc_clk_init = {0};ADC_ChannelConfTypeDef adc_ch_conf = {0};ADC_ADCX_CHY_CLK_ENABLE();                                                /* 使能ADCx时钟 */__HAL_RCC_GPIOA_CLK_ENABLE();                                             /* 开启GPIOA时钟 */if ((uint32_t)ADC_ADCX_DMACx > (uint32_t)DMA1_Channel7)                   /* 大于DMA1_Channel7, 则为DMA2的通道了 */{__HAL_RCC_DMA2_CLK_ENABLE();                                          /* DMA2时钟使能 */}else{__HAL_RCC_DMA1_CLK_ENABLE();                                          /* DMA1时钟使能 */}/* 设置ADC时钟 */adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;                    /* ADC外设时钟 */adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;                       /* 分频因子6时钟为72M/6=12MHz */HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);                                 /* 设置ADC时钟 *//* 设置ADC1通道0~5对应的IO口模拟输入AD采集引脚模式设置,模拟输入PA0对应 ADC1_IN0PA1对应 ADC1_IN1PA2对应 ADC1_IN2PA3对应 ADC1_IN3PA4对应 ADC1_IN4PA5对应 ADC1_IN5*/gpio_init_struct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5;  /* GPIOA0~5 */gpio_init_struct.Mode = GPIO_MODE_ANALOG;                                 /* 模拟 */HAL_GPIO_Init(GPIOA, &gpio_init_struct);/* 初始化DMA */g_dma_nch_adc_handle.Instance = ADC_ADCX_DMACx;                           /* 设置DMA通道 */g_dma_nch_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;               /* 从外设到存储器模式 */g_dma_nch_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE;                   /* 外设非增量模式 */g_dma_nch_adc_handle.Init.MemInc = DMA_MINC_ENABLE;                       /* 存储器增量模式 */g_dma_nch_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;  /* 外设数据长度:16位 */g_dma_nch_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;     /* 存储器数据长度:16位 */g_dma_nch_adc_handle.Init.Mode = DMA_NORMAL;                              /* 外设流控模式 */g_dma_nch_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM;                 /* 中等优先级 */HAL_DMA_Init(&g_dma_nch_adc_handle);__HAL_LINKDMA(&g_adc_nch_dma_handle, DMA_Handle, g_dma_nch_adc_handle);   /* 将DMA与adc联系起来 *//* 初始化ADC */g_adc_nch_dma_handle.Instance = ADC_ADCX;                                 /* 选择哪个ADC */g_adc_nch_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;                /* 数据对齐方式:右对齐 */g_adc_nch_dma_handle.Init.ScanConvMode = ADC_SCAN_ENABLE;                 /* 使能扫描模式 */g_adc_nch_dma_handle.Init.ContinuousConvMode = ENABLE;                    /* 使能连续转换 */g_adc_nch_dma_handle.Init.NbrOfConversion = 6;                            /* 赋值范围是1~16,本实验用到6个规则通道序列 */g_adc_nch_dma_handle.Init.DiscontinuousConvMode = DISABLE;                /* 禁止规则通道组间断模式 */g_adc_nch_dma_handle.Init.NbrOfDiscConversion = 0;                        /* 配置间断模式的规则通道个数,禁止规则通道组间断模式后,此参数忽略 */g_adc_nch_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;          /* 软件触发 */HAL_ADC_Init(&g_adc_nch_dma_handle);                                      /* 初始化 */HAL_ADCEx_Calibration_Start(&g_adc_nch_dma_handle);                       /* 校准ADC *//* 配置ADC通道 */adc_ch_conf.Channel = ADC_CHANNEL_2;                                      /* 配置使用的ADC通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                                    /* 采样序列里的第1个 */adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;                    /* 采样时间,设置最大采样周期:239.5个ADC周期 */HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 通道配置 */adc_ch_conf.Channel = ADC_CHANNEL_3;                                      /* 配置使用的ADC通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_2;                                    /* 采样序列里的第2个 */HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 配置ADC通道 */adc_ch_conf.Channel = ADC_CHANNEL_4;                                      /* 配置使用的ADC通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_3;                                    /* 采样序列里的第3个 */HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 配置ADC通道 */adc_ch_conf.Channel = ADC_CHANNEL_5;                                      /* 配置使用的ADC通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_4;                                    /* 采样序列里的第4个 */HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 配置ADC通道 *///    adc_ch_conf.Channel = ADC_CHANNEL_4;                                      /* 配置使用的ADC通道 */
//    adc_ch_conf.Rank = ADC_REGULAR_RANK_5;                                    /* 采样序列里的第5个 */
//    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 配置ADC通道 *///    adc_ch_conf.Channel = ADC_CHANNEL_5;                                      /* 配置使用的ADC通道 */
//    adc_ch_conf.Rank = ADC_REGULAR_RANK_6;                                    /* 采样序列里的第6个 */
//    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 配置ADC通道 *//* 配置DMA数据流请求中断优先级 */HAL_NVIC_SetPriority(ADC_ADCX_DMACx_IRQn, 3, 3);HAL_NVIC_EnableIRQ(ADC_ADCX_DMACx_IRQn);HAL_DMA_Start_IT(&g_dma_nch_adc_handle, (uint32_t)&ADC1->DR, mar, 0);     /* 启动DMA,并开启中断 */HAL_ADC_Start_DMA(&g_adc_nch_dma_handle, &mar, 0);                        /* 开启ADC,通过DMA传输结果 */
}/*************************单通道ADC采集(DMA读取)实验和多通道ADC采集(DMA读取)实验公用代码*******************************//*** @brief       使能一次ADC DMA传输*   @note      该函数用寄存器来操作,防止用HAL库操作对其他参数有修改,也为了兼容性* @param       ndtr: DMA传输的次数* @retval      无*/
void adc_dma_enable(uint16_t cndtr)
{ADC_ADCX->CR2 &= ~(1 << 0);                 /* 先关闭ADC */ADC_ADCX_DMACx->CCR &= ~(1 << 0);           /* 关闭DMA传输 */while (ADC_ADCX_DMACx->CCR & (1 << 0));     /* 确保DMA可以被设置 */ADC_ADCX_DMACx->CNDTR = cndtr;              /* DMA传输数据量 */ADC_ADCX_DMACx->CCR |= 1 << 0;              /* 开启DMA传输 */ADC_ADCX->CR2 |= 1 << 0;                    /* 重新启动ADC */ADC_ADCX->CR2 |= 1 << 22;                   /* 启动规则转换通道 */
}/*** @brief       ADC DMA采集中断服务函数* @param       无 * @retval      无*/
void ADC_ADCX_DMACx_IRQHandler(void)
{if (ADC_ADCX_DMACx_IS_TC()){g_adc_dma_sta = 1;                      /* 标记DMA传输完成 */ADC_ADCX_DMACx_CLR_TC();                /* 清除DMA1 数据流7 传输完成中断 */}
}ADC_HandleTypeDef g_adc2_handle;
ADC_HandleTypeDef g_adc1_handle;
void ADC2_Init(){GPIO_InitTypeDef gpio_init_struct;RCC_PeriphCLKInitTypeDef adc_clk_init = {0};ADC_ChannelConfTypeDef adc_ch_conf = {0};__HAL_RCC_ADC2_CLK_ENABLE();                                                /* 使能ADCx时钟 */__HAL_RCC_GPIOA_CLK_ENABLE();                                             /* 开启GPIOA时钟 */if ((uint32_t)ADC_ADCX_DMACx > (uint32_t)DMA1_Channel7)                   /* 大于DMA1_Channel7, 则为DMA2的通道了 */{__HAL_RCC_DMA2_CLK_ENABLE();                                          /* DMA2时钟使能 */}else{__HAL_RCC_DMA1_CLK_ENABLE();                                          /* DMA1时钟使能 */}/* 设置ADC时钟 */adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;                    /* ADC外设时钟 */adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;                       /* 分频因子6时钟为72M/6=12MHz */HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);                                 /* 设置ADC时钟 *//* 设置ADC1通道0~5对应的IO口模拟输入AD采集引脚模式设置,模拟输入PA0对应 ADC1_IN0PA1对应 ADC1_IN1PA2对应 ADC1_IN2PA3对应 ADC1_IN3PA4对应 ADC1_IN4PA5对应 ADC1_IN5*/gpio_init_struct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5;  /* GPIOA0~5 */gpio_init_struct.Mode = GPIO_MODE_ANALOG;                                 /* 模拟 */HAL_GPIO_Init(GPIOA, &gpio_init_struct);/* 初始化ADC */g_adc2_handle.Instance = ADC2;                                 /* 选择哪个ADC */g_adc2_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;                /* 数据对齐方式:右对齐 */g_adc2_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;                 /* 使能扫描模式 */g_adc2_handle.Init.ContinuousConvMode = ENABLE;                    /* 使能连续转换 */g_adc2_handle.Init.NbrOfConversion = 4;                            /* 赋值范围是1~16,本实验用到6个规则通道序列 */g_adc2_handle.Init.DiscontinuousConvMode = DISABLE;                /* 禁止规则通道组间断模式 */g_adc2_handle.Init.NbrOfDiscConversion = 0;                        /* 配置间断模式的规则通道个数,禁止规则通道组间断模式后,此参数忽略 */g_adc2_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;          /* 软件触发 */HAL_ADC_Init(&g_adc2_handle);                                      /* 初始化 */HAL_ADCEx_Calibration_Start(&g_adc2_handle);                       /* 校准ADC *//* 配置ADC通道 */adc_ch_conf.Channel = ADC_CHANNEL_2;                                      /* 配置使用的ADC通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                                    /* 采样序列里的第1个 */adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;                    /* 采样时间,设置最大采样周期:239.5个ADC周期 */HAL_ADC_ConfigChannel(&g_adc2_handle, &adc_ch_conf);               /* 通道配置 */adc_ch_conf.Channel = ADC_CHANNEL_3;                                      /* 配置使用的ADC通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_2;                                    /* 采样序列里的第2个 */HAL_ADC_ConfigChannel(&g_adc2_handle, &adc_ch_conf);               /* 配置ADC通道 */adc_ch_conf.Channel = ADC_CHANNEL_4;                                      /* 配置使用的ADC通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_3;                                    /* 采样序列里的第3个 */HAL_ADC_ConfigChannel(&g_adc2_handle, &adc_ch_conf);               /* 配置ADC通道 */adc_ch_conf.Channel = ADC_CHANNEL_5;                                      /* 配置使用的ADC通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_4;                                    /* 采样序列里的第4个 */HAL_ADC_ConfigChannel(&g_adc2_handle, &adc_ch_conf);               /* 配置ADC通道 */HAL_ADC_Start_IT(&g_adc2_handle);        //开启转换并触发中断HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0); // 设置中断优先级HAL_NVIC_EnableIRQ(ADC1_2_IRQn);         // 使能 ADC1 和 ADC2 的中断
}void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
//    static uint8_t ADC2CH=0;
//    if (hadc->Instance == ADC2)
//    {
//        
//        g_adc_dma_buf[ADC2CH++] = HAL_ADC_GetValue(hadc);
//        if(ADC2CH==4)ADC2CH=0;
//        
//    }
}void ADC1_2_IRQHandler(void)
{if (__HAL_ADC_GET_FLAG(&g_adc2_handle, ADC_FLAG_EOC) != RESET){HAL_ADC_IRQHandler(&g_adc2_handle); // 将 gdc2 的地址传递给 HAL_ADC_IRQHandler}// 检查 ADC2 是否触发了中断if (__HAL_ADC_GET_FLAG(&g_adc1_handle, ADC_FLAG_EOC) != RESET){HAL_ADC_IRQHandler(&g_adc1_handle); // 将 adc1 的地址传递给 HAL_ADC_IRQHandler}
}uint32_t ADC2_get_result(uint8_t ch)
{adc_channel_set(&g_adc2_handle , ch, ADC_REGULAR_RANK_1, ADC_SAMPLETIME_239CYCLES_5);    /* 设置通道,序列和采样时间 */HAL_ADC_Start(&g_adc2_handle);                            /* 开启ADC */HAL_ADC_PollForConversion(&g_adc2_handle, 10);            /* 轮询转换 */return (uint16_t)HAL_ADC_GetValue(&g_adc2_handle);        /* 返回最近一次ADC1规则组的转换结果 */
}

电机驱动和PWM(TIM2)

```c #ifndef __MOTOR_H #define __MOTOR_H #include "./SYSTEM/sys/sys.h" #include "./BSP/TIMER/gtim.h" void Motor_Init(void); void Motor_SetPWM(int16_t Speed); void PWM_Init(); void PWM_SetCompare1(uint16_t Compare); #endif

```c
#include "./SYSTEM/sys/sys.h"
//#include "PWM.h"#include "./BSP/Motor/Motor.h"
/*** 函    数:直流电机初始化* 参    数:无* 返 回 值:无*/
void Motor_Init(void)
{GPIO_InitTypeDef gpio_init_struct;__HAL_RCC_GPIOB_CLK_ENABLE();gpio_init_struct.Pin = GPIO_PIN_13;                     /* 引脚 */gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;            /* 推挽输出 */gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;          /* 高速 */HAL_GPIO_Init(GPIOB, &gpio_init_struct);       /* 初始化引脚 */gpio_init_struct.Pin = GPIO_PIN_12;                     /* 引脚 */HAL_GPIO_Init(GPIOB, &gpio_init_struct);       /* 初始化引脚 */PWM_Init();                                             //初始化直流电机的底层PWMHAL_GPIO_WritePin(GPIOB, GPIO_PIN_12,GPIO_PIN_SET);	    //PA4置高电平HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13,GPIO_PIN_RESET);	//PA5置低电平,设置方向为正转
}/*** 函    数:直流电机设置速度* 参    数:Speed 要设置的速度,范围:-100~100* 返 回 值:无*/
void Motor_SetPWM(int16_t Speed)
{if (Speed >= 0)							//如果设置正转的速度值{   if(Speed>1000)Speed=1000;HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12,GPIO_PIN_SET);	    //PA4置高电平HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13,GPIO_PIN_RESET);	//PA5置低电平,设置方向为正转PWM_SetCompare1(Speed);				//PWM设置为速度值}else									//否则,即设置反转的速度值{   if(Speed<-1000)Speed=-1000;HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12,GPIO_PIN_RESET);	//PA4置低电平HAL_GPIO_WritePin(GPIOB, GPIO_PIN_13,GPIO_PIN_SET);	    //PA5置高电平,设置方向为反转PWM_SetCompare1(-Speed);			//PWM设置为负的速度值,因为此时速度值为负数,而PWM只能给正数}
}void PWM_Init(){gtim_timx_pwm_chy_init(1000-1,36-1);
}void PWM_SetCompare1(uint16_t Compare){__HAL_TIM_SET_COMPARE(&g_timx_pwm_chy_handle,GTIM_TIMX_PWM_CHY,Compare);
}
/******************************************************************************************************* @file        gtim.h* @author      正点原子团队(ALIENTEK)* @version     V1.0* @date        2020-04-20* @brief       通用定时器 驱动代码* @license     Copyright (c) 2020-2032, 广州市星翼电子科技有限公司***************************************************************************************************** @attention** 实验平台:正点原子 STM32F103开发板* 在线视频:www.yuanzige.com* 技术论坛:www.openedv.com* 公司网址:www.alientek.com* 购买地址:openedv.taobao.com** 修改说明* V1.0 20211216* 第一次发布******************************************************************************************************/#ifndef __GTIM_H
#define __GTIM_H#include "./SYSTEM/sys/sys.h"/******************************************************************************************/
/* 通用定时器 定义 *//* TIMX 中断定义 * 默认是针对TIM2~TIM5.* 注意: 通过修改这4个宏定义,可以支持TIM1~TIM17任意一个定时器.*/#define GTIM_TIMX_INT                       TIM3
#define GTIM_TIMX_INT_IRQn                  TIM3_IRQn
#define GTIM_TIMX_INT_IRQHandler            TIM3_IRQHandler
#define GTIM_TIMX_INT_CLK_ENABLE()          do{ __HAL_RCC_TIM3_CLK_ENABLE(); }while(0)  /* TIM3 时钟使能 *//* TIMX PWM输出定义 * 这里输出的PWM控制LED0(RED)的亮度* 默认是针对TIM2~TIM5* 注意: 通过修改这几个宏定义,可以支持TIM1~TIM8任意一个定时器,任意一个IO口输出PWM*/
#define GTIM_TIMX_PWM_CHY_GPIO_PORT         GPIOA
#define GTIM_TIMX_PWM_CHY_GPIO_PIN          GPIO_PIN_0
#define GTIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)   /* PB口时钟使能 *//* TIMX REMAP设置* 因为我们LED0接在PB5上, 必须通过开启TIM3的部分重映射功能, 才能将TIM3_CH2输出到PB5上* 因此, 必须实现GTIM_TIMX_PWM_CHY_GPIO_REMAP* 对那些使用默认设置的定时器PWM输出脚, 不用设置重映射, 是不需要该函数的!* 具体用哪个函数、用不用重映射,查看stm32f1xx_hal_gpio_ex.h,搜__HAL_AFIO_REMAP_TIM3_PARTIAL,看相关系列函数*/
#define GTIM_TIMX_PWM_CHY_GPIO_REMAP()      do{__HAL_RCC_AFIO_CLK_ENABLE();\__HAL_AFIO_REMAP_TIM2_DISABLE();\}while(0)            /* 通道REMAP设置, 该函数不是必须的, 根据需要实现 */#define GTIM_TIMX_PWM                       TIM2 
#define GTIM_TIMX_PWM_CHY                   TIM_CHANNEL_1                               /* 通道Y,  1<= Y <=4 */
#define GTIM_TIMX_PWM_CHY_CCRX              TIM2->CCR1                                  /* 通道Y的输出比较寄存器 */
#define GTIM_TIMX_PWM_CHY_CLK_ENABLE()      do{ __HAL_RCC_TIM2_CLK_ENABLE(); }while(0)  /* TIM3 时钟使能 *//*********************************以下是通用定时器输入捕获实验相关宏定义*************************************//* TIMX 输入捕获定义 * 这里的输入捕获使用定时器TIM5_CH1,捕获WK_UP按键的输入* 默认是针对TIM2~TIM5. * 注意: 通过修改这几个宏定义,可以支持TIM1~TIM8任意一个定时器,任意一个IO口做输入捕获*       特别要注意:默认用的PA0,设置的是下拉输入!如果改其他IO,对应的上下拉方式也得改!*/
#define GTIM_TIMX_CAP_CHY_GPIO_PORT            GPIOB
#define GTIM_TIMX_CAP_CHY_GPIO_PIN             GPIO_PIN_6
#define GTIM_TIMX_CAP_CHY_GPIO_CLK_ENABLE()    do{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0)   /* PD口时钟使能 */#define GTIM_TIMX_CAP                          TIM4                       
#define GTIM_TIMX_CAP_IRQn                     TIM4_IRQn
#define GTIM_TIMX_CAP_IRQHandler               TIM4_IRQHandler
#define GTIM_TIMX_CAP_CHY                      TIM_CHANNEL_1                                 /* 通道Y,  1<= Y <=4 */
#define GTIM_TIMX_CAP_CHY_CCRX                 TIM4->CCR1                                    /* 通道Y的输出比较寄存器 */
#define GTIM_TIMX_CAP_CHY_CLK_ENABLE()         do{ __HAL_RCC_TIM4_CLK_ENABLE();\/*__HAL_AFIO_REMAP_TIM4_ENABLE();*/\}while(0)    /* TIM5 时钟使能 *//******************************************************************************************/
extern uint8_t  g_timxchy_cap_sta;  /* 输入捕获状态 */
extern uint16_t g_timxchy_cap_val;  /* 输入捕获值 */
extern TIM_HandleTypeDef g_timx_pwm_chy_handle;        /* 定时器x句柄 */void gtim_tim3_int_init(uint16_t arr, uint16_t psc);        /* 通用定时器 定时中断初始化函数 */
void gtim_tim2_int_init(uint16_t arr, uint16_t psc);
void gtim_timx_pwm_chy_init(uint16_t arr, uint16_t psc);    /* 通用定时器 PWM初始化函数 */
void gtim_timx_cap_chy_init(uint16_t arr, uint16_t psc);    /* 通用定时器 输入捕获初始化函数 */#endif
#include "./BSP/TIMER/gtim.h"
#include "./BSP/LED/led.h"
/*------------------------基本介绍------------------------*//*
通用定时器:TIM2、3、4、5和基本定时器相比,通用定时器的计数器允许递增、递减、中心对齐三种计数模式;用途上不仅可以触发DAC(数模转换),还可以触发ADC(模数转换);中断条件上,除了更新事件外还有触发事件、输入捕获、输出比较额外拥有4个独立通道,可用于输入捕获、输出比较、PMW输出、单脉冲模式可以用外部信号控制定时器,可以实现多个定时器互联的同步电路(级联,即用某个定时器的溢出事件来作为下一个定时器的时钟信号)支持编码器和霍尔传感器电路时钟源可以来自于:系统总线(APB,即内部时钟)、内部触发输入时钟(来自鱼其它定时器的控制器等)、IO引脚复用为定时器ETR引脚(外部时钟模式二)、本定时器通道一、通道二(外部时钟模式一)IO引脚复用查看STM32F103ZET6.pdf,搜索ETR即可。搜到的第二个TIM8_ETR就是,前面的PA0-WKUP就是引脚名称,在前面的数字即使引脚编号,查看自己芯片的封装类别即可。通道同理,搜CH1和CH2即可。控制器:可以接入其它定时器的内部触发输入,也可以到DAC/ADC。输入捕获、捕获/比较、输出比较:以CH1通道为例外部信号->IO引脚->(滤波器、边沿检测器)->产生捕获信号到捕获寄存器1或者2(除了事件外还可以产生中断,只不过中断需要配置)->将计数器当前值转移到捕获/比较影子寄存器,输入模式下再转移到捕获/比较预装载寄存器用于程序读取,输出模式下将捕获/比较预装载寄存器值转移到其影子寄存器(缓冲)->与计数器值比较->若相等->改变输出信号(输出信号高电平有效),并产生比较事件(如果配置了还会产生中断)->信号传到输出控制模块->输出控制模块产生输出信号到本通道输出引脚。注:输出引脚和输入引脚是同一引脚,它们是分时复用的关系,同时只有一边有效。*//*------------------------时钟源介绍------------------------*/
/*内部时钟,设置 TIMx_SMCR 的 SMS=0000,与基本定时器一样,时钟源来自系统总线(APB)时钟,预分频器大于2时会*2 。外部时钟模式一,设置 TIMx_SMCR 的 SMS=1111,来自外部输入引脚(TIx),一共有三种信号:当时钟源来自边沿检测器前的信号(TI1F_ED信号),时钟源信号是一个双边沿检测信号,不论是上升沿还是下降沿都会触发计数器的计数,注意这个信号只对通道1有效。来自于边沿检测器后的信号(TI1FP1、TI2FP2信号),是一个单边沿检测信号,根据配置决定上升沿还是下降沿,对通道1和2都有效。这三种信号都可以设置是否滤波(TIMx_CCMR1的IC1F位),以及滤波器滤波的频率(TIMx_CR1的CDK位)。数字滤波器的详细说明:在设置好工作频率后,还会设置采样次数(N),只有在采样器以工作频率连续采样到N个与当前输出值不同的输入信号时,才会更新输出值,当 8 次采样中有高有低,那就保持原来的输出。比如N等于8,原本输出低电平,第一次采样信号为高电平,第二次采样为低电平,但此时输出仍是低电平,且这8次采样的结果不会计入下一轮采样;只有当9~16次采样信号全部都是高电平时输出才会是高电平。外部时钟模式2,设置 TIMx_SMCR 的 ECE=1,来自外部触发输入(TIMx_ETR);内部触发输入,来自其它定时器的控制器(TIRx,x=0、1、2、3)。设置方式参考F10XXX参考手册的定时器同步章节,各个TIR信号编码查看从模式控制寄存器(TIMx_SMCR)的TS位,关于每个定时器中ITRx的细节,参见参考手册的表78。
*//*------------------------输出比较部分介绍------------------------*/
/*捕获/比较预装载寄存器何时会将值装在到其影子寄存器里(三个条件必须同时满足):当CCR1寄存器未处于被操作状态时;当通道被配置未输出时;该寄存器没有配置缓冲使能或定时器产生更新事件时。捕获/比较通道的输出部分(以通道1为例):寄存器将比较结果传到输出控制模块,输出控制模块根据配置(CCMR1的OC1M位)产生输出信号(OC1REF信号,高电平有效),这个信号再根据配置(CCER的CC1P位)决定是否反转,并输出到IO引脚(OC1信号)。输出PWM原理:当计数器的值大于等于捕获/比较寄存器(CRR)的值时,输出逻辑为1,否则为0。那么我们通过设置计数周期以及重装载寄存器(ARR)的值就可以产生一个我们需要的波形。ARR越大,周期越大、频率越小;CRR则与占空比相关,具体看计数器的计数方式。*/ /*------------------------PWM模式介绍------------------------*/
/*PWM有两种模式。PWM模式一:CNT<CCR1是为有效电平(OC1REF=1),CNT>=CR1是为无效电平(OC1REF=0)。PWM模式二:与模式一相反。*//*---------------输入捕获测量脉宽信号原理---------------*/
/*
简单来说就是通过两次捕获事件时计数器值之差,再加上这段时间内溢出事件的次数,就可以计算两次捕获事件的间隔时间。具体过程:捕获到一个沿上升沿检测事件后,计数器清零,同时把检测改成沿下降沿检测。记录溢出事件发生的次数,发生第二次捕获事件时再次读取计数器的值,计算。公式:T=(N*(ARR+1)+CRR)*(psc+1)/Ft;T是持续时间,N为两次捕获事件期间的溢出次数,ARR为重装载值,CRR是第二次捕获事件时计数器值,psc是定时器预分频系数,Ft是定时器时钟源频率。
*/TIM_HandleTypeDef g_tim3_handle; /* 定时器3句柄 */
TIM_HandleTypeDef g_tim2_handle; /* 定时器2句柄 *//*** @brief       通用定时器TIMX定时中断初始化函数* @note*              通用定时器的时钟来自APB1,当PPRE1 ≥ 2分频的时候*              通用定时器的时钟为APB1时钟的2倍, 而APB1为36M, 所以定时器时钟 = 72Mhz*              定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.*              Ft=定时器工作频率,单位:Mhz** @param       arr: 自动重装值。* @param       psc: 时钟预分频数* @retval      无*/
void gtim_tim3_int_init(uint16_t arr, uint16_t psc)
{__HAL_RCC_TIM3_CLK_ENABLE();                                 /* 使能TIMx时钟 */g_tim3_handle.Instance = TIM3;                              /* 通用定时器x */g_tim3_handle.Init.Prescaler = psc;                         /* 预分频系数 */g_tim3_handle.Init.CounterMode = TIM_COUNTERMODE_UP;        /* 递增计数模式 */g_tim3_handle.Init.Period = arr;                            /* 自动装载值 */HAL_TIM_Base_Init(&g_tim3_handle);HAL_NVIC_SetPriority(TIM3_IRQn, 1, 3);                      /* 设置中断优先级,抢占优先级1,子优先级3 */HAL_NVIC_EnableIRQ(TIM3_IRQn);                              /* 开启ITMx中断 */HAL_TIM_Base_Start_IT(&g_tim3_handle);                      /* 使能定时器x和定时器x更新中断 */
}void gtim_tim2_int_init(uint16_t arr, uint16_t psc)
{__HAL_RCC_TIM2_CLK_ENABLE();                                 /* 使能TIMx时钟 */g_tim2_handle.Instance = TIM2;                              /* 通用定时器x */g_tim2_handle.Init.Prescaler = psc;                         /* 预分频系数 */g_tim2_handle.Init.CounterMode = TIM_COUNTERMODE_UP;        /* 递增计数模式 */g_tim2_handle.Init.Period = arr;                            /* 自动装载值 */HAL_TIM_Base_Init(&g_tim2_handle);HAL_NVIC_SetPriority(TIM2_IRQn, 1, 2);                      /* 设置中断优先级,抢占优先级1,子优先级3 */HAL_NVIC_EnableIRQ(TIM2_IRQn);                              /* 开启ITMx中断 */HAL_TIM_Base_Start_IT(&g_tim2_handle);                      /* 使能定时器x和定时器x更新中断 */
}/*** @brief       定时器中断服务函数* @param       无* @retval      无*/
void TIM3_IRQHandler(void)
{/* 直接通过判断中断标志位的方式 */if(__HAL_TIM_GET_FLAG(&g_tim3_handle, TIM_FLAG_UPDATE) != RESET){//LED0_TOGGLE();//HAL_TIM_IRQHandler(&g_tim3_handle); /* 定时器中断公共处理函数,在btim.c里定义的 如果取消注释会看见0和1一起闪烁 */__HAL_TIM_CLEAR_IT(&g_tim3_handle, TIM_IT_UPDATE);  /* 清除定时器溢出中断标志位 */}
}void TIM2_IRQHandler(void)
{/* 直接通过判断中断标志位的方式 */if(__HAL_TIM_GET_FLAG(&g_tim2_handle, TIM_FLAG_UPDATE) != RESET){//LED1_TOGGLE();//HAL_TIM_IRQHandler(&g_tim2_handle); __HAL_TIM_CLEAR_IT(&g_tim2_handle, TIM_IT_UPDATE);  /* 清除定时器溢出中断标志位 */}
}/*********************************以下是通用定时器PWM输出实验程序*************************************/TIM_HandleTypeDef g_timx_pwm_chy_handle;        /* 定时器x句柄 *//*** @brief       通用定时器TIMX 通道Y PWM输出 初始化函数(使用PWM模式1)* @note*              通用定时器的时钟来自APB1,当PPRE1 ≥ 2分频的时候*              通用定时器的时钟为APB1时钟的2倍, 而APB1为36M, 所以定时器时钟 = 72Mhz*              定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.*              Ft=定时器工作频率,单位:Mhz** @param       arr: 自动重装值。* @param       psc: 时钟预分频数* @retval      无*/
void gtim_timx_pwm_chy_init(uint16_t arr, uint16_t psc)
{TIM_OC_InitTypeDef timx_oc_pwm_chy  = {0};                          /* 定时器PWM输出配置 */g_timx_pwm_chy_handle.Instance = GTIM_TIMX_PWM;                     /* 定时器x */g_timx_pwm_chy_handle.Init.Prescaler = psc;                         /* 定时器分频 */g_timx_pwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;        /* 递增计数模式 */g_timx_pwm_chy_handle.Init.Period = arr;                            /* 自动重装载值 */HAL_TIM_PWM_Init(&g_timx_pwm_chy_handle);                           /* 初始化PWM */timx_oc_pwm_chy.OCMode = TIM_OCMODE_PWM1;                           /* 模式选择PWM1 */timx_oc_pwm_chy.Pulse = arr / 2;                                    /* 设置比较值,此值用来确定占空比 *//* 默认比较值为自动重装载值的一半,即占空比为50% */timx_oc_pwm_chy.OCPolarity = TIM_OCPOLARITY_HIGH;                    /* 输出比较极性为高 */HAL_TIM_PWM_ConfigChannel(&g_timx_pwm_chy_handle, &timx_oc_pwm_chy, GTIM_TIMX_PWM_CHY); /* 配置TIMx通道y */HAL_TIM_PWM_Start(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY);       /* 开启对应PWM通道 */
}/*** @brief       定时器底层驱动,时钟使能,引脚配置此函数会被HAL_TIM_PWM_Init()调用* @param       htim:定时器句柄* @retval      无*/
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{if (htim->Instance == GTIM_TIMX_PWM){GPIO_InitTypeDef gpio_init_struct;GTIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE();               /* 开启通道y的CPIO时钟 */GTIM_TIMX_PWM_CHY_CLK_ENABLE();gpio_init_struct.Pin = GTIM_TIMX_PWM_CHY_GPIO_PIN; /* 通道y的CPIO口 */gpio_init_struct.Mode = GPIO_MODE_AF_PP;           /* 复用推完输出 */gpio_init_struct.Pull = GPIO_PULLUP;               /* 上拉 */gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;     /* 高速 */HAL_GPIO_Init(GTIM_TIMX_PWM_CHY_GPIO_PORT, &gpio_init_struct);GTIM_TIMX_PWM_CHY_GPIO_REMAP();                    /* IO口REMAP设置, 是否必要查看头文件配置的说明 */}
}/*********************************通用定时器输入捕获实验程序*************************************/TIM_HandleTypeDef g_timx_cap_chy_handle;      /* 定时器x句柄 *//*** @brief       通用定时器TIMX 通道Y 输入捕获 初始化函数* @note*              通用定时器的时钟来自APB1,当PPRE1 ≥ 2分频的时候*              通用定时器的时钟为APB1时钟的2倍, 而APB1为36M, 所以定时器时钟 = 72Mhz*              定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.*              Ft=定时器工作频率,单位:Mhz** @param       arr: 自动重装值* @param       psc: 时钟预分频数* @retval      无*/
void gtim_timx_cap_chy_init(uint16_t arr, uint16_t psc)
{TIM_IC_InitTypeDef timx_ic_cap_chy = {0};g_timx_cap_chy_handle.Instance = GTIM_TIMX_CAP;                     /* 定时器5 */g_timx_cap_chy_handle.Init.Prescaler = psc;                         /* 定时器分频 */g_timx_cap_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;        /* 递增计数模式 */g_timx_cap_chy_handle.Init.Period = arr;                            /* 自动重装载值 */HAL_TIM_IC_Init(&g_timx_cap_chy_handle);timx_ic_cap_chy.ICPolarity = TIM_ICPOLARITY_RISING;                 /* 上升沿捕获 一共有三种模式RISING、FALLING、BOTHEDGE */timx_ic_cap_chy.ICSelection = TIM_ICSELECTION_DIRECTTI;             /* 映射到TI1上,即映射到通道1上;一共有三种模式DIRECTTI、INDIRECTTITRC,即TI1、YI2、TRC */timx_ic_cap_chy.ICPrescaler = TIM_ICPSC_DIV1;                       /* 配置输入分频,不分频 可以设置为每2、4、8个事件触发一次捕获,吧1改成对应数字即可*/timx_ic_cap_chy.ICFilter = 0;                                       /* 配置输入滤波器,不滤波。这个值可以是0~16之间的整数,对应16个模式见捕获/比较模式寄存器1(TIMx_CCMR1)的位7:4 */HAL_TIM_IC_ConfigChannel(&g_timx_cap_chy_handle, &timx_ic_cap_chy, GTIM_TIMX_CAP_CHY);  /* 配置TIM5通道1 */__HAL_TIM_ENABLE_IT(&g_timx_cap_chy_handle, TIM_IT_UPDATE);         /* 使能更新中断 */HAL_TIM_IC_Start_IT(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY);     /* 开始捕获TIM5的通道1 */
}/*** @brief       通用定时器输入捕获初始化接口HAL库调用的接口,用于配置不同的输入捕获* @param       htim:定时器句柄* @note        此函数会被HAL_TIM_IC_Init()调用* @retval      无*/
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{if (htim->Instance == GTIM_TIMX_CAP)                    /*输入通道捕获*/{GPIO_InitTypeDef gpio_init_struct;GTIM_TIMX_CAP_CHY_CLK_ENABLE();                     /* 使能TIMx时钟 */GTIM_TIMX_CAP_CHY_GPIO_CLK_ENABLE();                /* 开启捕获IO的时钟 */gpio_init_struct.Pin = GTIM_TIMX_CAP_CHY_GPIO_PIN;  /* 输入捕获的GPIO口 */gpio_init_struct.Mode = GPIO_MODE_AF_PP;            /* 复用推挽输出 */gpio_init_struct.Pull = GPIO_PULLDOWN;              /* 下拉 */gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;      /* 高速 */HAL_GPIO_Init(GTIM_TIMX_CAP_CHY_GPIO_PORT, &gpio_init_struct);HAL_NVIC_SetPriority(GTIM_TIMX_CAP_IRQn, 1, 3);     /* 抢占1,子优先级3 */HAL_NVIC_EnableIRQ(GTIM_TIMX_CAP_IRQn);             /* 开启ITMx中断 */}
}/* 输入捕获状态(g_timxchy_cap_sta)* [7]  :0,没有成功的捕获;1,成功捕获到一次.* [6]  :0,还没捕获到高电平;1,已经捕获到高电平了.* [5:0]:捕获高电平后溢出的次数,最多溢出63次,所以最长捕获值 = 63*65536 + 65535 = 4194303*       注意:为了通用,我们默认ARR和CCRy都是16位寄存器,对于32位的定时器(如:TIM5),也只按16位使用*       按1us的计数频率,最长溢出时间为:4194303 us, 约4.19秒**      (说明一下:正常32位定时器来说,1us计数器加1,溢出时间:4294秒)*/
uint8_t g_timxchy_cap_sta = 0;    /* 输入捕获状态 */
uint16_t g_timxchy_cap_val = 0;   /* 输入捕获值 *//*** @brief       定时器中断服务函数* @param       无* @retval      无*/
void GTIM_TIMX_CAP_IRQHandler(void)
{HAL_TIM_IRQHandler(&g_timx_cap_chy_handle);  /* 定时器HAL库共用处理函数 */
}/*** @brief       定时器输入捕获中断处理回调函数* @param       htim:定时器句柄指针* @note        该函数在HAL_TIM_IRQHandler中会被调用* @retval      无*/
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{   if (htim->Instance == GTIM_TIMX_CAP){LED1_TOGGLE();if ((g_timxchy_cap_sta & 0X80) == 0)                /* 还未成功捕获(第7位为0)(即上一次捕获还未完成) */{if (g_timxchy_cap_sta & 0X40)                   /* 已经捕获过上升沿了,这次捕获到的是一个下降沿(第6位为1) */{g_timxchy_cap_sta |= 0X80;                  /* 标记成功捕获到一次高电平脉宽 */g_timxchy_cap_val = HAL_TIM_ReadCapturedValue(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY);  /* 获取当前的捕获值 */TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY);                      /* 一定要先清除原来的设置 */TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY, TIM_ICPOLARITY_RISING); /* 配置TIM5通道1上升沿捕获 */}else /* 还未开始,第一次捕获上升沿 */{g_timxchy_cap_sta = 0;                              /* 清空 */g_timxchy_cap_val = 0;g_timxchy_cap_sta |= 0X40;                          /* 标记捕获到了上升沿 */__HAL_TIM_SET_COUNTER(&g_timx_cap_chy_handle, 0);   /* 定时器5计数器清零 */TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY);   /* 一定要先清除原来的设置!! */TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY, TIM_ICPOLARITY_FALLING); /* 定时器5通道1设置为下降沿捕获 */}}}
}/*** @brief       定时器更新中断回调函数* @param        htim:定时器句柄指针* @note        此函数会被定时器中断函数共同调用的* @retval      无*/
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if (htim->Instance == GTIM_TIMX_CAP){if ((g_timxchy_cap_sta & 0X80) == 0)            /* 还未成功捕获 */{if (g_timxchy_cap_sta & 0X40)               /* 已经捕获到高电平了 */{if ((g_timxchy_cap_sta & 0X3F) == 0X3F) /* 高电平太长了,已经累计满了 */{TIM_RESET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY);                     /* 一定要先清除原来的设置 */TIM_SET_CAPTUREPOLARITY(&g_timx_cap_chy_handle, GTIM_TIMX_CAP_CHY, TIM_ICPOLARITY_RISING);/* 配置TIM5通道1上升沿捕获 */g_timxchy_cap_sta |= 0X80;          /* 标记成功捕获了一次 */g_timxchy_cap_val = 0XFFFF;}else      /* 累计定时器溢出次数 */{g_timxchy_cap_sta++;}}}}
}

电机编码器(TIM3)

```c #ifndef __ENCODER_H #define __ENCODER_H #include "./SYSTEM/sys/sys.h" #include "./SYSTEM/delay/delay.h"

define ENCODER_TIM TIM3

define ENCODER_TIM_CLK_ENABLE() __HAL_RCC_TIM3_CLK_ENABLE()

define ENCODER_TIM_PERIOD 65535 /* 定时器溢出值 */

define ENCODER_TIM_PRESCALER 0 /* 定时器预分频值 */

/* 定时器3中断TIM3_IRQn、TIM3_IRQHandler在gtim里,不过没用到 */

void Encoder_Init(void);
int16_t Encoder_Get(void);

endif


```c
//#include "stm32f10x.h"                  // Device header
#include "./BSP/Encoder/Encoder.h"
/*** 函    数:编码器初始化* 参    数:无* 返 回 值:无*/
TIM_Encoder_InitTypeDef Encoder_ConfigStructure;
TIM_HandleTypeDef TIM_EncoderHandle;        /* 定时器3句柄 */void Encoder_Init(void)
{/*开启时钟*/__HAL_RCC_GPIOB_CLK_ENABLE();/*GPIO初始化*/GPIO_InitTypeDef gpio_init_struct;gpio_init_struct.Pin = GPIO_PIN_6;                      /* 引脚 */gpio_init_struct.Mode = GPIO_MODE_INPUT;                /* 输出 */gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;          /* 高速 *///gpio_init_struct.Alternate = ENCODER_TIM_CH1_GPIO_AF;    /* 设置复用 */HAL_GPIO_Init(GPIOA, &gpio_init_struct);                /* 初始化引脚 */gpio_init_struct.Pin = GPIO_PIN_7;                      /* 引脚 *///gpio_init_struct.Alternate = ENCODER_TIM_CH2_GPIO_AF;    /* 设置复用 */HAL_GPIO_Init(GPIOA, &gpio_init_struct);                /* 初始化引脚 *///将PA6和PA7引脚初始化为上拉输入/*时基单元初始化*/ENCODER_TIM_CLK_ENABLE();TIM_EncoderHandle.Instance = ENCODER_TIM;TIM_EncoderHandle.Init.Prescaler = ENCODER_TIM_PRESCALER;TIM_EncoderHandle.Init.CounterMode = TIM_COUNTERMODE_UP;TIM_EncoderHandle.Init.Period = ENCODER_TIM_PERIOD;TIM_EncoderHandle.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;TIM_EncoderHandle.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;/* 设置编码器倍频数 */Encoder_ConfigStructure.EncoderMode = TIM_ENCODERMODE_TI12;/* 编码器接口通道1设置 */Encoder_ConfigStructure.IC1Polarity = TIM_ICPOLARITY_RISING;Encoder_ConfigStructure.IC1Selection = TIM_ICSELECTION_DIRECTTI;Encoder_ConfigStructure.IC1Prescaler = TIM_ICPSC_DIV1;Encoder_ConfigStructure.IC1Filter = 0;/* 编码器接口通道2设置 */Encoder_ConfigStructure.IC2Polarity = TIM_ICPOLARITY_FALLING;Encoder_ConfigStructure.IC2Selection = TIM_ICSELECTION_DIRECTTI;Encoder_ConfigStructure.IC2Prescaler = TIM_ICPSC_DIV1;Encoder_ConfigStructure.IC2Filter = 0;/* 初始化编码器接口 */HAL_TIM_Encoder_Init(&TIM_EncoderHandle, &Encoder_ConfigStructure);/* 清零计数器 */__HAL_TIM_SET_COUNTER(&TIM_EncoderHandle, ENCODER_TIM_PERIOD/2);/* 清零中断标志位 */__HAL_TIM_CLEAR_IT(&TIM_EncoderHandle,TIM_IT_UPDATE);/* 使能定时器的更新事件中断 */__HAL_TIM_ENABLE_IT(&TIM_EncoderHandle,TIM_IT_UPDATE);/* 设置更新事件请求源为:定时器溢出 */__HAL_TIM_URS_ENABLE(&TIM_EncoderHandle);/* 设置中断优先级 */HAL_NVIC_SetPriority(TIM3_IRQn, 5, 1);/* 使能定时器中断 */HAL_NVIC_EnableIRQ(TIM3_IRQn);/* 使能编码器接口 */HAL_TIM_Encoder_Start(&TIM_EncoderHandle, TIM_CHANNEL_ALL);}/*** 函    数:获取编码器的增量值* 参    数:无* 返 回 值:自上此调用此函数后,编码器的增量值*/
int16_t Encoder_Get(void)
{/*使用Temp变量作为中继,目的是返回CNT后将其清零*/int16_t Temp;Temp = __HAL_TIM_GetCounter(&TIM_EncoderHandle)-ENCODER_TIM_PERIOD/2;__HAL_TIM_SetCounter(&TIM_EncoderHandle, ENCODER_TIM_PERIOD/2);return Temp;
}

需要注意的是江协在读取完编码器之后将计数器归零;但我在实际使用后发现这样会导致编码器只能朝一个方向转,反方向转时会导致定时器卡死。所以我重置值使用的是计数器最大值的一半。如果你没有这种问题,建议改回0 。

PID闭环控制实验(定速)

:::warning 附件如下,请下载后将后缀名改为zip解压缩。

01PID_Closed_loop_Control(speed).docx

:::

:::info
对于编码器读取周期,有如下几点:

  1. 编码器读取间隔越长,受波动影响越小,测速越准确;
  2. 编码器周期不宜过长,防止PID调整后更新不及时;
  3. 编码器周期不宜过短,会造成中间读取的数据被浪费。

所以,答案只有一个:编码器的周期应和PID调控周期保持一致

:::

:::info
由于前期项目的调控过程都是在定时器中断里实现的,因此我们可以直接把定时器中断移动到main.c里。

:::

//#include "./stm32f1xx_it.h"
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
//#include "./BSP/EXTI/exti.h"
#include "./BSP/KEY/key.h"
#include "./BSP/TIMER/gtim.h"
#include "./BSP/TIMER/atim.h"
#include "./BSP/OLED/OLED.h"
#include "./BSP/ADC/adc.h"
#include "string.h"
#include "./BSP/Motor/Motor.h"
#include "./BSP/Encoder/Encoder.h"//当前电机速度和位置
int16_t speed=0,location=0;
float Target = 0,Actual = 0,Out = 0;		//目标值,实际值,输出值
float Kp =4,Ki =2,Kd=0.3;		//比例项,积分项,微分项的权重
float Error0, Error1, ErrorInt;	//本次误差,上次误差,误差积分int main(void)
{HAL_Init();sys_stm32_clock_init(RCC_PLL_MUL9);delay_init(72);usart_init(1,115200);led_init();key_init();//extix_init();//toggle_exit_bool();OLED_Init();LED_ON();Timer_Init();RP_Init();Motor_Init();Encoder_Init();int key_v = 0;Motor_SetPWM(0);while (1){OLED_Clear();//OLED_Update();switch(Key_GetNum()){case 1:key_v+=60;break;case 2:key_v-=60;break;case 3:key_v=0;break;case 4:key_v=-key_v;break;}Kp = RP_GetValue(1)/4095.0*20;Ki = RP_GetValue(2)/4095.0*20;Kd = RP_GetValue(3)/4095.0*20;//Target = RP_GetValue(4)/4095.0*260 - 130 + key_v;Target = key_v;OLED_Printf(0,0,OLED_6X8,"ADC:%-4d",RP_GetValue(1));OLED_Printf(0,8,OLED_6X8,"TAR:%+04.0f",Target);OLED_Printf(0,16,OLED_6X8,"ACT:%+04.0f",Actual);OLED_Printf(0,24,OLED_6X8,"OUT:%+04.0f",Out);OLED_Printf(0,32,OLED_6X8,"EINT:%+04.0f",ErrorInt);OLED_Printf(60,0,OLED_6X8,"P:%02.1f",Kp);OLED_Printf(60,8,OLED_6X8,"I:%02.1f",Ki);OLED_Printf(60,16,OLED_6X8,"D:%02.1f",Kd);my_USART_printf(1,"%.0f,%.0f\n",Target,Actual);//OLED_ReverseArea(0,0,54,63);OLED_Reverse();//OLED_Update();//Motor_SetPWM((RP_GetValue(1)/4095.0)*2000-1000+Target);//Motor_SetPWM(Target);//my_USART_printf(1,"%d,%d,%d,%d\n",RP_GetValue(1),RP_GetValue(2),RP_GetValue(3),RP_GetValue(4));delay_ms(20);}}void TIM1_UP_IRQHandler()
{if(__HAL_TIM_GET_FLAG(&g_tim1_handle, TIM_FLAG_UPDATE) == SET){Key_Tick();static int8_t count=0;count++;if(count>20){count=0;OLED_Update();//speed = Encoder_Get();//location+=speed;Actual = Encoder_Get();Error1 = Error0;Error0 = Target - Actual;/*误差积分(累加)*/ErrorInt += Error0;/*积分限幅*/if(ErrorInt > 600){ErrorInt = 600;}if(ErrorInt < -600){ErrorInt = -600;}/*PID计算*/Out = Kp * Error0 + Ki * ErrorInt + Kd * (Error0 - Error1);/*输岀限幅*/if(Out > 1000){Out = 1000;}if(Out < -1000){Out = -1000;}/*执行控制*/Motor_SetPWM(Out);}__HAL_TIM_CLEAR_IT(&g_tim1_handle, TIM_IT_UPDATE);  /* 清除定时器溢出中断标志位 */}
}

:::info
如果遇到明明target很小电机却满速转的情况,可能是电机驱动方向或编码器极性设置反了。把两个里任意一个改反即可。

如果要改编码器方向,将第44行49行的RISING、FALLING转换即可。

:::

PID调参

PID一般按照P、I、D的顺序调参。

一般来说,P、I、D的数量级与输入输出的比值相关。对于我们来说,Actual的范围是±140(如果你设置的读取周期不同,那么这个值也会不同,以你实测电机满转速时的读取值为准),但我们的输出范围是±1000(也以你TIM2设置的重装载值和电机驱动的参数为准)输出比输入大了一个数量级,因此我们的PID参数也应该在10这一数量级左右。(如果大两个,那就是100,相同就是1)。

根据江协的调参过程,我们应:

  1. 使用串口将目标值、实际值打印到电脑上,通过SerialPlot软件接收、绘图;另将KI、KP、KD打印到OLED上方便观察。
  2. 首先将KP、KI、KD都归零,然后设置一个较大的目标值;
  3. 逐渐增大KP,直到系统出现抖动;
  4. 减小KP使系统不至于抖动,KI增大一个很小的值,然后迅速变化目标值,观察曲线变化;
  5. 逐渐增大KI,直到你对两条曲线的贴合程度满意为止。
  6. 观察曲线变化过程,如果发现曲线经常一头“扎过”目标值(即过调),则适当增加KD,使过调消失。如果没有这种倾向,保持KD为0。

正常情况下,参数越大,反应越迅速,但更容易出现抖动过冲等现象;参数越小,调控越缓慢,但调控过程也越平滑

我调完参后发现PID分别为5、4、0左右时效果最好效果如图。波形是手按按键反转目标值得到的,平均每秒反转1.5次。

积分饱和

在“位置式PID实现”的末尾,我们提了一嘴输出限幅和积分限幅。输出限幅是为了是PID给出的参数合法,那么积分限幅呢?

:::color1
在此之前,我们不妨设想一下这样的情景:电机的最大转速为140,但我们的目标值设定在了180。这种情况下,由于实际速度永远达不到目标速度,就会导致ErrorInt不停增大!

如果在这时,我们突然对目标值取相反数会怎么样?由于误差积分已经累积到一个很庞大的值,积分项的输出盖过了其它两项的输出,导致系统迟迟给不出合适的反应(也就是滞后性),直到误差积分重新接近合适的值才会“幡然醒悟”。

Q:为什么积分饱和后过一段时间会自行消除?

A:回到刚才的情景:我们目标值为180,实际值为140,误差为+40,因此误差积分会持续变大。

然后我们反转目标值,变成-180,此时实际值依然为140,误差为-320,所以误差积分会变小,直到误差维持在0左右才不变。

:::

上面的例子就说明了对积分进行限制是一件多么必要的事。它可以让系统兼具对稳态误差的弥补功能和对目标值变化的快速反应。

对于积分应该限制在什么范围内也是需要测量的。就作者而言,将目标值设定在一个接近最大转速的值上,观察此时积分的大小;然后将目标值取反,再次观察积分的大小。这两次观察的范围就是我们积分项所需要的范围了。不过为了留有余地,建议在这个范围上扩大一点。

如:根据我实际测量,发现积分始终处于270~-270之间,那么我们可以将积分限制在±500这个范围内(不能理解为乘2,而是加200)。

更多描述见下文"PID算法改进-积分限幅"

增量式PID

对照上文修改一下变量声明和输出公式就行,不做赘述。具体过程可从视频[1-4]节34分钟处查看。

PID闭环控制(定位)

:::warning 附件如下,请下载后将后缀名改为zip解压缩。

02PID_Closed_loop_Control(location).docx

:::

//#include "./stm32f1xx_it.h"
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
//#include "./BSP/EXTI/exti.h"
#include "./BSP/KEY/key.h"
#include "./BSP/TIMER/gtim.h"
#include "./BSP/TIMER/atim.h"
#include "./BSP/OLED/OLED.h"
#include "./BSP/ADC/adc.h"
#include "string.h"
#include "./BSP/Motor/Motor.h"
#include "./BSP/Encoder/Encoder.h"//当前电机速度和位置
int16_t speed=0,location=0;
float Target = 0,Actual = 0,Out = 0;		//目标值,实际值,输出值
float Kp =4,Ki =2,Kd=0.3;		//比例项,积分项,微分项的权重
float Error0, Error1, ErrorInt;	//本次误差,上次误差,误差积分int main(void)
{HAL_Init();sys_stm32_clock_init(RCC_PLL_MUL9);delay_init(72);usart_init(1,115200);led_init();key_init();//extix_init();//toggle_exit_bool();OLED_Init();LED_ON();Timer_Init();RP_Init();Motor_Init();Encoder_Init();int key_v = 0;Motor_SetPWM(0);while (1){OLED_Clear();//OLED_Update();switch(Key_GetNum()){case 1:key_v+=204;break;case 2:key_v-=204;break;case 3:key_v=0;break;case 4:key_v=-key_v;break;}Kp = RP_GetValue(1)/4095.0*20;Ki = RP_GetValue(2)/4095.0*20;Kd = RP_GetValue(3)/4095.0*20;Target = RP_GetValue(4)/4095.0*1832 - 916 + key_v;//Target = key_v;OLED_Printf(0,0,OLED_6X8,"ADC:%-4d",RP_GetValue(1));OLED_Printf(0,8,OLED_6X8,"TAR:%+04.0f",Target);OLED_Printf(0,16,OLED_6X8,"ACT:%+04.0f",Actual);OLED_Printf(0,24,OLED_6X8,"OUT:%+04.0f",Out);OLED_Printf(0,32,OLED_6X8,"EINT:%+04.0f",ErrorInt);OLED_Printf(60,0,OLED_6X8,"P:%02.1f",Kp);OLED_Printf(60,8,OLED_6X8,"I:%02.1f",Ki);OLED_Printf(60,16,OLED_6X8,"D:%02.1f",Kd);my_USART_printf(1,"%.0f,%.0f,%.0f\n",Target,Actual,Out);//OLED_ReverseArea(0,0,54,63);OLED_Reverse();//OLED_Update();//Motor_SetPWM((RP_GetValue(1)/4095.0)*2000-1000+Target);//Motor_SetPWM(Target);//my_USART_printf(1,"%d,%d,%d,%d\n",RP_GetValue(1),RP_GetValue(2),RP_GetValue(3),RP_GetValue(4));delay_ms(20);}}void TIM1_UP_IRQHandler()
{if(__HAL_TIM_GET_FLAG(&g_tim1_handle, TIM_FLAG_UPDATE) == SET){Key_Tick();static int8_t count=0;count++;if(count>20){count=0;OLED_Update();speed = Encoder_Get();location+=speed;Actual = location;Error1 = Error0;Error0 = Target - Actual;/*误差积分(累加)*/ErrorInt += Error0;/*积分限幅*/if(ErrorInt > 600){ErrorInt = 600;}if(ErrorInt < -600){ErrorInt = -600;}/*PID计算*/Out = Kp * Error0 + Ki * ErrorInt + Kd * (Error0 - Error1);/*输岀限幅*/if(Out > 1000){Out = 1000;}if(Out < -1000){Out = -1000;}/*执行控制*/Motor_SetPWM(Out);}__HAL_TIM_CLEAR_IT(&g_tim1_handle, TIM_IT_UPDATE);  /* 清除定时器溢出中断标志位 */}
}

只不过是把Actual和Target的值替换为位置而已,没什么好讲的。

:::info
不过我发现在电机位置的控制中,KI项会大大增加过冲幅度,却对于削减稳定误差没什么帮助(因为电机处于平放状态下,没有外力作用,自然条件下就倾向于维持位置不变,因此该系统没有稳态误差;若电机连接轮子并且放在斜面上,就会存在稳态误差了),因此我将KI项置0 。(这个问题可以用积分分离来解决)

江协在视频中提到了另一种观点:有时由于out值过小,不足以驱动电机旋转,导致电机实际位置可能会与目标位置有一点微小的偏移。有人把这种误差也视作稳态误差,但江协把这两种误差分开讨论,后一种称为调控误差,因为调控误差的方向不确定,不符合前面稳态误差的定义。

:::

我调完参后发现PID分别为6.2、0、2.3左右时效果最好。

增量式PID

在视频中,江协演示了增量式PID调控位置的一个错误:

由于增量式PID的输出值为累加out,因此该调控方式受到上一次out的影响;

对于纯P项调控,当目标值变化缓慢时,out值相差不大,可以被视为“正确的”,经过P项修补后没问题;

当目标值快速变化时,上一次out对于当前的目标值不再“正确”,或out受到噪声干扰发送错误;那么仅靠P项很难纠正这种错误(因为P项与历史无关,所以上一次out的错误不能被纠正,P只是在一个错误的基础上修修补补而已,结果自然也是错误的)。

可以加入I项来弥补这个缺点。

所以增量式PID最好不要给KI设置为0 。

PID算法改进

PID的改进措施有很多:
  1. 积分限幅:限制积分的幅度,防止积分深度饱和
  2. 积分分离:误差小于一个限度才开始积分,反之则去掉积分部分
  3. 变速积分:根据误差的大小调整积分的速度
  4. 微分先行:将对误差的微分替换为对实际值的微分
  5. 不完全微分:给微分项加入一阶惯性单元(低通滤波器)
  6. 输出偏移:在非0输出时,给输出值加一个固定偏移
  7. 输入死区:误差小于一个限度时不进行调控

积分限幅

:::warning 附件如下,请下载后将后缀名改为zip解压缩。

03PID_Integral_limiter.docx

:::

要解决的问题

如果执行器因为卡住、断电、损坏等原因不能消除误差,则误差积分会无限制加大,进而达到深度饱和状态,此时PID控制器会持续输出最大的调控力,即使后续执行器恢复正常,PID控制器在短时间内也会维持最大的调控力,直到误差积分从深度饱和状态退出。

积分限幅实现思路

对误差积分或积分项输出进行判断,如果幅值超过指定阈值,则进行限制。

程序实现

有两种方法:一个是对误差积分项进行限幅,另一个则是对积分项进行限幅
    Actual = Encoder_Get();Error1 = Error0;Error0 = Target - Actual;/*误差积分(累加)*/ErrorInt += Error0;/*积分限幅*/if(ErrorInt > 800){ErrorInt = 800;}if(ErrorInt < -800){ErrorInt = -800;}/*PID计算*/Out = Kp * Error0 + Ki * ErrorInt + Kd * (Error0 - Error1);/*输岀限幅*/if(Out > 1000){Out = 1000;}if(Out < -1000){Out = -1000;}/*执行控制*/Motor_SetPWM(Out);
    Actual = Encoder_Get();Error1 = Error0;Error0 = Target - Actual;/*误差积分(累加)*/ErrorInt += Error0;InOut = Ki * ErrorInt;/*积分项限幅*/if(InOut > 1000){InOut = 1000;}if(InOut < -1000){InOut = -1000;}/*PID计算*/Out = Kp * Error0 + InOut + Kd * (Error0 - Error1);/*输岀限幅*/if(Out > 1000){Out = 1000;}if(Out < -1000){Out = -1000;}/*执行控制*/Motor_SetPWM(Out);

两种方法的好处和坏处:

对积分进行限幅:可以防止积分饱和,使积分不会因为误差的累计而无限增大,变相控制了积分项的输出。缺点是对于不同的KI,积分范围也是不同的,如果KI需要经常改变的话调试起来很麻烦。

对积分项进行限幅:不论KI等于多少,只要规定了整个积分项的最大值就不怕积分饱和了。但这种方法实际上并没有解决积分饱和的问题,只是减小了这个问题的影响。如果积分深度饱和,积分项还是会一直输出最大值,直到积分恢复正常。

如何得到限幅范围

除了在[上文](#r8eo9)里提到的实际测量的情况外,还可以通过计算得出合理的积分范围。

我们假设最坏情况:输出完全由积分组成。在这种情况下,积分 = 输出/Ki;

也就是说,积分始终不会超过这个范围。

:::warning
若使用这种方法,请特别注意Ki等于0的情况。可以额外加一个判断:若Ki等于0,则不进行积分。

就像这样:

其中FLT_MIN表示float型变量能表示的最小正数,需要引用float.h文件。

不过这么一来又有新的问题:我们使用电位器来控制三个参数,而电位器的读数不是那么稳定的。这种情况下可以用参数有意义的最小值来判断:把FLT_MIN改成0.1。

:::

积分分离

:::warning 附件如下,请下载后将后缀名改为zip解压缩。

04PID_integral_Separation.docx

:::

要解决的问题

积分项作用一般位于调控后期,用来消除持续的误差,调控前期一般误差较大且不需要积分项作用,如果此时仍然进行积分,则调控进行到后期时,积分项可能已经累积了过大的调控力,这会导致超调。(这就是在[PID闭环控制(定位)](#ebQhr)里遇到的问题。)

由于误差太小,此时P项输出的调控力不足以让电机转动,导致实际值和目标值始终差一点

积分分离实现思路

对误差大小进行判断,如果误差绝对值小于指定阈值,则加入积分项作用,反之,则直接将误差积分清零或不加入积分项作用。

程序实现

有两种实现方法:一种是当误差大于限度时,保持误差积分为0;另一种时积分照旧,但只有误差小于限度时积分项才会参与运算。
Actual = location;
Error1 = Error0;
Error0 = Target - Actual;/*误差积分(累加)*/
ErrorInt += Error0;/* 积分分离 */
if(fabs(Error0)>10){ErrorInt=0;
}/*PID计算*/
Out = Kp * Error0 + Ki * ErrorInt + Kd * (Error0 - Error1);/*输岀限幅*/
if(Out > 1000){Out = 1000;}
if(Out < -1000){Out = -1000;}/*执行控制*/
Motor_SetPWM(Out);
uint8_t c = 0;  /*这个变量声明放在合适的位置*/Actual = location;
Error1 = Error0;
Error0 = Target - Actual;/*误差积分(累加)*/
ErrorInt += Error0;/* 积分分离 */
if(fabs(Error0)>10){c=0;
}
else{c=1;
}/*积分限幅*/
if(ErrorInt > 1000/Ki){ErrorInt = 1000/Ki;}
if(ErrorInt < -1000/Ki){ErrorInt = -1000/Ki;}/*PID计算*/
Out = Kp * Error0 + c * Ki * ErrorInt + Kd * (Error0 - Error1);/*输岀限幅*/
if(Out > 1000){Out = 1000;}
if(Out < -1000){Out = -1000;}/*执行控制*/
Motor_SetPWM(Out);
展开查看:相同参数下两种积分分离前后对比(目标值 实际值 输出值

![第一种方法。在用手将转盘固定在一个非常小的误差时,输出值会不断增大,甚至达到上限。](https://cdn.nlark.com/yuque/0/2025/png/40561973/1750074865256-3ee3533e-695a-47b1-b2f5-bc868ae570b9.png)

变速积分

:::warning 附件如下,请下载后将后缀名改为zip解压缩。

05_PID_Changing_Integration_Rate.docx

:::

要解决的问题

如果积分分离阈值没有设定好,被控对象正好在阈值之外停下来,则此时控制器完全没有积分作用,误差不能消除

变速积分实现思路

变速积分是积分分离的升级版,变速积分需要设计一个函数值随误差绝对值增大而减小的函数,函数值作为调整系数,用于调整误差积分的速度或积分项作用的强度

:::info
变速积分是在积分分离方法二的基础上发展而来的。在方法二中由于积分一直累计,倒置积分项参与运算时会产生突变形成抖动;因此不妨将这种突变给减缓,让积分项从0一点点参与进运算里。

:::

:::warning
变速积分只是要求再积分前和积分后加一个缓冲函数,这个函数可以是线性的,也可以是非线性的,甚至可以是奇奇怪怪的形状。

变速积分没有抗积分饱和的效果。

:::

程序实现

```c /* 积分变速,k、c在合适的位置定义,c可以先设置为1,但需要实际调试才能确定合适的值 */ k = 1 / ( c*fabs(Error0) + 1 );

/误差积分(累加)/
ErrorInt += k *Error0;

/PID计算/
Out = Kp * Error0 + Ki * ErrorInt + Kd * (Error0 - Error1);

/输岀限幅/
if(Out > 1000){Out = 1000;}
if(Out < -1000){Out = -1000;}

/执行控制/
Motor_SetPWM(Out);


```c
/* 积分变速 */
k = 1 / ( c*fabs(Error0) + 1 );/*误差积分(累加)*/
ErrorInt += Error0;/*PID计算*/
Out = Kp * Error0 + k * Ki * ErrorInt + Kd * (Error0 - Error1);/*输岀限幅*/
if(Out > 1000){Out = 1000;}
if(Out < -1000){Out = -1000;}/*执行控制*/
Motor_SetPWM(Out);
两种方式的效果(目标值 实际值 输出值

:::warning 变速积分会导致误差回正的时间变长,如图:

用手给它一个恒定的误差,再松手,可以看到回正需要较长时间

:::

微分先行

:::warning 附件如下,请下载后将后缀名改为zip解压缩。

06_PID_Differential_Leading.docx

:::

要解决的问题

普通PID的微分项对误差进行微分,当目标值大幅度跳变时,误差也会瞬间大幅度跳变,这会导致微分项突然输出一个很大的调控力,如果系统的目标值频繁大幅度切换,则此时的微分项不利于系统稳定。

微分先行实现思路

将对误差的微分替换为对实际值的微分。

微分先行PID框图

:::tips
普通PID的微分项输出:

$ 𝑑𝑜𝑢𝑡(𝑘)=𝐾_𝑑∗(𝑒𝑟𝑟𝑜𝑟(𝑘)−𝑒𝑟𝑟𝑜𝑟(𝑘−1)) $

微分先行PID的微分项输出:

$ 𝑑𝑜𝑢𝑡(𝑘)=−𝐾_𝑑∗(𝑎𝑐𝑡𝑢𝑎𝑙(𝑘)−𝑎𝑐𝑡𝑢𝑎𝑙(𝑘−1)) $

:::

可以看到原本微分项要在计算误差后才会被计算,现在被提前了,因此这种方法叫微分先行。

你可能会在别的地方看到更复杂的公式。这是由于他们在微分先行的同时加了滤波器。

:::info
为什么微分先行要加负号呢?在此之前我们先回顾一下微分项的作用:阻碍系统的实际值产生变化。

假设当前实际值比目标值低。当我们计算误差时,实际值向上变化,误差值向下变化,两者斜率符号相反。现在我们计算的是实际值了,要还想相反就需要加上负号。

:::

程序实现

为了调试更直观,我们将微分项结果单独存入一个变量中,并通过串口打印出来。
/*Actual1需要另外定义*/Actual1 = Actual;
Actual = location;
Error1 = Error0;
Error0 = Target - Actual;/*误差积分(累加)*/
ErrorInt += Error0;
/*积分限幅*/
if(ErrorInt > 600){ErrorInt = 600;}
if(ErrorInt < -600){ErrorInt = -600;}/*PID计算*/
Difout = Kd * (Actual - Actual1);
Out = Kp * Error0 + Ki * ErrorInt + Difout;/*输岀限幅*/
if(Out > 1000){Out = 1000;}
if(Out < -1000){Out = -1000;}/*执行控制*/
Motor_SetPWM(Out);
前后对比(目标值 实际值 输出值 微分输出值

:::warning 加入微分先行后,在某种程度上会减慢调控的速度。因为在目标值突变后,P 项正输出,D 项因为突变也有个正输出,实际上是加强了 P 项的输出,使变化更迅速。但微分先行后,D 项无论何时都只会给出负输出,减缓了调控速度。

且对于电机控制来说,D 项的尖峰没有不良影响,因此不需要使用微分先行。

:::

不完全微分

:::warning 附件如下,请下载后将后缀名改为zip解压缩。

07_PID_Incomplete_Derivation.docx

:::

要解决的问题

传感器获取的实际值经常会受到噪声干扰,而PID控制器中的微分项对噪声最为敏感,这些噪声干扰可能会导致微分项输出抖动,进而影响系统性能。

不完全微分实现思路

给微分项加入一阶惯性单元(低通滤波器)。

不完全微分PID框图。

:::tips
普通PID的微分项输出:

$ 𝑑𝑜𝑢𝑡(𝑘)=𝐾_𝑑∗(𝑒𝑟𝑟𝑜𝑟(𝑘)−𝑒𝑟𝑟𝑜𝑟(𝑘−1)) $

不完全微分PID的微分项输出:

$ 𝑑𝑜𝑢𝑡(𝑘)=(1−𝛼)∗𝐾_𝑑∗(𝑒𝑟𝑟𝑜𝑟(𝑘)−𝑒𝑟𝑟𝑜𝑟(𝑘−1))+𝛼∗𝑑𝑜𝑢𝑡(𝑘−1) $

:::

:::info
其实直接对实际值加滤波也可以,等同于同时对 PID 三项加滤波。但在一些对响应要求比较迅速的项目中就不太合适了。同时受噪声干扰最大的就是 D 项(顺便一提,最小的是 I 项),因此只给 D 项加滤波。

一阶惯性单元就是本次应有的输出和上次输出取加权平均,得到本次实际输出:

$ y(k) = (1-α)x+αy(k-1) $

实际上也可以使用别的滤波器,不过谨记:滤波效果越强,原波形和现波形相位差越大,即延迟越大。

:::

程序实现

```c //#include "./stm32f1xx_it.h" #include "./SYSTEM/sys/sys.h" #include "./SYSTEM/usart/usart.h" #include "./SYSTEM/delay/delay.h" #include "./BSP/LED/led.h" //#include "./BSP/EXTI/exti.h" #include "./BSP/KEY/key.h" #include "./BSP/TIMER/gtim.h" #include "./BSP/TIMER/atim.h" #include "./BSP/OLED/OLED.h" #include "./BSP/ADC/adc.h" #include "string.h" #include "./BSP/Motor/Motor.h" #include "./BSP/Encoder/Encoder.h" #include "stdlib.h"

//当前电机速度和位置
int16_t speed=0,location=0;
float Target = 0,Actual = 0,Out = 0; //目标值,实际值,输出值
float Kp =4,Ki =2,Kd=0.3; //比例项,积分项,微分项的权重
float Error0, Error1, ErrorInt; //本次误差,上次误差,误差积分

float Difout=0;

int main(void)
{
HAL_Init();
sys_stm32_clock_init(RCC_PLL_MUL9);
delay_init(72);

usart_init(1,115200);led_init();key_init();OLED_Init();LED_ON();Timer_Init();RP_Init();Motor_Init();Encoder_Init();int key_v = 0;Motor_SetPWM(0);
while (1)
{OLED_Clear();//OLED_Update();switch(Key_GetNum()){case 1:key_v+=204;break;case 2:key_v-=204;break;case 3:key_v=0;break;case 4:key_v=-key_v;break;}Kp = RP_GetValue(1)/4095.0*20;Ki = RP_GetValue(2)/4095.0*20;Kd = RP_GetValue(3)/4095.0*20;//Target = RP_GetValue(4)/4095.0*1832 - 916 + key_v;Target = key_v;OLED_Printf(0,0,OLED_6X8,"ADC:%-4d",RP_GetValue(1));OLED_Printf(0,8,OLED_6X8,"TAR:%+04.0f",Target);OLED_Printf(0,16,OLED_6X8,"ACT:%+04.0f",Actual);OLED_Printf(0,24,OLED_6X8,"OUT:%+04.0f",Out);OLED_Printf(0,32,OLED_6X8,"EINT:%+04.0f",ErrorInt);OLED_Printf(60,0,OLED_6X8,"P:%02.1f",Kp);OLED_Printf(60,8,OLED_6X8,"I:%02.1f",Ki);OLED_Printf(60,16,OLED_6X8,"D:%02.1f",Kd);my_USART_printf(1,"%.0f,%.0f,%.0f,%.0f\n",Target,Actual,Out,Difout);//OLED_ReverseArea(0,0,54,63);OLED_Reverse();//OLED_Update();        delay_ms(20);
}

}

define Alpha 0.5

void TIM1_UP_IRQHandler()
{

if(__HAL_TIM_GET_FLAG(&g_tim1_handle, TIM_FLAG_UPDATE) == SET)
{Key_Tick();static int8_t count=0;count++;if(count>20){count=0;OLED_Update();speed = Encoder_Get();location+=speed;Actual = location;Error1 = Error0;Error0 = Target - Actual;Actual += rand()%41-20; //加入随机噪声/*误差积分(累加)*/ErrorInt += Error0;/*积分限幅*/if(ErrorInt > 600){ErrorInt = 600;}if(ErrorInt < -600){ErrorInt = -600;}/*PID计算*/Difout = Alpha * Kd * (Error0 - Error1) + (1 - Alpha) * Difout;Out = Kp * Error0 + Ki * ErrorInt + Difout;/*输岀限幅*/if(Out > 1000){Out = 1000;}if(Out < -1000){Out = -1000;}/*执行控制*/Motor_SetPWM(Out);}__HAL_TIM_CLEAR_IT(&g_tim1_handle, TIM_IT_UPDATE);  /* 清除定时器溢出中断标志位 */
}

}


<details class="lake-collapse"><summary id="ufd273428"><span class="ne-text">前后对比(</span><span class="ne-text" style="color: #DF2A3F">目标值</span><span class="ne-text"> </span><span class="ne-text" style="color: #8CCF17">实际值</span><span class="ne-text"> </span><span class="ne-text" style="color: #0C68CA">输出值 </span><span class="ne-text" style="color: #ED740C">微分输出值</span><span class="ne-text">)</span></summary><p id="u7d7891b8" class="ne-p" style="text-align: center"><img src="https://cdn.nlark.com/yuque/0/2025/png/40561973/1750251971257-4ecd9e69-e1b0-4713-b324-9781f0c0a3ea.png" width="796.7320410422788" id="u9f85a62b" class="ne-image"></p><p id="u0fa4d9bc" class="ne-p" style="text-align: center"><img src="https://cdn.nlark.com/yuque/0/2025/png/40561973/1750252086960-d21522dd-89a2-40c1-b3cf-f6bbe27993e5.png" width="418.30066141678293" id="u71a70201" class="ne-image"></p><p id="u8dec6da8" class="ne-p" style="text-align: center"><img src="https://cdn.nlark.com/yuque/0/2025/png/40561973/1750252132344-d36a2213-67d6-4cc7-89d2-df0da4b6c659.png" width="312.41830649565975" id="ud83a8d96" class="ne-image"></p><p id="u4350a6fc" class="ne-p" style="text-align: center"><img src="https://cdn.nlark.com/yuque/0/2025/png/40561973/1750252176376-dcf0a7c7-5e1e-4962-97d4-bc7ed9e2d226.png" width="890.1960950775913" id="u4804ad70" class="ne-image"></p></details>
<h2 id="hc3t7">输出偏移</h2>
:::warning
本节和下面输入死区共用一个项目。:::<h3 id="yP1uk">要解决的问题</h3>
对于一些启动需要一定力度的执行器,若输出值较小,执行器可能完全无动作,这可能会引起调控误差,同时会降低系统响应速度。<h3 id="SR5mu">输出偏移实现思路</h3>
若输出值为0,则正常输出0,不进行调控;若输出值非0,则给输出值加一个固定偏移,跳过执行器无动作的阶段。:::tips
输出偏移的PID输出值:$ out(k) =
\begin{cases}
0, & out(k)=0\\
out(k)+offset, & out(k)>0\\
out(k) - offset,& out(k)<0
\end{cases} $::::::info
通过实测我的套件在 out =41 的情况下正好正向稳定转动,out=-42 的情况下正好反向转动(41 也能转但是断断续续的):::<h3 id="AhAPc">程序实现</h3>
```c
/*PID计算*/
Out = Kp * Error0 + Ki * ErrorInt + Kd * (Error0 - Error1);/* 输出偏移 */
if(Out>1){Out+=40;
}
else if(Out<-1){Out-=40;
}
else if(Out<1&&Out>-1){Out=0;
}/*输岀限幅*/
if(Out > 1000){Out = 1000;}
if(Out < -1000){Out = -1000;}
效果(目标值 实际值 输出值

输入死区

:::warning 附件如下,请下载后将后缀名改为zip解压缩。

08_PID_Output_Offset_&Input_Dead_Zone.docx

:::

要解决的问题

在某些系统中,输入的目标值或实际值有微小的噪声波动,或者系统有一定的滞后,这些情况可能会导致执行器在误差很小时频繁调控,不能最终稳定下来。

:::color1
如果你像江协一样用电位器来控制目标值,应该会对这个问题深有体会。

(在上面的实验中为了保证目标值的稳定我都是用按键来控制目标值的)

:::

输入死区实现思路

若误差绝对值小于一个限度,则固定输出0,不进行调控

:::tips
输入死区的PID输出值:

$ out(k) =
\begin{cases}
0, & |ERROR(k)|<A\
out(k), & |ERROR(k)|>A
\end{cases} $

:::

程序实现

```c /* 输出偏移&输入死区 */ if(fabs(Error0)<5){Out=0; } else{if(Out>0.1){Out+=41;}else if(Out<-0.1){Out-=41;}else if(Out<0.1&&Out>-0.1){Out=0;} } ```
前后对比(目标值 实际值 输出值

:::info 输出偏移和输入死区做的事是在不引入 I 项的情况下实现较小的误差,从而规避 I 项的滞后性。

:::

双环 PID

单环PID只能对被控对象的一个物理量进行闭环控制,而当用户需要对被控对象的多个维度物理量(例如:速度、位置、角度等)进行控制时,则需要多个PID控制环路,即多环PID,多个PID串级连接,因此也称作串级PID。

多环PID相较于单环PID,功能上,可以实现对更多物理量的控制,性能上,可以使系统拥有更高的准确性、稳定性和响应速度。

双环串级PID图。

双环 PID 基本原理

为什么要使用双环 PID

:::color1 回想我们上面定位置控制的几个改进方法:输出偏移和输入死区,都是由于位置控制器输出的 PWM 波太小不足以驱动电机而造成的。为什么定速控制没有这个问题?因为定速控制会自动加大 PWM 来产生一个足以驱动变化的力。

那么我们只要让位置环根据位置输出速度,速度环接受速度调控电机就好了,不会出现驱动力不足的情况。这也是为什么多环 PID 通常在性能上更好的原因。(这里的性能不是指计算性能,而是指调控速度、调控效果等)

双环PID控制位置示意图

除此之外,多环 PID 抗干扰也更强。还是用上面的例子,如果我们将转盘位置用手转动,此时速度不为 0,因此速度环会输出负向力抵抗;同时,由于位置发生改变,位置环也会输出一个复速度给速度环,从而加大速度环的输出;所以双环实际上会产生更大的阻尼来阻止意料外的变动。

不仅如此,只要我们对外环的输出进行限幅,就可以达到类似“以指定速度旋转到指定位置”的效果

:::

确定调控周期 T

除了与单环 PID 周期相同的注意事项外,还要注意外环的调控周期不得小于内环,否则外环更新的目标值内环接收不到,属于无效调控;

另外,内环一般用于调控变化较快的物理量,外环用于变化较慢的物理量,所以一般内环的调控周期比外环快。

调参

由于内环可以独立工作,所以一般先使内环独立工作,调节内环,在套上外环,调节外环。

程序实现

:::warning 附件如下,请下载后将后缀名改为zip解压缩。

09_PID_Double_Closed_loop_Control(location).docx

:::

在实现双环 PID 之前,我们先将 PID 控制逻辑封装一下。

:::warning
本文的封装和江协不太一样。我的封装将上面那些算法改进全部包含在内并使用标志位的形式让用户选择使用。简单来说,此封装比江协封装功能更全面,但用法上也更麻烦。如果你什么改进措施都不要的话,那使用方法和江协几乎一样。

:::

#ifndef PID_H
#define PID_H#include "./SYSTEM/sys/sys.h"
#include "math.h"
#include "stdbool.h"
#include "float.h"
#include "stdlib.h"
#include "string.h"#define PID_OK      1
#define PID_ERROR   0
#define PID_RESET   0
#define PID_SET     1
#define PID_OPT_II  1<<0    //积分限幅选项
#define PID_OPT_IS  1<<1    //积分分离,作用于积分值
#define PID_OPT_CIR 1<<2    //变速积分,作用于积分速度
#define PID_OPT_DL  1<<3    //微分先行
#define PID_OPT_ID  1<<4    //不完全微分
#define PID_OPT_OO  1<<5    //输出偏移
#define PID_OPT_IDZ 1<<6    //输入死区#define __PID_GET_OPTION(__HANDLE__,__FLAG__)    (((__HANDLE__)->Option &(__FLAG__)) == (__FLAG__))typedef struct {float Target,Actual_l,Actual_n,Out;   //目标,上一次的实际值,这一次实际值,输出float Kp,Ki,Kd;                     //比例项,积分项,微分项的权重float Error_n, Error_l, ErrorInt;     //本次误差,上次误差,误差积分float Difout_n,Difout_l;            //这次微分项值,上次值uint8_t Option;                     //选择哪些优化float ID_A;         //不完全微分参数float IDZ_A;        //输入死区参数float OO_UP,OO_DOWN;//输出偏移参数float CIR_C;        //变速积分参数float IS_A;         //积分分离参数float II_MAX,II_MIN;//积分限幅参数,上下限float Out_max,Out_min;//输出限幅
}PID_t;//初始化结构体
void PID_InitStruct(PID_t* s);
//添加算法改进选项
void PID_Enable_Opt(PID_t* s,uint8_t opt);
//设置目标值
void PID_SetTarget(PID_t* s,float tar);
//更新实际值
void PID_UpdateActual(PID_t* s,float act);
//计算结果
void PID_UpdateOut(PID_t* s);#endif
#include "PID.h"//初始化结构体
void PID_InitStruct(PID_t* s){memset(s,0,sizeof(PID_t));
}//添加算法改进选项
void PID_Enable_Opt(PID_t* s,uint8_t opt){s->Option|=opt;
}//设置目标值
void PID_SetTarget(PID_t* s,float tar){s->Target = tar;
}//更新实际值
void PID_UpdateActual(PID_t* s,float act){s->Actual_l = s->Actual_n;s->Actual_n = act;
}//计算结果
void PID_UpdateOut(PID_t* s){float k=1;s->Error_l = s->Error_n;s->Error_n = s->Target - s->Actual_n;/*变速积分(可选)*/if(__PID_GET_OPTION(s,PID_OPT_CIR) != PID_RESET){k = 1/( s->CIR_C*fabs(s->Error_n) + 1 );}/*误差积分(累加)*/s->ErrorInt += k * s->Error_n;/*积分分离(可选)*/if(__PID_GET_OPTION(s,PID_OPT_IS) != PID_RESET){if(fabs(s->Error_n)>s->IS_A)s->ErrorInt=0;}/*积分限幅(可选)*/if(__PID_GET_OPTION(s,PID_OPT_II) != PID_RESET){if(s->ErrorInt > s->II_MAX){s->ErrorInt = s->II_MAX;}if(s->ErrorInt < s->II_MIN){s->ErrorInt = s->II_MIN;}}/*计算微分项*/s->Difout_l = s->Difout_n;s->Difout_n = s->Kd * (s->Error_n - s->Error_l);/*微分先行(可选)*/if(__PID_GET_OPTION(s,PID_OPT_DL) != PID_RESET){s->Difout_n = s->Kd * (s->Actual_n - s->Actual_l);}/*不完全微分(可选)*/if(__PID_GET_OPTION(s,PID_OPT_ID) != PID_RESET){s->Difout_n = (1-s->ID_A) * s->Difout_n + s->ID_A * s->Difout_l;}/*PID计算*/s->Out = s->Kp * s->Error_n + s->Ki * s->ErrorInt + s->Difout_n;/*输入死区(可选)*/if(__PID_GET_OPTION(s,PID_OPT_IDZ) != PID_RESET){if(fabs(s->Error_n)<s->IDZ_A){s->Out=0;}}/*输出偏移(可选)*/if(__PID_GET_OPTION(s,PID_OPT_OO) != PID_RESET){if(s->Out>0.5){s->Out+=s->OO_UP;}else if(s->Out<-0.5){s->Out-=s->OO_DOWN;}else{s->Out=0;}}/*输岀限幅*/if(s->Out > s->Out_max){s->Out = s->Out_max;}if(s->Out < s->Out_min){s->Out = s->Out_min;}
}
//#include "./stm32f1xx_it.h"
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
//#include "./BSP/EXTI/exti.h"
#include "./BSP/KEY/key.h"
#include "./BSP/TIMER/gtim.h"
#include "./BSP/TIMER/atim.h"
#include "./BSP/OLED/OLED.h"
#include "./BSP/ADC/adc.h"
#include "string.h"
#include "./BSP/Motor/Motor.h"
#include "./BSP/Encoder/Encoder.h"
#include "PID.h"//当前电机速度和位置
int16_t speed=0,location=0;PID_t SpeedLoop,LocationLoop;int main(void)
{HAL_Init();sys_stm32_clock_init(RCC_PLL_MUL9);delay_init(72);usart_init(1,115200);led_init();key_init();OLED_Init();LED_ON();Timer_Init();RP_Init();Motor_Init();Encoder_Init();int key_v = 0;Motor_SetPWM(0);/*初始化PID结构体*/PID_InitStruct(&SpeedLoop);PID_InitStruct(&LocationLoop);SpeedLoop.Kp = 5,SpeedLoop.Ki = 5,SpeedLoop.Kd=0;SpeedLoop.Out_max = 1000;SpeedLoop.Out_min=-1000;LocationLoop.Kp = 0.3,LocationLoop.Ki = 0.01,LocationLoop.Kd=0.1;LocationLoop.Out_max = 140;LocationLoop.Out_min=-140;PID_Enable_Opt(&SpeedLoop,PID_OPT_II);/*积分限幅*/SpeedLoop.II_MAX = SpeedLoop.Out_max/SpeedLoop.Ki;SpeedLoop.II_MIN = SpeedLoop.Out_min/SpeedLoop.Ki;PID_Enable_Opt(&LocationLoop,PID_OPT_IS);/*积分分离*/LocationLoop.IS_A=10;PID_Enable_Opt(&LocationLoop,PID_OPT_II);/*积分限幅*/LocationLoop.II_MAX = LocationLoop.Out_max/LocationLoop.Ki;LocationLoop.II_MIN = LocationLoop.Out_min/LocationLoop.Ki;while (1){OLED_Clear();//OLED_Update();switch(Key_GetNum()){case 1:key_v+=204;break;case 2:key_v-=204;break;case 3:key_v=0;break;case 4:key_v=-key_v;break;}LocationLoop.Kp = RP_GetValue(1)/4095.0*7;LocationLoop.Ki = RP_GetValue(2)/4095.0*7;LocationLoop.Kd = RP_GetValue(3)/4095.0*7;//Target = RP_GetValue(4)/4095.0*1832 - 916 + key_v;//LocationLoop.Target = key_v;PID_SetTarget(&LocationLoop,key_v);OLED_Printf(0,0,OLED_6X8,"ADC:%-4d",RP_GetValue(1));OLED_Printf(0,8,OLED_6X8,"TAR:%+04.0f",LocationLoop.Target);OLED_Printf(0,16,OLED_6X8,"ACT:%+04.0f",LocationLoop.Actual_n);OLED_Printf(0,24,OLED_6X8,"OUT:%+04.0f",LocationLoop.Out);OLED_Printf(0,32,OLED_6X8,"EINT:%+04.0f",LocationLoop.ErrorInt);OLED_Printf(60,0,OLED_6X8,"P:%03.2f",LocationLoop.Kp);OLED_Printf(60,8,OLED_6X8,"I:%03.2f",LocationLoop.Ki);OLED_Printf(60,16,OLED_6X8,"D:%03.2f",LocationLoop.Kd);my_USART_printf(1,"%.0f,%.0f,%.0f\n",LocationLoop.Target,LocationLoop.Actual_n,LocationLoop.Out);OLED_Reverse();//OLED_Update();delay_ms(20);}}void TIM1_UP_IRQHandler()
{if(__HAL_TIM_GET_FLAG(&g_tim1_handle, TIM_FLAG_UPDATE) == SET){Key_Tick();static int8_t count=0;count++;if(count%20==0){OLED_Update();speed = Encoder_Get();location+=speed;PID_UpdateActual(&SpeedLoop,speed);PID_UpdateOut(&SpeedLoop);Motor_SetPWM(SpeedLoop.Out);}if(count==40){count=0;PID_UpdateActual(&LocationLoop,location);PID_UpdateOut(&LocationLoop);/*执行控制*/PID_SetTarget(&SpeedLoop,LocationLoop.Out);}__HAL_TIM_CLEAR_IT(&g_tim1_handle, TIM_IT_UPDATE);  /* 清除定时器溢出中断标志位 */}
}

:::info
串口图像和上面基本一样,就不截图了。

使用体验:SpeedLoopPID 为 5、5、0,LocationLoop 为 0.3、0、0.1 的情况下有较好的结果,尤其是当用手强行转动转盘时能感到有一个很大的阻力。不过双环在调参时完全就是另外一种体验了:难。虽然回过头来想想也没多麻烦,但当时调到时候遇到的那种 P 加大了超调,D 加大了震荡,I 不为零就在目标值之间反复横跳的感觉实在太糟了。

:::

双环编程实战:倒立摆

一个倒立摆要实现如下功能:
  1. 保证摆锤道理状态不掉;
  2. 保证横杆维持在初始位置附近;
  3. 有外力作用时可以迅速回正。

基于上面的要求,我们需要调控的有两个量:摆锤的角度和横杆的位置。因此至少需要两个控制器才能完成。

添加角度传感器驱动

:::warning [10_PID_Inverted_Pendulum_SimpleDriver.docx](https://www.yuque.com/attachments/yuque/0/2025/docx/40561973/1750427359737-72d78d56-4b40-4161-a1b8-839a7c28026a.docx)

:::

我们的角度传感器本质上是一个电位器。虽然它不像一般电位器那样只能旋转一定角度,但其测量角度是有一定限制的。示意图如下:

电位器原理示意图


#ifndef __ADC_H
#define __ADC_H#include "./SYSTEM/sys/sys.h"#define ADC_DMA_BUF_SIZE        1 * 1      /* ADC DMA采集 BUF大小, 应等于ADC通道数的整数倍 采样次数*采样通道数 *//******************************************************************************************/
/* ADC及引脚 定义 */#define ADC_ADCX_CHY_GPIO_PORT              GPIOB
#define ADC_ADCX_CHY_GPIO_PIN               GPIO_PIN_0
#define ADC_ADCX_CHY_GPIO_CLK_ENABLE()      do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)  /* PA口时钟使能 */#define ADC_ADCX                            ADC1 
#define ADC_ADCX_CHY                        ADC_CHANNEL_8                                /* 通道Y,  0 <= Y <= 17 */ 
#define ADC_ADCX_CHY_CLK_ENABLE()           do{ __HAL_RCC_ADC1_CLK_ENABLE(); }while(0)   /* ADC1 时钟使能 *//* ADC单通道/多通道 DMA采集 DMA及通道 定义* 注意: ADC1的DMA通道只能是: DMA1_Channel1, 因此只要是ADC1, 这里是不能改动的*       ADC2不支持DMA采集*       ADC3的DMA通道只能是: DMA2_Channel5, 因此如果使用 ADC3 则需要修改*/
#define ADC_ADCX_DMACx                      DMA1_Channel1
#define ADC_ADCX_DMACx_IRQn                 DMA1_Channel1_IRQn
#define ADC_ADCX_DMACx_IRQHandler           DMA1_Channel1_IRQHandler#define ADC_ADCX_DMACx_IS_TC()              ( DMA1->ISR & (1 << 1) )    /* 判断 DMA1_Channel1 传输完成标志, 这是一个假函数形式,* 不能当函数使用, 只能用在if等语句里面 */
#define ADC_ADCX_DMACx_CLR_TC()             do{ DMA1->IFCR |= 1 << 1; }while(0) /* 清除 DMA1_Channel1 传输完成标志 */#define RP_Init                             ADC2_Init
#define RP_GetValue(x)                      ADC2_get_result(x+1)
/******************************************************************************************/void adc_init(void);                                                /* ADC初始化 */
void adc_channel_set(ADC_HandleTypeDef *adc_handle, uint32_t ch,uint32_t rank, uint32_t stime); /* ADC通道设置 */
uint32_t adc_get_result(uint32_t ch);                               /* 获得某个通道值  */
uint32_t adc_get_result_average(uint32_t ch, uint8_t times);        /* 得到某个通道给定次数采样的平均值 */void adc_dma_init(uint32_t mar);                                    /* ADC DMA采集初始化 */
void adc_dma_enable( uint16_t cndtr);                               /* 使能一次ADC DMA采集传输 */void adc_nch_dma_init(uint32_t mar);                                /* ADC多通道 DMA采集初始化 */void ADC2_Init();
uint32_t ADC2_get_result(uint8_t ch);#endif 
#include "./BSP/ADC/adc.h"
#include "./SYSTEM/delay/delay.h"ADC_HandleTypeDef g_adc_handle;   /* ADC句柄 *//*** @brief       ADC初始化函数*   @note      本函数支持ADC1/ADC2任意通道, 但是不支持ADC3*              我们使用12位精度, ADC采样时钟=12M, 转换时间为: 采样周期 + 12.5个ADC周期*              设置最大采样周期: 239.5, 则转换时间 = 252 个ADC周期 = 21us* @param       无* @retval      无*/
void adc_init(void)
{g_adc_handle.Instance = ADC_ADCX;                        /* 选择哪个ADC */g_adc_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;       /* 数据对齐方式:右对齐 */g_adc_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;       /* 非扫描模式,仅用到一个通道 */g_adc_handle.Init.ContinuousConvMode = DISABLE;          /* 关闭连续转换模式 */g_adc_handle.Init.NbrOfConversion = 1;                   /* 赋值范围是1~16,本实验用到1个规则通道序列 */g_adc_handle.Init.DiscontinuousConvMode = DISABLE;       /* 禁止规则通道组间断模式 */g_adc_handle.Init.NbrOfDiscConversion = 0;               /* 配置间断模式的规则通道个数,禁止规则通道组间断模式后,此参数忽略 */g_adc_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 触发转换方式:软件触发 */HAL_ADC_Init(&g_adc_handle);                             /* 初始化 */HAL_ADCEx_Calibration_Start(&g_adc_handle);              /* 校准ADC */
}/*** @brief       ADC底层驱动,引脚配置,时钟使能此函数会被HAL_ADC_Init()调用* @param       hadc:ADC句柄* @retval      无*/
void HAL_ADC_MspInit(ADC_HandleTypeDef *hadc)
{if(hadc->Instance == ADC_ADCX){GPIO_InitTypeDef gpio_init_struct;RCC_PeriphCLKInitTypeDef adc_clk_init = {0};ADC_ADCX_CHY_CLK_ENABLE();                                /* 使能ADCx时钟 */ADC_ADCX_CHY_GPIO_CLK_ENABLE();                           /* 开启GPIO时钟 *//* 设置ADC时钟 */adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;    /* ADC外设时钟 */adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;       /* 分频因子6时钟为72M/6=12MHz */HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);                 /* 设置ADC时钟 *//* 设置AD采集通道对应IO引脚工作模式 */gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN;             /* ADC通道IO引脚 */gpio_init_struct.Mode = GPIO_MODE_ANALOG;                 /* 模拟 */HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);}
}/*** @brief       设置ADC通道采样时间* @param       adcx : adc句柄指针,ADC_HandleTypeDef* @param       ch   : 通道号, ADC_CHANNEL_0~ADC_CHANNEL_17* @param       stime: 采样时间  0~7, 对应关系为:*   @arg       ADC_SAMPLETIME_1CYCLE_5, 1.5个ADC时钟周期        ADC_SAMPLETIME_7CYCLES_5, 7.5个ADC时钟周期*   @arg       ADC_SAMPLETIME_13CYCLES_5, 13.5个ADC时钟周期     ADC_SAMPLETIME_28CYCLES_5, 28.5个ADC时钟周期*   @arg       ADC_SAMPLETIME_41CYCLES_5, 41.5个ADC时钟周期     ADC_SAMPLETIME_55CYCLES_5, 55.5个ADC时钟周期*   @arg       ADC_SAMPLETIME_71CYCLES_5, 71.5个ADC时钟周期     ADC_SAMPLETIME_239CYCLES_5, 239.5个ADC时钟周期* @param       rank: 多通道采集时需要设置的采集编号,假设你定义channle1的rank=1,channle2 的rank=2,那么对应你在DMA缓存空间的变量数组AdcDMA[0] 就i是channle1的转换结果,AdcDMA[1]就是通道2的转换结果。 单通道DMA设置为 ADC_REGULAR_RANK_1*   @arg       编号1~16:ADC_REGULAR_RANK_1~ADC_REGULAR_RANK_16* @retval      无*/
void adc_channel_set(ADC_HandleTypeDef *adc_handle, uint32_t ch, uint32_t rank, uint32_t stime)
{ADC_ChannelConfTypeDef adc_ch_conf;adc_ch_conf.Channel = ch;                            /* 通道 */adc_ch_conf.Rank = rank;                             /* 序列 */adc_ch_conf.SamplingTime = stime;                    /* 采样时间 */HAL_ADC_ConfigChannel(adc_handle, &adc_ch_conf);     /* 通道配置 */
}/*** @brief       获得ADC转换后的结果* @param       ch: 通道值 0~17,取值范围为:ADC_CHANNEL_0~ADC_CHANNEL_17* @retval      无*/
uint32_t adc_get_result(uint32_t ch)
{adc_channel_set(&g_adc_handle , ch, ADC_REGULAR_RANK_1, ADC_SAMPLETIME_239CYCLES_5);    /* 设置通道,序列和采样时间 */HAL_ADC_Start(&g_adc_handle);                            /* 开启ADC */HAL_ADC_PollForConversion(&g_adc_handle, 10);            /* 轮询转换 */return (uint16_t)HAL_ADC_GetValue(&g_adc_handle);        /* 返回最近一次ADC1规则组的转换结果 */
}/*** @brief       获取通道ch的转换值,取times次,然后平均* @param       ch      : 通道号, 0~17* @param       times   : 获取次数* @retval      通道ch的times次转换结果平均值*/
uint32_t adc_get_result_average(uint32_t ch, uint8_t times)
{uint32_t temp_val = 0;uint8_t t;for (t = 0; t < times; t++)     /* 获取times次数据 */{temp_val += adc_get_result(ch);delay_ms(5);}return temp_val / times;        /* 返回平均值 */
}/***************************************单通道ADC采集(DMA读取)实验代码*****************************************/DMA_HandleTypeDef g_dma_adc_handle = {0};                                   /* 定义要搬运ADC数据的DMA句柄 */
ADC_HandleTypeDef g_adc_dma_handle = {0};                                   /* 定义ADC(DMA读取)句柄 */
uint8_t g_adc_dma_sta = 0;                                                  /* DMA传输状态标志, 0,未完成; 1, 已完成 *//*** @brief       ADC DMA读取 初始化函数*   @note      本函数还是使用adc_init对ADC进行大部分配置,有差异的地方再单独配置* @param       par         : 外设地址* @param       mar         : 存储器地址* @retval      无*/
void adc_dma_init(uint32_t mar)
{GPIO_InitTypeDef gpio_init_struct;RCC_PeriphCLKInitTypeDef adc_clk_init = {0};ADC_ChannelConfTypeDef adc_ch_conf = {0};ADC_ADCX_CHY_CLK_ENABLE();                                              /* 使能ADCx时钟 */ADC_ADCX_CHY_GPIO_CLK_ENABLE();                                         /* 开启GPIO时钟 */if ((uint32_t)ADC_ADCX_DMACx > (uint32_t)DMA1_Channel7)                 /* 大于DMA1_Channel7, 则为DMA2的通道了 */{__HAL_RCC_DMA2_CLK_ENABLE();                                        /* DMA2时钟使能 */}else{__HAL_RCC_DMA1_CLK_ENABLE();                                        /* DMA1时钟使能 */}/* 设置ADC时钟 */adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;                  /* ADC外设时钟 */adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;                     /* 分频因子6时钟为72M/6=12MHz */HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);                               /* 设置ADC时钟 *//* 设置AD采集通道对应IO引脚工作模式 */gpio_init_struct.Pin = ADC_ADCX_CHY_GPIO_PIN;                           /* ADC通道对应的IO引脚 */gpio_init_struct.Mode = GPIO_MODE_ANALOG;                               /* 模拟 */HAL_GPIO_Init(ADC_ADCX_CHY_GPIO_PORT, &gpio_init_struct);/* 初始化DMA */g_dma_adc_handle.Instance = ADC_ADCX_DMACx;                             /* 设置DMA通道 */g_dma_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;                 /* 从外设到存储器模式 */g_dma_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE;                     /* 外设非增量模式 */g_dma_adc_handle.Init.MemInc = DMA_MINC_ENABLE;                         /* 存储器增量模式 */g_dma_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;    /* 外设数据长度:16位 */g_dma_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;       /* 存储器数据长度:16位 */g_dma_adc_handle.Init.Mode = DMA_NORMAL;                                /* 外设流控模式 */g_dma_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM;                   /* 中等优先级 */HAL_DMA_Init(&g_dma_adc_handle);__HAL_LINKDMA(&g_adc_dma_handle, DMA_Handle, g_dma_adc_handle);         /* 将DMA与adc联系起来 */g_adc_dma_handle.Instance = ADC_ADCX;                                   /* 选择哪个ADC */g_adc_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;                  /* 数据对齐方式:右对齐 */g_adc_dma_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;                  /* 非扫描模式,仅用到一个通道 */g_adc_dma_handle.Init.ContinuousConvMode = ENABLE;                      /* 使能连续转换模式 */g_adc_dma_handle.Init.NbrOfConversion = 1;                              /* 赋值范围是1~16,本实验用到1个规则通道序列 */g_adc_dma_handle.Init.DiscontinuousConvMode = DISABLE;                  /* 禁止规则通道组间断模式 */g_adc_dma_handle.Init.NbrOfDiscConversion = 0;                          /* 配置间断模式的规则通道个数,禁止规则通道组间断模式后,此参数忽略 */g_adc_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;            /* 触发转换方式:软件触发 */HAL_ADC_Init(&g_adc_dma_handle);                                        /* 初始化 */HAL_ADCEx_Calibration_Start(&g_adc_dma_handle);                         /* 校准ADC *//* 配置ADC通道 */adc_ch_conf.Channel = ADC_ADCX_CHY;                                     /* 通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                                  /* 序列 */adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;                  /* 采样时间,设置最大采样周期:239.5个ADC周期 */HAL_ADC_ConfigChannel(&g_adc_dma_handle, &adc_ch_conf);                 /* 通道配置 *//* 配置DMA数据流请求中断优先级 */HAL_NVIC_SetPriority(ADC_ADCX_DMACx_IRQn, 3, 3);HAL_NVIC_EnableIRQ(ADC_ADCX_DMACx_IRQn);HAL_DMA_Start_IT(&g_dma_adc_handle, (uint32_t)&ADC1->DR, mar, 0);       /* 启动DMA,并开启中断 */HAL_ADC_Start_DMA(&g_adc_dma_handle, &mar, 0);                          /* 开启ADC,通过DMA传输结果 */
}/*************************单通道ADC采集(DMA读取)实验和多通道ADC采集(DMA读取)实验公用代码*******************************/DMA_HandleTypeDef g_dma_nch_adc_handle = {0};                               /* 定义要搬运ADC多通道数据的DMA句柄 */
ADC_HandleTypeDef g_adc_nch_dma_handle = {0};                               /* 定义ADC(多通道DMA读取)句柄 *//*** @brief       ADC N通道(6通道) DMA读取 初始化函数*   @note      本函数还是使用adc_init对ADC进行大部分配置,有差异的地方再单独配置*              另外,由于本函数用到了6个通道, 宏定义会比较多内容, 因此,本函数就不采用宏定义的方式来修改通道了,*              直接在本函数里面修改, 这里我们默认使用PA0~PA5这6个通道.**              注意: 本函数还是使用 ADC_ADCX(默认=ADC1) 和 ADC_ADCX_DMACx( DMA1_Channel1 ) 及其相关定义*              不要乱修改adc.h里面的这两部分内容, 必须在理解原理的基础上进行修改, 否则可能导致无法正常使用.** @param       mar         : 存储器地址 * @retval      无*/
void adc_nch_dma_init(uint32_t mar)
{GPIO_InitTypeDef gpio_init_struct;RCC_PeriphCLKInitTypeDef adc_clk_init = {0};ADC_ChannelConfTypeDef adc_ch_conf = {0};ADC_ADCX_CHY_CLK_ENABLE();                                                /* 使能ADCx时钟 */__HAL_RCC_GPIOA_CLK_ENABLE();                                             /* 开启GPIOA时钟 */if ((uint32_t)ADC_ADCX_DMACx > (uint32_t)DMA1_Channel7)                   /* 大于DMA1_Channel7, 则为DMA2的通道了 */{__HAL_RCC_DMA2_CLK_ENABLE();                                          /* DMA2时钟使能 */}else{__HAL_RCC_DMA1_CLK_ENABLE();                                          /* DMA1时钟使能 */}/* 设置ADC时钟 */adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;                    /* ADC外设时钟 */adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;                       /* 分频因子6时钟为72M/6=12MHz */HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);                                 /* 设置ADC时钟 *//* 设置ADC1通道0~5对应的IO口模拟输入AD采集引脚模式设置,模拟输入PA0对应 ADC1_IN0PA1对应 ADC1_IN1PA2对应 ADC1_IN2PA3对应 ADC1_IN3PA4对应 ADC1_IN4PA5对应 ADC1_IN5*/gpio_init_struct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5;  /* GPIOA0~5 */gpio_init_struct.Mode = GPIO_MODE_ANALOG;                                 /* 模拟 */HAL_GPIO_Init(GPIOA, &gpio_init_struct);/* 初始化DMA */g_dma_nch_adc_handle.Instance = ADC_ADCX_DMACx;                           /* 设置DMA通道 */g_dma_nch_adc_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;               /* 从外设到存储器模式 */g_dma_nch_adc_handle.Init.PeriphInc = DMA_PINC_DISABLE;                   /* 外设非增量模式 */g_dma_nch_adc_handle.Init.MemInc = DMA_MINC_ENABLE;                       /* 存储器增量模式 */g_dma_nch_adc_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;  /* 外设数据长度:16位 */g_dma_nch_adc_handle.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;     /* 存储器数据长度:16位 */g_dma_nch_adc_handle.Init.Mode = DMA_NORMAL;                              /* 外设流控模式 */g_dma_nch_adc_handle.Init.Priority = DMA_PRIORITY_MEDIUM;                 /* 中等优先级 */HAL_DMA_Init(&g_dma_nch_adc_handle);__HAL_LINKDMA(&g_adc_nch_dma_handle, DMA_Handle, g_dma_nch_adc_handle);   /* 将DMA与adc联系起来 *//* 初始化ADC */g_adc_nch_dma_handle.Instance = ADC_ADCX;                                 /* 选择哪个ADC */g_adc_nch_dma_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;                /* 数据对齐方式:右对齐 */g_adc_nch_dma_handle.Init.ScanConvMode = ADC_SCAN_ENABLE;                 /* 使能扫描模式 */g_adc_nch_dma_handle.Init.ContinuousConvMode = ENABLE;                    /* 使能连续转换 */g_adc_nch_dma_handle.Init.NbrOfConversion = 6;                            /* 赋值范围是1~16,本实验用到6个规则通道序列 */g_adc_nch_dma_handle.Init.DiscontinuousConvMode = DISABLE;                /* 禁止规则通道组间断模式 */g_adc_nch_dma_handle.Init.NbrOfDiscConversion = 0;                        /* 配置间断模式的规则通道个数,禁止规则通道组间断模式后,此参数忽略 */g_adc_nch_dma_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;          /* 软件触发 */HAL_ADC_Init(&g_adc_nch_dma_handle);                                      /* 初始化 */HAL_ADCEx_Calibration_Start(&g_adc_nch_dma_handle);                       /* 校准ADC *//* 配置ADC通道 */adc_ch_conf.Channel = ADC_CHANNEL_2;                                      /* 配置使用的ADC通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                                    /* 采样序列里的第1个 */adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;                    /* 采样时间,设置最大采样周期:239.5个ADC周期 */HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 通道配置 */adc_ch_conf.Channel = ADC_CHANNEL_3;                                      /* 配置使用的ADC通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_2;                                    /* 采样序列里的第2个 */HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 配置ADC通道 */adc_ch_conf.Channel = ADC_CHANNEL_4;                                      /* 配置使用的ADC通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_3;                                    /* 采样序列里的第3个 */HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 配置ADC通道 */adc_ch_conf.Channel = ADC_CHANNEL_5;                                      /* 配置使用的ADC通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_4;                                    /* 采样序列里的第4个 */HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 配置ADC通道 *///    adc_ch_conf.Channel = ADC_CHANNEL_4;                                      /* 配置使用的ADC通道 */
//    adc_ch_conf.Rank = ADC_REGULAR_RANK_5;                                    /* 采样序列里的第5个 */
//    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 配置ADC通道 *///    adc_ch_conf.Channel = ADC_CHANNEL_5;                                      /* 配置使用的ADC通道 */
//    adc_ch_conf.Rank = ADC_REGULAR_RANK_6;                                    /* 采样序列里的第6个 */
//    HAL_ADC_ConfigChannel(&g_adc_nch_dma_handle, &adc_ch_conf);               /* 配置ADC通道 *//* 配置DMA数据流请求中断优先级 */HAL_NVIC_SetPriority(ADC_ADCX_DMACx_IRQn, 3, 3);HAL_NVIC_EnableIRQ(ADC_ADCX_DMACx_IRQn);HAL_DMA_Start_IT(&g_dma_nch_adc_handle, (uint32_t)&ADC1->DR, mar, 0);     /* 启动DMA,并开启中断 */HAL_ADC_Start_DMA(&g_adc_nch_dma_handle, &mar, 0);                        /* 开启ADC,通过DMA传输结果 */
}/*************************单通道ADC采集(DMA读取)实验和多通道ADC采集(DMA读取)实验公用代码*******************************//*** @brief       使能一次ADC DMA传输*   @note      该函数用寄存器来操作,防止用HAL库操作对其他参数有修改,也为了兼容性* @param       ndtr: DMA传输的次数* @retval      无*/
void adc_dma_enable(uint16_t cndtr)
{ADC_ADCX->CR2 &= ~(1 << 0);                 /* 先关闭ADC */ADC_ADCX_DMACx->CCR &= ~(1 << 0);           /* 关闭DMA传输 */while (ADC_ADCX_DMACx->CCR & (1 << 0));     /* 确保DMA可以被设置 */ADC_ADCX_DMACx->CNDTR = cndtr;              /* DMA传输数据量 */ADC_ADCX_DMACx->CCR |= 1 << 0;              /* 开启DMA传输 */ADC_ADCX->CR2 |= 1 << 0;                    /* 重新启动ADC */ADC_ADCX->CR2 |= 1 << 22;                   /* 启动规则转换通道 */
}/*** @brief       ADC DMA采集中断服务函数* @param       无 * @retval      无*/
void ADC_ADCX_DMACx_IRQHandler(void)
{if (ADC_ADCX_DMACx_IS_TC()){g_adc_dma_sta = 1;                      /* 标记DMA传输完成 */ADC_ADCX_DMACx_CLR_TC();                /* 清除DMA1 数据流7 传输完成中断 */}
}ADC_HandleTypeDef g_adc2_handle;
ADC_HandleTypeDef g_adc1_handle;
void ADC2_Init(){GPIO_InitTypeDef gpio_init_struct;RCC_PeriphCLKInitTypeDef adc_clk_init = {0};ADC_ChannelConfTypeDef adc_ch_conf = {0};__HAL_RCC_ADC2_CLK_ENABLE();                                                /* 使能ADCx时钟 */__HAL_RCC_GPIOA_CLK_ENABLE();                                             /* 开启GPIOA时钟 */if ((uint32_t)ADC_ADCX_DMACx > (uint32_t)DMA1_Channel7)                   /* 大于DMA1_Channel7, 则为DMA2的通道了 */{__HAL_RCC_DMA2_CLK_ENABLE();                                          /* DMA2时钟使能 */}else{__HAL_RCC_DMA1_CLK_ENABLE();                                          /* DMA1时钟使能 */}/* 设置ADC时钟 */adc_clk_init.PeriphClockSelection = RCC_PERIPHCLK_ADC;                    /* ADC外设时钟 */adc_clk_init.AdcClockSelection = RCC_ADCPCLK2_DIV6;                       /* 分频因子6时钟为72M/6=12MHz */HAL_RCCEx_PeriphCLKConfig(&adc_clk_init);                                 /* 设置ADC时钟 *//* 设置ADC1通道0~5对应的IO口模拟输入AD采集引脚模式设置,模拟输入PA0对应 ADC1_IN0PA1对应 ADC1_IN1PA2对应 ADC1_IN2PA3对应 ADC1_IN3PA4对应 ADC1_IN4PA5对应 ADC1_IN5*/gpio_init_struct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4|GPIO_PIN_5;  /* GPIOA0~5 */gpio_init_struct.Mode = GPIO_MODE_ANALOG;                                 /* 模拟 */HAL_GPIO_Init(GPIOA, &gpio_init_struct);/* 初始化ADC */g_adc2_handle.Instance = ADC2;                                 /* 选择哪个ADC */g_adc2_handle.Init.DataAlign = ADC_DATAALIGN_RIGHT;                /* 数据对齐方式:右对齐 */g_adc2_handle.Init.ScanConvMode = ADC_SCAN_DISABLE;                 /* 使能扫描模式 */g_adc2_handle.Init.ContinuousConvMode = ENABLE;                    /* 使能连续转换 */g_adc2_handle.Init.NbrOfConversion = 4;                            /* 赋值范围是1~16,本实验用到6个规则通道序列 */g_adc2_handle.Init.DiscontinuousConvMode = DISABLE;                /* 禁止规则通道组间断模式 */g_adc2_handle.Init.NbrOfDiscConversion = 0;                        /* 配置间断模式的规则通道个数,禁止规则通道组间断模式后,此参数忽略 */g_adc2_handle.Init.ExternalTrigConv = ADC_SOFTWARE_START;          /* 软件触发 */HAL_ADC_Init(&g_adc2_handle);                                      /* 初始化 */HAL_ADCEx_Calibration_Start(&g_adc2_handle);                       /* 校准ADC *//* 配置ADC通道 */adc_ch_conf.Channel = ADC_CHANNEL_2;                                      /* 配置使用的ADC通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_1;                                    /* 采样序列里的第1个 */adc_ch_conf.SamplingTime = ADC_SAMPLETIME_239CYCLES_5;                    /* 采样时间,设置最大采样周期:239.5个ADC周期 */HAL_ADC_ConfigChannel(&g_adc2_handle, &adc_ch_conf);               /* 通道配置 */adc_ch_conf.Channel = ADC_CHANNEL_3;                                      /* 配置使用的ADC通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_2;                                    /* 采样序列里的第2个 */HAL_ADC_ConfigChannel(&g_adc2_handle, &adc_ch_conf);               /* 配置ADC通道 */adc_ch_conf.Channel = ADC_CHANNEL_4;                                      /* 配置使用的ADC通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_3;                                    /* 采样序列里的第3个 */HAL_ADC_ConfigChannel(&g_adc2_handle, &adc_ch_conf);               /* 配置ADC通道 */adc_ch_conf.Channel = ADC_CHANNEL_5;                                      /* 配置使用的ADC通道 */adc_ch_conf.Rank = ADC_REGULAR_RANK_4;                                    /* 采样序列里的第4个 */HAL_ADC_ConfigChannel(&g_adc2_handle, &adc_ch_conf);               /* 配置ADC通道 */HAL_ADC_Start_IT(&g_adc2_handle);        //开启转换并触发中断HAL_NVIC_SetPriority(ADC1_2_IRQn, 0, 0); // 设置中断优先级HAL_NVIC_EnableIRQ(ADC1_2_IRQn);         // 使能 ADC1 和 ADC2 的中断
}void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
//    static uint8_t ADC2CH=0;
//    if (hadc->Instance == ADC2)
//    {
//        
//        g_adc_dma_buf[ADC2CH++] = HAL_ADC_GetValue(hadc);
//        if(ADC2CH==4)ADC2CH=0;
//        
//    }
}void ADC1_2_IRQHandler(void)
{if (__HAL_ADC_GET_FLAG(&g_adc2_handle, ADC_FLAG_EOC) != RESET){HAL_ADC_IRQHandler(&g_adc2_handle); // 将 gdc2 的地址传递给 HAL_ADC_IRQHandler}// 检查 ADC2 是否触发了中断if (__HAL_ADC_GET_FLAG(&g_adc1_handle, ADC_FLAG_EOC) != RESET){HAL_ADC_IRQHandler(&g_adc1_handle); // 将 adc1 的地址传递给 HAL_ADC_IRQHandler}
}uint32_t ADC2_get_result(uint8_t ch)
{adc_channel_set(&g_adc2_handle , ch, ADC_REGULAR_RANK_1, ADC_SAMPLETIME_239CYCLES_5);    /* 设置通道,序列和采样时间 */HAL_ADC_Start(&g_adc2_handle);                            /* 开启ADC */HAL_ADC_PollForConversion(&g_adc2_handle, 10);            /* 轮询转换 */return (uint16_t)HAL_ADC_GetValue(&g_adc2_handle);        /* 返回最近一次ADC1规则组的转换结果 */
}

用法:在合适的位置定义变量 angle,然后初始化。

int16_t angle;
adc_dma_init((uint32_t)&ang);
adc_dma_enable(1);	//启动一次ADC转换
OLED_Printf(0,0,OLED_6X8,"ADC:%-4d",ang);//打印出来

:::info
理想状态下摆锤竖直时 ADC 读数是 2048;但我实测我的读数是 2010±10,因此以实际为准。

:::

PID 控制框图

![内环角度环:保证摆锤不倒;外环位置环:保证横杆不偏移](https://cdn.nlark.com/yuque/0/2025/png/40561973/1750418577935-1fc7d01f-0c90-47fd-aa0b-9819db280bb2.png)

解释:

  1. 角度环接收目标角度,根据实际角度于目标角度的差调控电机以维持摆锤维持在正确角度上;
  2. 位置环接收位置,并通过动态改变角度环目标值来间接控制电机旋转。
  3. 因此内环角度环的目标有两部分构成:一个是中心角度,用于保持摆锤稳定;另一个是位置环的输出值,用于调控横杆的位置。

第一步:初始化外设&搭建基本框架

在程序开始前将所有外设初始化好,以免要用到时忘记初始化;

除此之外,定时器中断、按键检测框架也要写好:

//#include "./stm32f1xx_it.h"
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
//#include "./BSP/EXTI/exti.h"
#include "./BSP/KEY/key.h"
#include "./BSP/TIMER/gtim.h"
#include "./BSP/TIMER/atim.h"
#include "./BSP/OLED/OLED.h"
#include "./BSP/ADC/adc.h"
#include "string.h"
#include "./BSP/Motor/Motor.h"
#include "./BSP/Encoder/Encoder.h"
#include "PID.h"//当前电机速度和位置
int16_t speed=0,location=0;int main(void)
{HAL_Init();sys_stm32_clock_init(RCC_PLL_MUL9);delay_init(72);usart_init(1,115200);led_init();key_init();OLED_Init();LED_ON();Timer_Init();RP_Init();Motor_Init();Encoder_Init();int key_v = 0;Motor_SetPWM(0);while (1){OLED_Clear();//OLED_Update();switch(Key_GetNum()){case 1:key_v+=204;break;case 2:key_v-=204;break;case 3:key_v=0;break;case 4:key_v=-key_v;break;}//        my_USART_printf(1,"%.0f,%.0f,%.0f\n",LocationLoop.Target,LocationLoop.Actual_n,LocationLoop.Out);//OLED_Update();delay_ms(20);}}void TIM1_UP_IRQHandler()
{if(__HAL_TIM_GET_FLAG(&g_tim1_handle, TIM_FLAG_UPDATE) == SET){Key_Tick();static int8_t count=0;count++;if(count%20==0){OLED_Update();}if(count==40){count=0;}__HAL_TIM_CLEAR_IT(&g_tim1_handle, TIM_IT_UPDATE);  /* 清除定时器溢出中断标志位 */}
}

并且,出于安全考虑,我们最先完成的是倒立摆的启停控制:需要我们手动通过按键来控制启停,并实现当程序判断倒立摆失败后自动停止。

因此我们定义一个变量 RunState,并规定其为 0 时表示系统停止,为 1 时表示倒立摆工作。

uint8_t RunState;

并通过按键一控制:

switch(Key_GetNum()){case 1:RunState = !RunState ;break;case 2:break;case 3:break;case 4:break;
}

下面就是在真正执行调控的函数里进行判断是否执行程序:

void TIM1_UP_IRQHandler()
{if(__HAL_TIM_GET_FLAG(&g_tim1_handle, TIM_FLAG_UPDATE) == SET){Key_Tick();if(RunState){//调控过程。。。}else{Motor_SetPWM(0);}__HAL_TIM_CLEAR_IT(&g_tim1_handle, TIM_IT_UPDATE);  /* 清除定时器溢出中断标志位 */}
}

接下来我们还要判断摆杆是不是倒了,来实现倒立摆失败时自动停止;

因此我们要读取角度传感器的值,并在中断里读取它:

uint16_t Angle;//在main函数之上定义全局变量
adc_dma_init((uint32_t)&Angle);//在中断里使能一次ADC转换,并通过DMA读取

然后判断摆杆是不是在中心角度附近,我们用宏定义的方法来定义中心角度:

#define CENTER_ANGLE    2010	//理想情况下是2048,根据实际调整
#define CENTER_RANGE    500		//定义当角度处于什么范围内时我们认为是可调控的,500的意思是2010±500

然后判断:

if(abs(Angle-CENTER_ANGLE) > CENTER_RANGE){RunState=0;
}

最后,在主函数里用 LED 灯指示运行状态并在 OLED 上输出即可:

OLED_Printf(0,0,OLED_6X8,"ADC:%-4d",Angle);
LED0(!RunState);

完整 main.c 如下:

//#include "./stm32f1xx_it.h"
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
//#include "./BSP/EXTI/exti.h"
#include "./BSP/KEY/key.h"
#include "./BSP/TIMER/gtim.h"
#include "./BSP/TIMER/atim.h"
#include "./BSP/OLED/OLED.h"
#include "./BSP/ADC/adc.h"
#include "string.h"
#include "./BSP/Motor/Motor.h"
#include "./BSP/Encoder/Encoder.h"
#include "PID.h"#define CENTER_ANGLE    2010
#define CENTER_RANGE    500//当前电机速度和位置
int16_t speed=0,location=0;uint16_t Angle;
uint8_t RunState;int main(void)
{HAL_Init();sys_stm32_clock_init(RCC_PLL_MUL9);delay_init(72);usart_init(1,115200);led_init();key_init();OLED_Init();LED_ON();RP_Init();Motor_Init();Encoder_Init();int key_v = 0;Motor_SetPWM(0);adc_dma_init((uint32_t)&Angle);Timer_Init();/*初始化PID结构体*/while (1){OLED_Clear();//OLED_Update();switch(Key_GetNum()){case 1:RunState = !RunState ;break;case 2:break;case 3:break;case 4:break;}//        my_USART_printf(1,"%.0f,%.0f,%.0f\n",LocationLoop.Target,LocationLoop.Actual_n,LocationLoop.Out);OLED_Printf(0,0,OLED_6X8,"ADC:%-4d",Angle);OLED_Reverse();LED0(!RunState);OLED_Update();delay_ms(20);}}void TIM1_UP_IRQHandler()
{if(__HAL_TIM_GET_FLAG(&g_tim1_handle, TIM_FLAG_UPDATE) == SET){Key_Tick();adc_dma_enable(1);//读取角度传感器值if(abs(Angle-CENTER_ANGLE) > CENTER_RANGE){RunState=0;}if(RunState){}else{Motor_SetPWM(0);}__HAL_TIM_CLEAR_IT(&g_tim1_handle, TIM_IT_UPDATE);  /* 清除定时器溢出中断标志位 */}
}

:::info
我在这里遇到了定时器 1 初始化失败的情况。经过排查发现是 初始化顺序 导致的。

应当把 timer 放在最后一个初始化。

:::

第二步:确定内环调控周期&调试内环 PID 参数

首先要确定调控周期,原则是:在硬件参数允许的情况下,结合实际情况,调控越快越好

我们可以先从 40ms 开始测试,发现倒立摆始终摆不起来,就把周期设置为 20ms,发现还不行,……最后确定周期为 5ms 较好。哈哈哈倒立摆摆不起来我就可以开摆了

if(RunState){count1++;if(count1>=5){count1=0;PID_UpdateActual(&AngleLoop,Angle);PID_UpdateOut(&AngleLoop);Motor_SetPWM(AngleLoop.Out);}
}else{Motor_SetPWM(0);
}

然后是创建 PID 结构体变量,开始调参:

//#include "./stm32f1xx_it.h"
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
//#include "./BSP/EXTI/exti.h"
#include "./BSP/KEY/key.h"
#include "./BSP/TIMER/gtim.h"
#include "./BSP/TIMER/atim.h"
#include "./BSP/OLED/OLED.h"
#include "./BSP/ADC/adc.h"
#include "string.h"
#include "./BSP/Motor/Motor.h"
#include "./BSP/Encoder/Encoder.h"
#include "PID.h"#define CENTER_ANGLE    2030
#define CENTER_RANGE    500//当前电机速度和位置
int16_t speed=0,location=0;PID_t AngleLoop,LocationLoop;uint16_t Angle;
uint8_t RunState = 0;int main(void)
{HAL_Init();sys_stm32_clock_init(RCC_PLL_MUL9);delay_init(72);usart_init(1,115200);led_init();key_init();OLED_Init();LED_ON();RP_Init();Motor_Init();Encoder_Init();adc_dma_init((uint32_t)&Angle);adc_dma_enable(1);Timer_Init();/*初始化PID结构体*/PID_InitStruct(&AngleLoop);PID_SetTarget(&AngleLoop,CENTER_ANGLE);//PID_Enable_Opt(&AngleLoop,PID_OPT_II);  //积分限幅//AngleLoop.II_MAX = 5000;AngleLoop.II_MIN = -5000;//PID_Enable_Opt(&AngleLoop,PID_OPT_ID);  //不完全微分//AngleLoop.ID_A = 0.5;//PID_Enable_Opt(&AngleLoop,PID_OPT_IS);  //积分分离//AngleLoop.IS_A = 50;//PID_Enable_Opt(&AngleLoop,PID_OPT_DL);  //微分先行AngleLoop.Kp = 2.35,AngleLoop.Ki = 0.03,AngleLoop.Kd = 1.15;AngleLoop.Out_max = 1000;AngleLoop.Out_min = -1000;int key_v = 0;float* pid[3] = {&AngleLoop.Kp,&AngleLoop.Ki,&AngleLoop.Kd};while (1){OLED_Clear();//OLED_Update();switch(Key_GetNum()){case 1:RunState=!RunState;AngleLoop.Out =0;AngleLoop.ErrorInt=0;break;case 2:*pid[key_v]+=0.05f;break;case 3:*pid[key_v]-=0.05f;break;case 4:key_v++;if(key_v==3)key_v=0;break;}//AngleLoop.Kp=RP_GetValue(1)/4095.0 *15;//AngleLoop.Ki=RP_GetValue(2)/4095.0 *15;//AngleLoop.Kd=RP_GetValue(3)/4095.0 *15;//AngleLoop.II_MAX = AngleLoop.Out_max/AngleLoop.Ki;//AngleLoop.II_MIN = AngleLoop.Out_min/AngleLoop.Ki;//Motor_SetPWM(RP_GetValue(4)/4095.0 *1000);my_USART_printf(1,"%d,%d,%d\n",(int)AngleLoop.Target,Angle,(int)AngleLoop.Out);//OLED_Update();//adc_dma_enable(1);OLED_Printf(0,0,OLED_6X8,"P:%-3.2f",AngleLoop.Kp);OLED_Printf(0,8,OLED_6X8,"I:%-3.2f",AngleLoop.Ki);OLED_Printf(0,16,OLED_6X8,"D:%-3.2f",AngleLoop.Kd);OLED_Printf(0,24,OLED_6X8,"tar:%d",(int)AngleLoop.Target);OLED_Printf(0,32,OLED_6X8,"act:%d",Angle);OLED_Printf(0,40,OLED_6X8,"out:%d",(int)AngleLoop.Out);OLED_Reverse();OLED_ReverseArea(0,key_v*8,6,8);OLED_Update();LED0(!RunState);delay_ms(20);}}void TIM1_UP_IRQHandler()
{if(__HAL_TIM_GET_FLAG(&g_tim1_handle, TIM_FLAG_UPDATE) == SET){Key_Tick();adc_dma_enable(1);//读取角度传感器值if(abs(Angle-CENTER_ANGLE) > CENTER_RANGE){RunState=0;AngleLoop.Out =0;AngleLoop.ErrorInt=0;}static uint8_t count1 = 1;if(1){if(RunState){count1++;if(count1>=2){count1=0;PID_UpdateActual(&AngleLoop,Angle);PID_UpdateOut(&AngleLoop);Motor_SetPWM(AngleLoop.Out);}}else{Motor_SetPWM(0);}}__HAL_TIM_CLEAR_IT(&g_tim1_handle, TIM_IT_UPDATE);  /* 清除定时器溢出中断标志位 */}
}

你可能注意到代码中注释掉了大量的改进措施参数。

这是因为最开始我对调参的效果不满意,思来想去觉得可能是算法的问题,于是添加了改进效果进去;

但这是错误的,改进算法应当放在最后一步进行,永远不要在参数调好之前乱加改进。

后来发现效果怎么都不好,倒立摆常常处于一个“假稳定”的状态中,用手轻轻碰一下就转起来了;

经过群友的点拨才把这些乱七八糟的步骤给去掉,从头开始重新调参。

我调参的步骤:

先调 P 项,从 0 一点一点往上加(为了不受电位器读取噪声我使用按键来进行调参)(注意此时其它两项都是 0),直到倒立摆可以较长时间维持“倒立”的状态(此时可能会有很大的抖动、横臂旋转等现象,没关系)

然后调 D 项消抖,和 P 一样。如果你反复都调不好,可以重新再调一边 P,让 P 换个值再调 D。

最后是 I 项,一定一定注意!这里的 I 是为了抵消 P 再误差较小的时候输出力不足造成的误差(见上文),因此 I 项一定要非常非常小!我这里的 I 给到 0.03,因为我发现 0.05 会加大抖动,0.01 又会调控力不足造成转动。

当参数调的差不多的时候。你可能会注意到有时候横臂会微微旋转以维持倒立,这是正常的,只要旋转速度别太快就行。后面我们会加上一个位置环来控制位置。

第三步:确定外环调控周期&调试外环参数

外环是位置环,相比上面的角度环来说参数要宽松不少,毕竟角度环可是能直接决定倒立摆能否成功倒立。

这里我们和内环一样,不选择任何改进,只是用最基本的 PID 计算。

//#include "./stm32f1xx_it.h"
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
//#include "./BSP/EXTI/exti.h"
#include "./BSP/KEY/key.h"
#include "./BSP/TIMER/gtim.h"
#include "./BSP/TIMER/atim.h"
#include "./BSP/OLED/OLED.h"
#include "./BSP/ADC/adc.h"
#include "string.h"
#include "./BSP/Motor/Motor.h"
#include "./BSP/Encoder/Encoder.h"
#include "PID.h"#define CENTER_ANGLE    2035
#define CENTER_RANGE    500//当前电机速度和位置
int16_t speed=0,location=0;PID_t AngleLoop,LocationLoop;uint16_t Angle;
uint8_t RunState = 0;int main(void)
{HAL_Init();sys_stm32_clock_init(RCC_PLL_MUL9);delay_init(72);usart_init(1,115200);led_init();key_init();OLED_Init();LED_ON();RP_Init();Motor_Init();Encoder_Init();adc_dma_init((uint32_t)&Angle);adc_dma_enable(1);Timer_Init();/*初始化PID结构体*/PID_InitStruct(&AngleLoop);PID_SetTarget(&AngleLoop,CENTER_ANGLE);//PID_Enable_Opt(&AngleLoop,PID_OPT_II);  //积分限幅//AngleLoop.II_MAX = 5000;AngleLoop.II_MIN = -5000;//PID_Enable_Opt(&AngleLoop,PID_OPT_ID);  //不完全微分//AngleLoop.ID_A = 0.5;//PID_Enable_Opt(&AngleLoop,PID_OPT_IS);  //积分分离//AngleLoop.IS_A = 50;//PID_Enable_Opt(&AngleLoop,PID_OPT_DL);  //微分先行AngleLoop.Kp = 2.0,AngleLoop.Ki = 0.03,AngleLoop.Kd = 6.21;AngleLoop.Out_max = 1000;AngleLoop.Out_min = -1000;PID_InitStruct(&LocationLoop);PID_SetTarget(&LocationLoop,0);LocationLoop.Kp = 0.5,LocationLoop.Ki = 0,LocationLoop.Kd = 5.72;LocationLoop.Out_max = 70;LocationLoop.Out_min = -70;int key_v = 0;float* pid[3] = {&AngleLoop.Kp,&AngleLoop.Ki,&AngleLoop.Kd};while (1){OLED_Clear();//OLED_Update();switch(Key_GetNum()){case 1:RunState=!RunState;AngleLoop.Out =0;AngleLoop.ErrorInt=0;break;case 2:*pid[key_v]+=0.02f;break;case 3:*pid[key_v]-=0.02f;break;case 4:key_v++;if(key_v==3)key_v=0;break;}//AngleLoop.Kp=RP_GetValue(1)/4095.0 *15;//AngleLoop.Ki=RP_GetValue(2)/4095.0 *15;//AngleLoop.Kd=RP_GetValue(3)/4095.0 *15;LocationLoop.II_MAX = LocationLoop.Out_max/LocationLoop.Ki;LocationLoop.II_MIN = LocationLoop.Out_min/LocationLoop.Ki;//Motor_SetPWM(RP_GetValue(4)/4095.0 *1000);my_USART_printf(1,"%d,%d,%d\n",(int)LocationLoop.Target,location,(int)LocationLoop.Out);//OLED_Update();//adc_dma_enable(1);OLED_Printf(0,0,OLED_6X8,"P:%-3.2f",AngleLoop.Kp);OLED_Printf(0,8,OLED_6X8,"I:%-3.2f",AngleLoop.Ki);OLED_Printf(0,16,OLED_6X8,"D:%-3.2f",AngleLoop.Kd);OLED_Printf(0,24,OLED_6X8,"tar:%d",(int)AngleLoop.Target);OLED_Printf(0,32,OLED_6X8,"act:%d",Angle);OLED_Printf(0,40,OLED_6X8,"out:%d",(int)AngleLoop.Out);OLED_Printf(64,0,OLED_6X8,"P:%-3.2f",LocationLoop.Kp);OLED_Printf(64,8,OLED_6X8,"I:%-3.2f",LocationLoop.Ki);OLED_Printf(64,16,OLED_6X8,"D:%-3.2f",LocationLoop.Kd);OLED_Printf(64,24,OLED_6X8,"tar:%d",(int)LocationLoop.Target);OLED_Printf(64,32,OLED_6X8,"act:%d",location);OLED_Printf(64,40,OLED_6X8,"out:%d",(int)LocationLoop.Out);OLED_Reverse();OLED_ReverseArea(64,key_v*8,8,8);OLED_Update();LED0(!RunState);delay_ms(20);}}void TIM1_UP_IRQHandler()
{if(__HAL_TIM_GET_FLAG(&g_tim1_handle, TIM_FLAG_UPDATE) == SET){Key_Tick();adc_dma_enable(1);//读取角度传感器值speed = Encoder_Get();location+=speed;    //获取电机的速度和位置if(abs(Angle-CENTER_ANGLE) > CENTER_RANGE){ //如果误差太大则自动退出调控RunState=0;AngleLoop.Out =0;AngleLoop.ErrorInt=0;PID_SetTarget(&AngleLoop,CENTER_ANGLE);LocationLoop.Out=0;LocationLoop.ErrorInt=0;location=0;}static uint8_t count1 = 0,count2 = 0;if(RunState){       //如果当前处于调控状态count1++;if(count1>=2){count1=0;PID_UpdateActual(&AngleLoop,Angle);     //更新实际值PID_UpdateOut(&AngleLoop);              //计算输出Motor_SetPWM(AngleLoop.Out);            //调控}}else{Motor_SetPWM(0);}if(RunState){count2++;if(count2>=20){count2=0;PID_UpdateActual(&LocationLoop,location);PID_UpdateOut(&LocationLoop);PID_SetTarget(&AngleLoop,CENTER_ANGLE - LocationLoop.Out);    //调控}}else{}__HAL_TIM_CLEAR_IT(&g_tim1_handle, TIM_IT_UPDATE);  /* 清除定时器溢出中断标志位 */}
}

:::info
最后我的参数是:

角度环(内环):P 2.0 I 0.03 D 6.21

位置环(外环):P 0.5 I 0 D 5.72

说实话这个参数效果只能算一般,不过我已经调累了。

如果你的横臂位置总是于目标有偏差,别着急加 I 项,先看看是不是中心角度设置的不对。如果你真要加,注意做好积分分离即可。

:::

到目前为止,我们的倒立摆已经可以说是完成了。如果你只是为了学习 PID,那下面“自动起摆”的部分可以忽略了(老实说我也不想写了,累死了,燃尽了啊啊啊)。

第四步:添加自动起摆逻辑控制

前言:起摆不只有一种方法,全看各位喜好。

哎,不想写了,回头再写吧。看官有缘再见。

⋰₰(OwO)₰⋱

丰川祥子祝你幸福 desuwa~

http://www.kefakeji.com/news/457.html

相关文章:

  • 分块
  • 并查集
  • 7-27
  • CVE-2021-21311 服务器端请求伪造(SSRF)漏洞 (复现)
  • 【Rag实用分享】小白也能看懂的文档解析和分割教程
  • 【纯干货】三张图深入分析京东开源Genie的8大亮点
  • JoyAgent综合测评报告
  • 【EF Core】为 DatabaseFacade 扩展“创建”与“删除”数据表功能
  • 亚马逊机器学习大学推出负责任AI课程 - 聚焦AI偏见缓解与公平性实践
  • FFmpeg开发笔记(七十八)采用Kotlin+Compose的NextPlayer播放器
  • 4.5.4 预测下一个PC
  • 第十六日
  • 2025“钉耙编程”中国大学生算法设计暑期联赛(3)
  • VMware Windows Linux Macos网盘下载
  • ZBrush 2025 中文版免费下载,附图文安装指南,小白也能快速上手!
  • k8s network
  • hyprland初尝试
  • 正则表达式 更新常用则表达式-----loading
  • 幼儿园小班线段树
  • 树02
  • 深入ADC采样
  • 学习笔记:MySQL :eq_range_index_dive_limit参数
  • Python字符串知识点总结
  • SQL Server 2025年7月更新 - 修复 CVE-2025-49718 Microsoft SQL Server 信息泄露漏洞
  • 读书笔记:Oracle数据库内存结构:系统全局区(SGA)详解
  • 小飞标签
  • 服务器配置的精细化控制(3960)
  • TCP连接优化的实战经验(7340)
  • 家庭主妇人到中年的生活困境很难突破防
  • 中间件架构的优雅实现(0454)