分享我开发的Arduino模糊控制库FuzzyControl

这是我开发的第一个库函数。使用这个库能够非常方便地构造一个双输入单输出模糊控制器,来实现模糊控制。希望能够给有需要的人带来帮助。

库下载地址https://github.com/re-high/Arduino-FuzzyControl-Library


为什么要开发模糊控制库

  • 模糊控制的优点:无模型控制
    在现代控制理论中,控制器的分析和综合大多依赖于精确的数学模型。由于被控对象过程的非线性、参数间的强耦合、过程机理复杂以及现场测量仪表条件不足,以致很多被控对象不能建立起数学模型,对于那些不能直接获得数学模型描述的系统,使用模糊控制往往能取得很好的效果。
    人们在手动控制中,被控过程的操作人员在长期观察、实践中积累许多经验,这些经验常用定性的、不精确的语言规则等形式加以描述,如“若炉温偏高则燃料适当减少”。系统在运行过程中,人们将观察到的过程输出与设定值比较,得到过程输出偏离设定值变化快慢的模糊语义描述,经逻辑推理得到控制量的模糊量:“适当减少燃料”,经反模糊化后,转化为一精确的控制量,实现整个模糊过程。 以模糊集和模糊推理为基础,对上述手工操作过程进行建模,即可得到模糊控制器。
  • 相对于另外一种无模型控制方法PID法,模糊控制算法更为复杂,要经过模糊化、模糊推理、解模糊,因此想在硬件中实现模糊控制算法代码量大很多。
    在以往的项目中,我一般用面向过程的编程思想来写构造模糊控制器的代码,代码量随着隶属度函数和规则数的增加而增加,当想修改规则时,要改的地方非常多,造成代码可读性差且不易移植的困难。
    所以我有了一个想法,用面向对象的编程思想开发一个Arduino的模糊控制库,把模糊化、模糊推理、解模糊这些步骤放在库文件里实现,用户只需要在主程序中调用相关函数添加变量的隶属度函数和添加控制规则,就能直接计算出输出量。这大大减少了用户用Arduino实现模糊控制时构造模糊控制器的时间,不需要自己编写模糊控制的过程。

受到仿真软件matlab中模糊工具箱的启发,我编写了FuzzyControl库,让在硬件中实现模糊控制和在matlab中使用模糊工具箱函数搭建模糊控制器一样简单。


Arduino的模糊控制库FuzzyControl

FuzzyControl库简介

FuzzyControl是我为了在Arduino中更方便地实现模糊控制而开发的Arduino库。不再需要用户自己编写模糊化、模糊推理、解模糊的过程,你只要添加变量的隶属度函数和控制规则就能直接计算出输出量。当你想实现电机转速控制、双轮自平衡车的角度控制时,都可以用FuzzyControl库轻松方便地构建一个双输入单输出模糊控制器。
下面,我将首先介绍FuzzyControl库函数,然后以Arduino Uno配合带编码器的直流电机,演示如何使用FuzzyControl库构造一个模糊控制器,实现电机的位置追踪控制

FuzzyControl库函数

setRange
功能:设置输入输出变量的论域
格式:setRange(input1rangeMin, input1rangeMax,input2rangeMin, input2rangeMax, outputrangeMin, outputrangeMax)
说明:setRange函数有6个输入变量:

  • input1rangeMin:输入量1的论域最小值
  • input1rangeMax:输入量1的论域最大值
  • input2rangeMin:输入量2的论域最小值
  • input2rangeMax:输入量2的论域最大值
  • outputrangeMin:输出量的论域最小值
  • outputrangeMax:输出量的论域最大值

addmf
功能:添加隶属度函数
格式:addmf( varType, mfIndex, trimf_a, trimf_b, trimf_c); addmf( varType, mfIndex, trapmf_a, trapmf_b, trapmf_c, trapmf_d);
说明:addmf函数有5或6个输入变量(根据隶属度函数是三角型还是梯形决定):

  • varType:要添加隶属度函数的变量类型(1表示input1,2表示input2,3表示input3)
  • mfIndex:模糊子集索引(模糊子集名称)
  • trimf_a、trimf_b、trimf_c是三角型隶属度函数的参数
  • trapmf_a、trapmf_b、trapmf_c、trapmf_d是梯型隶属度函数的参数

setrulenum
功能:输入控制规则总数目
格式:setrulenum(ruleNum);
说明:setrulenum函数有1个输入变量:

  • ruleNum:控制规则总数目

addrule
功能:增加控制规则
格式:addrule(ruleIndex, var1Index, var2Index, var3Index)
说明:addrule函数有4个输入变量:

  • ruleIndex:要添加的控制规则是第几条规则
  • var1Index:这条控制规则中的input1所属的模糊子集索引
  • var2Index:这条控制规则中的input2所属的模糊子集索引
  • var3Index:这条控制规则中的output所属的模糊子集索引

caculate
功能:完成模糊推理计算
格式:caculate( value1, value2);
说明:caculate函数有2个输入变量:

  • value1:输入量1的数值
  • value2:输入量2的数值

实例:使用FuzzyControl库实现电机的位置追踪控制

介绍完FuzzyControl库函数,下面就用来看一下怎么用FuzzyControl库构造一个两输入单输出模糊控制器,来控制直流电机追踪到理想的位置。本程序设置电机的理想位置信号设置为y=50sin(0.1pi * t ),即20s产生一个完整的正弦波,控制目标就是让电机的位置追踪这个理想正弦信号。

电路连接图

用到的硬件包括:Arduino Uno(控制器)、带编码器的直流电机、电机驱动模块TB6612FNG、电源。
注意:由于未找到编码器的素材,所以图中少画了编码器与Arduino的连接图。为了得到位置信号,应将编码器A相连接Arduino数字引脚2(中断0)、B相连接Arduino数字引脚3(中断1)。

实现过程

总体方案:选用两输入单输出模糊控制器,控制器输入为追踪误差(e)和误差变化率(ec),输出为PWM值。

  1. 定义覆盖输入、输出变量的模糊子集
    • 确定输入输出变量的论域:输入为追踪误差(e)和误差变化率(ec),初步设置输入范围为[-20,20];输出为PWM值,输出变化范围为[-255,255]。
    • 确定模糊集个数及各模糊集的隶属度函数:将追踪误差(e)和误差变化率(ec)都分为3个模糊集:N(负)Z(零)P(正);将输出的PWM值分为5个模糊集:NB(负大)NS(负小)ZZ(零)PS(正小)PB(正大)。
    • 输入隶属度函数选取为三角形+梯形隶属函数,输出隶属度函数选取为三角形隶属函数。
    • 模糊集参数的选取:对于输入追踪误差(e)和误差变化率(ec),初步选取模糊集参数如下:
      NumMFs(模糊子集个数)=3
      MF1=’N’:’trapmf’,[-20 -20 -5 0];
      MF2=’Z’:’trimf’,[-3 0 3];
      MF3=’P’:’trapmf’,[0 5 20 20] 对于输出PWM值,初步选取模糊集参数如下:NumMFs(模糊子集个数)=5 MF1='NB':'trimf',[-255 -180 -100]; MF2='NS':'trimf',[-200 -110 -20]; MF3='ZZ':'trimf',[-75 0 75]; MF4='PS':'trimf',[20 110 200]; MF5='PB':'trimf',[100 180 255]

利用库函数设置输入输出模糊子集程序代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
```
//setRange函数设置输入输出论域,输入范围为[-20,20],输出范围为[-255,255]
fuzzyController.setRange(-20,20,-20,20,-255,255);
/*addmf函数添加隶属度函数
* 第一个参数表示变量类型(1表示input1,2表示input2,3表示input3)
* 第二个参数表示模糊子集索引(模糊子集名称)
* 后面的参数是三角型或梯形隶属度函数的参数 */
fuzzyController.addmf(1,N,-20,-20,-5,0);
fuzzyController.addmf(1,Z,-3,0,3);
fuzzyController.addmf(1,P,0,5,20,20);
fuzzyController.addmf(2,N,-20,-20,-5,0);
fuzzyController.addmf(2,Z,-3,0,3);
fuzzyController.addmf(2,P,0,5,20,20);
fuzzyController.addmf(3,NB,-255,-180,-100);
fuzzyController.addmf(3,NS,-200,-110,-20);
fuzzyController.addmf(3,ZZ,-75,0,75);
fuzzyController.addmf(3,PS,20,110,200);
fuzzyController.addmf(3,PB,100,180,255);
  1. 设计并建立模糊控制规则:
    模糊规则就是输入量和输出量间的模糊蕴含关系。确定PWM值大小的核心思想为“当追踪误差增大时增大PWM值”,建立9条模糊控制规则:
    • if追踪误差是 N and 误差变化率是 N ,then PWM值是 NB;
    • if追踪误差是 N and 误差变化率是 Z ,then PWM值是 NS;
    • if追踪误差是 N and 误差变化率是 P ,then PWM值是 ZZ;
    • if追踪误差是 Z and 误差变化率是 N ,then PWM值是 NS;
    • if追踪误差是 Z and 误差变化率是 Z ,then PWM值是 ZZ;
    • if追踪误差是 Z and 误差变化率是 P ,then PWM值是 PS;
    • if追踪误差是 P and 误差变化率是 N ,then PWM值是 ZZ;
    • if追踪误差是 P and 误差变化率是 Z ,then PWM值是 PS;
    • if追踪误差是 P and 误差变化率是 P ,then PWM值是 PB;

在这里解释一下我这里的规则是如何建立的,比如第一条,追踪误差是N,表明追踪误差小于0,误差变化率是N,表明误差变化率也小于0,误差还在继续减小,这说明追踪误差的绝对值是在增大的,那输出的PWM值绝对值也是应该增大的,至于方向,是和连接电路时电机的转动方向有关,我在这里用实物测试出此时应该反转,所以PWM值为负,所以设置输出PWM值是 NB。再看第七条,追踪误差是P,表明追踪误差大于0,误差变化率是N,表明误差变化率小于0,两者结合在一起说明追踪误差虽然大于0但是误差量是在减少的,这个趋势是我们想要看到的所以设置输出 PWM值是 ZZ。其他规则同理。
在这里想告诉大家,规则的设定在构造模糊控制器中占有很重要的地位,一定要根据实际情况来设置模糊规则,判断逻辑推理的正确性。
利用库函数添加模糊规则程序代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
//setrulenum函数设置模糊控制规则总数
fuzzyController.setrulenum(9);
//依次添加控制规则,第一次输入参数表示当前输入的是第几条规则
//后面三个参数分别为input1、input2、output所属的模糊子集索引(模糊子集名称)
fuzzyController.addrule(1,N,N,NB);
fuzzyController.addrule(2,N,Z,NS);
fuzzyController.addrule(3,N,P,ZZ);
fuzzyController.addrule(4,Z,N,NS);
fuzzyController.addrule(5,Z,Z,ZZ);
fuzzyController.addrule(6,Z,P,PS);
fuzzyController.addrule(7,P,N,ZZ);
fuzzyController.addrule(8,P,Z,PS);
fuzzyController.addrule(9,P,P,PB);
  1. 完整程序代码
    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
    /*
    Arduino通过FuzzyControl库构造一个双输入单输出的模糊控制器,控制直流电机追踪到理想的位置(本程序中理想位置设置的为正弦信号)
    by 梁悦(https://re-high.github.io/)

    如需获得本示例程序详细电路信息以及如何使用FuzzyControl库的更多知识,请参考我的博客:
    https://re-high.github.io/

    */
    #include <FuzzyControl.h>
    //初始化一个双输入单输出的模糊控制器
    //三个输入参数分别是输入量1、输入量2、输出量的模糊子集个数
    FuzzyControl fuzzyController(3,3,5);
    int fuzhi = 50; //fuzhi:正弦波的幅值
    #define ENCODER_A_PIN 2 //直流电机编码器的A相连接引脚
    #define ENCODER_B_PIN 3 //直流电机编码器的B相连接引脚
    #define motor1 8 //电机
    #define motor2 9 //电机
    #define ENA 5 //使能引脚
    #define STBY 12
    //定义模糊子集名称及其对应的索引
    #define N 1
    #define Z 2
    #define P 3
    #define NB 1
    #define NS 2
    #define ZZ 3
    #define PS 4
    #define PB 5
    long position; //编码器每获取一个脉冲,position+1
    double position_actual; //当前位置
    double error_last = 0;
    long preTime=0;



    void setup() {
    // put your setup code here, to run once:
    Serial.begin(2000000);
    //setRange函数设置输入输出论域,输入范围为[-20,20],输出范围为[-255,255]
    fuzzyController.setRange(-20,20,-20,20,-255,255);
    /*addmf函数添加隶属度函数
    * 第一个参数表示变量类型(1表示input1,2表示input2,3表示input3)
    * 第二个参数表示模糊子集索引(模糊子集名称)
    * 后面的参数是三角型或梯形隶属度函数的参数 */
    fuzzyController.addmf(1,N,-20,-20,-5,0);
    fuzzyController.addmf(1,Z,-3,0,3);
    fuzzyController.addmf(1,P,0,5,20,20);
    fuzzyController.addmf(2,N,-20,-20,-5,0);
    fuzzyController.addmf(2,Z,-3,0,3);
    fuzzyController.addmf(2,P,0,5,20,20);
    fuzzyController.addmf(3,NB,-255,-180,-100);
    fuzzyController.addmf(3,NS,-200,-110,-20);
    fuzzyController.addmf(3,ZZ,-75,0,75);
    fuzzyController.addmf(3,PS,20,110,200);
    fuzzyController.addmf(3,PB,100,180,255);
    //setrulenum函数设置模糊控制规则总数
    fuzzyController.setrulenum(9);
    //依次添加控制规则,第一次输入参数表示当前输入的是第几条规则
    //后面三个参数分别为input1、input2、output所属的模糊子集索引(模糊子集名称)
    fuzzyController.addrule(1,N,N,NB);
    fuzzyController.addrule(2,N,Z,NS);
    fuzzyController.addrule(3,N,P,ZZ);
    fuzzyController.addrule(4,Z,N,NS);
    fuzzyController.addrule(5,Z,Z,ZZ);
    fuzzyController.addrule(6,Z,P,PS);
    fuzzyController.addrule(7,P,N,ZZ);
    fuzzyController.addrule(8,P,Z,PS);
    fuzzyController.addrule(9,P,P,PB);
    pinMode(motor1, OUTPUT); //电机引脚设置
    pinMode(motor2, OUTPUT);
    pinMode(ENA,OUTPUT);
    pinMode(STBY,OUTPUT);
    digitalWrite(STBY, HIGH);
    pinMode(ENCODER_A_PIN, INPUT);
    pinMode(ENCODER_B_PIN, INPUT);
    attachInterrupt(0, read_quadrature, CHANGE); //设置外部中断



    }

    void loop() {
    unsigned long now = millis(); //获取当前时间
    float position_ideal=fuzhi*sin(0.1*3.14*now/1000); //产生正弦波
    float TimeCh = (now-preTime)/1000.0;
    float SampleTime = 0.05; //设置采样时间
    if (TimeCh>=SampleTime)
    {
    position_actual =(double) position/130; //标准化当前位置,和编码器的线数有关,这里的position_actual单位是圈数
    double error = position_actual - position_ideal; //计算当前时刻的追踪偏差
    double DTerm = (error-error_last)/TimeCh; //计算偏差变化率
    error = constrain(error,-20,20); //避免输入量1超过设置的论域,对输入量1进行限幅
    DTerm = constrain(DTerm,-20,20); //避免输入量2超过设置的论域,对输入量2进行限幅
    int u = (int)fuzzyController.caculate(error,DTerm); // 计算模糊控制器的输出量
    u = constrain(u,-255,255); //对输出量限幅
    dianji(u); //把计算得到的控制量传给电机转动函数
    error_last = error; //把当前控制周期的追踪偏差赋值给error_last,给下个控制周期计算用
    preTime = now; //把当前控制周期的时刻值赋值给preTime,给下个控制周期计算用
    serialprint(position_ideal,position_actual); //串口监控函数
    }
    }
    /**************************************************************************
    函数功能:外部中断读取编码器数据
    **************************************************************************/
    void read_quadrature(){
    // found a low-to-high on channel A ENA脚下降沿中断触发
    if (digitalRead(ENCODER_A_PIN) == LOW){
    // check channel B to see which way 查询ENB的电平以确认是顺时针还是逆时针旋转
    if (digitalRead(ENCODER_B_PIN) == LOW)
    position++;
    }
    // found a high-to-low on channel A ENA脚上升沿中断触发
    else{
    // check channel B to see which way 查询ENB的电平以确认是顺时针还是逆时针旋转
    if (digitalRead(ENCODER_B_PIN) == LOW)
    position--;
    }
    }
    /**************************************************************************
    函数功能:电机转动
    入口参数:控制量u
    **************************************************************************/
    void dianji(int u){
    if(u<0){
    digitalWrite(motor1, HIGH); //设置电机转动方向
    digitalWrite(motor2, LOW);
    analogWrite(ENA,abs(u)); //把PWM绝对值传入使能引脚,让电机转动
    }
    else{
    digitalWrite(motor1, LOW);
    digitalWrite(motor2, HIGH);
    analogWrite(ENA,abs(u));
    }
    }
    /**************************************************************************
    函数功能:串口输出当前时刻理想的位置信号和当前位置
    **************************************************************************/
    void serialprint(float position_ideal,double position_actual){
    Serial.print(position_ideal);
    Serial.print(" ");
    Serial.println(position_actual);
    }

实验结果

打开Arduino IDE自带的串口绘图器,可以看到跟踪结果如下图所示,蓝色线为理想的位置信号红色线为电机的实际位置。模糊控制的效果还是很不错的!
控制器性能的好坏取决于变量的模糊子集设定和建立的控制规则,想进一步提高控制效果可以通过定义更多的模糊子集增加控制规则修改隶属度函数参数等方式实现。

从串口输出的数据可以导出到matlab中进行进一步分析,透过数据可以分析这个控制器目前的问题,来设计更优的控制器。本程序中我设置的隶属度函数参数就是反复实验后确定的,下图是优化控制器参数前后的跟踪结果的对比图。可以看到,优化参数前的模糊控制器也是在追踪目标信号的,但是追踪误差很大,在根据实际情况优化隶属度函数参数后,追踪误差明显减小。


总结

从实验可以看出,使用本FuzzyControl库构造一个双输入单输出的模糊控制器是非常方便的,和在matlab中使用模糊工具箱搭建模糊控制器的过程很像,不论是给变量添加隶属度函数还是增减规则,库函数都可以直接实现。
目前的FuzzyControl库还有一些问题存在,比如说只支持三角形和梯形隶属度函数以及由于采用COG解模糊法所以输出变量的隶属度函数最好设置为等腰三角型。如果之后有时间,我会再更新FuzzyControl库,让它变得更完善。