news 2026/10/20 4:39:08

STM32F407标准库六路串口配置与DMA发送避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F407标准库六路串口配置与DMA发送避坑指南

简介:这份资源面向使用STM32F407标准库进行多串口开发的嵌入式工程师与学生,聚焦6路USART的完整配置方案,解决多设备通信、模块化设计及并发数据流处理中的串口资源管理问题。包内共115个文件,以50个.c与50个.h源码文件为核心,涵盖USART、TIM、RCC、ADC、CAN等外设驱动,另含2个hex烧录文件、Keil工程文件(uvprojx、uvopt)及bat批处理脚本,压缩包约552KB,结构完整可直接编译。已有3893人学习下载,说明该方案在同类多串口应用中具有较高参考价值。读者可从中获取6路串口初始化、DMA发送通道分配、中断接收回调及多串口并行运行的实现思路,理解GPIO、时钟与DMA资源不冲突的配置方法,并借助工程文件快速搭建调试环境,适合需要提升串口通信效率与系统稳定性的中高级开发者参考。

1. 六路串口一起上:STM32F407 标准库环境到底怎么搭

第一次在 STM32F407 上同时开六路串口,是在一个多传感器采集的项目里。六路 UART 要同时跑,每路波特率还不一样,有的接上位机、有的接模块、有的做调试口。当时想当然地以为“标准库嘛,复制粘贴改个 USART 编号就行”,结果第一版代码烧进去,串口要么一个都收不到数据,要么 DMA 发到一半就卡死,调试口打印出来的全是乱码。后来才发现,标准库下多串口配置远不是改个宏定义那么简单——时钟使能、GPIO 复用、中断优先级、DMA 通道映射,每一项都有坑。

这套环境适合两类人:一是刚接触 STM32F407 标准库、想从单串口过渡到多串口的开发者;二是项目里确实需要多路串口并发、又不想上 HAL 库的团队。标准库虽然老,但在 F407 这种经典芯片上资料多、代码量小、执行效率高,很多工业现场还在用。装不好会怎样?轻则某一路串口不工作,重则 DMA 和中断打架,整个系统跑飞。下面按我实际搭环境的顺序,从工程创建到六路串口全部跑通,一步步来。

2. 工程骨架与时钟配置:标准库版本选择和基础环境搭建

2.1 标准库版本怎么选,别拿 F1 的库硬套 F4

STM32F407 属于 F4 系列,标准库要用STM32F4xx_DSP_StdPeriph_Lib_V1.8.0或更高版本。我见过有人拿 F1 的stm32f10x.h改改就往上套,编译能过,但时钟树配置完全对不上,串口波特率算出来全是错的。F4 的库文件结构是这样的:

STM32F4xx_DSP_StdPeriph_Lib_V1.8.0/ ├── Libraries/ │ ├── CMSIS/ │ │ ├── Device/ST/STM32F4xx/ │ │ │ ├── Include/ # stm32f4xx.h 等头文件 │ │ │ └── Source/Templates/ # system_stm32f4xx.c 启动文件 │ │ └── Include/ # core_cm4.h 等内核头文件 │ └── STM32F4xx_StdPeriph_Driver/ │ ├── inc/ # 外设驱动头文件 │ └── src/ # 外设驱动源文件 ├── Project/ │ └── STM32F4xx_StdPeriph_Templates/ │ ├── main.c │ ├── stm32f4xx_conf.h # 外设配置裁剪头 │ └── stm32f4xx_it.c/h # 中断服务函数 └── Utilities/

我一般会把Libraries整个拷到工程目录下,然后在 IDE 里添加头文件搜索路径。关键一步是stm32f4xx_conf.h,这个文件决定哪些外设驱动参与编译。六路串口要用到USART、GPIO、DMA、RCC、NVIC,对应的宏都要打开:

/* stm32f4xx_conf.h 中确保以下模块被使能 */ #include "stm32f4xx_rcc.h" #include "stm32f4xx_gpio.h" #include "stm32f4xx_usart.h" #include "stm32f4xx_dma.h" #include "misc.h" /* NVIC 优先级分组配置 */

这里有个细节:misc.h不在STM32F4xx_StdPeriph_Driver里,它在Libraries/CMSIS/Device/ST/STM32F4xx/下面,路径别加错。少了它,NVIC_Init和NVIC_PriorityGroupConfig都找不到。

2.2 系统时钟树配置:168MHz 下串口波特率才准

F407 标准库默认的系统时钟是 16MHz 内部 RC,直接拿来跑串口,波特率误差大得离谱。我一般用外部 8MHz 晶振,PLL 倍频到 168MHz。这段代码在system_stm32f4xx.c里改,或者自己写一个SystemInit覆盖:

/* 系统时钟配置:HSE 8MHz -> PLL -> 168MHz */ void SystemClock_Config(void) { RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); /* 等待 HSE 就绪,超时则停在这里方便排查 */ if (RCC_WaitForHSEStartUp() != SUCCESS) { while (1); } RCC_HCLKConfig(RCC_SYSCLK_Div1); /* AHB = 168MHz */ RCC_PCLK1Config(RCC_HCLK_Div4); /* APB1 = 42MHz,USART2~5 挂这里 */ RCC_PCLK2Config(RCC_HCLK_Div2); /* APB2 = 84MHz,USART1/6 挂这里 */ RCC_PLLConfig(RCC_PLLSource_HSE, 8, 336, 2, 7); RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); while (RCC_GetSYSCLKSource() != 0x08); }

参数说明:RCC_PLLConfig的五个参数分别是 PLL 源、M、N、P、Q。M=8 表示 8MHz 先分频到 1MHz,N=336 倍频到 336MHz,P=2 再分频得到 168MHz。Q=7 是给 USB 用的,这里不影响串口。APB1 分频 4 得到 42MHz,APB2 分频 2 得到 84MHz,这两个频率直接决定串口波特率的计算基准。如果 APB 频率设错,USART_Init里算出来的 BRR 值就是错的,现象就是能发不能收,或者收全是乱码。

提示:改完时钟后,先用调试口打印一下RCC_GetClocksFreq的结果,确认 PCLK1 和 PCLK2 跟预期一致,再往下配串口。

3. 六路串口逐个配:GPIO 复用、USART 参数与中断优先级

3.1 六路串口的引脚分配和时钟使能

F407 有 6 个 USART/UART:USART1、USART2、USART3、UART4、UART5、USART6。我用的引脚分配如下,这套分配避开了 JTAG 和 SDIO 的冲突引脚:

串口TX 引脚RX 引脚总线时钟宏
USART1PA9PA10APB2RCC_APB2Periph_USART1
USART2PA2PA3APB1RCC_APB1Periph_USART2
USART3PB10PB11APB1RCC_APB1Periph_USART3
UART4PC10PC11APB1RCC_APB1Periph_UART4
UART5PC12PD2APB1RCC_APB1Periph_UART5
USART6PC6PC7APB2RCC_APB2Periph_USART6

时钟使能要分两条总线写,别一股脑全塞进 APB2:

/* 使能 GPIO 和串口时钟 */ RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOB | RCC_AHB1Periph_GPIOC | RCC_AHB1Periph_GPIOD, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_USART6, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2 | RCC_APB1Periph_USART3 | RCC_APB1Periph_UART4 | RCC_APB1Periph_UART5, ENABLE);

GPIO 初始化时,模式要设成GPIO_Mode_AF,输出类型GPIO_OType_PushPull,复用功能用GPIO_PinAFConfig指定。以 USART1 为例:

GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_100MHz; GPIO_InitStruct.GPIO_OType = GPIO_OType_PushPull; GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP; GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);

其他五路照这个模板改引脚和复用宏就行。注意 UART4 和 UART5 的复用宏是GPIO_AF_UART4和GPIO_AF_UART5,不是GPIO_AF_USART4,写错了编译不报错但引脚没复用上。

3.2 USART 参数配置:波特率、字长和校验位

六路串口波特率我设成三组:USART1 和 USART6 用 115200,USART2 和 USART3 用 9600,UART4 和 UART5 用 38400。配置函数统一写一个,传参进去:

void USART_Config(USART_TypeDef* USARTx, uint32_t baudrate) { USART_InitTypeDef USART_InitStruct; USART_InitStruct.USART_BaudRate = baudrate; USART_InitStruct.USART_WordLength = USART_WordLength_8b; USART_InitStruct.USART_StopBits = USART_StopBits_1; USART_InitStruct.USART_Parity = USART_Parity_No; USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStruct.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USARTx, &USART_InitStruct); USART_Cmd(USARTx, ENABLE); }

USART_BaudRate填的是实际波特率数值,标准库内部会根据 PCLK 频率自动算 BRR。如果 PCLK 配置错了,这里填 115200 实际输出可能是 9600 的波形。USART_WordLength_8b配合USART_Parity_No就是最常见的 8N1 格式。如果接的模块需要偶校验,把USART_Parity改成USART_Parity_Even,同时字长要改成USART_WordLength_9b,否则校验位会挤掉数据位。

3.3 中断接收配置:NVIC 优先级分组和使能

六路串口都要中断接收,NVIC 优先级分组必须先定好。我一般用NVIC_PriorityGroup_2,2 位抢占优先级、2 位响应优先级,够用且不容易乱:

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStruct);

六路串口的中断通道分别是USART1_IRQn、USART2_IRQn、USART3_IRQn、UART4_IRQn、UART5_IRQn、USART6_IRQn。抢占优先级我按串口重要性排:USART1 最高设 1,USART6 设 2,其余设 3。响应优先级全设 0。这样高优先级串口的中断可以打断低优先级串口的处理,适合调试口和关键指令口。

使能接收中断用USART_ITConfig(USARTx, USART_IT_RXNE, ENABLE),然后在对应的stm32f4xx_it.c里写中断服务函数。每个串口的中断函数名不能写错,比如 UART4 的是UART4_IRQHandler,不是USART4_IRQHandler,写错了编译能过但中断永远不进。

4. DMA 发送配置:通道映射、传输模式和发送完成判断

4.1 DMA 通道映射表:六路串口分别走哪个流

F407 的 DMA 有两个控制器,DMA1 有 7 个流,DMA2 有 7 个流。串口发送的 DMA 请求映射是固定的,不能随便选:

串口DMA 控制器流通道
USART1_TXDMA2Stream7Channel4
USART2_TXDMA1Stream6Channel4
USART3_TXDMA1Stream3Channel4
UART4_TXDMA1Stream4Channel4
UART5_TXDMA1Stream7Channel4
USART6_TXDMA2Stream6Channel5

这张表我建议贴在显示器边上,配错一个流,对应的串口就发不出数据,而且不会报错,只是 DMA 传输完成标志永远不置位。

4.2 DMA 初始化:内存到外设模式的关键参数

以 USART1 的 DMA 发送为例:

DMA_InitTypeDef DMA_InitStruct; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); DMA_DeInit(DMA2_Stream7); DMA_InitStruct.DMA_Channel = DMA_Channel_4; DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStruct.DMA_Memory0BaseAddr = (uint32_t)tx_buffer; DMA_InitStruct.DMA_DIR = DMA_DIR_MemoryToPeripheral; DMA_InitStruct.DMA_BufferSize = tx_len; DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStruct.DMA_Mode = DMA_Mode_Normal; DMA_InitStruct.DMA_Priority = DMA_Priority_Medium; DMA_InitStruct.DMA_FIFOMode = DMA_FIFOMode_Disable; DMA_Init(DMA2_Stream7, &DMA_InitStruct);

DMA_DIR_MemoryToPeripheral表示从内存搬到外设,这是发送方向。DMA_PeripheralInc_Disable是因为外设地址固定是USART1->DR,不能自增。DMA_Mode_Normal表示传完指定长度就停,适合不定长发送;如果要循环发送固定数据,改成DMA_Mode_Circular。DMA_FIFOMode_Disable在直接模式下用,开了 FIFO 反而容易出玄学问题,我一般关掉。

使能 DMA 发送前,要先使能串口的 DMA 发送请求:

USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); DMA_Cmd(DMA2_Stream7, ENABLE);

4.3 发送完成判断:别用延时,用标志位

DMA 发送最忌讳发完就delay_ms等。正确做法是查DMA_FLAG_TCIF7或者开传输完成中断:

/* 等待 DMA 发送完成 */ while (DMA_GetFlagStatus(DMA2, DMA_FLAG_TCIF7) == RESET); DMA_ClearFlag(DMA2, DMA_FLAG_TCIF7);

如果是六路同时发,每路的标志位不一样,要分别判断。我一般给每路串口维护一个发送状态标志,DMA 传输完成中断里置位,主循环里检查。这样六路 DMA 可以并发启动,互不阻塞。

注意:DMA 传输完成标志在DMA_Mode_Normal下不会自动清除,必须手动DMA_ClearFlag,否则第二次发送时标志一直是置位的,判断逻辑就失效了。

5. 避坑与排查:六路串口跑不起来时先查这五处

5.1 现象:某一路串口完全无输出,其他五路正常

原因:DMA 流或通道映射错了。F407 的 DMA 请求映射是硬件固定的,比如 UART5_TX 只能用 DMA1_Stream7_Channel4,换成别的流,DMA 控制器收不到串口的请求信号,数据永远搬不出去。

解决:对照 4.1 的映射表逐路核对DMA_InitStruct.DMA_Channel和DMA_DeInit的流编号。改完后先单独测这一路,确认能发再合入六路。

5.2 现象:串口能发不能收,或者收几个字节就卡死

原因:中断优先级分组没设,或者NVIC_PriorityGroupConfig在NVIC_Init之后才调用。F407 的优先级分组是全局的,必须在所有NVIC_Init之前设好。另外,如果中断服务函数里没有清USART_IT_RXNE标志,会反复进中断,主循环被拖死。

解决:把NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2)放到main开头,所有外设初始化之前。中断服务函数里第一句就USART_ClearITPendingBit(USARTx, USART_IT_RXNE),再读USART_ReceiveData。

5.3 现象:DMA 发送的数据只有第一个字节对,后面全是错位

原因:DMA_MemoryInc设成了Disable,内存地址不自增,DMA 每次都从同一个地址搬数据,发出去的全是第一个字节。或者DMA_PeripheralDataSize和DMA_MemoryDataSize不一致,比如外设设 Byte、内存设 HalfWord,数据就乱了。

解决:发送方向必须DMA_MemoryInc_Enable,外设地址DMA_PeripheralInc_Disable。两边数据宽度统一设DMA_PeripheralDataSize_Byte和DMA_MemoryDataSize_Byte,除非你确定要传半字。

5.4 现象:六路串口同时收发时,系统偶尔死机

原因:DMA 传输完成中断和串口接收中断的优先级冲突,或者两个中断里操作了同一个全局变量没有保护。标准库没有自动的临界区保护,六路并发时共享的发送缓冲区索引、接收环形缓冲区指针都可能被踩。

解决:给每路串口独立的发送缓冲区和接收缓冲区,不要共用。DMA 传输完成中断的抢占优先级设得比串口接收中断低,让接收先处理。共享的计数器用__disable_irq()和__enable_irq()包住,或者用原子操作。

5.5 现象:波特率高的串口(115200)误码率高,低的正常

原因:APB 时钟分频不对,导致高波特率下 BRR 误差超过 3%。F407 的 USART1 和 USART6 挂 APB2,最高 84MHz;USART2~5 挂 APB1,最高 42MHz。如果 APB1 分频设成 2 得到 84MHz,超了 42MHz 上限,时钟树虽然能跑,但串口时序会飘。

解决:确认RCC_PCLK1Config是RCC_HCLK_Div4,RCC_PCLK2Config是RCC_HCLK_Div2。用示波器量一下 TX 引脚波形,115200 波特率下位宽应该是 8.68us 左右,偏差超过 0.3us 就要回头查时钟。

6. 装完怎么验证:六路串口回环测试和 DMA 发送计数

环境搭完,别急着接实际模块,先做回环测试。把每路串口的 TX 和 RX 用杜邦线短接,发什么收什么,六路同时跑,看有没有丢包。我一般写一个简单的测试函数:

/* 六路串口回环测试:发送递增数据,接收比对 */ void USART_LoopbackTest(void) { uint8_t tx_data[6] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; uint8_t rx_data[6] = {0}; USART_TypeDef* usart_list[6] = {USART1, USART2, USART3, UART4, UART5, USART6}; DMA_Stream_TypeDef* dma_stream[6] = {DMA2_Stream7, DMA1_Stream6, DMA1_Stream3, DMA1_Stream4, DMA1_Stream7, DMA2_Stream6}; uint32_t dma_flag[6] = {DMA_FLAG_TCIF7, DMA_FLAG_TCIF6, DMA_FLAG_TCIF3, DMA_FLAG_TCIF4, DMA_FLAG_TCIF7, DMA_FLAG_TCIF6}; DMA_TypeDef* dma_ctrl[6] = {DMA2, DMA1, DMA1, DMA1, DMA1, DMA2}; for (int i = 0; i < 6; i++) { /* 启动 DMA 发送 */ DMA_SetCurrDataCounter(dma_stream[i], 1); DMA_Cmd(dma_stream[i], ENABLE); /* 等待发送完成 */ while (DMA_GetFlagStatus(dma_ctrl[i], dma_flag[i]) == RESET); DMA_ClearFlag(dma_ctrl[i], dma_flag[i]); /* 等待接收 */ while (USART_GetFlagStatus(usart_list[i], USART_FLAG_RXNE) == RESET); rx_data[i] = USART_ReceiveData(usart_list[i]); /* 比对 */ if (rx_data[i] != tx_data[i]) { /* 记录哪一路出错,方便定位 */ printf("USART%d loopback fail: tx=0x%02X rx=0x%02X\r\n", i + 1, tx_data[i], rx_data[i]); } } }

这段代码一次测六路,每路发一个字节收一个字节。如果某一路rx_data和tx_data对不上,先查这一路的 GPIO 复用和 DMA 映射,再查波特率。六路全过之后,再把数据长度加到 64 字节、256 字节,测 DMA 的BufferSize和内存自增有没有问题。

验证 DMA 发送是否真的搬完了数据,不能只看DMA_Cmd使能了,要看DMA_GetCurrDataCounter是不是减到 0。我习惯在发送完成后读一次计数器:

/* 确认 DMA 实际传输的字节数 */ uint16_t remaining = DMA_GetCurrDataCounter(DMA2_Stream7); if (remaining != 0) { /* 没传完,可能是流被意外关闭或请求没使能 */ printf("DMA not finished, remaining=%d\r\n", remaining); }

还有一个容易忽略的点:六路串口同时做回环时,如果某一路的接收中断优先级设得太低,被其他串口的中断连续打断,USART_FLAG_RXNE可能溢出。现象是偶尔收到 0x00 或者上一次的残留数据。解决办法是把接收中断的抢占优先级统一设成一样,响应优先级按串口编号排,保证每个中断都能及时进。

从那以后我每次配多路串口,都强制走一遍「时钟树确认 → 引脚复用核对 → DMA 映射表对照 → 回环测试」这四步,少一步后面就要花两倍时间查玄学问题。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 4:38:41

网络安全面试高频题:从网络分层到渗透测试的全链路技术复盘

1. 从一道没答好的基础题说起&#xff1a;网络分层模型真的都掌握了吗&#xff1f;做网络安全这一行&#xff0c;不管是走攻防、走运维、还是走合规审计&#xff0c;面试官几乎开场就会从网络基础开始探底。我见过不少简历写得花团锦簇的候选人&#xff0c;一问OSI七层模型和TC…

作者头像 李华
网站建设 2026/10/20 4:37:40

SpinWait自旋等待如何优化客服系统高并发消息分发

做客服系统最怕什么&#xff1f;不是坐席人手不够&#xff0c;而是消息入口一打高并发&#xff0c;消息就卡在分发环节出不去。客户发一句&#xff0c;后台延迟三秒&#xff0c;坐席端会话队列越积越多&#xff0c;整个体验彻底崩塌。我压测过一个典型场景&#xff1a;单机同时…

作者头像 李华
网站建设 2026/10/20 4:37:40

Java字节码文件结构解析:从class文件到JVM执行机制

1. 字节码文件到底是个什么东西1.1 名字里藏着的两个概念“字节码文件”这个叫法&#xff0c;其实把两个层面的东西揉在了一起。第一个层面是“字节码”本身&#xff0c;指的是 JVM 能够识别的、介于源代码和机器码之间的一种中间指令格式&#xff1b;第二个层面是“文件”&…

作者头像 李华
网站建设 2026/10/20 4:37:34

RetinaFace实战:人脸检测原理、训练与部署全解析

人脸检测这块&#xff0c;RetinaFace这几年几乎成了绕不开的名字。我最早接触它是因为要做一批校园场景下的人脸抓拍&#xff0c;当时用通用目标检测模型跑了两版&#xff0c;精度一直卡在70%上下&#xff0c;后来换了RetinaFace&#xff0c;在小脸和密集场景下的表现直接上了一…

作者头像 李华
网站建设 2026/10/20 4:37:15

B站视频转文字实操指南:本地转录工具workbuddy安装与使用

1. 需求分析与工具选型思路1.1 为什么需要视频转文字我在处理视频内容时经常遇到一个非常现实的问题&#xff1a;视频里的信息密度太低了。一段十几分钟的采访视频&#xff0c;核心观点可能三句话就能说完&#xff0c;但为了获取这三句话&#xff0c;我必须从头到尾听一遍。如果…

作者头像 李华
网站建设 2026/10/20 4:36:03

dataWay数据通路设计:从增量同步到幂等写入的工程实践

1. 从“dataWay”这个名字说起&#xff1a;它到底想解决什么问题第一次看到“dataWay”这个项目标题&#xff0c;我的直觉是&#xff1a;这大概率是一个围绕“数据通路”做文章的东西。Way&#xff0c;路也&#xff1b;dataWay&#xff0c;数据走的那条路。名字起得很直白&…

作者头像 李华