VSCode 创建工程,设置 C/C++ 配置,导入 linux kernel 源码目录,方便 vscode 写代码自动补全,vscode 配置
{
"configurations": [
{
"name": "Linux",
"includePath": [
"/home/jeck/linux/linux_kernel/nxp/linux-imx-rel_imx_4.1.15_2.1.0_ga/include",
"/home/jeck/linux/linux_kernel/nxp/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include",
"/home/jeck/linux/linux_kernel/nxp/linux-imx-rel_imx_4.1.15_2.1.0_ga/arch/arm/include/generated"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "linux-gcc-x64"
},
{
"name": "linux_drive",
"includePath": [
"${workspaceFolder}/**"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "linux-gcc-x64"
}
],
"version": 4
}
新建一个文件 char_drive.c 文件:
驱动程序主要包含:驱动的注册和卸载,以及基本驱动操作,比如打开、关闭、读取、写入等操作,这里使用原子的例程分析:
打开字符设备,因为驱动在 linux 下就是一个文件,我们传入文件的 inode 和文件结构体用于关联设备。
/*
* @description : 打开设备
* @param - inode : 传递给驱动的inode
* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量
* 一般在open的时候将private_data指向设备结构体。
* @return : 0 成功;其他 失败
*/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
// printk("chrdevbase open!\r\n");
return 0;
}
关闭字符设备,关闭设备就是打开设备的逆操作,断开设备结构体和文件结构体的关联。
/*
* @description : 关闭/释放设备
* @param - filp : 要关闭的设备文件(文件描述符)
* @return : 0 成功;其他 失败
*/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
// printk("chrdevbase release!\r\n");
return 0;
}
读字符设备,传入驱动设备文件,这里我们并没有使用文件指针,模拟进行操作,使用 copy_to_user 内核函数,将数据打印到用户数据空间。
/*
* @description : 从设备读取数据
* @param - filp : 要打开的设备文件(文件描述符)
* @param - buf : 返回给用户空间的数据缓冲区
* @param - cnt : 要读取的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 读取的字节数,如果为负值,表示读取失败
*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/* 向用户空间发送数据 */
memcpy(readbuf, kerneldata, sizeof(kerneldata));
retvalue = copy_to_user(buf, readbuf, cnt);
if (retvalue == 0)
{
printk("kernel senddata ok!\r\n");
}
else
{
printk("kernel senddata failed!\r\n");
}
// printk("chrdevbase read!\r\n");
return 0;
}
写字符设备,和读字符串设备相同,只不过数据读写方向相反。
/*
* @description : 向设备写数据
* @param - filp : 设备文件,表示打开的文件描述符
* @param - buf : 要写给设备写入的数据
* @param - cnt : 要写入的数据长度
* @param - offt : 相对于文件首地址的偏移
* @return : 写入的字节数,如果为负值,表示写入失败
*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/* 接收用户空间传递给内核的数据并且打印出来 */
retvalue = copy_from_user(writebuf, buf, cnt);
if (retvalue == 0)
{
printk("kernel recevdata:%s\r\n", writebuf);
}
else
{
printk("kernel recevdata failed!\r\n");
}
// printk("chrdevbase write!\r\n");
return 0;
}
驱动基本操作程序写话后,编写驱动注册和注销接口:
驱动注册本质上就是将操作函数作为指针传递进去,挂载到调用结构体上,注册时要传入设备号和设备名称
/*
* @description : 驱动入口函数
* @param : 无
* @return : 0 成功;其他 失败
*/
static int __init chrdevbase_init(void)
{
int retvalue = 0;
/* 注册字符设备驱动 */
retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
if (retvalue < 0)
{
printk("chrdevbase driver register failed\r\n");
}
printk("chrdevbase init!\r\n");
return 0;
}
设备注销就只需要传入设备号和名称就行,注销函数会根据设备号清除相关结构体
/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit chrdevbase_exit(void)
{
/* 注销字符设备驱动 */
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
printk("chrdevbase exit!\r\n");
}
注册和注销函数还需要在注册一次,关联到系统模块加载和卸载相关的结构体上
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
除此之外还要作者信息,此处不能缺少
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");
编写 main 函数,根据传入参数进行驱动代码的测试,main 函数的参数也叫做命令参数,在 linux 下敲命令的时候就有用了,argv 指的是 char * argc[] 的大小 char * argc[]是一个指针数组,里面存放的是各个传入字符串的首地址,测试代码如下:
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
char readbuf[100], writebuf[100];
if (argc != 3)
{
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
/* 打开驱动文件 */
fd = open(filename, O_RDWR);
if (fd < 0)
{
printf("Can't open file %s\r\n", filename);
return -1;
}
if (atoi(argv[2]) == 1)
{ /* 从驱动文件读取数据 */
retvalue = read(fd, readbuf, 50);
if (retvalue < 0)
{
printf("read file %s failed!\r\n", filename);
}
else
{
/* 读取成功,打印出读取成功的数据 */
printf("read data:%s\r\n", readbuf);
}
}
if (atoi(argv[2]) == 2)
{
/* 向设备驱动写数据 */
memcpy(writebuf, usrdata, sizeof(usrdata));
retvalue = write(fd, writebuf, 50);
if (retvalue < 0)
{
printf("write file %s failed!\r\n", filename);
}
}
/* 关闭设备 */
retvalue = close(fd);
if (retvalue < 0)
{
printf("Can't close file %s\r\n", filename);
return -1;
}
return 0;
}
这段应用程序就是简单的传入设备文件名称,然后再根据后续参数进行读或者写的操作。
驱动程序使用 makefile 用交叉工具链进行编译:
KERNELDIR := /home/jeck/linux/linux_kernel/nxp/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENT_PATH := $(shell pwd)
obj-m := char_drive.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
KERNELDIR 为 Linux 内核目录,编译指令:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
编译完成,得到 ko 文件可用于内核加载:
然后直接在 linux 用交叉工具链将测试程序编译链接成可执行文件:
arm-linux-gnueabihf-gcc char_device_app.c -o test_app
模块编译成功后,创建内核模块目录:/lib/modules/4.1.15
,其中 4.1.15 是内核版本,可以通过 uname -r
查询。
复制 ko 文件和测试程序到文件系统目录下面,加载 ko 模块:
加载指令:
insmod char_drive.ko
然后使用 test_app 进行测试就行