RTRM: читаем справочное руководство
Мы уже встречались с выводами GPIO на nRF52833. В этом чипе (как и во многих других) выводы GPIO сгруппированы в порты. Есть два порта, Port 0 и Port 1, их сокращённо обозначают P0 и P1. Выводы внутри каждого порта нумеруются с нуля. В порту 0 есть 32 вывода, от P0.00 до P0.31, а в порту 1 — 10 выводов, от P1.00 до P1.09.
Первое, что нужно узнать, — какой вывод к какому светодиоду подключён. Раньше мы делали это, прослеживая схему. Но это сложный путь: нужная информация есть в таблице выводов MB2.
В таблице сказано:
ROW1, верхний ряд светодиодов, подключён к выводуP0.21.P0.21— это сокращение от «вывод 21 на порту 0».ROW5, нижний ряд светодиодов, подключён к выводуP0.19.
Итак, мы знаем, что нужно менять состояние выводов P0.21 и P0.19, чтобы включать и выключать верхний и нижний ряды. Эти выводы относятся к порту 0, поэтому для их настройки используем периферию P0.
С каждой периферией связан блок регистров. Блок регистров — это набор регистров, расположенных в непрерывной области памяти. Адрес, с которого начинается блок регистров, называется базовым адресом. Нам нужно узнать базовый адрес периферии P0. Эта информация есть в следующем разделе спецификации продукта микроконтроллера:
Раздел 4.2.4 «Instantiation» — стр. 22
В таблице сказано, что базовый адрес блока регистров P0 — 0x5000_0000.
У каждой периферии есть собственный раздел в документации. Каждый такой раздел заканчивается таблицей регистров, которые входят в блок регистров периферии. Для семейства периферии GPIO эта таблица находится в:
Раздел 6.8.2 «Registers» — стр. 144
OUT — это регистр, который мы будем использовать для установки и сброса. Его смещение от базового адреса P0 равно 0x504. Регистр OUT можно найти в спецификации продукта.
Описание этого регистра находится сразу под таблицей регистров GPIO:
Подраздел 6.8.2.1 «OUT» — стр. 145
Итак, 0x5000_0000 + 0x504 = 0x50000504. Знакомый адрес! Наконец-то!
Это тот самый регистр, в который мы писали. Документация сообщает кое-что интересное. Во-первых, в этот регистр можно и записывать, и читать из него. Во-вторых, регистр — это 32-битная область памяти, и каждый бит отражает состояние соответствующего вывода: например, бит 19 соответствует выводу 19. Установка бита в 1 включает выход соответствующего вывода, а установка в 0 — выключает его. Кроме того, видно, что по умолчанию все выходы отключены, так как значение всех битов после сброса — 0.
Мы будем использовать команду GDB examine: x. В зависимости от настроек GDB-сервера GDB может отказаться читать память, которая не описана. Это поведение можно отключить так:
set mem inaccessible-by-default off
Итак, начнём. Сначала отключим флаг inaccessible-by-default, затем поставим несколько точек останова, сбросим устройство и остановим его.
(gdb) set mem inaccessible-by-default off
(gdb) break 16
Breakpoint 1 at 0x172: file src/07-registers/src/main.rs, line 16.
Note: automatically using hardware breakpoints for read-only addresses.
(gdb) break 19
Breakpoint 2 at 0x17c: file src/07-registers/src/main.rs, line 19.
(gdb) break 22
Breakpoint 3 at 0x184: file src/07-registers/src/main.rs, line 22.
(gdb) break 25
Breakpoint 4 at 0x18c: file src/07-registers/src/main.rs, line 25.
(gdb) monitor reset halt
Resetting and halting target
Target halted
Хорошо. Продолжим до первой точки останова, прямо перед строкой 16, и выведем содержимое регистра по адресу 0x50000504.
(gdb) c
Continuing.
Breakpoint 1, registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:16
16 *(PORT_P0_OUT as *mut u32) |= 1 << 21;
(gdb) x 0x50000504
0x50000504: 0x00000000
Итак, на этом этапе значение регистра — 0x00000000, то есть 0. Это совпадает с данными спецификации продукта, где сказано, что 0 — «значение после сброса» (reset value) этого регистра. Значит, после сброса MCU в регистре будет 0.
Продолжим. Эта строка состоит из нескольких инструкций (чтение, побитовое ИЛИ и запись), поэтому нужно несколько раз давать отладчику команду продолжить выполнение, пока не дойдём до следующей точки останова.
(gdb) c
Continuing.
Program received signal SIGINT, Interrupt.
0x00000174 in registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:16
16 *(PORT_P0_OUT as *mut u32) |= 1 << 21;
(gdb) c
Continuing.
Breakpoint 2, registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:19
19 *(PORT_P0_OUT as *mut u32) |= 1 << 19;
Мы остановились прямо перед строкой 19, значит, строка 16 полностью выполнена. Посмотрим содержимое регистра OUT ещё раз:
(gdb) x 0x50000504
0x50000504: 0x00200000
Значение регистра OUT на этом этапе — 0x00200000, то есть 2097152 в десятичной записи, или 2^21. Значит, бит 21 установлен в 1, а остальные — в 0. Это соответствует строке 16, где в регистр OUT записывается 1 << 21, то есть единица, сдвинутая влево на 21 позицию, объединённая побитовым ИЛИ с текущим значением OUT (которое было 0).
Запись 1 << 21 (OUT[21] = 1) в OUT выставляет P0.21 в высокий уровень. Это включает верхний ряд светодиодов. Проверьте, что верхний ряд действительно горит.
(gdb) c
Continuing.
Да, я как раз собирался это сказать. Теперь нажмите c ещё раз, чтобы продолжить выполнение до следующей точки останова, и выведите её значение.
Program received signal SIGINT, Interrupt.
0x0000017e in registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:19
19 *(PORT_P0_OUT as *mut u32) |= 1 << 19;
(gdb) c
Continuing.
Breakpoint 3, registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:22
22 *(PORT_P0_OUT as *mut u32) &= !(1 << 21);
(gdb) x 0x50000504
0x50000504: 0x00280000
В строке 19 мы установили бит 19 регистра OUT в 1, оставив бит 21 без изменений. Результат — 0x00280000, то есть 2621440 в десятичной записи, или 2^19 + 2^21: значит, оба бита, 19 и 21, установлены в 1.
Запись 1 << 19 (OUT[19] = 1) в OUT выставляет P0.19 в высокий уровень. Это включает нижний ряд светодиодов. Значит, нижний ряд должен гореть.
Следующие строки снова выключают ряды: сначала верхний, затем нижний. На этот раз используется побитовое И в сочетании с побитовым НЕ. Мы вычисляем !(1 << 21): все биты равны 1, кроме бита 21. Затем применяем побитовое И с текущим значением OUT, так что только бит 21 становится 0, а остальные биты сохраняются.
Продолжайте выполнение и проверьте, что значения регистра OUT совпадают с вашими ожиданиями. Когда устройство попадёт в бесконечный цикл в конце функции main, можно нажать CTRL+C, чтобы приостановить выполнение.
(gdb) c
Continuing.
Program received signal SIGINT, Interrupt.
0x00000186 in registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:22
22 *(PORT_P0_OUT as *mut u32) &= !(1 << 21);
(gdb) c
Continuing.
Breakpoint 4, registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:25
25 *(PORT_P0_OUT as *mut u32) &= !(1 << 19);
(gdb) x 0x50000504
0x50000504: 0x00080000
(gdb) c
Continuing.
Program received signal SIGINT, Interrupt.
0x0000018e in registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:25
25 *(PORT_P0_OUT as *mut u32) &= !(1 << 19);
(gdb) c
Continuing.
^C
Program received signal SIGINT, Interrupt.
0x00000196 in registers::__cortex_m_rt_main () at src/07-registers/src/main.rs:28
28 loop {}
(gdb) x 0x50000504
0x50000504: 0x00000000
И вот теперь все светодиоды должны снова погаснуть!