串口与DMA
# 写在前面
如果要了解串口的原理与一些配置相关的东西,可以去找之前写过的一篇这篇文章。但是它到之后的程序阶段就最好先别看了,因为我自己也无法验证到底对不对。所以程序的部分改为以下内容
# 非 DMA 方式传输
# 阻塞式发送函数
特点:发送时没有发完单片机不能执行其他工作
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart,uint8_t *pData,uint16_t Size, uint32_t Timeout);
//参数1:huart,串口实例的指针
//参数2:*pData,待发送数据缓冲区的指针
//参数3:Size,待发送数据的字节数
//参数4:Timeout,超时时间值
//返回值:HAL_StatusTypeDef,函数执行状态。
2
3
4
5
6
7
如果超出超时时间值,会自己结束以免打扰其他进程
# 非阻塞式发送函数
HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart,uint8_t *pData, uint16_t Size);
//参数1:huart,串口实例的指针
//参数2:*pData,待发送数据缓冲区的指针
//参数3:Size,待发送数据的字节数
//返回值:HAL_StatusTypeDef,函数执行状态
////串口发送完毕中断回调函数
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart) //发完一半进入
2
3
4
5
6
7
8
9
10
发送完之后,会进入发送完中断
运行示例:
HAL_UART_Transmit_IT(&huart1, dat_Txd, 5);
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1);
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_9);
}
}
2
3
4
5
6
7
8
9
判断句柄是否正确的huart->Instance
是它的基地址
# 非阻塞式接收函数
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout);
//参数1:huart,串口实例的指针
//参数2:*pData,数据接收据缓冲区的指针
//参数3:Size,待接收数据的字节数
//参数4:Timeout,超时时间值
//返回值:HAL_StatusTypeDef,函数执行状态
2
3
4
5
6
7
不推荐使用,因为发来数据的对象发送的时间不可控,但你的时间是确定的
# 阻塞式接收函数
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart,uint8_t *pData,uint16_t Size);
//参数1:huart,串口实例的指针。
//参数2:*pData,数据接收据缓冲区的指针。
//参数3:Size,待接收数据的字节数。
//返回值:HAL_StatusTypeDef,函数执行状态。
////串口接收完毕中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);
2
3
4
5
6
7
8
9
10
接收完之后,会进入接收完中断
运行实例:
//使用中断,非阻塞方式
HAL_UART_Transmit_IT(&huart1, &dat_Rxd, 1);
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1)
{
if(dat_Rxd == 0x5A)
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_8);
}
}
2
3
4
5
6
7
8
9
10
11
接收完一个字节就中断
# 非 DMA 实例
我们循序渐进的用不同等级的代码来实现板间的串口通信
# 阻塞式发送与非阻塞式接收
/*
@brief 简单的串口通信
@type 使用阻塞式发送与非阻塞式接收
@coding UTF-8
*/
//// main 之前的内容
// 定义简单的LED开闭宏
#define LEDx_ON() HAL_GPIO_WritePin(GPIOx, GPIO_PINx, GPIO_PIN_SET)
#define LEDx_OFF() HAL_GPIO_WritePin(GPIOx, GPIO_PINx, GPIO_PIN_RESET)
// 收发使用的数组与数字
uint8_t Tx_str1[] = "LEDx is opened.\r\n";
uint8_t Tx_str2[] = "LEDx is cloesd.\r\n";
uint8_t Rx_data = 0;
//// 回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (Rx_data == 0xa1)
{
LEDx_ON();
HAL_UART_Transmit(&huart1, Tx_str1, sizeof(Tx_str1), 10000);
// @注意1
HAL_UART_Receive_IT(&huart, &Rx_data, 1);
}
else if (Rx_data == 0xa2)
{
LEDx_OFF();
HAL_UART_Transmit(&huart1, Tx_str2, sizeof(Tx_str2), 10000);
// @注意1
HAL_UART_Receive_IT(&huart, &Rx_data, 1);
}
}
//// main中的自定义内容
int main()
{
// forward
// 启动接受中断(非阻塞式接收函数)
HAL_UART_Receive_IT(&huart1, &Rx_data, 1);
// later
}
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
31
32
33
34
35
36
37
38
39
40
41
42
这段函数的作用就是使能了串口1,也就是 UART1
的接收中断,接受传入到串口1的信息,并且每接受一个数据帧(长度可设置,一般为 8bit,也就是两位16进制数)之后发生中断
在中断中,检测这个发来的2位16进制数,如果大小为 0xa1
则打开 LED,并向信息来源发送“LED is opened”;如果大小是 0xa2
则关闭 LED,并向信息来源发送“LED is closed”。
注意解析
- 在接收中断,也就是
HAL_UART_Receive_IT()
作用过一次之后,他就失效了,也就是这一句函数只管使用一次接收
# DMA 方式
# DMA 是什么
DMA 全称 Direct Memory Access
(直接存储器访问),是 STM32 的一个外设,它的特点在于:
在不占用 CPU 的情况下将数据从存储器直接搬运到外设,或者从外设直接搬运到存储器,当然也可以从存储器直接搬运到存储器。
比如在需要串口发送大量数据的时候,CPU 只需要发起 DMA 传输请求,然后就可以去做别的事情了,DMA 会将数据传输到串口发送,DMA 传输完之后会触发中断,CPU 如果有需要,可以对该中断进行处理,这样一来 CPU 的效率就大大提高了
# CubeMX设置
比起串口的设置只需要注意一下几点即可:
# 配置DMA
- 可以设置从 FLASH 到外设或者 SRAM 到外设
- 优先程度“Priority”也可调
- 数据宽度一般默认,在代码中设定
- 模式中,我们可以设置为“Circle”或者自己手动在 DMA 接受时像中断接收一样一遍又一遍的使能接收
# 函数部分
# 发送函数
HAL_StatusTypeDef HAL_UART_Transmit_DMA ( UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
//参数1:huart,串口实例的指针
//参数2:*pData,待发送数据缓冲区的指针
//参数3:Size,待发送数据的字节数
//返回值:HAL_StatusTypeDef,函数执行状态
2
3
4
5
# 接收函数
HAL_StatusTypeDef HAL_UART_Receive_DMA ( UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
//参数1:huart,串口实例的指针
//参数2:*pData,接收数据缓冲区的指针
//参数3:Size,待接收数据的字节数
//返回值:HAL_StatusTypeDef,函数执行状态
////串口接收完毕中断回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);
2
3
4
5
6
7
8
9
像中断接收的方法一样,我们在使用这个函数的时候也是只有一次作用的机会,如果没有在模式设置中设为“Circle”时,则必须手动开启
且 DMA 方式接收的时候依然会有接受完中断或者接收一半中断,我们可以通过这个特性设置双缓存区等等
注意
这里的设置中一定要开启UART
的全局中断,否则 DMA 的发送将只能工作一次!亲测
# 重定向printf
我们在普通环境下使用C编程时通常会使用 printf
函数打印数据,但是由于 printf
函数使用了半主机模式,所以直接使用标准库会导致程序无法运行。我们通常会使用一些方式对 printf
进行重定向来代替 HAL_Transmit_xx
来从串口输出数据,其实有很多方法,这篇文章 (opens new window)讲的不错,这里只记录下最简单的一个
# 通过 MicroLib 重定向
首先要在魔术棒里的“Target”中勾选“Use MicroLib”选项,一定要勾选!
由于在printf
的底层,它是不会一个字符一个字符去输出,而是会调用更底层的 I/O 函数:fputc
去逐个字符打印,所以只需要重定向此函数至串口即可:
可以在uart.c
中添加如下代码:
/* USER CODE BEGIN 1 */
#if 1
#include <stdio.h>
int fputc(int ch, FILE *stream)
{
/* 堵塞判断串口是否发送完成且等待 */
while((USART1->ISR & 0X40) == 0);
/* 串口发送完成,将该字符发送 */
USART1->TDR = (uint8_t) ch;
return ch;
}
#endif
/* USER CODE END 1 */
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注意
如果是 STM32F1 系列,上面这段代码不适用,请使用下面的代码
/* USER CODE BEGIN 1 */
#if 1
#include <stdio.h>
int fputc(int ch, FILE *stream)
{
/* 堵塞判断串口是否发送完成且等待 */
while((USART1->SR & 0X40) == 0);
/* 串口发送完成,将该字符发送 */
USART1->DR = (uint8_t) ch;
return ch;
}
#endif
/* USER CODE END 1 */
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
我们均使用了寄存器操作以提高效率!
其他两种方法可以了解一下!!