RT-Thread优化智能车设计

Posted 卓晴

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RT-Thread优化智能车设计相关的知识,希望对你有一定的参考价值。

学 校:重庆大学
队伍名称:风林火山
参赛队员:谢文祥、梁华林、陈俊霖
带队老师:李敏

简 介: 本文主要介绍了基于RT Thread操作系统的智能视觉组四轮循迹智能小车系统的原理、软硬件设计以及小车制作过程,对小车的系统介绍包括车模机械结构的设计、模块电路的设计、传感器信号的处理、控制算法、神经网络算法以及整车调试的方法等。设计以恩智浦公司的单片机RT1064为控制核心,采用数字摄像头MT9V032采集赛道元素信息,增量式编码器获取小车的速度,陀螺仪获取小车姿态。程序基于RT Thread操作系统, IAR 作为集成编译环境,使用 无线串口、LCD 、拨码开关和按键作为辅助调试手段。经过大量的软硬件测试,实现了智能小车高速下的稳定循迹运动。

关键词 RT-ThreadRT1064赛道识别神经网络

 

§01


  全国大学生智能汽车竞赛是由教育部高等学校自动化专业教学指导委员会主办。该竞赛以“立足培养,重在参与,鼓励探索,追求卓越” 为指导思想,旨在促进高等学校素质教育,培养大学生的综合知识运用能力、基本工程实践能力和创新意识。智能车竞赛涉及自动控制、传感技术、电子、电气、计算机、机械与汽车等多个学科,为大学生提供了一个充分展示想象力和创造力的舞台,吸引着越来越多来自不同专业的大学生参与其中,激发了大学生的创新思维,对于其实践、创新能力和团队精神的培养具有十分重要的价值。

  全国大学生智能汽车竞赛组织运行模式贯彻“政府倡导、专家主办、学生主体、社会参与”的 16 字方针,充分调动各方面参与的积极性。

  RT-Thread,全称是 Real Time-Thread,是一个嵌入式实时多线程操作系统,基本属性之一是支持多任务,任务与任务之间通过任务调度器进行非常快速地切换(调度器根据优先级决定此刻该执行的任务),造成多个任务在一个时刻同时运行的错觉。也是一款主要由中国开源社区主导开发的开源实时操作系统。实时线程操作系统不仅仅是一个单一的实时操作系统内核,它也是一个完整的应用系统,包含了实时、嵌入式系统相关的各个组件:TCP/IP协议栈,libc接口,图形用户界面等。

  本文是本届参赛队伍风林火山对最终参赛车模的总体概括,并对制作过程中的技术细节做了展示。本文的参考文献中,《C ++ primer plus》阐述了基本的C++语言语法和程序设计思路,《模拟电子技术基础》详细讲述了电路的基本知识,《嵌入式实时操作系统:RT-Thread设计与实现》对嵌入式系统的开发调试方法做了简介,并对RT-Thread进行了详细的讲解。《智能车制作》对实际制作智能车时的总体方案进行了简要的阐述。

  本文将在其后的正文部分按照顺序阐述:
  1. 模型车设计制作的主要思路以及实现的技术方案概要说明;
  2. 模型车结构设计;
  3. 电路设计说明;
  4. RT Thread 的运用和控制策略的介绍;
  5. 开发工具、制作、安装、调试过程说明;
  6. 模型车的主要技术参数说明。

 

§02 型车设计制作


一、主控芯片的选择

  AI视觉组限制芯片厂商只能为NXP,故选择官方推荐的RT1064, 该芯片的主频高,可以满足彩色摄像头使用,硬件资源丰富。

▲ 图2.1 RT1064核心板

二、稳压电路的选择

  电源模块为系统其他各个模块提供所需要的电源。设计中,除了需要考虑电压范围和电流容量等基本参数之外,还要在电源转换效率、噪声和干扰等方面进行优化。可靠的电源方案是整个硬件电路稳定可靠运行的基础。

  核心板上电存在时序问题,IO不能先于内核上电,否则会导致内核无法启动。可以通过使用CR引脚来控制外围设备供电电路使能。因为当CR引脚变为高电平时,表面内核启动成功。外部电路通过三极管和Mos管设计出合理的电路。整车电源由额定电压7.4V,满电电压8.4V的锂电池供电。通过不同的稳压方案,将电池电压转换成各个模块所需要的电压。

  1) 锂电池供电,正常使用时电压在7.4~8.4V。可直接用于电机供电。
  2) 通过TPS76850产生5v电压,供给编码器,串口通信,陀螺仪等。
  3) 使用稳压芯片AMS1117系列稳压到6V,为转向舵机和数字舵机供电。
  4) 通过TPS76833产生3.3v电压,为摄像头供电

▲ 图2.2 稳压电路原理图

三、驱动电路的选择

  使用MOS桥电路驱动电机,为避免电流反冲进芯片,造成芯片损坏,利用隔离芯片为桥电路提供信号。

▲ 图2.3 驱动电路原理图

四、电流环电路

  电流环指的是电流反馈系统。一般指的是将输出电流采用负反馈的方式接入处理环节的方法,来提高电流的稳定性.在使用电流环后,可以通过电流内环度外环相结合来提高系统的性能,提高车运行的稳定性。

▲ 图2.4 电流环原理图

五、传感器的选择

  比赛所用的摄像头可以分为两大类: CCD摄像头和CMOS摄像头。CCD摄像头图像对比度高、动态特性好,但供电电压比较高,需要12V的工作电压。耗电严重,拍摄的图像稳定性并不高。CMOS摄像头,体积小,图像稳定性较高,只需3.3V供电,耗电量低。总转风摄像头是一款基于MT9V032芯片设计的CMOS摄像头,是逐飞科技独家研发的一款高性能,在恩智浦竞赛市面上性能最优,最适合高速情况下的图像采集的全局快门摄像头。

▲ 图2.5 总转风摄像头

六、编码器的选择

  采用逐飞科技的带方向1024线光电编码器,输出轴上的机械几何位移量转换成脉冲或数字量,实现对速度的测量,从而完成对电机的闭环控制。

▲ 图2.4 1024线光电编码器

 

§03 感器安装


一、摄像头

  逐飞科技设计的130度无畸变总钻风广角摄像头,调节镜片中心离地面的高度为30cm ,视角为30°,智能车据此获得了良好且稳定的图像。

▲ 图3.1 模型车侧视图

二、编码器

  为了实现电机闭环,提高控制精度,本车使用了1024线带方向的编码器,并对编码器传回数据进行简单的滤波处理,在不牺牲处理时间的前提下尽可能提高编码器传回数据的精确度。

三、系统电路板的固定及连接

  C车模由于电机及编码器都在车尾部,重心偏后,所以我们在不影响舵机连杆运动的情况下将电路板尽量靠前安装,通过对硬件的合适处理,减小车的重心到最低,并通过四个电子秤保证车子重心尽可能在车中心。

 

§04 路设计


一、最小系统部分

  在设计主板的时候,主要考虑的原则是电路板足够窄,同时满足电流的要求,适当增大线宽,在布局布线的时候,尽可能考虑电流的流向。

▲ 图4.1 最小系统部分

二、电机驱动部分

  驱动电路对电流的要求更严格,电流线要格外加粗,同时为了利于散热,我进行了开窗处理,同时必须采用隔离芯片防止烧坏单片机。并且增加电流信号采集芯片,为电流环和电机保护提供硬件基础。

▲ 图4.2 电机驱动部分

 

§05 制算法


  基于RT Thread的控制算法

一、程序运行流程

  在AI视觉组的任务相对其他更加繁杂,包括基础循迹、数字识别、二维码识别、激光打靶等等。同时为了方便调试运行、需要TFT显示标志位、按键响应、蜂鸣器提示和LED闪烁等功能。对于这样一个智能车任务,完全可以将它分解成多个简单、容易解决的小问题,小问题逐个被解决,大问题也就随之解决了。在多线程操作系统中,也同样需要开发人员把一个复杂的应用分解成多个小的、可调度的、序列化的程序单元,当合理地划分任务并正确地执行时,这种设计能够让系统满足实时系统的性能及时间的要求

  在本次智能车比赛中我们主要使用了RTthread中的线程、时钟、线程同步等功能。

  用于检测电池电压变化的定时器bat_timer:

rt_timer_t bat_timer = rt_timer_create("bat_timer", adc_bat_thread, RT_NULL, ADC_BAT_INTERVAL, RT_TIMER_FLAG_PERIODIC); 
    rt_timer_start(bat_timer); 
 
void adc_bat_thread(void *parameters) 
{ 
    uint16 value = adc_mean_filter(ADC_1, ADC1_CH3_B14, 5); 
    f1.bat_voltage = (float)value * 3.3 / 0xFFF * 5.7; 
} 

  用于获取陀螺仪数据的定时器gyro_timer,并在定时器线程中进行姿态解算:

rt_timer_t gyro_timer = rt_timer_create("gyro_timer", gyro_thread, RT_NULL, GYRO_INTERVAL, RT_TIMER_FLAG_PERIODIC); 
rt_timer_start(gyro_timer); 
 
void gyro_thread(void *parameter) 
{ 
    get_icm20602_gyro_spi(); 
    get_icm20602_accdata_spi(); 
 
    inoutdev.update_gyro_y(); 
    inoutdev.update_gyro_z(); 
} 
 
void inoutdev_ctrl::update_gyro_z() 
{ 
    LPF_1_db(35, 1000, (float)(icm_gyro_z - gyro_z_offset), &gyro_z_filter); 
 
    yaw_rate = -0.03051757f * gyro_z_filter; 
    if (yaw_rate < 1.3 && yaw_rate > -1.3) 
        yaw_rate = 0; 
 
    // rt_kprintf("%f\\n", inoutdev.y_rate); 
    delta_z = 10.3 * yaw_rate / 1000.0; 
    gyro_z_angle += delta_z; 
    start_angle += delta_z; 
 
    if (fabs(gyro_z_angle) >= 360) 
        gyro_z_angle = 0; 
} 

  用于更新电流环的定时器current_timer:

current_timer = rt_timer_create("current_timer", current_thread, RT_NULL, CURRENT_INTERVAL, RT_TIMER_FLAG_PERIODIC); 
   
void current_thread(void *parameters) 
{ 
    if (0 == f1.chassis_update_type) 
    { 
        chassis.left_wheel.update_current(); 
        chassis.right_wheel.update_current(); 
    } 
    else if (1 == f1.chassis_update_type) 
    { 
    } 
} 

  用于更新速度环的定时器chassis_timer:

rt_timer_t timer = rt_timer_create("chassis_timer", chassis_thread, RT_NULL, CHASSIS_INTERVAL, RT_TIMER_FLAG_PERIODIC); 
rt_timer_start(timer); 
 
void chassis_thread(void *parameter) 
{ 
    if (0 == f1.chassis_update_type) 
    { 
        chassis.left_wheel.update(); 
        chassis.right_wheel.update(); 
        chassis.update_speed(); 
    } 
    else if (1 == f1.chassis_update_type) 
    { 
        chassis.left_wheel.get_speed(); 
        chassis.right_wheel.get_speed(); 
        chassis.update_speed(); 
 
        if (chassis.speed_now[0] > 2) 
        { 
            chassis.hard_bake_cnt = 0; 
            chassis.left_wheel.w_motor.run(-1000); 
            chassis.right_wheel.w_motor.run(-1000); 
        } 
        else 
        { 
            chassis.hard_bake_cnt++; 
            if (chassis.hard_bake_cnt >= 3) 
            { 
                chassis.set_speed(0, 0); 
                f1.chassis_update_type = 0; 
            } 
        } 
    } 
 
    chassis.update_dis(); 
} 

  用于更新TFT显示屏的定时器display_timer,并且其线程优先级最低,以确保其他控制小车运行的线程正常运行:

rt_thread_t tid_display = rt_thread_create("display_thread", display_thread, RT_NULL, 2048, 31, 5); 
rt_thread_startup(tid_display); 
 
void display_thread(void *param) 
{ 
    while (1) 
    { 
        ui_title_fresh(NULL); 
        rt_thread_mdelay(100); 
    } 
} 

  在工程代码中使用了基于RTthread的agile_button的库,agile_button是基于RT-Thread实现的button软件包,提供button操作的API。特性:代码简洁易懂,充分使用RT-Thread提供的API;详细注释;线程安全;断言保护;API操作简单

key1 = agile_btn_create(KEY1_PIN, PIN_LOW, PIN_MODE_INPUT_PULLUP); 
key2 = agile_btn_create(KEY2_PIN, PIN_LOW, PIN_MODE_INPUT_PULLUP); 
key3 = agile_btn_create(KEY3_PIN, PIN_LOW, PIN_MODE_INPUT_PULLUP); 
key4 = agile_btn_create(KEY4_PIN, PIN_LOW, PIN_MODE_INPUT_PULLUP); 
key5 = agile_btn_create(KEY5_PIN, PIN_LOW, PIN_MODE_INPUT_PULLUP); 
key6 = agile_btn_create(KEY6_PIN, PIN_LOW, PIN_MODE_INPUT_PULLUP); 
key7 = agile_btn_create(KEY7_PIN, PIN_LOW, PIN_MODE_INPUT_PULLUP); 

agile_btn_set_event_cb(key1, BTN_CLICK_EVENT, btn_event_cb); 
agile_btn_set_event_cb(key2, BTN_CLICK_EVENT, btn_event_cb); 
agile_btn_set_event_cb(key3, BTN_CLICK_EVENT, btn_event_cb); 
agile_btn_set_event_cb(key4, BTN_CLICK_EVENT, btn_event_cb); 
agile_btn_set_event_cb(key5, BTN_CLICK_EVENT, btn_event_cb); 
agile_btn_set_event_cb(key6, BTN_CLICK_EVENT, btn_event_cb); 
agile_btn_set_event_cb(key7, BTN_CLICK_EVENT, btn_event_cb); 

agile_btn_start(key1); 
agile_btn_start(key2); 
agile_btn_start(key3); 
agile_btn_start(key4); 
agile_btn_start(key5); 
agile_btn_start(key6); 
agile_btn_start(key7); 

  同时也使用了基于RTthread的easyblink的库。小巧轻便的LED控制软件包,可以容易地控制LED开、关、反转和各种间隔闪烁,占用RAM少,可以设置成线程安全型的;同时提供 RT-Thread 标准版和 Nano 版。

  特点:和其它LED同类软件相比,easyblink 有一个显著的特点,占用 RAM 特别少,其它 LED 软件一般每一个LED都需要创建一个线程,LED一多,线程数就多了,所占用的栈空间就相应的增大。而 easyblink 始终只使用一个守护线程(线程栈可以是预先分配的静态栈空间),无论多少个 LED,就一个线程。另外,有不需要移植就可以直接使用的 Nano 版,特别适合 RAM 紧张的产品。同时,也可以设置成线程安全型的。

    errno_led = easyblink_init_led(LED0_PIN, PIN_LOW); 
    info_beep = easyblink_init_led(BEEP_PIN, PIN_HIGH); 
easyblink(errno_led, -1, 200, 1000); 
 
void inoutdev_ctrl::beep(int num) 
{ 
    easyblink(info_beep, num, 30, 80); 
} 
 
void inoutdev_ctrl::lbeep(int num) 
{ 
    easyblink(info_beep, num, 200, 400); 
} 

二、定义信号量

  RT-Thread操作系统支持采用信号量、互斥量、邮箱、消息队列等等方式进行线程间的通信。由于我们在智能车的设计中只是用了信号量和互斥量,我们将仅介绍信号量和互斥量。

  信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它, 从而达到同步或互斥的目的。信号量就像一把钥匙,把一段临界区给锁住,只允许 有钥匙的线程进行访问:线程拿到了钥匙,才允许它进入临界区;而离开后把钥匙 传递给排队在后面的等待线程,让后续线程依次进入临界区。

  互斥量又叫相互排斥的信号量,是一种特殊的二值性信号量。它和信号量不同的是, 它支持互斥量所有权、递归访问以及防止优先级翻转的特性。互斥量的使用比较单一,因为它是信号量的一种,并且它是以锁的形式存在。 在初始化的时候,互斥量永远都处于开锁的状态,而被线程持有的时候则立刻转为 闭锁的状态。

chassis_mutex = rt_mutex_create("chassis_mutex", RT_IPC_FLAG_FIFO); 
csi_done_sem = rt_sem_create("csi_done_sem", 0, RT_IPC_FLAG_FIFO); 
ano_upload_sem = rt_sem_create("ano_upload_sem", 0, RT_IPC_FLAG_FIFO); 

三、初始化线程\\定时器

  对需要使用的硬件模块进行初始化,然后便开始为各个模块创建自己的线程,设置线程的运行周期,对于不能使用操作系统的裸机只能通过定时器中断来实现各个模块的周期运行,然而使用RT Thread后,可以更方便的管理各个模块的运行,也不用关心底层定时器怎么写,不必担心定时器数量是否够用,RT Thread编写的智能车代码更具有可读性,

current_timer = rt_timer_create("current_timer", current_thread, RT_NULL, CURRENT_INTERVAL, RT_TIMER_FLAG_PERIODIC); 

  电流环的定时器配置,设置定时周期为CURRENT_INTERVAL,优先级为RT_TIMER_FLAG_PERIODIC
  rt_timer_start 设置完成定时器的参数之后定时器不会立即启动,只有调用rt_timer_start函数之后定时器才开始计时,这个定时我们设定在主函数里面 rt_thread_mdelay(3000); rt_timer_start(current_timer);
  程序延时3s后开始计时,开启电流环
rt_thread_t tid_display = rt_thread_create("display_thread", display_thread, RT_NULL, 2048``,``31, ````5);
  rt_thread_startup(tid_display);
  在这里创建并开启了显示屏的线程,并且分配堆栈大小为2048,设置线程优先级为31,设置时间片为5

四、线程间通信

  线程通过获取信号量来获得信号量资源实例,当信号量值大于零时,线程将获得信号量,并且相应的信号量值都会减1。在调用这个函数时,如果信号量的值等于零,那么说明当前信号量资源实例不可用,申请该信号量的线程将根据time参数的情况选择直接返回、或挂起等待一段时间、或永久等待,直到其他线程或中断释放该信号量。如果在参数time指定的时间内依然得不到信号量,线程将超时返回,返回值是-RT_ETIMEOUT。这里我们获取csi图像采集的信号,判断图像采集是否完成,完成后再进行下一步工作,实现图像的处理,路径规划,元素识别,速度控制等等。

  当通过设定摄像头帧率后,摄像头完成一帧图像获取后发出信号量,在main函数中接受道信号量后继续运行,并进行后续的图像处理:

rt_sem_take(csi_done_sem, RT_WAITING_FOREVER); 
rt_sem_control(csi_done_sem, RT_IPC_CMD_RESET, RT_NULL); 

  inoutdev的类中clock函数用于统计实际帧率:

inoutdev.clock(); 
 
void inoutdev_ctrl::clock() 
{ 
    current_time = pit_get_ms(PIT_CH0); 
    fps = 1000 / (current_time - last_time); 
    last_time = current_time; 
} 

五、 控制策略选择

  PID控制器是一种线性控制器,它根据给定值与实际输出值构成控制偏差。将偏差的比例§、积分(I)和微分(D)通过线性组合构成控制量,对被控对象进行控制,故称PID控制器。传统PID控制器自出现以来,凭借其结构简单、稳定性好、工作可靠、调整方便等优点成为工业控制主要技术。当被控对象的结构和参数具有一定的不确定性,无法对其建立精确的模型时,采用PID控制技术尤为方便。PID控制原理简单、易于实现,但是其参数整定异常麻烦。对于小车的速度控制系统而言,由于其为时变非线性系统不同时刻需要选用不同的PID参数,采用传统的PID控制器,很难使整个运行过程具有较好的运行效果。

▲ 图5.1 PID控制器 原理图

1、位置式PID

  位置式PID中,由于计算机输出的u (k) 直接去控制执行机构(如阀门),u(k)的值和执行机构的位置(如阀门开度)是一一对应的,所以通常称公式2为位置式PID控制算法。

  位置式PID控制算法的缺点是:由于全量输出,所以每次输出均与过去的状态有关,计算时要对过去e(k)进行累加,计算机工作量大;而且因为计算机输出的u(k)对应的是执行机构的实际位置,如计算机出现故障,u(k)的大幅度变化,会引起执行机构位置的大幅度变化,这种情况往往是生产实践中不允许的,在某些场合,还可能造成严重的生产事故。因而产生了增量式PID 控制的控制算法,所谓增量式PID 是指数字控制器的输出只是控制量的增量△u(k)。

#ifndef __POS_PID__ 
#define __POS_PID__ 
 
#include "headfile.h" 
#include "rtthread.h" 
using namespace rtthread; 
 
class pos_pid 
{ 
public: 
    float kp = 0; 
    float ki = 0; 
    float kd = 0; 
 
    float error = 0; 
    float error_last = 0; 
    float error_diff = 0; 
 
    float error_history[5]; 
    float error_diff_history[5]; 
 
    float p_error = 0; 
    float i_error = 0; 
    float d_error = 0; 
 
    float last_out = 0; 
    float output = 0; 
    float target = 0; 
    float intergral = 0; 
 
    float anti_wind_radio = 0.5f; 
    float anti_windup_value = 0; 
 
    float maximum = 0; 
    float minimum = 0; 
 
    pos_pid() {} 
 
    pos_pid(float kp, float ki, float kd, float maximum, float minimum) : kp(kp), ki(ki), kd(kd), maximum(maximum), minimum(minimum) 
    { 
        anti_windup_value = maximum * anti_wind_radio; 
    } 
 
    void set_pid(float kp_p, float ki_p, float kd_p) 
    { 
        kp = kp_p; 
        ki = ki_p; 
        kd = kd_p; 
    } 
 
    void update(float current_point) 
    { 
        error = target - current_point; 
 
        for (int i = 5; i >= 1; i--) 
            error_history[i] = error_history[i - 1]; 
        error_history[0] = error; 
 
        intergral += ki * error; 
        __Limit_Both(intergral, anti_windup_value); 
 
        error_diff = (error - error_last); 
 
        for (int i = 5; i >= 1; i--) 
            error_diff_history[i] = error_diff_history[i - 1]; 
        error_diff_history[0] = error_diff; 
 
        p_error = kp * error; 
        i_error = intergral; 
        d_error = kd * error_diff; 
 
        last_out = p_error + i_error + d_error; 
        __Limit_AB(last_out, minimum, maximum); 
 
        error_last = error; 
        output = last_out; 
    } 
}; 
 
#endif 
 

2、增量式PID

  当执行机构需要的是控制量的增量(例如:驱动步进电机)时,可由式2推导出提供增量的PID控制算式。
  增量式PID具有以下优点:

(1) 由于计算机输出增量,所以误动作时影响小,必要时可用逻辑判断的方法关掉。

(2) 手动/自动切换时冲击小,便于实现无扰动切换。此外,当计算机发生故障时,由于输出通道或执行装置具有信号的锁存作用,故能保持原值。

(3) 算式中不需要累加。控制增量△u(k)的确定仅与最近k次的采样值有关,所以较容易通过加权处理而获得比较好的控制效果。
  但增量式PID也有其不足之处:积分截断效应大,有静态误差;溢出的影响大。使用时,常选择带死区、积分分离等改进PID控制算法。

#ifndef __INC_PID__ 
#define __INC_PID__ 
 
#include "common_macro.h" 
#include "rtthread.h" 
using namespace rtthread; 
 
class inc_pid 
{ 
public: 
    float kp = 0; 
    float ki = 0; 
    float kd = 0; 
    float error = 0; 
    float error_l = 0; 
    float error_ll = 0; 
    float p_error = 0; 
    float i_error = 0; 
    float d_error = 0; 
 
    float last_out = 0; 
    float output = 0; 
    float target = 0; 
    float maximum = 0; 
    float minimum = 0; 
 
    inc_pid() {} 
 
    inc_pid(float kp, float ki, float kd, float maximum, float minimum) 
        : kp(kp), ki(ki), kd(kd), maximum(maximum), minimum(minimum) {} 
 
    void set_pid(float kp_p, float ki_p, float kd_p) 
    { 
        kp = kp_p; 
        ki = ki_p; 
        kd = kd_p以上是关于RT-Thread优化智能车设计的主要内容,如果未能解决你的问题,请参考以下文章

智能车竞赛秘书处与RT-Thread关于第十七届智能车竞赛第一次会议

基于RT-Thread操作系统的 基础四轮组智能车设计与实践

基于 RT-Thread智能车控制算法开发-河南科技大学ROCKET

基于RT-Thread开发智能视觉组智能车-乐山师范学院

基于RT-Thread开发智能视觉组智能车 - 温州大学 - 春华秋实

第十六届全国大学生智能车竞赛RT-Thread创新专项奖获奖名单