How to be able to boot into a backup firmware instantly.
This is part 1 in a series of 3 blog-posts describing how I make Marlin and Klipper coexist on my 3-Printers:
- Dual Applications on ATmega2560 – Marlin AND Klipper – FAIL (well almost)
- AVR DualBoot Bootloader
- Install Klipper on your old 3D-Printer and still keep Marlin
I have been using Marlin on my 3D printers for the last 6 years, it is convenient just to be able to copy the gcode to an SD-card, install it, and press print. The only thing that bothers me is that it is so slow to copy things to the SD-card via USB, so you are kind of forced to do it this way.
It seems a lot of printers are moving to Klipper these days, and mine probably should too, this would make network access so much better, but I will lose the simplicity of just being able to print from an SD-card. And you are burning all your bridges, the Marlin firmware on the printer will be overwritten with Klipper.
Wouldn’t it be nice if you could use both Marlin and Klipper on the same 3D-printer, Normally the printer would just run Marlin, but if it received a Klipper directions it would reboot into Klipper.
For this first version, we will settle for a physical switch which depending on the position would boot the printer in either Marlin or Klipper. In this way, we do not need to make any changes to Marlin.
On an Arduino, we normally do have two functionalities in the firmware at the same time, namely the bootloader optiboot and the application sketch we are working on.
On the Atmega reset/interrupt vectors reside at address 0x0000, but if the IVSEL bit in the MCUCR register is set the reset/interrupt vector table will start at the Boot-Loader-base-address, in case of a 1k bootloader 0x3fc00.
Optiboot is not using interrupts, but we can add an interrupt vector table whose function solely is to call the relevant interrupt routine in application number 2
The following memory layout will be chosen
- Primary Application (Marlin) 128k , 0 – 128k
- Secondary Application (Klipper) 127k, 128k – 255k
- Bootloader (modified version ofOptiboot), 1k 255k – 256k
The primary application (which would be Marlin) would not need any modification, but the max size has been decreased t0 128k
The secondary application (which would be Klipper) would need to be relocated to 0x20000, which can be done by adding an extra line to the Klipper Makefile.
Optiboot needs an interrupt table where all the entries just jumps to the interrupt routines provided by the secondary application, and on startup depending on the state of DUALBOOT_BIT either jump to 0x0000 and we will be running the primary application, or set IVSEL and jump to 0x20000 and we will be running the secondary application.
That is all there is to it, (In Denmark we say: Hvor svært kan det være). I wonder why nobody has implemented something like this before, it seems to be straightforward to implement. And I can see other uses where it would be nice to have an alternative firmware available.
Proof of Concept
As I said very few changes are needed to the bootloader, download the sources from GitHub, the changes to Optiboot are trivial as shown below.
diff --git a/optiboot/bootloaders/optiboot/optiboot.c b/optiboot/bootloaders/optiboot/optiboot.c index 6d9e0cc..3a83565 100644 --- a/optiboot/bootloaders/optiboot/optiboot.c +++ b/optiboot/bootloaders/optiboot/optiboot.c @@ -702,6 +702,65 @@ void pre_main(void) { #endif "1:\n" ); +#ifdef DUALBOOT + asm(" jmp (%0+4)\n" + " jmp (%0+8)\n" + " jmp (%0+0xc)\n" ... Lines removed, but I am sure you figured this out ... + " jmp (%0+0xdc)\n" + " jmp (%0+0xe0)\n" + ::"i"((uint32_t)DUALBOOT_BASE)); +#endif } @@ -821,6 +880,15 @@ int main(void) { watchdogConfig(WATCHDOG_OFF); // Note that appstart_vec is defined so that this works with either // real or virtual boot partitions. +#ifdef DUALBOOT + MCUCR = 1<<IVCE; // enable interrupt vectors change + if ((DUALBOOT_PIN && (1<<DUALBOOT_BIT)) == 0) { + MCUCR = 1<<IVSEL; // move interrupt vectors to bootloader + asm(" jmp 0x3000\n"::); + } + MCUCR = 0; // interrupt vectors at 0 +#endif __asm__ __volatile__ ( // Jump to 'save' or RST vector #ifdef VIRTUAL_BOOT_PARTITION
To compile and flash this to the atmega250 I use an USBasp. I have created my own Makefile which just uses the Makefile in Optiboot, here are the relevant lines from my Makefile
MCU. = atmega2560 AVRDUDE = avrdude TTY = $(wildcard /dev/serial/by-id/usb-*) DEVICE = atmega2560 ISP = $(AVRDUDE) -c arduino -P $(TTY) -b 115200 -p $(DEVICE) DUAL_DEFS += -DDUALBOOT=$(DUAL_BASE) -DDUALBOOT_PORT=$(DUAL_PORT) -DDUALBOOT_BIT=$(DUAL_BIT) OPTIBOOT_DIR := optiboot/optiboot/bootloaders/optiboot OPTIBOOT_FLAGS += UART=0 BIGBOOT=1 OPTIBOOT_FLAGS += LED=B7 LED_START_FLASHES=3 OPTIBOOT_FLAGS += ISPTOOL=usbasp ISPPORT= ISPSPEED= LOCKFUSE=FF OPTIBOOT_DUAL += DEFS="$(DUAL_DEFS)" CUSTOM_VERSION=100 OPTIBOOT_SRC := $(OPTIBOOT_DIR)/optiboot.c OPTIBOOT_ELF := $(OPTIBOOT_DIR)/optiboot_$(MCU).elf # .elf -> make $(MCU) compiles to .elf # .flash -> make $(MCU)_isp will flash bootloader to device too # optiboot -> no changes # dualboot -> adds interript vector table and dualboot capability optiboot.flash optiboot.elf dualboot.flash dualboot.elf: $(OPTIBOOT_SRC) Makefile make -C $(OPTIBOOT_DIR) $(OPTIBOOT_FLAGS) \ $(if $(filter dualboot.%,$@), $(OPTIBOOT_DUAL)) \ clean \ $(MCU)$(if $(filter %.flash,$@),_isp) cp $(OPTIBOOT_ELF) $(@:flash=elf) $(if $(filter %.flash,$@), mkdir -p $(FIRMWARE_DIR); cp $(@:flash=elf) $(FIRMWARE_DIR))
To prove the concept I just wrote a tiny application that does interrupt-driven UART communication. The source is below, all the watchdog circus is not necessary but at one point I believed the Watchdog timer was causing me trouble.
#include <avr/pgmspace.h> #include <avr/wdt.h> #include <util/delay.h> #include <stdio.h> // for atmega2560 #define BOOTLOADEREND 0x3ffff #define STR_HELPER(s) #s #define STR(s) STR_HELPER(s) // IO-pin is referenced as Port,Bit f.ex arduino pin13 is B,7 // hence we only only need one define per pin passed from the Makefile //, where optiboot uses separate defines for port and bit // _... macroes unravels the setting needed // G_... is only used by the _... macroes #define G_REG(reg,port,bit) reg ## port #define G_BIT(port,bit) bit #define G_MASK(port,bit) (1<<bit) #define G_STR(port,bit) STR(port) "," STR(bit) #define _PORT(...) G_REG(PORT,__VA_ARGS__) #define _DDR(...) G_REG(DDR,__VA_ARGS__) #define _PIN(...) G_REG(PIN,__VA_ARGS__) #define _BIT(...) G_BIT(__VA_ARGS__) #define _MASK(...) G_MASK(__VA_ARGS__) #define _STR(...) G_STR(__VA_ARGS__) char volatile *cpt; ISR(USART0_UDRE_vect) { char c = *cpt++; if (*cpt == 0) { // c was the last char in buffer UCSR0B &= ~(1 << UDRIE0); // disable UDRE interrupt } UDR0 = c; } void uart_init() { uint16_t baud_setting = (F_CPU / 4 / BAUD - 1) / 2; if (baud_setting > 4095) { UCSR0A = 0; baud_setting = (F_CPU / 8 / BAUD - 1) / 2; } else { UCSR0A = 1 << U2X0; } UBRR0H = baud_setting >> 8; UBRR0L = baud_setting; UCSR0B = 1 << TXEN0; } void puts_polled(char *str) { while (*str) { while (!(UCSR0A & (1 << UDRE0))) ; // wait for UDR0 empty UDR0 = *str++; } } void puts_irq(char *str) { cpt = str; UCSR0B |= (1 << UDRIE0); // enable UDRE interrupt while (UCSR0B & (1 << UDRIE0)) ; // bit will be cleared by final char interrupt } char watchdog_count = 0; volatile char sleeping; ISR(WDT_vect) { sleeping = 0; watchdog_count++; puts_polled(" Bow Wow\r\n"); } void cause_of_reset() { puts_polled("\r\n-> "); if (MCUSR & (1 << WDRF)) { watchdog_count++; puts_polled("Watchdog"); } if (MCUSR & (1 << BORF)) { puts_polled("Brownout"); } if (MCUSR & (1 << EXTRF)) { puts_polled("External"); } if (MCUSR & (1 << PORF)) { puts_polled("Power On"); } puts_polled(" Reset\r\n"); MCUSR = 0; } int main(void) { char buffer[100]; uart_init(); cause_of_reset(); puts_polled(STR(NAME) " hello polled print, base:" STR(BASE)); wdt_enable(WDTO_2S); sei(); _DDR(DUALBOOT_PORT_BIT) &= ~_MASK(DUALBOOT_PORT_BIT); // 50 k internal pullup _PORT(DUALBOOT_PORT_BIT) |= _MASK(DUALBOOT_PORT_BIT); while (1) { wdt_reset(); puts_irq(STR(NAME) " hello IRQ print, base: " STR(BASE)); sprintf(buffer, ", Port,Bit: " _STR(DUALBOOT_PORT_BIT) " = %s, watchdog_count=%d\r\n", (_PIN(DUALBOOT_PORT_BIT) & _MASK(DUALBOOT_PORT_BIT)) ? "high" : "low", watchdog_count); puts_irq(buffer); if (watchdog_count==1) { puts_irq("Enable Watchdog IRQ, set WDTCSR = 1<<WDIE, next expect Barking\r\n"); WDTCSR = 1 << WDIE; // enable watchdo interrupts } puts_irq("sleep - watchdog is set to 2 sec\r\n"); sleeping = 1; while (sleeping); puts_irq("Survived Watchdog\r\n");// WDTCSR.WDIE must be set to enable watchdog interrupt } }
Both primary.elf and secondary.elf are compiled from the same source, below are the relevant lines from the Makefile
LDFLAGS += -Wl,--section-start=.text=0x20000 primary.elf: hello.c $(CC) -DNAME=$(@:.elf=) -DBASE=0 $(CFLAGS) -o $@ $< secondary.elf: hello.c $(CC) -DNAME=$(@:.elf=) -DBASE=0x20000 $(CFLAGS) $(LDFLAGS) -o $@ $< %.hex: %elf avr-objcopy -j .text -j .data -O ihex $< $@ flash: primary.hex secondary.hex $(ISP) -U flash:w:primary.hex:i -U flash:w:secondary.hex:i
To flash both firmware to the atmega2560. Avrdude is used to program it via the newly installed dualboot version of the optiboot bootloader on the atmega2560, the USBasp was only used for flashing the bootloader.
Success – but FAILING with Marling and Klipper as the secondary application
So it is testing time, to see what is going on the serial port I am just using
picocom -l /dev/serial/by-id/usb-* -b 115200
below is the output from my hello.c program
-> External Reset primary hello polled print, base:0 primary hello IRQ print, base: 0, Port,Bit: G,0 = high, watchdog_count=0 sleep - watchdog is set to 2 sec -> Watchdog Reset primary hello polled print, base:0 primary hello IRQ print, base: 0, Port,Bit: G,0 = high, watchdog_count=1 Enable Watchdog IRQ, set WDTCSR = 1<<WDIE, next expect Barking sleep - watchdog is set to 2 sec Bow Wow Survived Watchdog primary hello IRQ print, base: 0, Port,Bit: G,0 = high, watchdog_count=2 sleep - watchdog is set to 2 sec -> Watchdog Reset ....
Now we will pull pin G,0 down to ground and try again and we will see
-> External Reset secondary hello polled print, base:0x20000 secondary hello IRQ print, base: 0x20000, Port,Bit: G,0 = low, watchdog_count=0 sleep - watchdog is set to 2 sec -> Watchdog Reset secondary hello polled print, base:0x20000 secondary hello IRQ print, base: 0x20000, Port,Bit: G,0 = low, watchdog_count=1 Enable Watchdog IRQ, set WDTCSR = 1<<WDIE, next expect Barking sleep - watchdog is set to 2 sec Bow Wow Survived Watchdog secondary hello IRQ print, base: 0x9000, Port,Bit: G,0 = low, watchdog_count=2 sleep - watchdog is set to 2 sec -> Watchdog Reset
Everything works as expected, now it is time to try some real applications,
- Marlin is compiled with out any changes
- Klipper need the addition of –section-start directive to instruct the linker to relocate klipper to the upper address space.
- flash as before
Marlin works fine, but Klipper FAIL. hmm, the next thing to try is of course:
- Klipper compiled out of the box without any changes
- Marlin relocated to 0x20000
- flash as before
This time Klipper works as expected, but Marlin FAIL.
So we FAILED on our initial goal but also proved that dual applications are indeed possible.
- What is wrong with Klipper or Marlin why will they not run in the upper address space, or
- Are more changes needed to Optiboot to make this work?
The seasoned hacker has probably figured out what is wrong by now, a little hint might be that the atmega2560 is an 8-bit micro-processor working in a 16-bit address space (Harward architecture), but the processor we are using has 256 kbyte of FLASH memory (18bit)
So there might be good reasons why nobody has implemented a dualboot-bootloader on AVR before ;-(
Next post: