1. 系统架构与工程目标

在嵌入式云台控制系统中,实现“丝滑”运动的本质,不是单纯追求高刷新率或大扭矩,而是建立一套 可预测、可收敛、响应与平滑兼顾 的闭环控制逻辑。本项目以STM32F103C8T6(立创EDA推荐的国产替代主流型号)为核心控制器,通过ADC采集电位器模拟电压,经数字滤波后映射为0–180°舵机角度指令;再通过HC-05蓝牙模块构建主从双机无线链路,实现远程姿态遥控。整个系统需同时满足三项硬性指标:
- 实时性 :主循环周期稳定在25 ms(40 Hz),确保人眼不可察觉的运动断续;
- 稳定性 :消除ADC采样噪声导致的角度跳变,抑制舵机因指令突变产生的机械抖动与高频啸叫;
- 动态适应性 :在小角度微调(±3°内)时保持亚度级分辨率和平滑过渡,在大角度快速转向(>90°)时具备足够追赶能力,避免“拖尾感”。

这一目标无法通过裸机轮询+直接赋值实现。原始江邪科技模板中“按键直设角度”的逻辑,其本质是开环阶跃响应——每次按键触发一次角度跳变,舵机驱动芯片(如SG90内部PWM发生器)接收到新占空比后立即执行最大加速度转向,必然引发机械冲击与位置超调。而真实云台应用(如FPV图传云台、智能巡检机器人)要求的是 连续轨迹跟踪 ,即目标角度是一条时间连续函数 θₜ(t),而非离散点集。因此,必须在软件层引入 运动规划(Motion Profiling) 思想,将目标角度差 Δθ = |θ_target − θ_current| 转化为受控的速度指令,再由舵机自身动力学完成位置闭环。

本方案摒弃了常见的“固定步长+延时阻塞”粗暴方式(如每次加1°后 HAL_Delay(10) ),转而采用 一阶低通滤波器(First-Order Low-Pass Filter, FO-LPF)作为运动软化核心 ,配合动态步长策略,在单片机资源受限(无浮点协处理器、RAM仅20 KB)前提下达成毫秒级响应与亚度级精度的统一。

2. ADC信号调理与数字滤波设计

2.1 电位器接口与ADC配置原理

电位器作为最基础的模拟角度传感器,其输出为0–3.3 V连续电压,对应STM32的12位ADC输入范围(0–4095)。但直接读取原始AD值存在三重干扰源:
- 电源纹波 :USB供电或LDO输出的10–50 mV低频波动,叠加在有效信号上;
- 接触噪声 :碳膜电位器滑动端与电阻体间微小火花产生的尖峰脉冲(典型宽度<1 μs,幅值可达VDD);
- PCB耦合干扰 :临近数字走线(如SWD、USART TX)的开关噪声通过寄生电容注入模拟地。

硬件层面已通过以下措施抑制:
- 在电位器输出端并联100 nF陶瓷电容(X7R)至模拟地,构成RC低通(fc ≈ 1.6 kHz),滤除高频毛刺;
- 模拟地(AGND)与数字地(GND)单点连接于稳压芯片地端,切断地环路;
- ADC通道(如PA0)布线远离高速信号线,长度<2 cm。

软件层面,HAL库默认的单次转换模式( HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY); )虽简单,但存在 采样时刻不确定性 ——主循环中任意位置调用,易与系统其他中断(如SysTick)冲突,导致采样间隔抖动。因此,必须启用 定时器触发ADC规则组转换 。本方案选用TIM2作为触发源:

// TIM2配置为1 kHz更新事件(1 ms周期)
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71;      // APB1=72 MHz → 72 MHz/(71+1)=1 MHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 999;        // 1 MHz / 1000 = 1 kHz
HAL_TIM_Base_Init(&htim2);
HAL_TIM_Base_Start(&htim2);

// ADC配置为外部触发,来源为TIM2 TRGO
hadc1.Instance = ADC1;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.ScanConvMode = DISABLE; // 单通道,无需扫描
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO; // 关键!
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING;
HAL_ADC_Init(&hadc1);

此配置使ADC采样严格同步于TIM2计数器溢出时刻,采样间隔恒定为1 ms,为后续数字滤波提供等间隔输入序列,这是滑动平均与一阶低通算法成立的前提。

2.2 滑动平均滤波的局限性分析

初始方案采用5点滑动平均(Moving Average, MA-5):维护一个长度为5的环形缓冲区,每次新采样覆盖最旧值,求和后右移2位(等效除以4)得整数结果。代码结构如下:

#define MA_SIZE 5
uint16_t ma_buffer[MA_SIZE];
uint8_t ma_index = 0;
uint32_t ma_sum = 0;

uint16_t moving_average_filter(uint16_t new_sample) {
    ma_sum -= ma_buffer[ma_index];   // 减去将被覆盖的旧值
    ma_buffer[ma_index] = new_sample;
    ma_sum += new_sample;
    ma_index = (ma_index + 1) % MA_SIZE;
    return (uint16_t)(ma_sum >> 2);  // /4
}

该算法确能显著抑制随机噪声,实测ADC原始值标准差从±12 LSB降至±3 LSB。但其本质是 FIR滤波器 ,频率响应为 H(z) = (1 + z⁻¹ + z⁻² + z⁻³ + z⁻⁴)/5 ,存在明显缺陷:
- 相位延迟固定为2个采样周期 (即2 ms),当电位器快速旋转时,滤波输出始终滞后于真实角度,表现为云台“跟丢”现象;
- 对阶跃响应产生过冲与振铃 :当电位器从0°突变至180°,MA-5输出呈现“阶梯式上升”,第1–5次输出依次为36°、72°、108°、144°、180°,舵机被迫执行5次离散跳变,机械冲击不减反增;
- 无法区分噪声与有效信号变化率 :微小抖动与缓慢调节均被同等平滑,牺牲了小角度调节的灵敏度。

实验验证中,MA-5虽使OLED显示数值稳定,但舵机转动仍存在“咔哒”声,且快速反向调节时出现明显滞后(约0.5 s),证明其仅解决表象噪声,未触及运动控制本质。

2.3 一阶低通滤波器(FO-LPF)的工程实现

一阶低通滤波器传递函数为 H(s) = ω₀ / (s + ω₀) ,离散化后差分方程为:
y[n] = α × x[n] + (1−α) × y[n−1]
其中 x[n] 为当前ADC采样值, y[n] 为滤波输出, α 为滤波系数(0 < α < 1),决定系统时间常数 τ = (1−α)/(α×fs) ( fs 为采样率)。

该算法优势在于:
- 零相位延迟 (理论上):输出仅依赖当前与历史值,无固定延迟周期;
- 自适应响应 :α越大,系统越“激进”,对快速变化跟踪越好,但平滑性下降;α越小,系统越“保守”,抗噪强但响应迟钝;
- 计算极简 :仅需一次乘法(可用查表或移位近似)、两次加法,无循环缓冲区开销。

在STM32F103上,为规避浮点运算(HAL库未启用FPU),采用 定点Q15格式 实现:将α放大2^15倍(32768),所有运算在int32_t域进行,最终右移15位。选定α = 0.3,对应Q15值为9830(0.3 × 32768)。核心函数如下:

#define ALPHA_Q15 9830   // 0.3 in Q15
#define ONE_MINUS_ALPHA_Q15 (32768 - ALPHA_Q15)

int32_t lpf_state = 0; // 初始化为0

uint16_t first_order_lpf(uint16_t x_n) {
    int32_t x_q15 = (int32_t)x_n << 15; // 转Q15
    int32_t y_q15 = (ALPHA_Q15 * x_q15 + ONE_MINUS_ALPHA_Q15 * lpf_state) >> 15;
    lpf_state = y_q15;
    return (uint16_t)(y_q15 >> 15); // 转回uint16_t
}

此实现每调用一次耗时约1.2 μs(72 MHz主频),远低于25 ms主循环窗口,且内存占用仅8字节(lpf_state为int32_t)。

α = 0.3 的选择依据实测:
- 当电位器以中等速度(约2 s/圈)旋转时,滤波输出与真实角度偏差<0.5°;
- 对接触噪声(单点尖峰>500 LSB)抑制比达99.2%,残余波动<5 LSB;
- 阶跃响应无过冲,95%稳定时间约12 ms(对应3个采样点),完全匹配舵机动态特性。

3. 舵机运动规划:动态步长算法

3.1 为什么固定步长必然失败?

舵机(以SG90为例)内部是一个带位置反馈的闭环系统:接收PWM信号(周期20 ms,高电平0.5–2.5 ms对应0–180°),内部比较器生成误差信号,驱动电机转向直至反馈电位器电压匹配目标。其物理极限为:
- 最大角速度 :约0.1 s/60°(即600°/s),受限于电机扭矩与负载惯量;
- 最小可控步进 :理论为0.1°,但受齿轮间隙与摩擦影响,实际分辨率为1–2°;
- 启动/停止惯性 :电机从静止到全速需3–5 ms,急停会产生反向电动势震荡。

若主循环中采用固定步长(如每次+1°)并 HAL_Delay(10) ,则:
- 步长1°时,单次运动时间≈17 ms(含启动延迟),10 ms延时导致总周期27 ms,但舵机实际已到位,造成无效等待;
- 步长10°时,单次运动时间≈170 ms,10 ms延时完全淹没在运动过程中,主循环失去节奏,OLED刷新不同步;
- 更致命的是,固定步长无法处理 方向切换 :当目标从170°突变至10°,Δθ=160°,若仍按+1°步进,需160次循环(1.6 s)才能到达,期间舵机持续向错误方向转动,产生巨大机械应力。

因此,“丝滑”的核心是让舵机运动轨迹逼近理想的一阶惯性环节: θ(t) = θ_target × (1 − e^(−t/τ)) ,其中τ由步长动态调整。

3.2 动态步长算法设计与参数整定

本方案定义 步长Step = f(|Δθ|) ,分三段线性映射:
- 微调区 (|Δθ| ≤ 10°):Step = Step_min = 1°
目的 :保证亚度级分辨率,避免小幅度抖动被放大。1°步长对应SG90约0.3 ms PWM宽度变化,处于其线性响应区,无死区。
- 线性区 (10° < |Δθ| < 90°):Step = Step_min + (Step_max − Step_min) × (|Δθ| − 10) / 80
目的 :建立平滑过渡,避免段落切换处的运动突变。斜率由经验确定:80°跨度分配80单位步长增量,保证每度变化对应1单位步长变化,符合直觉。
- 快速区 (|Δθ| ≥ 90°):Step = Step_max = 10°
目的 :最大化追赶速度。10°步长对应SG90约3 ms PWM跳变,仍在电机响应线性区,可避免饱和失真。

算法伪代码:

int16_t calculate_step(int16_t delta_theta) {
    uint16_t abs_delta = (delta_theta < 0) ? -delta_theta : delta_theta;
    if (abs_delta <= 10) {
        return 1;
    } else if (abs_delta < 90) {
        return 1 + (10 - 1) * (abs_delta - 10) / 80; // 整数除法,自动截断
    } else {
        return 10;
    }
}

// 主循环中调用
int16_t target_angle = ...; // 滤波后映射的0-180值
int16_t current_angle = ...; // 当前舵机实际角度(通过反馈或开环积分)
int16_t delta = target_angle - current_angle;

if (abs(delta) > 1) { // 避免反复微调
    int16_t step = calculate_step(delta);
    if (delta > 0) {
        current_angle += step;
        if (current_angle > 180) current_angle = 180;
    } else {
        current_angle -= step;
        if (current_angle < 0) current_angle = 0;
    }
    set_servo_pulse(current_angle); // 更新PWM
}

此算法使舵机运动呈现“慢启-匀速-缓停”特征:小偏差时细腻微调,大偏差时果断加速,接近目标时自动降速,彻底消除机械冲击。实测从0°到180°转动时间稳定在0.85 s,全程无啸叫,OLED显示角度曲线光滑如正弦上升。

4. HC-05蓝牙主从通信协议设计

4.1 HC-05硬件连接与AT指令配对

HC-05工作在UART模式,需注意三点关键电气特性:
- 电平兼容性 :HC-05为3.3 V TTL电平,STM32F103 PA9/PA10(USART1)同样为3.3 V,可直连,无需MAX232电平转换;
- 供电能力 :HC-05峰值电流达40 mA,STM32的3.3 V引脚(通常由AMS1117-3.3提供)需确保输出电流≥100 mA,建议单独供电;
- AT指令模式 :需将KEY引脚拉高(接VCC)进入AT模式,此时模块指示灯慢闪(2 s周期),可用USB-TTL工具发送指令。

核心AT指令序列(波特率默认38400):

AT          // 测试通信,返回OK
AT+NAME=Master  // 设置主设备名称
AT+ROLE=1       // 1=Master, 0=Slave
AT+CMODE=0      // 0=指定地址连接,1=任意从机
AT+BIND=0011,223344  // 绑定从机MAC地址(需先查从机AT+ADDR?)
AT+INIT       // 初始化SPP
AT+INQ        // 查询附近从机(主设备专用)
AT+PAIR=0011,223344,10 // 配对从机,10秒超时
AT+LINK=0011,223344    // 建立连接

从机配置更简单:

AT+NAME=Slave
AT+ROLE=0
AT+CMODE=0
AT+INIT

配对成功后,主从机间建立透明串口通道,所有数据透传,无协议开销。

4.2 通信协议帧结构与防粘包设计

无线信道存在误码与丢包,必须设计健壮协议。本方案采用 自定义二进制帧 ,摒弃ASCII字符串(节省带宽、避免解析开销):
| 字段 | 长度 | 值 | 说明 |
|------|------|----|------|
| SOF | 1 byte | 0xAA | 帧起始符,避免与数据混淆 |
| Angle | 1 byte | 0–180 | 目标角度,直接映射,无符号整数 |
| Checksum | 1 byte | (0xAA + Angle) & 0xFF | 简单校验,检测单比特错误 |
| EOF | 1 byte | 0x55 | 帧结束符,增强边界识别 |

该帧长仅4字节,25 ms周期内可传输40帧(160 bytes/s),远超需求。接收端状态机实现:

typedef enum {
    WAIT_SOF,
    GET_ANGLE,
    GET_CHECKSUM,
    WAIT_EOF
} rx_state_t;

rx_state_t rx_state = WAIT_SOF;
uint8_t rx_buffer[4];
uint8_t rx_index = 0;

void uart_rx_callback(uint8_t data) {
    switch(rx_state) {
        case WAIT_SOF:
            if (data == 0xAA) {
                rx_buffer[0] = data;
                rx_index = 1;
                rx_state = GET_ANGLE;
            }
            break;
        case GET_ANGLE:
            rx_buffer[1] = data;
            rx_index = 2;
            rx_state = GET_CHECKSUM;
            break;
        case GET_CHECKSUM:
            rx_buffer[2] = data;
            if ((rx_buffer[0] + rx_buffer[1]) == rx_buffer[2]) {
                rx_state = WAIT_EOF;
            } else {
                rx_state = WAIT_SOF; // 校验失败,重置
            }
            break;
        case WAIT_EOF:
            if (data == 0x55) {
                target_angle = rx_buffer[1]; // 解析成功
                // 触发运动规划
            }
            rx_state = WAIT_SOF;
            break;
    }
}

此设计杜绝了“粘包”(多帧合并)与“拆包”(单帧分多次中断)问题,校验机制可过滤99%的无线误码。

4.3 主从机软件协同优化

  • 主机(遥控端)优化 :
  • 新增 last_sent_angle 变量,仅当 |current_angle − last_sent_angle| ≥ 1 时才组帧发送,避免冗余流量;
  • 最大步长提升至30°(原10°),系数α增至0.6,加快大角度响应——因主机无舵机,可激进滤波;
  • OLED刷新独立于主循环,使用TIM3触发中断(50 ms周期),避免 HAL_Delay() 阻塞通信。

  • 从机(云台端)优化 :

  • 接收角度先限幅: target_angle = CLAMP(recv_angle, 0, 180) ;
  • 动态步长区间调整:微调区缩至|Δθ| ≤ 3°(更高精度),线性区扩展至3–15°,快速区门槛降至15°——因从机需更快收敛;
  • 差值小于步长时直接置位: if (abs(delta) < step) current_angle = target_angle; ,消除最后1°爬行。

主从机主循环均采用 HAL_Delay(25) ,确保25 ms严格同步,避免因时钟漂移导致的响应错位。

5. 系统集成与实测验证

5.1 完整初始化流程

系统启动后,按严格时序初始化各外设,顺序不可颠倒:
1. RCC与时钟树 :启用HSI/PLL,配置SYSCLK=72 MHz,AHB=72 MHz,APB1=36 MHz,APB2=72 MHz;
2. GPIO :配置PA0(ADC1_IN0)、PA9/PA10(USART1_TX/RX)、PB0/PB1(TIM3_CH2/CH3,舵机PWM)、PA1(OLED_SCL)、PA2(OLED_SDA);
3. ADC :按2.1节配置TIM2触发;
4. USART1 :初始化为38400 bps,8N1,无流控;
5. TIM2 :1 kHz更新中断,用于ADC触发;
6. TIM3 :配置为PWM模式,CH2/CH3互补输出(占空比0.5–2.5 ms对应0–180°),预分频71,自动重载19999(20 ms周期);
7. OLED :SSD1306驱动,I²C模式,初始化后清屏;
8. 全局变量 : lpf_state=0 , current_angle=90 (中位), last_sent_angle=90 。

此流程确保所有外设在使能前已正确配置寄存器,避免未定义行为。

5.2 实测性能数据

在标准实验室环境(25°C,无强电磁干扰)下,使用示波器捕获TIM3_CH2输出PWM波形,并用高精度角度仪(精度±0.1°)测量SG90实际转动,结果如下:

测试项 指标 实测值
ADC噪声抑制 原始值标准差 ±12.3 LSB
滤波后标准差 ±2.1 LSB
运动平滑性 小角度(±5°)抖动 <0.3°(OLED显示稳定)
大角度(0→180°)时间 0.83–0.87 s(5次测量均值)
反向响应(180→0°)时间 0.85–0.89 s
蓝牙可靠性 1米距离丢包率 0.02%(10,000帧测试)
通信延迟(主机按键到从机动作) 38–42 ms(含25 ms主循环+13 ms UART传输)
系统资源 Flash占用 24.8 KB(Keil MDK)
RAM占用 4.2 KB(含栈、堆、全局变量)

关键波形验证:示波器捕获TIM3_CH2在0→180°过程中的PWM高电平宽度,呈现完美指数上升曲线(τ≈120 ms),证实动态步长算法成功模拟了一阶惯性环节。

5.3 常见问题与实战排错

  • 问题:舵机转动有规律“咔哒”声,OLED角度跳变
    原因 :ADC采样未同步,滑动平均缓冲区索引错乱,或LPF系数α过大(>0.5)导致响应过激。
    解决 :检查TIM2是否启动, HAL_ADC_Start_IT() 是否误用(应为 HAL_ADC_Start() ),重新烧录固件并用ST-Link Utility查看RAM中 lpf_state 值是否稳定增长。

  • 问题:蓝牙连接后无反应,OLED显示“NO DATA”
    原因 :HC-05未进入数据模式(KEY引脚未接地),或AT指令配置错误导致主从未绑定。
    解决 :用USB-TTL工具连接HC-05,发送 AT+STATE? 确认状态为 CONNECTED ;若为 INITIALIZED ,需重新执行 AT+LINK ;检查STM32 USART1的RX引脚是否虚焊。

  • 问题:快速旋转电位器时云台明显滞后
    原因 :LPF系数α过小(<0.2),或动态步长线性区斜率设置过缓。
    解决 :将α从0.3临时改为0.5,观察响应;若改善,逐步回调至0.4;检查 calculate_step() 中分母80是否误写为800。

  • 问题:舵机到达目标后轻微“嗡嗡”震动
    原因 :步长过小(如Step_min=0.5°),导致在目标附近反复1°微调,电机无法完全停稳。
    解决 :将Step_min设为1°,并在 if (abs(delta) < step) 分支中直接赋值,消除微调循环。

我在实际项目中曾因忽略ADC触发源配置,导致MA-5滤波输出随CPU负载波动,云台在WiFi路由器旁工作时抖动加剧。后来发现是TIM2时钟未使能( __HAL_RCC_TIM2_CLK_ENABLE() 遗漏),补上后问题消失。这提醒我们:嵌入式调试的第一原则是 逐级验证时钟与使能信号 ,而非急于修改算法。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐