К микроконтроллерам 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
Пример работы 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_TONEMELODY_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

Брелок на PMS150G в сборе
Брелок на PMS150G в сборе
Плата до установки компонентов
Плата до установки компонентов
Принципиальная схема брелка с PMS150G
Принципиальная схема брелка с PMS150G

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

И неожиданный победитель — ATtiny13!

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


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале 

Перейти ↩