- 新增混流解析函数 `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经纬度/光泵),支持灵活配置 - 新增文件模式下的分页浏览功能(滑块/上一页/下一页) - 优化实时模式下的光泵数据显示与表格列过滤
447 lines
13 KiB
C
447 lines
13 KiB
C
/**
|
||
******************************************************************************
|
||
* @file gps_driver.c
|
||
* @brief GPS NMEA数据接收和解析驱动实现
|
||
* @author Your Name
|
||
* @date 2026-02-07
|
||
******************************************************************************
|
||
*/
|
||
|
||
/* Includes ------------------------------------------------------------------*/
|
||
#include "gps_driver.h"
|
||
|
||
/* Private typedef -----------------------------------------------------------*/
|
||
|
||
/* Private define ------------------------------------------------------------*/
|
||
|
||
/* Private macro -------------------------------------------------------------*/
|
||
|
||
/* Private variables ---------------------------------------------------------*/
|
||
static uint8_t gps_rx_buffer[GPS_RX_BUFFER_SIZE]; // DMA接收缓冲区
|
||
static uint8_t gps_rx_byte; // 单字节接收缓冲
|
||
static char gps_nmea_buffer[GPS_NMEA_MAX_LENGTH]; // NMEA语句缓冲区
|
||
static uint16_t gps_nmea_index = 0; // NMEA缓冲区索引
|
||
static GPS_Data_t gps_data; // GPS数据
|
||
|
||
/* Private function prototypes -----------------------------------------------*/
|
||
static void GPS_ParseNMEA(char *nmea);
|
||
static void GPS_ParseGPGGA(char *nmea);
|
||
static void GPS_ParseGPRMC(char *nmea);
|
||
static double GPS_ConvertToDecimal(const char *coord, char direction);
|
||
|
||
/* Exported functions --------------------------------------------------------*/
|
||
|
||
/**
|
||
* @brief 初始化GPS驱动
|
||
* @retval HAL状态
|
||
*/
|
||
HAL_StatusTypeDef GPS_Init(void)
|
||
{
|
||
HAL_StatusTypeDef status;
|
||
|
||
// 清空GPS数据
|
||
memset(&gps_data, 0, sizeof(GPS_Data_t));
|
||
gps_data.data_valid = 0;
|
||
|
||
// 清空缓冲区
|
||
memset(gps_rx_buffer, 0, GPS_RX_BUFFER_SIZE);
|
||
memset(gps_nmea_buffer, 0, GPS_NMEA_MAX_LENGTH);
|
||
gps_nmea_index = 0;
|
||
|
||
// 启动UART接收(单字节中断接收)
|
||
status = HAL_UART_Receive_IT(&GPS_UART_HANDLE, &gps_rx_byte, 1);
|
||
|
||
if (status == HAL_OK) {
|
||
// 可选:启用UART空闲中断
|
||
__HAL_UART_ENABLE_IT(&GPS_UART_HANDLE, UART_IT_IDLE);
|
||
}
|
||
|
||
return status;
|
||
}
|
||
|
||
/**
|
||
* @brief GPS数据接收处理(在主循环中调用)
|
||
* @retval None
|
||
*/
|
||
void GPS_Process(void)
|
||
{
|
||
// 检查数据是否超时
|
||
if (gps_data.data_valid) {
|
||
uint32_t current_tick = HAL_GetTick();
|
||
if ((current_tick - gps_data.last_update_tick) > GPS_DATA_TIMEOUT_MS) {
|
||
gps_data.data_valid = 0; // 数据超时,标记为无效
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 获取GPS数据
|
||
* @param gps_data_out: 指向GPS数据结构体的指针
|
||
* @retval 1=数据有效, 0=数据无效或超时
|
||
*/
|
||
uint8_t GPS_GetData(GPS_Data_t *gps_data_out)
|
||
{
|
||
if (gps_data_out == NULL) {
|
||
return 0;
|
||
}
|
||
|
||
// 复制GPS数据
|
||
memcpy(gps_data_out, &gps_data, sizeof(GPS_Data_t));
|
||
|
||
return gps_data.data_valid;
|
||
}
|
||
|
||
/**
|
||
* @brief 检查GPS数据是否有效
|
||
* @retval 1=有效, 0=无效
|
||
*/
|
||
uint8_t GPS_IsDataValid(void)
|
||
{
|
||
return gps_data.data_valid;
|
||
}
|
||
|
||
/**
|
||
* @brief 获取GPS时间字符串
|
||
* @param buffer: 输出缓冲区
|
||
* @param size: 缓冲区大小
|
||
* @retval None
|
||
*/
|
||
void GPS_GetTimeString(char *buffer, uint16_t size)
|
||
{
|
||
if (buffer == NULL || size == 0) {
|
||
return;
|
||
}
|
||
|
||
if (gps_data.data_valid) {
|
||
snprintf(buffer, size, "%02d:%02d:%02d.%03d UTC",
|
||
gps_data.time.hour,
|
||
gps_data.time.minute,
|
||
gps_data.time.second,
|
||
gps_data.time.millisec);
|
||
} else {
|
||
snprintf(buffer, size, "No GPS Time");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 获取GPS位置字符串
|
||
* @param buffer: 输出缓冲区
|
||
* @param size: 缓冲区大小
|
||
* @retval None
|
||
*/
|
||
void GPS_GetPositionString(char *buffer, uint16_t size)
|
||
{
|
||
if (buffer == NULL || size == 0) {
|
||
return;
|
||
}
|
||
|
||
if (gps_data.data_valid) {
|
||
snprintf(buffer, size, "Lat: %.6f%c, Lon: %.6f%c, Alt: %.1fm, Sats: %d",
|
||
gps_data.position.latitude,
|
||
gps_data.position.lat_direction,
|
||
gps_data.position.longitude,
|
||
gps_data.position.lon_direction,
|
||
gps_data.position.altitude,
|
||
gps_data.position.satellites);
|
||
} else {
|
||
snprintf(buffer, size, "No GPS Position");
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @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接收完成回调函数
|
||
* @param huart: UART句柄
|
||
* @retval None
|
||
*/
|
||
void GPS_UART_RxCpltCallback(UART_HandleTypeDef *huart)
|
||
{
|
||
if (huart->Instance == GPS_UART_HANDLE.Instance) {
|
||
// 处理接收到的字节
|
||
char received_char = (char)gps_rx_byte;
|
||
|
||
// 检测NMEA语句开始标志 '$'
|
||
if (received_char == '$') {
|
||
gps_nmea_index = 0;
|
||
gps_nmea_buffer[gps_nmea_index++] = received_char;
|
||
}
|
||
// 检测NMEA语句结束标志 '\n'
|
||
else if (received_char == '\n') {
|
||
if (gps_nmea_index > 0 && gps_nmea_index < GPS_NMEA_MAX_LENGTH) {
|
||
gps_nmea_buffer[gps_nmea_index] = '\0'; // 字符串结束符
|
||
GPS_ParseNMEA(gps_nmea_buffer); // 解析NMEA语句
|
||
gps_nmea_index = 0; // 重置索引
|
||
}
|
||
}
|
||
// 累积NMEA语句字符
|
||
else if (gps_nmea_index > 0 && gps_nmea_index < (GPS_NMEA_MAX_LENGTH - 1)) {
|
||
if (received_char != '\r') { // 忽略回车符
|
||
gps_nmea_buffer[gps_nmea_index++] = received_char;
|
||
}
|
||
}
|
||
|
||
// 继续接收下一个字节
|
||
HAL_UART_Receive_IT(&GPS_UART_HANDLE, &gps_rx_byte, 1);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief UART空闲中断回调函数
|
||
* @param huart: UART句柄
|
||
* @retval None
|
||
*/
|
||
void GPS_UART_IdleCallback(UART_HandleTypeDef *huart)
|
||
{
|
||
if (huart->Instance == GPS_UART_HANDLE.Instance) {
|
||
// 可以在这里处理空闲中断
|
||
// 目前使用单字节接收,不需要特殊处理
|
||
}
|
||
}
|
||
|
||
/* Private functions ---------------------------------------------------------*/
|
||
|
||
/**
|
||
* @brief 解析NMEA语句
|
||
* @param nmea: NMEA语句字符串
|
||
* @retval None
|
||
*/
|
||
static void GPS_ParseNMEA(char *nmea)
|
||
{
|
||
if (nmea == NULL || nmea[0] != '$') {
|
||
return;
|
||
}
|
||
|
||
if (strncmp(nmea, "$GPGGA", 6) == 0 || strncmp(nmea, "$GNGGA", 6) == 0) {
|
||
GPS_ParseGPGGA(nmea);
|
||
} else if (strncmp(nmea, "$GPRMC", 6) == 0 || strncmp(nmea, "$GNRMC", 6) == 0) {
|
||
GPS_ParseGPRMC(nmea);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 解析GPGGA语句
|
||
* @param nmea: GPGGA语句字符串
|
||
* @retval None
|
||
*
|
||
* GPGGA格式示例:
|
||
* $GPGGA,123519.00,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47
|
||
*
|
||
* 字段说明:
|
||
* 0: $GPGGA
|
||
* 1: UTC时间 (hhmmss.ss)
|
||
* 2: 纬度 (ddmm.mmmm)
|
||
* 3: 纬度方向 (N/S)
|
||
* 4: 经度 (dddmm.mmmm)
|
||
* 5: 经度方向 (E/W)
|
||
* 6: 定位质量 (0=无效, 1=GPS, 2=DGPS, etc.)
|
||
* 7: 使用的卫星数量
|
||
* 8: HDOP水平精度因子
|
||
* 9: 海拔高度
|
||
* 10: 高度单位 (M)
|
||
* 11: 大地水准面高度
|
||
* 12: 高度单位 (M)
|
||
* 13: 差分GPS数据年龄
|
||
* 14: 差分参考站ID
|
||
* 15: 校验和
|
||
*/
|
||
static void GPS_ParseGPGGA(char *nmea)
|
||
{
|
||
char *token;
|
||
char *saveptr;
|
||
int field_index = 0;
|
||
char temp_buffer[32];
|
||
|
||
// 使用strtok_r进行字符串分割(线程安全)
|
||
token = strtok_r(nmea, ",", &saveptr);
|
||
|
||
while (token != NULL) {
|
||
switch (field_index) {
|
||
case 1: // UTC时间
|
||
if (strlen(token) >= 6) {
|
||
// 解析时间 hhmmss.ss
|
||
char hour_str[3] = {token[0], token[1], '\0'};
|
||
char min_str[3] = {token[2], token[3], '\0'};
|
||
char sec_str[3] = {token[4], token[5], '\0'};
|
||
|
||
gps_data.time.hour = (uint8_t)atoi(hour_str);
|
||
gps_data.time.minute = (uint8_t)atoi(min_str);
|
||
gps_data.time.second = (uint8_t)atoi(sec_str);
|
||
|
||
// 解析毫秒(如果有)
|
||
if (strlen(token) > 7 && token[6] == '.') {
|
||
char ms_str[4] = {0};
|
||
strncpy(ms_str, &token[7], 3);
|
||
gps_data.time.millisec = (uint16_t)atoi(ms_str);
|
||
} else {
|
||
gps_data.time.millisec = 0;
|
||
}
|
||
}
|
||
break;
|
||
|
||
case 2: // 纬度
|
||
if (strlen(token) > 0) {
|
||
strncpy(temp_buffer, token, sizeof(temp_buffer) - 1);
|
||
temp_buffer[sizeof(temp_buffer) - 1] = '\0';
|
||
}
|
||
break;
|
||
|
||
case 3: // 纬度方向
|
||
if (strlen(token) > 0) {
|
||
gps_data.position.lat_direction = token[0];
|
||
// 转换纬度为十进制度
|
||
gps_data.position.latitude = GPS_ConvertToDecimal(temp_buffer, token[0]);
|
||
}
|
||
break;
|
||
|
||
case 4: // 经度
|
||
if (strlen(token) > 0) {
|
||
strncpy(temp_buffer, token, sizeof(temp_buffer) - 1);
|
||
temp_buffer[sizeof(temp_buffer) - 1] = '\0';
|
||
}
|
||
break;
|
||
|
||
case 5: // 经度方向
|
||
if (strlen(token) > 0) {
|
||
gps_data.position.lon_direction = token[0];
|
||
// 转换经度为十进制度
|
||
gps_data.position.longitude = GPS_ConvertToDecimal(temp_buffer, token[0]);
|
||
}
|
||
break;
|
||
|
||
case 6: // 定位质量
|
||
if (strlen(token) > 0) {
|
||
gps_data.position.fix_status = (GPS_FixStatus_t)atoi(token);
|
||
}
|
||
break;
|
||
|
||
case 7: // 卫星数量
|
||
if (strlen(token) > 0) {
|
||
gps_data.position.satellites = (uint8_t)atoi(token);
|
||
}
|
||
break;
|
||
|
||
case 8: // HDOP
|
||
if (strlen(token) > 0) {
|
||
gps_data.position.hdop = (float)atof(token);
|
||
}
|
||
break;
|
||
|
||
case 9: // 海拔高度
|
||
if (strlen(token) > 0) {
|
||
gps_data.position.altitude = atof(token);
|
||
}
|
||
break;
|
||
|
||
default:
|
||
break;
|
||
}
|
||
|
||
token = strtok_r(NULL, ",", &saveptr);
|
||
field_index++;
|
||
}
|
||
|
||
// 更新数据有效标志和时间戳
|
||
if (gps_data.position.fix_status > GPS_FIX_INVALID) {
|
||
gps_data.data_valid = 1;
|
||
gps_data.last_update_tick = HAL_GetTick();
|
||
} else {
|
||
gps_data.data_valid = 0;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @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坐标格式转换为十进制度
|
||
* @param coord: GPS坐标字符串 (ddmm.mmmm 或 dddmm.mmmm)
|
||
* @param direction: 方向字符 (N/S/E/W)
|
||
* @retval 十进制度数
|
||
*
|
||
* 示例:
|
||
* 输入: "4807.038", 'N'
|
||
* 输出: 48.1173 (度)
|
||
*/
|
||
static double GPS_ConvertToDecimal(const char *coord, char direction)
|
||
{
|
||
if (coord == NULL || strlen(coord) < 4) {
|
||
return 0.0;
|
||
}
|
||
|
||
double value = atof(coord);
|
||
|
||
// 判断是纬度还是经度(纬度2位度数,经度3位度数)
|
||
int degree_digits = (strlen(coord) >= 5 && coord[4] == '.') ? 2 : 3;
|
||
|
||
// 提取度数和分钟
|
||
double degrees = (int)(value / 100.0);
|
||
double minutes = value - (degrees * 100.0);
|
||
|
||
// 转换为十进制度
|
||
double decimal = degrees + (minutes / 60.0);
|
||
|
||
// 根据方向调整符号
|
||
if (direction == 'S' || direction == 'W') {
|
||
decimal = -decimal;
|
||
}
|
||
|
||
return decimal;
|
||
}
|