Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA

в 14:39, , рубрики: dac, DMA, К1986ВЕ92QI, микроконтроллеры, миландр, отладка, ПДП, программирование микроконтроллеров, Работа со звуком, синусоида, синусоидальный сигнал, цап, Электроника для начинающих

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

DMA, или Direct Memory Access – технология прямого доступа к памяти, минуя центральный процессор.

— (с) отсюда.

Небольшое отступление.

От идеи использовать DMA до получения первых результатов прошла неделя упорной работы. Первые 3 дня пытался освоить его сам, но никак не получалось получить хоть какой-то результат. Все получилось лишь после того, как на официальном форуме мне дали пример конфигурации DMA под примерно такую же задачу. Через 4 дня его подробного изучения и подробного анализа документации, в голове появилась ясная картина структуры работы DMA.

Первое впечатление.

Контроллер прямого доступа в память MDR_DMA ............................................................................410

Открыв документацию я встал в ступор… Основная задача на начальном этапе освоения DMA — передать какое-нибудь значение в ЦАП. Ее и будем решать. В DMA есть так называемые «каналы». Они представляют из себя связку между приемником и передатчиком. В нашем случае между памятью и периферией (ЦАП). Какие могут быть связки — показано в таблице.

Какие могут быть связки — показано в таблице.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 1
Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 2

Как мы видим из таблицы — часть каналов зарезервированы под определенную периферию. ЦАП-а среди этой периферии нет. Остальную часть каналов можно использовать по своему назначению. Самым первым свободным каналом является канал 8. Его и будем настраивать. Но как? В документации есть раздел Правила обмена данными.

В нем прописано следующее.

Правила обмена данными
Контроллер использует правила обмена данными, перечисленные далее в Таблица 376, при соблюдении следующих условий:
— канал DMA включен, что выполняется установкой в состояние логической единицы разрядов управления chnl_enable_set[C] и master_enable;
— флаги запроса dma_req[C] и dma_sreq[C] не замаскированы, что выполняется установкой в состояние логического нуля разряда управления chnl_req_mask_set [C];
— контроллер находится не в тестовом режиме, что выполняется установкой в состояние логического нуля разряда управления int_test_en bit[C].

Сразу же найдем регистры, к которым принадлежат данные биты. Но прежде нужно подать сигнал тактирования на DMA.

Сделаем это в функции настройки DMA.

#define PCLK_EN_DMA                     (1<<5) //Маска включает тактирование DMA.
void DMA_Init_DAC (void)                          //Настройка DMA для DAC.
{
	RST_CLK->PER_CLOCK|=PCLK_EN_DMA;                //Включаем тактирование DMA.
}

После подачи тактирования, нам нужно включить DMA.

За это отвечает регистр DMA->CFG.
Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 3


#define CFG_master_enable             (1<<0)    //Маска разрешает работу контроллера.
DMA->CFG = CFG_master_enable;                   //Разрешаем работу DMA.

Следующим пунктом нужно включить бит chnl_enable_set[C]. Здесь C обозначает номер канала с нуля.

Он находится в регистре DMA->CHNL_ENABLE_SET.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 4

  DMA->CHNL_ENABLE_SET = 1<<8;                    //Разрешаем работу канала DMA 8.

После необходимо установить в «0» chnl_req_mask_set [0].

Этот бит находиться в регистре DMA->CHNL_REQ_MASK_SET.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 5

Все бы хорошо, записали бы 0 и все, но…

Разряд [C] = 0 не дает эффекта. Необходимо использовать
chnl_req_mask_clr регистр для разрешения
установки запросов;

Ладно.

Смотрим регистр DMA->CHNL_REQ_MASK_CLR.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 6

Здесь нам уже нужно установить единицу на нужном нам канале.

DMA->CHNL_REQ_MASK_CLR = 1<<8;                  //Разрешаем установку запросов на выполнение циклов DMA, по dma_sreq[] и dma_req[].

Ну и последним шагом для нас должно стать запись нуля в бит int_test_en bit[8]. Но о существовании данного бита нигде не написано. Так что — пропускаем.
В дополнение присвоим нашему каналу высокий приоритет.

Для этого существует регистр DMA->CHNL_PRIORITY_SET.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 7

DMA->CHNL_PRIORITY_SET = 1<<8;                  //Высокий приоритет. 

После включения канала, нужно определиться с режимом работы DMA.

Их целых 6.

— недействительный;
— основной;
— авто-запрос;
— «пинг-понг»;
— работа с памятью в режиме «исполнение с изменением конфигурации»;
— работа с периферией в режиме «исполнение с изменением конфигурации».

Проанализировав все я решил, что на начальном этапе мне хватит режима «основной».

Вот его описание.

Основной
В этом режиме контроллер работает только с основными или альтернативными управляющими данными канала. После того, как разрешена работа канала и контроллер получил запрос на обработку, цикл DMA выглядит следующим образом:
1. Контроллер выполняет 2^R передач. Если число оставшихся передач 0, контроллер переходит к шагу 3.
2. Осуществление арбитража:
— если высокоприоритетный канал выдает запрос на обработку, то контроллер начинает обслуживание этого канала;
— если периферийный блок или программное обеспечение выдает запрос на обработку (повторный запрос на обработку по каналу), то контроллер переходит к шагу 1.
3. Контроллер устанавливает dma_done[C] в состояние 1 на один такт сигнала hclk. Это указывает центральному процессору на завершение цикла DMA.

Подробнее мы с ним разберемся когда будем заполнять структуру настройки DMA канала.

Структура работы DMA.

Как оказалось, помимо регистров, DMA имеет еще и структуры-настройки. Честно сказать, очень долго вникал в принцип работы с этими структурами. Ранее, во времена STM32, я пользовался готовой библиотекой, потому что знаний языка не хватало для чтения документации. Теперь же, хоть и с определенным трудом, но я могу осознать весь принцип работы ДМА на низком уровне.

Для каждого канала канала следует задать свою структуру. Она состоит из четырех 32-х битных ячеек.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 8

Согласно документации, структура должна быть оформлена в такой последовательности.

— указатель конца данных источника;
— указатель конца данных приемника;
— разряды управления;
— вычисление адреса.

Заполнение структуры канала DMA

Начать предлагаю с заполнения ячейки настройки регистра.

Выбираем смещение адреса приемника (ЦАП).

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 9У нас он не меняется. Источник и приемник имеют разрядность полуслово (16 бит). Наш случай:

Разрядность данных источника = полуслово:
b11 = нет инкремента. Адрес остается равным значению области памяти dst_data_end_ptr.

Выбираем размерность данных источника и приемника.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 10Здесь выбираем полуслово. Так как наш массив uint16_t (16 бит).Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 11Здесь выбираем такое же полуслово.

Разрешаем процедуру арбитража.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 12
Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 13
Вот этот пункт очень долго держал меня в неведение. Дело в том, что DMA может передавать не всю посылку сразу, а по частям. Например у нас есть массив в 1024 элемента. Но мы хотим передавать в секунду по 128 элементов. Для этого мы можем выставить b0111 и после передачи 128 элементов передача прервется до повторного запуска процессором или периферией. Это будет полезно, когда мы будем связывать DMA с таймером. В нашем случае мы оставляем нули. Так как нам нужно передавать каждый элемент в строго определенный момент. Простая передача всего массива нам не подходит.

Задаем длину посылки.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 14
В предыдущей статье мы передавали массив длинной в 100 элементов. Поэтому здесь мы выберем 100-1 элементов (Так как 0 = одному элементу).

К сожалению, так и не понял, зачем это нужно. Оставим без изменений.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 15Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 16Оставляем пока без изменений.

Осталось лишь выбрать режим.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 17
Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 18
Выбираем режим «основной».

Ячейку конфигурации канала мы настроили.

У нас получилось следующее.

//Параметры для нашей структуры. 
#define dst_src       (3<<30)                 //Источник - 16 бит (полуслово).
#define src_inc       (1<<26)                 //Источник смещается на 16 бит после каждой передачи. 
#define src_size      (1<<24)                 //Отправляем по 16 бит.
#define dst_size      (1<<28)                 //Принимаем по 16 бит. (Приемник и передатчик должны иметь одинаковые размерности).
#define dst_prot_ctrl                         //Здесь настраивается различного рода защита (буферизация, привилегированный режим, )
#define R_power       (0<<14)                 //Арбитраж (приостановка передачи до внешнего сигнала, разрешающего ее продолжение) после каждой передачи. 
#define n_minus_1     (99<<4)                 //100  передачь DMA. 
#define next_useburst (0<<3)                  //Так и не удалось понять, что это...
#define cycle_ctrl    (1<<0)                  //Обычный режим.
//Настраиваем структуру.
#define ST_DMA_DAC_STRYKT dst_src|src_inc|src_size|dst_size|R_power|n_minus_1|next_useburst|cycle_ctrl

Теперь нужно создать массив структур и записать туда нашу настройку.

struct DAC_ST
{
	uint32_t Destination_end_pointer;                                     //Указатель конца данных приемника.
	uint32_t Source_end_pointer;                                          //Указатель конца данных источника
	uint32_t channel_cfg;                                                 //Конфигурация канала.
	uint32_t NULL;                                                        //Пустая ячейка. 
}

А вот следующий шаг отнял у меня почти 4 дня. Дело в том, что адрес каждой структуры строго фиксирован и может меняться лишь со смещением в килобайт.

Взглянем на массив структур.

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA - 19

Каждый канал может иметь две структуры. Первичную и альтернативную. Альтернативная нас пока не касается (Она нужна для других режимов работы). Нас интересует лишь первичная (правый столбик). Для того, чтобы контроллер увидел нашу структуру конфигурации восьмого канала — она должна быть расположена по адресу 0x20000080 или 0x20000280, или 0x20000480 и т. д. Этой записью я хотел показать, что структура должна быть обязательно в ОЗУ и должна быть выравнена по границе в 1024 байта. Опишем эту структуру.

__align(1024) DAC_ST; 
struct DAC_ST DAC_ST_ADC[8] ;

Еще небольшое пояснение. Главное, чтобы по указанному адресу присутствовала нужная структура. Данные структур 7-го канала или же 9-го DMA никак не волнуют. Их может и не быть. Технически, можно записать в ОЗУ по указанным адресам четыре 32-х битных ячейки и пользоваться. Но есть риск, что контроллер изменит их в процессе выполнения программы. Заполним ее в программе.

  DAC_ST_ADC[7].Destination_end_pointer = (uint32_t)C_4 + sizeof(C_4) - 1;           //Указатель на последний адрес источника (C_4 - массив значений синусоидального сигнала в 100 значений).
  DAC_ST_ADC[7].Source_end_pointer = (uint32_t)&(DAC->DAC2_DATA);                    //Указатель на последний (не меняется) адрес приемника (регистр данных DAC)
  DAC_ST_ADC[7].channel_cfg = (uint32_t)(ST_DMA_DAC_STRYKT);                         //Структура настройки канала. 
  DAC_ST_ADC[7].NULL = (uint32_t)0;                                                  //Первичная струтура.

Осталось только указать начальный адрес массива структур в регистре DMA -> CTRL_BASE_PTR.

DMA -> CTRL_BASE_PTR = (uint32_t)&DAC_ST_ADC;

Итогом нашей настройки стало.

#define CFG_master_enable             (1<<0)      //Маска разрешает работу контроллера.
#define PCLK_EN_DMA                   (1<<5)      //Маска включает тактирование DMA.

//Параметры для нашей структуры. 
#define dst_src       (3<<30)                 //Источник - 16 бит (полуслово).
#define src_inc       (1<<26)                 //Источник смещается на 16 бит после каждой передачи. 
#define src_size      (1<<24)                 //Отправляем по 16 бит.
#define dst_size      (1<<28)                 //Принимаем по 16 бит. (Приемник и передатчик должны иметь одинаковые размерности).
#define dst_prot_ctrl                         //Здесь настраивается различного рода защита (буферизация, привилегированный режим, )
#define R_power       (0<<14)                 //Арбитраж (приостановка передачи до внешнего сигнала, разрешающего ее продолжение) после каждой передачи. 
#define n_minus_1     (99<<4)                 //100  передачь DMA. 
#define next_useburst (0<<3)                  //Так и не удалось понять, что это...
#define cycle_ctrl    (1<<0)                  //Обычный режим.

//Настраиваем структуру.
#define ST_DMA_DAC_STRYKT dst_src|src_inc|src_size|dst_size|R_power|n_minus_1|next_useburst|cycle_ctrl
struct DAC_ST
{
	uint32_t Destination_end_pointer;                                     //Указатель конца данных приемника.
	uint32_t Source_end_pointer;                                          //Указатель конца данных источника
	uint32_t channel_cfg;                                                 //Конфигурация канала.
	uint32_t NULL;                                                        //Пустая ячейка. 
} 
__align(1024) DAC_ST; 
struct DAC_ST DAC_ST_ADC[8] ;

void DMA_and_DAC (void) 
{
  DAC_ST_ADC[7].Destination_end_pointer = (uint32_t)C_4 + sizeof(C_4) - 1;           //Указатель на последний адрес источника (C_4 - массив значений синусоидального сигнала в 100 значений).
  DAC_ST_ADC[7].Source_end_pointer = (uint32_t)&(DAC->DAC2_DATA);                    //Указатель на последний (не меняется) адрес приемника (регистр данных DAC)
  DAC_ST_ADC[7].channel_cfg = (uint32_t)(ST_DMA_DAC_STRYKT);                         //Структура настройки канала. 
  DAC_ST_ADC[7].NULL = (uint32_t)0;                                                  //Первичная струтура.
  RST_CLK->PER_CLOCK|=PCLK_EN_DMA;                                                   //Включаем тактирование DMA.
  DMA->CTRL_BASE_PTR = (uint32_t)&DAC_ST_ADC;                                        //Указываем адрес массива структур. 
  DMA->CFG = CFG_master_enable;                                                      //Разрешаем работу DMA.
} 

Получаем синусоидальный сигнал с помощью DMA.

Как мы помним, мы настроили DMA на остановку после каждой передачи. Теперь, с помощью системного таймера, нам нужно разрешать передачу следующего блока данных в DAC.

Конфигурируем таймер.

void Init_SysTick (void)                          
{
   SysTick->LOAD = 80000000/261.63/100-1;                 
   SysTick->CTRL |= CLKSOURCE|TCKINT|ENABLE;
}

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

volatile uint16_t Loop = 0;
volatile uint32_t Delay_dec = 0; 
void SysTick_Handler (void)
{
	if ((DAC_ST_ADC[7].channel_cfg & (0x3FF<<4)) == 0) {
	DAC_ST_ADC[7].channel_cfg = (uint32_t)(ST_DMA_DAC_STRYKT); }     //Перенастраиваем DMA.
  DMA->CHNL_ENABLE_SET   = 1<<8;                                   //Разрешаем работу канала DMA 8.
  DMA->CHNL_SW_REQUEST   = 1<<8;                                   //Запускаем цикл ДМА.
}

Вместо заключения.

Хоть нам и удалось научиться работать с DMA, но нам все равно еще не удалось разгрузить процессор. В следующей статье я разберу работу таймера и переложу работу с DMA на него, оставив мощности процессора для наших нужд.
Большое спасибо хочу сказать Yurock-у, который на официальном официальном форуме поделился примером кода конфигурации DMA под DAC. Изначально я планировал написать статью о разборе данного примера. Ибо разбирался я с ним около 3-х дней. Уж слишком сложным он для меня оказался. С использованием таймера и различных структур.
Код примера из урока.

Автор: Vadimatorikda

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js