
简介一套基于STM32F103与MPU6050的体感遥控车完整项目资料包面向嵌入式学习者和电子设计爱好者旨在改善传统按键遥控的繁琐操作。项目同时支持体感遥感与推杆两种控制模式利用手势姿态替代按键输入覆盖原理图设计、PCB绘制到嵌入式软件开发的完整流程。资料包共449个文件包含STM32的C/H源码、Keil工程文件、Altium Designer原理图/PCB文档、PDF设计说明及3D模型文件等整体约56.2MB目录按3D模型、硬件、软件模块清晰划分此外还提供系统功能框图与遥控器流程图便于理解整体设计方案。目前已有573人学习下载通过该包可获得车载主控板与遥控器的电路设计、完整的STM32工程源代码以及基于MPU6050的姿态检测与控制实现思路适合用作课程设计、毕业设计或项目练手的参考。1. 体感遥控车按一下午按键不如晃动一个MPU6050玩具车的遥控器如果还是一排按键拿到手的新鲜感撑不过十分钟。这个项目把遥控器换成了一块MPU6050姿态传感器手向前倾车就前进手腕往左拧车头就左转。本质上是把“按键事件”变成了“姿态连续量”再用STM32F103读取、组帧、无线发到车体。更特别的是它保留了推杆摇杆模式一套遥控器、两种操控手感切换只靠一个按键。这个项目适合正在做嵌入式课程设计、想把陀螺仪真正接到电机控制环路里的人也适合想搞清楚MPU6050姿态解算中角速度积分漂移怎么被压下去的开发者。下面从传感器读取开始逐步把这条路走通。2. MPU6050姿态解算从I2C读原始数据到角度融合2.1 为什么选MPU6050而不是9轴或传统按键MPU6050是六轴传感器内部集成了三轴加速度计和三轴陀螺仪通过I2C接口输出原始数据。相比九轴传感器它少了一颗磁力计也就少了地磁校准的那套流程对手势遥控这种短时间、大角度的场景来说已经足够。九轴的优势在于长时间航向不漂移但遥控车一局操作不过几分钟遥控器拿在手里也不需要用罗盘去校正方向。反直觉的一点是陀螺仪单独积分出来的角度会越漂越离谱而加速度计测静态角度又会在运动时被振动污染。所以项目里不能直接把加速计算出的角度拿来用也不能只信陀螺仪的角速度积分。正确做法是把两者融合。常见方案是MPU6050内置DMP直接输出四元数但在遥控器这种要求低延迟、逻辑可控的场景我一般倾向于自己用一阶互补滤波做省掉DMP库的移植和内存开销。2.2 硬件接口与寄存器初始化遥控器端的接线非常固定MPU6050的VCC接3.3VGND接GNDSCL和SDA分别接STM32F103的PB6、PB7I2C1AD0接低电平这样器件地址为0x68。注意STM32F103的I2C模块在总线超时处理上容易把人逼疯许多人会改用软件模拟I2C但这里用硬件I2C也可以只要加一个超时判断。接口关系如下引脚连接到STM32说明VCC3.3V不可接5V会烧模块GNDGND共地SCLPB6I2C1_SCL上拉4.7kΩSDAPB7I2C1_SDA上拉4.7kΩAD0GND选择0x68地址初始化代码里最关键的几个寄存器是电源管理、采样率、滤波带宽。下面是一段适用于STM32标准外设库和HAL库共用的初始化函数使用寄存器地址直接操作。#define MPU6050_ADDR 0xD0 // 0x68左移一位 void MPU6050_Init(void) { I2C_WriteByte(MPU6050_ADDR, 0x6B, 0x00); // PWR_MGMT_1解除休眠 I2C_WriteByte(MPU6050_ADDR, 0x19, 0x07); // SMPLRT_DIV采样率分频 I2C_WriteByte(MPU6050_ADDR, 0x1A, 0x06); // CONFIG低通滤波42Hz I2C_WriteByte(MPU6050_ADDR, 0x1B, 0x18); // GYRO_CONFIG±2000°/s I2C_WriteByte(MPU6050_ADDR, 0x1C, 0x08); // ACCEL_CONFIG±4g }这里每个寄存器都需要解释0x6B写入0x00是让芯片从休眠模式唤醒写0x40是复位实际初始化时先复位再解除休眠更稳定0x19样本率分频为7表示输出频率为1kHz除以8也就是125Hz这个频率对遥控协议更新足够0x1A低通滤波设置为6时对应42Hz截止频率可以有效滤除高频机械振动陀螺仪量程选择±2000°/s是因为手势挥动时的瞬时角速度可能超过500°/s而量程越小灵敏度越高需要根据实际动作取舍加速度计量程选±4g可以在快速倾斜时避免饱和。2.3 读取加速度和陀螺仪原始数据初始化完成后只需要循环读取数据寄存器。加速度计原始值的高字节从0x3B开始陀螺仪从0x43开始。下面是读取函数。typedef struct { int16_t acc_x, acc_y, acc_z; int16_t gyro_x, gyro_y, gyro_z; } MPU6050_Data; void MPU6050_ReadRaw(MPU6050_Data *d) { uint8_t buf[14]; I2C_ReadBytes(MPU6050_ADDR, 0x3B, buf, 14); // 一次读出加速度温度陀螺仪 d-acc_x (buf[0] 8) | buf[1]; d-acc_y (buf[2] 8) | buf[3]; d-acc_z (buf[4] 8) | buf[5]; d-gyro_x (buf[8] 8) | buf[9]; d-gyro_y (buf[10] 8) | buf[11]; d-gyro_z (buf[12] 8) | buf[13]; }这段代码的作用是连续读取14字节因为MPU6050的寄存器地址是自动递增的从0x3B开始读一次就能拿到加速度、温度、陀螺仪的全部数据省去多次I2C通信的开销。注意buf[6]和buf[7]是温度本项目用不到但读取时不能跳过。原始值是16位有符号数要换算成物理量需要除以对应量程的满刻度系数±2000°/s对应16.4 LSB/°/s±4g对应8192 LSB/g。注意I2C读时序必须在发送寄存器地址后加入重复起始信号否则器件不回复数据。2.4 一阶互补滤波计算姿态角MPU6050姿态解算的常见思路有三种直接读DMP四元数、用Mahony算法、用一阶互补滤波。这里用手势遥控场景要求响应快而非绝对精度所以选用一阶互补滤波即可。基本原理是加速度计计算出的角度在低频段可信陀螺仪积分出的角度在高频段可信把两者以系数α加权。#define alpha 0.96f #define dt 0.008f // 125Hz采样间隔 float pitch_angle, roll_angle; void MPU6050_UpdateFilter(void) { MPU6050_Data raw; MPU6050_ReadRaw(raw); float acc_pitch atan2f(raw.acc_y, raw.acc_z) * 57.29578f; float acc_roll atan2f(raw.acc_x, raw.acc_z) * 57.29578f; float gyro_pitch raw.gyro_x / 16.4f; float gyro_roll raw.gyro_y / 16.4f; pitch_angle alpha * (pitch_angle gyro_pitch * dt) (1 - alpha) * acc_pitch; roll_angle alpha * (roll_angle gyro_roll * dt) (1 - alpha) * acc_roll; }融合逻辑很直白每次递推先用陀螺仪角速度乘以dt做积分再用加速度计角度做长期修正。alpha越接近1姿态角越平滑但滞后越大alpha越小响应越快但噪声越大。实际调试时我先把alpha设为0.960.8ms的采样周期下手转90度滤波后的角度大约滞后30ms这个延迟在遥控链路里感知不强。关键点是atan2f要用y和z轴计算俯仰x和z轴计算横滚而yaw不能靠加速度计计算所以偏航角在遥控车里不能直接用互补滤波只能靠陀螺仪积分这也是为什么实际项目中偏航方向容易漂移通常不把yaw作为长时间稳定依据。3. 遥控器双模式切换与无线协议帧设计3.1 遥控器主循环推杆与手势模式的无缝切换遥控器端的MCU承担三个任务读取MPU6050姿态角、读取摇杆电压、把数据打包发给车体。项目里“遥感”模式指手势模式“推杆”模式指传统摇杆。主循环要实时检测按键切换同时根据模式选择不同的数据来源。推杆模式下摇杆电位器输出的电压经过ADC转换后得到0到4095的原始值需要映射到-1024到1024的有符号范围手势模式下MPU6050的pitch角映射成油门roll角映射成转向。两者共用同一个无线数据帧只是填充方式不同。while (1) { MPU6050_UpdateFilter(); if (KEY_Scan() MODE_KEY) { control_mode !control_mode; // 切换手势/推杆 } Rc_Data_t rc; if (control_mode 0) // 手势模式 { rc.speed (int16_t)(pitch_angle * 12); // 前倾为正 rc.steer (int16_t)(roll_angle * 8); // 右倾为正 } else // 推杆模式 { rc.speed (int16_t)((ADC_Read(CH1) - 2048) / 2); rc.steer (int16_t)((ADC_Read(CH2) - 2048) / 2); } rc.mode control_mode; Radio_SendFrame(rc); delay_ms(8); }这段循环里有几个容易出问题的点。第一是pitch_angle乘以12后手前倾30度speed就有360的数值恰好落在-1024到1024范围内如果乘数太大高角度会饱和车子一直全速很危险。第二是roll_angle乘以8映射到转向时要注意左右方向是否与人的直觉一致。第三是按键扫描必须做消抖我用的是状态机扫描方式连续两次读到按键状态一致才切换。注意ADC读取摇杆和MPU6050姿态更新共用同一个延时周期若把delay_ms改成systick调度更好控制时间同步。3.2 无线链路选型为什么用NRF24L01项目中车体和遥控器之间需要低延迟通信常见选择有蓝牙、Wi-Fi和NRF24L01。蓝牙模块的从机配对和主机模式配置在量产中比较麻烦传统蓝牙延迟在15ms到40ms之间Wi-Fi延迟更高且功耗大NRF24L01是一颗2.4GHz射频芯片SPI接口在2Mbps速率下单向空中延迟不到1ms实际加上MCU处理也能做到3ms以内。所以这里采用NRF24L01作为链路。它的缺点是只有125个频道在2.4GHz拥挤环境中需要重发机制但对于一台玩具车来说一分钟内偶尔丢掉一帧不会造成可感知的影响。3.3 数据帧格式定义与收发解析无线数据必须定义帧结构不能裸发否则接收端无法判断一帧从哪开始。我用一个固定的结构体在C语言里直接强转成字节数组。typedef struct { uint8_t head; // 0xAA 帧头 uint8_t mode; // 0手势 1推杆 int16_t speed; // 油门-1024~1024 int16_t steer; // 转向-1024~1024 uint8_t checksum; // 校验和 } Rc_Data_t;发送端按结构体顺序填充并计算校验和head到steer的最后一个字节累加。接收端拿到数据后先判断head是否为0xAA再校验checksum合法才更新PWM。注意结构体可能有对齐问题如果编译器设置了字节对齐为4结构体大小会变成12而不是8两边必须一致。我一般在代码里使用#pragma pack(1)消除对齐。校验和的计算和判断代码很轻量如下。uint8_t Rc_Checksum(Rc_Data_t *rc) { uint8_t ck 0; ck rc-head; ck rc-mode; ck (uint8_t)(rc-speed 0xFF); ck (uint8_t)(rc-speed 8); ck (uint8_t)(rc-steer 0xFF); ck (uint8_t)(rc-steer 8); return ck; }发送方把checksum填入结构体倒数第二个字节。接收方每收到一帧就重新计算checksum并与最后一字节比对不相等就丢弃。这能挡住大部分无线干扰产生的坏帧。注意NRF24L01的payload长度要设成结构体大小且自动应答关闭时接收端需要在每次收到数据后清空RX_DR标志位否则中断会一直触发。实际调试中我把payload长度固定为8字节和结构体完全一致避免NRF24L01在缓冲区里读到旧数据。3.4 NRF24L01收发配置要点NRF24L01使用SPISTC或STM32的硬件SPI频率建议在8MHz以内。无线速度选择2Mbps空中延时最短。发射功率选0dBm玩具车场景十几米内足够。配置寄存器时发送端和接收端的地址和无线频道必须一致管脚CE需要控制收发状态。标准步骤是初始化SPI写TX_ADDR和RX_ADDR写RF_CH、RF_SETUP、CONFIG最后把CE拉高进入接收或发送模式。void RF_Init(void) { SPI_WriteReg(0x00, 0x3E); // CONFIG: 上使能CRC16位 SPI_WriteReg(0x05, 0x00); // RF_CH: 2.400GHz SPI_WriteReg(0x06, 0x0E); // RF_SETUP: 2Mbps, 0dBm SPI_WriteReg(0x11, 0x02); // RX_PW_P0: 8字节 }寄存器含义是CONFIG中的EN_CRC和CRCO位决定CRC校验必须开启RF_CH写入0表示2400MHz实际通信时两边都要设成同一个频道比如写20就跳到2420MHz避开Wi-Fi低频段RF_SETUP里RF_DR为0时选2MbpsRF_PWR为11时对应0dBm发射功率RX_PW_P0设为8和帧长度一致。注意如果接收端清RX_DR标志位太慢或者把CE和CSN引脚顺序搞反会出现“error: no stm32 target found”这类调试器连接正常但射频不工作的怪现象先检查引脚初始化。4. 车载主控板的PWM电机控制与差速转向实现4.1 车载端硬件组成车体端主控仍然是STM32F103它负责接收无线数据、解析帧、输出PWM给电机驱动。驱动芯片我使用的是TB6612FNG相比L298N它体积小、压降低更适合这种小遥控车。TB6612的PWMA和PWMB分别控制两个电机转速AIN1、AIN2和BIN1、BIN2控制转向。STM32F103的定时器输出PWM到这两个引脚同时两个GPIO控制方向。下表是常用引脚分配功能STM32引脚驱动芯片引脚左电机PWMPA8PWMA左电机方向1PB12AIN1左电机方向2PB13AIN2右电机PWMPA9PWMB右电机方向1PB14BIN1右电机方向2PB15BIN2选择PA8和PA9是为了用TIM1的两个通道这两个定时器有互补PWM输出不过这里只需要普通PWM用TIM1的CH1和CH2即可。注意不要跟I2C的PB6/PB7冲突遥控器端的引脚与车载端可以不同如果车体也用MPU6050则需重新分配。4.2 定时器PWM配置流程电机驱动需要20kHz左右的PWM频率以避开人耳可听噪声同时保证mos管开关损耗不至于太高。下面代码用STM32标准外设库配置TIM1输出PWM。void PWM_Init(void) { GPIO_InitTypeDef gpio; TIM_TimeBaseInitTypeDef tim; TIM_OCInitTypeDef oc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC | RCC_APB2Periph_TIM1, ENABLE); gpio.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_9; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); tim.TIM_Prescaler 72 - 1; // 72MHz/72 1MHz tim.TIM_CounterMode TIM_CounterMode_Up; tim.TIM_Period 50 - 1; // 1MHz/50 20kHz TIM_TimeBaseInit(TIM1, tim); oc.TIM_OCMode TIM_OCMode_PWM1; oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_Pulse 0; oc.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, oc); TIM_OC2Init(TIM1, oc); TIM_CtrlPWMOutputs(TIM1, ENABLE); // 高级定时器必须开主输出 }配置逻辑是先把72MHz系统时钟分频到1MHz计数器周期设为50得到20kHz的PWM频率和常见的舵机50Hz不同电机驱动更适合高频。TIM1是高级定时器比普通定时器多一步TIM_CtrlPWMOutputs如果不开启MOE引脚PWM输出不会有波形。注意如果换用TIM2、TIM3则不需要这一句很多人把TIM1代码改到TIM3后忘记删除导致烧录后PWM无输出。PWM的占空比通过修改TIM_SetCompare1(TIM1, duty)和TIM_SetCompare2(TIM1, duty)控制duty范围0到50。车速控制逻辑是把接收到的speed值从-1024~1024映射到0~50同时根据正负决定方向引脚电平。4.3 手势转向与差速融合体感遥控车在转向时比较自然的操控是遥控器向右roll车上左右轮产生速度差实现右转。这里不能直接把steer加到左边、从右边减去需要先计算一个基准速度再做差速限幅。int16_t base_speed rc.speed; int16_t turn rc.steer; int16_t left_speed base_speed turn; int16_t right_speed base_speed - turn; left_speed CLAMP(left_speed, -1024, 1024); right_speed CLAMP(right_speed, -1024, 1024); Motor_SetSpeed(LEFT, left_speed); Motor_SetSpeed(RIGHT, right_speed);Motor_SetSpeed内部根据正负拆分方向和占空比。注意如果车是前轮转向型这套差速逻辑就要改成把转向量映射到舵机PWM我把这部分在代码里封装成MOTOR_TYPE宏用户可以根据自己的车架切换。常见错误是把左右speed直接赋给PWM比较寄存器没有考虑正负方向引脚正转和反转需要ATOMIC操作避免方向引脚和PWM值之间产生竞争。转向系数的调参也有讲究。假设手势roll角最大30度映射后steer最大240如果base_speed是600那么左轮840、右轮360差速比接近2.31转弯半径明显变小。如果base_speed只有200左轮440、右轮-40右边电机进入反转车子几乎原地旋转这在遥控车漂移时有趣但正常行驶容易失控。所以我一般把turn的范围限制在base_speed * 0.6以内保证同向行驶优先。4.4 电机驱动保护与电源处理车载端的电源拓扑特别容易被忽略电机瞬间堵转时电流可到2A而STM32和NRF24L01如果共用同一路5V电压会被拉低导致复位。正确做法是用一节2S锂电池经过DC-DC降到5V给逻辑供电电机从电池直接取电。TB6612的逻辑电源和电机电源要分开逻辑电源接5V电机电源接7.4V或根据电机额定电压选择。如果发现单片机偶尔重启用示波器观察VCC跌落波形多半是电源处理问题而不是代码问题。注意在电机引脚靠近驱动芯片的位置并联100nF陶瓷电容和一个470uF电解电容能明显减少PWM开关噪声对地平面的影响。5. 调试技巧用串口把姿态角和无线帧延迟一起拉出来写代码时最怕的不是逻辑错而是无从判断错在哪一步。这两个链路的交点可以用一个串口监视器同时验证在遥控器端把MPU6050滤波后的pitch、roll和发送的数据打印出来在车体端把接收到的speed、steer打印出来中间差多少毫秒就有数了。STM32F103只有一个串口时可以重定向printf到USART1使用下面的代码快速输出调试信息int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return ch; }调试时我在遥控器端每100ms打印一帧完整角度在车载端每100ms打印接收到的PWM值。把两个串口连到电脑不同串口或者用两个串口转USB模块就能对着屏幕来回晃动遥控器观察车端数据是否同步变化。如果车端数据变化但电机不动问题出在PWM配置或方向引脚如果车端数据根本不更新问题出在无线模块配置或帧头校验。姿态角验证有个技巧把遥控器平放在桌面上读取roll和pitch记录静止值。正常情况下加速度计的噪声应该在0.1g以内换算成角度误差不超过1度。手拿着快速晃动pitch变化应该跟手同步但不会出现剧烈跳变。如果角度在静止时缓慢漂移说明互补滤波的alpha太小或陀螺仪零点偏置没有校准。零点偏置可以在初始化后读取100次陀螺仪求平均值存到全局变量每次更新时减去这个偏移。最后再排查无线丢包。NRF24L01的自动重发次数寄存器SETUP_RETR可以设置重发延时和次数我通常写SPI_WriteReg(0x04, 0x1A)即重发延时500us重发10次。如果车端偶尔抽搐就把数据帧率从125Hz降到50Hz去掉delay_ms(8)改为20ms。这个项目最关键的调试心得是不要在代码里一上来就加各种滤波和融合先把原始值打印出来确认传感器和无线链路都正常再一层层加控制逻辑。本文还有配套的精品资源点击获取