Вступление
В предыдущей статье я рассказал о своем первом знакомстве с DMA. В ней мы делали связку DMA + SysTick. Статья получилась очень специфичной и сложной, ввиду неопытного кривого подхода. Набравшись опыта, в данной статье я расскажу о куда более простом и понятном способе работы с DMA.Основные аспекты
Из предыдущей статьи мы уяснили, что для того, чтобы запустить DMA, нужно:- Подать сигнал тактирвания на модуль DMA.
- Заполнить управляющую структуру работы DMA.
- Произвести настройку 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;
//Настраиваем первичную структуру.
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; //Пустая ячейка.
#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 */
//Настраиваем канал.
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; //Включаем тактирование.
#define CNTRL_CNT_EN (1<<0) //Маска включения таймера.
TIMER1->CNTRL |= CNTRL_CNT_EN; //Разрешаем работу таймера, событие = равенство значению.
TIMER1->ARR = 0xFFFF; //Считаем до...
Далее нужно связать наш таймер с DMA. В качества события, из-за которого будет происходить передача — выберем CNT == ARR в таймере 1. Это простое сравнение текущего значения таймера CNT с числом в регистре ARR.#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; //Подаем тактовый сигнал на таймер.
}
Настраиваем таймеры для генерации синусоиды
Как я уже сказал, для «восстановления» структуры мы должны настроить в следующем (втором) таймере прерывание по прохождении половины половины массива (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; //Отключаем тактирование таймеров.
//У нас 8000000 Гц/261.63/100 = 305.
TIMER1->ARR = 305; //Считаем до...
TIMER1->DMA_RE |= DMA_RE_CNT_ARR_EVENT_RE; //Разрешаем пинать DMA.
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;
}
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, начать пользоваться его преимуществами. В следующей статье мы закрепим наши умения и создадим подобие музыкальной шкатулки, а после — плеера.- 1. Переходим с STM32F103 на К1986ВЕ92QI. Или первое знакомство с российским микроконтроллером.
- 2. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Настройка проекта в keil и мигание светодиодом.
- 3. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Системный таймер (SysTick).
- 4. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Настройка тактовой частоты.
- 5. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть первая: генерируем прямоугольный и синусоидальный сигнал. Освоение ЦАП (DAC).
- 6. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть вторая: генерируем синусоидальный сигнал. Освоение DMA.
Комментарии (7)
Keroro
27.04.2015 08:44Я всё же не понимаю основной принцип действия этой системы. Есть массив данных для ЦАП (значения синусоиды), 100 точек. Есть контроллер ПДП и два таймера. Есть массив структур для параметров для ПДП (почему их 64?). Одна из структур «нацелена» на конец массива синусоиды, вторая-на его половину. Таймер1 «магически» (в железе) связан с каналом ПДП, и заставляет ДМА (по одному элементу массива синуса за раз?) передавать данные в ЦАП без каких-либо явных прерываний. Таймер2 генерирует прерывание каждые четверть периода синуса, в прерывании смотрим, прошли ли мы половину периода, и если прошли, переключаем ПДП на альтернативную управляющую структуру. Правильно?
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 первую структуру и хотя бы один элемент из следующей. Потому что есть риск, что мы проверим структуру как раз в тот момент, когда ПДП передал последней элемент структуры, но еще не попытался передать элемент из следующей. Тогда мы «восстановим» структуру и в этот же момент ДМА вместе перехода к новой структуре начнет заново передавать все из восстановленной.Keroro
27.04.2015 14:49Вроде понял. А таймер1 и передача по одному элементу нужна для того, чтобы управляя периодом таймера1 управлять выходной частотой, да? Я к тому, что можно было, наверное, настроить на передачу всего массива точек сразу, и только таймером2 контролировать момент, когда пора запускать выдачу следующего периода? Но тогда частота синуса была бы жестко привязана к тактовой частоте МК…
Vadimatorikda Автор
27.04.2015 15:00А таймер1 и передача по одному элементу нужна для того, чтобы управляя периодом таймера1 управлять выходной частотой, да?
Да, для этого.
Я к тому, что можно было, наверное, настроить на передачу всего массива точек сразу, и только таймером2 контролировать момент, когда пора запускать выдачу следующего периода? Но тогда частота синуса была бы жестко привязана к тактовой частоте МК…
Да, технически можно обойтись одним таймером, если сгенерировать синусоиду привязанной к частоте. Но это бред… В следующей статье (то, о чем говорю, уже дописано) мы сделаем небольшую функцию, которая будет меняя частоту таймера играть MIDI мелодию (одно голосую). А после покажу способ (сейчас еще выбираю, как будет проще), как сделать выдачу многоголосой музыки без изменения настроек таймера.
crrr
27.04.2015 22:00Оффтоп, конечно. Но может кто ткнет носом, как в СС2538 на том-же ядре таймером пинать пдп?..
Keroro
Спасибо. А код этого урока можно где-нибудь скачать?
Vadimatorikda Автор
Кода не осталось… Пришлось компоновать из статьи. Пока компоновал — удалил код для следующей статьи… Ну да ладно. Все к лучшему. Переписал. Статью дополнил. Пишите об успехах или вопросах, поясню.