PID 控制算法实践 — 基于 Arduino UNO 的直流电机 PWM 调速系统软件设计

第一章:绪论

直流电机调速系统的背景及现状

19 世纪 70 年代左右,直流电动机与交流电动机相继诞生,从此人类进入了电机传动的时代,电动机作为动力设备,为人类社会生产力的发展做出了巨大的贡献。但是最初的电机传动系统是不调速系统,或者是用性能极差、及其浪费电能的“刹车片”调速。随着生产制造技术越来越复杂,生产工艺的要求越来越高,对电机电传系统的要求也越来越高。而反馈控制技术的问世以及自动控制原理的发展,无疑对电传控制系统的井喷式发展起到了临门一脚的作用。

由于直流电机模型相较于交流电机更为简单,并且具有良好的线性度、较硬的机械特性,在 20 世纪 80 年代之前,直流电机一直在变速传动领域占据主导地位。

但是旧式的直流电机存在着电刷和换向器,这对直流电机调速系统的进一步发展造成了阻碍。不过,随着技术的进步,大功率直流无刷电动机等新型电机相继问世,使得直流电机具有了更为广阔的应用前景。况且在重载拖动领域,直流电机由于具有远优于交流电机的强大的重载起动能力,在这类特殊场合下,只有直流电机可以胜任。所以伴随着直流电机本身优良的特性,以及直流电机制造技术的不断发展,在未来很长一段时间内,直流调速系统将于交流调速系统并存,直流调速将继续在生产生活领域中发挥其作用。

直流调速系统的发展趋势

由于直流电机可以在带大负载条件下,实现平滑的无级调速,并且拥有较宽的调速范围,再加上直流电机其优良的机械特性,在未来仍然有很广泛的应用前景。

随着现代高科技产业的发展、功率电子学的发展、材料科学的进步,各种新型直流电机不断涌现,以及以控制理论为理论基础的各种高效的直流电机控制方案的发展,直流调速系统在未来仍然有巨大的应用与发展前景,并朝着控制更加智能、更加精准,效率不断提高,性能更为强大的方向持续迈进。

第二章:控制方案总体设计

控制方案的选择

直流电机的转速表达式为:

$$ n = \frac{U_d - IR}{K_e\Phi} $$

  • 式 1:$U_d$ 为电机的电枢电压、$I$ 为电枢电流、$R$ 为电磁绕组、$\Phi$ 为励磁的磁通量、$K_e$ 为电机的结构参数。

将电磁转矩方程 ($T_e = K_mI\Phi$) 代入上式,得机械特性方程式:

$$ n = \frac{U_d - IR}{K_e\Phi} = \frac{U_d}{K_e\Phi} - \frac{T_eR}{K_eK_m\Phi^2} = n0 - \Delta n $$

  • 式 2:$T_e$ 为电机的电磁转矩、 $Km\Phi$ 为电动机在额定磁通下的转矩电流比、$n0$ 为理想空载转速、$\Delta n$ 为转速降。

由式 1 可知,直流电机转速的调节共有以下三方案:

  • 方案一:调节电动机励磁 $\Phi$ 调速
  • 方案二:调节电枢回路电阻 $R$
  • 方案三:调节电枢电压 $U$

对于方案一,由机械特性方程可得,当减小电机磁通量时,电机的理想空载转速升高,动态转速降增大,所以电机的机械特性变软。所以通过这种方案调速,电机的机械特性较软,负载变化时转速波动较大。

对于方案二,通过改变电枢回路电阻进行调速,也会改变电机的机械特性,导致电机转速随负载变化产生较大波动。

对于方案三,由式 2 可知,当电枢电压改变时,电机的转速降不会改变,也就是说电机的机械特性不会随着电压改变而改变,所以电机可以始终保持其机械特性,电机转速不容易随着负载变化而产生较大波动。同时,由于电机的电枢电压与电机的转速成线性关系,对其转速的控制会更容易且更精准。

综上,我们采用调节电枢电压的方案进行电机调速。而调节电枢电压,最经济且有效实现的方法就是 PWM-M 技术。PWM-M 即脉宽调制 (Pulse Width Modulation) 变换器-直流电机系统,是利用全控型功率控制器件的导通与关断,将连续的直流电压转换为特殊的直流脉冲序列,并通过控制脉冲宽度或者周期来起到改变平均电压的效果。为此,我们采用工作在开关状态的电力电子器件,并通过改变 PWM 信号占空比来控制电枢电压。

下图为简易的 PWM-M 系统原理图,以及其系统输出信号的波形图:

简易 PWM-M 系统原理图
图 1:简易的 PWM-M 系统原理图
PWM-M 系统输出信号的波形图
图 2:简易的 PWM-M 系统输出信号的波形图

在图 1 中,$VT$ 是工作在开关状态下的全控型器件,设导通时间为 $t_{on}$,忽略管压降的作用,电源电压 $U_d$ 全部加在了电动机电枢两端;然后关断的时间为 $t_{off}$(二极管在此时续流),此时电动机电枢两端的电压降为零。以 $T = t_{on} + t_{off}$ 为周期,反复进行此过程,则电枢电压的波形图如图 2 所示。 $\overline{U_d}$ 为电枢电压的平均值,$\overline{U_d}$ 的表达式如下:

$$ \overline{U_d} = \frac{t_{on}}{T}U_d = \rho U_d $$

  • 式 3:$T$ 为电力电子功率开关的周期、$\rho$ 为占空比。

改变占空比的值,就可以改变电枢两端的电压 $U_d$ 的平均值 $\overline{U_d}$,进而实现对电机转速的控制,这就是 PWM-M 调速的原理。而改变 $\rho$ 的值,可采用的方法有如下这三种:

  1. 定宽调制法:保持 $t_{on}$ 一定,使 $t_{off}$ 在 $0 \sim \infty$ 范围内变化;
  2. 调宽调制法:保持 $t_{off}$ 一定,使 $t_{on}$ 在 $0 \sim \infty$ 范围内变化;
  3. 定频调制法:保持 $T = t_{on} + t_{off}$ 一定,使 $t_{on}$ 在 $0 \sim T$ 范围内变化。

由于 PWM 频率很容易引起噪音以及抖动,所以我们采用定频调制法。

具有转速闭环负反馈的直流电机调速系统

在上一小节,我们完成了对电机控制系统的执行机构方案的选择,即基于 PWM-M 技术的对直流电机电枢电压的调控。下面基于此方案,进行速度闭环负反馈系统的硬件选型以及控制算法设计。

一个具有转速负反馈的闭环直流电机调速系统,通常由控制器、电机驱动模块、直流电机、转速传感器这几部分组成。经过筛选,控制器我们选择 ATmega328p 单片机,电机驱动模块选择 TB6612FNG,速度传感器采用增量式霍尔编码器。转速闭环负反馈直流电机调速系统的系统框图如下图所示:

转速闭环负反馈直流电机调速系统的系统框图
图 3:转速闭环负反馈直流电机调速系统的系统框图

由图 3 可以看出,该系统对转速的调节是依照给定量与反馈量之间的偏差来进行的。我们设定的调速范围是空载 0~60 (r/s),输入给定转速 $n^{*}$ ,霍尔编码器拥有 A、B 两个霍尔传感器,在电机转动的时候采集边沿脉冲信号,然后输出两路具有一定相位差的方波信号,经单片机采集后,调用函数计算出实际转速 $n$,其偏差大小则为 $\Delta n = n^{*} - n$ 该偏差经过控制器运算后,输出相应的控制量,赋值给 PWM 寄存器。该单片机具有 8 位 PWM 输出功能,可以取值 0~255,输出的值与 255 的比值可以计算出相应的占空比。例如对 PWM 寄存器输入 0,则表示输出占空比位 0 的 PWM 信号,相当于没有电压输出。同理,对 PWM 寄存器输入 255,则表示输出占空比位为 100% 的 PWM 信号,相当于输出最大电压。所以通过控制器输出 PWM 信号,实现自动纠正误差,同时起到稳速的作用。原理如下:

$$ T_e - T_l = Jd\omega _r / d_t $$

  • 式 4:$T_e$ 为电机的电磁转矩、$T_l$ 为负载的转矩、$J$ 为转动惯量、$\omega _r$ 为电机的角速度。

式 4 为电机的机械运动方程式,有式可知,当电机负载增加时($T_l$ 增大),电机转速下降,所以反馈值 $n$ 也会随之下降,调节器的偏差 $\Delta n$ 增大,控制器输出的值升高,因而 PWM 信号的占空比提高,电机获得的平均电压升高($T_e$ 增大),电机的转速也增加了,进而使电机的最终转速基本保持不变。

由于本实验所采用的电机带有齿轮箱减速器,减速比 1:20,所以电机具体表现的特性为低负载惯量,因此我们只需采用比例——积分调节,不需要使用微分调节。所以我们采用的控制算法为增量式离散 PI 控制算法,公式如下:

$$ u(k) = K_p{ [ e(k)-e(k-1) ] + \frac{T}{T_i}\sum_{i=0}^{k} e(i) } $$

  • 式 5:$u(k)$ 为增量输出、$K_p$ 为比例增益、$T_i$ 为积分时间常数、$e(k)$ 为本次偏差、$e(k-1)$ 为上次偏差、$k$ 为离散采样序列、$T$ 为采样周期。

由于式 5 的算法需要对 $e(k)$ 全程进行累加,容易造成误差累计,导致稳定性不高,所以我们带入公式:

$$ K_i = K_p \frac{T}{T_i} $$

  • 式 6:$K_i$ 为积分增益。

然后对离散 PI 公式进行转化,可得:

$$ \Delta u(k) = K_p[e(k)-e(k-1)] + K_i ^{*}e(k) $$

  • 式 7

当采用增量式算法时,计算机输出的控制量 $\Delta u(k)$ 是基于本次执行机构的位置的增量,并不能表示执行机构的实际位置,因此要求执行机构具有对控制量增量的累计功能,才能完成对电机的操作。具体实现方法有硬件法以及软件法,由于所采用单片机外设的硬件的限制,我们采用软件法实现:

$$ \Delta u(k) += K_p[e(k)-e(k-1)] + K_i ^{*}e(k) $$

  • 式 8:$\Delta u(k)$ 表示目标控制位置与当前实际位置之间的偏差量。

由于 Arduino UNO 开发板的 PWM 信号输出具有记忆功能,当控制器没有对 PWM 寄存器赋值时,单片机可以实现持续的恒定占空比的 PWM 信号输出。又由于该单片机的 PWM 寄存器不具有自动累加功能,所以需要通过上式的算法,对 PWM 寄存器实时赋值,才可实现对电机实际转速的控制。

一般的,为了避免输出超过允许值,我们需要对输出进行限幅,同时还要对累加进行限幅,以防止积分深度饱和。由于我们采用的是上式的增量算法,是对输出的不断累加,因此仅对输出进行限幅即可,设定输出的幅值为 7200。

至此,我们完成了该直流电机调速系统的总体方案设计。

第三章:硬件系统设计

产生 PWM 信号的方法有很多,常见的有四种:

  • 利用电力电子器件组成的逻辑电路来产生 PWM 信号。
  • 利用软件算法控制单片机引脚模拟 PWM 信号。
  • 利用专用的 PWM 集成电路输出 PWM 信号。
  • 通过特殊的单片机的 PWM 接口,直接输出 PWM 信号。

前两种方法由于技术落后,目前已经被淘汰了。第三种方法目前也经常用到,它可以有效缓解控制器的工作压力,而且现在市场上的 PWM 集成电路除了能发射 PWM 信号外,还有很多其他的诸如安全保护之类的功能。但是当今更加主流的方法,是第四种方法。

本文利用 Arduino UNO 开发板来产生 PWM 信号,该硬件平台具有 14 路数字 I/O 口,其中 3,5,6,9,10,11 这六个接口是专门用来提供 8 位 PWM 信号输出功能的脉冲宽度调制接口,通过在程序中调用相应的函数,修改入口参数,即可实现对占空比的调节。

单片机数字 I/O 引脚功能设置

该单片机的数字 I/O 引脚在 pinMode 函数设定下,拥有输入 (INPUT) 与输出 (OUTPUT) 两种不同的工作模式。

  1. pinMode 设定的输入模式:

    对于 Arduino 单片机,当我们在主程序中用 pinMode 将引脚设定为 “INPUT” 的时候,该 I/O 的状态为“悬浮输入模式”,具有极高的输入阻抗,相当于断路,使得 I/O 口可以检测到极微弱的信号。以按键检测为例,我们将按键一端接在被设置的输入引脚,另一端接地:当按键按下,I/O 口被拉到 0V,读取低电平;当按键没有按下的时候,通过程序激活上拉电阻,将该 I/O 口通过上拉电阻接到电源正极,使得 I/O 口电平维持在一个较高的水平,单片机读取高电平信号。

  2. pinMode 设定的输出模式:

    当我们在主程序中用 pinMode 将引脚设定为 “OUTPUT” 的时候,该 I/O 的状态为“强推挽”。这样的话,引脚便具备了较强的驱动能力(几十毫安),但直接驱动电机仍然远远不够。所以实际使用中,输出引脚用于控制 TB6612 电机驱动芯片,从而间接控制电机的转动。

增量式霍尔编码器四倍频测速功能设计

增量式霍尔编码器由霍尔码盘、霍尔元件两个基本部分组成。如图所示,在霍尔码盘上均匀分布 13 个有磁性的点,相邻的两个磁性点之间代表一增量周期,每个增量周期对应的角度为 27.69°。

增量式霍尔编码器示意图
图 4:增量式霍尔编码器示意图

将 A、B 两个霍尔元件并列摆放,置于霍尔码盘旁边,通过两个霍尔元件读取信号的上升沿与下降沿,那么每经过一个检测点便可以读取四次位置信息,所以电机每转一圈,霍尔编码器可以采集到 52 个位置信息,将控制精度提高了四倍,即平均 $\pm 6.9\degree$/ 单位时间。霍尔码盘与电机同轴,电机旋转时,霍尔传感器单位时间内检测若干脉冲信号,即可实现对转速的计算。同时,通过判断 A 通道和 B 通道的两路输出信号的相位差,可以设计出鉴别运动正反方向的算法,我们将在第四章做相应的阐述。

硬件系统总体设计

该系统硬件的总体设计框图如下:

系统硬件的总体设计框图
图 5:系统硬件的总体设计框图

该硬件系统以 AVR 单片机为控制器,接受目标速度给定值,以增量型霍尔编码器为速度传感器,获取电机转速、转向信息,传递给单片机后,与给定值进行比较,运算并输出控制量,驱动 TB6612FNG 芯片,控制电机运动。电机转动信息再通过霍尔传感器回传单片机,如此构成一个闭环负反馈控制回路。

第四章:软件系统设计

系统初始化

系统初始化在第二章,我们已经对控制算法进行了选择,即基于 PWM 技术的增量型 PI 控制算法,下面我们将依据此算法,进行相应的程序设计。

与其他 IDE 不同,Arduino IDE 在编译程序时,会先从库文件中寻找 main.cpp,然后扫描 main.cpp 中所有的头文件,如果无法在当前目录找到,则去 Library 目录下查找所有子目录。找到后,将其加入到目录列表中。然后将 UNO 硬件的核心 C/CPP 程序在同级目录下生成一个目标文件,再将主程序、用户程序文件以及各个程序的头文件中所包含的函数库程序生成一个目标文件。最后将之连接起来,生成二进制可执行文件。main.cpp 程序源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include<Arduino.h>

int main() {
    init();
    setup();
    for(;;) {
        loop();
        if(serialEventRun)
            serialEventRun();
    }
    return 0;
}

在程序开始执行时,硬件的初始化由 init() 函数完成,init() 函数初始化了所有定时器,并将所有定时器设置为 cpu 频率的 64 分频。将 0 号定时器设定为系统运行的基准时钟。然后初始化 AD 模块,最后将串口 0 与 bootloader 断开。

标准的 Arduino 程序必须包含两个函数,即 setup 函数、loop 函数。其中 setup 函数用于系统的引脚初始化、串口初始化,以及设置中断的功能,loop 函数是 for(;;) 死循环的循环体,用来实现 OLED 周期性刷新显示,具体程序的流程图如图:

系统的初始化流程图
图 6:系统的初始化流程图

预编译指令

我们要实现的功能是对电机调速,具体实现的操作是按下一个按钮,电机提速,按下另一个按钮,电机减速,同时还要有 OLED 屏幕显示功能。

首先要用到单片机的功能有外部中断、定时中断,OLED 驱动功能,所以需要调用 PinChangeInt.h、MsTimer2.h、SSD1306.h、Arduino.h 这几个头文件所引用的文件。由于需要对转速进行分析,所以还需要用到 PC 端的上位机库文件 DATASCOPE.cpp。

其次需要定义的引脚有:

  • 直流电机驱动引脚:PWM、IN1、IN2,分别对应单片机的 9、10、11 号引脚。
  • 编码器信号接收引脚:ENCODER_AENCODER_B,分别对应单片机的 4、2 号引脚。
  • 按键信号采集引脚:KEY_UPKEY_DOWN,分别对应单片机的 17、16 号引脚。
  • OLED 显示屏引脚:OLED_DCOLED_CLKOLED_MOSIOLED_RESET,分别对应单片机的 5、8、7、6 号引脚。

然后按照引脚对应的编号将相关的引脚名进行宏定义。再对其他一些参数做宏定义,具体代码实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <SSD1306.h>
#include "DATASCOPE.h" /*PC 端上位机的库文件*/
#include <PinChangeInt.h> /*外部中断*/
#include <MsTimer2.h> /*定时中断*/
/**********OLED 显示屏引脚**********/
#define OLED_DC 5
#define OLED_CLK 8
#define OLED_MOSI 7
#define OLED_RESET 6
SSD1306 oled(OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, 0);
/**********TB6612 驱动引脚**********/
#define PWM 9 /*PWM 引脚*/
#define IN1 10
#define IN2 11
/**********编码器引脚**********/
#define ENCODER_A 4
#define ENCODER_B 2
/**********按键引脚**********/
#define KEY_Down 16 /*减速键*/
#define KEY_Up 17 /*加速键*/
/**********其他设定**********/
DATASCOPE data;
/*实例化一个上位机对象,对象名称为 data*/
float Velocity=0,Position=0,Motor=0;
/*定义:速度,位置,pid 控制输出*/
float Target_Velocity=0; /*初始目标速度(设为停机状态)*/
unsigned char Send_Count; /*上位机相关变量*/
float Velocity_KP,Velocity_KI; /*PID 系数*/

子程序模块设计

增量式离散 PI 控制算法子程序

我们所采用的控制算法为增量式离散 PI 算法,具体表达式见式 8,该算法的代码是整个系统的核心。

为了实现式 8 中算法,我们需要对本次偏差以及上次偏差进行存储,每次运算通过将给定值与实际值做差计算出的当前误差值,存入相应变量中,与上次误差值一起经由 PI 算法运算。然后将本次误差值幅值给存储上次误差的变量,并输出控制量,完成一次对电机的控制。具体代码实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int Incremental_PI (int Encoder,int Target) {
    static int Bias, Pwm, Last_bias;
    Bias = Encoder - Target; /*计算偏差*/
    Pwm += Velocity_KP * (Bias - Last_bias) + Velocity_KI * Bias;
    /*增量式 PI 控制器*/
    if (Pwm > 7200) 
        Pwm=7200; /*限幅*/
    Last_bias = Bias; /*保存上一次偏差*/
    return Pwm; /*增量输出*/
}

Incremental_PI() 函数中,参数 Encoder 为实际转速,Target 为目标转速,返回值为控制算法输出的控制量。static float Bias, Pwm, Last_bias; 表示定义三个随进程持续的变量名,Pwm 表示控制算法的增量输出,Bias 表示本次偏差,Last_bias 表示上次偏差。具体 PI 控制算法的参数,将在第五章参数整定部分做详细的论述。

电机驱动模块

Arduino UNO 开发板具有独立的 PWM 信号输出功能,我们使用了该单片机的 9 号引脚作为 PWM 信号的输出引脚,10 号以及 11 号引脚作为 TB6612FNG 的输出电平控制引脚,TB6612FNG 真值表如下:

TB6612FNG 真值表

因此我们可以通过判断控制量的正负,调用 digitalWrite() 函数,改变 IN1 与 IN2 的电平关系,进而改变电机转动方向,具体程序流程图如下:

PWM 信号输出程序流程图
图 7:PWM 信号输出程序流程图

由图可知如果控制量大于 0,IN1 为高电平,IN2 为低电平,电机正转;反之 IN1 为低电平,IN2 为高电平,电机反转。并将对电机的控制量,通过 analogWrite() 函数赋值给 PWM 寄存器。调用该函数后,PWM 引脚将产生一个恒定占空比的稳定的方波(占空比由参数 motor 的绝对值决定),直到下一次调用该函数时,如果控制量发生变化,则占空比也会随之更改。

按键扫描模块与给定值调节模块

由第三章可知,被 pinMode 定义的信号输入引脚,低电平表示按键按下,高电平表示按键松开,按键信息采集程序流程图如下。

按键信息采集程序流程图
图 8:按键信息采集程序流程图

该调速系统目的是尽可能实现对直流电机的无极调速,需要给定值缓慢变化,因而需要反复扫描按键是否按下,只要按下按键,给定值则会依照一定比率缓慢持续变化,由第三章可知,该霍尔编码器采用四倍频技术,且霍尔码盘上均匀分布 13 个磁性点,所以控制精度为 $\pm 6.9\degree$/ 单位时间,在一定控制精度内,基本实现了无极调速,通过利用程序获取按键信息采集程序采集到的按键信息的返回值,即可实现对全局变量 Target_Velocity 的修改,进而改变系统的目标转速,给定值调节程序的流程图如下:

给定值调节程序流程图
图 9:给定值调节程序流程图

显示模块赋值子程序

除了 loop 函数中实时刷新的固定显示内容之外,OLED 屏幕还负责实时显示电机转速等信息。具体实现代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
uint32_t oled_pow(uint8_t m,uint8_t n) {
    uint32_t result = 1;
    while (n--)
        result *= m;
    return result;
}
void OLED_ShowNumber(uint8_t x,uint8_t y,uint32_t num,uint8_t len) {
    u8 t,temp;
    u8 enshow=0;
    for (t=0; t<len; t++) {
        temp = (num / oled_pow(10, len-t-1)) % 10;
        oled.drawchar(x + 6 * t, y, temp + '0');
    }
}

上述两个函数中,为了方便维护,我们利用了 C99 标准中 inttypes.h 宏定义中的一些数据类型,其中 uint32_t 代表 long int 型,uint8_t 代表 char 型,u8 表示 unsigned char 型。

uint32_t oled_pow(uint8_t m, uint8_t n) 函数实现的功能是计算 nm 的乘积 的值(即 mn 次幂),并返回该值。

void OLED_ShowNumber(uint8_t x, uint8_t y, uint32_t num, uint8_t len) 这个函数功能是对 OLED 显示函数赋值。参数中,x 表示起始输入位置的横坐标,y 表示起始输入位置的纵坐标(即行数),num 表示要显示的数字,len 表示所显示的数字的位数,经过表达式 temp = (num / oled_pow(10, len-t-1)) % 10,得出的 temp 等于第 t 位数字的值,再经过 temp + '0',将 temp 对应的数值转换为该数值对应的 ACSCII 码,并赋值给 oled.drawchar 函数(该函数在 SSD1306.h 中声明),完成第 t 位数字显示,然后执行 for 循环,直至显示完该数据的所有位数。

霍尔编码器测速模块

由第三章可知,该霍尔编码器共有 A、B 两相,所以每个传感器分别对应一个 I/O 信号输入,并由霍尔编码器 A 通道测速程序 READ_ENCODER_A() 及霍尔编码器 B 通道测速程序 READ_ENCODER_B() 两个函数分别对 A、B 两相霍尔传感器的输出信号进行处理。

我们将外部中断触发方式设定为边沿脉冲触发方式。这样每个磁性点可以被测量四次,同时使霍尔传感器之间空隙的弧度等于霍尔元件有效面积的弧度、使霍尔元件有效检测面所对应的弧度的三倍略小于霍尔码盘两个磁极间弧度的两倍。这样就可以保证 A、B 两相输出的方波相位差为 90°。如此一来,当其中一个霍尔传感器触发了外部中断后,另一个传感器输出的点评信号在电机正转与反转两种不同的情况下是相反的,如图:

转向判断示意图
图 10:转向判断示意图

若外部中断 0 检测到边沿脉冲信号,进入霍尔编码器 A 通道测速程序,判断 ENCODER_A 引脚电平,如果是高电平,则说明触发外部中断的是上升沿,如果是低电平,则说明触发外部中断的是下降沿。此时立刻判断 ENCODER_B 引脚电平。如果 ENCODER_B 电平与 ENCODER_A 电平相同,则计数器减一,反之计数器加一。与之类似,当外部中断 4 检测到边沿脉冲信号,则进入霍尔编码器 B 通道测速程序,判断 ENCODER_B 引脚电平,如果是高电平,则说明触发外部中断的是上升沿,如果是低电平,则说明触发外部中断的是下降沿。此时立刻判断 ENCODER_A 引脚电平。如果 ENCODER_A 电平与 ENCODER_B 电平相同,则计数器加一,反之计数器减一。具体程序框图如下:

霍尔编码器 A 通道测速程序流程图
图 11:霍尔编码器 A 通道测速程序流程图
霍尔编码器 B 通道测速程序流程图
图 12:霍尔编码器 B 通道测速程序流程图

通过这样的设计,我们不仅实现了对转速的测量,还实现了对转动方向的判断。

电机控制核心模块

我们设定每 20ms 对电机控制一次。在 setup 中,设定每 5ms 进入一次定时中断,每次进入定时中断服务后,Count_Velocity 变量+1,然后判断 Count_Velocity 是否大于 4,如果条件成立说明 t = 采样时间,执行对电机的控制,并将 Count_Velocity 清零。电机控制程序流程图如下:

电机核心控制模块流程图
图 13:电机核心控制模块流程图

电机控制核心模块的功能是,当触发定时中断后,进入该函数,同时开启全局中断以继续接收外部中断读取的霍尔编码器信号。20ms 后,控制器读取电机位置,由于霍尔码盘每转一圈读取 52 个位置,且每秒采集 50 次信号,所以转速 Velocity = (Position / 52) * 50,调用 PI 算法运算后,将控制量发送给 PWM 寄存器进而控制电机,在对 PWM 寄存器赋值时,由于该单片机拥有 8 位 PWM 寄存器,而控制器的输出幅值为 7200,所以要将 PI 算法的输出值 Motor/28,所得的值赋值给 PWM 寄存器,完成一次对电机的控制。

与 MiniBalance 上位机通讯模块

MiniBalance 上位机通过串口将电机转速等信息实时传输给 PC 端,以方便对系统参数进行整定,以及分析系统的性能,该通讯模块流程图如下:

与 MiniBalance 上位机通讯模块流程图
图 14:与 MiniBalance 上位机通讯模块流程图

该通讯模块的功能是将电机转速的给定值与实际值,以 25Hz 的频率,经由串口发送到 MiniBalance 波形显示上位机,从而得到电机转速的波形图。单片机与上位机通信的数据帧长度固定为 4*N+2,N 为信道数,MiniBalance 支持最多 10 通道的信号传输,每个信道的数据固定占 4 个字节。该程序先将每个通道的数据,存入缓冲区对应的区间,再对缓冲区首地址插入帧头,数据位后的下一个地址插入结束符,结束符的值等于从帧头到结束符所占的字节数,用于示波器判断并定位各个信道。数据字节之间的传输延时不可以超过 1ms,否则认为当前帧结束,因而需要严格控制发送时序。

程序编译及烧录

windows 环境下,我们一般使用 Arduino IDE 做开发,通过安装。exe 应用程序,直接搭建开发环境,安装过程中选择安装 CH340 等驱动程序,安装完成后,将以上代码组成完整的程序(详见附录 3),并将开发板的 USB 串口与电脑连接,进行编译与烧录工作。

至此,该系统的软件设计以及相关的操作全部完成。

第五章:系统的参数整定过程与性能分析

系统的参数整定过程

整个控制过程中,最关键的就是算法参数的确定,由式 6 可知,其控制参数包括采样周期 $T$、比例系数 $K_p$、积分时间常数 $T_i$,而积分时间常数由比例系数以及采样时间共同决定。所以在确定采样周期为 20ms 的前提下,我们工作的关键就是确定比例系数 $K_p$。

控制器输出中的比例项没有延迟,只要误差一出现,比例部分会立即起作用,但是单纯的比例控制通常会产生静态误差;而积分项则可以消除稳态误差,但是积分项的作用与当前的误差值和过去历次的误差值的累加值成正比,因此积分作用本身具有严重的滞后性,对系统稳定性不利。因此 PI 控制器既克服了单纯的比例控制的有稳态误差的缺点,又避免了单纯积分调节响应慢,动态性能差的缺点,因此被广泛使用。整定过程遵循在输出不振荡的前提下,增大比例增益、增大积分增益,以追求更佳的性能的原则。下面是参数整定过程:

  1. 确定比例系数 $K_p$:

    首先采用纯比例调节,以确定比例系数。由于设定电机空载最高转速为 60r/s,所以我们将输入信号设定为最高速度的 70%,取输入信号为 42 (r/s),设定 $K_p=0$、$K_i=0$,并不断提高比例系数 $K_p$,直到系统抖动。得到的波形图如下:

    kp=64 ki=0 时系统在阶跃信号下的响应
    图 15:kp=64 ki=0 时系统在阶跃信号下的响应

    上图中,系统的比例系数 $K_p = 64$,白色曲线为输入的设定转速(单位 r/s),为阶跃信号,红色的曲线为电机实际转速曲线(单位 r/s)。由图可知,电机转速在此时出现了频率很高的不规则波动。所以我们在此基础上不断降低比例增益,直至系统达到更稳定的状态。如下图所示:

    kp=33 ki=0 时系统在阶跃信号下的响应
    图 16:kp=33 ki=0 时系统在阶跃信号下的响应

    由上图可知,当 $K_p = 33$ 时,在纯比例调节下,系统基本稳定。所以应将 PI 控制算法的比例系数设定为当前值的 60% 到 70%,这里取 $K_p$ 的值为 20,比例系数 $K_p$ 调试完成。

  2. 确定积分系数 $K_i$:

    比例系数 $K_p$ 确定后,设定一个较小的积分系数 $K_i$ 初值(这里取 $K_i=3$),然后逐渐增大 $K_i$,直至系统出现振荡。如下图所示:

    kp=20 ki=73 时系统在阶跃信号下的响应
    图 17:kp=20 ki=73 时系统在阶跃信号下的响应

    如上图所示,当 $K_i$ 等于 73 时,系统完全不可控,进入不稳定状态,电机转速出现了剧烈的不规则波动,所以我们在此基础上不断降低积分增益,直至系统达到稳定状态:

    kp=20 ki=19 时系统在阶跃信号下的响应
    图 18:kp=20 ki=19 时系统在阶跃信号下的响应

    由上图可知,当 $K_i = 19$ 时,系统达到稳态,所以 $K_i$ 应取值当前值的 150% 到 180%,这里取 $K_i = 30$。

    至此,该增量式离散 PI 控制算法的参数整定完毕,接下来我们分析系统的性能。

系统的性能分析

系统的性能指标

  • 静态指标:

    • 稳态误差: ± 2%。
  • 动态指标:

    1. 跟随指标:阶跃响应下:最大超调量小于 5%,调节时间小于 2.5s,震荡次数小于 4 次。
    2. 抗扰动指标:最大动态速降小于 10r/s,恢复时间小于 3s,震荡次数小于 10 次。

系统的稳态性能分析

本实验利用 MiniBalance 上位机绘制电机转速变化曲线,单片机向上位机发送信号的周期为 25ms,绘制周期为 250ms,即每次实际从 5 帧数据中选择 1 帧输入信号进行绘制,以免因串口传输数据丢帧导致绘制失败。每绘制一帧信号,横坐标加 1,并标出相应的纵坐标的转速值(单位 r/s),用直线将各个点连接起来得到电机转速走势曲线。输入幅值为 60(r/s) 的阶跃信号,检测电机转速的变化曲线。

具体的实验波形图如图所示:

输入阶跃信号下的转速波形图
图 19:输入阶跃信号下的转速波形图

当横坐标的值大于 7 以后,电机实际转速始终等于给定值,即系统的稳态误差为 0,符合系统稳态指标要求。

系统的跟随性能分析

由上图可知:

  • 超调量:由公式 $\%\text{Overshoot} = \frac{h_{(tp)} - h_{\infty}}{h_{\infty}} \times 100\%$,在图中,曲线在第二个绘制点达到峰值,所以超调量 $\%\text{Overshoot} \approx \frac{62 - 60}{60} \times 100\% = 3.3\%$。
  • 节时间:由图可知,该系统调节时间 $t_s = 1.75s$。
  • 震荡次数:3 次。

所以,该系统的动态跟随性能达标。

系统的抗扰动性能分析

下面测试当前参数下的系统抗扰动能力,由于电机采用 5V 的 USB 端口供电,达不到 TB6612FNG 的工作电压 12V,所以最终到达电枢的实际电压仅有 4.17V 到 4.31V,因而在无扰动带减速箱负载的情况下,电机最高转速为 60r/s,当设定值超过这个数值,电机速度将无法维持稳定。所以在测试该系统抗扰动能力时,我们将电机的最高速度设定在 50r/s,以使电机工作在一个相对稳定的状态下,得到的数据更有说服力。具体实验结果如下图所示:

在脉冲扰动下的转速波形图
图 20:在脉冲扰动下的转速波形图

在 10s 到 120s 时间段内,周期性的用拨片波动电机主轴上的紧固螺丝,模拟电机受到脉冲信号扰动,由图 5.6 可知,每当电机受到脉冲扰动,先是电机转速迅速下降到一个低谷,然后很快转速回升,在给定值周围波动,并且伴随着超调产生,可见该系统具有较强的抗扰动能力。电机的最大动态转速降的最大值为 8r/s, 恢复时间为 2.5s,最大震荡次数为 7 次,符合抗扰动指标。

因此可以得出结论,当 $K_p = 20$、$K_i = 30$ 时,该增量式离散 PI 控制算法所控制的直流电机调速系统具有一定的抗扰动能力。

结论

PWM 技术是直流电机调速中最有效的方法。本文详细的阐述了 PWM-M 调速系统的原理及实现方法,同时研究了基于 Arduino UNO 开发板对直流电机进行调速控制的原理及实现方法。除了拥有良好的静态性能、动态性能外,还拥有较强的抗扰动能力,开发板上的 OLED 显示屏可以直观的看到系统运行的数据。除了为 ATmega328p 单片机在直流调速技术中的应用提供了进一步验证外,也为我们以后对该算法的研究提供了帮助。

本文在直流电机调速系统设计中做了以下工作:

  • 鉴于以 ATmega328p 单片机为处理器的 Arduino UNO 开发板的广泛应用,本系统选用其作为直流电机调速系统的核心,构建了速度闭环负反馈控制系统,并设计了一套基于该闭环系统的增量式离散 PI 控制方案。
  • 介绍了 PWM-M 系统的基本原理与实现方法,同时设计了基于增量式霍尔编码器的四倍频“M 法”测速模块。
  • 采用 Arduino IDE 作为开发平台,定义了 MiniBalance 上位机与开发板的通信协议,编写了上位机与单片机的串口通信程序。本文设计的直流电机调速系统只是完成了其基本的一些功能,之后还可以在此基础上进行扩展。例如利用 Arduino UNO 的 WIFI 功能,设计近场网络控制系统等。

在今后的工作中,还需要进一步完善以下几个问题:

  • 对于直流电机的调速系统,单纯采用单闭环速度负反馈能实现的调速效果有限,今后要进一步对控制方案进行优化。
  • MiniBalance 上位机的绘制精度有限,以后工作中要考虑用更精确的波形显示上位机替代。
  • 该单片机仅拥有 8 位的 PWM 寄存器,控制精度有限,导致不能实现真正意义上的无极调速,在某些给定值下会产生一定的稳态误差。

附录 1:Arduino.h 文件

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
#ifndef Arduino_h
#define Arduino_h

#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>
#include <avr/pgmspace.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include "binary.h"

#ifdef __cplusplus
extern "C"
{
#endif

void yield(void);

#define HIGH 0x1
#define LOW  0x0
#define INPUT 0x0
#define OUTPUT 0x1
#define INPUT_PULLUP 0x2
#define PI 3.1415926535897932384626433832795
#define HALF_PI 1.5707963267948966192313216916398
#define TWO_PI 6.283185307179586476925286766559
#define DEG_TO_RAD 0.017453292519943295769236907684886
#define RAD_TO_DEG 57.295779513082320876798154814105
#define EULER 2.718281828459045235360287471352
#define SERIAL  0x0
#define DISPLAY 0x1
#define LSBFIRST 0
#define MSBFIRST 1
#define CHANGE 1
#define FALLING 2
#define RISING 3
#if defined(__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__) || defined(__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
#define DEFAULT 0
#define EXTERNAL 1
#define INTERNAL 2
#else
#if defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega1284__) || defined(__AVR_ATmega1284P__) || defined(__AVR_ATmega644__) || defined(__AVR_ATmega644A__) || defined(__AVR_ATmega644P__) || defined(__AVR_ATmega644PA__)
#define INTERNAL1V1 2
#define INTERNAL2V56 3
#else
#define INTERNAL 3
#endif
#define DEFAULT 1
#define EXTERNAL 0
#endif
// undefine stdlib's abs if encountered
#ifdef abs
#undef abs
#endif
#define min(a, b) ((a)<(b)?(a):(b))
#define max(a, b) ((a)>(b)?(a):(b))
#define abs(x) ((x)>0?(x):-(x))
#define constrain(amt, low, high) ((amt)<(low)?(low):((amt)>(high)?(high):(amt)))
#define round(x)     ((x)>=0?(long)((x)+0.5):(long)((x)-0.5))
#define radians(deg) ((deg)*DEG_TO_RAD)
#define degrees(rad) ((rad)*RAD_TO_DEG)
#define sq(x) ((x)*(x))
#define interrupts() sei()
#define noInterrupts() cli()
#define clockCyclesPerMicrosecond() ( F_CPU / 1000000L )
#define clockCyclesToMicroseconds(a) ( (a) / clockCyclesPerMicrosecond() )
#define microsecondsToClockCycles(a) ( (a) * clockCyclesPerMicrosecond() )
#define lowByte(w) ((uint8_t) ((w) & 0xff))
#define highByte(w) ((uint8_t) ((w) >> 8))
#define bitRead(value, bit) (((value) >> (bit)) & 0x01)
#define bitSet(value, bit) ((value) |= (1UL << (bit)))
#define bitClear(value, bit) ((value) &= ~(1UL << (bit)))
#define bitWrite(value, bit, bitvalue) (bitvalue ? bitSet(value, bit) : bitClear(value, bit))
// avr-libc defines _NOP() since 1.6.2
#ifndef _NOP
#define _NOP() do { __asm__ volatile ("nop"); } while (0)
#endif
typedef unsigned int word;
#define bit(b) (1UL << (b))
typedef bool boolean;
typedef uint8_t byte;

void init(void);

void initVariant(void);

int atexit(void (*func)()) __attribute__((weak));

void pinMode(uint8_t, uint8_t);

void digitalWrite(uint8_t, uint8_t);

int digitalRead(uint8_t);

int analogRead(uint8_t);

void analogReference(uint8_t mode);

void analogWrite(uint8_t, int);

unsigned long millis(void);

unsigned long micros(void);

void delay(unsigned long);

void delayMicroseconds(unsigned int us);

unsigned long pulseIn(uint8_t pin, uint8_t state, unsigned long timeout);

unsigned long pulseInLong(uint8_t pin, uint8_t state, unsigned long timeout);

void shiftOut(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder, uint8_t val);

uint8_t shiftIn(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder);

void attachInterrupt(uint8_t, void (*)(void), int mode);

void detachInterrupt(uint8_t);

void setup(void);

void loop(void);
// Get the bit location within the hardware port of the given virtual pin.
// This comes from the pins_*.c file for the active board configuration.
#define analogInPinToBit(P) (P)
// On the ATmega1280, the addresses of some of the port registers are
// greater than 255, so we can't store them in uint8_t's.
extern const uint16_t PROGMEM
port_to_mode_PGM[];
extern const uint16_t PROGMEM
port_to_input_PGM[];
extern const uint16_t PROGMEM
port_to_output_PGM[];
extern const uint8_t PROGMEM
digital_pin_to_port_PGM[];
// extern const uint8_t PROGMEM digital_pin_to_bit_PGM[];
extern const uint8_t PROGMEM
digital_pin_to_bit_mask_PGM[];
extern const uint8_t PROGMEM
digital_pin_to_timer_PGM[];
// Get the bit location within the hardware port of the given virtual pin.
// This comes from the pins_*.c file for the active board configuration.
// These perform slightly better as macros compared to inline functions
//
#define digitalPinToPort(P) ( pgm_read_byte( digital_pin_to_port_PGM + (P) ) )
#define digitalPinToBitMask(P) ( pgm_read_byte( digital_pin_to_bit_mask_PGM + (P) ) )
#define digitalPinToTimer(P) ( pgm_read_byte( digital_pin_to_timer_PGM + (P) ) )
#define analogInPinToBit(P) (P)
#define portOutputRegister(P) ( (volatile uint8_t *)( pgm_read_word( port_to_output_PGM + (P))) )
#define portInputRegister(P) ( (volatile uint8_t *)( pgm_read_word( port_to_input_PGM + (P))) )
#define portModeRegister(P) ( (volatile uint8_t *)( pgm_read_word( port_to_mode_PGM + (P))) )
#define NOT_A_PIN 0
#define NOT_A_PORT 0
#define NOT_AN_INTERRUPT -1
#ifdef ARDUINO_MAIN
#define PA 1
#define PB 2
#define PC 3
#define PD 4
#define PE 5
#define PF 6
#define PG 7
#define PH 8
#define PJ 10
#define PK 11
#define PL 12
#endif
#define NOT_ON_TIMER 0
#define TIMER0A 1
#define TIMER0B 2
#define TIMER1A 3
#define TIMER1B 4
#define TIMER1C 5
#define TIMER2  6
#define TIMER2A 7
#define TIMER2B 8
#define TIMER3A 9
#define TIMER3B 10
#define TIMER3C 11
#define TIMER4A 12
#define TIMER4B 13
#define TIMER4C 14
#define TIMER4D 15
#define TIMER5A 16
#define TIMER5B 17
#define TIMER5C 18
#ifdef __cplusplus
} // extern "C"
#endif
#ifdef __cplusplus
#include "WCharacter.h"
#include "WString.h"
#include "HardwareSerial.h"
#include "USBAPI.h"
#if defined(HAVE_HWSERIAL0) && defined(HAVE_CDCSERIAL)
#error "Targets with both UART0 and CDC serial not supported"
#endif
uint16_t makeWord(uint16_t w);
uint16_t makeWord(byte h, byte l);
#define word(...) makeWord(__VA_ARGS__)
unsigned long pulseIn(uint8_t pin, uint8_t state, unsigned long timeout = 1000000L);
unsigned long pulseInLong(uint8_t pin, uint8_t state, unsigned long timeout = 1000000L);
void tone(uint8_t _pin, unsigned int frequency, unsigned long duration = 0);
void noTone(uint8_t _pin);
// WMath prototypes
long random(long);
long random(long, long);
void randomSeed(unsigned long);
long map(long, long, long, long, long);
#endif

#include "pins_arduino.h"

#endif

附录 2:MiniBalance 上位机源码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
/****************************************************************
> MiniBalance 上位机。h 文件
***************************************************************/
#ifndef _DATASCOPE_H__
#define _DATASCOPE_H__
//导入 Arduino 核心头文件
#include"Arduino.h"

extern unsigned char DataScope_OutPut_Buffer[42];
class DATASCOPE {
    public:
        DATASCOPE();
        void Float2Byte(float *target, unsigned char *buf, unsigned char beg);
        void DataScope_Get_Channel_Data(float Data, unsigned char Channel);
        unsigned char DataScope_Data_Generate(unsigned char Channel_Number);
};
#endif
/****************************************************************
> MiniBalance 上位机。Cpp 文件
****************************************************************/
#include"DATASCOPE.h"

unsigned char DataScope_OutPut_Buffer[42] = {0};  //串口发送缓冲区	

DATASCOPE::DATASCOPE(){}

void DATASCOPE::Float2Byte(float *target, unsigned char *buf, unsigned char beg) {
    unsigned char *point;
    point = (unsigned char *) target;      //得到 float 的地址
    buf[beg] = point[0];
    buf[beg + 1] = point[1];
    buf[beg + 2] = point[2];
    buf[beg + 3] = point[3];
}

void DATASCOPE::DataScope_Get_Channel_Data(float Data, unsigned char Channel) {
    if ((Channel > 10) || (Channel == 0)) return;  //通道个数大于 10 或等于 0,												//直接跳出,不执行函数
    else {
        switch (Channel) {
            case 1:
                Float2Byte(&Data, DataScope_OutPut_Buffer, 1);
                break;
            case 2:
                Float2Byte(&Data, DataScope_OutPut_Buffer, 5);
                break;
            case 3:
                Float2Byte(&Data, DataScope_OutPut_Buffer, 9);
                break;
            case 4:
                Float2Byte(&Data, DataScope_OutPut_Buffer, 13);
                break;
            case 5:
                Float2Byte(&Data, DataScope_OutPut_Buffer, 17);
                break;
            case 6:
                Float2Byte(&Data, DataScope_OutPut_Buffer, 21);
                break;
            case 7:
                Float2Byte(&Data, DataScope_OutPut_Buffer, 25);
                break;
            case 8:
                Float2Byte(&Data, DataScope_OutPut_Buffer, 29);
                break;
            case 9:
                Float2Byte(&Data, DataScope_OutPut_Buffer, 33);
                break;
            case 10:
                Float2Byte(&Data, DataScope_OutPut_Buffer, 37);
                break;
        }
    }
}

unsigned char DATASCOPE::DataScope_Data_Generate(unsigned char Channel_Number) {
    if ((Channel_Number > 10) || (Channel_Number == 0)) { return 0; }  //通道个数大于 10 或等于 0,直接跳出,不执行函数
    else {
        DataScope_OutPut_Buffer[0] = '$';  //帧头
        switch (Channel_Number) {
            case 1:
                DataScope_OutPut_Buffer[5] = 5;
                return 6;
            case 2:
                DataScope_OutPut_Buffer[9] = 9;
                return 10;
            case 3:
                DataScope_OutPut_Buffer[13] = 13;
                return 14;
            case 4:
                DataScope_OutPut_Buffer[17] = 17;
                return 18;
            case 5:
                DataScope_OutPut_Buffer[21] = 21;
                return 22;
            case 6:
                DataScope_OutPut_Buffer[25] = 25;
                return 26;
            case 7:
                DataScope_OutPut_Buffer[29] = 29;
                return 30;
            case 8:
                DataScope_OutPut_Buffer[33] = 33;
                return 34;
            case 9:
                DataScope_OutPut_Buffer[37] = 37;
                return 38;
            case 10:
                DataScope_OutPut_Buffer[41] = 41;
                return 42;
        }
    }
    return 0;
}

附录 3:增量式 PI 控制算法 C 语言实现

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
#include <SSD1306.h>
#include "DATASCOPE.h"       /*PC 端上位机的库文件*/
#include <PinChangeInt.h>    /*外部中断*/
#include <MsTimer2.h>        /*定时中断*/
/**********OLED 显示屏引脚**********/
#define OLED_DC 5
#define OLED_CLK 8
#define OLED_MOSI 7
#define OLED_RESET 6

SSD1306 oled(OLED_MOSI, OLED_CLK, OLED_DC, OLED_RESET, 0);

/**********TB6612 驱动引脚**********/
#define PWM 9                /*PWM 引脚*/
#define IN1 10
#define IN2 11
/**********编码器引脚**********/
#define ENCODER_A 4
#define ENCODER_B 2
/**********按键引脚**********/
#define KEY_Down 16        /*减速键*/
#define KEY_Up 17           /*加速键*/
/**********其他设定**********/
DATASCOPE data;              /*实例化一个上位机对象,对象名称为 data*/
float Velocity = 0, Position = 0, Motor = 0;    /*定义:速度,位置,pid 控制输出*/
float Target_Velocity = 0;                /*初始目标速度(设为停机状态)*/
unsigned char Send_Count;                /*上位机相关变量*/
float Velocity_KP = 20, Velocity_KI = 30;    /*PID 系数*/

/*************************************************************
函数功能:向上位机发送数据
入口参数:无
返回  值:无
****************************************************************/
void DataScope(void) {
    int i;
    data.DataScope_Get_Channel_Data(Velocity, 1);  /*显示第一个数据*/
    data.DataScope_Get_Channel_Data(Target_Velocity, 2);
    /*显示第二个数据*/
    Send_Count = data.DataScope_Data_Generate(2);
    for (i = 0; i < Send_Count; i++) {
        Serial.write(DataScope_OutPut_Buffer[i]);
    }
    delay(50);  /*控制发送时序*/
}

/****************************************************************
函数功能:给定值调节
入口参数:无
返回  值:无
****************************************************************/
void Adjust(void) {
    float Velocity_Amplitude = 0.019;  /*速度调节增量的赋值 (6.9 度)*/
    int temp;   /*定义临时变量*/
    temp = My_click();   /*采集按键信息*/
    /*改变运行速度*/
    if (temp == 2)Target_Velocity += Velocity_Amplitude;
    if (temp == 1)Target_Velocity -= Velocity_Amplitude;
    if (Target_Velocity > 60)Target_Velocity = 60; /*速度最大值限幅*/
    if (Target_Velocity < 0)Target_Velocity = 0;   /*速度最小值限幅*/
}

/****************************************************************
函数功能:按键扫描
入口参数:无
返回  值:unsigned char 型
****************************************************************/
unsigned char My_click(void) {
    static unsigned char flag_key = 1; /*按键标志,默认到断开*/
    if (digitalRead(KEY_Down) == 0 ||
        digitalRead(KEY_Up) == 0) /*如果发生单击事件*/
    {
        flag_key = 0;                            /*按键按下*/
        if (digitalRead(KEY_Down) == 0)           /*减速键*/
        {
            return 1;
        }
        if (digitalRead(KEY_Up) == 0)        /*提速键*/
        {
            return 2;
        }
    } else if (digitalRead(KEY_Down) == 1 && digitalRead(KEY_Up) == 1)
        flag_key = 1; /*所有案件都松开了*/
    return 0;/*无按键按下*/
}

/****************************************************************
函数功能:计算 m 的 n 次幂
入口参数:uint8_t 型
返回  值:uint32_t 型
****************************************************************/
uint32_t oled_pow(uint8_t m, uint8_t n) {
    uint32_t result = 1;
    while (n--)
        result *= m;
    return result;
}

/****************************************************************
函数功能:OLED 显示
入口参数:uint8_t 型、uint32_t 型
返回  值:无
****************************************************************/
void OLED_ShowNumber(uint8_t x, uint8_t y, uint32_t num, uint8_t len) {
    u8 t, temp;
    u8 enshow = 0;
    for (t = 0; t < len; t++) {
        temp = (num / oled_pow(10, len - t - 1)) % 10;
        oled.drawchar(x + 6 * t, y, temp + '0');
    }
}

/****************************************************************
函数功能:将控制量发送给 PWM 寄存器
入口参数:int 型
返回  值:无
****************************************************************/
void Set_Pwm(int motor) {
    if (motor < 0) {
        digitalWrite(IN1, HIGH);
        digitalWrite(IN2, LOW);   /*TB6612 的电平控制*/
    } else {
        digitalWrite(IN1, LOW);
        digitalWrite(IN2, HIGH);  /*TB6612 的电平控制*/
    }
    analogWrite(PWM, abs(motor)); /*赋值给 PWM 寄存器*/
}

/****************************************************************
函数功能:将控制量发送给 PWM 寄存器
入口参数:int 型
返回  值:int 型
****************************************************************/
int Incremental_PI(int Encoder, int Target) {
    static int Bias, Pwm, Last_bias;
    Bias = Encoder - Target;                          /*计算偏差*/
    Pwm += Velocity_KP * (Bias - Last_bias) + Velocity_KI * Bias;
    /*增量式 PI 控制器*/
    if (Pwm > 7200) Pwm = 7200;                    /*限幅*/
    Last_bias = Bias;                           /*保存上一次偏差*/
    return Pwm;                                  /*增量输出*/
}

/****************************************************************
函数功能: 电机控制函数 核心代码
入口参数: 无
返回  值: 无
****************************************************************/
void control() {
    static unsigned char Count_Velocity;  /*位置检测用的变量*/
    sei();                                  /*全局中断开启*/
    if (++Count_Velocity > 4) {
        Velocity = Position / 52 * 50;    /*单位时间内读取位置信息*/
        Position = 0;           /*并清零*/
        Count_Velocity = 1;     /*计数器置 1*/
        Motor = Incremental_PI(Velocity, Target_Velocity);  /*速度 PI 控制器*/
    }
    Set_Pwm(Motor / 28);    /*输出电机控制量*/
    Adjust();              /*PID 参数调节*/
}

/****************************************************************
函数功能:初始化函数
入口参数:无
返回  值:无
***************************************************************/
void setup() {
    oled.ssd1306_init(SSD1306_SWITCHCAPVCC);
    oled.clear();                /*clears the screen and buffer*/
    pinMode(IN1, OUTPUT);
    /*设置 TB6612 方向控制引脚为输出模式*/
    pinMode(IN2, OUTPUT);
    /*设置 TB6612 方向控制引脚为输出模式*/
    pinMode(PWM, OUTPUT);
    /*设置 TB6612 速度控制引脚为输出模式*/
    digitalWrite(IN1, 0);        /*TB6612 方向控制引脚下拉*/
    digitalWrite(IN2, 0);        /*TB6612 方向控制引脚下拉*/
    digitalWrite(PWM, 0);        /*TB6612 速度控制引脚下拉*/
    pinMode(ENCODER_A, INPUT);   /*设置编码器引脚为输入模式*/
    pinMode(ENCODER_B, INPUT);   /*设置编码器引脚为输入模式*/
    Serial.begin(128000);            /*开启串口*/
    delay(200);                      /*延时等待初始化完成*/
    MsTimer2::set(5, control);      /*使用 Timer2 设置 5ms 定时中断*/
    MsTimer2::start();               /*中断使能*/
    ttachInterrupt(0, READ_ENCODER_A, CHANGE);
    /*开启外部中断*/
    ttachPinChangeInterrupt(4, READ_ENCODER_B, CHANGE);
}

/***************************************************************
函数功能:主循环程序体
入口参数:无
返回  值:无
***************************************************************/
void loop() {
    oled.drawstring(00, 0, "VELOCITY MODE");  /*速度模式*/
    if (Velocity != 0) 
        oled.drawstring(00, 01, "MOTOR O N"); /*速度不为零则电机开启*/
    else 
        oled.drawstring(00, 01, "MOTOR OFF"); /*速度为零则电机关闭*/

    /***********第 3 行显示速度 PID 控制 P 参数**************/
    oled.drawstring(0, 02, "V-KP:");
    OLED_ShowNumber(30, 02, Velocity_KP, 3);
    /*************第 4 行显示速度 PID 控制 I 参数************/
    oled.drawstring(0, 03, "V-KI:");
    OLED_ShowNumber(30, 03, Velocity_KI, 3);
    /****************第 6 行显示速度测量参数***************/
    oled.drawstring(00, 5, "VELOCITY:");
    OLED_ShowNumber(60, 5, Velocity, 5);
    /**************第 7 行显示速度目标值*******************/
    oled.drawstring(00, 6, "TARGET:");
    OLED_ShowNumber(60, 6, Target_Velocity, 5);
    /************************刷新*************************/
    oled.display();
    DataScope();
}

/***************************************************************
函数功能:外部中断读取编码器数据
入口参数:无
返回  值:无
***************************************************************/
void READ_ENCODER_A() {
    if (digitalRead(ENCODER_A) == HIGH) /*如果是上升沿触发的中断*/
    {
        if (digitalRead(ENCODER_B) == LOW)
            Position++;
        else
            Position--;
    } else /*如果是下降沿触发的中断*/
    {
        if (digitalRead(ENCODER_B) == LOW)
            Position--;
        else
            Position++;
    }
}

/****************************************************************
函数功能:外部中断读取编码器数据
入口参数:无
返回  值:无
****************************************************************/
void READ_ENCODER_B() {
    if (digitalRead(ENCODER_B) == LOW) /*如果是下降沿触发的中断*/
    {
        if (digitalRead(ENCODER_A) == LOW)
            Position++;
        else
            Position--;
    } else /*如果是上升沿触发的中断*/
    {
        if (digitalRead(ENCODER_A) == LOW)
            Position--;
        else
            Position++;
    }
}
0%