通过/dev/mem驱动Beaglebone GPIO

30

我正尝试编写一个用于让Beaglebone上的LED闪烁的C程序。我知道我可以使用sysfs方法...但我想看看是否可以通过映射/dev/mem的物理地址空间来获得相同的结果。

我有一个名为beaglebone_gpio.h的头文件,其中包含以下内容:

#ifndef _BEAGLEBONE_GPIO_H_
#define _BEAGLEBONE_GPIO_H_

#define GPIO1_START_ADDR 0x4804C000
#define GPIO1_END_ADDR 0x4804DFFF
#define GPIO1_SIZE (GPIO1_END_ADDR - GPIO1_START_ADDR)
#define GPIO_OE 0x134
#define GPIO_SETDATAOUT 0x194
#define GPIO_CLEARDATAOUT 0x190

#define USR0_LED (1<<21)
#define USR1_LED (1<<22)
#define USR2_LED (1<<23)
#define USR3_LED (1<<24)

#endif

接下来我有我的C程序,gpiotest.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h> 
#include "beaglebone_gpio.h"

int main(int argc, char *argv[]) {
    volatile void *gpio_addr = NULL;
    volatile unsigned int *gpio_oe_addr = NULL;
    volatile unsigned int *gpio_setdataout_addr = NULL;
    volatile unsigned int *gpio_cleardataout_addr = NULL;
    unsigned int reg;
    int fd = open("/dev/mem", O_RDWR);

    printf("Mapping %X - %X (size: %X)\n", GPIO1_START_ADDR, GPIO1_END_ADDR, GPIO1_SIZE);

    gpio_addr = mmap(0, GPIO1_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, GPIO1_START_ADDR);

    gpio_oe_addr = gpio_addr + GPIO_OE;
    gpio_setdataout_addr = gpio_addr + GPIO_SETDATAOUT;
    gpio_cleardataout_addr = gpio_addr + GPIO_CLEARDATAOUT;

    if(gpio_addr == MAP_FAILED) {
        printf("Unable to map GPIO\n");
        exit(1);
    }
    printf("GPIO mapped to %p\n", gpio_addr);
    printf("GPIO OE mapped to %p\n", gpio_oe_addr);
    printf("GPIO SETDATAOUTADDR mapped to %p\n", gpio_setdataout_addr);
    printf("GPIO CLEARDATAOUT mapped to %p\n", gpio_cleardataout_addr);

    reg = *gpio_oe_addr;
    printf("GPIO1 configuration: %X\n", reg);
    reg = reg & (0xFFFFFFFF - USR1_LED);
    *gpio_oe_addr = reg;
    printf("GPIO1 configuration: %X\n", reg);

    printf("Start blinking LED USR1\n");
    while(1) {
        printf("ON\n");
        *gpio_setdataout_addr= USR1_LED;
        sleep(1);
        printf("OFF\n");
        *gpio_cleardataout_addr = USR1_LED;
        sleep(1);
    }

    close(fd);
    return 0;
}

输出结果为:

Mapping 4804C000 - 4804DFFF (size: 1FFF)
GPIO mapped to 0x40225000
GPIO OE mapped to 40225134
GPIO SEDATAOUTADDR mapped to 0x40225194
GPIO CLEARDATAOUTADDR mapped to 0x40225190
GPIO1 configuration: FE1FFFFF
GPIO1 configuratino: FE1FFFFF
Start blinking LED USR1
ON
OFF
ON
OFF
...

但我看不到LED闪烁。

从程序的输出可以看出,配置是正确的,FE1FFFFF 是一致的,因为 GPIO1_21、GPIO1_22、GPIO1_23 和 GPIO1_24 都被配置为输出,每一个都驱动着一个 LED。

对于原因有什么想法吗?


2
我已经找到了解决方案...只需要在mmap中使用MAP_SHARED而不是MAP_PRIVATE即可。无论如何,我还是会留下这个问题。也许对其他人有用。 - Salvatore
1
只要其他人也有公平的机会回答问题,自问自答是完全正常的做法。 - Lundin
7个回答

13

小心使用。乍一看这似乎起作用,但它直接写入一个寄存器,而GPIO控制器驱动程序认为自己拥有该寄存器。这将导致奇怪且难以追踪的副作用,可能会影响此GPIO线或在同一组中的其他GPIO线。要使其可靠工作,您需要从内核GPIO驱动程序中禁用整个组。


1
有没有一种方法可以禁用GPIO控制器驱动程序,以便这不会成为问题? - sheridp

9
修复方法如下:
pio_addr = mmap(0, GPIO1_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, GPIO1_START_ADDR);

1
你确定这是我认为你需要在mmap中使用MAP_SHARED而不是MAP_PRIVATE的修复吗? - Rex Logan
你能解释一下为什么需要 MAP_SHARED 吗? - ocirocir
1
@RicoRico MAP_SHARED 意味着对 mmap 区域的写入将提交回映射的内存区域...而这种情况在 MAP_PRIVATE 中不会发生,这导致数据由于 COPY ON WRITE 被复制到另一个内存区域。 - Salvatore

8

原帖中显示的代码不适用于最新款的Beaglebone Black及其关联的3.12内核。控制寄存器偏移量似乎已经发生了变化;下面的代码已被验证可以正常工作:

#define GPIO0_BASE 0x44E07000
#define GPIO1_BASE 0x4804C000
#define GPIO2_BASE 0x481AC000
#define GPIO3_BASE 0x481AE000

#define GPIO_SIZE  0x00000FFF

// OE: 0 is output, 1 is input
#define GPIO_OE 0x14d
#define GPIO_IN 0x14e
#define GPIO_OUT 0x14f

#define USR0_LED (1<<21)
#define USR1_LED (1<<22)
#define USR2_LED (1<<23)
#define USR3_LED (1<<24)

int mem_fd;
char *gpio_mem, *gpio_map;

// I/O access
volatile unsigned *gpio;

static void io_setup(void)
{
    // Enable all GPIO banks
    // Without this, access to deactivated banks (i.e. those with no clock source set up) will (logically) fail with SIGBUS
    // Idea taken from https://groups.google.com/forum/#!msg/beagleboard/OYFp4EXawiI/Mq6s3sg14HoJ
    system("echo 5 > /sys/class/gpio/export");
    system("echo 65 > /sys/class/gpio/export");
    system("echo 105 > /sys/class/gpio/export");

    /* open /dev/mem */
    if ((mem_fd = open("/dev/mem", O_RDWR|O_SYNC) ) < 0) {
            printf("can't open /dev/mem \n");
            exit (-1);
    }

    /* mmap GPIO */
    gpio_map = (char *)mmap(
            0,
            GPIO_SIZE,
            PROT_READ|PROT_WRITE,
            MAP_SHARED,
            mem_fd,
            GPIO1_BASE
    );

    if (gpio_map == MAP_FAILED) {
            printf("mmap error %d\n", (int)gpio_map);
            exit (-1);
    }

    // Always use the volatile pointer!
    gpio = (volatile unsigned *)gpio_map;

    // Get direction control register contents
    unsigned int creg = *(gpio + GPIO_OE);

    // Set outputs
    creg = creg & (~USR0_LED);
    creg = creg & (~USR1_LED);
    creg = creg & (~USR2_LED);
    creg = creg & (~USR3_LED);

    // Set new direction control register contents
    *(gpio + GPIO_OE) = creg;
}

int main(int argc, char **argv)
{
    io_setup();
    while (1) {
        // Set LEDs
        *(gpio + GPIO_OUT) = *(gpio + GPIO_OUT) | USR0_LED;
        *(gpio + GPIO_OUT) = *(gpio + GPIO_OUT) | USR1_LED;
        *(gpio + GPIO_OUT) = *(gpio + GPIO_OUT) | USR2_LED;
        *(gpio + GPIO_OUT) = *(gpio + GPIO_OUT) | USR3_LED;

        sleep(1);

        // Clear LEDs
        *(gpio + GPIO_OUT) = *(gpio + GPIO_OUT) & (~USR0_LED);
        *(gpio + GPIO_OUT) = *(gpio + GPIO_OUT) & (~USR1_LED);
        *(gpio + GPIO_OUT) = *(gpio + GPIO_OUT) & (~USR2_LED);
        *(gpio + GPIO_OUT) = *(gpio + GPIO_OUT) & (~USR3_LED);

        sleep(1);
    }

    return 0;
}

我在这里发布此内容,因为似乎自3.8内核以来,mmap访问已停止工作,而且没有人自那时以来发布过可用的解决方案。我不得不使用/sys/class/gpio接口反向工程控制寄存器偏移量;我希望这个答案能减少一些使用新内核的BeagleBone GPIO时的挫败感。
该代码根据BSD许可证获得许可 - 随意在任何地方使用。
编辑:user3078565在上面的回答中是正确的。您需要通过将它们的触发器设置为none或通过编辑设备树完全隐藏它们来禁用默认用户LED GPIO驱动程序。如果未这样做,将导致LED闪烁,但也会偶尔被内核GPIO驱动程序覆盖其状态。
对于我的原始应用程序来说,这不是问题,因为它使用GPIO bank 0,该bank在很大程度上被内核GPIO驱动程序忽略。

嗨,谢谢你的代码。我只是想知道为什么你没有使用类似于 close(mem_fd); 这样的语句来关闭它。作为一个新手,我只是想知道是否有必要关闭它? - Yue Wang
偏移地址错误。请将其更正为0x4d、0x4e、0x4f,而不是0x14d、0x14e、0x14f。 - Krishna Murthy
谢谢你提供的在访问银行之前导出数据的提示,这是一个很难调试的问题。 - DXM

4

如果您想在用户空间控制任何硬件设备,可能还需要启用时钟。幸运的是,您可以使用dev/mem和mmap()来调整特定硬件设备的时钟控制寄存器,像我写的以下代码一样启用SPI0:

(定义的值都来自spruh73i.pdf寄存器描述)
#define CM_PER_BASE     0x44E00000  /* base address of clock control regs */
#define CM_PER_SPI0_CLKCTRL     0x4C        /* offset of SPI0 clock control reg */

#define SPIO_CLKCTRL_MODE_ENABLE 2          /* value to enable SPI0 clock */

int mem;            // handle for /dev/mem

int  InitSlaveSPI(void) // maps the SPI hardware into user space
{
    char *pClockControl;    // pointer to clock controlregister block (virtualized by OS)
    unsigned int value;

    // Open /dev/mem:
    if ((mem = open ("/dev/mem", O_RDWR | O_SYNC)) < 0)
    {
        printf("Cannot open /dev/mem\n");
        return 1;
    }
    printf("Opened /dev/mem\n");

    // map a pointer to the clock control block:
    pClockControl = (char *)mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, mem, CM_PER_BASE);

    if(pClockControl == (char *)0xFFFFFFFF) 
    {
        printf("Memory map failed. error %i\n", (uint32_t)pClockControl);
        close( mem );
        return 2;
    }

    value = *(uint32_t *)(pClockControl + CM_PER_SPI0_CLKCTRL);
    printf("CM_PER_SPI0_CLKCTRL was 0x%08X\n", value);

    *(uint32_t *)(pClockControl + CM_PER_SPI0_CLKCTRL) = SPIO_CLKCTRL_MODE_ENABLE;

    value = *(uint32_t *)(pClockControl + CM_PER_SPI0_CLKCTRL);
    printf("CM_PER_SPI0_CLKCTRL now 0x%08X\n", value);

    munmap( pClockControl, 4096 );              // free this memory map element

执行此代码片段后,我可以使用另一个mmap()指针访问SPI0寄存器。如果我不先启用SPI0模块时钟,则在尝试访问这些SPI寄存器时会出现总线错误。启用时钟是持久的:一旦以此方式启用,它将保持开启状态直到您禁用它,或者可能直到您使用spidev然后关闭它,或重启系统。因此,如果您的应用程序已经完成了对硬件的使用,您可能希望禁用它以节省电源。


2
为了启用GPIO bank,需要进行以下步骤:
enableClockModules () {
    // Enable disabled GPIO module clocks.
    if (mapAddress[(CM_WKUP_GPIO0_CLKCTRL - MMAP_OFFSET) / GPIO_REGISTER_SIZE] & IDLEST_MASK) {
      mapAddress[(CM_WKUP_GPIO0_CLKCTRL - MMAP_OFFSET) / GPIO_REGISTER_SIZE] |= MODULEMODE_ENABLE;
      // Wait for the enable complete.
      while (mapAddress[(CM_WKUP_GPIO0_CLKCTRL - MMAP_OFFSET) / GPIO_REGISTER_SIZE] & IDLEST_MASK);
    }
    if (mapAddress[(CM_PER_GPIO1_CLKCTRL - MMAP_OFFSET) / GPIO_REGISTER_SIZE] & IDLEST_MASK) {
      mapAddress[(CM_PER_GPIO1_CLKCTRL - MMAP_OFFSET) / GPIO_REGISTER_SIZE] |= MODULEMODE_ENABLE;
      // Wait for the enable complete.
      while (mapAddress[(CM_PER_GPIO1_CLKCTRL - MMAP_OFFSET) / GPIO_REGISTER_SIZE] & IDLEST_MASK);
    }
    if (mapAddress[(CM_PER_GPIO2_CLKCTRL - MMAP_OFFSET) / GPIO_REGISTER_SIZE] & IDLEST_MASK) {
      mapAddress[(CM_PER_GPIO2_CLKCTRL - MMAP_OFFSET) / GPIO_REGISTER_SIZE] |= MODULEMODE_ENABLE;
      // Wait for the enable complete.
      while (mapAddress[(CM_PER_GPIO2_CLKCTRL - MMAP_OFFSET) / GPIO_REGISTER_SIZE] & IDLEST_MASK);
    }
    if (mapAddress[(CM_PER_GPIO3_CLKCTRL - MMAP_OFFSET) / GPIO_REGISTER_SIZE] & IDLEST_MASK) {
      mapAddress[(CM_PER_GPIO3_CLKCTRL - MMAP_OFFSET) / GPIO_REGISTER_SIZE] |= MODULEMODE_ENABLE;
      // Wait for the enable complete.
      while (mapAddress[(CM_PER_GPIO3_CLKCTRL - MMAP_OFFSET) / GPIO_REGISTER_SIZE] & IDLEST_MASK);
    }
}

MMAP_OFFSET = 0x44C00000

MMAP_SIZE = 0x481AEFFF - MMAP_OFFSET

GPIO_REGISTER_SIZE = 4

MODULEMODE_ENABLE = 0x02

IDLEST_MASK = (0x03 << 16)

CM_WKUP = 0x44E00400

CM_PER = 0x44E00000

CM_WKUP_GPIO0_CLKCTRL = (CM_WKUP + 0x8)

CM_PER_GPIO1_CLKCTRL = (CM_PER + 0xAC)

CM_PER_GPIO2_CLKCTRL = (CM_PER + 0xB0)

CM_PER_GPIO3_CLKCTRL = (CM_PER + 0xB4)

我写了一个小库(GitHub链接),也许你会感兴趣。目前仅适用于数字引脚。

祝好


2
在C++中,您不需要调用析构函数,当对象被销毁时(对于栈分配的对象而言是离开作用域,对于堆分配的对象而言是使用delete),析构函数会自动执行。同样地,您不需要将枚举提取为unsigned char类型,这并不会更快,因为编译器可能会保留一些寄存器来存储“常量”,从而降低其下一段代码的运行效率,导致推入/弹出到堆栈计划的非最优化。 - xryl669

1
REF: madscientist159
// OE: 0 is output, 1 is input
#define GPIO_OE 0x14d
#define GPIO_IN 0x14e
#define GPIO_OUT 0x14f
should be
// OE: 0 is output, 1 is input
#define GPIO_OE 0x4d
#define GPIO_IN 0x4e
#define GPIO_OUT 0x4f

从无符号字符地址派生的偏移地址是无符号整数地址。

1
这个异常似乎是AM335x芯片中地址解码不完整的产物。0x4D、0x4E和0x4F作为基地址的偏移量可以用来访问这些寄存器是有道理的。C/C++指针算术将这些偏移量乘以4,得到真正的偏移量分别为0x134、0x138和0x13C。然而,通过0x14D、0x14E和0x14F可以访问这些寄存器的“影子”副本。我已经验证了这两组偏移量都可以工作。我没有尝试0x24D等。
GPIO_CLEARDATAOUT寄存器可以使用偏移量0x64进行访问,GPIO_SETDATAOUT寄存器可以使用偏移量0x65进行访问。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接