«Магический глаз» тёплых ламповых времён — эмуляция на ардуино

В магнитофонах и приёмниках моей юности использовались исчезнувшие ныне ламповые индикаторы уровня на лампах 6E1П или 6Е5С. Сейчас пришла пора ностальгирования по «старым временам» и на алиэкспрессе или амазоне можно купить собранные индикаторы, они почти также популярны как часы на лампах «Никси».

Поскольку лампа требует высокого напряжения для работы, в современных устройствах это решается с помощью преобразователя напряжения на таймере 555 упроавлящим мощным полевым транзистором включенным в первичную обмотку повышающего трансформатора, и дальше вторичная обмотка подключается к умножителю напряжения из 4–5 ступеней. Этого достаточно чтобы преобразовать входные 5 вольт в 250 с током 1–2 ma.

Я хочу поделиться своим домашним проектом, суть которого в эмуляции, насколько возможно, «зелёного глаза» лампы 6E1П с помощью быстрого OLED дисплея, контролируемого платой Arduino:

Самым сложным оказалось подобрать подходящий дисплей. На рынке их огромное количество, и большинство из них легко управляется известной библиотекой Adafruit-GFX-Library. Это прекрасная универсальная библиотека, очень простая в использовании, однако при использовании обычных плат Arduino она слишком медленная и не позволяет выводить качественную анимацию. После большого количества неудач методом проб и ошибок был найден дисплей на чипе SH1106, размером 128×64 точек и с голубым свечением. Для него предлагается библиотека ардуино U8G2. Особенностью этой библиотеки является наличие страничного вывода, т.е. сначала изображение создаётся в памяти контроллера, и затем единственной командой выводится на дисплей. Это позволяет создавать простые анимации с хорошей частотой обновления. Библиотека также содержит команды для построения геометрических примитивов — прямоугольников, треугольников и окружностей. Также возможен вывод простых битмапов, однако он слишком медленный чтобы использовать для анимации. Однако битмапы могут работать как маски для вырезания части изображения, и я использовал такой битмап для придания характерной формы экрана «зеленого глаза» — вертикального прямоугольника с округлой верхней стороной.

Общий принцип построения геометрии из примитивов:

33099ba612b3184c795253ea6f061fb5.jpg

Размеры примитивов меняются динамически в зависимости от напряжения на входе A0 платы Arduino.

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

Была еще одна техническая запинка, которую мне удалось преодолеть. Дисплей этот имеет синее свечение, а мне нужно ярко-зелёное. Проблема решилась с помощью высокотемпературного скотча дюймовой ширины, это интенсивно-жёлтая полиимидная липкая лента, материал также называют «каптон». По ширине она идеально соответствует дисплею и будучи аккуратно наклееной, прекрасно превращает голубое свечение экрана в зелёное, очень близкое по цвету к «ламповому».

Для эффектности я добавил к входу Arduino сигнал с простенькой платы микрофона с усилителем от Adafruit .

Дисплей и несколько дополнительных компонент для микрофона я разместил на вырезаной под размер Arduino простенькой монтажной плате, весь монтаж выполнен проводами:

464265cd45f9945924dfaa809220d6a3.jpg181c9c00b1f0b95833e06b0256604b2f.jpg

Диаграмма соединения деталей:

f70d8d8396a1a58a12fb9015c56e0d69.jpg

Сидя в карантине, я планирую сделать аналогичный вывод для старинного индикатора 6E5С (это западная лампа E81):

730040e525ec7d077312726218ab678b.JPG

Для любопытствующих приведу мой код для Ардуино. Я отнюдь не считаю себя продвинутым программистом микроконтроллеров и никоим образом не претендую на совершенство стиля и кода, меня вполне устраивает что он просто работает.

Для плавности изображения я использую усреднение по 5 замерам. Усреднение написано «руками», пытался использовать стандартную библиотеку RunningAvg, но для простенькой ArduinoUno вместе с библиотекой дисплея не хватает памяти.

Я взял себе за правило добавлять простой информативный header в свой код, а также пару строчек, которые при запуске напоминают из какого файла и когда этот код был залит.

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

  • WHEN: 07-SEP-2020

  • WHAT: Magic eye emulator with OLED 1.3″ 128×64 I2C Blue Display I2C address 0x3C

  • https://protosupplies.com/product/oled-1-3-128x64-i2c-blue-display/

  • Uses Universal 8bit Graphics Library (https://github.com/olikraus/u8g2/)

  • DETAILS:

  • without RunningAvg library as per

  • https://www.powertronika.com/2020/04/reducing-noise-from-sensor-data.html

  • bitmap for central triangle, side with circle and rectangle

  • On Mega2056 SDA A20, SCL A21

  • On UNO SDA A4, SCL A5

  • Use analogReference() for signal 1-2.5 v

  • Analog input on A0

  • Works on direct input from radio receiver /************************************************************/

#include #include

#ifdef U8X8_HAVE_HW_I2C #include #endif

#define ARR_SIZE 5

// U8g2 Contructor List (Frame Buffer) // The complete list is available here: https://github.com/olikraus/u8g2/wiki/u8g2setupcpp U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R1, /* reset=*/ U8X8_PIN_NONE); int sum, a, y_max, delta_side,delta_y; //delta_side,delta_y - for small side triangles when a>32; y_max - maximum height float p,p_avg,myRA[ARR_SIZE]; byte index;

#define owl_width 64 #define owl_height 36 static const unsigned char owl_bits[] U8X8_PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x80, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xff, 0xff, 0x0f, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0x3f, 0x00, 0x00, 0x00, 0x00, 0xfc, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0xff, 0xff, 0x01, 0x00, 0x00, 0x80, 0xff, 0xff, 0xff, 0xff, 0x07, 0x00, 0x00, 0xe0, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0xf8, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3f, 0xfc, 0xff, 0xff, 0xff };

void setup(void) { Serial.begin(115200); u8g2.begin(); Serial.print("Sketch: "); Serial.println(FILE); Serial.print("Uploaded: "); Serial.println(DATE);

delta_side=12; delta_y=40; y_max=100; sum=0; index=0; u8g2.setFont(u8g2_font_timR10_tr); u8g2.setFontDirection(2); }

void loop(void) { p=analogRead(A0); //averaging sum=sum-myRA[index]; myRA[index]=p; sum+=p; index=(index+1)%ARR_SIZE; p_avg=sum/ARR_SIZE; //myRA.addValue(p); //p_avg=myRA.getAverage(); a=min(map(p_avg,1,60,1,32),80); //drawScreen(a,p_avg,1); //print amplitude drawScreen(a,p_avg,0); //don't print amplitude }

void drawScreen(int a, int vu, boolean printVU) { int16_t delta, a_x,a_y,a1,r,r1; u8g2.clearBuffer(); delta=min(32,a); a1=min(a,72); a_y=min(map(a,4,32,18,32),50);//70 a_x=map(delta,4,32,16,10); r=map(delta,4,32,11,8); r1=map(delta,4,32,6,0); if (a<6) u8g2.drawBox(29,0,7,y_max); //central triangle else u8g2.drawTriangle(32,0,32-delta,y_max,32+delta,y_max); if (a1>32) { u8g2.drawTriangle(32-delta_side, delta_y,0,y_max,0,y_max-(a1-32)); u8g2.drawTriangle(32+delta_side, delta_y,64,y_max,64,y_max-(a1-32)); } //side triangles
u8g2.drawTriangle(28,10,a_x,a_y,a_x,10); u8g2.drawBox(0,10,a_x,a_y-10); u8g2.drawDisc(r1+1,a_y-r1-2,r,U8G2_DRAW_LOWER_RIGHT|U8G2_DRAW_LOWER_LEFT); u8g2.drawTriangle(36,10,64-a_x,a_y,64-a_x,10); u8g2.drawBox(64-a_x,10,a_x,a_y-10); u8g2.drawDisc(64-r1-1,a_y-r1-2,r,U8G2_DRAW_LOWER_RIGHT|U8G2_DRAW_LOWER_LEFT); u8g2.setDrawColor(0); u8g2.drawBox(0,0,64,14); u8g2.setBitmapMode(1); u8g2.setDrawColor(0); u8g2.drawXBMP(0,64,owl_width, owl_height, owl_bits); u8g2.setDrawColor(1); if (printVU) { u8g2.setCursor(40, 110); u8g2.print(vu); }
u8g2.sendBuffer();
}

.

© Habrahabr.ru