基于STM32的PS2双轴按键摇杆模块驱动移植与电机控制实战
基于STM32的PS2双轴按键摇杆模块驱动移植与电机控制实战最近在做一个用摇杆控制小车的小项目需要用到PS2那种游戏手柄上的摇杆。网上买了个模块几块钱一个挺便宜的。但拿到手发现卖家给的代码不一定能在自己的STM32开发板上直接跑通。今天我就以手头的立创开发板STM32F103系列为例带你从零开始把这个双轴摇杆模块的驱动移植到自己的工程里并且把读取到的摇杆数据转换成电机的转速控制信号。整个过程我会把每一步的原理和踩过的坑都讲清楚。这篇文章非常适合刚学完STM32基础想动手做点小项目练手的同学。你会学到如何用ADC读取模拟电压、如何处理按键信号以及如何将这些输入信号转化为控制指令。咱们废话不多说直接开始。1. 认识你的“游戏手柄”PS2双轴摇杆模块咱们先来看看手上这个模块长啥样、怎么工作的。这能帮你理解后续的代码为什么要那么写。这个模块其实就是从PS2游戏手柄上拆下来的那个金属摇杆集成到了一块小板上。它有三个输出引脚VRX (X轴)输出一个模拟电压对应摇杆左右X轴的偏移。VRY (Y轴)输出一个模拟电压对应摇杆上下Y轴的偏移。SW (按键)输出一个数字信号对应你是否把摇杆垂直按下去就像手柄上的L3/R3键。它是怎么工作的呢摇杆本质上是一个双向的10K电位器可调电阻。当你推动摇杆时内部滑动触点的位置改变导致VRX和VRY引脚对地或对电源的电阻值发生变化。模块的电路会把这个电阻变化转换成电压变化输出。供电模块工作电压是3.3V到5V兼容性很好直接接STM32的3.3V就行。原始状态当摇杆在中间位置不推时VRX和VRY引脚的电压大约是供电电压的一半。比如用5V供电中间电压就是2.5V左右。推动时当你朝着一个方向推动摇杆对应轴的输出电压会从中间值向0V或供电电压变化。比如向左推VRX电压降低趋向0V向右推VRX电压升高趋向5V。所以我们的任务就是用STM32的ADC模数转换器去读取VRX和VRY的电压用GPIO去读取SW引脚的电平。2. 硬件连接与引脚分配在写代码之前得先把模块和开发板连起来。连接非常简单只需要四根线摇杆模块引脚连接到STM32的引脚作用说明VCC3.3V 或 5V电源正极接3.3V最方便GNDGND电源地VRX任选一个具有ADC功能的GPIO (如 PA1)X轴模拟电压输出VRY任选一个具有ADC功能的GPIO (如 PA2)Y轴模拟电压输出SW任选一个普通GPIO (如 PA3)按键数字信号输出注意STM32的ADC通道是固定在某些引脚上的比如ADC1的通道1对应PA1通道2对应PA2。你不能随便选一个PA5就当成ADC1的通道1来用。一定要查你所用芯片的数据手册或引脚定义图。在本文的示例代码中我们按照立创开发板的资料使用了以下引脚定义你移植时可以修改成自己板子上的引脚SW (按键)GPIOA_Pin_3VRX (X轴)GPIOA_Pin_1(对应ADC1通道1)VRY (Y轴)GPIOA_Pin_2(对应ADC1通道2)3. 手把手移植驱动代码现在进入核心环节——写代码。我会把代码拆开揉碎了讲确保你能看懂每一行是干什么的。3.1 头文件定义 (bsp_joystick.h)头文件的主要作用是进行宏定义把硬件相关的引脚、端口、通道信息都集中放在这里。这样以后换引脚只需要改这个头文件非常方便。#ifndef _BSP_JOYSTICK_H_ #define _BSP_JOYSTICK_H_ #include stm32f10x.h // 1. 按键SW的引脚定义 (使用GPIO读取) #define RCC_SW RCC_APB2Periph_GPIOA // SW所在端口的时钟 #define PORT_SW GPIOA // SW所在的端口 #define GPIO_SW GPIO_Pin_3 // SW具体的引脚号 // 2. 摇杆模拟引脚VRX/VRY的GPIO定义 (它们也属于GPIOA) #define RCC_VRXY RCC_APB2Periph_GPIOA #define PORT_VRXY GPIOA // 3. 摇杆模拟引脚对应的ADC定义 #define RCC_VRXY_ADC RCC_APB2Periph_ADC1 // 使用的ADC1的时钟 #define PORT_VRXY_ADC ADC1 // 使用的ADC外设 // 4. VRX引脚详细配置 (PA1) #define GPIO_VRX GPIO_Pin_1 // 硬件引脚 #define CHANNEL_VRX_ADC ADC_Channel_1 // 对应的ADC1通道1 // 5. VRY引脚详细配置 (PA2) #define GPIO_VRY GPIO_Pin_2 // 硬件引脚 #define CHANNEL_VRY_ADC ADC_Channel_2 // 对应的ADC1通道2 // 6. 采样次数为了滤波取多次采样的平均值 #define SAMPLES 30 // 7. 函数声明 void ADC_Joystick_Init(void); // 初始化函数 unsigned int Get_Joystick_Percentage_value(char dir); // 获取摇杆百分比值 char Get_SW_state(void); // 获取按键状态 #endif3.2 初始化函数 (ADC_Joystick_Init)这个函数在bsp_joystick.c里负责把用到的GPIO和ADC外设配置好。STM32的任何一个外设比如GPIO、ADC在使用前都必须先开启它的时钟然后进行参数配置。void ADC_Joystick_Init(void) { // 第一部分开启时钟 —— 给外设“上电” RCC_APB2PeriphClockCmd (RCC_SW, ENABLE); // 开启按键SW所在GPIO端口的时钟 RCC_APB2PeriphClockCmd (RCC_VRXY, ENABLE); // 开启VRX/VRY所在GPIO端口的时钟 RCC_APB2PeriphClockCmd (RCC_VRXY_ADC, ENABLE); // 开启ADC1的时钟 // 第二部分配置GPIO引脚模式 GPIO_InitTypeDef GPIO_InitStructure; // 配置VRX和VRY引脚为模拟输入模式(AIN) // 当GPIO用作ADC输入时必须设置为模拟输入避免内部电路干扰电压信号 GPIO_InitStructure.GPIO_Pin GPIO_VRX | GPIO_VRY; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 关键模拟输入模式 GPIO_Init(PORT_VRXY, GPIO_InitStructure); // 配置SW按键引脚为上拉输入模式(IPU) // 模块内部按键按下时SW引脚接地。设置为上拉未按下时读到的就是高电平(1)按下时是低电平(0)。 GPIO_InitStructure.GPIO_Pin GPIO_SW; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_Init(PORT_SW, GPIO_InitStructure); // 第三部分配置ADC外设 ADC_DeInit(PORT_VRXY_ADC); // 先将ADC1恢复为默认状态这是个好习惯 ADC_InitTypeDef ADC_InitStruct; ADC_InitStruct.ADC_Mode ADC_Mode_Independent; // 独立模式只用ADC1 ADC_InitStruct.ADC_ScanConvMode DISABLE; // 非扫描模式因为我们一次只转换一个通道 ADC_InitStruct.ADC_ContinuousConvMode ENABLE; // 开启连续转换模式ADC会不停地转换 ADC_InitStruct.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; // 软件触发不需要外部信号 ADC_InitStruct.ADC_DataAlign ADC_DataAlign_Right; // 转换结果数据右对齐方便阅读 ADC_InitStruct.ADC_NbrOfChannel 1; // 规则通道序列中要转换的通道数为1 ADC_Init(PORT_VRXY_ADC, ADC_InitStruct); // 将以上配置写入ADC1的寄存器 // 配置ADC时钟ADCCLK。STM32F103的ADC时钟不能超过14MHz这里设为PCLK2的6分频72MHz/612MHz RCC_ADCCLKConfig(RCC_PCLK2_Div6); ADC_Cmd(PORT_VRXY_ADC, ENABLE); // 最后使能ADC1 }3.3 读取ADC值函数 (Get_Adc_Joystick_Value)这个函数是内部调用的负责对指定的ADC通道进行一次完整的采样和转换并返回多次采样的平均值。unsigned int Get_Adc_Joystick_Value(char CHx) { uint32_t Data 0; // 1. 配置要转换的ADC通道和采样时间 // 参数ADCx, 通道号, 转换顺序(1表示第一个), 采样周期(时间越长越准但速度慢) ADC_RegularChannelConfig(PORT_VRXY_ADC, CHx, 1, ADC_SampleTime_55Cycles5); // 2. ADC校准非常重要不校准读数可能不准 ADC_ResetCalibration(PORT_VRXY_ADC); // 复位校准寄存器 while (ADC_GetResetCalibrationStatus(PORT_VRXY_ADC)); // 等待复位完成 ADC_StartCalibration(PORT_VRXY_ADC); // 开始校准 while (ADC_GetCalibrationStatus(PORT_VRXY_ADC)); // 等待校准完成 // 3. 启动转换并读取数据 ADC_SoftwareStartConvCmd(PORT_VRXY_ADC, ENABLE); // 软件触发一次转换 // 4. 多次采样取平均值可以滤除一些偶然的干扰毛刺 for(int i 0; i SAMPLES; i) { Data ADC_GetConversionValue(PORT_VRXY_ADC); // 读取当前转换结果 delay_ms(2); // 稍作延时确保下次采样是独立的 } Data Data / SAMPLES; // 计算平均值 return Data; // 返回12位的ADC原始值0~4095 }提示STM32的ADC是12位的所以转换结果范围是0到4095对应输入电压0V到3.3V参考电压。3.4 获取摇杆百分比与按键状态这是给上层应用调用的两个接口函数已经把原始的ADC值处理成更直观的数据了。// 获取摇杆偏移的百分比 (0%~100%) // 参数 dir: 0 读取X轴1 读取Y轴 unsigned int Get_Joystick_Percentage_value(char dir) { int adc_new 0; int Percentage_value 0; if( dir 0) { adc_new Get_Adc_Joystick_Value(CHANNEL_VRX_ADC); // 读X轴 } else { adc_new Get_Adc_Joystick_Value(CHANNEL_VRY_ADC); // 读Y轴 } // 将ADC原始值(0-4095)转换为百分比(0-100) Percentage_value ((float)adc_new / 4095.0f) * 100.0f; return Percentage_value; } // 获取摇杆按键状态 // 返回 0: 按键被按下 1: 按键未按下 char Get_SW_state(void) { // 读取SW引脚的电平。因为我们配置了上拉所以 // 按键未按下 - 引脚被上拉到高电平 - GPIO_ReadInputDataBit 返回 SET (1) // 按键按下 - 引脚被模块内部接地 - 返回 RESET (0) if( GPIO_ReadInputDataBit(PORT_SW, GPIO_SW) RESET ) { return 0; // 按下 } else { return 1; // 未按下 } }4. 在工程中测试摇杆驱动写好了怎么验证它工作正常呢我们写一个简单的main函数来测试。#include stm32f10x.h #include board.h // 你的开发板初始化函数 #include bsp_uart.h // 串口驱动用于打印 #include stdio.h #include bsp_joystick.h int main(void) { board_init(); // 初始化系统时钟、基本外设等 uart1_init(115200); // 初始化串口1波特率115200用于连接电脑串口助手 ADC_Joystick_Init(); // 初始化我们的摇杆模块 printf(PS2 Joystick Demo Start.....\r\n); while(1) { // 1. 检测按键 if( Get_SW_state() 0 ) { printf(Button Pressed!!\r\n); } // 2. 读取并打印X轴和Y轴的百分比位置 printf(X [%d%%]\r\n, Get_Joystick_Percentage_value(0)); printf(Y [%d%%]\r\n, Get_Joystick_Percentage_value(1)); printf(----------------\r\n); delay_ms(200); // 延时200ms避免打印太快看不清 } }上电后的效果 把代码下载到开发板打开串口助手如XCOM、SSCOM设置好波特率115200。你推动摇杆串口就会实时打印出X和Y轴的百分比位置。把摇杆垂直按下去会看到“Button Pressed!!”的提示。5. 进阶实战用摇杆控制电机转速读取到摇杆数据只是第一步我们的最终目标是控制电机。假设我们控制一个直流电机速度由PWM的占空比决定0%-100%。那么摇杆的百分比值就可以直接映射为PWM的占空比。这里给出一个概念性的代码片段假设你已经写好了PWM初始化函数PWM_Init()和设置占空比的函数Set_Motor_Speed(int speed_percentage)。// 在main函数的循环中 while(1) { int speed_x, speed_y; // 获取摇杆X轴位置假设控制电机1 speed_x Get_Joystick_Percentage_value(0); // 获取摇杆Y轴位置假设控制电机2 speed_y Get_Joystick_Percentage_value(1); // 将百分比速度设置给电机 // 注意摇杆中间值是50%对应电机停止。需要做一个偏移计算。 // 例如speed_x (Get_Joystick_Percentage_value(0) - 50) * 2; // 这样摇杆在中间(50%)时speed_x为0在最左(0%)时speed_x为-100在最右(100%)时speed_x为100。 // 然后根据正负号控制电机方向绝对值控制速度。 Set_Motor_Speed(MOTOR_LEFT, speed_x); // 控制左电机 Set_Motor_Speed(MOTOR_RIGHT, speed_y); // 控制右电机 delay_ms(50); // 控制周期50ms更新一次速度 }关键点死区处理摇杆在中间可能有微小抖动导致ADC值在49%-51%之间跳动。可以设置一个“死区”比如48%-52%之间都认为是中位速度为零避免电机嗡嗡响却不走。映射关系摇杆的ADC值0-4095或百分比0-100需要根据你的电机驱动电路和算法映射成有效的PWM值。可能需要线性映射也可能需要更复杂的曲线如指数曲线来让操控感更好。滤波我们代码里已经做了简单的平均滤波。在实际移动平台上可能还需要加入软件低通滤波让速度变化更平滑避免电机急启急停。好了从模块原理、硬件连接、驱动编写到测试验证最后到控制电机的思路整个流程就完成了。你可以根据这个框架把它移植到任何一款STM32芯片上。最重要的不是照抄代码而是理解每一步为什么要这么做。遇到问题多查数据手册多用调试工具祝你玩得开心