基于petalinux内核源码编写字符设备驱动


前言

一个基础的petalinux工程,在配置工程的时候即可在图形界面进行kernel配置,然后编译出内核镜像,并且要在编译完整个工程后才有内核源码,且目录部分与传统Soc的SDK kernel部分不同。在驱动开发中,如果想要单独写驱动并在Xilinx平台上运行,该怎样操作呢?


一、字符设备基础

字符设备:是指只能按byte进行读写操作的设备,以字节为单位进行数据传输,不能随机读取设备中的某一数据、读取数据要按照先后数据。字符设备是面向流的设备,常见的字符设备有鼠标、键盘、串口、控制台和LED等。
一般每个字符设备或者块设备都会在/dev目录(可以是任意目录,这样是为了统一)下对应一个设备文件。linux用户层程序通过设备文件来使用驱动程序操作字符设备或块设备。

二、开发环境

petalinux版本:2022.2
kernel版本:5.15
开发板:ZCU106

三、编写驱动

1.内核目录

编译内核模块,最关键的是找到内核源码路径和选择好编译器,以上述版本为例,编译完petalinux工程后,生成的内核源码路径如下:

/home/lzw/petalinux/project/zcu106_demo/build/tmp/work/xilinx_zcu106-xilinx-linux/linux-xlnx/5.15.36+gitAUTOINC+19984dd147-r0/linux-xilinx_zcu106-standard-build

在这里插入图片描述

2.编写驱动

重要的结构体:Linux下一切皆是“文件”,struct file_operations中的成员函数会在用户层进行open(),read(),write(),close()等系统调用时最终被内核驱动调用。就像应用层写数据直接用系统调用的接口write(),不用关心对应驱动部分的write()是怎么实现的。下面列出一些常用的成员:

struct file_operations {
	struct module *owner;							//THIS_MODULE
	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 *);	//写入文件
	unsigned int (*poll) (struct file *, struct poll_table_struct *);	//轮询,查询是否可以非阻塞读写
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);	//32位ioctl
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);		//64位ioctl
	int (*mmap) (struct file *, struct vm_area_struct *);	//内存映射,帧缓冲用的多
	int (*open) (struct inode *, struct file *);			//打开设备
	int (*release) (struct inode *, struct file *);			//释放设备,对应close()
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);	//刷新数据
	int (*fasync) (int, struct file *, int);				//异步刷新数据
	...
};

搭建字符设备驱动框架,常用的几个接口如下:

static int __init test_init(void){}
static void __exit test_exit(void){}
module_init(test_init); //注册模块加载函数
module_exit(test_exit); //注册模块卸载函数

MODULE_LICENSE("GPL"); //添加模块 LICENSE 信息,必须加
MODULE_AUTHOR("xxx"); //添加模块作者信息,可不加

关于设备号注册有两种方法,以下分别介绍:
第一种:
一键注册字符设备(主设备号表示具体驱动,次设备号表示使用驱动的各个设备)
参数:主设备号、设备名、文件结构体
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops);

注销字符设备
参数:主设备号、设备名
static inline void unregister_chrdev(unsigned int major, const char *name);

这一种方式创建有两个弊端:
1.需要知道系统中有哪些设备号已经被占用了,可以通过cat /proc/devices查看
2.只有一个主设备号,次设备号的2^20-1范围全部丢弃。
所以更推荐用第二种的方式来编写。

第二种:
动态设备号申请函数:
dev:用于保存设备号
baseminor:次设备号起始地址,一般为0
count:申请数量
name:设备名
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);

静态设备号申请函数:
from-----指定申请的起始设备号
成功返回0,失败返回负数
int register_chrdev_region(dev_t from, unsigned count, const char *name);

设备号释放函数:
void unregister_chrdev_region(dev_t from, unsigned count);

字符设备和文件操作绑定:
int cdev_init(struct cdev *p, struct file_operations *fops);

注册字符设备:
int cdev_add(struct cdev *p, dev_t dev, unsigned count);

注销字符设备:
void cdev_del(struct cdev *p);


这样加载的驱动还不会在/dev目录下生成操作节点,需要用mknod来创建。
用法:mknod NAME TYPE [MAJOR MINOR]
例子:mknod test_dev c 211 0


如果想自动生成,还需要调用以下两个接口:
struct class * fclass = class_create(THIS_MODULE, "test_class");	//创建/sys/class/test_class
struct device * fdevice = device_create(fclass, NULL, cdev_num, NULL, "test_dev");	//创建/dev/test_dev

附上注销接口:
device_destroy(fclass,cdev_num);
class_destroy(fclass);

内核空间不能直接操作用户空间内存,所以数据交互需要调用接口:
内核空间的数据到用户空间的复制:
static inline long copy_to_user(void __user *to, const void *from, unsigned long n)

用户空间的数据到内核空间的复制:
static inline long copy_from_user(void *to,const void __user *from, unsigned long n)

了解了基本函数接口,就可以写一个基本的框架代码了。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/sched.h>
#include <linux/uaccess.h>

#define CHAR_MAJOR 200
#define CHAR_MINOR 0
#define CHAR_NAME "test_char"

struct cdev *cdev = NULL;
dev_t cdev_num;
struct class * fclass = NULL;
struct device * fdevice = NULL;
char swapbuf[100] = {"this is char_dev module test !"};

int char_open (struct inode *inode, struct file *file)
{
	printk("test_char open ~~~\n");
	return 0;
}

int char_close (struct inode *inode, struct file *file)
{
	printk("test_char close ~~~\n");
	return 0;
}

ssize_t char_read (struct file *file, char __user *buf, size_t size, loff_t *loff)
{
	int ret = 0;
	ret = copy_to_user(buf,swapbuf,sizeof(swapbuf));
	printk("char_read copy_to_user send buf = %s, ret = %d\n",swapbuf,ret);

	return ret;
}

ssize_t char_write (struct file *file, const char __user * buf, size_t size, loff_t *loff)
{
	int ret = 0;
	ret = copy_from_user(swapbuf,buf,size);
	printk("char_write copy_from_user recv buf = %s, ret = %d\n",buf,ret);

	return ret;
}

struct file_operations fops = {
	.owner = THIS_MODULE,
	.open = char_open,
	.read = char_read,
	.write = char_write,
	.release = char_close,
};


int __init char_init(void)
{
	int ret = 0;
	cdev_num = MKDEV(CHAR_MAJOR,CHAR_MINOR);
	ret = register_chrdev_region(cdev_num,1,CHAR_NAME);
	if(ret<0)
	{
		printk("test_char register_chrdev_region failed ~\n");
		return -1;
	}
	else
		printk("test_char register_chrdev_region success ~\n");

	cdev = cdev_alloc();
	if(cdev == NULL)
	{
		printk("test_char cdev_alloc failed ~\n");
		return -1;
	}
	else
		printk("test_char cdev_alloc success ~\n");

	cdev_init(cdev,&fops);

	ret = cdev_add(cdev,cdev_num,1);
	if(ret)
	{
		printk("test_char cdev_add failed ~\n");
		return -1;
	}
	else
		printk("test_char cdev_add success ~\n");

	fclass = class_create(THIS_MODULE, "test_class");//创建class
	if(fclass == NULL)
	{
		printk("test_char class_create failed ~\n");
		return -1;
	}
	else
		printk("test_char class_create success ~\n");

	fdevice = device_create(fclass, NULL, cdev_num, NULL, "test_dev");//在class下创建deivce
	if(fdevice == NULL)
	{
		printk("test_char device_create failed ~\n");
		return -1;
	}
	else
		printk("test_char device_create success ~\n");

	return 0;
}

void __exit char_exit(void)
{
	cdev_del(cdev);
	unregister_chrdev_region(cdev_num,1);
	device_destroy(fclass,cdev_num);
	class_destroy(fclass);


	return;
}

module_init(char_init);
module_exit(char_exit);

MODULE_LICENSE("GPL");

这样一个基本的字符设备驱动就写好了,再写个Makefile。

3.编写Makefile

obj-m := char_dev.o
KDIR:=/home/lzw/petalinux/project/zcu106_demo/build/tmp/work/xilinx_zcu106-xilinx-linux/linux-xlnx/5.15.36+gitAUTOINC+19984dd147-r0/linux-xilinx_zcu106-standard-build
 
all:
	make -C $(KDIR) M=$$PWD modules
clean:
	rm -rf Module.symvers modules.order .tmp_versions .*.cmd *.o *.ko *.mod *.mod. *.mod.c

编译前,还需要配置好Xilinx的交叉编译环境:

source ~/petalinux/project/zcu106_demo/images/linux/sdk/environment-setup-cortexa72-cortexa53-xilinx-linux
make

在这里插入图片描述
make生成ko文件后,在开发板insmod即可。

四、加载驱动

运行命令:

insmod char_dev.ko

这样在开发板就能够找到/dev/test_dev节点,一个基本的字符驱动就算是完成啦。
在这里插入图片描述

五、测试用例

附上测试程序

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(void)
{
	int fd = open("/dev/test_dev",O_RDWR);
	char buf[100] = {0};
	printf("open ~\n");
	
	int ret = read(fd,buf,sizeof(buf));
	printf("before write,read buf = %s, ret = %d\n",buf,ret);

	char str[10] = "123456789";
	ret = write(fd,str,sizeof(str));
	printf("write ret = %d\n",ret);

	ret = read(fd,buf,sizeof(buf));
	printf("after write,read buf = %s, ret = %d\n",buf,ret);

	close(fd);
	printf("close ~\n");
	
	return 0;
}

交叉编译后放到板子上运行,结果如下:
在这里插入图片描述
有时间打印的是内核输出,没有的是测试程序输出。可以看到应用层上read(),write()功能读取,写入数据的值都正确,驱动和测试程序运行正常。


总结

以上就是今天要讲的内容,本文介绍了在petalinux工程外编译字符驱动及测试的方法。制作不易,多多包涵。

  • 20
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值