一. 前言
二. 拆解和方案分析
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使用体验等角度入手,后面才真正进入拆解阶段。一个好的产品,外观结构设计绝对是重要元素,毕竟现在产品不仅仅是看功能的,也是要看颜值的,尤其是门锁是装在入户门的,是一个门面担当产品,有时候颜值可能是用户买单的第一参考要素,所谓的第一眼心动是很重要的。
我这里包装什么的都丢了,就不来开箱了,直接上手。我这里只有前后面板,和锂电池、其他的锁体、导向片、开孔孔位及尺寸图、保修卡、说明书、机械钥匙等没有,也不是本文的重点,也无关紧要,略去无伤大雅。
首先来个三大件,前后面板,电池的全家福合照,然后再逐一把玩。初体验也先从这三大件的整体开始。
2.1.1电池模块
首先来看电池模块,电池采用一体模块化设计,具备轻便,体积小,防护好,成本低等优点。
磨砂的质感
磨砂的质感看起来是非常不错的,颗粒度都比较合适且均匀,手感也不错。好的磨砂的质感是需要好的模具加工设计的,说明在产品设计细节上,还是比较注意的,走的高端路线,所以该花的成本要花,虽然电池是位于内部的组件不会经常触碰到,看到,但是走高端路线也必须保证其质量,这是塑造高端品牌形象需要注意的细节。
接口细节
再来看电池接口细节,铜触点,突出的颗粒能保证很好的接触,铜片也不薄。再来看,下面会有个突出的边缘,为什么要有这个设计呢? 电池安装后触点朝下卡在面板里,这个突出就是方便用手把电池抠出来的,这就是用户体验设计了,所以成熟的产品是需要不断地迭代的,尤其是用户体验,好的公司总是很关注用户体验和问题反馈然后不断迭代完善。
来个扣电池的特写,可以感受到操作比较方便。
再来看电池前后端都有一部分没有磨砂,这是为什么呢? 作为一个熟悉硬件的嵌入式软件开发工程师,一定是知道点模具设计知识的(狗头保护)。用我曾经第一份工作公司组织花了三个月的模具培训经历(有正经结业证书的那种),思考了一下,这里两个突出的地方注塑时需要用到顶针(应该不需要斜顶,角度不是斜的),这个光滑的地方就是顶针的接触面,其他磨砂的地方是模具内腔体做的,顶针这里一般就不做磨砂了,为什么不做呢,做了磨砂摩擦阻力大,顶针就不好脱模了(个人猜测,有专业的可以指出错误本人及时修改),另外猜测做了也不可能和其他表面一样,毕竟顶针边缘会有熔接线的,也不美观还增加成本所以没必要。
看到中间的细线,就是前后模的分界线,熔接线,整个电池是一体注塑的,这样电池作为一个独立的整体,模块化设计,减少了结构设计成本(如果可拆卸需要设计前后面板,卡扣结构,模具成本更高),体积更小,防潮性等防护性也比较好。
Type-C充电口
好评,新的产品肯定不能用Micro USB接口,要不太掉价了。旁边还有小的指示灯,细节也做的不错。实际充电看一下指示灯的效果。
总结下电池模块的设计就是,质感不错,模块化设计,体积小,防护性好,降低了成本。
电池上的使用提示,这也是用户体验的体现,日本的产品做得好的一个方面就是说明书做的非常好,非常详细,一步步指导使用,很注意细节。这也是产品用户体验改善的一个方面。
2.1.2后面板
先来个前后的”证件照”,这里把PCB的螺钉已经拆掉了。
这里有个疑问?为什么OPEN和CLOSE按键的字体是反的?
整体属于塑料面板加金属外壳的形式,塑料面板磨砂,金属壳体,质感还是非常不错的。很多低成本的很可能是全部是塑料的,质量和安全性就没法比了。
再来看把手部分细节和后面的走线
然后是开锁和按键部分细节,旋钮操作下来手感不错,回弹跟手。
后屏部分,一体设计,感觉还是很高端的,有定酷黑的商务风格。
喇叭和喇叭的走线,以及喇叭腔体,喇叭走的双绞线,
后面板控制部分,各PCB板,这个在后面的方案详细拆解分析中再分析,初步看到全部都做了三防处理,接插件也点胶做了加固,这也是细节和质量保证的体现。
2.1.3 前面板
同样的先来个前后”证件照”
再来看细节
前面板是金属壳体拉丝设计,后面板没有,前面板是在外面的,这也是给别人看的更高级点,也是差异化设计的体现,成本均衡设计的一个考量。给别人呈现好的颜值也是很重要的,自己看的就稍微差点没关系。
红外人体检测和TOF部分
刷卡部分
指纹部分
猫眼摄像头
带补光双摄人脸识别部分,上面有标志的地方是把手触摸检测。
机械锁和临时USB供电,也是typec接口。
2.1.4 外观结构整体总结
以上粗略的看了各个模块的外观和结构,整体上看到质感非常不错,也有设计感,属于时尚商务风,比较百搭点。也是有设计的,比如前后,内外配色质感上的差异化设计,整体风格沉稳,简约时尚但是不简单。磨砂塑壳和金属外壳结合的不错,缝隙较小,说明工艺控制的不错,金属拉丝体现高端,磨砂塑壳,手感质感不错。另外拿起来整个锁体是比较沉的,金属比较厚,和那种降成本全是塑料很薄的低成本款完全不一样。另外也有体现模块化的设计思想,整个设计,外观和结构都是不错的。
2.2 详细拆机分析-抽丝剥茧,不漏过一个细节
前面体验了整体的结构,外观,和风格,现在还是进入详细的拆解分析过程。还是按照前后面板分开进行。
2.2.1 前面板
2.2.1.1 拆解
我们先从前面板入手,
这四个是安装螺栓,不用管
这个按钮用于拆解告警,默认是压下的,拆开后释放,可以检测告警,有橡胶套保护,这也是体现设计细节的。
拆开这10个螺钉,揭开面板和橡胶套,橡胶套和面板都是一体的,面板还挺厚的,质量不错,一体冲压,说明厂家是没有在必要出省成本的。
里面来个特写
来看机械锁细节部分,通过传动结构传递到锁芯
Type-c临时供电部分,USB小板有个螺钉固定,有一个小的定位孔,涂了胶水防松动,可以看到后面所有PCB板和接插件都涂胶防松和防护,这也是不该省的都是没省的,质量还是不错的。
红色圈部分有定位槽防止板子晃动,细节满满。USB口晃动是导致USB口松动的最大原因,这个设计好评,相信很多人都有小家电由于碰到USB口线导致晃动,USB口松动的问题。
这个按钮的小板子,结构也是设计的刚刚好,该有的卡扣,定位都有,比较精细,整个看下来所以的结构设计都是比较精细的,不是那种随便卡卡就行的设计。
拆开这六个圆头螺钉,还有旁边的线束螺钉。就可以分开。
继续拆开这10个螺钉
打开面板,里面是前面板的心脏,好好来研究下这部分的方案
喇叭部分,这里还专门PCB板和金属壳体接地了。
2.2.1.2 IR人体检测和TOF
下图上面两个小孔是TOF传感器,大的球是人体红外检测传感器。
下面的板子是红外人体检测,上面的排线是TOF接到主板,排线是织布的,可以承受一定拉扯,这也是细节的考虑。
拧开螺钉看到背面
用一字螺丝刀,撬开tof部分的盖板
Tof板子贴合的比较紧,需要小心点
用了好大劲用镊子才撬开,原来后面涂了胶,可以看到传感器。
2.2.1.3 人脸识别和猫眼模块
对应的主控板,喇叭,和喇叭腔体
来看猫眼摄像头部分,金属底板用于散热,有橡胶保护套,可见该有的都有。
主板堆叠式设计,先拆开主板,下面是双摄人脸识别模块
继续拆开摄像头部分
继续拆开摄像头,注意这里螺丝刀旁边的是把手触摸按键PAD。
触摸按键和PAD
拆开双摄人脸识别模块
主要IC特写
FLASH用的GD的25Q128,16MB.旁边有串口调试接口。
SOC为WQ5007,WQ在智能门锁等AI市场领域IC占有比较大,是不错的选择。
这个8PIn IC看不清型号
再来看猫眼主控,可以看到后面板的SMA天线最终接到了这里,即猫眼的WIFI天线,接到了后面板室内。
蜂鸣器,喇叭,和面板的接线
主控是君正的SOCT21,WLAN是XR871,FLASH是FM25Q128
音频功放LN4891,旁边是三星的EMMC5.1 KLM4G1 4GB
旁边一个丝印xmc的,还有个小的ic看不到型号。
2.2.1.4 指纹模块
指纹模块,拧开两个螺钉,可以看到指纹模块小板
2.2.1.5 前面板主控(含刷卡)
拧开螺钉
看到按键,可以看到NFC线圈和按键是做在一起的。橡胶垫用于触摸按键背光之间的光隔离。
主控板的接口如下
主控板背面都是背光灯
主控用的赛普拉斯的cy8c6245azi-s3d42
提示音喇叭驱动用的PWM经过电机驱动LN8503做功放,这是一个降成本的方案,在低音质要求场景使用PWM+电机驱动,代替DAC+PA功放降低成本,这里应该是用来播放提示音。
这里2+1共3片74HC595做IO扩展,毕竟背光灯,按键这些需要比较多的IO。
这里丝印P8的应该是P-MOS,猜测是控制闪光灯或者电机之类的,作为大电流开关。
FM17580是复旦微的NFC控制芯片,话说复旦微在NFC领域的控制芯片用的还不少。
Flash是cfeon qh64a 104hip
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 后面板
再来看后面板
2.2.2.1 电池接口
先看电池安装部分
电池转接板
按箭头方向拉出后屏
这里就是电池仓,圈出部分就是电池接口
电池安装后如下,电池突出的地方方便扣出电池。
2.2.2.2 控制板
排线接把手,4p接CLOSE和OPEN按键,2p接喇叭
背面是后屏接口
如图小孔是恢复密码按键,按两次恢复初始密码,密码默认是12345678,按提示设置密码.
这里有点小建议,有时候要找个小的细小的东西去戳还不方便,如果直接能开孔露出按键是不是好点,毕竟这里是电池舱内,不是外露的,也无需考虑美观。
这两个接口接电机和前面板
Mcu为复旦微的Fm15l023,前面板还用了一颗复旦微的NFC控制芯片,看来复旦微占据了两席之地。
背面
OPEN CLOSE按键小板
手动开锁部分
走线细节,卡扣束缚走线,这里排线是布织的,因为要接到把手里面,需要承受一定拉力。
这里的线束卡片也是用的金属片,这里有个建议,就是卡片最好有限位,因为只有一个螺钉固定,打螺钉时卡片会转动,当然转动会碰到金属壳,转动一点也没什么大关系。
拧开按键,和这几个螺钉,打开喇叭腔
2.2.2.3后屏
后屏是后面板的重要部分,屏幕显示猫眼观察到的实时视频。视频通过前面板的猫眼控制模块的USB线传过来。
有扫码配网提示,直接拧开螺钉
接口部分
看到屏幕,控制板,屏幕是一体的。
看到控制板,触摸接口和屏幕接口
拆开排线
看到主控型号,是anyka安凯微电子的KY3179EE128
FLASH是XMC的25qh32,4MB。
2.2.2.4手扫识别
排线接到手扫模块,另外一根是天线:前面板过来的猫眼模块的WIFI天线。
拧开四个内六角
继续拧开螺钉
这里橡胶下面还有两个螺钉,橡胶为了挡住螺钉美观,细节不错。
看到前面板引入的wifi天线最终引入到了后面板室内
再看控制板,sma接前面的触摸感应,前面是触摸感应区
非接触距离感应传感器,实现手扫识别。
8p ic应该是一个专用的支持触摸的mcu,丝印打磨掉了看不到型号
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系统框图
根据前面的拆解,画出如下系统框图
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的使用,尤其是添加设备配网这一环。先扫描电池上的二维码进入小程序
然后用小程序扫码后屏的二维码(实际内容如下)
登录小程序,手机号快捷登录,
添加设备,扫码添加,扫码后屏的二维码
我这里已经激活
输入wifi账号和密码
按提示进入门锁菜单
按提示进入配网设置,勾选,点击下一步
将二维码对准猫眼摄像头
只支持2.4G不支持5G。
可以看到整个配网添加设备的过程还是比较简单,体验还是不错的。
这里总结下,使用猫眼的摄像头扫码连接路由器WIFI,这里利用了摄像头进行输入,简化了入网流程。一般其他家用电器都是设备先打开热点,手机连接热点进行通讯,将wifi密码和账户告诉设备,设备再断开热点重新连接路由器wifi,也就是需要两步。这里使用摄像头直接扫码获取路由器wifi信息只需要一步入网,更简单。
改造-玩转NES游戏
4.1 方案设计
我们从前面的方案分析,精简出如下的数据流,原来视频流有两条,一条是摄像头到猫眼模块通过WIFI到云端到手机,即可视对讲的路径,一条是摄像头到猫眼模块通过USB到后屏,TFT液晶屏显示,即本地猫眼可视的路径。
我们只需要替代摄像头,输入视频流就可以实现上述视频流,这样就可以在本地TFT屏幕和远程手机显示游戏界面。
当然要用门锁玩游戏,最直接的方式是基于猫眼模块的主控这里是君正的T21开发NES游戏模拟器,但是这是不现实的因为没有SDK,没有原来的业务源码,我们退而求其次,”劫持”视频流,输入我们的视频流,这样基于我们自己手里的控制板开发NES游戏模拟器。
而现成的USB充电接口是对外的,我们就可以用此USB接口跳线到后屏USB接口实现游戏视频流接入,并增加切换开关,可以切换使用原数据流还是USB口引入的数据流,这样可以切换,不影响原来的功能,如下所示(注:图中条线应该是跳线)
所以我们需要做的就是模拟猫眼的USB数据,即模拟猫眼同样的USB设备,将我们的NES游戏模拟器的视频帧数据转为同样的格式,后面我们抓包可以看到是MJPEG格式的UVC视频流,还有UAC的音频流。
4.2分析猫眼和后屏UVC+UAC传输协议
前面我们拆解分析,知道了猫眼和后屏是通过4P USB线进行传输,将猫眼采集的图像在后屏显示的,可以猜测是走的UVC协议,我们就测试分析下。
先找到线束,制作转接线监控传输过程。
- 先将猫眼转接到电脑,抓取枚举和传输数据等信息。
- 然后制作转接线,使用USB分析仪,监控猫眼和后屏的通讯过程。
- 最后将线引出到临时USB充电口,按照前面的设计,设置切换开关要么切换到猫眼要么切换到USB口,用自己的设备接到USB口,替代猫眼摄像头,显示任意内容在后屏,玩转NES游戏。
4.2.1 抓包猫眼的枚举和通讯信息
猫眼是如下4P usb接口
制作USB转接线接电脑,抓取到描述符和通讯过程,分析其通讯。
过程略,这里画出整个描述符拓扑结构。可以看出使用了UVC+UAC的复合设备,UVC使用MJPEG格式,所以我们也需要按照猫眼同样的方式实现UVC+UAC的设备。
具体实现参见本人分享的文章https://mp.weixin.qq.com/s/_5quPTyLQ-_T0D7MfMTqCw《USB系列之-UVC+UAC扬声器+麦克风实例分享》,这里不再赘述。
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
4.3.1.1原理图
原理图如下比较简单,
4.3.1.2 PCB
PCB如下,设计为10cm以内,这样打样比较便宜。
4.3.1.3 BOM
BOM成本3块钱左右
4.3.1.4 PCB打样
20块钱搞定包邮,速度也很快几天就回来了
4.3.1.5 焊接
很快样板回来了,做的紫色的,手工焊接下。
焊接好后如下
4.3.2驱动
4.3.2.1设备地址
我这里是PCF8574T不带A的型号,设备地址如下,我这A2-A1-A0都接地,所以读地址是0x41,写地址是0x40.
4.3.2.2输出模式
发送STOP-> 注意这里根据测试上电后第一次总是start无ack,所以这里加个stop回到默认状态。
发送START->
发送0x40写地址->
设备回ACK->
写8位数据->
设备回ACK->
写8位数据->
设备回ACK->
...
发送STOP
4.3.2.3输入模式
输入模式必须保证对应的端口先输出的是1(复位默认状态).
发送STOP-> 注意这里根据测试上电后第一次总是start无ack,所以这里加个stop回到默认状态。
发送START->
发送0x41读地址->
设备回ACK->
读8位数据->
主机回ACK->
读8位数据->
主机回ACK->
...
主机回NACK
发送STOP
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测试
根据平台移植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 */
}
示波器查看输出波形,然后按键查看按键识别测试获取状态正常。
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上远程显示,声音播放。
4.4.2 UVC显示设计
视频部分的框架如下,前面拆解知道,智能门锁后屏支持USB接口的UVC,可以实时视频,
即上行
摄像头->猫眼模块->USB(UVC)->后屏
->WIFI->云端->手机
而我们游戏只需要播放视频即可,我们可以直接对接USB接口,将游戏视频通过UVC发送到后屏,通过云端到手机端播放,也可以直接本地通过后屏的TFT液晶播放。
相关的实现文章可以参考本文公众号系列文章
由于后屏只支持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播放。
相关的实现文章可以参考本文公众号系列文章
https://mp.weixin.qq.com/s/QbngRy9ph2NIuULHxvQwqg
mp.weixin.qq.com/s/F9Q6ynmclC-SPmpOCQWysQ
https://mp.weixin.qq.com/s/bLSLwPjl5cC_8X-YxZo89Q
对应的接口实现如下,先将音频写入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 电脑上玩
4.5.2 门锁上玩
坦克大战
打麻将
冒险岛
超级玛丽
F1赛车
水管工马里奥
打网球
4.5.3 门锁远程可视对讲,远程玩
超级玛丽
F1赛车
水管工马里奥
打网球
大
坦克大战
冒险岛
4.6 总结
得益于我们之前分享总结了很多UVC,UAC的实例(参考上述本人的公众号分享文章),所以可以很快的模拟门锁的猫眼设备。这样可以快速的移植NES游戏模拟器,替换猫眼,实现使用门锁玩转NES游戏,甚至可以手机APP上远程可视对讲玩。寓工作学习于乐,可以看到工作学习是可以和玩结合起来的,作为嵌入式开发,建议多制造自己的轮子,轮子到用时方恨少,这也是本人公众号分享了很多嵌入式开发的轮子的原因。
开源的my-info-nes中有两百多个NES游戏,可以尽情的玩起来了。想象一下当某个朋友过来拜访,但是你在外面不方便先让对方直接进入,这时,先远程来一把麻将消磨时间,等你回去,不是很惬意吗。
总结
智能门锁市场现在可以说是一个竞争非常激烈的市场,但是其市场又是非常大的,怎么活下来在这片市场下分得一杯羹,甚至活得不错,需要有自己的实力。走低价路线,还是走高端差异化路线不同厂家有不同的选择。个人觉得厂家的产品不管如何,必须是要解决用户需求的。质量,体验上面有差异化且不俗的表现,消费者才会买单,当然成本控制也很重要,物美价廉永远是终极追求。但是怎么在提升这些方面时,控制成本,提高效率是值得思考的。个人觉得提高研发效率,成本从设计中省出来,联合各大优秀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游戏完全实录》
呵呵,游戏世代的产物,什么也离不开游戏,其实人类始终在游戏。
是的,成本是设计出来的,不是省料省出来的。
神秘的问题在于设计目的,锁门兼玩游戏?
还有品质保证。比如质量指标MTBF无故障工作时间是多少?
一项功能报废了,另一项还要不要?