Библиотека для 7ми-сегментного дисплея TM1637

Вывод информации на дисплей практически не отличается от вывода в консоль.
Функции hide(), show() и setBrightness(x) управляют состоянием дисплея.

ngTM1637.h
#pragma once
// ================================
// ---

#if defined(ARDUINO) && ARDUINO >= 100
  #include <Arduino.h>
#else
  #include <WProgram.h>
#endif

#if defined(__AVR_ARCH__)
  #include <avr/pgmspace.h>
#else
  #include <pgmspace.h>
#endif

#include <stdint.h>
#include <stdio.h>

template <const uint8_t Digits>
class TDriverTM1637 : public Print
{
private:
  static constexpr uint8_t cmd_address_autoincrement  = 0x40;
  static constexpr uint8_t cmd_address_not_increment  = 0x44;
  static constexpr uint8_t cmd_set_display_mode_off   = 0x80;
  static constexpr uint8_t cmd_set_address(const uint8_t address) { return 0xC0u | (7 & address); }
  static constexpr uint8_t cmd_brightness(const uint8_t level) { return 0x88u | (7 & level); }

  const uint8_t _DIO;
  const uint8_t _CLK;

protected:
  static constexpr uint8_t width = Digits;
  uint8_t brightness;

  size_t byteWrite(const uint8_t value) const;
  size_t rawWrite(const uint8_t addr, const uint8_t * const raw, const uint8_t count = Digits) const;
  size_t rawWrite(const uint8_t * const raw, const uint8_t count = Digits) const;
  
  inline void stop() const {
    digitalWrite(_CLK, HIGH);
    digitalWrite(_DIO, HIGH);
  }
  inline void restart() const {
    //stop();
    digitalWrite(_CLK, HIGH);
    digitalWrite(_DIO, HIGH);
    // Start
    digitalWrite(_DIO, LOW);
    digitalWrite(_CLK, LOW);
  }

public:
  TDriverTM1637() = delete;
  //TDriverTM1637(TDriverTM1637&) = delete;
  //TDriverTM1637(TDriverTM1637&&) = delete;

  TDriverTM1637(const uint8_t clk_pin, const uint8_t dio_pin)
    : Print(), _CLK(clk_pin), _DIO(dio_pin)
  {};

  /* начальная инициализация драйвера TM1637
   * порты DIO и CLK будут переключены в режим выхода
   * и включены все сегменты на минимальной яркости
   */
  void init();

  /* включить индикацию с указанием уровня яркости
   * от 0 - минимальный до 7 - максимальный уровень
   */
  inline void setBrightness(const uint8_t lvl = 0) { brightness = 7 & lvl; show(); }

  /* вернуть установленный уровень яркости
   */
  inline uint8_t getBrightness() const { return brightness; }

  /* включить индикацию
   * с ранее установленным уровнем яркости
   */
  void show() const;

    /* выключить/погасить/ индикацию
   * информация в буфере TM1637 сохраняется
   */
  void hide() const;
};

//  -------------
// | \    a    / |     a = 0x01
// |  \ ----- /  |     b = 0x02
// | f |     | b |     c = 0x04
// |   /-----\   |     d = 0x08
// |--<   g   >--|     e = 0x10
// |   \-----/   |     f = 0x20
// | e |     | c |     g = 0x40
// |  / ----- \  |     h = 0x80
// | /    d    \ | ---
//  ------------- | h |
//                 ---

#define _a_ 0x01
#define _b_ 0x02
#define _c_ 0x04
#define _d_ 0x08
#define _e_ 0x10
#define _f_ 0x20
#define _g_ 0x40
#define _h_ 0x80

template <class Inherited>
class T7SegmentDisplay : public Inherited
{
private:
  uint8_t frame_buff[2 * Inherited::width];
  uint8_t position;

public:
  T7SegmentDisplay() = delete;
  //T7SegmentDisplay(T7SegmentDisplay&) = delete;
  //T7SegmentDisplay(T7SegmentDisplay&&) = delete;

  T7SegmentDisplay(const uint8_t clk_pin, const uint8_t dio_pin);

protected:
  uint8_t getCharCode(const char x) const;
  virtual size_t write(uint8_t);
  using Print::write; // pull in write(str) and write(buf, size) from Print

private:
  struct T7SegmentCode {
    const char code;
    const uint8_t glif;
  };

  /* массив начертаний символов
   * должен быть отсортирован по возрастанию кода символа
   * для корректной работы алгоритма поиска 
   * методом половинного деления
   */
  static constexpr T7SegmentCode segment_code[] PROGMEM = {
    {'0', (_a_|_b_|_c_|_d_|_e_|_f_) },
    {'1', (_b_|_c_) },
    {'2', (_a_|_b_|_d_|_e_|_g_) },
    {'3', (_a_|_b_|_c_|_d_|_g_) },
    {'4', (_b_|_c_|_f_|_g_) },
    {'5', (_a_|_c_|_d_|_f_|_g_) },
    {'6', (_a_|_c_|_d_|_e_|_f_|_g_) },
    {'7', (_a_|_b_|_c_) },
    {'8', (_a_|_b_|_c_|_d_|_e_|_f_|_g_) },
    {'9', (_a_|_b_|_c_|_d_|_f_|_g_) },
    {'A', (_a_|_b_|_c_|_e_|_f_|_g_) },
    {'B', (_c_|_d_|_e_|_f_|_g_) },
    {'C', (_a_|_d_|_e_|_f_) },
    {'D', (_b_|_c_|_d_|_e_|_g_) },
    {'E', (_a_|_d_|_e_|_f_|_g_) },
    {'F', (_a_|_e_|_f_|_g_) },
    {'G', (_a_|_c_|_d_|_e_|_f_) },
    {'H', (_b_|_c_|_e_|_f_|_g_) },
    {'I', (_e_|_f_) },
    {'J', (_b_|_c_|_d_|_e_) },
    {'L', (_d_|_e_|_f_) },
    {'N', (_c_|_e_|_g_) },
    {'O', (_a_|_b_|_c_|_d_|_e_|_f_) },
    {'P', (_a_|_b_|_e_|_f_|_g_) },
    {'Q', (_a_|_b_|_c_|_f_|_g_) },
    {'R', (_e_|_g_) },
    {'S', (_a_|_c_|_d_|_f_|_g_) },
    {'T', (_d_|_e_|_f_|_g_) },
    {'U', (_b_|_c_|_d_|_e_|_f_) },
    {'Y', (_b_|_c_|_d_|_f_|_g_) },
    {'[', (_a_|_d_|_e_|_f_) },
    {']', (_a_|_b_|_c_|_d_) },
  };

  static constexpr uint8_t segment_code_count = sizeof(segment_code) / sizeof(T7SegmentCode);
};

/* -------------------------------------------------------------
 * объявление типа для четырехзначного дисплея 
 */
using TM1637 = T7SegmentDisplay<TDriverTM1637<4>>;

/* -------------------------------------------------------------
 * объявление массива перекодирования символов
 */
template <class Inherited>
const struct T7SegmentDisplay<Inherited>::T7SegmentCode T7SegmentDisplay<Inherited>::segment_code[];

/* -------------------------------------------------------------
 * реализация шаблона класса TDriverTM1637
 */
template <const uint8_t Digits>
void TDriverTM1637<Digits>::init() 
{
  stop();
  pinMode(_CLK, OUTPUT);
  pinMode(_DIO, OUTPUT);
  brightness = 0;

  restart();
  if (byteWrite(cmd_brightness(brightness)))
  {
    restart();
    byteWrite(cmd_set_address(0));

    restart();
    byteWrite(cmd_address_autoincrement);
    for (int ii = Digits; ii; --ii)
      byteWrite(0xFF);
  }
  stop();
}

template <const uint8_t Digits>
size_t TDriverTM1637<Digits>::byteWrite(const uint8_t value) const
{
  int8_t x = static_cast<int8_t>(value);

  // передать 8 бит младшим битом вперед
  for (uint8_t ii = 8; ii; --ii, x >>= 1) {
    digitalWrite(_CLK, LOW);
    digitalWrite(_DIO, 1 & x);
    digitalWrite(_CLK, HIGH);
  }

  // Для подтверждения приема TM1637 формирует ACK
  // (низкий уровень на линии данных) с минимальной
  // (100..150 наносекунд) задержкой относительно
  // заднего фронта 8го строба и чтобы избежать
  // сквозного тока по линии данных
  // нужно проверять ее состояние
  // если 1 - сразу перевести ее в режим входа
  //          а затем опусть строб
  // если 0 - сначала опусть строб и только после
  //          перевести линию данных в режим входа
  if (x) pinMode(_DIO, INPUT);
  digitalWrite(_CLK, LOW);
  pinMode(_DIO, INPUT_PULLUP);
  // INPUT_PULLUP здесь нужен, чтобы обнаружить
  // отсутсвие ACK когда дисплей не подключен

  // 9й строб - обработка ACK
  // TM1637 будет держать ACK до получения заднего фронта
  // поднимаем строб, это даст небольшую задержку перед
  // проверкой наличия ACK
  // задержка нужна потому что, когда дисплей не подключен
  // фронт на линии данных может быть завален
  digitalWrite(_CLK, HIGH);

  // проверить наличие ACK
  x = digitalRead(_DIO);
  digitalWrite(_DIO, x);
  pinMode(_DIO, OUTPUT);

  // завершаем передачу байта
  digitalWrite(_CLK, LOW);
  digitalWrite(_DIO, LOW);
 
  // 1 - if ACK;
  // 0 - not ACK;
  return !(x);
}

template <const uint8_t Digits>
void TDriverTM1637<Digits>::hide() const
{
  restart();
  byteWrite(cmd_set_display_mode_off);
  stop();
}

template <const uint8_t Digits>
void TDriverTM1637<Digits>::show() const
{
  restart();
  byteWrite(cmd_brightness(brightness));
  stop();
}

template <const uint8_t Digits>
size_t TDriverTM1637<Digits>::rawWrite(const uint8_t addr, const uint8_t * const raw, const uint8_t length) const
{
  int8_t count = 0;

  restart();
  if (byteWrite(cmd_set_address(addr)))
  {
    restart();
    byteWrite(cmd_address_autoincrement);
    count = (Digits < length) ? Digits : length;
    for (uint8_t ii = 0; count > ii; ++ii)
      byteWrite(raw[ii]);
  }
  stop();

  return count;
}

template <const uint8_t Digits>
size_t TDriverTM1637<Digits>::rawWrite(const uint8_t * const raw, const uint8_t count) const
{
  return rawWrite(0, raw, count);
}

/* -------------------------------------------------------------
 * реализация шаблона класса T7SegmentDisplay
 */
template <class Inherited>
T7SegmentDisplay<Inherited>::T7SegmentDisplay(const uint8_t clk_pin, const uint8_t dio_pin)
  : Inherited(clk_pin, dio_pin), position(Inherited::width - 1)
{
  memset(frame_buff, 0, Inherited::width);
}

template <class Inherited>
size_t T7SegmentDisplay<Inherited>::write(uint8_t x)
{
  switch (x) {
    case '\n': // FL - новая строка
      position = Inherited::width - 1;
      break;

    case '\r': // CR - возврат каретки
      Inherited::rawWrite(&frame_buff[position - (Inherited::width - 1)], Inherited::width);
      break;

    case '.': // включить точку в текущей позиции
    case ',':
      if (Inherited::width < position)
        frame_buff[position] |= _h_;
      break;

    default: // заполнение буфера
      if (((2 * Inherited::width) - 1) > position)
        frame_buff[++position] = getCharCode(x);
      break;
  }
  return 1;
}

template <class Inherited>
uint8_t T7SegmentDisplay<Inherited>::getCharCode(const char x) const
{
  uint8_t glif = 0;

  switch (x) {
    case ' ': glif = 0; break;
    case '*': glif = (_a_|_b_|_f_|_g_); break;
    case '-': glif = (_g_); break;
    case '_': glif = (_d_); break;
    case '~': glif = (_a_); break;
    default:
      const char upcase = x - ((('a' <= x) && (x <= 'z')) ? 0x20 : 0);
      uint8_t imax = segment_code_count - 1;
      uint8_t imin = 0;

      for (uint8_t step = segment_code_count; step; step /= 2) {
        const uint8_t index = imin + (imax - imin) / 2;
        const char key = pgm_read_byte(&(segment_code[index].code));

        if (key == upcase) {
          glif = pgm_read_byte(&(segment_code[index].glif));
          break;
        } else {
          if (key > upcase) imax = index - 1;
          else              imin = index + 1;
        }
      };
      break;
  }

  return glif;
}

и пример использования

ngTM1637.ino
#include "ngTM1637.h"

#define DISPLAY_DIO 10
#define DISPLAY_CLK 11

TM1637 display(DISPLAY_CLK, DISPLAY_DIO);
uint16_t count;

void setup()
{
  display.init();
  count = 0;
}

void loop()
{
  int ii;

  delay(1000);

  // вывод строки
  char timedot[8];
  char timestr[8];
  // время с мигающей точкой/двоеточием посередине
  sprintf_P(timedot, PSTR("%02i.%02i"), count / 60, count % 60);
  sprintf_P(timestr, PSTR("%02i%02i"), count / 60, count % 60);
  for (ii = 7; ii; --ii) {
    display.println(timestr);
    delay(600);
    display.println(timedot);
    delay(400);
  }

  // погасить и снова включить экран
  // информация на экране сохраняется
  display.hide();
  delay(600);
  display.show();

  // меняем яркость экрана
  // библиотека обрежет устанавливаеммый
  // уровень до допустимого 0..7 по маске
  // поэтому немного схалтурим...
  for (ii = 0; 5 * 8 + 1 > ii; ++ii) {
    display.setBrightness(ii);
    delay(96);
  }

  // очистка экрана
  display.println();
  delay(600);

  // вывод чисел
  // положительная температура
  auto celsius = F("*C");
  display.print(10);
  display.println(celsius);
  delay(1000);
  // отрицательная температура
  display.print(-2);
  display.println(celsius);
  delay(1000);

  // очистка экрана
  display.println();

  ++count;
}

А если вывести «Жуй компот»?

На русском не получится, а на международном, часть букв выводится.

Может таки лучше в Проекты? Можно перенести

1 лайк

Тогда подкорректируй фразу:

Я за. Но если готов к этому автор. Там такие тонны гомна могут прилететь… Уууу… ))

Они и тут прилететь могут )))

1 лайк

Тут не так страшно ))

Вообще - я бы на месте ТС посмотрел что тут «прилетит» и если не сильно, то в разделе проекты месту быть (с его желания, конечно же).

Ну, мое дело предложить )

С чем я полностью согласен. Немного подкорректировать описание и вообще «огонь»!

А тут?

Ну так

А у меня своя биб-ка есть. Уже прям с тоннами.

https://github.com/DetSimen/TM1637

2 лайка

Кстати, я ее применял! Отлично работает!

Для проектов мне кармы не хватает. Посмотрим, что тут прилетит, а после можно и в проекты, я не против, конечно, если считаете, что теме там место.

Я наследовал класс драйвера дисплея от Print, это один из предков Serial, чтобы вывод на дисплей был максимально схож по формату с выводом в последовательный порт. И как вы предлагаете отразить это в описании?

Я помню, вместе ее чинили :slightly_smiling_face:

1 лайк

Благодарствую, боярин.

Можно, не то, чтобы совет, а так – “на подумать”?

Я как-то делал библиотеку для семисегментников. Устройство было развесистое и для упрощения программирования я применил технику “виртуального экрана”. Подумайте, може Вам будет интересно реализовать нечто подобное.

Чтобы долго не описывать, я просто скопипастил кусок документации к той библиотеке:

Концепция виртуальных экранов

Часто бывает нужно выводить на экран несколько разных параметров. Например, температуру, там влажность и, скажем, время на Марсе. Причём совершенно необязательно выводить их одновременно. Вполне устроит, если выводить что-то одно, а при некоторых действиях оператора (нажатие кнопки или переключение режима) выводить другое. Так многие радиоприёмники постоянно показывают текущее время, а во время сканирования эфира – частоту.

Однако, если мы будем в программе постоянно помнить что у нас в данный момент выводится – программа будет запутанной до безобразия. Например, мы получили новое значение с датчика температуры. И что, надо смотреть, что именно сейчас показывает дисплей, если температуру, то показывать, а если время на Марсе, то пока не показывать – чем больше параметров и чем больше функций переключения, тем более запутанный код.

Ситуацию спасает концепция «виртуальных экранов». Когда мы для каждого показываемого параметра заводим свой виртуальный экран и любые программы, которые этот параметр вычисляют, никогда не заботятся (и ничего не знают) о реальном экране и о том, что там сейчас показывается. Они просто выводят свою информацию на свой виртуальный экран, который всегда находится в их полном распоряжении, и спокойно работают дальше. А связью между виртуальными экранами и реальным физическим экраном занимается отдельный модуль, который больше ничего не делает. Он (этот модуль) понятия не имеет что именно на этих кранах – это не его дело. Его работа связать «активный» виртуальный экран с реальным, и, когда надо, сделать «активным» другой виртуальный экран.

1 лайк

Правдой. Например, нельзя написать «Жуй компот» и все всё поймут…))

Вывод информации на дисплей практически не отличается от вывода в консоль за одним маленьким исключением.
На экран нельзя вывести “Жуй компот”.

2 лайка