Цифровой индикатор tm1637

 
    04.10.2021

Хард

    При разработке очередного девайса на базе digispark наткнулся на али на цифровой индикатор tm1637. Как показали эксперименты, плата действительно простая и универсальная. Цена вопроса примерно 200 рублей. Смысл в подключении по двум линиям для передачи данных, при этом никакого стандартного протокола нет. Можно сравнить с олед экраном, но проблема в том что почти все графические экраны требуют протокола i2c. А в нём есть конкретно прописанные тайминги, и фиксированная частота несущей, 400 кГц насколько помню. В простых контроллерах, вроде digispark, встроенной поддержки i2c нет, есть программная реализация, но она будет съедать много памяти и процессорного времени, и на остальные вычисления запросто может не хватить ресурсов.

    Сама плата выглядит так:

С обратной стороны сама микросхема и минимум деталей:
При желании я думаю без проблем можно подключить полевики и любое количество мощных диодов, увеличив габариты дисплея. Есть платы с 6 значным дисплеем. Также платы отличаются по доп.знаку точка для вывода цифр, либо двоеточие посередине, для отображения времени.

   

Софт

Под tm1637 сущестувет несколько библиотек. От Гайвера, Грува и т.д. Но есть маленькая проблемка - за спецэффекты приходится расплачиваться ресурсами, памятью и процессорным временем. К примеру библиотека, выдающая следующий результат, заняла 97% памяти digispark:
Я выкинул всё ненужное, оставил только вывод цифр. В исходной библиотеке при этом обнаружились некоторые странности:
#define CLK 1
#define DIO 2
#define _dash 0x40 //-
#define _degree 0x63

int c = 0;
byte tablo[5] = {0, 0, 0, 0, 7};//four digit + bright 0..7
const byte translator[16] = {0x3f, 0x06, 0x5b, 0x4f, 0x66,
0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7f,0x39, 0x3f, 0x79, 0x71};

void start(void){
  digitalWrite(CLK, HIGH);
  digitalWrite(DIO, HIGH);
  digitalWrite(DIO, LOW);
  digitalWrite(CLK, LOW);
}

void stop(void){
  digitalWrite(CLK, LOW);
  digitalWrite(DIO, LOW);
  digitalWrite(CLK, HIGH);
  digitalWrite(DIO, HIGH);
}

void writeByte(byte v){
  for(byte i = 0; i < 8; i++){
    digitalWrite(CLK, LOW);
    digitalWrite(DIO, v & 1);
    v >>= 1;
    digitalWrite(CLK, HIGH);
  }
  
  digitalWrite(CLK, LOW);
  digitalWrite(DIO, HIGH);
  digitalWrite(CLK, HIGH);
  digitalWrite(DIO, LOW);
}

void tm1637out(){
  start();
  writeByte(0x40);
  stop();           
  start();
  writeByte(0xc0);
  writeByte(tablo[0]);
  writeByte(tablo[1]);
  writeByte(tablo[2]);
  writeByte(tablo[3]);
  stop();
  start();
  writeByte(0x88 + (tablo[4] & 7));
  stop();
}

void setup() {
  pinMode(CLK, OUTPUT);
  pinMode(DIO, OUTPUT);
}

void loop() {
  tablo[0] = translator[(c / 100) % 10];
  tablo[1] = translator[(c /  10) % 10];
  tablo[2] = translator[ c        % 10];
  tablo[3] = _degree;
  tablo[4] = c % 8;
  tm1637out();

  c++;
 }
Плата подключается к выводам 1 и 2. Сложного вроде ничего нет. Однако меня тут не устроила производительность. Дело в том что хоть стандартный протокол и не воспроизводится, но управление битами происходит через digitalWrite, что бессмысленно съедает немало процессорного времени, поэтому решил уйти от вызова этой функции. При этом нужно подчеркнуть, следующий вариант будет работать только на digispark, так как явно указывается порт, в который выводятся данные, порт я узнал выведя его на экран. Получился следующий оптимизированный код:
#define CLK 1
#define DIO 2
#define tm1637bit(b) (1 << (b))
#define _dash 0x40 //-
#define _degree 0x63

int c = 0;
byte tablo[5] = {0xc0, 0, 0, 0, 0};//four sign
const byte translator[16] = {0x3f, 0x06,0x5b,
0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7f,
0x39,0x3f,0x79,0x71};

void CLKDIO11(){
    *(byte*)0x38 |= tm1637bit(CLK) + tm1637bit(DIO);
}

void CLK1(){
    *(byte*)0x38 |= tm1637bit(CLK);
}

void DIO1(){
    *(byte*)0x38 |= tm1637bit(DIO);
}

void CLK0(){
    *(byte*)0x38 &= 255 - tm1637bit(CLK);
}

void DIO0(){
    *(byte*)0x38 &= 255 - tm1637bit(DIO);
}

void start(void){
  CLKDIO11();
  DIO0();
  CLK0();
}

void stop(void){
  CLK0();
  DIO0();
  CLKDIO11();
}

void tm1637send(byte v){
  for(byte i = 0; i < 8; i++){
    CLK0();
    if( v & 1 ){
      DIO1();
    }else{
      DIO0();
    }
    
    v >>= 1;
    CLK1();
  }
  
  CLK0();
  CLKDIO11();
  DIO0();
}

void tm1637out(){
  start();
  for(byte p = 0; p < 5; p++){
    tm1637send(tablo[p]);
  }
  stop();
}

void tm1637bright(byte howmuch){
  start();
  tm1637send(0x88 + (howmuch & 7));
  stop();  
}

void setup() {
  pinMode(CLK, OUTPUT);
  pinMode(DIO, OUTPUT);

  tm1637bright(7);
}

void loop() {
  tablo[1] = translator[(c /1000) % 10];
  tablo[2] = translator[(c / 100) % 10];
  tablo[3] = translator[(c /  10) % 10];
  tablo[4] = translator[ c        % 10];
  tm1637out();

  c++;
 //delay(4);
}
Из спортивного интереса снял осциллограмму и к удивлению обнаружил, что подготовка числа для вывода занимает почти столько же процессорного времени, как и сам вывод. 92 против 100 микросекунд:
Причина проста - деление и умножение самые затратные операции для процессора, так как они в конечном итоге выполняются через сложение и битовые операции. По итогу оптимизировал ещё кое-что и ушёл от деления. Я думаю далеко ушёл от стандартов для tm1637, то есть с некоторыми платами может не работать, а также с длинными проводами и сильными помехами:
#define CLK 1
#define DIO 2
#define tm1637bit(b) (1 << (b))
#define _dash 0x40 //-
#define _degree 0x63

int c = 0;
const byte translator[16] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0x77, 0x7f, 0x39, 0x3f, 0x79, 0x71};

void CLKDIO11(){
    *(byte*)0x38 |= tm1637bit(CLK) + tm1637bit(DIO);
}

void CLK1(){
    *(byte*)0x38 |= tm1637bit(CLK);
}

void DIO1(){
    *(byte*)0x38 |= tm1637bit(DIO);
}

void CLK0(){
    *(byte*)0x38 &= 255 - tm1637bit(CLK);
}

void DIO0(){
    *(byte*)0x38 &= 255 - tm1637bit(DIO);
}

void tm1637send(byte v){
  for(byte i = 0; i < 8; i++){
    CLK0();
    if( v & 1 ){
      DIO1();
    }else{
      DIO0();
    }
    
    v >>= 1;
    CLK1();
  }
  
  CLK0();
  CLKDIO11();
  DIO0();
}

void tm1637out(byte tablo[5]){
  CLKDIO11();
  DIO0();
  CLK0();

  tm1637send(    0xc0);
  tm1637send(tablo[0]);
  tm1637send(tablo[1]);
  tm1637send(tablo[2]);
  tm1637send(tablo[3]);
  
  CLK0();
  DIO0();
  CLKDIO11();
  
  CLKDIO11();
}

void tm1637bright(byte howmuch){
  CLKDIO11();
  DIO0();
  CLK0();
  
  tm1637send(0x88 + (howmuch & 7));
  
  CLK0();
  DIO0();
  CLKDIO11();

  CLKDIO11();
}

void setup() {
  pinMode(CLK, OUTPUT);
  pinMode(DIO, OUTPUT);

  tm1637bright(7);
}

void loop() {
  byte tablo[4] = {0, 0, 0, 0};//four sign
  int c0;
  
  c0 = c;
  while( c0 >= 1000 ){
    c0 -= 1000;
    tablo[0]++;
  }

  while( c0 >= 100 ){
    c0 -= 100;
    tablo[1]++;
  }

  while( c0 >= 10 ){
    c0 -= 10;
    tablo[2]++;
  }

  tablo[0] = translator[tablo[0]];
  tablo[1] = translator[tablo[1]];
  tablo[2] = translator[tablo[2]];
  tablo[3] = translator[c0];
  tm1637out(tablo);

  c++;
  if( c >= 10000 ) c = 0;
  //delay(250);
}
Получилась следующая осциллограмма:
Подготовка числа делается быстрее раз в 8, конечная частота кадров составляет 8650 fps. Неплохо :).

    Что имеем в итоге - плата неприхотливая, удобная и универсальная. Вырисовывается вариант спидометра/одометра на digispark и двух-трёх tm1637.

   

Шестизначная плата tm1637

    Давно уже купил на Али 6-значные платы на tm1637, выдалась свободная минутка, решил опробовать. Но на этот раз я скачал даташит и работу с платой привёл в соответствие с ним. В прошлый раз я творчески интерпретивал чужую библиотеку, при помощи метода научного тыка. По сути не так и сложно вышло, например биты данных устанавливать при нулевом уровне строба:

Также по даташиту длительность строба не менее 1 мкСек, при той же паузе, получаем частоту передачи не более 500 кГц. Я на Digispark реализовал максимально быструю запись данных непосредственно в порт, то есть быстрее некуда, и это вполне укладывается в минимальные требования по даташиту:
Единственное, что я не реализовал, после окончании передачи байта и девятого такта надо ждать, пока tm1637 не поднимет напряжение на линии данных, но и без этого работает нормально. На другой, более быстрой плате надо будет или ждать, или подобрать паузу, если будут сбои. Ещё из приколов, у 6-значной платы странный порядок знаков. Почему так не знаю, вероятно плату так оказалось проще развести, а может сами индикаторы отличаются по выводам. Но это небольшая проблема. Выводим число 123456 и потом просто в коде меняем порядок вывода, чтобы отображалось правильно. Сам код ниже, он работает из 4х значной платой, только порядок надо расскоментировать, а 6-значный убрать. Также немного изменил логику управления яркостью, там 4й бит (8 десятичное) отвечает за в(ы)ключение, то есть число менее 8 - выключение индикатора, 8 + 1..7 это включение с заданной яркостью.
#define CLK 1
#define DIO 3
#define tm1637bit(b) (1 << (b))
#define _dash 0x40 //-
#define _degree 0x63

/*
 # DIO change only CLK = Low

 # Byte send start:
  CLK = High; DIO to Low
  so Idle CLK = High, DIO = High

 # After send one (9th) CLK = High with DIO = Low, then idle both High
 
 */

long c = 0;
const byte translator[16] = {0x3f, 0x06, 0x5b, 0x4f, 0x66, 0x6d, 0x7d, 0x07, 0x7f, 0x6f, 0x77, 0x7f, 0x39, 0x3f, 0x79, 0x71};

void CLK1(){
    *(byte*)0x38 |= tm1637bit(CLK);
}

void DIO1(){
    *(byte*)0x38 |= tm1637bit(DIO);
}

void CLK0(){
    *(byte*)0x38 &= 255 - tm1637bit(CLK);
}

void DIO0(){
    *(byte*)0x38 &= 255 - tm1637bit(DIO);
}

void tm1637idle(){
  DIO1();
  CLK1();
}

void tm1637cmdStart(){
  DIO0();
}

void tm1637send(byte v){
  for(byte i = 0; i < 8; i++){
    CLK0();
    if( v & 1 ){
      DIO1();
    }else{
      DIO0();
    }
    
    v >>= 1;
    CLK1();
  }
  
  CLK0();
  DIO0();
  CLK1();//9th stop CLK, wait for ACK
  CLK0();
 
}

void tm1637out(byte tablo[6], byte bright){
  tm1637cmdStart();

  tm1637send(    0x40);//0x40 mode = output, autoincrement
  
  CLK1();
  DIO1();
  DIO0();
  CLK0();
  
  tm1637send(    0xc0);//0xc0 address set command, 0..5 address

  //210543 for 6-digit
  //2345 for 4-digit
  
  tm1637send(tablo[2]);
  tm1637send(tablo[1]);
  tm1637send(tablo[0]);
  tm1637send(tablo[5]);
  tm1637send(tablo[4]);
  tm1637send(tablo[3]);

  /*
  tm1637send(tablo[2]);
  tm1637send(tablo[3]);
  tm1637send(tablo[4]);
  tm1637send(tablo[5]);*/

  CLK1();
  DIO1();
  DIO0();
  CLK0();

  tm1637send(0x80 + (bright & 15)); //0x80 control 0x8 - On, 0-7 - bright  

  tm1637idle();  
}

void setup() {
  pinMode(CLK, OUTPUT);
  pinMode(DIO, OUTPUT);

  tm1637idle();
}

void loop() {
  byte tablo[6] = {0, 0, 0, 0, 0, 0};
  long c0;

  c0 = c;
  
  while( c0 >= 100000 ){
    c0 -= 100000;
    tablo[0]++;
  }

  while( c0 >= 10000 ){
    c0 -= 10000;
    tablo[1]++;
  }

  while( c0 >= 1000 ){
    c0 -= 1000;
    tablo[2]++;
  }

  while( c0 >= 100 ){
    c0 -= 100;
    tablo[3]++;
  }

  while( c0 >= 10 ){
    c0 -= 10;
    tablo[4]++;
  }

  tablo[0] = translator[tablo[0]];
  tablo[1] = translator[tablo[1]];
  tablo[2] = translator[tablo[2]];
  tablo[3] = translator[tablo[3]];
  tablo[4] = translator[tablo[4]];
  tablo[5] = translator[c0];

  tm1637out(tablo, 15);

  c += 5;
  if( c >= 1000000 ) c = 0;
  delay(10);
}
Пример работы ниже. Теоритическая максимальная скорость отображения 6 знаков примерно 500`000 / (9 * 9) = порядка 6000 fps. Напомню, с 4 значным было более 8000 fps.
Также у платы tm1637 есть интересная фича - можно подключить 16 кнопок, одновременное нажатие нескольких не поддерживается. Сейчас пока не реализовывал, но если приспичит, реализую и дополню статью. На Али есть готовые платы с кнопками, но это просто, я думаю, тупо подпаяю пару кнопок к этой плате.

   

Фотогалерея