Доброго времени суток. В этом посте я расскажу, как мне удалось заставить STM32F407VET6 измерять аналоговые сигналы в указанные моменты времени с помощью DMA.
Главной моей причиной измерять сигналы в точное время была необходимость измерять токи, протекающие в шунтах контроллера трехфазного BLDC мотора.
Для упрощения и ограничения задачи условимся, что
-
мы знаем точные моменты времени, когда необходимо измерять сигнал;
-
из предыдущего пункта также ясно, сколько измерений должно быть произведено;
-
разница по времени между двумя последовательными измерениями может быть любой;
-
нам необходим только один канал для измерения в точные промежутки времени.
Учтем также следующие особенности STM32:
-
STM32 обладает DMA (DMA1) контроллером, который может избавить MCU от перекладывания данных из регистра ADC в RAM память по окончанию преобразования;
-
ADC преобразование может быть вызвано по некоторым событиям в том числе: TIMx_UP, TIMx_CCRy.
Таким образом каждое последующее преобразование может быть вызвано когда у таймера, который может триггерить ADC, срабатывает Capture/Compare или Update события. После окончания преобразования в работу вступает DMA1 и перекладывает измеренное значение в память MCU.
Конечно, можно на Capture/Compare событие активировать прерывание и в этом прерывании записывать новое значение в этот же Capture/Compare Register (CCR). Минусом этого подхода являются затраты микроконтроллера на обслуживание каждого прерывания (сохранение и восстановление стека для работы прерывания) а также сопутствующее ему отклонение времени измерения. И чем больше измерений должно быть сделано, тем больше будет отклонение. Конечно, можно учесть время входа и выхода из прерывания, но это слишком сложно, да и не нужно, если есть способ проще и лучше.
Обоснование использования CCR
Далее буду рассказывать про работу с Capture/Compare Register. С Update событием тоже можно сделать, принцип останется такой же, но регистры будут другие. Плюс использования CCR я вижу в том, что таймер всегда переполняется с одной и той же частотой.
С другой стороны, ту же самую работу по обновлению значения в TIMx->CCRy может взять на себя ещё один DMA (DMA2), если настроить его таким образом, что каждое событие по CCRy помимо запуска ADC также бы вызывало и обновление этого CCRy с помощью DMA2. Такой подход полностью освобождает нас от использования прерываний (всю работу берут на себя DMA2, ADC и DMA1), необходим лишь только массив значений CCR, который будет предоставлен DMA2 для отправки их в TIMx->CCRy. Поэтому же и все измерения произойдут в точно указанные промежутки времени.
Получается для работы алгоритма, должны соблюдаться следующие пункты:
-
ADC должен быть настроен на преобразование по внешнему событию от CCRy, каждое последующее преобразование должно ждать соответствующего ему CCR события;
-
После окончания преобразования DMA1 должен перекладывать преобразованное значение из регистра ADC в память MCU;
-
DMA2 должен срабатывать по тому же событию от CCR что и ADC. По каждому событию DMA2 должен записывать последующее значение в TIMx->CCRy регистр из массива чисел заданного при настройке DMA2.
-
Первый момент времени должен быть записан в CCR с помощью MCU, а не DMA.
Графически это будет выглядеть так:

Настройка периферии
Чтобы не описывать каждый пункт по отдельности, на рисунках укажу, какие параметры периферии должны быть установлены. Также скажу, что вся работа над проектом велась в STM32CubeIDE.
1. Для ADC должен быть установлен Scan Conversion Mode, выбран канал(IN1), настроен триггер и DMA1, который будет перекладывать готовые измерения в память.

2. Настройка таймера. Установку значений предделителя и AutoReload Register я опущу, т.к. они будут зависеть от вашего проекта. Для данного примера я экспериментально подобрал такие значения, которые бы позволили мне провести наглядный эксперимент, который я опишу ниже. Также для TIM3 необходимо добавить DMA, и указать его направление — из памяти в периферию, а также поставить галочку в пункте Increment Address для Memory. Прерывания для TIM3 устанавливались опционально для того, чтобы проверить, что в CCR каждый раз записываются новые значение с помощью DMA. Также в настройках конфигурации Output Compare CH1 может быть заменен на Output Compare No Output, если нет необходимости отображать состояние сравнения CCR с CNT на пине MCU.
Установка режима Toogle on match в TIM3 Output Compare Channel 1 позволяет отрабатывать каждое событие по CCR в ADC. Только в таком режиме со связкой в ADC : Trigger detection on both the rising and failling edges мне удалось заставить ADC запускать каждое преобразование.

3. На этом настройка периферии закончена, но для экспериментальной проверки включим DAC, который будет генерировать значения для оцифровывания, а пины микроконтроллера DAC и ADC IN1 соединим друг с другом, а также с каналом осциллографа.
С настройкой периферии разобрались, теперь необходимо написать код и провести эксперимент.
Код
Запишем нулевое значение в DAC и включим его. Далее в тестовом примере будет написана функция, которая постепенно увеличивает выходное значение DAC до достижения им потолка (4095).
HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, 0u);
__HAL_DAC_ENABLE(&hdac, DAC_CHANNEL_1);
Создадим массив для CCR значений и запишем в него случайные возрастающие значения, которые меньше чем значение, хранящееся в ARR, чтобы провести все измерения за один цикл таймера. Случайные значения взяты для того, чтобы глазами можно было убедиться в том, что сигнал действительно измеряется в разные не периодические моменты времени.
Инициализация значений CCR
uint16_t ccValues[MEASUREMENT_COUNT];
ccValues[0] = 115;
ccValues[1] = 252;
ccValues[2] = 388;
ccValues[3] = 475;
ccValues[4] = 582;
ccValues[5] = 646;
ccValues[6] = 727;
ccValues[7] = 871;
ccValues[8] = 993;
ccValues[9] = 1062;
ccValues[10] = 1211;
ccValues[11] = 1339;
ccValues[12] = 1425;
ccValues[13] = 1466;
ccValues[14] = 1541;
ccValues[15] = 1669;
ccValues[16] = 1818;
ccValues[17] = 1872;
ccValues[18] = 1963;
ccValues[19] = 2000;
Далее необходимо положить первое значение из массива в регистр CCR1, а также на всякий случай остановим таймер и сбросим его счетчик. Опционально включаются прерывания таймера по CC1 и заморозка счетчика в режиме отладки.
htim3.Instance->CCR1 = ccValues[0];
HAL_TIM_Base_Stop(&htim3);
htim3.Instance->CNT = 0;
__HAL_TIM_ENABLE_IT(&htim3, TIM_IT_CC1);
__HAL_DBGMCU_FREEZE_TIM3();
После этого запускаем преобразования ADC с DMA. ADC теперь будет ждать события от таймера.
HAL_ADC_Start_DMA(&hadc3, measuredAdcValues, MEASUREMENT_COUNT);
И следом запускается таймер с DMA2, при этом количество транзакций должно быть на единицу меньше, т.к. первое число уже находится в регистре CCR1.
HAL_TIM_OC_Start_DMA(&htim3, TIM_CHANNEL_1, &ccValues[1], MEASUREMENT_COUNT - 1u);
Осталось запустить на железке и проверить.
Проверка на железе
Подключив два щупа осциллографа к MCU (один к пину DAC/ADC IN1, другой к выходу TIM3_CH1) Можем наблюдать следующее изображение:

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

Как видим, измеренные значения расположенные по времени измерения сигнала образуют прямую линию, доказывая то, что сигнал был оцифрован и измерен корректно, и данный метод работает.
Я попытался повторить тот же самый трюк на STM32F103C8T6, но к сожалению мне не удалось добиться такого же эффекта. Как только происходит первое событие по сравнению с CCR, MCU тот час же пытается оцифровать сразу все запрошенное количество, не дожидаясь следующих событий. Если у кого-то получится это реализовать, я с радостью добавлю в статью.
Выводы
Используя АЦП настроенный на преобразование по запросу от таймера, DMA1, который перекладывает оцифрованные значения в память, а также DMA2 для настройки следующего момента измерения удалось реализовать механизм, который позволяет измерять сигнал в произвольно заданные моменты времени не расходуя вычислительных ресурсов микроконтроллера. Если таймер синхронизировать с таймером ШИМ, или использовать этот же таймер, который генерирует ШИМ, то можем легко измерять сигнал точно в нужный момент.
Update 1:
Ссылка на тестовый пример на github.
Автор: DIVON