1. 项目简介
1.1 实验目标
本实验在 NUCLEO-C542RC 开发板上,使用 LPUART1 通过板载虚拟串口,以 DMA 方式连续发送 JustFloat 格式的锯齿波数据,并在 VOFA+ 中显示波形。数据格式以 vofa_justfloat.md 为准。
功能需求:
- 使用 LPUART1,115200 波特率、8 数据位、1 停止位、无校验,通过板载 ST-LINK 虚拟串口发送数据;
- 使用 DMA 发送,每帧包含 1 个浮点采样值和固定帧尾,共 8 字节;
- 锯齿波数据每 10 ms 提交一帧,周期约 1 秒;
- 串口数据流只包含 JustFloat 数据帧,不插入文本、换行或图片数据;
- 在发送过程中不得改写 DMA 正在读取的缓冲区。
开发板和 MCU 型号: NUCLEO-C542RC 开发板,搭载 STM32C542RCT6 MCU。
<br>
1.2 实验条件
硬件:
- NUCLEO-C542RC 开发板
- 支持数据传输的 USB Type-C 线(用于连接 ST-LINK 接口,同时提供下载、调试和虚拟串口)
软件:
- STM32CubeMX2(用于引脚、时钟、LPUART1 和 DMA 配置与代码生成)
- VS Code + STM32Cube 插件(用于编译、烧录、调试)
- VOFA+(用于串口数据接收与波形显示)
<br>
1.3 VOFA+ JustFloat 协议说明
VOFA+ 支持多种数据引擎,JustFloat 是面向波形显示的二进制协议。其帧结构为:N 个 32 位小端浮点数 + 4 字节固定帧尾 0x00 0x00 0x80 0x7F。VOFA+ 依靠帧尾识别一帧结束,按帧长解析后绘制各通道波形。
JustFloat 的数据帧由两部分组成:
struct Frame {
float ch_data[CH_COUNT]; // 浮点通道数据(小端)
unsigned char tail[4]; // 固定帧尾
};
- ch_data 为浮点型通道数据,采用小端字节序。STM32 默认即为小端,可直接通过内存拷贝写入。
- CH_COUNT 为每帧的通道数量。
- tail 为固定帧尾,内容为 0x00 0x00 0x80 0x7F,对应 +Infinity 的小端表示。VOFA+ 依靠帧尾识别一帧的结束,因此帧尾必须完整、不能缺失。
本实验每帧发送 1 路锯齿波数据,CH_COUNT = 1,每帧共 8 字节:前 4 字节为当前采样值的 float 小端表示,后 4 字节为固定帧尾。串口数据流中只包含 JustFloat 数据帧,不插入文本、换行或调试打印。VOFA+ 侧串口参数需与 MCU 一致(115200、8N1、无流控),数据引擎选择 JustFloat。
<br>
2. 硬件说明
2.1 NUCLEO-C542RC 开发板
NUCLEO-C542RC 是 ST 基于 STM32C542RCT6 推出的 Nucleo-64 开发板。主要特性:
- MCU:STM32C542RCT6,Arm Cortex-M33 内核,最高主频 144 MHz,256 KB Flash,64 KB RAM
- 调试器:板载 STLINK-V3EC,支持大容量存储、虚拟串口和调试端口三种功能
- 用户 LED:LD1,连接 PA5
- 用户按键:B1,连接 PC13
- 扩展接口:Arduino Uno V3 和 ST morpho 连接器
本实验通过板载 ST-LINK 的虚拟串口(VCP)将 LPUART1 的数据传输到 PC,无需额外外接 USB 转串口模块。一根 USB 线同时完成供电、下载、调试和串口通信。
<br>
2.2 引脚说明
| 引脚 |
复用功能 |
Swap 后实际功能 |
说明 |
| PA2 |
LPUART1_RX(AF3) |
LPUART1_TX |
连接 ST-LINK VCP 的 RX |
| PA3 |
LPUART1_TX(AF3) |
LPUART1_RX |
连接 ST-LINK VCP 的 TX |
PA2、PA3:配置为 LPUART1 复用功能(AF3),并启用 Tx/Rx Pins swapping。引脚复用配置本身不变,通过外设内部交换完成信号路由。
STM32C542RCT6 的 LPUART1 默认复用功能(AF3)映射为 PA2=RX、PA3=TX。而 NUCLEO-C542RC 板载 ST-LINK 的虚拟串口要求 MCU 侧的 TX 引脚连接到 PA2。默认映射与板载 VCP 连线不匹配,因此必须启用 LPUART1 的 Tx and Rx Pins swapping 功能。交换后,PA2 实际输出 LPUART1_TX,PA3 实际接收 LPUART1_RX,与板载 VCP 正确对接。生成代码中的引脚注释仍显示原始复用名称,这是正常现象。
<br>
2.3 DMA 资源分配
| 资源 |
配置 |
说明 |
| LPDMA1_CH0 |
LPUART1_TX DMA |
内存到外设,源地址递增,目标地址固定,字节宽度 |
| LPDMA1_CH1 |
LPUART1_RX DMA |
已配置,本实验未启动接收 |
<br>
3. STM32CubeMX2 工程配置
3.1 新建工程
本实验采用 MCU 方式创建工程,即以芯片型号 STM32C542RC 为起点,而非开发板方式。
- 进入 MCU 选择界面
启动 STM32CubeMX2,在首页点击 MCU,进入芯片型号选择列表。
- 搜索并选中芯片
在 MCU name 输入框中输入 STM32C542RC(或完整型号 STM32C542RCT6),从筛选出的列表中选择对应型号。
- 填写项目信息
输入项目名称 LPUART_DMA 和项目保存路径。项目名称避免使用特殊字符,路径建议使用纯英文目录。

<br>
3.2 基础配置
工程创建后,软件自动进入 Pinout 视图。
时钟配置:本工程系统时钟配置为 144 MHz,LPUART1 时钟源为 APB3,频率 144 MHz,串口预分频系数为 1。

<br>
3.3 LPUART1 配置
(1)启用 LPUART1
在左侧外设列表中找到 Connectivity → LPUART1 ,激活(Active)LPUART1。
在 Main features 中,将 Mode 设置为 Asynchronous(异步模式)。设置完成后,Pinout 视图中 PA2 自动分配为 LPUART1_RX,PA3 自动分配为 LPUART1_TX(均为 AF3 复用功能)。


(2)LPUART1 参数设置
在 Main features 中设置:
| 参数 |
值 |
| Baud Rate |
115200 Bits/s |
| Word Length |
8 Bits (including Parity) |
| Parity |
None |
| Stop Bits |
1 |
| Data Direction |
Receive and Transmit |
| Hardware Flow Control |
None |
| Over Sampling |
16 |
<br>

<br>
(3)启用 TX/RX 引脚交换
在 Advanced Features 中,将 Tx and Rx Pins swapping 设置为 Enabled。
STM32C542RCT6 的 LPUART1 默认 AF3 映射为 PA2=RX、PA3=TX,而 NUCLEO-C542RC 板载 ST-LINK 的虚拟串口要求 MCU 侧的 TX 连接到 PA2。启用 Swap 后,外设内部交换 TX/RX 信号路由,使 PA2 实际输出 TX、PA3 实际接收 RX,与板载 VCP 正确对接。引脚复用配置(AF3)本身不变。

<br>
(4)配置 DMA Settings
在 DMA Settings 中,点击 DMA Tx 后的 “Enable” 开关 激活 TX DMA 请求,CubeMX2 会自动将该请求分配到 LPDMA1_CH0。
| 参数 |
值 |
| Request Tx |
LPUART1_CH0 |
| Mode |
Direct transfer |
| Peripheral Request |
LPUART1_Tx_dma |
| Direction |
Memory To Peripheral |
| Source Data Width |
Byte |
| Destination Data Width |
Byte |
| Priority |
Low |
如需接收功能,可选择 LPUART1_Rx,方向设为 Peripheral To Memory。

<br>
(5)使能中断
设置如下中断使能:
- LPUART1 global interrupt
- LPDMA1_CH0 interrupt(TX DMA 完成中断)
TX DMA 完成中断是 HAL_UART_TxCpltCallback 被调用的前提。
<br>
3.4 项目配置
- 点击 Project Settings,进入工程设置页面。
- 在 IDE Format 下拉选项中选择 CMake。
- 在 CMake Toolchain 下拉选项中选择 GCC。
- 确认配置无误后,点击 Generate Code。CubeMX2 会执行配置检查,如果没有错误,开始在指定路径下生成 CMake 工程文件。

<br>
4. 编程实现
4.1 实现思路
采用 “主循环定时调度 + DMA 单次发送 + 静态帧缓冲区” 的方案:
mx_system_init() // CubeMX2 生成:时钟、GPIO、LPUART1、DMA 初始化
│
▼
主循环 wave_process()
│
├─ 检查是否有发送错误待恢复
│ └─ 是 → HAL_UART_AbortTransmit(),等待下一个周期
│
├─ 距上次提交 < 10 ms?或 UART TX 状态非空闲?
│ └─ 是 → 直接返回
│
├─ 将当前采样值写入帧缓冲区前 4 字节(memcpy,小端)
│
├─ HAL_UART_Transmit_DMA() 提交一帧(8 字节)
│ ├─ HAL_OK → 采样序号加一,超过 99 归零
│ ├─ HAL_BUSY → 本次跳过,不计错误
│ └─ 其他 → 记录错误,置恢复标志
│
└─ 更新上次提交时刻
DMA 与阻塞发送的对比:HAL_UART_Transmit() 会逐字节等待发送寄存器空,115200 波特率下一帧 8 字节约需 0.7 ms,期间 CPU 无法执行其他任务。DMA 由硬件搬运数据,CPU 只需调用一次启动函数即可返回。
单帧提交与 DMA 循环模式:DMA 设为 Direct transfer(即 Normal 模式),每次传输完成后停止。主循环按 10 ms 周期重复提交,每帧的采样值都可以更新。循环模式下 DMA 会反复发送同一个缓冲区,波形无法变化。
提交前检查 TX 状态:HAL_UART_Transmit_DMA() 在上一帧尚未发完时调用会返回 HAL_BUSY。通过 HAL_UART_GetTxState() 判断为空闲再提交,避免无意义的忙状态返回,调度逻辑更清晰。
静态帧缓冲区:DMA 在后台读取内存。缓冲区如果定义为栈上的局部变量,函数返回后内存可能被覆盖,导致发送数据错乱。静态存储区在整个程序生命周期内有效,DMA 读取期间不会被回收。
时间调度使用 HAL_GetTick() 差值:(now - last) >= 10 的写法在毫秒计数器回绕时仍然正确,且是非阻塞的,主循环不会被延时卡住。
关键 HAL 函数:
| 步骤 |
HAL 函数 |
作用 |
| 获取 LPUART1 句柄 |
mx_lpuart1_uart_gethandle() |
返回 CubeMX2 生成的 LPUART1 句柄 |
| 查询 TX 状态 |
HAL_UART_GetTxState() |
判断上一帧是否发送完成 |
| 启动 DMA 发送 |
HAL_UART_Transmit_DMA() |
提交一帧数据,立即返回 |
| 中止发送 |
HAL_UART_AbortTransmit() |
出错时停止 DMA 并清理状态 |
| 获取毫秒时基 |
HAL_GetTick() |
10 ms 周期调度 |
<br>
4.2 关键代码
在 VS Code 中打开生成的 CMake 工程,找到 Core/Src/main.c,添加以下代码。
(1)文件头部添加头文件与宏定义
#include "main.h"
#include "mx_lpuart1.h"
#include <string.h>
/* Private define ------------------------------------------------------------*/
#define WAVE_SAMPLE_INTERVAL_MS 10U
#define WAVE_SAMPLES_PER_PERIOD 100U
#define JUSTFLOAT_FRAME_SIZE 8U
_Static_assert(sizeof(float) == 4U, "JustFloat requires a 32-bit float");
(2)静态帧缓冲区与统计变量
/* Private variables ---------------------------------------------------------*/
static uint8_t wave_tx_frame[JUSTFLOAT_FRAME_SIZE] =
{
0U, 0U, 0U, 0U, 0x00U, 0x00U, 0x80U, 0x7FU
};
static volatile uint32_t wave_tx_error_pending;
static volatile uint32_t wave_tx_completed_count;
static volatile uint32_t wave_tx_error_count;
/* Private functions prototype -----------------------------------------------*/
static void wave_process(void);
(3)主函数
int main(void)
{
if (mx_system_init() != SYSTEM_OK)
{
return (-1);
}
else
{
while (1)
{
wave_process();
}
}
}
(4)波形发送调度函数
static void wave_process(void)
{
static uint32_t last_sample_tick;
static uint32_t sample_index;
hal_uart_handle_t *huart = mx_lpuart1_uart_gethandle();
const uint32_t now = HAL_GetTick();
if (wave_tx_error_pending != 0U)
{
/* 出错后在中断外恢复:先停止 DMA,再等待下一个周期 */
wave_tx_error_pending = 0U;
if (HAL_UART_AbortTransmit(huart) != HAL_OK)
{
wave_tx_error_pending = 1U;
}
last_sample_tick = now;
return;
}
if (((uint32_t)(now - last_sample_tick) < WAVE_SAMPLE_INTERVAL_MS) ||
(HAL_UART_GetTxState(huart) != HAL_UART_TX_STATE_IDLE))
{
return;
}
/* STM32 为小端,float 直接拷贝到前 4 字节,后 4 字节帧尾保持不变 */
const float sample = (float)sample_index;
memcpy(wave_tx_frame, &sample, sizeof(sample));
const hal_status_t status = HAL_UART_Transmit_DMA(huart, wave_tx_frame,
sizeof(wave_tx_frame));
last_sample_tick = now;
if (status == HAL_OK)
{
sample_index++;
if (sample_index >= WAVE_SAMPLES_PER_PERIOD)
{
sample_index = 0U;
}
}
else if (status != HAL_BUSY)
{
wave_tx_error_count++;
wave_tx_error_pending = 1U;
}
}
(5)UART 发送完成与错误回调
void HAL_UART_TxCpltCallback(hal_uart_handle_t *huart)
{
if (huart == mx_lpuart1_uart_gethandle())
{
wave_tx_completed_count++;
}
}
void HAL_UART_ErrorCallback(hal_uart_handle_t *huart)
{
if (huart == mx_lpuart1_uart_gethandle())
{
wave_tx_error_count++;
wave_tx_error_pending = 1U;
}
}
代码说明:
wave_tx_frame 为静态数组,DMA 读取期间始终有效。前 4 字节每次发送前更新,后 4 字节 0x00 0x00 0x80 0x7F 为 JustFloat 固定帧尾。
memcpy 将 float 写入字节数组,避免指针强制转换的别名问题,也避免结构体填充影响帧长。
HAL_UART_GetTxState() 返回 HAL_UART_TX_STATE_IDLE 表示上一帧已发送完成,可以提交下一帧。
HAL_UART_Transmit_DMA() 返回 HAL_OK 只表示启动成功,不表示数据已发完;真正完成由 TX DMA 中断触发 HAL_UART_TxCpltCallback。
HAL_UART_ErrorCallback 在串口出错时被调用,置位 wave_tx_error_pending,主循环在下一轮调用 HAL_UART_AbortTransmit() 恢复。
- 采样序号从 0 递增到 99 后归零,配合 10 ms 周期,锯齿波周期约为 1 秒。
<br>
5. 编译烧录与运行测试
5.1 编译与烧录
- 打开工程:用 VS Code 打开 CubeMX2 生成的 CMake 工程文件夹(如
LPUART_DMA_cmake)。
- 编译:点击 VS Code 底部状态栏的 Build 按钮,或使用命令面板执行 CMake 构建。编译完成后,终端应输出
Build finished with exit code 0,无 error。
- 连接开发板:用支持数据传输的 USB Type-C 线连接 NUCLEO-C542RC 的 ST-LINK USB 口(CN1)。
- 确认设备识别:打开设备管理器,在“端口 (COM 和 LPT)”下应能看到 STLink COMx。
- 烧录:点击 VS Code 中的 Flash 按钮,将固件下载到开发板。

<br>
5.2 运行与测试
烧录完成后,点击 VS Code 中的 Run 按钮,程序开始运行。
(1)VOFA+ 连接设置
打开 VOFA+,在串口连接区域选择与开发板对应的 STLink COMx。
串口参数设置为:115200、8 数据位、1 停止位、无校验、无流控;数据引擎选择 JustFloat。
(2)波形显示
烧录完成后,点击 VS Code 中的 Run 按钮,程序开始运行。
打开 VOFA+,选择对应的 STLink COM 端口,串口参数设为 115200、8 数据位、1 停止位、无校验、无流控,数据引擎选择 JustFloat,打开串口连接并添加 1 个波形通道。观察波形,应显示连续、无断裂的锯齿波,数值从 0 递增到 99 后回落,周期约 1 秒。
编译、烧录、运行及 VOFA+ 波形显示均正常,符合预期。

<br>
6. 小结
本实验通过 LPUART1 和 DMA 实现了 JustFloat 格式锯齿波数据的连续发送,并在 VOFA+ 中成功显示波形,掌握了 STM32C5 系列 LPUART1 的 DMA 发送配置与使用方法。
核心逻辑可以概括为:“主循环定时调度 → 封装 JustFloat 帧 → DMA 单次发送 → 中断通知完成”。
需要注意的是:
- TX/RX 引脚交换。 STM32C542RCT6 的 LPUART1 默认 AF3 映射为 PA2=RX、PA3=TX,而板载 ST-LINK 虚拟串口要求 MCU 侧的 TX 连接到 PA2。必须在 CubeMX2 中启用 Tx and Rx Pins swapping,否则数据无法通过虚拟串口发送到 PC。
- DMA 模式与缓冲区。 DMA 设为 Direct transfer(Normal 模式),每帧发送完成后停止,由主循环按 10 ms 周期重复提交。帧缓冲区使用静态存储,保证 DMA 读取期间始终有效。
<br>
版权声明:
Copyright by [email]ai4mcu@qq.com[/email] 2026
Crated:2026-10