普通的驱动就是实现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打印一些信息来确定哪里出错了。