某日,博主在逛淘宝时,发现了一款电波钟模块。
博主第一次听说长波授时的概念还是在上初中的时候,出于兴趣,当年也下单了过一个 CASIO 出口转内销的电波授时闹钟。如今距离买下这款闹钟已有快七年时间了,虽然当年买的电波钟外壳已经泛黄,但是时间仍然分毫不差。如今刷到这么一个模块,又唤起了博主的回忆。
在这七年间,博主也曾经尝试弄清楚电波授时的解码原理以及数据报文格式,但是迫于当年的知识面与技术并不到位,所以在几次尝试与几次失败后,博主便放弃了。
而如今博主作为一个通信工程大学牲,想要再次挑战弄懂 JJY 长波授时的时间码结构,并尝试编写程序解码准确时间,于是果断下单了这款模块。
在收到模块后,经过两天的不懈努力,博主也终于成功达成了目标,了结了七年来的心事。这篇文章,会分享博主玩转 JJY 时间码模块的过程。
模块引脚与电平逻辑
几天后模块到手了,由于模块是拆机模块,成色老旧,为了避免断线和焊盘脱落,博主先用热溶胶加固了比较脆弱的位置,同时给引脚位置加装了 2.54mm 排针。
这款模块用了的经典的牛屎封装,经过一番 Google,得知真正的核心貌似用的是 MAS6180B。另外,这款模块的原理,实际上就是透过 MAS6180B 将长波授时信号进行放大与解调,并透过内部的比较器将其二进制输出。
这个模块有 5 个引脚,经过博主一番尝试和整理,其功能如下。
引脚名字 | 引脚功能 | 逻辑电平 | 默认状态 |
---|---|---|---|
60K1 | 60 kHz 使能 | 高电平使能 | 低电平 |
PON1 | 模块使能 | 低电平使能 | 高电平 |
3V1 | 3.3V 电源输入 | - | - |
TCO1 | 信号脉冲 | - | - |
GND1 | 电源接地 | - | - |
在整理引脚的过程中,博主也发现不同 JJY 模块间的引脚排布、引脚逻辑电平、引脚功能都不尽相同,所以在使用模块前,一定要先确认好模块的引脚功能和逻辑电平,以免翻车损坏模块。
`
模块测试
为了简化开发流程,博主使用 Arduino UNO 来测试模块的功能。但是在测试模块的过程中,博主发现模块的 TCO1 输出的脉冲电压只有 1V 左右,无法直接连接到 Arduino 的数字输入引脚。
由于使用的是 Arduino 单片机,而且博主也不想额外搭建电平转换电路,所以博主决定将模块的 TCO1 输出接到 Arduino 的模拟输入引脚上,然后透过 Arduino 的 analogRead()
函数来读取 TCO1 的电压值,进而分析出其电平高低。
JJY 时间码结构
在编写程序前,我们需要先了解一下 JJY 时间码的结构。NICT 在官网给出的时间码结构如下图所示。
JJY 的脉冲宽度总共分为 3 种,分别是 200ms、500ms、800ms,其中 200ms 的脉冲代表标记符,500ms 的脉冲代表逻辑 1,800ms 的脉冲代表逻辑 0。
在 NICT 给出的结构图中,可以看出,JJY 一帧完整的时间数据总共有 60 位,每一位对应到一个固定宽度的脉冲。透过上图,可以将时间主要位的索引整理如下表。
时间位名称 | 位数索引 |
---|---|
分钟 | [1:8] |
小时 | [12:18] |
天数 | [22:33] |
年份 | [41:48] |
星期 | [50:52] |
此外,JJY 对时间的编码形式是 BCD 码的一种变体,但是计算方式和 BCD 码相同。例如,若在分钟位接收到的数据是 00110010,结合上图中所给的位权重,那么将该数据换算成 10 进制,即分钟数为 12,其算式如下。
1 | 0*40 + 0*20 + 1*10 + 1*0 + 0*8 + 0*4 + 1*2 + 0*1 = 12 |
根据上图,还可以得出的一个结论是,当连续收到两次标记符信号时,就可以确认当前的时间是第 0 秒,也就是某一分钟的起始。
另外,JJY 的时间码还带有闰秒信息,当闰秒发生时,JJY 会在 59 秒的位置插入一个 800ms 的脉冲,用来表示闰秒。但是本文的目标只是实现对 JJY 的简单解码,所以不会对闰秒进行处理,也不会对奇偶校验码做处理。
硬件搭建
硬件连线图如下所示。
引脚的连线方式如下表所示。
模块引脚 | 单片机引脚 | 备注 |
---|---|---|
JJY 模块 GND1 | GND | 无 |
JJY 模块 TCO1 | A0 | 需要接入 Arduino 模拟引脚 |
JJY 模块 3V1 | 3.3V | 须 3.3V 供电避免烧坏模块 |
JJY 模块 PON1 | 11 | 无 |
JJY 模块 60K1 | 10 | 低电平 40 kHz,高电平 60 kHz |
由于博主的 Arduino UNO 临时没有找到,所以博主使用的是引脚功能是一样的,所以不会影响程序的编写。
下面是博主的硬件搭建图。
程序编写
Github 仓库:github.com/bclswl0827/JJYClock
博主已经写好了程序,也开源到了 GitHub 上,经过测试可以正常解码。下文介绍一些关键点与代码。
另外,博主使用的 IDE 是 PlatformIO,编译固件时需要注意。
重写 digitalRead() 函数
由于 JJY 模块输出的电平高低与 Arduino 的电平标准不同,所以可以使用 analogRead 来读取引脚模拟值,并根据人为的设定电平阈值来判断逻辑 1 或 0,因此,只需要对 digitalRead() 函数进行重写。
1 | int digitalRead(int pin) { |
声明每一位权重
前面提到过,JJY 的时间码是 BCD 码的一种变体,所以需要声明位权重,用来计算时间码的十进制值。
这里透过枚举类型来声明位权重,如下所示。
1 | // 分钟位权重 |
天数转月份与日期
由于 JJY 的天数位是从每年的第一天开始计数的,所以需要将天数转换成月份日期。
由于存在闰年,所以在调用函数时,除了传入天数,还需要传入是否为闰年的标记。
下方是天数转月份的代码,代码中,天数统一扩大了 1000 倍,这样可以避免浮点数运算带来的精度丢失问题。
1 | uint8_t Days2Month(uint8_t days, uint8_t leap) { |
下方是天数转日期的代码。
1 | uint8_t Days2Date(uint8_t days, uint8_t leap) { |
取得同步标记
前面也已经提到过,当连续收到两次标记符信号时,就可以确认 JJY 新的一帧数据已经开始。
所以,需要声明一个变量或者结构体,用来记录上一次脉冲的宽度,并与当前脉冲的宽度进行比较,若判断到上一次与这一次的脉冲宽度都是标记符信号,就可以确认 JJY 新的一帧数据已经开始。
下面是声明的用来存储状态的结构体。
1 | struct JJYStatus { |
解码器缓存
解码器缓存也为一个结构体,用来保存解码数据,存储解码器的状态,记录是否已经完成解码,以及一个用于记录数据缓存位置的计数器。
由于实际解码时,由于标记符会直接丢弃,所以缓存大小为 53 个数据。
1 | struct DecoderStatus { |
由于抛弃了标记符,所以上文提到的时间位索引表,也应做相应的调整。
成果
最后附上博主的成果,数据透过串口终端输出,可以看到,数据已经成功解码。