' Copyright Brad Campbell, 2007,2008 - All rights reserved ' This code is not USB compliant. Contact the author for any kind of commercial use. ' ' Thanks to Dick Streefland and Igor Cesko (Not quite sure the unicode translates) ' for inspiration ' http://www.xs4all.nl/~dicks/avr/usbtiny/index.html ' http://cesko.host.sk/IgorPlugUSB/IgorPlug-USB%20(AVR)_eng.htm ' The MAC COG relies on the CRC table defined in here, don't replace it without altering ' the MAC COG code! ' Hacky non-compliant CDC-ACM FullDuplexSerial_001 compliant USB serial port ' This uses bulk endpoints which are a gross no-no using low-speed USB ' Works under Windows, MacOS and Linux (with 2 minor kernel alterations) ' Work is in progress to develop a standard linux driver for this implementation which will then ' work for all the AVR based CDC units also. ' You *must* pull data from the RX buffer periodically. If it fills up Windows chucks a hissy fit. ' If you don't require inbound data, just hit rxcheck() every now and again and it will keep the buffer ' pared down. If the interface is not open, it is entirely possible for tx() to block. Be warned! ' Mac OSX is a great testing environment as it seems to have the strictest enumeration requirements ' If you have an older VIA chipset and are running Win2k or XP. Install the VIA 4-in-1 or Hyperion Drivers! ' For linux be sure to stty -F /dev/ttyACM0 -echo raw ' Rev 4 - Renamed out to _out so as to be able to present a VGA_Text "compatible" out() routine ' - Relocated dd_table from DAT to VAR to allow for multiple instances of object ' - Pass pin definition down to the low level cog ' - Released 19/09/2007 ' Rev 5 - Revised Config Descriptor bmAttributes from $40 to $E0. Bit 7 is a mandatory "1" (Linux validated and complained quietly) ' - LangID was backwards (Caused Windows not to report Manuf/Product Strings) ' - Serial status was not being sent - No great loss it would appear but we are now properly compliant ' - Was not acknowledging SET_LINE_CODE data packet properly. Windows kept choking on device open ' - Updated to lowlevel_008 ' - Changed config descriptor to Bus powered 100mA rather than Self powered (Allows a bus powered proto board) ' - Properly handle an aborted config pipe transfer (Windows re-enumeration on faulty hub fix) ' - Added parameter in Start() for packet pacing (VIA bugfix) ' Rev 6 - Code packing / refactoring for space / speed ' Rev 7 - Timeout on transmit to prevent writer hanging up ' - Updated to lowlevel_009 ' - Removed parameter in Start() for packet pacing as it's now default in lowlevel code and benchmarking ' has shown zero performance impact overall. win/win ' Rev 8 - Fixed USB revision and added Serial Number to USB descriptors ' - Code consolidation ' - Updated to lowlevel_010 - API change ' - 74 Longs free ' - Reduced TX timeout to 100ms (x5 retries) ' - Moved set address function to the low-level cog. ' - STALL on any config request we don't recognise (as per spec) ' Rev 9 - Here we go again! CON BSIZ = $1F ' Buffer performs nicely about here. Can go as low as $F and as high as $FF ' *Must* be a clean all ones bitmask VAR ' each endpoint in/out has 4 longs ' IN endpoints 1st ' byte counter ' bytes [0-3] ' bytes [4-7] ' spare ' OUT endpoints ' byte counter ' bytes [0-3] ' bytes [4-7] ' bytes [8-9] justified to LSB long retry long endpoints[32] ' Located here to allow multiple instances word dd_table[16] ' Up to 16 descriptors / strings per instance word txhead, txtail, rxhead, rxtail byte txbuf[BSIZ+1] byte rxbuf[BSIZ+1] OBJ ll : "USB_lowlevel_011" ctbl : "CRC16_001" PUB start(Pdminus, Pdplus, Penable) : OK | i Pcrcmask := ctbl.CRCTable ' Address of CRC table Dtable := @dd_table ' Address of Descriptor / String table wordfill(Dtable, 0, 16) ' Populate Descriptor / String table ' These are in order here - Strings *must* come first and in order they are described ' in the device descriptor Word[Dtable][1] := @dd_langid Word[Dtable][2] := @dd_vendor Word[Dtable][3] := @dd_product Word[Dtable][4] := @dd_serial Word[Dtable][7] := @dd_status Word[Dtable][8] := @dd_device Word[Dtable][9] := @dd_config Word[Dtable][10] := @dd_defmode Word[Dtable][11] := @dd_dcd_status ' Serial buffers / Fifo's ' Putting this here saves calculation in the COG when memory is tight rxh := @rxhead txh := @txhead rxt := @rxtail txt := @txtail rxb := @rxbuf txb := @txbuf ' These are separated as I believe the compiler shuffles the positions of long/word/byte buffers at compile time wordfill(@txhead, 0, 4) wordfill(@txbuf, 0 , BSIZ+1) ' Zero's txbuf & rxbuf together epb := @endpoints longfill(epb, 0, 32) ' Zero endpoint communications buffers OK := ll.start(Pcrcmask, epb, Pdminus, Pdplus, Penable) ' Crank up low level cog if OK <> 0 OK := cognew(@USB_APP, 0) PUB rxflush '' Flush receive buffer repeat while rxcheck => 0 PUB rxtime(ms) : rxbyte | ti '' Wait ms milliseconds for a byte to be received '' returns -1 if no byte received, $00..$FF if byte ti := cnt repeat until (rxbyte := rxcheck) => 0 or (cnt - ti) / (clkfreq / 1000) > ms PUB rx : rxbyte '' Receive byte (may wait for byte) '' returns $00..$FF repeat while (rxbyte := rxcheck) < 0 PUB rxcheck : rxbyte | i '' Check if byte received (never waits) '' returns -1 if no byte received, $00..$FF if byte rxbyte-- if rxtail <> rxhead rxbyte := rxbuf[rxtail] rxtail := (rxtail + 1) & BSIZ PUB tx(txbyte) : OK | i '' Send a byte out the USB port. May wait up to 0.5 seconds for space '' in buffer prior to discarding the 1st byte. Subsequent bytes will be '' unceremoniously dropped on the floor with no delay until there is buffer '' space available again. An ugly non-blocking behaviour '' Result is zero for a good send, non-zero for a problem OK := 1 if (retry < 5) or (txtail <> (txhead + 1) & BSIZ) repeat until ((txtail <> (txhead + 1) & BSIZ) or (retry > 5)) retry++ waitcnt(clkfreq/10 + cnt) if (txtail <> (txhead + 1) & BSIZ) txbuf[txhead] := txbyte txhead := (txhead + 1) & BSIZ retry := 0 OK := 0 ' We had a good send PUB out(txbyte) tx(txbyte) PUB str(stringptr) '' Send string repeat strsize(stringptr) tx(byte[stringptr++]) PUB dec(value) | i '' Print a decimal number if value < 0 -value tx("-") i := 1_000_000_000 repeat 10 if value => i tx(value / i + "0") value //= i result~~ elseif result or i == 1 tx("0") i /= 10 PUB hex(value, digits) '' Print a hexadecimal number value <<= (8 - digits) << 2 repeat digits tx(lookupz((value <-= 4) & $F : "0".."9", "A".."F")) PUB bin(value, digits) '' Print a binary number value <<= 32 - digits repeat digits tx((value <-= 1) & 1 + "0") ' ======================== Hacks for drop-in compatibility ========================== PUB putc(txbyte) tx(txbyte) PUB strln(stringptr) str(stringptr) out(10) out(13) PUB decx(decimal,temp) dec(decimal) ' ======================== Hacks for drop-in compatibility ========================== DAT beef long $beefAB03 USB_APP org wrlong zero, epb ' Spark up MAC thread now we are running usboot mov _out, #0 :loop mov pointer, epb rdlong da, pointer wz IF_NZ call #process_0i add pointer, #32 rdlong da, pointer wz IF_NZ call #process_1i add pointer, #32 rdlong da, pointer wz IF_NZ call #process_2i add pointer, #32 rdlong da, pointer wz IF_NZ call #process_3i sub pointer, #80 test _out, #1 wz IF_Z jmp #:loop1 rdlong da, pointer wz IF_Z call #process_0o :loop1 add pointer, #32 test _out, #2 wz IF_Z jmp #:loop2 rdlong da, pointer wz IF_Z call #process_1o :loop2 add pointer, #32 test _out, #4 wz IF_Z jmp #:loop3 rdlong da, pointer wz IF_Z call #process_2o :loop3 add pointer, #32 test _out, #8 wz IF_Z jmp #:loop4 rdlong da, pointer wz IF_Z call #process_3o :loop4 jmp #:loop reset mov bytecnt, #0 mov flags, #0 mov epoint, #0 call #rdbuf add epoint, #1 call #rdbuf add epoint, #1 call #rdbuf add epoint, #1 call #rdbuf jmp #usboot process_0i test da, flag_reset wz IF_NZ jmp #reset ' Lower level has requested a system reset mov epoint, #0 call #rdbuf test buf0, flag_setup wz ' Are we a setup packet? IF_Z jmp #:data_in ' This handles an aborted Pipe. Windows does this when re-enumerating after an error test _out, #1 wz IF_Z jmp #:next andn _out, #1 ' Switch off endpoint mov da, epb add da, #16 wrlong zero, da :next mov dc, buf1 shr dc, #8 and dc, #$FF ' Request Number mov flags, #0 mov bytecnt, buf0 and bytecnt, #$FF cmp bytecnt, #8 wz ' Number of bytes IF_NZ jmp #:end ' All setup packets are 8 bytes test buf0, flag_class wz ' Are we a class packet? IF_NZ jmp #:class cmp dc, #6 wz ' GET_DESCRIPTOR IF_NZ jmp #:setup3 mov dd, buf1 shr dd, #24 and dd, #$FF mov da, #0 cmp dd, #1 wz ' Device Descriptor IF_Z mov da, #8 cmp dd, #2 wz ' Config Descriptor IF_Z mov da, #9 cmp dd, #3 wz ' String Descriptor (see below) IF_Z jmp #:ss_string_d tjz da, #:send_stall ' Have we anything to send? jmp #:queue ' These string ID's are tied to the ID's specified in the Config Descriptor ' Make it easy and use 1/2/3. 0 is Mandatory if you have 1, 2 or 3. :ss_string_d mov dd, buf1 shr dd, #16 and dd, #$FF mov da, dd add da, #1 cmp da, #4 wc, wz IF_A jmp #:send_stall :queue mov ep0bl, buf2 shr ep0bl, #16 and ep0bl, #$1FF ' Max length call #queue_string test db, allones wz IF_Z jmp #:send_empty ' No string to queue mov ep0len, db mov ep0buf, da or _out, #1 ' Endpoint 0 jmp #:end :setup3 cmp dc, #0 wz ' GET_STATUS (need this if we are not picked up by a driver - lsusb causes a reboot) IF_Z mov da, #7 IF_Z jmp #:queue cmp dc, #9 wz ' SET_CONFIG (just pretend we've done it - we only have the one!) IF_Z jmp #:send_empty ' Default anything we don't handle jmp #:send_stall :send_empty or flags, flag_empty1 ' STATUS requests requires a DATA1 :send_any call #wrbuf jmp #:end :send_stall or flags, flag_stall jmp #:send_any :class cmp dc, #$20 wz ' SET_LINE_CODING IF_Z or flags, flag_comms ' Next data in is baud/stop/parity/bits cmp dc, #$21 wz ' GET_LINE_CODING IF_Z jmp #:clinecode cmp dc, #$22 wz ' SET_CONTROL_LINE_STATE IF_Z jmp #:clinestate test buf1, #$80 wz ' Host direction bit IF_NZ jmp #:end ' No need for empty packet jmp #:send_stall :clinecode mov da, #10 ' Queue up line code data to send jmp #:queue :clinestate mov da, buf1 shr da, #16 test da, #$01 wz ' DTR equivalent bit IF_NZ or _out, #2 ' We are off hook, enable data transmission IF_Z andn _out, #2 ' Stop transmission of DATA ' Turn off process bit call #send_serial_state ' Either way, queue up a serial state packet to send jmp #:send_empty :data_in test flags, flag_comms wz IF_NZ jmp #:end ' Save send line code pacaket into the hub buffer for playing back when Windows ' requests it call #unpack_buffer ' Make indirection and looping easy mov da, #10 ' Line Code data in HUB call #queue_string ' Get start of buffer in HUB into da movs :data_indr, #b_1 mov db, #7 :data_indr mov dc, 0-0 add :data_indr, #1 wrbyte dc, da add da, #1 djnz db, #:data_indr jmp #:send_empty :end mov da, epb wrlong zero, epb process_0i_ret ret ' da is table index, dc is maximum length ' returns length in db and address in da ' if db is 0 there is no string queue_string shl da, #1 add da, Dtable rdword db, da wz IF_Z jmp #queue_string_ret ' Descriptor is empty mov da, db rdbyte db, da ' Queue up the descriptor and send it off add da, #1 queue_string_ret ret ' Sends a serial port mask update on endpoint 3 send_serial_state mov ep3bl, #$1F ' Max send length mov da, #11 ' If this returns garbage we can kiss our backsides goodbye! call #queue_string test db, allones wz IF_Z jmp #send_serial_state_ret mov ep3len, db mov ep3buf, da or _out, #8 ' endpoint #3 send_serial_state_ret ret ' This queues up the endpoint for action process_1i ' This buffer space calculation code is _very_ fragile ' it's been tested with buffer sizes of $f, $1F, $3F & $FF and proven to work ' It could, however, be more efficient I'm sure and da, #$FF ' Length pointer mov bytecnt, da rdword db, rxh ' Head rdword dc, rxt ' Tail cmp dc, db wc, wz IF_C jmp #:p12 ' Head < Tail IF_Z jmp #:p14 ' Head == Tail sub dc, db ' Space = Tail - Head jmp #:p13 :p12 mov dd, #BSIZ ' Top space = Buffer size - head sub dd, db add dc, dd ' Total space = Top space + tail :p13 mov dd, bytecnt sub dc, #1 ' Always leave at least 1 byte spare in the buffer cmp dc, dd wc IF_C jmp #process_1i_ret ' Not enough room in the buffer :p14 mov epoint, #1 mov dd, da call #rdbuf movs :p1i, #b_1 call #unpack_buffer :p1i mov dc, 0-0 add :p1i, #1 mov dd, rxb add dd, db wrbyte dc, dd add db, #1 and db, #BSIZ djnz bytecnt, #:p1i wrword db, rxh process_1i_ret ret process_2i mov epoint, #2 call #rdbuf process_2i_ret ret process_3i mov epoint, #3 call #rdbuf process_3i_ret ret process_0o mov epoint, #0 mov epbuf, ep0buf mov eplen, ep0len mov epbl, ep0bl call #ep_string_out mov ep0bl, epbl mov ep0buf, epbuf mov ep0len, eplen process_0o_ret ret process_1o rdword da, txh rdword db, txt cmp da, db wz IF_Z jmp #process_1o_ret ' Nothing to send call #bufstart mov dc, #8 :loop mov dd, txb add dd, db rdbyte data, dd call #pushbuf add db, #1 and db, #BSIZ cmp db, da wz ' Buffer is empty IF_Z jmp #:done ' Sent all we have djnz dc, #:loop ' Done 8 bytes yet? :done wrword db, txt call #pushcrc call #packem mov epoint, #1 mov flags, #0 call #wrbuf process_1o_ret ret process_2o mov epoint, #2 mov flags, #0 mov bytecnt, #0 call #wrbuf process_2o_ret ret process_3o mov epoint, #3 mov epbuf, ep3buf mov eplen, ep3len mov epbl, ep3bl call #ep_string_out mov ep3bl, epbl mov ep3buf, epbuf mov ep3len, eplen process_3o_ret ret rdbuf mov da, epoint shl da, #5 add da, epb rdlong buf0, da add da, #4 rdlong buf1, da add da, #4 rdlong buf2, da sub da, #8 wrlong zero, da ' Clear the buffer, we have it now rdbuf_ret ret wrbuf mov da, epoint shl da, #5 add da, epb add da, #20 ' 16 + skip 1st long wrlong buf0, da add da, #4 mov db, flags ' Interleaved wrlong buf1, da add da, #4 or db, bytecnt ' Interleaved wrlong buf2, da mov flags, #0 sub da, #12 wrlong db, da ' Byte Counter / Flags ' call #wrdebug wrbuf_ret ret packem ' 18 Instructions mov buf0, b_1 shl b_2, #8 or buf0, b_2 shl b_3, #16 or buf0, b_3 shl b_4, #24 or buf0, b_4 mov buf1, b_5 shl b_6, #8 or buf1, b_6 shl b_7, #16 or buf1, b_7 shl b_8, #24 or buf1, b_8 mov buf2, b_9 shl b_10, #8 or buf2, b_10 packem_ret ret unpack_buffer mov b_4, buf1 mov b_1, b_4 shr b_4, #8 mov b_2, b_4 shr b_4, #8 mov b_3, b_4 shr b_4, #8 mov b_8, buf2 mov b_5, b_8 shr b_8, #8 mov b_6, b_8 shr b_8, #8 mov b_7, b_8 shr b_8, #8 unpack_buffer_ret ret ' Add byte to output buffer and CRC ' Trashes 'data' pushbuf movd :pindr, bptr add bptr, #1 :pindr mov 0-0, data xor data, crc and data, #$FF shl data, #1 add data, Pcrcmask rdword t1, data shr crc, #8 xor crc, t1 add bytecnt, #1 pushbuf_ret ret ' Place CRC on the end of the buffer pushcrc movd :pcrc1, bptr add bptr, #1 movd :pcrc2, bptr xor crc, allones mov data, crc and data, #$FF :pcrc1 mov 0-0, data mov data, crc shr data, #8 and data, #$FF :pcrc2 mov 0-0, data add bytecnt, #2 pushcrc_ret ret ' Initialise the output buffer bufstart mov bptr, #b_1 mov crc, crcmask mov bytecnt, #0 bufstart_ret ret ep_string_out test eplen, allones wz IF_Z jmp #:send_empty call #bufstart mov da, #8 :send_loop rdbyte data, epbuf add epbuf, #1 call #pushbuf sub da, #1 sub eplen, #1 sub epbl, #1 tjz da, #:send_next tjz epbl, #:send_next tjnz eplen, #:send_loop :send_next call #pushcrc call #packem jmp #:next :send_empty mov bytecnt, #0 mov flags, flag_empty :next call #wrbuf test epbl, allones wz ' If we are at the end of the buffer or request length we don't IF_Z jmp #:here ' send an empty packet cmp bytecnt, #$0A wz IF_Z jmp #ep_string_out_ret :here mov da, #1 shl da, epoint andn _out, da ep_string_out_ret ret crcmask long $0000FFFF allones long $FFFFFFFF zero long 0 ' Pointers to buffers in Hub ram Pcrcmask long 0 Dtable long 0 epb long 0 rxh long 0 rxt long 0 txh long 0 txt long 0 rxb long 0 txb long 0 _out long 0 ep0len long 0 ep0buf long 0 ep0bl long 0 ep1len long 0 ep1buf long 0 ep1bl long 0 ep2len long 0 ep2buf long 0 ep2bl long 0 ep3len long 0 ep3buf long 0 ep3bl long 0 eplen long 0 epbuf long 0 epbl long 0 bptr long 0 buf0 long 0 buf1 long 0 buf2 long 0 b_1 long 0 b_2 long 0 b_3 long 0 b_4 long 0 b_5 long 0 b_6 long 0 b_7 long 0 b_8 long 0 b_9 long 0 b_10 long 0 flag_stall long 1 << 20 flag_comms long 1 << 21 flag_reset long 1 << 22 'flag_rtoggle long 1 << 23 flag_empty1 long 1 << 24 ' Send an empty DATA1 packet flag_debug long 1 << 25 ' Debugging used in the app level flag_class long 1 << 26 ' We recieved a CLASS request flag_address long 1 << 27 ' Inbound setup packet is an address request flag_empty long 1 << 28 ' Send next logical DATAX token in an empty packet flag_setup long 1 << 29 ' Inbound packet is a setup request 'flag_waitp long 1 << 30 ' Are we waiting for an inbound packet flag_crc long 1 << 31 ' CRC error detected flags res 1 pointer res 1 epoint res 1 ' inbound endpoint data res 1 da res 1 db res 1 dc res 1 dd res 1 crc res 1 bytecnt res 1 t res 1 t1 res 1 fit $1F0 dd_device byte @dd_device_end-(@dd_device+1) byte @dd_device_end-(@dd_device+1) ' Descriptor length byte 1 ' Descriptor type (Device) byte $10, 1 ' USB Version byte 2 ' USB Device Class (CDC) byte 0 ' Device Subclass byte 0 ' Device Protocol byte 8 ' Max packet size byte $C0, $16 ' Vendor ID byte $E1, $05 ' Device PID byte $00, $01 ' Device Version byte 1 ' Vendor string index byte 2 ' Product string index byte 3 ' Serial number index byte 1 ' Number of configurations dd_device_end dd_config byte @dd_inter_acm_e-(@dd_config+1) ' Total length to return to the application level byte @dd_config_end-(@dd_config+1) ' Descriptor length byte 2 ' Descriptor type (Config) byte @dd_inter_acm_e-(@dd_config+1),0 ' Total descriptor length returned byte 2 ' Number of interfaces byte 1 ' Configuration index byte 0 ' Configuration name string index (none) ' Pick one of the below pairs. byte $A0 ' Bus powered byte 50 ' 100mA 'byte $E0 ' Device is self powered, Supports Remote_Wakeup (we never sleep) 'byte 0 ' Consumes no bus power dd_config_end dd_inter byte @dd_inter_e-@dd_inter byte 4 ' Descriptor type (Interface) byte 0 ' Index of this interface byte 0 ' Alternate interface (None) byte 1 ' Number of endpoints for this interface byte 2 ' Interface Class (CDC) byte 2 ' Interface Subclass (Modem) byte 1 ' Interface Protocol (AT Commands) byte 0 ' Interface name string index (none) dd_inter_e dd_class byte 5 ' Descriptor Length byte $24 ' Descriptor Type (CS_INTERFACE) byte 0 ' Descriptor sybtype byte $10, $01 ' CDC Specification release number byte 4 ' Descriptor Length byte $24 ' Descriptor Type (CS_INTERFACE) byte 2 ' Descriptor subtype (ACM Function) byte 2 ' Function capabilities (Set_Line_Coding,Set_Control_Line_State,Get_Line_Coding,Serial_State) byte 5 ' Descriptor Length byte $24 ' Descriptor Type (CS_INTERFACE) byte 6 ' Descriptor subtype (Union functional descriptor) byte 0 ' Master interface (Endpoint 0) byte 1 ' Slave interface (Endpoint 1) byte 5 ' Descriptor Length byte $24 ' Descriptor Type (CS_INTERFACE) byte 1 ' Descriptor subtype (Call management functional descriptor) byte 3 ' Function capabilities (Device handles call management, Device can send call management over data interface) byte 1 ' bDataInterface (Endpoint 1) - Interface to pass call management data over dd_class_e dd_endpoint byte @dd_endpoint_e-@dd_endpoint ' Descriptor length byte 5 ' Descriptor type (Endpoint) byte $83 ' Endpoint (In / 3) byte 3 ' Interrupt endpoint byte 8, 0 ' Maximum packet size byte $64 ' Polling interval (in mS) dd_endpoint_e dd_inter_acm byte 9 ' Descriptor Length byte 4 ' Descriptor type (interface) byte 1 ' Interface index byte 0 ' Alternate settings byte 2 ' Number of endpoint descriptors to follow byte $A ' Data interface class byte 0 ' Interface subclass byte 0 ' Interface protocol byte 0 ' Interface name string index byte 7 ' Descriptor Length byte 5 ' Descriptor type (Endpoint) byte 1 ' Endpoint number (Out / 1) byte 2 ' Bulk endpoint (Illegal and invalid for a low speed device) byte 8, 0 ' Max packet size byte 0 ' Polling interval byte 7 ' Descriptor Length byte 5 ' Descriptor type (Endpoint) byte $81 ' Endpoint number (In / 1) byte 2 ' Bulk endpoint (Illegal and invalid for a low speed device) byte 8, 0 ' Max packet size byte 0 ' Polling interval dd_inter_acm_e ' The following for alignment of the strings word 0 byte 0 dd_vendor byte @dd_vendor_e-(@dd_vendor+1) byte @dd_vendor_e-(@dd_vendor+1) byte 3 word "Brads USB Stack" byte 0 dd_vendor_e dd_product byte @dd_product_e-(@dd_product+1) byte @dd_product_e-(@dd_product+1) byte 3 word "Propeller CDC-ACM HACK" byte 0 dd_product_e dd_serial byte @dd_serial_e-(@dd_serial+1) byte @dd_serial_e-(@dd_serial+1) byte 3 word "1234" byte 0 dd_serial_e dd_langid byte 4 byte 4, 3, $09, $04 dd_langid_e dd_dcd_status byte @dd_dcd_status_e-(@dd_dcd_status+1) byte $a1, $20, 0, 0, 0, 0, 2, 0, 3, 0 dd_dcd_status_e dd_defmode byte @dd_defmode_e-(@dd_defmode+1) byte $80, $25, 0, 0, 0, 0, 8 ' 9600, 8-n-1 dd_defmode_e dd_status byte 2 byte 0, 0 dd_status_e