STM32 - 串口通信(HAL库)
为什么要用HAL库?
使用方便,可以完全使用GUI配置、支持更多的芯片型号和开发板、良好的封装与抽象、Easy上手和开发
有什么缺点?
封装层次较高,造成稍微的性能损失
STM32cubemx部分
以使用stm32系列的NUCLEO-F03RB为例
1.配置时钟
选择STM32F103RCTx系列芯片,配置时钟的同时会自动配置IO口引脚
· HAL库串口发送/接收函数
/* 串口发送数据,使用超时管理机制 */
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
/* 串口接收数据,使用超时管理机制 */
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
/* 串口中断模式发送 */
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
/* 串口中断模式接收 */
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
/* 串口DMA模式发送 */
HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
/* 串口DMA模式接收 */
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
/* 串口中断处理函数 */
HAL_StatusTypeDef HAL_UART_IRQHandler(UART_HandleTypeDef *huart);
/* 串口发送中断回调函数 */
HAL_StatusTypeDef HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
/* 串口发送一半中断回调函数(用的较少) */
HAL_StatusTypeDef HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart);
/* 串口接收中断回调函数 */
HAL_StatusTypeDef HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
/* 串口接收一半回调函数(用的较少) */
HAL_StatusTypeDef HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);
/* 串口接收错误函数 */
HAL_StatusTypeDef HAL_UART_ErrorCallback();
· 参数解释:
符号 | 释义 |
---|---|
UART_HandleTypeDef * huart | 串口号 |
uint8_t * pData | 存放数据的数组 |
uint16_t Size | 接收的数据长度 |
例如:
HAL_UART_Receive_IT(&huart1, (uint8_t *)&aRxBuffer, 1);
中断进行完之后,并不会直接退出,而是会进入中断回调函数中,我们在其中写入代码即可
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
/* 用户自定义的代码 */
}
发送
一般HAL_UART_Transmit()就够用了。
· 重定向printf函数
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif PUTCHAR_PROTOTYPE
{
HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, 0xFFFF); return ch;
}
· (扩展)优雅的Log系统
[LOG]:usart1_buffer:log hello warning hello error hello
[LOG]:Some LOG occur
[LOG]:Good morning!
[WARNING]:Some warning! occur
[LOG]:Good morning!
[ERROR]:../Core/Src/main.c:200:Some ERROR!! occur
[LOG]:Good morning!
· 怎么做到在前面自动标记log的种类,并且根据打印error所在位置呢?
#define __DEBUG__
#ifdef __DEBUG__
#define LOG(fmt, ...) printf("[LOG]:" fmt "\r\n", ##__VA_ARGS__)
#define LOG_WARNING(fmt, ...) printf("[WARNING]:" fmt "\r\n", ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) printf("[ERROR]:%s:%d:" fmt "\r\n", __FILE__, __LINE__, ##__VA_ARGS__)
#elif
#define LOG(fmt, ...)
#define LOG_WARNING(fmt, ...)
#define LOG_ERROR(fmt, ...)
#endif
关于可变参数宏和C语言的一些预定义的宏,详细用法参考微软文档:可变参数宏 | Microsoft Docs
这样相当于我们对printf又做了一层封装,实现了对不同log进行分类,并且由于是编译期展开的,所以 是零成本的抽象,不会对运行时性能造成影响。
使用起来也很简单,和printf的用法一样。
LOG("Some LOG occur %s", your_log);
LOG_ERROR("Some ERROR!! occur %s", your_error);
LOG_WARNING("Some WARNING! occur %s", your_warning);
接收
接收要比较麻烦,因为单片机不知道你什么时候来数据,而在你没来数据的时候也不能干等着,因此就 要涉及到中断,HAL库中涉及到的就是这两个函数。
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);/* 串口中断模式接收 */
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);/* 串口DMA模式接收 */
HAL_UART_Receive_IT这个在HAL库中的实现有点坑,只能接收到定长的数据然后触发中断,没办法处 理不定长的数据,而且容易丢数据,不推荐使用。
这里的解决方案是利用闲时中断加上DMA来实现不定长数据的接收和处理。
闲时中断加上DMA
关于闲时中断和DMA 的详细原理,推荐阅读博客:STM32 HAL CubeMX 串口IDLE接收空闲中断+DMA - 古月居 (guyuehome.com)
具体操作分四步走。
1.在cubeMX里面打开串口中断和配置dma
2.在串口初始化的时候加上这两句:
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);/* 启用闲时中断标识符 */
HAL_UART_Receive_DMA(&huart1, usart1_buffer, BUF_MAX_SIZE);/* 开启串口接收dma */
此处的usart1_buffer,BUF_MAX_SIZE为:
#define BUF_MAX_SIZE 128 /* buffer大小 */
uint8_t usart1_buffer[BUF_MAX_SIZE]; /* dma接收buffer */
3.在串口的中断回调函数中加上这几句
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* 最好加在HAL_UART_IRQHandler前,防止回调的代码影响idle flag */
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)/* 判断是否idle */
{
USER_UART_IDLECallback(&huart1);/* 调用自己的Idle回调函数 */
}
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
4.最后就是写你自己的中断回调函数啦
void USER_UART_IDLECallback(UART_HandleTypeDef *huart)/* 自定义函数 */
{
HAL_UART_DMAStop(huart);/* 停下DMA */
int len = 0;/* 接收数据长度 */
uint8_t *buffer;/* buffer指针 */
if (huart->Instance == huart1.Instance)
{
len = BUF_MAX_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
buffer = usart1_buffer;
}
/* 现在你有了接收到数据的长度len和数据的buffer指针了
你可以做你任何想做的... */
memset(buffer, 0, len);/* buffer清空 */
HAL_UART_Receive_DMA(huart, buffer, BUF_MAX_SIZE);/* 重新开启dma */
__HAL_UART_CLEAR_IDLEFLAG(huart);/* 清空idle flag */
}
(扩展)指令解析
现在我们能接收不定长的数据了,数据有了,但是要怎么解析比较方便呢,这里整理了一套还算好用的指令解析的方法,给大家分享一下。
受main函数的传入参数的启发,我们可以将一次传输的字符串进行分割,变成argc、argv 的形式。 在IDLE回调函数中进行扩展。
void USER_UART_IDLECallback(UART_HandleTypeDef *huart)
{
HAL_UART_DMAStop(huart);
char **argv = (char **)malloc(sizeof(char *) * 10); /* 参数值-字符串-二维数组 */
int argc = 0; /* 参数个数 */
int tem_p = 0;
int len = 0;
uint8_t *buffer;
if (huart->Instance == huart1.Instance)
{
len = BUF_MAX_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
LOG("usart1_buffer:%s", usart1_buffer);
buffer = usart1_buffer;
}
/* 现在你有了接收到数据的长度len和数据的buffer指针了
将buffer里面的数据加工,处理为argc,argv的形式 */
buffer[len++] = ' ';
buffer[len++] = 'a';
for (int i = 0; i < len; i++)
{
if (buffer[i] == ' ' || i == len - 1)
{
argv[argc] = (char *)malloc(sizeof(char) * (i - tem_p + 1));
/* 注意,此 处malloc了,一定要记得free掉内存 */
memcpy(argv[argc], buffer + tem_p, i - tem_p);
argv[argc][i - tem_p] = '\0';
tem_p = i + 1;
argc++;
if (argc >= 10) break;
}
} /* 将argc,argv进行解析 */
arg_prase(argc, argv);
memset(buffer, 0, len);
for (int i = 0; i < argc; i++)
{
free(argv[i]);//释放内存
} free(argv);/* 释放内存 */
HAL_UART_Receive_DMA(huart, buffer, BUF_MAX_SIZE);
__HAL_UART_CLEAR_IDLEFLAG(huart);
}
arg_prase的定义如下:
uint8_t is_str_equal(const char *str_p1, char *str_p2)
{
int i = 0;
while (str_p1[i])
{
i++;
if (str_p1[i] != str_p2[i])
return 0;
}
return 1;
}
void arg_prase(int argc, char **argv)
{
for (int i = 0; i < argc; i++)
{
if (is_str_equal("hello", argv[i]))
{
LOG("Good morning!");
}
else if (is_str_equal("log", argv[i]))
{
LOG("Some LOG occur");
}
else if (is_str_equal("error", argv[i]))
{
LOG_ERROR("Some ERROR!! occur");
}
else if (is_str_equal("warning", argv[i]))
{
LOG_WARNING("Some warning! occur");
}
}
}
总结
以上就是STM32串口通信的所有内容,理论基础学完,更重要的是实践操作,多打代码、多实践使用,百炼成钢、熟能生巧!
本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 (CC BY-NC-ND 4.0) 进行许可。