文章目录
- Linux驱动程序分类
- Linux应用程序和驱动程序的关系
- 简单的测试驱动程序
- 在petalinux中添加LED驱动
- 新字符设备驱动
Linux驱动程序分类
驱动程序分为字符设备驱动、块设备驱动和网络设备驱动。
字符设备是按字节访问的设备,比如以一个字节收发数据的串口,字符设备在Linux外设中占比最大。
块设备的特点是按一定格式存取的数据,具体的格式由文件系统决定。块设备以存储设备为主,存储设备的特点是以存储块为基础,因此得名块设备。
网络设备不同于上面两种,应用程序和网络设备驱动之间的通信由库和内核提供的一套数据包传输函数替代了open()、read()、write()等函数。
Linux应用程序和驱动程序的关系
(1)应用程序调用库函数提供的open()函数打开某个设备文件,该设备文件是在驱动加载成功之后在目录/dev中生成的,是应用程序调用相应硬件的入口。
(2)库根据open()函数的输入参数引起CPU异常进入内核,系统调用处于内核空间,应用程序无法直接访问,因此需要陷入到内核,方法就是软中断,陷入内核后还要指定系统调用号;
(3)内核的异常处理函数根据输入参数找到相应的驱动程序,返回文件句柄给库,库函数再返回给应用程序;
(4)应用程序再使用得到的文件句柄调用write()、read()等函数发出控制指令;
(5)库根据write()、read()等函数的输入参数引起CPU异常,进入内核;
(6)内核的异常处理函数根据输入参数调用相应的驱动程序执行相应的操作。
Linux应用程序调用驱动程序的步骤如下图所示。
应用程序中涉及到的open()、read()、write()等是由库提供的系统调用,通过执行某条指令引发异常进入内核,是应用程序操作硬件的途径。应用程序执行系统调用后进入内核,然后会使用驱动程序中对应的函数,驱动程序中的open()、read()、write()等函数是需要驱动开发人员实现的。应用程序运行于用户空间,驱动程序运行于内核空间,Linux系统可以通过MMU限制应用程序运行在某个内存块中,以避免这个应用程序出错导致整个系统崩溃,运行于内核空间的驱动程序是系统的一部分,驱动程序出错有可能牵连整个系统。
简单的测试驱动程序
在进行LED驱动开发之前,先使用下面的代码简单测试一下。
#include <linux/module.h>
static int __init chardev_init(void)
{
printk("Hello!\n");
return 0;
}
static void __exit chardev_exit(void)
{
printk("GoodBye!\n");
}
module_init(chardev_init);
module_exit(chardev_exit);
MODULE_LICENSE("GPL");
如果使用Linux内核编译上面的C文件,需要自己写Makefile,然后编译出驱动文件到7020开发板上验证,验证的结果如下图所示。
提示这个驱动是无效的模块格式,可能ZYNQ开发板就需要使用petalinux这样特定的工具进行开发,下面来看具体流程。
首先在petalinux的安装路径下设置环境变量。
source /opt/pkg/petalinux/settings.sh
进入到定制系统的根目录下,使用下面的命令添加字符设备驱动。
petalinux-create -t modules -n chardev
在当前路径下的/project-spec/meta-user/recipes-modules/下生成了一个名为chardev的文件夹,该文件夹下有以下三个文件,其中.c文件就是需要写入驱动代码的文件。
Makefile在创建工程的时候已经创建好了,里面的内容如下图所示,也不需要修改。
在C文件中写入驱动代码后,返回到自定义的/zynq7020目录下,使用下面的命令进行编译。
petalinux-build -c chardev
编译完成后的信息打印如下图所示。
由于编译成的驱动文件存放路径比较难找,因此直接在搜索栏中直接搜索驱动的名称就会出现,但是文件夹必须打开到自定义的工程的这一层才能搜索到。
可以右键该文件打开文件的具体存在位置为/opt/pkg/petalinux/zynq7020/build/tmp/sysroots-components/plnx_zynq7/chardev/lib /modules/4.14.0-xilinx-v2018.3/extra。
接下来就可以在开发板上验证该驱动了,验证的结果如下图所示。
在petalinux中添加LED驱动
同上面的示例,先在petalinux的安装路径下设置环境变量。
source /opt/pkg/petalinux/settings.sh
然后进入到定制系统的根目录下,使用下面的命令添加驱动。
petalinux-create -t modules -n psled1-driver
需要注意的是,驱动文件命名不能使用下划线,而要使用"-"代替。
创建成功以后,打印的消息提示创建的模块在当前路径下的/project-spec/meta-user/recipes-modules/psled1-driver中,进到这个目录下。
一步步进到最终的目录下,在files文件夹下的.c文件就是要写入驱动代码的地方,在里面键入下面的代码。
//该代码来自ZYNQ教程,教程在文末给出
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/ide.h>
#include <linux/types.h>
/* 驱动名称 */
#define DEVICE_NAME "ps_led1"
/* 驱动主设备号 */
#define GPIO_LED_MAJOR 200
/* gpio寄存器虚拟地址 */
static unsigned int gpio_add_minor;
/* gpio寄存器物理基地址 */
#define GPIO_BASE 0xE000A000
/* gpio寄存器所占空间大小 */
#define GPIO_SIZE 0x1000
/* gpio方向寄存器 */
#define GPIO_DIRM_0 (unsigned int *)(0xE000A204 - GPIO_BASE + gpio_add_minor)
/* gpio使能寄存器 */
#define GPIO_OEN_0 (unsigned int *)(0xE000A208 - GPIO_BASE + gpio_add_minor)
/* gpio控制寄存器 */
#define GPIO_DATA_0 (unsigned int *)(0xE000A040 - GPIO_BASE + gpio_add_minor)
/* 时钟使能寄存器虚拟地址 */
static unsigned int clk_add_minor;
/* 时钟使能寄存器物理基地址 */
#define CLK_BASE 0xF8000000
/* 时钟使能寄存器所占空间大小 */
#define CLK_SIZE 0x1000
/* AMBA外设时钟使能寄存器 */
#define APER_CLK_CTRL (unsigned int *)(0xF800012C - CLK_BASE + clk_add_minor)
/* open函数实现, 对应到Linux系统调用函数的open函数 */
static int gpio_leds_open(struct inode *inode_p, struct file *file_p)
{
/* 把需要修改的物理地址映射到虚拟地址 */
gpio_add_minor = (unsigned int)ioremap(GPIO_BASE, GPIO_SIZE);
clk_add_minor = (unsigned int)ioremap(CLK_BASE, CLK_SIZE);
/* MIO_0时钟使能 */
*APER_CLK_CTRL |= 0x00400000;
/* MIO_0设置成输出 */
*GPIO_DIRM_0 |= 0x00000001;
/* MIO_0使能 */
*GPIO_OEN_0 |= 0x00000001;
printk("gpio_test module open\n");
return 0;
}
/* write函数实现, 对应到Linux系统调用函数的write函数 */
static ssize_t gpio_leds_write(struct file *file_p, const char __user *buf, size_t len, loff_t *loff_t_p)
{
int rst;
char writeBuf[5] = {0};
printk("gpio_test module write\n");
rst = copy_from_user(writeBuf, buf, len);
if(0 != rst)
{
return -1;
}
if(1 != len)
{
printk("gpio_test len err\n");
return -2;
}
if(1 == writeBuf[0])
{
*GPIO_DATA_0 &= 0xFFFFFFFE;
printk("gpio_test ON\n");
}
else if(0 == writeBuf[0])
{
*GPIO_DATA_0 |= 0x00000001;
printk("gpio_test OFF\n");
}
else
{
printk("gpio_test para err\n");
return -3;
}
return 0;
}
/* release函数实现, 对应到Linux系统调用函数的close函数 */
static int gpio_leds_release(struct inode *inode_p, struct file *file_p)
{
printk("gpio_test module release\n");
return 0;
}
/* file_operations结构体声明, 是上面open、write实现函数与系统调用函数对应的关键 */
static struct file_operations gpio_leds_fops = {
.owner = THIS_MODULE,
.open = gpio_leds_open,
.write = gpio_leds_write,
.release = gpio_leds_release,
};
/* 模块加载时会调用的函数 */
static int __init gpio_led_init(void)
{
int ret;
/* 通过模块主设备号、名称、模块带有的功能函数(及file_operations结构体)来注册模块 */
ret = register_chrdev(GPIO_LED_MAJOR, DEVICE_NAME, &gpio_leds_fops);
if (ret < 0)
{
printk("gpio_led_dev_init_error\n");
return ret;
}
else
{
/* 注册成功 */
printk("gpio_led_dev_init_ok\n");
}
return 0;
}
/* 卸载模块 */
static void __exit gpio_led_exit(void)
{
/* 释放对虚拟地址的占用 */
iounmap((unsigned int *)gpio_add_minor);
iounmap((unsigned int *)clk_add_minor);
/* 注销模块, 释放模块对这个设备号和名称的占用 */
unregister_chrdev(GPIO_LED_MAJOR, DEVICE_NAME);
printk("gpio_led_dev_exit_ok\n");
}
/* 标记加载、卸载函数 */
module_init(gpio_led_init);
module_exit(gpio_led_exit);
/* 驱动描述信息 */
MODULE_AUTHOR("Alinx");
MODULE_ALIAS("gpio_led");
MODULE_DESCRIPTION("GPIO LED driver");
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL");
上面介绍的是直接编译驱动,也可以以图形化的形式进行编译,返回到自定义的/zynq7020目录下,输入下面的命令配置根文件系统。
petalinux-config -c rootfs
在弹出的图形化配置窗口中选择modules进入子菜单中。
按Y键将该驱动包括进来,然后保存退出。
根文件系统就配置成功了,然后使用petalinux-build命令编译该工程。
编译成功后打印下面的信息。
在/zynq7020目录下搜索驱动文件,如下图所示。
右键该文件选择打开文件存放位置,其存放在/zynq7020/build/tmp/sysroots-components/plnx_zynq7/psled1-driver/lib/modules/4.14.0-xilinx-v2018.3/extra,还是比较难找的,所以以后直接在工程目录下搜索即可。
在开发板上加载驱动,可以看到相应的设备号已经出现了。
使用下面的命令创建字符设备文件,指定主设备号和次设备号,设备文件名称为psled1,之后写应用程序的时候要使用该名称来操作字符设备。
mknod /dev/psled1 c 200 0
创建设备文件成功之后在/dev目录下就可以看到新添加的设备,如下图所示。
接下来写一个应用端的测试程序,用来传入数据点亮或者熄灭LED,程序如下。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd;
int status;
fd = open("/dev/psled1", O_RDWR);
if(fd < 0)
{
perror("open /dev/psled1 error!\n");
return fd;
}
status = atoi(argv[1]);
write(fd, &status, 1);
close(fd);
return 0;
}
将上面的程序通过交叉编译工具编译出适合在ARM平台运行的文件,将其发送到开发板验证,结果如下图所示。
如果采用下面的应用程序进行测试,LED将每隔一秒改变一下状态。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd;
int status;
fd = open("/dev/psled1", O_RDWR);
if(fd < 0)
{
perror("open /dev/psled1 error!\n");
return fd;
}
while(1)
{
status = 1;
write(fd, &status, 1);
sleep(1);
status = 0;
write(fd, &status, 1);
sleep(1);
}
close(fd);
return 0;
}
在开发板上执行后的结果如下图所示。
开发板上PS LED1的状态开始循环亮灭,如下动图所示。
终端里也是每隔一秒打印一次LED关闭或打开的状态。
卸载驱动程序后打印下面的信息。
新字符设备驱动
上面驱动代码中将设备号写死了,这样做有很多不便之处,因为编译驱动代码前需要查看目标系统中设备号的占用情况,驱动注册函数中仅有主设备号没有次设备号,这意味着一个设备会占用所有的次设备号,十分浪费资源。针对这些问题,Linux内核提出了新的字符设备注册方法,并由内核来管理设备号。
注册字符设备号的函数原型如下。
int register_chrdev_region(dev_t from,unsigned count,const char* name);
from :需要申请的起始设备号,取代了原有的主设备号和次设备号,在需要指定主次设备号的情况下,可以通过方法from = MKDEV(major,minor); 来实现。
count :需要申请的设备号个数。
name :设备名称。
在不需要指定主次设备号的情况下,设备号由内核来分配,传入指针来获取设备号,注册、注销设备号的函数原型如下。
int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char* name);
void unregister_chrdev_region(dev_t from,unsigned count);
dev :设备号指针,注册成功之后,主次设备号可以通过 major = MAJOR(dev); minor = MINOR(dev); 来获取。
baseminor :次设备号的起始地址。
新的注册方法使用cdev结构体来定义一个字符设备,cdev结构体如下。
struct cdev
{
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
cdev结构体的初始化函数原型如下。
void cdev_init(struct cdev *cdev,const struct file_operations *fops);
注册、注销字符设备的函数原型如下。
int cdev_add(struct cdev *cdev,dev_t dev,unsigned count);
void cdev_del(struct cdev *cdev);
类的创建和删除函数原型如下。
struct class *__class_create(struct module *owner, const char *name, struct lock_class_key *key);
void class_destroy(struct class *cls);
owner指定为THIS_MODULE,name是类的名称,第三个参数可以省略。
设备节点的创建和删除函数原型如下。
struct device *device_create(struct class *class, struct device *parent, dev_t devt, void * drvdata, const char *fmt, ...);
void device_destroy(struct class *class, dev_t devt);
class是通过class_create创建的类,parent是父设备,无则填NULL,devt是设备号,drvdata是设备可能用到的数据,没有则填NULL,fmt是设备名,创建成功后在/dev下生成。
下面代码使用的是新字符设备方法。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/ide.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#define DEVICE_NAME "psled1"
#define DEVID_COUNT 1 //设备号个数
#define DRIVE_COUNT 1 //驱动个数
/* gpio寄存器虚拟地址 */
static unsigned int gpio_add_minor;
/* gpio寄存器物理基地址 */
#define GPIO_BASE 0xE000A000
/* gpio寄存器所占空间大小 */
#define GPIO_SIZE 0x1000
/* gpio方向寄存器 */
#define GPIO_DIRM_0 (unsigned int *)(0xE000A204 - GPIO_BASE + gpio_add_minor)
/* gpio使能寄存器 */
#define GPIO_OEN_0 (unsigned int *)(0xE000A208 - GPIO_BASE + gpio_add_minor)
/* gpio控制寄存器 */
#define GPIO_DATA_0 (unsigned int *)(0xE000A040 - GPIO_BASE + gpio_add_minor)
/* 时钟使能寄存器虚拟地址 */
static unsigned int clk_add_minor;
/* 时钟使能寄存器物理基地址 */
#define CLK_BASE 0xF8000000
/* 时钟使能寄存器所占空间大小 */
#define CLK_SIZE 0x1000
/* AMBA外设时钟使能寄存器 */
#define APER_CLK_CTRL (unsigned int *)(0xF800012C - CLK_BASE + clk_add_minor)
#if 0
struct chardev
{
dev_t devid; //设备号
struct cdev cdev; //字符设备
struct class *class; //类
struct device *device; //设备节点
};
static struct chardev alinx_char = {
.cdev = {
.owner = THIS_MODULE,
},
};
#endif
dev_t devid; //设备号
struct cdev cdev; //字符设备
struct class *class; //类
struct device *device; //设备节点
static int gpio_leds_open(struct inode *inode_p, struct file *file_p)
{
gpio_add_minor = (unsigned int)ioremap(GPIO_BASE, GPIO_SIZE);
clk_add_minor = (unsigned int)ioremap(CLK_BASE, CLK_SIZE);
/* MIO_0时钟使能 */
*APER_CLK_CTRL |= 0x00400000;
/* MIO_0设置成输出 */
*GPIO_DIRM_0 |= 0x00000001;
/* MIO_0使能 */
*GPIO_OEN_0 |= 0x00000001;
printk("gpio_test module open\n");
return 0;
}
static ssize_t gpio_leds_write(struct file *file_p, const char __user *buf, size_t len, loff_t *loff_t_p)
{
int rst;
char writeBuf[5] = {0};
printk("gpio_test module write\n");
rst = copy_from_user(writeBuf, buf, len);
if(0 != rst)
{
return -1;
}
if(1 != len)
{
printk("gpio_test len err\n");
return -2;
}
if(1 == writeBuf[0])
{
*GPIO_DATA_0 &= 0xFFFFFFFE;
printk("gpio_test ON\n");
}
else if(0 == writeBuf[0])
{
*GPIO_DATA_0 |= 0x00000001;
printk("gpio_test OFF\n");
}
else
{
printk("gpio_test para err\n");
return -3;
}
return 0;
}
static int gpio_leds_release(struct inode *inode_p, struct file *file_p)
{
printk("gpio_test module release\n");
return 0;
}
static struct file_operations chardev_fops = {
.owner = THIS_MODULE,
.open = gpio_leds_open,
.write = gpio_leds_write,
.release = gpio_leds_release,
};
static int __init gpio_led_init(void)
{
#if 0
alloc_chrdev_region(&alinx_char.devid, 0, DEVID_COUNT, DEVICE_NAME); //注册设备号
cdev_init(&alinx_char.cdev, &chardev_fops); //初始化字符设备结构体
cdev_add(&alinx_char.cdev, alinx_char.devid, DRIVE_COUNT); //注册字符设备
alinx_char.class = class_create(THIS_MODULE, DEVICE_NAME); //创建类
if(IS_ERR(alinx_char.class))
{
return PTR_ERR(alinx_char.class);
}
alinx_char.device = device_create(alinx_char.class, NULL, alinx_char.devid, NULL, DEVICE_NAME); //创建设备节点
printk("alloc success, major = %d minor = %d\n",MAJOR(alinx_char.devid),MINOR(alinx_char.devid));
if (IS_ERR(alinx_char.device))
{
return PTR_ERR(alinx_char.device);
}
#endif
alloc_chrdev_region(&devid, 0, DEVID_COUNT, DEVICE_NAME); //注册设备号
cdev.owner = THIS_MODULE;
cdev_init(&cdev, &chardev_fops); //初始化字符设备结构体
cdev_add(&cdev, devid, DRIVE_COUNT); //注册字符设备
class = class_create(THIS_MODULE, DEVICE_NAME); //创建类
if(IS_ERR(class))
{
return PTR_ERR(class);
}
device = device_create(class, NULL, devid, NULL, DEVICE_NAME); //创建设备节点
printk("alloc success, major = %d minor = %d\n",MAJOR(devid),MINOR(devid));
if (IS_ERR(device))
{
return PTR_ERR(device);
}
return 0;
}
static void __exit gpio_led_exit(void)
{
iounmap((unsigned int *)gpio_add_minor);
iounmap((unsigned int *)clk_add_minor);
#if 0
cdev_del(&alinx_char.cdev); //注销字符设备
unregister_chrdev_region(alinx_char.devid, DEVID_COUNT); //注销设备号
device_destroy(alinx_char.class, alinx_char.devid); //删除设备节点
class_destroy(alinx_char.class); //删除类
#endif
cdev_del(&cdev); //注销字符设备
unregister_chrdev_region(devid, DEVID_COUNT); //注销设备号
device_destroy(class, devid); //删除设备节点
class_destroy(class); //删除类
printk("gpio_led_dev_exit_ok\n");
}
module_init(gpio_led_init);
module_exit(gpio_led_exit);
MODULE_LICENSE("GPL");
加载驱动之后,内核就会为设备指定主设备号和次设备号,不用再使用命中自己指定了。
参考文档:course_s6_ZYNQ那些事儿-Linux驱动篇V1.05