BeagleBone Black内核模块访问GPIO2/GPIO3触发段错误求助
问题根源与解决方案
我来帮你拆解下为什么访问GPIO2和GPIO3会触发段错误——核心是两个容易忽略的细节,而这两个细节在GPIO0/GPIO1上刚好没暴露出来:
1. 指针算术导致的寄存器地址错误
你代码里直接对void*类型的gpio_addr做加法操作,再转换成unsigned int*,这是个隐蔽的错误:
oe_addr = gpio_addr + GPIO_OE;
GPIO_OE是字节偏移量(0x134),但void*的加法行为在C标准里是未定义的,不同编译器处理方式不同。实际运行时,这个操作可能没有按字节偏移来计算,导致你访问的是错误的寄存器地址,直接触发段错误。
正确的做法是先把地址转成整数,或者转成字节类型指针再做偏移:
// 方式1:转成整数后计算 oe_addr = (volatile unsigned int *)((unsigned long)gpio_addr + GPIO_OE); // 方式2:转成字节指针再偏移 volatile unsigned char *gpio_byte_addr = gpio_addr; oe_addr = (volatile unsigned int *)(gpio_byte_addr + GPIO_OE);
2. GPIO2/GPIO3的时钟默认未启用
在BeagleBone Black的3.8.13内核中,GPIO0和GPIO1的时钟是默认开启的,但GPIO2和GPIO3的时钟默认是关闭的。当你尝试访问一个时钟未启用的外设寄存器时,会触发总线错误,表现为段错误。
要解决这个问题,必须先通过CM_PER寄存器组启用对应GPIO模块的时钟。
完整修复后的代码示例
下面是修正了以上两个问题的代码,以GPIO3为例:
#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/io.h> #include <linux/delay.h> // GPIO基地址与模块大小 #define GPIO1_START_ADDR 0x4804C000 #define GPIO2_START_ADDR 0x481AC000 #define GPIO3_START_ADDR 0x481AE000 #define GPIO_MODULE_SIZE 0x1000 // 每个GPIO模块占4KB内存 // GPIO寄存器字节偏移 #define GPIO_OE 0x134 #define GPIO_DATAOUT 0x13C // 时钟控制寄存器(用于启用GPIO时钟) #define CM_PER_GPIO2_CLKCTRL 0x44E000AC #define CM_PER_GPIO3_CLKCTRL 0x44E000B0 #define MODULEMODE_ENABLE 0x02 // 启用模块时钟的位 #define MODULEMODE_READY_MASK 0x08 // 时钟就绪状态位 MODULE_LICENSE("GPL"); static volatile void *gpio_mapped_addr; // 保存映射地址,用于退出时释放 static int hello_init(void) { volatile unsigned int *oe_addr; volatile unsigned int *dataout_addr; volatile unsigned int *clk_ctrl_reg; printk(KERN_NOTICE "Module: Initializing GPIO3 control module\n"); // 第一步:启用GPIO3的时钟 printk(KERN_NOTICE "Module: Enabling GPIO3 clock\n"); clk_ctrl_reg = ioremap(CM_PER_GPIO3_CLKCTRL, sizeof(unsigned int)); if (!clk_ctrl_reg) { printk(KERN_ERR "Module: Failed to map GPIO3 clock control register\n"); return -ENOMEM; } *clk_ctrl_reg |= MODULEMODE_ENABLE; // 等待时钟稳定就绪 while (!(*clk_ctrl_reg & MODULEMODE_READY_MASK)) { udelay(10); // 短延时等待 } iounmap(clk_ctrl_reg); // 用完立即释放时钟寄存器映射 // 第二步:映射GPIO3的寄存器内存 printk(KERN_NOTICE "Module: Mapping GPIO3 registers\n"); gpio_mapped_addr = ioremap(GPIO3_START_ADDR, GPIO_MODULE_SIZE); if (!gpio_mapped_addr) { printk(KERN_ERR "Module: Failed to map GPIO3 registers\n"); return -ENOMEM; } // 第三步:正确计算寄存器地址 printk(KERN_NOTICE "Module: Calculating register addresses\n"); oe_addr = (volatile unsigned int *)((unsigned long)gpio_mapped_addr + GPIO_OE); dataout_addr = (volatile unsigned int *)((unsigned long)gpio_mapped_addr + GPIO_DATAOUT); // 第四步:配置引脚为输出并设置高电平 printk(KERN_NOTICE "Module: Setting pin 19 to OUTPUT mode\n"); *oe_addr &= ~(1 << 19); // 清除OE位,设置为输出 printk(KERN_NOTICE "Module: Setting pin 19 to HIGH\n"); *dataout_addr |= (1 << 19); return 0; } static void hello_exit(void) { // 释放映射的GPIO内存 if (gpio_mapped_addr) { iounmap(gpio_mapped_addr); } printk(KERN_INFO "Module: Exiting GPIO control module\n"); } module_init(hello_init); module_exit(hello_exit);
额外说明
- 如果要操作GPIO2,只需要把代码中的
GPIO3_START_ADDR替换为GPIO2_START_ADDR,CM_PER_GPIO3_CLKCTRL替换为CM_PER_GPIO2_CLKCTRL即可。 - 模块退出时一定要调用
iounmap释放映射的内存,避免内核内存泄漏。 - 你的3.8.13内核版本比较老旧,建议如果条件允许,升级到更新的内核版本,新内核提供了更安全易用的GPIO子系统接口(比如
gpio_request、gpio_direction_output等),不需要直接操作硬件寄存器。
内容的提问来源于stack exchange,提问作者UndergroundCoding
相关产品推荐
相关产品推荐

