2017年7月13日星期四

基于ATtiny85轻松制作一款智能手表

这是基于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进行访问:
  1. static union {
  2.   uint8_t DataBytes[5];
  3.   struct {
  4.     uint8_t control;
  5.     long seconds;
  6.   } rtc;
  7. };
复制代码

显示
显示采用的是OLED。时钟表面是使用图形命令绘制线,并且这些都编辑一个缓冲区,该缓冲器为显示器上的每个像素存储一个位。这定义如下:
  1. // Screen buffer
  2. const int Buffersize = 64*6;
  3. unsigned char Buffer[Buffersize];
复制代码

一旦将时钟面绘制到缓冲区中,DisplayBuffer()函数将字节复制到显示器中即可显示缓冲区的内容:
  1. void DisplayBuffer () {
  2.   PINB = 1<<cs; // cs low
  3.   // Set column address range
  4.   Command(0x21); Command(32); Command(95);
  5.   // Set page address range
  6.   Command(0x22); Command(2); Command(7); 
  7.   for (int i = 0 ; i < Buffersize; i++) Data(Buffer[i]);
  8.   PINB = 1<<cs; // cs high
  9. }
复制代码

SSD1306可以处理高达128x64的显示,64x48显示位于该区域的中心,因此要解决此问题,您需要选择第32至95列(含)和第2至7页(含)。

画时钟
DrawClock()函数绘制时钟面和表针位置指定的时间,以小时、分钟和秒为单位。使用了’几个技巧来避免需要三角函数,并最小化所需的乘法和除法次数。该程序基本上执行以下迭代程序360次以生成圆上的点:
  1. x = x + Delta * y;
  2. y = y-Delta * x;
复制代码

其中Delta为弧度1度。使用定点运算通过存储它们乘以因子2 ^ 9来计算x和y的值
  1. void DrawClock (int hour, int minute, int second) {
  2.   int x = 0; int y = 23<<9;
  3.   for (int i=0; i<360; i++) {
  4.     int x9 = x>>9; int y9 = y>>9;
  5.     DrawTo(x9, y9);
  6.     // Hour marks
  7.     if (i%30 == 0) {
  8.       MoveTo(x9 - (x9>>3), y9 - (y9>>3));
  9.       DrawTo(x9, y9);
  10.     }
  11.     // Hour hand
  12.     if (i == hour * 30 + (minute>>1))
  13.       DrawHand(x9 - (x9>>2), y9 - (y9>>2));
  14.     // Minute hand
  15.     if (i == minute * 6 + second/10) DrawHand(x9, y9);
  16.     // Second hand
  17.     if (i == second * 6) {
  18.       MoveTo(0, 0);
  19.       DrawTo(x9, y9);
  20.     }
  21.     // Border of clock
  22.     MoveTo(x9, y9);
  23.     // if (x9 > 0) DrawTo(23, y9); else DrawTo (-23, y9);
  24.     x = x + (y9 * Delta);
  25.     y = y - ((x>>9) * Delta);
  26.   }
  27. }
复制代码

它调用DrawHand()将绘制菱形小时和分针从0、0绘制到x,y:
  1. void DrawHand (int x, int y) {
  2.    int v = x/2; int u = y/2;
  3.    int w = v/5; int t = u/5;
  4.    MoveTo(0, 0);
  5.    DrawTo(v-t, u+w);
  6.    DrawTo(x, y);
  7.    DrawTo(v+t, u-w);
  8.    DrawTo(0, 0);
  9. }
复制代码

线绘制由DrawTo()线绘图程序执行,它使用Bresenham线算法在两点之间画出最佳线,而不需要任何分割或乘法:
  1. void DrawTo (int x1, int y1) {
  2.   int sx, sy, e2, err;
  3.   int dx = abs(x1 - x0);
  4.   int dy = abs(y1 - y0);
  5.   if (x0 < x1) sx = 1; else sx = -1;
  6.   if (y0 < y1) sy = 1; else sy = -1;
  7.   err = dx - dy;
  8.   for (;;) {
  9.     PlotPoint(x0, y0);
  10.     if (x0==x1 && y0==y1) return;
  11.     e2 = err<<1;
  12.     if (e2 > -dy) {
  13.       err = err - dy;
  14.       x0 = x0 + sx;
  15.     }
  16.     if (e2 < dx) {
  17.       err = err + dx;
  18.       y0 = y0 + sy;
  19.     }
  20.   }
  21. }
复制代码

设定时间
当您首次向手表供电时,它将检查MCUSR寄存器以检测上电复位,并运行SetTime(),以允许您将时间设置为最接近的秒数。它的工作原理如下:

等待当前时间是一分钟,然后插入电池。手表将从12:00开始,一次逐步显示一分钟。当手表显示插入电池的时间(而不是当前时间)时,按复位按钮。手表将占用您设置时间的额外秒数,并显示当前时间。它现在可以使用了。

SetTime()例程将变量secs递增300,相当于五分钟,显示步骤之间的秒数。在每个步骤中,它将值的值写入RTC,其中添加了一个偏移量,用于计算自启动过程起经过的时间。
  1. void SetTime () {
  2.   unsigned long Offset = millis();
  3.   unsigned long secs = 0;
  4.   for (;;) {
  5.     int Mins = (unsigned long)(secs / 60) % 60;
  6.     int Hours = (unsigned long)(secs / 3600) % 12;
  7.     // Write time to RTC
  8.     rtc.control = 0x0C;
  9.     rtc.seconds = secs + ((millis()-Offset)/1000);
  10.     OneWireReset();
  11.     OneWireWrite(SkipROM);
  12.     OneWireWrite(WriteClock);
  13.     OneWireWriteBytes(5);
  14.     ClearBuffer();
  15.     DrawClock(Hours, Mins, -1);
  16.     DisplayBuffer();
  17.     unsigned long Start = millis();
  18.     while (millis()-Start < 500);
  19.     secs = secs + 60;
  20.   }
  21. }
复制代码


显示时间
ATtiny85通常处于睡眠模式,可忽略不计的电流。按钮连接到复位输入端口,该引脚唤醒处理器并产生复位。

主程序loop()首先读取RTC的时间到变量secs:
  1.   OneWireReset();
  2.   OneWireWrite(SkipROM);
  3.   OneWireWrite(ReadClock);
  4.   OneWireReadBytes(5);
  5.   OneWireReset();
  6.   secs = rtc.seconds;
复制代码
然后计算变量Hours、MinsSecs的值,并显示一个动画时钟,直到SleepTime过去(30秒):
  1. int Mins = (unsigned long)(secs / 60) % 60;
  2.   int Hours = (unsigned long)(secs / 3600) % 12;
  3.   int Secs = secs % 60;
  4.   unsigned long Start = millis();
  5.   unsigned long Now = Start;
  6.   while (Now-Start < SleepTime) {
  7.     ClearBuffer();
  8.     DrawClock(Hours, Mins, (Secs + (Now-Start)/1000) % 60);
  9.     DisplayBuffer();
  10.     Now = millis();
  11.   }
  12.   DisplayOff();
  13.   digitalWrite(dc,HIGH);
  14.   digitalWrite(clk,HIGH);
  15.   digitalWrite(data,HIGH);
  16.   digitalWrite(cs,HIGH);
  17.   pinMode(OneWirePin, OUTPUT); digitalWrite(OneWirePin,HIGH);
  18.   sleep_enable();
  19.   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设置合适的保险丝位,然后上传程序。

这是整个Tiny Face Watch程序:Tiny Face Watch程序

或者,在GitHub上,将它连接到PCB上的Eagle文件:GitHub上的Tiny Face Watch

参考链接:

没有评论:

发表评论