В этой части разберем работу аналого-цифрового преобразователя, встроенного в микроконтроллеры серии STM32F103. Я считаю это периферийное устройство одним из самых полезных во всем микроконтроллере, ведь именно оно позволяет сухому цифровому коду программ общаться с этим удивительным живым аналоговым миром, давая нам данные о параметрах окружающей среды, физических величинах, ориентации в пространстве или выполняет еще бесконечное множество разнообразных задач.
Перед прочтением рекомендуется ознакомиться с первой частью этой серии статей. Она находиться тут.
Итак, насколько мне известно во всех микроконтроллерах STM32 присутствует по крайней мере один аналого-цифровой преобразователь. В большинстве случаев это 12-битный АЦП последовательного приближения, хотя в некоторых специализированных моделях МК (STM32F373) уже стали ставить 16-битные ΔΣ АЦП. Так или иначе в данной статье речь пойдет именно о АЦП последовательного приближения (англ. SAR) на примере МК STM32F103C8T6 и платы Blue Pill, китайскую версию такой платы я покупал тут.
Применяемая схема АЦП от ST называется схемой последовательного приближения на переключаемых конденсаторах.
Если вкратце, то процесс разбивается на два основных этапа работы - это процесс работы УВХ (англ. SHA) и процесс преобразования (англ. ADC Conversion).
Устройство выборки и хранения сокр. УВХ (англ. Sample and Hold сокр. SHA) — схема, запоминающая напряжение на входе в определённый момент времени.
На этапе выборки-хранения конденсаторы заряжаются до входного напряжения (VIN), затем это напряжение отключается от входа АЦП.
На следующем этапе путем коммутации конденсаторов и сравнения их напряжения с долями от опорного напряжения (VREF) определяется значение напряжения, поступившего на вход.
Для STM32F103C8 напряжение VREF равно напряжению питания 3,3В, но для МК с большим количеством выводов может также иметься отдельный вход VREF+ для подачи внешнего опорного напряжения.
В зависимости от того какое состояние получил выход компаратора логика алгоритма последовательного приближения выберет соответствующую ветку цифрового кода и следующее состояние для коммутирующих ключей.
Так произойдет столько раз сколько разрядов имеет конкретный АЦП, в нашем случае это будет 12 шагов.
Подробнее про работу SAR АЦП в STM32 см. AN2834.
Время полной работы АЦП до получения результата складывается из времени выборки (англ. Sampling time) и времени преобразования (англ. ADC Conversion time).
Tconv = Sampling time + ADC Conversion time
Время преобразования для 12 битных АЦП ST это всегда 12.5 циклов тактовой частоты АЦП.
Стоит сразу отметить, что тактовая частота АЦП устанавливается путем деления частоты тактирования ядра на делитель ADC Prescaler, который выбирается из /2/4/6/8 так, чтобы получившаяся частота АЦП не превышала 14 МГц. Например, для частоты ядра 72МГц наименьший возможный делитель, удовлетворяющий условию, будет 6 и соответственно получиться ADC CLK = 12МГц.
Получение максимальной частоты тактирования АЦП возможно при частоте ядра 56МГц, тогда, при выборе делителя на 4, частота ADC CLK = 14МГц.
На практике я обычно предпочитаю поступиться максимальной частотой АЦП в пользу максимальной частоты ядра.
Следующей составляющей полного времени работы АЦП является время выборки, которое определяет время заряда конденсатора Сadc (см. типовую схему подключения). Эта емкость составляет всего 8пФ (max), но при достаточно высоком сопротивлении источника Rain и/или при использовании RC фильтра перед входом АЦП, понадобиться время, чтобы зарядить эту емкость до уровня Vain . Рекомендации по выбору этого времени исходя из сопротивления источника сигнала сведены в таблицу.
Это время выставляется при конфигурировании АЦП в разделе Sampling Time.
Таким образом, при установке минимального времени Sampling time = 1,5 цикла, суммарное время АЦП будет составлять 14 циклов тактовой, что соответствует 1,17μS для частоты АЦП 12МГц и 1,00μS для частоты 14МГц.
В практических применениях, если необходимо уменьшить время аналого-цифрового преобразования, то внутреннее сопротивление источника сигнала можно свести практически к нулю поставив перед входом АЦП буферный усилитель на ОУ.
У микроконтроллеров STM32F103C8T на борту имеется 2 аналого-цифровых преобразователя, они могут работать как в независимом режиме (англ. Independent mode), так и в совместном режиме (англ. Dual mode), при этом, совместный режим предполагает внутри себя еще целый ряд вариантов. Например, режимы с чередованием запуска АЦП на 7 тактов, что дает нам возможность, используя одновременно два АЦП на один входной канал, получить частоту выборок до 2МГц.
Еще у этих МК целых 12 мультиплицированных каналов аналого-цифрового преобразования, 10 из них с возможностью вывода на ножки микроконтроллера (PA0-PA7 и PB0, PB1) и 2 внутренних канала, которые в общем-то ничем не отличаются от остальных кроме того, что они уже подключены к внутреннему датчику температуры и к напряжению опорного источника.
Запуск АЦП может производиться по следующим триггерам:
— Программный триггер (англ. Software Trigger), когда аналого-цифровое преобразование начинается командой из кода программы.
— Аппаратный внутренний триггер (англ. Hardware Trigger), когда аналого-цифровое преобразование начинается по некоторому аппаратному событию, например от таймера.
Эти варианты выбираются в меню External Trigger Conversion Source:
— Аппаратный внешний триггер (англ. External Trigger — EXTI), когда аналого-цифровое преобразование начинается по внешнему событию такому как изменение уровня на ножках РB11 (ADC_EXTI11) и РС15 (ADC_EXTI15), для активации нужно выбрать группу каналов в выпадающем меню EXTI Conversion Trigger:
Режимы работы АЦП:
Независимый режим работы (Independent mode)
— Одноканальное однократное преобразование (англ. single-channel, single conversion) — АЦП запускается по программному или аппаратному триггеру, делает одно преобразование, сохраняет результат в регистр данных и останавливается до следующего запуска.
Пример HAL кода однократного одноканального преобразования:
/* USER CODE BEGIN PV */
uint16_t adc; // Переменная для хранения данных с АЦП
/* USER CODE END PV */
...
HAL_ADCEx_Calibration_Start(&hadc1); // Калибровка АЦП
while (1)
{
HAL_ADC_Start(&hadc1); // Запуск преобразования
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY); // Ожидаем окончания преобразования
adc = HAL_ADC_GetValue(&hadc1); // Сохраняем значение АЦП в переменную adc
}
Это важно: любой код, который пишется в main.c или других сгенерированных файлах проекта, должен располагаться между комментариями USER CODE BEGIN и USER CODE END, иначе, когда вы захотите изменить что-нибудь в файле конфигурации .ioc и пересобрать проект, ваш код с высокой вероятностью будет утерян.
/* USER CODE BEGIN # */
Ваш код должен быть здесь!
/* USER CODE END # */
Небольшая ремарка: особенностью АЦП STM32 является система объединения каналов в группы. Существует 2 типа групп: регулярная (англ. Regular Group) и инжектированная группа (Injected Group).
В регулярной группе может быть до 16 каналов, опрашиваемых в любом порядке и с возможностью повторения одного и того же канала для нескольких индексов группы. Эти индексы обозначаются как «Rank».
Важно: данные с любого канала регулярной группы перезаписывают один и тот же регистр данных АЦП, поэтому их нужно своевременно оттуда забирать.
Инжектированная группа может включать до 4-х каналов АЦП.
Ее особенность в том, что она прерывает преобразование регулярной группы и сразу начинает преобразование для своих каналов. По завершению инжектированной группы, регулярное преобразование продолжается с того канала, на котором произошла инжекция. Для каждого из четырех индексов инжектированной группы есть свой отдельный регистр хранения результата. Это значит, что для этих 4-х каналов можно читать данные из своих индивидуальных регистров данных АЦП. Если создана только инжектированная группа, то ее работа не будет отличаться от регулярной за исключением описанного выше.
Подробнее про инжектированную группу будет в разделе «преобразование инжектированных каналов».
Еще один важный момент, что любая группа может состоять всего из одного канала, и при конфигурации это дает возможность выбирать событие, по которому будет происходить старт преобразования для группы (англ. External Trigger Conversion Source) и время работы УВХ (англ. Sampling Time) для каждого члена группы.
Например, конфигурация, показанная ниже, будет работать точно так же как и предыдущая для одноканального однократного преобразования, но включает в себя группу из одного канала.
Еще один пример кода для одноканального однократного режима, но теперь с использованием прерывания по окончанию преобразования. Для этого нужно установить галочку во вкладке NVIC Settings:
А в разделе параметров кодогенератора появится команда создать соответствующий обработчик прерывания:
В файле main.c код будет выглядеть так:
/* USER CODE BEGIN PV */
uint16_t adc; // Переменная для хранения данных с АЦП
/* USER CODE END PV */
...
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) // CallBack функция из прерывания
{
if(hadc->Instance == ADC1) // Проверяем что прерывание пришло от АЦП1
{
adc = HAL_ADC_GetValue(&hadc1); // Читаем данное из регистра в нашу переменную
}
}
/* USER CODE END 0 */
...
int main(void)
{
...
/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1); // Калибровка АЦП1
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_ADC_Start_IT(&hadc1); // Запускаем АЦП в режиме генерации прерываний
HAL_Delay(500); //Задержка что бы прерывания не создавались слишком часто
}
В файле stm32f1xx_it.c редактируем обработчик прерываний:
/**
* @brief This function handles ADC1 and ADC2 global interrupts.
*/
void ADC1_2_IRQHandler(void)
{
/* USER CODE BEGIN ADC1_2_IRQn 0 */
HAL_ADC_ConvCpltCallback(&hadc1); // Вызов CallBack функции из main.c
return; // Возврат из прерывания
/* USER CODE END ADC1_2_IRQn 0 */
HAL_ADC_IRQHandler(&hadc1);
/* USER CODE BEGIN ADC1_2_IRQn 1 */
/* USER CODE END ADC1_2_IRQn 1 */
}
— Многоканальное (сканирующее) однократное преобразование (англ. multi-channel/scan mode, single conversion) — АЦП делает одно последовательное преобразование (Scan Conversion) до 16 каналов в любом заранее определенном порядке, при этом, для каждого канала можно выбрать свое время работы устройства выборки и хранения. Очередность опроса каналов устанавливается путем их связи с индексами (Rank) от 1 до 16, при этом время работы УВХ устанавливается в Sampling Time для каждого Rank отдельно.
В данном случае уже не подойдет прямой способ считывания результата из регистра АЦП, как это делалось при опросе одного канала, т.к. в этот регистр теперь будут последовательно сваливаться результаты измерений всех каналов в своей очередности, т.е. регистр будет постоянно переписываться. Здесь необходимо перехватить тот момент, когда данное по первому каналу уже готово, а следующее преобразование только началось и по этому событию нужно будет забрать результат.
Сделать это можно двумя способами, создав прерывание по готовности данных АЦП или воспользовавшись прямым доступом к памяти DMA (Direct Memory Access). Приведу пример с использованием DMA, это когда по событию готовности очередного данного происходит его чтение и дальнейшая перезапись в объявленную заранее пользовательскую переменную, в конкретном случае в массив из 3-х переменных для опроса 3-х каналов. Главным преимуществом данного способа является то, что процессор в данном случае вообще не выполняет действий по переносу, всю работу за него делает блок DMA.
Пример HAL кода многоканального однократного преобразования 3-х каналов с сохранением данных по DMA:
/* USER CODE BEGIN PV */
uint16_t adc[3]; // Массив переменных для хранения значений 3-х каналов АЦП
/* USER CODE END PV */
...
HAL_ADCEx_Calibration_Start(&hadc1); // Калибровка АЦП
while (1)
{
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&adc, 3); // Запуск преобразования с передачей по DMA
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY); // Ожидаем окончания преобразования
}
Важная ремарка: для микроконтроллеров STM32F1 использование прерываний для сохранения результата при многоканальном преобразовании можно корректно реализовать только для инжектированной группы каналов. Это связано с тем, что глобальное прерывание происходит по окончанию аналого-цифрового преобразования всей группы, а у регулярной группы только один регистр для хранения результата. Из-за этого результаты каналов просто перепишут регистр ADC_DR и останется только последняя оцифровка.
Для более продвинутых МК (F3, F4 и т.д.) существуют функции определения события конца преобразования (англ. End Of Conversion Selection) и с помощью нее можно настроить корректный режим установки флага конца преобразования и соответственно генерацию прерываний для каждого канала в группе.
Еще для многоканальных режимов можно применить функцию прерывистого преобразование каналов (англ. Discontinuous Conversion) это дает возможность разбить выбранную группу на части. То есть, по одной команде на запуск, АЦП будет оцифровывать только часть каналов из группы.
Например, разбитие регулярной группы, состоящей из 5-ти каналов на 3 части будет конфигурироваться следующим образом:
При этом сама последовательность будет как на следующей блок-схеме:
— Одноканальное непрерывное преобразование (англ. Continuous Conversion), в этом режиме АЦП по окончанию преобразования будет сам себя перезапускать. Это позволяет не тратить ресурсы МК на постоянный запуск АЦП, а текущее значение забирать из регистра данных в любой удобный момент.
Пример кода для одноканального непрерывного преобразования:
/* USER CODE BEGIN PV */
uint16_t adc; // Переменная для хранения данных с АЦП
/* USER CODE END PV */
...
HAL_ADCEx_Calibration_Start(&hadc1); // Калибровка АЦП
HAL_ADC_Start(&hadc1); // Запуск преобразования
while (1)
{
adc = HAL_ADC_GetValue(&hadc1); // Сохраняем значение АЦП в переменную adc
HAL_Delay(10);
}
— Многоканальное (сканирующее) непрерывное преобразование (англ. multi-channel, scan continuous conversion mode)
Позволяет оцифровывать регулярную или инжектированную группу, опрашивая каналы один за другим и после окончания перезапускаться.
Также как и предыдущий многоканальный метод, этот способ будет хорошо работать вместе с DMA с той лишь разницей, что теперь нам нужно будет запустить преобразование только один раз в сетапе программы.
/* USER CODE BEGIN PV */
uint16_t adc[3]; // Массив переменных для хранения значений АЦП
/* USER CODE END PV */
...
HAL_ADCEx_Calibration_Start(&hadc1); // Калибровка АЦП
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&adc, 3); // Запуск преобразования с передачей по DMA
while (1)
{
// Цикл пуст, т.к. все выполняется связкой аппаратных модулей ADC+DMA
}
— Режим преобразования инжектированных каналов (англ. Injected conversion mode)
Инжектированная группа может включать от 1 до 4 каналов, при этом, запуск инжектированной группы приведет к остановке преобразования регулярной группы на время оцифровки инжектированных каналов.
Событием запуска может быть как программный, так и аппаратный источник.
Этот режим можно использовать для синхронизации опроса каналов с каким-либо внешним событием.
Например, в системах частотного управления двигателями переключение транзисторов создает шум не позволяющий точно измерять ток и напряжение в этот момент. Используя таймер для создания временной задержки от сигнала на открытие/закрытие транзисторов, можно организовать стабильное получение критически важных для системы данных в моменты времени с наименьшим влиянием электромагнитной помехи.
При этом менее критичные к шумам данные могут продолжать приниматься с других каналов АЦП в составе регулярной группы.
Представленный ниже код реализует опрос регулярной группы из 3-х каналов IN0, IN1, IN2 с передачей данных по DMA, а так же инжектированной группы IN4, IN5, с сохранением данных по прерыванию о готовности АЦП.
/* USER CODE BEGIN PV */
uint16_t adc_reg[3]; // Массив переменных для регулярной группы
uint16_t adc_inj[2]; // Массив переменных для инжектированной группы
/* USER CODE END PV */
...
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* Call Back функция обработки прерывания по готовности данных инжектированной группы */
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef* hadc1)
{
HAL_ADC_Stop_DMA(hadc1); // Останавливаем работу DMA
adc_inj[0] = HAL_ADCEx_InjectedGetValue(hadc1, ADC_INJECTED_RANK_1); // Сохраняем данные
adc_inj[1] =HAL_ADCEx_InjectedGetValue(hadc1,ADC_INJECTED_RANK_2);
HAL_ADC_Start_DMA(hadc1, (uint32_t*)&adc_reg, 3);
// Возобновляем работу DMA
}
/* USER CODE END 0 */
...
HAL_ADCEx_Calibration_Start(&hadc1);
/* Запуск преобразования регулярных каналов с передачей по DMA */
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&adc_reg, 3);
while (1)
{
/* Запуск преобразования инжектированных каналов с включением прерывания */
HAL_ADCEx_InjectedStart_IT(&hadc1);
HAL_Delay(100);
}
В файле обработчиков прерываний stm32f1xx_it.c создаем вызов Call Back функции и далее выход из прерывания с помощью return.
void ADC1_2_IRQHandler(void)
{
/* USER CODE BEGIN ADC1_2_IRQn 0 */
/* Вызов Call Back функции по готовности инжектированной группы
HAL_ADCEx_InjectedConvCpltCallback(&hadc1);
return; // Возврат из прерывания
/* USER CODE END ADC1_2_IRQn 0 */
HAL_ADC_IRQHandler(&hadc1);
/* USER CODE BEGIN ADC1_2_IRQn 1 */
/* USER CODE END ADC1_2_IRQn 1 */
}
Еще одной особенностью инжектированных каналов является возможность задать смещение для оцифрованных данных. По сути число указанное в графе Injected Offset (см. ниже) будет вычитаться из результата преобразования.
Это нужно для того, чтобы компенсировать смещение входных аналоговых цепей, буферных и/или усилительных. Обычно операционные усилители даже имеющие характеристику Rail-to-Rail Output, все равно при однополярном питании не могут опускать свой выход ниже нескольких десятков милливольт. И эта функция дает возможность на аппаратном уровне вычесть это постоянное смещение до того как данные попадают в регистр результата.
На этом я закончу описание независимого режима работы АЦП, конечно эта статья не описывает всех тонкостей и трюков при работе с STM32. Тем более, если рассматривать более современные семейства, где количество возможных настроек и их комбинаций растет в геометрической прогрессии. Тем не менее, я думаю, это хорошая площадка для старта, а полное описание режимов работы АЦП содержится в документе AN3116.
В планах на будущее стоит расписать варианты работы 2-х АЦП в совместном режиме и возможно посмотреть еще некоторые хитрые и неочевидные моменты при работе с этой периферией.
Рекомендованная литература:
1 RM0008 STM32F1xx Reference Manual
2 STM32F103x8/B Datasheet
3 AN2834 How to get the best ADC accuracy in STM32 microcontrollers
4 AN3116 STM32™’s ADC modes and their applications
5 AN2668 Improving STM32F1 Series, STM32F3 Series and STM32Lx Series ADC resolution by oversampling
6 Как работают аналого-цифровые преобразователи и что можно узнать из спецификации на АЦП? — "Компоненты и технологии", № 3'2005
7 АЦП в STM32F1 и режимы работы (статья)
8 ADC (перевод из книги Mastering STM32)
9 ADC HAL stm32 (статья)
10 UM1850 Description of STM32F1 HAL and low-layer drivers
С наилучшими пожеланиями
Ваш, TDA
24.01.2021