面试项目 | 带你玩转大学生智能汽车项目

@[toc]

前言

本书适合有一些嵌入式入门基础的学习者阅读。希望阅读完本小书,能够解答大家这些问题

  • 硬件布线最基本的准则
  • 不带RTOS的嵌入式系统如何更好地开发
  • 嵌入式软件的分层设计怎么做
  • 在设计控制器时,被控系统的简化动态模型怎么获得
  • 传统的PID控制器设计在工程上到底怎么实施,如何仿真测试和写代码
  • Matlab/Simulink怎么可以帮助提高嵌入式控制器的开发和测试效率
  • 如何简单快速地做嵌入式控制器故障分析和错误排查,等等

智能车系统介绍

1.从智能车说起

本小书是以全国大学生智能车比赛作为案例来展开,这是我大学时候的最爱,也是目前自己带的学生创新实验室主要的比赛项目,所以站在这个角度给大家简单说一下如何构建一个基本的嵌入式系统,不求实现的多么完美,但求简洁而明白。

智能车比赛主要任务是做赛道寻线跟踪处理,用摄像头采集前方赛道图像,实现车速和转向的控制,达到跟踪赛道行驶的效果,如果类比人开车的话,摄像头就相当于司机的眼睛,图像处理和控制相当于老司机,转速控制相当于加减油门和刹车,转向控制相当于方向盘,这样对应理解了吧。

智能车整体结构的俯视图和侧视图如图1所示,后轮电机驱动,电池后放,摄像头中间放置,舵机改造为立式。

图1.智能车结构图

整个智能车系统里面涉及几部分:机械设计,硬件设计,嵌入式平台软件,控制算法软件与图像处理软件,本小书将会重点对后三部分详细展开介绍。这里要说一点,虽然本小书不探讨机械,但是整个智能车机械部分是非常重要的部分,或者可以这样说,机械调教决定了车的理论最高车速,而控制软件部分只决定实际车速,由于受限于自己自动化的专业背景,机械这方面不能过多展开。如果想深入学习机械,请大家阅读相关的专业书籍。

2.整体系统结构

智能车的整体结构如图2所示,上面是软件部分,主要包括图像处理,转速控制和转向控制,下面是硬件机械部分,主要包括电机,驱动,编码器,舵机,摄像头。整个系统的信号流和控制结构都可以从图中看出来,获取图像进行处理得到加减速指令和转向指令,下发给转速控制器和转向控制器,转向控制器负责控制舵机实现路径跟踪,转速控制器负责两个后轮的转速闭环控制。

  • 机械与硬件设计(电源,编码器,电机驱动,摄像头)

  • 嵌入式平台软件(系统结构,分层设计,模块设计)

  • 控制算法软件(转速PI控制器,Matlab仿真,转向控制器设计)

  • 图像处理软件(Matlab与C语言混合编程,图像处理与验证)

  • 系统测试与分析(串口辅助调试,IO辅助测试,Matlab可视化数据分析)

图2.智能车总体结构图

3.经验之谈

一个嵌入式产品都会涉及到机械,硬件和软件,需要三部分的协作才能够完成,如果把做这个比赛看成一个项目的话,那就要考虑时间上的安排,这里要注意三者时间上的关系,机械如果有定制件的话,迭代周期会是1-2个月,硬件的迭代周期一般是1-2周,软件的迭代周期是1-2天,务必要清楚这一点,用到的机械上大部件(电机,舵机,摄像头等),必须提前采购好,做好充分的备料,这玩意如果出了问题,妥妥地托一个星期没商量,所以在满足性能和可用性的前提下,尽量简化机械与硬件的设计,做好保护措施。同时也要提前安排好时间,比如PCB发出去之前,最好所有用到的元器件要买齐,不然PCB板回来了,根本焊不出一个完整板子。

系统分层设计

1.分层与模块化

说起分层,让我想起了大学刚毕业去的那家小公司,当时自己维护空调控制板,代码是前辈做的,功能相当棒,但是当我看到代码的那一刻,差点吐血,因为整个代码就两个文件shuikongtiao.c和shuikongtiao.h,里面的变量和函数定义全部用汉语拼音,哈哈哈,一万只草泥马从心中飞过。我擦,终于见识到了国产一线小厂的实力水平。看到代码的那一刻,就在想,这玩意居然好使,写代码的人是爽了,维护者怎么搞呀,一个.c文件上万行。当然这些都是内心戏,说出来,前辈肯定把我废了。

我大学时学C语言,刚开始码代码就是一个main.c然后用一个main函数搞定所有的事,比如下面:

#include<stdio.h>
#include<stdlib.h>

int main(int argc,char**argv)
{
    //定义变量,balabala
    int a,b;
    int sum;
    //输入数据或者读取文件
    scanf("%d %d",&a,&b);
    //处理逻辑
    sum=a+b;
    //打印输出
    printf("%d+%d=%d",a,b,sum);
    return 0;
}

这样的单文件单函数处理一些比如简单计算,文件读写,代码行不超过两个屏还不错,当代码超过2屏(大概150行),就要开始切分模块分割函数了,于是main.c变成了下面的样子。

#include<stdio.h>
#include<stdlib.h>
struct data{
    int a;
    int b;
}
int read_data(char*filename,struct data*d)
{
    //读取数据
}
int process_data(struct data*d)
{
    //处理数据和逻辑
}
int print_data(struct data*d)
{
    //输出数据
}

int main(int argc,char**argv)
{
    //定义变量,balabala
    struct data d;

    read_data("input.dat",&d)
    process_data(&d);
    print_data(&d);

    return 0;
}

通过将部分功能模块化抽离出函数,原本上千行的代码被切割为几个50-200行的代码,既方便阅读,又方便处理,随着功能继续完善,我们会产生不同的数据处理方式,比如添加,删除,修改,查看,查找,排序等,这时候我们就需要把处理数据的部分单独拿出来成为一个独立的模块,于是我们产生了新的模块process_data.c和process_data.h,其中.c文件负责模块的代码实现,.h负责模块的对外接口声明,其他的模块也类似,于是我们的代码变成了下面几个文件main.h,process_data.c和process_data.h,read_data.c和read_data.h,print_data.c和print_data.h。

//process_data.h
#ifndef __PROCESS_DATA_DEF__
#def __PROCESS_DATA_DEF__

//数据元素
struct data{
    int a;
    int b;
}
typedef struct data* dat;

//数据链表
struct data_list{
    struct data d;
    dat    next;
}
typedef struct data_list* dat_list;
extern int new_list   (dat_list dl);
extern int add_data   (dat_list dl,dat d);
extern int update_data(dat_list dl,int index,dat d);
extern int delete_data(dat_list dl,int index);
extern dat select_data(dat_list dl,char* cmd);
extern int sort_data  (dat_list dl);
extern int search_data(dat_list dl,dat d);

#endif
//process_data.c
#include "process_data.h"


int new_list   (dat_list dl)
{
}

int add_data   (dat_list dl,dat d)
{
    //添加数据
}
int update_data(dat_list dl,int index,dat d)
{
    //更新数据
}
int delete_data(dat_list dl,int index)
{
    //删除数据
}
dat select_data(dat_list dl,char* cmd)
{
    //查找数据
}
int sort_data  (dat_list dl)
{
    //排序数据
}
int search_data(dat_list dl,dat d)
{
    //查找数据
}

#endif

这时候的main.c就把process_data,read_data,print_data包含进来,即可以使用该模块,main.c的代码进一步缩减,框架和结构更清晰明了。

#include <stdio.h>
#include <stdlib.h>
#include "process_data.h"
#include "read_data.h"
#include "print_data.h"

int main(int argc,char**argv)
{
    //定义变量,balabala
    struct data d;
    dat_list dl;
    //输入数据
    new_list(dl);
    while(read_data("input.dat",&d) != 0)
         add_data(&d);

    //一系列的数据处理过程
    select_data(dl,d);

    //个性化显示数据
    print_data(&d);

    return 0;
}

随着系统功能进一步复杂,输入设备会有各种各样,输出设备与模式也会有各种各样的适配,为了控制系统的复杂度,会进一步进行分层,整体的进化流程就如图1所示。

图1.模块与分层进化图

其实说到底,最初其分成几个函数,到后面的模块化,再到最后的分层设计,都是在简化系统的复杂度,做到局部可控,这样才能hold住全场,让我们同一时刻只关注有限的信息量,毕竟都是人类,谁能一下子接受那么多code,更何况是凌乱的呢。善待code,善待自己,请从模块和分层开始。其实分层不是绝对的完美,所有的分层都会带来效率的降低,比如额外增加的函数调用时间损耗,但是为了可读性和可维护性,牺牲一点效率又能怎么样呢。不过千万不要过度分层,那是在装逼,不是在设计。

2.整个系统的总体设计

智能车系统的模块与分层划分,总体上分为三层,如图2所示。

  • 控制与图像层(转速与转向控制器,图像处理)
  • 嵌入式平台层(信号采集,器件驱动,任务调度)
  • 硬件与机械层(舵机,电机,硬件驱动,编码器,电源,摄像头,巴拉巴拉)

这三层划分,在一般的项目中正好对应四类工程师,控制与图像层对应控制与算法工程师,嵌入式平台层对应嵌入式软件工程师,硬件与机械层,对应嵌入式硬件工程师和机械设计工程师。如果要想实现一个完备的嵌入式系统产品,需要凑齐这四类人才才能够有备无患。

图2.整体系统模块结构图

控制与图像层

系统中图像处理模块图如图3所示,主要实现图像的处理,寻找中线,以及与Matlab2011a和VS2010配合实现对算法的快速仿真验证,大大提高开发效率,后文会重点介绍这里。

  • imCom:图像处理的公用模块
  • imProc:图像处理找中线和计算方向偏差的算法实现
  • imType:自定义数据类型
  • imCar:与Matlab的接口模块,用来快速批量验证算法

图3.图像处理模块图

系统中控制算法部分的模块图如图4所示,主要负责实现转速和转向控制,其中转速控制会结合Matlab/Simulink 进行仿真,寻找合理的PI控制参数,后面会详细展开如何设计PI控制器。

  • ControlVar:所有的共享全局变量
  • ControlParam:所有的全局配置参数
  • ControlGraphTask:图像和方向控制任务模块
  • ControlSpeedTask:速度控制任务模块

Control子模块介绍:

  • EIT_PID:PID控制器模块
  • EIT_SpeedL:左轮速度控制器
  • EIT_SpeedR:右轮速度控制器

图4.控制算法模块图

嵌入式平台层

嵌入式平台层,负责整个嵌入式软件系统的初始化,信号采集以及驱动执行,模块结构如图5所示,其中本小书会详细介绍EITLIb库中的电机驱动与编码器和摄像头采集部分。

  • CarDisplay:显示模块
  • CarSystem:系统初始化模块
  • CarTest:主循环模块
  • IntHandler:中断处理模块
  • Board:Vcan山外的K60核心板库
  • EITLib:自定义的硬件驱动库
  • Chip:Vcan山外实现的 K60的部件库
  • CMSIS:CMSIS支持库

图5.嵌入式平台模块结构图

硬件与机械层

硬件采用山外的K60核心板,其他部分,控制核心板和驱动电源板都是自制。模块结构如图6所示,其中主要的是Power,Camera,Motor,Sensor,LCD和Key模块图。

图6.系统硬件结构图

机械部分在ch2再做介绍。

到现在为止,对整个智能车系统有了一个总体的了解,下面我们会分模块进行详细的介绍。

机械与硬件设计部分

1.硬件布线

当初大学画电路板的时候,啥玩意都不懂,记得当初直接用面包板焊接,虽然好使,但其丑无比。后来工作也画过一些PCB,但是始终不得要领,以为能把线连上连对就万事大吉了。后来阅读了一些电子电路和硬件的书籍,有了自己的一点点体会,虽然不多,但是应付一般场合足够了。很简单的物理知识就可以理解,至于高手搞通信高频布线,要考虑群延时,分布电感电容,信号完整性分析,我觉得大家以后真的玩高级硬件的话,可以再深入,这里就算是一个简单入门吧。

这里我就先画一个非常简单的电路图,做一个简单的计算,大家就会明白。

比如一个电源给两个器件供电,一个工作电流10mA,一个工作电流10A,我们看下面图1中两种接线方式有啥区别?????

图1.两种接线方式理想电路图(红色为正,蓝色为负)

很多同学会说,不都是两个器件并联吗,不都是电源电压吗???电路原理的课上说啦,并联电压相等,所以上面的两张图,完全一个样子嘛,能有啥区别???!!!!

这里我要提醒大家啦,注意连线,那都是铜线,不是超导体,拿起小本子要记住啦,铜线也有阻抗,PCB布线也有阻抗。考虑进来,之后的图就是下面这样子图2所示。

图2.考虑线路阻抗的等效电路图

有的同学看到图2,会说,才20m欧,能有啥大事嘛。。。。大家要记住,我们这里还没说电源电压多少V呢,如果是1000V的话,那当然没事,但是如果电源是7V电压呢???你想想两种布局方式,1A器件与10A器件的静态工作电压有什么差别??

左侧布局 右侧布局
1A器件电压:7-(1+10)*(0.02*2)=6.56V 1A器件电压:7-10*0.02*2-1*0.02*4=6.52V
10A器件电压:7-10*0.02*4-1*0.02*2=6.16V 10A器件电压:7-10*0.02*2-1*0.02*2=6.56V

只通过一个简单的计算,我们就发现,右侧布局明显好于左侧布局,就因为10A电流在线路上产生了更大的线损,所以越靠近电源越好。这时候我们就得到这个结论,功率越大的器件,越靠近电源供电的话,那对整个系统工作的影响就越小。

于是我们的电路布局图就成为这个样子图3所示,大功率和小功率分布布局,大功率的地和电源尽可能从靠近电源的正负极直接引线,这样大功率器件对其他小功率器件的影响能够降到最小。

图3.大功率与小功率分开布局

下面我的电路继续升级,加入了PWM数字开关器件,还有一些小的器件,大家有没有想过,这样玩,会不会有啥问题。。。

图4.添加小器件和数字开关PWM部件

如果说第一条只用到了电路的欧姆定律,那这一条就要用到电磁感应原理,大家跟我的思路想哈,PWM要不停地开关,那0.5A电流就是交变的电流,交变的电流线圈会在整个线路环里产生交变的磁场,那是不是说所有包含在电源到0.5APWM器件的环路里的所有器件信号都会被这个交变磁场影响,因为交变的磁场会在其内部线路里又产生感性电动势和感应电流。这里只是0.5A,那如果10A电流也是开关器件呢,那简直瞬间干扰死那些线路里的小信号器件。于是我们就有了第二条定律,大功率器件的电源和地最好贴着走,不要包含小功率的器件。

于是图4就会进化到图5,虽然丑了点,是效果好呀!!!

图5.电源和地贴着走布局图

所以最终结论就是如下:

  • 尽可能大功率的器件靠近电源接线
  • 尽可能开关功率器件尤其是大功率的开关器件,电源和地包络器件越少越好。

铭记上面两条法则,应该能应付一般的PCB布线了。

智能车电路为例,整体电路分为三部分,电机驱动,舵机,控制电路,总体布线如图6所示,三大部分彻底分开,尤其是电机驱动这块,流大电流。

图6.智能车PCB布线图

电机驱动阻抗分析:

B车电机内阻150m欧,电池内阻80m欧,电源线,保险和开关20-30m欧,PCB布线电阻20m欧,整体线路内阻大概是250-300m欧,如果按照电池工作电压7-8V计算的话,那短路电流有20-30A

C车模相对好一些,电机内阻900m欧,加上电池内阻,电源线,开关,保险丝,PCB布线100-150m欧,总阻抗1欧,即使考虑到双电机并联,最大电流不超过15A左右。

2.电机H桥

聊完硬件布线,我们再聊聊直流电机,说简单点就是给它加直流电压,那它就转,比如图1的电路,一个开关K就可以控制电机转动和停止。

图7.开关控制电机转和停

如果我们想让既能让电机正转,又能反转,那该怎么设计呢,如图8所示,为了简化,图中未画出续流二极管。K1和K2通的时候,电机正转,K3和K4通的时候,电机反转,这个电路结构称为H桥电路。

图8.H桥正反转

H桥可以让电机工作于四个状态,如下表所示。

开关状态 电机状态
K1和K2闭合 正转
K3和K4闭合 反转
K2和K4闭合 刹车状态
四个开关都断开 滑行状态

截至目前,电机的工作电压,要么是电池电压,要么电压为0,要么是反向电池电压,如果想调压调速的时候,那怎么办呢???
搞电力电子的兄弟们发明了一种简单方法,比如现在电池只有7V电压,那可以这样玩,加个电子开关,给电机通电5ms7V,再断开5ms,然后依次循环,只要频率足够快就没啥事,这样等效下来是不是就相当于3.5V电压呀,然后通过调节开通关断时间比例,来连续调节电压,这就叫做PWM控制。简单点说,有了PWM调节,我们的电机就可以调压调速了

H桥调压调速具体玩法如图9所示。比如正向,下桥K2长通,然后给K1加PWM控制,K1导通的时候,电池电压加到电机上正向电流,如果图9的左边,K1关闭的时候,电池电机的电流通过K4的续流二极管进行续流,图9的右边。反转的话,依次类推即可。

图9.H桥调压调速图

电机控制的示例代码:

//初始化代码
void MotorR_Init(void)
{
   /*Motor Drive*/
   gpio_init (MOTORR_EN, GPO,0);
   FTM_PWM_init(MOTORR_FTM, MOTORR_PWMA, MOTORR_PWM_FREQ,0);
   FTM_PWM_init(MOTORR_FTM, MOTORR_PWMB, MOTORR_PWM_FREQ,0);

   /*Speed Measure*/
   FTM_QUAD_Init(MOTORR_ENCODE_FTM);
}
//PWM控制正反转
void MotorR_Run(int32 pwm)
{
   uint32 PWM_A =0;
   uint32 PWM_B =0;

   if(pwm>MOTORR_PWM_MAX)
       pwm=MOTORR_PWM_MAX;
   else if(pwm<MOTORR_PWM_MIN)
       pwm=MOTORR_PWM_MIN;

   if ( pwm >0 )
   {
      PWM_A = pwm;
   }
   else if ( pwm <0 )
   {
      PWM_B = -pwm;
   }
   FTM_PWM_Duty(MOTORR_FTM, MOTORR_PWMA,PWM_A);
   FTM_PWM_Duty(MOTORR_FTM, MOTORR_PWMB,PWM_B);
   gpio_set ( MOTORR_EN,1);
}
//刹车
void MotorR_Brake(void)
{   
   FTM_PWM_Duty(MOTORR_FTM, MOTORR_PWMA,0);
   FTM_PWM_Duty(MOTORR_FTM, MOTORR_PWMB,0);
   gpio_set  ( MOTORR_EN,1);
}
//滑行
void MotorR_Slip(void)
{
   gpio_set  (MOTORR_EN,0);
   FTM_PWM_Duty(MOTORR_FTM, MOTORR_PWMA,0);
   FTM_PWM_Duty(MOTORR_FTM, MOTORR_PWMB,0);
}

至于其他的硬件问题,我这里就不过多赘述,比如电源线正反要接对,稳压芯片前要加TVS管,抑制直流电机换向的尖峰脉冲电压,稳压电源的散热面积要大一些,尽量加保险丝保护,摄像头这块,我觉得山外的文档《ov7725数字摄像头编程基本知识笔记》已经解释的很清楚了,就不多赘述了。

3.机械这块简单说几句吧

首先有几点我一定要提醒大家:

  • 车前的防档杆必须加,否则你的舵机一撞墙齿轮就会被挤坏
  • 车底盘变形问题一定要注意
  • 轮胎务必加胎水,否则轮胎一上赛道滑的要命

其实,机械这块的改造,经过这十来年基本成型了,每年智能车比赛这块,大家差不多都一个模子。但是有个点,一定要强调,就是前轮定位的机械调教。

前轮定位的作用是保障汽车直线行驶的稳定性,转向轻便和减少轮胎的磨 损。前轮是转向轮,它的安装位置由主销内倾、主销后倾、前轮外倾和前轮前 束等 4 个项目决定,反映了转向轮、主销和前轴等三者在车架上的位置关系。 下面这几个角度的介绍摘自《第十届“飞思卡尔”杯全国大学生 智能汽车竞赛-北京科技大学电磁组一队》的技术报告,感谢他们做的这么好的总结。后续我仔细研究过汽车理论之后,再为大家详细画图详细介绍这几个角度对车辆的影响。

**主销后倾角
**

所谓主销后倾,是将主销(即转向轴线)的上端略向后倾斜。从汽车的侧面看去,主销轴线与通过前轮中心的垂线之间形成一个夹角,即主销后倾角。主销后倾的作用是增加汽车直线行驶时的稳定性和在转向后使前轮自动回正。由于主销后倾,主销(即转向轴线)与地面的交点位于车轮接地点的前面。这时,车轮所受到的阻力的作用点总是在主销轴线之后,相当于主销拖着车轮前进。这样,就能保持行驶方向的稳定性。当车转弯时,由于车轮所受阻力作用线,不通过主销轴线,这样,车轮所受阻力在主销方向有力矩
作用产生,迫使车轮自动偏转直到到车轮所受阻力作用线通过主销轴线,此时,车轮已回正,这就是转向车轮的自动回正功能。

主销后倾角越大,方向稳定性越好,自动回正作用也越强,但转向越沉重。汽车主销后倾角一般不超过3°,由前悬架在车架上的安装位置来保证。现代轿车由于采用低压宽幅子午线轮胎,高速行驶时轮胎的变形加大,接地点后移,因此主销后倾角可以减小,甚至为负值(变成主销前倾),以避免由于回正力矩过大而造成前轮摆振。

模型车通过增减黄色垫片的数量来改变主销后倾角的,由于竞赛所用的转向舵机力矩不大,过大的主销后倾角会使转向变得沉重,转弯反应迟滞,所以设置为0°,以便增加其转向的灵活性。

主销内倾角

所谓主销内倾,是将主销(即转向轴线)的上端向内倾斜。从汽车的前面看去,主销轴线与通过前轮中心的垂线之间形成一个夹角,即主销内倾角。主销内倾的作用是使车轮转向后能及时自动回正和转向轻便。对于模型车,通过调整前桥的螺杆的长度可以改变主销内倾角的大小,由于过大的内倾角也会增大转向阻力,增加轮胎磨损,所以在调整时可以近似调整为0°~3°左右,不宜太大。

主销内倾和主销后倾都有使汽车转向自动回正,保持直线行驶的功能。不同之处是主销内倾的回正与车速无关,主销后倾的回正与车速有关,因此高速时主销后倾的回正作用大,低速时主销内倾的回正作用大。

**车轮外倾角
**

前轮外倾角是指通过车轮中心的汽车横向平面与车轮平面的交线与地面垂 线之间的夹角,对汽车的转向性能有直接影响,它的作用是提高前轮的转向安 全性和转向操纵的轻便性。在汽车的横向平面内,轮胎呈“八”字型时称为“负 外倾”,而呈现“V”字形张开时称为正外倾。如果车轮垂直地面一旦满载就易 产生变形,可能引起车轮上部向内倾侧,导致车轮联接件损坏。所以事先将车 轮校偏一个正外倾角度,一般这个角度约在 1°左右,以减少承载轴承负荷,增 加零件使用寿命,提高汽车的安全性能。

模型车提供了专门的外倾角调整配件,近似调节其外倾角。由于竞赛中模 型主要用于竞速,所以要求尽量减轻重量,其底盘和前桥上承受的载荷不大, 所以外倾角调整为 0°即可,并且要与前轮前束匹配。

**前轮前束
**

所谓前束是指两轮之间的后距离数值与前距离数值之差,也指前轮中心线 与纵向中心线的夹角。前轮前束的作用是保证汽车的行驶性能,减少轮胎的磨 损。前轮在滚动时,其惯性力自然将轮胎向内偏斜,如果前束适当,轮胎滚动 时的偏斜方向就会抵消,轮胎内外侧磨损的现象会减少。像内八字那样前端小 后端大的称为“前束”,反之则称为“后束”或“负前束”。在实际的汽车中, 一般前束为 012mm。

在模型车中,前轮前束是通过调整伺服电机带动的左右横拉杆实现的。主 销在垂直方向的位置确定后,改变左右横拉杆的长度即可以改变前轮前束的大 小。在实际的调整过程中,我们发现较小的前束,约束 02mm 可以减小转向阻力, 使模型车转向更为轻便,但实际效果不是十分明显。

虽然模型车的主销后倾角、主销内倾角、车轮外倾角和前束等均可以调整, 但是由于车模加工和制造精度的问题,在通用的规律中还存在着不少的偶然性, 一切是实际调整的效果为准。

在实际调试中,我们发现适当增大内倾角的确可以增大转弯时车轮和地面的接触面积,从而增大车了地面的摩擦程度,使车转向更灵活,减小因摩擦不够而引起的转向不足的情况。前轮前束为0-1度左右,直线行驶更稳定。

硬件与机械就到此了,这块自己也还需要继续学习。

嵌入式平台软件搭建

1.从任务调度说起

最开始我们在单片机写代码的样子是怎样的呢?在ch1那一章我们对模块和分层进行了讨论,模块是对功能代码的封装,分层是在平台层面封装,都是在解决项目复杂度控制的问题,但是我们拿单片机最主要的目的是来执行任务Task帮我们做事的,比如读取ADC采样数据,读取键盘按键,输出PWM,I2C通讯,运行PID控制,等等。

那在单片机里如何组织任务调度的设计?

大循环调度

最初的最初,我们的任务调度简单直接——也就是大循环方式,示例代码如下:

int main()
{
    Dis_Interrupt();
    System_Init();
    En_Interrupt();

    while(1)
    {
        Task0_Run();
        Task1_Run();
        Task2_Run();
        Task3_Run();
        Task4_Run();
    }
}

void Task0_Run(void)
{
    Pot1Calc(); //加速器信号计算
    Pot2Calc(); //制动器信号计算(保留)
    TempCalc(); //电机及控制器温度计算
}
.....

大循环方式的任务调度如图1所示,优点就是简单直接,适合比较简单的系统,带来的不好的地方:

  • 每个任务的调度周期和时间是不固定的(if else的存在),无法保证确定的周期性执行任务
  • 随着任务数量的增加,系统会越来越慢
  • 如果遇上长时间任务,会拖累整个系统变慢

图1.大循环任务调度图

定时任务调度

为了克服大循环方式的缺点(任务调度周期性无法保证,任务数量增加系统会变慢),提出了定时的任务调度的方式,不过需要使用单片机一个定时器,来实现一个简单的任务调度器,利用定时器将CPU切割为一个等周期的时间片调度单元,然后利用标志位控制在每个时间片只调用一个任务。整个系统代码结构如下所示:

#define TASK_MAX_LENGTH 10
typedef struct
{
    Int16 Flag[TASK_MAX_LENGTH];
    Int16 Timer;
    Int32 Number;
} USERTASK;
USERTASK UserTask0={0,0,0,0,0,0,0,0,0,0,0,TASK_MAX_LENGTH};//任务初始化

//任务调度函数
void TaskScheduler(USERTASK* v)
{
    v->Flag[v->Timer++] = 1;
    if(v->Timer >= v->Number)
    {
        v->Timer = 0;
    }
}
//主函数
int main()
{
    Dis_Interrupt();
    System_Init();
    En_Interrupt();

    while(1)
    {
        Task0_Run();
        Task1_Run();
        Task2_Run();
        Task3_Run();
        Task4_Run();
    }
}

//1ms定时中断
__interrupt void Timer0_INT_MapedISR(void)
{
    TaskScheduler(&UserTask0);
}

//单个任务示例函数
void Task0_Run(void)
{
    if(UserTask0.Flag[0])
    {
        Pot1Calc();                    //加速器信号计算
        Pot2Calc();                    //制动器信号计算(保留)
        TempCalc();                    //电机及控制器温度计算

        UserTask0.Flag[0] = 0;
    }
}
......

定时任务调度的流程图如图2所示。与大循环调度方式对比,这种方式能够实现周期性的任务调度,同时随着任务的增加,依然能够保证调度的周期性,这种调度能够应对大多数的控制系统,比如TI的PMSM电机控制器,一般小的家电控制器,都可以搞定。但是使用时有几点要注意:

  1. 单个任务的最长时间长度务必保证不超过单个时间片,否则会导致周期性延迟
  2. 对于严格实时的控制周期任务,定时调度器不能够保证
  3. 对于长周期任务(比如通讯等待等),定时任务调度器要么把任务切割为小任务,要么安排几个连续的空闲周期来执行

图2.定时任务调度图

针对第1点,需要测试或者预估任务的最长执行时间,这个可以采用IO测试的方式解决(具体参见ch6)。

针对第2点,对于实时性要求高,并且周期控制快的任务(比如PID控制),只能将这个任务放到定时中断里做,示例代码如下:

//1ms定时中断
__interrupt void Timer0_INT_MapedISR(void)
{
    TaskScheduler(&UserTask0);
    Task_SpeedPID_Control();
}

//实时性要求高的任务,示例函数,如果控周期慢的话,也可以选择加入if(UserTask0.Flag[0])做判断
void Task_SpeedPID_Control(void)
{
   SpeedPID_Input();                    //读取输入指令和反馈信号
   SpeedPID_Run();                      //运行PID
   SpeedPID_Output();                   //输出PWM控制
}
......

针对第3点,我们可以将长周期任务放在最后面,如图3所示,可以把最后几个空闲周期都留给Task4执行。但是要注意,如果有多个长周期任务,依然会拖慢整个调度周期,于是就出现了基于优先级的任务调度方式,高优先级的任务可以中断低优先级的任务,在保证长周期任务调度的同时,短周期任务的调度依然能够保证,这就是RTOS。

图3.长周期调度方式

实时操作系统RTOS调度

实时操作系统,常用的小型RTOS有uCosII,FreeRTOS,Rt-thread,主要是任务优先级的调度方式不一样,这里感兴趣的同学,可以参见相关的专业书籍,对RTOS内核代码不做详细介绍。RTOS的对任务的调度方式如图4所示。Task0的优先级高,可以中断优先级低的Task1,等Task0执行完,然后RTOS会切换到Task1继续执行。

图4.RTOS任务调度方式图

2.智能车总体任务调度

智能车调度平台总体上只有两个任务SpeedControlTask和ControlGraphTask,考虑到系统简单,没有用RTOS和任务调度器,直接中断配合While实现,代码示例如下,运行时序如图5所示。

//main主循环
void main(void)
{                                                               
   DisableInterrupts;  
   CarSystem_Init();
   EnableInterrupts;
   Car_Test();//主循环在这里
   while(1);                                                  
}

//主循环
void  Car_Test(void)
{ 
    while(1)
    {  
       if(ImageOver)//图像DMA传输结束
       {
           ImageOver=0;
           img_extract((uint8 *)Image_Data, (uint8 *)imgbuff0, CAMERA_SIZE);//解压图像
           ControlGraphTask();//图像处理任务

           DataLog_Add();//数据记录
           if(DataLog_CheckEN())
              DataLog_Print();
      }
    }
}

//中断
#define CAM_VSYNC 29

void PORTA_handler(void)
{
    uint32 flag = PORTA_ISFR;
    PORTA_ISFR  = ~0; 
   if(flag & (1 << CAM_VSYNC))                                 //PTA29触发摄像头帧中断
   {
       ImageOver=0;                                            //清除图像采集标志                                  
       camera_vsync();
       gVar.time++;
   }
}
//DMA传输图像数据
void DMA0_IRQHandler()
{
    camera_dma();
    ImageOver=1;
}

//定时10ms中断,执行速度PID控制任务
void PIT0_IRQHandler(void)
{
   SpeedControlTask();
   PIT_Flag_Clear(PIT0);
}

图5.系统任务时序图

总体思路就是,每一幅图像的帧中断VSYNC触发PORTA_handler(PA29)中断函数,此时ImageOver清零,同时DMA开始传输图像,当DMA传输结束触发DMA0_IRQHandler中断,此时ImageOver=1,如果ControlGraphTask检测到的话,那就开始执行,如果ControlGraphTask在VSYNC到来清零ImageOver之前没有开始执行的话,那只能等待下一次DMA中断。最终测试结果,每两帧触发一次ControlGraphTask执行,控制周期为13.33ms。

3.嵌入式驱动层设计

嵌入式驱动层大部分复用了Vcan山外的板级库,新加入比较重要的库有EITMotorL,EITMotor_R,EIT_Steer和EIT_Log,封装在EITLib文件夹,总体的思路就是,.h负责接口,.c负责功能实现。

这里以Motor库为例,介绍一下嵌入式驱动库的封装。

考虑到速度控制用到Motor和Encode,所以把二者集成放到了一起,整体MotorR代码解析如下所示。

#ifndef __EIT_MOTORR_DEF__
#define __EIT_MOTORR_DEF__
#include "include.h"
/*Motor Driver*/
#define    MOTORR_PWM_MAX   1000                   //PWM范围:-1000到

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

ARM/Linux嵌入式真题 文章被收录于专栏

让实战与真题助你offer满天飞!!! 每周更新!!! 励志做最全ARM/Linux嵌入式面试必考必会的题库。 励志讲清每一个知识点,找到每个问题最好的答案。 让你学懂,掌握,融会贯通。 因为技术知识工作中也会用到,所以踏实学习哦!!!

全部评论

相关推荐

点赞 14 评论
分享
牛客网
牛客企业服务