Как стать автором
Обновить

Arduino AY player с экраном и кнопками

Уровень сложностиСредний
Время на прочтение15 мин
Количество просмотров11K

Конструируем музыкальный плеер PSG-файлов на чипе AY-3-8910 с OLED-экраном, кнопками управления и дополнительной памятью, а также подключаем выходные и входные сдвиговые регистры и прочие устройства с интерфейсами I2C и SPI к Arduino.

Оглавление

Как я уже рассказывал в предыдущих статьях, с музыкальным чипом AY-3-8910 меня связывает ностальгия по компьютеру ZX Spectrum. Описания различных устройств с этим чипом (далее буду называть его просто AY) вызывают у меня живой интерес, и мне захотелось сделать что-то своё или хотя бы повторить какой-то готовый проект — например, музыкальный плеер. Устройства, принимающие поток данных из компьютера, мне показались не очень интересными — хотелось автономности. В тот момент я как раз начал разбираться с Arduino — мне эта платформа показалась оптимальной с точки зрения простоты вхождения, поэтому я стал изучать именно такие устройства.

Первое устройство

Мне очень понравилась идея плеера с минимумом деталей, воспроизводящего PSG-файлы с SD-карты на чипе AY. Первой попыткой было повторить именно его, но тогда я только начинал разбираться с Arduino, и заработало оно далеко не сразу. Автор использовал Arduino Pro Mini, а я обзавелся Arduino Pro Micro (под перспективу сделать MIDI-синтезатор). Пришлось переписать процедуру записи данных в регистры AY, поскольку у этих двух Arduino совершенно разная связь между портами ввода-вывода микроконтроллера и пинами (контактами на плате). Устройство заработало, но оно умеет играть файлы с SD-карты только в случайном порядке – причем, не гарантируется, что файлы не будут повторяться. И нет возможности подключить никакие кнопки — свободных пинов у Arduino уже не осталось. Поэтому, невозможно даже элементарно переключиться на воспроизведение следующего трека, если текущий мне дослушивать не хочется.

Появилась идея добавить управление плеером через модуль Bluetooth со смартфона. Модуль был куплен, припаян к последовательному порту Arduino, установлено приложение RemoteXY на смартфон, подключена библиотека, добавлен код для связи между плеером и смартфоном, и всё даже заработало. Помимо кнопки «Следующий файл» и поля с названием текущего файла, я добавил на экран индикаторы громкости в каналах A/B/C:

Но работает это как-то нестабильно: то связь между приложением и плеером пропадает в случайные моменты времени (даже если смартфон находится совсем рядом), то плеер вдруг зависает. Причем, иногда плеер зависал даже без установки связи со смартфоном. Индикаторы громкости обновляются не очень часто, что мне также не понравилось. Причины этих проблем мне понять не удалось, и полной автономности всё равно не получилось, поэтому я решил поискать другие варианты.

Второе устройство

Следующим вариантом оказался другой плеер — на мой взгляд, более удачный. В нём используется библиотека для работы с SD-картой SDFat, поддерживающая длинные имена файлов. Кроме того, за счет использования сдвигового регистра 74HC595, решена проблема нехватки свободных пинов Arduino и добавлена кнопка для перехода на следующий трек. Дополнительно подключен OLED-дисплей SSD1306 с разрешением 128х32 точки, на котором показываются номер и название текущего трека, а также индикаторы громкости в каналах A/B/C. Сначала меня удивило, что дисплей используется в исключительно текстовом режиме, но затем стало ясно, почему: код библиотеки для работы с SD-картой сам по себе занимает в памяти много места, поэтому создать в памяти Arduino ещё и буфер для полноценной отрисовки графики не представляется возможным.

Энтузиасты даже сделали для этого варианта разводку печатной платы. Исходный код плеера мне также показался более логичным и продуманным, чем в предыдущем варианте. Автор заложил в коде режим «демо», который задается переменной bool demoMode. Если в ней значение true, то играется не весь трек, а только его начальная часть, которая через сколько-то секунд затухает по громкости до тишины, и воспроизведение переключается на следующий файл. Но, опять-таки, нет кнопки, по которой можно было бы переключаться между обычным режимом воспроизведения и «демо» прямо во время работы. А мне хотелось подключить не одну, а несколько кнопок — не только для переключения режима «демо», но и для переключения со случайного выбора файлов на последовательный, а также для перехода на предыдущий/следующий трек по одному файлу, либо перескакивая через 5 или 10 файлов.

Первая доработка

Автор исходного проекта передает через сдвиговый регистр 8 бит на шину данных чипа AY. Но для передачи данных в регистры чипа AY необходимо также изменять состояние сигналов BC1, BDIR, и перед воспроизведением каждого файла инициализировать чип с помощью сигнала Reset. Автор подключил эти сигналы к отдельным пинам Arduino. Но если поставить каскадом второй сдвиговый регистр 74HC595, то можно через всё те же 3 пина полностью управлять чипом AY.

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

Теперь, передавая последовательно не 8, а 16 бит данных, мы можем передавать сигналы на 8 контактов DA0...DA7 шины данных и контакты Reset, BC1 и BDIR чипа AY. Контакты первого регистра подключены к пинам Arduino 4, 6, 7 (здесь и далее имеются ввиду номера пинов, используемые в коде). Для передачи данных в сдвиговые регистры добавлены переменные byte outLo, outHi. В них постоянно хранится текущее состояние данных, передаваемое в чип AY: переменная outLo соответствует битам шины данных, т. е. сигналам DA0...DA7, а младшие 3 бита переменной outHi соответствуют сигналам Reset, BC1, BDIR. При каждом изменении хотя бы одного бита в сдвиговые регистры передаются оба байта с помощью процедуры out_595_word. Я вынес её вызов в процедуру:

void sendAY() {
  out_595_word(outLo, outHi);
}

Эта процедура вызывается каждый раз, когда нужно изменить состояние AY после изменения в соответствующих битах переменных outLo или outHi. Например, сброс состояния AY производится следующим образом:

#define BIT_AY_RESET 0
#define BIT_AY_BC1 1
#define BIT_AY_BDIR 2

void resetAY() {
  volumeA = volumeB = volumeC = 0;
  globalVolume = 1;

  bitClear(outHi, BIT_AY_BC1); // reset BC1
  bitClear(outHi, BIT_AY_BDIR); // reset BDIR
  bitClear(outHi, BIT_AY_RESET); // reset AY
  sendAY();
  delay(100);
  bitSet(outHi, BIT_AY_RESET); // unreset AY
  sendAY();
  delay(100);

  for (int i = 0; i < 16; i++) writeAY(i, 0);
}

Переменные volumeA, volumeB и volumeC используются для показа индикаторов громкости, а переменная globalVolume используется для плавного затухания громкости в конце трека в режиме «демо».

Для записи данных в регистр AY имеется процедура writeAY(byte port, byte data), в которой передача данных в AY производится аналогичным образом — выставляются или сбрасываются нужные биты в переменных outLo и OutHi, а затем вызывается процедура sendAY для передачи данных в чип через сдвиговые регистры.

В процедуре инициализации setup необходимо инициализировать регистр и пины Arduino, к которому он подключен:

static const byte OUT_SHIFT_DATA_PIN = 7;  // -> pin 14 of 74HC595
static const byte OUT_SHIFT_LATCH_PIN = 6; // -> pin 12 of 74HC595
static const byte OUT_SHIFT_CLOCK_PIN = 4; // -> pin 11 of 74HC595

void setupShiftOut() {
  pinMode(OUT_SHIFT_LATCH_PIN, OUTPUT);
  pinMode(OUT_SHIFT_DATA_PIN, OUTPUT);
  pinMode(OUT_SHIFT_CLOCK_PIN, OUTPUT);
  digitalWrite(OUT_SHIFT_LATCH_PIN, HIGH);
}

Вторая доработка

Чтобы принимать данные с более чем одной кнопки в Arduino, можно использовать входной сдвиговый регистр 74HC165. В отличие от схемы с резисторами, это позволяет обрабатывать и одновременные нажатия нескольких кнопок. Пока что у меня это не используется, но в будущем можно будет расширить функциональность такой «клавиатуры» на обработку одновременных нажатий. Также, если возникнет необходимость, можно подключить более одного такого регистра каскадом и обрабатывать нажатия более чем 8 кнопок, при этом количество задействованных пинов Arduino не изменится.

Контакты регистра подключены к пинам Arduino 8, 9, A0. Процедура in_165_byte считывает 8 бит из сдвигового регистра, к которому подключены кнопки, в переменную byte inBtn. Сброс бита в 0 в переменной означает, что соответствующая кнопка нажата.

Для детектирования нажатия кнопок я добавил класс CBtn, в котором также реализована защита от дребезга контактов. В конструкторе класса передается указатель на переменную inBtn, а также номер бита, на изменение которого экземпляр класса должен реагировать. После того, как вызвана процедура чтения данных из входного регистра, функция Pressed из каждого экземпляра класса CBtn возвращает true, если соответствующая кнопка была нажата.

В процедуре инициализации setup необходимо инициализировать регистр и пины Arduino, к которому он подключен:

static const byte IN_SHIFT_LATCH_PIN = 8; // -> pin 1 of 74HC165
static const byte IN_SHIFT_CLOCK_PIN = 9; // -> pin 2 of 74HC165
static const byte IN_SHIFT_DATA_PIN = A0; // -> pin 9 of 74HC165

void setupShiftIn() {
  pinMode(IN_SHIFT_DATA_PIN, INPUT);
  pinMode(IN_SHIFT_CLOCK_PIN, OUTPUT);
  pinMode(IN_SHIFT_LATCH_PIN, OUTPUT);
  digitalWrite(IN_SHIFT_LATCH_PIN, HIGH);
  digitalWrite(IN_SHIFT_CLOCK_PIN, LOW);
}

В процедурах ввода и вывода данных через сдвиговые регистры используются не стандартные процедуры записи и чтения данных digitalWrite и digitalRead, а запись и чтение портов микроконтроллера Atmega32u4. Чтобы понять, биты каких портов соответствуют пинам Arduino, я воспользовался одной из схем распиновки:

Например, для включения высокого уровня на пине 6 нужно установить значение 1 в бите 7 порта D.

Третья доработка

Следующая доработка связана с тем, что у меня используется не Arduino Pro Mini, а Arduino Pro Micro. Чтобы понять, как подключить OLED-дисплей к моему варианту, пришлось заняться поиском в сети. Поиск выдал инструкцию в видеоролике, из которого стало ясно, что нужно подключать контакты дисплея к Arduino Pro Micro следующим образом: SCL → pin 3, SDA → pin 2.

При работе по шине I2C нужно знать адрес, по которому вести обращение к устройству. В этом мне помог скетч I2C scanner, с помощью которого я определил, что адрес моего дисплея 0x3C. Адрес дисплея в коде задается в #define I2C_ADDRESS. У автора исходного устройства здесь также адрес 0x3C. Бывает, что адрес написан прямо на устройстве либо в документации к нему, но лучше это проверять с каждым устройством, подключаемым по I2C.

Также, для генерации тактовой частоты для чипа AY я использовал библиотеку FrequencyGenerator. С её помощью можно запустить аппаратный таймер номер 4, выдающий сигнал ШИМ нужной частоты на пин 5, который подключен к контакту Clock чипа AY. В клонах ZX Spectrum часто используется частота этого сигнала 1.75 МГц, но на Arduino можно выдать только близкую частоту 1.78 МГц, что вполне подходит.

Четвертая доработка

Библиотеки для работы с файлами на SD-карте имеют довольно богатую функциональность. Они позволяют не только получить список файлов в любой папке, но и открыть файл по имени. Однако, нет возможности открыть файл по его номеру в папке — можно только перечислять файлы от первого к последующим. Поэтому, для воспроизведения файлов в случайном порядке в обоих устройствах используется следующий подход:

  1. В начале работы корневая папка SD-карты сканируется, перечисляются все файлы с расширением PSG и запоминается их количество.

  2. Когда нужно перейти к очередному файлу, генерируется случайное число R от 1 до количества файлов.

  3. Файлы в папке заново перечисляются от первого до тех пор, пока не будет достигнут номер файла, равный R.

  4. В таком случае перебор прекращается, и файл с номером R запускается на воспроизведение.

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

Как мы помним, количество оперативной памяти в Arduino весьма ограниченно. Но есть возможность расширить её, подключив дополнительный чип статической памяти (SRAM). Здесь описано, как это сделать на примере чипа 23LC512, что добавляет 512 килобит, или 64 килобайта памяти. Когда я попытался его купить, именно этого чипа в продаже найти не смог, но нашел 23LC1024, объем которого в 2 раза больше. Чип подключается по интерфейсу SPI, контакт CS подключается к пину A2 (задается в константе SRAM_CS_PIN). Модуль SD shield для подключения SD-карты подключается параллельно по этому же интерфейсу, его контакт CS подключается к пину 10 (задается в константе SD_CS_PIN). Соответствующие библиотеки для работы с SD-картой и статической памятью используют сигналы CS (chip select) при каждом обращении к «своему» устройству по интерфейсу SPI, в результате два устройства, подключенных к SPI, могут работать независимо.

Список файлов в памяти

Писать самому процедуры для записи и чтения памяти 23LC1024 мне не хотелось, поэтому я решил воспользоваться готовой библиотекой. Их я нашел три, из которых почему-то заработала только одна. Причины этого мне неизвестны, но разбираться в чужих библиотеках мне не захотелось, поэтому я остановился на той, которая заработала в тестовом скетче (записывает строку текста в начало статической памяти, затем считывает строку из того же адреса и распечатывает в отладочный вывод).

Далее я решил написать класс CFileList, который позволяет хранить в статической памяти имена файлов и получать их по номеру файла. Представим всю эту память в виде непрерывного блока байтов длиной 131072 байт (задана в константе nMaxAdr = 0x20000). В начале (с адреса 0, по возрастанию) будем хранить имена файлов один за другим (для экономии места, без расширения PSG — оно у всех поддерживаемых файлов одинаковое). В конце блока (от конца к началу, по убыванию), расположим структуры, описывающие информацию о каждом файле:

struct fileDesc {
  uint24_t offset; // название типа условное
  uint16_t random_number;
};

static const byte szN = 3, szR = sizeof(uint16_t), szStruct = szN + szR;

Каждый элемент структуры состоит из двух чисел: 24-битного целого числа, в котором находится смещение имени файла от начала блока памяти (либо длина имени для первого файла), а также 16-битное случайное число от 0 до 65535 (ниже будет пояснено, зачем). Максимальное количество файлов в данной реализации также ограничим числом 65535 (задано в константе nMaxFiles) — этого хватит для достаточно большой музыкальной коллекции. Например, если объем одного PSG-файла в среднем 50 килобайт, полный объем такого количества файлов составит более 3 гигабайт, а длительность составит более 84 суток, если средняя длина одной композиции 2 минуты.

Структура данных во внешней памяти выглядит примерно так:

В классе имеются переменные: m_nFreeAdrLo хранит адрес начала свободного пространства для хранения имени очередного файла (в начале работы в ней значение 0), m_nFreeAdrHi хранит адрес конца свободного пространства для хранения структур fileDesc (в начале работы в ней значение nMaxAdr). В переменной m_nFiles хранится количество имен файлов (в начале работы в ней значение 0).

Продедура GetList сканирует корневую папку SD-карты, и для каждого очередного элемента папки делает следующее:

  1. Проверяет элемент на то, что это файл ненулевого размера с расширением PSG. Если это так, и текущее количество файлов не превышает максимально допустимого, имя файла добавляется во внешнюю память (если в ней ещё есть свободное место) в следующих шагах.

  2. Значение m_nFreeAdrHi уменьшается на размер структуры szStruct, и по этому адресу сохраняется текущее значение m_nFreeAdrLo, обрезанное до 24 бит (для экономии места).

  3. Имя файла без расширения копируется в адрес m_nFreeAdrLo, и этот адрес увеличивается на длину имени файла.

  4. Значение m_nFreeAdrHi еще раз уменьшается на размер структуры szStruct, по этому адресу сохраняется текущее значение m_nFreeAdrLo, обрезанное до 24 бит, и значение m_nFreeAdrHi обратно увеличивается на размер структуры szStruct.

  5. Текущее количество файлов m_nFiles увеличивается на 1.

Чтобы получить информацию о файле с индексом index (от нуля) в переменную fname, имеется процедура getFile(int index), которая делает следующее:

  1. Отнимает от nMaxAdr размер структуры szStruct, умноженный на номер файла (index + 1). По полученному адресу читает из статической памяти значение offset. Для первого файла в нем хранится длина имени файла — в таком случае достаточно прочитать из внешней памяти по адресу 0 количество байт, равное offset, в переменную fname.

  2. Для остальных файлов от полученного адреса отнимает размер структуры szStruct, и по полученному адресу получает значение смещения offset1 для следующего файла. Отняв от него offset для текущего файла, получает длину имени файла fnlen. После этого считывает из внешней памяти fnlen байт по адресу offset.

  3. Добавляет расширение PSG к считанному номеру файла — теперь можно передавать полное имя в процедуру открытия файла для его дальнейшего воспроизведения.

Теперь рассмотрим, зачем в каждой структуре имеется какое-то случайное число.

Генерация случайных чисел

Во время экспериментов, пробуя переключаться между файлами в случайном порядке, я обнаружил, что некоторые файлы довольно часто повторяются. Бывает даже, что при переходе к следующему «случайному» файлу опять играется тот же. Причина понятна — случайные числа выдает стандартная функция random(), которая не гарантирует, что выдаваемые числа не будут повторяться. Мне стало интересно — возможно ли генерировать случайные числа так, чтобы они не повторялись? Оказывается, да: существуют алгоритмы, позволяющие выдавать случайные числа от 0 до N-1 так, что они не повторяются. Например:

int N = 100, a[N], i, j, t;
for (i = 0; i < N; i++)
  a[i] = i;
for (i = 0; i < N; i++) {
  t = a[i];
  j = random(N) % (N - i) + i;
  a[i] = a[j];
  a[j] = t;
}

Это как раз то, что мне нужно: если в корневой папке SD-карты N файлов с расширением PSG, такой алгоритм позволит их при переборе в случайном порядке воспроизводить так, что они гарантированно будут сыграны точно по одному разу, пока не будет сыгран каждый из файлов.

Но этот алгоритм основан на том, что таблицу случайных чисел размером N нужно сформировать в памяти. Тогда я решил расширить структуры, описывающие файлы: добавить в каждую из них поле, в котором хранится случайное число. Для заполнения набора случайных чисел в статической памяти по приведенному выше алгоритму имеется процедура FillRandom.

В процедуре GetList после формирования списка файлов вызывается FillRandom. Также инициализируется переменная m_nRandom, в которой хранится количество извлеченных из списка случайных чисел. При извлечении очередного случайного числа в процедуре getRandom() значение m_nRandom увеличивается на 1. Если оказалось, что из списка неповторяющихся случайных чисел уже все выбраны, процедура FillRandom вызывается заново. При этом, если оказалось, что последнее ранее полученное случайное число совпадает с первым в списке новым случайным числом, то первое случайное число меняется местами с последним. Это делается для того, чтобы и здесь не случилось повторения двух случайных чисел.

Как известно, для инициализации генератора случайных чисел нужно вызывать функцию randomSeed, передавая в неё «настоящее» случайное число. В Arduino в качестве такого числа часто используют значение, считанное через АЦП с входного пина. Дело в том, что при чтении данных из АЦП в младших битах всегда присутствует шум, который и вносит фактор случайности. Автор исходного проекта использовал данный материал, в котором описано, как сделать такое значение более «случайным». При помощи специального алгоритма отсеиваются часто повторяющиеся значения.

Пятая доработка

В первом устройстве есть только линейный звуковой выход. Чтобы можно было подключить наушники прямо к плееру, я добавил в схему переключатель, позволяющий выдать звук с выходов чипа AY не на линейный выход, а аналоговый усилитель на микросхеме TDA7050 с регулятором громкости, взяв за основу данную схему:

Качество звука с этим усилителем, однако, оставляет желать лучшего. На максимальной громкости звук приемлемый, но в наушниках его уже невозможно слушать — слишком громко. А если сделать звук тихим, слышно много шумов: низкочастотный фон и прочие посторонние звуки. Для отладочных целей такого качества, впрочем, вполне достаточно (чтобы быстро проверить наличие звука, наушники подключить проще и быстрее, чем линейный выход к колонкам). Видимо, для качественного звукоусиления нужно использовать другую схему усилителя и выносить его с основной платы, но это выходит за рамки моей задачи. Через линейный выход на активных колонках звук никаких паразитных призвуков не имеет.

Прочие изменения

В коде изначального устройства при возникновении каких-либо ошибок (например, при чтении списка файлов с SD-карты) выводится информация в отладочный вывод. Я переделал код так, чтобы возможные ошибки выводились прямо на OLED-дисплей, например: «Files not found». Также, я добавил класс CStatusLed, позволяющий с помощью мигания встроенного светодиода показывать состояние. Если при чтении корневой папки SD-карты возникает ошибка, либо в корневой папке не найдено ни одного файла с расширением PSG, то светодиод мигает часто. Если же всё хорошо, он тоже мигает, но раз в секунду.

Также я расширил информацию, показываемую на дисплее. В первой строке показывается текущий режим: «Mode: seq/rand Demo: off/on». Mode: seq — последовательное воспроизведение файлов, rand — случайный выбор. Demo – режим «демо» вкл/выкл.

Во второй строке: номер текущего файла и количество файлов, а также размер текущего файла в килобайтах. В третьей строке: название текущего файла. В четвертой строке: прогресс воспроизведения текущего файла в процентах и индикаторы громкости в каналах A/B/C чипа AY. Они реализованы так же, как автором исходного проекта, только у него почему-то каналы A и C были переставлены местами. Я сделал так, чтобы канал A соответствовал левому каналу на звуковом выходе, а канал C – правому.

Питание устройства

Чтобы устройство с чипом AY работало стабильно, рекомендуют использовать хороший блок питания. В какой-то момент, подключив питание первым попавшимся БП от сотового телефона, я услышал, что при воспроизведении плеер «сходит с ума»: играет какие-то странные ноты, которых точно в исходном треке нет, а то и зависает. Я уже начал думать, что виновата SD-карта, на которую записаны PSG-файлы, и заменил её на другую, совершенно новую, но это не помогло. Зато смена блока питания на тот, который я использовал до этого, проблему полностью сняла. Подобные проблемы иногда возникают даже при питании устройства от компьютера по USB. Поэтому, если вдруг при воспроизведении возникают странные звуковые эффекты, рекомендую попробовать сменить питание. Автор первого устройства также справедливо заметил, что провода между Arduino и SD shield должны быть как можно короче, чтобы минимизировать возможные помехи, и на входе питания устройства необходимо подключить емкий конденсатор — например, электролитический на 470 uF.

Дополнительная информация

Напрямую к изучаемой здесь теме это отношения не имеет, но вдруг кому-то пригодится: в процессе поиска материалов по Arduino Pro Micro обнаружилось, что микроконтроллер Atmega32u4 имеет дополнительный недокументированный таймер номер 2. Существует datasheet, в котором этот таймер присутствует.

Итоговое устройство

Схема полученного устройства представлена на рисунке:

Примечания

Человек, повторивший данную схему, обнаружил, что у него она не работала, пока он не убрал конденсатор 0.1uF, подключенный к контактам 12 регистров 74HC595 (в центре схемы). Я его подключил в соответствии с рекомендациями в данном материале, и у меня с ним схема работает. Если на выходе AY-3-8910 нет звука, попробуйте убрать этот конденсатор.

Также он обнаружил, что в первоначальном варианте схемы были перепутаны контакты 1 и 2 регистра 74HC165 - теперь исправлено.

Назначение кнопок: первые 6 - переход назад/вперед на 1, 5 или 10 треков в последовательном режиме воспроизведения, либо на следующий трек в случайном. 7-я кнопка: переключение между случайным (rand) и последовательным (seq) режимами. 8-я кнопка: включение/выключение режима «демо».

Видео с демонстрацией работы устройства:

Итоговый код скетча для Arduino и схема (в том числе в формате PDF) представлены в репозитории.

Update: опубликовано продолжение.

Ссылки по теме

  1. Arduino AY player с минимумом деталей

  2. Arduino AY player с OLED-экраном

  3. Каскадное подключение сдвиговых регистров

  4. Скетч I2C scanner

  5. Неповторяющиеся случайные числа

  6. «Улучшение» случайности

  7. Библиотека FrequencyGenerator

  8. Библиотека SSD1306Ascii

  9. Библиотека SRAM_23LC

  10. Репозиторий с кодом для Arduino и схемой

Теги:
Хабы:
Всего голосов 18: ↑17 и ↓1+25
Комментарии37

Публикации

Истории

Работа

QT разработчик
4 вакансии
Программист C++
106 вакансий

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань