OpenVINOTM,给你看得见的未来!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » T.I.Tech wire的驱版本RT-Linux的兼容性问题

共1条 1/1 1 跳转至

T.I.Tech wire的驱版本RT-Linux的兼容性问题

工程师
2011-03-15 14:34:06    评分
Quadlator II--T.I.Tech wire的驱版本RT-Linux的兼容性问题
写于: 星期六 29 四月 @ 13:23:23
 
智能机器人开发Quadlator II第四章

四, T.I.Tech wire的驱版本RT-Linux的兼容性问题 作者:baredog

决定写这个内容的时候,是2003年,然而今天动笔的时候,却已经是2006年了。3年改变了太多的东西,以至于已经没有意义再写太多关于T.I.Tech wire和RT-Linux的一些细节了。在google上搜索一下RT-Linux的中文资源,目前在国内的嵌入式应用领域的uCLinux和RT-Linux取得了流行的地位。如果写,也是这2年一直在坚持做这方面的人更有资格写。

因此,大概以下这些内容也许还能有些帮助:

  • Linux的驱动的基本概念,用户空间和内核空间
  • RT-Linux和Linux的关系,RT-Linux的内核空间程序
  • 开发机器人控制程序时的一些思考

关于Linux驱动的著述也有不少,比较出名的有O'Relly的《Linux Device Driver》。驱动的概念在现代操作系统,例如Linux和Windows 2K/XP后是个很重要的概念。尤其是涉及控制硬件的一些开发。在早期Dos和Windows 9X平台下,没有太多对用户的限制,因为本身他们并不强调真正的多用户,任何人都可以直接操控硬件资源。相信那时的开发人员中,不少人有利用Turbo C操纵BIOS中断和IO端口的经历。在Visual C++盛行之后,仍然可以利用_inp和_outp系列函数,直接访问端口。而有相当的设备,如ISA板卡,直接对寄存器IO就可以完成大部分的控制。

然而这样简单的生活,不能永远继续下去。Windows 95后VxD的概念逐渐提了出来,一些复杂的设备,如PCI已经没有办法再用简单的IO加中断完成控制。好在设计机器人控制的一些设备-DA,AD和IO仍然不是很复杂。随后的Windows XP彻底让这些“老”开发人员退出了历史舞台,砸掉了他们的饭碗。XP下,禁止用户直接IO,必须通过WDM驱动来访问硬件,并且复杂的Plug & Play及电源管理,IRP队列等等,把WDM的学习曲线一下子提高到让人望而生畏的地步。在大学和研究者圈内(设备开发商们当然只好硬攻WDM了)大家开始逐渐把目光放开,寻找其他能够让学生在短期内掌握的方法,缩短学习周期,把主要注意力集中到课题本身上来。

这样的方法有3个:

  1. 不继续升级,仍然使用老的Windows 9X,甚至DOS。例如我在清华自动化的检测实验室中的电容成像系统,在我毕业前的2002年底,仍然坚持使用Windows 98+自制的ISA卡,利用Visual C++ 6的IO函数直接读写。在2004年春节后,才逐渐转向更为高速的USB。另一个例子是我在东京工业大学研究室中接触的Quadlator I,该系统在我接手时2003年春节,使用Windows 98+ISA卡,利用Borland C++ 4.0提供的IO和中断。后来我在2004年春节为其开发了DA,IO和力传感器的WDM,之后才升级到Windows XP。因此,这一方法的问题是,随着老旧系统逐渐得不到支持,甚至无法购买到,以及新的应用软件在旧系统上无法运行,必将越来越被动。
  2. 改PC机控制为单片机控制。由于单片机越来越快,存储空间越来越大,逐渐覆盖越来越多的科研,学习需求。并且随着免费的C51编译器和PICC编译器的出现,使用C语言对单片机编程也不再是不可想象的事情(早期的单片机C编译器可是天价)像Basic Stamp这样的单片系统,还能够使用BASIC语言,更加降低了学习曲线的难度。另外一个促进就是仿真器的价格下降,在2000年后,1000元左右的仿真器已经在中关村流行。使用单片机+C语言的好处是,可以直接用几条简单的语句控制任何任何I/O,中断,定时器,串口等等。直观易学,学生几乎不必理解操作系统的复杂知识,更不用提WDM了。稍微刻苦一些的同学,可以在1个月内完全掌握。
    这一方法的缺点是什么呢?由于单片机本身仅仅是一个芯片,没有外围电路的话,什么都做不了。因此还必须指导学生开发外围的电路,这带来了一个新的学习领域和学习曲线。电路的知识,包括模拟电路和数字电路,分立器件等等,另外,抗干扰,降噪声,布线,调试等等。这对学生的软硬件能力都有要求,随着知识的分化越来越细,越来越专业,这样的学生也越来越少。有不少学生,能够漂亮的给机器人建模,编写运动控制程序,却根本不敢碰电路。
    [这一点,日本的经验的确值得我们借鉴,日本提供给广大爱好者一些通用外围电路的Kit,这些Kit上已经配有单片机,晶振,IO口,LED,跳线等,爱好者买回后,只要自己稍微改动,甚至可以不改动就直接连接在自己的机器人系统中。剩下要做的只有用C语言或者BASIC编程一件事情了,免去了大量的不必要的重复开发。]
  3. 转向Linux操作系统和嵌入式系统。相对于WDM的复杂,Linux下的驱动开发要相对容易一些。并且Linux免费,开源的优点也非常受大学圈的欢迎。于是有越来越多的PC控制选用Linux,我在东京工业大学的第一个研究室,就采用Linux控制机器蛇。当需要把控制器缩小,直接放入机器人内部的时候,嵌入式Linux也是一个非常有竞争力的选择。2002年底前后,我接触了FSMLab的RT-Linux,那时候3.1尚且全部免费,并且提供了非常好的实时性能。控制机器人再合适不过了。Titech wire系统在大学圈内逐渐流行起来,也客观上推动了RT-Linux在学生中的普及。

为什么说Linux的驱动比WDM易学呢?最重要的一点就是内核模式。在kernel space中,程序几乎可以作任何事情,访问硬件和物理内存等等。而把一个程序放到内核里也非常容易,提供init module和cleanup module两个接口就可以了。用户空间的数据和内核空间的数据交换也非常容易,都是1个函数就搞定的事情。编译也同其他Linux程序没有什么区别。


内核空间和用户空间大概可以表示如上图,几乎所有的应用程序运行于用户空间,而系统级的一些进程,运行于内核空间,包括调度器,系统调用等等,硬件驱动程序正好也运行于内核空间。用户空间的进程,和内核空间的进程被彼此分隔开来。他们之间通过一些系统调用进行数据交换,例如用户空间的程序调用open, close, write, read可以访问内核空间的驱动程序;反之,内核空间的驱动程序通过copy_from_user和copy_to_user和用户空间交换数据。

这里,有读者也许问,既然驱动硬件的程序在内核空间,那么要用户空间程序还有什么用呢?难道不能像单片机程序那样,内核空间的程序(驱动)一启动,就进入一个无限循环,完成所有功能,直到系统下电退出?是的,理论上完全可以这样做,但是这样做的话,就根本不需要提供一个嵌入式的操作系统。嵌入式的操作系统给我们带来的好处是,把高层算法和底层驱动分开,把用户界面和后台任务分开的选择。

为什么要分开呢?这里我们举一个例子:比如一个走迷宫的机器老鼠,迷宫由白色的线画出,老鼠是一个3轮电动小车;老鼠眼睛是一个巡线光传感器。我们可以把控制机器老鼠的程序分成两个部分,一个是根据白线绘制地图,利用深度优先搜索算法,探索迷宫的出路。注意记录一些位置,防止老鼠在环路上绕圈,而永远走不出去。这类算法的时间要求不高,可以从容计算。另一部分是对电动小车轮子的控制,包括直行,后退,转弯,刹车等,这类控制算法对时间要求很高,比如一旦没有及时转弯,老鼠就可能脱离白线(相当于火车脱轨)冲出去等等。如果我们把对时间要求不高的算法也放在内核空间,不但白白耗费大量的时间计算,还可能影响轮子控制程序,造成老鼠反应迟钝。

事实上,我们人的大脑和神经系统,也是这样控制我们的身体的。比如我们清晨锻炼时候跑步,我们的一边从容的欣赏清晨的街景,可以呼吸早上新鲜的空气,可以让思维临时开小差想别的东西。可是我们的小脑和运动神经却在高速运转控制我们的跑步的姿态,迈步、落地、蹬腿,这些动作稍微失控,我们都会摔倒。我们的大脑和神经系统,就是这样分工合作,从而控制我们的身体的。


那么RT-Linux和Linux之间是什么样的关系呢?

可以这样理解RT-Linux,它是一个独立的内核,独立于Linux内核而单独存在,事实上RT-Linux把Linux内核变成了一个子任务,可以和其他实时任务共同被调度。最后RT-Linux以一个内核模块的形式出现,可以被动态的装载和卸除。

为了方便开发人员,RT-Linux还提供了一组扩展的API,可以方便的控制时间、内核内部线程;并且在内核空间和用户空间之间交换数据。

在兼容性上,我遇到过一些实际的问题,都是在2003年时候的事情了。首先RT-Linux包内有cpp目录,看上去以为支持C++,并且还有相关的例子。但是实际上cpp的程序有些问题,并不是说都不能用,也许在老的Linux内核上可以,但是当时的2.4上不成。所以titech wire光盘中的驱动(C++写的),没有办法用,我花了1周的时间,把他们全改写成了C的。现在回想起来,那时候我真是以一当十啊。


我当初在东京工业大学松野研究室第一次学习RT-Linux是通过一个例子,之后我就帮助一个大四的毕业生写了摄像头跟踪驱动程序。例子是学习最好的途径之一,在这里我也用一个例子来作为本章的结尾:

这个例子是rtlinux包中的一个例子,名叫frank,我对其稍作改动,程序的功能如下,它在内核空间启动2个线程,一个500ms执行一次,一个200ms执行一次,每个线程打印不同的内容到系统message里面。用户空间的程序可以理解为一个UI,它运行后,会通知内核的这个模块启动。

首先,由于内核空间的模块和用户程序需要交换数据,所以他们需要定义一个共同的数据结构,这个数据结构定义在一个单独的头文件control.h中

#define COMMAND_FIFO 3 //用户程序向内核模块发送命令所用的FIFO号

#define START_TASK 1 //用户程序通知内核模块开始运行的命令
#define STOP_TASK 2 //用户程序通知内核模块停止运行的命令

struct my_msg_struct {
int command; //内核模块和用户程序彼此通讯时的命令,诸如:启动,停止。
int task; //内核模块中的线程标识
int period; //内核模块中线程的实时运行周期
};

这里需要解释一下FIFO的概念,学习过数据结构课程的话,应该对FIFO不陌生。FIFO就是(First In First Out)的缩写,这种类型的典型实现是一个队列。大家排队买票,早到早买早离开。RT-Linux中大量使用FIFO作为通讯的途径。原因还是出于实时的考虑,一组数据到来后,如果还没有来得及处理,就先放入FIFO的末尾,处理线程总是从FIFO的头部取出数据进行处理。FIFO不仅用户用户空间的程序和内核模块通讯,也用户内核模块中的线程间彼此的通讯。

之后就可以写一个内核模块了。内核模块没有任何复杂要求,除了要包含必要的头文件外,只要提供入口和出口就可以了。他们的定义如下:

int init_module(void){
//入口初始化...
}

void cleanup_module(void)
{
//模块终止时的释放代码...
}

在入口初始化代码中,思路是首先建立5个FIFO,其中3个FIFO用于和用户通信,这3个FIFO中,一个负责传达用户的命令到内核空间,另外两个负责交换内核的2个线程的数据到用户程序。5个FIFO中剩下的2个FIFO用于内核内部2个线程间数据的传递。这个工作完成后,下一步的初始化工作是启动2个独立的线程,然后这2个线程进入休眠状态,等待用户的指令唤醒。有关这部分的一些概念可以参考前面的第二章。最后一步是启动一个handler来监测用户发来命令的那个FIFO。一图胜千言,这些思路可以参考下面的示意图:

其中,粉红色的是命令FIFO通道,负责传达用户命令给内核模块,两个绿色的是内部FIFO通道,负责将用户命令handler发出的数据传送给内部的两个独立线程,两个独立线程的运行结果通过两个蓝色的FIFO通道传给用户程序显示。

把这个思路用代码写出来就是这个样子:

pthread_t tasks[2];

int init_module(void)
{
int c[5];
pthread_attr_t attr;
struct sched_param sched_param;
int ret;

//建立5个FIFO
c[0] = rtf_create(1, 4000);
c[1] = rtf_create(2, 4000);
c[2] = rtf_create(3, 200); /* input control channel */
c[3] = rtf_create(4, 100); /* input control channel */
c[4] = rtf_create(5, 100); /* input control channel */

//建立第一个独立线程
pthread_attr_init (&attr);
sched_param.sched_priority = 4;
pthread_attr_setschedparam (&attr, &sched_param);
ret = pthread_create (&tasks[0], &attr, thread_code, (void *)1);

//建立第二个独立线程
pthread_attr_init (&attr);
sched_param.sched_priority = 5;
pthread_attr_setschedparam (&attr, &sched_param);
ret = pthread_create (&tasks[1], &attr, thread_code, (void *)2);

//建立响应用户命令的handler,监听3号FIFO
rtf_create_handler(3, &my_handler);
return 0;
}

相对应的释放代码就简单多了:

void cleanup_module(void)
{
rtf_destroy(1);
rtf_destroy(2);
rtf_destroy(3);
rtf_destroy(4);
rtf_destroy(5);

pthread_cancel (tasks[0]);
pthread_join (tasks[0], NULL);
pthread_cancel (tasks[1]);
pthread_join (tasks[1], NULL);
}

独立线程共用一段代码:

static char *data[] = {"Frank ", "Zappa "};

#define TASK_CONTROL_FIFO_OFFSET 4

void *thread_code(void *t)
{
int fifo = (int) t;
int taskno = fifo - 1;
struct my_msg_struct msg;

while (1) {
int ret;
int err;
ret = pthread_wait_np();
if ((err = rtf_get (taskno + TASK_CONTROL_FIFO_OFFSET, &msg, sizeof(msg)))
== sizeof(msg)) {
rtl_printf("Task %d: executing the "%d" command to task %d; period %d ",
fifo - 1, msg.command, msg.task, msg.period);
switch (msg.command) {
case START_TASK:
pthread_make_periodic_np(pthread_self(), gethrtime(),
msg.period * 1000);
break;
case STOP_TASK:
pthread_suspend_np(pthread_self());
break;
default:
rtl_printf("RTLinux task: bad command ");
return 0;
}
}
rtf_put(fifo, data[fifo - 1], 6);
}
return 0;
}

这段代码是一个很典型的线程代码,函数一起动后,就进入线程循环,监听内部FIFO通道上的数据,如果通过rtf_get收到启动命令,就根据用户的要求,设置这段实时线程的周期(单位可以精确到ns)。如果受到结束命令,就挂起线程停止运行。其他情况下,程序出于实时循环状态,各自不断的利用rtf_put将frank和zippa两个字符串通过FIFO通道发送给用户程序。其中还会把一些额外的信息通过rtl_printf打印到系统的log里(在/var/log/messages)

那么这两个线程由谁控制呢?如上面的框图所示,handler用于控制这两个实时线程,handler的代码如下:

int my_handler(unsigned int fifo)
{
struct my_msg_struct msg;
int err;

while ((err = rtf_get(COMMAND_FIFO, &msg,sizeof(msg))) == sizeof(msg)) {
rtf_put (msg.task + TASK_CONTROL_FIFO_OFFSET, &msg, sizeof(msg));
rtl_printf("FIFO handler: sending the "%d" command to task %d; period %d ",
msg.command, msg.task, msg.period);
pthread_wakeup_np (tasks [msg.task]);
}

if (err != 0) {
return -EINVAL;
}

return 0;
}

这个handler同样是一旦运行起来,就不断监视FIFO命令通道,一旦得到用户的命令,就将命令直接传递给响应的实时线程,并且唤起正在休眠的线程。

内核模块还必须和用户程序配合才能工作frank的用户程序非常简单直观,由于内核实时线程发上来的数据都是字符串(frank和zippa),所以用户程序用一个buffer来接受数据:

#define BUFSIZE 70
char buf[BUFSIZE];

然后主程序的任务,就是打开3个FIFO,一个用户发送命令,2个用户接受数据: 

int main()
{
fd_set rfds;
struct timeval tv;
int retval;

int fd0, fd1, ctl; //3个FIFO的描述符,fd是file descriptor的缩写

int n;
int i;
struct my_msg_struct msg;

//打开第一个FIFO用于读数据
if ((fd0 = open("/dev/rtf1", O_RDONLY)) < 0) {
fprintf(stderr, "Error opening /dev/rtf1 ");
exit(1);
}

//打开第二个FIFO用于读数据
if ((fd1 = open("/dev/rtf2", O_RDONLY)) < 0) {
fprintf(stderr, "Error opening /dev/rtf2 ");
exit(1);
}

//打开第三个FIFO用于发送命令
if ((ctl = open("/dev/rtf3", O_WRONLY)) < 0) {
fprintf(stderr, "Error opening /dev/rtf3 ");
exit(1);
}

/* 发送命令到命令FIFO,来通知内核模块启动2个内核实时线程 */
msg.command = START_TASK;
msg.task = 0;
msg.period = 500000; //第一个实时线程的周期是500毫秒

if (write(ctl, &msg, sizeof(msg)) < 0) {
fprintf(stderr, "Can't send a command to RT-task ");
exit(1);
}

msg.task = 1;
msg.period = 200000; //第二个实时线程的周期是200毫秒
if (write(ctl, &msg, sizeof(msg)) < 0) {
fprintf(stderr, "Can't send a command to RT-task ");
exit(1);
}

//监听2个数据FIFO,显示内核模块发送上来的数据,总共显示100条后停止。
for (i = 0; i < 100; i++) {
FD_ZERO(&rfds);
FD_SET(fd0, &rfds); //将第一个数据FIFO加入监听列表
FD_SET(fd1, &rfds); //将第二个数据FIFO加入监听列表

tv.tv_sec = 1; //超时周期为1秒,如果1秒内没有从内核受到任何数据,则下面的select函数返回。
tv.tv_usec = 0;

retval = select(FD_SETSIZE, &rfds, NULL, NULL, &tv);
if (retval > 0) {
if (FD_ISSET(fd0, &rfds)) {    //第一个FIFO通道内有数据送了上来
n = read(fd0, buf, BUFSIZE - 1);
buf[n] = 0;
printf("FIFO 1: %s ", buf);
}

if (FD_ISSET(fd1, &rfds)) {    //第二个FIFO通道内有数据送了上来
n = read(fd1, buf, BUFSIZE - 1);
buf[n] = 0;
printf("FIFO 2: %s ", buf);
}
}
}

fprintf(stderr, "frank_app: now sending commands to stop RT-tasks ");

/* 通过命令FIFO发送停止命令给内核模块 */
//停止第一个实时线程
msg.command = STOP_TASK;
msg.task = 0;
if (write(ctl, &msg, sizeof(msg)) < 0) {
fprintf(stderr, "Can't send a command to RT-task ");
exit(1);
}

//停止第二个实时线程
msg.task = 1;
if (write(ctl, &msg, sizeof(msg)) < 0) {
fprintf(stderr, "Can't send a command to RT-task ");
exit(1);
}

return 0;
}

至此,RT-Linux的一个最简单程序就完成了,我想读者应该思考的问题是,如果不是这样简单的例子,而是机器人的控制程序,那么实时的PID循环应该放在哪里? 机器人的姿态命令如何通过FIFO传给内核模块?各个电机的位置,速度,转矩等物理量在测量后(通过DIO或者AD),如何通过FIFO传回给用户程序?怎样通过内核模块,用户程序,和各硬件组成大的闭环。

=====

回到总目录

返回ROBOTDIY主页




关键词: T.I.Tech     版本     RT-Linux     兼容性         

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]