See a series of logs with overview of the project:
Open-source DC servomotor with extensive testing infrastructure.
To make the experience fit your profile, pick a username and tell us what interests you.
We found and based on your interests.
See a series of logs with overview of the project:
Finally, I had some time (and friends) to investigate what's happening with current sensing.
In Part 1, I made four interesting observations, but only deserves a response: Yes, it's normal for the voltage on the current shunt to be non-zero. (At least, that's what I was told) This is still problematic and has to be investigate further.
This time, I've altered the way the H-bridge is controlled, conducted additional measurements, and added even more measurements on top of those.
Let's summarize how the H-bridge controlled the motor: it drives the motor based on two digital input pins. The specifics vary with each H-bridge; what we used was:
I realized he might be onto something, as this is exactly what the datasheet recommends. Why? In 00 mode, the motor pins are not connected to anything. In 11 mode, the motor pins are shorted together.
To achieve gradual control, we use PWM. With a 25% duty cycle, we spend 25% of the time in the 10 state and 75% of the time in the 00 state.
A friend of mine, J. Mrazek (thanks!), suggested that this was not optimal and that I should switch between the 10 and 11 states instead. That is, one more option:
4. 11 input when the motor should not be idle.
After reviewing the datasheet of the DRV8251A, I realized he might be onto something, as this is exactly what the datasheet recommends. Why? In 00 mode, the motor pins are not connected to anything. In 11 mode, the motor pins are shorted together.
I won't detail all the consequences, as I'm not certain about them, but there is one practical aspect I can work with: in 11 mode, I can measure the current flowing through the motor (thanks to the internal current sensing of the H-bridge).
The way I see it, switching between 01 and 11 modes allows the 01 mode to let the current flow into the motor, while the 11 mode shows how it circulates inside—both providing good insight into the torque applied by the motor, which is the real value I'm interested in.
Let’s measure all the data again! We'll use the same table as in Part 1, so please refer there for an explanation.
power | 0% | 25% | 50% | 75% | 100% |
power source current | 14mA | 45mA | 83mA | 130mA | 170mA |
R7 voltage | 25mV | 172mV | 210mV | 245mV | 267mV |
calculated motor current | 15mA | 109mA | 131mA | 156mA | 169mA |
This time, the data table showed anomalies; the voltage on pin R7 no longer scaled linearly with the PWM duty cycle, which was baffling. This issue perplexed me for a while, and I was on the verge of discarding the H-bridge as this seemed bonkers.
In an attempt to understand what was happening, I also captured a screenshot of the voltage profile to observe how the voltage on R7 changed during the duration of one pulse.
As my modest drawing skills illustrate, the new measurements reveal significant changes in the voltage profile. The large red shape on the left, representing the current state, shows a symmetric rise and fall in voltage. On the right side, you can see the profile from previous measurements (using 01/00 mode). The difference is striking: in 11 mode, the voltage on R7 decreases much more slowly than in 00 mode, resulting in higher measured current per period.
The central question remains: why has the current stopped behaving linearly?
I understand that the PWM duty cycle and current might not scale linearly, but the non-linearty is too big.
We managed to borrow a high-quality current sensing probe from a friend—far more expensive than I'd typically invest in hobby equipment. With this, we were able to measure the current flowing through the motor. Unfortunately, I didn't take any pictures of the experiment, so there are no images to share this time.
power | 0% | 25% | 50% | 75% | 100% |
power source... |
So, given that prototype v3 has been on my table for a while and I've managed to mostly make it work, the time has come to go through the 'current sensing analysis' ritual.
What is this all about? The crucial functionality of the servo is its ability to sense the current flowing through the motor. If this feature has bugs or doesn't work perfectly, none of the control loops will function properly either. Most importantly, it would be difficult to determine that the issue lies with the current sensing and not with something else. (Trust me, I learned this the hard way with previous prototypes.)
To visualize this:
The first step is to summarize what we are working with. Here's the simplified circuit:
Our H-bridge, DRV8251A, has current-sensing capability, as documented here. What happens is that based on the current flowing through the bridge, current flows from |PROP| to GND. This is what is measured by the MCU STM32H5 for current sensing. We use a 1k resistor R7 as a shunt resistor.
Tom V. also recommended using a 10n capacitor C15 to handle noise. Frankly, this was a bit troublesome for my CS-focused brain, which has a complete lack of EE knowledge. The question is:
Let's say you sense a current from a PWM pulse, how will the capacitor affect the values read by ADC relative to it being absent?
(Yes, this should be easy to answer for any EE graduate, but here we are.)
The current from PROP is linearly scaled with a known fixed ratio: 1575 µA/A
Given that, we can formulate the basic formula:
V_R7 = I_R7 * R_R7 I_R7 = I * COEFF
What we want to know is: given some fixed voltage `V` measured on `R7`, what is the current flowing through the motor?
The answer to that is in the formula: `V_R7 = I * COEFF * R_R7`
Which, once we fill in the known information, gets simplified to: `V_R7 = I * 1.575`
Note that the resistor has quite a resistance for a shunt resistor, but given the drastic ratio between the real current and the current flowing through |PROP|, the values cancel each other out.
Given that we know this, let's take a scope and measure some things. What we can do is attach a motor to the servo, flash the production firmware, and use a control utility to set the servo to a fixed PWM duty cycle.
The motor is a scrapped Lewansoul LX15D. Since the motor was without any load, the expected current values should be low, as without resistance it won’t need much.
My trusted laboratory power source is powering all of this. The power source has the capability to measure current, so I relied on that for comparison values.
There are multiple scenarios during which I've taken data. In all cases, I relied on the Analog Discovery 2 for the measurements, as that device is my daily tool for such tasks. As you can see, I can even make fancy screenshots of the view from it :)
The interesting data is on Channel 2, which was attached to the current sense pin—effectively connected to the PA4 pin of the STM32H5 on the circuit above. I also made sure that I could observe the PWM duty cycle on the logic analyzer of the AD2; you can see it in the bottom row. (14kHz PWM frequency is about right)
Channel 2 gives us the voltage across the R7 resistor. I've also added a virtual channel,`Math 1`, that converts the values from Ch2 into the actual current flowing through the motor.
It should be noted that the current reported by the power source includes the power drained by the MCU and other peripherals, not just the motor, and at this point, there is no way for me to separate them. However, it should be manageable.
After all that was done and prepared, I measured relevant values for multiple scenarios. Note that I always used the `average` value for each channel:
power | 0% | 25% | 50% | 75% | 100% |
power source current | 11 mA | 31 mA | 57mA | 81mA | 106mA |
R7 voltage | 18 mV | 50 mV | 155mV | 177mV | 224mV |
calculated motor current | 12 mA | 32 mA | 98mA | 112mA | 142mA |
What do the measured values mean?...
Read more »From an algorithmic perspective, at its core, the main task of a servomotor is to take the desired control variable (e.g., position) and command the connected DC motor in such a way that the desired position is achieved.
For Servio, we aimed for somewhat advanced forms of motion planning and control. In the first milestone, we chose basic control loops for three variables: current, position, and velocity.
We implemented three modes for Servio, in which the firmware focuses on controlling the respective variable - current mode, position mode, and velocity mode. In each mode, multiple control loops can be active:
To make the loops work, we introduced a Kalman filter into the system to estimate the current velocity/position based on the sensor used for the axis angle. This was necessary as we needed some way to estimate the velocity accurately.
All of this is illustrated in the following diagram:
The main control loop, the only one in direct contact with the hardware, takes a desired current as an input and controls the power sent to the motor based on the currently sensed current.
It operates as a standard PI control loop, using current as the input values to produce the desired power for the motor. The power is represented by a value ranging from -100% to 100%. (The current loop is not aware that PWM is used.) This output is then fed into timers, which generate PWM pulses to the H-bridge used by Servio.
Based on some insights from the industry, we decided to use a PI controller at 10kHz, as we were informed that the key to making the control work correctly lies in the frequency. That is, a high-frequency PID will be more tolerant to PID coefficient tuning.
To achieve this, we generate PWM at a 20kHz frequency. Each control loop iteration spans two PWM periods:
Note that the exact frequencies of the control loop and the PWM are not fixed; we have just fixed the ratio between them. In the future, we want to experiment with changing the frequency, ideally to try a higher value. In the control loops schema below, you can see which parts of the system are active in control mode.
We sense the position from a potentiometer at a frequency higher than 1kHz, which governs the frequency of the position control loop. This loop uses a PID controller that takes the measured position and the goal position as inputs and calculates the desired current flow through the system. Thus, the position control serves as an input to our current control loop, which handles the rest.
We don't use the measured position directly; instead, we rely on a Kalman filter to perform some filtering on the measured value.
Given the high static friction in our test system, there were issues with the robustness of the loop. To address this, we decided to implement an extra bias. If the servomotor is not moving, the current output from the position control loop is multiplied by a scale value (2.0). This scale value linearly degrades once the servo starts moving and reappears linearly once the servo stops. While not a perfect solution, it has proven to be good enough as it is quite intuitive to configure. (This bias is not visible in the schema.)
In the schema below, you can see which parts of the system are active in position mode.
Velocity control operates on the same principle as position control; we use position readings to...
Read more »We can think of Continuous Integration (CI) in two ways: as an approach to development and as infrastructure. For Servio, both aspects are relevant, as we heavily utilize CI for development.
As a development approach, the philosophy centers on continuously testing new changes to ensure they do not disrupt the system as a whole. This contrasts with making large changes and integrating them into the system sporadically to see if anything breaks. Essentially, it emphasizes incremental updates.
From a coding perspective, we often use a powered test jig on the desk and periodically run the full test suite with it. This practice helps us identify any errors or issues introduced by changes—sometimes these are bugs, and sometimes they are expected outcomes.
Another perspective involves the backend connected to the server. Once any change is committed and merged into the main git repository, the same testing process automatically occurs in the background for each commit.
Regarding the library subprojects, emlabcpp and joque, each has its own CI setup and pipelines.
From Servio's perspective, these libraries should be tested through their own mechanisms without any explicit testing on Servio's part.
Regarding Docker containers, we opted for an automated build and testing environment by creating a Docker image based on the official Arch Linux distribution. We simply add our build tools and dependencies to this image and use it across all our pipelines for repositories. This image is publicly available, and we encourage its use. Dockerfile is here.
For the private repository, we have a GitHub runner for pipelines, equipped with a smaller version of the test jig with a Servio PCB permanently connected. Currently, this test jig is unpowered and disconnected, as leaving a power source permanently enabled presents a risk. This setup resides in my living room, and I have yet to decide to leave any power source permanently on for an automatically starting device.
For each commit in the private repository, we initiate a build of all firmware and tests on our runner and execute a full suite of tests. The test suite can be adjusted for an unpowered scenario, affecting many tests, some skip themselves, while others simply disable evaluation.
Relative to software, the hardware might be underdeveloped. This could be because the project is intentionally more focused on software. Importantly, from a hardware perspective, the Servio project only includes PCB designs, without any casing or mechanical designs. The general idea is that the casing, metal gears, DC motor, or potentiometer must be provided by the user. Servio essentially comprises just the PCB with its software.
For the project's development, we have so far created three iterations of the PCB and have a test jig in an unfinished state.
Generally, the requirements for Servio PCBs are straightforward: The PCB needs a compatible MCU and an H-bridge to control the motor. It would also be practical to include connections for a rotation sensor (at a minimum, a potentiometer), a power source to regulate voltage for components, LEDs for indication, and some connectors.
We intentionally practice minimalism in this way to ensure that designing a new PCB is as simple as possible, which allows for high flexibility in PCB design.
The first iteration was designed with the following goals:
We began development with this board and conducted most of the development on it. The only major issue was with the H-bridge, which led yaqwsx to hand-solder an alternative H-bridge to circumvent the limitation.
There was some debate about the kind of MCU we needed; we intentionally chose a more powerful one than we thought necessary. Why? To reduce development time by minimizing the need for performance optimizations.
The second iteration was intentionally made smaller, mostly at my request, so I could more easily integrate it into actual applications. The board was designed as two connected PCBs: one being the servo itself and the other a debug board.
The goals here were to:
Essentially, the second iteration intentionally narrowed the scope, and made stuff smaller.
With the second iteration of the development board, I put more effort into a test jig - a test harness that could be used for any development done by me or for further development automation.
The test jig contains a DC servomotor without a connected PCB to an external Servio PCB (v2) in a DIY-made box. There are other external boards that allow the addition of more sensors around the actual servo, such as an encoder directly connected to the servo shaft. During development, the servo was powered by a standard laboratory power source.
Key takeaways from this experience:
All in all, this part of the project will definitely have its own detailed log in the future.
Generally, v2 was almost adequate, but we wanted to add some final touches to the board and decided to create v3, with goals to:
The software architecture of the system could be analyzed in depth, and we will address this in the future. However, considering that it is "just a servo," its complexity and size are not significant. Most of the effort is devoted to perfecting it. When discussing software architecture, we should consider the structure of the firmware, the requirements from the controlling side, how tests fit into this framework, and what testing orchestration looks like.
The firmware can be described in following simple terms: Servo firmware should be a sophisticated control loop governed by messages. This simplification forms the basis of our firmware.
For effective testing, we focused on abstraction and decomposition. Our goal was to ensure that details specific to embedded hardware do not excessively infiltrate the codebase, enabling extensive testing on our computers.
The firmware structure comprises three layers. The bottom layer includes platform-specific code, which is minimal. This layer contains the Hardware Abstraction Layer (HAL) from the manufacturer and our setup functions for that platform. Each firmware version includes precisely one platform module in its source code.
The second layer is board-specific code, ideally encapsulated in a single .cpp file with pin and peripheral configurations. This layer typically invokes the platform-specific functions configured for the specific board.
The remainder resides in an independent layer, ideally agnostic to any platform or board-specific code. Currently, we cannot run 100% of this code on non-embedded hardware, but the percentage that requires embedded execution is extremely low. This limitation partly results from my laxity and our use of nanopb exclusively on the embedded side.
The independent layer contains the majority of the code, but we will delve into this in a future blog post, as it is not immediately relevant.
We chose protobuf messages as the format for communicating with the servo. Our long-term goal is flexibility in the underlying layer used for transmitting these messages. Currently, we support framing protobuf messages in COBS and transmitting them via full-duplex UART.
This approach means that software interfacing with the servo must create a valid protobuf message, encapsulate it in COBS, send it over UART, and await a response.
The servio repository includes the scmdio utility binary, facilitating communication with the servo through a CLI interface. For example, we can command the servo to switch to position control mode and move to position 0. This utility is primarily for configuration, as it implements a frontend for the configuration API in the protobufs.
Bash command would look like this:
$ smcido mode position 0
C++ code using boost asio could look like this:
boost::asio::awaitable< void > set_mode_position( cobs_port& port, float angle )
{
servio::Mode m;
m.set_position( angle );
servio::HostToServio hts;
hts.mutable_set_mode()->
co_await exchange( port, hts );
}
We employ various tests: unit tests, simulation tests, firmware tests, control tests, and blackbox tests, each with a unique focus.
Unit tests evaluate small, independent code segments on host devices (our laptops) without hardware access. For instance, we test our algorithm for storing configurations in flash memory using a raw memory buffer. Currently, the number of unit tests is lower than desired, but our extensive testing through other methods mitigates the need for more unit tests. Only code from the independent layer is tested here.
Start 1: cfg_utest_test 1/5 Test #1: cfg_utest_test ................... Passed 0.02 sec Start 2: cfg_storage_utest_test 2/5 Test #2: cfg_storage_utest_test ........... Passed 0.01 sec Start 3: control_utest_test 3/5 Test #3: control_utest_test ............... Passed 0.01 sec Start 4: kalman_utest_test 4/5 Test #4: kalman_utest_test ...................Read more »
Given that we understand the motivation behind Servio (as gleaned from the first log), let's delve deeper into its anatomy and other relevant aspects. These logs will contain some redundancy, as it does not neatly divide into parts but instead examines the subject from multiple perspectives.
This comprehensive overview will be released in five parts to avoid overwhelming readers with one lengthy log. Enjoy!
Servio can be viewed as a set of projects with varying degrees of independence. Each project has its own repository. There are two core projects: servio and servio(private). Servio is formed by an open-source public component (firmware, some utilities, some tests) and a private component (numerous tests). We tend to view these components as two intertwined projects that need to coexist.
In addition to these, we maintain some C++ libraries that we developed. These include `emlabcpp`, a generic embedded-related library, and `joque`, an utility library used by the private part of Servio. The rest of this section will provide more detailed descriptions of each project.
During early development, yaqwsx provided me with two PCB designs for prototyping (versions 1 and 2), both available in a standalone PCBs repository. Recently, Tomas Vavrinec agreed to design another prototype board. Overall, various PCB design projects are associated with Servio.
This is the public component of Servio and the project's central part (link). It includesthe entire firmware. The firmware depends only on the emlabcpp library and the standard C++ library, making it fully open-source. Additionally, there is a command-line utility for working with the servo `scmdio`, a basic set of unit tests, and black-box tests.
This should suffice for users who wish to modify the servo's code or inspect what they are using in their hardware. We will discuss the source code structure in greater detail later in another logs.
Our private repository contains our testing infrastructure and some glue that connects everything together.
Our testing infrastructure is capable of executing various types of tests (currently including unit tests, firmware tests, control tests, simulation tests, and black-box tests) adn generate HTML reports for analysis.
The challenge with both servio repositories is their separate yet tightly coupled nature. This requires us to maintain tight synchronization, which can be challenging.
In a future log, we will explain why we undergo this public/private separation.
emlabcpp, a few years older than Servio, is an opinionated C++20 library focused on embedded-related utilities. It emphasizes providing numerous small utilities or mini-libraries for embedded code.
The embedded-related aspect implies code with a low footprint that avoids dynamic memory and exceptions. Servio is built upon this library, which was expanded with utilities necessary for Servio during its development. Most notably, it includes an embedded-focused testing library for tests executed on the target hardware.
joque is a small library designed to parallelly execute a set of tasks while respecting their dependencies. It's what we use to orchestrate tests.
Generally, when feasible, I prefer to create a few smaller libraries from the project for future reuse.
We have separate repositories for PCB designs, primarily for development boards. These are somewhat standalone, especially the latest version, v3, designed by T. Vavrinec for us, though the repository remains his. v2 exists here, and v3 is here.
This independence applies only to the hardware design files. The software side must be compatible with the boards (pin assignments) and is currently stored in the main repository.
This is the first log recording for Servio, written retrospectively, as in the early days, I was preoccupied with developing the project and did not think about documenting the process. (A habit I find problematic, as writing things down generally helps in not forgetting something or passing information to others.)
Let's start by describing how and why it all began, focusing more on the why rather than the exact details.
First things first, what do we mean by a "smart servomotor"? It's a good idea to establish expectations and an understanding of basic terminology.
In our minds, a servomotor is a physical part with an output axis. The servomotor takes an input and, based on that, rotates the axis to a specific position determined by the input (with position being an angle of the axis).
While this is a predominant use case, there might be instances where we want the servo to spin the axis and hold angular velocity. Instead of focusing on the position of the axis, we might want to control how much force the axis should apply to spinning.
This component is fundamental for modern robotics, allowing us to use servos to articulate joints of robotic arms, spin wheels, or perform other actuating tasks.
As far as I know, there is no strict definition, but generally, we can say that one can interactively communicate with a smart servo (we can send it a message, and it will respond), while simple servos just receive input in some form (such as PWM, for example).
The benefit of a servo being smart is that, apart from telling it what to do, we can inquire about its success. With some servos, we can also monitor how well it is executing operations or how much force it required to do so.
We embarked on this journey with myself, Jan Veverak Koniarik, having the most available free time, and a friend of mine (yaqswx - Jan Mrazek). At the time, we were both Ph.D. students in robotics, sharing a common need for decent open-source servomotors in our research projects. Given that my friend was also a member of the local robotics club, there were more people interested in open source servo.
At that time, there were no open-source alternatives (though this might have changed over time; unfortunately, I lost track of other projects as I became engrossed in my own). Proprietary alternatives could be narrowed down to two options:
Dynamixel or similar: Well known brand (at least in the academia) with well equipped features, whitch is also well priced. We did liked our experience with dynamixels we just found them too pricey for open-source robots. Traditional industry uses different brands of servos, with even much higher price than Dynamixels, so that was no way to go.
Lewansoul (renamed to Hiwonder): Much well priced servomotors with quite a decent hardware quality (servomotor with metal gears for +-17$ at the time (2021) was a steal), however the software quality was kinda disappoiting, or more precisely: The quality of the smart part. The control loops of the servomotor were not the best and there was lack of configurability over communication protocol. That is: this was cheap enough but not capable enough.
So, we said: what the hell, it can't be THAT hard, how about making our own servo? (Obviously this assumption failed miserably)
We began by agreeing on the specifications and expected milestones. Initially, we were ambitious, but eventually, we set on set of goals that we did not fully met, but turned out to be practical enough:
Create an account to leave a comment. Already have an account? Log In.
> Your measured current might be proportional to the PWM value if you replace the motor in your test setup with a low inductance resistor. Pick a convenient value to convert the resistor voltage to current like 1 Ohm. This would allow you to test the current sense system in a linear environment.
I thought about that but I kinda want something to move with for the overal test suite, that is, I think that just resistor would damper the testing capability in other places.... it might be worth it to have one such test jig tho
You mean the big frame box? It's a testjig assembled out of totem: https://totemmaker.net/
@Shakezzz one more warning: the totem itself is lighweight plastic, it's extremely practical in the sense that it is easy to cut it to the length you want and assemble something fast (which is easy to modify)
The downside is that it is light, adding a 3mm aluminium baseplate on which it is mounted increased the rigidity a lot and it being heavy made the testjig less noisy too.
Become a member to follow this project and never miss any updates
Your measured current might be proportional to the PWM value if you replace the motor in your test setup with a low inductance resistor. Pick a convenient value to convert the resistor voltage to current like 1 Ohm. This would allow you to test the current sense system in a linear environment.
The current drawn by a motor is related to the PWM value and the motor speed. Turning a DC motor with some external force (or just inertia) generates a DC voltage. That DC voltage is also generated when you are driving the motor in a normal motor configuration. That voltage is called Back EMF. It is proportional to the speed of the motor. It is also opposite polarity of the voltage that is driving the motor. As a result motor current is related to motor speed for a given voltage.
Imotor = (Vdrive - VbackEMF)/Rmotor
If you think about the case where the motor is stopped, VbackEMF will be 0 and your motor current will be Vdrive/Rmotor.
In a frictionless configuration, Vdrive will equal VbackEMF and the motor current would be 0.