; minimal tftp implementation (client only) ; supports file upload and download TFTP_MAX_RESENDS = 10 TFTP_TIMER_MASK = $F8 ; mask lower two bits, means we wait for 8 x1/4 seconds .include "zeropage.inc" .include "../inc/common.inc" .include "../inc/error.inc" .export tftp_load_address .export tftp_ip .export tftp_download .export tftp_upload .export tftp_data_block_length .export tftp_current_memloc .export tftp_set_callback_vector .export tftp_callback_vector .export tftp_clear_callbacks .export tftp_filename .export tftp_filesize .export tftp_upload_from_memory .import ip65_process .import ip65_error .import udp_add_listener .import udp_remove_listener .import output_buffer .import udp_callback .import udp_send .import check_for_abort_key .import udp_inp .import ip_inp .importzp ip_src .importzp udp_src_port .importzp udp_data .import udp_send_dest .import udp_send_src_port .import udp_send_dest_port .import udp_send_len .import copymem .importzp copy_src .importzp copy_dest .import timer_read .bss ; packet offsets tftp_inp = udp_inp + udp_data tftp_outp = output_buffer ; everything after filename in a request at a relative address, not fixed, so don't bother defining offset constants tftp_server_port = 69 tftp_client_port_low_byte: .res 1 tftp_load_address: .res 2 ; address file will be (or was) downloaded to tftp_ip: .res 4 ; ip address of tftp server - set to 255.255.255.255 (broadcast) to send request to all tftp servers on local lan tftp_data_block_length: .res 2 tftp_send_len: .res 2 tftp_current_memloc: .res 2 ; tftp state machine tftp_initializing = 1 ; initial state tftp_initial_request_sent = 2 ; sent the RRQ or WRQ, waiting for some data tftp_transmission_in_progress = 3 ; we have sent/received the first packet of file data tftp_complete = 4 ; we have sent/received the final packet of file data tftp_error = 5 ; we got an error tftp_state: .res 1 ; current activity tftp_timer: .res 1 tftp_resend_counter: .res 1 tftp_break_inner_loop: .res 1 tftp_current_block_number: .res 2 tftp_actual_server_port: .res 2 ; this is read from the reply - it is not (usually) the port # we send the RRQ or WRQ to tftp_actual_server_ip: .res 4 ; this is read from the reply - it may not be the IP we sent to (e.g. if we send to broadcast) tftp_just_set_new_load_address: .res 1 tftp_opcode: .res 2 ; will be set to 4 if we are doing a RRQ, or 7 if we are doing a DIR tftp_filename: .res 2 ; pointer to null terminated name of file to upload/download tftp_filesize: .res 2 ; will be set by tftp_download, needs to be set before calling tftp_upload_from_memory tftp_bytes_remaining: .res 2 .code ; uploads a file to a tftp server with data retrieved from specified memory location ; inputs: ; tftp_ip: ip address of host to send file to (set to 255.255.255.255 for broadcast) ; tftp_filename: pointer to null terminated name of file to upload ; tftp_load_address: starting address of data to be sent ; tftp_filesize: length of data to send ; outputs: carry flag is set if there was an error ; if a callback vector has been set with tftp_set_callback_vector ; then the specified routine will be called once for each 512 byte packet ; to be sent to the tftp server tftp_upload_from_memory: ldax #copy_ram_to_tftp_block jsr tftp_set_callback_vector ldax tftp_filesize stax tftp_bytes_remaining lda #00 sta tftp_filesize sta tftp_filesize+1 ; uploads a file to a tftp server with data retrieved from user supplied routine ; inputs: ; tftp_ip: ip address of host to send file to (set to 255.255.255.255 for broadcast) ; tftp_filename: pointer to null terminated name of file to upload ; a callback vector should have been set with tftp_set_callback_vector ; outputs: carry flag is set if there was an error ; the specified routine will be called once for each 512 byte packet ; to be sent from the tftp server. tftp_upload: ldax #$0200 ; opcode 02 = WRQ jmp set_tftp_opcode ; download a file from a tftp server ; inputs: ; tftp_ip: ip address of host to download from (set to 255.255.255.255 for broadcast) ; tftp_filename: pointer to null terminated name of file to download ; tftp_load_address: memory location that data will be stored in, or $0000 to ; treat first 2 bytes received from tftp server as memory address that rest ; of file should be loaded into (e.g. if downloading a C64 'prg' file) ; outputs: carry flag is set if there was an error ; if a callback vector has been set with tftp_set_callback_vector ; then the specified routine will be called once for each 512 byte packet ; sent from the tftp server (each time AX will point at data block just arrived, ; and tftp_data_block_length will contain number of bytes in that data block) ; otherwise, the buffer at tftp_load_address will be filled ; with file downloaded. ; tftp_load_address: will be set to the actual address loaded into (NB - this field is ; ignored if a callback vector has been set with tftp_set_callback_vector) tftp_download: lda #00 sta tftp_filesize sta tftp_filesize+1 ldx #$01 ; opcode 01 = RRQ (A should already be zero from having just reset file length) set_tftp_opcode: stax tftp_opcode lda #tftp_initializing sta tftp_state ldax #0000 stax tftp_current_block_number ldax tftp_load_address stax tftp_current_memloc ldax #tftp_in stax udp_callback ldx #$69 inc tftp_client_port_low_byte ; each transfer uses a different client port lda tftp_client_port_low_byte ; so we don't get confused by late replies to a previous call jsr udp_add_listener bcc :+ ; bail if we couldn't listen on the port we want lda #IP65_ERROR_PORT_IN_USE sta ip65_error rts : lda #TFTP_MAX_RESENDS sta tftp_resend_counter @outer_delay_loop: jsr timer_read txa and #TFTP_TIMER_MASK sta tftp_timer ; we only care about the high byte lda #0 sta tftp_break_inner_loop lda tftp_state cmp #tftp_initializing bne @not_initializing jsr send_request_packet jmp @inner_delay_loop @not_initializing: cmp #tftp_error bne @not_error @exit_with_error: ldx #$69 lda tftp_client_port_low_byte jsr udp_remove_listener sec rts @not_error: cmp #tftp_complete bne @not_complete jsr send_ack ; send the ack for the last block bcs @not_complete ; if we couldn't send the ACK (e.g. coz we need to do an ARP request) then keep looping ldx #$69 lda tftp_client_port_low_byte jsr udp_remove_listener rts @not_complete: cmp #tftp_transmission_in_progress bne @not_transmitting jsr send_tftp_packet jmp @inner_delay_loop @not_transmitting: jsr send_request_packet @inner_delay_loop: jsr ip65_process jsr check_for_abort_key bcc @no_abort lda #IP65_ERROR_ABORTED_BY_USER sta ip65_error jmp @exit_with_error @no_abort: lda tftp_break_inner_loop bne @outer_delay_loop jsr timer_read txa and #TFTP_TIMER_MASK cmp tftp_timer beq @inner_delay_loop dec tftp_resend_counter bne @outer_delay_loop lda #IP65_ERROR_TIMEOUT_ON_RECEIVE sta ip65_error jmp @exit_with_error send_request_packet: lda #tftp_initializing sta tftp_state ldax tftp_opcode stax tftp_outp ldax tftp_filename stax ptr1 ldx #$01 ; we inc x/y at start of loop, so ldy #$ff ; set them to be 1 below where we want the copy to begin @copy_filename_loop: inx iny bmi @error_in_send ; if we get to 0x80 bytes, we've gone too far lda (ptr1),y sta tftp_outp,x bne @copy_filename_loop ldy #$ff @copy_mode_loop: inx iny lda tftp_octet_mode,y sta tftp_outp,x bne @copy_mode_loop inx txa ldx #0 stax udp_send_len ldx #$69 lda tftp_client_port_low_byte stax udp_send_src_port ldx #3 ; set destination address : lda tftp_ip,x sta udp_send_dest,x dex bpl :- ldax #tftp_server_port ; set destination port stax udp_send_dest_port ldax #tftp_outp jsr udp_send bcs @error_in_send lda #tftp_initial_request_sent sta tftp_state rts @error_in_send: lda #IP65_ERROR_TRANSMIT_FAILED sta ip65_error sec rts send_ack: ldax #$0400 ; opcode 04 = ACK stax tftp_outp ldax #04 stax tftp_send_len send_tftp_packet: ; TFTP block should be created in tftp_outp, we just add the UDP&IP stuff and send ldx tftp_current_block_number lda tftp_current_block_number+1 stax tftp_outp+2 ldx #$69 lda tftp_client_port_low_byte stax udp_send_src_port lda tftp_actual_server_ip sta udp_send_dest lda tftp_actual_server_ip+1 sta udp_send_dest+1 lda tftp_actual_server_ip+2 sta udp_send_dest+2 lda tftp_actual_server_ip+3 sta udp_send_dest+3 ldx tftp_actual_server_port lda tftp_actual_server_port+1 stax udp_send_dest_port ldax tftp_send_len stax udp_send_len ldax #tftp_outp jsr udp_send rts got_expected_block: lda tftp_current_block_number inc tftp_current_block_number bne :+ inc tftp_current_block_number+1 : lda #tftp_transmission_in_progress sta tftp_state lda #TFTP_MAX_RESENDS sta tftp_resend_counter lda #1 sta tftp_break_inner_loop ldax udp_inp+udp_src_port stax tftp_actual_server_port ldax ip_inp+ip_src stax tftp_actual_server_ip ldax ip_inp+ip_src+2 stax tftp_actual_server_ip+2 rts tftp_in: lda tftp_inp+1 ; get the opcode cmp #5 bne @not_an_error @recv_error: lda #tftp_error sta tftp_state lda #IP65_ERROR_TRANSMISSION_REJECTED_BY_PEER sta ip65_error rts @not_an_error: cmp #3 beq :+ jmp @not_data_block : lda #0 sta tftp_just_set_new_load_address ; clear the flag clc lda tftp_load_address adc tftp_load_address+1 ; is load address currently $0000? bne @dont_set_load_address lda tftp_callback_address_set ; have we overridden the default handler? bne @dont_set_load_address ; if so, don't skip the first two bytes in the file ldax udp_inp+$0c ; get first two bytes of data stax tftp_load_address ; make them the new load adress stax tftp_current_memloc ; also the current memory destination lda #1 ; set the flag sta tftp_just_set_new_load_address @dont_set_load_address: ldx tftp_inp+3 ; get the (low byte) of the data block dex cpx tftp_current_block_number beq :+ jmp @not_expected_block_number : ; this is the block we wanted jsr got_expected_block lda tftp_just_set_new_load_address bne @skip_first_2_bytes_in_calculating_header_length lda udp_inp+5 ; get the low byte of udp packet length sec sbc #$0c ; take off the length of the UDP header+OPCODE + BLOCK jmp @adjusted_header_length @skip_first_2_bytes_in_calculating_header_length: lda udp_inp+5 ; get the low byte of udp packet length sec sbc #$0e ; take off the length of the UDP header+OPCODE + BLOCK + first 2 bytes (memory location) @adjusted_header_length: sta tftp_data_block_length lda udp_inp+4 ; get high byte of the length of the UDP packet sbc #0 sta tftp_data_block_length+1 lda tftp_just_set_new_load_address bne @skip_first_2_bytes_in_calculating_copy_src ldax #udp_inp+$0c jmp @got_pointer_to_tftp_data @skip_first_2_bytes_in_calculating_copy_src: ldax #udp_inp+$0e @got_pointer_to_tftp_data: stax copy_src ldax #output_buffer+2 stax copy_dest ldax tftp_data_block_length stax output_buffer jsr copymem ldax #output_buffer jsr tftp_callback_vector jsr send_ack lda udp_inp+4 ; check the length of the UDP packet cmp #02 bne @last_block lda udp_inp+5 cmp #$0c bne @last_block beq @not_last_block @not_data_block: cmp #4 ; ACK is opcode 4 beq :+ jmp @not_ack : ; it's an ACK, so we must be sending a file ldx tftp_inp+3 ; get the (low byte) of the data block cpx tftp_current_block_number beq :+ jmp @not_expected_block_number : ; the last block we sent was acked so now we need to send the next one ldax #output_buffer+4 jsr tftp_callback_vector ; this (caller supplied) routine should fill the buffer with up to 512 bytes stax tftp_data_block_length clc adc #4 bcc :+ inx : stax tftp_send_len ldax #$0300 ; opcode 03 = DATA stax tftp_outp jsr got_expected_block jsr send_tftp_packet lda tftp_data_block_length+1 ; get length of data we just sent (high byte) cmp #2 bne @last_block @not_last_block: inc tftp_filesize+1 ; add $200 to file size inc tftp_filesize+1 ; add $200 to file size @not_ack: @not_expected_block_number: rts @last_block: lda tftp_data_block_length sta tftp_filesize ; this must be the first block that is not a multiple of 512, hence till now the low byte in tftp_filesize is still $00 lda tftp_data_block_length+1 ; this can only be 0 or 1 beq :+ inc tftp_filesize+1 : lda #tftp_complete sta tftp_state rts ; default handler when block arrives: ; copy to RAM ; assumes tftp_data_block_length has been set, and AX should point to start of data copy_tftp_block_to_ram: clc adc #02 ; skip the 2 byte length at start of buffer bcc :+ inx : stax copy_src ldax tftp_current_memloc stax copy_dest ldax tftp_data_block_length jsr copymem clc lda tftp_data_block_length ; update the location where the next data will go adc tftp_current_memloc sta tftp_current_memloc lda tftp_data_block_length+1 adc tftp_current_memloc+1 sta tftp_current_memloc+1 rts ; default handler for uploading a file copy_ram_to_tftp_block: stax copy_dest ldax tftp_current_memloc stax copy_src clc lda tftp_bytes_remaining+1 beq @last_block cmp #01 beq @last_block dec tftp_bytes_remaining+1 dec tftp_bytes_remaining+1 ldax #$0200 @length_is_set: stax tftp_data_block_length jsr copymem inc tftp_current_memloc+1 inc tftp_current_memloc+1 ldax tftp_data_block_length clc rts @last_block: ldax tftp_bytes_remaining jmp @length_is_set ; set up vector of routine to be called when each 512 packet arrives from tftp server ; when downloading OR for routine to be called when ready to send new block ; when uploading. ; when vector is called when downloading, AX will point to data that was downloaded, ; tftp_data_block_length will be set to length of downloaded data block. This will be ; equal to $200 (512) for each block EXCEPT the final block. THe final block will ; always be less than $200 bytes - if the file is an exact multiple if $200 bytes ; long, then a final block will be received with length $00. ; when vector is called when uploading, AX will point to a 512 byte buffer that ; should be filled with the next block. the user supplied routine should set AX ; to be equal to the actual number of bytes inserted into the buffer, which should ; equal to $200 (512) for each block EXCEPT the final block. The final block must ; always be less than $200 bytes - if the file is an exact multiple if $200 bytes ; long, then a final block must be created with length $00. ; inputs: ; AX - address of routine to call for each packet. ; outputs: none tftp_set_callback_vector: stax tftp_callback_vector+1 lda #1 sta tftp_callback_address_set rts ; clear callback vectors, i.e. all future transfers read from/write to RAM ; inputs: none ; outputs: none tftp_clear_callbacks: lda #0 sta tftp_callback_address_set ldax #copy_tftp_block_to_ram jmp tftp_set_callback_vector .rodata tftp_octet_mode: .asciiz "OCTET" .data tftp_callback_vector: jmp copy_tftp_block_to_ram ; vector for action to take when a data block received (default is to store block in RAM) tftp_callback_address_set: .byte 0 ; -- LICENSE FOR tftp.s -- ; The contents of this file are subject to the Mozilla Public License ; Version 1.1 (the "License"); you may not use this file except in ; compliance with the License. You may obtain a copy of the License at ; http://www.mozilla.org/MPL/ ; ; Software distributed under the License is distributed on an "AS IS" ; basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the ; License for the specific language governing rights and limitations ; under the License. ; ; The Original Code is ip65. ; ; The Initial Developer of the Original Code is Jonno Downes, ; jonno@jamtronix.com. ; Portions created by the Initial Developer are Copyright (C) 2009 ; Jonno Downes. All Rights Reserved. ; -- LICENSE END --