7

To gain better understanding of both Rust and Arduino (Uno), I'm trying to write direct hardware code for Arduino in Rust. Here's a very simple LED blink example, that I've tried to write.

I've made use of one library (crate) called avrd which only provides address mapping for ATMega328P microcontroller.

#![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);
        }
    }
}

(disassembly for the aforementioned snippet pasted at the end)

Now for some reason, if this delay value is 2 or greater, the LED never stops blinking. I think this delay function might be at fault, since putting the delay(2) above the line of code switching on the LED, makes the LED never switch on. Another bizarre thing, is if I change the code up a bit like so:

#[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;
        }
    }
}

Then though this time the LED switches on, and switches off, but only once (??!!). The infinite loop becomes finite and runs only once. I'm not sure if the code being generated is wrong or what.

Here's the .cargo/config.toml file:

[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"

The AVR toolchain that Rust is consuming is the same one that comes with Arduino, I haven't installed any separately (mentioning in case that toolchain causes problems on non-C platforms).

Here's the disassembly for the first code snippet (RUSTFLAGS="--emit asm" cargo run --release, not the final linked assembly):

    .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

And here's the final (linked) disassembly (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

EDIT: There is some confusion regarding toolchain and what party could be responsible for this issue. So here's the atmega328p.json file which is responsible for telling the Rust compiler how to compile for this backend (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"
}

From what I understand, everything up until the linking is done by the Rust/LLVM part. Linking is done by avr-gcc. As @EdgarBonet said, looks like the code generated by Rust (in the generated --emit asm) is correct but the final linked output is wrong. Basically I'd like to file this bug and for that I need to understand who to file it to.

6
  • Why are you calling loop() inside main?
    – Nick Gammon
    Commented Aug 18 at 7:54
  • 1
    Because the function signature fn main() -> ! implies the function should never exit. For this, I've put an infinite loop at the end so that this function never returns. Commented Aug 18 at 8:20
  • 1
    I have no idea how the Rust toolchain works but, judging from the disassembly, it assumes the register r1 is loaded with 0, just like the C/C++ toolchain does. Clearing r1 is the job of the C runtime. Does the Rust toolchain link the C runtime? Could you disassemble the complete program and check that r1 is indeed cleared before calling main? Commented Aug 18 at 8:35
  • @EdgarBonet For Linux code it does link into C-RT but I'm pretty sure it wouldn't do that for bare-metal code. Let me check, and add the final linked code. Commented Aug 18 at 8:37
  • @EdgarBonet I've added the final linked disassembly. Could you please check? I am quite inept in reading assembly let alone AVR assembly. Commented Aug 18 at 8:43

2 Answers 2

17

This looks like a bug in the toolchain: an off-by-one error in all relative jumps. For example, the main loop:

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

got translated to this by RUSTFLAGS="--emit asm":

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

which looks correct to me. However, on the disassembly of the final program, it looks like this:

  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

The instruction that turns the LED on is now out of the loop. There are other similar off-by-one errors within the delay function, on the instructions brcs, breq and rjmp.

I have no idea how to solve this, other than looking at the bug tracker of the AVR Rust toolchain.

Edit: The Arduino IDE does not come with a Rust compiler. You are thus using a separate, presumably experimental, compiler. My guess is that, unless explicitly asked to --emit asm, this Rust compiler compiles directly to machine code. It is pretty common nowadays to skip the assembly language and compile from a high-level language directly to machine code.

The first assembly snippet was obtained with --emit asm. It is not really a “disassembly”, but the result of a direct Rust → asm translation. The second snipped, generated by avr-objdump is a proper disassembly: the result of a Rust → machine code → asm translation.

My guess is that the bug lies in the Rust → machine code translation, which is why it does not affect the --emit asm output. Relative jumps on AVR are a bit unintuitive: they are relative to the program counter, which is a CPU register holding the address of the next instruction in the program. If you incorrectly assume these jumps are relative to the current instruction, you get precisely the faulty code of the second snippet.

6
  • All the avr-gcc, avr-g++, avr-objcopy, etc. are just symlink'd from Arduino's IDE. I'm not using some other variant of linker/compiler than the one that Arduino uses, so this is pretty weird - especially if the object code seems correct but the linked code seems wrong (which would've happened with the help of some AVR linker). Commented Aug 18 at 10:45
  • @zombiesauce: See amended answer. Commented Aug 18 at 12:33
  • I was not claiming that Arduino comes with a Rust compiler. Now I'm not exactly sure how Rust compiles to AVR, but it really offloads a lot of architecture-specific compilation to the GCC AVR toolchain. I think it generates MIR (middle intermediate representation) from the pure Rust toolchain, and the rest of the AVR stuff including linking is done via the those avr-gcc, avr-objcopy and the whole shebang. Now, instead of downloading it from Ubuntu's repository, since I already have Arduino; I have instead just exported their PATH. Hence maybe problem is in Rust <-> AVR toolchain interaction. Commented Aug 19 at 7:49
  • 1
    @zombiesauce: Re “I'm not exactly sure...”: Maybe you could launch the Rust compiler driver in verbose mode in order to see exactly how it interacts with the GCC toolchain. If the compiler proper generates an object file to be linked by avr-gcc, you could check whether this file contains intermediate language or machine code. If it's machine code, you could disassemble it and see whether it has the faulty relative jumps. Commented Aug 19 at 10:15
  • 1
    Nicely explained, although I wonder how this compiler ever got released if it compiles branches incorrectly. It also seems odd that if it can generate the correct assembler code, that this code is not used in the next step (ie. by the gcc assembler).
    – Nick Gammon
    Commented Aug 20 at 0:37
4

@EdgarBonet was correct. Turns out there is actually a problem in code generation for AVR in the latest nightly release of Rust compiler. Setting the nightly compiler version to 2023-12-11 (11th Dec, 2023) makes the program work flawlessly.

EDIT: Forgot to mention but I have already filed a bug-report/issue on Rust's GitHub.

1
  • 4
    At least this gave Edgar Bonet a chance to explain how the code generation works. Very interesting thread.
    – Nick Gammon
    Commented Aug 20 at 9:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.