Доброго времени суток. В этом посте я расскажу, как мне удалось заставить 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.
Alex-lab
Интересно, зачем нужно измерять в произвольные моменты?
Не может быть проще, преобразовывать непрерывно, а потом брать только нужные? Тогда АЦП может выполнять функцию защиты от перегрузок, и ДМА освободится. Как вариант.
xFFFF
Очень часто нужно измерять при наступлении какого-то события.
Бывает очень критично энергопотребление.
Andy_Big
Ну тут явно не тот случай :)
Alex-lab
В приведенной статье, эти события идут из таблицы через полновесный DMA2. Да и в целом, касательно управления BLDC, что может быть примером такого события?
DIVON Автор
В моем контроллере BLDC мотор управляется с помощью векторного управления. А из векторного управления известно, что всего может быть 8 возможных векторов тока (U+, W-, V+, U-, W+, V-, Zp, Zm). Применяя различную длительность этих векторов, можно задать любой угол и величину DQ тока. Также в моем контроллере, управление осуществляется с частотой 1кГц. Т.е. каждую миллисекунду должны быть измерены токи, которые текут в соответствующих фазах. А поскольку частота ШИМ составляет 20кГц, что в 20раз больше частоты управления, то в момент управления необходимо рассчитать точный момент измерения фазного тока и сделать несколько измерений к следующему моменту управления чтобы скорректировать D ток.
Alex-lab
Если с такого пояснения начать статью (+ временной диаграммой) она будет доступней и целостней.
DIVON Автор
Оно было, но я его потом вырезал, т.к. посчитал, что это отход от темы :)
IlyaGalkin
Зря, практический пример всегда полезен. Сейчас статья крайне абстракта и многим наверняка не совсем понятно для решения какой задачи это может потребоваться.