Firmware Training
Introduction
Thank you for your interest in joining the Mars Rover Team for the 2020-2021 season! To get you up to speed with the rover firmware, we have created a series of training modules for you to go through. This consists of some instruction on the platforms and tools we use, a couple quick tutorials, and a challenge to demonstrate you have collected all the knowledge you need to contribute to the team. Due to COVID-19 related reasons, you will not be able to test your code in our bay. After your work has been approved by a team member, you will be allowed to take on a task. If you come across any challenges during this training module, feel free to reach out to Cindy Li (Firmware team lead) on discord.
STM32F4 Series Microcontroller
To control the various subsystems of the rover, such as the arm or science modules, we use several control boards designed by the electrical team. At the heart of each of these boards this year will be a STM32F446RE MCU (Microcontroller Unit). If you are familiar with Arduino, you could consider this an Arduino Mega on steroids. It is probably overkill (the F4 series are ST's high-performance line) for what we're doing with it, but it certainly doesn't hurt to have the extra headroom in our use case.
The role of the firmware team is to bring life to these control boards that interface with the rover's physical sensors and actuators. Each board communicates to each other as well as with the rover's main computer, an Nvidia Jetson, via a bus protocol called CAN (Controller Area Network) which is commonly used in the automotive industry. The firmware allows the main computer to control all of the hardware through simplified CAN commands. For example, controlling a joint by sending the desired angular position.
Nucleo Development Boards
The STM32 Nucleo Boards are a series of development boards designed to allow anyone to try out new ideas and to quickly create prototypes with any STM32 MCU. Currently, all firmware team members have their own Nucleos. New members may be receiving Nucleos upon completion of the training depending on availability.
MBED OS
With a more powerful processor architecture that something like the Arduino, writing the firmware for it is consequently more complex, but is made quite approachable with Arm MBED OS. MBED OS provides a C/C++ software platform that is easy to learn while being versatile enough to be used in real products. There is also a large online community which is great for finding and sharing tutorials, user-made or vendor-made libraries, and ideas. MBED supports the NUCLEO-STM32F446RE platform. It also has a ton of built-in support for many different types of hardware peripherals and communication protocols, such as CAN.
C++
The language we use on the Firmware team is C++. C++ is a high-performing language commonly used in the Embedded Software industries. The primary reason we use C++ is that it is the language of choice for many first year courses.
On the team we make an effort to produce modern and elegant code. We like to take advantage of the more recent language features of C++. For one, it is a great learning opportunity. The other reason is that adhering to modern coding standards tends to produce better abstracted code, which is more modular and readable. Below are some examples comparing different approaches to solving the same problem. The “Reason” section names the modern tool, in case you would like to learn more about it.
Desired Action | Older Solution | Modern Solution | Reason |
Heap Allocation | int *ip = new int(5); delete ip; | unique_ptr<int> ip(new int(5)); | (Smart Pointers) Better protection against memory leaks |
Looping over container | vector<int> a = {1,2}; for (int i = 0; i < a.size(); i++) { // do something } | vector<int> a = {1,2}; for (int i : a) { // do something } | (Range based for-loop) No need to mentally check bounds and better readability |
These are just a few examples. The more you program, the more you will learn about these modern approaches. The more experienced C++ programmer should watch this video about modern coding standards: https://www.youtube.com/watch?v=XkDEzfpdcSg&t=2691s&ab_channel=CppCon.
If you need to catch up on C++, https://www.learncpp.com/ is a good site for learning.
CMake
Since the firmware repository is a little more complex than typical projects you would’ve encountered in lecture, it requires a slightly more sophisticated approach to compiling. For this, we use CMake. CMake is a tool for managing build-processes. This link serves as a nice introduction to CMake: https://cliutils.gitlab.io/modern-cmake/.
For the most part, using CMake is very simple and convenient with our repo thanks to the work of our members. An in-depth understanding of CMake is nice to have, but not critical.
Git and GitHub
Git is a version control tool that we use to manage our codebase and ease collaboration. If you haven't ran into Git already, you likely will as it is commonly used in the industry. This is a pretty good tutorial that should get you up to speed on it. Once you feel confident with Git and Github, take a look at our firmware repository. Make a fork of it, and clone this fork to your computer.
Building and Running a Program on a Rover Control Board
Once you have forked and cloned the firmware repository, make sure to read through the main README. This file contains a description of the repository structure, instructions on the tools you need to install, how to compile an application, and how to flash and run these programs on our boards. Make sure that you read through and understand the contents of this file, and feel free to ask any questions relating to it. Complete steps 1 and 2 of the README before coming back to this document. Next, issue the following command:
git clone --recurse-submodules https://github.com/<insert_username_here>/MarsRoverFirmware.git
Next, issue cd MarsRoverFirmware
Writing Your Own Program
Before beginning this section, ensure that you are able to compile apps per the instructions in the readme.
Now that you are set up to successfully compile our firmware apps, you should be ready to start coding your own program. Your first task is to build a program to control a servo.
Servos are a type of rotational actuator consisting of a motor, gearbox and rotational sensor built into one package. They allow for precise control of position as well as rotational speed. Typically, servos are controlled by a variable-duration pulse that occurs every 20ms (50Hz). This is a type of Pulse Width Modulation (PWM) signal.
To control the servo, you will read the voltage drop over a potentiometer (a variable resistor of which the resistance changes depending on how it is rotated). This is an analog value that can be measured using the Analog to Digital Converter (ADC) on the MCU. You can then generate a PWM signal with a pulse width that depends on the position of the potentiometer. MBED OS contains built-in libraries to both read analog voltages (AnalogIn) as well as output a PWM signal (PwmOut).
First, we begin by creating the first app we will need for this training. Under the test-apps folder, create a folder named tutorial-servo-pot-control. Within that folder create a CMakeLists.txt file. Also create include and src directories, within the tutorial-servo-pot-control folder.
Before we start writing any code, we should let CMake know how our new app is to be compiled.
Enter the following in your CMakeLists.txt
add_executable(tutorial-servo-pot-control) target_sources(tutorial-servo-pot-control PRIVATE src/main.cpp) target_link_libraries(tutorial-servo-pot-control PRIVATE mbed-os) mbed_set_post_build(tutorial-servo-pot-control)
At this point, we also need to let the top level CMake file know about the newly added app. Open the top-level CMakeLists.txt file at the root of the repository. Add the following line to the “Include Test Apps” section
add_app_subdirectory(${TEST_APPS_DIR}/tutorial-servo-pot-control)
We must also specify which targets are able to run this app. This allows us to only run our certain apps on boards that were designed for said apps. We do this by appending the following to the supported_build_configurations.yaml
tutorial-servo-pot-control: - UWRT_NUCLEO
This lets our build system know that the tutorial-servo-pot-control app is meant to run on the nucleo only.
Now, create a main.cpp file under test-apps/tutorial-servo-pot-control/src, and fill in the following.
#include "mbed.h" int main() { while(1) { } }
Now initialize our potentiometer voltage input on pin PA_0 and the servo PWM output on pin PA_1. Add this after the #include and before the main function.
#include "mbed.h" AnalogIn potVoltageIn(PA_0); PwmOut servoPwmOut(PA_1);
Next, set up the PWM period to 20ms (50Hz):
#include "mbed.h" AnalogIn potVoltageIn(PA_0); PwmOut servoPwmOut(PA_1); int main() { servoPwmOut.period_ms(20); while(1) { } }
Now for the fun part. Depending on the position of the potentiometer, the voltage on pin PA_0 will vary from 0.0V to 3.3V. This value needs to be mapped to a duty cycle range which corresponds to a pulsewidth of 1ms to 2ms:
#include "mbed.h" AnalogIn potVoltageIn(PA_0); PwmOut servoPwmOut(PA_1); int main() { servoPwmOut.period_ms(20); while(1) { float potVoltage = potVoltageIn.read(); servoPwmOut.pulsewidth((1 + potVoltage/3.3) / 1000); } }
Note that not all servos will accurately map their range to a pulsewidth of the standard 1ms - 2ms. The max and min pulse width that corresponds to the servo's max and min angle positions may vary by up 0.5ms or more, so you will need to experiment to find the correct pulse width range of the servo that you will be testing your code for. Some servos are even designed to have a range that is different then the standard, which can be determined from the datasheet.
Try building your tutorial-servo-pot-control app with the command
make APP=tutorial-servo-pot-control TARGET=UWRT_NUCLEO
.
If you’ve followed all the steps corrected, the app should build successfully.
Challenge
For the challenge, create an app called tutorial-servo-can-control under the test-apps folder. Configure this newly added app the same way we configured tutorial-servo-pot-control. Specifically, follow the same steps for configuring its CMake. Do not forget to let the top level CMake know that you have added a new app!
An important part of collaborating on software projects is the organization and structure of code, making it modular, reusable, and easy to understand. The first part of your challenge task is to create a servo library. Follow this tutorial, but instead of flashing LEDs try to apply the same concept to controlling a servo. Please name your library class TutorialServo. The header file should look like this:
#pragma once #include "mbed.h" class TutorialServo { public : // Constructor: Takes a servo pin name (ex. PA_1), and optionally a servo range // that has a default value of 180.0 degrees, a minimum pulsewidth of 1ms, and a // maximum pulsewidth of 2ms. TutorialServo(PinName servoPin, float servoRangeInDegrees = 180.0, float minPulsewidthInMs = 1, float maxPulsewidthInMs = 2) ; // Set servo position (ex. 45 deg) void setPositionInDegrees( const float degrees) ; // Get the servo range in degrees (ex: 90 deg) float getServoRangeInDegrees( ) const ; // Get the min pulse width in ms (ex: 1ms) float getMinPulseWidthInMs( ) const ; // Get the max pulse width in ms (ex: 2ms) float getMaxPulseWidthInMs( ) const ; private : PwmOut m_servoPwmOut ; const float m_servoRangeInDegrees ; const float m_minPulsewidthInMs; const float m_maxPulsewidthInMs; };
A brief aside on const usage seen above. The const in the function arguments indicates that the provided argument cannot be mutated by the function. The const at the end of a function means that the function does not mutate member variables. Const correctness is a critical aspect of C++. Read more at: https://isocpp.org/wiki/faq/const-correctness#overview-const.
Once you have implemented the library, we now look to make CMake aware of the TutorialServo library we just created. Append this to the end of your CMake file. This declares a TutorialServo library.
add_library (TutorialServo STATIC) target_sources (TutorialServo PRIVATE src/TutorialServo.cpp) target_include_directories (TutorialServo PUBLIC include) target_link_libraries (TutorialServo PRIVATE mbed-os)
Next, we need to let CMake know about this newly added library. We do this by adding the following to the line after the target_sources declaration for tutorial-servo-can-control.
target_link_libraries (tutorial-servo-can-control PRIVATE TutorialServo )
This tells CMake that the main.cpp program depends on a library called TutorialServo. Your main.cpp will likely depend on more than just the TutorialServo library. Add libraries as you need them. Please refer to the test-apps/test-can app for an example.
As mentioned earlier, CAN is the communications protocol that each of the rover's systems use to talk to each other. The second part of your challenge task is to write a program to control a servo via CAN. Utilize the TutorialServo class that you wrote in the first part of the challenge. Use the same pin (PA_1) as used earlier in the training module. The program should be able to receive CAN messages which contain a percentage of the range of motion of the servo as a float.
We recommend taking a look at the arm app to see a working implementation of CAN, as well as checking out this tutorial and this note on the MBED site.
Submitting Your Training for Review
Once you have completed the challenge, commit and push your code to your forked GitHub repository, then open a Pull Request (PR) from the master branch of your forked repository to the master branch of the firmware repository. Check out GitHub’s helpful guide on how to do this. Once your PR is created and ready for review, please notify one of the firmware leads on discord. After your training has been reviewed and approved, you may close your PR. DO NOT merge your PR into the repository!
If you have any questions let us know right away. Part of the challenge is for you to try and learn the interface on your own, but we don't want you to get stuck on this!
Testing Out Your Programs
NOTE: Due to COVID-19 related reasons, you will not be able to test your code in our bay. After your work has been approved by a team member, you will be allowed to take on a task.
Thank you for taking the time to complete the firmware challenge! Now it is time to test out the code that you wrote. In the robotics bay there is a NUCLEO-STM32F091RC dev board set up with a potentiometer and servo already connected to the required pins.
Using this dev board (look at repository README for flashing instructions as well as how to connect the serial interface), demonstrate the following:
- Upload the test_serial app that you compiled earlier and verify the board was flashed successfully and the LED begins to flash. Open up the serial interface on your computer you installed when going through the README and verify that you are receiving a repeating Hello World! message from the board. Make sure you set the baud rate on your computer's serial interface to 115200.
- Upload the tutorial_servo_control app you made and verify that when you rotate the potentiometer, the servo rotates as expected. If you have issues with it jittering or otherwise not behaving, check the data sheet of the servo and make sure that PWM frequency and pulse durations are correct.
- Upload that tutorial_servo_control_can app you made and verify that you can send the app CAN messages which change the position of the servo. You can use a CANable USB interface to send test CAN messages from your computer. Check out the setup guide here (you may need to sudo apt install can-utils). Note that in most cases don't use `/dev/ttyACM0` as the CAN device: You can use /dev/serial/by-id/*CAN*-if00 as the slcand device path to automatically select the path of the CANable device on Ubuntu. If ifconfig is not available, use the ip command to enable the computer CAN interface: sudo ip link set can0 up
Extra Reading
Below is a list of additional documentation that may be useful to you for this
- Communication Interfaces:
- Sensors