Nachdem wir unsere Toolchain installiert haben, wollen wir den ersten „Kernel“ bauen. Er soll nichts anderes machen, als den Pi für JTAG vorbereiten. Dafür müssen wir uns mit Details unserer Plattform herumschlagen, insbesondere mit der Beschreibung der I/O-Hardware, dem Speicher-Layout und dem Boot-Prozess.

Boot-Prozess

Unsere ARM-CPU ist nicht die einzige CPU auf dem Raspberry. Vielmehr gibt es einen weiteren Prozessor, die GPU VideoCore® IV. Anders als die Bezeichnung vermuten lässt, ist die GPU nicht nur für die Grafiksteuerung verantwortlich, sondern übernimmt z.B. auch den Boot-Prozess.1 Wenn der Strom eingeschaltet wird, übernimmt erst mal die GPU die Regie. Sie bootet von einem internen ROM, lädt dann von der SD-Karte die Datei bootcode.bin. Diese kann enthält einen Treiber, um Dateien im ELF-Format lesen. Als nächstes wird die eigentliche GPU-Firmware aus der Datei start.elf geladen. Die Firmware liest u.a. die Datei config.txt (so vorhanden) und konfiguriert das Board entsprechend, und lädt anschließend die Datei kernel.img auf die (aus AMR-Sicht) Adresse 0x08000. Von dieser Adresse aus startet dann auch der ARM.

Speicher und Formate

Layout und Linker

Die Datei kernel.img ist ein Binärfile, das unmittelbar Maschinencode enthält, also keine ELF-Datei oder ein anderes höheres Format. Wir müssen also erstens dafür sorgen, dass unser „Kernel“ von der Adresse 0x08000 lauffähig ist, und zweitens, dass er als reine Binärdatei vorliegt. Beim ersten hilft der Linker. In der „normalen“ Softwareentwicklung bekommt man vom Linker nicht viel mit, er macht ohne viel Aufsehen das, was man von ihm erwartet. Da wir hier aber ganz bestimmte Anforderungen haben, müssen wir es ihm mit einem Linker-Skript mitteilen. Unser Linker-Skript ist (noch) sehr einfach:

ENTRY(kernel_main)
SECTIONS
{
    /* Starts at LOADER_ADDR. */
    . = 0x8000;
    .text :
    {
        KEEP(*(.text.kernel_main))
        *(.text)
    }   
    .bss :
    {
        bss = .;
        *(.bss)
    }
}

Es sagt im Wesentlichen, dass unsere Einsprungstelle in den Code kernel_main heißt,2 und dass der Code an Adresse 0x0800 beginnt und zwar mit kernel_main.

Binärfile

Um aus dem vom Linker generierten ELF-File ein Binärfile zu machen, nutzen wir objcopy, genauer die Cross-Variante arm-none-eabi-objcopy. Der Name „objcopy“ ist ein klares Understatement, das dieses Programm nicht nur kopiert, sondern eine Vielzahl von Transformationen ausführen kann. Wir nutzen es allerdings nur dazu, aus dem ELF-File den Binärcode zu generieren, eine Aufgabe, die auf Plattformen mit einem Betriebssystem i.d.R. der Lader übernimmt.

Makefile

Eigentlich hat Rust sein eigenes Build-System cargo, mit dem wir schon die core-Bibliothek gebaut haben. Jedoch ist cargo nicht sonderlich gut für bare-metal Programme geeignet. Daher werden wir auf die klassische Form des Makefiles zurückgreifen.3

Unser Makefile geht davon aus, dass wir folgendes Verzeichnis-Layout haben:

aihpos
  |
  +- rust-corelib
  |
  +- jtag
      |
      +-- src
      +-- obj
      +-- build

In src sind alle Quelldateien. Die Objektdateien liegen in obj und alle anderen generierten Dateien in build. Das Makefile selbst befindet sich in jtag und sieht so aus:

BUILDDIR=build
SRCDIR=src
OBJDIR=obj
BUILDDIR=build
OBJCOPY=arm-none-eabi-objcopy
OBJDUMP=arm-none-eabi-objdump
RUSTC=rustc
LIBCORE=../rust-libcore/target/arm-none-eabihf/release/libcore.rlib
RUSTFLAGS= --target $(TARGET) -C panic=abort -g --crate-type staticlib --extern core=$(LIBCORE)
LINKFLAGS= -O0 -g -Wl,-gc-sections -mfpu=vfp -mfloat-abi=hard -march=armv6zk -mtune=arm1176jzf-s -nostdlib

TARGET=arm-none-eabihf
MAIN=kernel

IMAGE=$(BUILDDIR)/$(MAIN).img
LIST=$(BUILDDIR)/$(MAIN).list
ELF=$(BUILDDIR)/$(MAIN).elf

vpath %.rs $(SRCDIR)
vpath %.o $(OBJDIR)

SOURCES= main.rs 
OBJ= $(addsuffix .o, $(basename $(SOURCES)))

.PHONY: clean 

all: $(IMAGE) $(LIST)

main.o: main.rs panic.rs

$(IMAGE): $(ELF)
    $(OBJCOPY) $(ELF) -O binary $(IMAGE)

$(LIST): $(IMAGE)
    $(OBJDUMP) -d $(ELF) > $@

$(ELF): $(OBJ) 
    arm-none-eabi-gcc $(LINKFLAGS) -Tsrc/layout.ld  $(addprefix $(OBJDIR)/, $(OBJ)) -o $@

%.o: %.rs 
    $(RUSTC) $(RUSTFLAGS) $< -o $(OBJDIR)/$@

clean:
    rm -f $(OBJDIR)/*
    rm -f $(BUILDDIR)/*

Nach der Diskussion dürfte es nicht weiter überraschend sein. Einzig das Ziel „LIST“ haben wir noch nicht besprochen. Dies ist der mit Hilfe von objdump disassemblierte Code des erzeugten ELF-Files. Er kann manchmal bei der Fehlersuche nützlich sein.

Hardware

JTAG

Der Pi hat 54  universelle Kommunikationspins, die als Ausgabe (Standard beim Einschalten), Eingabe oder mit jeweils bis zu sechs Spezialfunktionen (Alternativfunktionen 0…5) beschaltet werden kann. Für JTAG werden fünf Steuerleitungen gebraucht, die jeweils auf zwei verschiedene Pins

  • Data Input (TDI), serieller Eingang, auf Pin 4 in der Alternativfunktion 5 oder Pin 26  in der Alternativfunktion 4
  • Test Data Output (TDO), serieller Ausgang,  auf Pin 5 in der Alternativfunktion 5 oder Pin 24  in der Alternativfunktion 4
  • Test Clock (TCK). Das Taktsignal,  auf Pin 13 in der Alternativfunktion 5 oder Pin 25  in der Alternativfunktion 4
  • Test Mode Select (TMS), Steuerleitung, auf Pin 12 in der Alternativfunktion 5 oder Pin 27  in der Alternativfunktion 4
  • Test Reset (TRST), Reset, auf Pin 12 in der Alternativfunktion 5 oder Pin 22  in der Alternativfunktion 4

Der Adapter (siehe letzter Beitrag) nutzt die Pin 22 bis 27, entsprechen müssen diese auf Alternativfunktion 4 gesetzt werden. Dies geschieht über die GPIO-Select-Register, genauer über GPIOSEL2. Jeweils drei aufeinanderfolgende Bits wählen die Funktion für ein GPIO-Pin aus.

Außerdem muss vorher für diese Pins die Pull-up/down-Steuerung deaktiviert werden. Dabei folgen wir der Beschreibung auf Seite 101 die ARM-Peripherie-Dokumentation.

LED

Damit wir sehen, dass unser Programm auch läuft, wollen wir die grüne LED des Raspberrys blinken lassen. Diese ist an den GPIO-Pin 47 angeschlossen. Die GPIO-Pins sind in zwei Sätze unterteilt, Satz 1 für Pin 0…31, und Satz 2 für Pin 32…53. Pin 47 ist also Pin 15 (wenn man mit Null anfängt zu zählen) des zweiten Satzes.

Software

Das erste Rust-Programm

Wir haben jetzt alle Informationen zusammen, um eine Software zu schreiben, die den Raspberry auf einen JTAG-Zugriff vorzubereiten. Wie schon im Teil 1 dieser Reihe diskutiert, soll Rust benutzt werden, mit – wenn nötig – etwas Assembler. In der Regel wird der Startup-Code in Assembler geschrieben, siehe z.B. hier. Das ist im Allgemeinen auch eine gute Idee: jede Rust-Funktion (oder C-Funktion) hat einen Prolog, in dem die Rücksprungadresse auf dem Stack gesichert wird. Allerdings ist zu Beginn ja noch gar kein Stack angelegt. Dies und andere Initialisierungen macht das Assemblerprogramm, ehe es in den in der höheren Programmiersprache geschriebenen Code ruft.

Allerdings ist ein solches Assembler-Modul in unserem Fall nicht nötig: Einerseits können wir Assembler-Befehle direkt in den Rust-Code einbetten, andererseits können wir unsere Startfunktion mit dem Attribute #[naked] versehen, so dass der Compiler keinen Prolog oder Epilog anlegt.

#![feature(asm,lang_items,core_intrinsics,naked_functions)]
#![no_std]

// Hardware-Adressen
const GPIO_BASE: u32 = 0x20200000;
const GPFSEL2: *mut u32 =   (GPIO_BASE+0x08) as *mut u32;
const GPSET1: *mut u32 = (GPIO_BASE+0x20) as *mut u32;
const GPCLR1: *mut u32 = (GPIO_BASE+0x2C) as *mut u32;
const GPPUD:      *mut u32 = (GPIO_BASE+0x94) as *mut u32;
const GPPUDCLK0:  *mut u32 = (GPIO_BASE+0x98) as *mut u32;

fn sleep(value: u32) {  
    for _ in 1..value {
        unsafe { asm!("":::"memory":"volatile"); } 
    }
} 

// kernel_main() erwartet, dass etwas unterhalb von 0x8000 keine
// Daten/Code liegen. Das ist normalerweise gewährleistet, da
// der Code entweder bei 0x8000 beginnt oder 0x0000 (bei "kernel_old=1"
// in config.txt). Für den letzteren Fall ist dieser Kernel klein
// genug, um nicht mit dem Stack zu kollidieren.

#[no_mangle]   // Name wird für den Export nicht verändert
#[naked]       // keinen Prolog
pub extern fn kernel_main() {
    // Setze den Stackpointer
    unsafe{ asm!("mov sp, #0x8000");  }
    
    // Pull up/down abschalten
    unsafe{ *GPPUD = 0 };
    sleep(150);
    unsafe{ *GPPUDCLK0 = (1 << 22) | (1 << 23) | (1 << 24) | (1 << 25) | (1 << 26) | (1 << 27) };
    sleep(150);
    unsafe{ *GPPUDCLK0 = 0 };

    // GPIO Pins 22 .. 27 auf alternative Funktion 4 (= 011) setzen
    let mut selection: u32 = unsafe{ *GPFSEL2};
    selection = selection & !((0b111 <<  6)  | (0b111 <<  9) | (0b111 <<  12) | (0b111 <<  15) | (0b111 <<  18) | (0b111 <<  21) );
    selection = selection | (0b011 <<  6) | (0b011 << 9) | (0b011 <<  12) | (0b011 <<  15) | (0b011 <<  18) | (0b011 <<  21);
    unsafe {  *GPFSEL2 = selection};

    // Als Lebenszeichen lassen wir die grüne LED blinken
    let led_on  = GPSET1;
    let led_off = GPCLR1; 
    loop {
        unsafe { *(led_on) = 1 << 15; }
        // Die Zeiten sind für die Debug-Version, die Release-Version benötigt
        // längere Zeiten.
        sleep(50000);
        unsafe { *(led_off) = 1 << 15; }
        sleep(50000);
    }
}

Panik!

Nun übersetzen wir dieses Programm.

  Terminal
$make
rustc –target arm-none-eabihf -C panic=abort -g –crate-type staticlib –extern core=../rust-libcore/target/arm-none-eabihf/release/libcore.rlib src/main.rs -o build/main.o
error: language item required, but not found: panic_fmt
error: aborting due to previous error
make: *** [main.o] Error 101
Hoppla! Der Linker hat ein undefiniertes Symbol. Ursache dafür ist, dass die Core-Bibliothek nicht völlig frei von externen Referenzen ist. Rust muss wissen, wie es sich in einem Ausnahmefall (Exception) verhalten soll, um evtl. sogar eine Recovery zu ermöglichen, und das ist plattform-spezifisch. Wenn allerdings wie in unserem Fall der der „Kernel“ selbst crashed, ist eine Recovery hoffnungslos. Es ist daher hinreichend, die undefinierten Symbole (es sind nämlich noch ein paar) einfach durch Dummy-Funktionen zu definieren. Dazu legen wir noch eine Datei panic.rs an:

use core::intrinsics;

#[lang = "eh_personality"] extern fn eh_personality() {}

#[no_mangle]
pub extern fn __aeabi_unwind_cpp_pr0() -> ()
{
    loop {}
}

#[no_mangle]
pub extern fn __aeabi_unwind_cpp_pr1() -> ()
{
    loop {}
}

#[allow(non_snake_case)] #[no_mangle]
pub extern "C" fn _Unwind_Resume() -> ! {
    loop {}
}

#[lang = "panic_fmt"]
#[no_mangle]
pub extern fn rust_begin_panic(_msg: core::fmt::Arguments,
                               _file: &'static str,
                               _line: u32) -> ! {
    unsafe { intrinsics::abort() }
}

Außerdem fügen wir main.rs noch eine Zeile hinzu:

include!("panic.rs");

Dieses Macro funktioniert ähnlich wie das #include in C und macht die Datei panic.rs logisch zu einem Bestandteil von main.rs. Dieses Vorgehen ist ein bisschen „unrustig“, da eigentlich in Rust kein Modul in mehr als einer Datei sein soll (dafür ruhig mehrere Module in einer Datei), aber ich habe diese Variante gewählt, um den eigentlichen Funktionscode halbwegs „rein“ zu halten ohne extra ein neues Modul anlegen zu müssen. Nun läuft die Übersetzung durch. Anschließend kopieren wir da Binär-Image auf die SD-Karte,

  Terminal
$make
rustc –target arm-none-eabihf -C panic=abort -g –crate-type staticlib –extern core=../rust-libcore/target/arm-none-eabihf/release/libcore.rlib src/main.rs -o obj/main.o
arm-none-eabi-gcc -O0 -g -Wl,-gc-sections -mfpu=vfp -mfloat-abi=hard -march=armv6zk -mtune=arm1176jzf-s -nostdlib -Tsrc/layout.ld  obj/main.o -o build/kernel.elf
arm-none-eabi-objcopy build/kernel.elf -O binary build/kernel.img
arm-none-eabi-objdump -d build/kernel.elf > build/kernel.list
cp build/kernel.img /Volume/boot/

Wenn der Raspberry mit dieser SD gestartet wird, fängt die grüne LED tatsächlich an zu blinken.

Mit dem Pi reden

Jetzt soll die JTAG-Lommunikation getestet werden. Dazu schreiben wir zwei openocd-Konfigurationsdateien4:

interface jlink
## Broadcom 2835 on Raspberry Pi
telnet_port 4444
gdb_port 3333
adapter_khz 0

if { [info exists CHIPNAME] } {
set _CHIPNAME $CHIPNAME
} else {
set _CHIPNAME raspi
}

reset_config none

if { [info exists CPU_TAPID ] } {
set _CPU_TAPID $CPU_TAPID
} else {
set _CPU_TAPID 0x07b7617F
}
jtag newtap $_CHIPNAME arm -irlen 5 -expected-id $_CPU_TAPID

set _TARGETNAME $_CHIPNAME.arm
target create $_TARGETNAME arm11 -chain-position $_TARGETNAME

In einem Terminal kann jetzt der OpenOCD-Server gestartet werden:

  Terminal
$openocd -f jlink.cfg -f raspi.cfg
Open On-Chip Debugger 0.10.0+dev-00093-g6b2acc0 (2017-03-28-11:17)
Licensed under GNU GPL v2
For bug reports, read
http://openocd.org/doc/doxygen/bugs.html
adapter speed: 1000 kHz
none separate
Info : auto-selecting first available session transport „jtag“. To override use ‚transport select ‚.
raspi.arm
Info : No device selected, using first device.
Info : J-Link V10 compiled Jan 9 2017 17:48:51
Info : Hardware version: 10.10
Info : VTarget = 3.335 V
Info : clock speed 1000 kHz
Info : JTAG tap: raspi.arm tap/device found: 0x07b7617f (mfg: 0x0bf (Broadcom), part: 0x7b76, ver: 0x0)
Info : found ARM1176
Info : raspi.arm: hardware has 6 breakpoints, 2 watchpoints

Nun kann ein weiteres Terminalfenster geöffnet werden, in dem ein Telnet gestartet wird.

  Terminal
$telnet localhost 4444
Connected to localhost.
Escape character is ‚^]‘.
Open On-Chip Debugger
> halt
target halted in ARM state due to debug-request, current mode: Supervisor
cpsr: 0x600001d3 pc: 0x000082e0

Jetzt kann man viele interessante Dinge machen. Beispielsweise können Register ausgelesen oder Speicherstellen gelesen oder geschrieben werden:

  Terminal
> reg r1
r1 (/32): 0x00007FD4
> mww 0x2020002C 0x8000
> mww 0x20200020 0x8000

Die letzten beiden Befehle schalten die grüne LED aus- und wieder an. Der vermutlich wichtigste Befehl ist aber load_image. Damit entfällt das Kopieren eines neuen Images auf die SD-Karte. Die Datei (wahlweise auch im ELF-Format) kann direkt auf den Pi gebracht werden.

Für ein noch etwas detaillierteres Debuggen kann openocd mit dem GNU-Debugger gekoppelt werden. Im Konfigurationsfile wurde der Port 3333 dafür freigegeben. Natürlich darf nicht der lokale Debugger genutzt werden5, sondern der ARM-Cross-Debugger:

  Terminal
$arm-none-eabi-gdb -q build/kernel.elf
Reading symbols from build/kernel.elf…done.
(gdb) target remote localhost:3333
Remote debugging using localhost:3333
0x000082e0 in core::mem::swap (x=0x7f98, y=0x7fd4) at ~/Development/aihPOS/Code/rust-libcore/rust/src/libcore/mem.rs:448
448 pub fn swap(x: &mut T, y: &mut T) {

Wer will, kann den Debugger auch in eine GUI oder IDE einbinden, z.B. mit der gdbgui. GDB-GUI

Nun kann man auch ohne Betriebssystem (fast) so bequem debuggen, wie man es von der Entwicklung von Desktop-Programmen gewohnt ist. Zwar ist die Arbeit über das JTAG-Interface etwas langsam, aber immer noch viel schneller als wenn man ständig die SD-Karte wechselt.

Morsezeichen

Manchmal will man eine schnelle Rückmeldung haben, ob eine bestimmte Stelle im Programm erreicht wurde oder ob ein Fehler aufgetreten ist. Solange wir keine Konsole haben, können wir die LED dafür nutzen. Dazu wird unser Blinkprogramm in ein eigenes Modul ausgelagert und parameterisiert:

#![allow(dead_code)]
//! Der Raspberry hat zwei LEDs. Dieses Modul nutzt die grüne LED,
//! um Signal zu generieren. Dies kann z.B. als Low-Level-Debugging-Interface
//! genutzt werden,
use hal::bmc2835::{Led,LedType};
// Hardware-Adressen
//const GPIO_BASE: u32 = 0x20200000;
//const GPSET1: *mut u32 = (GPIO_BASE+0x20) as *mut u32;
//const GPCLR1: *mut u32 = (GPIO_BASE+0x2C) as *mut u32;

#[derive(Clone,Copy)]
#[repr(u32)]
/// Blinkzeichen
pub enum Bc {
    /// langes Zeichen
    Long =  380000,
    /// kurzes Zeichen
    Short = 120000,
    /// Pause
    Pause = 250000,
}

type BlinkSeq = &'static [Bc];

// Blinksequenzen 
/// einmal blinken
pub const BS_DUMMY: BlinkSeq =   &[Bc::Long];
/// Ziffer 1 in Morsecode:  `–•`
pub const BS_ONE: BlinkSeq   =   &[Bc::Long,Bc::Short];
/// Ziffer 2 in Morsecode:  `–••`
pub const BS_TWO: BlinkSeq   =   &[Bc::Long,Bc::Short,Bc::Short];
/// Ziffer 3 in Morsecode:  `–•••`
pub const BS_THREE: BlinkSeq =   &[Bc::Long,Bc::Short,Bc::Short,Bc::Short];
/// "SOS" in Morsecode : `••• ––– •••`
pub const BS_SOS: BlinkSeq   =   &[Bc::Pause,Bc::Short,Bc::Short,Bc::Short,Bc::Pause,Bc::Long,Bc::Long,Bc::Long,Bc::Pause,Bc::Short,Bc::Short,Bc::Short];
/// "Hi" in Morsecode:  `•••• ••` 
pub const BS_HI: BlinkSeq    =   &[Bc::Short,Bc::Short,Bc::Short,Bc::Short,Bc::Pause,Bc::Short,Bc::Short];

#[inline(never)]
fn sleep(value: u32) {  
    for _ in 1..value {
        unsafe { asm!("":::"memory":"volatile"); } 
    }
}

/// Gibt eine Blinksequenz am LED aus
pub fn blink_once(s: BlinkSeq) {
    let mut led = Led::init(LedType::Green);
    //let led_on  = GPSET1;
    //let led_off = GPCLR1; 

    for &c in s {
        match c {
            Bc::Long => {
                led.switch(true);
                //unsafe { *(led_on) = 1 << 15; }
                sleep(Bc::Long as u32);
                //unsafe { *(led_off) = 1 << 15; }
                led.switch(false);
            },
            Bc::Short => {
                led.switch(true);
                //unsafe { *(led_on) = 1 << 15; }
                sleep(Bc::Short as u32);
                //unsafe { *(led_off) = 1 << 15; }
                led.switch(false);
            }
            Bc::Pause => {
                sleep(Bc::Pause as u32);
            }
        }
        sleep(Bc::Short as u32);
    }
}

/// Gibt eine Blinksequenz in endloser Wiederholung aus
pub fn blink(s: BlinkSeq) -> ! {

    loop {
        blink_once(s);
        sleep(400000);
    }
}

Man beachte, dass die auch hier benutzte Verzögerung durch „busy idling“ so nicht funktioniert, wenn wir die Codeoptimierung einschalten: Dann wird die Schleife zwar nicht vollständig wegoptimiert (das verhindert die Assembler-Anweisung), aber die Zeiten sind doch vollständig andere. Allerdings besteht bisher für eine Optimierung noch kein Grund – die Ausführungszeit ist derzeit noch egal, aber die Übersetzungszeit würde sich etwas verlängern.


  1. Diese Darstellung des Boot-Prozesses ist etwas vereinfacht. 

  2. Diese Information ist eigentlich überflüssig, da sie sich später im Binärfile nicht mehr wiederfindet. Der Linker braucht sie aber, er würde sonst das Fehlen monieren. 

  3. Vielleicht werde ich in späteren Folgen nochmal cargo oder die Cross-Compile-Variante xargo einsetzen. 

  4. Vergleiche https://github.com/dwelch67/raspberrypi/tree/master/armjtag. 

  5. Schon garn nicht in meiner Arbeitsumgebung auf dem Mac, da der Debugger hier auf MachO ausgelegt ist 


Kommentare