前言
一个基础的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工程外编译字符驱动及测试的方法。制作不易,多多包涵。