Практика использования mspdebug. Процесс становления джедаев

Продолжаем накачивать наши интеллектуальные мускулы.

Подведём итог проделанной работе по поиску неисправности. Мы определили, что в нашей программе неправильно инициализируется регистр P1DIR, и скорее всего не правильно проинциализирован сторожевой таймер. Оба бага предположительно находятся в функции init_system.

В каком файле находится эта функция? Где её искать?

Ну, поскольку наш проект крошечный, то из двух файлов, один из которых мы уже посмотрели и её там точно нет, остаётся одно — указать на другой. Это наверно в школе даунов отметили бы высокой оценкой. Но если проект состоит из нескольких десятков файлов (под сотню), то процесс поиска функции может оказаться весьма увлекательным занятием.

На помощь приходит утилита grep.

$ grep -n init_system *.[ch]

Эта утилита будет проверять все файлы, находящиеся в текущем директории, у которых имена окончиваются на «.c» или «.h» на предмет содержания в них строки «init_system». И в случае, если такая строка будет найдена, утилита выведет на экран эту строку.

Здесь опция -n говорит утилите, что каждую строку нужно сопровождать не только именем файла, в котором она нашлась, то также указывать номер строки.

mspdebugging.22

Искомая строка подсвечена красным, номер строки выделен тёмно-зелёный цветом.

Мы видим, что вообще функция init_system упоминается в проекте три раза — в трех разных файлах. В первом файле (hal.c) она определена. Во втором (hal.h) она объявлена для обращения к ней из других исходников. В третьем файле (main.c) осуществляется её вызов.

Ну, что ж, давайте откроем в текстовом редакторе файл hal.c и прокрутим его содержимое до строки 47.

// Инициализация системы
void init_system(void)
{
  init_ports();
  init_timers();
}

Ну что можно сказать? — Не густо. Не густо.

Я бы вам порекомендовал потренироваться в использовании утилиты grep — пару раз запустите её для поиска в проекте функций init_ports и init_timers.

Для тех же, кто воспринимает процесс обучения как каторгу, поднимайтесь сразу вверх по тексту. (Только не говорите потом, что Линукс — унылое гэ! Как вы относитесь к Линуксу, так и он к вам.)

Чуть выше мы видим функцию инициализации портов:

// Инициализируем порты --------------------------------------------------------
void init_ports(void)
{
  P1DIR =  LED_RED;   // Конфигурируем светодиод LED
  P1OUT = ~LED_RED;   // Выключим LED
}

Всё правильно! Забыли сконфигурировать порт для зелёного светодиода. Исправляемся:

// Инициализируем порты --------------------------------------------------------
void init_ports(void)
{
  P1DIR =   LED_GREEN + LED_RED;   // Конфигурируем порт под светодиоды
  P1OUT = ~(LED_GREEN + LED_RED);  // Изначально светодиоды должны быть погашены
}

Заодно давайте проинспектируем функцию init_timer на предмет инициализации сторожевого таймера.

// Конфигурируем таймеры -------------------------------------------------------
void init_timers(void)
{
  TACTL  = 0;      // Останавливаем таймер
  TACTL |= ID_3 + TASSEL_2 + TACLR;  //  SMCLK / 8 = 1.1 MHz / 8 = 137.5 kHz
  TACCR0 = 13749;  // Для T = 1.0 мc: К = 137500 Hz / 10 Hz - 1 = 13750 - 1

  TACTL |= MC_1;
}

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

Поскольку «собачий» таймер — это не совсем обычный таймер, то логически его нужно относить к категории модулей обеспечения работы системы, а не к категории таймеров. Названия сторожевого и обычного таймера (оба ведь называются таймерами) вносят логическую путаницу, чем искажают отражение мира в нашем сознании. А с кривым сознанием да с чрезмерными амбициями, как ни крути, всегда будет получается бесшабашный бандеростан.

Исправляем ошибку. Дописываем строку инициализации сторожевого таймера.

// Инициализация системы
void init_system(void)
{
  WDTCTL = WDTPW + WDTHOLD;  // Останавливаем собаку

  init_ports();
  init_timers();
}

Сохраняем исправленные исходники (жмём в текстовом редакторе на Ctrl-S), переходим в окно псевдотерминала (жмём Alt-Tab. Возможно вам придется не отпуская Alt нажать несколько раз на Tab) и запускаем компиляцию:

$ make

mspdebugging.23

Компиляция прошла удачно. Заливаем в камень:

$ make load

Зелёный светодиод весело заморгал. А красный как не горел, так и не горит. Продолжаем наши исследования, снова запускаем mspdebug:

$ make debug

Затем привычным движением руки загружаем таблицу символов:

(mspdebug) sym import mspdebugging.elf

И… прежде чем запустить процесс, давайте подумаем, что и как будем отлаживать.

Я предлагаю установить точку останова на обработчик прерывания timerA0_handler(). Выполняем:

(mspdebug) setbreak timerA0_handler

Затем вводим уже привычные команды сброса и пуск:

(mspdebug) reset
(mspdebug) run

И…

mspdebugging.24

… и убеждаемся, что по какой-то причине прерывание не вызывается. Зелёный светодиод нормально моргает, то есть программа выполняется, но вот прерывания от таймера TA0 почему- то не происходит.

Хорошо. Начинаем раскручивать клубок в обратную сторону. Вопрос: кто, или точнее — что, вызывает прерывание?

Прерывание возникает в результате поднятия флага прерывания. Флаг прерывания от таймера называется CCIFG и находится он в регистре TACCTL0.

Откуда это известно? Во первых это есть в документации на таймер, а во вторых можно обратиться в хэдерный файл

/usr/msp430/include/msp430f2001.h

В этом файле, к стати,  указан физический адрес регистра и номер бита этого флага. Адрес регистра TACCTL0 — 0x0162, номер бита CCIFG — 0 (нулевой).

Следует заметить, что регистр TACCTL 0 — двухбайтовый. На это косвенно намекает его адрес. Регистры периферийных модулей, адреса которых находятся в диапазоне 0x0000..0x00FF, являются байтовыми. Регистры периферийных модулей, адреса которых находятся в диапазоне 0x0100..0x01FF, являются двухбайтовыми.

mspdebugging.26

Останавливаем микроконтроллер, для этого нажимаем в отладчике комбинацию клавиш Ctrl-C.

mspdebugging.25

Это сродни «русской рулетке» — процессор может остановиться совершенно в любом месте.

Немного юмора. Есть еще техасская рулетка. Это когда делается наоборот. В техасской рулетке из полного барабана достаётся один патрон. А остальное всё как в русской рулетке…🙂

Не важно, где мы остановились. Нам важно знать взводится флаг прерывания или нет, и уже по результатам будем дальше искать причину.

mspdebugging.26

Проверка показывает, что в регистре содержится число 0x0001.

Нулевой бит этого числа находится в единичном состоянии. Значит, флаг всё-таки взведён, но при этом прерывания не возникает.

Ага! Скорее всего мы забыли разрешить прерывание от таймера! Смотрим на код инициализации таймера — ну, так оно и есть!

Прерывание от таймера TA0 разрешается установкой бита CCIE  в регистре TACCTL0. исправляем ошибку — дописываем строку в конец функции:

// Конфигурируем таймеры -------------------------------------------------------
void init_timers(void)
{
  TACTL  = 0;  // Останавливаем таймер
  TACTL |= ID_3 + TASSEL_2 + TACLR;  //  SMCLK / 8 = 1.1 MHz / 8 = 137.5 kHz
  TACCR0 = 13749;  // Для T = 1.0 мc: К = 137500 Hz / 10 Hz - 1 = 13750 - 1

  TACTL |= MC_1;
  
  TACCTL0 = CCIE;
}

Снова компилировать, снова заливать? — Не-ет! В этом нет никакого смысла. Мы можем поправить эту мелкую проблему в дебаггере и убедиться, что других ошибок в программе нет. (Ага, щаз!!!)

Мы уже знаем, что регистр TACTL0 имеет адрес 0x0162. Бит CCEI — имеет номер 4, этому биту соответствует hex-число 0x0010. В общем, нам нужно записать это значение по адресу 0x0162.

Но стоп! В этом регистре уже содержится значение 0x0001, и если мы запишем в регистр новое значение, то мы тем самым сбросим флаг прерывания. Чтобы сохранить флаг прерывания и одновременно установить разрешение прерывания, мы должны записать в регистр значение 0x0011.

Честно говоря, для данного случая это совсем не принципиально — сбросим мы случайно флаг своими неумными действиями или не сбросим. Но вообще на практике о таких тонкостях следует помнить и вести себя осторожно.

(mspdebug) mw 0x0162 11 00
(mspdebug) md 0x0162 2

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

mspdebugging.27

Теперь можем снова запустить приостановленный процесс:

(mspdebug) run

Процесс стартанёт с той точки, в которой он остановился.

Зелёный светодиод заморгал, но красный так и не загрелся.. Да и прерывание почему-то не произошло, точку останова мы ведь не «выключали»…

Хорошо! Останавливаем процессор (Ctrl-C) и проверяем состояние регистра TACCTL0:

mspdebugging.28

Флаг прерывания (CCIFG = 1)взведён, прерывание разрешено (CCIE = 1)… Почему нет прерывания?

Да потому, что мы забыли включить общий рубильник прерываний — GIE, который находится в регистре состояния SR (регистр R2).

— Ну, как же так! Семён Семёныч! (с)

Флаг GIE находится в бите 3, ему соответствует число 0x0008. Но поскольку в регистре уже содержится число 0x0001, то мы должны записать в него 0x0009. Выполняем команду записи в регистр нового значения и снова запускаем процесс:

(mspdebug) set SR 0x0009
(mspdebug) run

mspdebugging.29

На этот раз прерывание нормально отработало:

mspdebugging.30

Можно еще раз запустить процесс и убедиться, что точка останова на обработчике прерывания от таймера «работает».

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

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

В mspgcc есть специальная функция для этого дела, называется она __bis_SR_register(GIE).

int main(void)
{
  init_system();

  __bis_SR_register(GIE);

  while (1)
  {
    // Моргаем зелёным светодиодом
    P1OUT |= LED_GREEN; // Зажигаем зедёный светодиод
    delay_500ms();
    P1OUT = 0;          // Гасим зелёный светодиод
    delay_500ms();    
  }

  return 0;
}

Я обычно устанавливаю глобальный флаг разрешения прерываний в функции main(), код установки прописываю непосредственно перед бесконечным циклом. Таким образом получается, что сначала система настраивает свое оборудование, инициализирует переменные и только потом одним движением включается работа всех её подсистем.

Еще одна шутка юмора. Школа. Урок информатики ведёт физкультурник. Звенит звонок, физкультурник профессиональным движением плеча дергает рубильник — бах-х-х! Все компы разом гаснут. Ученики в голос:

— Владимир Семёнович! Мы же не сохранились! Ы-ы-ы…

Физкультурник:

— Ладно. Хрен с вами. Сохраняйтесь! — бах-х-х рубильником обратно.

Итак, давайте «выключим» точку останова и попробуем запустить микроконтроллер в свободный полёт. Точки останова «выключаются не по имени функции (не по адресу), а по их слоту (индексу) в таблице точек останов. (Вообще, таблица состоит всего их двух слотов.)

Чтобы узнать, в каком слоте прописана наша точка останова, нужно выполнить команду

(mspdebug) break

Наша точка находится в слоте под номером 0 (ноль).

Теперь выключаем её:

(mspdebug) delbreak 0

Команда delbreak без указания слота очистить оба слота.

mspdebugging.31

И снова даем команду run и любуемся независимым морганием светодиодов…

Но стоп!!!

— Что это, Ватсон? (с)

Почему красный светодиод так странно моргает? Почему длительность его вспышки изменяется?

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

Поскольку, нам предстоит ещё один серьезный заплыв в океане полном акул, я предлагаю зафиксировать наши изменения в коде программы. Для этого остановим процесс (Ctrl-C), выйдем из дебаггера (Ctrl-D), сохраним (если еще не сделали) отредактированные файлы проекта, заново откомпилируем и зальем полученный код в микроконтроллер.

В файле Makefile прописана зависимость цели load от файла mspdebugging.elf. То есть прежде чем выполнять действия по достижению цели, утилита make проверяет файл от которого зависит цель, и если потребуется, то утилита сначала убедиться, что этот файл находится в актуальном состоянии. Только после этой проверки утилита выполнит действия по достижению цели.

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

Таким образом, нам можно даже не набирать последовательно две команды — сначала make, потом make load. Мы можем сразу потребовать залить код в микроконтроллер:

$ make load

Утилита make просматривая зависимости определит, что поскольку изменились исходники, произведет сначала компиляцию исходников, потом линковку объектных модулей. В результате получится новый elf-файл, бинарный код из которого и будет залит в микроконтроллер.

У меня всё прошло без проблем. Поэтому я снова запускаю отладчик и начинаю процесс отладки.

$ make debug

После чего не забудьте загрузить в дебаггер таблицу символов.

(mspdebug) sym import mspdebugging.elf

Поскольку у нас проблемы с красным светодиодом, то будем сосредоточивать свое внимание на нём. Давайте поставим точку останова на адрес, которая меняет его состояние. Вот эта строчка в исходнике:

P1OUT = LED_RED; // Меняем состояние светодиода

Она находится в обработчике прерывания от таймера:

//==============================================================================
// Прерывание возникает с частотой 10 Гц, период 100 мс
void timerA0_handler(void) __attribute__((interrupt(TIMERA0_VECTOR)));
void timerA0_handler(void)
{
  static uint8_t t1s;
  
  if (++t1s >= 5)
  { // Сюда попадаем один раз в полсекунды
    t1s = 0;
 
    P1OUT = LED_RED;  // Меняем состояние светодиода
  }
}

В общем-то уже сейчас видно, что тут вкралась ошибка. Эта команда только зажигает красный светодиод. Но кто же тогда его гасит? Причем при зажигании красного светодиода происходит выключение зелёного светодиода. Мы ведь «внаглую» устанавливает бит LED_RED, совершенно игнорируя состояние LED_GREEN. Ай, как не красиво!

Исправляем найденную ошибку:

P1OUT |= LED_RED; // Меняем состояние светодиода

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

Есть два пути. Выполнить в дебаггере дизассемблирование участка кода, в данном случае — обработчика прерывания от таймера, и попытаться найти адрес ассеблерной команды, соответствующей команде на языке Си.

Поскольку таблица символов у нас уже подгружена мы можем задать команду так:

(mspdebug) dis timerA0_handler

В результате будет выведен целый кусок кода:

mspdebugging.32

Искомая команда находится по адресу 0x0FCA6. Как я её нашел? — Да очень просто!

Во первых это должна быть команда пересылки байта (MOV.B), ведь команда в Си-шном исходнике — это команда присвоения некоего значения некой переменной. То есть пересылка чего-то по какому-то адресу. Почему именно по адресу? — Да потому, что регистр P1OUT — это ячейка в адресном пространстве. К стати, мы даже знаем адрес этой ячейки (адрес регистра P1OUT) — он равен 0x0021.

Второй способ узнать адрес — это открыть в редакторе листинг программы и в нём найти адрес команды:

mspdebugging.33

В строке 259 находится фрагмент текста из исходника, а в строке 260 — ассемблерная команда, в которую транслировался этот код.

Нам повезло, что наша искомая команда простая. Поэтому её найти достаточно просто что в первом случае, что во втором. Очень сложно разобраться в ассемблерном коде, который получен в результате трансляции циклов for. Но это у вас еще впереди! Так что не расстраивайтесь, трудности еще будут!

К стати, команда, зажигающая светодиод — неправильная. Мы должны не зажигать, а менять его состояние. Поэтому нужно в исходнике сразу исправить команду:

    P1OUT ^= LED_RED;  // Меняем состояние светодиода

Итак. Устанавливает точку останова на адрес 0xFCA6 и запускаем программу на исполнение:

(mspdebug) setbreak 0xFCA6
(mspdebug) reset
(mspdebug) run

Программа возможно моргнет тем или другим светодиодом, или даже обоими и остановится в точке останова. Можно несколько раз дать команду run.

Вы уже заметили, что остановка программы случается всегда при погашенном красном светодиоде? Значит, однозначно его кто-то гасит! Эх-х, найти бы это место, где происходит гашение!

— Ничего. Достанем и из будущего! Не в первой! (с)

Нам известно, что изменение состояния светодиодов происходит при изменении регистра P1OUT. нам нужно отследит те места в программе, в которых происходит запись в этот регистр.

Для этого придётся выйти из дебаггера (Ctrl-D) и выполнить команду grep:

mspdebugging.34

Мы видим, что изменение регистра P1OUT происходит в проекте всего в четырех местах.

В строке 22 файла hal.c — недавно мы видели, что в этом месте состояние регистра с уже погашенным светодиодом.

В строке 46 файла hal.c — это строка инициализации. Она выполняется всего один раз. Тоже отпадает.

В строке 46 файла main.c — здесь осуществляется зажигание зелёного всетодиода. Причем, надо заметить, здесь код приписан корректно. Прежде чем установить бит зелёного светодиода, команда считывает состояние регистра P1OUT, дополняет его битом LED_GREEN и записывает полученный результат обратно. То есть состояние красного светодиода не меняется.

И наконец в строке 48 файла main.c мы видим явно, что обнуляется весь регистр. Для зелёного светодиода такое обнуление не страшно, так он что так, что эдак, всё равно должен быть погашен. А вот для красного светодиода — это смерть! Вот здесь-то мы его и гасим не смотря на то, что он должен продолжать гореть.

Ну что ж! Мы поймали и прихлопнули и этот баг!

Правим код:

...
  while (1)
  {
    // Моргаем зелёным светодиодом
    P1OUT |= LED_GREEN;  // Зажигаем зедёный светодиод
    delay_500ms();
    P1OUT &= ~LED_GREEN; // Гасим зелёный светодиод
    delay_500ms();    
  }
...

Заново компилируем и заливаем:

$ make load

Скрестив пальцы наблюдаем за поведением программы…

Кажется теперь можно кричать ура и целовать юных лаборанток!

Мы вместе с вами отладили программу. А в качестве побочнного эффекта получили не хилый гонорар — вы получили опыт работы с mspdebug.

Я поздравляю вас!

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

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s