STM_ATEM/User/data_storage.c
zhoujie 29cc814384 feat(scripts): 新增光泵磁力仪数据支持与混流解析
- 新增混流解析函数 `parse_mixed_stream()`,支持标准包与光泵包交替出现的二进制文件
- 新增光泵磁力仪模拟器 `OpticMagSimulatorThread`,以可配置频率发送BCD帧
- 新增光泵磁场显示标签页,支持文件模式与实时模式下的磁场曲线绘制
- 新增 `flash_counter` 模块,用于NOFX模式下文件序号持久化

♻️ refactor(data_storage): 重构数据存储目录与文件命名策略

- 会话目录改为按GPS日期命名(YYYYMMDD),无GPS时使用NOFX目录
- 文件命名改为基于GPS时间(HHMMSS)或Flash计数器(C开头),消除序号文件依赖
- 移除 `config_manager` 依赖和 `PARAM.TXT` 序号文件读写逻辑
- 新增 `GPS_ParseGPRMC()` 解析函数,提取日期字段用于目录命名

🐛 fix(ltc2508_driver): 修复DMA启动失败时资源泄漏问题

- SPI2/SPI3 DMA启动失败时主动停止已启动的DMA,防止资源泄漏
- 将SPI1的dummy发送缓冲区改为static,确保DMA传输期间数据有效

🐛 fix(rs485_driver): 修复DMA发送缓冲区可能被覆盖的问题

- 引入静态发送缓冲区 `s_tx_buf`,隔离调用方数据与DMA传输,防止TIM2 ISR覆盖

♻️ refactor(ATEMParse): 重构数据结构定义与解析逻辑

- 移除硬编码的V1/V2 dtype,改为动态构建 `make_dtype()` 函数
- 新增数据格式选项(GPS经纬度/光泵),支持灵活配置
- 新增文件模式下的分页浏览功能(滑块/上一页/下一页)
- 优化实时模式下的光泵数据显示与表格列过滤
2026-06-14 23:06:25 +08:00

514 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

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.

#include "data_storage.h"
#include "system_monitor.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
/**
* @brief 初始化数据存储模块
* @param handle: 数据存储句柄指针
* @retval HAL_StatusTypeDef
*/
HAL_StatusTypeDef DataStorage_Init(DataStorageHandle_t *handle)
{
if (handle == NULL) {
return HAL_ERROR;
}
// 初始化句柄
memset(handle, 0, sizeof(DataStorageHandle_t));
// 初始化双缓冲区
for (int i = 0; i < 2; i++) {
handle->buffers[i].index = 0;
handle->buffers[i].state = BUFFER_IDLE;
memset(handle->buffers[i].data, 0, DATA_STORAGE_BUFFER_SIZE);
}
// 设置活动缓冲区为0
handle->active_buffer = 0;
handle->flush_buffer = 1;
handle->flush_in_progress = 0;
// 读取Flash计数器计算本次会话起始值并立即写回
// 策略:上电读取存储值+1000作为本次起始立即写入防止异常断电重复使用同一区间
uint32_t flash_base = 0;
FlashCounter_Init(&flash_base);
handle->file_counter = flash_base + 1000;
FlashCounter_Write(handle->file_counter);
handle->stats.state = DATA_STORAGE_IDLE;
handle->initialized = 1;
return HAL_OK;
}
/**
* @brief 停止数据记录
* @param handle: 数据存储句柄指针
* @retval HAL_StatusTypeDef
*/
HAL_StatusTypeDef DataStorage_StopRecording(DataStorageHandle_t *handle)
{
if (handle == NULL || !handle->initialized) {
return HAL_ERROR;
}
if (handle->stats.state != DATA_STORAGE_RECORDING) {
return HAL_OK; // 没有在记录中
}
// 刷新所有缓冲区
for (int i = 0; i < 2; i++) {
if (handle->buffers[i].index > 0) {
handle->buffers[i].state = BUFFER_READY_TO_FLUSH;
DataStorage_FlushBuffer(handle, i);
}
}
// 强制同步所有数据到SD卡
f_sync(&handle->file);
// 关闭文件
f_close(&handle->file);
handle->stats.state = DATA_STORAGE_IDLE;
return HAL_OK;
}
/**
* @brief 写入数据包到存储
* @param handle: 数据存储句柄指针
* @param packet: 数据包指针
* @retval HAL_StatusTypeDef
*/
HAL_StatusTypeDef DataStorage_WriteData(DataStorageHandle_t *handle, const DataPacket_t *packet)
{
if (handle == NULL || packet == NULL || !handle->initialized) {
return HAL_ERROR;
}
if (handle->stats.state != DATA_STORAGE_RECORDING) {
return HAL_ERROR;
}
DataBuffer_t *active_buf = &handle->buffers[handle->active_buffer];
// 检查当前活动缓冲区空间
if (active_buf->index + sizeof(DataPacket_t) > DATA_STORAGE_BUFFER_SIZE) {
// 切换缓冲区
if (DataStorage_SwitchBuffer(handle) != HAL_OK) {
handle->stats.error_count++;
return HAL_ERROR;
}
active_buf = &handle->buffers[handle->active_buffer];
}
// 复制数据到活动缓冲区
memcpy(&active_buf->data[active_buf->index], packet, sizeof(DataPacket_t));
active_buf->index += sizeof(DataPacket_t);
active_buf->state = BUFFER_WRITING;
handle->stats.total_samples++;
return HAL_OK;
}
/**
* @brief 通用字节流写入支持任意包长度方案Y混流存储
* @param handle: 数据存储句柄指针
* @param data: 数据缓冲区指针
* @param size: 写入字节数
* @retval HAL_StatusTypeDef
*/
HAL_StatusTypeDef DataStorage_WriteRawBytes(DataStorageHandle_t *handle, const uint8_t *data, uint16_t size)
{
if (handle == NULL || data == NULL || !handle->initialized || size == 0) {
return HAL_ERROR;
}
if (handle->stats.state != DATA_STORAGE_RECORDING) {
return HAL_ERROR;
}
DataBuffer_t *active_buf = &handle->buffers[handle->active_buffer];
if (active_buf->index + size > DATA_STORAGE_BUFFER_SIZE) {
if (DataStorage_SwitchBuffer(handle) != HAL_OK) {
handle->stats.error_count++;
return HAL_ERROR;
}
active_buf = &handle->buffers[handle->active_buffer];
}
memcpy(&active_buf->data[active_buf->index], data, size);
active_buf->index += size;
active_buf->state = BUFFER_WRITING;
handle->stats.total_samples++;
return HAL_OK;
}
/**
* @brief 写入校正后的数据包到存储
* @param handle: 数据存储句柄指针
* @param packet: 数据包指针
* @retval HAL_StatusTypeDef
*/
HAL_StatusTypeDef DataStorage_WriteCorrectedData(DataStorageHandle_t *handle, const CorrectedDataPacket_t *packet)
{
if (handle == NULL || packet == NULL || !handle->initialized) {
return HAL_ERROR;
}
if (handle->stats.state != DATA_STORAGE_RECORDING) {
return HAL_ERROR;
}
DataBuffer_t *active_buf = &handle->buffers[handle->active_buffer];
// 检查当前活动缓冲区空间
if (active_buf->index + sizeof(CorrectedDataPacket_t) > DATA_STORAGE_BUFFER_SIZE) {
// 切换缓冲区
if (DataStorage_SwitchBuffer(handle) != HAL_OK) {
handle->stats.error_count++;
return HAL_ERROR;
}
active_buf = &handle->buffers[handle->active_buffer];
}
// 复制校正后的数据到活动缓冲区
memcpy(&active_buf->data[active_buf->index], packet, sizeof(CorrectedDataPacket_t));
active_buf->index += sizeof(CorrectedDataPacket_t);
active_buf->state = BUFFER_WRITING;
handle->stats.total_samples++;
return HAL_OK;
}
/**
* @brief 刷新缓冲区到文件
* @param handle: 数据存储句柄指针
* @retval HAL_StatusTypeDef
*/
HAL_StatusTypeDef DataStorage_Flush(DataStorageHandle_t *handle)
{
if (handle == NULL || !handle->initialized) {
return HAL_ERROR;
}
// 处理后台刷新任务
DataStorage_ProcessBackgroundTasks(handle);
return HAL_OK;
}
/**
* @brief 创建新的数据文件
* @param handle: 数据存储句柄指针
* @retval HAL_StatusTypeDef
*/
HAL_StatusTypeDef DataStorage_CreateNewFile(DataStorageHandle_t *handle)
{
if (handle == NULL || !handle->initialized) {
return HAL_ERROR;
}
GPS_Data_t gps;
GPS_GetData(&gps);
if (gps.data_valid) {
// GPS有效用 HHMMSS 命名,同秒冲突时追加 _1 _2 ...
char base[DATA_STORAGE_MAX_PATH_LEN];
snprintf(base, sizeof(base), "%s/%02u%02u%02u",
handle->current_session_path,
gps.time.hour, gps.time.minute, gps.time.second);
FILINFO fno;
snprintf(handle->stats.current_filename,
sizeof(handle->stats.current_filename), "%s.dat", base);
if (f_stat(handle->stats.current_filename, &fno) == FR_OK) {
for (int i = 1; i <= 99; i++) {
snprintf(handle->stats.current_filename,
sizeof(handle->stats.current_filename),
"%s_%d.dat", base, i);
if (f_stat(handle->stats.current_filename, &fno) != FR_OK) break;
}
}
} else {
// 无GPS用Flash计数器命名 C0001000.dat
snprintf(handle->stats.current_filename,
sizeof(handle->stats.current_filename),
"%s/C%08lu.dat",
handle->current_session_path, handle->file_counter++);
}
FRESULT res = f_open(&handle->file, handle->stats.current_filename,
FA_CREATE_ALWAYS | FA_WRITE);
if (res != FR_OK) {
handle->stats.error_count++;
SystemMonitor_ReportSDWriteError();
return HAL_ERROR;
}
handle->stats.file_count++;
handle->stats.current_file_size = 0;
SystemMonitor_ReportSDFileCreated();
return HAL_OK;
}
/**
* @brief 获取数据存储统计信息
* @param handle: 数据存储句柄指针
* @param stats: 统计信息结构体指针
* @retval None
*/
void DataStorage_GetStats(DataStorageHandle_t *handle, DataStorageStats_t *stats)
{
if (handle == NULL || stats == NULL || !handle->initialized) {
return;
}
memcpy(stats, &handle->stats, sizeof(DataStorageStats_t));
}
/**
* @brief 开始数据记录
* @param handle: 数据存储句柄指针
* @retval HAL_StatusTypeDef
*/
HAL_StatusTypeDef DataStorage_StartRecording(DataStorageHandle_t *handle)
{
if (handle == NULL || !handle->initialized) {
return HAL_ERROR;
}
if (handle->stats.state == DATA_STORAGE_RECORDING) {
return HAL_OK; // 已经在记录中
}
// 每次开始录制时确定会话目录此时GPS可能已定位
if (DataStorage_CreateSessionFolder(handle) != HAL_OK) {
handle->stats.state = DATA_STORAGE_ERROR;
return HAL_ERROR;
}
// 创建新文件
if (DataStorage_CreateNewFile(handle) != HAL_OK) {
handle->stats.state = DATA_STORAGE_ERROR;
return HAL_ERROR;
}
// 重置双缓冲区状态
for (int i = 0; i < 2; i++) {
handle->buffers[i].index = 0;
handle->buffers[i].state = BUFFER_IDLE;
}
handle->active_buffer = 0;
handle->flush_buffer = 1;
handle->flush_in_progress = 0;
handle->stats.state = DATA_STORAGE_RECORDING;
return HAL_OK;
}
/**
* @brief 刷新指定缓冲区到文件
* @param handle: 数据存储句柄指针
* @param buffer_index: 缓冲区索引 (0 或 1)
* @retval HAL_StatusTypeDef
*/
HAL_StatusTypeDef DataStorage_FlushBuffer(DataStorageHandle_t *handle, uint8_t buffer_index)
{
if (handle == NULL || !handle->initialized || buffer_index > 1) {
return HAL_ERROR;
}
DataBuffer_t *buffer = &handle->buffers[buffer_index];
// 检查缓冲区状态和数据
if (buffer->state != BUFFER_READY_TO_FLUSH || buffer->index == 0) {
return HAL_OK; // 没有数据需要刷新
}
// 标记缓冲区正在刷新
buffer->state = BUFFER_FLUSHING;
UINT bytes_written;
FRESULT res = f_write(&handle->file, buffer->data, buffer->index, &bytes_written);
if (res != FR_OK || bytes_written != buffer->index) {
handle->stats.error_count++;
buffer->state = BUFFER_READY_TO_FLUSH; // 恢复状态以便重试
SystemMonitor_ReportSDWriteError(); // 报告SD卡写入错误
return HAL_ERROR;
}
// 优化:减少同步频率以提高性能
// 使用静态变量记录刷新次数
static uint32_t flush_count = 0;
flush_count++;
// 每10次刷新才同步一次或者文件即将达到最大大小时同步
if (flush_count % 10 == 0 ||
handle->stats.current_file_size + bytes_written >= DATA_STORAGE_FILE_MAX_SIZE) {
f_sync(&handle->file);
}
// 更新统计信息
handle->stats.current_file_size += bytes_written;
SystemMonitor_ReportSDWrite(bytes_written); // 报告SD卡写入成功
// 重置缓冲区
buffer->index = 0;
buffer->state = BUFFER_IDLE;
// 检查文件大小是否超过限制
if (handle->stats.current_file_size >= DATA_STORAGE_FILE_MAX_SIZE) {
f_close(&handle->file);
DataStorage_CreateNewFile(handle);
}
return HAL_OK;
}
/**
* @brief 处理后台任务(异步刷新缓冲区)
* @param handle: 数据存储句柄指针
* @retval None
*/
void DataStorage_ProcessBackgroundTasks(DataStorageHandle_t *handle)
{
if (handle == NULL || !handle->initialized) {
return;
}
// 检查是否有缓冲区需要刷新
for (int i = 0; i < 2; i++) {
if (handle->buffers[i].state == BUFFER_READY_TO_FLUSH) {
// 刷新缓冲区
DataStorage_FlushBuffer(handle, i);
break; // 一次只处理一个缓冲区,避免阻塞太久
}
}
}
/**
* @brief 切换活动缓冲区
* @param handle: 数据存储句柄指针
* @retval HAL_StatusTypeDef
*/
HAL_StatusTypeDef DataStorage_SwitchBuffer(DataStorageHandle_t *handle)
{
if (handle == NULL || !handle->initialized) {
return HAL_ERROR;
}
DataBuffer_t *current_buf = &handle->buffers[handle->active_buffer];
// 标记当前缓冲区准备刷新
if (current_buf->index > 0) {
current_buf->state = BUFFER_READY_TO_FLUSH;
}
// 切换到另一个缓冲区
uint8_t next_buffer = (handle->active_buffer == 0) ? 1 : 0;
DataBuffer_t *next_buf = &handle->buffers[next_buffer];
// 检查目标缓冲区是否可用
if (next_buf->state == BUFFER_FLUSHING) {
// 目标缓冲区正在刷新,等待完成
handle->stats.error_count++;
SystemMonitor_ReportSDBufferFull(); // 报告缓冲区满
return HAL_ERROR;
}
// 切换活动缓冲区
handle->active_buffer = next_buffer;
next_buf->index = 0;
next_buf->state = BUFFER_WRITING;
return HAL_OK;
}
/**
* @brief 检查缓冲区是否有足够空间可用
* @param handle: 数据存储句柄指针
* @param required_size: 需要的空间大小(字节)
* @retval 1: 缓冲区可用, 0: 缓冲区不可用
*/
uint8_t DataStorage_IsBufferAvailable(DataStorageHandle_t *handle, uint32_t required_size)
{
if (handle == NULL || !handle->initialized) {
return 0;
}
// 如果未在记录状态,返回不可用
if (handle->stats.state != DATA_STORAGE_RECORDING) {
return 0;
}
DataBuffer_t *active_buf = &handle->buffers[handle->active_buffer];
// 检查当前活动缓冲区是否有足够空间
if (active_buf->index + required_size <= DATA_STORAGE_BUFFER_SIZE) {
return 1; // 当前缓冲区有足够空间
}
// 当前缓冲区空间不足,检查是否可以切换到另一个缓冲区
uint8_t next_buffer = (handle->active_buffer == 0) ? 1 : 0;
DataBuffer_t *next_buf = &handle->buffers[next_buffer];
// 如果另一个缓冲区正在刷新或准备刷新,则不可用
if (next_buf->state == BUFFER_FLUSHING || next_buf->state == BUFFER_READY_TO_FLUSH) {
return 0; // 无法切换,缓冲区不可用
}
// 另一个缓冲区可用
return 1;
}
/**
* @brief 创建新的会话文件夹
* @param handle: 数据存储句柄指针
* @retval HAL_StatusTypeDef
*/
HAL_StatusTypeDef DataStorage_CreateSessionFolder(DataStorageHandle_t *handle)
{
if (handle == NULL) {
return HAL_ERROR;
}
// 创建基础数据目录
FRESULT res = f_mkdir(DATA_STORAGE_BASE_PATH);
if (res != FR_OK && res != FR_EXIST) {
return HAL_ERROR;
}
GPS_Data_t gps;
GPS_GetData(&gps);
if (gps.date_valid) {
// GPS日期有效目录格式 0:/DATA/YYYYMMDD
snprintf(handle->current_session_path, sizeof(handle->current_session_path),
"%s/%04u%02u%02u",
DATA_STORAGE_BASE_PATH,
gps.date.year, gps.date.month, gps.date.day);
} else {
// 无GPS日期统一放入 NOFX 目录
snprintf(handle->current_session_path, sizeof(handle->current_session_path),
"%s", DATA_STORAGE_NOFX_DIR);
}
res = f_mkdir(handle->current_session_path);
if (res != FR_OK && res != FR_EXIST) {
return HAL_ERROR;
}
return HAL_OK;
}