arduino控制点阵屏与蜂鸣器_手把手教你制作ESP8266物联网创意点阵时钟,女朋友看了都想要!

97c3589d9b0428065ba388d344519a4a.png

本文作者:默

前段时间我在网上看到了一款很有意思的点阵时钟,它可以播报天气,查看 YouTube 的订阅数,还有好看的时间动画。你可以把它当做普通闹钟,也可以连接蓝牙把它当做音箱来使用。它的许多功能都很有意思,其中我最喜欢的是它的时间显示动画效果,然而它一千多的价格让我望而却步,放弃了入手的打算。不过既然身为创客,我为什么不制作一个属于自己独一无二的创意网络时钟呢?

1703330633f08582b8e879019e9bf8d7.png

说干就干,于是我就做了一个创意点阵时钟,先来看一下演示视频吧:

913ff4e9b2832a8c8b7a6a826c79c1b1.png
ESP8266创意点阵时钟演示https://www.zhihu.com/video/1252695346146377728

预期目标及功能

  • 网络自动校准时间
  • 无网络连接时及时反馈
  • 一键配置时钟网络
  • 自定义精美时间显示字体
  • 时间显示动画
  • 亮度自动调节
  • 时段提示

材料清单

  • ESP8266 Wemos mini 开发板 1 块;
  • 杜邦线若干;
  • 4 合 1 点阵模块;
  • 激光切割外壳;
  • 栎木滑面仿木纹贴纸。

c1fced8f0a26604d69c2a4be23130a66.png

电路原理图

电路连接关系如下图所示:

500599f5ff21e1cee1c61101e1746472.png

结构拼装

将 USB 数据线按下图所示方向由外壳背部插入开发板,使用热熔胶将开发板固定到木板上,保持稳定直到热熔胶凝固,主要热熔胶不要碰到数据线。

0d31a3fa708d6dff3035ff37efb9c686.png

将外壳前部与点阵屏按下图所示方式放置入面板凹槽,使用热熔胶固定点阵,保持稳定直到热熔胶凝固。

8c9f07b4f703749c03d81b789ec066e0.png

然后使用杜邦线按原理图正确连接电路,并拼接外壳底部与左右两侧,最后将外壳顶部进行封顶。

593deb3e975367153ed42e56acc7eef3.png

剪切合适大小的栎木滑面仿木纹贴纸,粘贴至外壳表面。注意留出点阵位置,可以适当使用刻刀雕刻出 USB 下载接口,以便进行供电及程序下载或更新。

7822f884c104846e51df735e62df28d0.png

程序设计

下面开始详细讲解程序设计过程。

开发环境

我们使用 Aduino 软件来编写本项目的程序,开发板选择 ESP8266 类型。至于如何在 Arduino 中配置 ESP8266 的开发环境,不在本文的介绍范围,请自行查阅相关资料。

程序思路

为了达到我们的预期目标,我们先绘制功能的思维导图,再根据思维导图逐步实现创意点阵时钟的程序设计。

72979e713d8e8a68e86d2b4ccb9f9be1.png

下面我们将具体讨论创意点阵时钟各个子功能是如何实现的。

获取网络时间

作为一个时钟,最重要的功能当然是显示时间啦。那么该如何从网络获取时间呢?

下面的例子演示了如何获取网络时间并将时间保存在变量中,其中 ESP8266WiFi.h 库的功能是连接网络,NtpClientLib.h 库的功能是获取 NTP 服务器的网络时间,SimpleTimer.h 库是用来设置定时器每秒刷新一次时间。该例子并没有串口打印当前时间,你可以添加串口打印相关代码用来调试程序。

#include <ESP8266WiFi.h>
#include <NtpClientLib.h>
#include <TimeLib.h>
#include <SimpleTimer.h>

SimpleTimer timer;

const PROGMEM char *ntpServer = "ntp1.aliyun.com";
int8_t timeZone = 8;

volatile int hour_variable;
volatile int minute_variable;
volatile int second_variable;

void Simple_timer() {
  hour_variable = NTP.getTimeHour24();
  minute_variable = NTP.getTimeMinute();
  second_variable = NTP.getTimeSecond();
}

void setup() {
  Serial.begin(9600);
  WiFi.begin("ssid", "password");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
    
  Serial.println("Local IP:");
  Serial.print(WiFi.localIP());
    
  NTP.setInterval(600);
  NTP.setNTPTimeout(1500);
  NTP.begin(ntpServer, timeZone, false);
    
  timer.setInterval(1000L, Simple_timer);
}

void loop() {
  timer.run();
}

点阵屏显示库:MD_Parola

MD_Parola 是 MAX7219 点阵屏的模块化滚动文本显示库,其主要特点如下:

  • 支持点阵屏显示文本时左对齐、右对齐或居中对齐;
  • 具有文字滚动,进入和退出效果;
  • 能够控制显示参数和动画速度;
  • 支持硬件 SPI 接口;
  • 可以在点阵屏虚拟多个显示区域;
  • 用户定义的字体和/或单个字符替换;
  • 支持双高显示;
  • 支持在混合显示文本和图形。

下面的例子简单演示了如何利用 MD_Parola 滚动显示字符串,其中 MD_Parola 对象有 4 个参数:分别为 SPI 管脚 DIN、CLK、CS 及点阵数目。下面我们所做的创意点阵时钟的显示功能均由此库开发。

#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>

MD_Parola P = MD_Parola(13,14,12,4);  //DIN(D7) CLK(D5) CS(D6)
MD_MAX72XX mx = MD_MAX72XX(13,14,12,4);  //DIN(D7) CLK(D5) CS(D6)

void setup() {
  mx.begin();
  P.begin();
}

void loop() {
  if (P.displayAnimate()) {
    P.displayScroll("Mixly", PA_LEFT, PA_SCROLL_LEFT, 50);
  }
}

点阵位图取模

要在点阵屏中显示图片,首先需要设计点阵图案(位图),然后对图案进行取模操作。点阵取模使用 PCtoLCD2002 取模软件,取模设置如下:

bbe814d9c05e2541bab02a3648724550.png

取模方式为阴码、逆向、逐列式,输出方式为 16 进制,注意格式设置为 C51 格式,其余参数按照默认取模方式设置即可。

位图显示函数:display_bitmap()

这里我们取模的数据格式为 uint8_t 数组,我们有自定义字体 0~9 和时间分隔符“:”,再加上一些自定义的图像,这就导致我们有大量的位图。为了方便的管理这些位图,我们使用指针数组 bitmap_data[] 去管理我们的位图。为了显示方便,我们定义了函数 display_bitmap(),该函数需要 3 个参数,分别为显示横坐标 abscissa、位图宽度 width 及指针数组 bitmap_data[] 中的位置 bitmap_number。需要注意的是我们这里并没有指定位图的高度,因为我们用到的 MAX7219 点阵屏分辨率为 8×32,所以这里我们默认位图高度为 8。

#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>

MD_Parola P = MD_Parola(13,14,12,4); //DIN(D7) CLK(D5) CS(D6) 
MD_MAX72XX mx = MD_MAX72XX(13,14,12,4);

uint8_t bitmap_data1[] = {0x3e, 0x2a, 0x3e};
uint8_t bitmap_data2[] = {0x2e, 0x2a, 0x3e};

uint8_t * bitmap_data[] = {
  bitmap_data1
  bitmap_data2
  ……
};

void display_bitmap(int abscissa, int width, int bitmap_number) {
  mx.control(MD_MAX72XX::UPDATE, MD_MAX72XX::OFF);
  mx.setBuffer(abscissa, width, bitmap_data[bitmap_number]);
  mx.control(MD_MAX72XX::UPDATE, MD_MAX72XX::ON);
}

时间显示:时、分

MD_Parola 库中,由于字体过大而且不美观,导致显示的时间过长,所以我们需要自定义字体。自定义字体如下图所示,值得注意的是 0~9 的位图宽度是 3,分割符“:”的宽度是 1。

0de03bbad29d6c94b85ba0b8c564566d.png

自定义字体取模数据如下所示:

uint8_t Small_font_0[] = {0x3e, 0x22, 0x3e};
uint8_t Small_font_1[] = {0x24, 0x3e, 0x20};
uint8_t Small_font_2[] = {0x3a, 0x2a, 0x2e};
uint8_t Small_font_3[] = {0x2a, 0x2a, 0x3e};
uint8_t Small_font_4[] = {0x0e, 0x08, 0x3e};
uint8_t Small_font_5[] = {0x2e, 0x2a, 0x3a};
uint8_t Small_font_6[] = {0x3e, 0x2a, 0x3a};
uint8_t Small_font_7[] = {0x02, 0x02, 0x3e};
uint8_t Small_font_8[] = {0x3e, 0x2a, 0x3e};
uint8_t Small_font_9[] = {0x2e, 0x2a, 0x3e};
uint8_t Small_font_10[] = {0x14};

下面我们分析如何显示时间,这里我们只显示小时和分钟。

这里我们有一个小技巧,我们可以把 0~9 的位图放到指针数组 bitmap_data[] 的 0~9 的位置上,时间分隔符“:”放置在数组序号 10 的位置上。由于前面我们定义了一个显示位图的函数 display_bitmap(),这样我们不需要通过任何映射就可以显示数字了,例如 display_bitmap(22, 3, 0) 就显示 0;display_bitmap(22, 3, 1) 就显示 1,这样是不是很方便呢?

为了分别获取小时和分钟的十位及个位,我们需要对其进行除法和取余操作,例如对小时 9 除 10 得到十位 0(为什么不是0.9?这是因为我们时间变量定义为整数,一个整数除以另一个整数结果只能为整数。还是不懂?那你就该补一下C语言基础知识了。),9 除 10 取余得到个位 9。由分析我们在合适的位置显示时间得到了下面的时间显示函数。

最后,为了显示更加美观,如果小时或分钟只有一位数,我们就需要进行补零操作,将 1:1 补零变成 01:01。显示时间的代码如下:

display_bitmap(22, 3, hour_variable / 10);
display_bitmap(18, 3, hour_variable % 10);
display_bitmap(14, 1, 10);
display_bitmap(12, 3, minute_variable / 10);
display_bitmap(8, 3, minute_variable % 10);

时间显示:秒

时间在流逝,但是我们上面并没有显示秒钟,那我们怎样感知时间的进度呢?为了解决这个问题,我们定义了下面的一系列位图,注意这里定义位图的宽度是 5 不是 8,我们每隔一秒切换一次下面的位图,看起来是不是像秒针在走动呢?

b05032bf0e8a6612c987915cbfbda1d1.png

使用取模软件分别对上述点阵图案取模:

uint8_t clock_0[] = {0x1c, 0x22, 0x2e, 0x22, 0x1c};
uint8_t clock_1[] = {0x1c, 0x22, 0x2a, 0x26, 0x1c};
uint8_t clock_2[] = {0x1c, 0x22, 0x2a, 0x2a, 0x1c};
uint8_t clock_3[] = {0x1c, 0x22, 0x2a, 0x32, 0x1c};
uint8_t clock_4[] = {0x1c, 0x22, 0x3a, 0x22, 0x1c};
uint8_t clock_5[] = {0x1c, 0x32, 0x2a, 0x22, 0x1c};
uint8_t clock_6[] = {0x1c, 0x2a, 0x2a, 0x22, 0x1c};
uint8_t clock_7[] = {0x1c, 0x26, 0x2a, 0x22, 0x1c};

前面我们指针数组 bitmap_data[] 的 0~10 位置都用来放置数字了,我们这里有 8 幅位图,所以放入指针数组 bitmap_data[] 的 11~18 位置,我们定义一个静态局部变量Clock_variable,设置其初始值为 11,每隔一秒 Clock_variable 变量的值增加 1,并显示对应序号的位图,当 Clock_variable 的值为 19 时,将它重新赋值为 11,这样我们就实现了秒表动画的设计。程序如下:

static int Clock_variable = 11;

display_bitmap(4, 5, Clock_variable);

Clock_variable = Clock_variable + 1;

if (Clock_variable == 19) {
  Clock_variable = 11;
}

上面我们设计了秒表动画,但是还有一个问题,由于点阵屏空间限制,我们没办法用数字显示精确的秒数,那怎么办呢?我们观察到,在点阵屏的底部还空了 2 个像素点的高度,所以我们可以在最后一行通过点数显示精确到秒数。

e8db1374fbf6e4b23555d98caa0863f4.png

如上图所示,最后一行前面有 5 个点,后面有 9 个点,因此秒数为 59 秒。显示秒数的代码如下:

if (second_variable / 10) {
  mx.drawLine(7, 22, 7, (23 - second_variable / 10), true);
}
if (second_variable % 10) {
  mx.drawLine(7, 14, 7, (15 - second_variable % 10), true);
}

其中 mx.drawLine() 为绘制线段的函数,它有 4个参数,分别为:线段起点横坐标、起点纵坐标、终点横坐标、终点纵坐标,以及显示状态(true 点亮线段;false 熄灭线段)。根据我们使用的 4 和 1 点阵坐标定义,其中横坐标最大为 7,纵坐标最大为 31。

e4958e612740c4a4a9dd3d880b252b3f.png

当秒数的个位为 0 的时候将线段清除,重复显示线段即可显示当前秒数了。这里我不对显示线段的位置、长度与秒数的关系进行分析,留给大家当做思考题活动一下大脑了。

时段图标显示

为了感知一天时间的变化,我们希望不同时间段用不同的图标进行提示。我们定义了太阳和月亮两个图标,它们的宽度都是 8,样式如下图所示。

36b9a95a13f2f64a15b4b04e332b8c85.png

使用取模软件取模数据如下:

uint8_t sun[] = {0x24, 0x00, 0xbd, 0x3c, 0x3c, 0xbd, 0x00, 0x24};
uint8_t moon[] = {0x38, 0x7c, 0xe2, 0xc0, 0xc4, 0x4e, 0x24, 0x00};

继续将太阳和月亮的取模数据添加到指针数组 bitmap_data[] 的位置 19 和 20。这里我们定义在 6 点到 18 点之间,在横坐标为 31 处显示太阳,其他时间显示月亮,程序如下:

if ((hour_variable >= 6) && (hour_variable <= 18)) {
  display_bitmap(31, 8, 19);
} else {
  display_bitmap(31, 8, 20);
}

一键配网:WiFiManager

如果我们在程序里固定 WiFi 信息,那么当网络环境变化时,时钟将不可用,此时你需要重新修改网络信息并上传程序,这无疑是很麻烦的。为此我们需要一种动态修改网络信息的办法,这里我们使用了 WiFiManager 库,该库支持通过网页对 WiFi 连接进行配置。下面是一个网络配置的简单示例,该例子上传成功后,将启用一个名为 ESP8266 的 WiFi 热点,使用手机连接此热点即可按提示对网络进行配置。这里你也可以使用其他热点名称,例如你的作品名称而不是 ESP8266。需要注意的是,ESP8266 仅支持 2.4G 频段的 WiFi 网络,不支持 5G 频段的 WiFi 网络。

#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h>

WiFiServer server(80);

void setup(){
 WiFiManager wifiManager;
 wifiManager.autoConnect("ESP8266");
 server.begin();
}

void loop(){

}

WiFi 连接反馈

当网络环境发生变化时,我们可能需要对网络重新进行配置,为此我们定义了下面的位图用于断网提示。该位图的宽度为 19,看上去像是 WiFi 被外星人劫持了,是不是很生动形象!

ff69288302bbfbcf555e3f537dc2948f.png

使用取模软件取模数据如下:

uint8_t wifi[] = {0x04, 0x06, 0x13, 0xDB, 0xDB, 0x13, 0x06, 0x04, 0x00, 0x70, 0x18, 0x7d, 0xb6, 0x3c, 0x3c, 0xb6, 0x7d, 0x18, 0x70};

这里我们使用 !(WiFi.status() != WL_CONNECTED) 语句来判断网络连接是否断开。当 WiFi 连接成功时,!(WiFi.status() != WL_CONNECTED) 返回真,这是我们可以同步时间;当 WiFi 断开时,!(WiFi.status() != WL_CONNECTED) 返回假,我们在点阵屏上显示 WiFi 断开连接提示,然后使用配网函数对网络进行配置,配网成功后再次显示正常的时间即可。代码如下:

if (!(WiFi.status() != WL_CONNECTED)) {
  hour_variable = NTP.getTimeHour24();
  minute_variable = NTP.getTimeMinute();
  second_variable = NTP.getTimeSecond();
} else {
  mx.clear();
  display_bitmap(25, 19, 21);
  WiFiManager wifiManager;
  wifiManager.autoConnect("ESP8266");
  server.begin();
  mx.clear();
}

小狗动画设计

为了时钟富有动态感,我们这里为时钟添加一个小狗的动画效果,该动画由两个宽度为 8 的动画帧构成,首先我们先使用取模软件绘制出这两帧图像,再点击水平镜像按钮得到镜像后的图像,最后生成字模即可。

974c6a5d2b03e872861630411899d087.png

使用取模软件取模数据如下:

uint8_t PROGMEM dog[] = {0x8C, 0x4C, 0xFE, 0x30, 0xB0, 0x70, 0xF0, 0x08, 0x0C, 0x0C, 0xFE, 0x30, 0x30, 0x30, 0xF8, 0x00,};

下面的例子演示将点阵划分为两个区域,区域 0 和区域 1。P.setZone() 函数将点阵划分为不同的显示区域,它有 3 个参数:分别为区域编号、起始点阵及终止点阵。P.begin() 指定区域数量,参数为空默认一个区域,这里我们有两个显示区域,故参数为 2,其中点阵编号与区域的对应关系如下图所示:

311d9e914364b927e0d577ca0c99a78f.png

P.setSpriteData() 函数为精灵动画的初始化函数,该函数接受 7 个参数:分别为初始化区域、动画开始精灵数据、动画开始精灵宽度、动画开始精灵帧数、动画结束精灵数据、动画结束精灵宽度、动画结束精灵帧数。

P.displayAnimate() 函数有两个作用,分别为反馈显示状态和动画执行函数。当作为反馈状态时,动画显示完成返回 1,未完成返回 0。当作为动画执行函数时,通过不断调用该函数实现动画的流畅运行,因此程序需要不断的调用 P.displayAnimate() 函数。

P.getZoneStatus() 函数作用类似 P.displayAnimate() 函数,不同的是它仅返回区域的显示状态。

P.displayZoneText() 函数为字符串的动画显示函数,该函数接受 7 个参数,分别为:显示区域、显示字符串、对齐方式、动画速度、文本显示时间、动画进入效果、动画退出效果。下面的代码演示了如何在区域显示精灵动画。这里我们显示字符串为空、显示时间为0,显示字符串为空保证了我们仅有小狗动画没有文字,显示时间为 0 保证了小狗动画的连贯性。

void setup() {
  P.begin(2);
  mx.begin();
  P.setZone(0, 0, 2);
  P.setZone(1, 3, 3);
  P.setSpriteData(1, dog, 8, 2, dog, 8, 2);
}

void loop() {
  P.displayAnimate();
  if (P.getZoneStatus(1)) {
    P.displayZoneText(1, "", PA_CENTER, 100, 0, PA_SPRITE, PA_SPRITE);
  }
}

自动亮度调节

当我们睡觉以后我们是不会看时间的,此时降低点阵显示的亮度有助于节能环保,因此我们需要根据时间段自动调节点阵显示的亮度。下面的代码在晚上 0~6 点亮度设置为 1,其他时间亮度设置为 10。P.setIntensity() 函数为区域亮度设置函数,其有两个参数,分别是:显示区域和亮度值,其中亮度值范围为 0~15。

if ((hour_variable >= 0) && (hour_variable < 6)) {
  P.setIntensity(0, 1);
  P.setIntensity(1, 1);
} else {
  P.setIntensity(0, 10);
  P.setIntensity(1, 10);
}

代码组合

最后,按照上述功能之间的逻辑关系,将代码组合在一起即可。由于篇幅限制,这里就不放完整的代码了。

使用说明

首先连接电源,时钟进行初始化,同时出现如下界面提示配网,此时开发板会自动开启名为 ESP8266 的无密码 WiFi 热点。

0b123356731a268c37bd60fc898dc278.png

打开手机,连接这个网络,配网步骤如下图所示:

7467ca6b9629e0bdeb50ecd6b24896c6.png

4b3de4f1063f5dc4b3039e474e3393fa.png

配网说明(以安卓手机为例):

  1. 打开手机设置选择 WiFi 设置打开 WiFi;
  2. 连接时钟热点 ESP8266(热点名由程序设置,可更改为其他名称);
  3. 选择登录进入网络配置页面;
  4. 点击配置 WiFi 进入图示页面点击扫描,扫描附近热点;
  5. 选择 WiFi 输入 WiFi 密码;
  6. 点击保存等待配网成功。

效果展示

1703330633f08582b8e879019e9bf8d7.png

82d1e27c5973cd275bc9879ce16d963d.png
  • 3
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值