mx6ull字符设备驱动(以及新字符设备驱动)开发笔记

在测试完后面的WIFI、4g网络驱动之后,这边需要测试一下ZigBee能否与开发板实现通信,看了网上的资料,可能需要修改设备树里面的串口信息啥的,索性先学习一下如何进行驱动开发,毕竟后面都是直接用的。

字符设备驱动

最底下的module_init()和module_exit()是模块的加载和卸载注册函数。

括号里的xxx_init和xxx_exit就是上面的两个函数,加载卸载。

static int __init chrdevbase_init(void)
{
	int retvalue = 0;

	/* 注册字符设备驱动 
	static inline int register_chrdev(unsigned int major, const char *name,
				  const struct file_operations *fops)
	这里major是设备号,name是设备名。最后一个是设备操作结构体,里面可以定义功能
	*/

	retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);


	if(retvalue < 0){
		printk("chrdevbase driver register failed\r\n");
	}
	printk("chrdevbase init! my first app?\r\n");
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit chrdevbase_exit(void)
{
	/* 注销字符设备驱动 */
	unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);


	printk("chrdevbase exit!\r\n");
}

/* 
 * 将上面两个函数指定为驱动的入口和出口函数 
 */

//模块的出口与入口
module_init(chrdevbase_init);   //入口,上面的static int __init chrdevbase_init(void)执行
module_exit(chrdevbase_exit); //出口,static void __exit chrdevbase_exit(void)执行

加载和卸载函数中的两个函数,是字符设备的注册与注销函数。参数为 设备号,设备名和一个设备结构体。

retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);

unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);

这个设备号不是随便给的,(名字可以),用命令cat /proc/devices 查看自己Linux开发板的空闲设备号。

然后是对设备的具体操作函数:

也就是代码中的open、release、write、read。这四个函数是通过之前的注册函数中的第三个参数的结构体定义的。

*
 * 设备操作函数结构体
 */
static struct file_operations chrdevbase_fops = {
	.owner = THIS_MODULE,	   //这个模块
	.open = chrdevbase_open,  //打开
	.read = chrdevbase_read,     //读
	.write = chrdevbase_write,     //写
	.release = chrdevbase_release,   //释放
};

对应的函数就是下面的四个

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
	//printk("chrdevbase open!\r\n");

	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue = 0;
	
	/* 向用户空间发送数据 */
	memcpy(readbuf, kerneldata, sizeof(kerneldata));//将上面定义的内核数据复制到读缓冲区的数组
	retvalue = copy_to_user(buf, readbuf, cnt);//通过函数 copy_to_user 将readbuf 中的数据复制到参数 buf 中
    //因为内核空间不能直接操作用户空间的内存,因此需要借助 copy_to_user 函数来完成内核空间的数据到用户空间的复制。
	if(retvalue == 0){
		printk("kernel senddata ok! 内核发送数据成功\r\n");
	}else{
		printk("kernel senddata failed!  内核发送数据失败\r\n");
	}
	
	//printk("chrdevbase read!\r\n");
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue = 0;
	/* 接收用户空间传递给内核的数据并且打印出来 */
	retvalue = copy_from_user(writebuf, buf, cnt);
	if(retvalue == 0){
		printk("这里是向内核写数据,kernel recevdata:%s\r\n", writebuf);
	}else{
		printk("kernel recevdata failed!\r\n");
	}
	
	//printk("chrdevbase write!\r\n");
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
	//printk("chrdevbase release!\r\n");
	return 0;
}

最后添加设备信息和作者信息(不添加可能报错),设备用GPL协议(就是开源协议的意思。)

/* 
 * LICENSE和作者信息
 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("caijilong");

驱动APP编写:

这个APP就是Linux应用,就像你手上的安卓程序,点开就能应用程序。这APP就是写好,你写一个命令就可操作这个驱动程序。

static char usrdata[] = {"usr data!"};
/*
 * @description		: main主程序
 * @param - argc 	: argv数组元素个数
 * @param - argv 	: 具体参数,字符串形式
 * @return 			: 0 成功;其他 失败
 ./chrdevbase /dev/chrdevbase <1>|<2>
 这里1是读文件,2是写文件
 argc是数组元素个数,argv是具体的字符串
 */
int main(int argc, char *argv[])
{
	int fd, retvalue; //fd是要读取的文件的描述符

	char *filename;

	char readbuf[100], writebuf[100];//读写缓冲区

	if(argc != 3){  //这里是argc如果不等于3,因为数组[2]的三个0 1 2,在写命令的时候./chrdevbaseApp /dev/chrdevbase 1
                        // 如果不写1,也算没有正确输入。返回打印 Error Usage

		printf("Error Usage!\r\n");

		return -1;

	}

	filename = argv[1];

	/* 打开驱动文件 */

	fd  = open(filename, O_RDWR);// 第二个参数,文件打开参数,这个是 读写模式

	if(fd < 0){

		printf("Can't open file %s\r\n", filename);

		return -1;

	}

	if(atoi(argv[2]) == 1){ 
        /* 从驱动文件读取数据
        这里的atoi函数是将字符串转为int
        在APP测试的时候最后输入的1 和2 是字符串形式的,这里转换一下,然后根据你输入的1和2,选择程序是读还是写。
        */

		retvalue = read(fd, readbuf, 50); //从设备文件中读取50个字节到读取缓冲区

		if(retvalue < 0){  //如果没读到,返回值是小于0 的,读到的返回读到的字节数。

			printf("read file %s failed!\r\n", filename);

		}else{

			/*  读取成功,打印出读取成功的数据 */

			printf("read data:%s\r\n",readbuf);

		}

	}

	if(atoi(argv[2]) == 2){

 	/* 向设备驱动写数据 */

		memcpy(writebuf, usrdata, sizeof(usrdata));

		retvalue = write(fd, writebuf, 50);//向设备文件写最多50个字节的数据,到写的缓冲区

		if(retvalue < 0){   //如果写了,返回值是写的字节数

			printf("write file %s failed!\r\n", filename);

		}

	}
	/* 关闭设备 */

	retvalue = close(fd);   //关闭

	if(retvalue < 0){

		printf("Can't close file %s\r\n", filename);

		return -1;
	}
	return 0;
}

用man open 查看open函数的信息。可以查看到该函数需要什么头文件和参数应用。(剩下的在代码中注释),另外三个也用man看。

 写好chrdevbaseApp后,编译一下。生成chrdevbaseApp文件,他是二进制的。

arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp

(Makefile我还不会自己写,用他的)最后将编译.c文件生成的.ko文件和chrdevbaseApp文件复制根目录文件下的驱动所在。

测试:因为之前测试过WiFi和4G的驱动了,这里如果是第一次用驱动,先depmod一下。

然后modprobe chrdevbase.ko 启动驱动,可以用lsmod查看驱动启动情况。用rmmod ~.ko 卸载驱动

这时候可以看到驱动加载成功。显示chrdevbase_init()里面的printk的内容。

要用APP的话还需要为设备驱动创建字符节点文件,命令如下。c是字符设备的意思,200主设备号,0次设备号

mknod /dev/chrdevbase c 200 0

然后就可以使用命令 ./chrdevbaseApp /dev/chrdevbase 1 来读内核数据,参数2来向写内核数据。

注意:一旦卸载设备驱动,rmmod chrdevbase.ko ,相应的字符设备节点文件也会被删除, 因为这并不是写入Linux内核然后编译烧写过来的,只是字符设备模块。

 

新字符设备驱动笔记

只是相对于前面的字符设备驱动有更新,更新在新的注册和注销方式以及自动生成字符设备节点文件的功能。

代码中用到了虚拟内存的概念,可以理解为驱动中用虚拟的内存去代替裸机实际物理地址的寄存器地址。

所有笔记写在代码注释中。(手写了一遍,顺了一遍流程。除了点灯自己的寄存器部分,其他所有的驱动代码都大同小异。)

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>

#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/*
自己手动写一遍字符设备驱动的代码,熟悉一下流程。
*/

#define MYNEWLED_NAME "mynewled"

#define LEDOFF 	0
#define LEDON 	1

/* 寄存器物理地址 
将物理寄存器的地址确定好,裸机寄存器
*/
#define CCM_CCGR1_BASE				(0X020C406C)	
#define SW_MUX_GPIO1_IO03_BASE		(0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE		(0X020E02F4)
#define GPIO1_DR_BASE				(0X0209C000)
#define GPIO1_GDIR_BASE				(0X0209C004)

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/*
 * @description		: LED打开/关闭
 * @param - sta 	: LEDON(0) 打开LED,LEDOFF(1) 关闭LED
 * @return 			: 无
 */
void led_switch(u8 sta)
{
	u32 val = 0;
	if(sta == LEDON) {
		val = readl(GPIO1_DR);
		val &= ~(1 << 3);	
		writel(val, GPIO1_DR);
	}else if(sta == LEDOFF) {
		val = readl(GPIO1_DR);
		val|= (1 << 3);	
		writel(val, GPIO1_DR);
	}	
}


/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int mynewled_open(struct inode *inode, struct file *filp)
{
	return 0;
}

/*
 * @description		: 从设备读取数据 
 * @param - filp 	: 要打开的设备文件(文件描述符)
 * @param - buf 	: 返回给用户空间的数据缓冲区
 * @param - cnt 	: 要读取的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 读取的字节数,如果为负值,表示读取失败
 */
static ssize_t mynewled_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件,表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t mynewled_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		printk("kernel write failed!\r\n");
		return -EFAULT;
	}

	ledstat = databuf[0];		/* 获取状态值 */

	if(ledstat == LEDON) {	
		led_switch(LEDON);		/* 打开LED灯 */
	} else if(ledstat == LEDOFF) {
		led_switch(LEDOFF);	/* 关闭LED灯 */
	}
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int mynewled_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations mynewled_fops = {
	.owner = THIS_MODULE,
	.open = mynewled_open,
	.read = mynewled_read,
	.write = mynewled_write,
	.release = 	mynewled_release,
};

// LED 的设备结构体
struct mynewled_dev
{
    /* data */
    struct   cdev    cdev;   //新的字符设备注册方法,字符设备结构
    dev_t       devid;      //设备号
    int         major;     //主设备号
    int         minor;      //次设备号
	struct	class	*class;//创建类用来,udev和mdev 自动创建
	struct	device	*device; //创建设备
};
//定义结构体变量 led变量
struct  mynewled_dev    mynewled;



//加载
static int __init mynewled_init(void)
{   
        u32 val = 0;
        int ret = 0;   
        printk("This is mynewled's init\r\n");
	/* 初始化LED */
	/* 1、寄存器地址映射 
	利用ioremap函数将物理寄存器地址转换为虚拟寄存器的地址
	*/
  	IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
	SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
  	SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
	GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
	GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

	/* 2、使能GPIO1时钟 */
	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3 << 26);	/* 清楚以前的设置 */
	val |= (3 << 26);	/* 设置新值 */
	writel(val, IMX6U_CCM_CCGR1);

	/* 3、设置GPIO1_IO03的复用功能,将其复用为
	 *    GPIO1_IO03,最后设置IO属性。
	 */
	writel(5, SW_MUX_GPIO1_IO03);
	
	/*寄存器SW_PAD_GPIO1_IO03设置IO属性
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
	 */
	writel(0x10B0, SW_PAD_GPIO1_IO03);

	/* 4、设置GPIO1_IO03为输出功能 */
	val = readl(GPIO1_GDIR);
	val &= ~(1 << 3);	/* 清除以前的设置 */
	val |= (1 << 3);	/* 设置为输出 */
	writel(val, GPIO1_GDIR);

	/* 5、默认关闭LED */
	val = readl(GPIO1_DR);
	val |= (1 << 3);	
	writel(val, GPIO1_DR);

    //2. 注册字符设备
    if(mynewled.major){//如果给定主设备号
        mynewled.devid  =   MKDEV(mynewled.major, 0); //设备号=主设备号和次设备号,高低位
        /*int register_chrdev_region(dev_t from, unsigned count, const char *name)
        三个参数分别为 参数 from 是要申请的起始设备号,也就是给定的设备号;参数 count 是要申请的数量,一
般都是一个;参数 name 是设备名字。
        */
        ret =   register_chrdev_region(mynewled.devid, 1, MYNEWLED_NAME);
    }else
    {
        /* 如果没有给定主设备号 
        通过函数动态分配一个设备号
        int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
			const char *name)
        这边四个参数,第一个是结构体指针(要注意)还是设备号,次设备号,需要申请的数量,名字
        */
        ret =   alloc_chrdev_region(&mynewled.devid, 0, 1, MYNEWLED_NAME);
        mynewled.major  =   MAJOR(mynewled.devid);
        mynewled.minor  =   MINOR(mynewled.devid);
    }
    if (ret < 0) //如果分配失败,ret获取的
    {
            printk("mynewled chrdev_region error! \r\n");
            return -1;
    }
    printk("mynewled major=%d,minor=%d\r\n",mynewled.major,mynewled.minor);
    
/*
注册字符设备
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合。
*/
//初始化cdev
    mynewled.cdev.owner =   THIS_MODULE;
    cdev_init(&mynewled.cdev, &mynewled_fops); //初始化字符设备
    cdev_add(&mynewled.cdev, mynewled.devid, 1); //添加字符设备,设备号和申请数量
    

	/*udev,mdev自动创建字符节点设备文件
	struct device *device_create(struct class *class, 
	struct device *parent,   父设备,一般没有写NULL
	dev_t devt,       设备号
	void *drvdata,      设备可能会使用的一些数据,一般为 NULL
	const char *fmt, ...)   设备名字
	*/
	mynewled.class	=	class_create(THIS_MODULE,	MYNEWLED_NAME);
	mynewled.device	=	device_create(mynewled.class,	NULL,	mynewled.devid,	NULL,	MYNEWLED_NAME);

    
    return 0;
}
//卸载
static void __exit mynewled_exit(void)
{
    printk("This is mynewled's exit\r\n");

	/* 取消映射 */
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);
    /*
    注销设备号
    void unregister_chrdev_region(dev_t from, unsigned count)
    参数设备号,申请的数量=卸载的数量
    */
    cdev_del(&mynewled.cdev);//
    unregister_chrdev_region(mynewled.devid, 1);

	device_destroy(mynewled.class,	mynewled.devid);
	class_destroy(mynewled.class);
    
}

/*
首先是驱动模块的加载和卸载
*/
module_init(mynewled_init);
module_exit(mynewled_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("CJL");

 

posted @ 2022-11-25 16:03  祈愿树下  阅读(64)  评论(0编辑  收藏  举报
// 侧边栏目录 // https://blog-static.cnblogs.com/files/douzujun/marvin.nav.my1502.css