STM32三轴机械臂控制:Marlin移植与步进电机驱动实现 简介基于STM32的Marlin三轴机械臂控制程序源码是一份面向高校课程设计、期末大作业及毕业设计场景的完整嵌入式项目。项目包含详细代码注释从主控初始化到机械臂运动控制均有清晰说明新手也能较快上手尤其适合需要高分结课项目的同学参考复用。整个资源包共168个文件核心为Keil MDK工程文件(.uvprojx/.uvoptx)与C语言驱动源码(.c/.h)其中覆盖stm32f10x_tim、adc、i2c等标准外设模块定时器用于输出PWM驱动机械臂电机ADC可采集角度/电位器反馈I2C便于连接外部传感器同时附有启动文件(.s)、编译输出(.hex/.axf/.map)及中间文件(.o/.crf/.dep)便于直接查看编译与调试信息包体仅3.38MB。目前已有152人学习下载。项目经过严格调试确保可以运行部署简单功能完善界面美观操作便捷既可快速搭建起三轴机械臂控制原型也可作为高分项目中代码组织、外设配置与调试思路的参考范例。1. 为什么用Marlin驱动三轴机械臂从3D打印机到机械臂的移植思路这套源码的工程文件里有一个很容易让人误会的名字Marlin。它原本是3D打印机的固件运动控制目标是让喷头在笛卡尔坐标系里走直线当它被移植到三轴机械臂上时很多人第一反应是“不能直接用”。实际上只要把Marlin的脉冲输出从打印机电机改接到机械臂的步进驱动器再在运动学层做一层坐标映射它内置的梯形加减速、G代码解析、回原点逻辑就变成了一套现成的三轴运动控制框架。源码里stm32f10x_tim.c负责生成步进脉冲stm32f10x_adc.c处理关节角度采样stm32f10x_i2c.c用来读写EEPROM保存零点参数三块外设恰好覆盖了机械臂控制的主要链路。这套程序的受众很明确做STM32课程设计、期末大作业或者本科毕设需要一套能跑起来、能演示、还能讲清楚原理的运动控制项目。新手可以直接在Keil5里打开工程沿着注释把PWM和ADC两个模块看懂有经验的人则可以把Marlin的规划器逻辑拆出来迁移到自己的机械臂板子上。源码附带的.uvproj和.uvgui文件说明工程本身是完整可编译的不需要从头搭建时钟和引脚配置。值得留意的是这类工程真正的难点不在接线而在“坐标系转换”和理解Marlin的planner模块。下面直接从工程文件结构开始拆。2. Keil工程结构与Marlin源码裁剪先看清这套程序在跑什么拿到压缩包先不急着烧录打开目录看一眼文件后缀能省很多排查时间。这一章把工程里出现的每个文件类型讲清楚并说明Marlin固件裁剪成机械臂版时需要保留哪些核心模块。2.1 工程文件后缀名uvproj、uvguix、axf各自承担什么角色源码包里的文件按功能分成三类工程配置、编译产物、外设驱动。工程配置包括STM32-DEMO.uvproj和STM32-DEMO.uvguix前者是Keil5的项目文件双击就能打开后者是用户界面布局文件记录窗口位置和断点信息。Administrator和20155这两个后缀表示不同Windows用户名下的界面状态用哪个用户名登录就加载哪份。uvgui_Administrator.bak和STM32-DEMO_uvproj.bak是自动备份防止工程配置损坏时能回滚。编译产物里最重要的是STM32-DEMO.axf这是链接器生成的最终可执行文件包含调试符号通过Keil的LOAD按钮直接烧进STM32F103。值得说明的是axf和hex的区别在于axf还携带变量名和源码行号信息所以在线调试时能看到C语言变量值而hex只是纯二进制。正式发布固件时转成hex发送自己维护就用axf。外设驱动文件stm32f10x_tim.c、stm32f10x_adc.c、stm32f10x_i2c.c来自标准外设库它们不是用户代码而是封装了定时器、ADC和I2C寄存器的操作函数。用户代码通过调用TIM_Cmd、ADC_SoftwareStartConvCmd、I2C_SendData这些API来操作硬件。做课程设计答辩时老师通常会问“这文件是谁写的”——答案就是ST官方标准库用户只改main.c和机械臂相关的逻辑文件。提示如果Keil版本低于5.23打开工程会提示“Device not found”需要先安装STM32F103系列器件包。常见做法是在Keil的Pack Installer里勾选Keil::STM32F1xx_DFP。2.2 从3D打印机固件到机械臂控制Marlin保留了哪些模块Marlin原始版本面向3D打印机包含加热床控制、挤出机温度PID、SD卡打印等功能这些对机械臂项目完全无用。所以裁剪的第一件事就是通过Configuration.h里的条件编译宏把无关功能关掉。机械臂版本核心保留三个模块GCode解析器、Planner运动规划器、Stepper步进驱动。GCode解析器处理从串口收到的G0/G1指令提取X、Y、Z坐标值。Planner计算的是速度曲线把一段移动拆成一串定时中断里的步进脉冲。Stepper模块则操作STM32定时器的PWM输出引脚生成电机驱动器需要的脉冲信号。这三个模块的调用关系是串口收到G代码后解析出坐标 - 调用planner的plan_buffer_line - 定时器中断里触发stepper的步进事件。// main.c 中裁剪后的初始化流程 RCC_Configuration(); // 时钟配置使能GPIO、TIM、ADC、I2C时钟 GPIO_Configuration(); // PA0-PA2输出步进脉冲PB0-PB2输出方向电平 I2C_EE_Init(); // 初始化I2C挂载AT24C02读取保存的零点偏移 ADC_Configuration(); // 配置ADC1规则组采样关节电位计电压 TIM2_Configuration(); // 定时器2产生PWM脉冲驱动X轴步进 TIM3_Configuration(); // 定时器3产生PWM脉冲驱动Y轴步进 TIM4_Configuration(); // 定时器4产生PWM脉冲驱动Z轴步进 USART1_Configuration(); // 串口1接收G代码指令 Marlin_init(); // 初始化运动规划器清空planner缓冲区 while (1) { parse_gcode_from_uart(); // 非阻塞轮询串口数据 manage_stepper_velocity();// 在定时器中断中更新速度状态机 }这段代码展示了裁剪后的主循环结构。Marlin原版是嵌入式单任务循环加定时器中断这里保留了同样的模式。参数说明TIM2_Configuration产生X轴脉冲输出引脚固定在PA0Marlin_init把planner的block缓冲区清零保证上电后不会有残留运动指令被执行。实际调试时如果机械臂上电后自己动了一下多半是planner缓冲区里存了上次掉电前的指令在init里主动清空就能解决。2.3 三轴步进电机的脉冲方向接线与引脚分配机械臂每个关节配一个步进电机驱动器常见的是A4988或DRV8825控制信号只需要两根线STEP脉冲和DIR方向。STM32的PA0、PA1、PA2分别接三个驱动器的STEP引脚PB0、PB1、PB2接DIR引脚。脉冲频率决定电机转速脉冲个数决定转动角度。功能定时器通道STM32引脚驱动器接口X轴脉冲TIM2_CH1PA0STEP_XY轴脉冲TIM3_CH1PA1STEP_YZ轴脉冲TIM4_CH1PA2STEP_ZX轴方向GPIOB Pin0PB0DIR_XY轴方向GPIOB Pin1PB1DIR_YZ轴方向GPIOB Pin2PB2DIR_Z使能信号GPIOB Pin3PB3EN低有效方向引脚的电位决定电机正反转驱动器内部一般集成了光耦隔离所以STM32的3.3V电平可以直接驱动。使能引脚低电平时驱动器处于工作状态高电平时电机自由转动。调试阶段建议把使能脚接到PB3用代码控制释放方便手动掰动机械臂关节来校准角度零点。3. 定时器PWM与梯形加减速步进电机控制的核心实现三轴机械臂的电机控制难点不是“转起来”而是“转得稳”。机械臂每个关节在启动和停止时都有转动惯量如果脉冲频率突变电机会失步甚至抖动。Marlin采用的梯形加减速策略把整个运动过程分成加速、匀速、减速三段这套逻辑在STM32定时器中断里实现。3.1 为什么用TIM输出脉冲而不是GPIO翻转很多初学者会直接在while循环里写GPIO_SetBits和GPIO_ResetBits来生成方波这种做法在低速时有效但频率一旦超过几千赫兹CPU会因为频繁进中断而占用率飙高同时脉冲间隔的抖动会直接导致电机噪音变大。定时器PWM模式则不一样脉冲波形由硬件生成CPU只需要设置CNT和CCR的值不需要干预每条边沿。更关键的是定时器可以精确控制脉冲个数。在向上计数模式下让定时器从当前CNT计数到ARR每溢出一次产生一个更新事件在更新中断里做步进计数。这样电机转多少度完全由“计数次数”决定不会丢步。3.2 TIM2 PWM初始化代码与脉冲产生逻辑以下代码基于标准外设库实现了TIM2通道1的PWM输出用来驱动X轴步进电机。void TIM2_Configuration(void) { TIM_TimeBaseInitTypeDef TIM_BaseInitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; GPIO_InitTypeDef GPIO_InitStructure; // 使能TIM2和GPIOA时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA0配置为复用推挽输出用于TIM2_CH1 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 预分频72-1计数频率1MHz也就是1us计数一次 TIM_BaseInitStructure.TIM_Period 1999; // 重装值决定脉冲频率 TIM_BaseInitStructure.TIM_Prescaler 72 - 1; // 分频 TIM_BaseInitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_BaseInitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_BaseInitStructure); // PWM模式1CNTCCR时输出高电平 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_Pulse 999; // 占空比50% TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInit(TIM2, TIM_OCInitStructure); // 使能更新中断用于步进计数 TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); }这段代码的核心参数是TIM_Prescaler和TIM_Period。72MHz主频分频后计时器频率为1MHzPeriod 1999意味着计数2000次溢出一次溢出频率为1MHz / 2000 500Hz也就是X轴的脉冲频率为500Hz。这个频率对应电机每秒转0.5圈假设驱动器16细分、电机1.8度步距角。TIM_Pulse 999设置50%占空比步进电机驱动器通常只关心脉冲边沿不要求特定占空比但保持50%能减少信号反射。在Marlin的stepper模块里TIM_Period不是固定的每次中断时都会重新计算。加速阶段逐步减小Period脉冲频率越来越高减速阶段逐步增大Period。这个动态修改重装值的操作在中断服务函数里完成而梯形加减速的状态机就是决定每次中断改多少的逻辑。3.3 梯形加减速状态机代码与参数调整梯形加减速把一次运动按速度特征分为三段加速阶段速度线性上升匀速阶段保持最大速度减速阶段线性下降到零。状态机用枚举类型表示当前处于哪个区间每次定时器更新中断时判断是否要切换状态。typedef enum { ACCEL_PHASE, CRUISE_PHASE, DECEL_PHASE, IDLE_PHASE } MovePhase; MovePhase current_phase IDLE_PHASE; int32_t current_speed; // 当前脉冲频率单位Hz int32_t target_speed; // 目标巡航速度 int32_t accel_steps; // 加速阶段需要的步数 int32_t decel_steps; // 减速阶段需要的步数 int32_t total_steps; // 本次运动总步数 int32_t step_count; // 已执行步数 void stepper_timer_isr(void) { // 输出一个步进脉冲后更新速度 GPIO_SetBits(GPIOA, GPIO_Pin_0); // STEP拉高 GPIO_ResetBits(GPIOA, GPIO_Pin_0); // STEP拉低产生上升沿 step_count; switch (current_phase) { case ACCEL_PHASE: current_speed accel_increment; if (step_count accel_steps) { current_phase CRUISE_PHASE; current_speed target_speed; } break; case DECEL_PHASE: current_speed - accel_increment; if (current_speed 0) { current_phase IDLE_PHASE; current_speed 0; } break; default: break; } // 根据当前速度计算定时器重装值 TIM_SetAutoreload(TIM2, 1000000 / current_speed - 1); }这个中断函数里的每条语句都对应一个控制指标。accel_increment是每次中断时速度的变化量它决定加速度accel_steps是加速阶段的步数阈值由Marlin的planner在每次运动开始时预先算好。TIM_SetAutoreload的参数1000000 / current_speed把频率换算成定时器计数周期这一行让脉冲频率跟随速度状态变化。参数机械臂上的经验值过大的后果过小的后果最大速度 target_speed2000-4000 Hz电机丢步、啸叫运动太慢演示效果差加速度 accel_increment100-200 Hz/中断启动抖动、机械冲击加减速时间过长占空比50%驱动器发热脉冲幅值不够可能不转需要特别说明的是这个状态机在真实Marlin代码里会更复杂因为Marlin用的是“速度-距离”前馈方式而不是简单递增。课程设计版用线性递增也能工作但运动结束时可能存在少许过冲。实际项目中会预留减速开始点的计算当剩余步数等于decel_steps时提前切换到减速阶段避免速度减不到零就到达目标位置。4. ADC角度采样与I2C参数存储让机械臂知道关节在哪儿开环的机械臂只能执行“转多少步”的指令但如果上电时关节不在零点或者步进电机丢步了控制系统就不知道实际角度。常见的低成本方案是在每个关节的旋转轴上装一个电位计用电位计的电压反映关节角度。stm32f10x_adc.c负责采集这个电压stm32f10x_i2c.c把标定好的零点偏移写入EEPROM。4.1 电位计做关节角度反馈的硬件接法电位计是一个三端元件两端分别接3.3V和GND中间抽头接STM32的ADC输入引脚。关节转动的角度会带动电位计转轴旋转抽头电压随之线性变化。这里的线性关系是近似的实际电位计的阻值变化有2%到3%的端到端偏差所以需要标定两个点角度最小时对应的电压和角度最大时对应的电压。机械结构上电位计需要通过联轴器或者齿轮与关节转轴同轴安装。如果直接焊接在PCB上需要保证PCB固定不动转轴旋转时不产生额外力矩。毕设答辩时老师常会问“电位计怎么固定”回答时说明用3D打印支架固定电位计外壳联轴器连接转轴即可。4.2 stm32f10x_adc.c规则组采样代码STM32F103的ADC是12位逐次逼近型参考电压3.3V测量分辨率约0.8mV。为了减少噪声通常让ADC连续采样8次再取平均同时用软件滤波剔除明显的突变值。uint16_t ADC_ReadJointAngle(uint8_t channel) { ADC_RegularChannelConfig(ADC1, channel, 1, ADC_SampleTime_55Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); uint32_t sum 0; for (uint8_t i 0; i 8; i) { while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) RESET); sum ADC_GetConversionValue(ADC1); ADC_SoftwareStartConvCmd(ADC1, ENABLE); } return (uint16_t)(sum / 8); }这段代码每次读取指定通道的ADC值连续采样8次取平均值。ADC_SampleTime_55Cycles5是55.5个ADC时钟周期的采样时间对应约18MHz ADC时钟下的3.08us采样窗口适合阻抗不超过10k欧姆的信号源。电位计阻值选10k到50k之间都可但阻值过大会增加采样时间导致角度更新率上不去。角度换算公式为float angle (adc_value - adc_min) * (max_angle - min_angle) / (adc_max - adc_min) min_angle;这里adc_min和adc_max是标定时测到的端点值max_angle和min_angle是机械臂关节的实际限位角度。如果机械臂是五杆并联结构角度换算关系就不是线性了但这种直角坐标模型的机械臂用线性公式足够了。采样周期建议设为10ms即100Hz的角度刷新率远高于机械臂的正常运动频率。4.3 I2C EEPROM存储零点偏移与PID参数机械臂上电后需要知道当前关节角度是多少这要求断电前保存最后一次的零点偏移值。AT24C02是2KB的EEPROM通过I2C接口挂载在STM32的PB6和PB7引脚上I2C1_SCL和I2C1_SDA。存储地址数据内容长度0x00X轴零点偏移ADC值2字节0x02Y轴零点偏移ADC值2字节0x04Z轴零点偏移ADC值2字节0x10运动速度参数标志位1字节0x11最大速度高字节1字节0x12最大速度低字节1字节I2C写入操作的代码要把握好时序。AT24C02写入一个字节后需要等待内部写周期完成通常5ms否则后续指令会被忽略。void EEPROM_WriteInt16(uint8_t addr, int16_t value) { I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, addr); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_SendData(I2C1, (uint8_t)(value 8)); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_SendData(I2C1, (uint8_t)(value 0xFF)); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1, ENABLE); DelayMs(5); // 等待内部写入完成 }地址0xA0是AT24C02的写地址最低位为0表示写操作。I2C_EVENT_MASTER_BYTE_TRANSMITTED这个事件标志字节已经发出但要注意它不等于EEPROM写入完成所以代码末尾的DelayMs(5)是必不可少的。调试时如果发现读回来的值全是0xFF首先检查延时是否去掉过其次检查上拉电阻是否焊好。注意EEPROM写寿命在100万次左右零点偏移写入不要放在主循环里只在标定完成后写一次。反复频繁写入会提前耗尽寿命。5. 坐标映射与逆运动学Marlin的三轴模型怎么对应机械臂关节Marlin内部用的是笛卡尔坐标系planner处理的是“从点A直线移动到点B”。而三轴机械臂的每个关节是旋转运动末端位置与关节角度的关系是非线性的。这一章解决“给定末端目标坐标怎么算出每个关节该转多少角度”的问题也就是逆运动学。5.1 关节坐标系与直角坐标系的关系典型的机械臂结构可以简化为两个旋转关节加一个升降关节。俯视平面内基座旋转角度θ1控制方向大臂和小臂共同决定末端在平面内的半径位置Z轴单独控制高度。Marlin里的XY对应末端在水平面的投影但机械臂的XY不是直接对应某个电机轴的转动需要先通过几何解算把XY换算成θ1和θ2的角度值再换算成电机脉冲数。如果不做这层换算而直接把Marlin的X坐标映射到基座电机上机械臂末端走的就不是直线而是弧线。课程设计只做点位运动时可以容忍但如果演示“画直线”就必须把逆运动学写在运动指令进入planner之前。5.2 两连杆逆解计算公式与C代码实现两连杆机械臂的逆运动学是平面几何问题。已知大臂长度L1、小臂长度L2、目标点(X, Y)先算出末端到基座的距离r再由余弦定理求小臂角度θ2最后由反正切求θ1。#define L1 150.0f // 大臂长度单位mm #define L2 120.0f // 小臂长度 #define PI 3.1415926f void inverse_kinematics(float x, float y, float *theta1, float *theta2) { float r sqrtf(x * x y * y); float cos_theta2 (r * r - L1 * L1 - L2 * L2) / (2.0f * L1 * L2); // 夹紧cos值防止超出[-1,1]范围 if (cos_theta2 1.0f) cos_theta2 1.0f; if (cos_theta2 -1.0f) cos_theta2 -1.0f; *theta2 acosf(cos_theta2); *theta1 atan2f(y, x) - atan2f(L2 * sinf(*theta2), L1 L2 * cosf(*theta2)); }这段代码需要重点理解cos_theta2的物理意义。如果目标点离基座距离r大于L1L2说明超出了机械臂的最大工作半径cos_theta2会大于1此时acos函数无定义代码里做了限幅处理机械臂会尝试伸到极限位置。atan2f(y, x)得到目标点的极坐标角度atan2f(L2 * sin(theta2), L1 L2 * cos(theta2))是补偿大臂转角导致的角度偏差两者相减就是基座关节的目标角度。参数说明L1和L2是常量单位mm由实际机械臂的连杆长度决定。如果尺寸不对逆解算出的角度和实际位置会有系统性偏差表现为画出来的图形整体偏移或变形。标定方法是用尺子量两个关节转轴中心的距离而不是量臂管的总长。5.3 逆解结果如何喂给Marlin的planner逆运动学算出的θ1和θ2是以弧度为单位的浮点数需要先转成角度再根据步进电机每度的脉冲数算出脉冲步数最后调用planner的规划接口。int32_t steps_for_angle(float angle_deg, int32_t steps_per_deg) { return (int32_t)(angle_deg * steps_per_deg); } void move_to_cartesian(float x, float y, float z) { float theta1, theta2; inverse_kinematics(x, y, theta1, theta2); // 弧度和度转换乘以每度脉冲数 int32_t steps_x steps_for_angle(theta1 * 180.0f / PI, X_STEPS_PER_DEG); int32_t steps_y steps_for_angle(theta2 * 180.0f / PI, Y_STEPS_PER_DEG); int32_t steps_z (int32_t)(z * Z_STEPS_PER_MM); // 调用Marlin的planner接口 planner_buffer_line(steps_x, steps_y, steps_z, feed_rate); }X_STEPS_PER_DEG取决于电机步距角、驱动器细分和减速比。常见的计算方式是1.8度步距角16细分则电机转一圈需要3200个脉冲如果关节通过5:1减速器输出则输出轴转一圈需要16000个脉冲平均每度脉冲数为16000/360 44.44。这些参数在Marlin的Configuration.h里以步进速率的形式定义。提示如果机械臂是直角坐标式结构比如XYZ三个方向都是丝杠滑块那逆运动学可以省略直接让Marlin的笛卡尔坐标对应三个电机轴。在动笔写逆解代码前先确认机械臂的机械结构别为了炫技强行套关节模型。6. 烧录、零点标定与串口调试把程序跑起来之前要确认的三件事很多同学把源码编译通过就以为大功告成结果上电后机械臂要么不动要么乱转。这三个步骤按顺序做能覆盖大多数问题。6.1 Keil烧录设置与ST-Link连接要点打开工程后在Options for Target的Debug选项卡里选择ST-Link Debugger点击Settings确认能识别到目标芯片。Flash Download选项卡里勾选Reset and Run这样烧录完成后程序立即执行不用手动按复位键。如果提示No Target Connected先检查ST-Link的SWDIO和SWCLK是否接反再检查STM32的BOOT0是否拉低到GND。6.2 零点标定流程先手动后自动零点标定的目的是让机械臂的关节角度与ADC采样值对应起来。首先通过串口给机械臂发送T0命令让所有关节释放力矩手动把每个关节掰到机械限位处记下此时ADC采样的电压值。然后把计算出的角度偏移写入EEPROM之后每次上电都会从这个基准开始。建议的标定顺序是X轴基座关节转到0度Y轴大臂水平Z轴抬到最高。标定代码放在主循环里由串口命令触发不要放在初始化函数中否则每次上电都会重新标定。EEPROM里保存的偏移值在首次标定后生效第二次上电不需要再标定。6.3 串口G代码联动测试与常见异常Marlin标准指令G0 X10 Y20 Z5表示快速移动到指定坐标G1则带速度控制。通过USB转TTL模块连接STM32的串口1波特率115200发送G1指令后机械臂应平滑运动到目标点。如果不动作按顺序排查先看串口是否收到数据在中断里翻转一个LED作为指示再看planner接收的步数是否为0可以在planner_buffer_line入口加打印最后检查定时器中断是否进得去在ISR里计数并通过串口上报。这三个检查点分别对应通信层、逻辑层、硬件层能快速定位故障。调试时把printf指向串口1比接OLED屏效率高得多每一行日志都带上时间戳判断运动是否卡在加速度计算上。本文还有配套的精品资源点击获取