linux驱动开发
字符设备驱动开发
初识
字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、 IIC、 SPI,LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动 。

在 Linux 中一切皆为文件,驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx” (xxx 是具体的驱动文件名字)的文件进行相应的操作即可实现对硬件的操作。
比如现在有个叫做/dev/led 的驱动文件,此文件是 led 灯的驱动文件。应用程
序使用 open 函数来打开文件/dev/led,使用完成以后使用 close 函数关闭/dev/led 这个文件。 open和 close 就是打开和关闭 led 驱动的函数,如果要点亮或关闭 led,那么就使用 write 函数来操作,也就是向此驱动写入数据,这个数据就是要关闭还是要打开 led 的控制参数。如果要获取led 灯的状态,就用 read 函数从驱动中读取相应的状态
open():打开驱动文件
wirte():写入数据控制灯的亮灭
read():读取灯的状态
close():关闭驱动函数
Linux程序运行在用户空间,而Linux 驱动属于内核的一部分,因此驱动运行于内核空间。 当我们在用户空间想要实现对内核的操作,比如使用 open 函数打开/dev/led 这个驱动,因为用户空间不能直接对内核进行操作,因此必须使用一个叫做 “系统调用”的方法来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作。 open、 close、 write 和 read 等这些函数是由 C 库提供的,在 Linux 系统中,系统调用作为 C 库的一部分。当我们调用 open 函数的时候流程如图

应用程序使用到的函数在具体驱动程序中都有与之对应的函数,
比如应用程序中调用了 open 这个函数,那么在驱动程序中也得有一个名为 open 的函数。每一个系统调用,在驱动中都有与之对应的一个驱动函数
驱动开发就是实现对应的函数
Linux 驱动有两种运行方式,第一种就是将驱动编译进 Linux 内核中,这样当 Linux 内核启动的时候就会自动运行驱动程序。第二种就是将驱动编译成模块(Linux 下模块扩展名为.ko),在Linux 内核启动以后使用“insmod”命令加载驱动模块。在调试驱动的时候一般都选择将其编译为模块,这样我们修改驱动以后只需要编译一下驱动代码即可,不需要编译整个 Linux 代码。
而且在调试的时候只需要加载或者卸载驱动模块即可,不需要重启整个系统。总之,将驱动编译为模块最大的好处就是方便开发,当驱动开发完成,确定没有问题以后就可以将驱动编译进Linux 内核中,当然也可以不编译进 Linux 内核中,具体看自己的需求。模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下:
module_init(xxx_init); //注册模块加载函数
module_exit(xxx_exit); //注册模块卸载函数初写
头文件
头文件路径前的 linux/ 表示这些头文件属于 Linux 内核源码,而不是标准 C 库
设备号
Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分
组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。 Linux 提供了一个名为 dev_t 的数据类型表示设备号,dev_t 定义在文件 include/linux/types.h 里面
其中高 12 位为主设备号, 低 20 位为次设备号。因此 Linux
系统中主设备号范围为 0~4095
static
确保驱动不对外暴露接口
C99结构体
声明并且定义
static struct file_operations chrdevbase_fops = { .owner = THIS_MODULE, // 明确指定成员,无需按顺序
.open = chrdevbase_open, // 可读性强
.read = chrdevbase_read,
.write = chrdevbase_write, .release = chrdevbase_release,
};传统
// 声明(仅告诉编译器类型)
static struct file_operations chrdevbase_fops;
// 定义(运行时赋值)
chrdevbase_fops.owner = THIS_MODULE;
chrdevbase_fops.open = chrdevbase_open; chrdevbase_fops.read = chrdevbase_read; // ... 其他成员赋值file_operations里是Linux内核定义的结构体,成员是很多函数指针,我们要规定函数指针指向自定义的函数
printf与printk
在 Linux 内核中没有 printf 这个函数。 printk 相当于 printf 的孪生兄妹, printf
运行在用户态, printk 运行在内核态。在内核中想要向控制台输出或显示一些内容,必须使用printk 这个函数。不同之处在于, printk 可以根据日志级别对消息进行分类,一共有 8 个消息级别,这 8 个消息级别定义在文件 include/linux/kern_levels.h 里面 ,0-7,默认是4
代码
1.数据流传
copy_to_user->硬件发送给用户,也就是读硬件
先把内核数据kerneldata放在缓冲区readbuf里面,再把缓冲区readbuf里面的内容发送给用户(函数参数列表里面会有变量)
copy_from_user->从硬件写入,写硬件
用户数据依然是在函数参数列表里拿,再放在内核缓冲区里面
module_init(xxx)//装载时会调用xxx函数
//你执行 insmod/modprobe 后,内核模块加载器会装载 .ko。
//module_init(char_device_init) 这个宏会把入口函数记录为该模块的初始化入口(等价于告诉内核“加载我时先调这个函数”)。
//内核在模块装载阶段调用这个入口,于是进入 char_device.c:76 的 char_device_init。
//在这个函数里你调用了 char_device.c:80 的 register_chrdev 完成字符设备注册
copy_to_user(buf,readbuf,cnt)
copy_from_user(writebuf,buf,cnt);
//内核空间
static char readbuf[100];//读缓冲区
static char writebuf[100];//写缓冲
//内核数据
static char kerneldata[]="让我受尽折磨!!";2.执行
加载完了就能操作 /dev/<驱动名>这个文件了
depmod //第一次加载驱动的时候需要运行此命令
modprobe led.ko //加载驱动
驱动加载成功以后创建“/dev/led”设备节点,命令如下:
mknod /dev/led c 200 0
驱动节点创建成功以后就可以使用 ledApp 软件来测试驱动是否工作正常,输入如下命令打
开 LED 灯:
./ledApp /dev/led 1 //打开 LED 灯
输入上述命令以后观察 I.MX6U-ALPHA 开发板上的红色 LED 灯是否点亮,如果点亮的话
说明驱动工作正常。在输入如下命令关闭 LED 灯:
./ledApp /dev/led 0 //关闭 LED 灯
输入上述命令以后观察 I.MX6U-ALPHA 开发板上的红色 LED 灯是否熄灭,如果熄灭的话
说明我们编写的 LED 驱动工作完全正常!至此,我们成功编写了第一个真正的 Linux 驱动设备
程序。
如果要卸载驱动的话输入如下命令即可:
rmmod led.ko3.执行流程
- 用户态调用 open、read、write、close
- 内核先交给 VFS
- VFS 再根据对象类型转给具体实现
#include<linux/types.h>
#include<linux/kernel.h>
#include<linux/delay.h>
#include<linux/ide.h>
#include<linux/init.h>
#include<linux/module.h>
#define CHAR_MAJOR 200
#define CHAR_NAME "char_device"
//内核空间
static char readbuf[100];//读缓冲区
static char writebuf[100];//写缓冲
//内核数据
static char kerneldata[]="让我受尽折磨!!";
static int char_device_open(struct inode *inode,struct file *file)
{
return 0;
}
static int char_device_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);
if(retvalue==0)
{
printk("kernel senddate ok!\r\n");
}
else{
printk("kernel senddata failed!\r\n");
}
return 0;
}
static ssize_t char_device_write(struct file *file,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 received data:%s\r\n",writebuf);
}
else
{
printk("kernel received failed!\r\n");
}
return 0;
}
static int char_device_release(struct inode *inode,struct file *file)
{
return 0;
}
static struct file_operations char_device_fops={
.owner=THIS_MODULE,
.open=char_device_open,
.read=char_device_read,
.write=char_device_write,
.release=char_device_release,
};
static int __init char_device_init(void)
{
int retvalue=0;
retvalue=register_chrdev(CHAR_MAJOR,CHAR_NAME,&char_device_fops);
if(retvalue<0)
{
printk("char device register failed\r\n");
}
printk("char_device init!\r\n");
return 0;
}
static void __exit char_device_exit(void)
{
unregister_chrdev(CHAR_MAJOR,CHAR_NAME);
printk("char_device exit!\r\n");
}
module_init(char_device_init);
module_exit(char_device_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ZLX");LED驱动开发
前置
真正的Linux驱动
要先实现MMU映射,用虚拟地址操控实际地址
//__iomem 是在编译的时候用的
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;/* 1、寄存器地址映射 */
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);//CCM_CCGR1_BASE将这个映射成IMX6U_CCM_CCGR1
val = readl(IMX6U_CCM_CCGR1);
/* 2、使能GPIO1时钟 */
writel(val, IMX6U_CCM_CCGR1);register_chrdev(LED_MAJOR, LED_NAME, &led_fops);把写的函数也指进去了
代码
#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 <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : led.c
作者 : 左忠凯
版本 : V1.0
描述 : LED驱动文件。
其他 : 无
论坛 : www.openedv.com
日志 : 初版V1.0 2019/1/30 左忠凯创建
***************************************************************/
#define LED_MAJOR 200 /* 主设备号 */
#define LED_NAME "led" /* 设备名字 */
#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 led_open(struct inode *inode, struct file *filp)
{
return 0;
}
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t led_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 led_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 led_release(struct inode *inode, struct file *filp)
{
return 0;
}
/* 设备操作函数 */
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.read = led_read,
.write = led_write,
.release = led_release,
};
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static int __init led_init(void)
{
int retvalue = 0;
u32 val = 0;
/* 初始化LED */
/* 1、寄存器地址映射 */
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);
/* 6、注册字符设备驱动 */
retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if(retvalue < 0){
printk("register chrdev failed!\r\n");
return -EIO;
}
return 0;
}
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit led_exit(void)
{
/* 取消映射 */
iounmap(IMX6U_CCM_CCGR1);
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
/* 注销字符设备驱动 */
unregister_chrdev(LED_MAJOR, LED_NAME);
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");
