From fbb40c7c98896a500f1b8cf3573cf8bfa84cb072 Mon Sep 17 00:00:00 2001 From: jonnosan Date: Fri, 21 Aug 2009 13:21:14 +0000 Subject: [PATCH] git-svn-id: http://svn.code.sf.net/p/netboot65/code@185 93682198-c243-4bdb-bd91-e943c89aac3b --- client/examples/make_sine_data.rb | 4 +- client/examples/sine_data.i | 32 ++-- client/examples/tune.bin | Bin 0 -> 2321 bytes client/examples/upnatom.s | 54 ++++--- client/inc/commonprint.i | 121 +++++++------- client/inc/nb65_constants.i | 21 ++- client/inc/ping.i | 66 ++++++++ client/inc/version.i | 2 +- client/ip65/Makefile | 13 +- client/ip65/function_dispatcher.s | 21 +++ client/ip65/icmp.s | 231 +++++++++++++++++++++++++-- client/ip65/url.s | 4 +- client/nb65/Makefile | 3 +- client/nb65/nb65_c64.s | 61 ++++--- client/test/Makefile | 8 +- client/test/test_disk_io.s | 3 - client/test/test_ping.s | 75 +++++++++ dist/make_error_codes.rb | 11 ++ dist/version_number.txt | 2 +- doc/README.C64.html | 24 ++- doc/nb65_api_technical_reference.doc | Bin 146432 -> 151552 bytes 21 files changed, 601 insertions(+), 155 deletions(-) create mode 100644 client/examples/tune.bin create mode 100644 client/inc/ping.i create mode 100644 client/test/test_ping.s create mode 100644 dist/make_error_codes.rb diff --git a/client/examples/make_sine_data.rb b/client/examples/make_sine_data.rb index 467381f..3caa125 100644 --- a/client/examples/make_sine_data.rb +++ b/client/examples/make_sine_data.rb @@ -1,8 +1,8 @@ f=File.open("sine_data.i","w") TABLE_ENTRIES=0x80 -AMPLITUDE=205 -OFFSET=50 +AMPLITUDE=255 +OFFSET=00 TABLE_ENTRIES.times do |i| value=OFFSET+Math.sin(Math::PI*i.to_f/TABLE_ENTRIES.to_f)*AMPLITUDE diff --git a/client/examples/sine_data.i b/client/examples/sine_data.i index 3c7933f..9bd4215 100644 --- a/client/examples/sine_data.i +++ b/client/examples/sine_data.i @@ -1,17 +1,17 @@ -.byte $32, $37, $3c, $41, $46, $4b, $50, $55 -.byte $59, $5e, $63, $68, $6d, $72, $77, $7b -.byte $80, $85, $89, $8e, $92, $97, $9b, $9f -.byte $a3, $a8, $ac, $b0, $b4, $b7, $bb, $bf -.byte $c2, $c6, $c9, $cd, $d0, $d3, $d6, $d9 -.byte $dc, $df, $e1, $e4, $e6, $e9, $eb, $ed -.byte $ef, $f1, $f3, $f4, $f6, $f7, $f8, $fa -.byte $fb, $fb, $fc, $fd, $fe, $fe, $fe, $fe -.byte $ff, $fe, $fe, $fe, $fe, $fd, $fc, $fb -.byte $fb, $fa, $f8, $f7, $f6, $f4, $f3, $f1 -.byte $ef, $ed, $eb, $e9, $e6, $e4, $e1, $df -.byte $dc, $d9, $d6, $d3, $d0, $cd, $c9, $c6 -.byte $c2, $bf, $bb, $b7, $b4, $b0, $ac, $a8 -.byte $a3, $9f, $9b, $97, $92, $8e, $89, $85 -.byte $80, $7b, $77, $72, $6d, $68, $63, $5e -.byte $59, $55, $50, $4b, $46, $41, $3c, $37 \ No newline at end of file +.byte $00, $06, $0c, $12, $18, $1f, $25, $2b +.byte $31, $37, $3d, $44, $4a, $4f, $55, $5b +.byte $61, $67, $6d, $72, $78, $7d, $83, $88 +.byte $8d, $92, $97, $9c, $a1, $a6, $ab, $af +.byte $b4, $b8, $bc, $c1, $c5, $c9, $cc, $d0 +.byte $d4, $d7, $da, $dd, $e0, $e3, $e6, $e9 +.byte $eb, $ed, $f0, $f2, $f4, $f5, $f7, $f8 +.byte $fa, $fb, $fc, $fd, $fd, $fe, $fe, $fe +.byte $ff, $fe, $fe, $fe, $fd, $fd, $fc, $fb +.byte $fa, $f8, $f7, $f5, $f4, $f2, $f0, $ed +.byte $eb, $e9, $e6, $e3, $e0, $dd, $da, $d7 +.byte $d4, $d0, $cc, $c9, $c5, $c1, $bc, $b8 +.byte $b4, $af, $ab, $a6, $a1, $9c, $97, $92 +.byte $8d, $88, $83, $7d, $78, $72, $6d, $67 +.byte $61, $5b, $55, $4f, $4a, $44, $3d, $37 +.byte $31, $2b, $25, $1f, $18, $12, $0c, $06 \ No newline at end of file diff --git a/client/examples/tune.bin b/client/examples/tune.bin new file mode 100644 index 0000000000000000000000000000000000000000..3bdabffaf3a2dcaa5daec1cb02f01071837e7b3d GIT binary patch literal 2321 zcmb7EeQaA-6~FKKy=(hAAJ4wejhAN38q?Y?b!RhMOYD&~4TQF;HmxAie5~?N434Ab z!%0zErOavPRcx~4in6(#-xxtfae?9r{@_830$QtNE!2SjfT$axYNmA(#s~@CsGY~T z_L7FFLgL)}&i$Qp?)lw&&wcM9=|Gn>i!#XFLC6s-&8e;Sgu|-tv?p4uuO#+LiKfD= zUFo6kXOgIY8*Vz)F8rFJgesF%As=Pg3 zR=3#WmH9*Ta(O?jm~<^(E-Rxn+32p%tSLvRB0|1`3*cT_ND7OvrOV|Kmr@SV0|o0+ zT~f@A((Lc|o7v>#uYn?4NbPuWPC9ZRW6kx_B|iVpT$C!kG}lL^$8(2h{{0M!m*J_* zj{#R61NGeHvV@fp$`&7|iz%o;vbV->Ix#|tGC|wsN2&XQ8k1p1>gzIWDM_G6M`(sq zgo1iWE}4{mIJOd}>O0IBV8+W-eIE=cQ3gR(9I$>V7F^QR9;n{h08MU#dD+(a0a|)g z?U3E8j`v=!){(_zNYA}QkM+nyNO_4Wd_gf3=KE=>Up-5g7P5zzl1Zs#e=kl;lJtc; zNmx3&TX~+Ql*3g0Hig>~*ZOG^s~=L>ntF$ltl`82oy(i&Cur_))tRy>C#ix8>N%Uj z6_isn_dNY9lwBwU@2tejP{Gn)Av0b{?qx|Q=|?Hs-rOnr(K*{(INKbmq(bFv7W|<~ zjR<0)@_}8p)kI# z`44#6&}Eo3WDI{Xv+#A)${xE;sw2l$}zSH>qzKQ*Fw|5 z8{U25L#L+e=zg-MbIkqU!+j>sbar;?GtfcJ%*0~V0qo8*XU=R^)=QUeuB>$a=+e!# zmFGHVZ?3Ss&Cks4s_4(@R{i?iTL@iet6l%$!XMvWs16HY8jPqJi6-kcq#(A_aMon3A2{`#L9`*T`X|5uNnzhJM*7LX2t@{JjSwusx8m?lz%vd(EeF9;rRpsG*;_dbH&4G44zyj+lZVvs= zoOa&pMzA>#?xiP7n=~^%PQ!Xrt*- zqEEbW7Xun5I2^fJt=)1&8v=VY9SH+d55$4G$f%3^EcyfI?&9MAb!JvUAK%t4qG6jh zOd|wZt;0CfFw__(&}tpVkQA~G5@@v!W2i1vU-i6TBQMY(ffhsoP7CmU5)es%3-H7r z;HsGKXysb@$4IM47$A?eatycgei!HBcM_LK7$7@cTov=~+RNkxCKThZu>KC7b}&9~A$GF_>AWBc_~VDv z)iv5GHr+O3(",0 @@ -885,7 +900,7 @@ musicdata_size=*-musicdata .segment "SAFE_BSS" ;we want our variables to start at $3000, out of the way of our music player and the font data -current_input_ptr_ptr: .res 2 + param_offset: .res 1 temp_bin: .res 1 @@ -899,6 +914,7 @@ download_buffer: download_buffer_length=8000 .res download_buffer_length +.res 10 ;filler scroll_buffer_0: .res 1000 diff --git a/client/inc/commonprint.i b/client/inc/commonprint.i index e51e192..ad1662d 100644 --- a/client/inc/commonprint.i +++ b/client/inc/commonprint.i @@ -10,7 +10,7 @@ .export failed_msg .export init_msg .export print - .export print_decimal + .export print_integer .export print_dotted_quad .export print_arp_cache .export mac_address_msg @@ -32,8 +32,8 @@ pptr = copy_src .bss -temp_bin: .res 1 -temp_bcd: .res 2 +temp_bin: .res 2 +temp_bcd: .res 3 temp_ptr: .res 2 .code .macro print_driver_init @@ -150,7 +150,7 @@ print: @print_loop: ldy #0 lda (pptr),y - beq @done_print + beq @done_print jsr print_a inc pptr bne @print_loop @@ -209,27 +209,24 @@ print_arp_cache: print_dotted_quad: sta pptr stx pptr + 1 - ldy #0 + lda #0 +@print_one_byte: + pha + tay lda (pptr),y - jsr print_decimal + ldx #0 + jsr print_integer + pla + cmp #3 + beq @done + clc + adc #1 + pha lda #'.' jsr print_a - - ldy #1 - lda (pptr),y - jsr print_decimal - lda #'.' - jsr print_a - - ldy #2 - lda (pptr),y - jsr print_decimal - lda #'.' - jsr print_a - - ldy #3 - lda (pptr),y - jsr print_decimal + pla + bne @print_one_byte +@done: rts @@ -252,58 +249,70 @@ print_mac: cpy #06 bne @one_mac_digit rts -print_decimal: ;print byte in A as a decimal number - pha - sta temp_bin ;save + +print_integer: ;print 16 bit number in AX as a decimal number + +;hex to bcd routine taken from Andrew Jacob's code at http://www.6502.org/source/integers/hex2dec-more.htm + stax temp_bin sed ; Switch to decimal mode lda #0 ; Ensure the result is clear sta temp_bcd sta temp_bcd+1 - ldx #8 ; The number of source bits + sta temp_bcd+2 + ldx #16 ; The number of source bits : asl temp_bin+0 ; Shift out one bit + rol temp_bin+1 lda temp_bcd+0 ; And add into result adc temp_bcd+0 sta temp_bcd+0 lda temp_bcd+1 ; propagating any carry adc temp_bcd+1 sta temp_bcd+1 + lda temp_bcd+2 ; ... thru whole result + adc temp_bcd+2 + sta temp_bcd+2 + dex ; And repeat for next bit bne :- + stx temp_bin+1 ;x is now zero - reuse temp_bin as a count of non-zero digits cld ;back to binary - - pla ;get back the original passed in number - bmi @print_hundreds ; if N is set, the number is >=128 so print all 3 digits - cmp #10 - bmi @print_units - cmp #100 - bmi @print_tens -@print_hundreds: - lda temp_bcd+1 ;get the most significant digit + ldx #2 + stx temp_bin+1 ;reuse temp_bin+1 as loop counter +@print_one_byte: + ldx temp_bin+1 + lda temp_bcd,x + pha + lsr + lsr + lsr + lsr + jsr @print_one_digit + pla and #$0f - clc - adc #'0' - jsr print_a - -@print_tens: - lda temp_bcd - lsr - lsr - lsr - lsr - clc - adc #'0' - jsr print_a -@print_units: - lda temp_bcd - and #$0f - clc - adc #'0' - jsr print_a - + jsr @print_one_digit + dec temp_bin+1 + bpl @print_one_byte rts - +@print_one_digit: + cmp #0 + beq @this_digit_is_zero + inc temp_bin ;increment count of non-zero digits +@ok_to_print: + clc + adc #'0' + jsr print_a + rts +@this_digit_is_zero: + ldx temp_bin ;how many non-zero digits have we printed? + bne @ok_to_print + ldx temp_bin+1 ;how many digits are left to print? + bne @this_is_not_last_digit + inc temp_bin ;to get to this point, this must be the high nibble of the last byte. + ;by making 'count of non-zero digits' to be >0, we force printing of the last digit +@this_is_not_last_digit: + rts print_hex: pha diff --git a/client/inc/nb65_constants.i b/client/inc/nb65_constants.i index b8e17fd..0573ce4 100644 --- a/client/inc/nb65_constants.i +++ b/client/inc/nb65_constants.i @@ -44,16 +44,15 @@ NB65_TFTP_UPLOAD EQU $24 ;upload: AX points to a TFTP transfer par NB65_TFTP_CALLBACK_UPLOAD EQU $25 ;upload: AX points to a TFTP transfer parameter structure, outputs: none NB65_DNS_RESOLVE EQU $30 ;inputs: AX points to a DNS parameter structure, outputs: DNS param structure updated with - ;NB65_DNS_HOSTNAME_IP updated with IP address corresponding to hostname. - -NB65_DOWNLOAD_RESOURCE EQU $40 ;inputs: AX points to a URL download structure, outputs: none - + ;NB65_DNS_HOSTNAME_IP updated with IP address corresponding to hostname. +NB65_DOWNLOAD_RESOURCE EQU $31 ;inputs: AX points to a URL download structure, outputs: none +NB65_PING_HOST EQU $32 ;inputs: AX points to destination IP address for ping, outputs: AX=time (in milliseconds) to get response NB65_PRINT_ASCIIZ EQU $80 ;inputs: AX=pointer to null terminated string to be printed to screen, outputs: none NB65_PRINT_HEX EQU $81 ;inputs: A=byte digit to be displayed on screen as (zero padded) hex digit, outputs: none NB65_PRINT_DOTTED_QUAD EQU $82 ;inputs: AX=pointer to 4 bytes that will be displayed as a decimal dotted quad (e.g. 192.168.1.1) NB65_PRINT_IP_CONFIG EQU $83 ;no inputs, no outputs, prints to screen current IP configuration - +NB65_PRINT_INTEGER EQU $84 ;inputs: AX=16 byte number that will be printed as an unsigned decimal NB65_INPUT_STRING EQU $90 ;no inputs, outputs: AX = pointer to null terminated string NB65_INPUT_HOSTNAME EQU $91 ;no inputs, outputs: AX = pointer to hostname (which may be IP address). @@ -115,6 +114,10 @@ NB65_PAYLOAD_LENGTH EQU $08 ;2 byte length of payload ; in a TCP connection, if the length is $FFFF, this actually means "end of connection" NB65_PAYLOAD_POINTER EQU $0A ;2 byte pointer to payload of packet (after all headers) +;offsets in ICMP listener parameter structure +NB65_ICMP_LISTENER_TYPE EQU $00 ;ICMP type +NB65_ICMP_LISTENER_CALLBACK EQU $01 ;2 byte address of routine to call when ICMP packet of specified type arrives + ;offsets in URL download structure ;inputs: @@ -122,10 +125,6 @@ NB65_URL EQU $00 ;2 byte pointer to null te NB65_URL_DOWNLOAD_BUFFER EQU $02 ;2 byte pointer to buffer that resource specified by URL will be downloaded into NB65_URL_DOWNLOAD_BUFFER_LENGTH EQU $04 ;2 byte length of buffer (download will truncate when buffer is full) -;AX = address of URL string -; url_download_buffer - points to a buffer that url will be downloaded into -; url_download_buffer_length - length of buffer - ;error codes (as returned by NB65_GET_LAST_ERROR) NB65_ERROR_PORT_IN_USE EQU $80 NB65_ERROR_TIMEOUT_ON_RECEIVE EQU $81 @@ -139,7 +138,7 @@ NB65_ERROR_NO_SUCH_LISTENER EQU $88 NB65_ERROR_CONNECTION_RESET_BY_PEER EQU $89 NB65_ERROR_CONNECTION_CLOSED EQU $8A NB65_ERROR_FILE_ACCESS_FAILURE EQU $90 -NB65_MALFORMED_URL EQU $A0 -NB65_DNS_LOOKUP_FAILED EQU $A1 +NB65_ERROR_MALFORMED_URL EQU $A0 +NB65_ERROR_DNS_LOOKUP_FAILED EQU $A1 NB65_ERROR_OPTION_NOT_SUPPORTED EQU $FE NB65_ERROR_FUNCTION_NOT_SUPPORTED EQU $FF diff --git a/client/inc/ping.i b/client/inc/ping.i new file mode 100644 index 0000000..6ca0df8 --- /dev/null +++ b/client/inc/ping.i @@ -0,0 +1,66 @@ +.import icmp_ping +.import icmp_echo_ip + +NUM_PING_RETRIES=3 +.bss +ping_retries: .res 1 + +.code +ping_loop: + ldax #remote_host + jsr print + nb65call #NB65_INPUT_HOSTNAME + bcc @host_entered + ;if no host entered, then bail. + rts +@host_entered: + stax nb65_param_buffer + jsr print_cr + ldax #resolving + jsr print + ldax nb65_param_buffer + nb65call #NB65_PRINT_ASCIIZ + jsr print_cr + ldax #nb65_param_buffer + nb65call #NB65_DNS_RESOLVE + bcc @resolved_ok +@failed: + print_failed + jsr print_cr + jsr print_errorcode + jmp ping_loop +@resolved_ok: + + lda #NUM_PING_RETRIES + sta ping_retries +@ping_once: + ldax #pinging + jsr print + ldax #nb65_param_buffer + jsr print_dotted_quad + lda #' ' + jsr print_a + lda #':' + jsr print_a + lda #' ' + jsr print_a + + ldax #nb65_param_buffer + nb65call #NB65_PING_HOST + +bcs @ping_error + jsr print_integer + ldax #ms + jsr print +@check_retries: + dec ping_retries + bpl @ping_once + jmp ping_loop + +@ping_error: + jsr print_errorcode + jmp @check_retries + + +ms: .byte " MS",13,0 +pinging: .byte "PINGING ",0 diff --git a/client/inc/version.i b/client/inc/version.i index 433ab7a..6e356b6 100644 --- a/client/inc/version.i +++ b/client/inc/version.i @@ -1 +1 @@ -.byte "0.9.24" +.byte "0.9.25" diff --git a/client/ip65/Makefile b/client/ip65/Makefile index c76907e..8c7831f 100644 --- a/client/ip65/Makefile +++ b/client/ip65/Makefile @@ -16,7 +16,6 @@ ETHOBJS= \ cs8900a.o \ eth.o \ arp.o \ - icmp.o \ udp.o \ ip65.o \ printf.o \ @@ -34,15 +33,17 @@ ETHOBJS= \ all: ip65.lib ip65_tcp.lib -ip65.lib: $(ETHOBJS) function_dispatcher.s ip.s +ip65.lib: $(ETHOBJS) function_dispatcher.s ip.s icmp.s $(AS) $(AFLAGS) function_dispatcher.s - $(AS) $(AFLAGS) ip.s - ar65 a ip65.lib $(ETHOBJS) function_dispatcher.o ip.o + $(AS) $(AFLAGS) ip.s + $(AS) $(AFLAGS) icmp.s + ar65 a ip65.lib $(ETHOBJS) function_dispatcher.o ip.o icmp.o -ip65_tcp.lib: tcp.o $(ETHOBJS) function_dispatcher.s ip.s tcp.s +ip65_tcp.lib: tcp.o $(ETHOBJS) function_dispatcher.s ip.s tcp.s icmp.s $(AS) $(AFLAGS) function_dispatcher.s -DTCP -DAPI_VERSION=2 $(AS) $(AFLAGS) ip.s -DTCP - ar65 a ip65_tcp.lib $(ETHOBJS) function_dispatcher.o ip.o tcp.o + $(AS) $(AFLAGS) icmp.s -DTCP + ar65 a ip65_tcp.lib $(ETHOBJS) function_dispatcher.o ip.o tcp.o icmp.o clean: rm -f *.o diff --git a/client/ip65/function_dispatcher.s b/client/ip65/function_dispatcher.s index 2d11e6a..db51f24 100644 --- a/client/ip65/function_dispatcher.s +++ b/client/ip65/function_dispatcher.s @@ -427,6 +427,13 @@ ip_configured: rts : + cpy #NB65_PRINT_INTEGER + bne :+ + jsr print_integer + clc + rts +: + ;these are the API "version 2" functions .ifdef API_VERSION @@ -466,6 +473,20 @@ ip_configured: jmp url_download : + + cpy #NB65_PING_HOST + .import icmp_echo_ip + .import icmp_ping + bne :+ + ldy #3 +@copy_ping_ip_loop: + lda (nb65_params),y + sta icmp_echo_ip,y + dey + bpl @copy_ping_ip_loop + jmp icmp_ping + +: cpy #NB65_TCP_CONNECT bne :+ .import tcp_connect diff --git a/client/ip65/icmp.s b/client/ip65/icmp.s index d4ceabe..0204861 100644 --- a/client/ip65/icmp.s +++ b/client/ip65/icmp.s @@ -2,6 +2,10 @@ ; .include "../inc/common.i" +.ifndef NB65_API_VERSION_NUMBER + .define EQU = + .include "../inc/nb65_constants.i" +.endif .export icmp_init .export icmp_process @@ -16,32 +20,51 @@ .exportzp icmp_code .exportzp icmp_cksum .exportzp icmp_data - + +.ifdef TCP + .export icmp_echo_ip + .export icmp_send_echo + .export icmp_ping +.endif + + + + .import ip65_process + .import ip65_error .import ip_calc_cksum .import ip_inp .import ip_outp - .import ip_broadcast + .import ip_broadcast + .import ip_send + .import ip_create_packet + .importzp ip_proto + .importzp ip_proto_icmp + .importzp ip_cksum_ptr .importzp ip_header_cksum .importzp ip_src .importzp ip_dest .importzp ip_data - + .importzp ip_len + .import eth_tx .import eth_inp .import eth_inp_len .import eth_outp - .import eth_outp_len - + .import eth_outp_len + .import timer_read + .import timer_timeout + .bss ; argument for icmp_add_listener -icmp_callback: .res 2 +icmp_callback: .res 2 + ; icmp callbacks -icmp_cbmax = 4 +icmp_cbmax = 2 icmp_cbtmp: .res 3 ; temporary vector icmp_cbveclo: .res icmp_cbmax ; table of listener vectors (lsb) icmp_cbvechi: .res icmp_cbmax ; table of listener vectors (msb) @@ -60,7 +83,32 @@ icmp_data = 4;offset of 'data' field in icmp packet icmp_echo_id = 4 ;offset of 'id' field in icmp echo request/echo response icmp_echo_seq = 6 ;offset of 'sequence' field in icmp echo request/echo response icmp_echo_data = 8 ;offset of 'data' field in icmp echo request/echo response - + +;icmp type codes +icmp_msg_type_echo_reply=0 +icmp_msg_type_destination_unreachable=3 +icmp_msg_type_source_quench=4 +icmp_msg_type_redirect=5 +icmp_msg_type_echo_request=8 +icmp_msg_type_time_exceeded=11 +icmp_msg_type_paramater_problem=12 +icmp_msg_type_timestamp=13 +icmp_msg_type_timestamp_reply=14 +icmp_msg_type_information_request=15 +icmp_msg_type_information_reply=16 + +;ping states +ping_state_request_sent=0 +ping_state_response_received=1 + + +.ifdef TCP +.segment "TCP_VARS" +icmp_echo_ip: .res 4 ; destination IP address for echo request ("ping") +icmp_echo_cnt: .res 1 ;ping sequence counter +ping_state: .res 1 + ping_timer: .res 2 ; +.endif .code @@ -84,7 +132,7 @@ icmp_init: ; generated and sent out (overwriting the eth_outp buffer) icmp_process: lda icmp_inp + icmp_type - cmp #8 ; ping + cmp #icmp_msg_type_echo_request ; ping beq @echo lda icmp_cbcount ; any installed icmp listeners? @@ -203,7 +251,7 @@ icmp_add_listener: rts -;add an icmp listener +;remove an icmp listener ;inputs: ; A = icmp type ;outputs: @@ -211,12 +259,12 @@ icmp_add_listener: ; clear if no error icmp_remove_listener: ldx icmp_cbcount ; any listeners installed? - beq @notfound + beq @notfound + dex : cmp icmp_cbtype,x ; check if type is listened beq @remove - inx - cpx icmp_cbcount - bne :- + dex + bpl :- @notfound: sec rts @@ -241,3 +289,158 @@ icmp_remove_listener: dec icmp_cbcount ; decrement counter clc rts + +.ifdef TCP + +; icmp_send_echo was contributed by Glen Holmer (ShadowM) + +;send an ICMP echo ("ping") request +;inputs: +; icmp_echo_ip: destination IP address +;outputs: +; carry flag - set if error, clear if no error +icmp_send_echo: + ldy #3 +: + lda icmp_echo_ip,y + sta ip_outp + ip_dest,y + dey + bpl :- + + + lda #icmp_msg_type_echo_request + sta icmp_outp + icmp_type + lda #0 ;not used for echo packets + sta icmp_outp + icmp_code + sta icmp_outp + icmp_cksum ;clear checksum + sta icmp_outp + icmp_cksum + 1 + sta icmp_outp + icmp_echo_id ;set id to 0 + sta icmp_outp + icmp_echo_id + 1 + inc icmp_echo_cnt + 1 ;big-endian + bne :+ + inc icmp_echo_cnt +: + ldax icmp_echo_cnt + stax icmp_outp + icmp_echo_seq + + ldy #0 +: + lda ip65_msg,y + beq @set_ip_len + sta icmp_outp + icmp_echo_data,y + iny + bne :- +@set_ip_len: + tya + clc + adc #28 ;IP header + ICMP type, code, cksum, id, seq + sta ip_outp + ip_len + 1 ;high byte first + lda #0 ;will never be >256 + sta ip_outp + ip_len + + ldax #icmp_outp ;start of ICMP packet + stax ip_cksum_ptr + tya + clc + adc #8 ;ICMP type, code, cksum, id, seq + ldx #0 ;AX = length of ICMP data + jsr ip_calc_cksum + stax icmp_outp + icmp_cksum + lda #ip_proto_icmp + sta ip_outp + ip_proto + jsr ip_create_packet + jmp ip_send + +;send a ping (ICMP echo request) to a remote host, and wait for a response +;inputs: +; icmp_echo_ip: destination IP address +;outputs: +; carry flag - set if no response, otherwise AX is time (in miliseconds) for host to respond +icmp_ping: + + lda #0 ;reset the "packet sent" counter + sta icmp_echo_cnt +@send_one_message: + jsr icmp_send_echo + bcc @message_sent_ok + ;we couldn't send the message - most likely we needed to do an ARP lookup. + ;so wait a bit, and retry + + jsr timer_read ; read current timer value + stax ping_timer +@loop_during_arp_lookup: + jsr ip65_process + ldax ping_timer + adc #50 ; set timeout to now + 50 ms + bcc :+ + inx +: + + jsr timer_timeout + bcs @loop_during_arp_lookup + jsr icmp_send_echo + bcc @message_sent_ok + ;still can't send? then give up + lda #NB65_ERROR_TRANSMIT_FAILED + sta ip65_error + rts +@message_sent_ok: + jsr timer_read ; read current timer value + stax ping_timer + ldax #icmp_ping_callback + stax icmp_callback + lda #icmp_msg_type_echo_reply + jsr icmp_add_listener + lda #ping_state_request_sent + sta ping_state +@loop_till_get_ping_response: + jsr ip65_process + + lda ping_state + cmp #ping_state_response_received + beq @got_reply + ldax ping_timer + inx ;x rolls over about 4 times per second + inx ;so we will timeout after about 2 seconds + inx + inx + inx + inx + inx + inx + + + jsr timer_timeout + bcs @loop_till_get_ping_response + lda #NB65_ERROR_TIMEOUT_ON_RECEIVE + sta ip65_error + lda #icmp_msg_type_echo_reply + jsr icmp_remove_listener + sec + rts +@got_reply: + lda #icmp_msg_type_echo_reply + jsr icmp_remove_listener + jsr timer_read + sec + sbc ping_timer + pha + txa + sbc ping_timer+1 + tax + pla + clc + rts + +icmp_ping_callback: + lda icmp_inp + icmp_echo_seq + cmp icmp_echo_cnt + bne @not_what_we_were_waiting_for + lda #ping_state_response_received + sta ping_state +@not_what_we_were_waiting_for: + rts + +ip65_msg: + .byte "ip65 - the 6502 IP stack",0 +.endif \ No newline at end of file diff --git a/client/ip65/url.s b/client/ip65/url.s index 62cd06a..d977566 100644 --- a/client/ip65/url.s +++ b/client/ip65/url.s @@ -107,7 +107,7 @@ url_parse: cmp #'H' beq @http @exit_with_error: - lda #NB65_MALFORMED_URL + lda #NB65_ERROR_MALFORMED_URL sta ip65_error @exit_with_sec: sec @@ -132,7 +132,7 @@ lda #url_type_gopher bcs @exit_with_sec jsr dns_resolve bcc :+ - lda #NB65_DNS_LOOKUP_FAILED + lda #NB65_ERROR_DNS_LOOKUP_FAILED sta ip65_error jmp @exit_with_sec : diff --git a/client/nb65/Makefile b/client/nb65/Makefile index f9ee1e6..501207c 100644 --- a/client/nb65/Makefile +++ b/client/nb65/Makefile @@ -11,6 +11,7 @@ INCFILES=\ ../inc/nb65_constants.i\ ../inc/version.i\ +TCP_INCFILES=../inc/gopher.i ../inc/telnet.i ../inc/ping.i IP65LIB=../ip65/ip65.lib IP65TCPLIB=../ip65/ip65_tcp.lib @@ -30,7 +31,7 @@ nb65_c64_ram.o: nb65_c64.s $(INCFILES) nb65_std_cart.o: nb65_c64.s $(INCFILES) $(AS) -DBANKSWITCH_SUPPORT=1 $(AFLAGS) -o $@ $< -nb65_tcp_cart.o: nb65_c64.s $(INCFILES) ../inc/gopher.i ../inc/telnet.i +nb65_tcp_cart.o: nb65_c64.s $(INCFILES) $(TCP_INCFILES) $(AS) -DBANKSWITCH_SUPPORT=3 $(AFLAGS) -o $@ $< nb65_rrnet.o: nb65_c64.s $(INCFILES) diff --git a/client/nb65/nb65_c64.s b/client/nb65/nb65_c64.s index 0401cb7..6ffddb0 100644 --- a/client/nb65/nb65_c64.s +++ b/client/nb65/nb65_c64.s @@ -55,6 +55,7 @@ .include "../inc/gopher.i" .include "../inc/telnet.i" + .include "../inc/ping.i" .endif .import cls .import beep @@ -72,6 +73,7 @@ .import parse_dotted_quad .import dotted_quad_value .import parse_integer + .import print_integer .import get_key_ip65 .import cfg_ip .import cfg_netmask @@ -592,8 +594,21 @@ net_apps_menu: jsr cls lda #14 jsr print_a ;switch to lower case + ldax #telnet_header + jsr print jmp telnet_main_entry @not_telnet: + cmp #KEYCODE_F2 + bne @not_gopher + jsr cls + lda #14 + jsr print_a ;switch to lower case + ldax #gopher_header + jsr print + jsr prompt_for_gopher_resource ;only returns if no server was entered. + jmp exit_gopher +@not_gopher: + cmp #KEYCODE_F3 bne @not_gopher_floodgap_com jsr cls @@ -605,18 +620,20 @@ net_apps_menu: stx resource_pointer_hi ldx #0 jsr select_resource_from_current_directory - - jmp exit_gopher + jmp exit_gopher @not_gopher_floodgap_com: + cmp #KEYCODE_F5 - bne @not_gopher + bne @not_ping jsr cls lda #14 jsr print_a ;switch to lower case - jsr prompt_for_gopher_resource ;only returns if no server was entered. - jmp exit_gopher - -@not_gopher: + ldax #ping_header + jsr print + jsr ping_loop + jmp exit_ping +@not_ping: + cmp #KEYCODE_F7 bne @not_main jmp main_menu @@ -680,13 +697,14 @@ cfg_get_configuration_ptr: rts .if (BANKSWITCH_SUPPORT=$03) +exit_ping: exit_telnet: exit_gopher: lda #142 jsr print_a ;switch to upper case lda #$05 ;petscii for white text jsr print_a - jmp main_menu + jmp net_apps_menu .endif .rodata @@ -695,32 +713,33 @@ netboot65_msg: .include "../inc/version.i" .byte 13,0 main_menu_msg: -.byte 13," MAIN MENU",13,13 +.byte 13,"MAIN MENU",13,13 .byte "F1: TFTP BOOT" .if (BANKSWITCH_SUPPORT=$03) -.byte " F3: NET APPS" +.byte " F3: NET APPS" .else -.byte " F3: BASIC" +.byte " F3: BASIC" .endif .byte 13 -.byte "F5: ARP TABLE F7: CONFIG",13,13 +.byte "F5: ARP TABLE F7: CONFIG",13,13 .byte 0 config_menu_msg: -.byte 13," CONFIGURATION",13,13 -.byte "F1: IP ADDRESS F2: NETMASK",13 -.byte "F3: GATEWAY F4: DNS SERVER",13 -.byte "F5: TFTP SERVER F6: RESET TO DEFAULT",13 +.byte 13,"CONFIGURATION",13,13 +.byte "F1: IP ADDRESS F2: NETMASK",13 +.byte "F3: GATEWAY F4: DNS SERVER",13 +.byte "F5: TFTP SERVER F6: RESET TO DEFAULT",13 .byte "F7: MAIN MENU",13,13 .byte 0 .if (BANKSWITCH_SUPPORT=$03) net_apps_menu_msg: -.byte 13," NET APPS",13,13 -.byte "F1: TELNET F3: GOPHER.FLOODGAP.COM",13 -.byte "F5: GOPHER F7: MAIN MENU",13,13 +.byte 13,"NET APPS",13,13 +.byte "F1: TELNET F2: GOPHER ",13 +.byte "F3: GOPHER (FLOODGAP.COM)",13 +.byte "F5: PING F7: MAIN MENU",13,13 .byte 0 cant_boot_basic: @@ -728,6 +747,10 @@ cant_boot_basic: gopher_initial_location: .byte "1gopher.floodgap.com",$09,"/",$09,"gopher.floodgap.com",$09,"70",$0D,$0A,0 +ping_header: .byte "ping",13,0 +gopher_header: .byte "gopher",13,0 +telnet_header: .byte "telnet",13,0 + .endif downloading_msg: .asciiz "DOWNLOADING " diff --git a/client/test/Makefile b/client/test/Makefile index 006b6bb..6702896 100644 --- a/client/test/Makefile +++ b/client/test/Makefile @@ -19,7 +19,6 @@ INCFILES=\ ../inc/net.i\ all: \ -# ip65test.dsk \ testdns.prg \ test_disk_io.prg \ test_disk_io.d64 \ @@ -31,8 +30,10 @@ all: \ testdottedquad.prg\ test_tcp.prg \ test_parser.prg \ + test_ping.prg \ test_get_url.prg \ - +# ip65test.dsk \ + %.o: %.c $(CC) -c $(CFLAGS) $< @@ -51,6 +52,9 @@ test_parser.prg: test_parser.o $(IP65TCPLIB) $(C64PROGLIB) $(INCFILES) ../cfg/c6 test_get_url.prg: test_get_url.o $(IP65TCPLIB) $(C64PROGLIB) $(INCFILES) ../cfg/c64prg.cfg $(LD) -m test_get_url.map -vm -C ../cfg/c64prg.cfg -o test_get_url.prg $(AFLAGS) $< $(IP65TCPLIB) $(C64PROGLIB) +test_ping.prg: test_ping.o $(IP65TCPLIB) $(C64PROGLIB) $(INCFILES) ../cfg/c64prg.cfg + $(LD) -m test_ping.map -vm -C ../cfg/c64prg.cfg -o test_ping.prg $(AFLAGS) $< $(IP65TCPLIB) $(C64PROGLIB) + %.pg2: %.o $(IP65LIB) $(APPLE2PROGLIB) $(INCFILES) ../cfg/a2bin.cfg $(LD) -C ../cfg/a2bin.cfg -o $*.pg2 $(AFLAGS) $< $(IP65LIB) $(APPLE2PROGLIB) diff --git a/client/test/test_disk_io.s b/client/test/test_disk_io.s index 9bc3974..919167f 100644 --- a/client/test/test_disk_io.s +++ b/client/test/test_disk_io.s @@ -289,9 +289,6 @@ fname: loading: .byte "LOADING ",0 .rodata -press_a_key_to_continue: - .byte "PRESS A KEY TO CONTINUE",13,0 - filetype: .byte "TYPE: $",0 diff --git a/client/test/test_ping.s b/client/test/test_ping.s new file mode 100644 index 0000000..a11c567 --- /dev/null +++ b/client/test/test_ping.s @@ -0,0 +1,75 @@ + .include "../inc/common.i" + .include "../inc/commonprint.i" + .include "../inc/net.i" + + .import exit_to_basic + + .import cfg_get_configuration_ptr + .import copymem + .importzp copy_src + .importzp copy_dest + + .import icmp_echo_ip + .import icmp_ping + + + .import __CODE_LOAD__ + .import __CODE_SIZE__ + .import __RODATA_SIZE__ + .import __DATA_SIZE__ + + .segment "STARTUP" ;this is what gets put at the start of the file on the C64 + + .word basicstub ; load address + +basicstub: + .word @nextline + .word 2003 + .byte $9e + .byte <(((init / 1000) .mod 10) + $30) + .byte <(((init / 100 ) .mod 10) + $30) + .byte <(((init / 10 ) .mod 10) + $30) + .byte <(((init ) .mod 10) + $30) + .byte 0 +@nextline: + .word 0 + +.code + +init: + jsr print_cr + init_ip_via_dhcp + jsr print_ip_config + jsr print_cr + + ;our default gateway is probably a safe thing to ping + ldx #$3 +: + lda cfg_gateway,x + sta icmp_echo_ip,x + dex + bpl :- + ldax #pinging + jsr print + + ldax #icmp_echo_ip + jsr print_dotted_quad + jsr print_cr + jsr icmp_ping + bcs @error + jsr print_integer + ldax #ms + jsr print + jsr print_arp_cache + rts +@error: + jmp print_errorcode + +.rodata +ms: .byte " MS",13,0 +pinging: .byte "PINGING ",0 +.bss +block_number: .res 1 +block_length: .res 2 +buffer1: .res 256 +buffer2: .res 256 \ No newline at end of file diff --git a/dist/make_error_codes.rb b/dist/make_error_codes.rb new file mode 100644 index 0000000..be43f9a --- /dev/null +++ b/dist/make_error_codes.rb @@ -0,0 +1,11 @@ +errors="\n" +IO.readlines("netboot65/inc/nb65_constants.i").each do |line| + if line=~/NB65_ERROR_(\S*).*(\$\S\S)/ then + code=$2 + description=$1.gsub("_"," ") + errors<<"\n" + end +end +errors<<"
ERROR CODEDESCRIPTION
#{code}#{description}
\n" +puts errors + diff --git a/dist/version_number.txt b/dist/version_number.txt index f76e5a8..f5b38be 100644 --- a/dist/version_number.txt +++ b/dist/version_number.txt @@ -1 +1 @@ -0.9.24 \ No newline at end of file +0.9.25 \ No newline at end of file diff --git a/doc/README.C64.html b/doc/README.C64.html index 265d3ce..bf5c18a 100644 --- a/doc/README.C64.html +++ b/doc/README.C64.html @@ -57,8 +57,9 @@ Once the IP stack is initialised, the "main menu" screen will be displayed. Ther
  • Line - Data is converted to/from ASCII, but each line of input can be edited and is not sent until the RETURN key is pressed.
  • Once a connection is made, it can be terminated by hitting RUN/STOP +
  • F2 : GOPHER. You will be prompted to enter the hostname (only - no port number can be specified) of a gopher server, and the gopher client will be launched connecting to the specified server.
  • F3 : GOPHER.FLOODGAP.COM. This will launch the Gopher client, and connect to the gopher portal at gopher://gopher.floodgap.com/
  • -
  • F5 : GOPHER. You will be prompted to enter the hostname (only - no port number can be specified) of a gopher server, and the gopher client will be launched connecting to the specified server. +
  • F5 : PING. You will be prompted for a hostname which will be pinged 3 times, and a response time (in milliseconds) is printed for each ping.
  • F7 : MAIN MENU. This will return to the main menu.
  • @@ -100,7 +101,26 @@ Files need to be placed in the 'boot/' folder.

    Due to a limitation in the menu selection code, only the first 128 PRG files in the boot/ folder can be selected. - +

    ERROR CODES

    +Most network functions will return an 8 bit error code if things go wrong. + + + + + + + + + + + + + + + + + +
    ERROR CODEDESCRIPTION
    $80PORT IN USE
    $81TIMEOUT ON RECEIVE
    $82TRANSMIT FAILED
    $83TRANSMISSION REJECTED BY PEER
    $84INPUT TOO LARGE
    $85DEVICE FAILURE
    $86ABORTED BY USER
    $87LISTENER NOT AVAILABLE
    $88NO SUCH LISTENER
    $89CONNECTION RESET BY PEER
    $8ACONNECTION CLOSED
    $90FILE ACCESS FAILURE
    $A0MALFORMED URL
    $A1DNS LOOKUP FAILED
    $FEOPTION NOT SUPPORTED
    $FFFUNCTION NOT SUPPORTED

    REQUIREMENTS

    1. RR-NET or compatible adaptor (to use under VICE, you will need pcap or winpcap installed)
    2. diff --git a/doc/nb65_api_technical_reference.doc b/doc/nb65_api_technical_reference.doc index 79088115180361e699df1ee8b4b82dcf005910ef..32e3bf5bb223124eb9506498eb7b71e15a56e4f6 100644 GIT binary patch delta 16194 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%0DKVmT&V=xZm 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