Конечный цикл работает бесконечно

Чтобы лучше понять Rust и Arduino (Uno), я пытаюсь написать аппаратный код для Arduino на Rust. Вот очень простой пример мигания светодиода, который я попытался реализовать.

Я использовал одну библиотеку (crate) под названием avrd, которая обеспечивает отображение адресов только для микроконтроллера ATMega328P.

#![no_std]
#![no_main]
#![feature(asm_experimental_arch)]

use core::{arch::asm, hint::black_box, panic::PanicInfo};

use avrd::atmega328p;

#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

#[inline(never)]
fn delay(x: u32) {
    for _ in 0..x {
        unsafe {
            asm!("nop");
        }
    }
}

unsafe fn write_reg(reg: *mut u8, val: u8, mask: u8) {
    let reg_val = reg.read_volatile();
    reg.write_volatile((reg_val & !mask) | (val & mask));
}

#[no_mangle]
extern "C" fn main() -> ! {
    const LED_BUILTIN: u8 = 5;
    unsafe {
        let portB_data_direction = atmega328p::DDRB;
        // set it to output mode
        write_reg(portB_data_direction, 1 << LED_BUILTIN, 1 << LED_BUILTIN);
        let portB = atmega328p::PORTB;
        // switch it on, hopefully..
        loop {
            write_reg(portB, 1 << LED_BUILTIN, 1 << LED_BUILTIN);
            delay(500_0000);
            write_reg(portB, 0, 1 << LED_BUILTIN);
            delay(500_0000);
        }
    }
}

(разборка вышеупомянутого фрагмента вставлена в конец)

По какой-то причине, если значение задержки равно 2 или больше, светодиод никогда не перестаёт мигать. Думаю, проблема в функции задержки, поскольку размещение delay(2) над строкой кода, включающей светодиод, приводит к тому, что светодиод никогда не включается. Ещё одна странность возникает, если я немного изменю код следующим образом:

#[no_mangle]
extern "C" fn main() -> ! {
    const LED_BUILTIN: u8 = 5;
    unsafe {
        let portB_data_direction = atmega328p::DDRB;
        // set it to output mode
        write_reg(portB_data_direction, 1 << LED_BUILTIN, 1 << LED_BUILTIN);
        let portB = atmega328p::PORTB;
        // switch it on, hopefully..
        let mut i = 0;
        loop {
            while i < 1000000 {
                i += 1;
                write_reg(portB, 1 << LED_BUILTIN, 1 << LED_BUILTIN);
            }
            i = 0;
            while i < 1000000 {
                i += 1;
                write_reg(portB, 0, 1 << LED_BUILTIN);
            }
            i = 0;
        }
    }
}

Но на этот раз светодиод включается и выключается, но только один раз (??!!). Бесконечный цикл становится конечным и выполняется только один раз. Не уверен, что генерируемый код неверен или что-то ещё.

Вот файл .cargo/config.toml:

[build]
target = "atmega328p.json" # Plucked from https://github.com/Rahix/avr-hal/

[unstable]
build-std = ["core"]

[target.'cfg(target_arch = "avr")']
runner = "ravedude uno --baudrate 57600"

Инструментарий AVR, который использует Rust, тот же самый, что идет в комплекте с Arduino. Я не устанавливал его отдельно (упоминаю на случай, если этот инструментарий вызовет проблемы на платформах, отличных от C).

Вот дизассемблированный фрагмент кода (RUSTFLAGS="--emit asm" cargo run --release, а не окончательная связанная сборка):

    .text
.set __tmp_reg__, 0
.set __zero_reg__, 1
.set __SREG__, 63
.set __SP_H__, 62
.set __SP_L__, 61
    .file   "arduino_blink.caf25912130a4f-cgu.0"
    .section    .text._ZN13arduino_blink5delay17h9627a982856e7dadE,"ax",@progbits
    .p2align    1
    .type   _ZN13arduino_blink5delay17h9627a982856e7dadE,@function
_ZN13arduino_blink5delay17h9627a982856e7dadE:
    ldi r24, 0
    ldi r25, 0
    ldi r18, 75
    ldi r20, 76
    ldi r21, 0
    movw    r22, r24
.LBB0_1:
    ldi r19, 1
    cpi r24, 64
    cpc r25, r18
    cpc r22, r20
    cpc r23, r21
    brlo    .LBB0_3
    mov r19, r1
.LBB0_3:
    andi    r19, 1
    cpi r19, 0
    breq    .LBB0_5
    subi    r24, 255
    sbci    r25, 255
    sbci    r22, 255
    sbci    r23, 255
    ;APP
    nop
    ;NO_APP
    rjmp    .LBB0_1
.LBB0_5:
    ret
.Lfunc_end0:
    .size   _ZN13arduino_blink5delay17h9627a982856e7dadE, .Lfunc_end0-_ZN13arduino_blink5delay17h9627a982856e7dadE

    .section    .text.main,"ax",@progbits
    .globl  main
    .p2align    1
    .type   main,@function
main:
    sbi 4, 5
.LBB1_1:
    sbi 5, 5
    call    _ZN13arduino_blink5delay17h9627a982856e7dadE
    cbi 5, 5
    call    _ZN13arduino_blink5delay17h9627a982856e7dadE
    rjmp    .LBB1_1
.Lfunc_end1:
    .size   main, .Lfunc_end1-main

А вот и окончательный (связанный) дизассемблированный файл (avr-objdump -d binary-name.elf disassembly.s):

Disassembly of section .text:

00000000 <.text>:
   0:   0c 94 34 00     jmp 0x68    ;  0x68
   4:   0c 94 3e 00     jmp 0x7c    ;  0x7c
   8:   0c 94 3e 00     jmp 0x7c    ;  0x7c
   c:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  10:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  14:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  18:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  1c:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  20:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  24:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  28:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  2c:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  30:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  34:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  38:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  3c:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  40:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  44:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  48:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  4c:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  50:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  54:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  58:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  5c:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  60:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  64:   0c 94 3e 00     jmp 0x7c    ;  0x7c
  68:   11 24           eor r1, r1
  6a:   1f be           out 0x3f, r1    ; 63
  6c:   cf ef           ldi r28, 0xFF   ; 255
  6e:   d8 e0           ldi r29, 0x08   ; 8
  70:   de bf           out 0x3e, r29   ; 62
  72:   cd bf           out 0x3d, r28   ; 61
  74:   0e 94 57 00     call    0xae    ;  0xae
  78:   0c 94 5f 00     jmp 0xbe    ;  0xbe
  7c:   0c 94 00 00     jmp 0   ;  0x0
  80:   80 e0           ldi r24, 0x00   ; 0
  82:   90 e0           ldi r25, 0x00   ; 0
  84:   2b e4           ldi r18, 0x4B   ; 75
  86:   4c e4           ldi r20, 0x4C   ; 76
  88:   50 e0           ldi r21, 0x00   ; 0
  8a:   bc 01           movw    r22, r24
  8c:   31 e0           ldi r19, 0x01   ; 1
  8e:   80 34           cpi r24, 0x40   ; 64
  90:   92 07           cpc r25, r18
  92:   64 07           cpc r22, r20
  94:   75 07           cpc r23, r21
  96:   10 f0           brcs    .+4         ;  0x9c
  98:   31 2d           mov r19, r1
  9a:   31 70           andi    r19, 0x01   ; 1
  9c:   30 30           cpi r19, 0x00   ; 0
  9e:   39 f0           breq    .+14        ;  0xae
  a0:   8f 5f           subi    r24, 0xFF   ; 255
  a2:   9f 4f           sbci    r25, 0xFF   ; 255
  a4:   6f 4f           sbci    r22, 0xFF   ; 255
  a6:   7f 4f           sbci    r23, 0xFF   ; 255
  a8:   00 00           nop
  aa:   f1 cf           rjmp    .-30        ;  0x8e
  ac:   08 95           ret
  ae:   25 9a           sbi 0x04, 5 ; 4
  b0:   2d 9a           sbi 0x05, 5 ; 5
  b2:   0e 94 40 00     call    0x80    ;  0x80
  b6:   2d 98           cbi 0x05, 5 ; 5
  b8:   0e 94 40 00     call    0x80    ;  0x80
  bc:   fa cf           rjmp    .-12        ;  0xb2
  be:   f8 94           cli
  c0:   ff cf           rjmp    .-2         ;  0xc0

ДОПОЛНЕНИЕ: Возникает некоторая путаница с тулчейном и тем, кто может быть ответственен за эту проблему. Итак, вот файл atmega328p.json, который сообщает компилятору Rust, как компилировать этот бэкенд (AVR): src:atmega32p.json

{
    "arch": "avr",
    "atomic-cas": false,
    "cpu": "atmega328p",
    "data-layout": "e-P1-p:16:8-i8:8-i16:8-i32:8-i64:8-f32:8-f64:8-n8-a:8",
    "eh-frame-header": false,
    "exe-suffix": ".elf",
    "late-link-args": {
        "gcc": [
            "-lgcc"
        ]
    },
    "linker": "avr-gcc",
    "llvm-target": "avr-unknown-unknown",
    "max-atomic-width": 8,
    "no-default-libraries": false,
    "pre-link-args": {
        "gcc": [
            "-mmcu=atmega328p"
        ]
    },
    "relocation-model": "static",
    "target-c-int-width": "16",
    "target-pointer-width": "16"
}

Насколько я понимаю, всё, вплоть до линковки, выполняется компонентом Rust/LLVM. Линковка выполняется через avr-gcc. Как сказал @EdgarBonet, похоже, что код, сгенерированный Rust (в сгенерированном --emit asm), правильный, но конечный линкованный вывод неверен. По сути, я хочу сообщить об этой ошибке, а для этого мне нужно понять, кому её следует сообщить.

, 👍7

Обсуждение

Почему вы вызываете loop() внутри main?, @Nick Gammon

Поскольку сигнатура функции fn main() -> ! подразумевает, что функция никогда не должна завершаться. Для этого я добавил бесконечный цикл в конце, чтобы эта функция никогда не возвращалась., @Vivek Yadav

Я понятия не имею, как работает инструментарий Rust, но, судя по дизассемблированию, он предполагает, что регистр r1 загружен значением 0, как и инструментарий C/C++. Очистка r1 — задача среды выполнения C. Связывает ли инструментарий Rust среду выполнения C? Не могли бы вы дизассемблировать программу целиком и убедиться, что r1 действительно очищается перед вызовом main?, @Edgar Bonet

@EdgarBonet Для кода Linux он действительно линкуется с C-RT, но я почти уверен, что для кода на физическом сервере это не сработает. Сейчас проверю и добавлю последний линкованный код., @Vivek Yadav

@EdgarBonet Я добавил последний связанный дизассемблированный код. Не могли бы вы проверить? Я совершенно не разбираюсь в ассемблере, не говоря уже об ассемблере AVR., @Vivek Yadav

Пожалуйста, не публикуйте [ваш вопрос на StackOverflow](https://stackoverflow.com/questions/78882263/finite-loop-runs-infinitely)). Решите, какой из них оставить, а другой удалить., @the busybee


2 ответа


Лучший ответ:

17

Похоже, это ошибка в цепочке инструментов: ошибка на единицу во всех Относительные прыжки. Например, основной цикл:

loop {
    write_reg(portB, 1 << LED_BUILTIN, 1 << LED_BUILTIN);
    delay(500_0000);
    write_reg(portB, 0, 1 << LED_BUILTIN);
    delay(500_0000);
}

было переведено на это с помощью RUSTFLAGS="--emit asm":

.LBB1_1:
    sbi 5, 5
    call    _ZN13arduino_blink5delay17h9627a982856e7dadE
    cbi 5, 5
    call    _ZN13arduino_blink5delay17h9627a982856e7dadE
    rjmp    .LBB1_1

Мне кажется, что это правильно. Однако, при разборке финальной версии программа выглядит так:

  b0:   2d 9a           sbi  0x05, 5 ;        PORTB |= (1<<5);
  b2:   0e 94 40 00     call 0x80    ; loop:  delay();
  b6:   2d 98           cbi  0x05, 5 ;        PORTB &= ~(1<<5);
  b8:   0e 94 40 00     call 0x80    ;        delay();
  bc:   fa cf           rjmp .-12    ;        goto loop;  // 0xb2

Инструкция, включающая светодиод, теперь вынесена из цикла. другие подобные ошибки, превышающие единицу, в пределах функции задержки, на инструкции brcs, breq и rjmp.

Я понятия не имею, как это решить, кроме как посмотреть в баг-трекере. инструментария AVR Rust.

Дополнение: Arduino IDE не поставляется с компилятором Rust. Вы Таким образом, используется отдельный, предположительно экспериментальный, компилятор. Я предполагаю, что, если явно не указано --emit asm, этот компилятор Rust Компилируется непосредственно в машинный код. В наши дни довольно часто пропускают язык ассемблера и компилировать из языка высокого уровня непосредственно в машинный код.

Первый фрагмент ассемблера был получен с помощью --emit asm. Он не на самом деле «разборка», а результат прямого Rust → asm Перевод. Второй фрагмент, сгенерированный avr-objdump, — это правильный Дизассемблирование: результат Rust → машинный код → asm перевод.

Я предполагаю, что ошибка кроется в машинном коде Rust → перевод, поэтому он не влияет на вывод --emit asm. Относительные переходы на AVR немного не интуитивны: они относительны счетчик программ, который представляет собой регистр ЦП, содержащий адрес Следующая инструкция в программе. Если вы неправильно предположите эти переходы выполняются относительно текущей инструкции, вы получаете точно Неправильный код второго фрагмента.

,

Все avr-gcc, avr-g++, avr-objcopy и т. д. — это просто символические ссылки из IDE Arduino. Я не использую какой-либо другой вариант линкера/компилятора, кроме того, что используется в Arduino, так что это довольно странно, особенно если объектный код кажется правильным, но линкованный код — неправильным (что случилось бы с помощью какого-нибудь линкера AVR)., @Vivek Yadav

@zombiesauce: См. исправленный ответ., @Edgar Bonet

Я не утверждал, что Arduino поставляется с компилятором Rust. Я не совсем уверен, как Rust компилируется в AVR, но он действительно перекладывает большую часть архитектурно-специфичной компиляции на набор инструментов GCC AVR. Полагаю, он генерирует MIR (промежуточное представление) из чистого набора инструментов Rust, а остальная работа с AVR, включая линковку, выполняется через avr-gcc, avr-objcopy и всё остальное. Теперь, вместо того, чтобы скачать его из репозитория Ubuntu, поскольку у меня уже есть Arduino, я просто экспортировал их PATH. Возможно, проблема во взаимодействии набора инструментов Rust и AVR., @Vivek Yadav

@zombiesauce: Re “_Я не совсем уверен..._”: Возможно, вам стоит запустить драйвер компилятора Rust в режиме расширенного вывода, чтобы увидеть, как именно он взаимодействует с инструментами GCC. Если компилятор генерирует объектный файл для линковки avr-gcc, можно проверить, содержит ли этот файл промежуточный язык или машинный код. Если это машинный код, можно дизассемблировать его и проверить, есть ли в нём ошибочные относительные переходы., @Edgar Bonet

Отлично объяснено, хотя интересно, как этот компилятор вообще вышел, если он неправильно компилирует ветви. Также странно, что, если он может генерировать правильный ассемблерный код, этот код не используется на следующем этапе (то есть ассемблером gcc)., @Nick Gammon

@NickGammon Согласен, это похоже на грубую ошибку. Хотя ежедневные релизы и не предназначены для массового потребления, они (должны быть таковыми, но я с этим раньше не сталкивался) очень нестабильны., @Vivek Yadav


4

@EdgarBonet был прав. Оказывается, в последней ночной версии компилятора Rust действительно есть проблема с генерацией кода для AVR. Установка версии ночного компилятора на 2023-12-11 (11 декабря 2023 г.) позволяет программе работать безупречно.

EDIT: Забыл упомянуть, но я уже отправил отчёт об ошибке/проблеме на GitHub Rust.

ДОПОЛНЕНИЕ №2: Ошибка была в тулчейне LLVM для AVR (помните, компиляция всё ещё выполняется LLVM). Ошибка в исходном коде исправлена.

,

По крайней мере, это дало Эдгару Бонету возможность объяснить, как работает генерация кода. Очень интересная тема., @Nick Gammon