Продолжим работу с АЦП через HAL-функции. Научимся запускать преобразование от таймера, считывать результат по прерыванию. Разработаем 4х канальный вольтметр среднего значения. Повторим проекты из предыдущих уроков, в которых АЦП работает в режиме сдвоенных преобразований.
Предыдущий урок Список уроков Следующий урок
В этой статье реализуются проекты уроков 28 и 29 с использованием HAL-библиотеки. Логику работы АЦП я подробно не объясняю. Если что-то непонятно, то лучше вернитесь к предыдущим урокам. Для понимания материала этого урока режимы и логику функционирования АЦП надо знать.
Четырех канальный вольтметр среднего значения.
Реализуем с использованием HAL-функций проект вольтметра из урока 28. Научимся работать с АЦП в фоновом режиме с использованием прерываний. А именно:
- запускать преобразование АЦП циклически от таймера;
- считывать результат в обработчике прерываний;
- вычислять среднее значение сигнала.
В этом проекте ожидание конверсии АЦП не будет блокировать выполнение основного цикла.
В результате должен получиться 4х канальный вольтметр среднего значения вполне применимый в практических задачах.
Повторю алгоритм работы программы.
- Аппаратный таймер циклически запускает преобразование 4х инжектированных каналов с периодом 100 мкс.
- По окончанию преобразования АЦП генерирует прерывание, в обработчике которого считываются и усредняются измеренные значения.
- Время усреднения результатов преобразований 250 мс или 2500 выборок по 0,1 мс.
- По истечению периода усреднения измеренные значения выводятся на LCD-дисплей и через UART передаются на компьютер.
За основу я взял проект из предыдущего урока Lesson31_1. В нем уже установлена библиотека для работы с LCD-дисплеем и конфигурирован UART.
Копируем проект или создаем новый. Открываем конфигуратор STM32CubeMX.
Настраиваем АЦП. Мы будем использовать только 4 инжектированных каналов.
- Регулярные каналы запрещаем.
- Число преобразований инжектированных каналов задаем 4.
- Разрешаем режим сканирования.
Запускать АЦП будем от таймера 2. Поэтому в качестве источника внешнего запуска выбираем событие таймера 2.
Осталось задать параметры каналов в последовательности сканирования.
Результат будем считывать по прерыванию АЦП. Поэтому разрешаем его в закладке NVIC Settings.
Настраиваем таймер 2. Мы научились делать это в уроке 16.
Наша задача конфигурировать таймер, чтобы он сбрасывался каждые 100 мкс.
В окне Timers -> TIM2 -> TIM2 Mode and Configuration разрешаем тактирование от внутреннего источника.
В окне Parameter Settings задаем:
- Предделитель 719. Коэффициент деления будет равен 720, период тактирующих импульсов на выходе предделителя – 10 мкс.
- Режим счетчика – Up. Будем считать в строну увеличения.
- В регистр перезагрузки установим значение 9. Коэффициент деления будет 10, а значит период тактирования счетчика 10 мкс * 10 = 100 мкс.
- В качестве выходного события таймера выбираем перезагрузку.
Прерывание по таймеру разрешать не надо. Мы запускаем АЦП по событию таймера.
Все. Создаем проект.
Считывание результата преобразование можно производить в обработчике прерывания, как это мы делали в уроке 28.
void ADC1_2_IRQHandler(void) {
}
Но в HAL-библиотеке для этого есть Callback-функции. Они вызываются по завершении какого-либо события. Нас интересует функция завершения преобразования HAL_ADCEx_InjectedConvCpltCallback.
Callback-функции объявляются в библиотеке HAL как “слабые ссылки”. При объявлении используется атрибут __weak. Это означает, что они могут быть переопределены пользователем в своем коде без объявления. Т.е. это своеобразные функции-заглушки.
Для нас важно, что достаточно создать в программе тело функции, не заботясь об остальном.
/* USER CODE BEGIN 0 */
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef* hadc)
{
}
Здесь и будем работать с результатом АЦП.
Callback-вызов общий для двух АЦП. Если используются оба модуля, то надо делать проверку, от которого из них пришел вызов функции.
void HAL_ADCEx_InjectedConvCpltCallback(ADC_HandleTypeDef* hadc)
{
if(hadc->Instance == ADC1)
{
// обработка АЦП1
}
}
Вот как выглядит блок чтения и усреднения результатов преобразований вольтметра.
- По каждому вызову функции HAL_ADCEx_InjectedConvCpltCallback результаты каналов накапливаются в массиве sumADC;
- Счетчик averageCounter считает число выборок. При достижении его равным 2500, перегружает накопленные числа в переменные среднего значения averageADC.
- Устанавливается признак flagReady, сообщающий о том, что появился новый результат измерений.
Остается в основном цикле периодически проверять флаг готовности flagReady и выводить измеренные величины на дисплей и в UART.
if( flagReady != 0 ) {
flagReady=0;
res= (float)averageADC[0] * VREF / 4096. / 2500.; // пересчет в напряжение
// вывод на LCD
. . . . . . . .
}
Еще надо добавить в исходный код калибровку АЦП
HAL_ADCEx_Calibration_Start(&hadc1);
А также запуск его в режиме с прерываниями
HAL_ADCEx_InjectedStart_IT(&hadc1);
и запуск таймера 2.
HAL_TIM_Base_Start(&htim2);
Компилируем программу, загружаем в микроконтроллер.
У меня не заработало. Таймер 2 генерирует событие по перезагрузке, но АЦП по нему не запускается.
Объяснить не могу. Опытным путем выяснил, что мешает, установленный в битах EXTSEL, источник запуска регулярной группы - программный запуск SWSTART.
Непонятно, как это влияет. Тем более, что регулярные каналы мы не используем, а внешний запуск для них запрещен.
Такое состояние битов EXTSEL устанавливает CubeMX при запрещении регулярных каналов.
Можно разрешить регулярные каналы и установить источник запуска для них любой, кроме программного. Можно изменить биты EXTSEL в регистре CR2.
Я исправил строку
// hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO;
в блоке конфигурации АЦП. При этом бит разрешения внешнего запуска для регулярных каналов остался в запрещающем состоянии.
После этого вольтметр заработал правильно.
Проект можно загрузить по ссылке
Работает с этим устройством и программа верхнего уровня из урока 28.
Проект 4х канального вольтметра с использованием АЦП в режиме сдвоенных преобразований.
Разработаем устройство подобное предыдущему проекту, только добавим условие, что пары каналов 0, 1 и 2, 3 должны считываться одновременно.
Модули АЦП будут работать в режиме сдвоенных преобразований, а точнее в режиме одновременных преобразований инжектированных каналов. Напомню, что это режим, при котором оба АЦП запускаются одновременно по сигналу запуска АЦП1.
Открываем STM32CubeMX.
Настраиваем систему тактирования, как в предыдущем проекте.
Настраиваем АЦП.
Для первого АЦП выбираем каналы IN0 и IN2.
Для второго каналы IN1 и IN3.
В обоих модулях запрещаем преобразования регулярных групп, для инжектированных задаем по два преобразования и настраиваем параметры каналов.
Теперь доступны все режимы АЦП1.
Выбираем Dual injected simultaneous mode.
Источник запуска – событие таймера 2.
Разрешаем прерывание АЦП 1.
Таймер 2 настраиваем аналогично предыдущему проекту.
Прерывание по таймеру разрешать не надо. Мы запускаем АЦП по событию таймера.
Не забудем про UART1.
Создаем проект.
В Atollic TrueStudio:
- Подключаем библиотеки.
/* USER CODE BEGIN Includes */
#include "LCD780.h"
#include "DelayDWT.h"
/* USER CODE END Includes */
- Правой кнопкой мыши нажимаем на папку LCD780 в проекте, Add/Remove Include Path -> OK.
- Ту же операцию повторяем с папкой DelayDWT.
- Правой кнопкой по Libraries, Properties -> C/C++ General -> Paths and Symbols -> Source Location -> Add Folder -> Apply.
- Добавим блок инициализации дисплея.
// инициализация дисплея
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_IOPAEN ;
initDelayDwt();
init_LCD(GPIOA, 1<<12, GPIOA, 1<<15, GPIOB, 1<<14, GPIOB, 1<<15, GPIOA, 1<<8, GPIOA, 1<<11, 16, 2, 1);
Теперь можно работать с функциями библиотеки LCD-дисплея.
Создаем Callback-функцию окончания преобразования инжектированных каналов. Она повторяет аналогичную функцию предыдущего проекта, за исключением строк чтения результата.
sumADC[0] += HAL_ADCEx_InjectedGetValue(&hadc1, 1);
sumADC[1] += HAL_ADCEx_InjectedGetValue(&hadc2, 1);
sumADC[2] += HAL_ADCEx_InjectedGetValue(&hadc1, 2);
sumADC[3] += HAL_ADCEx_InjectedGetValue(&hadc2, 2);
Результаты преобразований хранятся в регистрах разных модулях АЦП.
Остается в основном цикле периодически проверять флаг готовности flagReady и выводить измеренные величины на дисплей и в UART.
if( flagReady != 0 ) {
flagReady=0;
res= (float)averageADC[0] * VREF / 4096. / 2500.; // пересчет в напряжение
// вывод на LCD
. . . . . . . .
}
Добавим в исходный код калибровку АЦП
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADCEx_Calibration_Start(&hadc2);
А также запуск его в режиме с прерываниями
HAL_ADCEx_InjectedStart_IT(&hadc1);
и запуск таймера 2.
HAL_TIM_Base_Start(&htim2);
Подкорректируем биты EXTSEL регулярной группы.
// hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO;
Еще надо разрешить запуск АЦП2. HAL-функция конфигурации почему-то этого не делает.
ADC2->CR2 |= ADC_CR2_JEXTTRIG; // разрешение запуска АЦП2
Источник запуска задавать не нужно. Второй АЦП запускается от первого.
Компилируем, загружаем.
Вот мой проект.
Внешне работа устройства ничем не отличается от предыдущего проекта.
Проект простого осциллографа.
Повторим второй проект урока 29 – цифровой осциллограф. Напомню, что в нем мы создали цифровой осциллограф со следующими параметрами.
- Частота сканирования – 2 мГц.
- Длительность развертки 4 мс.
- Число выборок 8000.
- Запуск от компьютера.
Чтобы создать устройство со временем выборки 0,5 мкс (частота 2 мГц) необходимо использовать оба АЦП в режиме быстрых преобразований со смещением во времени. При этом АЦП должны работать с максимальным быстродействием. Все временные расчеты в уроке 29.
Проверку окончания преобразования и чтение результатов мы не сможем реализовать с помощью HAL-функций. Вызов функций достаточно длительная операция, а нам необходимо в цикле с периодом всего 1 мкс делать одну проверку и два чтения.
Поэтому настройку АЦП сделаем с помощью библиотеки HAL, а цикл сканирования оставим без изменений, т.е. с использованием обращений к регистрам CMSIS.
Я копировал проект Lesson29_2 и переименовал его в Lesson32_3.
Удалил блок инициализации АЦП.
Открыл проект конфигуратором CubeMX.
Настроил частоту тактирования АЦП на 14 мГц.
Теперь настраиваем АЦП.
Для первого модуля:
- выбираем канал IN0;
- разрешаем непрерывный режим преобразования;
- выбираем преобразование одного регулярного канала;
- задаем его параметры.
Для второго АЦП задаем абсолютно такую же конфигурацию.
Возвращаемся к АЦП 1 и задаем режим быстрых преобразований со смещением во времени.
Создаем проект.
Добавляем калибровку.
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADCEx_Calibration_Start(&hadc2);
И разрешение запуска АЦП 2.
ADC2->CR2 |= ADC_CR2_EXTTRIG; // разрешение запуска АЦП2
Строки запуска преобразования заменяем одной функцией.
HAL_ADC_Start(&hadc1);
Останавливаем преобразование функцией
HAL_ADC_Stop(&hadc1);
И все. Вот мой проект цифрового осциллографа.
Проверить можно программой верхнего уровня из урока 29.
Это синусоидальный сигнал частотой 1 кГц.
Это частота 500 Гц
Сигналы подавал с генератора через неэкранированные провода, без емкостного фильтра на входе АЦП. Поэтому есть шум.
В следующем уроке начнем изучать контроллер прямого доступа к памяти (DMA).
Уважаемый автор! Спасибо Вам за Ваши статьи.
Вопрос: Когда будет статья про генерацию ШИМ (если будет)?
Интересует генерация шим на любом пине, не только на выводах таймеров. Долго бодался с этим вопросом, но не хватает знаний. Стм-ки изучаю уже неделю, ранее кодил на аврках, там все значительно проще.
Спасибо еще раз:)
Здравствуйте!
Сегодня урок про ESP32 выложу. Начну DMA STM32. Следующая тема — ШИМ.
Эдуард, мы очень ждем! Иду по Вашему курсу, не забрасывайте пожалуйста!
Здравствуйте!
Не собираюсь. STM32 — приоритетная тема на сайте.
Эдуард, вы нас пугаете, вот уже полгода тишина. Вы в порядке?
Здравствуйте!
Посмотрел. Последний пост 8 июля. Вы несколько преувеличили.
Все равно, спасибо за беспокойство. Правда, приятно.
У меня все нормально. Увлекся разработкой системы умного растениеводства. В сентябре съездил в Абхазию, приехал — часть растений пострадала от засухи. Сейчас разрабатываю интеллектуальную систему полива. Очень красивая и простая система получается. Надеюсь, через пару недель вернуться к урокам STM32. Еще раз, спасибо за беспокойство.
Да ну, бросьте! Спасибо Вам за Ваш труд! Очень было непросто влиться в разработку на STM32 «с нуля», без опыта работы даже с Ардуино. Стало гораздо легче благодаря Вашим обстоятельным, подробным урокам. По сему очень хочется, чтобы Вы не останавливались!