作者之前讲解记录了系统移植部分内容,包括uboot、Linux和设备树、以及根文件系统这三个方面,接下来的将进入设备驱动部分的开发过程记录了。
系统移植部分可参考以下链接:
uboot移植可参考以下:
<Linux开发> -之-系统移植 uboot移植过程详细记录(第一部分)
<Linux开发> -之-系统移植 uboot移植过程详细记录(第二部分)
<Linux开发> -之-系统移植 uboot移植过程详细记录(第三部分)(uboot移植完结)
Linux内核及设备树移植可参考以下:
<Linux开发>系统移植 -之- linux内核移植过程详细记录(第一部分)
<Linux开发>系统移植 -之- linux内核移植过程详细记录(第二部分完结)
跟文件系统移植可参考以下:
<Linux开发>系统移植 -之- linux构建BusyBox根文件系统及移植过程详细记录
其它各驱动可到博主主页查看,由于后续会有越来越多的篇幅,就不一一列举链接到文章中了。
实验过程记录如下:
一、编程环境准备
1、安装虚拟机ubuntu,以及交叉工具链,这个在讲解系统移植部分也有说到,是必须的;
2、内核源码,这个也是系统移植中用到的内核源码,编译驱动时使用的内核源码,要与开发板运行的内核源码保存同一个版本;
3、编程软件VScode;
4、安装交叉工具链;
二、具体编程过程
1、vscode工程创建准备
(1)创建存放源码工程的目录,例如下图作者创建的文件夹;
(2)使用vscode在2-led文件夹内创建工程,并新建led.c和ledApp.c文件
(3)添加头文件路径
因为是编写Linux驱动,因此会用到Linux源码中的函数。我们需要在VSCode中添加Linux源码中的头文件路径。打开VSCode,按下“Crtl+Shift+P”打开VSCode的控制台,然后输入“C/C++: Edit configurations(JSON) ”,打开C/C++编辑配置文件,如下图所示:
打开以后会自动在.vscode目录下生成一个名为c_cpp_properties.json的文件,此文件修改后内容如下所示:
第7~9行就是添加好的Linux头文件路径。分别是开发板所使用的Linux源码下的include、arch/arm/include和arch/arm/include/generated这三个目录的路径,注意,这里使用了绝对路径。主要时添加绿色框内的内容,即是内核源码的路径,红色框则是源码的存放目录(根据读者自己实际存放的位置填写),后面紧接着的内容,都是一样的了。
(4)修改Linux内核源码顶层Makefile文件(作者也是开发时才踩这个坑的),谨记、除非系统移植的时候已经修改了。具体如下图所示:
用vscode打开内核源码的顶层目录,然后找到Makefile,在里面找到“ARCH”和“CROSS_COMPILE”这两个变量,更改后变成“ARCH ?= arm”和 “CROSS_COMPILE ?= arm-linux-gnueabihf-” ,注意行的末尾不能有空格,否则编译会出错。第一个是编译的对象,第二个是编译的工具链前缀。
2、在led.c中编写字符驱动源码,函数的作用说明,都在源码注释上说明,内容如下:
#include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/ide.h> #include <linux/init.h> #include <linux/module.h> #include <linux/errno.h> #include <linux/gpio.h> #include <asm/mach/map.h> #include <asm/io.h> /*************************************************************** * Copyright © onefu Co., Ltd. 2019-2021. All rights reserved. * 文件名 : led.c * 作者 : water * 版本 : V1.0 * 描述 : led 驱动文件。 * 其他 : 无 * 日志 : 初版V1.0 2021/10/27 water创建 * ***************************************************************/ #define LED_MAJOR 200 /*主设备号*/ #define LED_NAME "led" /*设备名字*/ #define LEDOFF 0 /*关灯*/ #define LEDON 1 /*开灯*/ /*寄存器物理地址 宏定义*/ #define CCM_CCGR1_BASE (0X020C406C) #define SW_MUX_GPIO1_IO03_BASE (0X020E0068) #define SW_PAD_GPIO1_IO03_BASE (0X020E02F4) #define GPIO1_DR_BASE (0X0209C000) #define GPIO1_GDIR_BASE (0X0209C004) /*映射后的寄存器虚拟地址指针*/ static void __iomem *IMX6U_CCM_CCGR1; static void __iomem *SW_MUX_GPIO1_IO03; static void __iomem *SW_PAD_GPIO1_IO03; static void __iomem *GPIO1_DR; static void __iomem *GPIO1_GDIR; /* * @description : 打开/关闭 led * @param – sta : LEDON(0) 打开 LED,LEDOFF(1) 关闭 LED * @return : 无 */ void led_switch(u8 sta) { u32 val = 0; if(sta == LEDON){ /* 判断控制传入的状态 如果表示 开*/ val = readl(GPIO1_DR); /* 读取GPIO1组的DR寄存器,即GPIO1组的所有 IO 的状态*/ val &= ~(1<<3); /* 只对第三位进行清零操作 保持其他位不变*/ writel(val, GPIO1_DR); /* 将更改后的GPIO1组的DR寄存器值,写回到DR寄存器*/ }else if(sta == LEDOFF){ /* 判断控制传入的状态 如果表示 关*/ val = readl(GPIO1_DR); /* 读取GPIO1组的DR寄存器,即GPIO1组的所有 IO 的状态*/ val |= (1<<3); /* 只对第三位进行置1操作 保持其他位不变*/ writel(val, GPIO1_DR); /* 将更改后的GPIO1组的DR寄存器值,写回到DR寄存器*/ } } /* * @description : 打开设备 * @param – inode : 传递给驱动的inode * @param - filp : 设备文件,file结构体有个叫做private_data的成员变量 * 一般在open的时候将private_data指向设备结构体。 * @return : 0 成功;其他 失败 */ static int led_open(struct inode *inode, struct file *filep) { printk("led open!\r\n"); /*终端输出提示*/ return 0; } /* *@description : 从设备读取数据 * @param - filp : 要打开的设备文件(文件描述符) * @param - buf : 返回给用户空间的数据缓冲区 * @param - cnt : 要读取的数据长度 * @param - offt : 相对于文件首地址的偏移 * @return : 读取的字节数,如果为负值,表示读取失败 */ static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { printk("led read !\r\n"); /*终端输出提示*/ return 0; } /* @description : 向设备写数据 @param - filp : 设备文件,表示打开的文件描述符 @param - buf : 要写给设备写入的数据 @param - cnt : 要写入的数据长度 @param - offt : 相对于文件首地址的偏移 @return : 写入的字节数,如果为负值,表示写入失败 */ static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { int retvalue = 0; unsigned char databuf[1]; unsigned char ledstat; retvalue = copy_from_user(databuf, buf, cnt);/*接收用户空间传递给内核的数据*/ if(retvalue < 0){ printk("kernel write failed! \r\n"); /*终端输出提示*/ return -EFAULT; /*返回错误*/ } ledstat = databuf[0]; /*将读取到的数据 赋值给状态变量*/ if(ledstat == LEDON){ /*判断状态变量 为 开灯*/ led_switch(LEDON); /*开灯*/ }else if(ledstat == LEDOFF){ /*判断状态变量 为 关灯*/ led_switch(LEDOFF); /*关灯*/ } return 0; } /* *@description : 关闭/释放设备 *@param - filp : 要关闭的设备文件(文件描述符) *@return : 0 成功;其他 失败 */ static int led_release(struct inode *inode, struct file *filp) { printk("led release ! \r\n"); return 0; } /* *设备操作函数结构体 */ static struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .read = led_read, .write = led_write, .release = led_release, }; /* *@description : 驱动入口函数 *@param : 无 *@return : 0 成功;其他 失败 */ static int __init led_init(void) { int retvalue = 0; u32 val = 0; /*以下开始 初始化LED的GPIO引脚*/ /* 1、寄存器地址映射 */ IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4); SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4); SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4); GPIO1_DR = ioremap(GPIO1_DR_BASE, 4); GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4); /* 2、使能GPIO1时钟 */ val = readl(IMX6U_CCM_CCGR1); /*将CCGR1 寄存器的值读取出来*/ val &= ~(3 << 26); /*清除旧的设置位*/ val |= (3 << 26); /*设置新位值*/ writel(val, IMX6U_CCM_CCGR1); /*写入CCGR1 寄存器的值*/ /* 3、设置GPIO1_IO03的复用功能,将其复用为GPIO1_IO03,最后设置IO属性 */ writel(5, SW_MUX_GPIO1_IO03); /* 配置 GPIO1_IO03 的 IO 属性 *bit 16:0 HYS 关闭 *bit [15:14]: 00 默认下拉 *bit [13]: 0 kepper 功能 *bit [12]: 1 pull/keeper 使能 *bit [11]: 0 关闭开路输出 *bit [11]: 0 关闭开路输出 *bit [5:3]: 110 R0/6 驱动能力 *bit [0]: 0 低转换率 */ writel(0x10B0, SW_PAD_GPIO1_IO03); /* 寄存器 SW_PAD_GPIO1_IO03 设置 IO 属性 */ /* 4、设置GPIO1_IO03为输出功能 */ val = readl(GPIO1_GDIR); /*将GPIO1_GDIR 寄存器的值读取出来*/ val &= ~(1 << 3); /*清除旧的设置位*/ val |= (1 << 3); /*设置新位值*/ writel(val, GPIO1_GDIR); /*写入GPIO1_GDIR 寄存器的值*/ /* 5默认关闭LED */ val = readl(GPIO1_DR); /*将GPIO1_DR 寄存器的值读取出来*/ val |= (1 << 3); /*设置新位值*/ writel(val, GPIO1_DR); /*写入GPIO1_DR 寄存器的值*/ /* 6、注册字符设备驱动*/ retvalue = register_chrdev(LED_MAJOR, LED_NAME, &led_fops); if(retvalue < 0){ printk("led driver register failed !\r\n"); return -EIO; } printk("led_init()"); return 0; } /* * @description : 驱动出口函数 * @param : 无 * @return : 无 */ static void __exit led_exit(void) { /*注销字符设备驱动*/ unregister_chrdev(LED_MAJOR, LED_NAME); printk("led_exit()\r\n"); } /*将上面两个函数指定为驱动入口 和 出口 函数*/ module_init(led_init); module_exit(led_exit); /*LICENSE 和 作者信息 模块描述信息 设备支持信息*/ MODULE_LICENSE("GPL"); MODULE_AUTHOR("water"); MODULE_DESCRIPTION ("OnFu This is a LED "); MODULE_SUPPORTED_DEVICE ("OneFu LED Device");
3、在ledApp.c文件中编写测试软件代码,代码内容如下:
#include "stdio.h" #include "unistd.h" #include "sys/types.h" #include "sys/stat.h" #include "fcntl.h" #include "stdlib.h" #include "string.h" /*************************************************************** * Copyright © onefu Co., Ltd. 2019-2021. All rights reserved. * 文件名 : ledApp.c * 作者 : water * 版本 : V1.0 * 描述 : led 驱测试APP。 * 其他 : 使用方法:./ledApp /dev/led <1>|<2> * argv[2] 0:关闭LED * argv[2] 1:打开LED * 日志 : 初版V1.0 2021/10/27 water创建 * ***************************************************************/ #define LEDOFF 0 #define LEDON 1 /* * @description : main主程序 * @param - argc : argv数组元素个数 * @param - argv : 具体参数 * @return : 0 成功;其他 失败 */ int main(int argc, char *argv[]) { int fd, retvalue; //fd: 文件描述符 用以对文件操作 retvalue:存放函数操作后的返回值 char *filename; //filename:文件名,有主函数参数传入赋值 unsigned char databuf[1]; //定义的buf,用来读写数据用 if(argc != 3){ //判断主函数传入的函数的参数的个数 printf("Error Usage!\r\n"); return -1; } filename = argv[1]; //获取第1个参数,存放的是文件的路径(即要操作的设备文件路径) fd = open(filename,O_RDWR); /*打开驱动文件*/ if(fd < 0){ printf("Can't open file %s\r\n",filename); /*打开失败,输出提示*/ return -1; } databuf[0] = atoi(argv[2]); /* 要执行的操作:打开或关闭 */ retvalue = write(fd, databuf, sizeof(databuf)); /*向设备驱动写入数据*/ if(retvalue < 0){ printf("LED Control Failed!\r\n",filename); /*写入错误输出提示*/ } retvalue = close(fd); /*关闭文件*/ if(retvalue < 0){ printf("Can't close file %s\r\n",filename); /*关闭错误输出提示*/ return -1; } return 0; }
三、编译
1、驱动编译
在led.c文件的同级目录下创建一个Makefile文件,输入以下内容:
KERNELDIR := /home/water/water/kernel/linux-imx-onefu-20211024 CURRENT_PATH := $(shell pwd) obj-m := led.o build: kernel_modules kernel_modules: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules clean: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean # KERNELDIR表示开发板所使用的Linux内核源码目录,使用绝对路径 # CURRENT_PATH表示当前路径,直接通过运行“pwd”命令来获取当前所处路径。 # obj-m表示将chrdevbase.c这个文件编译为chrdevbase.ko模块 # 具体的编译命令,后面的modules表示编译模块, #-C表示将当前的工作目录切换到指定目录中, #也就是KERNERLDIR目录。M表示模块源码目录, #“make modules”命令中加入M=dir以后程序会自动到指定的dir目录中读取模块的源码并将其编译为.ko文件 # #
第一行是内核源码的绝对路径,读者根据自己的实际路径修改即可,第三行的obj-m表示将lede.c这个文件编译为led.ko模块,就是对应的其余基本和上述一致即可。
编写完,保存,然后在终端输入:make ,进行编译驱动即可,编译结果如下图:
上图用的是vscode自带的终端编译,也可通过ubuntu的终端进入到对应的目录下输入make命令进行编译,编译成功后,当前目录下生成“led.ko”和其它一些文件,用的驱动文件就是这个“.ko”文件,其余不管。
2、测试APP编译
同样在vscode打开的终端输入:arm-linux-gnueabihf-gcc led.c -o ledApp ,对测试APP进行编译。然后会生成ledApp这个可执行文件,可通过“file ledApp”,这个命令查看文件信息,如下图:
四、运行测试
1、将驱动文件“led.ko” 和测试程序“ledApp”,拷贝到根文件系统(作者使用的是nfs挂载根文件系统的形式,详细可参考系统移植部分)的“lib/modules/4.1.15”目录下,如果不存在则创建目录,目录“4.1.15“主要是用来区别不同内核版本。拷贝后的目录下有下图红色框的这两个文件。
2、将开发板串口链接电脑,打开CRT,然后打开电源,当进入倒计时时按下回车,让开发板运行在uboot状态下,在这个状态下主要时配置以下环境变量,具体如下:
//设置bootcmd setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull-alientek-emmc.dtb; bootz 80800000 - 83000000' //设置bootargs setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.1.144:/home/water/linux/nfs/onefu-rootfs,proto=tcp rw ip=192.168.1.145:192.168.1.144:192.168.1.1:255.255.255.0::eth0:off' saveenv //保存环境变量 boot //启动
第2行:setenv bootcmd: 表示设置 环境变量中的 bootcmd 的值;
tftp 80800000 zImage:标志通过ftfp的形式从服务器下载zImage文件到 地址80800000;
tftp 83000000 imx6ull-alientek-emmc.dtb:同上一样;
bootz 80800000 - 83000000:设置boot启动的内核地址和设备地址。
第4行:setenv bootargs : 表示设置 环境变量中的 bootargs 的值;
console=ttymxc0,115200 :设置终端 和波特率;
root=/dev/nfs:设置root的启动目录是/dev/nfs;
nfsroot=192.168.1.144:/home/water/linux/nfs/onefu-rootfs:从服务器IP为192.168.1.144的对应目录;
proto=tcp :设置通信的方式 TCP;
rw :标识读写功能
ip=192.168.1.145:192.168.1.144:192.168.1.1:255.255.255.0:分别是,弟弟开发板IP。服务器IP,网关,掩码;
第5行: saveenv :保存设置的花鸟卷变量
第6行:boot:运行进入Linux。
3、进入Linux后,进入目录”/lib/modules/4.1.15“,然后用命令”ls“ 查看文件;
4、挂载驱动
输入如下命令加载led.ko驱动文件:
//先执行命令 depmod //在执行 modprobe led.ko
挂载成功会输出” led_init()“,如下图:
可使用”lsmod“查看,如下图:
可使用cat命令查看设备,如下图:
5、创建设备节点
驱动加载成功需要在/dev目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。输入如下命令创建/dev/chrdevbase这个设备节点文件:
mknod /dev/led c 200 0
可以使用“ls /dev/led -l”命令查看,结果如下图所示:
6、运行验证
首先进行打开LED,输入如下命令:
./ledApp /dev/led 1
结果如下图:
上图可看出,运行后打开LED输出提示。
接下来测试对led设备进行关闭操作,输入如下命令:
./ledeApp /dev/led 0
结果如下图:
通过上面两个命令可对LED灯进行开关控制,读者自行测试验证,笔者的操作成功运行。
7、卸载驱动模块
输入如下命令卸载驱动模块:
rmmod led.ko
通过”lsmod“命令查看模块是否还在,如下图:
有上图可看出,模块已经卸载完成。
至此,字符设备驱动的LED驱动开发过程,如上所记录。
如有不足之处还望指点,欢迎交流,共同学习。
联系方式QQ:759521350