Что даёт отладчик

Через SWD — Serial Wire Debug — можно записывать программу во Flash, останавливать и продолжать выполнение, читать память и регистры процессора.

Разделим названия, которые встретятся в настройках:

КомпонентРоль
STM32CubeIDEПоказывает исходники, кнопки управления и окна регистров.
GDBСопоставляет машинные адреса с функциями и строками исходного кода и управляет отладкой.
ST-LINK GDB serverПринимает команды GDB и обращается к аппаратному отладчику.
ST-LINKУстройство, которое связывает USB компьютера с отладочным портом STM32.
SWDИнтерфейс между ST-LINK и целевым микроконтроллером.
Цепочка подключения от STM32CubeIDE до STM32F401REНа компьютере STM32CubeIDE управляет GDB, GDB связывается с ST-LINK GDB server по TCP. Сервер по USB обращается к встроенному ST-LINK, который соединён с STM32F401RE по SWD.КомпьютерNUCLEO-F401RESTM32CubeIDEGDBST-LINKGDB serverST-LINKSTM32F401RETCPUSBSWDSWDIO / PA13 · SWCLK / PA14
IDE и GDB работают на компьютере. SWD начинается на участке между ST-LINK и целевым STM32. Ответы и данные проходят по той же цепочке обратно.

STM32CubeIDE запускает ST-LINK GDB server при старте отладочной сессии. Устройство цепочки и команды сервера описаны в UM2576.

Подключаем NUCLEO-F401RE

Подключаем USB-кабель с передачей данных к разъёму встроенного ST-LINK/V2-1, обозначенному на плате MB1136 как CN1. Устанавливаем обе перемычки CN2: они соединяют ST-LINK с целевым STM32. Расположение разъёма и перемычек сверяем с UM1724 для своей ревизии платы.

Название «двухпроводный интерфейс» относится к двум сигнальным линиям SWD. При внешнем подключении понадобятся и другие соединения:

СигналНа STM32F401REНазначение
SWDIOPA13Двунаправленная передача команд и данных.
SWCLKPA14Тактовый сигнал от ST-LINK.
GNDОбщая земляОбщая точка отсчёта уровней сигналов.
VTref / VDD_TARGETПитание целевого MCUВход опорного напряжения цели на отладчике.
NRSTВход сбросаАппаратный reset и подключение под сбросом.

На Nucleo эти соединения выполнены на плате. При подключении внешнего отладчика схему и допустимые напряжения сверяем с его документацией.

После сброса PA13 и PA14 готовы к работе через SWD. Назначение выводов и их состояние после сброса приведены в RM0368, разделах GPIO и Debug support.

Минимальная программа для первого опыта

Открываем проект f401_swd_debug.

f401_swd_debug/
  CMakeLists.txt
  CMakePresets.json
  cubeide-gcc.cmake
  STM32F401RETX_FLASH.ld
  Startup/startup_stm32f401retx.s
  Sources/main.cpp
  .project
  .cproject
  f401_swd_debug.launch
  README.md

Весь прикладной код из Sources/main.cpp:

#include "stm32f4xx.h"

int main()
{
    for (;;) {
        __NOP();
    }
}

__NOP() — инструкция ядра No Operation («пустая операция»), доступная через CMSIS. На ней удобно поставить точку останова и наблюдать адрес выполняемой инструкции. После неё процессор выполняет переход назад и снова проходит цикл.

До main() выполняется startup-код: подготавливает память программы, вызывает SystemInit() и инициализацию среды C/C++. Поэтому остановка в main() подтверждает, что процессор уже прошёл начальную часть запуска.

Собираем ELF с отладочной информацией

Нужны CMake, Ninja и ARM GCC. Для формата CMakePresets.json версии 3 нужен CMake 3.21 или новее — соответствие версий приведено в документации CMake Presets. Каталоги с ninja, arm-none-eabi-gcc и arm-none-eabi-g++ должны находиться в PATH текущего терминала. ARM GCC и Ninja можно взять из установленного STM32CubeIDE. Загрузка подмодулей описана в корневом README репозитория.

Из корня репозитория выполняем:

cd f401_swd_debug
cmake --preset Debug
cmake --build --preset Debug

Получаем f401_swd_debug/Debug/f401_swd_debug.elf. Имя CMake target — f401_swd_debug. В ELF находятся машинный код, адреса секций, имена символов и отладочная информация, связывающая инструкции со строками исходника. GDB использует эти сведения для остановки в main() и показа текущей строки.

В пресете Debug заданы -O0 -g3: -O0 отключает оптимизацию для удобства пошаговой отладки, а -g3 добавляет отладочную информацию с определениями макросов. В оптимизированной сборке несколько строк могут соответствовать одной инструкции, некоторые переменные исчезают, а порядок шагов становится менее очевидным.

Настраиваем запуск в STM32CubeIDE

В STM32CubeIDE на Eclipse импортируем проект и открываем сохранённую конфигурацию f401_swd_debug.launch:

  1. Выбираем File → Import → General → Existing Projects into Workspace.
  2. Указываем каталог f401_swd_debug. Оставляем Copy projects into workspace выключенным: рядом с проектом должен оставаться общий каталог Libraries.
  3. После сборки в терминале обновляем проект командой Refresh. Открываем Run → Debug Configurations → STM32 C/C++ Application → f401_swd_debug.
  4. Сверяем поля по таблице. Если сохранённая конфигурация не появилась, создаём новую STM32 C/C++ Application с этими значениями.
ВкладкаПолеЗначение для опыта
MainProjectf401_swd_debug
MainC/C++ ApplicationDebug/f401_swd_debug.elf
MainBuild before launchОтключено: сначала явно собираем пресетом Debug в терминале.
DebuggerDebug probeST-LINK (ST-LINK GDB server)
DebuggerInterfaceSWD
DebuggerFrequencyAuto; при проблемах снижаем до поддерживаемой частоты.
DebuggerReset behaviourConnect under reset; требуется связь NRST.
DebuggerGDB serverЛокальный автоматический запуск; порт 61234.
StartupLoad Image and SymbolsЗагрузка образа и символов из ELF этого проекта включена.
StartupSet breakpoint at / Stop atmain
StartupResumeВключено, чтобы после загрузки дойти до остановки в main.

После каждой правки кода выполняем обе команды CMake, затем запускаем отладку кнопкой Debug.

Назначение вкладок Main, Debugger и Startup описано в UM2609, раздел Debug.

Останавливаемся в main и находим PC и SP

Нажимаем Debug и при предложении переключаемся в перспективу Debug. В консоли должны появиться сообщения запуска GDB server, подключения ST-LINK и загрузки программы. После этого процессор должен остановиться в main(). В окне Debug выбираем верхний кадр текущего стека вызовов.

Подсвеченная строка обозначает место, к которому отладчик относит текущий адрес исполнения. Обычно соответствующая инструкция ещё не выполнена. Одна строка C++ может включать несколько машинных инструкций, поэтому точный адрес смотрим в окне Disassembly.

Открываем Window → Show View → Registers и находим два регистра:

  • PC, R15 — Program Counter. В остановленной сессии GDB показывает адрес инструкции, с которой продолжится выполнение. В нашем проекте код main() находится во Flash, в области адресов 0x0800….
  • SP, R13 — Stack Pointer. Указывает на текущую вершину стека. В этой программе используется основной стек MSP в SRAM, в области 0x2000…. После входа в функцию SP может отличаться от начального адреса стека: пролог функции уже мог сохранить регистры.

Точные адреса зависят от компилятора и компоновки. Проверяем, что PC указывает на код программы, а SP — на память стека. В GDB console выполняем:

info registers pc sp
x/i $pc

Первая команда показывает значения регистров, вторая — инструкцию по адресу PC. Команды вводим в консоль GDB. Синтаксис регистров $pc и $sp описан в руководстве GDB.

Читаем CPUID

В окне Expressions добавляем SCB->CPUID и выбираем шестнадцатеричный формат. SCB — описание System Control Block из CMSIS, а CPUID — его регистр с информацией о ядре.

Если вычислитель выражений не раскрывает макрос SCB, читаем тот же регистр по адресу. В Expressions можно ввести *(volatile unsigned int *)0xE000ED00, а в GDB console:

x/1wx 0xE000ED00

Здесь x означает просмотр памяти, 1 — один элемент, w — 32-битное слово, x — шестнадцатеричное представление. Адрес соответствует SCB->CPUID в используемом CMSIS.

В PM0214, раздел CPUID base register приведено значение 0x410FC241. Разберём его как пример:

БитыПолеЗначение примера
31:24Implementer0x41 — Arm.
23:20Variant0 — вариант ядра r0.
19:16Архитектурное поле0xF.
15:4PartNo0xC24 — Cortex-M4.
3:0Revision1 — ревизия p1; вместе с Variant получается r0p1.

Записываем значение со своей платы и определяем по таблице производителя, тип и ревизию ядра.

Точка останова и пошаговое выполнение

Ставим breakpoint на строке __NOP(); двойным щелчком по левому полю редактора и нажимаем Resume. Процессор продолжит выполнение и остановится, когда дойдёт до этой строки. Точка останова приостанавливает выполнение перед соответствующей инструкцией, чтобы можно было исследовать состояние программы.

Если после Resume процессор сразу снова останавливается на той же строке, короткий цикл уже успел вернуться к breakpoint.

КомандаЧто наблюдаем
ResumeВыполнение до следующей точки останова или другого события остановки.
Suspend / PauseОстановка по запросу пользователя в текущем месте выполнения.
Step IntoШаг по исходному коду с входом в вызываемую функцию, если это возможно.
Step OverШаг по исходному коду с выполнением вызываемой функции целиком.
Instruction stepping / stepiВыполнение одной машинной инструкции.

__NOP() раскрывается в инструкцию nop, поэтому Step Into и Step Over здесь дают одинаковый результат. Несколько инструкций могут относиться к одной строке: при шаге подсветка иногда остаётся на месте. Продвижение проверяем по PC и Disassembly.

Убираем breakpoint на __NOP(), открываем Disassembly и находим main. Включаем шаг по инструкциям или выполняем в GDB console:

disassemble /m main
x/6i $pc
stepi
x/i $pc

Ищем инструкцию nop и переход b к началу тела цикла. Между ними компилятор может добавить инструкции, связанные со строками исходника. После шага через nop PC переместится к следующей инструкции, после шага через переход — вернётся назад. Это и есть бесконечный цикл на уровне машинного кода.

Нажимаем Resume, затем Suspend. Отладчик остановит процессор в одной из инструкций цикла.

Сброс, продолжение и завершение сессии

Resume продолжает программу с текущего состояния. Reset запускает последовательность старта заново. Terminate завершает отладочную сессию.

В подготовленной launch-конфигурации действие Reset использует Software system reset и затем запускает выполнение. Чтобы после него снова попасть в main(), заранее ставим там обычный breakpoint: начальная остановка в main при запуске сессии может быть временной и уже удалённой.

  1. Приостанавливаем выполнение и ставим постоянный breakpoint в main().
  2. Выполняем Reset с программным сбросом системы. Если выбранное действие IDE оставило процессор остановленным в startup-коде, нажимаем Resume.
  3. Проверяем повторную остановку в main() и снова читаем PC, SP и CPUID.

Для ST-LINK GDB server эквивалентную последовательность можно задать в консоли GDB:

break main
monitor reset
continue

monitor reset выполняет системный сброс ядра и периферии. Команда continue продолжает запуск записанной во Flash программы. Команда сброса описана в UM2576, Monitor commands.

Завершаем сессию командой Terminate. Для самостоятельного запуска прошивки нажимаем аппаратную кнопку RESET на плате.

Если сессия не запускается

Читаем первое сообщение об ошибке в консоли сервера и выбираем проверку по таблице:

СимптомЧто проверитьСледующее действие
ST-LINK не обнаруженUSB-кабель, разъём ST-LINK, питание и наличие устройства в ОС.Попробовать кабель с передачей данных; проверить драйвер ST-LINK, если ОС не распознаёт устройство.
ST-LINK виден, целевой STM32 не найденПитание MCU, обе перемычки CN2, выбор SWD; при внешнем подключении — GND и VTref.Восстановить штатное подключение по UM1724 и повторить соединение.
Связь нестабильнаЧастоту SWD и качество соединений.Вместо Auto выбрать меньшую поддерживаемую частоту, например 950 кГц, если она доступна; укоротить внешние провода.
Прошивка быстро делает SWD недоступнымПеренастройку PA13/PA14 или переход в сон сразу после запуска.Выбрать Connect under reset при подключённом NRST.
ELF не найденProject, путь C/C++ Application и результат сборки.Собрать Debug, обновить проект и выбрать Debug/f401_swd_debug.elf.
Нет исходника или breakpoint не соответствует строкеСовпадение ELF с прошивкой, наличие символов, выбор Debug.Пересобрать и заново загрузить образ; проверить Disassembly.
Отладчик или порт занятДругую сессию IDE, GDB server или STM32CubeProgrammer.Завершить прежнее подключение и повторить запуск одной сессии.
SCB не распознаётся в ExpressionsЗагрузку символов и поддержку макросов вычислителем.Прочитать CPUID командой x/1wx 0xE000ED00.

Что должно получиться

Опыт завершён, если получилось загрузить ELF именно этого проекта, остановиться в main(), прочитать PC, SP и CPUID, увидеть nop с обратным переходом в Disassembly и снова попасть в main() после reset.

В следующей статье «SysTick: от задержки к прерыванию» настроим мигание LD2 по событию системного таймера. Поставим breakpoint в обработчике и увидим, как процессор переходит в него из основного цикла.

Исходный код и материалы

Проект: Stm32Course2026/f401_swd_debug. Команды сборки, последовательность опыта и статус проверки — в README проекта.

Дополнительное чтение: Carmine Noviello, Mastering STM32, Second Edition, версия от 28.02.2022: §5.1–5.2, с. 117–124; регистры ядра — §1.1.1.1, с. 4–7.