寄存器配置基础:地址映射、读写封装与位域操作
各位同学,今天我们来聊聊寄存器配置。说实话,这是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架构的内存序要求,搞反了读出来的数据全是乱的。
读写操作封装:让代码更安全
直接调用ioread32和iowrite32当然可以,但代码会变得很丑。我建议封装一下:
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清除,而且只能写对应的位。
- 哪些位是只读的(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