斐波那契数列指的是这样一个数列 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