300x250 AD TOP

Search This Blog

Pages

Featured Post

Using the ILI9488

The ILITEK ILI9488 is one of the larger and cheaper SPI displays available to the maker community,, available in 3.5" and 4". How...

Paling Dilihat

Powered by Blogger.

Feature Label Area

Thursday, July 28, 2022

Using the ILI9488

The ILITEK ILI9488 is one of the larger and cheaper SPI displays available to the maker community,, available in 3.5" and 4". However, there are a few workable issues that prevent this display from being great.


Specifications

What's called ILI9488 is actually the LCD controller with an optional touch panel, you can mostly find it with XPT2046 resistive touch controller.

ILI9488 (datasheet):
- 3/4 wire SPI, software configurable
- 480x320 Pixels
- 3 modes supported: 16bit (65k colors) / 18bit (262k colors) / 24bit (16.7m colors)

XPT2046 (datasheet):
- 12bit 125khz resistive touch panel
- pressure sensitive
- temperature sensor
- 4-wire SPI
- Supports touch interrupt

5v to 3.3v regulator, please note that you should short J1 if you're using 3.3v




General Issues


The ILI9488 can be bought in two versions, one with a diode and one without, I've yet to determine the functionality of the diode, but it seems that others think the diode can prevent the display from releasing the MISO line, unfortunately I didn't keep the diode so I can't validate this claim.

The schematics are available if you want to explore it further.



LVGL Issues

When I first started working with my ILI9488 the colors were a bit off but I attributed it to cheap and low quality display which was probably defective. but then I've started wondering if its possible to fix since the controller can be configured with other voltages it pushes to the panel. 

Original Configuration (16 bit)

Once I've discovered the setting, I've changed the brightness and changed how RGB565 is parsed into the format ILI9488 expects. note its not the most optimized way, but the modification was good enough for my tests.

After the modifications


I wanted to see if the display will work at 80Mhz, unfortunately its not, but seeing some of the graphics I guess that it can be fixed with a logic analyzer and some patience.

80Mhz



ESP32 Specific Issues

While it might not be specifically ESP32 issues, its issues that you might encounter while integrating it with ESP32. The most prominent issue is the way CS works in ESP32, it seems that CS issues are common in the embedded world, the STM32 has a similar issue with NSS not properly controlled by the cube's code.

To support multiple transactions with multiple devices on the same SPI bus, the ESP32 switches off the CS signal between transactions which is great, however, the way ILI9488 works is that if you switch off CS after you've sent a read request, it switches from 4-wire SPI to 3-wire SPI. 

There are a few ways to solve this issue:
1. Use software CS, set to low before a transaction and set to high after you're done receiving.
2. Use the new SPI_TRANS_CS_KEEP_ACTIVE flag for transactions.

But wait, what's the problem with working half duplex (3-wire)? 
Well, if you share the same SPI bus with the touch panel it locks the MISO line on LOW and won't allow the touch panel from transmitting touch data.

Solutions?

Well, I've given up on getting data from the display and for most uses its good enough, so you can disconnect the MISO line from the display and keep it working for the touch panel.

Another possibility is to put a 1k resistor in series to the display MISO. This way the panel won't lock the touch and you can keep the buggy code until you can get it fixed to your satisfaction.

Unfortunately working in half-duplex is not currently possible if you're using the LVGL driver since it will attempt to set the bus to 4-wire mode for the touch panel to work.



Tags: , , ,

Sunday, July 24, 2022

LVGL ESP32 and Desktop Development Walkthrough

UI for Embedded is always a hassle, find the right MCU, find the right Display, connect the right wires and that's even before writing the first line of code that actually shows anything on the display, drivers, graphic libraries and input libraries can be a pain to use, not to mention a pain to write.

Fortunately, we no longer live in a cave, we have PlatformIO, LVGL and drivers for many of the LCDs available for commercial use and maker community.

I propose a way to start quickly so we can save some of the bring up time for a new setup, configuration is done in the kconfig way, so its really go through the menu, change a setting, compile and test.

There is a getting started for ESP32 on LVGL github, Gabor Kiss-Vamosi wrote a tutorial and we have some documentation, Espressif event got an example repository, but I've discovered my way is a bit more flexible and easier to use once its set-up, since you can develop on the desktop (LVGL refers to it as Simulator) and don't have to upload each revision, plus you don't have to do it, just fork my project and you're good to go. In any case, the full instructions are below, so you can mix and match versions and for me it provided a pretty much consistent experience.

Hardware

There are many kits you can buy, each one with its own quirks but my goal was to test what I had and unfortunately I had none of the kits.

So I got out a perfboard, a few pin headers and a resistor (more on that later) and built my own. 



Please note that I have yet to test any of them, so do your own research before purchasing, the following are affiliate links.

ESP32-LCDKit

The ESP32-LCDKit looks like its the most versatile for Graphic development, the schematic is available and you can use which ever ESP32 you want (as long as its a 38 pins)

ESP-WROVER-KIT

Next in line is the ESP-WROVER-KIT, like the previous one, its schematics are also available, but one major drawback is that the kit does not include a touch screen, only a display so its less suitable for interactive UI.


ESP32-S2-Kaluga-1 Kit


Next in line from Espressif is the ESP32-S2-Kaluga-1 Kit, this kit is more versatile and according to the documentation it does have a 3.2" touch screen but it can also have an audio extension, a touch panel (not display, just a board with touch) and a camera module.



WT32-SC01


Last but not least, The WT32-SC01 seems like it has great potential for having all the hardware on a single board, mounting holes and capacitive touch screen, schematics and code samples are available.

Source Control

We can't really start a project without source control, how can we track changes? how can we go back to a stable state?

Atlassian has a great cheat sheet and there is built in support in Visual Studio Code. 

Lets initialize a new git repo:

git init

I also recommend committing each stage of your setup, it will help you to track changes and find out which code caused the change.

PlatformIO

PlatformIO is a development platform that enables writing code in multiple platforms while maintaining a consistent experience.

In this case, we'd like to initialize a new project:

pio init

LVGL

Now that we have an empty project, we'll need to add lvgl to it, so go ahead and extract latest release from https://github.com/lvgl/lvgl/releases into lib/lvgl.

Than take library.json from our example project and copy it into lib/lvgl root, what this library.json file actually does is allow you to select which parts of lvgl gets compiled.

Native / Desktop Drivers

There are multiple ways of working with LVGL but one of the better ways is getting your UI completely disconnected from your business logic and running the UI on your PC, this way its easier to design, debug and verify, on top of it, you can use LVGL's snapshot API to automatically validate your views so they can stay consistent no matter which changes you do. 

The way LVGL works on the desktop is by using SDL2.

So first we'll extract the latest source from https://github.com/lvgl/lv_drivers into lib/lv_drivers

Then we'll copy lv_drv_conf_template.h to include/native/lv_drv_conf.h and enable the file (change #if 0 to 1)

Then we'll modify library.json to include SDL dependency, otherwise PlatformIO dependency detection won't work properly due to the way SDL is included through a #DEFINE macro.

    "dependencies":[

        {

            "name":"SDL2"

        }

    ],

And modify library.json to remove an incompatible source file:

    "build": {

        "srcFilter" : [

            "-<display/ILI9341.c>"

        ]

    }

Lastly, we'll update include/native/lv_drv_conf.h in appropriate place and copy our menu configuration keys to SDL configuration keys, otherwise our menu won't control SDL (Desktop) display properly.

    #include "lvgl_native_drivers.h"

    #define USE_SDL 1

    #define SDL_HOR_RES     CONFIG_SDL_HOR_RES

    #define SDL_VER_RES     CONFIG_SDL_VER_RES

Since our native environment will need to use SDL, we should also add SDL2 to lib, there are different libraries for different environments, such as Windows and Linux.
In Windows, we'll need to download SDL for mingw, extract it into our lib folder and copy library.json from this project to your SDL library

In Ubuntu its as simple as
apt-get install libsdl2-2.0 libsdl2-dev

Lastly we need to add our native drivers configuration script run_lvgl_native_drivers_kconfig.py and configure it with custom_lvgl_native_drivers_kconfig_save_settings and custom_lvgl_native_drivers_kconfig_output_header configuration keys

ESP32 Hardware Display Drivers

Now that we have our desktop setup, we also want our hardware setup so we can flash our device and see how our design looks on the real hardware.

Please note that the drivers are not always configured ideally, if the colors seems a bit off, you should read the datasheet and make sure everything is configured properly.

Lets start by extracting the latest source from https://github.com/lvgl/lvgl_esp32_drivers into lib/lvgl_esp32_drivers and copy the library.json from this project

Some drivers are not working properly with PlatformIO's scons configuration and needs to be enabled/disabled on a per-file basis, you should look in library.json as an example.

Another thing we want to tell our library.json is which framework it should work with, for example, in our setup we have native and esp32 environments and the esp32 drivers should not be compiled on the native environment since none of Espressif's libraries exist or even needed for desktops.

Then we'll modify lvgl_helper.c to include "lv_conf.h" right after "sdkconfig.h", the vanilla setup assumes your lvgl is part of esp32 components which can make desktop configuration a problem.

    #include "sdkconfig.h"

    #include "lv_conf.h"

Then we need to add lvgl kconfig script (run_lvgl_kconfig.py) and set  its configuration  custom_lvgl_kconfig_save_settings, custom_lvgl_kconfig_output_headercustom_lvgl_kconfig_include_headers configuration sections to each relevant environment in platformio.ini

And add lvgl esp32 drivers kconfig script (run_lvgl_esp32_drivers_kconfig.py) and custom_lvgl_esp32_drivers_kconfig_save_settings, custom_lvgl_esp32_drivers_kconfig_output_header configuration section to each relevant environment in platformio.ini

These two scripts and their setting enables platformio.ini to use the target scripts for easy configuration. To see which scripts are installed for each environment:

> pio run --list-targets
Environment    Group     Name                        Title                        Description
-------------  --------  --------------------------  ---------------------------  -----------------------------------
native         Custom    lvgl-config                 lvgl-config                  Executes lvgl config
native         Custom    lvgl-esp32-drivers-config   lvgl-esp32-drivers-config    Executes lvgl esp32 drivers config
native         Custom    lvgl-native-drivers-config  lvgl-native-drivers-config   Executes lvgl native drivers config

esp32          Custom    lvgl-config                 lvgl-config                  Executes lvgl config
esp32          Custom    lvgl-esp32-drivers-config   lvgl-esp32-drivers-config    Executes lvgl esp32 drivers config
esp32          Custom    lvgl-native-drivers-config  lvgl-native-drivers-config   Executes lvgl native drivers config
esp32          Platform  buildfs                     Build Filesystem Image
esp32          Platform  erase                       Erase Flash
esp32          Platform  menuconfig                  Run Menuconfig
esp32          Platform  size                        Program Size                 Calculate program size
esp32          Platform  upload                      Upload
esp32          Platform  uploadfs                    Upload Filesystem Image
esp32          Platform  uploadfsota                 Upload Filesystem Image OTA

Now that we've assigned each configuration output to a different folder under include, we should tell platformio to include headers from these folders so each environment will get a different set of configuration files.

LVGL Configuration

LVGL uses configuration files separate from the driver configuration files to configure some aspects of it, such as fonts, widgets, colors and layouts, but we need to tell it where to take the configuration from.

We do it by adding these flags to build_flags for relevant environments in platformio.ini:

    -DLV_LVGL_H_INCLUDE_SIMPLE

    -DLV_CONF_INCLUDE_SIMPLE

    -DLV_CONF_PATH=lv_conf.h

Environment Hardware Abstraction

There is a small library in the demo project called lvgl_hal, it contains the setup for the drivers, obviously different from native to ESP32, you may need to modify it to your environment / programming.

So copy lvgl_hal to your lib folder.

Changing Configuration

Lets start with ESP32 configuration, LVGL can run lean or resource intensive, larger buffers may help with rendering speed, caching images and data can also help, my usual setup is 240Mhz CPU speed and 80Mhz PSRAM speed. 

To make these configuration, you'll need to modify ESP32 configuration by running:

pio run -e esp32 -t menuconfig

We can configure ESP32 drivers:

pio run -e esp32 -t lvgl-esp32-drivers-config




And native drivers, which at the time of this writing is only resolution:

pio run -e native -t lvgl-native-drivers-config



And lastly we'll want to configure our lvgl, using the same configuration for both desktop and embedded can help you to find bugs quicker.

pio run -e esp32 -t lvgl-config

pio run -e native -t lvgl-config




Runner

The runner library is intended as an abstraction of the main function, on a desktop its int main(argc,argv), on ESP32 its appmain() and on Arduino its setup() and loop(), instead of writing the same ifdefs everywhere, just copy the runner library, include it in your main file and use it:

MAIN(){

}


Unfortunately my ILI9488 is not configured properly (more on that later)



If you're looking for a solution to the ILI9488 configuration issue, you can read about it here.



Tags: , , , ,

Tuesday, July 12, 2022

RISC-V Linux on ESP32

I've been playing with the idea of running linux on ESP32 since the first days I've met its more robust module, the WROVER-B, on paper it seem possible since its a dual core 240Mhz and has 16MB flash and 8MB RAM, compared to our antique machines that could run linux, it seems like a beast.

Doing some research on it, I've understood that its MMU is insufficient for running Linux on it. during the past few years I've been looking into it to see if anyone else found the time to implement it and eventually I've decided its going to be a good opportunity to learn a bit more about RISCV and Buildroot. Two subjects I've been putting off for longer than I'd like to admit.

I've decided to start with something rather to write it all from scratch, which I didn't have time or energy to do for this project, I've looked into QEMU emulation for RISCV but taking this project apart and getting only a few components out of it to run on an embedded system seemed like too much work. Eventually I've found out about Fabrice Bellard's TinyEMU (demo).



RISC-V

RISC-V is the new ISA kid in the block, well, not really a kid and not really new, but it becomes more and more popular, Espressif got out the ESP32-C3 at 2020.

Previous Successes

Max Filippov patched the kernel to support ESP32 back at 2019, I'm pretty sure it runs a lot faster since its not an emulation.

Li XiongHui wrote the juiceVM which implemented RISCV ISA and runs on ESP32, he wrote about it on whycan and reddit and has video of it booting on YouTube, the video does state x30 speedup, which means the system booted in about 6 hours. Li never released the source code so the only improvements that can be done is by him and judging from my own life, you never have enough time for these things.

TinyEMU

"TinyEMU is a system emulator for the RISC-V and x86 architectures. Its purpose is to be small and simple while being complete."

Looking at its source code, seemed like the project went from mission impossible (with my current resources) to mission possible. Oh the naiveté.

ESP32

The ESP32 is a dual core 240Mhz MCU, it was released at September 2016 and its still one of the best value for money MCU you can get, one of its versions has 8MB or RAM and 16MB of FLASH. That amount of RAM on any of its competitors takes more than "I want it" to get it working, at the time it came out especially so. Espressif did an amazing job with esp-idf and one of their best features is listening to their customers and with the help of the maker community they've built an amazing framework.

TinyEMU on ESP32

Will it even compile?

Apparently yes, making it compile was very easy, some tweaks here and there and a missing standard library and it was compiled perfectly. 

But what can I do about memory? the ESP32 only has 8MB and half of it is not even accessible as a standard but rather bank switched with its own APIs (himem).

My thinking at the time was that it doesn't matter so much since the kernel will probably not need so much memory once it starts, for example, if I don't access files, the memory holding the file system and the file system functions will be left alone other than a periodic flush.

So something like a swap file will probably be good enough for this experiment, right?

Not so fast.

First, I had to find out that the standard way of accessing the SD card is not fast enough at around 150k per second. So I went to a journey to find all the bottlenecks.

Then I've discovered that 3MB for the virtual pages is too slow due to the PSRAM, using 80Mhz only improved a bit, so I went on a journey to find the fastest search trees, I've tried splay tree and eventually rested on AVL tree which was good enough.

But I could squeeze more out of the ESP32, I've implemented a rudimentary direct-mapped-cache so most memory accesses won't even search for their page (Professor Luis Ceze has a great presentation on the subject).

I was still not happy enough, my SD card is slow and no matter what I did, it slowed things down. I've measured how many page faults I had and decided that dirty pages should only be written once they are abandoned, so my LRU cache pushed dirty pages into himem and only when these himem pages reclaimed they got pushed to the page file.

At that point I was content enough, my kernel booted in 1:35 minutes. 


Buildroot

"Buildroot is a simple, efficient and easy-to-use tool to generate embedded Linux systems through cross-compilation."

I've always wanted to learn how the big guys do it, how embedded cameras, kiosks and perhaps even satellites gets their OS build without all the bloat and package managers.

So once I got my basic emulator working (without all the optimizations), it was time to start learning buildroot. 

Apparently its simple, you just download the archive, work through a few menus, read some documentation, modify the rootfs with overlays and you're done. issue a make command and you have your kernel and your rootfs.

In the old days (or so I've heard), you've had to build RISC-V toolchain and patch the kernel to get things going, These days buildroot comes with a precompiled toolchain from bootlin, so the whole experience was fun and easy to learn.

Thoughts for the future

Using the emulator to run embedded RISC-V code, implement SiFive GPIO and provide more flexible VM for running code on multiple embedded platforms. Communicating over the console is implemented in TinyEMU patches, or see here or through virtio.

Closing

This was a wonderful journey of learning, I've learned more than I wanted about:

You can find the fruits of this labor at:

Tags: , , , , , ,

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.

Relevant github repo: https://github.com/drorgl/esp32-sd-card-benchmarks


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: , , , , , , , , , ,