Прямой доступ к памяти

Прямой доступ к памяти

На этом уроке мы рассмотрим еще один способ получения результатов аналого-цифрового преобразования – прямой доступ к памяти.
 

 
Прямой доступ к памяти (англ. Direct Memory Access, DMA) представляет собой передачу данных из одной области памяти в другую. При этом процессорное ядро в такой передаче данных не участвует, а последовательное перемещение по адресам блока памяти производится автоматически. Прямой доступ к памяти может также использоваться для передачи данных между областью памяти и каким-нибудь периферийным устройством, например, аналого-цифровым преобразователем или интерфейсом связи.

При этом задачей процессора является инициализация прямого доступа к памяти. Для этого необходимо

  • назначить источник и приемник,
  • указать величину пересылаемой единицы данных (количество байт)
  • указать количество таких единиц данных.

 
Контроллер прямого доступа сообщит об окончании передачи данных, например с помощью прерывания, и далее можно проанализировать данные в теле программы.
Микроконтроллер STM32F303VCT6 содержит 12 каналов DMA, распределенных на 2 модуля, предназначенных как для копирования данных из одного участка памяти в другой, так и для передачи данных между областью памяти и периферийным блоком, в качестве которого может выступать аналого-цифровой преобразователь, цифро-аналоговый преобразователь, таймер или интерфейс связи.
Модуль DMA микроконтроллера STM32F303VCT6

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

DMA контроллер поддерживает обработку трех флагов событий:

  • половина передачи буфера - Half transfer
  • полная передача буфера - Transfer complete
  • ошибка при передаче - Transfer error.

 
Прерывания DMA
Конфигурация DMA предполагает назначение источника и приемника передаваемых данных, задание размера одной ячейки данных, а также количество таких ячеек в передаваемом блоке.

Каждая передача DMA состоит из трех операций:

  • Загрузка данных из регистра периферийного модуля или адреса в памяти через внутренний регистр.
  • Сохранение данных, загруженных во внутренний регистр в указанное место. Им может быть периферийный модуль или адрес в памяти.
  • Увеличение адресов источника и приемника при необходимости.

 
Как только весь блок данных будет заполнен или передан, передача DMA считается завершенной и сообщает о своем завершении.

 

Настройка модуля DMA

Давайте перейдем к настройке прямого доступа к памяти для нашего проекта. Откроем проект с предыдущего урока в STM32CubeMX. Прежде всего, убедимся, что тактирование модуля АЦП настроено правильно. Откроем вкладку Clock Configuration. Видим, что тактирование модулей ADC1 и ADC2 ведется от умножителя частоты PLL даже в случае если основная тактовая частота формируется без использования умножителя. Тактирование модулей ADC1 и ADC2 должно осуществляться на частоте от 16 до 72 МГц.
Тактирование ADC1 и ADC2

Теперь перейдём во вкладку Configuration. Займёмся настройкой модуля АЦП. Во-первых, нам больше не понадобятся прерывания – снимаем галочку. Для запуска преобразований выберем какой-нибудь фронт триггера, например нарастающий, и пусть преобразования запускаются программным способом – выбираем Software Trigger. Режим преобразований пусть останется Discontinuous.
Настройка АЦП

Переходим на вкладку DMA Settings и добавляем канал DMA, нажав на кнопку Add. Выбираем запрос для канала ADC1. Передача данных будет производиться из периферийного модуля в память.
Пускай она производится по циклу – выбираем режим Circular.
Адрес периферийного модуля, естественно, увеличиваться не будет, а увеличиваться автоматически будет только адрес памяти. Ну, и передача данных будет осуществляться половинными словами – Half Word, то есть 16-битными значениями. Можно также выбрать режим передачи байтами или полными 32-битными словами.
Настройка DMA

Теперь перейдем ко вкладке NVIC Settings и разрешим прерывание по окончании DMA.
Настройка прерываний DMA
Сгенерируем проект и перейдем в IAR.

Для запуска аналого-цифровых преобразований с использованием DMA используется функция

 
HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc, uint32_t* pData, uint32_t Length)

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

Обращаю Ваше внимание на то, что указатель на буфер для этой функции 32-разрядный, а передачу данных мы решили производить половинными словами по 16 бит. Поэтому мы описываем 16-битный массив для хранения данных. Для того, чтобы можно было анализировать значения этого массива из обработчика прерывания, я опишу этот массив с глобальной областью видимости, то есть перед функцией main(). Пусть массив хранит 32 результата преобразования. Ну, и сразу проинициализируем его нулями. Благо, это сделать несложно. Теперь, чтобы использовать этот массив для прямого доступа к памяти, указатель на него необходимо явно привести к тому типу, который требует функция. Ну, и количество преобразований, очевидно, не должно превышать размер массива.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/* USER CODE BEGIN 0 */
uint16_t Vs;
uint16_t Button;
uint16_t ADCres[32] = { 0 };
/* USER CODE END 0 */
int main(void)
{
  /* USER CODE BEGIN 1 */
  /* USER CODE END 1 */
  /* MCU Configuration----------------------------------------------------------*/
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();
  /* Configure the system clock */
  SystemClock_Config();
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_ADC1_Init();
  /* USER CODE BEGIN 2 */
  HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);
  HAL_ADC_Start_DMA(&hadc1, (uint32_t *)ADCres, 32);
  /* USER CODE END 2 */
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    if (Button > 4000)
    {
      double ToC = (1.43 - ((double)Vs * 3) / 4096) * 1000 / 4.3 + 25.0;
      if (ToC > 5) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_8, GPIO_PIN_SET);
      else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_8, GPIO_PIN_RESET);
      if (ToC > 10) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_9, GPIO_PIN_SET);
      else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_9, GPIO_PIN_RESET);
      if (ToC > 15) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_10, GPIO_PIN_SET);
      else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_10, GPIO_PIN_RESET);
      if (ToC > 20) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_11, GPIO_PIN_SET);
      else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_11, GPIO_PIN_RESET);
      if (ToC > 25) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_12, GPIO_PIN_SET);
      else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_12, GPIO_PIN_RESET);
      if (ToC > 30) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_13, GPIO_PIN_SET);
      else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_13, GPIO_PIN_RESET);
      if (ToC > 35) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_14, GPIO_PIN_SET);
      else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_14, GPIO_PIN_RESET);
      if (ToC > 40) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_15, GPIO_PIN_SET);
      else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_15, GPIO_PIN_RESET);
    }
    else
    {
      HAL_GPIO_WritePin(GPIOE, GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 |
        GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 |
        GPIO_PIN_14 | GPIO_PIN_15, GPIO_PIN_RESET);
    }
    HAL_ADC_Start(&hadc1);
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

Добавлена строка 4. Изменена строка 21

 

Перейдём к файлу обработчика прерываний. Обработчик для первого канала DMA уже сгенерировался автоматически. Поставим тут точку останова. Соберём проект и загрузим в отладочную плату. Запустим программу на выполнение до точки останова и посмотрим, что же мы получили в массиве данных. Для этого перейдём в меню View->Watch->Watch1 и зададим имя массива. Откроем массив и посмотрим данные в нём. Данными заполнена ровно половина массива. Как Вы помните, прерывание DMA возникает при завершении половины передачи данных или при полном завершении. Если запустим программу до точки останова еще раз, то увидим, что данные в массиве заполнились до конца. Следующий запуск перезаписывает первую половину массива, затем – вторую и так далее.

Данные в массиве чередуются – чётные элементы содержат результат измерения температуры, а нечётные – аналого-цифровое преобразование сигнала с кнопки. В этом легко убедиться, зажав кнопку.

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

 
HAL_DMA_StateTypeDef HAL_DMA_GetState(DMA_HandleTypeDef *hdma);

которой в качестве аргумента передается дескриптор канала DMA. Нас интересуют состояния

  • HAL_DMA_STATE_READY – полный буфер
  • HAL_DMA_STATE_READY_HALF – первая половина буфера.

 
Импортируем в файл обработчика прерываний stm32f3xx_it.c наш массив данных и напишем обработчик прерываний.

1
2
3
4
5
/* USER CODE BEGIN 0 */
extern uint16_t Vs;
extern uint16_t Button;
extern uint16_t ADCres[];
/* USER CODE END 0 */

И давайте попробуем запустить проект с предыдущего урока, но используя усреднение для полученных значений.
Если заполнена первая половина буфера, то проходимся в цикле для элементов от 0 до 16. Чётные элементы – это измерения температуры, нечетные – это сигнал с кнопки. Поскольку на каждом проходе цикла обрабатываются 2 значения, то и цикл получается с шагом 2.
Если заполнена вторая половина буфера, то параметр цикла наоборот меняется от 16 до 32 с шагом 2.
Найдем среднее арифметическое полученных сумм. Мы рассматривали 16 элементов, из них 8 измерений температуры и 8 сигналов с кнопки. Поэтому и делить результат нам нужно на 8.
Ну, а деление на 8 соответствует сдвигу числа вправо на 3 разряда, поэтому не откажу себе в удовольствии таким образом и найти среднее арифметическое.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void DMA1_Channel1_IRQHandler(void)
{
  /* USER CODE BEGIN DMA1_Channel1_IRQn 0 */
  Vs = 0; Button = 0;
  if (HAL_DMA_GetState(&hdma_adc1) == HAL_DMA_STATE_READY_HALF) {
    for (int i = 0; i<16; i += 2) {
      Vs += ADCres[i];
      Button += ADCres[i + 1];
    }
  }
  else if (HAL_DMA_GetState(&hdma_adc1) == HAL_DMA_STATE_READY) {
    for (int i = 16; i<32; i += 2) {
      Vs += ADCres[i];
      Button += ADCres[i + 1];
    }
  }
  Vs = Vs >> 3;
  Button = Button >> 3;
  /* USER CODE END DMA1_Channel1_IRQn 0 */
  HAL_DMA_IRQHandler(&hdma_adc1);
  /* USER CODE BEGIN DMA1_Channel1_IRQn 1 */
  /* USER CODE END DMA1_Channel1_IRQn 1 */
}

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

Если же нам не нужно прерывание по завершении половины передачи данных, то можно его отключить, сбросив соответствующий бит HTIE регистра DMA_CCR.
Регистр DMA_CCR
Бит HTIE
Добраться до него можно через указатель на структуру Instance, содержащуюся в одном из полей типа hdma_adc1. Находим определение требуемого нам бита – DMA_CCR_HTIE. Ну, и стандартная процедура сброса – операция побитового логического И со значением, представляющим собой инверсию бита в указанной позиции. Может быть, есть способ лучше, но я не нашла. К сожалению, STM32CubeMX не даёт доступа к тонкой настройке.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/* USER CODE BEGIN 0 */
uint16_t Vs;
uint16_t Button;
uint16_t ADCres[32] = { 0 };
/* USER CODE END 0 */
int main(void)
{
  /* USER CODE BEGIN 1 */
  /* USER CODE END 1 */
  /* MCU Configuration----------------------------------------------------------*/
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();
  /* Configure the system clock */
  SystemClock_Config();
  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_ADC1_Init();
  /* USER CODE BEGIN 2 */
  HAL_ADCEx_Calibration_Start(&hadc1, ADC_SINGLE_ENDED);
  HAL_ADC_Start_DMA(&hadc1, (uint32_t *)ADCres, 32);
  hdma_adc1.Instance->CCR &= ~DMA_CCR_HTIE;
  /* USER CODE END 2 */
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    if (Button > 4000)
    {
      double ToC = (1.43 - ((double)Vs * 3) / 4096) * 1000 / 4.3 + 25.0;
      if (ToC > 5) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_8, GPIO_PIN_SET);
      else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_8, GPIO_PIN_RESET);
      if (ToC > 10) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_9, GPIO_PIN_SET);
      else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_9, GPIO_PIN_RESET);
      if (ToC > 15) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_10, GPIO_PIN_SET);
      else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_10, GPIO_PIN_RESET);
      if (ToC > 20) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_11, GPIO_PIN_SET);
      else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_11, GPIO_PIN_RESET);
      if (ToC > 25) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_12, GPIO_PIN_SET);
      else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_12, GPIO_PIN_RESET);
      if (ToC > 30) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_13, GPIO_PIN_SET);
      else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_13, GPIO_PIN_RESET);
      if (ToC > 35) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_14, GPIO_PIN_SET);
      else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_14, GPIO_PIN_RESET);
      if (ToC > 40) HAL_GPIO_WritePin(GPIOE, GPIO_PIN_15, GPIO_PIN_SET);
      else HAL_GPIO_WritePin(GPIOE, GPIO_PIN_15, GPIO_PIN_RESET);
    }
    else
    {
      HAL_GPIO_WritePin(GPIOE, GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 |
        GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 |
        GPIO_PIN_14 | GPIO_PIN_15, GPIO_PIN_RESET);
    }
    HAL_ADC_Start(&hadc1);
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

Добавлена строка 22.

Ну, и снова поставим точку останова в обработчике прерываний и запустим программу на выполнение. Теперь после каждого запуска меняются все данные в массиве, а не половина. И если мы поставим точку останова в блоке завершения первой половины передачи данных, то никогда не попадём в этот блок. В таком варианте правильнее будет обрабатывать сразу все полученные данные. То есть делать цикл для всех элементов массива, и делить полученные суммы уже не на 8, а на 16, то есть сдвигать на 4 разряда. При этом обработчика прерывания DMA в файле stm32f3xx_it.c будет выглядеть так:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void DMA1_Channel1_IRQHandler(void)
{
  /* USER CODE BEGIN DMA1_Channel1_IRQn 0 */
  Vs = 0; Button = 0;
  for (int i = 0; i<32; i += 2) {
    Vs += ADCres[i];
    Button += ADCres[i + 1];
  }
  Vs = Vs >> 4;
  Button = Button >> 4;
  /* USER CODE END DMA1_Channel1_IRQn 0 */
  HAL_DMA_IRQHandler(&hdma_adc1);
  /* USER CODE BEGIN DMA1_Channel1_IRQn 1 */
  /* USER CODE END DMA1_Channel1_IRQn 1 */
}

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

Прокрутить вверх