ゆるく楽しむプログラミング

ATTiny85で2進化10進数表示の時計を作ってみる

時計の各桁を2進数で表示する時計を作ります。
プログラムに慣れている人ならピンとくる?
例えば13:59ならこういう表示になります。
 
    +8
  +4
+2
+1

作り付けにするので(貧乏性なので)コストダウンのためにATTiny85(130円)を使います。
※ATTiny85のArduino化は『ArduinoISP Writer』を参照

時計を実現するためには最低で13個のLEDを操作しなければならないので普通にやったらArduinoUNOでも出力ピンが足りません。
ましてやATTiny85は8ピン(電源/GND/Resetを除けば5ピン)です。
なので少ない信号線で複数の出力を操作できるシフトレジスタ(シリアル->パラレル変換IC)を使います。
※シフトレジスタについては『シフトレジスタ(U74HL595)の使い方』を参照

回路の製作

検証時にはLEDを16個も並べるの面倒なので4+4の8個に省略
回路図は全部描くと線が入り乱れて分かりにくくなるのでI2C(RTC:時計:とOLED:でバック情報表示用:)/スイッチ/シフトレジスタに分割して説明します。

前回と同じく RTCはDS1306、OLEDはSSD1307を使用しています。



I2C(RTCとOLED)

RTC(リアルタイムクロック)とOLED(有機液晶)をI2Cで接続します。
※各項目については前回の記事を参照『I2C』『OLED』『RTC




シフトレジスタ

LEDは16個付けるの大変なので省略しています。
1段目のQH′を2段目のSERに接続することで連動して16ビットのシフトレジスタとして動作します。

1段目のQH(80分とかはありえないので)、2段目のQG(40時とかありえないので)付けなくても問題ありません。
2段目のQHも時刻表示には必要ないですが、ステータス表示のために付けておいた方が便利です。
ディスプレイとして分秒表示にするのも楽しそうなので、分秒表示の可能性があるなら2段目のQGも付けておいた方が良いです。





ボタン

抵抗値は何でもか構わないのですが、ATTiny85で余っているピンが1番ピンしかないので、抵抗値が小さすぎるとリセットが掛かってしまします。大きすぎるとボタンの判別が付かなくなりますので適度な値になるように調整します。
現物合わせでソフトウエアで辻褄を合わせれば良いと思います、その(入力値を確認する)ためにOLEDを付けたので。

電源の違いか抵抗を付け間違えたのか、ブレッドボードで実験した時とユニバーサル基板で組んだ時とで入力値が変わってしまったので、認識範囲には余裕を持たせた方が良いと思う。
デバック用にOLEDを付けていたので実測値を確認できましたが、デバック手段がないとなぜボタンを認識しないのか原因がつかめないのではないかと思う。


ご利用は計画的に

16個のLEDを繋ぐとなると配線が結構大変な事になります。
配置・配線をよく考えて接続を確認しながら組まないと、思わぬところが断線していたりショートしていたりで動かない原因調査に手こずります。
※考えなしに適当に配線し始めたら大変な事になってしまいました。 (´・ω・`)



スケッチ

-- BinaryClock.ino --


#if defined(__AVR_ATtiny85__)
#include <TinyWireM.h>
#include <TinyOzOLED2.h>   //  ※デバック用 OLEDが無ければ必要ない
#else 
#include <Wire.h>
#include <OzOLED.h>      //  ※デバック用 OLEDが無ければ必要ない
#endif

#define DATAPIN   (1)  // 74HC595のRESへ
#define LATCHPIN  (3)  // 74HC595のRCLKへ
#define CLOCKPIN  (4)  // 74HC595のSRCLKへ
#define BTN_PIN    A0

// RTC デバイスアドレス(スレーブ)
#define RTC_ADDRESS 0x68  // 2進数 1101000

// 表示用Buffer
char dispStr[20] ;  //  ※デバック用 OLEDが無ければ必要ない

// 状態変位用変数
uint8_t sec ;     // 秒
uint8_t mnt ;     // 分
uint8_t how ;     // 時
uint8_t preTime ;
uint8_t mode ;    // 0:時刻表示 1:時刻設定

unsigned char btn ; // ボタンが押された 1-4

// 時刻設定
void SetTime(int h,int m) {
#if defined(__AVR_ATtiny85__)
  TinyWireM.beginTransmission(RTC_ADDRESS);
  TinyWireM.write(0x00);
  TinyWireM.write(0x0);
  TinyWireM.write(m);
  TinyWireM.write(h);
  TinyWireM.write(0x01);
  TinyWireM.write(0x31);
  TinyWireM.write(0x12);
  TinyWireM.write(0x21);
  TinyWireM.endTransmission();
#else 
  // I2Cスレーブに対して送信処理を開始する
  Wire.beginTransmission(RTC_ADDRESS);
  // レジスタのアドレスを指定する(0-63の内、先頭から)
  Wire.write(0x00);
  
  // ---------------------------------
  //  タイマーキーパーレジスタ
  // ---------------------------------
  // 2進化10進数(BCD)で指定します。(03以外)
  
  // [00]Seconds(00秒)
  Wire.write(0x0);
  // [01]Minutes(m分)
  Wire.write(m);
  // [02]Hours(h時)
  Wire.write(h);

  // [03]Day(月)
  // 1:日 2:月 3:火 4:水 5:木 6:金 7:土
  Wire.write(0x01);
  // [04]Date(31日)
  Wire.write(0x31);
  // [05]Month(12月)
  Wire.write(0x12);
  // [06]Years(2021年)
  Wire.write(0x21);
 
  // I2Cスレーブへの送信完了
  Wire.endTransmission();
#endif
}

void ShiftOut(unsigned int val )
{
  digitalWrite(LATCHPIN, LOW);    // 送信中はLATCHPINをLOWに

  unsigned int dit = 0x8000 ;
  for( int i = 0; i < 16; i++ )
  {
    digitalWrite(DATAPIN, !!(val & dit));
    digitalWrite(CLOCKPIN, HIGH);
    digitalWrite(CLOCKPIN, LOW);
    dit >>= 1 ;
  }

  digitalWrite(LATCHPIN, HIGH);   // 送信後はLATCHPINをHIGHに戻す
}

// 時刻取得
// 4桁の10進数 hhmm
void GetTime( ) {
  // RTCのレジスタテーブル(8byte)
  int RegTbl[8];

#if defined(__AVR_ATtiny85__)
  TinyWireM.beginTransmission(RTC_ADDRESS);
  TinyWireM.write(0x00);
  TinyWireM.endTransmission();
  TinyWireM.requestFrom(RTC_ADDRESS, 8);
  int i;
  for (i = 0; i < 8; i++) {
    while (TinyWireM.available() == 0 ) {}
    RegTbl[i] = TinyWireM.read();
  }
#else
  // レジスタのアドレスを先頭にする
  Wire.beginTransmission(RTC_ADDRESS);
  Wire.write(0x00);
  Wire.endTransmission();
 
  // I2Cスレーブに8byteのレジスタデータを要求する
  Wire.requestFrom(RTC_ADDRESS, 8);
 
  // 8byteのデータを取得する
  int i;
  for (i = 0; i < 8; i++) {
    while (Wire.available() == 0 ) {}
    RegTbl[i] = Wire.read();
  }
 #endif

  // 現在日時 : グローバル変数にセット
  how = RegTbl[2] & 0x3F ;
  mnt = RegTbl[1] & 0x7F ;
  sec = RegTbl[0] & 0x7F ;
}

// ボタンの状態をチェック
void checkButton( ) {
  int buttonStat ; // ボタンの状態
  int data ;
  buttonStat = analogRead(BTN_PIN) ;

  if (btn != 0) {
    btn = 0 ; 
    if (buttonStat >= 200) return ;
  }
  if (buttonStat <= 470) {
    btn = 4 ;
    SetTime(0,0) ;
    DispTime( ) ;
  }
  if (480 <= buttonStat && buttonStat <= 570 && btn == 0) {
    btn = 1 ;
    data = (how /16) * 10 + how % 16  + 1 ;
    data %= 24 ;
    how = (data /10) * 16 + data % 10 ;
    DispTime( ) ;
  }
  if (600 <= buttonStat && buttonStat <= 750 && btn == 0) {
    btn = 2 ; 
    data = (mnt /16) * 10 + mnt % 16  + 1 ;
    data %= 60 ;
    mnt = (data /10) * 16 + data % 10 ;
    DispTime( ) ;
  }
  if (760 <= buttonStat && buttonStat <= 800 && btn == 0) {
    btn = 3 ;
    if (mode == 0) {
      mode = 1;  
    } else {
      mode = 0;
      SetTime(how,mnt) ;
    }
    DispTime( ) ;
  }

  // --- DEBUG - ボタン状態表示 ---  //  ※デバック用 OLEDが無ければ必要ない
  sprintf(dispStr,"%0d-%d-%04d", btn , mode , buttonStat) ;
  OzOled.setCursorXY(1,7);
  OzOled.printString(dispStr);
  // ----------------------------
}

void DispTime( )
{
  unsigned short data ;
  // 時刻文字列   //  ※デバック用 OLEDが無ければ必要ない
  sprintf(dispStr,"%02x:%02x:%02x",how , mnt,sec) ;
  OzOled.setCursorXY(1,1);
  OzOled.printString(dispStr);

  // --- シフトレジスタ出力 ---
  data = (how << 8 ) | mnt ;
  if (mode == 1) {
    data |= 0x8000 ;
  }
  ShiftOut(data);
}

void setup()
{
  pinMode(DATAPIN, OUTPUT);
  pinMode(LATCHPIN, OUTPUT);
  pinMode(CLOCKPIN, OUTPUT);

  // ディスプレイ初期化  //  ※デバック用 OLEDが無ければ必要ない
  OzOled.init();
  OzOled.clearDisplay();
  GetTime( );
  preTime = sec ;
  DispTime( ) ;
}

void loop()
{
  checkButton( ) ;
  if (mode == 0) {
    GetTime( );
    if (preTime != sec) {
      DispTime( ) ;
      preTime = sec ;
    }
  } else {
      DispTime( ) ;
  }
  delay(100);
}

カバーを付けて出来上がり

最初にカバーありきでカバーに合わせて配置を決めたので配線が苦しい事になってます。
アイスクリーム自動販売機型2進化10進数表示時計の完成  (*´ω`*)