Source file: /~heha/ewa/PIC16F145x-Urlader/bootCDC1.zip/bootCDC-hvp.a15

; USB 512-Word CDC Bootloader for PIC16(L)F1454/5/9
; Copyright (c) 2015, Matt Sarnoff (msarnoff.org)
; Heavily modified 2017 by Henrik Haftmann (henrik.haftmann@gmail.com)
; for better CDC compatibility and extra commands for twiddeling with I/O
; Verified application use, added flash read function (with 'R' flag),
; and configuration space R/W access, 2018

; To stay in bootloader at powerup, RA3 (= !MCLR) must be held low.
; If there is no application programmed, the bootloader is active at powerup.

; After jumping to application code, RA3 is a free input for application's use
; but device requires high-voltage (8 V at !MCLR) programming.
; The application code cannot use other reset sources while RA3 is held low.
; (Other reset sources are RESET instruction, Watchdog, Stack Overflow, etc.)

; To be detected as a valid application, the lower 8 bits of the first
; instruction word (at address 0x200) must NOT be 0xFF.


; At application start, the device is configured with a 48 MHz CPU clock,
; using the internal oscillator and 3x PLL.

; Code notes:
; - Labels that do not begin with an underscore can be called as functions.
;   Labels that begin with an underscore are not safe to call,
;   they should only be reached via goto.
;
; - FSR[01][HL] are used as temporary registers in several places,
;   e.g. as loop counters. They're accessible regardless of the current bank
;   and automatically saved/restored on interrupt.
;
; - As much stuff as possible is packed into bank 0 of RAM.
;   This includes the buffer descriptors, bootloader state,
;   endpoint buffers 0 OUT, 0 IN, and the beginning of the 64-byte 1 OUT buffer.
;
; - Notification endpoint 2 is enabled but never used (= returns NAK to host)
;   The endpoint 2 IN buffer descriptor is left uninitialized.
;   The endpoint 2 OUT buffer descriptor is used as 4 bytes of Bank0 RAM.
;
; - The programming protocol is described in the 'usb16f1prog' script.
;   It is very minimal, but does provide checksum verification.
;   Writing the ID words (0x8000-8003) is now supported.
;   Writing the configuration words is not possible via self-programming.

	radix	dec
	list	n=0
	include	"p16f1454.inc"
	include	"mymacros.i15"

; Configuration
; !MCLR/RA3 pin must be configured as RA3
; The 0x200 words of bootloader itself is write-protected by _WRT_BOOT fuse bits
;TODO: The bootloader should support low-power USB idle.

 __config _CONFIG1,_FOSC_INTOSC&_WDTE_SWDTEN&_PWRTE_ON&_MCLRE_OFF	;0b11'1111'1000'1100
 __config _CONFIG2,_WRT_BOOT&_CPUDIV_NOCLKDIV&_LVP_OFF			;0b01'1111'1100'1110

; Constants used by the bootloader communications protocol.

; Command is determined by the length of the data packet received.
; If length == 1, Reset Device. The one byte sent must be 'R'. Returns 1 or INVALID_COMMAND
; If length == 2, read from address. So user can read from PORT etc. Returns byte read.
; If length == 3, write to address. So user can set PORT pins etc. Returns 1.
; If length == 4, Prepare program/readout: Set Address/Checksum/Erase Flag:
;	{word address; byte checksum; char eraseflag;}	// eraseflag=='E': erase this row
;							// eraseflag=='R': read this row
; If length == 64, transfer data for writing one flash page.
; Otherwise, it's a wrong command.

; Status codes returned from device to host
BSTAT_OK		equ	1	; all ok
BSTAT_INVALID_COMMAND	equ	2	; host sent an invalid command
BSTAT_INVALID_CHECKSUM	equ	3	; invalid checksum, no write performed
BSTAT_VERIFY_FAILED	equ	4	; data written to flash doesn't match data sent by host

CONFIG_DESC_LEN	equ	67	; total length of configuration descriptor and sub-descriptors

E0_SSH 		equ	3	; endpoint 0 buffer size: 1<<3 = 8
E1O_SSH		equ	6	; endpoint 1 OUT (CDC data) buffer size 1<<6 = 64
E1I_SSH		equ	6	; endpoint 1 IN (CDC data) buffer size

;Exy_STA bits
BSTALL		equ	2
DTSEN		equ	3
DTS		equ	6
UOWN		equ	7

;list of Bank0 RAM variables, starting with BDT
	cblock 0x20	;Bank 0 (completely full)
E0O_STA
E0O_CNT
E0O_ADR:2
E0I_STA
E0I_CNT
E0I_ADR:2
E1O_STA
E1O_CNT
E1O_ADR:2
E1I_STA
E1I_CNT
E1I_ADR:2
; Since we're only using 5 endpoints, use the BDT area for buffers,
; and use the 4 bytes normally occupied by the EP2 OUT buffer descriptor for variables.
USB_STATE	; some flags, see below
SETUPDAT_PTR	; pointer to descriptor to be sent (low byte only, high byte is 0x81 for all descriptors)
SETUPDAT_CNT	; remaining bytes to be sent
CHECKSUM	; for saving expected checksum
E2I_STA
E2I_CNT
E2I_ADR:2

E0_BUF:1<<E0_SSH	; shared buffer for EP0 Setup, Out, and In transfer
E1O_BUF:1<<E1O_SSH	; At address 0x40, last 16 bytes hang over into Bank 1
	endc
	cblock	0xB0	; Bank 1 (completely full)
E1I_BUF:1<<E1I_SSH	; 64 byte for EP1 IN
	endc
bmRequestType	equ	E0_BUF+0
bRequest	equ	E0_BUF+1
wValueL		equ	E0_BUF+2
wValueH		equ	E0_BUF+3
wLengthL	equ	E0_BUF+6
wLengthH	equ	E0_BUF+7
LIN_E0_BUF	equ	E0_BUF+0x1FE0	;0x2018 .. 0x201F
LIN_E1O_BUF	equ	E1O_BUF+0x1FE0	;0x2020 .. 0x205F
LIN_E1I_BUF	equ	E1I_BUF+0x1FB0	;0x2060 .. 0x209F

; USB_STATE bit flags
IS_CONTROL_READ	equ	7	; current EP0 transaction is a control read
READFLASH	equ	6	; return 64 byte, not 1
PENDING_RESET	equ	5	; do RESET opcode when idle
POLL_FROM_APP	equ	4	; usbPoll is called from outside: call usbRx @ 0x202
CDC_DCD		equ	1	; Data Carrier Detect bit (for CDC)
CONFIGURED	equ	0	; the device is configured

	goto	onReset		; to be continued further down in the file
	goto	usbInit
	bra	usbPoll		; Make callable from application code
	bra	usbTx		; Any data chunk must end with a short packet
	pagesel	0x204
	goto	0x204		; to application interrupt routine

usbPoll
	banksel	USB_STATE
	bsf	USB_STATE,POLL_FROM_APP
usbPoll1
	banksel	UIR		; Bank 29
	btfsc	UIR,URSTIF	; reset?
	 goto	usbReset	; if so, reset the USB interface
; service transactions
	btfss	UIR,TRNIF
	 return
	movfw	USTAT		; store the status (endpoint number + direction)
	movwf	FSR1H		; in that temp register
	bcf	UIR,TRNIF	; clear flag and advance USTAT fifo
	banksel	E0O_STA		; Bank 0
	andlw	0x78		; endpoint number !=0 ?
	brnz	usb_service_cdc	; if not endpoint 0, it's a CDC message
; Handles a control transfer on endpoint 0.
; args:		BSR=0, FSR1H=USTAT
; clobbers:	W, FSR1H
	btfsc	FSR1H,DIR	; is it an IN transfer or an OUT/SETUP?
	 bra	_usb_ctrl_in
; it's an OUT or SETUP transfer
	movfw	E0O_STA		; All non-PID bits are zero here because SETUP comes with DATA0
	xorlw	13<<2		; 13=PID_SETUP — is it a SETUP packet?
	brnz	arm_e0o		; if not, it's a DATA or STATUS OUT, ignore data and rearm buffer

; Handles a SETUP control transfer on endpoint 0.
; BSR=0
	clrf	E0O_STA		; let start with DATA1
	clrf	E0I_STA		; let start with DATA1
	clrf	SETUPDAT_CNT	; No data phase as standard
	bcf	USB_STATE,IS_CONTROL_READ
; get bmRequestType, but don't bother checking whether it's standard/class/vendor...
; the CDC and standard requests we'll receive have distinct bRequest numbers
	btfsc	bmRequestType,7	; is this device->host?
	 bsf	USB_STATE,IS_CONTROL_READ	; if so, this is a control read
	movfw	bRequest
; is it Set Address?
	addlw	-5			;5 = SET_ADDRESS
	brz	_setup_complete
; check request number: is it Get Descriptor?
	decfsz	WREG,w			;6 = GET_DESCRIPTOR
	 bra	_no_usb_get_descriptor
; Handles a Get Descriptor request.
; check descriptor type
	decfsz	wValueH,w		;1 = DESC_DEVICE
	 bra	_no_desc_device
	movlwf	low DEVICE_DESCRIPTOR,SETUPDAT_PTR
	movlw	18
	bra	_setup_complete_w
_no_desc_device
	assume	0
	decfsz	WREG,w			;2 = DESC_CONFIG
	 bra	_usb_ctrl_invalid
	movlwf	low CONFIGURATION_DESCRIPTOR,SETUPDAT_PTR
	movlw	CONFIG_DESC_LEN	; length includes all subordinate descriptors
	bra	_setup_complete_w
_no_usb_get_descriptor
; is it Get Configuration?
	decf	WREG,w
	decfsz	WREG,w			;8 = GET_CONFIG
	 bra	_no_usb_get_configuration

; Handles a Get Configuration request.
; BSR=0
; load a pointer to either a 0 or a 1 in ROM
; the 0 and 1 have been chosen so that they are adjacent
	movfw	USB_STATE
	andlw	1<<CONFIGURED	; bit 0
	addlw	low CONFIGURATION_01_CONSTANT
	movwf	SETUPDAT_PTR
	movlw	1
	bra	_setup_complete_w
_no_usb_get_configuration
	decfsz	WREG,w			;9 = SET_CONFIG
	 bra	_no_usb_set_config

; Handles a Set Configuration request.
; BSR=0
	assume	0
	movfw	wValueL		; should be 0 or 1
	movwf	USB_STATE
; Initializes the buffers for the CDC endpoints (1 OUT, 1 IN, and 2 IN).
; clobbers:	W, BSR=0
	movlw	1<<DTSEN	; expect DATA0 first
	call	arm_e1o_w
	movwf	E1I_STA		; USB NAK and toggle bit set: Send first packet as DATA0
	bra	_setup_complete
_no_usb_set_config
	addlw	9-0x20			;0x20 = SET_LINE_CODING
	brnz	_no_set_line_coding
	banksel	UCON
	bcf	UCON,PKTDIS
	banksel	0
	bcf	E0I_STA,UOWN
	clrf	E0I_CNT	; we'll be sending a zero-length packet
	movlw	0		; ignore odd/even
	bra	_armbfs
_no_set_line_coding
	decfsz	WREG,w			;0x21 = GET_LINE_CODING
	 bra	_no_get_line_coding
	movlwf	low LINE_CODING,SETUPDAT_PTR
	movlw	7
	bra	_setup_complete_w
_no_get_line_coding
	decfsz	WREG,w
	 bra	_usb_ctrl_invalid	;0x22 = SET_CONTROL_LINE_STATE
	bsf	USB_STATE,CDC_DCD
; unhandled request? fall through to _usb_ctrl_invalid
	movlw	7
_setup_complete_w
	movwf	SETUPDAT_CNT
; If requested length is shorter, transfer less than descriptor size
	tstf	wLengthH
	brnz	_setup_complete	; more than 255 bytes requested (Windows 7: 0x109)
	subwf	wLengthL,w
	brc	_setup_complete	; if W <= f, no need to adjust
	movfw	wLengthL
	movwf	SETUPDAT_CNT
; Finishes a successful SETUP transaction.
; SETUPDAT_CNT contains transfer length.
_setup_complete
	banksel	UCON
	bcf	UCON,PKTDIS		; reenable packet processing
	banksel	USB_STATE
	btfss	bmRequestType,7
	 bra	_cwrite
; this is a control read; prepare the IN endpoint for the data stage
; and the OUT endpoint for the status stage
	call	ep0_send_in		; read data into IN buffer
	movlw	1<<DTS|1<<DTSEN		; make OUT buffer ready for status stage
; value in W is used to specify the EP0 OUT flags
_armbfs	call	arm_e0o_w
; Send next packet to Endpoint 0 In, size already set to E0I_CNT
arm_e0i
	comf	E0I_STA,w	; toggle DTS
	andlw	1<<DTS
	iorlw	1<<DTSEN
arm_e0i_w			; W specifies STAT flags
	movwf	E0I_STA
	bsf	E0I_STA,UOWN
	return
; this is a control write: prepare the IN endpoint for the status stage
; and the OUT endpoint for the next SETUP transaction
_cwrite	bcf	E0I_STA,UOWN	; ensure we have ownership of the buffer
	clrf	E0I_CNT	; we'll be sending a zero-length packet
	movlw	1<<DTSEN|1<<BSTALL	; make OUT buffer ready for next SETUP packet
	bra	_armbfs			; arm OUT and IN buffers

; Finishes a rejected SETUP transaction: the endpoints are stalled
_usb_ctrl_invalid
	banksel	UCON
	bcf	UCON,PKTDIS	; reenable packet processing
	banksel	E0I_STA
	movlw	1<<DTSEN|1<<BSTALL
	call	arm_e0i_w
arm_ep0_out_stall	;for next SETUP
	movlw	1<<DTSEN|1<<BSTALL
	bra	arm_e0o_w
; Prepare reception of next Endpoint 0 Out Data (or Setup) packet
arm_e0o
	comf	E0O_STA,w	; toggle DTS
	andlw	1<<DTS
	iorlw	1<<DTSEN
arm_e0o_w			; W specifies STAT flags
	;assume	E0_BUF
	movwf	E0O_STA
	movlwf	1<<E0_SSH,E0O_CNT	; reset the buffer count
	bsf	E0O_STA,UOWN	; arm the OUT endpoint
	return

; Handles an IN control transfer on endpoint 0.
; Can be (descriptor) data or an acknowledge packet
; BSR=0
_usb_ctrl_in
	btfss	USB_STATE,IS_CONTROL_READ	; is this a control read or write?
	 bra	_check_for_pending_address
; fetch more data and re-arm the IN endpoint
	call	ep0_send_in
	bra	arm_e0i			; arm the IN buffer
	
; if this is the status stage of a Set Address request, assign the address here.
; The OUT buffer has already been armed for the next SETUP.
_check_for_pending_address
	movfw	bRequest
	xorlw	5
	skpz
	 return
; read the address out of the setup packed in the OUT buffer
	movfw	wValueL
	banksel	UADDR
	movwf	UADDR
	return

; Reads descriptor data from SETUPDAT_PTR, copies it to the EP0 IN buffer,
; and decrements SETUPDAT_CNT.
; Cannot simply point EP0_ADDR to flash as it is not accessible by USB SIE
; args:		BSR=0
; returns:	SETUPDAT_PTR advanced
;		SETUPDAT_CNT decremented
; clobbers:	W, FSR0, FSR1
ep0_send_in
	clrf	E0I_CNT	; initialize transfer length to 0
	tstf	SETUPDAT_CNT	; do nothing if there are 0 bytes to send
	skpnz
	 return
	movfw	SETUPDAT_PTR		; set up source pointer
	movwf	FSR0L
	movlwf	high DEVICE_DESCRIPTOR,FSR0H	;high part always 1, so 0x81 here
	movlwf	E0_BUF,FSR1L	; Here: Banked address (not a problem), set up destination pointer
	clrf	FSR1H
; byte copy loop
_bcopy	btfsc	E0I_CNT,E0_SSH	; 8 Bytes full?
	 bra	_bcdone
	moviw	FSR0++
	movwi	FSR1++
	incf	E0I_CNT,f	; increase number of bytes copied
	decfsz	SETUPDAT_CNT,f	; decrement number of bytes remaining
	 bra	_bcopy
; write back the updated source pointer
_bcdone	movfw	FSR0L
	movwf	SETUPDAT_PTR
	return

;Send data chunk to host. Note that a short packet ends a transfer unit (= chunk for ReadFile).
;In case the last packet is full, a zero-length packet must be appended to mark the end of chunk.
;args:	W=Bytes (0..64)
;	EP1IN buffer filled with data
;clobbers: W,bank=0
usbTx	banksel	0
_u1	btfsc	E1I_STA,UOWN	; spinloop until previous buffer is read by USB host
	 bra	_u1		; (Bug: May hang if USB connection is somehow lost in between)
	movwf	E1I_CNT		; put transfer size
	comf	E1I_STA,w	; toggle DTS
	andlw	1<<DTS		; clear all other bits (STALL too)
	iorlw	1<<DTSEN	; 0↕001000 = use toggle bit
	movwf	E1I_STA
	bsf	E1I_STA,UOWN	; give to SIE
	return

; Services a transaction on one of the CDC endpoints.
; args:		USTAT value in FSR1H
;		BSR=0
; clobbers:	W, FSR0, FSR1
usb_service_cdc
	btfsc	FSR1H,ENDP1	; ignore endpoint 2
	 return
	btfss	FSR1H,DIR	; if endpoint 1 IN, host had read the status; check for reset
	 bra	_no_in
	btfsc	USB_STATE,PENDING_RESET
	 btfsc	E1I_STA,UOWN	; Don't reset before any response is really read by host
	 return
	reset			; reset controller and jump to application
_no_in
	movfw	E1O_CNT		; sets Z!
	btfss	USB_STATE,POLL_FROM_APP
	 bra	_no_app
	call	0x202		; call application code for handling E1O_BUF = RxBuf data
; The application code should then:
; * inspect W = length and E1O_BUF = RxBuf
; * act as commanded by W and buffer content
; * optionally: put data into E1I_BUF, call usbTx with W=length, as often as needed
; * return
	pagesel	0		; restore PCLATH
	bra	_arm_e1o
_no_app
	brz	_arm_e1o	; (ignore and rearm OUT buffer)
	bcf	USB_STATE,READFLASH
	call	bootloader_exec_cmd	; execute command; status returned in W
	btfss	USB_STATE,READFLASH
	 bra	_one		; no
	movwf	FSR1H		; 32 = loop count (PMADR is already set up)
	;movlw	high LIN_E1I_BUF;=32
	movwf	FSR0H
	movlwf	low LIN_E1I_BUF,FSR0L	;=0x60
	banksel	PMCON1
_rloop	bsf	PMCON1,RD	; Capture one flash word
	nop
	nop
	movfw	PMDATL		; read LoByte
	movwi	FSR0++		; save it
	movfw	PMDATH		; read HiByte
	movwi	FSR0++		; save it
	incf	PMADRL,f	; don't straddle lo-hi boundaries
	decfsz	FSR1H
	 bra	_rloop
	movlw	64
	call	usbTx
	clrw
	bra	_full		; send zero-length packet to terminate transfer
_one
	banksel	E1I_BUF		; Bank 1
	movwf	E1I_BUF		; put status into IN buffer
	movlw	1		; output byte count is 1
_full	call	usbTx
_arm_e1o
	banksel	E1O_STA		; bank 0
	comf	E1O_STA,w	; data toggle
	andlw	1<<DTS
	iorlw	1<<DTSEN	; 0↕001000, ↕ = toggle
arm_e1o_w
	movwf	E1O_STA
	movlwf	1<<E1O_SSH,E1O_CNT	; length = full size
	bsf	E1O_STA,UOWN	; rearm OUT buffer
	retlw	1<<DTS|1<<DTSEN	; 01001000

; Executes a bootloader command.
; args:		command payload in EP1 OUT buffer
;		BSR=0
; returns:	status code in W
; clobbers:	BSR,FSR0,FSR1,E1O_CNT
bootloader_exec_cmd
	movfw	E1O_BUF+1	; address upper bits 
	movwf	FSR1H
	movfw	E1O_BUF+0	; address lower bits (in W and FSR1L)
	movwf	FSR1L
; check length of data packet
	decfsz	E1O_CNT,f
	 bra	_no_reset
; len==1: Resets the device if the received byte matches the reset character.
	xorlw	'R'		; check received character
	skpz
	 retlw	BSTAT_INVALID_COMMAND
; command is valid, process IN request first, then reset this microcontroller
	bsf	USB_STATE,PENDING_RESET	;enqueue to do a reset instruction
	retlw	BSTAT_OK
_no_reset
	decfsz	E1O_CNT,f
	 bra	_no_read_port
; len==2: Read byte
	movfw	INDF1
	return
_no_read_port
	decfsz	E1O_CNT,f
	 bra	_no_write_port
; len==3: Write byte
	movfw	E1O_BUF+2
	movwf	INDF1
	retlw	BSTAT_OK
_no_write_port	 
	decfsz	E1O_CNT,f
	 bra	_no_set_params
; len==4: Set write/read address, expected checksum of the next 32 words,
; and erases the row at that address if the last byte of the command matches "E"
	banksel	PMCON1		;bank 3
	clrf	PMCON1
	movfw	FSR1H		;copy address to PMADR
	movwf	PMADRH
	btfsc	FSR1H,7
	 bsf	PMCON1,CFGS	;select configuration space for given word address >= 32K
	movfw	FSR1L
	movwf	PMADRL
	banksel	E1O_BUF		;bank 0
	andlw	0x1F		;lower 5 bits must be 0 for programming and readout
	skpz
	 retlw	BSTAT_INVALID_COMMAND
	movfw	E1O_BUF+2	;expected checksum
	movwf	CHECKSUM	;save for verification during write command
	movfw	E1O_BUF+3	;command flag
	xorlw	'R'		;read?
	brnz	_no_r
	bsf	USB_STATE,READFLASH
	retlw	32
_no_r
; do we need to erase?
	xorlw	'R'^'E'
	skpz
	 retlw	BSTAT_OK	; if no reset command is given, return OK
; Erases the row of flash in PMADRH:PMADRL.
	banksel	PMCON1		;bank 3
	bsf	PMCON1,FREE
	bsf	PMCON1,WREN	; enable write and erase to program memory
	call	flash_unlock	; stalls until erase finishes
	;bcf	PMCON1,WREN	; clear write enable flag
	retlw	BSTAT_OK
_no_set_params
	movlw	64-4
	xorwf	E1O_CNT,f
	skpz
	 retlw	BSTAT_INVALID_COMMAND
; Verifies that the checksum of the 32 words (64 bytes) in the EP1 OUT buffer
; matches the previously sent value. If so, the 32 bytes are then written to
; flash memory at the address in PMADRH:PMADRL. (set by a prior command)
; The expected checksum is the two's complement of the sum of the bytes.
; If the data is valid, we can add the checksum to the sum of the bytes and
; the result will be 0. We initialize a temporary register with the expected
; checksum, and then add each byte to it as it's processed.
; If the value in the temp register is 0 after all 64 bytes have been copied
; to the write latches, proceed with the write.
	movlwf	low LIN_E1O_BUF,FSR0L	; set up read pointer
	;movlw	high LIN_E1O_BUF	0x2020: High == Low
	movwf	FSR0H
	movfwf	CHECKSUM,FSR1L	; keep checksum for next try and make it addressable
	banksel	PMCON1
	bsf	PMCON1,LWLO	; write to latches only
	bsf	PMCON1,WREN
; simultaneously compute the checksum of the 32 words and copy them to the
; write latches
	movlwf	32,FSR1H	; number of words to write, use for loop count
_wloop	moviw	FSR0++		; load lower byte
	addwf	FSR1L		; add lower byte to checksum
	movwf	PMDATL		; copy to write latch
	moviw	FSR0++		; load upper byte
	addwf	FSR1L		; add upper byte to checksum
	movwf	PMDATH		; copy to write latch
; after writing the last word to PMDATH:PMDATL,
; don't execute the unlock sequence or advance the address pointer!
	decf	FSR1H		; decrement loop count
	brz	_wcksum		; if 0, we're done writing to the latches
; still have more words to go
	call	flash_unlock	; execute unlock sequence
	incf	PMADRL,f	; increment write address
	bra	_wloop
; verify the checksum
_wcksum	bcf	PMCON1,LWLO	; flash entire page while writing last word
	tstf	FSR1L
	skpz
	 retlw	BSTAT_INVALID_CHECKSUM	; if there's a mismatch, abort the write
; checksum is valid, write the data
	;bsf	PMCON1,WREN	;still set
	call	flash_unlock		; stalls until write finishes
; verify the write: compare each byte in the buffer to its counterpart that
; was just written to flash.
; we do this backwards so we don't waste instructions resetting the pointers.
; (note: PMADRH:PMADRL is already pointing at the last written word, but FSR0
; is pointing to one byte past the end of the buffer)
	;bcf	PMCON1,WREN	; clear write enable
	bsf	FSR1H,5		; set loop count to 32 (just need to set one bit because it's already 0)
_vloop	bsf	PMCON1,RD	; read word from flash
	nop			; 2 required nops
	nop
	moviw	--FSR0		; get high byte of expected word
	subwf	PMDATH,w	; compare with high byte written to flash
	skpz
	 retlw	BSTAT_VERIFY_FAILED
	moviw	--FSR0		; get low byte of expected word
	subwf	PMDATL,w	; compare with low byte written to flash
	skpz
	 retlw	BSTAT_VERIFY_FAILED
	decf	PMADRL,f	; decrement read address
	decfsz	FSR1H,f		; decrement loop count
	 bra	_vloop
	retlw	BSTAT_OK

; Executes the flash unlock sequence, performing an erase or write.
; args:		PMCON1 bits CFGS, LWLO, FREE and WREN set appropriately
	assume	PMCON2	;BSR=3
; clobbers:	W
flash_unlock
	movlwf	0x55,PMCON2
	movlwf	0xAA,PMCON2
	bsf	PMCON1,WR
	nop
	nop
ret	return

onReset
; Check for valid application code: the lower 8 bits of the first word cannot be 0xFF
	clrf	FSR0L
	movlwf	0x82,FSR0H	; set high bit to read program memory 0x200
	banksel	PORTA		; Bank 0
	comf	INDF0,w		; ",f" should not modify flash too
	brz	_stay_in_boot	; if we have no application, enter bootloader mode
; We have a valid application. Check state of RA3
	btfss	PORTA,3		; If RA3 is low, stay in boot looder
	 bra	_stay_in_boot
; Start the application with BSR=0

	pagesel	0x200
	goto	0x200

; Not entering application code: initialize the USB interface and wait for commands.
_stay_in_boot
	banksel	OPTION_REG	; Bank 1
	bcf	OPTION_REG,NOT_WPUEN	; Enable weak pull-ups
	call	usbInit
; Idle loop. In bootloader mode, the MCU just spins here. No interrupts.
_loop	call	usbPoll1
	bra	_loop

usbInit
; Configure the oscillator (48MHz from INTOSC using 3x PLL)
	banksel	OSCCON		; bank 1
	movlwf	0xFC,OSCCON
; Wait for the oscillator and PLL to stabilize
_wosc	comf	OSCSTAT,w
	andlw	(1<<PLLRDY)|(1<<HFIOFR)|(1<<HFIOFS)
	brnz	_wosc
; Enable active clock tuning
	banksel	ACTCON		; Bank 7
	movlwf	(1<<ACTSRC)|(1<<ACTEN),ACTCON		; source = USB
; Initialize USB
	call	usbReset
; Attach to the bus (could be a subroutine, but inlining it saves 2 instructions)
	banksel	UCON		; Bank 29
	clrf	UCON		; reset UCON
	bsf	UCON,USBEN	; enable USB module and wait until ready
	return

; Initializes the USB system and resets all associated registers.
; clobbers:	W,BSR=0,FSR0,FSR1L
usbReset
; clear USB registers
	banksel	UIR		; Bank 29
	clrf	UIR
; set configuration
	movlwf	(1<<UPUEN)|(1<<FSEN),UCFG	; enable pullups, full speed, no ping-pong buffering
	;movlwf	(1<<TRNIE)|(1<<URSTIE),UIE	; TODO: Need interrupts for CPU wakeup later
; clear all BDT entries, variables, and buffers
	clrf	FSR0H
	movlwf	0x20,FSR0L	; BDT starts at 0x2000 aka 0x20, clear all Bank0 variables except RxBuf
	movwf	FSR1L		; use 0x20 as loop count
	clrw
_ramclr	movwi	FSR0++
	decfsz	FSR1L,f
	 bra	_ramclr
; reset ping-pong buffers and address
	banksel	UCON		; Bank 29: All about USB
	bsf	UCON,PPBRST
	clrf	UADDR
	bcf	UCON,PKTDIS	; enable packet processing
	bcf	UCON,PPBRST	; clear ping-pong buffer reset flag
; flush pending transactions
_tflush	btfss	UIR,TRNIF
	 bra	_initep
	bcf	UIR,TRNIF
	call	ret		; need at least 6 cycles before checking TRNIF again
	bra	_tflush

; initialize endpoints:
; 0 for control
; 1 for CDC bulk data
; 2 for CDC notifications (dummy IN endpoint, returns NAK, neither STALL nor silence)
; my intuition was that I should wait until a SET_CONFIGURATION is received
; before setting up endpoints 1 and 2... but there seemed to be a timing issue
; when doing so, so I moved them here
_initep	movlwf	1<<EPHSHK|1<<EPOUTEN|1<<EPINEN,UEP0
	movlwf	1<<EPHSHK|1<<EPCONDIS|1<<EPOUTEN|1<<EPINEN,UEP1
	movlwf	1<<EPHSHK|1<<EPCONDIS|1<<EPINEN,UEP2
; initialize endpoint buffers and counts
	banksel	E0O_ADR
	movlwf	low LIN_E0_BUF,E0O_ADR	; set endpoint 0 address low
	movwf	E0I_ADR
	movlwf	low LIN_E1O_BUF,E1O_ADR	; set endpoint 1 OUT address low
	movlwf	low LIN_E1I_BUF,E1I_ADR	; set endpoint 1 IN address low
	movlwf	high LIN_E0_BUF,E0O_ADR+1	; set all ADRH values to 0x20
	movwf	E0I_ADR+1
	movwf	E1O_ADR+1
	movwf	E1I_ADR+1
	goto	arm_ep0_out_stall

; Descriptors 
dtw macro x
	dt	low x,high x
 endm

DEVICE_DESCRIPTOR
	dt	18		; bLength
	dt	1		; bDescriptorType
	dtw	0x0200		; bcdUSB (USB 2.0)
	dt	2		; bDeviceClass (communication device)
	dt	0		; bDeviceSubclass
	dt	0		; bDeviceProtocol
	dt	1<<E0_SSH	; bMaxPacketSize0 (8 bytes)
	dtw	0x04D8		; idVendor
	dtw	0xEFDA		; idProduct
	dtw	1		; bcdDevice (1)
	dt	0		; iManufacturer
	dt	0		; iProduct
	dt	0		; iSerialNumber
	dt	1		; bNumConfigurations

CONFIGURATION_DESCRIPTOR
	dt	9		; bLength
	dt	2		; bDescriptorType
	dtw	CONFIG_DESC_LEN	; wTotalLength
	dt	2		; bNumInterfaces
	dt	1		; bConfigurationValue
	dt	0		; iConfiguration
	dt	0x80		; bmAttributes
	dt	50		; bMaxPower
;INTERFACE_DESCRIPTOR_0
	dt	9		; bLength
	dt	4		; bDescriptorType (INTERFACE)
	dt	0		; bInterfaceNumber
CONFIGURATION_01_CONSTANT	; Two adjanced addresses containing [0,1]
	dt	0		; bAlternateSetting
	dt	1		; bNumEndpoints
	dt	2		; bInterfaceClass (communication)
	dt	2		; bInterfaceSubclass (abstract control model)
	dt	1		; bInterfaceProtocol (V.25ter, common AT commands)
	dt	0		; iInterface
;HEADER_FUNCTIONAL_DESCRIPTOR
	dt	5		; bFunctionLength
	dt	0x24		; bDescriptorType (CS_INTERFACE)
	dt	0		; bDescriptorSubtype (header functional descriptor)
	dtw	0x0110		; bcdCDC (specification version, 1.1)
;ABSTRACT_CONTROL_MANAGEMENT_FUNCTIONAL_DESCRIPTOR
	dt	4		; bFunctionLength
	dt	0x24		; bDescriptorType (CS_INTERFACE)
	dt	2		; bDescriptorSubtype (abstract control management functional descriptor)
	dt	0x02		; bmCapabilities
;UNION_FUNCTIONAL_DESCRIPTOR
	dt	5		; bFunctionLength
	dt	0x24		; bDescriptorType (CS_INTERFACE)
	dt	6		; bDescriptorSubtype (union functional descriptor)
	dt	0		; bMasterInterface
	dt	1		; bSlaveInterface0
;CALL_MANAGEMENT_FUNCTIONAL_DESCRIPTOR
	dt	5		; bFunctionLength
	dt	0x24		; bDescriptorType (CS_INTERFACE)
	dt	1		; bDescriptorSubtype (call management functional descriptor)
	dt	3		; bmCapabilities
	dt	1		; dDataInterface
;ENDPOINT_DESCRIPTOR_2_IN
	dt	7		; bLength
	dt	5		; bDescriptorType (ENDPOINT)
	dt	0x82		; bEndpointAddress (2 IN)
	dt	0x03		; bmAttributes (transfer type: interrupt)
	dtw	8		; wMaxPacketSize (8)
	dt	2		; bInterval
;INTERFACE_DESCRIPTOR_1
	dt	9		; bLength
	dt	4		; bDescriptorType (INTERFACE)
	dt	1		; bInterfaceNumber
	dt	0		; bAlternateSetting
	dt	2		; bNumEndpoints
	dt	0x0A		; bInterfaceClass (data)
	dt	0		; bInterfaceSubclass
	dt	0		; bInterfaceProtocol
	dt	0		; iInterface
;ENDPOINT_DESCRIPTOR_1_OUT
	dt	7		; bLength
	dt	5		; bDescriptorType (ENDPOINT)
	dt	0x01		; bEndpointAddress (1 OUT)
	dt	0x02		; bmAttributes (transfer type: bulk)
	dtw	1<<E1O_SSH	; wMaxPacketSize (64)
	dt	0		; bInterval
;ENDPOINT_DESCRIPTOR_1_IN
	dt	7		; bLength
	dt	5		; bDescriptorType (ENDPOINT)
	dt	0x81		; bEndpointAddress (1 IN)
	dt	0x02		; bmAttributes (transfer type: bulk)
	dtw	1<<E1I_SSH	; wMaxPacketSize (64)
	dt	0		; bInterval
serialStateNotification2
	dt	0xA1,0x20,0,0,0,0,2,0
serialStateNotification
	dt	3,0
LINE_CODING
	dtw	9600
	dt	0,0,0,0,8

	if $>0x200
	 error "Too much code"
	endif

	end
Detected encoding: UTF-80