越来越发现树莓派很友好了,用来学习真的太棒了!在板子上可以建立整套编译环境,不管是应用还是内核模块,都可以即编即用,效率高的像飙车。。。
建立环境很简单
sudo apt update
sudo apt install raspberrypi-kernel-headers build-essential
之后就可以gcc这些玩意了。。
这次打算做多个版本。V1就是最简单的普通版,实现LED闪烁。V2增加内核节点,可以在用户空间进行控制。V3则是增加设备树的匹配或者其他。提供IOCTL的接口这次就不写了,因为很久之前写过。。。
V1:
my_blink_led.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/kthread.h>
#define LED_GPIO_PIN 538 // GPIO26
static struct task_struct *blink_thread;
// LED控制线程
static int blink_led_thread(void *data) {
int state = 0;
// 让LED持续闪烁
while (!kthread_should_stop()) {
gpio_set_value(LED_GPIO_PIN, state);
state = !state; // 切换LED状态
msleep(500); // 闪烁频率:500ms开,500ms关
}
// 离开时确保关闭LED
gpio_set_value(LED_GPIO_PIN, 0);
return 0;
}
// 模块初始化
static int __init my_led_init(void) {
int ret;
// 申请GPIO26
ret = gpio_request(LED_GPIO_PIN, "LED_GPIO");
if (ret) {
pr_err("Failed to request GPIO %d\n", LED_GPIO_PIN);
return ret;
}
// 设置GPIO26为输出模式
ret = gpio_direction_output(LED_GPIO_PIN, 0);
if (ret) {
pr_err("Failed to set GPIO %d as output\n", LED_GPIO_PIN);
gpio_free(LED_GPIO_PIN);
return ret;
}
// 创建线程来控制LED闪烁
blink_thread = kthread_run(blink_led_thread, NULL, "led_blink_thread");
if (IS_ERR(blink_thread)) {
pr_err("Failed to create LED blink thread\n");
gpio_free(LED_GPIO_PIN);
return PTR_ERR(blink_thread);
}
pr_info("LED blink module loaded\n");
return 0;
}
// 模块退出
static void __exit my_led_exit(void) {
// 停止线程
if (blink_thread) {
kthread_stop(blink_thread);
}
// 释放GPIO
gpio_free(LED_GPIO_PIN);
pr_info("LED blink module unloaded\n");
}
module_init(my_led_init);
module_exit(my_led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("fanged");
MODULE_DESCRIPTION("Simple GPIO LED Blink Module");
调用的是Linux的GPIO库,主要是gpio_request,gpio_direction_output,gpio_set_value这几个函数。
这里有个注意的地方,在定义GPIO口的时候,要定义成内部的那个538。不能直接写26,否则要报错。之前有讲过,两个具体差异后面有时间再看看(TODO)。
Makefile
obj-m += my_blink_led.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
之后直接make,然后sudo insmod my_blink_led.ko,卸载则是sudo rmmod my_blink_led。
基本上insmod之后,马上就能看到LED闪了,就像天上的星星。
V2:
my_blink_led.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/kthread.h>
#include <linux/sysfs.h>
#include <linux/fs.h>
#define LED_GPIO_PIN 538 // 使用 GPIO26 控制LED
static struct task_struct *blink_thread;
static int blink_delay = 500; // LED 闪烁的时间间隔(毫秒)
static int led_state = 1; // LED 状态(1=开启,0=关闭)
static int is_blinking = 1; // 是否允许闪烁
// LED 控制线程
static int blink_led_thread(void *data) {
while (!kthread_should_stop()) {
if (is_blinking) {
gpio_set_value(LED_GPIO_PIN, led_state);
led_state = !led_state;
msleep(blink_delay);
} else {
gpio_set_value(LED_GPIO_PIN, led_state); // 设置当前LED状态
msleep(100);
}
}
gpio_set_value(LED_GPIO_PIN, 0); // 模块卸载时关闭LED
return 0;
}
// sysfs 节点:用于配置LED是否闪烁
static ssize_t blink_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) {
return sprintf(buf, "%d\n", is_blinking);
}
static ssize_t blink_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count) {
int blink;
if (kstrtoint(buf, 10, &blink))
return -EINVAL;
is_blinking = blink;
return count;
}
// sysfs 节点:用于配置闪烁间隔
static ssize_t delay_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) {
return sprintf(buf, "%d\n", blink_delay);
}
static ssize_t delay_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count) {
int delay;
if (kstrtoint(buf, 10, &delay))
return -EINVAL;
blink_delay = delay;
return count;
}
// 定义 sysfs 接口属性
static struct kobj_attribute blink_attr = __ATTR(blink, 0664, blink_show, blink_store);
static struct kobj_attribute delay_attr = __ATTR(delay, 0664, delay_show, delay_store);
static struct attribute *attrs[] = {
&blink_attr.attr,
&delay_attr.attr,
NULL,
};
// sysfs 属性组
static struct attribute_group attr_group = {
.attrs = attrs,
};
static struct kobject *blink_kobj; // sysfs kobject
// 模块初始化
static int __init my_led_init(void) {
int ret;
// 请求 GPIO26
ret = gpio_request(LED_GPIO_PIN, "LED_GPIO");
if (ret) {
pr_err("Failed to request GPIO %d\n", LED_GPIO_PIN);
return ret;
}
// 设置 GPIO26 为输出模式
ret = gpio_direction_output(LED_GPIO_PIN, 0);
if (ret) {
pr_err("Failed to set GPIO %d as output\n", LED_GPIO_PIN);
gpio_free(LED_GPIO_PIN);
return ret;
}
// 创建 sysfs 节点
blink_kobj = kobject_create_and_add("blink_led", kernel_kobj);
if (!blink_kobj) {
pr_err("Failed to create sysfs kobject\n");
gpio_free(LED_GPIO_PIN);
return -ENOMEM;
}
// 创建 sysfs 属性组
ret = sysfs_create_group(blink_kobj, &attr_group);
if (ret) {
pr_err("Failed to create sysfs group\n");
kobject_put(blink_kobj);
gpio_free(LED_GPIO_PIN);
return ret;
}
// 创建 LED 闪烁控制线程
blink_thread = kthread_run(blink_led_thread, NULL, "led_blink_thread");
if (IS_ERR(blink_thread)) {
pr_err("Failed to create LED blink thread\n");
sysfs_remove_group(blink_kobj, &attr_group);
kobject_put(blink_kobj);
gpio_free(LED_GPIO_PIN);
return PTR_ERR(blink_thread);
}
pr_info("LED blink module loaded\n");
return 0;
}
// 模块退出
static void __exit my_led_exit(void) {
// 停止 LED 控制线程
if (blink_thread) {
kthread_stop(blink_thread);
}
// 移除 sysfs 节点
sysfs_remove_group(blink_kobj, &attr_group);
kobject_put(blink_kobj);
// 释放 GPIO
gpio_free(LED_GPIO_PIN);
pr_info("LED blink module unloaded\n");
}
module_init(my_led_init);
module_exit(my_led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("fanged");
MODULE_DESCRIPTION("Simple GPIO LED Blink Module with sysfs control");
代码通过kobject_create_and_add创建了sysfs节点,sysfs_create_group创建群组。群组就是下面的控制点,在struct kobj_attribute这个结构中进行定义。这个结构最后有两个回调函数,第一个用来get,后面一个用来set。(这个也是老套路中的老套路了)
Makefile,编译方法,insmod方法和上面一样。之后马上就能在/sys/kernel、下面看到节点。
控制是否闪烁:
开
echo 1 | sudo tee /sys/kernel/blink_led/blink
关
echo 0 | sudo tee /sys/kernel/blink_led/blink
控制闪烁频率(单位毫秒):
echo 200 | sudo tee /sys/kernel/blink_led/delay
至于为什么这里不用echo 200 > /sys/kernel/blink_led/delay这样看起来要简单一点的写法呢?还是权限,如果是root,那么两个没有区别。如果不是root,那么后面这个会遇到权限不足的报错。
V3及以后
后面单独写吧。。
参考:
Android新增LED设备--从底层到上层理解安卓架构_安卓增加led灯节点-CSDN博客
要自己实现一个不使用内核 `leds` 模块的简单 LED 驱动程序,并通过设备树配置,可以按照以下步骤来完成:
1. **编写设备树配置**:定义 GPIO 和 LED 的相关信息。
2. **编写自定义内核模块(.ko 文件)**:通过 `GPIO` 控制 LED 的亮灭。
3. **通过 sysfs 暴露控制接口**:允许用户通过 `/sys` 文件系统控制 LED 的开关。
4. **设备树绑定**:让设备树来配置 GPIO 引脚。### 1. 设备树配置
首先,编写设备树配置,用于指定 LED 所使用的 GPIO 引脚和相关信息。**`my_leds.dts`** 文件:
```dts
/dts-v1/;
/plugin/;/ {
compatible = "my_led_driver";fragment@0 {
target = <&gpio>;
__overlay__ {
my_led {
compatible = "my-led";
gpios = <&gpio 26 0>; // 使用GPIO26,active high
};
};
};
};
```- `compatible = "my-led";`:用于匹配自定义的 LED 驱动。
- `gpios = <&gpio 26 0>;`:GPIO26 是用于控制 LED 的引脚,`0` 表示 "active high"(高电平点亮 LED)。### 2. 编写内核模块
接下来,编写一个简单的 LED 驱动模块,这个模块通过 sysfs 暴露 LED 的控制接口。**`my_led_driver.c`** 文件:
```c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/gpio.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/sysfs.h>// 设备结构体
struct my_led_data {
int gpio_num;
};// 打开和关闭 LED 的函数
static ssize_t led_on(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
struct my_led_data *led_data = dev_get_drvdata(dev);
int state;// 将用户输入转换为整数
if (kstrtoint(buf, 10, &state))
return -EINVAL;// 控制 GPIO 引脚
gpio_set_value(led_data->gpio_num, state);return count;
}// 定义 sysfs 接口
static DEVICE_ATTR(state, 0220, NULL, led_on);// 设备树匹配表
static const struct of_device_id my_led_of_match[] = {
{ .compatible = "my-led", },
{},
};
MODULE_DEVICE_TABLE(of, my_led_of_match);// 平台驱动的 probe 函数
static int my_led_probe(struct platform_device *pdev)
{
struct my_led_data *led_data;
struct device *dev = &pdev->dev;
int ret;// 分配数据结构
led_data = devm_kzalloc(dev, sizeof(*led_data), GFP_KERNEL);
if (!led_data)
return -ENOMEM;// 获取设备树中配置的 GPIO 引脚
led_data->gpio_num = of_get_named_gpio(dev->of_node, "gpios", 0);
if (led_data->gpio_num < 0)
return led_data->gpio_num;// 申请 GPIO
ret = devm_gpio_request_one(dev, led_data->gpio_num, GPIOF_OUT_INIT_LOW, "my_led");
if (ret) {
dev_err(dev, "Failed to request GPIO\n");
return ret;
}// 在 sysfs 中创建 "state" 文件,用于控制 LED
ret = device_create_file(dev, &dev_attr_state);
if (ret) {
dev_err(dev, "Failed to create sysfs entry\n");
return ret;
}dev_set_drvdata(dev, led_data);
return 0;
}// 平台驱动的 remove 函数
static int my_led_remove(struct platform_device *pdev)
{
device_remove_file(&pdev->dev, &dev_attr_state);
return 0;
}// 定义平台驱动结构
static struct platform_driver my_led_driver = {
.probe = my_led_probe,
.remove = my_led_remove,
.driver = {
.name = "my_led",
.of_match_table = my_led_of_match,
},
};module_platform_driver(my_led_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple GPIO LED driver");
```### 3. 驱动实现的关键点
#### a) **设备树支持**
- `of_device_id` 表中的 `compatible` 字符串 (`my-led`) 与设备树中定义的 `compatible = "my-led"` 进行匹配。
- `of_get_named_gpio` 函数用于从设备树中获取 GPIO 引脚编号。#### b) **GPIO 控制**
- 驱动中使用 `gpio_set_value()` 来控制 GPIO 引脚的电平,从而点亮或熄灭 LED。
- `devm_gpio_request_one()` 用于向内核请求控制指定的 GPIO 引脚。#### c) **sysfs 接口**
- 通过 `device_create_file()` 创建了一个 `state` 文件,允许用户通过 `/sys/class/` 来控制 LED 的状态(亮灭)。
- 当用户写入 `1` 或 `0` 到 `state` 文件时,驱动程序会根据输入改变 GPIO 的电平,从而控制 LED 的亮灭。### 4. 编译驱动
首先,编译内核模块(确保你已设置好内核编译环境)。
```bash
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
```### 5. 设备树加载
将编译好的设备树文件(`my_leds.dts`)转换为设备树覆盖文件:
```bash
dtc -@ -I dts -O dtb -o my_leds.dtbo my_leds.dts
sudo cp my_leds.dtbo /boot/overlays/
```在 `/boot/config.txt` 中添加以下行以加载设备树覆盖文件:
```bash
dtoverlay=my_leds
```### 6. 加载内核模块
加载编译好的驱动模块:
```bash
sudo insmod my_led_driver.ko
```检查内核日志,确认驱动已正确加载并找到设备树中的配置:
```bash
dmesg | grep my_led
```### 7. 控制 LED
现在可以通过 `sysfs` 控制 LED:
```bash
# 开启 LED
echo 1 > /sys/devices/platform/my_led/state# 关闭 LED
echo 0 > /sys/devices/platform/my_led/state
```### 8. 总结
- 设备树配置 (`my_leds.dts`) 用于定义 LED 使用的 GPIO 引脚。
- 自定义内核驱动 (`my_led_driver.c`) 实现了基本的 LED 控制逻辑,并通过 `sysfs` 暴露给用户控制。
- 可以通过设备树和 sysfs 文件系统来控制 LED 的亮灭。这是一个简单的例子,展示了如何通过设备树和自定义内核模块来控制 GPIO 引脚上的 LED。如果需要扩展功能,比如支持更多 LED 或者复杂的控制逻辑,可以在此基础上进行进一步的开发。