JustPaste.it

Хакер - Теплый ламповый дисплей. Собираем монитор из электронно-лучевой трубки

hacker_frei
794a4f73c77f596a85ceaf01cfa1cb3d.png

 

https://t.me/hacker_frei

Candidum

Содержание статьи

  • Электронно-лучевая трубка
  • Выбор трубки
  • Питание и обвязка трубки
  • Видеоусилитель
  • ЦАП
  • Bluepill и фигуры Лиссажу
  • USB/SPI и фигуры Лиссажу
  • Графика
  • Выводим текст
  • Заключение

Мо­нитор с клас­сичес­ким кинес­копом сей­час мож­но отыс­кать раз­ве что в музее или на бло­шином рын­ке, да и там они попада­ются нечас­то. Тем инте­рес­нее пос­тро­ить такой сво­ими руками! В этой статье мы запус­тим элек­трон­но‑лучевую труб­ку, пос­мотрим на фигуры Лис­сажу, выведем текст и изоб­ражение. Будет мно­го схе­мотех­ники и кода.

На­вер­няка все видели часы на элек­трон­но‑лучевых труб­ках или экра­нах осциллог­рафов. Пос­ле неоно­вых часов это, навер­ное, сле­дующая по популяр­ности лам­повая замороч­ка. Обыч­но, кро­ме цифер­бла­та и нес­коль­ких цифр, на него ничего не выводят, и это таки не слу­чай­но! Собс­твен­но, о при­чинах дан­ного явле­ния мы и будем даль­ше мно­го говорить, а так­же обсу­дим те осо­бен­ности тру­бок и схе­мотех­ники, о которых обыч­но в интерне­те не пишут.

INFO

На­зывать ради­олам­пы труб­ками, элек­трон­ными труб­ками, ваку­умны­ми труб­ками и кла­пана­ми без­гра­мот­но. Прос­какива­ют такие наз­вания обыч­но по вине горе‑перевод­чиков, одна­ко в слу­чае элек­трон­но‑лучевых тру­бок и рен­тге­нов­ских тру­бок наз­вание плот­но уко­рени­лось и теперь общепри­нято. Такие дела.

ЭЛЕКТРОННО-ЛУЧЕВАЯ ТРУБКА

Элек­трон­но‑лучевые труб­ки, пожалуй, самые слож­ные ради­олам­пы в пла­не устрой­ства и управле­ния. Пред­назна­чены они, как нес­ложно догадать­ся, для вывода изоб­ражения. Здесь и далее мы будем говорить толь­ко об осциллог­рафичес­ких труб­ках с элек­трос­татичес­кой фокуси­ров­кой и элек­трос­татичес­ким откло­нени­ем.

297d928da740652876dec18467096494.gif

Как все‑таки работа­ет такая лам­па? Элек­тро­ны испуска­ются катодом, пос­ле чего про­ходят через сис­тему фокуси­ров­ки, которая в прос­тей­шем слу­чае сос­тоит из трех элек­тро­дов, как на рисун­ке выше. Пер­вый элек­трод управля­ет яркостью, вто­рой фокуси­ров­кой, а тре­тий, уско­ряющий, отве­чает за астигма­тизм. Пос­ле это­го пучок про­лета­ет через две пары откло­няющих элек­тро­дов, отве­чающих за горизон­таль­ное и вер­тикаль­ное откло­нение.

За­тем элек­тро­ны долета­ют до слоя люмино­фора и зас­тавля­ют его све­тить­ся. Если фокуси­ров­ка нас­тро­ена, то на экра­не горит точ­ка, положе­ние которой опре­деля­ется нап­ряжени­ем на откло­няющих элек­тро­дах. Изме­няя это нап­ряжение, мы можем выводить изоб­ражение. Но это все общие сло­ва, теперь перей­дем к кон­кре­тике, о которой обыч­но не пишут, раз­ве что на темати­чес­ких форумах.

ВЫБОР ТРУБКИ

Для экспе­римен­тов я выб­рал труб­ку 6ЛО1И. Мотиви­рова­ла меня ее низ­кая сто­имость (мне этот девайс обо­шел­ся в 400 руб­лей) и ее ком­пак­тность.

f89a05a9f49bc8913977809d57b44fcd.jpg

Од­нако уже во вре­мя сбор­ки и нас­трой­ки я осоз­нал, нас­коль­ко это пло­хой выбор, ведь имен­но из‑за исполь­зования 6ЛО1И я стол­кнул­ся с таким количес­твом труд­ностей. А дело в том, что у труб­ки есть такой показа­тель, как чувс­тви­тель­ность откло­няющей сис­темы. Изме­ряет­ся она в мил­лимет­рах на вольт, и у 6ЛО1И это зна­чение сос­тавля­ет око­ло 0,15 мм/В, для оси X — чуть мень­ше, для оси Y — чуть боль­ше. Такая чувс­тви­тель­ность край­не низ­кая, и для дви­жения луча по горизон­тали от левого края экра­на до пра­вого нуж­но поряд­ка 250 В, а по вер­тикали око­ло 200 В. Это доволь­но мно­го и тре­бует от виде­оуси­лите­ля очень хороше­го быс­тро­дей­ствия. Собс­твен­но, если пос­мотреть, что имен­но выпус­кала про­мыш­ленность на этих труб­ках, то ста­новит­ся ясно, что это были «показо­мет­ры» с шириной полосы не более нес­коль­ких десят­ков килогерц, нап­ример ОМШ-3М.

Здесь, прав­да, мож­но нем­ного схит­рить и понизить анод­ное нап­ряжение на труб­ке с пас­пор­тно­го 1200 В до, ска­жем, 700–1000 В. Яркость при этом сни­зит­ся, а чувс­тви­тель­ность откло­няющей сис­темы замет­но воз­растет, и в дан­ном слу­чае это разум­ный ком­про­мисс. В общем, советую взять труб­ку поп­рилич­нее — это силь­но упростит ковыря­ния с виде­оуси­лите­лем.

Но есть у 6ЛО1И и дос­тоинс­тва: устрой­ство ее нес­ложное, поэто­му и схе­ма питания у нее прос­тая.

ПИТАНИЕ И ОБВЯЗКА ТРУБКИ

Пе­ред тем как изоб­ретать свой велоси­пед, неп­лохо бы озна­комить­ся с уже изоб­ретен­ными вари­анта­ми. По уму, конеч­но, сто­ило бы соб­рать для анод­ного нап­ряжения импуль­сник со ста­били­заци­ей. Но пос­коль­ку для накала нуж­но 6,3 В, а в осциллог­рафичес­ких труб­ках боль­шая часть высоко­го нап­ряжения пода­ется на катод, то есть потен­циал катода око­ло –900 В, источник питания накала дол­жен быть надеж­но изо­лиро­ван от мас­сы. Про­ще все­го про­вер­нуть этот финт, исполь­зуя накаль­ную обмотку.

А раз уж нуж­на накаль­ная обмотка, зна­чит, тран­сфор­матор будет содер­жать и анод­ную обмотку, поэто­му высокое нап­ряжение мож­но получить умно­жите­лем. Как говорит­ся, 1000 В — это все­го лишь три раза по 330 В. Поэто­му, вдох­новив­шись про­ектом прос­того осциллог­рафа на 6ЛО1И, я раз­работал свою схе­му, в которой от исходной остался толь­ко кон­цепт.

WARNING

Раз­ность потен­циалов меж­ду положи­тель­ным и отри­цатель­ным пле­чами источни­ка питания пре­выша­ет 1000 В! Удар таким нап­ряжени­ем смер­тель­но опа­сен, а кро­ме того, это очень боль­но. Поэто­му будь край­не вни­мате­лен и осто­рожен! А если нет опы­та в работе с высоким нап­ряжени­ем, воз­можно, луч­ше и не свя­зывать­ся с этим бло­ком питания. Я пре­дуп­редил.

35d2b34da1b8eba6942059e95ba12d7c.png

Ос­новой бло­ка питания слу­жит 30-ват­тный торо­идаль­ный тран­сфор­матор с дву­мя обмотка­ми, накаль­ной и анод­ной. Анод­ная обмотка выда­ет 235 В, которые пос­тупа­ют на вып­рямитель и умно­житель](https://en.wikipedia.org/wiki/Voltage_multiplier). Вып­рямитель при­менен одно­полу­пери­одный, так как он хорошо сочета­ется с умно­жите­лем, а токи пот­ребле­ния схе­мы око­ло 0,5 мА. На выходе вып­рямите­ля получа­ем око­ло +330 В. На выходе умно­жите­ля име­ем, соот­ветс­твен­но, око­ло –660 В, что в сум­ме дает нам 1000 В — впол­не дос­таточ­ное нап­ряжение для работы труб­ки.

Об­рати вни­мание на резис­торы, шун­тиру­ющие кон­денса­торы вып­рямите­ля и умно­жите­ля: они могут сущес­твен­но прод­лить твою жизнь, пос­коль­ку кон­денса­торы — шту­ка ковар­ная (см. пре­дуп­режде­ние). Вооб­ще говоря, нес­мотря на пас­пор­тное анод­ное нап­ряжение 1200 В, 6ЛО1И работа­ет и от 1000 В, и даже от 500 В. При этом повыша­ется чувс­тви­тель­ность откло­няющей сис­темы и сни­жает­ся яркость све­чения.

При 1000 В яркость впол­не при­лич­ная. Обвязка самой 6ЛО1И впол­не стан­дар­тная, как и в упо­мяну­том выше про­екте. Сто­ит так­же обра­тить вни­мание, что к обще­му про­воду под­клю­чен не выход вып­рямите­ля, а сред­няя точ­ка делите­ля на резис­торах R5/R6. Это нуж­но, что­бы при­под­нять нап­ряжение на откло­няющих элек­тро­дах при исполь­зовании окон­чатель­ного вари­анта виде­оуси­лите­ля.

Де­ло в том, что нап­ряжение на вто­ром ано­де (астигма­тизм) дол­жно быть чуть ниже, чем на откло­няющих элек­тро­дах. Если нап­ряжение на них низ­ковато, то и на вто­ром ано­де его при­дет­ся занижать, в резуль­тате пада­ет яркость, исполь­зование же делите­ля поз­воля­ет обой­ти эту проб­лему. Да, нас­трой­ки яркости, фокуса и астигма­тиз­ма вли­яют друг на дру­га. Если вклю­чить устрой­ство на этом эта­пе, пос­ле прог­рева на экра­не появит­ся точ­ка, которую мож­но сфо­куси­ровать. Сиг­нал пода­ется на откло­няющие плас­тины, выводы 10, 11 опре­деля­ют откло­нение по оси Y, выводы 7, 8 — откло­нение по оси X. Теперь перей­дем к виде­оуси­лите­лю.

5515503e23cd0d408a5be6d86aa4e080.jpg
95afeded327d203b1c7f80878eb5e037.jpg

ВИДЕОУСИЛИТЕЛЬ

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

a20a83d9de7de5aa55465a584055184f.gif

Од­нако это решение неудоб­но, так как тре­бует допол­нитель­ного сме­щения на базу пер­вого тран­зисто­ра, в про­тив­ном слу­чае кас­кад работа­ет в нелиней­ном режиме, что совер­шенно неп­рием­лемо. Хотя если хочет­ся пос­мотреть фи­гуры Лис­сажу, а в качес­тве источни­ка сиг­нала исполь­зовать завод­ской ГСС, где мож­но задать сме­щение в пару вольт отно­ситель­но зем­ли, то такое решение впол­не рабочее. Изба­вить­ся от необ­ходимос­ти внеш­него сме­щения мож­но, исполь­зуя дву­поляр­ное питание, что я и сде­лал.

9eb848f93b357bb7995ff479dc2a393f.png

Уси­литель Y-канала иден­тичен. Как видишь, здесь появил­ся еще один источник питания — 5 В, это усложня­ет блок питания, но реша­ет проб­лему сме­щения, поэто­му на вход мож­но подавать сиг­нал непос­редс­твен­но с ЦАПа. Этот вари­ант уси­лите­ля чрез­вычай­но прост и под­ходит для экспе­римен­тов с труб­кой, одна­ко име­ет сущес­твен­ные огра­ниче­ния. И это в пер­вую оче­редь быс­тро­дей­ствие. Так, полоса про­пус­кания дан­ного уси­лите­ля будет око­ло 10 кГц, и выше этой час­тоты уси­ление дос­таточ­но быс­тро сни­жает­ся.

И что с того, спро­сишь ты? А из это­го сле­дует, что количес­тво сем­плов ЦАПа будет огра­ниче­но полосой про­пус­кания, что, в свою оче­редь, будет огра­ничи­вать раз­мер изоб­ражения (количес­тво точек), которое мож­но отри­совать без мер­цания. В дан­ном слу­чае количес­тво точек будет поряд­ка 500. А если под­нять час­тоту ЦАП, то изоб­ражение будет иска­жать­ся.

С дру­гой сто­роны, нес­коль­ко сотен точек впол­не дос­таточ­но для отри­сов­ки цифер­бла­та и стре­лок, нес­ложной геомет­ричес­кой кар­тинки или тех же фигур Лис­сажу. Собс­твен­но, в боль­шинс­тве конс­трук­ций подоб­ное изоб­ражение и выводят. А что делать, если мы хотим боль­шего, нап­ример вывес­ти на экран дос­таточ­но слож­ную кар­тинку в пару десят­ков тысяч точек? Для это­го при­дет­ся под­нимать быс­тро­дей­ствие, и самый прос­той спо­соб это сде­лать — под­нять токи выход­ного кас­када.

Кро­ме того, сто­ит иметь в виду, что кол­лектор­ные резис­торы вмес­те с емкостью откло­няющей сис­темы и выход­ной емкостью тран­зисто­ра обра­зуют RC ФНЧ, час­тоту сре­за которо­го мож­но при­мер­но при­кинуть, взяв емкость, ска­жем, 15 пФ. На прак­тике получа­ется замет­но хуже, чем в теории, ну да это как всег­да. Для резис­торов 220 К получа­ется зна­чение 48,25 кГц, а для резис­торов 3 К уже 3,54 МГц — то, что надо.

Нес­коль­ко усложним схе­му, исполь­зовав кас­кодное вклю­чение тран­зисто­ров. Такое вклю­чение поз­воля­ет сде­лать схе­му менее кри­тич­ной к парамет­рам высоко­воль­тных тран­зисто­ров. В целом кас­код работа­ет как иде­али­зиро­ван­ный кас­кад с общим эмит­тером. Нас, конеч­но, это не спа­сет, пос­коль­ку мы все рав­но упремся в парамет­ры труб­ки, зато поз­волит исполь­зовать дешевые высоко­воль­тные тран­зисто­ры в вер­хнем пле­че, нап­ример MJE13003, MJE13005. Одна­ко луч­ше все‑таки 2SC2611 или КТ940А.

Кро­ме того, добавим источник тока в эмит­терные цепи — так и работа­ет луч­ше, и нас­тра­ивать гораз­до удоб­нее. А сверх того на вход пос­тавим исто­ковые пов­торите­ли, что­бы не шун­тировать ЦАП. В пер­вом вари­анте схе­мы их не было, одна­ко ока­залось, что уси­литель замет­но шун­тировал ЦАП и силь­но про­сажи­вал нап­ряжение, потому пов­торите­ли приш­лось добавить.

b8a3c46ba4bd199fe015bda3e26c2f63.png

Дан­ный уси­литель обес­печива­ет полосу око­ло 1,5 МГц при раз­махе сиг­нала на выходе каж­дого пле­ча 75 В и уси­лении око­ло 15. При этом замена тран­зисто­ров на MJE13005 дает при­мер­но такой же резуль­тат, и улуч­шить его малыми уси­лиями уже не получит­ся. Нас­трой­ка уси­лите­ля сво­дит­ся к подс­трой­ке источни­ков тока резис­торами RV2 и RV5: нуж­но добить­ся на кол­лекто­рах тран­зисто­ров Q2, Q5, Q7, Q10 нап­ряжения чуть выше полови­ны питания (око­ло 120 В), а так­же к под­бору кон­денса­торов час­тотной кор­рекции С3, С6, С9, С12.

Сто­ит заметить, что раз мы собира­ем не осциллог­раф, а монитор, то добивать­ся ров­ной АЧХ уси­лите­ля — не опти­маль­ное решение. Поэто­му под­бор кон­денса­торов удоб­но вес­ти, смот­ря на качес­тво изоб­ражения, добива­ясь минималь­ных арте­фак­тов. Методи­ка под­бора кон­денса­торов доволь­но прос­тая — начав с заведо­мо мень­шей емкости, нап­ример 1 нФ, необ­ходимо пос­ледова­тель­но уве­личи­вать емкость в два раза, наб­людая изме­нения изоб­ражения. Ког­да емкость ока­жет­ся чрез­мерной, начинай ее умень­шать на полови­ну пре­дыду­щего шага, таким обра­зом шагов за пять мож­но подоб­рать нуж­ное зна­чение. Обра­ти вни­мание, что эмит­терные резис­торы в каналах X и Y раз­личны и кон­денса­торы кор­рекции, соот­ветс­твен­но, тоже. Токи тран­зисто­ров так­же мож­но нас­тра­ивать, ори­енти­руясь на изоб­ражение.

Конс­трук­ция получи­лась дос­таточ­но слож­ной (12 тран­зисто­ров), а еще она замет­но гре­ется, поэто­му нужен хороший ради­атор. Мой, конеч­но, дико избы­точен, но он мне попал­ся под руку и под­ходил по раз­мерам. Резис­торы в кол­лектор­ной цепи так­же силь­но гре­ются, поэто­му надо взять пятиват­тные (не про­волоч­ные!). Хорошо, уси­литель есть, теперь нужен источник сиг­нала.

875935f5ffe7b7825881d91e81ad2652.jpg

ЦАП

C точ­ки зре­ния соот­ношения цена/быс­тро­дей­ствие луч­шее решение — R-2R ЦАП. Пер­воначаль­но я пла­ниро­вал исполь­зовать Blue Pill как источник сиг­нала, и в этом слу­чае мож­но задей­ство­вать целый порт сра­зу на 2 ЦАПа (каналы X и Y). Одна­ко, ори­енти­руясь на дан­ный про­ект, я решил при­менить сдви­говые регис­тры 74HC59. В пла­не быс­тро­дей­ствия мы ничего не теря­ем, так как GPIO в stm32f103 работа­ют с час­тотой око­ло 2 МГц, и то при пря­мой записи в регис­тры, через обер­тки получа­ется нес­коль­ко мед­леннее. А вот шина SPI недур­но работа­ет на час­тоте 32 МГц, и ито­ге для двух 8-бит­ных каналов получа­ем 2 мегасем­пла в секун­ду, при этом ЦАП мож­но исполь­зовать незави­симо с дру­гими источни­ками сиг­нала. А кро­ме того, ЦАП на 74HC595 выда­ет сиг­нал до 5 В, что, учи­тывая низ­кую чувс­тви­тель­ность труб­ки, нам толь­ко на руку.

8af48754ba9b2720a8b77d1cf2a8d8b4.png

Сна­чала, конеч­но, схем­ка была поп­роще, в ней при­сутс­тво­вали толь­ко сдви­говые регис­тры. Мик­рокон­трол­лер писал в SPI два бай­та, а потом дер­гал нож­ку RCLK, и все было хорошо, все работа­ло. Потом мне захоте­лось вымодить мас­сивы поболь­ше, которые не помеща­лись в память кон­трол­лера, и тут было два вари­анта: при­ладить к кон­трол­леру флеш­ку, или под­клю­чить к ком­пу через USB/SPI. Я выб­рал вто­рой вари­ант, а в качес­тве USB/SPI исполь­зовал FT232H.

Это самый быс­трый USB/SPI из мне извес­тных, а кро­ме того, его мож­но при­обрести в виде готово­го модуля за тер­пимые день­ги (ну, некото­рое вре­мя назад так и было). Одна­ко у FT232H есть та же проб­лема, что и у кон­трол­лера SPI: порт работа­ет быс­тро, а GPIO мед­ленно, при­чем гораз­до мед­леннее, чем в кон­трол­лере, поэто­му дер­гать нож­ку регис­тра на каж­дые два бай­та неразум­но. Приш­лось малой кровью допилить недо-SPI 74HC595 до «поч­ти SPI». Идея дос­таточ­но прос­та: надо счи­тать так­товые импуль­сы и каж­дый 16-й дер­гать RCLK. Для это­го соб­ран делитель на 16 на четырех D-триг­герах. А что­бы знать, отку­да счи­тать импуль­сы, по сиг­налу CS про­исхо­дит уста­нов­ка триг­геров, что сра­баты­вает как син­хро­низа­ция.

Ко­неч­но, делитель про­ще было соб­рать на 74HC4040, но это как‑нибудь в дру­гой раз. Так или ина­че, мы получи­ли ЦАП, спо­соб­ный выдавать до 2 мегасем­плов в секун­ду, при­чем его ско­ростью мож­но управлять, меняя ско­рость шины SPI. О резис­торах мож­но ска­зать, что исполь­зовать резис­торы одно­го номина­ла удоб­но: получа­ешь пра­виль­ное соот­ношение соп­ротив­лений 1/2. В прин­ципе, мож­но сэконо­мить и исполь­зовать резис­торы 5К1 и 10К. Нем­ного пос­тра­дает линей­ность, что на глаз поч­ти незамет­но, впро­чем, эко­номия копе­ечная и того не сто­ит.

b2c5bb498b399cf3c6269bdd40cd01e9.jpg

BLUEPILL И ФИГУРЫ ЛИССАЖУ

Ана­лого­вая часть соб­рана, и ЦАП у нас есть. Вре­мя про­верить, как оно работа­ет. Самый прос­той тес­товый сиг­нал для соз­дания изоб­ражения — это два синуса с раз­ными час­тотами или фазами. Про­ще все­го такой сиг­нал взять с ГСС и подать на вхо­ды виде­оуси­лите­ля, одна­ко если ГСС под рукой нет, то сиг­нал мож­но сге­нери­ровать в мик­рокон­трол­лере бук­валь­но нес­коль­кими десят­ками строк.

Ге­нери­ровать синус в мик­рокон­трол­лере мож­но тре­мя спо­соба­ми. Во‑пер­вых, исполь­зуя биб­лиоте­ку math.h и фун­кцию sin(), одна­ко это далеко не луч­ший вари­ант по быс­тро­дей­ствию и рас­ходова­нию ресур­сов. Работа с пла­вающей точ­кой — это не то, для чего пред­назна­чены мик­рокон­трол­леры, впро­чем, дан­ный метод работа­ет. Дру­гой дос­таточ­но инте­рес­ный вари­ант генера­ции синуса — на осно­ве раз­нос­тных схем — упо­мина­ется здесь. Урав­нения там дос­таточ­но прос­тые, и с пер­вого взгля­да даже не ска­жешь, что на выходе получа­ется синус.

V -= X*R

X += V

Здесь R — это кон­стан­та. На осоз­нание вывода этих фор­мул меня не хва­тило, впро­чем, даже в момент окон­чания уни­вера гра­ница моих матема­тичес­ких спо­соб­ностей лежала где‑то в рай­оне дивер­генции гра­диен­та, а с тех пор ста­ло толь­ко хуже. Но при реали­зации в целочис­ленной матема­тике оно работа­ет, и работа­ет неп­лохо. Уж точ­но нам­ного быс­трее, чем биб­лиотеч­ный синус.

Тре­тий же метод генера­ции синуса — таб­личный, и вот он мне боль­ше все­го пон­равил­ся, осо­бен­но проз­рачностью уста­нов­ки фаз и час­тот. Кро­ме того, он демонс­три­ровал наиболь­шее быс­тро­дей­ствие. Суть метода: берем таб­лицу с заранее рас­счи­тан­ными зна­чени­ями синуса и прос­то выводим записан­ные в ней дан­ные через рав­ные про­межут­ки вре­мени с задан­ным шагом. Меняя шаг, мы меня­ем час­тоту, а меняя стар­товую точ­ку, меня­ем фазу. То, что надо!

...

uint8_t msin[256]={

127, 130, 133, 136, 139, 142, 145, 148, 151, 154, 157, 160, 163, 166,

169, 172, 175, 178, 181, 184, 186, 189, 192, 194, 197, 200, 202, 205,

207, 209, 212, 214, 216, 218, 221, 223, 225, 227, 229, 230, 232, 234,

235, 237, 239, 240, 241, 243, 244, 245, 246, 247, 248, 249, 250, 250,

251, 252, 252, 253, 253, 253, 253, 253, 254, 253, 253, 253, 253, 253,

252, 252, 251, 250, 250, 249, 248, 247, 246, 245, 244, 243, 241, 240,

239, 237, 235, 234, 232, 230, 229, 227, 225, 223, 221, 218, 216, 214,

212, 209, 207, 205, 202, 200, 197, 194, 192, 189, 186, 184, 181, 178,

175, 172, 169, 166, 163, 160, 157, 154, 151, 148, 145, 142, 139, 136,

133, 130, 127, 123, 120, 117, 114, 111, 108, 105, 102, 99, 96, 93, 90,

87, 84, 81, 78, 75, 72, 69, 67, 64, 61, 59, 56, 53, 51, 48, 46, 44,

41, 39, 37, 35, 32, 30, 28, 26, 24, 23, 21, 19, 18, 16, 14, 13, 12,

10, 9, 8, 7, 6, 5, 4, 3, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,

0, 1, 1, 2, 3, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 16, 18, 19, 21,

23, 24, 26, 28, 30, 32, 35, 37, 39, 41, 44, 46, 48, 51, 53, 56, 59,

61, 64, 67, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 102, 105,

108, 111, 114, 117, 120, 123};

static void spi1_init(void){

// Включаем порт и интерфейс

rcc_periph_clock_enable(RCC_SPI1);

rcc_periph_clock_enable(RCC_GPIOA);

/* Configure GPIOs:

* SCK=PA5

* MOSI=PA7

*/

gpio_set_mode(GPIOA, GPIO_MODE_OUTPUT_50_MHZ,

GPIO_CNF_OUTPUT_ALTFN_PUSHPULL, GPIO5|GPIO7);

spi_reset(SPI1);

spi_init_master(SPI1, SPI_CR1_BAUDRATE_FPCLK_DIV_2,

SPI_CR1_CPOL_CLK_TO_0_WHEN_IDLE,

SPI_CR1_CPHA_CLK_TRANSITION_1,

SPI_CR1_DFF_8BIT, SPI_CR1_LSBFIRST);

//spi_set_full_duplex_mode(SPI1);

spi_enable_software_slave_management(SPI1);

spi_set_nss_high(SPI1);

spi_enable(SPI1);

}

void gpio_init(){

rcc_periph_clock_enable(RCC_GPIOB);

gpio_set_mode(GPIOB, GPIO_MODE_OUTPUT_50_MHZ,

GPIO_CNF_OUTPUT_PUSHPULL, GPIO6|GPIO12);

/*

* GPIO4 - ST_CP

* GPIO12 - LED

*/

gpio_set(GPIOB,GPIO12);

gpio_clear(GPIOB,GPIO6);

}

void send_xy(uint8_t x, uint8_t y){

spi_xfer(SPI1,x);

spi_xfer(SPI1,y);

// Дергаем RCLK для записи в регистр

gpio_set(GPIOB,GPIO6);

gpio_clear(GPIOB,GPIO6);

}

void main(void){

rcc_clock_setup_in_hse_8mhz_out_72mhz();

spi1_init();

gpio_init();

uint32_t n=1;

uint8_t a=128, b=128;

while(1){

n++;

if(!(n%2))b++;

for(uint16_t i=0;i<256;i++){

send_xy(msin[a],msin[b]);

a+=1;

b+=3;

}

}

}

...

Вот такой нес­ложный код генери­рует дос­таточ­но инте­рес­ную кар­тинку, которая еще и дви­гать­ся будет.

0dffec41c42f78618cd101d75499e076.jpg

На этом мы, пожалуй, оста­вим Bluepill и перей­дем к x86_64.

USB/SPI И ФИГУРЫ ЛИССАЖУ

Для работы с FT232H на прос­торах AUR я отыс­кал биб­лиоте­ку libmpsse, но с ней все ока­залось не то что­бы глад­ко. Пакет дав­но не обновлял­ся и наот­рез отка­зывал­ся собирать­ся, хорошо, что в пер­вом же ком­мента­рии были советы, как это испра­вить. Вто­рую труд­ность я себе соз­дал сам. Труд­ность эта зак­лючалась в сов­мещении биб­лиотек из C и С++, но и это воп­рос реша­емый. Зачем так делать, спро­сишь ты? На то у меня были при­чины. Для работы с FT232H наш ЦАП уже про­качан до поч­ти нас­тояще­го SPI, оста­лось поп­равить код под пакет­ную переда­чу. Теперь вмес­то генера­ции коор­динат на лету мы их запишем в мас­сив и уже его скор­мим ЦАПу.

#include <stdio.h>

#include <math.h>

#include <string.h>

#include <cstdint>

#define FTDI 1

#ifdef FTDI

#include <stdlib.h>

extern "C"{

#include <mpsse.h>

}

#define SPEED 1000000

#endif

uint8_t buffer[10000];

uint16_t buf_shift=0;

uint8_t msin[256]={

127, 130, 133, 136, 139, 142, 145, 148, 151, 154, 157, 160, 163, 166, 169, 172, 175, 178, 181, 184, 186, 189, 192, 194, 197, 200, 202, 205, 207, 209, 212, 214, 216, 218, 221, 223, 225, 227, 229, 230, 232, 234, 235, 237, 239, 240, 241, 243, 244, 245, 246, 247, 248, 249, 250, 250, 251, 252, 252, 253, 253, 253, 253, 253, 254, 253, 253, 253, 253, 253, 252, 252, 251, 250, 250, 249, 248, 247, 246, 245, 244, 243, 241, 240, 239, 237, 235, 234, 232, 230, 229, 227, 225, 223, 221, 218, 216, 214, 212, 209, 207, 205, 202, 200, 197, 194, 192, 189, 186, 184, 181, 178, 175, 172, 169, 166, 163, 160, 157, 154, 151, 148, 145, 142, 139, 136, 133, 130, 127, 123, 120, 117, 114, 111, 108, 105, 102, 99, 96, 93, 90, 87, 84, 81, 78, 75, 72, 69, 67, 64, 61, 59, 56, 53, 51, 48, 46, 44, 41, 39, 37, 35, 32, 30, 28, 26, 24, 23, 21, 19, 18, 16, 14, 13, 12, 10, 9, 8, 7, 6, 5, 4, 3, 3, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 2, 3, 3, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 16, 18, 19, 21, 23, 24, 26, 28, 30, 32, 35, 37, 39, 41, 44, 46, 48, 51, 53, 56, 59, 61, 64, 67, 69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99, 102, 105, 108, 111, 114, 117, 120, 123};

uint8_t buffer2[512];

void singen(uint8_t sa,uint8_t sb,uint8_t fs){

static uint8_t a=0;

static uint8_t b=0;

b+=fs;

uint16_t n=0;

while(n<512){

buffer2[n++]=msin[a];

buffer2[n++]=msin[b];

a+=sa;

b+=sb;

}

}

uint16_t send_xy(uint8_t x, uint8_t y){

buffer[buf_shift++]=x;

buffer[buf_shift++]=y;

return buf_shift;

}

int main( int argc, const char** argv )

{

#ifdef FTDI

/*****************************************************************

* А теперь выведем с ходу на экран через FT232H

*****************************************************************/

struct mpsse_context *spi = NULL;

spi = MPSSE(SPI0, SPEED, LSB);

while(1){

singen(4,5,1);

Start(spi);

for(uint16_t i=0;i<10;i++)FastWrite(spi, (char*)buffer2, 512);

Stop(spi);

}

Close(spi);

#endif

}

Ком­пилиру­ем получен­ную прог­рамму:

g++ -O3 single-sin.c -o single-sin -lmpsse

За­пус­каем и убеж­даем­ся, что все работа­ет точ­но так же, как на кон­трол­лере. Пос­мотре­ли — работа­ет, но аппе­тит, как извес­тно, при­ходит во вре­мя еды. Хочет­ся вывес­ти на экран гра­фику и текст, этим и зай­мем­ся.

ГРАФИКА

Прак­тичес­ки во всех стать­ях, где опи­сывал­ся вывод изоб­ражения на осциллог­рафичес­кую труб­ку, пред­лага­ют взять ал­горитм Бре­зен­хэма и с его помощью рисовать на экра­не линии, а из них сос­тавлять кар­тинку. И этот под­ход, конеч­но, работа­ет, но уж очень это мутор­но. На мой вкус, все это не то, поэто­му будем изоб­ретать свой велоси­пед. Итак, нач­нем с прос­того: пусть у нас есть монох­ромное изоб­ражение раз­мером 256 × 256, нап­ример такое.

 

06e0d279139b704e1cb10bf921eea962.png

Дос­танем отту­да коор­динаты всех чер­ных точек. В этом нам поможет замеча­тель­ная биб­лиоте­ка OpenCV. Вооб­ще, в ней очень мно­го фун­кций для работы с изоб­ражени­ями, но здесь мы исполь­зуем ее лишь как декодер. А пос­коль­ку она все­ядная, мы авто­мати­чес­ки избавля­емся от при­вяз­ки к опре­делен­ному фор­мату кар­тинки и можем исполь­зовать любой на наш вкус, нап­ример .png. Зна­чит, декоди­руем изоб­ражение, про­бега­емся по получен­ному мас­сиву и находим коор­динаты всех чер­ных точек.

#include "opencv2/imgproc.hpp"

#include "opencv2/imgcodecs.hpp"

#include "opencv2/highgui.hpp"

#include <stdio.h>

#include <math.h>

#include <string.h>

#define FTDI 1

#ifdef FTDI

#include <stdlib.h>

extern "C"{

#include <mpsse.h>

}

#define SPEED 10000000

#endif

using namespace cv;

using namespace std;

Mat img;

int main( int argc, const char** argv )

{

char filename[256]={0};

strcpy(filename,argv[1]);

img = imread(filename, IMREAD_GRAYSCALE);

// Создаем массив с координатами точек контура

// Изображение предварительно подготовлено

printf("//Mat.rows %u\r\n", img.rows);

printf("//Mat.cols %u\r\n", img.cols);

uint32_t imgsize=img.rows*img.cols;

uint8_t *mix_img=(uint8_t*) calloc(imgsize+1,sizeof(uint8_t));

uint32_t n=0;

uint8_t *pic=(uint8_t*) calloc(2*imgsize+1,sizeof(uint8_t));

n=0;

for( uint64_t i=0;i<imgsize;i++){

if(img.data[i]<100){

pic[n++]=i%img.cols;

pic[n++]=i/img.cols;

}

}

printf("n=%u\r\n",n/2);

#ifdef FTDI

/*****************************************************************

* А теперь выведем с ходу на экран через FT232H

*****************************************************************/

struct mpsse_context *spi = NULL;

spi = MPSSE(SPI0, SPEED, LSB);

// for(uint8_t i=0;i<20;i++)printf("%u\r\n",out_mix[i]);

while(1){

Start(spi);

FastWrite(spi, (char*)pic, 2*n);

Stop(spi);

}

Close(spi);

#endif

free(pic);

}

Ка­залось бы, все готово, изоб­ражение на экра­не. Но, как в том анек­доте, есть один нюанс — мы видим боль­шое количес­тво арте­фак­тов.

ade3ef7dbe0007451f262694e6c49ac6.jpg

Ар­тефак­ты появ­ляют­ся в мес­тах, где лучу при­ходит­ся перес­какивать на отно­ситель­но боль­шое рас­сто­яние. В прос­тей­шем слу­чае с ними мож­но бороть­ся, сни­жая бит­рейт. Одна­ко это пло­хой спо­соб: арте­фак­ты, конеч­но, исче­зают, но вза­мен появ­ляет­ся мер­цание. Для срав­нения вот то же изоб­ражение с бит­рей­том 1 Мбит/с.

869ccdcb607d3f34e943afa946d4627a.jpg

Ар­тефак­тов поч­ти нет, но уже хорошо замет­но мер­цание. Так что же делать? Поп­робу­ем опти­мизи­ровать тра­екто­рию луча, сде­лав его путь по экра­ну минималь­ным (по воз­можнос­ти). Это при­водит нас к за­дачe ком­миво­яже­ра.

На­до ска­зать, это сама по себе весь­ма неп­ростая задача, но нам нет необ­ходимос­ти искать опти­маль­ный путь, так как в нашем слу­чае любое решение луч­ше чем то, что у нас есть. Поэто­му мы вос­поль­зуем­ся самым быс­трым методом — ме­тодом бли­жай­шего соседа. Суть его зак­люча­ется в том, что на каж­дом шаге луч будет дви­гать­ся в бли­жай­шую из оставших­ся точек.

Сна­чала я написал скрипт на питоне, и под­ход ока­зал­ся рабочим, одна­ко быс­тро ста­ло ясно, что слож­ность O^2 — это неп­рият­но. И если для про­рисов­ки наших 700 точек это тре­бова­ло десят­ков секунд, то за вре­мя для 10 000 точек уже мож­но нес­пешно попить чай­ку. Поэто­му, почесав в затыл­ке, я перепи­сал код на С, получив уско­рение где‑то в 60 раз.

Ре­зуль­тат неп­лох, сму­щало толь­ко то, что у меня в ноуте восемь логичес­ких ядер, из которых во вре­мя рас­чета одно пых­тело, а семь прох­лажда­лись. И тут я вспом­нил про Open MPI, с помощью которо­го вычис­ления мож­но рас­парал­лелить, прак­тичес­ки не меняя кода. Прог­раммист из меня такой себе, и все ста­дии рас­четов рас­парал­лелить у меня не выш­ло, но даже с тем, что уда­лось, получи­лось уско­рить это дело еще в четыре раза.

В таком исполне­нии прог­рамму­лина обра­баты­вает 700 точек поч­ти мгно­вен­но, 10 000 — за пару секунд и в целом может перева­рить око­ло 25 000 точек. Это успех. Так­же я добавил две полез­ные фиш­ки. Отри­сов­ка изоб­ражения в N заходов (три дают луч­ший резуль­тат) поз­воля­ет подавить мер­цание при боль­шом количес­тве точек и нес­коль­ко сни­жает вли­яние наводок на виде­оуси­литель. Еще одна фиш­ка — сох­ранение финаль­ного мас­сива в файл .h, что поз­воля­ет в даль­нейшем эко­номить вре­мя на обра­бот­ку изоб­ражения.

#include "opencv2/imgproc.hpp"

#include "opencv2/imgcodecs.hpp"

#include <stdio.h>

#include <math.h>

#include <string.h>

#include <omp.h>

#define N 3 // Число заходов в финальном массиве

//#define SAVE 1

#ifndef _OPENMP

static_assert(false, "openmp support required");

#endif

#define FTDI 1

#ifdef FTDI

#include <stdlib.h>

extern "C"{

#include <mpsse.h>

}

#define SPEED 10000000

#endif

using namespace cv;

using namespace std;

Mat img;

uint32_t min(uint32_t *, uint32_t);

uint32_t min(uint32_t *data, uint32_t datasize){

uint32_t min_ind=0;

for(uint32_t i=0;i<datasize;i++) if(data[i]<data[min_ind]) min_ind=i;

return min_ind;

};

int main( int argc, const char** argv )

{

char filename[256]={0};

strcpy(filename,argv[1]);

img = imread(filename, IMREAD_GRAYSCALE);

// Создаем массив с координатами точек контура

// Изображение предварительно подготовлено

printf("//Mat.rows %u\r\n", img.rows);

printf("//Mat.cols %u\r\n", img.cols);

uint8_t *vect_x = (uint8_t*) calloc(img.rows*img.cols+1,sizeof(uint8_t));

uint8_t *vect_y = (uint8_t*) calloc(img.rows*img.cols+1,sizeof(uint8_t));

uint32_t n=0;

for( uint64_t i=0;i<img.rows*img.cols;i++){

if(img.data[i]<100){

vect_y[n]=i/img.cols;

vect_x[n]=i%img.cols;

n++;

}

}

printf("//n=%u\r\n",n);

// Оптимизируем последовательность точек жадным алгоритмом

uint32_t * M = (uint32_t*) calloc(n*n+1,sizeof(uint32_t));

#pragma omp parallel shared(M,vect_x,vect_y) num_threads(8)

{

# pragma omp for

for(uint32_t i=0;i<n;i++){

for(uint32_t j=i;j<n;j++){

// Настоящее расстояние нам не нужно, поэтому корень можно не извлекать

if(i!=j){

M[i*n+j]=(uint32_t)(pow((vect_x[i]-vect_x[j]),2)+pow((vect_y[i]-vect_y[j]),2));

M[j*n+i]=M[i*n+j];

}

else M[i*n+j]=0xffffffff;

}

}

}//# pragma omp for

//print_matrix(M,n);

uint32_t *way = (uint32_t*) calloc(img.rows*img.cols+1,sizeof(uint32_t));

uint32_t *S = (uint32_t*) calloc(n+1,sizeof(uint32_t));

way[0]=0;

for(uint32_t i=1;i<n;i++){

#pragma omp parallel shared(M) num_threads(8)

{

# pragma omp for

for(uint32_t j=0;j<n;j++) S[j]=M[way[i-1]*n+j];

}

way[i]=min(S,n);

//printf("min: %u \r\n",way[i]);

#pragma omp parallel shared(M,way) num_threads(8)

{

# pragma omp for

for(uint32_t j=0;j<i;j++){

M[(way[i]*n)+way[j]]=0xffffffff;

M[(way[j]*n)+way[i]]=0xffffffff;}

}

//printf("i=%u way[i]=%u\r\n",i,way[i]);

//print_matrix(M,n);

}

//print_mas(way,n);

uint8_t *out = (uint8_t*) calloc(2*n+1,sizeof(uint8_t));

uint32_t j=0;

for(uint32_t i=0;i<n;i++){

out[j++]=vect_x[way[i]];

out[j++]=vect_y[way[i]];

};

/**********************************************************

* Расставляли, расставляли, а теперь замиксуем в N заходов

**********************************************************/

uint8_t *out_mix = (uint8_t*) calloc(2*n+1,sizeof(uint8_t));

j=0;

uint32_t k=0;

for(uint32_t i=0;i<N;i++){

j=2*i;

do{

out_mix[k++]=out[j];

out_mix[k++]=out[j+1];

j+=2*N;

}while(j<2*n);

}

// Освобождаем память

free(M);

free(vect_x);

free(vect_y);

free(way);

free(S);

free(out);

#ifdef SAVE

char arr_name[256]={0};

char out_file_name[256]={0};

if(argc>2){

strcpy(out_file_name,argv[2]);

strncpy(arr_name, out_file_name,strlen(out_file_name)-2);

}

else {

strncpy(arr_name, filename,strlen(filename)-4);

strcat(out_file_name,arr_name);

strcat(out_file_name,".h");

};

printf("create file %s\r\n",out_file_name);

FILE *fout;

fout=fopen(out_file_name,"w");

fprintf(fout,

"//************************************************\r\n\

//final result\r\n\

//generated by c prog black\r\n\

//************************************************\r\n");

fprintf(fout,"static const uint8_t %s[]={\r\n",arr_name);

fprintf(fout,"%u, ",out_mix[0]);

for(uint16_t i=1;i<2*n;i++){

fprintf(fout,"%u, ",out_mix[i]);

if(!(i%10))fprintf(fout,"\r\n");

}

fprintf(fout,"};\r\n");

fclose(fout);

#endif

#ifdef FTDI

/*****************************************************************

* А теперь выведем с ходу на экран через FT232H

*****************************************************************/

struct mpsse_context *spi = NULL;

spi = MPSSE(SPI0, SPEED, LSB);

//for(uint8_t i=0;i<20;i++)printf("%u\r\n",out_mix[i]);

//while(1){

for(uint64_t i=0; i<600;i++){

Start(spi);

FastWrite(spi, (char*)out_mix, 2*n);

Stop(spi);

}

Close(spi);

#endif

}

Со­бира­ем прог­рамму.

g++ -O3 -fopenmp `pkg-config opencv4 --cflags --libs` black.c -o black -lmpsse

Те­перь, если скор­мить этой прог­рамме нашу тес­товую кар­тинку, мы уви­дим, что арте­фак­тов ста­ло замет­но мень­ше без сни­жения бит­рей­та. Вот так это выг­лядит при бит­рей­те 10 Mбит/с.

8aba00e9639c5b29fe207ee9697b353a.jpg

Тем не менее арте­фак­ты отчетли­во вид­ны в мес­тах перес­кока меж­ду бук­вами. Ну да я спе­циаль­но выб­рал такое изоб­ражение, что­бы их было вид­но. У читате­ля здесь воз­никнет резон­ный воп­рос: а сто­ит ли зат­рачивать такие уси­лия ради того, что­бы неидеаль­но отри­совать столь прос­тую кар­тинку на высокой ско­рос­ти? Может, луч­ше прос­то сни­зить ско­рость? Отве­чу: затева­лось все это, что­бы отри­совы­вать изоб­ражения гораз­до более слож­ные, в 5000–20 000 точек, там сни­жать ско­рость — не вари­ант. Рас­смот­рим, нап­ример, кар­тинку повесе­лее.

dceccc24711d8e7dd76efc505a1ac554.jpg

Лег­ко видеть, что кар­тинка гораз­до более фак­турная. 

 Она цвет­ная, с полуто­нами, и раз­решение далеко не 256 × 256. Как быть? Тут нам поможет imagemagick — прод­винутый кон­соль­ный редак­тор гра­фики.

INFO

Все опи­сан­ное даль­ше навер­няка мож­но реали­зовать и средс­тва­ми opencv, да и биб­лиоте­ки imagemagick мож­но встро­ить пря­мо в код C. И это луч­ше бы удов­летво­ряло прин­ципу KISS, но, как я уже говорил, прог­раммист я пос­редс­твен­ный и ленивый, поэто­му нава­ять пару скрип­тов на Shell мне про­ще.

Итак, есть два кон­цепту­аль­но раз­ных спо­соба пре­обра­зовать цвет­ное изоб­ражение в монох­ромное: выделить кон­тур и отоб­разить цвет плот­ностью пик­селов. Для выделе­ния кон­тура мой скрипт сна­чала сжи­мает кар­тинку так, что­бы боль­шая сто­рона сос­тавля­ла 256 пик­селов, кон­верти­рует изоб­ражение в гра­дации серого и выделя­ет кон­тур. Получен­ное изоб­ражение скар­мли­вает­ся рас­смот­ренной выше прог­рамме. Вот сам скрипт.

#!/bin/zsh

##kontur_gen.sh

IMG=$1

echo "$1"

STR=$(identify "$IMG"|awk -F' ' '{print $3}')

echo "$STR"

X=$(echo $STR|awk -F'x' '{print $1}')

Y=$(echo $STR|awk -F'x' '{print $2}')

echo "$X $Y"

if [ $((X)) -lt $((Y)) ];

then

echo "X<Y"

convert -normalize -resize x256 -colorspace gray -edge $2 -negate $IMG temp.png

else

echo "X>Y"

convert -normalize -resize 256x -colorspace gray -edge $2 -negate $IMG temp.png

fi

./black "temp.png" "${1:0:(-3)}h"

Па­раметр $2 вли­яет на глу­бину поис­ка кон­туров и в общем слу­чае под­бира­ется под изоб­ражение. Обыч­но луч­ше все­го работа­ет зна­чение 2. Кар­тинка пос­ле обра­бот­ки выг­лядит так.

98da23a8a4b463db6b7da2d81ce967ba.png

Изоб­ражение содер­жит око­ло 12 000 точек и отоб­ража­ется на труб­ке вот так.

e2b2adb1e07c7f33ac1f4c44687f12e5.jpg

Вто­рой скрипт пра­вит яркость и кон­траст, сни­жает количес­тво цве­тов до 16, меня­ет раз­мер, перево­дит в оттенки серого и заменя­ет оттенки груп­пами по 4 × 4 пик­села.

#!/bin/zsh

#kontur_gen2.sh

IMG=$1

STR=$(identify "$IMG"|awk -F' ' '{print $3}')

X=$(echo $STR|awk -F'x' '{print $1}')

Y=$(echo $STR|awk -F'x' '{print $2}')

if [ $((X)) -lt $((Y)) ]

then

convert -normalize -brightness-contrast -30x30 -colors 16 -colorspace gray -resize x256 -ordered-dither o4x4 -negate $IMG temp.png

else

convert -normalize -brightness-contrast -30x30 -colors 16 -colorspace gray -resize 256x -ordered-dither o4x4 -negate $IMG temp.png

fi

./black "temp.png" "${1:0:(-3)}h"

Кар­тинка пос­ле обра­бот­ки выг­лядит сле­дующим обра­зом.

5507c737a2830b7d4e0625466d3cde78.png

Ри­сунок содер­жит око­ло 8000 точек и вот так выг­лядит на экра­не.

6d1b3afe08f13aae480c88c7a9ea15e6.jpg

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

for i in images/*;do ./kontur_gen2.sh $i;done

С изоб­ражени­ями разоб­рались, теперь перей­дем к тек­сту.

ВЫВОДИМ ТЕКСТ

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

Те­перь дело за малым: пишем фун­кцию отри­сов­ки сим­вола на экра­не (точ­нее, в буфере) и фун­кцию отри­сов­ки стро­ки.

uint8_t buffer[10000];

uint16_t buf_shift=0;

uint16_t send_xy(uint8_t x, uint8_t y){

buffer[buf_shift++]=x;

buffer[buf_shift++]=y;

return buf_shift;

}

void send_char(uint8_t chr, uint8_t x, uint8_t y){

uint8_t c=(chr<0xe0) ? chr - 0x20 : chr - 0x50;

uint16_t sh=0;

uint8_t c_len=0;

sh=shift[c];

c_len=ASCII[sh];

sh++;

for(uint8_t j=0;j<3;j++)for(uint8_t i=0;i<c_len;i++)send_xy(ASCII[sh+2*i]+x,ASCII[sh+2*i+1]+y);

}

void send_str(char *str,uint8_t x, uint8_t y){

while(*str){

if((uint8_t)*str==0xd0||(uint8_t)*str==0xd1) str++;

if(*str=='\r'){x=0;str++;}

if(*str=='\n'){y+=8*2;str++;}

send_char((uint8_t)*str++,x,y);

x+=6*2;

}

}

Ос­талось толь­ко передать фун­кции send_str() жела­емую стро­ку и положе­ние ее начала на экра­не, пос­ле чего отпра­вить буфер в ЦАП. Обра­ти вни­мание, что изоб­ражение каж­дого сим­вола записы­вает­ся в буфер триж­ды, что поз­воля­ет умень­шить количес­тво арте­фак­тов и под­нять яркость шриф­та, если тот отоб­ража­ется сов­мес­тно с дру­гими изоб­ражени­ями на экра­не. Теперь, ког­да мы уме­ем выводить на экран гра­фику и текст, мы можем сде­лать это одновре­мен­но (ну, поч­ти). Шрифт и изоб­ражение вклю­чены в виде заголо­воч­ных фай­лов.

#include "vector_font.h"

#include <stdio.h>

#include <math.h>

#include <string.h>

#include <cstdint>

#include "xaker-logo.h"

#define FTDI 1

#ifdef FTDI

#include <stdlib.h>

extern "C"{

#include <mpsse.h>

}

#define SPEED 10000000

#endif

uint8_t buffer[10000];

uint16_t buf_shift=0;

uint16_t send_xy(uint8_t x, uint8_t y){

buffer[buf_shift++]=x;

buffer[buf_shift++]=y;

return buf_shift;

}

void send_char(uint8_t chr, uint8_t x, uint8_t y){

uint8_t c=(chr<0xe0) ? chr - 0x20 : chr - 0x50;

uint16_t sh=0;

uint8_t c_len=0;

sh=shift[c];

c_len=ASCII[sh];

sh++;

for(uint8_t j=0;j<3;j++)for(uint8_t i=0;i<c_len;i++)send_xy(ASCII[sh+2*i]+x,ASCII[sh+2*i+1]+y);

}

void send_str(char *str,uint8_t x, uint8_t y){

while(*str){

if((uint8_t)*str==0xd0||(uint8_t)*str==0xd1) str++;

if(*str=='\r'){x=0;str++;}

if(*str=='\n'){y+=8*2;str++;}

send_char((uint8_t)*str++,x,y);

x+=6*2;

}

}

int main( int argc, const char** argv )

{

send_str("Candidum 2022",1,80);

send_str("Eritis sicut Deus,\r\n scientes bonum\r\n et malum.",1,100);

#ifdef FTDI

/*****************************************************************

* А теперь выведем с ходу на экран через FT232H

*****************************************************************/

struct mpsse_context *spi = NULL;

spi = MPSSE(SPI0, SPEED, LSB);

while(1){

Start(spi);

FastWrite(spi, (char*)xaker_logo,sizeof(xaker_logo));

FastWrite(spi, (char*)buffer, buf_shift);

Stop(spi);

}

Close(spi);

#endif

}

Ком­пилиру­ем, запус­каем и видим на экра­не сле­дующее.

a18275bedbe9f94b83da3843b666fe67.jpg

Выг­лядит доволь­но сим­патич­но.

ЗАКЛЮЧЕНИЕ

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

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei