Обновить

Делаем брелок с LED матрицей ярче

Уровень сложностиПростой
Время на прочтение13 мин
Охват и читатели18K
Всего голосов 21: ↑19 и ↓2+27
Комментарии32

Комментарии 32

Не понимаю, почему этот случай не произошел в самом первом тесте, возможно батарейка разрядилась.

Просто сейчас китайцы делают очень много хреновых батареек 2032 с высоким внутренним сопротивлением. Напряжение без нагрузки на них нормальное, в часах и на материнских платах работают отлично - но большие токи выдавать не способны даже кратковременно.

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

ИИ гугла подсказывает, что при 0.2 мА достигается указанная емкость из даташита, но сама батарейка может испытывать пики по 100 мА и выше. И это верно, т.к. иначе бы брелок вообще не светился, потому что 0.2 мА это совсем как-то мало. Странно, что в даташите от Panasonic про пики и большие токи ничего не написано.

Не совсем так.

Пиковый ток указан на диаграмме "Operating voltage vs. load resistance". Из неё видим, что при токе ~2 мА напряжение на батарейке упадёт до ~2,7 В. Это в дело вступает ESR (Equivalent Series Resistance) - эквивалентое последовательное сопротивление батарейки. Соответственно это сопротивление можно вычислить (3,0В - 2,7В) / 0,002А=150 Ом. Отсюда можно увидеть, что при КЗ батарейки ток будет всего: 3 В/150 Ом = 20 мА (!!!).

И тут вы можете спросить, а как-же токи по 100 мА через светодиоды?

А вот для этих пиковых токов параллельно батарейке ставится конденсатор большой ёмкости. Обычно это танталовый электролит с малыми утечками и малым ESR (у разного тантала на 100 мкФ он обычно от 0,01 до 1 Ом). Но скважность импульсов тока через светодиод должна быть 1:500 (100 мА / 0,2 мА), чтобы в промежутках между импульсами батарейка успела зарядить конденсатор своим малым током.

В любом случае использовать литиевую батарейку в таком устройстве плохо. Вот аккумулятор, который Вы применили во второй версии - имеет более чем на два порядка меньшее внутренее сопротивление (0,6 Ом https://www.powerstream.com/p/Lir2032.pdf) и гораздо более уместен при таких сравнительно больших токах.

Спасибо за объяснение. Понятно почему теперь с CR2032 столько проблем, я правда все равно не понимаю, почему у меня матрица светится нормально при отсутсвии конденсатора (на v1), но правда только с красной матрицей, по логике она должна вообще не светится.

Красные светодиоды начинают заметно светиться уже при токах 0,1 мА. Кроме того, реальная батарейка обычно имеет некоторый "запас" по параметрам. В документации нормальных производителей обычно указывается наихудший случай, т.е. при всех возможных отклонениях техпроцессов батарейка будет "не хуже документа".

Но прежде посмотрим на данные из википедии, как должно быть на самом деле. Падение напряжения на светодиодах:

Смотреть надо данные из даташита производителя матрицы.

если у нас батарейка 3.3В и падение напряжения 1.63В (красный) с токоограничивающим резистором 200 Ом, то ток будет (3.3 - 1.3) / 200 = 9.9 мА. А для падения напряжения 2.48В (синий) получаем ток 40 мА

Как это у вас вышло? (3,3 - 2,48) / 200 = 4 mA

хочу показать, что светодиодные матрицы должны светится по-разному, красные ярче, затем зеленые, затем синие.

Поэтому обычно на разные цвета ставят разные номиналы резисторов.

Спасибо большое за то, что нашли ошибки. 40 мА это опечатка, почему не догадался, что что-то не то, не понятно. А про светодиодные матрицы согласен, но почему-то был уверен, что можно смело брать данные из википедии, т.к. это уже стандартизированно. Надо было и то и другое привести. В целом для статьи это не критично, с учетом последнего комментария и разных фотографий, но так было бы грамотнее.

А если подобрать для каждого типа светодиода свой резистор, так что бы ток был одинаков, то какой ярче будет?

По логике должны одинаково светить, но после теста резистров по 100 и 200 Ом думаю, что разница будет небольшая или не понятная для глаза. Но даже при этом, если делаешь законченное устройство их стоит поставить, чтобы пользователь был уверен, что цветопередача корректная, например, в каком-нибудь дисплее. Если кто-то лучше разбирается в этом вопросе, поправьте, пожалуйста, т.к. пишу по своим рассуждениям.

По логике должны одинаково светить

При одинаковом токе и одинаковом КПД светодиодов зеленый будет светить визуально ярче. Это особенность человеческого зрения - максимальная чувствительность у глаза находится в районе 550нм, что соответствует зеленому цвету.

При одинаковых кпд и мощности, скорее. Напряжение на них разное - не меньше энергии кванта в электрон-вольтах.

Яркость в тех.описании (в канделах которая измеряется) как раз спектраоьную чувствительность глаза тоже учитывает.

Для некоторых изделий токоограничивающие резисторы вообще не ставят используя хак с высоким внутренним сопротивлением (не способностью отдать большой ток) элемента 2032 (всевозможные фонарики на 1 светодиод, эмуляторы сосулек на ёлку, горящего пламени). Можете попробовать закоротить (поставить на 10 Ом) резисторы на брелке V1.

Пример на микроконтроллере с резистором последовательно с батарейкой (эмуляция батарейки 2032)

Ролик на простом английском со схемой и сборкой

Интересно, надо будет попробовать при случае, правда думал, что уже закончил с этим проектом. Просто я боялся ставить резисторы с малым номиналом, чтобы не спалить светодиоды. Еще хотел добавить, что этот тест с токоограничивающими резисторами был один из нескольких и на конечный вывод статьи скорее всего не повлияет, т.е. эти резисторы можно поставить и ATtiny2313A и получить еще ярче картину, а не только CH32V003.

Интересно было бы вставить туда к примеру esp, с возможностью смены изо и анимации. Сделать например кнопку по которой модуль будет просыпаться и коннектится к телефону, а после загрузки кода нового изображения брелок ребутился бы espшка спала.

Интересно на сколько хватает данного аккумулятора

Батарейки при такой схеме очевидно хватит не надолго. Надо будет больше держать устройство во сне, использовать лучше BLE, чем WiFi и коннектиться лучше после нажатия по кнопке. Я думал делать брелок с Bluetooth, устройство сразу бы получилось интереснее, чем без него, но есть нюанс, что надо это все уложить в размеры светодиодный матрицы и может не получится. По-крайней мере скорее всего не получится на односторонней плате, как делал я, но на двухсторонней можно попробовать. Правда на момент поисков я не рассматривал ESP, а смотрел в сторону HC-05.

У вас может быть совершенно другие критерии и все может быть по-другому. Например, можно взять ESP + MAX7219 + литиевую батарейку с модулем заряда и устройство будет скажем в 1.5 раза больше, но зато с радикоаналом и возможностью подзарядки через USB. Но, как по мне, этот брелок ближе к игрушке и зачем все так усложнять - не понятно. Более, чем достаточно запрограммировать через программатор, включить, поморгать и выключить.

Я тоже думал за ESP после прочтения первой и второй статьи. Но увы, сама по себе ESP потребляет много, и анимация (а принципе любая) тоже. Есть к примеру ESP32C3 которая упакована в один чип с небольшим набором компонентов, но все же.

Можно попробовать использовать WS2812B, например от Lolin 8x8 RGB. Это матрица хоть и в разы меньше, с возможностью контролировать яркость и цвета, но не уверен что она обыграет по потреблению обычные светодиоды.

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

Интересно, можно попробовать заменить матрицу (взять самодельную), добавить Bluetooth через ESP и уже готов материал для следующей статьи, правда устройство заметно сложнее и по-хорошему надо придумать ему интересное применение, тогда можно браться. Пока правда я не планирую это делать, но посмотрим, может быть какая-та идея появится и захочется.

Для синих светодиодов и для подсевшей батарейки можно соорудить простой синхронный повышающий преобразователь напряжения, управляемый от мк. И им застабилизировать ток и яркость

Типа такого

ADC1 измеряет напряжение батарейки, ADC2 - ток через светодиод. Мк вычисляет, на сколько нужно поднять напряжение, и выдаёт расчитанный ШИМ-сигнал на преобразователь. В качестве преобразователя - обычный или высокотоковый логический инвертор.

Интересно, но можете пояснить подробнее про преобразователь? Про ADC1, ADC2, ШИМ понятно, а почему если подать ШИМ на 74AC04, то получится регулировать напряжение или стабилизировать ток? Логический инвертор же просто инвертирует сигнал ШИМ, т.е. это же можно сделать даже программно. В сети сходу про эту схему ничего не нашел.

Или подается ШИМ на питание 74AC04? Как в таком случае будет получаться повышение напряжения?

Если вскрыть КМОП-инвертор, из него можно достать пару транзисторов. На них собираем синхронный повышающий DCDC. Запараллелив их просто повышаем допустимый ток.

Вот такой DCDC

Можно и отдельные транзисторы взять, или сборку из двух. Но их искать надо, а 74я логика везде продаëтся.

При ШИМ 100% (постоянной логической 1) VCC будет равно напряжению батарейки. При 90%, например, VCC = Vbat / 0,9 то есть, на 11% выше Vbat.

Только аккуратно прошивку писать надо. Сначала отладить, а уже потом впаять катушку. Иначе при ошибке в софте мк сам себя сожжëт.

Вот как, интересно! Спасибо. Чтобы получше понять, сначало соберу эту схему в симуляторе, а потом поробую в каком-нибудь своем следующем устройстве.

Кажется, оно должно плохо работать из-за ошибок в алгоритме.

Динамическая индикация не даёт зажечь все светодиоды одновременно, из-за неё мы можем одновременно зажечь только 1 строку (или столбец). То есть одновременно может гореть только 1/8 часть матрицы.

Но из-за этого кода во внутреннем цикле получается сокращение до 1/64:

digitalWrite(col[thisCol], thisPixel);
if (thisPixel == HIGH) {
	// <-- неявная задержка из-за медленности digitalWrite,
    //      благодаря которой матрица вообще светится.
	digitalWrite(col[thisCol], LOW);
}

А из-за отсутствия задержек много времени будет потрачено на исполнение кода (обслуживающего индикацию), в это время матрица не горит.

Из delayWithRefresh() можно почти всё убрать.

void delayWithRefresh(unsigned long d) {
	unsigned long prevTime = millis();
	unsigned long duration = 0;

	while (true) {
		unsigned long time = millis();
		unsigned long tmp = (time - prevTime);
		prevTime = time;
		duration += tmp;
		if (duration > d) {
			break;
		}

		refreshScreen();
	}
}
void delayWithRefresh(uint32_t durationInMs) {
	uint32_t startTime = millis();
	while(millis() - startTime < durationInMs) {
		refreshScreen();
	}
}

Регулировка яркости если работает, то из-за аналогичной неявной задержки (на вызов milis() и т.д.) - у условия if (_pwm < _intensity) {<проводим здесь xxx времени>} нет ветки else с аналогичной задержкой.

В displayFrame() (который всё-таки не показывает, а загружает в SRAM) заранее упакованные (1 строка - 1 байт) значения из флеш-памяти распаковываются (8 проходов по каждому байту) и снова упаковываются, хотя их достаточно только прочитать из флеша. А читать их можно побайтово там, где они используются в коде - эта функция и массив _pixels оказываются не нужны[*].

Можно двоичные литералы применить здесь:

const uint8_t ghost_Frame_1[8] PROGMEM = { 0x1c, 0x3e ...

C++14 их даёт в таком виде: 0b0010'1111, но если компилятор старее, то библиотеки Arduino дают их в виде констант препроцессора типа B00101111.

const uint8_t ghost_Frame_1[8] PROGMEM = {
	0b00011100, 
	0b00111110,
	0b01101011,
	0b01001001,
	0b01111111,
	0b01100011,
	0b01111111,
	0b01010101 
};

Как видится полноценный алгоритм для матрицы:

Для каждой строки:

  • Подготовить столбцы - подать напряжение (логическую единицу) на нужные аноды диодов[**]

  • Включить строку - подать лог.0 на строку (т.е. на катоды диодов, чтобы ток втекал в мк)

  • Задержка - такая, чтобы получить частоту обновления в 100..1000 Гц

  • Выключить строку - подать лог.1 на строку

  • <-- сюда можно перекинуть часть задержки для реализации регулировки яркости

[*] вообще, особое отношение к константам во флеше требуется из-за того, что AVR с его гарвардской архитектурой слишком старый с точки зрения C++ - расширение Named Address Spaces есть только для C.

[**] без Arduino-фреймворка это можно сократить до PORTB = _pixels(i); (вместо того внутреннего цикла), если развести плату так, чтобы выводы столбцов приходились на один порт. Но в любом случае, фреймворк здесь добавляет проблем - заставляет думать о тяжести восьми вызовов digitalWrite().

___

Какая-то статья про динамическую индикацию с хорошим описанием алгоритма: https://microsin.net/programming/avr/led-matrix-dynamic-indication.html

___

Исправление алгоритма повысит ток (как и уменьшение резисторов), надо подумать:

  • насколько просядет напряжение питания (=> стабильность микроконтроллера)

  • не станет ли яркость строки зависеть от количества включённых светодиодов из-за просадки напряжения (сейчас она не может зависеть, потому что одновременно горит только 1 светодиод)

  • насколько безопасно для выходов микроконтроллера

Спасибо за развёрнутый комментарий, это действительно странно, что я сам не догадался избавиться от фреймворка Ардуино и оптимизировать код, да и никто в комментариях не предложил это. По ощущениям должно помочь, но могу сразу сказать, что алгоритм для ATtiny2313A и CH32V003 похожи, надо тогда оптимизировать оба. Но думаю лучше результат будет для ATtiny2313A. Пока вы совершенно правы, что я писал что горят 8 светодиодов одновременно, а по факту меньше из-за отсутствия оптимизации.

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

Сейчас правда понимаю, что и оптимизация не всесильна, там от многих задержек не избавиться, но возможно яркость в два раза удасться подтянуть. Я потом попробую это сделать.

Ещё, получается, displayFrame() занимается переворачиванием транспонированием кадра, от которого особенно не захочется избавляться из-за возможности "рисовать кадр" двоичными литералами.

И от которого нельзя удачно избавиться изменением направления сканирования матрицы (не построчно, а по столбцам), потому что зажигая строку мы имеем резистор параллельно каждому светодиоду, а при зажигании столбца такой "гарантии равномерного свечения" на этой плате не будет - будет один резистор на все светодиоды.

Я бы в такую сторону думал (выделил русским временные комменты):

for (uint8_t rowIdx = 0; rowIdx < 8; rowIdx++) {
	// 1. prepare columns (stored in transposed state)
	for (uint8_t i = 0; i < 8; i++) {
        // надо сделать указатель frame видимым отсюда
		uint8_t val = pgm_read_byte(frame + i);
		// viewed from software, val is bit-packed row of one frame
		// viewed from hardware, val is one column of LED matrix
		//  and we need rowIdx'th bit from each column here
		digitalWrite(col[i], val & (1 << rowIdx));
	}
	
	// 2. activate row
	digitalWrite(row[rowIdx], 0);
	
	// 3. delay in active state
	delayMicroseconds(1000);
	// итого частота обновления всей матрицы: до 1/(8*1000 мкс) = 125 Гц
	// ещё добавить к знаменателю время на (8+2)*8 = 64 вызовов digitalWrite():
	// до 1/(8*(1000 мкс) + 64*(5 мкс)) = 120 Гц
	
	// 4. deactivate row
	digitalWrite(row[rowIdx], 1);
	
	// 5. delay in inactive state for brightness adjustment
    // ...
}

Да, изначальный код написан так, чтобы постараться обеспечить равномерное свечение. Попробую сделать, как вы описали, но еще перепишу digitalWrite под конкретно эту задачу. Посмотрим.

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

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

О, классно.

____

Заодно поправлю себя в прошлом комменте:

резистор параллельно каждому светодиоду
(8+2)*8 = 64 80

Так как почти весь прирост должен быть от зажигания 8 светодиодов вместо 1 и задержки во включённом состоянии, не стал писать про отказ от digitalWrite().

Но вообще тогда думал, что можно упороться вплоть до использования регистров PINx (переключение пина без read-modify-write). И чересстрочной развёртки, чтобы достичь незаметности мерцания на более низкой частоте:

Идеи
for (uint8_t rowIdx_ = 0; rowIdx_ < 8; rowIdx_++) {
	uint8_t rowIdx = rowIdx_;
#if defined(USE_INTERLACED_SCAN)
	rowIdx = rowIdx_/4 + 2*(rowIdx_%4); // 0,2,4,6,1,3,5,7
#endif

    // 1. prepare columns
#define H(X) readAndUnpackPixel(frame, X, rowIdx) // helper macro
	setMatrixColumns(H(0), H(1), H(2), H(3), H(4), H(5), H(6), H(7));
#undef H
	
	// 2. activate row
	ToggleRow(rowIdx);
	
	// 3. delay in active state
	delayMicroseconds(1000);
	
	// 4. deactivate row
	ToggleRow(rowIdx);
}

/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

// Для инициализации; в т.ч. когда обновление прерывается, 
//  оставляя матрицу в неизвестном состоянии.
void SetRowsToHigh() {
	PORTD |= ((1 << 2) | (1 << 1) | (1 << 4) | (1 << 5));
	PORTB |= ((1 << 1) | (1 << 2) | (1 << 4));
	PORTA |= ((1 << 1));
}

void ToggleRow(uint8_t rowIdx) {
	switch(rowIdx) {
		case 0: PIND = (1 << 2); break;
		case 1: PINB = (1 << 1); break;
		case 2: PINB = (1 << 2); break;
		case 3: PIND = (1 << 1); break;
		case 4: PINB = (1 << 4); break;
		case 5: PINA = (1 << 1); break;
		case 6: PIND = (1 << 4); break;
		case 7: PIND = (1 << 5); break;
	}
}

/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

inline bool readAndUnpackPixel(frame, uint8_t colIdx, uint8_t rowIdx) {
	return (bool)(pgm_read_byte(frame + colIdx) & (1 << rowIdx));
    //     ^^^^^^~~~~ изначально написал !! вместо этого, но не знаю,
    //                 что лучше выразит намерение нормализовать bool
}

/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */

inline void setMatrixColumns(bool b0, bool b1, bool b2, bool b3,
							 bool b4, bool b5, bool b6, bool b7) {
	PORTD &= ~((1 << 0) | (1 << 3) | (1 << 6));
	PORTB &= ~((1 << 6) | (1 << 5) | (1 << 0) | (1 << 3));
	PORTA &= ~((1 << 0));
    // ^~~~~ можно вызывать реже, 
    // если в PINx передавать изменения относительно предыдущей строки
	PIND = (b0 << 0) | (b1 << 3) | (b4 << 6);
	PINB = (b2 << 6) | (b5 << 5) | (b6 << 0) | (b7 << 3);
	PINA = (b3 << 0);
}

С последними стандартами плюсов, наверное, можно без особых ужасов проинициализировать массив consteval-функцией, которая бы делала необходимые оптимизации (повернуть массив в compile-time при необходимости и класть дельту начиная со второй строки матрицы, чтобы не считать её в рантайме).

Интересно, про порт PINx ничего до этого не знал, про него правда в даташите ничего и не написано, но информация легко находится.

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

Возможно еще аналога PINx нет в CH32V003, но там есть BSHR, наверно можно с ним сделать похожее.

Еще возможно распаковка или транспонирование не нужно, т.к. когда я переписывал от неё отказался. Все работало без нее.

В даташите найти можно, хотя лучше знать, что искать.
https://ww1.microchip.com/downloads/en/DeviceDoc/doc8246.pdf#page=57

Да, этот прирост не нужен. Такие оптимизации пригодились бы, если бы у микроконтроллера были другие задачи, тогда ещё были бы нужны неблокирующие задержки, пришлось бы думать про (не)атомарность изменения PORTx.

использования регистров PINx (переключение пина без read-modify-write).

Я тут не подумал, что строка PORTB |= (1 << PB3); должна компилироваться в одну инструкцию SBI (Set Bit in I/O Register).

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Информация

Сайт
timeweb.cloud
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Timeweb Cloud