На этом уроке мы рассмотрим использование цифро-аналогового преобразователя в режиме генерации сигналов, заданных пользователем.
Для этой цели можно использовать два способа:
- установка произвольного аналогового значения с помощью функции HAL_DAC_SetValue()
- использование прямого доступа к памяти.
Рассмотрим оба случая.
Использование прерываний
Как я показывала на прошлом уроке, функция HAL_DAC_SetValue() может установить произвольное аналоговое значение, соответствующее двоичному коду в диапазоне от 0 до 212-1 = 4096. Установку этих значений, конечно, можно производить в рабочем цикле программы, но это очень неудобно, потому что необходимо постоянно отслеживать момент установки этих значений программным способом. Поэтому давайте для этих целей будем использовать обработчик прерываний.
Предлагаю сгенерировать сигнал прямоугольной формы, который будет переключаться между двумя аналоговыми значениями:
- 0,75 В, что соответствует четверти питающего напряжения или двоичному коду 1023
- 1,5 В, что соответствует половине питающего напряжения или двоичному коду 2047.
Допустим, мы хотим получить выходной сигнал с частотой 100 кГц. Для этого необходимо рассчитать содержимое регистра периода таймера.
Давайте снова создадим проект для микроконтроллера STM32F303VCT6. Укажем, что будем использовать канал 1 ЦАП на линии PA4 и таймер TIM6, который нам понадобится в качестве триггера.
Сконфигурируем внутреннюю тактовую частоту работы микроконтроллера равной 64 МГц и укажем частоту 32 МГц для модуля APB1.
Перейдем во вкладку Configuration в настройки ЦАП. Здесь укажем TIM6 в качестве триггера… Форму генерируемого сигнала не указываем.
Для таймера TIM6 меняем событие триггера на Update Event и указываем рассчитанное значение для регистра периода – 320.
Ну, и разрешим прерывания по переполнению таймера TIM6. Для этого переходим во вкладку NVIC Settings и ставим галочку в поле Timer 6 Interrupt.
Генерируем проект, сохраняем его и переходим в IAR.
Чтобы запустить таймер TIM6 и разрешить прерывания по переполнению этого таймера используем функцию
которой в качестве аргумента передается дескриптор таймера. Ну, и запускаем ЦАП с помощью функции
2
3
4
HAL_TIM_Base_Start_IT(&htim6);
HAL_DAC_Start(&hdac, DAC_CHANNEL_1);
/* USER CODE END 2 */
Теперь дело за обработчиком прерывания. Сам обработчик Cube для нас уже создал. Нам остается только перейти к файлу stm32f3xx_it.c и найти эту функцию. Она называется
Внутри нее мы и разместим функцию HAL_DAC_SetValue(), которая будет задавать выходное значение для ЦАП. Нам нужно менять это значение:
- если оно равно 1023, то поставить 2047,
- если оно равно 2047, то поставить 1023.
И тут я воспользуюсь одной хитростью, а именно – функцией «исключающее ИЛИ», которая в языке Си задается символом ^ («циркумфлекс»). Для этого рассмотрим представление нужных нам чисел в двоичном коде.
Вот число 1023: 00000011 11111111
А вот число 2047: 00000111 11111111
Как видим, эти два числа отличаются только десятым разрядом, значение в котором и нужно чередовать.
Единица в десятом разряде соответствует шестнадцатеричному числу 0x400 = 00000100 00000000.
Поэтому мы будем каждый раз в обработчике прерывания производить операцию «Исключающее ИЛИ» с этим значением. В результате получим, что десятый разряд числа будет меняться с нуля на единицу и наоборот.
С этой целью я введу в обработчике прерывания таймера статическую переменную с начальным значением 2047. Напомню, что статическая переменная обладает локальной областью видимости, то есть видна только в той функции, где она описана, но в то же время сохраняет за собой область памяти на протяжении всей работы программы. Инициализация такой переменной производится один раз при ее создании. Каждый раз, попадая в обработчик прерывания, мы производим операцию «Исключающее ИЛИ» с выбранным нами значением маски и далее подставляем значение этой переменной в функцию HAL_DAC_SetValue() в качестве последнего параметра.
2
3
4
5
6
7
8
9
10
11
12
{
/* USER CODE BEGIN TIM6_DAC_IRQn 0 */
static uint16_t value = 2047;
value = value ^ 0x400;
HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, value);
/* USER CODE END TIM6_DAC_IRQn 0 */
HAL_TIM_IRQHandler(&htim6);
HAL_DAC_IRQHandler(&hdac);
/* USER CODE BEGIN TIM6_DAC_IRQn 1 */
/* USER CODE END TIM6_DAC_IRQn 1 */
}
Сгенерируем проект и загрузим в отладочную плату.
Завершим работу программы и пронаблюдаем сигнал на выходе ЦАП с помощью осциллографа.
Как видим, сигнал меняется с частотой 100 кГц. Но можем заметить дребезг фронтов сигнала. Это связано с использованием выходного буфера, о котором я говорила на прошлом уроке. На самом деле, вывод ЦАП сейчас работает вхолостую. Если подключить нагрузку к этому выводу, то дребезг удастся заметно снизить. Это сигнал для случая нагрузки 5 кОм, подключенной к выходу ЦАП.
Если же отключить внутренний формирователь, то наблюдается другая проблема. Поскольку ток вывода очень маленький, то сказывается паразитная емкость щупа осциллографа, и сигнал имеет вот такую форму.
Использование прямого доступа к памяти
Второй способ формирования произвольного сигнала на выходе ЦАП – это использование канала прямого доступа к памяти, и этот способ, пожалуй, более интересный.
Давайте перейдем к настройкам модуля цифро-аналогового преобразователя и сразу перейдем во вкладку DMA Settings. Нажимаем кнопку Add … и выбираем DAC_CH1. Режим поставим в значение Circular, чтобы наши значения из массива памяти менялись по циклу. Инкремент адреса доступен только для ячеек памяти. И величина слова данных – достаточно 16 бит, соответствующего значению Half Word. ЦАП-то у нас 12-битный.
Прерывание таймера теперь можно убрать.
Вот и все настройки.
Переходим в IAR. Замечу, что в файле stm32f3xx_it.c пропала функция обработки прерывания таймера TIM6 вместе со всем пользовательским кодом. Нам она сейчас не нужна, но нужно быть внимательными при генерации проектов. Перейдем к main(). Прерывания таймера TIM6 нам сейчас не нужны, поэтому изменим имя функции запуска таймера на HAL_TIM_Base_Start().
И запустим ЦАП с использованием DMA. Для этого используется функция
Ей в качестве параметров передаются дескриптор ЦАП, номер канала, указатель на 32-битный массив данных, количество элементов этого массива данных и константа выравнивания.
2
3
4
HAL_TIM_Base_Start(&htim6);
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)data, 2, DAC_ALIGN_12B_R);
/* USER CODE END 2 */
Поскольку мы используем массив данных в 16-битном формате, при вызове функции необходимо явно привести этот аргумент к тому типу, который требует функция.
Давайте создадим этот массив из двух значений. Поскольку массив будет содержать константные значения, я рекомендую явно указать слово const, чтобы позволить компилятору уменьшить результирующий код проекта. Я не указываю количество значений в этом массиве, но указываю значения, которые присваиваются элементам.
2
3
const uint16_t data[] = {2047,1023};
/* USER CODE END 1 */
Ну, вот, собственно, и всё. Загружаем проект, запускаем и смотрим на результат.
Как видим, сигнал на выходе ЦАП нисколько не изменился, но от программы требуется намного меньше действий во время выполнения.
Генерация синусоидального сигнала
Ну, и еще одна распространенная задача – это генерация синуса. Всё, что нам нужно сделать – это сформировать для синуса массив заданных константных значений. Причем чем выше требуемая частота, тем меньше значений описывают требуемую функцию.
Давайте попробуем сгенерировать синусоиду высокой частоты. Скажем, тоже 100 кГц. При этом период составляет 640 отсчетов таймера.
А нам нужно за эти 640 отсчетов повторить синусоидальную форму сигнала.
Опишем сигнал N=20 значениями. При этом значение для регистра периода таймера TIM6 составит 32.
Перейдем в IAR.
Ну, и небольшое лирическое отступление по поводу того, как получить эти самые значения. Для этой цели я воспользуюсь программой Microsoft Excel.
- Открываем Excel, и в столбце A отмечаем номера точек – от 0 до 19.
- Второй столбец будет содержать значение аргумента. Период синуса составляет 2π, поэтому мы период разделим на количество точек, то есть 20, и умножим на соответствующий номер точки:
=2*ПИ()/20*A1 - Третий столбец – собственно значение синуса, полученное по формуле для заданного аргумента:
=SIN(B1) - Дальше необходимо задать амплитуду и начальное смещение. Пусть 0 синуса совпадает с напряжением 1,5В, а амплитуда составляет четверть питания:
=2047+1024*C1 - Теперь необходимо избавиться от дробной части. Округляем числа до 0 знаков после запятой:
=ОКРУГЛ(D1;0) - Ну, и, наконец, чтобы было проще в коде, сразу поставим запятые после значений:
=СЦЕПИТЬ(E1;",")
Скопируем полученные значения и вставим в наш код.
Осталось только удалить последнюю запятую.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const uint16_t data[] = {2047,
2363,
2649,
2875,
3021,
3071,
3021,
2875,
2649,
2363,
2047,
1731,
1445,
1219,
1073,
1023,
1073,
1219,
1445,
1731};
/* USER CODE END 1 */
Теперь в функции запуска ЦАП укажем, что теперь мы будем использовать 20 значений массива.
2
3
4
HAL_TIM_Base_Start(&htim6);
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)data, 20, DAC_ALIGN_12B_R);
/* USER CODE END 2 */
Изменилась строка 3
Загружаем проект в отладочную плату. И вот он, сигнал на выходе ЦАП.
Опять же, использование буфера без нагрузки приводит к некоторому дребезгу. А если буфер отключить, то мы можем наблюдать чистую синусоиду.
Отключение ЦАП
Еще один момент, о котором хотела сказать – это остановка ЦАП. Для этой цели используется функция
которой в качестве аргументов передаются дескриптор ЦАП и номер канала.
Давайте реализуем включение и отключение сигнала ЦАП по нажатию кнопки. Для этого перейдем к конфигурации проекта и зададим использование линии PA0 как входа.
В рабочем цикле поставим небольшую задержку… И проверим. Если кнопка нажата, то сигнал ЦАП будем отключать. А при отжатой кнопке – пусть работает: включаем ЦАП. Ну, и чтобы не дёргать ЦАП постоянно туда-сюда, введем дополнительную переменную, которая будет отслеживать изменение состояния кнопки. Нулевое значение этой переменной будет соответствовать отжатой кнопке, а единица – нажатой. При этом если реальное состояние кнопки и значение этой переменной не соответствуют друг другу, то состояние кнопки изменилось, и необходимо предпринимать заданные действия.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
HAL_TIM_Base_Start(&htim6);
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)data, 20, DAC_ALIGN_12B_R);
uint8_t keyflag = 0;
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_Delay(200);
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0)== GPIO_PIN_SET && keyflag == 0)
{
HAL_DAC_Stop_DMA(&hdac, DAC_CHANNEL_1);
keyflag = 1;
}
else if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0)== GPIO_PIN_RESET && keyflag == 1)
{
HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)data, 20, DAC_ALIGN_12B_R);
keyflag = 0;
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
Загрузим проект в отладочную плату и посмотрим на результат.
Сигнал ЦАП действительно пропадает при зажатой кнопке. А при отжатой – снова появляется.
А будут уроки про подключение каких-нибуть внешних устройств?
Пока, к сожалению, новыми уроками заниматься некогда
А не могли бы тогда посоветовать какую нибудь литературу для изучения, книги, лекции, видео?
P.S. Про DataSheets уже знаю.
А про Application Notes? На сайте st.com есть примеры использования микроконтроллеров для решения различных задач. Часто прямо с готовым кодом. Можно там посмотреть.