嵌入式Linux驱动——2 LED驱动(简单、面向对象、分层、分离)
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI, LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
目录
1.字符设备驱动程序框架
字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。比如我们最常见的点灯、按键、IIC、SPI, LCD 等等都是字符设备,这些设备的驱动就叫做字符设备驱动。
字符设备驱动程序框架:

举例 open函数调用流程:

编写驱动程序的套路:
- 定义自己的 file_operations 结构体
- 确定主设备号major(0表示系统自动分配主设备号)
- 指定驱动入口函数和出口函数(module_init(xxx_init); module_exit(xxx_exit); )
- 在驱动入口函数中注册设备(register_chrdev )、在驱动出口函数中注销设备
- 使用 class_create 和 device_create 函数创建设备节点
- 使用 ioremap 函数将寄存器物理地址映射到系统虚拟地址(因为不能直接操作物理地址,只能通过操作虚拟地址间接操作寄存器)
- 配置协议(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 驱动能支持多个板子的基础:分层思想
把驱动拆分为:
- 通用的框架(leddrv.c)(与硬件无关)
- 具体的硬件操作(board_X.c)(与硬件相关)
如图 leddrv.c 是通用的,对于不同的单板,实现不同的 board_X.c :

在 led.opr.h 文件中:
- 声明 led_opr 结构体(定义 .init 和 .ctl );
- 声明获取具体单板 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 文件中:
- 定义单板的 board_demo_led_init 函数和 board_demo_led_ctl 函数(即配置实现该单板功能的相关寄存器)
- 定义该单板的 led_operations 结构体
- 定义 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 文件中
- 调用 get_board_led_opr 函数获取单板的 led_operations 结构体
- 获取 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:
- 将 leddrv.c 和 board_demo.c 编译链接成一个ko文件:100ask.ko
- 交叉编译 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 驱动中的体现:
- 字符设备驱动程序抽象出一个 file_operations 结构体;
- 我们写的程序针对硬件部分抽象出 led_operations 结构体;
3.2 分层
上下分层,在前面 LED 驱动程序就分为 2 层:
- 上层实现硬件无关的操作,比如注册字符设备驱动:leddrv.c
- 下层实现硬件相关的操作,比如 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 文件:
- 声明 led_resource 结构体( pin 的GPIO组号和引脚号)
- 声明获取该结构体的函数: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 文件:
- 定义 A 板相关 PIN 的GPIO组号和引脚号的 led_resource 结构体
- 定义获取该结构体的函数: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:
- 若需定义多种资源,如:按键、中断、... ;则每种资源都定义一个 resource 结构体
- 若在同一种资源中需要定义多个引脚,可以采用通过 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 文件 :
- 获取A板相关引脚信息的 led_resource 结构体,并写出引脚初始化和控制函数
- 根据引脚初始化和控制函数定义 led_operations 结构体
- 定义获取该板 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 文件
- 声明 led_operations 结构体
- 声明获取该结构体的函数: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 文件:
应用层测试函数
跟上面第二部分的一样
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐



所有评论(0)