完整驱动代码实现
前言:终于到实战环节了
前面我们讲了硬件原理、pinctrl 子系统、gpio 子系统、设备树配置。说实话,这些理论知识确实挺多的。但好消息是:当你真正写驱动代码的时候,你会发现代码其实挺简洁的。
这一章我们来分析完整的 LED 驱动代码,看看它是如何使用 pinctrl 和 gpio 子系统的。
驱动的分层设计
我们的驱动采用了分层设计,把硬件操作和应用接口分开了:
┌─────────────────────────────────────────────────────────────┐
│ 字符设备接口 (pinctrl_gpio_demo_04_driver) │
│ file_operations: open, read, write, release │
└──────────────────────────┬──────────────────────────────────┘
│ 调用
▼
┌─────────────────────────────────────────────────────────────┐
│ 硬件抽象层 (led_hw) │
│ led_hw_init, led_set_status, led_get_status │
└──────────────────────────┬──────────────────────────────────┘
│ 调用 GPIO API
▼
┌─────────────────────────────────────────────────────────────┐
│ GPIO 子系统 │
│ of_get_named_gpio, gpio_direction_output │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 硬件寄存器 │
└─────────────────────────────────────────────────────────────┘这种分层设计的好处是:硬件抽象层专注于硬件操作,字符设备层专注于用户接口。两者各司其职,代码更清晰。
硬件抽象层:led_hw.h
首先来看头文件:
#pragma once
#include <linux/types.h>
int led_hw_init(void);
void led_hw_deinit(void);
void led_set_status(bool status);
bool led_get_status(void);这个接口非常简洁,就四个函数:
led_hw_init:初始化 LED 硬件(从设备树读取配置,设置 GPIO)led_hw_deinit:清理硬件资源led_set_status:设置 LED 状态(true = 亮,false = 灭)led_get_status:获取 LED 状态
⚠️ 注意:这里的 status 参数使用布尔值,而不是 0/1。这更符合人类思维,true 表示开,false 表示关。实际的 GPIO 值由硬件抽象层内部处理。
硬件抽象层:led_hw.c
现在让我们来看硬件抽象层的实现。
数据结构
struct led_handle {
int gpio_sub_sys_nr; // GPIO 编号
struct device_node *device_tree_node; // 设备树节点
};
static struct led_handle led;这个结构体保存了 LED 的硬件信息。gpio_sub_sys_nr 是 GPIO 子系统的全局编号,device_tree_node 是设备树节点的指针。
初始化函数
int led_hw_init(void)
{
/* 1. 获取设备树节点 */
led.device_tree_node = of_find_node_by_path("/imx_aes_led");
if (led.device_tree_node == NULL) {
pr_err("dtsled node can not found!\n");
return -EINVAL;
}
pr_info("dtsled node has been found!\n");
/* 2. 获取 compatible 属性 */
struct property *proper = of_find_property(led.device_tree_node, "compatible", NULL);
if (proper == NULL) {
pr_err("compatible property find failed\n");
} else {
pr_info("compatible = %s\n", (char *)proper->value);
}
/* 3. 获取 status 属性 */
const char *str;
if (of_property_read_string(led.device_tree_node, "status", &str) < 0) {
pr_err("status read failed!\n");
} else {
pr_info("status = %s\n", str);
}
/* 4. 获取 GPIO 编号 */
led.gpio_sub_sys_nr = of_get_named_gpio(led.device_tree_node, "led-gpio", 0);
if (led.gpio_sub_sys_nr < 0) {
pr_err("Can not parse to get the gpio nr");
return -EINVAL;
} else {
pr_info("Get the gpio handle: %d\n", led.gpio_sub_sys_nr);
}
/* 5. 设置为输出模式,初始值为 1(LED 熄灭) */
gpio_direction_output(led.gpio_sub_sys_nr, 1);
pr_info("LED Hardware init finished!\n");
return 0;
}这个函数做了 5 件事:
- 获取设备树节点:
of_find_node_by_path根据路径查找节点。 - 读取 compatible 属性:这只是调试信息,验证设备树是否正确。
- 读取 status 属性:同样是为了调试。
- 获取 GPIO 编号:
of_get_named_gpio是关键函数,它从设备树的led-gpio属性解析 GPIO 编号。 - 设置方向:
gpio_direction_output把 GPIO 设置为输出模式,初始值为 1。
这里有个细节需要注意:初始值是 1。因为我们的 LED 是低电平有效的,所以 1 表示熄灭。
你可能会问:为什么不直接用 gpio_set_value,而要用 gpio_direction_output?
答案是:gpio_direction_output 做了两件事——设置方向和设置初始值。而 gpio_direction_input 和 gpio_set_value 是分开的两个操作。所以 gpio_direction_output 更方便。
⚠️ 注意:这里的 GPIO 编号是全局编号(3),不是控制器内编号(也是 3,但含义不同)。of_get_named_gpio 会自动处理这个转换。
设置和获取状态
void led_set_status(bool status)
{
// 设置 GPIO 值
gpio_set_value(led.gpio_sub_sys_nr, (int)(!status));
}
bool led_get_status(void)
{
return !gpio_get_value(led.gpio_sub_sys_nr);
}这里有个取反操作 !status 和 !gpio_get_value()。为什么?
因为我们的 LED 是低电平有效的:
- 写 0 → LED 亮
- 写 1 → LED 灭
但我们的接口定义是:
true→ LED 亮false→ LED 灭
所以需要取反。status = true 时,GPIO 写 0;status = false 时,GPIO 写 1。
清理函数
void led_hw_deinit(void)
{
pr_info("Deinit LED Hardware\n");
if (led.device_tree_node) {
of_node_put(led.device_tree_node);
led.device_tree_node = NULL;
}
}of_node_put 是释放设备树节点引用的函数。当你用完一个设备树节点后,应该调用这个函数来释放引用。
字符设备层:主驱动文件
现在让我们来看看主驱动文件,它提供了字符设备接口。
数据结构
struct IMXAesLED {
dev_t devid; // 设备号
struct cdev char_device_handle; // 字符设备
struct class *char_device_class; // 设备类
struct device *char_device_device; // 设备
} led_handle;这个结构体保存了字符设备相关的信息。dev_t 是设备号类型,cdev 是字符设备结构体,class 和 device 用于自动创建设备节点。
file_operations 结构体
static int aes_chardev_open(struct inode *inode, struct file *filp) {
pr_info("Device: %s called open!\n", CHARDEV_NAME);
filp->private_data = &led_handle;
return 0;
}
static ssize_t aes_chardev_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) {
if (*offt > 0) {
return 0; // EOF
}
if (cnt > 1) {
cnt = 1;
}
*offt += cnt;
const bool led_status = led_get_status();
const char user_indication = led_status ? '1' : '0';
if (copy_to_user(buf, &user_indication, cnt) != 0) {
pr_warn("Failed to pass the led status to user!\n");
return -EFAULT;
}
return cnt;
}
static ssize_t aes_chardev_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) {
pr_info("aes_chardev_write: cnt=%zu\n", cnt);
if (cnt > 2) {
pr_warn("Get the unexpected data, that's too much!\n");
return -EINVAL;
}
char user_led_new_status = 0;
if (copy_from_user(&user_led_new_status, buf, 1) != 0) {
pr_warn("Failed to set the led status from user!\n");
return -EFAULT;
}
const bool led_new_status = (user_led_new_status == '1') ? true : false;
pr_info("LED status: %d (user_led_new_status='%c')\n", led_new_status, user_led_new_status);
led_set_status(led_new_status);
return 1;
}
static int aes_chardev_release(struct inode *inode, struct file *filp) {
pr_info("Device: %s called close!\n", CHARDEV_NAME);
filp->private_data = NULL;
return 0;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = aes_chardev_open,
.read = aes_chardev_read,
.write = aes_chardev_write,
.release = aes_chardev_release,
};这里的实现和我们在字符设备章节讲的类似。唯一的不同是,读写操作调用的是硬件抽象层的函数,而不是直接操作寄存器。
初始化流程
static int __init pinctrl_gpio_demo_04_init(void)
{
pr_info("=== Pin Control And GPIO Demo ===\n");
/* 1. 初始化硬件抽象层 */
led_hw_init();
/* 2. 初始化字符设备 */
init_led_handle(&led_handle);
pr_info("========================\n");
return 0;
}初始化流程很简单:先初始化硬件,再初始化字符设备。
init_led_handle 函数
static int init_led_handle(struct IMXAesLED *led_handle)
{
pr_info("Init the User Interfaces and driver handles\n");
/* 1. 申请设备号 */
alloc_chrdev_region(&led_handle->devid, 0, LED_CNT, CHARDEV_NAME);
{
const auto led_major_number = MAJOR(led_handle->devid);
const auto led_minor_number = MINOR(led_handle->devid);
pr_info("LED handle get the device number: major: %u, minor: %u\n",
led_major_number, led_minor_number);
}
/* 2. 初始化 cdev */
led_handle->char_device_handle.owner = THIS_MODULE;
cdev_init(&led_handle->char_device_handle, &fops);
if (cdev_add(&led_handle->char_device_handle, led_handle->devid, LED_CNT) < 0) {
pr_warn("Error when trying to make a cdev in kernel\n");
return -1;
}
pr_info("cdev series api called success!\n");
/* 3. 创建设备类 */
led_handle->char_device_class = class_create(CHARDEV_NAME);
if (IS_ERR(led_handle->char_device_class)) {
pr_warn("Failed to create a class\n");
return PTR_ERR(led_handle->char_device_class);
}
pr_info("class create success!\n");
/* 4. 创建设备 */
led_handle->char_device_device =
device_create(led_handle->char_device_class, NULL, led_handle->devid, NULL, CHARDEV_NAME);
if (IS_ERR(led_handle->char_device_device)) {
pr_warn("Failed to create a device\n");
return PTR_ERR(led_handle->char_device_device);
}
pr_info("device create success!\n");
return 0;
}这个函数使用了新 API(cdev + class + device),和旧 API (register_chrdev) 相比,新 API 更灵活,也更安全。
和旧 API 的对比:
关于字符设备 API 的详细内容,可以参考 00_chardev_base/12_new_chardev_api_overview.md。
真实输出分析
现在让我们来看看驱动加载时的真实输出:
[ 95.894724] pinctrl_gpio_demo_04_driver: loading out-of-tree module taints kernel.
[ 95.895579] === Pin Control And GPIO Demo ===
[ 95.895626] dtsled node has been found!
[ 95.895638] compatible = imxaes_led
[ 95.895654] status = okay
[ 95.895706] Get the gpio handle: 3
[ 95.895730] LED Hardware init finished!
[ 95.895741] Init the User Interfaces and driver handles
[ 95.895755] LED handle get the device number: major: 241, minor: 0
[ 95.895778] cdev series api called success!
[ 95.895848] class create success!
[ 95.896419] device create success!
[ 95.896444] ========================每一行都对应代码中的一条 pr_info。你可以清楚地看到初始化流程:
- 找到设备树节点
- 读取 compatible 和 status 属性
- 获取 GPIO 编号(3)
- 硬件初始化完成
- 申请设备号(major: 241)
- cdev 初始化成功
- 创建 class 成功
- 创建设备成功
应用层测试
应用层可以通过 /dev/AES_LED 设备文件来控制 LED:
# 点亮 LED
printf "1" > /dev/AES_LED
# 熄灭 LED
printf "0" > /dev/AES_LED
# 读取状态
cat /dev/AES_LED真实的内核输出:
[ 108.091762] Device: AES_LED called open!
[ 108.092023] aes_chardev_write: cnt=1
[ 108.092051] LED status: 1 (user_led_new_status='1')
[ 108.092095] Device: AES_LED called close!小结
我们的驱动代码展示了如何正确使用 pinctrl 和 gpio 子系统:
- 设备树配置:引脚复用和电气特性由 pinctrl 子系统处理,GPIO 编号和极性在设备树中指定。
- 硬件抽象层:使用
of_get_named_gpio获取 GPIO 编号,使用gpio_direction_output和gpio_set_value控制 GPIO。 - 字符设备层:使用新 API (
cdev+class+device) 创建字符设备,提供用户接口。
说实话,这个驱动代码非常简洁。我们不再需要手动映射寄存器、不再需要计算配置值、不再需要担心引脚冲突。这一切都由子系统帮我们处理了。
这就是子系统的价值所在:让驱动开发者专注于设备逻辑,而不是硬件细节。
下一步: 阅读 08_build_and_test.md 了解如何编译和测试驱动。