都说低级了,那么怎么说是低级呢?那就是直接操作寄存器了。突然想起很久以前做的路由器项目,当时增加了一个按键,就是在内核中直接读的寄存器位状态。当时很顺利,从接到任务,理解需求,查资料到交付,也就不到一周,所以印象不深。不过那时做的死循环这个印象深刻,换成现在肯定就是想做成中断来弄了。所以说低级并不是真的低级,只是现在封装做的很多了。光一个led,在内核中就有gpio,pinctrl,leds这几个封装库。对于二次开发来说,直接去操作寄存器的机会也不多了。但是理解操作寄存器,对于理解整个系统还是很重要,所以还是抽时间再来看这个。
还是先看看芯片手册是怎么写的
https://www.raspberrypi.org/app/uploads/2012/02/BCM2835-ARM-Peripherals.pdf
树莓派3B的芯片是BCM2837,但是据说IO口和2835是通用的。所以将就一起看了。
大概是这样的:
GPIO寄存器偏移
GPIO寄存器的偏移地址如下:
- GPIO Function Select Registers (GPFSEL): 用于设置GPIO引脚的功能(输入、输出、其他功能)。
- GPIO Pin Output Set Registers (GPSET): 用于设置GPIO引脚为高电平。
- GPIO Pin Output Clear Registers (GPCLR): 用于设置GPIO引脚为低电平。
- GPIO Pin Level Registers (GPLEV): 用于读取GPIO引脚的电平状态。
从上面图中也可以看到,具体偏移地址如下:
GPFSEL0
到GPFSEL5
: 0x0000 到 0x0014GPSET0
到GPSET1
: 0x001C 到 0x0020GPCLR0
到GPCLR1
: 0x0028 到 0x002CGPLEV0
到GPLEV1
: 0x0034 到 0x0038
3. GPIO26的寄存器地址
GPIO26的寄存器地址计算如下:
- Function Select Register:
GPFSEL2
(因为GPIO26在21-29范围内) - Output Set Register:
GPSET0
(因为GPIO26在0-31范围内) - Output Clear Register:
GPCLR0
(因为GPIO26在0-31范围内) - Level Register:
GPLEV0
(因为GPIO26在0-31范围内)
操作寄存器,就是需要位操作了,这部分之前写过,C的位操作-CSDN博客
在这里,博通文档的描述是0x7E200000,但是代码不这样用。因为ARM核心通过MMU映射过。
0x7E200000:这是 外设的总线地址(bus address),这是博通文档中提供的外设寄存器的地址。这个地址是博通芯片内部总线使用的物理地址。
0x3F200000:这是 物理地址(peripheral physical address),用于用户空间访问。树莓派的 Linux 内核中使用这个地址来映射外设寄存器,用户和内核都通过该地址访问 GPIO 和其他外设。0x7E200000 是总线地址,它表示设备在芯片总线上的位置。对于 ARM 核心,外设基地址从 0x7E000000 被重新映射到 0x3F000000,以便 ARM CPU 能够访问这些外设。(树莓派 4 的外设地址从 0xFE000000 开始,而不是 0x3F000000,因为树莓派 4 使用了不同的 MMU 地址映射策略。)
所以代码如下:
my_gpio_led.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/io.h> // 用于内存映射IO
#include <linux/delay.h> // 延迟函数
#define BCM2837_PERI_BASE 0x3F000000 // 树莓派 3B 外设基地址
#define GPIO_BASE (BCM2837_PERI_BASE + 0x200000) // GPIO 基地址
#define GPFSEL2 (gpio_base + 0x08) // GPFSEL2 控制 GPIO 20-29
#define GPSET0 (gpio_base + 0x1C) // GPSET0 控制 GPIO 0-31 的置位
#define GPCLR0 (gpio_base + 0x28) // GPCLR0 控制 GPIO 0-31 的清除
#define GPIO_PIN 26 // 使用 GPIO 26 控制 LED
static void __iomem *gpio_base; // GPIO寄存器的映射基地址
// 初始化 GPIO26 为输出模式
static void gpio_init(void) {
unsigned int reg_val;
// 设置 GPIO26 为输出模式
reg_val = ioread32(GPFSEL2); // 读取 GPFSEL2 的值
reg_val &= ~(7 << 18); // 清除 GPIO26 的 FSEL 位 (三位控制每个 GPIO)
reg_val |= (1 << 18); // 设置 GPIO26 为输出 (001 = 输出)
iowrite32(reg_val, GPFSEL2); // 写回 GPFSEL2
}
// 点亮 LED (设置 GPIO26 输出高电平)
static void led_on(void) {
iowrite32((1 << GPIO_PIN), GPSET0); // 设置 GPIO26
}
// 关闭 LED (设置 GPIO26 输出低电平)
static void led_off(void) {
iowrite32((1 << GPIO_PIN), GPCLR0); // 清除 GPIO26
}
// 模块加载函数
static int __init my_led_init(void) {
pr_info("LED GPIO Module loaded\n");
// 映射 GPIO 基地址
gpio_base = ioremap(GPIO_BASE, 0x100); // 映射寄存器地址
if (!gpio_base) {
pr_err("Failed to map GPIO base address\n");
return -EBUSY;
}
// 初始化 GPIO26 为输出模式
gpio_init();
// 点亮 LED
led_on();
pr_info("LED on (GPIO26)\n");
return 0;
}
// 模块卸载函数
static void __exit my_led_exit(void) {
// 关闭 LED
led_off();
pr_info("LED off (GPIO26)\n");
// 解除 GPIO 基地址映射
if (gpio_base)
iounmap(gpio_base);
pr_info("LED GPIO Module unloaded\n");
}
module_init(my_led_init);
module_exit(my_led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("fanged");
MODULE_DESCRIPTION("Direct GPIO control for LED on Raspberry Pi 3B");
首先通过ioremap映射地址。之前那些寄存器都是外设寄存器,在SOC里面作为一个子模块,首先MMU把这些子模块的寄存器,映射到了主内存中,这样便于操作。然后ioremap把物理地址映射成虚拟地址。
一个寄存器是32位,也就是4个byte,通过ioread32读出,iowrite32写入。
Makefile
```makefile
obj-m += my_gpio_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_gpio_led.ko马上就能看到灯亮了。
sudo rmmod my_gpio_led则可以关灯。
在树莓派 3B 上,直接通过操作寄存器控制 GPIO26 的 LED,而不使用 `gpio` 子系统和 `pinctrl` 子系统,可以通过直接访问 **Broadcom BCM2837 SoC** 的 GPIO 寄存器来实现。我们将通过内核模块,手动访问 GPIO 寄存器,并控制 GPIO26 的输出来点亮 LED。
### 1. 了解树莓派 GPIO 寄存器
树莓派的 GPIO 控制器有以下重要的寄存器:
- **GPIO Function Select Registers (`GPFSELx`)**:用于配置引脚的功能(输入、输出或其他外围功能)。
- **GPIO Pin Output Set Registers (`GPSETx`)**:用于设置 GPIO 引脚的输出(置位)。
- **GPIO Pin Output Clear Registers (`GPCLRx`)**:用于清除 GPIO 引脚的输出(复位)。#### 树莓派 3B 的 GPIO26 对应的寄存器:
- **GPIO26** 对应于:
- **GPFSEL2**:GPIO26 的功能选择寄存器(用于设置 GPIO26 为输出模式)。
- **GPSET0**:用于将 GPIO26 置为高电平(点亮 LED)。
- **GPCLR0**:用于将 GPIO26 置为低电平(关闭 LED)。#### GPIO 基地址:
在树莓派 3B(BCM2837)上,**外设寄存器**的基地址为 `0x3F000000`。GPIO 寄存器位于基地址的偏移 `0x200000` 处,因此 GPIO 基地址为:
```
GPIO_BASE = 0x3F200000
```### 2. 编写内核模块:直接操作寄存器控制 LED
下面的代码将通过直接访问 GPIO 寄存器控制 GPIO26 上的 LED。我们将手动设置 GPIO26 为输出模式,并控制它的电平。
#### `my_gpio_led.c`
```c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/io.h> // 用于内存映射IO
#include <linux/delay.h> // 延迟函数#define BCM2837_PERI_BASE 0x3F000000 // 树莓派 3B 外设基地址
#define GPIO_BASE (BCM2837_PERI_BASE + 0x200000) // GPIO 基地址#define GPFSEL2 (gpio_base + 0x08) // GPFSEL2 控制 GPIO 20-29
#define GPSET0 (gpio_base + 0x1C) // GPSET0 控制 GPIO 0-31 的置位
#define GPCLR0 (gpio_base + 0x28) // GPCLR0 控制 GPIO 0-31 的清除#define GPIO_PIN 26 // 使用 GPIO 26 控制 LED
static void __iomem *gpio_base; // GPIO寄存器的映射基地址
// 初始化 GPIO26 为输出模式
static void gpio_init(void) {
unsigned int reg_val;// 设置 GPIO26 为输出模式
reg_val = ioread32(GPFSEL2); // 读取 GPFSEL2 的值
reg_val &= ~(7 << 18); // 清除 GPIO26 的 FSEL 位 (三位控制每个 GPIO)
reg_val |= (1 << 18); // 设置 GPIO26 为输出 (001 = 输出)
iowrite32(reg_val, GPFSEL2); // 写回 GPFSEL2
}// 点亮 LED (设置 GPIO26 输出高电平)
static void led_on(void) {
iowrite32((1 << GPIO_PIN), GPSET0); // 设置 GPIO26
}// 关闭 LED (设置 GPIO26 输出低电平)
static void led_off(void) {
iowrite32((1 << GPIO_PIN), GPCLR0); // 清除 GPIO26
}// 模块加载函数
static int __init my_led_init(void) {
pr_info("LED GPIO Module loaded\n");// 映射 GPIO 基地址
gpio_base = ioremap(GPIO_BASE, 0x100); // 映射寄存器地址
if (!gpio_base) {
pr_err("Failed to map GPIO base address\n");
return -EBUSY;
}// 初始化 GPIO26 为输出模式
gpio_init();// 点亮 LED
led_on();
pr_info("LED on (GPIO26)\n");return 0;
}// 模块卸载函数
static void __exit my_led_exit(void) {
// 关闭 LED
led_off();
pr_info("LED off (GPIO26)\n");// 解除 GPIO 基地址映射
if (gpio_base)
iounmap(gpio_base);pr_info("LED GPIO Module unloaded\n");
}module_init(my_led_init);
module_exit(my_led_exit);MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Direct GPIO control for LED on Raspberry Pi 3B");
```### 3. 编写 `Makefile`
编写一个 `Makefile` 文件用于编译内核模块:
```makefile
obj-m += my_gpio_led.oall:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modulesclean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
```### 4. 编译和加载内核模块
1. **编译模块**:
在 `my_gpio_led.c` 文件和 `Makefile` 所在目录下,执行以下命令进行编译:```bash
make
```2. **加载模块**:
使用 `insmod` 将编译好的内核模块加载到内核中:```bash
sudo insmod my_gpio_led.ko
```3. **检查内核日志**:
使用 `dmesg` 查看模块加载时的内核日志:```bash
dmesg | tail
```日志应显示 "LED on (GPIO26)",表明 GPIO26 上的 LED 已经点亮。
4. **卸载模块**:
当你想关闭 LED 并卸载模块时,使用以下命令:```bash
sudo rmmod my_gpio_led
```再次使用 `dmesg` 查看日志,应该显示 "LED off (GPIO26)",表示 LED 已关闭。
### 5. 解释代码逻辑
- **`ioremap` 和 `iounmap`**:这些函数用于将物理地址映射到虚拟地址空间,允许内核模块访问设备的寄存器。我们通过 `ioremap` 将 GPIO 基地址映射到内存。
- **寄存器操作**:
- `GPFSEL2`:设置 GPIO26 为输出模式。
- `GPSET0`:将 GPIO26 设置为高电平,点亮 LED。
- `GPCLR0`:将 GPIO26 设置为低电平,关闭 LED。
- **点亮和关闭 LED**:通过 `led_on()` 和 `led_off()` 函数直接操作 GPIO26 的寄存器。### 6. 总结
在这个例子中,我们跳过了 Linux 的 `gpio` 和 `pinctrl` 子系统,直接操作树莓派 3B 的 GPIO 寄存器,通过 `ioremap` 将寄存器映射到内存,并直接控制 GPIO26 的电平来点亮和关闭 LED。
这种方法提供了更低级的硬件访问,但需要更多的寄存器级别的控制。如果您需要对 GPIO 进行更复杂的操作,通常建议使用 `gpio` 子系统和 `pinctrl` 子系统。