Микроконтроллеры STM32: использование АЦП и ЦАП

24 сентября 2018

АЦП и ЦАП могут быть полезны в ряде задач, например, если нужно считывать данные с аналоговых датчиков, или при работе со звуком. Многие МК семейства STM32 имеют встроенный АЦП (даже несколько), а некоторые МК также имеют и встроенный ЦАП. В этой заметке мы рассмотрим простой пример использования обоих устройств. Для экспериментов я использовал плату LimeSTM32 на базе STM32F405, имеющего как АЦП, так и ЦАП.

Итак, создаем новый проект. В STM32CubeMX во вкладке Pinout в дереве слева находим Peripherals и ставим галочки ADC1 → IN0 и DAC → OUT1 Configuration. Далее во вкладке Configuration жмем на ADC1 и меняем значение «Continuous Conversion Mode» на «Enabled», а значение «End of Conversion Selection» — на «EOC flag at the end of all conversions». Несмотря на название, последнее значение эквивалентно «Disabled», о чем несколько подробнее можно прочитать на StackExchange. Что же до DAC, то нам подойдут его настройки по умолчанию.

Для успешной компиляции кода дописываем в Makefile в списке C_SOURCES:

$(FIRMWARE)/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_adc.c \
$(FIRMWARE)/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_adc_ex.c \
$(FIRMWARE)/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dac.c \
$(FIRMWARE)/Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_dac_ex.c \

При настройке ЦАП выяснилось, что аналоговый выход есть только на пинах PA4 и PA5. Проблема в том, что в LimeSTM32 эти пины я вывел на гнезда для подключения Arduino-шилда и использовал, как CS и SCK SPI-шины (D10 и D13 в обозначениях Arduino). Другими словами, приходится выбирать, остаться либо без SPI-устройств на шилде, либо без ЦАП. А мне как раз очень хотелось выводить значения, получаемые от АЦП, на шилд с экранчиком на базе ST7735, который использует SPI. Для решения этой проблемы было решено немного подхачить плату так, чтобы вместо PA4 и PA5 для SPI использовались пины PB5 и PB3. Я сделал себе пометку внести это изменение в будущих ревизиях LimeSTM32.

Основной код прошивки вышел незамысловатым:

void init() {
    ST7735_Init();
    ST7735_FillScreen(ST7735_BLACK);

    HAL_ADC_Start(&hadc1);
    HAL_DAC_Start(&hdac, DAC_CHANNEL_1);
    UART_Printf("Ready!\r\n");
     
    // Похоже, что АЦП нужно время на инициализацию.
    // Без этой задержки в зависимости от тела loop() прошивка
    // может повиснуть.
    HAL_Delay(1);
}

void loop() {
    HAL_Delay(100);

    // Без изменения опции "End of Conversion Selection"
    // HAL_ADC_GetValue сработает только один раз, а при
    // втором вызове повиснет.

    HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
    uint32_t adc_val = HAL_ADC_GetValue(&hadc1);

    HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, adc_val);

    char tmp[128];
    snprintf(tmp, sizeof(tmp), "ADC: %04lu", adc_val); // 0 .. 4095
    ST7735_WriteString(11*0, 18*0, tmp, Font_11x18,
                       ST7735_WHITE, ST7735_BLACK);
}

Код просто считывает значение с АЦП и передает его как есть в ЦАП. Значения, используемые обоими устройствами, в данном случае 12-и битные. Интересно, что микроконтроллеры STM32 позволяют понижать разрядность сигнала, получая взамен более быструю работу устройств. Также существуют микроконтроллеры с 16-и битным АЦП.

В LimeSTM32 используется микроконтроллер STM32F405RGT6 (даташит [PDF]), либо его старший брат STM32F415RGT6. В обоих микроконтроллерах частота дискретизации АЦП составляет 2.4 Msps, а частота дискретизации ЦАП — 1 Msps. Следует учитывать, что это максимальные значения, и в даташите есть немало оговорок касаемо того, когда они достигаются. Помимо прочего, частота дискретизации АЦП зависит от напряжения питания, а частота дискретизации ЦАП — от того, насколько сильно изменяется сигнал.

Fun fact! ЦАП в STM32 основаны на R-2R лестнице. Устройство таких ЦАП ранее была описано в заметке Генерация синусоидального сигнала, а следовательно и звука, на FPGA. Используемый же АЦП является так называемым SAR ADC. Его работа основана на ЦАП и бинарном поиске такого его входного значения, чтобы получаемый в итоге сигнал был максимально близок к входному сигналу АЦП.

Проверить работу кода проще всего с помощью потенциометра и светодиода:

Микроконтроллеры STM32: пример использования АЦП и ЦАП

А еще можно убрать задержку в loop() и воспользоваться для теста генератором сигналов и осциллографом:

Тестирование DAC и ADC с помощью осциллографа и генератора сигналов

На первом канале мы видим вход АЦП, а на втором — выход ЦАП. Искажения сигнала объясняются тем, что мы его пробрасываем в цикле, как бы «в лоб», а стоило бы использовать прерывания или DMA. Однако эти темы уже выходят за рамками сего поста. Заинтересованный читателю стоит обратиться к статье Учимся передавать звук с использованием протокола I2S, в рамках которой был написан простой WAV-проигрыватель. Используя подход, аналогичный описанному в той статье, можно записывать звук при помощи АЦП и затем воспроизводить его при помощи ЦАП.

Если вы решите сделать такой проект в качестве домашнего задания, то вам может понадобится операционный усилитель, например NE5532. Также на eBay можно найти готовые модули на базе микрофонного усилителя MAX9814 (даташит [PDF]), аудио усилителя для динамиков PAM8403 (даташит [PDF]), и множество других. При использовании PAM8403 не удивляйтесь, если на на выходе у него окажется не синусоида, поскольку он является усилителем класса D.

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

Метки: , .

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

Также подпишитесь на RSS, Facebook, ВКонтакте, Twitter или Telegram.