вівторок, 8 березня 2016 р.

Таймери STM32 -- ШІМ/HAL

Беремо STM32CubeMX проект із попереднього прикладу (із відліком) та додаємо до нього TIM3, який генеруватиме на своєму каналі CH2, під'єднаному до PA7, PWM (ШІМ), змінюючи яскравість світлодіода, як і в попередньому пості, але засобами HAL.

"Малюємо" проект


Конфігуруємо TIM3

На вкладці Configuration тиснемо TIM3, бачимо вже звичне віконечко, в якому, правда, з'явився пункт PWM Generation Channel 2. Всі налаштування очевидні:


Варто зайти на вкладку GPIO Settings цього діалогу, хоча б щоб дати назву піну (згенеровані при виборі піна налаштування по замовчуванню цілком підійдуть):

Можна генерувати код (решту налаштувань детально розглядалися в попередньому пості). Зокрема, в наступних постах, до налаштування RCC не повертатимемося -- вони всюди однакові.

Код проекту

main()


У коді додасться функція MX_TIM3_Init(), яку слід викликати разом із іншими. Частина, пов'язана із TIM3, виглядатиме так:

  MX_TIM3_Init();

  HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
   HAL_Delay(50);
   if( TIM3->CCR2<TIM3->ARR )
    TIM3->CCR2 += 10;
   else
    TIM3->CCR2 = 10;
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

Функція HAL_TIM_PWM_Start() запускає генерацію ШІМ (PWM) на каналі 2. Раз HAL сам надає відлік часу, для затримки скористаємося його HAL_Delay(). Можливо погано шукав, але не знайшовши макрос чи функцію для маніпуляції CCRx, без якоїсь додаткової зміни стану таймера чи каналу, тому змінюю його вручну, згідно ідеї, вираженої в попередньому пості, що HAL не заважає (в більшості випадків) безпосередньо звертатися до периферії, якщо є така потреба.

Ініціалізація та запуск

Розглянемо коротко MX_TIM3_Init() та  HAL_TIM_PWM_Start() -- нові функції, пов'язані із таким використанням TIM3 та зміни до інших функцій, ним викликані -- нові рядки в HAL_TIM_Base_MspInit(). (Дозвіл тактування порту А виконується в MX_GPIO_Init() з gpio.c).

MX_TIM3_Init()

TIM_HandleTypeDef htim3;

/* TIM3 init function */
void MX_TIM3_Init(void)
{
  TIM_ClockConfigTypeDef sClockSourceConfig;
  TIM_MasterConfigTypeDef sMasterConfig;
  TIM_OC_InitTypeDef sConfigOC;

  htim3.Instance = TIM3;
  htim3.Init.Prescaler = 0;
  htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim3.Init.Period = 1000;
  htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  HAL_TIM_Base_Init(&htim3);

  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig);

  HAL_TIM_PWM_Init(&htim3);

  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig);

  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 0;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_2);

} 
 
Знайому з попереднього поста (див. MX_TIM1_Init()) частину пропустимо: це ініціалізація таймера викликом HAL_TIM_Base_Init(), джерел тактування функцією HAL_TIM_ConfigClockSource() та (відсутніх) підлеглих таймерів, HAL_TIMEx_MasterConfigSynchronization().

Новим є використання структури конфігурації каналу, TIM_OC_InitTypeDef, у якій вибирається режим PWM -- mode 1 (OCMode = TIM_OCMODE_PWM1), початкова тривалість імпульсу (Pulse = 0) та полярність (OCPolarity = TIM_OCPOLARITY_HIGH). Ще ця структура містить поля OCNPolarity (полярність інвертованого каналу, якщо такий присутній -- TIM3 його не має, це "привілей" лише просунутих таймерів), OCFastMode -- швидкий режим (подробиці див. документацію), OCIdleState -- стан піна під час стану спокою і OCNIdleState -- стан інвертованого піну, якщо такий є (див. Drivers\STM32F1xx_HAL_Driver\Src\stm32f1xx_hal_tim.h).

Функція ініціалізації відносно проста (варіанти для інших каналів викинуто для компактності):

HAL_StatusTypeDef HAL_TIM_PWM_ConfigChannel(TIM_HandleTypeDef *htim, TIM_OC_InitTypeDef* sConfig, uint32_t Channel)
{
  __HAL_LOCK(htim);

  /* Check the parameters */
  assert_param(IS_TIM_CHANNELS(Channel));
  assert_param(IS_TIM_PWM_MODE(sConfig->OCMode));
  assert_param(IS_TIM_OC_POLARITY(sConfig->OCPolarity));
  assert_param(IS_TIM_OCN_POLARITY(sConfig->OCNPolarity));
  assert_param(IS_TIM_FAST_STATE(sConfig->OCFastMode));
  assert_param(IS_TIM_OCNIDLE_STATE(sConfig->OCNIdleState));
  assert_param(IS_TIM_OCIDLE_STATE(sConfig->OCIdleState));

  htim->State = HAL_TIM_STATE_BUSY;

  switch (Channel)
  {
    case TIM_CHANNEL_1:
    {
// <.................. SKIPPED ....................>
    }
    break;

    case TIM_CHANNEL_2:
    {
      assert_param(IS_TIM_CC2_INSTANCE(htim->Instance));
      /* Configure the Channel 2 in PWM mode */
      TIM_OC2_SetConfig(htim->Instance, sConfig);

      /* Set the Preload enable bit for channel2 */
      htim->Instance->CCMR1 |= TIM_CCMR1_OC2PE;

      /* Configure the Output Fast mode */
      htim->Instance->CCMR1 &= ~TIM_CCMR1_OC2FE;
      htim->Instance->CCMR1 |= sConfig->OCFastMode << 8;
    }
    break;

    case TIM_CHANNEL_3:
    {
// <.................. SKIPPED ....................>    }
    break;

    case TIM_CHANNEL_4:
    {
// <.................. SKIPPED ....................>    }
    break;

    default:
    break;
  }

  htim->State = HAL_TIM_STATE_READY;

  __HAL_UNLOCK(htim);

  return HAL_OK;
}

Макрос __HAL_LOCK перевіряє поле Lock структури TIM_HandleTypeDef, що описує таймер. Якщо таймер заблокований -- повертає HAL_BUSY, інакше блокує його. (Дана функція, правда, ігнорує результат цього макросу), __HAL_UNLOCK -- розблоковує, записуючи в поле Lock значення HAL_UNLOCKED.

Перевірка параметрів вже має бути менш-більш звичною. Після всіх перевірок переходить до конфігурування. Спершу викликає  функцію TIM_OC2_SetConfig(), потім встановлює preload-біт для CCR2, налаштовує (у нас -- вимикає) швидкий режим.

Викликана TIM_OC2_SetConfig() робить все решту:

void TIM_OC2_SetConfig(TIM_TypeDef *TIMx, TIM_OC_InitTypeDef *OC_Config)
{
  uint32_t tmpccmrx = 0;
  uint32_t tmpccer = 0;
  uint32_t tmpcr2 = 0;

  /* Disable the Channel 2: Reset the CC2E Bit */
  TIMx->CCER &= ~TIM_CCER_CC2E;

  /* Get the TIMx CCER register value */
  tmpccer = TIMx->CCER;
  /* Get the TIMx CR2 register value */
  tmpcr2 =  TIMx->CR2;

  /* Get the TIMx CCMR1 register value */
  tmpccmrx = TIMx->CCMR1;

  /* Reset the Output Compare mode and Capture/Compare selection Bits */
  tmpccmrx &= ~TIM_CCMR1_OC2M;
  tmpccmrx &= ~TIM_CCMR1_CC2S;

  /* Select the Output Compare Mode */
  tmpccmrx |= (OC_Config->OCMode << 8);

  /* Reset the Output Polarity level */
  tmpccer &= ~TIM_CCER_CC2P;
  /* Set the Output Compare Polarity */
  tmpccer |= (OC_Config->OCPolarity << 4);

  if(IS_TIM_CCXN_INSTANCE(TIMx, TIM_CHANNEL_2))
  {
    assert_param(IS_TIM_OCN_POLARITY(OC_Config->OCNPolarity));
    assert_param(IS_TIM_OCNIDLE_STATE(OC_Config->OCNIdleState));
    assert_param(IS_TIM_OCIDLE_STATE(OC_Config->OCIdleState));

    /* Reset the Output N Polarity level */
    tmpccer &= ~TIM_CCER_CC2NP;
    /* Set the Output N Polarity */
    tmpccer |= (OC_Config->OCNPolarity << 4);
    /* Reset the Output N State */
    tmpccer &= ~TIM_CCER_CC2NE;

  }

  if(IS_TIM_BREAK_INSTANCE(TIMx))
  {
    /* Check parameters */
    assert_param(IS_TIM_OCNIDLE_STATE(OC_Config->OCNIdleState));
    assert_param(IS_TIM_OCIDLE_STATE(OC_Config->OCIdleState));

    /* Reset the Output Compare and Output Compare N IDLE State */
    tmpcr2 &= ~TIM_CR2_OIS2;
    tmpcr2 &= ~TIM_CR2_OIS2N;
    /* Set the Output Idle state */
    tmpcr2 |= (OC_Config->OCIdleState << 2);
    /* Set the Output N Idle state */
    tmpcr2 |= (OC_Config->OCNIdleState << 2);
  }

  /* Write to TIMx CR2 */
  TIMx->CR2 = tmpcr2;

  /* Write to TIMx CCMR1 */
  TIMx->CCMR1 = tmpccmrx;

  /* Set the Capture Compare Register value */
  TIMx->CCR2 = OC_Config->Pulse;

  /* Write to TIMx CCER */
  TIMx->CCER = tmpccer;
}

Детально коментувати її не буду, але з поправкою на if()-и, які налаштовують інвертовані канали та поведінку по break ("аварійна зупинка") -- відсутні для TIM3 можливості, порівняйте код із тим, що писався вручну у попередньому пості. Знайомо, правда? :-) Зауважу хіба, що функція не заморочується встановленням/очищенням окремих бітових полів -- вона спочатку їх повністю зануляє, а потім записує потрібне значення.

Як би там не було, знову видно, що концепція та реалізація HAL -- доволі проста.

HAL_TIM_PWM_Start()

Доволі проста функція, загальна схема якої має бути знайомою із попереднього поста:

/**
  * @brief  Starts the PWM signal generation.
  * @param  htim : TIM handle
  * @param  Channel : TIM Channels to be enabled
  *          This parameter can be one of the following values:
  *            @arg TIM_CHANNEL_1: TIM Channel 1 selected
  *            @arg TIM_CHANNEL_2: TIM Channel 2 selected
  *            @arg TIM_CHANNEL_3: TIM Channel 3 selected
  *            @arg TIM_CHANNEL_4: TIM Channel 4 selected
  * @retval HAL status
*/
HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel)
{
  /* Check the parameters */
  assert_param(IS_TIM_CCX_INSTANCE(htim->Instance, Channel));

  /* Enable the Capture compare channel */
  TIM_CCxChannelCmd(htim->Instance, Channel, TIM_CCx_ENABLE);

  if(IS_TIM_BREAK_INSTANCE(htim->Instance) != RESET)
  {
    /* Enable the main output */
    __HAL_TIM_MOE_ENABLE(htim);
  }

  /* Enable the Peripheral */
  __HAL_TIM_ENABLE(htim);

  /* Return function status */
  return HAL_OK;
}

Як і обіцяв, про біт MOE, будь такий у таймера є, HAL турбується, дозволяючи і просунутим таймерам керувати виводами.

Існують також "колеги" цієї функції, із трішки відмінними властивостями: HAL_TIM_PWM_Start_IT() та HAL_TIM_PWM_Start_DMA() і "опоненти": HAL_TIM_PWM_Stop(), HAL_TIM_PWM_Stop_IT(), HAL_TIM_PWM_Stop_DMA().

Використана нею TIM_CCxChannelCmd() теж проста -- всі дії мали б бути знайомими після ручної роботи з CMSIS:

void TIM_CCxChannelCmd(TIM_TypeDef* TIMx, uint32_t Channel, uint32_t ChannelState)
{
  uint32_t tmp = 0;

  /* Check the parameters */
  assert_param(IS_TIM_CC1_INSTANCE(TIMx));
  assert_param(IS_TIM_CHANNELS(Channel));

  tmp = TIM_CCER_CC1E << Channel;

  /* Reset the CCxE Bit */
  TIMx->CCER &= ~tmp;

  /* Set or reset the CCxE Bit */
  TIMx->CCER |=  (uint32_t)(ChannelState << Channel);
}

(Звичайно, всі "знайомо", "просто" і т.д., виходять із того, що читач володіє бітовими маніпуляціями і сяк-так розібрався із відповідними місцями попередніх постів.)

HAL_TIM_Base_MspInit() -- нове

Новими у цій функції є наступні рядки, що стосуються TIM3:

else if(htim_base->Instance==TIM3)
  {
  /* USER CODE BEGIN TIM3_MspInit 0 */

  /* USER CODE END TIM3_MspInit 0 */
    /* Peripheral clock enable */
    __TIM3_CLK_ENABLE();
  
    /**TIM3 GPIO Configuration    
    PA7     ------> TIM3_CH2 
    */
    GPIO_InitStruct.Pin = RED_LED_PA7_TIM3CH2_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_HIGH;
    HAL_GPIO_Init(RED_LED_PA7_TIM3CH2_GPIO_Port, &GPIO_InitStruct);

  /* USER CODE BEGIN TIM3_MspInit 1 */

  /* USER CODE END TIM3_MspInit 1 */
  }

Як бачимо, все просто -- вмикається тактування таймера, пін налаштовується в режимі AF-PP -- Alternative function-push-pull.

Завершальне слово

Повний main.c не наводжу -- весь код із нього описано, а виглядає він, через коментарі Cube, громіздко.

Скачати проект можна тут.

Немає коментарів:

Дописати коментар