树莓派3B-linux控制GPIO(不用树莓派的库)

这篇博客记录了我在用户程序中将物理地址映射到虚拟地址,然后使用虚拟地址控制树莓派3B的GPIO的过程。以下是整个过程的记录:

1、下载数据手册

和控制单片机IO口相似,如果用户想控制树莓派的GPIO,就得先知道GPIO相关寄存器的地址和设置的方法。树莓派的网站上提供了外设说明手册(Peripheral specification),这个手册对芯片上的外设怎么使用进行了描述。不过,Pi 3 的处理器是BCM2837,官网只提供了BCM2835(Pi 1 处理器)的外设说明手册。由于两个芯片外设上区别不大,我直接下载了BCM2835的手册来参考。下载手册的网址:https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2835/README.md

2、查阅GPIO相关寄存器地址和设置方法

翻到第5页,可以看到下图这个关于ARM地址映射的描述。

树莓派3B-linux控制GPIO(不用树莓派的库)_第1张图片

中间的部分为ARM的物理地址分配方式。IO外设(IO Peripherals)的物理地址分配在0x20000000(这是BCM2835的)。由于芯片不同,BCM2837的IO设备地址已经改为 0x3F000000,这一点,在官网提供的文档中也有说到。(https://www.raspberrypi.org/documentation/hardware/raspberrypi/peripheral_addresses.md)

从第89页开始,描述的就是GPIO外设的地址和设置方法。

第90-91页的表格标明了和GPIO相关的寄存器的地址(下图是90页的部分信息)。

树莓派3B-linux控制GPIO(不用树莓派的库)_第2张图片

GPFESLn(选择引脚功能)、GPSETn(设置引脚输出高电平)和GPCLRn(设置引脚输出低电平)是控制引脚输出电平需要用到的寄存器。手册后面的几页内容将详细描述这些寄存器如何设置。例如GPFSEL1的描述为:

树莓派3B-linux控制GPIO(不用树莓派的库)_第3张图片

根据手册的描述,我们得到了GPIO相关寄存器的地址和设置方法,接下来将编写一个控制引脚输出电平的程序。

3、根据手册的描述编写程序

我在这里选择GPIO的Pin3作为实验对象,以下的程序是以Pin3为例。

#include 
#include       //open函数的定义
#include      //close函数的定义
#include   
#include    //mmap函数的定义
#include       //errno的定义
#include 
#include      //uint8_t、uint32_t等类型的定义
#include      //sleep函数的定义

//BCM2837外设的物理地址
#define PERIPHERALS_PHY_BASE 0x3F000000
//外设物理地址的数量
#define PERIPHERALS_ADDR_SIZE 0x01000000

//引脚高电平
#define HIGH 0x01
//引脚低电平
#define LOW 0x00

int memfd;
                   
volatile uint32_t* bcm2837_peripherals_base;
volatile uint32_t* bcm2837_gpio_base;

//定义寄存器地址
volatile uint32_t* GPFSEL0;
volatile uint32_t* GPSET0;
volatile uint32_t* GPCLR0;


//将物理地址映射到用户进程的虚拟地址
int8_t paddr2vaddr();
//设置引脚3为输出功能
void pin3_select_output();
//控制引脚3的电平
void pin3_ctrl(uint8_t level);
//往地址addr写入值value
void write_addr(volatile uint32_t* addr, uint32_t value);
//读取地址addr的值
uint32_t read_addr(volatile uint32_t* addr);

int main()
{
    //物理地址映射到虚拟地址
    if(!paddr2vaddr())
    {
        return 0;
    }
        
    //pin3的功能选择为输出
    pin3_select_output();

    printf("Pin3 level:\n");
    while(1)
    {
    	//每两秒反转电平一次
        printf("High\n");
        pin3_ctrl(HIGH);
        sleep(2);
        printf("Low\n");
        pin3_ctrl(LOW);
        sleep(2);
    }
}

int8_t paddr2vaddr()
{
    if( (memfd = open("/dev/mem", O_RDWR | O_SYNC))  >= 0 )
    {
    	//“/dev/mem”内是物理地址的映像
    	//通过mmap函数将物理地址映射为用户进程的虚拟地址
        bcm2837_peripherals_base = mmap(NULL, PERIPHERALS_ADDR_SIZE, (PROT_READ | PROT_WRITE),
                                        MAP_SHARED, memfd, (off_t)PERIPHERALS_PHY_BASE);

        if(bcm2837_peripherals_base == MAP_FAILED)
        {
            fprintf(stderr, "[Error] mmap failed: %s\n", strerror(errno));
        }
	else
        {
            //计算控制pin3引脚的寄存器的地址
            bcm2837_gpio_base = bcm2837_peripherals_base + 0x200000 / 4;
            GPFSEL0 = bcm2837_gpio_base + 0x0000 / 4;
            GPSET0 = bcm2837_gpio_base + 0x001C / 4;
            GPCLR0 = bcm2837_gpio_base + 0x0028 / 4;
            printf("Virtual address:\n");
            printf("\tPERIPHERALS_BASE -> %X\n", (uint32_t)bcm2837_peripherals_base);
            printf("\tGPIO_BASE -> %X\n", (uint32_t)bcm2837_gpio_base);
            printf("\tGPFSEL0 -> %X\n", (uint32_t)GPFSEL0);
            printf("\tGPSET0 -> %X\n", (uint32_t)GPSET0);
            printf("\tGPCLR0 -> %X\n", (uint32_t)GPCLR0);
        }
        close(memfd);
    }
    else
    {
        fprintf(stderr, "[Error] open /dev/mem failed: %s\n", strerror(errno));
    }

    return bcm2837_peripherals_base != MAP_FAILED;
}

void pin3_select_output()
{
    uint32_t value = read_addr(GPFSEL0);
    //1111 1111 1111 1111 1111 0001 1111 1111 -> 0xFFFFF1FF
    //0000 0000 0000 0000 0000 0010 0000 0000 -> 0x00000200
    value = (value & 0xFFFFF1FF) | 0x00000200;
    write_addr(GPFSEL0, value);
}

void pin3_ctrl(uint8_t level)
{
    volatile uint32_t* reg;
    uint32_t value;

    if(level == HIGH)
    {
        reg = GPSET0;
    }
    else if(level == LOW)
    {
        reg = GPCLR0;
    }

    value = read_addr(reg);
    //1111 1111 1111 1111 1111 1111 1111 1011 -> 0xFFFFFFFB
    //0000 0000 0000 0000 0000 0010 0000 0100 -> 0x00000004
    value = (value & 0xFFFFFFFB) | 0x00000004;
    write_addr(reg, value);
}

void write_addr(volatile uint32_t* addr, uint32_t value)
{
    __sync_synchronize();
    *addr = value;
    __sync_synchronize();
}

uint32_t read_addr(volatile uint32_t* addr)
{
    uint32_t value;

    __sync_synchronize();
    value = *addr;
    __sync_synchronize();

    return value;
}

(代码参考了BCM2835驱动源码:http://www.airspayce.com/mikem/bcm2835/)

代码写完后,直接通过GCC编译即可,运行时要加上管理员权限,因为在普通用户的权限下,不能打开/dev/mem。

以上便是linux下控制树莓派3B的GPIO的整个过程的记录。如果大家发现问题,希望可以多多指正。

你可能感兴趣的:(linux)