Analytical Instruments Empowered by Rust - Tismo Technology
←
→
Page content transcription
If your browser does not render page correctly, please read the page content below
Analytical Instruments Empowered
by Rust
Deepak Hegde
Vishal Keshava Murthy
A case study on the use of the Rust programming language in
Analytical Instrument development
Presented at Pittcon 2021
Tismo Technology Solutions (P) Ltd.
22/2, Palmgrove Road
Bangalore 560047, INDIA
1design paradigms and other facets of Rust in
ABSTRACT the context of software development for an
analytical instrument, which in this case is
The objective of this paper is to explore the an elemental analyzer.
value of the Rust programming language in
the development of an analytical instrument. Background
The study was carried out by developing a
part of an Analytical Instrument software Analytical systems encompass various
using Rust. Experimental results and hardware interfaces, sensor frontends,
anecdotal evidence suggest that Rust is a external hardware and software systems,
viable option to tackle typical challenges communication mediums, computations
associated with developing analytical and HMIs.
instruments. Rust’s emphasis on safety, its An elemental analyzer for example could
support for modern programming contain sensors, values, actuators, PWMs,
paradigms and a thriving Eco system aids communication interfaces that make up the
the developer greatly in rapidly developing hardware interfaces of the system. It would
robust and reliable, high quality code also need data acquisition subsystems for
without incurring any performance measuring temperature, pressure, flow etc.
penalties. In addition to these hardware interfaces the
system would need software subsystems for
1. INTRODUCTION instrument configuration management,
various control processes for all relevant
A typical analytical system in the embedded parameters, coupled with sensor data
space is a combination of a multitude of calibration processing and analyzing
hardware and software subsystems. The capabilities.
development of these high stakes, complex Processing and control aside, a system of
systems which demand high reliability, this nature also mandates the need for
computational accuracy and repeatability various forms of communication interfaces
often poses unique technical challenges to and corresponding protocol support.
the developer. Long lifetimes, strict Command interface and UI is also a part of
regulatory requirements and critical time to this growing list of requirements.
market windows further add to the
complexity of development from a product Software reliability, repeatability of
management perspective. measurements and computational accuracy
are critical to products of this nature. The
Rust is a statically typed, systems level environments in which these devices are
programming language designed for safety, deployed demand adherence to multiple
concurrency and performance. The safety and regulatory compliance. Time to
language promises to alleviate the design market is also paramount to the commercial
challenges that plague the world of success of such an undertaking.
embedded software development. The
study focuses on exploring these issues in Owing to the scale of features of a product
detail and the language as a whole, diving like this, abstraction of hardware and
into the ecosystem, language constructs, functionality is a major hurdle in
2architecting software. The control and language, offers an important set of features
measurement algorithms deployed are that promises to address most of the needs
arduous to develop, implement and test. established above.
The system is inherently asynchronous
which makes concurrency and safety Objectives
cardinal for reliable operation of the
software. A wide variety of standard
The main objective of this study was to
communication interfaces such as CAN,
evaluate Rust as a language for the
Modbus, OPC etc. that need to be
development of embedded analytical
supported adds to the complexity. Features
systems. Quantifiable Code metrics such as
such as diagnostics, firmware upgrade
code size, binary size were collected and
support, intuitive GUI and portability, while
presented. In addition to these, soft metrics
not a part of the core features of the system
like ease of development, debug support,
are still imperative from a usability and
hardware architecture support etc. were
convenience perceptive.
also evaluated.
The key therefore, for successful
Evaluation of RTIC, a real time framework
development of instrument software are
developed in Rust that employs an interrupt
robust implementation architecture,
driven approach to writing concurrent code
support for inherently safe and reliable
was also an important goal. The tool chain,
programming paradigms and the ability to
package management system, external
manage and abstract the complexities and
library support etc. were also put under
real time observance of the system.
scrutiny since the development ecosystem is
an inseparable part of the software
C and C++ are the established programming
development experience.
languages currently in use for embedded
software development. Something
Since Rust is still evolving, it was also a part
seemingly as innocuous as the use of an
of the research effort to recognize
uninitialized variable can wreak havoc in an
shortcomings of the language and
otherwise perfect program. Once pointers,
determine future areas of study.
memory management and concurrency are
involved, null pointer dereferencing,
dangling pointers, memory leaks, stack Scope of the research
corruption and a whole host of hard to track
problems that affect code in insidious ways 1. Evaluation of Rust language features,
are introduced to the code base. The lack of development ecosystem and library support
ready to use modules and associated
package managers, like npm or NuGet 2. Evaluation of RTIC framework
offered by other modern languages also
result in slower development process 3. Collection of code metrics for Rust code
hindered by their inability to leverage compared against equivalent C code
mature prewritten modules.
Rust, a relatively new systems programming
3Research methods
A development environment was set up
with the Rust toolchain including the Rust
stable and nightly compiler, package
manager cargo, Rust formatter, Rust
language server RLS and debugger
codeLLDB.
A TCD controller application, one of the
subsystems of the elemental analyzer
involving temperature control and
measurement, configuration management,
timed tasks, a simple CLI, status indication
and a PID loop were developed in both C
and Rust. The code bases were developed
iteratively using established best
development practices and were subject to
multiple reviews and refactoring to ensure
code quality and performance.
Code metrics were collected for both
applications under various optimization
settings. Other non-quantifiable aspects of
development such as development
environment, toolchain, ease of
development, testing support etc. were also
evaluated and presented.
The structure of the document
The following sections will be covered in the
report:
1. Overview of Rust
2. Embedded Rust
3. Embedded Rust ecosystem
4. Embedded Rust Crates
5. Developed application
6. Useful Language features
7. Observations
43. Move semantics: Assignment, copy and
move operations are clearly distinct and
2. DETAILED ANALYSIS operate by different sets of rules.
2.1. Overview of Rust While the core features described above
help with code correctness, a variety of
concepts and paradigms inspired heavily
Rust[1] is a multi-paradigm systems
from other languages such as pattern
programming language developed by the
matching, anonymous functions and
Mozilla foundation with heavy emphasis on
algebraic data types from functional
safety, performance and support for
programming languages, traits and
concurrency. This systems programming
structures from object-oriented
language converts memory and concurrency
programming and features such as macros
related run time problems into errors that
and template programming from
get caught early during the compilation
metaprogramming languages makes Rust a
process. The compiler accomplishes to do so
very powerful and expressive language.
by adhering to the following fundamental
principles.
Rust also features the concept of unsafe
code, sections of the code can be marked
1. Immutability: Every entity is immutable
unsafe, inside of which potentially
by default
dangerous operations such as raw pointer
2. Ownership: Every entity has a clear
manipulation can be made under the
owner at all times which determines its
discretion of the programmer foregoing the
scope and lifetime. This enables memory
compiler checks for extra flexibility. Other
safety at compile time without relying on
prominent features include built in support
garbage collection
for testing, code formatting and profiling.
2.2. Embedded Rust
This distinction between the core and the
standard library ensures that the features
Bare-metal hardware environments are first
required for the application can be
class citizens of Rust[2]. The language itself is
customized as required based on the target
built in layers with a core layer libcore which
hardware and application requirements.
implements platform agnostic operations,
Rust also extends the powerful Ownership
provides API for language primitives and
mechanism to manage the peripherals by
APIs for processor features. All platform
treating them as variables. One can enforce
integration APIs and the run time
singleton pattern on peripherals, and these
environment are implemented in the stdlib.
can be shared as read-only, read-write types
Development for embedded applications
using the borrow checker paradigm
take place in a no_std environment meaning
that only the core layer is used. Memory
Since concurrency is one of the core focus of
management schemes, run time
rust, depending on the target platform,
environments , stack protection schemes
[12]
there is native support for atomic
and data structures[15] such as vectors and
operations. Synchronization mechanisms
maps are added as required externally on
such as critical sections, mutexes, interior
top of the libcore using crates[11].
mutability refcells are also provided. Real
5Time Interrupt Driven Concurrency
framework , RTIC is another option if RTOS
[6]
HAL crates are built atop PACs, by providing
like functionality is required. definitions to traits defined in
embedded-hal . This layer provides
[12]
Support for interoperability with C is peripheral specific hardware APIs using
another attractive feature of rust. This is which applications can be built. It should be
useful when validated C code needs to be noted however that these HALs are
integrated with Rust. Rust provides Foreign community driven and often do not support
Function interface (FFI) for this purpose. all features provided by the hardware due to
Tools like bindgen aid in generation of which Hardware registers of the device may
mapping metadata between languages.[1][8] still have to be manipulated directly through
services provided by the device specific PAC.
2.3. Embedded Rust ecosystem RustC[3], the Rust compiler is pivotal to the
traction that Rust has been gaining lately.
Compilation times can be pretty long in
A good development ecosystem is critical for
contrast with that of C or C++ but the
the adoption of the language into
output is fairly well optimized for the target
production quality software work. The Rust
platform. The error messages are very
ecosystem while still in its infancy, provides
explicit and provide relevant and helpful
excellent tools and reasonable coverage for
suggestions. Rust also ships with package
most target platforms. The toolchain set up
and build manager cargo that helps with the
process is fairly straight forward, and is
build process as well with dependency
explored in detail in the later sections of the
management and integration of external
paper.
libraries crates. RustC, and cargo coupled
with the debugger GDB can be setup inside
There is extensive support for the ARM
text editors such as VScode for a complete
cortex M series of processors, with
building, flashing and debugging platform.
Hardware abstractions available for MCUs
The Rust ecosystem also features inbuilt
from major chip vendors such as STM,
support for testing and code profiling.
Microchip, NXP, TI and Nordic. Xtensa and
Formatter rustfmt and static code checker
AVR are some of the notable architectures
rust-Clippy are other utilities packaged into
that yet aren't supported.[11]
the system.
The conventional layered approach for
development is followed, with the lowest 2.4. Embedded Rust crates
layer being the Peripheral Access Crate. The
PAC defines memory mapped registers for Rust crates are a large part of the appeal of
the particular target hardware. This layer is embedded rust. The libraries managed by
generally used in conjunction with a micro Cargo[4] helps the developer in leveraging
architecture crate that encompasses the Rust community and integrate tested
routines and functions common to the and verified features into the code. Crates
target architecture. SVD2Rust is a tool that are an indispensable part of the Rust
can be used for the generation of these ecosystem.
target specific Peripheral Access crates.[14]
6Since bare-metal embedded Rust The TCD subsystem of an elemental analyzer
development takes place in the embedded for the detection of nitrogen content in
environment it is up to the developer to protein samples was developed for the sake
select the required memory management of the experiment. The overall system works
scheme utilizing the respective crate[11]. on the Dumas principle in which a sample is
PACs and HALs introduced earlier, if purged and heated in a high temperature
available are also developed in the form of combustion furnace in the presence of pure
crates that can be used to build up oxygen such that the sample decomposes
applications. Several crates that are into carbon dioxide, water, nitrogen dioxide
designed to be used in no_std environments in addition with nitrogen as several oxides.
provide a wide variety of functionality
ranging from atomic access, bit field The combustion products are collected and
manipulation methods, circular buffers and the gas mixture is passed over hot copper to
heapless data structures. CRCs, linear remove any oxygen and convert nitrogen
algebra libraries, FFT support are some the dioxides into molecular nitrogen. The
prominent math libraries. Device drivers[17] sample is passed through traps that remove
for a large class of devices ranging from water and carbon dioxide. The measured
simple peripheral based sensors, memory signal from the thermal conductivity
modules, to displays and cellular modules detector for the sample is then converted
are also pervasive. Stacks developed in Rust into total nitrogen content.
for Bluetooth, Ethernet are some of the
protocol crates that are gaining traction. PID The TCD detector subsystem was
control[16], CLI[15] were some of the implemented in both C and Rust on
application specific crates evaluated and TM4C123XX[10], an ARM cortex M4 based
used in this project. board. Initial prototyping was carried out
using the Tiva launch pad with the same
2.5. Application overview MCU and an STM32F407 discovery board on
which the RTIC framework[6] was evaluated.
Figure 1
7Figure: 2
A traditional layered architecture was using the RTIC framework.[7]
utilized for the code structure with the MCU
HAL making up most of the lower layers 2.6. Useful language features
through the tm4c123x-hal[13] crate for
dealing with various onboard peripherals
The following language features were
such as SPI, UART etc. Additional crates
heavily utilized for the development of the
were used for runtime[11], memory
project.
management[14], and low-level register
access[17] functions. Other purely software
Struct and traits
constructs such as PID control[16] and CLI[15]
Structures in Rust are similar to C struct
were also built around crates for rapid
allowing heterogeneous data types. Unlike a
prototyping and development.
traditional Object Oriented programming
language Rust has no support for classes.
The hardware API layer was built atop the
Object oriented behavior is implemented
previously described HAL. Object based
using struct and traits instead. Traits are
design approach was utilized here, grouping
implemented for structures giving the
all similar functions into modules that
structure traditional class like methods.
exposed to the application layers above only
These methods however need to take an
the functionality required.
explicit self parameter if they need to act on
themselves. Since immutability is a core
The application layer is comprised of
aspect of the language all elements and
independent tasks for PID control, Heater
traits are private and immutable by default.
control, Data acquisition and a command
Consider the heater struct defined below:
Line Interface task. These were tied together
8pub struct Heater
{
status : bool,
pwm_pin : pwm::EvenPWM,
pwm_period_us:u32 ,
pwm_duty_cycle:u32,
}
impl Heater
{
pub fn disable(&mut self)
{
self.status = false;
self.pwm_pin.disable(());
}
fn dutycycle_to_period(&mut self,duty_percent:u32) -> u32
{
let res = duty_percent*(self.pwm_period_us)/100;
return res ;
}
}
Code snippet: 1
Here, the disable trait is defined for
structure heater. The elements on the Option type
structure are private by default and Null types are a common source of bugs in
therefore are not accessible to outside most traditional programming languages.
modules. The disable function takes a Rust provides Option type and Result type
mutable reference to itself and acts on the that can represent variables that can be
elements of the structure Heater. This API, uninitialized. Internally they are
marked as public is exposed to other implemented using templates.
modules so that they can interact with the Consider the example below:
heater structure. The function
dutycycle_to_period is a private method
that is inaccessible to outside modules.
pub fn do_pid_control_task(softtimer_block:&mut SoftTimers, task_frequency:u32, pid_bloc
k : &mut PidControl, prev_output : f32) ->Option
{
if softtimer_block.check_timer_ms(ESoftTimers::LedSoftTimer)
{
let res:Option;
let control_output = pid_block.run_loop(prev_output);
res = Some(control_output);
softtimer_block.set_timer_ms(ESoftTimers::PidSofttimer,task_frequency);
9return res;
}
else
{
return Option::None;
}
}
Code snippet: 2
The temperature control task runs only of the operation if the timer has expired or
periodically and returns res which is an returns the None type
option type. The res type contains the result
heater_control::do_temperature_control_task(&mut my_mcu.heater_controller , pid_output)
Code snippet: 3
Pattern Matching
This result is captured in option variable The match statement is used extensively in
pid_output and passed down to the the program, particularity when the option
do_temperature_control_task. The function type is involved. The
can then act on this parameter depending do_temperature_control_task from the
on whether it has a value or not. previous example utilizes match effectively
as shown.
pub fn do_temperature_control_task(pwm_block : &mut Heater, control_input_option : Opti
on)
{
match control_input_option
{
Some(val) => {pwm_block.set_dutycycle(val as u32);},
None => {},
}
}
Code snippet: 4
Here, if the option type does not have a Closures or anonymous functions as known
value then, no action is taken. in C++ are used extensively in the PACs.
Register manipulations are done utilizing
Closures closures
timer.icr.write(|w| w.tatocint().set_bit());
Code snippet: 5
10In this example, |W| is the closure that determining the visibility of functions inside.
performs the operation for setting a The scope operator as used in C++ is also
respective bit. available in Rust to keep the code structured
and modular. The syntax results in highly
Modules modular yet readable code without having
The Rust file system employs modules for to deal with separate implementation and
structuring code. Files are treated as interface files.
modules each with their own namespaces, Module heater control is imported into the
with access specifier as explained above file here using the keyword mod
mod heater_control;
heater_control::do_temperature_control_task(&mut my_mcu.heater_controller , pid_output)
;
Code snippet: 6
Public functions exposed by the heater the compiler cannot guarantee safety. While
module are accessed using the scope the language discourages the use of unsafe,
operator in this file. it is useful when dealing with raw pointers
or global values when it is determined to be
Unsafe blocks safe to do so by the developer as
unsafe keyword is necessary in places where demonstrated in the code snippet below.[9]
unsafe {
tm4c123x_hal::tm4c123x::NVIC::unmask(tm4c123x_hal::tm4c123x::Interrupt::TIMER0A)
}
Code snippet: 7
typestate checking[3]. The hardware
Templates therefore can be characterized as having
Templates are powerful features from the only a limited set of valid states with a clear
metaprogramming paradigm. It is useful for sequence of possible operations. A clear
generic programming and is used by Rust to example of this is the GPIO pin which is
accomplish monomorphization. Templates modeled such that the validity of operations
result in the increase in compile types but on it is determined by the state of the pin.
incur no performance penalties. Templated This results in much safer code that prevents
types are also used in the HAL libraries for wrong initialization at compile time
gpioc::PC5< AlternateFunction< AF1,PushPull>>
Code snippet: 8
PC5 here is defined as a pin of type configuration. The methods that can be
Alternate type AF1 with push pull applied on this pin are dictated by the
11typestate. It would result in a compiler error of guaranteeing safe concurrent access.
for an invalid method not defined for pins of While the usage of global mutable data is
this type. discouraged to prevent data race conditions,
they can help still be used inside unsafe
Concurrency blocks under the programmers discretion.[3]
Rust provides several features to aid
concurrency. The example below demonstrates the use of
The common paradigm of Foreground both the interrupt attribute for defining the
background process of using superloops timer0 interrupt as well as the utilization of
with interrupts is well supported, with the global mutable variable
use of attributes to define interrupt RUNNING_COUNTER.
handlers. On a few platforms atomic access
is supported for simple and effective means
static mut RUNNING_COUNTER:u32 = 0;
#[interrupt]
fn TIMER0A()
{
unsafe {
let periph_temp = tm4c123x_hal::Peripherals::steal();
let timer = periph_temp.TIMER0;
timer.icr.write(|w| w.tatocint().set_bit());
if RUNNING_COUNTER > MAX_TICK {RUNNING_COUNTER = 0;}
else {RUNNING_COUNTER+= 1 ;}
}
}
Code snippet: 9
within the critical section. The runtime
Critical sections are another crude but environment crate cortex_m also provides
effective concurrency mechanism supported standard resource management
by Rust. The functionality is provided by mechanisms such as mutexes to ensure safe
runtime environment crates such as concurrent access.
cortex_m[12]. These are often used with In the example below, the value of
closures, whose operations take place LED_MUTEX is set inside a critical section.
static LED_MUTEX: Mutex = Mutex::new(Cell::new(0));
cortex_m::interrupt::free(|cs| LED_MUTEX.borrow(cs).set(2) );
Code snippet: 10
12which project creation, library creation and
Real-Time Interrupt-driven Concurrency management are handled. CodeLLDB in
(RTIC)[7] is a framework that can be used for conjunction with JlinkGDB was used for
the development of larger more intricate flashing and debugging binaries.
real time systems. Features such as tasks, Since there were no IDEs for Rust
message passing, queues and scheduling development at the time, Microsoft Visual
provided by this framework makes this an Studio Code was used with the official Rust
excellent middle ground between extension. Tasks were setup for building and
developing concurrent programs from flashing the project within the IDE.
scratch using the concurrency mechanisms Debugging within the IDE was setup using
explained above and using full blown cortex-debug extension used in conjunction
schedulers such as FreeRtos. with Jlink GDB server.
Productivity
2.7. Development experience With the initial set up out of the way,
community driven libraries hosted on crates
were a good starting point for the project.
Learning
Embedded HAL implementation for TI and
The initial learning curve for Rust is steep
STM boards were readily available, although
especially for programmers with only C/C++
some features were unavailable or under
background. Programming concepts novel
the process of development requiring
to Rust such as the borrow checker and
register level manipulation of the hardware
move semantics are challenging and take
using the respective Peripheral Access crates.
time to get accustomed to. HAL interface
This is in stark contrast with C code since
construction requires careful thought and
chip manufacturer provide near complete
planning, established imperative or object
HAL which cuts down on development time
oriented approach for the same are hard to
of lower layers.
express in Rust. There are various official
Application development on the other hand
and community driven high quality
was made much simpler due to external
documentation for embedded systems
Crates. Suitable implementations for circular
development in Rust. The crates however
buffers, PID algorithms, CLI implementations
are purely community driven and therefore
etc. hosted on crates.io were easily
vary wildly in quality.
integrated into the code base. Inbuilt
support for testing and a built-in formatter
Setup and tools
also contributes to writing better quality
Setting up the development ecosystem is
code faster.
not a very involved process. There are
several tools that help the developer get
Ease of development
productive with the tool chain fairly early.
Rust provides strong abstraction with no
Rustup is the installer which aids in setting
performance tradeoffs which makes for
up the compiler and package manager for
highly expressive code. The compile times
development in Windows, Linux and Mac
are quite large especially if templates are
environment. Package management is
used heavily, due to all the static checks
handled by the Rust package manger Cargo,
performed. The error messages provided by
which also serves as the build system with
13the compiler are helpful and generally very Check Appendix section for metrics
accurate. Use of modules help with code collected in other optimization profiles.
architecture and help develop highly
structured easily maintainable code. Rusts
statically typed nature feel prohibitive at
SUMMARY AND CONCLUSIONS
times since it disallows implicit conversions,
or unhandled cases thereby making it hard The Rust programming language offers a
to use for prototyping. The use of raw rich set of features emphasizing safety
pointers is discouraged and the language which results in the development of reliable
does not fully support conventional OOP software. Safe programming paradigm helps
paradigms. in meeting safety requirements whereas
built-in support for testability helps with
2.8. Observations reliability and verifiability of critical
applications. Development process aided by
modern programming constructs and
The developed C and Rust code were used
portability across hardware, thanks to highly
for the capture of key code metrics. The
modular architecture, helps in improving
development process was kept identical for
time to market. The open source community
both languages. An effort was made to use
driven development helps in quick
idiomatic conventions to result in not only
adaptability in both developer communities
high performance but also high quality
and organization looking for a safe,
maintainable code. Memory footprint data
sustainable and stable ecosystem.
were collected from release versions of both
code bases with link time optimization
Code metrics collected suggest that the
enabled. The results are tabulated below:
code developed in Rust while about fifty
percent larger than code developed with its
Size No Optimization Size Optimized
counterpart C, is smaller in terms code base
ROM RAM ROM RAM
by close to fifteen percent. These penalties
C 20956 1348 18970 1243
however can be overlooked considering
Rust 100780 24 27936 24
improvements in code quality. While the
The code footprint in Rust is 50% larger ecosystem for embedded Rust is satisfactory
Table: 1 but pales in comparison to the maturity that
C/C++ provides. However the ecosystem is
Lines of code were also captured for both continuously evolving, and as the language
metrics as they are indicative of the gains traction this aspect will get better.
development effort. Overall, this modern language that is not
Lines of code weighed down by legacy like C++ is, offers a
C 1750 refreshing new option which, regardless of
Rust 1500 its future demands strong consideration
Rust code-base is 15% smaller
Table: 2
14References
1. S. Klabnik, C. Nichols. (2020). The Rust Programming Language.
2. Embedded Working Group. (2020). The Embedded Rust Book.
3. Embedded Working Group. (2020). The rustc book
4. Embedded Working Group. (2020). The Cargo book
5. Embedded Working Group. (2020). The Rustonomicon
6. P. Lindgren, Embedded Systems Group. (2020). Real-Time Interrupt-driven Concurrency.
7. Embedded Working Group. (2020). Discovery.
8. Embedded Working Group. (2020). The Embedonomicon.
9. Texas Instruments. (2014). TM4C123GH6PM Microcontroller.
10. The resources team. (2021). rust-embedded
11. The Cortex-M Team. (2020). Cortex-m-rt
12. J. Pallant, J. Aparicio. (2020). embedded-hal
13. D. Wood, J. Pallant, J.Aparicio, M. poulhies. (2020). tm4c123x-hal
14. J. Aparicio, P. Lindgren. (2021). heapless
1515. R. Horn. (2019). light-cli 16. K. Elkabany. (2020). PID Controller for Rust 17. D. Dockter. (2021). device-driver
You can also read