免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 1077 | 回复: 0
打印 上一主题 下一主题

Quadlator II--RT-Linux内核驱动基础(ZT) [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2006-05-04 19:47 |只看该作者 |倒序浏览
Quadlator II--RT-Linux内核驱动基础
作者:baredog

RT-Linux内核驱动基础

2003年筹划写的时候,原先这一章命名为RT-Linux内核驱动基础。现在动笔的时 候,不禁有些踌躇。这么大的题目,并非我能力所及,难免让读者失望了。(在国内忽悠盛行的今天,深怕扣上 忽悠读者的帽子)。思考下来,说些下面的内容总还能有所帮助:
内核模块如何访问和驱动硬件
怎样把用户控制指令转换为最终对硬件的指令
怎样把各个硬件反馈回来的状态的数据等物理量通知用户
怎样规划控制程序的架构

-------------------------------------------------------------------------------------------------------------


首先要说明,总共有哪些硬件需要驱动,为了组成反馈控制的大闭环(相对于titech robot driver内部的小闭环)。需要以下一些组件:




通讯模块,用于各个模块和嵌入式系统的通信。
DIO模块, 用于数字量输入输出,可以用来直接驱动舵机或者读取光电码盘等等。
DA模块,用户数字量转换为模拟量输出,输出的电压可以用来进一步驱动电机驱动器。
AD模块, 模拟量转换为数字量,如果是通过电位器进行位置反馈的话,可将位置信息数字化后,输入嵌入系统。

Quadlator II使用了除AD模块以外的全部其他模块。因此需要编写对应各个模块的驱动程序。这里我觉得没有必要把每个驱动程序的所有细节都在这里说明,第一,硬件千 变万化,并且随着科技的进步还会不断更新;第二,本着介绍东西到国内的想法,如果讲一些更加通用的原则,会更有帮助。

如果面临这样多的模块,如何把他们组成一个反馈闭环呢?一般有两种思路:直接电缆连接和采用某种总线。直接电缆连接的好处是快速,但是要给每对被连接的模 块专门制作连接用的接口。最后造成系统上接口不够用,例如:

核心系统(titechwire或者单片机)<==>DA(假设多通道,可以驱动4个电机)
核心系统<==>DIO (假设多通道可以读取3个光电码盘)

那么一个12电机,12个光点码盘的机器人,共需要上面的接口12/4+12/3=7个接口。这些接口都是数字两,假设是8位数字量,那么几乎很少有核心 系统能提供这么多IO口出来(共计8*7=56个IO口)。

因此,针对上述问题,titechwire的思路是采用总线的方法。总线的思路可以用下面的图来说明:




一条蓝色的数据总线从核心系统出发,最后又回到核心系统形成一个环路。这样只需要消耗2x8=16个IO口。所有的模块都接入到这个总线上,每个模块自身 有一个唯一的编号。这样当核心系统需要控制第4个模块时,它向总线上发出一个数据包,数据包的开始位置,有模块表示号4,这个数据包出发后沿总线循环一圈 后返回。在这个过程中,每个模块都会接收到这个数据包,模块会对比自己的唯一地址和数据包头的地址是否一致。如果不一致就不做任何处理,如果一致,就会收 取这个数据包,然后根据包内的内容进行动作。这就好比邮递员沿街道送邮包,只有邮包上标记的门牌号和实际门牌号一致的时候,邮递员才会把邮包送给这家的主 人。

著名的I2C总线就是采用这一类型的方案,实现芯片间的通讯(可以参考ACM-R3机器蛇的相关文章)。titechwire为了增加 总线的容量,有使用了逻辑地址和物理地址分开的概念,将所有的模块组成一个树状结构:如下图:


每个树枝节点的构造如下:



物理地址和逻辑地址分开的概念,就好比电话分机。例如某个电话局的电话号码只有2位(当然这不大可能,中国很多二线城市的电话号码现在都是8位了)。那么 这个电话局最多能够安装00-99共100部电话,假设这100部电话都装好了。但是过了一阵时间,号码为25的用户,由于公司业务需要,要增加3部电 话,这怎么办呢?在电话局不升位的前提下,可以给这个用户安装总机-分机系统。用户要和这个公司的第2部电话联系时,需要拨打号码25(物理地址)然后转 拨2号分机(逻辑地址)。

这种方案本质上是一种串行化的思想,对比前面并行化的思路,缺点在于速度上受影响,优点在于节省接口资源,如果总线频率足够高的话,其优势就可以体现出 来。采用这种方案后,titechwire驱动模块的能力极大的提高,可以充分发挥嵌入式系统的计算能力,控制非常复杂的机械系统。

------------------------------------------------------------------------------------------------------------------

由于采用了上述结构,造成了titechwire的驱动程序中有相当部分的内容是处理数据包,其定义了Packet作为总线上数据包的 模型。然后又 专门写了一个驱动来控制总线上数据的收发。这些细节比较复杂,但是写出来对国内的读者的直接参考意义不大,考虑到参考一些I2C总线的介绍书籍即可获得概 念,因此略去。

内核模块中,拥有对所有端口的访问控制能力,可以直接IO。例如:


  1. #define IOWRITE16(port, value) outport(port, value)
  2. #define IOREAD16(port) inport(port)

  3. static void IOWRITE32(const unsigned short port, const unsigned long data){
  4. /* First, write the LSB (16 bits) */
  5. IOWRITE16(port , (unsigned short) data);

  6. /* Next, write the MSB (16 bits) */
  7. IOWRITE16((unsigned short)(port+2), (unsigned short) (data >> 16));
  8. }

  9. static unsigned long IOREAD32(const unsigned short port){
  10. unsigned long dataLSB, dataMSB;

  11. /* First, read the LSB data (16 bits) */
  12. dataLSB = IOREAD16(port );

  13. /* Next, read the MSB data (16 bits) */
  14. dataMSB = IOREAD16((unsigned short)(port+2));

  15. /* Finally, construct the 32 bits data */
  16. return ((0xffffL & dataLSB) + (0xffff0000L & (dataMSB << 16)));
  17. }
复制代码


这段代码十分基本,目的是读写32位数据,因为直接调用Linux的system call函数中的outport和inport是操控16位的,所以如果读写32位数据,就要注意高低字节的顺序。可以看到,在Linux的内核模块中, 对硬件IO端口的访问是非常简单直观的。这也是前面第四部分说阐述过的。

大量的硬件操作是通过对硬件上的寄存器进行读写实现的。通常的做法是向命令寄存器中写入命令后,硬件做相应的动作。硬件返回的内容,可以通过读状态寄存器 获得。例如:


  1. void TwIsaRegWrite(const unsigned long regAdr, const unsigned long regData){
  2. IOWRITE32(TWReg_IOPortAddr, regAdr );
  3. IOWRITE32(TWDat_IOPortAddr, regData);
  4. }

  5. void TwIsaRegRead(const unsigned long regAdr, unsigned long *regData){
  6. IOWRITE32(TWReg_IOPortAddr, regAdr );
  7. *regData = IOREAD32(TWDat_IOPortAddr);
  8. }
复制代码


有了这样方便操控硬件的能力,就可以进一步进行复杂组合,操纵模块了,下面是DA驱动的头文件:


  1. #include "TwIoModule.h"
  2. #include "TwPort.h"

  3. typedef struct TwDaModule{
  4. TwIoModule IoModule;
  5. }TwDaModule;

  6. void writeDaData(TwDaModule* pDAC, const int daCh, const unsigned long DAdata);
  7. void writeDaDataAll(TwDaModule* pDAC, const unsigned long DAdata[8]);
  8. void readGeneric8words16bitsData(TwDaModule* pDAC, unsigned long data[8]);

  9. void TwDaModuleCreate(TwDaModule* pDAC, TwPort* pTwPort, int HexSwValue);
  10. void TwDaModuleDestroy(TwDaModule* pDAC);

复制代码


虽然是ANSI C的,但是仍然使用了面向对象的思想对DA进行建模。前面讨论兼容性的时候,讨论过为什么用C而不是C++的原因。内核模块全部工作在内核空间,而 Linux内核本身就是C写成的(还有极少数汇编)。使用C开发内核程序相对来说,会最大的减小兼容性问题的影响。这个DA模块中,最核心的输出功能实现 如下:

  1. //
  2. // 输出数据为32位
  3. // daCh: DA通道号,2个通道对应一个节点
  4. //
  5. void writeDaData(TwDaModule* pDAC, const int daCh, const unsigned long DAdata){
  6. switch(daCh){
  7. case 0:
  8. case 1:
  9. payLoad(&(pDAC->IoModule.outData32bitLSB[0]), daCh, DAdata);
  10. break;
  11. case 2:
  12. case 3:
  13. payLoad(&(pDAC->IoModule.outData32bitMSB[0]), daCh, DAdata);
  14. break;
  15. case 4:
  16. case 5:
  17. payLoad(&(pDAC->IoModule.outData32bitLSB[1]), daCh, DAdata);
  18. break;
  19. case 6:
  20. case 7:
  21. payLoad(&(pDAC->IoModule.outData32bitMSB[1]), daCh, DAdata);
  22. break;
  23. }
  24. }
复制代码


DA的基本概念,这里不再给出,相信这方面的参考资料应该很丰富。其他模块,包括总线控制,光电码盘读入的DIO,这里都略去。更有参 考价值的是这 些模块组合在一起形成一个独立的电机驱动的逻辑模块。这个模块和第四部分作为例子给出的Frank模块非常相似。并且第四部分结束时提出了一些值得思考的 问题,这些问题的答案也一并在这里给出。

首先,上层的机器人控制程序需要和内核驱动模块建立一个通讯协议,在FIFO中将按照此协议传递数据。其定义如下:
  1. typedef struct RT_CMD{
  2. enum CMD_IDS id; //命令,可以为:ID_POWER_ON, ID_START, ID_STOP, ID_DRIVE
  3. long value; //内核实时线程的运行周期,单位为毫秒
  4. double data[4][3]; //data may be speed, position, or torque
  5. }RT_CMD;
复制代码


然后内核模块初始化,建立3个FIFO,一个handler,并启动一个线程:


  1. int init_module(void){
  2. rtl_printf("motor_mod.c: call init module..................... ");
  3. EXPORT_NO_SYMBOLS;

  4. //建立用户控制程序和handler通讯用的FIFO
  5. rtf_destroy(HANDLE_FIFO);
  6. rtf_create(HANDLE_FIFO, sizeof(RT_CMD));
  7. rtf_create_handler(HANDLE_FIFO, cmd_handler);

  8. //建立用户控制程序向内核模块发送命令的FIFO
  9. rtf_destroy(CMD_FIFO);
  10. rtf_create(CMD_FIFO, sizeof(RT_CMD));

  11. //建立内核模块向用户控制程序返回数据结果的FIFO
  12. rtf_destroy(RESULT_FIFO);
  13. rtf_create(RESULT_FIFO, sizeof(RET_DATA)*BUFF_SIZE);

  14. return pthread_create(&motor_ctrl_thread, NULL,
  15. (void*)main_routine, NULL);
  16. }
复制代码


handler的实现,几乎和第四部分介绍的frank相同,也在此略去。控制主线程实现如下:


  1. void* main_routine(void* arg){
  2. int err;
  3. RT_CMD cmd;

  4. while(1){
  5. pthread_wait_np(); //suspend the current thread
  6. //until the next period(np: non-portable)

  7. //get the fifo channel
  8. if((err=rtf_get(CMD_FIFO, &cmd, sizeof(cmd))) == sizeof(cmd)){
  9. switch(cmd.id){
  10. case ID_POWER_ON:
  11. motor_create(&motor);
  12. break;

  13. case ID_START:
  14. rtl_printf("start. ");
  15. pthread_make_periodic_np(pthread_self(), gethrtime(),
  16. cmd.value*1000*1000); //value in ms
  17. tm_start= gethrtime();
  18. ret_data.i_pos=0;
  19. break;

  20. case ID_STOP:
  21. motor_destroy(&motor);
  22. rtl_printf("stop. ");
  23. pthread_suspend_np(pthread_self());
  24. break;

  25. case ID_DRIVE:
  26. motor_drive(cmd.data);
  27. break;

  28. default:
  29. break;
  30. }
  31. } //end get cmd
  32. } //while
  33. return 0;
  34. }
复制代码


主线程从FIFO通道中读取用户下达的命令,如果为上电命令(power on),就初始化电机驱动模块,反之如果为停止命令(stop)就释放电机控制模块。并且还将上电和启动(start)的概念进行了区分,因此机器人的电 源打开,并不一定立即动作,而是所有初始化工作完毕后等待用户最终的启动命令。一旦收到启动命令,就以用户指定的实时周期为单位,不断唤醒线程。如果受到 用户发来的驱动命令(drive),就会调用电机的驱动函数motor_drive,其实现如下:


  1. static void motor_drive(double da_value[4][3]){
  2. tm_now=gethrtime();
  3. ret_data.t=(double)(tm_now-tm_start)/1e9;

  4. motor_measure(&motor, ret_data.theta);
  5. motor_output (&motor, da_value);

  6. rtf_put(RESULT_FIFO, &ret_data, sizeof(ret_data));
  7. }

复制代码


电机驱动的函数,接受4足12个关节的DA输出指令,并且记录当前的时间,这个时间可以精确道纳秒,此后程序立即测量所有12个关节的光电码盘,来测量位 置反馈。然后将输出指令写入DA模块,驱动电机转动,最后程序将所有测量出的反馈量作为结果,通过数据FIFO发送给用户程序。

至此,第五部分开头提到的一些内容,都蜻蜓点水般的提了一下,希望能够有所帮助。在下一部分,我将尝试介绍PID控制的一些内容。

=================================END============================
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP