Chapter 6 Addendum: Input Modes Explained - Part 3

by <a href="http://community.silabs.com/t5/Welcome-and-Announcements/Community-Ranking-System-and-Recognition-Program/m-p/140490#U140490"><font color="#000000"><font size="2">Ninja</font></font> </a> lynchtron on ‎06-05-2017 04:06 PM

ch6_addendum.png

In the last section, the ACMP peripheral was used as an analog input device to level-shift the light detected from an LED on the light sensor.  In this section, we will configure the TIMER peripheral to receive input edges that can be used to decode a quadrature encoder.  We will also configure the PCNT as an input counter to count pulses from the blinking LED of the prior sections. 

 

TIMER

The TIMER peripheral was first covered in chapter 5 and used for measuring time or generating output waveforms, but it is also a versatile counter that is capable of capturing edges on the input pins to the MCU. 

 

In another mode called Quadrature Decoder mode, the TIMER module can be used across two TIMER capture pins to measure the speed and direction of a wheel.  The quadrature mode can work in either X2 or X4 modes, which simply means that both rising and falling edges of one or both pins can trigger counting events. 

 

To demonstrate the basic operation of the TIMER input mode, we will configure it to capture the direction and speed of a wheel using a quadrature encoder.  The ACZ11 mechanical encoder can be purchased on Mouser here, or you can find them on Amazon.  This one has 20 detents, which means that the rotation of the shaft provides a clicking feel and generates edges on the encoder pins with each click.  We can use such an encoder in X1, X2, or X4 modes.  The TIMER peripheral cannot count in X1 mode, but the PCNT module in the following section can only count in X1 mode. 

ACZ11_SPL.jpg

The ACZ11 has five pins: three pins on one side make up the encoder, the middle pin is ground, and the encoder mechanically connects the outside pins to this ground pin as the shaft rotates.  It is up to us to connect the outside pins to a pullup resistor so we can detect when the encoder is connecting a pin to ground.  The other two pins (on the other side of the encoder) make up a Single Pole Single Throw (SPST) switch when you push in on the knob, just like the volume/power knob on some automobile radios.

6_quadrature_decoder_scope_setup.png

When we rotate the encoder clockwise, edges on pin B lead pin A.  When we rotate counterclockwise, edges on pin A lead pin B. The width of the pulses is determined by the speed of rotation, and the signals are always at 50% duty cycle to one another. 

 

 The timer has three compare/capture pins, CC0, CC1, and CC2.  All of the pins can be used to clock the TIMER CNT field, but only CC0 and CC1 can be used to run the quadrature decoder.

6_quadrature_waveforms.png
No resistors are needed when using the encoder with the EFM32.  One of the input modes available on the GPIO peripheral is input mode with pullup, where the inputs provide ~40k ohm pullups to VCC.  This mode makes the connections easy.  Just connect one of the outside pins of the encoder to PD1, and the other to PD2.  These are TIMER0 location #3 compare and capture pins.

6_quadrature_decoder_connections.png

When TIMER is used as an input peripheral, it is counting incoming edges and not keeping track of time.  In quadrature mode, it is determining position or some fraction of rotation and not the rate of rotation.  To measure the rate of rotation, you need another timer to keep track of the sample period.

First, we will re-use the one second timer and interrupt from the ADC section of this chapter.  TIMER1 will keep track of time and interrupt after one second.  Then, we will set up TIMER0 as a quadrature counter connected to PD1 and PD2.

 

// This timer doesn't keep track of time, but edges on the input pins
void setup_quadrature_timer()
{
      CMU_ClockEnable(cmuClock_TIMER0, true);
      CMU_ClockEnable(cmuClock_GPIO, true);
 
      // Set CC0 channel defaults
      TIMER_InitCC_TypeDef timerCCInit = TIMER_INITCC_DEFAULT;
      timerCCInit.edge       = timerEdgeBoth;          // X4 mode
      timerCCInit.filter     = true;
 
      // Configure CC channel 0
      TIMER_InitCC(TIMER0, 0, &timerCCInit);
 
      // Configure CC channel 1 in exactly the same way
      TIMER_InitCC(TIMER0, 1, &timerCCInit);
 
      // Set TIMER0 defaults
      TIMER_Init_TypeDef timerInit = TIMER_INIT_DEFAULT;
      timerInit.mode       = timerModeQDec;      // Quadrature mode
      timerInit.quadModeX4 = false;             // X2
 
      // Configure TIMER0
      TIMER_Init(TIMER0, &timerInit);
 
      // Route the input pins to TIMER0 from Location 3 and enable CC0 and CC1 in the route
      TIMER0->ROUTE = (TIMER0->ROUTE & ~_TIMER_ROUTE_LOCATION_MASK) | TIMER_ROUTE_LOCATION_LOC3;
      TIMER0->ROUTE |= TIMER_ROUTE_CC0PEN;
      TIMER0->ROUTE |= TIMER_ROUTE_CC1PEN;
 
      // Enable the GPIO pins needed
 
      // CC0 on Location 3 is PD1
      GPIO_PinModeSet(gpioPortD, 1, gpioModeInputPullFilter, 1);
 
      // CC1 on Location 3 is PD2
      GPIO_PinModeSet(gpioPortD, 2, gpioModeInputPullFilter, 1);
}

I have enabled the filter on the CC0/CC1 configuration because I saw switch bounce on the signals on the mechanical encoder when viewing the output on the oscilloscope.

 

After setting up the two timers, the main while resets the TIMERs on each loop, then goes to sleep until TIMER1 wakes the MCU up after one second.  If we set up a variable called detents as an int16_t, only the TIMER0->CNT value is needed to fetch both direction and the number of detents detected.  This is because TIMER0->CNT is a 16-bit register, and the quadrature counter counts backwards when the shaft is rotated counterclockwise, producing a negative count.  However, in the code, you can also see how to fetch the direction bit from TIMER0->STATUS, and the results are consistent between the two methods.

 

      while (1)
      {
            // Reset the timers
            TIMER0->CNT = 0;
            TIMER1->CNT = 0;
 
            // Sleep until the timer expires
            EMU_EnterEM1();
 
            // One second has passed, fetch the number of detents
            int16_t detents = TIMER0->CNT;
 
            // Correct for X2 to X1 mode
            detents = detents / 2;
 
            if (detents != 0)
            {
                  // Fetch the direction
                  uint8_t direction = (TIMER0->STATUS & 0b10) >> 1;
 
                  // calculate the speed of rotation per minute, RPM
                  int16_t rpm = (detents * 60) / 20;
 
                  // Print out the message
                  if (direction == 1)
                  {
                        printf("Shaft Direction:CCW Detents:%d RPM:%d\n", detents, rpm);
                  }
                  else
                  {
                        printf("Shaft Direction:CW Detents:%d RPM:%d\n", detents, rpm);
                  }
            }
      }

I had to correct the count from X1 to X2 by dividing the TIMER0->CNT by 2 on every click of the encoder.   Note that you must do this calculation after TIMER->CNT has been converted to an int16_t or else you will lose the sign bit.  The check of detents != 0 ensures that we don’t see any output on the SWO debug printf console until there is rotation on the encoder.  The revolutions per minute (RPM) can be calculated by the number of detents times 60 seconds, divided by 20 detents per revolutions of the encoder.

 

You should see something like the following on the SWO console when you run the code, first by hand, and then by using a cordless drill to turn the knob at the drill's fastest setting.

 

Chapter 6: Input Modes!
Shaft Direction:CW Detents:1 RPM:3        << Turning by hand once each direction
Shaft Direction:CCW Detents:-1 RPM:-3
Shaft Direction:CW Detents:7 RPM:21
Shaft Direction:CW Detents:5 RPM:15
Shaft Direction:CW Detents:9 RPM:27
Shaft Direction:CW Detents:11 RPM:33      << Fastest able to turn by hand
Shaft Direction:CW Detents:112 RPM:336   
Shaft Direction:CW Detents:445 RPM:1335   << Using a ~1500 RPM drill
Shaft Direction:CW Detents:480 RPM:1440
Shaft Direction:CW Detents:480 RPM:1440
Shaft Direction:CW Detents:186 RPM:558
Shaft Direction:CCW Detents:-1 RPM:-3
Shaft Direction:CCW Detents:-145 RPM:-435
Shaft Direction:CCW Detents:-445 RPM:-1335  << Drill in reverse
Shaft Direction:CCW Detents:-447 RPM:-1341
Shaft Direction:CCW Detents:-120 RPM:-360
Shaft Direction:CCW Detents:-1 RPM:-3

My cordless drill lists the RPM spec at 1500 RPM, which is within 4% of what I measured in the clockwise direction, and 11% in the counterclockwise direction.  Since I have owned the drill for many years, this is within expectations.

 

More details about the TIMER peripheral can be found in Application Note AN0014 TIMER.

 

Pulse Counter (PCNT)

The Pulse Counter (PCNT) counts and measures incoming pulses to the MCU, even while the core is in EM3 sleep state.  Rather than wake the MCU on every edge on a GPIO pin, the PCNT counts pulses until a specific count is reached, which then wakes up the MCU if the PCNT interrupt is enabled.   Most EFM32 models have an 8-bit PCNT counter, capable of counting up to 255, but some models have a 16-bit counter on PCNT0.

 

The PCNT peripheral is a little less versatile than the TIMER peripheral, as it can only count either positive edges or negative edges, but not both in “single output oversampling” or “externally clocked single input” modes.  It can also work with quadrature encoders, but only in X1 mode.  It cannot operate in X2 or X4 quadrature modes.  However, it can keep counting all the way in EM3 energy mode if it is externally clocked by the pulses on the input pin, whereas the TIMER can only operate down to EM2 energy mode.

 

To demonstrate the basic operation of the PCNT, we can re-use our blinking LED and ACMP output to feed that output to the PCNT and count the number of LED pulses over time.  Once the count is reached, we can use the PCNT to interrupt the system and print the time to the SWO console.

6_pcnt.png
To re-use our blinking LED from the earlier section, all we need to do is place a wire jumper between PE2 (ACMP0_O) and PD0, which is our S0 pin for PCNT2.  There are two pins available for each PCNT, each with several locations, but the function that we want for S0 in this case is to clock the PCNT counter.  The optional S1 signal modifies the count of the S0 signal (forward or backwards), so be careful how you connect these pins. 

 

The following code configures PCNT’s S0 pin on PD0 to count in LFACLK oversampling mode.

 

#define PCNT_OVERFLOW                     (LED_BLINK_RATE / 2) -1
// Setup PCNT2 input on PD0, to count up to LED blink rate then generate interrupt
void setup_PCNT()
{
      /* Select LFRCO as clock source for LFA */
      CMU_ClockSelectSet(cmuClock_LFA, cmuSelect_LFRCO);
      CMU_ClockEnable(cmuClock_HFLE, true);     /* Enable renamed CORELE clock */
      CMU_ClockEnable(cmuClock_PCNT2, true);
      CMU_ClockEnable(cmuClock_GPIO, true);
 
      /* Set configuration for pulse counter */
      PCNT_Init_TypeDef pcntInit = PCNT_INIT_DEFAULT;
      pcntInit.mode       = pcntModeOvsSingle;  /* clocked by LFACLK */
      pcntInit.counter    = 0;                  /* Set initial value to 0 */
      pcntInit.top        = PCNT_OVERFLOW;      /* Set top to max value */
 
      /* Initialize Pulse Counter */
      PCNT_Init(PCNT2, &pcntInit);
 
      // Clear the interrupt flag
      pcnt_interrupt = false;
 
      /* Enable PCNT overflow interrupt */
      PCNT_IntEnable(PCNT2, PCNT_IF_OF);
 
      /* Enable PCNT2 interrupt vector in NVIC */
      NVIC_EnableIRQ(PCNT2_IRQn);
 
      /* Route PCNT2 input to location 0 -> PCNT2_S0IN on PD0 */
      PCNT2->ROUTE = PCNT_ROUTE_LOCATION_LOC0;
 
      // Enable the pin PD0 for the PCNT2_S0IN input
      GPIO_PinModeSet(gpioPortD, 0, gpioModeInput, 0);
 
      // Clear the count again, because enabling the already-high PD0 adds 1 to the count
      PCNT_CounterSet(PCNT2, 0);
}

Once the PCNT is configured, any pulse on the GPIO pin will trigger a count, even if the first pulse is provided simply by configuring the PD0 pin to enable the input mode if the PD0 pin happens to be at a high state when that occurs.  Therefore, the PCNT2 is reset to 0 after the GPIO mode is set to input.

At the module level (outside the main function), we define a global variable called pcnt_interrupt.

bool pcnt_interrupt = false;

We then setup a PCNT2 interrupt handler that simply sets pcnt_interrupt to true whenever the interrupt occurs.

 

void PCNT2_IRQHandler(void)
{
      /* Clear PCNT0 overflow interrupt flag */
      PCNT_IntClear(PCNT2, PCNT_IF_OF);
 
      pcnt_interrupt = true;
}

Now, in the main while loop, the pcnt_interrupt flag can be monitored and a message can be printed whenever the PCNT interrupt is triggered.

 

    setup_ACMP();
    setup_elapsed_timer();
    setup_blink_timer();
    setup_PCNT();
 
    TIMER1->CNT = 0;
    TIMER0->CNT = 0;
 
      while (1)
      {
            // Sleep until the timer expires
            EMU_EnterEM1();
 
            if (pcnt_interrupt)
            {
                  printf("PCNT Interrupt! Elapsed CNT:%d\n", TIMER1->CNT);
                  pcnt_interrupt = false;
                  TIMER1->CNT = 0;
            }
      }

Since TIMER1 is being used to track time and TIMER0 is being used to blink the LED, they are synchronized to 0 (as much as possible) before the while loop begins, just so that the counts can be compared easily and we don’t miss any early edges.  TIMER1 is cleared after every pcnt_interrupt occurs so we can use the TIMER1 CNT value directly without requiring any overflow logic.

 

If all goes well, you should see a listing of TIMER1 clock counts that occur between each PCNT2 interrupt.  Remember that you only have 16-bits on the TIMER peripheral, which means that it only counts from 0 to 65,535, and 8-bits (on most EFM32 models) for PCNT, which only counts from 0 to 255.  In this case, that works well because we expected to find 10 LED pulses in one second on PCNT, which is 13672 TIMER clock counts.  If your events take longer or your TIMER is configured with a lower divisor, you could run out of room in the CNT register before the required pulses arrive.

 

Chapter 6: Input Modes!
PCNT Interrupt! Elapsed CNT:13681
PCNT Interrupt! Elapsed CNT:13669
PCNT Interrupt! Elapsed CNT:13669
PCNT Interrupt! Elapsed CNT:13669
PCNT Interrupt! Elapsed CNT:13669
PCNT Interrupt! Elapsed CNT:13670
PCNT Interrupt! Elapsed CNT:13669
PCNT Interrupt! Elapsed CNT:13669
PCNT Interrupt! Elapsed CNT:13669
PCNT Interrupt! Elapsed CNT:13670
PCNT Interrupt! Elapsed CNT:13669
PCNT Interrupt! Elapsed CNT:13669

For some reason, the first loop had a little bit more error than subsequent loops, but the overall error of TIMER1’s 13672 clock count to PCNT’s 13669 to 13681 clocks between interrupts is only a 0.1% measurement error.

 

In oversampling mode, the PCNT has a maximum count rate of the LFACLK/2, which means that it can work reliably up to 32kHz.  If your pulses are faster than that, you will need to run the PCNT from the external clock pulses directly on the S0 pin, rather than sample the S0 pin with the LFACLK.  When you go that route, the PCNT peripheral has no internal clock, and your PCNT configuration write cycles will have no effect until there are three pulses on the S0 pin.  To make things easier, you can do your configuration of PCNT first while running on the LFACLK, and then switch to external S0 clocking after the PCNT is configured.  In externally-clocked mode, the PCNT can operate at a frequency up to the maximum specified rate of the MCU core clock.

 

The PCNT can be used to count objects passing by a sensor, where an LED or an infrared LED is emitted and detected by a light sensor.  When objects block the LED, the light sensor sees pulses/edges and the PCNT can then count the number of objects passing the sensor.  It can also be used to count revolutions of a wheel, either with the same light emitter/sensor or with current-inducing pulses created by magnets on the wheel that pass by a stationary sensor.

 

More details about the PCNT peripheral can be found in Application Note AN0024 Pulse Counter.

 

This wraps up our coverage of MCU input mechanisms.