Hatred's Log Place

DON'T PANIC!

Jan 3, 2016 - 7 minute read - programming embedded

OpenOCD, GDB и (сильно)удалённая отладка

Intro

Статью изначально опубликовал на Хабре: http://habrahabr.ru/post/274179/

Дано:

  1. устройство с ARM926E-JS (Cypress FX3) на борту;
  2. находится на другом континенте;
  3. подключено (JTAG+USB+COM) к Linux компу;
  4. на комп есть SSH доступ (и больше ничего, только SSH порт).

Проблема: устройство нужно отлаживать и писать под него код. И делать это, желательно, удобно.

Решение с использованием OpenOCD, GDB и Qt Creator, а так же описание пути к нему, под катом.

Настройка

Решений проблемы может быть много. Само быстрое и простое: запуск связки GDB+OpenOCD на удалённом компе через ssh сессию. Удобства не много, т.к. код удобнее править локально, а для отладки нужно постоянно заливать код на сервер при помощи scp или rsync.

После недолгих размышлений, приходит идея: а ведь мы можем запускать команды удалённо на сервере используя SSH: ssh user@host some-command some-arguments

Хм… А к тому же GDB может сам запустить OpenOCD в режиме конвейера (pipe) и общаться с ним. Так можно же сделать так, что бы запускался не просто OpenOCD, а удалённый, по ssh, и полученная связка уже использовалась для отладки.

К сожалению такой вариант оказался нежизнеспособным: соединение постоянно отваливалось по таймауту.

Следующая идея была: как-то поднять VPN и использовать его для подключения к любым портам на сервере, после чего запустить удалённо OpenOCD.

Но как поднять VPN, если нет никаких портов, кроме SSH? Ладно, знаем, что SSH может пробрасывать порты. Запускаем удалённо OpenOCD, пробрасываем порт… Да, чуть лучше, чем запуск в режиме конвейера. Но именно, что чуток. Для работы никак не годится.

Почти было решил бросить это дело и пользоваться самым первым, простым, надёжным, но неудобным решением, но тут набрал в гугле связку: SSH VPN. Сказать, что я был удивлён - не сказать ничего. Что бы не искать, этих двух ссылок достаточно:

После настроек, на удалённой машине появился интерфейс tap8 с адресом 192.168.100.1 и локально: интерфейс tap7 с адресом 192.168.100.2 (адреса пригодятся в дальнейшем).

Пробую запускать… О чудо! Решение оказалось рабочим! Код загружается, всё работает, точки останова ставятся. Проблема одна: медленно. И если с ожиданием обновления состояния (стектрейсы, локальные переменные и т.п.) я могу смириться, то загрузка 300 кБ elf’а занимает больше 6 минут. Локально быстрее. Значительно.

В любом случае, вот пара скриптов, которые реализуют данную схему (настройки SSH не привожу):

  1. openocd-remote - просто оболочка для запуска удалённого OpenOCD через ssh. Отмечу, что расположение файлов и директорий на локальной машине и удалённой я сделал одинаковым. В противном бы случае в этот же скрипт добавил бы препроцессинг параметров при помощи sed, что бы сделать замены. Плюс, OpenOCD у меня собран из Git и скопирован в ~/bin/openocd-git/{bin,share} (соответствующие директории). Конфиги для FX3 (о них дальше) лежат в ~/bin/openocd-git/. В ~/bin/ сделан симлинк на исполняемый файл openocd.

    #!/bin/sh
    echo shutdown | telnet host 4444
    exec ssh -TC user@host bin/openocd $@
    
  2. gdb-remote - подключается к удалённому OpenOCD, загружает код:

    #!/bin/sh
    
    gdbcfg=fx3_gdb.ini
    elf=some-code.elf
    
    cat > $gdbcfg << EOF
    set prompt (arm-gdb)
    set remotetimeout 30
    target remote 192.168.100.1:3333
    monitor halt
    monitor soft_reset_halt
    monitor adapter_khz 1000
    set endian little
    load
    EOF
    
    arm-none-eabi-gdb -x $gdbcfg $elf
    

NOTE

Команда

echo shutdown | telnet host 4444

заставит удалённый OpenOCD остановиться, что бы новый мог запуститься. К сожалению, текущая логика Qt Creator реализована так, что он уже успевает подцепиться на порт GDB (обычно он 3333) и это приводит к стабильному багу: отладка запускается ровно через раз. К счастью облом происходит почти сразу после запуска и с этим можно мириться.


Раздумья об ускорении запуска шли примерно в таком русле: простое копирование elf файла на удалённый сервер занимает секунд 10, плюс-минус. А вот бы было круто, загрузить образ на сервер и у в устройство загружать уже с него…

Штудирование документации по OpenOCD и вот оно: сам OpenOCD может загрузить код в устройство, а GDB просто подключится и даст команду на старт прошивки. Волшебная команда: load_image.

Первые эксперименты были неутешительными: загрузка проходит ОЧЕНЬ нестабильно. Код грузится, грузится быстро: 1 минута против 6 с хвостом). Но прошивка то стартует, то нет. При этом, если же в той же сессии GDB сделать load, то всё отлично запускается.

Начал искать различия. Заинтересовала последняя строчка загрузки через load:

Start address 0x40035948, load size 298456

Это навеяло залогировать после загрузки кода через load и через load_image (через OpenOCD) и перед стартом (continue) содержимое регистра $pc. И… отличие найдено: после load $pc установлен именно в этот “Start address”, тогда как после load_image в $pc остаётся что-то, в момент чего была начала загрузка. После установки $pc в правильное значение загрузка стала стабильной. Остался вопрос: магические числа не есть гуд. Но тут помогло то, что в GDB можно указать символ и будет взят его адрес. В случае FX3 этот символ: CyU3PFirmwareEntry (к слову, на локальных приложениях это будет, скорее всего, _start) и команда установки $pc превратилась в такую

set $pc = CyU3PFirmwareEntry

Кроме того, у GDB есть возможность звать команды оболочки, поэтому мы можем легко и непринуждённо при старте залить elf файл на удалённый сервер и дать команду запущенному OpenOCD загрузить его (любую команду для OpenOCD можно дать из GDB предварив её словом monitor).

Итоговый скрипт для запуска GDB:

#!/bin/sh

gdbcfg=fx3_gdb.ini
elf=some-code.elf

# Генерим конфиг для GDB
cat > $gdbcfg << EOF
set prompt (arm-gdb)
set remotetimeout 30
target remote 192.168.100.1:3333
shell scp $elf user@192.168.100.1:
monitor halt
monitor soft_reset_halt
monitor sleep 1000
monitor load_image %elf 0x00 elf
set $pc = CyU3PFirmwareEntry
EOF

arm-none-eabi-gdb -x $gdbcfg $elf

Скрипт для запуска OpenOCD остаётся таким же.

Что нам теперь нужно, что бы начать удалённую отладку:

  1. Запусить скрипт openocd-remote. Перезапускать его можно по потребностям.
  2. Остроить код и запустить gdb скриптом выше.
  3. PROFIT

А PROFIT ли? По мне, так нет. Код я пишу в Qt Creator и хочется в один клик всё это делать из него. И это делается в один клик. Достаточно:

  1. Открыть диалог настроек
  2. Выбрать Bare Metal и добавить новый GDB Server Provider с типом OpenOCD со следующими параметрами:
    1. Name: на ваше усмотрение, пусть будет FX3 Remote

    2. Startup mode: Startup in TCP/IP

    3. Host: 192.168.100.1

    4. Port: 3333

    5. Executable file: путь к openocd-remote, у меня это /home/alexd/bin/openocd-remote

    6. Root scripts directory: /home/alexd/bin/openocd-git/share/openocd/scripts - у вас может отличаться, главное помните, что умный настройщик проверяет эти директории на доступность, именно поэтому я делал одинаковое дерево на локальном компьютере и на удалённом.

    7. Configuration file: /home/alexd/bin/openocd-git/share/openocd/scripts/interface/ftdi/olimex-arm-usb-ocd-h.cfg - у меня используется отладчик Olimex ARM-USB-OCD-H, у вас может быть другой. Настройку отладчика не рассматриваю.

    8. Additional argumets: -f ~/bin/openocd-git/fx3-common.cfg -f ~/bin/openocd-git/fx3-threadx.cfg - эти скрипты я опубликую ниже.

    9. Init commands - самое интересное:

      monitor echo "Upload image..."
      shell scp %{DebuggedExecutable:NativeFilePath} user@192.168.100.1:
      monitor halt
      monitor soft_reset_halt
      monitor sleep 1000
      monitor echo "Load image..."
      monitor load_image %{DebuggedExecutable:FileName} 0x00 elf
      set $pc = CyU3PFirmwareEntry
      monitor echo "Run image..."
      

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

    10. Reset commands: monitor reset halt (оставляем по умолчанию).

  3. Затем идём в Devices, делаем Add -> Bare Metal Device -> Даём имя (пусть будет FX3 Device Remote) и назначаем наш GDB Server provider (FX3 Remote)
  4. Затем идём в Build & Runs и в используемом наборе (Kits) для Cypress (или ARM или чего у вас там) выбираем:
    1. Device type: Bare Metal Device
    2. Device: FX3 Device Remote

Тут мне пришлось создавать для набора - для локальной и для удалённой разработки. Но что ж… Пережить можно.

Всё, после чего в настройках проекта добавляем новый набор, конфигурируем его, на вкладке Run добавляем конфирурации в названии которых есть “(via GDB Server or hardware debugger)” и начинаем отладку простым нажатием F5.

Полезные материалы

Скрипты fx3-common.cfg, fx3-threadx.cfg, fx3-boot.cfg (для отладки бутлодера или когда нет ThreadX) соответственно:

  • fx3-common.cfg:

    ######################################
    # Target: CYPRESS FX3 ARM926-ejs
    # Common part
    ######################################
    if { [info exists CHIPNAME] } {
    set _CHIPNAME $CHIPNAME
    } else {
    set _CHIPNAME fx3
    }
    
    if { [info exists ENDIAN] } {
    set _ENDIAN $ENDIAN
    } else {
    set _ENDIAN little
    }
    
    if { [info exists CPUTAPID] } {
    set _CPUTAPID $CPUTAPID
    } else {
    set _CPUTAPID 0x07926069
    }
    
    #delays on reset lines
    adapter_nsrst_delay 200
    jtag_ntrst_delay 200
    
    adapter_khz 1000
    
    #reset_config trst_only
    #reset_config trst_only combined
    #reset_config trst_and_srst combined
    #reset_config trst_and_srst srst_pulls_trst
    # From the Cypress SDK
    reset_config trst_and_srst srst_pulls_trst
    # My own well worked
    #reset_config trst_only
    
    jtag newtap $_CHIPNAME cpu -irlen 4 -ircapture 0x1 -irmask 0xf -expected-id $_CPUTAPID
    jtag_rclk 3
    
  • fx3-threadx.cfg

    ######################################
    # Target: CYPRESS FX3 ARM926-ejs
    ######################################
    
    #source [find fx3-common.inc]
    
    ######################
    # Target configuration
    ######################
    set _TARGETNAME $_CHIPNAME.cpu 
    target create $_TARGETNAME arm926ejs -endian $_ENDIAN -chain-position $_TARGETNAME -rtos ThreadX
    adapter_khz 1000
    
  • fx3-boot.cfg

    ######################################
    # Target: CYPRESS FX3 ARM926-ejs
    ######################################
    
    #source [find fx3-common.inc]
    
    ######################
    # Target configuration
    ######################
    set _TARGETNAME $_CHIPNAME.cpu 
    target create $_TARGETNAME arm926ejs -endian $_ENDIAN -chain-position $_TARGETNAME 
    adapter_khz 1000
    

Тип проекта в Qt Creator для ARM, FX3 и иже с ними может быть Generic, но я написал CMake правила для FX3: https://github.com/h4tr3d/fx3-cmake и использую CMake Project manager, что позволяет легко иметь несколько конфигураций в разных директориях, теневую сборку и повод не путаться в параметрах сборки на сложных проектах.

Команды OpenOCD: http://openocd.org/doc/html/General-Commands.html

Для вычисления Entry Point автоматически, можно собрать GDB с поддержкой питон и воспользоваться рекомендациями: