- 论坛徽章:
- 0
|
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。例如:
- #define IOWRITE16(port, value) outport(port, value)
- #define IOREAD16(port) inport(port)
- static void IOWRITE32(const unsigned short port, const unsigned long data){
- /* First, write the LSB (16 bits) */
- IOWRITE16(port , (unsigned short) data);
- /* Next, write the MSB (16 bits) */
- IOWRITE16((unsigned short)(port+2), (unsigned short) (data >> 16));
- }
- static unsigned long IOREAD32(const unsigned short port){
- unsigned long dataLSB, dataMSB;
- /* First, read the LSB data (16 bits) */
- dataLSB = IOREAD16(port );
- /* Next, read the MSB data (16 bits) */
- dataMSB = IOREAD16((unsigned short)(port+2));
- /* Finally, construct the 32 bits data */
- return ((0xffffL & dataLSB) + (0xffff0000L & (dataMSB << 16)));
- }
复制代码
这段代码十分基本,目的是读写32位数据,因为直接调用Linux的system call函数中的outport和inport是操控16位的,所以如果读写32位数据,就要注意高低字节的顺序。可以看到,在Linux的内核模块中, 对硬件IO端口的访问是非常简单直观的。这也是前面第四部分说阐述过的。
大量的硬件操作是通过对硬件上的寄存器进行读写实现的。通常的做法是向命令寄存器中写入命令后,硬件做相应的动作。硬件返回的内容,可以通过读状态寄存器 获得。例如:
- void TwIsaRegWrite(const unsigned long regAdr, const unsigned long regData){
- IOWRITE32(TWReg_IOPortAddr, regAdr );
- IOWRITE32(TWDat_IOPortAddr, regData);
- }
- void TwIsaRegRead(const unsigned long regAdr, unsigned long *regData){
- IOWRITE32(TWReg_IOPortAddr, regAdr );
- *regData = IOREAD32(TWDat_IOPortAddr);
- }
复制代码
有了这样方便操控硬件的能力,就可以进一步进行复杂组合,操纵模块了,下面是DA驱动的头文件:
- #include "TwIoModule.h"
- #include "TwPort.h"
- typedef struct TwDaModule{
- TwIoModule IoModule;
- }TwDaModule;
- void writeDaData(TwDaModule* pDAC, const int daCh, const unsigned long DAdata);
- void writeDaDataAll(TwDaModule* pDAC, const unsigned long DAdata[8]);
- void readGeneric8words16bitsData(TwDaModule* pDAC, unsigned long data[8]);
- void TwDaModuleCreate(TwDaModule* pDAC, TwPort* pTwPort, int HexSwValue);
- void TwDaModuleDestroy(TwDaModule* pDAC);
复制代码
虽然是ANSI C的,但是仍然使用了面向对象的思想对DA进行建模。前面讨论兼容性的时候,讨论过为什么用C而不是C++的原因。内核模块全部工作在内核空间,而 Linux内核本身就是C写成的(还有极少数汇编)。使用C开发内核程序相对来说,会最大的减小兼容性问题的影响。这个DA模块中,最核心的输出功能实现 如下:
- //
- // 输出数据为32位
- // daCh: DA通道号,2个通道对应一个节点
- //
- void writeDaData(TwDaModule* pDAC, const int daCh, const unsigned long DAdata){
- switch(daCh){
- case 0:
- case 1:
- payLoad(&(pDAC->IoModule.outData32bitLSB[0]), daCh, DAdata);
- break;
- case 2:
- case 3:
- payLoad(&(pDAC->IoModule.outData32bitMSB[0]), daCh, DAdata);
- break;
- case 4:
- case 5:
- payLoad(&(pDAC->IoModule.outData32bitLSB[1]), daCh, DAdata);
- break;
- case 6:
- case 7:
- payLoad(&(pDAC->IoModule.outData32bitMSB[1]), daCh, DAdata);
- break;
- }
- }
复制代码
DA的基本概念,这里不再给出,相信这方面的参考资料应该很丰富。其他模块,包括总线控制,光电码盘读入的DIO,这里都略去。更有参 考价值的是这 些模块组合在一起形成一个独立的电机驱动的逻辑模块。这个模块和第四部分作为例子给出的Frank模块非常相似。并且第四部分结束时提出了一些值得思考的 问题,这些问题的答案也一并在这里给出。
首先,上层的机器人控制程序需要和内核驱动模块建立一个通讯协议,在FIFO中将按照此协议传递数据。其定义如下:
- typedef struct RT_CMD{
- enum CMD_IDS id; //命令,可以为:ID_POWER_ON, ID_START, ID_STOP, ID_DRIVE
- long value; //内核实时线程的运行周期,单位为毫秒
- double data[4][3]; //data may be speed, position, or torque
- }RT_CMD;
复制代码
然后内核模块初始化,建立3个FIFO,一个handler,并启动一个线程:
- int init_module(void){
- rtl_printf("motor_mod.c: call init module..................... ");
- EXPORT_NO_SYMBOLS;
- //建立用户控制程序和handler通讯用的FIFO
- rtf_destroy(HANDLE_FIFO);
- rtf_create(HANDLE_FIFO, sizeof(RT_CMD));
- rtf_create_handler(HANDLE_FIFO, cmd_handler);
- //建立用户控制程序向内核模块发送命令的FIFO
- rtf_destroy(CMD_FIFO);
- rtf_create(CMD_FIFO, sizeof(RT_CMD));
- //建立内核模块向用户控制程序返回数据结果的FIFO
- rtf_destroy(RESULT_FIFO);
- rtf_create(RESULT_FIFO, sizeof(RET_DATA)*BUFF_SIZE);
- return pthread_create(&motor_ctrl_thread, NULL,
- (void*)main_routine, NULL);
- }
复制代码
handler的实现,几乎和第四部分介绍的frank相同,也在此略去。控制主线程实现如下:
- void* main_routine(void* arg){
- int err;
- RT_CMD cmd;
- while(1){
- pthread_wait_np(); //suspend the current thread
- //until the next period(np: non-portable)
- //get the fifo channel
- if((err=rtf_get(CMD_FIFO, &cmd, sizeof(cmd))) == sizeof(cmd)){
- switch(cmd.id){
- case ID_POWER_ON:
- motor_create(&motor);
- break;
- case ID_START:
- rtl_printf("start. ");
- pthread_make_periodic_np(pthread_self(), gethrtime(),
- cmd.value*1000*1000); //value in ms
- tm_start= gethrtime();
- ret_data.i_pos=0;
- break;
- case ID_STOP:
- motor_destroy(&motor);
- rtl_printf("stop. ");
- pthread_suspend_np(pthread_self());
- break;
- case ID_DRIVE:
- motor_drive(cmd.data);
- break;
- default:
- break;
- }
- } //end get cmd
- } //while
- return 0;
- }
复制代码
主线程从FIFO通道中读取用户下达的命令,如果为上电命令(power on),就初始化电机驱动模块,反之如果为停止命令(stop)就释放电机控制模块。并且还将上电和启动(start)的概念进行了区分,因此机器人的电 源打开,并不一定立即动作,而是所有初始化工作完毕后等待用户最终的启动命令。一旦收到启动命令,就以用户指定的实时周期为单位,不断唤醒线程。如果受到 用户发来的驱动命令(drive),就会调用电机的驱动函数motor_drive,其实现如下:
- static void motor_drive(double da_value[4][3]){
- tm_now=gethrtime();
- ret_data.t=(double)(tm_now-tm_start)/1e9;
- motor_measure(&motor, ret_data.theta);
- motor_output (&motor, da_value);
- rtf_put(RESULT_FIFO, &ret_data, sizeof(ret_data));
- }
复制代码
电机驱动的函数,接受4足12个关节的DA输出指令,并且记录当前的时间,这个时间可以精确道纳秒,此后程序立即测量所有12个关节的光电码盘,来测量位 置反馈。然后将输出指令写入DA模块,驱动电机转动,最后程序将所有测量出的反馈量作为结果,通过数据FIFO发送给用户程序。
至此,第五部分开头提到的一些内容,都蜻蜓点水般的提了一下,希望能够有所帮助。在下一部分,我将尝试介绍PID控制的一些内容。
=================================END============================ |
|