日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

RM遥控器接收程序的分析

發(fā)布時(shí)間:2023/12/9 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 RM遥控器接收程序的分析 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

由遙控器接收分析串口與DMA

RM的遙控器在使用的過程中在大體上可以分成兩個(gè)部分:“信息的接收”與“信息的解析”,在信息的接收中主要用到了串口的空閑中斷和DMA雙緩沖區(qū)接收在本篇的信息接收部分主要根據(jù)RM官方給出的代碼來研究一下串口的空閑中斷與DMA雙緩沖區(qū)如何配合使用,在信息解析的時(shí)候主要來研究一下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//內(nèi)存緩沖區(qū)1hdma_usart1_rx.Instance->M0AR = (uint32_t)(rx1_buf);//memory buffer 2//內(nèi)存緩沖區(qū)2hdma_usart1_rx.Instance->M1AR = (uint32_t)(rx2_buf);//data length//數(shù)據(jù)長度hdma_usart1_rx.Instance->NDTR = dma_buf_num;//enable double memory buffer//使能雙緩沖區(qū)SET_BIT(hdma_usart1_rx.Instance->CR, DMA_SxCR_DBM);//enable DMA//使能DMA__HAL_DMA_ENABLE(&hdma_usart1_rx);}

從上面的代碼中我們也可以看出其中有很多直接對寄存器進(jìn)行的操作,但是這些寄存器完成的功能看起來好像跟一些函數(shù)完成的相同的工作,這也給了我們一個(gè)深入研究HAL庫或者說是stm32的方向。那么之后我們的分析方法就是將例程中的寄存器操作對照著參考手冊搞懂他們是在干嘛的,并且那這些東西跟功能類似的HAL庫提供的接口函數(shù)進(jìn)行一個(gè)對照與比較。

注意:參考手冊一定要找對,是F4參考手冊,可不是F1的,否則會讓你非常地困惑,我就是拿著F1的參考手冊對著找了兩三個(gè)小時(shí),發(fā)現(xiàn)很多地方都對不上,時(shí)間就白白浪費(fèi)掉了。

既然是想要以寄存器為切入點(diǎn),那么我們就以用到的寄存器來做分段

USART_CR3

SET_BIT

程序一開始就來了一個(gè)SET_BIT來使能DMA串口接收。我們點(diǎn)到它的定義處可以發(fā)現(xiàn),SET_BIT實(shí)際上是一個(gè)宏定義,而這個(gè)宏定義的作用就是將BIT賦值給寄存器REG。 而我們開啟DMA串口接收則是將USART_CR3_DMAR賦值給串口1的CR3寄存器,當(dāng)然,這里說到的賦值是“或等于”,這樣可以不影響該寄存器的其他標(biāo)志位。

#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同樣是一個(gè)宏定義,它的值是0x00000040,也就是在CR3寄存器的第7位上賦值了1.我們可以查一下參考手冊,看看這一位是什么作用。


注意,我們說的第七位是從1 開始的“第七個(gè)空”,而在手冊上的位7是從0開始的第七位,因此我們要找的是位6,可以看到該位是DMA使能接收器

USART_CR1

__HAL_UART_ENABLE_IT

在使能了DMA串口接收后,打開了串口的空閑中斷

__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

__HAL_UART_ENABLE_IT又是一個(gè)宏定義,它里面干的活兒和上面的SET_BIT基本上是一樣的,都是將宏定義好了的寄存器數(shù)值填入對應(yīng)的寄存器中

#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是一個(gè)宏定義

#define UART_IT_IDLE ((uint32_t)(UART_CR1_REG_INDEX << 28U | USART_CR1_IDLEIE))

它是UART_CR1_REG_INDEX左移了28位后或上了一個(gè)USART_CR1_IDLEIE,關(guān)鍵點(diǎn)在于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

可以看到實(shí)際上UART_IT_IDLE就是將CR1的第五個(gè)空填上了1,也就是位4置了1. 第四位是啥?我們看手冊

可以看到,是IDLE中斷使能,也就是空閑中斷使能

DMA_SXCR

__HAL_DMA_DISABLE

之后我們關(guān)閉了DMA,關(guān)閉DMA?為什么? 這也是我看到的時(shí)候的第一反應(yīng),別急,我們順著我們的這個(gè)方法往下走,當(dāng)答案浮現(xiàn)的時(shí)候,你會感到欣喜的。

__HAL_DMA_DISABLE,這又是一個(gè)宏定義,而且還是一對兒,他之前還有一個(gè)__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)

可以看到,這個(gè)宏定義又是一個(gè)直接的寄存器賦值操作,賦的值都是DMA_SxCR_EN,只不顧另一個(gè)取反了。我們看一下DMA_SxCR_EN是個(gè)啥,然后去找它對應(yīng)的寄存器位。

#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它是想要置寄存器的第一個(gè)空,也就是位0

看到了么,該位是數(shù)據(jù)流使能,也就是這一位置1,DMA才能真正發(fā)揮作用,因?yàn)镈MA就是傳輸數(shù)據(jù)流的嘛。

在關(guān)閉DMA后緊跟著一個(gè)While循環(huán)

while(hdma_usart1_rx.Instance->CR & DMA_SxCR_EN)
{
__HAL_DMA_DISABLE(&hdma_usart1_rx);
}

我們可以試著看一下這個(gè)循環(huán)到底在干嘛,用人話講出來就是:如果CR寄存器與上DMA_SxCR_EN為1(也就是如果SXCR寄存器的第一位只要還是1),就再給我關(guān)掉DMA.意思是非得給人家關(guān)了不行?

這一步是干啥的,我們還不太清楚,沒事兒繼續(xù)往下走。但是在這里我們先做一個(gè)知識的補(bǔ)充:

知識補(bǔ)充:

我們知道HAL庫是將每一個(gè)外設(shè)都給封裝成了一個(gè)句柄,具體來說就是一個(gè)“外設(shè)_HandleTypeDef” 實(shí)例化了的一個(gè)對象。例如,我們操作的串口1,就是在操作UART_HandleTypeDef示例化了的huart1。

這個(gè)huart1,里面包含了很多的東西。例如這個(gè)外設(shè)現(xiàn)在的狀態(tài),以及很多的配置項(xiàng)。其中有一項(xiàng)非常重要,可以說我們對這個(gè)外設(shè)的大部分操作都是在修改這個(gè)東西里面的值。這個(gè)很重要的“東西”就是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是一個(gè)USART_TypeDef類型的結(jié)構(gòu)體,里面裝的是 與串口相關(guān)的寄存器,,準(zhǔn)確得說,結(jié)構(gòu)體里面裝的是串口相關(guān)的寄存器的值。而這里面的寄存器名字和參考手冊上的名字是一一對應(yīng)的,因此我們可以通過這里來判斷我們?nèi)⒖际謨缘哪睦镎摇?/p>

有的時(shí)候我們需要知道某個(gè)外設(shè)的某個(gè)寄存器的地址,例如在DMA從外設(shè)到內(nèi)存?zhèn)鬏數(shù)臅r(shí)候,我們需要知道外設(shè)的對應(yīng)存放數(shù)據(jù)的那個(gè)寄存器的具體地址,例如在串口DMA中我們就要知道串口的DR寄存器(因?yàn)榇诮邮盏臄?shù)據(jù)是存到這個(gè)寄存器里的)的地址,好讓DMA知道從哪拿數(shù)據(jù)。 如果要那某個(gè)外設(shè)的寄存器的地址,我們就要用“外設(shè)名->寄存器名” 例如:

USART1->DR DMA1->HIFCR CAN2->BTR ...

DMA_SxPAR

在有了上面的那個(gè)補(bǔ)充的知識后我們可以更加得心應(yīng)手地去查看手冊并且可以很自信地明白下面這些操作是在干啥。

在我們退出while循環(huán)之后,我們把USART1的DR寄存器地址賦值到了DMA的PAR寄存器中,我們來看一下PAR寄存器是有什么本事

這個(gè)寄存器是存放讀/寫數(shù)據(jù)的外設(shè)數(shù)據(jù)寄存器的地址的。 所謂PAR就是peripheral address register外設(shè)地址寄存器。

它的作用就是DMA在用外設(shè)到存儲器模式的時(shí)候,高速DMA外設(shè)是在哪,該去哪拿數(shù)據(jù)。

在看這個(gè)寄存器的時(shí)候注意最下面的一句話“這些位收到寫保護(hù),只有DMA_SxCR寄存器中的EN為0時(shí)才可以寫入”,你可能會聯(lián)想到些什么,可能還沒有完全醒悟,我們可以接著往下看,到時(shí)候會給你揭曉。

DMA_SxM0/1AR

在將外設(shè)地址寫入到DMA的PAR寄存器中之后,我們又緊接著進(jìn)行了兩此賦值操作,將兩個(gè)數(shù)組的地址賦值給了DMA的M0AR和M1AR寄存器中。

這兩個(gè)寄存器是用來存放存儲器的地址的,作用就是告訴DMA數(shù)據(jù)拿到了以后拿去哪、放到哪。

他們和PAR寄存器一樣,都有這樣一句話這些位收到寫保護(hù)

DMA_SxNDTR

之后我們在DMA的NDTR寄存器中寫入了一個(gè)16位的數(shù),這個(gè)數(shù)是DMA傳輸?shù)拇笮?#xff0c;

這個(gè)寄存器只有16位可用,最大值是65535。它的作用就是告訴DMA傳多少個(gè) 數(shù)據(jù)以后結(jié)束(當(dāng)然如果開了循環(huán)模式的話不會停,會進(jìn)入DMA傳輸完成中斷,然后再重新裝填該寄存器,然后繼續(xù)傳輸)。因?yàn)槊看蜠MA傳輸后此寄存器將遞減,該寄存器還有計(jì)數(shù)的作用,這一點(diǎn)我們后面在信息解析的時(shí)候會提到。

這里面同樣有一句話需要注意:“只有在禁止數(shù)據(jù)流時(shí),才能向此寄存器執(zhí)行寫操作

DMA_SxCR

在我們通過填寫寄存器的值告訴了DMA,明確了從哪拿拿哪去拿多少的問題后我們使能了雙緩沖區(qū),也就是告訴DMA:我可是給你開了兩個(gè)緩沖區(qū)的,目的地有兩個(gè),別忘了“雨露均沾”,為什么要用雨露均沾?因?yàn)槭鼓芰穗p緩沖區(qū)并且開啟了循環(huán)模式之后,在一個(gè)緩沖區(qū)填滿后,DMA會自動地去把數(shù)據(jù)sei到另一個(gè)緩沖區(qū)中,這可不就是雨露均沾么

#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

這里又出現(xiàn)了那句話 “此位受到保護(hù)”

最后我們又通過調(diào)用__HAL_DMA_DISABLE的對象 __HAL_DMA_ENABLE打開了DMA。

流程總結(jié):

  • 通過向usart的CR3寄存器的第7位置1,開啟串口外設(shè)的DMA
  • 調(diào)用__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); 使能串口的空閑中斷
  • 調(diào)用__HAL_DMA_DISABLE(&hdma_usart1_rx); 失能DMA,來保證之后的數(shù)據(jù)流傳輸?shù)刂纺軌虮豁樌麑懭搿?/li>
  • 向DMA的PAR寄存器中寫入U(xiǎn)SART的數(shù)據(jù)寄存器的地址即源地址
  • 分別向DMA的寄存器SxM0AR和SxM1AR中寫入兩個(gè)存儲區(qū)的地址
  • 向DMA寄存器SxCR的第19位DBM,注意不是位19 ,位19說的是從0開始數(shù)。第19位是從1開始數(shù))置1,開啟雙緩沖區(qū)模式
  • 向DMA的NDTR寄存器中寫入數(shù)據(jù)流長度,因?yàn)槭请p緩沖區(qū),因此接了一幀數(shù)據(jù)(18個(gè)字節(jié))的雙倍,也就是36個(gè)字節(jié)
  • 調(diào)用__HAL_DMA_ENABLE(&hdma_usart1_rx);開啟DMA
  • 解開疑惑

    相信在這之前,大家心中的疑惑便已經(jīng)解開了。疑惑是什么?疑惑就是下面這段代碼的意義。

    __HAL_DMA_DISABLE(&hdma_usart1_rx); while(hdma_usart1_rx.Instance->CR & DMA_SxCR_EN) {__HAL_DMA_DISABLE(&hdma_usart1_rx); }

    第一個(gè)問題,為什么要把DMA給關(guān)掉?我給大家的暗示已經(jīng)夠多了。

    因?yàn)榻酉聛砦覀円獙MA的相關(guān)寄存器進(jìn)行配置,,明確從哪拿拿哪去拿多少。為了達(dá)到這個(gè)目的,我們需要向?qū)?yīng)的寄存器寫入數(shù)據(jù)、配置,而這些寄存器都有寫保護(hù),也就是說“當(dāng)DMA已經(jīng)被開啟,DMA的SXCR寄存器的第一位被置1時(shí),這些配置寄存器是無法進(jìn)行寫入的”因此我們需要將DMA給關(guān)掉,才能把配置寫入,讓DMA按照我們想要的方式運(yùn)行。

    第二個(gè)問題,后面這個(gè)while循環(huán)是在干嘛?

    因?yàn)閰⒖际謨灾杏羞@樣一段話:

    警告: 要關(guān)閉連接到 DMA 數(shù)據(jù)流請求的外設(shè),必須首先關(guān)閉外設(shè)連接 DMA 數(shù)據(jù)流,然后等待 EN = 0**。只有這樣才能安全地禁止外設(shè) 這就是為什么要加那個(gè)while等待的原因==

    如果使能了數(shù)據(jù)流,通過重置 DMA_SxCR 寄存器中的 EN 位將其禁止,然后讀取此位

    以確認(rèn)沒有正在進(jìn)行的數(shù)據(jù)流操作。將此位寫為 0 不會立即生效,因?yàn)閷?shí)際上只有所有

    當(dāng)前傳輸都已完成時(shí)才會將其寫為 0。當(dāng)所讀取 EN 位的值為 0 時(shí),才表示可以配置數(shù)

    據(jù)流。因此在開始任何數(shù)據(jù)流配置之前,需要等待 EN 位置 0。應(yīng)將先前的數(shù)據(jù)塊 DMA

    傳輸中在狀態(tài)寄存器(DMA_LISR 和 DMA_HISR)中置 1 的所有數(shù)據(jù)流專用的位置 0,

    然后才可重新使能數(shù)據(jù)流。

    也就是說:參考手冊可以解決掉我們大部分的問題,但是關(guān)鍵是我們要找到它到底寫在哪。反正這個(gè)警告是我無意間翻到的…

    1.2 DMA與雙緩沖區(qū)的開啟(API對比)

    通過1.1的講解,相信大家對寄存器如何配置串口DMA有了比較詳細(xì)的認(rèn)識,但是畢竟相比較于調(diào)用HAL給我們的函數(shù),我們還是很少會用到寄存器直接編程的,那么他們的區(qū)別到底在哪里?搞懂這些區(qū)別與聯(lián)系,相信會對HAL庫編程有一個(gè)更詳細(xì)的理解。下面我們開始對比吧

    庫函數(shù)開啟雙緩沖區(qū)

    那在網(wǎng)上看到的配置過程:

    HAL_DMAEx_MultiBufferStart() 用了這個(gè)函數(shù)來配置雙緩沖區(qū)

    HAL_StatusTypeDef HAL_DMAEx_MultiBufferStart(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t SecondMemAddress, uint32_t DataLength)SrcAddress:源內(nèi)存緩沖區(qū)地址; DstAddress:目標(biāo)內(nèi)存緩沖區(qū)地址; SecondMemAddress:第二個(gè)內(nèi)存緩沖區(qū)地址; DataLength:從源傳輸?shù)侥繕?biāo)的數(shù)據(jù)長度;

    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; }

    這個(gè)開啟雙緩存區(qū)的函數(shù)干了哪些工作:

  • 首先進(jìn)來先判斷了傳遞的參數(shù)的正確性和目前DMA的模式的正確與否。
  • assert_param(IS_DMA_BUFFER_SIZE(DataLength));IS_DMA_BUFFER_SIZE(SIZE) (((SIZE) >= 0x01U) && ((SIZE) < 0x10000U))

    這個(gè)函數(shù)是在檢驗(yàn)設(shè)置的數(shù)據(jù)長度是否合規(guī)長度要大于1小于10000

    并且判斷如果是從內(nèi)存到內(nèi)存模式的話是不允許循環(huán)模式的,也就不能夠開啟雙緩存區(qū)。

  • 如果滿足條件的話就先鎖上dma。

    __HAL_LOCK(hdma);

    這個(gè)鎖是Process Locked,起到的作用類似于上廁所的時(shí)候廁所門的那個(gè)“有人”標(biāo)志,如果上了操作系統(tǒng),多個(gè)進(jìn)程運(yùn)行,那么就要避免同時(shí)去操作dma的情況,尤其是同時(shí)用dma去寫入東西,因?yàn)槟菢泳筒恢罃?shù)據(jù)到底是誰寫的了。因此有進(jìn)程在用dma的時(shí)候就先把DMA給占住,說:我在用它了 。

  • 在DMA外設(shè)的CR寄存器中賦值

  • /* Enable the double buffer mode */ hdma->Instance->CR |= (uint32_t)DMA_SxCR_DBM;

    這個(gè)DMA_SxCR_DBM的值就是0x00040000

  • 將第二個(gè)內(nèi)存緩沖區(qū)地址寫入DMA的M1AR寄存器中

    /* Configure DMA Stream destination address */hdma->Instance->M1AR = SecondMemAddress;
  • 調(diào)用這個(gè)函數(shù)來配置dma的source、destnation address和數(shù)據(jù)長度

    DMA_MultiBufferSetConfig(hdma, SrcAddress, DstAddress, DataLength);

    這個(gè)函數(shù)里的內(nèi)容和直接用寄存器操作相同,也是在NDTR寄存器中寫入數(shù)據(jù)長度,在PAR寄存器和M0AR寄存器中分別寫入源地址和目標(biāo)地址。

  • 開啟DMA。

  • 這樣分析下來,我們可以發(fā)現(xiàn),不論是調(diào)用函數(shù)還是直接使用寄存器賦值,流程上幾乎是一樣的,都是都是先關(guān)閉DMA,然后給各種相關(guān)寄存器進(jìn)行賦值,最后開啟DMA。

    庫函數(shù)開啟串口DMA

    同樣,我們在網(wǎng)上看到的的一些教程是如何教我們開啟DMA的呢? 大多都是讓調(diào)用下面這個(gè)函數(shù)

    HAL_UART_Receive_DMA

    那么用SET_BIT寫入usart的CR3寄存器開啟DMA接收和直接用HAL_UART_Receive_DMA函數(shù)開啟DMA接收有什么區(qū)別?

    我認(rèn)為:這兩種形式都可以起到開啟DMA的作用,但是用SET_BIT直接操作寄存器賦值更單純,僅僅開啟了DMA的接收,而HAL_UART_Receive_DMA在開啟DMA傳輸?shù)臅r(shí)候會打開DMA傳輸完成中斷。

    我們可以來看一下HAL_UART_Receive_DMA這個(gè)函數(shù)內(nèi)部到底在干嘛

    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;} }

    我們可以發(fā)現(xiàn),在這個(gè)函數(shù)中很大一部分的內(nèi)容都是在維護(hù)USART的狀態(tài),以保證這個(gè)外設(shè)不會被我們用著用著給搞得爛七八糟的。

    其中真正起到“實(shí)質(zhì)性作用”是我上面打了星星的那一行,HAL庫主要調(diào)用UART_Start_Receive_DMA這個(gè)函數(shù)來開啟USART的DMA。

    我們再看看這個(gè)函數(shù)里是在干嘛

    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; }

    其中開局又是一套狀態(tài)的維護(hù),緊接著設(shè)置了一系列的回調(diào)函數(shù)。然后我們往下找找找,找到到了熟悉的字眼“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傳輸,和我們的例程中的操作可謂是一模一樣。

    但是,在我們從上往下找的時(shí)候,發(fā)現(xiàn)了標(biāo)注五角星的那條語句

    HAL_DMA_Start_IT(huart->hdmarx, (uint32_t)&huart->Instance->DR, *(uint32_t *)tmp, Size);

    Start_IT!!!,誰讓這家伙給我開啟DMA中斷的,在百度中沒有人跟我說過還有個(gè)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; }

    首先上來又先是一套狀態(tài)維護(hù)服務(wù)安排上。然后通過DMA_SetConfig函數(shù),將source, destination address and the data length寫入寄存器,直到個(gè)函數(shù)里面,才真真正正的將HAL_UART_Receive_DMA函數(shù)中的參數(shù)寫入相對應(yīng)的寄存器中,可見HAL的封裝真是一層一層的呀,層數(shù)真不少!

    我們最重要要看的是上面我標(biāo)注五角星的語句。

    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同時(shí)賦值了3個(gè)標(biāo)志位:TCIE、TEIE、DMEIE。然后我們翻看F4的參考手冊,看看他們都是干啥的。

    可以看到,都是中斷使能。這個(gè)小兔崽子,給打開了一堆的中斷。而且還在之后打開了半傳輸完成中斷

    if(hdma->XferHalfCpltCallback != NULL)
    {
    hdma->Instance->CR |= DMA_IT_HT;
    }

    這里面的hdma->XferHalfCpltCallback 是不是有點(diǎn)眼熟?沒錯(cuò),就是在UART_Start_Receive_DMA函數(shù)中寫入了UART_DMARxHalfCplt的。所以這個(gè)if語句中的指令是會被執(zhí)行的。也就是說,我們的半傳輸完成中斷會被開啟。

    把一切都設(shè)置好了以后,開啟了DMA外設(shè)

    __HAL_DMA_ENABLE(hdma);

    總結(jié)起來,這個(gè)HAL_UART_Receive_DMA函數(shù)干了這些事:

  • 把串口到內(nèi)存的DMA傳輸給打開了
  • 打開了DMA的傳輸完成中斷,和DMA數(shù)據(jù)“半傳輸完成中斷”。這個(gè)半就是我們填入的size參數(shù)的一半。
  • 等等,有點(diǎn)蒙了。再回一下例程中的操作:

    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

    我們不是當(dāng)初是用這個(gè)函數(shù)打開中斷的么,而且還清清楚楚地知道是打開了空閑中斷,而且之后的數(shù)據(jù)解析操作都是在這個(gè)串口的空閑中斷里進(jìn)行了呀。咋又給打開了個(gè)“完成中斷”和“半傳輸完成中斷”這是要干嘛,我到底該在哪個(gè)中斷里進(jìn)行操作?

    當(dāng)然還是在空閑中斷里進(jìn)行操作啦。要注意分清,串口中斷是串口上的,DMA中斷時(shí)DMA上的,這兩個(gè)人是沒有關(guān)系的。要在觀念中去把這兩個(gè)東西給分離開來。

    串口的空閑中斷是在串口接收數(shù)據(jù)的時(shí)候會根據(jù)接收的數(shù)據(jù)幀的“結(jié)束標(biāo)識”后面跟不跟“起始標(biāo)識”而選擇進(jìn)入的,如果說我們發(fā)的數(shù)據(jù)幀在一段時(shí)間內(nèi)(這個(gè)時(shí)間是很短的,但是很精準(zhǔn)的,不用擔(dān)心)結(jié)束標(biāo)識后面沒有再跟著起始標(biāo)識,那么就判斷這一次的數(shù)據(jù)發(fā)送完畢了,進(jìn)入空閑中斷。而DMA的中斷,是在DMA外設(shè)中設(shè)置的,DMA接收到從UART外設(shè)來的數(shù)據(jù)后進(jìn)行傳輸,傳輸?shù)皆瓉碓O(shè)定的值的一半的時(shí)候會進(jìn)一次半傳輸完成中斷,傳輸完之后會進(jìn)一次傳輸完成中斷。

    我們大概也可以理解為什么我們在cubemx里選擇對應(yīng)外設(shè)的DMA的時(shí)候cubemx會自動給我們把DMA的中斷給我們打開的原因了。

    在上面的程序中調(diào)用了DMA_SetConfig(hdma, SrcAddress, DstAddress, DataLength);這個(gè)函數(shù)才是真真正正地在設(shè)置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)//接收到數(shù)據(jù){__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//獲取接收數(shù)據(jù)長度,長度 = 設(shè)定長度 - 剩余長度this_time_rx_len = SBUS_RX_BUF_NUM - hdma_usart1_rx.Instance->NDTR;//reset set_data_lenght//重新設(shè)定數(shù)據(jù)長度hdma_usart1_rx.Instance->NDTR = SBUS_RX_BUF_NUM;//set memory buffer 1//設(shè)定緩沖區(qū)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//獲取接收數(shù)據(jù)長度,長度 = 設(shè)定長度 - 剩余長度this_time_rx_len = SBUS_RX_BUF_NUM - hdma_usart1_rx.Instance->NDTR;//reset set_data_lenght//重新設(shè)定數(shù)據(jù)長度hdma_usart1_rx.Instance->NDTR = SBUS_RX_BUF_NUM;//set memory buffer 0//設(shè)定緩沖區(qū)0DMA2_Stream2->CR &= ~(DMA_SxCR_CT);//enable DMA//使能DMA__HAL_DMA_ENABLE(&hdma_usart1_rx);if(this_time_rx_len == RC_FRAME_LENGTH){//處理遙控器數(shù)據(jù)sbus_to_rc(sbus_rx_buf[1], &rc_ctrl);}}} }

    RXNE與RXNEIE

    ? 在分析這個(gè)例程的代碼時(shí)上來就讓我迷惑了,咋上來就是一個(gè)RXNE標(biāo)志??咋沒見過這個(gè)玩意兒。去查看寄存器

    當(dāng)串口收到數(shù)據(jù)之后該位會被置1,并且如果RXNEIE這個(gè)時(shí)候也被置1時(shí),就會進(jìn)入中斷。于是便產(chǎn)生了一個(gè)疑問:一有數(shù)據(jù)進(jìn)來就會被置位?還是等數(shù)據(jù)足夠了再置位?經(jīng)過百度,知道了,每接收一個(gè)字節(jié)就會被置位,也就是說如果這個(gè)時(shí)候RXNEIE如果被使能,那么串口將每接收一個(gè)字節(jié)的數(shù)據(jù)就會進(jìn)一次中斷。是不是有點(diǎn)熟悉?HAL_UART_Recesive_IT這家伙不就是干這個(gè)事的么?找一下

    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函數(shù)實(shí)際上就是調(diào)用了__HAL_UART_ENABLE_IT,然后使能了RXNE中斷,那這個(gè)UART_IT_RXNE實(shí)際上是個(gè)啥?

    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這個(gè)函數(shù)本質(zhì)上就是在RXNEIE位置了1,使能了接收完成中斷

    回到上面的程序的分析

    if(huart1.Instance->SR & UART_FLAG_RXNE)//接收到數(shù)據(jù){__HAL_UART_CLEAR_PEFLAG(&huart1);}

    如果進(jìn)入中斷,并且讀取數(shù)據(jù)寄存器非空,那么就清除PE這個(gè)標(biāo)志。PE是個(gè)什么標(biāo)志?

    奇偶校驗(yàn)錯(cuò)誤??? 也就是說先判斷一手RXNE是為了清除奇偶校驗(yàn)錯(cuò)誤標(biāo)志位,主體還是為了避免在傳輸?shù)臅r(shí)候出現(xiàn)信號干擾,出現(xiàn)校驗(yàn)錯(cuò)誤時(shí)如果不及時(shí)清除校驗(yàn)錯(cuò)誤標(biāo)志位,那么會一直進(jìn)中斷,而進(jìn)不去主程序。 但是這里有一個(gè)前提昂,就是PEIE要被使能才能因?yàn)樵撳e(cuò)誤進(jìn)入中斷。

    在信息接收那一節(jié)中的RC_Init函數(shù)中,是直接操作寄存器開啟IDLE中斷的,因此不管有沒有校驗(yàn)錯(cuò)誤,都不會以內(nèi)校驗(yàn)錯(cuò)誤進(jìn)入中斷,因此這個(gè)判斷函數(shù)在直接賦值寄存器的方法開啟中斷時(shí)是不起作用的。那么為什么要加一個(gè)這樣的判斷呢?因?yàn)槲覀兇蠖鄶?shù)都不是直接賦值寄存器開啟IDLE中斷的而是調(diào)用HAL_UART_Recesive_IT、HAL_UART_Recesive_DMA來開啟中斷的**(HAL_UART_Recesive_DMA會主動調(diào)用HAL_UART_Recesive_IT這個(gè)函數(shù),給你把接收中斷打開**),這些函數(shù)因?yàn)槭荋AL給封裝的,所以很“規(guī)矩”,它會主動地在你開啟IT、DMA的時(shí)候給你把錯(cuò)誤中斷也打開。

    如果你用HAL庫函數(shù)打開中斷,就需要在IRQHandle中進(jìn)行相應(yīng)的標(biāo)志位的處理,但是HAL庫在中斷中調(diào)用下面這個(gè)函數(shù)把這些工作都給你做了

    HAL_UART_IRQHandler(&huart1);

    串口的DMA請求機(jī)制

    串口通信的時(shí)候是一個(gè)字節(jié)一個(gè)字節(jié)的傳輸?shù)?#xff0c;因?yàn)樵O(shè)置的數(shù)據(jù)位是8嘛。每一幀數(shù)據(jù)除了八個(gè)數(shù)據(jù)位還有一些校驗(yàn)位之類的,還有兩個(gè)很重要的地方,就是“起始位”和“停止位”。很多地方可以用到這兩個(gè)位來進(jìn)行一些判斷,例如,USART的空閑中斷。串口怎么知道現(xiàn)在空閑了呢?因?yàn)樗跈z測到一個(gè)停止位之后如果沒有檢測到起始位,那么就會認(rèn)為這一大串?dāng)?shù)據(jù)是一次發(fā)送的數(shù)據(jù)。還有一個(gè)地方的應(yīng)用就在DMA。

    思考一下,我們在Cubemx中設(shè)置的DMA傳輸大小是bit 、Half word 還是 Words是干嘛用的?是確定DMA轉(zhuǎn)發(fā)閾值用的,也就是說,我DMA在傳輸?shù)臅r(shí)候不是你來一個(gè)bit我就送走一個(gè)bit,跟入棧似的送到目標(biāo)寄存器。而是等存夠一定的數(shù)量的時(shí)候才會進(jìn)行一次傳輸。 那么DMA怎么知道我是不是要傳輸了呢? 外設(shè)在需要DMA傳輸?shù)臅r(shí)候會發(fā)起一個(gè)DMA請求,DMA在收到這個(gè)請求的時(shí)候就知道自己該送數(shù)據(jù)走了,于是便會進(jìn)行一次傳輸(這次傳輸?shù)拇笮∽匀痪褪侵霸O(shè)置好的bit、Half Word…)。 也就是說,DMA之所以能做到的一次傳輸傳輸固定大小的數(shù)據(jù),是因?yàn)槭褂肈MA的外設(shè)會在收到這個(gè)固定大小的數(shù)據(jù)之后對DMA發(fā)起一個(gè)請求,讓DMA幫忙把這么長的數(shù)據(jù)給送到要去的地方。(感受一下就可以發(fā)現(xiàn),我們所說的配置DMA,并不是配置DMA,而是配置外設(shè),是配置外設(shè)什么時(shí)候呼叫DMA,這也就是為什么USART中的DMA設(shè)置部分叫做**“DMA Request Settings”) 那么外設(shè)是如何做到精準(zhǔn)的每8bit、或者16bit、32bit請求一次DMA呢?答案是停止位**

    因?yàn)槊總鬏?bit的數(shù)據(jù)就會發(fā)送一個(gè)停止位,那么串口在使能了接收DMA之后.只要一接收到一個(gè)Bit就會產(chǎn)生一個(gè)**“DMA request”,DMA收到這個(gè)request之后就可以訪問外設(shè)中的數(shù)據(jù)進(jìn)行發(fā)送了。并且在訪問數(shù)據(jù)的時(shí)候會給外設(shè)一個(gè)應(yīng)答,這時(shí)候外設(shè)就知道有人來讀取了,這也就是為啥DMA可以消除RXNE標(biāo)志位了。(RXNE在接收到一字節(jié)**的數(shù)據(jù)之后會被置1,如果RXNEIE位已經(jīng)被置1,也就是打開了接收中斷后,RXNE一旦置1便進(jìn)入中斷)

    發(fā)現(xiàn)錯(cuò)誤

    再往下走,判斷USART1->SR & UART_FLAG_IDLE,如果IDLE標(biāo)志位被置1,說明遙控器已經(jīng)發(fā)完了一幀數(shù)據(jù),進(jìn)入這個(gè)中斷的原因是IDLE中斷(空閑中斷)。然后上來又是一個(gè)奇偶校驗(yàn)錯(cuò)誤位清空.

    __HAL_UART_CLEAR_PEFLAG(&huart1);

    奇偶校驗(yàn)錯(cuò)誤難道就這么頻繁?需要這么小心地去處理它么?更何況我們根本就沒使能PEIE位,所以我感覺如果是用寄存器開啟的串口+DMA接收,那么沒有必要這樣小心地去處理這個(gè)PEFLAG,但是我們看到它這么小心地在處理,可能在串口通訊的時(shí)候奇偶校驗(yàn)錯(cuò)誤很容易出現(xiàn)的,而且加上我們很多時(shí)候用HAL_UART_Recesive_DMA這個(gè)函數(shù)來開啟的,那么就很有可能被奇偶校驗(yàn)的錯(cuò)誤卡在中斷中出不來。但是根據(jù)自己的實(shí)際測試,如果是按照第一節(jié)寄存器的方法開啟串口DMA的話,去掉那兩個(gè)PEFLAG的清除也是可以使用的。

    而且這里我強(qiáng)烈懷疑是代碼寫錯(cuò)了,應(yīng)該是清除IDLE標(biāo)志位,這樣才是一個(gè)正常地有邏輯的流程

    __HAL_UART_CLEAR_IDLEFLAG(&huart1);

    傳輸數(shù)據(jù)長度計(jì)算

    回到代碼,如果是IDLE標(biāo)志位被置1,也就是我們現(xiàn)在進(jìn)入了空閑中斷,遙控器已經(jīng)發(fā)完了一幀數(shù)據(jù),我們就要首先記錄一下當(dāng)前到底收到了多少字節(jié),和我們預(yù)期的一幀數(shù)據(jù)長度是否一致,如果不一致,那么就說明這一幀數(shù)據(jù)是不準(zhǔn)確的,我們就直接廢棄這一幀數(shù)據(jù)了,這也就是后面的那個(gè)

    if(this_time_rx_len == RC_FRAME_LENGTH)

    的作用。

    而我們計(jì)算數(shù)據(jù)長度的方法就是根據(jù)DMA的NDTR寄存器的特性,

    因?yàn)槲覀冊谠O(shè)置串口的DMA Request的時(shí)候設(shè)置的是一個(gè)字節(jié)申請一次DMA傳輸,而我們進(jìn)入U(xiǎn)SART的空閑中斷是接收完一幀數(shù)據(jù)之后才進(jìn)入的,遙控器的一幀數(shù)據(jù)長度是18字節(jié)

    因此,在我們進(jìn)入空閑中斷的時(shí)候,dma已經(jīng)轉(zhuǎn)移了18次數(shù)據(jù)了,也就是說DMA的NDTR寄存器已經(jīng)有所改變了,準(zhǔn)確來說是減少了很多次了,我們可以根據(jù)NDTR中還剩下的字節(jié)數(shù)來簡介判斷出它到底傳輸了多少次,也就是說我們從上一次空閑中斷到這一次進(jìn)入中斷之間到底接收了多少個(gè)字節(jié)的數(shù)據(jù),這個(gè)數(shù)就是我們真實(shí)接收到的一幀的字節(jié)數(shù)。因此就有了下面的這個(gè)計(jì)算公式

    //獲取接收數(shù)據(jù)長度,長度 = 設(shè)定長度 - 剩余長度this_time_rx_len = SBUS_RX_BUF_NUM - hdma_usart1_rx.Instance->NDTR;

    當(dāng)然這里面還有很重要的一個(gè)問題,在我們進(jìn)入空閑中斷后如果串口又接收到數(shù)據(jù),并且傳夠了一個(gè)字節(jié)甚至多個(gè)字節(jié)的話,那么就會在我們在串口中處理數(shù)據(jù)的時(shí)候產(chǎn)生一個(gè)甚至多個(gè)DMA請求,那么我們的接收數(shù)據(jù)的計(jì)算不就不準(zhǔn)了么?這個(gè)NDTR正在看的時(shí)候還在減少,這肯定不會準(zhǔn)嘛,因此就需要在我們計(jì)算的時(shí)候,最好是一進(jìn)入空閑中斷就先把DMA給關(guān)了,不讓它再傳送數(shù)據(jù),也就保證了NDTR保持的是我們進(jìn)入空閑中斷的時(shí)刻之前剩余的長度。這就是下面這句話的作用

    //失效DMA __HAL_DMA_DISABLE(&hdma_usart1_rx);

    當(dāng)我們獲取完這次接收的數(shù)據(jù)長度以后,我們要重新給NDTR寄存器賦值,以便下次我們可以再次通過上面的公式進(jìn)行長度的計(jì)算。

    hdma_usart1_rx.Instance->NDTR = SBUS_RX_BUF_NUM;

    雙緩沖區(qū)的使用

    但是別忘了,我們是有兩個(gè)緩沖區(qū)來接收數(shù)據(jù)的。為什么要用雙緩沖區(qū)呢?我們知道,普通DMA的目標(biāo)數(shù)據(jù)儲存區(qū)域只有一個(gè),也就是如果當(dāng)數(shù)據(jù)存滿后,新的數(shù)據(jù)又傳輸過來了,那么舊的數(shù)據(jù)會被新的數(shù)據(jù)覆蓋。(這就是普通DMA的缺點(diǎn))而雙緩沖模式下,我們DMA的目標(biāo)數(shù)據(jù)儲存區(qū)域有兩個(gè),也就是雙緩沖,當(dāng)一次完整的數(shù)據(jù)傳輸結(jié)束后(即Counter值從初始值變?yōu)?),會自動指向另一個(gè)內(nèi)存區(qū)域。

    那么雙緩沖區(qū)是怎么用的呢?

    在網(wǎng)上搜到一篇帖子,給出了兩種方法,原文鏈接如下

    STM32的DMA雙緩沖模式詳解_zhang1079528541的博客-CSDN博客_dma雙緩沖模式

    兩種方法的區(qū)別在于我們設(shè)置的緩沖區(qū)的大小。

    第一種方法:我們可以設(shè)置兩個(gè)18字節(jié)大小的緩沖區(qū),也就是設(shè)置兩個(gè)大小剛剛好可以sei下一幀數(shù)據(jù)的緩沖區(qū),因?yàn)槲覀冊O(shè)置的是循環(huán)模式,因此當(dāng)數(shù)據(jù)傳輸量為0時(shí),DMA會自動去換到另一個(gè)緩沖區(qū)中并且將DMA的傳輸值給自動填充滿.參考手冊中是這么說的

    用這種方法的時(shí)候我們就不需要手動轉(zhuǎn)換當(dāng)前緩沖區(qū)了,我們只要關(guān)注我們收的是不是18字節(jié),然后解析即可

    第二種方法:我們將每一個(gè)緩沖區(qū)的大小改為比一幀數(shù)據(jù)長度大的值(比18大),這樣可以在一幀數(shù)據(jù)傳輸完成后不會因Counter值變0導(dǎo)致DMA指向下一內(nèi)存區(qū)域。DMA傳輸值不會自動填滿,且內(nèi)存區(qū)域還是指向當(dāng)前緩沖區(qū),然后我們將剩余數(shù)據(jù)量保存下來,再將DMA傳輸值填滿,接著把DMA指向另一個(gè)緩沖區(qū),最后通過判斷剩余數(shù)據(jù)量來決定是否對數(shù)據(jù)進(jìn)行處理。

    用這種方法更加地保險(xiǎn),我們可以很安全、“悠閑”地獲取到這一幀數(shù)據(jù)。我們這部分給出的代碼就是用的第二種方法

    其中

    if ((hdma_usart1_rx.Instance->CR & DMA_SxCR_CT) == RESET)
    {

    ? …

    ? //設(shè)定緩沖區(qū)1
    ? hdma_usart1_rx.Instance->CR |= DMA_SxCR_CT;

    }

    else

    {

    ? …

    ? //設(shè)定緩沖區(qū)0
    ? DMA2_Stream2->CR &= ~(DMA_SxCR_CT);

    }

    進(jìn)行的操作就是根據(jù)DMA CR寄存器的CT位的值來判斷當(dāng)前的緩沖區(qū)是誰,然后在處理過數(shù)據(jù)之后再將CT值設(shè)置為另一個(gè)緩沖區(qū),讓數(shù)據(jù)網(wǎng)另一個(gè)緩沖區(qū)中存。

    數(shù)據(jù)內(nèi)容解析

    最后重頭戲來了,將收到的數(shù)據(jù)的內(nèi)容給解析出來

    sbus_to_rc(sbus_rx_buf[0], &rc_ctrl);

    我們來看看這個(gè)sbus_to_rc函數(shù)是怎么解析數(shù)據(jù)的

    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; }

    這個(gè)函數(shù)有兩個(gè)參數(shù),其中sbus_buf就是我們要解析的緩沖區(qū),也就是我們在當(dāng)初初始化的時(shí)候設(shè)置好的兩個(gè)緩沖區(qū)數(shù)組中的其中一個(gè)。

    rc_ctrl是一個(gè)我們自己定義好的結(jié)構(gòu)體對象。 RC_ctrl_t結(jié)構(gòu)體是如是定義的

    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;

    結(jié)構(gòu)體中包含了4個(gè)通道(ch)用來存放上下左右撥桿的數(shù)據(jù),和兩個(gè)s數(shù)組用來存放左右上方的撥桿的數(shù)據(jù)

    遙控器數(shù)據(jù)處理函數(shù) sbus_to_rc 的功能是將通過 DMA 獲取到的原始數(shù)據(jù),按照遙控器的

    數(shù)據(jù)協(xié)議拼接成完整的遙控器數(shù)據(jù),以通道 0 的數(shù)據(jù)為例,從遙控器的用戶手冊中查到通道

    0 的長度為 11bit,偏移為 0。

    這說明如果想要獲取通道 0 的數(shù)據(jù)就需要將第一幀的 8bit 數(shù)據(jù)和第二幀數(shù)據(jù)的后三 bit 數(shù)據(jù)

    拼接,如果想要獲取通道 1 的數(shù)據(jù)就將第二幀數(shù)據(jù)的前 5bit 和第三幀數(shù)據(jù)的后 6bit 數(shù)據(jù)進(jìn)

    行拼接,不斷通過拼接就可以獲得所有數(shù)據(jù)幀,拼接過程的示意圖如下:

    解碼函數(shù) sbus_to_rc 通過位運(yùn)算的方式完成上述的數(shù)據(jù)拼接工作,十六進(jìn)制數(shù) 0x07ff 的二

    進(jìn)制是 0b0000 0111 1111 1111,也就是 11 位的 1,和 0x07ff 進(jìn)行與運(yùn)算相當(dāng)于截取出 11

    位的數(shù)據(jù)。

    通道 0 的數(shù)據(jù)獲取:首先將數(shù)據(jù)幀 1 和左移 8 位的數(shù)據(jù)幀 2 進(jìn)行或運(yùn)算,拼接出 16 位的數(shù)

    據(jù),前 8 位為數(shù)據(jù)幀 2,后 8 位為數(shù)據(jù)幀 1,再將其和 0x07ff 相與,截取 11 位,就獲得了

    由數(shù)據(jù)幀 2 后 3 位和數(shù)據(jù)幀 1 拼接成的通道 0 數(shù)據(jù)。其過程示意圖如下:

    我們看看代碼是如何實(shí)現(xiàn)的

    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位賦值給對應(yīng)的通道,這個(gè)通道的大小是16bit,所以是足夠放的。但是有一個(gè)問題,在第8行,

    sbus_buf[1] << 8

    這個(gè)sbus_buf[1]的大小是8個(gè)bit,我們學(xué)c語言的時(shí)候說了,左移以后右邊補(bǔ)0,左邊的數(shù)移出去以后就沒了。那這里sbus_buf[1]一共有8位,在左移8位那不一定是0么。

    懷著這個(gè)疑惑,打開了clion進(jìn)行debug,看看他到底是個(gè)啥值。結(jié)果如下:

    我們可以看到,雖然這個(gè)sbus_buf[1]規(guī)定的是一個(gè)8bit的大小,但是我們可以通過左移符號給它硬生生地把大小擴(kuò)展成最大32bit而且數(shù)據(jù)不丟失。這個(gè)問題的根本在于:stm32的寄存器是32bit的

    3. 總結(jié)

    通過寄存器來分析HAL庫是一件很枯燥的事情,但是這里面還是藏著許多的欣喜。當(dāng)你把每一個(gè)你經(jīng)常用起來習(xí)以為常的函數(shù)點(diǎn)開,點(diǎn)到最深層的寄存器層面的時(shí)候,你會發(fā)現(xiàn)之前你并不了解他,當(dāng)然你還會發(fā)現(xiàn),你再用它的時(shí)候會有更多的勇氣更加地信手拈來。

    在網(wǎng)上有人是這樣說的:高手編程都是初始化借用HAL的函數(shù),其它的直接操作寄存器。 但是可不要認(rèn)為這樣很裝x,學(xué)長是這樣說的:HAL庫中一進(jìn)去就是對外設(shè)狀態(tài)的維護(hù),如果說不用HAL庫,那么這個(gè)狀態(tài)指不定在哪就斷了沒人維護(hù)了,狀態(tài)也就亂了。所以說能用HAL庫就用HAL庫,那為什么有時(shí)候需要用寄存器呢,因?yàn)橛械膱鼍癏AL庫沒有幫你考慮到。我表示很認(rèn)同。

    總結(jié)

    以上是生活随笔為你收集整理的RM遥控器接收程序的分析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。