本文基于华清4412开发板,讲解如何从零开始编写led驱动程序和测试程序。首先介绍一下该4412开发板的led硬件原理图。
从原理图上我们可以看出,让led点亮的条件是往对应端口送高电平,熄灭的条件是送低电平。
从上面这幅图中可以看到对应引脚的寄存器配置,这里我们选择对LED2进行闪烁实验。我们需要把GPX2CON【7】配置寄存器设置为输出模式,也就是设置为0x1。我们还需要通过设置数据寄存器来控制LED的亮和灭,从下面这幅图可以看出我们往GPX2DAT【7】送1就能点亮LED2,送0就能使LED2熄灭。我们可以从这两幅图上得到对应寄存器的地址。
下面我们开始写代码:
#include <linux/module.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/device.h> #include <asm/uaccess.h> #include <asm/io.h> #include <linux/slab.h> #define GPX2CON 0x11000c40 //封装设备信息,面向对象编程思想 struct led_desc{ struct class *class; struct device *device; unsigned long *reg_virt_addr;//声明GPX2CON的虚拟地址 }; //定义设备 struct led_desc *led_dev; int led_open (struct inode *inode, struct file *filp) { printk("---------%s----------\n",__FUNCTION__); return 0; } ssize_t led_read (struct file *filp, char __user *buf, size_t count, loff_t *fpos) { printk("---------%s----------\n",__FUNCTION__); return 0; } ssize_t led_write (struct file *filp, const char __user *buf, size_t count, loff_t *fpos) { int value; int res; printk("---------%s----------\n",__FUNCTION__); res = copy_from_user(&value,buf, count); if(res > 0){ printk("copy to user error\n"); return -EFAULT; } printk("value from user is %d\n",value); //如果用户空间传过来的数据非0,则点亮LED2 if(value){ writel(readl(led_dev->reg_virt_addr+1) | (0x1<<7), led_dev->reg_virt_addr+1); }else{ writel(readl(led_dev->reg_virt_addr+1) & ~(0x1<<7), led_dev->reg_virt_addr+1); } return 0; } int led_close (struct inode *inode, struct file *filp) { printk("---------%s----------\n",__FUNCTION__); return 0; } const struct file_operations myfops = { .open = led_open, .read = led_read, .write = led_write, .release = led_close, }; int ret; static int __init led_drv_init(void) { int err; //u32 val; printk("---------%s----------\n",__FUNCTION__); //给设备对象申请空间 led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL); if(led_dev == NULL){ printk(KERN_ERR "kmalloc error\n"); return -ENOMEM; } //动态申请设备号 ret = register_chrdev(ret, "led_drv_test", &myfops); if(ret < 0){ printk(KERN_ERR "register chr_dev error\n"); err = -ENODEV; goto err_0; } //申请设备节点 led_dev->class = class_create(THIS_MODULE, "led_drv_class"); if(IS_ERR(led_dev->class)){ printk(KERN_ERR "class create error\n"); err = PTR_ERR(led_dev->class); goto err_1; } led_dev->device = device_create(led_dev->class,NULL, MKDEV(ret,0), NULL, "led%d",2); if(IS_ERR(led_dev->device)){ printk(KERN_ERR "device create error\n"); err = PTR_ERR(led_dev->device); goto err_2; } //地址映射 led_dev->reg_virt_addr = ioremap(GPX2CON, 8); if(led_dev->reg_virt_addr == NULL){ printk(KERN_ERR "ioremap error\n"); err = -ENOMEM; goto err_3; } //配置GPXC2CON寄存器为输出模式 *led_dev->reg_virt_addr &= ~(0xf<<28); *led_dev->reg_virt_addr |= (0x1<<28); /* val = readl(led_dev->reg_virt_addr); val &= ~(0xf<<28);] val |= (0x1<<28); writel(val,led_dev->reg_virt_addr); */ return 0; //错误处理 err_3: device_destroy(led_dev->class, MKDEV(ret, 0)); err_2: class_destroy(led_dev->class); err_1: unregister_chrdev(ret, "led_drv_test"); err_0: kfree(led_dev); return err; } static void __exit led_drv_exit(void) { printk("---------%s----------\n",__FUNCTION__); iounmap(led_dev->reg_virt_addr); device_destroy(led_dev->class, MKDEV(ret, 0)); class_destroy(led_dev->class); unregister_chrdev(ret, "led_drv_test"); kfree(led_dev); } module_init(led_drv_init); module_exit(led_drv_exit); MODULE_LICENSE("GPL");
以上就是led的驱动编写框架。分为这几个部分:
1、封装设备信息。这里把led的有关信息封装成一个结构体,然后用此结构体定义一个设备(led)。
2、入口函数里先给上一步定义的设备(led)申请空间,然后是动态申请设备号,当然也可以静态申请设备号,可以自定义设备号,只要不和已有的设备号重复就行。接下去是申请设备节点,也可以采用手动申请的方式,这里不多作介绍。最后是初始化映射地址和配置寄存器。
3、初始化完毕,我们就可以在空户空间和内核空间就是数据传递,这里我们采用从用户空间传递过来的数据来控制led2的亮和灭。
大致的思路就是这样。整个框架搭好了就好办了。下面我们编写测试程序,先上代码。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> int main(int argc,char **argv) { int fd; int value = 0; fd = open("/dev/led2",O_RDWR); if(fd < 0){ perror("open"); exit(1); } while(1){ value = 0; write(fd,&value,4); usleep(50000); value = 1; write(fd,&value,4); usleep(50000); } close(fd); return 0; }
好了,下面编写makefile
#当前目录 CUR_DIR = `pwd` #根文件系统所在目录 ROOTFS_DIR = /source/rootfs #内核所在目录 KERNEL_DIR = /home/linux/linux-3.14.1 #应用程序名称 APP_NAME = led_test #要编译的模块名称 MODULE_NAME = led_drv #交叉编译工具链 CROSS_COMPILE = /home/linux/toolchain/gcc-4.6.4/bin/arm-none-linux-gnueabi- CC = $(CROSS_COMPILE)gcc ifeq ($(KERNELRELEASE),) all: #编译模块 make -C $(KERNEL_DIR) M=$(CUR_DIR) modules #编译上层APP应用程序 $(CC) $(APP_NAME).c -o $(APP_NAME) clean: #清除模块 make -C $(KERNEL_DIR) M=$(CUR_DIR) clean #清除app rm -rf $(APP_NAME) install: #更新驱动模块和测试程序 cp -raf *.ko $(APP_NAME) $(ROOTFS_DIR)/drv_module else obj-m += $(MODULE_NAME).o endif
整个工程编译完毕。我们编译一下工程。
启动开发板,查看设备号。
加载led模块再看设备号。
设备节点也有了/dev/led2
程序开始执行,输出对应信息,开发板led2每隔50ms闪烁一次。
这里的测试程序只是先打开设备节点文件,所以我们可以看到先执行驱动程序的led_open函数,然后我们往内核传数据,相应地就执行驱动程序的led_write函数,由于一直在while循环中,所以并没有执行驱动程序里的led_read和led_close函数。
开发板上led2闪烁现场就不发了,图片看不出效果。好了,先写到这里。。。