• Об авторе
  • Программы и скрипты
    • Windows
      • BeatCalc
      • Segment Code Generator
    • Плагины для WordPress
      • IE6 Support for Twenty Ten Theme
      • WM-Footnotes
      • WM-Toc
  • Учебные материалы ТТУ
  • Фотогалерея
  • Legal notice

Alex.StarSpirals.Net

инженерный блог

Музыка на ATtiny2313: двухканальный синтезатор

20 августа, 2010 by Алексей

В статье я поведаю о том, как сделать на основе микроконтроллера Atmel ATtiny2313 двухканальный синтезатор прямоугольного сигнала и покажу, как из этого получить простой 8-битный музыкальный проигрыватель. Приводится схема рассматриваемого устройства и полный код на ассемблере.

Более подробно остановимся на следующем:

  • Музыкальная теория, необходимая для реализации музыкального проигрывателя;
  • Генератор частоты на 16-ти разрядных ШИМ-каналах;
  • Конвертация мелодии и удобная запись нот.

Поговорим немного и о работе с оперативной памятью SRAM и организацию работы программы по принципу обработки событий.

Содержание [Скрыть]
  1. 1 Введение
  2. 2 Схема устройства
  3. 3 Немного теории
    1. 3.1 Высота ноты
    2. 3.2 Длительность ноты
  4. 4 Планировка программы
    1. 4.1 Генерация заданной частоты
    2. 4.2 Выдержка длительности нот
    3. 4.3 Управление ходом программы
  5. 5 Программный код
    1. 5.1 Определения символьных имён
    2. 5.2 Макросы
    3. 5.3 Разметка SRAM
    4. 5.4 Таблица прерываний
    5. 5.5 Инициализация и основной цикл
    6. 5.6 Инициализация железа
    7. 5.7 Завершение программы
    8. 5.8 Установка ноты на канале
    9. 5.9 Генератор частоты
    10. 5.10 Обработчик системных часов
  6. 6 Музыкальная композиция
    1. 6.1 Преобразование к формату проигрывателя
    2. 6.2 Таблицы нот и длительностей в ассемблере
    3. 6.3 Запись композиции
  7. 7 Полный код программы
  8. 8 Результат
  9. 9 Примечания

Введение

Когда я только начинал разбираться с микроконтроллерами семейства AVR, я собрал крохотное устройство, где в качестве генератора звука использовался tiny13. Для этого там применялась ШИМ-модуляция гармонических колебаний. Тогда мне казалось, что существенным упущением было генерировать частоту из основного цикла без использования прерываний. Однако позже выяснилось, что модуляция синуса, даже с разрешением 8 бит и даже при наличии таблицы синусов для проигрывания мелодии задача весьма нетривиальная и крайне критичная по времени. В случае ATtiny2313 в наличии всего два таймера, а требуется контролировать ШИМ, модуляцию синуса на двух каналах, длительность нот, а также общий ход программы при сохранении многозадачности.

В итоге я решил, что буду рисовать музыку прямоугольными колебаниями[1]. Вообще моей главной задачей было получить как можно более точную частоту, элегантный способ задания высоты и длительности нот. И, как мне кажется, это было достигнуто.

Схема устройства

Схема (PDF): ATtiny2313_music_player

Для генерации сигнала будем использовать 16-битный таймер, поэтому нам нужны выводы микроконтроллера OC1A и OC1B. С них будем брать левый и правый каналы, предварительно пропустив через RC-фильтр, у которого две основных функции:

  • Работа в режиме интегратора — из ШИМ получается сигнал заданного уровня;
  • Работа в режиме фильтра нижних частот.

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

В данном случае я использовал значения R=470Ω, C=100nF. Это даёт частоту среза 3386.27 Гц, что вполне подходит для проигрывателя.

Насчёт подключения акустической системы: я включал напрямую к маломощному динамику на 8Ω, сил микроконтроллера хватало на достаточно громкое воспроизведения музыки. Причём я сводил два канала в один, просто соединяя два провода вместе. Вообще так делать не рекомендуется, весьма желательно воткнуть между выходом с микроконтроллера и акустической системой операционный усилитель-другой.

Немного теории

Нашей задачей является создание проигрывателя, значит, нужно вспомнить азы музыкальной теории. Композиции состоят из совокупности нот, а у всякой ноты имеется два фундаментальных параметра — высота и длительность.

Высота ноты

Высота ноты задаётся определённой частотой колебаний звукового сигнала. Для расчета периода колебаний по частоте нам будет нужна простейшая формула. К ней мы вернёмся немного позже:

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

Длительность ноты

В музыке длительность ноты понятие относительное и зависит от темпа, в котором исполняется произведение. Темп обычно задаётся в ударах в минуту (bpm), причём один удар равен одной четвертной ноте. Следовательно, запись 100 bpm (или =100) означает, что в минуте помещается ровно 100 четвертных нот. Нетрудно тогда посчитать и длительность одной четвертной ноты, 1/100 = 0.01 минут или 0.6 секунд[2].

Посчитаем для примера реальные длительности нот для темпа =120. Основой будет длительность четвертной ноты, равной в этом случае 60000/120=500 мс.

Обозначение Отношение Длительность (мс)
4/4 (целая) 2000
2/4 (половинная) 1000
1/4 (четвертная) 500
1/8 (восьмая) 250
1/16 (шестнадцатая) 125
1/32 (тридцатьвторая) 62.5

Планировка программы

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

Генерация заданной частоты

Нам нужен прямоугольный сигнал с частотой, соответствующей определённой ноте. Прямоугольник можно получить, чередуя выходные сигналы микроконтроллера 0→1→0→…, причём время стабильного состояния низкого и высокого уровней должно быть одинаковым. Оно будет тогда являться не чем иным, как полупериодом колебания. Следовательно, два раза по столько и получим полный период, откуда можно высчитать и частоту. Таким образом, частоту будем задавать через полупериод колебания.

Для генерации сигнала мы используем 16-битный таймер. Из даташита определим подходящий режим его работы. Это будет нормальный режим, с таким приколом: таймер считает от 0 до 65535, а при достижении значения OCR1A (OCR1B) чередует на соответствующем выводе уровень сигнала (0→1 или 1→0).

Этого, конечно, маловато будет. Ведь если таймер перекинул выход, он затем будет считать ещё 65535 тиков до следующего изменения, как тогда задать полупериод? С одной стороны логично предложить сброс таймера при достижении OCR1A. Но второй канал тогда определённо сильно пострадает.

Можно поступить следующим образом. Включим прерывания по достижении значений OCR1A и OCR1B. В обработчике такого прерывания будет код, прибавляющий к текущему значению регистра аккурат ровно столько же тиков, сколько нужно для задания полупериода. То есть, например, полупериод задан 2300 тиками таймера, текущее значение на момент прерывания было 4100. Тогда следующее прерывание должно произойти в момент времени 2300+4100=6400. Это значение и занесём в регистр. Вот и готов наш генератор прямоугольной частоты.

Давайте теперь рассмотрим конкретные цифры. Согласно нашей рабочей схеме, ATtiny2313 работает от кристалла с частотой 20.000 МГц. Тогда если взять предделитель /8 таймера1, получим, что длительность одного тика таймера равна:

Иными словами, мы можем задавать длительность полупериода с разрешением в 0.4 микросекунд. Выведем формулу для определения количества тиков таймера из исходной частоты:

Число тиков для, скажем, частоты 440.000 Гц (соответствует ноте ля 4-ой октавы — A4) определяется так:

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

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

Выдержка длительности нот

Поскольку у нас остался один восьмибитный таймер, то для задания времени звучания нот на каждом из каналов понадобятся софт-таймеры (то есть полученные программно). «Железный» же таймер заставим работать как генератор прерываний; в обработчик прерывания засунем код, который будет поддерживать правильную работу системы и следить за софт-таймерами.

Возьмём режим работы таймера0: сброс при достижении OCR0A. В качестве предделителя используем /64. Будем генерировать прерывания каждые 50 тиков, следовательно в регистр OCR0A требуется занести значение 49. При таком подходе каждое такое прерывание назовём тиком системных часов. Время одного тика:

Получаем разрешение для софт-таймеров в 0.16 мс. Этого хватит и для достаточно высокого темпа воспроизведения.

Софт-таймеры длительности нот организуем таким образом, чтобы они считали в обратную сторону. Оттикав заданное число, раз они инициируют смену ноты. Число тиков этих таймеров зависит от реальной длительности ноты следующим образом:

где tnote — длительность ноты в миллисекундах. Так, например, для длительности восьмой ноты в темпе =120 количество тиков будет равно:

Обратно же, в наличии некоторая погрешность, но куда без неё? :-)

Управление ходом программы

Сделаем так, что при нажатии одной кнопки начинается воспроизведение. При этом загорается светодиод. Программа сбрасывается в двух случаях (при этом светодиод гаснет):

  1. Вся мелодия сыграна;
  2. Нажата вторая кнопка.

Управление будем осуществлять из главного цикла программы.

Программный код

Ну вот мы и подошли, похоже, к самой интересной части статьи. Рассматривать код будем по кусочкам.

Определения символьных имён

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

 ; Проект для ATtiny2313
.INCLUDE "tn2313def.inc"

; ---------------------
; Определения регистров
; ---------------------

.DEF	SrSave	= R9		; Хранение регистра состояния

.DEF	Tone1H	= R10		; Старший байт тона канала 1
.DEF	Tone1L	= R11		; Младший байт тона канала 1
.DEF	Tone2H	= R12		; Старший байт тона канала 2
.DEF	Tone2L	= R13		; Младший байт тона канала 2

.DEF	ToneTH	= R14		; Старший байт тона
.DEF	ToneTL	= R15		; Младший байт тона

.DEF	Temp	= R17		; Временный регистр

.DEF	Temp1 	= R18		; Вспомогательные регистры
.DEF	Temp2	= R19
.DEF	Temp3	= R20
.DEF 	Temp4	= R21

.DEF	ADDRH	= R22		; Старший байт адреса
.DEF 	ADDRL	= R23		; Младший байт адреса

Регистры R10—R13 используются для хранения тона на протяжении всей длительности ноты для генерации частоты (можно использовать SRAM, но тут нам важна скорость отработки прерываний, так что лучше будет хранить данные в регистрах). R16 же я не использовал, потому что он активно задействован в макросах.

Макросы

Я использовал всего четыре макроса. Вот они:

 ; --------------------
; Определения макросов
; --------------------

; OUTI port, value :: отправить значение в порт
.MACRO OUTI
	ldi R16,@1 			
	out @0,R16
.ENDM

; MEMR register, (location) :: считать из памяти в регистр
.MACRO MEMR
	lds @0, @1
.ENDM

; MEMW (location), register :: записать в память регистр
.MACRO MEMW
	sts @0, @1
.ENDM

; MEMWI (location), value :: записать значение в память
.MACRO MEMWI
	ldi R16, @1
	sts @0, R16
.ENDM	

Из них реально полезными являются OUTI и MEMWI, два других, MEMR и MEMW, просто псевдонимы соответственно инструкций lds и sts, введённые исключительно для моего удобства.

Разметка SRAM

Для использования оперативной памяти в программах можно предварительно разметить необходимую область памяти под свои нужды. Делается это при помощи директивы .DSEG, последующих меток и соответствующих меткам директив .BYTE, «выделяющих» в памяти необходимое количество байт[3].

Для нашей программы зарезервируем всего 8 байт. В первых четырех будут хранится текущие адреса таблицы нот для первого и второго каналов. Остальные байты пойдут под 16-ти разрядные софт-таймеры.

Получаем такую структуру:

 ; -------------
; Разметка SRAM
; -------------

.DSEG

; Позиция первой мелодии
melody1:
	.BYTE 2

; Позиция второй мелодии
melody2:
	.BYTE 2

; Текущие длительности нот
duration1:
	.BYTE 2

duration2:
	.BYTE 2

Таблица прерываний

Согласно плану программы, нам нужно определить обработчики для трёх прерываний. Не стоит забывать также и о директивах .CSEG и .ORG 0x0000, означающих начало кода программы с адреса 0x0000.

 ; ---------------------
; Начало кода программы
; ---------------------

.CSEG
.ORG 0x0000

; ------------------
; Таблица прерываний
; ------------------

rjmp Init ; Обработчик сброса
reti
reti
reti
rjmp TIMER1_COMPA ; Обработчик CompareA таймера1
reti
reti
reti
reti
reti
reti
reti
rjmp TIMER1_COMPB ; Обработчик CompareB таймера1
rjmp TIMER0_COMPA ; Обработчик CompareA таймера0
reti
reti
reti
reti
reti

Остальные прерывания у нас не используются, и на них стоит заглушка в виде инструкции reti.

Инициализация и основной цикл

 ; -------------------------
; Начало основной программы
; -------------------------

Init:
	
	; Ждать, пока не нажата кнопка1
	cbi DDRD, 4
	sbi PORTD,4

waitkey:
	
	sbic PIND, 4
	rjmp waitkey
	
	; Задать выходы
	; (изначально отключить вывод прямоугольных колебаний)
	outi DDRB, (1<<PB0) | (0<<PB3) | (0<<PB4)

	; Задать стек
	outi SPL, low(RAMEND)
	
	; Включить светодиод
	sbi PORTB, 0
	
	; Инициализировать железо
	rcall Timer1_Init
	rcall TIMER_Init

	; Установить прерывания
	outi TIMSK, (1<<OCIE1A) | (1<<OCIE1B) | (1<<OCIE0A)

	; Записать изначальные локации мелодий в память
	
	; 1 канал
	memwi melody1, high(tblMelody1*2)
	memwi melody1+1, low(tblMelody1*2)
	
	; 2 канал
	memwi melody2, high(tblMelody2*2)
	memwi melody2+1, low(tblMelody2*2)

	; Очистить регистры сравнения
	outi OCR1AH, 0
	outi OCR1AL, 0

	outi OCR1BH, 0
	outi OCR1BL, 0

	; Установить ноты
	rcall setCh1
	rcall setCh2
	
	; Очистить таймер0
	outi TCNT0, 0

	; Очистить таймер1
	outi TCNT1H, 0
	outi TCNT1L, 0

	; Сбросить предделитель
	outi GTCCR, (1<<PSR10)

	; Включить прерывания и начать игру
	sei

back2:
	
	; Сброс, если кнопка2 нажата
	sbi PORTD,5
	
	sbis PIND, 5
	rjmp endplay

	rjmp back2

Обратите внимание, что стек нужно задать в явном виде. Поскольку на борту tiny2313 всего 128 байт оперативной памяти, то старшего байта у регистра стека нет.

Основной цикл программы в петле с возвращением в метку back2. Здесь проверяется нажатие второй кнопки, всё остальное организовано в обработчике системных часов.

Инициализация железа

Тут нужно просто задать параметры для двух таймеров.

 ; ----------------------
; Инициализация железа
; ----------------------

; Инициализация таймера1
Timer1_Init:

	; Включить смену уровней при сравнении
	outi TCCR1A, (1<<COM1A0) | (1<<COM1B0) | (0<<WGM10)

	; Включить таймер с предделителем /8
	outi TCCR1B, (2<<CS10)
	
	ret

; Инициализация таймера0 (системного таймера)
TIMER_Init:

	; Установить режим работы
	outi TCCR0A, (1<<WGM01) | (0<<WGM00)

	; Установить значение переполнения
	outi OCR0A, 49

	; Установить предделитель /64
	outi TCCR0B, 3

	ret

Завершение программы

Как было сказано, завершение[4] происходит в случае окончания проигрывания мелодии или нажатия кнопки 2.

 ; ---------------------
; Конец воспроизведения
; ---------------------

ENDPLAY:
	
	; Игра завершена или прервана
	cli
	
	; Очистить порты
	clr R16
	out PORTB, R16

	; Отменить воспроизведение на обоих каналах
	cbi DDRB, 3
	cbi DDRB, 4
	
	; Очистить флаги прерываний
	outi TIFR, (1<<OCF1A) | (1<<OCF1B) | (1<<OCF0A)
 
	rjmp Init

Установка ноты на канале

Рассмотрим код для установки ноты на канале. Всего у нас будет четыре таблицы данных, которые мы разместим во флеш-памяти, поскольку в EEPROM они целиком всё равно не влезут (да и скорость доступа оттуда низкая). Таблицы будут такими:

  • Таблица нот
  • Таблица длительностей
  • Таблица мелодии на канале 1
  • Таблица мелодии на канале 2

Все значения в таблицах будут кодироваться словами (2 байта). В случае нот мы кодируем высоту и длительность, а мелодии состоят из пар байт нота-длительность (плюс управляющие слова). Не обязательно заносить в таблицы нот и длительности все ноты подряд, достаточно лишь тех, что используются в конкретном произведении. Это позволяет сэкономить место.

Рассмотрим код для первого канала (код для второго канала отличается именами используемых регистров и адресов памяти, в остальном идентичен).

 ; -----------------------------------------
; Функции по управлению тона и длительности
; -----------------------------------------

; ------------------
; Установка канала 1
; ------------------
setCh1:
	
	; Загрузить ноту и длительность
	memr ZH, melody1
	memr ZL, melody1+1
	
	; Взять байт ноты
	lpm
	mov Temp1, R0

	; Взять байт длительности
	adiw zl, 1
	lpm
	mov Temp2, R0

	; Сохранить адрес следующей ноты
	adiw ZL, 1
	memw melody1, ZH
	memw melody1+1, ZL
	
	; Проверка на управляющий байт конца
	cpi Temp1, 255
	breq endplay

	; Проверка на паузу
	cpi Temp1, 0
	breq isPause1

	dec Temp1
	
	ldi ADDRH, high(tblNotes*2)
	ldi ADDRL, low(tblNotes*2)
	rcall getVals

	; Получить значение тона
	mov Tone1H, Temp3
	mov Tone1L, Temp4

	in ToneTH, OCR1AH
	in ToneTL, OCR1AL
	
	add ToneTL, Tone1L
	adc ToneTH, Tone1H

	out OCR1AH, ToneTH
	out OCR1AL, ToneTL

	; Включить вывод сигнала на первом канале
	sbi DDRB, 3

	rjmp noPause1
	
isPause1:
	
	; Пауза — отключить вывод сигнала на первом канале
	cbi DDRB, 3

noPause1:

	; Загрузить длительность
	ldi ADDRH, high(tblDuration*2)
	ldi ADDRL, low(tblDuration*2)
	mov Temp1, Temp2
	rcall getVals

	memw duration1, Temp3
	memw duration1+1, Temp4

	; Выход из процедуры
	ret

endplay1:
	rjmp endplay

В коде используется вызов процедуры getVals. Она просто считывает данные из указанного адреса флеш-памяти и заносит в регистры Temp3 и Temp4:

 ; --------------------------------------
; Загрузить значения из памяти программы
; --------------------------------------

getVals:
	
	mov ZH, ADDRH
	mov ZL, ADDRL

	lsl Temp1

	clr R16
	add ZL, Temp1
	adc ZH, R16

	lpm
	mov Temp4, R0

	adiw ZL, 1
	lpm
	mov Temp3, R0

	ret

Кроме того, есть переход — rjmp endplay, он здесь потому, что дальше программе было не достать метку endplay (из-за ограниченности длины прыжка, максимально равной 127 байт) и пришлось использовать вспомогательный переход.

Генератор частоты

Рассмотрим первый канал:

 ; --------------------------
; Управления тоном канала 1
; --------------------------

TIMER1_COMPA:
	
	; Отключить прерывания
	cli
	
	; Сохранить SREG
	in SrSave, SREG

	; Задать следующий полупериод
	in ToneTH, OCR1AH
	in ToneTL, OCR1AL
	add ToneTL, Tone1L
	adc ToneTH, Tone1H
	out OCR1AH, ToneTH
	out OCR1AL, ToneTL
	
	; Восстановить SREG
	out SREG, SrSave
	
	; Включить прерывания
	sei

	reti

Всё согласно рассуждениям выше. Кроме того:

  • Чтобы не произошло произвольной фигни, нужно отключать прерывания
  • Также, поскольку контроль тона может происходит часто, прерывания могут возникнуть где угодно в программе и грохнуть SREG, что опять же приведёт к рандомной фигне, так что его надо сохранять и восстанавливать

Всё вышесказанное справедливо и для второго канала.

Обработчик системных часов

Тот самый, который тикает раз в 160 микросекунд. Вот код:

 ; --------------------------
; Главный обработчик событий
; --------------------------

TIMER0_COMPA:

	; Проверить длительность на 1 канале
	memr Temp3, duration1
	memr Temp4, duration1+1
	
	clr R16
	subi Temp4, 1
	sbc Temp3, R16
	brcc saveDur1
	
	; Загрузить следующую ноту
	cli
	rcall setCh1
	sei

	rjmp nextDur

saveDur1:
	
	memw duration1, Temp3
	memw duration1+1, Temp4

nextDur:

	memr Temp3, duration2
	memr Temp4, duration2+1

	clr R16
	subi Temp4, 1
	sbc Temp3, R16
	brcc saveDur2

	cli
	rcall setCh2
	sei

	rjmp next1

saveDur2:

	memw duration2, Temp3
	memw duration2+1, Temp4

next1:
	
	reti 

На этом программный код готов. Теперь нужна какая-нибудь музыка, которую наш проигрыватель будет играть.

Музыкальная композиция

Lilium.mid

Здесь я на конкретном примере покажу, каким образом конвертировать заданную композицию в формат, понятный нашему плееру.

В качестве мелодии для проигрывателя я выбрал весьма красивую титульную тему Lilium из аниме Elfen Lied.

У нас двухканальный (двухголосый) синтезатор, значит нам нужно расписать композицию на два голоса, что я и сделал. Использовал я для этого MIDI-файл.

Как перекладывать музыку вариантов может быть несколько. Тут нам нужен какой-нибудь MIDI-редактор, способный показывать ноты во вменяемом виде[5]. Очень удобно использовать режим просмотра piano roll (пример в Nuendo 3), так как тут наглядно видно и ноты, и их длительности.

Преобразование к формату проигрывателя

Конвертацию делать придётся вручную (автоматические средства пока не доступны): выписываем каждую ноту ровно один раз и подбираем для неё число тиков по приведённой выше формуле. Смотрим, первая нота F#4. Посмотрим по таблице частоту этой ноты. Увидим значение 369.994. Значит, число тиков будет

Выпишем в таком духе все ноты, участвующие в произведении, и посчитаем для них число тиков таймера. Удобно для расчётов использовать Microsoft Excel или какой-нибудь математический пакет вроде Scilab или Maxima. Я, например, заготовил таблицу (Excel 2003) и PDF файл.

Затем пройдёмся по композиции ещё раз и выпишем все участвующие в нём длительности. Вычислим и для них соответствующее число тиков софт-таймера, учитывая темп произведения =60, что значит, что длительность четвертной ноты равна 1 секунде. Не забываем также и о паузах.

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

Таблицы нот и длительностей в ассемблере

Таблица будет состоять из слов (в данном случае размером два байта), значит нужно воспользоваться директивой .DW. Составим таблицу нот:

 ; --------------
; Таблицы данных
; --------------

; Таблица нот :: тон = 1/(8e-7*N)
tblNotes:
	.dw 11364	; A2
	.dw 10124	; B2
	.dw 9556	; C3
	.dw 9019	; C#3
	.dw 8513	; D3
	.dw 7584	; E3
	.dw 7159	; F3
	.dw 6757	; F#3
	.dw 6020	; G#3
	.dw 5682	; A3
	.dw 5363	; A#3
	.dw 5062	; B3
	.dw 4510	; C#4
	.dw 4257	; D4
	.dw 4018	; D#4
	.dw 3792	; E4
	.dw 3579	; F4
	.dw 3378	; F#4
	.dw 3010	; G#4
	.dw 2841	; A4
	.dw 2681	; A#4
	.dw 2531	; B4
	.dw 2389	; C5
	.dw 2255	; C#5
	.dw 2128	; D5
	.dw 1896	; E5
	.dw 1689	; F#5

Воспользуемся на этом этапе константами. Это очень удобное средство сопоставить положение ноты в таблице конкретному символьному имени, которое можно будет использовать при записи мелодии. Константы вводятся в эксплуатацию директивой .EQU.

 ; Зададим символьные имена нот

; Они отмечают реальные позиции
; нот в tblNotes

; Специальные значения: PAUSE = 0, END = 255

	.equ 	PAUSE	=	0
	.equ 	A2		=	1
	.equ 	B2		=	2
	.equ 	C3		=	3
	.equ 	Cs3		=	4
	.equ 	D3		=	5
	.equ 	E3		=	6
	.equ 	F3		=	7
	.equ 	Fs3		=	8
	.equ 	Gs3		=	9
	.equ 	A3		=	10
	.equ 	As3		=	11
	.equ 	B3		=	12
	.equ 	Cs4		=	13
	.equ 	D4		=	14
	.equ 	Ds4		=	15
	.equ 	E4		=	16
	.equ 	F4		=	17
	.equ 	Fs4		=	18
	.equ 	Gs4		=	19
	.equ 	A4		=	20
	.equ 	As4		=	21
	.equ 	B4		=	22
	.equ 	C5		=	23
	.equ 	Cs5		=	24
	.equ 	D5		=	25
	.equ 	E5		=	26
	.equ 	Fs5		=	27
	.equ	END		=	255

Сделаем то же самое и для таблицы длительностей:

 ; Таблица длительностей :: bpm=60
; Tim0 /64, 50тиков -> 1.6e-4 секунд на CMP эвент

tblDuration:
	.dw 25000	; полная нота
	.dw 18750	; 1/2 с точкой
	.dw 12500	; 1/2
	.dw 6250	; 1/4
	.dw 3125	; 1/8

; Символьные имена длительностей

	.equ d1		= 0
	.equ d12dot	= 1
	.equ d12	= 2
	.equ d14	= 3
	.equ d18	= 4

Обратите внимание, что тут я использовал обозначение целой ноты как d1, а половинной — d12, хотя корректнее было бы использовать d44 и d24. Однако мне так было удобнее.

Запись композиции

Когда всё это сделано, можно начинать записывать саму композицию, в два прохода — сначала один канал, а затем второй. Вот что получилось:

 ; Канал первой мелодии
tblMelody1:
	.db Fs4, d18	; Часть 1
	.db Cs5, d18
	.db Gs4, d18
	.db Gs3, d18
	.db Cs4, d18
	.db A4, d18
	.db A4, d18
	.db A3, d18
	.db Fs4, d18
	.db Cs5, d18
	.db Gs4, d18
	.db Gs3, d18
	.db Cs4, d18
	.db A4, d18
	.db B4, d18
	.db A4, d18
	.db Fs4, d18
	.db A3, d18
	.db D4, d18
	.db A3, d18
	.db B3, d18
	.db Gs4, d18
	.db E4, d18
	.db D4, d18
	.db E4, d18
	.db Gs3, d18
	.db Cs4, d18
	.db Gs3, d18
	.db As3, d18
	.db Fs4, d18
	.db D4, d18
	.db Cs4, d18
	.db D4, d18
	.db E4, d18
	.db Fs4, d14
	.db PAUSE, d18
	.db Gs4, d18
	.db A4, d18
	.db B4, d18
	.db A4, d18
	.db Cs4, d18
	.db Fs4, d18
	.db A4, d18
	.db Gs4, d14
	.db Fs4, d18 ; Часть 2
	.db Cs5, d18
	.db Gs4, d18
	.db Gs3, d18
	.db Cs4, d18
	.db A4, d18
	.db A4, d18
	.db A3, d18
	.db Fs4, d18
	.db Cs5, d18
	.db Gs4, d18
	.db Gs3, d18
	.db Cs4, d18
	.db A4, d18
	.db B4, d18
	.db A4, d18
	.db Fs4, d18
	.db A3, d18
	.db D4, d18
	.db A3, d18
	.db B3, d18
	.db Gs4, d18
	.db E4, d18
	.db D4, d18
	.db E4, d18
	.db Gs3, d18
	.db Cs4, d18
	.db Gs3, d18
	.db As3, d18
	.db Fs4, d18
	.db D4, d18
	.db Cs4, d18
	.db D4, d18
	.db B4, d18
	.db A4, d18
	.db Gs4, d18
	.db Fs4, d18
	.db F4, d18
	.db Ds4, d18
	.db F4, d18
	.db Fs4, d18
	.db As4, d18	; Часть 3
	.db B4, d18
	.db Cs5, d18
	.db D5, d18
	.db A4, d18
	.db Gs4, d18
	.db D5, d18
	.db Cs5, d18
	.db E5, d18
	.db A4, d18
	.db Cs5, d18
	.db C5, d18
	.db A4, d18
	.db Gs4, d18
	.db Fs4, d18
	.db A4, d18
	.db Cs4, d18
	.db Fs4, d18
	.db B4, d18
	.db Gs4, d12dot
	.db Fs4, d18	; Часть 4
	.db Cs5, d18
	.db Gs4, d18
	.db Gs3, d18
	.db Cs4, d18
	.db A4, d18
	.db A4, d18
	.db A3, d18
	.db Fs4, d18
	.db Cs5, d18
	.db Gs4, d18
	.db Gs3, d18
	.db Cs4, d18
	.db A4, d18
	.db B4, d18
	.db A4, d18
	.db Fs4, d18
	.db A3, d18
	.db D4, d18
	.db A3, d18
	.db B3, d18
	.db Gs4, d18
	.db E4, d18
	.db D4, d18
	.db E4, d18
	.db Gs3, d18
	.db Cs4, d18
	.db Gs3, d18
	.db As3, d18
	.db Fs4, d18
	.db D4, d18
	.db Cs4, d18
	.db D4, d18
	.db B4, d18
	.db A4, d18
	.db Gs4, d18
	.db Fs4, d18
	.db F4, d18
	.db Ds4, d18
	.db F4, d18
	.db Fs4, d18
	.db Cs4, d18
	.db Fs4, d18
	.db B4, d18
	.db Fs5, d12

	.db END, 0

; Канал второй мелодии
tblMelody2:
	.db PAUSE, d14
	.db F3, d12
	.db Fs3, d14
	.db A2, d18
	.db PAUSE, d18
	.db F3, d12
	.db Fs3, d14
	.db D3, d1
	.db Cs3, d12
	.db Fs3, d12
	.db B2, d12
	.db C3, d12
	.db Cs3, d12
	.db Gs3, d12
	.db F3, d12
	.db Fs3, d14
	.db A2, d18
	.db PAUSE, d18
	.db F3, d12
	.db Fs3, d14
	.db D3, d1
	.db Cs3, d12
	.db Fs3, d12
	.db B2, d14
	.db C3, d14
	.db Cs3, d14
	.db F3, d14
	.db Fs3, d12
	.db B3, d14
	.db E3, d14
	.db A3, d14
	.db Fs3, d14
	.db A3, d14
	.db C3, d14
	.db Cs3, d12
	.db Cs4, d12dot
	.db PAUSE, d14
	.db F3, d12
	.db Fs3, d14
	.db A2, d18
	.db PAUSE, d18
	.db F3, d12
	.db Fs3, d14
	.db D3, d1
	.db Cs3, d12
	.db Fs3, d12
	.db B2, d14
	.db C3, d14
	.db Cs4, d14
	.db F3, d14
	.db Fs3, d12
	.db A4, d12

	.db END, 0

Полный код программы

Вот полный код (комментарии на английском): ATtiny2313XBoard_MusicPlayer.asm

В полученной прошивке код и данные занимают где-то по 500 байт, оставляя свободным ещё 1КБ для разнообразного контента, будь то дополнительный код или музыка.

Результат

Запрограммируем контроллер, не забывая выставить фузы на работу от внешнего кристалла (CKSEL3...0 = 0). Нажмём на кнопку и из микроконтроллера начинает стремиться музыка:

[audio:http://alex.starspirals.net/wp-content/uploads/2010/08/Lilium_ATtiny2313_xBoard.mp3]

Задача выполнена :-) Надеюсь, материал был для вас полезным! Через некоторое время последует вторая часть статьи, посвещенная MIDI и последовательной передаче данных в контексте того же ATtiny2313.

Успехов!


Примечания

  1. ↑Вот наглядное сравнение двух видов колебаний:

    В случае с микроконтроллером за отрезок на рисунке [-1; 1] принимаются значения напряжения на выходе порта [0; 5] вольт (при напряжении питания +5В).

    Пример звучания синуса на 440 Гц:
    [audio:http://alex.starspirals.net/wp-content/uploads/2010/08/440HzSine.mp3|titles=440Hz, sine wave]

    А вот прямоугольный сигнал, 440 Гц:
    [audio:http://alex.starspirals.net/wp-content/uploads/2010/08/440HzSquare.mp3|titles=440Hz, square wave]

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

  2. ↑В дальнейших вычислениях нам будет удобнее всего измерять длительность нот в миллисекундах.
  3. ↑Разметка делается лишь для удобства обращения к памяти из программы и не гарантирует сохранения границ между данными так, как это задумывает автор.
  4. ↑Если возник вопрос, почему я не воткнул завершение программы в самый конец кода, то отвечу: потому что ATtiny2313 не поодерживает инструкцию перехода jmp, а поэтому блоки кода нужно расставлять по принципу досягаемости переходов.
  5. ↑Из бесплатных редакторов приходит в голову только Anvil Studio, однако когда пытаешься пользоваться этим куском софта, начинаешь понимать в чем весь прикол бесплатных программ. Шутка ли — ковать MIDI на наковальне.
Posted in: Музыка, Программирование, Электроника Tagged: 2 channel, 8 bit, assembler, atmel, attiny2313, avr, avr assembler, diy, lilium, mp3, music player, музыка, проигрыватель, сделай сам, синтезатор, Электроника
Август 2010
Пн Вт Ср Чт Пт Сб Вс
 1
2345678
9101112131415
16171819202122
23242526272829
3031  
« Июн   Окт »
Сайт работает при поддержке лучшего хостинга в Эстонии

Рубрики

  • ProgTime
  • Арт
  • Видео
  • Заметки
  • Музыка
  • Программирование
  • Размышления
  • Разное
  • Рецензии
  • События
  • Ссылки
  • ТТУ
  • Фото
  • Электроника

Метки

apache art-rock assembler atmel attiny13 attiny2313 avr avr assembler avrdude design diy ftp iq megadrum metal midi mp3 music php prog progressive ProgTime usbasp wap Арт Электроника арт-рок блог гитара музыка настройка сервера подсознание программирование прогрессив прогрессивная музыка рок сделай сам синтезатор скачать снег сознание соло фото фотография хороший дизайн

Мета

  • Войти
  • Лента записей
  • Лента комментариев
  • WordPress.org

Copyright © 2023 Alex.StarSpirals.Net.

Omega WordPress Theme by ThemeHall