Search
  • theengineeringocto

Measuring Speed of a BLDC E-Bike Hub Motor with STM32

Updated: Nov 30, 2020




I've recently embarked on a new challenge; designing an e-bike controller. Part of the project requires the need, the need to measure speed! </poor Top Gun pun>



The schematic is mostly complete (and will be blogged here at a later stage), and so I started thinking about how I will measure the speed, needed for my speedo.



For the project, I'm using an STM32f030xxxx. I haven't yet placed the order for them, but I do have STM32F407 discovery board lying around, which I can use as a pretty close substitute for the time being.


Hall Effect Sensors

The BLDC motor I have (I believe it's a chinese rip-off of a continental 9 motor) is a 1000W, 3 phase BLDC. It has 3 hall effect sensors built in, which you can provide with either 3.3V or 5V, and they will output a binary code to tell you what phases needed to be energised to give rotation. I won't go into the details here, but what's important is that for every magnet pole that goes over a hall effect sensor, the output toggles.

I clamped the BLDC hub motor, and wheel, with a vice. I used a blue pen to colour one of the spokes, so I know where I start, and a pair of long nose locking pliers as a crude pointer. I then rotated the wheel 4 times, and used my Rigol DS1054Z scope to count the pulses. 92 pulses, divided by 4 rotations, gives 23 pulses per rotation. It's important to do at least 2 rotations, in order to get a good average.


The circuit I used to filter and pull high the outputs of the hall effect sensors. Most hall effects are what's called 'open collector output'. This means that their output is grounded when the sensor detects a magnetic field, and floating when it doesn't. the 100K resistors are required to pull the output high, and the 100n farad capacitor to ground creates a low pass filter - needed to remove and noise from the motor and long wires.


The Math

So, now we know how many pulses there are in one wheel rotation, that's great! We can now calculate the distance travelled from this.

The above equation is one you've no doubt seen before. Another equation you might be familiar with is:

These can be combined to give speed of a wheel (a rotating circle):


And how does this tie up with counting those pulses, you ask? Well, we now know (thanks to the handy locking pliers - vice setup) that 23 pules is 1 full rotation. Logically, if we count 12 pulses, then we have rotated just over a half a full rotation, or half the circumference. Our equation above is missing the conversion of pulse count to number of rotations, so let's fix that:

We have now multiplied by a ratio of the number of pulses we will count, by how many pulses there are in a single rotation. You can see that if we counted 46 pulses, we'd have a multiple of 2, which would mean the distance travelled is 2 times the circumference, which is indeed correct.


Currently, if the diameter, 'D', is in meters (which it usually is), and time is in seconds, we have units of meters per second, or m/s. I don't know about you, but I can't work in m/s!

By multiplying by 2.23694, we can convert meters/second into miles/hour. Perfect.


Finally, we have one last thing to do. We need to add time. All types of speed measurements are actually an average speed - it's just that some averages are over a much shorter time (like speed cameras!). For my ebike, I think it would be suitable to update the speed reading every half a second, so time will equal 0.5, yielding our final equation:



Counting Pulses with STM32

The maths now done (not that there was a great deal of it). From it, we can see we need to count the number of pulses that occur in 0.5 seconds. There's many ways this could be done, but the method I chose was to trigger an interrupt on the rising edge of the hall effect pulse, and another interrupt after a 0.5s timer had elapsed. I've expressed this as a crude state diagram below, to help visualise the idea.




I'm not a fan of the HAL libraries for STMs, I prefer to to code straight down to the hardware level, after all, I'm an electronics engineer! It also gives me better visibility of what's going on, and also helps reduce the high level nature of microcontrollers - I find this important when I'm also designing the electronics too.


//enable the GPIO port A CLK
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

The first part of the code is setting up the digital input for the hall effect sensor. I connected this to GPIO port A, pin 0 (PA0). The main reason being that I was initially using the user button on the Discovery board, which is connected to this pin, and I was lazy to change the code. The snippet above configures the peripheral bus enable register to enable GPIO port A's clock. I'm using the stm32f40xx.h headers for the definitions of the bits (I find it makes the code easier to read, and saves the programmer a lot of work).


//EXTI is on syscfg clk, so needs to be enabled
RCC->AHB2ENR |= RCC_APB2ENR_SYSCFGEN; // en syscfg
SYSCFG->EXTICR[0] |= SYSCFG_EXTICR1_EXTI0_PA;	//line 0 -> port A

Next, we need to enable the System configuration controller clock, which manages the interrupts for the STM32. Then, we need to link line 0 (IE, Px0) with port A. This was quite confusing for me when I was first trying to get it working, but the long and short of it is there are 4 EXTICR (external interrupt configuration registers). Each one manages 4 of the the 'lines' (you can think of lines as port numbers, 0-15). You then assign a port to it, in this case, we're setting the bit to assign port A to EXTICR[0], in the 0 line section. Therefore, we've setup an external interrupt on PA0.

Visually, this is what we're doing - chucking 0000 (the code for port A) into the 4 bits of the register that referer to line 0.

//Configure the interrupts
EXTI->IMR |= EXTI_IMR_MR0; // set line 0 to be enabled
EXTI->RTSR|= EXTI_RTSR_TR0; // set it to trigger on a rising edge

NVIC_SetPriority(EXTI0_IRQn, 0x03);
NVIC_EnableIRQ(EXTI0_IRQn);

A bunch of code comes next to actually enable the interrupt, state what condition you want the interrupt to be triggered on, it's nested priority, and finally, enable the nested vector interrupt.

Most of the code is self explanatory, we need to enable the EXTI0 by setting it's register in the "interrupt mask register". I then set the bit in the rising trigger selection register, so that on a rising edge of PA0 a trigger is triggered.

The next two lines pertain to the nested vector interrupt controller, which handles how interrupts are handled, For example, what happens if two interrupts fire at the same time, or whilst we're inside the interrupt service routine of another? Well, here we set it's priority to 3, rather arbitrarily. It can have up to 256 priority levels (0 through to 255), where priority 0 is the highest. The default is 0. Lastly, we enable the interrupt request.


//Configure the port A pin 0 as an input
GPIOA->MODER	&= ~(GPIO_MODER_MODER12); 

Finally, after all that setup, we can configure port A0 as an input, and write our interrupt service routine as follows:


//interrupt service routine, for counting pulses
void EXTI0_IRQHandler(void) {
	if(EXTI->PR & (GPIOA->IDR) ) {
		//clear the progress reg
		EXTI->PR = 0x1;
		
		//LED_ON = !LED_ON; // toggle the led
		
		HAL_count++; // inc the counter
	}
}

The ISR is jumped to when the relevant trigger occurs, in this case, EXTI0. Once we're in the handler, we need to check to see if what triggered the interrupt. Strictly speaking, this step (the IF statement) isn't required, but the STM32 groups some EXTI line IRQs together, so it's good practise to check the progress register (PR) to see what caused the trigger. Although, it's probably better to use bit masks instead of the input data register, but hey ho!


We clear the register by writing a 1 to the opportiate register (weird, I know) and increment the counter.



Timing With STM32

Now we can count the pulses, we need to time them. Specifically, we need to reset the counter every 0.5 seconds, and calculate the average speed over that time.


//Configure timer 3 for a 0.5s delay, used for averaging the counts to get a speed reading
RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; // enable the timer
TIM3->PSC = 0xF3; // this should give a 0.5s timer
TIM3->DIER |= TIM_DIER_UIE; //enable timer interrupts
NVIC_EnableIRQ(TIM3_IRQn); // enable the IRQ
TIM3->CR1 |= TIM_CR1_CEN; //enable the counter

Setting up the timer is done with the above code. Here, I'm using timer 3, as it's a simple general purpose 16 bit timer; I'll leave the more advanced timers for other things like PWM. Again, the code is fairly self explanatory, and has the same process as the edge counter - enable the clock with the APB1ENR, configure the peripheral, and then enable it.


The configuration of the peripheral is done by setting the prescaler, with 243d. This means that the clock is now 243 times slower, in effect. I'm using the internal clock, which is 8MHz. Therefore, instead of 8MHz, I'm effectively running at ~32.9KHz. For a free running, 16 bit timer, there are 2^16 - 1 = 65535 ticks. We can calculate how long it would take to make all those ticks, running at 32.9KHz by dividing them, and essentially getting "seconds / tick": 32.7K/65535 = ~0.5seconds. Perfect.


Then we need only to enable timer interrupts, the IRQ, and finally the counter.


//interrupt service routine for the timer
int TIM3_IRQHandler(void) {
    TIM3->SR = 0;		// clear the status reg
    //LED_ON = !LED_ON; // toggle the led
    HAL_count_frozen = HAL_count; // freeze the counter
    HAL_count = 0;// reset the counter
	
    return HAL_count_frozen; // return the value for  
			     // processing outside the IRQ
}

The IRQ handler is quite simple, it clears the status register (so we can continue triggering interrupts), we capture the current counter value, then reset it and pass the captured value to the main loop.


// blink the LED every 0.5s, to check the timer is working
while(1){
     timer_count = TIM3->CNT; // get the value of 
     			      // the counter, used for debugging
		
     speed = ((HAL_count_frozen * cir/hall_counts_per_rot) / 0.5) *
        	mps_mph; // calculate the speed, in mph

     //turn on the LED
     if(LED_ON) {
         GPIOD->ODR |= GPIO_ODR_ODR_12;	 // write a 1 to the output 
                                         // data register to turn on
                                         // the LED
      } else {
         GPIOD->ODR &= ~(GPIO_ODR_ODR_12); // else, write a 0 to the 
                                           // output data register
      }
}

As you can see, the main while loop is very simple, we apply the math we went through before - and toggle the LED so we know things are a live! The whole code:


#include "stm32f4xx.h"                  // Device header

// Constants

const int hall_counts_per_rot = 23; // number of counts per one wheel rotation
const float wheel_diameter = 0.4;//  0.662; //meters
const float pi = 3.1412; // pi
const float cir = pi * wheel_diameter; // circumfrenece of the wheel
const float mps_mph = 2.23694; // m/s to m/h conversion

//define some vars
volatile unsigned char LED_ON; // this is used to toggle the LED
volatile unsigned int HAL_count, HAL_count_frozen, timer_count = 0; // used for counting

double speed = 0.0; // var to hold the speed

//main loop
int main(void){
	
	//Green LED is PD12
	//enable the GPIO PORT D CLK
	RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
	
	//Button is on PA0
	//enable the GPIO port A CLK
	RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
	
	//EXTI is on syscfg clk, so needs to be enabled
	RCC->AHB2ENR |= RCC_APB2ENR_SYSCFGEN; // en syscfg
	SYSCFG->EXTICR[0] |= SYSCFG_EXTICR1_EXTI0_PA;	//link line 0 with Port A (so we're using PA0 as the interrupt)
	
	//Configure the interupts
	EXTI->IMR |= EXTI_IMR_MR0; // set line 0 to be enabled
	EXTI->RTSR	|= EXTI_RTSR_TR0; // set it to trigger on a rising edge
	
	NVIC_SetPriority(EXTI0_IRQn, 0x03);
	NVIC_EnableIRQ(EXTI0_IRQn);
	
	//Configure the pin 12 as an output
	GPIOD->MODER	|= GPIO_MODER_MODER12_0;		// set the mode as ouput
	
	//Configure the port A pin 0 as an input
	GPIOA->MODER	&= ~(GPIO_MODER_MODER12);
		
	//Configure timer 3 for a 0.5s delay, used for averaging the counts to get a speed reading
	RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; // enable the timer
	TIM3->PSC = 0xF3; // this should give a 0.5s timer
	TIM3->DIER |= TIM_DIER_UIE; //enable timer interrupts
	NVIC_EnableIRQ(TIM3_IRQn); // enable the IRQ
	TIM3->CR1 |= TIM_CR1_CEN; //enable the counter
	
	
	// blink the LED every 0.5s, to check the timer is working
	while(1){
		
		timer_count = TIM3->CNT; // get the value of the counter, used for debugging
		
		speed = ((HAL_count_frozen * cir/hall_counts_per_rot) / 0.5) * mps_mph; // calculate the speed, in mph

		//turn on the LED
		if(LED_ON) {
			GPIOD->ODR	|= GPIO_ODR_ODR_12;		// write a 1 to the output data register to turn on the LED
		} else {
			GPIOD->ODR	&= ~(GPIO_ODR_ODR_12);	// else, write a 0 to the output data register
		}

	}
	
}


//interrupt service routine, for counting pulses
void EXTI0_IRQHandler(void) {
	if(EXTI->PR & (GPIOA->IDR) ) {
		//clear the progress reg
		EXTI->PR = 0x1;
		
		//LED_ON = !LED_ON; // toggle the led
		
		HAL_count++; // inc the counter
	}
}

//interrupt service routine for the timer
int TIM3_IRQHandler(void) {
  TIM3->SR = 0;		// clear the status reg
	//LED_ON = !LED_ON; // toggle the led
	HAL_count_frozen = HAL_count; // freeze the counter
	HAL_count = 0;// reset the counter
	
	return HAL_count_frozen; // return the value for processing outside the IRQ
}


Or copy it from my github gist https://gist.github.com/maxsimmonds1337/ed4ad4eadca03026ca39f6cdb87e0bd3 !



And We're Done!

That's it guys! We've done it, if you've followed along to the end then I thank you! Any questions or comments, leave them down below and I'll be glad to help!


~Max

27 views0 comments