Как вывести картинку (живую) с микроконтроллера, логической схемы, кофеварки, чтобы ее мог видеть человек? Ха! Существует чертова куча дисплеев, будь то color, либо monochrome. А если вам нужно не просто текст выводить, а графику, может да же и видео. Гораздо универсальнее будет монитор/TV, который есть почти в каждом доме.
Я остановился на VGA интерфейсе, так как с остальными наша кофеварка не справится. Рассмотрим его с железной части.
Вот все, что нам нужно. Остальные можно смело отрезать.
Я думаю, вы уже поняли, что RBG — цвет пикселя, а HSYNC — горизонтальная и VSYNC — вертикальная синхронизации. Все они INPUT. Так как сигнал RGB идет аналоговый — нужны 3 ЦАП-а, причем учитывается, что напряжение на RGB — не должно превышать 0.7v. Вот простейшая схема, для кофеварки с напряжением питания 3.3v.
ЦАП — универсальны, можно легко увеличить или уменьшить их разрядность, путем очевидного добавления или удаления пары резисторов.
И что же делать? Немного теории…
Линии синхронизации служат для того, чтобы монитор знал, где начало кадра, какое разрешение и какова частота кадров. Аналоговые входы служат для задания цвета. Чем выше напряжение мы подаем на аналоговый вход, тем более яркий оттенок соответствующего цвета мы получим.
Картинка на экране прорисовывается построчно сверху-вниз. А каждая строка прорисовывается слева-направо, и в конце каждой строки нужно подавать горизонтальный синхроимпульс. Можно сказать, сдвиг «рисующего луча монитора» на следующую строку. Когда мы прорисовали все строчки, нужно подать вертикальный синхроимпульс, сообщая монитору, что нужно вернуть «рисующий луч» в начальное положение. Но… также существуют, так называемые «бордюры», это участки, которые не являются видимыми. Они служат для дополнительной синхронизации ЭЛТ («рисующего луча») старых мониторов, а в современных они только для соблюдения стандарта.
Истинный стандартный VGA сигнал:
- Разрешение: 640x480
- Частота обновления экрана: 60-72Гц
- Количество цветов: n
Теперь дополним нашу картину бордюрами и прочим.
Вместе с «бордюрами» получается 525 строк, из них — 480 информационные.
Время одной строки вычисляется так: 1сек/60 кадров / 525 строк = 31.75 мкс. То есть, чтобы нарисовать 640 пикселей в 1-ну строку, необходимо пулять RGB данные с частотой ~25.175MHz. С синхроимпульсами — проще, так как их можно генерировать по таймерам.
И не дай Бог, вы хотя бы чуть-чуть, хотя бы на 2 мкс ошибётесь по таймингам… монитор даст вам пинка под зад и пошлет куда подальше, со своей табличкой!
Из соображений производительности было принято решение понизить «виртуальное» разрешение экрана, а физически оставить стандарт. (1 виртуальный пиксель будет занимать несколько реальных):
- Разрешение: 256×208, 272х208, 320×240
- Количество цветов одновременно: 256
Собственно говоря, начнем.
Реализация на ARM
ArduinoDue, у нее полным полно ресурсов. Существует специальная библиотека VGA. ЦАП и прочая периферия у нас уже есть. Подключаем так:
И тестовый скетч для этой конструкции:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // Hello World Colour using VGA library by stimmer // include VGA library #include <VGA.h> void setup() { // start 320×240 mode VGA.begin(320,240,VGA_COLOUR); } void loop() { // text colour VGA.setInk(random(256)); // text background colour VGA.setPaper(random(256)); // print message VGA.print(» Hello Arduino «); VGA.waitSync(); } |
Результат 320x240x8bb:
Да, это все хорошо, но теперь ложка дегтя: на формирование vga изображения уходит чертова куча ресурсов, из-за того, что архитектура микроконтроллера не подходит для генерации VGA. По другому, использование микроскопа для забивания гвоздей.
Реализация на AVR
Достаем Arduino Duemil. или голую avr. Берем пример отсюда.
Код, опять же:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | #define NOP asm(«nop») #define BLACK PORTB = B00000000; #define BLUE PORTB = B00000001; #define GREEN PORTB = B00000010; #define CYAN PORTB = B00000011; #define RED PORTB = B00000100; #define MAGENTA PORTB = B00000101; #define YELLOW PORTB = B00000110; #define WHITE PORTB = B00000111; unsigned int linecount = 1; void setup() { //Set pins 5 to 10 as outputs // 7 — HSYNC // 6 — VSYNC // 10, 9 e 8 — RGB DDRD |= B11100000; DDRB |= B11100111; PORTD |= B11000000; //set timer TCCR2A = 0×02; // WGM22=0 + WGM21=1 + WGM20=0 = Mode2 (CTC) TCCR2B |= (1 << CS20); // TCCR2B |= (1 << CS21); // Set prescaler TCCR2B &= ~(1 << CS22); // TCNT2 = 0; // clean counter TIMSK2 &= ~(1<<OCIE2A); // set comparison interrupt TIMSK2 |= (1<<TOIE2); // set overflow interrupt } void loop() { noInterrupts(); do{ BLACK; if (TCNT2 > 0x0f){ delayMicroseconds(1); NOP;NOP;NOP;NOP; TCNT2 = 0×00; // #### HSYNC ### PORTD &= ~(1 << 7); if (++linecount >= 525){ //525 lines linecount = 1; } PORTD |= (1 << 7); // ### VSYNC ### if ((linecount == 1)||(linecount == 2)){ PORTD &= ~(1 << 6); } else { PORTD |= (1 << 6); NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP; NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP; NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP; NOP;NOP;NOP;NOP;NOP; if ((linecount >= 9) && (linecount <= 489)){ WHITE; delayMicroseconds(3);NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP; BLACK; delayMicroseconds(3);NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP; BLUE; delayMicroseconds(3);NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP; GREEN; delayMicroseconds(3);NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP; CYAN; delayMicroseconds(3);NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP; RED; delayMicroseconds(3);NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP; MAGENTA; delayMicroseconds(3);NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP; YELLOW; delayMicroseconds(3);NOP;NOP;NOP;NOP;NOP;NOP;NOP;NOP; BLACK; NOP;NOP;NOP;NOP; } } } }while(1); } |
Выводим изображение формата 800×600, разрешением 8×1. Мм-да… Это никуда не годиться. Однако, появилась идея подключить к ней SRAM, которая будет выступать в качестве видеопамяти, а МК будет всего лишь переключать адреса и генерировать синхроимпульсы. Появился концепт:
Но он так и не стал реальностью, atmega168 не могла с такой скоростью переключать адреса, в случае с разгоном — то же ничего не вышло, так как на 3.3в ничего не работало.
——
Немного позже, один добрый человек MisterDi подсказал мне, что сделать видео-контроллер — дело не хитрое, обойдется в несколько корпусов микросхем серии к555. Либо, пишет, можно использовать ПЛИС. ПЛИС? — тогда я и понятия не имел что такое ПЛИС и с чем его едят.
——
Реализация на ПЛИС
Мне посоветовали некую особу: EPM240, серии MAXII. Имеет 80-портов и 240Ле — логических элементов. Я заказал у узкоглазых друзей это чудо.
Первая ошибка, которую я допустил — не заказал программатор понадеялся на LPT. Оказалось, что через LPT на Windows 7 x64 — программировать нельзя. Поэтому я заказал еще несколько плат у сайта Marsohod. (г.Таганрог). Выражаю им огромую благодарность. Концепт видео-контроллера:
Необходимые возможности:
- Генерация видеосигнала 640×480@60Hz
- Видеопамять
- Вывод изображения из видеопамяти 272x208x8bb
- Палитра цветов 12бит (256цветов).
- Аппаратная регулировка яркости всего изображения
- Эффект Scanline (для улучшения качества изображения)
- Прямой параллельный доступ к видеопамяти по общей шине 8бит
Необходимые железки:
- ПЛИС: EPM240
- Микросхема ОЗУ 128×8: CY7C1019DV33-10ZSX
- 3 ЦАП по 4бит каждый
- Кварц 25.175 MHz для генерации VGA
- Кварц 100MHz для работы с видеопамятью и палитрой.
Опять же, выражаю благодарность форуму Марсохода, благодаря им я познал все прелести работы с ПЛИС. Представьте, какие горы можно свернуть, используя МК и ПЛИС?!
Схему подключения можете увидеть в assigment editor-e.
…
Я также развел спец плату для этого, вывел все пины и подключил ОЗУ:
После нескольких недель работы и тестирования была составлена схема в Altera Quartus:
Логика алгоритма:
- Всегда генерируем синхроимпульсы
- Когда появился сигнал горизонтальной развертки — начинаем с помощью счетчиков считать адреса ОЗУ и выводить содержимое ячеек в ЦАП.
- Во время вертикальных бордюров передаем сигнал контроллеру, что, мол, надо отправлять мне данные, и открываем доступ контроллеру к видеопамяти.
Генерация VGA:
Первое, что нам нужно — два 10-ти битных счетчика: горизонтальный и вертикальный. Постоянный меандр, с частотой 25.175MHz необходимо подавать на вход горизонтального счетчика и при его переполнении подавать импульс вертикальному. Получается координатная система XY, также для удобства представим счетчики в виде десятичных:
Как вы могли заметить, мы используем не весь 10-ти битный счетчик. При достижении 799dec — необходимо его сбросить и подать импульс на второй. Второй считает до 524dec, затем вся система сбрасывается. Но не стоит забывать, что нам необходимо еще подавать синхроимпульсы, поэтому несложно додумать алгоритм использую стандарт 640×480@60Hz:
X
- От 0 до 799, при переполнении делаем сброс и переключаем вертикальный счетчик.(vertical clock)
- От 0 до 639 — разрешаем вывод пикселей на экран, иначе — запрещаем
- От 655 до 751 — горизонтальный синхроимпульс
Y
- Считаем от 0 до 524, при переполнение делаем сброс.
- От 0 до 479 — разрешаем вывод пикселей на экран, иначе запрещаем
- От 489 до 491 — вертикальный синхроимпульс
Станет понятнее, если нарисовать таблицу:
Горизонтальный счет | Пиксели | Время~ [мкс] |
Видео-данные | 640 | 25.422045680238 |
Передний бордюр | 16 | 0.63555114200596 |
Синхроимпульс | 96 | 3.8133068520357 |
Задний бордюр | 48 | 1.9066534260179 |
Всего | 800 | 31.777557100298 |
Вертикальный счет | Линии | Время~ [мс] |
Видео-данные | 480 | 15.253227408143 |
Передний бордюр | 10 | 0.31777557100298 |
Синхроимпульс | 2 | 0.063555114200596 |
Задний бордюр | 33 | 1.0486593843098 |
Всего | 525 | 16.683217477656 |
На языке программирования Verilog это можно описать таким образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | /////////////////////////////////////////////////////////////// //module which generates video sync impulses /////////////////////////////////////////////////////////////// module hvsync ( // inputs: input wire clock, // outputs: output reg line, output reg frame, output reg hsync, output reg vsync ); //variables reg [10:0]char_count; reg [10:0]line_count; reg end_of_line; reg [2:0]pixel_st; reg [2:0]line_st; reg end_of_frame; //permanent comb computations: always @* begin //horizontal processing if(char_count < 640) pixel_st = 0; //active video else if(char_count < 656) pixel_st = 1; //front porch else if(char_count < 752) pixel_st = 2; //hsync impuls else pixel_st = 3; //back porch if(char_count < 800) end_of_line = 0; else end_of_line = 1; //vert processing if(line_count < 480) line_st = 0; //active video lines else if(line_count < 490) line_st = 1; //front porch else if(line_count < 492) line_st = 2; //vsync impuls else line_st = 3; //front porch if(line_count < 525) end_of_frame = 0; else end_of_frame = 1; end //synchronous process always @(posedge clock) begin hsync <= (pixel_st==2′b10); vsync <= (line_st==2′b10); line <= (pixel_st==3′d4); frame <= (line_st==3′d4); if(end_of_line) begin char_count <= 0; if(end_of_frame) line_count <= 0; else line_count <= line_count + 1′b1; end else begin char_count <= char_count + 1′b1; end end endmodule |
Автор этого кода синхронизации: Николай.
Вывод изображения:
Теперь у нас два варианта: выводить данные из видеопамяти по координатам XY счетчиков синхронизации или завести еще один счетчик, который будет линейно считать адреса памяти. Я предпочел второй.
Так как разрешение 272x208x8bb будет занимать 56576байт — это адресное пространство шириной 16бит — необходим 16бит счетчик.
Счет он должен начинать только тогда, когда горизонтальный и вертикальный ==0 и частота его работы должна быть равна 25.175MHz/2=12.5875MHz для уменьшения горизонтального разрешения.
- Работа от частоты пикселей /2 = ~12MHz
- Считать от 1 до 56576
- Запрещать счет, если горизонтальный счетчик находится в Бордюре/Синхроимпульсе.
- Сбрасывать счет, если вертикальный счетчик находится в Бордюре(ах)/Синхроимпульсе.
Кроме того, необходимо уменьшить вертикальное разрешение, путем удвоения горизонтальных строк. Не все так просто, как кажется, ведь нам нужно разрешение 272×208, а из 640×480 мы можем получить только 320×240, в связи с этим нужно сделать «рамку» вокруг изображения.
Для получения «рамки» — пропускаем по горизонтальному счетчику 48 строк слева и справа; по вертикальному 32 сверху и снизу.
Как придать изображению эффект Scanline?
Эффект улучшает изображение низкого разрешения. При выводе изображения нужно затемнять все четные строки, убавлять яркость. Пример: ->
В этой версии ВП этот эффект реализован немного криво. Еще можно применить hd2x, но это сложнее.
Работа системы палитр.
В видепроцессор необходимо загружать индексированные данные или изображение с палитрой. Сама матрица изображения 272×208 и 8бит на пиксель, к этой матрице необходима палитра 256цветов и по 8бит на каждый цвет — в сумме 768байт. Как это работает:
Тем самым мы сокращаем размер изображения и не теряем качество. Кроме того, мы можем изменять изображения, путем смены палитры, что гораздо быстрее, чем его перерисовывать. Яркий пример можно увидеть здесь ->/.
Регулировка яркости.
Здесь все просто. На определенном адресе SRAM находится байт управления яркостью. Он знакового типа: -127 +127. Записывая туда значения, мы можем изменять яркость изображения, не меняя палитру. В ПЛИС стоит знаковый сумматор с сатурацией, который просто складывает этот байт со значением пикселя.
Cпособ подключения.
Подключение осуществляется с помощью параллельного интерфейса с 8бит шиной данных и 16бит шиной адреса.
Также используются сигналы —CS (Выбор устройства — видеопроцеесора), чтобы можно было использовать эту шину для нескольких устройств и —WE — разрешение записи.
Логика записи данных должна быть такой:
- -CS — низкий уровень
- Установка адреса
- Установка данных
- -WE — низкий уровень
- Задержка
- -CS и —WE — высокий уровень
Либо
- -CS — низкий уровень
- Установка адреса1
- -WE — низкий уровень
- Установка данных1
- Задержка (небольшая)
- Установка адреса2
- Установка данных2
- Задержка (небольшая)
- -CS и -WE — высокий уровень
Еще есть один вывод — —VSYNC, означающий: производится ли вывод на экран. Фактически, он будет низкого уровня(LOW), когда происходят VerticalPorch, VerticalPulse, верхняя и нижняя части «рамки». Это и есть время для записи данных в видеопамять, в любой другой момент видеопроцессор просто закроет доступ к видеопамяти.
В общей сумме, максимальное время, нужное для записи будет равно: ~3.431976166832184мс, с связи этим, частота работы всей передающей шины должна быть: ~17MHz, ну а, соответственно, скорость 17МегаБайт/в сек. Сопротивление портов видеопроцессора и передатчика должны быть согласованы. (резисторы в помощь!)
Формат изображения: Bitmap, разрешением 272×208 (1байт на пиксель) передается последовательно; цветовая палитра 1байт на канал, формата [r1 r2…r256]-[g1 g2…g256]-[b1 b2…b256].
Адреса
-
0x0000 - 0xDCFF (включительно) - Bitmap
-
0xDD00 - 0xDFFF (включительно) - Palette
-
0xE000 - Яркость
Я связал этот видеопроцессор и ArduinoDue. У чипа SAM3X имеется специальная ретрансляционная параллельная шина адреса и данных для таких целей. Однако в ArduinoDue были выведены не все пины (A6 и A9), поэтому пришлось выводить их напрямую от микросхемы. ATSAM3X позволила развить скорость передачи данных 38МегаБайт/сек, возможно можно и больше, тут необходимо тонко настраивать SMC контроллер.
А код для этой картинки — очень прост:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | #include <GraphicsEngine.h> #include "bmp.h" uint8_t palette[768]; //Palette int8_t light = 0; //L #include <Parallel.h> uint8_t* gram = (uint8_t*)0x60000000; //GPU ADDR void setup() { Parallel.begin(PARALLEL_BUS_WIDTH_8, PARALLEL_CS_0, 16, false, true); Parallel.setCycleTiming(1,1); Parallel.setPulseTiming(0,0,0,0); Parallel.setAddressSetupTiming(0,0,0,0); GFX.Init(VGA_MODE_272x208); GFX.FillScreenFromBuffer(image+3); memcpy(palette, image+3+56576, 768); pinMode(53, INPUT); //FRAME (GPU) /VSYNC } void render() { while(PIO_Get( g_APinDescription[53].pPort, PIO_INPUT, g_APinDescription[53].ulPin ) == 0); while(PIO_Get( g_APinDescription[53].pPort, PIO_INPUT, g_APinDescription[53].ulPin ) == 1); memcpy(gram+56576, palette, 768); memset(gram+57344, light, 1); GFX.SendScreenTo(gram); } void loop() { render(); } |
Собственно говоря, это наилучший вариант, который я смог сделать, видео-контроллера. Напоследок демо-видео:
[Прошивка ПЛИС v.2.5]
[Прошивка ПЛИС v.2.0]
[Прошивка ПЛИС v.1.0]
Для конвертации картинок — используйте эту программу//. Читает .bmp 8bbIndexed. 272×208
Да, вы правильно поняли. Для формирования изображения использовался следующий код:
always @(posedge VGA_CTRL_CLK)//25 МГц
begin
counter = counter + 1'd1;
if (counter <320)//первые 320
begin ///////первая камера
//data_adres=0;
CCD_R=sCCD_R;//Красный канал,идет на VGA адаптер, отображаем данные с камеры
CCD_G=sCCD_G;//Зеленый канал,идет на VGA адаптер
CCD_B=sCCD_B;//Синий канал,идет на VGA адаптер
end
else if(counter640)//если больше счетчик равен нулю
begin
counter=1'b0;
end
end
Причем если выводить полное изображение проблем не возникает.
И я не могу понять, то ли неправильно формирую изображение на вход VGA контроллера(скорее всего), то ли нужно сам контроллер изменить.
И извиняюсь за плохо оформленый код, с тегами не разобрался.
Старайтесь не использовать неблокирующие присваивания «=» — это не позволяет вам контролировать время на выполнение операций.
Насчет кода, не могу найти ошибки, но попробуйте этот:
Советую вам привязывать все ваши операции с пикселями и строками к модулю синхронизации (char_count и line_count), иначе возможны расхождения в счетах. Там бы это выглядело так:
Дефект сохранился, но несколько в иной форме.
Попробую переписать код VGA контроллера подобно вашему, с привязкой координат пикселей к модулю синхронизации. Когда будет что-то определенное-обязательно поделюсь.
Спасибо за помощь.
http://vfl.ru/fotos/78050ab48636446.html
Хм… Очень странно, видимо есть еще некоторые нюансы, которые я не учел.
Насчет модуля синхронизации, то все верно, но не обращайте внимание на готовый проект в этой статье: там как раз наоборот сделано, еще один счетчик считает параллельно, что вызывает глюки.
Как вы думаете, реально ли на этой плисине вытянуть разрешение 1280×1024? Или не выдаст она 108 мегагерц / память подкачает?
Нет, не вытянет. Проблема будет в блоке палитры, частота чтения значений цветов там — 100MHz, чтобы успеть подставить необходимые компоненты (RGB) в период 24MHz.
Если же частота пикселей уже ваши ~100MHz, в таком случае память должна работать на ~400MHz, что уже не реально. Впрочем, расширяя шину данных видеопамяти, скажем, до 32бит, будет возможность снизить эти 400 до ~200. Однако с таким количеством проводников и верхней частотой MAXII вряд ли справится. 🙂