超简单易用的 “在 pcduino 开发板上写 Linux 驱动控制板载 LED

在pcduino开发板上写驱动控制板载LED的闪烁

以下仅是对原作者文章的整版复制,由于工作较忙,尚无时间细整理其中的代码,急用的可通过上面的链接跳转至原作者博客。

由于关于pcduino的资料比较少,所以这篇文章是参考了pcduino爱好者论坛的一篇教程《手把手教你用A10点灯》,并且系统的结合了linux驱动的开发步骤。读完这篇文章,你不但可以对pcduino开发板的硬件结构有所了解,更重要的是可以对linux的驱动开发步骤有一个系统的认识。我也是一个linux驱动的新手,所以,写的不对的地方,请大家指正。

1.Linux驱动框架

这一部分将会手把手教你创建一个Linux的驱动程序框架,在下一部分,我们只需要将控制pcduino硬件部分的代码填入这个框架就可以了。像所有的应用程序都有一个main函数作为函数的入口一样,linux驱动程序的入口是驱动的初始化函数。这个初始化函数是 module_init 来指定的,同样,与初始化函数对应的驱动程序的退出函数是由 module_exit函数来指定的。下面就让我们动手写第一个版本的驱动程序吧。

[cpp]view plaincopy

    #include<linux/module.h>#include<linux/init.h>staticint__initled_init(void){printk("ledinit\n");return0;}staticvoid__exitled_exit(void){printk("ledexit\n");}module_init(led_init);module_exit(led_exit);

将上面代码保存为 led.c,接下来就要编写Makefile文件对刚刚编写的驱动程序进行编译了。新建Makefile文件,在里面输入:

[cpp]view plaincopy

    obj-m:=led.oall:make-C/usr/src/linux-headers-3.8.0-35-generic/M=/home/asus/drive/clean:rm*.orm*.korm*.orderrm*.symversrm*.mod.c

注意,Makefile 中的第三行,-C 后面的参数为你当前使用的内核的头文件所在的目录,你只需要修改为 "/usr/src/linux-headers-你的内核版本/" 即可,如果你不知道,当前使用的内核版本,可以输入:[cpp]view plaincopy

    uname-r

来进行查看。M 后面表示你的驱动所在的目录。改好之后保存,注意,这个文件的名字一定得是 "Makefile" 才行,make 和 rm命令前面一定是一个TAB符才行。输入命令:

[cpp]view plaincopy

    make

进行编译,完成之后,使用ls查看,可以看到得到的文件如下:

[cpp]view plaincopy

    built-in.oled.cled.koled.mod.cled.mod.oled.oMakefilemodules.orderModule.symvers

这里面的 led.ko 是我们得到的驱动文件,使用:[cpp]view plaincopy

    sudoinsmodled.ko

安装驱动。使用[cpp]view plaincopy

    dmesg

命令,会看到最后一行输出的是 “led init” ,这句话就是在 led_init 函数中输出的。使用命令:[cpp]view plaincopy

    sudormmodled.ko

来卸载 led 驱动。再使用: dmesg 命令,会发现,最后一行为 “led exit”。

上面写的这个驱动程序是没有什么作用的,在linux中,应用程序是通过设备文件来和驱动程序进行交互的。所以我们需要在驱动程序中建立设备文件,这个设备文件建立之后,就会存在于 /dev/ 目录下,应用程序就是通过对这个文件的读写,来向驱动程序发送命令,并通过驱动程序控制硬件的动作。每一个驱动程序对应着一个设备文件。要建立一个设备文件,首先必须拥有设备号才行,这个设备号就需要我们向linux系统提出申请,由linux系统为我们分配。设备号有主设备号和从设备号之分,主设备号使用来表示驱动的类型,从设备号表示使用同一个驱动的设备的编号,这里要申请的就是主设备号。使用 alloc_chrdev_region 函数来申请一个设备号。设备号的类型为 dev_t ,它是一个 32 位的数,其中 12 位用来表示主设备号,另外 20 位用来表示从设备号。可以使用 MAJOR 宏和 MINOR 宏来直接获取主设备号和从设备号。我们第二个版本的程序如下:

[cpp]view plaincopy

    #include<linux/module.h>#include<linux/init.h>#include<linux/fs.h>//驱动名#defineDEV_NAME"led"//从设备的个数#defineDEV_COUNT1//声明设备号staticdev_tdev_number;//初始化staticint__initled_init(void){//错误标记interr;printk("ledinit\n");//申请设备号err=alloc_chrdev_region(&dev_number,0,DEV_COUNT,DEV_NAME);if(err){printk("allocdevicenumberfail\n");returnerr;}//如果申请成功,打印主设备号printk("majornumber:%d\n",MAJOR(dev_number));return0;}staticvoid__exitled_exit(void){printk("ledexit\n");//注销申请的设备号unregister_chrdev_region(dev_number,DEV_COUNT);}

这个程序申请了一个设备号,并且打印出来,同样使用 dmesg 命令来查看,程序的注释已经很详细了,就不再多解释了。 保存之后,编译,安装新的驱动程序。在安装新的驱动程序之前,需要使用命令 sudo rmmod led.ko 将之前安装的驱动程序卸载,使用 dmesg 命令查看输出的结果:[cpp]view plaincopy

    [384.225850]ledinit[384.225854]majornumber:250

还可以使用命令 cat /proc/devices | grep ‘led’ 查看获得的设备号。

设备号申请完毕后,就可以在 /dev/ 目录下创建设备文件了。需要了解的是设备在内存中,使用结构体 cdev 来表示,并且将我们申请的设备号,以及对文件操作的回调函数,统统的关联起来。最后使用这个结构体,用函数 class_create 和 device_create 来创建一个设备文件。说了一下基本思路,还是先看程序吧:[cpp]view plaincopy

    #include<linux/module.h>#include<linux/init.h>#include<linux/fs.h>#include<linux/cdev.h>#include<linux/device.h>//驱动名#defineDEV_NAME"led"//从设备的个数#defineDEV_COUNT1//三个回调函数,当在应用程序执行相应的操作时//驱动程序会调用相应的函数来进行处理ssize_tled_write(structfile*,constchar__user*,size_t,loff_t*);intled_open(structinode*,structfile*);intled_release(structinode*,structfile*);//声明设备号staticdev_tdev_number;//设备在内存中表示的结构体staticstructcdev*cdevp;//注册文件操作的回调函数的结构体staticstructfile_operationsfops={.owner=THIS_MODULE,//注册相应的回调函数.open=led_open,.release=led_release,.write=led_write,};//用来创建设备文件的classstaticstructclass*classp;//初始化staticint__initled_init(void){//错误标记interr;printk("ledinit\n");//申请设备号err=alloc_chrdev_region(&dev_number,0,DEV_COUNT,DEV_NAME);if(err){printk("allocdevicenumberfail\n");returnerr;}//如果申请成功,打印主设备号printk("majornumber:%d\n",MAJOR(dev_number));//给cdev结构体在内存中分配空间cdevp=cdev_alloc();//如果分配失败if(cdevp==NULL){printk("cdevallocfailure\n");//注销前面申请的设备号unregister_chrdev_region(dev_number,DEV_COUNT);return-1;}//将cdev结构体与//注册文件操作的回调函数的结构体file_operations关联起来cdev_init(cdevp,&fops);//将cdev结构体和申请的设备号关联起来err=cdev_add(cdevp,dev_number,DEV_COUNT);if(err){printk("cdevaddfailure\n");//释放申请的cdev空间cdev_del(cdevp);//注销申请的设备编号unregister_chrdev_region(dev_number,DEV_COUNT);returnerr;}//给class分配空间classp=class_create(THIS_MODULE,DEV_NAME);if(classp==NULL){printk("classcreatefailure\n");//释放申请的cdev空间cdev_del(cdevp);//注销申请的设备编号unregister_chrdev_region(dev_number,DEV_COUNT);return-1;}//创建设备文件device_create(classp,NULL,dev_number,"%s",DEV_NAME);printk("/dev/%screatesuccess\n",DEV_NAME);return0;}staticvoid__exitled_exit(void){printk("ledexit\n");//释放分配的class空间if(classp){device_destroy(classp,dev_number);class_destroy(classp);}//释放分配的cdev空间if(cdevp){cdev_del(cdevp);}//注销申请的设备号unregister_chrdev_region(dev_number,DEV_COUNT);}module_init(led_init);module_exit(led_exit);//当在应用程序中执行open函数时,//会调用下面的这个函数intled_open(structinode*pinode,structfile*pfile){printk("ledopen\n");return0;}//当在应用程序中执行close函数时,//会调用下面的函数intled_release(structinode*pinode,structfile*pfile){printk("ledrelease\n");return0;}//当在应用程序中调用write函数时,//会调用下面的这个函数ssize_tled_write(structfile*pfile,constchar__user*buf,size_tcount,loff_t*l){printk("ledwrite");return0;}//指定采用的协议MODULE_LICENSE("GPL");

最后一行是指定采用的协议,一定得写上,否则会造成虽然编译通过,但是在安装时,会出现[cpp]view plaincopy

    insmod:errorinserting’led.ko’:-1Unknownsymbolinmodule

这个错误。编译,安装好,之后,我们就可以在 /dev/ 目录下找到 led 文件,使用命令:[cpp]view plaincopy

    ls-l/dev/led

结果如下:[cpp]view plaincopy

    crw——-1rootroot250,0Dec2610:52/dev/led

至此我们的linux设备驱动框架,已经完全建立起来了。接下来要做的工作,就是对 pcduino 开发板进行编程了。

2.对 pcduino 进行编程,控制 LED 闪烁

所使用的开发板是pcduino开发板,如下图:

这是一款开源硬件,采用的是cortex-A8的核心,板上可以安装ubuntu,android系统,我们使用的板子已经安装了 ubuntu 系统,通过 HDMI转VGA 线连接屏幕,并且通过usb接口,连接键盘和鼠标,直接在其自带的ubuntu系统上,编写驱动并运行。我们仔细的查看板子,会发现板上一共带有 3 个led灯,分别是 RX_LED,TX_LED,ON_LED,分别用来指示接收,发送和电源的状态。这里我们只控制 TX_LED 灯进行闪烁。查看 pcduino 的硬件原理图,查找 TX_LED 的连接位置,如下图:

会看到第三行 TX_LED 连接到 CPU 的PH15引脚,并且 L 即低电平时为激活状态,H 高电平时,为熄灭状态。得到这个信息说明,我们只需要控制 CPU 的引脚 PH15 的状态,就可以控制 TX_LED 的状态了。

所以接下来就需要我们去查看 A10 的芯片手册,来看一看到底怎么控制 PH15 这个引脚。

可以看到 A10 芯片的引脚有很多,而我们只关注 PH,因为我们要控制的就是 PH15 这个引脚。这里需要的一个概念就是,对一个引脚的控制至少需要有两个寄存器,一个是控制寄存器,一个是数据寄存器。控制寄存器用来控制引脚的工作模式,比如输出或者输入;数据寄存器用来向引脚输出数据或者从引脚读入数据。所以我们要先查看一下 PH15 的配置寄存器,如下图:

我们发现 PH15 控制寄存器一共有3位28-30,共有 8 种工作模式,由于要控制 led 的状态,我们将它设置为输出模式,所以 PH15 控制寄存器的内容应该为 001。那么这个寄存器在哪个位置呢,在表上有 Offset:0x100 我们知道,PH寄存器的偏移地址是 0x100,但是基地址是多少呢。再往前面查阅就会发现

所以基地址就是 0x01C20800。基地址和偏移地址都有了,我们就可以定位 PH_CFG1 寄存器的地址就是(0x01C20800+0x100),我们只需要将这个寄存器的第28-30位置为:

[cpp]view plaincopy

    302928001

就可以了。

当控制寄存器配置完成之后,我们就需要向数据寄存器写入数据来控制 led 的闪烁。我们同样查看芯片手册:

可以看到,PH的数据寄存器用每一位来表示一个引脚的状态。我们要控制 PH15 引脚,就需要对这个寄存器的第15位进行操作。所以,接下来就是,开始动手向驱动框架中添加对硬件操作的时候:

[cpp]view plaincopy

    #include<linux/module.h>#include<linux/init.h>#include<linux/fs.h>#include<linux/cdev.h>#include<linux/device.h>#include<asm/io.h>#include<asm/uaccess.h>//驱动名#defineDEV_NAME"led"//从设备的个数#defineDEV_COUNT1//定义与硬件相关的宏//基地址#defineBASE_ADDRESS0x01C20800//PH_CFG1寄存器的地址#definePH_CFG1(BASE_ADDRESS+0x100)//PH_DAT寄存器的地址#definePH_DAT(BASE_ADDRESS+0x10C)//三个回调函数,当在应用程序执行相应的操作时//驱动程序会调用相应的函数来进行处理ssize_tled_write(structfile*,constchar__user*,size_t,loff_t*);intled_open(structinode*,structfile*);intled_release(structinode*,structfile*);//声明设备号staticdev_tdev_number;//设备在内存中表示的结构体staticstructcdev*cdevp;//注册文件操作的回调函数的结构体staticstructfile_operationsfops={.owner=THIS_MODULE,//注册相应的回调函数.open=led_open,.release=led_release,.write=led_write,};//用来创建设备文件的classstaticstructclass*classp;//声明用来表示PH_CFG1内存地址的变量volatilestaticunsignedlong*__ph_cfg1;//用来表示PH_DAT内存地址的变量volatilestaticunsignedlong*__ph_dat;//初始化staticint__initled_init(void){//错误标记interr;printk("ledinit\n");//申请设备号err=alloc_chrdev_region(&dev_number,0,DEV_COUNT,DEV_NAME);if(err){printk("allocdevicenumberfail\n");returnerr;}//如果申请成功,打印主设备号printk("majornumber:%d\n",MAJOR(dev_number));//给cdev结构体在内存中分配空间cdevp=cdev_alloc();//如果分配失败if(cdevp==NULL){printk("cdevallocfailure\n");//注销前面申请的设备号unregister_chrdev_region(dev_number,DEV_COUNT);return-1;}//将cdev结构体与//注册文件操作的回调函数的结构体file_operations关联起来cdev_init(cdevp,&fops);//将cdev结构体和申请的设备号关联起来err=cdev_add(cdevp,dev_number,DEV_COUNT);if(err){printk("cdevaddfailure\n");//释放申请的cdev空间cdev_del(cdevp);//注销申请的设备编号unregister_chrdev_region(dev_number,DEV_COUNT);returnerr;}//给class分配空间classp=class_create(THIS_MODULE,DEV_NAME);if(classp==NULL){printk("classcreatefailure\n");//释放申请的cdev空间cdev_del(cdevp);//注销申请的设备编号unregister_chrdev_region(dev_number,DEV_COUNT);return-1;}//创建设备文件device_create(classp,NULL,dev_number,"%s",DEV_NAME);printk("/dev/%screatesuccess\n",DEV_NAME);return0;}staticvoid__exitled_exit(void){printk("ledexit\n");//释放分配的class空间if(classp){device_destroy(classp,dev_number);class_destroy(classp);}//释放分配的cdev空间if(cdevp){cdev_del(cdevp);}//注销申请的设备号unregister_chrdev_region(dev_number,DEV_COUNT);}module_init(led_init);module_exit(led_exit);//当在应用程序中执行open函数时,//会调用下面的这个函数intled_open(structinode*pinode,structfile*pfile){//临时变量unsignedlongtmp;printk("ledopen\n");//将PH15管脚设置为输出状态//将PH_CFG1这个硬件寄存器的地址,映射到linux内存,并获取映射后的地址//通过对这个地址的操作,就可以控制PH_CFG1__ph_cfg1=(volatileunsignedlong*)ioremap(PH_CFG1,4);//将设置PH15寄存器tmp=*__ph_cfg1;tmp&=~(0xf<<28);tmp|=(1<<28);*__ph_cfg1=tmp;//将灯初始化为熄灭的状态__ph_dat=(volatileunsignedlong*)ioremap(PH_DAT,4);tmp=*__ph_dat;tmp|=(1<<15);*__ph_dat=tmp;return0;}//当在应用程序中执行close函数时,//会调用下面的函数intled_release(structinode*pinode,structfile*pfile){printk("ledrelease\n");//注销分配的内存地址iounmap(__ph_dat);iounmap(__ph_cfg1);return0;}//当在应用程序中调用write函数时,//会调用下面的这个函数ssize_tled_write(structfile*pfile,constchar__user*buf,size_tcount,loff_t*l){intval;volatileunsignedlongtmp;printk("ledwrite\n");//从用户空间读取数据copy_from_user(&val,buf,count);printk("write%d\n",val);//从应用程序读取命令//来控制led灯tmp=*__ph_dat;if(val==1){//灯亮tmp&=~(1<<15);}else{//灯灭tmp|=(1<<15);}*__ph_dat=tmp;return0;}MODULE_LICENSE("GPL");

上面的是完整的控制pcduino上led闪烁的驱动程序,写完这个驱动程序之后,再写一个下面的测试程序就可以使 led 闪烁了,测试的代码如下:

[cpp]view plaincopy

    #include<stdio.h>#include<unistd.h>#include<fcntl.h>intmain(void){intfd;intval=1;//打开驱动对应的设备文件fd=open("/dev/led",O_RDWR);if(fd<0){printf("open/dev/lederror\n");return-1;}while(1){//写入高电平write(fd,&val,sizeof(int));//睡眠一秒sleep(1);//将电平反转val=0;//写入低电平write(fd,&val,sizeof(int));//睡眠一秒sleep(1);val=1;}close(fd);return0;}

使用 gcc testled.c 将该应用程序编译,假设生成a.out,安装新版的驱动程序后,使用[cpp]view plaincopy

    sudo./a.out

就可以看到 pcduino 上的 led 就开始闪烁了。

有多远,走多远,把足迹连成生命线。

超简单易用的 “在 pcduino 开发板上写 Linux 驱动控制板载 LED

相关文章:

你感兴趣的文章:

标签云: