Ацп с использованием dma

Оцифровка звука на STM32 (АЦП+DMA) и кодирование в Speex для передачи

В продолжение своей вчерашней статьи на Geektimes про Самодельный USB-свисток с микрофоном, STM32 и ESP8266 на борту хочу рассказать подробнее про реализацию оцифровки и кодирования звука на микроконтроллере STM32.

В статье покажу как настроить проект в STM32CubeMX, собирать данные с АЦП в два кольцевых буфера посредствам DMA, подключить библиотеку Speex и кодировать данные. Возможно многим материал покажется весьма очевидным, но надеюсь хоть кому-то он будет полезен.

Прошу под кат.

Что такое Speex?

Про Speex я узнал из статьи Распознавание речи на STM32F4-Discovery, советую почитать, большая часть кода взята оттуда.

Элементная база

В статье я буду использовать самую дешевую и распространенную отладочную плату на базе микроконтроллера STM32F103C8T6. К ней должен быть отдельно приобретен программатор. Подход не изменится и для любой платы Discovery. К отладке я подключал микрофонный модуль с усилителем Max9812.

Схему можно посмотреть в статье, указанной в самом начале. Там я завожу на АЦП сигнал прям с выхода Max9812.

Для этого на покупном модуле нужно закоротить конденсатор на ноге OUT (Пятой точкой чувствую, что так делать нельзя, но не знаю как правильно).

По входу получается сигнал с постоянной составляющей ~1,6V. Его мы отснимаем и в программе приводим к знаковому типу для выполнения кодирования.

Настройка проекта в STM32CubeMX

Создадим новый проект с микроконтроллером STM32F103C8T6. Первым делом указываем, что у нас подключен внешний кварцевый резонатор. Часовой кварц нам сейчас не нужен, хотя на отладочной плате он тоже есть.

Не забываем включить интерфейс отладки Serial Wire. Потом включаем необходимый вход АЦП, у меня это IN8 (см. схему в предыдущей статье). Ну и удобный таймер, по которому DMA будет забирать данные из буфера.

После этого заходим во вкладку Clock Configuration и настраиваем схему тактирования. У меня получилось так:

Я задал частоту для основной периферии микроконтроллера по максимуму в 72 МГц. На Таймеры тоже заведено 72 МГц, запомним это значение. Вы можете сделать по-другому, но тогда и таймер надо будет пересчитать по-своему.

Переходим во вкладку Configuration. Тут нам надо настроить АЦП, DMA и Таймер.

АЦП настраиваем по триггеру таймера 3. Тут же во вкладке DMA выделяем под это первый канал DMA Peripheral To Memory (из переферии в память). Приоритет не важен, если в программе больше ничего нет. Режим — Circular (циклический), размер данных Half Word (полслова, 2 байта) и адрес памяти будет инкрементироваться.

Далее настроим таймер. Speex поддерживает кодирование данные в узкой полосе частот (Narrowband, 8 кГц), широкой (wideband, 16 кГц) и ультраширокой (ultra-wideband, 32 кГц). Не будем нагружать контроллер, возьмем по минимуму. Получается контроллер должен отснимать данные с АЦП на частоте 8 кГц. На таймер нам приходит 72 МГц. Считаем:

Настраиваем таймер на значение 8999 (считать ведь он начинает с нуля) и событие по таймеру Update Event. Ставим галочку глобального прерывания.

Можно переходить к генерации проекта. Заходим в Project → Serrings. Укажем путь сохранения проекта и размеры стэка и кучи. Для кодирования Speex нам примерно понадобится 0x600 и 0x1600. После этого генерируем для своей среды и открываем, у меня это IAR.

Подключим библиотеку Speex

Первое, что нужно сделать, скопировать папку STM32F10x_Speex_Lib с библиотекой Speex в папку Drivers проекта. Потом добавим в проект группу libspeex, а в нее следующие файлы (см. скриншот).

В свойствах проекта на вкладке Preprocessor добавим дефайн HAVE_CONFIG_H и следующие дирректории:

$PROJ_DIR$/../Drivers/STM32F10x_Speex_Lib/include $PROJ_DIR$/../Drivers/STM32F10x_Speex_Lib/libspeex $PROJ_DIR$/../Drivers/STM32F10x_Speex_Lib/STM32 $PROJ_DIR$/../Drivers/STM32F10x_Speex_Lib/STM32/include $PROJ_DIR$/../Drivers/STM32F10x_Speex_Lib/STM32/libspeex

$PROJ_DIR$/../Drivers/STM32F10x_Speex_Lib/STM32/libspeex/iar

Попробуем скомпилировать, должно быть все хорошо без варнингов и ошибок.

Программирование

Тут главное писать код в специально отведенных USER CODE BEGIN-END блоках, тогда, в случае необходимости внесения изменений в проект Куба и повторной его генерации, весь ваш код сохранится. Работу с библиотекой я вынесу в отдельный файл speexx.c. Приведу его код и код заголовочного файла speexx.h сразу:

speexx.h#ifdef HAVE_CONFIG_H
#include “config.h”
#endif
#include
#include “stm32f1xx_hal.h” #define FRAME_SIZE 160 //*0.125мс = 20мс (сэмплирование 8кГц)
#define ENCODED_FRAME_SIZE 20 //ужимает в 8 раз
#define MAX_REC_FRAMES 90 //максимальное число записываемых фреймов, Время = MAX_REC_FRAMES*0,02сек extern __IO uint16_t IN_Buffer[2][FRAME_SIZE];
extern __IO uint8_t Start_Encoding; void Speex_Init(void);
void EncodingVoice(void);speexx.c#include “speexx.h” //SPEEX variables
__IO uint16_t IN_Buffer[2][FRAME_SIZE];
__IO uint8_t Start_Encoding = 0;
uint8_t Index_Encoding = 0;
uint32_t Encoded_Frames = 0; uint8_t REC_DATA[2][MAX_REC_FRAMES*ENCODED_FRAME_SIZE]; //сюда сохраняются закодированные данные
uint8_t* Rec_Data_ptr = &REC_DATA[0][0]; //указатель на кодируемые данные
uint8_t* Trm_Data_ptr; //указатель на передаваемые данные int quality = 4, complexity=1, vbr=0, enh=1;/* SPEEX PARAMETERS, MUST REMAINED UNCHANGED */
SpeexBits bits; /* Holds bits so they can be read and written by the Speex routines */
void *enc_state, *dec_state;/* Holds the states of the encoder & the decoder */ void Speex_Init(void)
{ /* Speex encoding initializations */ speex_bits_init(&bits); enc_state = speex_encoder_init(&speex_nb_mode); speex_encoder_ctl(enc_state, SPEEX_SET_VBR, &vbr); speex_encoder_ctl(enc_state, SPEEX_SET_QUALITY,&quality); speex_encoder_ctl(enc_state, SPEEX_SET_COMPLEXITY, &complexity);
} void EncodingVoice(void)
{ uint8_t i; //====================Если одна из половинок буфера заполнена====================== if(Start_Encoding > 0) { Index_Encoding = Start_Encoding – 1; for (i=0;iISR & DMA_FLAG_HT1) { Start_Encoding = 1; } //флаг половинной готовности DMA поднят, можно кодировать первую половину if (DMA1->ISR & DMA_FLAG_TC1) { Start_Encoding = 2; } //флаг окончания DMA поднят, можно кодировать вторую половину половину /* USER CODE END DMA1_Channel1_IRQn 0 */ HAL_DMA_IRQHandler(&hdma_adc1); /* USER CODE BEGIN DMA1_Channel1_IRQn 1 */ /* USER CODE END DMA1_Channel1_IRQn 1 */
} /**
* @brief This function handles TIM3 global interrupt.
*/
void TIM3_IRQHandler(void)
{ /* USER CODE BEGIN TIM3_IRQn 0 */ HAL_NVIC_ClearPendingIRQ(TIM3_IRQn); /* USER CODE END TIM3_IRQn 0 */ HAL_TIM_IRQHandler(&htim3); /* USER CODE BEGIN TIM3_IRQn 1 */ /* USER CODE END TIM3_IRQn 1 */
}

Таким образом вся основная программа сводится к запуску таймера и DMA, инициализации Speex и его кодирования (помимо стандартных инициализаций HAL конечно):

Speex_Init(); if(HAL_TIM_Base_Start_IT(&htim3) != HAL_OK) Error_Handler(); if(HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&IN_Buffer[0],FRAME_SIZE*2) != HAL_OK) Error_Handler(); while (1) { EncodingVoice(); }

А теперь немного пробегусь по коду. В функции Speex_Init инициализируется только кодировщик Speex, декодер нужно инициализировать отдельно.

Итак, мы настроили АЦП на срабатывание по триггеру таймера. Триггер таймера мы сбрасываем в прерывании каждые 0.125мс (8 кГц).

HAL_NVIC_ClearPendingIRQ(TIM3_IRQn);

По прерыванию DMA у нас происходит следующее:

if (DMA1->ISR & DMA_FLAG_HT1) { Start_Encoding = 1; }
if (DMA1->ISR & DMA_FLAG_TC1) { Start_Encoding = 2; }

Флаг DMA_FLAG_HT1 (half transfer complete) поднимается когда DMA выполнило работу на половину (читай первая половина буфера заполнена), а флаг DMA_FLAG_TC1 (transfer complete flag) соответственно, когда DMA закончило передачу (вторая половина заполнена).

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

#define FRAME_SIZE 160 //*0.125мс = 20мс (сэмплирование 8кГц)
#define ENCODED_FRAME_SIZE 20 //размер выходных данных
#define MAX_REC_FRAMES 90 //максимальное число записываемых фреймов, Время = MAX_REC_FRAMES*0,02сек

Семплирование АЦП идет в двойной буфер IN_Buffer[2][FRAME_SIZE], каждая половина размером 160 сэмплов. На выходе уже получаем ENCODED_FRAME_SIZE байт данных, которые отправляются в массив REC_DATA[2][MAX_REC_FRAMES*ENCODED_FRAME_SIZE] по адресу Rec_Data_ptr. Адрес инкрементируется на ENCODED_FRAME_SIZE.

После каждого кодирования счетчик Encoded_Frames инкрементируется и в момент, когда он станет равен MAX_REC_FRAMES, первая половина выходного буфера становится полностью заполнена и можно забирать данные. На это у нас есть время, пока заполняется вторая половина, и так по кругу. Данные забираем из REC_DATA[0] и REC_DATA[1] соответственно.

https://www.youtube.com/watch?v=0fpdNWFnggQ

Можно попробовать поиграться с рамерами фрейма, настройками качества и прочее, но я не стал.

int quality = 4, complexity=1, vbr=0, enh=1;/* SPEEX PARAMETERS, MUST REMAINED UNCHANGED */

Пример переданного звукового файла есть в репозитории первой статьи.

Материалы

1. Репозиторий с получившимся проектом на Github
2. Speex Codec Manual
3. Application Note от Silicon Labs

Источник: http://www.pvsm.ru/stm32/248822

Аналого цифровой преобразователь stm32 adc

ПодробностиКатегория: Микроконтроллеры STM32Опубликовано 19.06.2015 11:40Автор: AdminПросмотров: 5544

Давайте попробуем разобраться как работает аналого цифровой преобразователь – АЦП или adc в stm32.

АЦП как следует из названия преобразует аналоговый сигнал в сигнал понятный микроконтроллеру а именно цифровой код.

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

К примеру при частоте 24 МГц, время преобразования будет 1,17 мкс.

АЦП в микроконтроллерах STM32 имеет следующие параметры:

  • разрядность  – 12 бит;
  • количество каналов -18 шт (16 внешиних и 2 внутренних)
  • самокалибровка;
  • возможность запуска по внешнему событию;
  • работа с DMA – прямым доступом к памяти;
  • старт преобразования от внешнего события;
  • одиночное и непрерывное преобразование;
  • генерация прерывания после завершения преобразования.

Для считывания данных с 16-ти внешних каналов имеется мультиплексор, обращение к каждому каналу осуществляется поочередно.

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

Структурная схема микроконтроллера STM32

Измеренное напряжение сравнивается с опорным V_REF- и V_REF+.

V_REF- подключается к земле, а V_REF+ можно подключить к питанию микроконтроллера либо к источнику опорного напряжения (ИОН)

Напряжение Результат
V_Ref-
V_in V_in/(V_Ref+-VRef-)*4096
V_Ref+ 4096

Не буду описывать регистры для работы с АЦП вы их можете сами посмотреть в документации на соответствующий микроконтроллер stm, а лучше приведу код программы для настройки АЦП.

Код программы инициалицации ADC (АЦП) микроконтроллера stm32

void adc_init() { RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // настройки ADC ADC_InitTypeDef ADC_InitStructure; ADC_StructInit(&ADC_InitStructure); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // режим работы – одиночный, независимый ADC_InitStructure.

ADC_ScanConvMode = DISABLE; // не сканировать каналы, просто измерить один канал ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // однократное измерение ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // без внешнего триггера ADC_InitStructure.

ADC_DataAlign = ADC_DataAlign_Right; //выравнивание битов результат – прижать вправо ADC_InitStructure.

ADC_NbrOfChannel = 1; //количество каналов – одна штука ADC_Init(ADC1, &ADC_InitStructure); ADC_Cmd(ADC1, ENABLE); // настройка канала ADC_RegularChannelConfig(ADC1, ADC_Channel_8, 1, ADC_SampleTime_55Cycles5); // калибровка АЦП ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); }

 Метод получения результата измеренияuint16_t get_adc_value() { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); return ADC_GetConversionValue(ADC1); }

Данный метод работает следующим образом: сначала запускается преобразование, преобразование осуществляется до тех пор пока флаг преобразования EOC (END of Conversion) не будет изменен,  далее производится считывание данных с регистра DR.

Остается только в главном методе main проинициализировать на АЦП и в цикле получать значение.void main() { adc_init(); uint16_t value = 0; while(1) value = get_adc_value(); } 

Источник: http://www.radio-magic.ru/microcontrollers/stm32/233-adc-stm32

Записки программиста

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

В этой заметке мы рассмотрим простой пример использования обоих устройств. Для экспериментов я использовал плату LimeSTM32 на базе STM32F405, имеющего как АЦП, так и ЦАП.<\p>

Итак, создаем новый проект.

В 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<\p>

При настройке ЦАП выяснилось, что аналоговый выход есть только на пинах 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!
“);      

    // Похоже, что АЦП нужно время на инициализацию.

    // Без этой задержки в зависимости от тела 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-и битным АЦП.

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

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

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

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

Заинтересованный читателю стоит обратиться к статье Учимся передавать звук с использованием протокола I2S, в рамках которой был написан простой WAV-проигрыватель.

Используя подход, аналогичный описанному в той статье, можно записывать звук при помощи АЦП и затем воспроизводить его при помощи ЦАП.

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

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

При использовании PAM8403 не удивляйтесь, если на на выходе у него окажется не синусоида, поскольку он является усилителем класса D.

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

Источник: https://eax.me/stm32-adc-dac/

prog:dma:dma_intro [StartMilandr]

prog:dma:dma_intro

DMA (Direct Memory Access) необходим для пересылки данных, без использования ядра микроконтроллера. Это позволяет не тратить процессорное время на банальную пересылку данных в цикле. Ядро запускает обмен и занимается своими делами, пока не возникнет прерывание от DMA о том, что заданное количество данных передано.

Как и в любой функции Copy, для запуска необходимы следующие параметры:

  • Адреса откуда и куда
  • Количество передаваемых данных (цикл DMA)
  • Размерность передаваемых данных
  • Инкремент адресов после передачи каждого значения

После передачи очередного слова данных, адреса, как источника данных, так и приемника могут либо увеличиваться на размер слова данных, либо оставаться неизменными. Инкремент необходим, когда передается/принимается массив данных. Неизменный адрес необходим при считывании/записи в регистр, например, когда выбран адрес АПЦ или ЦАП.

Пересылаемые данные могут быть:

  • 8 -разрядные (Byte)
  • 16-разрядные (HalfWord)
  • 32-разрядные (Word)

Самый простой пример применения DMA – это копирование массивов. Обсуждать тут особо нечего, поскольку это соответствует обычному циклу копирования в программе, только копирование происходит параллельно работе ядра.

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

Например, можно настроить, чтобы одно слово данных копировалось из адреса источника, по событию готовности АЦП. Тогда, если адресом источника выбрать регистр ADC_RESULT, то DMA может без участия ядра считывать готовые значения из АЦП и складывать их, например, в массив.

Затем, когда DMA закончит передавать заданное количество данных (цикл DMA), сгенерируется прерывание от DMA, и ядро может обработать весь массив полученных данных от АЦП.

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

Аналогично, для вывода в ЦАП, адресом назначения DMA выбирается регистр DAC_DATA, а событием, по которому происходит передача слова, выбирается событие таймера CNT == ARR. Тогда с помощью DMA получится равномерный (по таймеру) вывод сигнала в ЦАП.

Форма сигнала для вывода в ЦАП в данном случае обычно прописывается массивом, который выбирается в качестве источника данных для DMA. Размер массива указывается в параметре – Количество передаваемых данных.

При окончании передачи массива возникает прерывание от DMA – окончание цикла DMA.

При возникновении прерывания по окончанию цикла DMA канал автоматически выключается – сбрасывается бит в регистре Enable. Если при этом к DMA по прежнему будут поступать запросы на обработку, то прерывание продолжит генерироваться, даже не смотря на то, что канал выключен. Поэтому чтобы не возникало непрерывных прерываний от DMA необходимо в этом прерывании

  • либо настроить новый цикл DMA и включить канал в регистре Enable
  • либо выключить запросы к DMA от периферии

Представим ситуацию, когда мы настроим DMA передать максимально большое количество данных – для МК “Миландр” это 1024-ре значения. Данные же настроим выводиться в ЦАП по таймеру и очень не быстро.

Получится, что пока ЦАП выводит медленный сигнал, то большую часть времени DMA простаивает. Т.е. DMA работает только когда получает событие периода от таймера – DMA скопирует значение в регистр DAC_DATA и на покой.

Это было бы не разумно, ведь есть еще много задач, которые можно было бы возложить на DMA.

Поэтому DMA имеет 32 канала и каждый канал настраивается отдельно на свою задачу. Все что мы рассматривали до этого, относилось к одному каналу DMA. Таким образом, мы можем одновременно запустить и копирование массивов, и считывание АЦП и запись в ЦАП и многое другое. Все это будет работать как бы “параллельно”.

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

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

Поэтому общее количество данных для передачи разбивается на “порции”.

Для этого, в настройках канала используется параметр R_power – количество передач до процедуры арбитража. Значение этого параметра задает степень 2 – т.е. возможны 1, 2, 4, 8, 16, 32, 64, 128, 256, 512 и 1024 непрерывные передачи, которые не могут прерваться процедурой арбитража.

Например, нам надо передать 100 значений из массива в массив. Если выставить R_power = 6 (ведь 26 = 64), то канал передаст 64-е значения и разрешит провести арбитраж. Далее:

  • Если нет запросов от более приоритетных каналов, то текущий канал закончит передачу оставшихся 36-х значений. После этого канал разрешит арбитраж и вызовет прерывание от DMA – завершение цикла DMA.
  • Если есть запросы от более приоритетных каналов, то DMA обрабатывает эти приоритетные каналы. Наш канал выставляет запрос на обработку и ожидает арбитраж, на котором DMA вернется к исполнению канала и будут переданы оставшиеся 36 значений.

Задавая параметр R_power необходимо учитывать, что, выставляя большое значение, мы ускоряем исполнение одного канала в ущерб исполнению других. Обычно блочная передача (R_power > 0) используется для программной пересылки данных.

Например, в SPL DMA используется при работе с FIFO буферами Ethernet. При работе же с такой периферией, как АЦП и ЦАП, нет смысла устанавливать значение R_power больше 0, ведь арбитраж логично отдавать после каждой передачи.

В случае же с периферией, где есть FIFO, возможна блочная передача.

Передачу по одному значению будем называть одиночной передачей, а передачу 2R_power количества данных – блочной передачей.

Периферийные блоки могут запрашивать блочные и одиночные передачи, используя внутренние сигналы dma_req[С] и dma_sreq[С]. С – здесь означает канал, т.е. у для каждого канала эти сигналы свои.

  • dma_req[C] – Запрос на блочную передачу DMA . Когда от периферии приходит запрос dma_req, контроллер DMA выполняет 2R_power передач, далее контроллер DMA ожидает новый запрос dma_req или dma_sreq от периферии. Если в цикле DMA осталось выполнить меньше количество передач, чем 2R_power, то выполняется оставшееся количество передач, чтобы закончить цикл DMA.
  • dma_sreq[C] – Запрос на одиночную передачу DMA (от single). Когда от периферии приходит запрос dma_sreq, контроллер DMA выполняет 1 передачу, далее контроллер DMA ожидает новый запрос dma_sreq от периферии.

Обработку одиночных запросов от dma_sreq[C] можно отключить/включить – для этого используются регистры CHNL_USEBURST_SET/CHNL_USEBURST_CLR. Тогда будут обрабатываться только запросы от dma_req[C].

Предполагаю, что запросы к DMA от периферии с FIFO формируются следующим образом (например UART):

  • Как только освобождается место в FIFO к DMA выставляется сигнал dma_sreq.
  • Как только срабатывает выставленный порог по заполнению в FIFO к DMA выставляется сигнал dma_req.

Таким образом, к DMA будет выставлено несколько запросов по линии dma_sreq, к тому моменту как сработает порог заполненности буфера и выставится запрос dma_req. DMA может ответить на каждый dma_sreq, либо при USEBURST закинет весь блок данных за один раз при dma_req.

Каналы DMA жестко привязаны к сигналам dma_req[С] и dma_sreq[С] от определенных периферийных блоков. Например, для микроконтроллеров серии 1986ВЕ9х каналы DMA могут обрабатывать следующие запросы от периферии.

НомерИсточник sreqИсточник req
UART1_TX UART1_TX
1 UART1_RX UART1_RX
2 UART2_TX UART2_TX
3 UART2_RX UART2_RX
4 SSP1_TX SSP1_TX
5 SSP1_RX SSP1_RX
6 SSP2_RX SSP2_TX
7 SSP2_RX SSP2_RX
8 ADC1_EC
9 ADC1_EC
10 TIMER1
11 TIMER1
12 TIMER1
13
31

Каналы 13 – 31 чисто програмные. Кстати, запрос на передачу аппаратного канала также можно выставить программно. Делается это через регистр CHNL_SW_REQUEST записью “1” в бит по индексу канала. Регистр доступен только на запись.

У каждого канала есть свой приоритет, и он определяется индексом канала. 0-ой канал самый приоритетный, далее приоритет следует в порядке убывания. Если посмотреть распределение каналов DMA для серии 1986ВЕ9х, то самый приоритетный – это UART1_TX. Приоритет по умолчанию можно перекрыть, задав каналу высший приоритет.

Для задания высшего приоритета используется регистр CHNL_PRIORITY_SET, в который необходимо записать “1” в бит по индексу канала. Для возвращения приоритета в обычное состояние (определяемое индексом), используется регистр CHNL_PRIORITY_CLR – запись “1” в бит по индексу канала.

Чтение обоих регистров возвращает текущее состояние приоритета канала: 0 – приоритет по индексу, 1 – высший приоритет.

На картинке ниже я попытался представить все выше сказанное про арбитраж и каналы DMA.

По картинке видно следующее:

  • Всего есть 32 канала DMA
  • Приоритет каналов убывает от 0-го канала к 31-му.
  • Каналы передают данные блоками по 2R_power, между которыми осуществляется арбитраж – смена канала.
  • DMA передача – это передача одного слова данных заданной размерности.
  • Цикл DMA – это общее количество данных, которое необходимо передать. Задается параметром N_minus_1.
  • Для передачи следующей порции данных необходим запрос на обслуживание канала DMA – DMA Request. По этой причине более приоритетный канал С-1 отдает управление каналу С – ведь запрос к С-1 еще не установлен к моменту арбитража. Поэтому исполнение переходит к каналу С, и только на следующем арбитраже канал С-1 передаст оставшееся слово.
  • Под DMA Request подразумевается запрос по сигналам sreq, req или программный запрос.
  • Каждый канал управляется своей структурой, состоящей из 4-х 32-битных слов.
    • Последнее слово резервное и не используется.
    • Два слова в структуре задают адрес источника и приемника данных
    • Все настройки обмена задаются в слове Control.
      • Общее количество передач задается полем N_minus_1 на при запуске. Далее это значение используется для вычисления текущего адреса передаваемых данных и уменьшается контроллером DMA при передаче каждого слова.
      • Младшие 3 бита Control задают режим работы канала. При окончании цикла DMA, это поле сбрасывается контроллером в 0, что означает режим STOP – канал работу закончил.

По поводу программных каналов DMA стоит отметить, что если не выставить режим “Авто запрос”, то необходимо в цикле программно вызывать DMA Request, чтобы канал участвовал в арбитраже и имел возможность передать следующую порцию данных. Ведь в данном случае нет аппаратных сигналов req и sreq, которые бы выставляли DMA Request.

Каждый канал управляется своей структурой, которая состоит из 4-х 32-разрядных слов и в которой заданы все упомянутые выше параметры:

  • Source End Pointer – Конечный адрес источника данных
  • Destination End Pointer – Конечный адрес приемника данных
  • Control – Параметры обмена
  • Reserved – Не используется.

Параметры цикла DMA канала находятся в слове Control. Это слово меняется в процессе работы канала, и по этому слову можно диагностировать, сколько осталось данных для передачи и закончил ли работу канал. Именно это слово должно быть восстановлено начальными значениями, при перезапуске цикла DMA. Поля слова Control:

битыполеЗначенияОписание
31..30 dst_inc b00 – байт b01 – полуслово b10 – полусловоb11 – ноль/нет инкремента Инкремент адреса приемника
29..28 dst_size Размерность данных приемника, всегда равна src_size!
27..26 src_inc Инкремент адреса источника
25..24 src_size Размерность данных источника
23..21 dst_prot_ctrl b001 – Привилегированый доступ b010 – Буферизируетсяb100 – Кэшируется Флаги привилегий приемника
20..18 src_prot_ctrl Флаги привилегий источника
17..14 R_power b000 – 1 передача Передач до арбитража
b001 – 2 передачи
b010 – 4 передачи
b1010 – 1024 передачи
13..4 N_minus_1 0 – 1024 Передач в цикле DMA
3 next_useburst b0 – sreq и req Переопределение CHNL_USEBURST_SET[C] в режимес изменением конфигурации
b1 – req
2..0 cycle_ctrl b000 – STOP Режим работы
b001 – Основной
b010 – Авто-запрос
b011 – Пинг-понг
b100/b101 – Реконфигурация с памятью
b110/b111 – Реконфигурация с периферией

Контроллер DMA вычисляет адрес текущей передачи из конечного адреса по следующей формуле:

Addr = data_end_ptr – (n_minus_1 CHNL_PRIORITY_SET |= 1

Источник: https://startmilandr.ru/doku.php/prog:dma:dma_intro

Урок stm32cube adc dma (стм32 ацп настройка dma) – подборка отличного видео с youtube

2 год назад

https://www.youtube.com/watch?v=wdHV6ZtzYqU

В этом видео разбираю работу АЦП (ADC) с использованием таймера (TIM) и DMA Группа ВК: http://vk.com/radioandelectronics Я ВКонтакте: http://vk.com/id65222672 Мой твиттер: https://twitter.com/VHEMaster Старый канал: https://www.youtube.com/user/VHEMaster Партнёрская программа: https://youpartnerwsp.com/join?99101

2 год назад

АЦП ADS1115 16-бит – http://ali.ski/jw9P-5 Скетч – https://goo.gl/XTNqOO Музыка: Kevin MacLeod – Severe Tire Damage http://incompetech.

com ================================================== CashBack сервис EPN (Кэшбэк): Купить товары со скидкой (AliExpress, GearBest, BangGood) – http://epngo.bz/cashback_index/d1a54 Браузерный плагин ePN Cashback – http://epngo.

bz/cashback_install_plugin/d1a54 Зарабатывай с партнерской программой: EPN – http://epngo.bz/epn_index/d1a54 ADMITAD – https://www.admitad.com/ru/promo/?ref=b9875c55ed ==================================================

2 год назад

A waveform generator using DAC on STM32F4Discovery is build.

Connecting the generator output to an ADC input, the waveform may be visualized via USART on the PC by a LabView virtual instrument using VISA. Both direct conversions and DMA conversions are presented.

The USB to TTL adapter used is CH340, which worked well even at 921600 bps. Part of the ARM course at Hyperion University. www.hyperion.ro

4 год назад

https://www.youtube.com/watch?v=wdHV6ZtzYqU

В данном уроке рассматривается генерация кода и исправление его до рабочей версии. Яндекс-деньги: 410012533560895 Специально для сайта amberclan.clan.su Скачать исходники урока можно от сюда: http://amberclan.clan.su/load/stm32/primery_raboty_s_stm32f4_discovery/3-1-0-47 Партнёр медиакомпании https://youpartnerwsp.com/join?17282

3 год назад

STM32Cube STM32F103C8Tx + SWD ST-LINK V2 программатор . Небольшая отладочная плата и программатор за малые деньги. То что я использую или использовал в своих видео : STM32F103C8T6 отладочная плата http://alipromo.com/redirect/cpa/o/odz3mombw78h1lerm8qymuj32ydi1vjv/ ESP8266 отладочная плата http://alipromo.

com/redirect/cpa/o/oc9brnbrwypiyzn0haf3td1hbmqdcpt2/ Программатор STM32&STM8 http://alipromo.com/redirect/cpa/o/oa378i3nidtz9kc3stdvrirgcrrz5mrx/ Логический анализатор http://alipromo.com/redirect/cpa/o/o63n8onn7vyk06pkyhcel86i1nxtfgsh/ Проводки для логического анализатора http://alipromo.

com/redirect/cpa/o/o8dcmswvnsdun0bitvboq1icmvqkaht4/ NEOWAY M590 GSMGPRS модуль http://alipromo.com/redirect/cpa/o/opy858h5be14ks030sy6gq14nl6trbgu/ Блютуз модуль http://alipromo.com/redirect/cpa/o/opy8c6iwwlqpa59fsn98fkmdgcbaq5cv/ Дисплей ST7735S http://alipromo.

com/redirect/cpa/o/o248nydwn1hbmjy469du1dqj89pibel7/ Проводки http://alipromo.com/redirect/cpa/o/o8dcicb9w1a9fzogkd3jyi20mkaen6wh/ Датчик движения http://alipromo.com/redirect/cpa/o/oe22b2k612322r1mnpvq4h2pag0oy1ts/ BME280 крутой датчик http://alipromo.

com/redirect/cpa/o/odi9d4ogwp1mqbp6nk1amfnafujegbwb/ DC-DC step-down http://alipromo.com/redirect/cpa/o/o9hrvjgzrigx0waezd0w3efoakeprn4i/ Бумага для ЛУТ http://alipromo.com/redirect/cpa/o/o7kyo7n6lt4333hjnfqs82lhrh2hsg4k/

2 год назад

Using HAL API and Cube Mx, we program an ADC in DMA circular mode, and the results are transmitted using USART with DMA. The very high speed of the transmission needs a proper USB-TTL adapter (FTDI and CH340 worked well, other China clones didn't!). © Universitatea Hyperion 2016. www.hyperion.ro

3 год назад

STMicroelectronic's STM32CubeMX is a powerful graphical software configuration tool which enables users to generate C initialization code using a wizard interface. In this webinar recording, you will learn how to use it together with Keil MDK to set up and maintain projects for the STM32 micro controller families.

8 год назад

Источник: http://AlexKolobok.ru/watch/DQoT77F-Dig

ZiB

Привожу простой пример совместной работы АЦП+ПДП для платы STM32F4-Discovery.

Задача (источник http://kazus.ru/forums/showthread.php?t=104295):

  1. По событию от таймера запускать преобразования регулярного канала АЦП.
  2. По окончании преобразования АЦП, контроллер ПДП должен скопировать данные в ОЗУ.

Запускать преобразования АЦП в STM32F4 можно от нескольких источников, для примера я выбрал Таймер 3:

Запросы от АЦП “подключены” к нулевому каналу нулевого потока контроллера ПДП:

Примечание: Возможно “поток” не очень хороший перевод в данном случае.

Исходный код:

/* * File: main.c * Date: 29.04.2013 * Denis Zheleznyakov http://ziblog.ru */ #include “main.h”

uint16_t adc_buffer[8]; //——————————————————————————

inline static void gpio(void)
{ RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_GPIODEN; // светодиод
PIN_CONFIGURATION(PIN_LED); // кнопка
PIN_CONFIGURATION(PIN_BUTTON); // АЦП
PIN_CONFIGURATION(PIN_ADC_CH1);
} //——————————————————————————

inline static void adc(void)
{ ADC_InitTypeDef ADC_InitStructure; ADC_CommonInitTypeDef ADC_CommonInitStructure; // разрешаем тактирование
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //
ADC_InitStructure.ADC_ScanConvMode = ENABLE; ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T3_TRGO; ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising; ADC_InitStructure.ADC_NbrOfConversion = 1; ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; ADC_Init(ADC1, &ADC_InitStructure); // выбор канала
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_3Cycles); // базовая настройка
ADC_CommonInitStructure.ADC_Mode = ADC_Mode_Independent; ADC_CommonInitStructure.ADC_Prescaler = ADC_Prescaler_Div2; ADC_CommonInitStructure.ADC_DMAAccessMode = ADC_DMAAccessMode_Disabled; ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles; ADC_CommonInit(&ADC_CommonInitStructure); //
ADC_DiscModeCmd(ADC1, DISABLE); ADC_EOCOnEachRegularChannelCmd(ADC1, ENABLE); ADC_DMARequestAfterLastTransferCmd(ADC1, ENABLE); ADC_DMACmd(ADC1, ENABLE); // включаем АЦП
ADC_Cmd(ADC1, ENABLE);
} //——————————————————————————

inline static void dma(void)
{ DMA_InitTypeDef DMA_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); // АЦП
DMA_DeInit(DMA2_Stream0); DMA_InitStructure.DMA_Channel = DMA_Channel_0; DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t) &ADC1->DR; DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t) &adc_buffer[0]; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; DMA_InitStructure.DMA_BufferSize = ARRAY_LENGHT(adc_buffer); DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Disable; DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull; DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; DMA_Init(DMA2_Stream0, &DMA_InitStructure); DMA_Cmd(DMA2_Stream0, ENABLE);
} //——————————————————————————

inline static void timer(void)
{ TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // базовая нстйрока
TIM_TimeBaseStructInit(&TIM_TimeBaseStructure); TIM_TimeBaseStructure.TIM_Period = 625 – 1; TIM_TimeBaseStructure.TIM_Prescaler = 2000 – 1; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV4; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); // выход синхронизации
TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); // запуск таймера
TIM_Cmd(TIM3, ENABLE);
} //——————————————————————————

void main(void)
{ gpio(); adc(); dma(); timer(); while (1) { if (PIN_SIGNAL(PIN_BUTTON)) { PIN_ON(PIN_LED); } else { PIN_OFF(PIN_LED); } }
}

Полный проект можно получить используя данный код совместно с шаблоном к конце записи:

http://ziblog.ru/2012/09/09/linii-vvoda-vyivoda-stm32-chast-3.html

2013-05-08

Небольшое дополнение от читателя блога (http://kazus.ru/forums/showthread.php?t=104295&page=4).

Кстати по поводу статьи, для полного эффекта если будет время то добавьте в статью этот код(использование только CMSIS):

void DMA2_Stream4_IRQHandler(void)
{ GPIOD->ODR ^=((1>>12)|(1>>13)|(1>>14)|(1>>15));
} uint16_t BUFF[100];
int main(void)
{ SystemInit(); //*********************GPIO*************************
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN; RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN; GPIOA->MODER |= GPIO_MODER_MODER6; //ADC1 CH6 PA6
//*********************TIM3*************************
RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; // тактирование таймера
TIM3->PSC = 160-1; // предделитель
TIM3->ARR = 1000-1; // переполнение
TIM3->CR2 |= TIM_CR2_MMS_1; // output (TRGO)
TIM3->DIER |= TIM_DIER_UDE; TIM3->CR1 |= TIM_CR1_CEN; //запуск счета
//********************DMA***************************
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN; DMA2_Stream4->CR&=~ DMA_SxCR_CHSEL; // 000: channel 0 selected
DMA2_Stream4->PAR= (uint32_t)&ADC1->DR; //
DMA2_Stream4->M0AR=(uint32_t)&BUFF[0]; // Массив
DMA2_Stream4->NDTR=100; // Длина буфера
DMA2_Stream4->CR|= DMA_SxCR_MINC; // Инкремент адреса
DMA2_Stream4->CR|= DMA_SxCR_MSIZE_0; // 16 бит
DMA2_Stream4->CR|= DMA_SxCR_PSIZE_0; // 16 бит
DMA2_Stream4->CR|= DMA_SxCR_CIRC; // Непрерывный режим
DMA2_Stream4->CR&=~ DMA_SxCR_DIR; // 01: peripheral-to-Memory
DMA2_Stream4->CR|= DMA_SxCR_PL; // 11: Very high приоритет
DMA2_Stream4->CR|= DMA_SxCR_TCIE; // Transfer complete interrupt enable
DMA2_Stream4->CR|= DMA_SxCR_EN; // Вкл. передачу
NVIC_EnableIRQ(DMA2_Stream4_IRQn); NVIC_SetPriority(DMA2_Stream4_IRQn,5); //********************ADC1**CH6 PA6***************
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; ADC1->CR1 |=ADC_CR1_SCAN; // Scan mode
ADC1->CR2 |=(ADC_CR2_EXTSEL_3); //tim 3 (TRGO)
ADC1->SQR3 =6; //выбор канала PA6
ADC1->CR2 |=ADC_CR2_DMA; //разрешаем рабуту DMA
ADC1->CR2 |=ADC_CR2_DDS; //DMA disable selection (for single ADC mode)
ADC1->CR2 &=~ADC_CR2_CONT; //Continuous conversion
ADC1->CR2 |=ADC_CR2_EXTEN_0; //01: Trigger detection on the rising edge
ADC1->CR2 |= ADC_CR2_EOCS; //разрешаем прерывания
ADC1->CR2 |=ADC_CR2_ADON; //Вкл. переобразования

Источник: http://ziblog.ru/2013/04/29/prostoy-primer-atsp-pdp-adc-dma-dlya-stm32f4-discovery.html

Ацп в stm32. часть 1

Микроконтроллер штука цифровая и обменивается с внешним миром цифоровыми сигналами: нулями и единицами. Однако иногда перед микроконтроллером встает задача произвести измерение какой либо плавно изменяющейся величины.

Это может быть всё то, что принимает несколько промежуточных состояний (а не только два) например это может быть темепература, напряжение, сила тока, освещенность и так далее, примеров много. Однако, нога контроллера настроенная на вход различает только два состояния – присутствие на ноге напряжения (лог. 1) и его отсутствие (лог. 0).

Для измерения  температуры это малопригодно, ведь мало кому интересен градусник имеющий только два состояния -50 и +200 градусов 🙂 Для решения проблем измерения аналоговых величин придумали АЦП – Аналого-цифровой преобразователь.

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

Тут нарисована обвязка которая должна быть обязательно, если мы хотим использовать АЦП. К ней относятся два конденсатора C4 и С3 плюс дроссель L1. Все это добро предназначено для того чтоб обеспечить АЦП контроллера качественным питанием без каких либо помех. Располагать конденсаторы желательно как можно ближе к выводу AVCC.

На всякий случай уточню, что питается модуль АЦП отдельно от всего остального контроллера. На плате STM32VL Discovery уже вся эта обвязка имеется, что очень удобно для нас. Остальные детальки будут использованы нами в следующей статье, где мы будем применять АЦП на практике.  Забегая вперед скажу, что в STM32 с АЦП всё не так просто как например в AVR.

Имеется очень много параметров, о значении некоторых мне пока  мало что понятно. Но попробуем разобраться что к чему. Начать следует с того что контроллер может измерять напряжения только с определённых ножек в названии которых присутствует слово ADC1_INx, где х некоторый уникальный номер канала. АЦП контроллера может опрашивать каждый из каналов поочередно.

И вот тут начинается то, что взорвало мне мозг в клочья поначалу. В STM32 есть два варианта чтения данных с ножек ADC1_INx. Первый называется “Регулярные каналы” (regular channels в даташите).

Использование этого метода опроса состоит в том, что АЦП опрашивает по очереди некоторый заранее настроенный список каналов, после каждого опроса результат записывается в один и тот же регистр. Это означает, что нужно своевременно забирать результат преобразования из этого регистра, в противном случае результат будет перетираться.

Совсем иначе дело обстоит с “Инжектированными каналами” (Injected channels). В случае использования этого метода опроса можно записывать результат измерения каждого канала АЦП в свой отдельный регистр ничего не перезатирая. Но к сожалению таких регистров всего 4 а ножек АЦП у контроллера 16 штук.

Но лично мне еще никогда не доводилось использоваь более двух каналов АЦП, так что использование инжектированных каналов мне пришлось больше по душе 🙂 Еще стоит отметить, что АЦП STM32 обладает приятной штуковиной под названием Analog watchdog.

Его предназначение в том, чтоб подать сигнал в случае если напряжние на определённом канале выйдет за допустимый диапазон, это позволяет сэкономить процессорное время за счёт того, что нам не придётся программно заставлять АЦП производить измерение и потом сравнивать полученное значение с пороговыми. Удобная вещь, но пока не использовал.

Следующая плюшка – встроенный термометр. В даташите пишут, что использовать его для измерения абсолютных температур лучше не стоит, а вот для того чтоб отследить изменение температуры оно вполне годно. Начнем наше знакомство с АЦП  как обычно с краткого описания регистров, тех назначение которых я более или менее понял. В первую очередь стоит сказать о регистрах в которые записывается результат измерения АЦП, таковых у нас пять. Первый регистр это ADC_DR:

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

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

Таким образом, если мы не используем ни прерываний ни DMA то использовать регулярные группы можно только если канал в этой группе всего один (так у нас не будет перетираться регистр DR данными с разных каналов). Что касается самого регистра, то видно, что он 16-ти битный. А АЦП у нас 12-ти битный.

Для удобства данные могут выравниваться как по левому краю регистра так и по правому. Это решается битом ALIGN регистра ADC_CR2. Мы еще вернемся к нему позже. В следующие четыре регистра ADC_JDRx записываются результаты преобразования для каждого из 4-х инжектированных каналов.

Ни каких других особенностей этот регистр не имеет. Другой не маловажный регистр который нужно рассмотреть это ADC_SR, он отражает состояние АЦП на данный момент: 

STRT – этот бит устанавливается когда стартует преобразование для регулярного канала

JSTRT – тоже самое но для инжектированного канала

JEOC – закончилось преобразование для инжектированного канала

EOC – закончилось преобразование инжектированного или регулярного канала

AWD – Analog watchdog сработал

Все биты сбрасываются программным путём, а EOC может быть сброшен еще и автоматически когда из регистра ADC_DR заберут данные. Для настройки основных АЦП используют два регистра ADC_CR1 и ADC_CR2. Структура первого из них выглядит следующим образом: 

AWDEN – Analog watchdog мониторит состояние всех каналов регулярной группы

JAWDEN – Analog watchdog мониторит состояние всех каналов инжектированной группы

AWDSGL – Analog watchdog мониторит состояние одного канала указанного в битах AWDCH

AWDCH[4:0] – Номер канала АЦП который будет мониторить Analog watchdog

Как правильно выставлять все эти биты, проясняет следующая таблица: 

JAUTO – разрешает автоматическое преобразование для каналов в инжектированной группе после каналов  регулярной группы.

SCAN – не совсем понятно что такое, судя по всему нужно ставить этот бит если опрашиваем несколько каналов ацп за раз. 

JEOCIE –  Включает или выключает прерывания для инжектированных каналов
AWDIE – Включает или выключает прерывания от Analog watchdog
EOCIE – Включает или выключает прерывания по окончанию преобразования в регулярной или инжектированной группе

Следующий регистр настроек это ADC_CR2

TSVREFE – включает температурный сенсор. До установки этого бита читать что-то с канала к которому он подключен – бесполезно.

SWSTART – запускает преобразование для каналов входящих в состав регулярной группы. Для того, чтоб преобразование было запущено этим битом нужно предварительно установить биты EXTSEL[2:0] в единицы.

JSWSTART – запускает преобразование для каналов входящих в состав инжектированной группы. Для того, чтоб преобразование было запущено этим битом нужно предварительно установить биты JEXTSEL[2:0] в единицы.

EXTTRIG – Разрешает использовать внешнее событие для старта преобразования каналов в регулярной группе

EXTSEL[2:0] – Этими битами выбирается источник который будет запускать преобразование каналов в регулярной группе. Доступны следующие битовые комбинации:  

EXTSEL2

EXTSEL1

EXTSEL0

Источник

Timer 1 CC1 event

1

Timer 1 CC2 event

1

Timer 1 CC3 event

1

1

Timer 2 CC2 event

1

Timer 3 TRGO event

1

1

Timer 4 CC4 event

1

1

EXTI line 11

1

1

1

SWSTART

JEXTTRIG – Разрешает использовать внешнее событие для старта преобразования каналов в инжектированой группе

JEXTSEL[2:0] – Этими битами выбирается источник который будет запускать преобразование каналов в инжектированой группе. Доступны следующие битовые комбинации: 

JEXTSEL2

JEXTSEL1

JEXTSEL0

Источник

Timer 1 TRGO event

1

Timer 1 CC4 event

1

Timer 2 TRGO event

1

1

Timer 2 CC1 event

1

Timer 3 CC4 event

1

1

Timer 4 TRGO event

1

1

EXTI line15

1

1

1

JSWSTART

ALIGN – Устанавливает выравнивание результата в регистре данных (0 – по правому, 1 – по левому)

RSTCAL – Сбрасывает значение калибровки

CAL – Запускает калибровку АЦП. После завершения калибровки бит сбраывается в ноль

CONT – Разрешает непрерывное преобразование

ADON – Включает/выключает модуль АЦП

DMA – Разрешает использовать DMA

Самое страшное позади 🙂 , теперь рассмотрим регистры ADC_SMPR1 и ADC_SMPR2. В них задается время выборки индивидуально для каждого канала. 

 

Четыре регистра ADC_JOFRx хранят значения которые будут вычитаться из соответствующих регистров ADC_JDRx. Для чего нужна такая фича, я пока не очень догадываюсь. Но как говорится если звёзды зажигают значит это кому-нибудь нужно.

Регистры ADC_HTR и ADC_LTR задают верхнюю и нижню границу с которыми Analog watchdog сравнивает значение выбранного канала АЦП. Рисовать табличку тут смысла особого нет, так как в регистр просто записываются данные в первые 12 бит.

Следующие три регистра ADC_SQRx определяют, какие каналы входят в регулярную группу. Всего можно добавить не более 16-ти каналов. Битами L[3:0] нужно задать число каналов в группе (0000 – 1 канал … 1111 – 16 каналов).

 На всякий случай нарисую все три регистра тут, чтоб в даташит лишний раз не лазить: 

И последний регистр на сегодня это ADC_JSQR. Он уже успел принести мне немало хлопот пока я не прочитал то что про него написано мелким  шрифтом в примечании. Для начала нужно сказать, что он хранит в себе номера каналов которые входят с состав инжектированной группы. Сам регистр выгядит так: 

Битами JSQx задаются номера каналов, а в JL[1:0] записывается количество каналов в инжектированной группе. Вот тут-то и начинаются сложности.

Если в инжектированную группу добавлены все четыре канала то путаницы ни какой нет: В регистр ADC_JDR1 будут записывать данные с канала указанного битами JSQ1, в ADC_JDR2 с канала JSQ2 итд. Но все меняется если мы хотим включить в состав инжектированной группы не все 4 канала, а например только один.

Было бы логично предположить, что если канал один то выбирать его нужно битами JSQ1, если каналов 2 то битами JSQ1 и JSQ2 и так далее. Но это не так. В даташите написанно примерно тоже самое что я изобразил в виде таблицы:

Кол-во каналов

ADC_JDR1

ADC_JDR2

ADC_JDR3

ADC_JDR4

4

JSQ1

JSQ2

JSQ3

JSQ4

3

JSQ2

JSQ3

JSQ4

2

JSQ3

JSQ4

1

JSQ4

Из таблицы видно, что если мы включаем в группу только один канал то его мы выбираем битами JSQ4 а читаем из регистра ADC_JDR1. Вот такие дела. Ну на сегодня хватит таблиц и какой либо информации вообще. В следующей статье будет практика применения. Спасибо что дочитали до конца 🙂

Источник: http://easystm32.ru/for-beginners/18-adc-in-stm32

Ссылка на основную публикацию
Adblock
detector
",css:{backgroundColor:"#000",opacity:.6}},container:{block:void 0,tpl:"
"},wrap:void 0,body:void 0,errors:{tpl:"
",autoclose_delay:2e3,ajax_unsuccessful_load:"Error"},openEffect:{type:"fade",speed:400},closeEffect:{type:"fade",speed:400},beforeOpen:n.noop,afterOpen:n.noop,beforeClose:n.noop,afterClose:n.noop,afterLoading:n.noop,afterLoadingOnShow:n.noop,errorLoading:n.noop},o=0,p=n([]),h={isEventOut:function(a,b){var c=!0;return n(a).each(function(){n(b.target).get(0)==n(this).get(0)&&(c=!1),0==n(b.target).closest("HTML",n(this).get(0)).length&&(c=!1)}),c}},q={getParentEl:function(a){var b=n(a);return b.data("arcticmodal")?b:(b=n(a).closest(".arcticmodal-container").data("arcticmodalParentEl"),!!b&&b)},transition:function(a,b,c,d){switch(d=null==d?n.noop:d,c.type){case"fade":"show"==b?a.fadeIn(c.speed,d):a.fadeOut(c.speed,d);break;case"none":"show"==b?a.show():a.hide(),d();}},prepare_body:function(a,b){n(".arcticmodal-close",a.body).unbind("click.arcticmodal").bind("click.arcticmodal",function(){return b.arcticmodal("close"),!1})},init_el:function(d,a){var b=d.data("arcticmodal");if(!b){if(b=a,o++,b.modalID=o,b.overlay.block=n(b.overlay.tpl),b.overlay.block.css(b.overlay.css),b.container.block=n(b.container.tpl),b.body=n(".arcticmodal-container_i2",b.container.block),a.clone?b.body.html(d.clone(!0)):(d.before("
"),b.body.html(d)),q.prepare_body(b,d),b.closeOnOverlayClick&&b.overlay.block.add(b.container.block).click(function(a){h.isEventOut(n(">*",b.body),a)&&d.arcticmodal("close")}),b.container.block.data("arcticmodalParentEl",d),d.data("arcticmodal",b),p=n.merge(p,d),n.proxy(e.show,d)(),"html"==b.type)return d;if(null!=b.ajax.beforeSend){var c=b.ajax.beforeSend;delete b.ajax.beforeSend}if(null!=b.ajax.success){var f=b.ajax.success;delete b.ajax.success}if(null!=b.ajax.error){var g=b.ajax.error;delete b.ajax.error}var j=n.extend(!0,{url:b.url,beforeSend:function(){null==c?b.body.html("
"):c(b,d)},success:function(c){d.trigger("afterLoading"),b.afterLoading(b,d,c),null==f?b.body.html(c):f(b,d,c),q.prepare_body(b,d),d.trigger("afterLoadingOnShow"),b.afterLoadingOnShow(b,d,c)},error:function(){d.trigger("errorLoading"),b.errorLoading(b,d),null==g?(b.body.html(b.errors.tpl),n(".arcticmodal-error",b.body).html(b.errors.ajax_unsuccessful_load),n(".arcticmodal-close",b.body).click(function(){return d.arcticmodal("close"),!1}),b.errors.autoclose_delay&&setTimeout(function(){d.arcticmodal("close")},b.errors.autoclose_delay)):g(b,d)}},b.ajax);b.ajax_request=n.ajax(j),d.data("arcticmodal",b)}},init:function(b){if(b=n.extend(!0,{},a,b),!n.isFunction(this))return this.each(function(){q.init_el(n(this),n.extend(!0,{},b))});if(null==b)return void n.error("jquery.arcticmodal: Uncorrect parameters");if(""==b.type)return void n.error("jquery.arcticmodal: Don't set parameter \"type\"");switch(b.type){case"html":if(""==b.content)return void n.error("jquery.arcticmodal: Don't set parameter \"content\"");var e=b.content;return b.content="",q.init_el(n(e),b);case"ajax":return""==b.url?void n.error("jquery.arcticmodal: Don't set parameter \"url\""):q.init_el(n("
"),b);}}},e={show:function(){var a=q.getParentEl(this);if(!1===a)return void n.error("jquery.arcticmodal: Uncorrect call");var b=a.data("arcticmodal");if(b.overlay.block.hide(),b.container.block.hide(),n("BODY").append(b.overlay.block),n("BODY").append(b.container.block),b.beforeOpen(b,a),a.trigger("beforeOpen"),"hidden"!=b.wrap.css("overflow")){b.wrap.data("arcticmodalOverflow",b.wrap.css("overflow"));var c=b.wrap.outerWidth(!0);b.wrap.css("overflow","hidden");var d=b.wrap.outerWidth(!0);d!=c&&b.wrap.css("marginRight",d-c+"px")}return p.not(a).each(function(){var a=n(this).data("arcticmodal");a.overlay.block.hide()}),q.transition(b.overlay.block,"show",1*")),b.overlay.block.remove(),b.container.block.remove(),a.data("arcticmodal",null),n(".arcticmodal-container").length||(b.wrap.data("arcticmodalOverflow")&&b.wrap.css("overflow",b.wrap.data("arcticmodalOverflow")),b.wrap.css("marginRight",0))}),"ajax"==b.type&&b.ajax_request.abort(),p=p.not(a))})},setDefault:function(b){n.extend(!0,a,b)}};n(function(){a.wrap=n(document.all&&!document.querySelector?"html":"body")}),n(document).bind("keyup.arcticmodal",function(d){var a=p.last();if(a.length){var b=a.data("arcticmodal");b.closeOnEsc&&27===d.keyCode&&a.arcticmodal("close")}}),n.arcticmodal=n.fn.arcticmodal=function(a){return e[a]?e[a].apply(this,Array.prototype.slice.call(arguments,1)):"object"!=typeof a&&a?void n.error("jquery.arcticmodal: Method "+a+" does not exist"):q.init.apply(this,arguments)}}(jQuery)}var debugMode="undefined"!=typeof debugFlatPM&&debugFlatPM,duplicateMode="undefined"!=typeof duplicateFlatPM&&duplicateFlatPM,countMode="undefined"!=typeof countFlatPM&&countFlatPM;document["wri"+"te"]=function(a){let b=document.createElement("div");jQuery(document.currentScript).after(b),flatPM_setHTML(b,a),jQuery(b).contents().unwrap()};function flatPM_sticky(c,d,e=0){function f(){if(null==a){let b=getComputedStyle(g,""),c="";for(let a=0;a=b.top-h?b.top-h{const d=c.split("=");return d[0]===a?decodeURIComponent(d[1]):b},""),c=""==b?void 0:b;return c}function flatPM_testCookie(){let a="test_56445";try{return localStorage.setItem(a,a),localStorage.removeItem(a),!0}catch(a){return!1}}function flatPM_grep(a,b,c){return jQuery.grep(a,(a,d)=>c?d==b:0==(d+1)%b)}function flatPM_random(a,b){return Math.floor(Math.random()*(b-a+1))+a}