Доброго времени суток. В этом посте я расскажу, как мне удалось заставить 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.
Графически это будет выглядеть так:
![](https://habrastorage.org/getpro/habr/upload_files/ded/3dd/298/ded3dd298df139bd024dc86ad6cd5f8c.png)
Настройка периферии
Чтобы не описывать каждый пункт по отдельности, на рисунках укажу, какие параметры периферии должны быть установлены. Также скажу, что вся работа над проектом велась в STM32CubeIDE.
1. Для ADC должен быть установлен Scan Conversion Mode, выбран канал(IN1), настроен триггер и DMA1, который будет перекладывать готовые измерения в память.
![](https://habrastorage.org/getpro/habr/upload_files/083/041/69f/08304169fb00e1a8c77a889d565a095b.png)
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 запускать каждое преобразование.
![](https://habrastorage.org/getpro/habr/upload_files/bf2/646/810/bf26468109684b9e4370c120c32fdd8d.png)
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) Можем наблюдать следующее изображение:
![Желтый - сигнал с DAC, зеленый - выход TIM3_CH1 Желтый - сигнал с DAC, зеленый - выход TIM3_CH1](https://habrastorage.org/getpro/habr/upload_files/be9/279/1c5/be92791c5caaed54add3b1e08f79d903.jpg)
На осциллограмме каждое переключение зеленого сигнала является моментом измерения. Можно увидеть, что присутствует разброс по времени между измерениями.
Теперь перенесем измеренные значения ADC на график и посмотрим, что получилось:
![Результат измерения, по горизонтальной оси значения CCR, по вертикальной оцифрованные значения DAC Результат измерения, по горизонтальной оси значения CCR, по вертикальной оцифрованные значения DAC](https://habrastorage.org/getpro/habr/upload_files/f2c/bb9/225/f2cbb92259ee10fd8636f96c7317a270.png)
Как видим, измеренные значения расположенные по времени измерения сигнала образуют прямую линию, доказывая то, что сигнал был оцифрован и измерен корректно, и данный метод работает.
Я попытался повторить тот же самый трюк на 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
Зря, практический пример всегда полезен. Сейчас статья крайне абстракта и многим наверняка не совсем понятно для решения какой задачи это может потребоваться.