Атомарные регистры GPIO
Ранее мы смотрели только на регистр GPIO_OUT. В нём хранится полное 32-битное выходное значение для всех выводов GPIO. Но на практике библиотека rp-hal не пишет в GPIO_OUT напрямую. Вместо этого она использует атомарные вспомогательные регистры: GPIO_OUT_SET, GPIO_OUT_CLR и GPIO_OUT_XOR.
Эти атомарные регистры — регистры только для записи внутри блока SIO, которые сами по себе не хранят значений. Когда вы записываете в них, биты, которые вы установили, используются для изменения базового регистра GPIO_OUT:
- GPIO_OUT_SET устанавливает указанные биты в 1. Этот регистр находится по адресу
0xd0000018согласно даташиту. - GPIO_OUT_CLR сбрасывает указанные биты в 0. Этот регистр находится по адресу
0xd0000020согласно даташиту. - GPIO_OUT_XOR инвертирует указанные биты.
Изменяются только те биты, которые вы записали как 1. Все остальные биты остаются нетронутыми. Это делает код безопаснее и предотвращает случайные изменения других выводов.
Например, чтобы управлять GPIO25:
-
Чтобы установить GPIO25 в высокий уровень, нужно записать 1 в бит 25 регистра GPIO_OUT_SET. Значение GPIO_OUT_SET будет равно
0b00000010_00000000_00000000_00000000(или в шестнадцатеричном виде0x02000000). -
Чтобы установить GPIO25 в низкий уровень, нужно записать 1 в бит 25 регистра GPIO_OUT_CLR. Значение GPIO_OUT_CLR будет равно
0b00000010_00000000_00000000_00000000(или в шестнадцатеричном виде0x02000000).
Эти операции меняют только бит 25 в GPIO_OUT, оставляя все остальные биты без изменений.
Внутри rp-hal: установка вывода в высокий или низкий уровень
Если проследить, что делают set_high() и set_low() внутри rp-hal, видно, что они никогда не пишут в GPIO_OUT напрямую. Вместо этого они пишут в атомарные регистры GPIO_OUT_SET и GPIO_OUT_CLR.
Код внутри rp-hal выглядит так:
#![allow(unused)]
fn main() {
#[inline]
pub(crate) fn _set_low(&mut self) {
let mask = self.id.mask();
self.id.sio_out_clr().write(|w| unsafe { w.bits(mask) });
}
#[inline]
pub(crate) fn _set_high(&mut self) {
let mask = self.id.mask();
self.id.sio_out_set().write(|w| unsafe { w.bits(mask) });
}
}
Когда выполняются эти функции write(), в итоге вызывается core::ptr::write_volatile(). write_volatile выполняет некоторые предварительные проверки, а затем внутренняя функция компилятора intrinsics::volatile_store выполняет финальную запись по адресу MMIO. Именно эта volatile-запись и меняет реальный аппаратный регистр.
Теперь посмотрим, как это выглядит при пошаговом выполнении в GDB.
Точка останова на write_volatile
Есть много способов добраться до write_volatile. Один из них — пройти по set_low() или set_high() пошагово командами stepi и nexti в GDB. Но мы пойдём более коротким путём: поставим точку останова прямо на core::ptr::write_volatile.
Нужно учесть одну вещь. Если поставить эту точку останова сразу после сброса (например, сразу после monitor reset halt), GDB остановится много раз. Это происходит потому, что write_volatile используется во многих местах при запуске. Поэтому мы не будем её ставить в самом начале.
Вместо этого выполните шаги из предыдущей главы. Когда программа остановится на первой точке останова в вашем коде, например так:
Continuing.
Thread 1 hit Breakpoint 1, 0x100002f8 in pico_debug::__cortex_m_rt_main () at src/main.rs:63
63 led_pin.set_high().unwrap();
Tip
Проверить точки останова можно командой
info break. Удалить точку останова можно командойdelete <номер>.
Теперь, когда мы прошли стартовый код, поставим точку останова на write_volatile:
(gdb) break core::ptr::write_volatile
Затем продолжим выполнение:
(gdb) continue
Вы должны увидеть примерно такой вывод:
Thread 1 received signal SIGINT, Interrupt.
rp235x_hal::gpio::eh1::{impl#1}::set_high<rp235x_hal::gpio::pin::bank0::Gpio25, rp235x_hal::gpio::pull::PullDown> (self=0x2007ffbd)
at /home/implrust/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/rp235x-hal-0.3.1/src/gpio/mod.rs:1549
1549 fn set_high(&mut self) -> Result<(), Self::Error> {
Продолжаем снова:
(gdb) continue
Теперь мы остановились внутри функции write_volatile:
Thread 1 hit Breakpoint 3, core::ptr::write_volatile<u32> (dst=0xd0000018, src=33554432)
at /home/implrust/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ub_checks.rs:76
76 if ::core::ub_checks::$kind() {
Обратили внимание на аргументы функции? Адрес назначения dst равен 0xd0000018 — это адрес регистра GPIO_OUT_SET. Значение src равно 33554432. В шестнадцатеричном виде это 0x02000000. В двоичном виде это 0b00000010_00000000_00000000_00000000. Это точная битовая маска для GPIO25.
Посмотрим на дизассемблирование функции, чтобы увидеть, что происходит на уровне ассемблера:
(gdb) disas
Dump of assembler code for function _ZN4core3ptr14write_volatile17hc4948e781ca030f6E:
0x10008084 <+0>: push {r7, lr}
0x10008086 <+2>: mov r7, sp
0x10008088 <+4>: sub sp, #24
0x1000808a <+6>: str r2, [sp, #4]
0x1000808c <+8>: str r1, [sp, #8]
0x1000808e <+10>: str r0, [sp, #12]
0x10008090 <+12>: str r0, [sp, #16]
0x10008092 <+14>: str r1, [sp, #20]
=> 0x10008094 <+16>: b.n 0x10008096 <_ZN4core3ptr14write_volatile17hc4948e781ca030f6E+18>
0x10008096 <+18>: ldr r2, [sp, #4]
0x10008098 <+20>: ldr r0, [sp, #12]
0x1000809a <+22>: movs r1, #4
0x1000809c <+24>: bl 0x100080ac <_ZN4core3ptr14write_volatile18precondition_check17h8beabfccc7ba3236E>
0x100080a0 <+28>: b.n 0x100080a2 <_ZN4core3ptr14write_volatile17hc4948e781ca030f6E+30>
0x100080a2 <+30>: ldr r0, [sp, #8]
0x100080a4 <+32>: ldr r1, [sp, #12]
0x100080a6 <+34>: str r0, [r1, #0]
0x100080a8 <+36>: add sp, #24
0x100080aa <+38>: pop {r7, pc}
End of assembler dump.
Ключевая инструкция находится по адресу 0x100080a6. Это строка, которая фактически записывает в аппаратный регистр. В этот момент r1 содержит адрес GPIO_OUT_SET, а r0 — значение, которое будет записано.
Посмотрим поближе. Поставим ещё одну точку останова прямо на эту инструкцию:
(gdb) break *0x100080a6
Затем продолжим:
(gdb) continue
Если вас прервали, продолжите снова:
Thread 1 received signal SIGINT, Interrupt.
core::ptr::write_volatile<u32> (dst=0xd0000018, src=33554432)
at /home/implrust/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ub_checks.rs:77
77 precondition_check($($arg,)*);
Продолжаем снова:
(gdb) c
Continuing.
Thread 1 hit Breakpoint 4, 0x100080a6 in core::ptr::write_volatile<u32> (dst=0xd0000018, src=33554432)
at /home/implrust/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:2201
2201 intrinsics::volatile_store(dst, src);
GDB остановится прямо на инструкции записи. Если снова выполнить disas, вы увидите стрелку, указывающую на эту строку:
...
0x100080a4 <+32>: ldr r1, [sp, #12]
=> 0x100080a6 <+34>: str r0, [r1, #0]
0x100080a8 <+36>: add sp, #24
Перед выполнением этой инструкции записи проверим значения в регистрах r0 и r1:
(gdb) i r $r0
r0 0x2000000 33554432
(gdb) i r $r1
r1 0xd0000018 3489660952
Проверим также текущее значение регистра GPIO_OUT:
(gdb) x/x 0xd0000010
0xd0000010: 0x00000000
Сейчас там одни нули. На этом этапе светодиод ещё выключен, потому что мы ещё не выполнили инструкцию записи.
Теперь сделаем один шаг вперёд:
(gdb) nexti
# или
(gdb) ni
После выполнения этой команды вы должны увидеть, как загорелся светодиод. Теперь снова проверим регистр GPIO_OUT:
(gdb) x/x 0xd0000010
0xd0000010: 0x02000000
Регистр теперь показывает 0x02000000 — это в точности битовая маска для GPIO25. Это подтверждает, что наша операция записи успешно установила вывод светодиода в высокий уровень.
Попробуйте сами
Теперь пришло время применить полученные знания на практике. Дайте программе работать, пока она не дойдёт до точки останова set_low. Затем продолжите выполнение, пока не дойдёте до функции write_volatile.
На этот раз всё будет немного иначе. Адрес назначения будет 0xd0000020 — это регистр GPIO_OUT_CLR. Как следует из названия, этот регистр используется для сброса выводов GPIO, а не для их установки.
Выполните пошаговое выполнение, как и раньше. Когда вы выполните инструкцию str, светодиод погаснет. Если после этого посмотреть регистр GPIO_OUT, он снова будет содержать одни нули. Это подтверждает, что бит для GPIO25 сброшен, и светодиод выключен.