樹莓派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地址映射的描述。

中間的部分爲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頁的部分信息)。

GPFESLn(選擇引腳功能)、GPSETn(設置引腳輸出高電平)和GPCLRn(設置引腳輸出低電平)是控制引腳輸出電平需要用到的寄存器。手冊後面的幾頁內容將詳細描述這些寄存器如何設置。例如GPFSEL1的描述爲:

根據手冊的描述,我們得到了GPIO相關寄存器的地址和設置方法,接下來將編寫一個控制引腳輸出電平的程序。

3、根據手冊的描述編寫程序

我在這裏選擇GPIO的Pin3作爲實驗對象,以下的程序是以Pin3爲例。

#include <stdio.h>
#include <fcntl.h>      //open函數的定義
#include <unistd.h>     //close函數的定義
#include <sys/types.h>  
#include <sys/mman.h>   //mmap函數的定義
#include <errno.h>      //errno的定義
#include <string.h>
#include <stdint.h>     //uint8_t、uint32_t等類型的定義
#include <unistd.h>     //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的整個過程的記錄。如果大家發現問題,希望可以多多指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章