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

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

【项目复盘】【四轴飞行器设计】驱动开发部分-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(); // 最后一个字节发送NACKelseI2C_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_INDEXMIN_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; // 通道1case HAL_TIM_ACTIVE_CHANNEL_2: return CHANNEL2_INDEX; // 通道2case HAL_TIM_ACTIVE_CHANNEL_3: return CHANNEL3_INDEX; // 通道3case HAL_TIM_ACTIVE_CHANNEL_4: return CHANNEL4_INDEX; // 通道4default: return INVALID_CHANNEL;  // 无效通道}
}/*** @brief 定时器输入捕获中断回调函数* @param htim 定时器句柄* * 该函数在定时器捕获事件发生时触发。* 它根据当前通道索引读取捕获值,计算脉宽,并更新映射值。* 上升沿和下降沿捕获交替进行。*/
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {if (htim->Instance == TIM4) {  // 检查是否为 TIM4uint32_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}; // 初始角度均为0void 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

3. IIC调试工具

我只接触过逻辑分析仪

3.2. PWM相关

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/96190.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/96190.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/96190.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

wpf之ComboBox

前言 wpf中ComboBox的应用非常广泛&#xff0c;本文就来介绍ComboBox在wpf中的应用。 1、非MVVM模式下 1.1 xaml添加元素<ComboBox x:Name"cbx_test1" SelectedIndex" 0" ><ComboBoxItem >小明</ComboBoxItem ><ComboBoxItem &g…

从零开始学AI——13

前言 夏天快要过去&#xff0c;本书也快接近尾声了。 第十三章 13.1 半监督学习 在此之前&#xff0c;我们讨论的所有学习范式都具有非常明确的边界条件&#xff1a; 监督学习&#xff1a;我们拥有大量带标签的数据样本(xi,yi)(x_i, y_i)(xi​,yi​)&#xff0c;目标是学习从输…

k8sday12数据存储(1/2)

目录 一、简单基本存储 1、EmptyDir 1.1概念 1.2作用 1.3配置文件 1.4测试 2、HostPath 2.1概念 2.2作用 2.3配置文件 2.4测试 ①、数据共享 ②、持久化存储 3、NFS 3.1概念 3.2作用 3.3NFS服务安装 ①、设置主节点为NFS服务器 ②、给副节点安装NFS客户端工…

Spring Framework 常用注解详解(按所属包分类整理)

在使用 Spring Framework 进行开发时&#xff0c;注解&#xff08;Annotation&#xff09;是实现 依赖注入&#xff08;DI&#xff09;、组件扫描、AOP 切面、事务管理 和 Web 请求映射 的核心手段。Spring 提供了丰富且结构清晰的注解体系&#xff0c;这些注解按照功能被组织在…

ROADS落地的架构蓝图

2 ROADS落地的架构蓝图 将ROADS体验从理念转化为现实&#xff0c;需要一套完整且自顶向下的架构蓝图作为支撑。华为的实践表明&#xff0c;数字化转型的成功依赖于多个架构层次的协同推进&#xff0c;而非单点技术的应用。该蓝图通常包含以下五个关键层次&#xff0c;每一层都承…

如何构建一个神经网络?从零开始搭建你的第一个深度学习模型

在深度学习的海洋中&#xff0c;神经网络就像一艘船&#xff0c;承载着数据的流动与特征的提取。而构建一个神经网络&#xff0c;就像是在设计这艘船的结构。本文将带你一步步了解如何使用 PyTorch 构建一个完整的神经网络模型&#xff0c;涵盖网络层的组织、前向传播与反向传播…

自学嵌入式第二十三天:数据结构(3)-双链表

一、strtokchar * strtok(char *s1,char *s2);截断字符串&#xff0c;在s1字符串中找到s2截取前一段返回&#xff0c;如需要再次截取剩余段&#xff0c;再使用此函数s1输入NULL即可&#xff1b;二、bzerobzero(char *p,size_t size);清零,从p地址开始&#xff0c;清零size个bit…

河南萌新联赛2025第六场 - 郑州大学

暑期集训已经接近尾声&#xff0c;一年六场的暑期萌新联赛也已经结束了&#xff0c;进步是比较明显的&#xff0c;从一开始的七八百名到三四百名&#xff0c;虽然拿不出手&#xff0c;但是这也算对两个月的集训的算法初学者的我一个交代。 比赛传送门&#xff1a;河南萌新联赛…

2-1.Python 编码基础 - 基础运算符(算术运算符、赋值运算符、比较运算符、逻辑运算符)

一、算术运算符 1、基本介绍编号运算符说明示例输出结果1两数相加10 20302-两数相减10 - 20-103*两数相乘&#xff0c;或者返回一个被重复若干次的字符串10 * 202004/两数相除10 / 200.55//两数相除并返回商的整数部分9 // 246%两数相除并返回余数10 % 507**幂运算10 ** 21002…

CMOS知识点 MOS管不同工作区域电容特性

知识点14&#xff1a;MOSFET的电容主要来源于其物理结构&#xff1a;栅氧层电容&#xff1a;栅极&#xff08;G&#xff09;与衬底&#xff08;B&#xff09;、沟道、源&#xff08;S&#xff09;、漏&#xff08;D&#xff09;之间隔着二氧化硅绝缘层&#xff0c;自然形成电容…

预测性维护+智能优化:RK3568+FPGA方案在储能行业的应用

在储能行业&#xff0c;RK3568FPGA方案通过预测性维护和智能优化技术&#xff0c;显著提升系统可靠性和经济性。该方案采用异构架构&#xff08;FPGA处理高速信号采集&#xff0c;RK3568负责策略计算与通信管理&#xff09;&#xff0c;实现微秒级响应和精准控制。‌26一、预测…

工业4.0时代,耐达讯自动化Profibus转光纤如何重构HMI通信新标准?“

在智能制造与工业4.0浪潮下&#xff0c;HMI&#xff08;人机界面&#xff09;作为设备与操作员之间的“桥梁”&#xff0c;承担着实时数据显示、设备监控及交互控制的核心职能。然而&#xff0c;传统Profibus总线在HMI连接中常因电磁干扰、传输距离限制等问题&#xff0c;导致画…

SpringClound——网关、服务保护和分布式事务

一、网关网络的关口&#xff0c;负责请求的路由、转发、身份验证server:port: 8080 spring:cloud:nacos:discovery:server-addr: 192.168.96.129:8848gateway:routes:- id: item-serviceuri: lb://item-servicepredicates:- Path/items/**,/search/**- id: user-serviceuri: lb…

【C++】模版(初阶)

目录 一. 函数模版 1. 格式 原理 2. 函数模版的实例化 二. 类模板 void Swap(int& left, int& right) {int temp left;left right;right temp; }void Swap(double& left, double& right) {double temp left;left right;right temp; }void Swap(char&…

InfluxDB 开发工具链:IDE 插件与调试技巧(二)

四、利用 IDE 插件提升开发效率 4.1 代码编写技巧 在使用安装了 InfluxDB 插件的 IDE 进行代码编写时&#xff0c;我们可以充分利用插件提供的代码导航和智能提示功能&#xff0c;来显著提高编写 InfluxDB 相关代码的效率和准确性。 以一个涉及多个 Measurement 和复杂查询条…

定制开发开源AI智能名片S2B2C商城小程序:场景体验新维度与四重目标达成

摘要&#xff1a;本文聚焦于定制开发开源AI智能名片S2B2C商城小程序&#xff0c;探讨其在场景体验领域的应用与价值。通过深入分析场景体验的最高境界——深体验、强认知、高传播、关系深化这四个目标&#xff0c;阐述该小程序如何凭借自身特性与功能&#xff0c;在商业场景中实…

开源 GIS 服务器搭建:GeoServer 在 Linux 系统上的部署教程

GeoServer 是一个开源的地理信息服务服务器&#xff0c;可以发布地图、矢量数据和栅格数据。 1. 更新系统 sudo apt update && sudo apt upgrade -y2. 安装 Java 11 GeoServer 需要 Java 运行环境&#xff0c;这里用 OpenJDK 11。 sudo apt install openjdk-11-jdk…

前端面试通关:Cesium+Three+React优化+TypeScript实战+ECharts性能方案

前端面试题详解与更多面试题 WebGLCesiumThree 1. 自我介绍 回答要点&#xff1a; 教育背景和工作经验技术栈和专长领域参与过的重点项目个人优势和学习能力职业规划 示例&#xff1a; “我是一名有前端开发经验的工程师&#xff0c;熟练掌握React、Vue等主流框架&#x…

集成电路学习:什么是Object Tracking目标跟踪

Object Tracking:目标跟踪 Object Tracking,即目标跟踪,是计算机视觉领域的一个重要研究方向,它专注于在视频帧序列中连续地监测和定位一个或多个目标对象的位置。以下是对目标跟踪技术的详细解析: 一、定义与目的 定义: 目标跟踪是指在视频序列中,通过特定的算法…

深入理解计算机系统

参考书籍 8-18 处理器体系结构不同于冯诺依曼与哈佛体系 压栈与退栈与理解c等高级语言的工作原理息息相关&#xff0c;也是常用的攻击手段 Buffer Overflow的主要技术基础 day2 继续读前言之类的 本书前言 这本书&#xff0c;讲述应用程序员如何能够利用系统知识来编写更好…