[Из песочницы] ТВ-таймер обратного отсчета на микроконтроллере AVR

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

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

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

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

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

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

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

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

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<


Генератор синхросигнала
//генератор синхросигнала
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

В конце каждой строки контроллер вгоняется в сон, по прерыванию по переполнению таймера просыпается, после чего сразу вызывается функция 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<


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

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

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

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

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

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

Функция вывода строки
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.

Во время невидимой части кадра делается ход часов и таймера, а также реакция на команды, полученные по 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<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<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, работающего только на отправку, а также простая функция чтения команд с пульта (без декодирования).

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

Приемник ИК и UART
/*
* Tiny85_UART.c
*
* Created: 19.04.2016 21:22:52
* Author: Antonio
*/

#include
#include «dbg_putchar.h»
#include
//#include
#include

// пороговое значение для сравнения длинн импульсов и пауз
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 <TIMSK = (1 << TOIE0)|(1<ir_start_timer ();

dbg_tx_init ();

DDRB|=_BV (PB4);

TCCR1 |= (1<GTCCR |= (1<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 0×2880822a: blink=blink_delay; dbg_putchar ('1'); break;
case 0×8280282a: blink=blink_delay; dbg_putchar ('2'); break;
case 0×8a0020aa: blink=blink_delay; dbg_putchar ('3'); break;
case 0×0a00a0aa: blink=blink_delay; dbg_putchar ('4'); break;
case 0×0280a82a: blink=blink_delay; dbg_putchar ('5'); break;
case 0×2a888022: blink=blink_delay; dbg_putchar ('6'); break;
case 0×0200a8aa: blink=blink_delay; dbg_putchar ('7'); break;
case 0×0a80a02a: blink=blink_delay; dbg_putchar ('8'); break;
case 0×22888822: blink=blink_delay; dbg_putchar ('9'); break;
case 0×20888a22: blink=blink_delay; dbg_putchar ('0'); break;
case 0×0008aaa2: blink=blink_delay; dbg_putchar ('O'); break;
case 0×280882a2: blink=blink_delay; dbg_putchar ('U'); break;
case 0×8880222a: blink=blink_delay; dbg_putchar ('D'); break;
case 0×0808a2a2: blink=blink_delay; dbg_putchar ('L'); break;
case 0xa0080aa2: blink=blink_delay; dbg_putchar ('R'); break;
case 0×20088aa2: blink=blink_delay; dbg_putchar ('*'); break;
case 0×220888a2: blink=blink_delay; dbg_putchar ('#'); break;
default: break;
}
ir.code = 0;
//===================================================================

}
}
}


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

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

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

сборка

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

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

сборка

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

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

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

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

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

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

© Habrahabr.ru