树莓派3B点灯(4)-- 自写驱动(低级版)

发布于:2024-09-18 ⋅ 阅读:(8) ⋅ 点赞:(0)

都说低级了,那么怎么说是低级呢?那就是直接操作寄存器了。突然想起很久以前做的路由器项目,当时增加了一个按键,就是在内核中直接读的寄存器位状态。当时很顺利,从接到任务,理解需求,查资料到交付,也就不到一周,所以印象不深。不过那时做的死循环这个印象深刻,换成现在肯定就是想做成中断来弄了。所以说低级并不是真的低级,只是现在封装做的很多了。光一个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 到 0x0014
  • GPSET0 到 GPSET1: 0x001C 到 0x0020
  • GPCLR0 到 GPCLR1: 0x0028 到 0x002C
  • GPLEV0 到 GPLEV1: 0x0034 到 0x0038

3. GPIO26的寄存器地址

GPIO26的寄存器地址计算如下:

  • Function Select RegisterGPFSEL2 (因为GPIO26在21-29范围内)
  • Output Set RegisterGPSET0 (因为GPIO26在0-31范围内)
  • Output Clear RegisterGPCLR0 (因为GPIO26在0-31范围内)
  • Level RegisterGPLEV0 (因为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.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
```

### 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` 子系统。


网站公告

今日签到

点亮在社区的每一天
去签到