Search code examples
ctimerstm32gamecontroller

Programming rotary encoder with timers


I've designed a custom PCB with an STM32F103CBT6 microcontroller and a 16 MHz crystal oscillator. The board features a button matrix with 16 push buttons, 2 standalone buttons, and 4 rotary encoders.

While I've successfully programmed the button matrix and standalone buttons using a polling method to check button states, I'm now tackling the challenge of programming the rotary encoders. I'm exploring two options.

The first option involves setting all encoder pins as gpio_input and monitoring them for state changes, similar to the button matrix (e.g., registering an input when a pin goes low). However, this introduces bouncing/jumping on the pins, leading to incorrect or duplicate input.

The second option, which has been recommended to me, is working with timers. Each encoder is connected to Channel 1 and Channel 2 of a timer. For example, Encoder 1 is connected to Timer 1 (Channel 1 = PA8 and Channel 2 = PA9), Encoder 2 to Timer 2 (Channel 1 = PA0 and Channel 2 = PA1), Encoder 3 to Timer 3 (Channel 1 = PA6 and Channel 2 = PA7), and Encoder 4 to Timer 4 (Channel 1 = PB6 and Channel 2 = PB7).

As I'm not too familiar with working with timers on an STM32, I've looked for information online and tried various approaches with minor success. Currently, I have Encoder 1 working with Timer 1, but the input is incorrect, providing random button 1 and button 2 inputs, and not even on every pulse.

The objective is to generate unique values for clockwise and counterclockwise movements for each encoder, which will be sent to the PC as an HID report. This means I need 8 different input values (2 per encoder, x4), such as clockwise bit 0 and counterclockwise bit 1 for Encoder 1, and bit 2 and bit 3 for Encoder 2, and so on.

The code i currently have with timer config:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM1) {
        int16_t currentEncoderValue = TIM1->CNT;
        int16_t encoderDiff = currentEncoderValue - prevEncoderValue;

        if (encoderDiff > 0) {
            // Clockwise rotation detected
            buttonReport.buttons |= (1 << 0);
        } else if (encoderDiff < 0) {
            // Counterclockwise rotation detected
            buttonReport.buttons |= (1 << 1);
        }

        if (encoderDiff != 0) {
            buttonReport.report_id = 1;
            USBD_CUSTOM_HID_SendReport(&hUsbDeviceFS, (uint8_t*)&buttonReport, sizeof(buttonReport));
        }

        prevEncoderValue = currentEncoderValue;
        __HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE);
    }
}

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_TIM1_Init();
  MX_TIM2_Init();
  MX_TIM3_Init();
  MX_TIM4_Init();
  MX_USB_DEVICE_Init();

  HAL_TIM_Encoder_Start(&htim1, TIM_CHANNEL_ALL);
  HAL_TIM_Encoder_Start(&htim2, TIM_CHANNEL_ALL);
  HAL_TIM_Encoder_Start(&htim3, TIM_CHANNEL_ALL);
  HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL);

  HAL_TIM_Base_Start_IT(&htim1);
  HAL_TIM_Base_Start_IT(&htim2);
  HAL_TIM_Base_Start_IT(&htim3);
  HAL_TIM_Base_Start_IT(&htim4);

  while (1) {
      SCANALL();
       HAL_Delay(50);
  }
}

static void MX_TIM1_Init(void)
{

  /* USER CODE BEGIN TIM1_Init 0 */

  /* USER CODE END TIM1_Init 0 */

  TIM_Encoder_InitTypeDef sConfig = {0};
  TIM_MasterConfigTypeDef sMasterConfig = {0};

  /* USER CODE BEGIN TIM1_Init 1 */

  /* USER CODE END TIM1_Init 1 */
  htim1.Instance = TIM1;
  htim1.Init.Prescaler = 1;
  htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim1.Init.Period = 1;
  htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim1.Init.RepetitionCounter = 0;
  htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
  sConfig.EncoderMode = TIM_ENCODERMODE_TI12;
  sConfig.IC1Polarity = TIM_ICPOLARITY_FALLING;
  sConfig.IC1Selection = TIM_ICSELECTION_DIRECTTI;
  sConfig.IC1Prescaler = TIM_ICPSC_DIV1;
  sConfig.IC1Filter = 10;
  sConfig.IC2Polarity = TIM_ICPOLARITY_FALLING;
  sConfig.IC2Selection = TIM_ICSELECTION_DIRECTTI;
  sConfig.IC2Prescaler = TIM_ICPSC_DIV1;
  sConfig.IC2Filter = 10;
  if (HAL_TIM_Encoder_Init(&htim1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }
  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
  if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM1_Init 2 */

  /* USER CODE END TIM1_Init 2 */

}

Solution

  • Cube/HAL calls HAL_TIM_PeriodElapsedCallback() upon Update event, i.e. counter rollover, so with your setup it takes 65535 pulses when the encoder is turned in one direction, to achieve that.

    The usual/recommended procedure is to sample the counters periodically, e.g. in a timed loop in main(). Interrupts are not that useful with encoders.

    Btw., TIM1 specifically has separate interrupt vectors for its various interrupt sources, see vector table in startup file (for update, it's TIM1_UP_IRQHandler).