ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F103+ATGM332D北斗GPS解析与SD卡日志存储实践

STM32F103+ATGM332D北斗GPS解析与SD卡日志存储实践 简介基于STM32F103单片机与北斗GPS_ATGM332D模块的GPS解码与SD卡存储实验例程面向嵌入式开发初学者及进阶人员帮助快速掌握北斗/GPS定位数据的采集、解析与落盘存储方法。工程整合了ATGM332D模块底层驱动源码完整覆盖单片机系统时钟与UART接口初始化、NMEA标准报文解析、经纬度高度时间速度等关键字段提取以及将解析结果以文件形式写入SD卡的功能模块另外还包含通信异常与校验失败等错误处理逻辑。压缩包内共122个文件以53个C源文件和52个头文件为主辅以汇编启动文件、工程配置文件与文本说明整体包体大小仅838KB目录结构紧凑清晰便于按模块定位代码。目前已有482人学习下载是理解STM32的GPIO、UART、中断以及文件系统操作的实战样板从串口接收、格式切割到文件写入完整链路清晰适合课堂实验与项目预研也可作为后续车载导航、手持设备等定位应用的基础工程。1. 从淘宝模块到离线定位记录仪这包源码把整条链路串起来了拿到“基于STM32F103单片机北斗GPS_ATGM332D模块 GPS_Decode_SDCard测试实验软件例程源码.rar”这个标题多数人第一反应是解压、烧录、看串口输出。但真正值钱的不是几行NMEA解析函数而是它背后那条完整的数据链路ATGM332D输出北斗/GPS双模定位语句STM32F103通过串口接收软件解码成经纬度、时间、速度再写入SD卡形成可离线分析的日志文件。这套组合在车辆轨迹回放、人员巡检打卡、农业机械作业记录、物流追踪设备里非常常见是典型的低成本离线定位方案。本文按硬件接线、NMEA解码、SD卡落盘、上电排错的顺序把每个环节讲透适合正在做定位终端原型、或者想把手头GPS模块接到F103上的开发者。新手可以照参数表接线老手则能在这套例程的边界和踩坑点上省下半天调试时间。2. 硬件搭接与串口分配STM32F103 接 ATGM332D 的引脚和初始化顺序2.1 最小系统上电顺序与 ATGM332D 模块的供电约束STM32F103 最小系统是这块板子的地基但 ATGM332D 对供电的要求比单片机本身苛刻。模块的 VCC 范围是 2.7V 到 3.6V典型值 3.3V峰值电流在冷启动搜索卫星时能到 50mA 左右。如果用 AMS1117-3.3 从 5V 降压供电注意 1117 的压差约 1V输入 5V 没问题但板子上的 3.3V 与 GND 之间至少要并一个 100uF 电解电容加一个 0.1uF 陶瓷电容否则模块在搜星瞬间拉低电压会导致反复重启表现就是串口每隔几秒重复打印一次启动 LOG。上电顺序上先给单片机供电再给 GPS 模块供电或者两者同时上电都可以ATGM332D 内部有上电复位电路不需要手动复位。但有一点要提醒如果模块的 V_BCKP 引脚接了备用电池或大电容模块会记住上次的星历冷启动时间能从 35 秒缩短到 15 秒左右。标题里的例程源码通常默认不处理 V_BCKP直接用 3.3V 供电即可因为测试环境大多在窗口边。引脚配线遵循“串口交叉”原则ATGM332D 的 TXD 接 STM32F103 的 RXD模块的 RXD 接单片机的 TXD。模块第 8 脚TXD和第 9 脚RXD在 3.3V 电平下工作可以直连 F103 的 PA10 和 PA9USART1不需要电平转换。注意模块的 PPS 脉冲脚第 15 脚默认输出 1PPS 秒脉冲测试时可以用来验证定位是否有效但千万别把它接到单片机的 BOOT0 或 NRST 上否则可能引发意外复位。2.2 串口 1 和串口 3 用于 GPS 接收的差异标题里同时出现 GPS_Decode 和 SDCard意味着至少要占用一个串口给 GPS、一个 SPI 给 SD 卡。常见做法是 USART1 接 GPSSPI1 接 SD 卡。如果项目里还需要调试串口打印日志就涉及串口分配问题。F103 的 USART1 挂在 APB2 总线上时钟 72MHzUSART3 挂在 APB1 总线上最大 36MHz这意味着同样设置 115200 波特率USART1 的波特率误差要比 USART3 更小尤其当系统时钟不是精确 72.000MHz 时。USART1 的默认引脚 PA9TX和 PA10RX不带重映射直接可用USART3 的默认引脚 PB10TX和 PB11RX也是默认可用但要注意 PB10/PB11 在 JTAG 调试时可能被占用。如果用标准 20 针 JTAGPB3、PB4、PA15 这些引脚会被调试器占用PB10/PB11 不受影响。但如果你用的是 SWD 两线调试只占 PA13/PA14那么串口 1、串口 3 的默认引脚都能释放。实际项目中我一般把 USART1 给 GPS、USART3 给调试打印因为 GPS 报文约 100 字节每秒一次波特率 9600 就够对波特率误差不敏感调试串口用 115200走 APB2 总线的 USART1 更稳。反过来如果调试串口只是低速打印USART3 完全没问题。2.3 CubeMX 下的 USART 与 SPI 初始化参数无论你用的是标准外设库 v3.5 还是 HAL 库初始化参数本质上一样。用 STM32CubeMX 配置时按下表设定参数可以少踩一半的坑外设引脚模式关键参数USART1PA9 TX / PA10 RX异步收发波特率 9600数据位 8停止位 1无校验无流控USART3PB10 TX / PB11 RX异步收发波特率 115200数据位 8停止位 1无校验SPI1PA5 SCK / PA6 MISO / PA7 MOSI主机模式速率 18MHz极性 CPOL0相位 CPHA0MSB 先行GPIOPA4 或 PB12推挽输出SD 卡 CS 片选初始置高SPI 速率说明一下SD 卡 SPI 模式初始化阶段建议先用 400kHz 以下的低速等发送了 CMD0 和 CMD1/ACMD41 之后再把速率提到 18MHz。HAL 库中可以用__HAL_SPI_SET_BAUDRATE动态调整。标题例程如果直接用 18MHz 初始化部分 SD 卡会卡在 ACMD41 上表现为f_mount返回FR_DISK_ERR。这也是为什么很多源码里 SPI 初始化分两步。// SPI1 和 USART1 的 HAL 初始化片段CubeMX 生成后手动调整 void MX_SPI1_Init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; // CPOL0 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // CPHA0 hspi1.Init.NSS SPI_NSS_SOFT; // 软件片选手动拉 CS hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_128; // 初始 562.5kHz hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi1); }这段代码有几个参数值得较真。SPI_NSS_SOFT表示片选信号由普通 GPIO 手动控制这样方便在发送命令前后精确控制 CS 时序避免硬件 NSS 自动翻转导致 SD 卡状态机错乱。预分频设成 128在 72MHz 主频下得到 562.5kHz符合 SD 卡 SPI 模式的初始化要求。等f_mount成功后再改预分频到 4对应 18MHz读扇区速度能提升近 30 倍。代码逻辑上先低速握手、后高速传输符合 SD 卡规范。3. GPS_Decode 的 NMEA 解析实现从 $GNGGA 到结构体的完整转换3.1 ATGM332D 输出的 NMEA 语句与优先级ATGM332D 默认输出的 NMEA 语句不止一种。典型配置下会周期输出$GNGGA、$GNRMC、$GNGSA、$GPGSV、$BDGSV、$GNVTG等语句每条以$开头、以回车换行\r\n结尾整体约每秒输出一次。$GNGGA是全球定位数据包含定位时间、纬度、经度、定位质量、卫星数、海拔高度是日志存储最常解析的语句$GNRMC是推荐最小定位信息包含日期、速度、航向适合做轨迹回放。标题里的GPS_Decode这个词组暗示源码把解析逻辑独立成了一个模块而不是在中断里直接处理字符串。这种分层是对的串口中断只负责把字节放进环形缓冲区主循环里按行提取 NMEA 帧再交给解析函数。解析函数按语句类型分派到不同的处理函数。在实际设备上ATGM332D 处于冷启动阶段时输出的是$GNRMC吗不是。冷启动时经纬度为 0GGA 和 RMC 依然会输出但定位状态位是V无效而不是A有效。所以解析代码必须判断状态位否则会把0.0000,N,0.0000,E这种无效数据写进 SD 卡后续做轨迹回放会出现原点跳变。3.2 环形缓冲区接收进入解析前的关键准备串口中断接收 GPS 数据时如果每收到一个字节就立刻去解析字符串会阻塞中断且容易丢失后续字节。标准做法是维护一个环形缓冲区中断里只做buf[tail] data主循环里检查head ! tail再取数据。环形缓冲区大小建议 512 字节因为 NMEA 最长的一条语句约 100 字节加上 GGA 后紧跟 RMC 的连续输出512 字节能存下 3 秒内的数据足够主循环处理。一个容易踩的坑是缓冲区回绕判断。写缓冲区时tail到达数组末尾要归零判断空与满不能只用head tail因为满状态也会出现这个等式。常见做法是留一个空位即缓冲区大小是 2 的幂且实际可用空间为 N-1或者用一个单独的计数变量。下面这段代码是标题例程中比较典型的实现#define RING_BUF_SIZE 512 static uint8_t ring_buf[RING_BUF_SIZE]; static volatile uint16_t head 0, tail 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t ch USART_ReceiveData(USART1); ring_buf[tail] ch; tail (tail 1) % RING_BUF_SIZE; // 回绕取模 } }中断函数中tail (tail 1) % RING_BUF_SIZE实现了回绕512 是 2 的幂编译器会把取模优化成位与运算效率高。但如果缓冲区写满而主循环来不及读新数据会覆盖旧数据吗不会因为没有做“满”保护tail会追上head覆盖旧数据。GPS 每秒约 700 字节主循环任何一次卡顿超过 1 秒就可能丢帧。所以建议在写 SD 卡时采用后文的分批写入策略避免长时间阻塞读串口。主循环中从缓冲区取一帧数据用的是逐字符查找\n的方式从head开始读直到遇见\n截取出以$开头的一整行然后交给解析函数。这样可以保证解析函数处理的是完整 NMEA 帧不会出现半帧错位。3.3 用 strstr 和 strchr 拆字段避免 strcmp 的整串比较坑拿到一行$GNGGA,101234.00,3101.23456,N,12123.45678,E,1,08,1.2,15.6,M,0.0,M,,*5F之后首先要判断语句类型。很多初学者用strcmp(line, $GNGGA)结果永远匹配不上因为 line 里包含完整的逗号分隔内容而strcmp是比较整个字符串。正确做法是strstr(line, $GNGGA)判断前缀或者直接比较前 6 个字符strncmp(line, $GNGGA, 6)。顺手对比$GNRMC也一样。字段拆分推荐用strchr配合指针偏移。每找到一个逗号就替换为字符串结束符\0然后指针后移继续找下一个。这种方式原地修改缓冲区不额外分配内存。下面以解析$GNGGA为例提取关键字段typedef struct { uint8_t valid; // 定位有效标识 float latitude; // 纬度度 float longitude; // 经度度 uint8_t satellites; // 可见卫星数 float altitude; // 海拔米 uint8_t hour, min, sec; // UTC 时间 } gps_info_t; void GPS_Decode_GGA(char *line, gps_info_t *gps) { char *p line; char *field[15]; // GGA 最多 14 个逗号分隔字段 int idx 0; while (idx 15 p ! NULL) { field[idx] p; p strchr(p, ,); if (p) *p \0; // 逗号替换为结束符p 跳到下个字段 } if (field[6][0] 0) { // 定位状态0无效1GPS 定位2差分定位 gps-valid 0; } else { gps-valid 1; } gps-hour (field[1][0] - 0) * 10 (field[1][1] - 0); // UTC 时 gps-min (field[1][2] - 0) * 10 (field[1][3] - 0); gps-sec (field[1][4] - 0) * 10 (field[1][5] - 0); gps-latitude atof(field[2]) / 100.0; // ddmm.mmmm 转 dd.dddddd gps-longitude atof(field[4]) / 100.0; gps-satellites atoi(field[7]); gps-altitude atof(field[9]); }解析逻辑有几个容易忽略的细节。field[2]的格式是度分格式例如3101.23456表示 31 度 01.23456 分转成十进制度需要先除以 100 得到31.0123456这是“度 分”合并表示后四位小数是分的精度。field[6]是定位质量指示0 表示无定位1 表示 GPS 定位2 表示差分定位。北斗与 GPS 双模下这个字段为 1 时可能来自北斗卫星但 NMEA 语句不直接区分星座需要看$GNGSA里的卫星编号前缀才能分清楚。代码中把,替换为\0是对原始缓冲区做原地修改这意味着后续不能再对整行做字符串操作但这种局部解析场景完全够用。字段数量 15 的设定对应标准 GGA 语句如果模块固件追加了厂商自定义扩展字段解析会截断到第 15 个字段不影响前序字段的正确性。4. 把解析结果写入 SD 卡FATFS 初始化与缓冲区落盘策略4.1 SPI 模式下 SD 卡的 FATFS 挂载流程SD 卡在 SPI 模式下和 FATFS 配合是 STM32F103 上最常用的数据存储组合。挂载流程分三层底层 SPI 读写扇区、中层 SD 卡命令协议、上层 FATFS 文件操作。HAL 库里有 SPI 读写程序但 SD 卡的 CMD0、CMD8、ACMD41 命令序列需要自己实现。初始化命令序列大概如下CS 拉高发送大于 74 个时钟脉冲至少 10 字节 0xFF让 SD 卡进入 SPI 模式CS 拉低发送 CMD00x40等待响应 0x01 表示进入空闲态发送 CMD80x48带参数 0x000001AA 和校验 0x87判断卡是否支持 SDHC循环发送 CMD55 ACMD410x77带参数 0x40000000直到响应为 0x00。整个过程在低速 SPI 下完成成功后初始化完成。FATFS 挂载代码通常是这样的FATFS fs; // 文件系统对象 FIL file; // 文件对象 FRESULT res; res f_mount(fs, , 1); // 立即挂载到驱动 0 if (res FR_OK) { // 挂载成功接下来创建或打开日志文件 } else if (res FR_NO_FILESYSTEM) { // SD 卡没有 FAT 文件系统需要格式化f_mkfs }f_mount的第三个参数设为 1 表示强制立即挂载如果 SD 卡上已有 FAT32 文件系统则直接可用返回FR_NO_FILESYSTEM时用f_mkfs(, FM_FAT32, 0, work_buf, sizeof(work_buf))格式化注意格式化会清空整张卡。标题里的例程如果是第一次跑SD 卡常常没格式化这一步是必踩的坑。格式化缓冲区work_buf在 FF_USE_LFN 开启时还需要额外空间建议定义 512 字节数组。SPI 模式还涉及一个常见兼容性问题部分新出的大容量 SDXC 卡不兼容 SPI 模式或者兼容性很差表现为初始化超时。手头测试环境最好备一张 2GB 到 16GB 的普通 microSD 卡格式化为 FAT32簇大小 32KB。这也是很多 GPS 记录仪源码只写 FATFS 而不处理 exFAT 的原因。4.2 缓冲与落盘节奏f_write 和 f_sync 的取舍GPS 数据每秒一条如果每条都f_open、f_write、f_closeSD 卡连续写入速度没问题但树叶目录的频繁更新会导致 FAT 表反复擦写缩短卡寿命。更合理的策略是内存中开辟一个 512 字节或 1KB 的缓冲区攒够一定量再写入。FATFS 本身有内部扇区缓冲默认 512 字节但如果每次只写几十字节f_write会先拷贝到内部缓冲等缓冲满或调用f_sync时才真正落盘。关键问题是掉电保护。如果设备在写入过程中突然断电FATFS 缓冲区的数据会丢失文件系统可能留下不一致的目录项。常见的做法是f_write后隔一段时间调用一次f_sync把缓冲区刷到 SD 卡同时更新文件长度。定位设备通常用 JSON 或 CSV 格式逐行追加日志我会这样组织写入char line_buf[128]; int len snprintf(line_buf, sizeof(line_buf), %02d:%02d:%02d,%.6f,%.6f,%d,%.1f\r\n, gps.hour, gps.min, gps.sec, gps.latitude, gps.longitude, gps.satellites, gps.altitude); res f_write(file, line_buf, len, bw); if (res FR_OK bw len) { write_counter; if (write_counter 30) { // 约 30 秒同步一次 f_sync(file); write_counter 0; } }这段代码把每行 CSV 写入文件流write_counter计数达到 30 次时调用f_sync。f_write的返回值FR_OK表示操作成功bw是实际写入字节数必须等于len才算完整写入。调用f_sync的时机要权衡频繁调用会拖慢主循环30 秒一次既保证了 30 秒内最多丢失 30 条日志又不会明显影响 GPS 接收。如果想更稳写入每次 1KB 以上的块再落盘日志断电丢失量更少。4.3 文件目录设计按天切分日志文件避免单个文件无限膨胀长时间运行后单个日志文件会越来越大FAT32 单文件上限 4GB但对嵌入式设备来说文件超过 100MB 后用电脑打开也费劲。更合理的做法是按日期切分文件名每天创建一个新文件。文件名生成用 RTC 时钟或 GPS 时间。GPS 时间来自 UTC且代表格林尼治时间如果要按北京时间切分需要加 8 小时再判断日期。文件命名建议用LOG_YYYYMMDD.csv这种固定长度格式方便排序和后续脚本处理。代码中拼接文件名需要额外注意字符串长度和目录存在性char fname[32]; uint8_t year gps.utc_year; // 已转换成北京时间 uint8_t mon gps.utc_mon; uint8_t day gps.utc_day; sprintf(fname, 0:/LOG_%04d%02d%02d.csv, 2000 year, mon, day); f_open(file, fname, FA_OPEN_ALWAYS | FA_WRITE); f_lseek(file, f_size(file)); // 文件已存在时指针移到末尾继续追加f_open用FA_OPEN_ALWAYS | FA_WRITE如果文件不存在就创建存在就打开。打开后f_lseek到文件末尾避免覆盖旧数据。如果单片机没有 RTC 电池掉电后日期从 2000 年重新开始可能导致文件名重复覆盖。所以日志文件打开成功后把当前的日期与文件名日期比对如果确实需要新文件就f_close旧的再开新的。这个判断不复杂但能有效避免“日期归零后日志互相覆盖”的悲剧。还有一种情况必须处理写日志过程中f_open失败最常见原因是 SD 卡被拔出或 FATFS 文件系统损坏。此时代码应该进入重试状态尝试重新挂载文件系统一次如果仍然失败则停止写文件但保持 GPS 解码运行把数据留在 RAM 缓冲区并点亮错误指示灯。这样至少保证定位功能不受影响。5. 上电自测与三层排错先拆开验证定位、串口、SD 卡5.1 用 USB-TTL 直接验证 ATGM332D 是否输出 NMEA拿到例程第一步别急着烧录。先把 ATGM332D 模块的 TXD 接 USB-TTL 的 RX模块 GND 与转换器共地用串口助手以 9600 波特率监听。这一步的关键是确认模块本身工作正常。打开接地气的串口助手正常现象是每秒出现一组以$GNGGA、$GNRMC开头的语句如果没有任何输出优先检查供电模块上的 PPS 指示灯是否闪烁。上电瞬间 PPS 灯会常亮一下随后进入搜索模式定位成功后每秒闪一次。如果串口助手收到乱码先核对波特率ATGM332D 出厂默认 9600部分商家会配置成 115200。如果你买的模块是带有“GPS北斗”丝印的裸板引脚间距 2.54mm大概率默认 9600。乱码还有一种可能是 USB-TTL 的电平问题如果模块是 3.3V 电平而 USB-TTL 的逻辑电平是 5V长时间直接连接会损坏模块建议用 3.3V 供电的 USB-TTL或接一个电平转换。确认模块输出正常再烧录例程到 F103 才有意义。5.2 DAP 下载失败与 BOOT1 引脚状态的处理例程烧录过程中最常遇到的问题是 DAP 下载失败报错信息类似“Cannot access target”且反复连接不上。排查顺序很固定先看 BOOT0 和 BOOT1 的电平。BOOT1 在 F103 上通过 BOOT1 引脚的电平决定启动模式当 BOOT11、BOOT01 时从系统存储器启动进入 ISP 模式当 BOOT1 悬空或拉低、BOOT00 时从 Flash 启动正常执行用户程序。很多最小系统板把 BOOT1 悬空DAP 下载通常没问题但如果你的板子上 BOOT1 被上拉到高电平DAP 能连接但烧录后程序不运行因为芯片一直在系统存储器里打转。下载失败如果同时出现在 SWD 模式还要检查 PA13/PA14 是否被程序复用成了 GPIO。标题里的例程如果初始化了 USART1 的 PA9/PA10不影响 SWD但如果 SPI1 的 NSS 引脚用了 PA15、SCK 用了 PA5那也不冲突。真正的坑是 20 针 JTAG 模式下使用 PA15、PB3、PB4 作为 GPIO。解决办法改用 SWD 模式只占 PA13/PA14把 PB3/PB4 释放出来。启动文件“启动文件下载”这个热词在这里也需要一笔F103 的启动文件startup_stm32f10x_hd.s必须与芯片容量匹配CL 型号用startup_stm32f10x_cl.s高密度用startup_stm32f10x_hd.s。如果例程包的工程文件里缺失启动文件从标准外设库 v3.5 里拷贝注意不要拷成ld或md的版本否则中断向量表偏移错位程序可能跑飞。5.3 把解析中间态透传到调试串口例程排错时GPS 解析是否成功、SD 卡是否写入都不能靠现象猜。最直接的方法是在 USART3 上输出调试信息把关键状态一步一步打出来。调试串口输出格式可以这样组织printf(GPS RAW: %s\r\n, line_buf); // 原始 NMEA 帧 printf(GPS DEC: %d %06.4f %06.4f %d\r\n, gps.valid, gps.latitude, gps.longitude, gps.satellites); printf(SD WRITE: %d bytes, ret%d\r\n, len, res);printf重定向到 USART3 时注意fputc的实现里要等待发送完成标志否则高波特率下会丢字符。用 HAL 库就是HAL_UART_Transmit用标准外设库就是USART_SendData加循环等待USART_FLAG_TXE。调试时先打原始 NMEA确认串口接收链路正常再打解码后的结构体确认解析函数没有算错经纬度最后打f_write的返回值确认落盘链路。三层输出对照哪里断了一目了然。提示调试串口和 GPS 串口不要接反。我就见过把 USB-TTL 接到 PA9/PA10 上调试结果 GPS 数据包全被调试电脑接收单片机什么也没收到的案例。确认接线时以模块丝印为准不要只看排针顺序。另一个实用技巧是把关键状态压缩成单个字节用 SWD 的ITM或普通 GPIO 翻转输出配合逻辑分析仪看时序。比如 PA0 翻转一次表示收到一帧 GPSPA1 翻转一次表示写完一次 SD 卡。逻辑分析仪抓 10 秒波形就能直观看到解析频率和落盘频率是否正常这个手段比串口打印更不干扰时序。能把原始帧、解码结构体、SD 卡返回值三个中间态都透传出来这套例程基本就没有看不到的问题了。最后一个建议把调试串口的输出级别做成宏开关量产固件里关掉调试输出只保留文件系统错误码上报别让调试信息占据存储带宽。本文还有配套的精品资源点击获取
返回列表