PUYA USART — 串口轮询收发(LL 库)

PUYA PY32F0xx_Firmware V1.5.0 USART 串口轮询收发(USART_HyperTerminal_Polling_Init)示例教学(LL 库版本)

USART — 串口轮询收发(LL 库)

HAL vs LL:PUYA 固件库提供 HAL 和 LL 两套 API。

  • HAL 库:封装度高,可移植性强,适合快速开发
  • LL 库:接近寄存器操作,代码精简效率高,适合性能敏感场景 本文使用 LL 库版本。

固件库: PY32F0xx_Firmware V1.5.0
芯片: PY32F030(PY32F002A / F003 同库可参考)
源文件: PUYA/PY32F0xx_Firmware_V1.5.0/Projects/PY32F030-STK/Example_LL/USART/USART_HyperTerminal_Polling_Init/Src/main.c


功能简介

本示例实现串口“超级终端”式的轮询(Polling)通信:上电后先向电脑发送一段欢迎语,再接收你从键盘敲入的 12 个字符,原样回传,最后点亮 LED 表示测试通过。

与 HAL 版(用 HAL_UART_Transmit / HAL_UART_Receive 一行搞定)不同,LL 版没有封装好的“发 N 字节”函数,需要你自己用 LL_USART_TransmitData8() 逐字节发送,并用 LL_USART_IsActiveFlag_TXE() / LL_USART_IsActiveFlag_RXNE() 轮询标志位。这正是 LL 的典型特征:给你最底层的积木,组合方式由你决定

硬件准备

  • PY32F030 开发板
  • USB 转串口线(或板载 ST-Link VCP),连接到 USART1 的 TX(PA2) / RX(PA3)
  • 电脑端串口助手(如 PuTTY、串口调试助手),波特率 9600 / 8N1
  • 板载绿 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
 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
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#include "main.h"
#include "py32f030xx_ll_Start_Kit.h"

#define COUNTOF(__BUFFER__)   (sizeof(__BUFFER__) / sizeof(*(__BUFFER__)))
#define TXSTARTMESSAGESIZE    (COUNTOF(aTxStartMessage) - 1)
#define TXENDMESSAGESIZE      (COUNTOF(aTxEndMessage) - 1)

uint8_t aRxBuffer[12] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
uint8_t aTxStartMessage[] = "\n\r USART Hyperterminal communication based on polling \n\r Enter 12 characters using keyboard :\n\r";
uint8_t aTxEndMessage[] = "\n\r Example Finished\n\r";

uint8_t *TxBuff = NULL;
__IO uint16_t TxCount = 0;
uint8_t *RxBuff = NULL;
__IO uint16_t RxCount = 0;

static void APP_SystemClockConfig(void);
static void APP_ConfigUsart(USART_TypeDef *USARTx);
static void APP_UsartTransmit(USART_TypeDef *USARTx, uint8_t *pData, uint16_t Size);
static void APP_UsartReceive(USART_TypeDef *USARTx, uint8_t *pData, uint16_t Size);

int main(void)
{
  APP_SystemClockConfig();

  BSP_LED_Init(LED_GREEN);          /* 初始化 LED,用于测试通过指示 */

  APP_ConfigUsart(USART1);          /* 配置 USART1:PA2/PA3,9600-8N1 */

  /* 发送欢迎语 */
  APP_UsartTransmit(USART1, (uint8_t*)aTxStartMessage, TXSTARTMESSAGESIZE);
  /* 接收 12 字节 */
  APP_UsartReceive(USART1, (uint8_t *)aRxBuffer, 12);
  /* 回传接收到的数据 */
  APP_UsartTransmit(USART1, (uint8_t*)aRxBuffer, 12);
  /* 发送结束语 */
  APP_UsartTransmit(USART1, (uint8_t*)aTxEndMessage, TXENDMESSAGESIZE);

  BSP_LED_On(LED_GREEN);            /* 测试通过,亮灯 */
  while (1) {}
}

/**
  * @brief  系统时钟配置(LL 版,手动配 HSI + 时基)
  */
static void APP_SystemClockConfig(void)
{
  LL_RCC_HSI_Enable();
  while (LL_RCC_HSI_IsReady() != 1) {}
  LL_RCC_SetAHBPrescaler(LL_RCC_SYSCLK_DIV_1);
  LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_HSISYS);
  while (LL_RCC_GetSysClkSource() != LL_RCC_SYS_CLKSOURCE_STATUS_HSISYS) {}
  LL_RCC_SetAPB1Prescaler(LL_RCC_APB1_DIV_1);
  LL_Init1msTick(8000000);
  LL_SetSystemCoreClock(8000000);
}

/**
  * @brief  USART 配置:GPIO 复用 + 波特率等参数
  */
static void APP_ConfigUsart(USART_TypeDef *USARTx)
{
  if (USARTx == USART1)
  {
    LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);          /* 开 GPIOA 时钟 */
    LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_USART1);       /* 开 USART1 时钟 */

    LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin        = LL_GPIO_PIN_2;                 /* PA2 = TX */
    GPIO_InitStruct.Mode       = LL_GPIO_MODE_ALTERNATE;        /* 复用功能 */
    GPIO_InitStruct.Speed      = LL_GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
    GPIO_InitStruct.Pull       = LL_GPIO_PULL_UP;
    GPIO_InitStruct.Alternate  = LL_GPIO_AF1_USART1;            /* 复用为 USART1 */
    LL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = LL_GPIO_PIN_3;                       /* PA3 = RX */
    LL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  }
  /* 若用 USART2,结构类似,复用 AF4,此处略 */

  /* USART 参数 */
  LL_USART_InitTypeDef USART_InitStruct = {0};
  USART_InitStruct.BaudRate            = 9600;
  USART_InitStruct.DataWidth           = LL_USART_DATAWIDTH_8B;
  USART_InitStruct.StopBits            = LL_USART_STOPBITS_1;
  USART_InitStruct.Parity              = LL_USART_PARITY_NONE;
  USART_InitStruct.TransferDirection   = LL_USART_DIRECTION_TX_RX;
  USART_InitStruct.HardwareFlowControl = LL_USART_HWCONTROL_NONE;
  USART_InitStruct.OverSampling        = LL_USART_OVERSAMPLING_16;
  LL_USART_Init(USARTx, &USART_InitStruct);

  LL_USART_ConfigAsyncMode(USARTx);     /* 配置为全双工异步模式 */
  LL_USART_Enable(USARTx);              /* 使能 USART 模块 */
}

/**
  * @brief  LL 轮询发送:逐字节等待 TXE 后写入
  */
static void APP_UsartTransmit(USART_TypeDef *USARTx, uint8_t *pData, uint16_t Size)
{
  TxBuff  = pData;
  TxCount = Size;
  while (TxCount > 0)
  {
    while (LL_USART_IsActiveFlag_TXE(USARTx) != 1);   /* 等待发送数据寄存器空 */
    LL_USART_TransmitData8(USARTx, *TxBuff);          /* 写入一个字节 */
    TxBuff++;
    TxCount--;
  }
  while (LL_USART_IsActiveFlag_TC(USARTx) != 1);      /* 等待发送完成 */
}

/**
  * @brief  LL 轮询接收:逐字节等待 RXNE 后读取
  */
static void APP_UsartReceive(USART_TypeDef *USARTx, uint8_t *pData, uint16_t Size)
{
  RxBuff  = pData;
  RxCount = Size;
  while (RxCount > 0)
  {
    while (LL_USART_IsActiveFlag_RXNE(USARTx) != 1);  /* 等待接收数据寄存器非空 */
    *RxBuff = LL_USART_ReceiveData8(USARTx);          /* 读出一个字节 */
    RxBuff++;
    RxCount--;
  }
}

void APP_ErrorHandler(void)
{
  while (1) {}
}

代码分段讲解

1. 时钟与 GPIO 复用

LL 里 GPIO 和 USART 的时钟分属不同的“组”:GPIO 在 LL_IOP_GRP1,USART1 在 LL_APB1_GRP2。这和 HAL 的 __HAL_RCC_xxx_CLK_ENABLE() 宏对应,但名字更直白地反映了总线分组。PA2/PA3 要设为 LL_GPIO_MODE_ALTERNATE 并指定 Alternate = LL_GPIO_AF1_USART1,才能把引脚接到 USART1。

2. 参数配置:结构体 + Init

LL 的 LL_USART_Init() 同样接收结构体,这一点和 HAL 的 UART_InitTypeDef 很像——LL 并不是完全不用结构体,而是把“一次性参数”用结构体批量写入,把“运行时动作”拆成独立函数

3. 发送:手动轮询 TXE

HAL 一句 HAL_UART_Transmit() 内部帮你做了“等待 TXE→写 DR→等待 TC”的循环;LL 把这段循环直接暴露给你:

1
2
while (LL_USART_IsActiveFlag_TXE(USARTx) != 1);  /* 等空 */
LL_USART_TransmitData8(USARTx, byte);            /* 写一字节 */

好处是你可以精确控制发送节奏、插入其它操作,甚至做“发送一半去干别的事”这类 HAL 难以表达的逻辑。

4. 接收:手动轮询 RXNE

同理,接收就是等 RXNE(接收寄存器非空)后 LL_USART_ReceiveData8() 读一个字节。注意 LL 没有“超时”概念,若对方不发数据,这里的 while永远等下去——真实项目务必加超时或改用中断/DMA。

实验现象

  1. 打开串口助手(9600 / 8N1),复位开发板。
  2. 先收到欢迎语:USART Hyperterminal communication based on polling ... Enter 12 characters ...
  3. 在串口助手输入恰好 12 个字符并发送,开发板原样回传这 12 个字符。
  4. 最后收到 Example Finished,板载绿 LED 点亮,表示流程成功。

常见问题

  1. 串口助手收不到任何字符?

    • 确认 TX/RX 交叉连接(板子 TX 接 USB 转串口的 RX)。
    • 确认波特率 9600、8 位数据、1 停止位、无校验、无流控,与代码一致。
    • 确认 APP_SystemClockConfig() 里系统时钟确实是 8MHz(LL_Init1msTick(8000000)),否则波特率实际值会偏离。
  2. 输入不足 12 字符程序就“卡住”?

    • LL 轮询接收没有超时,必须收满 12 字节才继续。这是示例故意为之;真实项目建议加超时或改用中断。
  3. 想重定向 printf?

    • LL 版需要自己实现 _write() / fputc() 并在里面调用 LL_USART_TransmitData8();HAL 版则常配合 HAL_UART_Transmit 做重定向(参见 HAL 版文档)。

小结

  • LL 串口收发 = 结构体配参数 + 独立函数做动作 + 手动轮询标志位
  • 相比 HAL_UART_Transmit/Receive,LL 代码更长,但每一步都看得见、改得动,没有隐藏的超时/状态机。
  • 性能敏感或资源紧张时,LL 能省下 HAL 那一层封装开销。

拓展应用

  • 给轮询接收加超时计数,避免“永远等待”。
  • 改用 LL_USART_EnableIT_RXNE() + 中断回调函数,实现不阻塞的串口接收(参考 USART_HyperTerminal_IT_Init)。
  • 用 DMA 搬运大量数据(参考 USART_HyperTerminal_DMA_Init),CPU 几乎零参与。
  • 把发送函数封装成 printf 风格的可变参数函数,方便打日志。
世界是你们
使用 Hugo 构建
主题 StackJimmy 设计