Probing with Unity - GaloisInc/betaflight GitHub Wiki

Prelude

In this little tutorial I will attempt to demonstrate how to use Unity for debugging rather for Test Driven Development.

At first, what is Unity? In simple words, it is a framework for unit testing. In more simple words, it has some very useful functions and macros that let you interact with a function of your choice and compare it to an expected output that you have defined. Often, this is called Test Driven Development. As many things in life, you can call it whatever you want. In reality, it is a marely a comparison with a beautiful output and a hardcore name.

For the purposes of this little tutorial, there is no reason to go further in Test Driven Development.

What we will do here is to use a little test to uncover dependencies in Betaflight. In order to do that we will send a probe. This is correct. We will set up a function to probe our target.

Set Up

We can begin by setting up Unity. There is not much to set up honestly. Create a folder in the root directory and call it anything you want. I called mine unit_testing. If you want to be fancy and I assume you are using Linux mkdir <my_testing_folder> . If you are not using Linux, I am not sure what are you doing here.

Anyway, once we have our little folder we will go and plug Unity. You can clone the repo from the link above in to your directory. Be mindful that you have to delete the .git file once you clone it, if you want to push to your own branch any changes later.

Once we have Unity set. We will add a Makefile to compile our test source code,BF source code, and K210 specific files. The Makefile will live in the top directory in the folder we just created, neighboring with the Unity folder we cloned. Be mindful, the Makefile I have here as an example makes couple of assumptions:

  1. You have installed Kendryte's RISCV toolchain in /opt/

  2. You have a target_mcu.mk file named RISCV_K210.mk

  3. You want to build a binary or .elf

  4. You have target name called MAIXBIT and RISCV_K210

  5. You have folders named bin and obj

  6. source.mk in Betaflight

Example of a Makefile for our probe test.

# ---------------------------------- //
# Unit Tests for the K210
#----------------------------------- //
# The target to build, see VALID_TARGETS below
TARGET   	 	:= MAIXBIT
FORKNAME  		:= BETAFLIGHT
REVISION 		:= RISCVK210
# -----------------------------------//
# Compile-time options
OPTIONS    		?=
# compile for OpenPilot BootLoader support
FLASH_SIZE 		?=
ROOT 			= ../
BIN_DIR			= ./bin
SRC_DIR    		= $(ROOT)/src/main
TEST_DIR 		= unit_testing_k210
TEST_SRC		= ./test_src
OBJECT_DIR 		= ./obj
TARGET_DIR 		= $(USER_DIR)/target

vpath %.c ../src/main/drivers
vpath %.c ../lib/main/RISCV_K210/drivers
vpath %.c ../lib/main/RISCV_K210/bsp
vpath %.h ../src/main/drivers
vpath %.h ../lib/main/RISCV_K210/drivers/include
vpath %.h ../lib/main/RISCV_K210/bsp/include
vpath %.ld ../src/link
vpath %.mk ../make
vpath %.c test_src
# ----------------------------------- //
LINKER_DIR		= ../src/link
TARGET_DIR_SRC 	= $(notdir $(wildcard $(TARGET_DIR)/*.c))
VPATH          := $(SRC_DIR):$(SRC_DIR)/startup
#------------------------------------ //
# Search path for sources
SRC        		:= $(shell find $(SRC_DIR) -name '*.c'))
LD_FLAGS        :=
EXTRA_LD_FLAGS  :=
#-------------------------------------//
include $(ROOT)/make/targets.mk
include $(ROOT)/make/system-id.mk
include $(ROOT)/make/targets_list.mk
include $(ROOT)/make/mcu/RISCV_K210.mk
VPATH 			:= $(VPATH):$(ROOT)/make/mcu
VPATH 			:= $(VPATH):$(ROOT)/make
# start specific includes
include $(ROOT)/make/mcu/$(TARGET_MCU).mk
ifndef TOOLS_DIR
TOOLS_DIR 		:= 	$(ROOT)/tools
endif
BUILD_DIR 		:= 	$(ROOT)/build
DL_DIR    		:= 	$(ROOT)/download
# ---------------------------------//
VPATH 			:= 	$(VPATH):$(ROOT)/make/mcu \
					$(VPATH):$(ROOT)/make \
				  	$(VPATH):$(TARGET_DIR) \
					$(SRC_DIR):$(SRC_DIR)/startup \
					$(VPATH):$(USER_DIR):$(TEST_DIR) \
					$(VPATH):$(RISCV_SRC )
# ------------------------------------//
INCLUDE_DIRS    := $(INCLUDE_DIRS) \
                   $(TARGET_DIR) \
                   $(ROOT)/lib \
                   $(ROOT)/src
# -------------------------------//
TARGET_FLASH_SIZE := $(MCU_FLASH_SIZE)
# ------------------------------------//
DEVICE_FLAGS  	:= $(DEVICE_FLAGS) -DTARGET_FLASH_SIZE=$(TARGET_FLASH_SIZE)
# ----------------------------------- //
.DEFAULT_GOAL 	:= binary
# ----------------------------------------- //
TARGET_DIR     = $(ROOT)/src/main/target/$(BASE_TARGET)
TARGET_DIR_SRC = $(notdir $(wildcard $(TARGET_DIR)/*.c))
# ------------------------------------------//
# RISCV Tools
RISCV64_SDK_PREFIX = /opt/kendryte-toolchain/bin/riscv64-unknown-elf-
CROSS_CC    	:= $(RISCV64_SDK_PREFIX)gcc
CROSS_CXX   	:= $(RISCV64_SDK_PREFIX)g++
CROSS_GDB   	:= $(RISCV64_SDK_PREFIX)-gdb
OBJCOPY    		:= $(RISCV64_SDK_PREFIX)objcopy
OBJDUMP     	:= $(RISCV64_SDK_PREFIX)objdump
SIZE        	:= $(RISCV64_SDK_PREFIX)size
# ---------------------------------------- //
TARGET_S19      = $(TARGET_BASENAME).s19
TARGET_BIN      = $(TARGET_BASENAME).bin
TARGET_HEX      = $(TARGET_BASENAME).hex
TARGET_DFU      = $(TARGET_BASENAME).dfu
TARGET_ZIP      = $(TARGET_BASENAME).zip
TARGET_ELF      = $(OBJECT_DIR)/$(FORKNAME)_$(TARGET).elf
TARGET_EXST_ELF = $(OBJECT_DIR)/$(FORKNAME)_$(TARGET)_EXST.elf
TARGET_UNPATCHED_BIN = $(OBJECT_DIR)/$(FORKNAME)_$(TARGET)_UNPATCHED.bin
TARGET_LST      = $(OBJECT_DIR)/$(FORKNAME)_$(TARGET).lst
TARGET_OBJS     = $(addsuffix .o,$(addprefix $(OBJECT_DIR)/$(TARGET)/,$(basename $(SRC))))
TARGET_DEPS     = $(addsuffix .d,$(addprefix $(OBJECT_DIR)/$(TARGET)/,$(basename $(SRC))))
TARGET_MAP      = $(OBJECT_DIR)/$(FORKNAME)_$(TARGET).map
# ------------------------------------------------- //
TARGET_EXST_HASH_SECTION_FILE = $(OBJECT_DIR)/$(TARGET)/exst_hash_section.bin
##--------------------------------------------------------- //
CLEAN_ARTIFACTS := $(TARGET_BIN)
CLEAN_ARTIFACTS += $(TARGET_HEX)
CLEAN_ARTIFACTS += $(TARGET_ELF) $(TARGET_OBJS) $(TARGET_MAP)
CLEAN_ARTIFACTS += $(TARGET_LST)
CLEAN_ARTIFACTS += $(TARGET_DFU)
# --------------------------------------------------//
FC_VER_MAJOR 	:= $(shell grep " FC_VERSION_MAJOR" ../src/main/build/version.h | awk '{print $$3}' )
FC_VER_MINOR 	:= $(shell grep " FC_VERSION_MINOR" ../src/main/build/version.h | awk '{print $$3}' )
FC_VER_PATCH 	:= $(shell grep " FC_VERSION_PATCH" ../src/main/build/version.h | awk '{print $$3}' )
FC_VER := $(FC_VER_MAJOR).$(FC_VER_MINOR).$(FC_VER_PATCH)
## Make sure build date and revision is updated on every incremental build
# ------------------------------------------------ //
$(OBJECT_DIR)/$(TARGET)/build/version.o : $(SRC)
# ------------------------------------------------- //
include $(ROOT)/make/source.mk
include test_src/test_source.mk
vpath %.mk ../make/source.mk
vpath %.c Unity/src
vpath %.h Unity/src
vpath %.c test_src/test_config
vpath %.c test_src/test_build
vpath %.c test_src/test_runners
vpath %.h test_src/test_config
vpath %.h test_src/test_build
VPATH 			:= Unity/src
$(TARGET_LST): $(TARGET_ELF)
	$(V0) $(OBJDUMP) -S --disassemble $< > $@

$(TARGET_BIN): $(TARGET_ELF)
	@echo "Creating BIN $(TARGET_BIN)" "$(STDOUT)"
	$(V1) $(OBJCOPY) -O binary $< $@

$(TARGET_ELF): $(TARGET_OBJS) $(LD_SCRIPT)
	@echo "Linking $(TARGET)  " "$(STDOUT)"
	$(V1) $(CROSS_CC) -o $@ $(filter-out %.ld,$^) $(LD_FLAGS)

# Compile
## compile_file takes two arguments: (1) optimisation description string and (2) optimisation compiler flag
define compile_file
	echo "%% ($(1)) $<" "$(STDOUT)" && \
	$(CROSS_CC) -c -o $@ $(CFLAGS) $(2) $<
endef
# --------------------------------------------------------------- //
ifeq ($(DEBUG),GDB)
$(OBJECT_DIR)/$(TARGET)/%.o: %.c
	$(V1) mkdir -p $(dir $@)
	$(V1) $(if $(findstring $<,$(NOT_OPTIMISED_SRC)), \
		$(call compile_file,not optimised, $(CC_NO_OPTIMISATION)) \
	, \
		$(call compile_file,debug,$(CC_DEBUG_OPTIMISATION)) \
	)
else
$(OBJECT_DIR)/$(TARGET)/%.o: %.c
	$(V1) mkdir -p $(dir $@)
	$(V1) $(if $(findstring $<,$(NOT_OPTIMISED_SRC)), \
		$(call compile_file,not optimised,$(CC_NO_OPTIMISATION)) \
	, \
		$(if $(findstring $(subst ./src/main/,,$<),$(SPEED_OPTIMISED_SRC)), \
			$(call compile_file,speed optimised,$(CC_SPEED_OPTIMISATION)) \
		, \
			$(if $(findstring $(subst ./src/main/,,$<),$(SIZE_OPTIMISED_SRC)), \
				$(call compile_file,size optimised,$(CC_SIZE_OPTIMISATION)) \
			, \
				$(call compile_file,optimised,$(CC_DEFAULT_OPTIMISATION)) \
			) \
		) \
	)
endif
# --------------------------------------------------//
# Assemble
$(OBJECT_DIR)/$(TARGET)/%.o: %.s
	$(V1) mkdir -p $(dir $@)
	@echo "%% $(notdir $<)" "$(STDOUT)"
	$(V1) $(CROSS_CC) -c -o $@ $(ASFLAGS) $<

$(OBJECT_DIR)/$(TARGET)/%.o: %.S
	$(V1) mkdir -p $(dir $@)
	@echo "%% $(notdir $<)" "$(STDOUT)"
	$(V1) $(CROSS_CC) -c -o $@ $(ASFLAGS) $<

TARGETS_CLEAN = $(addsuffix _clean,$(VALID_TARGETS))
# Make the binary ------------------------ #


binary:
	$(V0) $(MAKE) -j8 $(TARGET_BIN)

## clean             : clean up temporary / machine-generated files
clean:
	@echo "Cleaning $(TARGET_BASENAME)"
	$(V0) rm -f $(CLEAN_ARTIFACTS)
	$(V0) rm -rf $(OBJECT_DIR)/$(TARGET)
	@echo "Cleaning $(TARGET) succeeded."

## test_clean        : clean up temporary / machine-generated files (tests)
test-%_clean:
	$(MAKE) test_clean

test_clean:
	$(V0) $(MAKE) clean || true

$(TARGETS_CLEAN):
	$(V0) $(MAKE) -j8 TARGET=$(subst _clean,,$@) clean

## clean_all         : clean all valid targets
clean_all: $(TARGETS_CLEAN) test_clean

# mkdirs
$(DL_DIR):
	mkdir -p $@

$(TOOLS_DIR):
	mkdir -p $@

$(BUILD_DIR):
	mkdir -p $@

version:
	@echo $(FC_VER)

target-mcu:
	@echo $(TARGET_MCU)

# rebuild everything when makefile changes
$(TARGET_OBJS): Makefile $(TARGET_DIR)/target.mk $(wildcard make/*)

# include auto-generated dependencies
-include $(TARGET_DEPS)
# -----------------------------------------//

Once we have the Makefile ready we need to create a folder to put our first source file for our little test.

We will create a folder and name it again anything we want. I have mine as test_eeprom. So lets go ahead and `mkdir test_eeprom'. In the folder we will create our first .c file with our first test. I called mine test_config_eeprom.c. In that file we will put :

  1. Includes

  2. Two functions - setup and teardown,and a couple of Unity's macros. They are part of Unity's framework.

  3. Our first test function

  4. Main

  5. A function probe that we will place in the source file under test e.g config_eeprom.c

Optional: A cup of coffee at the end of this

Here is an example :

* Unittests for configs
 * Author :nikolay nikolov
 */

#define UNITY_SUPPORT_64

#include <setjmp.h>
#include <stdio.h>
#include "../../../src/main/config/config_eeprom.h"
#include "../../Unity/src/unity.h"
#include "../../Unity/src/unity_internals.h"


void setUp(void) { }
void tearDown(void) { }
//------------------
void test_probe(void)
{
	TEST_ASSERT_EQUAL(0,probe(0));
}

int main(void)
{
	UNITY_BEGIN();
	RUN_TEST(test_probe);
	return (UNITY_END());
}


We stated some assumptions above. We assumed there is a source.mk in Betaflight and our target board is defined as RISCV_K210 in the Betaflight's structure.

In order to focus only on one function we will have to ensure that we build only what we need. Hence, at the top of the source.mk (located in the make folder) we can put the below:

##---------------------------------------------------------------------+
## RISCV Source Files and drivers --------------------------------------+
## ---------------------------------------------------------------------+
ifneq ($(TARGET),$(filter $(TARGET),$(RISCV_K210)))
SRC += \
		 		$(STDPERIPH_DIR) \
#		 		$(MCU_COMMON_SRC)
endif

This is all good but then you will ask, how do we build our test source files and the rest of the files we have under test (such as the config_eeprom.c). I see you have been following along.

For that reason we will create another Makefile that lives in the src folder where we placed our source file. We can call it test_source.mk.

In that Makefile we can add something like the below :


# ----------------------------------------------------------+
## RISCV Test Drivers  --------------------------------------+
## ----------------------------------------------------------+

SRC += \
		test_config/test_config_eeprom.c \
		../Unity/src/unity.c \
		../../src/main/config/config_eeprom.c

Now we should be all set. Let's double check.

  1. Makefile for our little testing environment - check

  2. Testing Framework - check

  3. Makefile to get all the sources we need for testing - check

  4. Source file to add our test

  5. Correct includes in the .c file

Now we need to add our probe function. I created just a dummy function like this in .c and a prototype in .h :

int probe(){return 0;}

This is right. An empty function that returns zero.

What is this good for ? Well, we are trying to examine dependencies, therefore we want our test to pass but at the same time we would like to get some useful warnings and errors that reveal the dependencies.

After I compiled I got this:


In file included from test_src/../../src/main/config/config_eeprom.c:24:
..//src/main/config/config.h:59:33: error: 'TARGET_BOARD_IDENTIFIER' undeclared here (not in a function)
     char boardIdentifier[sizeof(TARGET_BOARD_IDENTIFIER) + 1];
                                 ^~~~~~~~~~~~~~~~~~~~~~~
In file included from ..//src/main/drivers/io_def.h:54,
                 from ..//src/main/drivers/io.h:126,
                 from ..//src/main/drivers/flash.h:26,
                 from test_src/../../src/main/config/config_eeprom.c:34:
..//src/main/drivers/io_def_generated.h:1319:4: warning: #warning "No pins are defined. Maybe you forgot to define TARGET_IO_PORTx in target.h" [-Wcpp]
 #  warning "No pins are defined. Maybe you forgot to define TARGET_IO_PORTx in target.h"
    ^~~~~~~
In file included from test_src/../../src/main/config/config_eeprom.c:35:
..//src/main/drivers/system.h:84:34: error: unknown type name 'IRQn_Type'
 void registerExtiCallbackHandler(IRQn_Type irqn, extiCallbackHandlerFunc *fn);void unregisterExtiCallbackHandler(IRQn_Type irqn, extiCallbackHandlerFunc *fn);
                                  ^~~~~~~~~
..//src/main/drivers/system.h:84:114: error: unknown type name 'IRQn_Type'
 void registerExtiCallbackHandler(IRQn_Type irqn, extiCallbackHandlerFunc *fn);void unregisterExtiCallbackHandler(IRQn_Type irqn, extiCallbackHandlerFunc *fn);
                                                                                                                  ^~~~~~~~~
make[1]: *** [Makefile:152: obj/MAIXBIT/../../src/main/config/config_eeprom.o] Error 1
make[1]: *** Waiting for unfinished jobs....
make[1]: Leaving directory '/home/picard/Dev/betaflight/unit_testing_k210'
make: *** [/home/picard/Dev/betaflight/unit_testing_k210/Makefile:184: binary] Error 2

Why is this useful? Compared to before, now only one file has been build and we can identify the dependencies only for this file. What is more, with our probe function we can probe around and test the waters. We can print and test. But this is for a later day.

Cheers.

⚠️ **GitHub.com Fallback** ⚠️