该文章主要目的为了学习并掌握以下几个方面:
嵌入式系统由硬件、驱动、操作系统、应用,这几部分层次构成。其中,驱动程序是硬件层与系统层之间的交互层,主要作用是操作底层硬件,实现硬件控制,而应用层位于操作系统层之上,应用层以操作系统为中介,对驱动层的相关主要函数进行调用(如open(),read(),write()等函数),进而实现对硬件控制。
通常,驱动程序是编译进操作系统内核中的,与内核融为一体,区别在于,驱动程序可以以模块化的形式动态编译进内核中,相当于拼图一样,灵活可拆卸。操作系统可以理解为庞大的函数库,提供驱动和应用程序的调用。
应用程序的存放在文件系统中,文件系统可以理解为一个目录系统,由很多目录组成,而这些应用程序就可以存放在某目录下某文件中(相当于Windows电脑下各个盘符和目录一样,存在各个应用程序和文件),因此应用程序可以方便被查找和运行。
(以上均为个人理解o( ̄︶ ̄)o,有不足之处请指教哦。下面正式进入实操)
编写驱动程序和测试程序,实现:开发板任意2个按键按下,3个led灯反转,并在shell终端显示开发板运行的相关信息。
前期条件:开发板烧录好u-boot,linux内核,文件系统
芯片:S3C440
开发板:韦老大的JZ2440
引脚说明 KEY1:GPP0,KEY2:GPG3 LED1:GPP4,LED2:GPP5,LED3:GPP6
本实验源码下载链接:”点击此处”
驱动程序很多中,本文章以字符驱动程序为入门。学完驱动程序后,就感觉还算蛮简单,主要是熟悉它的整体结构形式,以及底层寄存器的控制。下面为编写流程:
1. 新建以驱动源码文件,名为:drv_keyled.c ,然后找好一个已经写好的字符驱动源码,作为模板参考。
2. 将需要的头文件写进来,可以直接从模板中copy过来(这些头文件就是系统内核的源码库,驱动程序会从中调用),头文件如下:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
3.定义需要用的寄存器名称等
typedef unsigned long Uint32;
volatile unsigned long *gpiog_con=NULL; //寄存器
volatile unsigned long *gpiog_data=NULL; //寄存器
volatile unsigned long *gpiof_con=NULL; //寄存器
volatile unsigned long *gpiof_data=NULL;//寄存器
int major;//主设备号
//类和设备类,可以帮助用于自动创建设备文件
static struct class* drv_keyled_class; //定义类
static struct class_device* drv_keyled_class_device; //定义一类设备
4.绑定驱动有关的重要函数到内核中。关联绑定后,使得应用层(用户层)调用open(),read(),write()等函数时,系统能找到驱动对应的xxx_open(),xxx_read(),xxx_write()函数,代码如下:
//关键函数绑定(结构体)
static struct file_operations drv_key_fops=
{
.owner = THIS_MODULE,
.write = drv_keyled_write,
.read = drv_keyled_read,
.open = drv_keyled_open,
};
//初始化、卸载函数绑定(宏)
module_init(drv_keyled_init);
module_exit(drv_keyled_exit);
5.关键驱动函数编写——初始化和卸载函数(即drv_keyled_init,drv_keyled_exit)。
初始化函数主要实现功能如下几点:(卸载函数与初始化函数相反,略述)
- 向内核注册驱动,即将结构体绑定的关键函数告诉内核,同时从内核中获取主设备号:major,主设备号用于应用程序对具体哪个驱动的识别。
- 创建类和类设备,运行后,系统能自动创建设备文件:/dev/keyled,相当于应用层上手动完成“mknod“命令操作。
- 物理地址映射到虚拟地址VA(ioremap函数),由于系统开启了MMU,因此程序都是在虚拟地址运行,如果要完成指定寄存器的控制,就需要将该寄存器的物理地址进行映射,通过虚拟地址完成寄存器控制。
初始化和卸载函数调用方法:
- 当应用层运行命令“insmod”进行驱动装载时,系统自动调用初始化函数。
- 当应用层运行命令“rmmod”进行驱动卸载时,系统自动调用卸载函数。
源码如下:
/*
函数名:drv_keyled_init
功能:初始化模块功能(insmod装载驱动时调用)
*/
static int drv_keyled_init(void)
{
//注册主设备号,由fops结构体告诉内核绑定的函数
major = register_chrdev(0, "drv_keyled", &drv_key_fops);
//创建类
drv_keyled_class = class_create(THIS_MODULE, "drv_keyled");
//创建类设备
drv_keyled_class_device = class_device_create(drv_keyled_class, NULL, MKDEV(major, 0), NULL, "keyled");// "/dev/keyled"
//虚拟地址VA映射
gpiof_con = (unsigned long*)ioremap(0x56000050, 12); //映射物理地址的起始地址与长度,返回虚拟地址
gpiof_data = gpiof_con+1;//地址在类型长度上加1(即+4地址)
gpiog_con = (unsigned long*)ioremap(0x56000060, 12); //映射物理地址的起始地址与长度,返回虚拟地址
gpiog_data = gpiog_con+1;
return 0;
}
/*
函数名:drv_keyled_exit
功能:卸载模块功能(rmmod卸载驱动时调用)
*/
static void drv_keyled_exit(void)
{
unregister_chrdev(major, "drv_keyled");
class_device_unregister(drv_keyled_class_device);
class_destroy(drv_keyled_class);
iounmap(gpiof_con);
iounmap(gpiog_con);
}
6.关键驱动函数编写—–xxx_open(),xxx_read()等函数(即drv_keyled_open,drv_keyled_open等)。
该部分的编写是编写驱动源码的核心内容,实现了对寄存器配置与控制操作。当应用层调用open(),read(),系统就得调用这些对应的函数,因此,这些函数里的内容以及要实现什么样的功能,由用户自行发挥。该实验将这些函数定义成如下功能:
- drv_keyled_open(): 实现按键和LED的引脚配置与初始化操作。
- drv_keyled_read(): 实现对按键电平状态的读取。
- drv_keyled_write(): 实现对LED灯的亮灭控制。 (注:实验只用到了open,read,write三个常用函数,系统其实还提供了很多其他函数,可以见file_operations结构体里的内容)
源码如下:
/************驱动关键函数实现************/
/*
函数名:drv_keyled_open
功能:配置引脚功能
*/
int drv_keyled_open(struct inode *inode, struct file *file)
{
char key_dat=1;
printk("drv_keyled_open2\n");
//GPF4.5.6
*gpiof_con |= (1<<2*4) | (1<<2*5) | (1<<2*6) ; //led 输出
*gpiof_data &= ((~(1<<4)) & (~(1<<5)) & (~(0<<6))); //初始化2个点亮
//GPG3,GPP0,GPP2
*gpiog_con |= (0x3<<2*11) ; //按键
return 0;
}
/*
函数名:drv_keyled_write
功能:控制led亮灭,实现反转
*/
static ssize_t drv_keyled_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
unsigned char key_data[2]={1,1};
Uint32 data=*gpiof_data;
//测试:读取用户传入的按键值
copy_from_user(key_data, buf, count);
printk("kernel:the key is:%d , %d\n",key_data[0] ,key_data[1] ); //检验用户空间传输的按键值
//实现反转led电平(反转指定位电平同时其他的位不影响)
*gpiof_data |= (1<<4) | (1<<5) |(1<<6);
*gpiof_data &= ~(data & ((1<<4) | (1<<5) |(1<<6))); //
return 0;
}
/*
函数名:drv_keyled_read
功能:读key电平
*/
static ssize_t drv_keyled_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
unsigned char key_data[2]={1,1};
unsigned char keybuf=0;
if(size != sizeof(key_data))//用户空间(应用程序)要读取的字节与内核空间存的字节数一致
{
printk("read size has err\n");
return -EINVAL; //返回错误
}
//把读取的按键值发给用户空间(应用端)
key_data[0] = (*gpiof_data & (1<<0)) ? 1 : 0;
key_data[1] = (*gpiog_data & (1<<3)) ? 1 : 0;
if( key_data[0] == 0)
{
printk("key1 press \n");
while(!keybuf)//按下弹出
{
keybuf= (*gpiof_data & (1<<0)) ? 1 : 0;
}
}
if( key_data[1] == 0)
{
printk("key2 press \n");
while(!keybuf)//按下弹出
{
keybuf= (*gpiog_data & (1<<3)) ? 1 : 0;
}
}
copy_to_user(buf, key_data, sizeof(key_data));//copy_to_user(用户空间,内核空间,字节数)
return sizeof(key_data);
}
用户空间与内核空间进行消息传递的关键函数:copy_from_user()和copy_to_user()。
copy_from_user():通常在xxx_write()函数内调用,实现用户空间的数据到内核空间的传递,传递的数据存在buf的形参中。copy_from_user()可以将buf中的数据读出来。
copy_to_user(): 通常在xxx_read()函数内调用,实现用户空间读取内核空间的数据,copy_to_user()是将内核里的数据存于buf中,提供用户层read()的读取。
7.声明驱动(模块的许可证声明),声明后,某块才能被正常安装到系统内核。固定格式如下:
MODULE_LICENSE(“GPL”);
8.编写驱动的makefile文件
同样地,找一写好的驱动makefile模板,修改主机上存储开发板的linux源码目录(第一行),并更改obj-m选项(最后一行),代码如下:
KERN_DIR = /work/mysystem/linux-2.6.22.6
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += drv_keyled.o
obj-m的主要功能:将驱动代码编译成模块(.ko文件),与obj-m对立的是:obj-y,是将代码直接编译到内核中。
1.新建以应用程序文件,名为:keyled_test.c ,同样找一应用程序模板,把必要头文件包含进来,如下:
#include
#include
#include
#include
2.编写内容,根据驱动提供的几个关键函数功能,用户层需要调取这些功能函数,完成最终实现。实验目的是按下按键,反转LED灯,因此需要调用read函数,读出内核传递过来的数据(按键值),根据按键值是否按下,调用write函数,实现对led控制。
程序开始需要通过设备文件,打开对应的驱动,然后返回一句柄fd,程序可以通过fd完成设备的读写操作。
形参:int argc, char **argv的作用:
当系统启动应用程序时,可以在启动程序文件的后面加上要传入的参数,这些参数就会传进int argc, char **argv的形参中,其中:
(注:文件名的本身也是个参数,因此参数至少1个,启动程序文件方式见后面操作)
- argc:显示的是传入参数的个数
- 指针argv[i]:显示第i个参数的字符串内容
源码如下:
int main(int argc, char **argv)
{
int fd;
unsigned char key_data[2]={1,1};
fd = open("/dev/keyled", O_RDWR); //应用端通过识别设备文件来识别驱动
if (fd < 0)
{
printf("can't open!\n");
}
if(argc == 2)//argc[0]为文件名本身
{
printf("argv=%s\n",argv[1]);
}
else if(argc == 3)
{
printf("argv=%s , %s\n",argv[1],argv[2]);
}
printf("hello word fd=%d\n",fd);
while(1)
{
read(fd,key_data,sizeof(key_data));
if(key_data[0] == 0 || key_data[1] == 0)
{
printf("user:k1=%d k2=%d\n",key_data[0],key_data[1]);//检验内核到用户空间的数据传输
write(fd,key_data,sizeof(key_data));
}
}
return 0;
}
3.测试程序的makefile
采用arm-linux-gcc编译器进行编译,生成可执行程序文件keyled_test ,内容如下:
CROSS.=arm-linux-
keyled_test : keyled_test.c
$(CROSS)gcc -o $@ $<
clean:
rm keyled_test
make编译驱动模块以及测试程序后,分别生成.ko文件和可执行文件,这两个文件需要存放在根文件系统上,分为完成驱动装载和运行测试程序。
开发板挂载根文件系统,通常采用NFS网络方式直接挂载到PC主机上,简单方便,并大大减少程序调试时间,但是笔者由于路由器问题,无法建立主机和开发板的网络连接,只能采用直接下载根文件系统到开发板上运行勒,很麻烦,每一次调试都得重新下一次,简直崩溃o(╥﹏╥)o,下面是驱动装载与测试程序运行具体步骤:
1.在制作好的根文件系统上新建以任意名称的文件夹:/moduel,将.ko文件和应用程序执行文件存在该目录中。
2.如果采用NFS挂载方式,可以省略此步。该步主要将文件系统下载在开发板中,因此需要在主机上把制作好的根文件系统转成映像文件(采用mkyaffs2image工具),然后由dnw工具,通过usb方式 下载到开发板中。(篇幅有限,具体过程略述啦^_^,能用NFS最好咯)。
3.启动开发板(uboot,kernel,文件系统都下载并装载好),操作系统启动后,在终端上进行驱动装载(关键命令:insmod),如下:
insmod为驱动装载,lsmod为查看装载情况
4.运行测试程序,如下图所示,图中可以观察到如下几个结论:
- 运行的程序名称后可加传入的参数,如图所示的参数为:“iu”和“tr”。
- 按下按键时,可以终端打印出相关信息,这些信息都是编写程序时实现的。
- 图中显示的kernel:…. 和user:…. 分别是内核空间与用户空间之间传递的数据信息,可以看出它们传递的数据是按键值,并且结果正确的。
4.开发板测试结果,激动人心的当然是开发板的实际运行效果啦,随着两个按键的任意按下,3个LED等就会翻转,看看效果(^▽^):
1.查看设备与设备文件。设备显示在/proc/devices虚拟文件系统内,设备文件可以在/dev目录下查看。
可以看出主设备号为252,次设备号为0
2.查看当前测试程序的进程和cpu占有率,此时启动的应用程序需要在后台运行(末位加上&字符),才能到前端查看进程和cpu占有率。
可以看出:进程号为791,cpu占有率为98%。