Pull to refresh

Проект инфо-панели оповещения об авариях (Часть 2)

Reading time9 min
Views21K
Приветствую всех.
В первой части была рассмотрена концепция самой инфо-панели, которую я собираю для офиса (где, напомню, затруднён нормальный мониторинг за некоторыми серверами, в том числе, из-за топологии сети — серая подсеть с частью важного оборудования, которому не требуется выход в интернет, но от его работы зависит работоспособность услуг телефонии, в частности, TDM и E1).
Теперь накопилось достаточно материала для продолжения темы.

Вот так выглядят индикаторы:



В окончании проекта будет приведён весь код с пояснениями.
Код распространяется по лицензии WTFPL.
За информацию по лицензии, а так же за код для Ethernet модуля ENC28J60 ещё одно спасибо Lifelover.

Проект ещё не закончен, далее будет продолжение.

К сожалению, на текущий момент устройство ещё не собрано, и пришлось довольно много ждать (при разработке столкнулся с неприятными трудностями как при изготовлении плат, так и при закупке некоторых расходников).
Готовы модули индикации (2 из 3), хотя работать можно и только с ними. Частично собрана «библиотека» работы с LED-дисплеем.
Пока нет интерфейса взаимодействия между двумя контроллерами (здесь вопрос к хабровчанам, как сделать лучше — сам склоняюсь к реализации через UART).

Кого заинтересовал — добро пожаловать под кат.
Предупреждение: Много фото.


На текущий момент хорошо работает только модуль, отвечающий за вывод информации на дисплей.
Реализована динамическая индикация (частота обновления порядка 125Hz). Модуль управляется с контроллера ATmega48 (на мекете — ATmega1284p). Оба работают на тактовой частоте 12МГц и питании 3.3V (для совместимости с microSD картой и Ethernet модулем). Питание модуля индикации — должно составлять 5V 0~2.2А/модуль, то есть чуть больше 10W при полном заполнении экрана.
При типичной работе (например, режим часов/календаря) устройство будет потреблять минимум мощности).
В настоящий момент вопрос питания актуален, но склоняюсь к варианту:
48V (PoE) -> 5V (Fly-Back) -> 3.3V (LDO).
Цепь питания 3.3V по расчётам не должна потреблять более 0.5А тока (совместная работа Ethernet модуля (~150-250мА) и 2 контроллеров (вряд ли более 20мА), SD карты (не замерял, но вряд ли более 50мА), часов реального времени (DS1307), хотя последние планирую запитывать от 5V через фильтры питания).

Сейчас собраны два модуля идникации, то же фото, что в заголовке, но без кадрирования. Фото ниже.
Скрытый текст
Два модуля индикации заработали вместе.


Общий концепт работы устройства был несколько пересмотрен с первого поста. Теперь оно будет состоять из двух процессоров, как водится, Master (ATMefa1284p) и Slave(ATMega48). Плата ЦП ещё не готова (т.к. пока не полностью выбрана переферия и не хочется переделывать всё по нескольку раз). Сейчас остались некоторые вопросы по звуку, например, стоит ли вообще заморачиваться с воспроизведением потокового аудио (8bit, ~22kHz) прямо из сети, или проще отбросить идею? Вариант с чтением звуковых файлов с SD карты — заманчив, но тут возникает проблема управления ими, а так же (возможно, загрузки их на карту прямо из сети). Возможно, остановлюсь на втором варианте.
Мастер будет отвечать за поддержку сети, звук(возможно), а так же (возможно) простейший telnet доступ.
Позже постараюсь реализовать обнолвение ПО по сети (если хватит прямоты рук), либо с SD-карты (скорее, uSD в режиме SPI). Руки не доходят собрать макетку.

Платы индикаторов чуть раньше
Скрытый текст


Знаю, довольно страшно.

Таких будет 3.
Плата электроники — в процессе… пока не получается вытравить. Фоторезист смывается при проявлении.
Фото очередной неудачи: (Теперь Positiv 20). Похоже, этот метод мне ещё не даётся.
Скрытый текст

Самое лучшее, что удалось сделать на текущий момент… к несчастью, тоже повреждена. Потёки и неровный слой резиста сделали своё коварное дело.
Чаще получается нечто подобное:

ПН-ВЩ-50 трёхлетней выдержки — это… очень печально.

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

Уже припаяна сама плата, массив тразнисторных ключей, сдвиговые регистры.

Даже все строки и столбцы работают.

В процессе отладки.
По какой-то причине строка выводится с ошибками. (Устарело)


upd 2014-05-11: Удалось победить «драйвер» дисплея, получив мягкий скролл строки по всему «экрану».
На данный день небольшую победу можно посмотреть здесь.
Прошу прощения за ссылку на ВК, но иного места для заливки видео пока не нашёл. Если кто подскажет — буду благодарен.
Готовится вторая плата индикатора — уже собраны все компоненты, осталось спаять сами матрицы.
Код… пока не терпит критики, потому будет допиливаться. Хотя уже сейчас можно произвольно изменять размерность экрана и это не повлияет на его работоспособность (в разумных пределах).
Использование RAM — минимально. Только под буфер текста (либо графики, но на 1 кадр), и несколько пенеменных. Вполне укладываюсь в доступную RAM в 512 байт.
upd 2014-06-02: Готовы два индикатора, код масштабируется на индикаторы в пару движений — изменить один дефайн и пересобрать проект. Фото было выше.
Код управления работает, но пока не заточен под обработку двумя контроллерами. На макетной плате работает только на одном контроллере, второй пока ждёт своей очереди. На 48-й меге нет JTAG'а, потому отлаживать на железе будет значительно сложнее. Стараюсь исправить все баги ещё до сборки платы контроллеров.

Возник вопрос, можно ли обновить ПО «соседнего» контроллера с flash накопителя. Удобнее всего (из того, что легко подключить — microSD/SPI, но придётся писать свой загрузчик). Сейчас обдумываю вариант прошивки Slave'а через стантарнтый ISP интерфейс, подключенный к Master'у (придётся выводить разъёмы для программирования на оба контроллера, а так же джамперы, чтобы Master мог обновлять ПО «драйвера экрана».

Спасибо Alexeyslav за подсказку о управлении индикаторами по SPI. Данный вариант рассматривался, но, был случайно забыт. Сейчас данные выводятся методом «ногодрыга» на котроллере.
\\ Однако, ресурсов потребляет мало, потому не столь фатально.
\\ Когда опишу новый вариант — постараюсь привести эпюры сигналов.

Немного кода:
led.h
/*
Модуль управления LED матрицами для AlarmPanel.
*/
// Поле: (3*9)*8
// Хранить будем 27*8 = 216 байт.

#define MATRIX_COUNT	16			// Количество индикаторов
#define ARRAY_SIZE	MATRIX_COUNT * 8	// Суммарный объём памяти для массива.
#define VisibleChars	ARRAY_SIZE / 6

#define LED_MODE_OFF		0
#define LED_MODE_TXT_SCROLL	1
#define LED_MODE_TXT_NO_SCROLL	2
#define LED_MODE_IMG		3

#define LED_MODE_MAX LED_MODE_IMG

void LED_Reset(void);
	// Сбрасывает все индикаторы. Должна вызываться, когда нужно погасить индикацию.
void LED_PushBits(unsigned char DataByte, unsigned char Bits);
	// Проталкивает байт в строку.
void LED_FirstLine(void);
	// Сброс и выбор первой строки.
void LED_NextLine(void);
	// Подготовка новой строки (переключение вертикальной защёлки)
void LED_Sync(void);
	// А эта "интересная" команда - ни что иное, как дерганье пином "Latch" на всех сдвиговых регистрах. Позволяет избежать "дребезга".
//char HEX(unsigned char dataNibble);
//	// Преобразование числа в HEX формат (принимаются значения [0..15], возвращается номер символа).
void LED_DrawChar(uint8_t CharN, uint8_t LineNum, /*uint8_t SkipCols,*/ uint8_t ShowCols);
	// Вывод символа на дисплей. При этом можно пропустить несколько столбцов. (НомерСимвола, НомерСтроки, ПропускСтолбцов)
void LED_DrawString(void);
	// Регенерация строки. Должно выполняться каждую 1ms (1000/8 = 125Hz), в крайнем случае, каждые 2мс (500/8 = 62.5Hz)
//void LED_NewString(alarm_packet_t *data);
	// Получение новой строки из сети. Должна получать ссылку на память, откуда забирать, и количество, которое забирать.
void LED_DrawImg(void);
	// Вывод изображения. При этом не будут работать никакие переменные, отвечающие за обработку строк. Только номер текущей строки и счётчик.
	// В этот раз будем считать, что нужно вывести 8 бит из байта (то есть, ВЕСЬ).
void LED_Poll(void);
	// Собственно, "обработчик", который будет вызываться каждый тик таймера.
	// Да, он сам решит, что делать.
	
// Знакогенератор.
// Вырезан - занимает ОЧЕНЬ много места в посте.

led.c
#include "led.h"
//#define TextScrollMS 50
uint8_t DataArray[ARRAY_SIZE];		// Массив символов.
uint8_t CurrMode;			// Текущий режим отображения: {0=Выключено, 1=Текст со скроллом, 2=Текст без скролла, 3=Графика}
uint8_t CurrLength;			// Текущий размер массива (актуально для текста).
uint8_t CurrLine;			// Текущая строка
int16_t CurrSkipChars;			// Количество прокручиваемых символов
// Пропускать можно более чем 127 символов (ибо буфер строки больше 128-ми), так что... int16.
uint8_t CurrColSkip;			// Позиция сдвига внутри символа (дополнительная прокрутка)

volatile uint8_t TextScrollDelayMS;
volatile uint16_t LastMS;

void LED_SetMode(uint8_t NewMode)
{
	if (NewMode <= LED_MODE_MAX)
	{ CurrMode = NewMode;
	} else { CurrMode = LED_MODE_OFF; };
};

void LED_Poll(void){
	if (CurrMode == LED_MODE_OFF) { return; };
	if (CurrMode == LED_MODE_TXT_SCROLL) { LED_DrawString(); };
	if (CurrMode == LED_MODE_TXT_NO_SCROLL) { LED_DrawString(); };
	if (CurrMode == LED_MODE_IMG) { LED_DrawImg(); };
};

void LED_Reset(void) {
	char t;
	CurrLine = 0;
	CurrSkipChars = -VisibleChars;
	LED_PORT |= (1<<LED_OE);			// Переводим выходы всех регистров из Z-состояния
	LED_PORT |= (1<<LED_DATA_V);			// Поднимаем пин данных.
	for (t=0;t<8;t++)
	{
		LED_PORT |= (1<<LED_C_V);		// Проталкиваем 0 в вертикальный регистр
		LED_PORT &= ~(1<<LED_C_V);		// Повторяем, пока он не закончится.
	};
	LED_PORT &= ~(1<<LED_OE);			// Z-состояние убираем..
};

void LED_PushBits(unsigned char DataByte, unsigned char Bits) {
	while (Bits > 0)				// Для оставшихся битов
	{
		Bits--;					// Отсчитываем 1 колонку
		if (DataByte & 0x80)			// Если старший бит = 1 (после сдвигов)
		{
			LED_PORT |= (1<<LED_DATA_H);	// Поднимаем пин "Data"
		} else {
			LED_PORT &= ~(1<<LED_DATA_H);	// Иначе - опускаем его
		};
		LED_PORT |= (1<<LED_C_H);		// И дёргаем такт для горизонтальной синхронизации
		LED_PORT &= ~(1<<LED_C_H);		// Проталкиваем 1 бит в горизонтальные регистры.
		DataByte <<= 1;				// Сдвигаем оставшиеся биты влево на 1.
	};						// Повторяем, пока не закончатся колонки.
};

void LED_FirstLine(void) {
	LED_PORT &= ~(1<<LED_DATA_V);			// Опускаем пин данных
	LED_PORT |= (1<<LED_C_V);			// Дёргаем тактирование
	LED_PORT &= ~(1<<LED_C_V);			// Опускаем такт.
	LED_PORT |= (1<<LED_DATA_V);			// Поднимаем пин обратно
//	LED_Sync();					// Синхронизация
};

void LED_NextLine(void) {
	LED_PORT |= (1<<LED_C_V);			// Проталкиваем бит в вертикальный регистр
	LED_PORT &= ~(1<<LED_C_V);			// ОДИН раз.

	CurrLine++;					// Следующая строка
	CurrLine &= 0x07;				// Только 7 строк
							// Если произошёл переход на новую строку, то
	if (CurrLine==0)
	{
		LED_FirstLine();
		// Если прошло времени больше, чем нужно ждать для скролла, то
		if (((ticks_count - LastMS) > TextScrollDelayMS)|(ticks_count < LastMS))
		{
			LastMS = ticks_count;		// Запоминаем новое время
			if (CurrMode == LED_MODE_TXT_SCROLL)
			{
				CurrColSkip--;			// Прокручиваем на 1 колонку
				if (CurrColSkip == 255)		// Целочисленное переполнение, ибо меньше нуля..
				{
					CurrColSkip = 5;	// Теперь пропускаем все 5/6 столбцов...
					CurrSkipChars++;	// И добавляем 1 к пропущенным байтам из буфера.
				};
								// Если мы выползли ЗА буфер
				if (CurrSkipChars > CurrLength)
				{
								// Начинаем с самого начала (из-за правой стороны)
					CurrSkipChars = -VisibleChars;
				};
			} else {
				CurrColSkip = 0;
				CurrSkipChars = 0;
			};
		};
	};
};

void LED_Sync(void) {
	// Заготовка
	LED_PORT |= (1<<LED_OE);			// Переводим выходы всех регистров в Z-состояние
	LED_PORT |= (1<<LED_LATCH);			// Защёлкиваем данные
	LED_PORT &= ~(1<<LED_LATCH);			// Хватит защёлкивать. Опускаем пин.
	LED_PORT &= ~(1<<LED_OE);			// Разрешаем выводам работать. Возможно предосторожность изляшняя.
};

void LED_DrawChar(uint8_t CharN, uint8_t LineNum, /*uint8_t SkipCols,*/ uint8_t ShowCols)
{
	unsigned char temp = 0;
	unsigned char cols;
	cols = ShowCols;				// Количество отображаемых колонок
	if (cols == 0)
	{
		return;
	};
	if (CharN == 168) {				// Ё
		temp = pgm_read_byte(&CharsRu[65][LineNum]);
	};
	if (CharN == 184) {				// ё
		temp = pgm_read_byte(&CharsRu[66][LineNum]);
	};
	if (CharN > 191) {				// Русские символы
		temp = pgm_read_byte(&CharsRu[CharN-192][LineNum]);
	};
	if ((CharN > 32)&(CharN <= 126)) {		// Английские символы и некоторые знаки
		temp = pgm_read_byte(&Chars[CharN-32][LineNum]);
	};

	// Экономия RAM. Было бы быстрее из памяти, но так всего +1 такт (формально, 1 такт.... вероятно, больше).
	LED_PushBits(temp, cols);			// Оставшееся выводим на экран
};

void LED_DrawString(void)
{
	int16_t First;					// Первый видимый символ
	int16_t Last;					// Последний выводимый символ
	int16_t SymbolsSkip;				// Количество пропускаемых символов
							// Так же работает, как количество добавляемых символов.
	uint8_t i;					// Переменная "i" - более 40 лет на рынке счётчиков!
	uint8_t EndedWithChar;				// Флаг, так его растак... Но без него никак.
	SymbolsSkip = CurrSkipChars-1;			// Копируем количество пропускаемых символов
	while (SymbolsSkip < 0) {
		SymbolsSkip ++;				// Пока количество не сравняется с 0
		LED_PushBits(0, 6);			// Проталкиваем нули (гасим строку).
	};
							// Нужное количество знакомест пропущено.
							// Вычисляем ПЕРВЫЙ символ, который нужно отобразить.
	if (CurrSkipChars <= 0)				// Если нужно добавить пустых знакомест
	{
		First = -1;				// Начинаем показ с самого первого символа.
	} else {
		First = CurrSkipChars-2;		// Иначе - количество пропускаемых символов-2
							// Чтобы самые дальние символы не исчезали перед покиданием
							// индикатора.
	};
	Last = First + CurrSkipChars + VisibleChars;	// Вычисляем последний символ.
	// Если последний символ всё же больше нуля (в случае полностью сдвинутой строки)
	if (Last > First)
	{
		// Попытка вывести на экран больше, чем есть в массиве
		if (Last > CurrLength-1)
		{
			// Получаем правильную позицию последнего символа.
			Last = CurrLength-1;
			// Теперь нужно вычислить, сколько необходимо пустого пространства.
			// Вычисляем, как (Общая длинна видимых символов) + Текущий сдвиг - Последний видимый символ - 1
			// -1 - ибо нумеруем, как нормальные люди, с нуля.
			SymbolsSkip = VisibleChars + CurrSkipChars - Last - 1;
		};
		if (SymbolsSkip > 0)
			{ EndedWithChar = 0; } else { EndedWithChar = 1; };
		// Конструкция выше - это проверка, закончится ли строка символом из массива, или нет.
		// Если нужно вывести более 1 символа...
		for (i = First+1; i < Last; i++)
		{
			// Выводим символы, которые можем без обработки.
			LED_DrawChar(DataArray[i], CurrLine, 6);
		};
		// Выводим последний... По идее, правильно.
		if (EndedWithChar)
		{
			LED_DrawChar(DataArray[Last], CurrLine, 6-CurrColSkip);
		} else {
			LED_DrawChar(DataArray[Last], CurrLine, 6);
		};

		// Если нужно дополнить пробелами в конце..
		if (SymbolsSkip > 0)
		{
			// В количестве чуть МЕНЕЕ чем нужно пропустить
			// целых символов,...
			for (i=1;i<SymbolsSkip;i++)
			{
				// ...выводим пустые знакоместа.
				LED_PushBits(0, 6);
			};
			// Если что-то остаётся - выводим заполнение.
			LED_PushBits(0, 6-CurrColSkip);
		};
	};
	LED_Sync();					// Синхронизируем.
	LED_NextLine();					// И переводим строку.
};

void LED_DrawImg(void)
{
	uint8_t i;					// Счётчик
	// Диапазон значений - от Номера текущей строки*8 (адрес первого байта)
	// и до следующей строки.
	for (i = CurrLine*MATRIX_COUNT; i<CurrLine*(MATRIX_COUNT+1); i++)
	{
		// Выводим символ на дисплей... Полностью, все 8 бит.
		LED_PushBits(DataArray[i], 8);
	};
	// Фактически, при этом будет выведено только MATRIX_COUNT байт.
	// Довольно быстрая процедура.
	LED_Sync();					// Синхронизируем.
	LED_NextLine();					// И переводим строку.
};
Tags:
Hubs:
+17
Comments23

Articles

Change theme settings