Top.Mail.Ru

STM32 ADC (АЦП) и DMA. Обзор, настройка и пример проекта.

Пришло время разобраться, что из себя представляет модуль ADC, он же АЦП, в микроконтроллерах STM32. Давайте по привычной схеме, сначала кратко пройдемся по теории, во второй части статьи - практический пример.

Теоретическая часть.

ADC в микроконтроллерах STM32.

И начнем с основных характеристик АЦП в STM32:

  • АЦП является 12-ти битным.
  • Возможна генерация прерывания по окончанию преобразования, по окончанию преобразования с инжектированного канала, а также возможно прерывание от Analog Watchdog (что это такое расскажу чуть ниже).
  • Возможно одиночное преобразование и преобразование в непрерывном режиме.
  • В наличии присутствует самокалибровка.
  • Доступен запуск преобразования от внешнего события.
  • Может работать в связке с DMA.

Структурная схема из даташита:

Структурная схема АЦП в STM32.

Пока не забыл про Analog Watchdog, опишу его работу.

Он нужен для того, чтобы следить, что измеренное напряжение не выходит за определенные значения. Причем может сканироваться как конкретный канал, так и группа каналов. В регистры ADC_HTR и  ADC_LTR заносим значения верхнего и нижнего порога соответственно, и в случае, если проверяемое напряжение выходит за эти пределы, генерируется прерывание. Полезнейшая вещь 👍

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

Как и в микроконтроллерах AVR, возможно выравнивание результата по правому или по левому краю. Тут правда результат 12-ти битный, но суть та же. Вот как все это выглядит в регистрах:

Регистры АЦП.

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

  • Регистр ADC_SR. Статусный регистр. Тут хранятся флаги – например, флаг, сигнализирующий об окончании преобразования.
  • Регистр ADC_CR1. Контрольный регистр. Тут всякие биты, разрешающие или запрещающие те или иные события. Например, включение Analog Watchdog для регулярных каналов, аналогично для инжектированных, включение различных прерываний – все это находится здесь, в этом регистре.
  • Регистр ADC_CR2. Еще один контрольный регистр, содержащий биты, отвечающие за использование АЦП совместно с DMA, а также за запуск преобразования от внешнего источника. Кроме того, тут можно разрешить или запретить использование датчика температуры. Важный бит – SWSTART, который запускает преобразование регулярных каналов.
  • Регистры ADC_SMPRx отвечают за время выборки.
  • В ADC_SQR и ADC_JSQR выбираем номера нужных нам каналов.
  • И, наконец, регистр данных – ADC_DR – там и только там мы будем забирать наши драгоценные результаты.

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

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

Сегодня мы как раз это и реализуем, ставим себе следующую задачу:

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

Между делом накидаю еще ссылок с использованием ADC в STM32:

Продолжаем... Наверно никого не удивлю выбором контроллера для реализации этого проекта - использовать буду STM32F10x, а точнее STM32F103VET6. И, конечно, последние несколько лет почти ни один проект для STM32 не обходится без использования STM32CubeMx.

Практическая часть.

Инициализация.

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

  • PA0: ADC1, канал 0
  • PA2: ADC1, канал 2
  • PC1: ADC1, канал 11
  • PC2: ADC1, канал 12
  • PC3: ADC1, канал 13
  • PC5: ADC1, канал 15

Всего 6 каналов, как и планировали. Приступаем к делу. Запускаем CubeMx и создаем новый проект. Я обычно работаю в IARv8, но, на самом деле, не знаю, насколько он пользуется популярностью по сравнению с 7-ой версией, поэтому для этого проекта использую именно IARv7. Напишите в комментариях, кто какую версию использует для своих проектов на сегодняшний день )

Проект создан, сразу же активируем необходимые выводы микроконтроллера. Это у нас - 6 входов АЦП, 2 входа для внешнего кварца и 2 входа для подключения отладчика:

Настройка АЦП в STM32.

Далее привычным нам образом настраиваем тактирование всего и вся:

Настройки тактирования в STM32CubeMx.

Задаем все по максимуму - 72 МГц для APB2, 36 МГц - APB1, на модули ADC1, 2, 3 отправляем 12 МГц. В качестве источника тактирования выбираем внешний кварцевый резонатор, который мы уже активировали на вкладке Pinout&Configuration ранее.

Базовую подготовку и настройку проекта на этом заканчиваем и переходим непосредственно к интересующим нас ADC и DMA. Окно настроек АЦП:

Конфигурация АЦП в STM32CubeMx.

Параметры конфигурируем следующим образом:

STM32 АЦП (ADC) и DMA.

Давайте разберем все значения подробнее:

  • Mode - здесь оставляем Independent mode, в общем-то, другие варианты сейчас недоступны (потому что мы задействовали только один модуль ADC)
  • Data alignment - выравнивание данных по правому краю
  • Scan Conversion Mode - этот параметр активизируем в том случае, если собираемся работать с несколькими каналами
  • Continious Conversion Mode - отключаем, будем запускать преобразования программно
  • Enable Regular Conversions - каналы настраиваем как регулярные
  • Number Of Conversion - здесь указываем число, равное числу каналов, которые мы будем опрашивать (в данном примере у нас 6 каналов)
  • External Trigger Conversion Source - этот параметр позволяет настроить событие, по которому будет запускаться АЦП, пока не используем
  • и, наконец, далее следуют настройки для каждого из каналов в отдельности.

Для каналов АЦП у нас есть три параметра. Rank и Channel позволяют установить очередность опроса. Поскольку нам очередность не важна, будем опрашивать каналы просто по порядку, в соответствии с их номерами. С этим все понятно, а вот на параметре Sampling Time хотелось бы остановиться поподробнее.

ADC в STM32 работает в общих чертах так... Все входные каналы АЦП подключаются к аналоговому мультиплексору, на выходе которого установлен конденсатор. При измерении напряжения N-го канала мультиплексор переключается в соответствующее состояние и входной сигнал начинает заряжать конденсатор. Собственно, напряжение на этом конденсаторе и будет подвергаться измерению. А параметр Sampling Time это ни что иное, как время в тактах, которое выделяется на заряд этого внутреннего конденсатора.

Возникает вполне резонный вопрос - почему бы не делать всегда это время минимальным? Меньшее время измерения - лучше?

Тонкость заключается в том, что если ток входного сигнала мал, то конденсатор просто не успеет зарядиться. Поэтому измеренное значения напряжения будет неточным. В таком случае необходимо увеличить Sampling Time этого канала. Общее же время преобразования вычисляется следующим образом:

T_{conv} = Sampling Time + 12.5\medspace cycles

Значение 12.5 соответствует контроллерам серии STM32F10x. Для STM32F4, например, это значение составляет 12 тактов. В принципе, разница невелика.

Возвращаемся к STM32CubeMx, с АЦП мы разобрались, переходим на вкладку настроек DMA:

DMA канал для последовательных АЦП преобразований.

Здесь у нас:

  • Increment Address - адрес регистра данных в периферии - не инкрементируем, адрес в памяти - инкрементируем
  • Data Width - Half Word - 16 бит данных.

Пример использования ADC и DMA.

С настройками на этом заканчиваем, генерируем проект и вносим в него наши изменения. Первым делом определим переменные, при этом не забывайте о том, чтобы помещать весь пользовательский код в секции USER_CODE. Код, помещенный в такие секции не будет удален/изменен/перемещен при перегенерации проекта. Добавим прямо в файл main.c:

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
#define ADC_CHANNELS_NUM                                         6

/* USER CODE END PD */

/* USER CODE BEGIN PV */
uint16_t adcData[ADC_CHANNELS_NUM];
float adcVoltage[ADC_CHANNELS_NUM];

/* USER CODE END PV */

В буфере adcData[] будем сохранять сырые данные с АЦП, а в adcVoltage[] - пересчитанные в Вольты значения. В функции main() добавляем запуск наших последовательных преобразований функцией HAL_ADC_Start_DMA():

int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_ADC1_Init();
  /* USER CODE BEGIN 2 */
  HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcData, ADC_CHANNELS_NUM);
  
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

Запустили АЦП и DMA и ожидаем результат. По окончании преобразования будет вызвана callback-функция HAL_ADC_ConvCpltCallback(). Для использования этой функции нам нужно просто переопределить ее и добавить туда любые необходимые нам операции:

/* USER CODE BEGIN 4 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
  if(hadc->Instance == ADC1)
  {
    for (uint8_t i = 0; i < ADC_CHANNELS_NUM; i++)
    {
      adcVoltage[i] = adcData[i] * 3.3 / 4095;
    }
  }
}

/* USER CODE END 4 */

Здесь мы проверяем, что источником вызова функции является именно модуль ADC1, и спокойно обрабатываем принятые данные. В нашем случае мы пересчитываем сырые значения в значения напряжения в Вольтах. Вот и все!

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

HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcData, ADC_CHANNELS_NUM);

Но! Следует отметить, что не стоит запускать преобразование непосредственно из callback-функции. Это может привести к тому, что новое преобразование завершится настолько быстро, что программа просто не успеет перейти к выполнению другого кода и будет постоянно крутиться в callback-функции.

На этом на сегодня заканчиваем, спасибо за внимание и до новых встреч на нашем сайте 🤝

Проект для этой статьи - ссылка.

Подписаться
Уведомить о
guest

17 комментариев
Старые
Новые
Межтекстовые Отзывы
Посмотреть все комментарии
LexResident
LexResident
2 лет назад

Архив битый

LexResident
LexResident
Ответ на комментарий  Aveal
2 лет назад

Благодарю, теперь все отлично.

sid
sid
1 год назад

очепятка " * 3.3 / 4095 "

Алексей
Алексей
11 месяцев назад

Добрый день! не совсем понятно почему в HAL_ADC_Start_DMA передается указатель на uint32_t в то время как из ADC мы получаем 16-битное значение и массив, в котором хранятся полученные значения тоже из 16-битных элементов

Илья
Илья
Ответ на комментарий  Алексей
7 месяцев назад

Хороший вопрос)

Александр
Александр
5 месяцев назад

пример не рабочий

Александр
Александр
Ответ на комментарий  Aveal
5 месяцев назад

Добрый день. Пробовал этот пример на stm32L151. Записывает в буфер только первый вход АЦП.

12val12
12val12
4 месяцев назад

Добрый день
а как записать напримр 512 отсчетов с одного входа АЦП . с помощью ДМА
const int adcChunksize =512;
uint16_t ADC_buff1 [adcChunksize];
uint16_t ADC_buff2 [adcChunksize];
при этом ацп драйвится от timer 4 trigger out event
(каждые 2 мксек на отсчет по настройкам таймера )
потом когда запишется буфер 1 я анализирую и дучаю что с ним делать
( на это есть целая милисекунда)в то же время пишется буфер 2
.когда запишется буфер 2 я думаю что с ним делать
при этом пишется снова буфер 1

Александр
Александр
5 дней назад

Ошибка тут:
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adcData, ADC_CHANNELS_NUM);
куда данные лягут? Может все же так - (uint32_t*)&adcData

Александр
Александр
Ответ на комментарий  Aveal
4 дней назад

Проверил, в железе все варианты рабочие, на Proteus adc stm32 врет, особенно при опросе нескольких регулярных/инжектированных каналов на одном ацп

17
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x