Как стать автором
Обновить

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

Время на прочтение9 мин
Количество просмотров21K
В прошлой статье нам удалось получить звук, но это очень дорого нам далось. Во первых, мы разогнали контроллер до максимальной скорости. А во вторых, кроме генерирования звука контроллер ничего не может, так как большая часть процессорного времени занята постоянным обновлением значения ЦАП-а. Не хорошо это. Именно сейчас остро стоит вопрос об использовании ДМА.
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.

Теги:
Хабы:
Всего голосов 20: ↑16 и ↓4+12
Комментарии8

Публикации

Истории

Работа

Программист С
37 вакансий

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань