【问题标题】:Why am I getting segmentation fault on Beaglebone Black when accessing GPIO2 and GPIO3 through kernel module?为什么通过内核模块访问 GPIO2 和 GPIO3 时 Beaglebone Black 出现分段错误?
【发布时间】:2023-08-06 11:37:01
【问题描述】:

我一直试图通过内核模块访问 beaglebone black 上的 GPIO2 和 GPIO3,但没有成功。每次我尝试将输出值分配给 GPIO 2 和 3 时,都会出现分段错误。

完全相同的代码(具有适当的引脚分配)适用于 GPIO0 和 GPIO1。

我尝试了 P8 和 P9 上与 GPIO2 和 GPIO3 相关的各种引脚,但均未成功。另一方面,相同的代码适用于 GPIO0 和 GPIO1,并具有适当的引脚分配。

对于引脚值,我使用的是官方 BBB 手册。为了获得适当的 I/O GPIO 可用性,我正在从 beagleboard.com 检查此图:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <net/tcp.h>

//Macros
#define GPIO1_START_ADDR 0x4804C000
#define GPIO2_START_ADDR 0x481AC000
#define GPIO2_END_ADDR 0x481ACFFF
#define GPIO3_START_ADDR 0x481AE000

#define SIZE (GPIO2_END_ADDR - GPIO2_START_ADDR)
#define GPIO_OE 0x134
#define GPIO_DATAOUT 0x13C

//A couple of standard descriptions
MODULE_LICENSE("GPL");

static int hello_init(void)
{
    volatile void *gpio_addr;
    volatile unsigned int *oe_addr;
    volatile unsigned int *dataout_addr;

    printk(KERN_NOTICE "Module: Initializing module\n");

    printk(KERN_NOTICE "Module: Map GPIO\n");
    gpio_addr = ioremap(GPIO3_START_ADDR,SIZE);

    printk(KERN_NOTICE "Module: Set oe_addr\n");
    oe_addr = gpio_addr + GPIO_OE;

    printk(KERN_NOTICE "Module: Set dataout_addr\n");
    dataout_addr = gpio_addr + GPIO_DATAOUT;

    //Code will work up to here for any GPIO.
    //It crashes on the following for GPIO2 and GPIO3:

    printk(KERN_NOTICE "Module: Set pin to OUTPUT\n");
    *oe_addr &= (0xFFFFFFFF ^ (1<<19));

    printk(KERN_NOTICE "Module: Set pin output to HIGH\n");
    *dataout_addr |= (1<<19);

    return 0;
}

static void hello_exit(void)
{
    printk(KERN_INFO "Exit module.\n");
}

module_init(hello_init);
module_exit(hello_exit);

如果我屏蔽了这两行 *oe_addr &amp;= (0xFFFFFFFF ^ (1&lt;&lt;19));*dataout_addr |= (1&lt;&lt;19);,该程序在所有 GPIO 上运行均无故障。

$uname -a: Linux beaglebone 3.8.13-bone79

为什么访问 GPIO2 和 GPIO3 时出现分段错误?

【问题讨论】:

  • "如果我屏蔽了这两行 [...] 程序会为所有 GPIO 运行而不会出现故障。" ... 如果你这样做了,那么 IO 不是完全访问!?此外,这不是真正的代码 - module_init()module_exit() 在任何函数之外被“调用”,这是不可能的。如果代码不是真实的,我们怎么能相信它会显示失败?
  • @Clifford 你是什么意思它不是代码?它运行。我在其中一个引脚上连接了一个 LED,当我运行它时 LED 会亮起。这就是内核模块的格式化方式。插入模块时调用 module_init(arg),移除模块时调用 module_exit(arg),其中 arg 是这些宏将指向的函数。
  • 我认为(来自快速谷歌)你需要在ioremap() 之前调用request_mem_region()
  • 您的模块绝对没有任何业务试图访问“GPIO(控制)寄存器”,这些寄存器已经由引脚控制 (pinctrl) 子系统拥有。阅读有关获取和使用 GPIO 引脚的正确方法的内核文档:kernel.org/doc/Documentation/gpio “完全相同的代码(具有适当的引脚分配)适用于 GPIO0 和 GPIO1。” -- 更有可能是得到一个误报。
  • @CallMeTheMan,如果其他 GPIO 正在使用上面的代码,而只有 GPIO2 和 GPIO3 没有,这意味着时钟没有启用到 GPIO2/3 你是否检查了 CM_PER_GPIO2/3_CLKCTRL 的值.我没有找到方便的基地址数据表。在设备树中启用它应该有帮助(status="okay"),或者在 u-boot 中刚刚找到这个链接 (e2e.ti.com/support/arm/sitara_arm/f/791/t/248181)

标签: c module linux-kernel beagleboneblack


【解决方案1】:

经过大量研究,我发现了一些有用的链接,例如 this onethis one

需要指出的是,GPIO 寄存器 1、2 和 3 的默认设置是时钟禁用,因此在尝试访问寄存器时出现分段错误。当系统请求导出 GPIO 时,它会启用时钟并且 GPIO 寄存器可供使用。

要解决此问题,我们需要手动启用这些 GPIO 的时钟。使用链接中的代码示例,我无法做到这一点。

但是通过使用

echo 5 > /sys/class/gpio/export
echo 65 > /sys/class/gpio/export
echo 105 > /sys/class/gpio/export

在运行插入 mod 之前,我发现事情可以正常工作。通过监控每个 GPIO 上的时钟值,我发现该值从某个值变为“2”。但是,手动将 2 输入到这些值中不足以使 GPIO 工作。

如果我找到通过内存控制正确启用时钟的方法,我将更新此答案。

编辑:

经过更多的大惊小怪和研究,我已经让代码正常工作。我已经把它写成一个单独的模块,它是在插入问题上发布的模块之前插入的:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <net/tcp.h>

#define CM_PER_ADDR 0x44E00000
#define CM_PER_SIZE 0x3FF
#define CM_PER_GPIO1_ADDR   0xAC
#define CM_PER_GPIO2_ADDR   0xB0
#define CM_PER_GPIO3_ADDR   0xB4

#define GPIO_COUNT 3


//A couple of standard descriptions
MODULE_LICENSE("GPL");

static int hello_init(void)
{
    static volatile void* cm_per;
    static volatile unsigned int* cm_per_gpio[GPIO_COUNT];

    static volatile int cm_per_addr[GPIO_COUNT] = {CM_PER_GPIO1_ADDR, CM_PER_GPIO2_ADDR, CM_PER_GPIO3_ADDR};

    static int i = 0;

    printk(KERN_NOTICE "Module2: Initializing module\n");

    cm_per = ioremap(CM_PER_ADDR, CM_PER_SIZE);
        if(!cm_per){
            printk (KERN_ERR "Error: Failed to map GM_PER.\n");
            return -1;  //Break to avoid segfault
        }

    for(i = 0; i < GPIO_COUNT; i++){
        cm_per_gpio[i] = cm_per + cm_per_addr[i];

        //Check if clock is disabled
        if(*cm_per_gpio[i] != 0x2){
        printk(KERN_NOTICE "Enabling clock on GPIO[%d] bank...\n", (i+1));
            *cm_per_gpio[i] = 0x2;  //Enable clock
            //Wait for enabled clock to be set
            while(*cm_per_gpio[i] != 0x2){}
        }

        //Print hex value of clock
        printk(KERN_NOTICE "cm_per_gpio[%d]: %04x\n", (i+1), *(cm_per_gpio[i]));
    }


    return 0;
}

static void hello_exit(void)
{
    printk(KERN_INFO "Module: Exit module.\n"); //Print exit notice and exit without exploding anythin
}

module_init(hello_init);
module_exit(hello_exit);

AM335x and AMIC110 Sitara™ ProcessorsTechnical Reference Manual,我们可以看到 CM_PER_GPIO#_CLKCTRL 寄存器 是如何组织的(其中 # 代表我们正在查看的 GPIO 组):

表 8-60。 CM_PER_GPIO2_CLKCTRL 寄存器字段说明

它也告诉我们寄存器的reset(默认)值是30000h,意思是CLOCK DISABLED,意思是module disabled

【讨论】:

  • 您的“答案”只是必要的,因为您坚持以不良行为方式访问 GPIO 引脚。您发布的代码是编写不佳的“内核”代码:(1) ioremap() 之前没有调用 request_mem_region(),(2) 宏 ioreadN() 和 iowriteN() 不与来自 ioremap() 的虚拟地址一起使用,(3) read-modify-write寄存器操作是未受保护的关键区域,(4) 没有 iounmap(),(5) 它不是内核编码风格。您通过发布写得不好、不可靠的代码作为“解决方案”来为社区提供服务。
【解决方案2】:

你的代码为什么会出现分段错误的答案实际上是无关紧要的,因为作为一个内核模块,它被误导了,需要被扔掉,你需要重写它。您的模块绝对没有任何业务试图直接访问“GPIO(控制)寄存器”,这些寄存器已经由 pin-control (pinctrl) 子系统拥有。

GPIO 引脚是内核管理的(通用)资源。您会编写一个刚开始使用任意内存块作为其缓冲区的驱动程序吗?
希望不会,因为内存是由内核管理的(另一种)资源。
但是你的模块只是随心所欲地使用GPIO管脚!

请参阅正确的 GPIO 文档以了解您正在使用的确切 Linux 内核版本:Documentation/gpio.txt for version 3.8.13

您的模块可以使用的可用例程包括:

gpio_request()
gpio_free()

gpio_direction_input()
gpio_direction_output()

gpio_get_value()
gpio_set_value()

(顺便说一句,您的代码忽略了检查 ioremap() 的返回值,它可能为 null,然后可能导致分段错误。)

【讨论】:

  • 感谢您的输入,但它没有回答问题。我可能会考虑使用您在下一个项目中建议的 gpio 访问方法,但是我遇到的问题是由于 GPIO 上的时钟被禁用。
  • “但是它没有回答问题” -- 您是否在代码中添加了检查(如答案的最后一行所述)? “但是我遇到的问题是由于时钟被禁用” - 这是如何确认的?我见过一个禁用的外设时钟导致外设不响应,但永远不会导致段故障。除了 pinctrl 子系统应该启用时钟。 “我可能会……在我的下一个项目中,” -- 当你真正拥有一个真正的操作系统时,你正在使用裸机或微控制器技术。这是不可靠或不便携的不良代码。
  • 感谢您的意见,我会牢记在心。请注意,根据社区的要求,这不是我的代码的全部内容,它只是复制错误的一个片段。我专门写了它在这里发布。我的代码确实检查内存是否正确映射。我已经发布了我自己的问题的答案,其中指出通过更改时钟以启用 GPIO 变得可访问。在导出属于这些寄存器的引脚之前和之后监控 GPIO 时钟将显示时钟切换到 2。如果我有任何进一步的信息,我会更新答案。
  • 经过进一步审查,我在技术参考上发现禁用时钟导致分段错误的原因如下:“0x0 = DISABLED:模块被 SW 禁用。对模块的任何 OCP 访问都会导致错误,除非是由模块唤醒(异步唤醒)引起的。”检查我更新的答案以获得解释。干杯
最近更新 更多