本帖最后由 qinyunti 于 2024-5-11 17:31 编辑


一. 前言  
二. 拆解和方案分析
2.1 初体验-简约时尚不简单  
2.1.1电池模块
2.1.2后面板
2.1.3 前面板
2.1.4 外观结构整体总结
2.2 详细拆机分析-抽丝剥茧,不漏过一个细节
2.2.1 前面板
2.2.2 后面板
2.2.3方案分析总结
三. APP体验
四. 改造-玩转NES游戏
4.1 方案设计
4.2分析猫眼和后屏UVC+UAC传输协议
4.2.1 抓包猫眼的枚举和通讯信息
4.3手柄设计  
4.4移植NES游戏模拟器
4.4.1系统框图
4.4.2 UVC显示设计  
4.3.3 UAC音频设计
4.4.4 按键获取
4.5玩起来  
4.5.1 电脑上玩  
4.5.2 门锁上玩  
4.5.3 门锁远程可视对讲,远程玩
4.6 总结
五. 总结
六. 附录 视频和软硬件代码仓库

前言

由于智能门锁的便捷,智能等优势,越来越得到了市场的认可,也越来越普及,同时市场竞争也越来越激烈。据我所知,几乎所有的亲戚朋友和邻居,家家户户都换上了智能门锁。有些是比较低端的只有指纹识别和刷卡功能的智能锁,有些则是更高端的带人脸识别,猫眼,可视对讲等功能的智能锁。从我个人角度来讲,接受智能门锁的最大动力是解决了忘记带钥匙这一最重要的痛点需求,所以一个好的产品永远是需要解决至少一个痛点的。当然市场大,到了一定阶段,入局者也会增加,竞争也会越来越激烈,竞争的方式无非就是走价格路线比如低端的百来块钱就能包安装一把的门锁,或者走高端差异化路线,高端的一两千甚至更高。        我们就来拆解凯迪仕某款智能门锁,来学习借鉴下优秀厂家优秀产品的设计方案,我们的目标是尽可能的详细(本文全文200张左右图片,1万多字,200多页,花费一个多月时间完成),全方面360度,无死角的拆解分析,以期对整个系统整体和细节有一个全面的分析。

当然作为电子爱好者,作为玩家,我们不仅仅如此,我们更需要玩点有意思的,既然智能门锁体现在智能上面,那么他的硬件配置肯定不会低,我们就因地制宜,将其改造为可以玩NES游戏的游戏机:坦克大战,魂斗罗...... 本地玩,远程玩......, 这才是我们拆解分析学习,学以致乐的态度。

拆解和方案分析

我们这里选择凯迪仕这个品牌的K20 Pro Max这个型号的智能门锁,属于较高端系列,带猫眼,人脸识别,可是对讲等功能。因为凯迪仕这个品牌在门锁领域的知名度较高,其产品也非常不错,也有较高端产品,所以就选他了。拆解借鉴肯定要参考优秀的厂家和优秀的产品。

拆解我们由整体到细节的思路进行。先初体验,然后详细拆解分析,最后APP体验也是重要一环,当然终极乐趣还是改造为NES游戏机,使用门锁玩游戏,甚至远程玩。

2.1 初体验-简约时尚不简单

本篇重点在外观和结构等,先整体上体验,后面才是详细的拆解分析。

拆解前,还是先把玩一下整体,从外观设计,结构,APP使用体验等角度入手,后面才真正进入拆解阶段。一个好的产品,外观结构设计绝对是重要元素,毕竟现在产品不仅仅是看功能的,也是要看颜值的,尤其是门锁是装在入户门的,是一个门面担当产品,有时候颜值可能是用户买单的第一参考要素,所谓的第一眼心动是很重要的。

我这里包装什么的都丢了,就不来开箱了,直接上手。我这里只有前后面板,和锂电池、其他的锁体、导向片、开孔孔位及尺寸图、保修卡、说明书、机械钥匙等没有,也不是本文的重点,也无关紧要,略去无伤大雅。

首先来个三大件,前后面板,电池的全家福合照,然后再逐一把玩。初体验也先从这三大件的整体开始。

170712cc129dzcsi6cd2si

2.1.1电池模块

首先来看电池模块,电池采用一体模块化设计,具备轻便,体积小,防护好,成本低等优点。

磨砂的质感

磨砂的质感看起来是非常不错的,颗粒度都比较合适且均匀,手感也不错。好的磨砂的质感是需要好的模具加工设计的,说明在产品设计细节上,还是比较注意的,走的高端路线,所以该花的成本要花,虽然电池是位于内部的组件不会经常触碰到,看到,但是走高端路线也必须保证其质量,这是塑造高端品牌形象需要注意的细节。

170712f4757mmdbxsvqmb0

接口细节

再来看电池接口细节,铜触点,突出的颗粒能保证很好的接触,铜片也不薄。再来看,下面会有个突出的边缘,为什么要有这个设计呢? 电池安装后触点朝下卡在面板里,这个突出就是方便用手把电池抠出来的,这就是用户体验设计了,所以成熟的产品是需要不断地迭代的,尤其是用户体验,好的公司总是很关注用户体验和问题反馈然后不断迭代完善。

170712a8w06ptrizp44tph

来个扣电池的特写,可以感受到操作比较方便。

170713l0lher34ss0xocms

再来看电池前后端都有一部分没有磨砂,这是为什么呢? 作为一个熟悉硬件的嵌入式软件开发工程师,一定是知道点模具设计知识的(狗头保护)。用我曾经第一份工作公司组织花了三个月的模具培训经历(有正经结业证书的那种),思考了一下,这里两个突出的地方注塑时需要用到顶针(应该不需要斜顶,角度不是斜的),这个光滑的地方就是顶针的接触面,其他磨砂的地方是模具内腔体做的,顶针这里一般就不做磨砂了,为什么不做呢,做了磨砂摩擦阻力大,顶针就不好脱模了(个人猜测,有专业的可以指出错误本人及时修改),另外猜测做了也不可能和其他表面一样,毕竟顶针边缘会有熔接线的,也不美观还增加成本所以没必要。

170713dfmnoonxfdfn48dx

170713lxm1d19xi6tc16x3

看到中间的细线,就是前后模的分界线,熔接线,整个电池是一体注塑的,这样电池作为一个独立的整体,模块化设计,减少了结构设计成本(如果可拆卸需要设计前后面板,卡扣结构,模具成本更高),体积更小,防潮性等防护性也比较好。

Type-C充电口

好评,新的产品肯定不能用Micro USB接口,要不太掉价了。旁边还有小的指示灯,细节也做的不错。实际充电看一下指示灯的效果。

170713nx9k3v2a2k2exdql

170713k1nz9n6mjb1q4k71

总结下电池模块的设计就是,质感不错,模块化设计,体积小,防护性好,降低了成本。

电池上的使用提示,这也是用户体验的体现,日本的产品做得好的一个方面就是说明书做的非常好,非常详细,一步步指导使用,很注意细节。这也是产品用户体验改善的一个方面。

170713ln2iq4y2inbhvecp

2.1.2后面板

先来个前后的”证件照”,这里把PCB的螺钉已经拆掉了。

这里有个疑问?为什么OPEN和CLOSE按键的字体是反的?

170714j7cz0fest00xqbmt

170714p1lzaxx748bl664l

整体属于塑料面板加金属外壳的形式,塑料面板磨砂,金属壳体,质感还是非常不错的。很多低成本的很可能是全部是塑料的,质量和安全性就没法比了。

170714wdenpcnkutpkykup

再来看把手部分细节和后面的走线

170715jpoihk5qq316c5op

170715gejteqdytwjsljtl

然后是开锁和按键部分细节,旋钮操作下来手感不错,回弹跟手。

170715l0y0cchqhfdfbqbw

后屏部分,一体设计,感觉还是很高端的,有定酷黑的商务风格。

170715rli97tyjzg71ylye

喇叭和喇叭的走线,以及喇叭腔体,喇叭走的双绞线,

170715jjkk99zmzyi1nj9u

170715e9el2mgbevvklm9g

后面板控制部分,各PCB板,这个在后面的方案详细拆解分析中再分析,初步看到全部都做了三防处理,接插件也点胶做了加固,这也是细节和质量保证的体现。

170716tqkqtekqgsscguej

2.1.3 前面板

同样的先来个前后”证件照”

170716lvyo8v8wry58ccg9

170716el86hhhhy6tq69jv

再来看细节

前面板是金属壳体拉丝设计,后面板没有,前面板是在外面的,这也是给别人看的更高级点,也是差异化设计的体现,成本均衡设计的一个考量。给别人呈现好的颜值也是很重要的,自己看的就稍微差点没关系。

170717xu7ks77ssbwcktkk

红外人体检测和TOF部分

170717mb1rgs1fpfc9c9z5

刷卡部分

170717iqpum6umq2jlx0m5

指纹部分

170717nhf1ozq2mhq4m36n

猫眼摄像头

170717wircjqxiqz0igrlc

带补光双摄人脸识别部分,上面有标志的地方是把手触摸检测。

170717nyrlrqrqq7r07yvx

机械锁和临时USB供电,也是typec接口。

170718p8hogdmgaqpfmdo9

2.1.4 外观结构整体总结

以上粗略的看了各个模块的外观和结构,整体上看到质感非常不错,也有设计感,属于时尚商务风,比较百搭点。也是有设计的,比如前后,内外配色质感上的差异化设计,整体风格沉稳,简约时尚但是不简单。磨砂塑壳和金属外壳结合的不错,缝隙较小,说明工艺控制的不错,金属拉丝体现高端,磨砂塑壳,手感质感不错。另外拿起来整个锁体是比较沉的,金属比较厚,和那种降成本全是塑料很薄的低成本款完全不一样。另外也有体现模块化的设计思想,整个设计,外观和结构都是不错的。

2.2 详细拆机分析-抽丝剥茧,不漏过一个细节

前面体验了整体的结构,外观,和风格,现在还是进入详细的拆解分析过程。还是按照前后面板分开进行。

2.2.1 前面板

2.2.1.1 拆解

我们先从前面板入手,

这四个是安装螺栓,不用管

170718hxzr0waxyblxwgwx

这个按钮用于拆解告警,默认是压下的,拆开后释放,可以检测告警,有橡胶套保护,这也是体现设计细节的。

170718zwiod8qtjdxwv00g

拆开这10个螺钉,揭开面板和橡胶套,橡胶套和面板都是一体的,面板还挺厚的,质量不错,一体冲压,说明厂家是没有在必要出省成本的。

170719vabb1zpjynwwcwpa

170719qaaxumm9b3914dwd

里面来个特写

170719iz74co579876p74t

来看机械锁细节部分,通过传动结构传递到锁芯

170719w2acwgggmycdzgcc

170719wlqmn1sa3a3zbnmi

Type-c临时供电部分,USB小板有个螺钉固定,有一个小的定位孔,涂了胶水防松动,可以看到后面所有PCB板和接插件都涂胶防松和防护,这也是不该省的都是没省的,质量还是不错的。

红色圈部分有定位槽防止板子晃动,细节满满。USB口晃动是导致USB口松动的最大原因,这个设计好评,相信很多人都有小家电由于碰到USB口线导致晃动,USB口松动的问题。

170719jpffgks7ho2hqzkz

170720x3nfprm66az616xn

170720puqip7opr4gjpsso

这个按钮的小板子,结构也是设计的刚刚好,该有的卡扣,定位都有,比较精细,整个看下来所以的结构设计都是比较精细的,不是那种随便卡卡就行的设计。

170720mehw3w1n3pituroo

拆开这六个圆头螺钉,还有旁边的线束螺钉。就可以分开。

170720wwi3gui2k1ziuorj

170720dmzxzxhsrh6q11tq

继续拆开这10个螺钉

170720nuououuya2r8k7l2

打开面板,里面是前面板的心脏,好好来研究下这部分的方案

170721gbjzhz4kbcbahaze

喇叭部分,这里还专门PCB板和金属壳体接地了。

170721lhhz4hjodct4djlk

170721ygne93nwg17nni1z

2.2.1.2 IR人体检测和TOF

下图上面两个小孔是TOF传感器,大的球是人体红外检测传感器。

170722aj00fjo8f9k3f11q

下面的板子是红外人体检测,上面的排线是TOF接到主板,排线是织布的,可以承受一定拉扯,这也是细节的考虑。

170722h9v33bmbvybpycrs

拧开螺钉看到背面

170722cgg94twwte0e6t6z

170722gukw9skutw8e2tsy

170722vjwgk5gwtsvqegsf

用一字螺丝刀,撬开tof部分的盖板

170722bbvpz9pyy1byzvbr

170723rm7zwpmthmi7vifh

Tof板子贴合的比较紧,需要小心点

170723d80o00izh0tr000n

用了好大劲用镊子才撬开,原来后面涂了胶,可以看到传感器。

170723kqxpt1qy7317tt15

170723b3wuvldmbwmaab9b

170723f8izcc01iwukkqfq

2.2.1.3 人脸识别和猫眼模块

对应的主控板,喇叭,和喇叭腔体

170723lypynnnflrgw2n2z

170724dky2mysmyk4o53zv

来看猫眼摄像头部分,金属底板用于散热,有橡胶保护套,可见该有的都有。

170724l3dzudfuhv683dd7

170724qbo7h45a6l7ccroe

170724tya84hy0oxqk2zlo

主板堆叠式设计,先拆开主板,下面是双摄人脸识别模块

170725w8ejk661kzb6knj8

170725kcc9y8e555hto555

继续拆开摄像头部分

170730gnxxp2jyf22qqb9e

继续拆开摄像头,注意这里螺丝刀旁边的是把手触摸按键PAD。

170730wdtdzt4ddtptdqtb

170730celjwvmo6tk6gebb

170731gj4bv6b809rj9d91

触摸按键和PAD

170731keh47hmju4spp724

170731sl7kgkgukskms48g

拆开双摄人脸识别模块

170732fev48840e78004vn

170732s6bm66nggqynj6px 170732qn1sfeggezfqtlsq

主要IC特写

FLASH用的GD的25Q128,16MB.旁边有串口调试接口。

170733mylhy4h44hpl7jn7

SOC为WQ5007,WQ在智能门锁等AI市场领域IC占有比较大,是不错的选择。

170733hwkty2xzwt5wdwen

这个8PIn IC看不清型号

170733t2cznfevbqxvff1f

再来看猫眼主控,可以看到后面板的SMA天线最终接到了这里,即猫眼的WIFI天线,接到了后面板室内。

170733mlds19ixly1gdyc7

170733ef5jo1tvbbjsyvjd

蜂鸣器,喇叭,和面板的接线

170733nc8pok8knkm5811z

主控是君正的SOCT21,WLAN是XR871,FLASH是FM25Q128

170734uv4mv00bmm0tfjbc

音频功放LN4891,旁边是三星的EMMC5.1 KLM4G1 4GB

170734azib1mcv4m8m5r8t

旁边一个丝印xmc的,还有个小的ic看不到型号。

170734cum2x2hmou2ka96f

2.2.1.4 指纹模块

指纹模块,拧开两个螺钉,可以看到指纹模块小板

170735z4985ty54a5hgc9a

170735lg9e1119sooyoooc

170735c6tneegnrle1dlbe

2.2.1.5 前面板主控(含刷卡)

拧开螺钉

170736eqygao00qe4ygaea

看到按键,可以看到NFC线圈和按键是做在一起的。橡胶垫用于触摸按键背光之间的光隔离。

170736vyzcflcu0olwz07w

170736mkv3pdzkvkgnpqmv

主控板的接口如下

170736j7d3a13h5d2me2zi

主控板背面都是背光灯

170736vdnbjkckg9guxymr

主控用的赛普拉斯的cy8c6245azi-s3d42

170736jdggxknz79be3ocr

170737lo9pa4696lp9xabn

提示音喇叭驱动用的PWM经过电机驱动LN8503做功放,这是一个降成本的方案,在低音质要求场景使用PWM+电机驱动,代替DAC+PA功放降低成本,这里应该是用来播放提示音。

170737a5bgfd9pctz9nr5i

这里2+1共3片74HC595做IO扩展,毕竟背光灯,按键这些需要比较多的IO。

170737dvmqzzq7ngeghzgt

这里丝印P8的应该是P-MOS,猜测是控制闪光灯或者电机之类的,作为大电流开关。

170738ovdpr3svxfledddx

FM17580是复旦微的NFC控制芯片,话说复旦微在NFC领域的控制芯片用的还不少。

170738eul3vpltz93pabwa

Flash是cfeon qh64a 104hip

170738l66yefs0ajzei6ae

2.2.1.6前面板方案总结

将各模块主要IC个方案厂家等信息列出如下:

主要IC

模块

IR人体检测

运放 TL8544

PIR-RE200BE-V1.1

TOF

S0Y0-B 2020-06-24

人脸识别+猫眼

人脸识别:双摄像头带红外补光

FLASH:GD25Q128

SOC:WQ5007

SEV2_MR_V2.020210430

Sense Time

猫眼摄像头

猫眼主控

SOC:君正T21

WLAN:XR871

FLASH:FM25Q128

音频功放LN4891

VLM-04-V4.1

指纹

HL-D

控制板

MCU:cy8c6245azi-s3d42

提示音:LN8503电机驱动+PWM

NFC:FM17850

FLASH:cfeon qh64a- 104hip

M30J0 C 20210929

2.2.2 后面板

再来看后面板

170739rzlsvkzyvpcvszny

2.2.2.1 电池接口

先看电池安装部分

电池转接板

170739nuycsw6yr883cm47

按箭头方向拉出后屏

170739ei6ujel40hiz00jy

这里就是电池仓,圈出部分就是电池接口

170739qk5d3vet535jq3k5

电池安装后如下,电池突出的地方方便扣出电池。

170739nqiurl4qa204r0za

2.2.2.2 控制板

排线接把手,4p接CLOSE和OPEN按键,2p接喇叭

170739xokeogdfa6veegkv

背面是后屏接口

170740q2rt13g4thgus4r5

如图小孔是恢复密码按键,按两次恢复初始密码,密码默认是12345678,按提示设置密码.

这里有点小建议,有时候要找个小的细小的东西去戳还不方便,如果直接能开孔露出按键是不是好点,毕竟这里是电池舱内,不是外露的,也无需考虑美观。

170740kz47am5pzi4r7ppr

这两个接口接电机和前面板

170740p1q13sr9zbf3xy8z

Mcu为复旦微的Fm15l023,前面板还用了一颗复旦微的NFC控制芯片,看来复旦微占据了两席之地。

170740gatujnuas2i56u6f

背面

170740oe4czpkilvyem4iv

OPEN CLOSE按键小板

170741gg1e01rvbbr3960n

170742wpfp8mza900r07fw

手动开锁部分

170742w5a1x55ne1xneqgb

170742pcrt9f1gd1ub9zku

走线细节,卡扣束缚走线,这里排线是布织的,因为要接到把手里面,需要承受一定拉力。

这里的线束卡片也是用的金属片,这里有个建议,就是卡片最好有限位,因为只有一个螺钉固定,打螺钉时卡片会转动,当然转动会碰到金属壳,转动一点也没什么大关系。

170742loglwnakynzobkfn

拧开按键,和这几个螺钉,打开喇叭腔

170742v5xywh55prhy98rz

170743ls3ikz2ecv21rhjr 170743tc3zcrhmy4l3mf7l

2.2.2.3后屏

后屏是后面板的重要部分,屏幕显示猫眼观察到的实时视频。视频通过前面板的猫眼控制模块的USB线传过来。

有扫码配网提示,直接拧开螺钉

170743v595cpdknopj5c25

接口部分

170743mwwfzlc8n9fbakmz

看到屏幕,控制板,屏幕是一体的。

170744tbgzitkrrt167rbk

看到控制板,触摸接口和屏幕接口

170744nmumoqf75fqlgogq

拆开排线

170744mcs6wr7w08gvv1rf

看到主控型号,是anyka安凯微电子的KY3179EE128

170745gqyjp9fygos9gys9

FLASH是XMC的25qh32,4MB。

170745mpp4hhkzjt8pgkgu

2.2.2.4手扫识别

排线接到手扫模块,另外一根是天线:前面板过来的猫眼模块的WIFI天线。

170745t5wseiuwwqaddcsw

170745xov4zcjuduwmb4vn

拧开四个内六角

170745noez20f555pn2zm0

继续拧开螺钉

170745v2oza9ghfq8pbo2g

这里橡胶下面还有两个螺钉,橡胶为了挡住螺钉美观,细节不错。

170746fozfjbcwowek7o1n

看到前面板引入的wifi天线最终引入到了后面板室内

170746nk3uo63uzuz6gzep

再看控制板,sma接前面的触摸感应,前面是触摸感应区

170746e8q6f092xba6v9hx

170746mvxi3qqqijwa3iiv

非接触距离感应传感器,实现手扫识别。

170746w4t7o0xt7tongftg

8p  ic应该是一个专用的支持触摸的mcu,丝印打磨掉了看不到型号

170746vgepg8qcuklneq5i

2.2.2.5后面板方案总结

将各模块主要IC个方案厂家等信息列出如下:

主要IC

模块

后屏

TFT

FPC-ZY39705

2022.04.17HX

SOC:ANYKA KY3179EE128

FLASH:25QH32

VP-V1.1

控制板

mcu:Fm15l023

R5H60-A  2022.1.12

电池模块(接口)

P050-A 2021-04-26

手扫识别

触摸MCU

距离传感器

S0Q0-E 2021-11-04

2.2.3方案分析总结

前面进行了详细的拆解,把每一个模块的每一部分细节都暴漏了出来,也把各个模块的PCB主要的IC进行了记录。现在就基于此画出系统框图,可以形象的看到整个方案。

2.2.3.1系统框图

根据前面的拆解,画出如下系统框图

170747lc04b9z9haich7zi

2.2.3.2方案总结

整个方案分为前后面板部分:

前面板部分:

1)前面板主要包含人脸识别模块,猫眼模块,控制模块。人脸识别模块直接接到控制板,控制板还接了指纹模块,触摸,USB供电,拆解检测,PWM驱动的提示音播放喇叭,按键和NFC线圈,TOF模块。

前控制板和猫眼模块通过10P线交互,和后控制板通过2x10P线交互。

猫眼模块接了对讲喇叭,蜂鸣器,IR人体检测(检测到人唤醒),和一颗摄像头。猫眼的WIFI天线走到了后面板,因为要使用室内的WIFI信号。猫眼通过4P的USB走传到后屏显示(猜测是UVC协议),这也是我们玩转NES游戏要”劫持”这一部分,实现游戏。

2)设计中考虑了成本,比如提示音喇叭播放,使用PWM方案+便宜的电机驱动芯片做功放,代替DAC+PA的方案,成本较低。所以成本是设计出来的,不是省料省出来的。

3)几个大的模块的方案:人脸识别用的WQ的5007的方案,猫眼用的君正T21方案,主控MCU用的赛普拉斯cy8c6245azi-s3d42

4)前面板共8块PCB板,图中带颜色的框。

后面板部分:

后面板主要包括,后屏,电池,控制板部分。

后屏用的安凯微电子的KY3179EE128方案,控制板用的复旦微的FM15L023,前控制板的NFC也是用的复旦微的FM17850。

后面板共5块PCB板,图中带颜色的框。

最后总结下:

整个锁的系统从外观,结构设计可以看出是不错的,包括金属拉丝,塑壳的磨砂设计,甚至前后差异化的风格设计都体现了厂家是下了功夫的,整体感觉还是属于高端产品的。

走高端路线,但是也要进行成本设计,该省的省,不该省的不省,通过设计省成本,而不是省料省成本。比如PWM驱动喇叭的设计方案就是典型的设计节省成本,值得借鉴。

前后面板主要模块使用了成熟的厂商方案,比如WQ的人脸识别方案,君正的猫眼方案,一套锁说简单也简单,说不简单也不简单,毕竟也有这么多模块和系统,所以依赖成熟的产业才能做到高效低成本,一家完成所有不太可能。整个拆解来看还是非常不错的,也有很多值得借鉴学习的地方。

APP体验

产品的用户体验也是重要的一环,所以这里也来体验下APP的使用,尤其是添加设备配网这一环。先扫描电池上的二维码进入小程序

170747fu67cbhqckk2cx8q

然后用小程序扫码后屏的二维码(实际内容如下)

170747gc6ybjqqyzs9s93r

登录小程序,手机号快捷登录,

170747rf009qmnl9l1uqzo

170747xq7mt8ywy7mtx778

170747boccuzcskcm7z8le

添加设备,扫码添加,扫码后屏的二维码

170748y1rvikk9v04vr33o

170748udtuz2utt3y2hty4

我这里已经激活

170748jhwquntnmcnqhnd8

输入wifi账号和密码

170748iftvtfyhl5pgyxrf

按提示进入门锁菜单

170748ileleuuevrvzl0fl

按提示进入配网设置,勾选,点击下一步

将二维码对准猫眼摄像头

170748p5u6gg9ooxu4x65v

只支持2.4G不支持5G。

可以看到整个配网添加设备的过程还是比较简单,体验还是不错的。

这里总结下,使用猫眼的摄像头扫码连接路由器WIFI,这里利用了摄像头进行输入,简化了入网流程。一般其他家用电器都是设备先打开热点,手机连接热点进行通讯,将wifi密码和账户告诉设备,设备再断开热点重新连接路由器wifi,也就是需要两步。这里使用摄像头直接扫码获取路由器wifi信息只需要一步入网,更简单。

改造-玩转NES游戏

4.1 方案设计

我们从前面的方案分析,精简出如下的数据流,原来视频流有两条,一条是摄像头到猫眼模块通过WIFI到云端到手机,即可视对讲的路径,一条是摄像头到猫眼模块通过USB到后屏,TFT液晶屏显示,即本地猫眼可视的路径。

我们只需要替代摄像头,输入视频流就可以实现上述视频流,这样就可以在本地TFT屏幕和远程手机显示游戏界面。

170749ivnj4x7s3vh777n6

当然要用门锁玩游戏,最直接的方式是基于猫眼模块的主控这里是君正的T21开发NES游戏模拟器,但是这是不现实的因为没有SDK,没有原来的业务源码,我们退而求其次,”劫持”视频流,输入我们的视频流,这样基于我们自己手里的控制板开发NES游戏模拟器。

而现成的USB充电接口是对外的,我们就可以用此USB接口跳线到后屏USB接口实现游戏视频流接入,并增加切换开关,可以切换使用原数据流还是USB口引入的数据流,这样可以切换,不影响原来的功能,如下所示(注:图中条线应该是跳线)

170749utscet567nk4bv59

所以我们需要做的就是模拟猫眼的USB数据,即模拟猫眼同样的USB设备,将我们的NES游戏模拟器的视频帧数据转为同样的格式,后面我们抓包可以看到是MJPEG格式的UVC视频流,还有UAC的音频流。

4.2分析猫眼和后屏UVC+UAC传输协议

前面我们拆解分析,知道了猫眼和后屏是通过4P USB线进行传输,将猫眼采集的图像在后屏显示的,可以猜测是走的UVC协议,我们就测试分析下。

先找到线束,制作转接线监控传输过程。

  • 先将猫眼转接到电脑,抓取枚举和传输数据等信息。
  • 然后制作转接线,使用USB分析仪,监控猫眼和后屏的通讯过程。
  • 最后将线引出到临时USB充电口,按照前面的设计,设置切换开关要么切换到猫眼要么切换到USB口,用自己的设备接到USB口,替代猫眼摄像头,显示任意内容在后屏,玩转NES游戏。

4.2.1 抓包猫眼的枚举和通讯信息

猫眼是如下4P usb接口

170749pvcjzcf44jw135j7

制作USB转接线接电脑,抓取到描述符和通讯过程,分析其通讯。

170749k2ym02t22ghiqwyg

过程略,这里画出整个描述符拓扑结构。可以看出使用了UVC+UAC的复合设备,UVC使用MJPEG格式,所以我们也需要按照猫眼同样的方式实现UVC+UAC的设备。

具体实现参见本人分享的文章https://mp.weixin.qq.com/s/_5quPTyLQ-_T0D7MfMTqCw《USB系列之-UVC+UAC扬声器+麦克风实例分享》,这里不再赘述。

170749jlrrn77y71tr0ya4

4.3手柄设计

为了方便玩转NES游戏,我们先参考常见的NES游戏机手柄,自行设计一个手柄。一般手柄有上下左右,select,start,a,b共8个按键。直接使用8个IO或者2x4矩阵按键也可以但是浪费IO,所以这里使用IIC接口的PCF8574T扩展8路IO,并且IIC地址可配,可以接多片支持双手柄。

4.3.1硬件设计

硬件使用嘉立创EDA在线版设计,项目已经开源

https://oshwhub.com/qinyunti/key

170749qzbli2zr2bbkpvjc

4.3.1.1原理图

原理图如下比较简单,

170750dgss82sgpoamw1aa

4.3.1.2 PCB

PCB如下,设计为10cm以内,这样打样比较便宜。

170750jutozyyl33ez3ap3

4.3.1.3 BOM

BOM成本3块钱左右

170750b2bzz9l6tcb7jj2l

4.3.1.4 PCB打样

20块钱搞定包邮,速度也很快几天就回来了

170750m44id047a4b40aab

4.3.1.5 焊接

很快样板回来了,做的紫色的,手工焊接下。

170751r68xulzgewe7fics

170751ippy4d1ggi5jgh15

170751o5gggzw997uptu9u

焊接好后如下

170751ww4m6oen4xj9rf2e

4.3.2驱动

4.3.2.1设备地址

我这里是PCF8574T不带A的型号,设备地址如下,我这A2-A1-A0都接地,所以读地址是0x41,写地址是0x40.

170751idodochct7cn8o77

4.3.2.2输出模式

发送STOP-> 注意这里根据测试上电后第一次总是start无ack,所以这里加个stop回到默认状态。

发送START->

发送0x40写地址->

设备回ACK->

写8位数据->

设备回ACK->

写8位数据->

设备回ACK->

...

发送STOP

170752pjfp6ojjs062ahdo

4.3.2.3输入模式

输入模式必须保证对应的端口先输出的是1(复位默认状态).

发送STOP-> 注意这里根据测试上电后第一次总是start无ack,所以这里加个stop回到默认状态。

发送START->

发送0x41读地址->

设备回ACK->

读8位数据->

主机回ACK->

读8位数据->

主机回ACK->

...

主机回NACK

发送STOP

170752u4kzhhne0j4tuxue

4.3.2.4驱动代码

这里使用之前实现的IO模拟IIC的实现

超级精简系列之三:超级精简的IO模拟IIC的C实现

https://mp.weixin.qq.com/s/ESzWWqxHpQevsWfjV0s2VQ

io_iic.c

#include "io_iic.h"

/**

*                     _______________________   

*    SCL ____________|                       |__

*        ————————————————————————

*    SDA                         |______________

*        (1)      (2)        (4)         (6)

*                        (3)           (5)

*    其中(3) SDA低建立时间 (5) SDA高保持时间

*    (1) 拉高SDA  (4)拉高SDA产生上升沿

*    (2) SCL拉高 SCL高时SDA上升沿即停止信号

*/

void io_iic_start(io_iic_dev_st* dev)

{

    /* SCL高时,SDA下降沿 */

    dev->sda_write(1);               /* (1) SDA拉高以便后面产生下降沿  */

    dev->scl_write(1);               /* (2) 拉高SCL                   */

    if(dev->delay_pf != 0)           /* (3) SCL高保持*/

    {

        dev->delay_pf(dev->delayus);

    }

    dev->sda_write(0);              /* (4)SCL高时SDA下降沿 启动    */

    if(dev->delay_pf != 0)          /* (5)SCL高保持               */

    {

        dev->delay_pf(dev->delayus);

    }

    dev->scl_write(0);             /* (6)SCL恢复                 */

}

/**

*                     _______________________   

*    SCL ____________|                       |____

*                                 ————————————————

*    SDA ————————————————————————|

*        (1)      (2)        (4)         (6)

*                        (3)           (5)

*    其中(3) SDA低建立时间 (5) SDA高保持时间

*    (1) 拉低SDA  (4)拉高SDA产生上升沿

*    (2) SCL拉高 SCL高时SDA上升沿即停止信号

*/

void io_iic_stop(io_iic_dev_st* dev)

{

    /* SCL高时,SDA上升沿 */

    dev->sda_write(0);               /* (1)SDA先输出低以便产生上升沿 */

    dev->scl_write(1);               /* (2)SCL高                   */

    if(dev->delay_pf != 0)           /* (3)SCL高保持               */

    {

        dev->delay_pf(dev->delayus);

    }

    dev->sda_write(1);               /* (4)SCL高时SDA上升沿 停止    */

    if(dev->delay_pf != 0)           /* (5)SCL高保持               */

    {

        dev->delay_pf(dev->delayus);

    }

    dev->scl_write(0);               /* (6)SCL恢复                 */

}

/**

*   |       B0               | B1~B6|    B7               |           NACK/ACK      |

*                 ___________      _            __________              ____________

*    ____________|           |   x  |__________|          |____________|            |

* (1)[2]      (4)                                      (6)[7]        (9)[10]       (12)

*        (3)           (5)                                     (8)           (11)

* 其中(1)(6)(12)拉低SCL;(4)(9)拉高SCL;

* [2]输出  [7]转为读 [10]读ACK;

* (3)(8)低保持时间,(5)(11)高保持时间。

*/

int io_iic_write(io_iic_dev_st* dev, uint8_t val)

{

    uint8_t tmp = val;

    uint8_t ack = 0;

    if(dev == 0)

    {

        return -1;

    }

    if((dev->scl_write == 0) || (dev->sda_write == 0) || (dev->sda_read == 0) || (dev->sda_2read == 0))

    {

        return -1;

    }

    /* SCL下降沿后准备数据,对方上升沿采集数据,高位在前 */

    for(uint8_t i=0; i<8; i++)

    {

        dev->scl_write(0);               /* (1) SCL拉低以便修改数据    */

        if((tmp & 0x80) != 0)            /* [2] 准备SDA数据            */

        {

            dev->sda_write(1);

        }

        else

        {

            dev->sda_write(0);  

        }

        if(dev->delay_pf != 0)

        {

            dev->delay_pf(dev->delayus); /* (3) SCL拉低时间即数据建立时间 */

        }

        dev->scl_write(1);                /*(4) SCL上升沿对方采样        */

        if(dev->delay_pf != 0)

        {

            dev->delay_pf(dev->delayus); /* (5) SCL高保持时间,数据保持时间 */

        }

        tmp <<= 1;                       /* 处理下一位           */

    }

    dev->scl_write(0);                   /* (6)SCL归0  完成8个CLK */

    dev->sda_2read();                    /* [7]SDA转为读          */

    if(dev->delay_pf != 0)

    {

        dev->delay_pf(dev->delayus);     /* (8)第九个时钟拉低时间  */

    }

    dev->scl_write(1);                   /* (9)SCL上升沿          */

    ack = dev->sda_read();               /* [10]上升沿后读         */

    if(dev->delay_pf != 0)

    {

        dev->delay_pf(dev->delayus);     /* (11)第九个时钟高保持    */

    }

    dev->scl_write(0);                   /* (12)恢复SCL到低        */

    return (ack==0) ? 0 : -1;

}

/**

*   |       B0               | B1~B6|    B7               |           NACK/ACK      |

*                 ___________      _            __________              ____________

*    ____________|           |   x  |__________|          |____________|            |

* (1)[2]      (4)[5]                                    (7)[8]       (10)         (12)

*        (3)           (6)                                     (9)           (11)

* 其中(1)(7)(12)拉低SCL;(4)(10)拉高SCL;

* [2]转为读  [5]读 [8]输出ACK;

* (3)(9)低保持时间,(6)(11)高保持时间。

*/

int io_iic_read(io_iic_dev_st* dev, uint8_t* val, uint8_t ack)

{

    uint8_t tmp = 0;

    if((dev == 0) || (val == 0))

    {

        return -1;

    }

    if((dev->scl_write == 0) || (dev->sda_write == 0) || (dev->sda_read == 0) || (dev->sda_2read == 0))

    {

        return -1;

    }

    /* SCL下降沿后对方准备数据,上升沿读数据,高位在前 */

    for(uint8_t i=0; i<8; i++)

    {

        tmp <<= 1;                      /* 处理下一位,先移动后读取             */

        dev->scl_write(0);              /* (1)                               */

        dev->sda_2read();               /* [2]转为读输入高阻,以便对方能输出     */

        if(dev->delay_pf != 0)           

        {

            dev->delay_pf(dev->delayus);/* (3)SCL低保持时间                    */

        }

        dev->scl_write(1);              /* (4)SCL上升沿                        */

        if(dev->sda_read())             /* (5)读数据(SCL低时对方已经准备好数据)  */

        {

            tmp |= 0x01;                /* 高位在前,最后左移到高位              */

        }      

        if(dev->delay_pf != 0)         

        {

            dev->delay_pf(dev->delayus);/* (6)SCL高保持时间                     */

        }

    }

    dev->scl_write(0);                  /* (7)恢复SCL时钟为低                   */

    dev->sda_write(ack);                /* [8]准备ACK信号(SCL低才能更行SDL)      */

    if(dev->delay_pf != 0)

    {

        dev->delay_pf(dev->delayus);    /* (9)第九个SCL拉低时间                  */

    }

    dev->scl_write(1);                  /* (10)SCL上升沿发数据触发对方读          */

    if(dev->delay_pf != 0)

    {

        dev->delay_pf(dev->delayus);    /* (11)第九个SCL拉高保持时间              */

    }

    dev->scl_write(0);                  /* (12)第九个SCL完成恢复低                */

    //dev->sda_write(1);                /* 这里无需驱动SDA,后面可能还是读),需要发送时再驱动 */

    *val = tmp;

    return 0;

}

void io_iic_init(io_iic_dev_st* dev)

{

    if((dev != 0) && (dev->init != 0))

    {

        dev->init();

    }

}

void io_iic_deinit(io_iic_dev_st* dev)

{

    if((dev != 0) && (dev->deinit != 0))

    {

        dev->deinit();

    }

}

io_iic.h

#ifndef IO_IIC_H

#define IO_IIC_H

#ifdef __cplusplus

    extern "C"{

#endif

#include <stdint.h>

typedef void    (*io_iic_scl_write_pf)(uint8_t val);   /**< SCL写接口     */

typedef void    (*io_iic_sda_write_pf)(uint8_t val);   /**< SDA写接口     */

typedef void    (*io_iic_sda_2read_pf)(void);          /**< SDA转为读接口 */

typedef uint8_t (*io_iic_sda_read_pf)(void);           /**< SDA读接口     */

typedef void    (*io_iic_delay_us_pf)(uint32_t delay); /**< 延时接口      */

typedef void    (*io_iic_init_pf)(void);               /**< 初始化接口     */

typedef void    (*io_iic_deinit_pf)(void);             /**< 解除初始化接口 */

/**

* \struct io_iic_dev_st

* 接口结构体

*/

typedef struct

{

    io_iic_scl_write_pf scl_write;   /**< scl写接口    */

    io_iic_sda_write_pf sda_write;   /**< sda写接口    */

    io_iic_sda_2read_pf sda_2read;   /**< sda转为读接口 */

    io_iic_sda_read_pf  sda_read;    /**< sda读接口     */

    io_iic_delay_us_pf  delay_pf;    /**< 延时接口      */

    io_iic_init_pf      init;        /**< 初始化接口    */

    io_iic_deinit_pf    deinit;      /**< 解除初始化接口 */

    uint32_t            delayus;     /**< 延迟时间      */

} io_iic_dev_st;

/**

* \fn io_iic_start

* 发送启动信号

* \param[in] dev \ref io_iic_dev_st

*/

void io_iic_start(io_iic_dev_st* dev);

/**

* \fn io_iic_stop

* 发送停止信号

* \param[in] dev \ref io_iic_dev_st

*/

void io_iic_stop(io_iic_dev_st* dev);

/**

* \fn io_iic_write

* 写一个字节

* \param[in] dev \ref io_iic_dev_st

* \param[in] val 待写入的值

* \retval 0 写成功(收到了ACK)

* \retval -2 写失败(未收到ACK)

* \retval -1 参数错误

*/

int io_iic_write(io_iic_dev_st* dev, uint8_t val);

/**

* \fn io_iic_read

* 读一个字节

* \param[in] dev \ref io_iic_dev_st

* \param[out] val 存储读到的值

* \param[in] ack 1发送NACK 0发送ACK

* \retval 0 读成功

* \retval -1 参数错误

*/

int io_iic_read(io_iic_dev_st* dev, uint8_t* val, uint8_t ack);

/**

* \fn io_iic_init

* 初始化

* \param[in] dev \ref io_iic_dev_st

*/

void io_iic_init(io_iic_dev_st* dev);

/**

* \fn io_iic_deinit

* 解除初始化

* \param[in] dev \ref io_iic_dev_st

*/

void io_iic_deinit(io_iic_dev_st* dev);

#ifdef __cplusplus

    }

#endif

#endif

pcf8574.c

#include "pcf8574.h"

#define PCF8574_WR_ADDR 0x40  /**< 写地址 */

#define PCF8574_RD_ADDR 0x41  /**< 读地址 */

/**

* \fn pcf8574_read

* 读数据

* \param[in] dev \ref pcf8574_dev_st

* \param[in] val 存读出的值

* \retval 0  成功

* \retval <0 失败

*/

int pcf8574_read(pcf8574_dev_st* dev, uint8_t* val)

{

    /* 启动 */

    dev->stop(); /* 如果直接start 上电后第一次总是无ACK,所以先stop进入确定状态 */

    dev->start();

    /* 发送读地址 */

    if(0 != dev->write(PCF8574_RD_ADDR | ((dev->addr & 0x07)<<1)))

    {

        dev->stop();

        return -1;

    }

    /* 读数据 */

    if(0 != dev->read(val,0))

    {

        dev->stop();

        return -2;  

    }

    /* 结束 */

    dev->stop();

    return 0;

}

/**

* \fn pcf8574_write

* 写数据

* \param[in] dev \ref pcf8574_dev_st

* \param[in] val 待写入的值

* \retval 0  成功

* \retval <0 失败

*/

int pcf8574_write(pcf8574_dev_st* dev, uint8_t val)

{

    /* 启动 */

    dev->stop();  /* 如果直接start 上电后第一次总是无ACK,所以先stop进入确定状态 */

    dev->start();

    /* 发送写地址 */

    if(0 != dev->write(PCF8574_WR_ADDR | ((dev->addr & 0x07)<<1)))

    {

        dev->stop();

        return -1;

    }

    /* 写数据 */

    if(0 != dev->write(val))

    {

        dev->stop();

        return -2;

    }


    /* 结束 */

    dev->stop();

    return 0;

}

/**

* \fn pcf8574_init

* 初始化

* \param[in] dev \ref pcf8574_dev_st

*/

void pcf8574_init(pcf8574_dev_st* dev)

{

    dev->init();

}

/**

* \fn pcf8574_deinit

* 解除初始化

* \param[in] dev \ref pcf8574_dev_st

*/

void pcf8574_deinit(pcf8574_dev_st* dev)

{

    dev->deinit();

}

pcf8574.h

#ifndef PCF8574_H

#define PCF8574_H

#ifdef __cplusplus

    extern "C"{

#endif

#include <stdint.h>

typedef void    (*pcf8574_iic_start_pf)(void);                          /**< IIC启动接口 */

typedef void    (*pcf8574_iic_stop_pf)(void);                           /**< IIC停止接口 */

typedef int     (*pcf8574_iic_read_pf)(uint8_t* val, uint8_t ack);      /**< IIC读接口   */

typedef int     (*pcf8574_iic_write_pf)(uint8_t val);                   /**< IIC写接口   */

typedef void    (*pcf8574_iic_init_pf)(void);                           /**< 初始化接口 */

typedef void    (*pcf8574_iic_deinit_pf)(void);                         /**< 解除初始化接口 */

typedef void    (*pcf8574_iic_delay_us_pf)(uint32_t delay); /**< 延时接口      */

/**

* \struct pcf8574_dev_st

* 接口结构体

*/

typedef struct

{

    pcf8574_iic_start_pf  start;   /**< IIC启动接口 */

    pcf8574_iic_stop_pf   stop;    /**< IIC停止接口 */

    pcf8574_iic_read_pf   read;    /**< IIC读接口   */

    pcf8574_iic_write_pf  write;   /**< IIC写接口   */

    pcf8574_iic_init_pf   init;    /**< 初始化接口  */

    pcf8574_iic_deinit_pf deinit;  /**< 解除初始化接口  */

    uint8_t              addr;     /**< 3位硬件地址    */

} pcf8574_dev_st;

/**

* \fn pcf8574_read

* 读数据

* \param[in] dev \ref pcf8574_dev_st

* \param[in] val 存读出的值

* \retval 0  成功

* \retval <0 失败

*/

int pcf8574_read(pcf8574_dev_st* dev, uint8_t* val);

/**

* \fn pcf8574_write

* 写数据

* \param[in] dev \ref pcf8574_dev_st

* \param[in] val 待写入的值

* \retval 0  成功

* \retval <0 失败

*/

int pcf8574_write(pcf8574_dev_st* dev, uint8_t val);

/**

* \fn pcf8574_init

* 初始化

* \param[in] dev \ref pcf8574_dev_st

*/

void pcf8574_init(pcf8574_dev_st* dev);

/**

* \fn pcf8574_deinit

* 解除初始化

* \param[in] dev \ref pcf8574_dev_st

*/

void pcf8574_deinit(pcf8574_dev_st* dev);

#ifdef __cplusplus

    }

#endif

#endif

4.3.3测试

170752mn85neq52nnz2ob4

根据平台移植IO操作

Key.c

#include "io_iic.h"

#include "pcf8574.h"

#include "key.h"

#include "gpio.h"

/* IIC IO操作的移植 */

static void io_iic_scl_write_port(uint8_t val)

{

    if(val)

    {

        gpio_write(GPIO_09, 1);

    }

    else

    {

        gpio_write(GPIO_09, 0);

    }

}

static void io_iic_sda_write_port(uint8_t val)

{

    gpio_close(GPIO_07);

    gpio_open(GPIO_07, GPIO_DIRECTION_OUTPUT);

    if(val)

    {

        gpio_write(GPIO_07, 1);

    }

    else

    {

        gpio_write(GPIO_07, 0);

    }

}

static void io_iic_sda_2read_port(void)

{

    gpio_write(GPIO_07, 1);

    gpio_close(GPIO_07);

    gpio_open(GPIO_07, GPIO_DIRECTION_INPUT);

}

static uint8_t io_iic_sda_read_port(void)

{

    if(0 == gpio_read(GPIO_07))

    {

        return 0;

    }

    else

    {

        return 1;

    }

}

static void io_iic_delay_us_port(uint32_t delay)

{

    uint32_t volatile t=delay;

    while(t--);

}

static void io_iic_init_port(void)

{

    gpio_open(GPIO_07, GPIO_DIRECTION_OUTPUT);

    gpio_open(GPIO_09, GPIO_DIRECTION_OUTPUT);

    gpio_write(GPIO_07, 0);

    gpio_write(GPIO_09, 0);

}

static void io_iic_deinit_port(void)

{

    gpio_close(GPIO_07);

    gpio_close(GPIO_09);

}

static io_iic_dev_st iic_dev=

{

    .scl_write = io_iic_scl_write_port,

    .sda_write = io_iic_sda_write_port,

    .sda_2read = io_iic_sda_2read_port,

    .sda_read = io_iic_sda_read_port,

    .delay_pf = io_iic_delay_us_port,

    .init = io_iic_init_port,

    .deinit = io_iic_deinit_port,

    .delayus = 200,

};

/* pcf8574接口移植 */

static void pcf8574_iic_start_port(void)

{

    io_iic_start(&iic_dev);

}

static void pcf8574_iic_stop_port(void)

{

    io_iic_stop(&iic_dev);

}

static int pcf8574_iic_read_port(uint8_t* val, uint8_t ack)

{

    return io_iic_read(&iic_dev,val,ack);

}

static int pcf8574_iic_write_port(uint8_t val)

{

    return io_iic_write(&iic_dev,val);

}

static void pcf8574_iic_init_port(void)

{

    io_iic_init(&iic_dev);

}

static void pcf8574_iic_deinit_port(void)

{

    io_iic_deinit(&iic_dev);

}

pcf8574_dev_st pcf8574_dev=

{

     .start = pcf8574_iic_start_port,

     .stop = pcf8574_iic_stop_port,

     .read = pcf8574_iic_read_port,

     .write = pcf8574_iic_write_port,

     .init = pcf8574_iic_init_port,

     .deinit = pcf8574_iic_deinit_port,

     .addr = 0,

};

/* 对外接口 */

void key_init(void)

{

    pcf8574_init(&pcf8574_dev);

}

void key_deinit(void)

{

    pcf8574_deinit(&pcf8574_dev);

}

int key_write(uint8_t val)

{

    return pcf8574_write(&pcf8574_dev,val);

}

int key_read(uint8_t* val)

{

    return pcf8574_read(&pcf8574_dev,val);

}

Key.h

#ifndef KEY_H

#define KEY_H

#ifdef __cplusplus

    extern "C"{

#endif

#include <stdint.h>

void key_init(void);

void key_deinit(void);

int key_write(uint8_t val);

int key_read(uint8_t* val);

#ifdef __cplusplus

    }

#endif

#endif

测试代码

    key_init();

    os_delay(1000);

    while (1)

    {

        int res;

        uint8_t key_pre = 0;

        uint8_t key_now = 0;

        /* 初始状态 */

        if(0 == (res = key_read(&key_now)))

        {

            if(key_now != key_pre)

            {

                key_pre = key_now;

            }

        }

        else

        {

            printf("key%d,read err,res:%d\n",res);

        }

        for(int n=0; n<10; n++)

        {

            for(int i=0; i<8; i++)

            {

                if(0 != (res = key_write(1<<i)))

                {

                    printf("key%d,write err,res:%d\n",i,res);

                }

                os_delay(10);

            }

        }

        /* 读之前先转为全输出1 */

        key_write(0xFF);

        /* 初始状态 */

        if(0 == (res = key_read(&key_now)))

        {

            if(key_now != key_pre)

            {

                key_pre = key_now;

            }

        }

        else

        {

            printf("key%d,read err,res:%d\n",res);

        }

        /* 读状态,有改变则打印 */

        while(1)

        {

            if(0 == key_read(&key_now))

            {

                if(key_now != key_pre)

                {

                    key_pre = key_now;

                    printf("key change to %02x\n",key_now);

                }

            }

            else

            {

                printf("key%d,read err,res:%d\n",res);

            }

            os_delay(1);

        }

        /* code */

    }

示波器查看输出波形,然后按键查看按键识别测试获取状态正常。

170752a9ujzl9u4ibu3l42

4.4移植NES游戏模拟器

NES相关移植好的代码参考本人开源的仓库https://gitee.com/qinyunti/my-info-nes.git,基于InfoNES进行修改,如果使用需要实现自己的显示,音频输出,按键获取等接口。文件系统基于littlefs。通过串口shell导入rom文件进行运行(shell实现可以参见本人公众号文章https://mp.weixin.qq.com/s/XLmbJn0SKoDT1aLdxHDrbg 一个超级精简高可移植的shell命令行C实现)。

4.4.1系统框图

整个系统框图如下,主要有和PC通过串口shell实现文件传输,用于导入rom游戏文件,IIC接口获取我们自行设计的手柄按键,PDM/PWM驱动的本地喇叭播放声音。

UVC显示,UAC音频,通过USB导入到后屏显示或者手机APP上远程显示,声音播放。

170752p4rqjj18166jrk5c

4.4.2 UVC显示设计

视频部分的框架如下,前面拆解知道,智能门锁后屏支持USB接口的UVC,可以实时视频,

即上行

摄像头->猫眼模块->USB(UVC)->后屏

->WIFI->云端->手机

而我们游戏只需要播放视频即可,我们可以直接对接USB接口,将游戏视频通过UVC发送到后屏,通过云端到手机端播放,也可以直接本地通过后屏的TFT液晶播放。

170752znle92ahhrklnrng

相关的实现文章可以参考本文公众号系列文章

170753cf5ttjjzct0o9fnm

170753id3n88e6odxq1exj

由于后屏只支持MJPEG格式,所以需要将NES模拟器的RGB565格式转为MJPEG,而我们的MJEPG编码器只支持输入NV12格式,所以需要先将RGB565转为NV12然后编码为MJPEG通过UVC传输到后屏。由于NES模拟器显示大小是256x240像素,而后屏是1280x720,所以RGB565转NV12时同时进行放大处理,实现代码如下

void InfoNES_LoadFrame(void)

{

  framebuffer_sync((uint16_t*)WorkFrame, NES_DISP_WIDTH,NES_DISP_HEIGHT);

}

void framebuffer_sync(uint16_t * buffer, uint16_t x, uint16_t y)

{

    //memset(DDR_IN_BUFFER_ADDR+H_SIZE*V_SIZE,0x80,H_SIZE*V_SIZE/2);

    //uint8_t sx=H_SIZE/x;

    //uint8_t sy=V_SIZE/y;

    uint8_t sx = 5;

    uint8_t sy = 3;

    uint8_t* py = (uint8_t*)DDR_IN_BUFFER_ADDR;

    uint8_t* puv = (uint8_t*)(DDR_IN_BUFFER_ADDR+(uint32_t)H_SIZE*V_SIZE);

    uint8_t* p;

    uint32_t offset;

    uint8_t y00;

    uint8_t y01;

    uint8_t y10;

    uint8_t y11;

    uint8_t u00;

    uint8_t u01;

    uint8_t u10;

    uint8_t u11;

    uint8_t v00;

    uint8_t v01;

    uint8_t v10;

    uint8_t v11;

    for(int j=0;j<y;j+=2)

    {

        /* 一次处理2行 */

        for(int i=0;i<x;i+=2)

        {

            /* 一次处理两列 */

            /* 输入2x2 4个点 */

            offset = j*x+i;

            rgb565_2_yuv(buffer[offset], &y00, &u00, &v00);

            rgb565_2_yuv(buffer[offset+1], &y01, &u01, &v01);

            rgb565_2_yuv(buffer[offset+x], &y10, &u10, &v10);

            rgb565_2_yuv(buffer[offset+x+1], &y11, &u11, &v11);

            /* Y方向j放大sy倍 X方向i放大sx倍 */

            /* 第一行的2个Y */         

            for(int sy_j=0; sy_j<sy; sy_j++)  /* 放大sy倍 */

            {

                p = py+(j*sy)*H_SIZE+i*sx + sy_j*H_SIZE;    /* 第1行j */

                for(int sx_i=0; sx_i<sx; sx_i++)  /* 第1个点放大sx列 */

                {

                    *p++ = y00;

                }

                for(int sx_i=0; sx_i<sx; sx_i++) /* 第2个点放大sx列 */

                {

                    *p++ = y01;

                }

            }

            /* 第二行的2个Y */

            for(int sy_j=0; sy_j<sy; sy_j++)  /* 放大sy倍 */

            {

                p = py+((j+1)*sy)*H_SIZE+i*sx + sy_j*H_SIZE;    /* 第2行j+1*/

                //for(int sx_i=0; sx_i<sx; sx_i++)  /* 第1个点放大sx列 */

                //{

                //    *p++ = y10;

                //}

                *p++ = y10;

                *p++ = y10;

                *p++ = y10;

                *p++ = y10;

                *p++ = y10;

                //for(int sx_i=0; sx_i<sx; sx_i++) /* 第2个点放大sx列 */

                //{

                //    *p++ = y11;

                //}

                *p++ = y11;

                *p++ = y11;

                *p++ = y11;

                *p++ = y11;

                *p++ = y11;

            }

            /* 2x2个点得到一个uv */

            uint8_t u = ((uint16_t)u00+(uint16_t)u01+(uint16_t)u10+(uint16_t)u11)/4;

            uint8_t v = ((uint16_t)v00+(uint16_t)v01+(uint16_t)v10+(uint16_t)v11)/4;

            for(int sy_j=0; sy_j<sy; sy_j++)  /* 放大sy倍行 */

            {

                p = puv+((j/2)*sy)*H_SIZE + i*sx + sy_j*H_SIZE;

                //for(int sx_i=0; sx_i<sx; sx_i++)  /* 放大sx倍列 */

                //{

                //    *p++ = u;

                //    *p++ = v;

                //}

                *p++ = u;

                *p++ = v;

                *p++ = u;

                *p++ = v;

                *p++ = u;

                *p++ = v;

                *p++ = u;

                *p++ = v;

                *p++ = u;

                *p++ = v;

            }

        }

    }

    wq_cache_flush(WQ_DCACHE_ID_ACORE,DDR_IN_BUFFER_ADDR,H_SIZE*V_SIZE*3/2);

    s_sync_flag_u8 = 1;

}

4.3.3 UAC音频设计

音频部分的框架如下,前面拆解知道,智能门锁后屏支持USB接口的UAC,可以实现音频对讲,

即上行

ADC采集到音频->UAC->后屏->云端->手机

反方向

手机->云端->后屏->UAC->PWM或者PDM->喇叭播放

而我们游戏只需要播放音频即可,我们可以直接对接USB接口,将游戏音频通过UAC发送到后屏,通过云端到手机端播放,也可以直接本地通过PWM或者PDM播放。

170753xbxj404btwq4xuuq

相关的实现文章可以参考本文公众号系列文章

170754wc3pazb89zzd6c5c

https://mp.weixin.qq.com/s/QbngRy9ph2NIuULHxvQwqg

mp.weixin.qq.com/s/F9Q6ynmclC-SPmpOCQWysQ

https://mp.weixin.qq.com/s/bLSLwPjl5cC_8X-YxZo89Q

170754g6bzq66qbb6ka8bb

对应的接口实现如下,先将音频写入fifo池,然后uac从fifo池中获取数据发送,fifo池的实现可以参考本人文章https://mp.weixin.qq.com/s/PV-sUxzTEKbobgyt4BKRlA超级精简系列之十九:超级精简的循环FIFO池,C实现

/* Sound Output 5 Waves - 2 Pulse, 1 Triangle, 1 Noise. 1 DPCM */

void InfoNES_SoundOutput(int samples, BYTE *wave1, BYTE *wave2, BYTE *wave3, BYTE *wave4, BYTE *wave5){

  int i;

  for (i = 0; i < samples; i++)

  {

    final_wave[waveptr] =

      ( (uint16_t)wave1 + (uint16_t)wave2 + (uint16_t)wave3 + (uint16_t)wave4 + (uint16_t)wave5 ) / 5;

    waveptr++;

    if ( waveptr == 441 )

    {

      waveptr = 0;

      adc_mic_output(final_wave,sizeof(final_wave));

    }

  }

}

void adc_mic_output(uint8_t* buffer,uint32_t len)

{

    //return fifo_pool_in(&out_fifo_pool_dev, buffer, len);

    fifo_pool_in(&in_fifo_pool_dev, buffer, len);

    //xprintf("wav out\r\n");

}

4.4.4 按键获取

前面我们手柄设计时已经进行了测试,只需要定时获取更新按键状态  key_update();

提供接口key_get_state获取按键状态。


uint8_t key_get_state(void)

{

    return key_state;

}

static void key_update(void)

{

    uint8_t keyval = 0;

    if(key_read(&keyval) == 0)

    {

        if((keyval & 0x80) == 0)

        {

        key_state = 0x01;

        }

        if((keyval & 0x40) == 0)

        {

        key_state = 0x02;

        }

        if((keyval & 0x10) == 0)

        {

        key_state = 0x04;

        }

        if((keyval & 0x20) == 0)

        {

        key_state = 0x08;

        }

        if((keyval & 0x08) == 0)

        {

        key_state = 0x10;

        }

        if((keyval & 0x02) == 0)

        {

        key_state = 0x20;

        }

        if((keyval & 0x04) == 0)

        {

        key_state = 0x40;

        }

        if((keyval & 0x01) == 0)

        {

        key_state = 0x80;

        }

    }else

    {

        key_state = 0;

    }

}

/* Get a joypad state

* bit

* 0    A        7F

* 1    B        BF

* 2    Select   EF

* 3    Start    DF

* 4    UP       F7

* 5    DOWN     FD

* 6    LEFT     FB

* 7    RIGHT    FE

*/

void InfoNES_PadState( DWORD *pdwPad1, DWORD *pdwPad2, DWORD *pdwSystem )

{

    *pdwPad1 = key_get_state();

    *pdwPad2 = 0;

    *pdwSystem = 0;

}

4.5玩起来

由于是UVC,所以可以先在PC上调试,测试,然后接门锁进行测试。

4.5.1 电脑上玩

170754zelzr3xraa7jwl0e

4.5.2 门锁上玩

坦克大战

170754hf9fj95fcl51faok

170754t4zn31goz34bwoy3

170754klb4bb9r8yxi95x5

170755z8a34a3c4p43a7n7

170755bv076re3r09tgffq

打麻将

170755ed2irnrfdyyvbbdv

170756rjubrduqbin11jst

冒险岛 170756basarn81ad68rp11 170756ng7ghqxpggcciggx 170756wq8i80dpdiwvoii3

超级玛丽 170756ckljjoj8epnp23l8 170756r0x6axaspkqf03bx 170757d3t4axmr0mmrlpo4

F1赛车

170757i4c484ccddw64447 170757qw31iszclilvw73i 170757z96jljrbcj60bjz9

水管工马里奥 170757zfojqmomjcmtkffj 170757utbbz2t6bf2b6yr5

打网球 170758wk4007awlf7l8tfa 170758ht4h46tvtajayy6k

4.5.3 门锁远程可视对讲,远程玩

超级玛丽

170758kks888mimt8tu8p2

F1赛车

170758ybdhhe6699621x2u

水管工马里奥

170758l8akm8f1aas3u388

打网球

170758h6zh7qqzbq6js6qc 170759gjegecerjiej4y8b

坦克大战

170759lxf9l696xxf9g69f 170759wf0f0rwfr0wtb055 m

冒险岛

170800opj5hlpmc3zhpql4

4.6 总结

得益于我们之前分享总结了很多UVC,UAC的实例(参考上述本人的公众号分享文章),所以可以很快的模拟门锁的猫眼设备。这样可以快速的移植NES游戏模拟器,替换猫眼,实现使用门锁玩转NES游戏,甚至可以手机APP上远程可视对讲玩。寓工作学习于乐,可以看到工作学习是可以和玩结合起来的,作为嵌入式开发,建议多制造自己的轮子,轮子到用时方恨少,这也是本人公众号分享了很多嵌入式开发的轮子的原因。

开源的my-info-nes中有两百多个NES游戏,可以尽情的玩起来了。想象一下当某个朋友过来拜访,但是你在外面不方便先让对方直接进入,这时,先远程来一把麻将消磨时间,等你回去,不是很惬意吗。

170800flrxpp8hphrxprnz

总结

智能门锁市场现在可以说是一个竞争非常激烈的市场,但是其市场又是非常大的,怎么活下来在这片市场下分得一杯羹,甚至活得不错,需要有自己的实力。走低价路线,还是走高端差异化路线不同厂家有不同的选择。个人觉得厂家的产品不管如何,必须是要解决用户需求的。质量,体验上面有差异化且不俗的表现,消费者才会买单,当然成本控制也很重要,物美价廉永远是终极追求。但是怎么在提升这些方面时,控制成本,提高效率是值得思考的。个人觉得提高研发效率,成本从设计中省出来,联合各大优秀IC厂商(比如上述拆解中看到的WQ在AI等领域的方案应用等)和方案厂商打造平台化优秀的平台方案才是基础之道。仅靠价格战,低质低价总是要被市场淘汰的。

当然拆解学习优秀厂家的优秀产品方案也是学习进步的一个方法,但是我们拆解不仅仅是拆解,还需要玩的开心,所有我们基于此打造了使用智能门锁玩转NES游戏的分享,也鼓励大家有玩的心态去学习,玩的开心。

从上述拆解可以学习到优秀厂家优秀产品的设计,比如语音输出的PWM+电机驱动的方案替代DAC+PA功放,就是一个典型的成本是设计出来的而不是省料省出来的好的说明。当然也还有很多值得学习的地方,前面拆解过程也有说明,这里就不再赘述。当然拆解过程也有一些小的个人建议,不一定对,比如门锁复位按钮,要找一个小针不方便,如果可以直接按到就最好了,比如线束的固定卡片只有一个螺钉固定,如果有限位就更好了,方便生产操作,也不会随着使用而移动。

另外一点讨论与感想: 从上述拆解分析可以看出,目前智能门锁方案,采用的是模块化设计,不同厂家方案的组合,人脸识别,猫眼,后屏等都是不同厂家的方案,这有点好处就是各个模块可以择优选择优秀的供货商,减小产品系统级开发难度,但是存在沟通,不同厂家方案设计不同的问题,功耗成本也相对较高。那么是否以后会出现一个平台级的产品,能包含人脸识别,猫眼,后屏用一个SOC实现呢? 这样功耗成本都可以做到更低,个人猜测也是有可能的,尤其是现在RISC-V异构多核SOC的兴起,个人感觉是有可能出现的。

总之拆解不懈,学习不怠,学习别人好的,激发自己创新设计更好的,保持学习和创新的激情,才是我们拆解并且玩转的终极意义。期待下一篇超级硬核的拆解玩转分享。

附录 视频和软硬件代码仓库

硬件手柄设计:https://oshwhub.com/qinyunti/key

软件my-info-nes:https://gitee.com/qinyunti/my-info-nes.git

演示视频: 面包板社区的芯视频下搜索《【拆解】+ 什么?智能锁也能玩游戏-智能锁超级详细拆解分析与改造玩转NES游戏完全实录》