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.
#define PCLK_EN_DMA (1<<5) //Маска включает тактирование DMA.
void DMA_Init_DAC (void) //Настройка DMA для DAC.
{
RST_CLK->PER_CLOCK|=PCLK_EN_DMA; //Включаем тактирование DMA.
}
После подачи тактирования, нам нужно включить DMA.
#define CFG_master_enable (1<<0) //Маска разрешает работу контроллера.
DMA->CFG = CFG_master_enable; //Разрешаем работу DMA.
Следующим пунктом нужно включить бит chnl_enable_set[C]. Здесь C обозначает номер канала с нуля.
DMA->CHNL_ENABLE_SET = 1<<8; //Разрешаем работу канала DMA 8.
После необходимо установить в «0» chnl_req_mask_set [0].
Все бы хорошо, записали бы 0 и все, но…
Разряд [C] = 0 не дает эффекта. Необходимо использовать
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 = 1<<8; //Высокий приоритет.
После включения канала, нужно определиться с режимом работы DMA.
— недействительный;
— основной;
— авто-запрос;
— «пинг-понг»;
— работа с памятью в режиме «исполнение с изменением конфигурации»;
— работа с периферией в режиме «исполнение с изменением конфигурации».
Основной
В этом режиме контроллер работает только с основными или альтернативными управляющими данными канала. После того, как разрешена работа канала и контроллер получил запрос на обработку, цикл DMA выглядит следующим образом:
1. Контроллер выполняет 2^R передач. Если число оставшихся передач 0, контроллер переходит к шагу 3.
2. Осуществление арбитража:
— если высокоприоритетный канал выдает запрос на обработку, то контроллер начинает обслуживание этого канала;
— если периферийный блок или программное обеспечение выдает запрос на обработку (повторный запрос на обработку по каналу), то контроллер переходит к шагу 1.
3. Контроллер устанавливает dma_done[C] в состояние 1 на один такт сигнала hclk. Это указывает центральному процессору на завершение цикла DMA.
Структура работы DMA.
Как оказалось, помимо регистров, DMA имеет еще и структуры-настройки. Честно сказать, очень долго вникал в принцип работы с этими структурами. Ранее, во времена STM32, я пользовался готовой библиотекой, потому что знаний языка не хватало для чтения документации. Теперь же, хоть и с определенным трудом, но я могу осознать весь принцип работы ДМА на низком уровне.
Согласно документации, структура должна быть оформлена в такой последовательности.
— указатель конца данных источника;
— указатель конца данных приемника;
— разряды управления;
— вычисление адреса.
Заполнение структуры канала DMA
Начать предлагаю с заполнения ячейки настройки регистра.
Разрядность данных источника = полуслово:
b11 = нет инкремента. Адрес остается равным значению области памяти dst_data_end_ptr.
Вот этот пункт очень долго держал меня в неведение. Дело в том, что 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 дня. Дело в том, что адрес каждой структуры строго фиксирован и может меняться лишь со смещением в килобайт.
__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-х дней. Уж слишком сложным он для меня оказался. С использованием таймера и различных структур.
Код примера из урока.
2. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Настройка проекта в keil и мигание светодиодом.
3. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Системный таймер (SysTick).
4. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Настройка тактовой частоты.
5. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC).
Комментарии (8)
Vadimatorikda Автор
21.04.2015 19:12Для меня, видимо… Ссылку указал. То, что вы описали, будет представлено в следующей статье. Хочется показать сначала реализацию от «что первое приходит на ум» до «как надо».
Vadimatorikda Автор
21.04.2015 19:18Для того, чтобы сделать то, что вы описали — нужно уметь работать с таймером. А пока что у меня не было такого урока. Следующим будет именно он.
Keroro
22.04.2015 09:39Тоже что-то подобное делал, только на PIC24. Там еще были отдельные векторы прерываний для случая, когда DMA прошел половину буфера, и случая, когда DMA закончил передачу. Можно было, получив INT, что половина пройдена, посчитать амплитуду\фазу для следующего периода синуса (DMA при этом продолжает выдавать вторую часть синуса), а получив сигнал об окончании текущего периода, перезарядить DMA на новую выдачу. Тут такого нет?
ЗЫ: а почему один из хабов статьи — Objective C?Vadimatorikda Автор
22.04.2015 13:59Тут такое есть (сказали ниже).
ЗЫ: а почему один из хабов статьи — Objective C?
Я вроде бы на C пишу… Значит и этот тег тоже выбрать следовало. Разве нет?Keroro
22.04.2015 18:22Это чисто Эппловское расширение Си. Между С и C# куда больше общего, чем между С и Objective C…
vertu77
22.04.2015 11:03+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 используется не только для канала ЦАП.
Sergei2405
Что то очень заумное использование DMA.
Я бы сделал запрос одиночных передач по DMA от Таймера 1.
В данном случае Таймер только генерирует запросы, а передачи идут из ОЗУ в DAC.
Запрос по переполнению Таймера, один отсчет ушел в DAC
Следующий запрос, еще один отсчет ушел в DAC и так далее по таблице 1024 раза (ну или 100 как вы любите).
После обнуления таблицы процессор ее регенерит за время между запросами от Таймера.
Изменяя частоту переполнения таймера можно легко выставлять битрейт.
Кстати, подобный пример уже делали для кого-то, и в службе тех поддержки он должен быть.
Обратитесь в support@mi....dr.ru