【Linux】基于Imx6ull Pro开发板和platform_device+platform_driver框架的LED驱动设计以及上机测试

一、简介

驱动方面一共有三种设计方式,一是将物理设备资源和其驱动操作写在同一个驱动文件里;二是BUS总线+platform_device+platform_driver的框架,三是使用现代规范的设备树方式(其底层是从设备树提取物理设备资源,然后由内核自动创建platform_device相关设备给驱动部分使用)+platform_driver。

重点是:基于正确的框架以及流程而设计开发驱动。不要嫌麻烦,如果想要持续性进行基于Linux内核的开发,就必须要懂得在Linux下的开发规范以及其框架。

注:在基于Linux内核的开发中,学习的东西能够帮助你极大地增强你程序设计的解耦能力,即不同的硬件资源(寄存器之类的具体资源)+对应的同类型硬件资源的对应驱动(GPIO类型驱动)+对于应用层面的不同模块的模块代码设计(GPIO类型下的不同输入输出模式下的ADC、EXTI对接不同硬件模块)------三层解耦思维。

1.1 原始写法

cpp 复制代码
//原始的驱动写法

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/delay.h>
#include <linux/poll.h>
#include <linux/mutex.h>
#include <linux/wait.h>
#include <linux/uaccess.h>
#include <linux/device.h>
#include <asm/io.h>

static int major;
static struct class *led_class;

/* registers */
// IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 地址:0x02290000 + 0x14
static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;

// GPIO5_GDIR 地址:0x020AC004
static volatile unsigned int *GPIO5_GDIR;

//GPIO5_DR 地址:0x020AC000
static volatile unsigned int *GPIO5_DR;

static ssize_t led_write(struct file *filp, const char __user *buf,
			 size_t count, loff_t *ppos)
{
	char val;
	int ret;
	
	/* copy_from_user : get data from app */
	ret = copy_from_user(&val, buf, 1);

	/* to set gpio register: out 1/0 */
	if (val)
	{
		/* set gpio to let led on */
		*GPIO5_DR &= ~(1<<3);
	}
	else
	{

		/* set gpio to let led off */
		*GPIO5_DR |= (1<<3);
	}
	return 1;
}

static int led_open(struct inode *inode, struct file *filp)
{
	/* enable gpio5
	 * configure gpio5_io3 as gpio
	 * configure gpio5_io3 as output 
	 */
	*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 &= ~0xf;
	*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 |= 0x5;

	*GPIO5_GDIR |= (1<<3);
	
	return 0;
}

static struct file_operations led_fops = {
	.owner		= THIS_MODULE,
	.write		= led_write,
	.open		= led_open,
};

/* 入口函数 */
static int __init led_init(void)
{
	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	major = register_chrdev(0, "100ask_led", &led_fops);

	/* ioremap */
	// IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 地址:0x02290000 + 0x14
	IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x02290000 + 0x14, 4);
	
	// GPIO5_GDIR 地址:0x020AC004
	GPIO5_GDIR = ioremap(0x020AC004, 4);
	
	//GPIO5_DR 地址:0x020AC000
	GPIO5_DR  = ioremap(0x020AC000, 4);

	led_class = class_create(THIS_MODULE, "myled");
	device_create(led_class, NULL, MKDEV(major, 0), NULL, "myled"); /* /dev/myled */
	
	return 0;
}

static void __exit led_exit(void)
{
	iounmap(IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3);
	iounmap(GPIO5_GDIR);
	iounmap(GPIO5_DR);
	
	device_destroy(led_class, MKDEV(major, 0));
	class_destroy(led_class);
	
	unregister_chrdev(major, "100ask_led");
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");

1.2 总线设备驱动模型(platform_device+platform_driver的框架)

在platform_device注入了资源,类似于在总线上挂载了物理设备资源

1.3 设备树模型(dts+platform_driver的框架)

由父节点和子节点都具备compatible属性才能支持直接转化为platform_device

二、基于BUS总线+platform_device+platform_driver框架的驱动代码

(1)工作流程

这里简单解释BUS总线+platform_device+platform_driver框架的工作流程

2.1 驱动代码:使用platform_device管理具体的设备资源

gpio_led_pdev_drv.c

cpp 复制代码
#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 <linux/platform_device.h>
#include <linux/of.h>

/* GPIO3_0 */
/* bit[31:16] = group */
/* bit[15:0]  = which pin */
#define GROUP(x) (x>>16)
#define PIN(x)   (x&0xFFFF)
#define GROUP_PIN(g,p) ((g<<16) | (p))

/* 定义LED相关寄存器物理地址资源 */
#define IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3_PHYS 0x02290000 + 0x14
#define GPIO5_GDIR_PHYS 0x020AC004
#define GPIO5_DR_PHYS 0x020AC000

// 在不使用设备树注册物理设备资源时,需要手动使用platform_device来注册物理设备资源

/* 定义LED引脚资源(与设备树作用相同) */
// 很多人不知道platform_device的资源是如何匹配的, 这里需要手动指定资源的start和flags
/* 资源定义:包含LED所需的所有寄存器地址 */
static struct resource led_resources[] = {
    {
        .start = IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3_PHYS,
        .end   = IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3_PHYS + 3,
        .flags = IORESOURCE_MEM,
        .name  = "led_mux_reg",
    },
    {
        .start = GPIO5_GDIR_PHYS,
        .end   = GPIO5_GDIR_PHYS + 3,
        .flags = IORESOURCE_MEM,
        .name  = "led_gdir_reg",
    },
    {
        .start = GPIO5_DR_PHYS,
        .end   = GPIO5_DR_PHYS + 3,
        .flags = IORESOURCE_MEM,
        .name  = "led_dr_reg",
    },
};

static void led_dev_release(struct device *dev)
{
    printk("led platform device released\n");
}

/* 定义平台设备 */
static struct platform_device gpio_led_pdev = {
    .name           = "100ask_led",  /* 用于与平台驱动匹配 */
    .id             = -1,
    .num_resources  = ARRAY_SIZE(led_resources),
    .resource       = led_resources,
    .dev            = {
        .release = led_dev_release,
    },
};

static int __init gpio_led_pdev_init(void)
{
    int err;
    err = platform_device_register(&gpio_led_pdev); //注册platform_device物理设备

    return 0;
}

static void __exit gpio_led_pdev_exit(void)
{
	platform_device_unregister(&gpio_led_pdev);
}

module_init(gpio_led_pdev_init);
module_exit(gpio_led_pdev_exit);
MODULE_LICENSE("GPL");

2.2 驱动代码:编写LED字符设备驱动,使用字符设备驱动给用户提供操作接口

led_opr.h

cpp 复制代码
#ifndef _LED_OPR_H
#define _LED_OPR_H

struct led_operations {
	int (*init) (int which); /* 初始化LED, which-哪个LED */       
	int (*ctl) (int which, char status); /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
};

struct led_operations *get_board_led_opr(void);


#endif

gpio_led_chardev_drv.h

cpp 复制代码
#ifndef __GPIO_LED_CHARDEV_DRV_H
#define __GPIO_LED_CHARDEV_DRV_H

#include "led_opr.h"

void led_class_create_device(int minor);
void led_class_destroy_device(int minor);
void register_led_operations(struct led_operations *opr);

#endif

gpio_led_chardev_drv.c

cpp 复制代码
#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 "gpio_led_chardev_drv.h"

// 对于led字符设备驱动,需要实现open, release, read, write等函数
// 是基于 物理设备的GPIO引脚的platform_device ---> 字符设备操作接口驱动封装 ---> 使用platform_driver来注册字符设备驱动和匹配物理设备

static int major = 0;
static struct class *led_class;
struct led_operations *p_led_opr;


#define MIN(a, b) (a < b ? a : b)


void led_class_create_device(int minor)
{
	device_create(led_class, NULL, MKDEV(major, minor), NULL, "gpio_led_chardev%d", minor); /* /dev/gpio_led_chardev0,1,... */
}
void led_class_destroy_device(int minor)
{
	device_destroy(led_class, MKDEV(major, minor));
}
void register_led_operations(struct led_operations *opr)
{
	p_led_opr = opr;
}

EXPORT_SYMBOL(led_class_create_device);
EXPORT_SYMBOL(led_class_destroy_device);
EXPORT_SYMBOL(register_led_operations);

static ssize_t led_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	int err;
	char status;
	struct inode *inode = file_inode(file);
	int minor = iminor(inode);
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	err = copy_from_user(&status, buf, 1);

	/* 根据次设备号和status控制LED */
	p_led_opr->ctl(minor, status);
	
	return 1;
}

static int led_drv_open (struct inode *node, struct file *file)
{
	int minor = iminor(node);
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* 根据次设备号初始化LED */
	p_led_opr->init(minor); // p_led_opr->init 其具体实现在上一层的gpio_led_pdrv_drv.c
	
	return 0;
}

static int led_drv_close (struct inode *node, struct file *file)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static struct file_operations gpio_led_chardev_fops = {
	.owner	 = THIS_MODULE,
	.open    = led_drv_open,
	.read    = led_drv_read,
	.write   = led_drv_write,
	.release = led_drv_close,
};

static int __init gpio_led_chardev_init(void)
{
	int err;
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	major = register_chrdev(0, "gpio_led_chardev", &gpio_led_chardev_fops);  /*  注册字符设备 /dev/gpio_led_chardev */


	led_class = class_create(THIS_MODULE, "gpio_led_chardev_class"); // 创建类 /sys/class/gpio_led_chardev_class

	err = PTR_ERR(led_class);
	if (IS_ERR(led_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "gpio_led_chardev");
		return -1;
	}
	
	return 0;
}

static void __exit gpio_led_chardev_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	class_destroy(led_class);
	unregister_chrdev(major, "gpio_led_chardev");
}

module_init(gpio_led_chardev_init);
module_exit(gpio_led_chardev_exit);
MODULE_LICENSE("GPL");

2.3 驱动代码:使用platform_driver管理对应的同类型的设备驱动,同时其中接入LED字符设备驱动接口,从而达到操作字符设备控制开发板的实际硬件

gpio_led_pdrv_drv.c

cpp 复制代码
#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 <linux/platform_device.h>  // 包含platform设备模型相关定义
#include <linux/io.h>              // 包含ioremap/iounmap等函数定义
#include <linux/of.h>              // 包含设备树相关结构体定义(of_device_id)

#include "led_opr.h"
#include "gpio_led_chardev_drv.h"

/* GPIO3_0 */
/* bit[31:16] = group */
/* bit[15:0]  = which pin */
#define GROUP(x) (x>>16)
#define PIN(x)   (x&0xFFFF)
#define GROUP_PIN(g,p) ((g<<16) | (p))

/* 保存映射后的寄存器虚拟地址 */
static volatile unsigned int *g_mux_reg;
static volatile unsigned int *g_gdir_reg;
static volatile unsigned int *g_dr_reg;

/* LED初始化:配置GPIO复用和方向 */
static int led_init(int which)
{
    printk("led_init: configure LED hardware\n");
    
    /* 配置复用为GPIO功能 */
    *g_mux_reg &= ~0xf;
    *g_mux_reg |= 0x5;
    
    /* 配置为输出模式 */
    *g_gdir_reg |= (1 << 3);  // GPIO5_3输出
    
    return 0;
}

/* LED控制:亮/灭 */
static int led_ctl(int which, char status)
{
    printk("led_ctl: set LED %s\n", status == '1' ? "on" : "off");
    
    if (status == '1') {
        /* 点亮:清除bit3 */
        *g_dr_reg &= ~(1 << 3);
    } else if (status == '0') {
        /* 熄灭:设置bit3 */
        *g_dr_reg |= (1 << 3);
    }
    return 0;
}

/* 定义LED操作接口 */
static struct led_operations led_opr = {
    .init = led_init,
    .ctl  = led_ctl,
};

// platform_driver需要匹配设备树或者platform_device的实际设备资源, 使用platform_driver_register()注册平台驱动

struct led_operations *get_board_led_opr(void)
{
    return &led_opr;
}

/* 平台驱动probe函数:匹配设备后执行 */
static int gpio_led_pdrv_probe(struct platform_device *pdev)
{
    struct resource *res;
    
    printk("led_pdrv_probe: device matched\n");
    
    /* 获取并映射复用寄存器资源 */
    res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "led_mux_reg");
    g_mux_reg = ioremap(res->start, resource_size(res));
    
    /* 获取并映射方向寄存器资源 */
    res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "led_gdir_reg");
    g_gdir_reg = ioremap(res->start, resource_size(res));
    
    /* 获取并映射数据寄存器资源 */
    res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "led_dr_reg");
    g_dr_reg = ioremap(res->start, resource_size(res));
    
    /* 注册LED操作接口 */
    register_led_operations(&led_opr);
    
    /* 创建字符设备节点 */
    led_class_create_device(0);  // 次设备号0
    
    return 0;
}

static int gpio_led_pdrv_remove(struct platform_device *pdev)
{
    printk("led_pdrv_remove: device removed\n");
    
    /* 销毁字符设备节点 */
    led_class_destroy_device(0);
    
    /* 解除地址映射 */
    iounmap(g_mux_reg);
    iounmap(g_gdir_reg);
    iounmap(g_dr_reg);
    
    return 0;
}

/* 定义平台驱动 */
static struct platform_driver gpio_led_pdrv = {
    .probe  = gpio_led_pdrv_probe,
    .remove = gpio_led_pdrv_remove,
    .driver = {
        .name = "100ask_led",  /* 与平台设备name匹配 */
    },
};

static int __init gpio_led_pdrv_init(void)
{
    int err;
    err = platform_driver_register(&gpio_led_pdrv); 
    return 0;
}

static void __exit gpio_led_pdrv_exit(void)
{
    platform_driver_unregister(&gpio_led_pdrv);
}

module_init(gpio_led_pdrv_init);
module_exit(gpio_led_pdrv_exit);
MODULE_LICENSE("GPL");

2.4 应用代码:使用LED字符设备驱动编写应用程序

cpp 复制代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv)
{
    int fd;
    char status;

    if(argc != 3)
    {
        printf("Usage: %s <dev> <status>\n", argv[0]);
        return -1;
    }

    fd = open(argv[1], O_RDWR);
    if(fd < 0)
    {
        printf("open %s failed\n", argv[1]);
        return -1;
    }

    if(0 == strcmp(argv[2], "on"))
    {
        status = '1';
        write(fd, &status, 1);
    }else if(0 == strcmp(argv[2], "off"))
    {
        status = '0';
        write(fd, &status, 1);
    }

    close(fd);

    return 0;
}

2.5 makefile

cpp 复制代码
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH,          比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH,          比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin 
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
#       请参考各开发板的高级用户使用手册

# export ARCH=arm
# export CROSS_COMPILE=arm-linux-gnueabihf-
# export PATH=$PATH:/home/zky/ProjectsHub/Linux_Projects/100ask_imx6ull_pro_Env/Imx6ull_Pro_Programs_Workspaces/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/bin

KERN_DIR = /home/zky/ProjectsHub/Linux_Projects/100ask_imx6ull_pro_Env/100ask_imx6ull-sdk/Linux-4.9.88

all:
	make -C $(KERN_DIR) M=`pwd` modules
	$(CROSS_COMPILE)gcc -o app_led app_led.c

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order

# 参考内核源码drivers/char/ipmi/Makefile
# 要想把a.c, b.c编译成ab.ko, 可以这样指定:
# ab-y := a.o b.o
# obj-m += ab.o



obj-m += gpio_led_pdev_drv.o gpio_led_pdrv_drv.o gpio_led_chardev_drv.o

三、上机测试

3.1 编译

cpp 复制代码
export ARCH=arm

export CROSS_COMPILE=arm-linux-gnueabihf-

export PATH=$PATH:/home/zky/ProjectsHub/Linux_Projects/100ask_imx6ull_pro_Env/Imx6ull_Pro_Programs_Workspaces/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/bin

make

adb push app_led /root/Imx6ull_Pro_Examples_Workspaces_DIR/EX2_Linux_Driver_Layer_DIR/E4_led_adv_DIR

adb push gpio_led_pdev_drv.ko /root/Imx6ull_Pro_Examples_Workspaces_DIR/EX2_Linux_Driver_Layer_DIR/E4_led_adv_DIR

adb push gpio_led_chardev_drv.ko /root/Imx6ull_Pro_Examples_Workspaces_DIR/EX2_Linux_Driver_Layer_DIR/E4_led_adv_DIR

adb push gpio_led_pdrv_drv.ko /root/Imx6ull_Pro_Examples_Workspaces_DIR/EX2_Linux_Driver_Layer_DIR/E4_led_adv_DIR

adb shell

cd /root/Imx6ull_Pro_Examples_Workspaces_DIR/EX2_Linux_Driver_Layer_DIR/E4_led_adv_DIR

lsmod

insmod gpio_led_pdev_drv.ko

insmod gpio_led_pdev_drv.ko

insmod gpio_led_pdev_drv.ko

lsmod

# 注册字符设备成功
ls /dev/gpio_led_*

# 使用应用程序操作字符设备
./app_led /dev/gpio_led_chardev0 on

./app_led /dev/gpio_led_chardev0 off

3.2 上机

实际中确实操作字符设备并点亮了led

cpp 复制代码
# 开启内核的printk信息打印功能,但是这个只能在串口那里看到,而adb口不会输出信息
echo "7 4 1 7" > /proc/sys/kernel/printk

注:切记不要死记硬背,建议实际上手去亲身体会并理解。

相关推荐
2035去旅行17 小时前
bootcmd 详细讲解
linux·嵌入式·arm·soc
陈希12318 小时前
Linux 防火墙配置实战:firewalld、iptables 与云安全组
linux·运维·服务器
女神下凡19 小时前
芯参谋(95):嵌入式文件系统_开发指南
linux·服务器·arm开发·驱动开发·设计规范
且从容.1 天前
Ubuntu安装flume
linux·ubuntu·flume
承渊政道1 天前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
且从容.1 天前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落1 天前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨1 天前
WSL2的安装与使用
linux·wsl
微小的鱼1 天前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng1 天前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器