STM32平衡小车(复盘)_stm32平衡小车教程
目录
电机驱动代码编写
驱动模块-tb6612
定时器PWM输出配置
编码器代码编写
编码器模块的实现
获取编码器的数值
姿态角获取代码编写
陀螺仪模块-mpu6050
IIC协议
姿态角获取
PID控制算法代码编写
PID理论
PID控制算法实现
突然一时兴起,想把之前做过的stm32平衡小车重新整理一下,包含代码编写(分模块讲解),简单pid理论的学习。
一个完整的stm32平衡小车项目分为软件代码部分和硬件AD画图打板,其中软件层面设计定时器代码编写,主要用于输出PWM控制电机转速和编码器模块驱动;mpu6050模块的代码编写,设计IIC总线,该模块用于获取小车当前的姿态角;PID代码编写,PID理论的学习是整个项目的核心。代码层面我们分模块一个个来讲解。
电机驱动代码编写
驱动模块-tb6612
在使用tb6612之前,我们先来了解一下什么是直流电机,官方的解释是:直流电机(direct current machine)是指能将直流电能转换成机械能(直流电动机)或将机械能转换成直流电能(直流发电机)的旋转电机。它是能实现直流电能和机械能互相转换的电机。当它作电动机运行时是直流电动机,将电能转换为机械能。我们小车上的电机就直流电机。
决定直流电机转动方向的是电流的方向,决定直流电机的转速是电流的大小。可以通过单片机的引脚改变电流大小方向来控制直流电机,但是单片机IO口带负载能力较弱,所有我们需要借助外部电机驱动。
tb6612模块示意图
为了明确tb6612和电机、单片机之间是如何接线,暂时省去未用到的引脚,接线图如下:
模块接线示意图
上图涉及编码器A、B相,我们暂时不管,主要关注AIN、BIN(控制电机正反转),AO、BO(官方手册说12V 的正负电源分别接 M1 和 M2,若 M1 接+12V,则 M2 接+12V 的 GND。反之即可。即AIN、BIN所谓控制的正反转是相对的,和AO、BO有关),PWMA、PWMB(控制电机转速)。
AO、BO接线不过多赘述,如、像上图连即可,AIN、BIN对应的引脚需要在代码上配置一下,这些引脚能输出0和1即可。
电机初始化代码
这里我们用到了GPIOB端口,所以我们需要使能其时钟。另外值得一提的是IO模式,对于stm32来说,IO模式有:上拉输入模式(GPIO_Mode_IPU)、下拉输入模式(GPIO_Mode_IPD)、模拟输入模式(GPIO_Mode_AIN)、浮空输入模式(GPIO_Mode_IN_FLOATING)、推挽输出模式(GPIO_Mode_Out_PP)、开漏输出模式(GPIO_Mode_Out_OD)、复用推挽输出模式 (GPIO_Mode_AF_PP)和复用开漏输出模式(GPIO_Mode_AF_OD)。
这里我们用到了推挽输出,就稍微了解一下什么是推挽输出。推挽输出既可以输出高电平,也可以输出低电平,连接数字器件。推挽输出的结构一般是指两个三极管分别受到两个互补信号的控制,在一个三极管被导通的时候另一个三极管被截止。
GPIO内部示意图
推挽电路原理图
当IN输入低电平时,P-MOS导通,N-MOS截止,此时OUT输出的电压是VDD,高电平;当IN输入高电平时,N-MOS导通,P-MOS截止,此时OUT输出的电压是VSS,低电平。用推挽输出模式,可以得到一个明确的高电平或者低电平,可以实现高低电平的快速切换。说到推挽电路,那就少不了拉电流、灌电流。
拉电流和灌电流原理图
左图:推挽输出驱动LED时,LED接地,此时电流的方向是从MCU往外输出,这个电流称为拉电流;
右图:推挽输出驱动LED时,LED接电源,此时电流的方向是从外往MCU输入,这个电流称为灌电流。
定时器PWM输出配置
上述引脚的配置仅仅能让电机实现正反转,要让电机能够按某种速度转动需要通过PWM来控制。PWM,即脉冲宽度调制(Pulse Width Modulation),是一种利用数字信号来控制模拟电路的有效技术。其基本原理是通过改变一系列固定频率脉冲的宽度,从而调节这些脉冲的占空比(高电平时间与整个周期时间的比例),以此来模拟连续的模拟信号。
PWM改变输出电压
在stm32中实现PWM改,变电压是通过定时器来实现的,stm32中定时器分为高级、通用和基本定时器。
stm32定时器分类
高级控制定时器框图
本项目中使用定时器1来实现PWM控制转速,要使定时器输出PWM主要配置三个内容:配置GPIO、配置定时器基础参数和配置PWM通道。
定时器1配置PWM代码
配置GPIO部分,GPIOA引脚8(通道1)和引脚11(通道4)配置成了复用推挽输出模式,推挽复用输出模式与推挽输出模式很是类似。只是输出的高低电平的来源不同,不是让CPU直接写输出数据寄存器(由GPIO的ODR寄存器控制),取而代之利用片上外设(此处是TIM1)模块的复用功能输出来决定的。
复用推挽输出GPIO内部电路图
接着是配置定时器基础参数部分,最主要配置:预分频器(PSC):分频系统时钟以设定计数器频率;自动重载值(ARR):决定PWM周期;计数模式:通常选择向上计数;时钟分频因子。
配置定时器基础参数结构体
主要关注预分频器(PSC)和自动重载值(ARR),首先我们要知道定时器时间计算公式:
定时器时间计算公式
其中Tclk是输入时钟频率一般来说是72MHz,PSC用来降低定时器时钟频率的参数,我们这里写0,ARR自动重装载值,是定时器溢出前的计数值,这里我们写7199,Tout定时器溢出时间,根据公式可以得到Tout = 7200 x 1 / 72000000 = 0.001s = 1000us。
关于这个定时器溢出时间,我问了一下deepseek,它说定时器的溢出时间(即PWM周期)是需要精心设计的,它会直接影响电机的工作性能(如噪音、响应速度、驱动效率等)。
因为我们这里ARR取7199,加1就是7200,所以我们改变PWM是需要一个限幅操作,即不能超过7200。
最后一个配置是PWM通道相关的,仅对于输出PWM来说,这个配置我们主要关注:TIM_OCMode(选择输出比较模式),TIM_OutputState(使能或禁用输出通道),TIM_OCPolarity(设置输出极性,即有效电平)。
配置定时器通道相关的结构体
输出比较模式分为PWM1和PWM2,我们这里选择PWM1,即CNT < CCRx时有效,CNT ≥ CCRx时无效,CNT就是就是当前计数的次数,CCRx是捕获/比较寄存器,包含了装入当前捕获/比较1寄存器的值(预装载值),当前捕获/比较寄存器参与同计数器TIMx_CNT的比较,并在OC1端口上产生输出信号。
TIM_OutputState毫无疑问得配置成使能,TIM_OCPolarity决定了PWM信号的有效电平极性(即占空比对应的电平是高电平还是低电平),我们这里选择高电平有效,TIM_OCPolarity的配置需要PWM模式严格匹配。
最后一个是TIM_Pulse,设置比较值(CCRx寄存器的值),决定PWM占空比,0到自动重载值(ARR),后续就是通过改变该值来改变电机得转速。
最后千万不要忘了,几个使能:
易忽视的几个使能
这里特别说一下TIM_CtrlPWMOutputs(TIM1,ENABLE);这是高级定时器独有的,千万不能忘记。这里配置的是刹车和死区寄存器(TIMx_BDTR),虽然我们这里没用到,但是得使能MOE: 主输出使能 (Main output enable)。
编码器代码编写
编码器模块的实现
编码器是一种将角位移或者角速度转换成一串电数字脉冲的旋转式传感器。编码器从输出数据类型上分,可以分为增量式编码器(输出脉冲信号,通过计数脉冲测量相对位移)和绝对式编码器(输出唯一编码值,直接反映绝对位置);从检测原理上来分,还可以分为光学式、磁式、感应式、电容式。常见的是光电编码器(光学式)和霍尔编码器(磁式)。
在电机控制中,增量式编码器最常用,其输出两路正交信号(A相和B相)和一路索引信号(Z相)。我们主要关注输出两组存在一定相位差的方波信号——A、B相。
获取A、B相输出示意图
正转反转的判别
有了上述编码器相关的基础,我们就需要考虑如何采集数据,即如何测速。直接使用定时器的编码器模式,采用四倍频方式,提高测量精度,也就是把AB相的上升沿和下降沿都采集。
左右两个电机,所以有两个编码器,需要两个定时器,通用定时器和高级定时器都可以,我们这里选择定时器2和3,并且使用其通道1和通道2,代码展示以定时器2为例。
通用定时器框图
可以看到编码器接口只能由通道1和通道2输入。
定时器2编码器的配置
主要也是配置三部分内容:配置GPIO、配置定时器基础参数和配置定时器编码器模式。首先关注一下引脚的模式,这里选择的是浮空输入模式(GPIO_Mode_IN_FLOATING)。
浮空输入模式GPIO内部图
浮空输入模式下,I/O端口的电平信号直接进入输入数据寄存器。也就是说,I/O的电平状态是不确定的,完全由外部输入决定;如果在该引脚悬空(在无信号输入)的情况下,读取该端口的电平是不确定的。所以在要读取外部信号时通常配置IO口为浮空输入模式。
至于定时器基础参数的配置在这就不过多赘述了,内容和之前PWM那块差不多,主要讲解一下配置定时器编码器模式。
配置定时器编码器模式结构体成员
这几个成员变量我们一一来分析一下,TIM_Channel毫无疑问就是通道1和通道2了,但是大家仔细看看上面配置编码器相关的代码,并没有配置TIM_Channel这个参数,有人可能想了解到底是怎么回事,可能就会点开TIM_ICStructInit(&TIM_ICInitStructure);这个函数,果不其然里面有TIM_Channel相关的配置:
TIM_ICStructInit函数体实现
很明显只有通道1,那通道2怎么办呢,后面我查了相关资料,意思就是TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);这个函数中有一个形参TIM_EncoderMode_TI12,这表示编码器模式使用TI1和TI2(即通道1和通道2)作为编码器的输入。此函数内部会自动配置通道1和通道2为编码器输入模式,因此不需要显式设置通道的ICInit结构体。
既然说到了TIM_EncoderMode_TI12,刚好就分析一下编码器计数模式,开始我们说到:编码器采用四倍频方式,提高测量精度,也就是把AB相的上升沿和下降沿都采集,所以这个参数的作用就是在T1和T2的每个跳变沿均计数。
T1和T2的每个跳变沿均计数示意图
计数方向与编码器信号的关
但是不知道大家看到这有没有和我一样的疑惑,既然这里配置T1和T2每个跳变沿都计数,为什么后面还得配置有效跳变沿的极性呢,查看stm32的参考手册里关于极性的说明: 捕获 / 比较使能寄存器 (TIMx_CCER) 关于极性的配置说明
再结合deepseek的解释;
大致意思就是不依赖TIM_ICPolarity设置的单一极性,而是根据A/B相的相位关系,隐式捕获所有边沿。 我也不确定这个解释是否正确,也欢迎大家讨论。
TIM_ICSelection这个参数就是把通道配置成输入:
捕获 / 比较模式寄存器 1(TIMx_CCMR1)配置输入模式 最后剩下TIM_ICPrescaler和TIM_ICFilter,一个是 预分频器一个是滤波,一般来说就是不分频,即捕获输入口上检测到的每一个边沿都触发一次捕获,滤波就是<= 0xF即可。
获取编码器的数值
读取CNT的值
有了编码器测出来的数值我们就可以推算出速度,但是在平衡小车这个项目中并不需要精确的速度,所以在这里我不做详细的讲解,从deepseek上拷贝计算公式附在下面:
速度计算公式
姿态角获取代码编写
陀螺仪模块-mpu6050
MPU6050是一款六轴姿态传感器,能够测量芯片自身在 X、Y、Z 三个轴向上的加速度和角速度参数。通过对这些数据进行融合处理,可以进一步计算出设备的姿态角,从而精确感知设备的倾斜和旋转状态。这种能力使MPU6050 成为许多需要检测自身姿态的应用场景中的核心元件。
之所以说MPU6050是六轴运动处理模块,是因为里面集成了两个关键的传感器——陀螺仪和加速度计,陀螺仪可以测量物体绕着三个轴旋转的速度,也称为角速度。角速度是指物体每秒钟绕轴旋转的角度。通过陀螺仪的数据,你可以知道设备是在左右旋转、上下颠倒还是前后倾斜;加速度计可以测量物体在三个轴上的线性加速度。这里的加速度不仅仅指的是物体加速时的情况,还包括重力加速度。因此,即使静止不动的物体也会有一个加速度计读数,这个读数反映了重力的方向。通过分析加速度计的数据,可以判断出设备的大致朝向和是否在移动。
三个轴怎么看,这是一个关键,几何上来说我们可以分为X轴、Y轴和Z轴:
轴向表示
X 轴角度(滚转角 Roll)即为绕 X 轴旋转方向的角度,Y 轴角度(俯仰角 Pitch)即为绕 Y 轴旋转方向的角度,Z 轴角度(偏航角 Yaw)即为绕 Z 轴旋转方向的角度,三者合称姿态角/欧拉角(Euler angles)。
如何让mpu6050与stm32进行通信呢,我们先来看看mpu6050模块有哪些引脚:
mpu6050实物图
mpu6050引脚说明
很明显了,就是IIC通信了,具体IIC的实现我们后续详细讲解,我们可以看到一个XDA和一个XCL引脚,MPU6050 可以外接一个 IIC 设备,通常可以外接一个三轴的磁力计来实现完整的九轴输出。
知道了MPU6050是什么,能干什么之后,我们需要对其整体一个框架进行简单分析:
MPU6050系统框图
MPU6050 对陀螺仪和加速度计分别用了三个16位的 ADC,将其测量的模拟量转化为可输出的数字量,并保存到寄存器中供用户读取。
前面提到数据保存到寄存器中,那我们就来了解一下MPU6050中的主要用到的寄存器:
MPU6050 Register Map
可以看到MPU6050的寄存器手册中说明了很多寄存器,我们
通过配置这些寄存器,可以实现对MPU6050的初始化和数据读取。相关寄存器具体的描述说明大家可以找MPU6050寄存器手册自行翻找。
IIC协议
IIC(Inter-Integrated Circuit)总线是一种由NXP(原PHILIPS)公司开发的两线式串行总线,用于连接微控制器及其外围设备。多用于主控制器和从器件间的主从通信,在小数据量场合使用,传输距离短,任意时刻只能有一个主机等特性。
IIC总线物理拓扑图
可以看到IIC通信主要用到两根线:SCL(数据线,控制数据发送的时序)和SDA(数据线,用来传输数据),主机控制SCL,产生时钟信号,至于主机此时和哪个从机通信,就看主机选择了哪个从机的地址,每个从机都有自己的地址。而且,值得一提的是IIC是半双工,而不是全双工 ,同一时间只可以单向通信。
这里对于IIC的硬件电路图做一些补充,关于上拉电阻,SCL和SDA都需要接上拉电阻,为什么呢,是因为要求各设备接到总线的输出的需要开漏输出,这样可以实现多个设备从设备共享总线,对于开漏输出模式,其IO口只能输出低电平或高阻态,前面我们说到的推挽输出IO口可以输出高电平或低电平,这里也简单总结一下,IO口的输出就三种状态:高电平、低电平和高阻态。我们来看看开漏输出的电路图:
开漏输出GPIO电路图及线与状态
开漏输出有什么作用呢?1:改变高电平电压;
高阻态接上拉电阻改变高电平电压
2:线与(几个GPIO同时控制输入)
线与
至于这个上拉电阻的取值,一般来说,经验的取值是4.7k或10k,不要太大或太小,太小会导致流经mos管的电流过大,可能烧坏mos管,电流一般来说要小于3mA;电阻太大可能会导致高低电平变换时间较长。
值得注意的是多台设备不需要并联上拉电阻,不然等效电阻阻值就不是我们想要的了。还有IIC的三种传输模式:标准模式:100Kbit/s,快速模式:400Kbit/s,高速模式:3.4Mbit/s(很多IIC设备不支持)。
有了这些前置知识后,我们就要探讨IIC的数据传输了。IIC的数据主要就是依靠SCL、SDA两根线,以及高、低电平来传输。
SDA上可以传输0101这样的二进制数,但是仅仅只有SDA线我们不能分隔数据,这时就需要SCL时钟同步线,一般来说,规定SCL高电平时表示工作状态,低电平时表示休息状态,并且SCL按固定频率进行高低电平直接的变换。在数据传输过程中,我们要求在SCL高电平时,主机SDA上的数据不能变换,即不能发生高低电平的转换,此时从机检测到SCL处于工作状态,就会把SDA上的数据读取下来,如果主机需要改变SDA上的数据,就要等到SCL低电平时,改变SDA的高低电平,此时从机检测到SCL处于休息状态,SDA上的数据不会被读取下来,等到SCL再变成高电平,即工作状态时,此时SDA数据也已经发生改变了,从机读取的SDA上的数据就是更新过的数据了。
数据传输时序图
按照上述说法,就可以无限次发送二进制数,直到数据全部发完,但仅仅这样还是不够,首先我们不知道我们要发送的数据什么时候开始发,什么时候发送结束,所以我们需要引入起始信号和结束信号。
我们需要在第一个工作期间前加入起始信号,在最后一个工作期间后加入结束信号,
起始和终止信号说明
这两个信号如何表示呢,我们查看MPU6050的数据手册,可以看到这样一段话:
起始和终止信号原文说明
意思就是说,起始信号:SCL线处于高电平时,SDA线从高电平到低电平的转换;终止信号:SCL线仍处于高电平时,SDA线从低电平到高电平的转换。重点在于SCL高电平时,SDA发送跳变。
其时序图如下:
起始和终止信号时序图
有了上述的起始和终止信号还不够,要想完整进行IIC的数据传输,我们需要确保接收方能正确接收到数据,所以我们还需要引入应答位(ACK)。
数据传输中,每8位后面接一个应答位,即第9个工作期间,接收方发送应答信号,
应答信号说明
查看MPU6050数据手册:
ACK信号原文说明
阅读手册,我们得知,因为SDA总线外接上拉电阻,所以空闲时,SDA默认是高电平,如果接收设备接收到信号,应答:SCL为高电平,SDA保持低电平。
ACK信号时序图
引入了上述几个信号,IIC协议才能正确传输数据,
简易完整时序示意图
有了上述基础后,我们可以来讨论IIC的数据格式了,
IIC数据传输原文及时序图
MPU6050的数据手册上提到,“开始通信时,主设备首先发送一个7位的从设备地址,随后是一个第8位,即读/写位。读/写位指示主设备是从从设备接收数据还是向其写入数据(0表示写,1表示读)。然后,主设备释放SDA线并等待从设备发出的确认信号(ACK)”。
关于MPU6050的地址,其是数据手册上也有说明,
MPU6050地址原文说明
数据手册上说到,“MPU6050始终作为从设备,MPU-60X0的从地址是b110100X,共7位。7位地址中的LSB位由AD0引脚的逻辑电平决定。这使得两个MPU-60X0可以连接到同一I2C总线上。在这种配置下,其中一个设备的地址应为b1101000(AD0引脚为低电平),另一个设备的地址应为b1101001(AD0引脚为高电平)”。
选中了读写的从设备后,需要选择读写的寄存器,首先来看看写寄存器:
写寄存器数据手册说明
简单来说就是主机选择从机后,并选择写模式,从机回复ACK,主机再发送要写的寄存器地址,从机回复ACK,主机再发送要写的数据,从机回复ACK,最后主机发送终止信号。
值得一提的是,“在最后一个ACK信号之后,如果需要写入多个字节,主设备可以继续输出数据而不是发送停止信号。在这种情况下,MPU-60X0会自动递增寄存器地址并将数据加载到相应的寄存器中”。
读寄存器比写寄存器稍显复杂,
读寄存器数据手册说明
对读MPU6050寄存器来说,需要发送两次起始信号,原文:“要读取内部MPU-60X0寄存器,主设备发送启动条件,随后是I2C地址和写入位,然后是待读寄存器的地址。接收到MPU-60X0发出的ACK信号后,主设备发送启动信号,接着是从设备地址和读取位”。
即一次是告诉选择哪个从设备,及读哪个寄存器,一次是选择读模式。
读写寄存器数据格式示意图
姿态角获取
现在我们对MPU6050和IIC协议有了相关的知识储备后,就可以开始编程来获取MPU6050测量处理的数据。
首先配置引脚,我们选用PB6作为SCL,PB7作为SDA,
软件IIC的GPIO配置
起始信号触发,
起始信号代码编写
终止信号触发,
终止信号代码编写
产生ACK应答信号,
应答信号编写
产生NACK不应答信号,
非应答信号编写
等待应答信号,
等待应答信号代码编写
发送一个字节数据,
发送一个字节数据代码编写
接收一个字节数据,
接收一个字节数据代码编写
写寄存器操作,包括选择从机地址,选择写模式,待写寄存器地址和待写数据,
写寄存器代码编写
读寄存器操作,包括选择从机地址,选择写模式并先写入待读寄存器地址,再次触发起始信号,选择读模式,
读寄存器代码编写
连续写数据,上文说到,指向寄存器的指针,即其地址会自增,
连续写数据代码编写
连续读数据,
连续读数据代码编写
有了上述基础读写函数后,我们要开始对MPU6050相关寄存器进行操作,首先对MPU6050进行初始化,
MPU6050初始化代码编写
获取MPU6050测量处理的数值,
获取MPU6050测量值代码编写
这里我说明一下,以上代码不可直接使用,部分是我写的,部分是截取大鱼电子给出的平衡车代码,为了方便理解,我折中选取了一些,搞懂了原理,看这些代码就会很简单了。
以上代码可以实现直接获取MPU6050的测量数值,但是这些值是仅仅是角速度和加速度,我们用于后续PID控制,还需要最后一步,就是姿态角的解算,即获得pitch:俯仰角、roll:横滚角和yaw:航向角。
姿态角的解算涉及较多的数学知识,有兴趣的话大家可以自行去了解,总之,我们想获取姿态角直接在网上找以下文件即可,
姿态角解算代码
通过上述函数,就可以获取姿态角,以便后续PID的控制。
PID控制算法代码编写
PID理论
PID控制算法是一种广泛应用于工业控制系统的反馈控制方法,通过调节比例(Proportional)、积分(Integral)和微分(Derivative)三个环节的参数,实现对系统的精确控制。
PID算法是典型的闭环控制算法,闭环控制算法核心是:PID控制通过实时采集被控对象的实际输出值(如温度、转速、位置等),与设定值(目标值)进行比较生成误差信号,并基于误差的比例(P)、积分(I)、微分(D)三部分动态调整控制量。例如:
比例项:直接反映当前误差,快速响应偏差,系数大,可以加快调节,减小误差,但过大的比例使系统稳定性下降,甚至造成系统不稳定;
积分项:累积历史误差,消除稳态误差(如温度控制系统中的微小偏差);
微分项:预测误差变化趋势,抑制超调和振荡,反映系统偏差信号的变化率 e(t)-e(t-1),具有预见性,能预见偏差变化的趋势,产生超前的控制作用,在偏差还没有形成之前,已被微分调节作用消除,因此可以改善系统的动态性能。但是微分对噪声干扰有放大作用,加强微分对系统抗干扰不利;
PID控制系统框图:
PID控制系统框图
该结构由主要由三部分组成:传感器:测量被控对象的实际输出值(如温度传感器、MPU6050等);控制器(PID算法):计算误差并生成控制量;执行机构:根据控制量调整系统(如调节阀门开度或电机转速)。
这里补充一下,与闭环控制算法对用的是开环控制算法,开环控制:仅根据设定值输出控制信号,不依赖被控量的反馈(如简单的定时开关控制)。其优点是结构简单,但无法自动修正环境扰动或系统误差。
模拟PID计算公式:
模拟PID计算公式
模拟PID主要通过模拟电路(如运算放大器、电阻、电容等)实现比例、积分、微分控制,直接处理连续信号(如电压、电流)。
数字PID通过计算机或微控制器(如MCU、DSP)编程实现,将连续信号离散化为数字信号后处理。在单片机系统中处理的一般是数字量,所以要想实现PID算法可能不能用上述模拟PID计算公式,而要用数字PID计算公式:
数字PID计算公式及说明
这里说明一下,数字PID分为位置式PID和增量式PID,上述公式描述的是位置式PID。位置式PID:直接输出控制量的绝对值(如阀门开度、电机目标位置),公式中累积历史误差的积分项;增量式PID:输出控制量的增量(如阀门开度变化量、电机转动角度增量),仅依赖最近几次误差值。在平衡小车PID的实现中,我们用到的式位置式PID,所以暂时先不关注增量式PID。
在我们平衡小车项目中PID框架里的执行机构就是电机转速,而电机转速是由PWM的占空比决定的,所以经过PID算法处理后的结果会作用到定时器PWM上,如图所示:
平衡小车PID控制框图
对于平衡小车项目来说,需要实现直立换(PD)、速度环(PI)和转向环(PD)。
直立环(角度控制)
实时调整小车倾角,维持直立平衡,采用PD控制(比例-微分),无需积分项(I),避免响应滞后和振荡。
直立环作用示意图
平衡小车直立环的调试过程包括确定平衡小车的机械中值、确定kp值的极性(也就是正负号)和大小、kd值的极性和大小等步骤。
1、确定平衡小车的机械中值:把平衡小车放在地面上,绕电机轴旋转平衡小车,记录能让小车接近平衡的角度,一般都在0°附近的。
2、估算kp值的大小与极性(令kd=0):首先我们估计kp的取值范围。我们的PWM设置的是7200代表占空比100%,再考虑避免电机的死区,我们直立环返回的PWM在7000左右的时候电机就会满载。假如我们设定kp值为700,那么平衡小车在±10°的时候就会满转。根据我们的感性认识,这显然太大了,那我们就可以估计kp值在0~700之间。首先大概我们给一个值kp=-200,我们可以观察到,小车往哪边倒,电机会往反方向加速让小车倒下,就是一个我们不愿看到的正反馈的效果。说明kp值的极性反了,接下来我们设定kp=200,这个时候可以看到平衡小车有直立的趋势,虽然响应太慢,但是,我们可以确定kp值是正的。具体的数据接下来再仔细调试。
3、确定kp值的大小(令kd=0):设定kp=100,这个时候我们可以看到,小车虽然有平衡的趋势,但是显然响应有点慢了;设定kp=250,这个时候我们可以看到,小车虽然有平衡的趋势,而且响应有所加快,总体感觉良好;设定kp=500,这个时候我们可以看到,小车的响应明显加快,而且来回推动小车的时候,会有一定幅度的低频抖动。说明这个时候kp值已经足够大了,需要增加微分控制削弱p控制,抑制低频抖动。
4、确定kd值的极性与大小(令kp=0):我们得到的MPU6050输出的陀螺仪的原始数据,通过观察数据,我们发现最大值不会超过4位数,再根据7000代表占空比100%,所以我们估算kd值应该在0~2之间。我们先设定kd=-0.5,当我们拿起小车旋转的时候,车轮会反向转动,并没有能够实现跟随效果。这说明了kd的极性反了;接下来,我们设定kd=0.5,这个时候我们可以看到,当我们旋转小车的时候,车轮会同向以相同的速度跟随转动,这说明我们实现了角速度闭环,至此,我们可以确定kd的极性是正的。不断尝试其他值,直到达到高频小幅的状态。
速度环(速度控制)
维持小车速度稳定,抑制因直立环调整引起的位移漂移。而且假设车模在上面直立控制调节下已经能够保持平衡了,但是由于安装误差,传感器实际测量的角度与车模角度有偏差,因此车模实际不是保持与地面垂直,而是存在一个倾角。在重力的作用下,车模就会朝倾斜的方向加速前进。如果没有速度调节是难以维持0速度的。采用PI控制(比例-积分),积分项消除稳态误差,如静止时的小范围移动。
1. 确定kp的范围:积分项由偏差的积分得到,所以积分控制和比例控制的极性相同的,而根据工程经验,在不同的系统中,PID 参数相互之间会有一定的比例关系。在我们的平衡小车速度控制系统里面,一般我们可以把ki 值设置为ki = kp/200;我们发现两路编码器相加最大值在160左右,而由经验可知,一般平衡小车行驶的最快速度不会超过电机最大速度的40%,再根据PWM = 7000时,在加上电机死区、占空比接近100%,我们可以大概估算kp 最大=7000/(160*40%)=109.37。
2.确定kp值的极性(关闭直立环):当我们旋转其中一个小车轮胎的时候,两个轮胎会往相同的方向加速,直至电机的最大速度,这是典型的正反馈效果,也是我们期望看到的。
3.确定kp值(打开直立环):设定kp=30,ki=kp/200这个时候我们可以看到,小车的速度控制比较弱,很难让速度恒定;设定kp=50,ki=kp/200这个时候我们可以看到,小车的速度控制的响应有所加快,静止抖动可接受;设定kp=-70,ki=kp/200这个时候我们可以看到,小车虽然回正力度增大了,而且响应更加快了,但是稍微加入一点的干扰都会让小车大幅度摆动,抗干扰能力明显不足,所以这组参数不可取。
转向环(方向控制)
控制左右轮差速,实现转向或抑制非期望旋转,采用PD控制(比例-微分),根据场景选择比例(P)或微分(D)控制;抑制转向(如直线行驶):采用微分项(D),通过陀螺仪Z轴角速度反馈抑制自旋,主动转向(如遥控转向):采用比例项(P),根据目标转向角度调整差速。
这里也有一个项目工程经验的说法:kp=100*kd。
PID控制算法实现
MPU6050模块数值的读取通过外部中断来实现,该模块上引出了中断引脚,所以先初始化一下外部中断:
外部中断初始化函数
这里说明一下:
各个环PID算法实现:
直立环PD算法代码实现
这里关注一下Gyro,这是y轴角速度(所谓x轴、y轴是根据自己如何摆放MPU6050这个模块的),大家可以回顾一下微分算法是怎么样的,即kd*两次误差之差,为什么这里是角速度呢,回想一下角速度的公式,就知道为什么使用角速度了,当然我看网上其他代码也有直接写两次误差之差的形式。
速度环PI算法代码实现
这里编码器的值乘0.8、0.2是一阶低通滤波,为了防止编码器数值发生突变。而且积分是一个累计的过程,所以这里Encoder_Integral是静态变量,电机关闭后清除积分。
转向环PD算法代码实现
这里要注意需要抑制转向时,则需要kd微分调节,主动转向时,要去掉转向约束,即kd=0。
外部中断里进行PID调控代码
这里因为编码器是对称安装的,所以有一个需要进行取反操作,经过直立环、速度环即可保持平衡了,转向换是用于蓝牙控制转向时起作用的,因为是差速转弯,所以最后加载给motor的pwm一个加一个减。
什么时候转弯以及转多少角度的数据,通过蓝牙来传输:
蓝牙控制转向代码
全局参数定义
以上就是PID平衡小车相关的知识点,代码不是完整的,结合网上讲解、自己之前的调参操作以及最后的理解分析给出的一套我认为好理解的代码及文字说明,涉及标准库和hal库,重在理解吧。