Простой вывод звука для микроконтроллерных устройств
kayo — Ср, 11/06/2014 - 11:17
Нередко бывает необходимо или просто удобно как-то озвучивать немые устройства, делая их более дружественными к человекам. Однако сия задача сопряжена с рядом трудностей. Во-первых, для вывода звука нужен некий выходной аналоговый интерфейс, но далеко не все контроллеры имеют на борту ЦАП (DAC). Во-вторых, сформированный аналоговый сигнал необходимо как-то усиливать, чтобы подать на динамическую головку с низким сопротивлением 4, 8, или 16 Ом. По этим двум причинам часто от честного формирования аналогового звука отказываются в пользу прямоугольных импульсов заданной частоты на пьезо излучателе. Так поступают, например, в материнских платах с PC Speaker, но ничего помимо обычной пищалки этим не получится реализовать. Нас же данный способ категорически не устраивает, на дворе двадцать первый век в конце то концов, поэтому мы займёмся организацией честного аналогового звука.
Только рассыпуха, только хардкор
Итак, подбрасывая радиоактивные полешки в топку иллюмиума, приступим. Сразу договоримся, контроллера с ЦАП у нас нет, даже когда мы применяем контроллер с ЦАП, он нужен нам для других куда более важных целей. С другой стороны аналоговый усилитель для нас применить проблематично и не столько по соображениям доступности (найти то можно всё, что угодно при желании), сколько исходя из эффективности в плане потребляемой мощности. Да-да, промежуточные и выходные транзисторные каскады аналоговых усилителей работают подобно делителям напряжения, когда на вход поступает сигнал в половину предельной амплитуды, по крайней мере половина потребляемой мощности тратится на нагрев воздуха вокруг вашего устройства. Оно нам надо? Нет.
Но как же быть? Однако решение есть, это так называемый усилитель D-класса. Признаюсь честно, сперва я придумал применить мостовую схему для формирования сигнала, а уже потом узнал, что это в сущности является частью усилителя D-класса. Здесь уместно сразу привести схему, чтобы всё стало понятно:
Как видно, обмотку диффузора динамической головки качает мостовая схема из четырёх МДП (MOSFET) транзисторов, которые управляются четырьмя линиями: две из которых прямые (SND_1P, SND_2P), две другие (SND_1N, SND_2N) — инверсные по отношению к первым. На эти линии подаются прямоугольные импульсы с выводов ШИМ (PWM) контроллера. Аналоговый сигнал формируется ФНЧ (Low-Pass Filter), в качестве которого выступает L201, C202. Инертность диффузора динамика также волне успешно отфильтровывает всё, что выше 50 КГц. Вот иллюстрация того, как работает модуляция:
Конечно, форма аналогового сигнала на выходе не такая гладкая, а имеет ступеньки, размер которых зависит от битовой глубины модулируемого сигнала. Чем больше бит, тем меньше ступеньки, но в то же время ниже верхняя частота модуляции, которая в свою очередь ограничивает частоту результирующего аналогового сигнала. Как выяснилось, в индуктивности L201 нет особой необходимости, индуктивности самой динамической головки вполне хватает. Однако, ёмкость C202 желательна. Конденсатор C201 срезает постоянную составляющую, предотвращая бесполезный нагрев ключей. Можно считать, что это своеобразный ФВЧ (High-Pass Filter) для пропускания того, что выше нижней частоты воспринимаемого человеком звука.
Микроконтроллеры STM32F10x помимо прямых имеют инверсные выводы ШИМ, чем и обусловлено такое схемотехническое решение. На самом деле, хватило бы двух выводов: одного ШИМ, а другого для переключения полярности, но тогда схемотехника несколько усложнится. Может возникнуть закономерный вопрос: почему бы не ограничиться полумостом? Я сперва попробовал этот вариант, но в данном устройстве мостовая схема питается от шины питания МК, а 3.3V не хватило для достижения приемлемой громкости звука с динамиком 8 Ом. Полный мост позволяет толкать диффузор от -3.3V до +3.3V, что в совокупности составляет вдвое больше, то есть 6.6V и громкость получилась более чем достаточная.
Последнее, что касается схемы: резисторы R203, R204, R205, R206 ограничивают ток зарядки/разрядки ёмкости затвора транзисторов, резисторы R201, R202, R207, R208 обеспечивают закрытие всех ключей в условиях отсутствия сигналов на входах.
У STM32 у каждого таймера 4 независимых компаратора, которые можно использовать для формирования 4х сигналов PWM с единой несущей, и разным заполнением. В данном случае используется только два вывода, для двухканального аудио пришлось бы задействовать все 4. Следующая схема показывает подключение к выводам МК:
Как видим, я использовал аппаратный таймер TIM1 для формирования ШИМ сигналов. Конечно, ШИМ сигнал должен быть подведен лишь к одному плечу: верхнему или нижнему, другое должно быть просто открыто или закрыто на протяжении всего цикла полуволны. Но схемотехнически это реализуется не так просто, а программно потребуется делать лишнее действие для переключения.
Драйвер для вывода звука
Что ж с железом разобрались, осталось реализовать программную часть. Для работы нам понадобится очередь сэмплов, через которую подсистема ввода/вывода будет взаимодействовать со звуковым драйвером. Чтобы организовать интерфейс очереди, нам понадобится примитив синхронизации, который блокирует поток, читающий сэмплы в очередь драйвера, когда в ней больше нет места для новых сэмплов, и заблаговременно разблокирует его, когда в очереди осталось меньше некоторого порогового количества сэмплов.
Это пороговое значение вместе с длиной очереди определяет задержку (Latency). Чем короче очередь, то есть меньше общее число сэмплов в ней, тем меньше задержка. Но малая задержка заставляет подсистему ввода-вывода чаще обращаться к внешнему устройству и считывать меньшее количество данных за раз, что увеличивает потребление энергии. Поэтому следует выбирать наибольшую возможную задержку, то есть увеличивать очередь и уменьшать её минимальный порог заполнения, но так, чтобы подсистема ввода/вывода поспевала считать ещё сэмплов до того, как очередь опустеет.
Событие переполнения очереди принято называть Overrun (поторопились), а событие опустошения — Underrun (непоспели). Наша реализация драйвера автоматически стартует воспроизведение при появлении минимального числа сэмплов в очереди, и автоматически прекращает, когда сэмплы в очереди полностью закончились.
Поскольку здесь я использую микроконтроллер STM32F103, максимальная частота тактирования таймера составляет 72 МГц. Не трудно убедиться, что точный период можно выдержать не для всякой частоты сэмплирования. Например, чтобы отсчитывать периоды для частоты сэмплирования 48 КГц, требуется ровно 1500 отсчётов, то есть 72e6 / 48e3 = 1500.0. Однако, для частоты 44.1 КГц требуется не целое число отсчётов, то есть 72e6 / 44.1e3 = 1632.6530… Если просто использовать наиболее близкое целое значение в качестве периода для целевой частоты сэмплирования, скорость воспроизведения получится несколько ниже или выше, на столько, что это может быть заметно. Поэтому, чтобы всё было честно, необходимо так называемое ресемплирование.
Ресемплирование можно реализовать самым дешевым методом в рантайме непосредственно в прерывании сэмплирования драйвера. Для этого нам потребуется завести переменную со значением в интервале от 0 до 1, к которой на каждом шаге будет прибавляться разница между требуемым периодом и актуальным, то есть та самая дробная часть. Таким образом, мы при каждом срабатывании прерывания сэмплирования будем иметь реальное положение курсора в очереди сэмплов, который окажется где-то между текущим и следующим в зависимости от близости значения к 0 или 1. Зная точное положение, мы можем получить реальное значение сэмпла, например, линейной интерполяцией между двумя соседними:
$$S_r = S_a (1 - \tau) + S_b \tau,$$
где τ — значение интерполирующей переменной, Sa — значение текущего сэмпла, Sb — значение следующего сэмпла, Sr — результирующее значение.
Формула работает для случая, когда актуальный интервал меньше требуемого, то есть воспроизведение всегда торопится и значение τ растёт. В реальности необходимо учитывать направление изменения этого значения и если оно убывает, то вместо Sa брать значение предыдущего сэмпла, а вместо Sb — значение текущего.
И наконец необходимо обрабатывать ситуации когда τ становится больше 1, если оно растёт, и меньше 0, если убывает, В первом случае требуется перескакивать через сэмпл, вместо перехода к следующему, а во втором просто не переходить к следующему. Напомним, что всё это должно происходить в обработчике прерывания сэмплирования.
Следуя правильным канонам embedded разработки, обработчики прерываний должно писать как можно более лёгкими в части вычислений, поэтому лучше перенести ресемплирование в поток вывода, хоть очередь в этом случае потребуется несколько длиннее для той же самой задержки. Может показаться, что алгоритм ресемплирования плох, поскольку использует арифметику с плавающей точкой, однако реализация вполне может быть целочисленной и даже обязана таковой быть.
Итак, алгоритм запуска драйвера:
- Вычисляем глубину бит из частоты сэмплирования и частоту тактирования шины, на которой висит таймер.
- Устанавливаем вычисленную верхнюю границу счёта таймера (значение регистра автосброса).
- Вешаем обработчик прерывания сэмплирования в момент сброса/перезапуска таймера.
- Разблокируем поток воспроизведения для начала работы.
Алгоритм работы драйвера:
- При достижении минимального числа сэмплов в очереди драйвер разблокирует поток воспроизведения.
- Поток воспроизведения получает от драйвера число сэмплов, которые можно вывести.
- Поток воспроизведения считывает сэмплы и передаёт в драйвер.
- Драйвер в обработчике сэмплирования считывает очередной сэмпл из очереди ввода и сравнивает значение сэмпла с нулём.
- Если значение равно нулю, выключает оба канала ШИМ.
- Если значение больше нуля, устанавливает его в верхний канал ШИМ, выключает нижний канал.
- Если значение меньше нуля, устанавливает его в нижний канал, выключает верхний канал.
Обработка потока сэмплов
На 32-разрядных микроконтроллерах STM с ядром Cortex-M3 в нашем распоряжении таймеры с 16-разрядным счётчиком, отсюда наибольшая теоретически достижимая глубина звукового сэмпла равна 16-битам. На деле же требуемая частота сэмплирования ограничивает эту глубину 12/11/10 битами, но этого вполне достаточно для большинства применений, мы же не Hi-Fi технику тут мастерим.
Итак, первое, что нам потребуется сделать, — определить делитель и период таймера. Для начала определимся с интервалом частот, которые будет поддерживать драйвер. Я взял общепринятый интервал от 8 КГц до 96 КГц. Поскольку у нас килогерцы, нам потребуется максимальная частота для ШИМ, то есть в качестве делителя примем 1. Далее рассчитаем некоторые параметры.
Частота сэмплирования должна быть больше либо равна требуемой воспроизводимым звуковым потоком. Отсюда период таймера, то есть количество отсчётов для текущей частоты сэмплирования, ns получается как:
$$n_s = \frac{f_c}{f_r},$$
где fc — частота тактирования таймера (72 МГц для данного случая), fr — требуемая частота сэмплирования.
Понятно, что количество отсчётов в наиболее общем случае получится не целым числом. Далее мы разберёмся, требуется ли учитывать эту дробную часть. Реальная частота сэмплирования, на которой таймером будет достигаться верхняя граница счёта, fs:
$$f_s = \frac{f_c}{floor (n_s)}.$$
где floor — функция округления до целого в меньшую сторону (необходима здесь поскольку реальная частота должна быть больше либо равна требуемой).
Глубина сэмпла ds рассчитывается как:
$$d_s = floor (log_2 (floor (n_s))).$$
Для удобства составим табличку:
fr, Гц | ns, отсчётов | fs, Гц | ds, бит |
8 000 | 9 000.000 000 | 8 000.000 000 | 13 |
11 025 | 6 530.612 245 | 11 026.033 690 | 12 |
16 000 | 4 500.000 000 | 16 000.000 000 | 12 |
22 050 | 3 265.306 122 | 22 052.067 381 | 11 |
44 100 | 1 632.653 061 | 44 117.647 059 | 10 |
48 000 | 1 500.000 000 | 48 000.000 000 | 10 |
96 000 | 750.000 000 | 96 000.000 000 | 9 |
Данный подход гарантирует, что реальная частота сэмплирования будет больше либо равна требуемой, а глубина сэмпла будет максимально возможной. Поскольку частота выше требуемой, то мы торопимся и корректировать необходимо в меньшую сторону, интерполяция должна происходить между текущим и предыдущим сэмплом.
Теперь, чтобы понять, действительно ли нам необходима коррекция отсчетов, то есть должны ли мы учитывать ту самую дробную часть для некоторых частот, рассудим таким образом:
Длительность периода одного отсчета равна 1/72e6 секунд, максимальная дробная часть для частоты 44100 Гц это 0.653 отсчёта, а значит 0.653/72e6 секунды на один лишь период указанной частоты, значит рассинхронизация во времени составит в общей сложности 44117.647×0.653/72e6 секунд на каждую секунду. Это приблизительно 1.44 секунды в час забегания вперёд, что для многих приложений оказывается совершенно несущественно.
Тот же результат можно получить если просто взять разницу периодов требуемой и реальной частот и умножить на реальную частоту: (1/44100-1/44117.647)×44117.647.
Самое последнее, что нам осталось сделать, — вычислить реальное значение сэмпла, то есть заполнение ШИМ, исходя из числа отсчетов для используемой частоты. Здесь всё просто:
$$s_{pwm} = \frac{s_{src} k_{gain} n_s}{a_{src}^2},$$
где ssrc — считанное значение сэмпла, kgain — коэффициент усиления, asrc — максимальное значение ssrc и kgain.
Детали реализации
API нашего драйвера спроектирован исходя из соображений эффективности. Дабы избежать копирования сэмплов, приложение получает прямой доступ к буферу драйвера, в который может считывать данные непосредственно из устройства ввода, если при этом не требуется никаких преобразований, как-то декодирования потока.
Приложение проигрывателя в потоке чтения запрашивает у драйвера буфер, получая указатель на начало непрерывной области памяти и количество сэмплов, которые можно туда записать. Эта операция блокирует поток, если в буфере пока достаточно сэмплов для воспроизведения.
Когда буфер получен, приложение может записать туда сэмплы в количестве вплоть до полученного от драйвера, после чего оно уведомляет наш драйвер об актуальном числе записанных им сэмплов.
Далее процедура повторяется в цикле, пока не будет достигнут конец воспроизводимого потока.
Итак, вот наш API:
#define SND_NUM_SAMPLES(rate, msec) ((rate)*(msec)/1000) #define SND_BUF_SAMPLES(buf) (sizeof(buf)/sizeof(snd_sample_t)) typedef uint32_t snd_length_t; typedef int32_t snd_double_sample_t; typedef int16_t snd_sample_t; #define SND_SAMPLE_BYTES sizeof(snd_sample_t) #define SND_SAMPLES_BYTES(samples) ((samples)*(SND_SAMPLE_BYTES)) #define SND_BYTES_SAMPLES(bytes) ((bytes)/(SND_SAMPLE_BYTES)) #define SND_SAMPLE_UBITS (SND_SAMPLE_BYTES << 3) #define SND_SAMPLE_BITS (SND_SAMPLE_UBITS - 1) #define SND_SAMPLE_MIN (-(1 << SND_SAMPLE_BITS)) #define SND_SAMPLE_MAX (-1 - SND_SAMPLE_MIN) #ifndef SND_DEFAULT_RATE #define SND_DEFAULT_RATE 44100 #endif #ifndef SND_DEFAULT_VOL #define SND_DEFAULT_VOL SND_SAMPLE_MAX #endif #ifndef SND_CHANNELS #define SND_CHANNELS 2 #endif typedef struct { PWMDriver *pwmp; int8_t channels[SND_CHANNELS]; snd_sample_t *buffer; snd_length_t length; snd_length_t hi_flow; snd_length_t lo_flow; } SNDConfig; typedef enum { SND_INIT, SND_STOPPED, SND_PLAYING } SNDState; typedef struct SNDDriver SNDDriver; struct SNDDriver { SNDDriver *sndp; const SNDConfig *sndcfg; PWMConfig pwmcfg; uint16_t rate; uint16_t vol; SNDState state; uint32_t period; /* period (ticks) */ snd_sample_t *write; /* write pointer */ snd_sample_t *read; /* read pointer */ snd_sample_t *right; /* right edge of buffer */ snd_length_t count; /* num of samples in buffer */ BinarySemaphore bsem; }; void sndObjectInit(SNDDriver *sndp); void sndStart(SNDDriver *sndp, const SNDConfig *cfgp); void sndStop(SNDDriver *sndp); void sndSetRate(SNDDriver *sndp, uint16_t rate); #define sndGetRate(sndp) ((sndp)->rate) void sndSetVol(SNDDriver *sndp, uint16_t vol); #define sndGetVol(sndp) ((sndp)->vol) snd_length_t sndPreWrite(SNDDriver *sndp, snd_sample_t** data); void sndPostWrite(SNDDriver *sndp, snd_length_t size);
Ничего особо интересного тут нет. В конфиг драйвера передаётся целевой PWM драйвер для использования, указатель на циклический буфер для сэмплов, устанавливается два порога: минимального и максимального числа сэмплов в буфере для работы механизма suspend/resume воспроизведения.
Вот паттерн использования в качестве примера:
SNDDriver SNDD1; snd_sample_t sndbuf[SND_NUM_SAMPLES(SND_DEFAULT_RATE /* Hz */, 50 /* mSec */)]; SNDConfig sndcfg = { &PWMD1, {0, 1}, sndbuf, SND_BUF_SAMPLES(sndbuf), SND_NUM_SAMPLES(SND_DEFAULT_RATE, 40) /* highest flow edge (begin playing) */, SND_NUM_SAMPLES(SND_DEFAULT_RATE, 10) /* lowest flow edge (resume writing) */ }; void playerInit(){ sndObjectInit(&SNDD1); } void playerStart(){ /* здесь конфигурим порты PWM, что-то типа */ configPad(SND_CH1, ALTERNATE_PUSHPULL); configPad(SND_CH2, ALTERNATE_PUSHPULL); sndStart(&SNDD1, &sndcfg); } void playerStop(){ sndStop(&SNDD1); configPad(SND_CH1, RESET); configPad(SND_CH2, RESET); } void playerThread(void *){ ... if(chFileStreamFatOpen(file, path, FILE_READ) == FILE_ERROR){ return; } /* код чтения заголовков потока */ ... sndSetRate(&SNDD1, riff.SampleRate); sndSetVol(&SNDD1, SND_DEFAULT_VOL); for(; !chThdShouldTerminate(); ){ /* читаем поток в буфер драйвера */ snd_sample_t *buffer; size_t needed = SND_SAMPLES_BYTES(sndPreWrite(&SNDD1, (snd_sample_t**)&buffer)); size_t readed = chSequentialStreamRead((BaseSequentialStream*)file, (uint8_t*)buffer, needed); sndPostWrite(&SNDD1, SND_BYTES_SAMPLES(readed)); if(readed < needed){ /* конец потока достигнут */ chFileStreamClose(file); break; } } }
Внутренняя реализация драйвера несколько интереснее:
#include "ch.h" #include "hal.h" #include "pwm_audio.h" static SNDDriver *SNDP = NULL; static void sndResumeI(SNDDriver *sndp){ if(sndp->state == SND_PLAYING){ return; } pwmChangePeriodI(sndp->sndcfg->pwmp, sndp->period); sndp->state = SND_PLAYING; } static void sndSuspendI(SNDDriver *sndp){ if(sndp->state == SND_STOPPED){ return; } uint8_t i; for(i = 0; i < SND_CHANNELS; i++){ if(sndp->sndcfg->channels[i] > -1){ pwmDisableChannelI(sndp->sndcfg->pwmp, sndp->sndcfg->channels[i]); } } pwmChangePeriodI(sndp->sndcfg->pwmp, 0); sndp->state = SND_STOPPED; } static void sndSampleIsr(PWMDriver *pwmp){ SNDDriver *sndp = SNDP; /* находим SND драйвер, работающий с этим драйвером PWM */ for(; sndp && sndp->sndcfg->pwmp != pwmp; sndp = sndp->sndp); if(!sndp) return; /* проверяем наличие сэмплов в буфере */ if(sndp->count > 0){ /* применяем к-т усиления к сэмплу и приводим к требуемой глубине бит */ snd_sample_t sample = ((((snd_double_sample_t)(*sndp->read) * (snd_double_sample_t)sndp->vol) >> SND_SAMPLE_BITS) * sndp->period) >> SND_SAMPLE_BITS; chSysLockFromIsr(); /* выводим сэмпл, устанавливая заполнение PWM сигнала */ if(sample > 0){ pwmDisableChannelI(sndp->sndcfg->pwmp, sndp->sndcfg->channels[1]); pwmEnableChannelI(sndp->sndcfg->pwmp, sndp->sndcfg->channels[0], sample); }else{ pwmDisableChannelI(sndp->sndcfg->pwmp, sndp->sndcfg->channels[0]); pwmEnableChannelI(sndp->sndcfg->pwmp, sndp->sndcfg->channels[1], -sample); } /* увеличиваем позицию чтения в буфере */ if(++sndp->read == sndp->right){ /* переходим к началу при достижении конца буфера */ sndp->read = sndp->sndcfg->buffer; } /* уведомляем поток воспроизведения когда в буфере осталось мало сэмплов */ if(--sndp->count <= sndp->sndcfg->lo_flow){ chBSemSignalI(&sndp->bsem); } chSysUnlockFromIsr(); }else{ /* сэмплов в буфере больше нет, приостанавливаем воспроизведение */ chSysLockFromIsr(); sndSuspendI(sndp); chSysUnlockFromIsr(); } } snd_length_t sndPreWrite(SNDDriver *sndp, snd_sample_t** data){ /* если буфер уже заполнен полностью, блокируем поток воспроизведения */ if(sndp->count == sndp->sndcfg->length){ chBSemWait(&sndp->bsem); } *data = sndp->write; chSysLock(); /* определяем число сэмплов, которые уже можно читать в буфер */ snd_length_t length = sndp->read > sndp->write ? sndp->read - sndp->write : sndp->right - sndp->write; chSysUnlock(); return length; } void sndPostWrite(SNDDriver *sndp, snd_length_t size){ chSysLock(); sndp->write += size; sndp->count += size; /* если достигли конца буфера, переходим к началу */ if(sndp->write == sndp->right){ sndp->write = sndp->sndcfg->buffer; } /* возобновляем воспроизведение, если в буфере уже достаточно сэмплов */ if(sndp->count >= sndp->sndcfg->hi_flow){ sndResumeI(sndp); } chSysUnlock(); } void sndObjectInit(SNDDriver *sndp){ chBSemInit(&sndp->bsem, FALSE); } void sndStart(SNDDriver *sndp, const SNDConfig *cfgp){ chDbgCheck((sndp != NULL) && (cfgp != NULL) && (cfgp->pwmp != NULL) && (cfgp->channels[0] < PWM_CHANNELS) && (cfgp->channels[1] < PWM_CHANNELS), "sndStart"); sndp->sndcfg = cfgp; /* инициализируем конфигурацию PWM */ sndp->pwmcfg.callback = sndSampleIsr; /* задействуем каналы */ int i; for(i = 0; i < PWM_CHANNELS; i++){ sndp->pwmcfg.channels[i].mode = PWM_OUTPUT_DISABLED | PWM_COMPLEMENTARY_OUTPUT_DISABLED; sndp->pwmcfg.channels[i].callback = NULL; } /* нам необходимо как-то получить максимальную частоту от драйвера, поэтому проинициализируем его безопасным образом */ sndp->pwmcfg.frequency = 10000; sndp->pwmcfg.period = 10000; pwmStart(sndp->sndcfg->pwmp, &sndp->pwmcfg); pwmStop(sndp->sndcfg->pwmp); /* можно получать требуемую нам частоту */ sndp->pwmcfg.frequency = sndp->sndcfg->pwmp->clock; sndp->pwmcfg.period = 0; /* настраиваем каналы */ uint8_t c; for(i = 0; i < PWM_CHANNELS; i++){ for(c = 0; c < SND_CHANNELS; c++){ if(cfgp->channels[c] == i){ sndp->pwmcfg.channels[i].mode = PWM_OUTPUT_ACTIVE_HIGH | PWM_COMPLEMENTARY_OUTPUT_ACTIVE_HIGH; } } } /* настраиваем буфер */ sndp->read = sndp->write = cfgp->buffer; sndp->count = 0; sndp->right = cfgp->buffer + cfgp->length; /* добавляем драйвер в очередь */ sndp->sndp = SNDP; SNDP = sndp; sndp->state = SND_STOPPED; sndSetRate(sndp, SND_DEFAULT_RATE); sndSetVol(sndp, SND_DEFAULT_VOL); /* запускаем PWM */ pwmStart(sndp->sndcfg->pwmp, &sndp->pwmcfg); } void sndStop(SNDDriver *sndp){ /* удаляем драйвер из очереди */ SNDDriver **sndpp = &SNDP; for(; *sndpp && *sndpp != sndp; sndpp = &(*sndpp)->sndp); if(*sndpp){ *sndpp = (*sndpp)->sndp; sndp->sndp = NULL; chSysLock(); sndSuspendI(sndp); chSysUnlock(); /* останавливаем PWM устройство */ pwmStop(sndp->sndcfg->pwmp); sndp->sndcfg = NULL; } } void sndSetRate(SNDDriver *sndp, uint16_t rate){ if(sndp->rate == rate) return; sndp->rate = rate; /* определяем требуемый период таймера через частоту (то есть наше число тактов) */ sndp->period = sndp->pwmcfg.frequency / rate; if(sndp->state == SND_PLAYING){ pwmChangePeriod(sndp->sndcfg->pwmp, sndp->period); } } void sndSetVol(SNDDriver *sndp, uint16_t vol){ sndp->vol = vol; }
Закругляемся
Сказать по правде, я ожидал получить от такого инженерного решения нечто меньшее, чем получил. Технология работает действительно хорошо для своего исполнения. Аппаратная часть легко раскачала 8 Омный динамик при питании от Li-Ion батареи на 3.6V, саунд получился довольно громкий, и FET-ы моста в SOT23 корпусах вполне нормально себя чувствуют. В общем, неплохо для усилителя в половину квадратного сантиметра.
В качестве дальнейшего развития можно попробовать реализовать поддержку стерео режима, а пока и на этом успокоимся ^_^

Правильно ли я понимаю,
Валерий (не проверено) — Ср, 05/10/2016 - 00:27Правильно ли я понимаю, что
при разрядности исходного сигнала равной n, частота ШИМ при 72 МГц будет равна 72000000/(2^n), т.е. при 96 кГц максимум это 9 бит?
Насколько мне удалось заметить в специализированных звуковых микросхемах ШИМ используется центрально выровненный, т.е. в случае процессора это ещё в два раза меньшая частота ШИМ, и следовательно получаем уже 8 бит?
Если имеется ввиду частота
kayo — Чт, 27/10/2016 - 01:16Отправить комментарий