- PVSM.RU - https://www.pvsm.ru -

ТВ-таймер обратного отсчета на микроконтроллере AVR

Первый результат

Однажды один мой друг спросил, на чем бы я сделал таймер обратного отсчета, чтобы на телевизоре показывал большие цифры. Понятно, что можно подключить ноутбук / iPad / Android и написать приложение, только ноутбук — громоздко, а написанием мобильных приложений ни друг, ни я никогда не занимались.

И тут я вспомнил, что видел в сети проекты тв-терминалов на микроконтроллере AVR. В голове сразу появилась идея объединить маленькие символы в большие и мы решили попробовать. Как-то само собой получилось, что основную работу пришлось делать мне.

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

Найдено было много проектов, но оказалось, что большинство из них критериям не особо соответствуют. Впоследствии стало ясно, что главное — понять принцип формирования видеосигнала, а дальше дело пойдет. Но на данном этапе безусловным фаворитом стал проект Максима Ибрагимова «Простой VGA/видео адаптер» [1], он и лег в основу моей поделки. Однако, в процессе работы от него осталась только структура, реализацию пришлось переделать практически полностью.

Дополнительной задачей, которую я практически сам себе придумал, стало задание начального времени с ИК-пульта.

В качестве основного контроллера я решил использовать ATMega168, работающий на 20МГц. Аппаратная часть формирователя видеосигнала выглядит так:

схема формирователя видеосигнала

Начал я с того, что выкинул из проекта все, что касается VGA, так как его делать не планировал. Попутно изучал стандарты кодирования видеосигнала, наиболее доступной мне показалась картинка с сайта Мартина Хиннера [2]:

image.

По этой картинке делал генератор сигнала синхронизации.

В основе генератора — Timer1 в режиме fastPWM. Дополнительно глобальной переменной организован счетчик синхроимпульсов. По каждому прерыванию переполнения таймера происходит проверка номера синхроимпульса на ключевое значение, изменение длительности следующего синхроимпульса и период следующего синхроимпульса (полная строка / половина строки). Если не требуется изменений, делаются стандартные действия — увеличивается счетчик синхроимпульсов, изменяются другие переменные.

#define

// 2. System definitions

#define Timer_WholeLine	F_CPU/15625		//One PAL line 64us
#define Timer_HalfLine	Timer_WholeLine/2	//Half PAL line = 32us
#define Timer_ShortSync Timer_WholeLine/32	//2us
#define Timer_LongSync	Timer_ShortSync*15	//30us
#define Timer_NormalSync Timer_WholeLine/16	//4us
#define Timer_blank	Timer_WholeLine/8		//8us

//Global definitions for render PAL

#define PAL_FPS	50

#define pal_first_visible_line1 40
#define pal_last_visible_line1	290 //pal_first_visible_line1+pal_row_count*pal_symbol_height

#define horiz_shift_delay 15

Инициализация таймера (фрагмент функции)

// Initialize Sync for PAL
synccount = 1;	
VIDEO_DDR |= (1<<SYNC_PIN);
OCR1B = Timer_LongSync;
TCCR1A = (1<<COM1B1)|(1<<COM1B0)|(0<<WGM10)|(1<<WGM11);	//Fast PWM,Set OC1B on Compare Match,
														// clear OC1B at BOTTOM (inverting mode)  
TCCR1B = (1<<WGM12)|(1<<WGM13)|(1<<CS10);				//full speed;TOP = ICR1
ICR1 = Timer_HalfLine;					//Начинаем с двух прерываний на строку.
TIMSK1 = (1<<OCIE1B);				//enable interrupt from
row_render=0;
y_line_render=0;

Генератор синхросигнала

//генератор синхросигнала
volatile unsigned int synccount;		//  счетчик импульсов синхронизации

EMPTY_INTERRUPT (TIMER1_COMPB_vect);

void MakeSync(void)
{
	switch (synccount)
	{
		case 5://++++++++++++++++++++++++++++++++++++++++++++++++++++++++=
			Sync=Timer_ShortSync;
			synccount++;
			break;
		case 10://++++++++++++++++++++++++++++++++++++++++++++++++++++++++
			ICR1 = Timer_WholeLine;
			Sync= Timer_NormalSync;
			synccount++;
			break;
		case 315://++++++++++++++++++++++++++++++++++++++++++++++++++++++++
			ICR1 = Timer_HalfLine;
			Sync= Timer_ShortSync;
			synccount++;
			break;
		case 321://++++++++++++++++++++++++++++++++++++++++++++++++++++++++
			Sync=Timer_LongSync;
			synccount=1;
			framecount++;
			linecount = 0;
			break;
		default://++++++++++++++++++++++++++++++++++++++++++++++++++++++++
		synccount++;
			video_enable_flg = ((synccount>pal_first_visible_line1)&&(synccount<pal_last_visible_line1));
			break;
	}
}

сигнал кадровой синхронизации стандарта PAL

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

Вывод видеосигнала организован через SPI, работающий на максимальной частоте, равной половине частоты тактового сигнала.

#define

#define SPI_PORT	PORTB
#define SPI_DDR		DDRB
#define MOSI	PORTB3
#define MISO	PORTB4
#define SCK		PORTB5

//Вывод видео
#define VIDEO_PORT	SPI_PORT
#define VIDEO_DDR	SPI_DDR
#define VIDEO_PIN 	MOSI

#define VIDEO_OFF DDRB=0b00100100; 
#define VIDEO_ON DDRB=0b00101100;

Инициализация SPI (фрагмент)

	//Set SPI PORT DDR bits
	VIDEO_DDR |= (1<<MOSI)|(1<<SCK)|(0<<MISO);
	SPSR = 1 << SPI2X;
	SPCR = (1 << SPE) | (1 << MSTR); //SPI enable as master ,FREQ = fclk/2

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

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

разрывы при передаче через SPI

Маленькое пояснение

Даже немного не так. Выход MOSI остается в высоком уровне после передачи байта, а на этой фотке выход видео включен через инвертор 74НС04, а байты шрифтов инвертируются перед выдачей, поэтому разрывы черные. Без инвертора получаются белые вертикальные полоски.

Чтобы победить этот недостаток, пришлось воспользоваться трюком, предложенным в проекте TellyMate [3], который заключается в переключении ножки вывода видео в высокоимпедансное состояние, когда нужно, и таким образом повторять последний бит в выводимом байте. Эта часть функции очень критична по времени и отказ от ассемблера привел к необходимости использовать бубен найти хитрое решение.

Функция вывода строки

inline void DrawString (unsigned char *str_buffer[], struct FONT_INFO *font, unsigned char str_symbols)
{
		unsigned char symbol_width;
		//unsigned char symbol_heigth;
		
		unsigned char i;
		unsigned char * _ptr;
		unsigned char * _ptr1;
		
		//symbol_heigth = font->height;
			
		y_line_render++;
		//Set pointer for render line (display buffer)
		_ptr = &str_buffer[row_render * str_symbols];
		
		unsigned char j;
		register unsigned char _S;
		unsigned char _S1;
		
		//Cycle for render line
		i = str_symbols;
		while(i--)
		{
			symbol_width = font->width[(* _ptr)];
			//Set pointer for render line (character generator)
			_ptr1 = &font->bitmap[font->offset[* _ptr]+y_line_render*symbol_width];
		
			_S1 = 0;					//предыдущий байт
			_S = pgm_read_byte(_ptr1); //текущий байт
			_ptr1++;
			
			j=symbol_width;					//вывод одного символа
			while (1)
			{
				if (_S1 & 0b1)
					{
						goto matr;
					}
				VIDEO_OFF;
matr:			NOP;
				SPDR = _S;
				VIDEO_ON;
				_S1 = _S;
				_S = pgm_read_byte(_ptr1++);	
				NOP;	
				NOP;
				if (!--j) break;
			}
			_ptr++;
			VIDEO_OFF;			
		}
		
}

После того, как изображение было получено, стало ясно, что ни о каком приеме и разборе ИК-посылок с пульта не может идти речи, просто не хватит скорости, поэтому оставил прием команд по UART. Приемом ИК займется другой микроконтроллер.

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

Также имеется структура, описывающая шрифт, для более простого доступа из программы.

Шрифт

// Character bitmaps for Digital-7 Mono 120pt
const unsigned char PROGMEM Digital7_Bitmaps[] =
{
	// @0 '0' (71 pixels wide)
	0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x80, //                 #############################################   #
	0x00, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0xE0, //               ###############################################   ###
	0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xF0, //              ###############################################   #####
	0x00, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF1, 0xF8, //             ################################################   ######
...
...
}

const unsigned char Digital7_Height = 105;

const unsigned char Digital7_Width[] =
{
	9, 		/* 0 */
	9, 		/* 1 */
	9, 		/* 2 */
	9, 		/* 3 */
	9, 		/* 4 */
	9, 		/* 5 */
	9, 		/* 6 */
	9, 		/* 7 */
	9, 		/* 8 */
	9, 		/* 9 */
	3 		/* : */
};

const unsigned int Digital7_Offset[] =
{
	0	, 		/* 0 */
	945, 		/* 1 */
	1890, 		/* 2 */
	2835, 		/* 3 */
	3780, 		/* 4 */
	4725, 		/* 5 */
	5670, 		/* 6 */
	6615, 		/* 7 */
	7560, 		/* 8 */
	8505, 		/* 9 */
	9450 		/* : */
};

Шрифты генерировал программой DotFactory [4].

Во время невидимой части кадра делается ход часов и таймера, а также реакция на команды, полученные по UART.

Прием по UART

unsigned char clock_left;
bool clock_set;

volatile unsigned char MinTens, MinOnes;
volatile unsigned char SecTens, SecOnes;

static void pal_terminal_handle(void)
{
	unsigned char received_symbol = 0;
	// Parser received symbols from UART
	while(UCSR0A & (1<<RXC0))
	{
		received_symbol = UDR0;
		if (received_symbol=='#')
			{
				clock_left=5;
				clock_set = true;
			}
		if ((received_symbol>0x2F)&&(received_symbol<0x3A))
			{
				if (clock_set)
					{
						time_array[5-clock_left] = received_symbol - 0x30;
						clock_left--;
						if (clock_left==3)
							{
								clock_left--;
							}
						if (clock_left==0)
							{
								time_array[6] = 0;
								time_array[7] = 0;
								clock_set = false;
							}
					}
				else
					{
						if ((pause==0)||_Stop)
						{
							MinTens = 0;
						}
						else
						{
							MinTens = MinOnes;
						}
						MinOnes = received_symbol - 0x30;
						SecTens = 0;
						SecOnes = 0;
						pause = 4;
						_Stop = false;
						
						str_array[0] = MinTens;
						str_array[1] = MinOnes;
						str_array[2] = 0x0A;
						str_array[3] = SecTens;
						str_array[4] = SecOnes;
					}
				//time_array[] = {1, 2, 10, 5, 5};
				
			}
	}
}

Функция Main();

volatile bool _Stop;

struct FONT_INFO
{
	unsigned char height;
	unsigned char * bitmap;
	unsigned int * offset;
	unsigned char * width;
} Digital7, comdot;

int main(void)
{	
    avr_init();
	
	//fonts
	Digital7.bitmap = &Digital7_Bitmaps;
	Digital7.height = Digital7_Height;
	Digital7.offset = &Digital7_Offset;
	Digital7.width = &Digital7_Width;
	
	comdot.bitmap = &comdotshadow_Bitmaps;
	comdot.height = comdotshadow_Height;
	comdot.offset = &comdotshadow_Offset;
	comdot.width = &comdotshadow_Width;

	MinTens = 0;
	MinOnes = 0;
	SecTens = 0;
	SecOnes = 0;
	
	str_array[0] = MinTens;
	str_array[1] = MinOnes;
	str_array[2] = 0x0A;
	str_array[3] = SecTens;
	str_array[4] = SecOnes;
	
	unsigned char *semicolon = &time_array[2];
	sei();
	
    while (1) 
    {
		sleep_mode();
		MakeSync();

		if (UCSR0A & (1<<RXC0))
		{
			//Parse received symbol
			pal_terminal_handle();
			//Can easealy add here RX polling buffer
			//to avoid display flickering
			continue;
		}
		//Check visible field
		if(video_enable_flg)
			{
				linecount++;
				//OK, visible
				//Main render routine
#define firstline	36
#define secondline  200
				//To make horizontal shift rendered image
				unsigned char k;
				for (k=horiz_shift_delay; k>0; k--)
					{
						NOP;
					}
				if ((linecount == firstline)||(linecount == secondline))
					{
						row_render = 0;
						y_line_render = 0;
					}
	
				if ((linecount> firstline) && (linecount< firstline+(Digital7.height)))
					{
						DrawString(&str_array, &Digital7, 5);	
					}
				if ((linecount> secondline) && (linecount< secondline+(comdot.height)))
					{
						DrawString(&time_array, &comdot, 5);
					}
								
			}
		else
		{
		//Not visible
		//Can do something else..	
		//You can add here your own handlers..
// 			VIDEO_OFF;
			if (framecount==PAL_FPS)
				{
				framecount=0;
				//=========================================
				if (*semicolon== 11)
					{
						*semicolon=10;
					}
				else
					{
						*semicolon=11;
					}
				if (++time_array[7] == 10)
					{
						framecount = 1;// коррекция секунд
						time_array[7]=0;
						if (++time_array[6]==6)
							{
								framecount = 3; // коррекция секунд
								time_array[6]=0;
								if (++time_array[4]==10)
									{	
										time_array[4]=0;
										if (++time_array[3]==6)
											{
												time_array[3]=0;
												if ((++time_array[1]==4) && (time_array[0]==2))
													{
														time_array[0]=0;
														time_array[1]=0;
													}
												if (time_array[1]== 9)
													{
														time_array[1]=0;
														time_array[0]++;
													}
											}		
									}
							}
					}
				
				//=========================================
				if ((pause==0)&&(_Stop==false))
					{								
						if ((SecOnes--)==0)
						{
							SecOnes=9;
							if ((SecTens--) == 0)
							{
								SecTens = 5;
								if ((MinOnes--) == 0)
								{
									MinOnes = 9;
									if (MinTens == 0)
									{
										_Stop = true;
									}
									else
									{
										MinTens--;
									}
								}	
							}	
						}
					if (!_Stop)
						{
						str_array[0] = MinTens;
						str_array[1] = MinOnes;
						str_array[2] = 0x0A;
						str_array[3] = SecTens;
						str_array[4] = SecOnes;	
						}

					}
				else
					{
						pause--;
					}

				}
		}
		
		
    }
}

В качестве контроллера, декодирующего ИК-пульт и отправляющего команды по UART, я взял ATTiny45. Поскольку у него нет аппаратного UART, на просторах интернета была найдена очень компактная функция программного UART [5], работающего только на отправку, а также простая функция чтения команд с пульта [6] (без декодирования).

Все это было быстренько собрано в кучу и откомпилировано. Коды кнопок пульта жестко прошиты в коде. Дополнительно сделал мигание светодиода при приеме команды.

Приемник ИК и UART

/*
* Tiny85_UART.c
*
* Created: 19.04.2016 21:22:52
* Author: Antonio
*/

#include <avr/io.h>
#include «dbg_putchar.h»
#include <avr/interrupt.h>
//#include <stdlib.h>
#include <stdbool.h>

// пороговое значение для сравнения длинн импульсов и пауз
static const char IrPulseThershold = 9;// 1024/8000 * 9 = 1.152 msec
// определяет таймаут приема посылки
// и ограничивает максимальную длину импульса и паузы
static const uint8_t TimerReloadValue = 100;
static const uint8_t TimerClock = (1 << CS02) | (1 << CS00);// 8 MHz / 1024

volatile unsigned char blink = 0;

#define blink_delay 3;

volatile struct ir_t
{
// флаг начала приема полылки
uint8_t rx_started;
// принятый код
uint32_t code,
// буфер приёма
rx_buffer;
} ir;

static void ir_start_timer()
{

TCNT0 = 0;
TCCR0B = TimerClock;
}

// когда таймер переполнится, считаем, что посылка принята
// копируем принятый код из буфера
// сбрасываем флаги и останавливаем таймер
ISR(TIMER0_OVF_vect)
{
ir.code = ir.rx_buffer;
ir.rx_buffer = 0;
ir.rx_started = 0;
if(ir.code == 0)
TCCR0B = 0;
TCNT0 = TimerReloadValue;
}

ISR(TIMER1_OVF_vect)
{
if (blink==0)
{
OCR1B = 0;
}
else
{
OCR1B = 200;
blink--;
}
}

// внешнее прерывание по фронту и спаду
ISR(INT0_vect)
{
uint8_t delta;
if(ir.rx_started)
{
// если длительность импульса/паузы больше пороговой
// сдвигаем в буфер единицу иначе ноль.
delta = TCNT0 — TimerReloadValue;
ir.rx_buffer <<= 1;
if(delta > IrPulseThershold) ir.rx_buffer |= 1;
}
else{
ir.rx_started = 1;
ir_start_timer();
}
TCNT0 = TimerReloadValue;
}

void dbg_puts(char *s)
{
while(*s) dbg_putchar(*s++);
}

int main(void)
{

GIMSK |= _BV(INT0);
MCUCR |= (1 << ISC00) | (0 <<ISC01);
TIMSK = (1 << TOIE0)|(1<<TOIE1);
ir_start_timer();

dbg_tx_init();

DDRB|=_BV(PB4);

TCCR1 |= (1<<CS13)|(1<<CS12)|(0<<CS11)|(0<<CS10);
GTCCR |= (1<<COM1B1)|(0<<COM1B0)|(1<<PWM1B);
OCR1C = 255;
OCR1B = 0;
blink=0;
sei();

//dbg_puts(&HelloWorld);
while (1)
{
// если ir.code не ноль, значит мы приняли новую комманду
if(ir.code)
{
// конвертируем код в строку
//ultoa(ir.code, buf, 16);
// dbg_puts(buf); //и выводим в порт
//==================================================================
switch (ir.code)
{
case 0x2880822a: blink=blink_delay; dbg_putchar('1'); break;
case 0x8280282a: blink=blink_delay; dbg_putchar('2'); break;
case 0x8a0020aa: blink=blink_delay; dbg_putchar('3'); break;
case 0x0a00a0aa: blink=blink_delay; dbg_putchar('4'); break;
case 0x0280a82a: blink=blink_delay; dbg_putchar('5'); break;
case 0x2a888022: blink=blink_delay; dbg_putchar('6'); break;
case 0x0200a8aa: blink=blink_delay; dbg_putchar('7'); break;
case 0x0a80a02a: blink=blink_delay; dbg_putchar('8'); break;
case 0x22888822: blink=blink_delay; dbg_putchar('9'); break;
case 0x20888a22: blink=blink_delay; dbg_putchar('0'); break;
case 0x0008aaa2: blink=blink_delay; dbg_putchar('O'); break;
case 0x280882a2: blink=blink_delay; dbg_putchar('U'); break;
case 0x8880222a: blink=blink_delay; dbg_putchar('D'); break;
case 0x0808a2a2: blink=blink_delay; dbg_putchar('L'); break;
case 0xa0080aa2: blink=blink_delay; dbg_putchar('R'); break;
case 0x20088aa2: blink=blink_delay; dbg_putchar('*'); break;
case 0x220888a2: blink=blink_delay; dbg_putchar('#'); break;
default: break;
}
ir.code = 0;
//===================================================================

}
}
}

Итоговая схема получилась такая:

Схема таймера

Первую версию собрал на макетной плате с использованием кусков оргстекла в качестве корпуса.

сборка

Блок питания купил самый простой на 12В 500мА в местном магазине.

Пультик заказывал на ebay. [7]

сборка

Вот результат:

полученное изображение

Таймер используется для информирования говорящего с кафедры об отведенном времени.

использование таймера

В планах — переделать на stm32, уместить в один контроллер, оформить в корпус покрасивее.

Спасибо за внимание.

Автор: antonluba

Источник [8]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/avr/121581

Ссылки в тексте:

[1] проект Максима Ибрагимова «Простой VGA/видео адаптер»: http://www.vga-avr.narod.ru/

[2] сайта Мартина Хиннера: http://martin.hinner.info/vga/pal.html

[3] TellyMate: http://www.batsocks.co.uk/products/Other/TellyMate.htm

[4] DotFactory: https://github.com/pavius/the-dot-factory

[5] функция программного UART: http://www.avrfreaks.net/forum/code-c-avr-gcc-software-transmit-only-uart

[6] функция чтения команд с пульта: http://we.easyelectronics.ru/Soft/prostoy-universalnyy-dekoder-ik-du.html

[7] заказывал на ebay.: http://www.ebay.com/itm/400985211071?_trksid=p2057872.m2749.l2649&ssPageName=STRK%3AMEBIDX%3AIT

[8] Источник: https://habrahabr.ru/post/301598/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best