RM遥控器接收程序的分析
由遙控器接收分析串口與DMA
RM的遙控器在使用的過程中在大體上可以分成兩個部分:“信息的接收”與“信息的解析”,在信息的接收中主要用到了串口的空閑中斷和DMA雙緩沖區接收在本篇的信息接收部分主要根據RM官方給出的代碼來研究一下串口的空閑中斷與DMA雙緩沖區如何配合使用,在信息解析的時候主要來研究一下RM官方給出的代碼例程是怎么在那解析的。
1. 信息的接收
1.1 串口初始化(寄存器)
例程
首先我們給出串口的初始化部分官方給出的代碼
void RC_init(uint8_t *rx1_buf, uint8_t *rx2_buf, uint16_t dma_buf_num) {//enable the DMA transfer for the receiver request//使能DMA串口接收SET_BIT(huart1.Instance->CR3, USART_CR3_DMAR);//enalbe idle interrupt//使能空閑中斷__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);//disable DMA//失效DMA__HAL_DMA_DISABLE(&hdma_usart1_rx);while(hdma_usart1_rx.Instance->CR & DMA_SxCR_EN){__HAL_DMA_DISABLE(&hdma_usart1_rx);}hdma_usart1_rx.Instance->PAR = (uint32_t) & (USART1->DR);//memory buffer 1//內存緩沖區1hdma_usart1_rx.Instance->M0AR = (uint32_t)(rx1_buf);//memory buffer 2//內存緩沖區2hdma_usart1_rx.Instance->M1AR = (uint32_t)(rx2_buf);//data length//數據長度hdma_usart1_rx.Instance->NDTR = dma_buf_num;//enable double memory buffer//使能雙緩沖區SET_BIT(hdma_usart1_rx.Instance->CR, DMA_SxCR_DBM);//enable DMA//使能DMA__HAL_DMA_ENABLE(&hdma_usart1_rx);}從上面的代碼中我們也可以看出其中有很多直接對寄存器進行的操作,但是這些寄存器完成的功能看起來好像跟一些函數完成的相同的工作,這也給了我們一個深入研究HAL庫或者說是stm32的方向。那么之后我們的分析方法就是將例程中的寄存器操作對照著參考手冊搞懂他們是在干嘛的,并且那這些東西跟功能類似的HAL庫提供的接口函數進行一個對照與比較。
注意:參考手冊一定要找對,是F4參考手冊,可不是F1的,否則會讓你非常地困惑,我就是拿著F1的參考手冊對著找了兩三個小時,發現很多地方都對不上,時間就白白浪費掉了。
既然是想要以寄存器為切入點,那么我們就以用到的寄存器來做分段
USART_CR3
SET_BIT
程序一開始就來了一個SET_BIT來使能DMA串口接收。我們點到它的定義處可以發現,SET_BIT實際上是一個宏定義,而這個宏定義的作用就是將BIT賦值給寄存器REG。 而我們開啟DMA串口接收則是將USART_CR3_DMAR賦值給串口1的CR3寄存器,當然,這里說到的賦值是“或等于”,這樣可以不影響該寄存器的其他標志位。
#define SET_BIT(REG, BIT) ((REG) |= (BIT)) SET_BIT(huart1.Instance->CR3, USART_CR3_DMAR);寄存器值宏定義 #define USART_CR3_DMAR_Msk (0x1UL << USART_CR3_DMAR_Pos) /*!< 0x00000040 */ #define USART_CR3_DMAR USART_CR3_DMAR_Msk /*!<DMA Enable Receiver*/USART_CR3_DMAR同樣是一個宏定義,它的值是0x00000040,也就是在CR3寄存器的第7位上賦值了1.我們可以查一下參考手冊,看看這一位是什么作用。
注意,我們說的第七位是從1 開始的“第七個空”,而在手冊上的位7是從0開始的第七位,因此我們要找的是位6,可以看到該位是DMA使能接收器
USART_CR1
__HAL_UART_ENABLE_IT
在使能了DMA串口接收后,打開了串口的空閑中斷
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
__HAL_UART_ENABLE_IT又是一個宏定義,它里面干的活兒和上面的SET_BIT基本上是一樣的,都是將宏定義好了的寄存器數值填入對應的寄存器中
#define __HAL_UART_ENABLE_IT(__HANDLE__, __INTERRUPT__) ((((__INTERRUPT__) >> 28U) == UART_CR1_REG_INDEX)? ((__HANDLE__)->Instance->CR1 |= ((__INTERRUPT__) & UART_IT_MASK)): \(((__INTERRUPT__) >> 28U) == UART_CR2_REG_INDEX)? ((__HANDLE__)->Instance->CR2 |= ((__INTERRUPT__) & UART_IT_MASK)): \((__HANDLE__)->Instance->CR3 |= ((__INTERRUPT__) & UART_IT_MASK)))而UART_IT_IDLE是一個宏定義
#define UART_IT_IDLE ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_IDLEIE))它是UART_CR1_REG_INDEX左移了28位后或上了一個USART_CR1_IDLEIE,關鍵點在于USART_CR1_IDLEIE,它的值是
#define USART_CR1_IDLEIE_Pos (4U) #define USART_CR1_IDLEIE_Msk (0x1UL << USART_CR1_IDLEIE_Pos) /*!< 0x00000010 */ #define USART_CR1_IDLEIE USART_CR1_IDLEIE_Msk /*!<IDLE Interrupt Enable可以看到實際上UART_IT_IDLE就是將CR1的第五個空填上了1,也就是位4置了1. 第四位是啥?我們看手冊
可以看到,是IDLE中斷使能,也就是空閑中斷使能
DMA_SXCR
__HAL_DMA_DISABLE
之后我們關閉了DMA,關閉DMA?為什么? 這也是我看到的時候的第一反應,別急,我們順著我們的這個方法往下走,當答案浮現的時候,你會感到欣喜的。
__HAL_DMA_DISABLE,這又是一個宏定義,而且還是一對兒,他之前還有一個__HAL_DMA_ENABLE,我們把他倆一起給看了
#define __HAL_DMA_ENABLE(__HANDLE__) ((__HANDLE__)->Instance->CR |= DMA_SxCR_EN) #define __HAL_DMA_DISABLE(__HANDLE__) ((__HANDLE__)->Instance->CR &= ~DMA_SxCR_EN)可以看到,這個宏定義又是一個直接的寄存器賦值操作,賦的值都是DMA_SxCR_EN,只不顧另一個取反了。我們看一下DMA_SxCR_EN是個啥,然后去找它對應的寄存器位。
#define DMA_SxCR_EN_Pos (0U) #define DMA_SxCR_EN_Msk (0x1UL << DMA_SxCR_EN_Pos) /*!< 0x00000001 */ #define DMA_SxCR_EN DMA_SxCR_EN_Msk可以看出,DMA_SxCR_EN它是想要置寄存器的第一個空,也就是位0
看到了么,該位是數據流使能,也就是這一位置1,DMA才能真正發揮作用,因為DMA就是傳輸數據流的嘛。
在關閉DMA后緊跟著一個While循環
while(hdma_usart1_rx.Instance->CR & DMA_SxCR_EN)
{
__HAL_DMA_DISABLE(&hdma_usart1_rx);
}
我們可以試著看一下這個循環到底在干嘛,用人話講出來就是:如果CR寄存器與上DMA_SxCR_EN為1(也就是如果SXCR寄存器的第一位只要還是1),就再給我關掉DMA.意思是非得給人家關了不行?
這一步是干啥的,我們還不太清楚,沒事兒繼續往下走。但是在這里我們先做一個知識的補充:
知識補充:
我們知道HAL庫是將每一個外設都給封裝成了一個句柄,具體來說就是一個“外設_HandleTypeDef” 實例化了的一個對象。例如,我們操作的串口1,就是在操作UART_HandleTypeDef示例化了的huart1。
這個huart1,里面包含了很多的東西。例如這個外設現在的狀態,以及很多的配置項。其中有一項非常重要,可以說我們對這個外設的大部分操作都是在修改這個東西里面的值。這個很重要的“東西”就是Instance,我們以huart1中的instance為例,來看看這里面到底是啥
USART_TypeDef *Instance; /*!< UART registers base address */ typedef struct {__IO uint32_t SR; /*!< USART Status register, Address offset: 0x00 */__IO uint32_t DR; /*!< USART Data register, Address offset: 0x04 */__IO uint32_t BRR; /*!< USART Baud rate register, Address offset: 0x08 */__IO uint32_t CR1; /*!< USART Control register 1, Address offset: 0x0C */__IO uint32_t CR2; /*!< USART Control register 2, Address offset: 0x10 */__IO uint32_t CR3; /*!< USART Control register 3, Address offset: 0x14 */__IO uint32_t GTPR; /*!< USART Guard time and prescaler register, Address offset: 0x18 */ } USART_TypeDef;我們看到Instance是一個USART_TypeDef類型的結構體,里面裝的是 與串口相關的寄存器,,準確得說,結構體里面裝的是串口相關的寄存器的值。而這里面的寄存器名字和參考手冊上的名字是一一對應的,因此我們可以通過這里來判斷我們去參考手冊的哪里找。
有的時候我們需要知道某個外設的某個寄存器的地址,例如在DMA從外設到內存傳輸的時候,我們需要知道外設的對應存放數據的那個寄存器的具體地址,例如在串口DMA中我們就要知道串口的DR寄存器(因為串口接收的數據是存到這個寄存器里的)的地址,好讓DMA知道從哪拿數據。 如果要那某個外設的寄存器的地址,我們就要用“外設名->寄存器名” 例如:
USART1->DR DMA1->HIFCR CAN2->BTR ...DMA_SxPAR
在有了上面的那個補充的知識后我們可以更加得心應手地去查看手冊并且可以很自信地明白下面這些操作是在干啥。
在我們退出while循環之后,我們把USART1的DR寄存器地址賦值到了DMA的PAR寄存器中,我們來看一下PAR寄存器是有什么本事
這個寄存器是存放讀/寫數據的外設數據寄存器的地址的。 所謂PAR就是peripheral address register外設地址寄存器。
它的作用就是DMA在用外設到存儲器模式的時候,高速DMA外設是在哪,該去哪拿數據。
在看這個寄存器的時候注意最下面的一句話“這些位收到寫保護,只有DMA_SxCR寄存器中的EN為0時才可以寫入”,你可能會聯想到些什么,可能還沒有完全醒悟,我們可以接著往下看,到時候會給你揭曉。
DMA_SxM0/1AR
在將外設地址寫入到DMA的PAR寄存器中之后,我們又緊接著進行了兩此賦值操作,將兩個數組的地址賦值給了DMA的M0AR和M1AR寄存器中。
這兩個寄存器是用來存放存儲器的地址的,作用就是告訴DMA數據拿到了以后拿去哪、放到哪。
他們和PAR寄存器一樣,都有這樣一句話這些位收到寫保護
DMA_SxNDTR
之后我們在DMA的NDTR寄存器中寫入了一個16位的數,這個數是DMA傳輸的大小,
這個寄存器只有16位可用,最大值是65535。它的作用就是告訴DMA傳多少個 數據以后結束(當然如果開了循環模式的話不會停,會進入DMA傳輸完成中斷,然后再重新裝填該寄存器,然后繼續傳輸)。因為每次DMA傳輸后此寄存器將遞減,該寄存器還有計數的作用,這一點我們后面在信息解析的時候會提到。
這里面同樣有一句話需要注意:“只有在禁止數據流時,才能向此寄存器執行寫操作”
DMA_SxCR
在我們通過填寫寄存器的值告訴了DMA,明確了從哪拿、拿哪去、拿多少的問題后我們使能了雙緩沖區,也就是告訴DMA:我可是給你開了兩個緩沖區的,目的地有兩個,別忘了“雨露均沾”,為什么要用雨露均沾?因為使能了雙緩沖區并且開啟了循環模式之后,在一個緩沖區填滿后,DMA會自動地去把數據sei到另一個緩沖區中,這可不就是雨露均沾么
#define DMA_SxCR_DBM_Msk (0x1UL << DMA_SxCR_DBM_Pos) /*!< 0x00040000 */ #define DMA_SxCR_DBM DMA_SxCR_DBM_Msk SET_BIT(hdma_usart1_rx.Instance->CR, DMA_SxCR_DBM);這一步同樣是用SET_BIT來操作的,賦值的是DMA的CR寄存器的位18
這里又出現了那句話 “此位受到保護”
最后我們又通過調用__HAL_DMA_DISABLE的對象 __HAL_DMA_ENABLE打開了DMA。
流程總結:
解開疑惑
相信在這之前,大家心中的疑惑便已經解開了。疑惑是什么?疑惑就是下面這段代碼的意義。
__HAL_DMA_DISABLE(&hdma_usart1_rx); while(hdma_usart1_rx.Instance->CR & DMA_SxCR_EN) {__HAL_DMA_DISABLE(&hdma_usart1_rx); }第一個問題,為什么要把DMA給關掉?我給大家的暗示已經夠多了。
因為接下來我們要對DMA的相關寄存器進行配置,,明確從哪拿、拿哪去、拿多少。為了達到這個目的,我們需要向對應的寄存器寫入數據、配置,而這些寄存器都有寫保護,也就是說“當DMA已經被開啟,DMA的SXCR寄存器的第一位被置1時,這些配置寄存器是無法進行寫入的”因此我們需要將DMA給關掉,才能把配置寫入,讓DMA按照我們想要的方式運行。
第二個問題,后面這個while循環是在干嘛?
因為參考手冊中有這樣一段話:
警告: 要關閉連接到 DMA 數據流請求的外設,必須首先關閉外設連接 的 DMA 數據流,然后等待 EN 位 = 0**。只有這樣才能安全地禁止外設 這就是為什么要加那個while等待的原因==
如果使能了數據流,通過重置 DMA_SxCR 寄存器中的 EN 位將其禁止,然后讀取此位
以確認沒有正在進行的數據流操作。將此位寫為 0 不會立即生效,因為實際上只有所有
當前傳輸都已完成時才會將其寫為 0。當所讀取 EN 位的值為 0 時,才表示可以配置數
據流。因此在開始任何數據流配置之前,需要等待 EN 位置 0。應將先前的數據塊 DMA
傳輸中在狀態寄存器(DMA_LISR 和 DMA_HISR)中置 1 的所有數據流專用的位置 0,
然后才可重新使能數據流。
也就是說:參考手冊可以解決掉我們大部分的問題,但是關鍵是我們要找到它到底寫在哪。反正這個警告是我無意間翻到的…
1.2 DMA與雙緩沖區的開啟(API對比)
通過1.1的講解,相信大家對寄存器如何配置串口DMA有了比較詳細的認識,但是畢竟相比較于調用HAL給我們的函數,我們還是很少會用到寄存器直接編程的,那么他們的區別到底在哪里?搞懂這些區別與聯系,相信會對HAL庫編程有一個更詳細的理解。下面我們開始對比吧
庫函數開啟雙緩沖區
那在網上看到的配置過程:
HAL_DMAEx_MultiBufferStart() 用了這個函數來配置雙緩沖區
HAL_StatusTypeDef HAL_DMAEx_MultiBufferStart(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t SecondMemAddress, uint32_t DataLength)SrcAddress:源內存緩沖區地址; DstAddress:目標內存緩沖區地址; SecondMemAddress:第二個內存緩沖區地址; DataLength:從源傳輸到目標的數據長度;HAL_DMAEx_MultiBufferStart具體代碼長這樣:
HAL_StatusTypeDef HAL_DMAEx_MultiBufferStart(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t SecondMemAddress, uint32_t DataLength) {HAL_StatusTypeDef status = HAL_OK;/* Check the parameters */assert_param(IS_DMA_BUFFER_SIZE(DataLength));/* Memory-to-memory transfer not supported in double buffering mode */if (hdma->Init.Direction == DMA_MEMORY_TO_MEMORY){hdma->ErrorCode = HAL_DMA_ERROR_NOT_SUPPORTED;status = HAL_ERROR;}else{/* Process Locked */__HAL_LOCK(hdma);if(HAL_DMA_STATE_READY == hdma->State){/* Change DMA peripheral state */hdma->State = HAL_DMA_STATE_BUSY; /* Enable the double buffer mode */hdma->Instance->CR |= (uint32_t)DMA_SxCR_DBM; //DMA_SxCR_DBM : 0x00040000 /* Configure DMA Stream destination address */hdma->Instance->M1AR = SecondMemAddress;/* Configure the source, destination address and the data length */DMA_MultiBufferSetConfig(hdma, SrcAddress, DstAddress, DataLength);/* Enable the peripheral */__HAL_DMA_ENABLE(hdma);}else{/* Return error status */status = HAL_BUSY;}}return status; }這個開啟雙緩存區的函數干了哪些工作:
這個函數是在檢驗設置的數據長度是否合規長度要大于1小于10000
并且判斷如果是從內存到內存模式的話是不允許循環模式的,也就不能夠開啟雙緩存區。
如果滿足條件的話就先鎖上dma。
__HAL_LOCK(hdma);這個鎖是Process Locked,起到的作用類似于上廁所的時候廁所門的那個“有人”標志,如果上了操作系統,多個進程運行,那么就要避免同時去操作dma的情況,尤其是同時用dma去寫入東西,因為那樣就不知道數據到底是誰寫的了。因此有進程在用dma的時候就先把DMA給占住,說:我在用它了 。
在DMA外設的CR寄存器中賦值
這個DMA_SxCR_DBM的值就是0x00040000
將第二個內存緩沖區地址寫入DMA的M1AR寄存器中
/* Configure DMA Stream destination address */hdma->Instance->M1AR = SecondMemAddress;調用這個函數來配置dma的source、destnation address和數據長度
DMA_MultiBufferSetConfig(hdma, SrcAddress, DstAddress, DataLength);這個函數里的內容和直接用寄存器操作相同,也是在NDTR寄存器中寫入數據長度,在PAR寄存器和M0AR寄存器中分別寫入源地址和目標地址。
開啟DMA。
這樣分析下來,我們可以發現,不論是調用函數還是直接使用寄存器賦值,流程上幾乎是一樣的,都是都是先關閉DMA,然后給各種相關寄存器進行賦值,最后開啟DMA。
庫函數開啟串口DMA
同樣,我們在網上看到的的一些教程是如何教我們開啟DMA的呢? 大多都是讓調用下面這個函數
HAL_UART_Receive_DMA那么用SET_BIT寫入usart的CR3寄存器開啟DMA接收和直接用HAL_UART_Receive_DMA函數開啟DMA接收有什么區別?
我認為:這兩種形式都可以起到開啟DMA的作用,但是用SET_BIT直接操作寄存器賦值更單純,僅僅開啟了DMA的接收,而HAL_UART_Receive_DMA在開啟DMA傳輸的時候會打開DMA傳輸完成中斷。
我們可以來看一下HAL_UART_Receive_DMA這個函數內部到底在干嘛
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) {/* Check that a Rx process is not already ongoing */if (huart->RxState == HAL_UART_STATE_READY){if ((pData == NULL) || (Size == 0U)){return HAL_ERROR;}/* Process Locked */__HAL_LOCK(huart);/* Set Reception type to Standard reception */huart->ReceptionType = HAL_UART_RECEPTION_STANDARD;return (UART_Start_Receive_DMA(huart, pData, Size)); ☆☆☆☆☆☆}else{return HAL_BUSY;} }我們可以發現,在這個函數中很大一部分的內容都是在維護USART的狀態,以保證這個外設不會被我們用著用著給搞得爛七八糟的。
其中真正起到“實質性作用”是我上面打了星星的那一行,HAL庫主要調用UART_Start_Receive_DMA這個函數來開啟USART的DMA。
我們再看看這個函數里是在干嘛
HAL_StatusTypeDef UART_Start_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) {uint32_t *tmp;huart->pRxBuffPtr = pData;huart->RxXferSize = Size;huart->ErrorCode = HAL_UART_ERROR_NONE;huart->RxState = HAL_UART_STATE_BUSY_RX;/* Set the UART DMA transfer complete callback */huart->hdmarx->XferCpltCallback = UART_DMAReceiveCplt;/* Set the UART DMA Half transfer complete callback */huart->hdmarx->XferHalfCpltCallback = UART_DMARxHalfCplt;/* Set the DMA error callback */huart->hdmarx->XferErrorCallback = UART_DMAError;/* Set the DMA abort callback */huart->hdmarx->XferAbortCallback = NULL;/* Enable the DMA stream */tmp = (uint32_t *)&pData;HAL_DMA_Start_IT(huart->hdmarx, (uint32_t)&huart->Instance->DR, *(uint32_t *)tmp, Size); ☆☆☆☆/* Clear the Overrun flag just before enabling the DMA Rx request: can be mandatory for the second transfer */__HAL_UART_CLEAR_OREFLAG(huart);/* Process Unlocked */__HAL_UNLOCK(huart);/* Enable the UART Parity Error Interrupt */ATOMIC_SET_BIT(huart->Instance->CR1, USART_CR1_PEIE);/* Enable the UART Error Interrupt: (Frame error, noise error, overrun error) */ATOMIC_SET_BIT(huart->Instance->CR3, USART_CR3_EIE);/* Enable the DMA transfer for the receiver request by setting the DMAR bitin the UART CR3 register */ATOMIC_SET_BIT(huart->Instance->CR3, USART_CR3_DMAR);return HAL_OK; }其中開局又是一套狀態的維護,緊接著設置了一系列的回調函數。然后我們往下找找找,找到到了熟悉的字眼“SET_BIT”
/* Enable the DMA transfer for the receiver request by setting the DMAR bit
in the UART CR3 register */
ATOMIC_SET_BIT(huart->Instance->CR3, USART_CR3_DMAR);
這句話開啟了我們的USART的DMA傳輸,和我們的例程中的操作可謂是一模一樣。
但是,在我們從上往下找的時候,發現了標注五角星的那條語句
HAL_DMA_Start_IT(huart->hdmarx, (uint32_t)&huart->Instance->DR, *(uint32_t *)tmp, Size);
Start_IT!!!,誰讓這家伙給我開啟DMA中斷的,在百度中沒有人跟我說過還有個DMA的中斷呀?不都是用的串口的中斷么?
開中斷?開了什么中斷?我再看看你這家伙偷偷干了些啥
HAL_StatusTypeDef HAL_DMA_Start_IT(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength) {HAL_StatusTypeDef status = HAL_OK;/* calculate DMA base and stream number */DMA_Base_Registers *regs = (DMA_Base_Registers *)hdma->StreamBaseAddress;/* Check the parameters */assert_param(IS_DMA_BUFFER_SIZE(DataLength));/* Process locked */__HAL_LOCK(hdma);if(HAL_DMA_STATE_READY == hdma->State){/* Change DMA peripheral state */hdma->State = HAL_DMA_STATE_BUSY;/* Initialize the error code */hdma->ErrorCode = HAL_DMA_ERROR_NONE;/* Configure the source, destination address and the data length */DMA_SetConfig(hdma, SrcAddress, DstAddress, DataLength);/* Clear all interrupt flags at correct offset within the register */regs->IFCR = 0x3FU << hdma->StreamIndex;/* Enable Common interrupts*/hdma->Instance->CR |= DMA_IT_TC | DMA_IT_TE | DMA_IT_DME; ☆☆☆ if(hdma->XferHalfCpltCallback != NULL) ☆{ ☆hdma->Instance->CR |= DMA_IT_HT; ☆} ☆☆/* Enable the Peripheral */__HAL_DMA_ENABLE(hdma);}else{/* Process unlocked */__HAL_UNLOCK(hdma); /* Return error status */status = HAL_BUSY;}return status; }首先上來又先是一套狀態維護服務安排上。然后通過DMA_SetConfig函數,將source, destination address and the data length寫入寄存器,直到個函數里面,才真真正正的將HAL_UART_Receive_DMA函數中的參數寫入相對應的寄存器中,可見HAL的封裝真是一層一層的呀,層數真不少!
我們最重要要看的是上面我標注五角星的語句。
hdma->Instance->CR |= DMA_IT_TC | DMA_IT_TE | DMA_IT_DME;
這句話中的DMA_IT_TC 、DMA_IT_TE 和DMA_IT_DME都是宏定義:
#define DMA_IT_TC ((uint32_t)DMA_SxCR_TCIE) /*!< 0x00000010 */ #define DMA_IT_TE ((uint32_t)DMA_SxCR_TEIE) /*!< 0x00000008 */ #define DMA_IT_DME ((uint32_t)DMA_SxCR_DMEIE) /*!< 0x00000002 */可以看到把CR同時賦值了3個標志位:TCIE、TEIE、DMEIE。然后我們翻看F4的參考手冊,看看他們都是干啥的。
可以看到,都是中斷使能。這個小兔崽子,給打開了一堆的中斷。而且還在之后打開了半傳輸完成中斷
if(hdma->XferHalfCpltCallback != NULL)
{
hdma->Instance->CR |= DMA_IT_HT;
}
這里面的hdma->XferHalfCpltCallback 是不是有點眼熟?沒錯,就是在UART_Start_Receive_DMA函數中寫入了UART_DMARxHalfCplt的。所以這個if語句中的指令是會被執行的。也就是說,我們的半傳輸完成中斷會被開啟。
把一切都設置好了以后,開啟了DMA外設
__HAL_DMA_ENABLE(hdma);
總結起來,這個HAL_UART_Receive_DMA函數干了這些事:
等等,有點蒙了。再回一下例程中的操作:
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
我們不是當初是用這個函數打開中斷的么,而且還清清楚楚地知道是打開了空閑中斷,而且之后的數據解析操作都是在這個串口的空閑中斷里進行了呀。咋又給打開了個“完成中斷”和“半傳輸完成中斷”這是要干嘛,我到底該在哪個中斷里進行操作?
當然還是在空閑中斷里進行操作啦。要注意分清,串口中斷是串口上的,DMA中斷時DMA上的,這兩個人是沒有關系的。要在觀念中去把這兩個東西給分離開來。
串口的空閑中斷是在串口接收數據的時候會根據接收的數據幀的“結束標識”后面跟不跟“起始標識”而選擇進入的,如果說我們發的數據幀在一段時間內(這個時間是很短的,但是很精準的,不用擔心)結束標識后面沒有再跟著起始標識,那么就判斷這一次的數據發送完畢了,進入空閑中斷。而DMA的中斷,是在DMA外設中設置的,DMA接收到從UART外設來的數據后進行傳輸,傳輸到原來設定的值的一半的時候會進一次半傳輸完成中斷,傳輸完之后會進一次傳輸完成中斷。
我們大概也可以理解為什么我們在cubemx里選擇對應外設的DMA的時候cubemx會自動給我們把DMA的中斷給我們打開的原因了。
在上面的程序中調用了DMA_SetConfig(hdma, SrcAddress, DstAddress, DataLength);這個函數才是真真正正地在設置DMA的寄存器
static void DMA_SetConfig(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength) {/* Clear DBM bit */hdma->Instance->CR &= (uint32_t)(~DMA_SxCR_DBM);/* Configure DMA Stream data length */hdma->Instance->NDTR = DataLength;/* Memory to Peripheral */if((hdma->Init.Direction) == DMA_MEMORY_TO_PERIPH){/* Configure DMA Stream destination address */hdma->Instance->PAR = DstAddress;/* Configure DMA Stream source address */hdma->Instance->M0AR = SrcAddress;}/* Peripheral to Memory */else{/* Configure DMA Stream source address */hdma->Instance->PAR = SrcAddress;/* Configure DMA Stream destination address */hdma->Instance->M0AR = DstAddress;} }可以看到這里的寄存器操作,和我們直接操作寄存器的那一套幾乎上是一模一樣了,只不過是考慮到了更多的場景。
2. 信息的解析
例程
void USART1_IRQHandler(void) {if(huart1.Instance->SR & UART_FLAG_RXNE)//接收到數據{__HAL_UART_CLEAR_PEFLAG(&huart1);}else if(USART1->SR & UART_FLAG_IDLE){static uint16_t this_time_rx_len = 0;__HAL_UART_CLEAR_PEFLAG(&huart1);if ((hdma_usart1_rx.Instance->CR & DMA_SxCR_CT) == RESET){/* Current memory buffer used is Memory 0 *///disable DMA//失效DMA__HAL_DMA_DISABLE(&hdma_usart1_rx);//get receive data length, length = set_data_length - remain_length//獲取接收數據長度,長度 = 設定長度 - 剩余長度this_time_rx_len = SBUS_RX_BUF_NUM - hdma_usart1_rx.Instance->NDTR;//reset set_data_lenght//重新設定數據長度hdma_usart1_rx.Instance->NDTR = SBUS_RX_BUF_NUM;//set memory buffer 1//設定緩沖區1hdma_usart1_rx.Instance->CR |= DMA_SxCR_CT;//enable DMA//使能DMA__HAL_DMA_ENABLE(&hdma_usart1_rx);if(this_time_rx_len == RC_FRAME_LENGTH){sbus_to_rc(sbus_rx_buf[0], &rc_ctrl);}}else{/* Current memory buffer used is Memory 1 *///disable DMA//失效DMA__HAL_DMA_DISABLE(&hdma_usart1_rx);//get receive data length, length = set_data_length - remain_length//獲取接收數據長度,長度 = 設定長度 - 剩余長度this_time_rx_len = SBUS_RX_BUF_NUM - hdma_usart1_rx.Instance->NDTR;//reset set_data_lenght//重新設定數據長度hdma_usart1_rx.Instance->NDTR = SBUS_RX_BUF_NUM;//set memory buffer 0//設定緩沖區0DMA2_Stream2->CR &= ~(DMA_SxCR_CT);//enable DMA//使能DMA__HAL_DMA_ENABLE(&hdma_usart1_rx);if(this_time_rx_len == RC_FRAME_LENGTH){//處理遙控器數據sbus_to_rc(sbus_rx_buf[1], &rc_ctrl);}}} }RXNE與RXNEIE
? 在分析這個例程的代碼時上來就讓我迷惑了,咋上來就是一個RXNE標志??咋沒見過這個玩意兒。去查看寄存器
當串口收到數據之后該位會被置1,并且如果RXNEIE這個時候也被置1時,就會進入中斷。于是便產生了一個疑問:一有數據進來就會被置位?還是等數據足夠了再置位?經過百度,知道了,每接收一個字節就會被置位,也就是說如果這個時候RXNEIE如果被使能,那么串口將每接收一個字節的數據就會進一次中斷。是不是有點熟悉?HAL_UART_Recesive_IT這家伙不就是干這個事的么?找一下
HAL_StatusTypeDef UART_Start_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) { huart->pRxBuffPtr = pData; huart->RxXferSize = Size; huart->RxXferCount = Size;huart->ErrorCode = HAL_UART_ERROR_NONE; huart->RxState = HAL_UART_STATE_BUSY_RX;/* Process Unlocked */ __HAL_UNLOCK(huart);/* Enable the UART Parity Error Interrupt */ __HAL_UART_ENABLE_IT(huart, UART_IT_PE);/* Enable the UART Error Interrupt: (Frame error, noise error, overrun error) */ __HAL_UART_ENABLE_IT(huart, UART_IT_ERR);/* Enable the UART Data Register not empty Interrupt */ __HAL_UART_ENABLE_IT(huart, UART_IT_RXNE); ☆☆☆☆☆☆return HAL_OK; }可以看到HAL_UART_Recesive_IT函數實際上就是調用了__HAL_UART_ENABLE_IT,然后使能了RXNE中斷,那這個UART_IT_RXNE實際上是個啥?
define UART_IT_RXNE ((uint32_t)(UART_CR1_REG_INDEX <<28U | USART_CR1_RXNEIE))
#define USART_CR1_RXNEIE_Msk (0x1UL << USART_CR1_RXNEIE_Pos) /*!< 0x00000020 */ #define USART_CR1_RXNEIE USART_CR1_RXNEIE_Msk /*!<RXNE Interrupt Enable喏,RXNEIE出來了。也就是說HAL_UART_Recesive_IT這個函數本質上就是在RXNEIE位置了1,使能了接收完成中斷
回到上面的程序的分析
if(huart1.Instance->SR & UART_FLAG_RXNE)//接收到數據{__HAL_UART_CLEAR_PEFLAG(&huart1);}如果進入中斷,并且讀取數據寄存器非空,那么就清除PE這個標志。PE是個什么標志?
奇偶校驗錯誤??? 也就是說先判斷一手RXNE是為了清除奇偶校驗錯誤標志位,主體還是為了避免在傳輸的時候出現信號干擾,出現校驗錯誤時如果不及時清除校驗錯誤標志位,那么會一直進中斷,而進不去主程序。 但是這里有一個前提昂,就是PEIE要被使能才能因為該錯誤進入中斷。
在信息接收那一節中的RC_Init函數中,是直接操作寄存器開啟IDLE中斷的,因此不管有沒有校驗錯誤,都不會以內校驗錯誤進入中斷,因此這個判斷函數在直接賦值寄存器的方法開啟中斷時是不起作用的。那么為什么要加一個這樣的判斷呢?因為我們大多數都不是直接賦值寄存器開啟IDLE中斷的而是調用HAL_UART_Recesive_IT、HAL_UART_Recesive_DMA來開啟中斷的**(HAL_UART_Recesive_DMA會主動調用HAL_UART_Recesive_IT這個函數,給你把接收中斷打開**),這些函數因為是HAL給封裝的,所以很“規矩”,它會主動地在你開啟IT、DMA的時候給你把錯誤中斷也打開。
如果你用HAL庫函數打開中斷,就需要在IRQHandle中進行相應的標志位的處理,但是HAL庫在中斷中調用下面這個函數把這些工作都給你做了
HAL_UART_IRQHandler(&huart1);串口的DMA請求機制
串口通信的時候是一個字節一個字節的傳輸的,因為設置的數據位是8嘛。每一幀數據除了八個數據位還有一些校驗位之類的,還有兩個很重要的地方,就是“起始位”和“停止位”。很多地方可以用到這兩個位來進行一些判斷,例如,USART的空閑中斷。串口怎么知道現在空閑了呢?因為他在檢測到一個停止位之后如果沒有檢測到起始位,那么就會認為這一大串數據是一次發送的數據。還有一個地方的應用就在DMA。
思考一下,我們在Cubemx中設置的DMA傳輸大小是bit 、Half word 還是 Words是干嘛用的?是確定DMA轉發閾值用的,也就是說,我DMA在傳輸的時候不是你來一個bit我就送走一個bit,跟入棧似的送到目標寄存器。而是等存夠一定的數量的時候才會進行一次傳輸。 那么DMA怎么知道我是不是要傳輸了呢? 外設在需要DMA傳輸的時候會發起一個DMA請求,DMA在收到這個請求的時候就知道自己該送數據走了,于是便會進行一次傳輸(這次傳輸的大小自然就是之前設置好的bit、Half Word…)。 也就是說,DMA之所以能做到的一次傳輸傳輸固定大小的數據,是因為使用DMA的外設會在收到這個固定大小的數據之后對DMA發起一個請求,讓DMA幫忙把這么長的數據給送到要去的地方。(感受一下就可以發現,我們所說的配置DMA,并不是配置DMA,而是配置外設,是配置外設什么時候呼叫DMA,這也就是為什么USART中的DMA設置部分叫做**“DMA Request Settings”) 那么外設是如何做到精準的每8bit、或者16bit、32bit請求一次DMA呢?答案是停止位**
因為每傳輸8bit的數據就會發送一個停止位,那么串口在使能了接收DMA之后.只要一接收到一個Bit就會產生一個**“DMA request”,DMA收到這個request之后就可以訪問外設中的數據進行發送了。并且在訪問數據的時候會給外設一個應答,這時候外設就知道有人來讀取了,這也就是為啥DMA可以消除RXNE標志位了。(RXNE在接收到一字節**的數據之后會被置1,如果RXNEIE位已經被置1,也就是打開了接收中斷后,RXNE一旦置1便進入中斷)
發現錯誤
再往下走,判斷USART1->SR & UART_FLAG_IDLE,如果IDLE標志位被置1,說明遙控器已經發完了一幀數據,進入這個中斷的原因是IDLE中斷(空閑中斷)。然后上來又是一個奇偶校驗錯誤位清空.
__HAL_UART_CLEAR_PEFLAG(&huart1);
奇偶校驗錯誤難道就這么頻繁?需要這么小心地去處理它么?更何況我們根本就沒使能PEIE位,所以我感覺如果是用寄存器開啟的串口+DMA接收,那么沒有必要這樣小心地去處理這個PEFLAG,但是我們看到它這么小心地在處理,可能在串口通訊的時候奇偶校驗錯誤很容易出現的,而且加上我們很多時候用HAL_UART_Recesive_DMA這個函數來開啟的,那么就很有可能被奇偶校驗的錯誤卡在中斷中出不來。但是根據自己的實際測試,如果是按照第一節寄存器的方法開啟串口DMA的話,去掉那兩個PEFLAG的清除也是可以使用的。
而且這里我強烈懷疑是代碼寫錯了,應該是清除IDLE標志位,這樣才是一個正常地有邏輯的流程
__HAL_UART_CLEAR_IDLEFLAG(&huart1);傳輸數據長度計算
回到代碼,如果是IDLE標志位被置1,也就是我們現在進入了空閑中斷,遙控器已經發完了一幀數據,我們就要首先記錄一下當前到底收到了多少字節,和我們預期的一幀數據長度是否一致,如果不一致,那么就說明這一幀數據是不準確的,我們就直接廢棄這一幀數據了,這也就是后面的那個
if(this_time_rx_len == RC_FRAME_LENGTH)
的作用。
而我們計算數據長度的方法就是根據DMA的NDTR寄存器的特性,
因為我們在設置串口的DMA Request的時候設置的是一個字節申請一次DMA傳輸,而我們進入USART的空閑中斷是接收完一幀數據之后才進入的,遙控器的一幀數據長度是18字節
因此,在我們進入空閑中斷的時候,dma已經轉移了18次數據了,也就是說DMA的NDTR寄存器已經有所改變了,準確來說是減少了很多次了,我們可以根據NDTR中還剩下的字節數來簡介判斷出它到底傳輸了多少次,也就是說我們從上一次空閑中斷到這一次進入中斷之間到底接收了多少個字節的數據,這個數就是我們真實接收到的一幀的字節數。因此就有了下面的這個計算公式
//獲取接收數據長度,長度 = 設定長度 - 剩余長度this_time_rx_len = SBUS_RX_BUF_NUM - hdma_usart1_rx.Instance->NDTR;當然這里面還有很重要的一個問題,在我們進入空閑中斷后如果串口又接收到數據,并且傳夠了一個字節甚至多個字節的話,那么就會在我們在串口中處理數據的時候產生一個甚至多個DMA請求,那么我們的接收數據的計算不就不準了么?這個NDTR正在看的時候還在減少,這肯定不會準嘛,因此就需要在我們計算的時候,最好是一進入空閑中斷就先把DMA給關了,不讓它再傳送數據,也就保證了NDTR保持的是我們進入空閑中斷的時刻之前剩余的長度。這就是下面這句話的作用
//失效DMA __HAL_DMA_DISABLE(&hdma_usart1_rx);當我們獲取完這次接收的數據長度以后,我們要重新給NDTR寄存器賦值,以便下次我們可以再次通過上面的公式進行長度的計算。
hdma_usart1_rx.Instance->NDTR = SBUS_RX_BUF_NUM;
雙緩沖區的使用
但是別忘了,我們是有兩個緩沖區來接收數據的。為什么要用雙緩沖區呢?我們知道,普通DMA的目標數據儲存區域只有一個,也就是如果當數據存滿后,新的數據又傳輸過來了,那么舊的數據會被新的數據覆蓋。(這就是普通DMA的缺點)而雙緩沖模式下,我們DMA的目標數據儲存區域有兩個,也就是雙緩沖,當一次完整的數據傳輸結束后(即Counter值從初始值變為0),會自動指向另一個內存區域。
那么雙緩沖區是怎么用的呢?
在網上搜到一篇帖子,給出了兩種方法,原文鏈接如下
STM32的DMA雙緩沖模式詳解_zhang1079528541的博客-CSDN博客_dma雙緩沖模式
兩種方法的區別在于我們設置的緩沖區的大小。
第一種方法:我們可以設置兩個18字節大小的緩沖區,也就是設置兩個大小剛剛好可以sei下一幀數據的緩沖區,因為我們設置的是循環模式,因此當數據傳輸量為0時,DMA會自動去換到另一個緩沖區中并且將DMA的傳輸值給自動填充滿.參考手冊中是這么說的
用這種方法的時候我們就不需要手動轉換當前緩沖區了,我們只要關注我們收的是不是18字節,然后解析即可
第二種方法:我們將每一個緩沖區的大小改為比一幀數據長度大的值(比18大),這樣可以在一幀數據傳輸完成后不會因Counter值變0導致DMA指向下一內存區域。DMA傳輸值不會自動填滿,且內存區域還是指向當前緩沖區,然后我們將剩余數據量保存下來,再將DMA傳輸值填滿,接著把DMA指向另一個緩沖區,最后通過判斷剩余數據量來決定是否對數據進行處理。
用這種方法更加地保險,我們可以很安全、“悠閑”地獲取到這一幀數據。我們這部分給出的代碼就是用的第二種方法
其中
if ((hdma_usart1_rx.Instance->CR & DMA_SxCR_CT) == RESET)
{
? …
? //設定緩沖區1
? hdma_usart1_rx.Instance->CR |= DMA_SxCR_CT;
}
else
{
? …
? //設定緩沖區0
? DMA2_Stream2->CR &= ~(DMA_SxCR_CT);
}
進行的操作就是根據DMA CR寄存器的CT位的值來判斷當前的緩沖區是誰,然后在處理過數據之后再將CT值設置為另一個緩沖區,讓數據網另一個緩沖區中存。
數據內容解析
最后重頭戲來了,將收到的數據的內容給解析出來
sbus_to_rc(sbus_rx_buf[0], &rc_ctrl);
我們來看看這個sbus_to_rc函數是怎么解析數據的
void sbus_to_rc(volatile const uint8_t *sbus_buf, RC_ctrl_t *rc_ctrl) {if (sbus_buf == NULL || rc_ctrl == NULL){return;}rc_ctrl->rc.ch[0] = (sbus_buf[0] | (sbus_buf[1] << 8)) & 0x07ff; //!< Channel 0rc_ctrl->rc.ch[1] = ((sbus_buf[1] >> 3) | (sbus_buf[2] << 5)) & 0x07ff; //!< Channel 1rc_ctrl->rc.ch[2] = ((sbus_buf[2] >> 6) | (sbus_buf[3] << 2) | //!< Channel 2(sbus_buf[4] << 10)) &0x07ff;rc_ctrl->rc.ch[3] = ((sbus_buf[4] >> 1) | (sbus_buf[5] << 7)) & 0x07ff; //!< Channel 3rc_ctrl->rc.s[0] = ((sbus_buf[5] >> 4) & 0x0003); //!< Switch leftrc_ctrl->rc.s[1] = ((sbus_buf[5] >> 4) & 0x000C) >> 2; //!< Switch rightrc_ctrl->mouse.x = sbus_buf[6] | (sbus_buf[7] << 8); //!< Mouse X axisrc_ctrl->mouse.y = sbus_buf[8] | (sbus_buf[9] << 8); //!< Mouse Y axisrc_ctrl->mouse.z = sbus_buf[10] | (sbus_buf[11] << 8); //!< Mouse Z axisrc_ctrl->mouse.press_l = sbus_buf[12]; //!< Mouse Left Is Press ?rc_ctrl->mouse.press_r = sbus_buf[13]; //!< Mouse Right Is Press ?rc_ctrl->key.v = sbus_buf[14] | (sbus_buf[15] << 8); //!< KeyBoard valuerc_ctrl->rc.ch[4] = sbus_buf[16] | (sbus_buf[17] << 8); //NULLrc_ctrl->rc.ch[0] -= RC_CH_VALUE_OFFSET;rc_ctrl->rc.ch[1] -= RC_CH_VALUE_OFFSET;rc_ctrl->rc.ch[2] -= RC_CH_VALUE_OFFSET;rc_ctrl->rc.ch[3] -= RC_CH_VALUE_OFFSET;rc_ctrl->rc.ch[4] -= RC_CH_VALUE_OFFSET; }這個函數有兩個參數,其中sbus_buf就是我們要解析的緩沖區,也就是我們在當初初始化的時候設置好的兩個緩沖區數組中的其中一個。
rc_ctrl是一個我們自己定義好的結構體對象。 RC_ctrl_t結構體是如是定義的
typedef struct {struct{int16_t ch[4];char s[2];} __attribute__((__packed__)) rc;struct{int16_t x;int16_t y;int16_t z;uint8_t press_l;uint8_t press_r;} __attribute__((__packed__))mouse;struct{uint16_t v;} __attribute__((__packed__))key;} __attribute__((__packed__))RC_ctrl_t;結構體中包含了4個通道(ch)用來存放上下左右撥桿的數據,和兩個s數組用來存放左右上方的撥桿的數據
遙控器數據處理函數 sbus_to_rc 的功能是將通過 DMA 獲取到的原始數據,按照遙控器的
數據協議拼接成完整的遙控器數據,以通道 0 的數據為例,從遙控器的用戶手冊中查到通道
0 的長度為 11bit,偏移為 0。
這說明如果想要獲取通道 0 的數據就需要將第一幀的 8bit 數據和第二幀數據的后三 bit 數據
拼接,如果想要獲取通道 1 的數據就將第二幀數據的前 5bit 和第三幀數據的后 6bit 數據進
行拼接,不斷通過拼接就可以獲得所有數據幀,拼接過程的示意圖如下:
解碼函數 sbus_to_rc 通過位運算的方式完成上述的數據拼接工作,十六進制數 0x07ff 的二
進制是 0b0000 0111 1111 1111,也就是 11 位的 1,和 0x07ff 進行與運算相當于截取出 11
位的數據。
通道 0 的數據獲取:首先將數據幀 1 和左移 8 位的數據幀 2 進行或運算,拼接出 16 位的數
據,前 8 位為數據幀 2,后 8 位為數據幀 1,再將其和 0x07ff 相與,截取 11 位,就獲得了
由數據幀 2 后 3 位和數據幀 1 拼接成的通道 0 數據。其過程示意圖如下:
我們看看代碼是如何實現的
void sbus_to_rc(volatile const uint8_t *sbus_buf, RC_ctrl_t *rc_ctrl) {if (sbus_buf == NULL || rc_ctrl == NULL){return;}rc_ctrl->rc.ch[0] = (sbus_buf[0] | (sbus_buf[1] << 8)) & 0x07ff; //!< Channel 0rc_ctrl->rc.ch[1] = ((sbus_buf[1] >> 3) | (sbus_buf[2] << 5)) & 0x07ff; //!< Channel 1rc_ctrl->rc.ch[2] = ((sbus_buf[2] >> 6) | (sbus_buf[3] << 2) | //!< Channel 2(sbus_buf[4] << 10)) &0x07ff;rc_ctrl->rc.ch[3] = ((sbus_buf[4] >> 1) | (sbus_buf[5] << 7)) & 0x07ff; //!< Channel 3rc_ctrl->rc.s[0] = ((sbus_buf[5] >> 4) & 0x0003); //!< Switch leftrc_ctrl->rc.s[1] = ((sbus_buf[5] >> 4) & 0x000C) >> 2; //!< Switch rightrc_ctrl->mouse.x = sbus_buf[6] | (sbus_buf[7] << 8); //!< Mouse X axisrc_ctrl->mouse.y = sbus_buf[8] | (sbus_buf[9] << 8); //!< Mouse Y axisrc_ctrl->mouse.z = sbus_buf[10] | (sbus_buf[11] << 8); //!< Mouse Z axisrc_ctrl->mouse.press_l = sbus_buf[12]; //!< Mouse Left Is Press ?rc_ctrl->mouse.press_r = sbus_buf[13]; //!< Mouse Right Is Press ?rc_ctrl->key.v = sbus_buf[14] | (sbus_buf[15] << 8); //!< KeyBoard valuerc_ctrl->rc.ch[4] = sbus_buf[16] | (sbus_buf[17] << 8); //NULLrc_ctrl->rc.ch[0] -= RC_CH_VALUE_OFFSET;rc_ctrl->rc.ch[1] -= RC_CH_VALUE_OFFSET;rc_ctrl->rc.ch[2] -= RC_CH_VALUE_OFFSET;rc_ctrl->rc.ch[3] -= RC_CH_VALUE_OFFSET;rc_ctrl->rc.ch[4] -= RC_CH_VALUE_OFFSET; }可以看到這里面主要就是一些位的移動與組合,然后&07ff來取出11位賦值給對應的通道,這個通道的大小是16bit,所以是足夠放的。但是有一個問題,在第8行,
sbus_buf[1] << 8
這個sbus_buf[1]的大小是8個bit,我們學c語言的時候說了,左移以后右邊補0,左邊的數移出去以后就沒了。那這里sbus_buf[1]一共有8位,在左移8位那不一定是0么。
懷著這個疑惑,打開了clion進行debug,看看他到底是個啥值。結果如下:
我們可以看到,雖然這個sbus_buf[1]規定的是一個8bit的大小,但是我們可以通過左移符號給它硬生生地把大小擴展成最大32bit而且數據不丟失。這個問題的根本在于:stm32的寄存器是32bit的
3. 總結
通過寄存器來分析HAL庫是一件很枯燥的事情,但是這里面還是藏著許多的欣喜。當你把每一個你經常用起來習以為常的函數點開,點到最深層的寄存器層面的時候,你會發現之前你并不了解他,當然你還會發現,你再用它的時候會有更多的勇氣更加地信手拈來。
在網上有人是這樣說的:高手編程都是初始化借用HAL的函數,其它的直接操作寄存器。 但是可不要認為這樣很裝x,學長是這樣說的:HAL庫中一進去就是對外設狀態的維護,如果說不用HAL庫,那么這個狀態指不定在哪就斷了沒人維護了,狀態也就亂了。所以說能用HAL庫就用HAL庫,那為什么有時候需要用寄存器呢,因為有的場景HAL庫沒有幫你考慮到。我表示很認同。
總結
以上是生活随笔為你收集整理的RM遥控器接收程序的分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [css] 使用css画出一个五角星
- 下一篇: 工作259:uni--页面--验证码添加