为我的监控摄像头适配Thingino
Table of contents
契机¶
因为一些原因,得到了一部“双目云台摄像头”。因为讨厌原系统依赖的云平台和私有软件,故尝试刷机/修改系统。这篇文章是根据进度日报对整个过程的复盘。
设备分析¶
- 摄像头型号: LY-SM-YT40
- 完全看不出名堂
- 厂商:合肥岭雁科技(linkintec)
- 生产企业:深圳市乔安科技(Jooan)
- 云平台:移动爱家
拆解过程概要¶
按压底部二维码的贴纸,发现两处可能的螺丝位,撕开(表扬一下,贴纸质量不错)符合预期。卸下螺丝,便能卸下摄像头的下半部分,暴露出本体与下部副摄像头的连接排线。解开排线,彻底分离下半部分外壳。


将云台轻轻旋转对齐至中位,发现四个螺丝孔位,推测它们固定了上半部分外壳,卸下后验证了想法。轻轻拆除上半部分外壳。
摄像头电路主体在云台半球后方暴露出来。为保证稳定性,云台电机和电源部分点有红色胶水。暂时可以不拆线路。观察发现云台上半球是直接扣进转轴,轻掰转轴两侧支架、取下半球。

卸下电机和散热硅胶块块,暴露出了SoC本体和NAND存储器,可以确认平台为Ingenic T40XP。

进一步拆解发现,靠近中间的两颗螺丝是镜头模组固定螺丝,而周边三颗螺丝才是主板固定螺丝。

固件备份¶
首先尝试了从串口终端直接备份,但被u-boot和系统双重密码挡下。又尝试了官方推荐的ingenic-cloner,但似乎没有现成的配置,并且电脑USB不认这台设备。手上也没有合适的工装。只好选择拆芯片上编程器来备份固件。
NAND型号为FORESEE F35SQA001G,容量128MiB,采用CH341A和 SNANDer 进行备份。备份速度挺慢的。 重复备份操作,得到两份备份,对比确认读取无误。
snander -r dump.bin
snander -r dump2.bin
固件拆解¶
根据u-boot启动日志可见分区表(也可以从dump里的u-boot env中找到),对比binwalk结果判断,可以按它分割固件。
Creating 8 MTD partitions on "sfc_nand":
0x000000000000-0x000000100000 : "boot"
0x000000100000-0x000000200000 : "bootenv"
0x000000200000-0x000000600000 : "kernel"
0x000000600000-0x000001200000 : "rootfs"
0x000001200000-0x000001a00000 : "appfs"
0x000001a00000-0x000004c00000 : "config"
0x000004c00000-0x000005400000 : "appfs1"
0x000005400000-0x000005500000 : "confbak"
编写python脚本(也可以直接dd)处理固件,拆分各个分区。经检查,rootfs存放了系统通用基础组件,而appfs存放了streamer等服务的主程序。系统为busybox init。rootfs和appfs均为squashfs,可以通过squashfuse进行挂载。
其它次要分区有UBI格式的,处理起来比较繁琐,但若只是提取文件,可以使用ubi_reader:
uvx --from ubi_reader ubireader_extract_files ubi_partition_image.bin
从init脚本来看,rootfs在启动时仅仅是加载了WiFi驱动、设定一些性能参数,随后将控制权交给appfs的jzstart.sh进一步加载特定sensor驱动、启动主程序。
后续分析发现,部分设备GPIO配置明文存储在全局配置文件config.org(json,后缀表示origin)中,推测是为了方便复用相关软件。
修改固件获取shell¶
linux kernel的入口点由u-boot env给的cmdline确认,而它们恰巧又在bootenv分区,故我选择修改了bootenv分区的内容。编写了一个脚本,将txt格式的boot env转换成binary格式。头部四个字节是后续所有字节的CRC32值。
将cmdline中init=/linuxrc修改为init=/bin/sh,得到了修改后的bootenv分区,一样通过snander刷入NAND芯片:
# NAND写入前要擦除,snander不会自动处理
snander -e -a 0x000000100000 -l 0x000000100000
snander -w modded_env.bin -a 0x0000000000100000 -l 0x0000000000100000
虽然获取了shell,但没什么用处,只是导出了一份设备树。
初步适配Thingino¶
理解Thingino¶
Thingino使用Buildroot2构建,对每个摄像头都有自己的配置。我在ArchLinux上创建了个Ubuntu 24.04的rootfs用于环境搭建。复制了一份摄像头配置,参考项目文档开始编译。而测试发现master分支难以编译,须选用stable分支进行适配。
GROUP环境变量可以用来选择摄像头配置文件夹。比如默认是camera,但设置了GROUP之后,就会变成camera-$GROUP,方便区分不同状况的摄像头配置。而camera-exp就属于测试中的摄像头配置。
有了在u-boot env和设备配置文件中找到的GPIO信息,初步配置得以完成。
编译并安装¶
跳过编译过程。
由于开源版u-boot暂时缺少对NAND flash的支持,我便将NAND换成了16MiB的NOR flash,反正都能用。 ¯\(ツ)/¯
但是启动不了……每次启动后,u-boot会在清除env分区时将整个flash清空……花了点时间定位问题,是flash擦除页面大小和分区没有对齐,根源在flash驱动内部状态overflow。修好擦除大小问题后,成功通过串口看到了Thingino后台(不放图了,按F键进入failsafe模式)。
适配摄像头组件¶
到目前为止,设备能跑Thingino,但是摄像头、照明、联网功能全部失灵,需要适配/修复。
按钮和指示灯¶
按钮¶
按钮有两个,reset和call/通话。
按钮也好找,因为按钮是输入量,直接所有的GPIO读一遍,按住按钮再读一遍,就能找到了。
另外reset这个按钮有在配置里写出来。
指示灯¶
这个难搞,纯猜的。配置里有写照明灯的GPIO,之前有测出按钮的GPIO,猜了个范围,试了下,没想到猜对了。
不敢轻易将所有GPIO设置为输出试一遍就是怕不小心玩坏什么。
WiFi¶
WiFi是ATBM6012BX,这点可以通过3点轻松判断:
- 拆包检查既有驱动
- 查看芯片丝印
- lsusb查看PID:VID
真没想到这里的WiFi竟然走USB。
WiFi模块的电源有个开关,很巧的是这个GPIO写在了原厂固件配置里。轻松配置好。但是在系统启动后遇到了意料之外的错误:
[ 62.812011] atbm6012bx 1-1:1.0: Direct firmware load for /lib/firmware/atbm6012bx_fw.bin failed with error -2
不对呀,固件明明一起打包进去了,怎么会找不到?(-2代表-ENOENT,no such file or directory)没办法,clone一份kernel源码看看问题。找到drivers/base/firmware_class.c中这样写:
/* direct firmware loading support */
static char fw_path_para[256];
static const char * const fw_path[] = {
fw_path_para,
"/lib/firmware/updates/" UTS_RELEASE,
"/lib/firmware/updates",
"/lib/firmware/" UTS_RELEASE,
"/lib/firmware"
};
原来固件默认加载位置就那么几个(fw_path_para也只是多自定义一个)……而WiFi模块驱动里写的固件名称是个绝对路径,相当于路径重复了两遍……
就先临时打个补丁改下吧。然后就好了,嗯。
摄像头传感器(sensor)¶
研究一番发现摄像头传感器走的是MIPI CSI,在预期之中。一开始是一点头绪都没有,就尝试直接把原厂驱动插进去看看能不能用。有点曲折的是我还不怎么会用buildroot。总之想办法塞进去后,它能用。
不过实际上是我先尝试逆向。使用Ghidra反编译原厂sensor驱动的内核模块后,提取sensor初始化参数,发现自己的逆向驱动有图像却很暗,非常不爽,感觉功亏一篑。折腾许久,甚至用Ghidra逆向了自己的内核模块,才发现程序流程出现了意外的分支,究其根源是switch-case中漏掉了一个break (XoX)
中间本想通过加载已有源代码来加快分析,但是加载所需配置非常头疼,完全搞不懂,所以没有一个header加载成功过…… QvQ
我的摄像头有两个sensor,也是逆向了才知道,主摄和副摄的数据线正好和SoC自己的顺序反过来。
尝试了用一个内核驱动服务多个同款sensor,结果不行,似乎因为共享内存问题,内核会Oops。还是别Oops为好,不差这点体积。
总之sensor驱动也就搞定了。然后我也成了Ghidra初级用户。
云台电机¶
云台用的是5线四相步进电机。从原厂固件中的脚本分析,电机的引脚同样写在设备全局配置中。对比了下既有配置,竟然还找到一款类似产品,Jooan出品。不过还是得测试。
SDK中4.4内核的电机版本与Thingino不兼容,我尝试把3.10的直接复制过去,发现用不了,怎么办呢?只好在结合4.4既有基础上从3.10的电机驱动程序那边引入对应的功能。
结果改着改着,发现原来3.10的电机驱动程序里藏了几个核心bug,也难怪直接复制之后不能用。主要问题包括:
- 把所有相位都通电当成了上电正常状态
- 遗漏的逻辑反转判断
- 在spinlock和interrupt上下文中使用不安全的
gpio_direction_output()
在4.4内核的既有代码算法有点意思,会计算最大公约数、将斜方向移动分成相同的小段,尝试解决运动抖动的问题。可惜光这样还不够,抖动会在每一个小段的末尾发生,因为移动距离较短的方向会不断加减速。我的解决方法是,在每个小段上,将运动较短方向上的触发时机均匀地分散开,与较长的对齐,尽量保证两边消耗的时间相同。算法细节就不提了,没什么含量(还花了我不少时间,脑子不行叻)。不过interrupt上下文内不能做除法,就因为除以零会触发中断,这点算是学到了。
其它组件¶
运气比较好,什么IR-CUT、红外管之类的东西在配置里有写,我只需要测试一下就好。
不过关于扬声器GPIO,我一开始以为它是信号输出引脚,可实际是功放的开关,让我研究了半天。
优化调整¶
云台活动范围¶
云台电机的步进角为5.625/64,转一周是4096步,搞不懂为什么厂家固件参数给到了4200,实在难绷,余量留太多。
于是我根据限位器的角度测算了大致活动范围的步进数,应该差不多了,没必要转到底。
夜间模式自动切换¶
当前Thingino的日夜自动切换由prudynt控制,原理是观察sensor的exposure并与预设阈值判断。算是一种非常简单的防抖/滤波算法。就是……exsposure的范围太小了啦!1万哪里够啊,我这边最大值都56万了!通常数值是5万上下!
所以就,想办法把这个值的合法范围扩大到了100万。
结语¶
也没什么好说的,就是逐渐熟悉项目,逐渐适应项目,发现一个又一个问题,解决一个又一个问题。不久之后会忘光的吧,所以写点东西记录一下。
在这里丢一个编译好的固件。目前只有一个摄像头能用,而且禁用了Telegram Bot。录像够用,动作检测也是,够用。