UART串口通讯

输出重定向

重写fputc函数让数据发向上位机

1
2
3
4
5
6
7
#include <stdio.h>

int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}

这样就可以让正常使用printf并让它输出到电脑的串口助手以方便调试。

阻塞模式:主要函数

阻塞模式意味着该函数会一直停留在当前调用位置,直到数据发送完成或发生超时。这种模式可能会 导致程序在数据传输期间停止响应其他任务,因此它适合较短时间的通信任务。

串口阻塞方式接收函数HAL_UART_Receive

1
2
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData,
uint16_t Size, uint32_t Timeout)

串口阻塞方式发送函数HAL_UART_Transmit

1
2
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData,
uint16_t Size, uint32_t Timeout)

注意,在中断函数(TIM,UART,…)里,用阻塞发送可能没问题,但是用阻塞接收大概率会直接卡死

中断模式:主要函数和时间线(自上而下)

——最好把耗时的程序都从中断回调里拿走,放到主函数!

串口中断方式接收函数HAL_UART_Receive_IT

第一次调用该函数会使能中断,因为硬件默认禁用外部中断所以需要使能。并且该函数会对存储数据的属性做配置,将状态机状态转换成HAL_UART_STATE_BUSY_RX。注意,这个函数不会调起RXNE标志。并且在做完这些事情后迅速返回主循环。只是起到使能硬件和刷新状态机的作用。硬件会在后台等待数据的到来。

1
2
3
4
5
6
7
8
9
10
11
12
13
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
// 这里只设置软件状态和使能中断
huart->pRxBuffPtr = pData; // 数据存到哪里
huart->RxXferCount = Size; // 要接收多少字节
huart->RxState = HAL_UART_STATE_BUSY_RX; // 软件状态

// 关键:只使能中断,不设置RXNE标志!
// 使能中断只用调用一次该函数即可完成。
SET_BIT(huart->Instance->CR1, USART_CR1_RXNEIE); // 允许硬件通知我们

return HAL_OK;
}

后台硬件

如果有数据,硬件自动挂起RXNE标志,该标志是中断启用的一个条件,在标志挂起后,NVIC检测到需求,并直接暂停CPU当前指令

1
2
// 这是硬件自动完成的,不是软件代码!
USART1->SR |= USART_SR_RXNE; // 硬件自动设置RXNE标志

函数HAL_UART_IRQHandler

在状态机状态为HAL_UART_STATE_BUSY_RX,RXNE标志挂起,使能中断的条件下该函数开始接收数据并存储。接收完成后,将RXNE标志撤下,把状态机的状态改为 HAL_UART_STATE_READY,最后调用用户的Callback中断回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
{
// 这个函数在中断发生时被自动调用

/* 接收中断处理 */
if((__HAL_UART_GET_IT(huart, UART_IT_RXNE) != RESET) &&
(__HAL_UART_GET_IT_SOURCE(huart, UART_IT_RXNE) != RESET)) {

// 关键:只有在BUSY_RX状态下才处理!
if(huart->RxState == HAL_UART_STATE_BUSY_RX) {
// 读取硬件数据寄存器
uint8_t received_data = (uint8_t)(huart->Instance->DR);

// 存储到用户缓冲区
*huart->pRxBuffPtr = received_data;
huart->pRxBuffPtr++;
huart->RxXferCount--;

// 如果接收完成
if(huart->RxXferCount == 0U) {
// 清除RXNE中断标志
__HAL_UART_CLEAR_IT(huart, UART_CLEAR_RXNE);
// 状态改为READY
huart->RxState = HAL_UART_STATE_READY;
// 调用用户回调函数
HAL_UART_RxCpltCallback(huart);
}
}
}
}

基于以上,我们可以把中断回调函数写成这样(无论什么中断里面最好不要加阻塞!!!)

1
2
3
4
5
6
7
8
void HAL_UART_RxCpltCallback(UART_HandleTypeDef*huart)
{
if (huart==&huart1)
{
quene[tail++] =aRxBuffer;//某些需要的命令
HAL_UART_Receive_IT(&huart1,(uint8_t*)&aRxBuffer,1);
}
}

这样可以保持接收数据结束后一直是BUSY状态,也就是说数据到来之后我们不必再次调用HAL_UART_Receive_IT函数,注意,此时调用完HAL_UART_Receive_IT会直接返回去执行主循环的程序,并继续执行主循环,这种写法可以代替HAL_UART_Receive_IT在主循环中来回调用的写法。不仅如此,这样在添加了一些判断条件的前提下,可以一个一个接收字节。带来的好处,就是当我们不知道有多少字节输入或者输出的时候,不会因为数据比预料的短而超时并造成数据接收失败。

DMA模式:主要函数和注意点

DMA的最大的好处就是CPU占用率极低

DMA发送函数 HAL_UART_Transmit_DMA

1
2
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData,
uint16_t Size)

在使用该函数前,最好在stm32f1xx_hal_uart.c 中的HAL_UART_Transmit_DMA里的

1
ATOMIC_SET_BIT(huart->Instance->CR3, USART_CR3_DMAT);

下面加一行

1
huart->gState = HAL_UART_STATE_READY;

以及时更新状态机防止意外的堵塞

相应的中断回调函数(如果有需要就要自己写)

1
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)

DMA接收函数

接收方式有两种,对应了HAL库中两组不同的函数

第一类

正常DMA中断接收函数

1
2
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData,
uint16_t Size)

对应的中断回调函数

1
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)

适用场景:定长协议发送,对实时性要求很高的地方

注意,接收需要考虑时序问题,如果刚调用完接收函数就直接去处理数据,最后处理的结果是乱掉的,因为还未接收完,而主频很高就直接执行下一行了。并且,在接收数据密度很高的情况下,两次采集之间要给缓冲时间。否则哪一次都采集不到就被下一次打断了,最好的方法就是把下一次接收放到接收中断回调的最后一行。

第二类

DMA空闲中断接收函数

1
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(&huart2, (uint8_t*)receive_buffer,MAX_RECEIVE_SIZE)

当调用此函数时,DMA开始接收,直到读到空闲位(即该位没有数据发送),然后DMA触发中断,对应的中断回调函数

1
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart,uint16_t Size)

适用场景:不定长协议发送,对实时性要求没有那么高的地方。

注意,当你调用一次中断接收函数时,**它不止会进入中断回调函数一次!**它在一次打断后会继续接收数据直到下一次读到空闲位并再一次进入中断回调函数,直到你的缓冲区满了。MAX_RECEIVE_SIZE就是你的缓冲区大小,而Size是它这一次读了多少数据(size的得出是因为内部有一个向下计数的计数器)。所以如果你想一次读一个的话,也是将接收函数写在中断回调的末尾。而非TIM里,这样会让TIM的打断变的不准确并且你读到的数据不一定完整(无论什么中断里面最好不要加阻塞!!!)。

比如我要通过UART从驱动板获取电机编码器数据,而编码器数据是不定长协议(因为速度大小在变)

我就可以这样

1
2
3
4
5
6
7
8
9
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart,uint16_t Size)
{
if(huart==&huart2)
{
dealing_data();//这一步应该放到主循环用环形队列处理,以免影响中断响应速度
clear_receive_data();
HAL_UARTEx_ReceiveToIdle_DMA(&huart2,(uint8_t*)receive_buffer,MAX_RECEIVE_SIZE);
}
}

对于不定长信息接收

可以采用队列一个一个字符收,也可以采用空闲中断一次一次收

如果选择空闲中断:

  • 数据有明确帧结构
  • 帧间有足够空闲时间
  • 追求CPU效率
  • 数据量不大

如果选择字符接收:

  • 实时性要求极高
  • 数据流连续无间隔
  • 协议复杂需要逐字解析
  • 资源受限缓冲区小