Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Атомарные регистры 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 сброшен, и светодиод выключен.