大家好,我是王鸽,今天总结一下底层驱动这块读写函数代码总结,有个大概的认识,驱动和硬件的交互,最后都要回归到寄存器的读写操作上,所以对驱动的读写函数了解还是很必要的。那就开始介绍几种读写。
内存映射I/O (MMIO)
Linux 设备驱动中最基础的硬件交互方式,用于直接访问CPU内存空间的寄存器(如片上外设),提供了最高效的硬件控制手段。
// 读操作u8 readb(constvolatilevoid __iomem *addr); // 8位u16 readw(constvolatilevoid __iomem *addr); // 16位u32 readl(constvolatilevoid __iomem *addr); // 32位// 写操作voidwriteb(u8 value, volatilevoid __iomem *addr);voidwritew(u16 value, volatilevoid __iomem *addr);voidwritel(u32 value, volatilevoid __iomem *addr);
底层实现:
通过ioremap()将物理地址映射到内核虚拟地址。
底层使用汇编指令(如ARM的ldr/str,x86的mov)直接操作内存。
包含内存屏障(如__iormb()/__iowmb())保证访问顺序。
具体的例子
void writeb(u8 value, volatile void __iomem *addr);功能
向指定内存映射 I/O 地址写入 1 字节(8位) 数据。参数
参数 | 类型 | 说明 |
|---|
value | u8 (unsigned char) | 要写入的 8 位数据 |
addr | volatile void __iomem * | 内存映射 I/O 的目标地址 |
关键特性
volatile:告诉编译器不要优化此操作(硬件寄存器值可能随时改变)
__iomem:表示这是 I/O 内存地址(非普通内存)
原子操作:保证单次写入不可分割
无返回值:只执行写入操作
代码示例
// 启用设备中断 (设置寄存器的第 0 位)void enable_interrupts(struct device *dev){ u8 ctrl_reg; // 1. 读取当前控制寄存器值 ctrl_reg = readb(dev->regs + CONTROL_REG_OFFSET); // 2. 设置中断使能位 (BIT0) ctrl_reg |= 0x01; // 3. 写回控制寄存器 writeb(ctrl_reg, dev->regs + CONTROL_REG_OFFSET);}// 发送命令到设备void send_device_command(struct device *dev, u8 cmd){ // 直接写入命令寄存器 writeb(cmd, dev->regs + COMMAND_REG_OFFSET);}
而u16 readw(const volatile void __iomem *addr); // 16位使用
// 读取设备状态寄存器u16 get_device_status(struct device *dev){ // 直接读取16位状态寄存器 return readw(dev->regs + STATUS_REG_OFFSET);}// 从数据寄存器读取温度值intread_temperature(struct device *dev){ u16 raw_value; // 1. 启动温度转换 writeb(START_CONV_CMD, dev->regs + COMMAND_REG); // 2. 等待转换完成 while (!(readw(dev->regs + STATUS_REG) & CONV_COMPLETE_BIT)) udelay(10); // 3. 读取16位温度值 raw_value = readw(dev->regs + DATA_REG); return convert_raw_temp(raw_value);}
另外读取出来的数据需要考虑大小端转化,字节序处理。
u16 val = readw(addr);u16 host_val = le16_to_cpu(val); // 小端设备转主机序
位操作
// 安全修改特定位u8 reg = readb(ctrl_reg);reg |= BIT(3); // 设置第3位reg &= ~BIT(5); // 清除第5位writeb(reg, ctrl_reg);
I2C总线读写
通过I2C协议读写从设备寄存器。
函数
// 单次读写inti2c_master_send(struct i2c_client *client, constchar *buf, int count);inti2c_master_recv(struct i2c_client *client, char *buf, int count);// SMBus封装(推荐)s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 reg);s32 i2c_smbus_write_byte_data(struct i2c_client *client, u8 reg, u8 value);s32 i2c_smbus_read_word_data(struct i2c_client *client, u8 reg);
SMBus 是基于 I2C 的变种协议,常用于主板上的低速设备管理(如温度传感器、电源管理等)。SMBus 是 I2C 的子集,有更严格的时序要求,所有 SMBus 设备兼容 I2C,但反之不一定成立。SMBus 统一不同 I2C 控制器的访问方式。
I2C总线底层的实现
i2c_smbus_read_byte_data(const struct i2c_client *client, u8 command)函数
s32 i2c_smbus_read_byte_data(conststruct i2c_client *client, u8 command){ union i2c_smbus_data data; int status; status = i2c_smbus_xfer(client->adapter, client->addr, client->flags, I2C_SMBUS_READ, command, I2C_SMBUS_BYTE_DATA, &data); return (status < 0) ? status : data.byte;}EXPORT_SYMBOL(i2c_smbus_read_byte_data);
client:指向 I2C 设备的结构体指针,command:要读取的寄存器地址或命令码。
I2C_SMBUS_BYTE_DATA:指定传输类型其中i2c_smbus_xfer函数是这样子的:
i2c_smbus_read_byte_data工作流程
使用场景
// 读取温度传感器(地址0x48)的温度寄存器(命令码0x00)struct i2c_client *client = ...; u8 temp_reg = 0x00;int temperature = i2c_smbus_read_byte_data(client, temp_reg);if (temperature < 0) { dev_err(&client->dev, "读取失败: %d\n", temperature);} else { dev_info(&client->dev, "当前温度: %d°C\n", temperature);}
s32 i2c_smbus_write_byte_data函数
s32 i2c_smbus_write_byte_data(const struct i2c_client *client, u8 command, u8 value){ union i2c_smbus_data data; data.byte = value; return i2c_smbus_xfer(client->adapter, client->addr, client->flags, I2C_SMBUS_WRITE, command, I2C_SMBUS_BYTE_DATA, &data);}EXPORT_SYMBOL(i2c_smbus_write_byte_data);
I2C_SMBUS_BYTE_DATA这个是参数类型
传输类型对照:
错误码示例
-EIO:总线通信错误
-ENODEV:设备无响应
-EAGAIN:临时错误(可重试)
status = i2c_smbus_xfer(client->adapter, client->addr, client->flags, I2C_SMBUS_READ, command, I2C_SMBUS_WORD_DATA, &data);
SPI总线读写
通过SPI协议读写从设备寄存器。
// 单次传输intspi_write(struct spi_device *spi, constvoid *buf, size_t len);intspi_read(struct spi_device *spi, void *buf, size_t len);// 先写后读(常用)intspi_write_then_read(struct spi_device *spi, constvoid *txbuf, size_t n_tx, void *rxbuf, size_t n_rx);
SPI总线读写底层实现
介绍一下SPI的传输结构体
spi_write函数是 Linux 内核中用于同步 SPI 数据写入的标准实现。它是一个简化封装函数,用于通过 SPI 总线发送数据。spi_write向指定的 SPI 设备同步写入数据(阻塞式操作,直到传输完成)
staticinlineintspi_write(struct spi_device *spi, constvoid *buf, size_t len){ struct spi_transfer t = { //描述单个 SPI 传输操作的结构体 .tx_buf = buf, // 发送数据缓冲区 .len = len, // 发送数据长度 }; struct spi_message m; spi_message_init(&m); // 初始化 SPI 消息 spi_message_add_tail(&t, &m); // 将传输加入消息队列 return spi_sync(spi, &m); //阻塞式函数,会等待整个传输完成 0 表示成功,负数表示错误码 //实际驱动中应检查 spi_sync() 的错误码,根据错误码判断问题。}
具体的工作流程
同样的spi_read代码
staticinlineintspi_read(struct spi_device *spi, void *buf, size_t len){ struct spi_transfer t = { .rx_buf = buf, .len = len, }; struct spi_message m; spi_message_init(&m); spi_message_add_tail(&t, &m); return spi_sync(spi, &m);}
// 先写后读(常用)
int spi_write_then_read(struct spi_device *spi, const void *txbuf, size_t n_tx, void *rxbuf, size_t n_rx);
staticinlineintspi_write_and_read(struct spi_device *spi, constvoid *tx_buf, void *rx_buf, size_t len){ struct spi_transfer t = { .tx_buf = tx_buf, .rx_buf = rx_buf, .len = len, }; struct spi_message m; spi_message_init(&m); spi_message_add_tail(&t, &m); return spi_sync(spi, &m);}
通用的读写API
简化寄存器访问的统一框架,支持I2C/SPI/MMIO等总线。管它什么外设,直接就一套API。
// 初始化Regmapstruct regmap_config config = {.reg_bits = 8, // 寄存器地址位数.val_bits = 16, // 寄存器值位数};
struct regmap *regmap = devm_regmap_init_i2c(client, &config); // I2C设备// 读写寄存器intregmap_write(struct regmap *map, unsignedint reg, unsignedint val);intregmap_read(struct regmap *map, unsignedint reg, unsignedint *val);
regmap_write函数原型
/** * regmap_write(): Write a value to a single register * * @map: Register map to write to * @reg: Register to write to * @val: Value to be written * * A value of zero will be returned on success, a negative errno will * be returned in error cases. */int regmap_write(struct regmap *map, unsigned int reg, unsigned int val){ int ret; if (reg % map->reg_stride) return -EINVAL; map->lock(map->lock_arg); ret = _regmap_write(map, reg, val); map->unlock(map->lock_arg); return ret;}EXPORT_SYMBOL_GPL(regmap_write);
regmap_write的底层调用流程
regmap_read函数原型
/** * regmap_read(): Read a value from a single register * * @map: Register map to write to * @reg: Register to be read from * @val: Pointer to store read value * * A value of zero will be returned on success, a negative errno will * be returned in error cases. */int regmap_read(struct regmap *map, unsigned int reg, unsigned int *val){ int ret; if (reg % map->reg_stride) return -EINVAL; map->lock(map->lock_arg); ret = _regmap_read(map, reg, val); map->unlock(map->lock_arg); return ret;}EXPORT_SYMBOL_GPL(regmap_read);
优势:
自动处理总线细节(如I2C的Repeated Start)。
提供缓存、批量读写等高级功能。
底层共通机制
延时处理:SPI/I2C函数可能休眠,不可用于原子上下文。错误处理:返回负数错误码(如-EIO表示传输失败);
DMA支持:大块数据可能通过DMA传输(如SPI的dma_map_single())
总结
| | |
|---|
| | |
| i2c_smbus_read_byte_data() | |
| | |
| regmap_read()/regmap_write() | |
实际开发中:
注意上下文:SPI/I2C函数可能休眠,中断上下文需使用spi_async()等异步接口。