diff --git a/06_drivers_gpio_uart/Makefile b/06_drivers_gpio_uart/Makefile index b6b62c58..b84ca328 100644 --- a/06_drivers_gpio_uart/Makefile +++ b/06_drivers_gpio_uart/Makefile @@ -5,6 +5,12 @@ # Default to the RPi3 BSP ?= rpi3 +# Default to a serial device name that is common in Linux. +DEV_SERIAL ?= /dev/ttyUSB0 + +# Query the host system's kernel name +UNAME_S = $(shell uname -s) + # BSP-specific arguments ifeq ($(BSP),rpi3) TARGET = aarch64-unknown-none-softfloat @@ -51,13 +57,23 @@ KERNEL_ELF = target/$(TARGET)/release/kernel DOCKER_IMAGE = rustembedded/osdev-utils DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t +DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils +DOCKER_ARG_DEV = --privileged -v /dev:/dev DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) DOCKER_ELFTOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) -EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +# Dockerize commands that require USB device passthrough only on Linux +ifeq ($(UNAME_S),Linux) + DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) + + DOCKER_MINITERM = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) +endif + +EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +EXEC_MINITERM = ruby ../utils/miniterm.rb -.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu clippy clean readelf objdump nm check +.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check all: $(KERNEL_BIN) @@ -78,6 +94,9 @@ qemu: $(KERNEL_BIN) @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) endif +miniterm: + @$(DOCKER_MINITERM) $(EXEC_MINITERM) $(DEV_SERIAL) + clippy: RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) diff --git a/06_drivers_gpio_uart/README.md b/06_drivers_gpio_uart/README.md index 58d46e4c..da30d32b 100644 --- a/06_drivers_gpio_uart/README.md +++ b/06_drivers_gpio_uart/README.md @@ -66,18 +66,29 @@ on the SD card._ 5. Insert the SD card into the RPi and connect the USB serial to your host PC. - Wiring diagram at [top-level README](../README.md#usb-serial). -6. Run `screen` (you might need to install it first): +6. Run the `miniterm` target, which opens the UART device on the host: ```console -$ sudo screen /dev/ttyUSB0 230400 +$ make miniterm ``` -> ❗ **NOTE**: Depending on your host operating system, the serial device name might differ. -> For example, on `macOS`, it might be something like `/dev/tty.usbserial-0001`. +> ❗ **NOTE**: `Miniterm` assumes a default serial device name of `/dev/ttyUSB0`. Depending on your +> host operating system, the device name might differ. For example, on `macOS`, it might be +> something like `/dev/tty.usbserial-0001`. In this case, please give the name explicitly: -7. Hit Enter to kick off the kernel boot process. Observe the output: ```console +$ DEV_SERIAL=/dev/tty.usbserial-0001 make miniterm +``` + +7. Hit Enter after seeing "`Connected`" to kick off the kernel boot process and observe + the output: + +```console +Miniterm 1.0 + + +[MT] ✅ Connected [0] Booting on: Raspberry Pi 3 [1] Drivers loaded: 1. BCM GPIO @@ -86,7 +97,7 @@ $ sudo screen /dev/ttyUSB0 230400 [3] Echoing input now ``` -8. Exit screen by pressing ctrl-a ctrl-d or disconnecting the USB serial. +8. Exit by pressing ctrl-c. ## Diff to previous ```diff @@ -116,6 +127,59 @@ diff -uNr 05_safe_globals/Cargo.toml 06_drivers_gpio_uart/Cargo.toml [target.'cfg(target_arch = "aarch64")'.dependencies] cortex-a = { version = "4.x.x" } +diff -uNr 05_safe_globals/Makefile 06_drivers_gpio_uart/Makefile +--- 05_safe_globals/Makefile ++++ 06_drivers_gpio_uart/Makefile +@@ -5,6 +5,12 @@ + # Default to the RPi3 + BSP ?= rpi3 + ++# Default to a serial device name that is common in Linux. ++DEV_SERIAL ?= /dev/ttyUSB0 ++ ++# Query the host system's kernel name ++UNAME_S = $(shell uname -s) ++ + # BSP-specific arguments + ifeq ($(BSP),rpi3) + TARGET = aarch64-unknown-none-softfloat +@@ -51,13 +57,23 @@ + DOCKER_IMAGE = rustembedded/osdev-utils + DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial + DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t ++DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils ++DOCKER_ARG_DEV = --privileged -v /dev:/dev + + DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) + DOCKER_ELFTOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) + +-EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) ++# Dockerize commands that require USB device passthrough only on Linux ++ifeq ($(UNAME_S),Linux) ++ DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) ++ ++ DOCKER_MINITERM = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) ++endif ++ ++EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) ++EXEC_MINITERM = ruby ../utils/miniterm.rb + +-.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu clippy clean readelf objdump nm check ++.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check + + all: $(KERNEL_BIN) + +@@ -78,6 +94,9 @@ + @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) + endif + ++miniterm: ++ @$(DOCKER_MINITERM) $(EXEC_MINITERM) $(DEV_SERIAL) ++ + clippy: + RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) + + diff -uNr 05_safe_globals/src/_arch/aarch64/cpu.rs 06_drivers_gpio_uart/src/_arch/aarch64/cpu.rs --- 05_safe_globals/src/_arch/aarch64/cpu.rs +++ 06_drivers_gpio_uart/src/_arch/aarch64/cpu.rs diff --git a/07_uart_chainloader/README.md b/07_uart_chainloader/README.md index 907b6770..a19d23b3 100644 --- a/07_uart_chainloader/README.md +++ b/07_uart_chainloader/README.md @@ -103,20 +103,7 @@ Binary files 06_drivers_gpio_uart/demo_payload_rpi4.img and 07_uart_chainloader/ diff -uNr 06_drivers_gpio_uart/Makefile 07_uart_chainloader/Makefile --- 06_drivers_gpio_uart/Makefile +++ 07_uart_chainloader/Makefile -@@ -5,6 +5,12 @@ - # Default to the RPi3 - BSP ?= rpi3 - -+# Default to a serial device name that is common in Linux. -+DEV_SERIAL ?= /dev/ttyUSB0 -+ -+# Query the host system's kernel name -+UNAME_S = $(shell uname -s) -+ - # BSP-specific arguments - ifeq ($(BSP),rpi3) - TARGET = aarch64-unknown-none-softfloat -@@ -15,7 +21,8 @@ +@@ -21,7 +21,8 @@ OBJDUMP_BINARY = aarch64-none-elf-objdump NM_BINARY = aarch64-none-elf-nm LINKER_FILE = src/bsp/raspberrypi/link.ld @@ -126,7 +113,7 @@ diff -uNr 06_drivers_gpio_uart/Makefile 07_uart_chainloader/Makefile else ifeq ($(BSP),rpi4) TARGET = aarch64-unknown-none-softfloat KERNEL_BIN = kernel8.img -@@ -25,7 +32,8 @@ +@@ -31,7 +32,8 @@ OBJDUMP_BINARY = aarch64-none-elf-objdump NM_BINARY = aarch64-none-elf-nm LINKER_FILE = src/bsp/raspberrypi/link.ld @@ -136,34 +123,25 @@ diff -uNr 06_drivers_gpio_uart/Makefile 07_uart_chainloader/Makefile endif # Export for build.rs -@@ -51,13 +59,24 @@ - DOCKER_IMAGE = rustembedded/osdev-utils - DOCKER_CMD = docker run --rm -v $(shell pwd):/work/tutorial -w /work/tutorial - DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i -t -+DOCKER_ARG_DIR_UTILS = -v $(shell pwd)/../utils:/work/utils -+DOCKER_ARG_DEV = --privileged -v /dev:/dev - - DOCKER_QEMU = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE) - DOCKER_ELFTOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE) - --EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) -+# Dockerize commands that require USB device passthrough only on Linux -+ifeq ($(UNAME_S),Linux) -+ DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) -+ +@@ -67,13 +69,14 @@ + ifeq ($(UNAME_S),Linux) + DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV) + +- DOCKER_MINITERM = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) + DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_UTILS) $(DOCKER_IMAGE) -+endif + endif --.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu clippy clean readelf objdump nm check -+EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) + EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE) +-EXEC_MINITERM = ruby ../utils/miniterm.rb +EXEC_MINIPUSH = ruby ../utils/minipush.rb -+ + +-.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check +.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu qemuasm chainboot clippy clean readelf objdump nm \ + check all: $(KERNEL_BIN) -@@ -71,13 +90,19 @@ +@@ -87,15 +90,18 @@ $(DOC_CMD) --document-private-items --open ifeq ($(QEMU_MACHINE_TYPE),) @@ -178,13 +156,14 @@ diff -uNr 06_drivers_gpio_uart/Makefile 07_uart_chainloader/Makefile + @$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) -d in_asm endif +-miniterm: +- @$(DOCKER_MINITERM) $(EXEC_MINITERM) $(DEV_SERIAL) +chainboot: + @$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(CHAINBOOT_DEMO_PAYLOAD) -+ + clippy: RUSTFLAGS="$(RUSTFLAGS_PEDANTIC)" $(CLIPPY_CMD) - -@@ -88,7 +113,10 @@ +@@ -107,7 +113,10 @@ readelf --headers $(KERNEL_ELF) objdump: $(KERNEL_ELF) diff --git a/utils/minipush.rb b/utils/minipush.rb index 61447858..9c743bd3 100755 --- a/utils/minipush.rb +++ b/utils/minipush.rb @@ -5,60 +5,26 @@ # # Copyright (c) 2020 Andre Richter -require 'rubygems' -require 'bundler/setup' -require 'io/console' -require 'colorize' +require_relative 'miniterm' require 'ruby-progressbar' -require 'serialport' -require 'timeout' require_relative 'minipush/progressbar_patch' +require 'timeout' -class ConnectionError < StandardError; end class ProtocolError < StandardError; end # The main class -class MiniPush +class MiniPush < MiniTerm def initialize(serial_name, binary_image_path) - @target_serial_name = serial_name - @target_serial = nil + super(serial_name) + + @name_short = 'MP' @binary_image_path = binary_image_path @binary_size = nil @binary_image = nil - @host_console = IO.console end private - def serial_connected? - File.exist?(@target_serial_name) - end - - def wait_for_serial - loop do - break if serial_connected? - - print "\r[MP] ⏳ Waiting for #{@target_serial_name}" - sleep(1) - end - end - - def open_serial - wait_for_serial - - @target_serial = SerialPort.new(@target_serial_name, 230_400, 8, 1, SerialPort::NONE) - - # Ensure all output is immediately flushed to the device. - @target_serial.sync = true - rescue Errno::EACCES => e - puts - puts "[MP] 🚫 #{e.message} - Maybe try with 'sudo'" - exit - else - puts - puts '[MP] ✅ Connected' - end - # The three characters signaling the request token are expected to arrive as the last three # characters _at the end_ of a character stream (e.g. after a header print from Miniload). def wait_for_binary_request @@ -92,7 +58,7 @@ class MiniPush def send_binary pb = ProgressBar.create( total: @binary_size, - format: '[MP] ⏩ Pushing %k KiB %b🦀%i %p%% %r KiB/s %a', + format: "[#{@name_short}] ⏩ Pushing %k KiB %b🦀%i %p%% %r KiB/s %a", rate_scale: ->(rate) { rate / 1024 }, length: 92 ) @@ -104,69 +70,16 @@ class MiniPush end end - def terminal - @host_console.raw! - - Thread.abort_on_exception = true - Thread.report_on_exception = false - - # Receive from target and print on host console. - target_to_host = Thread.new do - loop do - char = @target_serial.getc - - raise ConnectionError if char.nil? - - # onlcr - @host_console.putc("\r") if char == "\n" - @host_console.putc(char) - end - end - - # Transmit host console input to target. - loop do - c = @host_console.getc - - # CTRL + C in raw mode was pressed - if c == "\u{3}" - target_to_host.kill - break - end - - @target_serial.putc(c) - end - end - - def connetion_reset - @target_serial&.close - @target_serial = nil - @host_console.cooked! - end - - # When the serial lost power or was removed during R/W operation. - def handle_reconnect - connetion_reset - - puts - puts "[MP] ⚡ #{'Connection Error: Reinsert the USB serial again'.light_red}" - end - # When the serial is still powered. def handle_protocol_error connetion_reset puts - puts "[MP] ⚡ #{'Protocol Error: Remove and insert the USB serial again'.light_red}" + puts "[#{@name_short}] ⚡ " \ + "#{'Protocol Error: Remove and insert the USB serial again'.light_red}" sleep(1) while serial_connected? end - def handle_unexpected(error) - connetion_reset - - puts - puts "[MP] ⚡ #{"Unexpected Error: #{error.inspect}".light_red}" - end - public def run @@ -187,7 +100,7 @@ class MiniPush ensure connetion_reset puts - puts '[MP] Bye 👋' + puts "[#{@name_short}] Bye 👋" end end diff --git a/utils/miniterm.rb b/utils/miniterm.rb new file mode 100755 index 00000000..fb05189a --- /dev/null +++ b/utils/miniterm.rb @@ -0,0 +1,138 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# SPDX-License-Identifier: MIT OR Apache-2.0 +# +# Copyright (c) 2020 Andre Richter + +require 'rubygems' +require 'bundler/setup' +require 'io/console' +require 'colorize' +require 'serialport' + +class ConnectionError < StandardError; end + +# The main class +class MiniTerm + def initialize(serial_name) + @name_short = 'MT' + @target_serial_name = serial_name + @target_serial = nil + @host_console = IO.console + end + + private + + def serial_connected? + File.exist?(@target_serial_name) + end + + def wait_for_serial + loop do + break if serial_connected? + + print "\r[#{@name_short}] ⏳ Waiting for #{@target_serial_name}" + sleep(1) + end + end + + def open_serial + wait_for_serial + + @target_serial = SerialPort.new(@target_serial_name, 230_400, 8, 1, SerialPort::NONE) + + # Ensure all output is immediately flushed to the device. + @target_serial.sync = true + rescue Errno::EACCES => e + puts + puts "[#{@name_short}] 🚫 #{e.message} - Maybe try with 'sudo'" + exit + else + puts + puts "[#{@name_short}] ✅ Connected" + end + + def terminal + @host_console.raw! + + Thread.abort_on_exception = true + Thread.report_on_exception = false + + # Receive from target and print on host console. + target_to_host = Thread.new do + loop do + char = @target_serial.getc + + raise ConnectionError if char.nil? + + # Translate incoming newline to newline + carriage return. + @host_console.putc("\r") if char == "\n" + @host_console.putc(char) + end + end + + # Transmit host console input to target. + loop do + c = @host_console.getc + + # CTRL + C in raw mode was pressed. + if c == "\u{3}" + target_to_host.kill + break + end + + @target_serial.putc(c) + end + end + + def connetion_reset + @target_serial&.close + @target_serial = nil + @host_console.cooked! + end + + # When the serial lost power or was removed during R/W operation. + def handle_reconnect + connetion_reset + + puts + puts "[#{@name_short}] ⚡ #{'Connection Error: Reinsert the USB serial again'.light_red}" + end + + def handle_unexpected(error) + connetion_reset + + puts + puts "[#{@name_short}] ⚡ #{"Unexpected Error: #{error.inspect}".light_red}" + end + + public + + def run + open_serial + terminal + rescue ConnectionError, EOFError, Errno::EIO + handle_reconnect + retry + rescue StandardError => e + handle_unexpected(e) + ensure + connetion_reset + puts + puts "[#{@name_short}] Bye 👋" + end +end + +if __FILE__ == $PROGRAM_NAME + puts 'Miniterm 1.0'.cyan + puts + + # CTRL + C handler. Only here to suppress Ruby's default exception print. + trap('INT') do + # The `ensure` block from `MiniTerm::run` will run after exit, restoring console state. + exit + end + + MiniTerm.new(ARGV[0]).run +end