看似合理的结果,来自从未存在过的数据
一个小型设备监测机器振动,每十毫秒收集一块测量数据,交给人工智能模型。只看平均运行时间,模型似乎足够快,但有些输出仍难以解释。问题可能发生在任何统计误差之前:模型读取测量值时,采集电路已经开始用下一批数据覆盖它。于是模型读到了来自两个时段的数据片段,却不一定触发任何程序错误。
摘要。本文研究何时可以复用一段内存,而不改变仍在读取的输入。我们构造合成时间线,推导数据生命周期条件,比较双缓冲和三缓冲,再引入明确的所有权管理。核心结论是:增加内存可以吸收一次延迟,但防止覆盖占用中的数据块需要使用协议;资源不足时,必须明确报告数据损失。所有数值均来自 Python 仿真,不是开发板或 EL-AI 产品上的实测。
两项同时进行的活动
缓冲区是临时保存数据的内存区域。生产者填入数据,消费者读取数据。这里的生产者是由 DMA(直接内存访问)辅助的采集过程:设备传输数据,不必让 CPU 逐个复制采样值。与此同时,负责计算的 CPU 可以执行推理,即把模型应用于数据。并行进行很有价值,但两个参与者也可能访问同一块内存。
可以想象两个托盘:一个被填满时,另一个正在被读取。这就是双缓冲,也常称为乒乓缓冲的直觉。不过类比存在重要限制:内存中没有一只手自动提醒“这个托盘还在使用”。内容改变后,有效地址仍然有效。保存指针只是记住从哪里读取,并不会冻结那里的内容。我们需要的是:所有依赖这个数据块的读取完成之前,整块数据保持稳定。
实验:八个数据块和一次孤立延迟
假设传感器以每秒 16,000 个采样值产生单通道数据。每块包含 160 个采样值,每个值用两个字节表示。因此一块占 320 字节,采集时间为 160/16,000 秒,即 10 毫秒。这些参数是为便于理解而选取的,并非经过测试的硬件规格。数据块从零编号:块 0 在 0 至 10 毫秒写入,块 1 在 10 至 20 毫秒写入,依此类推。
消费者按到达顺序一次处理一个数据块。八个块的处理时间设为 6、6、14、6、6、6、6、6 毫秒,平均为 7 毫秒。假设输入必须在整个处理期间保持有效,包括 CPU 开始之前的等待。模型不加入额外中断、复制、缓存或同步开销。我们不是估算神经网络的速度,而是单独研究输入的生命周期与内存复用之间的关系。
使用两个循环缓冲区时,一个保存偶数块,另一个保存奇数块。块 2 在 30 毫秒采集完成,CPU 从 30 毫秒读取到 44 毫秒。但块 4 在 40 毫秒开始写入同一个缓冲区。读取与覆盖重叠了四毫秒。平均处理时间低于 10 毫秒无法阻止这一事件:正确性涉及每个数据块,而不只是整个序列的平均表现。
复用之前到底有多少时间?
为计算这个时间窗口,定义 T 为采集周期、B 为缓冲区数量、k 为数据块编号。块 k 在 rₖ=(k+1)T 时刻采集完成;块 k+B 开始时,它的缓冲区被重新写入,即 dₖ=(k+B)T。这里 d 表示数据可用性的截止时刻,不是临床或控制截止时间。两者之差就是采集完成后的可用时间:
Cₖ 是处理该块所需时间,Wₖ 是开始之前的等待;它们与 T 一样,以毫秒为单位。第二行表示等待加处理必须在复用前结束。B=2 时只剩 10 毫秒:正在采集的缓冲区不能当作额外空闲等待空间。对块 2,得到 0+14≤10,条件不成立。B=3 时条件变为 14≤20,这一次延迟可以容纳。
为包含对后续块的影响,开始时刻 sₖ 取当前块到达时刻与前一块结束时刻 fₖ₋₁ 中较晚的一个,再加上 Cₖ。这是一个不抢占的串行处理器,即不会中断一个块去执行另一个块:
裕量 mₖ 为正,表示在复用前已不再需要这些数据;为负则表示生命周期重叠。块 3 在 40 毫秒到达,却要等到 44 毫秒,最终在 50 毫秒结束,恰好与同一缓冲区开始接收块 5 的时刻相同,裕量为零。仿真按理想事件顺序处理相等情况;真实系统不应把它视为保证,因为它没有给时序抖动、同步或实际最后一次访问留下余量。
| 块 | 到达 ms | 开始 ms | 结束 ms | B=2复用 ms | 裕量 ms |
|---|---|---|---|---|---|
| 0 | 10 | 10 | 16 | 20 | 4 |
| 1 | 20 | 20 | 26 | 30 | 4 |
| 2 | 30 | 30 | 44 | 40 | -4 |
| 3 | 40 | 44 | 50 | 50 | 0 |
| 4 | 50 | 50 | 56 | 60 | 4 |

从左向右读图:灰色表示采集,蓝色表示在窗口内完成的读取,红色表示延续到复用之后的读取。两个面板中的 CPU 时间完全相同,改变的只是生产者何时返回同一块内存。表格还揭示了块 3 的等待:如果只把每个 Cₖ 与 T 比较,就会忽略积压工作的影响。
模型究竟可能读到什么
发生重叠并不能自动证明预测会改变多少,这取决于模型的读取顺序。但我们可以构造一个数据一致性的最小反例。考虑索引 0、40、80、120 四个代表位置,新块 4 分别在 40、42.5、45、47.5 毫秒开始覆盖它们。在 44 毫秒对这四个位置做理想快照,会得到代际标签 [4, 4, 2, 2]。标签表示值来自哪个块,不是传感器实测振幅。
设备因而可能从一个从未被完整采集过的信号构造特征。我们没有运行神经网络,也不为这个例子指定诊断错误率。这里证明的是一个更有限却根本的问题:“本次推理使用块 2”这一约定被破坏。在干净数据集上提高模型精度,不能修复这种约定。
为每个缓冲区指定所有者
现在改变规则:生产者不再自动选择下一个循环地址,而只能使用空闲缓冲区。我们区分四种状态:FREE 表示可用,FILLING 表示已留给采集,READY 表示完整且等待处理,READING 表示已留给消费者。正常路径为 FREE → FILLING → READY → READING → FREE。“就绪”不等于“空闲”:等待的数据已经属于尚未读取它们的任务。
第二个仿真用事件队列实现这些状态。40 毫秒时,若只有两个缓冲区,块 2 仍在读取,块 3 刚刚就绪,没有空闲缓冲区供块 4 使用。选定的策略是整块丢弃新输入,并记录编号。最终完成的是块 0、1、2、3、5、6、7:不允许覆盖,但明确损失了一块。使用三个缓冲区时,同一条有限轨迹没有丢块。
这一结果区分了两个常被混淆的要求:已交付数据的完整性,以及采集序列的无缺失性。可以满足前者,却失去后者。递增块编号帮助接收者发现从 3 跳到 5,但不能重建缺失测量。在时间序列分析中,这个空缺必须保留其时间位置;悄悄拼接剩余块,会虚构传感器并未提供的连续性。
为什么三个缓冲区不能解决所有延迟
额外空间能吸收暂时扰动,不能消除持续失衡。再次计算循环方案,让每次处理都耗时 12 毫秒,T 仍为 10 毫秒。第一个块之后,每次到达增加 2 毫秒等待,因此 Wₖ=2k 毫秒。三个缓冲区提供 20 毫秒窗口,裕量成为 20−(2k+12)=8−2k 毫秒。块 4 的裕量为零,块 5,即第六个块,比复用时刻晚 2 毫秒结束。程序验证了这个首次超限。
当单个消费者持续慢于生产者时,有限数量的缓冲区不可能无限期保存此数据流的所有块。明确所有权可以保护占用中的块,但最终必须丢弃、在允许时减慢源头,或增加处理能力。这里“丢弃一个块”是抽象决策;在真实外设上,需要验证如何停止、重定向或忽略采集,避免 DMA 实际上仍向占用中的缓冲区写入。
复制、保留,还是减少工作量
立即把输入复制到私有内存,可以缩短传感器缓冲区必须保持不变的时间。此时复用条件中的 Cₖ 应替换为直到最后一次读取原始缓冲区的时长,而不能自动使用整个推理时间。不过复制必须在覆盖之前完成,目标内存也必须在需要期间保持占用,传输还消耗时间和内存带宽。我们没有测量这些成本,也不假定复制总是最佳方案。
在本例中,从两个缓冲区增加到三个,仅数据块存储就从 640 字节变为 960 字节,多出 320 字节。这不是应用的 RAM 峰值,还需考虑模型权重和激活、栈、队列、对齐以及其他结构。“零复制”即直接读取采集内存,也不等于“零等待”:它省去复制,却可能延长缓冲区占用。正确选择取决于系统受限的资源,以及任务能够接受哪些损失。
从仿真到固件:还需验证什么
Zephyr 的官方环形缓冲区文档说明了访问内存、传输数据以及确认已生产或消费字节之间的分离。并发部分指出,它不提供通用的内部并发控制;文档区分单生产者、单消费者和多个参与者,并讨论跨 CPU 的写入可见性。这是理解约定的参考,不代表我们的仿真实现了该内核。已阅读 Concepts、Instantiation and Usage、Concurrency、Internal Operation 各节;具体 API 取决于所选固件版本。
教学程序对同一时刻的事件按以下顺序处理:CPU 完成、采集完成、新采集开始。状态在单个 Python 进程中改变。这使案例具有确定性,却不能证明微控制器上的原子性、缓存一致性或操作顺序。物理测试需要明确开发板和外设、运行时版本、DMA 配置、缓冲区放置、同步策略,以及访问时间线;还必须测试饱和状态,而不只是正常运行。
结论与可复现性
即使平均处理速度看起来足够快,传感器也确实可能覆盖模型仍在读取的数据。关键在于每个块需要保留多久,以及谁有权复用其内存。在本例中,第三个缓冲区吸收了一次孤立延迟;明确所有权避免了损坏,但在两个缓冲区下需要丢弃一块。这两项观察都不能建立通用实时保证。实际含义是应把模型、采集和丢失策略一起设计:只有知道输出来自哪些数据,AI 答案才有明确意义。
下方短代码重现循环时间线:max 保留等待,reuse−end 计算裕量。压缩包还包含基于所有权的事件仿真、完整轨迹、混合代际反例和持续过载案例。断言检查了重叠块、丢弃块,以及固定 12 毫秒处理时的首次超限。没有随机性,因此无需种子。时间线扫描复杂度随块数线性增长;此实现的事件队列使用堆,复杂度为 O(n log n + nB),内存为 O(nB+n+B):n 是数据块数,B 是缓冲区数。每个事件会搜索空闲缓冲区并记录全部 B 个状态。B 固定时,成本分别为 O(n log n) 和 O(n)。
主要来源与代码
Zephyr Project Documentation — Ring Buffers.
T = 10 # milliseconds
costs = [6, 6, 14, 6, 6, 6, 6, 6]
for B in (2, 3):
end = 0
for k, C in enumerate(costs):
release = (k + 1) * T
start = max(release, end)
end = start + C
reuse = (k + B) * T
print(B, k, start, end, reuse - end)
代码、数据与说明 · JSON. 教学计算使用 Python 3.14.0,图使用 Matplotlib 3.11.2。分析由 AI 辅助,不声称经过同行评审或人工审核。原创 ImageGen 封面仅作示意,不记录 EL-AI 人员、场所或实际安装。来源查阅于 2026 年 10 月 3 日。

