赛灵思 ZYNQ UltraScale+ MPSoC Petalinux驱动开发:Linux字符驱动开发

在这里插入图片描述

赛灵思 ZYNQ UltraScale+ MPSoC:Linux字符驱动开发

1、Linux驱动程序简介

Linux驱动可分为三类:字符设备驱动、块设备驱动、网络设备驱动。

字符设备是Linux驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写的设备,读写数据是分先后顺序的,常见的字符设备有:LED、按键、I2CSPILCD等。

块设备驱动的特点是它按一定格式存储数据,具体的格式由文件系统决定;通常以存储设备EMMCFLASHSD卡、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驱动相关加载/卸载相关命令:

驱动加载命令:insmodmodprobe

insmod:是最简单的驱动模块加载命令,用于加载指定的.ko模块;如:

	insomd led.ko

insmod命令的缺点是:不能解决模块的依赖关系,如key.ko驱动依赖led.ko,就必须先加载led.ko模块。

modprobe:modprobe会自动分析模块的依赖关系,将所需要的依赖模块加载到内核中;主要智能在提供了模块依赖性分析、错误检查、错误报告等。

驱动卸载命令:rmmodmodprobe -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、readwrite等具体的设备操作函数。再添加驱动描述信息:

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点亮和熄灭。

PSLED使用的是MIO41引脚;查看UG1085GPIO手册可知: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学习笔记汇总

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kevin的学习站

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值