STM32 解码遥控器 PWM、PPM、S.Bus 协议:标准库与 HAL 库实现整理
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. 参考资料
- Espruino:Futaba S.Bus System,说明了 S.Bus 的 100000 baud、偶校验、2 停止位、反相和 25 字节结构。
https://www.espruino.com/SBus - bolderflight/sbus:说明了 S.Bus 100000 8E2、25 字节包、通道和 flags 位定义。
https://github.com/bolderflight/sbus - sigrok sbus_futaba decoder:说明了 S.Bus 典型 UART 参数、25 字节帧、16 个 11bit 通道和 flags。
https://www.sigrok.org/wiki/Protocol_decoder%3ASbus_futaba
更多推荐
所有评论(0)