Апр 282012
 

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

В отличие от предыдущих примеров:

в данном случае для настройки нужных периферийных модулей будут использованы библиотеки SPL (Standard Peripheral Library). В двух словах работу программы можно описать следующим образом:

  1. В RAM формируется массив синусоиды с необходимой амплитудой. Шаг дискретизации будет равен 10 , то есть массив будет содержать 360 значений.
  2. Производится инициализация всех причастны модулей периферии. Настраивается система тактирования RCC, а также GPIO, DAC, DMA и таймер. Канал DMA осуществляет пересылку значений массива синуса в регистр данных DAC. Цифро-аналоговый преобразователь работает в 12-разрядном режиме. Как только данные поступают в регистр DAC, он начинает преобразование и выводит соответствующее аналоговое значение на внешний вывод. Таймер используется для формирования запроса DMA через необходимый промежуток времени, то есть задает период синусоидального сигнала.
  3. После инициализации и запуска периферийных модулей, программа больше никак не задействована в формировании сигнала на выходе DAC, теперь все это производится аппаратно, в фоновом режиме.

Пример программы был опробован на плате STM32VL-DISCOVERY.

На первом этапе необходимо сформировать массив синусоиды. Это можно сделать разными способами. В данном примере использована стандартная математическая библиотека, в которой есть функция вычисления синуса. Для этого в начале программы подключается файл “math.h”. Кроме него, как обычно, подключается заголовочный файл “stm32f10x.h”, необходимый для настройки периферии STM32.

#include "stm32f10x.h"
#include "math.h"

Далее задаются две величины, чтобы при встрече их в тексте компилятор подменял их на заданные значения. Это адрес 12-разрядного регистра данных DAC (с выравниванием значения вправо). Физический адрес данного регистра в пространстве памяти необходим для указания его в качестве адреса приемника при пересылке значений через канал DMA. И второе значение – число Пи.

#define DAC_DHR12R1_ADDRESS   0x40007408
#define pi  3.14159

Затем необходимые переменные, а именно массив синусоиды и переменная для цикла.

uint16_t Sinus[360], i;

После них идет объявление структур, необходимы для инициализации соответствующих модулей:

DMA_InitTypeDef            DMA_InitStructure;
TIM_TimeBaseInitTypeDef    TIM_TimeBaseStructure;
GPIO_InitTypeDef           GPIO_InitStructure;
DAC_InitTypeDef            DAC_InitStructure;

А далее объявляются функции инициализации периферии и вычисления массива синуса:

void Sinus_calculate(void);
void RCC_Configuration(void);
void GPIO_Configuration(void);
void Timer_Configuration(void);
void DMA_Configuration(void);
void DAC_Configuration(void);

В основной функции main() производится поочередный вызов всех соответствующих функций, после чего программа уходит в бесконечный цикл while(1), а синусоида на выходе будет формироваться независимо от того, что выполняется в последующем коде программы, т.е. ядро микроконтроллера при этом остается полностью свободным для выполнения других задач.

int main()
{
  Sinus_calculate();
  RCC_Configuration();
  GPIO_Configuration(); 
  DMA_Configuration();
  DAC_Configuration();
  Timer_Configuration();
  while(1);
}

Первым делом вызывается функция, в которой вычисляются значения синуса от 00 до 3600 с шагом через 10 . При этом, амплитудные значения сразу же нормируются относительно 12-разрядной величины, т.е. максимального значения 4095. Также, при вычислении значения синуса производится округление полученной величины с помощью функции lround из той же математической библиотеки, а затем приведение к необходимому типу данных uint16_t.

void Sinus_calculate()
{
  for(i=0; i<360; i++)
  {
    Sinus[i] = (uint16_t) lround((sin(i*(pi/180)) + 1)*4095/2);
  }
}

Затем подключаются тактовые сигналы к нужным периферийным модулям в функции инициализации RCC, а также настраивается вывод PA4, который является выходом 1-го канала DAC:

void RCC_Configuration()
{ 
  SystemCoreClockUpdate();
  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);
}

void GPIO_Configuration()
{
  //Выход DAC Output 1
  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_4;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
}

Затем производится настройка таймера, который задает период выходного сигнала посредством периодичности вызова запросов DMA. В данном случае, при тактовой частоте 24МГц, таймер должен формировать событие с частотой, которая определяется частотой выходного сигнала (50Гц) и числом выводимых значений массива синусоиды за один период синуса (360 значений). Соответственно, перемножив 360 на 50, получим значение 18000. Тогда, 24 000 000/18 000 = 1333.3333. Округлив, получим значение 1333.

void Timer_Configuration()
{
  //TIM2 - управление DAC (формирование синусоиды гармоники) 
  TIM_TimeBaseStructInit(&TIM_TimeBaseStructure); 
  TIM_TimeBaseStructure.TIM_Period = 1333;          
  TIM_TimeBaseStructure.TIM_Prescaler = 0;       
  TIM_TimeBaseStructure.TIM_ClockDivision = 0;    
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  
  TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

  //TIM2 TRGO selection 
  TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);
  
  TIM_DMACmd(TIM2, TIM_DMA_Update, ENABLE);
  
  TIM_Cmd(TIM2, ENABLE);
}

Теперь осталось настроить канал DMA и DAC. Для пересылки значений по запросу от таймера TIM2 используется канал 2 DMA1 (у данного мк всего один модуль DMA, но поскольку в более продвинутых семействах, например STM32F103, есть и второй канал DMA, а файлы библиотек для них общие, то и в данном случае необходимо использовать нумерацию при обращении к модулю DMA). Здесь в качестве адреса приемника задается регистр данных DAC (12-разрядный, со выравниванием вправо), источником задан массив синуса, для этого используется указатель на первый элемент этого массива. Направление передачи – из памяти в периферию, размер буфера равен 360 значений, затем снова по кругу с начального адреса массива, так как задан циклический режим. Размерность передаваемых данных – 16 бит.

void DMA_Configuration()
{
  DMA_InitStructure.DMA_PeripheralBaseAddr = DAC_DHR12R1_ADDRESS;
  DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&Sinus;
  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
  DMA_InitStructure.DMA_BufferSize = 360;
  DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
  DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
  DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
  DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
  DMA_InitStructure.DMA_Priority = DMA_Priority_High;
  DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
  DMA_Init(DMA1_Channel2, &DMA_InitStructure);
  DMA_Cmd(DMA1_Channel2, ENABLE);
}

И, наконец, настройки цифро-аналогового преобразователя. Здесь выбран режим, в котором внешний запуск отключен, а значит преобразование начинается автоматически, как только в какой-либо регистр данных записывается новое значение (в данном случае новые данные поступают через модуль DMA).

void DAC_Configuration()
{
  /* DAC channel1 Configuration */
  DAC_InitStructure.DAC_Trigger = DAC_Trigger_None;
  DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None;
  DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Disable;
  DAC_Init(DAC_Channel_1, &DAC_InitStructure);
  DAC_Cmd(DAC_Channel_1, ENABLE);
}

Ниже приведен весь текст программы:

#include "stm32f10x.h"
#include "math.h" 

#define DAC_DHR12R1_ADDRESS   0x40007408
#define pi  3.14159

uint16_t Sinus[360], i;

DMA_InitTypeDef            DMA_InitStructure;
TIM_TimeBaseInitTypeDef    TIM_TimeBaseStructure;
GPIO_InitTypeDef           GPIO_InitStructure;
DAC_InitTypeDef            DAC_InitStructure;

void Sinus_calculate(void);
void RCC_Configuration(void);
void GPIO_Configuration(void);
void Timer_Configuration(void);
void DMA_Configuration(void);
void DAC_Configuration(void);

int main()
{
  Sinus_calculate();
  RCC_Configuration();
  GPIO_Configuration(); 
  DMA_Configuration();
  DAC_Configuration();
  Timer_Configuration();
  while(1);
}

void Sinus_calculate()
{
  for(i=0; i<360; i++)
  {
    Sinus[i] = (uint16_t) lround((sin(i*(pi/180)) + 1)*4095/2);
  }
}

void RCC_Configuration()
{ 
  SystemCoreClockUpdate();
  RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);
}

void GPIO_Configuration()
{
  //Выход DAC Output 1
  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_4;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
  GPIO_Init(GPIOA, &GPIO_InitStructure);
}

void Timer_Configuration()
{
  //TIM2 - управление DAC (формирование синусоиды гармоники) 
  TIM_TimeBaseStructInit(&TIM_TimeBaseStructure); 
  TIM_TimeBaseStructure.TIM_Period = 1333;          
  TIM_TimeBaseStructure.TIM_Prescaler = 0;       
  TIM_TimeBaseStructure.TIM_ClockDivision = 0;    
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  
  TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

  //TIM2 TRGO selection 
  TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update);
  
  TIM_DMACmd(TIM2, TIM_DMA_Update, ENABLE);
  
  TIM_Cmd(TIM2, ENABLE);
}

void DMA_Configuration()
{
  DMA_InitStructure.DMA_PeripheralBaseAddr = DAC_DHR12R1_ADDRESS;
  DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)&Sinus;
  DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
  DMA_InitStructure.DMA_BufferSize = 360;
  DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
  DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
  DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
  DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
  DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
  DMA_InitStructure.DMA_Priority = DMA_Priority_High;
  DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
  DMA_Init(DMA1_Channel2, &DMA_InitStructure);
  DMA_Cmd(DMA1_Channel2, ENABLE);
}

void DAC_Configuration()
{
  /* DAC channel1 Configuration */
  DAC_InitStructure.DAC_Trigger = DAC_Trigger_None;
  DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None;
  DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Disable;
  DAC_Init(DAC_Channel_1, &DAC_InitStructure);
  DAC_Cmd(DAC_Channel_1, ENABLE);
}

Результат работы программы приведен на рисунке ниже. Осциллограмма снята с вывода PA4 платы STM32VL-DISCOVERY:

Sinus

А по следующей ссылке можно загрузить данный проект, который создан в IAR.

Проект работы DAC с DMA

И, напоследок, хочется отметить, что использование математических функций из библиотеки math.h для вычисления значений синуса и округления данных значений, приводит к довольно приличному увеличению размера программы, можете сами посмотреть размер .hex файла в папке Debug->Exe. Другой вариант – самостоятельно прописать значения массива синуса при написании программы (достаточно только четверти периода, от 0 до 90 градусов, остальные квадранты содержат те же значения, и их можно заполнить уже программно). Такой вариант значительно уменьшит размер выходного файла, можете проверить сами. Может как-то можно избежать такого раздувания размера файла при использовании мат. функций, я пока не нашел способа. Если знаете – сообщите.

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

Другие статьи:

  3 Responses to “STM32. DAC — цифро-аналоговый преобразователь. Часть 3”

  1. Вам спасибо! только начинаю работать с этими МК, ваш курс очень помогает.

  2. Подскажите пожалуйста, как отсеживать что данные на аналоговом PA4(5) выходе устновлены(по документации требуется 7-12мкс) и можно выставлять другие ( т.е. загружать в DAC_DHRx)? Если ли какой нибудь флаг сигнализирующий это?

    • Только по времени Tsetting, совершенно верно. Плюс один такт для переноса данных из регистра DAC_DHR в регистр DAC_DOR. Других способов контроля установки уровня напряжения на выводе нет.