QEMU
Comenzaremos a escribir un programa para el LM3S6965, un microcontrolador Cortex-M3. Hemos elegido esto como nuestro objetivo inicial porque puede ser emulado usando QEMU Así que no es necesario manipular el hardware en esta sección y podemos centrarnos en las herramientas y el proceso de desarrollo.
IMPORTANTE Usaremos el nombre "app" para el nombre del proyecto en este tutorial. Cada vez que veas la palabra "app", debes reemplazarla con el nombre que seleccionaste para tu proyecto. O bien, también podrías nombrar tu proyecto "app" y evitar las sustituciones.
Creando un programa Rust no estandar
Usaremos la plantilla del proyecto cortex-m-quickstart
para generar un nuevo proyecto a partir de ella.
El proyecto creado contendrá una aplicación básica: Un buen punto de partida para una nueva
aplicación de Rust embebido.
Además, el proyecto contendrá un directorio de ejemplos, con varias aplicaciones separadas, Destacando
algunas de las principales funciones de Rust embebido.
Usando cargo-generate
Primero instala cargo-generate
cargo install cargo-generate
Luego genera un nuevo proyecto
cargo generate --git https://github.com/rust-embedded/cortex-m-quickstart
Project Name: app
Creating project called `app`...
Done! New project created /tmp/app
cd app
Usando git
Clona el repositorio
git clone https://github.com/rust-embedded/cortex-m-quickstart app
cd app
Y luego rellene los marcadores de posición en el archivo Cargo.toml
[package]
authors = ["{{authors}}"] # "{{authors}}" -> "John Smith"
edition = "2018"
name = "{{project-name}}" # "{{project-name}}" -> "app"
version = "0.1.0"
# ..
[[bin]]
name = "{{project-name}}" # "{{project-name}}" -> "app"
test = false
bench = false
Usando neither
Obtenga la última snapshot de la plantilla cortex-m-quickstart
y extráigala.
curl -LO https://github.com/rust-embedded/cortex-m-quickstart/archive/master.zip
unzip master.zip
mv cortex-m-quickstart-master app
cd app
O puedes navegar hasta cortex-m-quickstart
, Haga clic en el botón verde "Clonar o descargar" y luego haga clic en "Descargar ZIP".
Luego, complete los marcadores de posición en el archivo Cargo.toml
como se hizo en la segunda parte de la versión "Usando git
".
Descripción general del programa
Para mayor comodidad, aquí se encuentran las partes más importantes del código fuente. src/main.rs
:
#![no_std]
#![no_main]
use panic_halt as _;
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
loop {
// tu código va aquí
}
}
Este programa es un poco diferente de un programa Rust estándar, así que veámoslo más de cerca.
#![no_std]
indica que este programa no se vinculará al paquete estándar,
std
. En lugar de ello, se vinculará a su subconjunto: la crate "core".
#![no_main]
Indica que este programa no utilizará la interfaz estandarmain
que la mayoría de los programas Rust usan. La razón principal para usarno_main
es que usando la interfaz main
en no_std
El contexto requiere
nightly.
use panic_halt as _;
. Esta caja proporciona un panic_handler
que define el comportamiento del programa en caso de panico. Lo explicaremos con más detalle en el capitulo del libro
Panico
#[entry]
es un atributo proporcionado por la crate cortex-m-rt
que se utiliza para marcar el punto de entrada del programa. Como no utilizamos la interfaz estándarmain
Necesitamos otra forma de indicar el punto de entrada del programa y esa sería #[entry]
.
fn main() -> !
. Nuestro programa será el único proceso que se ejecuta en el hardware de destino, así que no queremos que finalice. Usamos una
función divergente, la parte -> ! en la firma de la función garantiza en tiempo de compilación que ese será el caso
compilación cruzada
El siguiente paso es compilar de forma cruzada el programa para la arquitectura Cortex-M3.
Eso es tan simple como correr cargo build --target $TRIPLE
Si sabes cuál podria ser el target de compilación ($TRIPLE
).
Afortunadamente, el .cargo/config.toml
en la plantilla tiene la respuesta:
tail -n6 .cargo/config.toml
[build]
# Elija UNO de estos targets de compilación
# target = "thumbv6m-none-eabi" # Cortex-M0 y Cortex-M0+
target = "thumbv7m-none-eabi" # Cortex-M3
# target = "thumbv7em-none-eabi" # Cortex-M4 y Cortex-M7 (sin FPU)
# target = "thumbv7em-none-eabihf" # Cortex-M4F y Cortex-M7F (con FPU)
Para realizar una compilación cruzada para la arquitectura Cortex-M3 tenemos que usar
thumbv7m-none-eabi
. Ese target no se instala automáticamente al instalar las herramientas de Rust. Si aún no lo has hecho, sería un buen momento para añadirlo.
rustup target add thumbv7m-none-eabi
Dado que el target de compilación thumbv7m-none-eabi
se ha establecido como predeterminado en el archivo .cargo/config.toml
,
los dos comandos siguientes hacen lo mismo:
cargo build --target thumbv7m-none-eabi
cargo build
inspeccionando
Ahora tenemos un binario ELF no nativo en target/thumbv7m-none-eabi/debug/app
.
Podemos inspeccionarlo usando cargo-binutils
.
Con cargo-readobj
podemos imprimir los headers ELF para confirmar que se trata de un binario ARM.
cargo readobj --bin app -- --file-headers
Tenga en cuenta que:
--bin app
es una forma abreviada de inspeccionar el binario en target/$TRIPLE/debug/app`--bin app
también (re)compilará el binario, si es necesario
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0x0
Type: EXEC (Executable file)
Machine: ARM
Version: 0x1
Entry point address: 0x405
Start of program headers: 52 (bytes into file)
Start of section headers: 153204 (bytes into file)
Flags: 0x5000200
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 2
Size of section headers: 40 (bytes)
Number of section headers: 19
Section header string table index: 18
cargo-size
puede imprimir el tamaño de las secciones del enlazador del binario.
cargo size --bin app --release -- -A
Usamos --release
para inspeccionar la versión optimizada
app :
section size addr
.vector_table 1024 0x0
.text 92 0x400
.rodata 0 0x45c
.data 0 0x20000000
.bss 0 0x20000000
.debug_str 2958 0x0
.debug_loc 19 0x0
.debug_abbrev 567 0x0
.debug_info 4929 0x0
.debug_ranges 40 0x0
.debug_macinfo 1 0x0
.debug_pubnames 2035 0x0
.debug_pubtypes 1892 0x0
.ARM.attributes 46 0x0
.debug_frame 100 0x0
.debug_line 867 0x0
Total 14570
Un repaso sobre las secciones de enlace ELF
.text
contiene las instrucciones del programa.rodata
contiene valores constantes como cadenas.data
contiene variables asignadas estáticamente cuyos valores iniciales no son cero.bss
también contiene variables asignadas estáticamente cuyos valores iniciales son cero.vector_table
es una sección no estándar que usamos para almacenar la tabla de vectores (interrupciones)- Las secciones
.ARM.attributes
y.debug_*
contienen metadatos y no se cargarán en el destino al flashear el binario.
IMPORTANTE: Los archivos ELF contienen metadatos como información de depuración, por lo que su tamaño en disco no refleja con precisión el espacio que ocupará el programa al instalarse en un dispositivo. Siempre use cargo-size
para comprobar el tamaño real de un binario.
cargo-objdump
Se puede utilizar para desmontar el binario.
cargo objdump --bin app --release -- --disassemble --no-show-raw-insn --print-imm-hex
NOTA Si el comando anterior indica un error de "Argumento de línea de comando desconocido", consulte el siguiente informe de error:
https://github.com/rust-embedded/book/issues/269
NOTA: Esta salida puede variar según el sistema. Las nuevas versiones de rustc, LLVM y las bibliotecas pueden generar diferentes ensamblados. Hemos truncado algunas instrucciones para que el fragmento sea pequeño.
app: file format ELF32-arm-little
Disassembly of section .text:
main:
400: bl #0x256
404: b #-0x4 <main+0x4>
Reset:
406: bl #0x24e
40a: movw r0, #0x0
< .. truncated any more instructions .. >
DefaultHandler_:
656: b #-0x4 <DefaultHandler_>
UsageFault:
657: strb r7, [r4, #0x3]
DefaultPreInit:
658: bx lr
__pre_init:
659: strb r7, [r0, #0x1]
__nop:
65a: bx lr
HardFaultTrampoline:
65c: mrs r0, msp
660: b #-0x2 <HardFault_>
HardFault_:
662: b #-0x4 <HardFault_>
HardFault:
663: <unknown>
Funcionamiento
A continuación, veamos cómo ejecutar un programa embebido en QEMU. Esta vez usaremos el ejemplo hello
, que realmente hace algo.
Para mayor comodidad, aquí está el código fuente de examples/hello.rs
:
//¡Imprime "Hola, mundo!" en la consola del host usando semihosting
#![no_main]
#![no_std]
use panic_halt as _;
use cortex_m_rt::entry;
use cortex_m_semihosting::{debug, hprintln};
#[entry]
fn main() -> ! {
hprintln!("Hello, world!").unwrap();
// salir de QEMU
// NOTA no ejecute esto en el hardware; puede dañar el estado de OpenOCD
debug::exit(debug::EXIT_SUCCESS);
loop {}
}
Este programa usa un método llamado semihosting para imprimir texto en la consola del host. Al usar hardware real, esto requiere una sesión de depuración, pero al usar QEMU, funciona perfectamente.
Comencemos compilando el ejemplo:
cargo build --example hello
El binario de salida estará ubicado en
target/thumbv7m-none-eabi/debug/examples/hello
.
Para ejecutar este binario en QEMU, ejecute el siguiente comando:
qemu-system-arm \
-cpu cortex-m3 \
-machine lm3s6965evb \
-nographic \
-semihosting-config enable=on,target=native \
-kernel target/thumbv7m-none-eabi/debug/examples/hello
Hello, world!
El comando debería salir correctamente (código de salida = 0) después de imprimir el texto. En *nix, puedes comprobarlo con el siguiente comando:
echo $?
0
Analicemos ese comando QEMU:
-
qemu-system-arm
. Este es el emulador QEMU. Existen algunas variantes de estos binarios QEMU; este realiza una emulación completa del sistema de máquinas ARM, de ahí su nombre. -
-cpu cortex-m3
. Esto le indica a QEMU que emule una CPU Cortex-M3. Especificar el modelo de CPU nos permite detectar algunos errores de compilación: por ejemplo, ejecutar un programa compilado para Cortex-M4F, que tiene una FPU de hardware, generará un error en QEMU durante su ejecución. -
-machine lm3s6965evb
. Esto le indica a QEMU que emule el LM3S6965EVB, una placa de evaluación que contiene un microcontrolador LM3S6965. -
-nographic
. Esto le indica a QEMU que no inicie su GUI. -
-semihosting-config (..)
. Esto indica a QEMU que habilite el semihosting. El semihosting permite al dispositivo emulado, entre otras cosas, usar la salida estándar del host, la salida estándar del servidor y la entrada estándar del servidor, y crear archivos en el host. -
-kernel $file
. Esto le indica a QEMU qué binario cargar y ejecutar en la máquina emulada.
¡Escribir ese largo comando QEMU es demasiado trabajo! Podemos configurar un ejecutor personalizado para simplificar el proceso.
.cargo/config.toml
tiene un ejecutor comentado que invoca QEMU; descomentémoslo:
head -n3 .cargo/config.toml
[target.thumbv7m-none-eabi]
# Descomente esto para hacer que `cargo run` ejecute programas en QEMU
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"
Este ejecutor solo se aplica al target thumbv7m-none-eabi
, que es nuestro target de compilación predeterminado.
Ahora cargo run
compilará el programa y lo ejecutará en QEMU:
cargo run --example hello --release
Compiling app v0.1.0 (file:///tmp/app)
Finished release [optimized + debuginfo] target(s) in 0.26s
Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel target/thumbv7m-none-eabi/release/examples/hello`
Hello, world!
Depuración
La depuración es fundamental para el desarrollo integrado. Veamos cómo se realiza.
Depurar un dispositivo embebido implica una depuración remota, ya que el programa que queremos depurar no estará ejecutándose en la máquina donde corre el depurador (GDB o LLDB).
La depuración remota implica un cliente y un servidor. En una configuración QEMU, el cliente será un proceso GDB (o LLDB) y el servidor será el proceso QEMU que también ejecuta el programa integrado.
En esta sección usaremos el ejemplo hello
que ya compilamos.
El primer paso de depuración es iniciar QEMU en modo de depuración:
qemu-system-arm \
-cpu cortex-m3 \
-machine lm3s6965evb \
-nographic \
-semihosting-config enable=on,target=native \
-gdb tcp::3333 \
-S \
-kernel target/thumbv7m-none-eabi/debug/examples/hello
Este comando no imprimirá nada en la consola y bloqueará la terminal. Esta vez, hemos pasado dos indicadores adicionales:
-
-gdb tcp::3333
. Esto le indica a QEMU que espere una conexión GDB en el puerto TCP 3333. -
-S
. Esto le indica a QEMU que congele la máquina al iniciar. Sin esto, el programa habría llegado al final del proceso principal antes de que pudiéramos iniciar el depurador.
A continuación, iniciamos GDB en otra terminal y le indicamos que cargue los símbolos de depuración del ejemplo:
gdb-multiarch -q target/thumbv7m-none-eabi/debug/examples/hello
NOTA: Es posible que necesite otra versión de gdb en lugar de gdb-multiarch
,
según la que haya instalado en el capítulo de instalación.
También podría ser arm-none-eabi-gdb
o simplemente gdb
.
Luego, dentro del shell GDB, nos conectamos a QEMU, que está esperando una conexión en el puerto TCP 3333.
target remote :3333
Remote debugging using :3333
Reset () at $REGISTRY/cortex-m-rt-0.6.1/src/lib.rs:473
473 pub unsafe extern "C" fn Reset() -> ! {
Verás que el proceso se detiene y que el contador del programa apunta a una función llamada "Reset". Este es el controlador de reinicio: lo que los núcleos Cortex-M ejecutan al arrancar.
Tenga en cuenta que en algunas configuraciones, en lugar de mostrar la línea
Reset () en $REGISTRY/cortex-m-rt-0.6.1/src/lib.rs:473
como se muestra arriba, gdb puede imprimir algunas advertencias como:
core::num::bignum::Big32x40::mul_small () at src/libcore/num/bignum.rs:254
src/libcore/num/bignum.rs: No existe el archivo o directorio
Es un fallo conocido. Puedes ignorar esas advertencias sin problema; Lo más probable es que la ejecución se encuentre en Reset().
Este controlador de reinicio llamará a nuestra función principal.
Vamos a saltarnos este paso usando un punto de interrupción y el comando continue
.
Para establecer el punto de interrupción, primero veamos dónde queremos interrumpir nuestro código con el comando list
.
list main
Esto mostrará el código fuente, del archivo examples/hello.rs.
6 use panic_halt as _;
7
8 use cortex_m_rt::entry;
9 use cortex_m_semihosting::{debug, hprintln};
10
11 #[entry]
12 fn main() -> ! {
13 hprintln!("Hello, world!").unwrap();
14
15 // salir de QEMU
Nos gustaría agregar un punto de interrupción antes de "¡Hola, mundo!", que está en la línea 13. Lo hacemos con el comando break
:
break 13
Ahora podemos indicarle a gdb que se ejecute hasta nuestra función principal, con el comando continue
:
continue
Continuing.
Breakpoint 1, hello::__cortex_m_rt_main () at examples\hello.rs:13
13 hprintln!("Hello, world!").unwrap();
Ya estamos cerca del código que imprime "¡Hola mundo!". Avancemos con el comando next
.
next
16 debug::exit(debug::EXIT_SUCCESS);
En este punto deberías ver "¡Hola, mundo!" impreso en la terminal que ejecuta qemu-system-arm
.
$ qemu-system-arm (..)
Hello, world!
Llamar a next
nuevamente finalizará el proceso QEMU.
next
[Inferior 1 (Remote target) exited normally]
Ahora puede salir de la sesión GDB.
quit