STM32 解码遥控器 PWM、PPM、S.Bus 协议:标准库与 HAL 库实现整理

1. 前言

航模、车模、无人机遥控接收机常见输出方式主要有三种:

协议 引脚数量 STM32 常用外设 特点
PWM 每个通道 1 根信号线 定时器输入捕获 简单直观,但占用引脚多
PPM 多个通道共用 1 根信号线 定时器输入捕获 接线少,靠脉冲间隔区分通道
S.Bus 1 根串口信号线 USART/UART 数字协议,通道多,通常需要反相

在这里插入图片描述

本文以 STM32 为例,整理 PWM、PPM、S.Bus 的解码方法,并分别给出标准库版本和 HAL 库版本的核心代码。

说明:

  • 示例默认 STM32 主频为 72MHz,例如 STM32F103。
  • 定时器预分频设置为 72 - 1,得到 1MHz 计数频率,即 1 个计数 = 1us
  • 具体引脚和定时器通道需要根据自己的芯片和工程修改。

2. PWM 遥控信号解码

2.1 PWM 信号特点

普通遥控 PWM 通常为:

周期:约 20ms
高电平宽度:约 1000us ~ 2000us
中位:约 1500us

对应关系一般可以理解为:

1000us -> 摇杆最小
1500us -> 摇杆中位
2000us -> 摇杆最大

PWM 的缺点是:每个通道都需要一根信号线。例如 6 通道接收机就要接 6 个 STM32 输入捕获引脚。

2.2 PWM 接线示意图

在这里插入图片描述

注意:接收机和 STM32 必须共地。接收机信号线电平是否能直接接 STM32,需要根据接收机输出电平和 STM32 IO 是否 5V 容忍判断。

2.3 PWM 时序图

在这里插入图片描述

PWM 解码核心:

高电平宽度 = 下降沿捕获值 - 上升沿捕获值

3. 标准库解码 PWM

下面以 PA0 -> TIM2_CH1 为例。

#include "stm32f10x.h"

volatile uint16_t pwm_rise = 0;
volatile uint16_t pwm_width = 1500;

void TIM2_PWM_Input_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_ICInitTypeDef TIM_ICInitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1;
    TIM_TimeBaseStructure.TIM_Period = 0xFFFF;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
    TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_BothEdge;
    TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
    TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
    TIM_ICInitStructure.TIM_ICFilter = 0x03;
    TIM_ICInit(TIM2, &TIM_ICInitStructure);

    TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE);

    NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    TIM_Cmd(TIM2, ENABLE);
}

void TIM2_IRQHandler(void)
{
    uint16_t capture;

    if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET)
    {
        TIM_ClearITPendingBit(TIM2, TIM_IT_CC1);
        capture = TIM_GetCapture1(TIM2);

        if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0))
        {
            pwm_rise = capture;
        }
        else
        {
            pwm_width = capture - pwm_rise;
        }
    }
}

pwm_width 的单位是 us。


4. HAL 库解码 PWM

TIM_HandleTypeDef htim2;

volatile uint16_t pwm_rise = 0;
volatile uint16_t pwm_width = 1500;

void MX_TIM2_Init(void)
{
    TIM_IC_InitTypeDef sConfigIC = {0};

    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 72 - 1;
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 0xFFFF;
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_IC_Init(&htim2);

    sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_BOTHEDGE;
    sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
    sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
    sConfigIC.ICFilter = 3;
    HAL_TIM_IC_ConfigChannel(&htim2, &sConfigIC, TIM_CHANNEL_1);

    HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1);
}

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    uint16_t capture;

    if (htim->Instance == TIM2 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
    {
        capture = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);

        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_SET)
        {
            pwm_rise = capture;
        }
        else
        {
            pwm_width = capture - pwm_rise;
        }
    }
}

5. PPM 遥控信号解码

5.1 PPM 信号特点

PPM 把多个通道合并到一根线上。

常见特征:

通道值:约 1000us ~ 2000us
同步间隔:通常大于 3000us
一帧周期:通常约 20ms

PPM 解码核心不是测量高电平宽度,而是测量相邻两个上升沿之间的时间差。

5.2 PPM 时序图

在这里插入图片描述

PPM 解码核心:

diff = 当前上升沿捕获值 - 上一次上升沿捕获值

if diff > 同步阈值:
    新一帧开始,通道索引清零
else:
    diff 就是当前通道值

6. 标准库解码 PPM

下面以 PA6 -> TIM3_CH1 为例。

#define PPM_CHANNEL_NUM 8
#define PPM_SYNC_TIME   3000

volatile uint16_t ppm_channels[PPM_CHANNEL_NUM];
volatile uint8_t ppm_index = 0;
volatile uint16_t ppm_last_capture = 0;

void TIM3_PPM_Input_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_ICInitTypeDef TIM_ICInitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1;
    TIM_TimeBaseStructure.TIM_Period = 0xFFFF;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
    TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

    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 = 0x03;
    TIM_ICInit(TIM3, &TIM_ICInitStructure);

    TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE);

    NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    TIM_Cmd(TIM3, ENABLE);
}

void TIM3_IRQHandler(void)
{
    uint16_t capture;
    uint16_t diff;

    if (TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET)
    {
        TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);

        capture = TIM_GetCapture1(TIM3);
        diff = capture - ppm_last_capture;
        ppm_last_capture = capture;

        if (diff > PPM_SYNC_TIME)
        {
            ppm_index = 0;
        }
        else
        {
            if (ppm_index < PPM_CHANNEL_NUM)
            {
                ppm_channels[ppm_index++] = diff;
            }
        }
    }
}

7. HAL 库解码 PPM

#define PPM_CHANNEL_NUM 8
#define PPM_SYNC_TIME   3000

TIM_HandleTypeDef htim3;

volatile uint16_t ppm_channels[PPM_CHANNEL_NUM];
volatile uint8_t ppm_index = 0;
volatile uint16_t ppm_last_capture = 0;

void MX_TIM3_Init(void)
{
    TIM_IC_InitTypeDef sConfigIC = {0};

    htim3.Instance = TIM3;
    htim3.Init.Prescaler = 72 - 1;
    htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim3.Init.Period = 0xFFFF;
    htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_IC_Init(&htim3);

    sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
    sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
    sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
    sConfigIC.ICFilter = 3;
    HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_1);

    HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
}

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    uint16_t capture;
    uint16_t diff;

    if (htim->Instance == TIM3 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
    {
        capture = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
        diff = capture - ppm_last_capture;
        ppm_last_capture = capture;

        if (diff > PPM_SYNC_TIME)
        {
            ppm_index = 0;
        }
        else
        {
            if (ppm_index < PPM_CHANNEL_NUM)
            {
                ppm_channels[ppm_index++] = diff;
            }
        }
    }
}

8. S.Bus 协议解码

8.1 S.Bus 基本特点

S.Bus 是常见数字遥控协议,常用于 Futaba、FrSky、Radiolink 等接收机。

常见串口参数:

项目 参数
波特率 100000
数据位 8 位
校验 偶校验
停止位 2 位
简写 8E2
电平 通常为反相 UART
帧长度 25 字节
通道 16 个比例通道 + 2 个数字通道

S.Bus 一帧结构:

Byte 0     : 0x0F,帧头
Byte 1-22  : 16 个通道数据,每个通道 11bit
Byte 23    : 标志位
Byte 24    : 帧尾,常见为 0x00

8.2 S.Bus 接线示意图

在这里插入图片描述

注意:

  • S.Bus 通常是反相信号。
  • 如果 STM32 支持 UART RX 反相,可以直接开启硬件反相。
  • 如果使用 STM32F1 这类通常不支持硬件反相的芯片,一般需要外接三极管、MOS 管、反相器或光耦反相电路。

8.3 S.Bus 帧结构图


在这里插入图片描述

9. S.Bus 通道解析函数

标准库和 HAL 库可以共用下面的解析函数。

#include <stdint.h>

#define SBUS_FRAME_LEN 25

uint16_t sbus_channel[16];
uint8_t sbus_ch17;
uint8_t sbus_ch18;
uint8_t sbus_lost_frame;
uint8_t sbus_failsafe;

void SBUS_Decode(uint8_t *buf)
{
    if (buf[0] != 0x0F)
    {
        return;
    }

    sbus_channel[0]  = ((buf[1]     | buf[2]  << 8) & 0x07FF);
    sbus_channel[1]  = ((buf[2]>>3  | buf[3]  << 5) & 0x07FF);
    sbus_channel[2]  = ((buf[3]>>6  | buf[4]  << 2 | buf[5] << 10) & 0x07FF);
    sbus_channel[3]  = ((buf[5]>>1  | buf[6]  << 7) & 0x07FF);
    sbus_channel[4]  = ((buf[6]>>4  | buf[7]  << 4) & 0x07FF);
    sbus_channel[5]  = ((buf[7]>>7  | buf[8]  << 1 | buf[9] << 9) & 0x07FF);
    sbus_channel[6]  = ((buf[9]>>2  | buf[10] << 6) & 0x07FF);
    sbus_channel[7]  = ((buf[10]>>5 | buf[11] << 3) & 0x07FF);

    sbus_channel[8]  = ((buf[12]    | buf[13] << 8) & 0x07FF);
    sbus_channel[9]  = ((buf[13]>>3 | buf[14] << 5) & 0x07FF);
    sbus_channel[10] = ((buf[14]>>6 | buf[15] << 2 | buf[16] << 10) & 0x07FF);
    sbus_channel[11] = ((buf[16]>>1 | buf[17] << 7) & 0x07FF);
    sbus_channel[12] = ((buf[17]>>4 | buf[18] << 4) & 0x07FF);
    sbus_channel[13] = ((buf[18]>>7 | buf[19] << 1 | buf[20] << 9) & 0x07FF);
    sbus_channel[14] = ((buf[20]>>2 | buf[21] << 6) & 0x07FF);
    sbus_channel[15] = ((buf[21]>>5 | buf[22] << 3) & 0x07FF);

    sbus_ch17       = (buf[23] & 0x01) ? 1 : 0;
    sbus_ch18       = (buf[23] & 0x02) ? 1 : 0;
    sbus_lost_frame = (buf[23] & 0x04) ? 1 : 0;
    sbus_failsafe   = (buf[23] & 0x08) ? 1 : 0;
}

S.Bus 原始值常见范围约为:

最小:172
中位:992
最大:1811

转换为类似 PWM 的 1000us ~ 2000us

uint16_t SBUS_To_PWM(uint16_t raw)
{
    if (raw < 172) raw = 172;
    if (raw > 1811) raw = 1811;

    return (uint16_t)((raw - 172) * 1000 / (1811 - 172) + 1000);
}

10. 标准库接收 S.Bus

10.1 USART 初始化

void USART1_SBUS_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    USART_InitStructure.USART_BaudRate = 100000;
    USART_InitStructure.USART_WordLength = USART_WordLength_9b;
    USART_InitStructure.USART_StopBits = USART_StopBits_2;
    USART_InitStructure.USART_Parity = USART_Parity_Even;
    USART_InitStructure.USART_Mode = USART_Mode_Rx;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_Init(USART1, &USART_InitStructure);

    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);

    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);

    USART_Cmd(USART1, ENABLE);
}

10.2 USART 中断接收

uint8_t sbus_buf[25];
uint8_t sbus_index = 0;

void USART1_IRQHandler(void)
{
    uint8_t data;

    if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
    {
        data = USART_ReceiveData(USART1) & 0xFF;

        if (sbus_index == 0)
        {
            if (data == 0x0F)
            {
                sbus_buf[sbus_index++] = data;
            }
        }
        else
        {
            sbus_buf[sbus_index++] = data;

            if (sbus_index >= 25)
            {
                sbus_index = 0;
                SBUS_Decode(sbus_buf);
            }
        }
    }
}

11. HAL 库接收 S.Bus

11.1 UART 初始化

CubeMX 中 UART 参数设置:

BaudRate   : 100000
WordLength : 9 Bits
Parity     : Even
StopBits   : 2
Mode       : RX

HAL 代码示例:

UART_HandleTypeDef huart1;

void MX_USART1_UART_Init(void)
{
    huart1.Instance = USART1;
    huart1.Init.BaudRate = 100000;
    huart1.Init.WordLength = UART_WORDLENGTH_9B;
    huart1.Init.StopBits = UART_STOPBITS_2;
    huart1.Init.Parity = UART_PARITY_EVEN;
    huart1.Init.Mode = UART_MODE_RX;
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart1.Init.OverSampling = UART_OVERSAMPLING_16;

    HAL_UART_Init(&huart1);
}

如果芯片支持硬件反相,可以增加类似配置:

huart1.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_RXINVERT_INIT;
huart1.AdvancedInit.RxPinLevelInvert = UART_ADVFEATURE_RXINV_ENABLE;

不同 STM32 系列 HAL 字段可能不完全一致,需要以对应芯片 HAL 库为准。

11.2 HAL 中断接收

uint8_t sbus_rx_byte;
uint8_t sbus_buf[25];
uint8_t sbus_index = 0;

void SBUS_Start_Receive(void)
{
    HAL_UART_Receive_IT(&huart1, &sbus_rx_byte, 1);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    uint8_t data;

    if (huart->Instance == USART1)
    {
        data = sbus_rx_byte;

        if (sbus_index == 0)
        {
            if (data == 0x0F)
            {
                sbus_buf[sbus_index++] = data;
            }
        }
        else
        {
            sbus_buf[sbus_index++] = data;

            if (sbus_index >= 25)
            {
                sbus_index = 0;
                SBUS_Decode(sbus_buf);
            }
        }

        HAL_UART_Receive_IT(&huart1, &sbus_rx_byte, 1);
    }
}

main 中启动接收:

int main(void)
{
    HAL_Init();
    SystemClock_Config();

    MX_GPIO_Init();
    MX_USART1_UART_Init();

    SBUS_Start_Receive();

    while (1)
    {
        // 使用 sbus_channel[0] ~ sbus_channel[15]
    }
}

12. 三种协议对比

项目 PWM PPM S.Bus
引脚数量 1 1
解码难度 简单 中等 中等
抗干扰 一般 一般 较好
通道数量 取决于接线 常见 8 通道 常见 16 通道
STM32 外设 定时器输入捕获 定时器输入捕获 USART/UART
是否需要反相 不需要 不需要 通常需要

13. 常见问题

13.1 PWM 数值不稳定

优先检查:

  • 定时器是否配置为 1MHz。
  • 输入捕获滤波是否开启。
  • 接收机和 STM32 是否共地。
  • 信号线是否过长。
  • 是否存在定时器溢出处理问题。

13.2 PPM 通道错位

通常是同步时间阈值不合适。

可以先使用:

#define PPM_SYNC_TIME 3000

如果仍然错位,可以尝试:

#define PPM_SYNC_TIME 4000

13.3 S.Bus 接收不到数据

优先检查:

  • 波特率是否为 100000
  • 是否为偶校验。
  • 是否为 2 个停止位。
  • 是否处理了反相信号。
  • 接收机输出是否真的是 S.Bus,而不是 i-Bus、PPM 或 PWM。
  • STM32 的 UART 是否支持该波特率和对应校验配置。

14. 总结

STM32 解码遥控器信号时,可以按协议选择外设:

PWM   -> 定时器输入捕获,测量高电平宽度
PPM   -> 定时器输入捕获,测量相邻上升沿间隔
S.Bus -> 串口接收 25 字节数据帧,再解析 16 个 11bit 通道

如果是入门学习,建议先从 PWM 开始,再学习 PPM,最后学习 S.Bus。

如果是实际飞控、车控或机器人项目,优先推荐 S.Bus,因为接线少、通道多、数据格式统一。


15. 参考资料

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐