ТВ-таймер зворотного відліку на мікроконтролері 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<<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); //interrupt enable 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, який полягає в переключенні ніжки виведення відео в высокоимпедансное стан, коли потрібно, і таким чином повторювати останній біт в виводиться байті. Ця частина функції дуже критична по часу і відмова від асемблера призвів до необхідності використовувати бубон знайти хитре рішення.

Функція виводу рядка
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<<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.. 
//Here You can add 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 <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.

збірка

Ось результат:

отримане зображення

Таймер використовується для інформування говорить з кафедри про відведеному часу.

використання таймера

У планах — переробити на stm32, вмістити в один контролер, оформити в корпус красивіше.

Дякую за увагу.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.