阅读量: 次  文章字数: 5.6k字  阅读时长: 20分钟

本篇文章介绍《巡线解迷宫机器人》的相关内容。

前言

前段时间自己趁着开发木心软核处理器的间隙,有幸参加了芯来科技举办的《RISC-V处理器嵌入式开发》在线课程,该课程基于全球首颗RISC-V架构的通用量产微控制器GD32VF103,采用理论学习与动手实践相结合的方式,详细介绍了RISC-V嵌入式处理器的基础知识实战开发等内容。作为本土最早一批的RISC-V拓荒者,我觉得芯来此次以直播课的形式来推广国产RISC-V处理器应用生态的做法是非常值得称赞的,希望后期能够有更多有益于国产处理器生态建设的活动推出。

好了言归正传,本次芯来在线课程的最后有一个终极考核,需要各位学员基于RV-STAR开发板(或其他采用GD32VF103处理器的开发板)做一个面向嵌入式应用方面的小项目,题材与形式均不限。考虑到自己是第一次使用国产处理器来做实际的项目,在上手开发的过程中可能会遇到一些阻力(比如环境配置、代码封装等方面的),所以这次我打算先做一个简单点的项目来练练手,想来想去觉得自己下半年计划要做的巡线解迷宫机器人最为合适,不过由于时间有限的缘故,我决定先把这个机器人的原理验证机先搭建出来,等硬件和软件都经过验证没有问题后,我再实现一款像稚晖君所做的Qbot那样的小型且高度集成的巡线机器人。

概述

巡线解迷宫机器人是一款能够通过红外反射传感器实现自主巡线以及解迷宫等功能的小型三轮机器人,该机器人的硬件核心采用的是芯来基于GD32VF103处理器所自主设计的RV-STAR开发板,它主要负责处理红外反射传感器采所集到的模拟数据,并将处理后的数据后向下发送给电机驱动模块来精确控制两个直流电机的转向和转速。由于该机器人为原理验证机且为了提高项目整体的开发速度,机器人的所有元器件均采用TB上的现有模块,并使用洞洞板直接焊接的方式来构建其机械结构和电气连接。

在软件层面上,该机器人内部集成有经典的PID闭环控制算法,可根据模拟输入数据自动调整整个系统的动态平衡,使得机器人最终能够快速且平滑地沿着黑线移动。除此之外,该项目最大的亮点是我根据GD32VF103处理器的库函数手册等相关资料为RV-STAR开发板封装了一个类Arduino语法的静态链接库,取名为RVStarArduino,这样不仅我可以将自己之前基于Arduino所做的机器人项目中的一些代码无缝地移植过来,从而大大提高了项目的开发效率,还使得一些刚接触GD32嵌入式开发的小白用户能够快速上手实践,打消他们对传统嵌入式应用开发的畏难心理,毕竟不是所有人都喜欢研究底层技术嘛。

最后,该项目的代码可以从我GitHub仓库上获得,感兴趣的朋友可以下载到本地玩一玩,当然如果你在使用的过程中遇到了一些问题,欢迎在GitHub上给我提交Issues或者在文章评论区里留言,有空的话我肯定会及时回复的。

巡线解迷宫机器人图1

原理

硬件

  • 电机电源层

    电机电源层是整个机器人硬件架构中最底层且最基础的单元,它主要包含直流减速电机、电机驱动模块以及电源管理模块共三大部分,其中关于直流减速电机这块儿,我本次采用的是目前市场上应用广泛且体积较小的N20减速电机,考虑到机器人总装之后的负载情况以及自己对机器人巡线速度的需求,我最终购买的N20电机减速比为30:1,且额定电压为6V时,其空载转速为300RPM(即每分钟300转),这样就能保证动力系统在大负载情况下也能提供较为稳定的扭矩和速度输出,从而使得机器人的巡线过程变得更加高效。

    电机驱动模块我选用的是智能小车里面最常用的TB6612FNG,它相较于其他模块的最大优点在于其在驱动两路大电流电机的同时还能保持自身体积非常小巧,因为我这次做的巡线解迷宫机器人的尺寸非常小,为了节省宝贵的内部空间,TB6612FNG模块几乎可以说是我的唯一选择。至于该模块具体该如何使用,网上有很多教程可供参考,这里我就不展开讲解了,不过有两点大家需要注意一下:一是TB6612FNG的3个GND引脚是互通的;二是其STBY引脚必须接高电平才能正常驱动电机。

    最后,电源管理部分我使用的是TB上的一款低成本的锂电池充放电一体模块,这个模块最厉害的地方在于它不仅能够通过外置USB接口给锂电池充电,还能在锂电池放电的时候将其电压从3.7V升到5V,功能相当于原先的充电模块+升压模块,这对于寸土寸金的机器人内部空间来说简直就是一个梦幻般的存在。此外,该模块还带有智能省电功能,即当外接负载所需电流小于50mA达到30秒时会自动关闭电源输出,从而有效降低电池电量的损耗。

    电机电源层正面

    电机电源层背面

  • 主控处理层

    主控处理层作为上层决策单元,是整个机器人硬件架构中的核心。在本次的巡线解迷宫机器人中,它主要用于采样红外反射传感器回传的模拟数据并根据PID算法进行数学运算,最后将计算结果以逻辑电平信号的形式反馈给电机控制板,从而实现对直流电机转向与转速的精确控制。当然除了最核心的功能之外,主控处理层这块儿将来还会涉及到与带有IIC接口的OLED显示屏以及基于ESP8266的Wi-Fi模块间的数据交互等内容,也许到时候主控的性能才能被完全地发挥出来。

    RV-STAR开发板示意图(引用自官方论坛)

    接下来为了让大家对主控有一个基本的认识,这里先简要介绍一下芯来主推的这款RV-STAR开发板(以下内容引用自官网论坛):

    RV-STAR是一款基于GD32VF103 MCU的RISC-V评估开发板,提供了板载调试器、Reset和Wakeup用户按键、RGB LED、USB OTG,以及EXMC、Arduino和PMOD扩展接口等资源。

    相较于开发板本身的不俗性能以及所搭载的丰富外设资源,其实我更看重的是它在硬件上完全兼容Arduino这一点。玩过Arduino的同学们都知道,Arduino之所以一经推出便火遍全世界,就是在于它在软硬件层面上的易用性,而RV-STAR在硬件上支持Arduino扩展,这无疑可以让其复用很多现有的Arduino扩展板,如果后期能打通软件语法方面的壁垒(感兴趣的可以关注一下我写的RVStarArduino库),就可以真正实现将RV-STAR无缝接入到庞大且成熟的Arduino生态中,我想这也许是未来国产处理器生态环境建设可以走的捷径之一吧。

  • 外设模块层

    外设模块层位于机器人硬件架构的最顶层,主要用于与外部环境进行数据交互,在本项目最早的规划中这部分应该是由红外反射传感器、OLED显示屏与Wi-Fi透传模块等三个核心外设组成,不过由于时间缘故后两个我并没有添加到机器人上,所以这次我就先详细介绍一下红外反射传感器的原理与使用,而OLED和Wi-Fi模块我打算留到下篇的时候再讲。

    如下图所示,机器人底盘前端布置的一排黑色物体便是红外反射传感器,考虑到机器人的体积以及主控的运算能力,在器件选型方面我采用的是DIP封装的红外反射管TCRT5000,为了尽可能提高机器人巡线的精度,我总共使用了4个红外反射传感器,后期实际测试效果相当不错。在数据采集方面,我参考了稚晖君文章里所提到的方法,即直接使用主控的ADC功能来读取传感器的模拟数据,这样做的好处是不仅可以在代码中动态调整传感器的阈值,更重要的是它还能够让我们获得额外的道路信息,这对于提高机器人巡线的鲁棒性是非常有益的,具体内容我会在后文的算法部分进行讲解。

    电机电源层背面的四路红外反射传感器

软件

  • 环境配置

    古人曾经说过:工欲善其事,必先利其器,广大开发者们想要高效地开发嵌入式应用,简单易用且性能强大的开发工具肯定是必不可少的。根据论坛快速入门板块的介绍,RV-STAR开发板支持芯来的Nuclei Studio、Segger的Embedded Studio以及基于VSCode的PlatformIO等多种开发平台,当然如果你追求极致的开发效率,直接在命令行中用Make来编译、调试与上传代码也是没有问题的,不过这里我还是推荐小白用户首选IDE,这样后期在配置环境的过程中遇到的问题会少一些。

    有关具体如何配置IDE以及设备驱动的方法,官方手册和论坛里都介绍得很详细了,这里就不再赘述了,接下来我主要想讲讲自己是如何在Ubuntu 16.04等这类依赖低版本Python 3的GNU/Linux系统中安装最新版本的PlatformIO扩展插件,以及如何在PlatformIO中安装和使用我编写的RVStarArduino库进行二次开发。

    由于新版的PlatformIO最低仅支持Python 3.6,所以对于像我一样使用旧版GNU/Linux系统的人来说是无法使用PlatformIO的,唯一的解决办法就是通过第三方软件源来安装新版本的Python 3,具体操作如下:

    1
    2
    3
    $> sudo add-apt-repository ppa:deadsnakes/ppa
    $> sudo apt update
    $> sudo apt install python3.8 python3.8-distutils

    安装完成后需要手动将系统的Python 3环境变量指向最新安装的这个版本(下面命令中的python3.5请根据自己系统的实际情况进行修改):

    1
    2
    3
    $> sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.5 1
    $> sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 2
    $> sudo update-alternatives --config python3

    配置成功后即可按照正常流程在VSCode中安装PlatformIO扩展插件,但是注意如果你的系统使用的是类GNOME桌面环境的话,那切换到高版本Python 3很有可能会导致GNOME命令行终端软件无法使用,所以在装完PlatformIO后需要再次使用下面这个命令将Python 3的版本切换回去:

    1
    $> sudo update-alternatives --config python3

    至此,如果你能在重启VSCode后看到如下界面即表明PlatformIO扩展插件已成功安装。

    PlatformIO的主界面

    接下来我介绍一下如何在PlatformIO中安装RVStarArduino库,其实这个步骤非常简单,如下图所示,各位只需要在PlatformIO的库管理器中搜索RVStarArduino关键词,并在其介绍界面中点击【Add to Project】即可将库文件添加到自己的项目中。除此之外,你也可以直接在platformio.ini配置文件中添加库依赖参数,这样PlatformIO在编译项目代码的时候会自动下载对应版本的库文件。

    RVStarArduino库的主界面1

    RVStarArduino库的主界面2

  • 核心算法

    巡线解迷宫机器人的软件部分主要涉及传感器读取以及PID控制等核心算法,其中传感器读取部分我采用的是模拟式输入,即通过主控的ADC功能直接获取红外反射传感器的模拟电压值,数值范围为0~1023,估计看到这儿很多人都会问为什么我不使用更简单的数字式输入,其实这主要是因为现实场景中的很多事物并不是只有0和1两种状态,在本项目中红外反射传感器相对于黑线的位置是实时变化的,如果采用模拟量的话,系统可以表示两个边界之间的所有位置信息,精度相较于数字量的0和1会有比较大的提升。除此之外,得益于精度的显著提升,模拟量所表示的信息还能通过加权求和的方式更好地表征系统误差的大小,这对于后面的PID运算过程至关重要。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 获取每个红外反射传感器的模拟值
    // Get the analog value of each infrared reflection sensor
    uint16_t a0 = analogRead(A0);
    uint16_t a1 = analogRead(A1);
    uint16_t a2 = analogRead(A2);
    uint16_t a3 = analogRead(A3);

    // 使用加权求和的方式来表示系统误差
    // Use weighted summation to represent systematic errors
    float turn_error = 0.0;
    turn_error = turn_error + g_turn_error_k0 * a0;
    turn_error = turn_error + g_turn_error_k1 * a1;
    turn_error = turn_error - g_turn_error_k1 * a2;
    turn_error = turn_error - g_turn_error_k0 * a3;

    接下来就到了最经典的PID部分了,所谓PID就是比例、积分和微分的英文缩写,主要应用于各种闭环控制系统中。根据反馈器件的类型,PID中常见的控制环有:速度环、角度环和转向环,那大家可以猜猜本项目中使用的是哪种控制环吗?没错,因为我选择的是可以返回位置信息的红外反射传感器,所以机器人实际应用的是转向环PID算法。关于PID算法的原理网上有很多详细的资料可供参考,这里我就不再过多介绍了,大家可以尝试直接阅读下面的代码来理解其中的精髓。

    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
    // 计算转向环PID
    // Calculate turn PID
    g_turn_p = g_turn_kp * turn_error;
    g_turn_i = g_turn_ki * (g_turn_i + turn_error);
    g_turn_d = g_turn_kd * (turn_error - g_turn_error_last);
    float turn_sum = (g_turn_p + g_turn_i + g_turn_d) / 100;

    // 根据计算结果与当前所处状态控制电机的转速或转向
    // Control the speed or direction of the motor according to the
    // calculation result and the current state
    int16_t motor_speed_a = MOTOR_SPEED_REF;
    int16_t motor_speed_b = MOTOR_SPEED_REF;

    // 规定机器人水平向左为负向右为正,此时如果PID计算结果为正,则机器人需要向右偏移,
    // 右侧电机应降低一定的速度,速度变化值由PID计算结果的绝对值表示,反方向同理
    // It is stipulated that the horizontal left of the robot is negative
    // and the right is positive. At this time, if the PID calculation
    // result is positive, the robot needs to shift to the right, and the
    // right motor should reduce a certain speed. The speed change value is
    // represented by the absolute value of the PID calculation result.
    // The same goes in the opposite direction
    if (turn_sum > 0) {
    motor_speed_b = motor_speed_b - turn_sum;
    }
    else {
    motor_speed_a = motor_speed_a - abs(turn_sum);
    }

    setMotorSpeed(MOTOR_A, motor_speed_a);
    setMotorSpeed(MOTOR_B, motor_speed_b);

    核心算法完整代码如下所示:

    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
    float g_turn_kp = 20.0;
    float g_turn_ki = 0.0;
    float g_turn_kd = 5.0;
    float g_turn_p = 0.0;
    float g_turn_i = 0.0;
    float g_turn_d = 0.0;

    float g_turn_error_k0 = 2.0;
    float g_turn_error_k1 = 1.0;
    float g_turn_error_last = 0.0;

    uint32_t g_timer_turn_pid = 0;

    void cacluteTurnPID(void) {
    uint32_t ms = millis();
    if (ms - g_timer_turn_pid >= 50) {
    #ifdef DEBUG_PID_CYCLE
    printf("Time turn: %lums\n", ms);
    #endif
    // 获取每个红外反射传感器的模拟值
    // Get the analog value of each infrared reflection sensor
    uint16_t a0 = analogRead(A0);
    uint16_t a1 = analogRead(A1);
    uint16_t a2 = analogRead(A2);
    uint16_t a3 = analogRead(A3);
    #ifdef DEBUG_ADC_VALUE
    printf("A0: %u, A1: %u, A2: %u, A3: %u\n", a0, a1, a2, a3);
    #endif
    // 使用加权求和的方式来表示系统误差
    // Use weighted summation to represent systematic errors
    float turn_error = 0.0;
    turn_error = turn_error + g_turn_error_k0 * a0;
    turn_error = turn_error + g_turn_error_k1 * a1;
    turn_error = turn_error - g_turn_error_k1 * a2;
    turn_error = turn_error - g_turn_error_k0 * a3;

    // 计算转向环PID
    // Calculate turn PID
    g_turn_p = g_turn_kp * turn_error;
    g_turn_i = g_turn_ki * (g_turn_i + turn_error);
    g_turn_d = g_turn_kd * (turn_error - g_turn_error_last);
    float turn_sum = (g_turn_p + g_turn_i + g_turn_d) / 100;

    #ifdef DEBUG_PID_VALUE
    printf("Turn E: %.2f, Turn P: %.2f, Turn I: %.2f, Turn D: %.2f, "
    "Turn Sum: %.2f\n",
    turn_error, g_turn_p, g_turn_i, g_turn_d, turn_sum);
    #endif

    // 根据计算结果与当前所处状态控制电机的转速或转向
    // Control the speed or direction of the motor according to the
    // calculation result and the current state
    int16_t motor_speed_a = MOTOR_SPEED_REF;
    int16_t motor_speed_b = MOTOR_SPEED_REF;

    // 规定机器人水平向左为负向右为正,此时如果PID计算结果为正,则机器人需要向右偏移,
    // 右侧电机应降低一定的速度,速度变化值由PID计算结果的绝对值表示,反方向同理
    // It is stipulated that the horizontal left of the robot is negative
    // and the right is positive. At this time, if the PID calculation
    // result is positive, the robot needs to shift to the right, and the
    // right motor should reduce a certain speed. The speed change value is
    // represented by the absolute value of the PID calculation result.
    // The same goes in the opposite direction
    if (turn_sum > 0) {
    motor_speed_b = motor_speed_b - turn_sum;
    }
    else {
    motor_speed_a = motor_speed_a - abs(turn_sum);
    }

    #ifdef DEBUG_MTR_VALUE
    printf("Motor A: %d, Motor B: %d\n\n", motor_speed_a, motor_speed_b);
    #endif

    setMotorSpeed(MOTOR_A, motor_speed_a);
    setMotorSpeed(MOTOR_B, motor_speed_b);

    g_turn_error_last = turn_error;
    g_timer_turn_pid = ms;
    }
    }

问题

  • 我在测试TB6612FNG驱动板的时候,发现无论自己怎么设置电机始终不转,在排查硬件问题的过程中,我先后用万用表测试了电源管理模块输出和TB6612FNG驱动板的逻辑输入引脚,发现电源供电电压与信号高低电平均正常,所以首先可以排除电源供电不足和逻辑信号错误所导致电机不转的可能,之后我对驱动板的相邻引脚做了短路测试,偶然发现其BIN0、BIN1和GND引脚短接了,而短接出现的原因比较特殊:因为我是将两个电机直接固定在双面敷铜的洞洞板上,此时电机的外壳与正面的焊盘紧密接触,而背面的BIN和GND信号正好走在电机外壳的区域内,焊接时焊锡过孔导致两个信号与电机外壳联通,从而出现短接。但神奇的是解决短路问题后电机依旧不转,最后我是在面包板上复现TB6612FNG驱动电路的时候才发现自己竟然犯了一个非常低级的错误——忘记在代码中设置电机的PWM信号,我当初下意识认为PWM只用于调速,只要AIN或BIN信号设置对就能让电机转起来,可惜事与愿违。这个经历给我的启示在于做项目时要以官方提供的芯片或模块手册为准,不要想当然地按照自己的想法去做,否则后期出现问题的概率会比较高。

  • 在测试四路红外反射传感器的时候,我发现板子串口输出的数值竟然全部一样,经过单步调试后最终确定是自己编写的RVStarArduino库里的analogRead函数存在Bug,使得函数每次只能返回第一次设置的ADC通道的数值。解决方法是根据网上STM32 ADC多通道数据采集范例,将通道配置库函数从初始化阶段移动到analogRead函数中,这样系统就能根据不同的引脚编号来读取相对应的ADC通道数值了。

  • 转向环PID算法中存在浮点变量和与之相关的运算过程,但是令人费解的是,程序在执行过程中始终无法在串口里打印出浮点数值,后来经过仔细排查发现系统自带的newlib为了优化编译后的体积,默认并没有开启浮点数运算功能,所以程序一遇到浮点变量就直接跳过执行了。解决方法其实很简单,只需要在编译器的链接参数中指定【-u _printf_float】即可。

成果

巡线解迷宫机器人图2

巡线解迷宫机器人图3

巡线解迷宫机器人图4

巡线解迷宫机器人图5

巡线解迷宫机器人图6

总结

巡线解迷宫机器人是我基于国产处理器来开发嵌入式应用的首次尝试,通过一个月左右的理论学习与动手实践,我学会了如何在项目正式开始前做需求分析,如何根据机器人所要实现的功能来做软硬件方案的选型,如何在遇到Bug的时候通过调试机制来定位并解决问题,如何规划机器人内部空间使硬件布局更加合理与美观等等。虽然最后因为其他项目节点的缘故,我并没有给机器人添加OLED显示和Wi-Fi传输模块,同时也没有实现解迷宫功能,但是我至少验证了采用RISC-V架构的国产嵌入式处理器无论是在运行时性能还是在开发便捷性等方面均不输于目前主流的STM32,所以本项目成果从原理验证的角度上来看还是达到了自己的预期。

最后我一直坚信学习某个理论知识最好的办法就是将其应用到某个具体的项目中,特别是对于嵌入式开发来说,在开始上手前应该先给自己选择一个比较有挑战性的小项目,然后在后期学习的过程中通过不断地搜集资料与解决问题,尽自己最大的努力将其实现出来,这样到时候你就会发现很多原本晦涩难懂的理论知识都是那么的合理与自然。本次巡线解迷宫机器人项目就是一个最好的范本,对此感兴趣的同学也可以自己尝试做一个,然后去实现一些文中提到却没实现的功能,当然相比最后的成果,享受过程才是最重要的,预祝大家玩得开心。

留言

2021-04-14