這篇博客記錄了我在用戶程序中將物理地址映射到虛擬地址,然後使用虛擬地址控制樹莓派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的整個過程的記錄。如果大家發現問題,希望可以多多指正。