这篇博客记录了我在用户程序中将物理地址映射到虚拟地址,然后使用虚拟地址控制树莓派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
#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的整个过程的记录。如果大家发现问题,希望可以多多指正。