由于项目开发原因要用到STM32H7系列的微控制器,为了缩短项目开发周期和提高效率,采用了ST官方的STM32CubeIDE进行开发,这个集成开发环境的特点主要有:1.从硬件电路设计开始为硬件工程师提供的各个管脚IO功能的映射及功能,方便硬件电路的设置与布局;2.可视化的硬件配置使得已经掌握单片机编程的使用者更加高效开发,只需注重于项目或系统自身功能部分的实现即可。
但不得顺带提一下该开发平台的不友好性:区别于STM32官方的std标准库,可视化配置采用了ST自主封装的HAL库(或者有的称为LL库),其初衷是给项目开发者提高效率和节省项目周期的,但是过高的集成度或者封装程度却会使某些底层外设的协同运作中产生干涉或者不兼容的效果,这也是我们老手使用者的一个头疼之处。
相信有过单片机、微控制器或者微处理器开发经验的人都知道(以下都简称微控制器吧),微控制器有各种通信接口,比如说UART、CAN、I2C、SPI、DCMI等等,这些通信接口在通信速度、通信稳定性以及通信底层协议等等上都具有各自的特点以及优缺点。但是说到数据的接收,各种接口往往都有相类似的接收数据的方式,以下主要说其中的三种:1.阻塞方式接收;2.中断方式接收;3.DMA方式接收数据(各别通信接口没有)
简单来说就是在系统循环里直接轮询数据,以阻塞方式去读取数据,这样一来当数据量或者数据频率过大时,会过多消耗硬件的资源,系统会把过多时间消耗在了数据的接收上。如果此时应用者在使用这个带操作系统的应用时就会傻傻的在那里等待数据的接收,等数据接收完后才能进行下一步操作。
以串口UART为例,微控制器具有其自身的资源调度方案,中断是最简单的一种,在STM32CubeMX配置好串口中断后,开发环境就会生成中断优先级配置函数、中断处理函数UART_IRQHandler(包含标志位判断、标志位清除还有数据的接收或发送),但是用户还需在系统初始化中开启所需的中断比如说串口接收中断,通过
__HAL_UART_ENABLE_IT(&huart1,UART_IT_RXNE)开启,开启RXNE中断后,串口每收到一个数据就会进入一次中断,然后在中断处理函数UART_IRQHandler中进行标志位的处理和数据的接收,接收完成后可以跟踪到函数HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart),这一回调函数的作用一般是作一些软标志位的置位,告诉系统你已经Ready了,或者可以类比为操作系统中的异步事件处理。
说了这么多,终于要说到本篇的正题了,当如果终端设备要传输过来的数据量过多的时候,采用中断接收的方法就往往不太对用户者友好了,这时,每接收一个字节的数据,就要进入中断函数,然后又是一系列的标志位的处理,数据的接收或者存储,最后再回调,返回系统一个事件,这样一系列频繁的进出中断处理,往往也不太尽如人意阿。那这时,我们就想把这种进出中断的次数进行尽可能的降低,于是就引入了我们的主角,DMA(Directly Memory Access)。DMA控制器接收到DMA请求后,会得到CPU的总线控制权,在数据传输完成后,又会把总线返回给CPU。这样一系列工作下来,便在存储器和外设之间连通了一条数据通道,使系统更加高效了。
static void MX_UART4_Init(void)
{1
/* USER CODE BEGIN UART4_Init 0 */
/* USER CODE END UART4_Init 0 */
/* USER CODE BEGIN UART4_Init 1 */
/* USER CODE END UART4_Init 1 */
huart4.Instance = UART4;
huart4.Init.BaudRate = 115200;
huart4.Init.WordLength = UART_WORDLENGTH_8B;
huart4.Init.StopBits = UART_STOPBITS_1;
huart4.Init.Parity = UART_PARITY_NONE;
huart4.Init.Mode = UART_MODE_TX_RX;
huart4.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart4.Init.OverSampling = UART_OVERSAMPLING_16;
huart4.Init.OneBitSampling = UART_ONE_BIT_SAMPLE_DISABLE;
huart4.Init.ClockPrescaler = UART_PRESCALER_DIV1;
huart4.AdvancedInit.AdvFeatureInit = UART_ADVFEATURE_NO_INIT;
if (HAL_UART_Init(&huart4) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_SetTxFifoThreshold(&huart4, UART_TXFIFO_THRESHOLD_1_8) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_SetRxFifoThreshold(&huart4, UART_RXFIFO_THRESHOLD_1_8) != HAL_OK)
{
Error_Handler();
}
if (HAL_UARTEx_DisableFifoMode(&huart4) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN UART4_Init 2 */
// __HAL_DMA_ENABLE(&hdma_uart4_rx);
__HAL_UART_ENABLE_IT(&huart4, UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart4,(uint8_t *)skp_data,buffersize);
/* USER CODE END UART4_Init 2 */
}
由于STM32的HAL库自生成的中断处理函数UART_IRQHandler()并没有对空闲中断的处理,而需要用户进行自行编写。
由于配置的DMA流模式为单次模式(Normal),因此需要在每次接收回调函数中后重新开启串口DMA接收。
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
uint32_t tmp;
if(UART4 == huart->Instance)
{
if((__HAL_UART_GET_FLAG(&huart4,UART_FLAG_IDLE) != RESET))
{
__HAL_UART_CLEAR_IDLEFLAG(&huart4);
tmp = huart4.Instance->ISR;
tmp = huart4.Instance->RDR;
HAL_UART_DMAStop(&huart4);
rx_len = buffersize - __HAL_DMA_GET_COUNTER(&hdma_uart4_rx);
recv_end_flag = 1;
HAL_UART_Receive_DMA(&huart4,(uint8_t *)skp_data,buffersize);
}
}
}
函数主体中用一个标志位来轮询接收完成这一异步事件是否到来,若置位了,则进行进一步处理。这里由于调试还没有成功,因此在接收完成后先把数据从另一个串口转发出去。于是发现数据有错误。经过几天调试,仍然没发现端倪。
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_TIM1_Init();
MX_UART4_Init();
MX_USART3_UART_Init();
/* Initialize interrupts */
MX_NVIC_Init();
/* USER CODE BEGIN 2 */
// HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_1);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
if(recv_end_flag == 1)
{
HAL_UART_Transmit(&huart3,(uint8_t *)skp_data,rx_len,1000);
recv_end_flag = 0;
rx_len = 0;
memset(skp_data,0,rx_len);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
(2021-01-24)
在漫长的调试后,我发现可以将DMA的模式调整为循环模式(Circular),在这种模式下串口的DMA接收会具有更高的实时性,这时候我们只需定义适当大小的接收缓冲数组,然后每次串口空闲中断下会接收到数据,当数据量大于缓冲数组大小时,数据会从首地址开始重新覆盖,更重要的是,在循环模式下,不用频繁的开启和停止DMA,使整个系统的实时性更好了因此我们可以定义一个静态寻址下标,来接收我们所需的数据,具体代码如下(STM32Cube中串口DMA模式改为Circular):
void UART4_IRQHandler(void)
{
/* USER CODE BEGIN UART4_IRQn 0 */
UART_IDLEHandler();
/* USER CODE END UART4_IRQn 0 */
HAL_UART_IRQHandler(&huart4);
/* USER CODE BEGIN UART4_IRQn 1 */
/* USER CODE END UART4_IRQn 1 */
}
/* USER CODE BEGIN 1 */
static uint8_t CurrentIndex = 0;
uint8_t RxBuff[BUFFER_SIZE];
uint8_t DataBuffer[BUFFER_SIZE];
volatile uint8_t RxLen;
volatile uint8_t RxClp=0;
extern UART_HandleTypeDef huart3;
void UART_IDLEHandler(void)
{
uint32_t temp;
uint8_t Len = 0;
if((__HAL_UART_GET_FLAG(&huart4,UART_FLAG_IDLE) != RESET))
{
__HAL_UART_CLEAR_IDLEFLAG(&huart4);
temp = huart4.Instance->ISR;
temp = huart4.Instance->RDR;
//HAL_UART_DMAStop(&huart4);
temp = 0;
Len = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_uart4_rx);
while(temp!= Len)
{
DataBuffer[temp++] = RxBuff[CurrentIndex++];
if(CurrentIndex >= BUFFER_SIZE)
CurrentIndex = 0;
}
RxLen = temp;
HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
HAL_UART_Transmit(&huart3,(uint8_t *)DataBuffer,(uint16_t)RxLen,100);
//HAL_UART_Receive_DMA(&huart4,(uint8_t *)RxBuff,BUFFER_SIZE);
}
}
经过一轮调试后,发现接收的数据还是不对。调试到这里的话,我的思绪已经想不出别的办法了…
顺带描述一下我的系统:我使用的是市面上十分普遍的蓝牙模块HC-05(蓝牙A),蓝牙A连接到我的传感器,传感器的速率是10Hz,然后通过两个HC-05模块进行配对(即与另一个蓝牙B配对),而后蓝牙B与我的微控制器STM32H7的串口4(UART4)连接,串口4采用DMA循环模式+空闲中断进行接收并存到数组中,然后通过另一个串口3进行发送数据。
系统运作的描述:系统起电后,蓝牙A与蓝牙B还没配对时,蓝牙A已经能收到传感器数据,而后两个蓝牙配对成功,接下来就运行了我上述调试的代码,后面通过PC串口助手接收串口3数据发现,只能接收到部分帧头0x55或者0xAA,而且没有规律性可言。传感器的数据一帧是固定的8个字节,以0x55为帧头,0xAA为帧尾。
在重新梳理整个系统的时序和逻辑后,发现系统起电后,两个蓝牙需要一定的时间去配对,方可接收传感器的数据,于是在系统初始化阶段加入一段适当的阻塞延时,等待蓝牙配对成功,随后启动传感器数据流模式,最终发现之前的空闲中断与DMA工作正常,维持了一周的Bug,通过逐步细查找出了问题所在:
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_UART4_Init();
MX_USART3_UART_Init();
/* Initialize interrupts */
MX_NVIC_Init();
/* USER CODE BEGIN 2 */
HAL_UART_Receive_DMA(&huart4,rcvData,BUFFERSIZE);
__HAL_UART_ENABLE_IT(&huart4, UART_IT_IDLE);
HAL_Delay(1000);
HAL_UART_Transmit(&huart4,startMeasure,8,10);
/* USER CODE END 2 */
(2021-01-30 End)