OIS(光学防抖)大家都听说过,但OIS的镜头位置数据是怎么从硬件读到算法层的?NCS又是什么?这篇深入聊聊Camera子系统里这条不太被关注的数据流。
一、为什么需要NCS?
先说背景。CamX的Node和Pipeline通常以像素帧边界为单位处理数据——一帧进、一帧出。但EIS(电子防抖)算法不一样,它需要的是帧边界附近的数据,甚至要聚合多帧数据,在任意时刻请求数据。
这就产生了一个矛盾:
NCS(Node Communication Service)就是为了解决这个矛盾而设计的基础设施——它允许Node之间传递不按帧边界对齐的数据。
二、OIS数据流全链路
OIS数据从硬件到算法的完整路径:
OIS硬件控制器
↓ I2C读取
OIS Submodule Driver(轮询线程)
↓ NCS接口
CamX OIS Driver
↓ NCS数据源注册
EIS Node(NCS客户端)
↓ 请求OIS数据
EIS算法处理关键点:OIS数据不是按帧来的,而是按固定频率采样。OIS硬件有一个固定大小的片上缓存,数据需要通过I2C以足够快的速率读出来,否则会丢数据。
为此,OIS驱动会启动一个独立的轮询线程,按照XML配置的频率定期读取数据。这里没有直接的I2C Read API,是通过寄存器配置的方式读取。
三、XML配置详解
OIS数据的读取完全由XML配置驱动。核心配置结构是OISLensPositionReadInfo,包含三个部分:
3.1 采样参数
这三个参数的关系很关键:
采样频率 = 500Hz → 每个采样2ms
总采样数 = 12 → 12个采样需要24ms(12 × 2ms)
读取间隔 = 10ms → 每10ms读一次
约束:readIntervalInMS < (1000 / sampleFrequencyInHZ) × totalSamples
10ms < (1000/500) × 12 = 24ms ✓
注意:readInterval要考虑软件和CCI延迟,留出余量3.2 寄存器读取配置
<!-- readSettings:I2C寄存器读取配置 -->
<readSettings>
<regSetting>
<registerAddr>0x8A</registerAddr> <!-- OIS数据寄存器地址 -->
<registerData>51</registerData> <!-- 读取51字节 -->
<regAddrType>2</regAddrType> <!-- 地址类型:2字节 -->
<regDataType>1</regDataType> <!-- 数据类型:1字节 -->
<operation>READ</operation> <!-- 操作类型:读 -->
</regSetting>
</readSettings>3.3 数据格式配置(FormatInfo)
这是最核心的部分——告诉框架如何解析读回来的原始数据。每个字段都用FormatInfo结构描述:
以一个实际例子说明数据布局:
<!-- 每个采样5字节:1字节有效标记 + 2字节X + 2字节Y -->
<!-- 12个采样 + 2字节时间戳 = 51字节 -->
<formatInfo>
<positionValidSamples>
<offset>0</offset> <!-- 第0字节:有效采样数 -->
<sizeInBytes>1</sizeInBytes>
<offsetIncrementBytes>0</offsetIncrementBytes> <!-- 只有一个,不递增 -->
</positionValidSamples>
<positionX>
<offset>1</offset> <!-- 第1-2字节:X位置 -->
<sizeInBytes>2</sizeInBytes>
<offsetIncrementBytes>4</offsetIncrementBytes> <!-- 下一个X在+4处 -->
</positionX>
<positionY>
<offset>3</offset> <!-- 第3-4字节:Y位置 -->
<sizeInBytes>2</sizeInBytes>
<offsetIncrementBytes>4</offsetIncrementBytes> <!-- 下一个Y在+4处 -->
</positionY>
<timeStamp>
<offset>49</offset> <!-- 最后2字节:时间戳 -->
<sizeInBytes>2</sizeInBytes>
<offsetIncrementBytes>0</offsetIncrementBytes> <!-- 只有一个时间戳 -->
</timeStamp>
<isPerSampleTimestamp>FALSE</isPerSampleTimestamp> <!-- 只有最后一个采样有时间戳 -->
<timerClockFrequency>19200000</timerClockFrequency>
<timeStampUnit>INTERNALCLOCKTICKS</timeStampUnit> <!-- 时间戳单位:内部时钟周期 -->
</formatInfo>数据布局图示:
原始数据(51字节):
┌───┬──────────────────────────────────────────────────────┬──────┐
│ V │ S1(X,Y) S2(X,Y) S3(X,Y) ... S12(X,Y) │ TS │
│ 1 │ 2+2 2+2 2+2 2+2 │ 2 │
└───┴──────────────────────────────────────────────────────┴──────┘
0 1 3 5 7 9 11 13 ... 49 51
V = positionValidSamples (1字节)
X = positionX (2字节)
Y = positionY (2字节)
TS = timestamp (2字节, 仅最后一个采样)💡 关键理解:offsetIncrementBytes是4,意味着从一个样本的X位置到下一个样本的X位置偏移4字节。因为中间隔了2字节的Y位置,所以X→Y是2字节,Y→下一个X是2字节,总共4字节。
3.4 每个采样都有时间戳的情况
如果isPerSampleTimestamp设为TRUE,数据布局会变化:
每个采样5字节 → 6字节(增加2字节时间戳)
总数据量 = 1 + (2+2+2) × 12 = 73字节
offsetIncrementBytes变为6(X→Y是2,Y→TS是2,TS→下一个X是2)
timestamp的offset从49变为5(第一个时间戳在第5字节)四、自定义OIS Library
如果OIS硬件的原始数据格式不符合标准的FormatInfo结构,可以实现自定义解析库。
4.1 核心数据结构
// OIS库需要填充的输出结构
typedef struct LensPosition {
FLOAT shiftX; // X方向镜头位移
FLOAT shiftY; // Y方向镜头位移
UINT64 timeStampInNS; // 纳秒级时间戳
} LensPosition;
// 框架传给OIS库的输入+输出结构
typedef struct LensPositionDataInfo {
LensPosition* pLensPos; // 输出:格式化后的数据
UINT validSamples; // 输出:有效采样数
UINT32 totalSamples; // 输入:pLensPos能容纳的采样数
UINT8* pRawLensPosData; // 输入:原始数据指针
UINT32 rawDataSize; // 输入:原始数据大小
UINT64 QTimerInNS; // 输入:读取时的QTimer值
} LensPositionDataInfo;4.2 实现自定义解析函数
// 自定义格式化函数实现
static bool FormatOISLensPositionData(
LensPositionDataInfo* pLensPosData)
{
// 1. 从pRawLensPosData解析原始数据
UINT8* rawData = pLensPosData->pRawLensPosData;
UINT32 dataSize = pLensPosData->rawDataSize;
// 2. 按自己的格式解析每个采样
// ... 自定义解析逻辑 ...
// 3. 填充输出
pLensPosData->pLensPos[0].shiftX = parsedX;
pLensPosData->pLensPos[0].shiftY = parsedY;
pLensPosData->pLensPos[0].timeStampInNS =
convertToNS(rawTimestamp, pLensPosData->QTimerInNS);
pLensPosData->validSamples = actualValidCount;
return true;
}
// 注册API
void GetOISLibraryAPIs(OISLibraryAPI* pOISLibraryAPI) {
pOISLibraryAPI->pFormatOISLensPositionData =
FormatOISLensPositionData;
}4.3 部署流程
# 1. 编译生成so库
# 库命名规范:com.<vendor>.ois.<ois_name>.so
# 例如:com.oem.ois.custom_ois.so
# 2. 放置到指定路径
# /vendor/etc/camera/com.oem.ois.custom_ois.so
# 3. 在vendor.mk中添加
# PRODUCT_PACKAGES += com.oem.ois.custom_ois.so
# 4. OIS驱动会自动加载指定名称的so库五、CHI Node如何使用NCS获取OIS数据
EIS等CHI Node通过NCS接口获取OIS数据,核心是三个API:
这种设计让EIS可以在任意时刻请求OIS数据,不受帧边界限制,实现了跨帧数据聚合。
六、避坑指南
- — 必须小于理论最大值,考虑CCI延迟和软件调度抖动
- — 这是格式化正确性的关键,算错一个字节后面全部错位
- —
INTERNALCLOCKTICKS需要配合timerClockFrequency转换成纳秒 - — 如果实现了
pFormatOISLensPositionData,XML中的FormatInfo不会生效 - — 高采样率带来更多I2C中断和CPU负载,功耗文档也提到过这点
📌 本文核心要点
- OIS数据通过I2C轮询读取,由XML完全配置驱动
- NCS解决的是"帧对齐Pipeline"和"时间对齐算法"的矛盾
- FormatInfo的offset/sizeInBytes/offsetIncrementBytes三参数定义数据解析规则
- 自定义OIS Library可以处理非标准数据格式,优先级高于XML配置
- CHI Node通过GetDataSource/GetData/PutDataSource三接口使用NCS
觉得有帮助?点个「在看」转发给同事吧 👇
更多Camera开发实战内容
欢迎加入知识星球「小驰成长圈」
120+ Camera工程师 · 340+ 实战内容 · 已运营1565天

微信扫码 · 加入星球
评论 (0)