从传感器到数据流:手把手教你用Linux IIO驱动一个虚拟温度计(附完整代码)
在嵌入式Linux开发中,传感器数据采集是一个常见需求。工业I/O(IIO)子系统作为Linux内核的标准框架,为各类传感器提供了统一的接口。本文将带你从零开始,实现一个虚拟温度传感器的完整驱动,并构建应用层数据采集方案。
1. IIO驱动开发基础准备
在开始编码前,我们需要明确几个核心概念。IIO子系统采用"设备-通道"模型组织传感器数据,每个物理传感器对应一个IIO设备,而设备中的不同测量维度(如三轴加速度计的X/Y/Z轴)则表示为通道。
典型的IIO驱动开发涉及以下关键结构体:
struct iio_dev { // 代表一个IIO设备 const struct iio_info *info; // 回调函数集合 struct iio_chan_spec const *channels; // 通道描述数组 int num_channels; // 通道数量 unsigned long modes; // 设备工作模式 // ...其他成员 }; struct iio_chan_spec { // 通道描述结构 enum iio_chan_type type; // 通道类型(温度、加速度等) int channel; // 通道编号 int scan_index; // 缓冲区中的索引位置 struct { // 数据格式描述 char sign; // 's'有符号/'u'无符号 u8 realbits; // 有效位数 u8 storagebits; // 存储位数 } scan_type; // ...其他成员 };开发环境配置建议:
- 内核版本:Linux 4.19+(推荐5.x系列)
- 工具链:根据目标平台选择(如arm-linux-gnueabihf)
- 调试工具:
iio_info:查看IIO设备信息hexdump:检查原始数据sysfs接口:手动读写通道值
2. 虚拟温度传感器驱动实现
我们实现一个具有两个温度通道的虚拟设备,模拟实际硬件行为。
2.1 通道定义与数据格式
首先定义通道规格,明确数据属性:
#define NUM_CHANNELS 3 // 2个温度通道 + 1个比例因子通道 static const struct iio_chan_spec temp_channels[] = { { // 通道0 .type = IIO_TEMP, .channel = 0, .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), .address = 0, .scan_index = 0, .scan_type = { .sign = 'u', .realbits = 16, .storagebits = 16, .shift = 0, }, .indexed = 1, }, { // 通道1 .type = IIO_TEMP, .channel = 1, .info_mask_separate = BIT(IIO_CHAN_INFO_RAW), .address = 1, .scan_index = 1, .scan_type = { /* 同通道0 */ }, .indexed = 1, }, { // 比例因子通道 .type = IIO_TEMP, .channel = 0, .info_mask_separate = BIT(IIO_CHAN_INFO_SCALE), .address = 0, .scan_index = -1, // 不参与缓冲区扫描 } };关键参数说明:
info_mask_separate:定义通道支持的属性(RAW原始值/SCALE比例因子)scan_index:缓冲区中的位置索引,-1表示不参与缓冲realbits/storagebits:确保与实际传感器数据手册一致
2.2 核心回调函数实现
实现read_raw回调,为应用层提供数据:
static int temp_read_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, int *val, int *val2, long mask) { switch (mask) { case IIO_CHAN_INFO_RAW: // 读取原始值 /* 模拟温度值:通道0=25°C,通道1=30°C */ *val = (chan->address == 0) ? 2500 : 3000; return IIO_VAL_INT; case IIO_CHAN_INFO_SCALE: // 读取比例因子 *val = 83; // 0.083°C/LSB return IIO_VAL_INT; default: return -EINVAL; } } static const struct iio_info temp_info = { .read_raw = temp_read_raw, };温度值处理技巧:
- 内部以毫摄氏度为单位存储(2500 = 25.00°C)
- 比例因子83表示0.083°C/LSB,符合常见温度传感器特性
- 实际项目中应根据传感器数据手册计算这些值
2.3 设备注册与初始化
完成驱动的probe函数,注册IIO设备:
static int temp_probe(struct platform_device *pdev) { struct iio_dev *indio_dev; int ret; // 分配IIO设备结构体 indio_dev = devm_iio_device_alloc(&pdev->dev, 0); if (!indio_dev) return -ENOMEM; // 填充设备信息 indio_dev->name = "virtual_temp"; indio_dev->channels = temp_channels; indio_dev->num_channels = NUM_CHANNELS; indio_dev->info = &temp_info; indio_dev->modes = INDIO_DIRECT_MODE; // 使用直接访问模式 // 注册设备 ret = devm_iio_device_register(&pdev->dev, indio_dev); if (ret < 0) { dev_err(&pdev->dev, "Failed to register IIO device\n"); return ret; } return 0; }提示:
INDIO_DIRECT_MODE表示通过sysfs直接访问,适合低速传感器。高速设备应考虑使用触发缓冲模式。
3. 应用层数据采集方案
驱动注册后,用户空间可通过sysfs或字符设备访问数据。
3.1 Sysfs接口使用
驱动注册后自动生成以下接口:
/sys/bus/iio/devices/iio:deviceX/ ├── in_temp0_raw # 通道0原始值 ├── in_temp1_raw # 通道1原始值 └── in_temp0_scale # 比例因子读取温度的C语言示例:
#include <stdio.h> #include <fcntl.h> #include <unistd.h> int read_temp_channel(int channel) { char path[256]; int fd, temp_raw; float scale = 0.083; // 已知比例因子 snprintf(path, sizeof(path), "/sys/bus/iio/devices/iio:device0/in_temp%d_raw", channel); fd = open(path, O_RDONLY); if (fd < 0) { perror("Open failed"); return -1; } char buf[16]; read(fd, buf, sizeof(buf)); close(fd); sscanf(buf, "%d", &temp_raw); return (int)(temp_raw * scale); // 转换为实际温度 }3.2 触发缓冲模式扩展
对于需要高效采集的场景,可以扩展为缓冲模式:
// 在probe函数中添加: indio_dev->modes |= INDIO_BUFFER_TRIGGERED; // 配置缓冲区和触发回调 iio_triggered_buffer_setup(indio_dev, NULL, &trigger_handler, NULL); // 触发处理函数 irqreturn_t trigger_handler(int irq, void *p) { struct iio_dev *indio_dev = p; u16 buffer[2]; // 填充缓冲区 buffer[0] = read_temp(0); buffer[1] = read_temp(1); iio_push_to_buffers(indio_dev, buffer); return IRQ_HANDLED; }缓冲模式优势:
- 减少用户空间频繁读取的开销
- 支持硬件触发和定时采样
- 适合高速数据采集场景
4. 调试与性能优化
4.1 常用调试技巧
查看设备树:
ls /sys/bus/iio/devices/读取原始数据:
cat /sys/bus/iio/devices/iio:device0/in_temp0_raw缓冲模式调试:
hexdump -C /dev/iio:device0
4.2 性能优化建议
| 优化方向 | 具体措施 | 适用场景 |
|---|---|---|
| 采样速率 | 调整触发器频率 | 高速数据采集 |
| 功耗 | 使用间歇模式 | 电池供电设备 |
| 精度 | 校准比例因子 | 高精度测量 |
| 延迟 | 启用DMA缓冲 | 实时性要求高 |
常见问题排查:
- 数据异常:检查
scan_type定义是否与传感器一致 - sysfs节点缺失:确认
info_mask_separate配置正确 - 缓冲溢出:增大
buffer/length值
5. 完整代码实现
最后给出虚拟温度传感器的完整驱动代码:
#include <linux/module.h> #include <linux/platform_device.h> #include <linux/iio/iio.h> #define DRV_NAME "virtual_temp" #define NUM_CHANNELS 3 static const struct iio_chan_spec temp_channels[] = { { /* 通道0 */ }, { /* 通道1 */ }, { /* 比例因子通道 */ } }; static int temp_read_raw(/* 参数 */) { /* 实现同前 */ } static const struct iio_info temp_info = { .read_raw = temp_read_raw, }; static int temp_probe(struct platform_device *pdev) { /* 实现同前 */ } static const struct of_device_id temp_of_match[] = { { .compatible = "virtual,temp" }, {} }; MODULE_DEVICE_TABLE(of, temp_of_match); static struct platform_driver temp_driver = { .driver = { .name = DRV_NAME, .of_match_table = temp_of_match, }, .probe = temp_probe, }; module_platform_driver(temp_driver); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("Virtual Temperature Sensor Driver");应用层测试程序:
#include <stdio.h> // ...其他头文件 int main() { printf("Channel 0: %d°C\n", read_temp_channel(0)); printf("Channel 1: %d°C\n", read_temp_channel(1)); return 0; }在实际项目中,我曾遇到一个温度传感器数据漂移问题,最终发现是realbits配置错误导致。将16位配置为12位后,数据解析恢复正常。这提醒我们,驱动开发必须严格遵循硬件数据手册的参数定义。