为linux3.4.2内核编写LED驱动

开发环境

  • JZ2440 V3开发板
  • Linux-3.4.2内核
  • 宿主机:Ubuntu 16.04_64位
  • 交叉编译器:arm-linux-gcc (version 4.3.2 )

1、linux字符设备驱动框架

  • 用户应用程序通过调用C库里已经实现的 open 、read、write等库函数来操作文件(在Linux中,一切皆文件,所有硬件设备在内核看来均是文件)。
  • 库函数(open等)的调用引发操作系统(Linux 内核)产生一个异常中断(软中断:swi val),于是CPU控制权交给内核,进入内核异常处理。
  • 内核根据发生中断的原因(中断号),调用相应处理函数(sys_open,sys_read等)。
  • sys_open等函数会根据中断类型,调用相关设备(字符设备、块设备、网络设备等)的驱动程序里的相应函数,如led_open,led_read等。

为linux3.4.2内核编写LED驱动_第1张图片

一个简单的字符驱动程序框架主要包括以下几个部分:

  1. 与C库函数相对应的设备操作函数,例如led_open,led_read等

    static int first_drv_open(struct inode *inode, struct file *file)
    {
    	printk("first_drv_open\n");
    	return 0;
    }
     
    static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
    {
    	printk("first_drv_write\n");
    	return 0;
    }
    
  2. 用一个结构体封装上述函数

    static struct file_operations first_drv_fops = {
        .owner  =   THIS_MODULE,    /* 指向编译模块时自动创建的__this_module变量的宏 */
        .open   =   first_drv_open,     
    	.write	=	first_drv_write,	   
    };
    
  3. 通过驱动注册函数将上述结构体告知内核,使设备的各种操作函数与库函数对应起来

    int major;	//主设备号
    static int first_drv_init(void)
    {
    	major = register_chrdev(0, "first_drv", &first_drv_fops); // 注册, 告诉内核
        firstdrv_class = class_create(THIS_MODULE, "firstdrv");
        firstdrv_class_dev = device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz"); /* /dev/xyz */
    
    	printk("first_drv_init\n");
    	return 0;
    }
    
    static void first_drv_exit(void)
    {
    	unregister_chrdev(major, "first_drv"); // 卸载
    }
    
    • 以下两个函数实现了在内核中加入设备信息,mdev就可以根据/sys下生成的设备信息,自动创建设备节点:
      • class_create会在/sys下创建 firstdrv这个类,
      • firstdrv_class_dev会在firstdrv类下创建xyz这个设备,
      • mdev会自动创建一个dev/xyz设备节点。
  4. 指定模块加载函数、卸载函数和授权信息等

module_init(first_drv_init);
module_exit(first_drv_exit);

MODULE_LICENSE("GPL");

当用户应用程序使用open函数打开某个设备(/dev/led)的时候,系统首先查看出该设备是字符设备,然后根据其主设备号,去字符设备文件操作指针数组中找到对应的file_operation结构体地址。

驱动注册的过程就是上述过程的逆过程:创建设备的主、次设备号major、minor,并以major为索引,将设备的file_operation结构体指针写进设备对应的文件操作指针数组中去,实现注册。

  1. Makefile编写
KERN_DIR = /home/leon/linux-3.4.2	//宿主机上编译到开发板上的linux内核的根目录
 
all:
	make -C $(KERN_DIR) M=`pwd` modules 
 
clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order
 
obj-m	+= first_drv.o
  1. 编译驱动模块

驱动编译依赖于内核文件,因此需要解压内核源码后再编译。

并且使用的交叉编译工具也要和编译内核镜像uImage文件一致,用到的内核版本也要和板子上运行的一致。编译成功后生成.ko文件。

  1. 加载驱动模块

启动开发板进入linux系统命令行,并将编译好的驱动模块first_dri.ko复制到开发板的根文件系统中,执行加载命令:

JZ2440 # insmod first_dri.ko

执行cat /proc/devices可以查看设备驱动是否成功加载到系统(主设备号+设备名称)

执行cat /proc/modular可以查看驱动模块是否成功加载到系统

  1. 编写驱动测试函数

编写驱动测试函数,在宿主机上交叉编译后得到可执行文件,并将其复制到开发板根文件系统中,运行后查看结果。要注意的是,编译驱动测试程序的工具链版本和文件系统的版本要一致,不然应用程序可能会缺少某些库而无法运行

#include 
#include 
#include 
#include 
 
/* firstdrvtest on
  * firstdrvtest off
  */
int main(int argc, char **argv)
{
	int fd;
	int val = 1;
	fd = open("/dev/xyz", O_RDWR);	//打开设备节点名,而不是驱动程序
	if (fd < 0)
	{
		printf("can't open!\n");
	}
	write(fd, &val, 4);
	return 0;
}
arm-linux-gcc  firstdrvtest.c -o firstdrvtest	//虚拟机上编译,得到可执行测试文件

cp firstdrvtest /home/leon/nfs_root/first_fs	//拷贝到开发板中,开发板通过挂载NFS共享文件系统启动
JZ2440 # ./firstdrvtest on	//开发板上运行测试程序,查看结果
first_drv_init
first_drv_open
first_drv_write
JZ2440 # 
JZ2440 # rmsmod first_dri.ko	//移除驱动模块
JZ2440 # rm /dev/xyz			//卸载设备节点

2、LED字符设备驱动编写

前面已经写好了字符设备驱动程序框架,现在实现led字符设备驱动只需要往框架里填充函数就行了。整体设想就是:

  • open中实现端口初始化(输入输出配置)
  • 入口函数中完成虚拟地址映射
  • write中实现LED操作
    • copy_from_user 用户空间到内核空间传递数据。
    • copy_to_user(); 内核空间到用户空间传递数据

完整驱动代码如下:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
 
static struct class *firstdrv_class;
static struct class_device	*firstdrv_class_dev;
 
volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;
 
 
static int first_drv_open(struct inode *inode, struct file *file)
{
	/* 配置GPF4,5,6为输出 */
	*gpfcon &= ~((0x3<<(4*2)) | (0x3<<(5*2)) | (0x3<<(6*2)));
	*gpfcon |= ((0x1<<(4*2)) | (0x1<<(5*2)) | (0x1<<(6*2)));
	return 0;
}
 
static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
	int val;
 
	copy_from_user(&val, buf, count); //	copy_to_user();
 
	if (val == 1)
	{
		// 点灯
		*gpfdat &= ~((1<<4) | (1<<5) | (1<<6));
	}
	else
	{
		// 灭灯
		*gpfdat |= (1<<4) | (1<<5) | (1<<6);
	}
	
	return 0;
}
 
static struct file_operations first_drv_fops = {
    .owner  =   THIS_MODULE,    
    .open   =   first_drv_open,     
	.write	=	first_drv_write,	   
};
 
 
int major;
static int first_drv_init(void)
{
	major = register_chrdev(0, "first_drv", &first_drv_fops);	//0表示主设备号由系统指定
 
	firstdrv_class = class_create(THIS_MODULE, "firstdrv");
 
	firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xyz");
 
	gpfcon = (volatile unsigned long *)ioremap(0x56000050, 16);
	gpfdat = gpfcon + 1;
 
	return 0;
}
 
static void first_drv_exit(void)
{
	unregister_chrdev(major, "first_drv");
 
	class_device_unregister(firstdrv_class_dev);
	class_destroy(firstdrv_class);
	iounmap(gpfcon);
}
 
module_init(first_drv_init);
module_exit(first_drv_exit);
 
MODULE_LICENSE("GPL");
  • 地址映射函数ioremap的理解:

由于驱动是属于内核的一部分,是不能直接访问硬件的物理地址的,因此对于Soc上的物理地址,我们需要进行地址映射以后才能够访问。
进程的虚拟地址和物理内存的地址关系如下所示:

为linux3.4.2内核编写LED驱动_第2张图片

在驱动中进行地址映射只要用ioremap函数就可以了。

ioremap宏定义在asm/io.h内:

#define ioremap(cookie,size)   __ioremap(cookie,size,0)

__ioremap函数原型为(arm/mm/ioremap.c):

void __iomem * __ioremap(unsigned long phys_addr, size_t size, unsigned long flags);

phys_addr:要映射的起始的物理地址;
size:为映射的地址长度,
flags:要映射的地址空间和权限有关的标志(我们不需要关心)

由于我们的目的是点亮LED灯,我们只需要操作控制寄存器(设置引脚输出)和数据寄存器(设置引脚电平)就可以了,我们先在文件头部对要操作的寄存器进行定义:

volatile unsigned long *gpfcon = NULL;
volatile unsigned long *gpfdat = NULL;

volatile是一个特征修饰符,volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

查看JZ2440开发板原理图:

为linux3.4.2内核编写LED驱动_第3张图片

本次涉及的GPIO管脚为GPF4,GPF5,GPF6。
再查看主芯片S3C2440A的芯片用户手册,查的GPFCON和GPFDAT的物理地址如下:

为linux3.4.2内核编写LED驱动_第4张图片

然后我们就可以在入口函数中调用ioremap函数对物理地址进行映射了,由于GPFCON和GPFDAT在物理地址上是紧挨着的,我们方便起见,只需要用映射GPFCON的地址,而GPFDAT只需要在GPFCON的指针上+1就可以了。

同样,在出口函数里也要对映射的地址进行释放,释放映射的地址我们用到了iounmap函数,参数只要将映射的指针传进去就可以了。由于只对GPFCON地址进行了映射操作,这里只对GPFCON进行释放就可以了。

  • GPIOF的设置

需要将GPIOF4、5、6引脚设置为输出引脚,点亮和熄灭LED只需要设置GPFDAT的第4到6位为0和1就行:

为linux3.4.2内核编写LED驱动_第5张图片

为linux3.4.2内核编写LED驱动_第6张图片

3、LED字符设备驱动的改进

改进思路:利用次设备号,实现LED的精准控制。(原来是3个灯同时亮灭,现在想可以独立控制)

方法:创建多个设备节点,根据传入参数,执行不同的硬件操作

/*
 * 执行insmod命令时就会调用这个函数 
 */
static int __init s3c24xx_leds_init(void)
//static int __init init_module(void)
 
{
    int ret;
	int minor = 0;
 
    gpio_va = ioremap(0x56000000, 0x100000);
	if (!gpio_va) {
		return -EIO;
	}
 
    /* 注册字符设备
     * 参数为主设备号、设备名字、file_operations结构;
     * 这样,主设备号就和具体的file_operations结构联系起来了,
     * 操作主设备为LED_MAJOR的设备文件时,就会调用s3c24xx_leds_fops中的相关成员函数
     * LED_MAJOR可以设为0,表示由内核自动分配主设备号
     */
    ret = register_chrdev(LED_MAJOR, DEVICE_NAME, &s3c24xx_leds_fops);
    if (ret < 0) {
      printk(DEVICE_NAME " can't register major number\n");
      return ret;
    }
 
	leds_class = class_create(THIS_MODULE, "leds");
	if (IS_ERR(leds_class))
		return PTR_ERR(leds_class);
     
	leds_class_devs[0] = class_device_create(leds_class, NULL, MKDEV(LED_MAJOR, 0), NULL, "leds"); /* /dev/leds */
	
	for (minor = 1; minor < 4; minor++)  /* /dev/led1,2,3 */
	{
		leds_class_devs[minor] = class_device_create(leds_class, NULL, MKDEV(LED_MAJOR, minor), NULL, "led%d", minor);
		if (unlikely(IS_ERR(leds_class_devs[minor])))
			return PTR_ERR(leds_class_devs[minor]);
	}
        
    printk(DEVICE_NAME " initialized\n");
    return 0;
}

驱动测试应用程序:

#include 
#include 
#include 
#include 
 
/*
  *  ledtest  
  */
 
void print_usage(char *file)
{
    printf("Usage:\n");
    printf("%s  \n",file);
    printf("eg. \n");
    printf("%s /dev/leds on\n", file);
    printf("%s /dev/leds off\n", file);
    printf("%s /dev/led1 on\n", file);
    printf("%s /dev/led1 off\n", file);
}
int main(int argc, char **argv)
{
    int fd;
    char* filename;
    char val;
 
    if (argc != 3)
    {
        print_usage(argv[0]);
        return 0;
    }
    filename = argv[1];
    fd = open(filename, O_RDWR);
    if (fd < 0)
    {
        printf("error, can't open %s\n", filename);
        return 0;
    }
 
    if (!strcmp("on", argv[2]))
    {
        // 亮灯
        val = 0;
        write(fd, &val, 1);
    }
    else if (!strcmp("off", argv[2]))
    {
        // 灭灯
        val = 1;
        write(fd, &val, 1);
    }
    else
    {
        print_usage(argv[0]);
        return 0;
    }
    
    
    return 0;
}

你可能感兴趣的:(嵌入式开发,Linux3.4.2驱动开发,LED驱动,JZ2440,Linux-3.4.2,字符型设备驱动程序)