Цифровой индикатор 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.

   

Фотогалерея