Отладка по UART или встроенный GDB server

AVR
Хочу подробно описать в деталях и коде, как можно отлаживать AVR по UART, не прибегая к использованию внутрисхемной отладке по JTAG, не тратя лишние пины, а задействуя лишь UART, прерывания по таймеру и возможности самопрограммирования FLASH-памяти контроллера.
В этой статье речь пойдет о программной заглушке, которая будет приостанавливать выполнение основной программы, возвращать состояние процессора, читать и писать в память, короче, выполнять роль отладчика, исполняясь непосредственно на контроллере.

Я был оч удивлен, когда на просторах сети не нашел ни одного достойного решения для, казалось бы, нужной задачи, как отладка AVR по UART. На запрос «avr gdb stub» я получил пару куцих решений, которые уже давным давно не поддерживаются, несовместимы с текущей версией gdb и содержат ряд существенных недостатков: отсутсвие исполнения интрукций по шагам или же замедление исполнения отлаживаемой программы в сотни раз. Мне показалось, что это хороший шанс попытаться разобраться в теме и, возможно, улучшить существующие решения.


Disclaimer
Я сразу хотел бы предупредить, что речь пойдет об отладке только в консольном gdb, а если быть совсем точным, то его модификации под AVR — avr-gdb версии 7.4. Именно avr-gdb (далее просто gdb) будет выполнять роль front-end, общааясь по UART с gdb сервером, исполняющимся на контроллере. Мое решение тоже не лишено недостатков: бинарного кода получилось много — ~6кб, хотя никаких попыток оптимизации и не предпринималось, а для расстановки breakpoints и выполнения инструкций по шагам необходима поддержка SPM/LPM в самом AVR чипе, но эти недостатки входили в рамки моей задачи, да и темы, затронутые в процессе реализации, оказались весьма интересными. Также отмечу, что эксперементировал я и затачивал все под ATmega16 и не делал попыток реализовать для нескольких архитектур, хотя, архитектурно зависимые части старался обернуть макросами.

Отладчик внутри ядра ОС
Для начала немного общей теории. Ядра современных операционных систем включают в себя код, который позволяет отлаживать само ядро (даже Linus Torvalds, ярый противник отладчиков, сдался, и в основную ветвь Linux были залиты все патчи кернельного отладчика kgdb). Код заглушки отладчика приостанавливает выполнение ядра, сохраняет состояние регистров процессора, переключается между стеками выполнения, анализирует память, короче, процесс отладки ядра практически ничем не отличается от отладки любого другого приложения, работающего в пространстве пользователя. Обычно, заглушка получает команды по UART или TCP от самого отладчика, запущенного на другой машине. Т.е. получается классическая архитектура клиент-сервер, где в роли сервера выступает оч тонкая заглушка, умеющая работать с конкретной архитектурой процессора, а в роли клиента — отладчик, задача которого сформировать низкоуровневые команды для заглушки, чтобы расставить точки останова по адресам или «раскрутить» стек с выводом вызванных функций и их параметров на консоль. Код отладочной заглушки упрощается, если сам процессор поддерживает отладочные инструкции или программные прерывания: например, со времен процессоров 8086 Intel включает в свою архитектуру x86 аппаратную поддержку single-step interrupt, т.е. вызов обработчика прерывания после выполнения одной инструкции. Такая аппаратная поддержка позволяет реализовать отладчик ядра с полным набором функций, не прибегая к разным хитростям, эмулирующим выполнение одной инструкции. В контроллерах AVR нет поддержки подобных инструкций, а есть единственная инструкция BREAK, позволяющая выполнять отладку по JTAG. Так как JTAG нас совсем не интересует, то в настоящей статьи я опишу несколько возможных подходов к расстановке точек останова и исполнению инструкций по шагам.

GDB и его протокол для удаленного взаимодействия
Архитектура GDB позволяет отлаживать приложения, выполняющиеся на другой машине (процессоре, контроллере, etc), передавая текстовые команды и ожидая ответа. Все команды низкоуровневые и оч простые, вот неполный список:
  • прервать/продолжить выполнение программы
  • установить/снять точку останова(адрес)
  • записать/прочитать из памяти(адрес, кол-во байт)
  • записать/прочитать регистр(порядковый номер регистра)
  • прочитать все регистры

Сама команда формируется оч просто:



Данные пакета и есть команды. Обычно, команда начинается с буквы, затем могут следовать параметры:

  • c — продолжить выполнение
  • D — прекратить отладку
  • g — прочитать все регистры
  • g или G — прочитать/записать все регистры
  • m или M — прочитать/записать память
  • z или Z — установить/снять точку останова

А вот так выглядит общение gdb с заглушкой при исследовании памяти:



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

Также хотелось бы отметить, что в процессе реализации протокола я брал за основу simulavr. Главным образом меня интересовало множество нюансов отладки под AVR: порядок и список возвращаемых регистров, способ адресации flash/ram памяти, поддерживаемые команды avr-gdb.

Архитектура заглушки под AVR

Как уже писалось выше, заглушка-сервер должна уметь:
  1. приостановить выполнение основной программы по команде от отладчика или по breakpoint'у
  2. шагать и выполнять ровно одну инструкцию
  3. читать и писать в память или в регистры

Если с третьим пунктом все понятно, то первые два не имеют аппаратной поддержки и нуждаются в определенной эмуляции.

Прерванная программа по Ctrl-C
В документации по gdb говорится, что пользователь может в любой момент прервать выполнение программы нажатием Ctrl-C в отладчике. Т.е. заглушка должна обработать команду прерывания (в терминах протокола avr-gdb это просто байт 0x03), приостановить основную программу и ждать следующей команды от gdb. Ничего не напоминает? Это описание работы любого прерывания контроллера, а в нашем случае UART прерывания. Т.е. пришел байт 0x03 по UART, и мы свалились в RXC прерывание, приостановив выполнение основной программы. Отлично. Что дальше? Дальше мы должны сразу же сохранить все регистры на стеке и проинициализировать верхушкой стека указатель на структуру со всеми регистрами. Код сохранения контекста из FreeRTOS прекрасно подходит для этой задачи, нуждаясь в минимальных изменениях.

/* Определяем структуру для всех регистров */
struct gdb_regs_ctx {
	uint8_t stack_bottom;
	uint8_t r31;
	uint8_t r30;
	uint8_t r29;
	uint8_t r28;
	uint8_t r27;
	uint8_t r26;
	uint8_t r25;
	uint8_t r24;
	uint8_t r23;
	uint8_t r22;
	uint8_t r21;
	uint8_t r20;
	uint8_t r19;
	uint8_t r18;
	uint8_t r17;
	uint8_t r16;
	uint8_t r15;
	uint8_t r14;
	uint8_t r13;
	uint8_t r12;
	uint8_t r11;
	uint8_t r10;
	uint8_t r9;
	uint8_t r8;
	uint8_t r7;
	uint8_t r6;
	uint8_t r5;
	uint8_t r4;
	uint8_t r3;
	uint8_t r2;
	uint8_t r1;
	uint8_t sreg;
	uint8_t r0;
	uint8_t pc_h;
	uint8_t pc_l;
};
/* Указатель на стек со всеми сохраненными регистрами */
static struct gdb_regs_ctx *gdb_regs;

/* Макрос сохранения всех регистров в стек и инициализации
   указателя gdb_regs верхушкой стека, т.е. SP */
#define GDB_SAVE_CONTEXT()					\
	asm volatile (	"push	r0			\n\t"	\
			"in	r0, __SREG__		\n\t"	\
			"cli				\n\t"	\
			"push	r0			\n\t"	\
			"push	r1			\n\t"	\
			"clr	r1			\n\t"	\
			"push	r2			\n\t"	\
			"push	r3			\n\t"	\
			"push	r4			\n\t"	\
			"push	r5			\n\t"	\
			"push	r6			\n\t"	\
			"push	r7			\n\t"	\
			"push	r8			\n\t"	\
			"push	r9			\n\t"	\
			"push	r10			\n\t"	\
			"push	r11			\n\t"	\
			"push	r12			\n\t"	\
			"push	r13			\n\t"	\
			"push	r14			\n\t"	\
			"push	r15			\n\t"	\
			"push	r16			\n\t"	\
			"push	r17			\n\t"	\
			"push	r18			\n\t"	\
			"push	r19			\n\t"	\
			"push	r20			\n\t"	\
			"push	r21			\n\t"	\
			"push	r22			\n\t"	\
			"push	r23			\n\t"	\
			"push	r24			\n\t"	\
			"push	r25			\n\t"	\
			"push	r26			\n\t"	\
			"push	r27			\n\t"	\
			"push	r28			\n\t"	\
			"push	r29			\n\t"	\
			"push	r30			\n\t"	\
			"push	r31			\n\t"	\
			"lds	r26, gdb_regs		\n\t"	\
			"lds	r27, gdb_regs + 1	\n\t"	\
			"in	r0, __SP_L__		\n\t"	\
			"st	x+, r0			\n\t"	\
			"in	r0, __SP_H__		\n\t"	\
			"st	x+, r0			\n\t"	\
			)

Работа по инициализации указателя на стек происходит здесь:

1. lds	r26, gdb_regs
   lds	r27, gdb_regs + 1
2. in	r0, __SP_L__
3. st	x+, r0
4. in	r0, __SP_H__
5. st	x+, r0

1. загружаем адрес указателя в регистры X (r26, r27)
2. сохраняем SP_L в r0
3. сохраняем r0 в память по адресу из X, потом итерируем на 1 байт
4. сохраняем SP_H в r0
5. сохраняем r0 в память по адресу из X

Я не буду приводить макрос GDB_RESTORE_CONTEXT, так как он делает тоже самое, что и GDB_SAVE_CONTEXT, но в обратном порядке.

Как теперь воспользоваться этими макросами в самом прерывании? Первое, что нужно сделать, так это сказать компилятору не создавать пролог и эпилог для функции, т.е. сделать ее «голой». Для этого есть специальный флажок-макрос ISR_NAKED, который развертывается в специальный аттрибут компилятору gcc: __attribute__ ((naked)). Вот что получилось:

ISR(USART_RXC_vect, ISR_NAKED)
{
	GDB_SAVE_CONTEXT();
        /* PC согласно даташиту на ATmega16, сохраненный на стеке, требует обнуления старших трех [15..13] бит.
           это оч важно, так как реально там лежит мусор, от которого gdb сносит голову */
	gdb_ctx->regs->pc_h &= 0x1f;

	/* обрабатываем команды от gdb */
        gdb_trap();

	GDB_RESTORE_CONTEXT();
        /* не забываем "правильно" выйти из прерывания */
	asm volatile ("reti \n\t");
}

После вызова GDB_SAVE_CONTEXT gdb_ctx будет указывать на память стека, которая содержит состояние всех регистров: 32 общих, текущий PC (адрес инструкции, которая будет выполняться после прерывания, т.е. куда прыгнет reti), SREG, короче все, что нужно для отладки.

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

Что такое breakpoint?
Это специальная инструкция при выполнении которой процессор должен прервать текущую программу, сменить контекст и прыгнуть в какой-нибудь обработчик, т.е. выполнить программное прерывания. Для x86 breakpoint — это однобайтная инструкция 0xСC, или INT 3. Когда заглушке-серверу приходит команда от gdb установить точку останова по адресу, например, 0xdeadface, то заглушка вычитывает и запоминает оригинальную инструкцию, а потом заменяет ее на инструкцию программного прерывания, т.е. просто пишет 0xCC по адресу 0xdeadface. Все. Всю остальную работу сделает процессор, выполнив программное прерывания и прыгнув в обработчик INT 3. Снятие точки останова — это обратная операция, т.е. замена 0xCC на оригинальную инструкцию.

У AVR же нет ничего подобного. А как же эмуляция software interrupt на INT0..2 пинах? Да, такая возможность есть, т.е. сконфигурировав пин как выход и выставив у порта нужный бит инструкцией sbi мы попадем в обработчик прерывания. Но вся проблема в том, что мы туда попадем спустя 4 инструкции, т.е. процессор выполнит sbi, а потом еще 3 инструкции после. Какая же это точка останова, если процессор ее пробегает?! Не пойдет.

Можно попробовать инструкцию CALL. Пропатчил FLASH-память контроллера, вписал CALL по нужному адресу и жди, когда процессор прыгнет в нашу ловушку. Прекрасно. Но CALL — 32-битная инструкция, а минимальный размер инсутркции на 8-bit AVR — 16-бит. Получается, что если мы будем патчить память 32-битными инструкциями, то рано или поздно мы нарушим целостность программы и выполним мусор. Вот иллюстрация:



  1. Мы находимся в «ловушке» отладчика, но, вернувшись, мы попадем на 0x1da8.
  2. Выставляем breakpoint на адрес 0x1da6, заменяя две 16-битные инструкции LPM, AND на нашу 32-битную инструкцию-«ловушку» CALL.
  3. Выйдя из «ловушки», мы сразу же исполним второе слово инструкции CALL, т.е. для процессора это мусор
Такое решение никуда не годится.

У CALL есть 16-битный брат RCALL, но он может адресовать 8K байт, чего явно недостаточно для контроллеров с FLASH-памятью большей 8Kb. Не пойдет.

Здесь я встретил оч простое, чертовски медленное, но красивое решение для AVR: после разрешения прерывания будет выполнена следующая инструкция до выполнения отложенного прерывания. Т.е. в переводе на человечий: контроллер не будет всегда выполнять только прерывания, как бы много их не приходило, а между прерываниями всегда будет выполняться по одной инструкции. Таким образом код становится оч простым: выполнили инструкцию, ушли в прерывание, сверили текущий PC c адресом точки останова, если не он, то заряжаем еще одно отложенное прерывание, выходим из обработчика на выполнение очередной инструкции, а если все-таки это breakpoint, то ждем команд от gdb. Оч просто, но и оч медленно, так как большую часть времени мы проведем в прерывании. Не пойдет.

Еще немного подумав, я пришел к решению патчить FLASH контроллера 16-битной инструкцией rjmp -1 или 0xcfff — бесконечный while(1);. Пока процессор крутится в «ловушке», не меняя своего состояния, мы можем сверить текущий PC с адресом точки останова из оч редкого (раз в секунду) обработчика прерывания по таймеру. При таком подходе мы не нагружаем контроллер бесконечными вложенными прерываниями, но и имеем возможность выставить точку останова, не потеряв состояние процессора. Данный подход мне понравился, его и реализовал. Возможно, есть еще какое-то решение, которое я проглядел.

Что такое шаг в одну инструкцию?
Это команда stepi от gdb, по которой процессор должен выполнить ровно одну инструкцию отлаживаемой программы и снова вернуться в заглушку, ожидая дальнейших команд от gdb. Как я говорил выше, архитектура x86 аппаратно поддерживает исполнение одной инструкции, чего в AVR опять же нет. При ранее описываемом подходе с вложенными прерываниями такое «шагание» делается оч просто, но в нашем случае очевидного решения нет. Хотя, ведь можно выставлять breakpoint на следующую инструкцию за текущей, а вся задача сводится к определению адреса следующей инструкции. Например, для интструкций перехода мы должны вычислять адрес перехода и выставлять точку останова на этот адрес. Заглянув в AVR Instruction Set Manual я выписал все инструкции группами, которые куда-нибудь да прыгают. Вот что получилось:

  1. CALL, JMP
    адрес перехода во втором слове инструкции
  2. ICALL, IJMP, EICALL, EIJMP
    адрес перехода в регистрах r31, r32
  3. RCALL, RJMP
    адрес перехода — 11..0 биты инструкции
  4. RET, RETI
    адрес перехода в стеке
  5. CPSE, SBRC, SBRS, SBIC, SBIS
    адрес перехода либо PC + 1, PC + 2 либо PC + 3
  6. BREQ, BRNE, BRCS, BRCC, BRSH, BRLO, BRMI, BRPL, BRGE,
    BRLT, BRHS, BRHC, BRTS, BRTC, BRVS, BRVC, BRIE, BRID
    адрес перехода либо PC + 1, либо PC + k + 1, где k — 7-битный адрес
  7. LDS, STS
    32-битная инструкция, т.е. адрес PC + 2
  8. все остальные
    16-битные инструкции, т.е. адрес PC + 1

Вся сложность в анализе групп 5 и 6, так как нужно будет делать всю работу за процессор, т.е. анализировать массу флагов для вычисления правильного адреса перехода. Но намного проще расставить точки по всем адресам, т.е. для пятой группы — это 3 точки останова, а для шестой группы — 2. Получается, что нужно лишь по коду текущей инструкции понять, к какой группе она принадлежит, и расставить точки везде, где только можно, а, вернувшись снова в прерывание, снять эти breakpoint'ы. Код выглядит так:

static void gdb_insert_breakpoints_on_next_pc(uint16_t pc)
{
	uint16_t opcode;

        /* Вычитываем код инструкции по текущему адресу */
	opcode = safe_pgm_read_word((uint32_t)pc << 1);

        /* Выставляем breakpoint по адресу из второго слова инструкции если это
           CALL или JMP */
	if ((opcode & CALL_MASK) == CALL_OPCODE ||
	    (opcode & JMP_MASK) == JMP_OPCODE)
		gdb_insert_breakpoint(safe_pgm_read_word(((uint32_t)pc + 1) << 1));
        /* Для ICALL, IJMP, EICALL, EIJMP берем адрес из регистров r31, r30 */
	else if (opcode == ICALL_OPCODE || opcode == IJMP_OPCODE ||
		 opcode == EICALL_OPCODE || opcode == EIJMP_OPCODE)
		gdb_insert_breakpoint((gdb_ctx->regs->r31 << 8) | gdb_ctx->regs->r30);
        /* Для RCALL, RJMP берем адрес из 11..0 битов */
	else if ((opcode & RCALL_MASK) == RCALL_OPCODE ||
		 (opcode & RJMP_MASK) == RJMP_OPCODE) {
		int16_t k = (opcode & REL_K_MASK) >> REL_K_SHIFT;
		/* k может быть и отрицательным, не забываем о знаковом бите */
		if (k & 0x0800)
			k |= 0xf000;
		gdb_insert_breakpoint(pc + k + 1);
	}
        /* Для RET, RETI берем адрес из стека */
	else if ((opcode & RETn_MASK) == RETn_OPCODE) {
		uint8_t pc_h = *(&gdb_ctx->regs->pc_h + 2) & RET_ADDR_MASK;
		gdb_insert_breakpoint((pc_h << 8) | *(&gdb_ctx->regs->pc_l + 2));
	}
        /* Инструкции могут прыгнуть на 3 адреса, выставим breakpoints для всех */
	else if ((opcode & CPSE_MASK) == CPSE_OPCODE ||
	         (opcode & SBRn_MASK) == SBRn_OPCODE ||
		 (opcode & SBIn_MASK) == SBIn_OPCODE) {
		gdb_insert_breakpoint(pc + 1);
		gdb_insert_breakpoint(pc + 2);
		gdb_insert_breakpoint(pc + 3);
	}
        /* Инструкции могут прыгнуть на 2 адреса, выставим breakpoints для всех */
	else if ((opcode & BRCH_MASK) == BRCH_OPCODE) {
                /* Для всех BRANCH инструкций адрес прячется в 7 битах */
		int8_t k = (opcode & BRCH_K_MASK) >> BRCH_K_SHIFT;
                /* не забываем о знаковости */
		if (k & 0x40)
			k |= 0x80;
		gdb_insert_breakpoint(pc + 1);
		gdb_insert_breakpoint(pc + k + 1);
	}
	/* 32-битные инструкции, выставляем точку на 2 слова вперед */
	else if ((opcode & LDS_MASK) == LDS_OPCODE ||
		 (opcode & STS_MASK) == STS_OPCODE)
		gdb_insert_breakpoint(pc + 2);
	/* 16-битные инструкции, выставляем точку на 1 слово вперед */
	else
		gdb_insert_breakpoint(pc + 1);
}

В итоге мы имеем механизм для выполнения ровно одной инструкции, основанный на точках останова. Все оч просто.

Как писать/читать FLASH-память?

Для замены инструкции на breakpoint инструкцию необходимо уметь читать и писать FLASH. Если для чтения в avr-libc есть специальные вызовы: pgm_read_[byte/word/dword/float]: зови функцию, получай данные, то с записью все сложнее. Писать во FLASH мы можем только из специальной секции NRWW, которая располагается в конце FLASH памяти, т.е. код, отвечающий за перезапись каких-то участков во FLASH, должен находиться именно в этой секции. Компилятору можно явно указать, к какой секции принадлежит функция:
__attribute__ ((section(".nrww"),noinline))
static void safe_pgm_write(const void *ram_addr,
						   uint16_t rom_addr,
						   uint16_t sz);

А линковщику указать, по какому адресу располагать нужную нам секцию:

-Wl,--section-start=.nrww=0x3ea0 

В моем случае контроллера ATmega16 с 16 Кб памяти я пишу 352 байтную функцию в самый конец FLASH-памяти, т.е. 0x4000 — 352 = 0x3ea0.

Алгоритм записи во FLASH или self-programming происходит в три этапа:
  1. Сначала заполняется специальная временная страница памяти (для ATmega16 — это 128 байт)
  2. Страница стирается из FLASH (_не_ временная страница)
  3. Копируется уже заполненная временная страница во FLASH (т.е. на место стертой)

Если мы хотим записать во FLASH данные, адрес или размер которых не кратен странице, то необходимо сначала заполнить временную страницу, вычитав FLASH, иначе мы запишем мусор. Алгоритм записи буфера из оперативной памяти в произвольный адрес во FLASH выглядит так:

/* rom_addr - в словах, sz - в байтах, но обязан быть кратен двум. */
__attribute__ ((section(".nrww"),noinline))
static void safe_pgm_write(const void *ram_addr,
						   uint16_t rom_addr,
						   uint16_t sz)
{
	uint16_t *ram = (uint16_t*)ram_addr;

	/* Sz должен быть не равен нулю и кратен двум */
	if (!sz || sz & 1)
		return;

	/* Ждем, если что-то писалось в EEPROM */
	eeprom_busy_wait();

	/* из байт в слово */
	sz >>= 1;

        /* округляем адрес в ROM до страницы. итерируем адрес страницы ровно на размер страницы */
	for (uint16_t page = ROUNDDOWN(rom_addr, SPM_PAGESIZE_W),
		 end_page = ROUNDUP(rom_addr + sz, SPM_PAGESIZE_W),
		 off = rom_addr % SPM_PAGESIZE_W;
		 page < end_page;
		 page += SPM_PAGESIZE_W, off = 0) {

		/* адрес страницы из слов в байты */
		uint32_t page_b = (uint32_t)page << 1;

		/* пробегаем по страницы, итерируемся по словам */
		for (uint16_t page_off = 0;
			 page_off < SPM_PAGESIZE_W;
			 ++page_off) {
			/* в байты */
			uint32_t rom_addr_b = ((uint32_t)page + page_off) << 1;

			/* если смещения совпадают - заполняем временную страницу
                           данными из буфера */
			if (page_off == off) {
				boot_page_fill(rom_addr_b,  *ram);
				if (sz -= 1) {
					off += 1;
					ram += 1;
				}
			}
			/* если смещения разные - вычитываем из FLASH */
			else
				boot_page_fill(rom_addr_b, safe_pgm_read_word(rom_addr_b));
		}

		/* чистим страницу, ждем завершения. */
		boot_page_erase(page_b);
		boot_spm_busy_wait();

		/* пишем временную страницу, ждем завершения. */
		boot_page_write(page_b);
		boot_spm_busy_wait();
	}

	/* включаем RWW секцию, иначе останемся здесь на веки вечные */
	boot_rww_enable ();
}


Данный алгоритм используется для записи инструкций точек останова во FLASH.

Что же получилось?

Получилась полноценная AVR реализация заглушки для avr-gdb, позволяющая отлаживать код, используя UART и одно прерывания по таймеру. Для демонстрации отладки я использовал небольшой алгоритм сортировки, взятый из ядра Linux:



Код заглушки: github.com/rouming/AVR-GDBServer

Используемые материалы
Howto: GDB Remote Serial Protocol
GDB remote protocol
AVR109 Self-programming
AVR bootloader FAQ
AVR Instruction Set Manual
Simulavr gdbserver.cpp source
FreeRTOS context switch source



  • +24
  • 18 октября 2012, 15:53
  • rouming

Комментарии (40)

RSS свернуть / развернуть
Интересно. И даже полезно, если не хочется включать jtag. Но громоздко, а так хочется простую замену dW.
0
да, это проблема.
но, я думаю, код можно хорошо ужать для AVR, написав утилиту, которая будет стоять на хосте посередине между avr-gdb и этой заглушкой
[ avr-gdb <--> avr-gdb-lite ] <--> [ AVR with stub ]
              HOST                       MCU

такая утилита аггрегировала бы команды от gdb и отправляла бы оч короткие, ужатые (бинарные) версии на сам контроллер, облегчая разбор и анализ.
но пришлось бы еще запускать эту утилиту, чего изначально не хотелось.
0
А почему не совместили два подхода, бесконечный цикл для брякпоинтов и вложенные прерывания для пошага? Всё ж ресурсы флеша на запись ограничены, если отлаживать так какой-то цикл, наверное можно досмерти изнасиловать атмегу?
+1
  • avatar
  • ACE
  • 18 октября 2012, 17:31
Можно, тогда придется пожертвовать одним пином для того, чтобы заряжать из UART обработчика отложенное прерывание.
0
Спасибо за статью. Давно хотел разобраться поподробнее как-же работает gdb, но не знал с какой стороны подступиться.
0
Интересно. Но ШЕСТЬ КИЛОБАЙТ для отладчика??? Для практики пригодно не более килобайта, и то слишком много. Так что пока это просто интересный экскурс в работу GDB.
0
  • avatar
  • _YS_
  • 18 октября 2012, 17:59
Комментарием выше я уже писал, что можно оч сильно упростить код заглушки и, возможно, действительно сделать _отладчик_. А пока лишь я поделился рабочей идеей, не требующей ни дополнительных пинов, ни запуска каких-то еще утилит. Только gdb и 6 кб на флеше.
0
Как грится, теперь вы знаете точно почему используете JTAG. А по теме, очень познавательно в части работы с gdb. Сам я лишь раз в жизни запускал именно текстовую консоль, чтобы вручную ввести какие-нить команды отладчику. Жду не дождусь, когда эту самую поддержку gdb доделает автор HappyJTAG2, чтобы можно было писать и отлаживать программы во всяких там эклипсах и прочих.
0
  • avatar
  • uni
  • 18 октября 2012, 18:19
На самом деле AVR работает следующим образом: если выполняется функция-обработчик прерывания и приходит ещё прерывание, то следующее прерывание выполнится после того, как текущий обработчик выполнит reti, но перед вызовом ожидающего обработчика будет выполнена ровно одна команда. =)
0
Статья интересная и познавательная (+1), вместо слово «затычка» лучше использовать gdb stub, а то этим же словом (затычка) вы называете и вечный цикл (путанница).
контроллер не будет всегда выполнять только прерывания, как бы много их не приходило, а между прерываниями всегда будет выполняться по одной инструкции
Оч просто, но и оч медленно, так как большую часть времени мы проведем в прерывании. Не пойдет.
… так пошаговая отладка и есть очень медленно, а заодно сохраняете флэш память (имхо)
0
> вместо слово «затычка»
вы хотели сказать заглушка? я стараюсь не использовать американизмы, либо все на русском, либо на английском. как я заметил, все стараются писать исключительно на русском, даже комментарии к коду переводят. следую внегласным правилам сообщества.
> …а заодно сохраняете флэш память (имхо)
сохранение пина мне было важней. думаю, это опция: либо пин, либо частая запись во флеш. т.е. в идеале нужно под макросами реализовать и решение, основанное на вложенных прерываниях.
0
… извиняюсь, «заглушка» конечно =)
0
Много и вкусно, спасибо. Вы меня опередили, я собирался написать stub для stm32.
0
… вы собирались написать stub или статью? :)
0
… просто мне интересно, работал ли кто-то с redboot, хотя ~50KB для Cortex'ов огорчает
0
Работал с RedBoot (AT91SAM9X25, -G45). Использую только GDB через JTAG. Также пробовал через Ethernet — не понравилось, медленно.
0
И stub и статью :)
0
Да, спасибо за статью, огромное! Ибо я как раз изобретал такой велосипед для STM8!
0
к сожалению моя статья не облегчит вам изобретение велосипеда. я не вижу, чтоб были какие-нибудь toolchain в свободном доступе для stm8. gdb не поддерживает эту архитектуру, а без gdb моя заглушка совершенно бессмысленна. хотя, как я вижу из вот этого дока, есть поддержка trap, что облегчит выставление breakpoint'ов.
0
Тут как раз смысл был в том, чтобы прикрутить свой тулчейн (свою среду разработки) к STM8.

А ваша статья дала мне мысли по поводу реализации отладки на контроллере.
0
я не вижу, чтоб были какие-нибудь toolchain в свободном доступе для stm8. gdb не поддерживает эту архитектуру, а без gdb моя заглушка совершенно бессмысленна.

ST Visual Develop вроде использует GDВ. Там в корне лежит файл gdb7.exe
0
а там рядом исходников не лежит? так как в исходниках самого gdb я не нашел упоминаний архитектуры stm8, а если разработчики st visual develop что-то пилили и не поделились исходниками, то они нарушили GPL.
возможно, что они просто используют gdb как клиент для своего jtag.
0
А зачем он, если есть аппаратная отладка? Аналогично с STM8.
0
Я тоже думаю зачем он, если мне и аппаратная отладка не нужна. На этом все и останавливается.
0
Интересно.
Алсо, статья написана хорошо, но раздражает постоянное «оч». Неужели сложно добавить еще три буквы?
0
  • avatar
  • Vga
  • 18 октября 2012, 22:51
простите, такой стиль
0
Это можно исправить командой «Найти и заменить» в блокноте :)
0
Статья заставила задуматься, и вот что я понял. Дело в том, что компиляция с опцией -g (добавлять отладочную информацию) подразумевает, что код будет отлаживаться. А в данном случае отлаживается голый код. Это первый момент.
Второй момент состоит в том, что обычно код с отладочной информацией и в разы больше по объёму, и работает в разы медленнее. Следовательно, если вместо отладочной информации вставить в код команду RETI после каждой команды, то сам код можно будет выполнять из обработчика прерывания, после каждой выполненной команды возвращаясь в основной цикл программы, то есть в отладчик. И всего-то в два раза отлаживаемый код увеличится при этом.
Но при этом флэш не надо будет каждый раз программировать, вставляя/убирая точки код точек останова. По-моему это гениально.
0
1. отладочная информация никак не влияет на объем _кода_. отладочная информация помогаяет дебаггеру раскрутить стек, понять по каким адресам / регистрам лежат локальные переменные и проч и проч. отладочную информацию можно держать отдельно от основной программы и загружать _только_ во время отладки.
2. на объем кода влияют опции компилятора, например опции оптимизации. с отладкой это вообще никак не связано.
3. очевидно, что из пункта 1 и 2 следует, что программа с отладочной информацией работает ровно точно так же, как и без.
4. простите, про RETI я вообще ничего не понял.
0
В пп.1-3 не учтен только момент проверки ошибки доступа к памяти, когда между переменными вставляется маркер. Очевидно, что работает программа уже не точно так же. И объем кода уваличивается.
0
я бы перестал пользоваться компилятором, который без моего ведома пытается контроллировать доступ к памяти и что-то там расставляет. это такой overhead, что такую технику стоит применять в случае ловли какого-то конкретного бага (например, использовать valgrind). но еще раз. никакого отношения к дебажной информации это не имеет. это мета-информация (команды дебаггеру) которая может быть, а может и не быть.
0
0
я не понял, как это связано с отладочной информацией. вы _явно_ скомпилировались и слинковались с библиотекой, которая переопределила вызовы операций с памятью. точно так же вы можете использовать какие-то другие техники отладки памяти. из всего этого не следует, что при генерации и включении отладочной информации внезапно код станет медленнее и больше. это совсем разные вещи.
0
4. Ну, у Вас отладчик работает внутри прерывания, а я говорю о варианте, когда наоборот — отлаживаемый код на исполнение вызывается из прерывания, поэтому по команде RETI происходит возврат в отладчик. =)
0
хорошо. а «дебажный» RETI откуда появится в самом коде? и как отличить «дебажный» RETI от RETI самой программы?
0
Это самая сложная часть. Нужно брать elf-файл и вместо avr-objcopy делать из него прошивку, добавляя код отладчика и вставляя повсюду эти RETI. =)
0
Вы путаете тёплое с мягким) Отладочная информация с кодом во встраиваемых системах (при таком низкоуровневом коде) связана только внутри отладчика. Код при этом может иметь любой уровень оптимизации. Хоть нулевой, хоть максимальный, хотя при этом отлаживать будет несколько неудобно ввиду того, что команды могут исполняться в ином, чем записанный, порядке или вообще пропускаться (встроены в другие, выброшены нафиг или посчитаны заранее).

Отладочная информация может встраиваться в файл программы в программах, работающих под ОС, например в exe-файлы, если линкёр об этом попросить. При этом могут остаться разного рода проверки, выкидываемые в релизе, которые дополнительно увеличивают код программы. Там это действительно связано.

Непонятно Оо
0
10 из 10, святые угодники, 10 из 10!
0
А мне вот немного непонятно как будет работать отладка и конкретно точки останова внутри какого-либо прерывания. Точнее, насколько я помню, и как уже было сказано выше, в авр-ках если процессор вывалился в прерывание, то любое другое прерывание произойдет только после выхода из первого. Это значит что если мы поставим точку останова в каком-либо прерывании, то процессор зависнет навсегда в бесконечном цикле, когда дойдет до этой точки, т.к. прерывание по таймеру никогда не вызовется, из-за того, что процессор уже находится в одном из обработчиков прерываний. Если мне не изменяет память, в обработчиках перрываний можно сбрасывать глобальный бит I и тогда прерывания могут прерывать друг-друга. Но в таком случае все точки останова должны ставиться только после команды сброса этого бита.
0
Если мне не изменяет память, в обработчиках перрываний можно сбрасывать глобальный бит I и тогда прерывания могут прерывать друг-друга.
I сбрасывается при переходе на обработчик прерывания ядром. Чтобы разрешить вложенные прерывания нужно его установить (например командой sei).
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.