Скриптовый отладчик в KEIL. Ассемблер А51, задание тестовых значений переменных при отладке програмы.

  Что такое скриптовый отладчик в KEIL? Это командный интерпретатор доступный только в режиме отладки. Его использование возможно из специальной отладочной консоли. В качестве командного языка используется подмножество языка С с некоторыми ограничениями:
  • прописные и строчные символы не различаются;
  • существуют ограничения по работе с указателями и адресной арифметикой;
  • не может использоваться для вызова процедур и функций отлаживаемой программы;
  • не поддерживает определение структур;
  Отладчик может выполнять скрипты, или по иному, встроенные в отладчик и/или написанные программистом процедуры и функции. В них доступны публичные константы и переменные отлаживаемой программы (Symbols) и команды отладчика. Также поддерживаются стандартные С операторы управления ходом выполнения программы if, else, while, do, switch, case, break, continue, goto.
  Хотя данная статья написана применительно к ассемблеру А51 архитектуры MCS-51, данные приемы универсальны, и могут быть применены и к языку С, и к архитектуре ARM с минимальными переделками или без них.

  Доступ к отладочным окнам появляется при переходе в режим отладки и может быть осуществлен или через кнопки отладочного ToolBar (выделены кнопки, о которых пойдет речь в статье),



или через появившиеся подменю в меню View:



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

$SET (Debug)

_VARS	SEGMENT	DATA
		RSEG	_VARS
AV_OVF_F:	DS		1	    ; Старший байт счетчика частоты
AV_Value:	DS		4	    ; буфер входных значений
AV_Value3	DATA	AV_Value+3	    ; for debug
AV_Value2	DATA	AV_Value+2	    ; for debug
AV_Value1	DATA	AV_Value+1	    ; for debug
AV_Value0	DATA	AV_Value+0	    ; for debug


$IF (Debug)				    ; Отладочные значения регистров			
	MOV		AV_Value3, #0x03    ; 4 байт
	MOV		AV_Value2, #0xD0    ; 3 байт 
	MOV		AV_Value1, #0x80    ; 2 байт  
	MOV		AV_Value0, #0x00    ; 1 байт
$ELSE							
;-- Перенос данных измерений из входных регистров в рабочие ------------------;
	MOV		R0, #AV_Value	    ; Начальный адрес
	MOV		@R0, SBUF	    ; 1 байт
	INC		R0		    ;
	MOV		@R0, TL0	    ; 2 байт
	INC		R0		    ;
	MOV		@R0, TH0	    ; 3 байт
	INC		R0		    ;
	MOV		@R0, AV_OVF_F	    ; 4 байт
$ENDIF

При частом изменении отладочных значений такое задание тестового значения переменных начинает напрягать. Ведь для того чтобы изменить его при таком способе отладки необходимо:
  • Выйти из режима отладки;
  • Вернуться к месту присвоения и изменить тестовое значение;
  • Перекомпилировать программу с новыми значениями;
  • Войти в режим отладки;
  • Вернуться к нужному месту программы;
Если необходимо проделывать эти операции по несколько десятков раз, поневоле задумаешься об упрощении.
  Для облегчения присвоения тестового значения можно использовать Configuration Wizard. Это псевдокод написанный в комментариях и оформленный спецтегами. При наличии такого кода в проекте появляется дополнительная вкладка «Configuration Wizard».
Для данной задачи его код будет выглядеть примерно так:

;- Мастер подстановки входных настроечных значений ---------------------------;
;- включен при DG_EnInVar EQU 1, используется только при отладке -------------;
;  *** <<< Use Configuration Wizard in Context Menu >>> ***
;- Header --------------------------------------------------------------------;
; <h> Setting debugging of input variables
; <i> Setting debugging of input variables online
;- Debug input variables -----------------------------------------------------;
;	<e> Enable debug input variables
;	<i> Enable setting of input variables for debugging
DG_EnInVar	EQU  1
;		<o> Value+3: fourth byte <0x00-0xFF>
;		<i> Fourth byte input register.
DG_Value_3	EQU	0x42	;
;		<o> Value+2: the third byte <0x00-0xFF>
;		<i> The third byte input register.
DG_Value_2	EQU	0xEA	;
;		<o> Value+1: second byte <0x00-0xFF>
;		<i> Second byte input register.
DG_Value_1	EQU	0x58	;
;		<o> Value+0: first byte <0x00-0xFF>
;		<i> First byte input register.
DG_Value_0	EQU	0x2F	;
;	</e>
; </h>
;  *** <<< end of configuration section >>> ***

С таким способом подстановки тестовых значений работать более удобно.



Отладочный код почти не изменился:

IF (DG_EnInVar = 1)				; Отладочные значения регистров
	MOV		AV_Value3, #DG_Value_3	; 4 байт
	MOV		AV_Value2, #DG_Value_2	; 3 байт
	MOV		AV_Value1, #DG_Value_1	; 2 байт
	MOV		AV_Value0, #DG_Value_0	; 1 байт
ELSE
;-- Перенос данных измерений из входных регистров в рабочие ------------------;
	MOV		R0, #AV_Value		; Начальный адрес
	MOV		@R0, SBUF		; 1 байт
	INC		R0			;
	MOV		@R0, TL0		; 2 байт
	INC		R0			;
	MOV		@R0, TH0		; 3 байт
	INC		R0			;
	MOV		@R0, AV_OVF_F		; 4 байт
ENDIF

  Однако главной задачи — значительно ускорить отладку, он не решает. Все равно требуется перекомпиляция проекта, а хотелось бы от нее избавиться. Вот здесь и может помочь скриптовый отладчик.
  Он изменяет значения переменных внутри самой программы, поэтому перекомпиляции при изменении тестовых значений не требуется. Изменив тестовые значения через скриптовый отладчик, можно тут же продолжать работать далее. Отладка значительно облегчается и убыстряется.
  Сначала уберем из программы старый отладочный код с условной компиляцией и создадим для отладчика собственную функцию, которая и будет перезаписывать тестовые значения.
  Находясь в режиме отладки из меню Debug -> Function Editor (Open Ini File)... вызываем редактор функций. Он сразу откроет диалог открытия INI файла. Т.к. мы его только создаем, закроем диалог, нажмем кнопку New и Save as, сохраним под нужным именем (например Input.ini) в каталоге программы, и затем заново откроем его в редакторе функций. Теперь можно ввести свою функцию. Применительно к моему случаю у меня получилось так:

FUNC void Transfer (void) {
	unsigned long int val;
	val = getlong("Value, dec or hex(0x12345678)");
	if (val == 0)
		_break_ = 1;
	else {
          	AV_Value3 = val>>24;
          	AV_Value2 = val>>16&0xff;
          	AV_Value1 = val>>8&0xff;
          	AV_Value0 = val&0xff;
          	printf("Value (dec) = %lu.\n", val);
	}
}

Краткий комментарий:
  • определим временную переменную типа DWord;
  • с помощью встроенной функции getlong присвоим ей значение (введенное вручную число). Функция getlong вызывает стандартный InputBox в который нужно ввести значение в десятичном (по умолчанию) или шестнадцатиричном виде. Это значение (или 0, если число невалидно) и возвращается в качестве результата getlong (присваивается временной переменной);
  • предусмотрим закрытие модального окна ввода с остановом программы при нулевом вводе;
  • если ввод правильный далее DWord разделяется побайтно и присваивается входному буферу. Все символьные выражения программы, доступные отладчику можно увидеть набрав DIR в командной строке отладчика. В более развернутом виде их также можно посмотреть в окне View -> Symbols Window
  • последней для контроля введенного значения вызывается стандартная функция printf, она выведет в окно отладчика введенное число в десятичном виде;
Сохраним файл Save и скомпилируем его Compile, убедимся что ошибок нет, в строке Compile Errors — compilation complete, no errors. В командном окне отладчика также не должно быть сообщений об ошибке.
Наша функция для отладчика готова, теперь осталось ее подключить. Сделать это можно двумя способами;
  1. В командной строке отладчика ввести INCLUDE Input.ini
  2. Ввести название файла в строке Initialization File на странице Debug настроек проекта.
  Ну вот, функцию мы написали, к отладчику подключили, и как теперь ее использовать? Для пошаговой отладки лучше назначить ее на кнопку. Тогда использовать ее можно будет в любой момент.
Введем в командной строке отладчика DEFINE BUTTON «Input», «Transfer()». Этим мы создаем на ToolBox кнопку Input,



при нажатии она вызовет функцию Transfer().



  На ToolBox всегда присутствует на одну кнопку больше, чем мы создали. Самая первая кнопка Update Windows создается автоматически и присутствует всегда. Нажатие на нее приводит к обновлению значений во всех окнах просмотра.
  ToolBox имеет диалоговое окно которое располагается поверх всех других окон и иногда это мешает. Его можно закрыть, тогда доступ к кнопкам можно получить из меню, нажав стрелку вниз рядом с кнопкой ToolBox.



Начинаем пошаговую отладку с выявлением багов.

  Ну вот, код программы мы почистили, и от багов как нам кажется избавились. Как теперь перейти от пошаговой отладки к более производительной проверке? Можно воспользоваться возможностью привязать срабатывание скриптов-функций к точкам останова.
  На самом деле точка останова не обязательно останавливает программу. Это делают только стандартные точки, устанавливаемые мышкой. Они достигнув точки останова, вызывают стандартный обработчик, который и останавливает выполнение программы. Мы же можем определить свой обработчик, и он отработав требуемые действия, продолжит выполнение программы. Остановить программу можно и в этом случае, надо внутри функции-обработчика присвоить единицу внутренней переменной _break_.
  Мне показалось удобным вывести полученный результат BCD преобразования в отладочную консоль. В основном коде выходной буфер BCD преобразования описан так:

AV_BCDBuffer:	DS	10	        ; Буфер неупакованных BCD (разрядов)
AV_BCDBuffer9	DATA	AV_BCDBuffer+9	; for debug
AV_BCDBuffer8	DATA	AV_BCDBuffer+8	; for debug
AV_BCDBuffer7	DATA	AV_BCDBuffer+7	; for debug
AV_BCDBuffer6	DATA	AV_BCDBuffer+6	; for debug
AV_BCDBuffer5	DATA	AV_BCDBuffer+5	; for debug
AV_BCDBuffer4	DATA	AV_BCDBuffer+4	; for debug
AV_BCDBuffer3	DATA	AV_BCDBuffer+3	; for debug
AV_BCDBuffer2	DATA	AV_BCDBuffer+2	; for debug
AV_BCDBuffer1	DATA	AV_BCDBuffer+1	; for debug
AV_BCDBuffer0	DATA	AV_BCDBuffer+0	; for debug

Функция вывода простая, выводит содержимое BCD буфера в виде 10-разрядного десятичного числа:

FUNC void BCD (void) {
	printf("BCD = %d%d%d%d%d%d%d%d%d%d\n", AV_BCDBuffer9, AV_BCDBuffer8,
		AV_BCDBuffer7, AV_BCDBuffer6, AV_BCDBuffer5, AV_BCDBuffer4,
		AV_BCDBuffer3, AV_BCDBuffer2, AV_BCDBuffer1, AV_BCDBuffer0);
}

Т.к. введенное число приводится к десятичному виду и результат BCD преобразования — десятичное число, ввод и результат должны совпасть.
Контрольные функции готовы, осталось эти функции назначить точкам останова. Выбираем строку в программе где ввод уже должен быть произведен и ставим мышкой точку останова. Затем вызываем окно свойств (Debug -> Breakpoints...) и редактируем, присвоив свою функцию. Здесь в качестве выражения я указал адрес в коде из старой точки останова.



Keil спросит надо ли заменить старую точку останова, подтверждаем.



В окне редактирования показано, что функция установлена.



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



Опять подтверждаем.



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



  Открываем окно командной строки отладчика, если в нем есть какие либо записи очищаем его (правая кнопка -> Clear Window). Все готово для контрольной отладки, запускаем программу на непрерывное выполнение кнопкой Run. Программа запросит число, введем его, программа сделает бесконечный цикл и снова запросит число, а мы в это время можем оценить правильность работы, сравнив числа в командном окне. Продолжать можно до тех пор пока не убедитесь в правильности работы, или не увидите ошибочный вывод.
Вот например я ввел максимальное 32-битное число 0xFFFFFFFF и получил такой вывод отладчика:



Числа не совпадают, надо продолжать искать ошибки.
  Выйти из бесконечного цикла можно введя 0 или пустую строку. Если нашли ошибку, снова введите контрольное значение кнопкой ввода на ToolBox и продолжайте пошаговую отладку. На время пошаговой отладки точки останова можно временно отключить зайдя в их свойства и сняв галки с нужных (ненужных). Если вы убедились в правильной работе программы, можно выйти из отладки. При этом учтите, привязка функций к точкам останова сохраняется только пока вы в отладчике. Если выйти из отладчика и снова зайти, точки останова привязанные функции потеряют.
  Есть способ привязать точки останова «навечно» при отладке. Во время замены старых точек в окне отладчика появляются соответствующие команды (BreakSet). Для данного случая это BS C:0x037B,1,«Transfer()» и BS \413,1,«BCD()». Эти команды можно перенести в INI файл. При входе в отладчик, он каждый раз заново перекомпилирует подключаемый файл, точки будут подключены автоматом. Я не рекомендую так делать…
Такое подключение возможно только если вы не редактируете код программы (с последующей перекомпиляцией).
  • вы временно закомментировали строку кода в программе. Из нумерации строк кода она пропала и ваша первая точка (C:0x037B) может стоять уже не там, где вы ее поставили;
  • вы добавили/удалили комментарий (пробел) между строками в программе, код не изменился, первая точка на месте, зато вторая может указывать уже не на ту строку, где ставилась;
  • вы добавили в программу команду, в этом случае могут уехать обе точки;
Легче заново связать их с функциями, чем вылавливать такие глюки.
  Скриптовый отладчик может работать только со встроенным симулятором Keil. Связано это с тем, что он должен иметь полный доступ к внутренней области памяти отлаживаемой программы. При применении внешнего симулятора, например Proteus VSM Simulator, отладочные файлы грузятся в него и это условие не выполняется. Внешний JTAG отладчик выполняет отладку в среде Keil, и хотя и не использует внутренний симулятор, может теоретически работать со скриптовым отладчиком, нужно проверять по документации или экспериментально.
  Толчком к освоению такого способа отладки послужила статья Особенности отладки программ для микроконтроллеров семейства 8051 в среде Keil uVision.
  P.S. Если вывод в консоль слишком длинный, или его хочется просмотреть не спеша, позже, можно продублировать его в log файл. Перед началом вывода наберите в консоли (внесите в Ini файл) команду log > .\input.log. В каталоге отлаживаемой программы будет создан файл input.log, и весь отладочный вывод будет продублирован в него. Если файл с таким именем уже был, он перезапишется. Если хочется добавить вывод в конец существующего, надо не создать, а открыть файл командой log >> .\input.log. Если такого файла не было, он будет создан. Закрывается log файл командой log off.
  • +5
  • 10 февраля 2016, 19:15
  • anakost

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

RSS свернуть / развернуть
Мы для подобных целей (вернее для автоматизации тестирования) использовали скрипты + GDB. Из скриптов скармливали команды на вход GDB, анализировали вывод GDB. Пришлось немного повозится, но в результате получился отличный механизм для автоматизации тестов.
0
  • avatar
  • e_mc2
  • 10 февраля 2016, 22:31
Для анализа вывода отладчика, можно продублировать его поток в лог файл. А там уже можно прикрутить любой парсер…
Добавил в статью.
0
Для анализа вывода отладчика, можно продублировать его поток в лог файл

Можно, но в случае связки скрипты + GDB в этом нет смысла. GDB запускался как дочерний процесс, его stdin/stdout перехватывались через pipe без промежуточных файлов.
0
Ну кто же сомневается в том, что GNU Debugger гораздо более развитый отладчик, чем Keil.
0
Да я не говорю, что он более (или менне) развитый. Дело не в отладчике и не в Keil vs GNU.

Я просто говорю, что если вы хотите подключится к GDB – то лучше связаться пайпами с дочерним процессом, без промежуточного файла.
0
Да, я для стм32 через Ini-файл тоже кнопочки делал иногда или скрипт для подключения к рабочему устройству без перезагрузки и перепрошивки, почему-то кейл такое не позволяет без определённой команды в окне команд. Память надо было так дампить, чтоб можно было устройство записью этой памяти и регистров привести к нужному зависшему состоянию если что, переменные прописывать и всякое такое. Пригождается редко, но бывает полезным.
0
Тяжко читать, имхо. Но все равно плюсую, наверняка пригодится
0
Просто только кошки…
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.