目录
- 赛灵思 ZYNQ UltraScale+ MPSoC:Linux字符驱动开发
- 1、Linux驱动程序简介
- 2、Linux字符设备开发步骤
- 2.1、系统调用
- 2.2、驱动模块的加载与卸载
- 2.2.1、驱动加载/卸载方式:
- 2.2.2、驱动注册函数和卸载注册函数
- 2.2.3、字符设备注册与注销
- 2.2.4、实现设备操作函数
- 3、字符设备驱动实验
- 3.1、硬件环境
- 3.2、LED字符设备驱动编写
- 3.3、将驱动模块添加到系统中
- 3.4、Makefile单独编写驱动
- 3.5、编写应用程序
- 3.6、测试程序
赛灵思 ZYNQ UltraScale+ MPSoC:Linux字符驱动开发
1、Linux驱动程序简介
Linux驱动可分为三类:字符设备驱动、块设备驱动、网络设备驱动。
字符设备是
Linux
驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写的设备,读写数据是分先后顺序的,常见的字符设备有:LED
、按键、I2C
、SPI
、LCD
等。块设备驱动的特点是它按一定格式存储数据,具体的格式由文件系统决定;通常以存储设备
EMMC
、FLASH
、SD
卡、EEPROM
等。网络设备驱动与字符设备和块设备有很大区别,应用程序和网络设备驱动之间的驱动是由内核提供的一套数据包传输函数代替了
open()、read()、write()
函数;WiFi
、以太网等属于网络设备。
Linux应用程序对驱动程序的调用流程如下:
①、应用程序调用库函数提供的
open()
函数打开某个设备文件;②、库根据
open()
函数的输入参数引起CPU
异常,进入内核;③、
Linux
内核的异常处理函数根据输入参数找到相应的驱动程序,返回文件句柄给库,库函数再返回给应用程序;④、应用程序再使用得到的文件句柄调用
write()、read()
等函数发出控制指令;⑤、库根据
write()、read()
等函数的输入参数引起CPU
异常,进入内核;⑥、内核的异常处理函数根据输入参数调用相应的驱动程序执行相应的操作。
应用程序运行宇用户空间,驱动程序运行与内核空间。Linux
系统可以通过MMU
限制应用程序运行于某个内存块中,以避免这个应用程序出现错误导致整个系统崩溃。运行于内核空间的驱动程序属于系统的一部分。
2、Linux字符设备开发步骤
Linux
下实现设备驱动的步骤大致如下:
①、查看原理图以及数据手册,了解设备操作方法;
②、修改设备树文件;
③、套用与设备相近的框架,或找到内核中相似设备的驱动代码直接修改,实现驱动程序的初始化以及操作函数;
④、将驱动编译进内核或单独编译加载驱动;
⑤、编写应用程序测试驱动程序。
2.1、系统调用
系统调用即设备操作函数,是字符设备驱动的核心。在内核文件include/linux/fs.h
中定义了数据结构体 file_operations
,包括了所有内核驱动操作函数:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t,loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*mremap)(struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t,loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long,
unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *,loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct
pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
file_operation
结构体中有如下几个比较重要的、常用的函数:
owner
拥有该结构体的模块的指针,一般设置为THIS_MODULE
;
read
函数用于读取设备文件;
write
函数用于向设备文件写入(发送)数据;
poll
是个轮询函数,用于查询设备是否可以进行非阻塞的读写;
open
函数用于打开设备文件;
release
函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应。
2.2、驱动模块的加载与卸载
2.2.1、驱动加载/卸载方式:
Linux
驱动有两种运行方式:第一种将驱动编译进内核中,Linux
内核启动时就会自动运行驱动程序;第二种是将驱动编译成模块(Linux
下驱动模块扩展名为.ko
),当Linux
启动后使用insmod
命令加载驱动模块,这种方式方便调试。
Linux驱动相关加载/卸载相关命令:
驱动加载命令:
insmod
和modprobe
insmod
:是最简单的驱动模块加载命令,用于加载指定的.ko
模块;如:insomd led.ko
insmod
命令的缺点是:不能解决模块的依赖关系,如key.ko
驱动依赖led.ko
,就必须先加载led.ko
模块。
modprobe:modprobe
会自动分析模块的依赖关系,将所需要的依赖模块加载到内核中;主要智能在提供了模块依赖性分析、错误检查、错误报告等。驱动卸载命令:
rmmod
和modprobe -r
rmmod
:用于卸载指定驱动模块;如:rmmod led.ko
modprobe -r
:可用于卸载驱动模块,但需注意:使用modprobe -r
命令卸载驱动会将驱动所依赖的其他驱动模块一起卸载。总结:可使用
modprobe
命令加载驱动模块,使用rmmod
命令卸载驱动。
2.2.2、驱动注册函数和卸载注册函数
注册函数:module_init(xxx_init)
module_init
函数用来向Linux内核注册一个模块加载函数,参数xxx_init
即需要注册的具体函数;使用insmod
命令加载驱动模块时就会调用驱动注册函数。
卸载注册函数:module_exit(xxx_exit)
module_exit
函数用来向Linux内核注册模块卸载函数,参数xxx_exit
参数即具体的卸载函数,当调用rmmod
命令时,就会调用module_exit
函数。
字符设备驱动模块加载和卸载模板:
/* 驱动入口函数 */
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
return 0;
}
static void __exit xxx_exit(void)
{
/* 出口函数具体内容 */
}
module_init(xxx_init);
module_exit(xxx_exit);
2.2.3、字符设备注册与注销
对应字符设备,当驱动模块加载成功后需要注册字符设备,卸载驱动模块时也需要注销字符设备。字符设备的注册和注销函数原型如下:
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)
static inline void unregister_chardev(unsigned int major,const char *name)
register_chrdev
函数用于注册字符设备,参数如下:
major:
主设备号,Linux
下每个设备都有设备号,设备号分为主设备号和次设备号两部分;
name:
设备名称,指向一串字符串;
fops:
结构体file_operations
类型指针,指向设备的操作函数集合变量;
unregister_chardev
函数用户注销字符设备,参数如下:
major:
要注销的设备对应的主设备号;
name:
要注销的设备对应的设备号。
static struct file_operations test_fops;
static int __init xxx_init(void)
{
/* 入口函数具体内容 */
int retvalue = 0;
/* 注册字符设备驱动 */
retvalue = register_chardev(200,"chrtest",&test_fops);
if(retvalue < 0)
{
/* 字符设备注册失败,自行处理 */
}
return 0;
}
/* 驱动出口函数 */
static void __exit xxx_exit(void)
{
/* 注销字符设备驱动 */
unregister_chrdev(200,"chrtest");
}
/* 驱动入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
Linux设备号
每个设备文件都有主次设备号major
,主设备号是唯一的,每个主设备下有次设备号minor
,次设备号在这个设备下也是唯一的。在Linux
系统下使用如下命令可以查看已被注册的主设备号:
cat/proc/devices
2.2.4、实现设备操作函数
file_operations
结构体就是设备的具体操作函数;需要对该结构体变量进行初始化,即初始化其中的open、release、read
和write
等具体的设备操作函数。再添加驱动描述信息:
MODULE_LICENSE("GPL");
字符设备的驱动框架如下:
/* 驱动名称 */
#define DEVICE_NAME "gpio_leds"
/* 驱动主设备号 */
#define GPIO_LED_MAJOR 200
/* open 函数实现,对应到Linux系统调用函数的open函数 */
static int gpio_leds_open(struct inode_p, struct file *file_p)
{
return 0;
}
/* write函数实现,对应到Linux系统调用函数的write函数 */
static ssize_t gpio_leds_write(struct file *file_p, const char __user *buf, size_t led, loff_t *loff_t_p)
{
return 0;
}
/* release函数实现,对应到Linux系统调用函数的close函数 */
static int gpio_leds_release(struct inode *inode_p, struct file *file_p)
{
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;
/* 通过模块主设备号、名称、模块带有的功能函数来注册模块 */
ret = register_chrdev(GPIO_LED_MAJOR,DEVICE_NAME,&gpio_leds_fops);
if(ret < 0)
{
return ret;
}
else
{
}
return 0;
}
/* 卸载模块函数 */
static void __exit gpio_led_exit(void)
{
/* 注销模块,释放模块对这个设备号和名称的占用 */
unregister_chrdev(GPIO_LED_MAJOR,DEVICE_NAME);
}
/* 注册模块入口和出口函数 */
module_init(gpio_led_init);
module_exit(gpio_led_exit);
/* 添加LICENSE信息 */
MODULE_LICENSE("GPL");
3、字符设备驱动实验
3.1、硬件环境
通过编写板子上PS
端的MIO
实现LED
设备驱动,通过驱动可控制LED
点亮和熄灭。
PS
端LED
使用的是MIO41
引脚;查看UG1085
中GPIO
手册可知:GPIO
寄存器基地址为0xFF0A0000
,
由上图可知,MIO41
属于Bank1
,控制GPIO
需要三步:使能、设置方向、控制输出,Bank1
的使能寄存器OEN_1:0xFF0A0248
、方向寄存器DIRM_1:0xFF0A0244
、控制寄存器DATA_1:0xFF0A0044
。
3.2、LED字符设备驱动编写
led_drv.c
#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 "gpio_leds"
/* 驱动主设备号 */
#define GPIO_LED_MAJOR 200
/* gpio寄存器虚拟地址 */
static unsigned long gpio_add_minor;
/* gpio寄存器物理基地址 */
#define GPIO_BASE 0xFF0A0000
/* gpio寄存器所占空间大小 */
#define GPIO_SIZE 0x1000
/* gpio方向寄存器 */
#define GPIO_DIRM_1 (unsigned int *)(0x0000000000000244 + (unsigned long)gpio_add_minor)
/* gpio使能寄存器 */
#define GPIO_OEN_1 (unsigned int *)(0x0000000000000248 + (unsigned long)gpio_add_minor)
/* gpio控制寄存器 */
#define GPIO_DATA_1 (unsigned int *)(0x0000000000000044 + (unsigned long)gpio_add_minor)
/* open函数实现, 对应到Linux系统调用函数的open函数 */
static int gpio_leds_open(struct inode *inode_p, struct file *file_p)
{
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_1 |= 0x00004000;
printk("gpio_test ON *GPIO_DATA_1 = 0x%X\r\n", *GPIO_DATA_1);
}
else if(0 == writeBuf[0])
{
*GPIO_DATA_1 &= 0xFFFFBFFF;
printk("gpio_test OFF *GPIO_DATA_1 = 0x%X\r\n", *GPIO_DATA_1);
}
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_ng\n");
return ret;
}
else
{
/* 注册成功 */
printk("gpio_led_dev_init_ok\n");
/* 把需要修改的物理地址映射到虚拟地址 */
gpio_add_minor = ioremap_wc(GPIO_BASE, GPIO_SIZE);
printk("gpio_add_minor = 0x%lX\n", gpio_add_minor);
printk("GPIO_DIRM_1 = 0x%lX\n", (unsigned long)GPIO_DIRM_1);
printk("GPIO_OEN_1 = 0x%lX\n", (unsigned long)GPIO_OEN_1);
/* MIO_0设置成输出 */
*GPIO_DIRM_1 |= 0x00004000;
/* MIO_0使能 */
*GPIO_OEN_1 |= 0x00004000;
printk("*GPIO_DIRM_1 = 0x%X\n", *GPIO_DIRM_1);
printk("*GPIO_OEN_1 = 0x%X\n", *GPIO_OEN_1);
}
return 0;
}
/* 卸载模块 */
static void __exit gpio_led_exit(void)
{
*GPIO_OEN_1 &= 0xFFFFBFFF;
/* 释放对虚拟地址的占用 */
iounmap(gpio_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("kevin");
MODULE_ALIAS("gpio_led");
MODULE_DESCRIPTION("GPIO LED driver Test");
MODULE_VERSION("v1.0");
MODULE_LICENSE("GPL");
ioremap_wc()
函数用于把物理地址映射到虚拟地址。在Linux
中由于MMU
内存映射的关系,无法直接操作物理地址,而需把物理地址映射到虚拟地址上在操作对应的虚拟地址。ioremap_wc()
定义在头文件arch/arm/include/asm/io.h
中。#define ioremap(cookie.size) __arm_ioremap((cookie),(size),MT_DEVICE)
ZU5EV为64位SOC,在64位系统中使用ioremap_wc(),在32位系统中使用oremap()。
3.3、将驱动模块添加到系统中
1、在petalinux
工程目录下,使用如下命令添加新驱动:
petalinux-create -t modules --name lef_drc
2、将驱动内容拷贝到petalinux
模块下,在rootfs
中添加驱动
petalinux-config -c rootfs
3、编译系统
petalinux-build
3.4、Makefile单独编写驱动
新建Makefile
文件
modname: = ps-led
obj-m: = $(modname).o
PWD := $(shell pwd)
MAKE := make
/* 内核地址 */
KERNELDIR =
CROSS_COMPILE = aarch64-linux-gnu-ARCH=arm64
all:
$(MAKR) ARCH=$(ARCH) CROSS_COMPILE = $(CROSS_COMPILE) -C $(KERNELDIR)
M=$(PWD) modules
clean:
rm -rf $(modname).ko *.o *mod* \.*cmd *odule* .tmp_versions
.PHONY:all clean
使用make
编译生成.ko
文件
3.5、编写应用程序
led_app.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char **argv)
{
int fd;
char buf;
if(3 != argc)
{
printf("none para\n");
return -1;
}
fd = open(argv[1], O_RDWR);
if(fd < 0)
{
printf("Can't open file %s\r\n", argv[1]);
return -1;
}
if(!strcmp("on",argv[2]))
{
printf("ps_led1 on\n");
buf = 1;
write(fd, &buf, 1);
}
else if(!strcmp("off",argv[2]))
{
printf("ps_led1 off\n");
buf = 0;
write(fd, &buf, 1);
}
else
{
printf("wrong para\n");
return -2;
}
close(fd);
return 0;
}
编译得到可执行文件:led_app
3.6、测试程序
1、将可执行文件添加到Linux系统中
2、加载驱动模块 insmod new-led-drv.ko
3、创建设备文件:
/* 模板:mknod /dev/xxx type major minor */
mknod /dev/ps-led c 200 0
3、运行应用程序:./led_app
printf("请关注微信公众号:Kevin的学习站,关于AUTSAR和自动驾驶嵌入式相关的文章!")
赛灵思-Zynq UltraScale+ MPSoC学习笔记汇总