【项目复盘】【四轴飞行器设计】驱动开发部分

由于在参加面试时总需要花时间一点一点的回忆自己的项目内容,故我打算直接写一系列的项目复盘博客,方便每次面试前的回忆。内容仅作分享交流,如有谬误欢迎指正。

本项目系列的文章目录如下:

【项目复盘】【四轴飞行器设计】驱动开发部分-CSDN博客

【项目复盘】【四轴飞行器设计】姿态解算部分-CSDN博客

【项目复盘】【四轴飞行器设计】控制部分-CSDN博客


本篇文章主要讲解该项目中的嵌入式软件驱动开发部分,我将讲解该项目用到了哪些模块、如何开发以及一些需要注意的八股内容考察点。

1. 模块组成

该四轴飞行器的模块组成如下:

  1. 主控:STM32F401RBT6

  2. 姿态解算:GY86

  3. 电机驱动与控制:遥控器、接收机、无刷电机

2. 驱动开发方法

这里涉及到的驱动有:

  1. 串口通信驱动

  2. 软件IIC通信时序驱动

  3. 基于IIC的GY86驱动

  4. 遥控器、接收机、无刷电机的驱动

2.1. 串口通信驱动

串口的驱动开发比较简单,我们使用的是HAL库,所以直接在STM32CUBEMX中配置即可,最终选择的波特率为9600。

此外,串口相关的驱动书写代码可以参考蓝桥杯中的串口代码:【蓝桥杯嵌入式】【模块】八、UART相关配置及代码模板-CSDN博客

核心注意点如下:

  1. 重写fputc函数实现串口输出重定向

  2. 基于定时器实现串口不定长接收

2.2. 软件IIC通信时序驱动

这里我将重点讲解IIC的时序含义及理解。

2.2.1. 整体代码
复制代码
#include "i2c_hal.h"

#define DELAY_TIME	20

//
void SDA_Input_Mode()
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};

    GPIO_InitStructure.Pin = GPIO_PIN_7;
    GPIO_InitStructure.Mode = GPIO_MODE_INPUT;
    GPIO_InitStructure.Pull = GPIO_PULLUP;
    GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
}

//
void SDA_Output_Mode()
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};

    GPIO_InitStructure.Pin = GPIO_PIN_7;
    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_InitStructure.Pull = GPIO_NOPULL;
    GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
}

//
void SDA_Output( uint16_t val )
{
    if ( val )
    {
        GPIOB->BSRR |= GPIO_PIN_7;
    }
    else
    {
        GPIOB->BRR |= GPIO_PIN_7;
    }
}

//
void SCL_Output( uint16_t val )
{
    if ( val )
    {
        GPIOB->BSRR |= GPIO_PIN_6;
    }
    else
    {
        GPIOB->BRR |= GPIO_PIN_6;
    }
}

//
uint8_t SDA_Input(void)
{
	if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) == GPIO_PIN_SET){
		return 1;
	}else{
		return 0;
	}
}

//
static void delay1(unsigned int n)
{
    uint32_t i;
    for ( i = 0; i < n; ++i);
}

//
void I2CStart(void)
{
    SDA_Output(1);
    delay1(DELAY_TIME);
    SCL_Output(1);
    delay1(DELAY_TIME);
    SDA_Output(0);
    delay1(DELAY_TIME);
    SCL_Output(0);
    delay1(DELAY_TIME);
}

//
void I2CStop(void)
{
    SCL_Output(0);
    delay1(DELAY_TIME);
    SDA_Output(0);
    delay1(DELAY_TIME);
    SCL_Output(1);
    delay1(DELAY_TIME);
    SDA_Output(1);
    delay1(DELAY_TIME);

}

//
unsigned char I2CWaitAck(void)
{
    unsigned short cErrTime = 5;
    SDA_Input_Mode();
    delay1(DELAY_TIME);
    SCL_Output(1);
    delay1(DELAY_TIME);
    while(SDA_Input())
    {
        cErrTime--;
        delay1(DELAY_TIME);
        if (0 == cErrTime)
        {
            SDA_Output_Mode();
            I2CStop();
            return ERROR;
        }
    }
    SCL_Output(0);
    SDA_Output_Mode();
    delay1(DELAY_TIME);
    return SUCCESS;
}

//
void I2CSendAck(void)
{
    SDA_Output(0);
    delay1(DELAY_TIME);
    delay1(DELAY_TIME);
    SCL_Output(1);
    delay1(DELAY_TIME);
    SCL_Output(0);
    delay1(DELAY_TIME);

}

//
void I2CSendNotAck(void)
{
    SDA_Output(1);
    delay1(DELAY_TIME);
    delay1(DELAY_TIME);
    SCL_Output(1);
    delay1(DELAY_TIME);
    SCL_Output(0);
    delay1(DELAY_TIME);

}

//
void I2CSendByte(unsigned char cSendByte)
{
    unsigned char  i = 8;
    while (i--)
    {
        SCL_Output(0);
        delay1(DELAY_TIME);
        SDA_Output(cSendByte & 0x80);
        delay1(DELAY_TIME);
        cSendByte += cSendByte;
        delay1(DELAY_TIME);
        SCL_Output(1);
        delay1(DELAY_TIME);
    }
    SCL_Output(0);
    delay1(DELAY_TIME);
}

//
unsigned char I2CReceiveByte(void)
{
    unsigned char i = 8;
    unsigned char cR_Byte = 0;
    SDA_Input_Mode();
    while (i--)
    {
        cR_Byte += cR_Byte;
        SCL_Output(0);
        delay1(DELAY_TIME);
        delay1(DELAY_TIME);
        SCL_Output(1);
        delay1(DELAY_TIME);
        cR_Byte |=  SDA_Input();
    }
    SCL_Output(0);
    delay1(DELAY_TIME);
    SDA_Output_Mode();
    return cR_Byte;
}

//
void I2CInit(void)
{
    GPIO_InitTypeDef GPIO_InitStructure = {0};

    GPIO_InitStructure.Pin = GPIO_PIN_7 | GPIO_PIN_6;
    GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStructure.Pull = GPIO_PULLUP;
    GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
}
2.2.2.通信时序讲解

参考:[10-1] I2C通信协议_哔哩哔哩_bilibili

在开始讲解之前,先讲IIC的三个基本性质,从这三个基本性质入手可以更方便地理解时序:

  1. IIC在不工作时,SCL和SDA由于上拉电阻地存在始终处于高电平状态。在工作时,SCL始终处于低电平。

  2. 在进行通信时,如果有一方释放了SDA,就说明另一方获得了SDA地控制权,成为主机。

  3. 在SDA只有在SCL处于低电平时才可以进行高低切换用于发送数据,否则将会造成通信的开始或结束。

2.2.2.1. IIC启动与停止

如图,可以参考之前的性质三,如果在SCL高电平时进行SDA的状态切换,便会造成通信的开始活或结束。具体来说,是SCL高电平时,若拉低SDA,则IIC通信开始;当SCL高电平时,若拉高SDA,则通信结束。

2.2.2.2. IIC发送应答/非应答/接收应答

这里的应答就是一位数据,所以发送/接收应答实际也就是发送/接收一位数据。

在IIC发送数据时,就是在SCL低电平(工作状态时),改变SDA的电平状态用于代表数据,比如,SDA为低电平,代表这一位数据为0,之后将SCL拉高,从机将会在拉高的期间读取SDA上的数据,由此实现数据的发送和接收。

对于发送应答而言,实际就是主机向从机发送数据'0',因此,将SDA置低后,拉高SCL,让从机读取,之后再将SCL拉低,以便维持工作状态。

对于发送非应答而言,实际就是主机向从机发送数据'1',其余的同上。

对于等待应答,其原理便是在一个时间区间内作为从机读取SDA上的数据,如果收到了主机的应答'0',即为等待应答成功,否则为失败。因此在等待应答时,要先将SDA拉高,释放SDA,以便从机能操控SDA线发送数据,接着拉低SCL,使得从机在SCL拉低这段时间里操作SDA的电平,接着拉高SCL,主机在这段时间内读取SDA上的数据,如果有应答数据,则应答成功,否则失败。

2.2.2.3. IIC发送/接收一个字节

一个字节为8位,所以收发一个字节实际就是将收发一位的操作循环执行八次。

对于发送一个字节,我们在一个八次的循环内重复类似于"发送应答"的操作,即先将SCL拉低进入工作状态,接收高位现行,发送待发数据的最高位,接着拉高SCL让从机读取这一位,接着循环进行该操作。

对于接收一个字节,我们在一个八次的循环内重复类似于"接收应答"的操作,首先拉高SDA将其释放,以便从机操控,接着在八次的循环里先拉低SCL进入工作状态,从机也在这段时间内操作SDA进行数据的装填,然后拉高SCL进行数据的读取,获得一位数据,重复该操作八次便可得到一个字节的数据。

2.2.2.4. 基于IIC读/写寄存器

在这里,我以蓝桥杯中的eeprom读写为例来说明如何基于IIC来读写外设的寄存器,后续GY86的读写方法跟这里类似。

复制代码
void eeprom_write(uint8_t addr, uint8_t data)
{
	I2CStart();
	I2CSendByte(0xa0);
	I2CWaitAck();
	I2CSendByte(addr);
	I2CWaitAck();
	I2CSendByte(data);
	I2CWaitAck();
	I2CStop();
}

uint8_t eeprom_read(uint8_t addr)
{
	I2CStart();
	I2CSendByte(0xa0);
	I2CWaitAck();
	I2CSendByte(addr);
	I2CWaitAck();
	I2CStop();
	
	I2CStart();
	I2CSendByte(0xa1);
	I2CWaitAck();
	
	uint8_t ret = I2CReceiveByte();
	I2CSendNotAck();
	I2CStop();
	
	return ret;
}

首先对于写寄存器,步骤如下:开始IIC通信-发送要写的外设IIC写地址-发送要写的寄存器地址-发送要写的数据。需要注意的是,外设的IIC地址和要写的寄存器地址是两个不同的东西,IIC地址用于区分在一条总线上的不同外设,而寄存器地址则是在一个外设内的不同地址。

对于读寄存器,会复杂一些,步骤如下:开始IIC通信-发送要读的外设IIC写地址-发送要读的寄存器地址-结束通信-开始通信-发送要读的外设IIC读地址-读取数据-结束通信。这里发现我们需要先进行一步写的操作,告诉外设我们要操作的寄存器是哪一个,接着重新开始IIC时序,在这个时序中直接读取,从机由于在第一次通信中知道了主机想读的寄存器是哪一个,因此便可以进行数据的发送。

2.3. 基于IIC的GY86驱动

由于GY86是一个十轴传感器,其内包含了三轴磁力计、 三轴陀螺仪 和气压高度计,而在该四轴项目中我们主要操作的还是其内的陀螺仪,即MPU6050,故此只讲解MPU6050相关的驱动。

2.3.1. 整体代码
复制代码
/**
 * @brief 从MPU6050批量读取数据
 * @param reg 起始寄存器地址
 * @param buf 存储读取数据的缓冲区
 * @param len 读取数据的长度
 * @retval 0: 成功, 1: 失败
 */
uint8_t MPU6050_ReadData(uint8_t reg, uint8_t *buf, uint16_t len)
{
    I2C_Start();
    I2C_SendByte((MPU6050_I2C_ADDR << 1) | 0); // 发送设备地址+写指令
    if (I2C_WaitAck() != 0)
    {
        I2C_Stop();
        return 1;
    }

    I2C_SendByte(reg); // 发送起始寄存器地址
    if (I2C_WaitAck() != 0)
    {
        I2C_Stop();
        return 1;
    }

    I2C_Start();
    I2C_SendByte((MPU6050_I2C_ADDR << 1) | 1); // 发送设备地址+读指令
    if (I2C_WaitAck() != 0)
    {
        I2C_Stop();
        return 1;
    }

    for (uint16_t i = 0; i < len; i++)
    {
        buf[i] = I2C_ReceiveByte();
        if (i == (len - 1))
            I2C_SendNotAck(); // 最后一个字节发送NACK
        else
            I2C_SendAck();
    }
    I2C_Stop();
    return 0;
}

整体的步骤实际与2.2.2.4中的内容基本一致,只不过该函数设计为批量读取,故进行了一个长度为len的循环,可以一次性读取多个字节的数据。

2.4. 遥控器、接收机、无刷电机的驱动

由于我们采用了电调,所以可以将无刷电机的驱动转换为对PWM输出的控制,而遥控器、接收机部分可以认为是PWM接收的控制,下面将一一讲解。

2.4.1. 遥控器与接收机
2.4.1.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 curMapVal[CHANNEL_COUNT] = {0.5,0,0.5,0.5};          //存储当前通道的map值
static float pwmMapVal[CHANNEL_COUNT] = {0};          // 存储每个通道映射到控制值的结果(0.0 到 1.0)

//pwmMapVal规定
/*
pwmMapVal[0]:通道一  右手左右   控制航向
pwmMapVal[1]:通道二  右手上下   控制升降
pwmMapVal[2]:通道三  左手上下   控制俯仰
pwmMapVal[3]:通道四  左手左右   控制横滚
1.升降会控制四个电机,即通道2脉宽增大将会导致四个通道的PWM输出占空比增大
2.横滚会控制分别控制通道13和24,向右拨滑杆,飞机沿x轴顺时针转,24通道占空比增加,13通道占空比减小
3.俯仰分别控制通道12和34,向上拨滑杆,飞机沿y轴顺时针旋转,12占空比增加,34减少
4.偏航分别控制通道14和23,向右拨滑杆,飞机沿z轴顺时针转,14占空比增加,23减少
*/
// 函数声明
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);
static void MapPWMToAngle(uint32_t channelIndex, float pwmVal);
/**
 * @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;

    // 根据通道索引选择不同的映射范围
    switch (channelIndex) {
        case CHANNEL3_INDEX:
            MIN_MOTORVAL = MIN_MOTORVAL3;
            MAX_MOTORVAL = MAX_MOTORVAL3;
            SUB_MOTORVAL = SUB_MOTORVAL3;
            break;
        case CHANNEL2_INDEX:
            MIN_MOTORVAL = MIN_MOTORVAL2;
            MAX_MOTORVAL = MAX_MOTORVAL2;
            SUB_MOTORVAL = SUB_MOTORVAL2;
            break;
        default:  // 偏航控制:CHANNEL14_INDEX,俯仰控制:CHANNEL12_INDEX,横滚控制:CHANNEL24_INDEX
            MIN_MOTORVAL = MIN_MOTORVAL14;
            MAX_MOTORVAL = MAX_MOTORVAL14;
            SUB_MOTORVAL = SUB_MOTORVAL14;
            break;
    }

    // 限制脉宽在有效范围内
    if (width < MIN_MOTORVAL) {
        width = MIN_MOTORVAL;
    }
    if (width > MAX_MOTORVAL) {
        width = MAX_MOTORVAL;
    }

    // 映射值计算
    float mappedValue = ((float)(width - MIN_MOTORVAL)) / SUB_MOTORVAL;
//    pwmMapVal[channelIndex] = mappedValue;
		
		float deta = 0;
		float tmp ;
		
		if(channelIndex == CHANNEL2_INDEX) {  // 升降控制 
			tmp	= curMapVal[CHANNEL2_INDEX]; //记录上次中断时通道2的值
				curMapVal[CHANNEL2_INDEX] = mappedValue;
				deta = mappedValue - tmp;
				
			//保证了在右手上下不变的情况下,通道2不参与转速调整
				pwmMapVal[CHANNEL1_INDEX] += deta; // 电机1增加
				pwmMapVal[CHANNEL2_INDEX] += deta; // 电机2增加
				pwmMapVal[CHANNEL3_INDEX] += deta; // 电机3增加
				pwmMapVal[CHANNEL4_INDEX] += deta; // 电机4增加
		}else if(channelIndex == CHANNEL3_INDEX || channelIndex == CHANNEL4_INDEX){
//			pwmMapVal[channelIndex] = mappedValue;
			MapPWMToAngle(channelIndex, mappedValue);//0-1映射为-30-30度,表示期望的角度倾斜
		}

//				case CHANNEL4_INDEX:  // 横滚控制
//					  tmp	= curMapVal[channelIndex];
//						curMapVal[channelIndex] = mappedValue;
//						deta = mappedValue - tmp;
//						pwmMapVal[CHANNEL1_INDEX] -= deta; // 电机13减小
//						pwmMapVal[CHANNEL3_INDEX] -= deta; // 电机13减小
//						pwmMapVal[CHANNEL2_INDEX] += deta;      // 电机24增加
//						pwmMapVal[CHANNEL4_INDEX] += deta;      // 电机24增加
//						break;

//				case CHANNEL3_INDEX:  // 俯仰控制
//					  tmp	= curMapVal[channelIndex];
//						curMapVal[channelIndex] = mappedValue;
//						deta = mappedValue - tmp;
//						pwmMapVal[CHANNEL1_INDEX] += deta; // 电机12增加
//						pwmMapVal[CHANNEL2_INDEX] += deta; // 电机12增加
//						pwmMapVal[CHANNEL3_INDEX] -= deta; // 电机34减小
//						pwmMapVal[CHANNEL4_INDEX] -= deta; // 电机34减小
//						break;

//				case CHANNEL1_INDEX:  // 偏航控制
//					  tmp	= curMapVal[channelIndex];
//						curMapVal[channelIndex] = mappedValue;
//						deta = mappedValue - tmp;
//						pwmMapVal[CHANNEL1_INDEX] += deta; // 电机14增加
//						pwmMapVal[CHANNEL4_INDEX] += deta; // 电机14增加
//						pwmMapVal[CHANNEL2_INDEX] -= deta; // 电机23减小
//						pwmMapVal[CHANNEL3_INDEX] -= deta; // 电机23减小
//						break;
   
}




/**
 * @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];
}

// 存储映射后的角度值
static float angleMapVal[CHANNEL_COUNT] = {0.0, 0.0, 0.0, 0.0}; // 初始角度均为0

void Receiver_SetMappedValue(uint32_t channelIndex, float deta) {
	pwmMapVal[channelIndex] += deta;
}
/**
 * @brief 映射通道控制值到角度
 * 
 * @param channelIndex 通道索引
 * @param pwmVal 映射到0-1范围的控制值
 * 
 * 对于通道3(俯仰控制)和通道4(横滚控制),将它们的0-1映射值转换为-30°到30°的角度值。
 */
static void MapPWMToAngle(uint32_t channelIndex, float pwmVal) {
    // 校验通道
    if (channelIndex == CHANNEL3_INDEX || channelIndex == CHANNEL4_INDEX) {
        // 映射公式: 角度 = (控制值 - 0.5) * 60°  
        // 例如,控制值为0.0时,角度为-30°;控制值为1.0时,角度为30°
        angleMapVal[channelIndex] = (pwmVal - 0.5) * 60.0f;
    }
}

/**
 * @brief 获取映射后的角度值
 * 
 * @param channelIndex 通道索引
 * @return 映射后的角度值(-30°到30°)
 */
float Receiver_GetMappedAngle(uint32_t channelIndex) {
    if (channelIndex == CHANNEL3_INDEX || channelIndex == CHANNEL4_INDEX) {
        return angleMapVal[channelIndex];
    }
    return 0.0f; // 如果不是有效通道,返回0°
}
2.4.1.2. 原理讲解

我们可以大致这样理解:从遥控器发出的PPM波经过接收机后,转化为了PWM波进入MCU,所以我们需要做的,就是在MCU内进行PWM的接收与结算,计算出其频率和占空比(实际上计算占空比就够了),由此获得遥控器滑杆的操作内容,基于该内容发送适当频率、占空比的PWM给电调,用于驱动无刷电机转动。

关于PWM接收与解算的方法,可以看这篇文章:【蓝桥杯嵌入式】【模块】六、PWM相关配置及代码模板-CSDN博客

在当前的办法中,我们没有直接采用占空比计算,而是使用了一个相对笨拙的计算脉宽的方法,通过计算两次中断间的计数值差值,映射为0-1的控制值,再用于后续的PWM输出控制。

在开发过程中,我们也通过人为记录的方式,记录下了各个通道的脉宽范围:

出现这种脉宽值的原因是当时在开发时没有摸索清楚遥控器的使用方法,造成了拨杆不同方向上的脉宽范围不同。

这个方法后续会优化为直接使用占空比。

2.4.2. 无刷电机的驱动

之前提过,由于有了电调的存在,我们无需再关心无刷电机复杂的驱动方法,而是直接使用PWM输出到电调,让电调来进行相应的信号转换用于驱动无刷电机。

2.4.2.1. 整体代码
复制代码
#include "Motor.h"
#include "tim.h"
#include "MySerial.h"

/**
 * @brief 初始化电机 PWM 输出
 * 
 * 此函数开启四个通道的 PWM 输出,用于驱动舵机或电机。
 * 在调用此函数前,需确保定时器(TIM3)已通过 HAL 库初始化。
 */
void Motor_Init(void) 
{
    // 开启 TIM3 的 4 个 PWM 通道
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3);
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);
		Motor_SetPulse(1,0.05);
		HAL_Delay(1000);
		Motor_SetPulse(2,0.05);
		HAL_Delay(1000);
		Motor_SetPulse(3,0.05);
		HAL_Delay(1000);
		Motor_SetPulse(4,0.05);
		HAL_Delay(1000);
}

/**
 * @brief 设置指定通道的 PWM 占空比
 * 
 * @param channel PWM 通道号(1 ~ 4)
 * @param Pulse 占空比百分比(0.0 ~ 1.0),表示 PWM 高电平所占比例。
 *              - 0.0:完全低电平
 *              - 1.0:完全高电平
 *              - 其他值:高低电平按比例分配
 * 
 * 该函数会将占空比转换为定时器比较寄存器的值。
 */
void Motor_SetPulse(int channel, float Pulse)
{
    // 确保占空比在有效范围内(0.0 ~ 1.0)
    if (Pulse < 0.0f) Pulse = 0.0f;
    if (Pulse > 1.0f) Pulse = 1.0f;

    // 根据占空比计算计数值
    int duty = (int)(ARR_VAL * Pulse);  // ARR_VAL 是自动重装载值

    // 根据通道号设置对应的比较值
    switch(channel) {
        case 1: 
            __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_1, duty);
            break;
        case 2: 
            __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_2, duty);
            break;
        case 3: 
            __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_3, duty);
            break;
        case 4: 
            __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_4, duty);
            break;
        default:
            // 无效通道号,忽略设置
            break;
    }
}
2.4.2.1. 原理讲解

PWM输出的方法是比较简单的,依旧可以参考这篇文章:【蓝桥杯嵌入式】【模块】六、PWM相关配置及代码模板-CSDN博客

在这里需要讲解一下PWM输出在该项目中的注意点:

  1. 电调的PWM驱动频率是有要求的,我们买的这个电调要求的额定PWM频率为50HZ。

  2. 在开发过程中,我们发现PWM输出的占空比范围与电机转速的范围映射为0.05-0.15分别对应电机的最小和最大转速。

  3. 电调额定频率、电机转速与占空比的映射数据都需要查看相关器件的说明书才能得知。

3. 可能考的八股

3.1. IIC相关

  1. 通信时序

  2. IIC的应用

传感器数据、存储器eeprom、显示OLED/LCD

  1. IIC调试工具

我只接触过逻辑分析仪

3.2. PWM相关

  1. PWM捕获频率和占空比的原理

对于频率测量,可以捕获两次上升沿触发的计数值差值,再用时钟频率/预分频系数/捕获值。

对于占空比测量,分别设置中断触发方式为上升沿和下降沿,计算两次中断之间间隔的计数值,即可获得高电平的持续时间,这个间隔再除以一个周期的计数值,便可得到占空比。

相关推荐
逼子格11 分钟前
【Protues仿真】基于AT89C52单片机的舵机和直流电机控制
单片机·嵌入式硬件·硬件工程·硬件工程师·电机驱动·l298n·直流电机控制
GodKK老神灭17 分钟前
STM32 AFIO模块
stm32·单片机·嵌入式硬件
狂奔的sherry3 小时前
一会儿能ping通一会ping不通解决方案
运维·网络·单片机·嵌入式硬件
qq_401700413 小时前
单片机驱动继电器接口
单片机·嵌入式硬件
anghost1501 天前
基于 STM32 的多传感器健康监测系统设计
stm32·单片机·嵌入式硬件
玉~你还好吗1 天前
【嵌入式电机控制#34】FOC:意法电控驱动层源码解析——HALL传感器中断(不在两大中断内,但重要)
单片机·嵌入式系统·电机控制
STC_USB_CAN_80511 天前
所有普通I/O口都支持中断的51单片机@Ai8051U, AiCube 图形化配置
单片机·嵌入式硬件·51单片机
正点原子1 天前
《ESP32-S3使用指南—IDF版 V1.6》第三十四章 RGB触摸实验
单片机·物联网·嵌入式
dumpling01201 天前
新手向:使用STM32通过RS485通信接口控制步进电机
stm32·单片机·嵌入式硬件