Генерация музыкального звука на Atmega328

andriano
andriano аватар
Онлайн
Зарегистрирован: 20.06.2015

Требуется генерить периодический сигнал с регулируемой скважностью и частотой, изменяемой в пределах 16-4160 Гц с точностью не хуже 0.2%. В наихудшем случае указанная погрешность должна обеспечиваться до частот 1 кГц, увеличиваясь к 4 кГц не более чем до 0.4%.

По диапазону и точности идеально подходит режим таймера 1 Phase Correct PWM с коэффициентом предделения 8.

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

Управление частотой звука (вибрато) осуществляется по прерыванию. Но прерыванием я пользуюсь стандартным - от 0 таймера по совету ЕвгенийП http://arduino.ru/forum/programmirovanie/etyudy-dlya-nachinayushchikh-me...
Т.е. нужно править включаемые файлы.

Как вариант - запрограммировать на прерывание таймер 2.

К аналоговому входу А1 нужно подключить потенциометр 0-5В (регулирует глубину вибрато), а к 9 пину через делитель примерно 1/10 вход аудио (осциллограф).
 

unsigned long icr; // ICR+1;
unsigned char vibrato = 0;

void Every_1024us(const unsigned long ii) {
  static int counter; // при частоте вызовов 978 Гц частоте 7 Гц соотв. период 140 тиков
  if (counter < 70) {
    ICR1=(icr-1) + (counter - 35)*vibrato/64;
  } else {
    ICR1=(icr-1) + (105 - counter)*vibrato/64;
  }
  OCR1A = ICR1/7;  // скважность 1/7
  counter++;
  if(counter == 140) counter = 0;
}

void setup() {
  Serial.begin(115200);
  pinMode (9,OUTPUT); // выход генератора PB2
  TCCR1A=1<<COM1A1; //подключить выход OC1A первого таймера
  TCCR1B=2|(1<<WGM13);; // //  divider=8;
  icr = 2273; 
  Timer0_Hook = Every_1024us;
}

void loop() {
  delay(500);
  vibrato = analogRead(A0)/4;
  Serial.print(vibrato);
  Serial.print('\t');
  Serial.println(35*vibrato/64);
}

Предполагаемые источники проблемы:

1. При занесении коэффициента деления в 16-разрядный регистр таймера старший и младший байты заносятся неодновременно, в результате чего коэффициент оказывается совсем не тем, который нужен.

2. В момент, когда таймер почти досчитал до конца, в регист заносится меньшее число, в результате чего тайме "пропускает" момент окончания счета.

Возможны ли какие другие источники проблемы?

Как с этим бороться?

dimax
dimax аватар
Онлайн
Зарегистрирован: 25.12.2013

andriano, думаю ситуация (1) маловероятна, иначе бы везде писали про возможность такой проблемы. А вот то что у вас ICR1 и OCR1A записываются не одновременно ( и не синхронизируясь по счёту таймера1)  -может повлиять, т.к. эти регистры перечитываются при переходе TCNT1 через ноль. Если новое значение ICR1 прочиталось, а OCR1A не успело -как раз может быть пропуск. Можно попробовать произвести рассчёт значений ICR1 OCR1A записав новые значения во временные переменные. Затем последовательными командами остановить таймер1, переписать регистры ICR1 OCR1A и запустить таймер1.

andriano
andriano аватар
Онлайн
Зарегистрирован: 20.06.2015
dimax, а остановка таймера не создаст помеху в сигнале сама по себе? То есть, если мы приостановим таймер, у нас длина периода получиться больше, чем нужно?
Может, лучше отслеживать "опасные" моменты и в эти моменты просто пропускать запись в порты таймера? Ну обновиться он не через 1 мс, а через две - на слух гораздо менее заметно (скорее - вообще не заметно), чем случайные флуктуации периода генерируемого тона.
 
PS. Да, по поводу "ситуации 1", она как раз описана (я читал у А. В. Евстифеева, которого, кажется, Вы мне и порекомендовали), а также описан способ ее преодоления благодаря имеющимся аппаратным средствам (служебный 8-разрядный регистр). Но при определении констант (файл iom328p.h) 16-разрядный регистр ICR1 описан как ячейка памяти _SFR_MEM16(0x86), поэтому нет уверенности, что с памятью этот механизм сработает.
 
PPS. И еще подумалось, если что-то не успело, может, важен порядок, в котором записываются регистры? И еще: одним регистром мы устанавливаем длительность периода, а другим - длительность положительного полупериода. Если мы что-то не успели вовремя переопределить, то одна из величин будет взята старая, а вторая - новая. В результате чуть изменится скважность, но к пропуску генерации это не должно привести. Так что мне тут что-то непонятно.
dimax
dimax аватар
Онлайн
Зарегистрирован: 25.12.2013

andriano, по идее остановка таймера и передача трёх команд  затянет текущий фронт импульса где-то на 5 тактов МК. Вряд ли это можно будет уловить слухом ) В любом случае попробуйте остановить таймер перед записью регистров, хотя бы в качестве эксперимента.

andriano
andriano аватар
Онлайн
Зарегистрирован: 20.06.2015

dimax пишет:

А вот то что у вас ICR1 и OCR1A записываются не одновременно ... Если новое значение ICR1 прочиталось, а OCR1A не успело -как раз может быть пропуск.

Решил проверить.

Я так понимаю, проверка должна состоять в том, что если убрать эту неодновременность, то должен исчезнуть и дефект.

Один из способов убрать неодновременность - вообще отказаться от изменени OCR1A - пусть скважность будет плавать вместе с частотой. Собственно, для меня такой способ проверки проще, т.к. я еще не разобрался, как остановить таймер, а потом пустить его с того же места.

Перенес установку OCR1A из прерывания в setup. Дефект остался. Так что думаю, одновременность тут ни при чем.

Да, провал в генерации - где-то порядка 30 мс, т.е. очень похоже на полный полупериод счетчтика, считающего до упора (до 65535).

 

Да, еще: по 5 изданию Евстифеева Микроконтроллеры AVR семейств Tiny и Mega фирмы ATMEL

стр. 298 (13.6.3.5. Режим Phase and Frequency Correct PWM)

Цитата:
Как и в остальных режимах, при работе с какими либо фиксированными значениями модуля счета, для задания его рекомендуется использовать регистр захвата. При этом регистр OCR1A (OCR3A) может использоваться для формирования ШИМсигнала. Если же в процессе формирования ШИМ сигнала его частота меняется очень часто, для задания модуля счета рекомендуется использовать регистр сравнения.

Вот так4ая вот рекомендация. А я меняю модуль счета почти 1000 раз в секунду.

Только этот совет я не понял, как реализовывать. Если мы использовали ICR1 для задания периода, а OCR1A - полупериода, то если использовать OCR1A для периода, что использовать для полупериода? OCR1B? ICR1? Во втором случае полдучается хрен редьки не слаще, т.к. оба регистра нужно менять с одинаковой частотой.

andriano
andriano аватар
Онлайн
Зарегистрирован: 20.06.2015

dimax пишет:

andriano, по идее остановка таймера и передача трёх команд  затянет текущий фронт импульса где-то на 5 тактов МК. Вряд ли это можно будет уловить слухом ) В любом случае попробуйте остановить таймер перед записью регистров, хотя бы в качестве эксперимента.

Ладно, завтра снова буду перечитывать Евсифеева, и как только пойму, как остановить счет, не обнуляя регистр, попробую.

Сегодня же думаю, раз регистр обновляется при переходе через 0, значит, в этот момент и может происходить сбой. Думаю запрещать переустановку частоты, если до конца счета осталось 1-3 тика таймера. Кстати, как отличить число 3, считанное из счетчика в полупериод его инкркмента, от такого же 3, считанного во время декремента?

dimax
dimax аватар
Онлайн
Зарегистрирован: 25.12.2013

andriano пишет:

Ладно, завтра снова буду перечитывать Евсифеева, и как только пойму, как остановить счет, не обнуляя регистр, попробую.

Не обнуляя чего?? Остановить просто - TCCR1B=0, снова запустить TCCR1B=18;  Между ними обновить ICR1, делов-то на пять минут проверить этот вариант.

andriano
andriano аватар
Онлайн
Зарегистрирован: 20.06.2015

Пойду посмотрю.

Но у меня сомнения по поводу того, что это будет незаметно на слух. Сейчас дефекты большие, но их частота менее 1 в секунду. Если мы будем останавливать таймер, то это 1000 раз в секунду. Т.е. с такой частотой у нас будет увеличиваться период. Возникнут интермодуляционные искажения - появятся негармонические частоты равные суммам и разностям частот музыкального тона и частоты прерываний. А негармонические частоты очень заметны.

andriano
andriano аватар
Онлайн
Зарегистрирован: 20.06.2015

Это жуть - теперь сигнал состоит практически из одних помех.

dimax
dimax аватар
Онлайн
Зарегистрирован: 25.12.2013

andriano, ну значит остался последний вариант - перепрограммировать ICR1 сразу после перехода TCNT1 через 0. Думаю корректно это можно сделать только в собственном прерывании по переполнению тамера1.  TIMSK1=1<<TOIE1; ISR (TIMER1_OVF_vect) { ..... }

 

andriano
andriano аватар
Онлайн
Зарегистрирован: 20.06.2015

Сегодня читал не Евстфеева, а оригинальную 660-страничную документацию.

То ли оригинал всегда лучше копии, то ли "повторение мать учения", но выход нашел. Собственно, мы зациклились на переходе через 0, и в этом была наша ошибка, т.к. отслеживать имеет смысл, наоборот, переход через TOP.

Вот такой фрагмент решил проблему.

  unsigned int tmpOCR1A = tmpICR1/7;  // скважность 1/7
  int a0 = TCNT1;
  if(TCNT1 < (OldICR - 30)) { // TCNT1
    ICR1 = tmpICR1;
    OldICR = tmpICR1;
    OCR1A = tmpOCR1A;
    c1++;
  } else {
    c2++;
  }
  int a1 = TCNT1;
  lenRegWrite = a1 - a0;

Альтернативная ветка (т.е. отказ от обновления регистров) реализуется примерно в 1.5% случаев, при этом обновление происходит не через 1 мс, а через две.

Но при повышении ноты (увеличении частоты) этот процент будет увеличиваться.

Поэтому настораживает, что выполняется этот фрагмент целых 32 такта. Ладно бы такта процессора, так ведь такта предделителя на 8, что равняется 256 тактам процессора. Что-то одного условного и четырех операторов присвоения, мне кажется, многовато. Буду копать ассемблерный код того, что скомпилировалось.

dimax
dimax аватар
Онлайн
Зарегистрирован: 25.12.2013

andriano, что то я не соображу, чем переход через ноль отличается от перехода через TOP? Их же по идее разделяет всего один такт МК.

andriano
andriano аватар
Онлайн
Зарегистрирован: 20.06.2015

Нет, там сначала счетчик инкрементируется, а потом, когда дойдет до TOP, - декрементируется. Собственно, корректность фазы вытекает как раз из симметричности процесса. Ну и дополнительная двойка в формуле для частоты - оттуда же: один раз считается до ICR1 вверх, а потом от него - вниз до 0. В все это один период выходной частоты.

 

Выяснил, откуда ~256 тактов в фрагменте из 4 строк.

Оказывается оптимизатор строку 1 переместил после строки 5. В принципе - логично: раз результат вычисления используется только внутри условия, то и незачем его считать снаружи - вдруг не понадобится. Т.е. он соптимизировал общее время выполнения, тогда как мне нужно соптимизировать время между чтением TCNT1 и записью ICR1. Точное количество тактов посчитать не удалось - там вызов процедуры деления, а это дело долгое.

переписал строку 1 так:

unsigned int tmpOCR1A = tmpICR1/8 + tmpICR1/64;  // скважность ~1/7

такое деление выполняется через сдвиги, а потому намного быстрее.

Вместо 32 тактов получил 13.

Но все равно непонятно: это порядка 100 тактов процессора, а по листингу около 62 операций (считая одну 4-байтовую за две). Хорошо хоть, запись в ICR1 (строка 4) сдедует сразу после условного перехода (строка3), в общем, со всем вычислением примерно 20 команд.

Уменьшил зазор в строке 3 с 30 до 3 тактов. За то время, пока писал, произошло 3 дефекта. Т.е. один раз в несколько минут. Надо к этой тройке добавить еще шаг изменения регистра ICR1, и, думаю, все будет нормально. А шаг учитывать нужно, т.к. при более высоких нотах он будет больше.

dimax, спасибо за помощь.

(я еще обращусь)

Теперь хочу заставить генерить звук таймер 2. Проблема в том, что 256 - недостаточное разрешение для ноты даже в пределах одной октавы, а там, учитывая сетку значений предделителя, нужно обеспечить перестройку при одном коэффициенте до 3 октав.

Пока идея формировать коэффициент деления как произведение коэффициента таймера, который генерит прерывания, и софтового счетчика, который изменяется по этим прерываниям и в некоторые моменты программно же устанавливает пин в 0 или 1.

Кстати, какие нибуд протоколы типа SoftwareSerial или I2C не используют таймер 2?

ВН
Offline
Зарегистрирован: 25.02.2016

andriano пишет:
 переписал строку 1 так:

unsigned int tmpOCR1A = tmpICR1/8 + tmpICR1/64;  // скважность ~1/7

такое деление выполняется через сдвиги, а потому намного быстрее. 

Что через сдвиги быстрее это понятно, только не вполне очевидно, как мне кажется, что такой код выполняется через сдвиги.

Т.е. для исключения неоднозначности предлагаю таки делать такую запись в виде сдвигов

andriano
andriano аватар
Онлайн
Зарегистрирован: 20.06.2015

Ну, вообще-то, если при формальной замене одной операции деления на две время выполнения сокращается в 2.5 раза, мне кажется, диагноз очевиден.

Но для сомневающихся могу привести фрагмент листинга

     16a:	60 91 84 00 	lds	r22, 0x0084
     16e:	70 91 85 00 	lds	r23, 0x0085
     172:	80 91 00 01 	lds	r24, 0x0100
     176:	90 91 01 01 	lds	r25, 0x0101
     17a:	03 97       	sbiw	r24, 0x03	; 3
     17c:	68 17       	cp	r22, r24
     17e:	79 07       	cpc	r23, r25
     180:	68 f5       	brcc	.+90     	; 0x1dc <_Z12Every_1024usm+0xce>
     182:	30 93 87 00 	sts	0x0087, r19
     186:	20 93 86 00 	sts	0x0086, r18
     18a:	30 93 01 01 	sts	0x0101, r19
     18e:	20 93 00 01 	sts	0x0100, r18
     192:	c9 01       	movw	r24, r18
     194:	66 e0       	ldi	r22, 0x06	; 6
     196:	96 95       	lsr	r25
     198:	87 95       	ror	r24
     19a:	6a 95       	dec	r22
     19c:	e1 f7       	brne	.-8      	; 0x196 <_Z12Every_1024usm+0x88>
     19e:	73 e0       	ldi	r23, 0x03	; 3
     1a0:	36 95       	lsr	r19
     1a2:	27 95       	ror	r18
     1a4:	7a 95       	dec	r23
     1a6:	e1 f7       	brne	.-8      	; 0x1a0 <_Z12Every_1024usm+0x92>
     1a8:	82 0f       	add	r24, r18
     1aa:	93 1f       	adc	r25, r19
     1ac:	90 93 89 00 	sts	0x0089, r25
     1b0:	80 93 88 00 	sts	0x0088, r24