入坑树莓派-07-BCM2835驱动研究

入坑树莓派-07-BCM2835驱动研究

1.前言

前面的文章中我们提到,树莓派4搭载的SOC是博通BCM2711,配套的函数库是bcm2835-1.73.tar.gz。

这个函数库实质上只有两个小文件:

我们尝试把他集成到之前写的控制单色LED灯小程序中,并删掉一切影响我们理解原理的代码,得到了如下的极简小程序:

#include <sys/types.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <time.h>

#define BCM2835_GPFSEL0             0x0000
#define BCM2835_GPSET0              0x001c
#define BCM2835_GPCLR0              0x0028

#define BCM2835_GPIO_FSEL_OUTP      0x01
#define BCM2835_GPIO_FSEL_MASK      0x07

volatile uint32_t *bcm2835_gpio = NULL;

uint32_t bcm2835_peri_read(volatile uint32_t* paddr)
{
    uint32_t ret;
    __sync_synchronize();
    ret = *paddr;
    __sync_synchronize();
    return ret;
}

void bcm2835_peri_write(volatile uint32_t* paddr, uint32_t value)
{
      __sync_synchronize();
      *paddr = value;
      __sync_synchronize();
}

void bcm2835_peri_set_bits(volatile uint32_t* paddr, uint32_t value, uint32_t mask)
{
    uint32_t v = bcm2835_peri_read(paddr);
    v = (v & ~mask) | (value & mask);
    bcm2835_peri_write(paddr, v);
}

void bcm2835_gpio_fsel(uint8_t pin, uint8_t mode)
{
    volatile uint32_t* paddr = bcm2835_gpio + BCM2835_GPFSEL0/4 + (pin/10);
    uint8_t   shift = (pin % 10) * 3;
    uint32_t  mask = BCM2835_GPIO_FSEL_MASK << shift;
    uint32_t  value = mode << shift;
    bcm2835_peri_set_bits(paddr, value, mask);
}

void bcm2835_gpio_set(uint8_t pin)
{
    volatile uint32_t* paddr = bcm2835_gpio + BCM2835_GPSET0/4 + pin/32;
    uint8_t shift = pin % 32;
    bcm2835_peri_write(paddr, 1 << shift);
}

void bcm2835_gpio_clr(uint8_t pin)
{
    volatile uint32_t* paddr = bcm2835_gpio + BCM2835_GPCLR0/4 + pin/32;
    uint8_t shift = pin % 32;
    bcm2835_peri_write(paddr, 1 << shift);
}

int bcm2835_init(void)
{
    FILE *fp = fopen("/proc/device-tree/soc/ranges", "rb");
    if (NULL == fp)
        return -1;

    unsigned char buf[16];
    if (fread(buf, 1, 16, fp) != 16)
    {
        fclose(fp);
        return -1;
    }
    fclose(fp);

    uint32_t peri_off = (buf[8]  << 24) | (buf[9]  << 16) | (buf[10] << 8) | (buf[11] << 0);
    uint32_t peri_siz = (buf[12] << 24) | (buf[13] << 16) | (buf[14] << 8) | (buf[15] << 0);

    int memfd = open("/dev/mem", O_RDWR | O_SYNC);
    if (memfd < 0)
        return -1;

    uint32_t* peri_addr = mmap(NULL, peri_siz, (PROT_READ | PROT_WRITE), MAP_SHARED, memfd, peri_off);
    if (peri_addr == MAP_FAILED)
    {
        close(memfd);
        return -1;
    }

    bcm2835_gpio = peri_addr + 0x200000 / 4;

    close(memfd);

    return 0;
}

// gcc -o blink blink.c

#define PIN_R 17  // 红色LED灯GPIO端口
#define PIN_Y 27  // 黄
#define PIN_G 22  // 绿
#define PIN_B 5   // 蓝

int main(int argc, char **argv)
{
    if (bcm2835_init() < 0)
      return 1;

    bcm2835_gpio_fsel(PIN_R, BCM2835_GPIO_FSEL_OUTP);
    bcm2835_gpio_fsel(PIN_Y, BCM2835_GPIO_FSEL_OUTP);
    bcm2835_gpio_fsel(PIN_G, BCM2835_GPIO_FSEL_OUTP);
    bcm2835_gpio_fsel(PIN_B, BCM2835_GPIO_FSEL_OUTP);
    while (1)
    {
        bcm2835_gpio_set(PIN_R);
        usleep(50000);
        bcm2835_gpio_clr(PIN_R);
        bcm2835_gpio_set(PIN_Y);
        usleep(50000);
        bcm2835_gpio_clr(PIN_Y);
        bcm2835_gpio_set(PIN_G);
        usleep(50000);
        bcm2835_gpio_clr(PIN_G);
        bcm2835_gpio_set(PIN_B);
        usleep(50000);
        bcm2835_gpio_clr(PIN_B);
    }
    return 0;
}

下面我们深入分析。

2.初始化

初始化函数是bcm2835_init函数。

这个函数的第一步是从linux的proc文件系统中打开了一个文件(/proc/device-tree/soc/ranges),然后从中读取了16个字节。

这16个字节的内容是有结构的,其中9~12字节存放的是树莓派外设的地址偏移量,13~16字节存放的是树莓派外设的内存大小。

拿到这两个信息后,我们就可以调用mmap,将外设的内存映射到进程的地址空间了:

GPIO是外设的一种,因此还要再将外设的地址转换为GPIO的地址:

后面,我们就可以通过向这块内存读写数据来实现跟GPIO引脚通信了(从GPIO内存到GPIO引脚的信号转换,是linux驱动层完成的)。

3.设置引脚

我们调用的是bcm2835_gpio_fsel函数,其中fsel=function select,即功能选择,即选择引脚的功能。

可选的功能定义如下:

可以看到,虽然有很多,但主要是预留的,真正有用的也就是输入/输出模式,即设置要从GPIO引脚上读取电平还是输出电平。

我们要点亮LED灯,因此是要输出电平:

我们看这个函数做了啥:

大概就是往前面映射的地址上的某个位置写了些东西,具体写了啥不用关心,因为这跟GPIO的驱动设计有关,我们理解到写了个数据这一层就行了。

4.控制LED灯

控制LED灯,其实就是控制LED灯所在的某个引脚的电平高低:

最终还是操作内存:

5.原理

上述过程的原理图如下:

  • GPIO是linux外设的一种,有专门的linux外设驱动程序
  • Linux系统启动时,会加载GPIO驱动程序,并将GPIO外设各个管脚映射到系统的内存中
  • 这块内存的具体位置(偏移量),记录在/proc/device-tree/soc/ranges
  • 然后调用mmap,填入相关的偏移量等信息,可以将内存映射到用户空间中,可以通过指针直接访问
  • 这块内存,包含两块信息,一块是各个管脚输入/输出状态,一块是各个管脚的具体值
  • 我们修改第一块内存的值,可以将设定不同管脚的输入输出状态
  • 根据输入/输出状态再修改或者读取第二块内存的值,就可以实现从各个管脚上读取/写入高低电平
  • 从而最终实现通过GPIO管脚传递信息的功能(点亮LED等)

bcm2835这个库,严格来讲是一个用户空间的驱动程序(区别于内核空间的驱动程序),他做的事情就是将上述过程封装成简单易用的接口,方便用户空间的应用程序(C代码、python代码等)控制外设。

6.参考资料

  • 《Linux设备驱动程序》中文第三版

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注