Интересно было узнать, есть ли возможность тестировать программу, написанную для микроконтроллера 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):
Теперь попробую запустить прошивку командой:
simavr -g -m attiny13a blink.elf
Для присоединения к прошивке воспользуюсь avr-gdb+gdb-dashboard
Команды:
avr-gdb
- для запуска отладчика>>> file blink.elf
- для загрузки отладочной информации об исполняемом файле- `>>> target remote :1234 - для подключения к эмулятору(по умолчанию порт 1234)
>>> b main
- для установки точки останова на функции main
По рисунку выше видно, что эмулятор остановился на выполнении вектора прерываний RESET с 0-адресом.
Так же можно присоединиться для отладки с помощью radare2:
Команды для присоединения к отладке:
radare2 -a avr -d gdb://127.0.0.1:1234
- команда для присоединения к симулятору>>> ds
- выполнить текущую инструкцию на которую указывает регистр pc>>> pd 10 @pc
- показать 10 инстукций от текущего, адреса сохраненного в PC
Хотелось бы упомянуть один аспект. Для исследования SRAM памяти необходимо дампить адреса с 0x800000, там отображаются регистры, порты ввода вывода и пр, а так же стек. Дамп адресов с 0 покажет содержимое flash памяти.
px 0x19f @0x800000
- дамп SRAMdr=
- печать всех регистров
И это еще не все возможности 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 и адрес выполняемой инструкции. Т.е он показывает полную доступность состояния микроконтроллера.
Вот результат работы симуляции:
Можно отследить временные интервалы смены сигналов на выходе и состояние выходов микроконтроллера.
Ссылка на демонстрационный код
Команды сборки:
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 микросекунд.
Вот результаты моделирования прошивки:
Итог: очень удобное средство для моделирования AVR микроконтроллеров.