TinkerNode 制作斐波那契时钟

斐波那契数列指的是这样一个数列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368........ 这个数列从第3项开始,每一项都等于前两项之和: f(n)=f(n-1)+f(n-2)  , n>2, f(1)=1,f(2)=1。

这样的数列非常简单,但是有着很多有趣的特性。比如:而且当n趋向于无穷大时,前一项与后一项的比值越来越逼近黄金分割0.618(或者说后一项与前一项的比值小数部分越来越逼近 0.618)。

可以看到越到后面,的比值越接近黄金比。

人类和禽兽的一个很大的区别在于对颜色的识别。比如,相对于人类来说,狗狗可以称为色弱。宠物眼科医师已经得出结论,狗所看到的颜色与红绿色盲的人有点相似。狗的眼睛有蓝色和绿色的光感受体,但没有红色的光感受体。因此,狗无法轻易区分黄色,绿色和红色,但是他们可以轻松识别不同深浅的蓝色、紫色和灰色。颜色仅仅是狗感知环境的因素之一。亮度,对比度,尤其是物体运动,都是非常重要的因素,并且这些因素更能帮助狗狗来感知它们周围的环境。【参考1】

狗狗看到的颜色:

人类看到的颜色:

据说人眼对于颜色的敏感是千万年进化而来的结果。比如,准确通过识别书上红色的成熟果实,能够大大提升采集工作的效率…….沿着这种理论,女性应该能分辨出更多的红色,而显示确实如此。譬如,在我眼中都是红色的口红,在老婆眼中可能是朱红 (vermeil),粉红 (pink),梅红(plum),玫瑰红(rose),桃红(peach blossom),樱桃红 ( cherry),桔红(reddish orange),石榴红(garnet),枣红(purplish red),莲红(lotus red),浅莲红(fuchsia pink),豉豆红(bean red)…….

将斐波那契数列和颜色结合在一起,就有了斐波那契时钟,这是国外众筹网站上的一个项目:

想读出这个时钟的时间需要一点技巧:钟表面有5个区域,每个区域代表着不同的权值,比如最小的方块表示1 最大的表示5:

表面上有4种颜色:红色,黄色,蓝色和白色。其中白色表示0. 其他的取值是由其所在区域的权值决定的。具体时间的计算如下

小时数 = 红色数值 + 蓝色数值

分钟数 = (绿色数值 + 蓝色数值) x 5

表示范围就是从 00:00到12:55. 同样的时间可能由不同的方式进行表示。比如:12:55 可以由下面2种方式来表示

红色=1  蓝色=1+2+3+5=11 绿色=0

因此,时间计算方法如下:

小时数 = 1+11=12

分钟数 = (0 + 11) x 5=55

同样的,下面这样也表示 12:55

再来一个复杂的,比如表面如下:

红色=1 绿色=2 蓝色=3+5=8

小时数 = 1+8=9

分钟数 = (2 + 8) x 5=50

所以上面表示的是 9:50 (另外,这个时间总共有6种表示方法)

从表示方法上也能看出来时间是以5分钟为单位的。无法表示注诸如  12:01 这样的时间,只有 12:00或者 12:05 这样。

00:00到 12:55一共有 152个时间,有些时间有很多个表示方法总共有992个方案。

上面介绍了最原始的斐波那契时钟, 小的表面并不能满足我的要求,因此,我设计了一个使用 TinkerNode(ESP32)发出 VGA信号的方案,这样可以将带有 VGA 的显示器变成时钟。电路上非常简单,最主要的是 VGA 接口上的 R,G,B 3个Pin和 Vsync 和 Hsync 2个Pin。

PCB 设计如下:

和之前的设计一样,硬件只是整个作品的一小部分,绝大多数工作是软件来完成的。

需要解决的第一个问题是时间的表示,为此,我编写了一个C语言代码将所有的可能时间拆分表示, 最终结果在 clockdata.h 中。比如,当前时间是  12:55 。首先要在 TimeToIndex [] 中查找(12*12+55/5=155), 其中  TimeToIndex [155] 定义如下:

//155  12:55  990

{2,990}

意思是: 12:55 有2种表示方法,在 TimeToSectionColor [] 这个表格中990 开始处:

//155  12:55  990

{SECRED  ,SECBLUE ,SECBLUE ,SECBLUE ,SECBLUE },

{SECBLUE ,SECRED  ,SECBLUE ,SECBLUE ,SECBLUE }

第一种方法, Section 0-4 颜色分别为 RED,BLUE,BLUE,BLUE,BLUE。 第二种方法,Section 0-4 颜色分别为 BLUE,RED,BLUE,BLUE,BLUE。根据不同区域,将对应区域设定设定为指定的颜色即可。

接下来是时间的获取, TinkerNode 支持物联网,可以轻松的获得,在 DFRobot_NBIOT\examples\LocalTime\CalLocalTime_NB 有这样的例子(此外,对于 TinkerNode开发板还可以通过 GPS/ WIFI 来实现自动获得)。完整代码如下:

// TinkerNode Quectel BC20 模块支持
#include "DFRobot_BC20.h"

// VGA 支持库
#include <ESP32Lib.h>
#include <Ressources/Font8x8.h>

// 计算后的的时间对颜色表示方法
#include "clockdata.h"

DFRobot_BC20 myBC20;

// VGA 显示引脚
const int redPin = 22;
const int greenPin = 21;
const int bluePin = 19;
const int vsyncPin = 18;
const int hsyncPin = 23;

const int TIMEX= 560;
const int TIMEY= 450;

// 3 Bit VGA 支持
VGA3BitI vga;

void setup()
{
  Serial.begin(115200);
  while(!myBC20.powerOn()){
    delay(1000);
    Serial.print(".");
  }
  Serial.println("BC20 started!");
  while(!myBC20.checkNBCard()){
    Serial.println("Please insert the NB SIM card !");
    delay(1000);
  }
  Serial.println("Waitting for access ...");
  while(myBC20.getGATT() == 0){
    Serial.print(".");
    delay(1000);
  }
  Serial.println("Waiting for NB time...");
  // 通过 BC20 获得当前时间
  while(myBC20.getCLK()){
    if(sCLK.Year > 2000){
      break;
    }
    Serial.print(".");
    delay(1000);
  }
  Serial.println();
  Serial.println("Configure local time");
  // 将获得的时间设置给ESP32
  configLocalTime(sCLK.Year,sCLK.Month,sCLK.Day,sCLK.Hour,sCLK.Minute,sCLK.Second);
Serial.printf("sdata %d-%02d-02d %02d:%02d:%02d\n",sCLK.Year,sCLK.Month,sCLK.Day,sCLK.Hour,sCLK.Minute,sCLK.Second);
  // 显示模式为  640x480 (主要是内存限制)
	vga.init(vga.MODE640x480, redPin, greenPin, bluePin, hsyncPin, vsyncPin);
	// 使用 8x8 字库
	vga.setFont(Font8x8);
	// 背景为白色
	vga.clear(vga.RGB(0xffffff));
  /*
	// 这里可以再屏幕上显示一下当前内存剩余情况
	// 设置文字颜色
	vga.setTextColor(vga.RGB(0));
  // 设置屏幕输出位置
  vga.setCursor(0, 20);
  //show the remaining memory
  vga.print("free memory: ");
  vga.print((int)heap_caps_get_free_size(MALLOC_CAP_DEFAULT));
  */
}

// 颜色索引到颜色值转换函数
int SectionColor(int ColorIndex) {
  if (ColorIndex==SECRED) {return vga.RGB(0xFF);}
  if (ColorIndex==SECGREEN) {return vga.RGB(0xFF00);}
  if (ColorIndex==SECBLUE) {return vga.RGB(0xFF0000);}  
  return vga.RGB(0xFFFFFF);
}

// 记录上一个秒数
int LastSecond=0xFF;

// 记录上一个颜色
int LastTimeIndex=0xFFFF;

void loop()
{
  int TimeIndex;

  // 取得当前时间
  tm timeinfo;
  if(!getLocalTime(&timeinfo)){
    Serial.println("Failed to obtain time");
    return;
  }
  AnalysisTime(&timeinfo);
  Serial.printf("%02d:%02d:%02d\n",timeinfo.tm_hour,timeinfo.tm_min,timeinfo.tm_sec);
  // 如果秒发生变化
  if (timeinfo.tm_sec!=LastSecond) {
      // 串口输出当前时间
      //Serial.printf("%02d:%02d:%02d\n",timeinfo.tm_hour,timeinfo.tm_min,timeinfo.tm_sec);
      // 擦除之前屏幕上绘制的时间
      vga.fillRect(TIMEX,TIMEY,64,64,vga.RGB(0xffffff));
      // 设置文字颜色
      vga.setTextColor(vga.RGB(0));
      vga.setCursor(TIMEX,TIMEY);
      // 如果小时只有1位,那么前面用 0 填充      
      if (timeinfo.tm_hour<10) {vga.print("0");}
      vga.print(timeinfo.tm_hour);
      vga.print(":");
      // 如果分钟只有1位,那么前面用 0 填充
      if (timeinfo.tm_min<10) {vga.print("0");}
      vga.print(timeinfo.tm_min);
      vga.print(":");
      // 如果秒只有1位,那么前面用 0 填充
      if (timeinfo.tm_sec<10) {vga.print("0");}
      vga.print(timeinfo.tm_sec);
      LastSecond=timeinfo.tm_sec;
      
      // 时间表示范围是从 0:0 到 12:55, 所以对于12点特别处理
      if (timeinfo.tm_hour!=12) {TimeIndex=(timeinfo.tm_hour % 12)*12+(timeinfo.tm_min/5);}
      
      // 只有当需要改变屏幕显示的时候才重新绘制
      if (LastTimeIndex!=TimeIndex) {
        // 
        Serial.print("New time index:");
        Serial.printf("%d  [%d %d]\n",TimeIndex,TimeToIndex[TimeIndex].start,TimeToIndex[TimeIndex].number);
        
        // 生成下一个时间的颜色,对于有多个可能性的事件,随机选择一个
        int PoolIndex=TimeToIndex[TimeIndex].start+random(TimeToIndex[TimeIndex].number);
        Serial.print("Pool index:");
        Serial.println(PoolIndex);
        Serial.printf("Pool [%d %d %d %d %d]\n",
                TimeToSectionColor[PoolIndex].timepool[0],
                TimeToSectionColor[PoolIndex].timepool[1],
                TimeToSectionColor[PoolIndex].timepool[2],
                TimeToSectionColor[PoolIndex].timepool[3],
                TimeToSectionColor[PoolIndex].timepool[4]
        );
        
        // 使用圆形表示时间
        if (TimeToSectionColor[PoolIndex].timepool[0]==SECNONE) {
              vga.fillCircle(80,80,80,vga.RGB(0xFFFF));
              vga.fillCircle(80,80,75,SectionColor(TimeToSectionColor[PoolIndex].timepool[0]));
          }else{  
              vga.fillCircle(80,80,80,SectionColor(TimeToSectionColor[PoolIndex].timepool[0]));
        } 
        if (TimeToSectionColor[PoolIndex].timepool[1]==SECNONE) {
              vga.fillCircle(200,40,40,vga.RGB(0xFFFF));
              vga.fillCircle(200,40,35,SectionColor(TimeToSectionColor[PoolIndex].timepool[1]));
        }else {
              vga.fillCircle(200,40,40,SectionColor(TimeToSectionColor[PoolIndex].timepool[1]));
        }
        if (TimeToSectionColor[PoolIndex].timepool[2]==SECNONE) {
              vga.fillCircle(200,120,40,vga.RGB(0xFFFF));
              vga.fillCircle(200,120,35,SectionColor(TimeToSectionColor[PoolIndex].timepool[2]));
        }else {
              vga.fillCircle(200,120,40,SectionColor(TimeToSectionColor[PoolIndex].timepool[2]));
        }
        if (TimeToSectionColor[PoolIndex].timepool[3]==SECNONE) {
              vga.fillCircle(120,280,120,vga.RGB(0xFFFF));
              vga.fillCircle(120,280,115,SectionColor(TimeToSectionColor[PoolIndex].timepool[3]));
        }else {
              vga.fillCircle(120,280,120,SectionColor(TimeToSectionColor[PoolIndex].timepool[3]));
        }
        if (TimeToSectionColor[PoolIndex].timepool[4]==SECNONE) {
              vga.fillCircle(440,200,200,vga.RGB(0xFFFF));
              vga.fillCircle(440,200,195,SectionColor(TimeToSectionColor[PoolIndex].timepool[4]));
        }else {
              vga.fillCircle(440,200,200,SectionColor(TimeToSectionColor[PoolIndex].timepool[4]));
        }
        

        /*
        // 使用正方形表示时间
        vga.fillRect(0,0,160,160,SectionColor(TimeToSectionColor[PoolIndex].timepool[0]));
        vga.fillRect(160,0,80,80,SectionColor(TimeToSectionColor[PoolIndex].timepool[1]));
        vga.fillRect(160,80,80,80,SectionColor(TimeToSectionColor[PoolIndex].timepool[2]));
        vga.fillRect(0,160,240,240,SectionColor(TimeToSectionColor[PoolIndex].timepool[3]));
        vga.fillRect(240,0,400,400,SectionColor(TimeToSectionColor[PoolIndex].timepool[4]));
        */
        
        /*
        // 这里可以预览一下8种颜色
        vga.fillRect(0,0,80,80,  vga.RGB(0x000000));
        vga.fillRect(80,0,80,80, vga.RGB(0x0000FF));
        vga.fillRect(160,0,80,80,vga.RGB(0x00FF00));
        vga.fillRect(240,0,80,80,vga.RGB(0xFF0000));
        vga.fillRect(320,0,80,80,vga.RGB(0xFF00FF));
        vga.fillRect(400,0,80,80,vga.RGB(0xFFFFFF));
        vga.fillRect(480,0,80,80,vga.RGB(0x00FFFF));
        vga.fillRect(560,0,80,80,vga.RGB(0xFFFF00));
        */

        LastTimeIndex=TimeIndex;
      }
   } 
   delay(900);  
}

完整代码下载:

最终,提到颜色,不由得让人想起乐嘉老师的“色彩心理学”,他的学说完全可以作为检验一个人常识和逻辑水平的试金石。

本文首发 https://mc.dfrobot.com.cn/thread-307842-1-1.html

参考:

1. 作者:汪小喵 https://www.zhihu.com/question/22005284/answer/64313689

工作视频在B站可以看到

https://www.bilibili.com/video/BV1df4y1Y7rR

对应的电路图和 PCB

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注