入坑树莓派-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设备驱动程序》中文第三版