Программист, радиоинженер, Студент
0,0
рейтинг
14 ноября 2015 в 21:12

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

Вступление.

В предыдущей статье мы с вами повторили общую структуру таймера и детально рассмотрели ручной способ настройки ШИМ канала с использованием CMSIS. Но многим не нравится «копаться в регистрах» и они предпочитают принципиально другой уровень абстракции, позволяющий, как им кажется, упростить задачу. В этой статье я попытаюсь показать вам все плюсы и минусы данного подхода.

Изменение в подаче материала.

Во всех предыдущих статьях я описывал все знания по какой-либо задаче в одном последовательно сформированном тексте, стараясь не упустить всех тонкостей и деталей, получая при этом достаточно объемную, но исчерпывающую любые вопросы, статью. В результате, мнения о моих статьях разделились. Кому-то нравилось, что в статьях нет отсылок к литературе и вся необходимая информация для понимания статьи находится в самой статье или в ее предшественниках. А кому-то наоборот было не интересно читать про «элементарные вещи» (такие, как синтаксис языка, стандартная организация блоков, и т.д.) и люди закрывали статью не прочтенной. Так как я не люблю отсылать людей к литературе, но при этом не хочу, чтобы что-либо в статье было непонятно, то материал теперь будет излагаться следующим образом: основной текст — текст для людей, разбирающихся в том, о чем читают и имеющих некоторый опыт работы по данной тематике. Для тех, кто что-либо не знает или не до конца понимает — под спойлерами с пометками «Пояснения к....» — будет собрана вся необходимая для понимания статьи информация.

Задача.

Наша задача решить ту же задачу, что мы решали в предыдущей статье, но с использованием только лишь возможностей SPL. По итогу мы сможем сказать, какой подход более нагляден, быстр и меньше весит. Как следствие — мы создадим столько же функций, сколько было в предыдущей реализации с такими же именами, за исключением того, что добавим «SPL», чтобы можно было их отличить и сравнить влияние каждой функции на вес и производительность кода (Заменяя функцию ручной инициализации на функцию с автоматической инициализацией средствами SPL).

Настройка портов ввода-вывода средствами SPL (PORT).

Начать предлагаю с самого простого. С портов ввода-вывода для вручную управляемого светодиода. Раньше эта функция называлась initPinPortCForLed. Теперь будет initPinPortCForLedSPL. Имена последующих функций будут иметь такой же принцип именования. Как мы помним, для того, чтобы порт запустился и мы смогла зажечь светодиод — нужно:
  1. Подать сигнал тактирования на порт.
  2. Инициализировать сам порт.
  3. Выставить значение в регистр RXTX.
Так было, когда мы работали с CMSIS напрямую. С SPL все немного иначе. Для настройки любого периферийного модуля нужно заполнить структуру. Иногда — не одну. И потом ее передать в функцию, которая сама все настроит.
Пояснение к сказанному.
Можно провести аналогию с постройкой дома: вы делаете чертеж по всем требованием, а потом передаете его людям, которые сами строят дом. Вас не касается, как будет построен дом. Вы подразумеваете, что в точности — как на вашем чертеже. Здесь «чертеж» — это настроенная вами структура. А «люди, строящие дом» — функция SPL. Для каждого блока периферии существует своя структура. Узнать, какая требуется структура, можно заглянув в файл в папке spl (в дереве проекта) с именем MDR32F9Qx_имя_периферии.
Для начала нам нужно подать сигнал тактирования на порт. Для этого нам нужно обратиться к файлу MDR32F9Qx_rst_clk.h. В нем, в самом конце, есть функции, которые нам может предоставить SPL. Из всего многообразия функций, нас интересует лишь RST_CLK_PCLKcmd. С ее помощью мы можем подать сигнал тактирования на любой блок периферии.
void RST_CLK_PCLKcmd(uint32_t RST_CLK_PCLK, FunctionalState NewState);
У функции есть два параметра:
  • RST_CLK_PCLK — имя блока периферии, на который нужно подать или с которого нужно снять тактовый сигнал (сигнал тактирования). Возможные имена можно найти поиском по этому же .h файлу, набрав RST_CLK_PCLK в качестве искомого.
    Возможные значения параметра RST_CLK_PCLK
    #ifdef USE_MDR1986VE9x /* For cortex M3 */
    
    	#define RST_CLK_PCLK_CAN1           PCLK_BIT(MDR_CAN1_BASE)
    	#define RST_CLK_PCLK_CAN2           PCLK_BIT(MDR_CAN2_BASE)
    	#define RST_CLK_PCLK_USB            PCLK_BIT(MDR_USB_BASE)
    	#define RST_CLK_PCLK_EEPROM         PCLK_BIT(MDR_EEPROM_BASE)
    	#define RST_CLK_PCLK_RST_CLK        PCLK_BIT(MDR_RST_CLK_BASE)
    	#define RST_CLK_PCLK_DMA            PCLK_BIT(MDR_DMA_BASE)
    	#define RST_CLK_PCLK_UART1          PCLK_BIT(MDR_UART1_BASE)
    	#define RST_CLK_PCLK_UART2          PCLK_BIT(MDR_UART2_BASE)
    	#define RST_CLK_PCLK_SSP1           PCLK_BIT(MDR_SSP1_BASE)
    	#define RST_CLK_PCLK_09             PCLK_BIT(0x40048000)
    	#define RST_CLK_PCLK_I2C            PCLK_BIT(MDR_I2C_BASE)
    	#define RST_CLK_PCLK_POWER          PCLK_BIT(MDR_POWER_BASE)
    	#define RST_CLK_PCLK_WWDG           PCLK_BIT(MDR_WWDG_BASE)
    	#define RST_CLK_PCLK_IWDG           PCLK_BIT(MDR_IWDG_BASE)
    	#define RST_CLK_PCLK_TIMER1         PCLK_BIT(MDR_TIMER1_BASE)
    	#define RST_CLK_PCLK_TIMER2         PCLK_BIT(MDR_TIMER2_BASE)
    	#define RST_CLK_PCLK_TIMER3         PCLK_BIT(MDR_TIMER3_BASE)
    	#define RST_CLK_PCLK_ADC            PCLK_BIT(MDR_ADC_BASE)
    	#define RST_CLK_PCLK_DAC            PCLK_BIT(MDR_DAC_BASE)
    	#define RST_CLK_PCLK_COMP           PCLK_BIT(MDR_COMP_BASE)
    	#define RST_CLK_PCLK_SSP2           PCLK_BIT(MDR_SSP2_BASE)
    	#define RST_CLK_PCLK_PORTA          PCLK_BIT(MDR_PORTA_BASE)
    	#define RST_CLK_PCLK_PORTB          PCLK_BIT(MDR_PORTB_BASE)
    	#define RST_CLK_PCLK_PORTC          PCLK_BIT(MDR_PORTC_BASE)
    	#define RST_CLK_PCLK_PORTD          PCLK_BIT(MDR_PORTD_BASE)
    	#define RST_CLK_PCLK_PORTE          PCLK_BIT(MDR_PORTE_BASE)
    	#define RST_CLK_PCLK_26             PCLK_BIT(0x400D0000)
    	#define RST_CLK_PCLK_BKP            PCLK_BIT(MDR_BKP_BASE)
    	#define RST_CLK_PCLK_28             PCLK_BIT(0x400E0000)
    	#define RST_CLK_PCLK_PORTF          PCLK_BIT(MDR_PORTF_BASE)
    	#define RST_CLK_PCLK_EBC            PCLK_BIT(MDR_EBC_BASE)
    	#define RST_CLK_PCLK_31             PCLK_BIT(0x400F8000)
    
    	#define IS_RST_CLK_PCLK(PCLK)       ((((PCLK) & RST_CLK_PCLK_09) == 0x00) && \
    										 (((PCLK) & RST_CLK_PCLK_26) == 0x00) && \
    										 (((PCLK) & RST_CLK_PCLK_28) == 0x00) && \
    										 (((PCLK) & RST_CLK_PCLK_31) == 0x00))
    #endif // #ifdef USE_MDR1986VE9x /* For cortex M3 */
    Прошу обратить внимание, что для каждой серии микроконтроллеров этот список свой. В большинстве пунктов списки идентичны, но некоторые индивидуальные для каждой серии строки могут отличатся.
  • NewState — состояние, в которое нужно перевести сигнал тактирования. Либо DISABLE — отключить тактирование, либо ENABLE — включить тактирование.
Вспомним, что наш светодиод подключен к PC1. Не сложно догадаться, что в нашем случае функция будет выглядеть так.
RST_CLK_PCLKcmd(RST_CLK_PCLK_PORTC, ENABLE);	 // Включаем тактирование порта C.
Теперь на нужный нам порт ввода-вывода подан тактовый сигнал и мы можем начать его настраивать. Для начала нам необходимо найти функцию, которая настраивает порт. Она находится в файле MDR32F9Qx_port.h. Называется PORT_Init и имеет следующий вид.
void PORT_Init(MDR_PORT_TypeDef* PORTx, const PORT_InitTypeDef* PORT_InitStruct);
Как мы видим, у этой функции так же 2 параметра:
  1. MDR_PORT_TypeDef — имя порта, который мы настраиваем. В формате MDR_PORTX, где вместо X — буква нашего порта (A, B, C...). В нашем случае будет MDR_PORTC.
  2. Второй параметр — это структура вида PORT_InitTypeDef. Ее описание находится в том же файле (MDR32F9Qx_port.h). К сожалению, описание SPL полностью на английском. Как следствие, человеку, не знающему английского языка и незнакомому с устройством периферии на уровне регистров будет довольно тяжко. Да и иногда об истинном значении комментариев к функциям приходится по началу только гадать. Понимание их назначения приходит лишь после тщательного изучения блок-схемы того или иного периферийного модуля.
    Описание структуры PORT_InitTypeDef
    typedef struct
    {
      uint16_t PORT_Pin;                     /*!< Specifies PORT pins to be configured.
                                                  This parameter is a mask of @ref PORT_pins_define values. */
      PORT_OE_TypeDef PORT_OE;               /*!< Specifies in/out mode for the selected pins.
                                                  This parameter is one of @ref PORT_OE_TypeDef values. */
      PORT_PULL_UP_TypeDef PORT_PULL_UP;     /*!< Specifies pull up state for the selected pins.
                                                  This parameter is one of @ref PORT_PULL_UP_TypeDef values. */
      PORT_PULL_DOWN_TypeDef PORT_PULL_DOWN; /*!< Specifies pull down state for the selected pins.
                                                  This parameter is one of @ref PORT_PULL_DOWN_TypeDef values. */
      PORT_PD_SHM_TypeDef PORT_PD_SHM;       /*!< Specifies SHM state for the selected pins.
                                                  This parameter is one of @ref PORT_PD_SHM_TypeDef values. */
      PORT_PD_TypeDef PORT_PD;               /*!< Specifies PD state for the selected pins.
                                                  This parameter is one of @ref PORT_PD_TypeDef values. */
      PORT_GFEN_TypeDef PORT_GFEN;           /*!< Specifies GFEN state for the selected pins.
                                                  This parameter is one of @ref PORT_GFEN_TypeDef values. */
      PORT_FUNC_TypeDef PORT_FUNC;           /*!< Specifies operating function for the selected pins.
                                                  This parameter is one of @ref PORT_FUNC_TypeDef values. */
      PORT_SPEED_TypeDef PORT_SPEED;         /*!< Specifies the speed for the selected pins.
                                                  This parameter is one of @ref PORT_SPEED_TypeDef values. */
      PORT_MODE_TypeDef PORT_MODE;           /*!< Specifies the operating mode for the selected pins.
                                                  This parameter is one of @ref PORT_MODE_TypeDef values. */
    }PORT_InitTypeDef;
    Пояснение: что такое структура, как ее заполнять, откуда брать значения?
    Структура, по сути, представляет из себя массив, каждая фиксированная (имеет свое место в массиве) ячейка которого содержит какой-то параметр. В отличии от массива, каждый параметр структуры может иметь свой тип. Как и массив, перед заполнением, структуру необходимо создать.
    PORT_InitTypeDef Led0PortC_structInit;		// На порту C.
    Здесь PORT_InitTypeDef — это тип. Иначе говоря — просто шаблон, на основании которого происходит «разметка» памяти. Led0PortC_structInit — имя конкретной структуры, придуманное нами. Как создавая переменную типа uint32_t мы задавали ее имя, к примеру Loop, так и тут мы создаем структуру типа PORT_InitTypeDef с именем Led0PortC_structInit. Важно отметить, что объявление структуры должно быть сделано в функции до первой команды. Иначе проект не соберется. После создания структуры необходимо ее заполнить. Заполнение идет следующим образом.
    имя_структуры.ее_параметр = какое-то значение;
    И так — для каждого параметра из описания. В описании к каждой ячейке есть пояснение, какие значения можно в нее записывать. Как правило, если значением является не какое-то произвольное число из какого-либо диапазона, то в описании есть слово ref. С помощью слова, стоящего после него, можно найти в файле все доступные значения для данной ячейки. Возьмем в пример первую ячейку.
    uint16_t PORT_Pin;                     /*!< Specifies PORT pins to be configured.
                                                  This parameter is a mask of @ref PORT_pins_define values. */
    Используя поиск, находим PORT_pins_define.
    Видим следующее.
    #define PORT_Pin_0                  0x0001U  /*!< Pin 0 selected */
    #define PORT_Pin_1                  0x0002U  /*!< Pin 1 selected */
    #define PORT_Pin_2                  0x0004U  /*!< Pin 2 selected */
    #define PORT_Pin_3                  0x0008U  /*!< Pin 3 selected */
    #define PORT_Pin_4                  0x0010U  /*!< Pin 4 selected */
    #define PORT_Pin_5                  0x0020U  /*!< Pin 5 selected */
    #define PORT_Pin_6                  0x0040U  /*!< Pin 6 selected */
    #define PORT_Pin_7                  0x0080U  /*!< Pin 7 selected */
    #define PORT_Pin_8                  0x0100U  /*!< Pin 8 selected */
    #define PORT_Pin_9                  0x0200U  /*!< Pin 9 selected */
    #define PORT_Pin_10                 0x0400U  /*!< Pin 10 selected */
    #define PORT_Pin_11                 0x0800U  /*!< Pin 11 selected */
    #define PORT_Pin_12                 0x1000U  /*!< Pin 12 selected */
    #define PORT_Pin_13                 0x2000U  /*!< Pin 13 selected */
    #define PORT_Pin_14                 0x4000U  /*!< Pin 14 selected */
    #define PORT_Pin_15                 0x8000U  /*!< Pin 15 selected */
    #define PORT_Pin_All                0xFFFFU  /*!< All pins selected */
    У нас PORTC вывод 1. По идеи, мы можем написать
    Led0PortC_structInit.PORT_Pin				= PORT_Pin_1;
    Но у нас еще со времен прошлой статьи остался такой define.
    // Подключение светодиодов.	
    #define LED0						(1<<0)			// PORTC.
    #define LED1						(1<<1)			// PORTC.
    Это такая же маска порта, что и в описании, только с другим именем. Но она дает более ясное представление о том, что мы подключаем, так что я запишу ее.
    Led0PortC_structInit.PORT_Pin				= LED1;	// Пин нашего светодиода.
Заполнив структуру останется лишь указать ее в качестве параметра функции SPL PORT_Init, не забывая про "&" перед именем структуры (передаем указатель на структуру).
Получим готовую функцию вида.
void initPinPortCForLedSPL (void)
{
	PORT_InitTypeDef Led0PortC_structInit; // На порту C.
	RST_CLK_PCLKcmd(RST_CLK_PCLK_PORTC, ENABLE); // Включаем тактирование порта C.
	
	Led0PortC_structInit.PORT_Pin = LED1; // Пин нашего светодиода.
	Led0PortC_structInit.PORT_FUNC = PORT_FUNC_PORT; // Вывод работают в режиме обычного порта.
	Led0PortC_structInit.PORT_GFEN = PORT_GFEN_OFF; // Входной фильтр отключен.
	Led0PortC_structInit.PORT_MODE = PORT_MODE_DIGITAL; // Вывод цифровой.
	Led0PortC_structInit.PORT_OE = PORT_OE_OUT; // Вывод работает на выход.
	Led0PortC_structInit.PORT_PD = PORT_PD_DRIVER; // Управляемый драйвер.
	Led0PortC_structInit.PORT_PD_SHM = PORT_PD_SHM_OFF; // Триггер Шмитта выключен.
	Led0PortC_structInit.PORT_PULL_DOWN = PORT_PULL_DOWN_OFF; // Подтяжка в 0 отключена.
	Led0PortC_structInit.PORT_PULL_UP = PORT_PULL_UP_OFF; // Подтяжка в 1 отключена. 
	Led0PortC_structInit.PORT_SPEED = PORT_SPEED_MAXFAST;	 // Работа вывода с максимальной скоростью.

	PORT_Init(MDR_PORTC, &Led0PortC_structInit); // Инициализируем порт.
}
Функция инициализации клавиш проходит аналогичным образом. Разница лишь в том, что мы указываем режим вывода вместо выхода — вход (PORT_OE_IN), а так же включаем входной фильтр (PORT_GFEN_ON).
Функция инициализации выводов, подключенных к кнопкам.
void initPinForButtonSPL (void)
{
	// Генерируем структуры инициализации портов.
	PORT_InitTypeDef buttonPortB_structInit; // Структура для иницализации входов кнопоки на порту C.
	PORT_InitTypeDef buttonPortC_structInit; // Выходы на порту C.
	PORT_InitTypeDef buttonPortE_structInit; // Не порту E.
	
	// Включаем тактирование портов.
	RST_CLK_PCLKcmd(RST_CLK_PCLK_PORTB, ENABLE); // Включаем тактирование порта B.
	RST_CLK_PCLKcmd(RST_CLK_PCLK_PORTC, ENABLE); // Включаем тактирование порта C.
	RST_CLK_PCLKcmd(RST_CLK_PCLK_PORTE, ENABLE); // Включаем тактирование порта E.
	
	// Заполняем стркутуры портов.
	buttonPortB_structInit.PORT_FUNC = PORT_FUNC_PORT; // Выводы работают в режиме обычного порта.
	buttonPortB_structInit.PORT_GFEN = PORT_GFEN_ON; // Входной фильтр отключен на обоих выводах.
	buttonPortB_structInit.PORT_MODE = PORT_MODE_DIGITAL; // Оба вывода цифровые.
	buttonPortB_structInit.PORT_OE = PORT_OE_IN; // Выводы работают на вход.
	buttonPortB_structInit.PORT_PD = PORT_PD_DRIVER; // Управляемый драйвер.
	buttonPortB_structInit.PORT_PD_SHM = PORT_PD_SHM_OFF; // Триггер Шмитта выключен.
	buttonPortB_structInit.PORT_Pin = UP_MSK|RIGHT_MSK; // Все вышеупомянутые настройки только для двух выводов.
	buttonPortB_structInit.PORT_PULL_DOWN = PORT_PULL_DOWN_OFF; // Подтяжка в 0 отключена.
	buttonPortB_structInit.PORT_PULL_UP = PORT_PULL_UP_OFF; // Подтяжка в 1 отключена. 
	buttonPortB_structInit.PORT_SPEED = PORT_SPEED_MAXFAST; // Работа выводов с максимальной скоростью.
	
	buttonPortC_structInit.PORT_FUNC = PORT_FUNC_PORT; // PORTC.
	buttonPortC_structInit.PORT_GFEN = PORT_GFEN_ON;			
	buttonPortC_structInit.PORT_MODE = PORT_MODE_DIGITAL;	
	buttonPortC_structInit.PORT_OE = PORT_OE_IN;						
	buttonPortC_structInit.PORT_PD = PORT_PD_DRIVER;			
	buttonPortC_structInit.PORT_PD_SHM = PORT_PD_SHM_OFF;		
	buttonPortC_structInit.PORT_Pin = SELECT_MSK;						
	buttonPortC_structInit.PORT_PULL_DOWN = PORT_PULL_DOWN_OFF;		
	buttonPortC_structInit.PORT_PULL_UP 	= PORT_PULL_UP_OFF;			
	buttonPortC_structInit.PORT_SPEED = PORT_SPEED_MAXFAST;		

	buttonPortE_structInit.PORT_FUNC = PORT_FUNC_PORT; // PORTE.
	buttonPortE_structInit.PORT_GFEN = PORT_GFEN_ON;							
	buttonPortE_structInit.PORT_MODE = PORT_MODE_DIGITAL;		
	buttonPortE_structInit.PORT_OE = PORT_OE_IN;					
	buttonPortE_structInit.PORT_PD = PORT_PD_DRIVER;				
	buttonPortE_structInit.PORT_PD_SHM = PORT_PD_SHM_OFF;			
	buttonPortE_structInit.PORT_Pin = DOWN_MSK|LEFT_MSK;				
	buttonPortE_structInit.PORT_PULL_DOWN = PORT_PULL_DOWN_OFF;		
	buttonPortE_structInit.PORT_PULL_UP = PORT_PULL_UP_OFF;			
	buttonPortE_structInit.PORT_SPEED = PORT_SPEED_MAXFAST;		

	// Инициализируем порты.
	PORT_Init(MDR_PORTB, &buttonPortB_structInit);
	PORT_Init(MDR_PORTC, &buttonPortC_structInit);
	PORT_Init(MDR_PORTE, &buttonPortE_structInit);
}

Настройка таймера для генерации ШИМ (PWM).

Прежде чем настраивать сам таймер — настроим вывод порта ввода-вывода, на который будем выводить ШИМ в режим альтернативной функции. Помним, что в прошлой статье мы использовали порт PORTA и вывод 1.
Выйдет следующее.
RST_CLK_PCLKcmd(RST_CLK_PCLK_PORTA, ENABLE); // Включаем тактирование порта A.
	
	PWMPortA_structInit.PORT_FUNC = PORT_FUNC_ALTER; // Вывод работают в режиме альтернативной функции.
	PWMPortA_structInit.PORT_GFEN = PORT_GFEN_OFF; // Входной фильтр отключен.
	PWMPortA_structInit.PORT_MODE = PORT_MODE_DIGITAL; // Вывод цифровой.
	PWMPortA_structInit.PORT_OE = PORT_OE_OUT; // Вывод работает на выход.
	PWMPortA_structInit.PORT_PD = PORT_PD_DRIVER; // Управляемый драйвер.
	PWMPortA_structInit.PORT_PD_SHM = PORT_PD_SHM_OFF; // Триггер Шмитта выключен.
	PWMPortA_structInit.PORT_Pin = PORT_Pin_1; // Пин нашего светодиода.
	PWMPortA_structInit.PORT_PULL_DOWN = PORT_PULL_DOWN_OFF; // Подтяжка в 0 отключена.
	PWMPortA_structInit.PORT_PULL_UP = PORT_PULL_UP_OFF; // Подтяжка в 1 отключена. 
	PWMPortA_structInit.PORT_SPEED = PORT_SPEED_MAXFAST;	 // Работа вывода с максимальной скоростью.

	PORT_Init(MDR_PORTA, &PWMPortA_structInit); // Инициализируем порт.
Теперь мы можем приступить к настройке самого таймера. Прежде всего, нам нужно подать тактирование на TIMER1. Сделать это можно так же с помощью функции RST_CLK_PCLKcmd, рассмотренной ранее.
RST_CLK_PCLKcmd(RST_CLK_PCLK_TIMER1, ENABLE); // Включаем тактирование таймера 1.
Далее стоит ясно обозначить задачу. Нам нужно:
  1. Настроить основной таймер.
  2. Настроить канал таймера.
  3. Настроить вывод таймера.
  4. Настроить тактовую частоту для работы всего таймера.
  5. Включить таймер.
Для каждого пункта в SPL есть своя функция, а для первых трех пунктов существуют еще и свои структуры. Все функции и их параметры находятся в файле MDR32F9Qx_timer.h. Начнем с первого пункта. Для инициализации основного таймера существует функция TIMER_CntInit.
void TIMER_CntInit(MDR_TIMER_TypeDef* TIMERx, const TIMER_CntInitTypeDef* TIMER_CntInitStruct);
Здесь два параметра.
  • TIMERx — выбранный для инициализации таймер. Указывается он в формате MDR_TIMERX, где X — номер нужного таймера. В нашем случае — MDR_TIMER1.
  • Структура типа TIMER_CntInitTypeDef — в данной структуре имеется перечисление всех возможных параметров основного таймера.
    Вот ее описание.
    typedef struct {

    #if defined(USE_MDR1986VE9x) /* For Cortex M3 */
    uint16_t TIMER_IniCounter; /*!< Specifies the initial counter value.
    This parameter can be a number between 0x0000 and 0xFFFF. */
    #elif ((defined (USE_MDR1986VE3)) || (defined (USE_MDR1986VE1T)))
    uint32_t TIMER_IniCounter; /*!< Specifies the initial counter value.
    This parameter can be a number between 0x0000 and 0xFFFFFFFF. */
    #endif // #elif ((defined (USE_MDR1986VE3)) || (defined (USE_MDR1986VE1T)))

    uint16_t TIMER_Prescaler; /*!< Specifies the prescaler value used to divide the TIMER clock.
    This parameter can be a number between 0x0000 and 0xFFFF.
    CLK = TIMER_CLK/(TIMER_Prescaler + 1) */

    #if defined(USE_MDR1986VE9x) /* For Cortex M3 */
    uint16_t TIMER_Period; /*!< Specifies the period value to be loaded into the
    Auto-Reload Register (ARR) at the next update event.
    This parameter must be a number between 0x0000 and 0xFFFF. */
    #elif ((defined (USE_MDR1986VE3)) || (defined (USE_MDR1986VE1T))) /* For Cortex M1 */
    uint32_t TIMER_Period; /*!< Specifies the period value to be loaded into the
    Auto-Reload Register (ARR) at the next update event.
    This parameter must be a number between 0x0000 and 0xFFFFFFFF. */
    #endif // #elif ((defined (USE_MDR1986VE3)) || (defined (USE_MDR1986VE1T))) /* For Cortex M1 */

    uint16_t TIMER_CounterMode; /*!< Specifies the counter mode.
    This parameter can be a value of ref TIMER_Counter_Mode */

    uint16_t TIMER_CounterDirection; /*!< Specifies the counter direction.
    This parameter can be a value of ref TIMER_Counter_Direction */

    uint16_t TIMER_EventSource; /*!< Specifies the Counter Event source.
    This parameter can be a value of ref TIMER_Event_Source */

    uint16_t TIMER_FilterSampling; /*!< Specifies the filter sampling clock (FDTS).
    This parameter can be a value of ref TIMER_Filter_Sampling */

    uint16_t TIMER_ARR_UpdateMode; /*!< Specifies the Auto-Reload Register (ARR) updating mode.
    This parameter can be a value of ref TIMER_ARR_Update_Mode */

    uint16_t TIMER_ETR_FilterConf; /*!< Specifies the ETR Filter configuration.
    This parameter can be a value of ref TIMER_FilterConfiguration */

    uint16_t TIMER_ETR_Prescaler; /*!< Specifies the ETR Prescaler configuration.
    This parameter can be a value of ref TIMER_ETR_Prescaler */

    uint16_t TIMER_ETR_Polarity; /*!< Specifies the ETR Polarity configuration.
    This parameter can be a value of ref TIMER_ETR_Polarity */

    uint16_t TIMER_BRK_Polarity; /*!< Specifies the BRK Polarity configuration.
    This parameter can be a value of ref TIMER_BRK_Polarity */
    } TIMER_CntInitTypeDef;
Заполнив все поля и инициализировав таймер получим следующее.

TIMER_CntInitTypeDef timerPWM_structInit; // Структура для настройки основного таймера (без каналов).
// Заполняем структуру таймера.
timerPWM_structInit.TIMER_ARR_UpdateMode = TIMER_ARR_Update_Immediately; // Регистр ARR можно обновлять в любое время.
timerPWM_structInit.TIMER_BRK_Polarity = TIMER_BRKPolarity_NonInverted; // BRK сигнал не инвертируется (нас этот параметр не касается).
timerPWM_structInit.TIMER_CounterDirection = TIMER_CntDir_Up; // Считаем "вверх". CNT инкрементируется (CNT++). 
timerPWM_structInit.TIMER_CounterMode = TIMER_CntMode_ClkFixedDir; // Считаем в одну сторону, вверх.
timerPWM_structInit.TIMER_ETR_FilterConf = TIMER_Filter_1FF_at_TIMER_CLK; // Сигнал зафиксирован в 1-м триггере на частоте TIM_CLK (В нашем случае оставляем по-умолчанию).
timerPWM_structInit.TIMER_ETR_Polarity = TIMER_ETRPolarity_NonInverted; // ETR на входе не инвертируется (мы его и не используем).
timerPWM_structInit.TIMER_ETR_Prescaler = TIMER_ETR_Prescaler_None; // Частота ETR на входе не делится (ETR не используем.).
timerPWM_structInit.TIMER_EventSource = TIMER_EvSrc_None; // Таймер не вызывает прерываний.
timerPWM_structInit.TIMER_FilterSampling = TIMER_FDTS_TIMER_CLK_div_1; // FDTS = TIMER_CLK. (Так не используем.).
timerPWM_structInit.TIMER_IniCounter = 0; // Считаем с 0. Начальное значение счетчика. (CNT = 0.).
timerPWM_structInit.TIMER_Period = PWM_speed; // Считаем до указанного в параметрах функции значения (ARR = PWM_speed).
timerPWM_structInit.TIMER_Prescaler = 32000 - 1;// Делитель входного сигнала. PSG регистр. 
	
TIMER_CntInit(MDR_TIMER1, &timerPWM_structInit); // Инициализируем основной таймер.
Как можно видеть из комментариев к коду заполнения структуры — большинство пунктов остаются по-умолчанию (отключенными). Но не смотря на это, из все равно нужно указать.
Далее нужно инициализировать канал таймера. В нашем случае — первый. За инициализицию каналов отвечает функция TIMER_ChnInit.
void TIMER_ChnInit(MDR_TIMER_TypeDef* TIMERx, const TIMER_ChnInitTypeDef* TIMER_ChnInitStruct)
Первым параметром идет имя инициализируемого таймера. Оно остается тем же, что и у функции инициализации основного таймера. А вот структура уже типа TIMER_ChnInitTypeDef.
Вот ее описание.
typedef struct
{
uint16_t TIMER_CH_Number; /*!< Specifies the TIMER Channel number to be configured.
This parameter can be a value of ref TIMER_CH_Number */

uint16_t TIMER_CH_Mode; /*!< Specifies the TIMER Channel mode.
This parameter can be a value of ref TIMER_CH_Mode */

uint16_t TIMER_CH_ETR_Ena; /*!< Enables or disables ETR.
This parameter can be a value of FunctionalState */

uint16_t TIMER_CH_ETR_Reset; /*!< Enables or disables ETR Reset.
This parameter can be a value of ref TIMER_CH_ETR_Reset */

uint16_t TIMER_CH_BRK_Reset; /*!< Enables or disables BRK Reset.
This parameter can be a value of ref TIMER_CH_BRK_Reset */

uint16_t TIMER_CH_REF_Format; /*!< Specifies the REF signal format.
This parameter can be a value of ref TIMER_CH_REF_Format */

uint16_t TIMER_CH_Prescaler; /*!< Specifies the TIMER Channel Prescaler configuration.
This parameter can be a value of ref TIMER_CH_Prescaler */

uint16_t TIMER_CH_EventSource; /*!< Specifies the Channel Event source.
This parameter can be a value of ref TIMER_CH_EventSource */

uint16_t TIMER_CH_FilterConf; /*!< Specifies the TIMER Channel Filter configuration.
This parameter can be a value of ref TIMER_FilterConfiguration */

uint16_t TIMER_CH_CCR_UpdateMode; /*!< Specifies the TIMER CCR, CCR1 update mode.
This parameter can be a value of ref TIMER_CH_CCR_Update_Mode */

uint16_t TIMER_CH_CCR1_Ena; /*!< Enables or disables the CCR1 register.
This parameter can be a value of FunctionalState */

uint16_t TIMER_CH_CCR1_EventSource; /*!< Specifies the Channel CCR1 Event source.
This parameter can be a value of ref TIMER_CH_CCR1_EventSource */
}TIMER_ChnInitTypeDef;
Так же заполняем и инициализируем.
TIMER_ChnInitTypeDef timerPWM_channelStructInit; // Структура канала ШИМ.
// Заполняем структуру PWM канала.
timerPWM_channelStructInit.TIMER_CH_BRK_Reset = TIMER_CH_BRK_RESET_Disable; // Сброс канала BRK не производится (BRK не используем).
timerPWM_channelStructInit.TIMER_CH_CCR1_Ena =	DISABLE; // CCR1 не используем.
timerPWM_channelStructInit.TIMER_CH_CCR1_EventSource =	TIMER_CH_CCR1EvSrc_PE; // Выбор события по входному каналу для CAP1: положительный фронт по Chi. (По умолчанию, мы не используем).
timerPWM_channelStructInit.TIMER_CH_CCR_UpdateMode = TIMER_CH_CCR_Update_Immediately; // Регистр CCR можно обновлять в любое время (CCR не используем).
timerPWM_channelStructInit.TIMER_CH_ETR_Ena = DISABLE; // ETR не используется.
timerPWM_channelStructInit.TIMER_CH_ETR_Reset = TIMER_CH_ETR_RESET_Disable; // Сброс ETR не производится.
timerPWM_channelStructInit.TIMER_CH_EventSource = TIMER_CH_EvSrc_PE;								// Выбор события по входному каналу: положительный фронт. (Так же не используется).
timerPWM_channelStructInit.TIMER_CH_FilterConf = TIMER_Filter_1FF_at_TIMER_CLK; // Входной сигнал от TIMER_CLK фиксируется одним триггером.
timerPWM_channelStructInit.TIMER_CH_Mode = TIMER_CH_MODE_PWM; // Канал в ШИМ режиме.
timerPWM_channelStructInit.TIMER_CH_Number = TIMER_CHANNEL1; // Первый канал. 
timerPWM_channelStructInit.TIMER_CH_Prescaler = TIMER_CH_Prescaler_None; // В канале частота не делится.
timerPWM_channelStructInit.TIMER_CH_REF_Format = TIMER_CH_REF_Format3; // Сигнал REF меняется при CNT == ARR.
	
TIMER_ChnInit(MDR_TIMER1, &timerPWM_channelStructInit); // Инициализируем канал.
Замечу, что именно в этой функции мы формируем сигнал на REF для ШИМ.
Далее нужно настроить канал таймера на выход. Для этого есть функция TIMER_ChnOutInit.
void TIMER_ChnOutInit(MDR_TIMER_TypeDef* TIMERx, const TIMER_ChnOutInitTypeDef* TIMER_ChnOutInitStruct);
Первым параметром так же идет имя нашего таймера. Второй — структура TIMER_ChnOutInitStruct.
Ее описание.
typedef struct
{
uint16_t TIMER_CH_Number; /*!< Specifies the TIMER Channel number to be configured.
This parameter can be a value of ref TIMER_CH_Number */

uint16_t TIMER_CH_DirOut_Polarity; /*!< Specifies the TIMER CHx output polarity.
This parameter can be a value of ref TIMER_CH_OUT_Polarity */

uint16_t TIMER_CH_DirOut_Source; /*!< Specifies the TIMER CHx output source.
This parameter can be a value of ref TIMER_CH_OUT_Source */

uint16_t TIMER_CH_DirOut_Mode; /*!< Specifies the TIMER CHx output enable source.
This parameter can be a value of ref TIMER_CH_OUT_Mode */

uint16_t TIMER_CH_NegOut_Polarity; /*!< Enables or disables the TIMER CHxN output inversion.
This parameter can be a value of ref TIMER_CH_OUT_Polarity */

uint16_t TIMER_CH_NegOut_Source; /*!< Specifies the TIMER CHxN output source.
This parameter can be a value of ref TIMER_CH_OUT_Source */

uint16_t TIMER_CH_NegOut_Mode; /*!< Specifies the TIMER CHxN output enable source.
This parameter can be a value of ref TIMER_CH_OUT_Mode */

uint16_t TIMER_CH_DTG_MainPrescaler; /*!< Specifies the main prescaler of TIMER DTG.
This parameter can be a number between 0x0000 and 0x00FF.
Delay DTGdel = TIMER_CH_DTG_MainPrescaler*(TIMER_CH_DTG_AuxPrescaler + 1) clocks. */

uint16_t TIMER_CH_DTG_AuxPrescaler; /*!< Specifies the auxiliary prescaler of TIMER DTG.
This parameter can be a number between 0x0000 and 0x000F.
Delay DTGdel = TIMER_CH_DTG_MainPrescaler*(TIMER_CH_DTG_AuxPrescaler + 1) clocks. */

uint16_t TIMER_CH_DTG_ClockSource; /*!< Specifies the TIMER DTG clock source.
This parameter can be a value of ref TIMER_CH_DTG_Clock_Source */
}TIMER_ChnOutInitTypeDef;
После заполнения структуры и инициализации будем наблюдать следующий код.
TIMER_ChnOutInitTypeDef timerPWM_channelOUTPWMStructInit; // Структура настройки выхода канала ШИМ.
// Параметры выхода.
timerPWM_channelOUTPWMStructInit.TIMER_CH_DirOut_Mode = TIMER_CH_OutMode_Output; // Всегда выход.
timerPWM_channelOUTPWMStructInit.TIMER_CH_DirOut_Polarity = TIMER_CHOPolarity_NonInverted; // Неинвертированный.
timerPWM_channelOUTPWMStructInit.TIMER_CH_DirOut_Source = TIMER_CH_OutSrc_REF; // На выход REF сигнал.
timerPWM_channelOUTPWMStructInit.TIMER_CH_DTG_AuxPrescaler = 0; // Делителя не стоит.
timerPWM_channelOUTPWMStructInit.TIMER_CH_DTG_ClockSource = TIMER_CH_DTG_ClkSrc_TIMER_CLK; // Источник тактового сигнала для DTG - TIMER_CLK. Но DTG мы все равно не используем.
timerPWM_channelOUTPWMStructInit.TIMER_CH_DTG_MainPrescaler = 0; // Делитель сигнала на DTG.
timerPWM_channelOUTPWMStructInit.TIMER_CH_NegOut_Mode = TIMER_CH_OutMode_Input; // Инвертный канал на вход. Все остальные его параметр берем по умолчанию, т.к. они не важны.
timerPWM_channelOUTPWMStructInit.TIMER_CH_NegOut_Polarity = TIMER_CHOPolarity_NonInverted; // Без инвертирования инвертированного канала.
timerPWM_channelOUTPWMStructInit.TIMER_CH_NegOut_Source = TIMER_CH_DTG_ClkSrc_TIMER_CLK; // Источник тактового сигнала для DTG - TIMER_CLK.
timerPWM_channelOUTPWMStructInit.TIMER_CH_Number = TIMER_CHANNEL1; // Первый канал.
	
TIMER_ChnOutInit(MDR_TIMER1, &timerPWM_channelOUTPWMStructInit); // Настраиваем канал на выход.
Теперь осталось только подать тактирование на таймер и запустить его. Для подачи тактового сигнала (именно тот, на основе которого таймер ведет счет) есть функция TIMER_BRGInit.
void TIMER_BRGInit(MDR_TIMER_TypeDef* TIMERx, uint32_t TIMER_BRG);
Первый параметр, как обычно, имя таймера, второй — делитель. Делитель рассчитывается так же, как и для регистра PSG в предыдущей статье (по сути эта функция лишь пишет наш делитель в PSG...). Так же отмечу, что эта же функция и разрешает подачу сигнала тактирования на таймер по-умолчанию. Ну а за включение отвечает функция TIMER_Cmd.
void TIMER_Cmd(MDR_TIMER_TypeDef* TIMERx, FunctionalState NewState)
Параметры — имя таймера и его состояние ENABLE/DISABLE.
По итогу всей настройки получаем следующее.
// Инициализация таймера в режиме ШИМ для работы со светодиодом в режиме SPL.
void initTimerPWMledSPL (uint32_t PWM_speed)
{
PORT_InitTypeDef PWMPortA_structInit; // Структура для инициализации вывода таймера в режиме ШИМ.
TIMER_CntInitTypeDef timerPWM_structInit; // Структура для настройки основного таймера (без каналов).
TIMER_ChnInitTypeDef timerPWM_channelStructInit; // Структура канала ШИМ.
TIMER_ChnOutInitTypeDef timerPWM_channelOUTPWMStructInit; // Структура настройки выхода канала ШИМ.

RST_CLK_PCLKcmd(RST_CLK_PCLK_PORTA, ENABLE); // Включаем тактирование порта A.
RST_CLK_PCLKcmd(RST_CLK_PCLK_TIMER1, ENABLE); // Включаем тактирование таймера 1.

PWMPortA_structInit.PORT_FUNC = PORT_FUNC_ALTER; // Вывод работают в режиме альтернативной функции.
PWMPortA_structInit.PORT_GFEN = PORT_GFEN_OFF; // Входной фильтр отключен.
PWMPortA_structInit.PORT_MODE = PORT_MODE_DIGITAL; // Вывод цифровой.
PWMPortA_structInit.PORT_OE = PORT_OE_OUT; // Вывод работает на выход.
PWMPortA_structInit.PORT_PD = PORT_PD_DRIVER; // Управляемый драйвер.
PWMPortA_structInit.PORT_PD_SHM = PORT_PD_SHM_OFF; // Триггер Шмитта выключен.
PWMPortA_structInit.PORT_Pin = PORT_Pin_1; // Пин нашего светодиода.
PWMPortA_structInit.PORT_PULL_DOWN = PORT_PULL_DOWN_OFF; // Подтяжка в 0 отключена.
PWMPortA_structInit.PORT_PULL_UP = PORT_PULL_UP_OFF; // Подтяжка в 1 отключена.
PWMPortA_structInit.PORT_SPEED = PORT_SPEED_MAXFAST; // Работа вывода с максимальной скоростью.

PORT_Init(MDR_PORTA, &PWMPortA_structInit); // Инициализируем порт.

// Заполняем структуру таймера.
timerPWM_structInit.TIMER_ARR_UpdateMode = TIMER_ARR_Update_Immediately; // Регистр ARR можно обновлять в любое время.
timerPWM_structInit.TIMER_BRK_Polarity = TIMER_BRKPolarity_NonInverted; // BRK сигнал не инвертируется (нас этот параметр не касается).
timerPWM_structInit.TIMER_CounterDirection = TIMER_CntDir_Up; // Считаем «вверх». CNT инкрементируется (CNT++).
timerPWM_structInit.TIMER_CounterMode = TIMER_CntMode_ClkFixedDir; // Считаем в одну сторону, вверх.
timerPWM_structInit.TIMER_ETR_FilterConf = TIMER_Filter_1FF_at_TIMER_CLK; // Сигнал зафиксирован в 1-м триггере на частоте TIM_CLK (В нашем случае оставляем по-умолчанию).
timerPWM_structInit.TIMER_ETR_Polarity = TIMER_ETRPolarity_NonInverted; // ETR на входе не инвертируется (мы его и не используем).
timerPWM_structInit.TIMER_ETR_Prescaler = TIMER_ETR_Prescaler_None; // Частота ETR на входе не делится (ETR не используем.).
timerPWM_structInit.TIMER_EventSource = TIMER_EvSrc_None; // Таймер не вызывает прерываний.
timerPWM_structInit.TIMER_FilterSampling = TIMER_FDTS_TIMER_CLK_div_1; // FDTS = TIMER_CLK. (Так не используем.).
timerPWM_structInit.TIMER_IniCounter = 0; // Считаем с 0. Начальное значение счетчика. (CNT = 0.).
timerPWM_structInit.TIMER_Period = PWM_speed; // Считаем до указанного в параметрах функции значения (ARR = PWM_speed).
timerPWM_structInit.TIMER_Prescaler = 32000 — 1; // Делитель входного сигнала. PSG регистр.

TIMER_CntInit(MDR_TIMER1, &timerPWM_structInit); // Инициализируем основной таймер.

// Заполняем структуру PWM канала.
timerPWM_channelStructInit.TIMER_CH_BRK_Reset = TIMER_CH_BRK_RESET_Disable; // Сброс канала BRK не производится (BRK не используем).
timerPWM_channelStructInit.TIMER_CH_CCR1_Ena = DISABLE; // CCR1 не используем.
timerPWM_channelStructInit.TIMER_CH_CCR1_EventSource = TIMER_CH_CCR1EvSrc_PE; // Выбор события по входному каналу для CAP1: положительный фронт по Chi. (По умолчанию, мы не используем).
timerPWM_channelStructInit.TIMER_CH_CCR_UpdateMode = TIMER_CH_CCR_Update_Immediately; // Регистр CCR можно обновлять в любое время (CCR не используем).
timerPWM_channelStructInit.TIMER_CH_ETR_Ena = DISABLE; // ETR не используется.
timerPWM_channelStructInit.TIMER_CH_ETR_Reset = TIMER_CH_ETR_RESET_Disable; // Сброс ETR не производится.
timerPWM_channelStructInit.TIMER_CH_EventSource = TIMER_CH_EvSrc_PE; // Выбор события по входному каналу: положительный фронт. (Так же не используется).
timerPWM_channelStructInit.TIMER_CH_FilterConf = TIMER_Filter_1FF_at_TIMER_CLK; // Входной сигнал от TIMER_CLK фиксируется одним триггером.
timerPWM_channelStructInit.TIMER_CH_Mode = TIMER_CH_MODE_PWM; // Канал в ШИМ режиме.
timerPWM_channelStructInit.TIMER_CH_Number = TIMER_CHANNEL1; // Первый канал.
timerPWM_channelStructInit.TIMER_CH_Prescaler = TIMER_CH_Prescaler_None; // В канале частота не делится.
timerPWM_channelStructInit.TIMER_CH_REF_Format = TIMER_CH_REF_Format3; // Сигнал REF меняется при CNT == ARR.

TIMER_ChnInit(MDR_TIMER1, &timerPWM_channelStructInit); // Инициализируем канал.

// Параметры выхода.
timerPWM_channelOUTPWMStructInit.TIMER_CH_DirOut_Mode = TIMER_CH_OutMode_Output; // Всегда выход.
timerPWM_channelOUTPWMStructInit.TIMER_CH_DirOut_Polarity = TIMER_CHOPolarity_NonInverted; // Не инвертированный.
timerPWM_channelOUTPWMStructInit.TIMER_CH_DirOut_Source = TIMER_CH_OutSrc_REF; // На выход REF сигнал.
timerPWM_channelOUTPWMStructInit.TIMER_CH_DTG_AuxPrescaler = 0; // Делителя не стоит.
timerPWM_channelOUTPWMStructInit.TIMER_CH_DTG_ClockSource = TIMER_CH_DTG_ClkSrc_TIMER_CLK; // Источник тактового сигнала для DTG — TIMER_CLK. Но DTG мы все равно не используем.
timerPWM_channelOUTPWMStructInit.TIMER_CH_DTG_MainPrescaler = 0; // Делитель сигнала на DTG.
timerPWM_channelOUTPWMStructInit.TIMER_CH_NegOut_Mode = TIMER_CH_OutMode_Input; // Инвертный канал на вход. Все остальные его параметр берем по умолчанию, т.к. они не важны.
timerPWM_channelOUTPWMStructInit.TIMER_CH_NegOut_Polarity = TIMER_CHOPolarity_NonInverted;// Без инвертирования инвертированного канала.
timerPWM_channelOUTPWMStructInit.TIMER_CH_NegOut_Source = TIMER_CH_DTG_ClkSrc_TIMER_CLK; // Источник тактового сигнала для DTG — TIMER_CLK.
timerPWM_channelOUTPWMStructInit.TIMER_CH_Number = TIMER_CHANNEL1; // Первый канал.

TIMER_ChnOutInit(MDR_TIMER1, &timerPWM_channelOUTPWMStructInit); // Настраиваем канал на выход.

TIMER_BRGInit(MDR_TIMER1, TIMER_HCLKdiv1); // Подаем источник сигнала для счета (частота процессора без предделителя). // В этой функции выбор делителя (у нас «1») и включение подачи такта.
TIMER_Cmd(MDR_TIMER1, ENABLE); // Включаем таймер.
}

Настройка таймера для вызова прерываний (IRQ)

Далее нам нужно настроить таймер, генерирующий прерывания для опроса клавиш. Здесь нам нужно будет настроить таймер лишь по первой структуре. Так как каналы и выходы мы не используем. Инициализация таймера будет выглядеть так же, как у предыдущего, за исключением ячейки TIMER_EventSource. В ней мы должны указать, по какому событию у нас происходит прерывание. В прошлой статье мы использовали CNT == ARR. Его и используем.
А вообще, возможны следующие варианты.
#define TIMER_EvSrc_None                      (((uint32_t)0x0) << TIMER_CNTRL_EVENT_SEL_Pos)  /*!< No events. */
#define TIMER_EvSrc_TM1                       (((uint32_t)0x1) << TIMER_CNTRL_EVENT_SEL_Pos)  /*!< Selects TIMER1 (CNT == ARR) event. */
#define TIMER_EvSrc_TM2                       (((uint32_t)0x2) << TIMER_CNTRL_EVENT_SEL_Pos)  /*!< Selects TIMER2 (CNT == ARR) event. */
#define TIMER_EvSrc_TM3                       (((uint32_t)0x3) << TIMER_CNTRL_EVENT_SEL_Pos)  /*!< Selects TIMER3 (CNT == ARR) event. */
#define TIMER_EvSrc_CH1                       (((uint32_t)0x4) << TIMER_CNTRL_EVENT_SEL_Pos)  /*!< Selects Channel 1 event. */
#define TIMER_EvSrc_CH2                       (((uint32_t)0x5) << TIMER_CNTRL_EVENT_SEL_Pos)  /*!< Selects Channel 2 event. */
#define TIMER_EvSrc_CH3                       (((uint32_t)0x6) << TIMER_CNTRL_EVENT_SEL_Pos)  /*!< Selects Channel 3 event. */
#define TIMER_EvSrc_CH4                       (((uint32_t)0x7) << TIMER_CNTRL_EVENT_SEL_Pos)  /*!< Selects Channel 4 event. */
#define TIMER_EvSrc_ETR                       (((uint32_t)0x8) << TIMER_CNTRL_EVENT_SEL_Pos)  /*!< Selects ETR event. */
Так же не забудем про включение тактирования таймера и подачу на него тактового сигнала для счета.
Вот так мы инициализировали таймер.
TIMER_CntInitTypeDef timerButtonCheck_structInit; // Структура для настройки основного таймера вызова прерывания для опроса клавиш.
RST_CLK_PCLKcmd(RST_CLK_PCLK_TIMER2, ENABLE); // Включаем тактирование таймера 1.
TIMER_BRGInit(MDR_TIMER2, TIMER_HCLKdiv1); // Подаем источник сигнала для счета (частота процессора без предделителя).

// Заполняем структуру основного таймера.
timerButtonCheck_structInit.TIMER_ARR_UpdateMode = TIMER_ARR_Update_Immediately; // Регистр ARR можно обновлять в любое время.
timerButtonCheck_structInit.TIMER_BRK_Polarity = TIMER_BRKPolarity_NonInverted; // BRK сигнал не инвертируется (нас этот параметр не касается).
timerButtonCheck_structInit.TIMER_CounterDirection = TIMER_CntDir_Up; // Считаем «вверх». CNT инкрементируется (CNT++).
timerButtonCheck_structInit.TIMER_CounterMode = TIMER_CntMode_ClkFixedDir; // Считаем в одну сторону, вверх.
timerButtonCheck_structInit.TIMER_ETR_FilterConf = TIMER_Filter_1FF_at_TIMER_CLK; // Сигнал зафиксирован в 1-м триггере на частоте TIM_CLK (В нашем случае оставляем по-умолчанию).
timerButtonCheck_structInit.TIMER_ETR_Polarity = TIMER_ETRPolarity_NonInverted; // ETR на входе не инвертируется (мы его и не используем).
timerButtonCheck_structInit.TIMER_ETR_Prescaler = TIMER_ETR_Prescaler_None; // Частота ETR на входе не делится (ETR не используем.).
timerButtonCheck_structInit.TIMER_EventSource = TIMER_EvSrc_TM2; // Таймер вызывает прерывание при CNT = ARR.
timerButtonCheck_structInit.TIMER_FilterSampling = TIMER_FDTS_TIMER_CLK_div_1; // FDTS = TIMER_CLK. (Так не используем.).
timerButtonCheck_structInit.TIMER_IniCounter = 0; // Считаем с 0. Начальное значение счетчика. (CNT = 0.).
timerButtonCheck_structInit.TIMER_Period = 250/25; // Считаем до указанного в параметрах функции значения (ARR = PWM_speed).
timerButtonCheck_structInit.TIMER_Prescaler = 32000 — 1; // Делитель входного сигнала. PSG регистр.

TIMER_CntInit(MDR_TIMER2, &timerButtonCheck_structInit); // Инициализируем основной таймер.
Далее воспользуемся встроенной в CMSIS функцией для разрешения прерывания от всего таймера (ее мы разобрали в предыдущей статье) и включим таймер.
Полноценная функция инициализации.
// Настройка таймера для генерации прерываний 25 раз в секунду при помощи SPL.
void initTimerButtonCheckSPL (void) 
{ 
 TIMER_CntInitTypeDef timerButtonCheck_structInit; // Структура для настройки основного таймера вызова прерывания для опроса клавиш.
 RST_CLK_PCLKcmd(RST_CLK_PCLK_TIMER2, ENABLE); // Включаем тактирование таймера 1.
 TIMER_BRGInit(MDR_TIMER2, TIMER_HCLKdiv1); // Подаем источник сигнала для счета (частота процессора без предделителя).
	
 // Заполняем структуру основного таймера.
 timerButtonCheck_structInit.TIMER_ARR_UpdateMode = TIMER_ARR_Update_Immediately; // Регистр ARR можно обновлять в любое время.
 timerButtonCheck_structInit.TIMER_BRK_Polarity = TIMER_BRKPolarity_NonInverted; // BRK сигнал не инвертируется (нас этот параметр не касается).
 timerButtonCheck_structInit.TIMER_CounterDirection = TIMER_CntDir_Up; // Считаем "вверх". CNT инкрементируется (CNT++). 
 timerButtonCheck_structInit.TIMER_CounterMode = TIMER_CntMode_ClkFixedDir; // Считаем в одну сторону, вверх.
 timerButtonCheck_structInit.TIMER_ETR_FilterConf = TIMER_Filter_1FF_at_TIMER_CLK; // Сигнал зафиксирован в 1-м триггере на частоте TIM_CLK (В нашем случае оставляем по-умолчанию).
 timerButtonCheck_structInit.TIMER_ETR_Polarity = TIMER_ETRPolarity_NonInverted; // ETR на входе не инвертируется (мы его и не используем).
 timerButtonCheck_structInit.TIMER_ETR_Prescaler = TIMER_ETR_Prescaler_None; // Частота ETR на входе не делится (ETR не используем.).
 timerButtonCheck_structInit.TIMER_EventSource = TIMER_EvSrc_TM2; // Таймер вызывает прерывание при CNT = ARR.
 timerButtonCheck_structInit.TIMER_FilterSampling = TIMER_FDTS_TIMER_CLK_div_1; // FDTS = TIMER_CLK. (Так не используем.).
 timerButtonCheck_structInit.TIMER_IniCounter = 0; // Считаем с 0. Начальное значение счетчика. (CNT = 0.).
 timerButtonCheck_structInit.TIMER_Period = 250/25;																// Считаем до указанного в параметрах функции значения (ARR = PWM_speed).
 timerButtonCheck_structInit.TIMER_Prescaler = 32000 - 1; // Делитель входного сигнала. PSG регистр. 

 TIMER_CntInit(MDR_TIMER2, &timerButtonCheck_structInit); // Инициализируем основной таймер.
	
 TIMER_ITConfig(MDR_TIMER2, TIMER_STATUS_CNT_ARR, ENABLE); // Разрешаем прерывание по CNT = ARR.	
 NVIC_EnableIRQ(Timer2_IRQn); // Разрешаем прерывание от таймера в целом.
	
 TIMER_Cmd(MDR_TIMER2, ENABLE); // Включаем таймер.
}

Переводим прерывание на SPL.

Последним шагом будет перевод на SPL функций в прерывании. Прерывание имеет все то же стандартное имя, указанное в стартап файле. Помним, что при входе в прерывание нам нужно сбросить флаг статуса таймера. Для этого служит функция TIMER_ClearFlag.
void TIMER_ClearFlag(MDR_TIMER_TypeDef* TIMERx, uint32_t Flags)
В качестве параметров требуется указать имя порта и флага прерывания. В нашем случае будет:
TIMER_ClearFlag(MDR_TIMER2, TIMER_STATUS_CNT_ARR); // Сбрасываем флаг. Обязательно первой коммандой.
После этого мы инвертировали состояние светодиода, показывая, что прерывание сработало. В SPL нет функции инвертирования бита, зато есть функция чтения и записи единичных бит. Ими и воспользуемся.
uint8_t PORT_ReadInputDataBit(MDR_PORT_TypeDef* PORTx, uint32_t PORT_Pin);
void PORT_WriteBit(MDR_PORT_TypeDef* PORTx, uint32_t PORT_Pin, BitAction BitVal);
У обеих первым параметром идет имя порта, далее у функции чтения нужно указать имя пина. Указывается именно маска. У функции записи, после имени порта следует указать бит, который записывается (так же маской) и значение бита (0 или 1). Функцию чтения можно использовать как параметр функции записи. Тогда мы получим:
PORT_WriteBit(MDR_PORTC, LED1, !PORT_ReadInputDataBit(MDR_PORTC, LED1)); // Записываем инвертированное значение бита.
После этого нам нужно считывать данные с кнопок. Причем по одной кнопке. Для этого воспользуемся функцией PORT_ReadInputDataBit, разобранной выше.
Опрос кнопок будет выглядеть так.
if (PORT_ReadInputDataBit(MDR_PORTB, UP_MSK) == 0) PWM_speed--; // Проверяем, нажата ли какая-нибудь клавиша. Если нажата - что-то делаем с частотой.
		else if (PORT_ReadInputDataBit(MDR_PORTE, DOWN_MSK) == 0) PWM_speed++;			
		else if (PORT_ReadInputDataBit(MDR_PORTE, LEFT_MSK) == 0) PWM_speed--;
		else if (PORT_ReadInputDataBit(MDR_PORTB, RIGHT_MSK)== 0) PWM_speed++;
Осталось только сменить частоту по окончании опроса кнопок. Для этого есть функция TIMER_SetCntAutoreload.
void TIMER_SetCntAutoreload(MDR_TIMER_TypeDef* TIMERx, uint16_t Autoreload)
Нам нужно лишь указать таймер с PWM и новую частоту.
По итогу имеем прерывание вида.
void Timer2_IRQHandler (void)
{
	TIMER_ClearFlag(MDR_TIMER2, TIMER_STATUS_CNT_ARR); // Сбрасываем флаг. Обязательно первой коммандой.																										
	PORT_WriteBit(MDR_PORTC, LED1, !PORT_ReadInputDataBit(MDR_PORTC, LED1)); // Записываем инвертированное значение бита.		
	if (PORT_ReadInputDataBit(MDR_PORTB, UP_MSK) == 0) PWM_speed--; // Проверяем, нажата ли какая-нибудь клавиша. Если нажата - что-то делаем с частотой.
		else if (PORT_ReadInputDataBit(MDR_PORTE, DOWN_MSK) == 0) PWM_speed++;			
		else if (PORT_ReadInputDataBit(MDR_PORTE, LEFT_MSK) == 0) PWM_speed--;
		else if (PORT_ReadInputDataBit(MDR_PORTB, RIGHT_MSK)== 0) PWM_speed++;
	
	// Проверяем, чтобы частота не вышла за пределы диапазона от 250 Гц до 0.5 Гц.
	if (PWM_speed < 1) PWM_speed = 1;									
		else if (PWM_speed > 500) PWM_speed = 500;
	
	TIMER_SetCntAutoreload(MDR_TIMER1, PWM_speed); // Меняем частоту. 					
}

Подведение итогов.


Как мы могли убедиться, с использованием SPL код стал выглядеть намного обьёмнее. Но давайте сравним вес полученного кода. В код, написанный лишь с использованием CMSIS со всеми видами оптимизации занимает столько.

Наш же код с оптимизацией -O0 весит столько.

С оптимизацией -O3.

Код с использованием SPL весит в 2-2.5 раза больше, чем написанный вручную. Результат не плохой, но все же уступает ручному написанию. Скорость выполнения, конечно, много меньше. Это тема отдельной статьи, коих не мало. Теперь подведем итоги.
Плюсы использования SPL
  1. Написанный с помощью SPL код воспринимается однозначно, при наличии соответствующего описания к самой SPL (чего пока, к сожалению, нет).
  2. При правильном заполнении структур — все будет сконфигурировано верно. Отсутствие ошибок. Мне пока не удалось обнаружить ни единой ошибки в SPL. Даже несмотря на то, что это B-версия.

Минусы использования SPL
  1. Размер кода больше, чем при ручной настройке.
  2. Скорость выполнения так же ниже.
  3. На данный момент с SPL сложнее разобраться, чем с CMSIS.
  4. Не всегда можно угадать логику человека, который писал библиотеку. Порой неработоспособность объясняется тем, что то, что должно было быть включено в функцию — вынесено из нее в другу.
  5. Огромное количество места строк кода для инициализации структур.

Вывод

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

Файл проекта.

P.S. Спасибо Amomum за подсказку с --feedback unused. Благодаря этому код стал весить действительно много меньше, в связи с этим подправил статью.
Список предыдущих статей.
Вадим @Vadimatorikda
карма
40,0
рейтинг 0,0
Программист, радиоинженер, Студент
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

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

  • 0
    А как же КДПВ? или не провёл ещё никто для наших импортозамещенниц няшные фотосессии?
    • 0
      КДПВ была целым видео к предыдущей статье (в первой части). Это, можно сказать, продолжение. Что не влезло в предыдущую (было бы сложно читать их вместе...). Так что КДПВ для обеих в первой.
  • +2
    А он такой же российский, как в своё время серия 580 была?
    • 0
      Разработка — да, но на основе Cortex-m3 купленного. Спор на эту тему был в самой первой статье. Можете изучить при желании.
  • 0
    Вся проблема перехода с STM32 на К1986ВЕ92QI заключается в отсутствии последнего а не в программировании банальной периферии.
    Я даже на аналог AVR никак не могу перейти т.к. не поставляется. Зато в железном корпусе.
    Может, через год микросхема придет, запаяю, пощупаю.
    • 0
      Периферия не всегда банальная. Взять тот же DMA… (Есть в предыдущих статьях). А так — да. Но никто не мешает позвонить в Миландр и договориться о покупке нескольких чипов. Вам еще их и доставят в любую точку страны.
      • 0
        Когда что-то работает совсем не так как описано, или описано неявно — это да, вызывает трудности. Тут и у именитых производителей проблемы бывают. В этом плане меня TI периодически напрягает.

        А вот наш чип хоть какой-нибудь пощупать пока не удалось.
        Плату не покупают т.к. этот космолет уже куплен, а пощупать нельзя т.к. не я один.
        Микросхема заказана, печать есть, в ней работает китайский дублер, ждем нашего (с китайским кристаллом?).
      • 0
        Если микросхемы такие дорогие и тяжело производимые — можно сделать отладочные платы в виде законченного устройства с электрически защищенными внешними интерфейсами с сдавать их в аренду за недорого.
        • 0
          Я вот думаю на эту тему. Вроде бы даже есть группы в контакте, которые продают готовые платки с этими контроллерами. Стоят значительно дешевле моей.
  • +1
    Попробуйте включить ключ компилятору и линкеру '--feedback unused', он должен выкинуть все неиспользуемые функции после двух компиляций. Как правило, размер бинарника уменьшается заметно.
    • 0
      Огромное спасибо! Сейчас заменю картинки и пересчитаю сравнение, для предоставления более точных результатов.
    • 0
      А -flto в их линкере поддерживается? Если да, то тоже способ чуток заоптимизировать.
      • 0
        Судя по тому, что я нагуглил, аналогом является галка «cross module optimization». На моем первом попавшемся под руку проекте никакого результата она не дала (но это, конечно, ни о чем не говорит).

        Тоже надо запомнить, спасибо за идею.
  • 0
    По поводу скорости выполнения — достаточно спорный вопрос. Как правило, SPL используется в основном на этапе инициализации периферии, т.е. один раз после включения, соответственно какого-то заметного эффекта на скорость работы прошивки это не оказывает.

    В каких-то критичных по времени кусках — в прерываниях, например — можно спокойно использовать только CMSIS (или ассемблер).
    • 0
      Полностью согласен. Но ради интереса в одной из следующих статей постараюсь использовать в прерывании сначала CMSIS, а потом SPL. В каком-нибудь очень критичном ко времени примере.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.