尧图精选

STM32电动车跷跷板控制:DMP姿态解算与PID闭环调参实战

🕒 发布时间:2026/9/16 12:19:43 📁 来源:尧图网络
简介一份基于STM32的2021年电子设计大赛校赛电动车跷跷板项目工程面向电子设计竞赛参赛者和嵌入式单片机学习者可用于复现赛题、完成课程设计或深入理解电动车动力控制与跷跷板动态平衡的完整实现流程。压缩包共251个文件大小约8.94MB以C源文件与H头文件为主同时包含Keil工程文件、启动文件、编译中间文件、AXF可执行文件、编译脚本和日志目录结构清晰便于按模块检索。代码围绕车体平衡这一核心目标涉及MPU6050姿态解算、定时器PWM输出、ADC采集、I2C与CAN通信等STM32常用外设展示了传感器数据读取、电机控制策略以及闭环调节在跷跷板场景中的协同配合。对于准备电子设计竞赛或学习标准外设库开发的读者可以直接参考其工程组织方式、外设初始化流程和调试信息记录方法减少从零搭建环境的时间。目前已有70人学习浏览整体完整度较高适合按需借鉴二次开发。1. 电动车跷跷板控制的破题点STM32 为什么选 DMP 而不选互补滤波朋友传给我一份「2021年电子设计大赛校赛-电动车跷跷板.zip」压缩包里没有任何文档只有一整套 STM32 标准外设库工程文件inv_mpu.c、inv_mpu_dmp_motion_driver.c、stm32f10x_tim.c、stm32f10x_adc.c、stm32f10x_can.c。这份文件清单本身就是答案——电动车要在中间支撑的跷跷板上通过前后移动改变重心让板子稳定在水平或指定角度。难点不在驱动电机而在于角度反馈回路MPU6050 的角度如果直接用加速度计姿态解算电机震动和跷跷板倾角混在一起控制必然发抖。工程里选 DMP 而不是自己写互补滤波是为了在定时器中断里直接拿到四元数换算出的俯仰角把姿态解算的算力开销从主循环里挪走。这个项目适合正在备赛电子设计大赛的学生、刚入门 STM32 电机控制的开发者或者拿到一个旧工程想快速理解外设配合关系的人。下面按我实际拆解这个工程的路子来写。2. 标准外设库工程骨架从 keilkill.bat 到 TIM/ADC 外设初始化老比赛工程里出现STM32_StandardLibrary.axf说明 Keil 生成过可烧录镜像keilkill.bat通常是清理工程中间文件的脚本。这个项目里真正值得看的是stm32f10x_rcc.c、stm32f10x_tim.c、stm32f10x_adc.c这几个文件之间的配合方式。2.1 axf 与 keilkill.bat工程状态判断.axf是 ARM 编译链接后的 ELF 文件比.hex包含更多调试信息Keil 在 Debug 模式下下载程序时刷新它。keilkill.bat一般是清理.o、.d、.crf这类临时文件的脚本我在新路径下动工前会先跑一轮避免带着旧的对象文件修改配置出现“改了半天不生效”的假象。keilkill.bat脚本里常见的内容是递归删除Objects、Listings目录下的中间文件再清理源文件旁的.o和.d。运行后重新 Buildaxf会重新生成。如果工程里存在多个外设库源文件但 Keil 组件里只勾选了部分.c也要在这里一并确认勾选状态。2.2 RCC 配置让 TIM1、ADC1、I2C1 和外设总线时钟先跑起来标准外设库里stm32f10x_rcc.c提供RCC_APB2PeriphClockCmd等函数。跷跷板车通常把电机 PWM 放在 TIM1APB2 总线72MHzMPU6050 接在 I2C1PB6/PB7ADC1 采样电池电压。初始化顺序我一般放在主函数最前面void SystemClock_Config(void) { ErrorStatus HSEStartUpStatus; RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); // 开启外部晶振 HSE HSEStartUpStatus RCC_WaitForHSEStartUp(); if(HSEStartUpStatus SUCCESS) { RCC_HCLKConfig(RCC_SYSCLK_Div1); // AHB 时钟不分频 RCC_PCLK2Config(RCC_HCLK_Div1); // APB2 72MHz供 TIM1/ADC1 RCC_PCLK1Config(RCC_HCLK_Div2); // APB1 36MHz供 I2C1/TIM2 FLASH_SetLatency(FLASH_Latency_2); FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable); RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); RCC_PLLCmd(ENABLE); while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY) RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); while(RCC_GetSYSCLKSource() ! 0x08); } }这段代码把 SYSCLK 固定到 72MHz。PCLK136MHz是因为 I2C1 的最大时钟限制而 TIM1 挂在 APB2 上得到 72MHz 计数源。RCC_PLLMul_9表示 8MHz 晶振倍频 9 倍输出 72MHz。很多人把 APB1 也配成 72MHz会导致 I2C1 超频异常。2.3 定时器与 ADC 外设的分工配置stm32f10x_tim.c里需要关心两个定时器TIM1 输出 PWM 驱动电机TIM2 做编码器位置计数。下面是 TIM1 的 PWM 初始化void TIM1_PWM_Init(u16 arr, u16 psc) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_8; // TIM1_CH1 GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period arr; // 例如 999 TIM_TimeBaseStructure.TIM_Prescaler psc; // 例如 71 TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_CtrlPWMOutputs(TIM1, ENABLE); // TIM1 高级定时器必须开主输出 TIM_Cmd(TIM1, ENABLE); }arr999, psc71得到72MHz / (711) / (9991) 1kHz的 PWM 频率。电机驱动常用 1kHz 到 20kHz低于人耳可听范围会产生噪音高于 20kHz 会增加开关损耗。TIM_CtrlPWMOutputs是 TIM1 和 TIM8 这类高级定时器特有的漏写会导致 PWM 输出全为低电平。ADC 部分通过stm32f10x_adc.c实现void ADC1_Init(void) { ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC 时钟 72MHz / 6 12MHz ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); }这里需要区分 APB1 和 APB2 的时钟源TIM1 属于高级定时器挂在 APB2TIM2、TIM3、I2C1 挂在 APB1。初始化错误经常发生在“TIM1 无法输出 PWM”或“I2C 一直 busy”这两个现象上。外设总线时钟配置工程对应文件TIM1 PWMAPB272MHz预分频 71周期 999stm32f10x_tim.cTIM2 EncoderAPB136MHz预分频 0stm32f10x_tim.cADC1APB212MHz采样时间 239.5 周期stm32f10x_adc.cI2C1APB1400kHzstm32f10x_i2c.cCAN1APB1625kbpsstm32f10x_can.cFlashAHB等待周期 2stm32f10x_flash.c这块工程骨架建立后控制主体才能真正跑在固定的时间基准上否则后面 PID 调参时参数会随着时钟抖动来回变。3. 读取 MPU6050 姿态角inv_mpu.c 与 DMP 驱动的工作方式MPU6050 是六轴传感器包含三轴加速度计、三轴陀螺仪和温度计。工程文件名里的inv_mpu.c与inv_mpu_dmp_motion_driver.c揭示了关键选型使用 InvenSense DMP 接管传感器数据融合输出四元数甚至带零漂校准的姿态角而不是自己在 MCU 里写低通滤波。3.1 DMP 与互补滤波的取舍互补滤波通常是这样pitch 0.98 * (pitch gyro_y * dt) 0.02 * accel_pitch;这种实现简单但陀螺仪零偏会导致角度漂移在电机反复启停的跷跷板场景下每次震动都会让加速度计数据变得不可信。而 DMP 在 MPU6050 内部以固定频率输出四元数需要外部用dmp_read_fifo读取。它省掉 STM32 主循环的三角函数运算但要求 I2C 时序正确、中断引脚连接稳定。工程里可以裁剪默认功能。比如只做跷跷板平衡控制不需要 TAP 检测和线性加速度输出。我一般把DMP_FEATURE_TAP关掉只保留DMP_FEATURE_6X_LP_QUAT和DMP_FEATURE_SEND_CAL_GYRO这样 FIFO 深度不会被无关数据撑满。3.2 I2C 读写与 DMP 初始化MPU6050 FIFO 读取依赖正确的 I2C 时序下面是基于stm32f10x_i2c.c的 I2C1 初始化与单字节写实现void I2C1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_OD; // I2C 必须开漏 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); I2C_InitStructure.I2C_Mode I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle I2C_DutyCycle_2; I2C_InitStructure.I2C_Ack I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockSpeed 400000; // 400kHz飞线长时降到 100k I2C_Init(I2C1, I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); } u8 I2C_WriteOneByte(u8 addr, u8 reg, u8 val) { while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); I2C_GenerateSTART(I2C1, ENABLE); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, addr, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, reg); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)); I2C_SendData(I2C1, val); while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1, ENABLE); return 0; }I2C_ClockSpeed 400000时 MPU6050 工作在高速模式但上拉电阻阻值必须匹配。如果飞线较长我建议降回100000。I2C_WriteOneByte里的CheckEvent是轮询方式每次写寄存器都要等总线完成如果后续代码加入更高优先级的控制中断需要把这里改成带超时的状态机否则在总线异常时容易卡死。DMP 初始化顺序固定if (mpu_init(NULL) ! 0) { Error_Handler(); // 检查 AD0 接地和 I2C 地址是否正确 } if (dmp_load_motion_driver_firmware() ! 0) { Error_Handler(); } dmp_set_orientation(0); dmp_set_fifo_rate(100); // 100Hz 输出控制周期与它同步 mpu_set_sensors(INV_XYZ_GYRO | INV_XYZ_ACCEL); mpu_configure_fifo(INV_XYZ_GYRO | INV_XYZ_ACCEL); mpu_set_sample_rate(100); dmp_enable_feature(DMP_FEATURE_6X_LP_QUAT | DMP_FEATURE_SEND_CAL_GYRO); dmp_enable_fifo(1);dmp_set_fifo_rate(100)决定了主循环拿到的姿态角速率。跷跷板电机频繁正反转如果角度输出频率低于 50HzPID 微分项会看到很大的跳变高于 200Hz 则每个控制周期内 PWM 占空比变化过快电机电流波动反而大。100Hz 是我在这个结构里用得最多的值。3.3 从 FIFO 中读取角度并做合理性校验short gyro[3], accel[3], sensors; float pitch; unsigned char more; long quat[4]; unsigned long timestamp; dmp_read_fifo(gyro, accel, quat, timestamp, sensors, more); if (sensors INV_WXYZ_QUAT) { float q0 quat[0] / 16384.0f; float q1 quat[1] / 16384.0f; float q2 quat[2] / 16384.0f; float q3 quat[3] / 16384.0f; pitch atan2(2.0f * (q0 * q2 - q1 * q3), 1.0f - 2.0f * (q2 * q2 q3 * q3)) * 180.0f / PI; }more参数表示 FIFO 缓冲区里还有未读完的数据说明主循环处理速度跟不上 DMP 输出频率。此时应该优先提高主循环频率而不是把dmp_set_fifo_rate调低否则控制时延增大。quat除以 16384.0f 是因为四元数定点精度是 Q15转换为浮点后采用atan2计算俯仰角可以避免 90° 附近的分段错误。提示DMP 初始化失败时先查 AD0 是否接地、I2C 时钟是否超限再去查中断引脚配置。不要一上来就怀疑传感器损坏。读取完 FIFO 后最好用加速度计交叉验证一次if (fabs(accel_pitch) 30.0f fabs(pitch) 30.0f) { pitch 0; // 异常跳变时回零等待下一帧 }这是比赛现场加的保护电机刚起步瞬间机械冲击会让 DMP 输出偶尔跳变几度直接用于控制会让小车猛冲一下。用加速度计的短期近似和 DMP 输出做合理性窗口虽然不是最优解但在五分钟比赛里最实用。配置项推荐值说明mpu_set_sample_rate100 Hz与 FIFO 输出率一致dmp_set_fifo_rate100 Hz角度输出速率太高扰动大太低响应慢DMP_FEATURE6X_LP_QUAT只保留六轴低功耗四元数I2C 时钟400 kHz飞线超过 20cm 时降到 100kHz4. 电机 PWM 与小车位置闭环TIM 输出比较 PID有了实时俯仰角下一步就是用这个角度闭环控制小车位置。跷跷板的物理关系是小车位于跷跷板左端板子重心偏左要回到水平小车必须向右移动。但电池、车身和结构重量分布不均匀所以必须用 PID 调节位置。4.1 为什么用 TIM 输出 PWM 而不是 DA 输出STM32F103 没有真正的 DA 输出即便有电机驱动也需要功率级。用 TIM 输出比较产生 PWM 是最直接的方法。上一章 TIM1_PWM_Init 已配置 TIM1_CH1电机驱动模块常用 TB6612两条 PWM 线接驱动芯片输入方向引脚控制正反转。新人常犯的错误是初始化后忘记将方向引脚设为输出导致电机只往一个方向转调参无从下手。为了拿到车轮实际速度工程中还需要编码器接口。TIM2 的编码器模式void TIM2_Encoder_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_EncoderInterfaceConfig(TIM2, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 10; TIM_ICInit(TIM2, TIM_ICInitStructure); TIM_ClearFlag(TIM2, TIM_FLAG_Update); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); } s32 GetPosition(void) { return TIM2-CNT; }编码器模式计数方向自动跟随电机方向读取TIM2-CNT即可得到当前位置。TIM_ICFilter 10对编码器信号做硬件滤波能消除接触抖动。注意编码器不能和电机 PWM 使用同一个定时器否则计数器会和周期重装互相干扰造成位置值跳变。4.2 位置环加角度环的串级 PID跷跷板车的控制结构是串级闭环外环是位置环内环是角度环。角度环输入是 DMP 俯仰角输出是期望速度位置环输入是编码器位置差输出是角度目标。两个环需要先调内环再闭合外环。typedef struct { float kp, ki, kd; float integral; float last_error; } Pid_TypeDef; float Pid_Update(Pid_TypeDef *pid, float setpoint, float measurement) { float error setpoint - measurement; pid-integral error; // 积分限幅 if (pid-integral 300) pid-integral 300; if (pid-integral -300) pid-integral -300; float derivative error - pid-last_error; pid-last_error error; return pid-kp * error pid-ki * pid-integral pid-kd * derivative; }integral限幅的作用是防止积分饱和当小车卡在跷跷板中点无法继续移动时积分项如果无限增大会在卡涩消除时突然输出大 PWM造成猛烈冲撞。last_error在每个控制周期结束前更新控制周期由 TIM2 更新中断决定而不是放在主循环里随机调用。控制周期内执行顺序读取 MPU6050 DMP 角度pitch。读取编码器位置pos。位置环以pos_target为设定点输出angle_target。角度环以angle_target和pitch做 PID输出 PWM 占空比。将 PWM 占空比写入TIM1-CCR1方向引脚根据正负决定。4.3 PID 参数表与试凑顺序先用值对应参数现象kp2, ki0, kd0角度环小幅振荡能回到水平kp4, ki0.01, kd1.2角度环静态误差减小但电机吱吱响kp0.1, ki0, kd0位置环缓慢回到指定位置kp0.3, ki0.002, kd0.05位置环稳定移动无过冲我先把角度环 P 从 1 加到 3观察跷跷板是否在水平附近往复摆动。摆动幅度收敛到 ±2° 后再加轻微 D 抑制过冲。位置环最后加先让pos_target等于跷跷板几何中点之后再用遥控改变目标点。若出现高频发抖多半是角度环 D 太大或差分噪声而不是 PWM 频率问题。5. 现场调参与排错ADC 电压监测、CAN 通信和 Flash 参数保存比赛现场最容易翻车的不只是控制算法还有电源和参数一致性问题。这一章把stm32f10x_adc.c、stm32f10x_can.c和stm32f10x_flash.c为什么要放在工程里说清楚。5.1 ADC 采样电池电压防止电机堵转复位当小车走到跷跷板边缘又打滑时电机电流激增电池电压会瞬间跌落。STM32 的复位电压一般在 1.8V 附近跌穿这个值时看门狗都无力回天。ADC 采样电压并在低于阈值时降低 PWM 占空比是一种保命策略u16 SampleBatteryVoltage(void) { ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); return ADC_GetConversionValue(ADC1); } void VoltageProtection(void) { u16 adc SampleBatteryVoltage(); float voltage adc * 3.3f / 4096.0f * 11.0f; if (voltage 6.8f) { TIM_SetCompare1(TIM1, 0); // 电压不足时直接关 PWM } }ADC_SampleTime_239Cycles5是较长的采样时间能滤掉高频噪声但 12MHz ADC 时钟下需要约 20us。电压触底时直接清零 PWM 比调整 PID 更快因为这时的控制目标是保护系统而不是保持平衡。*11.0f是分压电阻比例比如采样电阻 10k1k 时电压计算公式改为adc * 3.3f / 4096.0f * 11.0f。5.2 CAN 通信多板协作与调试信息透传工程里出现stm32f10x_can.c可能用于遥控器端和车体端通信或者双车协作场景。CAN 的优势是双线差分现场 2 米线缆可以直接拖缺点是波特率配置不对时排查困难。void CAN_Config(void) { CAN_InitTypeDef CAN_InitStructure; GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_9; // CAN_RXPB8 CAN_TXPB9 GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); CAN_DeInit(CAN1); CAN_InitStructure.CAN_TTCM DISABLE; CAN_InitStructure.CAN_ABOM ENABLE; CAN_InitStructure.CAN_AWUM ENABLE; CAN_InitStructure.CAN_NART DISABLE; CAN_InitStructure.CAN_RFLM DISABLE; CAN_InitStructure.CAN_TXFP DISABLE; CAN_InitStructure.CAN_Mode CAN_Mode_Normal; CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_8tq; CAN_InitStructure.CAN_BS2 CAN_BS2_7tq; CAN_InitStructure.CAN_Prescaler 4; CAN_Init(CAN1, CAN_InitStructure); }CAN_Prescaler4, BS18, BS27在 APB136MHz 下得到波特率36MHz / 4 / (187) 625kbps。如果对端是另一块 STM32用同样配置即可。保活机制上我建议每 100ms 互发一帧CAN_ID 0x100的标准帧内容是最新角度和位置方便用 CAN 分析仪在调试电脑上实时绘图比串口稳定很多。5.3 Flash 保存 PID 和零点偏移每台车因为机械装配差异零点俯仰角不会完全一样。比赛现场重新标定零点不现实所以需要把空载时读到的角度偏移存进片内 Flash。stm32f10x_flash.c中的编程流程#define PID_FLASH_ADDR 0x0803F800 // 最后一页避开程序区 void WritePIDToFlash(Pid_TypeDef *angle_pid) { FLASH_Unlock(); FLASH_ErasePage(PID_FLASH_ADDR); u32 *p (u32*)angle_pid; for (int i 0; i sizeof(Pid_TypeDef) / 4; i) { FLASH_ProgramWord(PID_FLASH_ADDR i * 4, p[i]); } FLASH_Lock(); }sizeof(Pid_TypeDef) / 4在结构体对齐下可能不等于实际字段个数如果出现FLASH_ErasePage卡死先确认目标地址没有超出0x08040000。写入前必须保证数据长度小于 1KB只擦除一页。空载时用按键触发一次写入把当前pitch_offset一起写入然后复位加载。模块工程对应文件现场排错关键点时钟stm32f10x_rcc.c检查 HSE 晶振是否起振PLL 是否倍频到 72M定时器stm32f10x_tim.cTIM1 必须开 TIM_CtrlPWMOutputsI2Cstm32f10x_i2c.c若读取 MPU6050 失败先降低 I2C 速度ADCstm32f10x_adc.c电压跌落保护触发时可增加采样点数CANstm32f10x_can.c波特率与 APB1 时钟相关需匹配Flashstm32f10x_flash.c写入前关中断被看门狗复位会损坏数据6. 把跷跷板平衡控制移植到其他移动平台一个验收脚本如果你把这份工程改到两轮自平衡车、三维滚球或智能车坡道起步结构基本一样差异在传感器安装方向和控制周期。跷跷板的自然振荡频率大约在 1Hz 到 2Hz我的角度环周期取 5ms位置环周期取 10ms用定时器更新中断挂载不同 flag。两轮自平衡车的自然频率更高角度环可能需缩短到 2ms。你可以快速判断把车悬空手动让车体小幅摆动如果 DMP 输出能跟上且没有明显相位滞后说明控制频率够用。传感器安装方向也影响后续算法。MPU6050 在跷跷板上水平安装pitch 就是车体俯仰角如果斜装 45°四元数转欧拉角的轴要重新定义。一种快速验证方式是在终端打印静止时的 Euler 角跟物理倾角比对相差不超过 0.5° 再进主流程。如果想让这套代码更可用我一般会在主循环开始前加一段循环测试// 主循环测试逻辑验证传感器初始化和连续读取稳定性 int self_test_count 0; while (self_test_count 100) { if (DMP_Read() 0) { self_test_count; } else { self_test_count 0; Error_Handler(); // 连续失败 100 次之前暴露接线问题 } }这段代码在进入比赛主流程前先跑 100 次 DMP 读取如果有连续错误就停在Error_Handler()中好处是避免“控制跑飞”和“传感器没工作”混在一起排查。DMP FIFO 读取连续失败通常不是 MPU6050 物理损坏而是 I2C 引脚被复用或 AD0 电平错误跟焊接关系不大。接手这类比赛工程时按“时钟 → FIFO → 编码器 → PWM → 电压保护”的顺序验收外设再调算法。第一轮直接检查RCC_GetClocksFreq返回值和核心频率是否 72MHz第二轮挂上dmp_read_fifo前打印原始陀螺仪数据确认方向第三轮用示波器看 PWM 输出。这个验收脚本能把你从几十个外设文件的纠缠里快速捞出来也避免比赛现场分不清“传感器坏了”和“PID 没调好”这两件完全不同的事。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →