【DIY教程】做一个永远准时的WiFi时钟

ngHackerX86

新手请勿跳过的琐碎简介

  系统时间是单片机系统中经常用到的要素,一般来说,采用RTC时钟可以获取较准确的时间。但是,大家如果有过使用街边买的便宜电子表的经验,就会知道,如果不进行对时,电子表用着用着就不准了,一年产生的误差在十几秒到几十秒之间。这是因为电子表的精度依赖于所使用的晶振,一般低端的电子产品里使用的晶振,其精度多在20ppm、10ppm(百万分之一)这两档上。对于20ppm的晶振,理论上一年的最大误差为31S,同时,受到温度变化、电容是否匹配等因素的影响,实际误差可能大于这个值。同理,单片机RTC时钟的精度依赖于RTC晶振,使用时间长了之后,精度大概率不会让人满意。

  那么,有没有啥解决办法呢?

  有一种大家都很容易想到的办法是——氪金。毕竟,氪金带来力量是普遍规律,这一点在电子设计领域表现得格外突出。既然误差源于晶振,那提高晶振精度不就好了嘛。的确,如果愿意花钱,那么你可以使用高精度的温补晶振——顾名思义,这种晶振具有温度补偿功能。而且,作为具备补偿功能的高端产品,本身的精度一般也很不错,0.1ppm的一抓一大把,如果使用这样的晶振,理论最大年误差为1.55S,这还要啥自行车啊。

  不过,0.1ppm的温补晶振,价格一般在50RMB以上。

  所以我们来看下一个方案吧。

  这几年物联网之类的概念还是炒的挺热的,相关产品也出货不少,其中乐鑫的ESP8266这款芯片可以说是个划时代的东西(指价格),将单片机系统接入网络的成本一下子降到了7RMB以内,接入网络可以给单片机系统带来许多强大的功能,比如获取网络时间、获取自己账户的B站粉丝数、获取天气信息甚至在线播放badapple等等。

  所以,第二个为单片机系统提供准确时间的方案就是,通过ESP系列或类似的具有WiFi功能的芯片,获取网络时间,然后发送给主控单片机进行显示。

  如果有电子设计领域的熟手路过,看到这可能会笑出声来。因为,其实ESP系列的芯片本身就是一块单片机,而且其性能在常见32位单片机里算是相当不错的,引脚数量也不少,电子爱好者个人轻度使用完全足够。

  但是,外挂芯片方案也是有着自己的优势的——WiFi模块和主控MCU相对分离,在程序设计上可以较为容易地处理系统的层次结构。而且,毕竟有很多人只熟悉51、STM32啥的

  琐碎的话就说到这里,马上开始动手做吧!

材料清单

  1. ESP8266-01S模块 × 1
  2. ESP8266下载器(基于CP2104)× 1
  3. STM32F103C8T6核心板 × 1
  4. 0.96‘ OLED屏(IIC接口)× 1

    总的材料花费在30左右

如何获取网络时间

  准确的网络时间一般通过NTP(Network Time Protocol)来获取,具体的实现流程可能稍显复杂,而且这些流程往往是相对固定的,没什么意思(毕竟能修改的东西才好玩嘛),为此,笔者决定选用比较简单的方式来给大家做个示范。

  我们连接WiFi是通过ESP8266实现的,通常大家给ESP8266写程序的方式有这些:

  1. 采用官方SDK进行开发(基于C/C++)
  2. 烧录micropython固件以后写micropython
  3. 使用Arduino IDE

  从设计意图就能知道,在我们想偷懒的时候该选择哪一个——SDK面向的主要是是使用其产品进行深度开发的工程师;micropython主要是为了给软件开发者玩硬件提供便利;而Arduino是为了给非专业人士提供控制硬件进行交互的傻瓜化方法,所以就选它了。

  首先去下载Arduino,官网选个最新的版本即可,链接在这:

https://www.arduino.cc/en/Mai...

  标有“ZIP file for non admin install”字样的是解压后直接可以运行的版本。

  下载后打开,然后从菜单栏依次选择“文件 -> 首选项 ”,在“附加开发板管理网址”中填入http://arduino.esp8266.com/st...,确认。

  然后从菜单栏依次选择“工具 -> 开发板 -> 开发板管理器”,稍稍等待一会,然后在搜索框中输入esp8266,选择对应的库进行安装,下面是安装好的效果:

P2.jpg

  为了使用NTP,我们需要再额外安装一些库(站在大神的肩膀上XD)。打开“工具 -> 管理库”,输入NTPClient,安装名字完全匹配的那个库。之后,以相同方式安装“ArduinoJson”这个库。另外,为了使用WiFi功能,还需要ESP8266WiFi和WiFiUdp这两个库,不过这些是软件自带的,直接使用就行。

连接WiFi

  要连接WiFi,最简单的方式是在程序里预先配置好SSID和密码后直接连接,具体如下:

const char *ssid     = "your SSID";
const char *password = "your password";

  另外顺便说一下,Arduino的程序结构基本就是setup和loop这两个函数,setup类似于我们平常使用的初始化函数,loop类似于单片机编程常用的while(1)循环。初始化操作一般放在setup函数里面(比如连接WiFi),如下:

  WiFi.begin(ssid, password);

  while ( WiFi.status() != WL_CONNECTED ) {
    delay ( 500 );
    Serial.print ( "." );
  }

通过NTP获取GMT时间

  学过C语言的同学应该都知道“Unix时间”,用time函数就可以获取,其数值表示自1970年01月01日 0:00:00至当前所经过的秒数,时间标准为GMT时间。我们通过NTP获取的就是这样的数值(借助NTPClient库)。

  timeClient.update();
  epoch_time = timeClient.getEpochTime(); //epoch_time为预先定义的32位无符号数

转换时间数据格式

  直接获取的时间数据不太适合人类理解,一般人应该不能一眼从“1585144726”这样的数据解读出当前的时间吧?所以,把它转换成一般的时间格式是很有必要的。

  使用Arduino的时候就该偷懒嘛,这次还是用现成代码解决问题,笔者使用了一位网友的代码来进行时间格式的转换,地址:https://blog.csdn.net/mill_li...

  数据转换结果将被存入如下的结构体:

typedef struct
{
    unsigned short nYear;
    unsigned char nMonth;
    unsigned char nDay;
    unsigned char nHour;
    unsigned char nMin;
    unsigned char nSec;
    unsigned char DayIndex; /* 0 = Sunday */
} mytime_struct;

数据打包发送

  如果我们仅仅是想做个能显示时间的时钟的话,只要发送一个时间数据,然后在主控单片机那边解析出时分秒什么的就可以了。但是,如果考虑到要方便后期添加各种诸如天气显示之类的功能的话,就有必要选用一种灵活的数据交换格式了。

  笔者在这里选择选择json。

  JSON是一种简洁、轻量的、基于文本的数据交换格式,使用json交换数据时,可以避免考虑一些大端小端之类的问题。关于json的格式,可以看这里:https://www.cnblogs.com/mcgra...

  我们之前下载的ArduinoJson库可以帮助我们完成打包Json字符串和通过串口发送它的工作。部分代码如下:

StaticJsonDocument<200> doc; //创建Json对象
//添加关键字和对应的值
doc["year"] = 0;
doc["mon"]  = 0;
···
//每次接收完网络时间并完成格式转换后,更新Json对象中的数据
doc["year"] = my_time.nYear;
doc["mon"]  = my_time.nMonth;
doc["day"]  = my_time.nDay;
···

serializeJson(doc, Serial);//通过串口发送根据Json对象制造的Json字符串

  发送完数据以后就没ESP8266什么事了,这部分的完整代码在这里:https://gitee.com/multicolore...

  接下来,我们可以在主控单片机上接收Json字符串、解析数据并且进行显示了,笔者这里选用STM32、Keil,显示用0.96寸、IIC接口的OLED屏。~~~~

STM32接收JSON字符串

安装Jansson库

  在比较正经的单片机开发中,单片机接收字符串一般通过cjson来实现,不过需要自己移植一下。在Keil下进行开发时,可以使用官方提供的Jansson这个库来对Json进行处理。

  使用库前得先去官网把pack下下来,地址:http://www2.keil.com/mdk5/par...

  下载完双击安装即可。

  之后打开一个Keil的工程,在如图位置单击打开运行时环境管理窗口

P1.jpg

  然后勾选下图位置:

P3.jpg

  之后点击确认即可。

Jansson库API简单说明

  Jansson这个库提供了一下用于处理Json的API,这里我们用到以下几个:

//从Json字符串创建Json对象
json_t *json_loads(const char *input, size_t flags, json_error_t *error)

//解析Json对象,获取数据
int json_unpack(json_t *root, const char *fmt, ...)

//删除Json对象
json_delete(json_t *object)

  json_delete的使用最为简单,调用的时候把json对象的名称传入就行。

  json_loads的第一个参数是指定的字符串首地址(也就是它的名称),第二个参数常用“JSON_ENCODE_ANY”,第三个是要求预先创建的一个json错误对象,用于一些错误提示信息的存储。

  json_unpack使用的时候稍稍复杂些,需要填入一个类似于{s:i,s:i,s:i}这样的列表,这里s代表字符串,i代表int型变量,同时,可以指定解析得到的数据的存储位置,如下:

json_unpack(epoch_time_raw,"{s:i,s:i,s:i,s:i,s:i,s:i,s:i}", \
"year",&year,"mon",&mon,"day",&day,"day_index",&day_index,  \
"hour",&hour,"min",&min,"sec",&sec);

  更详细的信息请参阅Jansson的API reference,地址:https://jansson.readthedocs.i...

其他

  串口接收的程序是基于原子的代码改的,没有做什么工作,而且相关的讲解也比较多,这里就不赘述了。奉上粗糙的代码一份,招待不周,还请原谅。

  地址:https://gitee.com/multicolore...

(测试代码基于STM32F103C8T6,V3.5版本库函数)~~~~

阅读 5.2k

在嵌入式的道路上疯狂跑偏

16 声望
15 粉丝
0 条评论

在嵌入式的道路上疯狂跑偏

16 声望
15 粉丝
文章目录
宣传栏