300x250 AD TOP

Search This Blog

Pages

Featured Post

ESP32 SD Card Optimization

 Reading and writing SD Cards with ESP32 should be simple, however, the amount of moving parts in esp-idf makes that a complicated task, fir...

Paling Dilihat

Powered by Blogger.

Feature Label Area

Wednesday, June 29, 2022

ESP32 SD Card Optimization

 Reading and writing SD Cards with ESP32 should be simple, however, the amount of moving parts in esp-idf makes that a complicated task, first to understand and then to optimize.

Lets disect the stack:


File Operations

Working with files should not be new to you, however, there are some differences between ESP32 and working on a full pledged OS, these differences can affect how the product will behave.

On OS the buffered and unbuffered behaves very similar, in the end its the OS's job to buffer the disk operations so the system and applications are more responsive

However, due to resource limitations, on an MCU there is no such buffering other than the Standard library buffering and disk operations requirements.

In turn, this means you'll need to use the API in a manner suitable for these limitations.

Its probably best to avoid the buffered APIs if you don't plan to write unstructured data, such as logs and textual information, the unbuffered calls will pass the read and write requests as-is, so you can have as much control as you need without modifying any of the libraries.

Standard Library

The standard library is responsible for translating the buffered and unbuffered calls to the next layer, so eventually these functions will call the platform's implementation

Specifically in ESP32, newlib was compiled with 128 bytes buffer, this creates very inefficient calls to the file system since each fread/fwrite will split each call to 128 bytes which is significantly less than the sector size.

You may ease some of this inefficiency by increasing the buffer size for the buffered calls:

if(setvbuf(file, NULL, _IOFBF, 4096) != 0) {
  //handle error
}

File Stream Adapter

The file stream adapters, or the VFS enables mounting multiple file systems under different root paths, so in essence each root path refers to a different mount.

When opening a file through the standard library, a new file descriptor is created in the file system driver (currently only FATFS), it then gets translated into VFS file descriptor and returned to the standard library.

When writing to the file descriptor, the process reverses and the file descriptor is translated back to the filesystem's file descriptor.

File System

Currently only FATFS is implemented as file system (FAT) in ESP32, however, using other file systems such as LittleFS requires very little effort.

The file system is responsible for opening, reading, writing and other file system operations to read and persist from a physical medium.

For example, when opening a file, the FATFS will go to the root directory and traverse subdirectories until it finds where the file is located while reading the file allocation table, all of that is done with raw access to the underlying storage, i.e. read sector x,y,z.

FATFS was designed for resource constrained systems, so it will not buffer more than it must and will read and write as little as possible to achieve the task it needs, even if it means inefficient calls to the storage. for example, writing unaligned data will result in read sector, update part of it, write it back.

This creates inefficiencies when working with unaligned random accesses to files since it requires FATFS to "waste" time getting the data it needs in blocks and not using part of that data.

But these inefficiencies can be reduced by making sure all your random access to files is aligned to the sector sizes. 

More issues you may encounter is when your reads and writes cross the cluster boundaries, each cluster can be hosted on a different place in the block device which will require two separate calls to the underlying storage.

To reduce the amount of waste these calls create, FATFS implements fast seek (and in esp-idf), which caches the cluster link map table, however this works only on read only files or preallocated write files.

Also, if you plan to use your design as a product, keep in mind that Windows formats SD cards as EXFAT which is turned off by default in esp-idf. this can cause some usability issues to your customers and if you choose you can override that configuration. There are two ways of doing it, each with its own advantages and drawbacks:

1. copy the component/fatfs to your project's root directory under component/fatfs and rebuild the project.

2. Override package files and enable it in your ffconf.h.

File System Adapter

The file system adapter's job is to read and write data from a block device, optionally with wear leveling driver in between, since SD card implement wear levelling internally, this layer is not used when working with SD cards.

Most block devices such as SD cards and eMMC implement their storage in blocks, usually 512 or 1024 bytes per block (or sector), which means that the FATFS layer must speak in sectors, so if it needs to update 2048 bytes, it will do it in 512 bytes chunks in most cases.

Hardware Drivers

The hardware driver's job is to read and write sectors to the underlying storage media, currently SD SPI and SDMMC 1 and 4 bits are impended.

Recommendations:

  • Configure your hardware drivers as fast as possible, SPI and SDMMC
  • Prefer Sequential over Random reads and writes, SD cards were designed for sequential access.
  • If you must use random access, avoid using the buffering APIs 
  • If you must use random read / write, prefer aligned read and write in sector size chunks.
  • If you must use buffering APIs, increase the buffer to at least the sector size in sector size increments (your average buffer size aligned to 512 or 1024 )
  • Use FATFS cluster cache (or fast seek) if applicable, please note that to reduce incompatibilities the cache is disabled for writeable files in vfs_fat.c, however, you can override it if you know what you're doing.
  • Enable EXFAT if its a customer facing product
  • Prefer SDMMC over SPI, though it requires pull ups and you might need to adjust your design due to GPIO bootloader modes, however, I was able to use high speed sdmmc by activating the internal pullup on GPIO 12 programmatically and it was enough.

This has been a learning experience, hopefully my lessons will ease your learning experience.

Tags: , , , , , , , , , ,

Wednesday, June 8, 2022

PlatformIO Menu Configuration Integration

One of the hurdles when building a custom library or software is how to allow the user to configure it. We can have a header file with comments and tell the user how to configure it but if multiple defines start to have dependencies or the value is more than true or false, there's a chance the user will get this wrong and the a whole class of issues is going to be opened and time is going to be wasted.

source


The linux kernel solved it with the Kconfig utility, which was reimplemented using the kconfiglib. fortunately its easier to install and use and the integration requires only menuconfig and genconfig utilities.

To implement it in our software we need to do the following:

KConfig file

write your configuration file, here's an example with one boolean and one string configuration values:

mainmenu "Sample configurable project using Kconfig"

config FOO
    bool "Foo module"
    help
        The infamous Foo module
config BAR
    string "Bar Value"
    help
        The Bar Value

platformio.ini

Once we have the configuration file we need to tell menuconfig and genconfig where the file is, where to save the configuration settings and where to generate the header file with the configuration values.

; menuconfig runner
extra_scripts = 
    scripts/run_menuconfig.py
; path to Kconfig file
custom_kconfig_config = scripts/configs/Kconfig
; configuration settings file
custom_kconfig_save_settings = include/custom_config.config
; configuration settings file and header file header comment
custom_kconfig_comment_header =
    File Header
    hello world
; output configuration header file
custom_kconfig_output_header = include/custom_config.h

All we need to do now is execute the runner

pio run -t kconfig


when we quit and save, menuconfig will generate the configuration setting file to custom_kconfig_save_settings:

#File Header
#hello world
CONFIG_FOO=y
CONFIG_BAR="hello world"

and then execute genconfig and generate the header file to custom_kconfig_output_header:

// File Header
// hello world
#define CONFIG_FOO 1
#define CONFIG_BAR "hello world"

and lastly, we can use the header file like any other header file and get our configuration from it:

#include <custom_config.h>
MAIN()
{
    printf("Program started!\r\n");
    printf("Bar Value %s\r\n", CONFIG_BAR);
}


Tags: , , ,

Tuesday, June 7, 2022

PlatformIO Dynamic Code Analysis and Coverage

Like static code analysis, dynamic code analysis provides another level of coding errors detection and can help diagnose memory access issues such as use-after-free or uninitialized read and even memory leaks and memory write overruns.

source


On top of that, we can write every line of code that has been used and later correlate that and say which lines were executed and which were not and that is the essence of code coverage. 

Code Coverage

A unit test is just code that executes your code, by executing your code and providing it with information it needs and checking its return values and side effects we can determine if your code does what it was designed to do.

Running unit tests is usually done by creating another executable or firmware that runs all your tests one by one and reports their status. 

Eventually, if this executable writes which lines were executed, a reporting tool can later read that data and cross reference it with the source code to produce statistics and reports.

To make it work, we need to disable optimizations and compile debug symbols, otherwise we'll get bad results if any. 

So first, we'll add the required flags to our environment in platformio.ini:

build_flags = -ggdb -lgcov -O0 --coverage

Now when we run tests, the coverage data will be written into the gcda and gcno files we can view it  with a VSCode Extension directly:



But it might not be enough, we can also view it in command line or even in CI so well need to build a report from it, we'll use gcovr do build a tracefile from it and since we have more than one test module, we'll want to merge all the tracefiles to a single report.

Lets start by adding our runner to platformio.ini:

extra_scripts = 
    scripts/run_gcovr.py

The runner hooks into the test executable generation and executes gcovr and generate a tracefile for each test executable.

When the tests are done, we can get the coverage info by running:

pio run -t gcovr

We can then browse the .reports/coverage.html


If you have other requirements from gcovr, there are many options for the output and you can use a configuration file to specify what you need.


Dr. Memory

By hooking into the PlatformIO testing mechanism and intercepting the compiled tests we can execute the tests under Dr. Memory's supervision and get a report of where exactly code was leaking or accessing memory it wasn't supposed to, it does require the developer to not leave dangling memory after tests so each malloc will need to be matched with free and every new will have to be delete(ed).

This step does not require any intervention from the user other than add run_drmemory.py to extra_scripts:

extra_scripts = 
    scripts/run_drmemory.py

Just note that Dr. Memory can do more than that, so its good to explore it further.




To sum things up, dynamic code analysis tools are essential to writing quality code with the added benefit of finding which parts of your code have leaks or not covered by enough tests.

Continue reading about static code analysis...

Tags: , , ,

Thursday, June 2, 2022

PlatformIO Static Code Analysis

Source


Writing code and testing it with unit and integration tests can provide high quality executable, however, it does not indicate if its a maintainable code, moreover code can have side effects when used outside its designed use, so in essence, tests pass, QA approved but then it gets in the field and crashes. 

One of the ways to make sure code is maintainable, understandable and quick to get into is code reviews, however, using style checks and static code analysis can help automate some of it before a human can review it.

Applications are like Swiss cheese, one tool can verify certain aspects of it but many tools can discover more possible issues.

Code Style 

Code style defines how a code looks like to the developer, it will most likely not affect how the code is run but it can affect how quickly a developer can understand what its doing.

clang-format

clang-format is a C++ code formatter and supports many formatting options that can help with readability and consistent source code format.

to install clang-format:
pip install clang-format

Once installed, we can generate a default clang-format configuration file and edit it if needed, clang-format defines a few standard styles: LLVM, GNU, Google, Chromium, Microsoft, Mozilla, WebKit

the following line will generate the configuration based on LLVM:
clang-format -style=LLVM -dump-config > .clang-format

For PlatformIO specific integration you can use my runner and execute it as follows:

pio run -t format

cpplint

cpplint is a tool that checks Google's C++ Style Guide, it can report the issues to command line or you can use the VSCode Extension and see the issues while writing code.

While the style guide is very strict, you can control how cpplint behaves by adjusting the linelength and which rule to apply or ignore in cpplint.cfg

For PlatformIO specific integration you can use my runner and execute it as follows:

pio run -t lint

By combining clang-format and cpplint you can avoid ever styling your code manually

Static Code Analysis

While coding style can really help for readability of code and preventing confusion, styling by itself does not contribute to the quality of code executed. Lets explore what is available 

Compiler Warnings

A great source of warnings and issues that originate from mistakes or lack of understanding of C/C++ languages is compiler warnings, the default warning level attempts to balance between certain mistakes and an attempt not to overwhelm the developer.

These flags can help you to switch between simple mistakes and pedantic development style, if you find yourself in a pickle or would like to avoid the pickle all together it might be beneficial to use the more restrictive warning levels but like everything in software, the tool does not make the software and you'll need to understand why you need to fix what the compiler tells you to fix.

Chris Coleman wrote about The Best and Worst GCC Compiler Flags For Embedded other than the GCC warnings documentation but in summary:

-Wall - enables warnings about questionable practices that are easy to avoid

-Wextra - more warnings 

-Wshadow - shadowing is a readability issue that can also lead to bugs since the developer might get confused about which variable is actually in use.

-Wdouble-promotion - some MCUs have FPU that supports floats only, whenever a floating point gets promoted to double for any reason this warning will tell you about it since you might lose performance over it.

-Wformat=2 - checks scanf and printf mistakes

-Wformat-truncation - checks snprintf has enough room, heuristics based.

-Wundef - warning if undefined identifier is evaluated in the preprocessor.

-Weffc++ - Warn about violations of style guidelines from Scott Meyers’ Effective C++ series of books

In any case, you can always view which warnings are enabled by:

gcc -Q --help=warnings

If you'd like to see which ones are enabled with using a certain warning level:

gcc -Wall -Wextra -Q --help=warnings

PlatformIO Check

PlatformIO check provides easy access to two static code analyzers, cppcheck and clang-tidy. to use them we need to add:


check_tool = cppcheck, clangtidy

and then run:

pio check

This should get you results similar to this when running the checks:
Checking native > cppcheck (platform: native)
----------------------------------------------------------------------------------------------
src\port_arduino.h:6: [low:style] Function types shall be in prototype form with named parameters [misra-c2012-8.2]
...
================================ [PASSED] Took 3.53 seconds ==================================
Checking native > clangtidy (platform: native)
----------------------------------------------------------------------------------------------
src\main.cpp:6: [medium:warning] system include stdio.h not allowed  [llvmlibc-restrict-system-libc-headers]
...
================================ [PASSED] Took 1.34 seconds ==================================

Component            HIGH    MEDIUM    LOW
------------------  ------  --------  -----
lib\circularbuffer    0        0       32
...

Total                 2        17      60

Environment    Tool       Status    Duration
-------------  ---------  --------  ------------
native         cppcheck   PASSED    00:00:03.533
native         clangtidy  PASSED    00:00:01.341
================================= 2 succeeded in 00:00:04.874 ================================

cppcheck

cppcheck is a free static code analyzer, it detects common mistakes and also supports a subset of MISRA standard, you can find examples and explanations in the Zephyr documentation.

To support MISRA checks, you'll need to add a few things to the default PlatformIO configuration.

1. in platformio.ini section:

check_flags =
    cppcheck: --enable=all --addon=./scripts/misra.json --addon=cert --addon=threadsafety --addon=y2038

2. download misra.py, misra_9.py and cppcheckdata.py from cppcheck repository and place it in scripts folder.

3. add misra.json to the scripts folder, this is the configuration for the MISRA addon, I've disabled rule 17.7 in this example.

{
    "script": "scripts/misra.py",
    "args": ["--rule-texts=scripts/misra.txt","--suppress-rules 17.7"]
}

4. download misra.txt and place it in the scripts folder for the addon to pick up and use as messages.

This should get you results similar to this when running the checks:

Checking native > cppcheck (platform: native)
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
src\port_arduino.h:6: [low:style] Function types shall be in prototype form with named parameters [misra-c2012-8.2]
src\port_arduino.h:7: [low:style] Function types shall be in prototype form with named parameters [misra-c2012-8.2]
src\port_arduino.h:10: [low:style] Function types shall be in prototype form with named parameters [misra-c2012-8.2]
lib\examplelib\ProductionCode.h:3: [low:style] Function types shall be in prototype form with named parameters [misra-c2012-8.2]
src\main.cpp:6: [low:style] The Standard Library input/output functions shall not be used [misra-c2012-21.6]
src\main.cpp:22: [low:style] Do not use the rand() function for generating pseudorandom numbers [cert-MSC30-c]
lib\circularbuffer\CircularBuffer.cpp:32: [low:style] There should be no unused parameters in functions [misra-c2012-2.7]
lib\circularbuffer\CircularBuffer.cpp:88: [low:style] A string literal shall not be assigned to an object unless the object's type is pointer to const-qualified char [misra-c2012-7.4]
lib\circularbuffer\CircularBuffer.cpp:102: [low:style] A string literal shall not be assigned to an object unless the object's type is pointer to const-qualified char [misra-c2012-7.4]
lib\circularbuffer\CircularBuffer.cpp:105: [low:style] A string literal shall not be assigned to an object unless the object's type is pointer to const-qualified char [misra-c2012-7.4]
lib\circularbuffer\CircularBuffer.h:50: [low:style] Function types shall be in prototype form with named parameters [misra-c2012-8.2]
lib\circularbuffer\CircularBuffer.h:52: [low:style] Function types shall be in prototype form with named parameters [misra-c2012-8.2]
lib\circularbuffer\CircularBuffer.h:53: [low:style] Function types shall be in prototype form with named parameters [misra-c2012-8.2]
lib\circularbuffer\CircularBuffer.h:54: [low:style] Function types shall be in prototype form with named parameters [misra-c2012-8.2]


clang-tidy

clang-tidy is llvm's static code analayzer, one of the more interesting features is that it can fix some errors it finds. 

to run it with fix you can run it as follows:
pio check --flags "clangtidy: --fix"

This should get you results similar to this when running the checks:
Checking native > clangtidy (platform: native)
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
src\main.cpp:6: [medium:warning] system include stdio.h not allowed  [llvmlibc-restrict-system-libc-headers]
src\main.cpp:6: [medium:warning] inclusion of deprecated C++ header 'stdio.h'; consider using 'cstdio' instead  [hicpp-deprecated-headers,modernize-deprecated-headers]
src\main.cpp:9: [medium:warning] system include stdlib.h not allowed  [llvmlibc-restrict-system-libc-headers]
src\main.cpp:9: [medium:warning] inclusion of deprecated C++ header 'stdlib.h'; consider using 'cstdlib' instead  [hicpp-deprecated-headers,modernize-deprecated-headers]
src\main.cpp:16: [medium:warning] declaration must be declared within the '__llvm_libc' namespace  [llvmlibc-implementation-in-namespace]

Flawfinder

Flawfinder is a simple tool for scanning source code for possible security weaknesses (or "flaws"). 

For PlatformIO specific integration you can use my runner and execute it as follows:

pio run -t flawfinder

This should get you results similar to this when running flawfinder:

flawfinder -C -c -D -i -S -Q include src lib\arduino-printf lib\circularbuffer lib\defectedLib lib\examplelib lib\runner
src\main.cpp:21:2:  [0] (format) printf:If format strings can be influenced by an attacker, they can be exploited (CWE-134).  Use a constant for the format specification. Constant format string, so not considered risky.
        printf("test broken %d\r\n", FindFunction_WhichIsBroken(78));
src\main.cpp:24:2:  [0] (format) printf:If format strings can be influenced by an attacker, they can be exploited (CWE-134).  Use a constant for the format specification. Constant format string, so not considered risky.
        printf("displaying float %.6f", c);
lib\circularbuffer\CircularBuffer.cpp:90:5:  [2] (buffer) char:Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120).  Perform bounds checking, use functions
that limit length, or ensure that the size is larger than the maximum possible length.
    char sval[10];
lib\circularbuffer\CircularBuffer.cpp:98:9:  [0] (format) snprintf:If format strings can be influenced by an attacker, they can be exploited, and note that sprintf variations do not always \0-terminate (CWE-134).  Use a constant for the format specification. Constant format string, so not considered risky.
        snprintf(sval, sizeof(sval), "%d", buffer[printIndex]);

lib\circularbuffer\CircularBuffer.cpp:90:5:  [2] (buffer) char:Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120).  Perform bounds checking, use functions
that limit length, or ensure that the size is larger than the maximum possible length.
    char sval[10];

doxygen

while doxygen is not a static code analyzer per-se, it can help to generate documentation, call graphs and help other developers to understand the code in shorter time.

To ease with function documentation, you can use the Doxygen VSCode Extension.

Install doxygen and graphviz which is used to generate the graphs.

generate a basic configuration file:

doxygen -g .doxygen

As a quick start override the following settings:

OUTPUT_DIRECTORY       = docs
INPUT                  = lib src
BUILTIN_STL_SUPPORT    = YES
EXTRACT_ALL            = YES
EXTRACT_STATIC         = YES
WARN_NO_PARAMDOC       = YES
RECURSIVE              = YES
STRIP_CODE_COMMENTS    = NO
REFERENCED_BY_RELATION = YES
REFERENCES_RELATION    = YES
GENERATE_LATEX         = NO
MACRO_EXPANSION        = YES
HAVE_DOT               = YES
UML_LOOK               = YES
CALL_GRAPH             = YES
CALLER_GRAPH           = YES
INTERACTIVE_SVG        = YES

Generate the documentation:

doxygen .doxygen

doxygen .doxygen

And finally, you should be able to see the documentation by opening the docs/html/index.html, search for functions and browse the documentation, you'll see that just after a few minutes you already know the basic structure, which function references which and you'll get a feeling of the general flow of the application.


Code Metrics

Code metrics are used to find hot spots that go against clean code's most prominent rule, “The first rule of functions is that they should be small.”
With code metrics we can find long functions, long files, complex functions and the most basic thing we can do to our team members to save them from this.

Lizard

Lizard is a code complexity analyzer, while it supports many languages, we only want it to work with C/C++ code.

First, we'll need to setup lizard's setting in platformio.ini, in this case we're limiting the complexity to 15, maximum length of functions to 100 lines and maximum arguments passed to a function is 1.

cyclomatic_complexity_analyzer = --CCN 15 --length 100 --arguments 1 --warning-msvs

For PlatformIO specific integration you can use my runner and execute it as follows:

pio run -t lizard

And see results similar to this:
lib\examplelib\ProductionCode2.c(4): warning: ThisFunctionHasNotBeenTested has 6 NLOC, 1 CCN, 28 token, 2 PARAM, 8 length
lizard -Eduplicate include src lib\arduino-printf lib\circularbuffer lib\defectedLib lib\examplelib lib\runner
*** Error 1
================================================
  NLOC    CCN   token  PARAM  length  location
------------------------------------------------
       8      2     48      0      10 setup@16-25@src\main.cpp
       3      1      5      0       4 loop@27-30@src\main.cpp
       7      2     19      0      10 main@10-19@src\port_arduino.h
       5      1     44      1       5 CircularBuffer::CircularBuffer@32-36@lib\circularbuffer\CircularBuffer.cpp
       4      1     12      0       4 CircularBuffer::~CircularBuffer@38-41@lib\circularbuffer\CircularBuffer.cpp
       4      1     10      0       4 CircularBuffer::IsEmpty@43-46@lib\circularbuffer\CircularBuffer.cpp
       4      1     10      0       4 CircularBuffer::IsFull@48-51@lib\circularbuffer\CircularBuffer.cpp
       8      3     49      1       8 CircularBuffer::Put@53-60@lib\circularbuffer\CircularBuffer.cpp
      11      3     51      0      12 CircularBuffer::Get@62-73@lib\circularbuffer\CircularBuffer.cpp
       4      1     10      0       4 CircularBuffer::Capacity@75-78@lib\circularbuffer\CircularBuffer.cpp
       5      2     23      1       5 CircularBuffer::Next@80-84@lib\circularbuffer\CircularBuffer.cpp
      18      5    116      0      22 CircularBuffer::Print@86-107@lib\circularbuffer\CircularBuffer.cpp
      11      2     69      0      11 dynamic_buffer_overrun_018@5-15@lib\defectedLib\bufferLibrary.c
       4      1     14      0       4 memory_leak_001@17-20@lib\defectedLib\bufferLibrary.c
       9      3     36      1       9 FindFunction_WhichIsBroken@11-19@lib\examplelib\ProductionCode.c
       4      1      9      1       4 FunctionWhichReturnsLocalVariable@21-24@lib\examplelib\ProductionCode.c
       6      1     28      2       8 ThisFunctionHasNotBeenTested@4-11@lib\examplelib\ProductionCode2.c
       3      1     11      0       3 setup@30-32@lib\runner\runner.h
13 file analyzed.
==============================================================
NLOC    Avg.NLOC  AvgCCN  Avg.token  function_cnt    file
--------------------------------------------------------------
     19       5.5     1.5       26.5         2     src\main.cpp
      9       7.0     2.0       19.0         1     src\port_arduino.h
      0       0.0     0.0        0.0         0     src\sdkconfig.h
      1       0.0     0.0        0.0         0     lib\arduino-printf\arduino-printf.h
     65       7.0     2.0       36.1         9     lib\circularbuffer\CircularBuffer.cpp
     29       0.0     0.0        0.0         0     lib\circularbuffer\CircularBuffer.h
     18       7.5     1.5       41.5         2     lib\defectedLib\bufferLibrary.c
      2       0.0     0.0        0.0         0     lib\defectedLib\bufferLibrary.h
     16       6.5     2.0       22.5         2     lib\examplelib\ProductionCode.c
      2       0.0     0.0        0.0         0     lib\examplelib\ProductionCode.h
      7       6.0     1.0       28.0         1     lib\examplelib\ProductionCode2.c
      1       0.0     0.0        0.0         0     lib\examplelib\ProductionCode2.h
     12       3.0     1.0       11.0         1     lib\runner\runner.h

===============================================================================================================
No thresholds exceeded (cyclomatic_complexity > 15 or length > 1000 or nloc > 1000000 or parameter_count > 100)
==========================================================================================
Total nloc   Avg.NLOC  AvgCCN  Avg.token   Fun Cnt  Warning cnt   Fun Rt   nloc Rt
------------------------------------------------------------------------------------------
       181       6.6     1.8       31.3       18            0      0.00    0.00
Duplicates
===================================
Total duplicate rate: 0.00%
Total unique rate: 100.00%


Now we can see a warning that a function has two parameters (over 1 of the limit we set)
We can also see statistics for the entire project analysis, this can help us locate functions that are approaching the limits we set and find duplicate code.

Coding Standards

In addition to MISRA, there are other interesting standards that you should be aware of.

SEI CERT C Coding Standard (PDF), what I really like about this standard is the explanation each rule have and why its in the standard.

Lastly but not less important is the AUTOSAR Guidelines for the use of the C++14 language in critical and safety-related systems (PDF), like the others, one of the more important sections is the rational for including the guidelines can provide an important insight into the "why" and is always a good reading material.

Test Sample

Lastly, if you're currently evaluating code standard tools and static code analysis tools, you may find the itc-benchmarks beneficial.

References:

Evaluation of Open Source Static Analysis Security Testing (SAST) Tools for C

FOSS Static Analysis Tools for Embedded Systems and How to Use Them

Joint Strike Fighter Air Vehicle C++ Coding Standards

Tags: , , , , , , , , , ,

Sunday, May 22, 2022

Unit Testing with ESP32 and PlatformIO

Writing Quality Code and Tests usually goes hand in hand, some find it more productive to write code and then write tests for it and others, following TDD/BDD write tests before code. The benefits of each is out of scope for this article, what is in scope is how to reach the nirvana of software development by using tests.


source


Lets start with the basics.

PlatformIO tests can be run on:

  • Locally - on the development machine, it requires that gcc is installed and in the path. for windows this can be done with mingw gcc.
  • On Device - PlatformIO automates this by building the tests, uploading them and resetting the device in order for them to execute and wait for 2 lines, one stating how many tests were executed and another stating if any of the tests failed.
  • Remotely - PlatformIO automates this by building the code on the development machine, copying the firmware to the remote agent, uploading to the device connected to the remote agent and waiting for the test results.

To reach that nirvana we can write tests that execute on the ESP32, but no matter how fast your machine is, compiling, uploading and executing tests on ESP32 can take anywhere from 1 to 10 minutes and that will reduce the effectiveness of the TDD/BDD cycles, one of the big enemies of workflow is slow workflow where the mind wanders off.

Another option would be to do quick development cycles on the development machine and every once in a while running the tests on the device and lastly ensuring code quality by allowing the CI on the build machine to make sure the code is built and tested on an actual device.

The basic Tenet

All testing should be written in the same way, we prepare code and data for the unit under test, we execute the operation and we should verify the outcome is the one we expected, this is called the AAA Pattern, or Arrange-Act-Assert.

Arrange - prepare objects and data for operation

Act  - execute operation

Assert - check results/side effects

If you have the same arrange for many tests, consider the "setup" phase most testing frameworks have, but to maintain a coherent style one should strive to keep the generic parts in the setup phase and the test specific parts in the test itself.

If you elected to use the "setup" phase, just note that you also have the "tear down" phase to do the cleanup from the setup phase - keeping them out of the test is usually a good practice.

Another tenet is repeatability, tests which are not repeatable cannot be trusted and are soon ignored. avoid testing random values in unit tests.

Unit Tests

Unit tests are the most basic kind of testing but are sometimes misunderstood, the purpose of the unit tests are to verify a needed functionality works in the way it was designed to work during the life of the application, there is a broad interpretation of what constitutes a 'unit' but most agree that a unit is a small chunk of code (or function) that can be accessed from outside the module its in.

Note: while testing every individual functionality manually can also verify the code "works", its not doing it throughout the life of the application, a manual test can lead to a waste of time and will not test the same functionality every time. 

PlatformIO provides a way to run tests and split them into groups, each group can be set to either run or not run on each platform (ESP32, Native and more), we can split by library, header or other significant group.

Project Tests Structure

For very simple projects you can put a single main file in the test folder and run all tests from it, keep in mind if it gets too big it might not fit in ESP32. 
For more complex projects, its possible to create multiple executables in the test folder by using subfolders, each subfolder starting with test_ will create a separate executable.

Like in all other unit test demos, we'll start with a simple calculator, it has addition, subtraction, multiplication and division.

int Calculator::add(int a, int b)
{
    return a + b;
}

int Calculator::sub(int a, int b)
{
    return a - b;
}

int Calculator::mul(int a, int b)
{
    return a * b;
}

int Calculator::div(int a, int b)
{
    return a / b;
}

Testing Frameworks

PlatformIO supports out of the box a few testing frameworks, the more popular ones in the embedded world are unity, cpputest and doctest.

Now we'll write some tests

Unity

Unity is popular due to its low overhead and simple use, it was the first framework supported on PlatformIO.

#include <calculator.h>
#include <unity.h> //Unity Testing Framework
#include <runner.h> //Simplifies main()

Calculator calc;

void test_function_calculator_addition(void) {
    TEST_ASSERT_EQUAL(32, calc.add(25, 7));
}

void test_function_calculator_subtraction(void) {
    TEST_ASSERT_EQUAL(20, calc.sub(23, 3));
}

void test_function_calculator_multiplication(void) {
    TEST_ASSERT_EQUAL(50, calc.mul(25, 2));
}

void test_function_calculator_division(void) {
    TEST_ASSERT_EQUAL(32, calc.div(96, 3));
}

void process() {
    UNITY_BEGIN();
    RUN_TEST(test_function_calculator_addition);
    RUN_TEST(test_function_calculator_subtraction);
    RUN_TEST(test_function_calculator_multiplication);
    RUN_TEST(test_function_calculator_division);
    UNITY_END();
}

MAIN(){
    process();
}

doctest

doctest is a similar framework to Catch, except for the slow compilation time, some developers really like Catch's way of doing things but really hate the slow compilation time, doctest while natively supported by PlatformIO still have issues when compiling for ESP32, you can find a modified doctest in the examples that works fine with both ESP32 and natively.


#define DOCTEST_CONFIG_IMPLEMENT
#define DOCTEST_THREAD_LOCAL
#include <doctest/doctest.h> //doctest testing framework
#include <runner.h> //Simplifies main()

MAIN(){
    const int argc_ = 3;
    const char *argv_[] = {
        "exe",
        "-d",
        "-s"};
    return doctest::Context(argc_, argv_).run();
}

#include <calculator.h>

Calculator calc;


TEST_CASE("calculator addition"){
    CHECK(32== calc.add(25, 7));
}

TEST_CASE("calculator subtraction"){
    CHECK(20 == calc.sub(23, 3));
}

TEST_CASE("calculator multiplication"){
    CHECK(50 ==  calc.mul(25, 2));
}

TEST_CASE("calculator division"){
    CHECK(32 == calc.div(96, 3));
}

CppUTest

#include <runner.h> //Simplifies main()

#define CPPUTEST_USE_LONG_LONG 1 //mandatory for cpputest to work with esp32
#include "CppUTest/CommandLineTestRunner.h"
#include "CppUTest/TestPlugin.h"
#include "CppUTest/TestRegistry.h"
#include "CppUTestExt/IEEE754ExceptionsPlugin.h"
#include "CppUTestExt/MockSupportPlugin.h"

MAIN(){
    const char * argv_[] = {
        ""
        "",
        "-v",
        "-c",
        "-o",
        "eclipse"
        //"teamcity"//"eclipse"//"junit"
    };
    return CommandLineTestRunner::RunAllTests(5, argv_);
}

#include <calculator.h>

Calculator calc;

TEST_GROUP(Calculator){ };


TEST(Calculator, Addition){
    CHECK(32== calc.add(25, 7));
}

TEST(Calculator, Subtraction){
    CHECK(20 == calc.sub(23, 3));
}

TEST(Calculator, Multiplication){
    CHECK(50 ==  calc.mul(25, 2));
}

TEST(Calculator, Division){
    CHECK(32 == calc.div(96, 3));
}

If you're migrating to PlatformIO and already use a different framework which is not on the supported list, you might be interested to know how to setup PlatformIO to work with custom framework

Environments

To use PlatformIO effectively, we'll need to tell it which setups its going to work with, such as Platforms, Boards and Frameworks, the combination of these can be grouped into environments in platformio.ini, however, environments can contain more than that

Lets define two environments, one for ESP32 and one for natively running on the development machine.

[env:esp32]
platform = espressif32
board = esp32doit-devkit-v1
framework = espidf

[env:native]
platform = native

Running Tests

Running tests in PlatformIO is pretty simple, this command will run all tests in all environments, so if we have a native and ESP32 environment defined, it will executes all tests against them.
pio test

But lets say we want to run only native environment:
pio test -e native

Lastly, some labs have the device connected to a remote agent and shared by multiple developers or even a CI agent

pio remote test

Limiting Tests to Specific Environments

We can always use ifdef guards to build tests for specific platforms but for the sake of order we're probably going to want to group common, embedded and desktop tests. I propose that we'll have 3 separate folders for our sample tests and tell PlatformIO to ignore the incompatible tests on the incompatible platforms.

[env:esp32]
platform = espressif32
board = esp32doit-devkit-v1
framework = espidf
test_ignore = test_desktop

[env:native]
platform = native
test_ignore = test_embedded

Now that we have our tests working, lets see how the results look like

>pio test -e native
Verbosity level can be increased via `-v, -vv, or -vvv` option
Collected 3 tests

Processing test_common in native environment
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Building...
Testing...
test\test_common\test_calculator.cpp:49: test_function_calculator_addition      [PASSED]
test\test_common\test_calculator.cpp:50: test_function_calculator_subtraction   [PASSED]
test\test_common\test_calculator.cpp:51: test_function_calculator_multiplication        [PASSED]
test\test_common\test_calculator.cpp:52: test_function_calculator_division      [PASSED]
------------------------------------------------------------------------------------ native:test_common [PASSED] Took 2.75 seconds ------------------------------------------------------------------------------------

Processing test_desktop in native environment
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Building...
Testing...
test\test_desktop\test_calculator.cpp:49: test_function_calculator_addition     [PASSED]
test\test_desktop\test_calculator.cpp:50: test_function_calculator_subtraction  [PASSED]
test\test_desktop\test_calculator.cpp:51: test_function_calculator_multiplication       [PASSED]
test\test_desktop\test_calculator.cpp:44: test_function_calculator_division: Expected 32 Was 33 [FAILED]
------------------------------------------------------------------------------------ native:test_desktop [FAILED] Took 2.42 seconds ------------------------------------------------------------------------------------

======================================================================================================= SUMMARY =======================================================================================================
Environment    Test          Status    Duration
-------------  ------------  --------  ------------
native         test_common   PASSED    00:00:02.753
native         test_desktop  FAILED    00:00:02.421

_________________________________________________________________________________________________ native:test_desktop _________________________________________________________________________________________________
test\test_desktop\test_calculator.cpp:44:test_function_calculator_division:FAIL: Expected 32 Was 33

Emulators

PlatformIO has integrated a few emulators, unfortunately none of them is for ESP32, however, Espressif has worked on QEMU, which might make it into the supported emulators one day.

Multithreading

Testing multithreaded code does not technically constitutes a unit test, however, sometimes we want to verify the integration between components bridged by concurrency patterns.

More over, unit testing multithreaded code can lead to issues, such as irreproducible or sparse failures with no certain way to check why and in turn lead to tests being ignored or deleted.

In general, when developing multithreaded code the main issues raise when threads share memory and data, depending on architecture and word size, accessing unaligned variables and structs usually compiles to multiple instructions that access and dissect the aligned memory into smaller chunks which will cause issues if multiple threads access that data since the context switch can occur between instructions.

With that being said, if you must test multithreaded code, you'll need to control the timing or wait for something to happen and not depend on sleeps and delays since it will make your code non-deterministic.

If you must share data between threads, do it with the appropriate concurrency pattern, such as messages, queues and lists.

You may find more interesting patterns with etl.

RTOS

RTOS use on a microcontroller can enable higher quality code by separating responsibilities to individual tasks and functions, allowing them to interact safely, the downside of that is the added complexity of working with RTOS. The esp-idf has integrated FreeRTOS already for you, saving some of the learning curve but I do recommend reading the FreeRTOS documentation and even going into some of its source code to understand exactly what's going on, if you really need it, FreeRTOS has been ported to Windows/Linux so you test a part of your code on your development machine.



Tags: , , ,

Monday, November 15, 2021

USART with DMA on STM32

 I've been working with many projects that use the USART and not one was like the other alghough hardware resources were pretty similar. 

So I've sat down and decided to make a boilerplate for USART with DMA implementation that uses binary semaphores to notify when data arrives and buffers the output to create as little delay as possible as well as leave as much CPU as possible for the rest of the system.

For this demo I'll be using the STM32F446 Nucleo-64.



By default, it has the USART2 pins connected to the on board ST-Link so its possible to just open a terminal, watch logs and send commands to the MCU with as little effort as possible.


Once we have the basics setup in the IDE and the USART2 Enabled as Asynchronous, We'll go ahead and add DMA Channels:



One for read, one for write and set them both to Normal mode.

Enable global interrupts:



We then go ahead and add FreeRTOS, so we can demo a general application:


And go ahead and USE_NEWLIB_REENTRANT so we can use printf:


And lastly we'll go to project manager and mark the Generate peripheral initialization as pair of '.c/.h' files per peripheral for just to keep our application a bit cleaner:


A known bug (1,2,3,4) in HAL generated projects is that the DMA is not initialized in order, a simple solution will be to duplicate the DMA initialization call to the 'USER CODE BEGIN SysInit' section in main.c so whenever the project is regenerated, the change won't get lost.

1
2
3
/* USER CODE BEGIN SysInit */
  MX_DMA_Init();
/* USER CODE END SysInit */


Once our project is generated, we'll add a circular buffer of choice, in this case I've chosen to use Tilen Majerle's lwrb - Lightweight ring buffer manager.

Next in our usart.c, we'll add 2 semaphores for the tx and rx buffers, 2 aligned buffers for the DMA and 2 buffers for rx and tx, we'll use our "USER  CODE BEGIN 0" for that so we'll keep them when the project is regenerated through STM32CubeMX/IDE. 

Feel free to change the buffer sizes, though for my needs I didn't see a reason to go higher.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/* USER CODE BEGIN 0 */

#include <cmsis_os.h>
#include "lwrb/lwrb.h"

static SemaphoreHandle_t readSemaphore;
static osSemaphoreId writeSemaphore;

#define TX_DMA_BUFFER_SIZE 16
__aligned(32) uint8_t TX_DMA_buffer[TX_DMA_BUFFER_SIZE];

#define RX_DMA_BUFFER_SIZE 16
__aligned(32) uint8_t RX_DMA_buffer[RX_DMA_BUFFER_SIZE];

lwrb_t rx_buffer;
uint8_t rx_buffer_container[255];

lwrb_t tx_buffer;
uint8_t tx_buffer_container[255];

void initialize_buffers(void) {
	osSemaphoreDef(WRITESEM);
	writeSemaphore = osSemaphoreCreate(osSemaphore(WRITESEM), 1);

	vSemaphoreCreateBinary(readSemaphore);
	if (readSemaphore == NULL) {
		Error_Handler();
	}

	if (lwrb_init(&rx_buffer, rx_buffer_container, sizeof(rx_buffer_container)) != 1){
		Error_Handler();
	}
	if (lwrb_init(&tx_buffer, tx_buffer_container, sizeof(tx_buffer_container)) != 1){
		Error_Handler();
	}
}

/* USER CODE END 0 */

Note we included also our buffer initialization routine in the header.

Next we'll add the DMA start in our MX_USART2_UART_Init function in usart.c:

1
2
3
4
  /* USER CODE BEGIN USART2_Init 2 */
  HAL_UARTEx_ReceiveToIdle_DMA(&huart2, RX_DMA_buffer, RX_DMA_BUFFER_SIZE);
  __HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT);
  /* USER CODE END USART2_Init 2 */

Thanks for the tip about DMA_IT_HT from ControllersTech.

Next we'll add our USART tx/rx functions in usart.c. If you're wondering about the xSemaphoreGiveFromISR at line 23, its used to notify the waiting thread about new data rather than continuous polling that will either waste CPU time or cause a delay between received bytes until the thread realizes it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/* USER CODE BEGIN 1 */

static int tx_next_chunk(void) {
	int number_of_items_in_tx_buffer = lwrb_read(&tx_buffer, TX_DMA_buffer, TX_DMA_BUFFER_SIZE);
	if (number_of_items_in_tx_buffer > 0) {
		if (HAL_UART_Transmit_DMA(&huart2, TX_DMA_buffer,
				number_of_items_in_tx_buffer) != HAL_OK) {
			assert(0);
		}
		__HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT);
	}
	return number_of_items_in_tx_buffer;
}

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) {
	if (huart->Instance == USART2) {
		if (lwrb_write(&rx_buffer,  RX_DMA_buffer, Size) != Size ){
			//buffer overrun
		}

		HAL_UARTEx_ReceiveToIdle_DMA(huart, RX_DMA_buffer, RX_DMA_BUFFER_SIZE);
		BaseType_t xHigherPriorityTaskWoken;
		xSemaphoreGiveFromISR(readSemaphore,&xHigherPriorityTaskWoken);
	}
}

int get_rx_data(uint8_t *buffer, size_t buffer_length, uint32_t timeout) {
	xSemaphoreTake(readSemaphore,pdMS_TO_TICKS(timeout ));
	return lwrb_read(&rx_buffer, buffer, buffer_length);
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) {
	if (huart->Instance == USART2) {
		tx_next_chunk();
	}
}

void put_tx_data_with_wait(uint8_t *buffer, size_t buffer_length) {
	int retries = 1000;
	while (retries > 0) {
		int pushed_bytes = put_tx_data(buffer, buffer_length);
		buffer_length -= pushed_bytes;
		buffer += pushed_bytes;
		if (buffer_length <= 0) {
			break;
		}
		osDelay(1);
		retries--;
	}
}

int put_tx_data(uint8_t *buffer, size_t buffer_length) {
	int ret = 0;
	if (osSemaphoreWait(writeSemaphore, osWaitForever) == osOK) {
		ret = lwrb_write(&tx_buffer, buffer, buffer_length);
		osSemaphoreRelease(writeSemaphore);
	}

	if (huart2.gState == HAL_UART_STATE_READY) {
		tx_next_chunk();
	}
	return ret;
}

/* USER CODE END 1 */

And our function prototypes in usart.h:

1
2
3
4
5
/* USER CODE BEGIN Prototypes */
void put_tx_data_with_wait(uint8_t *buffer, size_t buffer_length);
int put_tx_data(uint8_t *buffer, size_t buffer_length);
int get_rx_data(uint8_t *buffer, size_t buffer_length, uint32_t timeout);
/* USER CODE END Prototypes */

And lastly we'll create our echo demo in StartDefaultTask in our freertos.c:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void StartDefaultTask(void const * argument)
{
  /* USER CODE BEGIN StartDefaultTask */
	while (1){
		uint8_t temp_buffer[64];
		size_t read_bytes;
		read_bytes =get_rx_data(temp_buffer, sizeof(temp_buffer), 100);
		put_tx_data_with_wait(temp_buffer,read_bytes);
	}
  /* USER CODE END StartDefaultTask */
}

What the demo does is essentially waiting for up to 64 bytes or 100ms and transmitting back what it got. so this thread is waiting most of the time, the DMA does most of the work and the ring buffer is just there to make sure everything plays together nicely.

The demo project can be found here:

https://github.com/drorgl/usart-boilerplate




Tags: , , ,