К микроконтроллерам Padauk я давно присматривался. Острой необходимости в их использовании у меня нет, но очень интересовали. В какой-то момент этот интерес взял верх, и я решил попробовать что-нибудь сделать на них. Если посмотреть репозитории с примерами Free PDK, то все делают простенькое проигрывание мелодий. Я не стал долго размышлять и тоже решил сделать проигрывание мелодий, но с одним условием — чтобы небольшая мелодия проигрывалась на самом дешевом и простеньком МК, таком как PMS150C или PMS150G.
Я постараюсь вспомнить всё, с чем столкнулся: от программатора Free PDK, обновления поддержки PlatformIO, создания отладочной платы под PFS154 и PMS150C (с адаптерами), музыкального брелока с PMS150G и платы с ATtiny13 — до разбора алгоритма для написания мелодий, которые можно ужать в 1 КБ памяти, а напоследок попробуем снимать значения c АЦП PFS122 и регулировать громкость музыки средствами PWM.
В статье есть видео с проигрыванием мелодий; если хотите, можете с ними ознакомиться перед началом чтения.
И вы уже могли заметить про упоминание ATtiny13. Я случайно решил сделать такое же проигрывание мелодий как на PMS150C, но на ATtiny13. Скажу сразу, потому что эта информация может быть вам полезна — на PMS150C я ужал мелодию в 1009 байт памяти, а на ATtiny13 точно самый же код (почти) занял у меня 550 байт. Вот такая вот обманочка!
❯ Введение
Обычно я начинаю с какой-нибудь истории, почему начал делать то, что делаю, но здесь все просто. Микроконтроллеры Padauk известны тем, что они самые дешевые, правда есть нюанс — самые дешевые те, которые программируются один раз. Второй нюанс — их дешевизна при покупке партии от 1000 штук и больше. Впрочем, это не важно, главное, они захватили мое внимание и было интересно наконец-то их изучить.
И не было никакой идеи, которую я долго вынашивал. Наверное, этот проект похож на Arduino UNO DIY, но с акцентом на то, что я решил отвлечься от основных задач и поработать с Padauk. Возможно, правда, я хотел побольше поработать с музыкой — именно поэтому идея сделать пищалку на Padauk мне понравилась.
Ну, как «побольше поработать»… Я уже воспроизводил музыку на пищалке — в этом нет ничего сложного. Но меня всегда вдохновляет идея DIY, поэтому большую часть этого проекта я реализовал с нуля.
Переходим к делу!
❯ Подготовка
О микроконтроллерах Padauk уже довольно много материала — и качественного. Это простые МК, поэтому о них можно сразу сказать многое. Поэтому, если вы только начинаете, то советую ознакомиться со статьей «Осваиваем 3-рублёвые микроконтроллеры PADAUK» (назовем её статья786266) — в ней все настолько хорошо написано, что если будете ей следовать, то у вас проект сразу запустится и заработает. Вначале я пытался делать все по-своему и проект не собирался — оказалось, что версия компилятора не та, и об этом я узнал из того материала. И это были еще не все грабли.
Давайте коротко обрисую наших гостей.
Микроконтроллеры Padauk обрели свою популярность где-то 7 лет назад, точнее о них я узнал из роликов с YouTube канала EEVBlog, которые датируются от 2018-го года. Их прозвали трехцентовыми, тогда как CH32V003 десятицентовыми. Имелся в виду МК PMS150C-u06. Основная особенность, что это OTP (One-Time Programmable) — другими словами их можно программировать один раз (правда я где-то слышал, что некоторые умудряются их программировать по несколько раз, пуская программу по тому пути, где ничего раньше не было. Но как это делается, не знаю). PMS150C-u6 — это микроконтроллер с шестью ножками, с 1024 байт флеш-памяти и 64 байт оперативной, максимальная рабочая частота процессора 8 MHz, но внутренняя частота (IHRC) может быть больше (16 МГц). Технические характеристики лучше смотрите на сайте производителя.
У нас PMS150C стоят около 10 рублей, можно найти за 6 рублей, а в Китае за 1 рубль (но это не точно). Звучит, конечно, хорошо, но вот официальный программатор, если покупать на Aliexpress, стоит около 7 000 рублей, что не выглядит привлекательно, если хочется просто поиграться на вечер. Но есть решение — это проект Free PDK, на котором есть схема программатора и который появился из YouTube роликов EEVBlog (если поищите на маркетплейсах, то сможете найти набор для сборки этого программатора. И продавцу отдельное спасибо за продвижение сообщества Padauk).
❯ Free PDK программатор

Как вы уже поняли, я решил начать с набора для пайки программатора. Если ищите, чего такого полезного спаять, чтобы потом пригодилось, то это хороший выбор. Самая большая сложность — это типоразмер 0603 компонентов. Другими словами, его тяжело паять, но возможно, а если использовать фен или паяльный столик, то можно упростить себе задачу, но я пользовался паяльником.
После того, как вы спаяли программатор, его надо прошить. Инструкцию читайте в статье786266, но можете и из репозитория Free PDK. Есть одна особенность, что везде используется версия прошивки из master, если вы соберете утилиту easypdkprog из master, то узнаете, что некоторые микроконтроллеры не поддерживаются:
Список поддерживаемых МК из master ветки
PS C:\Users\m039\Downloads\EASYPDKPROG_WIN_20200713_1.3\EASYPDKPROG> .\easypdkprog.exe list Supported ICs: MCU390 (0xC31): OTP : 2048 (14 bit), RAM: 128 bytes (RO) PFS154 (0xAA1): FLASH: 2048 (14 bit), RAM: 128 bytes PFS172 (0xCA6): FLASH: 2048 (14 bit), RAM: 128 bytes PFS173 (0xEA2): FLASH: 3072 (15 bit), RAM: 256 bytes PMC131 (0xC83): OTP : 1536 (14 bit), RAM: 88 bytes (RO) PMC251 (0x058): OTP : 1024 (16 bit), RAM: 59 bytes (RO) PMC271 (0xA58): OTP : 1024 (16 bit), RAM: 64 bytes (RO) PMS131 (0xC83): OTP : 1536 (14 bit), RAM: 88 bytes (RO) PMS132 (0x109): OTP : 2048 (14 bit), RAM: 128 bytes (RO) PMS132B (0x109): OTP : 2048 (14 bit), RAM: 128 bytes (RO) PMS133 (0xC19): OTP : 4096 (15 bit), RAM: 256 bytes (RO) PMS134 (0xC19): OTP : 4096 (15 bit), RAM: 256 bytes (RO) PMS150C (0xA16): OTP : 1024 (13 bit), RAM: 64 bytes PMS152 (0xA27): OTP : 1280 (14 bit), RAM: 80 bytes PMS154B (0xE06): OTP : 2048 (14 bit), RAM: 128 bytes PMS154C (0xE06): OTP : 2048 (14 bit), RAM: 128 bytes PMS15A (0xA16): OTP : 1024 (13 bit), RAM: 64 bytes PMS171B (0xD36): OTP : 1536 (14 bit), RAM: 96 bytes PMS271 (0xA58): OTP : 1024 (16 bit), RAM: 64 bytes (RO)
Список поддерживаемых МК из development ветки
PS C:\Users\m039> easypdkprog.exe list Supported ICs: MCU390 (0xC31): OTP : 2048 (14 bit), RAM: 128 bytes (RO) PFC151 (0xCA7): FLASH: 2048 (14 bit), RAM: 128 bytes PFC154 (0x34A): FLASH: 2048 (14 bit), RAM: 128 bytes PFC161 (0xCA7): FLASH: 2048 (14 bit), RAM: 128 bytes PFC232 (0xBA8): FLASH: 2048 (14 bit), RAM: 128 bytes PFS121 (0xCA6): FLASH: 2048 (14 bit), RAM: 128 bytes PFS122 (0xCA6): FLASH: 2048 (14 bit), RAM: 128 bytes PFS123 (0xD44): FLASH: 3072 (15 bit), RAM: 256 bytes PFS154 (0x542): FLASH: 2048 (14 bit), RAM: 128 bytes PFS172 (0xCA6): FLASH: 2048 (14 bit), RAM: 128 bytes PFS172B (0xCA6): FLASH: 2048 (14 bit), RAM: 128 bytes PFS173 (0xD44): FLASH: 3072 (15 bit), RAM: 256 bytes PFS173B (0xD44): FLASH: 3072 (15 bit), RAM: 256 bytes PMC131 (0xC83): OTP : 1536 (14 bit), RAM: 88 bytes (RO) PMC251 (0x058): OTP : 1024 (16 bit), RAM: 59 bytes (RO) PMC271 (0xA58): OTP : 1024 (16 bit), RAM: 64 bytes (RO) PMS131 (0xC83): OTP : 1536 (14 bit), RAM: 88 bytes (RO) PMS132 (0x109): OTP : 2048 (14 bit), RAM: 128 bytes (RO) PMS132B (0x109): OTP : 2048 (14 bit), RAM: 128 bytes (RO) PMS133 (0xC19): OTP : 4096 (15 bit), RAM: 256 bytes (RO) PMS134 (0xC19): OTP : 4096 (15 bit), RAM: 256 bytes (RO) PMS150C (0xA16): OTP : 1024 (13 bit), RAM: 64 bytes PMS150G (0x639): OTP : 1024 (13 bit), RAM: 64 bytes PMS152 (0xA27): OTP : 1280 (14 bit), RAM: 80 bytes PMS154B (0xE06): OTP : 2048 (14 bit), RAM: 128 bytes PMS154C (0xE06): OTP : 2048 (14 bit), RAM: 128 bytes PMS15A (0xA16): OTP : 1024 (13 bit), RAM: 64 bytes PMS15B (0x639): OTP : 1024 (13 bit), RAM: 64 bytes PMS171B (0xD36): OTP : 1536 (14 bit), RAM: 96 bytes PMS271 (0xA58): OTP : 1024 (16 bit), RAM: 64 bytes (RO)
Поэтому, если хотите версию из development, то можете её сами собрать. Я собирал под Windows и лучше всего у меня получилось в среде msys2. Или можете взять последнюю версию easypdkprog из моего репозитория.
Но, чтобы easypdkprog заработал, нужно также залить прошивку не из master, а из development в программатор. Прошивку я тоже положил в репозиторий.
Ради интереса я взял PMS150G специально, чтобы проверить, прошьется оно или нет. И да, я успешно прошил PMS150G с помощью easypdkprog из development ветки.
Отличия от оригинального программатора
Перед тем, как приступить к PlatformIO, надо сказать пару слов, что писать программу для МК Padauk можно двумя способами — с помощью компилятора SDCC или компилятора от Padauk. В первом случае — это обычный С, во втором — это называется «Mini-C», со своими особенностями. Я деталей не знаю, просто скажу, что дальше в статье буду использовать SDCC и обычный С, С++ вроде не поддерживается.
И программатор Free PDK может работать только с программами, написанными на SDCC, а не на Padauk компиляторе. А оригинальный программатор, наоборот, не может с ними работать.
Небольшой казус с программатором
Когда я спаял программатор, залив в него прошивку, он определился, но очень долго пытался добиться того, чтобы МК определился командой easypdkprog probe. Стыдно это признавать, но я пытался вставить пины микроконтроллера ровно так, как в гнезде программатора, а это так не делается. Нужно соединять пины с одинаковыми именами. В любом случае, про это написано в статье786266. Как только я все правильно подсоединил, смог залить тестовую программу в PMS150C и помигать светодиодом.
❯ Отладочная плата

Начну с того, что с отладочной платой произошла злая судьба. Решил я сделать переходник от программатора на гнезда под микроконтроллеры, чтобы можно было их проще прошивать. Выбрал PMS150C и PFS154 и под них сделал два гнезда. К моему удивлению оказалось, что это не ATtiny и не ATmega, у которых пины совпадают у микроконтроллеров. Поэтому PFS122, который я заказал позже, уже не подходил под эту плату. Какие-то совпадают, какие-то нет. Очень удивился.
Вторая засада — вот эта кнопка:


Как вы думаете, какие выводы соединяются, когда кнопка нажата? Ответ: первый, второй или шестой и пятый. Когда разомкнута, то первый и третий или шестой и четвертый. Вот как догадаться, какие выводы замкнуты, если кнопка нажата или разжата, это вообще ребус. Но больше всего меня смутило то, что изображено в Easy EDA, разве это понятно?

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


Здесь все просто — это два гнезда под PFS154 и PMS150C. Возможно, принципиальная схема не сильно понятна, но на разводке видно, что это плата для упрощенного подключения микроконтроллеров к программатору — плюс, два светодиода, кнопка и конденсатор.
Дальше я не буду приводить принципиальные схемы, только разводки, потому что они выглядят понятнее.




В целом, мне нравится, как получилась отладочная плата — из-за дополнительных разъемов по бокам, которые используются для подключения к беспаячной макетной плате. Кнопка включения питания оказалось не настолько необходимой, как я планировал, потому что запустить программу можно с помощью easypdkprog start, другими словами — подать напряжение на нее (дополню, что только во время загрузки программы подается напряжение, в других случаях нет).
После того, как разобрались с программатором и отладочной платой, можно приступать к программированию.
❯ PlatformIO
Мне нравится PlatformIO и сразу хотел программировать в нем. К моему везенью, 1500WK1500 уже успел сделать поддержку PlatformIO для Padauk. Но когда я установил ее, то потратил много времени на то, что казалось бы, программа компилируется, но зависает на вот этой ошибке:
Ошибка при загрузки программы с PlatformIO
PS C:\Users\m039> easypdkprog.exe write .\firmware.ihx -n PFS122 Erasing IC... done. Writing IC (88 words)... done. Calibrating IC * IHRC SYSCLK=1000000Hz @ 4.00V ... calibration result: 0Hz (0x00) out of range. ERROR: Calibration failed
Оказалось, что скорее всего причина ошибки была в том, что при компиляции использовался SDCC из репозитория PlatformIO, а его версия отличается от той, которую используют в Free PDK. После того, как я скачал компилятор версии 4.2.0, по наводке опять из статьи786266, то у меня все успешно скомпилировалось.
После всех манипуляций я создал свой репозиторий с файлами PlatformIO — pdk-platformio. Он уже скачивает нужную версию SDCCи содержит бинарник easypdkprog из development ветки. Другими словами полностью обновил поддержку PlatformIO и добавил немного МК, как PMS150G и PFS122 (еще я в него положил последнюю версию прошивки, в папке firmware, но об этом уже писал. И еще есть небольшие изменения в заголовочных файлах, которые я попытался добавить и в оригинальный репозиторий).
В любом случае, можете пользоваться или подглядывать.
Но еще у меня вылезает такое сообщение об ошибках.
Сообщение об ошибке SDCC PlatformIO
[12/26/2025, 9:12:15 PM] Unable to resolve configuration with compilerPath "C:/Users/m039/.platformio/packages/toolchain-sdcc/bin/sdcc.exe". Using "cl.exe" instead.
Точно не определил, что это такое, но возможно PlatformIO плохо поддерживает SDCC, как указано в этом обсуждении на Github. Это обсуждение началось в 2021 году и так ничего не изменилось, поэтому возможно PlatformIO не лучший вариант для программирования Padauk, но пока я им пользовался и все было хорошо.
❯ Простые примеры программ
Моргаем светодиодом
Давайте посмотрим базовый пример с морганием светодиода. Не думаю, что мне стоит объяснять, что делает каждый регистр, скорее всего многие мои читатели умнее меня и справятся с этим при желании. Но постараюсь объяснить странности и отличия заметил.
Проекты из этой статьи находятся в репозитории pdk-playground.
Код Blink
/* BlinkLED Turns an LED on for one second, then off for one second, repeatedly. Uses a timing loop for delays. */ #include <pdk/device.h> #include "auto_sysclock.h" #include "delay.h" // LED is placed on the PA4 pin (Port A, Bit 4) with a current sink configuration #define LED_BIT 4 // LED is active low (current sink), so define helpers for better readability below #define turnLedOn() PA &= ~(1 << LED_BIT) #define turnLedOff() PA |= (1 << LED_BIT) // Main program void main() { // Initialize hardware PAC |= (1 << LED_BIT); // Set LED as output (all pins are input by default) turnLedOff(); // Main processing loop while (1) { turnLedOn(); _delay_ms(1000); turnLedOff(); _delay_ms(1000); } } // Startup code - Setup/calibrate system clock unsigned char _sdcc_external_startup(void) { // Initialize the system clock (CLKMD register) with the IHRC, ILRC, or EOSC clock source and correct divider. // The AUTO_INIT_SYSCLOCK() macro uses F_CPU (defined in the Makefile) to choose the IHRC or ILRC clock source and divider. // Alternatively, replace this with the more specific PDK_SET_SYSCLOCK(...) macro from pdk/sysclock.h AUTO_INIT_SYSCLOCK(); // Insert placeholder code to tell EasyPdkProg to calibrate the IHRC or ILRC internal oscillator. // The AUTO_CALIBRATE_SYSCLOCK(...) macro uses F_CPU (defined in the Makefile) to choose the IHRC or ILRC oscillator. // Alternatively, replace this with the more specific EASY_PDK_CALIBRATE_IHRC(...) or EASY_PDK_CALIBRATE_ILRC(...) macro from easy-pdk/calibrate.h AUTO_CALIBRATE_SYSCLOCK(TARGET_VDD_MV); return 0; // Return 0 to inform SDCC to continue with normal initialization. }
Ах... мне объяснять ничего не надо, в коде полно комментариев, которые были, когда я его утащил в свой репозиторий.
С точки зрения включения портов ввода/вывода, здесь все просто: делаем 4-й пин на вывод и затем включаем, выключаем его. Но как вы могли сразу понять, не стал же я просто так показывать пример Blink, если в нем принципиально ничего нового. Но в нем есть код для калибровки микроконтроллера, который находится в sdccexternal_startup. На самом деле я сильно не вдавался что он делает, но заметил такую особенность, что иногда надо изменить частоту (например, в файле platformio.ini), чтобы примеры кода с работой по UART заработали. Но это косвенно связано с калибровкой.
В любом случае этот код нужно вставлять, но по правде говоря и без него все скомпилируется, просто вам нужно самим где-то задать нужную частоту, чтобы задержки (или что от неё зависят) работали правильно.
Пример компиляции Blink, можете увидеть строчки про калибровку
Processing padauk (platform: https://github.com/m039/pdk-platformio.git; board: pfs122; framework: easypdk) ---------------------------------------------------------------------------------------------------------------------------------- Verbose mode can be enabled via `-v, --verbose` option CONFIGURATION: https://docs.platformio.org/page/boards/padauk/pfs122.html PLATFORM: PADAUK PDK13 PDK14 PDK15 Microcontrollers (0.0.1+sha.a24fc4d) > Generic PFS122 HARDWARE: PFS122 1MHz, 128B RAM, 2KB Flash PACKAGES: - framework-easypdk @ 0.0.1+sha.390ab39 - tool-easypdkprog @ 1.3.0+sha.7cc7fc8 - toolchain-sdcc @ 1.40400.0+sha.1f2a1e7 LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf LDF Modes: Finder ~ chain, Compatibility ~ soft Found 0 compatible libraries Scanning dependencies... No dependencies Building in release mode Checking size .pio\build\padauk\firmware.ihx Advanced Memory Usage is available via "PlatformIO Home > Project Inspect" RAM: [= ] 5.5% (used 7 bytes from 128 bytes) Flash: [= ] 8.0% (used 163 bytes from 2048 bytes) Configuring upload protocol... AVAILABLE: easy-pdk-programmer CURRENT: upload_protocol = easy-pdk-programmer Uploading .pio\build\padauk\firmware.ihx Erasing IC... done. Writing IC (87 words)... done. Calibrating IC * IHRC SYSCLK=1000000Hz @ 4.00V ... calibration result: 999593Hz (0x57) done.
❯ Моргаем светодиодом… на ассемблере
Долго я не возился с ассемблером, но надо было глянуть, иначе полностью не разберешься, как все устроено. Код ниже в основном добывал из листинга дизассемблера, т.к. в интернете по ассемблеру для Padauk ничего не находилось, но потом я всё же нашел примеры.
Код Blink на ассемблере
.module blink .optsdcc -mpdk14 .area DATA .area OSEG (OVR,DATA) .area RSEG (ABS) .org 0x0000 __flag = 0x0000 __sp = 0x0002 __clkmd = 0x0003 __ihrcr = 0x000b __ilrcr = 0x0039 __eoscr = 0x000a __inten = 0x0004 __intrq = 0x0005 __integs = 0x000c __padier = 0x000d __pa = 0x0010 __pac = 0x0011 __paph = 0x0012 __pbdier = 0x000e __pb = 0x0014 __pbc = 0x0015 __pbph = 0x0016 __t16m = 0x0006 __t16c:: .ds 2 __tm2c = 0x001c __tm2ct = 0x001d __tm2s = 0x0017 __tm2b = 0x0009 __tm3c = 0x0032 __tm3ct = 0x0033 __tm3s = 0x0034 __tm3b = 0x0035 __bgtr = 0x001a __gpcc = 0x0018 __gpcs = 0x0019 __rfcc = 0x0036 __rfccrh = 0x0037 __rfccrl = 0x0038 __pwmg0c = 0x0020 __pwmg0s = 0x0021 __pwmg0dth = 0x0022 __pwmg0dtl = 0x0023 __pwmg0cubh = 0x0024 __pwmg0cubl = 0x0025 __pwmg1c = 0x0026 __pwmg1s = 0x0027 __pwmg1dth = 0x0028 __pwmg1dtl = 0x0029 __pwmg1cubh = 0x002a __pwmg1cubl = 0x002b __pwmg2c = 0x002c __pwmg2s = 0x002d __pwmg2dth = 0x002e __pwmg2dtl = 0x002f __pwmg2cubh = 0x0030 __pwmg2cubl = 0x0031 __misc = 0x0008 __misc2 = 0x000f __misclvr = 0x001b .area DATA _delay_loop_32_PARM: .ds 4 .area SSEG .area HOME .area HEADER (ABS) .area HOME .area GSINIT .area GSFINAL .area GSINIT .area PREG (ABS) .area HEADER (ABS) .org 0x0000 call __sdcc_external_startup goto _main .area GSINIT .area GSFINAL .area HOME .area HOME .area CODE _main: ; Set 4 pin to output set1.io __pac, #4 ; Main loop _loop: ; Turn led off set0.io __pa, #4 ; Delay 100000 (0x0186a0) mov a, #0xa0 mov _delay_loop_32_PARM+0, a mov a, #0x86 mov _delay_loop_32_PARM+1, a mov a, #0x01 mov _delay_loop_32_PARM+2, a clear _delay_loop_32_PARM+3 call _delay_loop_32 ; Turn led on set1.io __pa, #4 ; Delay 100000 (0x0186a0) mov a, #0xa0 mov _delay_loop_32_PARM+0, a mov a, #0x86 mov _delay_loop_32_PARM+1, a mov a, #0x01 mov _delay_loop_32_PARM+2, a clear _delay_loop_32_PARM+3 call _delay_loop_32 goto _loop _delay_loop_32: 1$: dec _delay_loop_32_PARM+0 subc _delay_loop_32_PARM+1 subc _delay_loop_32_PARM+2 subc _delay_loop_32_PARM+3 mov a, _delay_loop_32_PARM+0 or a, _delay_loop_32_PARM+1 or a, _delay_loop_32_PARM+2 or a, _delay_loop_32_PARM+3 t1sn.io f, z goto 1$ ret __sdcc_external_startup: ; src\main.c: 18: AUTO_INIT_SYSCLOCK(); mov a, #0x1c mov.io __clkmd, a ; src\main.c: 23: AUTO_CALIBRATE_SYSCLOCK(TARGET_VDD_MV); and a, #'R' and a, #'C' and a, #(1) and a, #((1000000)) and a, #((1000000)>>8) and a, #((1000000)>>16) and a, #((1000000)>>24) and a, #((4000)) and a, #((4000)>>8) and a, #(0x0b) ; src\main.c: 25: return 0; // Return 0 to inform SDCC to continue with normal initialization. ; src\main.c: 26: } ret #0x00 .area CODE .area CONST .area CABS (ABS)
Но для того, чтобы проект скомпилировался, пришлось добавить поддержку ассемблера в PlatformIO.
В листинге происходит обычное моргание светодиода. По памяти могу сказать две особенности, какие заметил: на все про все у нас один регистр, называется ACC, или A. Как вы понимаете, его достаточно. Искал в документации, что может быть чего ещё есть, но не нашел. В любом случае, все остальное хранится в оперативке.
Вторая особенность — это код калибровки. Он какой-то очень странный. Понял только, что при первом запуске происходит калибровка, записываются значения и больше этого не делается. Предполагаю, что в коде калибровки программатор Free PDK успевает как-то замерять частоту SPI и на основе её передает значения МК. Но в код программатора я лишь чуточку заглянул, но сразу убежал, иначе будут сниться кошмары.

❯ Исследование мелодий
После того, как базовое понимание есть и проект работает, переходим к основной части статьи — к проигрыванию мелодий.
Что такое PWM
Для проигрывание мелодий будем использовать пищалку или зумер. Пищалки бывают активные или пассивные. На активную подается напряжение и она пищит, но нельзя изменить тональность. На пассивную, если подать напряжение, то она издаст звук и утихнет, но если включать и выключать напряжение с определенной частотой, то будет нужное звучание и писк.
Подать колебательный сигнал на пищалку можно программно или с помощью таймера, но обычно для этого используют аппаратный модуль PWM в микроконтроллерах. В Padauk он, конечно, есть. Будем использовать его.
Что же такое PWM, он же ШИМ? Скорее всего, вы это все знаете, но попробую объяснить просто. Если подадите напряжение, например, на лампочку, то она загорится. Если уберете его, то погаснет. Если будете включать ее 50% времени и достаточно часто, то она будет тускло гореть. Для лампочки это как если подали напряжение в два раза меньше. 50% может быть любым, это называется скважность. Например, при скважности 10%, сигнал включен 10% времени, все остальное он выключен.
Для пищалки скважность не нужна, достаточно 50%, но аппаратный модуль PWM нужен, потому что для этих целей можно было бы использовать и таймер, но тогда самим надо было выставлять значение на портах ввода/вывода. Аппаратный PWM это делает за нас.
Я приведу пример кода моргания светодиодом через PWM. Взял его из стандартных примеров.
Код Led PWM
#include <pdk/device.h> #include "auto_sysclock.h" #include "delay.h" #define PWM_MAX 255 #define LED_BIT 3 // PA3 (TM2PWM) void main() { PAC |= (1 << LED_BIT); TM2B = 0x00; TM2C = (uint8_t)(TM2C_INVERT_OUT | TM2C_MODE_PWM | TM2C_OUT_PA3 | TM2C_CLK_IHRC); TM2S = 0x0; while (1) { uint8_t fadeValue; for (fadeValue = 0; fadeValue < PWM_MAX; fadeValue += 5) { TM2B = fadeValue; _delay_ms(30); } for (fadeValue = PWM_MAX; fadeValue > 0; fadeValue -= 5) { TM2B = fadeValue; _delay_ms(30); } } } // Startup code - Setup/calibrate system clock unsigned char _sdcc_external_startup(void) { // Initialize the system clock (CLKMD register) with the IHRC, ILRC, or EOSC clock source and correct divider. // The AUTO_INIT_SYSCLOCK() macro uses F_CPU (defined in the Makefile) to choose the IHRC or ILRC clock source and divider. // Alternatively, replace this with the more specific PDK_SET_SYSCLOCK(...) macro from pdk/sysclock.h AUTO_INIT_SYSCLOCK(); // Insert placeholder code to tell EasyPdkProg to calibrate the IHRC or ILRC internal oscillator. // The AUTO_CALIBRATE_SYSCLOCK(...) macro uses F_CPU (defined in the Makefile) to choose the IHRC or ILRC oscillator. // Alternatively, replace this with the more specific EASY_PDK_CALIBRATE_IHRC(...) or EASY_PDK_CALIBRATE_ILRC(...) macro from easy-pdk/calibrate.h AUTO_CALIBRATE_SYSCLOCK(TARGET_VDD_MV); return 0; // Return 0 to inform SDCC to continue with normal initialization. }
Конфигурируем регистры и плавно моргаем:

Чуть не забыл самое главное сказать — если посмотрите на гифку, то увидите, что светодиод находится на макетной плате, а не используется тот, который на отладочной. Это все потому, что нельзя выбрать любой вывод для аппаратного PWM, только 3. По крайней мере — это справедливо для PMS150C, на который я ровнялся. Но когда я разводил плату, это никак учесть не мог, но из-за разъемов на отладочной плате к этому выводу подсоединился.
Издаем звуки на пищалке
Функции tone как в Arduino у нас нет, придется писать свою. И это с одной стороны хорошо, потому что мы сразу её оптимизируем. Но сначала я написал вот такую:
Изначальный код tone.h
void tone(long frequency) { #define TONE_FREQ (16000000) #define MAX_SCALE (32 * 256) if (frequency <= 0) { TM2B = 0; } else { long scale = TONE_FREQ / 2 / frequency; uint8_t s1 = 0; if (scale > MAX_SCALE) { scale = TONE_FREQ / 2 / frequency / 4; s1 = 1; if (scale > MAX_SCALE) { scale = TONE_FREQ / 2 / frequency / 16; s1 = 2; if (scale > MAX_SCALE) { scale = TONE_FREQ / 2 / frequency / 64; s1 = 3; } } } else { TM2B = 0; return; } if (scale >= 256) { scale = scale / 256; TM2B = 0xFF; TM2S = s1 << 5 | (scale) & 0x1F; } else { TM2B = scale; TM2S = s1 << 5 | 0; } } }
У аппаратного PWM есть два режима: period mode или PWM mode. В первом случае скважность 50%, во втором регулируемая программно, но из-за этого будет урезана частота PWM. Поэтому для проигрывания мелодий лучше использовать period mode, потому что с урезанной частотой не получится играть хорошо. Точнее можно, но не все будет работать, потому что некоторые ноты будут накладываться друг на друга и звучать некорректно (решение в таком случае задавать ноты вручную, чтобы не накладывались, но об этом далее).
Для выбора period mode нужно записать значение в TM2C регистр, например, так TM2C = (uint8_t)(TM2C_MODE_PERIOD | TM2C_OUT_PA3 | TM2C_CLK_IHRC).
А для того, чтобы задать частоту на выходе PWM, нужно записать в регистры TM2S и TM2B определенные значения. Рассчитываются эти значения по формуле Frequency of Output = Y ÷ [2 × (K+1) × S1 × (S2+1) ], где Y — выбранная частота тактирования, в нашем случае это 16000000; K — регистр скважности, да, он используется не для скважности, т.к. мы теперь в period mode, равно числу от 0 до 255; S1 — множитель, который может быть одним из 1, 4, 16, 64; S2 — множитель, который может быть любым числом от 0 до 31.
Итак, чтобы на выходе у нас была нота A4, её частота 440, конфигурируем TM2C и записываем, например, TM2S = 3 << 5 | 31 (выбираю S1 и S2 максимальными), записываю TM2B = 7 (16000000 ÷ (2 × 64 × 32 × 440) - 1 = 7.87). Как вы могли видеть, в наших расчетах у TM2B получилось 7.87, что можно понять как 7 или как 8. Т.е. при других S1 и S2 будет другая погрешность.
Вернемся к функции tone выше. Примерно так выглядит функция tone в Arduino, я пытался брать её, и как вы можете понять, она эвристически определяет множители, другими словами, если множитель подходит, использует его, если нет, то использует следующий, поэтому если две ноты накладываются, то ничего с этим поделать нельзя.
Вот исправленная версия:
Исправленная версия tone.h
void tone(long frequency) { #define TONE_FREQ (16000000 / (2 * 64 * 7)) if (frequency <= 0) { TM2B = 0; } else { TM2S = 3 << 5 | (7 - 1); TM2B = TONE_FREQ / frequency - 1; } }
Здесь код проще и он означает, что S1 = 64, а S2 = 6, следовательно нота у нас может быть с минимальной частотой 70 Гц и максимальной 17857, с шагом 70 ((17857 - 70) / 256). Другими словами, мы избавились от эвристического подхода и задали определенный диапазон частот, которые в нашей песне скорее всего будет.
Диапазон частот можно уменьшить, например, увеличив S2, но пока я именно этот и использую в финальной мелодии. Почему — не помню, но другие не подходили при оптимизации.
А можете догадаться, в чем еще проблема в этом коде, из-за чего он полностью не подходит?
Это деление. На PMS150C с делением код может не скомпилироваться вообще, флеш памяти не хватит. Деление мы использовать не можем. Точнее, лучше от него избавиться.
Решение — это использовать константы. Например, так:
Финальная версия tone
#define TONE_FREQ (16000000 / (2 * 64 * 7)) #define F(x) (TONE_FREQ / x - 1) #define NOTE_A4 F(440) void tone(long frequency) { if (frequency <= 0) { TM2B = 0; } else { TM2S = 3 << 5 | (7 - 1); TM2B = frequency; } }
В принципе это все, если запишете правильное значение в TM2C и вызовете tone(NOTE_A4), то будет слышна нота A4.
Проигрываем мелодии
Звуки научились издавать, теперь перейдем к проигрыванию мелодий. У меня на руках были микроконтроллеры PMS150C-u6 и PFS154. Всю разработку я проводил на PFS154, у него флеш памяти 2КБ. У PMS150C 1КБ. Другими словами надо уложить мелодию в 2КБ и при желании в 1КБ.
Сразу расскажу, как сделал в финальной программе. Мелодии проще всего брать из MIDI файлов. Для этого находим какую-нибудь песню в сети и прогоняем через конвертор, который уже преобразует MIDI в код для Arduino. Да, зачем усложнять задачу, когда можно сделать так просто, но выходной формат работать не будет. (Кстати, я написал скрипт, который помог мне быстро перевести из этого формата в свой.)
Например, вот такой код для Arduino будет если прогнать Twinkle Twinkle Little Star:
Twinkle Twinkle Little Star через конвертер
// Can be moved in header file i.e notes.h #define ARRAY_LEN(array) (sizeof(array) / sizeof(array[0])) #define C4 262 #define G4 392 #define A4 440 #define C3 131 #define F4 349 #define E4 330 #define D4 294 #define G2 98 const int midi1[90][3] = { {C4, 602, 30}, {G4, 602, 30}, {A4, 602, 30}, {C3, 1201, 62}, {F4, 602, 30}, {E4, 602, 30}, {D4, 602, 30}, {C3, 1201, 62}, {G4, 602, 30}, {F4, 602, 30}, {E4, 602, 30}, {G2, 1201, 62}, {G4, 602, 30}, {F4, 602, 30}, {E4, 602, 30}, {G2, 1201, 62}, {C4, 602, 30}, {G4, 602, 30}, {A4, 602, 30}, {C3, 1201, 62}, {F4, 602, 30}, {E4, 602, 30}, {D4, 602, 30}, {C3, 1201, 0}, }; void playMidi(int pin, const int notes[][3], size_t len){ for (int i = 0; i < len; i++) { tone(pin, notes[i][0]); delay(notes[i][1]); noTone(pin); delay(notes[i][2]); } } // Generated using https://github.com/ShivamJoker/MIDI-to-Arduino // main.ino or main.cpp void setup() { // put your setup code here, to run once: // play midi by passing pin no., midi, midi len playMidi(11, midi1, ARRAY_LEN(midi1)); } void loop() { // put your main code here, to run repeatedly: }
Код playMidi очень простой: пробегаемся по масиву, для каждой ноты запускаем tone, затем ждем, затем останавливаем проигрывание и опять ждем.
Вот такой у меня получился код, который абстрагирует значения присущие к мелодии (тон, продолжительность звучания, продолжительность тишины):
Абстрактный код
uint8_t delay_ms(uint16_t time) { for (uint16_t i = 0; i < time; i++) { _delay_ms(1); if (isButtonActive()) { if (!buttonPressed) { return 1; } } else { buttonPressed = 0; } } return 0; } void playMelody() { for (int thisNote = 0; thisNote < MELODY_SIZE; thisNote++) { tone(MELODY_TONE(thisNote)); if (delay_ms(MELODY_DURATION(thisNote))) { return; } tone(0); if (delay_ms(MELODY_NO_TONE_DURATION(thisNote))) { return; } } }
Здесь можете увидеть макросы MELODY_TONE, MELODY_DURATION и MELODY_NO_TONE_DURATION. С помощью них достаются соответствующие значения, там немножко хитро, поэтом об этом чуть попозже. Но хочу обратить ваше внимание на функцию delay_ms. Функции delay для Padauk нету в библиотеки, а если использовать delayms с произвольным параметром, то происходит деление и памяти для микроконтроллера не хватает, поэтому нашлось решение написать свою функцию delay_ms.
Давайте рассмотрим финальный вариант одной из мелодии — Комарово:
komarovo_optimized.h
#ifndef _KOMAROVO_O_ #define _KOMAROVO_O_ #include <stdint.h> #include "delay.h" #define TONE_FREQ (16000000 / (2 * 64 * 7)) #define F(x) (TONE_FREQ / x - 1) #define NOTE_A4 F(440) #define NOTE_Ab4 F(466) #define NOTE_C5 F(523) #define NOTE_G4 F(392) #define NOTE_F4 F(349) #define NOTE_D5 F(587) #define NOTE_E4 F(330) #define NOTE_D4 F(294) #define NOTE_E5 F(659) #define NOTE_F5 F(698) #define NOTE_G5 F(784) #define NOTE_Cb5 F(554) #define DURATION_0 0 #define DURATION_135 1 #define DURATION_270 2 #define DURATION_15 3 #define DURATION_150 4 #define DURATION_285 5 #define DURATION_30 6 #define DURATION_420 7 #define DURATION_165 8 #define DURATION_300 9 #define DURATION_45 10 #define DURATION_180 11 #define DURATION_1080 12 #define DURATION_315 13 #define DURATION_60 14 #define DURATION_195 15 #define DURATION_1095 16 #define DURATION_840 17 #define DURATION_330 18 #define DURATION_75 19 #define DURATION_465 20 #define DURATION_210 21 #define DURATION_1110 22 #define DURATION_345 23 #define DURATION_90 24 #define DURATION_480 25 #define DURATION_225 26 #define DURATION_360 27 #define DURATION_105 28 #define DURATION_375 29 #define DURATION_120 30 #define DURATION_255 31 const uint16_t melody_durations[] = { 0, // 0 138, // 1 275, // 2 21, // 3 150, // 4 283, // 5 33, // 6 421, // 7 163, // 8 300, // 9 50, // 10 175, // 11 1079, // 12 313, // 13 67, // 14 188, // 15 1092, // 16 846, // 17 337, // 18 79, // 19 458, // 20 217, // 21 1117, // 22 346, // 23 92, // 24 483, // 25 225, // 26 354, // 27 100, // 28 371, // 29 125, // 30 250, // 31 }; typedef uint16_t melody_data; const melody_data melody[92] = { (NOTE_A4 << 10) | (DURATION_60 << 5) | DURATION_45, (NOTE_A4 << 10) | (DURATION_75 << 5) | DURATION_60, (NOTE_A4 << 10) | (DURATION_45 << 5) | DURATION_195, (NOTE_A4 << 10) | (DURATION_165 << 5) | DURATION_75, (NOTE_Ab4 << 10) | (DURATION_60 << 5) | DURATION_195, (NOTE_A4 << 10) | (DURATION_60 << 5) | DURATION_90, (NOTE_C5 << 10) | (DURATION_45 << 5) | DURATION_330, (NOTE_Ab4 << 10) | (DURATION_375 << 5) | DURATION_135, (NOTE_Ab4 << 10) | (DURATION_60 << 5) | DURATION_60, (NOTE_A4 << 10) | (DURATION_45 << 5) | DURATION_90, (NOTE_G4 << 10) | (DURATION_45 << 5) | DURATION_195, (NOTE_G4 << 10) | (DURATION_165 << 5) | DURATION_90, (NOTE_F4 << 10) | (DURATION_60 << 5) | DURATION_195, (NOTE_G4 << 10) | (DURATION_60 << 5) | DURATION_90, (NOTE_Ab4 << 10) | (DURATION_45 << 5) | DURATION_315, (NOTE_A4 << 10) | (DURATION_330 << 5) | DURATION_150, (NOTE_A4 << 10) | (DURATION_60 << 5) | DURATION_60, (NOTE_A4 << 10) | (DURATION_60 << 5) | DURATION_60, (NOTE_A4 << 10) | (DURATION_105 << 5) | DURATION_300, (NOTE_A4 << 10) | (DURATION_90 << 5) | DURATION_60, (NOTE_Ab4 << 10) | (DURATION_75 << 5) | DURATION_165, (NOTE_A4 << 10) | (DURATION_105 << 5) | DURATION_135, (NOTE_C5 << 10) | (DURATION_30 << 5) | DURATION_225, (NOTE_Ab4 << 10) | (DURATION_360 << 5) | DURATION_120, (NOTE_A4 << 10) | (DURATION_75 << 5) | DURATION_60, (NOTE_Ab4 << 10) | (DURATION_75 << 5) | DURATION_60, (NOTE_A4 << 10) | (DURATION_90 << 5) | DURATION_285, (NOTE_G4 << 10) | (DURATION_75 << 5) | DURATION_60, (NOTE_G4 << 10) | (DURATION_45 << 5) | DURATION_165, (NOTE_D5 << 10) | (DURATION_345 << 5) | DURATION_420, (NOTE_A4 << 10) | (DURATION_45 << 5) | DURATION_75, (NOTE_A4 << 10) | (DURATION_45 << 5) | DURATION_90, (NOTE_A4 << 10) | (DURATION_45 << 5) | DURATION_210, (NOTE_A4 << 10) | (DURATION_75 << 5) | DURATION_150, (NOTE_Ab4 << 10) | (DURATION_60 << 5) | DURATION_195, (NOTE_A4 << 10) | (DURATION_30 << 5) | DURATION_90, (NOTE_C5 << 10) | (DURATION_30 << 5) | DURATION_360, (NOTE_Ab4 << 10) | (DURATION_345 << 5) | DURATION_135, (NOTE_Ab4 << 10) | (DURATION_60 << 5) | DURATION_75, (NOTE_A4 << 10) | (DURATION_60 << 5) | DURATION_75, (NOTE_G4 << 10) | (DURATION_90 << 5) | DURATION_270, (NOTE_G4 << 10) | (DURATION_60 << 5) | DURATION_75, (NOTE_F4 << 10) | (DURATION_45 << 5) | DURATION_225, (NOTE_G4 << 10) | (DURATION_45 << 5) | DURATION_75, (NOTE_Ab4 << 10) | (DURATION_45 << 5) | DURATION_315, (NOTE_A4 << 10) | (DURATION_330 << 5) | DURATION_165, (NOTE_F4 << 10) | (DURATION_75 << 5) | DURATION_60, (NOTE_F4 << 10) | (DURATION_75 << 5) | DURATION_60, (NOTE_F4 << 10) | (DURATION_30 << 5) | DURATION_210, (NOTE_F4 << 10) | (DURATION_120 << 5) | DURATION_135, (NOTE_E4 << 10) | (DURATION_30 << 5) | DURATION_210, (NOTE_F4 << 10) | (DURATION_60 << 5) | DURATION_60, (NOTE_A4 << 10) | (DURATION_30 << 5) | DURATION_375, (NOTE_G4 << 10) | (DURATION_270 << 5) | DURATION_195, (NOTE_G4 << 10) | (DURATION_90 << 5) | DURATION_60, (NOTE_G4 << 10) | (DURATION_90 << 5) | DURATION_45, (NOTE_G4 << 10) | (DURATION_165 << 5) | DURATION_75, (NOTE_F4 << 10) | (DURATION_45 << 5) | DURATION_210, (NOTE_E4 << 10) | (DURATION_30 << 5) | DURATION_90, (NOTE_D4 << 10) | (DURATION_465 << 5) | DURATION_165, (NOTE_D5 << 10) | (DURATION_45 << 5) | DURATION_210, (NOTE_E5 << 10) | (DURATION_45 << 5) | DURATION_90, (NOTE_D5 << 10) | (DURATION_45 << 5) | DURATION_330, (NOTE_C5 << 10) | (DURATION_1110 << 5) | DURATION_120, (NOTE_E5 << 10) | (DURATION_180 << 5) | DURATION_75, (NOTE_E5 << 10) | (DURATION_60 << 5) | DURATION_60, (NOTE_E5 << 10) | (DURATION_45 << 5) | DURATION_360, (NOTE_F5 << 10) | (DURATION_1110 << 5) | DURATION_120, (NOTE_F5 << 10) | (DURATION_120 << 5) | DURATION_135, (NOTE_G5 << 10) | (DURATION_45 << 5) | DURATION_90, (NOTE_F5 << 10) | (DURATION_15 << 5) | DURATION_345, (NOTE_E5 << 10) | (DURATION_1095 << 5) | DURATION_165, (NOTE_E5 << 10) | (DURATION_105 << 5) | DURATION_135, (NOTE_F5 << 10) | (DURATION_45 << 5) | DURATION_90, (NOTE_E5 << 10) | (DURATION_45 << 5) | DURATION_345, (NOTE_D5 << 10) | (DURATION_1110 << 5) | DURATION_120, (NOTE_D5 << 10) | (DURATION_180 << 5) | DURATION_90, (NOTE_E5 << 10) | (DURATION_45 << 5) | DURATION_75, (NOTE_D5 << 10) | (DURATION_15 << 5) | DURATION_330, (NOTE_C5 << 10) | (DURATION_840 << 5) | DURATION_180, (NOTE_E5 << 10) | (DURATION_165 << 5) | DURATION_75, (NOTE_E5 << 10) | (DURATION_180 << 5) | DURATION_75, (NOTE_E5 << 10) | (DURATION_180 << 5) | DURATION_60, (NOTE_E5 << 10) | (DURATION_30 << 5) | DURATION_225, (NOTE_F5 << 10) | (DURATION_1110 << 5) | DURATION_135, (NOTE_F5 << 10) | (DURATION_195 << 5) | DURATION_75, (NOTE_F5 << 10) | (DURATION_150 << 5) | DURATION_75, (NOTE_F5 << 10) | (DURATION_30 << 5) | DURATION_255, (NOTE_E5 << 10) | (DURATION_1080 << 5) | DURATION_135, (NOTE_A4 << 10) | (DURATION_30 << 5) | DURATION_210, (NOTE_Cb5 << 10) | (DURATION_75 << 5) | DURATION_180, (NOTE_D5 << 10) | (DURATION_0 << 5) | DURATION_480, }; #define MELODY_SIZE sizeof(melody) / sizeof(melody_data) #define MELODY_TONE(x) ((melody[x] >> 10) & 0x3f) #define MELODY_NO_TONE_DURATION(x) melody_durations[(melody[x] >> 5) & 0x1f] #define MELODY_DURATION(x) melody_durations[melody[x] & 0x1F] void tone(uint8_t frequency) { if (frequency <= 0) { TM2B = 0; } else { TM2S = 3 << 5 | (7 - 1); TM2B = frequency; } } #endif
На примере этой мелодии можно увидеть, какие способы оптимизации я использовал, чтобы ужать мелодию в 1КБ флеш памяти.
Одна запись мелодии это uint16_t, другими словами 2 байта. Под ноту отведено 6 битов, все остальное на задержки, по 5 битов. Другими словами, разных нот у нас может быть 2**6=64, а разных задержек 2**5=32. Например, можно сказать, что в конвертере у задержки тип int два байта, а я ужал его в 5 бит, но для того, чтобы задержка была верная, нужно где-то хранить массив из табличных данных, массив или словарь, индекс которого будет соответствующая запись мелодии.
Но есть нюансы, например, конвертор может сгенерировать много разных задержек, в моем скрипте я эти задержки группирую и подгоняю, чтобы их было меньше 32.
Тоже самое делая и с тоном, но для тона я решил не заводить отдельную таблицу, т.к. если подкрутить множители, то все ноты можно уложить в 6 бит. Например, для ноты D4 (294 Гц) получается множитель 60 (16000000 ÷ (2 × 64 × 7) ÷ 294), а для ноты NOTE_G5 (784 Гц), получается множитель 22 (16000000 ÷ (2 × 64 × 7) ÷ 784). Т.е. ноты лежат в диапазоне от 22 до 60, т.е. умещаются в 6 битов.
Если скомпилировать программу с этой мелодией, то получим 1009 байтов. Не стал записывать отдельного видео, т.к. в конце статьи есть со всеми мелодиями, какие успел сделать. Можете перейти к нему.
Полноценная программа
Моя цель была не просто проиграть мелодию, но сделать это в форме законченного устройства. Например, в виде брелка. Для простоты я выбрал, что брелок питается от CR2032 и у него есть кнопка, по нажатию на которую играет музыка. Все остальное время микроконтроллер спит.
main.c
#include <pdk/device.h> #include "auto_sysclock.h" #include "delay.h" #include "melodies/komarovo_optimized.h" #include "config.h" uint8_t buttonPressed; uint8_t delay_ms(uint16_t time) { for (uint16_t i = 0; i < time; i++) { _delay_ms(1); if (isButtonActive()) { if (!buttonPressed) { return 1; } } else { buttonPressed = 0; } } return 0; } void playMelody() { for (int thisNote = 0; thisNote < MELODY_SIZE; thisNote++) { tone(MELODY_TONE(thisNote)); if (delay_ms(MELODY_DURATION(thisNote))) { return; } tone(0); if (delay_ms(MELODY_NO_TONE_DURATION(thisNote))) { return; } } } void main() { buttonSetup(); uint8_t clkmd = CLKMD; while (1) { if (isButtonActive()) { if (buttonPressed) { return; } CLKMD = clkmd; buttonPressed = 1; buzzerOn(); playMelody(); if (isButtonActive()) { buttonPressed = 1; } buzzerOff(); } else { buttonPressed = 0; } buzzerOff(); CLKMD = 0xF4; CLKMD &= ~CLKMD_ENABLE_IHRC; sleep(); } } // Startup code - Setup/calibrate system clock unsigned char _sdcc_external_startup(void) { // Initialize the system clock (CLKMD register) with the IHRC, ILRC, or EOSC clock source and correct divider. // The AUTO_INIT_SYSCLOCK() macro uses F_CPU (defined in the Makefile) to choose the IHRC or ILRC clock source and divider. // Alternatively, replace this with the more specific PDK_SET_SYSCLOCK(...) macro from pdk/sysclock.h AUTO_INIT_SYSCLOCK(); // Insert placeholder code to tell EasyPdkProg to calibrate the IHRC or ILRC internal oscillator. // The AUTO_CALIBRATE_SYSCLOCK(...) macro uses F_CPU (defined in the Makefile) to choose the IHRC or ILRC oscillator. // Alternatively, replace this with the more specific EASY_PDK_CALIBRATE_IHRC(...) or EASY_PDK_CALIBRATE_ILRC(...) macro from easy-pdk/calibrate.h AUTO_CALIBRATE_SYSCLOCK(TARGET_VDD_MV); return 0; // Return 0 to inform SDCC to continue with normal initialization. }
Cпециально вынес все устройство специфичное в отедльный файл config.h, чтобы чтение main.c было проще.
config.h
#ifndef __CONFIG__ #define __CONFIG__ #define BUZZER_BIT 3 // PA3 (TM2PWM) #define BUTTON_BIT 4 // PA4 #define isButtonActive() !(PA & (1 << BUTTON_BIT)) #define sleep() __asm stopsys __endasm; #define buzzerOn()\ PAC |= (1 << BUZZER_BIT);\ TM2C = (uint8_t)(TM2C_MODE_PERIOD | TM2C_OUT_PA3 | TM2C_CLK_IHRC); #define buzzerOff()\ PAC &= ~(1 << BUZZER_BIT);\ TM2C = 0; #define buttonSetup()\ PADIER |= (1 << BUTTON_BIT);\ PAPH |= (1 << BUTTON_BIT); #endif
Пока я тестировал этот код, то столкнулся с такой особенностью, что микроконтроллер хоть и входил в сон, но батарейка села за два дня. Получается, что-то потребляло ток. С помощью шунтов определил, что если не выключить PWM, то на выводе будет ток, поэтому перед тем, как уходить в сон, нужно выключить все, что имеется. Еще в даташите я подметил, что перед уходом в сон частоту меняют, уменьшают её, наверно так лучше, поэтому я этот код взял себе тоже.
Еще вы можете заметить обработку кнопки. Я сделал её без прерывания, потому что когда разводил плату не учел, что прерывание у PMS150C может быть только на определенный пин, т.е. нельзя его выбрать. Но ничего страшного, дописать обработчик без прерывания было не сложно, потому что МК может выходить из сна при изменении на любом пине, что уже хорошо.
Брелок на PMS150G



И результатом всей статьи я решил сделать небольшое устройство — брелок, который может играть только одну мелодию. Алгоритм работы брелка простой: включается, спит и ждет нажатия кнопки, если кнопка нажата, то играет мелодию, в противном случае засыпает. Все вроде получилось хорошо, но есть почему-то проблема с батарейкой, как понимаю при проигрывании мелодии получается очень сильная просадка по напряжению и МК перезагружается, я изменил фьюзы с помощью PDK_SET_FUSE(FUSE_LVR_2V), но сильно лучше не стало. Помогло использовать вместо CR2032, LIR2032 (но изменение LDO в брелке с ATtiny13 эту проблему решило).
И неожиданный победитель — ATtiny13!

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

У ATtiny13 тоже есть режимы PWM и воспользуемся таким же, т.е. скважность 50% и более гибка регулировка частоты. При этом я сделал отдельную плату, которую можно воспринимать как законченное устройство, с кнопкой, CR2032 и глубоким сном.
main.c
#include "komarovo.h" #include "config.h" uint8_t buttonPressed; void setup() { buttonSetup(); } uint8_t delay_ms(uint16_t time) { for (uint16_t i = 0; i < time; i++) { _delay_ms(1); if (isButtonActive()) { if (!buttonPressed) { return 1; } } else { buttonPressed = 0; } } return 0; } void playMelody() { for (int thisNote = 0; thisNote < MELODY_SIZE; thisNote++) { tone(MELODY_TONE(thisNote)); if (delay_ms(MELODY_DURATION(thisNote))) { return; } tone(0); if (delay_ms(MELODY_NO_TONE_DURATION(thisNote))) { return; } } } void loop() { if (isButtonActive()) { if (buttonPressed) { return; } buttonPressed = 1; buzzerOn(); playMelody(); if (isButtonActive()) { buttonPressed = 1; } buzzerOff(); } else { buttonPressed = 0; } buzzerOff(); sleep(); }
komarovo.h
#ifndef KOMAROVO #define KOMAROVO #include "config.h" #define TONE_FREQ (F_CPU / (2 * 256)) #define F(x) (uint8_t)(TONE_FREQ / x - 1) #define NOTE_A4 F(440) #define NOTE_Ab4 F(466) #define NOTE_C5 F(523) #define NOTE_G4 F(392) #define NOTE_F4 F(349) #define NOTE_D5 F(587) #define NOTE_E4 F(330) #define NOTE_D4 F(294) #define NOTE_E5 F(659) #define NOTE_F5 F(698) #define NOTE_G5 F(784) #define NOTE_Cb5 F(554) #define DURATION_0 0 #define DURATION_135 1 #define DURATION_270 2 #define DURATION_15 3 #define DURATION_150 4 #define DURATION_285 5 #define DURATION_30 6 #define DURATION_420 7 #define DURATION_165 8 #define DURATION_300 9 #define DURATION_45 10 #define DURATION_180 11 #define DURATION_1080 12 #define DURATION_315 13 #define DURATION_60 14 #define DURATION_195 15 #define DURATION_1095 16 #define DURATION_840 17 #define DURATION_330 18 #define DURATION_75 19 #define DURATION_465 20 #define DURATION_210 21 #define DURATION_1110 22 #define DURATION_345 23 #define DURATION_90 24 #define DURATION_480 25 #define DURATION_225 26 #define DURATION_360 27 #define DURATION_105 28 #define DURATION_375 29 #define DURATION_120 30 #define DURATION_255 31 const uint16_t melody_durations[] PROGMEM = { 0, // 0 138, // 1 275, // 2 21, // 3 150, // 4 283, // 5 33, // 6 421, // 7 163, // 8 300, // 9 50, // 10 175, // 11 1079, // 12 313, // 13 67, // 14 188, // 15 1092, // 16 846, // 17 337, // 18 79, // 19 458, // 20 217, // 21 1117, // 22 346, // 23 92, // 24 483, // 25 225, // 26 354, // 27 100, // 28 371, // 29 125, // 30 250, // 31 }; typedef uint16_t melody_data; const melody_data melody[92] PROGMEM = { (NOTE_A4 << 10) | (DURATION_60 << 5) | DURATION_45, (NOTE_A4 << 10) | (DURATION_75 << 5) | DURATION_60, (NOTE_A4 << 10) | (DURATION_45 << 5) | DURATION_195, (NOTE_A4 << 10) | (DURATION_165 << 5) | DURATION_75, (NOTE_Ab4 << 10) | (DURATION_60 << 5) | DURATION_195, (NOTE_A4 << 10) | (DURATION_60 << 5) | DURATION_90, (NOTE_C5 << 10) | (DURATION_45 << 5) | DURATION_330, (NOTE_Ab4 << 10) | (DURATION_375 << 5) | DURATION_135, (NOTE_Ab4 << 10) | (DURATION_60 << 5) | DURATION_60, (NOTE_A4 << 10) | (DURATION_45 << 5) | DURATION_90, (NOTE_G4 << 10) | (DURATION_45 << 5) | DURATION_195, (NOTE_G4 << 10) | (DURATION_165 << 5) | DURATION_90, (NOTE_F4 << 10) | (DURATION_60 << 5) | DURATION_195, (NOTE_G4 << 10) | (DURATION_60 << 5) | DURATION_90, (NOTE_Ab4 << 10) | (DURATION_45 << 5) | DURATION_315, (NOTE_A4 << 10) | (DURATION_330 << 5) | DURATION_150, (NOTE_A4 << 10) | (DURATION_60 << 5) | DURATION_60, (NOTE_A4 << 10) | (DURATION_60 << 5) | DURATION_60, (NOTE_A4 << 10) | (DURATION_105 << 5) | DURATION_300, (NOTE_A4 << 10) | (DURATION_90 << 5) | DURATION_60, (NOTE_Ab4 << 10) | (DURATION_75 << 5) | DURATION_165, (NOTE_A4 << 10) | (DURATION_105 << 5) | DURATION_135, (NOTE_C5 << 10) | (DURATION_30 << 5) | DURATION_225, (NOTE_Ab4 << 10) | (DURATION_360 << 5) | DURATION_120, (NOTE_A4 << 10) | (DURATION_75 << 5) | DURATION_60, (NOTE_Ab4 << 10) | (DURATION_75 << 5) | DURATION_60, (NOTE_A4 << 10) | (DURATION_90 << 5) | DURATION_285, (NOTE_G4 << 10) | (DURATION_75 << 5) | DURATION_60, (NOTE_G4 << 10) | (DURATION_45 << 5) | DURATION_165, (NOTE_D5 << 10) | (DURATION_345 << 5) | DURATION_420, (NOTE_A4 << 10) | (DURATION_45 << 5) | DURATION_75, (NOTE_A4 << 10) | (DURATION_45 << 5) | DURATION_90, (NOTE_A4 << 10) | (DURATION_45 << 5) | DURATION_210, (NOTE_A4 << 10) | (DURATION_75 << 5) | DURATION_150, (NOTE_Ab4 << 10) | (DURATION_60 << 5) | DURATION_195, (NOTE_A4 << 10) | (DURATION_30 << 5) | DURATION_90, (NOTE_C5 << 10) | (DURATION_30 << 5) | DURATION_360, (NOTE_Ab4 << 10) | (DURATION_345 << 5) | DURATION_135, (NOTE_Ab4 << 10) | (DURATION_60 << 5) | DURATION_75, (NOTE_A4 << 10) | (DURATION_60 << 5) | DURATION_75, (NOTE_G4 << 10) | (DURATION_90 << 5) | DURATION_270, (NOTE_G4 << 10) | (DURATION_60 << 5) | DURATION_75, (NOTE_F4 << 10) | (DURATION_45 << 5) | DURATION_225, (NOTE_G4 << 10) | (DURATION_45 << 5) | DURATION_75, (NOTE_Ab4 << 10) | (DURATION_45 << 5) | DURATION_315, (NOTE_A4 << 10) | (DURATION_330 << 5) | DURATION_165, (NOTE_F4 << 10) | (DURATION_75 << 5) | DURATION_60, (NOTE_F4 << 10) | (DURATION_75 << 5) | DURATION_60, (NOTE_F4 << 10) | (DURATION_30 << 5) | DURATION_210, (NOTE_F4 << 10) | (DURATION_120 << 5) | DURATION_135, (NOTE_E4 << 10) | (DURATION_30 << 5) | DURATION_210, (NOTE_F4 << 10) | (DURATION_60 << 5) | DURATION_60, (NOTE_A4 << 10) | (DURATION_30 << 5) | DURATION_375, (NOTE_G4 << 10) | (DURATION_270 << 5) | DURATION_195, (NOTE_G4 << 10) | (DURATION_90 << 5) | DURATION_60, (NOTE_G4 << 10) | (DURATION_90 << 5) | DURATION_45, (NOTE_G4 << 10) | (DURATION_165 << 5) | DURATION_75, (NOTE_F4 << 10) | (DURATION_45 << 5) | DURATION_210, (NOTE_E4 << 10) | (DURATION_30 << 5) | DURATION_90, (NOTE_D4 << 10) | (DURATION_465 << 5) | DURATION_165, (NOTE_D5 << 10) | (DURATION_45 << 5) | DURATION_210, (NOTE_E5 << 10) | (DURATION_45 << 5) | DURATION_90, (NOTE_D5 << 10) | (DURATION_45 << 5) | DURATION_330, (NOTE_C5 << 10) | (DURATION_1110 << 5) | DURATION_120, (NOTE_E5 << 10) | (DURATION_180 << 5) | DURATION_75, (NOTE_E5 << 10) | (DURATION_60 << 5) | DURATION_60, (NOTE_E5 << 10) | (DURATION_45 << 5) | DURATION_360, (NOTE_F5 << 10) | (DURATION_1110 << 5) | DURATION_120, (NOTE_F5 << 10) | (DURATION_120 << 5) | DURATION_135, (NOTE_G5 << 10) | (DURATION_45 << 5) | DURATION_90, (NOTE_F5 << 10) | (DURATION_15 << 5) | DURATION_345, (NOTE_E5 << 10) | (DURATION_1095 << 5) | DURATION_165, (NOTE_E5 << 10) | (DURATION_105 << 5) | DURATION_135, (NOTE_F5 << 10) | (DURATION_45 << 5) | DURATION_90, (NOTE_E5 << 10) | (DURATION_45 << 5) | DURATION_345, (NOTE_D5 << 10) | (DURATION_1110 << 5) | DURATION_120, (NOTE_D5 << 10) | (DURATION_180 << 5) | DURATION_90, (NOTE_E5 << 10) | (DURATION_45 << 5) | DURATION_75, (NOTE_D5 << 10) | (DURATION_15 << 5) | DURATION_330, (NOTE_C5 << 10) | (DURATION_840 << 5) | DURATION_180, (NOTE_E5 << 10) | (DURATION_165 << 5) | DURATION_75, (NOTE_E5 << 10) | (DURATION_180 << 5) | DURATION_75, (NOTE_E5 << 10) | (DURATION_180 << 5) | DURATION_60, (NOTE_E5 << 10) | (DURATION_30 << 5) | DURATION_225, (NOTE_F5 << 10) | (DURATION_1110 << 5) | DURATION_135, (NOTE_F5 << 10) | (DURATION_195 << 5) | DURATION_75, (NOTE_F5 << 10) | (DURATION_150 << 5) | DURATION_75, (NOTE_F5 << 10) | (DURATION_30 << 5) | DURATION_255, (NOTE_E5 << 10) | (DURATION_1080 << 5) | DURATION_135, (NOTE_A4 << 10) | (DURATION_30 << 5) | DURATION_210, (NOTE_Cb5 << 10) | (DURATION_75 << 5) | DURATION_180, (NOTE_D5 << 10) | (DURATION_0 << 5) | DURATION_480, }; #define MELODY_SIZE sizeof(melody) / sizeof(melody_data) #define MELODY_TONE(x) ((pgm_read_word(&melody[x]) >> 10) & 0x3f) #define MELODY_NO_TONE_DURATION(x) pgm_read_word(&melody_durations[(pgm_read_word(&melody[x]) >> 5) & 0x1F]) #define MELODY_DURATION(x) pgm_read_word(&melody_durations[pgm_read_word(&melody[x]) & 0x1F]) void tone(uint8_t frequency) { if (frequency <= 0) { buzzerOff(); OCR0A = 0; } else { buzzerOn(); TCCR0B = (1 << WGM02) | (1 << CS02) | (0 << CS01) | (0 << CS00); OCR0A = frequency; } } #endif
config.h
#ifndef CONFIG__ #define CONFIG__ #include <avr/io.h> #include <avr/sleep.h> #include <avr/interrupt.h> #define BUTTON_BIT 1 // B1 #define BUZZER_BIT 0 // B0 #define buttonSetup() \ DDRB &= ~(1 << BUTTON_BIT);\ PORTB |= (1 << BUTTON_BIT);\ GIMSK |= (1 << PCIE);\ PCMSK |= (1 << BUTTON_BIT);\ sei(); #define buzzerOff() \ TCCR0A = 0;\ DDRB &= ~(1 << BUZZER_BIT); #define buzzerOn() \ DDRB |= (1 << BUZZER_BIT);\ TCCR0A = (1 << COM0A0) | (1 << WGM00) | (0 << WGM01); #define sleep() \ set_sleep_mode(SLEEP_MODE_PWR_DOWN);\ sleep_enable();\ sleep_cpu();\ sleep_disable(); #define isButtonActive() !(PINB & (1 << BUTTON_BIT)) #endif
Программу я также разбил на 3 файла main.c(более абстрактный), config.h(специфичный для МК), komarovo.h (отдельный файл с мелодией).
Есть небольшие особенности, с тем, что PWM нужно полностью обрубать когда задана нулевая частота, иначе будут артефакты. Во всем остальном код очень и очень похож на тот, который был у PMS150G. Может быть еще pgm_read_byte часто встречается.
Что же касается того, почему память под мелодию у ATtiny13 больше, чем у PMS150G мне на самом деле было сразу ясно, потому что где-то полгода назад я писал программу на ассемблере для PIC и удивился той системе как хранятся данные во флеш-памяти. Другими словами, как хранятся константы из С языка.
Вот так хранятся данные мелодии в ATtiny13:
Массив melody из листинга ассемблера ATtiny13
00000056 <melody>: 56: ca a5 6e a6 4f a5 13 a5 cf 9d d8 a5 52 89 a1 9f ..n.O.......R... 66: ce 9d 58 a5 4f b9 18 b9 cf d1 d8 b9 4d 9d 44 a6 ..X.O.......M.D. 76: ce a5 ce a5 89 a7 0e a7 68 9e 81 a7 da 88 7e 9f ........h.....~. 86: 6e a6 6e 9e 05 a7 6e ba 48 b9 e7 7a 53 a5 58 a5 n.n...n.H..zS.X. 96: 55 a5 64 a6 cf 9d d8 a4 db 88 e1 9e d3 9d d3 a5 U.d............. a6: 02 bb d3 b9 5a d1 53 b9 4d 9d 48 a6 6e d2 6e d2 ....Z.S.M.H.n.n. b6: d5 d0 c1 d3 d5 dc ce d1 dd a4 4f b8 0e bb 0a bb ..........O..... c6: 13 b9 55 d1 d8 dc 88 fa 55 79 58 6d 52 79 de 8a ..U.....UyXmRy.. d6: 73 6d ce 6d 5b 6d de 66 c1 67 58 59 77 64 08 6e sm.m[m.f.gXYwd.n e6: 81 6f 58 65 57 6d de 7a 78 79 53 6d 72 78 2b 8a .oXeWm.zxySmrx+. f6: 13 6d 73 6d 6e 6d da 6c c1 66 f3 65 93 64 df 64 .msmnm.l.f.e.d.d 106: 81 6d d5 a4 6b 82 19 78 .m..k..x
А вот так данные хранятся в PMS150G:
Массив melody из листинга ассемблера PMS150C
000080 570 _melody: 000080 CA 01 571 ret #0xca 000082 9D 01 572 ret #0x9d ; 40394 000084 6E 01 573 ret #0x6e 000086 9E 01 574 ret #0x9e ; 40558 000088 4F 01 575 ret #0x4f 00008A 9D 01 576 ret #0x9d ; 40271 00008C 13 01 577 ret #0x13 00008E 9D 01 578 ret #0x9d ; 40211 000090 CF 01 579 ret #0xcf 000092 95 01 580 ret #0x95 ; 38351 000094 D8 01 581 ret #0xd8 000096 9D 01 582 ret #0x9d ; 40408 000098 52 01 583 ret #0x52 00009A 85 01 584 ret #0x85 ; 34130 00009C A1 01 585 ret #0xa1 00009E 97 01 586 ret #0x97 ; 38817 0000A0 CE 01 587 ret #0xce 0000A2 95 01 588 ret #0x95 ; 38350 0000A4 58 01 589 ret #0x58 0000A6 9D 01 590 ret #0x9d ; 40280 0000A8 4F 01 591 ret #0x4f 0000AA B1 01 592 ret #0xb1 ; 45391 0000AC 18 01 593 ret #0x18 0000AE B1 01 594 ret #0xb1 ; 45336 0000B0 CF 01 595 ret #0xcf 0000B2 C9 01 596 ret #0xc9 ; 51663 0000B4 D8 01 597 ret #0xd8 0000B6 B1 01 598 ret #0xb1 ; 45528 0000B8 4D 01 599 ret #0x4d 0000BA 95 01 600 ret #0x95 ; 38221 0000BC 44 01 601 ret #0x44 0000BE 9E 01 602 ret #0x9e ; 40516 0000C0 CE 01 603 ret #0xce 0000C2 9D 01 604 ret #0x9d ; 40398 0000C4 CE 01 605 ret #0xce 0000C6 9D 01 606 ret #0x9d ; 40398 0000C8 89 01 607 ret #0x89 0000CA 9F 01 608 ret #0x9f ; 40841 0000CC 0E 01 609 ret #0x0e 0000CE 9F 01 610 ret #0x9f ; 40718 0000D0 68 01 611 ret #0x68 0000D2 96 01 612 ret #0x96 ; 38504 0000D4 81 01 613 ret #0x81 0000D6 9F 01 614 ret #0x9f ; 40833 0000D8 DA 01 615 ret #0xda 0000DA 84 01 616 ret #0x84 ; 34010 0000DC 7E 01 617 ret #0x7e 0000DE 97 01 618 ret #0x97 ; 38782 0000E0 6E 01 619 ret #0x6e 0000E2 9E 01 620 ret #0x9e ; 40558 0000E4 6E 01 621 ret #0x6e 0000E6 96 01 622 ret #0x96 ; 38510 0000E8 05 01 623 ret #0x05 0000EA 9F 01 624 ret #0x9f ; 40709 0000EC 6E 01 625 ret #0x6e 0000EE B2 01 626 ret #0xb2 ; 45678 0000F0 48 01 627 ret #0x48 0000F2 B1 01 628 ret #0xb1 ; 45384 0000F4 E7 01 629 ret #0xe7 0000F6 76 01 630 ret #0x76 ; 30439 0000F8 53 01 631 ret #0x53 0000FA 9D 01 632 ret #0x9d ; 40275 0000FC 58 01 633 ret #0x58 0000FE 9D 01 634 ret #0x9d ; 40280 000100 55 01 635 ret #0x55 000102 9D 01 636 ret #0x9d ; 40277 000104 64 01 637 ret #0x64 000106 9E 01 638 ret #0x9e ; 40548 000108 CF 01 639 ret #0xcf 00010A 95 01 640 ret #0x95 ; 38351 00010C D8 01 641 ret #0xd8 00010E 9C 01 642 ret #0x9c ; 40152 000110 DB 01 643 ret #0xdb 000112 84 01 644 ret #0x84 ; 34011 000114 E1 01 645 ret #0xe1 000116 96 01 646 ret #0x96 ; 38625 000118 D3 01 647 ret #0xd3 00011A 95 01 648 ret #0x95 ; 38355 00011C D3 01 649 ret #0xd3 00011E 9D 01 650 ret #0x9d ; 40403 000120 02 01 651 ret #0x02 000122 B3 01 652 ret #0xb3 ; 45826 000124 D3 01 653 ret #0xd3 000126 B1 01 654 ret #0xb1 ; 45523 000128 5A 01 655 ret #0x5a 00012A C9 01 656 ret #0xc9 ; 51546 00012C 53 01 657 ret #0x53 00012E B1 01 658 ret #0xb1 ; 45395 000130 4D 01 659 ret #0x4d 000132 95 01 660 ret #0x95 ; 38221 000134 48 01 661 ret #0x48 000136 9E 01 662 ret #0x9e ; 40520 000138 6E 01 663 ret #0x6e 00013A CA 01 664 ret #0xca ; 51822 00013C 6E 01 665 ret #0x6e 00013E CA 01 666 ret #0xca ; 51822 000140 D5 01 667 ret #0xd5 000142 C8 01 668 ret #0xc8 ; 51413 000144 C1 01 669 ret #0xc1 000146 CB 01 670 ret #0xcb ; 52161 000148 D5 01 671 ret #0xd5 00014A D4 01 672 ret #0xd4 ; 54485 00014C CE 01 673 ret #0xce 00014E C9 01 674 ret #0xc9 ; 51662 000150 DD 01 675 ret #0xdd 000152 9C 01 676 ret #0x9c ; 40157 000154 4F 01 677 ret #0x4f 000156 B0 01 678 ret #0xb0 ; 45135 000158 0E 01 679 ret #0x0e 00015A B3 01 680 ret #0xb3 ; 45838 00015C 0A 01 681 ret #0x0a 00015E B3 01 682 ret #0xb3 ; 45834 000160 13 01 683 ret #0x13 000162 B1 01 684 ret #0xb1 ; 45331 000164 55 01 685 ret #0x55 000166 C9 01 686 ret #0xc9 ; 51541 000168 D8 01 687 ret #0xd8 00016A D4 01 688 ret #0xd4 ; 54488 00016C 88 01 689 ret #0x88 00016E EE 01 690 ret #0xee ; 61064 000170 55 01 691 ret #0x55 000172 75 01 692 ret #0x75 ; 30037 000174 58 01 693 ret #0x58 000176 69 01 694 ret #0x69 ; 26968 000178 52 01 695 ret #0x52 00017A 75 01 696 ret #0x75 ; 30034 00017C DE 01 697 ret #0xde 00017E 86 01 698 ret #0x86 ; 34526 000180 73 01 699 ret #0x73 000182 69 01 700 ret #0x69 ; 26995 000184 CE 01 701 ret #0xce 000186 69 01 702 ret #0x69 ; 27086 000188 5B 01 703 ret #0x5b 00018A 69 01 704 ret #0x69 ; 26971 00018C DE 01 705 ret #0xde 00018E 62 01 706 ret #0x62 ; 25310 000190 C1 01 707 ret #0xc1 000192 63 01 708 ret #0x63 ; 25537 000194 58 01 709 ret #0x58 000196 55 01 710 ret #0x55 ; 21848 000198 77 01 711 ret #0x77 00019A 60 01 712 ret #0x60 ; 24695 00019C 08 01 713 ret #0x08 00019E 6A 01 714 ret #0x6a ; 27144 0001A0 81 01 715 ret #0x81 0001A2 6B 01 716 ret #0x6b ; 27521 0001A4 58 01 717 ret #0x58 0001A6 61 01 718 ret #0x61 ; 24920 0001A8 57 01 719 ret #0x57 0001AA 69 01 720 ret #0x69 ; 26967 0001AC DE 01 721 ret #0xde 0001AE 76 01 722 ret #0x76 ; 30430 0001B0 78 01 723 ret #0x78 0001B2 75 01 724 ret #0x75 ; 30072 0001B4 53 01 725 ret #0x53 0001B6 69 01 726 ret #0x69 ; 26963 0001B8 72 01 727 ret #0x72 0001BA 74 01 728 ret #0x74 ; 29810 0001BC 2B 01 729 ret #0x2b 0001BE 86 01 730 ret #0x86 ; 34347 0001C0 13 01 731 ret #0x13 0001C2 69 01 732 ret #0x69 ; 26899 0001C4 73 01 733 ret #0x73 0001C6 69 01 734 ret #0x69 ; 26995 0001C8 6E 01 735 ret #0x6e 0001CA 69 01 736 ret #0x69 ; 26990 0001CC DA 01 737 ret #0xda 0001CE 68 01 738 ret #0x68 ; 26842 0001D0 C1 01 739 ret #0xc1 0001D2 62 01 740 ret #0x62 ; 25281 0001D4 F3 01 741 ret #0xf3 0001D6 61 01 742 ret #0x61 ; 25075 0001D8 93 01 743 ret #0x93 0001DA 60 01 744 ret #0x60 ; 24723 0001DC DF 01 745 ret #0xdf 0001DE 60 01 746 ret #0x60 ; 24799 0001E0 81 01 747 ret #0x81 0001E2 69 01 748 ret #0x69 ; 27009 0001E4 D5 01 749 ret #0xd5 0001E6 9C 01 750 ret #0x9c ; 40149 0001E8 6B 01 751 ret #0x6b 0001EA 7E 01 752 ret #0x7e ; 32363 0001EC 19 01 753 ret #0x19 0001EE 74 01 754 ret #0x74 ; 29721
А здесь хитрость инженерной мысли. Каждый байт констант из С хранится двумя байтами, но в области кода, в виде инструкции ret, как понимаю код её 01. Происходит переход по этому месту в коде и сразу же возвращается назад, но в регистре ACC будет нужная константа. Например, первым в списке видно ret #0xca, что переводится в два байта CA 01, что возвращает число 0xCA. Наверно, кто писал на ассемблере ничего нового не открыл, а вот кто не писал, тот может изрядно удивится, что его МК Padauk, на который он так рассчитывал, не сможет сделать то, что хотелось бы. Но на самом деле это сильно не беда, т.к. в другом ассемблерном коде, вроде, нет никаких подвохов. Поэтому если у вас не много констант, то можно и не переживать.
❯ Бонус: эксперименты с регулировкой PWM с помощью энкодера
Время экспериментов! Меня интересовал один момент и хотел попробовать, следовательно такая задача: программно изменить громкость с помощью изменения PWM. Для этого придется использовать PWM в режиме PWM, а не period mode, чтобы можно было изменять скважность.
У меня получилось два эксперимента. В первом я считываю значения с АЦП и регулирую скважность с помощью него. Во втором я считываю значения с энкодера, ставлю скважность 50% и играю мелодию столько по времени, сколько у меня значение на АЦП. Другими словами, делаю PWM на PWM.
Но надо уточнить, что ни в PMS150G, ни в PFS154 нет АЦП, поэтому я взял для этих целей PFS122, у которого на борту 12 битный АЦП.
main.c
#include <pdk/device.h> #include <stdlib.h> #include "auto_sysclock.h" #include "delay.h" #define CONFIG_PWM_VOLUME 1 #define NOTE_C4 (uint32_t)(2 << 5 | 15) // 260HZ (262) #define NOTE_D4 (uint32_t)(2 << 5 | 13) // 300HZ (294) #define NOTE_E4 (uint32_t)(2 << 5 | 12) // 325Hz (330) #define NOTE_F4 (uint32_t)(2 << 5 | 11) // 355Hz (349) #define NOTE_G4 (uint32_t)(2 << 5 | 10) // 390Hz (392) #define NOTE_A4 (uint32_t)(2 << 5 | 9) // 434Hz (440) #define NOTE_B4 (uint32_t)(2 << 5 | 8) // 488Hz (494) #define NOTE_C5 (uint32_t)(2 << 5 | 7) // 558Hz (523) #define REST (uint32_t)0 #define DURATION_4 (uint32_t)(1000 / 4) #define DURATION_2 (uint32_t)(1000 / 2) const uint32_t melody[] = { (NOTE_C4 << 24) | (DURATION_4 << 12) | DURATION_4, (NOTE_C4 << 24) | (DURATION_4 << 12) | DURATION_4, (NOTE_G4 << 24) | (DURATION_4 << 12) | DURATION_4, (NOTE_G4 << 24) | (DURATION_4 << 12) | DURATION_4, (NOTE_A4 << 24) | (DURATION_4 << 12) | DURATION_4, (NOTE_A4 << 24) | (DURATION_4 << 12) | DURATION_4, (NOTE_G4 << 24) | (DURATION_2 << 12) | DURATION_2, (REST << 24) | (DURATION_4 << 12) | DURATION_4, (NOTE_F4 << 24) | (DURATION_4 << 12) | DURATION_4, (NOTE_F4 << 24) | (DURATION_4 << 12) | DURATION_4, (NOTE_E4 << 24) | (DURATION_4 << 12) | DURATION_4, (NOTE_E4 << 24) | (DURATION_4 << 12) | DURATION_4, (NOTE_D4 << 24) | (DURATION_4 << 12) | DURATION_4, (NOTE_D4 << 24) | (DURATION_4 << 12) | DURATION_4, (NOTE_C4 << 24) | (DURATION_2 << 12) | DURATION_2, (REST << 24) | (DURATION_4 << 12) | DURATION_4 }; #define PWM_MAX 255 #define BUZZER_BIT 3 // PA3 (TM2PWM) #define ADC_BIT 3 // PB3 #define TONE_FREQ (16000000 / 64) #define MAX_SCALE (32) uint8_t adc = 127; uint8_t lastTone; char buffer[8] = {0}; void tone(uint8_t frequency); void pullAdc(); void delay(uint16_t delay) { #if CONFIG_PWM_VOLUME delay /= 16; for (uint16_t i = 0; i < delay; i++) { pullAdc(); uint8_t s1 = 0; uint8_t s2 = 0; for (uint8_t j = 0; j < 0xF; j++) { if (j < adc && s1 == 0) { tone(lastTone); s1 = 1; } if (j >= adc && s2 == 0) { tone(0); s2 = 1; } _delay_us(1000); } } #else for (uint16_t i = 0; i < delay; i++) { _delay_ms(1); } pullAdc(); #endif } void tone(uint8_t frequency) { if (frequency == 0) { TM2C = 0; TM2S = 0; TM2B = 0; } else { TM2C = (uint8_t)(TM2C_MODE_PWM | TM2C_OUT_PA3 | TM2C_CLK_IHRC); TM2S = frequency; #if CONFIG_PWM_VOLUME TM2B = 127; #else TM2B = adc; #endif } } void playMelody() { for (int thisNote = 0; thisNote < (sizeof(melody) / sizeof(uint32_t)); thisNote++) { lastTone = (melody[thisNote] >> 24) & 0xFF; tone(lastTone); delay(melody[thisNote] & 0xFFF); lastTone = 0; tone(lastTone); delay((melody[thisNote] >> 12) & 0xFFF); } } void pullAdc() { ADCC |= ADCC_START_ADC_CONV | ADCC_ADC_ENABLE; while (!(ADCC & ADCC_IS_ADC_CONV_READY)) ; adc = ADCRH / 16; ADCC &= ~ADCC_ADC_ENABLE; } void main() { PAC |= (1 << BUZZER_BIT); PBC &= ~(1 << ADC_BIT); PBPH &= ~(1 << ADC_BIT); PBPL &= ~(1 << ADC_BIT); PBDIER &= ~(1 << ADC_BIT); ADCC = ADCC_CH_AD3_PB3; ADCM = ADCM_CLK_SYSCLK_DIV16; while (1) { playMelody(); _delay_ms(4000); } } // Startup code - Setup/calibrate system clock unsigned char _sdcc_external_startup(void) { // Initialize the system clock (CLKMD register) with the IHRC, ILRC, or EOSC clock source and correct divider. // The AUTO_INIT_SYSCLOCK() macro uses F_CPU (defined in the Makefile) to choose the IHRC or ILRC clock source and divider. // Alternatively, replace this with the more specific PDK_SET_SYSCLOCK(...) macro from pdk/sysclock.h AUTO_INIT_SYSCLOCK(); // Insert placeholder code to tell EasyPdkProg to calibrate the IHRC or ILRC internal oscillator. // The AUTO_CALIBRATE_SYSCLOCK(...) macro uses F_CPU (defined in the Makefile) to choose the IHRC or ILRC oscillator. // Alternatively, replace this with the more specific EASY_PDK_CALIBRATE_IHRC(...) or EASY_PDK_CALIBRATE_ILRC(...) macro from easy-pdk/calibrate.h AUTO_CALIBRATE_SYSCLOCK(TARGET_VDD_MV); return 0; // Return 0 to inform SDCC to continue with normal initialization. }
Здесь я играю мелодию Twinkle Twinkle, Little Star на повторе. Код объяснять в деталях не буду, возможно вы уже устали, привожу как есть, может быть кто-то захочет посмотреть.
По флагу CONFIG_PWM_VOLUME вы можете понять где код выполняется для первого эксперимента, а где код для второго. Возможно надо пояснить за второй, когда играю ноту не все время. Все происходит в коде функции задержки: если задержка 100 мс, то я разбиваю эту задержку не на 100 раз по 1 мс, а допустим 10 раз, но по 10 мс, а эти 10 мс я уже разбиваю на время когда звук есть и когда звук нет. Например, можно разбить 10 мс на 16, привести значения АЦП к 16 значениям и пробегаться циклом от нуля до 16 и если число меньше значения АЦП, то звук есть, если больше, то нет. Получается мелодия играет, если на ацп 4, то 4 / 16 = 0.25, получается одну четвертую времени, другими словами громкость должна быть в четыре раза быть меньше (для восприятия звука это не верно, там децибелы и логарифмы, но вы меня поняли).
Как можете видеть эксперимент с варьированием громкости получился криво, в первом случае не дало никакого эффекта (когда изменяем скважность), во втором можно слышать сильные искажения. Возможно можно что-то сделать со вторым случаем, но это надо думать. Как одна из идей, это поставить большой конденсатор, чтобы сглаживать всплески, правда идея была сделать это программно, но можно и это попробовать.
❯ Результат
Чтобы проще было найти, размещаю в конце статьи видео с мелодиями, какие успел сделать на микроконтроллерах Padauk:
Статью я стал писать еще до Нового года и после разошелся и начал заметно больше экспериментировать с пищалками. За моими экспериментами можете наблюдать в группе Планета M039, можете зайти, чтобы посмотреть что еще я успел сделать с пищалками и если все будет хорошо, то постараюсь это оформить в новую статью.
Все файлы находятся в двух уже знакомых репозиториях: avr-playground, pdk-playground.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩

