打印
[MM32硬件]

【EV Board (MM32L0136C7P)测评】基于NTP对时的互联网时钟

[复制链接]
875|9
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本帖最后由 HonestQiao 于 2022-12-6 11:47 编辑

#申请原创# 一、想法起源
之前使用ESP32+OLED(SSD1306)只做过基于NTP对时的互联网时钟,无需RTC,也不需要对时,效果非常好。MM32L0136C7P开发板上自带了SLCD,其上有两个数字显示区域,用来做时钟显示,再合适不过了。

在官方最新的演示代码中,只有SLCD各笔画的统一显示测试。
后经过坛友分享,拿到了MM32L0130_LibSamples_V020_1201.zip,其中有SCLD数字和单位符号显示的实例。

经过一番研究后,将ESP8266与MM32L0136C7P相结合,实现了如下的电子时钟:


二、实现规划
查找资料的时候,看到有大佬在MM32上,实现了ESP8266的透传,非常的厉害。
不过我这个应用,就是简单的进行NTP对时即可。
所以NTP对时这个部分,就让ESP8266自己完成就好了。
ESP8266对时后,通过串口,每秒钟,自动上报一次当前时间数据。

MM32L0136C7P开发板则通过串口,来获取ESP8266提供的时间信息。
为了确保处理效率更好,经过研究学习例子,最终使用中断的方式,来进行数据的接收,效率更高。

最后,为了整体显示效果更好,把MM32L0136C7P开发板上的4个LED都利用起来了,根据时间信息中当前秒的个位数,来决定LED的显示,这样就活了起来。

具体规的功能:
1. 由ESP8266直接联网,进行NTP对时,然后通过串口发送时间信息,发送数据的格式为:Tyyyy-mm-dd HH:MM:SS XX,其中XX为CRC8校验码
2. MM32L0136C7P开发板通过串口获取时间信息,进行解析,然后显示到SLCD屏幕上。
3. MM32L0136C7P开发板取秒数中的第0、1、2、3bit位,该n位为1,则点亮对应的LDn

三、硬件物料


1. ESP8266-S1

ESP32一般都要大一点点,而ESP8266-01则较为较小,所示使用了一块ESP8266-01来提供网络对时功能。


为了方便烧录,我还有一个ESP8266-01的底座:

把ESP8266-01插上底座,就可以直接接到电脑上,进行固件下载烧录了。

另外,ESP8266-01正常运行时,需要CH_PD高电平使能,通常是直接VCC:

所以,直接在ESP8266-01上面,在VCC和CH_PD引脚上,焊了一个跳线,刚好用MM32L0136C7P开发板上的UART1的跳线帽:


2、MM32L0136C7P开发板
参考原理图,以及演示代码,使用UART1来进行和ESP8266-01进行通讯,所对应的引脚选用PA9、PA10



为了稳固,把ESP8266-01开发板,直接用胶枪给沾到了MM32L0136C7P开发板右上角:


具体接线,就在ESP8266-01背后偷偷进行了:

不过MM32L0136C7P开发板的接线只能在正面进行。

四、软件设计
软件部分,分为两个部分,一个是ESP8266上面的NTP对时和事件信息提供,一个是MM32L0136C7P开发板上的主逻辑。

1. ESP8266部分
简单起见,使用了Arduino IDE进行开发,且还提供了相应的实例。
具体代码如下:
/*
* ESP8266_NTP_Time_Clock.ino
* NTP Time Clock at ESP8266
*
*/

#include <TimeLib.h>
#include <ESP8266WiFiMulti.h>
#include <WiFiUdp.h>

#define DEBUG_MODE 0

ESP8266WiFiMulti wifiMulti;
// WiFi connect timeout per AP. Increase when connecting takes longer.
const uint32_t connectTimeoutMs = 5000;

const char ssid[] = "OpenBSD";  //  your network SSID (name)
const char pass[] = "********";       // your network password

// NTP Servers:
static const char ntpServerName[] = "ntp.aliyun.com";

// TimeZone
const int timeZone = 8;     // CST +8


WiFiUDP Udp;
unsigned int localPort = 8888;  // local port to listen for UDP packets

static const uint8_t crc_table[] = {
    0x00, 0x07, 0x0e, 0x09, 0x1c, 0x1b, 0x12, 0x15, 0x38, 0x3f, 0x36, 0x31,
    0x24, 0x23, 0x2a, 0x2d, 0x70, 0x77, 0x7e, 0x79, 0x6c, 0x6b, 0x62, 0x65,
    0x48, 0x4f, 0x46, 0x41, 0x54, 0x53, 0x5a, 0x5d, 0xe0, 0xe7, 0xee, 0xe9,
    0xfc, 0xfb, 0xf2, 0xf5, 0xd8, 0xdf, 0xd6, 0xd1, 0xc4, 0xc3, 0xca, 0xcd,
    0x90, 0x97, 0x9e, 0x99, 0x8c, 0x8b, 0x82, 0x85, 0xa8, 0xaf, 0xa6, 0xa1,
    0xb4, 0xb3, 0xba, 0xbd, 0xc7, 0xc0, 0xc9, 0xce, 0xdb, 0xdc, 0xd5, 0xd2,
    0xff, 0xf8, 0xf1, 0xf6, 0xe3, 0xe4, 0xed, 0xea, 0xb7, 0xb0, 0xb9, 0xbe,
    0xab, 0xac, 0xa5, 0xa2, 0x8f, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9d, 0x9a,
    0x27, 0x20, 0x29, 0x2e, 0x3b, 0x3c, 0x35, 0x32, 0x1f, 0x18, 0x11, 0x16,
    0x03, 0x04, 0x0d, 0x0a, 0x57, 0x50, 0x59, 0x5e, 0x4b, 0x4c, 0x45, 0x42,
    0x6f, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7d, 0x7a, 0x89, 0x8e, 0x87, 0x80,
    0x95, 0x92, 0x9b, 0x9c, 0xb1, 0xb6, 0xbf, 0xb8, 0xad, 0xaa, 0xa3, 0xa4,
    0xf9, 0xfe, 0xf7, 0xf0, 0xe5, 0xe2, 0xeb, 0xec, 0xc1, 0xc6, 0xcf, 0xc8,
    0xdd, 0xda, 0xd3, 0xd4, 0x69, 0x6e, 0x67, 0x60, 0x75, 0x72, 0x7b, 0x7c,
    0x51, 0x56, 0x5f, 0x58, 0x4d, 0x4a, 0x43, 0x44, 0x19, 0x1e, 0x17, 0x10,
    0x05, 0x02, 0x0b, 0x0c, 0x21, 0x26, 0x2f, 0x28, 0x3d, 0x3a, 0x33, 0x34,
    0x4e, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5c, 0x5b, 0x76, 0x71, 0x78, 0x7f,
    0x6a, 0x6d, 0x64, 0x63, 0x3e, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2c, 0x2b,
    0x06, 0x01, 0x08, 0x0f, 0x1a, 0x1d, 0x14, 0x13, 0xae, 0xa9, 0xa0, 0xa7,
    0xb2, 0xb5, 0xbc, 0xbb, 0x96, 0x91, 0x98, 0x9f, 0x8a, 0x8d, 0x84, 0x83,
    0xde, 0xd9, 0xd0, 0xd7, 0xc2, 0xc5, 0xcc, 0xcb, 0xe6, 0xe1, 0xe8, 0xef,
    0xfa, 0xfd, 0xf4, 0xf3
};

time_t getNtpTime();
void digitalClockDisplay();
void printDigits(int digits);
void sendNTPpacket(IPAddress &address);
uint8_t crc8(uint8_t *p, uint8_t len);

void setup()
{
  WiFi.persistent(false);

  Serial.begin(115200);
  while (!Serial) ; // Needed for Leonardo only

  delay(250);
  Serial.println("I:ESP8266 NTP Time Clock");
  Serial.print("I:Connecting to ");
  Serial.println(ssid);

  // WiFi.begin(ssid, pass);
  // while (WiFi.status() != WL_CONNECTED) {
  //   delay(500);
  //   Serial.print(".");
  // }

  WiFi.mode(WIFI_STA);
  wifiMulti.addAP("OpenBSD", "13581882013");
  while (wifiMulti.run(connectTimeoutMs) != WL_CONNECTED) {
    delay(500);   
    Serial.print(".");
  }
  Serial.println("OK");

  Serial.print("I:IP address is ");
  Serial.println(WiFi.localIP());
  Serial.println("I:Starting UDP");
  Udp.begin(localPort);
  Serial.print("I:Local port: ");
  Serial.println(Udp.localPort());
  Serial.println("I:waiting for sync");
  setSyncProvider(getNtpTime);
  setSyncInterval(300);
}

time_t prevDisplay = 0; // when the digital clock was displayed

void loop()
{
  if (timeStatus() != timeNotSet) {
    if (now() != prevDisplay) { //update the display only if time has changed
      prevDisplay = now();
      digitalClockDisplay();
    }
  }
}

uint8_t crc8(char *p, uint8_t len)
{
        uint16_t i;
        uint16_t crc = 0x0;

        while (len--) {
                i = (crc ^ *p++) & 0xFF;
                crc = (crc_table[i] ^ (crc << 8)) & 0xFF;
        }

        return crc & 0xFF;
}

void digitalClockDisplay()
{
  // digital clock display of the time
  // Serial.print(hour());
  // printDigits(minute());
  // printDigits(second());
  // Serial.print(" ");
  // Serial.print(day());
  // Serial.print(".");
  // Serial.print(month());
  // Serial.print(".");
  // Serial.print(year());
  // Serial.println();
  char timeStr[25] = {0};
  sprintf(timeStr, "T:%04d-%02d-%02d %02d:%02d:%02d ", year(), month(), day(), hour(), minute(), second());
  // Serial.println(timeStr);

  uint8_t crc = crc8(timeStr, strlen(timeStr));
  sprintf(timeStr, "%s%02X", timeStr, crc);
  Serial.println(timeStr);
}

void printDigits(int digits)
{
  // utility for digital clock display: prints preceding colon and leading 0
  Serial.print(":");
  if (digits < 10)
    Serial.print('0');
  Serial.print(digits);
}

/*-------- NTP code ----------*/

const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message
byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets

time_t getNtpTime()
{
  IPAddress ntpServerIP; // NTP server's ip address

  while (Udp.parsePacket() > 0) ; // discard any previously received packets
  Serial.println("I:Transmit NTP Request");
  // get a random server from the pool
  WiFi.hostByName(ntpServerName, ntpServerIP);
  Serial.print("I:");
  Serial.print(ntpServerName);
  Serial.print(": ");
  Serial.println(ntpServerIP);
  sendNTPpacket(ntpServerIP);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) {
    int size = Udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      Serial.println("I:Receive NTP Response");
      Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
      unsigned long secsSince1900;
      // convert four bytes starting at location 40 to a long integer
      secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];
      return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR;
    }
  }
  Serial.println("E:No NTP Response :-(");
  return 0; // return 0 if unable to get the time
}

// send an NTP request to the time server at the given address
void sendNTPpacket(IPAddress &address)
{
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12] = 49;
  packetBuffer[13] = 0x4E;
  packetBuffer[14] = 49;
  packetBuffer[15] = 52;
  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}


该部分的代码,参考了Arduino IDE的8266例子中的:


在此基础上,进行了一些小的修改:
  • 将WiFi连接部分,修改为了WiFiMul,这样就可以同时设置多组WiFi连接,更方便使用。
  • 添加了查表计算CRC8的代码,以便接收端收到后进行校验,确保数据准确。不过,MM32L0136C7P不分,我没有使用到,大家有需要可以在接收后进行校验。
  • 修改了信息的输出,正常信息使用I:开头,错误信息使用E:开头,时间信息使用T:开头,方便接收端进行解析


将以上代码烧录到ESP8266,然后打开串口终端,就可以收到启动的信息,以及时间信息:


测试验证完成,就可以连到MM32L0136C7P开发板上使用了。

2. MM32L0136C7P开发板
该部分的代码,参考了SLCD_ShowOnStop、UART_Tx_DMA_Interrupt_Rx_Interrupt。
UART_Tx_DMA_Interrupt_Rx_Interrupt实例,提供了中断收发数据的演示,基本不用修改,连引脚定义都是对应的,直接就可以使用了。
SLCD_ShowOnStop,则提供了显示数字和单位符号的演示,其中关键的调用如下:
  • LCD_Clear():清屏
  • LCD_DisplayNumber1(索引, '字符', 小数点标志):大号数字区域显示
  • LCD_DisplayNumber2(索引, '字符', 小数点标志):小号数字区域显示
  • LCD_DisplayUnit(索引, 显示标志):单位符号显示


大号数字显示:LCD_DisplayNumber1(索引, '字符', 小数点标志)
索引对应如下:


字符对应如下:

从上表中可知,除了0-9,还能显示一部分字母,但是不全,例如没有K,因为没法显示区分。

小数点标志,就表示是否显示小数点,0-不显示,1-显示


小号数字显示:LCD_DisplayNumber2(索引, '字符', 小数点标志)
索引对应如下:


小号区域的显示字符,演示代码只有0-9。当然,可以根据自己的需要进行扩展。
小数点标志和大号区域相同。


单位符号显示:LCD_DisplayUnit(索引, 显示标志)
索引有10个,对应下图:
{"S1 ", "S2 ", "S3 ", "S4 ", "S9 ", "T1 ", "W1 ", "C1 ", "C2 ", "C3 "};


时钟部分,要显示的:,使用的就是C2、C3,索引为8、9

最终,main.c的代码如下:
////////////////////////////////////////////////////////////////////////////////
/// [url=home.php?mod=space&uid=288409]@file[/url]    main.c
/// [url=home.php?mod=space&uid=187600]@author[/url]  AE TEAM
/// [url=home.php?mod=space&uid=247401]@brief[/url]   THIS FILE PROVIDES ALL THE SYSTEM FUNCTIONS.
////////////////////////////////////////////////////////////////////////////////

// Define to prevent recursive inclusion
#define _MAIN_C_

// Files includes

#include "delay.h"
#include "slcd.h"
#include "led.h"
#include "uart_txdma_rx_interrupt.h"

////////////////////////////////////////////////////////////////////////////////
/// @addtogroup MM32_Example_Layer
/// @{

////////////////////////////////////////////////////////////////////////////////
/// @addtogroup MAIN
/// @{

////////////////////////////////////////////////////////////////////////////////
/// @addtogroup MAIN_Exported_Constants
/// @{
#define        GET_BIT(x, bit)        ((x & (1 << bit)) >> bit)        /* 获取第bit位 */

#define APP_TITLE "I:MM32 NTP Net Clock\r\n"

u8 gSendBuf[] = "I:yyyy-mm-dd HH:MM:SS\r\n";

////////////////////////////////////////////////////////////////////////////////
/// [url=home.php?mod=space&uid=247401]@brief[/url]  This function is main entrance.
/// @param  None.
/// @retval  0.
////////////////////////////////////////////////////////////////////////////////
s32 main(void)
{
    u8 len;
    u16 times = 0;
   
    u16 year;
    u8 month,day,hour,minute,second;
    u8 crc;
    u8 buf[25] = {0};
   
    DELAY_Init();
    LED_Init();
    DELAY_Ms(100);
    //slcd_test();
    slcd_init();

   
    UART1_NVIC_Init(115200);
    NVIC_Configure(DMA1_Channel2_3_IRQn, 2, 0);

    UART1_SendString(APP_TITLE);
    DMA_NVIC_Send_Config(DMA1_Channel2, (u32)&UART1->TDR, (u32)gSendBuf, sizeof(gSendBuf)/sizeof(gSendBuf[0]));
   
    LCD_Clear();
   
    LCD_DisplayNumber2(0, '2', 0);
    LCD_DisplayNumber2(1, '0', 0);
    LCD_DisplayNumber2(2, '2', 0);
    LCD_DisplayNumber2(3, '2', 0);


    LCD_DisplayNumber1(0, '-', 0);
    LCD_DisplayNumber1(1, 'C', 0);
    LCD_DisplayNumber1(2, 'l', 0);
    LCD_DisplayNumber1(3, 'o', 0);
    LCD_DisplayNumber1(4, 'c', 0);
    LCD_DisplayNumber1(5, 'L', 0);
   
    DELAY_Ms(5000);
               
    while(1) {
        if(gUartRxSta & 0x8000) {
            //receive data length
            len = gUartRxSta & 0x3fff;
            if(len==0x1a && gUartRxBuf[0]=='T' && gUartRxBuf[1]==':') {
                sscanf(gUartRxBuf, "T:%04d-%02d-%02d %02d:%02d:%02d %2X", &year, &month, &day, &hour, &minute, &second, &crc);
                sprintf(buf, "%04d-%02d-%02d %02d:%02d:%02d\r\n", year, month, day, hour, minute, second);
                UART1_SendString(buf);
                LCD_DisplayNumber2(0, ((month/10)%10)?('0'+((month/10)%10)):' ', 0);
                LCD_DisplayNumber2(1, '0'+((month/1)%10), 0);
                LCD_DisplayNumber2(2, ((day/10)%10)?('0'+((day/10)%10)):'-', 0);
                LCD_DisplayNumber2(3, '0'+((day/1)%10), 0);
               
                LCD_DisplayNumber1(0, ((hour/10)%10)?('0'+((hour/10)%10)):' ', 0);
                LCD_DisplayNumber1(1, '0'+((hour/1)%10), 0);
                LCD_DisplayNumber1(2, '0'+((minute/10)%10), 0);
                LCD_DisplayNumber1(3, '0'+((minute/1)%10), 0);
                LCD_DisplayNumber1(4, '0'+((second/10)%10), 0);
                LCD_DisplayNumber1(5, '0'+((second/1)%10), 0);
               
                LCD_DisplayUnit(8,1);
                LCD_DisplayUnit(9,1);
               
                LED1_OFF();
                LED2_OFF();
                LED3_OFF();
                LED4_OFF();
                if(GET_BIT(second, 0)) {
                    LED1_ON();
                }
                if(GET_BIT(second, 1)) {
                    LED2_ON();
                }
                if(GET_BIT(second, 2)) {
                    LED3_ON();
                }
                if(GET_BIT(second, 3)) {
                    LED4_ON();
                }
            } else {
                DMA_NVIC_Send_Config(DMA1_Channel2, (u32)&UART1->TDR, (u32)gUartRxBuf, len);
            }
            gUartRxSta = 0;
        }
        else {
            times++;
            if(times % 5000 == 0) {
                UART1_SendString(APP_TITLE);
            }

            if(times % 500 == 0) UART1_SendString("I:Please input Data, End with Enter\r\n");
            if(times % 50 == 0){
                LED1_TOGGLE();
                LED2_TOGGLE();
                LED3_TOGGLE();
                LED4_TOGGLE();
            }
            DELAY_Ms(100);
        }
    }

}

/// @}

/// @}

/// @}


其中的逻辑也不复杂,具体如下:
  • 初始化LED、SLCD、串口以及中断和DMA设置
  • LCD清屏,然后显示”2022 - ClocL“,没有K,只好用大L代替了
  • 然后就是检测接收状态了,收到了,且符合时间字符串的格式,则解析其中的年月日时分秒,然后显示到SLCD,并根据秒来确定对应的LED显示,实际上就是对应1、2、4、8

另外,还有下面两个地方的代码,需要修改:



修改其中LED对应引脚的配置,以对应MM32L0136C7P开发板。

五、实际效果
将以上MM32L0136C7P开发板的代码,使用Keil编译下载后,就可以看到具体效果了:
【以下播放,加快了速度】



六、代码分享
1. 官方演示代码: MM32L0130_LibSamples_V020_1201.zip (5.49 MB)
2. 本应用代码:https://gitee.com/honestqiao/MM32_SLCD_NTP_Net_CLock

要使用本应用的代码,请先下载官方演示代码,并解压,然后将 MM32_SLCD_NTP_Net_CLock 目录放置到 MM32L0130_LibSamples_V020/MM32L0130_Samples/SLCD下,具体如下:
红色框柱的为关键代码,如果编译的时候提示找不到对应的文件,请自己添加一下到工程中。
  

使用特权

评论回复
沙发
10299823| | 2022-12-12 09:58 | 只看该作者
可以实现GPS的授时吗?              

使用特权

评论回复
板凳
dspmana| | 2022-12-12 12:02 | 只看该作者
这个不错,比较实用一些,NTP对时是趋势。

使用特权

评论回复
地板
HonestQiao|  楼主 | 2023-1-7 14:30 | 只看该作者
10299823 发表于 2022-12-12 09:58
可以实现GPS的授时吗?

挂gps模块也可以呀

使用特权

评论回复
5
wwppd| | 2023-1-9 14:39 | 只看该作者
ntp是网络反馈时间吗?              

使用特权

评论回复
6
mikewalpole| | 2023-1-9 15:53 | 只看该作者
这个怎么解析的时间呢?              

使用特权

评论回复
7
1988020566| | 2023-1-11 20:48 | 只看该作者
怎么utc时间转换为北京时间?              

使用特权

评论回复
8
jackcat| | 2023-2-2 10:03 | 只看该作者
这个可以通过gps授时吧。              

使用特权

评论回复
9
ccook11| | 2023-2-2 12:01 | 只看该作者
NTP是网络数据的吗?              

使用特权

评论回复
10
alvpeg| | 2023-2-4 12:34 | 只看该作者
http访问方式。              

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

37

主题

91

帖子

2

粉丝