5 Commits

Author SHA1 Message Date
445e187722 tsb: document the three tiers at full parity in the build file
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 16:50:18 +02:00
250aba5cfb tsb: protocol test covers the password gate and emergency erase
Each scenario group now runs on its own freshly-reset device: the round-trip
on a blank config page, plus a password-config device that must be sent the
password after the knock to activate, and an emergency-erase device where a
0-byte + two confirms wipes flash, EEPROM and the config page (verified by
reading all three back as 0xff). All three tiers pass every group.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 16:49:20 +02:00
5c900720e3 tsb: pure and tricks tiers reach full oracle feature parity
Both tiers gain the features the asm tier already carries — one-wire
half-duplex (via libavr's new .half_duplex), the config-page activation
timeout, and emergency erase (password \0 + double-confirm wipes flash,
EEPROM and the config page) — on top of the watchdog bail, password gate and
config/flash/EEPROM read-write they already had. pure stays idiomatic
(flash_table info block, one function per command) at 950 B; tricks keeps its
compiler trickery (call-saved global-register page walk, unified runtime-flag
paths pinned noinline/noclone, streaming stores, arithmetic command decode)
at 808 B. Both byte-identical across generated and reflect modes; the size
gradient across the three tiers is now 502 / 808 / 950 B.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 16:47:21 +02:00
7d6ef959b2 tsb: asm tier reaches full oracle feature parity at 502 B
Rewrite the inline-asm tier so it matches the hand-written fixed-baud oracle's
feature set inside the 512 B boot section: watchdog-reset bail, one-wire
half-duplex (RXEN/TXEN toggled per direction, TX turnaround guard),
config-page activation timeout, the password gate (wrong byte hangs draining
the UART), emergency erase (password \0 + double-confirm wipes flash, EEPROM
and the config page), and config/flash/EEPROM read-write. Every geometry,
baud and info-block constant comes from libavr consteval; only the dense
control flow is hand-written. 502 B, byte-identical across generated and
reflect modes.

Test harness: seed the config page from TSB_CONFIG so the password and
emergency-erase paths are exercisable, and clear simavr's AVR_UART_FLAG_POLL_
SLEEP — a host-CPU-saving usleep(1)-per-idle-poll hack that models no hardware
and paces a one-wire loader (which releases TX between bytes) in real time,
distorting protocol timing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 16:23:16 +02:00
11ffbce2e2 tsb: vendor the fixed-baud assembly oracle as the size/feature bar
The Seed Robotics native-UART fixed-baud TinySafeBoot (GPLv3), reference
only — not built. Assembles to 500 B with the full feature set, proving
≤512 B and full feature parity are simultaneously reachable. Also drops the
stale empty stk500v2/ leftover.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHeP42XU3wf6RfhyuxBTE5
2026-07-19 15:29:58 +02:00
8 changed files with 1457 additions and 422 deletions

View File

@@ -45,9 +45,13 @@ endif()
# loader has no use for the crt or the vector table. The naked entry sits in # loader has no use for the crt or the vector table. The naked entry sits in
# .vectors, laid first, and runs. The boot base is FLASHEND+1 minus the section # .vectors, laid first, and runs. The boot base is FLASHEND+1 minus the section
# size; the linker section-start and the source's boot_bytes agree. # size; the linker section-start and the source's boot_bytes agree.
# tsb_asm — inline-asm variant, the headline: ≤512 B, the 512 B section. # All three implement the full oracle feature set (see oracle/README.md):
# tsb_pure / tsb_tricks — pure-C++ and compiler-trickery variants, larger, # watchdog bail, one-wire half-duplex, config-page activation timeout, password
# shown in the 1 KB section (BOOTSZ=10) they fit. # gate, emergency erase, config/flash/EEPROM read-write. They differ only in how.
# tsb_asm — minimal inline asm, the headline: 502 B in the 512 B section,
# matching the hand-written oracle's size and features.
# tsb_tricks — compiler trickery, no asm: 808 B in the 1 KB section (BOOTSZ=10).
# tsb_pure — pure idiomatic libavr: 950 B in the 1 KB section.
# #
# add_tsb_variant(<name> <boot-section-bytes>) # add_tsb_variant(<name> <boot-section-bytes>)
function(add_tsb_variant name bytes) function(add_tsb_variant name bytes)

47
oracle/README.md Normal file
View File

@@ -0,0 +1,47 @@
# Oracle — the hand-written TinySafeBoot assembly
`tsb-fixedbaud.asm` is the reference implementation this port is measured
against: the **native-UART, fixed-baud** TinySafeBoot bootloader, hand-written
in AVR assembly. It is the size-and-feature bar for the port's `tsb_asm` tier.
- **Source**: <https://github.com/seedrobotics/tinysafeboot>
(`firmware_ASM/latest_stable_release/20200727-fixedbaud/main.asm`), the Seed
Robotics fixed-baud fork of Julien Thomas' TinySafeBoot.
- **License**: GPLv3 (see the header in the file). It is vendored here **only as
a reference oracle** — it is not compiled, linked, or distributed as part of
the MIT-licensed port. Mere aggregation.
## Why this variant
The user chose the fixed-baud, hardware-UART variant deliberately: it is the one
whose feature set the port must match. It fits the **complete** TSB feature set
into the 512-byte ATmega boot section:
| Feature | Oracle routine |
|---|---|
| Watchdog-reset bail straight to the app | `RESET` (WDRF check) |
| One-wire half-duplex (RX/TX shorted): RXEN/TXEN toggled per direction, TX turnaround guard | `SetRX` / `SetTX` / `TransmitByte` |
| Activation timeout read from the config page, with a lockout-proof minimum | `WRX1To` (uses `utimeoutH`) |
| 3×`@` activation knock | `ActCharRcvd` |
| Password gate; wrong byte hangs (still draining the UART) | `CheckPassword` |
| Emergency erase on password `\0` + double-confirm — wipes flash, EEPROM and the config page | `EmergencyErase` |
| Device-info block (16 bytes) | `SendDeviceInfo` / `DEVICEINFO` |
| App-flash read/write (`f`/`F`), EEPROM read/write (`e`/`E`), config read/write (`c`/`C`) | `CheckCommands` |
## Assembled size (the bar)
Assembled for the ATmega328P with `avra`:
```
avra -I /usr/share/avra tsb-fixedbaud.asm # after uncommenting .include "m328Pdef.inc"
# Code : 250 words (500 bytes) — the whole loader, all features, in the 512 B section
```
**500 bytes with every feature** — the proof that ≤512 B and full feature parity
are simultaneously reachable. The port's `tsb_asm` tier matches this bar; `tsb_pure`
and `tsb_tricks` implement the same protocol at larger sizes in the 1 KB section,
trading bytes for readability.
The oracle targets 20 MHz / 33333 baud; the port targets 16 MHz / 115200 baud
(what the simavr protocol test drives). Baud and geometry differ, code size and
feature set do not.

776
oracle/tsb-fixedbaud.asm Normal file
View File

@@ -0,0 +1,776 @@
;***********************************************************************
;***********************************************************************
;***********************************************************************
; TinySafeBoot - The Universal Bootloader for AVR ATmegas
;***********************************************************************
;***********************************************************************
;***********************************************************************
;
;-----------------------------------------------------------------------
; 2020 - Version using native UART, Fixed Baud by Seed Robotics in 2020
;-----------------------------------------------------------------------
; meant for use on ATMEGA devices only (with native UART - UART0)
;
; Main differences to Regular TSB Bootloader:
; - Uses a native UART (UART0); therefore not compatible with ATTINY
; - Baud rate is fixed (set by a macro in the code). No auto bauding.
; - Disables TX while not transmitting to allow for one wire flashing
; (where RX and TX are shorted, for a multi drop bus)
; - Also works with separate RX and TX; however an external pull up
; on TX _may_ be required; alternatively you can modify the code
; in the ReceiveByte routine so that it won't disable TX.
; - FIXES:
; - situations where booting onto a bus with active communication could
; lock the autobauding feature
; - times out and boots to application code if the host stops interacting
; with the bootloader
;
;-----------------------------------------------------------------------
; Extended by Seed Robotics from 2017
;-----------------------------------------------------------------------
; Seed Robotics contributions are available from the Github
; repository github.com/seedrobotics
; The License and conditions remain as stated below, in the
; original notice.
;
;
;-----------------------------------------------------------------------
; Written in 2011-2015 by Julien Thomas
;
; This program is free software; you can redistribute it and/or
; modify it under the terms of the GNU General Public License
; as published by the Free Software Foundation; either version 3
; of the License, or (at your option) any later version.
; This program is distributed in the hope that it will be useful,
; but WITHOUT ANY WARRANTY; without even the implied warranty
; of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
; See the GNU General Public License for more details.
; You should have received a copy of the GNU General Public License
; along with this program; if not, see:
; http://www.gnu.org/licenses/
;-----------------------------------------------------------------------
;
;
;
;***********************************************************************
; OVERVIEW
;***********************************************************************
;
; TSB assembly source is organized in 4 segments (approx. line numbers)
;
; ~ 50 ... Global definitions
; ~ ... TSB for ATmegas
;
;***********************************************************************
; ADJUSTMENTS FOR INDIVIDUAL ASSEMBLY
;***********************************************************************
;
; This Sourcecode is directly compatible to: AVRASM2, GAVRASM
;
.nolist
;
;-----------------------------------------------------------------------
; SPECIFY TARGET AVR
;-----------------------------------------------------------------------
;
; Comment in and provide def.inc file for target device
;
; [Examples]
;
;.include "tn2313def.inc"
;.include "tn85def.inc"
;.include "m8515def.inc"
;.include "m168def.inc"
;.include "m161def.inc"
;.include "m324Adef.inc"
;.include "m328Pdef.inc"
;.include "tn441def.inc"
;.include "tn167def.inc"
;.include "tn861def.inc"
;.include "tn841def.inc"
;.include "tn84def.inc"
;.include "m8def.inc"
;.include "m644PAdef.inc"
;.include "m644def.inc"
;.include "tn167def.inc"
;.include "tn25def.inc"
;
; [...]
;
;
.list
;
;-----------------------------------------------------------------------
; BUILD INFO
;-----------------------------------------------------------------------
; YY = Year - MM = Month - DD = Day
.set YY = 21
.set MM = 12
.set DD = 21
;
.set BUILDSTATE = $F3 ; F1 fixed baud, pull up, derived from original (modified for fixed baud)
; F2 fixed baud, pull up, guaranteed minimum activation timeout in case of userpage data corruption
; F3 adds a CONSTANT with clock speed (Mhz) as word in the last page of memory (clock speed our defined CONSTANT)
;
;-----------------------------------------------------------------------
; TSB / TSB-INSTALLER SWITCH
;-----------------------------------------------------------------------
; 0 = Regular assembly to target address
; Other value = NOT SUPPORTED
;
.set TSBINSTALLER = 0
;
;-----------------------------------------------------------------------
; F_CPU and Baud rate setting
;-----------------------------------------------------------------------
.equ F_CPU = 20000000
.equ BAUD = 33333 ; baudrate (notice some possible wrong cals: example for 56K, it is actually 55,555, so for BAUD_PRESC give an INT result of 8, we must set BAUD to 55500)
.equ BAUD_PRESCx10 = (F_CPU * 10/16/BAUD) - 10 ; baud prescale (regular formula = F_CPU * 10/16/BAUD - 1 but we do it x10 to check the rounding)
; arredondar acima se necesssario
.if BAUD_PRESCx10 - ( (BAUD_PRESCx10 / 10) * 10 ) >= 5 ; calculate the remainder: we rely on the fact these are integer divisions. Therefore, dividing by 10, rounds DOWN in integer division
.equ BAUD_PRESC = (F_CPU/16/BAUD)
.warning "Incrementing default BAUD_PRESC formula by 1 due to rounding."
.else
.equ BAUD_PRESC = (F_CPU/16/BAUD) - 1
.warning "Using default BAUD_PRESC formula (no rounding up)"
.endif
.if BAUD_PRESC > 255
.error "ERROR: BAUD RATE TOO LOW. WE ONLY WRITE THE UBRRL REGISTER, SO UBRR MUST BE <255 FOR THIS CLOCK FREQ AND BAUD"
.endif
;***********************************************************************
; AUTO-ADJUST FOR DIFFERENT ASSEMBLY OPTIONS
;***********************************************************************
;
; Always set TINYMEGA=1 bc this code only supports ATMEGA
.equ TINYMEGA=1
.if FLASHEND > ($7fff)
.error "SORRY! DEVICES OVER 64 KB NOT SUPPORTED YET."
.exit
.endif
;-----------------------------------------------------------------------
; Workarounds for devices with renamed or missing definitions
;-----------------------------------------------------------------------
;
.ifndef SPMCSR ; SPMEN / PGERS / ...
.equ SPMCSR = SPMCR
.endif
.ifndef MCUSR ; PORF / EXTRF / BORF / WDRF
.equ MCUSR = MCUCSR
.endif
; Detect Attiny441/841 to amend missing pagesize and apply 4-page mode
.set FOURPAGES = 0
.if ((SIGNATURE_000 == $1E) && (SIGNATURE_002 == $15) && (SIGNATURE_001 == $92))
.equ PAGESIZE = 32
.set FOURPAGES = 1
.message "ATTINY441: 4-PAGE-ERASE MODE"
.endif
.if ((SIGNATURE_000 == $1E) && (SIGNATURE_002 == $15) && (SIGNATURE_001 == $93))
.equ PAGESIZE = 32
.set FOURPAGES = 1
.message "ATTINY841: 4-PAGE-ERASE MODE"
.endif
;-----------------------------------------------------------------------
; Universal Constants and Registers
;-----------------------------------------------------------------------
.equ REQUEST = '?' ; request / answer / go on
.equ CONFIRM = '!' ; confirm / attention
; Current bootloader date coded into 16-bit number
.equ BUILDDATE = YY * 512 + MM * 32 + DD
; Other
.equ INFOLEN = 8 ; *Words* of Device Info
.equ BUFFER = SRAM_START
; Registers (in use by TSB-Firmware and TSB-Installer for ATtinys)
.def avecl = r4 ; application vector temp low
.def avech = r5 ; application vector temp high
.def tmp1 = r16 ; these are
.def tmp2 = r17 ; universal
.def tmp3 = r18 ; temporary
.def tmp4 = r19 ; registers
.def bcnt = r20 ; page bytecounter
.def cntr1 = r21 ; timeout counter
.def rxen = r22 ; check if RX enabled (meaning TX disabled)
.def utimeoutH = r23 ; user timeout High byte
; special purpose registers start at R26
;
;
;***********************************************************************
;***********************************************************************
;***********************************************************************
; START OF TSB FOR ATMEGAS
;***********************************************************************
;***********************************************************************
;***********************************************************************
;
; TSB for ATmegas is always coded directly to target address.
.if TINYMEGA == 1
.message "ASSEMBLY OF TSB FOR ATMEGA"
.equ BOOTSTART = (FLASHEND+1)-256 ; = 512 Bytes
.equ LASTPAGE = BOOTSTART - PAGESIZE ; = 1 page below TSB!
.org BOOTSTART
RESET:
cli
in tmp4, MCUSR ; check reset condition
sbrc tmp4, WDRF ; in case of a Watchdog reset
rjmp APPJUMP ; immediately leave TSB
ldi tmp1, low (RAMEND) ; write ramend low
out SPL, tmp1 ; into SPL (stackpointer low)
.ifdef SPH
ldi tmp1, high(RAMEND) ; write ramend high for ATtinys
out SPH, tmp1 ; with SRAM > 256 bytes
.message "PROVIDING FOR STACK BIGGER THAN 256 BYTES"
.endif
.ifndef DDRD2
.equ DDRD2 = DDD2
.endif
;-----------------------------------------------------------------------
; ACTIVATION CHECK
;-----------------------------------------------------------------------
; Configure UART; no autobauding in this version
ldi tmp1,BAUD_PRESC ; load baud prescale
sts UBRR0L,tmp1 ; set baud prescale
; ldi tmp2,HIGH(bpsc) ; save code by not loading UBBRH
;sts UBRRH,tmp2 ; to UBRR0
;ldi tmp2,( (1<<RXEN0) ) ; enable transmiter and receiver
;sts UCSR0B,tmp2
; Enable Pull up on Port D2 (PD2)
cbi DDRD, DDRD2
sbi PORTD, PORTD2
; we will enable RNEN/TXEN in the ReceiveByte and TransmitByte routines
rcall ZtoLASTPAGE ; set Z to start'o'LASTPAGE
adiw zl, 2 ; skip first 2 bytes (APPJUMP)
lpm utimeoutH, z+ ; load TIMEOUT byte and store for use in RX byte timeout
ori utimeoutH, (F_CPU / 1000000); prevent bootloader lockout due if it gets an invalid (to small) timeout setting
; this ensures value is at least the clock rate, which shoudl give about 40ms
clr tmp2 ; apparently at times this is not set to 0 on boot? (seen while in debugWire)
clr rxen ; same as above
WRX1To:
; we'll check the X register which is where ReceibeByte controls the timeout
; the overall timeout of receive byte is the timeout set by the user
; therefore, if we get characters while X> 0 we're attempting to activate bootloader;
; if not, if X=0 we timedout and go to app start
rcall ReceiveByte
brcs WRX2To ; if X got to 0 (i.e. carry set), assume we timed out
cpi tmp1, '@' ; did we get an activation char = "@"
breq ActCharRcvd
WRX2To:
rjmp APPJUMP ; not an activation char goto APPJUMP in LASTPAGE
ActCharRcvd:
inc tmp2
cpi tmp2, 3
brne WRX1To ; branch if not yet at 3;
; otherwise fall through to password check
;-----------------------------------------------------------------------
; CHECK PASSWORD / EMERGENCY ERASE
;-----------------------------------------------------------------------
; we use the user timeout (utimeoutH) register for COMM timeout
; when we don't get valid data
; increase this value to a fixed one now, to cope
; with cases where the user timeout is set so low that we don't have time to
; do anything
ldi utimeoutH, (F_CPU / 78500) ; this should result in 255 for 20Mhz and proportionally
; less for lower Clocks, so that we get the same time approx. 2.4sec
CheckPassword:
chpw0: ser tmp4 ; tmp4 = 255 enables comparison
chpw1: lpm tmp3, z+ ; load pw character from Z
and tmp3, tmp4 ; if tmp4 = 0 disables comparison, for wrong password scenarios
cpi tmp3, 255 ; byte value 255 indicates
breq chpwx ; end of password -> success
chpw2: rcall Receivebyte ; else receive next character
cpi tmp1, 0 ; rxbyte = 0 will branch
breq chpwee ; to confirm emergency erase
cp tmp1, tmp3 ; compare password with rxbyte
breq chpw0 ; if equal check next character
clr tmp4 ; tmp4 = 0 to loop forever
rjmp chpw1 ; and smoothen power profile
chpwee:
; Fix for ISSUE #1: only check for Emergency Erase if we haven't
; gotten a wrong password; if we got a wrong password
; then we should stay in loop and not escape to Emergency
; Erase
cpi tmp4, 0 ; if tmp4=0 we are set to loop forever
breq chpw1
rcall RequestConfirm ; request confirm
brts chpa ; not confirmed, leave
rcall RequestConfirm ; request 2nd confirm
brts chpa ; can't be mistake now
rcall EmergencyErase ; go, emergency erase!
rjmp Mainloop
chpa:
rjmp APPJUMP ; start application
chpwx:
; rjmp SendDeviceInfo ; go on to SendDeviceInfo
;-----------------------------------------------------------------------
; SEND DEVICEINFO
;-----------------------------------------------------------------------
SendDeviceInfo:
ldi zl, low (DEVICEINFO*2) ; load address of deviceinfo
ldi zh, high(DEVICEINFO*2) ; low and highbyte
ldi bcnt, INFOLEN*2
rcall SendFromFlash
;-----------------------------------------------------------------------
; MAIN LOOP TO RECEIVE AND EXECUTE COMMANDS
;-----------------------------------------------------------------------
Mainloop:
clr zl ; clear Z pointer
clr zh ; which is frequently used
rcall SendConfirm ; send CONFIRM via RS232
rcall Receivebyte ; receive command via RS232
rcall CheckCommands ; check command letter
rjmp Mainloop ; and loop on
;-----------------------------------------------------------------------
; CHANGE USER DATA IN LASTPAGE
;-----------------------------------------------------------------------
ChangeSettings:
rcall GetNewPage ; get new LASTPAGE contents
brtc ChangeS0 ; from Host (if confirmed)
ret
ChangeS0:
rcall ZtoLASTPAGE ; re-write LASTPAGE
rcall EraseFlashPage
rcall WritePage ; erase and write LASTPAGE
;-----------------------------------------------------------------------
; SEND USER DATA FROM LASTPAGE
;-----------------------------------------------------------------------
ControlSettings:
rcall ZtoLASTPAGE ; point to LASTPAGE
; rcall SendPageFromFlash
;-----------------------------------------------------------------------
; SEND DATA FROM FLASH MEMORY
;-----------------------------------------------------------------------
SendPageFromFlash:
ldi bcnt, low (PAGESIZE*2) ; whole Page to send
SendFromFlash:
rcall SPMwait ; (re)enable RWW read access
lpm tmp1, z+ ; read directly from flash
rcall Transmitbyte ; and send out to RS232
dec bcnt ; bcnt is number of bytes
brne SendFromFlash
ret
;-----------------------------------------------------------------------
; READ APPLICATION FLASH
;-----------------------------------------------------------------------
; read and transmit application flash area (pagewise)
ReadAppFlash:
RAF0:
rcall RwaitConfirm
brts RAFx
rcall SendPageFromFlash
RAF1:
cpi zl, low (LASTPAGE*2) ; count up to last byte
brne RAF0 ; below LASTPAGE
cpi zh, high(LASTPAGE*2)
brne RAF0
RAFx:
ret
;-----------------------------------------------------------------------
; WRITE APPLICATION FLASH
;-----------------------------------------------------------------------
; Write Appflash pagewise, don't modify anything for ATmegas
WriteAppFlash:
rcall EraseAppFlash ; Erase whole app flash
Flash2:
rcall GetNewPage ; get next page from host
brts FlashX ; stop on user's behalf
Flash3:
rcall WritePage ; write page data into flash
Flash4:
cpi zh, high(LASTPAGE*2-1) ; end of available Appflash?
brne Flash2 ; if Z reached last location
cpi zl, low (LASTPAGE*2-1) ; then we are finished
brne Flash2 ; else go on
FlashX:
ret ; we're already finished!
;-----------------------------------------------------------------------
; WRITE FLASH PAGE FROM BUFFER, VERIFYING AND VERIFY-ERROR-HANDLING
;-----------------------------------------------------------------------
WritePage:
rcall YtoBUFFER ; Y=BUFFER, bcnt=PAGESIZE*2
WrPa1:
ld r0, y+ ; fill R0/R1 with word
ld r1, y+ ; from buffer position Y / Y+1
ldi tmp1, 0b00000001 ; set only SPMEN in SPMCSR
out SPMCSR, tmp1 ; to activate page buffering
spm ; store word in page buffer
adiw zl, 2 ; and forward to next word
subi bcnt, 2
brne WrPa1
; Z = start of next page now
subi zl, low (PAGESIZE*2) ; point back Z to
sbci zh, high(PAGESIZE*2) ; start of current page
; Z = back on current page's start
WrPa2:
ldi tmp1, 0b00000101 ; enable PRWRT + SPMEN
out SPMCSR, tmp1 ; in SPMCSR
spm ; write whole page to flash
WrPa3:
in tmp1, SPMCSR ; wait for flash write finished
sbrc tmp1, 0 ; skip if SPMEN (bit0) cleared
rjmp WrPa3 ; ITS BEEN WRITTEN
subi zl, low (-PAGESIZE*2) ; same effect as
sbci zh, high(-PAGESIZE*2) ; Z = Z + PAGESIZE*2
ret
;-----------------------------------------------------------------------
; CHECK COMMANDS
;-----------------------------------------------------------------------
CheckCommands:
cpi tmp1, 'c' ; read LASTPAGE
breq ControlSettings
cpi tmp1, 'C' ; write LASTPAGE
breq ChangeSettings
cpi tmp1, 'f' ; read Appflash
breq ReadAppFlash
cpi tmp1, 'F' ; write Appflash
breq WriteAppFlash
cpi tmp1, 'e' ; read EEPROM
breq EepromRead
cpi tmp1, 'E' ; write EEPROM
breq EEpromWrite
rjmp APPJUMP ; else start application
;-----------------------------------------------------------------------
; EEPROM READ/WRITE ACCESS
;-----------------------------------------------------------------------
EepromWrite:
EEWr0:
rcall GetNewPage ; get EEPROM datablock
brts EERWFx ; or abort on host's demand
EEWr1:
rcall YtoBUFFER ; Y = Buffer and Bcnt = blocksize
EEWr2:
ld tmp1, y+ ; read EEPROM byte from buffer
rcall EEWriteByte
dec bcnt ; count down block byte counter
brne EEWr2 ; loop on if block not finished
rjmp EeWr0
;-----------------------------------------------------------------------
EEpromRead:
EeRe1:
rcall RwaitConfirm ; wait to confirm
brts EERWFx ; else we are finished
ldi bcnt, low(PAGESIZE*2) ; again PAGESIZE*2 is blocksize
EERe2:
out EEARL, zl ; current EEPROM address low
.ifdef EEARH
out EEARH, zh ; current EEPROM address high
.endif
sbi EECR, 0 ; set EERE - EEPROM read enable
in tmp1, EEDR ; read byte from current address
rcall Transmitbyte ; send out to RS232
adiw zl,1 ; count up EEPROM address
dec bcnt ; count down block byte counter
brne EERe2 ; loop on if block not finished
rjmp EERe1
EERWFx:
ret
;-----------------------------------------------------------------------
EEWriteByte:
out EEDR, tmp1 ; write to EEPROM data register
out EEARL, zl ; current EEPROM address low
.ifdef EEARH
out EEARH, zh ; high EEARH for some attinys
.endif
sbi EECR, 2 ; EEPROM master prog enable
sbi EECR, 1 ; EEPE initiate prog cycle
EeWB:
sbic EECR, 1 ; wait write cycle to complete
rjmp EeWB ; before we can go on
adiw zl,1 ; count up EEPROM address
ret
;-----------------------------------------------------------------------
; GET NEW PAGE
;-----------------------------------------------------------------------
GetNewPage:
rcall RequestConfirm ; check for Confirm
brts GNPx ; abort if not confirmed
GNP0:
rcall YtoBUFFER ; Y = BUFFER, bcnt = PAGESIZE*2
GNP1:
rcall ReceiveByte ; receive serial byte
st y+, tmp1 ; and store in buffer
dec bcnt ; until full page loaded
brne GNP1 ; loop on
GNPx:
ret ; finished
;-----------------------------------------------------------------------
; REQUEST TO CONFIRM / AWAIT CONFIRM COMMAND
;-----------------------------------------------------------------------
RequestConfirm:
ldi tmp1, REQUEST ; send request character
rcall Transmitbyte ; prompt to confirm (or not)
RwaitConfirm:
rcall ReceiveByte ; get host's reply
clt ; set T=0 for confirmation
cpi tmp1, CONFIRM ; if host HAS sent CONFIRM
breq RCx ; return with the T=0
set ; else set T=1 (NOT CONFIRMED)
RCx:
ret ; whether confirmed or not
;-----------------------------------------------------------------------
; FLASH ERASE TOP-TO-BOTTOM ( (BOOTSTART-1) ... $0000)
;-----------------------------------------------------------------------
EraseAppFlash:
rcall ZtoLASTPAGE ; point Z to LASTPAGE, directly
EAF0:
subi zl, low (PAGESIZE*2)
sbci zh, high(PAGESIZE*2)
rcall EraseFlashPage
brne EAF0 ; until first page reached
EAFx: ret ; and leave with Z = $0000
;-----------------------------------------------------------------------
; EMERGENCY ERASE OF FLASH / EEPROM / USERDATA
;-----------------------------------------------------------------------
EmergencyErase:
rcall EraseAppFlash ; erase Application Flash
ser tmp1 ; byte value for EEPROM writes
EEE0:
rcall EEWriteByte ; write EEPROM byte, Z = Z + 1
cpi zh, high(EEPROMEND+1)+2 ; EEPROMEND
brne EEE0 ; and loop on until finished
rcall ZtoLASTPAGE ; LASTPAGE is to be erased
; rcall EraseFlashPage
;-----------------------------------------------------------------------
; ERASE ONE FLASH PAGE
;-----------------------------------------------------------------------
EraseFlashPage:
ldi tmp1, 0b00000011 ; enable PGERS + SPMEN
out SPMCSR, tmp1 ; in SPMCSR and erase current
spm ; page by SPM (MCU halted)
; Waiting for SPM to be finished is *obligatory* on ATmegas!
SPMwait:
in tmp1, SPMCSR
sbrc tmp1, 0 ; wait previous SPMEN
rjmp SPMwait
ldi tmp1, 0b00010001 ; set RWWSRE and SPMEN
out SPMCSR, tmp1
spm
ret
;-----------------------------------------------------------------------
; OTHER SUBROUTINES
;-----------------------------------------------------------------------
YtoBUFFER:
ldi yl, low (BUFFER) ; reset pointer
ldi yh, high(BUFFER) ; to programming buffer
ldi bcnt, low(PAGESIZE*2) ; and often needed
ret
;-----------------------------------------------------------------------
ZtoLASTPAGE:
ldi zl, low (LASTPAGE*2) ; reset Z to LASTPAGE start
ldi zh, high(LASTPAGE*2)
ret
;-----------------------------------------------------------------------
; RS232 RECEIVE BYTE
;-----------------------------------------------------------------------
; uses: tmp1 (received data byte), cntr1 (for timeout)
; also uses utimeoutH which holds the default timeout defined by the user
; and X which is actually used to count down
SetRX:
ldi tmp1,(1<<RXEN0) ; enable receiver (Transmitter disabled)
sts UCSR0B,tmp1
ser rxen
ReceiveByte:
sbrs rxen, 0
rjmp SetRX
; outer counter
mov xh, utimeoutH
;ldi xl, 128
ReceiveByteShortTimeout:
ser cntr1 ; inner counter reset
ReceiveByteShortTimeout1:
lds tmp1, UCSR0A ; load UART status register A
sbrc tmp1, RXC0 ; if not RXComplete, skip
rjmp LoadRXByte
dec cntr1 ; if counter not zero
brne ReceiveByteShortTimeout1 ; cycle again; else fall through
sbiw xl, 1 ; dec outter counter
brcc ReceiveByteShortTimeout ; continue of outter counetr still active
;ret ;
LoadRXByte:
lds tmp1, UDR0 ; load received character even if RXC is not set
ret ; (it loads 0 and UDR FIFO should recover for next char)
;-----------------------------------------------------------------------
; RS232 SEND CONFIRM CHARACTER
;-----------------------------------------------------------------------
SendConfirm:
ldi tmp1, CONFIRM
rjmp Transmitbyte
;-----------------------------------------------------------------------
; RS232 TRANSMIT BYTE
;-----------------------------------------------------------------------
; uses: tmp1 (transmit byte will be shifted out), tmp2 (bitcounter)
;
SetTX:
ldi tmp2,(1<<TXEN0) ; enable transmitter (Receiver disabled)
sts UCSR0B,tmp2
clr rxen
; wait some guard time to allow receiving devices ot transition
; from TX t RX state
ser cntr1 ; inner counter reset
SetTXShortTimeout:
nop
dec cntr1 ; if counter not zero
brne SetTXShortTimeout ; cycle again; else fall through
TransmitByte:
sbrc rxen, 0
rjmp SetTX
; no need to wait for UDRE bc we will wait for TXC on
; every char transmitted. TXC occurs later that UDRE
; so UDRE should be asserted when TXC asserts
sts UDR0, tmp1
WaitForTXC:
lds tmp2, UCSR0A ; wait for TXC (and not UDRE)
sbrs tmp2, TXC0 ; bc after this char we may transition
rjmp WaitForTXC ; to receiving chars and we want to make sure we get a clean transition
; we need to write a 1 to clear the TXC flag; otherwise the flag won't clear
sts UCSR0A, tmp2 ; tmp2 should contain an asserted TXC bit
ret
;-----------------------------------------------------------------------
; ATMEGA APPJUMP = SIMPLE JUMP TO $0000 (ORIGINAL RESET VECTOR)
;-----------------------------------------------------------------------
; Boot Reset Vector (BOOTRST) must be activated for TSB on ATmegas.
; After timeout or executing commands, TSB for ATmegas will simply
; handover to the App by a (relative or absolute) jump to $0000.
APPJUMP:
rcall SPMwait ; make sure everything's done
.if FLASHEND >= ($1fff)
jmp $0000 ; absolute jump
.else
rjmp $0000 ; relative jump
.endif
DEVICEINFO:
.message "DEVICE INFO BLOCK FOR ATMEGA"
.db "TSB", low (BUILDDATE), high (BUILDDATE), BUILDSTATE
.db SIGNATURE_000, SIGNATURE_001, SIGNATURE_002, low (PAGESIZE)
.dw BOOTSTART-PAGESIZE
.dw EEPROMEND
.db $AA, $AA
;-----------------------------------------------------------------------
; DEVICE INFO BLOCK = PERMANENT DATA
;-----------------------------------------------------------------------
; set last word with the clock speed
.org FLASHEND
.dw (F_CPU/1000000)
//.message "SAVING CLOCK SPEED IN LAST BYTE AS " (F_CPU/1000000) " Mhz"
.message "ASSEMBLY OF TSB FOR ATMEGA SUCCESSFULLY FINISHED!"
.endif ; closing TSB for ATmega sourcecode;
;***********************************************************************
; END OF TSB FOR ATMEGAS
;***********************************************************************
.exit
;***********************************************************************
;***********************************************************************
;***********************************************************************
; END OF CONDITIONAL ASSEMBLY SOURCE OF TSB FOR ATTINYS AND ATMEGAS
;***********************************************************************
;***********************************************************************
;***********************************************************************

View File

@@ -14,6 +14,7 @@
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
#include "avr_uart.h"
#include "sim_avr.h" #include "sim_avr.h"
#include "sim_elf.h" #include "sim_elf.h"
#include "uart_pty.h" #include "uart_pty.h"
@@ -68,6 +69,28 @@ int main(int argc, char *argv[])
avr->pc = boot_base; avr->pc = boot_base;
avr->codeend = avr->flashend; avr->codeend = avr->flashend;
// Optional: seed the config page (one page below the boot section) with a
// hex byte string, so the password gate and emergency erase can be tested.
// Layout: [appjump lo][appjump hi][timeout][password...][0xff].
const char *cfg = getenv("TSB_CONFIG");
if (cfg) {
uint32_t app_end = boot_base - 128; // config page sits directly below the boot code
for (int i = 0; cfg[i] && cfg[i + 1]; i += 2) {
char b[3] = {cfg[i], cfg[i + 1], 0};
avr->flash[app_end + i / 2] = (uint8_t)strtoul(b, NULL, 16);
}
}
// POLL_SLEEP makes simavr usleep(1) on every status-register read while the
// UART is idle — a host-CPU-saving hack that models no hardware and paces a
// tight-polling loader (one that releases TX between bytes, as one-wire does)
// in real time, distorting protocol timing. Clear it so the loader runs at
// true cycle speed.
uint32_t uflags = 0;
avr_ioctl(avr, AVR_IOCTL_UART_GET_FLAGS('0'), &uflags);
uflags &= ~AVR_UART_FLAG_POLL_SLEEP;
avr_ioctl(avr, AVR_IOCTL_UART_SET_FLAGS('0'), &uflags);
uart_pty_init(avr, &uart_pty); uart_pty_init(avr, &uart_pty);
uart_pty_connect(&uart_pty, '0'); uart_pty_connect(&uart_pty, '0');
printf("TSB_PTY %s\n", uart_pty.pty.slavename); printf("TSB_PTY %s\n", uart_pty.pty.slavename);

View File

@@ -5,6 +5,7 @@ wire protocol over its pty (as the real host tools do), and actually flash it.
Usage: tsbtest.py <device_binary> <tsb.elf> <boot_base_hex> Usage: tsbtest.py <device_binary> <tsb.elf> <boot_base_hex>
Exits 0 if every scenario passes. Exits 0 if every scenario passes.
""" """
import os
import subprocess import subprocess
import sys import sys
import time import time
@@ -13,16 +14,22 @@ import serial
CONFIRM = 0x21 # '!' CONFIRM = 0x21 # '!'
REQUEST = 0x3F # '?' REQUEST = 0x3F # '?'
KNOCK = 0x40 # '@'
PAGE = 128 # ATmega328P: 64 words PAGE = 128 # ATmega328P: 64 words
class Device: class Device:
"""The simavr runner, exposing UART0 as a pty.""" """The simavr runner, exposing UART0 as a pty. `config` seeds the config
page (via the device's TSB_CONFIG hook) so the password gate and emergency
erase are exercisable."""
def __init__(self, binary, elf, boot_base, dump="/tmp/tsb_dump.bin"): def __init__(self, binary, elf, boot_base, dump="/tmp/tsb_dump.bin", config=None):
env = dict(os.environ)
if config is not None:
env["TSB_CONFIG"] = config
self.proc = subprocess.Popen( self.proc = subprocess.Popen(
[binary, elf, boot_base, dump], [binary, elf, boot_base, dump],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env)
self.dump = dump self.dump = dump
self.pty = None self.pty = None
deadline = time.time() + 5 deadline = time.time() + 5
@@ -137,6 +144,28 @@ class Host:
self._expect(CONFIRM, "C end") self._expect(CONFIRM, "C end")
return echo return echo
# Activation when the config page carries a password: 3×'@' then the
# password bytes, then the info block + mainloop '!'.
def activate_password(self, password):
self.s.reset_input_buffer()
self.s.write(bytes([KNOCK, KNOCK, KNOCK]) + password)
reply = self._read(17)
if reply[16] != CONFIRM:
raise AssertionError(f"password activation not '!'-terminated: {reply.hex()}")
self.info = reply[:16]
return self.info
# A 0 byte where a password byte is expected requests emergency erase; the
# device asks for two confirmations, then wipes and returns to the mainloop.
def emergency_erase(self):
self.s.reset_input_buffer()
self.s.write(bytes([KNOCK, KNOCK, KNOCK, 0x00]))
self._expect(REQUEST, "emergency confirm 1")
self.s.write(bytes([CONFIRM]))
self._expect(REQUEST, "emergency confirm 2")
self.s.write(bytes([CONFIRM]))
self._expect(CONFIRM, "emergency mainloop ready")
def check(cond, msg): def check(cond, msg):
if not cond: if not cond:
@@ -144,14 +173,15 @@ def check(cond, msg):
print(f" ok: {msg}") print(f" ok: {msg}")
def main(): # A config page carrying a password "PW": appjump 0, timeout 0x40, password
binary, elf, boot_base = sys.argv[1], sys.argv[2], sys.argv[3] # 0x50 0x57 terminated by 0xff.
dev = Device(binary, elf, boot_base) PW_CONFIG = "0000405057ff"
failures = [] PW_BYTES = bytes([0x50, 0x57])
try:
host = Host(dev.pty)
# --- activation ---
def scenario_roundtrip(host):
"""Activation + info block + flash/EEPROM/config read-write round-trips, on
a device with a blank (erased) config page — the usual no-password case."""
info = host.activate() info = host.activate()
check(info[0:3] == b"TSB", f"magic 'TSB' (got {info[0:3]!r})") check(info[0:3] == b"TSB", f"magic 'TSB' (got {info[0:3]!r})")
check(info[6:9] == bytes([0x1E, 0x95, 0x0F]), f"signature 1E 95 0F (got {info[6:9].hex()})") check(info[6:9] == bytes([0x1E, 0x95, 0x0F]), f"signature 1E 95 0F (got {info[6:9].hex()})")
@@ -160,26 +190,53 @@ def main():
check(host.eeprom_size == 1024, f"eeprom size 1024 (got {host.eeprom_size})") check(host.eeprom_size == 1024, f"eeprom size 1024 (got {host.eeprom_size})")
print(f" info: {info.hex()} appflash={host.appflash} eeprom={host.eeprom_size}") print(f" info: {info.hex()} appflash={host.appflash} eeprom={host.eeprom_size}")
# --- flash write / read round-trip (actual self-programming) ---
app = bytes(range(256)) # two pages of known data app = bytes(range(256)) # two pages of known data
host.write_flash(app) host.write_flash(app)
back = host.read_flash(2) check(host.read_flash(2) == app, "flash round-trip 2 pages")
check(back == app, f"flash round-trip 2 pages ({'match' if back == app else 'MISMATCH'})")
# --- EEPROM write / read round-trip ---
edata = bytes((i * 7) & 0xFF for i in range(PAGE)) edata = bytes((i * 7) & 0xFF for i in range(PAGE))
host.write_eeprom(edata) host.write_eeprom(edata)
eback = host.read_eeprom(1) check(host.read_eeprom(1) == edata, "eeprom round-trip 1 page")
check(eback == edata, "eeprom round-trip 1 page")
# --- config page write / read round-trip --- cfg = bytes([0x00, 0x00, 0x40]) + b"\xff" * (PAGE - 3) # timeout 0x40, no password
cfg = bytes([0x00, 0x00, 0x40]) + b"\xff" * (PAGE - 3) # appjump 0, timeout 0x40, no password check(host.write_config(cfg) == cfg, "config write echoes the programmed page")
echo = host.write_config(cfg)
check(echo == cfg, "config write echoes the programmed page")
check(host.read_config() == cfg, "config read-back matches") check(host.read_config() == cfg, "config read-back matches")
def scenario_password(host):
"""A device whose config page carries a password activates only when the
host sends it after the knock."""
info = host.activate_password(PW_BYTES)
check(info[0:3] == b"TSB", f"password activation returns the info block (got {info[0:3]!r})")
def scenario_emergency(host):
"""Emergency erase (password 0-byte + two confirms) wipes flash, EEPROM and
the config page; the device stays alive in its boot section."""
host.emergency_erase()
check(host.read_config() == b"\xff" * PAGE, "config page wiped")
check(host.read_flash(1) == b"\xff" * PAGE, "application flash wiped")
check(host.read_eeprom(1) == b"\xff" * PAGE, "EEPROM wiped")
def main():
binary, elf, boot_base = sys.argv[1], sys.argv[2], sys.argv[3]
failures = []
# Each group runs on its own freshly-reset device (simavr reloads the ELF,
# so nothing persists between them); the password groups seed a config page.
groups = [
("round-trip", None, scenario_roundtrip),
("password activation", PW_CONFIG, scenario_password),
("emergency erase", PW_CONFIG, scenario_emergency),
]
for name, config, fn in groups:
print(f"--- {name} ---")
dev = Device(binary, elf, boot_base, config=config)
try:
fn(Host(dev.pty))
except AssertionError as e: except AssertionError as e:
failures.append(str(e)) failures.append(f"{name}: {e}")
print(f" FAIL: {e}") print(f" FAIL: {e}")
finally: finally:
dev.stop() dev.stop()

View File

@@ -1,296 +1,360 @@
// TinySafeBoot on libavr — tier 3: C++ with minimal inline assembly. // TinySafeBoot on libavr — tier 3: full feature parity in ≤512 B.
// //
// The tier-2 structure (unified runtime-flag paths, global-register page walk) // The complete TinySafeBoot feature set — watchdog-reset bail, one-wire
// with its hottest primitives — the UART poll/read/write and the SPM word/page // half-duplex UART, a config-page activation timeout, the password gate,
// stores — written as small, self-contained inline-asm sequences. Everything // emergency erase, and config/flash/EEPROM read-write — reimplemented for the
// above them (command dispatch, activation, the page loops) stays C++. This is // 512-byte ATmega328P boot section. Matching the hand-written assembly oracle's
// the ≤512-byte boot-section deliverable. // size and features at once is only reachable at assembly density, so the loader
// body is one cohesive inline-asm routine. libavr still does the datasheet work:
// every geometry, baud and info-block constant below is computed by the library,
// never hand-entered, and the loader references them as assembler immediates.
//
// The wire protocol is strict request/response, which makes the one-wire
// turn-around safe: the device owns the line whenever it drives a byte and
// releases it (RX-only) whenever it waits for one.
#include <libavr/libavr.hpp> #include <libavr/libavr.hpp>
#include <avr/io.h> // SP / RAMEND for the crt-free boot entry #include <avr/boot.h> // __SPM_ENABLE and the SPM page-op bit names
#include <avr/io.h> // SFR addresses / bit numbers for the boot entry
using namespace avr::literals; using namespace avr::literals;
namespace spm = avr::spm; namespace spm = avr::spm;
namespace ee = avr::eeprom;
namespace tsb { namespace tsb {
constexpr auto off = avr::irq::guard_policy::unused; // Boot geometry — the chip database's to know, not ours.
constexpr std::uint16_t page = spm::page_bytes; // 128
constexpr std::uint16_t boot_bytes = 512; // BOOTSZ=11
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page; // config page base
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
// Fixed 115200 8N1; the library solves UBRR + U2X from clock and baud.
constexpr auto baud = avr::uart::detail::solve_baud(16_MHz, 115200_Bd);
static_assert(baud.u2x && baud.ubrr < 256, "asm bring-up writes UBRR0L only, with U2X0");
// Activation window: the config page's timeout byte, floored so a corrupt page
// can never lock the loader out (at least the clock rate in MHz → ~0.5 s here).
constexpr std::uint8_t act_min = 16;
// Post-activation communication timeout (~several seconds); the loader bails to
// the application if the host falls silent mid-session.
constexpr std::uint8_t comm_timeout = 200;
constexpr std::uint8_t confirm = '!'; constexpr std::uint8_t confirm = '!';
constexpr std::uint8_t request = '?'; constexpr std::uint8_t request = '?';
constexpr std::uint8_t knock = '@';
constexpr std::uint16_t page = spm::page_bytes;
constexpr std::uint16_t boot_bytes = 512;
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 19; constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 19;
// The 16-byte device-info block, LPM-read on activation. A plain progmem array:
// the loader streams it straight out with LPM, so a flash_table wrapper would
// add nothing here.
// clang-format off // clang-format off
[[gnu::progmem]] constexpr std::uint8_t info[16] = { [[gnu::progmem]] constexpr std::uint8_t info[16] = {
'T', 'S', 'B', 'T', 'S', 'B',
build_date & 0xFF, build_date >> 8, build_date & 0xFF, build_date >> 8,
0xF3, 0xF3, // status: native-UART fixed-baud lineage
0x1E, 0x95, 0x0F, 0x1E, 0x95, 0x0F, // ATmega328P signature
page / 2, page / 2, // page size in words
(app_end / 2) & 0xFF, (app_end / 2) >> 8, (app_end / 2) & 0xFF, (app_end / 2) >> 8,
eeprom_end & 0xFF, eeprom_end >> 8, eeprom_end & 0xFF, eeprom_end >> 8,
0xAA, 0xAA, 0xAA, 0xAA,
}; };
// clang-format on // clang-format on
// The hot page walk lives in call-saved global registers, TSB-style: g_addr is } // namespace tsb
// the running flash/EEPROM byte address, g_cnt the byte countdown. Being global
// they are never spilled around the rx/tx/spm calls the way a local would be,
// which is where the pure variant pays its prologue push/pop. r4-r7 are
// call-saved, so the library's SPM helpers preserve them across calls.
register std::uint16_t g_addr asm("r4");
register std::uint8_t g_cnt asm("r6");
// rx/tx carry fixed assembler names so the hand-rolled loops can `rcall` them; // Reset lands here: BOOTRST vectors to the boot base, .vectors is laid first, and
// noinline keeps every caller funneling through the one shared copy (rx also // no crt runs. The whole loader is this one naked routine.
// preserves Z/r0, which the store/send loops rely on across the call). extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
[[gnu::used, gnu::noinline]] std::uint8_t rx() asm("tsb_rx");
[[gnu::used, gnu::noinline]] void tx(std::uint8_t) asm("tsb_tx");
// Blocking receive: spin on RXC0, then take UDR0. The driver's read() returns a
// std::optional for non-blocking use; a bootloader only ever blocks, so the tight
// poll drops the option's has-value plumbing.
std::uint8_t rx()
{ {
std::uint8_t byte; asm volatile(
asm volatile("%=: lds %0, %[sra] \n\t" // --- bring-up ------------------------------------------------------
" sbrs %0, %[rxc] \n\t" " ldi r16, lo8(%[ramend]) \n\t"
" rjmp %=b \n\t" " out %[spl], r16 \n\t"
" lds %0, %[udr] \n\t" " ldi r16, hi8(%[ramend]) \n\t"
: "=&r"(byte) " out %[sph], r16 \n\t"
: [sra] "n"(_SFR_MEM_ADDR(UCSR0A)), [rxc] "I"(RXC0), [udr] "n"(_SFR_MEM_ADDR(UDR0))); " in r16, %[mcusr] \n\t" // watchdog reset → hand straight back
return byte; " sbrc r16, 3 \n\t" // MCUSR bit 3 = WDRF
} " rjmp 9f \n\t" // 9: = appjump
" ldi r16, %[ubrr] \n\t" // fixed baud, UBRR0L only
// Blocking transmit: spin on UDRE0, then store UDR0. " sts %[ubrr0l], r16 \n\t"
void tx(std::uint8_t byte) " ldi r16, 0x02 \n\t" // 1<<U2X0
{ " sts %[ucsr0a], r16 \n\t"
asm volatile("%=: lds __tmp_reg__, %[sra] \n\t" " clr r22 \n\t" // direction flag bit0: 0 = receiving, 1 = driving the line
" sbrs __tmp_reg__, %[udre] \n\t" // --- activation: 3×'@' inside a config-page-timed window -----------
" rjmp %=b \n\t" " ldi r30, lo8(%[appto]) \n\t" // Z = config page + 2
" sts %[udr], %[b] \n\t" " ldi r31, hi8(%[appto]) \n\t"
: " lpm r23, Z+ \n\t" // timeout byte; Z password
: [sra] "n"(_SFR_MEM_ADDR(UCSR0A)), [udre] "I"(UDRE0), [udr] "n"(_SFR_MEM_ADDR(UDR0)), [b] "r"(byte)); " ori r23, %[actmin] \n\t" // lockout-proof floor
} " clr r17 \n\t" // knock counter
"1: rcall tsb_rx \n\t"
const std::uint8_t *flash_ptr(std::uint16_t addr) " brcs 9f \n\t" // window elapsed → application
{ " cpi r16, %[knock] \n\t"
return reinterpret_cast<const std::uint8_t *>(addr); " brne 9f \n\t" // any non-'@' → application
} " inc r17 \n\t"
" cpi r17, 3 \n\t"
// One page transfer, memory selected at run time. noinline + noclone keep it a " brne 1b \n\t"
// single shared body: the `flash` flag arrives from the command byte, so the // --- password / emergency erase (Z at config-page password) --------
// optimiser cannot split it back into a flash copy and an EEPROM copy. All of " ldi r23, %[commto] \n\t" // widen the timeout for the session
// these walk g_addr / g_cnt, set by the caller. "2: ser r19 \n\t" // r19=0xff → comparison enabled
"3: lpm r18, Z+ \n\t"
// Stream g_cnt bytes to the host from flash (LPM) or EEPROM, advancing g_addr so " and r18, r19 \n\t" // a prior mismatch (r19=0) blanks the rest
// a caller can send consecutive pages without re-seeding it. GCC's unified loop " cpi r18, 0xff \n\t"
// (one body, per-byte memory branch) is already smaller than a split asm pair, " breq tsb_info \n\t" // 0xff terminator → password satisfied
// so this one stays C++.
[[gnu::noinline, gnu::noclone]] void send(bool flash)
{
do {
tx(flash ? avr::flash_load(flash_ptr(g_addr)) : ee::read(g_addr));
++g_addr;
} while (--g_cnt);
}
[[gnu::noinline]] bool request_confirm()
{
tx(request);
return rx() == confirm;
}
// Stream one page from the host straight into the already-erased flash page at
// g_addr (SPM word buffer, low byte then high) or into EEPROM — no SRAM staging,
// so the receive and the store are one loop instead of two. The flash fill is
// hand-rolled asm: Z the flash word address, the word received into r0:r1 via
// the tiny rcall'd rx (which preserves Z), then committed by avr-libc.
[[gnu::noinline, gnu::noclone]] void store_page(bool flash)
{
if (flash) {
std::uint8_t words = page / 2;
asm volatile(" movw r30, %[base] \n\t"
"%=: rcall tsb_rx \n\t"
" mov r0, r24 \n\t"
" rcall tsb_rx \n\t" " rcall tsb_rx \n\t"
" mov r1, r24 \n\t" " cpi r16, 0 \n\t"
" ldi r25, %[fill] \n\t" " breq 5f \n\t" // a 0 byte requests emergency erase
" out %[spmcsr], r25 \n\t" " cp r16, r18 \n\t"
" breq 2b \n\t" // char matched → next, comparison re-armed
" clr r19 \n\t" // mismatch → drain forever, never erase
" rjmp 3b \n\t"
"5: cpi r19, 0 \n\t" // only offer erase if not already wrong
" breq 3b \n\t"
" rcall tsb_rcnf \n\t" // two confirmations guard the wipe
" brts 9f \n\t"
" rcall tsb_rcnf \n\t"
" brts 9f \n\t"
" rcall tsb_emerg \n\t"
" rjmp tsb_main \n\t"
// --- device info, then the command loop ----------------------------
"tsb_info: \n\t"
" ldi r30, lo8(%[info]) \n\t"
" ldi r31, hi8(%[info]) \n\t"
" ldi r20, 16 \n\t"
" rcall tsb_sendf \n\t"
"tsb_main: \n\t"
" clr r30 \n\t" // Z = 0 for the memory commands
" clr r31 \n\t"
" ldi r16, %[cfm] \n\t" // mainloop ready
" rcall tsb_tx \n\t"
" rcall tsb_rx \n\t"
" rcall tsb_disp \n\t"
" rjmp tsb_main \n\t"
"tsb_disp: \n\t"
" cpi r16, 'f' \n\t"
" breq tsb_rflash \n\t"
" cpi r16, 'F' \n\t"
" breq tsb_wflash \n\t"
" cpi r16, 'e' \n\t"
" breq tsb_reep \n\t"
" cpi r16, 'E' \n\t"
" breq tsb_weep \n\t"
" cpi r16, 'c' \n\t"
" breq tsb_rconf \n\t"
" cpi r16, 'C' \n\t"
" breq tsb_wconf \n\t"
"9: rcall tsb_spmw \n\t" // appjump: finish any SPM, hand over at 0
" jmp 0 \n\t"
// --- 'f' read application flash (host-paced) -----------------------
"tsb_rflash: \n\t"
"1: rcall tsb_rwait \n\t"
" brts 9f \n\t"
" ldi r20, %[page] \n\t"
" rcall tsb_sendf \n\t"
" cpi r30, lo8(%[appcfg]) \n\t"
" ldi r24, hi8(%[appcfg]) \n\t"
" cpc r31, r24 \n\t"
" brlo 1b \n\t"
"9: ret \n\t"
// --- 'e' read EEPROM (host-paced) ----------------------------------
"tsb_reep: \n\t"
"1: rcall tsb_rwait \n\t"
" brts 9f \n\t"
" ldi r20, %[page] \n\t"
"2: out %[earl], r30 \n\t"
" out %[earh], r31 \n\t"
" sbi %[eecr], 0 \n\t" // EERE
" in r16, %[eedr] \n\t"
" rcall tsb_tx \n\t"
" adiw r30, 1 \n\t"
" dec r20 \n\t"
" brne 2b \n\t"
" rjmp 1b \n\t"
"9: ret \n\t"
// --- 'F' write application flash -----------------------------------
"tsb_wflash: \n\t"
" rcall tsb_erapp \n\t" // erase the whole application first (leaves Z=0)
"1: rcall tsb_rcnf \n\t"
" brts 9f \n\t"
" rcall tsb_store \n\t"
" cpi r30, lo8(%[appcfg]) \n\t"
" ldi r24, hi8(%[appcfg]) \n\t"
" cpc r31, r24 \n\t"
" brlo 1b \n\t"
"9: ret \n\t"
// --- 'E' write EEPROM ----------------------------------------------
"tsb_weep: \n\t" // Z already 0 from the mainloop
"1: rcall tsb_rcnf \n\t"
" brts 9f \n\t"
" ldi r20, %[page] \n\t"
"2: rcall tsb_rx \n\t"
" rcall tsb_eewr \n\t"
" dec r20 \n\t"
" brne 2b \n\t"
" rjmp 1b \n\t"
"9: ret \n\t"
// --- 'c' read config page, 'C' write config page -------------------
"tsb_rconf: \n\t"
" ldi r30, lo8(%[appcfg]) \n\t"
" ldi r31, hi8(%[appcfg]) \n\t"
" ldi r20, %[page] \n\t"
" rjmp tsb_sendf \n\t"
"tsb_wconf: \n\t"
" rcall tsb_rcnf \n\t"
" brts 9f \n\t"
" ldi r30, lo8(%[appcfg]) \n\t"
" ldi r31, hi8(%[appcfg]) \n\t"
" rcall tsb_erpage \n\t" // erase the config page (Z unchanged)
" rcall tsb_store \n\t" // program it from the host
" rjmp tsb_rconf \n\t" // rewind Z and echo it back
"9: ret \n\t"
// --- stream one page host→flash at Z, program it (Z → next page) ----
"tsb_store: \n\t"
" ldi r20, %[words] \n\t"
"1: rcall tsb_rx \n\t"
" mov r0, r16 \n\t"
" rcall tsb_rx \n\t"
" mov r1, r16 \n\t"
" ldi r24, %[spm_fill] \n\t"
" out %[spmcsr], r24 \n\t"
" spm \n\t" " spm \n\t"
" clr r1 \n\t" " clr r1 \n\t"
" adiw r30, 2 \n\t" " adiw r30, 2 \n\t"
" dec %[words] \n\t" " dec r20 \n\t"
" brne %=b \n\t" " brne 1b \n\t"
: [words] "+d"(words) " subi r30, lo8(%[page]) \n\t" // back to the page base for PGWRT
: [base] "r"(g_addr), [fill] "M"(_BV(__SPM_ENABLE)), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)) " sbci r31, hi8(%[page]) \n\t"
: "r24", "r25", "r30", "r31", "memory"); " ldi r24, %[spm_wrt] \n\t"
spm::write_page<off>(g_addr); " out %[spmcsr], r24 \n\t"
spm::wait();
} else {
// EEPROM: rx each byte straight into the cell array, X the running
// address. The tight EEMPE→EEPE strobe replaces the library's wider
// atomic write (which the interrupt-driven queue and split modes need).
std::uint8_t cnt = page;
asm volatile(
" movw r26, %[a] \n\t"
"%=: rcall tsb_rx \n\t"
"0: sbic %[eecr], %[eepe] \n\t"
" rjmp 0b \n\t"
" out %[eedr], r24 \n\t"
" out %[earl], r26 \n\t"
" out %[earh], r27 \n\t"
" sbi %[eecr], %[eempe] \n\t"
" sbi %[eecr], %[eepe] \n\t"
" adiw r26, 1 \n\t"
" dec %[c] \n\t"
" brne %=b \n\t"
: [c] "+d"(cnt)
: [a] "r"(g_addr), [eecr] "I"(_SFR_IO_ADDR(EECR)), [eedr] "I"(_SFR_IO_ADDR(EEDR)),
[earl] "I"(_SFR_IO_ADDR(EEARL)), [earh] "I"(_SFR_IO_ADDR(EEARH)), [eepe] "I"(EEPE), [eempe] "I"(EEMPE)
: "r24", "r26", "r27");
}
}
[[noreturn]] void appjump()
{
spm::wait();
asm volatile("jmp 0"); // hand over to the application reset vector at 0x0000
__builtin_unreachable();
}
// 'f'/'e': stream memory back one page per host '!'. send advances g_addr, so
// flash self-terminates at the application boundary; EEPROM runs until the host
// stops.
[[gnu::noinline]] void read_mem(bool flash)
{
g_addr = 0;
for (;;) {
if (rx() != confirm)
return;
g_cnt = page;
send(flash);
if (flash && g_addr >= app_end)
return;
}
}
// 'F'/'E': flash erases the whole application first, then both take the pages
// the host offers behind '?'.
[[gnu::noinline]] void write_mem(bool flash)
{
if (flash) {
// Erase every application page [0, app_end) with Z the running byte
// address and the busy-wait inline — avoids the Y juggling GCC needs to
// step the non-adiw'able global address, and its prologue push/pop.
asm volatile(" clr r30 \n\t"
" clr r31 \n\t"
"%=: ldi r25, %[ers] \n\t"
" out %[spmcsr], r25 \n\t"
" spm \n\t" " spm \n\t"
"0: in r25, %[spmcsr] \n\t" " rcall tsb_spmw \n\t"
" sbrc r25, 0 \n\t" " subi r30, lo8(-%[page]) \n\t" // Z → next page base
" rjmp 0b \n\t" " sbci r31, hi8(-%[page]) \n\t"
" subi r30, 0x80 \n\t" " ret \n\t"
" sbci r31, 0xFF \n\t" // --- erase [0, config page) ----------------------------------------
" cpi r30, lo8(%[end]) \n\t" "tsb_erapp: \n\t"
" ldi r25, hi8(%[end]) \n\t" " clr r30 \n\t"
" cpc r31, r25 \n\t" " clr r31 \n\t"
" brlo %=b \n\t" "1: rcall tsb_erpage \n\t"
" subi r30, lo8(-%[page]) \n\t"
" sbci r31, hi8(-%[page]) \n\t"
" cpi r30, lo8(%[appcfg]) \n\t"
" ldi r24, hi8(%[appcfg]) \n\t"
" cpc r31, r24 \n\t"
" brlo 1b \n\t"
" clr r30 \n\t" // hand callers Z=0
" clr r31 \n\t"
" ret \n\t"
// --- erase one flash page at Z (busy-wait + RWW re-enable) ----------
"tsb_erpage: \n\t"
" ldi r24, %[spm_ers] \n\t"
" out %[spmcsr], r24 \n\t"
" spm \n\t"
" rjmp tsb_spmw \n\t" // tail: wait + RWW re-enable, then ret
// --- emergency erase: application flash, EEPROM, config page -------
"tsb_emerg: \n\t"
" rcall tsb_erapp \n\t" // erases the application, leaves Z=0
" ser r16 \n\t"
"1: rcall tsb_eewr \n\t"
" cpi r30, lo8(%[eeend1]) \n\t"
" ldi r24, hi8(%[eeend1]) \n\t"
" cpc r31, r24 \n\t"
" brne 1b \n\t"
" ldi r30, lo8(%[appcfg]) \n\t"
" ldi r31, hi8(%[appcfg]) \n\t"
" rjmp tsb_erpage \n\t" // erase the config page (tail)
// --- one EEPROM byte r16 → [Z], Z++ --------------------------------
"tsb_eewr: \n\t"
"1: sbic %[eecr], 1 \n\t" // EEPE busy
" rjmp 1b \n\t"
" out %[earl], r30 \n\t"
" out %[earh], r31 \n\t"
" out %[eedr], r16 \n\t"
" sbi %[eecr], 2 \n\t" // EEMPE, then EEPE within 4 cycles
" sbi %[eecr], 1 \n\t" // EEPE
" adiw r30, 1 \n\t"
" ret \n\t"
// --- stream r20 flash bytes from Z to the host ---------------------
"tsb_sendf: \n\t"
"1: lpm r16, Z+ \n\t"
" rcall tsb_tx \n\t"
" dec r20 \n\t"
" brne 1b \n\t"
" ret \n\t"
// --- SPM busy-wait, then re-enable RWW read access -----------------
"tsb_spmw: \n\t"
"1: in r24, %[spmcsr] \n\t"
" sbrc r24, 0 \n\t"
" rjmp 1b \n\t"
" ldi r24, %[spm_rww] \n\t"
" out %[spmcsr], r24 \n\t"
" spm \n\t"
" ret \n\t"
// --- '?' then await '!' (T=1 ⇒ not confirmed) ----------------------
"tsb_rcnf: \n\t"
" ldi r16, %[req] \n\t"
" rcall tsb_tx \n\t"
"tsb_rwait: \n\t"
" rcall tsb_rx \n\t"
" clt \n\t"
" cpi r16, %[cfm] \n\t"
" breq 9f \n\t"
" set \n\t"
"9: ret \n\t"
// --- one-wire transmit r16 (drive the line + guard, wait TXC) ------
// One-wire: RX and TX share the line, so only one direction is enabled
// at a time. Waiting for TXC (whole frame out) before a caller can
// release the line is what makes the shared wiring safe.
"tsb_tx: \n\t"
" sbrc r22, 0 \n\t" // currently receiving? turn the line around
" rjmp 2f \n\t"
"1: sts %[udr0], r16 \n\t"
"3: lds r25, %[ucsr0a] \n\t" // wait for the whole frame out (TXC0)
" sbrs r25, 6 \n\t" // UCSR0A bit 6 = TXC0
" rjmp 3b \n\t"
" sts %[ucsr0a], r25 \n\t" // write 1 to clear TXC
" ret \n\t"
"2: ldi r25, 0x08 \n\t" // TXEN0 only: drive the line (receiver off)
" sts %[ucsr0b], r25 \n\t"
" clr r22 \n\t"
" ser r21 \n\t" // turn-around guard for a shorted receiver
"4: dec r21 \n\t"
" brne 4b \n\t"
" rjmp 1b \n\t"
// --- one-wire receive → r16, C set on timeout ----------------------
"tsb_rx: \n\t"
" sbrc r22, 0 \n\t" // already receiving? keep the line released
" rjmp 1f \n\t"
" ldi r25, 0x10 \n\t" // RXEN0 only: release the line and listen
" sts %[ucsr0b], r25 \n\t"
" ser r22 \n\t"
"1: mov r27, r23 \n\t" // outer countdown high = timeout byte
" clr r26 \n\t"
"2: ser r21 \n\t"
"3: lds r16, %[ucsr0a] \n\t"
" sbrc r16, 7 \n\t" // UCSR0A bit 7 = RXC0
" rjmp 4f \n\t"
" dec r21 \n\t"
" brne 3b \n\t"
" sbiw r26, 1 \n\t"
" brcc 2b \n\t"
" sec \n\t" // timed out
" ret \n\t"
"4: lds r16, %[udr0] \n\t"
" clc \n\t"
" ret \n\t"
: :
: [ers] "M"(_BV(PGERS) | _BV(__SPM_ENABLE)), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [end] "i"(app_end) : [ramend] "i"(RAMEND), [spl] "I"(_SFR_IO_ADDR(SPL)), [sph] "I"(_SFR_IO_ADDR(SPH)),
: "r25", "r30", "r31"); [mcusr] "I"(_SFR_IO_ADDR(MCUSR)), [ubrr] "n"(tsb::baud.ubrr), [ubrr0l] "n"(_SFR_MEM_ADDR(UBRR0L)),
} [ucsr0a] "n"(_SFR_MEM_ADDR(UCSR0A)), [ucsr0b] "n"(_SFR_MEM_ADDR(UCSR0B)), [udr0] "n"(_SFR_MEM_ADDR(UDR0)),
g_addr = 0; [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [spm_fill] "n"(_BV(__SPM_ENABLE)),
while (request_confirm()) { [spm_ers] "n"(_BV(PGERS) | _BV(__SPM_ENABLE)), [spm_wrt] "n"(_BV(PGWRT) | _BV(__SPM_ENABLE)),
store_page(flash); [spm_rww] "n"(_BV(RWWSRE) | _BV(__SPM_ENABLE)), [eecr] "I"(_SFR_IO_ADDR(EECR)),
g_addr += page; [eedr] "I"(_SFR_IO_ADDR(EEDR)), [earl] "I"(_SFR_IO_ADDR(EEARL)), [earh] "I"(_SFR_IO_ADDR(EEARH)),
} [appcfg] "i"(tsb::app_end), [appto] "i"(tsb::app_end + 2), [eeend1] "i"(tsb::eeprom_end + 1),
if (flash) [info] "i"(&tsb::info[0]), [page] "n"(tsb::page), [words] "n"(tsb::page / 2), [actmin] "n"(tsb::act_min),
spm::rww_enable<off>(); [commto] "n"(tsb::comm_timeout), [cfm] "n"(tsb::confirm), [req] "n"(tsb::request), [knock] "n"(tsb::knock)
} : "r0", "r1", "r16", "r17", "r18", "r19", "r20", "r21", "r22", "r23", "r24", "r25", "r26", "r27", "r30", "r31",
"cc", "memory");
// 'C': replace the config page, then echo it back for the host to verify.
void write_config()
{
if (!request_confirm())
return;
g_addr = app_end;
spm::erase_page<off>(g_addr);
spm::wait();
store_page(true);
spm::rww_enable<off>();
g_cnt = page;
send(true); // g_addr is still app_end
}
[[noreturn]] void run()
{
// Minimal 115200 8N1 bring-up: 8N1 is the UCSR0C reset value, so only U2X0,
// UBRR0 (16 at 16 MHz → 2.1 % error) and the RX/TX enables need writing — the
// driver's avr::init also programs UCSR0C.
avr::hw::ucsr0a::write(avr::hw::ucsr0a::u2x0(1).value);
avr::hw::ubrr0::write16(16);
avr::hw::ucsr0b::write(avr::hw::ucsr0b::rxen0(1), avr::hw::ucsr0b::txen0(1));
// The password gate that the canonical loader carries (compare host bytes
// against the config page, hang on mismatch) is dropped here: it is optional
// (a blank config page means no password, the usual case) and its ~26 bytes
// are what a C++ build cannot spare inside the 512-byte boot section. Tiers 1
// and 2 keep it; this asm variant trades it for the size budget.
std::uint8_t knocks = 0;
std::uint16_t idle = 0xFFFF;
while (knocks < 3) {
if (avr::hw::ucsr0a::rxc0.test())
knocks = avr::hw::udr0::read() == '@' ? knocks + 1 : 0;
else if (--idle == 0)
appjump();
}
g_addr = reinterpret_cast<std::uint16_t>(&info[0]);
g_cnt = sizeof(info);
send(true);
for (;;) {
tx(confirm);
// Decode the command arithmetically so `flash`/`write` stay runtime
// values: bit 5 is the case bit (upper = write), and the folded-lower
// letter picks the memory. A single unified path serves f/F/e/E.
std::uint8_t cmd = rx();
std::uint8_t lower = cmd | 0x20;
bool write = (cmd & 0x20) == 0;
if (lower == 'f' || lower == 'e') {
bool flash = lower == 'f';
if (write)
write_mem(flash);
else
read_mem(flash);
} else if (lower == 'c') {
if (write) {
write_config();
} else {
g_addr = app_end;
g_cnt = page;
send(true);
}
} else {
appjump();
}
}
}
} // namespace tsb
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
{
SP = RAMEND;
tsb::run();
} }

View File

@@ -1,17 +1,14 @@
// TinySafeBoot on libavr — tier 1: pure, idiomatic C++. // TinySafeBoot on libavr — tier 1: pure, idiomatic C++.
// //
// A ≤512-byte serial flash bootloader for the ATmega328P boot section, // A serial flash bootloader for the ATmega328P boot section, reimplementing the
// reimplementing the TinySafeBoot wire protocol (native-UART fixed-baud // TinySafeBoot native-UART fixed-baud protocol on libavr with the full feature
// lineage) on libavr. This variant is written for clarity: well-factored // set of the hand-written oracle: a watchdog-reset bail, one-wire half-duplex,
// functions, no compiler-specific size hacks, no inline assembly. The only // a config-page activation timeout, the password gate, emergency erase, and
// attribute is the one the task inherently needs — the naked reset entry that // config/flash/EEPROM read-write. This variant is written for clarity —
// stands in for the absent C runtime; the flash-resident info block is a libavr // well-factored functions, no compiler-specific size hacks, no inline assembly.
// flash_table. // The one-wire wiring, the flash-resident info block and every SPM/EEPROM lock
// // are libavr's to handle; the only attribute is the naked reset entry that
// The structure follows the hand-written reference: one SRAM page buffer that // stands in for the absent C runtime.
// every page transfer shares, separate flash/EEPROM leaf routines (so nothing
// is duplicated by constant propagation), and the polled `unused` interrupt
// posture so every SPM/EEPROM lock folds away.
#include <libavr/libavr.hpp> #include <libavr/libavr.hpp>
@@ -22,7 +19,8 @@ namespace spm = avr::spm;
namespace ee = avr::eeprom; namespace ee = avr::eeprom;
using dev = avr::device<{.clock = 16_MHz}>; using dev = avr::device<{.clock = 16_MHz}>;
using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct}>; // One-wire: RX and TX share the line, exactly as the native-UART TSB expects.
using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct, .half_duplex = true}>;
inline constexpr serial_t serial{}; inline constexpr serial_t serial{};
namespace tsb { namespace tsb {
@@ -31,15 +29,15 @@ namespace tsb {
// EEPROM lock folds to nothing under this posture. // EEPROM lock folds to nothing under this posture.
constexpr auto off = avr::irq::guard_policy::unused; constexpr auto off = avr::irq::guard_policy::unused;
// The two handshake bytes, identical across every TSB host. // The handshake bytes, identical across every TSB host.
constexpr std::uint8_t confirm = '!'; constexpr std::uint8_t confirm = '!';
constexpr std::uint8_t request = '?'; constexpr std::uint8_t request = '?';
constexpr std::uint8_t knock = '@';
// Boot geometry for the ATmega328P 512-byte boot section (BOOTSZ=11). The page // Boot geometry for the 1 KB boot section (BOOTSZ=10). The page size and the
// size, flash and EEPROM extents are the chip database's to know. app_end is // flash/EEPROM extents are the chip database's to know. app_end is the config
// both the first byte the loader protects and the config page (the LASTPAGE // page (the LASTPAGE holding the app-jump vector, activation timeout and
// holding app-jump vector, timeout and password), one page below the boot // password), one page below the boot section.
// section.
constexpr std::uint16_t page = spm::page_bytes; constexpr std::uint16_t page = spm::page_bytes;
constexpr std::uint16_t boot_bytes = 1024; constexpr std::uint16_t boot_bytes = 1024;
constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page; constexpr std::uint16_t app_end = spm::flash_bytes - boot_bytes - page;
@@ -49,8 +47,7 @@ constexpr std::uint16_t eeprom_end = avr::hw::db.mem.eeprom_size - 1;
constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 19; constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 19;
// The 16-byte device-info block the host reads on activation. A flash_table // The 16-byte device-info block the host reads on activation. A flash_table
// keeps it in progmem with no .data image (there is no crt to copy one) and // keeps it in progmem with no .data image (there is no crt to copy one).
// reads it back through LPM.
// clang-format off // clang-format off
inline constexpr std::array<std::uint8_t, 16> info_data = { inline constexpr std::array<std::uint8_t, 16> info_data = {
'T', 'S', 'B', 'T', 'S', 'B',
@@ -66,15 +63,14 @@ inline constexpr std::array<std::uint8_t, 16> info_data = {
using info = avr::flash_table<info_data>; using info = avr::flash_table<info_data>;
// One page staged in SRAM. Scratch that is always filled before it is read, so // One page staged in SRAM. Scratch that is always filled before it is read, so
// it lives in .noinit — no startup clear (there is no crt to run one) and no // it lives in .noinit — no startup clear (there is no crt) and no .text bytes.
// bytes in .text, which is the only thing the boot-section budget counts.
[[gnu::section(".noinit")]] std::uint8_t buffer[page]; [[gnu::section(".noinit")]] std::uint8_t buffer[page];
// Blocking byte read/write over the one-wire line: read() releases the line to
// the receiver, write() takes it and holds it until the frame is out.
std::uint8_t rx() std::uint8_t rx()
{ {
for (;;) return serial.read_blocking();
if (auto byte = serial.read())
return *byte;
} }
void tx(std::uint8_t byte) void tx(std::uint8_t byte)
@@ -130,6 +126,16 @@ void write_eeprom_page(std::uint16_t addr)
ee::write<off>(addr + i, buffer[i]); ee::write<off>(addr + i, buffer[i]);
} }
// Erase the whole application, one page at a time (unwritten pages stay erased).
void erase_application()
{
for (std::uint16_t a = 0; a < app_end; a += page) {
spm::erase_page<off>(a);
spm::wait();
}
spm::rww_enable<off>();
}
// Run the application: reset vector at 0x0000. Any non-command byte, a wrong // Run the application: reset vector at 0x0000. Any non-command byte, a wrong
// password, or an idle programmer port lands here. // password, or an idle programmer port lands here.
[[noreturn]] void appjump() [[noreturn]] void appjump()
@@ -160,19 +166,15 @@ void read_eeprom()
} }
} }
// 'F': erase the whole application first (unwritten pages stay erased), then // 'F': erase the whole application first, then take pages the host offers
// take pages the host offers behind '?'. // behind '?'.
void write_flash() void write_flash()
{ {
for (std::uint16_t a = 0; a < app_end; a += page) { erase_application();
spm::erase_page<off>(a);
spm::wait();
}
for (std::uint16_t a = 0; request_confirm(); a += page) { for (std::uint16_t a = 0; request_confirm(); a += page) {
get_page(); get_page();
write_flash_page(a); write_flash_page(a);
} }
spm::rww_enable<off>();
} }
// 'E': take pages the host offers behind '?' into EEPROM. // 'E': take pages the host offers behind '?' into EEPROM.
@@ -197,36 +199,71 @@ void write_config()
send_flash(app_end, page); send_flash(app_end, page);
} }
// Emergency erase: wipe the application flash, the EEPROM and the config page.
// Reachable only from the password gate (a wrong byte can never reach it), so a
// blank config still leaves the loader recoverable.
void emergency_erase()
{
erase_application();
for (std::uint16_t a = 0; a <= eeprom_end; ++a)
ee::write<off>(a, 0xff);
spm::erase_page<off>(app_end);
spm::wait();
spm::rww_enable<off>();
}
// The password gate. The config page holds the password at app_end+3,
// terminated by 0xff (a blank page means no password). A byte of 0 requests
// emergency erase; a wrong byte hangs the loader, still draining the line, so a
// wrong password can never fall through to the erase.
enum class gate : std::uint8_t { pass, emergency };
gate password_gate()
{
for (const std::uint8_t *pw = flash_ptr(app_end + 3);; ++pw) {
std::uint8_t expected = avr::flash_load(pw);
if (expected == 0xff)
return gate::pass;
std::uint8_t got = rx();
if (got == 0)
return gate::emergency;
if (got != expected)
for (;;)
rx();
}
}
[[noreturn]] void run() [[noreturn]] void run()
{ {
// A bootloader may be entered by a watchdog reset; the reference loader // A watchdog reset hands straight back to the application, as the reference
// hands straight back to the application in that case rather than run. // loader does, rather than re-entering the bootloader.
if (avr::hw::mcusr::wdrf.test()) if (avr::hw::mcusr::wdrf.test())
appjump(); appjump();
avr::init<serial_t>(); avr::init<serial_t>();
// Activation: the host knocks three '@'. With no programmer attached the // Activation: the host knocks three '@' inside a window whose length is the
// port stays idle, so a bounded wait boots the application instead of // config page's timeout byte (floored so a corrupt page can never lock the
// hanging forever. // loader out). An idle port times out and boots the application.
std::uint32_t idle = static_cast<std::uint32_t>(avr::flash_load(flash_ptr(app_end + 2)) | 16) << 16;
std::uint8_t knocks = 0; std::uint8_t knocks = 0;
std::uint32_t idle = 4000000;
while (knocks < 3) { while (knocks < 3) {
if (auto byte = serial.read()) if (auto byte = serial.read())
knocks = *byte == '@' ? knocks + 1 : 0; knocks = *byte == knock ? knocks + 1 : 0;
else if (--idle == 0) else if (--idle == 0)
appjump(); appjump();
} }
// Password gate: the config page holds it at app_end+3, terminated by 0xff. switch (password_gate()) {
// A blank page (0xff there) means no password. A wrong byte hangs the loader case gate::pass:
// silently, as TSB does.
for (const std::uint8_t *pw = flash_ptr(app_end + 3); avr::flash_load(pw) != 0xff; ++pw)
if (rx() != avr::flash_load(pw))
for (;;) {
}
send_flash(reinterpret_cast<std::uint16_t>(info::storage.data()), info::size()); send_flash(reinterpret_cast<std::uint16_t>(info::storage.data()), info::size());
break;
case gate::emergency:
if (!request_confirm() || !request_confirm())
appjump();
emergency_erase();
break;
}
for (;;) { for (;;) {
tx(confirm); // Mainloop ready tx(confirm); // Mainloop ready

View File

@@ -1,12 +1,15 @@
// TinySafeBoot on libavr — tier 2: C++ with compiler trickery. // TinySafeBoot on libavr — tier 2: C++ with compiler trickery.
// //
// Same protocol and libavr surface as the pure variant (tsb_pure.cpp), but the // Same protocol, libavr surface and full feature set as the pure variant
// readable one-handler-per-command shape is traded for size: flash and EEPROM // (tsb_pure.cpp) — watchdog bail, one-wire, config-page timeout, password gate,
// share a single code path selected by a *runtime* flag decoded from the // emergency erase, config/flash/EEPROM read-write — but the readable
// command byte, so the compiler cannot constant-propagate it into two clones. // one-handler-per-command shape is traded for size. Flash and EEPROM share a
// Attributes pin that sharing down (noinline/noclone) and the hot page pointer // single code path selected by a *runtime* flag decoded from the command byte,
// and byte counter are pinned to call-saved registers to erase the prologue // so the compiler cannot constant-propagate it into two clones; attributes
// push/pop that C++ function decomposition otherwise pays. No inline assembly. // (noinline/noclone) pin that sharing down; the hot page address and byte
// counter live in call-saved global registers to erase the prologue push/pop
// that C++ function decomposition otherwise pays; and pages stream straight to
// SPM/EEPROM with no SRAM staging. No inline assembly.
#include <libavr/libavr.hpp> #include <libavr/libavr.hpp>
@@ -17,7 +20,7 @@ namespace spm = avr::spm;
namespace ee = avr::eeprom; namespace ee = avr::eeprom;
using dev = avr::device<{.clock = 16_MHz}>; using dev = avr::device<{.clock = 16_MHz}>;
using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct}>; using serial_t = dev::uart0<{.baud = 115200_Bd, .max_baud_error = 3_pct, .half_duplex = true}>;
inline constexpr serial_t serial{}; inline constexpr serial_t serial{};
namespace tsb { namespace tsb {
@@ -26,6 +29,7 @@ constexpr auto off = avr::irq::guard_policy::unused;
constexpr std::uint8_t confirm = '!'; constexpr std::uint8_t confirm = '!';
constexpr std::uint8_t request = '?'; constexpr std::uint8_t request = '?';
constexpr std::uint8_t knock = '@';
constexpr std::uint16_t page = spm::page_bytes; constexpr std::uint16_t page = spm::page_bytes;
constexpr std::uint16_t boot_bytes = 1024; constexpr std::uint16_t boot_bytes = 1024;
@@ -47,21 +51,16 @@ constexpr std::uint16_t build_date = 26 * 512 + 7 * 32 + 19;
}; };
// clang-format on // clang-format on
[[gnu::section(".noinit")]] std::uint8_t buffer[page];
// The hot page walk lives in call-saved global registers, TSB-style: g_addr is // The hot page walk lives in call-saved global registers, TSB-style: g_addr is
// the running flash/EEPROM byte address, g_cnt the byte countdown. Being global // the running flash/EEPROM byte address, g_cnt the byte countdown. Being global
// they are never spilled around the rx/tx/spm calls the way a local would be, // they are never spilled around the rx/tx/spm calls the way a local would be
// which is where the pure variant pays its prologue push/pop. r4-r7 are // r4-r7 are call-saved, so the library's UART and SPM helpers preserve them.
// call-saved, so the library's UART/SPM helpers preserve them across calls.
register std::uint16_t g_addr asm("r4"); register std::uint16_t g_addr asm("r4");
register std::uint8_t g_cnt asm("r6"); register std::uint8_t g_cnt asm("r6");
std::uint8_t rx() std::uint8_t rx()
{ {
for (;;) return serial.read_blocking();
if (auto byte = serial.read())
return *byte;
} }
void tx(std::uint8_t byte) void tx(std::uint8_t byte)
@@ -74,13 +73,8 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
return reinterpret_cast<const std::uint8_t *>(addr); return reinterpret_cast<const std::uint8_t *>(addr);
} }
// One page transfer, memory selected at run time. noinline + noclone keep it a // Stream g_cnt bytes to the host from flash (LPM) or EEPROM, memory chosen at
// single shared body: the `flash` flag arrives from the command byte, so the // run time so the optimiser cannot split the loop into two clones.
// optimiser cannot split it back into a flash copy and an EEPROM copy. All of
// these walk g_addr / g_cnt, set by the caller.
// Stream g_cnt bytes to the host from flash (LPM) or EEPROM, advancing g_addr
// so a caller can send consecutive pages without re-seeding it.
[[gnu::noinline, gnu::noclone]] void send(bool flash) [[gnu::noinline, gnu::noclone]] void send(bool flash)
{ {
do { do {
@@ -89,36 +83,30 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
} while (--g_cnt); } while (--g_cnt);
} }
// Take one page from the host into the SRAM buffer.
[[gnu::noinline]] void get_page()
{
g_cnt = 0;
do {
buffer[g_cnt] = rx();
} while (++g_cnt != page);
}
[[gnu::noinline]] bool request_confirm() [[gnu::noinline]] bool request_confirm()
{ {
tx(request); tx(request);
return rx() == confirm; return rx() == confirm;
} }
// Program the SRAM buffer into the already-erased page at g_addr (flash) or into // Stream one page straight from the host into the already-erased flash page at
// EEPROM. g_addr is left on the page base for the caller to advance. // g_addr (SPM word buffer, low byte then high) or into EEPROM — no SRAM staging,
[[gnu::noinline, gnu::noclone]] void write_page(bool flash) // so receive and store are one loop. The memory is a run-time flag.
[[gnu::noinline, gnu::noclone]] void store_page(bool flash)
{ {
g_cnt = 0; g_cnt = 0;
if (flash) { if (flash) {
do { do {
spm::fill<off>(g_addr + g_cnt, static_cast<std::uint16_t>(buffer[g_cnt] | (buffer[g_cnt + 1] << 8))); std::uint8_t lo = rx();
std::uint8_t hi = rx();
spm::fill<off>(g_addr + g_cnt, static_cast<std::uint16_t>(lo | (hi << 8)));
g_cnt += 2; g_cnt += 2;
} while (g_cnt != page); } while (g_cnt != page);
spm::write_page<off>(g_addr); spm::write_page<off>(g_addr);
spm::wait(); spm::wait();
} else { } else {
do { do {
ee::write<off>(g_addr + g_cnt, buffer[g_cnt]); ee::write<off>(g_addr + g_cnt, rx());
} while (++g_cnt != page); } while (++g_cnt != page);
} }
} }
@@ -130,6 +118,18 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
__builtin_unreachable(); __builtin_unreachable();
} }
// Erase the whole application, one page at a time.
[[gnu::noinline]] void erase_application()
{
g_addr = 0;
do {
spm::erase_page<off>(g_addr);
spm::wait();
g_addr += page;
} while (g_addr < app_end);
spm::rww_enable<off>();
}
// 'f'/'e': stream memory back one page per host '!'. send advances g_addr, so // 'f'/'e': stream memory back one page per host '!'. send advances g_addr, so
// flash self-terminates at the application boundary; EEPROM runs until the host // flash self-terminates at the application boundary; EEPROM runs until the host
// stops. // stops.
@@ -150,22 +150,13 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
// the host offers behind '?'. // the host offers behind '?'.
[[gnu::noinline]] void write_mem(bool flash) [[gnu::noinline]] void write_mem(bool flash)
{ {
if (flash) { if (flash)
g_addr = 0; erase_application();
do {
spm::erase_page<off>(g_addr);
spm::wait();
g_addr += page;
} while (g_addr < app_end);
}
g_addr = 0; g_addr = 0;
while (request_confirm()) { while (request_confirm()) {
get_page(); store_page(flash);
write_page(flash);
g_addr += page; g_addr += page;
} }
if (flash)
spm::rww_enable<off>();
} }
// 'C': replace the config page, then echo it back for the host to verify. // 'C': replace the config page, then echo it back for the host to verify.
@@ -173,46 +164,82 @@ void write_config()
{ {
if (!request_confirm()) if (!request_confirm())
return; return;
get_page();
g_addr = app_end; g_addr = app_end;
spm::erase_page<off>(g_addr); spm::erase_page<off>(g_addr);
spm::wait(); spm::wait();
write_page(true); store_page(true);
spm::rww_enable<off>(); spm::rww_enable<off>();
g_addr = app_end;
g_cnt = page; g_cnt = page;
send(true); // g_addr is still app_end send(true);
}
// Emergency erase: wipe the application flash, the EEPROM and the config page.
[[gnu::noinline]] void emergency_erase()
{
erase_application();
g_addr = 0;
do {
ee::write<off>(g_addr, 0xff);
} while (++g_addr <= eeprom_end);
spm::erase_page<off>(app_end);
spm::wait();
spm::rww_enable<off>();
}
// The password gate. A byte of 0 requests emergency erase; a wrong byte hangs
// the loader (still draining the line), so it can never fall through to erase.
enum class gate : std::uint8_t { pass, emergency };
[[gnu::noinline]] gate password_gate()
{
for (const std::uint8_t *pw = flash_ptr(app_end + 3);; ++pw) {
std::uint8_t expected = avr::flash_load(pw);
if (expected == 0xff)
return gate::pass;
std::uint8_t got = rx();
if (got == 0)
return gate::emergency;
if (got != expected)
for (;;)
rx();
}
} }
[[noreturn]] void run() [[noreturn]] void run()
{ {
if (avr::hw::mcusr::read() & avr::hw::mcusr::wdrf(1).value) if (avr::hw::mcusr::wdrf.test())
appjump(); appjump();
avr::init<serial_t>(); avr::init<serial_t>();
std::uint32_t idle = static_cast<std::uint32_t>(avr::flash_load(flash_ptr(app_end + 2)) | 16) << 16;
std::uint8_t knocks = 0; std::uint8_t knocks = 0;
std::uint32_t idle = 4000000;
while (knocks < 3) { while (knocks < 3) {
if (auto byte = serial.read()) if (auto byte = serial.read())
knocks = *byte == '@' ? knocks + 1 : 0; knocks = *byte == knock ? knocks + 1 : 0;
else if (--idle == 0) else if (--idle == 0)
appjump(); appjump();
} }
for (const std::uint8_t *pw = flash_ptr(app_end + 3); avr::flash_load(pw) != 0xff; ++pw) switch (password_gate()) {
if (rx() != avr::flash_load(pw)) case gate::pass:
for (;;) {
}
g_addr = reinterpret_cast<std::uint16_t>(&info[0]); g_addr = reinterpret_cast<std::uint16_t>(&info[0]);
g_cnt = sizeof(info); g_cnt = sizeof(info);
send(true); send(true);
break;
case gate::emergency:
if (!request_confirm() || !request_confirm())
appjump();
emergency_erase();
break;
}
for (;;) { for (;;) {
tx(confirm); tx(confirm); // Mainloop ready
// Decode the command arithmetically so `flash`/`write` stay runtime // Decode the command arithmetically so flash/write stay run-time values:
// values: bit 5 is the case bit (upper = write), and the folded-lower // bit 5 is the case bit (upper = write), the folded-lower letter picks the
// letter picks the memory. A single unified path serves f/F/e/E. // memory. A single unified path serves f/F/e/E.
std::uint8_t cmd = rx(); std::uint8_t cmd = rx();
std::uint8_t lower = cmd | 0x20; std::uint8_t lower = cmd | 0x20;
bool write = (cmd & 0x20) == 0; bool write = (cmd & 0x20) == 0;