АЦП преобразования в указанные моменты времени на STM32

  • Tutorial

Доброго времени суток. В этом посте я расскажу, как мне удалось заставить 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) Можем наблюдать следующее изображение:

Желтый - сигнал с DAC, зеленый - выход TIM3_CH1
Желтый - сигнал с DAC, зеленый - выход TIM3_CH1

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

Теперь перенесем измеренные значения ADC на график и посмотрим, что получилось:

Результат измерения, по горизонтальной оси значения CCR, по вертикальной оцифрованные значения DAC
Результат измерения, по горизонтальной оси значения CCR, по вертикальной оцифрованные значения DAC

Как видим, измеренные значения расположенные по времени измерения сигнала образуют прямую линию, доказывая то, что сигнал был оцифрован и измерен корректно, и данный метод работает.

Я попытался повторить тот же самый трюк на STM32F103C8T6, но к сожалению мне не удалось добиться такого же эффекта. Как только происходит первое событие по сравнению с CCR, MCU тот час же пытается оцифровать сразу все запрошенное количество, не дожидаясь следующих событий. Если у кого-то получится это реализовать, я с радостью добавлю в статью.

Выводы

Используя АЦП настроенный на преобразование по запросу от таймера, DMA1, который перекладывает оцифрованные значения в память, а также DMA2 для настройки следующего момента измерения удалось реализовать механизм, который позволяет измерять сигнал в произвольно заданные моменты времени не расходуя вычислительных ресурсов микроконтроллера. Если таймер синхронизировать с таймером ШИМ, или использовать этот же таймер, который генерирует ШИМ, то можем легко измерять сигнал точно в нужный момент.

Update 1:
Ссылка на тестовый пример на github.

Комментарии 14

    0
    Интересно, зачем нужно измерять в произвольные моменты?
    Не может быть проще, преобразовывать непрерывно, а потом брать только нужные? Тогда АЦП может выполнять функцию защиты от перегрузок, и ДМА освободится. Как вариант.
      +1
      Интересно, зачем нужно измерять в произвольные моменты?

      Очень часто нужно измерять при наступлении какого-то события.
      Не может быть проще, преобразовывать непрерывно, а потом брать только нужные?

      Бывает очень критично энергопотребление.
        +1
        Бывает очень критично энергопотребление.

        Ну тут явно не тот случай :)
          0
          Очень часто нужно измерять при наступлении какого-то события.

          В приведенной статье, эти события идут из таблицы через полновесный DMA2. Да и в целом, касательно управления BLDC, что может быть примером такого события?
            0
            В моем контроллере BLDC мотор управляется с помощью векторного управления. А из векторного управления известно, что всего может быть 8 возможных векторов тока (U+, W-, V+, U-, W+, V-, Zp, Zm). Применяя различную длительность этих векторов, можно задать любой угол и величину DQ тока. Также в моем контроллере, управление осуществляется с частотой 1кГц. Т.е. каждую миллисекунду должны быть измерены токи, которые текут в соответствующих фазах. А поскольку частота ШИМ составляет 20кГц, что в 20раз больше частоты управления, то в момент управления необходимо рассчитать точный момент измерения фазного тока и сделать несколько измерений к следующему моменту управления чтобы скорректировать D ток.
              +2
              Если с такого пояснения начать статью (+ временной диаграммой) она будет доступней и целостней.
                0

                Оно было, но я его потом вырезал, т.к. посчитал, что это отход от темы :)

                  +1
                  Зря, практический пример всегда полезен. Сейчас статья крайне абстракта и многим наверняка не совсем понятно для решения какой задачи это может потребоваться.
        0

        Очень интересно про togle on match, как Вы поняли, что АЦП преобразует не каждый раз?
        Я делал led драйвер buck current mode на g071 и АЦП у меня работал в таком же режиме, capture compare был всегда один и тот же т.к. мне интересно было сэмплить после коммутации. И я ничего такого не замечал, но я не то чтобы прям проверял.
        Ещё вопрос про HAL, почему вы не проверяете, что халовские функции возвращают? Или это просто для удобства восприятия кода?

          0
          >Очень интересно про togle on match, как Вы поняли, что АЦП преобразует не каждый раз?

          Экспериментально, я видел, что в CCR записываются новые значения, но в массиве преобразованных значений не появлялись новые.

          >Я делал led драйвер buck current mode на g071 и АЦП у меня работал в таком же режиме, capture compare был всегда один и тот же т.к. мне интересно было сэмплить после коммутации.
          И я ничего такого не замечал, но я не то чтобы прям проверял.

          У меня есть предположение, что если в параметре таймера External Trigger Conversion Edge установлен Trigger detection on the rising/failing edge флаг события по CCR устанавливается и сбрасывается только тогда, когда CNT и CCR равны. и единожды подняв флаг при первом сравнении и сделав преобразование, флаг так и остается висеть не смотря даже на то, что новое значение записано в CCR. Далее при работе в этом же режиме когда произойдет следующее сравнение таймер попытается снова поднять уже поднятый на предыдущем преобразовании флаг но в конечном итоге ADC не увидит никакого изменения флага и останется его ждать. А вот когда флаг при каждом сравнении меняет своё состояние всё работает нормально.

          >Ещё вопрос про HAL, почему вы не проверяете, что халовские функции возвращают? Или это просто для удобства восприятия кода?
          Это статья не про то, как правильно программировать и обходить различные ошибки. Кому будет необходимо — напишет, в этой же статье я описываю концепцию. Если будет слишком много воды, то самая главная суть описанная в статье будет расплывчата.
          0

          Пишите ещё про BLDC. С удовольствием глянул бы на Ваш проект, если он есть в открытом доступе

            +1
            Нет, его нет в открытом доступе. Но могу сказать, что я разрабатываю активную электромагнитную подвеску для автомобиля и уже есть некоторые успехи: рассчитал опытный образец линейного электромагнитного двигателя 1:10, изготовил его из металла, и реализовал на нем векторное управление с нуля. На данный момент достиг того времени, когда необходимо научить систему удерживать уровень в зависимости от колебаний основания.
            +1

            Очень интересна тема управления такими моторами. Пишите ещё

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

          Самое читаемое