基础篇

哔哩哔哩视频地址

工程参考代码链接 Github Gitee

使用CubeMX建立Keil5工程

这里我们偷懒直接使用点亮LED灯的工程

配置串口并生成工程

image-20250706161839368

按照上图配置完串口后点击右上角生成工程image-20250706162045732

硬件连接串口

同样拿最便宜的DAPlink举例

image-20250706170311379

TX --> PA10(RX) 电脑发送单片机接收

RX --> PA9(TX) 电脑接收单片机发送

GND --> GND 与单片机共地(已共地的可以不接)

打开工程分析生成的代码

image-20250706163008759

使用到的HAL库函数说明

点击跳转F4的UASRT手册

1
2
3
4
5
6
/* 作用为初始化串口外设 */
HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart);
/* 作用为串口发送数据 */
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout);
/* 作用为串口接受数据 */
HAL_StatusTypeDef HAL_USART_Receive(USART_HandleTypeDef *husart, uint8_t *pRxData, uint16_t Size, uint32_t Timeout);

学习将函数应用到代码实现通信

完成连接后电脑应该可以识别到串口连接(查看设备管理器可以看到识别的硬件串口,如果旁边有感叹号说明需要安装对应的驱动),并打开串口软件进行通信,推荐串口软件Jcom

image-20250706171313227

单片机发送电脑接收

尝试用单片机发送串口信息到电脑上

1
2
3
4
5
6
7
8
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
/* 通过串口1发送“Hello” */
HAL_UART_Transmit(&huart1, (uint8_t*)"Hello", 5, 0xFF);

/* USER CODE END 2 */

编译烧录程序后查看串口软件,成功接收到了Hello信息

image-20250706171704215

电脑发送单片机接收

尝试用电脑发送串口信息到单片机上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
/* 通过串口1发送“Hello” */
HAL_UART_Transmit(&huart1, (uint8_t*)"Hello", 5, 0xFF);

/* 接收信息缓存,只接收5个字节数据 */
char recv_str[10];
HAL_UART_Receive(&huart1, recv_str, 5, 0xFFFFFFFF);

/* 将接收到的信息发回给电脑 */
HAL_UART_Transmit(&huart1, recv_str, 5, 0xFF);

/* USER CODE END 2 */

编译烧录程序后查看串口软件,成功接收到了Hello信息,然后我们发送12345给单片机

image-20250706173105902

串口常用技巧

变量波形显示

比如我希望输出一个连续的三角波幅值在[0, 99],周期为1000ms,并同时输出一个幅值一样频率加倍的锯齿波

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
/* 通过串口1发送“Hello” */
HAL_UART_Transmit(&huart1, (uint8_t*)"Hello", 5, 0xFF);

/* 输出变量 */
char wave_out = 0;

/* LED计时变量 */
uint16_t led_time = 0;

/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* LED计时到500ms状态切换一次 */
if(led_time >= 500)
{
led_time = 0; // 重新计时
/* 翻转引脚状态 */
HAL_GPIO_TogglePin(LED_G_GPIO_Port, LED_G_Pin);
}

/* 隔5ms发送一次变量点 */
if(wave_out >= 100)
{
/* 如果wave_out大于等于100,则输出由99 -> 0, 一个100个点 */
/* 通过串口1发送三角波变量数据,并增加包头包尾用于解析数据 */
HAL_UART_Transmit(&huart1, (uint8_t[]){0xD2, (199 - wave_out), 0xD3}, 3, 0xFF);
}else{
/* 如果wave_out小于100,则输出由0 -> 99, 一个100个点 */
/* 通过串口1发送三角波变量数据,并增加包头包尾用于解析数据 */
HAL_UART_Transmit(&huart1, (uint8_t[]){0xD2, wave_out, 0xD3}, 3, 0xFF);
}

/* 隔5ms发送一次变量点 */
if(wave_out >= 100)
{
/* 如果wave_out大于等于100,则输出由0 -> 99, 一个100个点 */
/* 通过串口1发送锯齿波变量数据,并增加包头包尾用于解析数据 */
HAL_UART_Transmit(&huart1, (uint8_t[]){0xCE, (wave_out % 100), 0xCF}, 3, 0xFF);
}else{
/* 如果wave_out小于100,则输出由0 -> 99, 一个100个点 */
/* 通过串口1发送锯齿波变量数据,并增加包头包尾用于解析数据 */
HAL_UART_Transmit(&huart1, (uint8_t[]){0xCE, wave_out, 0xCF}, 3, 0xFF);
}

/**
* wave_out在[0, 199]循环
* 一个周期200个数据点,一个数据点5ms
* 所以波形一个周期为1000ms
*/
wave_out++;
if(wave_out >= 200)
{
wave_out = 0;
}

/* 保持电平状态方便观察 */
HAL_Delay(5); // 延时10ms
led_time += 5; // LED计时增加

/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */

image-20250706180933172

以三角波解析为例按图配置,包头包尾并不是必须的,但是如果没有包头包尾很有可能数据会错乱,甚至必要情况下需要增加校验码

image-20250706181135190

打开曲线后可以看到输出的变量曲线

image-20250706181107586

可读日志输出(非重定向)

如果我们直接使用HAL_UART_Transmit函数打印可读日志会导致代码移植性差、可读性差、代码量增加,所以很多人会选择使用重定向,把串口发送重定向到printf函数,使用printf来打印日志,但是这样就需要开启微库(Micro LIB)支持,容易造成死机问题,而且这样的printf依然有使用限制。所以我通常会构造类printf函数来代替串口发送函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
#include "stdio.h"
#include "stdarg.h"
#define CONSOLEBUF_SIZE 512
static char Uart_buf[CONSOLEBUF_SIZE];
void PrintfDebug(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
int length = vsnprintf(Uart_buf, sizeof(Uart_buf) - 1, fmt, args);
va_end(args);
HAL_UART_Transmit(&huart1,(uint8_t *)Uart_buf, length, 0xff);
}

/* USER CODE END 0 */

构造了一个PrintfDebug函数,尝试在代码中输出LED引脚状态

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
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
/* 通过串口1发送“Hello” */
HAL_UART_Transmit(&huart1, (uint8_t*)"Hello", 5, 0xFF);

/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* 打印日志并输出变量, 输出“\r\n”是为了换行显示 */
PrintfDebug("LED Green State %d\r\n", HAL_GPIO_ReadPin(LED_G_GPIO_Port, LED_G_Pin));

/* 反转引脚状态 */
HAL_GPIO_TogglePin(LED_G_GPIO_Port, LED_G_Pin);

/* 延时500ms */
HAL_Delay(500);

/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */

然后打开串口软件就可以看到打印的信息

image-20250706182914767

练习部分

那我再提几个问题大家可以有空思考怎么做

  1. F4的手册内有大量关于Usart的函数和宏命令,有很多命令是有妙用的
  2. 使用HAL_UART_Transmit发送串口数据会堵塞程序的执行,是否能使用DMA来发送数据就不会堵塞程序执行
  3. 如何像cmd窗口一样跟单片机进行交互,可以了解一下 Letter-Shell
  4. 通过串口来收发送文件,例如Ymodel协议

进阶篇

先预告,等基础部分搞得差不多的时候开工