STM_ATEM/User/gps_driver.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

447 lines
13 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.

/**
******************************************************************************
* @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;
}