DAC в STM32

ЦАП (цифро-аналоговый преобразователь) нужен для преобразования числового кода в напряжение. Я опишу работу с ЦАП в stm32f100c4t6, там их целых 2 еще и 12-битных.

Они умеют:
  • Вывод напряжения от 0 до Vref+ (здесь это Vdda)
  • Генератор белого шума
  • Генератор треугольного сигнала
  • По DMA каналу на каждый ЦАП
  • Вывод данных по событию таймера или внешнего события
  • Возможность работы в 8-битном режиме
  • Возможность одновременной загрузки разных данных (через один регистр) в оба ЦАП

Структурная схема ЦАП:


Расчет выходного напряжения ЦАП производиться по такой формуле:
DACout = Vref*DOR/4095
Где Vref – опорное напряжение, в нашем случае это Vdda, DOR – значение в выходном регистре.

Перед началом работы с ЦАП, как и со всей периферией в stm, на него надо подать тактовые импульсы. Делается это в регистре RCC->APB1ENR установкой бита RCC_APB1ENR_DACEN.
RCC->APB1ENR |= RCC_APB1ENR_DACEN; 	// вкл. тактирование ЦАП


В документации говориться что вывод к которому подключен ЦАП должен быть настроен как Analog in, но оно работает и так (с любо конфигурацией), даже тактирование порта можно не включать. После включения ЦАП он сам перейдет в нужный режим. Собственно ЦАП включается так:
DAC->CR |= DAC_CR_EN1;		// вкл. ЦАП 1

Соответственно для включения ЦАП2 надо установить бит DAC_CR_EN2 в том же регистре. Только включать его надо в последнюю очередь, поскольку многие конфигурационные биты не могут быть изменены с поднятым флагом ENx.

Вывод данных

Сначала данные заносятся в регистр предварительно хранения (data holding register, DHRx), дальше по событию (если включено) переносятся в выходной регистр (DORx), это занимает 3 такта APB1 если событие аппаратное и 1 если программное. Если вывод данных по событию выключен (по умолчанию), то это делается автоматически за 1 такт.
DAC->CR |= DAC_CR_TEN1;	// включение вывода данных от события


Для переноса данных возможны такие события:

Эти биты устанавливаются в регистре DAC->CR, для удобства в файле stm32f10x.h созданы битовые поля:
#define  DAC_CR_TSEL1                        ((uint32_t)0x00000038)        /*!<TSEL1[2:0] (DAC channel1 Trigger selection) */
#define  DAC_CR_TSEL1_0                      ((uint32_t)0x00000008)        /*!<Bit 0 */
#define  DAC_CR_TSEL1_1                      ((uint32_t)0x00000010)        /*!<Bit 1 */
#define  DAC_CR_TSEL1_2                      ((uint32_t)0x00000020)        /*!<Bit 2 */


Т.е. для вывода данных с ЦАП1 по внешнему событию надо написать
DAC->CR |= DAC_CR_TSEL1_2 |  DAC_CR_TSEL1_1;

Для программного события достаточно
DAC->CR |= DAC_CR_TSEL1;

(Для ЦАП2 соответственно DAC_CR_TSEL2 и т. д.)

Программное событие генерируется установкой бита DAC_SWTRIGR_SWTRIG1 (для ЦАП2 DAC_SWTRIGR_SWTRIG2) в регистре DAC->SWTRIGR.
DAC->SWTRIGR |= DAC_SWTRIGR_SWTRIG1;	// программное события запуска 
преобразования
Бит сбрасывается автоматически через 1 такт.

Формат данных

Данные в регистр предварительного хранения могут загружаться в нескольких форматах: 12 бит выравнивание по правому краю, 12 бит выравнивание по левому краю (для 16-битного числа), 8 бит. Для каждого способа существует свой регистр, что намного упрощает настройку.

Соответственно регистры называются:
DAC->DHR8Rx – для 8 битного доступа
DAC->DHR12Lx – выравнивание по левому краю
DAC->DHR12Rx – по правому
x – номер ЦАП (1 или 2)

Одновременный доступ к двум ЦАП осуществляется в таком формате

Соответственно название регистров:
DAC->DHR8RD
DAC->DHR12LD
DAC->DHR12RD

Старшая часть битов для ЦАП2, младшая для ЦАП1. т.е. в регистре DAC->DHR8RD биты [7:0] для ЦАП1, а [15:8] – ЦАП2.

Данные из каждого из этих регистров сразу загружаются в регистр DHRx, а после команды в DORx (см. структурную схему).

Выходной буфер

Также каждый ЦАП имеет выходной буфер для того чтобы обойтись без внешнего повторителя-усилителя. По умолчанию он включен, за него отвечает бит DAC_CR_BOFF1 в регистре DAC->CR (для ЦАП2 бит называется DAC_CR_BOFF2). Если бит установлен то буфер выключен.
DAC->CR |= DAC_CR_BOFF1;	// выкл выходной буфер.


С буфером динамические характеристики немного хуже. Замеры выходного сопротивления собственного ЦАП проведу позже, в апноуте написано что при нагрузке 5.1 кОм без буфера выходное напряжение из 3.3 В падает до 1.2 В.

Минимум для запуска ЦАП

Чтобы заставить работать ЦАП1 достаточно прописать (инициализация)
RCC->APB1ENR |= RCC_APB1ENR_DACEN;      // вкл. тактирование ЦАП
DAC->CR |= DAC_CR_EN1;          // вкл. ЦАП 1

и в нужное время загружать в него данные такой командой
DAC->DHR12R1 = a;       // загрузка данных в ЦАП1


ЦАП, таймер и DMA


Для управления ЦАП создано 2 таймера: таймер 6 и таймер 7. Они очень просты и имеют минимум настроек. Конечно управлять можно и каким-то другим (7, 3, 2, 4, 5, 15), но эти специально для ЦАП, в документации так и написано.

Настройка таймера

Для начала настроим таймер. Он будет считать до переполнения и по переполнению будет запускаться передача по DMA.

void TIM6_init(void)
{
	RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;		// тактирование таймера

	TIM6->PSC = 0XFFFF;				// предделитель
	TIM6->ARR = 244;				// переполнение две секунды	

	TIM6->DIER |= TIM_DIER_UIE;		// прерывание по переполнению
	TIM6->DIER |= TIM_DIER_UDE;		// влк. запуск ПДП
		
	TIM6->CR2 |= TIM_CR2_MMS_1; 	// ЦАП будет запускаться по переполнению
	TIM6->CR1 |= TIM_CR1_CEN;		// запуск счета			
}


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

В инициализации все поля понятны кроме TIM6->CR2 |= TIM_CR2_MMS_1; это значит что внешнее событие TRGO будет генерироваться при обновлении таймера (или при переполнении, что почти то же само).

При внешнем событии ЦАП, данные переносятся из регистра DHRx в DORx, а DMA может поместить данные только в DHRx. При чем ЦАП инициирует перенос данных по внешнему событию (только по внешнему). Для чего ЦАПу может понадобиться инициировать передачу DMA я не придумал, т.к. если настроить чтобы он преобразовывал данные по событию таймера, то данные перейдут с регистра DHRx в DORx, а потом через ПДП загрузятся новые в DHRx, т.е. данные на выходе появятся только при следующем событии.

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

Инициализация DMA

void DMA_init (void)
{
	RCC->AHBENR |= RCC_AHBENR_DMA1EN;	// подаю такты на DMA1
	DMA1_Channel3->CCR |= DMA_CCR3_MSIZE_0;		// будем передавать 16 бит данных
	DMA1_Channel3->CCR |= DMA_CCR3_PSIZE_0;
	DMA1_Channel3->CCR |= DMA_CCR3_CIRC; 	// цыклическая передача
	DMA1_Channel3->CCR |= DMA_CCR3_DIR;		// чтение с памяти

	DMA1_Channel3->CNDTR = 1;			// для того чтобы работало 
	DMA1_Channel3->CPAR = (uint32_t) &DAC->DHR12R1;
	DMA1_Channel3->CMAR = (uint32_t) &a;

	DMA1_Channel3->CCR |= DMA_CCR3_EN;	// вкл. ПДП
}


DMA1_Channel3->CNDTR = 1; — это количество данных которые будут переданы, если оставить 0, то ПДП ничего не передаст. В принципе можно ввести любое число, но от этого ничего не измениться, т.к. инкримента адресов не происходит, а передача циклическая (по окончании передачи кол-ва байт записанных в CNDTR счетчик обнулится и все пойдет по кругу).

Для вывода какого-то массива надо записать адрес начала массива в регистр DMA1_Channel3->CMAR , а в DMA1_Channel3->CNDTR количество данных которое будет выводиться. Для вывода одновременно на оба ЦАПа надо просто изменить адрес переферии в DMA и формат данных конечно, к примеру
DMA1_Channel3->CPAR = (uint32_t) &DAC->DHR12RD;
DMA1_Channel3->CCR |= DMA_CCR3_MSIZE_1;		// будем передавать 32 бит данных
DMA1_Channel3->CCR |= DMA_CCR3_PSIZE_1;

Комментарии (20)

RSS свернуть / развернуть
Все, дописал, вроде ничего не забыл.
0
А при запуске ПДП в таймере TIM6->DIER |= TIM_DIER_UDE; если при этом у ЦАПа не поставлен бит DAC_CR_TEN1,(данные из DHRx сразу летят в DHRx) и ПДП настроен на работу в циклическом режиме, то он запустится один раз и будет молотить данные из массива в ЦАП бесконечно? Или же он будет ресетится при каждом переполнении таймера и генерировании им события?
0
И еще: в RM0041 есть картинка 22 (стр.149) и таблица 54 (ст. 150). Там рассказывается какое устройство может сделать реквест какого канала DMA. Получается, что сам DMA может кидать данные из любого, доступного для записи, регистра периферии в память и наоборот, а указанные устройства лишь запускают передачу нужного нам канала DMA?
0
Да, с DMA все верно
0
Перечитываю еще раз датащит и думаю что все таки каждый канал ДМА работает только с тем списком устройств, которые указанны на картинке 22. Например в SPI есть бит TXDMAEN для 3-его канала ДМА. В ЦАП1 — бит DMAEN1. Получается что при что при инициации ДМА каким нибудь устройством, оно само знает в какой регистр периферии ему пихать данные? Для ЦАПа например это DHR для SPI — DR. Но зачем тогда мы дополнительно указываем адрес регистра получателя/приемника периферии — CPAR?
0
Только инициализация передачи привязана к переферии, адреса памяти могут быть любыми, мы их сами задаем.
0
инициализация передачи как-то некрасиво звучит, я имел ввиду событие которое запускает передачу данных
0
Тогда я совершенно не вижу профита в подходе. Не считая таймеров, где событие, по которому ДМА начинает цикл обмена, в других регистрах периферии мы САМИ ВРУЧНУЮ пишем бит, по которому начинается работа ДМА. Дык зачем раскидывать этот бит в несколько регистров? Не проще ли сделать какой нибудь GO_DMA_CH3 регистре управления ДМА?
0
Наверное потому что так логичнее, запуск передачи ПДП — функция переферии. Это как возможнось генерации прерывания, они ведь не сделали один здоровый регистр в котором были бы собраны все флаги разрешающие прерывания.

И если вдуматься во внутреннюю структуру, представте некий блок — какая-то периферия, к которому подключена системная шина, порты ввода-вывода, есть выходы для генерации прерывания и выход для запуска DMA. Проще будет в этом блоке настраивать что включено а что нет, разрешать определенные выходы и входы.
0
Был на семинаре по STM32L. Докладчик сказал, что обмен может быть только с теми устройствами, какие могут инициализировать ДМА. Решил проверить. По событию таймера6 кидаю массив не на ЦАП, а на PA ( DMA1_Channel3->CPAR = (uint32_t)&GPIOA->ODR ). Работает же) Вы правы оказались — работает с любой (скорее всего) периферией, а не только с той, кто инициализировал. Видимо докладчик не сильно заморачивался с ДМА и ответил наобум.
0
Немного вас не понял, ПДП будет передавать данные при каждом событии таймера (прерывание или совпадение), сам по себе он ничего никуда не пошлет.
0
Пытаю заставить массив циклически выбрасывать на DAC с помощью DMA. Запустить процесс начала работы DMA нужно 1 раз.
Установка бита DMA_CCR3_EN не достаточна для запуска выбранного канала DMA и нужен еще реквест от периферии. Для этого смотрю опять же картинку или таблицу и выбираю или TIM6 или DAC1.
У таймера6, включение DMA по переполнению это бит TIM_DIER_UDE и биты TIM_CR2_MMS настроенные на вырабатывание TRGO по update — вот так MMS2:0 [010]. Тут все понятно: когда таймер6 досчитывает до значения в ARR, то генерится событие TRGO, которое запускает DMA. Но будет ли DMA начинать выброс массива на ЦАП или это никак не повлияет на работу DMA?
С DAC не очень все понятно: если хотим запустить DMA от DAC, то нужно установить бит DMAEN1 в DAC_CR. Делаем мы это например в процедуре инициации DAC. Получается, что вместе с процедурой инициации DAC начинает работать и DMA?
0
Но будет ли DMA начинать выброс массива на ЦАП или это никак не повлияет на работу DMA?
Не совсем понятно что вы имеете ввиду под «выбросом массива», я так понимаю что данные надо гнать с определенными периодом, а не просто так.

С таймером вы все правильно поняли. Единственный запуск это будет запуск таймера, потом по событию таймера DMA будет пересылать данные в ЦАП, автоматически увеличивать указатель на байт из массива, и так по кругу пока не выполнит заданное количество передач данных, дальше генерируется прерывание завершения пересылки DMA.

Получается, что вместе с процедурой инициации DAC начинает работать и DMA?
Нет, DMA не заработает, но ЦАП будет пытаться «вытащить» из него данные.
0
я так понимаю что данные надо гнать с определенными периодом, а не просто так
Ну есть частный случай — пустить через ЦАП синус на максимальной частоте.
0
Тогда пусть таймер генерирует событие каждый такт.

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

Что вы пытаетесь сделать?
0
Пытаюсь посмотреть на что способны DAC и DMA в STM32. Какие есть режимы работы, ограничения итп
0
Кстати, делал по вашему примеру и циклическая передача не заработала до тех пор, пока не поставил разрешение инкремента памяти,
DMA1_Channel3->CCR |= DMA_CCR3_MINC;     // MEMORY INC
что не совсем очевидно, т.к уже задан размер данных чтения и записи и выставлен бит циклической передачи и самое главное — размер массива для передачи. По идее, и так понятно, что инкремент будет.

void DMA1_init (void)
 {      DMA1_Channel3->CCR &= ~DMA_CCR3_EN;      // выкл. ПДП
        RCC->AHBENR |= RCC_AHBENR_DMA1EN;       
        DMA1_Channel3->CCR |= DMA_CCR3_MSIZE_0; // 16 битные данные из массива
        DMA1_Channel3->CCR |= DMA_CCR3_PSIZE_0; //16 битные данные в регистр
        DMA1_Channel3->CCR |= DMA_CCR3_CIRC;    // циклическая передача
        DMA1_Channel3->CCR |= DMA_CCR3_DIR;     // чтение из памяти
        //DMA1_Channel3->CCR |= DMA_CCR3_PINC;     // PERIPH INC
        DMA1_Channel3->CCR |= DMA_CCR3_MINC;     // MEMORY INC
        DMA1_Channel3->CNDTR = 45;               //число данных
        DMA1_Channel3->CPAR  = (uint32_t)&GPIOA->ODR;//DAC->DHR12R1; //;
        DMA1_Channel3->CMAR  = (uint32_t)&Sin45;
        DMA1_Channel3->CCR |= DMA_CCR3_EN;      // вкл. ПДП
 }
0
А зачем может понадобиться инкрементировать адрес периферии?

Видно чипы еще не до конца продуманы и содержат некоторые «интересные фичи».
0
протупил, при передачи данных с памяти в память это нужно
0
Это наверно потому что у них регистры называются не «приемник» и «источник», а «память» и «периферия». Получается что даже при передаче из памяти в память мы будем будем настраивать регистры «периферии» DMA_CCR3_PSIZE и DMA_CCR3_PINC
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.