自制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即可。
这是编译好的固件。 不错的设计,想问一下楼主,FC的15针公头怎么解决?普通的公头太短了,插不进去FC的扩展接口。 69yuan 发表于 2019-5-23 10:05
不错的设计,想问一下楼主,FC的15针公头怎么解决?普通的公头太短了,插不进去FC的扩展接口。 ...
坛友的改造方案。 高手的无私奉贤值得表扬,辛苦了! 精彩,楼主动手强,也写得很详细。
2.4G方案是目前无线的最低延迟吗? 给技术帝赞一个,有半成品发布吗,自己焊接改造就可以,编程实在不会啊:'( 非常好,楼主是技术帝,到目前我都还没开发fc的无线套件,等空了,开发一套套件,等玩家自己组装。 xunxun 发表于 2019-5-25 12:07
精彩,楼主动手强,也写得很详细。
2.4G方案是目前无线的最低延迟吗?
我没用过其他无线手柄,所以实在不知道我自己做的这个手柄到底是不是最低延迟。
不过根据网上的说法,延迟高低主要是由通信协议的复杂程度,以及数据传输速率决定的。这个模块虽然是2.4G无线模块,但由于协议很简单,所以延迟比较低。
我用这个手柄玩一些ACT游戏,比如超级马里奥、忍者龙剑传、热血系列,完全感觉不到延迟,和游戏机自带的手柄没有任何区别。唯一的区别就是这个手柄是用火花山寨手柄改造的,外壳是垃圾回收塑料,粗糙+脆弱,导电橡胶弹性不佳,手感不是一般的差。
我手里还有十几张板子,等我有空了我再做一套,到时候买一个好一点的手柄改造。 ppad 发表于 2019-5-28 17:27
给技术帝赞一个,有半成品发布吗,自己焊接改造就可以,编程实在不会啊 ...
很可惜,我手里没有多余的单片机了,LDO也找不到了,锂电池以及充电模块更是没有了。反正就是该用到的元件基本都没了,所以没法发布半成品。
两个板子倒是有十几张剩余。
你要是有USB转TTL串口板,完全可以自己买这两种单片机,然后我给你编译好的固件,你自己用串口板下载到单片机里面就行了。两个2.4G无线模块也得通过串口板用AT命令设置参数,不过好在我在设计电路的时候就已经预留好了串口,可以直接在线下载和在线设置参数。总的来说并不麻烦。 孙大师 发表于 2019-5-28 18:55
非常好,楼主是技术帝,到目前我都还没开发fc的无线套件,等空了,开发一套套件,等玩家自己组装。 ...
能得到孙大师的赞誉,不胜荣幸!
不如开发一个多功能手柄,比如用SFC手柄改造,L/R键配合其他组合键可以用来切换2P~4P、调节连发速度、软重启等各种功能。接收端插到15PIN扩展口。