
Гостевой пост от Ярцева Валерия, который поделился с нами своим опытом в создании полезного устройства.
Как-то раз, в преддверии ночного заморозка, включая дополнительный контур электрического отопительного котла, я вдруг осознал, что не могу больше жить без точного знания температуры воды на входе в котёл и на выходе из него. Причём это знание я хочу получать издалека, не подходя к котлу вплотную и не разглядывая циферки на каком-нибудь там 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. Отрезан необходимый по длине кусок и обработан электрорубанком. Далее — саморезы, электротехнические разъёмы. Получилось быстро и надёжно. На вопросы «Почему так?» гордо отвечаю «Это современный сельский промышленный дизайн».

