在 QEMU 中建模时钟树
什么是时钟?
时钟是为建模 QEMU 中时钟分发而开发的 QOM 对象。 可以描述平台的时钟分发,并检测时钟树配置错误, 例如 PLL 配置错误、时钟源选择错误或时钟被禁用。
对象为 Clock,QOM 名称是 clock,C 宏为 TYPE_CLOCK。
时钟通常用于建模设备输入和输出,创建方式类似 GPIO。 不同设备的输入和输出可以相互连接。
这种情况下 Clock 是 Device 的子对象,但这不是强制要求。 时钟可以独立于设备,例如在任何设备之外创建时钟,表示整机主时钟源。
时钟示例:
+---------+ +----------------------+ +--------------+
| 时钟1 | | 设备 B | | 设备 C |
| | | +-------+ +-------+ | | +-------+ |
| |>>-+-->>|时钟2 | |时钟3 |>>--->>|时钟6 | |
+---------+ | | | (输入)| | (出) | | | | (输入)| |
| | +-------+ +-------+ | | +-------+ |
| | +-------+ | +--------------+
| | |时钟4 |>>
| | | (出) | | +--------------+
| | +-------+ | | 设备 D |
| | +-------+ | | +-------+ |
| | |时钟5 |>>--->>|时钟7 | |
| | | (出) | | | | (输入)| |
| | +-------+ | | +-------+ |
| +----------------------+ | |
| | +-------+ |
+----------------------------->>|时钟8 | |
| | (输入)| |
| +-------+ |
+--------------+
时钟定义位于 include/hw/core/clock.h,
设备相关函数位于 include/hw/core/qdev-clock.h。
时钟状态
时钟状态即周期,以整数保存,单位为 2 -32 ns。 特殊值 0 表示时钟不活动或被门控关闭。 时钟不建模信号本身的引脚翻转,也不建模占空比等属性。
输入和输出时钟都包含此状态,因此可随时查询当前周期。 更新时钟时,新值立即传播到时钟树中全部连接的时钟。
为便于使用,各状态读写函数都提供带单位后缀的辅助版本:
_ns:以纳秒处理周期。_hz:以赫兹处理频率。
周期 0 转换为频率 0,反之亦然。0 始终表示时钟已禁用。
添加新时钟
必须在 Device 实例的 init 方法中添加时钟。
使用 qdev_init_clock_in() 添加输入时钟,
参数包括名称、回调、回调的不透明参数,以及应触发回调的事件掩码,详见后文。
输出时钟更简单,只需名称。典型用法:
qdev_init_clock_in(DEVICE(dev), "clk_in", clk_in_callback, dev, ClockUpdate);
qdev_init_clock_out(DEVICE(dev), "clk_out");
两个函数都返回创建的 Clock 指针,应保存在设备状态结构中以供后续使用。 QOM 引用机制会自动删除这些对象。
也可以用静态数组描述时钟输入和输出,再将数组传给 qdev_init_clocks() 初始化。
效果等同于对每项分别调用 qdev_init_clock_in/out()。
include/hw/core/qdev-clock.h 提供辅助宏,便于构造数组。
以下为设备创建一个输入时钟和一个输出时钟:
/* device structure containing pointers to the clock objects */
typedef struct MyDeviceState {
DeviceState parent_obj;
Clock *clk_in;
Clock *clk_out;
} MyDeviceState;
/*
* callback for the input clock (see "Callback on input clock
* change" section below for more information).
*/
static void clk_in_callback(void *opaque, ClockEvent event);
/*
* static array describing clocks:
* + a clock input named "clk_in", whose pointer is stored in
* the clk_in field of a MyDeviceState structure with callback
* clk_in_callback.
* + a clock output named "clk_out" whose pointer is stored in
* the clk_out field of a MyDeviceState structure.
*/
static const ClockPortInitArray mydev_clocks = {
QDEV_CLOCK_IN(MyDeviceState, clk_in, clk_in_callback, ClockUpdate),
QDEV_CLOCK_OUT(MyDeviceState, clk_out),
QDEV_CLOCK_END
};
/* device initialization function */
static void mydev_init(Object *obj)
{
/* cast to MyDeviceState */
MyDeviceState *mydev = MYDEVICE(obj);
/* create and fill the pointer fields in the MyDeviceState */
qdev_init_clocks(mydev, mydev_clocks);
[...]
}
另一种创建方式是直接调用 object_new(TYPE_CLOCK),
此时它不属于设备的输入或输出。
完整 QOM 层级建立后,应调用 clock_setup_canonical_path()。
新建时钟周期为 0,即禁用,可通过 clock_set_ns() 或 clock_set_hz() 修改。
如果时钟周期固定且永不改变,例如开发板主时钟源,设置后就无需其他操作。 连接时钟时,此值会传播到其他时钟,设备在首次复位时读取正确值。
时钟回调
可通过以下方式设置回调:
作为
qdev_init_clock_in()参数传入。作为
QDEV_CLOCK_IN()参数,初始化传给qdev_init_clocks()的数组。直接调用
clock_set_callback()。
回调类型必须为:
typedef void ClockCallback(void *opaque, ClockEvent event);
opaque 是此前传给 qdev_init_clock_in() 或 clock_set_callback() 的指针;
使用 qdev_init_clocks() 时则为 dev 设备指针。
event 表示调用原因。
注册时指定关注的 ClockEvent 值掩码,只有这些事件会触发回调。
当前支持:
ClockPreUpdate:输入时钟周期即将更新时调用。 若设备操作需要知道旧周期,此事件很有用。 回调期间clock_get()、clock_ticks_to_ns()等 API 使用旧周期。
ClockUpdate:输入时钟周期改变后调用。 回调期间clock_ticks_to_ns()等 API 使用新周期。
一个时钟只能有一个回调,不能为不同事件注册不同函数。 必须让同一回调监听所有关注事件,再通过 event 区分。
从设备获取时钟
qdev_get_clock_in() 和 dev_get_clock_out()
可用于获取设备输入或输出时钟。例如:
Clock *clk = qdev_get_clock_in(DEVICE(mydev), "clk_in");
或者:
Clock *clk = qdev_get_clock_out(DEVICE(mydev), "clk_out");
连接两个时钟
使用 clock_set_source() 连接两个时钟。
clock_set_source(clk2, clk1); 使 clk2 跟随 clk1 的周期变化,
每次更新 clk1 时也更新 clk2。
连接不同设备的时钟时,优先用 qdev_connect_clock_in() 设置输入时钟源。
例如将 devB 的 clk2 输入连接到 devA 的 clk1 输出:
qdev_connect_clock_in(devB, "clk2", qdev_get_clock_out(devA, "clk1"))
这里使用了 qdev_get_clock_out(),但任意时钟都能驱动输入时钟,包括另一个输入时钟。 下图展示一些连接方式;一个时钟也可以驱动多个其他时钟。
+------------+ +--------------------------------------------------+
| 设备 A | | 设备 B |
| | | +---------------------+ |
| | | | 设备 C | |
| +-------+ | | +-------+ | +-------+ +-------+ | +-------+ |
| |时钟1 |>>-->>|时钟2 |>>+-->>|时钟3 | |时钟5 |>>>>|时钟6 |>>
| | (出) | | | | (输入)| | | | (输入)| | (出) | | | (出) | |
| +-------+ | | +-------+ | | +-------+ +-------+ | +-------+ |
+------------+ | | +---------------------+ |
| | |
| | +--------------+ |
| | | 设备 D | |
| | | +-------+ | |
| +-->>|时钟4 | | |
| | | (输入)| | |
| | +-------+ | |
| +--------------+ |
+--------------------------------------------------+
上例中 Device A 更新 Clock 1 后,Clock 2、Clock 3 和 Clock 4 都获得新周期。
时钟连接后,不能断开或更改连接关系。
时钟乘数与除数设置
默认情况下,连接的子时钟与其源(父)时钟周期相同。
Clock API 内置周期乘除机制,可让子时钟采用不同周期。
调用 clock_set_mul_div() 指定乘数和除数后,
全部子时钟的周期为 parent_period * multiplier / divider。
例如父时钟为 8 MHz,乘数设为 2、除数设为 3,则子时钟为 12 MHz。
运行时可修改乘除数,用于建模客户机可编程倍频或分频的时钟控制器。
与 clock_set() 类似,clock_set_mul_div() 修改状态后返回 true, 即乘数、除数或两者发生变化时返回 true。
它不会自动调用 clock_propagate()。
运行时修改乘除数后,必须自行调用该函数传播变化。
未连接的输入时钟
新建输入时钟被禁用,周期为 0,直到更新周期前都视为禁用。 一直未连接的时钟会保持初始值 0。 若不希望如此,应在设备实例初始化时调用 clock_set()、clock_set_ns() 或 clock_set_hz()。例如:
clk = qdev_init_clock_in(DEVICE(dev), "clk-in", clk_in_callback,
dev, ClockUpdate);
/* set initial value to 10ns / 100MHz */
clock_set_ns(clk, 10);
要强制要求开发板代码连接时钟,可在设备 realize 方法中检查 clock_has_source():
if (!clock_has_source(s->clk)) {
error_setg(errp, "MyDevice: clk input must be connected");
return;
}
这只检查是否建立连接;连接的输出仍可能被禁用或尚未配置,周期仍为 0。 应通过时钟回调得知周期变化。
获取时钟频率或周期
使用 clock_get() 或 clock_get_hz() 获取当前状态。
clock_get() 返回完整精度的内部周期,以无符号 64 位整数表示,单位为 2^-32 纳秒。 很多情况下 clock_ticks_to_ns() 更方便,见下文到期时间计算。
clock_get_hz() 返回向下取整的频率,因此存在舍入误差,用于计算时应谨慎。
也可为频率变化注册回调。 以下假设 clock_callback 已注册为 ClockUpdate 回调:
void clock_callback(void *opaque, ClockEvent event) {
MyDeviceState *s = (MyDeviceState *) opaque;
/*
* 'opaque' is the argument passed to qdev_init_clock_in();
* usually this will be the device state pointer.
*/
/* do something with the new period */
fprintf(stdout, "device new period is %" PRIu64 "* 2^-32 ns\n",
clock_get(dev->my_clk_input));
}
若仅为向人显示频率,例如调试,应使用 clock_display_freq(),
它返回“33.3 MHz”之类的格式化字符串,使用后调用者必须用 g_free() 释放。
QTest 也可通过 QMP 访问 QOM 属性 qtest-clock-period 获取周期。
此属性仅在 qtest 加速器下存在,普通 QEMU 运行时不可用。
计算到期时间
常见需求是计算时钟经过 N 个节拍需要多久,用于设置定时器到期时间。
clock_ticks_to_ns() 接收无符号 64 位节拍数,返回所需纳秒数。
不要简单地把“纳秒周期”乘以节拍数,因为周期可能不是整数纳秒, 累积误差可能很大。
周期很长且节拍数很大时,结果理论上可能超出 64 位。 为避免溢出,clock_ticks_to_ns() 会把结果饱和到 INT64_MAX, 这是 QEMUTimer API 接受的最大有效值。 INT64_MAX 纳秒接近 300 年,因此更晚的到期时间可视为永不发生。 调用者通常不应特殊处理饱和结果,而应直接把定时器设置到这个遥远的未来。 当然,若还要对返回值继续计算,而不只是传给 timer_mod_ns() 等 QEMUTimer 函数, 就需要防止后续计算溢出。
获取节拍数
需要计算某段时间内节拍数时,使用 clock_ns_to_ticks()。
它处理非整数纳秒周期,避免舍入误差。
时钟停止,即周期为 0 时返回 0。
若输入导致节拍数溢出 64 位,例如极短周期和极长时长,
结果会截断,相当于 64 位输出回绕。
修改时钟周期
设备可用 clock_update()、clock_update_ns() 或 clock_update_hz()
修改输出,并触发全部已连接输入更新。
例如,初始化时创建 clkout 输出,并在设备状态中保存指针:
dev->clkout = qdev_init_clock_out(DEVICE(dev), "clkout");
之后除下述情形外,可以随时修改:
clock_update_hz(dev->clkout, 1000 * 1000 * 1000); /* 1GHz */
更新可能通过已连接时钟和回调触发副作用,因此必须持有 QEMU I/O 锁。
同理,只有允许对其他对象产生副作用时才能更新。因此以下阶段禁止更新:
迁移期间;
复位的 enter 阶段。
clock_update[_ns|_hz]() 等价于使用相同参数调用 clock_set[_ns|_hz](),
再调用 clock_propagate()。
因此可将状态设置与副作用触发分离,便于复用处理设备复位和迁移的代码。
时钟别名
有时需要转发或继承其他设备的时钟。
例如组合设备时,希望直接暴露子设备时钟而不干预它。
qdev_alias_clock() 提供此功能,也可用不同名称暴露,适用于输入和输出。
例如 B 是 A 的子设备,device_a_instance_init() 可以这样做:
void device_a_instance_init(Object *obj)
{
AState *A = DEVICE_A(obj);
BState *B;
/* create object B as child of A */
[...]
qdev_alias_clock(B, "clk", A, "b_clk");
/*
* Now A has a clock "b_clk" which is an alias to
* the clock "clk" of its child B.
*/
}
函数不返回时钟对象,新时钟方向与原时钟相同,只增加指向已有时钟的链接。 上例中 B 仍是唯一可以操作此时钟的对象,A 不得修改周期或设置回调。 输入时钟的情况如下:
+--------------------------+
| 设备 A |
| +--------------+ |
| | 设备 B | |
| | +-------+ | |
>>"b_clk">>>| "clk" | | |
| (输入) | | (入) | | |
| | +-------+ | |
| +--------------+ |
+--------------------------+
迁移
时钟状态不自动迁移,各设备必须自行处理。别名时钟不得迁移。
要确保正确恢复状态,有两种方式。
第一种是在设备 vmstate 描述中添加条目,使用 VMSTATE_CLOCK 宏。
通常用于输入时钟,例如:
MyDeviceState {
DeviceState parent_obj;
[...] /* some fields */
Clock *clk;
};
VMStateDescription my_device_vmstate = {
.name = "my_device",
.fields = (const VMStateField[]) {
[...], /* other migrated fields */
VMSTATE_CLOCK(clk, MyDeviceState),
VMSTATE_END_OF_LIST()
}
};
第二种是根据已有信息恢复,适合根据设备状态恢复输出时钟。
可在 post_load() 迁移回调中使用 clock_set[_ns|_hz]()。
为现有设备加入时钟支持时,应注意迁移兼容性,直接添加 VMSTATE_CLOCK() 会破坏兼容。 可以将其放入带适当 needed 函数的 vmstate 子节, 并在 pre_load() 中用 clock_set() 设置默认值, 以供源虚拟机未发送时钟状态时使用。
整个迁移过程中不得使用 clock_update[_ns|_hz]() 或 clock_propagate(),
否则会对状态尚不确定的其他设备触发副作用。