ADC HAL stm32





Здравствуйте.

В статье описана работа с АЦП микроконтроллера stm32 с использованием библиотеки HAL.


АЦП (Аналого-Цифровой Преобразователь) достаточно сложная штука, а режимов работы очень много, тем не менее я постараюсь описать работу с этим интерфейсом с помощью HAL.


Примеры сделаны для платы BluePill (stm32F103C8T6) однако благодаря HAL'у всё ниже написанное можно переносить на другие МК.

У stm32F103 нету некоторых функций, которые есть у более продвинутых камней, их описание сделано в конце статьи.


Используемая среда разработки — TrueStudio, предварительная генерация — CubeMX.

Так же Вам пригодится потенциометр…



… чтоб изменять показания АЦП. Если такой штучки нет, то нет, просто соедините 3.3V со входом АЦП.




Итак, у нас есть два АЦП (ADC1 и ADC2) на других платах их может быть больше. Ножки обоих АЦП подключены к одному мультиплексору…


Reference manual RM0008 стр. 217

Vref — опорное напряжение для АЦП. У описываемой платы этих входов нет. Схема питания выглядит так…






У BluePill в общей сложности можно настроить 10 аналоговых входов…




Работу АЦП можно условно разделить на два режима — «Независимый» и «Парный». В первом случае можно запускать АЦП1 и АЦП2 (с любыми выбранными каналами) независимо друг от друга (это обычный режим), а во втором можно сделать так, что один АЦП будет запускать другой. Либо оба АЦП настроить на работу с одной и той же ножкой (далее канал).

Если к одному АЦП подключено несколько каналов, то их нельзя опрашивать по отдельности, можно только все вместе. Порядок чтения каналов можно менять.

Каналы АЦП делятся на два вида: регулярные каналы (regular channels) и инжектированные (Injected channels).
Суть инжектированного канала заключается в том, что у него есть своя отдельная ячейка для сохранения результата. То есть если каналы РА0, РА1, РА2, РА3 настроить как инжектированные, то результаты будут сохранены в четыре разные ячейки.
Инжектированных каналов может быть не больше четырёх. Любой аналоговый вход можно настроить как инжектированный.

У регулярных каналов всего одна ячейка на всех. То есть если каналы РА0, РА1, РА2, РА3 настроить как регулярные, то результат работы каждого канала будет записываться в одну и ту же ячейку, затирая предыдущие данные. Своевременно забирать результаты нам поможет DMA.

Ещё есть два внутренних канала, один подключён к встроенному термометру, а другой измеряет напряжение питания МК. Эти каналы называют оконным компаратором. Оба этих канала можно настроить как WatchDog.


В stm32 АЦП работает по методу последовательного приближения. Если есть желание почитать об этом, то загляните сюда.





Сначала будем работать с регулярными каналами, а потом с инжектированными.



Создадим проект в CubeMX


Настройте тактирование ADC (ADC Prescaler)



Частота АЦП не должна превышать 14МГц.


Инициализируйте ADC1



На вход ADC_IN0 (PA0) нужно подать считываемый сигнал, а в UART будем выводить показания.




В main.c добавьте хедер:

/* USER CODE BEGIN Includes */
#include "string.h" // это для функции strlen()
/* USER CODE END Includes */


Пару глобальных переменных:

/* USER CODE BEGIN PV */
char trans_str[64] = {0,};
uint16_t adc = 0;
/* USER CODE END PV */


Перед бесконечным циклом добавляем калибровку АЦП:

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
/* USER CODE END 2 */


И в бесконечный цикл добавьте следующий код:

/* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  HAL_ADC_Start(&hadc1); // запускаем преобразование сигнала АЦП
	  HAL_ADC_PollForConversion(&hadc1, 100); // ожидаем окончания преобразования
	  adc = HAL_ADC_GetValue(&hadc1); // читаем полученное значение в переменную adc
	  HAL_ADC_Stop(&hadc1); // останавливаем АЦП (не обязательно)
	  snprintf(trans_str, 63, "ADC %d\n", adc);
	  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
	  HAL_Delay(1000);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }


Прошивайте и смотрите результат. Вы должны получить значение от 0 до 4095 (АЦП у нас 12-ти битный).

про АЦП2
Чтобы читать данные со второго АЦП нужно просто изменить hadc1 на hadc2 (предварительно активировав какой-нибудь канал).

/* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  HAL_ADC_Start(&hadc1); // запускаем преобразование сигнала АЦП
	  HAL_ADC_PollForConversion(&hadc1, 100); // ожидаем окончания преобразования
	  adc = HAL_ADC_GetValue(&hadc1); // читаем полученное значение в переменную adc
	  HAL_ADC_Stop(&hadc1); // останавливаем АЦП
	  snprintf(trans_str, 63, "ADC %d\n", adc);
	  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);

          uint16_t adc2 = 0;
          HAL_ADC_Start(&hadc2); 
	  HAL_ADC_PollForConversion(&hadc2, 100); 
	  adc2 = HAL_ADC_GetValue(&hadc2);
	  HAL_ADC_Stop(&hadc2); 
	  snprintf(trans_str, 63, "ADC %d\n", adc2);
	  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);

	  HAL_Delay(1000);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }


Далее (до определённого момента) все примеры будут для первого АЦП.





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

Активируйте глобальное прерывание и сгенерируйте проект…



Добавьте колбек, в котором результат будет копироваться в переменную:

/* USER CODE BEGIN 0 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    if(hadc->Instance == ADC1) //check if the interrupt comes from ACD1
    {
	adc = HAL_ADC_GetValue(&hadc1);
    }
}
/* USER CODE END 0 */


После калибровки запустим АЦП:

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADC_Start_IT(&hadc1);
/* USER CODE END 2 */


В бесконечном цикле будем выводить результат и снова запускать АЦП:

while (1)
{
  snprintf(trans_str, 63, "ADC %d\n", (uint16_t)adc);
  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
  adc = 0;
  HAL_ADC_Start_IT(&hadc1);
  HAL_Delay(500);
  ...





Чтобы каждый раз не запускать АЦП, можно активировать режим циклического чтения каналов…



while (1)
{
  snprintf(trans_str, 63, "ADC %d\n", (uint16_t)adc);
  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
  adc = 0;
  //HAL_ADC_Start_IT(&hadc1);
  HAL_Delay(500);
  ...

В этом режиме АЦП будет стартовать автоматически после окончания предыдущего преобразования.

Тут есть один нюанс
Нужно увеличить время сэмплирования…


Если этого не сделать, то опрос АЦП будет происходить слишком быстро и программа будет постоянно висеть в прерывании. Подробно про сэмплирование написано ниже.

Другой вариант, это запускать АЦП без прерываний:

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADC_Start(&hadc1);
/* USER CODE END 2 */


В цикле делать так:

snprintf(trans_str, 63, "ADC %d\n", (uint16_t)HAL_ADC_GetValue(&hadc1));
HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);




Independent mode (независимый режим) — сейчас там нет возможности выбрать что-либо, однако если настроить АЦП_1 и АЦП_2, то появятся различные варианты («Парный режим» см. ниже).

Data Alignment — выравнивание данных по левому или правому краю. Поскольку АЦП 12-разрядный, а регистры 16-разрядные, то предусмотрена возможность выравнивания результата измерения по левому или по правому краю этих регистров.

Scan Conversion Mode — этот режим становится активным когда опрашиваются несколько каналов на одном АЦП.

Continious Conversion Mode — опрос канала/каналов производится циклически (нет необходимости перезапуска).

Discontinuous Conversion Mode — этот режим позволяет сканировать несколько каналов (на одном АЦП) так, чтобы опрос происходил не по всем каналам, а по заранее заданным группам каналов (см. ниже).

Enable Regular ConversionsEnable — каналы настроены как регулярные.




Теперь будем опрашивать два канала на одном АЦП с помощью DMA…



Scan Conversion Mode — стал активным так как работаем с несколькими каналами.

Continious Conversion Mode — отключим.

Enable Regular ConversionsEnable — каналы попрежнему настроены как
регулярные.

Number Of Conversion — кол-во каналов для опроса.

External Trigger Conversion Source — событие, которое будет запускать АЦП. Сейчас указан программный запуск, но можно делать это с помощью таймеров (см. ниже).

Rank — очерёдность опроса.

Channel — номер канала. В данном случае первым будет опрашиваться канал №0, а вторым №1. Каналы можно поменять местами.

Sampling Time — время (указывается в тактах), которое тратится на заряд внутреннего конденсатора. Чем больше значение, тем точнее результат (сейчас указано 1.5 такта).

АЦП работает так (очень упрощённо): поступающий сигнал сначала заряжает внутренний конденсатор, а потом происходит измерение напряжения в этом конденсаторе. Соответственно у поступающего сигнала должен быть большой ток чтоб быстро зарядить конденсатор. Если внешний сигнал слабенький, то нужно увеличить Sampling Time.

Время затрачиваемое на преобразование (опрос) одного канала можно посчитать так:

1.5 такта (заряд конденсатора) + 12.5 обязательных тактов (преобразование сигнала) = 14 тактов.
При условии, что АЦП тактируется от 12МГц, тогда 14 тактов выполнятся за одну с хвостиком микросекунду.


RM0008




Настраиваем DMA…




Оставляем прерывание только от DMA…




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


Переменную adc превращаем в массив:

/* USER CODE BEGIN PV */
char trans_str[64] = {0,};
volatile uint16_t adc[2] = {0,}; // у нас два канала поэтому массив из двух элементов
/* USER CODE END PV */


В колбеке делаем так:

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    if(hadc->Instance == ADC1)
    {
	HAL_ADC_Stop_DMA(&hadc1); // это необязательно
	snprintf(trans_str, 63, "ADC %d %d\n", (uint16_t)adc[0], (uint16_t)adc[1]);
	HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
	adc[0] = 0;
	adc[1] = 0;
	HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&adc, 2);
    }
}

Обратите внимание, DMA вызывает тот же колбек, что и ADC global interrupts.


После калибровки запускаем АЦП:

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&adc, 2); // стартуем АЦП
//HAL_ADC_Start_IT(&hadc1); // это здесь не нужно
/* USER CODE END 2 */


Убираем всё из бесконечного цикла и компилируем программу. Должно получится так…


Первый канал я подключил через делитель напряжения, а второй напрямую.


Если поменять каналы местами…



… то соответственно будет так…




DMA может работать только с АЦП1 и с АЦП3 (в более мощных МК). Передать данные из АЦП2 через DMA можно в режиме парной работы с АЦП1.





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

Оставляем один канал (для удобства чтения) и триггером указываем таймер №3 — External Trigger Conversion Source ⇨ Timer 3 Trigger Out event…




Отключаем DMA и активируем глобальное прерывание АЦП…




Настраиваем таймер следующим образом…


Таймер будет переполнятся каждые 650мс и создавать событие, которое будет запускать АЦП (Trigger Event Selection ⇨ Update Event). Прерывание включать не нужно.


Возвращаем переменную adc к прежнему виду:

/* USER CODE BEGIN PV */
char trans_str[64] = {0,};
volatile uint16_t adc = 0;
/* USER CODE END PV */


В колбеке (по окончанию опроса) будем копировать результат в переменную и выводить в UART:

/* USER CODE BEGIN 0 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    if(hadc->Instance == ADC1) //check if the interrupt comes from ACD1
    {
	adc = HAL_ADC_GetValue(&hadc1);
	snprintf(trans_str, 63, "ADC %d\n", adc);
	HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
	adc = 0;
    }
}
/* USER CODE END 0 */


После калибровки стартуем АЦП и таймер:

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADC_Start_IT(&hadc1);
HAL_TIM_Base_Start(&htim3);
/* USER CODE END 2 */


Компилируем и запускаем программу.



Другие таймеры могут запускать АЦП в режиме сравнения на определённом канале.

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


Таймер будет переполнятся так же как и в предыдущем примере, каждые 650мс, а сравнение будет происходить в момент обнуления и «толкать» АЦП.


В настройках АЦП нужно только изменить External Trigger Conversion Source



В коде, после калибровки, необходимо сделать так:

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADC_Start_IT(&hadc1);
HAL_TIM_OC_Start(&htim4, TIM_CHANNEL_4);
/* USER CODE END 2 */


Всё, можно пробовать. Остальные таймеры настраиваются аналогично.



Если совместить пример с DMA и этот, тогда можно свести к минимуму участие CPU.

Запустите DMA в циклическом режиме…


Mode ⇨ Circular.


Запустите АЦП в режиме DMA и таймер:

/* USER CODE BEGIN 2 */
  HAL_ADCEx_Calibration_Start(&hadc1);
  HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&adc, 2); 
  HAL_TIM_OC_Start(&htim4, TIM_CHANNEL_4);



Данные выводим в колбеке:

/* USER CODE BEGIN 0 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    if(hadc->Instance == ADC1)
    {
        snprintf(trans_str, 63, "ADC %d %d\n", (uint16_t)adc[0], (uint16_t)adc[1]);
        HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
    }
}

DMA работает циклично, а таймер «толкает» преобразование. Преобразование закончилось — данные скопировались в буфер через DMA, таймер снова толкнул АЦП — закончилось преобразование и данные снова скопировались в буфер через DMA (DMA работая в циклическом режиме постоянно ожидает окончания преобразования).




Запустим АЦП в режиме Discontinuous Conversion Mode.

В этом режиме можно опрашивать несколько каналов одного АЦП по частям. Допустим нам нужно опросить шесть каналов по два за раз, тогда если в Number of Discontinuous Conversion указать 2, то получится три группы по два канала. После запуска АЦП будет опрошена первая группа и работа прекратится, после повторного запуска опрос начнётся со второй группы, минуя первую, ну и после третьего запуска будет опрошена третья группа минуя первые две. Если бы было пять каналов, то в последней «группе» был бы один канал.

Будем опрашивать два канала (две «группы» по одному каналу). Отключаем глобальные прерывания АЦП и настраиваем следующим образом…


Number of Discontinuous Conversion — указываем 1, то есть при первом запуске будет опрошен один канал, а при втором другой.


Всё что запускалось после калибровки нам сейчас не нужно:

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
/* USER CODE END 2 */


В бесконечном цикле делаем так:

/* USER CODE BEGIN WHILE */
  while (1)
  {
	  uint16_t adc1 = 0;
	  uint16_t adc2 = 0;

	  HAL_ADC_Start(&hadc1);
	  HAL_ADC_PollForConversion(&hadc1, 100);
	  adc1 = HAL_ADC_GetValue(&hadc1); 

	  HAL_ADC_Start(&hadc1);
	  HAL_ADC_PollForConversion(&hadc1, 100);
	  adc2 = HAL_ADC_GetValue(&hadc1); 

	  HAL_ADC_Stop(&hadc1); // не обязательно
	  snprintf(trans_str, 63, "ADC %d %d\n", adc1, adc2);
	  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
	  HAL_Delay(500);
          ...

Запускаем АЦП, ждём, получаем результат первого канала, запускаем снова, ждём, и получаем результат второго канала.





Пришло время познакомиться с инжектированными каналами.


Как уже говорилось, инжектированных каналов может быть не больше четырёх и у них есть свои регистры для сохранения данных. Инжектированные каналы имеют приоритет над регулярными. Допустим у Вас постоянно опрашиваются регулярные каналы и если в какой-то момент запустить опрос инжект. канала, то опрос регулярных будет приостановлен на время обработки инжектированного (подразумевается, что и те и другие относятся к одному АЦП).


Отключайте все прерывания, DMA и прочее. Настраиваем…



Enable Regular ConversionsDisable — регулярные каналы отключены (однако ничто не мешает работать одновременно и с регулярными и с инжект. каналами).

Number Of Conversion — включены два инжект. канала.

External Trigger Conversion Source — то же самое, что и для регулярных каналов.

Injected Conversion Mode — можно указать уже знакомое Discontinuous Conversion, либо Auto Injected, либо ничего.

В разделе Rank всё то же самое, что и для регул. каналов + появился пункт Injected Offset (смещение). Можете поиграть этим значением (от 0 до 4095) и поглядеть на результат. Мне не очень понятно для чего это можно применить.


В коде пропишем глобальный массив adc[2]:

/* USER CODE BEGIN PV */
char trans_str[64] = {0,};
volatile uint16_t adc[2] = {0,};
/* USER CODE END PV */


После калибровки всё уберите:

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
/* USER CODE END 2 */


В бесконечном цикле делаем так:

/* USER CODE BEGIN WHILE */
while (1)
{
   HAL_ADCEx_InjectedStart(&hadc1); // запускаем опрос инжект. каналов
   HAL_ADC_PollForConversion(&hadc1,100); // ждём окончания

   // результат опроса каждого канала записывается в свой регистр, а мы забираем его и копируем в переменную
   adc[0] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);
   adc[1] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_2);

   snprintf(trans_str, 63, "ADC_INJ %d %d\n", (uint16_t)adc[0], (uint16_t)adc[1]);
   HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
   adc[0] = 0;
   adc[1] = 0;
   HAL_Delay(1000);
   ...

У инжект. каналов своя функция для запуска и свои функции получения результата. Функция ожидания такая же как и у регулярных каналов.




В режиме Discontinuous Conversion то же самое, что и с регулярными каналами.



Изменяем только бесконечный цикл:

/* USER CODE BEGIN WHILE */
  while (1)
  {
	  HAL_ADCEx_InjectedStart(&hadc1); // запускаем опрос 0-го инжект. канала
	  HAL_ADC_PollForConversion(&hadc1,100); // ждём окончания
	  adc[0] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);

	  HAL_ADCEx_InjectedStart(&hadc1); // запускаем опрос 1-го инжект. канала
	  HAL_ADC_PollForConversion(&hadc1,100); // ждём окончания
	  adc[1] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_2);

	  snprintf(trans_str, 63, "ADC_INJ %d %d\n", (uint16_t)adc[0], (uint16_t)adc[1]);
	  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
	  adc[0] = 0;
	  adc[1] = 0;

	  HAL_ADCEx_InjectedStop(&hadc1);
	  HAL_Delay(500);

Чтоб проверить работу режима закомментируйте HAL_ADCEx_InjectedStart для 1-го инжект. канала.


Опрашиваться он не будет так как мы в конце останавливаем (HAL_ADCEx_InjectedStop) работу. Если и эту строчку закомментить, тогда в первом цикле будет читаться канал №0, а во втором канал №1.




Auto Injected — этот режим позволяет непрерывно (не нужно каждый раз запускать) опрашивать инжект. каналы при условии, что опрашивается хотя бы один регулярный канал. Сначала регулярные затем инжектированные.


Настраивается это так…



Добавлен регулярный канал (IN2).

Включен Continuous Conversion Mode (в разделе ADC_Settings).

В разделе ADC_Injected_ConversionMode появился единственный возможный пункт External Trgger on injected channels are disabled. Это означает, что нельзя использовать никакие триггеры (так как триггером является окончание преобразования рег. канала).


После калибровки пишем так:

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADC_Start(&hadc1);
/* USER CODE END 2 */


В бесконечном цикле рисуем следующее:

/* USER CODE BEGIN WHILE */
while (1)
{
    uint16_t adc_inj[2] = {0,};
    uint16_t adc_reg = 0;

    adc_inj[0] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);
    adc_inj[1] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_2);
    adc_reg = HAL_ADC_GetValue(&hadc1);
    snprintf(trans_str, 63, "ADC inj0 %d inj1 %d reg2 %d\n", (uint16_t)adc_inj[0], (uint16_t)adc_inj[1], (uint16_t)adc_reg);
    HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
	  
    HAL_Delay(500);
    ...

Опрашиваются два инжект. и один рег. каналы в циклическом режиме. Здесь, и в других примерах пауза ставится для того, чтоб в UART выводилось не с бешеной скоростью.

Можно то же самое делать через прерывание (колбек для рег. каналов).

Надо сказать, что это весьма удобный режим, одной командой запускается всё что нужно.




Чтобы читать инжект. каналы по прерыванию, нужно включить глобальное прерывание АЦП…


Регулярный канал (из предыдущего примера) отключите, он нам здесь не нужен.


У инжект. каналов свой колбек:

/* USER CODE BEGIN 0 */
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    if(hadc->Instance == ADC1) //check if the interrupt comes from ACD1
    {
	adc[0] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);
	adc[1] = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_2);
	snprintf(trans_str, 63, "ADC_INJ %d %d\n", (uint16_t)adc[0], (uint16_t)adc[1]);
	HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
	adc[0] = 0;
	adc[1] = 0;
	//HAL_ADCEx_InjectedStart_IT(&hadc1);
    }
}
/* USER CODE END 0 */


После калибровки пишем так:

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
//HAL_ADCEx_InjectedStart_IT(&hadc1);
/* USER CODE END 2 */


А в бесконечном цикле будем периодически запускать:

/* USER CODE BEGIN WHILE */
while (1)
{
    HAL_ADCEx_InjectedStart_IT(&hadc1);
    HAL_Delay(500);
    ...

Можно и в прерывании запускать (раскомментив строчки в прерывании и после калибровки), но тогда в UART всё будет слишком быстро валиться.





Внутренние каналы и Analog WatchDog


Опрос внутренних каналов (Temperature Sensor и Vrefint) ничем не отличается от остальных, их можно настроить как регулярные или инжектированные. Поскольку использовать эти каналы интереснее всего в контексте WatchDog'а, то я решил описать их сейчас, а не раньше.


Для начала просто почитаем внутренние каналы, настроив их как инжектированные (чтоб удобнее было данные получать)


Из нового здесь только увеличенное время Sampling Time (рекомендуется для более точного результата), ну и названия самих каналов.


После калибровки ничего не нужно:

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
/* USER CODE END 2 */


А в бесконечном цикле сделаем так:

/* USER CODE BEGIN WHILE */
  while (1)
  {
	  uint16_t ts = 0;
	  uint16_t vref = 0;

	  HAL_ADCEx_InjectedStart(&hadc1);
	  HAL_ADC_PollForConversion(&hadc1,100);

	  ts = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);
	  vref = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_2);

	  snprintf(trans_str, 63, "TS %d Vref %d\n", (uint16_t)ts, (uint16_t)vref);
	  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);

	  HAL_ADCEx_InjectedStop(&hadc1);
	  HAL_Delay(500);
          ...

Прошиваем и смотрим.




Тут следует пояснить что же за цифру (1461) возвращает канал Verefint. Это некая калибровочная величина, которая получается следующим образом: если подать на микроконтроллер напряжение равное 3.3 вольта, то это значение будет соответствовать 1.2 вольта…


Datasheet STM32F103x8, STM32F103xB — cd00161566.pdf

Если понижать входное напряжение, то это значение будет расти, а если повышать, то значение будет уменьшаться.

Использовать данные из канала Verefint можно так…

...
uint16_t volt = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_3);

float vdd = 1.20 * 4096.0 / vref;
snprintf(trans_str, 63, "Vref: %d Vdd: %.2f\n", vref, vdd);
HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);

float f_volt = volt * vdd / 4096.0;
snprintf(trans_str, 63, "volt %d  f_volt %.2f\n", volt , f_volt);
HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
...



Первая строчка это значение из канала Verefint, и вычисленное напряжение подающееся на микроконтроллер (3.14). Вторая строчка это измерение напряжения на «пальчиковой» батарейки. Таким образом можно производить измерения не зная фактического опорного напряжения.

Это справедливо только для чипов F103, у других микроконтроллеров по другому. Например у F0 и F3 калибровочное значение записывается в ПЗУ при изготовлении, то есть оно фиксированное, а канал Verefint, на моей плате DiscoveryF303, возвращает значение 4095.

Прочитать калибровочное значение записанное в ПЗУ можно так…

#define VREFINT_CAL_ADDR 0x1FFFF7BA // адрес в ПЗУ где лежит калибровочное значение
uint16_t vrefint_cal_adr = *((uint16_t*)VREFINT_CAL_ADDR); // читаем значение



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




Теперь настроим прерывание, которое будет вызываться при превышении порогового значения на канале Temperature Sensor



Watchdog Mode — см. ниже.

Analog WatchDog Channel — указываем канал, который будет вызывать прерывание. Прерывание могут вызывать не только внутренние каналы, но и любые другие. Например, если активировать IN9 и настроить его в разделе ADC_Injected_ConversionMode, то он появится в этом пункте. Разумеется канал может быть как регулярным так и инжектированным.

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

В последнем пункте активируем прерывание WatchDog'а.


В коде нужно добавить специальный колбек, а всё остальное оставить как в предыдущем примере:

/* USER CODE BEGIN 0 */
void HAL_ADC_LevelOutOfWindowCallback(ADC_HandleTypeDef* hadc)
{
    if(hadc->Instance == ADC1) //check if the interrupt comes from ACD1
    {
        uint16_t ts = 0;
        ts = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);
        snprintf(trans_str, 63, "Interrupt TS %d\n", (uint16_t)ts);
        HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
    }
}


WatchDog сработает как только значение на канале Temperature Sensor выйдет за рамки порогового.

Прошивайте плату и положите что-нибудь горячее на камень.


Характерная особенность — нам не нужно нигде вызывать функцию запускающую прерывание (..._IT), достаточно просто опрашивать каналы в обычном режиме.



Если не нужно опрашивать никакие аналоговые каналы, а нужен только WatchDog'а (например по температуре), тогда достаточно после калибровки сделать так:

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADCEx_InjectedStart_IT(&hadc1);
/* USER CODE END 2 */




Watchdog ModeWatchDog может срабатывать не только от одного канала.

Single Injected — от одного инжектированного канала.

Single regular or Injected — от одного инжектированного канала или регулярного.

All Injected — от любых инжектированных каналов. В этом режиме исчезает пункт Analog WatchDog Channel. То есть, если на любом из активированных каналов результат преобразования выйдет за рамки порогового значения, то сработает WatchDog.

В зависимости от режима работы и выбранных каналов могут появляться различные варианты.




В разделе Mode есть пункт Conversion Trigger, он нужен для запуска АЦП от внешнего сигнала (подача высокого или низкого уровня на определённый пин).


Инжектированные каналы будут стартовать при появлении сигнала на ножке РС15 (ADC_EXTI15)



Conversion Trigger — здесь выбирается какие именно каналы (инжект., регул., или те и другие) будут запускаться.

External Trigger Conversion Source ⇨ EXTI line15. У регулярных каналов свой пин.


В настройках GPIO нужно указать какой сигнал (Rising, Falling или оба) будет запускать АЦП…


В данном случае при переходе с LOW на HIGH.
Пин подтянут к «земле».



После калибровки запускаем АЦП в обычном режиме:

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADCEx_InjectedStart(&hadc1);
/* USER CODE END 2 */


В бесконечном цикле делаем так:

/* USER CODE BEGIN WHILE */
  while (1)
  {
	  uint16_t ts = 0;
	  uint16_t vref = 0;

	  ts = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);
	  vref = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_2);

	  snprintf(trans_str, 63, "TS %d Vref %d\n", (uint16_t)ts, (uint16_t)vref);
	  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);

	  HAL_Delay(500);


После запуска программы и кратковременной подачи «плюса» на пин РС15 Вы увидите результат преобразования. Чтоб обновить данные нужно снова подать импульс.


Чтобы забирать результат по прерыванию нужно включить глобальное прерывание АЦП.

Добавить уже знакомый колбек:

/* USER CODE BEGIN 0 */
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef* hadc)
{
	if(hadc->Instance == ADC1) //check if the interrupt comes from ACD1
	{
		uint16_t ts = 0;
		uint16_t vref = 0;

		ts = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);
		vref = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_2);

		snprintf(trans_str, 63, "TS %d Vref %d\n", (uint16_t)ts, (uint16_t)vref);
		HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
                HAL_ADCEx_InjectedStart_IT(&hadc1);
	}
}


После калибровки сделать так:

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADCEx_InjectedStart_IT(&hadc1);
/* USER CODE END 2 */


А в бесконечном цикле можно всё удалить.

Компилируем, запускаем и тыкаем пин.

С другими вариантами пункта Conversion Trigger я думаю всё понятно — выбираем нужные каналы (или те и другие), после калибровки прописываем запуск выбранных каналов и соответствующие колбек/колбеки.




Парный режим


Настроим два АЦП (по одному каналу на каждый) и сделаем так, что при запуске первого АЦП будет одновременно запускаться второй. Таким образом одной командой запустятся два АЦП и будут работать синхронно.


Для первого АЦП выбираем канал IN0, а для второго IN1. После этого в первом АЦП в разделе ADCs_Common_Settings укажите режим Dual regular simultaneous mode only (настраивать нужно именно в таком порядке). Обратите внимание, что есть притушеные пункты — они активируются в зависимости от выбранных каналов (регул. инжект.).

Больше ничего трогать не надо, АЦП2 настроится автоматически…



В АЦП2 будет указан тот же режим, только его нельзя изменить так как АЦП2 подчинён АЦП1.

Еще нужно включить глобальное прерывание АЦП.


В колбеке получаем результат:

/* USER CODE BEGIN 0 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
	if(hadc->Instance == ADC1) //check if the interrupt comes from ACD1
	{
		uint16_t in0 = 0;
		uint16_t in1 = 0;

		in0 = HAL_ADC_GetValue(&hadc1);
		in1 = HAL_ADC_GetValue(&hadc2);

		snprintf(trans_str, 63, "ADC in0 %d in1 %d\n", (uint16_t)in0, (uint16_t)in1);
		HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
	}
}

В колбеке слушаем только первый АЦП, результат получаем с обоих.


После калибровки нужно запустить работу АЦП1 по прерыванию и активировать АЦП2:

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADCEx_Calibration_Start(&hadc2);
HAL_ADC_Start_IT(&hadc1);
HAL_ADC_Start(&hadc2);
/* USER CODE END 2 */

Калибруем оба АЦП.


В бесконечном цикле перезапускам АЦП1 каждые полсекунды:

/* USER CODE BEGIN WHILE */
  while (1)
  {
    HAL_ADC_Start_IT(&hadc1);
    HAL_Delay(500);
    ...

Повторюсь, это можно делать в самом колбеке, но тогда в UART всё будет слишком быстро выводится.



Чтобы получать данные без прерываний нужно после калибровки сделать так:

/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADCEx_Calibration_Start(&hadc2);
//HAL_ADC_Start_IT(&hadc1);
HAL_ADC_Start(&hadc2);
/* USER CODE END 2 */


А в бесконечном цикле вот так:

/* USER CODE BEGIN WHILE */
  while (1)
  {
	  uint16_t in0 = 0;
	  uint16_t in1 = 0;

	  HAL_ADC_Start(&hadc1);

	  HAL_ADC_PollForConversion(&hadc1, 100); // ждём

	  in0 = HAL_ADC_GetValue(&hadc1);
	  in1 = HAL_ADC_GetValue(&hadc2);

	  snprintf(trans_str, 63, "ADC in0 %d in1 %d\n", (uint16_t)in0, (uint16_t)in1);
	  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);

	  HAL_Delay(500);
          ...


Поскольку время преобразования — Sampling Time ⇨ 1.5 Cycles у нас одинаковое для обоих каналов, а АЦП работают синхронно, то в функции ожидания можно указать любое АЦП (&hadc1 или &hadc2). Если же у канала IN1 увеличить время, то и ожидать нужно его.


Помимо «ручного» запуска можно использовать таймеры в качестве триггера.

Для инжектированных каналов всё то же самое, только нужно подставить соответствующие функции.

пример
В CubeMX нужно указать Dual injected simultaneous mode only

/* USER CODE BEGIN 2 */
  HAL_ADCEx_Calibration_Start(&hadc1);
  HAL_ADCEx_Calibration_Start(&hadc2);
  HAL_ADCEx_InjectedStart(&hadc2);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  uint16_t in0 = 0;
	  uint16_t in1 = 0;

	  HAL_ADCEx_InjectedStart(&hadc1);
	  HAL_ADC_PollForConversion(&hadc1, 100);

	  in0 = HAL_ADCEx_InjectedGetValue(&hadc1, ADC_INJECTED_RANK_1);
	  in1 = HAL_ADCEx_InjectedGetValue(&hadc2, ADC_INJECTED_RANK_1);

	  snprintf(trans_str, 63, "ADC inj0 %d inj1 %d\n", (uint16_t)in0, (uint16_t)in1);
	  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);

	  HAL_Delay(500);
          ...


Чтоб не менять настройки, оставьте этот пример на потом.






Выше я писал, что АЦП2 не может самостоятельно работать с DMA, однако это возможно в парном режиме.


В CubeMX нужно настроить только DMA…



Размер данных нужно указать Word (целое слово).

В конце преобразования (на ADC1 или ADC2) через DMA передаётся 32-битный регистр ADC1_DR содержащий в левой половине «слова» данные АЦП2, а в правой АЦП1 (данные с АЦП у нас 16-ти битные). Такая вот интересная работа.


Добавьте глобальный массив adc[2]:

/* USER CODE BEGIN PV */
char trans_str[64] = {0,};
volatile uint16_t adc[2] = {0,};
/* USER CODE END PV */


В колбеке будем выводить данные:

/* USER CODE BEGIN 0 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    if(hadc->Instance == ADC1)
    {
    	//HAL_ADCEx_MultiModeStop_DMA(&hadc2);
        snprintf(trans_str, 63, "ADC %d %d\n", (uint16_t)adc[0], (uint16_t)adc[1]);
        HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
        adc[0] = 0;
        adc[1] = 0;
        //HAL_ADCEx_MultiModeStart_DMA(&hadc1, (uint32_t*)&adc, 2);
    }
}


После калибровки делаем так:

/* USER CODE BEGIN 2 */
  HAL_ADCEx_Calibration_Start(&hadc1);
  HAL_ADCEx_Calibration_Start(&hadc2);
  HAL_ADC_Start(&hadc2);
  HAL_ADCEx_MultiModeStart_DMA(&hadc1, (uint32_t*)&adc, 1);

Сначала запускается АЦП2, а потом АЦП1 в мульти-режиме.

В бесконечном цикле перезапускаем АЦП1:

/* USER CODE BEGIN WHILE */
  while (1)
  {
	  HAL_ADCEx_MultiModeStart_DMA(&hadc1, (uint32_t*)&adc, 1);
	  HAL_Delay(1000);
          ...


Обратите внимание, что длина равна 1 так как в DMA указано целое «слово».


Данные получаемые из каналов можно накапливать (для анализа или ещё чего-то). Если допустим нужно десять раз прочитать каналы, а потом смотреть результат, тогда делаем так:

Для обоих каналов включаем режим постоянного преобразования…



У DMA включаем инкремент памяти…



Увеличиваем буфер до 20:

/* USER CODE BEGIN PV */
char trans_str[64] = {0,};
volatile uint16_t adc[20] = {0,}; // по десять значений для каждого канала
/* USER CODE END PV */


И количество запрашиваемых данных указываем десять:

/* USER CODE BEGIN 2 */
  HAL_ADCEx_Calibration_Start(&hadc1);
  HAL_ADCEx_Calibration_Start(&hadc2);
  HAL_ADC_Start(&hadc2);
  HAL_ADCEx_MultiModeStart_DMA(&hadc1, (uint32_t*)&adc, 10);

После преобразования в буфере будут лежать по десять результатов с каждого канала. В колбеке выводить их как-то так…

snprintf(trans_str, 63, "ADC %d %d %d %d %d %d %d %d %d %d\n", (uint16_t)adc[0], (uint16_t)adc[1], (uint16_t)adc[2]...



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




Более точное измерение одного канала…

Dual fast interleaved mode only — этот режим работает следующим образом: после запуска, АЦП2 стартует немедленно, а АЦП1 через семь тактов. Может работать только с одним регулярным каналом.





Dual slow interleaved mode only — этот режим такой же как предыдущий, но АЦП1 стартует с большей задержкой…




Эти режимы можно использовать для измерения быстро меняющегося сигнала. Если бы мы измеряли этот сигнал последовательно, то не узнали бы что было в момент между измерениями, а так у нас получается практически непрерывное измерение одного канала.


С остальными режимами я пока не разбирался.




Функционал более продвинутых МК


Каналы можно настраивать как обычные — Single-ended, а можно как дифференциальные…



Дифференциальный АЦП
Взято отсюда.

«АЦП в режиме дифференциального входа, это когда на два входа приходят разные напряжения. Одно вычитается из другого, да еще может умножаться на коэффициент усиления. Зачем это нужно? А, например, когда надо замерить перекос напряжения измерительного моста. У какого-нибудь тензомоста при входном напряжении в пять вольт выходные сигналы будут различаться между собой всего лишь 30мВ, вот и поймай его. А так подал на диф вход, подогнал нужный коэффициент усиления и красота!»

Для экспериментов подайте на IN1 измеряемое напряжение, а IN2 посадите на «землю».

пример
/* USER CODE BEGIN 2 */  
  HAL_ADCEx_Calibration_Start(&hadc1, ADC_DIFFERENTIAL_ENDED); // ADC_SINGLE_ENDED или ADC_DIFFERENTIAL_ENDED
  HAL_ADC_Start(&hadc1);


/* USER CODE BEGIN WHILE */
  while (1)
  {
	  uint32_t adc = 0;
	  HAL_ADC_Start(&hadc1); // запускаем преобразование сигнала АЦП
	  HAL_ADC_PollForConversion(&hadc1, 100); // ожидаем окончания преобразования
	  adc = HAL_ADC_GetValue(&hadc1); // читаем полученное значение в переменную adc
	  HAL_ADC_Stop(&hadc1); // останавливаем АЦП
	  snprintf(trans_str, 63, "ADC %d\n", (uint16_t)adc);
	  HAL_UART_Transmit(&huart1, (uint8_t*)trans_str, strlen(trans_str), 1000);
	  HAL_Delay(500);
          ...



Clock Prescaler — устанавливается предделитель частоты АЦП.

Resolution — можно менять разрешение АЦП. Чем меньше, тем быстрее происходит преобразование, но снижается точность.

DMA Continuous Requests — непрерывный запрос к DMA. Нужно включить режим Continuous Conversion, а настройках DMA указать Circular.

End Of Conversion Selection — позволяет выбрать, будет ли установлен флаг конца преобразования (EOC) после каждого преобразования или после завершения всей последовательности преобразования. Если опрашивать два (или более) канала, тогда в режиме End of single conversion прерывание будет вызываться после преобразования каждого канала, а в режиме End of sequence conversion только после преобразования обоих.

Overrun behaviour — данные будут сохранятся пока их не прочитают, либо перезаписываться.

Low Power Auto Wait — этот пункт мне не совсем понятен, то ли это связано с отключением АЦП для экономии энергии на время между преобразованиями, то ли что-то другое.



В разделе ADC_Regular_conversionMode добавился пункт External Trigger Conversion Edge, он служит для выбора фронта сигнала (HIGH, LOW или оба) от триггера.



Что касается кода, то тоже есть отличия, например калибровка вызывается с доп. параметром…

HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED); // обычные каналы
HAL_ADCEx_Calibration_Start(&hadc1, ADC_DIFFERENTIAL_ENDED); // диф. каналы


Так же есть отдельные функции для инжект. и рег. каналов — посмотрите в файле stm32f3xx_hal_adc_ex.c.




Это всё. На первое время должно хватить того, что описано в статье, но в любом случае нужно обязательно изучать Reference Manual.


Всем спасибо


AN3116


Форум (рус.)

Телеграм-чат STM32


  • 0
  • 14965
Поблагодарить автора




Telegram-чат istarik

Задать вопрос по статье
Telegram-канал istarik

Известит Вас о новых публикациях






Комментарии (0)

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