由遥控器接收和分析串口DMA
RM遥控器在使用过程中一般可分为信息接收和信息分析两部分,主要用于信息接收中串口的空闲中断DMA本文双缓冲区接收的信息接收部分主要基于RM研究官方给出的代码,在信息分析中,我们主要研究它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 //内存缓冲区1 hdma_usart1_rx.Instance->M0AR = (uint32_t)(rx1_buf); //memory buffer 2 //内存缓冲区2 hdma_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对库提供的接口函数进行比较和比较。
因为我们想以寄存器为切入点,所以我们用寄存器做分段
USART_CR3
SET_BIT
程序一开始就来了SET_BIT来使能DMA串口接收。我们可以点击它的定义,SET_BIT它实际上是一个宏定义,这个宏定义的作用是将赋值给寄存器。 而我们开启DMA接收串口就是将USART_CR3_DMAR给串口1赋值CR当然,存器,当然,这里提到的赋值是或等于,不会影响寄存器的其他标志。
#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 /*!< 0x00000040 */ #define USART_CR3_DMAR USART_CR3_DMAR_Msk /*!<DMA Enable Receiver*/
USART_CR3_DMAR它也是一个宏定义,它的值是0x00000040,也就是在CR1.我们可以查看参考手册,看看这个是什么作用。
注意,我们说的第七个是从1 开头的第七个空,手册上的位7是从0开始的第七个,所以我们要找的是位6,可以看出位置是
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. 第四个是什么?我们看手册
可见,是的
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 __HA_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,里面包含了很多的东西。例如这个外设现在的状态,以及很多的配置项。其中有一项非常重要,可以说我们对这个外设的大部分操作都是在修改这个东西里面的值。这个很重要的“东西”就是,我们以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就是外设地址寄存器。
它的作用就是DMA在用外设到存储器模式的时候,高速DMA外设是在哪,该去哪拿数据。
在看这个寄存器的时候注意最下面的一句话“”,你可能会联想到些什么,可能还没有完全醒悟,我们可以接着往下看,到时候会给你揭晓。
DMA_SxM0/1AR
在将外设地址写入到DMA的PAR寄存器中之后,我们又紧接着进行了两此赋值操作,将两个数组的地址赋值给了DMA的M0AR和M1AR寄存器中。
这两个寄存器是用来存放存储器的地址的,作用就是告诉DMA数据拿到了以后拿去哪、放到哪。
他们和PAR寄存器一样,都有这样一句话
DMA_SxNDTR
之后我们在DMA的NDTR寄存器中写入了一个的数,这个数是DMA传输的大小,
这个寄存器只有16位可用,最大值是65535。它的作用就是告诉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。
流程总结:
- 通过向usart的CR3寄存器的第7位置1,开启串口外设的DMA
- 调用__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); 使能串口的空闲中断
- 调用__HAL_DMA_DISABLE(&hdma_usart1_rx); 失能DMA,来保证之后的数据流传输地址能够被顺利写入。
- 向DMA的PAR寄存器中写入USART的数据寄存器的地址
- 分别向DMA的寄存器SxM0AR和SxM1AR中写入两个存储区的地址
- 向DMA寄存器SxCR的(,注意不是位19 ,位19说的是从0开始数。第19位是从1开始数)置1,
- 向DMA的NDTR寄存器中写入数据流长度,因为是双缓冲区,因此接了一帧数据(18个字节)的双倍,也就是36个字节
- 调用__HAL_DMA_ENABLE(&hdma_usart1_rx);开启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按照我们想要的方式运行。
第二个问题,后面这个while循环是在干嘛?
因为参考手册中有这样一段话:
**。只有这样才能安全地禁止外设 这就是为什么要加那个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;
}
这个开启双缓存区的函数干了哪些工作:
- 首先进来先判断了传递的参数的正确性和目前DMA的模式的正确与否。
assert_param(IS_DMA_BUFFER_SIZE(DataLength)); IS_DMA_BUFFER_SIZE(SIZE) (((SIZE) >= 0x01U) && ((SIZE) < 0x10000U))
这个函数是在检验设置的数据长度是否合规长度要大于1小于10000
并且判断如果是从内存到内存模式的话是不允许循环模式的,也就不能够开启双缓存区。
-
如果满足条件的话就先锁上dma。
__HAL_LOCK(hdma);
这个锁是,起到的作用类似于上厕所的时候厕所门的那个“有人”标志,如果上了操作系统,多个进程运行,那么就要避免同时去操作dma的情况,尤其是同时用dma去写入东西,因为那样就不知道数据到底是谁写的了。因此有进程在用dma的时候就先把DMA给占住,说:我在用它了 。
-
在DMA外设的CR寄存器中赋值
/* Enable the double buffer mode */
hdma->Instance->CR |= (uint32_t)DMA_SxCR_DBM;
这个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
那么
我认为:这两种形式都可以起到开启DMA的作用,但是用SET_BIT直接操作寄存器赋值更单纯,,而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 bit
in 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);
,谁让这家伙给我开启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函数干了这些事:
- 把串口到内存的DMA传输给打开了
- 打开了DMA的传输完成中断,和DMA数据“半传输完成中断”。这个半就是我们填入的size参数的一半。
等等,有点蒙了。再回一下例程中的操作:
__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
//设定缓冲区1
hdma_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
//设定缓冲区0
DMA2_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出来了。也就是说
回到上面的程序的分析
if(huart1.Instance->SR & UART_FLAG_RXNE)//接收到数据
{
__HAL_UART_CLEAR_PEFLAG(&huart1);
}
如果进入中断,并且读取数据寄存器非空,那么就清除PE这个标志。PE是个什么标志?
也就是说先判断一手RXNE是为了清除奇偶校验错误标志位,主体还是为了避免在传输的时候出现信号干扰,出现校验错误时如果不及时清除校验错误标志位,那么会一直进中断,而进不去主程序。 但是这里有一个前提昂,就是才能因为该错误进入中断。
在信息接收那一节中的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在收到这个请求的时候就知道自己该送数据走了,于是便会进行一次传输(这次传输的大小自然就是之前设置好的bit、Half Word…)。 也就是说,DMA之所以能做到的一次传输传输固定大小的数据,是因为使用DMA的外设会在收到这个固定大小的数据之后对DMA发起一个请求,让DMA帮忙把这么长的数据给送到要去的地方。(感受一下就可以发现,我们所说的配置DMA,并不是配置DMA,而是配置外设,是配置外设什么时候呼叫DMA,这也就是为什么USART中的DMA设置部分叫做**“DMA Request Settings”停止位**
因为每传输8bit的数据就会发送一个停止位,那么串口在使能了接收DMA之后.只要一接收到一个Bit就会产生一个**“DMA request”接收到一字节**的数据之后会被置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这个函数来开启的,那么就很有可能被奇偶校验的错误卡在中断中出不来。但是根据自己的实际测试,
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
传输数据长度计算
回到代码,如果是IDLE标志位被置1,也就是我们现在进入了空闲中断,遥控器已经发完了一帧数据,我们就要首先记录一下当前到底收到了多少字节,和我们预期的一帧数据长度是否一致,如果不一致,那么就说明这一帧数据是不准确的,我们就直接废弃这一帧数据了,这也就是后面的那个
if(this_time_rx_len == RC_FRAME_LENGTH)
的作用。
而我们计算数据长度的方法就是根据DMA的NDTR寄存器的特性,
因为我们在设置串口的DMA Request的时候设置的是一个字节申请一次DMA传输,而我们进入USART的空闲中断是接收完一帧数据之后才进入的,
因此,在我们进入空闲中断的时候,dma已经转移了18次数据了,也就是说DMA的NDTR寄存器已经有所改变了,准确来说是减少了很多次了,我们可以,也就是说我们从上一次空闲中断到这一次进入中断之间到底接收了多少个字节的数据,这个数就是我们真实接收到的一帧的字节数。因此就有了下面的这个计算公式
//获取接收数据长度,长度 = 设定长度 - 剩余长度
this_time_rx_len = SBUS_RX_BUF_NUM - hdma_usart1_rx.Instance->NDTR;
当然这里面还有很重要的一个问题,在我们进入空闲中断后如果串口又接收到数据,并且传够了一个字节甚至多个字节的话,那么就会在我们在串口中处理数据的时候产生一个甚至多个DMA请求,那么我们的接收数据的计算不就不准了么?这个NDTR正在看的时候还在减少,这肯定不会准嘛,因此就需要在我们计算的时候,,不让它再传送数据,也就。这就是下面这句话的作用
//失效DMA
__HAL_DMA_DISABLE(&hdma_usart1_rx);
当我们获取完这次接收的数据长度以后,我们要,以便下次我们可以再次通过上面的公式进行长度的计算。
hdma_usart1_rx.Instance->NDTR = SBUS_RX_BUF_NUM;
双缓冲区的使用
但是别忘了,我们是有两个缓冲区来接收数据的。为什么要用双缓冲区呢?我们知道,普通DMA的目标数据储存区域只有一个,也就是如果。(这就是普通DMA的缺点)而双缓冲模式下,我们DMA的目标数据储存区域有两个,也就是双缓冲,当一次完整的数据传输结束后(即Counter值从初始值变为0),会自动指向另一个内存区域。
那么双缓冲区是怎么用的呢?
在网上搜到一篇帖子,给出了两种方法,原文链接如下
STM32的DMA双缓冲模式详解_zhang1079528541的博客-CSDN博客_dma双缓冲模式
:我们可以设置两个18字节大小的缓冲区,也就是,因为我们设置的是循环模式,因此当数据传输量为0时,DMA会自动去换到另一个缓冲区中并且将DMA的传输值给自动填充满.参考手册中是这么说的
用这种方法的时候我们就
:我们将每一个缓冲区的大小改为比一帧数据长度大的值(比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 0
rc_ctrl->rc.ch[1] = ((sbus_buf[1] >> 3) | (sbus_buf[2] << 5)) & 0x07ff; //!< Channel 1
rc_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 3
rc_ctrl->rc.s[0] = ((sbus_buf[5] >> 4) & 0x0003); //!< Switch left
rc_ctrl->rc.s[1] = ((sbus_buf[5] >> 4) & 0x000C) >> 2; //!< Switch right
rc_ctrl->mouse.x = sbus_buf[6] | (sbus_buf[7] << 8); //!< Mouse X axis
rc_ctrl->mouse.y = sbus_buf[8] | (sbus_buf[9] << 8); //!< Mouse Y axis
rc_ctrl->mouse.z = sbus_buf[10] | (sbus_buf[11] << 8); //!< Mouse Z axis
rc_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 value
rc_ctrl->rc.ch[4] = sbus_buf[16] | (sbus_buf[17] << 8); //NULL
rc_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;
}
这个函数有两个参数,其中就是我们要解析的缓冲区,也就是我们在当初初始化的时候设置好的两个缓冲区数组中的其中一个。
是一个我们自己定义好的结构体对象。 结构体是如是定义的
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;
结构体中包含了用来存放左右上方的拨杆的数据
遥控器数据处理函数 sbus_to_rc 的功能是将通过 DMA 获取到的原始数据,按照遥控器的
数据协议拼接成完整的遥控器数据,以通道 0 的数据为例,从遥控器的用户手册中查到通道
0 的长度为 11bit,偏移为 0。
这说明如果想要获取通道 0 的数据就需要将
行拼接,不断通过拼接就可以获得所有数据帧,拼接过程的示意图如下:
解码函数 sbus_to_rc 通过位运算的方式完成上述的数据拼接工作,
通道 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 0
rc_ctrl->rc.ch[1] = ((sbus_buf[1] >> 3) | (sbus_buf[2] << 5)) & 0x07ff; //!< Channel 1
rc_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 3
rc_ctrl->rc.s[0] = ((sbus_buf[5] >> 4) & 0x0003); //!< Switch left
rc_ctrl->rc.s[1] = ((sbus_buf[5] >> 4) & 0x000C) >> 2; //!< Switch right
rc_ctrl->mouse.x = sbus_buf[6] | (sbus_buf[7] << 8); //!< Mouse X axis
rc_ctrl->mouse.y = sbus_buf[8] | (sbus_buf[9] << 8); //!< Mouse Y axis
rc_ctrl->mouse.z = sbus_buf[10] | (sbus_buf[11] << 8); //!< Mouse Z axis
rc_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 value
rc_ctrl->rc.ch[4] = sbus_buf[16] | (sbus_buf[17] << 8); //NULL
rc_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的大小,但是我们可以通过左移符号给它硬生生地把大小
3. 总结
通过寄存器来分析HAL库是一件很枯燥的事情,但是这里面还是藏着许多的欣喜。当你把每一个你经常用起来习以为常的函数点开,点到最深层的寄存器层面的时候,你会发现之前你并不了解他,当然你还会发现,你再用它的时候会有更多的勇气更加地信手拈来。
在网上有人是这样说的:高手编程都是初始化借用HAL的函数,其它的直接操作寄存器。 但是可不要认为这样很装x,学长是这样说的:HAL库中一进去就是对外设状态的维护,如果说不用HAL库,那么这个状态指不定在哪就断了没人维护了,状态也就乱了。所以说能用HAL库就用HAL库,那为什么有时候需要用寄存器呢,因为有的场景HAL库没有帮你考虑到。我表示很认同。