Дискретизация

Дискретизация

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

 
Дискретизация – это преобразование непрерывной функции в последовательность измерений, представляемых чаще всего в цифровой форме. Такие измерения можно производить с постоянным или переменным шагом, и для большинства задач интересны именно равноотстоящие измерения.
Дискретизация с постоянным шагом

Но прежде, чем перейти к настройке таймера для запуска преобразований давайте обратимся к вопросу измерения времени между преобразованиями.
Ядро Cortex-M3 / M4 имеет встроенный модуль отладки DWT (Data Watch and Trace Unit), который включает в себя регистр тактового цикла CYCCNT, а отладочный интерфейс IAR, именуемый C-SPY, в сочетании со своими макросами, реализует соответственно 64-битный регистр цикла CYCLECOUNTER. Этот регистр обеспечивает точное и расширенное время выполнения измерений.

Чтобы показать значение этого регистра при использовании отладчика C-SPY Debugger, выбираем View->Register и в окне групп регистров выбираем CPU Registers. В этой группе имеются три регистра, которые связаны с CYCLECOUNTER: CCTIMER1, CCTIMER2 и CCSTEP. Содержимое этих регистров обновляется на каждой точке останова. Нас больше всего интересует регистр CCSTEP, который представляет собой временные интервалы, измеренные в машинных тактах, между двумя точками останова. Циклы CYCLECOUNTER, CCTIMER1 и CCTIMER2 показывают накопленные тактовые циклы.
Значения регистров CCTIMER1 и CCTIMER2 могут быть изменены пользователем, в отличие от регистров CYCLECOUNTER и CCSTEP.


Поставим точку останова в обработчике прерываний и запустим программу на выполнение.

Чтобы разрешить работу этих регистров, необходимо, во-первых, включить модуль DWT. Для этого выбираем в окне регистров Debug Registers и проверяем, что бит TRCENA в регистре DEMCR установлен в 1.
Включаем TRACE
Во-вторых, заходим в регистры этого модуля отладки, выбрав в окне Data Watch and Trace Unit, и устанавливаем в 1 бит CYCCNTENA регистра DWT_CTRL. Счетчик циклов становится доступен в регистре DWT_CYCCNT.
Бит CYCCNTENA регистра DWT_CTRL
Но удобнее за ним наблюдать всё-таки из окна CPU Registers. Поэтому переходим к этому окну. И давайте определим время между остановами в обработчике прерываний. Это время составляет около 6700 машинных тактов. Именно столько тратится на заполнение массива из 32 элементов данными при выбранном нами типе программного триггера.
Кстати, чтобы не устанавливать вручную биты в регистрах отладки каждый раз при запуске программы, можно прописать это в программе следующим кодом.

1
2
3
  CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; // разрешаем TRACE
  DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; // Разрешаем счетчик тактов
  DWT->CYCCNT = 0; // Обнуляем счетчик

Именно из регистра DWT->CYCCNT можно получать отладочную информацию при необходимости.

Перейдём непосредственно к настройке таймера. Открываем конфигурацию модуля ADC в STM32CubeMX и в качестве триггера выберем, например, сигнал с таймера TIM6. При этом режим Discontinuous по одному сигналу триггера будет производить столько преобразований, сколько указано в параметре Number of Discontinuous Conversions.
Синхронизация АЦП по TIM6

Ну, и чтобы использовать сигнал с таймера для синхронизации, необходимо этот таймер включить и настроить. На вкладке Pinout активируем таймер TIM6 и переходим к настройке этого модуля на вкладке Configuration. Выбираем тип триггера Update Event и устанавливаем значение периода таймера. Поскольку в случае программного триггера на одно преобразование приходилось около 200 машинных тактов, то для таймера не имеет смысла ставить значение меньше этого. Ну, допустим, таймер будет считать до 1024, и тогда в регистр периода мы помещаем значение 1023.
Настройка таймера TIM6
Генерируем файл проекта и открываем в IARe. Теперь, прежде чем использовать прямой доступ к памяти, необходимо включить таймер. Для этого используется уже знакомая нам функция

 
HAL_StatusTypeDef HAL_TIM_Base_Start(TIM_HandleTypeDef *htim)


Все изменения файла main.c
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
/* USER CODE BEGIN 2 */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
DWT->CYCCNT = 0;
HAL_TIM_Base_Start(&htim6);
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_IT(&hadc1);
  /* USER CODE END WHILE */
  /* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}

Добавлены строки 2, 3, 4, 5.

 
Загрузим проект в отладочную плату и понаблюдаем, что происходит со временем между прерываниями. А это около 32000 машинных тактов. Что примерно соответствует 32 измерениям через 1024 машинных такта. Массив данных при этом выглядит примерно так же, как и в случае программного триггера.

Если же нам необходимо преобразовывать оба канала как можно ближе друг к другу по времени, то количество Discontinuous преобразований в настройках АЦП мы увеличиваем до 2.

Пронаблюдаем время между прерываниями для такого решения. И мы видим, что общее время заполнения массива через DMA уменьшилось практически вдвое. Но всё-таки составляет примерно 3/5 того времени, которое требовалось в предыдущем случае, а это нехорошо. Ну, а если мы заглянем в массив данных, то увидим, что всё ещё хуже. Модуль прямого доступа к памяти не всегда успевает сохранить данные, особенно для канала кнопки, у которого время выборки составляет 1,5 машинных цикла. В предыдущем случае с равноотстоящими по времени преобразованиями мы этого явления не наблюдали, поскольку промежутки между преобразованиями были достаточными для сохранения данных.
Недостаточное время для сохранения результатов преобразования
Эту проблему можно устранить, увеличив общую частоту тактирования, которая, в первую очередь, оказывает влияние на частоту тактирования модуля DMA. Поставим частоту тактирования 64МГц с использованием PLL. Но не забываем, что согласно документации время выборки для канала температуры составляет не менее 2,2мкс. Для частоты 64МГц это составляет

2,2 мкс · 64 МГц = 140,8 машинных такта

Выбираем ближайшее большее значение – это 181,5 машинный цикл.

Генерируем проект и переходим в IAR. Загружаем проект в отладочную плату. И теперь этот нежелательный эффект исчез, и общее время преобразования уже ближе к расчетному.
Но имеется другая неприятность – аналого-цифровое преобразование сигнала с кнопки откуда-то берёт ненулевое значение даже если кнопка отжата. Это – результат повышения частоты. С повышением частоты время машинного цикла сократилось, а следовательно, сократилось и время выборки, которое для кнопки составляет 1,5 машинных цикла. Мы видим, что этого недостаточно для корректного преобразования. Увеличим это время хотя бы до 4,5 машинных циклов и посмотрим на результат. Теперь все значения в массиве вернулись на круги своя. Общее время заполнения массива данными при этом не изменилось.
Запустим программу на выполнение без отладки и убедимся, что она сохранила свою работоспособность.

1 комментарий к “Дискретизация”

  1. Артур

    Спасибо большое, очень полезная тема. Отдельное спасибо за то, что показано много подводных камней с которыми можно столкнуться (а мне уже пришлось), когда в режиме работы АЦП с двумя каналами, наблюдается как бы взаимовлияние каналов, теперь понятно как это правильно устранить и в чем суть проблемы. Так же и DMA, который не успевает и получается каша, а где искать причину не было очевидно. На практике я выяснил, что частота шины AHB1, тактирущей DMA (stm32F103C8T6), должна быть выше частоты шины APB2, тактирующей таймер, когда задача стоит в копировании с помощью DMA значений из массива данных в регистр сравнения таймера, работающего в режиме ШИМ, с целью формирования сигнала определенной скважности  изменяемого в процессе работы программы путем внесения данных в массив из которого уже DMA копирует в регистр сравнения. При небольших частотах ШИМ сигнал получался корректным, а вот при увеличении частоты уже начинали проскакивать лишние биты (импульсы).

Оставьте комментарий

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

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