STM Урок 18. HAL. ADC. Regular Channel. DMA

 

 

 

Урок 18

HAL. ADC. Regular Channel. DMA

 

 

Проект создаём из ADC_REGULAR_INT, называем его ADC_REGULAR_DMA.


Запускаем Cube, добавляем в наш ADC1 ещё 3 канала – INT6, INT8, INT8. Заходим в Configuration. Прерывания от ADC можно пока отключить. Заходим по кнопке DMA, там в закладку DMA2 и включим там DMA следующим образом


 


image00


Применяем изменения. Заходим по кнопке ADC там настраиваем параметры следующим образом

 

image02

 

Ещё надо будет настроить очередь каналов  


image01

 

Генерируем и запускаем наш проект. Добавим также в него lcd.c. Соберем его.

Исправим наши переменные в массивы данных

 

/* Private variables ———————————————————*/

uint16_t ADC_Data[4];

/* USER CODE END PV */

 /* USER CODE BEGIN 1 */

        float u[4];

        char str[9];

  /* USER CODE END 1 */


Там же на 108 странице даташита HAL_User_Manual можно посмотреть, как пишется код для АЦП с использованием DMA. Исправим функцию старта, ну и исправим немного задержки для вывода вводных слов.


        HAL_Delay(500);

        LCD_Clear();

        LCD_SetPos(4, 0);

        LCD_SendChar('s');

        LCD_SetPos(8, 1);

        LCD_SendChar('t');

        LCD_SetPos(12, 2);

        LCD_SendChar('m');

        LCD_SetPos(16, 3);

        LCD_SendChar('3');

        LCD_SendChar('2');

        HAL_Delay(500);

        LCD_Clear();

        HAL_ADC_Start_DMA(&hadc1,(uint32_t*) &ADC_Data,4);

  /* USER CODE END 2 */


Также подправим использование переменных в бесконечном цикле


                u[0]=((float)ADC_Data[0])*3/4096;//занесём результат преобразований в переменную

                sprintf(str,"%.2fv",u[0]);//преобразуем результат в строку

                LCD_SetPos(0,3);//покажем результат на ЖКИ-дисплее


Уберём всё из функции внизу модуля.


void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc1)

{

}


Соберём, прошьём, проверим.

 

 

Теперь надо будет показать данные со всех источников напряжения

Для этого сделаем нашу переменную для данных неоптимизируемой


volatile uint16_t ADC_Data[4];


также мы в главную функцию добавим переменную для перебора элементов массивов


        char str[9];

        uint8_t i;

  /* USER CODE END 1 */


и внесем некоторые изменения в бесконечный цикл


while (1)

  {

                for(i=0;i<4;i++)

                {

                        u[i]=((float)ADC_Data[i])*3/4096;//занесём результат преобразований в переменную

                        sprintf(str,"%.2fv",u[i]);//преобразуем результат в строку

                        LCD_SetPos(0,i);//покажем результат на ЖКИ-дисплее

                        LCD_String(str);

                }


Теперь попробуем посчитать количество преобразований

Для этого добавим ещё одну неоптимизируемую переменную в главный модуль.


volatile uint16_t ADC_Data[4];

volatile uint32_t cnt1;

/* USER CODE END PV */


Проинициализируем её в главной фукнии


  MX_ADC1_Init();

  /* USER CODE BEGIN 2 */

        cnt1=0;

        LCD_ini();

 

Добавим увеличение этой переменной в функцию нашу перехвата окончаний преобразований

 

/* USER CODE BEGIN 4 */

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc1)

{

        cnt1++;

}

/* USER CODE END 4 */

 

И покажем счётчик на экране дисплея в бесконечном цикле (задержку можно уже убирать, она у нас теперь физически происходит).

 

                        LCD_String(str);

                }

                sprintf(str,"%10u",cnt1);//преобразуем результат в строку

                LCD_SetPos(6,0);//покажем результат на ЖКИ-дисплее

                LCD_String(str);

  /* USER CODE END WHILE */

 

Прошьём, зальём и посмотрим.

Ну и для полной ясности нам нужен будет ещё один счётчик милисекунд от начала запуска контроллера.

Для этого добавим ещё переменную

 

volatile uint32_t cnt1, cnt2;

/* USER CODE END PV */


Проинициализируем её также в главной функции


        cnt1=0;cnt2=0;


Также подключим её в модуль stm32f4xx_it.c


#include "stm32f4xx_it.h"

/* USER CODE BEGIN 0 */

extern volatile uint32_t cnt2;

/* USER CODE END 0 */

/* External variables ———————————————————*/


Проинкрементируем её в этом же модуле в функции системного таймера


void SysTick_Handler(void)

{

  /* USER CODE BEGIN SysTick_IRQn 0 */

        cnt2++;

  /* USER CODE END SysTick_IRQn 0 */


И также отобразим на дисплее


                        sprintf(str,"%10u",cnt1);//преобразуем результат в строку

                        LCD_SetPos(6,0);//покажем результат на ЖКИ-дисплее

                        LCD_String(str);

                        sprintf(str,"%10u",cnt2);

                        LCD_SetPos(6,1);//покажем результат на ЖКИ-дисплее

                        LCD_String(str);

  /* USER CODE END WHILE */


Прошьём, зальём и посмотрим.

Вообщем-то, если посчитать нам частоту, разделив эти 2 показателя, то мы получим где-то 650 кгц, что весьма внушительно.

Я пробовал операцию деления прописать в бесконечном цикле и отобразить результат на экране, но, скорей всего из-за нехватки ресурсов, получил висяк, поэтому будем делить в уме.

 

Предыдущий урок Программирование МК STM32 Следующий урок

 

Исходный код

 

Отладочную плату и дисплей можно приобрести здесь:

STM32F4-DISCOVERY

Дисплей LCD 20×4

 

 

Смотреть ВИДЕОУРОК

 

STM32 HAL. ADC. Regular Channel. DMA

 

5 комментариев на “STM Урок 18. HAL. ADC. Regular Channel. DMA
  1. ano60:

    Большое спасибо за урок. Все крайне доходчиво.

    Пытаюсь с помощью CubeMX настроить triple regular simultaneous mode only с DMA и не получается. Сведений по этому поводу в сети мало, именно такого примера не нашел. Не сможете ли подсказать?

  2. Сергей:

    Подскажите, пожалуйста, продолжаю знакомиться с STM32 и микроконтроллерами в целом, дошёл до DMA и появилось пара вопросов:

    1) Почему для АЦП всегда не используют DMA, если это такой классный инструмент? Зачем предусмотрены ещё 2-а режима Polling и Interrupt, для каких задач, если по сути всё можно сделать через DMA? Или не всё?

    2) Что приводит в действие DMA, т.е. чьи ресурсы он расходует? Ведь опрашивать (переносить значения в память), например, АЦП с такой частотой это же какие ресурсы нужны, за счёт чего это достигается, если мы не задействуем процессор?

     

    Например, если мне надо каждые 100 мс получать данные с 4-х каналов ацп, лучше это сделать через таймер (по прерыванию таймера получать данные, обрабатывать и сразу выводить на дисплей), прерывания или DMA? Что больше будет загружать сам МК? Ведь DMA будет опрашивать/переносить данные с большой частотой, что само по себе что-то нагрузит. А если, например, через таймер, то я настрою его на 100 мс и буду по прерываниям сразу обрабатывать значения, т.е. "дергать" буду лишь каждые 100мс, в отличии от DMA, который будет гораздо чаще опрашивать.

    • Вопросы интересные конечно. Но так просто на них ответить невозможно. DMA по определению не расходует ресурсы процессора. На то он и переводится «Прямой доступ к памяти». У этой периферии своя логика есть и операции копирования происходят минуя АЛУ процессора (раз уж Вы назвали контроллер процессором, то пусть будет процессор). Использование DMA не всегда также может происходить по ряду причин. Либо все каналы уже где-то используются, что реже, либо нужен полный контроль над всем процессом копирования, а не над частями, либо человек (программист) не знает, как его использовать.

      Приводит в действие ДМА именно процессор, но он лишь даёт команду с параметрами, куда, откуда и сколько копировать, а если ДМА не используется, то каждая единица информации попадает из источника сначала в регистр общего назначения, а затем в приёмник, что очень сильно нагружает процессор.

      По 3 вопросу, я бы использовал таймер. ДМА здесь нецелесообразен. ДМА лучше использовать, когда необходимо передавать данные по несколько байтов сразу, а у Вас будет скорей всего по 1-2 байта на канал (я не заню, какое разрешение у Вас будет в АЦП). Также зависит использование или неиспользование данной периферии (ДМА) в вашем случае от того, каким образом будет информация выводиться на дисплей — в символьном или в графическом виде. Если Вы пытаетесь нарисовать осциллограмму, то без ДМА Вы может и обойдётесь, но процесс пойдёт очень медленно. То есть всё зависит от ситуации. Принцип такой. Если данных сразу надо много куда-то копировать без преобразования, то ДМА надо использовать.

      • Сергей:

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

        Вроде не путал определения в своём сообщении. МК (контроллер) это всё устройство в целом, т.е. это и процессор, и таймера, и DMA и другое. Как я понял из Вашего сообщения, DMA в действие приводит процессор (даёт команду с параметрами, куда, откуда и сколько копировать), а можете подсказать в тактах, как часто процессору приходится давать подобные команды, т.е. интересует насколько процессор загружен во время максимальной работы DMA (максимальная частота). 

        По 3-му вопросу, планирую сделать вольтметр-амперметр, в котором будет ещё 2 датчика, т.е. буду использовать 1 АЦП и 4-е канала (регулярный). Полученные данные в виде числовых значений (uint16_t) вывожу на 2-х строчный дисплей. Почитав руководства и просмотрев Ваши уроки, понял следующее:

        1) Режим Polling не подходит, т.к. данный режим рекомендуют только при использовании одного канала.

        2) Режим Interruption подходит.

        3) Режим DMA тоже подходит. Планировал 2-а варианта, 1-й когда DMA постоянно записывает полученные значения из АЦП в память и я периодически к памяти обращаюсь забирая значения. 2-й варианта, я добавляю таймер, который каждые 100мс будет включать DMA и как в 1-м варианте забираю значения из переменной.

        Из-за того, что у меня пока нет понимания как именно работает DMA и насколько его целесообразно использовать в полном режиме, когда он с очень большой частотой заносит значения (ведь настроить его на работу, например, 100мс, как я понимаю нельзя), "родился" 2-й вариант для DMA, т.е. чтобы "дергать" его только каждые 100мс.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

*