一些前置知识

并、串行通信的区别

并行通信 串行通信
传输原理 数据各个位同时传输 数据按位顺序传输
优点 速度快 占用引脚资源少
缺点 占用引脚资源多 速度相对较慢

串行通信的分类

(*)1、按照数据传送方向,分为:

单工:数据传输只支持数据在一个方向上传输;
半双工:允许数据在两个方向上传输。但是,在某一时刻,只允许数据在一个方向上传输,它实际上是一种切换方向的单工通信;它不需要独立的接收端和发送端,两者可以合并一起使用一个端口。
全双工:允许数据同时在两个方向上传输。因此,全双工通信是两个单工通信方式的结合,需要独立的接收端和发送端。

(***) 2、按照通信方式,分为:

同步通信:带时钟同步信号传输。比如:SPI,IIC通信接口。
异步通信:不带时钟同步信号。比如:UART(通用异步收发器),单总线。
在同步通讯中,收发设备上方会使用一根信号线传输信号,在时钟信号的驱动下双方进行协调,同步数据。例如,通讯中通常双方会统一规定在时钟信号的上升沿或者下降沿对数据线进行采样。

在异步通讯中不使用时钟信号进行数据同步,它们直接在数据信号中穿插一些用于同步的信号位,或者将主题数据进行打包,以数据帧的格式传输数据。通讯中还需要双方规约好数据的传输速率(也就是波特率)等,以便更好地同步。常用的波特率有4800bps、9600bps、115200bps等。

在同步通讯中,数据信号所传输的内容绝大部分是有效数据,而异步通讯中会则会包含数据帧的各种标识符,所以同步通讯效率高,但是同步通讯双方的时钟允许误差小,稍稍时钟出错就可能导致数据错乱,异步通讯双方的时钟允许误差较大。

三种串口通信方式

轮询方式

特点:发一个,接受一个。

缺点:占用cpu,利用率较低。

轮询模式

波特率:常见的用115200,9600Bits/s。需要保证接收端和发送端波特率一致

字节长度:8字节,外加前面1位起始位,后面1位结束位,一共10位。

各种参数

轮询模式代码

主要用到的hal库函数如下:

1
2
3
4
5
HAL_UART_Receive(&huart1,receveData,2,HAL_MAX_DELAY);
//接收,第一位为串口地址,第二位为指针,第三位为数组长度,第四位为等待最大时长(HAL_MAX_DELAY为无限制时长)

HAL_UART_Transmit(&huart1,receveData,2,100);
//发送,第一位为串口地址,第二位为指针,第三位为数组长度,第四位为等待最大时长(HAL_MAX_DELAY为无限制时长)

代码实践

通过轮询模式实现串口收发数据:

实践一:实现收发“Hello World”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...

char message[]="Hello World"

/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */

HAL_UART_Transmit(&huart1,(uint8_t*)message,strlen(message),100);
//char强转为uint8_t类型
HAL_Delay(1000);

}

/* USER CODE BEGIN 3 */
}

实践二:实现发送对应信息,点亮对应小灯

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
...

uint8_t receveData[2];

/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */

HAL_UART_Receive(&huart1,receveData,2,HAL_MAX_DELAY);
HAL_UART_Transmit(&huart1,receveData,2,100);
if(receveData[1]=='1'&&receveData[0]=='R'){
HAL_GPIO_TogglePin(LED_RED_GPIO_Port,LED_RED_Pin);
}

if(receveData[1]=='1'&&receveData[0]=='Y'){
HAL_GPIO_WritePin(LED_YELLOW_GPIO_Port,LED_YELLOW_Pin,GPIO_PIN_SET);
}

if(receveData[1]=='1'&&receveData[0]=='G'){
HAL_GPIO_WritePin(LED_GREEN_GPIO_Port,LED_GREEN_Pin,GPIO_PIN_SET);
}

//实现对传输信息的接收,并点亮对应的引脚
/* USER CODE BEGIN 3 */
}
...

中断模式

和轮询类似,只是CPU在寄存器空闲时可以处理其他事情。大致原理如下:

中断模式

transmit发送和轮询一样,而receive接收,由于不会等待,所以在回调函数中书写相关的代码逻辑。

你需要在cubemx中开启全局中断。

开启全局中断

中断模式代码

主要用到的hal库函数如下:

1
2
3
4
5
6
7
8
9
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
//弱函数,中断回调函数
}

HAL_UART_Transmit_IT(&huart1,receveData,2);//第一个为串口,第二个为指针,第三个为数组长度

HAL_UART_Receive_IT(&huart1,receveData,2);//第一个为串口,第二个为指针,第三个为数组长度

代码实践

实践一:与中断的实践二功能相同

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
31
32
33
34
35
36
/* USER CODE BEGIN PV */
uint8_t receveData[2];
/* USER CODE END PV */

...

/* USER CODE BEGIN 0 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)//在中断处理函数中书写代码逻辑
{


HAL_UART_Transmit_IT(&huart1,receveData,2);
if(receveData[1]=='1'&&receveData[0]=='R'){
HAL_GPIO_TogglePin(LED_RED_GPIO_Port,LED_RED_Pin);
}

if(receveData[1]=='1'&&receveData[0]=='Y'){
HAL_GPIO_WritePin(LED_YELLOW_GPIO_Port,LED_YELLOW_Pin,GPIO_PIN_SET);
}

if(receveData[1]=='1'&&receveData[0]=='G'){
HAL_GPIO_WritePin(LED_GREEN_GPIO_Port,LED_GREEN_Pin,GPIO_PIN_SET);
}

HAL_UART_Receive_IT(&huart1,receveData,2);//每次结束后,开启下一次的接收
}

/* USER CODE END 0 */

...

/* USER CODE BEGIN 2 */

HAL_UART_Receive_IT(&huart1,receveData,2);//在main函数中开启接受的中断

/* USER CODE END 2 */

DMA模式

搬运数据,创建通道,等待完成后通过中断处理函数通知cpu。

优点:进一步解放cpu。

开启DMA:(从左至右,从上至下,顺时针)开启的DMA通道,数据传输方向,优先级(一般默认),接收端和发送端是否地址自增,模式。

DMA

DMA模式代码

主要用到的hal库函数如下:

1
2
3
HAL_UART_Transmit_DMA(&huart1,receveData,2);//第一个为串口,第二个为指针,第三个为数组长度

HAL_UART_Receive_DMA(&huart1,receveData,2);//第一个为串口,第二个为指针,第三个为数组长度

实践与上述两种方法类似,略。

实现不定长数据接收

主要用到的hal库函数如下:

1
2
3
4
5
6
7
8
9
10
11
__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
//所用的中断回调函数

HAL_UARTEx_ReceiveToIdle(&huart1,receveData,sizeof(receveData));

HAL_UARTEx_ReceiveToIdle_IT(&huart1,receveData,sizeof(receveData));

HAL_UARTEx_ReceiveToIdle_DMA(&huart1,receveData,sizeof(receveData));
//第三位是最大接收的长度,不大于数组长度


此方式下DMA的缺点:传输过半中断会触发中断,即发送一半就会截断。

通过取消使能串口,取消中断。如下:

1
2
3
4
5
extern DMA_HandleTypeDef hdma_usart1_rx;
//在main.h中进行定义(如果勾选了为每个外设生成单独的.c.h文件)

__HAL_DMA_DISABLE_IT(&hdma_usart1_rx,DMA_IT_HT);
//第一个为dma的串口,在中断处理函数和main函数中都要添加

不定长数据接收代码

代码如下:

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
31
32
/* USER CODE BEGIN PV */

uint8_t receveData[5];

/* USER CODE END PV */


/* USER CODE BEGIN 0 */

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{

if(huart==&huart1)
{
HAL_UART_Transmit_DMA(&huart1,receveData,Size);
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,receveData,sizeof(receveData));
//每次结束后都开启接收
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx,DMA_IT_HT);
//取消使能
}

}
/* USER CODE END 0 */
...
/* USER CODE BEGIN 2 */

HAL_UARTEx_ReceiveToIdle_DMA(&huart1,receveData,sizeof(receveData));
//开启第一次接收
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx,DMA_IT_HT);//取消使能

/* USER CODE END 2 */

主要参考资源

1.【STM32】串口通信基本原理(超基础、详细版)_stm32串口通信工作原理-CSDN博客

2.【keysking的STM32教程】 第8集 STM32的串口通信_哔哩哔哩_bilibili

3.【STM32入门教程-2024】第9集 STM32串口原理与串口中断模式收发 | keysking的stm32教程_哔哩哔哩_bilibili

4.【工作STM32】第10集 STM32串口DMA模式与收发不定长数据 | keysking的stm32教程_哔哩哔哩_bilibili