基于STM32和PS2的遥控无线小车

这篇博客介绍了如何使用STM32F103C8T6微控制器和PS2遥控器搭建一个无线小车。内容包括所需的材料清单、硬件连接、关键程序代码以及调试技巧。提供了详细的淘宝购买链接和源码获取方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

欢迎大家关注博主的公众号 大海电子,在大海电子公众号里会和大家分享我的一些看法和知识。

不定期的会发布一些有趣的硬件作品及电路分析、软件源码分享。

需要以下源码工程请扫码关注大海电子,回复“基于STM32F103C8T6的PS2遥控小车源码及接线”,建议直接复制双引号里面的内容。(免费)

一、所需材料

整个工程文件:(淘宝购买)

https://item.taobao.com/item.htm?spm=a1z38n.10677092.0.0.4c651deb7q5Hra&id=582278394068

1、小车底盘

 2WD(2驱):

https://item.taobao.com/item.htm?spm=a1z10.3-c-s.w4002-17409464480.12.fff43961B3dZVc&id=567079436225

4WD(4驱):

https://item.taobao.com/item.htm?spm=a1z10.3-c-s.w4002-17409464480.9.fff43961dgCABI&id=567173416961

2、STM32F103C8T6核心板

https://item.taobao.com/item.htm?spm=a1z10.3-c-s.w4002-17409464480.9.fff43961vbraDx&id=567200541464

3、PS2遥控器

https://item.taobao.com/item.htm?spm=a1z10.3-c-s.w4002-17409464480.15.fff43961KPX3S0&id=577723467175

4、L298N电机驱动模块

https://item.taobao.com/item.htm?spm=a1z10.3-c-s.w4002-17409464480.9.fff43961Oz8Sab&id=567173392291

5、18650电池

https://item.taobao.com/item.htm?spm=a1z10.3-c-s.w4002-17409464480.12.fff43961RsCgQd&id=577486077956

(5号充电电池)

https://item.taobao.com/item.htm?spm=a1z10.3-c-s.w4002-17409464480.12.fff43961dqhPEU&id=577324372710

6、18650电池座

https://item.taobao.com/item.htm?spm=a1z10.3-c-s.w4002-17409464480.9.437c3961h9AdcC&id=568450483270

(5号电池座,底盘自带)

https://item.taobao.com/item.htm?spm=a1z10.3-c-s.w4002-17409464480.9.fff43961dqhPEU&id=567280122150

7、杜邦线若干

https://item.taobao.com/item.htm?spm=a1z10.3-c-s.w4002-17409464480.9.fff43961ozUBpi&id=567071877141

二、硬件连接

三、程序源码

Main.c

#include "led.h"

#include "delay.h"

#include "sys.h"

#include "usart.h"  

#include "pstwo.h"

/*********************************************************

功能:通过PS2控制小车运动

作者:大海电子

时间:180531

硬件平台:STM32F103C8T6

时钟:8Mhz

硬件引脚分配:

//手柄接口

DI=PB12;

DO=PB13 ;

CS=PB14;

CLK=PB15;

//L298N 电机驱动控制引脚

IN1=PA0;    //左侧电机正  

IN2=PA1; //左侧电机负  

IN3=PA2;    //右侧电机正    

IN4=PA3; //右侧电机负

**********************************************************/  

#define go {IN1=1;IN2=0;IN3=0;IN4=1;}

#define back {IN1=0;IN2=1;IN3=1;IN4=0;}

#define stop {IN1=1;IN2=1;IN3=1;IN4=1;}

#define turnleft {IN1=1;IN2=1;IN3=0;IN4=1;}

#define turnright {IN1=1;IN2=0;IN3=1;IN4=1;}

 int main(void)

 {  

 u8 key=0;

delay_init();       //延时函数初始化   

NVIC_Configuration();  //设置NVIC中断分组2:2位抢占优先级,2位响应优先级

uart_init(9600);   //串口初始化为9600

  LED_Init();      //LED端口初始化

PS2_Init();  

     

while(1)

{

key=PS2_DataKey();

 switch(key)

 {

 case 5:go;break;

 case 7:back;break;

 case 8:turnleft;break;

 case 6:turnright;break;

 default:stop;break;  

 }

if(key!=0)                   //有按键按下

     {

printf("%d  is  pressed  \r\n",key);

     }

// printf(" %5d %5d %5d %5d\r\n",PS2_AnologData(PSS_LX),PS2_AnologData(PSS_LY),

//                               PS2_AnologData(PSS_RX),PS2_AnologData(PSS_RY) );

       delay_ms(50);

}  

 }

pstwo.c

#include "pstwo.h"

#include "usart.h"

u16 Handkey;

u8 Comd[2]={0x01,0x42}; //开始命令。请求数据

u8 Data[9]={0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}; //数据存储数组

u16 MASK[]={

    PSB_SELECT,

    PSB_L3,

    PSB_R3 ,

    PSB_START,

    PSB_PAD_UP,

    PSB_PAD_RIGHT,

    PSB_PAD_DOWN,

    PSB_PAD_LEFT,

    PSB_L2,

    PSB_R2,

    PSB_L1,

    PSB_R1 ,

    PSB_GREEN,

    PSB_RED,

    PSB_BLUE,

    PSB_PINK

}; //按键值与按键明

//手柄接口初始化    输入  DI->PB12

//                  输出  DO->PB13    CS->PB14  CLK->PB15

void PS2_Init(void)

{

    //输入  DI->PB12

RCC->APB2ENR|=1<<3;     //使能PORTB时钟

GPIOB->CRH&=0XFFF0FFFF;//PB12设置成输入 默认下拉  

GPIOB->CRH|=0X00080000;   

    //  DO->PB13    CS->PB14  CLK->PB15

RCC->APB2ENR|=1<<3;    //使能PORTB时钟           

GPIOB->CRH&=0X000FFFFF;

GPIOB->CRH|=0X33300000;//PB13、PB14、PB15 推挽输出        

}

//向手柄发送命令

void PS2_Cmd(u8 CMD)

{

volatile u16 ref=0x01;

Data[1] = 0;

for(ref=0x01;ref<0x0100;ref<<=1)

{

if(ref&CMD)

{

DO_H;                   //输出以为控制位

}

else DO_L;

CLK_H;                        //时钟拉高

delay_us(50);

CLK_L;

delay_us(50);

CLK_H;

if(DI)

Data[1] = ref|Data[1];

}

}

//判断是否为红灯模式

//返回值;0,红灯模式

//   其他,其他模式

u8 PS2_RedLight(void)

{

CS_L;

PS2_Cmd(Comd[0]);  //开始命令

PS2_Cmd(Comd[1]);  //请求数据

CS_H;

if( Data[1] == 0X73)   return 0 ;

else return 1;

}

//读取手柄数据

void PS2_ReadData(void)

{

volatile u8 byte=0;

volatile u16 ref=0x01;

CS_L;

PS2_Cmd(Comd[0]);  //开始命令

PS2_Cmd(Comd[1]);  //请求数据

for(byte=2;byte<9;byte++)          //开始接受数据

{

for(ref=0x01;ref<0x100;ref<<=1)

{

CLK_H;

CLK_L;

delay_us(50);

CLK_H;

      if(DI)

      Data[byte] = ref|Data[byte];

}

        delay_us(50);

}

CS_H;

}

//对读出来的PS2的数据进行处理      只处理了按键部分         默认数据是红灯模式  只有一个按键按下时

//按下为0, 未按下为1

u8 PS2_DataKey()

{

u8 index;

PS2_ClearData();

PS2_ReadData();

Handkey=(Data[4]<<8)|Data[3];     //这是16个按键  按下为0, 未按下为1

for(index=0;index<16;index++)

{     

if((Handkey&(1<<(MASK[index]-1)))==0)

return index+1;

}

return 0;          //没有任何按键按下

}

//得到一个摇杆的模拟量  范围0~256

u8 PS2_AnologData(u8 button)

{

return Data[button];

}

//清除数据缓冲区

void PS2_ClearData()

{

u8 a;

for(a=0;a<9;a++)

Data[a]=0x00;

}

四、调试

  1. 若转动反向反了则说明电机正负或者左右反了,调整线和程序均可以解决。
  2. 建议使用18650锂电池供电,5号电池电压过低了些,若使用4驱方式,需要增加升压板,并且5号电池耗电很快。
  3. 上面的源码并非全部源码,简单的头文件等,LED及IO初始化等都为写入,如果需要完整的打开链接通过旺旺与我索要。

<think>我们面对的问题是:当使用PS2手柄控制小车时,手柄没有输入操作时,小车电机会出现抖动。这通常是由于手柄摇杆在中间位置时,读取的模拟值存在微小波动(噪声)导致的。由于小车的电机控制程序通常会将摇杆的模拟值映射为电机的速度,当摇杆值在零值附近波动时,电机就会反复正反转,从而产生抖动。 解决方案的核心是引入一个“死区”(Dead Zone)处理。死区是指在摇杆的中心位置附近设置一个区域,在这个区域内,摇杆的微小变化将被忽略,即输出值为零。只有当摇杆的偏移量超过死区范围时,才输出实际的映射值。 具体步骤: 1. 获取PS2手柄摇杆的原始值。通常摇杆的XY轴的值范围是0-255(或类似范围),中心位置在128附近(具体取决于手柄型号库的实现)。但实际中心值可能因手柄不同而略有偏差,需要根据实际情况调整。 2. 设置死区阈值。例如,可以设定一个死区范围,假设为±10。也就是说,当摇杆的值在[118, 138]之间(以128为中心)时,认为摇杆处于中间位置,输出0。 3. 对摇杆值进行死区处理。如果摇杆值在死区内,则将其置为中间值(如128),然后映射到0;如果超出死区,则按比例映射到电机的速度范围。 4. 将处理后的值转换为电机速度。通常,电机速度范围是-255到255(对应全速反转正转),但也可以根据实际需求调整。 5. 应用电机速度。注意,在设置电机速度时,应避免在0速度附近设置过小的非零值,因为过小的速度可能不足以驱动电机转动,反而引起振动。 下面是一个伪代码示例,展示如何实现死区处理: ```python # 假设读取的摇杆值xy,范围0-255,中心值128 def apply_deadzone(value, center=128, deadzone=10): if abs(value - center) < deadzone: return center # 返回中心值,后续映射为0 return value # 读取原始摇杆值 raw_x = ps2.get_joystick_x() # 假设这样获取 raw_y = ps2.get_joystick_y() # 应用死区 deadzone = 10 center = 128 x = apply_deadzone(raw_x, center, deadzone) y = apply_deadzone(raw_y, center, deadzone) # 将摇杆值映射到电机速度(例如,将x轴用于控制左右电机的差速,或者y轴用于前进后退) # 这里以y轴控制前进后退为例,将y轴的值从[0,255]映射到[-255,255] # 注意:死区处理后,中心值128会映射为0,而小于128大于128的部分分别映射为负正 speed = 0 if y != center: # 如果不在死区内 # 映射到-255到255,注意:当y=0时,speed=-255;y=255时,speed=255;y=128时,speed=0 speed = int((y - center) * 255 / (255 - center)) # 注意分母是255-128=127,但这样写不对,因为中心到两端的距离是127.5,但实际可用127或128作为分母的一半 # 更准确的映射方法:将[0, center]映射到[-255,0],将[center, 255]映射到[0,255] # 但是注意,死区处理后,y值不会在[center-deadzone, center+deadzone]内,所以我们可以这样: if y < center: # 从[0, center-deadzone]映射到[-255,0] speed = int(map_range(y, 0, center-deadzone, -255, 0)) else: # 从[center+deadzone, 255]映射到[0,255] speed = int(map_range(y, center+deadzone, 255, 0, 255)) # 其中map_range函数是一个线性映射函数,可以这样定义: def map_range(value, in_min, in_max, out_min, out_max): return (value - in_min) * (out_max - out_min) / (in_max - in_min) + out_min # 然后使用speed设置电机速度 ``` 注意:上面的映射方法在死区外是线性的,并且避开了死区内的值。另外,实际应用中,可能只需要一个轴(如y轴)来控制前进后退,另一个轴(x轴)控制转向。具体控制方式取决于小车的运动模型。 另一种更简洁的方式是直接计算偏移量,然后判断是否在死区内,然后映射: ```python # 定义死区函数,返回一个在[-1,1]区间的值,0表示中心,然后乘以最大速度 def deadzone_joystick(value, center=128, deadzone=10, max_speed=255): # 将原始值归一化到[-1,1]区间,中心为0 normalized = (value - center) / (center) # 因为从0到center,除以center得到[-1,1]?实际上center是128,那么分母应该是127.5,但我们可以用128作为分母的一半 # 更准确:分母用center(128)作为从中心到0或255的距离,所以归一化分母应该是128(因为0到128128到255都是128的跨度,但实际最大值是255,最小值0,中心128,所以从中心到一端的距离是127) # 因此归一化可以: (value - center) / (255 - center) 当value>center时,分母用255-center=127;当value<center时,分母用center=128?这样不对称 # 为了对称,我们使用127.5作为分母的一半,但整数处理可以统一用127(因为255-128=127,128-0=128,不对称,所以通常用127作为分母的一半,这样最大值归一化到1,最小值归一化到-128/127≈-1.0079,近似对称) # 所以归一化可以改为: (value - center) / 127.0 这样范围是[-128/127, 127/127] ≈ [-1.0079, 1] # 但为了简单,我们使用对称处理:假设摇杆从0到255,中心128,那么偏移量从-128到127,除以128(取绝对值最大为128)得到[-1, 0.992](近似),但通常可以接受。 # 因此归一化: normalized = (value - center) / 128.0 # 然后判断归一化后的绝对值是否小于死区的归一化值 deadzone_normalized = deadzone / 128.0 if abs(normalized) < deadzone_normalized: return 0 else: # 线性映射到[-max_speed, max_speed] # 注意:归一化值在[-1,1]区间(除了边界),我们将其缩放到[-max_speed, max_speed] return int(normalized * max_speed) # 使用: speed_y = deadzone_joystick(y, center=128, deadzone=10, max_speed=255) ``` 这种方法将死区处理归一化、映射结合在一起,更简洁。 另外,除了软件死区处理,还可以考虑硬件滤波,例如在读取摇杆值时使用软件滤波(如移动平均、卡尔曼滤波等)来平滑噪声。但死区是最直接有效的方法。 总结:解决PS2手柄未操作时小车电机抖动的问题,主要是在摇杆值读取后加入死区处理,忽略中心附近的微小波动。 注意事项: - 死区大小需要根据实际情况调整,过大会导致摇杆不灵敏,过小可能无法消除抖动。 - 不同手柄的摇杆中心值可能不同,需要校准(例如,有些手柄中心值可能不是128,可以在初始化时读取几次取平均作为中心值)。 如果问题仍然存在,可以检查: - 电源供应是否稳定,电压不足可能导致电机驱动不稳定。 - 电机驱动电路或电机驱动模块(如L298N、TB6612等)是否有问题。 - 程序其他部分是否有干扰。 但根据经验,死区处理是最常见的解决方案。 §§相关问题§§: 1. 如何校准PS2手柄摇杆的中心值? 2. 除了死区处理,还有哪些方法可以消除电机抖动? 3. PS2手柄与Arduino或其他微控制器的常见连接方式有哪些?</think>### PS2手柄未操作时小车电机抖动的解决方案 当PS2手柄无输入时电机抖动,通常由摇杆中心点漂移或信号噪声引起。以下是有效解决方法: **设置摇杆死区(Dead Zone)** 在代码中为摇杆值设置死区范围,忽略中心点附近的微小波动 ```python # 定义死区阈值(根据实际调整) DEADZONE = 10 # 处理摇杆值函数 def process_joystick(value): center = 128 # PS2摇杆中心值 if abs(value - center) < DEADZONE: return center # 返回中心值 return value # 应用死区 x_val = process_joystick(ps2.get_joystick_x()) y_val = process_joystick(ps2.get_joystick_y()) ``` **添加低通滤波** 使用移动平均滤波平滑信号,公式为: $$y[n] = \alpha \cdot x[n] + (1-\alpha) \cdot y[n-1]$$ 其中$\alpha$为滤波系数(0.1-0.3较佳) ```cpp // C++实现示例 float filtered_value = 0; const float alpha = 0.2; void loop() { int raw = ps2.readJoystick(); filtered_value = alpha * raw + (1 - alpha) * filtered_value; motor_control(filtered_value); } ``` **优化电机控制逻辑** - 当摇杆值在死区内时强制输出零速 - 设置最小启动电压阈值 - 使用电机使能引脚代替PWM调零 ```arduino if (abs(speed) < MIN_SPEED_THRESHOLD) { digitalWrite(MOTOR_EN_PIN, LOW); // 彻底关闭电机 } else { analogWrite(MOTOR_PWM_PIN, speed); } ``` **硬件改进措施** 1.PS2接收器与控制器间加装磁珠滤波器 2. 电机驱动电源与逻辑电源分离 3. 在电机引脚并联104电容吸收干扰 4. 使用屏蔽线连接手柄接收器 **信号校准流程** 1. 手柄静止时记录100次摇杆读数 2. 计算平均值作为中心基准值$C$ $$C = \frac{1}{N}\sum_{i=1}^{N} x_i$$ 3. 计算标准差$\sigma$确定死区范围 $$ \sigma = \sqrt{\frac{1}{N}\sum_{i=1}^{N}(x_i - C)^2} $$ 4. 设置死区为$C \pm 3\sigma$ > **注意**:当使用锂电池供电时,电压低于7.4V可能引发驱动芯片异常,需监测电源电压。
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值