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.
loop()
insidemain
?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.r1
is loaded with0
, just like the C/C++ toolchain does. Clearingr1
is the job of the C runtime. Does the Rust toolchain link the C runtime? Could you disassemble the complete program and check thatr1
is indeed cleared before callingmain
?