一、前言
基于esp32和ws2812的彩色像素时钟,目前网上有许多类似作品,但大多使用的为esp32原生的编程语言,鉴于Python的简洁易懂,我打算使用本文记录一次使用micropython编程的像素时钟过程。
micropython
首先确保你的esp32已经刷了micropython,如果你还没有,请查阅其他相关资料教程,这里不再赘述,我使用的编程软件是thonny,可以很方便的对esp32进行程序调试
thonny官网:Thonny, Python IDE for beginners
如果你无法打开github,可以使用镜像站下载(该链接为我写文章时thonny最新版本,如果thonny有更新的版本,可以将下载链接的“github.com”替换为“hub.nuaa.cf”)https://hub.nuaa.cf/thonny/thonny/releases/download/v4.1.4/thonny-4.1.4.exe
二、硬件连接
1.ws2812
接线如下图(此处散热片是我之前做别的东西时装的,已经拆不下来了):
2.DS1302
接线如下图:
3.ESP32
三、编程
1.初始化数据
首先是需要初始化的一些东西
from machine import Pin from ds1302 import DS1302 from machine import RTC import machine, neopixel, time, network, ntptime wlan = network.WLAN(network.STA_IF) n = 32*8 p = 13 printtime = [10,10,10,10,10,10] ds = DS1302(clk = Pin(25), dio = Pin(26), cs = Pin(27)) np = neopixel.NeoPixel(machine.Pin(p), n)
2.ds1302模块驱动
从网上找到了ds1302的驱动程序
保存为ds1302.py
from machine import Pin DS1302_REG_SECOND = (0x80) DS1302_REG_MINUTE = (0x82) DS1302_REG_HOUR = (0x84) DS1302_REG_DAY = (0x86) DS1302_REG_MONTH = (0x88) DS1302_REG_WEEKDAY= (0x8A) DS1302_REG_YEAR = (0x8C) DS1302_REG_WP = (0x8E) DS1302_REG_CTRL = (0x90) DS1302_REG_RAM = (0xC0) class DS1302: def __init__(self, clk, dio, cs): self.clk = clk self.dio = dio self.cs = cs self.clk.init(Pin.OUT) self.cs.init(Pin.OUT) def _dec2hex(self, dat): return (dat//10) * 16 + (dat % 10) def _hex2dec(self, dat): return (dat//16) * 10 + (dat % 16) def _write_byte(self, dat): self.dio.init(Pin.OUT) for i in range(8): self.dio.value((dat >> i) & 1) self.clk.value(1) self.clk.value(0) def _read_byte(self): d = 0 self.dio.init(Pin.IN) for i in range(8): d = d | (self.dio.value() << i) self.clk.value(1) self.clk.value(0) return d def _get_reg(self, reg): self.cs.value(1) self._write_byte(reg) t = self._read_byte() self.cs.value(0) return t def _set_reg(self, reg, dat): self.cs.value(1) self._write_byte(reg) self._write_byte(dat) self.cs.value(0) def _wr(self, reg, dat): self._set_reg(DS1302_REG_WP, 0) self._set_reg(reg, dat) self._set_reg(DS1302_REG_WP, 0x80) #开启 def start(self): t = self._get_reg(DS1302_REG_SECOND + 1) self._wr(DS1302_REG_SECOND, t & 0x7f) #关闭 def stop(self): t = self._get_reg(DS1302_REG_SECOND + 1) self._wr(DS1302_REG_SECOND, t | 0x80) #秒 def second(self, second=None): if second == None: return self._hex2dec(self._get_reg(DS1302_REG_SECOND+1)) % 60 else: self._wr(DS1302_REG_SECOND, self._dec2hex(second % 60)) #分 def minute(self, minute=None): if minute == None: return self._hex2dec(self._get_reg(DS1302_REG_MINUTE+1)) else: self._wr(DS1302_REG_MINUTE, self._dec2hex(minute % 60)) #时 def hour(self, hour=None): if hour == None: return self._hex2dec(self._get_reg(DS1302_REG_HOUR+1)) else: self._wr(DS1302_REG_HOUR, self._dec2hex(hour % 24)) #工作日 def weekday(self, weekday=None): if weekday == None: return self._hex2dec(self._get_reg(DS1302_REG_WEEKDAY+1)) else: self._wr(DS1302_REG_WEEKDAY, self._dec2hex(weekday % 8)) #天 def day(self, day=None): if day == None: return self._hex2dec(self._get_reg(DS1302_REG_DAY+1)) else: self._wr(DS1302_REG_DAY, self._dec2hex(day % 32)) #月 def month(self, month=None): if month == None: return self._hex2dec(self._get_reg(DS1302_REG_MONTH+1)) else: self._wr(DS1302_REG_MONTH, self._dec2hex(month % 13)) #年 def year(self, year=None): if year == None: return self._hex2dec(self._get_reg(DS1302_REG_YEAR+1)) + 2000 else: self._wr(DS1302_REG_YEAR, self._dec2hex(year % 100)) #获取或设置时间 def date_time(self, dat=None): if dat == None: return [self.year(), self.month(), self.day(), self.weekday(), self.hour(), self.minute(), self.second()] else: self.year(dat[0]) self.month(dat[1]) self.day(dat[2]) self.weekday(dat[3]) self.hour(dat[4]) self.minute(dat[5]) self.second(dat[6]) #返回设置结果 def ram(self, reg, dat=None): if dat == None: return self._get_reg(DS1302_REG_RAM + 1 + (reg % 31)*2) else: self._wr(DS1302_REG_RAM + (reg % 31)*2, dat)
3.ws2812
对于ws2812,micropython有内置的库neopixel,因此我们不需要额外添加驱动
4.坐标转换
neopixel需要灯的序号来给ws2812发送数据,对于点阵屏来说是及其不方便的,因此这里我写了一个函数用于通过坐标点亮指定的灯
def set_pixel(x,y,color): if x<0 or x>=32 or y<0 or y>=8: raise ValueError("x,y are out of range") index = x*8+y np[index] = color #np.write()
不难看出,我以点阵屏右上角为坐标原点,向下为y轴正方向,向左为x轴正方向建的坐标系。对于为何将np.write()注释掉,稍候我会解释
5.数字显示
这里我写了一个函数,输入指定的数字和坐标(数字为3*5,坐标定位数字右上角)即可显示数字在对应位置
def get_xy(i): if i>7 or i<1: raise ValueError("i is out of range") if i==1: return [(2,0),(2,1),(2,2)] elif i==2: return [(2,2),(2,3),(2,4)] elif i==3: return [(2,0),(1,0),(0,0)] elif i==4: return [(2,2),(1,2),(0,2)] elif i==5: return [(2,4),(1,4),(0,4)] elif i==6: return [(0,0),(0,1),(0,2)] elif i==7: return [(0,2),(0,3),(0,4)]
^为了减少代码量,先将“8”字(类似数码管)每个笔画都写在一个函数里
def set_figure(f,x,y,color): ze = get_xy(3)+get_xy(1)+get_xy(2)+get_xy(5)+get_xy(6)+get_xy(7) on = [(1,0), (1,1), (2,1), (1,2), (1,3), (1,4),(2,4),(0,4)] tw = get_xy(3)+get_xy(6)+get_xy(4)+get_xy(2)+get_xy(5) th = get_xy(3)+get_xy(6)+get_xy(4)+get_xy(7)+get_xy(5) fo = get_xy(1)+get_xy(4)+get_xy(6)+get_xy(7) fi = get_xy(3)+get_xy(1)+get_xy(4)+get_xy(7)+get_xy(5) si = get_xy(3)+get_xy(1)+get_xy(4)+get_xy(2)+get_xy(7)+get_xy(5) se = get_xy(3)+get_xy(6)+get_xy(7) ei = get_xy(3)+get_xy(1)+get_xy(2)+get_xy(4)+get_xy(5)+get_xy(6)+get_xy(7) ni = get_xy(3)+get_xy(1)+get_xy(6)+get_xy(4)+get_xy(7)+get_xy(5) for i in ei : set_pixel(i[0]+x,i[1]+y,(0,0,0)) for i in on : set_pixel(i[0]+x,i[1]+y,(0,0,0)) if f==0: for i in ze : set_pixel(i[0]+x,i[1]+y,color) elif f==1: for i in on : set_pixel(i[0]+x,i[1]+y,color) elif f==2: for i in tw : set_pixel(i[0]+x,i[1]+y,color) elif f==3: for i in th : set_pixel(i[0]+x,i[1]+y,color) elif f==4: for i in fo : set_pixel(i[0]+x,i[1]+y,color) elif f==5: for i in fi : set_pixel(i[0]+x,i[1]+y,color) elif f==6: for i in si : set_pixel(i[0]+x,i[1]+y,color) elif f==7: for i in se : set_pixel(i[0]+x,i[1]+y,color) elif f==8: for i in ei : set_pixel(i[0]+x,i[1]+y,color) elif f==9: for i in ni : set_pixel(i[0]+x,i[1]+y,color)
这里由于切换数字要先清除之前的数字,因此先写入“8”的数字颜色为(0,0,0)和1的颜色为(0,0,0)(由于数字“1”与其他数字不一样,因此只用“8”来清空不够(看成果就知道了))
6.时间显示
下面进行时间的显示
def print_time(x,y,color): global printtime thetime = rtc.datetime()[4:7] set_figure(thetime[0]//10,x-3,y,color) set_figure(thetime[0]-10*(thetime[0]//10),x-7,y,color) set_figure(thetime[1]//10,x-13,y,color) set_figure(thetime[1]-10*(thetime[1]//10),x-17,y,color) set_figure(thetime[2]//10,x-23,y,color) set_figure(thetime[2]-10*(thetime[2]//10),x-27,y,color) #left : set_pixel(19,2,color) set_pixel(19,4,color) #right : set_pixel(9,2,color) set_pixel(9,4,color) np.write()
7.时间校准
到这里,已经可以显示基本的时间信息了,但我们需要校准时间并使时间在断电后依旧可以走时
所以这里我写了一个函数,先检查网络连接,若有网则使用网络校准时间并校准ds1302,没有网则使用ds1302校准时间
这里我已经在boot.py内配置好了WIFI如果你还没有配置,添加下面的语句到boot.py
SSID = '(你的WIFI名称)' PASSWORD = '(你的WIFI密码)' def connect_wifi(ssid, password, timeout=10): start = time.time() wlan = network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): print('connecting to network...') wlan.connect(ssid, password) while not wlan.isconnected(): if time.time() - start > timeout: return False pass print('network config:', wlan.ifconfig()) return True if not connect_wifi(SSID, PASSWORD): print('Failed to connect WiFi')
校准时间函数:(小时+8是因为ntp服务器默认的时间为UTC0)
def set_time(): if wlan.isconnected(): try: ntptime.host = 'ntp1.aliyun.com' ntptime.settime() rtc.datetime(rtc.datetime()[0:4]+(int(rtc.datetime()[4])+8,)+rtc.datetime()[5:8]) ds.date_time(rtc.datetime()[0:7]) except: rtc.datetime(ds.date_time()+[0]) else: rtc.datetime(ds.date_time()+[0])
8.测试时间显示
此时,基本的时间显示已经可以运行了,是时候测试一下了(注意,颜色不要调的过亮,否则可能会发热严重)
该程序会每隔5h校准一次时间
set_time() while(True): if t%180000 == 0: set_time() t = 0 print_time(28,1,(20,20,20)) time.sleep(0.1) t += 1
但是,好景不长,我发现每循环一次时,就会闪烁一下,非常不好看(此时我还没有注释掉坐标转换函数内的np.write()语句
我反复思考,怎么让它不会闪烁呢,显然,闪烁是因为每次更新数字时,都要擦掉原来的数字再重新显示,因此我想了老长时间,想出一个麻烦但貌似可行的办法,每次更新时,不擦全部数字,而是擦掉与原来数字不一样的部分,为此,我改动了时间显示函数,将数字显示里的先擦除所有数字语句删掉,然后添加了一个用于擦除不一样的像素的函数
改动的时间显示函数:
def print_time(x,y,color): global printtime thetime = rtc.datetime()[4:7] #print(thetime[4]," ",thetime[5]," ",thetime[6]) #print(thetime[4]//10,thetime[4]-thetime[4]//10,thetime[5]//10,thetime[5]-thetime[4]//10,thetime[6]//10,thetime[6]-thetime[4]//10) if printtime[0] != thetime[0]//10: printtime[0] = thetime[0]//10 smooth_clear(printtime[0],x-3,y) #clear set_figure(printtime[0],x-3,y,color) if printtime[1] != thetime[0]-10*(thetime[0]//10): printtime[1] = thetime[0]-10*(thetime[0]//10) smooth_clear(printtime[1],x-7,y) set_figure(printtime[1],x-7,y,color) if printtime[2] != thetime[1]//10: printtime[2] = thetime[1]//10 smooth_clear(printtime[2],x-13,y) #clear set_figure(printtime[2],x-13,y,color) if printtime[3] != thetime[1]-10*(thetime[1]//10): printtime[3] = thetime[1]-10*(thetime[1]//10) smooth_clear(printtime[3],x-17,y)#clear set_figure(printtime[3],x-17,y,color) if printtime[4] != thetime[2]//10: printtime[4] = thetime[2]//10 smooth_clear(printtime[4],x-23,y) #clear set_figure(printtime[4],x-23,y,color) if printtime[5] != thetime[2]-10*(thetime[2]//10): printtime[5] = thetime[2]-10*(thetime[2]//10) smooth_clear(printtime[5],x-27,y) set_figure(printtime[5],x-27,y,color) #set_figure(thetime[0]//10,x-3,y,color) #set_figure(thetime[0]-10*(thetime[0]//10),x-7,y,color) #set_figure(thetime[1]//10,x-13,y,color) #set_figure(thetime[1]-10*(thetime[1]//10),x-17,y,color) #set_figure(thetime[2]//10,x-23,y,color) #set_figure(thetime[2]-10*(thetime[2]//10),x-27,y,color)
添加的擦除函数:
def smooth_clear(later,x,y): if later==0: cl = [(1,2)] for i in cl : set_pixel(i[0]+x,i[1]+y,(0,0,0)) np.write() elif later==1: cl = [(2,0), (0,0), (0,1), (0,2), (0,3), (2,3), (2,2)] for i in cl : set_pixel(i[0]+x,i[1]+y,(0,0,0)) np.write() elif later==2: cl = [(2,1), (1,1), (1,3)] for i in cl : set_pixel(i[0]+x,i[1]+y,(0,0,0)) np.write() elif later==3: cl = [(2,3)] for i in cl : set_pixel(i[0]+x,i[1]+y,(0,0,0)) np.write() elif later==4: cl = [(1,0),(2,4), (1,4)] for i in cl : set_pixel(i[0]+x,i[1]+y,(0,0,0)) np.write() elif later==5: cl = [(0,1)] for i in cl : set_pixel(i[0]+x,i[1]+y,(0,0,0)) np.write() elif later==6: pass elif later==7: cl = [(1,2), (2,1), (2,2), (2,3), (2,4), (1,4)] for i in cl : set_pixel(i[0]+x,i[1]+y,(0,0,0)) np.write() elif later==8: pass elif later==9: cl = [(2,3)] for i in cl : set_pixel(i[0]+x,i[1]+y,(0,0,0)) np.write()
是不是非常麻烦,等我写完静下来想了一会之后,突然发现这些根本没必要,因为neopixel是先设置颜色信息,再用np.write()来发送数据,所以我只需要将擦除数字和设置数字全部放在np.write()前就可以了,于是我删除了这一大堆代码,复原函数之后注释掉了坐标转换函数中的np.write()(白忙活了老长时间)
9.日期显示和颜色渐变
丰富一下时钟,我又添加了日期显示功能,不过是通过像素形状和数目来实现的
实现函数:
def print_day(): day = rtc.datetime()[1:4] for i in range(day[1]): set_pixel(31-i//8,i-(i//8)*8,get_color(2)) #clear week for i in range(2): set_pixel(26-i,6,get_color(3)) for i in range(2): set_pixel(22-i,6,get_color(3)) for i in range(2): set_pixel(18-i,6,get_color(3)) for i in range(2): set_pixel(14-i,6,get_color(3)) for i in range(2): set_pixel(10-i,6,get_color(3)) for i in range(2): set_pixel(6-i,6,get_color(3)) for i in range(2): set_pixel(2-i,6,get_color(3)) #set week for i in range(2): set_pixel(26-i-day[2]*4,6,(20,0,0)) #clear month for i in range(12): set_pixel(20-i,7,get_color(4)) #set month set_pixel(21-day[0],7,(15,13,13)) np.write()
这里的get_color()函数是我写的渐变色函数,它会随着日期或者时间逐渐变化颜色(如正午是偏红色,深夜是深蓝色)
def get_color(mod): day = rtc.datetime()[1:4] thetime = rtc.datetime()[4:7] now = thetime[0]*60+thetime[1] if mod==1:#realtime if now>720: i = now-720 return (30-int(30*(i/720)),int(10-8*(i/720)),int(15*(i/720))) else: i = 720-now return (30-int(30*(i/720)),int(10-8*(i/720)),int(15*(i/720))) elif mod==2:#mday return (10,2,10) elif mod==3:#week return (day[2]*2,12-day[2]*2,0) elif mod==4:#month return (0,day[0],12-day[0])
10.最终程序
from machine import Pin from ds1302 import DS1302 from machine import RTC import machine, neopixel, time, random, _thread, network, ntptime wlan = network.WLAN(network.STA_IF) n = 32*8 p = 13 printtime = [10,10,10,10,10,10] ds = DS1302(clk = Pin(25), dio = Pin(26), cs = Pin(27)) np = neopixel.NeoPixel(machine.Pin(p), n) rtc = RTC() def set_pixel(x,y,color): if x<0 or x>=32 or y<0 or y>=8: raise ValueError("x,y are out of range") index = x*8+y np[index] = color #np.write() def set_figure(f,x,y,color): ze = get_xy(3)+get_xy(1)+get_xy(2)+get_xy(5)+get_xy(6)+get_xy(7) on = [(1,0), (1,1), (2,1), (1,2), (1,3), (1,4),(2,4),(0,4)] tw = get_xy(3)+get_xy(6)+get_xy(4)+get_xy(2)+get_xy(5) th = get_xy(3)+get_xy(6)+get_xy(4)+get_xy(7)+get_xy(5) fo = get_xy(1)+get_xy(4)+get_xy(6)+get_xy(7) fi = get_xy(3)+get_xy(1)+get_xy(4)+get_xy(7)+get_xy(5) si = get_xy(3)+get_xy(1)+get_xy(4)+get_xy(2)+get_xy(7)+get_xy(5) se = get_xy(3)+get_xy(6)+get_xy(7) ei = get_xy(3)+get_xy(1)+get_xy(2)+get_xy(4)+get_xy(5)+get_xy(6)+get_xy(7) ni = get_xy(3)+get_xy(1)+get_xy(6)+get_xy(4)+get_xy(7)+get_xy(5) for i in ei : set_pixel(i[0]+x,i[1]+y,(0,0,0)) for i in on : set_pixel(i[0]+x,i[1]+y,(0,0,0)) if f==0: for i in ze : set_pixel(i[0]+x,i[1]+y,color) elif f==1: for i in on : set_pixel(i[0]+x,i[1]+y,color) elif f==2: for i in tw : set_pixel(i[0]+x,i[1]+y,color) elif f==3: for i in th : set_pixel(i[0]+x,i[1]+y,color) elif f==4: for i in fo : set_pixel(i[0]+x,i[1]+y,color) elif f==5: for i in fi : set_pixel(i[0]+x,i[1]+y,color) elif f==6: for i in si : set_pixel(i[0]+x,i[1]+y,color) elif f==7: for i in se : set_pixel(i[0]+x,i[1]+y,color) elif f==8: for i in ei : set_pixel(i[0]+x,i[1]+y,color) elif f==9: for i in ni : set_pixel(i[0]+x,i[1]+y,color) def get_xy(i): if i>7 or i<1: raise ValueError("i is out of range") if i==1: return [(2,0),(2,1),(2,2)] elif i==2: return [(2,2),(2,3),(2,4)] elif i==3: return [(2,0),(1,0),(0,0)] elif i==4: return [(2,2),(1,2),(0,2)] elif i==5: return [(2,4),(1,4),(0,4)] elif i==6: return [(0,0),(0,1),(0,2)] elif i==7: return [(0,2),(0,3),(0,4)] def set_time(): if wlan.isconnected(): try: ntptime.host = 'ntp1.aliyun.com' ntptime.settime() rtc.datetime(rtc.datetime()[0:4]+(int(rtc.datetime()[4])+8,)+rtc.datetime()[5:8]) ds.date_time(rtc.datetime()[0:7]) except: rtc.datetime(ds.date_time()+[0]) else: rtc.datetime(ds.date_time()+[0]) def print_time(x,y,color): global printtime thetime = rtc.datetime()[4:7] set_figure(thetime[0]//10,x-3,y,color) set_figure(thetime[0]-10*(thetime[0]//10),x-7,y,color) set_figure(thetime[1]//10,x-13,y,color) set_figure(thetime[1]-10*(thetime[1]//10),x-17,y,color) set_figure(thetime[2]//10,x-23,y,color) set_figure(thetime[2]-10*(thetime[2]//10),x-27,y,color) #left : set_pixel(19,2,color) set_pixel(19,4,color) #right : set_pixel(9,2,color) set_pixel(9,4,color) np.write() def get_color(mod): day = rtc.datetime()[1:4] thetime = rtc.datetime()[4:7] now = thetime[0]*60+thetime[1] if mod==1:#realtime if now>720: i = now-720 return (30-int(30*(i/720)),int(10-8*(i/720)),int(15*(i/720))) else: i = 720-now return (30-int(30*(i/720)),int(10-8*(i/720)),int(15*(i/720))) elif mod==2:#mday return (10,2,10) elif mod==3:#week return (day[2]*2,12-day[2]*2,0) elif mod==4:#month return (0,day[0],12-day[0]) def print_day(): day = rtc.datetime()[1:4] for i in range(day[1]): set_pixel(31-i//8,i-(i//8)*8,get_color(2)) #clear week for i in range(2): set_pixel(26-i,6,get_color(3)) for i in range(2): set_pixel(22-i,6,get_color(3)) for i in range(2): set_pixel(18-i,6,get_color(3)) for i in range(2): set_pixel(14-i,6,get_color(3)) for i in range(2): set_pixel(10-i,6,get_color(3)) for i in range(2): set_pixel(6-i,6,get_color(3)) for i in range(2): set_pixel(2-i,6,get_color(3)) #set week for i in range(2): set_pixel(26-i-day[2]*4,6,(20,0,0)) #clear month for i in range(12): set_pixel(20-i,7,get_color(4)) #set month set_pixel(21-day[0],7,(15,13,13)) np.write() set_time() t=0 while(True): if t%180000 == 0: set_time() t = 0 print_time(28,1,get_color(1)) print_day() time.sleep(0.1) t += 1
四、效果与后记
效果图
我觉得贴上一层纸会更好(因为没粘上所以有的地方纸翘起来导致有点糊)
后记
由于学业原因,暂时只做了这个简陋的像素时钟初版,还有许多想加的功能没有添加 ,如翻页显示功能,温湿度显示功能等等,等学业不紧张后,后面的版本会一一尝试实现。
感谢你的阅读