Использование матрицы LED 16×16 и датчиков SHT1x для контроля за нагревательным котлом

Гостевой пост от Ярцева Валерия, который поделился с нами своим опытом в создании полезного устройства.

Как-то раз, в преддверии ночного заморозка, включая дополнительный контур электрического отопительного котла, я вдруг осознал, что не могу больше жить без точного знания температуры воды на входе в котёл и на выходе из него. Причём это знание я хочу получать издалека, не подходя к котлу вплотную и не разглядывая циферки на каком-нибудь там MT-16S2H. Ещё хотелось бы завершить этот проект до того, как он успеет надоесть.

Учитывая все требования вышеупомянутого техзадания, выбор пал на Arduino,
на цифровой датчик SHT1 и на светодиодную матрицу 16×16. Что получилось, читайте далее.

С датчиком температуры, как и ожидалось, всё оказалось просто. В общем-то случайным образом была выбрана библиотека, которая сразу заработала.

По поводу матрицы были некоторые сомнения. Меньше $10 она стоит потому, что является т.н. матрицей с динамическим управлением. Т.е. на ней нельзя просто так взять и включить какой-то произвольный набор светодиодов (что можно сделать в матрице со статическим управлением). В данной матрице можно подать питание только на один какой-то выбранный ряд. И уже только в этом ряду можно включить или выключить любой светодиод по своему усмотрению. Как же сформировать изображение на всей матрице? Непрерывно переключать ряды, включая в каждом из них соответствующие светодиоды. Если это делать достаточно быстро, то будет как в кино: глаз не будет замечать процесс собственно смены кадров.

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

В интернете я нашёл несколько примеров кода, формирующих изображение на данной матрице.

Одним из них был родной китайский (вместе с подробными схемами самой матрицы). Пример выводил на матрицу иероглифы, в т.ч. в движении. Сам код был ужасен. Как их там учат программированию — непонятно.

Некто даже сделал игрушку на этой матрице (на китайском оригинальном коде).

Другие примеры были значительнее аккуратнее и выглядели весьма прилично: в этом, например, автор использовал ассемблерные вставки для запрещения/разрешения аппаратных прерываний, попадались примеры с использованием ShiftOut.

Самое интересное, что лучше всего работал именно китайский код. При использовании других алгоритмов формирования изображения, светодиоды горели немного слабее и были заметны мелькания на крайних рядах. Поэтому в данном проекте я использовал оригинальный китайский алгоритм. Сам код, конечно, был тщательно облагорожен.

Программа состоит из двух файлов.

Центральная часть — Heater.ino:

// Yartsev Valeriy, Yaroslavl, Russia, icq=34169291
#include
#include

#include "led1616.h"

#define HeatDataPin 2
#define HeatClockPin 3
#define ColdDataPin 4
#define ColdClockPin 5

// Как часто производить опрос датчиков, в миллисекундах.
#define OperResTime 2000

SHT1x Heat_SHT1x( HeatDataPin, HeatClockPin );
SHT1x Cold_SHT1x( ColdDataPin, ColdClockPin );

void setup()
{
  InitLED();
}
void loop()
{
// Очищаем матрицу
  LED_ClearDisplay();
// Считываем показания с двух датчиков,
// округляем до целых значений и загружаем в буфер матрицы
  LED_LoadRow( round( Heat_SHT1x.readTemperatureC() ), 0 );
  LED_LoadRow( round( Cold_SHT1x.readTemperatureC() ), 2 );
// Заданное время непрерывно формируем изображение
  unsigned long LastGet = millis();
  while( millis() - LastGet < OperResTime ) LED_Display();
}

2 секунды в цикле непрерывно происходит формирование изображения на матрице.
Затем управление передаётся библиотеке, которая с двух датчиков считывает данные.
На это время, около секунды, матрица, конечно, гаснет.

Второй файл — led1616.h:

// Yartsev Valeriy, Yaroslavl, Russia, icq=34169291
#define PORT_ROWD   6
#define PORT_ROWC   7
#define PORT_ROWB   8
#define PORT_ROWA   9
#define PORT_GATE  10
#define PORT_DATA  11
#define PORT_CLOCK 12
#define PORT_LATCH 13

#define PORT_TOTAL 8

#define AREA_SIZE 8
#define AREA_TOTAL 4

#define DIGITS_TOTAL 10

uint8_t
// Список портов для инициализации
 PortList[PORT_TOTAL] =
 {
   PORT_GATE, PORT_LATCH, PORT_DATA, PORT_CLOCK,
   PORT_ROWA, PORT_ROWB, PORT_ROWC, PORT_ROWD
 },
// Буфер матрицы, т.е. данные, которые на неё отображаются
 DisplayBuf[AREA_SIZE*AREA_TOTAL],
// Фонты цифр (двоичные, для непосредственного использования)
 BinFont8x8[DIGITS_TOTAL][AREA_SIZE];

// Символьное представление фонтов.
// Конвертируются в двоичные перед началом основного цикла.
char *StrFont8x8[DIGITS_TOTAL][AREA_SIZE]
{
  {
    "  0000  ",
    " 00  00 ",
    " 00  00 ",
    " 00  00 ",
    " 00  00 ",
    " 00  00 ",
    "  0000  ",
    "        "
  },
  {
    "   00   ",
    "  000   ",
    " 0000   ",
    "   00   ",
    "   00   ",
    "   00   ",
    " 000000 ",
    "        "
  },
  {
    "  0000  ",
    " 00  00 ",
    "     00 ",
    "    00  ",
    "   00   ",
    "  00    ",
    " 000000 ",
    "        "
  },
  {
    "  0000  ",
    " 00  00 ",
    "     00 ",
    "   000  ",
    "     00 ",
    " 00  00 ",
    "  0000  ",
    "        "
  },
  {
    "    000 ",
    "   0 00 ",
    "  0  00 ",
    " 00  00 ",
    " 000000 ",
    "     00 ",
    "     00 ",
    "        "
  },
  {
    " 000000 ",
    " 00     ",
    " 00     ",
    " 000000 ",
    "     00 ",
    "     00 ",
    " 000000 ",
    "        "
  },
  {
    " 000000 ",
    " 0      ",
    " 0      ",
    " 000000 ",
    " 0    0 ",
    " 0    0 ",
    " 000000 ",
    "        "
  },
  {
    " 000000 ",
    "     00 ",
    "     00 ",
    "    00  ",
    "   00   ",
    "   00   ",
    "   00   ",
    "        "
  },
  {
    "  0000  ",
    " 00  00 ",
    " 00  00 ",
    "  0000  ",
    " 00  00 ",
    " 00  00 ",
    "  0000  ",
    "        "
  },
  {
    "  0000  ",
    " 00  00 ",
    " 00  00 ",
    "  00000 ",
    "     00 ",
    " 0   00 ",
    "  0000  ",
    "        "
  }
};

// Конвертирует фонты из строкового представления в двоичное
void InitLED()
{
  for( int i = 0; i < DIGITS_TOTAL; i++ )
   for( int j = 0; j < AREA_SIZE; j++ )
   {
     BinFont8x8[i][j] = 0;
     for( int k = 0; k < AREA_SIZE; k++ )
     {
       BinFont8x8[i][j] <<= 1;
       if( StrFont8x8[i][j][k] != '0' )  BinFont8x8[i][j]++;
     }
   }
  for( int i = 0; i < PORT_TOTAL; i++ )
   pinMode( PortList[i], OUTPUT );
}

// Загружает указанную цифру в указанную область буфера матрицы
void LED_LoadDigit( int Digit, int AreaNum )
{
// Обрабатываем только цифры, т.е. с 0 по 9
  if( ( Digit < 0 ) || ( Digit >= DIGITS_TOTAL ) )  return;
// Если вывод идёт в нижний ряд, то опускаем цифру на 1 ряд ниже
  int DownShift = 0;
  if( AreaNum > 1 )  DownShift++;
// Контролируем номер области (с 0 по 3) и
// корректируем нумерацию области с аппаратной на естественную
  switch( AreaNum )
  {
    case 1:  case 2:               AreaNum = 3 - AreaNum;
    case 0:  case AREA_TOTAL - 1:  break;
    default:                       return;
  }
// Заносим в указанную область буфера матрицы фонт указанной цифры
  int BufShift = AreaNum * AREA_SIZE;
  for( int i = 0; i < AREA_SIZE; i++ )
   DisplayBuf[BufShift+i] =
    ( ( ( DownShift == 1 ) && ( i == 0 ) )
      ? 0xFF
      : BinFont8x8[Digit][i-DownShift]
    );
}
// Отображает один ряд матрицы
void LED_Send( unsigned char What )
{
  digitalWrite( PORT_CLOCK, LOW );
  delayMicroseconds( 1 );
  digitalWrite( PORT_LATCH, LOW );
  delayMicroseconds( 1 );
  for( int i = 0 ; i < AREA_SIZE ; i++ )   {     if( What & 1 )  digitalWrite( PORT_DATA, HIGH );	     else            digitalWrite( PORT_DATA, LOW  );     delayMicroseconds( 1 );     digitalWrite( PORT_CLOCK, HIGH );     delayMicroseconds( 1 );     digitalWrite( PORT_CLOCK, LOW );     delayMicroseconds( 1 );		     What >>= 1;
  }
}
// Отображает буфер матрицы
void LED_Display()
{
  for( int i = 0 ; i < AREA_SIZE * AREA_TOTAL / 2 ; i++ )   {     digitalWrite( PORT_GATE, HIGH );     LED_Send( DisplayBuf[i + AREA_SIZE * AREA_TOTAL / 2 ] );     LED_Send( DisplayBuf[i] );     digitalWrite( PORT_LATCH, HIGH );     delayMicroseconds( 1 );     digitalWrite( PORT_LATCH, LOW );     delayMicroseconds( 1 );     digitalWrite( PORT_ROWA,   i        & 1 );     digitalWrite( PORT_ROWB, ( i >> 1 ) & 1 );
    digitalWrite( PORT_ROWC, ( i >> 2 ) & 1 );
    digitalWrite( PORT_ROWD, ( i >> 3 ) & 1 );
    digitalWrite( PORT_GATE, LOW );
    delayMicroseconds( 500 );
  }
}
void LED_LoadRow( int Num, int AreaStart )
// Загружает в указанную область буфера матрицы двухзначное число.
// Если число > 99, то загружается 99. Если < 0, то 0 {   if( Num > 99 )  Num = 99;
  else
   if( Num < 0 )  Num = 0;
  LED_LoadDigit( Num / 10, AreaStart++ );
  LED_LoadDigit( Num % 10, AreaStart   );
}
// Гасит все светодиоды матрицы. Это нужно сделать перед тем,
// как передать управление какому-то процессу,
// который требует время.
void LED_ClearDisplay()
{
  for( int i = 0; i < AREA_SIZE * AREA_TOTAL; i++ )
   DisplayBuf[i] = 0xFF;
  LED_Display();
}

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

Конечно, код не идеален, фонты кривоватые, но, как я писал выше, проект хотелось завершить до того, как он успел бы надоесть.

Зная мощность котла, разность температур на входе и выходе, удельную теплоёмкость воды и предположив, что закон сохранения энергии в котле выполняется, можно оценить дебет котла.
В моём случае: 7500 Вт = 4187 Дж/(кг*К) * 3 К * X кг/с. Получаем примерно 0.6 литра в секунду.

Про монтаж. Была взята стандартная обрезная доска 15×2.5. Отрезан необходимый по длине кусок и обработан электрорубанком. Далее — саморезы, электротехнические разъёмы. Получилось быстро и надёжно. На вопросы «Почему так?» гордо отвечаю «Это современный сельский промышленный дизайн».

Общий план

Общий план

Крепление датчика на трубе

Крепление датчика на трубе

  • Alex Tango

    За идею плюс, за слаботочку огромный минус, поставьте все в электрощиты — поберегитесь, количество нарушений правил монтажа и электропожаробезопасности зашкаливает!

    • YVN

      Да, я работаю над этим…

    • Василий

      Расскажи, что не так со слаботочкой? Вопрос от новичка

  • алекс

    код не рабочий

  • алекс

    отлаживался он видать позже чем был сюда выложен

  • alex

    // Любич Александр Сергеевич alexander.lubich@gmail.com

    #include «Arduino.h»
    #define PORT_ROWD 6
    #define PORT_ROWC 7
    #define PORT_ROWB 8
    #define PORT_ROWA 9
    #define PORT_GATE 10
    #define PORT_DATA 11
    #define PORT_CLOCK 12
    #define PORT_LATCH 13

    #define PORT_TOTAL 8

    #define AREA_SIZE 8
    #define AREA_TOTAL 4

    #define DIGITS_TOTAL 10

    //uint8_t
    // Список портов для инициализации
    char PortList[PORT_TOTAL] =
    {
    PORT_GATE, PORT_LATCH, PORT_DATA, PORT_CLOCK,
    PORT_ROWA, PORT_ROWB, PORT_ROWC, PORT_ROWD
    };
    // Буфер матрицы, т.е. данные, которые на неё отображаются
    char DisplayBuf[AREA_SIZE*AREA_TOTAL],
    // Фонты цифр (двоичные, для непосредственного использования)
    BinFont8x8[DIGITS_TOTAL][AREA_SIZE];

    // Символьное представление фонтов.
    // Конвертируются в двоичные перед началом основного цикла.
    char *StrFont8x8[DIGITS_TOTAL][AREA_SIZE]
    {
    {
    » 0000 «,
    » 00 00 «,
    » 00 00 «,
    » 00 00 «,
    » 00 00 «,
    » 00 00 «,
    » 0000 «,
    » »
    },
    {
    » 00 «,
    » 000 «,
    » 0000 «,
    » 00 «,
    » 00 «,
    » 00 «,
    » 000000 «,
    » »
    },
    {
    » 0000 «,
    » 00 00 «,
    » 00 «,
    » 00 «,
    » 00 «,
    » 00 «,
    » 000000 «,
    » »
    },
    {
    » 0000 «,
    » 00 00 «,
    » 00 «,
    » 000 «,
    » 00 «,
    » 00 00 «,
    » 0000 «,
    » »
    },
    {
    » 000 «,
    » 0 00 «,
    » 0 00 «,
    » 00 00 «,
    » 000000 «,
    » 00 «,
    » 00 «,
    » »
    },
    {
    » 000000 «,
    » 00 «,
    » 00 «,
    » 000000 «,
    » 00 «,
    » 00 «,
    » 000000 «,
    » »
    },
    {
    » 000000 «,
    » 0 «,
    » 0 «,
    » 000000 «,
    » 0 0 «,
    » 0 0 «,
    » 000000 «,
    » »
    },
    {
    » 000000 «,
    » 00 «,
    » 00 «,
    » 00 «,
    » 00 «,
    » 00 «,
    » 00 «,
    » »
    },
    {
    » 0000 «,
    » 00 00 «,
    » 00 00 «,
    » 0000 «,
    » 00 00 «,
    » 00 00 «,
    » 0000 «,
    » »
    },
    {
    » 0000 «,
    » 00 00 «,
    » 00 00 «,
    » 00000 «,
    » 00 «,
    » 0 00 «,
    » 0000 «,
    » »
    }
    };

    // Конвертирует фонты из строкового представления в двоичное
    void InitLED()
    {
    for( int i = 0; i < DIGITS_TOTAL; i++ )
    for( int j = 0; j < AREA_SIZE; j++ )
    {
    BinFont8x8[i][j] = 0;
    for( int k = 0; k < AREA_SIZE; k++ )
    {
    BinFont8x8[i][j] <<= 1;
    if( StrFont8x8[i][j][k] != '0' ) BinFont8x8[i][j]++;
    }
    }
    for( int i = 0; i < PORT_TOTAL; i++ )
    pinMode( PortList[i], OUTPUT );
    }

    // Загружает указанную цифру в указанную область буфера матрицы
    void LED_LoadDigit( int Digit, int AreaNum )
    {
    // Обрабатываем только цифры, т.е. с 0 по 9
    if( ( Digit = DIGITS_TOTAL ) ) return;
    // Если вывод идёт в нижний ряд, то опускаем цифру на 1 ряд ниже
    int DownShift = 0;
    if( AreaNum > 1 ) DownShift++;
    // Контролируем номер области (с 0 по 3) и
    // корректируем нумерацию области с аппаратной на естественную
    switch( AreaNum )
    {
    case 1: case 2: AreaNum = 3 — AreaNum;
    case 0: case AREA_TOTAL — 1: break;
    default: return;
    }
    // Заносим в указанную область буфера матрицы фонт указанной цифры
    int BufShift = AreaNum * AREA_SIZE;
    for( int i = 0; i < AREA_SIZE; i++ )
    DisplayBuf[BufShift+i] =
    ( ( ( DownShift == 1 ) && ( i == 0 ) )
    ? 0xFF
    : BinFont8x8[Digit][i-DownShift]
    );
    }
    // Отображает один ряд матрицы
    void LED_Send( unsigned char What )
    {
    digitalWrite( PORT_CLOCK, LOW );
    delayMicroseconds( 1 );
    digitalWrite( PORT_LATCH, LOW );
    delayMicroseconds( 1 );
    for( int i = 0 ; i >= 1;
    }
    }
    // Отображает буфер матрицы
    void LED_Display()
    {
    for( int i = 0 ; i > 1 ) & 1 );
    digitalWrite( PORT_ROWC, ( i >> 2 ) & 1 );
    digitalWrite( PORT_ROWD, ( i >> 3 ) & 1 );
    digitalWrite( PORT_GATE, LOW );
    delayMicroseconds( 500 );
    }
    }
    void LED_LoadRow( int Num, int AreaStart )
    // Загружает в указанную область буфера матрицы двухзначное число.
    // Если число > 99, то загружается 99. Если 99 ) Num = 99;
    else
    if( Num < 0 ) Num = 0;
    LED_LoadDigit( Num / 10, AreaStart++ );
    LED_LoadDigit( Num % 10, AreaStart );
    }
    // Гасит все светодиоды матрицы. Это нужно сделать перед тем,
    // как передать управление какому-то процессу,
    // который требует время.
    void LED_ClearDisplay()
    {
    for( int i = 0; i < AREA_SIZE * AREA_TOTAL; i++ )
    DisplayBuf[i] = 0xFF;
    LED_Display();
    }