简介:这是一份基于STC15F2K60S2单片机的Air530卫星授时继电器工程源码,面向单片机开发者与物联网、自动化控制方向的学习者,解决GPS高精度授时与继电器定时开关的控制需求。压缩包共收录19个文件,以C程序源码、头文件、Keil工程配置、Hex烧录文件及OLED显示资源为主,整体仅76KB,结构紧凑、便于直接打开工程学习或移植复用。源码覆盖Air530模块串口通信、UTC时间解析、EEPROM定时参数存储、OLED状态显示和继电器驱动逻辑,同时附有Keil工程与可烧录Hex文件,便于查看完整编译链路或直接验证效果;通过分析代码可掌握GPS数据读取、时间比对及定时输出等关键实现,为二次开发节省大量时间。目前已有424人学习/下载,适合具备一定单片机基础并希望深入理解卫星授时模块应用或制作定时控制装置的开发者参考。
1. 用 Air530 做卫星授时继电器:先让单片机拥有一套“可信时钟”
搞工业定时控制的人都知道,最怕的不是继电器动作慢,而是不知道“当前是哪一秒”。单片机自带 RTC 一天能漂几十秒,联网校时在断网车间里又指望不上,这时候卫星授时是最直接的出路。Air530 是正点原子推出的一款 GPS+北斗双模模块,把它的串口接到单片机,用 NMEA 报文里的 UTC 时间校准本地时钟,再用模块的 PPS 秒脉冲把动作时刻对齐到秒边界,就能让继电器在指定时刻可靠吸合。这条链路拆开看是四个环节:Air530 模块使用、单片机 GPS 数据解析、UTC 时间换算、GPIO 驱动继电器输出。适合做高精度定时电源、场馆自动化打铃或正在做单片机 GPS 课设的嵌入式开发者。
2. Air530 模块使用基础:GPS 模块接线、天线与 NMEA 输出配置
2.1 为什么打样阶段先选 Air530 这类现成 GPS 模块
Air530 这类 GPS 模块的定位是“开箱即串口”:上电后自动通过 UART 输出 NMEA 0183 协议句子,不需要额外 SDK 就能在串口助手看到数据。模块板载无源陶瓷天线,引出供电、UART 和 PPS 秒脉冲引脚,有些版本还带 IPEX 座,方便外接有源天线。对做授时继电器来说,选模块的核心指标不是定位精度,而是时间输出的一致性和 PPS 引脚是否独立引出。
这里有个常见误解:看到模块手册写着定位精度 2.5 米,就以为授时也准。实际上 GPS 授时误差主要来自接收机内部时钟与卫星时间同步的过程,以及报文传输链路里的抖动,定位精度高不等于授时误差小。好在民用 GPS 接收机输出 UTC 时间的精度对本项目完全够用,Air530 的 PPS 引脚用途就是给单片机一个对外部世界“整秒”的硬件中断。
2.2 单片机 GPS 接线表与 3.3V 电平注意点
Air530 供电 3.3V,UART 也是 3.3V 电平。用 STM32F103 这类 3.3V 系统可以直接连接;如果用 51 单片机,模块 TX 输出到 51 的 RX 没有问题,但 51 的 TX 输出接近 5V 高电平,建议对地分压后再接到模块 RX,避免长期超压损坏模块引脚。
| 信号 | Air530 引脚 | 单片机引脚 | 说明 |
|---|---|---|---|
| 电源 | VCC 3.3V | 3.3V 电源 | 电流峰值约 30~50mA,注意需稳定供电 |
| 地 | GND | GND | 与单片机共地,不能省略 |
| 模块发送 | TXD | USART2_RX(PA3) | NMEA 数据流入口 |
| 模块接收 | RXD | USART2_TX(PA2) | 上电后若不需要配置指令可悬空 |
| 秒脉冲 | PPS | PB1 + EXTI1 | 每秒一个高脉冲,接外部中断 |
| 有源天线 | IPEX | — | 放大器供电由模块 VCC 通过偏置电路提供 |
接完线先别写代码,用 USB-TTL 转接模块直接听 Air530 的 TXD。波特率先试 9600,再试 115200,部分固件出厂配置不同。能连续看到$GPRMC、$GPGGA这类句子,说明模块和天线环境都正常。串口收数据建议用空闲中断或 DMA 配合行缓冲,不建议在主循环里逐字节阻塞等待,否则后面 PPS 中断来了抢不到 CPU。发送、跳变频繁的报文按 1Hz 输出足够授时场景使用,不需要刻意配 5Hz。
2.3 Air530 模块使用时的天线选择:无源陶瓷天线与有源天线场景
授时项目最常见的失败不是代码问题,是收不到星。Air530 板载的无源陶瓷天线增益有限,适合外壳朝上、靠近窗口的环境。设备如果装入金属机箱或贴墙安装,就要用 IPEX 座外接有源天线,否则冷启动可能十几分钟不定位。差异在于:无源陶瓷天线靠板载 LNA 放大信号,外接有源天线自带放大器,需要供电,接线时要确认天线规格,不要盲插。
天线放置的经验有两条:陶瓷天线朝上,远离继电器线圈和大电流走线。继电器吸合瞬间线圈产生磁场突变,离天线太近会让接收机短暂失锁,室内环境中尤其明显。
提示:调试阶段先让继电器线路悬空,等模块完成搜星锁定再接入负载,避免吸合噪声干扰定位过程。
3. 卫星授时原理:解析 GPS 数据里的 UTC 时间并用 PPS 对齐秒边界
3.1 NMEA 0183 里的时间字段:为什么授时优先用 $GPRMC
GPS 模块输出的 NMEA 句子有很多种,授时项目只用一句$GPRMC。典型报文如下:
$GPRMC,063555.000,A,2233.1234,N,11355.5678,E,0.0,0.0,150624,4.0,W,A*38按逗号拆开看关键字段:
| 字段位置 | 示例值 | 含义 |
|---|---|---|
| 1 | 063555.000 | UTC 时间,HHMMSS.SSS |
| 2 | A | 定位有效(V 表示无效) |
| 4 | 2233.1234,N | 纬度,本项目不需要 |
| 6 | 11355.5678,E | 经度,本项目不需要 |
| 9 | 150624 | UTC 日期,DDMMYY |
$GPGGA句子也能拿到 UTC 时间,但它不包含日期,还要靠第二条句子凑日期。RMC 一句话同时带时间和日期,对单片机来说少一次帧同步,更省事。
3.2 C 语言解析 RMC 时间与日期的代码实现
NMEA 句子是逗号分隔文本,解析思路是逐字段扫描。下面是 STM32 HAL 环境下可直接用的函数,只读取需要的字段,不修改原始缓冲区:
// 解析 $GPRMC 中的 UTC 时间与日期 // 入参: rmc 指向已收到并以 '\0' 结尾的 RMC 行 // 出参: h, m, s, ms 为时、分、秒、毫秒,day/month/year 为公历日期 // 返回: 1 表示定位有效且字段齐全,0 表示不可用于授时 int parse_rmc_time(const char *rmc, int *h, int *m, int *s, int *ms, int *day, int *month, int *year) { const char *p = rmc; int field = 0; char tmp[16]; while (*p) { if (*p == ',') { field++; p++; continue; } if (field == 1) { // 时间字段 HHMMSS.SSS int len = 0; while (*p && *p != ',' && len < 15) tmp[len++] = *p++; tmp[len] = 0; if (len >= 6) { *h = (tmp[0]-'0')*10 + (tmp[1]-'0'); *m = (tmp[2]-'0')*10 + (tmp[3]-'0'); *s = (tmp[4]-'0')*10 + (tmp[5]-'0'); *ms = 0; if (len >= 10 && tmp[7] >= '0' && tmp[7] <= '9') { *ms = (tmp[7]-'0')*100 + (tmp[8]-'0')*10 + (tmp[9]-'0'); } } } else if (field == 2 && *p == 'V') { return 0; // 定位无效,时间不可信 } else if (field == 9) { // 日期字段 DDMMYY int len = 0; while (*p && *p != ',' && len < 15) tmp[len++] = *p++; tmp[len] = 0; if (len >= 6) { *day = (tmp[0]-'0')*10 + (tmp[1]-'0'); *month = (tmp[2]-'0')*10 + (tmp[3]-'0'); *year = 2000 + (tmp[4]-'0')*10 + (tmp[5]-'0'); } } else { while (*p && *p != ',') p++; // 跳过其他字段 } } return (field >= 9) ? 1 : 0; }这段代码不用strtok,原因是strtok会原地改写缓冲区,一旦 RMC 行还要用于调试打印或二次校验,原始数据就被破坏了。这里用字段索引扫描,遇到目标字段才复制内容,其他字段直接跳过。第 2 个字段的判断放在时间字段之后,和协议定义保持一致:即使时间已经收到,只要状态是V,这帧数据就不能用于授时。返回值0时上层应当丢弃整帧,而不是凑合使用。
3.3 PPS 秒脉冲:NMEA 报文给“哪一秒”,PPS 给“这一秒从哪开始”
NMEA 报文有一个固有缺陷:句子按串口波特率逐字节发送,单片机“收到完整报文并解析完”的时刻,与卫星定义的整秒没有固定对齐关系。报文在串口线、中断响应和应用层取数之间流动,延迟每次都不同。如果靠“收到 RMC 的瞬间”去触发继电器,抖动可能到几十毫秒,长期运行后还可能出现时间累积偏差。
PPS 是解决对齐问题的关键。Air530 在定位有效后每秒输出一个上升沿,这个上升沿与 UTC 秒边界对齐,误差在微秒量级。因此授时继电器的正确做法是:用 RMC 里的日期和时间设置“当前是哪一秒”,用 PPS 上升沿刷新秒计数并作为触发基准。NMEA 解析慢几十毫秒完全不影响后续动作。
3.4 UTC 转北京时间:跨日进位与闰秒、GPS 误差来源
卫星输出的是 UTC 时间,国内应用要转成东八区北京时间,规则是 UTC 加 8 小时。换算最容易出错的地方是跨日:UTC 时间 18:30:00 在北京已经是次日 02:30:00,日期要跟着进位。
// UTC 时间转北京时间(东八区),日期随小时进位 void utc_to_beijing(int *h, int *m, int *s, int *day, int *month, int *year) { (void)m; (void)s; // 分和秒不需要改动 *h += 8; if (*h >= 24) { *h -= 24; static const int dim[] = {31,28,31,30,31,30,31,31,30,31,30,31}; int max_day = dim[*month - 1]; if ((*year % 4 == 0 && *year % 100 != 0) || *year % 400 == 0) { if (*month == 2) max_day = 29; } if (++*day > max_day) { *day = 1; if (++*month > 12) { *month = 1; (*year)++; } } } }这里只处理小时和日期进位,因为 NMEA 的 UTC 日期在每天 23:59:59 之后会自动翻到次日,时区换算的日期调整只会出现在h >= 24分支里。关于闰秒,民用 GPS 接收机输出的 UTC 已经由系统修正,单片机不需要也不应该手工补闰秒,凡是代码里写“每年固定加一秒”的都会把授时做偏。GPS 误差来源主要包括卫星星历误差、大气层信号延迟和接收机噪声,对授时来说表现为 PPS 沿的少量抖动,这个量级在电磁继电器场景下可以忽略。
4. 单片机 GPS 授时继电器的源码框架:状态机、时间校验与精准触发
4.1 源码怎么拆:解析、换算、PPS 与驱动分离
Air530 源码组织上我建议按功能拆成四个模块,后续维护和移植都省事。gps_rmc.c负责串口收帧和 RMC 解析,对外只提供一个parse_rmc_time()接口;utc_convert.c放 UTC 与北京时间的换算;relay_ctrl.c管 GPIO 初始化和继电器动作;main.c只做状态流转和主循环调度。PPS 中断属于硬件相关代码,放在stm32f1xx_it.c或单独文件里,中断回调里只置标志、做秒计数,不要调用解析函数,避免在中断上下文处理复杂逻辑。
4.2 设备状态机:冷启动到可触发
直接在主循环里写“解析 RMC,然后判断时间”看起来简单,实际运行会发现冷启动首帧时间出来时定位还没完成,RMC 字段是V,时间不可信,此时如果贸然设置系统时间,后面 PPS 又不来,时间基准就乱了。常见做法是用状态机收窄工作条件。
| 状态 | 进入条件 | 该状态下做什么 |
|---|---|---|
| 失锁空闲 | 上电初始化 | 清空 RMC 缓冲,等待串口数据 |
| 时间待定 | 收到有效 RMC 但尚未对齐 PPS | 保存 UTC 时间,标记待对齐 |
| 授时锁定 | PPS 上升沿连续出现 | 用 PPS 刷新秒计数,允许触发动作 |
| 触发保持 | 到达目标时刻 | 置继电器输出,持续设定脉宽 |
| 失锁回落 | 连续超过 10 秒无 PPS | 关闭继电器输出,回到失锁空闲 |
状态机把“时间可用”和“秒脉冲可用”拆成两个独立条件。RMC 里有时分秒但 PPS 一直没来,说明设备不满足精确定时条件,此时不允许执行继电器动作,否则动作时刻没有秒沿对齐,误差不可控。
4.3 GPS 数据异常处理:回跳、丢帧与周数翻转
授时项目运行中经常出问题的是数据跳变,而不是冷启动。GPS 模块在信号被遮挡或星历切换时,可能出现时间回跳,比如上一帧是 12:00:02,下一帧变成 11:59:58。这种数据不能直接用于同步,需要做连续性校验,正常情况每秒一帧,时间差应落在 0 到 5 秒内,超出范围就丢弃并等待重新连续。
// 时间连续性护栏:只允许前进 0~5 秒 // cur_s, last_s 为同一时区下的当日秒数 int time_step_ok(int cur_s, int last_s) { int diff = cur_s - last_s; if (diff >= 0 && diff <= 5) return 1; if (diff < 0 && diff > -86400) return 0; // 明显回跳 return 1; // 跨日大跳变交给上层判断 }丢帧时 PPS 可能仍在输出,但 RMC 连续几十帧都是V,这说明接收机还没恢复定位精度,应以上层解析的状态为准,不执行时间同步。周数翻转在 GPS 里每 1024 周发生一次,Air530 出厂固件一般已处理,但移植到其他模块时要警惕跨周后 UTC 日期显示倒退的情况。
4.4 主循环 + PPS 中断的继电器触发实现
下面这段代码是主循环与 PPS 中断配合的参考实现,以 STM32 HAL 库为例。PPS 接 PB1,配置为上升沿外部中断;主循环只负责把目标时刻写入变量,PPS 中断里做秒计数和继电器触发。
static volatile uint32_t utc_sec; // 经 RMC/PPS 校准的 UTC 总秒数 static volatile uint32_t target_utc; // 目标动作时刻,UTC 总秒数 static volatile uint8_t out_state; // 继电器当前输出状态 static volatile uint16_t miss_pps; // 连续丢失 PPS 计数 // PPS 外部中断回调:每 1 秒触发一次(上升沿) void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == PPS_PIN) { utc_sec++; miss_pps = 0; if (target_utc != 0 && utc_sec == target_utc) { HAL_GPIO_WritePin(RELAY_PORT, RELAY_PIN, GPIO_PIN_SET); out_state = 1; } } } // 主循环:消费解析好的 RMC 并换算 UTC 总秒数 void loop(void) { if (rmc_ready) { rmc_ready = 0; int h, m, s, ms, d, mo, y; if (parse_rmc_time(rmc_buf, &h, &m, &s, &ms, &d, &mo, &y)) { uint32_t cand = to_total_seconds(y, mo, d, h, m, s); // RMC 晚于 PPS 约一拍,预料之外偏差超过 5 秒则丢弃 if (abs((int)(cand - (utc_sec + 1))) <= 5) { utc_sec = cand; } } } // 继电器开启后按脉宽定时关断 if (out_state && (HAL_GetTick() - out_start_tick >= relay_pulse_ms)) { HAL_GPIO_WritePin(RELAY_PORT, RELAY_PIN, GPIO_PIN_RESET); out_state = 0; target_utc = 0; } }逻辑说明:PPS 中断里utc_sec++维护秒计数,主循环把 RMC 解析出的时间换算成 UTC 总秒数后与当前计数比较。正常情况下 RMC 报文在整秒后几十毫秒才到达单片机,所以候选秒应为utc_sec + 1,允许 ±5 秒偏差是给串口缓冲和输出周期抖动留余量。target_utc在主循环中由目标北京时间转成 UTC 后写入,到达后由 PPS 沿驱动继电器打开,而不是主循环轮询触发,这样动作落在秒边界上,抖动只剩中断延迟和继电器机械吸合时间。
参数说明:relay_pulse_ms是动作脉宽,建议至少 100ms,普通电磁继电器吸合需要 5~15ms,脉宽太短可能没完全吸合就关断。miss_pps在 PPS 中断里清零,主循环超时检测里自增,超过 10 次就整体回到失锁状态。
注意:不要在中断里调用
HAL_Delay()或打印串口日志,PPS 中断处理时间应控制在几十微秒内。
GPIO 驱动继电器这部分,单片机引脚不能直接带电磁线圈。常见做法是 NPN 三极管或 N-MOS 管做开关,继电器线圈反向并联续流二极管,必要时加光耦隔离。续流二极管要靠近线圈引脚安装,否则关断瞬间的反向电动势可能反复复位单片机。
| 参数 | 建议值 | 说明 |
|---|---|---|
| 动作脉宽 | 100~1000 ms | 小于 100ms 继电器可能未完全吸合 |
| 目标时刻偏移 | 0~59 s | 支持“整分后第 N 秒”动作 |
| 授时锁定等待 | ≤ 3 分钟 | 冷启动一般 30~90 秒出时间 |
| 失锁超时 | 10 s | 超时关闭输出并回到失锁状态 |
| 驱动管选型 | 500mA 以上 | 5V 继电器线圈电阻约 70~100 欧姆 |
5. 进阶验证:双 PPS 对比法量化授时继电器的动作误差
5.1 测量接线与读数方法
做完一套授时继电器,最关心的是“到底准不准”。不依赖实验室仪器也能验证:拿第二块 GPS 模块做参考源,把它的 PPS 接到逻辑分析仪或示波器通道 A,把继电器驱动管的输出接到通道 B,两个通道同时采样,测量两个沿的时间差。手头没有第二块 GPS 模块时,可以用单片机定时器替代:PPS 中断里把定时器清零,继电器打开瞬间读取定时器计数值,得到动作时刻相对 PPS 的延迟。
测量时注意,UART 打印语句不要放在 PPS 中断里,否则会延迟继电器引脚翻转,测出来的数据是代码执行开销,不是真实误差。主循环读到触发标志后再打印,测得的数值才反映系统真实行为。干接点输出建议通过光耦转成 3.3V 电平再进逻辑分析仪,直接测继电器触点两端会有触点抖动,电平毛刺影响触发读数。
5.2 十次中位数统计与继电器机械吸合补偿
单次动作误差没有参考价值,至少要统计 10 次取中位数,不要取平均值。GPS 秒脉冲本身存在少量抖动,分布不对称,平均会被个别异常脉冲带偏;中位数更接近真实偏移。另一个常见误区是把 NMEA 解析完成的时刻当成动作基准,实测会发现这个基准本身就带几十毫秒随机抖动,必须用 PPS 边沿做对齐。
如果测试中动作时刻整体偏移约 20~30ms,问题往往不在 GPS 模块,而是继电器机械吸合时间。补偿做法是在目标 UTC 时刻上预减去一个relay_compensation_ms:实测中位数为 18ms 就提前 18ms 翻转引脚,机械触点最终在目标秒附近闭合。这个参数要先在真实天线环境下校准,模拟器与实收信号信噪比不同,PPS 抖动也会有差异。现场校准时建议把实测中位数写入配置区,再在连续三个整分时刻各触发一次,记录三次读数的最大跨度作为该现场环境的动作漂移区间,如果超过 30ms 就检查天线摆放与继电器线圈反向电动势干扰。
本文还有配套的精品资源,点击获取