一、FC游戏手柄介绍
提到FC游戏机手柄,想必80 90后的那批人必然会燃起激情了,那是我们难以忘记的童年!
在时序上,FC 手柄的控制电路由 1 个 8 位并入串出的移位寄存器(CD4021)和一个时基集成电路(NE555,用于连发)构成。

可采用 STM32 的 3 个普通 IO 连接 FC 手柄的 Clock、Data 和 Latch 信号,通过设置 IO 口状态,按照特定的时序读取手柄按键值。如先将 Latch 置 1 锁存当前状态,再置 0,然后在 Clock 的作用下,依次读取 A、B、SELECT、START、UP、DOWN、LEFT、RIGHT 这 8 个按键的键值,按下为 0,松开为 1。
二、FC游戏手柄驱动实现
驱动代码是2个手柄的,方便双人游戏!!
1.FC游戏手柄相关宏定义
/*
手柄DB9母头定义
母头 ---> 公头
DATA ---> DB9_2 ---> DB9_4 GPIOA_2
LATCH ---> DB9_3 ---> DB9_3 GPIOA_3
CLOCK ---> DB9_4 ---> DB9_2 GPIOA_4
+5V ---> DB9_6 ---> DB9_9
GND ---> DB9_8 ---> DB9_7
*/
#define JOYPAD1 1
#define JOYPAD2 2
#define JoyPad_Delay_us 0
/* JOYPAD1引脚 定义 */
#define JOYPAD1_CLK_GPIO_PORT GPIOB
#define JOYPAD1_CLK_GPIO_PIN GPIO_PIN_13
#define JOYPAD1_CLK_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* JOYPAD1_CLK口时钟使能 */
#define JOYPAD1_LAT_GPIO_PORT GPIOB
#define JOYPAD1_LAT_GPIO_PIN GPIO_PIN_14
#define JOYPAD1_LAT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* JOYPAD1_LAT口时钟使能 */
#define JOYPAD1_DATA_GPIO_PORT GPIOD
#define JOYPAD1_DATA_GPIO_PIN GPIO_PIN_13
#define JOYPAD1_DATA_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOD_CLK_ENABLE(); }while(0) /* JOYPAD1_DATA口时钟使能 */
/* JOYPAD2引脚 定义 */
#define JOYPAD2_CLK_GPIO_PORT GPIOB
#define JOYPAD2_CLK_GPIO_PIN GPIO_PIN_2
#define JOYPAD2_CLK_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* JOYPAD2_CLK口时钟使能 */
#define JOYPAD2_LAT_GPIO_PORT GPIOC
#define JOYPAD2_LAT_GPIO_PIN GPIO_PIN_4
#define JOYPAD2_LAT_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0) /* JOYPAD2_LAT口时钟使能 */
#define JOYPAD2_DATA_GPIO_PORT GPIOB
#define JOYPAD2_DATA_GPIO_PIN GPIO_PIN_1
#define JOYPAD2_DATA_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* JOYPAD2_DATA口时钟使能 */
/* JOYPAD1手柄连接引脚 */
#define JOYPAD1_CLK(x) do{ x ? \
HAL_GPIO_WritePin(JOYPAD1_CLK_GPIO_PORT, JOYPAD1_CLK_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(JOYPAD1_CLK_GPIO_PORT, JOYPAD1_CLK_GPIO_PIN, GPIO_PIN_RESET); \
}while(0) /* JOYPAD_CLK */
#define JOYPAD1_LAT(x) do{ x ? \
HAL_GPIO_WritePin(JOYPAD1_LAT_GPIO_PORT, JOYPAD1_LAT_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(JOYPAD1_LAT_GPIO_PORT, JOYPAD1_LAT_GPIO_PIN, GPIO_PIN_RESET); \
}while(0) /* JOYPAD_LATCH */
#define JOYPAD1_DATA HAL_GPIO_ReadPin(JOYPAD1_DATA_GPIO_PORT, JOYPAD1_DATA_GPIO_PIN) /* JOYPAD_DATA */
/* JOYPAD2手柄连接引脚 */
#define JOYPAD2_CLK(x) do{ x ? \
HAL_GPIO_WritePin(JOYPAD2_CLK_GPIO_PORT, JOYPAD2_CLK_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(JOYPAD2_CLK_GPIO_PORT, JOYPAD2_CLK_GPIO_PIN, GPIO_PIN_RESET); \
}while(0) /* JOYPAD_CLK */
#define JOYPAD2_LAT(x) do{ x ? \
HAL_GPIO_WritePin(JOYPAD2_LAT_GPIO_PORT, JOYPAD2_LAT_GPIO_PIN, GPIO_PIN_SET) : \
HAL_GPIO_WritePin(JOYPAD2_LAT_GPIO_PORT, JOYPAD2_LAT_GPIO_PIN, GPIO_PIN_RESET); \
}while(0) /* JOYPAD_LATCH */
#define JOYPAD2_DATA HAL_GPIO_ReadPin(JOYPAD2_DATA_GPIO_PORT, JOYPAD2_DATA_GPIO_PIN) /* JOYPAD_DATA */
2.FC游戏手柄GPIO初始化
/**
* @brief 初始化手柄接口
* @param 无
* @retval 无
*/
void joypadInit(uint8_t joypadNum)
{
GPIO_InitTypeDef gpio_init_struct = {0};
if(JOYPAD1 == joypadNum)
{
JOYPAD1_CLK_GPIO_CLK_ENABLE(); /* CLK 所在IO时钟初始化 */
JOYPAD1_LAT_GPIO_CLK_ENABLE(); /* LATCH 所在IO时钟初始化 */
JOYPAD1_DATA_GPIO_CLK_ENABLE(); /* DATA 所在IO时钟初始化 */
gpio_init_struct.Pin = JOYPAD1_CLK_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(JOYPAD1_CLK_GPIO_PORT, &gpio_init_struct); /* JOYPAD1_CLK 引脚模式设置 */
gpio_init_struct.Pin = JOYPAD1_LAT_GPIO_PIN;
HAL_GPIO_Init(JOYPAD1_LAT_GPIO_PORT, &gpio_init_struct); /* JOYPAD1_LAT 引脚模式设置 */
gpio_init_struct.Pin = JOYPAD1_DATA_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_INPUT;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(JOYPAD1_DATA_GPIO_PORT, &gpio_init_struct); /* JOYPAD1_DATA 引脚模式设置 */
printf("joypad1 init ok !\r\n");
}
else if(JOYPAD2 == joypadNum)
{
JOYPAD2_CLK_GPIO_CLK_ENABLE(); /* CLK 所在IO时钟初始化 */
JOYPAD2_LAT_GPIO_CLK_ENABLE(); /* LATCH 所在IO时钟初始化 */
JOYPAD2_DATA_GPIO_CLK_ENABLE(); /* DATA 所在IO时钟初始化 */
gpio_init_struct.Pin = JOYPAD2_CLK_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(JOYPAD2_CLK_GPIO_PORT, &gpio_init_struct); /* JOYPAD1_CLK 引脚模式设置 */
gpio_init_struct.Pin = JOYPAD2_LAT_GPIO_PIN;
HAL_GPIO_Init(JOYPAD2_LAT_GPIO_PORT, &gpio_init_struct); /* JOYPAD1_LAT 引脚模式设置 */
gpio_init_struct.Pin = JOYPAD2_DATA_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_INPUT;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_MEDIUM;
HAL_GPIO_Init(JOYPAD2_DATA_GPIO_PORT, &gpio_init_struct); /* JOYPAD1_DATA 引脚模式设置 */
printf("joypad2 init ok !\r\n");
}
}
3.FC游戏手柄键值获取
/**
* @brief 读取手柄按键值
* @note FC手柄数据输出格式:
* 每给一个脉冲,输出一位数据,输出顺序:
* A -> B -> SELECT -> START -> UP -> DOWN -> LEFT -> RIGHT.
* 总共8位, 对于有C按钮的手柄, 按下C其实就等于 A + B 同时按下.
* 按下是1,松开是0.
* @param 无
* @retval 按键结果, 格式如下:
* [7]:右
* [6]:左
* [5]:下
* [4]:上
* [3]:Start
* [2]:Select
* [1]:B
* [0]:A
*/
uint8_t joypadRead(uint8_t joypadNum)
{
volatile uint8_t temp = 0;
uint8_t t;
if(JOYPAD1 == joypadNum)
{
JOYPAD1_LAT(1); /* 锁存当前状态 */
delay_us(JoyPad_Delay_us);
JOYPAD1_LAT(0);
for (t = 0; t < 8; t++) /* 移位输出数据 */
{
temp >>= 1;
if (JOYPAD1_DATA == 0)
{
temp |= 0x80; /* LOAD之后,就得到第一个数据 */
}
JOYPAD1_CLK(1); /* 每给一次脉冲,收到一个数据 */
delay_us(JoyPad_Delay_us);
JOYPAD1_CLK(0);
delay_us(JoyPad_Delay_us);
}
}
else if(JOYPAD2 == joypadNum)
{
JOYPAD2_LAT(1); /* 锁存当前状态 */
delay_us(JoyPad_Delay_us);
JOYPAD2_LAT(0);
for (t = 0; t < 8; t++) /* 移位输出数据 */
{
temp >>= 1;
if (JOYPAD2_DATA == 0)
{
temp |= 0x80; /* LOAD之后,就得到第一个数据 */
}
JOYPAD2_CLK(1); /* 每给一次脉冲,收到一个数据 */
delay_us(JoyPad_Delay_us);
JOYPAD2_CLK(0);
delay_us(JoyPad_Delay_us);
}
}
// printf("joypadNum%d=0x%X\r\n",joypadNum,temp);
return temp;
}
三、游戏体验
