STM_ATEM/4KHz_UART_Bottleneck_Analysis.md
zhoujie 28b4dc6af1 feat(系统): 新增4KHz采样率串口瓶颈分析及优化方案文档
- 新增《4KHz_UART_Bottleneck_Analysis.md》详细分析文档,识别阻塞式串口发送为主要瓶颈
- 新增《Code_Optimization_Summary.md》代码修改总结文档,记录优化实施细节
- 新增《Final_Solution_Explanation.md》最终方案说明文档,阐述中断+DMA非阻塞发送的最优方案

🐛 fix(串口驱动): 将RS485驱动改为DMA非阻塞发送

- 修改`User/rs485_driver.c`中的`RS485_SendData`函数,使用`HAL_UART_Transmit_DMA`替代阻塞式发送
- 启用`RS485_TxCpltCallback`回调函数,在DMA传输完成后自动切换回接收模式并清除忙标志
- 添加忙状态检查机制,防止上一次传输未完成时启动新传输

♻️ refactor(主循环): 优化定时器中断处理策略并启用串口输出

- 修改`Core/Src/main.c`中的定时器配置,将TIM2周期从999调整为124,提高中断频率至8KHz
- 简化`HAL_TIM_PeriodElapsedCallback`中断处理函数,取消循环处理多个数据包的逻辑
- 启用串口数据输出模式(`DATA_OUTPUT_MODE_UART=1`),禁用存储卡模式以降低负载

📝 docs(配置): 更新IOC配置文件和监控文件路径

- 更新`STM_ATEM_F405.ioc`中的TIM2配置,同步定时器周期修改
- 修改`User/system_monitor.h`中的监控状态文件路径,从"MONITOR.TXT"改为"LOG.TXT"
2026-02-07 14:04:36 +08:00

442 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 4KHz采样率下串口输出数据瓶颈详细分析
## 1. 问题概述
在4KHz采样率下系统串口输出数据来不及导致数据丢失或溢出。本文档详细分析瓶颈所在。
---
## 2. 系统数据流分析
### 2.1 数据产生速率
- **采样率**: 4000 samples/sec (每个样本包含3通道ADC数据)
- **采样周期**: 250μs/sample
- **数据包大小**:
- 原始数据包 (`DataPacket_t`): 22字节
- 校正数据包 (`CorrectedDataPacket_t`): 26字节
### 2.2 数据输出需求
每秒需要通过串口输出的数据量:
- **原始数据**: 4000 samples/sec × 22 bytes = **88,000 bytes/sec** (704 Kbps)
- **校正数据**: 4000 samples/sec × 26 bytes = **104,000 bytes/sec** (832 Kbps)
---
## 3. 串口配置分析
### 3.1 USART1 (RS485数据输出)
```c
huart1.Init.BaudRate = 2000000; // 2 Mbps
huart1.Init.WordLength = UART_WORDLENGTH_8B;
huart1.Init.StopBits = UART_STOPBITS_1;
huart1.Init.Parity = UART_PARITY_NONE;
```
**理论传输能力**:
- 波特率: 2,000,000 bps
- 每字节传输时间: 10 bits (1起始位 + 8数据位 + 1停止位) / 2,000,000 = **5μs/byte**
- 理论最大吞吐量: 2,000,000 / 10 = **200,000 bytes/sec**
### 3.2 USART3 (调试输出)
```c
huart3.Init.BaudRate = 921600; // 921.6 Kbps
```
**理论传输能力**:
- 波特率: 921,600 bps
- 每字节传输时间: 10 bits / 921,600 = **10.85μs/byte**
- 理论最大吞吐量: 921,600 / 10 = **92,160 bytes/sec**
---
## 4. 关键瓶颈识别
### 4.1 **瓶颈1: 阻塞式串口发送 (主要瓶颈)**
#### 问题代码位置
**文件**: `User/rs485_driver.c:29`
```c
HAL_StatusTypeDef RS485_SendData(uint8_t *pData, uint16_t Size)
{
HAL_GPIO_WritePin(g_de_re_port, g_de_re_pin, GPIO_PIN_SET);
ret = HAL_UART_Transmit(g_huart_485, pData, Size, 0xffff); // ⚠️ 阻塞式发送
// ...
}
```
#### 问题分析
1. **使用阻塞式发送**: `HAL_UART_Transmit()` 会阻塞CPU直到所有数据发送完成
2. **每个数据包的发送时间**:
- 校正数据包: 26 bytes × 5μs/byte = **130μs**
- 原始数据包: 22 bytes × 5μs/byte = **110μs**
3. **与采样周期的冲突**:
- 采样周期: 250μs
- 串口发送时间: 130μs
- **占用率**: 130μs / 250μs = **52%**
4. **实际影响**:
- CPU在串口发送期间被完全阻塞
- 无法及时处理下一个ADC中断
- 导致数据溢出和丢失
#### 证据
从 [`main.c:189-192`](Core/Src/main.c:189) 可以看到:
```c
#if DATA_OUTPUT_MODE_UART
// 发送校正后的数据包到串口
RS485_SendData((uint8_t*)&g_corrected_packet, sizeof(CorrectedDataPacket_t));
#endif
```
这个调用在 [`ProcessAdcData()`](Core/Src/main.c:149) 函数中,该函数在 [`HAL_TIM_PeriodElapsedCallback()`](Core/Src/main.c:720) 的1ms定时器中断中被调用。
---
### 4.2 **瓶颈2: 调试输出占用大量时间**
#### 问题代码位置
**文件**: `Core/Src/main.c:264-316`
```c
static void DebugOutput_PrintSystemStats(void)
{
char buffer[256];
// ... 多次 snprintf 和 DebugOutput_SendString 调用
HAL_UART_Transmit(&huart3, (uint8_t*)str, strlen(str), 100); // ⚠️ 阻塞式发送
}
```
#### 问题分析
1. **调试输出频率**: 每1秒输出一次 (1000ms间隔)
2. **每次输出数据量**: 约500-800字节的统计信息
3. **发送时间估算**:
- 800 bytes × 10.85μs/byte = **8,680μs** (约8.7ms)
4. **影响**:
- 虽然频率低但每次执行会阻塞CPU约8.7ms
- 在这期间可能错过多个ADC采样 (8.7ms / 0.25ms = 约35个样本)
---
### 4.3 **瓶颈3: CRC16计算开销**
#### 问题代码位置
**文件**: `User/data_packet.c:6-19`
```c
uint16_t Calculate_CRC16(const uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) { // ⚠️ 嵌套循环
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc = crc >> 1;
}
}
}
return crc;
}
```
#### 问题分析
1. **算法复杂度**: O(n×8)每字节需要8次循环
2. **每个数据包的CRC计算**:
- 数据长度: 约20字节
- 循环次数: 20 × 8 = 160次
- 估算时钟周期: 约800-1200 cycles (约5-7μs)
3. **调用频率**: 每个采样周期调用2次 (打包时1次)
4. **总开销**: 约10-14μs/sample
---
### 4.4 **瓶颈4: 定时器中断处理策略不当**
#### 问题代码位置
**文件**: `Core/Src/main.c:720-735`
```c
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2) {
// ADC是4KHz采样率定时器是1KHz
uint8_t processed_count = 0;
const uint8_t max_process_per_interrupt = 8; // ⚠️ 每次最多处理8个
while (processed_count < max_process_per_interrupt) {
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
ProcessAdcData(); // ⚠️ 包含阻塞式串口发送
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
processed_count++;
}
}
}
```
#### 问题分析
1. **定时器频率**: 1KHz (每1ms触发一次)
2. **处理策略**: 每次中断尝试处理最多8个ADC数据
3. **问题**:
- 如果每个 `ProcessAdcData()` 包含130μs的串口发送
- 8个数据包 = 8 × 130μs = **1040μs** (超过1ms中断周期)
- 导致中断处理时间过长,影响系统实时性
---
## 5. 时序分析
### 5.1 理想情况 (无串口输出)
```
时间轴 (每250μs一个周期):
|--ADC中断--|--处理数据(30μs)--|--空闲(220μs)--|--ADC中断--|
```
### 5.2 实际情况 (阻塞式串口输出)
```
时间轴:
|--ADC中断--|--处理(30μs)--|--串口发送(130μs)--|--空闲(90μs)--|--ADC中断--|
↑ 只剩90μs余量
```
### 5.3 最坏情况 (串口发送 + 调试输出)
```
时间轴:
|--ADC--|--处理--|--串口(130μs)--|--ADC--|--处理--|--串口(130μs)--|--调试输出(8700μs)--|
↑ 阻塞8.7ms丢失约35个样本
```
---
## 6. 数据溢出证据
### 6.1 系统监控统计
从 [`main.c:678-680`](Core/Src/main.c:678) 可以看到溢出检测:
```c
if(LTC2508_ERROR_TIMEOUT == LTC2508_TriggerDmaRead())
{
// 数据来不及处理
SystemMonitor_ReportDataOverflow();
}
```
### 6.2 缓冲区状态
- **ADC缓冲区**: 128个缓冲区 (`LTC2508_BUFFER_COUNT = 128`)
- **写入速度**: 4000 samples/sec
- **处理速度**: 受串口发送限制,实际 < 4000 samples/sec
- **结果**: 缓冲区逐渐填满最终溢出
---
## 7. 瓶颈优先级排序
| 优先级 | 瓶颈 | 影响程度 | 解决难度 |
|--------|------|----------|----------|
| **P0** | 阻塞式串口发送 | ⭐⭐⭐⭐⭐ 严重 | 🔧 中等 |
| **P1** | 调试输出阻塞 | ⭐⭐⭐⭐ | 🔧 简单 |
| **P2** | 定时器中断策略 | ⭐⭐⭐ 中等 | 🔧 简单 |
| **P3** | CRC16计算开销 | ⭐⭐ 较低 | 🔧 中等 |
---
## 8. 解决方案建议
### 8.1 **立即解决 (P0): 改用DMA非阻塞发送**
#### 方案1: 启用UART DMA发送
```c
// 修改 rs485_driver.c
HAL_StatusTypeDef RS485_SendData(uint8_t *pData, uint16_t Size)
{
if (g_rs485_tx_busy) {
return HAL_BUSY; // 上一次传输未完成
}
g_rs485_tx_busy = 1;
HAL_GPIO_WritePin(g_de_re_port, g_de_re_pin, GPIO_PIN_SET);
// 使用DMA非阻塞发送
return HAL_UART_Transmit_DMA(g_huart_485, pData, Size);
}
void RS485_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart == g_huart_485) {
HAL_GPIO_WritePin(g_de_re_port, g_de_re_pin, GPIO_PIN_RESET);
g_rs485_tx_busy = 0;
}
}
```
**优点**:
- CPU不再阻塞发送时间从130μs降至约5μs (仅DMA启动时间)
- 释放约125μs的CPU时间 (每个样本)
- 提升50%的系统响应能力
**注意事项**:
- 需要确保数据包缓冲区在DMA传输期间保持有效
- 建议使用双缓冲机制或静态缓冲区
---
### 8.2 **高优先级 (P1): 禁用或优化调试输出**
#### 方案1: 禁用调试输出 (临时方案)
```c
// main.c
#define ENABLE_SYSTEM_MONITOR 0 // 禁用调试输出
```
#### 方案2: 使用DMA发送调试信息
```c
static void DebugOutput_SendString(const char* str)
{
if (str == NULL) return;
// 使用DMA非阻塞发送
HAL_UART_Transmit_DMA(&huart3, (uint8_t*)str, strlen(str));
}
```
#### 方案3: 降低调试输出频率
```c
#define DEBUG_OUTPUT_INTERVAL_MS 5000 // 从1秒改为5秒
```
---
### 8.3 **中等优先级 (P2): 优化定时器中断策略**
#### 方案: 改为事件驱动处理
```c
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2) {
// 只处理一个数据包,避免中断时间过长
ProcessAdcData();
}
}
```
或者在主循环中处理
```c
while (1) {
// 主循环中持续处理ADC数据
ProcessAdcData();
// 其他低优先级任务
DataStorage_ProcessBackgroundTasks(&g_data_storage);
}
```
---
### 8.4 **低优先级 (P3): 优化CRC16计算**
#### 方案1: 使用查表法
```c
static const uint16_t crc16_table[256] = {
// 预计算的CRC表
0x0000, 0xC0C1, 0xC181, 0x0140, ...
};
uint16_t Calculate_CRC16_Fast(const uint8_t *data, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc = (crc >> 8) ^ crc16_table[(crc ^ data[i]) & 0xFF];
}
return crc;
}
```
**性能提升**: 约3-5倍加速
#### 方案2: 使用硬件CRC (STM32F4内置)
```c
// 使用STM32的硬件CRC单元
// 注意STM32的CRC是CRC32需要适配或考虑改用CRC32
```
---
## 9. 实施优先级建议
### 阶段1: 立即实施 (解决主要瓶颈)
1. **启用UART1 DMA发送** (已配置DMA只需修改代码)
2. **禁用或降低调试输出频率**
**预期效果**:
- 串口发送CPU占用从52%降至2%
- 释放约125μs/sample的CPU时间
- 基本解决数据溢出问题
### 阶段2: 优化改进
3. **优化定时器中断处理策略**
4. **使用CRC16查表法**
**预期效果**:
- 进一步提升系统稳定性
- 降低CPU占用至10%以下
### 阶段3: 长期优化
5. **考虑提高串口波特率** (如果硬件支持)
6. **实施数据压缩** (如果需要更高采样率)
---
## 10. 性能对比预测
### 10.1 当前性能 (阻塞式发送)
| 指标 | 数值 |
|------|------|
| 串口发送CPU占用 | 52% |
| 每样本处理时间 | 160μs |
| 最大稳定采样率 | ~3KHz |
| 数据溢出风险 | |
### 10.2 优化后性能 (DMA发送)
| 指标 | 数值 |
|------|------|
| 串口发送CPU占用 | 2% |
| 每样本处理时间 | 35μs |
| 最大稳定采样率 | >10KHz |
| 数据溢出风险 | ✅ 低 |
---
## 11. 结论
### 11.1 根本原因
**阻塞式串口发送是4KHz采样率下数据来不及的根本原因**占用了52%的采样周期时间导致CPU无法及时处理下一个ADC中断。
### 11.2 关键数据
- 采样周期: 250μs
- 阻塞式串口发送: 130μs (52%)
- 实际处理时间: 30μs (12%)
- 剩余余量: 90μs (36%)
### 11.3 解决方案
**启用UART DMA非阻塞发送**是最有效的解决方案可以将串口发送的CPU占用从52%降至2%,完全解决数据溢出问题。
### 11.4 实施建议
1. **立即**: 修改 `rs485_driver.c` 使用 `HAL_UART_Transmit_DMA()`
2. **立即**: 禁用或降低调试输出频率
3. **短期**: 优化定时器中断处理策略
4. **长期**: 实施CRC查表法和其他优化
---
## 12. 附录:关键代码位置
| 文件 | 行号 | 描述 |
|------|------|------|
| [`Core/Src/main.c`](Core/Src/main.c:189) | 189-192 | 串口发送调用 |
| [`Core/Src/main.c`](Core/Src/main.c:720) | 720-735 | 定时器中断处理 |
| [`Core/Src/main.c`](Core/Src/main.c:264) | 264-316 | 调试输出函数 |
| [`User/rs485_driver.c`](User/rs485_driver.c:18) | 18-47 | RS485发送函数 |
| [`User/data_packet.c`](User/data_packet.c:6) | 6-19 | CRC16计算 |
| [`Core/Src/usart.c`](Core/Src/usart.c:44) | 44 | UART1波特率配置 |
| [`Core/Src/usart.c`](Core/Src/usart.c:116) | 116-131 | UART1 DMA配置 |
---
**文档版本**: 1.0
**创建日期**: 2026-02-07
**分析工具**: 代码审查 + 时序分析 + 性能计算