这是基于ATtiny85系列的简约手表系列中的第三款。该款手表通过在微型64x48 OLED显示屏上绘制模拟的手表来显示时间。它使用独立的晶振控制的低功耗RTC芯片来保持每月几秒钟的时间,并在不显示时间的时候将处理器和显示器置于睡眠状态,以便使得使用寿命超过一年。
当按下手表表面上的按钮时会显示时间,并且会在显示屏上显示模拟的手表,并且带有一个移动的秒针。 30秒后显示屏将自动变暗,以保持电池的使用寿命。
简介
这款手表基于Maxim Integrated的DS2417 RTC芯片,该芯片采用一个小型6脚封装,使用32.768 kHz晶振来保持精确的时间。它可以通过1线接口与主控制器ATtiny85进行通信,该接口仅使用一个I/O引脚来发送和接收数据。因为RTC芯片主要工作是计时,ATtiny85在其不需要实际显示时间时可以在掉电模式下保持睡眠,从而大大降低了功耗。
该显示器使用一个SPI接口的小型单色64x48 OLED显示屏,具有可从Aliexpress购买,或可从Sparkfun购买类似的。显示器需要4个引脚驱动,ATtiny85刚好满足应用要求,剩余一个引脚用于1-Wire接口。您不能读取显示内存,因此要做图形,您需要写入RAM中的缓冲区,然后将其复制到显示器上。因为显示器是64x48像素,因此它需要68x48 / 8或384字节的存储器用于图形缓冲区,这也刚好在ATtiny85的能力之内。
现在显示屏变暗的总功耗约为8μA,单个CR2016电池估计超过一年的电池寿命。
电路
以下是Tiny Face手表的电路:
晶振是标准的32.768 kHz石英表晶体; Maxell指定6pf负载电容,我选择了一个MS1V-T1K晶振,其优点是可以焊接到板上,但我预计任何手表晶振都是可以的。
按钮采用的是广泛使用的微型SMD触觉开关,通常用作处理器电路板上的复位按钮。 使用了一个33kΩ电阻和0.1μF电容,确保在首次施加电源时显示屏能正常复位。
电池是一个20毫米的纽扣电池。鉴于电流消耗低,我决定使用更细小的CR2016电池,并在Mouser找到了合适的SMD电池座。或者,您可以使用CR2032电池,可以从Sparkfun购买SMD 20mm电池座。
表带需要是12mm的螺纹穿透型,我发现了一个德国的供应商。我也尝试过英国的供应商这种比较便宜的替代品,但是请注意这个会更短,如果你手腕比较小,这个表带可能不适合。
装配
对于这个项目,我使用了Seeed Studio的PCB服务,并选择蓝色PCB来匹配显示。这是布局:
手表使用SMD组件,所有组件与电池座分开焊接到电路板前面。我使用SOIC ATtiny85和0805电阻和电容,所以他们应该比较容易用手焊接。 DS2417 RTC芯片采用TSOC封装,可能是最难焊接的元件,因为它的引脚位于封装下面。
我在250°C使用Youyue 858D +热风枪将SMD组件焊接到电路板的前面,然后使用传统的烙铁将电池座焊接到电路板背面。如果没有热风枪,您可以使用细小的烙铁焊接SMD组件。
以下照片显示了安装显示屏前电路板的前面,安装显示屏的电路板和电路板的背面:
我建议在将显示器安装在电路板上之前先测试电路板。您可以通过显示器的7针连接器访问除RST之外的ISP编程所需的所有ATtiny85引脚。
程序
本节将介绍Tiny Face Watch程序的各个部分。
1-Wire接口
为了与RTC进行通信,我使用了简单的1线接口。它将RTC中的五个字节数据读入数组DataBytes [5]。第一个字节是一个配置字节,最后四个字节给出一个长整数的秒数;为了更容易理解,我定义了一个联合体,所以时间字节可以使用类似rtc.seconds进行访问:
- static union {
- uint8_t DataBytes[5];
- struct {
- uint8_t control;
- long seconds;
- } rtc;
- };
复制代码
显示
显示采用的是OLED。时钟表面是使用图形命令绘制线,并且这些都编辑一个缓冲区,该缓冲器为显示器上的每个像素存储一个位。这定义如下:
- // Screen buffer
- const int Buffersize = 64*6;
- unsigned char Buffer[Buffersize];
复制代码
一旦将时钟面绘制到缓冲区中,DisplayBuffer()函数将字节复制到显示器中即可显示缓冲区的内容:
- void DisplayBuffer () {
- PINB = 1<<cs; // cs low
- // Set column address range
- Command(0x21); Command(32); Command(95);
- // Set page address range
- Command(0x22); Command(2); Command(7);
- for (int i = 0 ; i < Buffersize; i++) Data(Buffer[i]);
- PINB = 1<<cs; // cs high
- }
复制代码
SSD1306可以处理高达128x64的显示,64x48显示位于该区域的中心,因此要解决此问题,您需要选择第32至95列(含)和第2至7页(含)。
画时钟
DrawClock()函数绘制时钟面和表针位置指定的时间,以小时、分钟和秒为单位。使用了’几个技巧来避免需要三角函数,并最小化所需的乘法和除法次数。该程序基本上执行以下迭代程序360次以生成圆上的点:
- x = x + Delta * y;
- y = y-Delta * x;
复制代码
其中Delta为弧度1度。使用定点运算通过存储它们乘以因子2 ^ 9来计算x和y的值
- void DrawClock (int hour, int minute, int second) {
- int x = 0; int y = 23<<9;
- for (int i=0; i<360; i++) {
- int x9 = x>>9; int y9 = y>>9;
- DrawTo(x9, y9);
- // Hour marks
- if (i%30 == 0) {
- MoveTo(x9 - (x9>>3), y9 - (y9>>3));
- DrawTo(x9, y9);
- }
- // Hour hand
- if (i == hour * 30 + (minute>>1))
- DrawHand(x9 - (x9>>2), y9 - (y9>>2));
- // Minute hand
- if (i == minute * 6 + second/10) DrawHand(x9, y9);
- // Second hand
- if (i == second * 6) {
- MoveTo(0, 0);
- DrawTo(x9, y9);
- }
- // Border of clock
- MoveTo(x9, y9);
- // if (x9 > 0) DrawTo(23, y9); else DrawTo (-23, y9);
- x = x + (y9 * Delta);
- y = y - ((x>>9) * Delta);
- }
- }
复制代码
它调用DrawHand()将绘制菱形小时和分针从0、0绘制到x,y:
- void DrawHand (int x, int y) {
- int v = x/2; int u = y/2;
- int w = v/5; int t = u/5;
- MoveTo(0, 0);
- DrawTo(v-t, u+w);
- DrawTo(x, y);
- DrawTo(v+t, u-w);
- DrawTo(0, 0);
- }
复制代码
线绘制由DrawTo()线绘图程序执行,它使用Bresenham线算法在两点之间画出最佳线,而不需要任何分割或乘法:
- void DrawTo (int x1, int y1) {
- int sx, sy, e2, err;
- int dx = abs(x1 - x0);
- int dy = abs(y1 - y0);
- if (x0 < x1) sx = 1; else sx = -1;
- if (y0 < y1) sy = 1; else sy = -1;
- err = dx - dy;
- for (;;) {
- PlotPoint(x0, y0);
- if (x0==x1 && y0==y1) return;
- e2 = err<<1;
- if (e2 > -dy) {
- err = err - dy;
- x0 = x0 + sx;
- }
- if (e2 < dx) {
- err = err + dx;
- y0 = y0 + sy;
- }
- }
- }
复制代码
设定时间
当您首次向手表供电时,它将检查MCUSR寄存器以检测上电复位,并运行SetTime(),以允许您将时间设置为最接近的秒数。它的工作原理如下:
等待当前时间是一分钟,然后插入电池。手表将从12:00开始,一次逐步显示一分钟。当手表显示插入电池的时间(而不是当前时间)时,按复位按钮。手表将占用您设置时间的额外秒数,并显示当前时间。它现在可以使用了。
SetTime()例程将变量secs递增300,相当于五分钟,显示步骤之间的秒数。在每个步骤中,它将值的值写入RTC,其中添加了一个偏移量,用于计算自启动过程起经过的时间。
- void SetTime () {
- unsigned long Offset = millis();
- unsigned long secs = 0;
- for (;;) {
- int Mins = (unsigned long)(secs / 60) % 60;
- int Hours = (unsigned long)(secs / 3600) % 12;
- // Write time to RTC
- rtc.control = 0x0C;
- rtc.seconds = secs + ((millis()-Offset)/1000);
- OneWireReset();
- OneWireWrite(SkipROM);
- OneWireWrite(WriteClock);
- OneWireWriteBytes(5);
- ClearBuffer();
- DrawClock(Hours, Mins, -1);
- DisplayBuffer();
- unsigned long Start = millis();
- while (millis()-Start < 500);
- secs = secs + 60;
- }
- }
复制代码
显示时间
ATtiny85通常处于睡眠模式,可忽略不计的电流。按钮连接到复位输入端口,该引脚唤醒处理器并产生复位。
主程序loop()首先读取RTC的时间到变量secs:
- OneWireReset();
- OneWireWrite(SkipROM);
- OneWireWrite(ReadClock);
- OneWireReadBytes(5);
- OneWireReset();
- secs = rtc.seconds;
复制代码
然后计算变量Hours、Mins和Secs的值,并显示一个动画时钟,直到SleepTime过去(30秒):
- int Mins = (unsigned long)(secs / 60) % 60;
- int Hours = (unsigned long)(secs / 3600) % 12;
- int Secs = secs % 60;
- unsigned long Start = millis();
- unsigned long Now = Start;
- while (Now-Start < SleepTime) {
- ClearBuffer();
- DrawClock(Hours, Mins, (Secs + (Now-Start)/1000) % 60);
- DisplayBuffer();
- Now = millis();
- }
- DisplayOff();
- digitalWrite(dc,HIGH);
- digitalWrite(clk,HIGH);
- digitalWrite(data,HIGH);
- digitalWrite(cs,HIGH);
- pinMode(OneWirePin, OUTPUT); digitalWrite(OneWirePin,HIGH);
- sleep_enable();
- sleep_cpu();
复制代码
然后熄灭显示屏,并将处理器设置成睡眠状态以最小化功耗。
其他选项
时钟面只占据显示屏中间的48x48像素,因此在显示屏的每一侧有两个8x48像素的区域,可用于显示其他信息。例如,您可以使用ATtiny85上的温度传感器来测量环境温度,并以图形显示方式将其显示在显示屏的一边。
编译程序
我使用Spence Konde的ATTiny Core编译了这个程序。在Boards菜单的ATtiny Universal标题下选择ATtinyx5 series选项。然后在子菜单下选择Timer 1 Clock: CPU, B.O.D. Disabled, ATtiny85, 8 MHz (internal)。
我使用Sparkfun的Tiny AVR编程板编程ATtiny85。选择Burn Bootloader设置合适的保险丝位,然后上传程序。
参考链接:
|
没有评论:
发表评论