В уроке расскажу об использовании HAL-библиотеки для настройки конфигурации и управления модулем АЦП. В качестве примеров реализуем проекты из предыдущих уроков с применением HAL-функций. Разберемся, как настраивать АЦП с помощью конфигуратора STM32CubeMX.
Предыдущий урок Список уроков Следующий урок
В предыдущих уроках мы подробно разобрали все режимы работы АЦП, его функциональные возможности, управляющие регистры. Если вся эта информация уложилась у вас в голове, хотя бы на уровне принципов управления и возможностей АЦП, то работа с ним через HAL-функции не представит ни малейших затруднений. Имена функций и аргументов сами подскажут, как их использовать. А настройка конфигурации с помощью STM32CubeMX сведется к выбору закладок меню.
Я все время беспокоюсь, что мои уроки об использовании HAL-библиотеки будут похожи на большинство уроков из Интернета, построенных по принципу – сюда задаем то-то, здесь пишем то-то и “Вуаля!”. Они и получаются похожими, потому что смысл операций с АЦП я объяснял в предыдущих уроках. А в этом показываю более простой способ их реализации. Но этот материал не может быть полноценно воспринят без предыдущих уроков.
Давайте реализуем все проекты уроков 27-30 с использованием библиотеки HAL. Увидите насколько это просто.
Еще. В справочнике библиотеки HAL появилось описание функций работы с АЦП. Я не буду в уроке описывать их форматы, смотрите в справочнике.
Настройка конфигурации АЦП с помощью STM32CubeMX.
Я подробно расскажу о задании конфигурации через CubeMX и одновременно настрою модуль АЦП на первый пример урока 27, а именно режим один канал, однократное преобразование.
Открываем STM32CubeMX.
Разрешаем работу генератора.
Мы будем использовать аналоговые входы IN0 … IN3. Выбираем их в поле Analog -> ADC1 -> Mode.
Переходим в окно настройки тактирования. Задаем основную частоту тактирования 72 мГц.
Предделитель АЦП (ADC Prescaler) устанавливаем на коэффициент деления 6. Частота тактирования АЦП равна 12 мГц.
Возвращаемся к окну конфигурации АЦП Configuration -> Parameter Settings.
В поле Mode выбираем Independent mode – независимый режим работы АЦП.
Поле Data Alignment задает режим выравнивания результата преобразования. Оставляем выравнивание по правому краю (Right alignment).
Следующие 3 поля разрешают или запрещают следующие режимы:
- Scan Conversion Mode - режим сканирования каналов;
- Continues Conversion Mode - режим непрерывного преобразования;
- Discontinuous Conversion Mode - прерывистый режим.
Напомню таблицу основных режимов.
Состояние битов | Режим | |
CONT | SCAN | |
0 | 0 | Один канал, однократное преобразование |
0 | 1 | Несколько каналов, однократное преобразование |
1 | 0 | Один канал, непрерывное преобразование |
1 | 1 | Несколько каналов, непрерывное преобразование |
Для однократного преобразования одного канала запрещаем все поля.
Ниже две корневые закладки, ADC_Regular_ConversionMode и ADC_Injected_ConversionMode определяют конфигурацию аналоговых входов для регулярных и инжектированных групп.
В этом проекте мы собираемся использовать только один регулярный канал.
Открываем закладку для регулярной группы.
- Enable Regular Conversions – разрешает преобразование регулярной группы.
- Number Of Conversion – количество каналов сканирования. Сейчас задаем 1.
- External Trigger Conversion Source – источник запуска преобразований. Выбираем программный запуск от бита SWSTART.
- Rank – последовательность преобразования каналов. У нас в параметре Number Of Conversion задан только один кагал. Поэтому только одно поле Rank.
Открываем его и устанавливаем:
- Channel – номер канала для этого преобразования.
- Sampling Time – время выборки сигнала.
В этом проекте мы не используем прерывания и DMA. Все, настройка конфигурации АЦП закончена.
Разрешим работу UART в асинхронном режиме со скоростью 9600 бит/сек.
Создаем проект. У меня Lesson31_1.
Думаю, вы уже оценили простоту конфигурирования АЦП таким образом. Мы не просматривали бесконечные битовые поля регистров, боясь установить что-то не так или забыть про что-то. Мы просто выбрали из того, что нам предложил конфигуратор CubeMX.
В принципе, можно настраивать конфигурацию АЦП с помощью CubeMX, а работать с АЦП через регистры CMSIS.
Давайте посмотрим и осознаем, что сделал конфигуратор. Это необходимо для понимания работы HAL-библиотеки. Возможно, нам в будущем потребуется изменять режимы АЦП динамически или захочется задать его конфигурацию без использования STM32CubeMX.
В файле stm32f1xx_hal_msp.c :
Разрешается тактирование АЦП1 и порта GPIOA.
__HAL_RCC_ADC1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
Конфигурируются выводы GPIOA 0 … 3 как аналоговые входы.
GPIO_InitStruct.Pin = GPIO_PIN_0|GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3;
GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
АЦП конфигурируется с помощью функции
HAL_ADC_Init (ADC_HandleTypeDef * hadc)
Функция имеет один аргумент – структуру параметров конфигурации типа ADC_HandleTypeDef. А одним из элементов этой структуры является другая структура типа ADC_InitTypeDef. Она определяет основные параметры АЦП. Для конфигурации АЦП необходимо задать значения полей этих структур и вызвать функцию инициализации HAL_ADC_Init.
Разберемся на примере нашего проекта. Все происходит в файле main.c.
Создается экземпляр структуры типа ADC_HandleTypeDef с именем hadc1.
ADC_HandleTypeDef hadc1;
Поле Instance определяет базовый адрес АЦП, т.е. какой именно модуль АЦП будет использоваться.
hadc1.Instance = ADC1;
Дальше заданы поля структуры Init. Это параметры АЦП, которые мы устанавливали в STM32CubeMX.
hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
И в завершение вызывается функция инициализации.
if (HAL_ADC_Init(&hadc1) != HAL_OK)
{
Error_Handler();
}
Для установки параметров каналов используется функция
HAL_ADC_ConfigChannel (ADC_HandleTypeDef * hadc, ADC_ChannelConfTypeDef * sConfig).
Создается экземпляр структуры типа ADC_ChannelConfTypeDef.
ADC_ChannelConfTypeDef sConfig = {0};
В нем выбирается канал, параметры которого в данный момент устанавливаются.
sConfig.Channel = ADC_CHANNEL_0;
Задаются параметры канала
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_28CYCLES_5;
И вызывается функция конфигурации канала.
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
Если нужно настроить несколько каналов, то функция вызывается для каждого канала.
Добавим еще калибровку АЦП.
/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
На этом настройка конфигурации АЦП закончена.
Все эти операции сделал за нас конфигуратор CubeMX, но ничего не мешает выполнить их нам самим или изменить параметры в ходе выполнения программы.
Еще нам потребуется определение опорного напряжения АЦП, которое будем использовать при вычислениях измеренного напряжения.
/* USER CODE BEGIN PD */
#define VREF 3.3065 // напряжение ИОН
Мы собираемся выводить информацию на LCD-дисплей. Добавим в проект библиотеку для работы с ним из урока 25.
Скопируем в проект папку Libraries из проекта предыдущего урока. В ней содержатся библиотеки для работы с LCD-дисплеем.
В 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-дисплея.
Мы создали шаблон проекта для урока. Дальше будем разрабатывать конкретные проекты.
Режим один канал, однократное преобразование.
Конфигурацию АЦП для режима однократного преобразования одного канала мы уже установили.
В основном цикле:
- запускаем АЦП;
- ждем окончания преобразования;
- считываем результат;
- пересчитываем его в напряжение;
- выводим на LCD-дисплей;
- передаем на компьютер через UART.
- Для разрешения работы АЦП и запуска преобразования используется функция HAL_ADC_Start.
- Функция HAL_ADC_PollForConversion ожидает окончание преобразования. Программа в ней "подвисает". Второй аргумент – время тайм-аута в мс.
- Чтение результата преобразования регулярных каналов происходит с помощью функции HAL_ADC_GetValue.
Остальные операции цикла аналогичны действиям из проектов предыдущих уроков.
Проект можно загрузить по ссылке.
Один канал, однократное преобразование инжектированного канала.
Сделаем тоже самое для инжектированного канала. В этом случае используются другие HAL-функции.
Я копировал проект с именем Lesson31_2 и открыл в нем файл Lesson31_1.ioc.
Изменяем конфигурацию АЦП через CubeMX.
Запрещаем работу регулярной группы, устанавливаем параметры для инжектированной.
- Число преобразований -1.
- Запуск – программный.
- Автозапуск – запрещен.
- Очередь каналов – 1.
- Канал – 0.
- Время выборки – 28,5 циклов.
- Смещение нуля – 0.
Создаем проект.
Добавился блок конфигурации инжектированных каналов.
sConfigInjected.InjectedChannel = ADC_CHANNEL_0;
sConfigInjected.InjectedRank = ADC_INJECTED_RANK_1;
sConfigInjected.InjectedNbrOfConversion = 1;
sConfigInjected.InjectedSamplingTime = ADC_SAMPLETIME_28CYCLES_5;
sConfigInjected.ExternalTrigInjecConv = ADC_INJECTED_SOFTWARE_START;
sConfigInjected.AutoInjectedConv = DISABLE;
sConfigInjected.InjectedDiscontinuousConvMode = DISABLE;
sConfigInjected.InjectedOffset = 0;
if (HAL_ADCEx_InjectedConfigChannel(&hadc1, &sConfigInjected) != HAL_OK)
{
Error_Handler();
}
Для определения параметров используется структура sConfigInjected типа ADC_InjectionConfTypeDef.
Параметры расписаны в справочнике, хотя и без него все понятно.
Сама установка производится функцией HAL_ADCEx_InjectedConfigChannel.
Для запуска, ожидания завершения преобразования и чтения результата инжектированной группы используются другие функции. Это:
- HAL_ADCEx_InjectedStart – запуск;
- HAL_ADCEx_InjectedPollForConversion – ожидание;
- HAL_ADCEx_InjectedGetValue – чтение результата.
У функции HAL_ADCEx_InjectedGetValue добавился еще один аргумент – номер инжектированного канала.
Основной цикл программы выглядит так.
Полностью проект можно загрузить по ссылке.
В обоих последних примерах функции ожидания завершения преобразования блокируют выполнение программы.
Один канал, непрерывное преобразование.
Вернемся к проекту Lesson31_1.
Единственное изменение, которое мы сделаем – разрешим непрерывное преобразование.
Это можно сделать в строке
hadc1.Init.ContinuousConvMode = ENABLE;
или с помощью CubeMX.
Теперь достаточно запустить преобразование один раз. АЦП будет автоматически конвертировать значение сигнала, и загружать результат в регистр DR. Остается только в цикле считывать его, вычислять значение напряжения и выводить на дисплей.
Здесь окончательный проект.
В отличие от предыдущих двух проектов, в этом программа не блокируется для ожидании результата конверсии.
Непрерывное преобразование одного регулярного и одного инжектированного каналов.
Можно запустить непрерывное преобразование для регулярного и инжектированного канала одновременно. Тогда получим два регистра с постоянно обновляемыми значениями сигналов на двух аналоговых входах.
Напомню, что у инжектированных каналов нет режима непрерывных преобразований, но есть возможность установить режим их автоматического запуска после конверсии регулярной группы.
Открываем CubeMx.
- Разрешаем работу и регулярной, и инжектированной групп на преобразование одного канала.
- Для регулярной группы выбираем канал 0, для инжектированной - канал 1.
- Разрешаем режим непрерывных преобразований (Continues Converson Mode).
- Разрешаем режим автозапуска инжектированной группы (Injected Conversion Mode).
В программе один раз запускаем преобразование, и после этого у нас есть два регистра, в которых хранятся всегда “свежие” значения сигналов на двух аналоговых каналах.
Вот, как выглядит основной цикл.
Здесь можно загрузить весь проект.
Чтение результатов также происходит в неблокирующем режиме.
Несколько каналов, однократное преобразование.
В этом примере по программному запуску будут происходить 5 преобразований: для одного регулярного канала и четырех инжектированных.
Настраиваем АЦП с помощью STM32CubeMX.
- Разрешаем режим сканирования и настраиваем регулярную группу на 1 преобразование канала IN4.
- Непрерывный режим запрещаем.
Для инжектированной группы устанавливаем 4 преобразования, разрешаем автозапуск и заполняем параметры очереди сканирования каналы 0…3.
В основном цикле:
- Запускаем преобразование регулярной группы.
HAL_ADC_Start(&hadc1); // запуск преобразования
- Ожиданием окончания сканирования всех каналов.
HAL_ADCEx_InjectedPollForConversion(&hadc1, 10); // ожидание окончания преобразования
- Считываем результаты преобразований, вычисляем напряжение и выводим на дисплей.
Вот этот проект.
Работа с АЦП происходит в блокирующем режиме. При ожидании окончания преобразований программа зависает.
Несколько каналов, непрерывное преобразование.
Модифицируем предыдущий проект таким образом, что преобразования будут происходить в непрерывном режиме. В результате в пяти регистрах будут всегда храниться “свежие” значения сигналов на входах пяти каналов IN0 … IN4.
В настройках АЦП одно изменение - разрешим непрерывный режим.
В программе:
- Размещаем функцию запуска до основного цикла.
- Функцию ожидания окончания преобразования убираем.
- В цикле считываем значения измерений из регистров результатов и выводим данные на дисплей.
Полный проект можно загрузить по ссылке.
Еще раз подчеркну, что простота работы с АЦП через HAL-функции основана на понимании всех режимов и управляющих параметров АЦП, которым были посвящены предыдущие уроки.
В следующем уроке продолжим работу с АЦП через функции библиотеки HAL.
Нашёл у Вас в 21-м уроке функцию HAL_TIM_PeriodElapsedCallback(). Подскажите, а в чём преимущества перед обычным обработчиком прерывания? И как определять какой таймер вызвал данную колбэк функцию, сравнивать в данной функции указатели?
Здравствуйте!
Вызов функции инициируется прерыванием. Преимущество — системный подход к программированию.
Определить таймер можно так:
if(htim1->Instance == TIM1)
{
// таймер 1
}
Просто устанавливайте флаг(бит и обрабатывайте его в любом месте). Учитывайте и понимайте, что автор старается объяснить несколько подходов к решению задачи. В чем ценность его лекций. Всё не нужное можно пропустить, а потом вернуться.
Если Вы собираете просто любительский конструктор Вам этого достаточно.
Здравствуйте. Подскажите, пожалуйста, в каких случаях срабатывают прерывания TIM2_IRQHandler? Т.е. если я использую данный таймер с прерываниями (как у Вас в статье), необходимо ли мне производить доп. проверку в функции TIM2_IRQHandler или у таймеров только по одному событию формируется прерывание? Например, как в АЦП, есть общие прерывания (HAL_ADC_IRQHandler), а есть отдельная функция HAL_ADC_ConvCpltCallback(), которую более предпочтительнее использовать. Есть ли аналогично в таймерах? Спасибо.
Здравствуйте!
TIM2_IRQHandler срабатывает по любому разрешенному событию таймера TIM2.
TIM1 — расширенный таймер. У него на каждое событие свой вектор прерывания.
.word TIM1_BRK_IRQHandler
.word TIM1_UP_IRQHandler
.word TIM1_TRG_COM_IRQHandler
.word TIM1_CC_IRQHandler
Для таймера TIM2 один общий вектор.
.word TIM2_IRQHandler
Определить по какому событию было вызвано прерывание можно с помощью регистра состояния TIM2.
CMSIS
Здравствуйте, попробовал работу с АЦП. Не совсем понял смысл
HAL_ADC_Start(&hadc1); // запуск преобразования
while (1)
{
res= (float)HAL_ADC_GetValue(&hadc1) * VREF / 4096. ; // чтение результата и пересчет в напряжение
// вывод на LCD
// передача на компьютер
HAL_Delay(300);
}
В таком случае он просто один раз измеряет и все и на дальнейшие изменения напряжения не реагирует или может где то еще нужно что то дописать? По точности. Хочу сказать что все таки лучше усреднять данные. Так точность больше будет. То есть лучше делать так.
while (1)
{
/* USER CODE END WHILE */
res = 0;
res_sr = 0;
for(i = 0; i < 100; i++)
{
HAL_ADC_Start(&hadc1); // запуск преобразования
HAL_ADC_PollForConversion(&hadc1, 10); // ожидание окончания преобразования
res= (float)HAL_ADC_GetValue(&hadc1) * VREF / 4096; //
res_sr = res_sr + res;
}
res = res_sr/100;
sprintf(str,"%f \r\n", res);
CDC_Transmit_FS((uint8_t*) str,strlen(str));
//sprintf((char *)str, "%d.%03d V", (uint16_t)res, ((uint16_t)((res — (uint16_t)res)*1000.)) );
HAL_Delay(300);
/* USER CODE BEGIN 3 */
100 раз померили, потом усреднили. Еще хотел спросить у Атмеги советуют вешать индуктивность и конденсатор на AVCC, для увеличения точности. В STM32 я так понял никаких дополнительных схемотехнических решений не требуется. Или что то нужно?
Здравствуйте!
В зависимости от режима. При однократном преобразовании выполнит только одно измерение. В режиме непрерывного преобразования будет работать постоянно.
По второму вопросу. Все зависит от источника питания, реальной схемы контроллера, подключенных к нему нагрузок. Цепи, про которые вы спрашиваете, это фильтр, который снижает на входе питания АЦП различные помехи, выбросы напряжения. Если в системе существуют выбросы по питанию, то лучше использовать фильтры на аналоговом питании микроконтроллера и входе внешнего источника опорного напряжения.
Добрый день! Эдуард а такой вопрос не подскажите, вот у вас есть библиотека #include «LCD780.h», для работы с дисплеями на контроллере HD4470. Дисплеи ЖКИ и OLED питаются от 5 В. А STM32 питание 3.3 В. Можно в схеме использовать питание дисплея 3.3 Вольта, чтобы не усложнять? не делать 2 питания. Будут они работать?
Здравствуйте!
Да, конечно. Но не все LCD-дисплеи допускают питание 3 В.
При подключении LCD к STM32 используются только входные сигналы. Поэтому на входы LCD с питанием 5 В поступают сигналы с уровнем 3,3 В, что вполне допустимо. Но ничего не мешает использовать LCD с питанием 3 В.
Здравствуйте, Эдуард. Я насчет запуска постоянного преобразования. HAL_ADC_Start(&hadc1); // запуск преобразования. Включаю режим как у вас в примере ContinuousConvMode а он все равно только один раз обмеряет и все. STM32F415RG у меня, может есть какие нибудь особенности у него? Не сталкивались с такой проблемой?
Привет Эдуард.
Я пытался вывести сигнал на монитор Nextion с АЦП Arduino
в числовой компонент (например n0 в Nextion)
Однако частота этого компонента постоянно прыгает от 1-2
до 10-20 бит ,хотя на вход Arduino c датчика вакуума поступает очень стабильное напряжение практически
без флуктуаций, с уровнем 0.993 в ,на осциллографе флуктуации от 4 до 8 мв,если я правильно прочитал показания.то есть максимум 0,8 процента.
Флуктуации частоты компонента nполучается более 11 процентов
Я нашел описание похожей схемы
для Nextion и STM32 в интернете,но для STM32F4xx.
Как это использовать с моей STM32F103 или попробовать
сделать АЦП по материалу этого урока, но я не знаю как совместить этот проект с Nextion.
Может если заменить Arduino на
STM32 ,то это поможет.
Как по Вашему мнению поможет
это решить проблему?
С уважением
Юрий
PS скрины выслал на Ваш адрес электронной почты