Сб 29 марта 2014
By Oleg Mazko
In Embedded .
tags: TDD CMock Unity
Тема профессиональной разработки ПО для микроконтроллеров с использованием современных методик уже поднималась . Собственно если вкратце, то было предложено максимально разделять исходный код на отдельные модули и тестировать их логику на ПК независимо от железа. Такой наивный подход способен значительно упростить жизнь разработчику, но в то же время имеет серьёзные недостатки особенно на стыке софта перефирии:
увеличивается объём кода за счёт дополнительных функций или #ifdef
на стыке с периферией (всякие UART , I2C , порты ввода-вывода и т.д.). Например логику по установке битов портов можно завернуть в функцию set_flag()
и при написании тестов задействовать соотв. ей mock - однако это увеличит общий объём кода
на уровне периферии код часто так и остаётся недопокрытым тестами. К примеру периферийные модули имеют свойство по характерным событиям выбрасывать флаги, наполнять регистры данными - изобретать велосипеды и придумывать фейковые периферийные модули ? Как минимум нужно прекрасно понимать как эта периферия работает, плюс асинхронность
ну а о тестировании фич специфичных для архитектуры микроконтроллера типа MIPS16 вообще и говорить не приходится
Итак далее будет рассмотрен пример с использованием микроконтроллеров Microchip , эмулятора и TDD фреймворка Ceedling . Эмулятор микроконтроллера запускается на ПК и соответственно использует его вычислительные ресурсы, а Ceedling это не более чем средство автоматизации, написано на Ruby плюс всё что может понадобится для покрытия тестами кода на C (Unity , CMock и даже CException ).
#apt-get install ruby
~$ gem install ceedling
~$ ceedling new PIC_Demo
~$ cd PIC_Demo/
~$ ls
build project.yml rakefile.rb src test vendor
Ещё нам понадобится C компилятор для микроконтроллеров PIC и среда разработки MPLAB . Всё это можно бесплатно загрузить с официального сайта Microchip и после установки этого добра должно получиться примерно следующее:
~$ which xc16-gcc
/opt/microchip/xc16/v1.21/bin/xc16-gcc
~$ which mplab_ide
/usr/bin/mplab_ide
Никак не обойтись нам без Hello World :
src/hello.c
#include <stdio.h> /* Required for printf */
int main ( void )
{
printf ( "Hello, world!" );
return 0 ;
}
Компиляция + линковка:
~$ xc16-gcc -omf= elf -mcpu= 24EP64MC206 src/hello.c \
-o build/hello.elf -Wl,-Tp24EP64MC206.gld
~$ file build/hello.elf
build/hello.elf: ELF 32 -bit LSB executable, \
version 1 ( SYSV) , statically linked, not stripped
Эмуляция. Самый простой случай - использовать sim30 , который поставляется вместе с С компилятором xc16-gcc :
~$ echo "
LD pic24epsuper
LC build/hello.elf
IO NULL /tmp/ $$ .txt
RP
E
Q
" | sim30
~$ cat /tmp/$$ .txt
Hello, world!
Прелестно ! Важно отметить что при эмуляции в sim30 есть ограничения и можно указывать только семейства чипов, а не какой-либо конкретно по отдельности:
~$ echo DH | sim30 | grep LD
LD <devicename> -Load Device: \
dspic30super dspic33epsuper pic24epsuper pic24fpsuper pic24super
Как результат в процессе эмуляции поддерживается не вся имеющаяся на борту микроконтроллера периферия и об этом мы ещё поговорим чуть позже, ну а сейчас самое время начать получать удовольствие от юнит тестов с использованием Ceedling . В Hello World всего одна функция main. Не секрет, что в C программе функция с таким именем может быть только одна, а поскольку в Ceedling она уже явно имеется, для тестов hello.c тут нужны #ifdef
- но это частный случай, исключение из правил так сказать и пугаться не стоит:
src/hello.h
#ifndef hello_H
#define hello_H
#ifdef TEST
int test_main ( void );
#endif
#endif // hello_H
src/hello.c
#include <stdio.h> /* Required for printf */
#include "hello.h"
#ifdef TEST
int test_main ( void )
#else
int main ( void )
#endif
{
printf ( "Hello, world!" );
return 0 ;
}
test/test_main.c
#include "unity.h"
#include "hello.h"
void setUp ( void ){
}
void tearDown ( void ){
}
void test_main_function_should_always_return_0 ( void ){
TEST_ASSERT_EQUAL ( 0 , test_main ());
}
По умолчанию Ceedling заточен под gcc , поэтому нужно допилить project.yml , указав компилятор, линковщик в секции :tools:
:
project.yml
:tools:
:test_compiler:
:executable: xc16-gcc
:arguments:
- -mcpu=24EP64MC206
- -x c
- -c
- "${1}"
- -o "${2}"
- -D$: COLLECTION_DEFINES_TEST_AND_VENDOR
- -I"$": COLLECTION_PATHS_TEST_SUPPORT_SOURCE_INCLUDE_VENDOR
- -Wall
- -Wextra
- -mlarge-code
- -mlarge-arrays
- -mlarge-data
:test_linker:
:executable: xc16-gcc
:arguments:
- -mcpu=24EP64MC206
- -omf=elf
- ${1}
- -o "./build/TestBuild.out"
- -Wl,-Tp24EP64MC206.gld
Запуск всех тестов выглядит так:
# для просмотра всех возможностей Ceedling
# rake -T
~$ rake test:all
# build/test/out/cmock.o: Link Error: \
# Could not allocate section .bss, size = 32774 bytes, attributes = bss
# Link Error: Could not allocate data memory
Линковщик ругается - микроконтроллеру не хватает памяти для работы с cmock , поэтому уменьшим аппетиты этого замечательного инструмента в секции :defines:
:
project.yml
:commmon: &common_defines
- UNITY_INT_WIDTH=16
- CMOCK_MEM_INDEX_TYPE=uint16_t
- CMOCK_MEM_PTR_AS_INT=uint16_t
- CMOCK_MEM_ALIGN=1
- CMOCK_MEM_SIZE=4096
Пробуем:
~$ rake test:all
# ERROR: Test executable "test_main.out" failed.
# > Produced no final test result counts in $stdout:
# sh: 1: build/test/out/test_main.out: not found
# > And exited with status: [127] (count of failed tests).
# > This is often a symptom of a bad memory access in source or test code.
Тут Ceedling наивно пытается выполнить собранную программу test_main.out , но не знает как, зато мы знаем что это делается с помощью sim30 :
test/simulation/sim30_instruction.txt
LD pic24epsuper
LC ./build/TestBuild.out
IO NULL ./test/simulation/out.txt
RP
E
quit
test/simulation/sim_test_fixture.rb
OUT_FILE = "test/simulation/out.txt"
File . delete OUT_FILE if File . exists? OUT_FILE
pipe = IO . popen ( "sim30 ./test/simulation/sim30_instruction.txt" )
trap ( "INT" ) { Process . kill ( "KILL" , pipe . pid ); exit }
Process . wait ( pipe . pid )
if File . exists? OUT_FILE
file_contents = File . read OUT_FILE
print file_contents
else
print "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! \n " \
"! Program was not simulated ? ! \n " \
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
end
Рассказать Ceedling в секции :tools:
как всем этим управлять:
project.yml
:test_fixture:
:executable: ruby
:name: "Microchip simulator test fixture"
:stderr_redirect: :win
:arguments:
- test/simulation/sim_test_fixture.rb
Запуск... Вуаля !
~$ rake test:all
# ----------------------
# UNIT TEST OTHER OUTPUT
# ----------------------
# [test_main.c]
# - "Hello, world!"
# -------------------------
# OVERALL UNIT TEST SUMMARY
# -------------------------
# TESTED: 1
# PASSED: 1
# FAILED: 0
# IGNORED: 0
Вот ! Теперь проект будет жить и развиваться вместе с тестами. Предлагаю помигать светодиодом и mockнуть результат сего действа:
src/gpio_led.h
#ifndef gpio_led_H
#define gpio_led_H
void gpio_led_init ( void );
void gpio_led_set ( int brightness );
void gpio_led_clear ();
#endif // gpio_H
src/system.h
#ifndef system_H
#define system_H
#include <stdbool.h>
bool system_should_abort_app ();
#endif // system_H
src/hello.c
#include "hello.h"
#include "system.h"
#include "gpio_led.h"
#ifdef TEST
int test_main ( void )
#else
int main ( void )
#endif
{
gpio_led_init ();
while ( ! system_should_abort_app ()) {
gpio_led_set ( 11 );
gpio_led_clear ();
}
return 0 ;
}
test/test_main.c
#include "unity.h"
#include "hello.h"
#include "mock_system.h"
#include "mock_gpio_led.h"
void setUp ( void ){
gpio_led_init_Expect ();
}
void tearDown ( void ){
}
void test_main_function_without_loop ( void ){
system_should_abort_app_ExpectAndReturn ( true );
TEST_ASSERT_EQUAL ( 0 , test_main ());
}
void test_main_function_loop_one_iteration ( void ){
system_should_abort_app_ExpectAndReturn ( false );
gpio_led_set_Expect ( 11 );
gpio_led_clear_Expect ();
system_should_abort_app_ExpectAndReturn ( true );
TEST_ASSERT_EQUAL ( 0 , test_main ());
}
Пробуем:
~$ rake test:all
# -------------------------
# OVERALL UNIT TEST SUMMARY
# -------------------------
# TESTED: 2
# PASSED: 2
# FAILED: 0
# IGNORED: 0
Как несложно догадаться из test/test_main.c в пределах одного юнит теста Ceedling линкует все файлы, указанные в его #include
, причём если присутствует префикс mock
, вместо оригинальной *.c имплементации Ceedling автоматически генерирует mock согласно интерфейсу, прописанному в соответствующем *.h .
Чуть ближе к железу на уровень портов ввода-вывода:
src/gpio_led.c
#include "gpio_led.h"
#include <xc.h>
void gpio_led_init () {
TRISAbits . TRISA0 = 0 ;
}
void gpio_led_set ( int brightness ) {
/* TODO: brightness :) */
LATAbits . LATA0 = 1 ;
}
void gpio_led_clear () {
LATAbits . LATA0 = 0 ;
}
test/test_gpio_led.c
#include "unity.h"
#include "gpio_led.h"
#include <xc.h>
#include <string.h>
void setUp ( void ){
gpio_led_init ();
LATABITS clean = { 0 };
memcpy (( void * ) & LATAbits , ( void * ) & clean , sizeof clean );
}
void tearDown ( void ){
}
void test_gpio_led_set ( void ){
TEST_ASSERT_EQUAL ( 0 , LATAbits . LATA0 );
gpio_led_set ( 11 );
TEST_ASSERT_EQUAL ( 1 , LATAbits . LATA0 );
}
void test_gpio_led_clear ( void ){
test_gpio_led_set ();
gpio_led_clear ();
TEST_ASSERT_EQUAL ( 0 , LATAbits . LATA0 );
}
Пробуем:
~$ rake test:all
# -------------------------
# OVERALL UNIT TEST SUMMARY
# -------------------------
# TESTED: 4
# PASSED: 4
# FAILED: 0
# IGNORED: 0
Очевидно, что такой TDD подход на порядок удобнее предыдущих . Тут очень многое зависит от возможностей эмулятора и иногда функционала sim30 бывает недостаточно. Типичный пример - логика обработка прерываний:
src/gpio_button.h
#ifndef gpio_button_H
#define gpio_button_H
void gpio_button_init ( void );
#endif // gpio_H
src/gpio_button.c
#include "gpio_button.h"
#include "gpio_led.h"
#include <xc.h>
void gpio_button_init () {
TRISFbits . TRISF0 = 1 ;
CNENFbits . CNIEF0 = 1 ;
IFS1bits . CNIF = 0 ;
IEC1bits . CNIE = 1 ;
}
void __attribute__ (( interrupt , auto_psv )) _CNInterrupt ( void )
{
IFS1bits . CNIF = 0 ;
gpio_led_set ( 42 );
}
test/test_gpio_button.c
#include "unity.h"
#include "gpio_button.h"
#include "mock_gpio_led.h"
#include <xc.h>
void setUp ( void ){
gpio_button_init ();
}
void tearDown ( void ){
}
void test_gpio_button_interrupt ( void ){
gpio_led_set_Expect ( 42 );
IFS1bits . CNIF = 1 ;
asm ( "NOP" );
}
Упс:
~$ rake test:all
# ------------------------
# FAILED UNIT TEST SUMMARY
# ------------------------
# [test_gpio_button.c]
# Test: test_gpio_button_interrupt
# At line (13): "Function 'gpio_led_set' called less times than expected"
Похоже эмулятор sim30 не обрабатывает прерывания. Альтернатива - использовать эмулятор mdb из среды разработки MPLAB . Текущий MPLAB X IDE v2.05 написан на Java и поэтому эмулятор работает очень небыстро, ну маемо те шо маемо:
test/simulation/mplab_sim_instructions.txt
Device PIC24EP64MC206
Hwtool SIM -p
Program ./build/TestBuild.out
Run
Quit
test/simulation/sim_test_fixture.rb
OUT_FILE = "test/simulation/out.txt"
File . delete OUT_FILE if File . exists? OUT_FILE
pipe = IO . popen ( "/opt/microchip/mplabx/mplab_ide/bin/mdb.sh " \
"./test/simulation/mplab_sim_instructions.txt " \
"> #{ OUT_FILE } " )
trap ( "INT" ) { Process . kill ( "KILL" , pipe . pid ); exit }
Process . wait ( pipe . pid )
if File . exists? OUT_FILE
file_contents = File . read OUT_FILE
print file_contents
else
print "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! \n " \
"! Program was not simulated ? ! \n " \
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
end
Финальный аккорд:
~$ rake test:all
# -------------------------
# OVERALL UNIT TEST SUMMARY
# -------------------------
# TESTED: 5
# PASSED: 5
# FAILED: 0
# IGNORED: 0
Исходники тут .
There are comments .