一开始,我以为这只是几条 I²C 命令
第一次看到 CXN0102 的资料时,我的想法很简单:模块通过 I²C 和 MCU 通信,发送几个命令,就能控制画面、读取温度,应该不会太复杂。
真正把资料从头看到尾之后,才发现事情没有这么轻松。
它当然还是 I²C,但这条总线上不仅有“发命令”和“收数据”,还挂着一根 COM_REQ。模块有话要说时,不会主动把数据塞到总线上,而是先把 COM_REQ 拉高,等 HOST 注意到之后再来读取。更麻烦的是,模块有些命令会回复,有些命令只在出错时回复,还有一些通知完全是模块主动发起的。
如果只把 CXN0102 当成一个普通的 I²C 外设,驱动很快就会变成一堆“能发出去,但不知道现在到底发生了什么”的代码。
所以我想把这次整理下来的东西写下来。不是再列一遍完整命令表,而是从最容易让人困惑的地方开始,看看怎样才能把 CXN0102 真正接进一个可靠的 MCU 系统里。
第一个疑惑:0x77 到底应该怎么发
先从最基础的地址开始。
CXN0102 的 I²C 从地址是 7 位地址 0x77。这个数字本身没有什么特别,但不同 MCU 的 HAL 对地址的要求并不统一。有的接口让你传 7 位地址,有的接口却希望你传入已经包含读写位的 8 位地址。
如果是后者,那么:
写地址:0xEE读地址:0xEF这也是调试时很容易误判的地方。代码看起来没有问题,SCL 也在跳,SDA 甚至也有数据,但模块就是不应答。最后一看逻辑分析仪,才发现地址格式传错了。
因此,第一次联调时我会先做一件很朴素的事:不要急着发复杂命令,先确认 HAL 的地址定义,再用逻辑分析仪确认地址阶段是否收到了 ACK。这个检查很简单,却能省掉后面大量无意义的排查。
还有一件事不能只看软件。固件规格书没有规定 SDA、SCL 和 COM_REQ 的电平、上拉阻值以及最高 I²C 频率。也就是说,不能因为手边其他模块都是 3.3 V,就顺手把 CXN0102 也按 3.3 V 设计。电平、上拉、电源时序、复位和视频接口,都应该回到硬件规格或原理图里确认。
第二个细节:普通命令永远有一个 OP0
CXN0102 的普通命令格式非常规整:
CMD OP0 OP1 OP2 ... OPnCMD 是命令编号,OP0 表示后面有多少个操作数字节。
最容易漏掉的就是没有操作数时的 OP0。Start Input 看起来像是命令 0x01,但真正需要发送的是:
01 00不能只发送一个 01。
设置视频位置也是同样的道理:
26 09 PAN TILT FLIP 64 00 00 00 00 00这里 26 是命令,09 表示后面有 9 个操作数。后面的 64 和几个 00 并不是可以随意删除的内容,它们属于这个命令规定的字段和保留字段。
另外,普通命令必须在一个完整的 I²C transaction 中发送完。不要把 CMD、OP0 和操作数拆成几次写入。模块接收一帧数据时,如果字节之间停顿接近 400 ms,还可能触发接收超时。
协议中所有多字节数字都是小端序。比如:
0xAABBCCDD → DD CC BB AAPAN、TILT、亮度和对比度这类有符号单字节参数,则用 8 位补码表示。PAN=-10 时,线上看到的是 F6,而不是一个带负号的数字。
这些细节单独看都不难,真正让人疲惫的是:它们往往不会在代码编译时出错,只会在模块沉默不应答时,才慢慢暴露出来。
最让我在意的地方:COM_REQ 和 32 字节 Notify
如果说普通 Request 是“我问模块一个问题”,那么 Notify 就是“模块主动来找我”。
CXN0102 有命令结果要返回,或者检测到温度、安全和内部状态变化时,会把 COM_REQ 拉高。HOST 看到这个信号后,再通过 I²C 读取数据。
握手过程可以简化成这样:
CXN0102:COM_REQ = HighHOST: START → 0x77 + Read → 读取 32 字节 → NACK → STOPCXN0102:COM_REQ = Low这里有一个看上去很反直觉的要求:即使 Notify 实际只有几个有效字节,HOST 也必须读取完整的 32 字节。
Notify 的有效部分是:
CMD OP0 RESULT DATA...OP0 告诉我们从它后面开始有多少有效数据,剩余部分只是 dummy。也就是说,读取长度不能根据 OP0 动态决定,应该先固定读满 32 字节,再按照 OP0 解析有效内容。
COM_REQ 可以接 GPIO 外部中断,但中断函数里不要直接做阻塞式 I²C。ISR 只需要设置一个标志或释放信号量,真正的读取放在通信任务或主循环中完成。模块最多缓存 3 条 Notify,所以读取完一条后,如果 COM_REQ 仍然为高,就继续把剩下的通知取出来。
我比较喜欢把这部分单独做成一个 Notify 队列。这样一来,命令响应和主动通知不会互相覆盖,温度异常也不会因为某个业务线程正在等待参数读取而被忽略。
上电以后,最重要的不是“等几秒”\n+
很多驱动喜欢上电后固定延时几秒,然后直接开始发命令。这个方法有时能跑,有时又会在不同温度、不同电源条件下变得不稳定。
CXN0102 上电后会处于 Booting 状态,典型启动时间大约 5 秒,但真正可靠的判断依据不是延时,而是 Boot Completed Notify:
00 01 RESULT只有 RESULT=0x00 才表示模块启动成功。
所以软件状态最好不要只有一个简单的 powered 标志,而是至少区分:
Booting → Ready → Active ↑ │ └─ Stop ┘上电之后先进入 Booting,收到成功的启动通知后进入 Ready,发送 Start Input 后才进入 Active。这样一来,应用层看到的就不再是“电源已经打开”,而是“模块现在确实可以接受哪类命令”。
Start Input 和 Stop Input 分别是:
01 0002 00Start Input 可以在视频还没有准备好时发送,模块会先输出黑画面,等输入有效后再切换到视频。输入已经准备好的情况下,规格书给出的切换时间不超过 33.3 ms。
这个状态划分看起来有些啰嗦,但它会让后面的关机、测试图和异常恢复变得清楚很多。
停止画面:有时候最后一帧比黑屏更重要
普通 Stop Input 会显示 Opening picture,但产品实际想要的效果可能完全不同。有的场景需要黑屏,有的场景希望显示一张固定的 Opening picture,还有的场景不希望画面突然跳走,而是保持最后一帧。
这时可以使用 Stop Input Specially:
0C 01 SELECTSELECT 的取值是:
| SELECT | 停止后的画面 |
|---|---|
| 00 | Opening picture |
| 01 | Mute Black picture |
| 02 | Mute Opening picture |
| 04 | Final picture,保持最后一帧 |
静音使用类似的命令:
03 01 SELECTSELECT=0x00 表示取消静音。0x03 在这两个命令中都是保留值,不要拿来试。
这些选择看似只是几个枚举值,但它们其实直接决定了用户看到的体验:停止视频时是干净地黑掉、显示品牌画面,还是把最后一帧留在屏幕上。驱动层应该把这些值封装成有意义的 API,不要让上层业务到处散落着 0、1、2、4 这样的魔法数字。
关机是一件需要认真对待的事
对很多外设来说,关机就是拉低电源。但 CXN0102 不是这样处理更稳妥。
如果模块处于 Active 状态,应该先发送 Stop Input,等待成功响应,再发送 Shutdown:
0B 01 00确认 Shutdown 成功后,才按照硬件电源时序切断供电。
软件重启使用:
0B 01 01重启命令写成功,并不意味着模块已经重启完成。发送完 Reboot 后,软件状态应该回到 Booting,然后重新等待 00 01 00。
这件事值得单独强调,因为“写成功”和“状态完成”在这套协议里经常不是同一件事。I²C 外设收到了命令,只能说明数据到达了;它什么时候完成动作,要看对应的 Notify 或后续状态。
画面调整:先读出来,再动手改
CXN0102 支持视频位置、翻转、光轴和双相位调整。刚开始调试时,我更建议先使用 Get 命令读取当前值,再使用 Set 修改一个参数,最后再读回来确认。
视频位置由 PAN、TILT 和 FLIP 组成:
26 09 PAN TILT FLIP 64 00 00 00 00 00PAN 范围是 -30~+30,TILT 范围是 -20~+30:
00:不翻转01:左右翻转02:上下翻转03:上下且左右翻转当 PAN=-10、TILT=+5、左右翻转时,完整帧是:
26 09 F6 05 01 64 00 00 00 00 00光轴有 R0、R1、G0、G1、B 五路,水平每个单位是 1/8 像素,垂直每个单位是 1/2 像素。双相位则是范围 -100~+100 的有符号 32 位小端整数。
调整流程开始后,模块允许的命令会变少。比如简易光轴调整期间,主要只能发送调整、读取和退出命令。这个阶段最好由驱动维护独立状态,暂时禁止其他业务线程插入普通命令,否则很容易把一次完整的调整流程打断。
画质和测试图,是最容易让调试变得有趣的部分
亮度、对比度、色调、饱和度和锐度都可以通过 MCU 调整:
| 参数 | 常用范围 |
|---|---|
| Brightness | -31~+31 |
| Contrast | -15~+15 |
| Hue U/V | -15~+15 |
| Saturation U/V | -15~+15 |
| Sharpness | 0~6 |
单项设置分别使用 43、45、47、49 和 4F 命令。发送前要做范围检查,尤其要注意有符号参数的编码。
规格书在 Set All 命令的 Hue 和 Saturation 范围上有一处不一致。为了让驱动在不同固件上更稳妥,建议统一限制在 -15~+15,除非供应方明确确认目标固件支持更大的范围。
测试图只能在 Ready 状态使用。Color bar、Cross hatch、Raster、Ramp、Circle、Checkerboard 和分辨率图,都是联调时很有价值的工具。它们不像一张普通图片那样只是“显示出来”,而是在帮我们判断投影位置、颜色、扫描线和几何校正到底有没有问题。
不过,测试图命令中的 GENERAL 字段会随着图案变化而改变含义。Checkerboard 的 GENERAL=0x00 和 0x01 在 v1.5.1 中都被描述为前景单色,差异没有说明;如果需要明确的分块效果,使用 0x02~0xFF 会更容易得到可预测的结果。
温度异常不是普通错误
CXN0102 会主动发送几类重要通知:
| CMD | 含义 |
|---|---|
| 00 | Boot Completed |
| 10 | Emergency Notify |
| 11 | 温度异常或恢复 |
| 12 | 命令队列或接收异常 |
温度可以通过 A0 00 读取,返回当前温度、Mute 阈值和系统停止阈值。默认情况下,Mute 阈值为 60 °C,系统停止阈值为 65 °C,但产品逻辑最好使用实际读取到的参数。
收到 Emergency Notify 后,不能把它当成普通的命令失败。Laser Safety Module 异常、固件 Abort、MEMS 或激光异常,都可能让模块进入 Safety Stop。此时应该立即停止普通画面控制,进入产品自己的安全故障流程,而不是重新发送 Start Input 试图“碰碰运气”。
Temperature Stop 和 Safety Stop 也应该在软件中区分。温度异常恢复后,模块可能回到原来的状态;安全停止则不能按照同样的方式自动恢复。
如果收到 CMD=0x12、RESULT=0xFB,说明待处理命令太多了,需要放慢发送速度。RESULT=0xF0 则表示 Request 接收超时,应该丢弃未完成帧,从完整的命令边界重新开始发送。
最后,驱动不应该只是一个“大函数”
整理到这里,我觉得 CXN0102 最适合用下面这样的方式组织:
产品状态机 ↓CXN0102 命令 API 与参数校验 ↓请求调度、响应匹配、Notify 队列 ↓I²C HAL、COM_REQ GPIO、中断和互斥锁底层负责把数据可靠地写出去、读回来;命令层负责组帧和检查参数;调度层负责排队、超时和匹配响应;最上层则决定当前状态到底允不允许执行某个动作。
这样组织之后,很多问题都会变得容易解释:
- I²C 写入成功,不等于画面已经开始输出。
- Reboot 写入成功,不等于模块已经重新启动。
- Shutdown 写入成功之前,不能断电。
- Error Notify 类型的命令成功时本来就可能没有成功通知。
- 主动的温度和安全通知,不能因为当前正在等待另一个命令而被丢掉。
CXN0102 命令列表
下面这份列表把文章中提到的常用命令集中放在一起,方便调试时查找。除主动通知外,普通命令都要按照 CMD、OP0、操作数的格式发送;没有操作数时也要保留 OP0=00。
基本控制
| CMD | 名称 | Request 示例 | 响应 |
|---|---|---|---|
| 01 | Start Input | 01 00 | Always Notify |
| 02 | Stop Input | 02 00 | Always Notify |
| 0C | Stop Input Specially | 0C 01 SELECT | Always Notify |
| 03 | Mute / UnMute | 03 01 SELECT | Always Notify |
| 07 | Save User Param | 07 05 00 00 POS OPT_PHS PQ | Always Notify |
| 08 | Initialize User Param | 08 00 | Always Notify |
| 0B | Shutdown / Reboot | 0B 01 OPTION | Always Notify |
Stop Input Specially 和 Mute/UnMute 中,SELECT=00、01、02、04 分别对应 Opening、Mute Black、Mute Opening 和 Final picture;SELECT=03 是保留值。Shutdown/Reboot 中 OPTION=00 为关机,OPTION=01 为软件重启。
位置与校准
| CMD | 名称 | Request |
|---|---|---|
| 25 | Get Video Output Position | 25 00 |
| 26 | Set Video Output Position | 26 09 PAN TILT FLIP 64 00 00 00 00 00 |
| 27 | Get Optical Alignment | 27 00 |
| 28 | Set Optical Alignment | 28 0D 后跟 13 个参数 |
| 29 | Get Biphase | 29 00 |
| 2A | Set Biphase | 2A 04 后跟 4 字节小端数据 |
| 32 | Easy Optical Control | 32 00 |
| 33 | Easy Optical Plus | 33 00 |
| 34 | Easy Optical Minus | 34 00 |
| 35 | Easy Optical Exit | 35 01 SAVE |
| 36 | Easy Biphase Control | 36 00 |
| 37 | Easy Biphase Plus | 37 00 |
| 38 | Easy Biphase Minus | 38 00 |
| 39 | Easy Biphase Exit | 39 01 SAVE |
设置位置、光轴和双相位的命令通常只在参数合法时执行;简易调整命令则必须在对应的调整流程中使用。35 和 39 中 SAVE=00 表示不保存退出,SAVE=01 表示保存退出。
画质与测试图
| CMD | 名称 | Request |
|---|---|---|
| 40 | Get All Picture Quality | 40 00 |
| 41 | Set All Picture Quality | 41 09 后跟 9 个参数 |
| 42 / 43 | Get / Set Brightness | 42 00 / 43 01 VALUE |
| 44 / 45 | Get / Set Contrast | 44 00 / 45 01 VALUE |
| 46 / 47 | Get / Set Hue | 46 00 / 47 02 U V |
| 48 / 49 | Get / Set Saturation | 48 00 / 49 02 U V |
| 4E / 4F | Get / Set Sharpness | 4E 00 / 4F 01 VALUE |
| A3 | Output Test Picture | A3 11 后跟测试图参数 |
Brightness 的范围是 -31~+31,Contrast 是 -15~+15,Hue 和 Saturation 的 U/V 是 -15~+15,Sharpness 是 0~6。Set All 中的 Hue 和 Saturation 也建议限制在 -15~+15。
A3 测试图命令只能在 Ready 状态使用,PATTERN 和 GENERAL 的具体含义会随测试图类型变化。
信息读取
| CMD | 名称 | Request | 返回信息 |
|---|---|---|---|
| A0 | Get Temperature | A0 00 | 当前温度、Mute 阈值、停止阈值 |
| A1 | Get Time | A1 00 | 累计运行时间,单位为秒 |
| A2 | Get Version | A2 00 | 固件、参数和数据版本 |
| B2 | Get LOT Number | B2 00 | LOT 号 |
| B4 | Get Serial Number | B4 00 | 序列号 |
模块主动通知
| CMD | 名称 | 典型格式 | 处理重点 |
|---|---|---|---|
| 00 | Boot Completed Notify | 00 01 RESULT | RESULT=00 后进入 Ready |
| 10 | Emergency Notify | 10 03 RESULT R0 R1 | 进入安全故障处理 |
| 11 | Temperature Emergency / Recovery | 11 01 RESULT | 区分 Mute、停止和恢复 |
| 12 | Command Emergency Notify | 12 02 RESULT REFERENCE | 降低发送速率或重新组帧 |
主动通知不是由 HOST 主动请求的命令,它们都通过 COM_REQ 触发读取。读取后仍然要固定取满 32 字节,再依据 OP0 解析有效数据。
写在最后
CXN0102 的协议让我印象比较深的一点是:它表面上是一组命令,实际上更像是一个需要被认真对待的小系统。
你要先等它启动,再判断它是否真的 Ready;要区分命令已经收到,还是动作已经完成;要在模块发出 COM_REQ 时及时回应;要在温度和安全事件出现时把普通业务放到一边。那些看起来有些麻烦的状态和握手,最后都是为了让投影模块在真实产品里更可控、更安全。
如果以后再接入类似的模块,我大概不会一上来就写业务功能,而是先把三件事搭好:可靠的 I²C 收发、独立的 Notify 队列,以及清晰的状态机。等这三件事稳定下来,画面控制、光轴调整和其他功能,反而只是往上面一层层添加能力而已。
联调时,逻辑分析仪仍然是最诚实的伙伴。去看看 START、地址、ACK/NACK、32 字节 Notify 和 STOP,通常比盯着一行“写入成功”的日志更接近问题的真相。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时







