由于在參加面試時總需要花時間一點一點的回憶自己的項目內容,故我打算直接寫一系列的項目復盤博客,方便每次面試前的回憶。內容僅作分享交流,如有謬誤歡迎指正。
本項目系列的文章目錄如下:
【項目復盤】【四軸飛行器設計】驅動開發部分-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捕獲頻率和占空比的原理
對于頻率測量,可以捕獲兩次上升沿觸發的計數值差值,再用時鐘頻率/預分頻系數/捕獲值。
對于占空比測量,分別設置中斷觸發方式為上升沿和下降沿,計算兩次中斷之間間隔的計數值,即可獲得高電平的持續時間,這個間隔再除以一個周期的計數值,便可得到占空比。