Тема профессиональной разработки ПО для микроконтроллеров с использованием современных методик уже поднималась. Собственно если вкратце, то было предложено максимально разделять исходный код на отдельные модули и тестировать их логику на ПК независимо от железа. Такой наивный подход способен значительно упростить жизнь разработчику, но в то же время имеет серьёзные недостатки особенно на стыке софта перефирии:
-
увеличивается объём кода за счёт дополнительных функций или
#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
Исходники тут.