Compare commits
5 Commits
f32a27ff15
...
445e187722
| Author | SHA1 | Date | |
|---|---|---|---|
| 445e187722 | |||
| 250aba5cfb | |||
| 5c900720e3 | |||
| 7d6ef959b2 | |||
| 11ffbce2e2 |
@@ -45,9 +45,13 @@ endif()
|
||||
# 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
|
||||
# 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.
|
||||
# tsb_pure / tsb_tricks — pure-C++ and compiler-trickery variants, larger,
|
||||
# shown in the 1 KB section (BOOTSZ=10) they fit.
|
||||
# All three implement the full oracle feature set (see oracle/README.md):
|
||||
# watchdog bail, one-wire half-duplex, config-page activation timeout, password
|
||||
# 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>)
|
||||
function(add_tsb_variant name bytes)
|
||||
|
||||
47
oracle/README.md
Normal file
47
oracle/README.md
Normal 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
776
oracle/tsb-fixedbaud.asm
Normal 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
|
||||
;***********************************************************************
|
||||
;***********************************************************************
|
||||
;***********************************************************************
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "avr_uart.h"
|
||||
#include "sim_avr.h"
|
||||
#include "sim_elf.h"
|
||||
#include "uart_pty.h"
|
||||
@@ -68,6 +69,28 @@ int main(int argc, char *argv[])
|
||||
avr->pc = boot_base;
|
||||
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_connect(&uart_pty, '0');
|
||||
printf("TSB_PTY %s\n", uart_pty.pty.slavename);
|
||||
|
||||
133
test/tsbtest.py
133
test/tsbtest.py
@@ -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>
|
||||
Exits 0 if every scenario passes.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
@@ -13,16 +14,22 @@ import serial
|
||||
|
||||
CONFIRM = 0x21 # '!'
|
||||
REQUEST = 0x3F # '?'
|
||||
KNOCK = 0x40 # '@'
|
||||
PAGE = 128 # ATmega328P: 64 words
|
||||
|
||||
|
||||
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(
|
||||
[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.pty = None
|
||||
deadline = time.time() + 5
|
||||
@@ -137,6 +144,28 @@ class Host:
|
||||
self._expect(CONFIRM, "C end")
|
||||
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):
|
||||
if not cond:
|
||||
@@ -144,45 +173,73 @@ def check(cond, msg):
|
||||
print(f" ok: {msg}")
|
||||
|
||||
|
||||
# A config page carrying a password "PW": appjump 0, timeout 0x40, password
|
||||
# 0x50 0x57 terminated by 0xff.
|
||||
PW_CONFIG = "0000405057ff"
|
||||
PW_BYTES = bytes([0x50, 0x57])
|
||||
|
||||
|
||||
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()
|
||||
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[14] == info[15], f"device-type bytes 14==15 (got {info[14]:#x},{info[15]:#x})")
|
||||
check(host.pagesize == PAGE, f"page size {PAGE} (got {host.pagesize})")
|
||||
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}")
|
||||
|
||||
app = bytes(range(256)) # two pages of known data
|
||||
host.write_flash(app)
|
||||
check(host.read_flash(2) == app, "flash round-trip 2 pages")
|
||||
|
||||
edata = bytes((i * 7) & 0xFF for i in range(PAGE))
|
||||
host.write_eeprom(edata)
|
||||
check(host.read_eeprom(1) == edata, "eeprom round-trip 1 page")
|
||||
|
||||
cfg = bytes([0x00, 0x00, 0x40]) + b"\xff" * (PAGE - 3) # timeout 0x40, no password
|
||||
check(host.write_config(cfg) == cfg, "config write echoes the programmed page")
|
||||
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]
|
||||
dev = Device(binary, elf, boot_base)
|
||||
failures = []
|
||||
try:
|
||||
host = Host(dev.pty)
|
||||
|
||||
# --- activation ---
|
||||
info = host.activate()
|
||||
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[14] == info[15], f"device-type bytes 14==15 (got {info[14]:#x},{info[15]:#x})")
|
||||
check(host.pagesize == PAGE, f"page size {PAGE} (got {host.pagesize})")
|
||||
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}")
|
||||
|
||||
# --- flash write / read round-trip (actual self-programming) ---
|
||||
app = bytes(range(256)) # two pages of known data
|
||||
host.write_flash(app)
|
||||
back = host.read_flash(2)
|
||||
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))
|
||||
host.write_eeprom(edata)
|
||||
eback = host.read_eeprom(1)
|
||||
check(eback == edata, "eeprom round-trip 1 page")
|
||||
|
||||
# --- config page write / read round-trip ---
|
||||
cfg = bytes([0x00, 0x00, 0x40]) + b"\xff" * (PAGE - 3) # appjump 0, timeout 0x40, no password
|
||||
echo = host.write_config(cfg)
|
||||
check(echo == cfg, "config write echoes the programmed page")
|
||||
check(host.read_config() == cfg, "config read-back matches")
|
||||
|
||||
except AssertionError as e:
|
||||
failures.append(str(e))
|
||||
print(f" FAIL: {e}")
|
||||
finally:
|
||||
dev.stop()
|
||||
# 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:
|
||||
failures.append(f"{name}: {e}")
|
||||
print(f" FAIL: {e}")
|
||||
finally:
|
||||
dev.stop()
|
||||
|
||||
if failures:
|
||||
print(f"FAILED ({len(failures)})")
|
||||
|
||||
594
tsb/tsb_asm.cpp
594
tsb/tsb_asm.cpp
@@ -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)
|
||||
// with its hottest primitives — the UART poll/read/write and the SPM word/page
|
||||
// stores — written as small, self-contained inline-asm sequences. Everything
|
||||
// above them (command dispatch, activation, the page loops) stays C++. This is
|
||||
// the ≤512-byte boot-section deliverable.
|
||||
// The complete TinySafeBoot feature set — watchdog-reset bail, one-wire
|
||||
// half-duplex UART, a config-page activation timeout, the password gate,
|
||||
// emergency erase, and config/flash/EEPROM read-write — reimplemented for the
|
||||
// 512-byte ATmega328P boot section. Matching the hand-written assembly oracle's
|
||||
// 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 <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;
|
||||
namespace spm = avr::spm;
|
||||
namespace ee = avr::eeprom;
|
||||
|
||||
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 request = '?';
|
||||
|
||||
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::uint8_t knock = '@';
|
||||
|
||||
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
|
||||
[[gnu::progmem]] constexpr std::uint8_t info[16] = {
|
||||
'T', 'S', 'B',
|
||||
build_date & 0xFF, build_date >> 8,
|
||||
0xF3,
|
||||
0x1E, 0x95, 0x0F,
|
||||
page / 2,
|
||||
0xF3, // status: native-UART fixed-baud lineage
|
||||
0x1E, 0x95, 0x0F, // ATmega328P signature
|
||||
page / 2, // page size in words
|
||||
(app_end / 2) & 0xFF, (app_end / 2) >> 8,
|
||||
eeprom_end & 0xFF, eeprom_end >> 8,
|
||||
0xAA, 0xAA,
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
// 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
|
||||
// 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;
|
||||
// noinline keeps every caller funneling through the one shared copy (rx also
|
||||
// preserves Z/r0, which the store/send loops rely on across the call).
|
||||
[[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("%=: lds %0, %[sra] \n\t"
|
||||
" sbrs %0, %[rxc] \n\t"
|
||||
" rjmp %=b \n\t"
|
||||
" lds %0, %[udr] \n\t"
|
||||
: "=&r"(byte)
|
||||
: [sra] "n"(_SFR_MEM_ADDR(UCSR0A)), [rxc] "I"(RXC0), [udr] "n"(_SFR_MEM_ADDR(UDR0)));
|
||||
return byte;
|
||||
}
|
||||
|
||||
// Blocking transmit: spin on UDRE0, then store UDR0.
|
||||
void tx(std::uint8_t byte)
|
||||
{
|
||||
asm volatile("%=: lds __tmp_reg__, %[sra] \n\t"
|
||||
" sbrs __tmp_reg__, %[udre] \n\t"
|
||||
" rjmp %=b \n\t"
|
||||
" sts %[udr], %[b] \n\t"
|
||||
:
|
||||
: [sra] "n"(_SFR_MEM_ADDR(UCSR0A)), [udre] "I"(UDRE0), [udr] "n"(_SFR_MEM_ADDR(UDR0)), [b] "r"(byte));
|
||||
}
|
||||
|
||||
const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
{
|
||||
return reinterpret_cast<const std::uint8_t *>(addr);
|
||||
}
|
||||
|
||||
// One page transfer, memory selected at run time. noinline + noclone keep it a
|
||||
// single shared body: the `flash` flag arrives from the command byte, so the
|
||||
// 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. GCC's unified loop
|
||||
// (one body, per-byte memory branch) is already smaller than a split asm pair,
|
||||
// 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"
|
||||
" mov r1, r24 \n\t"
|
||||
" ldi r25, %[fill] \n\t"
|
||||
" out %[spmcsr], r25 \n\t"
|
||||
" spm \n\t"
|
||||
" clr r1 \n\t"
|
||||
" adiw r30, 2 \n\t"
|
||||
" dec %[words] \n\t"
|
||||
" brne %=b \n\t"
|
||||
: [words] "+d"(words)
|
||||
: [base] "r"(g_addr), [fill] "M"(_BV(__SPM_ENABLE)), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR))
|
||||
: "r24", "r25", "r30", "r31", "memory");
|
||||
spm::write_page<off>(g_addr);
|
||||
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"
|
||||
"0: in r25, %[spmcsr] \n\t"
|
||||
" sbrc r25, 0 \n\t"
|
||||
" rjmp 0b \n\t"
|
||||
" subi r30, 0x80 \n\t"
|
||||
" sbci r31, 0xFF \n\t"
|
||||
" cpi r30, lo8(%[end]) \n\t"
|
||||
" ldi r25, hi8(%[end]) \n\t"
|
||||
" cpc r31, r25 \n\t"
|
||||
" brlo %=b \n\t"
|
||||
:
|
||||
: [ers] "M"(_BV(PGERS) | _BV(__SPM_ENABLE)), [spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [end] "i"(app_end)
|
||||
: "r25", "r30", "r31");
|
||||
}
|
||||
g_addr = 0;
|
||||
while (request_confirm()) {
|
||||
store_page(flash);
|
||||
g_addr += page;
|
||||
}
|
||||
if (flash)
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
|
||||
// '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
|
||||
|
||||
// Reset lands here: BOOTRST vectors to the boot base, .vectors is laid first, and
|
||||
// no crt runs. The whole loader is this one naked routine.
|
||||
extern "C" [[gnu::naked, gnu::used, gnu::section(".vectors")]] void __boot_entry()
|
||||
{
|
||||
SP = RAMEND;
|
||||
tsb::run();
|
||||
asm volatile(
|
||||
// --- bring-up ------------------------------------------------------
|
||||
" ldi r16, lo8(%[ramend]) \n\t"
|
||||
" out %[spl], r16 \n\t"
|
||||
" ldi r16, hi8(%[ramend]) \n\t"
|
||||
" out %[sph], r16 \n\t"
|
||||
" in r16, %[mcusr] \n\t" // watchdog reset → hand straight back
|
||||
" sbrc r16, 3 \n\t" // MCUSR bit 3 = WDRF
|
||||
" rjmp 9f \n\t" // 9: = appjump
|
||||
" ldi r16, %[ubrr] \n\t" // fixed baud, UBRR0L only
|
||||
" sts %[ubrr0l], r16 \n\t"
|
||||
" ldi r16, 0x02 \n\t" // 1<<U2X0
|
||||
" sts %[ucsr0a], r16 \n\t"
|
||||
" clr r22 \n\t" // direction flag bit0: 0 = receiving, 1 = driving the line
|
||||
// --- activation: 3×'@' inside a config-page-timed window -----------
|
||||
" ldi r30, lo8(%[appto]) \n\t" // Z = config page + 2
|
||||
" ldi r31, hi8(%[appto]) \n\t"
|
||||
" lpm r23, Z+ \n\t" // timeout byte; Z → password
|
||||
" ori r23, %[actmin] \n\t" // lockout-proof floor
|
||||
" clr r17 \n\t" // knock counter
|
||||
"1: rcall tsb_rx \n\t"
|
||||
" brcs 9f \n\t" // window elapsed → application
|
||||
" cpi r16, %[knock] \n\t"
|
||||
" brne 9f \n\t" // any non-'@' → application
|
||||
" inc r17 \n\t"
|
||||
" cpi r17, 3 \n\t"
|
||||
" brne 1b \n\t"
|
||||
// --- password / emergency erase (Z at config-page password) --------
|
||||
" ldi r23, %[commto] \n\t" // widen the timeout for the session
|
||||
"2: ser r19 \n\t" // r19=0xff → comparison enabled
|
||||
"3: lpm r18, Z+ \n\t"
|
||||
" and r18, r19 \n\t" // a prior mismatch (r19=0) blanks the rest
|
||||
" cpi r18, 0xff \n\t"
|
||||
" breq tsb_info \n\t" // 0xff terminator → password satisfied
|
||||
" rcall tsb_rx \n\t"
|
||||
" cpi r16, 0 \n\t"
|
||||
" breq 5f \n\t" // a 0 byte requests emergency erase
|
||||
" 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"
|
||||
" clr r1 \n\t"
|
||||
" adiw r30, 2 \n\t"
|
||||
" dec r20 \n\t"
|
||||
" brne 1b \n\t"
|
||||
" subi r30, lo8(%[page]) \n\t" // back to the page base for PGWRT
|
||||
" sbci r31, hi8(%[page]) \n\t"
|
||||
" ldi r24, %[spm_wrt] \n\t"
|
||||
" out %[spmcsr], r24 \n\t"
|
||||
" spm \n\t"
|
||||
" rcall tsb_spmw \n\t"
|
||||
" subi r30, lo8(-%[page]) \n\t" // Z → next page base
|
||||
" sbci r31, hi8(-%[page]) \n\t"
|
||||
" ret \n\t"
|
||||
// --- erase [0, config page) ----------------------------------------
|
||||
"tsb_erapp: \n\t"
|
||||
" clr r30 \n\t"
|
||||
" clr r31 \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"
|
||||
:
|
||||
: [ramend] "i"(RAMEND), [spl] "I"(_SFR_IO_ADDR(SPL)), [sph] "I"(_SFR_IO_ADDR(SPH)),
|
||||
[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)),
|
||||
[spmcsr] "I"(_SFR_IO_ADDR(SPMCSR)), [spm_fill] "n"(_BV(__SPM_ENABLE)),
|
||||
[spm_ers] "n"(_BV(PGERS) | _BV(__SPM_ENABLE)), [spm_wrt] "n"(_BV(PGWRT) | _BV(__SPM_ENABLE)),
|
||||
[spm_rww] "n"(_BV(RWWSRE) | _BV(__SPM_ENABLE)), [eecr] "I"(_SFR_IO_ADDR(EECR)),
|
||||
[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),
|
||||
[info] "i"(&tsb::info[0]), [page] "n"(tsb::page), [words] "n"(tsb::page / 2), [actmin] "n"(tsb::act_min),
|
||||
[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");
|
||||
}
|
||||
|
||||
135
tsb/tsb_pure.cpp
135
tsb/tsb_pure.cpp
@@ -1,17 +1,14 @@
|
||||
// TinySafeBoot on libavr — tier 1: pure, idiomatic C++.
|
||||
//
|
||||
// A ≤512-byte serial flash bootloader for the ATmega328P boot section,
|
||||
// reimplementing the TinySafeBoot wire protocol (native-UART fixed-baud
|
||||
// lineage) on libavr. This variant is written for clarity: well-factored
|
||||
// functions, no compiler-specific size hacks, no inline assembly. The only
|
||||
// attribute is the one the task inherently needs — the naked reset entry that
|
||||
// stands in for the absent C runtime; the flash-resident info block is a libavr
|
||||
// flash_table.
|
||||
//
|
||||
// The structure follows the hand-written reference: one SRAM page buffer that
|
||||
// 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.
|
||||
// A serial flash bootloader for the ATmega328P boot section, reimplementing the
|
||||
// TinySafeBoot native-UART fixed-baud protocol on libavr with the full feature
|
||||
// set of the hand-written oracle: a watchdog-reset bail, one-wire half-duplex,
|
||||
// a config-page activation timeout, the password gate, emergency erase, and
|
||||
// config/flash/EEPROM read-write. This variant is written for clarity —
|
||||
// well-factored functions, no compiler-specific size hacks, no inline assembly.
|
||||
// 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
|
||||
// stands in for the absent C runtime.
|
||||
|
||||
#include <libavr/libavr.hpp>
|
||||
|
||||
@@ -22,7 +19,8 @@ namespace spm = avr::spm;
|
||||
namespace ee = avr::eeprom;
|
||||
|
||||
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{};
|
||||
|
||||
namespace tsb {
|
||||
@@ -31,15 +29,15 @@ namespace tsb {
|
||||
// EEPROM lock folds to nothing under this posture.
|
||||
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 request = '?';
|
||||
constexpr std::uint8_t knock = '@';
|
||||
|
||||
// Boot geometry for the ATmega328P 512-byte boot section (BOOTSZ=11). The page
|
||||
// size, flash and EEPROM extents are the chip database's to know. app_end is
|
||||
// both the first byte the loader protects and the config page (the LASTPAGE
|
||||
// holding app-jump vector, timeout and password), one page below the boot
|
||||
// section.
|
||||
// Boot geometry for the 1 KB boot section (BOOTSZ=10). The page size and the
|
||||
// flash/EEPROM extents are the chip database's to know. app_end is the config
|
||||
// page (the LASTPAGE holding the app-jump vector, activation timeout and
|
||||
// password), one page below the boot section.
|
||||
constexpr std::uint16_t page = spm::page_bytes;
|
||||
constexpr std::uint16_t boot_bytes = 1024;
|
||||
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;
|
||||
|
||||
// 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
|
||||
// reads it back through LPM.
|
||||
// keeps it in progmem with no .data image (there is no crt to copy one).
|
||||
// clang-format off
|
||||
inline constexpr std::array<std::uint8_t, 16> info_data = {
|
||||
'T', 'S', 'B',
|
||||
@@ -66,15 +63,14 @@ inline constexpr std::array<std::uint8_t, 16> info_data = {
|
||||
using info = avr::flash_table<info_data>;
|
||||
|
||||
// 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
|
||||
// bytes in .text, which is the only thing the boot-section budget counts.
|
||||
// it lives in .noinit — no startup clear (there is no crt) and no .text bytes.
|
||||
[[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()
|
||||
{
|
||||
for (;;)
|
||||
if (auto byte = serial.read())
|
||||
return *byte;
|
||||
return serial.read_blocking();
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
// 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
|
||||
// password, or an idle programmer port lands here.
|
||||
[[noreturn]] void appjump()
|
||||
@@ -160,19 +166,15 @@ void read_eeprom()
|
||||
}
|
||||
}
|
||||
|
||||
// 'F': erase the whole application first (unwritten pages stay erased), then
|
||||
// take pages the host offers behind '?'.
|
||||
// 'F': erase the whole application first, then take pages the host offers
|
||||
// behind '?'.
|
||||
void write_flash()
|
||||
{
|
||||
for (std::uint16_t a = 0; a < app_end; a += page) {
|
||||
spm::erase_page<off>(a);
|
||||
spm::wait();
|
||||
}
|
||||
erase_application();
|
||||
for (std::uint16_t a = 0; request_confirm(); a += page) {
|
||||
get_page();
|
||||
write_flash_page(a);
|
||||
}
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
|
||||
// 'E': take pages the host offers behind '?' into EEPROM.
|
||||
@@ -197,36 +199,71 @@ void write_config()
|
||||
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()
|
||||
{
|
||||
// A bootloader may be entered by a watchdog reset; the reference loader
|
||||
// hands straight back to the application in that case rather than run.
|
||||
// A watchdog reset hands straight back to the application, as the reference
|
||||
// loader does, rather than re-entering the bootloader.
|
||||
if (avr::hw::mcusr::wdrf.test())
|
||||
appjump();
|
||||
|
||||
avr::init<serial_t>();
|
||||
|
||||
// Activation: the host knocks three '@'. With no programmer attached the
|
||||
// port stays idle, so a bounded wait boots the application instead of
|
||||
// hanging forever.
|
||||
// Activation: the host knocks three '@' inside a window whose length is the
|
||||
// config page's timeout byte (floored so a corrupt page can never lock the
|
||||
// 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::uint32_t idle = 4000000;
|
||||
while (knocks < 3) {
|
||||
if (auto byte = serial.read())
|
||||
knocks = *byte == '@' ? knocks + 1 : 0;
|
||||
knocks = *byte == knock ? knocks + 1 : 0;
|
||||
else if (--idle == 0)
|
||||
appjump();
|
||||
}
|
||||
|
||||
// Password gate: the config page holds it at app_end+3, terminated by 0xff.
|
||||
// A blank page (0xff there) means no password. A wrong byte hangs the loader
|
||||
// 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());
|
||||
switch (password_gate()) {
|
||||
case gate::pass:
|
||||
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 (;;) {
|
||||
tx(confirm); // Mainloop ready
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
// TinySafeBoot on libavr — tier 2: C++ with compiler trickery.
|
||||
//
|
||||
// Same protocol and libavr surface as the pure variant (tsb_pure.cpp), but the
|
||||
// readable one-handler-per-command shape is traded for size: flash and EEPROM
|
||||
// share a single code path selected by a *runtime* flag decoded from the
|
||||
// command byte, so the compiler cannot constant-propagate it into two clones.
|
||||
// Attributes pin that sharing down (noinline/noclone) and the hot page pointer
|
||||
// and byte counter are pinned to call-saved registers to erase the prologue
|
||||
// push/pop that C++ function decomposition otherwise pays. No inline assembly.
|
||||
// Same protocol, libavr surface and full feature set as the pure variant
|
||||
// (tsb_pure.cpp) — watchdog bail, one-wire, config-page timeout, password gate,
|
||||
// emergency erase, config/flash/EEPROM read-write — but the readable
|
||||
// one-handler-per-command shape is traded for size. Flash and EEPROM share a
|
||||
// single code path selected by a *runtime* flag decoded from the command byte,
|
||||
// so the compiler cannot constant-propagate it into two clones; attributes
|
||||
// (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>
|
||||
|
||||
@@ -17,7 +20,7 @@ namespace spm = avr::spm;
|
||||
namespace ee = avr::eeprom;
|
||||
|
||||
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{};
|
||||
|
||||
namespace tsb {
|
||||
@@ -26,6 +29,7 @@ constexpr auto off = avr::irq::guard_policy::unused;
|
||||
|
||||
constexpr std::uint8_t confirm = '!';
|
||||
constexpr std::uint8_t request = '?';
|
||||
constexpr std::uint8_t knock = '@';
|
||||
|
||||
constexpr std::uint16_t page = spm::page_bytes;
|
||||
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
|
||||
|
||||
[[gnu::section(".noinit")]] std::uint8_t buffer[page];
|
||||
|
||||
// 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
|
||||
// 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 UART/SPM helpers preserve them across calls.
|
||||
// they are never spilled around the rx/tx/spm calls the way a local would be —
|
||||
// r4-r7 are call-saved, so the library's UART and SPM helpers preserve them.
|
||||
register std::uint16_t g_addr asm("r4");
|
||||
register std::uint8_t g_cnt asm("r6");
|
||||
|
||||
std::uint8_t rx()
|
||||
{
|
||||
for (;;)
|
||||
if (auto byte = serial.read())
|
||||
return *byte;
|
||||
return serial.read_blocking();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// One page transfer, memory selected at run time. noinline + noclone keep it a
|
||||
// single shared body: the `flash` flag arrives from the command byte, so the
|
||||
// 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.
|
||||
// Stream g_cnt bytes to the host from flash (LPM) or EEPROM, memory chosen at
|
||||
// run time so the optimiser cannot split the loop into two clones.
|
||||
[[gnu::noinline, gnu::noclone]] void send(bool flash)
|
||||
{
|
||||
do {
|
||||
@@ -89,36 +83,30 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
} 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()
|
||||
{
|
||||
tx(request);
|
||||
return rx() == confirm;
|
||||
}
|
||||
|
||||
// Program the SRAM buffer into the already-erased page at g_addr (flash) or into
|
||||
// EEPROM. g_addr is left on the page base for the caller to advance.
|
||||
[[gnu::noinline, gnu::noclone]] void write_page(bool flash)
|
||||
// Stream one page straight from the host into the already-erased flash page at
|
||||
// g_addr (SPM word buffer, low byte then high) or into EEPROM — no SRAM staging,
|
||||
// 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;
|
||||
if (flash) {
|
||||
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;
|
||||
} while (g_cnt != page);
|
||||
spm::write_page<off>(g_addr);
|
||||
spm::wait();
|
||||
} else {
|
||||
do {
|
||||
ee::write<off>(g_addr + g_cnt, buffer[g_cnt]);
|
||||
ee::write<off>(g_addr + g_cnt, rx());
|
||||
} while (++g_cnt != page);
|
||||
}
|
||||
}
|
||||
@@ -130,6 +118,18 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
__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
|
||||
// flash self-terminates at the application boundary; EEPROM runs until the host
|
||||
// stops.
|
||||
@@ -150,22 +150,13 @@ const std::uint8_t *flash_ptr(std::uint16_t addr)
|
||||
// the host offers behind '?'.
|
||||
[[gnu::noinline]] void write_mem(bool flash)
|
||||
{
|
||||
if (flash) {
|
||||
g_addr = 0;
|
||||
do {
|
||||
spm::erase_page<off>(g_addr);
|
||||
spm::wait();
|
||||
g_addr += page;
|
||||
} while (g_addr < app_end);
|
||||
}
|
||||
if (flash)
|
||||
erase_application();
|
||||
g_addr = 0;
|
||||
while (request_confirm()) {
|
||||
get_page();
|
||||
write_page(flash);
|
||||
store_page(flash);
|
||||
g_addr += page;
|
||||
}
|
||||
if (flash)
|
||||
spm::rww_enable<off>();
|
||||
}
|
||||
|
||||
// 'C': replace the config page, then echo it back for the host to verify.
|
||||
@@ -173,46 +164,82 @@ void write_config()
|
||||
{
|
||||
if (!request_confirm())
|
||||
return;
|
||||
get_page();
|
||||
g_addr = app_end;
|
||||
spm::erase_page<off>(g_addr);
|
||||
spm::wait();
|
||||
write_page(true);
|
||||
store_page(true);
|
||||
spm::rww_enable<off>();
|
||||
g_addr = app_end;
|
||||
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()
|
||||
{
|
||||
if (avr::hw::mcusr::read() & avr::hw::mcusr::wdrf(1).value)
|
||||
if (avr::hw::mcusr::wdrf.test())
|
||||
appjump();
|
||||
|
||||
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::uint32_t idle = 4000000;
|
||||
while (knocks < 3) {
|
||||
if (auto byte = serial.read())
|
||||
knocks = *byte == '@' ? knocks + 1 : 0;
|
||||
knocks = *byte == knock ? knocks + 1 : 0;
|
||||
else if (--idle == 0)
|
||||
appjump();
|
||||
}
|
||||
|
||||
for (const std::uint8_t *pw = flash_ptr(app_end + 3); avr::flash_load(pw) != 0xff; ++pw)
|
||||
if (rx() != avr::flash_load(pw))
|
||||
for (;;) {
|
||||
}
|
||||
|
||||
g_addr = reinterpret_cast<std::uint16_t>(&info[0]);
|
||||
g_cnt = sizeof(info);
|
||||
send(true);
|
||||
switch (password_gate()) {
|
||||
case gate::pass:
|
||||
g_addr = reinterpret_cast<std::uint16_t>(&info[0]);
|
||||
g_cnt = sizeof(info);
|
||||
send(true);
|
||||
break;
|
||||
case gate::emergency:
|
||||
if (!request_confirm() || !request_confirm())
|
||||
appjump();
|
||||
emergency_erase();
|
||||
break;
|
||||
}
|
||||
|
||||
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.
|
||||
tx(confirm); // Mainloop ready
|
||||
// Decode the command arithmetically so flash/write stay run-time values:
|
||||
// bit 5 is the case bit (upper = write), 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;
|
||||
|
||||
Reference in New Issue
Block a user