Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Опрашиваем клавиши, генерируем ШИМ. Сравнение кода на CMSIS и SPL (PWM+TIM+PORT). Часть первая

от автора


Вступление.

Отступление.

С последней написанной мною статьи прошло уже довольно много времени, за что прошу прощения: ЕГЭ, поступление, начало учебы. Теперь же, когда до сессии еще далеко, а учебный процесс уже отнимает не так много времени, я могу продолжить писать статьи об освоении нашего К1986ВЕ92QI.

План работы.

В комментариях к предыдущим статьям меня просили осветить не только работу с микроконтроллером через настройку регистров, но и с использованием SPL (Универсальной библиотеки для авто настройки периферии.). Когда мы только начинали, я не стал этого делать, ибо соблазн использовать SPL вместо ручной настройки по средствам CMSIS был бы велик, и вы бы, очень вероятно, вопреки здравому смыслу, начали бы использовать SPL везде, где только можно было бы. Сейчас же, научившись работе с некоторыми блоками периферии вручную, мы можем коснуться SPL и сравнить КПД обоих подходов в реальной задачи.

Цель.

В качестве учебной цели, давайте помигаем светодиодом по средствам ШИМ-а (Широтно-импульсной модуляции.), при этом регулируя кнопками его частоту. Кнопки так же будем опрашивать в прерывании, вызванного другим таймером, а в момент опроса — будем инвертировать состояние второго светодиода. В реализации данной задачи нам понадобится:
1. Настроить вывод порта ввода-вывода, подключенного к светодиоду, для ручного управления. Этим светодиодом будем показывать, что мы зашли в прерывание и опросили кнопки.
2. Настроить вывод порта ввода-вывода, подключенного ко второму светодиоду, в режим управления от таймера. Именно сюда будет подаваться ШИМ сигнал от первого таймера.
3. Настроить первый таймер в режим подачи ШИМ сигнала на второй светодиод.
4. Настроить таймер для вызова прерывания, в котором мы будем опрашивать клавиши.
5. Разрешить использование прерываний на уровне таймера (по конкретному событию) и на уровне общей таблице векторов прерываний от второго таймера в целом.

Ручная настройка.

Таймер 1. Реализация ШИМ.

С работой таймера мы уже сталкивались в этой статье. Но в тот раз у нас были совсем другие цели и нынешняя настройка немного сложнее той, что была описана в приведенной выше статье. Начнем.

  1. Для начала создадим функцию, которая будет инициализировать таймер. На вход она должна принимать какое-то значение, характеризующее скорость ШИМ. У нее может быть абсолютно любое имя.
    Например, такое.

    // Инициализация таймера в режиме ШИМ для работы со светодиодом.  void initTimerPWMled (uint32_t PWM_speed)  {  }
  2. Далее стоит вспомнить структуру таймера.
    Структура таймера.

    Структура у всех трех таймеров нашего микроконтроллера одна и та же. Каждый таймер имеет 4 канала, каждый из которых позволяет работать в режиме «захвата» и ШИМ. Нас интересует последний. Так же у каждого канала есть выходы. Причем 2: «прямой» и инвертированный. Нас интересует «прямой». В качестве выхода для выдачи сигнала ШИМ — будем использовать вывод первого канала первого таймера. Перед тем как перейти к регистрам — выделим основную задачу: наша цель, чтобы подождав некоторое время, таймер сам менял состояние на своем выходе циклично.

  3. Прежде чем мы начнем настраивать таймер — нам нужно настроить вывод порта ввода-вывода на работу с таймером. О том, как настраивать выводы портов ввода-вывода я рассказывал очень подробно тут. Мы решили использовать прямой выход первого канала первого таймера.
    Выводы имеют следующие имена.

    Следовательно, нам нужен канал TMR1_CH1.

    Находим его.

    Как мы видим, он подключен альтернативной функцией к каналу PA1. Не смотря на то, что есть еще выводы, которые подключены к TMR1_CH1, мы будем использовать именно PA1.
    Для этого нам нужно подать тактирование на порт (а заодно и на таймер 1) и перевести вывод в режим альтернативной функции.

    MDR_RST_CLK->PER_CLOCK |= RST_CLK_PCLK_TIMER1|RST_CLK_PCLK_PORTA; // Включаем таймер и тактирование порта A. 	MDR_PORTA->OE        |= (1<<1);	 // Выход. 	MDR_PORTA->FUNC   |= (2<<(1*2)); // Режим работы - альтернативная функция. 	MDR_PORTA->ANALOG |= (1<<1); // Цифровые. 	MDR_PORTA->PWR |= (3<<(1*2)); // Максимальная скорость пин обоих светодиодов.
  4. Далее нам нужно разрешить подачу тактового сигнала на сам таймер (включить мы его уже включили, а вот подать сигнал, с которого он и будет считать — не подали). Для этого есть регистр MDR_RST_CLK->TIM_CLOCK.
    TIM_CLOCK

    Тут нам нужно лишь подать тактирование на таймер.

    MDR_RST_CLK->TIM_CLOCK |= RST_CLK_TIM_CLOCK_TIM1_CLK_EN;	// Подаем тактирование без предделителя.
  5. А теперь — регистры самого таймера. Несмотря на то, что у таймера очень много регистров — большинство из них копируют друг друга, так как структура регистров управления для каждого канала — одна и та же. Для начала рассмотрим регистры всего таймера, а потом для конкретного канала.
    1. Регистр CNT можно назвать основой. Именно значение в нем сравнивается с «эталонным» и в случае совпадения происходит какое-либо действие. Именно с него таймер начинает считать. В нашем случае достаточно, чтобы он был равен нулю. Несмотря на то, что он при включении и так должен был быть равен нулю, на всякий случай лучше сбросить его, т.к. возможно, что после программной перезагрузки значение в нем будет не ноль.
      CNT

      MDR_TIMER1->CNT = 0;		// Считай с 0.
    2. PSG. Данный регистр отвечает за деление входного сигнала. В нашем случае на вход таймера подается 8000000 импульсов в секунду (т.к. по умолчанию частота контроллера 8 МГц = 8000000 Гц), а делители перед таймером мы не использовали. Как видно из описания, от того делителя, который мы выберем, нужно отнять 1 и это число положить в регистр. Т.к. мы планируем менять частоту ШИМ в приделах от 0.5 Гц до 250 Гц (От медленного мигания раз в 2 секунды, до неразличимого человеческим глазом мельканием, похожим на тусклое горение), то подходящим делителем может быть 32000. Это число входит в диапазон 16-ти битного числа. Таким образом, каждые 32000 тиков в CNT будет пробавляться/убавляться (в зависимости от настройки) единица.
      PSG

      MDR_TIMER1->PSG = 32000-1;		// На таймер идет частота TIM_CLK/32000.
    3. ARR. Именно с этим числом будет сравниваться число в CNT. Так как у нас 250 тиков — это одна секунда, то выберем половину этого времени, чтобы за секунду светодиод успел поменять свое состояние дважды. Именно это число мы укажем при вызове функции инициализации таймера.
      ARR

      MDR_TIMER1->ARR = PWM_speed;				// 1 секунда 250 тиков. У нас частота 2 герца.
    4. С общими настройками таймера разобрались. Можно приниматься за настройку сигнала для выхода. Для каждого канала можно настроить свой сигнал. В нашем случае (для первого канала), служит регистр CH1_CNTRL. Как мы условились выше, у нас на выходе всегда должен быть какой-то сигнал. Либо «0» — либо «1». «Мертвая зона» нам не нужна. И нам нужно, чтобы и «0» и «1» были равные промежутки времени. Для этих целей есть сигнал REF. Он может быть либо «1», либо «0». Так же мы можем менять его значения всякий раз, когда CNT == ARR. Для этого нам нужно в ячейку OCCM записать 0x03 (0b011). Все остальные параметры нас устраивают и по-умолчанию.
      CH1_CNTRL

      MDR_TIMER1->CH1_CNTRL = 3<<TIMER_CH_CNTRL_OCCM_Pos;	// Переключение REF, если CNT = CCR;
    5. Теперь нам нужно настроить выход канала. Мы договорились использовать первый. Тут нам понадобится регистр CH1_CNTRL1. Мы уже сформировали сигнал REF. Теперь нам нужно лишь настроить «прямой» вывод на выход и подать на него REF. Важно не перепутать группы бит SELO и SELOE. SELO выбирает, какой сигнал идет на вывод, а SELOE выбирает, будет ли вывод являться выходом или нет.
      CH1_CNTRL1

      MDR_TIMER1->CH1_CNTRL1 = (2<<TIMER_CH_CNTRL1_SELO_Pos) // На выход выдается сигнал c REF. 	| (1<<TIMER_CH_CNTRL1_SELOE_Pos); // Канал всегда работает на выход.
    6. Теперь нам осталось лишь включить таймер в центральном регистре (я намеренно не рассматривал его ранее, так как его нужно использовать лишь по окончании настройки всего таймера).
      CNTRL

      MDR_TIMER1->CNTRL = TIMER_CNTRL_CNT_EN;				// Включаем таймер.
    7. В итоге мы получаем работающую функцию, инициализирующую таймер в режиме ШИМ и вывод, на котором и происходят колебания логических уровней.
      Итоговая функция инициализации TIMER1

      // Инициализация таймера в режиме ШИМ для работы со светодиодом.  void initTimerPWMled (uint32_t PWM_speed)  { 	MDR_RST_CLK->PER_CLOCK |= RST_CLK_PCLK_TIMER1|RST_CLK_PCLK_PORTA; // Включаем таймер и тактирование порта A. 	MDR_RST_CLK->TIM_CLOCK |= RST_CLK_TIM_CLOCK_TIM1_CLK_EN; // Подаем тактирование без предделителя.  	 	MDR_PORTA->OE 		|= (1<<1);	 // Выход. 	MDR_PORTA->FUNC 	|= (2<<(1*2)); // Режим работы - альтернативная функция. 	MDR_PORTA->ANALOG |= (1<<1); // Цифровые. 	MDR_PORTA->PWR |= (3<<(1*2)); // Максимальная скорость пин обоих светодиодов. 	 	MDR_TIMER1->CNT = 0; // Считай с 0. 	MDR_TIMER1->PSG = 32000-1; // На таймер идет частота TIM_CLK/32000. 	MDR_TIMER1->ARR = PWM_speed; // 1 секунда 250 тиков. У нас частота 2 герца.  	MDR_TIMER1->CH1_CNTRL = 3<<TIMER_CH_CNTRL_OCCM_Pos; // Переключение REF, если CNT = CCR; 	MDR_TIMER1->CH1_CNTRL1 = (2<<TIMER_CH_CNTRL1_SELO_Pos) // На выход выдается сигнал c REF. 	| (1<<TIMER_CH_CNTRL1_SELOE_Pos); // Канал всегда работает на выход. 	MDR_TIMER1->CNTRL = TIMER_CNTRL_CNT_EN; // Включаем таймер. }

Таймер 2. Вызов прерываний для опроса клавиш, изменение частоты ШИМ.

Теперь перед нами стоит задача проверить, нажата ли какая-либо клавиша и на основании нажатия изменить частоту нашего ШИМ-а. Опрашивать клавиатуру мы будем 25 раз в секунду и без проверки отпущенного нажатия. Это даст нам возможность делать большей разбег параметра ШИМ-а при нажатии.

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

    Как мы можем видеть, клавиши подключены к трем различным портам. Следовательно, нам нужно настроить все три порта. Замечу, что подтяжка и конденсаторная защита от дребезга уже присутствует на плате и включать внутреннюю подтяжку не нужно. С настройкой портов мы сталкивались неоднократно.

    Конечный код инициализации будет выглядеть следующим образом.

    Define-ы.

    // Маски бит портов клавиш.  #define DOWN_MSK				(1<<1)	// PORTE #define SELECT_MSK				(1<<2)	// PORTC #define LEFT_MSK					(1<<3)	// PORTE #define UP_MSK					(1<<5)	// PORTB #define RIGHT_MSK 			(1<<6)	// PORTB  #define PWRMAX_UP_MSK   (3<<2*5)// PORTB #define PWRMAX_RIGHT_MSK   (3<<2*6) #define PWRMAX_SELECT_MSK    (3<<2*2)// PORTC. #define PWRMAX_DOWN_MSK   (3<<2*1)// PORTE. #define PWRMAX_LEFT_MSK   (3<<2*3)

    Сама функция настройки.

    // Инициализация пинов на портах B, C, E для работы с кнопками навигации,  // установленными на плате.  // Подключение кнопок описано в inc файле. void initPinForButton (void)  {	 	MDR_RST_CLK->PER_CLOCK |= RST_CLK_PCLK_PORTB|RST_CLK_PCLK_PORTC|RST_CLK_PCLK_PORTE; // Включаем тактирование портов B, C, E. 	   MDR_PORTB->OE 		&= ~((uint32_t)(UP_MSK|RIGHT_MSK)); // Входы. 	MDR_PORTB->FUNC 	&= ~((uint32_t)(UP_MSK|RIGHT_MSK)); // Режим работы - порт. 	MDR_PORTB->ANALOG |= UP_MSK|RIGHT_MSK;	// Цифровые. 	MDR_PORTB->PULL 	&= ~((uint32_t)(UP_MSK|RIGHT_MSK|UP_MSK<<16|RIGHT_MSK<<16)); // Подтяжка отключена. 	MDR_PORTB->PD 		&= ~((uint32_t)(UP_MSK|RIGHT_MSK|UP_MSK<<16|RIGHT_MSK<<16)); // Триггер Шмитта выключен гистерезис 200 мВ // Управляемый драйвер. 	MDR_PORTB->PWR		|= PWRMAX_UP_MSK|PWRMAX_RIGHT_MSK; // Максимальная скорость обоих выводов. 	MDR_PORTB->GFEN		|= UP_MSK|RIGHT_MSK; // Фильтр импульсов включен (фильтрация импульсов до 10 нс).    MDR_PORTC->OE 		&= ~((uint32_t)(SELECT_MSK)); // Вход. 	MDR_PORTC->FUNC 	&= ~((uint32_t)(SELECT_MSK)); // Режим работы - порт. 	MDR_PORTC->ANALOG |= SELECT_MSK; // Цифровой. 	MDR_PORTC->PULL 	&= ~((uint32_t)(SELECT_MSK|SELECT_MSK<<16)); // Подтяжка отключена. 	MDR_PORTC->PD 		&= ~((uint32_t)(SELECT_MSK|SELECT_MSK<<16)); // Триггер Шмитта выключен гистерезис 200 мВ.																						// Управляемый драйвер. 	MDR_PORTC->PWR		|= PWRMAX_SELECT_MSK; // Максимальная скорость вывода. 	MDR_PORTC->GFEN		|= SELECT_MSK; // Фильтр импульсов включен (фильтрация импульсов до 10 нс).    MDR_PORTE->OE 		&= ~((uint32_t)(DOWN_MSK|LEFT_MSK)); // Входы. 	MDR_PORTE->FUNC 	&= ~((uint32_t)(DOWN_MSK|LEFT_MSK)); // Режим работы - порт. 	MDR_PORTE->ANALOG |= DOWN_MSK|LEFT_MSK; // Цифровые. 	MDR_PORTE->PULL 	&= ~((uint32_t)(DOWN_MSK|LEFT_MSK|DOWN_MSK<<16|LEFT_MSK<<16)); // Подтяжка отключена. 	MDR_PORTE->PD 		&= ~((uint32_t)(DOWN_MSK|LEFT_MSK|DOWN_MSK<<16|LEFT_MSK<<16)); // Триггер Шмитта выключен гистерезис 200 мВ.		// Управляемый драйвер. 	MDR_PORTE->PWR		|= PWRMAX_DOWN_MSK|PWRMAX_LEFT_MSK;	// Максимальная скорость обоих выводов. 	MDR_PORTE->GFEN		|= DOWN_MSK|LEFT_MSK;	// Фильтр импульсов включен (фильтрация импульсов до 10 нс). }
  2. Так как все таймеры имеют одинаковую структуру, то настройка второго таймера до определенного момента будет идентична настройки предыдущего. Так же создадим функцию, которая будет инициализировать таймер.
    У меня она выглядит так.

    // Настройка таймера для генерации прерываний 25 раз в секунду. void initTimerButtonCheck (void)  {  }
  3. Далее все как в первом таймере, только ARR не 125 (пол секунды), а 10 (1/25-я).
    Заполнение регистров.

    MDR_RST_CLK->PER_CLOCK |= RST_CLK_PCLK_TIMER2; // Включаем тактирование таймера 2. MDR_RST_CLK->TIM_CLOCK |= RST_CLK_TIM_CLOCK_TIM2_CLK_EN; // Подаем тактирование без пред делителя.  MDR_TIMER2->CNT = 0;// Считай с 0. MDR_TIMER2->PSG = 32000-1; // На таймер идет частота TIM_CLK/32000. MDR_TIMER2->ARR = 10; // 1 секунда 250 тиков. У нас 25 опросов в секунду => 250/25=10.
  4. Далее нам нужно, чтобы при совпадении CNT и ARR у нас происходило прерывание. Для этого нам нужен регистр IE. Из всего многообразия различных случаев, вызывающих прерывание, нам нужен самый простой: CNT_ARR_EVENT_IE.
    IE

    MDR_TIMER2->IE 	= TIMER_IE_CNT_ARR_EVENT_IE;		// Разрешаем прерывание по совпадению CNT и ARR.
  5. Теперь при CNT == ARR у нас возникает прерывание. Но оно нам ничего не даст, потому что по умолчанию прерывания от всего таймера запрещены. Исправить это можно, разрешив прерывание от всего таймера в контроллере NVIC. В предыдущих статьях мы уже имели с ним дело. Но тогда мы промелькнули его вскользь. Для того, чтобы разрешить или запретить прерывания — в CMSIS есть собственные функции. Бояться их не стоит, ибо они представляют из себя простые макросы в одну СИ-команду. Но они здорово улучают читабельность кода.
    Вот какие команды CMSIS мы можем использовать.

    Отсюда нам нужна функция NVIC_EnableIRQ.

    Ее параметр можно узнать из таблицы в файле MDR32Fx.h

    /* MDR32Fx Interrupt Number Definition */ typedef enum IRQn { /*---- Cortex-M3 Processor Exceptions Numbers --------------------------------*/   NonMaskableInt_IRQn     = -14,  /*!<  2 Non Maskable Interrupt              *///!< NonMaskableInt_IRQn   HardFault_IRQn          = -13,  /*!<  3 Hard Fault Interrupt                *///!< HardFault_IRQn   MemoryManagement_IRQn   = -12,  /*!<  4 Memory Management Interrupt         *///!< MemoryManagement_IRQn   BusFault_IRQn           = -11,  /*!<  5 Bus Fault Interrupt                 *///!< BusFault_IRQn   UsageFault_IRQn         = -10,  /*!<  6 Usage Fault Interrupt               *///!< UsageFault_IRQn   SVCall_IRQn             = -5,   /*!< 11 SV Call Interrupt                   *///!< SVCall_IRQn   PendSV_IRQn             = -2,   /*!< 14 Pend SV Interrupt                   *///!< PendSV_IRQn   SysTick_IRQn            = -1,   /*!< 15 System Tick Timer Interrupt         *///!< SysTick_IRQn  /*---- MDR32Fx specific Interrupt Numbers ------------------------------------*/   CAN1_IRQn               =  0,   /*!< CAN1 Interrupt                         *///!< CAN1_IRQn   CAN2_IRQn               =  1,   /*!< CAN1 Interrupt                         *///!< CAN2_IRQn   USB_IRQn                =  2,   /*!< USB Host Interrupt                     *///!< USB_IRQn   DMA_IRQn                =  5,   /*!< DMA Interrupt                          *///!< DMA_IRQn   UART1_IRQn              =  6,   /*!< UART1 Interrupt                        *///!< UART1_IRQn   UART2_IRQn              =  7,   /*!< UART2 Interrupt                        *///!< UART2_IRQn   SSP1_IRQn               =  8,   /*!< SSP1 Interrupt                         *///!< SSP1_IRQn   I2C_IRQn                =  10,  /*!< I2C Interrupt                          *///!< I2C_IRQn   POWER_IRQn              =  11,  /*!< POWER Detecor Interrupt                *///!< POWER_IRQn   WWDG_IRQn               =  12,  /*!< Window Watchdog Interrupt              *///!< WWDG_IRQn   Timer1_IRQn             =  14,  /*!< Timer1 Interrupt                       *///!< Timer1_IRQn   Timer2_IRQn             =  15,  /*!< Timer2 Interrupt                       *///!< Timer2_IRQn   Timer3_IRQn             =  16,  /*!< Timer3 Interrupt                       *///!< Timer3_IRQn   ADC_IRQn                =  17,  /*!< ADC Interrupt                          *///!< ADC_IRQn   COMPARATOR_IRQn         =  19,  /*!< COMPARATOR Interrupt                   *///!< COMPARATOR_IRQn   SSP2_IRQn               =  20,  /*!< SSP2 Interrupt                         *///!< SSP2_IRQn   BACKUP_IRQn             =  27,  /*!< BACKUP Interrupt                       *///!< BACKUP_IRQn   EXT_INT1_IRQn           =  28,  /*!< EXT_INT1 Interrupt                     *///!< EXT_INT1_IRQn   EXT_INT2_IRQn           =  29,  /*!< EXT_INT2 Interrupt                     *///!< EXT_INT2_IRQn   EXT_INT3_IRQn           =  30,  /*!< EXT_INT3 Interrupt                     *///!< EXT_INT3_IRQn   EXT_INT4_IRQn           =  31   /*!< EXT_INT4 Interrupt                     *///!< EXT_INT4_IRQn }IRQn_Type;

    Нам нужен второй таймер. Следовательно наша функция будет выглядеть так.

    NVIC_EnableIRQ(Timer2_IRQn); // Разрешаем прерывание от таймера в целом.
  6. Осталось только включить таймер и наша конечная функция будет иметь следующий вид.
    Инициализация таймера 2 для опроса кнопок.

    // Настройка таймера для генерации прерываний 25 раз в секунду.
    void initTimerButtonCheck (void)
    {
    MDR_RST_CLK->PER_CLOCK |= RST_CLK_PCLK_TIMER2; // Включаем тактирование таймера 2.
    MDR_RST_CLK->TIM_CLOCK |= RST_CLK_TIM_CLOCK_TIM2_CLK_EN; // Подаем тактирование без предделителя.
    MDR_TIMER2->CNT = 0; // Считай с 0.
    MDR_TIMER2->PSG = 32000-1; // На таймер идет частота TIM_CLK/32000.
    MDR_TIMER2->ARR = 10; // 1 секунда 250 тиков. У нас 25 опросов в секунду => 250/25=10.
    MDR_TIMER2->IE = TIMER_IE_CNT_ARR_EVENT_IE; // Разрешаем прерывание по совподению CNT и ARR.
    NVIC_EnableIRQ(Timer2_IRQn); // Разрешаем прерывание от таймера в целом.
    MDR_TIMER2->CNTRL = TIMER_CNTRL_CNT_EN; // Включаем таймер.
    }
  7. Теперь нам нужно создать обработчик прерывания. Его имя строго фиксировано в файле startup_MDR32F9Qx.s. На весь таймер есть всего один вектор прерывания. Все названия там интуитивно понятны. Наш называется Timer2_IRQHandler. Создадим функцию с пустыми входными параметрами. И первой же командой нужно сбросить флаг прерывания, из-за которого мы сюда попали. Иначе после выхода из прерывания мы попадем обратно в его начало. Сбрасывать флаг в конце так же нельзя, ибо не хватает времени, чтобы он был «полностью сброшен» и в итоге мы все равно попадаем в прерывание с несброшенным флагом. Обязательно нужно, чтобы перед выходом из прерывания была хотя бы одна команда, разделяющая сброс флага и выходом из прерывания. Сбросить флаг можно в регистре STATUS.
    STATUS

    Так как у нас всего одно событие из таймера используется, то мы можем смело записывать «0» во весь регистр. Если бы у на было несколько событий, то мы должны были бы сначала проверить, какое из событий произошло. В нашем случае функция будет иметь следующий вид.

    void Timer2_IRQHandler (void) { 	MDR_TIMER2->STATUS  = 0;		// Сбрасываем флаг. Обязательно первой коммандой.	 // Здесь обязательно должна быть хоть одна команда. }
  8. В самом начале статьи мы определились, что при входе в прерывание мы будем менять состояние светодиода, чтобы показать, что прерывание было обработано. Для этого нам нужно воспользоваться одним из двух пользовательских светодиодов, подключенных к выводам PC0 и PC1.
    Подключение светодиодов.

    Предлагаю для этой цели использовать PC0 (на плате он слева). А светодиод, подключенные к PC1 нужно отключить от выхода микроконтроллера и проводом подключить к PA1 (нашему выводу ШИМ).

    Инициализация светодиодов будет выглядеть следующим образом.

    // Подключение светодиодов.	 #define LED0			(1<<0)	// PORTC. #define LED1			(1<<1)	// PORTC.  #define PWRMAX_LED0			 (3<<2*0)	// Максимальная скорость работы порта. #define PWRMAX_LED1			 (3<<2*1)	  // Инициализация порта C для работы с двумя светодиодами. void initPinPortCForLed (void)  { 	MDR_RST_CLK->PER_CLOCK |= RST_CLK_PCLK_PORTC; // Включаем тактирование портов C. 	MDR_PORTC->OE 		|= LED0|LED1; // Выход. 	MDR_PORTC->FUNC 	&= ~((uint32_t)(LED0|LED1)); // Режим работы - порт. 	MDR_PORTC->ANALOG |= LED0|LED1; // Цифровые. 	MDR_PORTC->PULL 	&= ~((uint32_t)(LED0|LED1|LED0<<16|LED1<<16)); // Подтяжка отключена. 	MDR_PORTC->PD 		&= ~((uint32_t)(LED0|LED1|LED0<<16|LED1<<16)); // Триггер Шмитта выключен гистерезис 200 мВ. 																																			// Управляемый драйвер. 	MDR_PORTC->PWR		|= PWRMAX_LED0|PWRMAX_LED1; // Максимальная скорость пин обоих светодиодов. 	MDR_PORTC->GFEN		&= ~((uint32_t)(LED0|LED1)); // Фильтрация импульсов отключена. }

    Функция настраивает оба светодиода, но т.к. второй отключен (перемычкой), то никакой разницы не будет.

  9. Осталось только опросить клавиши и изменить значение в ARR таймера ШИМ. Но наши кнопки подключены к 3-м разным портам. Можно, конечно, по-старинке. Брать значения с целого порта и с помощью маски смотреть конкретные выводы, но в этом случае намного удобнее использовать BitBanding. Если не углубляться в подробности, то у нас каждый бит области периферии (порты ввода-вывода в том числе) имеет свою собственную 32-х битную ячейку. В которой записано либо «1» либо «0». В зависимости от состояния бита. С ними можно работать как с обыкновенными регистрами. Запись «1» даст 1 в нужном бите реального регистра. «0» — соответственно, 0. Для того, чтобы получить адреса этих ячеек, можно воспользоваться очень удобным калькулятором Catethysis-а. Разберем на примере. У нас клавиша UP подключена к выводу 5 порта B. Идем в документацию и смотрим адрес регистра порта B.
    Там находим

    Вбиваем этот адрес в поле «регистр», а в поле «бит» пишем 5. На выходе получаем 0x43600014. Именно работая с ячейкой по этому адресу мы работаем с битом 5 порта B. Но просто записать 0x43600014 = 1 — нельзя. А вот *(uint32_t*)0x43600014 = 1 — можно.

    Теперь, подобным образом можно переписать все выводы, подключенные к кнопкам.

    // Читать состояние клавишы. #define DOWN_FLAG					*(uint32_t*)0x43900004	 #define SELECT_FLAG				*(uint32_t*)0x43700008 #define LEFT_FLAG					*(uint32_t*)0x4390000c #define UP_FLAG						*(uint32_t*)0x43600014 #define RIGHT_FLAG				*(uint32_t*)0x43600018

    Точно так же можно сделать и для светодиода.

    #define LED0_FLAG						*(uint32_t*)0x43700000
  10. Теперь осталось лишь записать опрос кнопок и изменение регистра ARR таймера 1.
    Итоговая функция будет выглядеть так.

    int PWM_speed = 125;
    void Timer2_IRQHandler (void)
    {
    MDR_TIMER2->STATUS = 0; // Сбрасываем флаг. Обязательно первой коммандой.
    LED1_FLAG = !LED1_FLAG; // Показываем, что прерывание было обработано.
    if (UP_FLAG == 0) PWM_speed—; // Проверияем, нажата ли какая-нибудь клавиша. Если нажата — что-то делаем с частотой.
    else if (DOWN_FLAG == 0) PWM_speed++;
    else if (LEFT_FLAG == 0) PWM_speed—;
    else if (RIGHT_FLAG == 0) PWM_speed++;
    if (PWM_speed < 1) PWM_speed = 1; // Проверяем, чтобы частота не вышла за пределы диапазона от 250 Гц до 0.5 Гц.
    else if (PWM_speed > 500) PWM_speed = 500;
    MDR_TIMER1->ARR = PWM_speed; // Меняем частоту.
    }
  11. Основная функция main содержит в себе лишь перечисление всех выше описанных функций.
    Выглядит так.

    int main (void)
    {
    initTimerPWMled(PWM_speed); // Запускаем ШИМ. Параметр — скорость ШИМ.
    initPinForButton(); // Настраиваем кнопки.
    initPinPortCForLed(); // Работа светодиода (клавиша считывается).
    initTimerButtonCheck(); // Инициализация таймера.
    while (1)
    {
    }
    }

Вместо заключения.

В этой статье мы рассмотрели реализацию поставленной задачи без использования библиотеки CPL. В следующей статье мы реализуем ту же задачу, но уже с использованием только лишь SPL и сравним результаты.

Список предыдущих статей.

ссылка на оригинал статьи http://habrahabr.ru/post/267051/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *