December 28, 2015

High-Speed Quadrature Decoding with the STM32F4 Nucleo...Because Lasers and Bass

I've been using the STM32F4 Nucleo dev boards for everything lately, and needed to do high speed (edges at MHz frequency) quadrature decoding with one.  Here are the snippets of code to do this.

This setup uses PA_0 and PA_1 plus Timer 2 (one of the two 32-bit timers), which are on the Arduino-compatible A0 and A1 of the Nucleo board (pinout).

The setup code is mostly copy-and-pasted from here.  That code got me to encoder edges at ~600 Khz before it started losing counts.  Turns out there's a digital filter on the GPIO pins I had inadvertently turned on with the copy-pasta code.

    // configure GPIO PA0 & PA1 as inputs for Encoder
    RCC->AHB1ENR |= 0x00000001;  // Enable clock for GPIOA
    GPIOA->MODER   |= GPIO_MODER_MODER0_1 | GPIO_MODER_MODER1_1 ;           //PA0 & PA1 as Alternate Function   /*!< GPIO port mode register,               Address offset: 0x00      */
    GPIOA->OTYPER  |= GPIO_OTYPER_OT_0 | GPIO_OTYPER_OT_1 ;                 //PA0 & PA1 as Inputs               /*!< GPIO port output type register,        Address offset: 0x04      */
    GPIOA->OSPEEDR |= 0x00000011;//|= GPIO_OSPEEDER_OSPEEDR0 | GPIO_OSPEEDER_OSPEEDR1 ;     // Low speed                        /*!< GPIO port output speed register,       Address offset: 0x08      */
    GPIOA->PUPDR   |= GPIO_PUPDR_PUPDR0_1 | GPIO_PUPDR_PUPDR1_1 ;           // Pull Down                        /*!< GPIO port pull-up/pull-down register,  Address offset: 0x0C      */
    GPIOA->AFR[0]  |= 0x00000011 ;                                          //  AF01 for PA0 & PA1              /*!< GPIO alternate function registers,     Address offset: 0x20-0x24 */
    GPIOA->AFR[1]  |= 0x00000000 ;                                          //                                  /*!< GPIO alternate function registers,     Address offset: 0x20-0x24 */
    // configure TIM2 as Encoder input
    RCC->APB1ENR |= 0x00000001;  // Enable clock for TIM2
    TIM2->CR1   = 0x0001;     // CEN(Counter ENable)='1'     < TIM control register 1
    TIM2->SMCR  = 0x0003;     // SMS='011' (Encoder mode 3)  < TIM slave mode control register
    TIM2->CCMR1 = 0x0101;     // CC1S='01' CC2S='01'         < TIM capture/compare mode register 1
    TIM2->CCMR2 = 0x0000;     //                             < TIM capture/compare mode register 2
    TIM2->CCER  = 0x0011;     // CC1P CC2P                   < TIM capture/compare enable register
    TIM2->PSC   = 0x0000;     // Prescaler = (0+1)           < TIM prescaler
    TIM2->ARR   = 0xffffffff; // reload at 0xfffffff         < TIM auto-reload register
    TIM2->CNT = 0x0000;  //reset the counter before we use it  

And to read the count, just

position = TIM2->CNT;

The critical change was in line 16, which used to read

    TIM2->CCMR1 = 0xF1F1;     

Changing "F" to "0" disables the filter for input capture 1 and 2.  "F" sets the filter as slow as possible, so I likely could have left some filtering and still gotten good enough results (with perhaps better noise-immunity).  More details about filtering can be found on page 358 of the 841 page reference manual for the STM32F4 series.

I was able to test up to ~5 Mhz edges using a 12,500 CPR optical encoder coupled to a DC motor, with no missed counts.  Probably could have gone faster, but I didn't have an easy way of testing.

%%  end of useful reference material, on to project-ramblings  %%
%%  proceed at your own risk  %%

Now why did I need to read encoder edges at several megahertz?  Well, this term I took 2.171 (Digital Feedback Control Systems, with Prof. David Trumper and TA Will Bosworth.  A++, would highly recommend.), and for my final project built a subwoofer with closed-loop velocity control of the speaker cone from an audio signal reference.

Lots of possible velocity sensing methods exist, and they can basically all be broken down into three categories:  direct velocity measurement, position measurement with differentiation to get velocity, and acceleration measurement with integration to get velocity.

For direct velocity measurement, I thought of a few strategies.
  • Attach a second voice coil to the speaker cone.  Voice coil voltage will be proportional to velocity.  The only tricky bit is making sure the magnetics of the second voice coil are completely isolated from those of the driving coil.
  • Wrap a second coil of very thin sense-windings around the original voice coil windings.  This has been done.  The problem with this strategy is that, in addition to the back-emf from the permanent magnet, the driving coil and sense coil are coupled.  This can be sorted out with some math, but I preferred not to go that route.
  • Somehow measure the back-emf of the coil between switching transients.  During the dead-time in the switching of the h-bridge (or Class-D amplifier with no output filter, in audio-lingo), the voltage across the coil isn't railed at (±)power-supply voltage, and it should be possible to figure out the speaker's back emf during this period.  I stared at a scope trace of coil voltage for a few minutes and decided this would be really hard.  This would be the holy grail of closed-loop speaker nonsense, as it requires no external sensor.
Measuring position and differentiating can be risky business, because high-frequency noise gets amplified by differentiation.  A few linear position measuring methods are:
  • Laser + mirror + 1-D PSD, as suggested by Prof. Trumper.  Fix a laser, and point it at a little mirror on the speaker cone.  The reflected laser spot lands on the PSD, which gives you cone position.  Gain of the sensor can be adjusted by changing the angles of the laser, mirror, or PSD.  Sadly, PSDs proved difficult to find over a short timeframe.
  • Linear optical encoder strip.  These exist down to absurd resolution.
  • Magnet + hall effect sensor.  I built one of these, but it just didn't have the necessary resolution. 
  • And many more
And of course, for acceleration + integration to get velocity, just stick an accelerometer on the speaker cone.  This could be an attractive option- the usual drift problem associated with integrating an accelerometer can be solved by a slow high-pass filter, since audio is always AC anyway.  However, the accelerations are actually very large:  1 mm peak-to-peak excursion at 100 Hz is already 20 G's of acceleration, which would saturate a lot of cheap accelerometers.

So the path forward was unclear.  The most reasonable solutions are probably the secondary voice coil for sensing or the secondary sense winding, but I was talking to Peter about this, and he suggested using an interferometer for position feedback, and said he had all the parts to put one together.  Cut to a few days later, I show up at MITERS and Peter's built an interferometer in the back room.  The eventual output of the interferometer was to be a digital quadrature signal, hence the need for high-speed quadrature decoding.

So begins the story of the closed-loop subwoofer with interferometer feedback.  More details about the hardware and controls stuff to follow soon...


  1. Excuse me, I don't know nothing about mechanics or DIY-builts... but I am very found of sound, I did not understood what you planned to do with this speaker, but I am very curious about it. I mean, what would be the result in sound? Did you planned it or just tried to see?

  2. Aude: Check the paper at link 'this has been done' in above discussion of sense methods, has section on benefits of position sensing.