Three Phase Encoder?

mwomack

Well-known member
I am looking to write code for a generic three phase encoder that you find in bldc scooter motors. I've got a setup where I can read the signals coming from the encoders, and I have written software for some test interrupts to count the 'ticks' as the signals are fired. Which is great. But now I want to generalized it into a class like the existing Encoder class in the teensy library. So, I have two questions:

1) Has someone already done this and I'm just duplicating effort?

2) Assuming it has not been done before, I was looking at the existing Encoder class code (which is for a quadrature encoder), and it has this complicated manner of referencing a struct pointer that is stored in an array where each element in the array is associated with each possible pin the interrupt can be attached to. And there is an interrupt method for each interrupt pin that essentially passes parameters into a common method. I think that all makes sense...but before I start to emulate it I just want to make sure there isn't something better out there that I should be considering. My code is going to be interrupt-only, so I think I can safely ignore the non-interrupt stuff. And like I said, I think I understand the basics of what the Encoder code is doing and I can replicate appropriately in my three phase code.

3) In an earlier discussion, someone had mentioned that the Teensy had some builtin functions for Encoders. When I looked at relevant code, it seemed to be quadrature encoder specific. Am I wrong and there is generic encoder support builtin to the Teensy?

thanks for any advice and pointers.

-Mark
 
Teensy 3.x and 4.x both have various timer peripherals that can count edges or measure time between edges. I'm not sure what you mean by "three phase encoder". Do you mean 3 signal outputs. Are they A/B (quadrature) and M (marker)?
 
Teensy 3.x and 4.x both have various timer peripherals that can count edges or measure time between edges. I'm not sure what you mean by "three phase encoder". Do you mean 3 signal outputs. Are they A/B (quadrature) and M (marker)?

If you look at the document provided you come up with this:-
3phase enc.png
 
Yes, there are three square waves instead of just the two used for quadrature encoders. And they trigger (edge and falling) in U V W order when going forward and W V U when going reverse. These BLDC motors are like the ones used on hoverboards and eScooters.
Like I said, I have some working code using interrupts on three teensy (4.0) pins. My motors had a fair bit of noise, which can be filtered out via hardware or software. I was just looking to see if there were any builtin capabilities or anyone had already written code for this type of encoder. I was thinking that maybe I would write my library to support a limited number of encoders (like 2 or 3) instead of being as flexible as the existing quadrature Encoder library. You could still put the signals on any pins that support interrupts, but there would just be 6 or 9 pre-defined interrupt methods that would be used (instead of 50+). My robot only has two motors, each with its own encoder, and I think that would be most use cases.
 
Wouldn't any 2 signals out of 3 be sufficient for being fed into a quadrature encoder? [or is it actually a quadrature decoder?]

Paul
 
If you used only a pair of signals you'd lose resolution. Depending on the application that may not matter.
If you invert U you get a nice clean W-U-V sequence. You could then feed each pair (WU, UV, VW) into 3 different encoders and sum the results and you'd get the full resolution using only standard encoders.

I can't help thinking that there must be some clever trick you could do that exploited the fact that the output is a grey code but I'm not sure what it would be.
 
can't help thinking that there must be some clever trick you could do that exploited the fact that the output is a grey code but I'm not sure what it would be.

If it is a grey code, only one input is allowed to change at each state. This should make the decoding state machine pretty simple. Would be fun to integrate it to the EncoderTool (https://github.com/luni64/EncoderTool) but I don't have a test system to play with...
 
If you used only a pair of signals you'd lose resolution. Depending on the application that may not matter.
If you invert U you get a nice clean W-U-V sequence.

I'm not sure I follow that. I guess I'd need to see a picture. I don't see how inverting U by itself does that, unless you mean as a pairing with the non-inverted versions. But as far as state changes, only one phase changes at any given point, rising or falling.

But yes, just limiting to two of the three signals would reduce the resolution, and on a robot I am using it as feedback on how far the motor (and attached wheel) has turned (in radians). The 120 ticks/revolution resolution is not the greatest, but is probably enough for what I am using it for. Using encoders as odometry on a moving robot is never perfect anyways (wheel slippage, bounces, etc).

My code for a simple three phase encoder, limited to supporting only two encoders, is here (first version, feedback appreciated). It is a 'dumbed' down version from the one I was using for debugging and understanding but was dependent on knowledge of intended motor direction. I don't really keep track of the rising/falling state of the phases, just the previous phase change. But after reading all of the above, I feel like I should probably apply myself to get better error detection in place. I was pretty focused on getting the encoder errors filtered out at the hardware level, which I did using capacitors and Schmitt triggers. It would be trivial to add a state variable and update it based on the current value at interrupt (and only update it if it is a valid state change).

-Mark
 
If you used only a pair of signals you'd lose resolution
Yes, true. Interestingly, on page 3 of the article referenced by the OP, the code for the Teensy 3.5 only calculates direction and speed, not distance (yet).

If it is a grey code, only one input is allowed to change at each state
To me it's a 3-bit Gray code without state '000' and '111', but still adheres to "only one input is allowed to change at each state".

Could you not write a simple program on a Teensy to be the test system.
That was my first thought too!

Paul
 
I updated my ThreePhaseMotorEncoder code to better support state and error checking. The current 'state' of the VUW inputs is recorded, and with every interrupt call it updates the state based on the value of the pin. Because just one bit has changed, there are only two valid choices for the next state. Depending on which one is matched, the direction of the motor is determined and the tick count is updated accordingly. When a 'fault' is detected (the new state does not match the two valid states), then the interrupt is ignored but a fault count is incremented. The code supports two encoders but it can be updated to support more very easily, the number of encoders is just a constant (but one would need to add three new interrupt methods for each added encoder).

I'm reasonably happy with the code. Would appreciate any feedback. I did test it out on my setup up with two motors, one going CW and the other one CCW. My circuit hardware filters out transient signals (faults), and the software reports zero faults. So at least I know I have the state transitions right. :) My test sketch is located here if you want to take a look, but you would need my hardware setup with the motors and such.

Thank you for the helpful and useful discussions.

-Mark
 
Would appreciate any feedback.

I had a quick look at your ThreePhaseMotorEncoder class. I played a bit yesterday evening and ended up with roughly the same state machine as you implemented :).

Regarding your interrupt functions: I'd embedd the whole pin interrupt handling in the ThreePhaseMotorEncoder class. This would simplify your code significantly and would allow users to setup as many enocders as they want. Of course embedding is not possible with the stock attachInterrupt, but there is attachInterruptEx (https://github.com/luni64/attachInterruptEx) which can do this. Here an untested (but compiling) stub showing how to embed pin interrupts to a c++ class.

Please note: for the sake of simplicity I packed everything inline into the class but your setup with *.h and *.cpp files is better of course.

Code:
#include "Arduino.h"
#include "attachInterruptEx.h"

const uint8_t UMASK = 0b00000110;
const uint8_t WMASK = 0b00000101;
const uint8_t VMASK = 0b00000011;
const uint8_t UPOS  = 0;
const uint8_t WPOS  = 1;
const uint8_t VPOS  = 2;

uint8_t CWNEXTSTATE[]{0, 3, 6, 2, 5, 1, 4, 0};
uint8_t CCWNEXTSTATE[]{0, 5, 3, 1, 6, 4, 2, 0};
const bool CW(false);
const bool CCW(true);

class ThreePhaseMotorEncoder
{
 public:
    ThreePhaseMotorEncoder() //::public MotorEncoder  this stub only shows how to embedd pin interrupts...
    {
        //...
    }

    void begin(uint8_t phaseVPin, uint8_t phaseWPin, uint8_t phaseUPin)
    {
        attachInterruptEx(phaseUPin, [this, phaseUPin] { handleInterrupt(phaseUPin, UMASK, UPOS); }, CHANGE);
        attachInterruptEx(phaseVPin, [this, phaseVPin] { handleInterrupt(phaseVPin, VMASK, VPOS); }, CHANGE);
        attachInterruptEx(phaseWPin, [this, phaseWPin] { handleInterrupt(phaseWPin, WMASK, WPOS); }, CHANGE);
    }

    int32_t read(void);
    int32_t write(int32_t value);
    int32_t readFaults(void);

 private:
    void setEncoder(void);

    void handleInterrupt(uint8_t pin, u_int8_t mask, uint8_t pos)
    {
        // Immediately capture the pin state
        uint8_t pinValue = digitalRead(pin);
        uint8_t newState = (state & mask) + (pinValue << pos);

        if (newState == CWNEXTSTATE[state])
        {
            count++;
            direction = CW;
            state     = newState;
        }
        else if (newState == CCWNEXTSTATE[state]) // If the new state is in the CCW array, then going CCW
        {
            count--;
            direction = CCW;
            state     = newState;
        }

        else // This is noise to be ignored, but recorded
        {
            faultCount++;
        }
    }
    int32_t count       = 0;
    uint32_t faultCount = 0;
    uint32_t direction;
    uint8_t state;
};

//-----------------------------

ThreePhaseMotorEncoder encoder1, encoder2, encoder3;

void setup()
{
    encoder1.begin(0, 1, 2); // you can have as many encoders as you like...
    encoder2.begin(7, 8, 9);
    encoder3.begin(12, 11, 10);
}

void loop()
{
}
 
Last edited:
I had a quick look at your ThreePhaseMotorEncoder class. I played a bit yesterday evening and ended up with roughly the same state machine as you implemented :).

Great minds think alike. Lol. But it was the thread on gray codes that I was following to its logical conclusion, so thank you.

Regarding your interrupt functions: I'd embed the whole pin interrupt handling in the ThreePhaseMotorEncoder class. This would simplify your code significantly and would allow users to setup as many enocders as they want.

I really like this. It is much simpler than the 50+ interrupt methods used in the existing (Quadrature) Encoder teensy library. But it does introduce (2) dependencies. I'm fine with that for my own code (it's not like I don't have other cross-dependencies elsewhere).

I take that there is a reason why there has to be a 'begin' method and the attachments can't just be done in the constructor? 'this' doesn't exist in the constructor?

I'm going to look at integrating this right now...

-Mark
 
But it does introduce (2) dependencies.
Yes, this is suboptimal. You can also use this version: https://github.com/luni64/TeensyHelpers/tree/master/src/attachInterruptEx which doesn't have any dependencies (it uses std::function instead of the homebrew callback helper). Just copy the two files attachInteruptEx.h and attachInterrupt.cpp into your source folder.

I take that there is a reason why there has to be a 'begin' method and the attachments can't just be done in the constructor? 'this' doesn't exist in the constructor?

"this" does exist in the constructor. However, the problem with constructors in embedded programming is that, on global objects, they are called very early in the boot procedure. Some parts of the hardware may not yet be fully initialized at those early boot stages.
Things like pinMode usually work, but more complicated stuff like attachInterrupt may fail. Calling begin() from user code guarantees that the hardware is completely set up. So, it's not beautiful but safe.

I'm going to look at integrating this right now...
Have fun with it. If you happen to need an IntervalTimerEx (same features), you'll also find it in the TeensyHelpers repo linked above.
 
Yes, this is suboptimal. You can also use this version:

I like that solution.

Calling begin() from user code guarantees that the hardware is completely set up. So, it's not beautiful but safe.

But certainly not out of line with other Arduino classes (ie Serial.begin()), so perfectly acceptable.

I have updated the classes on a branch for now (.h here, .cpp here), and tested them on my rig. Soooooo much cleaner now, thank you for this. I feel like the existing Encoder class could benefit from some attachInterruptEx love.

I think I am going to 'graduate' my MotorManager library into its own repository with all of these recent changes and a few more I need to do. I will be sure to credit you and your TeensyHelper github. Thank you for all of this useful code and advice.

-Mark
 
Yes, this is suboptimal. You can also use this version: https://github.com/luni64/TeensyHelpers/tree/master/src/attachInterruptEx which doesn't have any dependencies (it uses std::function instead of the homebrew callback helper). Just copy the two files attachInteruptEx.h and attachInterrupt.cpp into your source folder.

A follow-up question specific to this...does this 'standalone' version of attachInterruptEx work with non-Teensy microcontrollers? I'd like to have a version of a quadrature encoder that is not dependent on the teensy and can run on other boards. I looked at the code, and I am unsure of the Teensy specifics. The includes in the .cpp file?

-Mark
 
I am looking to write code for a generic three phase encoder that you find in bldc scooter motors.


Hi. These are typically Hall sensor (actually Hall switch) outputs used for commutating a BLDC. Some have 120 degree spacing, some have 60 degree spacing, so you'd need to have the option of configuring for either scheme. (with 120 degree 000 and 111 are illegal states, with 60 degree they are valid).

Its Gray code after Frank Gray who discovered/patented them. Both the 120 degree and 60 degree forms are Gray codes.

For many applications the 6 counts, 12 or 18 or 24 counts per revolution (depending on pole count in the motor) is way too coarse, but simple speed monitoring should work quite well, but don't expect a position control loop to work well, for that 1000's of counts per revolution are more like it.

If you XOR all 3 inputs together you get a single input that switches on every count (but you lose direction information).
 
Hi. These are typically Hall sensor (actually Hall switch) outputs used for commutating a BLDC. Some have 120 degree spacing, some have 60 degree spacing, so you'd need to have the option of configuring for either scheme. (with 120 degree 000 and 111 are illegal states, with 60 degree they are valid).

Its Gray code after Frank Gray who discovered/patented them. Both the 120 degree and 60 degree forms are Gray codes.

Yes, exactly so. Here is the implementation I came up with: https://github.com/markwomack/MotorAndEncoderManager/blob/main/src/ThreePhaseMotorEncoder.cpp

My particular motor gets 120 ticks per revolution (using both rising and falling changes). Not the greatest, but I think it will be enough for my outdoor RoboMagellan robot where there are other inputs as well (GPS, visual odometry). On my robots with more conventional motors and quadrature encoders, they achieve the higher tick counts because the hall sensors are working with the motor shaft, not the wheel shaft, and a gear ratio for torque. But with the BLDC motors it's all-in-one. I guess there are some options you can attach to the wheel that provide higher counts per revolution, but like I said, I think this is sufficient for my current use. It was fun to code though. And I had to do some work to clean up the signals, so I got my hands dirty with some actual electronics.

-Mark
 
A follow-up question specific to this...does this 'standalone' version of attachInterruptEx work with non-Teensy microcontrollers? I'd like to have a version of a quadrature encoder that is not dependent on the teensy and can run on other boards. I looked at the code, and I am unsure of the Teensy specifics. The includes in the .cpp file?

-Mark
AttachInterruptEx does not use any Teensy hardware related things. It should run on every platform which uses at least a full gcc14 compliant toolchain. (gcc14 is required for the templates generating all that relay functions without manually defining them for each and every interrupt pin). It won't run for the old AVR boards since gccavr only implements a small subset of modern c++ features.
 
Back
Top