8️⃣

8桁7セグメントLEDの使い方

2023/05/12に公開

用語

用語 意味
MAX7219 8桁7セグメントLEDを扱いやすくしてくれる裏にいるチップ
DP Decimal Point の略で右下のドットのこと

データシート

https://akizukidenshi.com/download/ds/maxim/max7219_21.pdf

MAX7219 は8x8マトリックスLEDで使ったような?

  • 8x8マトリックスのLEDの数は64個
  • 8桁7セグメントもDPを含めると8個のLEDが8桁で64個
  • つまり MAX7219 は用途に関係なく64個のLEDをまとめて扱うのに特化している
  • とはいえ7セグメントLED用の内蔵フォントを持っていたりする

動作確認

#include <SPI.h>

#define COLUMN_BEGIN 1
#define COLUMN_END   9
#define COLUMN_COUNT COLUMN_END - COLUMN_BEGIN

#define PHYSICAL_COLUMN_1 COLUMN_BEGIN + 7
#define PHYSICAL_COLUMN_2 COLUMN_BEGIN + 6
#define PHYSICAL_COLUMN_3 COLUMN_BEGIN + 5
#define PHYSICAL_COLUMN_4 COLUMN_BEGIN + 4
#define PHYSICAL_COLUMN_5 COLUMN_BEGIN + 3
#define PHYSICAL_COLUMN_6 COLUMN_BEGIN + 2
#define PHYSICAL_COLUMN_7 COLUMN_BEGIN + 1
#define PHYSICAL_COLUMN_8 COLUMN_BEGIN + 0

#define ADDR_NOOP          0
#define ADDR_DECODE_MODE   9
#define ADDR_BRIGHTNESS   10
#define ADDR_SCAN_LIMIT   11
#define ADDR_SHUTDOWN     12
#define ADDR_DISPLAY_TEST 15

#define BRIGHTNESS_MIN 0
#define BRIGHTNESS_MAX 15

#define SHUTDOWN_ON  0
#define SHUTDOWN_OFF 1

#define DISPLAY_TEST_OFF 0
#define DISPLAY_TEST_ON  1

#define DP_ON 0x80

#define HYPHEN 10
#define SPACE  15

void setup() {
  SPI.begin();
  SPI.setBitOrder(MSBFIRST);

  led_write(ADDR_DECODE_MODE, B11111111);
  led_write(ADDR_BRIGHTNESS, BRIGHTNESS_MAX / 4);
  led_write(ADDR_SCAN_LIMIT, COLUMN_COUNT - 1);
  led_write(ADDR_SHUTDOWN, SHUTDOWN_OFF);
  led_write(ADDR_DISPLAY_TEST, DISPLAY_TEST_OFF);

  clear();

  led_write(PHYSICAL_COLUMN_4, 1);
  led_write(PHYSICAL_COLUMN_8, 2);
}

void clear() {
  led_write_all(SPACE);
}

void led_write_all(int value) {
  for (int i = COLUMN_BEGIN; i < COLUMN_END; i++) {
    led_write(i, value);
  }
}

void led_write(int addr, int value) {
  digitalWrite(SS, LOW);
  SPI.transfer(addr);
  SPI.transfer(value);
  digitalWrite(SS, HIGH);
}

uint32_t count = 0;

void loop() {
  led_write(ADDR_DISPLAY_TEST, count & 1 ? DISPLAY_TEST_ON : DISPLAY_TEST_OFF);
  delay(1000 * 1.0);
  count += 1;
}

データシートから読み取れる数値にすべて名前をつけて定義しておく。

当初はデータシートと見比べやすいという理由からマジックナンバーを使って書いていたが、動作確認用のサンプルを書く時点で混乱して先に進めなくなったので、データシートと見比べやすいという理由であってもマジックナンバーを使うのはやめた方がいい。

デコードモードと内蔵フォント

7セグメントLEDはLEDが8の字に配置されているというだけで文字とは関係がない。そこで文字を指定するとその文字が出るようにするのがデコードモードになる。有効にするカラムはビットの位置と一致しているため次のようにすると、

led_write(ADDR_DECODE_MODE, B11111111);

すべてのカラムがデコードモードになる。そのあとで、

led_write(PHYSICAL_COLUMN_1, 1);

とすれば左端が 1 になる。

形状 備考
0 0
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 8
9 9
10 - ここから A..F とは続かない
11 E
12 H
13 L
14 P
15 空白
0x80 . 組み合わせ可能
  • 10 以上は A..F ではない
    • データシートを読まないとこの割り当てはわからない
  • DP は組み合わせることができる
    • 例えば 5. と表示するなら 5 | 0x80 とする
  • 消去したいときは空白の 15 を書き込む
    • 専用の消去命令があるわけではない
  • 15 まで16進表記で 16 から独自の割り当てにしてほしかった(小声)
  • 数値だけ表示できればいい場合に向いている

-123.456 を表示するのはわりとたいへん

led_write(PHYSICAL_COLUMN_1, SPACE);
led_write(PHYSICAL_COLUMN_2, HYPHEN);
led_write(PHYSICAL_COLUMN_3, 1);
led_write(PHYSICAL_COLUMN_4, 2);
led_write(PHYSICAL_COLUMN_5, 3 | DP_ON);
led_write(PHYSICAL_COLUMN_6, 4);
led_write(PHYSICAL_COLUMN_7, 5);
led_write(PHYSICAL_COLUMN_8, 6);

どの桁に何を表示するか自分で管理しないといけない。

数値文字列を簡単に表示する

void led_print(const char *str) {
  String s = String(str);
  int dp = 0;
  int pos = 0;
  int column = COLUMN_BEGIN;
  while (true) {
    if (column >= COLUMN_END) {
      break;
    }
    if (pos >= s.length()) {
      while (column < COLUMN_END) {
        led_write(column, SPACE);
        column += 1;
      }
      break;
    }
    const char ch = s.charAt(s.length() - 1 - pos);
    int data = -1;
    if (isDigit(ch)) {
      data = ch - '0';
    } else if (isSpace(ch)) {
      data = SPACE;
    } else if (ch == '-') {
      data = HYPHEN;
    } else if (ch == '.') {
      dp = DP_ON;
    }
    if (data >= 0) {
      led_write(column, data | dp);
      column++;
      dp = 0;
    }
    pos += 1;
  }
}
led_print("-123.456");

E H L P の文字にも対応するなら -HYPHEN に割り当てる部分のようにする。

上のコードはこちらを参考にさせていただいた🙏

https://lowreal.net/2016/03/18/1

以下は led_print を使った例になる。

カウンタの表示

uint32_t count = 0;

void loop() {
  char str[80];
  sprintf(str, "%lu", count);
  led_print(str);
  count += 1;
  delay(1000 / 60);
}

%u だと16ビットになって5桁しか表示されないのでそこだけ注意する。

分秒を表示するメソッド

void time_print(int total_sec) {
  const int m = total_sec / 60;
  const int s = total_sec % 60;
  char str[80];
  sprintf(str, "%02d.%02d", m, s);
  led_print(str);
}

コロンはないため DP で代用する。

経過時間を表示する

void loop() {
  time_print(millis() / 1000);
  delay(1000);
}

3分タイマー

uint32_t count = 3 * 60;

void loop() {
  time_print(count);
  count -= 1;
  delay(1000);
}

SHUTDOWN とは?

CSS で言うところの display: none に相当する。

led_write(ADDR_SHUTDOWN, SHUTDOWN_ON);

とすれば非表示になって節電できる。内部では動いている。完全に電源が切れているわけではない。

led_write(ADDR_SHUTDOWN, SHUTDOWN_OFF);

で表示される。定数にしてあるので違和感はないが実際の値は ON が 0 で OFF が 1 なのでマジックナンバーを使ってしまうと混乱する。SHUTDOWN ではなく ACTIVE や DISPLAY のような名前にした方がよかったと思う(小声)。

デコードを使わない場合

数字がそのまま表示されなくなる替わりに自由にLEDの明滅を制御できるようになる。次のようにカラムに対応するビットを0にすれば、

led_write(ADDR_DECODE_MODE, B00000000);

そのカラムはデコード OFF になる。通常は混在した状態で使うことはないので全体の ON OFF でもよかった。

セグメントラインと対応するビットの簡単な覚え方

6の鏡文字をイメージすると覚えやすい。

ビットが右から順に D0〜D7 まで並んでいるとして、

  1. 8 の字の上辺を D6 とする
  2. そこから右回りに外周を 5 4 3 2 1 で一周し
  3. ハイフンが 0 で終わる

なのでエディタ上では B0 まで入力し、残りの6文字を「左右反転した6」のイメージで入力していけばいい。最後に DP が必要なら B0B1 に変更する。

独自の形状を表現する

led_write_all(B01001000);

ライブラリ LedControl を使う例

#include "LedControl.h"

LedControl lc = LedControl(MOSI, SCK, SS, 1);

void setup() {
  lc.shutdown(0, false);
  lc.setIntensity(0, 8);
  lc.clearDisplay(0);

  lc.setChar(0, 7, 'a', true);
  lc.setChar(0, 6, 'b', true);
  lc.setChar(0, 5, 'c', true);
  lc.setChar(0, 4, 'd', true);
  lc.setDigit(0, 3, 1, true);
  lc.setDigit(0, 2, 2, true);
  lc.setDigit(0, 1, 3, true);
  lc.setDigit(0, 0, 4, true);
}

void loop() {
}
  • メリット
    • シンプルに書ける
    • アルファベットのフォントを内蔵している
  • デメリット
    • 表現が難しいアルファベットは代替文字になったり表示されなかったりする
    • ライブラリのソースコードが一般的に推奨される書き方とは隔りがあり、(自分には)コードの信頼性があまり高くないように感じた

8桁7セグメントLEDを連結する

Arduino → A → B のように連結している場合 addr, value の送信を増やせば奥から順に反応するようになるので、両方に同じ値を指定する場合は次のように変更するだけでいい。

   void led_write(int addr, int value) {
     digitalWrite(SS, LOW);
+    // B に送信
+    SPI.transfer(addr);
+    SPI.transfer(value);
     // A に送信
     SPI.transfer(addr);
     SPI.transfer(value);
     digitalWrite(SS, HIGH);
   }

しかし A と B でそれぞれ別の変更したいとき異様におかしな感じになってくる。具体的には A だけに送信するには B にも送信しないといけないので、 B に仕方なく ADDR_NOOP と無駄な1バイトを送信しないといけない。

void led_write2(int addr1, int value1, int addr2, int value2) {
  digitalWrite(SS, LOW);
  SPI.transfer(addr2);
  SPI.transfer(value2);
  SPI.transfer(addr1);
  SPI.transfer(value1);
  digitalWrite(SS, HIGH);
}

void setup() {
  // snip

  led_write2(PHYSICAL_COLUMN_4, 1, ADDR_NOOP, 0);
  led_write2(PHYSICAL_COLUMN_8, 2, ADDR_NOOP, 0);
  led_write2(ADDR_NOOP, 0, PHYSICAL_COLUMN_4, 3);
  led_write2(ADDR_NOOP, 0, PHYSICAL_COLUMN_8, 4);
}

void loop() {
}

絶対に何か間違っている気がする。またこれが非常に不安定で3回に1回は表示されなかったりする。この原因もよくわかっていない。さらに loop の中で呼ぶと表示されなくなったりする。連続実行が問題なのだろうか。……もうなんもわからん。

LedControl を使った場合

#include "LedControl.h"

LedControl lc = LedControl(MOSI, SCK, SS, 2);

void setup() {
  lc.shutdown(0, false);
  lc.setIntensity(0, 8);
  lc.clearDisplay(0);

  lc.shutdown(1, false);
  lc.setIntensity(1, 8);
  lc.clearDisplay(1);

  lc.setDigit(0, 4, 1, false);
  lc.setDigit(0, 0, 2, false);
  lc.setDigit(1, 4, 3, false);
  lc.setDigit(1, 0, 4, false);
}

void loop() {
}

こちらも同様で3回に1回は正しく表示されない。

まとめ

  • データシートの数値に名前をつけておけば簡単に扱える
  • 手っ取り早く数字を表示するにはデコードモードにする
  • アルファベットなどを表示するならデコードモードをOFFにして自力で形状を指定する
  • ライブラリ LedControl はあまり使いたくない
  • 連結すると急に扱いが難しくなる。その上、不安定になる

参照

Discussion