Однажды один мой друг спросил, на чем бы я сделал таймер обратного отсчета, чтобы на телевизоре показывал большие цифры. Понятно, что можно подключить ноутбук / iPad / Android и написать приложение, только ноутбук — громоздко, а написанием мобильных приложений ни друг, ни я никогда не занимались.
И тут я вспомнил, что видел в сети проекты тв-терминалов на микроконтроллере AVR. В голове сразу появилась идея объединить маленькие символы в большие и мы решили попробовать. Как-то само собой получилось, что основную работу пришлось делать мне.
Конечно, небольшой опыт разработки устройств на МК у меня есть, но всегда проще взять готовое, поэтому я начал с активного поиска готового решения вывода на телевизор. Основным критерием поиска стала, в первую очередь, простота, по возможности, использование языка С без ассемблерных вставок, высокое качество изображения.
Найдено было много проектов, но оказалось, что большинство из них критериям не особо соответствуют. Впоследствии стало ясно, что главное — понять принцип формирования видеосигнала, а дальше дело пойдет. Но на данном этапе безусловным фаворитом стал проект Максима Ибрагимова «Простой VGA/видео адаптер», он и лег в основу моей поделки. Однако, в процессе работы от него осталась только структура, реализацию пришлось переделать практически полностью.
Дополнительной задачей, которую я практически сам себе придумал, стало задание начального времени с ИК-пульта.
В качестве основного контроллера я решил использовать ATMega168, работающий на 20МГц. Аппаратная часть формирователя видеосигнала выглядит так:
Начал я с того, что выкинул из проекта все, что касается VGA, так как его делать не планировал. Попутно изучал стандарты кодирования видеосигнала, наиболее доступной мне показалась картинка с сайта Мартина Хиннера:
.
По этой картинке делал генератор сигнала синхронизации.
В основе генератора — Timer1 в режиме fastPWM. Дополнительно глобальной переменной организован счетчик синхроимпульсов. По каждому прерыванию переполнения таймера происходит проверка номера синхроимпульса на ключевое значение, изменение длительности следующего синхроимпульса и период следующего синхроимпульса (полная строка / половина строки). Если не требуется изменений, делаются стандартные действия — увеличивается счетчик синхроимпульсов, изменяются другие переменные.
// 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;
}
}
В конце каждой строки контроллер вгоняется в сон, по прерыванию по переполнению таймера просыпается, после чего сразу вызывается функция MakeSync(), задающая настройки таймера на следующий период синхронизации, после чего, если номер синхросчетчика попадает в видимую область, начинается вывод видеосигнала.
Вывод видеосигнала организован через SPI, работающий на максимальной частоте, равной половине частоты тактового сигнала.
#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;
//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 не может передавать несколько байт данных подряд. После каждого байта один бит пропускается, из-за чего возникают разрывы на изображении.
Чтобы победить этот недостаток, пришлось воспользоваться трюком, предложенным в проекте 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.
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};
}
}
}
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, работающего только на отправку, а также простая функция чтения команд с пульта (без декодирования).
Все это было быстренько собрано в кучу и откомпилировано. Коды кнопок пульта жестко прошиты в коде. Дополнительно сделал мигание светодиода при приеме команды.
* 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, уместить в один контроллер, оформить в корпус покрасивее.
Спасибо за внимание.
Автор: antonluba