-
Servio Overview (Part 2/5) - Software Architecture
01/27/2024 at 19:25 • 0 commentsSoftware Architecture Perspective
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.
Firmware
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.
Interfacing
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 0C++ 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 ); }Tests
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 ................ Passed 0.00 sec Start 5: metrics_utest_test 5/5 Test #5: metrics_utest_test ............... Passed 0.00 secSimulation tests assess control loops. They involve test scenarios that provide inputs like "start at velocity `0rad/s`, after 1 second move to velocity `0.7rad/s`, and remain steady," along with criteria for successful outcomes. By integrating our control loop with a simple motor simulator, we can test without hardware, focusing solely on code from the independent layer.
![]()
Firmware tests, similar in nature to unit tests, are conducted exclusively on the embedded device. They are ideal for evaluating aspects closely tied to the hardware, such as the reliability of periodic timers. These tests are beneficial for both long-term maintenance and assessing new PCB designs.
Benchmarks form part of the firmware tests, analyzing quantitative properties like control loop frequency or CPU time spent in interrupts. These benchmarks do not directly influence test outcomes but are monitored to ensure modifications do not introduce issues. They primarily focus on the platform or board layers but also include the independent layer due to its significant impact.
![]()
Control tests, which I highly value, are essentially integration tests that employ real hardware and control loops. We use test scenarios from the simulation tests on fully powered servos, allowing us to observe the hardware’s response to various scenarios in real-time. A future blog post will cover this in detail.
![]()
Failing test where velocity clearly oscilates more than it should Blackbox tests are conducted on the final firmware build. They involve interfacing with the servo via protobuf messages to test the frontend. The focus here is on the integration of different components.
The public repository includes unit and blackbox tests, while simulation, firmware, and control tests are in a private repository.
Test Orchestration
To manage our diverse range of tests, we developed a unified approach for test execution, crucial for effective CI integration. We created joque, a library that orchestrates test execution by concurrently running tasks with respect to their dependencies.
The test_orchestrator binary scans the servio build for unit tests, generates simulation tests, identifies firmware tests, produces control tests, and detects blackbox tests. These tasks are then executed by joque. Post-execution, the orchestrator compiles a comprehensive report from all tests.
We will provide more details on this later, as each test type requires a distinct approach within the orchestration framework. The key is that joque's abstraction is versatile enough to optimally manage each test type.
-
Servio Overview (Part 1/5) - Projects
01/26/2024 at 07:00 • 0 commentsGiven 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!
Project's Perspective
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.
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.
servio(private)
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
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
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.
PCBs v1, v2, and v3
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.
-
Inception and motivaton
01/04/2024 at 16:27 • 0 commentsThis 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.
What is smart servomotor?
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.
What Makes a Servo Smart?
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.
Motivation
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)
Early goals
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:
- The firmware is written in C++.
- We use CMake as a build system, following standard practices as closely as possible.
- Basic principle: Servio is a device that responds to messages sent over a communication bus and executes a closed-loop control loop for motor and encoder.
- Three control modes: position control, velocity control, and current control.
- A minimum of a 10kHz control loop for current control.
- Develop basic automated tests.
- The end goal is to create a replacement PCB for Lewansoul LX15D.
- Support for potentiometer, quadratic encoder, or magnetic encoder as sensory input.
Let's discuss the goals in more details:
- We chose C++ primarily because we are proficient in it. Additionally, we believe the language is well-suited for our needs.
- Our choice of CMake was less about the tool itself (which I sometimes find frustrating) and more about avoiding alternatives, such as IDEs provided by manufacturers.
- The basic principle is a solid goal as it helps filter out impractical ideas. For instance, if we consider adding battery management, we can assess its relevance to the primary function.
- The control modes define how we envision controlling the servo - position control for moving to a specific position, velocity control for moving at a certain speed, and current control for maintaining a specific current.
- Based on knowledge/discussions with more experienced people in the industry, we got a hint that the sweet magic trick to make PID control loops work reliably (which we wanted to use) is not perfect fine-tuning of parameters, but a high-frequency loop. When we asked "what is high?" we were told that we want 10kHz for the current loop and only 1kHz for velocity control loop is enough. Do we understand WHY this is so? Nah, but we went for 10kHz anyway.
- As software developers, automated testing is a natural part of our process, a practice not always common in embedded systems.
- We decided to focus solely on software and PCB, sourcing other components, like metal gears or casings, from our favorite cheap servo, the LX15D.
- The last goal was actually a point of controversy among us. Yaqwsx correctly argued that magnetic encoders are superior to potentiometers, as they are more reliable, support 360-degree rotation without a dead zone, etc. I mostly argued that that might be true, but unless I can buy a servo with a magnetic encoder mounted and positioned inside, I am not interested, as I refuse to try positioning a magnetic encoder where a potentiometer used to be. We agreed to support both. (And quadrature encoders just for the sake of completeness).
So it begins
Well, this is primarily what we began with. I recommend focusing more on the essence of the text rather than the minutiae. The overall nature of it still remains valid. In subsequent logs, I will switch modes and describe the entire project from top to bottom, inside and out, including a few tangential details along the way.
P.S: This text was NOT written by ChatGPT, that unwieldy stubborn thing was used only as glorified spellchecker.



