
简介这份文档围绕自由摆平板控制系统展开面向电子设计竞赛参赛者、嵌入式学习者与自动控制课程实践者提供一套完整设计方案。压缩包仅含1个docx文件约36KB篇幅精炼却覆盖系统方案、硬件选型与软件实现思路。设计以STM32F103VET6为主控配合TFT液晶显示与高精度WDD35D4导电塑料角位移传感器经A/D采样实时获取摆杆和平板角度再由四宏细分驱动器驱动步进电机调节平板转向与转角使硬币在摆动中与平板保持相对静止、激光笔稳定命中接收靶。文档保留控制器、角度检测、电机与电源模块的方案论证说明弃用51单片机、普通电位器、ADXL345和伺服电机的理由并给出摆架框架、检测系统、控制驱动系统三部分结构。已有308人学习适合参考完整赛题方案、选型依据与系统搭建逻辑。1. 自由摆上的平板控制系统一枚硬币撑起来的机电闭环把一枚硬币放在摆杆末端那块巴掌大的平板上扶着摆杆拉到 45° 松手摆杆来回甩硬币不能滚落。这个自由摆平板控制系统干的就是这件事STM32F103VET6 读两路 WDD35D4 导电塑料角位移传感器一路测摆杆相对竖直的摆角一路测平板相对摆杆的转角两者相减得到平板相对水平面的真实倾角再让步进电机经细分驱动器把平板往反方向拧回去让倾角始终贴着 0°。整套硬件不贵但机械回差、采样噪声、PID 参数会一起找你麻烦调不好就是一路抖到硬币飞出去。它适合正在做课程设计、竞赛题或者手里只有开环步进却想把定位精度做上去的开发者——照着跑一遍能拿到一套能采、能算、能调、能改的闭环框架。2. 硬件架构与选型从摆杆到平板的三层链路这套系统的骨架可以粗暴地切成三层摆架框架负责把重力变成可重复的摆动激励检测系统负责把机械角度变成数字控制驱动系统负责把数字变回机械动作。三层之间只有两条物理连线——传感器的模拟电压进 ADC单片机的脉冲出到驱动器。搞清楚这两条线的方向后面所有代码和参数才有落点。2.1 三层结构里谁决定精度摆架框架包括底座、摆杆、轴承和末端平板平板固定在步进电机的输出轴上电机壳体跟着摆杆一起摆。这一层的加工精度直接决定控制上限轴承有轴向窜动摆杆就会左右晃传感器读到的角度里混进了不属于摆动的分量平板和电机轴不同心转起来就有周期性偏摆PID 会把这个偏摆当成真实误差去追越追越抖。我的经验是轴承用带预紧的深沟球轴承摆杆尽量短粗平板用 2mm 铝板加工而不是 3D 打印件减少受力变形。检测系统是两个 WDD35D4 加各自的模拟前端。摆杆那一路安装在摆轴一侧用连杆或者齿轮跟摆杆联动平板那一路装在电机轴尾端直接读平板相对摆杆的转角。两路输出都是 05V 的模拟电压进 STM32 的 PA0、PA1 两个 ADC 通道。这里有个容易忽略的点传感器的供电和 ADC 的参考电压必须是同一个源否则电源一波动码值和角度之间的比例就跟着变。控制驱动系统是 STM32 的定时器输出脉冲经细分驱动器放大后带动步进电机。驱动器只认三样东西脉冲、方向电平、使能电平所以单片机和驱动器之间天然是光耦隔离的好位置把电机的干扰挡在控制板外面。模块器件与主控的连接关键指标主控STM32F103VET6—72MHz3 路 ADC多路通用定时器角度检测WDD35D4 ×2PA0 / PA1 模拟输入独立线性度约 0.1%机械转角 345°显示TFT 液晶SPI 或 FSMC刷新率够看波形即可执行两相混合式步进电机TIM4 脉冲 两个 GPIO1.8°/步配细分驱动器电源12V 电池 稳压—5V 给传感器与液晶3.3V 给 MCU2.2 主控、传感器、执行器的选型账控制器在 51 和 STM32 之间选本质是在算力和开发成本之间选。51 单片机能跑 PID但两路 ADC 加 TFT 刷新加脉冲输出挤在一起中断响应会明显吃紧而且片内 RAM 放不下滤波窗口和波形缓存。STM32F103VET6 的 8 通道 12 位 ADC 配 DMA 可以完全不用 CPU 搬运定时器资源足够把控制周期和脉冲输出分开浮点运算虽然有软件模拟开销但 5ms 的控制周期完全扛得住。角度传感器的三个方案里普通碳膜电位器最先被排除线性度差、耐磨性差摆几次零位就漂了。ADXL345 看起来很美三轴加速度直接解算倾角但它是靠重力分量算角度的摆杆一甩离心加速度和切向加速度全部叠加进去动态过程中读出来的角度根本不能信——静态标定很准一动就翻车这是很多人第一次做摆类控制时踩的坑。WDD35D4 是导电塑料电阻体纯机械接触式测量不受加速度影响只要把分压电路和 ADC 做好动态下也稳。电机在伺服和步进之间选是性能和成本的取舍。伺服闭环、惯量小、能高速制动但一套下来价格顶得上整个项目其余部分。步进电机开环控制16 细分之后每脉冲 0.1125°对于平板这种小角度补偿够用低频振动靠细分和加减速压下去。代价是丢步没人告诉你所以代码里必须自己记账发出去多少脉冲理论上平板该转多少度一旦和传感器读数对不上就说明丢步了。2.3 电源链路与上电顺序12V 电池进来要分三路电机驱动器直接吃 12V传感器和液晶要 5VMCU 要 3.3V。原文方案是用 L7805 和 LM2576 做转换落地时建议把顺序调一下12V 先经 LM2576-ADJ 降到 7V 左右再进 L7805 降到 5V最后用 AMS1117-3.3 给 MCU。这样 7805 上的压差从 7V 降到 2V发热小很多不至于摸上去烫手——线性稳压器压差大、电流大发热是必然的加散热片只是补救。上电顺序也有讲究。正确顺序是先给控制板上电等 MCU 初始化完成、驱动器使能信号拉低之后再给驱动器动力电断电反过来先断动力电再断控制电。顺序反了会出现驱动器上电瞬间电机轴自己抖一下的情况平板被甩偏如果这时候程序正在采零位零位就标错了后面无论怎么调都差那么几度。FreeSwingPlate/ ├── CMSIS/ # 内核与启动文件 ├── STM32F10x_FWLib/ # 标准外设库 ├── user/ │ ├── main.c # 初始化 主循环显示、按键 │ ├── stm32f10x_it.c # 中断服务TIM4 脉冲、TIM3 控制节拍 │ ├── adc_dma.c # 双通道角度采样 │ ├── angle.c # 标定、滤波、相对角解算 │ ├── pid.c # 位置式 PID 前馈 │ ├── step.c # 步进脉冲发生与加减速 │ ├── tft.c # 液晶驱动与局部刷新 │ └── key.c # 按键标定、启停、参数加减 └── output/ # 编译产物把采样、解算、控制、执行拆成独立 .c 文件不是为了好看是为了调试时能单独把某一段摘出来跑。比如怀疑 PID 有问题就在 main 里塞一段固定目标值的测试代码绕开传感器直接喂角度两分钟就能判断是算法还是硬件。3. 角度采集链路WDD35D4 双通道采样与零位标定从传感器抽头到程序里一个可信的角度值中间隔着分压、采样、滤波、标定四道工序任何一道偷懒后面的 PID 都在拿噪声当误差算。这一章按照信号流动的顺序往下走每一段都给可抄的代码。3.1 模拟前端分压电阻、RC 与跟随器WDD35D4 本质是一个三端电位器两端接 5V 和 GND中间抽头随轴转动输出 05V。它常见有 1k、2k、5k 几种阻值阻值越大越省电但输出阻抗也越大ADC 采样时对采样保持电容充电就慢读数会偏低。常见做法是选 2k 或者 5k 的型号抽头后面串 10k 电阻、对地并 0.1μF 电容组成低通截止频率约 160Hz正好压在控制带宽之上把驱动器开关噪声挡掉。如果摆杆摆动幅度大、导线又长再加一级 LM358 电压跟随器把输出阻抗从几 kΩ 降到几十 ΩADC 读数会稳得多。跟随器的供电用同一路 5V输出直接进 PA0、PA1注意 PA0 同时是唤醒引脚上电瞬间若有电压可能影响启动稳妥的做法是换到 PA1、PA2把 PA0 空出来。3.2 双通道 ADC 加 DMA 的采样骨架两路角度要求同一时刻采样否则算相对角时会有相位差。用 ADC1 的规则组扫描模式一次连续转换两个通道DMA 循环搬运到内存CPU 一次都不用介入。/* adc_dma.c —— 规则组扫描 DMA 循环一次把摆杆、平板两路取回 */ #define ADC_BUF_LEN 32 static volatile uint16_t g_adc_raw[2][ADC_BUF_LEN]; /* [0]摆杆 [1]平板 */ void AngleAdc_Init(void) { GPIO_InitTypeDef gpio; ADC_InitTypeDef adc; DMA_InitTypeDef dma; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); /* 模拟输入必须配成 AIN配成浮空输入会引入额外漏电流 */ gpio.GPIO_Pin GPIO_Pin_1 | GPIO_Pin_2; gpio.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, gpio); DMA_DeInit(DMA1_Channel1); dma.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; /* 源ADC 数据寄存器 */ dma.DMA_MemoryBaseAddr (uint32_t)g_adc_raw; /* 目的二维数组首地址 */ dma.DMA_DIR DMA_DIR_PeripheralSRC; dma.DMA_BufferSize 2 * ADC_BUF_LEN; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; /* 内存地址递增 */ dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Circular; /* 循环模式永不停止 */ DMA_Init(DMA1_Channel1, dma); DMA_Cmd(DMA1_Channel1, ENABLE); ADC_DeInit(ADC1); adc.ADC_Mode ADC_Mode_Independent; adc.ADC_ScanConvMode ENABLE; /* 扫描一次转两个通道 */ adc.ADC_ContinuousConvMode ENABLE; adc.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfChannel 2; ADC_Init(ADC1, adc); /* 采样时间给到 55.5 周期导电塑料电位器输出阻抗不低太短会掉码 */ ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 2, ADC_SampleTime_55Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); /* 上电必须做一次自校准否则零位偏差能到十几个码 */ ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); }几个参数不能随手改。规则组通道顺序决定了数据落进内存的排列通道 1 排第 1通道 2 排第 2DMA 缓冲里就是 P1、P2、P1、P2 交替存放所以g_adc_raw[0]和g_adc_raw[1]是交错覆盖的读的时候按通道索引取——这类数组在调试器里看是一维的很容易把两个通道的数据串起来用结果摆杆角度跟着平板跑查半天查不出原因。ADC_BUF_LEN决定环形缓冲深度32 组在 5ms 控制周期下绰绰有余。采样时间 55.5 个 ADCCLK按 12MHz 算约 4.6μs两通道一路扫下来不到 10μs相对控制周期可以忽略。3.3 零位标定与角度换算原始码值本身没有意义要变成角度得先定两个点。做法是把摆杆竖直、平板调水平记下此时的码值作为 0°再把平板抬起一个已知角度用角度块或者量角器30° 比较好读记下第二个码值两点确定一条直线。/* angle.c —— 两点标定 中值均值混合滤波 */ typedef struct { float zero_code; /* 0 度对应的 ADC 码值 */ float deg_per_code; /* 每码值对应多少度 */ } AngleCalib_t; static AngleCalib_t s_calib[2]; /* [0]摆杆 [1]平板 */ static const float s_adc_full 4095.0f; /* 两点标定code_a 对应 0 度code_b 对应 deg_b 度 */ void Angle_Calib(uint8_t ch, uint16_t code_a, uint16_t code_b, float deg_b) { s_calib[ch].zero_code (float)code_a; s_calib[ch].deg_per_code deg_b / ((float)code_b - (float)code_a); } /* 窗口取 9 点排序取中值丢掉尖峰其余 8 点求均值压低随机噪声 */ static uint16_t Median9(const uint16_t *src) { uint16_t t[9]; uint8_t i, j; for (i 0; i 9; i) t[i] src[i]; for (i 0; i 8; i) for (j 0; j 8 - i; j) if (t[j] t[j 1]) { uint16_t tmp t[j]; t[j] t[j 1]; t[j 1] tmp; } return t[4]; } float Angle_Read(uint8_t ch) { uint16_t win[9]; uint32_t sum 0; uint8_t i; for (i 0; i 9; i) win[i] g_adc_raw[ch][i]; /* 取最近 9 个点 */ uint16_t med Median9(win); for (i 0; i 9; i) sum win[i]; float avg (float)(sum - med) / 8.0f; /* 去掉中值再平均 */ return (avg - s_calib[ch].zero_code) * s_calib[ch].deg_per_code; }zero_code和deg_per_code是这套采集链路唯一需要人工介入的两个数。标定前先让系统空跑两分钟等 7805 和电机驱动器的温度稳定再采因为热漂会让码值缓慢偏移冷机标定、热机使用跑十分钟就差出一度多。标定值建议写进 Flash 的最后一页上电读出来不然每次断电重来。滤波窗口长度 9 是权衡出来的结果5ms 控制周期下9 个点覆盖 45ms对应 22Hz 的有效带宽比机械摆动的基频通常在 12Hz高一个量级不会引入明显相位滞后窗口再长PID 的微分项就会拿到钝化的信号响应变肉。3.4 相对角解算符号搞错就全盘皆输两个传感器装的位置决定了公式长什么样必须先把符号约定钉死。我采用的约定是theta_rod为摆杆偏离竖直方向的角度向右摆为正theta_plate为平板相对摆杆的转角逆时针为正。那么平板相对水平面的倾角就是/* 平板相对水平面的倾角两个传感器读数之差符号由安装方向决定 */ float phi theta_plate - theta_rod;这个减法看起来简单但符号错了是新手最常见的翻车点。验证方法很土但有效用手把摆杆往右掰 20°观察phi的变化方向如果平板实际上被抬高了而phi变负就把某个传感器的deg_per_code加个负号或者调换分压的两端接线。别在代码里靠试错蒙符号蒙对了当下能用换一批硬件就得重来。算完phi再加一个死区判断绝对值小于 0.15° 时输出 0避免电机在目标点附近来回蹭。这个死区比编码器分辨率大一点比人眼能察觉的倾斜小得多正好卡在中间。4. 控制与执行PID 输出怎么变成步进电机的脉冲采集链路给出的是一个带噪声的角度控制环要做的事情是把它摁在 0° 附近并且把还需要转多少度翻译成发多少个脉冲、以多快的频率发。这一段是整套系统里最容易写出玄学参数的地方也是最能体现工程功底的地方。4.1 控制目标拆解稳的是相对角不是绝对角很多人一开始会把目标设成平板绝对水平代码里直接把phi的绝对值当误差结果摆杆摆到极限位置时平板怎么都追不上因为机械转角有限位。正确的目标是平板相对水平面的倾角趋近于 0而phi本身已经把这个目标表达清楚了PID 的输入就是0 - phi输出是平板还需要转过的角度增量。这里有个物理约束必须提前算清楚。假设摆杆最大摆角 ±45°平板要抵消这个角度电机输出轴至少得能转 ±45°再留 20% 余量。如果电机轴和平板轴之间加了 1:2 的减速电机要转 ±90°按 16 细分、1.8°/步算就是 90 / 0.1125 800 个脉冲。这个数字决定了脉冲频率的上限也决定了你能不能在一个摆动周期约 1.5s内把角度补回来。控制周期定在 5ms用 TIM3 定时中断触发。周期太短滤波窗口跟不上微分项全是噪声周期太长一个摆动周期内只有几十次修正机会追不上。5ms 对应 200Hz 的控制频率对 12Hz 的机械摆动来说有足够带宽。4.2 位置式 PID 的实现与限幅用位置式而不是增量式是因为输出直接对应还需要转多少度物理意义清楚调试时看到数字就知道平板要往哪边走。/* pid.c —— 位置式 PID输出单位是度 */ typedef struct { float kp, ki, kd; float i_sum; /* 积分累加 */ float i_max; /* 积分限幅单位度·秒 */ float err_last; float out_max; /* 输出限幅单位度 */ } PID_t; static PID_t s_pid { .kp 3.2f, .ki 0.06f, .kd 0.35f, .i_sum 0.0f, .i_max 40.0f, .err_last 0.0f, .out_max 45.0f }; float PID_Calc(PID_t *p, float target, float fb, float dt) { float err target - fb; /* target 恒为 0fb 就是 phi */ float d (err - p-err_last) / dt; /* dt 必须换算成秒 */ p-err_last err; p-i_sum err * dt; if (p-i_sum p-i_max) p-i_sum p-i_max; if (p-i_sum -p-i_max) p-i_sum -p-i_max; float out p-kp * err p-ki * p-i_sum p-kd * d; if (out p-out_max) out p-out_max; /* 限幅要和机械行程对齐 */ if (out -p-out_max) out -p-out_max; return out; }dt传 0.005单位是秒。这一行是最容易出问题的地方很多人把控制周期写成 5毫秒微分项就大了 1000 倍PID 直接发散电机像抽风一样左右猛甩。i_max和out_max也不能省摆杆摆到极限位置时误差持续存在积分会一直累加等摆杆回头积攒的积分项让电机猛冲过头这就是典型的积分饱和也是硬币飞出去的头号原因。kp决定响应刚度给大了系统在目标点附近高频抖动ki消除静态误差给大了超调明显kd抑制超调但会把传感器噪声放大成电机啸叫。这三个数的整定顺序在后面 4.5 节给一张现象对照表。4.3 前馈补偿用摆杆的角加速度提前半拍纯反馈的问题是永远慢半拍误差出现了才动作而摆杆的扰动是连续变化的。加一路前馈把摆杆的角加速度估出来直接叠加到电机指令上相当于提前把要发生的扰动抵消掉一部分。/* 由摆杆角度做二阶差分估角加速度前馈系数从 0 开始慢慢加 */ static float s_theta_prev 0.0f, s_v_prev 0.0f; static float s_k_ff 0.0f; /* 先置 0等 PID 整定完再逐步加到 0.2 左右 */ float FeedForward(float theta_rod, float dt) { float v_now (theta_rod - s_theta_prev) / dt; /* 一阶差分得角速度 */ float acc (v_now - s_v_prev) / dt; /* 二阶差分得角加速度 */ s_theta_prev theta_rod; s_v_prev v_now; return s_k_ff * acc; }二阶差分对噪声极其敏感如果theta_rod里还带着 0.5° 的抖动差分两次能放大到几十度每秒平方前馈输出直接把电机打成振动棒。所以前馈的输入必须是滤波之后的角度而且s_k_ff一定要在 PID 调稳之后再一点点加每次加 0.02观察波形一旦出现高频抖动立刻退回去。这个系数没有理论最优值靠试。4.4 从角度指令到脉冲细分、减速比与梯形加减速PID 输出的是平板还要转 2.3°这样的角度量要变成脉冲中间隔着细分、减速比、步距角三个换算。/* step.c —— 角度指令换算成脉冲数再用梯形加减速发出去 */ #define MOTOR_STEP_ANGLE 1.8f /* 步距角度 */ #define MICRO_STEP 16 /* 驱动器细分数 */ #define GEAR_RATIO 2.0f /* 电机转 2 圈平板转 1 圈 */ /* 平板转 1 度需要的脉冲数 减速比 / (步距角 / 细分) */ #define PULSE_PER_DEG (GEAR_RATIO / (MOTOR_STEP_ANGLE / MICRO_STEP)) void Step_MoveDeg(float deg) { int32_t pulses (int32_t)(deg * PULSE_PER_DEG); if (pulses 0) return; s_dir (pulses 0) ? 1 : 0; /* 方向脚1 正转 0 反转 */ DIR_GPIO_WRITE(s_dir); s_step_left (pulses 0) ? pulses : -pulses; s_accel_steps s_step_left / 3; /* 前 1/3 行程用于加减速 */ s_arr s_arr_min; /* 起步周期最长速度最慢 */ TIM4-ARR s_arr; TIM_Cmd(TIM4, ENABLE); } /* TIM4 更新中断每次中断翻转一次 STEP 电平一个完整脉冲占两次中断 */ void TIM4_IRQHandler(void) { if (TIM_GetITStatus(TIM4, TIM_IT_Update) RESET) return; TIM_ClearITPendingBit(TIM4, TIM_IT_Update); if (s_step_left 0) { TIM_Cmd(TIM4, DISABLE); s_busy 0; return; } s_level !s_level; if (s_level) STEP_GPIO_SET(); else { STEP_GPIO_CLR(); s_step_left--; } /* 剩余步数进入减速区线性拉长周期 */ if (s_step_left s_accel_steps s_arr s_arr_max) { s_arr s_accel_inc; if (s_arr s_arr_max) s_arr s_arr_max; TIM4-ARR s_arr; } }PULSE_PER_DEG算下来是 2.0 / (1.8/16) ≈ 17.8也就是平板每转 1 度要发 17.8 个脉冲。这个数决定了分辨率PID 输出 0.1° 时对应 1.8 个脉冲取整误差 0.05° 左右够用如果减速比提高到 1:5分辨率能到 0.02°但电机转速要求也跟着涨需要重新核算矩频特性。s_arr是定时器的自动重装值直接决定脉冲频率。s_arr_min对应最高速取值时先算电机在额定电流下的启动频率通常 1kHz 以内比较安全超过就容易丢步。加减速用最简单的线性方式剩 1/3 行程开始减速——步进电机不能急停从 2kHz 直接停会失步听到咔一声平板就偏了几度。方向脚DIR必须在发第一个脉冲之前至少提前 5μs 拉好驱动器需要建立时间。有些驱动器对方向信号的建立时间要求到 20μs保守起见在DIR_GPIO_WRITE之后加一句delay_us(20)。4.5 参数整定顺序与现象对照表整定不要三个参数一起动。先把ki、kd置 0只加kp从 0.5 开始往上加加到系统在目标点附近开始等幅振荡然后退回振荡值的 60%。接着加kd从 0.1 开始加到振荡被压下去、响应不再拖泥带水。最后加ki从 0.02 起加到静态误差消失就停再加就会超调。每改一次参数让摆杆完整摆三个来回再下结论因为系统的动态特性跟摆幅有关。现象大概率原因处理方向目标点附近高频抖动电机啸叫kd 过大把噪声放大了降 kd或加长滤波窗口摆动到极限位置后回中时猛冲积分饱和减小 i_max或在限位区暂停积分始终差 12°稳不下来ki 太小或有机械回差加 ki同时检查齿轮间隙响应明显滞后追不上摆杆kp 偏小或控制周期偏长加 kp确认控制周期没被刷屏拖慢换向后第一下没反应回差死区加反向补偿脉冲或单向逼近这张表是踩过一遍之后总结的实际调试时现象可能混着出现先按抖动 → 超调 → 静差的顺序逐层剥不要一次改三个参数。5. 避坑与排查五个让硬币掉下去的现场问题硬件焊完、代码烧进去第一次松手摆杆大概率硬币会飞。这一章列的五个问题是我在类似结构上反复遇到的每个都按现象—原因—解决写清楚照着对号入座能省不少时间。5.1 采样与控制链路里的三个坑第一个坑是 ADC 读数在小角度区跳两三个码。现象很直观平板静止不动液晶上的角度值在 -0.3° 到 0.3° 之间来回跳PID 的微分项把这个跳动放大成电机的小幅抖动手摸电机能感觉到持续的高频振动。原因有两层一是 WDD35D4 抽头输出阻抗偏高ADC 采样时间设成默认的 1.5 周期时采样保持电容充不满读数系统性偏低并且随通道切换跳变二是模拟前端没做 RC驱动器开关噪声顺着地线串进来。解决办法是把采样时间提到 55.5 周期抽头到 ADC 之间加 10kΩ 串阻和 0.1μF 对地电容软件上再叠一层 9 点中值加均值。做完这三步静止时的跳动能压到 ±0.05° 以内。第二个坑是控制周期被显示刷新拖成随机值。现象是刚上电调好的参数一切换到波形显示页面就开始振荡退回去又好了。原因是把 TFT 刷屏和 PID 计算塞进了同一个主循环刷一屏全屏要十几毫秒PID 的dt却还按 5ms 算相当于微分项凭空大了三倍。解决很干脆PID 计算、角度解算、脉冲发生全部放进 TIM3 的 5ms 中断主循环只负责按键扫描和显示显示降频到 100ms 刷一次并且只刷变化的数字区域不刷整屏。中断里绝不要调用任何带阻塞的延时函数哪怕delay_ms(1)也不行。第三个坑是零位缓慢漂移。跑十分钟角度零点从 0° 慢慢偏到 1.5°怎么调 PID 都压不住。原因不是算法是 7805 发热导致 5V 输出轻微下降而传感器是比率式输出供电变了码值就变偏偏标定值还是冷机时存的。如果 ADC 的参考电压用的是 VDDA 而不是外部基准这个漂移会全额反映到读数上。解决有两个方向硬件上把 5V 换成基准更好的稳压或者给传感器单独供电软件上每次上电自动采一次零位前提是上电时摆杆确实竖直并且用比值法而不是绝对码值ratio raw / 4095角度只跟比值有关跟绝对电压无关。温度补偿在课程设计里不现实上电归零是性价比最高的做法。5.2 机械结构与驱动时序里的两个坑第四个坑是换向死区。现象是平板从右往左追的时候前 12° 电机在转但平板不动等间隙吃掉之后突然一顿冲过头再往回找形成极限环。这是齿轮或者同步带的回差属于机械问题软件只能缓解不能根治。缓解办法是单向逼近不管目标在哪边最后一段行程永远从同一侧靠近比如始终让电机正转到位需要反转时先多转 2° 再正转回来。另外在换向时补几个脉冲填间隙补偿量实测确定一般 815 个脉冲。根治要靠机械同步带加预紧轮或者用弹簧消除齿轮间隙。第五个坑是大摆幅时丢步。现象是摆杆摆到 ±40° 附近平板明显跟不上回头看传感器读数累计偏差好几度而且越摆越大。原因通常是加速度超过了步进电机的矩频特性——电机转速越高输出扭矩越小PID 一着急就把脉冲频率拉到极限电机直接失步还不报警。也可能是驱动器电流拨码设小了或者细分设得太高导致每步扭矩不够。解决办法是限制加减速斜率把s_accel_inc调大让加速更平缓同时把驱动器拨码调到电机额定电流比如 42 步进电机设 1.5A细分数在 816 之间选不要盲目追高。真要彻底解决得在平板轴上加一个绝对式编码器做闭环丢步了能自己找回来。5.3 把液晶和串口当示波器用调这种快节奏的闭环靠盯液晶上的数字没用人眼跟不上。实用的做法是开一路串口每 5ms 打一行 CSV格式是phi, theta_rod, out, pulses上位机用任意串口工具存成文件再用表格软件画曲线。看三样东西phi的收敛速度、out有没有饱和、pulses和phi的变化是否对得上。对不上就说明有丢步或者回差。液晶的作用是现场快速判断显示三行就够当前倾角、PID 输出、剩余脉冲数。刷新区域限定在固定坐标的十几个字符用局部刷新函数别整屏清。按键留两个做参数加减调 PID 时不用反复烧程序这个改动十分钟就能加能省下大量编译下载时间。6. 二次开发把定题Demo改成能复用的闭环框架这套代码写完交作业够用但真要复用到别的项目上第一件事是把控制核心和执行层解耦。现在的PID_Calc直接操作全局变量Step_MoveDeg直接改定时器寄存器两者黏在一起换个执行机构就得大改。我一般的做法是抽一个控制器接口出来/* controller.h —— 控制器的统一接口换算法不用动上层 */ typedef struct { void (*init)(void); float (*update)(float target, float feedback, float dt); /* 返回控制量 */ void (*reset)(void); } Controller_t; extern const Controller_t ctrl_pid; /* 位置式 PID 实现 */ extern const Controller_t ctrl_pd_ff; /* PD 前馈实现 */ /* 执行层也只暴露一个角度接口内部爱用步进还是舵机都行 */ typedef struct { void (*move_deg)(float deg); int (*is_busy)(void); } Actuator_t;这样一来main里只依赖这两个结构体指针换算法就是换一行赋值。想试纯 PD、想试模糊控制、想加 Smith 预估器都只动一个 .c 文件。这套抽象在课程设计里看起来有点小题大做但你只要做过第二个类似项目就会庆幸当初分了层。第二件事是把参数搬到串口协议上。定义一个极简的文本协议P 3.2改 kpI 0.06改 kiD 0.35改 kdS保存到 FlashR恢复默认。解析用状态机别用sscanf那种重家伙中断里跑不动。有了这个调参从改代码—编译—下载—松手的四步循环变成敲一行回车效率差好几倍。保存到 Flash 要注意擦写寿命只在按S的时候写一次别每次改都写。第三件事是验证方法要固定下来不能凭感觉说差不多了。我通常跑三个测试阶跃测试把平板人为掰偏 10° 松手看超调量和稳定时间超调控制在 15% 以内、稳定时间 1s 以内算合格正弦跟踪用手匀速摆动摆杆看phi的幅值衰减和相位滞后衰减超过 30% 说明带宽不够跌落测试硬币放在平板上摆杆拉到最大角度松手连续十次不掉算过。这三个测试跑完参数才算定下来换一块板子重跑一遍还能过就说明这套参数有余量。最后说个习惯。从那以后我每次改完控制参数都会先用串口把phi、out、pulses三条曲线存一份文件命名带上日期和参数值调到后面忘了哪组参数好翻文件比翻记忆靠谱得多。这套东西看着笨但在需要反复试错的闭环调试里留痕就是后悔药。希望帮到你。本文还有配套的精品资源点击获取