Вступление
Отступление
С последней написанной мною статьи прошло уже довольно много времени, за что прошу прощения: ЕГЭ, поступление, начало учебы. Теперь же, когда до сессии еще далеко, а учебный процесс уже отнимает не так много времени, я могу продолжить писать статьи об освоении нашего К1986ВЕ92QI.
План работы
В комментариях к предыдущим статьям меня просили осветить не только работу с микроконтроллером через настройку регистров, но и с использованием SPL (Универсальной библиотеки для авто настройки периферии.). Когда мы только начинали, я не стал этого делать, ибо соблазн использовать SPL вместо ручной настройки по средствам CMSIS был бы велик, и вы бы, очень вероятно, вопреки здравому смыслу, начали бы использовать SPL везде, где только можно было бы. Сейчас же, научившись работе с некоторыми блоками периферии вручную, мы можем коснуться SPL и сравнить КПД обоих подходов в реальной задачи.
Цель
В качестве учебной цели, давайте помигаем светодиодом по средствам ШИМ-а (Широтно-импульсной модуляции.), при этом регулируя кнопками его частоту. Кнопки так же будем опрашивать в прерывании, вызванного другим таймером, а в момент опроса — будем инвертировать состояние второго светодиода. В реализации данной задачи нам понадобится:
1. Настроить вывод порта ввода-вывода, подключенного к светодиоду, для ручного управления. Этим светодиодом будем показывать, что мы зашли в прерывание и опросили кнопки.
2. Настроить вывод порта ввода-вывода, подключенного ко второму светодиоду, в режим управления от таймера. Именно сюда будет подаваться ШИМ сигнал от первого таймера.
3. Настроить первый таймер в режим подачи ШИМ сигнала на второй светодиод.
4. Настроить таймер для вызова прерывания, в котором мы будем опрашивать клавиши.
5. Разрешить использование прерываний на уровне таймера (по конкретному событию) и на уровне общей таблице векторов прерываний от второго таймера в целом.
Ручная настройка
Таймер 1. Реализация ШИМ
С работой таймера мы уже сталкивались в этой статье. Но в тот раз у нас были совсем другие цели и нынешняя настройка немного сложнее той, что была описана в приведенной выше статье.
Начнем.
- Для начала создадим пустую оболочку функции, которая будет инициализировать таймер. На вход она должна принимать какое-то значение, характеризующее скорость ШИМ. У нее может быть абсолютно любое имя.
Например, такое.// Инициализация таймера в режиме ШИМ для работы со светодиодом. void initTimerPWMled (uint32_t PWM_speed) { }
- Далее стоит вспомнить структуру таймера.
Структура таймера.
Структура у всех трех таймеров нашего микроконтроллера одна и та же. Каждый таймер имеет 4 канала, каждый из которых позволяет работать в режиме «захвата» и ШИМ. Нас интересует последний. Так же у каждого канала есть выходы. Причем 2: «прямой» и инвертированный. Нас интересует «прямой». В качестве выхода для выдачи сигнала ШИМ — будем использовать вывод первого канала первого таймера. Перед тем как перейти к регистрам — выделим основную задачу: наша цель, чтобы подождав некоторое время, таймер сам менял состояние на своем выходе циклично. - Прежде чем мы начнем настраивать таймер — нам нужно настроить вывод порта ввода-вывода на работу с таймером. О том, как настраивать выводы портов ввода-вывода я рассказывал очень подробно тут.
Мы решили использовать прямой выход первого канала первого таймера.
Выводы имеют следующие имена.
Следовательно, нам нужен канал 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)); // Максимальная скорость пин обоих светодиодов.
- Далее нам нужно разрешить подачу тактового сигнала на сам таймер (включить мы его уже включили, а вот подать сигнал, с которого он и будет считать — не подали). Для этого есть регистр MDR_RST_CLK->TIM_CLOCK.TIM_CLOCKТут нам нужно лишь подать тактирование на таймер.
MDR_RST_CLK->TIM_CLOCK |= RST_CLK_TIM_CLOCK_TIM1_CLK_EN; // Подаем тактирование без предделителя.
- А теперь — регистры самого таймера. Несмотря на то, что у таймера очень много регистров — большинство из них копируют друг друга, так как структура регистров управления для каждого канала — одна и та же. Для начала рассмотрим регистры всего таймера, а потом для конкретного канала.
- Регистр CNT можно назвать основой. Именно значение в нем сравнивается с «эталонным» и в случае совпадения происходит какое-либо действие. Именно с него таймер начинает считать.
В нашем случае достаточно, чтобы он был равен нулю. Несмотря на то, что он при включении и так должен был быть равен нулю, на всякий случай лучше сбросить его, т.к. возможно, что после программной перезагрузки значение в нем будет не ноль.
CNT
MDR_TIMER1->CNT = 0; // Считай с 0.
- PSG. Данный регистр отвечает за деление входного сигнала. В нашем случае на вход таймера подается 8000000 импульсов в секунду (т.к. по умолчанию частота контроллера 8 МГц = 8000000 Гц), а делители перед таймером мы не использовали. Как видно из описания, от того делителя, который мы выберем, нужно отнять 1 и это число положить в регистр. Т.к. мы планируем менять частоту ШИМ в приделах от 0.5 Гц до 250 Гц (От медленного мигания раз в 2 секунды, до неразличимого человеческим глазом мельканием, похожим на тусклое горение), то подходящим делителем может быть 32000. Это число входит в диапазон 16-ти битного числа. Таким образом, каждые 32000 тиков в CNT будет пробавляться/убавляться (в зависимости от настройки) единица.
PSGMDR_TIMER1->PSG = 32000-1; // На таймер идет частота TIM_CLK/32000.
- ARR. Именно с этим числом будет сравниваться число в CNT. Так как у нас 250 тиков — это одна секунда, то выберем половину этого времени, чтобы за секунду светодиод успел поменять свое состояние дважды. Именно это число мы укажем при вызове функции инициализации таймера.ARR
MDR_TIMER1->ARR = PWM_speed; // 1 секунда 250 тиков. У нас частота 2 герца.
- С общими настройками таймера разобрались. Можно приниматься за настройку сигнала для выхода. Для каждого канала можно настроить свой сигнал. В нашем случае (для первого канала), служит регистр CH1_CNTRL. Как мы условились выше, у нас на выходе всегда должен быть какой-то сигнал. Либо «0» — либо «1». «Мертвая зона» нам не нужна. И нам нужно, чтобы и «0» и «1» были равные промежутки времени. Для этих целей есть сигнал REF. Он может быть либо «1», либо «0». Так же мы можем менять его значения всякий раз, когда CNT == ARR. Для этого нам нужно в ячейку OCCM записать 0x03 (0b011). Все остальные параметры нас устраивают и по умолчанию.
CH1_CNTRLMDR_TIMER1->CH1_CNTRL = 3<<TIMER_CH_CNTRL_OCCM_Pos; // Переключение REF, если CNT = CCR;
- Теперь нам нужно настроить выход канала. Мы договорились использовать первый. Тут нам понадобится регистр 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); // Канал всегда работает на выход.
- Теперь нам осталось лишь включить таймер в центральном регистре (я намеренно не рассматривал его ранее, так как его нужно использовать лишь по окончании настройки всего таймера).CNTRL
MDR_TIMER1->CNTRL = TIMER_CNTRL_CNT_EN; // Включаем таймер.
- В итоге мы получаем работающую функцию, инициализирующую таймер в режиме ШИМ и вывод, на котором и происходят колебания логических уровней.Итоговая функция инициализации 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; // Включаем таймер. }
- Регистр CNT можно назвать основой. Именно значение в нем сравнивается с «эталонным» и в случае совпадения происходит какое-либо действие. Именно с него таймер начинает считать.
Таймер 2. Вызов прерываний для опроса клавиш, изменение частоты ШИМ.
Теперь перед нами стоит задача проверить, нажата ли какая-либо клавиша и на основании нажатия изменить частоту нашего ШИМ-а. Опрашивать клавиатуру мы будем 25 раз в секунду и без проверки отпущенного нажатия. Это даст нам возможность делать большей разбег параметра ШИМ-а при нажатии.- Прежде чем настраивать таймер, настроим выводы для всех клавиш, что есть на нашей отладочной плате.Подключены они следующем образом.Как мы можем видеть, клавиши подключены к трем различным портам. Следовательно, нам нужно настроить все три порта. Замечу, что подтяжка и конденсаторная защита от дребезга уже присутствует на плате и включать внутреннюю подтяжку не нужно. С настройкой портов мы сталкивались неоднократно.Конечный код инициализации будет выглядеть следующим образом.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 нс). }
- Так как все таймеры имеют одинаковую структуру, то настройка второго таймера до определенного момента будет идентична настройки предыдущего. Так же создадим функцию, которая будет инициализировать таймер. У меня она выглядит так.
// Настройка таймера для генерации прерываний 25 раз в секунду. void initTimerButtonCheck (void) { }
- Далее все как в первом таймере, только 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.
- Далее нам нужно, чтобы при совпадении CNT и ARR у нас происходило прерывание. Для этого нам нужен регистр IE. Из всего многообразия различных случаев, вызывающих прерывание, нам нужен самый простой: CNT_ARR_EVENT_IE. IE
MDR_TIMER2->IE = TIMER_IE_CNT_ARR_EVENT_IE; // Разрешаем прерывание по совпадению CNT и ARR.
- Теперь при 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); // Разрешаем прерывание от таймера в целом.
- Осталось только включить таймер и наша конечная функция будет иметь следующий вид.Инициализация таймера 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; // Включаем таймер.
} - Теперь нам нужно создать обработчик прерывания. Его имя строго фиксировано в файле startup_MDR32F9Qx.s. На весь таймер есть всего один вектор прерывания. Все названия там интуитивно понятны. Наш называется Timer2_IRQHandler. Создадим функцию с пустыми входными параметрами. И первой же командой нужно сбросить флаг прерывания, из-за которого мы сюда попали. Иначе после выхода из прерывания мы попадем обратно в его начало. Сбрасывать флаг в конце так же нельзя, ибо не хватает времени, чтобы он был «полностью сброшен» и в итоге мы все равно попадаем в прерывание с несброшенным флагом. Обязательно нужно, чтобы перед выходом из прерывания была хотя бы одна команда, разделяющая сброс флага и выходом из прерывания. Сбросить флаг можно в регистре STATUS. STATUSТак как у нас всего одно событие из таймера используется, то мы можем смело записывать «0» во весь регистр. Если бы у на было несколько событий, то мы должны были бы сначала проверить, какое из событий произошло. В нашем случае функция будет иметь следующий вид.
void Timer2_IRQHandler (void) { MDR_TIMER2->STATUS = 0; // Сбрасываем флаг. Обязательно первой коммандой. // Здесь обязательно должна быть хоть одна команда. }
- В самом начале статьи мы определились, что при входе в прерывание мы будем менять состояние светодиода, чтобы показать, что прерывание было обработано. Для этого нам нужно воспользоваться одним из двух пользовательских светодиодов, подключенных к выводам 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)); // Фильтрация импульсов отключена. }
- Осталось только опросить клавиши и изменить значение в 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
- Теперь осталось лишь записать опрос кнопок и изменение регистра 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; // Меняем частоту.
} - Основная функция main содержит в себе лишь перечисление всех выше описанных функций. Выглядит так.int main (void)
{
initTimerPWMled(PWM_speed); // Запускаем ШИМ. Параметр — скорость ШИМ.
initPinForButton(); // Настраиваем кнопки.
initPinPortCForLed(); // Работа светодиода (клавиша считывается).
initTimerButtonCheck(); // Инициализация таймера.
while (1)
{
}
}
Вместо заключения
В этой статье мы рассмотрели реализацию поставленной задачи без использования библиотеки SPL. В следующей статье мы реализуем ту же задачу, но уже с использованием только лишь SPL и сравним результаты.
Итоговый проект.
Небольшое дополнение
В данный момент Миландр выпустил новую, переработанную версию SPL. К сожалению, пока не в виде полноценной версии. В текущей версии нельзя смотреть состояние регистров в окнах keil. Но уже скоро выйдет полная версия и этот недостаток будет исправлен. Для тех, кто хочет попробовать новую версию — вот ссылка на чистый проект.
Кому необходимы окошки — может по-прежнему пользоваться стабильной старой.
Список предыдущих статей.
- 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. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть третья: генерируем синусоидальный сигнал. Простой взгляд на DMA + первое знакомство с таймерами.
- 8. Переходим с STM32 на российский микроконтроллер К1986ВЕ92QI. Практическое применение: Генерируем и воспроизводим звук. Часть четвертая: создаем цифровую часть одноголосной и многоголосой музыкальной открытки.
Amomum
Простите, а какая связь между SPL и регистрами в окнах Кейла? Может быть, вы путаете SPL файлом описания процессора (который .sfr)?
Vadimatorikda
Вы правы. Имело ввиду, что пока что данного файла нет в комплекте с библиотекой. Он, предполагаю, сейчас дорабатывается. Точной информацией я не владею.