Brushless DC Motor Controller

Last updated on Sep 18, 2017
SUMMARY

This project involves the design of a software platform that provides a good basis when developing motor controllers for brushless DC motors (BLDC/PMSM). It consist of a basic but clean and readable implementation of a sensored field oriented control algorithm. Included is a logging feature that will simplify development and allows users to visualize what is happening. The project shows that Ada successfully can be used for a bare-metal project that requires fast execution.

Motivation

The recent advances in electric drives technologies (batteries, motors and power electronics) has led to increasingly higher output power per cost, and power density. This in turn has increased the performance of existing motor control applications, but also enabled some new - many of them are popular projects amongst diyers and makers, e.g. electric bike, electric skateboard, hoverboard, segway etc. 

On a hobby-level, the safety aspects related to these is mostly ignored. Professional development of similar applications, however, normally need to fulfill some domain specific standards putting requirements on for example the development process, coding conventions and verification methods. For example, the motor controller of an electric vehicle would need to be developed in accordance to ISO 26262, and if the C language is used, MISRA-C, which defines a set of programming guidelines that aims to prevent unsafe usage of the C language features. 

Since the Ada programming language has been designed specifically for safety critical applications, it could be a good alternative to C for implementing safe motor controllers used in e.g. electric vehicle applications. For a comparison of MISRA-C and Ada/SPARK, see this report. Although Ada is an alternative for achieving functional safety, during prototyping it is not uncommon that a mistake leads to destroyed hardware (burned motor or power electronics). Been there, done that! The stricter compiler of Ada could prevent such accidents. 

Moreover, while Ada is not a particularly "new" language, it includes more features that would be expected by a modern language, than is provided by C. For example, types defined with a specified range, allowing value range checks already during compile time, and built-in multitasking features. Ada also supports modularization very well, allowing e.g. easy integration of new control interfaces - which is probably the most likely change needed when using the controller for a new application. 

This project should consist of and fulfill:

  • Core software for controlling brushless DC motors, mainly aimed at hobbyists and makers.
  • Support both sensored and sensorless operation.
  • Open source software (and hardware).
  • Implementation in Ada on top of the Ravenscar runtime for the stm32f4xx.
  • Should not be too difficult to port to another microcontroller.

And specifically, for those wanting to learn the details of motor control, or extend with extra features:

  • Provide a basic, clean and readable implementation.
  • Short but helpful documentation
  • Meaningful and flexible logging.
  • Easy to add new control interfaces (e.g. CAN, ADC, Bluetooth, whatever).

Hardware

The board that will be used for this project is a custom board that I previously designed with the intent to get some hands-on knowledge in motor control. It is completely open source and all project files can be found on GitHub

  • Microcontroller STM32F446, ARM Cortex-M4, 180 MHz, FPU
  • Power MOSFETs 60 V
  • Inline phase current sensing
  • PWM/PPM control input
  • Position sensor input as either hall or quadrature encoder
  • Motor and board temp sensor (NTC)
  • Expansion header for UART/ADC/DAC/SPI/I2C/CAN

It can handle power ranges in the order of what is required by an electric skateboard or electric bike, depending on the used battery voltage and cooling. 

There are other inverter boards with similar specifications. One very popular is the VESC by Benjamin Vedder. It is probably not that difficult to port this project to work on that board as well. 

Board


Rough project plan

Continuously updated...

I thought it would be appropriate to write down a few bullets of what needs to be done. The list will probably grow...

  • Create a port of the Ravenscar runtime to the stm32f446 target on the custom board. Done!
  • Add stm32f446 as a device in the Ada Drivers Library. Done!
  • Get some sort of hello world application running to show that stuff works. Done!
  • Investigate and experiment with interrupt handling with regards to overhead. Done!
  • Create initialization code for all used mcu peripherals. Almost!
  • Sketch the overall software architecture and define interfaces. Kinda!
  • Implementation! Basic!
  • Documentation... Could be better!

Support for the STM32F446

The microprocessor that will be used for this project is the STM32F446. In the current version of the Ada Drivers Library and the available Ravenscar embedded runtimes, there is no explicit support for this device. Fortunately, it is very similar to other processors in the stm32f4-family, so adding support for stm32f446 was not very difficult once I understood the structure of the repositories. I forked these and added them as submodules in this project's repo

Compared to the Ravenscar runtimes used by the discovery-boards, there are differences in external oscillator frequency, available interrupt vectors and memory sizes. Otherwise they are basically the same. 

An important tool needed to create the new driver and runtime variants is svd2ada. It generates device specification and body files in ada based on an svd file (basically xml) that describes what peripherals exist, how their registers look like, their address', existing interrupts, and stuff like that. It was easy to use, but a little bit confusing how flags/switches should be set when generating driver and runtime files. After some trail and error I think I got it right. I created a Makefile for generating all these file with correct switches. 

I could not find an svd-file for the stm32f446 directly from ST, but found one on the internet. It was not perfect though. Some of the source code that uses the generated data types seems to make assumptions on the structure of these types. Depending on how the svd file looks, svd2ada may or may not generate them in the expected way. There were also other missing and incorrect data in the svd file, so I had to manually correct these. There are probably additional issues that I have not found yet...

It is alive!

I made a very simple application consisting of a task that is periodically delayed and toggles the two leds on the board each time the task resumes. The leds toggles with the expected period, so the oscillator seems to be initialized correctly. 

Next up I need to map the different mcu pins to the corresponding hardware functionality and try to initialize the needed peripherals correctly. 

The control algorithm and its use of peripherals

There are several methods of controlling brushless motors, each with a specific use case. As a first approach I will implement sensored FOC, where the user requests a current value (or torque value). 

To simplify, this method can be divided into the following steps, repeated each PWM period (typically 20 kHz):

  1. Sample the phase currents
  2. Transform the values into a rotor fixed reference frame
  3. Based on the requested current, calculate a new set of phase voltages
  4. Transform back to the stator's reference frame
  5. Calculate PWM duty cycles as to create the calculated phase voltages

Fortunately, the peripherals of the stm32f446 has a lot of features that makes this easier to implement. For example, it is possible to trigger the ADC directly from the timers that drives the PWM. This way the sampling will automatically be synchronized with the PWM cycle. Step 1 above can thus be started immediately as the ADC triggers the corresponding conversion-complete-interrupt. In fact, many existing implementations perform all the steps 1-to-6 completely within an ISR. The reason for this is simply to reduce any unnecessary overhead since the performed calculations is somewhat lengthy. The requested current is passed to the ISR via global variables. 

I would like to do this the traditional way, i.e. to spend as little time as possible in the ISR and trigger a separate Task to perform all calculations. The sampled current values and the requested current shall be passed via Protected Objects. All this will of course create more overhead. Maybe too much? Need to be investigated. 


A sketch of the architecture as I imagine it right now

PWM and ADC is up and running

I have spent some time configuring the PWM and ADC peripherals using the Ada Drivers Library. All in all it went well, but I had to do some smaller changes to the drivers to make it possible to configure the way I wanted. 

  • PWM is complementary output, center aligned with frequency of 20 kHz
  • PWM channels 1 to 3 generates the phase voltages
  • PWM channel 4 is used to trigger the ADC, this way it is possible to set where over the PWM period the sampling should occur
  • By default the sampling occurs in the middle of the positive waveform (V7)
  • The three ADC's are configured to Triple Multi Mode, meaning they are synchronized such that each sampled phase quantity is sampled at the same time. 
  • Phase currents and voltages a,b,c are mapped to the injected conversions, triggered by the PWM channel 4
  • Board temperature and bus voltage is mapped to the regular conversions triggered by a timer at 14 kHz
  • Regular conversions are moved to a volatile array using DMA automatically after the conversions complete
  • ADC create an interrupt after the injected conversions are complete

The drivers always assumed that the PWM outputs are mapped to a certain GPIO, so in order to configure the trigger channel I had to add a new procedure to the drivers. Also, the Scan Mode of the ADCs where not set up correctly for my configuration, and the config of injected sequence order was simply incorrect. I will send a pull request to get these changes merged with the master branch. 

Interrupt overhead/latency

As was described in previous posts the structure used for the interrupt handling is to spend minimum time in the interrupt context and to signal an awaiting task to perform the calculations, which executes at a software priority level with interrupts fully enabled. The alternative method is to place all code in the interrupt context. 

This Ada Gem and its following part describes two different approaches for doing this type of task synchronization. Both use a protected procedure as the interrupt handler but signals the awaiting task in different ways. The first uses an entry barrier and the second a Suspension Object. The idiom using the entry barrier has the advantage that it can pass data as an integrated part of the signaling, while the Suspension Object behaves more like a binary semaphore. 

For the ADC conversion complete interrupt, I tested both methods. The protected procedure used as the ISR read the converted values consisting of six uint16. For the entry barrier method these where passed to the task using an out-parameter. When using the second method the task needed to collect the sample data using a separate function in the protected object.

Overhead in this context I define as the time from that the ADC generates the interrupt, to the time the event triggered task starts running. This includes, first, an isr-wrapper that is a part of the runtime which then calls the installed protected procedure, and second, the execution time of the protected procedure which reads the sampled data, and finally, the signaling to the awaiting task. 

I measured an approximation of the overhead by setting a pin high directly in the beginning of the protected procedure and then low by the waiting task directly when waking up after the signaling. For the Suspension Object case the pin was set low after the read data function call, i.e. for both cases when the sampled data was copied to the task. The code was compiled with the -O3 flag. 

The first idiom resulted in an overhead of ~8.4 us, and the second ~10 us. This should be compared to the period of the PWM which at 20 kHz is 50 us. Obviously the overhead is not negligible, so I might consider using the more common approach for motor control applications of having the current control algorithm in the interrupt context instead. However, until the execution time of the algorithm is known, the entry barrier method will be assumed... 

Note: "Overhead" might be the wrong term since I don't know if during the time measured the cpu was really busy. Otherwise it should be called latency I think...

Purple: Center aligned PWM at 50 % duty where the ADC triggers in the center of the positive waveform. Yellow: Pin state as described above. High means time of overhead/latency.

Reference frames

A key benefit of the FOC algorithm is that the actual control is performed in a reference frame that is fixed to the rotor. This way the sinusoidal three phase currents, as seen in the stator's reference frame, will instead be represented as two DC values, assuming steady state operation. The transforms used (Clarke and Park) requires that the angle between the rotor and stator is known. As a first step I am using a quadrature encoder since that provides a very precise measurement and very low overhead due to the hardware support of the stm32. 

Three types has been defined, each representing a particular reference frame: Abc, Alfa_Beta and Dq. Using the transforms above one can simply write:

declare
   Iabc  : Abc;  --  Measured current (stator ref)
   Idq   : Dq;   --  Measured current (rotor ref)
   Vdq   : Dq;   --  Calculated output voltage (rotor ref)
   Vabc  : Abc;  --  Calculated output voltage (stator ref)
   Angle : constant Float := ...;
begin
   Idq := Iabc.Clarke.Park(Angle);

   --  Do the control...

   Vabc := Vdq.Park_Inv(Angle).Clarke_Inv;
end;

Note that Park and Park_Inv both use the same angle. To be precise, they both use Sin(Angle) and Cos(Angle). Now, at first, I simply implemented these by letting each transform calculate Sin and Cos locally. Of course, that is a waste for this particular application. Instead, I defined an angle object that when created also computed Sin and Cos of the angle, and added versions of the transforms to use these "ahead-computed" values instead. 

declare
   --  Same...

   Angle : constant Angle_Obj := Compose (Angle_Rad); 
   --  Calculates Sin and Cos
begin
   Idq := Iabc.Clarke.Park(Angle);

   --  Do the control...

   Vabc := Vdq.Park_Inv(Angle).Clarke_Inv;
end;

This reduced the execution time somewhat (not as much as I thought, though), since the trigonometric functions are the heavy part. Using lookup table based versions instead of the ones provided by Ada.Numerics might be even faster...

It spins!

The main structure of the current controller is now in place. When a button on the board is pressed the sensor is aligned to the rotor by forcing the rotor to a known angle. Currently, the requested q-current is set by a potentiometer. 


As of now, it is definitely not tuned properly, but it at least it shows that the general algorithm is working as intended. 

In order to make this project easier to develop on, both for myself and any other users, I need to add some logging and tuning capabilities. This should allow a user to change and/or log variables in the application (e.g. control parameters) while the controller is running. I have written a tool for doing this (over serial) before, but then in C. It would be interesting to rewrite it in Ada. 

Contract Based Programming

So far, I have not used this feature much. But when writing code for the logging functionality I ran into a good fit for it. 

I am using Consistent Overhead Byte Stuffing (COBS) to encode the data sent over uart. This encoding results in unambiguous packet framing regardless of packet content, thus making it easy for receiving applications to recover from malformed packets. The packets are separated by a delimiter (value 0 in this case), making it easy to synchronize the receiving parser. The encoding ensures that the encoded packet itself does not contain the delimiter value. 

A good feature of COBS is that given that the raw data length is less than 254, then the overhead due to the encoding is always exactly one byte. I could of course simply write this fact as a comment to the encode/decode functions, allowing the user to make this assumption in order to simplify their code. A better way could be to write this condition as contracts. 

   Data_Length_Max : constant Buffer_Index := 253;

   function COBS_Encode (Input : access Data)
                         return Data
   with
      Pre => Input'Length <= Data_Length_Max,
      Post => (if Input'Length > 0 then
                  COBS_Encode'Result'Length = Input'Length + 1
               else
                  Input'Length = COBS_Encode'Result'Length);

   function COBS_Decode (Encoded_Data : access Data)
                         return Data
   with
      Pre => Encoded_Data'Length <= Data_Length_Max + 1,
      Post => (if Encoded_Data'Length > 0 then
                  COBS_Decode'Result'Length = Encoded_Data'Length - 1
               else
                  Encoded_Data'Length = COBS_Decode'Result'Length);

Logging and Tuning

I just got the logging and tuning feature working. It is an Ada-implementation using the protocol as used by a previous project of mine, Calmeas. It enables the user to log and change the value of variables in the application, in real-time. This is very helpful when developing systems where the debugger does not have a feature of reading and writing to memory while the target is running. 

The data is sent and received over uart, encoded by COBS. The interfaces of the uart and cobs packages implements an abstract stream type, meaning it is very simple to change the uart to some other media, and that e.g. cobs can be skipped if wanted. 

Example

The user can simply do the following in order to get the variable V_Bus_Log loggable and/or tunable:

V_Bus_Log  : aliased Voltage_V;
...
Calmeas.Add (Symbol      => V_Bus_Log'Access,
             Name        => "V_Bus",
             Description => "Bus Voltage [V]");


It works for (un)signed integers of size 8, 16 and 32 bits, and for floats. 

After adding a few variables, and connecting the target to the gui:



As an example, this could be used to tune the current controller gains:



As expected, the actual current comes closer to the reference as the gain increases

As of now, the tuning is not done in a "safe" way. The writing to added symbols is done by the separate task named Logger, simply by doing unchecked writes to the address of the added symbol, one byte at a time. At the same time the application is reading the symbol's value from another task with higher prio. The optimal way would be to pass the value through a protected type, but since the tuning is mostly for debugging purposes, I will make it the proper way later on...

Note that the host GUI is not written in Ada (but Python), and is not itself a part of this project. 

Architecture overview

Here is a figure showing an overview of the software:

(Actually, the hall-sensor part and speed controller is not added yet, but it shows the intentions for the future)

The blue peripheral boxes just above Ada Drivers Library has interfaces towards the application that is hardware independent, and thus provides a HAL. 

Inverter System

This is a cyclic task that makes "high" level decisions such as to set the control mode (e.g. current or speed or sensor alignment etc), to map control inputs to the appropriate functionality, perform filtering, do the speed control and stuff like that. 

Current Control

This is a task that is triggered by the ADC, which in turn is triggered to do its conversion once every PWM cycle. The current controller will take the readings and, depending on the specific control algorithm, control the stator current by commanding a new triplet of duty cycles to the PWM peripheral. 

Position

As of now, since only a quadrature encoder is supported, this is actually not a separate task. The encoder peripheral provides very good position data by a single read to a timer-register, but when adding hall-sensor support this might better become a task that is triggered by hall commutations. Also sensorless operation could have functionality in this package. 

Logger

The logger task is cyclic. It calls the Calmeas package that will buffer the values of logged variables. The task then calls the communication stack as to send the logged data and to check for received data (e.g. requests to change value of a variable, or set the Calmeas sample rate). Here you could also add logging to other media, for example Bluetooth, CAN or SD. 

Summary

This project involves the design of a software platform that provides a good basis when developing motor controllers for brushless motors. It consist of a basic but clean and readable implementation of a sensored field oriented control algorithm. Included is a logging feature that will simplify development and allows users to visualize what is happening. The project shows that Ada successfully can be used for a bare-metal project that requires fast execution. 

The design is, thanks to Ada's many nice features, much easier to understand compared to a lot of the other C-implementations out there, where, as a worst case, everything is done in a single ISR. The combination of increased design readability and the strictness of Ada makes the resulting software safer and simplifies further collaborative development and reuse. 

Some highlights of what has been done:

  • Porting of the Ravenscar profiles to a custom board using the STM32F446
  • Adding support for the STM32F446 to Ada_Drivers_Library project
  • Adding some functionality to Ada_Drivers_Library in order to fully use all peripheral features
  • Fixing a bug in Ada_Drivers_Library related to a bit more advanced ADC usage
  • Written HAL-isch packages so that it is easy to port to another device than STM32
  • Written a communication package and defined interfaces in order to make it easier to add control inputs.
  • Written a logging package that allows the developer to debug, log and tune the application in real-time.
  • Implemented a basic controller using sensored field oriented control
  • Well documented specifications with a generated html version

Future plans:

  • Add hall sensor support and 6-step block commutation
  • Add sensorless operation
  • Add CAN support (the pcb has currently no transceiver, though)
  • SPARK proving
  • Write some additional examples showing how to use the interfaces.
  • Port the software to the very popular VESC-board.

Open

  • The project is under the GPL-3.0 license
  • All software is hosted on Github: https://github.com/osannolik/a...
  • Development was made with GNAT GPL - ARM Elf toolchain for linux and svd2ada generator
  • The logging tool Calmeas is open source as well: https://github.com/osannolik/c...
  • The custom board is fully open and was designed using the open source application Kicad. 
  • Documentation generated using gnatdoc

Collaborative

  • The entries on this project blog was written in such a way as to be descriptive and pedagogical for contributors
  • All packages and subprograms are documented and an html version of the documentation is found in the doc folder in the repo
  • Added board support for the embedded runtimes, see fork
  • Added features and bug fixes to Ada_Drivers_Library, see fork.
  • The peripheral interfaces is designed to be hardware independent as seen from the application, i.e. the interfaces and types does not depend on Ada_Drivers_Library. See subfolder peripherals
  • The packages and interfaces involved in sending and receiving data overrides an abstract stream type, which increases adaptability to other media
  • Added makefiles to make it easy to build, flash, documentation and generate code from the SVD-file

Dependable

  • Contract-based programming of subprograms where deemed appropriate and meaningful
  • Protected objects are used for task-to-task communication
  • Encapsulation techniques such as private and limited types are used to as large extent as possible
  • The last-chance handler forces the hardware into a safe state, in case of an unhandled exception, and prints the name of the exception using semi-hosting.
  • Used compiler switches: -gnatwa, -gnatwe, -gnatQ, -gnatw.X, -gnaty, -gnatyO,
    -gnatyM120
  • The code is readable and commented appropriately

Inventive

  • Ada was used for an application that traditionally is dominated by C implementations. 
  • NOT doing all calculation in the ISR is maybe not inventive in itself, but other hobbyist implementations do it that way...
  • Created an Ada implementation of COBS


Project Files