☰
STM32L152RE串口收发实战:从时钟配置到中断与DMA
2026/10/13 8:49:12 网站建设 项目流程

1. 为什么在L系列上折腾串口值得单独写一篇

拿到STM32L152RE这块板子的时候,我第一反应是"低功耗系列,串口能有什么花样"。结果真上手才发现,L系列的串口配置和F系列有一堆细节差异,尤其是时钟树、低功耗模式下的唤醒、以及LPUART和USART的区别,踩过的坑足够写满两页笔记。这篇就把我在STM32L152RE上跑通串口收发数据的完整过程拆开讲,从时钟配置到中断接收,再到实际调试中遇到的怪问题,全部摊开说。

串口这东西看起来简单,TX接RX、RX接TX、共地,三根线就能通。但真正在项目里用起来,你会发现"能发出去"和"稳定收发"之间隔着十万八千里。STM32L152RE属于STM32L1系列,基于Cortex-M3内核,主打低功耗,它的USART外设和F1系列同源但时钟使能方式不同,很多人从F103转过来直接照搬代码,结果串口死活不出数据,问题就出在时钟配置上。

这篇文章适合谁看?如果你手里正好有STM32L152RE的开发板或者自定义板,想快速把串口收发跑通,不想在时钟和寄存器上耗太久,那这篇就是给你写的。如果你是从F系列转过来的老手,也可以看看L系列有哪些不一样的地方,避免踩我踩过的坑。我会从最基础的时钟配置讲起,一直讲到中断接收和实际调试技巧,每个步骤都解释为什么这么做,而不是只丢一段代码让你抄。

2. 先把时钟树理清楚:L152RE的USART时钟从哪来

2.1 默认时钟源和F系列的关键差异

STM32L152RE复位后默认使用MSI(内部多速时钟)作为系统时钟,频率大概是2.097MHz。这一点和F103默认用HSI(8MHz)完全不同。很多人代码里直接写RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE)就以为万事大吉,但在L152RE上,USART1挂在APB2总线上,USART2和USART3挂在APB1上,时钟使能没问题,问题在于系统时钟频率不对,导致波特率算出来偏差巨大。

我实测过,用默认MSI 2.097MHz去配115200波特率,实际输出的波特率偏差超过5%,串口助手收到的全是乱码。所以第一步必须先把系统时钟切到HSE或者HSI,让主频跑到32MHz(L152RE最高支持32MHz),这样波特率计算才准确。

具体操作上,我习惯用HSE外部晶振,8MHz的晶振经过PLL倍频到32MHz。配置代码如下:

RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET); RCC_PLLConfig(RCC_PLLSource_HSE, RCC_PLLMul_4, RCC_PLLDiv_2); RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); while (RCC_GetSYSCLKSource() != 0x08);

这里有个细节:L152RE的PLL配置比F103多了一个RCC_PLLDiv_2参数,这是L系列特有的分频设置。如果你从F103抄代码,这里会编译报错,因为函数原型不一样。

2.2 波特率计算的坑:别直接用库函数默认值

系统时钟配好后,接下来是USART初始化。标准库的USART_Init函数会根据你传入的波特率和当前APB时钟自动计算BRR寄存器值。但这里有个陷阱:如果你在时钟切换之前就调用了USART_Init,它用的还是旧的时钟频率,算出来的BRR就是错的。

我的做法是先把系统时钟切好,再初始化USART。另外,L152RE的USART1在APB2上,APB2的时钟默认等于系统时钟,不分频。所以32MHz主频下,USART1的时钟就是32MHz。波特率115200对应的BRR值大约是32MHz/(16*115200)=17.36,取整后实际波特率会有微小偏差,但在容忍范围内。

如果你要用更高的波特率,比如921600,那偏差就会放大。这时候可以考虑用 oversampling 8模式,把过采样率从16降到8,BRR值翻倍,精度更高。不过大多数场景115200够用了,没必要折腾。

2.3 引脚配置:复用功能别忘开时钟

STM32L152RE的USART1默认引脚是PA9(TX)和PA10(RX)。配置GPIO的时候,除了设置复用推挽输出和浮空输入,还要记得使能GPIOA的时钟和AFIO时钟(如果用了重映射)。标准库的写法:

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_40MHz; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);

注意L系列用的是RCC_AHBPeriphClockCmd而不是F系列的RCC_APB2PeriphClockCmd来使能GPIO时钟,因为L152RE的GPIO挂在AHB总线上。这个差异如果不注意,GPIO配置不生效,串口自然没反应。

3. 轮询发送:最简单但也最容易出问题的方式

3.1 发送一个字节的完整流程

轮询发送是最基础的串口发送方式,核心就是等TXE标志位置位,然后把数据写进DR寄存器。代码看起来很简单:

void USART_SendByte(USART_TypeDef* USARTx, uint8_t data) { while(USART_GetFlagStatus(USARTx, USART_FLAG_TXE) == RESET); USART_SendData(USARTx, data); }

但这里有个隐藏问题:TXE置位只表示发送数据寄存器空了,可以写入下一个字节,并不代表上一个字节已经完整移出移位寄存器。如果你发完最后一个字节就立刻关闭串口或者进入低功耗,最后一个字节可能还没发完就被截断了。正确的做法是在发送完所有数据后,再等TC(传输完成)标志位置位:

while(USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET);

我刚开始用的时候没注意这个,发一串字符串,最后一个字符总是丢失,查了半天才发现是TC没等。

3.2 发送字符串的两种写法对比

发送字符串有两种常见写法,一种是逐个字节发送,另一种是用DMA。先说不带DMA的:

void USART_SendString(USART_TypeDef* USARTx, char* str) { while(*str) { USART_SendByte(USARTx, *str++); } while(USART_GetFlagStatus(USARTx, USART_FLAG_TC) == RESET); }

这种写法在低波特率下会阻塞CPU,比如115200波特率发100个字节,大概需要8.7ms,这期间CPU什么都干不了。如果你的系统对实时性有要求,建议用中断发送或者DMA发送。但在调试阶段,轮询发送最直观,出问题也容易定位。

3.3 实测中的意外:电平不匹配导致收不到数据

有一次我用L152RE和另一个3.3V的单片机通信,双方都是3.3V电平,按理说直接连就行。但实际测试时,L152RE能发出去,对方也能收到,但L152RE收不到对方的数据。用示波器看波形,发现对方的TX引脚在空闲时是高电平,但L152RE的RX引脚配置成了浮空输入,没有上拉,导致空闲时电平不稳定,起始位检测出错。

解决办法很简单:把RX引脚配置成上拉输入(GPIO_PuPd_UP),或者外部加一个4.7kΩ的上拉电阻。这个坑在实验室环境下可能不明显,因为杜邦线短,干扰小,但一旦线拉长或者环境复杂,问题就暴露了。

4. 中断接收:让CPU不用一直盯着串口

4.1 中断接收的配置步骤

轮询接收需要CPU不断查询RXNE标志位,效率太低。实际项目里基本都用中断接收。配置中断接收的步骤:

  1. 使能USART的接收中断:USART_ITConfig(USART1, USART_IT_RXNE, ENABLE)
  2. 配置NVIC:设置优先级并使能USART1_IRQn
  3. 在中断服务函数里读取数据并清除标志位

NVIC配置代码:

NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);

中断服务函数:

void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint8_t data = USART_ReceiveData(USART1); // 处理数据 USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }

注意USART_ClearITPendingBit这行,虽然读取DR寄存器会自动清除RXNE标志,但显式清除更保险,尤其是在中断嵌套的场景下。

4.2 接收不定长数据的处理思路

串口接收最麻烦的不是收固定长度的数据,而是收不定长数据。比如对方发一条指令,长度不固定,你怎么知道什么时候收完?常见方案有三种:

第一种是用空闲中断(IDLE)。STM32的USART支持IDLE中断,当总线空闲一个字节时间后触发。你可以在IDLE中断里判断一帧数据接收完毕。配置方法:

USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);

然后在中断里先读SR寄存器再读DR寄存器来清除IDLE标志:

if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) { USART_ReceiveData(USART1); // 清除IDLE标志 // 一帧数据接收完毕 }

第二种是用定时器做超时判断。每收到一个字节就重置定时器,如果定时器超时了还没收到新字节,就认为一帧结束。这种方式灵活但占用一个定时器资源。

第三种是协议里带长度字段或者结束符。比如约定每条指令以\n结尾,收到\n就认为一帧结束。这种方式最简单,但需要通信双方约定好协议。

我一般优先用IDLE中断,因为它不占用额外定时器,而且响应及时。但IDLE中断有个坑:如果对方发送速度很快,两个字节之间没有空闲时间,IDLE可能不会触发。所以高速通信场景下,还是得配合定时器超时来判断。

4.3 中断优先级配置的注意事项

L152RE的中断优先级分组和F系列一样,用NVIC_PriorityGroupConfig设置。我一般用分组2,2位抢占优先级,2位响应优先级。串口中断的抢占优先级不要设太高,除非你的串口通信是系统最核心的功能。设太高会影响其他中断的响应,比如定时器中断被延迟,导致系统时序错乱。

另外,如果你同时用了USART1和USART2,两个中断服务函数的优先级要合理分配。如果USART1的数据需要立刻处理,USART2的数据可以稍后处理,那就把USART1的抢占优先级设高一点。

5. 低功耗模式下的串口唤醒:L系列的主场

5.1 STOP模式下串口还能收数据吗

STM32L152RE主打低功耗,STOP模式下功耗可以降到微安级别。但STOP模式下系统时钟停了,USART还能工作吗?答案是:USART1可以在STOP模式下通过RX引脚唤醒MCU,但前提是配置了正确的唤醒源。

具体来说,你需要把USART1的RX引脚配置为唤醒源,并且使能USART的唤醒中断。在STOP模式下,USART1的时钟被关闭,但它可以检测RX引脚上的起始位,如果检测到起始位,会触发唤醒事件,把MCU从STOP模式唤醒。唤醒后,USART1需要重新初始化才能正常接收数据。

配置步骤大致如下:

// 使能USART1的唤醒功能 USART_WakeUpConfig(USART1, USART_WakeUp_AddressBit); // 进入STOP模式 PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); // 唤醒后重新配置时钟和USART

这里有个关键点:唤醒后系统时钟会切回MSI,你需要重新配置时钟到HSE,并且重新初始化USART。如果忘了这一步,串口收到的数据全是乱码。

5.2 低功耗串口LPUART和普通USART的区别

STM32L152RE没有LPUART,LPUART是L4系列才有的。但L152RE的USART1在低功耗模式下有特殊待遇,可以在STOP模式下保持唤醒能力。USART2和USART3就不行,它们在STOP模式下完全停止工作。

所以如果你的项目需要在低功耗模式下接收串口数据,必须用USART1。这一点在硬件设计阶段就要确定好,别等到PCB打样回来才发现USART2接在了主通信口上,那就只能改板了。

5.3 实测功耗数据与唤醒延迟

我实测过L152RE在STOP模式下的功耗,用USART1做唤醒源,功耗大概在1.2μA左右(不含外部电路)。唤醒延迟大概在10μs到20μs之间,具体取决于时钟恢复时间。如果你对唤醒延迟有要求,可以在唤醒后先用MSI跑,等数据接收完再切到HSE,这样唤醒更快。

但要注意,MSI的频率精度不如HSE,如果用MSI接收数据,波特率偏差可能比较大。所以我的做法是:唤醒后立刻切HSE,等HSE稳定后再接收数据。虽然多花了几微秒,但数据可靠性有保障。

6. 调试串口时那些让人抓狂的怪问题

6.1 串口助手收到乱码的排查顺序

乱码是串口调试中最常见的问题,排查顺序应该是:先查波特率,再查时钟,最后查电平。

波特率不匹配是最常见的乱码原因。比如你代码里设的是115200,串口助手设的是9600,那收到的全是乱码。但有时候波特率设的一样还是乱码,那就要查系统时钟了。用示波器测一下TX引脚上的波形,算一下实际波特率。如果实际波特率和设定值偏差超过3%,那基本就是时钟配置有问题。

电平问题也会导致乱码。比如你的MCU是3.3V电平,但串口模块是5V电平,虽然有些模块兼容3.3V,但长期工作可能不稳定。用万用表量一下TX引脚的空闲电平,正常应该是3.3V左右。如果只有1.8V或者更低,那可能是引脚配置成了开漏输出但没加上拉电阻。

6.2 发送正常但接收不到数据的几种可能

发送正常说明TX引脚和时钟配置没问题,接收不到数据就要查RX引脚。常见原因有:

  • RX引脚配置错误:应该配成浮空输入或上拉输入,如果配成了推挽输出,那肯定收不到数据。
  • 中断没使能:如果你用中断接收,检查NVIC和USART的接收中断是否都使能了。
  • 波特率偏差太大:发送对波特率偏差的容忍度比接收高,有时候发送能正常工作,但接收因为偏差累积导致采样错误。
  • 硬件连接问题:TX和RX有没有接反?共地了没有?这些低级错误我见过太多次了。

6.3 用示波器抓波形的实用技巧

示波器是调试串口的利器。抓波形的时候,触发方式设为下降沿触发,因为串口的起始位是下降沿。时基设成100μs/div左右,能看清一个字节的波形。测量波特率的时候,量起始位下降沿到第一个数据位上升沿的时间,取倒数就是波特率。

如果波形上有毛刺或者振铃,那可能是线太长或者阻抗不匹配。短线(小于20cm)一般没问题,长线建议用双绞线或者加匹配电阻。我在一个项目里用了一米长的杜邦线,波形惨不忍睹,换成屏蔽线后立刻正常。

7. 从轮询到DMA:什么时候该升级发送方式

7.1 DMA发送的配置要点

轮询发送在数据量小的时候没问题,但如果要发几百个字节,CPU被占用几毫秒甚至几十毫秒,系统响应就会变慢。这时候就该上DMA了。L152RE的USART1支持DMA发送,配置步骤:

  1. 使能DMA时钟:RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE)
  2. 配置DMA通道:源地址是数据缓冲区,目标地址是USART1的DR寄存器
  3. 使能USART的DMA发送请求:USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE)

DMA配置代码:

DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)txBuffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize = len; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; DMA_Init(DMA1_Channel4, &DMA_InitStructure); DMA_Cmd(DMA1_Channel4, ENABLE);

注意USART1的TX对应DMA1_Channel4,RX对应DMA1_Channel5。这个对应关系在参考手册里有,别搞错了。

7.2 DMA发送完成中断的处理

DMA发送完成后会触发传输完成中断,你可以在中断里做一些收尾工作,比如切换RS485的方向引脚,或者启动下一次发送。配置DMA传输完成中断:

DMA_ITConfig(DMA1_Channel4, DMA_IT_TC, ENABLE);

然后在DMA1_Channel4_IRQHandler里清除标志位并处理。这里有个坑:DMA的传输完成中断标志需要先读TCIF再写0清除,和普通外设的中断清除方式不太一样。

7.3 轮询、中断、DMA三种方式的选型建议

方式CPU占用适用场景缺点
轮询高调试、少量数据阻塞CPU,实时性差
中断中中等数据量、不定长接收频繁中断影响系统
DMA低大量数据、高速通信配置复杂,占用DMA通道

我的建议是:调试阶段用轮询,快速验证硬件和时钟配置。产品阶段接收用中断+IDLE,发送用DMA。这样兼顾了实时性和CPU效率。

8. 写在最后:几个让我少走弯路的习惯

串口调试这件事,说难不难,说简单也不简单。我踩过的坑里,大部分都是因为"想当然"——想当然以为F系列的代码能直接用在L系列上,想当然以为波特率设对了就没问题,想当然以为电平匹配就能通信。后来我养成了几个习惯,分享出来供参考。

第一个习惯是每次换芯片型号,先花十分钟看参考手册里的时钟树和引脚复用表。别小看这十分钟,能省下后面几个小时的调试时间。第二个习惯是手边常备一个USB转串口模块和示波器,出问题先看波形再查代码,比盯着代码瞎猜高效得多。第三个习惯是每写一个串口驱动,先写一个最简单的回环测试——TX接RX,发什么收什么,确认底层没问题了再接外部设备。

STM32L152RE的串口功能不算复杂,但低功耗特性和时钟配置的差异让它和F系列用起来手感不一样。把时钟树理清楚,把中断和DMA配好,剩下的就是调协议和优化体验了。希望这篇内容能帮你少踩几个坑,顺利把串口跑通。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询