目录

1.字符设备驱动程序框架

字符设备驱动程序框架:

编写驱动程序的套路:

 简单的LED驱动代码:

2.支持多个单板驱动的 LED 驱动程序

LED 驱动能支持多个板子的基础:分层思想

3.驱动设计的思想(面向对象、上下分层、左右分离)

3.1面向对象

3.2 分层

3.3 分离


1.字符设备驱动程序框架

        字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI, LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。

字符设备驱动程序框架:

 举例 open函数调用流程:

编写驱动程序的套路:

  1. 定义自己的 file_operations 结构体
  2. 确定主设备号major(0表示系统自动分配主设备号)
  3. 指定驱动入口函数和出口函数(module_init(xxx_init); module_exit(xxx_exit); )
  4. 在驱动入口函数中注册设备(register_chrdev )、在驱动出口函数中注销设备
  5. 使用 class_create 和 device_create 函数创建设备节点
  6. 使用 ioremap 函数将寄存器物理地址映射到系统虚拟地址(因为不能直接操作物理地址,只能通过操作虚拟地址间接操作寄存器)
  7. 配置协议(MODULE_LICENSE("GPL"))

 简单的LED驱动代码:

#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;
}


/*定义file_operations结构体*/
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");


2.支持多个单板驱动的 LED 驱动程序

LED 驱动能支持多个板子的基础:分层思想

把驱动拆分为:

  1. 通用的框架(leddrv.c)(与硬件无关)
  2. 具体的硬件操作(board_X.c)(与硬件相关)

如图 leddrv.c 是通用的,对于不同的单板,实现不同的 board_X.c :

led.opr.h 文件中:

  1. 声明 led_opr 结构体(定义 .init 和 .ctl );
  2.  声明获取具体单板 led_opr 结构体的函数 struct led_operations  *get_board_led_opr(void); (在 board_X.c 中定义的)

示例代码:

#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

在具体某单板的 board_X.c 文件中:

  1. 定义单板的 board_demo_led_init 函数和 board_demo_led_ctl 函数(即配置实现该单板功能的相关寄存器
  2. 定义该单板的 led_operations 结构体
  3. 定义 get_board_led_opr 函数(用于获取 led_operations 结构体)

board_demo.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 "led_opr.h"

static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */	   
{
	
	printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
	return 0;
}

static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
{
	printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
	return 0;
}

static struct led_operations board_demo_led_opr = {
	.init = board_demo_led_init,
	.ctl  = board_demo_led_ctl,
};

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

在通用的 leddrv.c 文件中

  1. 调用 get_board_led_opr 函数获取单板的 led_operations 结构体
  2. 获取 led_operations 结构体后,就是根据该结构体用户传入的主次设备号进行 led_init / led_exit 和 open / read / write

demo 示例代码:

#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 "led_opr.h"

#define LED_NUM 2

/* 1. 确定主设备号                                                                 */
static int major = 0;
static struct class *led_class;
struct led_operations *p_led_opr;


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

/* 3. 实现对应的open/read/write等函数,填入file_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;
}

/* write(fd, &val, 1); */
static ssize_t led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	int err;
	char status;

	//函数中虽然没有传入inode结构体,但在 led_drv_open 函数中已经将file和设备相关联,通过这个函数获取inode结构体
	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)
{	
	/*内核会根据用户传入的设备节点文件自动找到相应的inode结构体,其中包含了设备的主、次设备号*/
	int minor = iminor(node);
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* 根据次设备号初始化LED */
	p_led_opr->init(minor);
	
	return 0;
}

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

/* 2. 定义自己的file_operations结构体                                              */
static struct file_operations led_drv = {
	.owner	 = THIS_MODULE,
	.open    = led_drv_open,
	.read    = led_drv_read,
	.write   = led_drv_write,
	.release = led_drv_close,
};

/* 4. 把file_operations结构体告诉内核:注册驱动程序                                */
/* 5. 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数 */
static int __init led_init(void)
{
	int err;
	int i;
	
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	major = register_chrdev(0, "100ask_led", &led_drv);  /* /dev/led */


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

	for (i = 0; i < LED_NUM; i++)
		device_create(led_class, NULL, MKDEV(major, i), NULL, "100ask_led%d", i); /* /dev/100ask_led0,1,... */

	p_led_opr = get_board_led_opr();
	
	return 0;
}

/* 6. 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数           */
static void __exit led_exit(void)
{
	int i;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	for (i = 0; i < LED_NUM; i++)
		device_destroy(led_class, MKDEV(major, i)); /* /dev/100ask_led0,1,... */

	device_destroy(led_class, MKDEV(major, 0));
	class_destroy(led_class);
	unregister_chrdev(major, "100ask_led");
}


/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */

module_init(led_init);
module_exit(led_exit);

MODULE_LICENSE("GPL");

PS:获取主次设备节点的两个函数

1.在 int led_drv_open (struct inode *node, struct file *file) 中:

(参数中传入了 inode结构体,直接通过这个结构体获取)

/*内核会根据用户传入的设备节点文件自动找到相应的inode结构体,其中包含了设备的主、次设备号*/
int minor = iminor(node);

2.在 led_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset) 函数中:

(参数中没传入 inode 结构体,但在 led_drv_open 中,设备和 file 结构体已经相关联,先通过 file_inode 函数获取 inode 结构体)

//函数中虽然没有传入inode结构体,但在 led_drv_open 函数中已经将file和设备相关联,通过这个函数获取inode结构体
struct inode *inode = file_inode(file); 
int minor = iminor(inode);

应用层 ledtest.c 文件:

        传入参数,正常的 open / read / write;

demo 示例代码:

#include <string.h>
#include <unistd.h>
#include <stdio.h>


// ledtest /dev/myled on
// ledtest /dev/myled off

int main(int argc, char **argv)
{
	int fd;
	char status = 0;
	
	if (argc != 3)
	{
		printf("Usage: %s <dev> <on|off>\n", argv[0]);
		printf("  eg: %s /dev/myled on\n", argv[0]);
		printf("  eg: %s /dev/myled off\n", argv[0]);
		return -1;
	}
	// open
	fd = open(argv[1], O_RDWR);
	if (fd < 0)
	{
		printf("can not open %s\n", argv[0]);
		return -1;
	}

	// write
	if (strcmp(argv[2], "on") == 0)
	{
		status = 1;
	}

	write(fd, &status, 1);
	return 0;	
}

Makefile:

  1. 将 leddrv.c 和 board_demo.c 编译链接成一个ko文件:100ask.ko
  2. 交叉编译 ledtest.c 文件

demo 示例代码:

KERN_DIR = /home/book/100ask_roc-rk3399-pc/linux-4.4

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

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

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

# leddrv.c board_demo.c 编译成 100ask.ko
100ask_led-y := leddrv.o board_demo.o
obj-m	+= 100ask_led.o


3.驱动设计的思想(面向对象、上下分层、左右分离)

3.1面向对象

在前面 LED 驱动中的体现:

  1. 字符设备驱动程序抽象出一个 file_operations 结构体
  2. 我们写的程序针对硬件部分抽象出 led_operations 结构体;

3.2 分层

上下分层,在前面 LED 驱动程序就分为 2 层:

  1. 上层实现硬件无关的操作,比如注册字符设备驱动:leddrv.c
  2. 下层实现硬件相关的操作,比如 board_A.c 实现单板 A 的 LED 操作

3.3 分离

还能不能改进?分离

左右分离

在 board_A.c 中,实现了一个 led_operations,为 LED 引脚实现了 init、ctl 函数:

static struct led_operations board_demo_led_opr = { 
 .num = 1, 
 .init = board_demo_led_init, 
 .ctl = board_demo_led_ctl, 
}; 

如果硬件上更换一个引脚来控制 LED 怎么办?就要去修改上面结构体中的 init、ctl 函数

但其实每个GPIO引脚的操作都是类似的,我们可以设计一种通用的GPIO操作方法

比如单板 board_A.c 使用芯片 chipY,那就可以写出:chipY_gpio.c,它实现芯片 Y 的 GPIO 操作,适用于芯片 Y 的所有 GPIO 引脚,然后在board_A_led.c中定义专用于A板的引脚

led_resource.h 文件:

  1. 声明 led_resource 结构体( pin 的GPIO组号引脚号
  2. 声明获取该结构体的函数:get_led_resouce

demo 示例代码:

#ifndef _LED_RESOURCE_H
#define _LED_RESOURCE_H

/* GPIO3_0 */
/* bit[31:16] = group */ /*获取GPIO组号*/
/* bit[15:0]  = which pin */ /*获取引脚号*/
#define GROUP(x) (x>>16)
#define PIN(x)   (x&0xFFFF)
#define GROUP_PIN(g,p) ((g<<16) | (p)) //将GPIO组号和引脚号组装为一个32位整数

struct led_resource {
	int pin;
};

struct led_resource *get_led_resouce(void);

#endif

board_A_led.c 文件:

  1. 定义 A 板相关 PIN 的GPIO组号和引脚号的 led_resource 结构体
  2. 定义获取该结构体的函数:get_led_resouce

demo 示例代码:

#include "led_resource.h"

static struct led_resource board_A_led = {
	.pin = GROUP_PIN(3,1),
};

struct led_resource *get_led_resouce(void)
{
	return &board_A_led;
}

PS:

  1. 若需定义多种资源,如:按键、中断、... ;则每种资源都定义一个 resource 结构体
  2. 若在同一种资源中需要定义多个引脚,可以采用通过 ID 选择所需引脚的方法(类似一种数组,通过索引寻找),以 led 为例:
    #include "led_resource.h"
    
    /* 定义多个LED资源 */
    static struct led_resource board_leds[] = {
        {.pin = GROUP_PIN(3, 1)},  // LED 0
        {.pin = GROUP_PIN(3, 2)},  // LED 1
        {.pin = GROUP_PIN(5, 0)},  // LED 2
    };
    
    /* 根据ID获取LED资源 */
    struct led_resource *get_led_resource(int led_id)
    {
        if (led_id >= 0 && led_id < sizeof(board_leds) / sizeof(board_leds[0])) {
            return &board_leds[led_id];
        }
        return NULL;  // 无效ID返回NULL
    }

    调用示例:

    struct led_resource *led0 = get_led_resource(0);  // 获取第1个LED
    struct led_resource *led1 = get_led_resource(1);  // 获取第2个LED

chip_demo_gpio.c 文件 :

  1. 获取A板相关引脚信息的 led_resource 结构体,并写出引脚初始化和控制函数
  2. 根据引脚初始化和控制函数定义 led_operations 结构体
  3. 定义获取该板 led_operations 结构体的函数:get_board_led_opr

demo 示例代码:

#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 "led_opr.h"
#include "led_resource.h"

static struct led_resource *led_rsc;
static int board_demo_led_init (int which) /* 初始化LED, which-哪个LED */	   
{	
	//printk("%s %s line %d, led %d\n", __FILE__, __FUNCTION__, __LINE__, which);
	if (!led_rsc)
	{
		led_rsc = get_led_resouce();
	}
	
	printk("init gpio: group %d, pin %d\n", GROUP(led_rsc->pin), PIN(led_rsc->pin));
	switch(GROUP(led_rsc->pin))
	{
		case 0:
		{
			printk("init pin of group 0 ...\n");
			break;
		}
		case 1:
		{
			printk("init pin of group 1 ...\n");
			break;
		}
		case 2:
		{
			printk("init pin of group 2 ...\n");
			break;
		}
		case 3:
		{
			printk("init pin of group 3 ...\n");
			break;
		}
	}
	
	return 0;
}

static int board_demo_led_ctl (int which, char status) /* 控制LED, which-哪个LED, status:1-亮,0-灭 */
{
	//printk("%s %s line %d, led %d, %s\n", __FILE__, __FUNCTION__, __LINE__, which, status ? "on" : "off");
	printk("set led %s: group %d, pin %d\n", status ? "on" : "off", GROUP(led_rsc->pin), PIN(led_rsc->pin));

	switch(GROUP(led_rsc->pin))
	{
		case 0:
		{
			printk("set pin of group 0 ...\n");
			break;
		}
		case 1:
		{
			printk("set pin of group 1 ...\n");
			break;
		}
		case 2:
		{
			printk("set pin of group 2 ...\n");
			break;
		}
		case 3:
		{
			printk("set pin of group 3 ...\n");
			break;
		}
	}

	return 0;
}

static struct led_operations board_demo_led_opr = {
	.init = board_demo_led_init,
	.ctl  = board_demo_led_ctl,
};

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

led_opr.h 文件

  1. 声明 led_operations 结构体
  2. 声明获取该结构体的函数:get_board_led_opr

demo 示例代码:

#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

ledtest.c 文件:

应用层测试函数

跟上面第二部分的一样

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐