嵌入式Linux下CAN总线驱动程序设计
2011-10-18王保和
王保和
(电子科技大学,四川 成都 611731)
嵌入式Linux下CAN总线驱动程序设计
王保和
(电子科技大学,四川 成都 611731)
嵌入式Linux下开发各类CAN总线设备,需设计相应的驱动程序,以CAN控制器MCP2510为例,详细介绍了CAN总线控制器硬件接口设计,以及嵌入式Linux下设备驱动程序的开发流程和技巧,结合CAN控制器的特点,设计相关的数据结构和操作代码来实现CAN控制器的设备驱动程序。
嵌入式Linux;CAN总线;S3C2440;设备驱动程序;MCP2510
(一)引言
CAN现场总线已经成为在仪表装置通信的新标准,其在短距离条件下具有高达1Mbps的数据传输能力,由于其成本低,实时性好,抗干扰能力强,因此广泛应用于车载数据采集系统及汽车电子控制网络。与此同时,利用 ARM处理器具有高性能耗能低等优点和嵌入式 linux的多任务处理的能力,在ARM和嵌入式Linux平台下开发CAN总线系统已经得到广泛应用。本文主要介绍如何在基于ARM和嵌入式linux平台下开发CAN总线控制器的驱动程序。
(二)CAN 控制器与S3C2440硬件接口设计
CAN节点主要有微控制器,CAN控制器,CAN收发器组成,在本设计中采用三星S3C2440作为微控制器,CAN控制器和收发器采用的是Microchip公司的MCP2510和MCP2551,S3C2440是一款ARM9TDMI内核的RISC处理器,CPU主频最高可达500M,处理速度快,可满足 CAN节点实时性高的需求,但由于其片内不带CAN控制器,所以硬件上需要外扩CAN控制器MCP2510,MCP2510完全支持CAN1.2、CAN2.0A、CAN2.0B等版本的协议,能够发送和接受标准和扩展报文,它还同时具备验收滤波以及报文管理功能,它与微控制器的通讯是采用SPI口实现的,其 SPI口数据传输速率高达 5Mb/s,同时高速 CAN收发器MCP2551把 CAN控制器生成的数字信号转化成为适合总线传输的差分信号,它也为CAN控制器和CAN总线的之间加入了缓冲器可以有效抑制高压尖峰信号,具有很强的抗噪特性。在本设计中S3C2440被设置为SPI的主设备,MCP2551作为从设备,GPG6引脚控制对从设备的操作,MISO是主设备输入从设备输出,MOSI是主设备输出从设备输入,在这里为了减少I/O口占用,MCP2510采用通用中断引脚与S3C2440的外部中断1相连,MCP2510与S3C2440硬件接口电路如图1所示:
图1 MCP2510与S3C2440硬件接口电路
(三)MCP2510的读写与状态控制操作
MCP2510是Microchip公司推出的功能很强的CAN控制器,该芯片包含三个发送缓存和两个接收缓存,可以对发送优先级进行管理,可滤除无用信息,其主要功能是在 MCU的控制下实现 CAN规范,其内部的所有寄存器都映射在一个地址表上,MCU通过 SPI口发送相应的命令和数据来完成对MCP2510的初始化,工作状态的控制以及数据的读写。在实现驱动程序之前需要完成对 MCP2510进行读写或控制的底层函数,而这些函数主要是根据 SPI口的通信时序完成 MCP2510的读写操作或状态控制如下:比如MCP2510的SPI口写时序如图2所示,可以按照此时序实现对MCP2510进行写操作的的底层函数。其他的操作以此类推。
图2 MCP2510的SPI口写时序
write2510(u8 cmd,u8 addr,u8 data)
{CS_L;//拉低GPG6引脚启动对MCP2510的操作
iowrite8(cmd,SPTDAT0);//写指令到 SPI 口
wait;//等待发送操作完成
iowrite8(addr,SPTDAT0);//写地址到SPI口
wait;//等待发送操作完成
iowrite8(data,SPTDAT0);//写数据到SPI口
wait;//等待发送操作完成
CS_H;//拉高GPG6引脚结束对MCP2510的操作
}
(四)CAN控制器驱动程序的接口
由于对MCP2510的控制是以字节为单位逐个进行I/O操作的设备,所以其属于字符设备,驱动程序是用户应用程序与硬件的接口,需要屏蔽设备的工作细节提供给用户程序一系列的标准调用,其主要就是调用操作 MCP2510的底层函数实现open,read,write,ioctl,release等系统调用函数,来完成与内核的通信,为了方便用户程序与驱动的交互,可以根据CAN控制器的工作特点定义MCP2510的设备结构体。MCP2510设备结构体的定义
struct MCP2510{
wait_queue_head_t read_wq;//读进程的等待队列
wait_queue_head_t write_wq;//写进程的等待队列
unsigned char en_read;//读进程等待的条件
unsigned char en_write;//写进程等待的条件
struct CANR_MSG rec_buf;//接收报文缓冲器
struct CANT_MSG tra_buf;//发送报文缓冲器
struct SET_FILTER set_filter; //MCP2510寄存器配置
struct cdev cdev; // 字符设备结构体
}
CANR_MSG结构体作为接收报文的缓冲区,CANR_MSG结构体作为发送报文的缓冲区,en_read是读进程等待队列的等待标志,en_write是写进程等待队列的等待标志,在驱动程序中运用这种机制可以很好地解决与应用程序通信的问题,定义 SET_FILTER结构体是为了用户应用程序可以配置MCP2510。
1.open函数的实现
open函数实现对S3C2440的SPI口的初始化,以及通过SPI口对MCP2510的寄存器进行相应的初始化。包括CAN总线波特率的设置,设置报文滤波以及屏蔽寄存器,开启中断使能等。
2.ioctl函数的实现
该函数在 linux中标准定义是 int (*ioctl)(struct inode* inode,struct file* filp,unsigned int cmd,unsigned long arg),用户程序可以通过传递参数给该函数实现对MCP2510的控制,其中形参cmd是命令,arg是命令的参数,cmd可以控制CAN控制器复位,进入各种模式,如配置,监听,正常,回环,睡眠等模式,还可以配置有关报文滤波的寄存器,因此可实现用户程序动态配置所需要接收的报文,其中arg是用户程序传递的一个指向SET_FILTER结构体的用户空间指针。
3.read函数的实现
当用户进程调用read函数读取接收的报文时,必须等待en_read这个变量变为 1,如果为 1的话,就会调用copy_to_user函数将储存在内核空间的报文数据(即设备结构体MCP2510里面的CANR_MESSAGE)拷贝到用户空间,否则该进程就会进入 read_wq等待队列,直到接收中断程序中唤醒它。
4.write函数的实现
当用户进程调用 write函数发送报文时,将同样需要等待 en_write变量,当发送完一帧报文后中断程序就会将en_write变量置位,这时将调用 copy_from_user 函数将用户空间的报文数据拷贝到内核空间的发送缓冲区(即设备结构体MCP2510里面的CANT_MESSAGE),然后将MCP2510中的发送缓冲控制寄存器(TXBnCTL)的TXREQ置位,启动报文发送请求。
5.release函数的实现
关闭设备,以及释放申请的中断号和分配的内存空间。
6.中断函数的实现
Linux内核将所有的中断统一编号,使用一个 irq_desc结构数组来描述这些中断;每个数组项对应一个中断号,里面记录了中断处理函数入口,底层的硬件访问接口,中断状态等,嵌入式 linux内核会维护一个中断信号线注册表,所以在使用中断前必须先申请中断号,使用完之后要释放该中断号,这里使用的就是函数 request_irq(IRQ_EINT1,can_interrupt,IRQT_FALLING,DEVICE_NAME,dev_id);来注册一个中断处理程序,IRQ_EINT1是所要申请的中断号,can_interrupt是中断处理函数的指针,IRQT_FALLING是中断触发的方式,这里选择的是下降沿触发,DEVICE_NAME是产生中断的设备名称,dev_id主要用于共享中断线,当一个中断处理程序需要释放时,内核可以根据该参数找到中断处理函数链表中的需要删除的中断处理程序, 由于 MCP2510的中断比较多,所以在中断处理程序需要处理各缓冲器的接收和发送中断以及唤醒和错误处理。中断函数代码片段如下:
icode=read2510(0x0e)&0x0e;//读取CANSTA寄存器
switch(icode)
{case 0x0c://接收缓冲器0中断
for(i=1;i<=13;i++)
{rbaddr=(icode<<3)+i;
temp=read2510(rbaddr);
*((unsigned char *)(&mcp2510->rbuf)+i-1)=temp;
}//拷贝接收到的报文ID和数据到设备结构体的缓冲区
mcp2510->en_read=1;//将读使能标志置1
modbits2510(0x2c,0x01,0x00); //清除中断标志
wake_up_interruptible(&mcp2510->read_wq);//唤醒正在等待的读进程
break;
case 0x0a://接收缓冲器1中断
…...
}
上述代码中icode保存的是从MCP2510的CANSTA寄存器中读取的中断信息,将icode值左移3位即可得到接收缓冲器0在CAN控制器中的寄存器映射地址,然后将接收到的报文ID和数据拷贝到设备结构体中的CANR_MSG类型缓冲器,然后唤醒读进程,用户应用程序就可以得到底层硬件接收到的报文数据。其他的中断都可以按这种方法实现。
(五)模块化驱动程序的注册
Linux驱动程序的开发有两种实现方式,一种是直接将相关的驱动程序编译进内核,但这样势必会造成内核非常庞大,而且在开发中非常不方便,每次得重新烧写内核镜像。另外一种就是将具体的设备驱动程序独立编译,成为可安装的模块,可以动态地加载和卸载模块。本文采用后一种方式,因此需要实现 init_module和 cleanup_module函数,init_module函数会调用 register_chrdev函数来申请设备号,同时动态地给设备结构体分配空间,而cleanup_module函数则实现在卸载模块时的释放内存空间,注销主设备号,以及释放中断号等。
(六)驱动程序的编译
在编写完嵌入式 linux驱动程序后,需要将驱动程序添加到内核中,然后编译内核,具体步骤如下:
1.将驱动程序 mcp2510.c文件拷贝到内核的drivers/char目录下,该目录下是字符设备的设备驱动程序。
2.修改drivers/char目录下的Kconfig文件,添加如下脚本:
config MCP2510
tristate 〝CAN controler driver〞
default m
help
XXXXXXXX
添加这段脚本意味着在配置内核时,在字符设备驱动的配置菜单中就会有相应的 MCP2510这个选项,这个驱动模块可以配置为编译进内核,编译为模块,要么不编译,在这里默认的是编译为模块,“help”后面的内容为帮助信息。在内核顶层目录下运行make menuconfig 后会生成.config文件,该文件决定文件是否编译进内核或模块。
3.修改drivers/char目录下的Makefile文件,在其中添加obj-$(CONFIG_MCP2510)=
MCP2510.o,在这里CONFIG_MCP2510的变量值为m,表示该文件要作为模块编译,可以在第二步生成的.config文件中找到该变量的赋值。
4.在内核源代码顶层目录下运行make modules命令,就会在drivers/char目录下生成MCP2510.ko文件。
(七)驱动程序模块的加载与测试
现在将驱动程序MCP2510.ko和用户测试程序从PC交叉编译平台拷贝到arm+linux平台下,也可以通过NFS服务挂载宿主机下的目录,这样更易于调试,运行 insmod MCP2510.ko这时可以查看/proc/devices文件可以看到加载的设备信息,查看/proc/interrupts文件可以看到申请的中断号信息,当这些都没问题时就可以运行用户测试程序./mcp2510_test.o。
(八)结 语
本文介绍了基于ARM平台的CAN总线设备的硬件接口设计以及在嵌入式linux系统下开发CAN控制器的驱动程序的开发流程,结合 CAN控制器的工作特点,合理地设计了数据结构以及使用了等待队列的机制和使用缓冲区的控制方法。经过应用程序测试证明该驱动可以正确运行。同时也给在嵌入式Linux操作系统下开发类似的接口驱动程序提供了参考。
[1] 王黎明,夏立,邵英,等.CAN 现场总线系统的设计与应用[M].北京:电子工业出版社,2008.
[2] 宋宝华.Linux设备驱动开发详解[M].北京:人民邮电出版社,2008.
[3] 饶运涛,王进宏,等.现场总线 CAN 原理与应用技术[M].北京:北京航空航天大学出版社,2007.
[4] 郑灵翔.嵌入式接口技术与 Linux驱动开发[M].北京:北京航空航天大学出版社,2010.
TP336
A
1008-1151(2011)06-0019-02
2011-04-26
王保和,电子科技大学硕士研究生,研究方向为嵌入式系统。