当前位置: 首页 > news >正文

QEMU:如何组织与 I2C 设备的透明交互

大家好!我是大聪明-PLUS

在嵌入式软件开发中,高效的硬件虚拟化正变得越来越重要,它显著提高了开发的速度和灵活性。无需焊接电路板、等待硬件到货,也无需在每个芯片的测试台之间带着示波器奔波。只需在笔记本电脑上运行虚拟机即可。 

虚拟化允许您在完全可复现的条件下调试驱动程序和应用程序,并行处理不同的功能,甚至在物理设备准备就绪之前就开始编写代码。这在开发和测试嵌入式解决方案时尤为重要,因为嵌入式解决方案通常需要使用外围设备,例如 I2C 设备:温度、压力、湿度传感器、EEPROM 存储器和其他组件。

如何在虚拟环境中合理地组织与这些设备的交互?与GPIO一样,QEMU 中的 I2C 也面临着类似的挑战:需要一种透明地组织主机设备与虚拟模型外设模块之间通信的方法。开发人员通常使用 QMP 协议或特殊脚本来实现这一点,这既不方便又不直观。理想情况下,我更喜欢使用熟悉的实用程序,例如i2cgeti2cset,以及 libi2c 之类的库。

我们嵌入式软件开发部门萌生了一个想法,并通过libcuse库实现了这个想法。它允许客户操作系统直接与 QEMU 虚拟机上运行的虚拟 I2C 设备交互。这种方法保留了虚拟化的所有优势,同时仍然支持直接访问设备的实际数据。

CUSE:当“设备”位于内核之外时

CUSE是FUSE框架 的扩展 ,用于直接从用户空间实现字符设备驱动程序。开发人员无需编写在特权内核模式下运行的代码,而是可以创建一个常规应用程序。该应用程序使用 libfuse 库和 cuse 内核模块注册一个新的字符设备(例如 /dev/my_custom_device)。

这在实践中是如何应用的?让我们看看虚拟 I2C 控制器在 QEMU 中是如何工作的。

  1. QEMU 虚拟机管理程序中的 remote-i2c-controller 模块为客户操作系统模拟硬件 - I2C 控制器。

  2. 在客户操作系统中,此虚拟控制器驱动程序使用 CUSE 在主机系统上创建一个字符设备(例如 /dev/i2c-33)。主机上的程序(例如 i2cget 或 i2cset 等 i2-ctools 实用程序)现在可以打开并与此设备文件交互。

  3. 主机实用程序发出的所有系统调用(open()、read()、write())和 ioctl() 控制命令都通过 CUSE 机制重定向到 QEMU 中的虚拟 I2C 控制器。

  4. 反过来,QEMU 将这些命令转换为对客户操作系统的虚拟 I2C 总线的请求。

 

 

组件与 CUSE 的交互

架构:其内部工作原理

初始配置包括一个 QEMU 虚拟机、一个内部带有 I2C 接口的虚拟传感器、一个主机设备以及主机上的一组 i2c-tools 实用程序。

首先要向 QEMU 添加一个新的虚拟设备。在 QEMU 对象模型中,我们创建一个基本类型 (TYPE_DEVICE) 的设备。该设备将充当一个层,负责所有功能——从初始化客户系统中的虚拟 I2C 节点到处理后续的系统调用。

  • CUSE接口用于在用户空间创建和管理虚拟字符设备/dev/i2c-*。

  • I2C 操作控制块负责处理各种类型的 SMBus 命令和 I2C 事务。

  • 计时器和下半部机制提供异步事件处理,这使得系统在执行操作时不会被阻塞。

该模块开辟了许多重要的发展机会:

  • 无需物理访问硬件即可测试 I2C 设备的驱动程序,

  • 使用详细的 I2C 外设创建更精确的虚拟环境模型,

  • 节省开发嵌入式应用程序的时间和资源,

  • 使用现有的解决方案与 I2C 设备交互,例如 i2c-tools,

  • 测试QEMU虚拟机组件和I2C总线模块。

模块使用示例

要使用,您需要将模块添加到 QEMU 启动参数中:

./qemu-system-arm \-M virt \-cpu cortex-a53 \-nographic \-device tmp105,id=temp_sensor,address=0x40,bus=i2c0.0 \-device remote-i2c-controller,i2cbus=i2c0.0,devname=i2c-33 \
<...>

我们指定的设备名称为:remote-i2c-controller,然后是 I2C 总线,我们为其创建一个虚拟节点并在 devname(/dev/i2c-33)中指定其名称。

启动后,系统中应该会出现一个可通过 FUSE 访问的特殊设备。让我们检查一下该设备是否已经出现:

ls /dev/i2c-33

让我们尝试使用 i2c-tools 实用程序:


i2cget -y 0 0x40 0x00i2cset -y 0 0x40 0x01 0xAB

执行

 

初始化FUSE接口

在 QEMU 结构中创建新设备后,我们的下一个任务是将 FUSE 会话集成到其中。

static int i2c_fuse_export(RemoteI2CControllerState *i2c, Error **errp)
{struct fuse_session *session = NULL;char fuse_opt_dummy[] = FUSE_OPT_DUMMY;char fuse_opt_fore[] = FUSE_OPT_FORE;char fuse_opt_debug[] = FUSE_OPT_DEBUG;char *fuse_argv[] = { fuse_opt_dummy, fuse_opt_fore, fuse_opt_debug };char dev_name[128];struct cuse_info ci = { 0 };char *curdir = get_current_dir_name();int ret;/* Set device name for CUSE dev info */sprintf(dev_name, "DEVNAME=%s", i2c->devname);const char *dev_info_argv[] = { dev_name };memset(&ci, 0, sizeof(ci));ci.dev_major = 0;ci.dev_minor = 0;ci.dev_info_argc = 1;ci.dev_info_argv = dev_info_argv;ci.flags = CUSE_UNRESTRICTED_IOCTL;int multithreaded;session = cuse_lowlevel_setup(ARRAY_SIZE(fuse_argv), fuse_argv, &ci,&i2cdev_ops, &multithreaded, i2c);if (session == NULL) {error_setg(errp, "cuse_lowlevel_setup() failed");errno = EINVAL;return -1;}/* FIXME: fuse_daemonize() calls chdir("/") */ret = chdir(curdir);if (ret == -1) {error_setg(errp, "chdir() failed");return -1;}i2c->ctx = iohandler_get_aio_context();aio_set_fd_handler(i2c->ctx, fuse_session_fd(session),read_from_fuse_export, NULL,NULL, NULL, i2c);i2c->fuse_session = session;trace_remote_i2c_master_fuse_export();return 0;
}

cuse_lowlevel_setup()会创建一个 FUSE 会话,但不会启动循环。我们将使用aio_set_fd_handler() 自行处理事件。

FUSE 操作处理程序

实现 FUSE 会话后的下一个任务是准备一组处理程序,这些处理程序将响应通过 FUSE 可用的标准操作:ioctl()、open()、release() 等。

static const struct cuse_lowlevel_ops i2cdev_ops = {.init    	= i2cdev_init,.open    	= i2cdev_open,.release 	= i2cdev_release,.read    	= i2cdev_read,.ioctl   	= i2cdev_ioctl,.poll    	= i2cdev_poll,
};

主要处理人员:

  • open() 初始化设备上下文,检查访问权限并分配资源。例如,打开 /dev/i2c-33 会​​创建 i2cdev_state 结构,该结构存储总线状态、目标设备地址以及对 FUSE 会话的引用。

  • release() — 释放资源,重置内部状态(例如,重置 I2C_SLAVE 地址),并终止活动操作。

  • ioctl() 是一个系统调用,允许我们处理所有标准 I2C 命令:

    • I2C_FUNCS - 返回支持的功能(i2c-dev 中的所有内容)。

    • I2C_SLAVE - 设置目标设备的地址:有效范围从 0x00 到 0x7F。

    • I2C_SMBUS — 处理所有类型的 SMBus 操作:BYTE、BYTE_DATA、WORD_DATA、BLOCK_DATA、I2C_BLOCK。

模块中有一个函数负责处理系统调用,其中ioctl类型和对应的处理程序通过switch来确定:

static void i2cdev_ioctl(fuse_req_t req, int cmd, void *arg,struct fuse_file_info *fi, unsigned flags,const void *in_buf, size_t in_bufsz, size_t out_bufsz)
{RemoteI2CControllerState *s = fuse_req_userdata(req);<...>switch (ctl) {case I2C_SLAVE_FORCE:fuse_reply_ioctl(req, 0, NULL, 0);break;case I2C_FUNCS:i2cdev_functional(s, req, arg, in_buf);break;case I2C_SLAVE:i2cdev_address(s, req, arg, in_buf);break;case I2C_SMBUS: {i2cdev_cmd_smbus(s, req, arg, in_buf, in_bufsz, out_bufsz);}break;default:fuse_reply_err(req, EINVAL);break;}
}

处理程序会分析接收到的数据并生成响应结构。现在,当接收到系统调用时,系统会选择合适的处理程序,主机将从该处理程序接收响应。

用于虚拟 I2C 总线的适配器

乍一看,这似乎很简单:客户操作系统进行系统调用,然后我们将数据传输到虚拟 I2C 总线。但实际情况要复杂得多。问题在于,客户操作系统所需的数据格式与实际 I2C 总线能够理解的格式不一致。

Linux 使用 struct i2c_smbus_ioctl_data:

union i2c_smbus_data {__u8 byte;__u16 word;__u8 block[I2C_SMBUS_BLOCK_MAX + 2]; /* block[0] is used for length *//* and one more for user-space compatibility */
};

QEMU中的虚拟I2C总线使用i2c_start_send()和i2c_send()进行通信:

int i2c_send(I2CBus *bus, uint8_t data

我们编写了适配器函数,将通过 ioctl 接收的数据转换为 I2C 数据包,并将其发送到虚拟 I2C 总线,反之亦然。

主要适配器:

send_data_to_slave() — 接受 i2c_smbus_ioctl_data 结构体、地址和操作类型。适配器功能:

  • 提取命令(cmd)和数据(data),

  • 形成一个字节包:[cmd, data[0], data[1], ...],

  • 使用正确的参数调用 i2c_smbus_write_*(),

  • 处理错误 - 例如,如果设备没有响应,则处理 EIO。

static void send_data_to_slave(RemoteI2CControllerState *i2c,fuse_req_t req,const struct i2c_smbus_ioctl_data *in_val,const void *in_buf)
{union i2c_smbus_data data;uint8_t buf[64] = { 0 };size_t i = 0;/* Get SMBus data structure */<...>/* Parse data from SMBus struct */<...>/* Send data to I2C bus */i2c_start_send(i2c->i2c_bus, i2c->address);for (i = 0; i < buf[2]; i++) {i2c_send(i2c->i2c_bus, buf[3 + i]);}i2c->address = 0x0;i2c->ioctl_state = I2C_IOCTL_FINISHED;fuse_reply_ioctl(req, 0, NULL, 0);trace_remote_i2c_master_i2cdev_send(in_val->size);
}

recv_data_from_slave() — 接受地址、命令和响应缓冲区。适配器功能:

  • 使用所需类型调用 i2c_smbus_read_*(),

  • 将结果复制到 data->byte、data->word 或 data->block,

  • 返回读取的字节长度(或负错误代码)。

static void recv_data_from_slave(RemoteI2CControllerState *i2c,fuse_req_t req,const struct i2c_smbus_ioctl_data *in_val,const void *in_buf)
{union i2c_smbus_data *smbus_data = (union i2c_smbus_data *)(in_buf + sizeof(struct i2c_smbus_ioctl_data));uint8_t receive_byte = 0;size_t i = 0;/* Send command to slave */i2c_start_send(i2c->i2c_bus, i2c->address);i2c_send(i2c->i2c_bus, in_val->command);i2c_start_recv(i2c->i2c_bus, i2c->address);/* Receive data from slave */switch (in_val->size) {case I2C_SMBUS_BYTE_DATA:smbus_data->byte = i2c_recv(i2c->i2c_bus);break;case I2C_SMBUS_WORD_DATA:receive_byte = i2c_recv(i2c->i2c_bus);smbus_data->word = ((uint16_t)receive_byte) & 0xFF;receive_byte = i2c_recv(i2c->i2c_bus);smbus_data->word |= (((uint16_t)receive_byte) << 8) & 0xFF00;break;case I2C_SMBUS_I2C_BLOCK_BROKEN:case I2C_SMBUS_BLOCK_DATA:case I2C_SMBUS_I2C_BLOCK_DATA:{uint8_t len = smbus_data->block[0];for (i = 0; i < len; i++) {smbus_data->block[1 + i] = i2c_recv(i2c->i2c_bus);}}break;}i2c->ioctl_state = I2C_IOCTL_FINISHED;fuse_reply_ioctl(req, 0, smbus_data, sizeof(union i2c_smbus_data *));
}

操作异步

如果客户操作系统发送从传感器读取的请求,而此时总线已被另一个设备(例如 EEPROM)使用,则无法立即执行该操作,因此我们使用:

  • QEMU 定时器:remote_i2c_timer_cb(),

  • 下半部分处理程序: remote_i2c_bh(),

  • 通过 i2c_schedule_pending_master() 调度操作。

QEMU 有一个轻量级的下半部回调机制,用于延迟需要异步处理的工作。它不会阻塞主线程,并且可以按计划安全地进行调用。

运行i2c_schedule_pending_master(),假设没有人控制 I2C 总线,将把remote-i2c-master放入主队列并调用 remote_i2c_bh() 处理程序在总线上执行必要的操作:

static void remote_i2c_bh(void *opaque)
{RemoteI2CControllerState *s = opaque;if (s->is_recv) {recv_data_from_slave(s, s->req, s->in_val, s->in_buf);} else {send_data_to_slave(s, s->req, s->in_val, s->in_buf);}i2c_end_transfer(s->i2c_bus);i2c_bus_release(s->i2c_bus);if (s->ioctl_state == I2C_IOCTL_FINISHED) {s->ioctl_state = I2C_IOCTL_START;s->last_ioctl = 0;}
}

如果总线忙,则会启动一个计时器来检查总线的状态:

static void remote_i2c_timer_cb(void *opaque)
{RemoteI2CControllerState *s = opaque;s->is_recv = (s->ioctl_state == I2C_IOCTL_RECV);if (i2c_bus_busy(s->i2c_bus)) {timer_mod(s->timer,qemu_clock_get_ms(QEMU_CLOCK_VIRTUAL) + 5);} else {i2c_bus_master(s->i2c_bus, s->bh);i2c_schedule_pending_master(s->i2c_bus);}
}

无论如何,来自主机的调用都会等待对发送的 ioctl 调用的响应。

I2C 设备交易示例

作为示例,让我们尝试使用熟悉的 i2cget 从地址 0x48 处的 TMP105 传感器读取温度。

remote-i2c-master中系统调用事务的一般流程图:

 

一旦虚拟节点打开,对支持的 I2C 功能的请求如下:

i2cget.c

if (ioctl(file, I2C_FUNCS, &funcs) < 0) { <...> }

I2C 支持的函数列表位于i2c.h中:

  • I2C_FUNC_I2C

  • I2C_FUNC_SMBUS_QUICK

  • I2C_FUNC_SMBUS_字节

  • I2C_FUNC_SMBUS_字节_数据

  • I2C_FUNC_SMBUS_BLOCK_DATA

  • I2C_FUNC_SMBUS_WORD_数据

  • I2C_FUNC_SMBUS_I2C_BLOCK

在响应中,我们的模块声明支持所有标准 I2C 操作。然后,客户系统发出 ioctl 调用来设置目标 I2C 设备的地址:

i2cget.c

if (ioctl(file, I2C_SLAVE, address) < 0) { <...> }

远程 i2c 主模块接收地址,检查其正确性(从 0 到 127 的验证)并将其存储在设备的内部状态中。

如果地址设置成功,系统即可使用该设备,所有操作都将指向指定的地址。该地址将保存到下一次 I2C_SLAVE 调用或会话结束,因此同一地址可用于多个操作,而无需重新设置。

此时,模块已准备好接受读取或写入调用。例如,读取一个字节数据的代码如下:

i2cget.c

struct i2c_smbus_ioctl_data args;args.read_write = I2C_SMBUS_READ;
args.command = 0; // SMBus commands
args.size = I2C_SMBUS_BYTE;
args.data = data;ioctl(file, I2C_SMBUS, &args);

客户系统使用I2C_SMBUS ioctl 调用,它可以处理各种数据类型:

  • I2C_SMBUS_BYTE_DATA用于传输一个字节,

  • I2C_SMBUS_WORD_DATA为 16 位值,

  • I2C_SMBUS_BLOCK_DATA用于可变长度数据块。

read_write标志用于确定操作类型(读取或写入)。

当系统想要发送数据时,remote-i2c-master会解析i2c_smbus_ioctl_data结构,提取命令和数据,然后将它们转换为实际 I2C 总线可以理解的格式。

当从主机请求数据时,会发生相反的过程:系统向设备发送命令,然后,一旦虚拟 I2C 总线发送了数据,就读取响应。

模块中的错误通过返回错误代码来处理。这些错误代码会被传回客户操作系统,使其能够正确响应设备问题。

当系统请求发送或接收数据时,远程 i2c 主设备会检查总线状态。如果总线繁忙,则使用 remote_i2c_timer_cb() 定时器在指定时间间隔后重试。在总线上建立主设备状态后,将调用下半部分处理程序 remote_i2c_bh()。根据操作类型,在其中调用 recv_data_from_slave() 或 send_data_to_slave(),完成系统调用并将结果返回给客户机操作系统。

结论

在 QEMU 中开发remote-i2c-master模块使我们能够实现与系统的这种程度的集成,以至于客户操作系统不会注意到真实和虚拟 I2C 设备之间的差异。

开发人员可以使用熟悉的工具和方法来处理虚拟传感器、EEPROM 等。这为更便捷地测试和调试嵌入式系统(尤其是在使用微控制器和传感器时)提供了机会。

该模块是通用的;它可以在不同的虚拟机中使用,而无需发明与虚拟 I2C 总线协同工作的变通方法。

 

http://www.dtcms.com/a/494613.html

相关文章:

  • 精密电子东莞网站建设技术支持视频网站建设类图
  • AI+大数据时代:从架构重构看时序数据库的价值释放——关键概念、核心技巧与代码实践
  • CoRL-2025 | VLM赋能高阶推理导航!ReasonNav:在人类世界中实现与人类一致的导航
  • ARM开发板基础与文件传输
  • 【读书笔记】《一念之差》
  • ssh端口探测 端口测试
  • 计算机操作系统:避免死锁
  • YOLOv3 详解:核心改进、网络架构与目标检测实践
  • Redis过期键的删除策略有哪些?
  • 云南网站建设设计公司百度网站怎么做的
  • HTTP请求走私漏洞介绍
  • 【论文笔记】Introduction to Explainable AI
  • shizuku —详细教程
  • MySQL的CRUD
  • 【C语言】基本语法结构(上篇)
  • 云原生进化论:加速构建 AI 应用
  • 【论文阅读】PathMR: Multimodal Visual Reasoning for Interpretable Pathology Analysis
  • 做护肤品好的网站不用流量的地图导航软件
  • 网站建网站建设wordpress自动标签添加内链插件
  • Java集合【开发的重点*】
  • 深度学习笔记39-CGAN|生成手势图像 | 可控制生成(Pytorch)
  • 第7篇 halcon12导出c++在vs2019配置环境显示图片
  • Socket.IO 聊天应用实例
  • 首发即交付,智元精灵G2携均胜集团过亿订单落地
  • 网站建设需要步骤到哪里查网站备案信息
  • 哈尔滨网站制作哪里专业西安公司网站制作要多少钱
  • WPF中的DataTemplate
  • 浙江建设局网站泰安北京网站建设公司哪家好
  • TensorFlow2 Python深度学习 - 使用Dropout层解决过拟合问题
  • Python数据分析实战:基于5年地铁犯罪数据构建多维安全评估模型【数据集可下载】