✨ 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经纬度/光泵),支持灵活配置 - 新增文件模式下的分页浏览功能(滑块/上一页/下一页) - 优化实时模式下的光泵数据显示与表格列过滤
This commit is contained in:
parent
047ef08318
commit
29cc814384
@ -177,7 +177,7 @@ NVIC.DebugMonitor_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false
|
|||||||
NVIC.EXTI1_IRQn=true\:0\:0\:false\:false\:true\:true\:true\:true
|
NVIC.EXTI1_IRQn=true\:0\:0\:false\:false\:true\:true\:true\:true
|
||||||
NVIC.ForceEnableDMAVector=true
|
NVIC.ForceEnableDMAVector=true
|
||||||
NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false
|
NVIC.HardFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false
|
||||||
NVIC.MemoryManagement_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false
|
NVIC.MemoryManagement_IRQn=true\:0\:0\:true\:false\:true\:false\:false\:false
|
||||||
NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false
|
NVIC.NonMaskableInt_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false
|
||||||
NVIC.OTG_FS_IRQn=true\:11\:0\:true\:false\:true\:false\:true\:true
|
NVIC.OTG_FS_IRQn=true\:11\:0\:true\:false\:true\:false\:true\:true
|
||||||
NVIC.PendSV_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false
|
NVIC.PendSV_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false
|
||||||
@ -292,8 +292,8 @@ ProjectManager.MainLocation=Core/Src
|
|||||||
ProjectManager.NoMain=false
|
ProjectManager.NoMain=false
|
||||||
ProjectManager.PreviousToolchain=
|
ProjectManager.PreviousToolchain=
|
||||||
ProjectManager.ProjectBuild=false
|
ProjectManager.ProjectBuild=false
|
||||||
ProjectManager.ProjectFileName=STM_ATEM_F405.ioc
|
ProjectManager.ProjectFileName=STM_ATEM_F405_App.ioc
|
||||||
ProjectManager.ProjectName=STM_ATEM_F405
|
ProjectManager.ProjectName=STM_ATEM_F405_App
|
||||||
ProjectManager.ProjectStructure=
|
ProjectManager.ProjectStructure=
|
||||||
ProjectManager.RegisterCallBack=
|
ProjectManager.RegisterCallBack=
|
||||||
ProjectManager.StackSize=0x1000
|
ProjectManager.StackSize=0x1000
|
||||||
File diff suppressed because it is too large
Load Diff
BIN
Scripts/磁通门_光泵_ 示例数据.dat
Normal file
BIN
Scripts/磁通门_光泵_ 示例数据.dat
Normal file
Binary file not shown.
@ -26,7 +26,7 @@
|
|||||||
// RS485 串口输出开关
|
// RS485 串口输出开关
|
||||||
// 1 = 每帧 ADC 数据通过 USART1 以 2Mbps 发送
|
// 1 = 每帧 ADC 数据通过 USART1 以 2Mbps 发送
|
||||||
// 0 = 禁用串口输出,降低功耗和 CPU 占用
|
// 0 = 禁用串口输出,降低功耗和 CPU 占用
|
||||||
#define CFG_UART_OUTPUT_ENABLED 1
|
#define CFG_UART_OUTPUT_ENABLED 0
|
||||||
|
|
||||||
// SD 卡数据存储开关
|
// SD 卡数据存储开关
|
||||||
// 1 = 每帧数据写入 SD 卡 DATA/SESSION_xxx/ 目录下的文件
|
// 1 = 每帧数据写入 SD 卡 DATA/SESSION_xxx/ 目录下的文件
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
#include "data_storage.h"
|
#include "data_storage.h"
|
||||||
#include "system_monitor.h"
|
#include "system_monitor.h"
|
||||||
#include "config_manager.h"
|
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@ -31,10 +30,12 @@ HAL_StatusTypeDef DataStorage_Init(DataStorageHandle_t *handle)
|
|||||||
handle->flush_buffer = 1;
|
handle->flush_buffer = 1;
|
||||||
handle->flush_in_progress = 0;
|
handle->flush_in_progress = 0;
|
||||||
|
|
||||||
// 创建新的会话文件夹(每次上电创建新文件夹)
|
// 读取Flash计数器,计算本次会话起始值并立即写回
|
||||||
if (DataStorage_CreateSessionFolder(handle) != HAL_OK) {
|
// 策略:上电读取存储值+1000作为本次起始,立即写入,防止异常断电重复使用同一区间
|
||||||
return HAL_ERROR;
|
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->stats.state = DATA_STORAGE_IDLE;
|
||||||
handle->initialized = 1;
|
handle->initialized = 1;
|
||||||
@ -213,24 +214,47 @@ HAL_StatusTypeDef DataStorage_CreateNewFile(DataStorageHandle_t *handle)
|
|||||||
return HAL_ERROR;
|
return HAL_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成文件名 (基于时间戳),文件存储在当前会话文件夹中
|
GPS_Data_t gps;
|
||||||
uint32_t timestamp = HAL_GetTick();
|
GPS_GetData(&gps);
|
||||||
snprintf(handle->stats.current_filename, sizeof(handle->stats.current_filename),
|
|
||||||
"%s%s%08lX.dat", handle->current_session_path, DATA_STORAGE_FILE_PREFIX, timestamp);
|
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,
|
FRESULT res = f_open(&handle->file, handle->stats.current_filename,
|
||||||
FA_CREATE_ALWAYS | FA_WRITE);
|
FA_CREATE_ALWAYS | FA_WRITE);
|
||||||
|
|
||||||
if (res != FR_OK) {
|
if (res != FR_OK) {
|
||||||
handle->stats.error_count++;
|
handle->stats.error_count++;
|
||||||
SystemMonitor_ReportSDWriteError(); // 报告文件创建错误
|
SystemMonitor_ReportSDWriteError();
|
||||||
return HAL_ERROR;
|
return HAL_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
handle->stats.file_count++;
|
handle->stats.file_count++;
|
||||||
handle->stats.current_file_size = 0;
|
handle->stats.current_file_size = 0;
|
||||||
SystemMonitor_ReportSDFileCreated(); // 报告文件创建成功
|
SystemMonitor_ReportSDFileCreated();
|
||||||
|
|
||||||
return HAL_OK;
|
return HAL_OK;
|
||||||
}
|
}
|
||||||
@ -265,6 +289,12 @@ HAL_StatusTypeDef DataStorage_StartRecording(DataStorageHandle_t *handle)
|
|||||||
return HAL_OK; // 已经在记录中
|
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) {
|
if (DataStorage_CreateNewFile(handle) != HAL_OK) {
|
||||||
handle->stats.state = DATA_STORAGE_ERROR;
|
handle->stats.state = DATA_STORAGE_ERROR;
|
||||||
@ -452,108 +482,32 @@ HAL_StatusTypeDef DataStorage_CreateSessionFolder(DataStorageHandle_t *handle)
|
|||||||
return HAL_ERROR;
|
return HAL_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从配置管理器获取并递增会话序号
|
// 创建基础数据目录
|
||||||
uint32_t session_number = Config_IncrementSessionNumber();
|
|
||||||
|
|
||||||
// 生成会话文件夹名(基于序号)
|
|
||||||
snprintf(handle->current_session_path, sizeof(handle->current_session_path),
|
|
||||||
"%s/%s%06lu", DATA_STORAGE_BASE_PATH, DATA_STORAGE_FOLDER_PREFIX, session_number);
|
|
||||||
|
|
||||||
// 创建基础数据目录(如果不存在)
|
|
||||||
FRESULT res = f_mkdir(DATA_STORAGE_BASE_PATH);
|
FRESULT res = f_mkdir(DATA_STORAGE_BASE_PATH);
|
||||||
if (res != FR_OK && res != FR_EXIST) {
|
if (res != FR_OK && res != FR_EXIST) {
|
||||||
return HAL_ERROR;
|
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);
|
res = f_mkdir(handle->current_session_path);
|
||||||
if (res != FR_OK && res != FR_EXIST) {
|
if (res != FR_OK && res != FR_EXIST) {
|
||||||
return HAL_ERROR;
|
return HAL_ERROR;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存更新后的配置(包含新的会话序号)
|
|
||||||
if (Config_Save() != HAL_OK) {
|
|
||||||
// 即使保存失败,也继续使用该文件夹
|
|
||||||
// 这不是致命错误
|
|
||||||
}
|
|
||||||
|
|
||||||
return HAL_OK;
|
return HAL_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 从文件加载会话序号
|
|
||||||
* @param session_number: 用于存储序号的指针
|
|
||||||
* @retval HAL_StatusTypeDef
|
|
||||||
*/
|
|
||||||
HAL_StatusTypeDef DataStorage_LoadSessionNumber(uint32_t *session_number)
|
|
||||||
{
|
|
||||||
if (session_number == NULL) {
|
|
||||||
return HAL_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
FIL file;
|
|
||||||
FRESULT res;
|
|
||||||
UINT bytes_read;
|
|
||||||
char buffer[16];
|
|
||||||
|
|
||||||
// 打开PARAM.TXT文件
|
|
||||||
res = f_open(&file, DATA_STORAGE_PARAM_FILE, FA_READ);
|
|
||||||
if (res != FR_OK) {
|
|
||||||
// 文件不存在,返回初始序号0
|
|
||||||
*session_number = 0;
|
|
||||||
return HAL_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取序号
|
|
||||||
res = f_read(&file, buffer, sizeof(buffer) - 1, &bytes_read);
|
|
||||||
if (res != FR_OK) {
|
|
||||||
f_close(&file);
|
|
||||||
*session_number = 0;
|
|
||||||
return HAL_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加字符串结束符
|
|
||||||
buffer[bytes_read] = '\0';
|
|
||||||
|
|
||||||
// 关闭文件
|
|
||||||
f_close(&file);
|
|
||||||
|
|
||||||
// 转换为数字
|
|
||||||
*session_number = (uint32_t)atoi(buffer);
|
|
||||||
|
|
||||||
return HAL_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief 保存会话序号到文件
|
|
||||||
* @param session_number: 要保存的序号
|
|
||||||
* @retval HAL_StatusTypeDef
|
|
||||||
*/
|
|
||||||
HAL_StatusTypeDef DataStorage_SaveSessionNumber(uint32_t session_number)
|
|
||||||
{
|
|
||||||
FIL file;
|
|
||||||
FRESULT res;
|
|
||||||
UINT bytes_written;
|
|
||||||
char buffer[16];
|
|
||||||
|
|
||||||
// 创建或覆盖PARAM.TXT文件
|
|
||||||
res = f_open(&file, DATA_STORAGE_PARAM_FILE, FA_CREATE_ALWAYS | FA_WRITE);
|
|
||||||
if (res != FR_OK) {
|
|
||||||
return HAL_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将序号转换为字符串
|
|
||||||
snprintf(buffer, sizeof(buffer), "%lu", session_number);
|
|
||||||
|
|
||||||
// 写入序号
|
|
||||||
res = f_write(&file, buffer, strlen(buffer), &bytes_written);
|
|
||||||
if (res != FR_OK || bytes_written != strlen(buffer)) {
|
|
||||||
f_close(&file);
|
|
||||||
return HAL_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 关闭文件
|
|
||||||
f_close(&file);
|
|
||||||
|
|
||||||
return HAL_OK;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -7,15 +7,15 @@
|
|||||||
#include "ff.h"
|
#include "ff.h"
|
||||||
#include "data_packet.h"
|
#include "data_packet.h"
|
||||||
#include "correction.h"
|
#include "correction.h"
|
||||||
|
#include "flash_counter.h"
|
||||||
|
#include "gps_driver.h"
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
// 数据存储配置(数值由 app_config.h 中 CFG_* 统一管理)
|
// 数据存储配置(数值由 app_config.h 中 CFG_* 统一管理)
|
||||||
#define DATA_STORAGE_BUFFER_SIZE CFG_STORAGE_BUFFER_SIZE
|
#define DATA_STORAGE_BUFFER_SIZE CFG_STORAGE_BUFFER_SIZE
|
||||||
#define DATA_STORAGE_FILE_MAX_SIZE CFG_STORAGE_FILE_MAX_SIZE
|
#define DATA_STORAGE_FILE_MAX_SIZE CFG_STORAGE_FILE_MAX_SIZE
|
||||||
#define DATA_STORAGE_BASE_PATH "0:/DATA" // 数据存储基础路径
|
#define DATA_STORAGE_BASE_PATH "0:/DATA" // 数据存储基础路径
|
||||||
#define DATA_STORAGE_FILE_PREFIX "/ADC_DATA_" // 文件名前缀
|
#define DATA_STORAGE_NOFX_DIR "0:/DATA/NOFX" // 无GPS定位时的目录
|
||||||
#define DATA_STORAGE_FOLDER_PREFIX "SESSION_" // 文件夹名前缀
|
|
||||||
#define DATA_STORAGE_PARAM_FILE "0:/PARAM.TXT" // 记录会话序号的文件
|
|
||||||
#define DATA_STORAGE_MAX_PATH_LEN 128 // 最大路径长度
|
#define DATA_STORAGE_MAX_PATH_LEN 128 // 最大路径长度
|
||||||
|
|
||||||
// 缓冲区状态
|
// 缓冲区状态
|
||||||
@ -61,6 +61,7 @@ typedef struct {
|
|||||||
uint8_t initialized;
|
uint8_t initialized;
|
||||||
uint8_t flush_in_progress; // 刷新进行中标志
|
uint8_t flush_in_progress; // 刷新进行中标志
|
||||||
char current_session_path[DATA_STORAGE_MAX_PATH_LEN]; // 当前会话文件夹路径
|
char current_session_path[DATA_STORAGE_MAX_PATH_LEN]; // 当前会话文件夹路径
|
||||||
|
uint32_t file_counter; // NOFX模式下的文件序号(从Flash读取后+1000)
|
||||||
} DataStorageHandle_t;
|
} DataStorageHandle_t;
|
||||||
|
|
||||||
// 函数声明
|
// 函数声明
|
||||||
@ -76,10 +77,6 @@ HAL_StatusTypeDef DataStorage_CreateNewFile(DataStorageHandle_t *handle);
|
|||||||
// 文件夹管理函数
|
// 文件夹管理函数
|
||||||
HAL_StatusTypeDef DataStorage_CreateSessionFolder(DataStorageHandle_t *handle);
|
HAL_StatusTypeDef DataStorage_CreateSessionFolder(DataStorageHandle_t *handle);
|
||||||
|
|
||||||
// 序号管理函数
|
|
||||||
HAL_StatusTypeDef DataStorage_LoadSessionNumber(uint32_t *session_number);
|
|
||||||
HAL_StatusTypeDef DataStorage_SaveSessionNumber(uint32_t session_number);
|
|
||||||
|
|
||||||
// 双缓冲区管理函数
|
// 双缓冲区管理函数
|
||||||
HAL_StatusTypeDef DataStorage_SwitchBuffer(DataStorageHandle_t *handle);
|
HAL_StatusTypeDef DataStorage_SwitchBuffer(DataStorageHandle_t *handle);
|
||||||
HAL_StatusTypeDef DataStorage_FlushBuffer(DataStorageHandle_t *handle, uint8_t buffer_index);
|
HAL_StatusTypeDef DataStorage_FlushBuffer(DataStorageHandle_t *handle, uint8_t buffer_index);
|
||||||
|
|||||||
59
User/flash_counter.c
Normal file
59
User/flash_counter.c
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
#include "flash_counter.h"
|
||||||
|
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint32_t magic;
|
||||||
|
uint32_t counter;
|
||||||
|
uint32_t checksum; // magic ^ counter,用于检测 Flash 损坏
|
||||||
|
} FlashCounterData_t;
|
||||||
|
|
||||||
|
HAL_StatusTypeDef FlashCounter_Init(uint32_t *counter)
|
||||||
|
{
|
||||||
|
const FlashCounterData_t *p = (const FlashCounterData_t *)FLASH_COUNTER_ADDR;
|
||||||
|
|
||||||
|
if (p->magic == FLASH_COUNTER_MAGIC &&
|
||||||
|
(p->magic ^ p->counter) == p->checksum) {
|
||||||
|
*counter = p->counter;
|
||||||
|
} else {
|
||||||
|
// 未初始化或校验失败,视为 0
|
||||||
|
*counter = 0;
|
||||||
|
}
|
||||||
|
return HAL_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HAL_StatusTypeDef FlashCounter_Write(uint32_t counter)
|
||||||
|
{
|
||||||
|
FLASH_EraseInitTypeDef erase = {
|
||||||
|
.TypeErase = FLASH_TYPEERASE_SECTORS,
|
||||||
|
.Sector = FLASH_COUNTER_SECTOR,
|
||||||
|
.NbSectors = 1,
|
||||||
|
.VoltageRange = FLASH_VOLTAGE_RANGE_3, // 2.7 ~ 3.6 V
|
||||||
|
};
|
||||||
|
uint32_t sector_error = 0;
|
||||||
|
|
||||||
|
HAL_FLASH_Unlock();
|
||||||
|
|
||||||
|
if (HAL_FLASHEx_Erase(&erase, §or_error) != HAL_OK) {
|
||||||
|
HAL_FLASH_Lock();
|
||||||
|
return HAL_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlashCounterData_t data = {
|
||||||
|
.magic = FLASH_COUNTER_MAGIC,
|
||||||
|
.counter = counter,
|
||||||
|
.checksum = FLASH_COUNTER_MAGIC ^ counter,
|
||||||
|
};
|
||||||
|
|
||||||
|
uint32_t addr = FLASH_COUNTER_ADDR;
|
||||||
|
uint32_t *word = (uint32_t *)&data;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < sizeof(FlashCounterData_t) / 4; i++) {
|
||||||
|
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr, word[i]) != HAL_OK) {
|
||||||
|
HAL_FLASH_Lock();
|
||||||
|
return HAL_ERROR;
|
||||||
|
}
|
||||||
|
addr += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
HAL_FLASH_Lock();
|
||||||
|
return HAL_OK;
|
||||||
|
}
|
||||||
20
User/flash_counter.h
Normal file
20
User/flash_counter.h
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#ifndef FLASH_COUNTER_H
|
||||||
|
#define FLASH_COUNTER_H
|
||||||
|
|
||||||
|
#include "main.h"
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// STM32F405 1MB Flash 最后一个扇区(Sector 11, 128KB)
|
||||||
|
// 地址范围 0x080E0000 ~ 0x080FFFFF
|
||||||
|
// 确保链接脚本 FLASH ORIGIN+LENGTH 不超过 0x080E0000
|
||||||
|
#define FLASH_COUNTER_SECTOR FLASH_SECTOR_11
|
||||||
|
#define FLASH_COUNTER_ADDR 0x080E0000UL
|
||||||
|
#define FLASH_COUNTER_MAGIC 0x5A5A1234UL
|
||||||
|
|
||||||
|
// 从 Flash 读取计数器。首次使用(未写入)时 *counter = 0,返回 HAL_OK
|
||||||
|
HAL_StatusTypeDef FlashCounter_Init(uint32_t *counter);
|
||||||
|
|
||||||
|
// 将新计数器值擦除写入 Flash(约 1~2 ms)
|
||||||
|
HAL_StatusTypeDef FlashCounter_Write(uint32_t counter);
|
||||||
|
|
||||||
|
#endif // FLASH_COUNTER_H
|
||||||
@ -26,6 +26,7 @@ static GPS_Data_t gps_data; // GPS数据
|
|||||||
/* Private function prototypes -----------------------------------------------*/
|
/* Private function prototypes -----------------------------------------------*/
|
||||||
static void GPS_ParseNMEA(char *nmea);
|
static void GPS_ParseNMEA(char *nmea);
|
||||||
static void GPS_ParseGPGGA(char *nmea);
|
static void GPS_ParseGPGGA(char *nmea);
|
||||||
|
static void GPS_ParseGPRMC(char *nmea);
|
||||||
static double GPS_ConvertToDecimal(const char *coord, char direction);
|
static double GPS_ConvertToDecimal(const char *coord, char direction);
|
||||||
|
|
||||||
/* Exported functions --------------------------------------------------------*/
|
/* Exported functions --------------------------------------------------------*/
|
||||||
@ -147,6 +148,23 @@ void GPS_GetPositionString(char *buffer, uint16_t size)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取GPS日期字符串(格式 YYYYMMDD)
|
||||||
|
*/
|
||||||
|
void GPS_GetDateString(char *buffer, uint16_t size)
|
||||||
|
{
|
||||||
|
if (buffer == NULL || size == 0) return;
|
||||||
|
|
||||||
|
if (gps_data.date_valid) {
|
||||||
|
snprintf(buffer, size, "%04u%02u%02u",
|
||||||
|
gps_data.date.year,
|
||||||
|
gps_data.date.month,
|
||||||
|
gps_data.date.day);
|
||||||
|
} else {
|
||||||
|
buffer[0] = '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief UART接收完成回调函数
|
* @brief UART接收完成回调函数
|
||||||
* @param huart: UART句柄
|
* @param huart: UART句柄
|
||||||
@ -209,11 +227,11 @@ static void GPS_ParseNMEA(char *nmea)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否为GPGGA或GNGGA语句
|
|
||||||
if (strncmp(nmea, "$GPGGA", 6) == 0 || strncmp(nmea, "$GNGGA", 6) == 0) {
|
if (strncmp(nmea, "$GPGGA", 6) == 0 || strncmp(nmea, "$GNGGA", 6) == 0) {
|
||||||
GPS_ParseGPGGA(nmea);
|
GPS_ParseGPGGA(nmea);
|
||||||
|
} else if (strncmp(nmea, "$GPRMC", 6) == 0 || strncmp(nmea, "$GNRMC", 6) == 0) {
|
||||||
|
GPS_ParseGPRMC(nmea);
|
||||||
}
|
}
|
||||||
// 可以添加其他NMEA语句的解析,如GPRMC等
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -347,6 +365,50 @@ static void GPS_ParseGPGGA(char *nmea)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 解析 GPRMC / GNRMC 语句,提取日期字段
|
||||||
|
*
|
||||||
|
* GPRMC 格式:
|
||||||
|
* $GPRMC,HHMMSS.ss,A,lat,N,lon,E,speed,course,DDMMYY,mv,mvE*CS
|
||||||
|
* field 0: $GPRMC
|
||||||
|
* field 1: 时间 HHMMSS.ss
|
||||||
|
* field 2: 状态 A=有效 V=无效
|
||||||
|
* field 9: 日期 DDMMYY ← 目标字段
|
||||||
|
*/
|
||||||
|
static void GPS_ParseGPRMC(char *nmea)
|
||||||
|
{
|
||||||
|
char *token;
|
||||||
|
char *saveptr;
|
||||||
|
int field_index = 0;
|
||||||
|
uint8_t status_valid = 0;
|
||||||
|
|
||||||
|
token = strtok_r(nmea, ",", &saveptr);
|
||||||
|
while (token != NULL) {
|
||||||
|
switch (field_index) {
|
||||||
|
case 2: // 状态
|
||||||
|
status_valid = (token[0] == 'A') ? 1 : 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 9: // 日期 DDMMYY
|
||||||
|
if (status_valid && strlen(token) >= 6) {
|
||||||
|
char dd[3] = {token[0], token[1], '\0'};
|
||||||
|
char mm[3] = {token[2], token[3], '\0'};
|
||||||
|
char yy[3] = {token[4], token[5], '\0'};
|
||||||
|
gps_data.date.day = (uint8_t)atoi(dd);
|
||||||
|
gps_data.date.month = (uint8_t)atoi(mm);
|
||||||
|
gps_data.date.year = 2000u + (uint16_t)atoi(yy);
|
||||||
|
gps_data.date_valid = 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
token = strtok_r(NULL, ",", &saveptr);
|
||||||
|
field_index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 将GPS坐标格式转换为十进制度
|
* @brief 将GPS坐标格式转换为十进制度
|
||||||
* @param coord: GPS坐标字符串 (ddmm.mmmm 或 dddmm.mmmm)
|
* @param coord: GPS坐标字符串 (ddmm.mmmm 或 dddmm.mmmm)
|
||||||
|
|||||||
@ -54,6 +54,15 @@ typedef struct {
|
|||||||
uint16_t millisec; // 毫秒
|
uint16_t millisec; // 毫秒
|
||||||
} GPS_Time_t;
|
} GPS_Time_t;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief GPS日期结构体(来自 GPRMC/GNRMC)
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint8_t day; // 日 (1~31)
|
||||||
|
uint8_t month; // 月 (1~12)
|
||||||
|
uint16_t year; // 年 (完整年份,如 2025)
|
||||||
|
} GPS_Date_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief GPS位置结构体
|
* @brief GPS位置结构体
|
||||||
*/
|
*/
|
||||||
@ -72,9 +81,11 @@ typedef struct {
|
|||||||
* @brief GPS数据结构体
|
* @brief GPS数据结构体
|
||||||
*/
|
*/
|
||||||
typedef struct {
|
typedef struct {
|
||||||
GPS_Time_t time; // GPS时间
|
GPS_Time_t time; // GPS时间(来自GPGGA)
|
||||||
|
GPS_Date_t date; // GPS日期(来自GPRMC)
|
||||||
GPS_Position_t position; // GPS位置
|
GPS_Position_t position; // GPS位置
|
||||||
uint8_t data_valid; // 数据有效标志 (1=有效, 0=无效)
|
uint8_t data_valid; // 时间与位置有效(GPGGA fix > 0)
|
||||||
|
uint8_t date_valid; // 日期有效(GPRMC 已收到且 status='A')
|
||||||
uint32_t last_update_tick; // 最后更新时间戳
|
uint32_t last_update_tick; // 最后更新时间戳
|
||||||
} GPS_Data_t;
|
} GPS_Data_t;
|
||||||
|
|
||||||
@ -143,6 +154,14 @@ void GPS_UART_RxCpltCallback(UART_HandleTypeDef *huart);
|
|||||||
*/
|
*/
|
||||||
void GPS_UART_IdleCallback(UART_HandleTypeDef *huart);
|
void GPS_UART_IdleCallback(UART_HandleTypeDef *huart);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取GPS日期字符串(格式 YYYYMMDD,无定位时返回空字符串)
|
||||||
|
* @param buffer: 输出缓冲区(至少9字节)
|
||||||
|
* @param size: 缓冲区大小
|
||||||
|
* @retval None
|
||||||
|
*/
|
||||||
|
void GPS_GetDateString(char *buffer, uint16_t size);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -91,30 +91,33 @@ LTC2508_StatusTypeDef LTC2508_TriggerDmaRead(void)
|
|||||||
current_buffer->dma_complete_count = 0;
|
current_buffer->dma_complete_count = 0;
|
||||||
current_buffer->timestamp = HAL_GetTick();
|
current_buffer->timestamp = HAL_GetTick();
|
||||||
|
|
||||||
// SPI2 和 SPI3 作为从机只接收
|
// SPI2 和 SPI3 作为从机只接收;必须在 SPI1(主机)启动时钟前挂好 DMA,否则从机会丢失前几个时钟
|
||||||
if (HAL_SPI_Receive_DMA(g_hspi2, (uint8_t*)current_buffer->data[1], LTC2508_DATA_LEN) != HAL_OK)
|
if (HAL_SPI_Receive_DMA(g_hspi2, (uint8_t*)current_buffer->data[1], LTC2508_DATA_LEN) != HAL_OK)
|
||||||
{
|
{
|
||||||
current_buffer->state = LTC2508_BUFFER_EMPTY;
|
current_buffer->state = LTC2508_BUFFER_EMPTY;
|
||||||
g_ltc2508_stats.dma_error_count++;
|
g_ltc2508_stats.dma_error_count++;
|
||||||
g_ltc2508_stats.error_count++;
|
g_ltc2508_stats.error_count++;
|
||||||
g_ltc2508_stats.last_error = LTC2508_ERROR_DMA;
|
g_ltc2508_stats.last_error = LTC2508_ERROR_DMA;
|
||||||
// return LTC2508_ERROR_DMA;
|
return LTC2508_ERROR_DMA;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (HAL_SPI_Receive_DMA(g_hspi3, (uint8_t*)current_buffer->data[2], LTC2508_DATA_LEN) != HAL_OK)
|
if (HAL_SPI_Receive_DMA(g_hspi3, (uint8_t*)current_buffer->data[2], LTC2508_DATA_LEN) != HAL_OK)
|
||||||
{
|
{
|
||||||
current_buffer->state = LTC2508_BUFFER_EMPTY;
|
current_buffer->state = LTC2508_BUFFER_EMPTY;
|
||||||
|
HAL_SPI_DMAStop(g_hspi2);
|
||||||
g_ltc2508_stats.dma_error_count++;
|
g_ltc2508_stats.dma_error_count++;
|
||||||
g_ltc2508_stats.error_count++;
|
g_ltc2508_stats.error_count++;
|
||||||
g_ltc2508_stats.last_error = LTC2508_ERROR_DMA;
|
g_ltc2508_stats.last_error = LTC2508_ERROR_DMA;
|
||||||
// return LTC2508_ERROR_DMA;
|
return LTC2508_ERROR_DMA;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SPI1 作为主机收发,先发一个 dummy 数据触发时钟
|
// static:DMA 传输异步完成,必须保证缓冲区生命周期覆盖整个传输过程
|
||||||
uint16_t dummy_tx[LTC2508_DATA_LEN] = {0}; // 可以是任意值
|
static uint16_t s_dummy_tx[LTC2508_DATA_LEN] = {0};
|
||||||
if (HAL_SPI_TransmitReceive_DMA(g_hspi1, (uint8_t*)dummy_tx, (uint8_t*)current_buffer->data[0], LTC2508_DATA_LEN) != HAL_OK)
|
if (HAL_SPI_TransmitReceive_DMA(g_hspi1, (uint8_t*)s_dummy_tx, (uint8_t*)current_buffer->data[0], LTC2508_DATA_LEN) != HAL_OK)
|
||||||
{
|
{
|
||||||
current_buffer->state = LTC2508_BUFFER_EMPTY;
|
current_buffer->state = LTC2508_BUFFER_EMPTY;
|
||||||
|
HAL_SPI_DMAStop(g_hspi2);
|
||||||
|
HAL_SPI_DMAStop(g_hspi3);
|
||||||
g_ltc2508_stats.dma_error_count++;
|
g_ltc2508_stats.dma_error_count++;
|
||||||
g_ltc2508_stats.error_count++;
|
g_ltc2508_stats.error_count++;
|
||||||
g_ltc2508_stats.last_error = LTC2508_ERROR_DMA;
|
g_ltc2508_stats.last_error = LTC2508_ERROR_DMA;
|
||||||
|
|||||||
@ -7,6 +7,11 @@ static GPIO_TypeDef* g_de_re_port = NULL;
|
|||||||
static uint16_t g_de_re_pin = 0;
|
static uint16_t g_de_re_pin = 0;
|
||||||
volatile uint8_t g_rs485_tx_busy = 0;
|
volatile uint8_t g_rs485_tx_busy = 0;
|
||||||
|
|
||||||
|
/* DMA 异步读取期间此缓冲区必须保持有效;
|
||||||
|
静态副本隔离调用方的包结构体,防止下一 TIM2 ISR 覆盖源包时 DMA 仍在读取 */
|
||||||
|
#define RS485_TX_BUF_SIZE 64
|
||||||
|
static uint8_t s_tx_buf[RS485_TX_BUF_SIZE];
|
||||||
|
|
||||||
void RS485_Init(UART_HandleTypeDef *huart, GPIO_TypeDef* de_re_port, uint16_t de_re_pin)
|
void RS485_Init(UART_HandleTypeDef *huart, GPIO_TypeDef* de_re_port, uint16_t de_re_pin)
|
||||||
{
|
{
|
||||||
g_huart_485 = huart;
|
g_huart_485 = huart;
|
||||||
@ -26,11 +31,15 @@ HAL_StatusTypeDef RS485_SendData(uint8_t *pData, uint16_t Size)
|
|||||||
return HAL_BUSY; // 上一次传输未完成,返回忙状态
|
return HAL_BUSY; // 上一次传输未完成,返回忙状态
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Size > RS485_TX_BUF_SIZE) return HAL_ERROR;
|
||||||
|
|
||||||
|
memcpy(s_tx_buf, pData, Size);
|
||||||
|
|
||||||
g_rs485_tx_busy = 1; // 标记为忙状态
|
g_rs485_tx_busy = 1; // 标记为忙状态
|
||||||
HAL_GPIO_WritePin(g_de_re_port, g_de_re_pin, GPIO_PIN_SET); // 设置为发送模式
|
HAL_GPIO_WritePin(g_de_re_port, g_de_re_pin, GPIO_PIN_SET); // 设置为发送模式
|
||||||
|
|
||||||
// 使用DMA非阻塞发送
|
// 使用DMA非阻塞发送
|
||||||
ret = HAL_UART_Transmit_DMA(g_huart_485, pData, Size);
|
ret = HAL_UART_Transmit_DMA(g_huart_485, s_tx_buf, Size);
|
||||||
|
|
||||||
if (ret != HAL_OK)
|
if (ret != HAL_OK)
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user