一、字符设备驱动框架
具体的对应关系见上一篇文章,这里只对需要实现的部分进行说明
2.字符设备驱动开发基础(一个虚拟的字符设备驱动开发流程)
字符设备驱动编写,主要工作就是驱动对应的open close read write函数的编写说白了,就是
对file_operations结构体的成员变量的实现;
在linux内核代码中,file_operations的定义在/include/linux/fs.h
这在个.h文件中定义了很多对于linux文件很重要的概念,包括inode、file_operations
等等;
1.1 使用vscode作为linux下编辑器的设置
在当前目录下创建.vscode文件夹,其下创建如下设置当前目录的头文件搜索路径(注意选择自己的内核源码路径)
c_cpp_properties.json
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/home/qjy/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/include",
"/home/qjy/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include",
"/home/qjy/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek/arch/arm/include/generated/"
],
"defines": [],
"compilerPath": "/usr/bin/clang",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "clang-x64"
}
],
"version": 4
}
1.2 驱动模块加载卸载测试(仅含有模块注册和卸载函数的驱动测试,具体框架在1.4中)
内容:编写一个仅含有驱动的注册和卸载函数的.c文件,将其编译成驱动模块后加载到内核,观察是否加载成功
目的:验证环境搭建是否存在问题;
printk()的头文件:#include <linux/kernel.h>
chrdevbase.c
#include <linux/module.h>
#include <linux/kernel.h>
static int __init chrdevbase_init(void)
{
printk("chrdevbase_init\r\n");
return 0;
}
static void __exit chrdevbase_exit(void)
{
printk("chrdevbase_exit\r\n");
}
/**
* 模块入口与出口
*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QJY");
1.3 Makefile
KERNELDIR := /home/qjy/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
编译之后的目录为:(编译——拷贝到ubuntu nfs下的rootfs中)
开发板中加载模块:
1.4 字符设备驱动框架搭建与函数实现
下图所示是本实验使用的字符设备驱动框架,并不完善;
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/uaccess.h> // copy from user
#define CHRDEVBASE_MAJOR 200 // 主设备号
#define CHRDEVBASE_NAME "chrdevbase" // 名字
static char readbuf[100]; // 读缓冲区
static char writebuf[100]; // 写缓冲区
static char kernel_data[] = {"kernel data!"};
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
printk("chrdevbase_open\r\n");
return 0;
}
static int chrdevbase_close(struct inode *inode, struct file *filp)
{
printk("chrdevbase_release\r\n");
return 0;
}
static ssize_t chrdevbase_read(struct file *filp, __user char *buf,
size_t cnt, loff_t *ppos)
{
int retval = 0;
printk("chrdevbase_reade\r\n");
// 从内核中读取数据
memcpy(readbuf, kernel_data, sizeof(kernel_data));
retval = copy_to_user(buf, readbuf, cnt);
if(retval == 0) {
printk("kernel data sended! OK\r\n");
}else {
printk("kernel data sended failed!\r\n");
}
return 0;
}
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *ppos)
{
int retval = 0;
printk("chrdevbase_write\r\n");
// 从用户空间向内核空间写数据
copy_from_user(writebuf, buf, cnt);
if(retval == 0) {
printk("kernel reveived OK\r\n");
}else {
printk("kernel reveived failed!\r\n");
}
return 0;
}
/* 字符设备操作集合*/
static struct file_operations chrdevbase_fops={
.owner = THIS_MODULE,
.open = chrdevbase_open,
.release = chrdevbase_close,
.read = chrdevbase_read,
.write = chrdevbase_write,
};
static int __init chrdevbase_init(void)
{
int ret = 0;
printk("chrdevbase_init\r\n");
/* 注册字符设备*/
ret = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME,
&chrdevbase_fops);
if(ret < 0) {
printk("chrdevbase init failed!\r\n");
}
return 0;
}
static void __exit chrdevbase_exit(void)
{
printk("chrdevbase_exit\r\n");
/* 注销字符设备*/
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
}
/**
* 模块入口与出口
*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("QJY");
二、驱动模块的加载和卸载
驱动的加载有两种方式:
- 直接写进内核文件:编译内核后内核(zImage)与驱动融为一体
- 编译成
.ko
模块文件,使用modprobe
指令加载模块;(我们使用这种)
第二种方法的好处在于,方便调试,可以随时加载和卸载模块,而不是重新编译整个内核文件;
2.1 开发板的准备工作(alpha-i.mx6ull)
- 启动:uboot代码编译后,烧写在sd卡中,开发板从sd卡启动
- 内核zImage和设备树文件.dtb:使用tftp从ubuntu中读取;
- rootfs根文件系统在ubuntu中,使用nfs挂载;
- 设置uboot变量
bootargs 和 bootcmd
完成2、3的设置;
下面是设置之后的uboot环境变量
=> print
baudrate=115200
board_name=EVK
board_rev=14X14
boot_fdt=try
bootargs=console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.5.103:/home/qjy/linux/nfs/rootfs ip=192.168.5.111:192.168.5.103:192.168.5.1:255.255.255.0::eth0:off
bootcmd=tftp 80800000 zImage;tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000
bootcmd_mfg=run mfgtool_args;bootz ${loadaddr} ${initrd_addr} ${fdt_addr};
bootdelay=3
bootscript=echo Running bootscript from mmc ...; source
console=ttymxc0
ethact=FEC1
ethaddr=00:04:9f:04:d2:35
ethprime=FEC
fdt_addr=0x83000000
fdt_file=imx6ull-alientek-emmc.dtb
fdt_high=0xffffffff
fileaddr=83000000
filesize=8d32
findfdt=if test $fdt_file = undefined; then if test $board_name = EVK && test $board_rev = 9X9; then setenv fdt_file imx6ull-9x9-evk.dtb; fi; if test $board_name = EVK && test $board_rev = 14X14; then setenv fdt_file imx6ull-14x14-evk.dtb; fi; if test $fdt_file = undefined; then echo WARNING: Could not determine dtb to use; fi; fi;
gatewayip=192.168.5.1
image=zImage
initrd_addr=0x83800000
initrd_high=0xffffffff
ip_dyn=yes
ipaddr=192.168.5.111
loadaddr=0x80800000
loadbootscript=fatload mmc ${mmcdev}:${mmcpart} ${loadaddr} ${script};
loadfdt=fatload mmc ${mmcdev}:${mmcpart} ${fdt_addr} ${fdt_file}
loadimage=fatload mmc ${mmcdev}:${mmcpart} ${loadaddr} ${image}
mfgtool_args=setenv bootargs console=${console},${baudrate} rdinit=/linuxrc g_mass_storage.stall=0 g_mass_storage.removable=1 g_mass_storage.file=/fat g_mass_storage.ro=1 g_mass_storage.idVendor=0x066F g_mass_storage.idProduct=0x37FF g_mass_storage.iSerialNumber="" clk_ignore_unused
mmcargs=setenv bootargs console=${console},${baudrate} root=${mmcroot}
mmcautodetect=yes
mmcboot=echo Booting from mmc ...; run mmcargs; if test ${boot_fdt} = yes || test ${boot_fdt} = try; then if run loadfdt; then bootz ${loadaddr} - ${fdt_addr}; else if test ${boot_fdt} = try; then bootz; else echo WARN: Cannot load the DT; fi; fi; else bootz; fi;
mmcdev=0
mmcpart=1
mmcroot=/dev/mmcblk0p2 rootwait rw
netargs=setenv bootargs console=${console},${baudrate} root=/dev/nfs ip=dhcp nfsroot=${serverip}:${nfsroot},v3,tcp
netboot=echo Booting from net ...; run netargs; if test ${ip_dyn} = yes; then setenv get_cmd dhcp; else setenv get_cmd tftp; fi; ${get_cmd} ${image}; if test ${boot_fdt} = yes || test ${boot_fdt} = try; then if ${get_cmd} ${fdt_addr} ${fdt_file}; then bootz ${loadaddr} - ${fdt_addr}; else if test ${boot_fdt} = try; then bootz; else echo WARN: Cannot load the DT; fi; fi; else bootz; fi;
netmask=255.255.255.0
panel=TFT7016
script=boot.scr
serverip=192.168.5.103
如果重新配置,需要更新的环境变量为:bootcmd、bootargs、ipaddr、ethaddr、gatewayip、netmask、serverip;
2.2 驱动模块的加载卸载
将编译出来的.ko模块放到根文件系统中;
使用depmod
指令分析模块依赖心,为使用modprobe准备;
使用modprobe / insmod
指令完成对模块的加载;
使用modprobe -r / rmmod
指令完成对模块的卸载;
使用lsmod
查看已经加载的模块;
如果使用modprobe出现问题,见
Linux驱动加载问题“.ko模块无法加载modprobe: module ‘xxx.ko’ not found”解决方法
Linux depmod命令用于分析可载入模块的相依性。
depmod(depend module)可检测模块的相依性,供modprobe在安装模块时使用。
3. 应用程序的编写
/**
* 本文件对应驱动模块CharDevBase的测试APP
* argv[1] : filename:要进行互动的驱动设备名称(通过/dev/
* argv[2] : 1:表示从驱动中读取数据操作; 2:表示向驱动中写数据操作
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#define READ_SIZE 50
#define WRITE_SIZE 50
int main(int argc,char* argv[])
{
int fd;
int read_ret = 0, write_ret = 0;
char read_buf[100]; // 用户空间读取数据缓冲区
char write_buf[100]; // 用户空间写入数据缓冲区
char write_data[] = "Im the written data, I come from UserSapce!\n";
memcpy(write_buf,write_data, sizeof(write_data));
if(argc != 3){
printf("参数不足!\n");
return -1;
}
fd = open(argv[1], O_RDWR);
if(fd < 0){
printf("Now in userSpace: open error!\n");
return errno;
}
if(atoi(argv[2]) == 1){// 读操作
if((read_ret = read(fd, read_buf,READ_SIZE)) == -1){
printf("Now in userSpace: reading error~\n");
return errno;
}else{
printf("Now in userSpace: the received data: %s\n", read_buf);
}
}
if(atoi(argv[2]) == 2){// 写操作
if((write_ret = write(fd, write_buf,WRITE_SIZE)) == -1){
printf("Now in userSpace: reading error~\n");
return errno;
}
}
usleep(200);
if(-1 == close(fd)){
printf("Now in userSpace: close error!\n");
return errno;
}
return 0;
}
3.1编译应用程序并将.ko与App文件拷贝到rootfs中
注意,驱动模块的编译方法在之前的makefile中
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
sudo cp ./*.ko ./chrdevbaseApp ../../../nfs/rootfs/lib/modules/4.1.15