ЦАП в STM32

ЦАП (или DAC по-буржуйски)  это АЦП с точностью до наоборот - он преобразовывает некоторые цифровые данные в их аналоговое представление (читай напряжение). Говоря еще проще - ЦАП позволит нам относительно плавно изменять напряжение на ноге контроллера. Области практического применения: генерация звука, и сигналов произвольной формы. Можно прикрутить к контроллеру SD карточку и сделать wav плеер. Производительности контроллера точно хватит, ибо я делал такое даже на AVR, а у них кстати нет ни какого встроенного ЦАПа и я прикручивал внешний. Работать с ЦАПом очень легко, и в этой статейке я попробую рассказать все, что мне известно о ЦАПе в STM32. На картинке ниже - генерация синуса при помощи ЦАПа

stm32 dac

 Для начала немного характеристик:

  • Выходное напряжение от 0 до VddA вольт
  • Аппаратная генерация шума и сигнала треугольной формы
  • Два независимых канала
  • Возможность работы в 8-ми и 12-ти разрядном режиме с левым или правым выравниванием
ЦАПом используются две ноги PA5 и PA4, именно с них-то мы и будем снимать сигнал. Даже если мы не будем ни коем образом настраивать их, все равно все будет работать, видимо DAC сам сделает все что нужно. Настраивается DAC при помощи единственного регистра DAC_CR. Он 32-х битный, младшие 16 бит настраивают ЦАП1 а старшие 16 бит - ЦАП2. Весьма странная нумерация кстати, обычно отсчёт начинается с нуля :)
 
DAC CR
ENx - включает DAC. Некоторые биты данного регистра нельзя изменить пока он установлен в единицу. Поэтому самый разумный вариант устанавливать его в последнюю очередь когда все остальное уже настроено.
BOFFx - включает буфер, благодаря которому напряжение на выходе DAC не проседает при большой нагрузке. Если этот бит сброшен то буфер включен.
TENx - включает внешний запуск преобразования. Подробности будут ниже.
TSELx[2:0] - источник запуска преобразования. Доступны следующие комбинации: 
 

TSELx2

TSELx1

TSELx0

Источник запуска

0

0

0

Timer 6 TRGO event

0

0

1

Timer 3 TRGO event

0

1

0

Timer 7 TRGO event

0

1

1

Timer 5 or timer 15 TRGO event

1

0

0

Timer 2 TRGO event

1

0

1

Timer 4 TRGO event

1

1

0

External line9

1

1

1

Software trigger

 
Т.е. ЦАП может начать преобразование от сигнала одного из таймеров, от смена логического уровня на одной из ног контроллера или от установки бита SWTRIGx в регистре DAC_SWTRIG.
WAVEx[1:0] - этими битами можно включить генерацию белого шума или сигнала треугольной формы (бит TENx должен быть включен!). Допускаются следующие комбинации: 
 

WAVEx1

WAVEx0

Выходной сигнал

0

0

Ничего не генерируется само

0

1

Белый шум

1

Х

Сигнал треугольной формы

 
 
MAMPx[3:0] - биты задают амплитуду белого шума или сигнала треугольной формы (в зависимости от того что у нас выбрано битами WAVEx[1:0]). Табличку приводить не буду, скажу только то, что  чем больше число которое мы запишем в эти 4 бита - тем больше амплитуда сигнала.  Теперь разберемся как сделать так чтоб на выходе DAC появилось напряжение. В даташите написана полезная формула которая гласит:
 
напряжение на выходе ЦАП = Vref*DORx/4095
 
Что такое Vref я думаю понятно, это напряжение на одноименной ноге, гораздо интересней что такое DORx и как туда попадают данные. DORx это регистр в зависимости от содержимого кого меняется напряжение на выходе DAC. Но писать туда напрямую ничего нельзя, он только для чтения. Данные в него загружаются через другие регистры которых аж три штуки на канал:

DAC_DHR12Rx - для записи 12 бит данных. Выравнивание по правому краю.
DAC_DHR12Lx - для записи 12 бит данных. Выравнивание по левому краю.
DAC_DHR8Rx - для записи 8 бит данных. Выравниваение по правому краю.

Чтоб лучше понять в какие биты этих регистров нужно писать данные, я скопировал из даташита эту картинку которая наглядно все показывает: 

single DAC

Теперь настало время рассказать про бит TENx  из регистра DAC_CR. Если он сброшен, то как только мы запишем данные в один из этих трех регистров, спустя несколько тактов они скопируются в регистр DORx и на ноге DACa появится напряжение. Совсем другая ситуация если бит TENx  установлен в единицу. В этом случае, данные из любого DAC_DHRxxxx регистра не будут немедленно скопированы в  DORx. Копирование произойдет когда возникнет одно из событий которые выбрано битами TSELx[2:0] в регистре DAC_CR . Можно управлять каналами ЦАПа по-отдельности а так же одновременно двумя сразу. Это осуществляется тремя регистрами: DAC_DHR12RDDAC_DHR12LDDAC_DHR8RD. Записывая данные в один из них мы в один момент времени можем изменять состояние сразу двух ЦАПов. Картинка ниже наглядно показывает это (cерый цвет это DAC2 а черный DAC1):

dual DAC

Теперь попробуем написать код который бы генерировал сигнал трёх форм: Белый шум, треугольник и синус. Для генерации последнего - я использовал табличный метод. Т.е. он не вычисляется, а значение просто извлекается из массива и выводится в ЦАП всякий раз когда возникает прерывание от таймера

 
#include "stm32f10x.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
 
/* Массив, элементы которого нужно быстро запихивать в DAC чтоб получить синус */
const uint16_t sin[32] = {
 2047, 2447, 2831, 3185, 3498, 3750, 3939, 4056, 4095, 4056,
 3939, 3750, 3495, 3185, 2831, 2447, 2047, 1647, 1263, 909,
 599, 344, 155, 38, 0, 38, 155, 344, 599, 909, 1263, 1647};
unsigned char i=0;
 
int main(void) {
  /* Включаем порт А */
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  /* Включаем ЦАП */
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);
  /* Включаем таймер 6 */
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE);
 
  /* Настраиваем ногу ЦАПа */
  GPIO_InitTypeDef GPIO_InitStructure;
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
 
  /* Настраиваем таймер так чтоб он тикал почаще */
  TIM6->PSC = 0;
  TIM6->ARR = 500;
  TIM6->DIER |= TIM_DIER_UIE; //разрешаем прерывание от таймера
  TIM6->CR1 |= TIM_CR1_CEN; // Начать отсчёт!
  NVIC_EnableIRQ(TIM6_DAC_IRQn); //Разрешение TIM6_DAC_IRQn прерывания
 
  /* Включить DAC1 */
  DAC->CR |= DAC_CR_EN1;
 
  /* Бесконечный цикл */
  while (1)
  {
  }
}
 
/*Обработчик прерывания от таймера 6 */
void TIM6_DAC_IRQHandler(void) {
  TIM6->SR &= ~TIM_SR_UIF; //Сбрасываем флаг UIF
  DAC->DHR12R1=sin[i++]; //Запихиваем в ЦАП очередной элемент массива
  if (i==32) i=0; //Если вывели в ЦАП все 32 значения то начинаем заново
}

После запуска этой программы на ноге PA4 будет происходить примерно следующее: 

sin

Не нужно присматриваться чтоб увидеть, что синус получился так себе. Но можно сделать его более гладким, это не проблема. Нужно просто увеличить раза в два,  массив со значениями синуса, чтоб не было таких резких переходов. А теперь попробуем получить белый шум. И тут я продемонстрирую немного магии :) Белый шум будет генерироваться вообще без всякого участия нашей программы, т.е. аппаратно. Для этого нужно включить внешний запуск преобразования, а источником его запуска поставить таймер. Таймер будет досчитывать до значения записанного в регистр ARR, а затем обнуляться и генерировать событие которое запустит преобразование. И так бесконечно. В результате на выходной ноге мы будем иметь белый шум

 
#include "stm32f10x.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_gpio.h"
 
int main(void) {
  /* Включаем порт А */
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  /* Включаем ЦАП */
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);
  /* Включаем таймер 6 */
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE);
 
  /* Настраиваем ногу ЦАПа */
  GPIO_InitTypeDef GPIO_InitStructure;
  GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
 
  /* Настраиваем таймер так чтоб он тикал почаще */
  TIM6->PSC = 0;
  TIM6->ARR = 500;
  TIM6->CR2=TIM_CR2_MMS_1; /* Таймер будет источником событий для ЦАПа */
  TIM6->CR1 |= TIM_CR1_CEN; // Начать отсчёт!
 
  /* Включить DAC1 */
  DAC->CR |= DAC_CR_TEN1; /* Преобразование по возникновению события ... */
  DAC->CR &= ~DAC_CR_TSEL1; /* ... от таймера 6*/
  DAC->CR |= DAC_CR_WAVE1_0; /* Генерация шума */
  //DAC->CR |= DAC_CR_WAVE1_1; /* Генерация сигнала треугольной формы */
  DAC->CR |= DAC_CR_MAMP1; /* Максимальная амплитуда */
  DAC->CR |= DAC_CR_EN1; /* Включить ЦАП1 */
 
  /* Бесконечный цикл */
  while (1)
  {
  }
}
 

На экране осциллографа они выглядит вот так: 

noise

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

triangle

Есть еще один важный момент, в режиме генерации шума или треугольника можно задавать смещение сигнала относительно нуля. Это делается записью значения в любой регистр DAC_DHRxxxx. Ну вот и всё, в одной из следующих статеек я попробую сделать плеер на микроконтроллере STM32 подключив к нему SD карточку памяти. Вопросы, предложения и деньги  как обычно принимаются в комментариях.