Вступление

В предыдущей статье я рассказал о своем первом знакомстве с DMA. В ней мы делали связку DMA + SysTick. Статья получилась очень специфичной и сложной, ввиду неопытного кривого подхода. Набравшись опыта, в данной статье я расскажу о куда более простом и понятном способе работы с DMA.

Основные аспекты

Из предыдущей статьи мы уяснили, что для того, чтобы запустить DMA, нужно:
  • Подать сигнал тактирвания на модуль DMA.
  • Заполнить управляющую структуру работы DMA.
  • Произвести настройку DMA.
  • Произвести настройку канала.
  • Включить передачу.
  • По окончании передачи «восстановить» (перезаписать) структуру.
Для того, что бы «выдавать» данные в строго определенное время — мы пользовались системным таймером SysTick. Из-за этого нам приходилось постоянно заходить в прерывание от таймера и там повторно запускать каждую пересылку DMA. Кроме того, при каждом повторном запуске передачи, мы еще и проверяли, передался ли весь пакет наших данных или нет. Согласитесь, подобный подход напрочь убивает все плюсы DMA. Можно сказать, даже усложняет жизнь. Как оказалось, в нашем микроконтроллере есть замечательная связка DMA+Таймер. Таким образом, мы можем 1 раз настроить таймер так, чтобы без входа в прерывание он сам говорил DMA, что пора отправить следующее значение в DAC. Так же вспомним, что ранее мы использовали режим передачи «основной». Он нас заставлял постоянно останавливаться и восстанавливать структуру. В нашем DMA есть режим «пинг-понг». Его суть такова: используя первичную структуру (ту, что мы использовали в предыдущей статье), мы передаем первую часть данных (массива). В это время мы можем настроить альтернативную (читай, вторую) структуру так, чтобы когда закончится выполнение первой, мы без остановки сразу начали передавать то, что записано во второй. Во время передачи по второй структуре — мы можем восстановить или изменить первую, что дает нам возможность делать сколь угодно длинные передачи.

Настройка структуры DMA, самого 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 n_minus_1     (49<<4)                      //50  передач (-1) DMA. 
#define cycle_ctrl    (3<<0)                       //Пинг-понг.

struct DAC_ST
{
	uint32_t Destination_end_pointer;                                     //Указатель конца данных приемника.
	uint32_t Source_end_pointer;                                          //Указатель конца данных источника
	uint32_t channel_cfg;                                                 //Конфигурация канала.
	uint32_t NULL;                                                        //Пустая ячейка. 
} 

__align(1024) DAC_ST;                                                   //Выравниваем массив структур по 1024 байта. 
struct DAC_ST DAC_ST_ADC[32+32];                                        //Создаем массив структур для всех каналов. 

//Источник/приемник = 16 бит, отправляем/принимаем = 16 бит, защиты нет, 50 передач, пинг-понг.
uint32_t DMA_DAC_InitST_PR  = dst_src|src_inc|src_size|dst_size|n_minus_1|cycle_ctrl;
uint32_t DMA_DAC_InitST_ALT = dst_src|src_inc|src_size|dst_size|n_minus_1|cycle_ctrl;	
Как вы могли заметить — я выделил память под 32 первичные и 32 альтернативные структуры, что в сумме будет давать килобайт памяти в ОЗУ. Я пошел на данный шаг, чтобы не мучиться со смещениями. В будущем легко будет сделать смещение и оставить 2 структуры. Далее нужно заполнить эти структуры.
Заполнение структур
//Настраиваем первичную структуру.
  DAC_ST_ADC[10-1].Destination_end_pointer = (uint32_t)C_4 + (sizeof(C_4))/2 - 1;       //Указатель на последний элемент середины массива (C_4 - массив значений синусоидального сигнала в 100 значений).
  DAC_ST_ADC[10-1].Source_end_pointer = (uint32_t)&(DAC->DAC2_DATA);                    //Указатель на последний (не меняется) адрес приемника (регистр данных DAC).
  DAC_ST_ADC[10-1].channel_cfg = (uint32_t)(DMA_DAC_InitST_PR);                         //Структура настройки первичной структуры.
	DAC_ST_ADC[10-1].NULL = (uint32_t)0;                                                  //Пустая ячейка.
	//Настройка альтернативной структуры. 
  DAC_ST_ADC[10-1+32].Destination_end_pointer = (uint32_t)C_4 + sizeof(C_4) - 1;        //Указатель на последний элемент массива (C_4 - массив значений синусоидального сигнала в 100 значений).
  DAC_ST_ADC[10-1+32].Source_end_pointer = (uint32_t)&(DAC->DAC2_DATA);                 //Указатель на последний (не меняется) адрес приемника (регистр данных DAC).
  DAC_ST_ADC[10-1+32].channel_cfg = (uint32_t)(DMA_DAC_InitST_ALT);                     //Структура настройки альтернативной структуры.
DAC_ST_ADC[10-1+32].NULL = (uint32_t)0;                                               //Пустая ячейка.
Как вы могли заметить, разница у этих двух структур лишь в конечных адресах источника данных. В первичной мы указываем центр массива (передача будет осуществляться с начала до середины), а во вторичной конец (передача с середины до конца).
Теперь настроим DMA.
#define CFG_master_enable                    (1<<0)//Маска разрешает работу контроллера.
#define PCLK_EN_DMA                          (1<<5)//Маска включает тактирование DMA.
//Настраиваем контроллер DMA.
  RST_CLK->PER_CLOCK|=PCLK_EN_DMA;                                                      //Включаем тактирование DMA.
  DMA->CTRL_BASE_PTR = (uint32_t)&DAC_ST_ADC;                                           //Указываем адрес массива структур. 
  DMA->CFG = CFG_master_enable;                                                         //Разрешаем работу DMA.
Ну и осталось лишь настроить канал. Но какой? Мы можем обратиться к таблице, приведенной в предыдущей статье. Но. После написания предыдущей статьи мне сообщили о моей ошибке. Дело в том, что та таблица не совсем верна.
Более точная таблица выглядит так.
/** @defgroup DMA_valid_channels DMA valid channels
  * @{
  */
#define DMA_Channel_UART1_TX    ((uint8_t)(0))
#define DMA_Channel_UART1_RX    ((uint8_t)(1))
#define DMA_Channel_UART2_TX    ((uint8_t)(2))
#define DMA_Channel_UART2_RX    ((uint8_t)(3))
#define DMA_Channel_SSP1_TX     ((uint8_t)(4))
#define DMA_Channel_SSP1_RX     ((uint8_t)(5))
#define DMA_Channel_SSP2_TX     ((uint8_t)(6))
#define DMA_Channel_SSP2_RX     ((uint8_t)(7))
#define DMA_Channel_ADC1        ((uint8_t)(8))
#define DMA_Channel_ADC2        ((uint8_t)(9))
#define DMA_Channel_TIM1        ((uint8_t)(10))
#define DMA_Channel_TIM2        ((uint8_t)(11))
#define DMA_Channel_TIM3        ((uint8_t)(12))
#define DMA_Channel_SW1         ((uint8_t)(13))
#define DMA_Channel_SW2         ((uint8_t)(14))
#define DMA_Channel_SW3         ((uint8_t)(15))
#define DMA_Channel_SW4         ((uint8_t)(16))
#define DMA_Channel_SW5         ((uint8_t)(17))
#define DMA_Channel_SW6         ((uint8_t)(18))
#define DMA_Channel_SW7         ((uint8_t)(19))
#define DMA_Channel_SW8         ((uint8_t)(20))
#define DMA_Channel_SW9         ((uint8_t)(21))
#define DMA_Channel_SW10        ((uint8_t)(22))
#define DMA_Channel_SW11        ((uint8_t)(23))
#define DMA_Channel_SW12        ((uint8_t)(24))
#define DMA_Channel_SW13        ((uint8_t)(25))
#define DMA_Channel_SW14        ((uint8_t)(26))
#define DMA_Channel_SW15        ((uint8_t)(27))
#define DMA_Channel_SW16        ((uint8_t)(28))
#define DMA_Channel_SW17        ((uint8_t)(29))
#define DMA_Channel_SW18        ((uint8_t)(30))
#define DMA_Channel_SW19        ((uint8_t)(31))
#define IS_DMA_CHANNEL(CHANNEL) (CHANNEL <= (DMA_Channels_Number - 1))

/** @} */ /* End of group DMA_valid_channels */
Таблицу мне дали на официальном форуме, но она есть и в официальной библиотеке. Мы будем использовать таймер 1. => наш канал — десятый.
//Настраиваем канал. 
	DMA->CHNL_ENABLE_SET   = 1<<10;                                                       //Разрешаем работу 10 канала.
Мы получили следующую функцию.
//-------------------------------------------------
//Настраиваем DMA для связки с DAC.
//-------------------------------------------------
void DMA_to_DAC_and_TIM1 (void) 
{
	//Настраиваем первичную структуру.
  DAC_ST_ADC[10-1].Destination_end_pointer = (uint32_t)C_4 + (sizeof(C_4))/2 - 1;       //Указатель на последний элемент середины массива (C_4 - массив значений синусоидального сигнала в 100 значений).
  DAC_ST_ADC[10-1].Source_end_pointer = (uint32_t)&(DAC->DAC2_DATA);                    //Указатель на последний (не меняется) адрес приемника (регистр данных DAC).
  DAC_ST_ADC[10-1].channel_cfg = (uint32_t)(DMA_DAC_InitST_PR);                         //Структура настройки первичной структуры.
	DAC_ST_ADC[10-1].NULL = (uint32_t)0;                                                  //Пустая ячейка.
	//Настройка альтернативной структуры. 
  DAC_ST_ADC[10-1+32].Destination_end_pointer = (uint32_t)C_4 + sizeof(C_4) - 1;        //Указатель на последний элемент массива (C_4 - массив значений синусоидального сигнала в 100 значений).
  DAC_ST_ADC[10-1+32].Source_end_pointer = (uint32_t)&(DAC->DAC2_DATA);                 //Указатель на последний (не меняется) адрес приемника (регистр данных DAC).
  DAC_ST_ADC[10-1+32].channel_cfg = (uint32_t)(DMA_DAC_InitST_ALT);                     //Структура настройки альтернативной структуры.
	DAC_ST_ADC[10-1+32].NULL = (uint32_t)0;                                               //Пустая ячейка.
  
	//Настраиваем контроллер DMA.
  RST_CLK->PER_CLOCK|=PCLK_EN_DMA;                                                      //Включаем тактирование DMA.
  DMA->CTRL_BASE_PTR = (uint32_t)&DAC_ST_ADC;                                           //Указываем адрес массива структур. 
  DMA->CFG = CFG_master_enable;                                                         //Разрешаем работу DMA.
	
	//Настраиваем канал. 
	DMA->CHNL_ENABLE_SET   = 1<<10;                                                       //Разрешаем работу 10 канала.
}

Знакомство с таймером

Прежде всего таймер нужно включить (затактировать).

#define PER_CLOCK_TIMER1_ONCLK             (1<<14) //Маска разрешения тактирования таймера 1.
RST_CLK->PER_CLOCK |= PER_CLOCK_TIMER1_ONCLK;                   //Включаем тактирование.
А теперь собственно, стоит начать разбираться. Все три таймера имеют одинаковые возможности. По крайней мере на первый взгляд. Каждый таймер имеет очень богатый функционал => множество регистров. Но в них очень легко разобраться. Никаких граблей мне на пути изучения работы таймеров не встретилось. Мы помним, что мы настроили DMA так, чтобы таймер мог управлять им. Для этой цели нам достаточно будет лишь подождать определенное количество времени и передать следующую порцию данных. Таймер имеет 4 канала «сравнения» и основной счетчик. Нам будет достаточно использовать основной счетчик.
Взглянем на основной регистр таймера.
Здесь нам следует разрешить работу таймера.

#define CNTRL_CNT_EN                       (1<<0)  //Маска включения таймера.
TIMER1->CNTRL |= CNTRL_CNT_EN;                  //Разрешаем работу таймера, событие = равенство значению.
В качестве теста — будем считать до 0xFFFF (в последствии разберемся с временными интервалами). По умолчанию счет идет от 0.
TIMER1->ARR = 0xFFFF;                                           //Считаем до...
Далее нужно связать наш таймер с DMA. В качества события, из-за которого будет происходить передача — выберем CNT == ARR в таймере 1. Это простое сравнение текущего значения таймера CNT с числом в регистре ARR.
Для этого есть регистр DMA_RE
Здесь нам нужно выбрать связь по достижению счетчиком нужного значения.
Можно было бы сказать, что настройка завершена, НО. Сейчас наш таймер тактируется от частоты HCLK, которая без пред делителя = 8МГц. Наше значение 0xFFFF будет достигнуто мгновенно. И мы не сможем проследить нашу передачу. Для решения этой проблемы мы должны включить пред делитель.
Для этих целей служит регистр RST_CLK->TIM_CLOCK в блоке тактирования.
Для пробы я включил самый большой делитель. Так же здесь следует и подать сигнал тактирования на таймер. Чтобы таймер начал считать от HCLK через пред делитель.
В результате мы получаем такую функцию.
#define PER_CLOCK_TIMER1_ONCLK             (1<<14) //Маска разрешения тактирования таймера 1.
#define TIM_CLOCK_TIM1_CLK_EN              (1<<24) //Маска включения частоты на таймере 1.
#define SHARE_HCLK_TIMER                    7      //Делим тактовую частоту HCLK на... (0 = не делим, 1 = /2, 2 = /4).
#define TIM_CLOCK_TIM1_BRG   (SHARE_HCLK_TIMER<<0) //Делим частоту на таймере 1 на SHARE_HCLK_TIMER.
#define CNTRL_CNT_EN                       (1<<0)  //Маска включения таймера.
#define DMA_RE_CNT_ARR_EVENT_RE            (1<<1)  //Маска разрешения запроса DMA при событии CNT == ARR;

void Init_TIMER1_to_DMA_and_DAC2 (void)
{
	RST_CLK->PER_CLOCK |= PER_CLOCK_TIMER1_ONCLK;                   //Включаем тактирование.
	TIMER1->CNTRL |= CNTRL_CNT_EN;                  //Разрешаем работу таймера, событие = равенство значению.
	TIMER1->ARR = 0xFFFF;                                           //Считаем до...
	TIMER1->DMA_RE |= DMA_RE_CNT_ARR_EVENT_RE;                      //Разрешаем "пинать" DMA.
	RST_CLK->TIM_CLOCK |= TIM_CLOCK_TIM1_CLK_EN|TIM_CLOCK_TIM1_BRG; //Подаем тактовый сигнал на таймер.
}
Теперь, если мы включим наш пример, то мы будем наблюдать, как происходит смена значений в регистре значения напряжения в DAC2 с интервалом примерно секунда. Чтож, первый этап пройден. Правда вот процесс прервется на середине нашего массива. Дело в том, что таймер может лишь повторно «включать» передачу. Но как только мы передали половину массива — мы «израсходовали» первую структуру. Сейчас необходимо ее восстановить. Для этого мы будем использовать второй таймер. Многие спросят «Почему не прерывание по передаче от DMA или по передаче половины массива?». Дело в том, что в наш DMA может генерировать лишь прерывание по окончании передачи. Возможности вызывать прерывание по прохождении середины нет. Но и тут не все так просто. Вспомним, что мы передаем не все подряд, а по частям. Наш DMA этого не понимает. Прерывание генерируется во время простоя DMA. Иначе говоря, DMA не понимает, что передан не весь пакет данных, а лишь один элемент массива. Этот факт делает использование прерывания от DMA для нашей задачи непригодным.

Настраиваем таймеры для генерации синусоиды

Как я уже сказал, для «восстановления» структуры мы должны настроить в следующем (втором) таймере прерывание по прохождении половины половины массива (1/4 всего массива). Почему именно одной четвертой — объясню далее. Но перед этим перенастроим первый таймер. Нам нужно определиться со скоростью «пинков» DMA. Смотрим. Контроллер тактируется с частотой 8МГц. Наш массив содержит 100 элементов. Частота ноты до первой октавы, как мы помним, 261.63 Гц. Вместимость регистра счета 0xFFFF (до этого значения может считать счетчик). Делим 8000000 Гц/261.63Гц/100 нот = раз в 305 тактов «пинать» DMA. Это много меньше максимального значения регистра сравнения таймера. Таким образом, нам не придется использовать пред делитель.
Подаем тактирование на оба таймера.
RST_CLK->PER_CLOCK |= PER_CLOCK_TIMER1_ONCLK|PER_CLOCK_TIMER2_ONCLK;                   //Включаем тактирование таймера 1 и 2.
После включения тактирования таймера (всего блока) иногда случается, что таймер начинают свою работу по ранее настроенным параметрам. Из-за этого, еще до их настройки, бывают различные глюки. Чтобы это предотвратить нужно отключить сигнал тактирования счетного блока (чтобы таймер по старым параметрам не успел досчитать до какого-нибудь значения, от которого вызовет, к примеру прерывание).
Отключаем тактирование счетного блока.
RST_CLK->TIM_CLOCK = 0;                                                                //Отключаем тактирование таймеров.
Далее указываем период обращения к DMA и разрешаем его.
//У нас 8000000 Гц/261.63/100 = 305.
	TIMER1->ARR = 305;                                                                     //Считаем до...
	TIMER1->DMA_RE |= DMA_RE_CNT_ARR_EVENT_RE;                                             //Разрешаем пинать DMA.
Теперь настраиваем таймер 2. Для простоты, тактировать его будем так же без пред делителя. Так же решаем, как часто вызывать прерывание. В следствие того, что мы тактируем оба таймера от одного источника, мы не можем обновлять значение структуры в тот самый момент, когда был передан последний элемент первой структуры. Потому что когда DMA сделает попытку передать элемент, следующий за последним из первой структуры — он наткнется на ее конец и автоматически перейдет к альтернативной. Таким образом, нужно дождаться, когда DMA решит, что первичная структура «исчерпана» и начнет передавать из альтернативной. Для этого достаточно дождаться передачи хотя бы одного пакета из альтернативной структуры. После чего можно будет заполнить первичную заново. Тоже самое нужно будет делать и с альтернативной. Когда в ней закончатся передачи, нужно дождаться, пока DMA передаст хоть 1 пакет из первичной, после чего можно перезаписать альтернативную. Мы передаем 100 элементов. По 50 в каждой структуре. Мы могли бы вызывать прерывание вместе с пятидесятой передачей, но по описанным выше причинам, нам нужно дождаться хотя бы 51-й передачи. Для того, чтобы не мучиться со смещениями, сделаем проверку обеих структура раз в 25 передач.
Таким образом, период нашего таймер будет 305*25.
TIMER2->ARR = 305*25;                                                                  //После передачи половины массива.

Немного о прерываниях

Для того, чтобы случилось прерывание в момент достижения таймером нужного значения, нужно.
  • Разрешить прерывание по событию: достижение счетчиком нужного значения, в регистре TIMERx->IE
    #define TIMERx_IE_CNT_ARR_EVENT_IE         (1<<1)  //Маска: Флага разрешения прерывания по событию совпадения CNT и ARR.
    TIMER2->IE  = TIMERx_IE_CNT_ARR_EVENT_IE;                                         //Прерывание по достижении границы.
  • Сбросить флаг наличия каких-либо прерываний TIMERy->STATUS
    Дело в том, что после разрешения прерывания — автоматически устанавливается его флаг. Чтобы сразу же после настройки не уйти в прерывание — нужно его сбросить.
    TIMER2->STATUS=0;                                                   //Сбрасываем флаги прерываний.
  • Разрешить прерывание от таймера в контроллере прерываний NVIC.
    На тему этого контроллера стоит выделить отдельную статью, но в рамках данной статьи ограничимся следующей информационной базой: для того, чтобы контроллер «заметил», что таймер требует прерывания, нужно разрешить это самое прерывание. Делается это в регистре NVIC->ISER[0]. Нам нужно узнать, какой номер имеет прерывание от таймера. Для этого заходим в стартап файл и ищем вектор с нужным именем.
    DCD     Timer2_IRQHandler          ; IRQ15
    NVIC->ISER[0] = 1<<15;    //Разрешили прерывание от таймера 2.
  • Нужно создать функцию-обработчик прерывания и сбросить в ней флаг прерывания (это делается вручную).
    void Timer2_IRQHandler (void)                                                           //Меняем структуры.
    	TIMER2->STATUS=0;    
    }

Заканчиваем настройку таймеров.

После того, как мы указали параметры таймеров, нужно разрешить их работу и подать тактовый сигнал на счетчик.

#define CNTRL_CNT_EN                       (1<<0)  //Маска включения таймера.
#define TIM_CLOCK_TIM1_CLK_EN              (1<<24) //Маска включения частоты на таймере 1.
#define TIM_CLOCK_TIM2_CLK_EN              (1<<25) //Маска включения частоты на таймере 2.
	TIMER1->CNTRL = CNTRL_CNT_EN;                                                          //Разрешаем работу таймеров.
	TIMER2->CNTRL = CNTRL_CNT_EN;      
	RST_CLK->TIM_CLOCK = TIM_CLOCK_TIM1_CLK_EN|TIM_CLOCK_TIM2_CLK_EN;                      //Подаем тактовый сигнал на таймеры.
Итогом настройки стала функция.
#define PER_CLOCK_TIMER1_ONCLK             (1<<14) //Маска разрешения тактирования таймера 1.
#define PER_CLOCK_TIMER2_ONCLK             (1<<15) //Маска разрешения тактирования таймера 1.
#define TIM_CLOCK_TIM1_CLK_EN              (1<<24) //Маска включения частоты на таймере 1.
#define SHARE_HCLK_TIMER1                   7      //Делим тактовую частоту HCLK на... (0 = не делим, 1 = /2, 2 = /4).
#define TIM_CLOCK_TIM1_BRG   (SHARE_HCLK_TIMER1<<0) //Делим частоту на таймере 1 на SHARE_HCLK_TIMER.
#define CNTRL_CNT_EN                       (1<<0)  //Маска включения таймера.
#define CNTRL_EVENT_SEL                    (1<<8)  //Маска выбора сточник события: CNT == ARR;
#define DMA_RE_CNT_ARR_EVENT_RE            (1<<1)  //Маска разрешения запроса DMA при событии CNT == ARR;
#define IE_CNT_ARR_EVENT_IE                (1<<1)  //Маска разрешения прерывания при событии CNT == ARR;
#define TIM_CLOCK_TIM2_CLK_EN              (1<<25) //Маска включения частоты на таймере 2.
#define SHARE_HCLK_TIMER2                   7      //Делим тактовую частоту HCLK на... (0 = не делим, 1 = /2, 2 = /4).
#define TIM_CLOCK_TIM2_BRG   (SHARE_HCLK_TIMER2<<8)//Делим частоту на таймере 1 на SHARE_HCLK_TIMER.

#define CH1_CNTRL_CAP_nPWM_Z               (1<<15) //Маска: какнал в режиме "захват". 
#define CH1_CNTRL_CHPSC_8                  (3<<6)  //Маска: предворительный делитель для канала на 8.
#define CHy_CNTRL2_CCR1_EN                 (1<<2)  //Маска: включить канал 1.
#define TIMERx_IE_CNT_ARR_EVENT_IE         (1<<1)  //Маска: Флага разрешения прерывания по событию совпадения CNT и ARR.
void Init_TIMER1_to_DMA_and_DAC2 (void)
{
RST_CLK->PER_CLOCK |= PER_CLOCK_TIMER1_ONCLK|PER_CLOCK_TIMER2_ONCLK;                   //Включаем тактирование таймера 1 и 2.
RST_CLK->TIM_CLOCK = 0;                                                                //Отключаем тактирование таймеров.
	
  //У нас 8000000 Гц/261.63/100 = 305.
TIMER1->ARR = 305;                                                                     //Считаем до...
TIMER1->DMA_RE |= DMA_RE_CNT_ARR_EVENT_RE;                                             //Разрешаем пинать DMA.

TIMER2->ARR = 305*25;                                                                  //После передачи половины массива.
TIMER2->IE  = TIMERx_IE_CNT_ARR_EVENT_IE;                                              //Прерывание по достижении границы. 
TIMER2->STATUS=0;                                                                      //Сбрасываем флаги прерываний. 
NVIC->ISER[0] = 1<<15;                                                                 //Разрешили прерывание от таймера 2.
	
TIMER1->CNTRL = CNTRL_CNT_EN;                                                          //Разрешаем работу таймеров.
TIMER2->CNTRL = CNTRL_CNT_EN;      
	
RST_CLK->TIM_CLOCK = TIM_CLOCK_TIM1_CLK_EN|TIM_CLOCK_TIM2_CLK_EN;                      //Подаем тактовый сигнал на таймеры.
}

Пишем функцию смены структур.

Мы описали прерывание. Осталось лишь проверить в нем: если закончилась первичная структура и был передан хотя бы 1 блок из альтернативной структуры — перезаписать первичную. Тоже самое со вторичной. Не забудем так же, что после передачи первой структуры — DMA блокирует канал. Нужно снова разрешить его работу, чтобы по «пинкам» от таймера 1 продолжала идти передача.
Получившееся прерывание
#define ST_Play_P           (DAC_ST_ADC[10-1].channel_cfg    & (1023<<4))               //Для проверки колличества оставшихся передачь в первичной сруктуре.
#define ST_Play_ALT         (DAC_ST_ADC[10-1+32].channel_cfg & (1023<<4))               //Для проверки колличества оставшихся передачь в альтернативной сруктуре.
void Timer2_IRQHandler (void)                                                           //Меняем структуры.
{
	if ((ST_Play_P == 0) && (ST_Play_ALT <= (48<<4))) //Если прошли первую половину и уже начата передача второй - переключиться на 2-ю.	
  	DAC_ST_ADC[10-1].channel_cfg = (uint32_t)(DMA_DAC_InitST_PR); 
	if ((ST_Play_ALT == 0) && (ST_Play_P <= (48<<4)))
  	DAC_ST_ADC[10-1+32].channel_cfg = (uint32_t)(DMA_DAC_InitST_ALT); 
	DMA->CHNL_ENABLE_SET   = 1<<10; 
	TIMER2->STATUS=0;    
}
Код основной функции с последнего примера изменяется лишь включением новой функции и выключения SysTick.
Вот он.
int main (void)
{
  HSE_Clock_ON();                                  //Разрешаем использование HSE генератора. 
  HSE_Clock_OffPLL();                              //Настраиваем "путь" сигнала и включаем тактирование от HSE генератора.
  Buzzer_out_DAC_init();                           //Настраиваем порт для ЦАП.
  DAC_Init();                                      //Настраиваем ЦАП.
	DMA_to_DAC_and_TIM1();                           //Настраиваем DMA для работы с DAC2 через таймер 1.
	Init_TIMER1_to_DMA_and_DAC2();                   //Настраиваем таймер 1.
	
  while (1) 
		{                    
		}
}
Запустив программу мы получим на выходе следующую картину.Скачать звук можно здесь. Скачать код примера тут.

Об ошибках

Долго мучился с подбором момента для смены структур. Все время выходило что-то типа этого.

Подведем итог

Нам удалось иначе взглянуть на DMA, начать пользоваться его преимуществами. В следующей статье мы закрепим наши умения и создадим подобие музыкальной шкатулки, а после — плеера.

Комментарии (7)


  1. Keroro
    27.04.2015 08:01

    Спасибо. А код этого урока можно где-нибудь скачать?


    1. Vadimatorikda Автор
      27.04.2015 14:28

      Кода не осталось… Пришлось компоновать из статьи. Пока компоновал — удалил код для следующей статьи… Ну да ладно. Все к лучшему. Переписал. Статью дополнил. Пишите об успехах или вопросах, поясню.


  1. Keroro
    27.04.2015 08:44

    Я всё же не понимаю основной принцип действия этой системы. Есть массив данных для ЦАП (значения синусоиды), 100 точек. Есть контроллер ПДП и два таймера. Есть массив структур для параметров для ПДП (почему их 64?). Одна из структур «нацелена» на конец массива синусоиды, вторая-на его половину. Таймер1 «магически» (в железе) связан с каналом ПДП, и заставляет ДМА (по одному элементу массива синуса за раз?) передавать данные в ЦАП без каких-либо явных прерываний. Таймер2 генерирует прерывание каждые четверть периода синуса, в прерывании смотрим, прошли ли мы половину периода, и если прошли, переключаем ПДП на альтернативную управляющую структуру. Правильно?


  1. Vadimatorikda Автор
    27.04.2015 09:22

    Есть массив структур для параметров для ПДП (почему их 64?).

    Каждый канал имеет первичную и альтеранативную структуру (того две). Каналов DMA имеет 32 (0..31). На каждый канал по 2 структуры = 64 структуры. Мы используем всего 1 канал и можем оставить только 2 структуры. Остальную память можно использовать по собственному назначению. НО. Каждая структура имеет свой фиксированный адрес со смещением в 1024. К примеру для канала «0» первичная структура может размещаться в 0x20000000 или 0x20000200, 0x20000400 и т.д… Нельзя разместить структуру для третьего канала (если мы используем только третий канал) по адресу 0x20000000. Он обязательно должен быть 0x20000030, или 0x20000230, или 0x20000430 и т.д.
    Таймер1 «магически» (в железе) связан с каналом ПДП, и заставляет ДМА (по одному элементу массива синуса за раз?)

    Таймер 1 железно (в чипе) связан с DMA. Когда таймер генерирует сигнал — он «производит процедуру арбитража». Иначе говоря, разрешает передачу. В структуре мы указали, что контроллер должен ждать этого разрешения после каждой передачи. Если бы мы хотели передавать, к примеру, по 128 значений, то в структуре нужно было бы указать «производить арбитраж через каждые 128 передач». Тогда бы за один вызов таймера мы бы передавали 128 значений.
    Таймер2 генерирует прерывание каждые четверть периода синуса, в прерывании смотрим, прошли ли мы половину периода, и если прошли, переключаем ПДП на альтернативную управляющую структуру.

    Не только половину. Мы проверяем, передал ли DMA первую структуру и хотя бы один элемент из следующей. Потому что есть риск, что мы проверим структуру как раз в тот момент, когда ПДП передал последней элемент структуры, но еще не попытался передать элемент из следующей. Тогда мы «восстановим» структуру и в этот же момент ДМА вместе перехода к новой структуре начнет заново передавать все из восстановленной.


    1. Keroro
      27.04.2015 14:49

      Вроде понял. А таймер1 и передача по одному элементу нужна для того, чтобы управляя периодом таймера1 управлять выходной частотой, да? Я к тому, что можно было, наверное, настроить на передачу всего массива точек сразу, и только таймером2 контролировать момент, когда пора запускать выдачу следующего периода? Но тогда частота синуса была бы жестко привязана к тактовой частоте МК…


  1. Vadimatorikda Автор
    27.04.2015 15:00

    А таймер1 и передача по одному элементу нужна для того, чтобы управляя периодом таймера1 управлять выходной частотой, да?

    Да, для этого.
    Я к тому, что можно было, наверное, настроить на передачу всего массива точек сразу, и только таймером2 контролировать момент, когда пора запускать выдачу следующего периода? Но тогда частота синуса была бы жестко привязана к тактовой частоте МК…

    Да, технически можно обойтись одним таймером, если сгенерировать синусоиду привязанной к частоте. Но это бред… В следующей статье (то, о чем говорю, уже дописано) мы сделаем небольшую функцию, которая будет меняя частоту таймера играть MIDI мелодию (одно голосую). А после покажу способ (сейчас еще выбираю, как будет проще), как сделать выдачу многоголосой музыки без изменения настроек таймера.


  1. crrr
    27.04.2015 22:00

    Оффтоп, конечно. Но может кто ткнет носом, как в СС2538 на том-же ядре таймером пинать пдп?..