Как стать автором
Обновить

Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Опрашиваем клавиши, генерируем ШИМ. Часть первая

Время на прочтение 15 мин
Количество просмотров 30K

Вступление


Отступление

С последней написанной мною статьи прошло уже довольно много времени, за что прошу прощения: ЕГЭ, поступление, начало учебы. Теперь же, когда до сессии еще далеко, а учебный процесс уже отнимает не так много времени, я могу продолжить писать статьи об освоении нашего К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)
    {
    }
    }

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

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

Итоговый проект на github

Небольшое дополнение

В данный момент Миландр выпустил новую, переработанную версию SPL. К сожалению, пока не в виде полноценной версии. В текущей версии нельзя смотреть состояние регистров в окнах keil. Но уже скоро выйдет полная версия и этот недостаток будет исправлен. Для тех, кто хочет попробовать новую версию — вот ссылка на чистый проект.

Список предыдущих статей.
Теги:
Хабы:
+17
Комментарии 6
Комментарии Комментарии 6

Публикации

Истории

Работа

Программист С
43 вакансии

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн