Запись звука с помощью микроконтроллера на SD карту.

Запись звука с помощью микроконтроллера на SD карту.

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

Для начала в общих чертах рассмотрим, что мы хотим сделать. Мы хотим записать звук, а что такое звук? Звук — это есть не что иное, как колебания среды(воздуха, воды и т.д) в определённом диапазоне частот, другими словами, звук — это волна, которая распространяется в среде. То есть в вакууме звук распространяться не может(нет среды, нет звука) и если мы в вакууме попробуем поговорить друг с другом, мы просто друг друга не услышим. Эти самые колебания среды с помощью микрофона можно улавливать и преобразовывать в электрический сигнал, которые будет нести информацию, заложенную в звуке. Но электрический сигнал, на выходе микрофона очень слабый, поэтому его надо усилить, сделаем это с помощью усилителя на ОУ. Схема была взята из апноута Atmel, перевод которого можно почитать тут. В зависимости от микрофона номинал R5 может быть изменен, его значение определяет коэффициент усиления.
Запись звука с помощью микроконтроллера на SD карту.

Первым делом, как собрал эту схему решил посмотреть как выглядят простые звуки, будет ли буква “а” просто гармоникой(волной одной определённой частоты) или это нечто более сложное. Ниже осциллограмма с протяжной буквой «аааааа», снятая с выхода ОУ.
Запись звука с помощью микроконтроллера на SD карту.

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

Теперь этот аналоговый сигнал надо преобразовать в нули и единицы понятные микроконтроллеру, для этого воспользуемся встроенным в МК аналого-цифровым преобразователем(АЦП). АЦП через определённые промежутки времени(частота квантования) будет преобразовывать аналоговый сигнал приходящий ему на вход, в соответствующий цифровой код(набор нулей и единиц). Далее, данные полученные с АЦП необходимо записать в открытый файл на карточке, но есть одно, но мы не сможем потом такой файл воспроизвести на компьютере, потому что у него нет формата. Для того чтобы компьютер понял, что это файл с музыкой, а точнее, wav файл, необходимо это и ещё другую метаинформацию записать в начало файла. Wav файл разделен на секции(чанки), они бывают разных типов. Минимальное количество секций в wav файле равняется двум, первая — это секцию формата ("fmt ") и вторая — это секцию данных ("data"). Про структуру wav файла можно почитать тут, а описание этих секций зададим с помощью структуры, описанной ниже.

 struct wave_header {

	// RIFF header
	uint32_t riffsig;	  // Chunk ID - "RIFF" (0x52494646)
	uint32_t filesize;     // file size
	uint32_t wavesig;     // RIFF Type - "WAVE" (0x57415645)

	// format chunk
	uint32_t fmtsig;	  // Chunk ID - "fmt " (0x666D7420)
	uint32_t fmtsize;     // Chunk Data Size - 16
	uint16_t type;        // Compression code - WAVE_FORMAT_PCM = 1
	uint16_t nch;         // Number of channels - 1
	uint32_t freq;        // Sample rate - частота сэмлирования
	uint32_t rate;        // Data rate - Average Bytes Per Second
	uint16_t block;       // BlockAlign = SignificantBitsPerSample / 8 * NumChannels; Количество байт на одну выборку
	uint16_t bits;        // Significant bits per sample - 8, 16, 24 или 32.

	// data chunk
	uint32_t datasig;     // chunk ID - "data" (0x64617461)
	uint32_t datasize;    // размер данных
	uint8_t data[];       // данные выборок

} wave_hdr;

Возникает вопрос, с какой частотой АЦП должно производить захват данных, другими словами, какая должна быть частота дискретизации? Ответ на этот вопрос очень прост, частота дискретизации АЦП должна соответствовать, частоте дискретизации wav файла.

Wav файл может воспроизводиться с разной частотой дискретизации, мы для воспроизведения файла будем использовать 11025 выборок в секунду, значит и АЦП должно делать 11025 выборок в секунду.

Для того чтоб получить такую частоту дискретизации, будем запускать АЦП по таймеру. Сам таймер настроим по совпадению, то есть мы зададим число, досчитывав, до которого будет вызываться прерывание и запускаться преобразование АЦП.

void start_capture(uint16_t freq)
{
	// Настраиваем АЦП, выбрав канал ADC0	
	//выравниваем результат влево
	ADMUX = 1<<ADLAR; 
	//запуск по совпадению таймера
	SFIOR |= (1<<ADTS1) | (1<<ADTS0);
	//включаем АЦП, режим непрерывного преобразования, разрешаем прерывание, Fadc = F_CPU/8
	ADCSRA = (1<<ADEN) | (1<<ADATE) | (1<<ADIE) | (1<<ADPS1) | (1<<ADPS0);

	//Настраиваем таймер0	
	//выбираем режим CTC, делим частоту на 8, Ftim = F_CPU/8
	TCCR0 = (1<<WGM01) | (1<<CS01);
	//разрешаем прерывание по совпадению
	TIMSK |= 1<<OCIE0;
	//устанавливаем зачение OCR0
	OCR0 = F_CPU/8UL/freq;
	
}

Думаю у кого-то может возникнуть вопрос, как рассчитывалось значение

OCR0 = F_CPU/8UL/freq;


время_одного_такта_таймера х кол-во_тактов = период одной выборки


Теперь, воспользовавшись тем что период величина обратная частоте T = 1/F перевернём всё выражение и получим

частота таймера х 1/кол-во тактов = частота выборок


кол-во тактов = частота таймера/частоту выборок


OCR0 = Ftim/freq;


Так как частота таймера — это частота МК, делённая на 8 получим

OCR0 = F_CPU/8UL/freq;



C расчётом разобрались, таким образом, каждый раз по совпадению таймера будет возникать прерывание, которое мы оформим так,
EMPTY_INTERRUPT(TIMER0_COMP_vect);

чтобы для него не генерировался никакой код, только reti(возврат из прерывания).

Это прерывание будет запускать преобразование АЦП, а по окончании преобразования будет возникать прерывание от АЦП.
ISR(ADC_vect)
{
        //если буфер не полон, сохраняем результат преобразования в буфер
	if(capture_pointer < CAPTURE_BUFLEN)
	{
		capture_buf[capture_pointer] = ADCH;
		capture_pointer ++;		
	}

}

Теперь у нас оцифрованные данные складываются в буфер. Так как запись возможна только блоками по 512К, размер буфера сделаем таким же. То есть мы ждём пока заполнится буфер и записываем его на карточку, предварительно записав туда служебную информацию и да, записывать информацию можно будет только при нажатой кнопке. Схема подключения выглядит следующим образом.
Запись звука с помощью микроконтроллера на SD карту.



#define F_CPU 8000000UL
#include <avr/io.h>
#include <avr/interrupt.h>
#include "spi.h"
#include "pff.h"
#include "diskio.h"

//размер буффера для сэмплов
#define CAPTURE_BUFLEN  512 // 512 сэмплов примерно 46 мс.

#define led_off		PORTB &= ~(1<<PB1);
#define led_on		PORTB |= (1<<PB1);

#define button_off	 ( (PINB & (1<<PINB0)) == 0 )
#define button_on	 ( (PINB & (1<<PINB0)) != 0 )


 struct wave_header {

	// RIFF header
	uint32_t riffsig;	  // Chunk ID - "RIFF" (0x52494646)
	uint32_t filesize;    // file size
	uint32_t wavesig;     // RIFF Type - "WAVE" (0x57415645)

	// format chunk
	uint32_t fmtsig;	  // Chunk ID - "fmt " (0x666D7420)
	uint32_t fmtsize;     // Chunk Data Size - 16
	uint16_t type;        // Compression code - WAVE_FORMAT_PCM = 1
	uint16_t nch;         // Number of channels - 1
	uint32_t freq;        // Sample rate - частота сэмлирования
	uint32_t rate;        // Data rate - Average Bytes Per Second
	uint16_t block;       // BlockAlign = SignificantBitsPerSample / 8 * NumChannels; Количество байт на одну выборку
	uint16_t bits;        // Significant bits per sample - 8, 16, 24 или 32.

	// data chunk
	uint32_t datasig;     // chunk ID - "data" (0x64617461)
	uint32_t datasize;    // размер данных
	uint8_t data[];       // данные выборок

} wave_hdr;

uint8_t capture_buf[CAPTURE_BUFLEN];
uint16_t capture_pointer = 0;
uint8_t last_button_state = 3;


EMPTY_INTERRUPT(TIMER0_COMP_vect);
// Прерывание от ADC.
ISR(ADC_vect)
{
//если буфер не полон, сохраняем результат преобразования в буфер
	if(capture_pointer < CAPTURE_BUFLEN)
	{
		capture_buf[capture_pointer] = ADCH;
		capture_pointer ++;		
	}

}

void start_capture(uint16_t freq)
{
	
	// Настраиваем АЦП, выбрав канал ADC0	
	//выравниваем результат влево
	ADMUX = 1<<ADLAR; 
	//запуск по совпадению таймера
	SFIOR |= (1<<ADTS1) | (1<<ADTS0);
	//включаем АЦП, режим непрерывного преобразования, разрешаем прерывание, Fadc = F_CPU/8
	ADCSRA = (1<<ADEN) | (1<<ADATE) | (1<<ADIE) | (1<<ADPS1) | (1<<ADPS0);
	
	
	//Настраиваем таймер0	
	//устанавливаем зачение OCR0
	OCR0 = F_CPU/8UL/freq;
	//выбираем режим CTC, делим частоту на 8, Ftim = F_CPU/8
	TCCR0 = (1<<WGM01) | (1<<CS01);
	//разрешаем прерывание по совпадению
	TIMSK |= 1<<OCIE0;

}

void record (UINT br)
{
	 // RIFF header
	 wave_hdr.riffsig		= 0x46464952;
	 wave_hdr.filesize		= sizeof(wave_hdr);
	 wave_hdr.wavesig		= 0x45564157;

	 // format chunk
	 wave_hdr.fmtsig		= 0x20746D66;
	 wave_hdr.fmtsize		= 16;
	 wave_hdr.type			= 1;
	 wave_hdr.nch			= 1;
	 wave_hdr.freq			= 11025;
	 wave_hdr.rate			= 11025;
	 wave_hdr.block			= 1;
	 wave_hdr.bits			= 8;

	 // data chunk
	 wave_hdr.datasig		= 0x61746164;
	 wave_hdr.datasize		= 1000000;//размер файла указываем сами


	//устанавливаем указатель записи
	pf_lseek(0);
			
	//записываем загловок
	pf_write(&wave_hdr, wave_hdr.filesize, &br);				
}


int main(void)
{
	_delay_ms(700);
	FATFS fs;//объявляем объект типа FATFS
	UINT br; //счетчик прочитанных байт
	DDRB |= 1<<PB1;//настраиваем вывод для подключения светодиода
	DDRB &= ~(1<<PB0);//настраиваем вывод для подключения кнопки
	PORTB |= (1<<PB0);//подяжка
	
	while(pf_mount(&fs) != FR_OK);
	
	//мигаем светодиодом, говоря о том что карточка смонтировалась
	led_on
	_delay_ms(700);
	led_off
	
	
	//открываем файл 
	if(pf_open("hello.wav") == FR_OK)
	{
		//мигаем светодиодом, говоря о том что файл открыт
		led_on
		_delay_ms(700);
		led_off
		//записываем служебную информацию
		record(br);
		//запускаем запись
		start_capture(wave_hdr.freq);
		//разрешаем прерывания
		sei();
	
		for (;;)
		{
			//если кнопка нажата
			if (button_on)
			{
				//если до этого кнопка была отпущена
				if (last_button_state == 0)
				{
					sei();	//разрешаем прерывания
				}
				
				//если буфер заполнен,
				if (capture_pointer == (CAPTURE_BUFLEN - 1))
				{
					
					pf_write(capture_buf, CAPTURE_BUFLEN,&br);//записываем буфер на карточку
					if(br == CAPTURE_BUFLEN)
					{
						PORTB ^= 1<<PB1;// изменяем состояние светоиод
					}
					capture_pointer = 0;//устанавливаем указатель записи в начало буфера
				}
				last_button_state = 1;
			}
			
			// если кнопка отпущена
			if (button_off && last_button_state != 0)
			{
				cli();//запрещаем прерывания
				last_button_state = 0;
				led_off//гасим светодиод
			}
		}
	}
	
	while(1)
	{
	
	}
}

Конечно же, часть семплов будет потеряна во время записи на карточку, а создать второй буфер не позволяет размер памяти МК, но поставленная задача выполнена, мы хоть и примитивно, но научились записывать звук на sd карточку с помощью МК.
Проект и пример записи оставлю в архиве Desktop.rar [125,32 Kb] (cкачиваний: 608)
комментарии
1