mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
4667 字
13 分钟
我为什么在 CXN0102 的 0x77 上折腾了半天
2026-09-04

一开始,我以为这只是几条 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 ... OPn

CMD 是命令编号,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 AA

PAN、TILT、亮度和对比度这类有符号单字节参数,则用 8 位补码表示。PAN=-10 时,线上看到的是 F6,而不是一个带负号的数字。

这些细节单独看都不难,真正让人疲惫的是:它们往往不会在代码编译时出错,只会在模块沉默不应答时,才慢慢暴露出来。

最让我在意的地方:COM_REQ 和 32 字节 Notify#

如果说普通 Request 是“我问模块一个问题”,那么 Notify 就是“模块主动来找我”。

CXN0102 有命令结果要返回,或者检测到温度、安全和内部状态变化时,会把 COM_REQ 拉高。HOST 看到这个信号后,再通过 I²C 读取数据。

握手过程可以简化成这样:

CXN0102:COM_REQ = High
HOST: START → 0x77 + Read → 读取 32 字节 → NACK → STOP
CXN0102: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 00
02 00

Start Input 可以在视频还没有准备好时发送,模块会先输出黑画面,等输入有效后再切换到视频。输入已经准备好的情况下,规格书给出的切换时间不超过 33.3 ms。

这个状态划分看起来有些啰嗦,但它会让后面的关机、测试图和异常恢复变得清楚很多。

停止画面:有时候最后一帧比黑屏更重要#

普通 Stop Input 会显示 Opening picture,但产品实际想要的效果可能完全不同。有的场景需要黑屏,有的场景希望显示一张固定的 Opening picture,还有的场景不希望画面突然跳走,而是保持最后一帧。

这时可以使用 Stop Input Specially:

0C 01 SELECT

SELECT 的取值是:

SELECT停止后的画面
00Opening picture
01Mute Black picture
02Mute Opening picture
04Final picture,保持最后一帧

静音使用类似的命令:

03 01 SELECT

SELECT=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 00

PAN 范围是 -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
Sharpness0~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含义
00Boot Completed
10Emergency 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 示例响应
01Start Input01 00Always Notify
02Stop Input02 00Always Notify
0CStop Input Specially0C 01 SELECTAlways Notify
03Mute / UnMute03 01 SELECTAlways Notify
07Save User Param07 05 00 00 POS OPT_PHS PQAlways Notify
08Initialize User Param08 00Always Notify
0BShutdown / Reboot0B 01 OPTIONAlways 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
25Get Video Output Position25 00
26Set Video Output Position26 09 PAN TILT FLIP 64 00 00 00 00 00
27Get Optical Alignment27 00
28Set Optical Alignment28 0D 后跟 13 个参数
29Get Biphase29 00
2ASet Biphase2A 04 后跟 4 字节小端数据
32Easy Optical Control32 00
33Easy Optical Plus33 00
34Easy Optical Minus34 00
35Easy Optical Exit35 01 SAVE
36Easy Biphase Control36 00
37Easy Biphase Plus37 00
38Easy Biphase Minus38 00
39Easy Biphase Exit39 01 SAVE

设置位置、光轴和双相位的命令通常只在参数合法时执行;简易调整命令则必须在对应的调整流程中使用。35 和 39 中 SAVE=00 表示不保存退出,SAVE=01 表示保存退出。

画质与测试图#

CMD名称Request
40Get All Picture Quality40 00
41Set All Picture Quality41 09 后跟 9 个参数
42 / 43Get / Set Brightness42 00 / 43 01 VALUE
44 / 45Get / Set Contrast44 00 / 45 01 VALUE
46 / 47Get / Set Hue46 00 / 47 02 U V
48 / 49Get / Set Saturation48 00 / 49 02 U V
4E / 4FGet / Set Sharpness4E 00 / 4F 01 VALUE
A3Output Test PictureA3 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返回信息
A0Get TemperatureA0 00当前温度、Mute 阈值、停止阈值
A1Get TimeA1 00累计运行时间,单位为秒
A2Get VersionA2 00固件、参数和数据版本
B2Get LOT NumberB2 00LOT 号
B4Get Serial NumberB4 00序列号

模块主动通知#

CMD名称典型格式处理重点
00Boot Completed Notify00 01 RESULTRESULT=00 后进入 Ready
10Emergency Notify10 03 RESULT R0 R1进入安全故障处理
11Temperature Emergency / Recovery11 01 RESULT区分 Mute、停止和恢复
12Command Emergency Notify12 02 RESULT REFERENCE降低发送速率或重新组帧

主动通知不是由 HOST 主动请求的命令,它们都通过 COM_REQ 触发读取。读取后仍然要固定取满 32 字节,再依据 OP0 解析有效数据。

写在最后#

CXN0102 的协议让我印象比较深的一点是:它表面上是一组命令,实际上更像是一个需要被认真对待的小系统。

你要先等它启动,再判断它是否真的 Ready;要区分命令已经收到,还是动作已经完成;要在模块发出 COM_REQ 时及时回应;要在温度和安全事件出现时把普通业务放到一边。那些看起来有些麻烦的状态和握手,最后都是为了让投影模块在真实产品里更可控、更安全。

如果以后再接入类似的模块,我大概不会一上来就写业务功能,而是先把三件事搭好:可靠的 I²C 收发、独立的 Notify 队列,以及清晰的状态机。等这三件事稳定下来,画面控制、光轴调整和其他功能,反而只是往上面一层层添加能力而已。

联调时,逻辑分析仪仍然是最诚实的伙伴。去看看 START、地址、ACK/NACK、32 字节 Notify 和 STOP,通常比盯着一行“写入成功”的日志更接近问题的真相。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

我为什么在 CXN0102 的 0x77 上折腾了半天
https://blog.srka.top/posts/cxn0102-mcu-communication/
作者
Lira
发布于
2026-09-04
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录