Интересно было узнать, есть ли возможность тестировать программу, написанную для микроконтроллера AVR, сделать полноценные тесты без JTAG, в среде линукс.

Отличным решением оказалась библиотека: simavr

Для себя я взял fork данной библиотеки https://github.com/JarrettBillingsley/simavr/tree/tinyusi с имплементацией USI механизма.

Вот ссылка на мою версию simavr с обновлениями из оригинального buserror/simavr+поддержка usi из tinyuse+ядро attiny26: здесь

В качестве теста для симуляции был выбран следующий код:

#define F_CPU 9600000

#include <avr/io.h>
#include <util/delay.h>

void main() {
    DDRB = 0xff;
    while (1) {
        PORTB = 0xff;
        _delay_ms(1);
        PORTB = 0;
        _delay_ms(1);
    }
}

Получен elf файл

Вот как его содержимое перегнанное в bin формат выглядит в Cutter+rizin(rizin почти равно radar2):

Cutter_blink

Теперь попробую запустить прошивку командой:

simavr -g -m attiny13a blink.elf

Для присоединения к прошивке воспользуюсь avr-gdb+gdb-dashboard

avr-gdb_blink

Команды:

  • avr-gdb - для запуска отладчика
  • >>> file blink.elf - для загрузки отладочной информации об исполняемом файле
  • `>>> target remote :1234 - для подключения к эмулятору(по умолчанию порт 1234)
  • >>> b main - для установки точки останова на функции main

avrgdb_blink2

По рисунку выше видно, что эмулятор остановился на выполнении вектора прерываний RESET с 0-адресом.

Так же можно присоединиться для отладки с помощью radare2:

radare2_blink

Команды для присоединения к отладке:

  • radare2 -a avr -d gdb://127.0.0.1:1234 - команда для присоединения к симулятору
  • >>> ds - выполнить текущую инструкцию на которую указывает регистр pc
  • >>> pd 10 @pc - показать 10 инстукций от текущего, адреса сохраненного в PC

Хотелось бы упомянуть один аспект. Для исследования SRAM памяти необходимо дампить адреса с 0x800000, там отображаются регистры, порты ввода вывода и пр, а так же стек. Дамп адресов с 0 покажет содержимое flash памяти.

  • px 0x19f @0x800000 - дамп SRAM
  • dr= - печать всех регистров

И это еще не все возможности simavr. Главная возможность - это возможность создать окружающую среду для симуляции воздействия на микроконтроллер: кнопки, сигналы для АЦП и компаратора, возможность получать состояние микроконтроллера, содержимое портов ввода вывода, для сравнения с тестовыми эталонами, запись выходных и входных сигналов в vcd файл.

Возьму все ту же прошивку blink, которая по сути выставляет высокий потенциал на выходах порта B и потом через промежуток времени заменяет на низкий потенциал. И так повторяется в цикле.

Добавлю данную прошивку во враппер и запишу смену выходных сигналов:

#include <stdlib.h>
#include <stdio.h>
#include <libgen.h>
#include <pthread.h>
#include "sim_avr.h"
#include "sim_elf.h"
#include "sim_vcd_file.h"
#include "avr_ioport.h"

avr_t *avr = NULL;
avr_vcd_t vcd_file;

int main(int argc, char *argv[]) {
    elf_firmware_t f;
    elf_read_firmware("blink.elf", &f);
    f.frequency = 9600000;
    avr = avr_make_mcu_by_name("attiny13a");
    avr_init(avr);
    avr_load_firmware(avr, &f);
    avr->ioend = 0x60;
    avr_vcd_init(avr, "gtkwave_output.vcd", &vcd_file, 1);
    avr_vcd_add_signal(&vcd_file,
            avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'),
                    IOPORT_IRQ_PIN_ALL), 8, "PORTB");
    int cnt = 0;
    avr_ioport_state_t state;
    avr_vcd_start(&vcd_file);
    while (1) {
        avr_run(avr);
        //printf("PC=%x\n", avr->pc);
        //avr_ioctl(avr, AVR_IOCTL_IOPORT_GETSTATE('B'), &state);
        //printf("PORT%c %02x DDR %02x PIN %02x\n", state.name, state.port, state.ddr, state.pin);
        if (cnt++ == 96000) {
            break;
        }
    }
    avr_vcd_stop(&vcd_file);
    avr_vcd_close(&vcd_file);
    return 0;
}
  • Комментарии к коду:
  • elf_read_firmware("blink.elf", &f); - читаю прошивку
  • f.frequency = 9600000; -выставляю частоту(на случай, если в файле прошивки нет этой информации)
  • avr = avr_make_mcu_by_name("attiny13a"); - создаю ядро симулятора нужного мне микроконтроллера
  • avr->ioend = 0x60; - явно указываю, с какого адреса начинается память SRAM, для возможности отслеживания изменения сигналов и запись в VCD файл. почему-то по умолчанию это значение присваивает 0 и логирование в симуляторе не работало.
  • avr_vcd_add_signal(&vcd_file, avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), IOPORT_IRQ_PIN_ALL), 8, "PORTB"); - добавляю сигнал(в днном случае выводы PORTB, все пины) для логирования в VCD файл
  • avr_vcd_start(&vcd_file); - начало записи в VCD файл
  • в цикле выполняется инструкция avr_run(avr); которая выполняет одну команду микропроцессора
  • счетчик if (cnt++ == 96000) ограничивает количество выполняемых инструкций

В данной программе есть момент, который хочу прокомментировать - имеется закомментированный код:

        //printf("PC=%x\n", avr->pc);
        //avr_ioctl(avr, AVR_IOCTL_IOPORT_GETSTATE('B'), &state);
        //printf("PORT%c %02x DDR %02x PIN %02x\n", state.name, state.port, state.ddr, state.pin);

данный код(если его раскомментировать) позволяет выводить содержимое PORTB и адрес выполняемой инструкции. Т.е он показывает полную доступность состояния микроконтроллера.

Вот результат работы симуляции:

vcd_blink_workspace

Можно отследить временные интервалы смены сигналов на выходе и состояние выходов микроконтроллера.

Ссылка на демонстрационный код

Команды сборки:

gcc -o blink_test main.c -I/usr/local/include/simavr/ -lsimavr
avr-gcc -O3 -g -o blink.elf blink.c -mmcu=attiny13a

Теперь вторая симуляция - ранее я разбирал статью, где была создана прошивка для attiny26l микроконтроллера, который должен был заменить холтек.

Для симуляции я несколько упростил прошивку, убрал самотестирование и задержки, чтоб вывод был нагляднее. Протестирую алгоритм определения максимума на синусоиде входного сигнала и переключение выводов, управляющих реле.

Код прошивки и обертки можно найти здесь

Обертка для simavr:

#include <stdlib.h>
#include <stdio.h>
#include <libgen.h>
#include <pthread.h>
#include <math.h>
#include "sim_avr.h"
#include "sim_elf.h"
#include "sim_vcd_file.h"
#include "avr_ioport.h"
#include "avr_adc.h"
#include "sim_gdb.h"

avr_t *avr = NULL;
avr_vcd_t vcd_file;
avr_irq_t *adcPort0, *adcPort1, *adcPort2;
uint32_t sineTimer = 625;
uint32_t sineCounter = 0;
double sinePeriod = 4.5;
double sineStage = 0.0;
int simulationCompleted = 0;

#define STOP_TIME 2000
double maxVoltage = 220;

uint16_t adcToVolt(uint16_t data) {
    double mill = data * 5.0;
    return round(mill);
}

uint16_t adcToVoltdirect(double data) {
    return round(data * 1000.0);
}

static avr_cycle_count_t sinegenerating(avr_t *avr, avr_cycle_count_t when,
        void *param)
{
    if (!simulationCompleted) {

        double res = sin(sineStage * 3.1415926535 / 180.0) * maxVoltage;
        if (res < 0.0) res = 0.0;

        sineStage += sinePeriod;
        if (sineStage >= 360.0) {
            sineStage = 0.0;
        }

        avr_raise_irq(adcPort1, adcToVoltdirect(res * 0.0144606));
        avr_raise_irq(adcPort0, adcToVoltdirect(res * 0.0144606));
        sineCounter++;
        if (sineCounter == 360) {
            maxVoltage = 160;
        } else if (sineCounter == 720){
            maxVoltage = 185;
        } else if (sineCounter == 1080){
            maxVoltage = 240;
        } else if (sineCounter == 1440) {
            maxVoltage = 280;
        } else if (sineCounter == 1800) {
            maxVoltage = 220;
        }
        avr_cycle_timer_register_usec(avr, sineTimer, sinegenerating, NULL);
        if (sineCounter == STOP_TIME) {
            simulationCompleted = 1;
        }
    }
    return 0;
}

int main(int argc, char *argv[]) {
    elf_firmware_t f;
    elf_read_firmware("sven600.elf", &f);
    f.frequency = 8000000;
    avr = avr_make_mcu_by_name("attiny26");
    avr_init(avr);
    avr_load_firmware(avr, &f);
    avr->ioend = 0x60;
    avr->avcc = 5000;
    avr->aref = 5000;
    avr->vcc = 5000;
    //avr->gdb_port = 1234;
    //avr->state = cpu_Stopped;
    //avr_gdb_init(avr);
    adcPort0 = avr_io_getirq(avr, AVR_IOCTL_ADC_GETIRQ, ADC_IRQ_ADC0);
    adcPort1 = avr_io_getirq(avr, AVR_IOCTL_ADC_GETIRQ, ADC_IRQ_ADC1);
    adcPort2 = avr_io_getirq(avr, AVR_IOCTL_ADC_GETIRQ, ADC_IRQ_ADC2);
    avr_vcd_init(avr, "gtkwave_output.vcd", &vcd_file, 1);
    avr_vcd_add_signal(&vcd_file,
            avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), IOPORT_IRQ_PIN4),
            1, "RELE1");
    avr_vcd_add_signal(&vcd_file,
            avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), IOPORT_IRQ_PIN5),
            1, "RELE2");
    avr_vcd_add_signal(&vcd_file,
            avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), IOPORT_IRQ_PIN6),
            1, "RELE3");
    avr_vcd_add_signal(&vcd_file,
            avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('A'), IOPORT_IRQ_PIN5),
            1, "YELLOW");
    avr_vcd_add_signal(&vcd_file,
            avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('A'), IOPORT_IRQ_PIN4),
            1, "RED");
    avr_vcd_add_signal(&vcd_file,
            avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), IOPORT_IRQ_PIN0),
            1, "SDA");
    avr_vcd_add_signal(&vcd_file,
            avr_io_getirq(avr, AVR_IOCTL_IOPORT_GETIRQ('B'), IOPORT_IRQ_PIN2),
            1, "SCL");
    avr_vcd_add_signal(&vcd_file, adcPort0, 16, "LineOut");
    avr_vcd_add_signal(&vcd_file, adcPort1, 16, "LineIn");
    avr_vcd_add_signal(&vcd_file, adcPort2, 16, "Temp");

    avr_vcd_start(&vcd_file);
    avr_raise_irq(adcPort0, adcToVolt(0));
    avr_raise_irq(adcPort1, adcToVolt(0));
    avr_raise_irq(adcPort2, adcToVolt(0));
    avr_cycle_timer_register_usec(avr, sineTimer, sinegenerating, NULL);
    while (!simulationCompleted) {
        avr_run(avr);
    }
    avr_vcd_stop(&vcd_file);
    avr_vcd_close(&vcd_file);
    return 0;
}

в данной обертке моделируется АЦП, на входах ADC0, ADC1, ADC2. Наиболее мне интересный - ADC1.

я устанавливаю напряжение на входах микроконтроллера в милливольтах:

    avr->avcc = 5000;
    avr->aref = 5000;
    avr->vcc = 5000;

Делаю связку перемнных с АЦП:

    adcPort0 = avr_io_getirq(avr, AVR_IOCTL_ADC_GETIRQ, ADC_IRQ_ADC0);
    adcPort1 = avr_io_getirq(avr, AVR_IOCTL_ADC_GETIRQ, ADC_IRQ_ADC1);
    adcPort2 = avr_io_getirq(avr, AVR_IOCTL_ADC_GETIRQ, ADC_IRQ_ADC2);

Задаю отслеживание входа АЦП avr_vcd_add_signal(&vcd_file, adcPort1, 16, "LineIn"); и устанавливаю первое значение на АЦП1 avr_raise_irq(adcPort1, adcToVolt(0));

А далее вот эта функция-таймер задают весь ход симуляции - avr_cycle_timer_register_usec(avr, sineTimer, sinegenerating, NULL);, sineTimer говорит, что через 625 микросекунд нужно выполнить функцию sinegenerating, а в самой функции вычисляется значение синусоиды входного напряжения, которое и будет подаваться на АЦП:

        double res = sin(sineStage * 3.1415926535 / 180.0) * maxVoltage;
        if (res < 0.0) res = 0.0;

        sineStage += sinePeriod;
        if (sineStage >= 360.0) {
            sineStage = 0.0;
        }

        avr_raise_irq(adcPort1, adcToVoltdirect(res * 0.0144606));
        avr_raise_irq(adcPort0, adcToVoltdirect(res * 0.0144606));

А далее проверяется, сколько раз уже выполнилась эта функция и в зависимости от этого меняется максимальное значение входящего напряжения, которое в свою очередь заставляет переключаться выходы реле и светодиодов.

sineCounter++;
        if (sineCounter == 360) {
            maxVoltage = 160;
        } else if (sineCounter == 720){
            maxVoltage = 185;
        } else if (sineCounter == 1080){
            maxVoltage = 240;
        } else if (sineCounter == 1440) {
            maxVoltage = 280;
        } else if (sineCounter == 1800) {
            maxVoltage = 220;
        }

и в конечном итоге опять регистрируется следующее событие через 625 микросекунд.

Вот результаты моделирования прошивки:

sven600_vcd1

sven600_vcd2

Итог: очень удобное средство для моделирования AVR микроконтроллеров.

Добавить комментарий

Следующая запись Предыдущая запись