寄存器配置基础:地址映射、读写封装与位域操作

各位同学,今天我们来聊聊寄存器配置。说实话,这是Sensor驱动开发里最基础、也最容易翻车的地方。我见过不少新手,上来就对着datasheet一顿猛写,结果传感器死活不工作——最后发现是寄存器地址写错了,或者位操作搞反了。

嗯,咱们今天就把这块彻底讲透。

寄存器地址映射:从物理地址到虚拟地址

先说说地址映射。在Linux系统里,用户空间程序是不能直接访问物理地址的。你得通过ioremap()把物理地址映射到内核的虚拟地址空间。说白了,就是给硬件寄存器办个「通行证」。

我习惯这样写:

#include <linux/io.h>

#define SENSOR_BASE_ADDR   0x1C200000
#define REG_CTRL           0x00
#define REG_STATUS         0x04
#define REG_DATA           0x08

static void __iomem *sensor_base;

static int sensor_probe(struct platform_device *pdev)
{
    struct resource *res;
    
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res)
        return -ENODEV;
    
    sensor_base = devm_ioremap(&pdev->dev, res->start, resource_size(res));
    if (!sensor_base)
        return -ENOMEM;
    
    return 0;
}

这里有个坑,我提醒一下:ioremap()返回的是void __iomem *类型,千万别直接当普通指针用。我曾经见过有人直接拿它做memcpy,结果系统直接挂了。

注意:映射后的地址不能直接解引用,必须用ioread32()/iowrite32()这类访问函数。这是ARM架构的内存序要求,搞反了读出来的数据全是乱的。

读写操作封装:让代码更安全

直接调用ioread32iowrite32当然可以,但代码会变得很丑。我建议封装一下:

static inline u32 sensor_read(u32 reg)
{
    return ioread32(sensor_base + reg);
}

static inline void sensor_write(u32 reg, u32 val)
{
    iowrite32(val, sensor_base + reg);
}

你看,这样写起来就清爽多了。而且万一以后要加调试打印,改一个地方就行。

我个人习惯再加一层:

#define SENSOR_READ(reg)      sensor_read(reg)
#define SENSOR_WRITE(reg,val) sensor_write(reg,val)

为什么这么做?因为有些寄存器操作需要加延迟,比如写完后要等几个微秒。用宏定义可以方便地插入这些逻辑。

位域操作技巧:掩码与移位

好了,重头戏来了。寄存器操作的核心就是位域操作。你想想看,一个32位的寄存器,可能同时控制着使能、模式、中断、状态……每个字段只占几位。

我常用的套路是这样的:

/* 定义位域 */
#define CTRL_ENABLE      BIT(0)
#define CTRL_MODE_MASK   GENMASK(3, 1)
#define CTRL_MODE_SHIFT  1
#define CTRL_INT_EN      BIT(4)

/* 设置某个位 */
SENSOR_WRITE(REG_CTRL, SENSOR_READ(REG_CTRL) | CTRL_ENABLE);

/* 清除某个位 */
SENSOR_WRITE(REG_CTRL, SENSOR_READ(REG_CTRL) & ~CTRL_ENABLE);

/* 设置多位的值 */
u32 val = SENSOR_READ(REG_CTRL);
val &= ~CTRL_MODE_MASK;
val |= (mode << CTRL_MODE_SHIFT) & CTRL_MODE_MASK;
SENSOR_WRITE(REG_CTRL, val);

这里有个细节:GENMASK(h, l)是内核提供的宏,生成从l位到h位的掩码。比如GENMASK(3, 1)就是0b1110。用这个比手写0x0E要清晰得多。

核心原则:读-改-写三步走。先读回原值,修改对应位,再写回去。千万别直接写,否则会把其他位冲掉。

避坑指南:我踩过的雷

我曾经在一个项目里,传感器死活读不到数据。查了两天,最后发现是寄存器地址偏移算错了。datasheet上写的是0x20,我用了0x200。嗯,多了一个0,浪费两天。

还有一次,我在写中断状态寄存器时,直接写了SENSOR_WRITE(REG_INT_STATUS, 0xFFFFFFFF)想清除所有中断。结果把保留位也写了,传感器直接死机。后来才知道,中断状态寄存器通常是写1清除,而且只能写对应的位。

经验之谈:写寄存器前,一定先看datasheet里的「Register Description」表格。重点关注:
- 哪些位是只读的(RO)
- 哪些位是写1清除(W1C)
- 哪些位是保留位(Reserved),必须写默认值

实战:一个完整的寄存器配置流程

咱们拿一个典型的I2C温湿度传感器来练手。假设它的配置寄存器长这样:

位域 名称 描述
[7] POWER_DOWN 1=掉电,0=正常工作
[6:4] RESOLUTION 000=8位,001=10位,010=12位,011=14位
[3:0] RESERVED 保留,写0

初始化代码可以这样写:

#define REG_CONFIG      0x01
#define CFG_PD_MASK     BIT(7)
#define CFG_RES_MASK    GENMASK(6, 4)
#define CFG_RES_SHIFT   4

static int sensor_init(void)
{
    u32 val;
    
    /* 先读回当前值 */
    val = sensor_read(REG_CONFIG);
    
    /* 清除掉电位:让传感器工作 */
    val &= ~CFG_PD_MASK;
    
    /* 设置分辨率为14位 */
    val &= ~CFG_RES_MASK;
    val |= (0x03 << CFG_RES_SHIFT) & CFG_RES_MASK;
    
    /* 写回寄存器 */
    sensor_write(REG_CONFIG, val);
    
    return 0;
}

你看,整个过程就是:读、改、写。没什么高深的,但每一步都不能错。

小结

寄存器配置说白了就是三件事:地址要映射对,读写要封装好,位操作要小心。我做了这么多年驱动,发现90%的问题都出在这三个环节上。

嗯,今天的内容就到这儿。记住:写寄存器前,先深呼吸,再看一遍datasheet。


公众号:蓝海资料掘金营,微信deep3321