Запуск DMA на К1986ВЕ92FI пошагово
Здравствуйте уважаемые читатели. Больше двух лет назад Миландр в связи с санкциями, попал в «блэк-лист» завода, производившего кристаллы разработанных микросхем. После этого поставки микросхем быстро сошли на нет, в том числе «народного» К1986ВЕ92QI в пластиковом корпусе. Больше года Миландр не подавал признаков жизни, однако работа кипела, кристалл 1986ВЕ9х был перепроектирован, год назад появились опытные образцы. Производство кристаллов осталось зарубежным (Микрон банально не умеет делать флеш), однако корпусировку гражданских кристаллов развернули в России. Поскольку производственных мощностей для корпусирования в QFP у Миландра нет, кристаллы стали паковать в QFN, которые не требуют опрессовки кристалла пластмассой. А к лету 2024 Миландр начал серийные поставки микросхем. Итак, гражданский микроконтроллер теперь обозначается К1986ВЕ92FI, его можно достать, а значит с ним снова можно работать.
Вопросы программирования 1986ВЕ92 многократно описаны, пути обхода ошибок давно изучены. Как говорили ещё в 2017 — «нормальный арм, хоть и с особенностями». Однако, несмотря на описание периферии и особенностей работы с ней, есть одно почти белое пятно, на котором многие спотыкаются — контроллер DMA прямого доступа к памяти.
Итак, контроллер прямого доступа к памяти (КПДП) — специализированное устройство, предназначенное для передачи массивов данных между памятью и периферией без участия процессорного ядра. КПДП по аппаратным запросам перферийного устройства, либо по программным запросам процессорного ядра осуществляет передачу данных, используя штатные шины адреса/данных/управления микроконтроллера. Для избежания конфликта ядро и КПДП подключены к шине АНВ через арбитр, при этом КПДП имеет приоритет обращения и может останавливать ядро на время передачи. К сожалению, описание КПДП в даташите на К1986ВЕ92FI довольно скупо на подробности и даже при решении простейших задач могут возникать сложности, казалось бы на ровном месте.
Одна из ростейших задач — циклический вывод массива данных из памяти в периферийное устройство. Например, мы строим генератор синусоидального сигнала и нам нужно выводить заранее рассчитанную синусную таблицу на цифроаналоговый преобразователь по сигналам таймера. Пусть, будет нужно генерировать синусоидальный сигнал с частотой 4кГц, для упрощения последующей фильтрации частоту дискретизации зададим 400кГц, тогда синусная таблица будет состоять из 100 точек. Выводить данные в регистр ЦАП с частотой 400кГц довольно затратно по машинному времени даже на частоте 80МГц из-за задержек реакции на прерывание и возврате из него. А ведь помимо вывода сигналов могут быть другие целевые задачи, в том числе требующие реального времени. Значит, пора задействовать прямой доступ к памяти!
Все настройки ПДП-пересылок хранятся массиве управляющих структур, хранящихся в оперативной памяти. В регистрах КПДП хранятся только базовые настройки, например, адрес массива управляющих структур. КПДП при передаче данных должен сперва загрузить управляющую структуру, прежде чем осуществить пересылку данных.
Диаграмма работы КПДП изображена на рисунке:
Диаграмма работы КПДП при одиночной передаче данных
В первом такте КПДП обнаруживает запрос на передачу данных, через пару тактов процессорное ядро освобождает шину и КПДП начинает работу:
RC — читает настройки канала.
RSP — читает конечный адрес данных источника.
RDP — читает конечный адрес данных приёмника.
RD — читает данные из источника.
WD — записывает данные в приёмник.
WC — записывает настройки канала.
Наконец, КПДП освобождает шину — в итоге одна пересылка ПДП занимает десять тактов шины. Это может показаться небыстрым, но процессорное ядро выполняет такую задачу ещё медленнее. Настройки канала при каждой пересылке изменяются, поэтому при окончании передачи массива данных настройки нужно восстанавливать.
Для упрощения задачи циклической передачи данных, КПДП предусматривает работу с двумя структурами данных — первичной и альтернативной, а также имеет режим работы «пинг-понг», позволяющий при условии восстановления настроек автоматически переключаться между ними. То есть, массив разбивается на две половины, настройки для первой половины записываются в первичную структуру, а настройки для второй половины пишутся в альтернативную структуру. По окончанию передачи массива КПДП вызывает прерывание и переключается на работу по новой структуре, во время прерывания можно успеть восстановить старую структуру, и наоборот. А можно и не разбивать массив пополам и в первичную, и в альтернативную структуры записать одинаковые настройки.
Итак, нам понадобится настроить не только таймер 1, ЦАП, КПДП и порт, но и модули SSP, которые будут в неактивном состоянии подавать ложные запросы ПДП. А если используется АЦП, то надо учитывать неотключаемые запросы ПДП при окончании преобразования! Включаем тактирование необходимых модулей:
MDR_RST_CLK ->PER_CLOCK = 0x23F44130; //тактирование всех портов, ПДП, ЦАП, SSP 1 и 2, таймера 1
MDR_RST_CLK ->TIM_CLOCK = 0x01000000; //разрешаем тактовую частоту на таймер 1
MDR_RST_CLK->SSP_CLOCK = 0x03000000; //разрешаем тактовую частоту на SSP 1 и 2
Настраиваем выход ЦАП на порту Е:
MDR_PORTE ->OE = 0x0001; //PE0-выход, остальные - входы
MDR_PORTE ->FUNC = 0x00000000; //режим работы - порт
MDR_PORTE ->ANALOG = 0xFFFE; //контакт PE0 аналоговый
MDR_PORTE ->PULL = 0x00CEFF30; //подтяжка PE2-PE3, PE6, PE7 к питанию, неразваренные PE4, PE5, PE8-PE15 к земле
MDR_PORTE ->PD = 0xFFFF0000; //триггер шмитта 400мВ или управляемый драйвер
MDR_PORTE ->PWR = 0x00000001; //выход только РЕ0
Настраиваем таймер 1:
MDR_TIMER1 ->CNT = 0x0000; //сброс основного счётчика
MDR_TIMER1 ->PSG = 0x0000; //предделитель 1/1
MDR_TIMER1 ->ARR = 199; //основание делителя равно 200, частота 400 кГц
MDR_TIMER1 ->CNTRL = 0x0002; //режим счёта - прямой, таймер вырублен!
MDR_TIMER1 ->BRKETR_CNTRL = 0x0000; //BRK ETR не используются
MDR_TIMER1 ->STATUS = 0x0000; //гасим флаги
MDR_TIMER1 ->IE = 0x00000000; //прерывания не используются
MDR_TIMER1 ->DMA_RE = 0x00000001; //запрос ПДП при переходе через ноль
Настраиваем ЦАП:
MDR_DAC ->CFG = 0x0008; //ЦАП асинхронные, опора ЦАП2 от аналогового питания
MDR_DAC ->DAC2_DATA = 0x0000; //обнуляем ЦАП2
Периферия настроена, теперь настраиваем КПДП. Объявляем структуры управляющих данных и передаваемый массив данных:
struct dma {
unsigned int DEP;//Указатель конца данных приемника
unsigned int SEP;//Указатель конца данных источника
unsigned int CFG;//Конфигурация канала
unsigned int NULL;//Пустая ячейка
}
dma __attribute__ ( ( aligned ( 1024 ) ) );//Выравниваем массив структур по 1024 байта
struct dma dmas [ 32 + 32 ];//Создаем массив структур для всех каналов
unsigned short dac_array [ 100 ] = {//таблица для вывода в ЦАП
2048,2176,2304,2431,2557,2680,2801,2919,3034,3145,3251,3353,3449,3540,3625,3704,
3776,3842,3900,3951,3995,4031,4059,4079,4091,4095,4091,4079,4059,4031,3995,3951,
3900,3842,3776,3704,3625,3540,3449,3353,3251,3145,3034,2919,2801,2680,2557,2431,
2304,2176,2048,1919,1791,1664,1538,1415,1294,1176,1061,950,844,742,646,555,
470,391,319,253,195,144,100,64,36,16,4,0,4,16,36,64,
100,144,195,253,319,391,470,555,646,742,844,950,1061,1176,1294,1415,
1538,1664,1791,1919
};
Настраиваем управляющие структуры, пустые ячейки .NULL можно не заполнять, всё равно КПДП к ним не имеет доступа:
//настройка первичной структуры
dmas [ 9 ].DEP = ( unsigned int )&dac_array + ( 99 << 1 );
dmas [ 9 ].SEP = ( unsigned int )&( MDR_DAC ->DAC2_DATA );
dmas [ 9 ].CFG = ( ( 3 << 30 )//источник - 16 бит (полуслово)
| ( 1 << 28 )//принимаем по 16 бит, приемник и передатчик должны иметь одинаковые размерности
| ( 1 << 26 )//источник смещается на 16 бит после каждой передачи
| ( 1 << 24 )//отправляем по 16 бит
| ( 0 << 14 )//арбитраж производится после каждой передачи ПДП
| ( 99 << 4 )//100 передач ПДП
| ( 3 << 0 ) );//режим работы пинг-понг
//настройка альтернативной структуры
dmas [ 41 ].DEP = ( unsigned int )&dac_array + ( 99 << 1 );
dmas [ 41 ].SEP = ( unsigned int )&( MDR_DAC ->DAC2_DATA );
dmas [ 41 ].CFG = ( ( 3 << 30 )//источник - 16 бит (полуслово)
| ( 1 << 28 )//принимаем по 16 бит, приемник и передатчик должны иметь одинаковые размерности
| ( 1 << 26 )//источник смещается на 16 бит после каждой передачи
| ( 1 << 24 )//отправляем по 16 бит
| ( 0 << 14 )//арбитраж производится после каждой передачи ПДП
| ( 99 << 4 )//100 передач ПДП
| ( 3 << 0 ) );//режим работы пинг-понг
Наконец, настраиваем собственно контроллер ПДП:
MDR_DMA ->CFG = 0x0001; //разрешаем работу КПДП
MDR_DMA ->CTRL_BASE_PTR = ( unsigned int )&dmas;//задаём базовый адрес первичной структуры
MDR_DMA ->ALT_CTRL_BASE_PTR = ( ( unsigned int )&dmas ) + 512;//задаём базовый адрес альтернативной структуры
MDR_DMA ->CHNL_SW_REQUEST = 0x00000000; //не используется
MDR_DMA ->CHNL_USEBURST_SET = 0x00000000; //не используется
MDR_DMA ->CHNL_USEBURST_CLR = 0x00000000; //не используется
MDR_DMA ->CHNL_REQ_MASK_SET = 0xFFFFBFF; //маскируем ненужные каналы
MDR_DMA ->CHNL_REQ_MASK_CLR = 0x00000400; //снимаем маску с нужного канала
MDR_DMA ->CHNL_ENABLE_SET = 0x00000400; //разрешаем работу с таймером 1
MDR_DMA ->CHNL_ENABLE_CLR = 0xFFFFBFF; //запрещаем остальные каналы
MDR_DMA ->CHNL_PRI_ALT_SET = 0x00000400; //разрешаем использовать первичную структуру для нужного канала
MDR_DMA ->CHNL_PRI_ALT_CLR = 0x00000000; //не используется
MDR_DMA ->CHNL_PRIORITY_SET = 0x00000400; //устанавливаем высокий приоритет для нужного канала
MDR_DMA ->CHNL_PRIORITY_CLR = 0xFFFFBFF; //и низкий приоритет для остальных
MDR_DMA ->ERR_CLR = 0x00000000; //сброс ошибок
NVIC_ClearPendingIRQ(DMA_IRQn);//сброс флага прерывания
NVIC_EnableIRQ(DMA_IRQn);//разрешаем прерывание от КПДП
Поскольку, КПДП при работе меняет только настройки канала, не трогая при этом адреса источника и приёмника данных, то в функции прерывания по завершении передачи данных достаточно только восстановить настройки канала:
void DMA_IRQHandler() {
if ( ( MDR_DMA ->CHNL_PRI_ALT_SET & 0x00000400 ) != 0x00000400 ){
//если КПДП работает с альтернативной структурой, то перенастраиваем первичную
dmas [ 9 ].CFG = ( ( 3 << 30 )//источник - 16 бит (полуслово)
| ( 1 << 28 )//принимаем по 16 бит, приемник и передатчик должны иметь одинаковые размерности
| ( 1 << 26 )//источник смещается на 16 бит после каждой передачи
| ( 1 << 24 )//отправляем по 16 бит
| ( 0 << 14 )//арбитраж производится после каждой передачи ПДП
| ( 99 << 4 )//100 передач ПДП
| ( 3 << 0 ) );//режим работы пинг-понг
}
else {
//если КПДП работает с первичной структурой, то перенастраиваем альтернативную
dmas [ 41 ].CFG = ( ( 3 << 30 )//источник - 16 бит (полуслово)
| ( 1 << 28 )//принимаем по 16 бит, приемник и передатчик должны иметь одинаковые размерности
| ( 1 << 26 )//источник смещается на 16 бит после каждой передачи
| ( 1 << 24 )//отправляем по 16 бит
| ( 0 << 14 )//арбитраж производится после каждой передачи ПДП
| ( 99 << 4 )//100 передач ПДП
| ( 3 << 0 ) );//режим работы пинг-понг
}
NVIC_ClearPendingIRQ(DMA_IRQn);//сброс флага прерывания
}
Под конец, не забываем запустить таймер:
MDR_TIMER1 ->CNTRL |= ( 1 << 0 );//включаем таймер1
MDR_TIMER1 ->STATUS = 0;//гасим флаги
В результате нашей работе получаем такой сигнал на выходе ЦАП:
Осциллограмма на выходе ЦАП К1986ВЕ92FI
Готово, вывод сигнала в ЦАП силами КПДП заработал, и почти без участия процессорного ядра. Прерывание по завершению передачи ПДП вполне можно использовать для задач реального времени вместо прерывания от таймера. В статье рассмотрен самый простейший пример работы с контроллером прямого доступа к памяти К1986ВЕ92FI, с которого можно начать ознакомление с ПДП-пересылками в микроконтроллерах фирмы Миландр.