ZYNQ嵌入式Linux——main 03 Linux第一个驱动的现代化改造
register_chrdev 和unregister_chrdev 这两个函数是老版本驱动使用的函数,现在新的字符设备驱动已经不再使用这两个函数,而是使用 Linux 内核推荐的新字符设备驱动 API 函数。
一 之前的驱动存在哪些问题
1、设备号需要提前写,而且需要写死,导致不够灵活:
register_chrdev(
LED_MAJOR,
LED_NAME,
&led_fops);
之前写的注册字符设备函数如上,仅提供一个主设备号,设备名,这样会浪费设备号,并且需要事先确定哪个主设备没用。
2、需要查找设备号,手动创建节点,不方便。
二 新驱动的几个先决概念:
学习几个概念:
1,设备号:
一个32bit的无符号整数,其中,12bit的主设备号,20bit的次设备号。
通常,主设备号用于标识和硬件设备关联的驱动程序,一个主设备号也可以被多个设备驱动程序共享。
比方说硬盘的快速模式和慢速模式,就是一个主设备的俩次设备。
如何查看主设备号分配:cat /proc/devices
dev_t这个类型就是专指设备号,定义在/include/linux/types.h下。
常用法如下:
int MAJOR(dev_t dev) //获得dev的主设备号
int MINOR(dev_t dev) //获得dev的次设备号
dev_t MKDEV(unsignde int major,unsigned int minor) //由主次设备号获得dev_t数据的宏。
dev_t dev = MKDEV(int major,int minor);//两个整数变成dev_t
unsigned major = MAJOR(dev);//查看dev_t的主设备号
unsigned minor = MINOR(dev);
分配方式1,直接写死:
eg:
register_chrdev(
LED_MAJOR,
LED_NAME,
&led_fops);
我们看下函数声明:
/include/linux/fs.h:
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
{
return __register_chrdev(major, 0, 256, name, fops);
}
extern int __register_chrdev(unsigned int major, unsigned int baseminor,
unsigned int count, const char *name,
const struct file_operations *fops);
显然,这个函数会从主设备号开始,占用256个次设备号:
分配方式2,动态分配:(优选)
fs/char_dev.c:
int alloc_chrdev_region(
dev_t *dev,
unsigned int firstminor, //指定要分配的第一个次设备号
unsigned int count, //需要分配多少设备号
char *name) //设备名字
//eg:
ret = alloc_chrdev_region(
&newchrled.devid,
0, //指定要分配的第一个次设备号。
1,
"lednew")
这个函数注册成功后,会把指针指向的dev给修改好。下面看下定义:
__register_chrdev_region的函数比较长,总之,如果主设备号是0,就是尝试注册到成功为止。
如果非0,就是走手动注册路线,如果这次失败就失败。
如果已经到了major上限,则返回error。
fs/char_dev.c:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}
分配方式3,静态分配(需要指定特定设备号):
如果指定的设备号分配失败(IS_ERR(cd)),就输出PTR_ERR(cd)(非0)
int register_chrdev_region(
dev_t first,
unsigned int count,
char *name);
//eg:
ret = register_chrdev_region(
devid, //dev_t类型的结构体
1,
"lednew");
来看定义:
fs/char_dev.c:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
{
struct char_device_struct *cd;
dev_t to = from + count;
dev_t n, next;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
cd = __register_chrdev_region(MAJOR(n), MINOR(n),
next - n, name);
if (IS_ERR(cd))
goto fail;
}
return 0;
fail:
to = n;
for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
return PTR_ERR(cd);
}
2,内核驱动结构体cdev:
1、cdev定义
如下:
linux/cdev.h:
//cdev的定义
struct cdev {
struct kobject kobj; //内嵌的内核对象
struct module *owner; //该字符设备所在的内核模块的对象指针.
const struct file_operations *ops;//该结构描述了字符设备所能实现的方法,即file_operations
struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
unsigned int count; //隶属于同一主设备号的次设备号的个数
};
2、cdev初始化方法
先初始化:
/fs/char_dev.c:
定义:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops;
}
示例:
static struct cdev led_cdev;//声明一个static的cdev结构体
cdev_init(&led_cdev, &led_fops);//用cdev_init函数进行初始化
cdev_add:向linux系统添加字符设备
int cdev_add(
struct cdev *p, //cdev结构体
dev_t dev, //设备号
unsigned count); //要添加的设备数量
注销字符设备之后要释放掉设备号,设备号释放函数如下:
void unregister_chrdev_region(dev_t from, unsigned count)
此函数有两个参数:
from:要释放的设备号。
count:表示从 from 开始,要释放的设备号数量。
3,自动创建device节点
我们在建立好file_operations这个类之后,使用device_create函数创建设备:
用device_destroy函数清理。如下:
struct device *device_create(
struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...);
//参数 class 就是设备要创建哪个类下面
//参数 parent 是父设备,一般设置为NULL
//参数 devt 是设备号
//参数 drvdata 是设备可能会使用的一些数据,一般为NULL
// 参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx 这个设备文件。返回值就是创建好的设备。
void device_destroy(struct class *class, dev_t devt);
//参数 class 就是设备要创建哪个类下面
//参数 devt 是设备号
注意到有个class,如何创建?
利用class_create(/include/linux/device/class.h)进行创建:
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
使用示例如下:
/* 设备驱动相关结构体*/
static struct cdev led_cdev;
static dev_t led_dev_t;
static struct class *led_dev_class;
static struct device *led_device;
........
/* 4-1 创建类*/
led_dev_class = class_create(THIS_MODULE, LED_NAME);
if (IS_ERR(led_dev_class))
{
ret = PTR_ERR(led_dev_class);
goto out3;
}
/* 4-2 创建设备节点*/
led_device = device_create(
led_dev_class,
NULL,
led_dev_t,
NULL,
LED_NAME);
if (IS_ERR(led_device))
{
ret = PTR_ERR(led_device);
goto out4;
}
.......
三 修改上节的函数
先对齐思路,即新式驱动如何做:
- 要注册驱动,需要用cdev_add函数。
- cdev_add函数,需要一个cdev结构体、设备号、数量
- cdev结构体的初始化,需要cdev_init函数,init的时候,需要设备号、fileop。
- 设备号,需要分配,两种方法:
- 动态:alloc_chrdev_region()
- 静态:register_chrdev_region()
register_chrdev_region直接将Major注册进入,而 alloc_chrdev_region从Major = 0 开始,逐个查找设备号,直到找到一个闲置的设备号,并将其注册进去
- **udev(mdev)**来实现设备文件的自动创建
一:修改点
修改点1:头文件
增加了这三个头文件:
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/cdev.h>
修改点2:声明几个static变量:
/* 设备驱动相关结构体*/
static struct cdev led_cdev;
static dev_t led_dev_t;
static struct class *led_dev_class;
static struct device *led_device;
修改点3:init函数中的注册字符设备部分
原来:
ret = register_chrdev(LED_MAJOR, LED_NAME, &led_fops);
if (0 > ret)
{
printk(KERN_ERR "Register LED driver failed!\r\n");
return ret;
现在:
/* 1 申请设备号 */
ret = alloc_chrdev_region(
&led_dev_t,
0,
1,
LED_NAME);
if (ret)
goto out1;
printk("lednew major=%d,minor=%d\r\n",
MAJOR(led_dev_t),
MINOR(led_dev_t));
/* 2 初始化cdev */
led_cdev.owner = THIS_MODULE;
cdev_init(&led_cdev, &led_fops);
/* 3 向内核添加cdev */
ret = cdev_add(&led_cdev, led_dev_t, 1);
if (ret)
goto out2;
/* 4-1 创建类*/
led_dev_class = class_create(THIS_MODULE, LED_NAME);
if (IS_ERR(led_dev_class))
{
ret = PTR_ERR(led_dev_class);
goto out3;
}
/* 4-2 创建设备节点*/
led_device = device_create(
led_dev_class,
NULL,
led_dev_t,
NULL,
LED_NAME);
if (IS_ERR(led_device))
{
ret = PTR_ERR(led_device);
goto out4;
}
return 0;
out4:
class_destroy(led_dev_class);
out3:
cdev_del(&led_cdev);
out2:
unregister_chrdev_region(led_dev_t, 1);
out1:
iounmap(data_addr);
iounmap(dirm_addr);
iounmap(outen_addr);
iounmap(intdis_addr);
iounmap(aper_clk_ctrl_addr);
return ret;
二:源码
// #include <linux/typlinuxkes.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 <linux/uaccess.h>
#include <asm/io.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/cdev.h>
// #define LED_MAJOR 200 /* 主设备号 */
#define LED_NAME "lednew" /* 设备名字 */
/* GPIO 相关寄存器地址定义 */
#define ZYNQ_GPIO_REG_BASE 0xE000A000
#define DATA_OFFSET 0x00000040
#define DIRM_OFFSET 0x00000204
#define OUTEN_OFFSET 0x00000208
#define INTDIS_OFFSET 0x00000214
#define APER_CLK_CTRL 0xF800012C
/* 映射后的寄存器虚拟地址指针 */
static void __iomem *data_addr;
static void __iomem *dirm_addr;
static void __iomem *outen_addr;
static void __iomem *intdis_addr;
static void __iomem *aper_clk_ctrl_addr;
/* 设备驱动相关结构体*/
static struct cdev led_cdev;
static dev_t led_dev_t;
static struct class *led_dev_class;
static struct device *led_device;
static int led_open(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t led_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *offt)
{
return 0;
}
static ssize_t led_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
{
int ret;
int val;
char kern_buf[1];
ret = copy_from_user(kern_buf, buf, cnt); // 得到应用层传递过来的数据
if (0 > ret)
{
printk(KERN_ERR "kernel write failed!\r\n");
return -EFAULT;
}
val = readl(data_addr);
if (0 == kern_buf[0])
val &= ~(0x1U << 7); // 如果传递过来的数据是 0 则关闭 led
else if (1 == kern_buf[0])
val |= (0x1U << 7); // 如果传递过来的数据是 1 则点亮 led
writel(val, data_addr);
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,
};
static int __init led_init(void)
{
u32 val;
int ret;
/* 1.寄存器地址映射 */
data_addr = ioremap(ZYNQ_GPIO_REG_BASE + DATA_OFFSET, 4);
dirm_addr = ioremap(ZYNQ_GPIO_REG_BASE + DIRM_OFFSET, 4);
outen_addr = ioremap(ZYNQ_GPIO_REG_BASE + OUTEN_OFFSET, 4);
intdis_addr = ioremap(ZYNQ_GPIO_REG_BASE + INTDIS_OFFSET, 4);
aper_clk_ctrl_addr = ioremap(APER_CLK_CTRL, 4);
/* 2.使能 GPIO 时钟 */
val = readl(aper_clk_ctrl_addr);
val |= (0x1U << 22);
writel(val, aper_clk_ctrl_addr);
/* 3.关闭中断功能 */
val |= (0x1U << 7);
writel(val, intdis_addr);
/* 4.设置 GPIO 为输出功能 */
val = readl(dirm_addr);
val |= (0x1U << 7);
writel(val, dirm_addr);
/* 5.使能 GPIO 输出功能 */
val = readl(outen_addr);
val |= (0x1U << 7);
writel(val, outen_addr);
/* 6.默认关闭 LED */
val = readl(data_addr);
val &= ~(0x1U << 7);
writel(val, data_addr);
/* 7.注册字符设备驱动 */
/* 1 申请设备号 */
ret = alloc_chrdev_region(
&led_dev_t,
0,
1,
LED_NAME);
if (ret)
goto out1;
printk("lednew major=%d,minor=%d\r\n",
MAJOR(led_dev_t),
MINOR(led_dev_t));
/* 2 初始化cdev */
led_cdev.owner = THIS_MODULE;
cdev_init(&led_cdev, &led_fops);
/* 3 向内核添加cdev */
ret = cdev_add(&led_cdev, led_dev_t, 1);
if (ret)
goto out2;
/* 4-1 创建类*/
led_dev_class = class_create(THIS_MODULE, LED_NAME);
if (IS_ERR(led_dev_class))
{
ret = PTR_ERR(led_dev_class);
goto out3;
}
/* 4-2 创建设备节点*/
led_device = device_create(
led_dev_class,
NULL,
led_dev_t,
NULL,
LED_NAME);
if (IS_ERR(led_device))
{
ret = PTR_ERR(led_device);
goto out4;
}
return 0;
out4:
class_destroy(led_dev_class);
out3:
cdev_del(&led_cdev);
out2:
unregister_chrdev_region(led_dev_t, 1);
out1:
iounmap(data_addr);
iounmap(dirm_addr);
iounmap(outen_addr);
iounmap(intdis_addr);
iounmap(aper_clk_ctrl_addr);
return ret;
}
static void __exit led_exit(void)
{
/* 1.1 卸载设备 */
// unregister_chrdev(LED_MAJOR, LED_NAME);
device_destroy(led_dev_class, led_dev_t);
/* 1.2 注销类 */
class_destroy(led_dev_class);
/* 1.3 删除cdev */
cdev_del(&led_cdev);
/* 1.4 注销设备号 */
unregister_chrdev_region(led_dev_t, 1);
/* 2.取消内存映射 */
iounmap(data_addr);
iounmap(dirm_addr);
iounmap(outen_addr);
iounmap(intdis_addr);
iounmap(aper_clk_ctrl_addr);
}
/* 驱动模块入口和出口函数注册 */
module_init(led_init);
module_exit(led_exit);
MODULE_AUTHOR("lyw <xx@qq.com>");
MODULE_DESCRIPTION("Lyw ZYNQ GPIO LED Driver");
MODULE_LICENSE("GPL");
四 编译一个可以实时加载的内核模块
make -C $(KERN_DIR) M=`pwd` modules
- -C参数:指定一个目录,在该目录中查找名为Makefile的文件,并执行那个makefile。
- M参数:在linux内核编译的过程中,M参数可以指定内核模块的源码所在的目录。
- modules参数:指示构建系统仅编译内核模块,而不编译整个内核。生成相应的.ko(内核对象)文件。
为了方便,我们写一个makefile,放在和led.c一个目录下面:Makefile:
KERN_DIR := /home/lyw/xilinx-linux/linux-kernel-driver-develop/linux-xlnx-xlnx_rebase_v5.15_LTS_2022.1
obj-m := led.o
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` clean
make编译成功后,就会生成一个名为“led.ko”的驱动模块文件
五 运行测试
- 将上一小节编译出来的 led.ko 和 ledApp 这两个文件拷贝到 NFS 共享目录下根文件系统的/lib/modules/5.4.0-xilinx 文件夹下
- 启动开发板进入这个目录加载驱动
depmod //第一次加载驱动的时候需要运行此命令
modprobe led.ko //加载驱动
- 创建“/dev/led”设备节点,命令如下:
mknod /dev/led c 200 0
这里面就是创建字符设备驱动节点,
c代表字符设备,200代表主设备号,0代表次设备号。
必须先确保内核中有相应的驱动程序来支持这个设备号,并且该驱动程序已经加载到内核中!才可以成功创建这个节点。
- 测试点亮小灯:
编写一个测试APP:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
int fd, ret;
unsigned char buf[1];
if (3 != argc)
{
printf("Usage:\n"
"\t./ledApp /dev/led 1 @ close LED\n"
"\t./ledApp /dev/led 0 @ open LED\n");
return -1;
}
/* 打开设备 */
fd = open(argv[1], O_RDWR);
if (0 > fd)
{
printf("file %s open failed!\r\n", argv[1]);
return -1;
}
/* 将字符串转换为 int 型数据 */
buf[0] = atoi(argv[2]);
/* 向驱动写入数据 */
ret = write(fd, buf, sizeof(buf));
if (0 > ret)
{
printf("LED Control Failed!\r\n");
close(fd);
return -1;
}
/* 关闭设备 */
close(fd);
return 0;
}
编译出来:
$CC ledApp.c -o ledApp
//拷贝到开发板root的home文件夹下面
cp ledApp ~/linux/nfs/rootfs/home/root/
进入开发板shell
//点亮 LED 灯
./ledApp /dev/led 1
//熄灭 LED 灯
./ledApp /dev/led 0
- 卸载驱动:
rmmod led.ko