摘要:本文介绍如何编写平衡车控制类以及确定驱动死区
从本篇文章开始,将介绍平衡车软件的开发及调试方法。平衡车开发中,最关键的就是要让小车能够稳定的站立起来,这是最基础也是最核心的一步,除了软件开发工作,这一步的关键就是PID参数的调节。
下面就来逐步的实现,让平衡车先站立起来的目标。为了实现这个目标的软件结构并不复杂,大致流程如下图所示:
系统初始化部分包括了对蓝牙串口(主要用于PID调试和反馈小车状态)、LEDC控制器初始化和姿态控制器MPU6050初始化这三个部分。
平衡车处理器进入正常运行阶段之后,就是重复在做一件事情,根据姿态控制器的姿态信息来改变对小车电机的控制,通过这种不停的动态调整,来达到一种动态平衡的状态,使平衡车保持在平衡位置附近。
接下来就来实现一个小车类,这个类定义了与驱动小车电机相关的各种配置信息和方法。先来看一下类的声明,以及在头文件中定义的一些配置信息。如下所示:
/********************************* car.h by 一起玩儿科技 2024/02/23 ********************************/ #pragma once #ifndef _YQWKJ_BALANCE_CAR_ #define _YQWKJ_BALANCE_CAR_ #include <Arduino.h> // 默认控制引脚 #define R_IN1 25 #define R_IN2 27 #define L_IN1 32 #define L_IN2 17 // PWM通道占用 #define L_CH1 0 #define L_CH2 1 #define R_CH1 2 #define R_CH2 3 //左右轮死区pwm值 #define LEFT_MOTOR_PWM_OFFSET 41 #define RIGHT_MOTOR_PWM_OFFSET 41 class Car { public: // 构造函数 Car(); // 初始化方法 void begin(); void motor(int leftPWM, int rightPWM); private: // 私有属性 // 两轮的驱动引脚 uint8_t l1 = L_IN1, l2 = L_IN2, r1 = R_IN1, r2 = R_IN2; // 处理偏移量 int calcPwmOffset(int pwm, int offset); // 输出PWM void outputPWM(int pwm, int ch0, int ch1); }; #endif |
关于类的一些基本知识和编写方法,在前面已经介绍了,如有疑问,请参考之前的文章。
在这个头文件中,定义了驱动电机所适用的引脚,以及所占用的PWM通道的情况。所定义的两个方法,其中begin()方法用于完成与小车相关的各种初始化工作,motor()方法用于实现对小车的驱动。关于PWM的相关知识可以参考之前的文章。
motor()方法的两个参数就是PID算法所计算出来的控制输入值,该值为整型,可正可负。符号表示车轮需要转动的方向,而绝对值则表示需要车轮转动的快慢,绝对值越大,要求车轮转动的越快。
接下来就看一下这个类的实现方法,如下所示:
/********************************* car.c by 一起玩儿科技 2024/02/23 ********************************/ #include "car.h" // 无参构造函数 Car::Car() {} // 初始化方法 void Car::begin() { uint8_t pins[] = { L_IN1, L_IN2, R_IN1, R_IN2 }; uint8_t chs[] = { L_CH1, L_CH2, R_CH1, R_CH2 }; for (int i = 0; i < 4; i++) { ledcSetup(chs[i], 1000, 8); ledcAttachPin(pins[i], chs[i]); } } // 补偿函数 int Car::calcPwmOffset(int pwm, int offset) { if(pwm>0){ pwm += offset; } else if(pwm<0) { pwm -= offset; } return constrain(pwm, -255, 255); } // 输出PWM void Car::outputPWM(int pwm, int ch0, int ch1) { if (pwm == 0) { ledcWrite(ch0, 0); ledcWrite(ch1, 0); } else if (pwm < 0) { ledcWrite(ch0, 0); ledcWrite(ch1, 0 - pwm); } else { ledcWrite(ch0, pwm); ledcWrite(ch1, 0); } } // 驱动轮子运动 void Car::motor(int leftPWM, int rightPWM) { //-----补偿左右轮差值 leftPWM = calcPwmOffset( leftPWM, LEFT_MOTOR_PWM_OFFSET); rightPWM = calcPwmOffset( rightPWM, RIGHT_MOTOR_PWM_OFFSET); // 输出控制PWM outputPWM( leftPWM, L_CH1, L_CH2 ); outputPWM( rightPWM, R_CH1, R_CH2 ); } |
初始化方法就不再解释了,之前的文章已经见过多次了。下面重点来看一下motor()方法,这个方法的作用就是用PID算法得到的控制量,来驱动平衡车的车轮。
在现在这个初始阶段,我们对小车车轮的驱动控制是开环控制,注意,这里指的是通过PWM信号控制马达转动这件事情,这是一个开环控制,因为我们给定一个PWM信号之后,并不会去关心车轮实际的转速是多少,反过来,车轮的转速也不会影响到我们输出的PWM信号的占空比。我们只是简单的认为,车轮的转速与PWM信号的占空比是成正比例的,也就是PWM信号占空比越大,小车车轮的转速越大。在后边,我会介绍如何通过霍尔编码器来测量小车的转速,从而实现小车车轮驱动的闭环控制。在当前阶段,实现小车的站立和基本的行走功能,使用开环控制小轮也完全能够实现。
在前面的讲解中已经介绍过了,在Arduino IDE中,可以使用ledcWrite()函数来输出PWM信号,该函数所接受的参数取值范围是0~255,对应着输出PWM信号的占空比为0~100%。理想情况是ledcWrite()的参数在0~255之间从小到大的变动时,车轮转速也会从慢到快的线性转动。但实际情况不是这样的,制作精良的电机,线性度会稍微好一些,而质量差的电机,线性度就没有保证了。但是,无论怎样一个电机,都会存在一个死区,也就是说电机不是驱动信号从0变到1就立刻开始运动,而是当占空比小于某个值的时候,电机根本不会转动。因此我们要剔除这个死区的影响,要找到电机不动的最大占空比,也就是可以保持电机不动的最大ledcWrite()参数值。
我的两个电机测试出来的死区最大驱动值是41,定义在了小车类的头文件中,如下所示:
//左右轮死区pwm值 #define LEFT_MOTOR_PWM_OFFSET 41 #define RIGHT_MOTOR_PWM_OFFSET 41 |
下面来介绍一些如何测量这个死区的最大值。首先要保证可以用ESP32处理器正确的驱动小车的车轮,这就要求你完成前面所有的组装测试工作。测量的方法就是从0开始,依次增加ledcWrite()的参数值,每次+1,找到轮子刚好能转动的ledcWrite()参数值(后边称此值为最小转动值),再减去1,就是驱动参数死区的最大值。在寻找最小转动值的时候,可以使用用手辅助启动,也就是朝着旋转的方向,转动一下轮子,看看轮子是否能够转动起来。这个测试可以多进行几次,最后取一个平均值。除了从小到大的测试,还可以从大到小进行测试,找到最后转动的值,就是最小转动值。恰好停下来的值就是死区最大值。
可以用之前的程序修改一下来进行这个测试工作,在这里我也给出一个简单的测试程序,需要注意的是,在使用这个测试程序的时候,需要先把car.h文件中定义的死区值修改为0,如下所示:
//左右轮死区pwm值 #define LEFT_MOTOR_PWM_OFFSET 0 #define RIGHT_MOTOR_PWM_OFFSET 0 |
测试程序的代码如下所示:
#include "BluetoothSerial.h" #include "car.h" int pwm = 0; Car car; BluetoothSerial SerialBT; void serialDebug() { if (SerialBT.available() > 0 || Serial.available() > 0) { //delay(5); char CMD = SerialBT.available() > 0 ? SerialBT.read() : Serial.read(); Serial.println(CMD); switch (CMD) { case 'r': // 机械平衡点调整 pwm++; break; case 'f': pwm--; break; } pwm = constrain(pwm, 0, 255); SerialBT.print("当前pwm:"); SerialBT.println(pwm); } } void setup() { // put your setup code here, to run once: SerialBT.begin("Balance CAR"); Serial.begin(115200); // 车初始化 car.begin(); car.motor(0, 0); Serial.println("Init OK......"); } void loop() { // put your main code here, to run repeatedly: serialDebug(); car.motor(pwm, pwm); //马达输出 delay(100); } |
在这个程序中,通过串口或者蓝牙,发送小写的“r”字符,就可以不断的增加pwm的值,“f”字符会减小pwm的值。希望多一些耐心,定会找到这两个车轮的死区最大值。
好了,朝着完成的目标又前进了一步。下期见!