К микроконтроллерам 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 МГц). Технические характеристики лучше смотрите на сайте производителя, думаю мне не нужно их приводить больше.
У нас они стоят около 10 рублей, можно найти за 6 рублей, а в Китае вроде они за 1 рубль продаются, но точно не знаю. Я про PMS150C. Звучит, конечно, хорошо, но вот официальный программатор, если покупать на Aliexpress стоит около 7к рублей, что не выглядит привлекательно, если хочется просто поиграться на вечер. Но есть решение, это проект Free PDK, на котором есть схема программатора и который появился из YouTube роликов EEVBlog. (Если поищите на маркетплейсах, то сможете найти набор для сборки этого программатора. И продавцу отдельное спасибо за продвижение сообщества Padauk.) Что очевидно, что без этого программатора я бы никогда не решился заниматься 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, если в нем принципиально ничего нового. Но в нем есть код для калибровки микроконтроллера, который находится в _sdcc_external_startup. На самом деле я сильно не вдавался что он делает, но заметил такую особенность, что иногда надо изменить частоту (например, в файле platformio.ini), чтобы примеры кода с работой по UART заработали. Но это косвенно связано с калибровкой.
В любом случае этот код нужно вставлять, но по правде говоря и без него все скомпилируется, просто вам нужно самим где-то задать нужную частоту, чтобы задержки (или что от неё зависят) работали правильно.
Пример компиляции Blink, можете увидеть строчки про калибровку
rocessing 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
Чуть не забыл самое главное сказать, если посмотрите на гифку то увидите, что светодиод находится на макетной плате, а не используется тот, который на отладочной. Это все потому, что нельзя выбрать любой вывод для апаратного 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 нету в библиотеки, а если использовать _delay_ms с произвольным параметром, то происходит деление и памяти для микроконтроллера не хватает, поэтому нашлось решение написать свою функцию 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

Брелок на PMS150G в сборе

Плата до установки компонентов

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

Законченное устройство на 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.
