В прошлой статье нам удалось получить звук, но это очень дорого нам далось. Во первых, мы разогнали контроллер до максимальной скорости. А во вторых, кроме генерирования звука контроллер ничего не может, так как большая часть процессорного времени занята постоянным обновлением значения ЦАП-а. Не хорошо это. Именно сейчас остро стоит вопрос об использовании ДМА.
От идеи использовать DMA до получения первых результатов прошла неделя упорной работы. Первые 3 дня пытался освоить его сам, но никак не получалось получить хоть какой-то результат. Все получилось лишь после того, как на официальном форуме мне дали пример конфигурации DMA под примерно такую же задачу. Через 4 дня его подробного изучения и подробного анализа документации, в голове появилась ясная картина структуры работы DMA.
Открыв документацию я встал в ступор… Основная задача на начальном этапе освоения DMA — передать какое-нибудь значение в ЦАП. Ее и будем решать. В DMA есть так называемые «каналы». Они представляют из себя связку между приемником и передатчиком. В нашем случае между памятью и периферией (ЦАП). Какие могут быть связки — показано в таблице.
Как мы видим из таблицы — часть каналов зарезервированы под определенную периферию. ЦАП-а среди этой периферии нет. Остальную часть каналов можно использовать по своему назначению. Самым первым свободным каналом является канал 8. Его и будем настраивать. Но как? В документации есть раздел Правила обмена данными.
Сразу же найдем регистры, к которым принадлежат данные биты. Но прежде нужно подать сигнал тактирования на DMA.
После подачи тактирования, нам нужно включить DMA.
Следующим пунктом нужно включить бит chnl_enable_set[C]. Здесь C обозначает номер канала с нуля.
После необходимо установить в «0» chnl_req_mask_set [0].
Все бы хорошо, записали бы 0 и все, но…
Ладно.
Здесь нам уже нужно установить единицу на нужном нам канале.
Ну и последним шагом для нас должно стать запись нуля в бит int_test_en bit[8]. Но о существовании данного бита нигде не написано. Так что — пропускаем.
В дополнение присвоим нашему каналу высокий приоритет.
После включения канала, нужно определиться с режимом работы DMA.
Как оказалось, помимо регистров, DMA имеет еще и структуры-настройки. Честно сказать, очень долго вникал в принцип работы с этими структурами. Ранее, во времена STM32, я пользовался готовой библиотекой, потому что знаний языка не хватало для чтения документации. Теперь же, хоть и с определенным трудом, но я могу осознать весь принцип работы ДМА на низком уровне.
Согласно документации, структура должна быть оформлена в такой последовательности.
Начать предлагаю с заполнения ячейки настройки регистра.
Ячейку конфигурации канала мы настроили.
А вот следующий шаг отнял у меня почти 4 дня. Дело в том, что адрес каждой структуры строго фиксирован и может меняться лишь со смещением в килобайт.
Еще небольшое пояснение. Главное, чтобы по указанному адресу присутствовала нужная структура. Данные структур 7-го канала или же 9-го DMA никак не волнуют. Их может и не быть. Технически, можно записать в ОЗУ по указанным адресам четыре 32-х битных ячейки и пользоваться. Но есть риск, что контроллер изменит их в процессе выполнения программы. Заполним ее в программе.
Осталось только указать начальный адрес массива структур в регистре DMA -> CTRL_BASE_PTR.
Как мы помним, мы настроили DMA на остановку после каждой передачи. Теперь, с помощью системного таймера, нам нужно разрешать передачу следующего блока данных в DAC.
Далее в прерывании системного таймера нам нужно проверять — передал ли DMA все. Если да — то нужно настроить его структуру заново. Дело в том, что после каждой передачи DMA самостоятельно отнимает от количества передач по единице. Поэтому после всех передач — нужно восстановить изначальное значение для передачи синусоиды повторно. После этого нужно по новой разрешить работу канала (после передачи канал становиться запрещенным) и повторно запустить передачу.
Хоть нам и удалось научиться работать с DMA, но нам все равно еще не удалось разгрузить процессор. В следующей статье я разберу работу таймера и переложу работу с DMA на него, оставив мощности процессора для наших нужд.
Большое спасибо хочу сказать Yurock-у, который на официальном официальном форуме поделился примером кода конфигурации DMA под DAC. Изначально я планировал написать статью о разборе данного примера. Ибо разбирался я с ним около 3-х дней. Уж слишком сложным он для меня оказался. С использованием таймера и различных структур.
Код и аудио на github.
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 канала.Основной
В этом режиме контроллер работает только с основными или альтернативными управляющими данными канала. После того, как разрешена работа канала и контроллер получил запрос на обработку, цикл DMA выглядит следующим образом:
1. Контроллер выполняет 2^R передач. Если число оставшихся передач 0, контроллер переходит к шагу 3.
2. Осуществление арбитража:
— если высокоприоритетный канал выдает запрос на обработку, то контроллер начинает обслуживание этого канала;
— если периферийный блок или программное обеспечение выдает запрос на обработку (повторный запрос на обработку по каналу), то контроллер переходит к шагу 1.
3. Контроллер устанавливает dma_done[C] в состояние 1 на один такт сигнала hclk. Это указывает центральному процессору на завершение цикла DMA.
Структура работы DMA.
Как оказалось, помимо регистров, DMA имеет еще и структуры-настройки. Честно сказать, очень долго вникал в принцип работы с этими структурами. Ранее, во времена STM32, я пользовался готовой библиотекой, потому что знаний языка не хватало для чтения документации. Теперь же, хоть и с определенным трудом, но я могу осознать весь принцип работы ДМА на низком уровне.
Для каждого канала канала следует задать свою структуру. Она состоит из четырех 32-х битных ячеек.
Согласно документации, структура должна быть оформлена в такой последовательности.
— указатель конца данных источника;
— указатель конца данных приемника;
— разряды управления;
— вычисление адреса.
Заполнение структуры канала DMA
Начать предлагаю с заполнения ячейки настройки регистра.
Выбираем смещение адреса приемника (ЦАП).
У нас он не меняется. Источник и приемник имеют разрядность полуслово (16 бит). Наш случай:
Разрядность данных источника = полуслово:
b11 = нет инкремента. Адрес остается равным значению области памяти dst_data_end_ptr.
Выбираем размерность данных источника и приемника.
Здесь выбираем полуслово. Так как наш массив uint16_t (16 бит).Здесь выбираем такое же полуслово.
Разрешаем процедуру арбитража.
Вот этот пункт очень долго держал меня в неведение. Дело в том, что DMA может передавать не всю посылку сразу, а по частям. Например у нас есть массив в 1024 элемента. Но мы хотим передавать в секунду по 128 элементов. Для этого мы можем выставить b0111 и после передачи 128 элементов передача прервется до повторного запуска процессором или периферией. Это будет полезно, когда мы будем связывать DMA с таймером. В нашем случае мы оставляем нули. Так как нам нужно передавать каждый элемент в строго определенный момент. Простая передача всего массива нам не подходит.
Вот этот пункт очень долго держал меня в неведение. Дело в том, что DMA может передавать не всю посылку сразу, а по частям. Например у нас есть массив в 1024 элемента. Но мы хотим передавать в секунду по 128 элементов. Для этого мы можем выставить b0111 и после передачи 128 элементов передача прервется до повторного запуска процессором или периферией. Это будет полезно, когда мы будем связывать DMA с таймером. В нашем случае мы оставляем нули. Так как нам нужно передавать каждый элемент в строго определенный момент. Простая передача всего массива нам не подходит.
Задаем длину посылки.
В предыдущей статье мы передавали массив длинной в 100 элементов. Поэтому здесь мы выберем 100-1 элементов (Так как 0 = одному элементу).
В предыдущей статье мы передавали массив длинной в 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.
Список предыдущих статей.
1. Переходим с STM32F103 на К1986ВЕ92QI. Или первое знакомство с российским микроконтроллером.
2. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Настройка проекта в keil и мигание светодиодом.
3. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Системный таймер (SysTick).
4. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Настройка тактовой частоты.
5. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC).
2. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Настройка проекта в keil и мигание светодиодом.
3. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Системный таймер (SysTick).
4. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Настройка тактовой частоты.
5. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC).