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 1
design 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 2
architecting 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 3
Research 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 4
3. 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 5
Time 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] 6
Since 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 7
Figure: 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 8
pub 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); 9
return 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 10
In 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 11
typestate. 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 12
which 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 13
the 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 14
References 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 15
15. R. Horn. (2019). light-cli 16. K. Elkabany. (2020). PID Controller for Rust 17. D. Dockter. (2021). device-driver
You can also read