普通的驱动就是实现file_operation结构体的各个函数,然后使用register_chrdev来注册,使用device_create来创建设备节点。这种方式是把驱动和设备资源合在一个C文件里面了,当我们的设备资源换一个板子或者换一个引脚的时候,我们需要重新修改驱动的各个函数。非常不方便。而platform架构的驱动是将设备资源和设备资源分开的,当我们的设备有更改的时候,只需要修改设备资源这个C文件即可。
1. 编程步骤
platform驱动分为两个部分,平台设备和平台驱动,所有需要两个C文件。其实套路都差不多,都是什么注册函数,入口函数,出口函数这些。
1. 1 平台设备
1.1.1 定义资源
使用结构体struct resource来定义
- start 表示资源的其实地址,学过单片机的都知道,操作硬件无非就是操作寄存器地址。
- end 表示资源的结束地址
- name 给该资源去一个名字
- flags 表示资源的类型
eg:
struct resource button_resource[] = { { .start = 0x20c406c, .end = 0x20c406c+3, // IORESOURCE_MEM必须要加上end,不然会报错 .name = "CCGR", .flags = IORESOURCE_MEM, }, { .start = 0x229000c, .end = 0x229000c+3, .name = "SW_MUX_CTL", .flags = IORESOURCE_MEM, }, { .start = 0x2290050, .end = 0x2290050+3, .name = "SW_PAD_CTL", .flags = IORESOURCE_MEM, }, { .start = 0x20ac004, .end = 0x20ac004+3, .name = "GDIR", .flags = IORESOURCE_MEM, }, { .start = 0x20ac008, .end = 0x20ac008+3, .name = "PSR", .flags = IORESOURCE_MEM, }, { .start = 1, .end = 0, .name = "pin", .flags = IORESOURCE_IRQ, }, { .start = 30, .end = 0, .name = "clock_offset", .flags = IORESOURCE_IRQ, }, };
1.1.2 实现平台资源结构体
- name: 平台设备结构体的名字,相当重要,在平台驱动和平台设备匹配的时候比较的就是这个name
- dev下面的release函数必须要指定,不然会报错,目前也还没用到该函数。
- num_resources 表示资源的数量,即资源结构体的大小
- resource 表示资源,即1.1.1中实现的资源结构体数组
eg:
struct platform_device button_device = { .name = "my_button", .id = -1, .num_resources = ARRAY_SIZE(button_resource), .resource = button_resource, .dev = { .release = button_release, }, };
1.1.3 定义入口函数
其内部调用平台设备注册函数platform_device_register来注册前面实现的设备
// 入口函数 static int button_platform_device_init(void) { // 注册设备信息 // int platform_device_register(struct platform_device *pdev) return 0; }
1.1.4 定义出口函数
其内部调用函数platform_device_unregister来取消对设备的注册
// 出口函数 static void button_platform_device_exit(void) { // 取消设备 // void platform_device_unregister(struct platform_device *pdev) }
1.1.5 绑定出口和入口函数
当我们在终端使用ismod来加载模块或使用rmmod来卸载模块的时候,就会调用你绑定的函数来实现
module_init(button_platform_driver_init); module_exit(button_platform_driver_exit); MODULE_LICENSE("GPL"); // 这个必须要有
1.2 平台驱动
1.2.1 实现file_operation结构体的各个函数(按需实现)
hello驱动已经详细说了,这里不细说了。
// 这些函数名是自定的,我这里写的是按键相关的,所以就是button什么什么的,只不过参数和返回值不能改。 static ssize_t button_drv_read(struct file * file, char __user * buf, size_t size, loff_t *offset) { // 读取寄存器的值 // 通过copy_to_user(to, from, size)将结果返回给上层系统调用open // 返回值就是读取到的字节大小,由于目前遇到的都是字符设备,所以大小一般都是1 return size; } static ssize_t button_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset) { // 通过copy_from_user()获取到buf里面的数据 // 写入寄存器 // 返回值就是写入的字节大小 return 0; } static int button_drv_open(struct inode *node, struct file *file) { // 初始化设备 return 0; } static int button_drv_close(struct inode *node, struct file *file) { // 关闭设备 return 0; } static struct file_operations button_opr = { .open = button_drv_open, .release = button_drv_close, .read = button_drv_read, .write = button_drv_write, };
1.2.2 定义platform_driver结构体
并填充实现里面的函数,其中probe和remove必须实现
1.2.2.1 probe函数
该函数是在平台设备模块和平台驱动模块成功匹配后调用的,当匹配成功了表明设备资源我有了,设备驱动我也有了,所以该函数里面主要来注册驱动,读取资源,物理地址到虚拟地址的映射,添加设备节点等等初始化工作。
static int button_probe(struct platform_device *pdev) { // 获取资源 platform_get_resource(pdev, IORESOURCE_MEM, i); // 地址映射 ioremap // 注册驱动 register_chrdev() // 添加设备节点 class_create() device_create() }
1.2.2.2 remove函数
remove函数时候probe函数相反的,当平台驱动和平台设备如何一个被注销时,该函数就会被执行。该函数主要工作就是注销驱动,释放虚拟地址的映射,注销设备节点
static int button_remove(struct platform_device *pdev)
1.2.3 实现平台驱动入口函数
该函数主要工作就是使用platform_driver_register来注册平台驱动
static int button_platform_driver_init(void) { // 使用platform_driver_register来注册平台驱动 return 0; }
1.2.4 实现平台驱动出口函数
该函数主要工作就是使用platform_driver_unregister来注销平台驱动
static void button_platform_driver_exit(void) { // platform_driver_unregister来注销平台驱动 }
1.2.5 绑定入口函数和出口函数
当使用ismod和rmmod指令的时候会调用指定的函数
module_init(button_platform_driver_init); // 与ismod相关 module_exit(button_platform_driver_exit); // 与rmmod先关 MODULE_LICENSE("GPL"); // 必须要有
2. 实例
实现简单地按键驱动(基于野火I.MX6ULL PRO开发板)
由于使用的轮询方式,且驱动写的很简单,造成了我按一下按钮,那个变量bnt直接飙升,不是一个数一个数的递增。
2.1 平台设备文件
button_device.c
#include <linux/module.h> #include <linux/init.h> #include <linux/platform_device.h> static void button_release(struct device *dev) { printk("%s\n", __FUNCTION__); return; } struct resource button_resource[] = { { .start = 0x20c406c, .end = 0x20c406c+3, // IORESOURCE_MEM必须要加上end,不然会报错 .name = "CCGR", .flags = IORESOURCE_MEM, }, { .start = 0x229000c, .end = 0x229000c+3, .name = "SW_MUX_CTL", .flags = IORESOURCE_MEM, }, { .start = 0x2290050, .end = 0x2290050+3, .name = "SW_PAD_CTL", .flags = IORESOURCE_MEM, }, { .start = 0x20ac004, .end = 0x20ac004+3, .name = "GDIR", .flags = IORESOURCE_MEM, }, { .start = 0x20ac008, .end = 0x20ac008+3, .name = "PSR", .flags = IORESOURCE_MEM, }, { .start = 1, .end = 0, .name = "pin", .flags = IORESOURCE_IRQ, }, { .start = 30, .end = 0, .name = "clock_offset", .flags = IORESOURCE_IRQ, }, }; struct platform_device button_device = { .name = "my_button", // 这个名字必须要和设备资源结构体里面的name一致,不然这两个模块匹配不了 .id = -1, .num_resources = ARRAY_SIZE(button_resource), .resource = button_resource, .dev = { .release = button_release, }, }; // 入口函数 static int button_platform_device_init(void) { int ret = 0; printk("%s\n", __FUNCTION__); ret = platform_device_register(&button_device); // 注册设备信息 return ret; } // 出口函数 static void button_platform_device_exit(void) { printk("%s\n", __FUNCTION__); platform_device_unregister(&button_device); } module_init(button_platform_device_init); module_exit(button_platform_device_exit); MODULE_LICENSE("GPL");
2.2 平台驱动文件
button_driver.c
#include <linux/module.h> #include <linux/fs.h> #include <linux/errno.h> #include <linux/miscdevice.h> #include <linux/kernel.h> #include <linux/major.h> #include <linux/mutex.h> #include <linux/proc_fs.h> #include <linux/seq_file.h> #include <linux/stat.h> #include <linux/init.h> #include <linux/device.h> #include <linux/tty.h> #include <linux/kmod.h> #include <linux/gfp.h> #include <asm/mach/map.h> #include <linux/platform_device.h> #include <asm/io.h> static u32 *vi_ccgr; static u32 *vi_mux_ctl; static u32 *vi_pad_ctl; static u32 *vi_gdir; static u32 *vi_psr; static u32 pin; static u32 clock_offset; static int major = 0; static struct class *button_class; static ssize_t button_drv_read(struct file * file, char __user * buf, size_t size, loff_t *offset) { char status = 0; int ret; if (*vi_psr & (1<<pin)) { //printk("按键点击了\n"); status = 1; } else { status = 0; } ret = copy_to_user(buf, &status, 1); return 1; } static ssize_t button_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return 0; } static int button_drv_open(struct inode *node, struct file *file) { // 初始化设备 *(vi_ccgr) |= (3<<clock_offset); *(vi_mux_ctl) &= ~0xf; *(vi_mux_ctl) |= 0x5; *(vi_gdir) &= ~(1<<pin); printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return 0; } static int button_drv_close(struct inode *node, struct file *file) { printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__); return 0; } static struct file_operations button_opr = { .open = button_drv_open, .release = button_drv_close, .read = button_drv_read, .write = button_drv_write, }; static int button_probe(struct platform_device *pdev) { int i = 0; struct resource *res; u32 ph_addr[5]; int err; // 获取资源 for (i=0;i<5;i++) { res = platform_get_resource(pdev, IORESOURCE_MEM, i); if (res == NULL) { printk("not get resource\n"); return -1; } ph_addr[i] = res->start; } res = platform_get_resource(pdev, IORESOURCE_IRQ, 0); pin = res->start; res = platform_get_resource(pdev, IORESOURCE_IRQ, 1); clock_offset = res->start; // 内存映射 vi_ccgr = ioremap(ph_addr[0], 4); vi_mux_ctl = ioremap(ph_addr[1], 4); vi_pad_ctl = ioremap(ph_addr[2], 4); vi_gdir = ioremap(ph_addr[3], 4); vi_psr = ioremap(ph_addr[4], 4); // 完成设备注册的操作 // register_chrdev() class_create() device_create() major = register_chrdev(0, "my_button", &button_opr); button_class = class_create(THIS_MODULE, "button_class"); err = PTR_ERR(button_class); if (IS_ERR(button_class)) { unregister_chrdev(major, "my_button"); return -1; } device_create(button_class, NULL, MKDEV(major, 0), NULL, "my_button"); printk("%s\n", __FUNCTION__); return 0; } static int button_remove(struct platform_device *pdev) { printk("%s\n", __FUNCTION__); // 释放虚拟内存 iounmap(vi_ccgr); iounmap(vi_mux_ctl); iounmap(vi_pad_ctl); iounmap(vi_gdir); iounmap(vi_psr); // 完成设备取消注册的操作 device_destroy(button_class, MKDEV(major,0)); class_destroy(button_class); unregister_chrdev(major, "my_button"); printk("%s\n", __FUNCTION__); return 0; } struct platform_driver button_driver = { .probe = button_probe, .remove = button_remove, .driver = { .name = "my_button", }, }; // 入口函数 static int button_platform_driver_init(void) { int ret = 0; printk("%s\n", __FUNCTION__); ret = platform_driver_register(&button_driver); // 注册设备信息 return ret; } // 出口函数 static void button_platform_driver_exit(void) { printk("%s\n", __FUNCTION__); platform_driver_unregister(&button_driver); // 注册设备信息 } module_init(button_platform_driver_init); module_exit(button_platform_driver_exit); MODULE_LICENSE("GPL");
2.3 应用层测试程序
button_test.c
#include<stdio.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #include<unistd.h> int main(int argc, char **argv) { int fd; char status = 0; int ret = 0; int bnt = 0; if (argc !=2 ) { printf("Usage: %s <dev-path>\n", argv[0]); return -1; } fd = open(argv[1], O_RDWR); if (fd < 0) { perror("open error"); return -1; } while(1) { ret = read(fd, &status, 1); if (ret < 0) { perror("read error"); return -1; } else if (ret == 1 && status == 1) { printf("button %d\n", bnt++); } } return 0; }
2.3 Makefile文件
Makefile
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- // 该参数与你的交叉编译工具有关 export ARCH CROSS_COMPILE KERN_DIR = /home/hxd/workdir/ebf_linux_kernel_6ull_depth1/build_image/build // 该目录与你的编译好的内核目录有关 all: make -C $(KERN_DIR) M=`pwd` modules $(CROSS_COMPILE)gcc -o button_test button_test.c clean: make -C $(KERN_DIR) M=`pwd` modules clean rm -rf modules.order rm -f button_test obj-m += button_device.o button_driver.o
3. 编译运行
3.1 编译好后将得到的button_device.ko,button_driver.ko和button_test拷贝到开发板上。
我是直接用的nfs服务器
3.2 insmod来安装模块
3.3 运行测试程序
4. 遇到的问题
4.1 在定义资源的时候(struct resource),必须要要给end赋值
4.2 虚拟地址没有映射成功
编译没有报错,insmod没有报错,但运行就会报错。是因为你忘了映射或者映射失败,造成空地址的操作,从而报错
[12003.876125] Unable to handle kernel NULL pointer dereference at virtual address 00000000 [12003.885014] pgd = b0deced9 [12003.887738] [00000000] *pgd=00000000 [12003.894107] Internal error: Oops: 5 [#1] PREEMPT SMP ARM [12003.899448] Modules linked in: button_driver(O) button_device(O) g_multi snd_soc_imx_wm8960 snd_soc_wm8960 goodix brcmfmac brcmutil snd_soc_fsl_sai snd_soc_fsl_asrc imx_pcm_dma_v2 snd_soc_core snd_pcm_dmaengine snd_pcm snd_timer [last unloaded: button_driver] [12003.922477] CPU: 0 PID: 948 Comm: button_test Tainted: G O 4.19.35-imx6 #1.2202stable [12003.931615] Hardware name: Freescale i.MX6 UltraLite (Device Tree) [12003.937821] PC is at button_drv_open+0x10/0x70 [button_driver] [12003.943673] LR is at chrdev_open+0xac/0x194
4.3 报错处理经验
在遇到报错时,可以查看报错信息,一般都是有报错的堆栈信息,一级一级就能找到最初的报错函数,从而缩小范围。
也可以在函数内部使用printk打印一些信息来确定哪里出错了。