@工程师们,无人机、平板、耳机、京东卡,板哥喊你拿大奖!
大奖叠加单篇奖!👆扫码去社区发帖参加吧!
4.1 方案设计
我们从前面的方案分析,精简出如下的数据流,原来视频流有两条,一条是摄像头到猫眼模块通过WIFI到云端到手机,即可视对讲的路径,一条是摄像头到猫眼模块通过USB到后屏,TFT液晶屏显示,即本地猫眼可视的路径。
我们只需要替代摄像头,输入视频流就可以实现上述视频流,这样就可以在本地TFT屏幕和远程手机显示游戏界面。
当然要用门锁玩游戏,最直接的方式是基于猫眼模块的主控这里是君正的T21开发NES游戏模拟器,但是这是不现实的因为没有SDK,没有原来的业务源码,我们退而求其次,”劫持”视频流,输入我们的视频流,这样基于我们自己手里的控制板开发NES游戏模拟器。
而现成的USB充电接口是对外的,我们就可以用此USB接口跳线到后屏USB接口实现游戏视频流接入,并增加切换开关,可以切换使用原数据流还是USB口引入的数据流,这样可以切换,不影响原来的功能,如下所示(注:图中条线应该是跳线)
所以我们需要做的就是模拟猫眼的USB数据,即模拟猫眼同样的USB设备,将我们的NES游戏模拟器的视频帧数据转为同样的格式,后面我们抓包可以看到是MJPEG格式的UVC视频流,还有UAC的音频流。
前面我们拆解分析,知道了猫眼和后屏是通过4P USB线进行传输,将猫眼采集的图像在后屏显示的,可以猜测是走的UVC协议,我们就测试分析下。
1.先将猫眼转接到电脑,抓取枚举和传输数据等信息。
2.然后制作转接线,使用USB分析仪,监控猫眼和后屏的通讯过程。
3.最后将线引出到临时USB充电口,按照前面的设计,设置切换开关要么切换到猫眼要么切换到USB口,用自己的设备接到USB口,替代猫眼摄像头,显示任意内容在后屏,玩转NES游戏。
4.2.1 抓包猫眼的枚举和通讯信息
猫眼是如下4P usb接口
制作USB转接线接电脑,抓取到描述符和通讯过程,分析其通讯。
过程略,这里画出整个描述符拓扑结构。可以看出使用了UVC+UAC的复合设备,UVC使用MJPEG格式,所以我们也需要按照猫眼同样的方式实现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打样
4.3.1.5 焊接
很快样板回来了,做的紫色的,手工焊接下。
4.3.2 驱动
4.3.2.1设备地址
我这里是PCF8574T不带A的型号,设备地址如下,我这A2-A1-A0都接地,所以读地址是0x41,写地址是0x40.
发送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
void io_iic_start(io_iic_dev_st* dev)
{
dev->sda_write(1);
dev->scl_write(1);
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayus);
}
dev->sda_write(0);
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayus);
}
dev->scl_write(0);
}
void io_iic_stop(io_iic_dev_st* dev)
{
dev->sda_write(0);
dev->scl_write(1);
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayus);
}
dev->sda_write(1);
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayus);
}
dev->scl_write(0);
}
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;
}
for(uint8_t i=0; i<8; i++)
{
dev->scl_write(0);
if((tmp & 0x80) != 0)
{
dev->sda_write(1);
}
else
{
dev->sda_write(0);
}
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayus);
}
dev->scl_write(1);
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayus);
}
tmp <<= 1;
}
dev->scl_write(0);
dev->sda_2read();
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayus);
}
dev->scl_write(1);
ack = dev->sda_read();
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayus);
}
dev->scl_write(0);
return (ack==0) ? 0 : -1;
}
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;
}
for(uint8_t i=0; i<8; i++)
{
tmp <<= 1;
dev->scl_write(0);
dev->sda_2read();
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayus);
}
dev->scl_write(1);
if(dev->sda_read())
{
tmp |= 0x01;
}
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayus);
}
}
dev->scl_write(0);
dev->sda_write(ack);
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayus);
}
dev->scl_write(1);
if(dev->delay_pf != 0)
{
dev->delay_pf(dev->delayus);
}
dev->scl_write(0);
*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
typedef void (*io_iic_scl_write_pf)(uint8_t val);
typedef void (*io_iic_sda_write_pf)(uint8_t val);
typedef void (*io_iic_sda_2read_pf)(void);
typedef uint8_t (*io_iic_sda_read_pf)(void);
typedef void (*io_iic_delay_us_pf)(uint32_t delay);
typedef void (*io_iic_init_pf)(void);
typedef void (*io_iic_deinit_pf)(void);
typedef struct
{
io_iic_scl_write_pf scl_write;
io_iic_sda_write_pf sda_write;
io_iic_sda_2read_pf sda_2read;
io_iic_sda_read_pf sda_read;
io_iic_delay_us_pf delay_pf;
io_iic_init_pf init;
io_iic_deinit_pf deinit;
uint32_t delayus;
} io_iic_dev_st;
void io_iic_start(io_iic_dev_st* dev);
void io_iic_stop(io_iic_dev_st* dev);
int io_iic_write(io_iic_dev_st* dev, uint8_t val);
int io_iic_read(io_iic_dev_st* dev, uint8_t* val, uint8_t ack);
void io_iic_init(io_iic_dev_st* dev);
void io_iic_deinit(io_iic_dev_st* dev);
#ifdef __cplusplus
}
#endif
#endif
int pcf8574_read(pcf8574_dev_st* dev, uint8_t* val)
{
dev->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;
}
int pcf8574_write(pcf8574_dev_st* dev, uint8_t val)
{
dev->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;
}
void pcf8574_init(pcf8574_dev_st* dev)
{
dev->init();
}
void pcf8574_deinit(pcf8574_dev_st* dev)
{
dev->deinit();
}
#ifndef PCF8574_H
#define PCF8574_H
#ifdef __cplusplus
extern "C"{
#endif
#include
typedef void (*pcf8574_iic_start_pf)(void);
typedef void (*pcf8574_iic_stop_pf)(void);
typedef int (*pcf8574_iic_read_pf)(uint8_t* val, uint8_t ack);
typedef int (*pcf8574_iic_write_pf)(uint8_t val);
typedef void (*pcf8574_iic_init_pf)(void);
typedef void (*pcf8574_iic_deinit_pf)(void);
typedef void (*pcf8574_iic_delay_us_pf)(uint32_t delay);
typedef struct
{
pcf8574_iic_start_pf start;
pcf8574_iic_stop_pf stop;
pcf8574_iic_read_pf read;
pcf8574_iic_write_pf write;
pcf8574_iic_init_pf init;
pcf8574_iic_deinit_pf deinit;
uint8_t addr;
} pcf8574_dev_st;
int pcf8574_read(pcf8574_dev_st* dev, uint8_t* val);
int pcf8574_write(pcf8574_dev_st* dev, uint8_t val);
void pcf8574_init(pcf8574_dev_st* dev);
void pcf8574_deinit(pcf8574_dev_st* dev);
#ifdef __cplusplus
}
#endif
#endif
4.3.3 测试
#include "io_iic.h"
#include "pcf8574.h"
#include "key.h"
#include "gpio.h"
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,
};
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);
}
#ifndef KEY_H
#define KEY_H
#ifdef __cplusplus
extern "C"{
#endif
#include
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<
{
printf("key%d,write err,res:%d\n",i,res);
}
os_delay(10);
}
}
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);
}
}
示波器查看输出波形,然后按键查看按键识别测试获取状态正常。
4.4 移植NES游戏模拟器
NES相关移植好的代码参考本人开源的仓库https://gitee.com/qinyunti/my-info-nes.git,基于InfoNES进行修改,如果使用需要实现自己的显示,音频输出,按键获取等接口。文件系统基于littlefs。通过串口shell导入rom文件进行运行(shell实现可以参见本人公众号文章《
一个超级精简高可移植的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)
{
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;j2)
{
for(int i=0;i2)
{
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);
for(int sy_j=0; sy_j
{
p = py+(j*sy)*H_SIZE+i*sx + sy_j*H_SIZE;
for(int sx_i=0; sx_i
{
*p++ = y00;
}
for(int sx_i=0; sx_i
{
*p++ = y01;
}
}
for(int sy_j=0; sy_j
{
p = py+((j+1)*sy)*H_SIZE+i*sx + sy_j*H_SIZE;
*p++ = y10;
*p++ = y10;
*p++ = y10;
*p++ = y10;
*p++ = y10;
*p++ = y11;
*p++ = y11;
*p++ = y11;
*p++ = y11;
*p++ = y11;
}
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
{
p = puv+((j/2)*sy)*H_SIZE + i*sx + sy_j*H_SIZE;
*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播放。