复合设备

简介

在本章内,我们将关注复合设备。 复合设备是指一个设备中包含其他设备。

这些设备解决了硬件级构成问题,其中“设备”(从用户的角度来看)是由几个不同的硬件块实现的。

示例中包括:

  • 一个由 I2C 设备和 GPIO 组成的触摸板,
  • 一个 MAC 芯片和一个或多个 PHY 组成的网络设备,或者
  • 一个音频控制器和一组编解码器组成的音频设备

在这种场景下,硬件关系在启动时就已被板卡驱动程序所知(不管是静态还是动态方式,例如 ACPI )。

我们将使用astro-audio设备作为示例:

Figure: Composite hardware device on I2C bus with GPIOs

这个设备的特性是:

  • 一个 I2C 接口
  • 两组 GPIO (一组为默认,一组为使能接口)
  • 批量数据传输的 MMIO(memory mapped I/O)
  • 产生驱动中断的 IRQ(Interrupt request)线

注意, ZX_PROTOCOL_I2CZX_PROTOCOL_GPIO 协议被用来传输数据;也就是说,I2C 信息和 GPIO 引脚状态是通过各自的驱动发送和接收。

ZX_PROTOCOL_PDEV 部分则与之不同。 这个协议仅被用于授权 MMIO 和 IRQ 的访问(图中绿色标记); PDEV 处理真实的 MMIO 数据和中断;它们直接由astro-audio 驱动自己来处理。

创建复合设备

为了创建一个复合设备,需要设置一些数据结构。

绑定说明

我们需要一些绑定说明(zx_bind_inst_t)来告诉我们匹配的设备。 这些绑定说明在介绍章节中已经在“Registration” topic中讨论过。

对于astro-audio 设备,我们有:

  1. static const zx_bind_inst_t root_match[] = {
  2. BI_MATCH(),
  3. };
  4. static const zx_bind_inst_t i2c_match[] = {
  5. BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_I2C),
  6. BI_ABORT_IF(NE, BIND_I2C_BUS_ID, ASTRO_I2C_3),
  7. BI_MATCH_IF(EQ, BIND_I2C_ADDRESS, I2C_AUDIO_CODEC_ADDR),
  8. };
  9. static const zx_bind_inst_t fault_gpio_match[] = {
  10. BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_GPIO),
  11. BI_MATCH_IF(EQ, BIND_GPIO_PIN, GPIO_AUDIO_SOC_FAULT_L),
  12. };
  13. static const zx_bind_inst_t enable_gpio_match[] = {
  14. BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_GPIO),
  15. BI_MATCH_IF(EQ, BIND_GPIO_PIN, GPIO_SOC_AUDIO_EN),
  16. };

这些绑定说明是用来寻找设备。

示例中我们有4个绑定说明数组;root_match[]包含其他3个设备的共有信息,接下来就是其他3个设备: I2C (i2c_match[])设备和两个 GPIO (fault_gpio_match[]enable_gpio_match[])。

接下来这些绑定说明被放入到一个结构体数组(device_fragment_part_t)中,它定义了各个分块:

Figure: Binding instructions gathered into a fragment
array

astro-audio设备中,我们有:

  1. static const device_fragment_part_t i2c_fragment[] = {
  2. { countof(root_match), root_match },
  3. { countof(i2c_match), i2c_match },
  4. };
  5. static const device_fragment_part_t fault_gpio_fragment[] = {
  6. { countof(root_match), root_match },
  7. { countof(fault_gpio_match), fault_gpio_match },
  8. };
  9. static const device_fragment_part_t enable_gpio_fragment[] = {
  10. { countof(root_match), root_match },
  11. { countof(enable_gpio_match), enable_gpio_match },
  12. };

这时候,我们有3个分块设备,i2c_fragment[],fault_gpio_fragment[]enable_gpio_fragment[]

分块设备匹配规则

分块设备遵守以下规则:

  1. 第一个元素必须描述设备树的根 — 这就是为什么我们用root_match助记符。 注意,这个需求可能会改变,因为大多数用户都会提供一个”始终匹配”元素。
  2. 最后一个元素必须描述目标设备自身。
  3. 剩余元素必须以从根到目标设备的链路按顺序匹配设备。一些设备可能被跳过,但是每一个元素必须匹配。
  4. 路径上每一个设备都有一个属性,范围从BIND_TOPO_STARTBIND_TOPO_END (基本上是总线,例如 I2C 和 PCI )都必须匹配。这些匹配序列必须是唯一的。

最后,我们将它们组合成一个类型为device_fragment_t放入fragments[]的集合体中。

Figure: Gathering fragments into an aggregate

现在这样给了我们一个单独的fragments[]标识,在创建复合设备时可以使用。

astro-audio中,它看起来如下所示:

  1. static const device_fragment_t fragments[] = {
  2. { "i2c", countof(i2c_fragment), i2c_fragment },
  3. { "gpio-fault", countof(fault_gpio_fragment), fault_gpio_fragment },
  4. { "gpio-enable", countof(enable_gpio_fragment), enable_gpio_fragment },
  5. };

创建设备

对于单(非复合)设备来说,我们使用device_add()(参考之前 “Registration” section )来添加设备。

对于复合设备,我们使用device_add_composite()接口:

  1. zx_status_t device_add_composite(
  2. zx_device_t* dev,
  3. const char* name,
  4. const zx_device_prop_t* props,
  5. size_t props_count,
  6. const device_fragment_t* fragments,
  7. size_t fragments_count,
  8. uint32_t coresident_device_index);

其参数说明如下:

Argument Meaning
dev 父设备
name 设备名
props 属性(see “Declaring a Driver”)
`props_count props中的条目数量
fragments 单个片段设备
`fragments_count fragments中的条目数量
coresident_device_index 使用的哪一个驱动主机

dev值必须是zx_device_t对应的”sys“设备(例如,平台总线驱动设备)。

注意,coresident_device_index被用来指示哪一个驱动主机是新设备要用的。 如果你指定UINT32_MAX,设备将驻留在一个新的驱动主机上。

注意astro-audio使用 pbus_composite_device_add()接口而不是device_add_composite()。 两者的区别在于pbus_composite_device_add()是平台总线驱动提供的 API,它封装了 device_add_composite(),并插入了一个用于过渡直接访问资源的额外片段,例如 MMIO , IRQs 和 BTIs。

使用复合设备

从编码角度来看,一个复合设备只是一个普通设备的角色,但是它没有 banjo 协议。这意味着你可以访问所有的组成复合设备的单独分块设备。

第一件事就是为每个分块检索出设备。 这通过device_get_fragment()完成:

  1. bool device_get_fragment (
  2. zx_device_t* parent,
  3. const char* fragment_name,
  4. zx_device_t** fragment);

其参数说明如下:

Argument Meaning
parent Pointer to zx_device_t representing parent
fragment_name The name of the fragment you wish to fetch
fragment Pointer to zx_device_t representing the fragment

程序首先通过声明一个zx_device_t*指针数组来获取设备,然后调用device_get_fragment()接口:

  1. zx_device_t* fragment;
  2. bool found = device_get_fragment(&composite, "fragment-name", &fragment);
  3. if (!found) {
  4. zxlogf(ERROR, "could not get fragment-name");
  5. return ZX_ERR_INTERNAL;
  6. }

提供给 device_get_fragment()的分块名和device_fragment_t提供给板卡驱动调用device_add_composite()中的名字是一样的。

进阶用法

接下来我们讨论一些专门的/更进阶的用法。

复合设备和代理

接下来将以astro-audio 驱动中展示的是一些比之前展示的更复杂的结构。

Figure: Composite hardware device using proxies

分块设备被绑定在一个内部驱动上(在//src/devices/internal/drivers/fragment 目录中)。

如果有必要,驱动会处理跨进程边界的代理。 代理使用DEVICE_ADD_MUST_ISOLATE 机制(详情参见 Isolate devices 章节)。

当一个设备使用DEVICE_ADD_MUST_ISOLATE机制被添加时,最终两个设备被创建: 一个为普通设备,与父进程为同一进程,另一个为代理。

在新的驱动主机中创建一个代理;如果普通设备的驱动是normal.so,那么代理的驱动为normal.proxy.so。 这个驱动需要去实现一个 create() 方法,它将调用device_add() 并存储它所给的 IPC 通道。 为了满足代理子节点请求,这个通道将被用于后续和普通设备的通信中。

普通设备实现一个rxrpc 回调,每次驱动运行时收到来自代理共享通道消息时调用该函数。

所以为了实现一个新的协议代理,必须修改fragment.proxy.so驱动来处理发送消息到普通设备的所需协议,然后修改fragment.so 驱动来适配这些服务消息。

分块代理在/src/devices/internal/drivers/fragment/fragment-proxy.cc实现,而另一半在/src/devices/internal/drivers/fragment/fragment.cc