雷精灵2046 发表于 2019-5-22 14:16:45

自制FC无线手柄

本帖最后由 雷精灵2046 于 2019-6-14 09:28 编辑

没有用复杂的分立元件搭建发射接收电路,直接用的现成的2.4G无线模块jdy-40。


发射端靠3.7V锂电池供电,用了一片SOP8封装的STC单片机,负责产生连发脉冲,同时负责检测电池电量,电压低于3V左右的时候触发低压中断,低压指示LED就会亮起来告知该充电了。平时发射模块和单片机均处于睡眠状态,有按键按下才会唤醒,从而实现省电。实测睡眠状态下整机电流约1~2uA。板子洗了一下稍微干净了一点。因为对于微功耗设备来说,焊接残余的助焊剂可能会增加漏电流。

买了一个火花的FC山寨手柄,去掉里面的牛屎板子,并修正一下塑料挡板,把锂电池和充电模块装进去。
为了减少体积,充电模块裁剪得十分小,并且更换了充电电阻使之适应这个小小的锂电池。这么小容量的电池,也不需要多么大的充电电流是吧?
然后统统用热熔胶粘上。充电电流比较小,所以发热很少,不会熔化热熔胶。



最后把按键、导电橡胶和电路板装上去。
精心裁剪了电路板,正好扣上,不会阻碍其他元件,也不会影响后盖和拧螺丝。


完工!


这是充电的MicroUSB插座。旁边的小孔用于透出低压报警LED的光线。


接收端用一片DIP16封装的STC单片机模拟CD4021。为啥要用单片机模拟而不是直接用CD4021?因为jdy-40模块接收端在有按键按下的时候输出高电平,无按键按下输出低电平,和普通手柄正好相反。当然我也可以用两片74HC00或者74HC04之类的逻辑门进行反相,但那就增加了芯片的数量。对于FC手柄这种工作频率并不高的设备来说,STC单片机完全可以胜任模拟工作。



电路设计有点小小的错误,所以有几根飞线。原本设计使用低压差LDO是XC6206,结果在做这个接收模块的时候,买的LDO居然找不到了!无奈只好用了AMS1117。幸好对于接收模块来说是5V降到3.3V,1117可以胜任。要是发射端手柄的话,3.7V降到3.3V,那就非得用6206不可了。

实测十分灵敏,延迟极小几乎感觉不出来。可惜这山寨手柄的手感并不佳,尤其是方向键,软塌塌的。看来我得买个剪线手柄改装。

这是电路图,有兴趣的朋友可以参考。


我信奉开源主义,十分痛恨把技术藏着掖着。好东西大家分享嘛!所以两个单片机的源代码大公开!
编译器:Keil 51。

发射端:
#include <INTRINS.H>
#include "STC15W.h"


/*
--------------------------
|1 (OUT)        \__/   (INT1) 8|
|                                                   |
|2 (VCC)                   (INT0) 7|
|               STC15W204S                   |
|3 (LED)                       (TXD) 6|
|                                                   |
|4 (GND)                       (RXD) 5|
--------------------------

LED ---|<|---[===]--- VCC
           Red          330

Fosc = 6MHz
*/


// 矩形波输出脚
sbit OUT = P5^4;
// 低压指示灯引脚
sbit LED = P5^5;

// 停机标志位
bit isReadyToHalt = 0;

// 矩形波次数累计
static volatile unsigned char count = 0;


static void GPIO_Init(){
        // P3口设置为准双向,默认靠内部弱上拉输出高电平
        P3M0 = 0;
        P3M1 = 0;
        P3 = 0xFF;
        // P5口设置为准双向,默认靠内部弱上拉输出高电平
        P5M0 = 0;
        P5M1 = 0;
        P5 = 0xFF;
}

static void Timer0_Init(){
        // 16毫秒@6.000MHz
        AUXR &= 0x7F;        // 定时器时钟12T模式
        TMOD &= 0xF0;        // 设置定时器模式
        TL0 = 0xC0;                // 设置定时初值
        TH0 = 0xE0;                // 设置定时初值
        TF0 = 0;                // 清除TF0标志
        TR0 = 1;                // 定时器0开始计时
}

static void Interrupt_Init(){
        // INT0中断触发类型为下降沿触发
        IT0 = 1;
        // 允许INT0中断
        EX0 = 1;
        // INT1中断触发类型为下降沿触发
        IT1 = 1;
        // 允许INT1中断
        EX1 = 1;
        // 允许定时器0中断
        ET0 = 1;
        // 允许全局中断
        EA = 1;
}

static void INT0_Isr() interrupt 0{
        // 矩形波次数累计归零
        count = 0;
        // 重开定时器0中断
        ET0 = 1;
}

static void Timer0_Isr() interrupt 1{
        // 输出矩形波
        OUT = ~OUT;
        // 累计矩形波次数
        count++;
        // 超过250个矩形波,准备停机
        if(count > 250){
                count = 0;
                isReadyToHalt = 1;
        }
}

static void INT1_Isr() interrupt 2{
        // 矩形波次数累计归零
        count = 0;
        // 重开定时器0中断
        ET0 = 1;
}


void main(){
        // 初始化P3和P5口
        GPIO_Init();
        // 初始化定时器0
        Timer0_Init();
        // 初始化中断
        Interrupt_Init();
       
        while(1){
                // 检测电量
                if(PCON & (1 << 5)){
                        // 如果电量低,低压指示灯亮
                        LED = 0;
                        // 清电量检测硬件标志位
                        PCON &= ~(1 << 5);
                }else{
                        // 电量正常,低压指示灯灭
                        LED = 1;
                }

                // 检测到停机标志
                if(isReadyToHalt == 1){
                        // 暂时关闭定时器0中断
                        ET0 = 0;
                        // 停机之前先把矩形波输出脚置低电平,以方便INT0和INT1唤醒CPU
                        OUT = 0;
                        // 停机之前先把低压指示灯灭掉,以省电
                        LED = 1;
                        // 进入停机模式
                        PCON |= 0x02;
                        _nop_();
                        _nop_();
                        _nop_();
                        _nop_();
                        // 唤醒,清标志位
                        isReadyToHalt = 0;
                        // 重开定时器0中断
                        ET0 = 1;
                }
        }
}

硬件参数配置:


接收端:
#include <INTRINS.H>
#include "STC15W.h"


/*

*---------------------------*
|1 (GPIO2)       \__/(GPIO1)16|
|                                                        |
|2 (GPIO3)                   (GPIO0)15|
|                                                        |
|3 (GPIO4)                                  14|
|                                                        |
|4 (GPIO5)                        (DATA)13|
|                  STC15W204S                |
|5 (GPIO6)                   (LATCH)12|
|                                                        |
|6 (VCC)                       (CLK)11|
|                                                          |
|7 (GPIO7)                       (TXD)10|
|                                                        |
|8 (GND)                       (RXD) 9|
*---------------------------*
Fosc = 12MHz

P1.0 -> 上
P1.1 -> 左
P1.2 -> 下
P1.3 -> 右
P1.4 -> SEL
P1.5 -> STA
P5.4 -> B
P5.5 -> A

*/


sbit CLK        = P3^2;
sbit LATCH        = P3^3;
sbit DATA        = P3^6;

bit isReady = 0;
static unsigned char key = 0;
static unsigned char buf = 0;        // 双缓冲。这个缓冲区保存从P1和P5组合而来的键值
static unsigned char bufReady = 0;        // 双缓冲。这个缓冲区保存上面那个缓冲区的备份
static const unsigned char data mask[] = {0x80, 0x40, 0x10, 0x20, 0x01, 0x04, 0x02, 0x08};        // A B SELECT START UP DOWN LEFT RIGHT。为提升速度,这个表放到RAM中
static unsigned char idx = 0;


static void GPIO_Init(){
        // P1口和P5口用于接收并行信号,全部初始化为准双向,依靠内部弱上拉输出高电平
        P1M0 = 0;
        P1M1 = 0;
        P1= 0xFF;
        P5M0 = 0;
        P5M1 = 0;
        P5= 0xFF;
        // P3口初始化为准双向
        P3M0 = 0;
        P3M1 = 0;
        P3 = 0xFF;
}

static void Interrupt_Init(){
        // INT0中断(CLK)触发类型为上升沿+下降沿
        IT0 = 0;
        // 允许INT0中断
        EX0 = 1;
        // INT1中断(LATCH)触发类型为上升沿+下降沿
        IT1 = 0;
        // 允许INT1中断
        EX1 = 1;
        // 开启全局中断
        EA = 1;
}

static void INT0_Isr() interrupt 0{
        // 只有已经成功锁存才允许CLK移位
        if(isReady == 0)
                return;
        // 读当前引脚电平,如果是低电平则说明是下降沿,此时直接忽略该中断
        if(CLK == 0)
                return;
        // CLK上升沿到来的时候,取锁存值的下一位输出
        idx++;
        DATA = key & mask;
        // 如果已经完成7次移位,则一轮读取完成
        if(idx >= 7)
                isReady = 0;
}

static void INT1_Isr() interrupt 2{
        // 读当前引脚电平,如果是低电平则说明是下降沿,此时直接忽略该中断
        if(LATCH == 0)
                return;
        // 当LATCH上升沿到来的时候,锁存所有按键状态,同时把键值A输出到DATA
        key = bufReady;
        DATA = key & 0x80;//mask;        // 为了加速运算,直接取表中的值而不是读表
        idx = 0;
        // 允许CLK进行移位
        isReady = 1;
}

void main(){
        GPIO_Init();
        Interrupt_Init();
       
        while(1){
                //PCON |= 0x01;        // 进入省电模式
                //_nop_();
                //_nop_();
                //_nop_();
                //_nop_();
                buf = ~((P1 & 0x3F) | ((P5 << 2) & 0xC0));
                bufReady = buf;
        }
}

硬件参数配置无特殊要求,晶振频率选择12M即可。

这是编译好的固件。

69yuan 发表于 2019-5-23 10:05:23

不错的设计,想问一下楼主,FC的15针公头怎么解决?普通的公头太短了,插不进去FC的扩展接口。

雷精灵2046 发表于 2019-5-23 11:27:39

69yuan 发表于 2019-5-23 10:05
不错的设计,想问一下楼主,FC的15针公头怎么解决?普通的公头太短了,插不进去FC的扩展接口。 ...

坛友的改造方案。

yinlaijun 发表于 2019-5-25 10:42:27

高手的无私奉贤值得表扬,辛苦了!

xunxun 发表于 2019-5-25 12:07:18

精彩,楼主动手强,也写得很详细。
2.4G方案是目前无线的最低延迟吗?

ppad 发表于 2019-5-28 17:27:23

给技术帝赞一个,有半成品发布吗,自己焊接改造就可以,编程实在不会啊:'(

孙大师 发表于 2019-5-28 18:55:54

非常好,楼主是技术帝,到目前我都还没开发fc的无线套件,等空了,开发一套套件,等玩家自己组装。

雷精灵2046 发表于 2019-5-29 16:49:20

xunxun 发表于 2019-5-25 12:07
精彩,楼主动手强,也写得很详细。
2.4G方案是目前无线的最低延迟吗?

我没用过其他无线手柄,所以实在不知道我自己做的这个手柄到底是不是最低延迟。
不过根据网上的说法,延迟高低主要是由通信协议的复杂程度,以及数据传输速率决定的。这个模块虽然是2.4G无线模块,但由于协议很简单,所以延迟比较低。
我用这个手柄玩一些ACT游戏,比如超级马里奥、忍者龙剑传、热血系列,完全感觉不到延迟,和游戏机自带的手柄没有任何区别。唯一的区别就是这个手柄是用火花山寨手柄改造的,外壳是垃圾回收塑料,粗糙+脆弱,导电橡胶弹性不佳,手感不是一般的差。
我手里还有十几张板子,等我有空了我再做一套,到时候买一个好一点的手柄改造。

雷精灵2046 发表于 2019-5-29 16:56:14

ppad 发表于 2019-5-28 17:27
给技术帝赞一个,有半成品发布吗,自己焊接改造就可以,编程实在不会啊 ...

很可惜,我手里没有多余的单片机了,LDO也找不到了,锂电池以及充电模块更是没有了。反正就是该用到的元件基本都没了,所以没法发布半成品。
两个板子倒是有十几张剩余。
你要是有USB转TTL串口板,完全可以自己买这两种单片机,然后我给你编译好的固件,你自己用串口板下载到单片机里面就行了。两个2.4G无线模块也得通过串口板用AT命令设置参数,不过好在我在设计电路的时候就已经预留好了串口,可以直接在线下载和在线设置参数。总的来说并不麻烦。

雷精灵2046 发表于 2019-5-29 17:00:34

孙大师 发表于 2019-5-28 18:55
非常好,楼主是技术帝,到目前我都还没开发fc的无线套件,等空了,开发一套套件,等玩家自己组装。 ...

能得到孙大师的赞誉,不胜荣幸!
不如开发一个多功能手柄,比如用SFC手柄改造,L/R键配合其他组合键可以用来切换2P~4P、调节连发速度、软重启等各种功能。接收端插到15PIN扩展口。
页: [1] 2 3
查看完整版本: 自制FC无线手柄