Micropython——基于PYB的霍尔编码器电机的PID控制

因为打算用PYB做机器人核心板,那么作为机器人底盘的最基本控制算法PID必然不可或缺。我自己写了一个PID算法的小py文件,可以在主函数中直接进行调用,采用PID算法可以让机器人的行驶速度达到设定的速度,使机器人实现闭环控制。

硬件基础

采用PYB、L298N、编码器电机进行实战验证,接线和编码器电机的使用可以参考我的这篇博客。

Micropython——基于PYB的霍尔编码器电机测速与使用

需要注意的是,在霍尔编码器电机的PID控制中,我们需要用到硬件模块:

  • PYB
  • L298N 或 TB6612
  • 霍尔编码器电机
  • 12V电源或可调电压源

电机的接线如下(如果是有两个电机需要进行接线,参考仓库中的接线图):
在这里插入图片描述

在这里需要注意的是:接ENA引脚时,需要将跳线帽拔掉,再将PWM引脚接到靠外的ENA引脚上,不要接到靠里边的引脚上!

在这里插入图片描述

PID算法理论基础

这里可以直接参阅我写的这篇关于PID控制理论的博客,读者如果还是觉得不太懂,也可以再找找有其他大佬的关于PID理论的博客进行深入学习。

Algorithm——PID控制算法理论

PID控制的相关代码模块准备

在pid控制前,需要准备好如下部分代码:

  • 电机驱动模块
  • 编码器读取模块
  • PID模块

电机驱动模块

在这里我们只是用一个电机进行控制,因为我做的是一个差速的机器人底盘,所以写的就是两个电机的驱动,这里可以只使用单个的即可,同时你也可以在此基础上自行改动。

motor.py

from pyb import Pin, Timer
import pid

class Motor:
    def __init__(self, pwm_range):
        # 左轮A, 右轮B
        self.io_p_A = Pin("Y5", Pin.OUT_PP)  # 电机A的正极IO口
        self.io_n_A = Pin("Y6", Pin.OUT_PP)
        self.io_p_B = Pin("X11", Pin.OUT_PP)  # 电机B的正极IO口
        self.io_n_B = Pin("X12", Pin.OUT_PP)
        self.io_PWM_A = Pin("Y3", Pin.OUT_PP)   # 电机A、B的PWM输入IO口
        self.io_PWM_B = Pin("Y4", Pin.OUT_PP)
        timer_M = Timer(4, freq=1000)    # PWM输出定时器
        self.ch_PWM_A = timer_M.channel(3, Timer.PWM, pin=self.io_PWM_A)
        self.ch_PWM_B = timer_M.channel(4, Timer.PWM, pin=self.io_PWM_B)
        self.pwm_range = pwm_range  # PWM范围

    def motor_run(self, pwm_A, pwm_B):
        '''
        函数功能:电机驱动
        入口参数:两个电机的PWM值(范围为pwm_range)
        '''
        self.ch_PWM_A.pulse_width_percent(100*(pwm_A/self.pwm_range)) # 给电机PWM占空比,默认pwm_range为100
        self.ch_PWM_B.pulse_width_percent(100*(pwm_B/self.pwm_range))
        # print("pwm_A:", 100*(pwm_A / self.pwm_range))
        # print("pwm_B:", 100*(pwm_B / self.pwm_range))


    def direction_control(self, pwm_A, pwm_B):
        '''
        函数功能:控制方向,并将PWM值转换为正值
        入口参数:两个电机的PWM值(范围为pwm_range)
        返回值 :两个电机的正PWM值
        '''
        def ahead_m_A():
            self.io_p_A.high(); self.io_n_A.low();
            # self.io_p_A.low(); self.io_n_A.high();
        def back_m_A():
            self.io_p_A.low(); self.io_n_A.high();
            # self.io_p_A.high(); self.io_n_A.low();
        def ahead_m_B():
            self.io_p_B.high(); self.io_n_B.low();
            # self.io_p_B.low(); self.io_n_B.high();
        def back_m_B():
            self.io_p_B.low(); self.io_n_B.high();
            # self.io_p_B.high(); self.io_n_B.low();
        def stop():
            self.io_p_A.low(); self.io_n_A.low();
            self.io_p_B.low(); self.io_n_B.low();
        
        if (pwm_A > 0 and pwm_B >= 0) or (pwm_A >= 0 and pwm_B > 0): # 向前
            ahead_m_A()
            ahead_m_B()
            # print("ahead")
        elif (pwm_A < 0 and pwm_B <= 0) or (pwm_A <= 0 and pwm_B < 0): # 向后
            back_m_A() 
            back_m_B()
            # print("back")
        elif pwm_A < 0 and pwm_B > 0: # 原地左转,只有当没有其他动作时才会原地左转
            back_m_A()
            ahead_m_B()
            # print("left")
        elif pwm_A > 0 and pwm_B < 0: # 原地右转,只有当没有其他动作时才会原地右转
            ahead_m_A()
            back_m_B()
            # print("right")
        else:
            stop()
            # print("stop")

        pwm_A = abs(pwm_A)
        pwm_B = abs(pwm_B)
        return pwm_A, pwm_B

增量式PID模块

写好后已经测试过没有问题,可以作为包import进来直接使用

pid.py

import pyb
'''
PID使用说明:
    输入参数:当前的编码器值,目标速度对应的编码器值
    输出参数:此时应当的给电机的pwm值
'''
class PID:
    def __init__(self, pwm_range, kp_A, ki_A, kd_A, kp_B, ki_B, kd_B):
        pwm_A = 0
        pwm_B = 0
        err = 0
        err_A = 0
        err_B = 0
        last_err_A = 0
        last_err_B = 0
        self.pwm_range = pwm_range
        self.kp_A = kp_A
        self.ki_A = ki_A
        self.kd_A = kd_A
        self.kp_B = kp_B
        self.ki_B = ki_B
        self.kd_B = kd_B
        self.pwm_A = 0
        self.pwm_B = 0
        self.err = err
        self.err_A = err_A
        self.err_B = err_B
        self.last_err_A = last_err_A
        self.last_err_B = last_err_B
    
    # 增量PID原理代码
    def incremental_pid(self, now, target):
        '''
        函数功能:增量PI控制器
        入口参数:当前的编码器值,目标速度对应的编码器值
        返回值  :电机PWM
        根据增量式离散PID公式:
        pwm += Kp[e(k) - e(k-1)] + Ki*e(k) + Kd[e(k) - 2e(k-1) + e(k-2)]
        e(k)代表本次偏差;
        e(k-1)代表上一次的偏差;    以此类推
        pwm代表增量输出。
        '''
        err = target - now
        pwm = pwm + self.kp*(err - last_err) + self.ki*err + self.kd*(err - last_err)
        if (pwm >= self.pwm_range): # 限幅,防止pwm值超出100
            pwm = self.pwm_range
        if (pwm <= -self.pwm_range):
            pwm = -self.pwm_range
        last_err = err
        return pwm

    def pid_A(self, now, target):
        self.err_A = target - now
        self.pwm_A = self.pwm_A + self.kp_A*(self.err_A - self.last_err_A) + self.ki_A*self.err_A + self.kd_A*(self.err_A - self.last_err_A)
        if (self.pwm_A >= self.pwm_range):
            self.pwm_A = self.pwm_range
        if (self.pwm_A <= -self.pwm_range):
            self.pwm_A = -self.pwm_range
        self.last_err_A = self.err_A
        # print("pwm_A", self.pwm_A)
        return self.pwm_A

    def pid_B(self, now, target):
        self.err_B = target - now
        self.pwm_B = self.pwm_B + self.kp_B*(self.err_B - self.last_err_B) + self.ki_B*self.err_B + self.kd_B*(self.err_B - self.last_err_B)
        if (self.pwm_B >= self.pwm_range):
            self.pwm_B = self.pwm_range
        if (self.pwm_B <= -self.pwm_range):
            self.pwm_B = -self.pwm_range
        self.last_err_B = self.err_B
        return self.pwm_B

编码器读取计数模块

写好后测试没问题,也直接可以使用,但需要改变对应的引脚,因为大家的定时器、引脚使用可能不同。同时,在改变引脚的时候一定要注意,要选择高级定时器,因为只有高级定时器才可以配置为编码器模式。

encoder.py

import pyb
from pyb import Timer, Pin
import cmath
"""
入口参数:编码器读取频率

# 编码器电机参数参考
    1.光电编码器电机(MG513P30_12V)的参数:
        减速比:1:30
        分辨率(电机驱动线数):500ppr(编码器精度,转一圈输出的脉冲数)

    2.霍尔编码器电机()的参数:
        减速比:1:30
        分辨率:13ppr

# 底盘机械参数参考
    标准轮式机器人硬件尺寸参数:
        轮子直径:6.5cm
        两轮间距:16cm

    履带式机器人硬件尺寸参数:
        轮子直径:4cm
        两轮间距:23cm
"""

class Encoder:
    '''
    机器人自身及编码器电机配置
    '''
    def __init__(self, encoder_freq):
        '''————————————————————————————可调参数——————————————————————————————————'''
        # -------机器人机械参数--------
        self.wheel_distance = 0.160         # 轮间距,单位:cm
        self.Wheel_diameter = 0.065         # 轮直径:单位:cm
        # -------机器人编码器电机参数--------
        reduction_ratio = 1/30              # 减速比,
        resolution_ratio = 500              # 分辨率,单位:ppr
        
        '''————————————————————————————系统参数——————————————————————————————————'''
        #配置编码器AB相读取模式,利用定时器中断进行读取
        p1_A = Pin("Y1"); p2_A = Pin("Y2");
        tim_A = Timer(8)
        tim_A.init(prescaler=0,period=10000)
        ch1_A = tim_A.channel(1,Timer.ENC_AB,pin=p1_A); ch2_A = tim_A.channel(2,Timer.ENC_AB, pin=p2_A);
        p1_B = Pin("X1"); p2_B = Pin("X2");
        tim_B = Timer(5)
        tim_B.init(prescaler=0,period=10000)
        ch1_B = tim_B.channel(1,Timer.ENC_AB,pin=p1_B); ch2_B = tim_B.channel(2,Timer.ENC_AB, pin=p2_B)
        
        # 初始化系统参数
        freq_multiplier = 4 # 倍频数
        self.pi = cmath.pi # pi的值
        self.encoder_freq = encoder_freq
        self.encoder_precision = freq_multiplier * resolution_ratio / reduction_ratio # 编码器精度 = 倍频数*编码器精度(电机驱动线数)*电机减速比
        self.tim_A = tim_A
        self.tim_B = tim_B
        self.encoder_A = 0
        self.encoder_B = 0
        # 定时器中断:固定时间读取编码器数值
        timer_read = Timer(10,freq=self.encoder_freq,callback=self.encoder_cb)

    def encoder_cb(self, encoder):
        '''
        函数功能:通过定时器中断读取编码器A、B计数值,并重置编码器
        '''    
        if self.tim_B.counter() > 5000:
            self.encoder_B = 10000 - self.tim_B.counter()
        else:
            self.encoder_B = -self.tim_B.counter()
        self.tim_B.counter(0)
        if self.tim_A.counter() > 5000:
            self.encoder_A = -(10000 - self.tim_A.counter()) 
        else:
            self.encoder_A = self.tim_A.counter()
        self.tim_A.counter(0)

    def target_enc_process(self, speed):
        '''
        函数功能:将目标速度值转化为目标编码器值
        入口参数:目标速度值
        返回值  :目标编码器值
        # 目标编码器值=(目标速度*编码器精度)/(轮子周长*控制频率)
        '''
        enc = (speed*self.encoder_precision) / ( (self.pi*self.Wheel_diameter) * self.encoder_freq)
        return enc
    
    def enc_to_speed(self, enc):
        '''
        函数功能:将编码器值转化为速度值
        入口参数:编码器值
        返回值:速度值
        速度值=(采集到的脉冲数/编码器精度)*轮子周长*控制频率
        '''
        speed = (enc / self.encoder_precision) * (self.pi*self.Wheel_diameter) * self.encoder_freq
        return speed
    
    '''
    # 已知量,单位m
        v = 0 # 机器人的线速度
        w = 0 # 机器人的角速度
        v_l = 0 # 左轮速度
        v_r = 0 # 右轮速度
        D = 0.2 # 轮间距
        d = D/2
    # 速度、角速度、左右轮速的关系式
        v_l = v + wd
        v_r = v - wd
        v = (v_l + v_r)/2 
        w = (v_l - v_r)/D
    '''

    def speed_to_anglin(self, speed_A, speed_B):
        '''
        函数功能:将两轮速度值转化为角速度和线速度值
        入口参数:两轮速度值
        返回值:角速度,线速度
        '''
        linear_vel = (speed_A + speed_B)/2
        angular_vel = (speed_B - speed_A)/self.wheel_distance
        return angular_vel, linear_vel

    def anglin_to_speed(self, angular_vel, linear_vel):
        '''
        函数功能:将角速度和线速度值转化为两轮速度值
        入口参数:角速度,线速度
        返回值:两轮速度值
        '''
        speed_A = linear_vel - angular_vel * self.wheel_distance/2
        speed_B = linear_vel + angular_vel * self.wheel_distance/2
        return speed_A, speed_B

PID控制电机

在这里的PID控制器中,我们采用:

  • 输入:目标编码器值和当前编码器值
  • 输出:电机预期的PWM值
  • 参数:P、I、D值

这里需要注意的是:目标编码器值是将目标速度经过编码器速度公式计算得出,公式可以参考encoder.py中的target_enc_process()函数,在这里我已经将公式封装成了一个函数直接进行调用即可。

主控代码

main.py

import pyb
from pyb import LED, Timer

import encoder
import pid
import motor

def mainTimer_cb(cb):
    '''
    函数功能:main函数的定时器中断函数,用于总体程序逻辑的定时执行
    修改:修改对应的程序时间,以及程序的功能标志位(flag)
    '''
    global count, reset_flag, enc_flag, pid_flag, toggle_flag
    LED(3).toggle()
    count += 1
    if (count%1000) == 0: # 重置计数 1s
        reset_flag = True
        if (count%10) == 0: # pid刷新并控制电机 15ms
            pid_flag = True
        if (count%500) == 0: # LED闪烁 500ms
            toggle_flag = True

if __name__ == '__main__':
    
	# 初始化
    count = 0 # 程序定时计数初值
    reset_flag = False
    toggle_flag = False
    pid_flag = False

    # 初始化系统参数
    pwm_range = 100 # PWM的范围(0-100),默认值,不要改动
    encoder_freq = 100 # 编码器读取频率,和电机控制频率相同    

    target_enc_A = 10 # 初始化时的  速度值
    target_enc_B = 10
    
    encoder_freq = 100 # 编码器读取频率
    '''
        机器人参数整定:
        1.先在encoder.py中调整机器人机械参数、编码器参数
        2.调整以下PID参数,一般只用PI即可。
    '''
    # -------编码器PID参数--------
    kp_A = 0.8
    ki_A = 0.3
    kd_A = 0
    kp_B = 1.0
    ki_B = 0.3
    kd_B = 0

	# 定时器初始化:主程序逻辑控制,频率为1000Hz,每次计时1ms
    mainTimer = Timer(1, freq=1000, callback=mainTimer_cb) 
    print("定时器初始化----")
	# 编码器初始化
    enc = encoder.Encoder(encoder_freq)
    print("编码器初始化----")
    # 初始化PID控制器
    pid = pid.PID(pwm_range, kp, ki, kd)
    print("PID初始化----")
    # 初始化电机
    motor = motor.Motor(pwm_range)
    print("电机初始化----")

#-----------------------逻辑循环--------------------------#
    #主程序逻辑
    while True:
        # 重置计数器 1s
        if reset_flag == True:
            count = 0
            reset_flag = False

        # pid刷新并控制电机 15ms
        if pid_flag == True:
            enc_A = enc.encoder_A
            enc_B = enc.encoder_B
            target_enc_A = enc.target_enc_process(target_speed_A)
            target_enc_B = enc.target_enc_process(target_speed_B)
            if target_enc_A > 0 or target_enc_B > 0: 
	            pwm_A = pid.pid_A(cur_enc_A, target_enc_A) 
	            pwm_B = pid.pid_B(cur_enc_B, target_enc_B)
	        elif target_enc_A < 0 or target_enc_B < 0:
	            pwm_A = pid.pid_A(cur_enc_A, target_enc_A) 
	            pwm_B = pid.pid_B(cur_enc_B, target_enc_B)  
	        else:
	            pwm_A = 0
	            pwm_B = 0
	        pwm_A = pid.pid_A(cur_enc_A, target_enc_A) 
	        pwm_B = pid.pid_B(cur_enc_B, target_enc_B) 
	        pwm_A, pwm_B = motor.direction_control(pwm_A, pwm_B)
	        self.motor.motor_run(pwm_A, pwm_B)
            pid_flag = False


效果展示

在只加了P的情况下,大家可以自行改变参数进行调节验证,这里仅做原理性的讲解和功能性演示。同时利用自己写的可视化绘图工具实时显示了一下pid调节情况:

在这里插入图片描述

下图是我用单个电机进行试验的画面,在上图中我是使用的两个编码器电机的底盘进行调节,所以如果大家使用单个电机,那么只需要大家需要微调一下上边的代码即可,将A或B任意的量全部置零即可。
在这里插入图片描述

在这里插入图片描述

PID调节技巧

最后给大家大概说一下PID的调节方法:

  1. P,I,D置 0,逐渐增大 P,直至响应时间达到预期效果并出现震荡
  2. 增大微分项 D,直至波动减到预期
  3. 若存在过冲,减小 P
  4. 增大积分项 I

如果有想要完整PYB差速底盘主控代码的同学可以直接去我的仓库下载,我已经将项目公开,并会不断更新: PYB差速主控底盘

  • 13
    点赞
  • 104
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值