news 2026/9/16 12:19:38

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32电动车跷跷板控制:DMP姿态解算与PID闭环调参实战

简介:一份基于STM32的2021年电子设计大赛校赛电动车跷跷板项目工程,面向电子设计竞赛参赛者和嵌入式单片机学习者,可用于复现赛题、完成课程设计,或深入理解电动车动力控制与跷跷板动态平衡的完整实现流程。压缩包共251个文件,大小约8.94MB,以C源文件与H头文件为主,同时包含Keil工程文件、启动文件、编译中间文件、AXF可执行文件、编译脚本和日志,目录结构清晰,便于按模块检索。代码围绕车体平衡这一核心目标,涉及MPU6050姿态解算、定时器PWM输出、ADC采集、I2C与CAN通信等STM32常用外设,展示了传感器数据读取、电机控制策略以及闭环调节在跷跷板场景中的协同配合。对于准备电子设计竞赛或学习标准外设库开发的读者,可以直接参考其工程组织方式、外设初始化流程和调试信息记录方法,减少从零搭建环境的时间。目前已有70人学习浏览,整体完整度较高,适合按需借鉴二次开发。

1. 电动车跷跷板控制的破题点:STM32 为什么选 DMP 而不选互补滤波

朋友传给我一份「2021年电子设计大赛校赛-电动车跷跷板.zip」,压缩包里没有任何文档,只有一整套 STM32 标准外设库工程文件:inv_mpu.cinv_mpu_dmp_motion_driver.cstm32f10x_tim.cstm32f10x_adc.cstm32f10x_can.c。这份文件清单本身就是答案——电动车要在中间支撑的跷跷板上通过前后移动改变重心,让板子稳定在水平或指定角度。难点不在驱动电机,而在于角度反馈回路:MPU6050 的角度如果直接用加速度计姿态解算,电机震动和跷跷板倾角混在一起,控制必然发抖。工程里选 DMP 而不是自己写互补滤波,是为了在定时器中断里直接拿到四元数换算出的俯仰角,把姿态解算的算力开销从主循环里挪走。这个项目适合正在备赛电子设计大赛的学生、刚入门 STM32 电机控制的开发者,或者拿到一个旧工程想快速理解外设配合关系的人。下面按我实际拆解这个工程的路子来写。

2. 标准外设库工程骨架:从 keilkill.bat 到 TIM/ADC 外设初始化

老比赛工程里出现STM32_StandardLibrary.axf说明 Keil 生成过可烧录镜像,keilkill.bat通常是清理工程中间文件的脚本。这个项目里真正值得看的是stm32f10x_rcc.cstm32f10x_tim.cstm32f10x_adc.c这几个文件之间的配合方式。

2.1 axf 与 keilkill.bat:工程状态判断

.axf是 ARM 编译链接后的 ELF 文件,比.hex包含更多调试信息,Keil 在 Debug 模式下下载程序时刷新它。keilkill.bat一般是清理.o.d.crf这类临时文件的脚本,我在新路径下动工前会先跑一轮,避免带着旧的对象文件修改配置,出现“改了半天不生效”的假象。

keilkill.bat

脚本里常见的内容是递归删除ObjectsListings目录下的中间文件,再清理源文件旁的.o.d。运行后重新 Build,axf会重新生成。如果工程里存在多个外设库源文件但 Keil 组件里只勾选了部分.c,也要在这里一并确认勾选状态。

2.2 RCC 配置:让 TIM1、ADC1、I2C1 和外设总线时钟先跑起来

标准外设库里,stm32f10x_rcc.c提供RCC_APB2PeriphClockCmd等函数。跷跷板车通常把电机 PWM 放在 TIM1(APB2 总线,72MHz),MPU6050 接在 I2C1(PB6/PB7),ADC1 采样电池电压。初始化顺序我一般放在主函数最前面:

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。PCLK1=36MHz是因为 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); }

arr=999, psc=71得到72MHz / (71+1) / (999+1) = 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 属于高级定时器,挂在 APB2;TIM2、TIM3、I2C1 挂在 APB1。初始化错误经常发生在“TIM1 无法输出 PWM”或“I2C 一直 busy”这两个现象上。

外设总线时钟配置工程对应文件
TIM1 PWMAPB272MHz,预分频 71,周期 999stm32f10x_tim.c
TIM2 EncoderAPB136MHz,预分频 0stm32f10x_tim.c
ADC1APB212MHz,采样时间 239.5 周期stm32f10x_adc.c
I2C1APB1400kHzstm32f10x_i2c.c
CAN1APB1625kbpsstm32f10x_can.c
FlashAHB等待周期 2stm32f10x_flash.c

这块工程骨架建立后,控制主体才能真正跑在固定的时间基准上,否则后面 PID 调参时参数会随着时钟抖动来回变。

3. 读取 MPU6050 姿态角:inv_mpu.c 与 DMP 驱动的工作方式

MPU6050 是六轴传感器,包含三轴加速度计、三轴陀螺仪和温度计。工程文件名里的inv_mpu.cinv_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_QUATDMP_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 工作在高速模式,但上拉电阻阻值必须匹配。如果飞线较长,我建议降回100000I2C_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)决定了主循环拿到的姿态角速率。跷跷板电机频繁正反转,如果角度输出频率低于 50Hz,PID 微分项会看到很大的跳变;高于 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 时降到 100kHz

4. 电机 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 更新中断决定,而不是放在主循环里随机调用。

控制周期内执行顺序:

  1. 读取 MPU6050 DMP 角度pitch
  2. 读取编码器位置pos
  3. 位置环以pos_target为设定点,输出angle_target
  4. 角度环以angle_targetpitch做 PID,输出 PWM 占空比。
  5. 将 PWM 占空比写入TIM1->CCR1,方向引脚根据正负决定。

4.3 PID 参数表与试凑顺序

先用值对应参数现象
kp=2, ki=0, kd=0角度环小幅振荡,能回到水平
kp=4, ki=0.01, kd=1.2角度环静态误差减小,但电机吱吱响
kp=0.1, ki=0, kd=0位置环缓慢回到指定位置
kp=0.3, ki=0.002, kd=0.05位置环稳定移动,无过冲

我先把角度环 P 从 1 加到 3,观察跷跷板是否在水平附近往复摆动。摆动幅度收敛到 ±2° 后,再加轻微 D 抑制过冲。位置环最后加,先让pos_target等于跷跷板几何中点,之后再用遥控改变目标点。若出现高频发抖,多半是角度环 D 太大或差分噪声,而不是 PWM 频率问题。

5. 现场调参与排错:ADC 电压监测、CAN 通信和 Flash 参数保存

比赛现场最容易翻车的不只是控制算法,还有电源和参数一致性问题。这一章把stm32f10x_adc.cstm32f10x_can.cstm32f10x_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是分压电阻比例,比如采样电阻 10k+1k 时,电压计算公式改为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_RX=PB8 CAN_TX=PB9 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_Prescaler=4, BS1=8, BS2=7在 APB1=36MHz 下得到波特率36MHz / 4 / (1+8+7) = 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_CtrlPWMOutputs
I2Cstm32f10x_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 没调好”这两件完全不同的事。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 12:17:41

学术写作规范与AI辅助实战指南

1. 学术写作的范式转变&#xff1a;从自由创作到规则游戏十年前我刚读研究生时&#xff0c;导师递给我一摞A4纸打印的论文模板&#xff0c;说&#xff1a;"先把这个格式背下来再写东西。"当时觉得这简直是学术八股&#xff0c;直到自己投稿被连续拒了三次才明白&…

作者头像 李华
网站建设 2026/9/16 12:16:55

量子门基础与Qiskit框架解析

1. 量子门基础与Qiskit框架概述量子计算正在重塑我们对计算的认知边界。作为一名长期从事量子算法开发的工程师&#xff0c;我深刻体会到量子门作为量子计算基本操作单元的核心地位。与经典计算机中的逻辑门不同&#xff0c;量子门操作的是量子比特&#xff08;qubit&#xff0…

作者头像 李华
网站建设 2026/9/16 12:16:01

AS5147U与R7KA8D2KFLCAC磁位置传感系统实战指南

1. 这不是普通电位器&#xff1a;AS5147UR7KA8D2KFLCAC组合为何专治高精度旋转测量你有没有遇到过这样的场景&#xff1a;在调试一个伺服电机反馈环时&#xff0c;用传统电位器测角度&#xff0c;刚上电读数就跳变3度&#xff1b;换上某款霍尔芯片&#xff0c;低速转动时输出抖…

作者头像 李华