关键字:uClinux;嵌入式操作系统;CAN总线;设备驱动程序
1、uClinux操作系统概述
uClinux是Linux2.0的一个分支,它被设计用于没有MMU的微控制器领域,即被广泛应用于嵌入式Linux领域。uClinux的最大特征就是没有MMU(内存管理单元模块)。它很适合那些没有MMU的处理器,如ARM7TDMI,m68ez328等。
uClinux具有完全的TCP/IP协议栈,同时对其他许多的网络协议都提供支持。这些网络协议都在uClinux上得到了很好的实现。uClinux可以称作是一个针对嵌入式系统的优秀网络操作系统。
2、Linux驱动程序设计概述
Linux系统内核通过设备驱动程序与外围设备交互,设备驱动程序是Linux内核的一部分,它是一组数据结构和函数,这些数据结构和函数通过定义的接口控制一个或多个设备。
和UNIX一样,Linux中所有的设备均作为文件来对待,这些文件一般称为特殊文件,这样做的一个好处是使用户或应用程序可按操纵普通文件的方式进行访问控制硬件设备。
Linux内核有三种类型的设备驱动程序:字符设备驱动程序、块设备驱动程序和网络设备驱动程序。Linux的设备由一个主设备号(major)和一个次设备号(minor)标识。主设备号唯一标识了设备类型,它是块设备表或字符设备表中设备表的索引。次设备号仅由设备驱动程序解释,用于识别同类设备中,I/O请求所涉及到的那个设备。设备驱动程序可以分为3个主要组成部分:
(l)自动配置和初始化子程序,负责检测所要驱动的硬件设备是否能正常工作。
(2)服务于I/O请求的子程序,又称为驱动程序的上半部分。
(3)中断服务子程序,又称为驱动程序的下半部分。
3、uClinux下CAN设备的驱动程序编写
根据上文对LINUX下设备驱动程序的描述,以及参考相关的实例分析,下面对CAN总线设备SJA1000的驱动程序进行编写。
CAN设备驱动程序实际上是linux内核直接对sja1000器件的初始化与读写操作。经分析,sja1000 CAN驱动程序构成包括如下几个部分:
1)定义sja1000芯片内所有寄存器的访问地址,用于完成对其内部寄存器以及缓冲区的读写访问。例如:
#define IO_PMOD (*(volatile unsigned *)0x3ff5000)
#define IO_PDATA (*(volatile unsigned *)0x3ff5008)
#define IO_PCON (*(volatile unsigned *)0x3ff5004)
#define SJA_MOD (0x2700000) #define SJA_CMR (0x2700004)
…………………
#define SJA_CANRXB7 (0x270006c) #define SJA_CANRXB8 (0x2700070)
因为在我们的系统中,对sja1000的读写是采用的部分模拟时序的方式,所以用到了S3C4510的IO端口。下面对sja1000地址的定义进行分析。因为uClinux运行的时候,采用的是32位方式,即两个相邻地址间相隔4个字节,而在sja1000内部的地址间的间隔只有1个字节。虽然可以对S3C4510的内部寄存器定义为在访问sja1000的时候,将位宽度定义为8位,但这样会与linux系统运行不匹配,经测试发现读写不正常。所以将sja1000的地址定义为32位宽度。于是各个寄存器地址为(基址+sja1000内部地址×4)。这里将sja1000的基址定义为0x2700000。
2)编写对SJA1000内部寄存器访问的读写函数
因为S3C4510B处理器的地址和数据总线是分开的,而SJA1000的地址与数据总线是8位分时复用的。所以我们只有采用先向sja1000的8位地址数据总线上送出地址,然后再送数据或者读数据的方式。片选信号/CS,读信号/RD,写信号/WR仍由S3C4510B自己产生。需要模拟的是锁存信号ALE、地址数据总线AD0-AD7。参照sja1000时序图,具体的操作步骤见下面程序和注释。
写子程序如下:
void sja_write(unsigned int data, unsigned int addr)
{ unsigned char tmp;
tmp=(addr)>>2;//将32位地址右移2位,tmp的低8位即为sja1000实际地址。
outl(tmp,addr);//将地址信息作为数据送往SJA1000数据总线
IO_PDATA=0x32;//ALE=0,让SJA1000将该地址锁存
outl(data,addr);//将数据信息送往SJA1000数据总线
O_PDATA=0x33; } //将ALE置高电平,74HC245的/OE置高位
读子程序如下:
unsigned char sja_read(unsigned int addr)
{ unsigned char data;
volatile unsigned int data1;
unsigned char tmp;
tmp=(addr)>>2; //将32位地址右移2位,tmp的低8位即为sja1000实际地址S3C2410
outl(tmp,addr); //将地址信息作为数据送往SJA1000数据总线
IO_PDATA=0x32; //p0-ALE=0,锁存地址信息
IO_PDATA=0x12; //p5-245dir=0,将74HC245的方向置为CPU输入方向
data1=inl(addr); //读出所需的数据
IO_PDATA=0x33; //ALE置高,74HC245置为不工作状态
data=data1; return(data); }//返回数据
后面对sja1000的初始化、CAN发送与CAN接收函数中需要对寄存器操作均调用sja_write()和sja_read()函数进行。
3)定义驱动程序的文件结构
在LINUX系统中,对硬件设备的访问也是被当作文件来操作的。这里定义的文件接口将可以在外部的应用程序中被调用。在CAN驱动程序中,只定义了读CAN信息(CAN接收)、向CAN节点写信息(CAN发送)、打开CAN设备、关闭CAN设备等4个文件接口。定义信息如下面的程序所示。在sja1000_fops中所定义的函数都必须在驱动程序中编写。
static struct file_operations sja1000_fops = {
read: sja1000_read, //CAN接收数据
write: sja1000_write,//CAN发送
open: sja1000_open, //打开设备S3C2410 开发板II(B)+3.5寸带触摸TFT液晶屏
release: sja1000_release, };//关闭设备
4)定义sja1000_write :CAN发送函数(写函数)
static int sja1000_write(struct file *filp, const char *buf, size_t size,
loff_t *offp){ }
在CAN总线控制器Sja1000初始化完成后,即可设置CAN发送,具体对sja1000寄存器的相关操作的相关程序可参阅SJA1000器件的数据手册。
5)定义sja1000_read :CAN接收函数(读函数)
static int sja1000_read(struct file *filp, char *buf, size_t size,
loff_t *offp) { }
该函数完成对CAN总线网络上相应信息的接收。在本系统中CAN接收采用的是查询方式。
6)编写sja1000_open:文件打开函数
static int sja1000_open(struct inode *inode,struct file *file) { }S3C2410 开发板II(B)+3.5寸带触摸TFT液晶屏
进程调用该函数表示对设备的占用。如果返回为-1,表示设备已被其他进程占用,打开非法。如果采用中断方式,对中断的注册也可放在本函数中。
7)编写sja1000_release:文件关闭函数
static int sja1000_release(struct inode *inode, struct file *file) { }
该函数进程完成对设备占有权的释放,释放后,其他的进程就可以访问这个设备了。
8)编写sja1000_init(void):void sja1000_init(void) { } CAN设备初始化函数
该函数完成设备在LINUX内核中的登记。并完成对sja1000初始化。
Sja1000寄存器配置通过调用上面已写好的sja_write()函数完成。驱动函数登记我们采用的是静态加载的方式,通过调用register_chrdev()完成,程序如下:
if(result = register_chrdev(254,"sja1000",&sja1000_fops)) S3C2410 开发板II(B)+3.5寸带触摸TFT液晶屏
printk("S3C4510-sja1000: Error %d registering device sja1000\n", result);
其中,254是为sja1000设备分配的主设备号,“sja1000”是显示在/dev中的设备名,sja1000_fops为对应的文件系统指针。返回值小于0表示失败,大于或等于0表示成功。
9)将驱动程序加到uClinux内核中
当驱动程序sja1000.c编写完成后,下面的工作就是将它加到uClinux内核中了。这需要修改uClinux的源代码,然后重新编译uClinux内核。
①将设备驱动程序文件sja1000.c复制到/uClinux-dist/linux/drivers/char目录下。该目录保存了uClinux字符设备的设备驱动程序。修改该目录下mem.c文件,在Init chrdev_init()函数中增加如下代码:
#ifdef CONFIG_SJA1000_DRIVER device_init() #endifS3C2410 开发板II(B)+3.5寸带触摸TFT液晶屏
其中CONFIG_SJA1000_DRIVER是在配置uClinux内核时赋值的。
②在uClinux/linux/drivers/char目录下 Makefile中增加如下代码:
ifeq($(CONFIG_SJA1000_DRIVER,y) L_OBJS+=sja1000.c endif
如果在配置uClinux内核的时候选择了支持我们定义的设备,则在编译内核的时候会编译sja1000.c,生成 sja1000.o文件。
③修改 /uClinux-dist/linux/arch/m68knonunu目录下 config.in文件,在 comment' Character devices’语句下面加上
bool 'support for sja1000 driver'CONFIG_SJA1000_DRIVERS3C2410 开发板II(B)+3.5寸带触摸TFT液晶屏
这样,在编译内核,运行make menuconfig的时候,且在配置字符设备时就会有选项:
support for sja1000 driver 当选中这个选项的时候,设备驱动就加到内核中了。
④在romfs中加上设备驱动程序对应的设备文件。设备文件都被包含在/dev目录下。uClinux中使用的根文件系统是romfs文件系统。这个文件系统是一个只读文件系统,所以设备文件必须在编译内核的时候加到romfs文件系统的image中。
不同的硬件系统对应不同的设备文件,在/uClinux-dist/vendors目录下,分别定义了它们的Makefile文件。在uClinux-dist/Vendors里S3C4510对应的目录下找到它的Makefile文件,并找到区域DEVICES=\ tty,c,5,0 console,c,5,1 cua0,c,5,64 cual,c,5,65\,在后面再加上设备项 sja1000,c,254,1\就行了。
③重新编译内核;在shell中将当前目录cd到uClinux-dist目录下,然后:S3C2410 开发板II(B)+3.5寸带触摸TFT液晶屏
#make menuconfig #make dep #make
当驱动程序和uClinux内核一起编译链接并生成映像下载到目标板运行以后,可以通过查看/proc/devices,如果已经显示有sja1000,则表明设备加载成功。如果使用了中断,也可以查看/proc/interrupts,该文件记录了当时已经完成的所有系统中断情况。
4、结束语
实时性方面,可以使用进程间通信如管道、消息队列、共享内存等方法将CAN总线的接收中断与应用程序直接关联,加快系统对CAN总线事件的响应速度。进一步可以通过RTLinux和RTAI(Real Time Application Interface)这两种方案增强uClinux的实时性。
本文作者创新点:有效地解决了在没有MMU的CPU之上开发一些简单任务操作系统或控制程序效率低、程序简单的缺点。通过使用嵌入式uClinux,它既保存了原先Linux操作系统稳定性、功能强大等优点,又对内核的代码重新编写,减少了内核容量,提高了效率。同时也提出了在CAN总线设备下设计驱动程序的方法。
5、参考文献
[1] 白小明,邱桃荣.基于Linux的嵌入式实时操作系统的研究 [J].微计算机信息,2006,5-22:78-79。http://www.51kaifa.com/shop/read.php?ID=4812
[2] 魏长江,张凌云,李国财.基于uClinux的设备驱动程序设计方法及应用实例[J].煤矿机械,2005,8
[3] 蔡莉,卢珞先.RS-485通信与CAN总线的接口设计[J].武汉理工大学学报(信息与管理工程版), 2002,1
[4] 胡晨峰.JFFS2文件系统在uClinux中的应用[J].电子产品世界,2003,7 http://www.51kaifa.com/shop/read.php?ID=4812