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