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.
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:
And specifically, for those wanting to learn the details of motor control, or extend with extra features:
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.
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.
I thought it would be appropriate to write down a few bullets of what needs to be done. The list will probably grow...
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...
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.
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):
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.
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.
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.
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...
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...
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.
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);
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.
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.
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.
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.
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.
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.
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.
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: