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

  • Tutorial
В прошлой статье нам удалось получить звук, но это очень дорого нам далось. Во первых, мы разогнали контроллер до максимальной скорости. А во вторых, кроме генерирования звука контроллер ничего не может, так как большая часть процессорного времени занята постоянным обновлением значения ЦАП-а. Не хорошо это. Именно сейчас остро стоит вопрос об использовании ДМА.
DMA, или Direct Memory Access – технология прямого доступа к памяти, минуя центральный процессор.
— (с) отсюда.


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


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

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


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

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


Как мы видим из таблицы — часть каналов зарезервированы под определенную периферию. ЦАП-а среди этой периферии нет. Остальную часть каналов можно использовать по своему назначению. Самым первым свободным каналом является канал 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.

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

Следующим пунктом нужно включить бит chnl_enable_set[C]. Здесь C обозначает номер канала с нуля.
Он находится в регистре DMA->CHNL_ENABLE_SET.
  DMA->CHNL_ENABLE_SET = 1<<8;                    //Разрешаем работу канала DMA 8.

После необходимо установить в «0» chnl_req_mask_set [0].
Этот бит находиться в регистре DMA->CHNL_REQ_MASK_SET.

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

Ладно.
Смотрим регистр DMA->CHNL_REQ_MASK_CLR.

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

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

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-х битных ячеек.


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


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


Начать предлагаю с заполнения ячейки настройки регистра.
Выбираем смещение адреса приемника (ЦАП).
У нас он не меняется. Источник и приемник имеют разрядность полуслово (16 бит). Наш случай:
Разрядность данных источника = полуслово:
b11 = нет инкремента. Адрес остается равным значению области памяти dst_data_end_ptr.

Выбираем размерность данных источника и приемника.
Здесь выбираем полуслово. Так как наш массив uint16_t (16 бит).Здесь выбираем такое же полуслово.

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


Вот этот пункт очень долго держал меня в неведение. Дело в том, что DMA может передавать не всю посылку сразу, а по частям. Например у нас есть массив в 1024 элемента. Но мы хотим передавать в секунду по 128 элементов. Для этого мы можем выставить b0111 и после передачи 128 элементов передача прервется до повторного запуска процессором или периферией. Это будет полезно, когда мы будем связывать DMA с таймером. В нашем случае мы оставляем нули. Так как нам нужно передавать каждый элемент в строго определенный момент. Простая передача всего массива нам не подходит.

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

В предыдущей статье мы передавали массив длинной в 100 элементов. Поэтому здесь мы выберем 100-1 элементов (Так как 0 = одному элементу).

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

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


Выбираем режим «основной».

Ячейку конфигурации канала мы настроили.
У нас получилось следующее.
//Параметры для нашей структуры. 
#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 дня. Дело в том, что адрес каждой структуры строго фиксирован и может меняться лишь со смещением в килобайт.
Взглянем на массив структур.
Каждый канал может иметь две структуры. Первичную и альтернативную. Альтернативная нас пока не касается (Она нужна для других режимов работы). Нас интересует лишь первичная (правый столбик). Для того, чтобы контроллер увидел нашу структуру конфигурации восьмого канала — она должна быть расположена по адресу 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-х дней. Уж слишком сложным он для меня оказался. С использованием таймера и различных структур.

Код и аудио на github.

Средняя зарплата в IT

120 000 ₽/мес.
Средняя зарплата по всем IT-специализациям на основании 3 227 анкет, за 1-ое пол. 2021 года Узнать свою зарплату
Реклама
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее

Комментарии 8

    +2
    Что то очень заумное использование DMA.
    Я бы сделал запрос одиночных передач по DMA от Таймера 1.
    В данном случае Таймер только генерирует запросы, а передачи идут из ОЗУ в DAC.

    Запрос по переполнению Таймера, один отсчет ушел в DAC
    Следующий запрос, еще один отсчет ушел в DAC и так далее по таблице 1024 раза (ну или 100 как вы любите).
    После обнуления таблицы процессор ее регенерит за время между запросами от Таймера.
    Изменяя частоту переполнения таймера можно легко выставлять битрейт.

    Кстати, подобный пример уже делали для кого-то, и в службе тех поддержки он должен быть.
    Обратитесь в support@mi....dr.ru

      0
      Для меня, видимо… Ссылку указал. То, что вы описали, будет представлено в следующей статье. Хочется показать сначала реализацию от «что первое приходит на ум» до «как надо».
        0
        Для того, чтобы сделать то, что вы описали — нужно уметь работать с таймером. А пока что у меня не было такого урока. Следующим будет именно он.
          0
          Тоже что-то подобное делал, только на PIC24. Там еще были отдельные векторы прерываний для случая, когда DMA прошел половину буфера, и случая, когда DMA закончил передачу. Можно было, получив INT, что половина пройдена, посчитать амплитуду\фазу для следующего периода синуса (DMA при этом продолжает выдавать вторую часть синуса), а получив сигнал об окончании текущего периода, перезарядить DMA на новую выдачу. Тут такого нет?
          ЗЫ: а почему один из хабов статьи — Objective C?
            0
            Тут такое есть (сказали ниже).
            ЗЫ: а почему один из хабов статьи — Objective C?

            Я вроде бы на C пишу… Значит и этот тег тоже выбрать следовало. Разве нет?
              0
              Это чисто Эппловское расширение Си. Между С и C# куда больше общего, чем между С и Objective C…
                0
                Спасибо, изменю.
            +2
            В структуре DMA этого процессора есть возможность работы с «половинками» синусоиды (режим пинг-понг).
            Ниже инициализация DMA-ЦАП для такого подхода с использованием библиотек Миландр.
            За основу взят код примера из этой библиотеки.

            DMA_ChannelInitTypeDef DMA_InitStr_TIM1;
            DMA_CtrlDataInitTypeDef DMA_PriCtrlStr_TIM1;
            DMA_CtrlDataInitTypeDef DMA_AltCtrlStr_TIM1;
            ........
            
            // ФОРМИРОВАНИЕ СИНУСОИДЫ - ДМА И ЦАП
            DMA_DeInit_();
            DMA_StructInit(&DMA_InitStr_TIM1);
            /* Set Primary Control Data */
            DMA_PriCtrlStr_TIM1.DMA_SourceBaseAddr = (uint32_t)Sine12bit1;
            DMA_PriCtrlStr_TIM1.DMA_DestBaseAddr = (uint32_t)(&(MDR_DAC->DAC2_DATA));
            DMA_PriCtrlStr_TIM1.DMA_SourceIncSize = DMA_SourceIncHalfword;
            DMA_PriCtrlStr_TIM1.DMA_DestIncSize = DMA_DestIncNo;
            DMA_PriCtrlStr_TIM1.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
            DMA_PriCtrlStr_TIM1.DMA_Mode = DMA_Mode_PingPong;
            DMA_PriCtrlStr_TIM1.DMA_CycleSize = TABLE_SIN_SIZE/2;
            DMA_PriCtrlStr_TIM1.DMA_NumContinuous = DMA_Transfers_1;
            DMA_PriCtrlStr_TIM1.DMA_SourceProtCtrl = DMA_SourcePrivileged;
            DMA_PriCtrlStr_TIM1.DMA_DestProtCtrl = DMA_DestPrivileged;
            
            /* Set Alternate Control Data */
            DMA_AltCtrlStr_TIM1.DMA_SourceBaseAddr = (uint32_t)Sine12bit2;
            
            DMA_AltCtrlStr_TIM1.DMA_DestBaseAddr   = (uint32_t)(&(MDR_DAC->DAC2_DATA));
            
            DMA_AltCtrlStr_TIM1.DMA_SourceIncSize = DMA_SourceIncHalfword;
            DMA_AltCtrlStr_TIM1.DMA_DestIncSize = DMA_DestIncNo;
            DMA_AltCtrlStr_TIM1.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
            DMA_AltCtrlStr_TIM1.DMA_Mode = DMA_Mode_PingPong;
            DMA_AltCtrlStr_TIM1.DMA_CycleSize = TABLE_SIN_SIZE/2;
            DMA_AltCtrlStr_TIM1.DMA_NumContinuous = DMA_Transfers_1;
            DMA_AltCtrlStr_TIM1.DMA_SourceProtCtrl = DMA_SourcePrivileged;
            DMA_AltCtrlStr_TIM1.DMA_DestProtCtrl = DMA_DestPrivileged;
            
            /* Set Channel Structure */
            DMA_InitStr_TIM1.DMA_PriCtrlData = &DMA_PriCtrlStr_TIM1;
            DMA_InitStr_TIM1.DMA_AltCtrlData = &DMA_AltCtrlStr_TIM1;
            DMA_InitStr_TIM1.DMA_Priority = DMA_Priority_High;
            DMA_InitStr_TIM1.DMA_UseBurst = DMA_BurstClear;
            DMA_InitStr_TIM1.DMA_SelectDataStructure = DMA_CTRL_DATA_PRIMARY;
            
            /* Init DMA channel TIM2*/
            DMA_Init(DMA_Channel_TIM1, &DMA_InitStr_TIM1);
            
            /* Enable dma_req or dma_sreq to generate DMA request */
            MDR_DMA->CHNL_REQ_MASK_CLR = DMA_SELECT(DMA_Channel_TIM1);
            MDR_DMA->CHNL_USEBURST_CLR = DMA_SELECT(DMA_Channel_TIM1);
            
            /* Enable DMA_Channel_TIM2 */
            DMA_Cmd(DMA_Channel_TIM1, ENABLE);
            


            В прерывании по таймеру заполняем размеры половинок:

               DMA_AltCtrlStr_TIM1.DMA_CycleSize = TABLE_SIN_SIZE/2;
               DMA_PriCtrlStr_TIM1.DMA_CycleSize = TABLE_SIN_SIZE/2;
               DMA_Init(DMA_Channel_TIM1, &DMA_InitStr_TIM1);
            


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

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

            Самое читаемое