【項目復盤】【四軸飛行器設計】驅動開發部分

由于在參加面試時總需要花時間一點一點的回憶自己的項目內容,故我打算直接寫一系列的項目復盤博客,方便每次面試前的回憶。內容僅作分享交流,如有謬誤歡迎指正。

本項目系列的文章目錄如下:

【項目復盤】【四軸飛行器設計】驅動開發部分-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;講述應用程序員如何能夠利用系統知識來編寫更好…