2、采集流程基础:采集流程的五个阶段与状态机模型

做采集开发这些年,我见过太多新手一上来就写采集循环,结果不是丢帧就是设备挂死。其实采集这件事,说白了就是一套固定的流程——你只要把每个阶段该做的事做对,整个系统就稳了。

我个人习惯把采集流程拆成五个阶段:初始化、配置、启动、循环采集、停止/清理。这五个阶段就像五级台阶,少走一步都不行。

2.1 五个阶段详解

2.1.1 初始化阶段

这个阶段干的事很简单:分配资源、打开设备、注册回调。我刚开始做采集时,总觉得初始化就是调个 open() 完事,结果有一次在嵌入式设备上跑,忘了分配 DMA 缓冲区,采集一启动就崩了。

初始化阶段的核心任务:

  • 设备枚举与选择:找到可用的采集设备(摄像头、采集卡、传感器等)
  • 资源预分配:申请内存池、DMA 缓冲区、帧缓冲队列
  • 驱动/内核模块加载:确保底层硬件就绪
  • 错误状态检查:确认设备未被其他进程占用
我的经验:初始化阶段一定要做「幂等处理」——多次调用初始化不会导致资源泄漏。我曾经因为没做这个,在热插拔场景下内存泄漏到系统 OOM。

2.1.2 配置阶段

配置阶段是「谈条件」的阶段。你要告诉设备:我要什么分辨率、什么帧率、什么像素格式。设备会告诉你:我能给什么。

配置项通常包括:

配置参数 说明 常见取值
分辨率 采集图像的宽高 1920x1080, 3840x2160
帧率 每秒采集帧数 30fps, 60fps, 120fps
像素格式 色彩编码方式 NV12, YUYV, RGB24
缓冲区数量 帧缓冲池大小 3~8 个
触发模式 采集触发方式 软件触发, 硬件触发

这里有个坑:配置顺序很重要。我遇到过先设分辨率再设帧率,结果帧率被自动降级的情况。你想想看,设备内部可能有依赖关系——先设大参数,再设小参数,设备会做兼容调整。反过来可能就报错了。

2.1.3 启动阶段

启动阶段就是「扣扳机」。配置完成后,调用 start() 或 streamon(),设备开始往缓冲区里填数据。

启动时要注意:

  • 确认所有配置已提交:有些设备需要 commit 操作才能生效
  • 检查缓冲区状态:所有缓冲区必须处于「空闲」或「入队」状态
  • 启动顺序:先启动采集端,再启动处理端(避免数据堆积)
注意:启动阶段如果失败,一定要回滚到配置阶段,而不是直接重试启动。我曾经在项目里直接重试启动,结果设备状态错乱,最后只能重启整个系统。

2.1.4 循环采集阶段

这是采集流程的核心。说白了就是一个死循环:

while (running) {
    // 1. 从设备取一帧(阻塞或非阻塞)
    frame = dequeue_buffer(device);
    
    // 2. 处理帧数据(显示、编码、分析)
    process_frame(frame);
    
    // 3. 把缓冲区还给设备
    enqueue_buffer(device, frame);
    
    // 4. 检查是否要停止
    if (should_stop()) break;
}

这个循环看起来简单,但实际项目里最容易出问题:

  • 丢帧:处理速度跟不上采集速度,缓冲区被覆盖
  • 死锁:取帧和处理帧用了同一个锁
  • 内存泄漏:取出来的帧没有及时归还

我建议在循环里加一个性能监控——统计采集帧率、处理帧率、丢帧率。这样出问题时能快速定位是采集端慢了还是处理端慢了。

2.1.5 停止/清理阶段

很多人觉得停止就是调个 stop() 完事。其实不然。停止阶段要做的事:

  1. 停止采集流:通知设备停止数据推送
  2. 等待未完成帧:确保所有正在处理的帧完成
  3. 归还所有缓冲区:把设备占用的缓冲区释放
  4. 释放资源:关闭设备、释放内存、销毁线程
  5. 状态重置:把状态机重置到初始状态
关键点:停止阶段必须「优雅」。我曾经在停止时直接 kill 采集线程,结果 DMA 缓冲区没来得及释放,下次启动时设备报「缓冲区已被占用」。

2.2 状态机模型在采集中的应用

采集流程天然适合用状态机来管理。为什么?因为采集的每个阶段都有明确的进入条件和退出条件,状态之间不能乱跳。

下面是我常用的采集状态机模型:

采集状态机模型 IDLE(空闲) INIT(初始化) CONFIG(配置) RUNNING(运行) STOPPING(停止) open() 成功 start() stop() cleanup() 失败 配置失败 状态转换规则: • 只能按 IDLE → INIT → CONFIG → RUNNING → STOPPING → IDLE 顺序流转 • 任何阶段失败都必须回退到 IDLE 状态,不能跨状态跳转

这个状态机有几个关键规则:

  • 单向流转:只能按 IDLE → INIT → CONFIG → RUNNING → STOPPING → IDLE 的顺序走
  • 失败回退:任何阶段失败,都必须回到 IDLE 状态,不能原地重试
  • 状态锁定:在某个状态下,只处理该状态对应的事件,忽略其他事件
我的实践:我在项目中用了一个简单的枚举 + 状态转换表来实现状态机。每个状态对应一个处理函数,状态转换通过查表决定。这样代码结构清晰,而且容易加日志——每次状态切换都打一条日志,调试时特别有用。

2.3 状态机带来的好处

用状态机管理采集流程,说白了就是给系统上了一道「保险」:

  • 防止非法操作:比如在 RUNNING 状态下调用 open(),状态机直接拒绝
  • 简化错误处理:任何错误都统一回退到 IDLE,不用写复杂的 if-else
  • 便于调试:看日志里的状态转换,就知道系统卡在哪一步
  • 可测试性强:每个状态可以单独测试,状态转换也可以写单元测试

嗯,这里要注意:状态机不是万能的。如果你的采集流程特别简单(比如就一个 open-read-close),用状态机反而显得臃肿。但一旦涉及多线程、异步回调、热插拔,状态机就是你的救星。