在学习这部分之间,建议大家先看之前这篇博客,里面包含对PWM一些重要概念的基本介绍。
1. 基本原理
1.1 PWM是什么
这一部分可以看我之前的博客,已经对PWM有了基本的介绍。
1.2 什么叫捕获PWM波,这跟解析接收机信号有什么关系
1.2.1 单片机、接收机、遥控器之间的关系
对于我使用的Microzone的6C基础遥控器和Radiolink R7EH接收机来说,它们二者与单片机之间有如下的联系:
关于PPM波,可以简单认为其是一个周期内集成了多个PWM波。
图源遥控器说明文档
可以这样简单认为,遥控器发送的是一束PPM波信号,里面集成了多个通道的PWM波信号,而接收机接收到遥控器发的PPM后,会将该PPM拆分成多个通道单独的类似于PWM波的信号,我们的单片机只需要接收其中1-4通道的信号并进行捕获解析就可以了。
1.2.2 PWM捕获
简单来说,PWM捕获,就是指我们接收到一束PWM波后,分析出其一个周期内的占空比。所以,问题的关键在于如何计算出占空比。
在之前的文章中,我们介绍过与PWM有关的两个寄存器值,CCR和ARR,其中ARR是重装载值,代表一个周期内计数器能达到的最大值;CCR则可以理解为计数器的值,它与ARR的比值就是占空比。
因此,如果我们能做到对于接收到的一个周期的PWM波,算出其CCR和ARR,就自然而然地可以得出其占空比。
1.2.3 如何算CCR
同PWM输出类似,PWM捕获,同样可以依靠定时器实现,并且还要使用定时器中断功能。下面我将阐述中断的作用。
对于这样一束波,我们可以看到脉宽时间与周期的比值,实际就是占空比。而进入脉宽时间时,必定会有一个从低电平到高点平跳变的过程,其有个专门的名词叫做"上升沿",同理,当脉宽时间结束时,也会相应的有个"下降沿"。STM32的定时器,恰恰为我们提供了三种中断触发方式:上升沿触发、下降沿触发、上升沿和下降沿都触发。
试想,如果我们初始设置为上升沿触发,那么对于捕获到的PWM波,当检测到其刚进入脉宽时间时,会进入中断,这时我们在中断内获取此时的CCR值,再将中断触发方式设置为下降沿触发,那么当检测到脉宽时间结束时,又会进入中断,此时我们再获得此时的CCR值,与之间的相减,不就得到了脉宽时间内CCR的变化量了吗,这个值比上ARR,就是占空比。
2. STM32CUBEMX配置
配置可参考如下视频:
定时器-输入捕获(频率、占空比测量)_哔哩哔哩_bilibili
下图是我的配置:
3. 实现代码
3.1 源文件
#include "Receiver.h"
#include "tim.h"
#include "MySerial.h"
// 数据存储
static uint32_t risingEdgeTime[CHANNEL_COUNT] = {0}; // 存储每个通道的上升沿捕获时间
static uint32_t fallingEdgeTime[CHANNEL_COUNT] = {0}; // 存储每个通道的下降沿捕获时间
static uint8_t isRisingEdge[CHANNEL_COUNT] = {1}; // 每个通道的标志位:1表示检测上升沿,0表示检测下降沿
static uint32_t pwmWidth[CHANNEL_COUNT] = {0}; // 存储每个通道的脉宽(单位:计数值)
static float pwmMapVal[CHANNEL_COUNT] = {0}; // 存储每个通道映射到控制值的结果(0.0 到 1.0)
// 函数声明
static uint32_t CalculatePWMWidth(uint32_t risingEdge, uint32_t fallingEdge, uint32_t period);
static void MapPWMWidthToValue(uint32_t width, uint32_t channelIndex);
static uint32_t GetChannelIndex(TIM_HandleTypeDef *htim);
/**
* @brief 计算脉宽
* @param risingEdge 上升沿捕获的计数值
* @param fallingEdge 下降沿捕获的计数值
* @param period 定时器的自动重装载值(ARR)
* @return 脉宽值(单位:计数值)
*
* 该函数根据上升沿和下降沿时间点计算脉宽(高电平时间)。
* 如果发生计数器溢出,考虑溢出的补偿周期。
*/
static uint32_t CalculatePWMWidth(uint32_t risingEdge, uint32_t fallingEdge, uint32_t period) {
if (fallingEdge >= risingEdge) {
return fallingEdge - risingEdge; // 没有溢出,直接计算差值
} else {
return (period - risingEdge) + fallingEdge; // 溢出时补偿
}
}
/**
* @brief 映射脉宽到控制值
* @param width 脉宽值(单位:计数值)
* @param channelIndex 通道索引
*
* 根据不同通道的范围(MIN_MOTORVAL、MAX_MOTORVAL)将脉宽值映射到 0.0 到 1.0 的范围。
* 特定通道的映射范围通过 `channelIndex` 确定。
*/
static void MapPWMWidthToValue(uint32_t width, uint32_t channelIndex) {
float MIN_MOTORVAL, MAX_MOTORVAL, SUB_MOTORVAL;
// 根据通道索引选择不同的映射范围
if (channelIndex == CHANNEL3_INDEX) {
MIN_MOTORVAL = MIN_MOTORVAL3;
MAX_MOTORVAL = MAX_MOTORVAL3;
SUB_MOTORVAL = SUB_MOTORVAL3;
} else if (channelIndex == CHANNEL2_INDEX) {
MIN_MOTORVAL = MIN_MOTORVAL2;
MAX_MOTORVAL = MAX_MOTORVAL2;
SUB_MOTORVAL = SUB_MOTORVAL2;
} else {
MIN_MOTORVAL = MIN_MOTORVAL14;
MAX_MOTORVAL = MAX_MOTORVAL14;
SUB_MOTORVAL = SUB_MOTORVAL14;
}
// 限制脉宽在有效范围内
if (width < MIN_MOTORVAL) width = MIN_MOTORVAL;
if (width > MAX_MOTORVAL) width = MAX_MOTORVAL;
// 映射值计算
pwmMapVal[channelIndex] = ((float)(width - MIN_MOTORVAL)) / SUB_MOTORVAL;
}
/**
* @brief 获取当前通道索引
* @param htim 定时器句柄
* @return 通道索引(0 ~ CHANNEL_COUNT-1),或 INVALID_CHANNEL 表示无效通道
*
* 根据定时器通道,返回对应的通道索引。该索引用于索引捕获数据的数组。
*/
static uint32_t GetChannelIndex(TIM_HandleTypeDef *htim) {
switch (htim->Channel) {
case HAL_TIM_ACTIVE_CHANNEL_1: return CHANNEL1_INDEX; // 通道1
case HAL_TIM_ACTIVE_CHANNEL_2: return CHANNEL2_INDEX; // 通道2
case HAL_TIM_ACTIVE_CHANNEL_3: return CHANNEL3_INDEX; // 通道3
case HAL_TIM_ACTIVE_CHANNEL_4: return CHANNEL4_INDEX; // 通道4
default: return INVALID_CHANNEL; // 无效通道
}
}
/**
* @brief 定时器输入捕获中断回调函数
* @param htim 定时器句柄
*
* 该函数在定时器捕获事件发生时触发。
* 它根据当前通道索引读取捕获值,计算脉宽,并更新映射值。
* 上升沿和下降沿捕获交替进行。
*/
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
if (htim->Instance == TIM4) { // 检查是否为 TIM4
uint32_t channelIndex = GetChannelIndex(htim); // 获取通道索引
if (channelIndex == INVALID_CHANNEL) return; // 无效通道直接返回
// 读取捕获值
uint32_t capturedValue = HAL_TIM_ReadCapturedValue(htim, channelIndex * 4); // 修正参数传递错误
if (isRisingEdge[channelIndex]) { // 上升沿捕获
risingEdgeTime[channelIndex] = capturedValue;
__HAL_TIM_SET_CAPTUREPOLARITY(htim, channelIndex * 4, TIM_INPUTCHANNELPOLARITY_FALLING); // 切换到下降沿
} else { // 下降沿捕获
fallingEdgeTime[channelIndex] = capturedValue;
pwmWidth[channelIndex] = CalculatePWMWidth(risingEdgeTime[channelIndex], fallingEdgeTime[channelIndex], TIM4->ARR); // 计算脉宽
MapPWMWidthToValue(pwmWidth[channelIndex], channelIndex); // 映射脉宽到控制值
__HAL_TIM_SET_CAPTUREPOLARITY(htim, channelIndex * 4, TIM_INPUTCHANNELPOLARITY_RISING); // 切换回上升沿
}
isRisingEdge[channelIndex] = !isRisingEdge[channelIndex]; // 切换边沿标志位
}
}
/**
* @brief 接收机初始化
*
* 启动定时器捕获中断,用于捕获 4 个通道的信号。
*/
void Receiver_Init(void) {
HAL_TIM_IC_Start_IT(&htim4, TIM_CHANNEL_1); // 启动通道1中断
HAL_TIM_IC_Start_IT(&htim4, TIM_CHANNEL_2); // 启动通道2中断
HAL_TIM_IC_Start_IT(&htim4, TIM_CHANNEL_3); // 启动通道3中断
HAL_TIM_IC_Start_IT(&htim4, TIM_CHANNEL_4); // 启动通道4中断
}
/**
* @brief 映射值接口
*
* @return 返回对应通道的映射值
*/
float Receiver_GetMappedValue(uint32_t channelIndex)
{
return pwmMapVal[channelIndex];
}
3.2 头文件
#ifndef __RECEIVER_H
#define __RECEIVER_H
#include "main.h"
// 宏定义
#define CHANNEL_COUNT 4 // 通道数量
#define CHANNEL1_INDEX 0 // 通道 1 索引
#define CHANNEL2_INDEX 1 // 通道 2 索引
#define CHANNEL3_INDEX 2 // 通道 3 索引
#define CHANNEL4_INDEX 3 // 通道 4 索引
#define INVALID_CHANNEL 255 // 无效通道标志
#define MAX_MOTORVAL14 1750 // 14通道最大脉宽值
#define MIN_MOTORVAL14 1250 // 14通道最小脉宽值
#define MAX_MOTORVAL3 2000 // 3最大脉宽值
#define MIN_MOTORVAL3 1000 // 3最小脉宽值
#define MAX_MOTORVAL2 1750 // 2最大脉宽值
#define MIN_MOTORVAL2 1500 // 2最小脉宽值
#define SUB_MOTORVAL14 500 // 14通道脉宽范围的
#define SUB_MOTORVAL3 1000 // 3通道脉宽范围
#define SUB_MOTORVAL2 250 // 2通道脉宽范围
void Receiver_Init(void); //初始化函数
float Receiver_GetMappedValue(uint32_t channelIndex); //返回映射值,方便外层调用
#endif // __RECEIVER_H
一些解释
在我的代码中,有一个函数叫做MapPWMWidthToValue,其作用是将脉宽值映射到0-1的范围。
为了实现这个函数,我手动记录了各个通道对应的遥控器拨杆拨到最小、中间、最大时的脉宽值,通道与拨杆的对应关系见下图:
因此才有了以下宏定义:
#define MAX_MOTORVAL14 1750 // 14通道最大脉宽值
#define MIN_MOTORVAL14 1250 // 14通道最小脉宽值
#define MAX_MOTORVAL3 2000 // 3最大脉宽值
#define MIN_MOTORVAL3 1000 // 3最小脉宽值
#define MAX_MOTORVAL2 1750 // 2最大脉宽值
#define MIN_MOTORVAL2 1500 // 2最小脉宽值