
;===============================================================================
;--- some nice header should come here...

;    Uses real addresses into the buffers as pointers (rather than offset).
;    The + is that it is fast (no need to calculate the address).
;    The - is that a byte from buffer is wasted (the byte just behind tail
;    pointer gets never used); i.e. the buffers must be set to be 1 byte
;    longer than the longest message to be received/transmitted at once.

;--- these are only definitions for CR and LF (ASCII control characters) for those,
;    who are not able to remember it...
CR           EQU   13     
LF           EQU   10

;===============================================================================
;--- the following are declarations of variables
;    it is better to use the DS or such 
RX_TAIL      EQU   08H      ;pointers of receiver FIFO
RX_HEAD      EQU   09H
TX_TAIL      EQU   0AH      ;pointers of transmitter FIFO
TX_HEAD      EQU   0BH
RX_BUFF      EQU   0CH      ;receiver buffer
RX_BUFF_END  EQU   13H
TX_BUFF      EQU   RX_BUFF_END+1  ;14H   ;transmitter buffer
TX_BUFF_END  EQU   1FH

NEEDTI   BIT   01H   ;20H.1     ;a flag, determining, whether transmission is
                                ;in progress (0) or not (1)

SP_INIT  EQU   30H          ;the stack beginning (in fact, one byte below it)

;===============================================================================

   ORG   0
   LJMP  MAIN

   ORG   23H
   LJMP  SER_ISR


;===============================================================================
   ORG   40H
MAIN:
   MOV   SP,#SP_INIT
;--- the initialisation routine - I put it inline, but others prefer to
;    have it as a subroutine
SER_INIT:
   MOV   SCON,#01010000B    ;8N1
   MOV   TMOD,#00100001B
   MOV   TH1,#-3            ;9600Bd @ XTAL=11.0592MHz
   SETB  TR1
;
   MOV   RX_TAIL,#RX_BUFF   ;the pointers contain the address rather than an offset
   MOV   RX_HEAD,#RX_BUFF   ;and are initialised to the same value,
   MOV   TX_TAIL,#TX_BUFF   ;indicating, that the buffers are empty
   MOV   TX_HEAD,#TX_BUFF
   SETB  NEEDTI             ;flag indicates that transmission is NOT in progress
;
   SETB  ES
   SETB  EA                 ;interrupts enabled (gurus may use MOV IE,xxx)
;
   MOV   DPTR,#SINIT
   LCALL TEXT_OUT           ;spits out an invitation string
;
;===============================================================================
;--- now this is supposed to be an example "main". 
;    I have no idea how to demonstrate the benefits of the interrupt driven 
;    serial in a more showy way; so this simply waits until at least 3 characters
;    are received, then simply spits them out at once.

LOOP:
   LCALL GET_RX_NR          ;get the number of characters in receive buffer
   CJNE  A,#3,MAIN_X2   
MAIN_X2:                    
   JC    LOOP               ;loop until it is >= 3

   MOV   R2,A               
MAIN_X3:
   LCALL GET_CHAR           ;simply read all these characters
   LCALL PUT_CHAR_WAIT      ;and put write them back -> echo
   DJNZ  R2,MAIN_X3
   
   SJMP  LOOP               ;do this forever....
;
;---
SINIT:                     ;this is the invigtation string, 
   DB    CR,LF
   DB    'SERIAL PORT INITIALISED!'
   DB    CR,LF,0             ;...terminated by zero, as TEXT_OUT requires it


;===============================================================================
;--- the following are "utilities" to access the serial port, to be called from 
     "main" - in C these would be called getch, putch, printf or similar
;***************************
;-- get number of bytes sitting in receive buffer into A
;   input: none; output: number in A; uses: C
GET_RX_NR:                 
   CLR   C
   MOV   A,RX_HEAD
   SUBB  A,RX_TAIL
   JNC   GET_RX_NR_X1          ;in case the head has wrapped around but tail not
   ADD   A,#RX_BUFF_END-RX_BUFF+1   ;add length of buffer to get the proper number
GET_RX_NR_X1:
   RET
;***************************
;-- wait until a character is recveived into the receive buffer, then return it
;   input: none; output: received byte in A; uses: R0,C
GET_CHAR_WAIT:
;   LCALL GET_CHAR            ;this is an alternative way how to accomplish it
;   JC    GET_CHAR_WAIT       ;but it uses up extra 2 bytes from stack
;   RET
   MOV   A,RX_TAIL            ;wait until the pointers get different
   CJNE  A,RX_HEAD,GET_CHAR_X1   ;meaning there is something in buffer
   SJMP  GET_CHAR_WAIT

;-- return a character from receiver if available; if not, return carry set
;   input: none; output: received byte in A, C clear if A valid; uses: R0,C
GET_CHAR:
   MOV   A,RX_TAIL           ;if pointers are the same, it means
   CJNE  A,RX_HEAD,GET_CHAR_X1
   SETB  C                       ;the buffer is empty, so return with C set
   RET
GET_CHAR_X1:
   INC   A                   ;prepare the new "tail" pointer
   CJNE  A,#RX_BUFF_END+1,GET_CHAR_X2
   MOV   A,#RX_BUFF          ;wrap it around, if necessary
GET_CHAR_X2:
   XCH   A,RX_TAIL           ;store the new pointer and get the old pointer
   MOV   R0,A
   MOV   A,@R0               ;retrieve the byte from buffer tail
   CLR   C                   ;and clear carry to indicate the byte is valid
   RET

;it might seem that the problem with interrupt kicking in after the pointer
;has been updated but the byte pointed by old value of pointer has not been 
;processed (here: picked up), as is in case of PUT_CHAR (see below), occurs
;here, too; and the countermeasure (disabling interrupts) has to be taken.
;The problem would be, that if RX_TAIL already points to the byte after
;the one we are going to pick up, if in that time a new byte arrives and 
;the interrupt kicks in, the new byte would overwrite the byte to be picked
;up.
;Fortunately(?), the nature of the pointers being adresses (as mentioned at the
;very beginning) is, that the byte just before that pointed by the "tail" pointer
;gets never written, hence this problem would not occur.

;***************************   
;-- wait until there is a space for one byte in transmit buffer, then insert
;   the byte from A into it
;   input: A; output: C clear to indicate success; uses: R0,C
PUT_CHAR_WAIT:
;   LCALL PUT_CHAR         ;as with GET_CHAR_WAIT, an alternative solution
;   JC    PUT_CHAR_WAIT
;   RET
   MOV   R0,A
   MOV   A,TX_HEAD         ;check if tail pointer after advancing would not meet
   INC   A                 ;head pointer
   CJNE  A,#TX_BUFF_END+1,PUT_CHAR_WAIT_X1
   MOV   A,#TX_BUFF
PUT_CHAR_WAIT_X1:
   CJNE  A,TX_TAIL,PUT_CHAR_X2      ;if not, the buffer is not full
   SJMP  PUT_CHAR_WAIT_X1           ;otherwise it's full so wait

;-- insert a byte from A into transmit buffer, if there is space for it
;   else return carry set to indicate failure (but preserve byte in A)
;   input: A; output: C clear if success; uses: R0,C
PUT_CHAR:
   MOV   R0,A            ;preserve byte to be transmitted into R0
   MOV   A,TX_HEAD       ;check the space in transmit buffer
   INC   A               ;   - for description see PUT_CHAR_WAIT above
   CJNE  A,#TX_BUFF_END+1,PUT_CHAR_X1
   MOV   A,#TX_BUFF
PUT_CHAR_X1:
   CJNE  A,TX_TAIL,PUT_CHAR_X2
   MOV   A,R0            ;if no space, restore A
   SETB  C               ;and return C set to indicate failure
   RET
PUT_CHAR_X2:
   MOV   C,EA            ;preserve interrupts status
   CLR   EA              ;disable interrupts (see details below)
   XCH   A,TX_HEAD       
   XCH   A,R0
   MOV   @R0,A
   JNB   NEEDTI,PUT_CHAR_X3
   CLR   NEEDTI
   SETB  TI
PUT_CHAR_X3:
   MOV   EA,C            ;reenable interrupts
   CLR   C
   RET

;Here the problem of interrupt kicking in after TX_HEAD has been updated, 
;possibly transmitting prematurely the byte pointed by previous value of TX_HEAD
;prior it is updated, is a real problem; addressed by disabling the interrupts.
;
;In fact, there is no need to disable the interrupts globally, so it would be
;enough to manipulate ES rather than EA; similarly, there might be no need for
;storing the old value (depending on context). So this is a slightly overkill
;solution, but should work.
;***************************
;-- transmit a zero-terminated string pointed to by DPTR
;   input: DPTR pointing to string
;   output: DPTR pointing after the terminating zero
;   uses: A,R0,C,2 bytes of stack
TEXT_OUT:
   CLR   A
   MOVC  A,@A+DPTR
   INC   DPTR
   JZ    TEXT_OUT_X1
   LCALL PUT_CHAR_WAIT
   SJMP  TEXT_OUT
TEXT_OUT_X1:
   RET

;===============================================================================
;--- the serial interrupt service routine itself
;***************************
SER_ISR:
   PUSH  PSW
   PUSH  ACC
   MOV   A,R0     ;alternative: pushing R0 addressed directly if banks are never
   PUSH  ACC      ;switched or if a bank is selected explicitly after pushing PSW

;-- first, the receive ISR
   JNB   RI,SER_ISR_TX
SER_ISR_RX:       
   CLR   RI                       ;RI needs to be cleared "manually"
   MOV   A,RX_HEAD                ;advance head pointer 
   INC   A
   CJNE  A,#RX_BUFF_END+1,SER_ISR_RX1
   MOV   A,#RX_BUFF               ;  if needed, wrap around
SER_ISR_RX1:
   CJNE  A,RX_TAIL,SER_ISR_RX2    ;if matches tail pointer, buffer is full...
                                  ; it is up to the user how to cope with this,
                                  ; in PC, a beep comes here...
   SJMP  SER_ISR_TX               ; we simply throw this byte away...
SER_ISR_RX2:
   XCH   A,RX_HEAD                ;the advanced pointer is stored and the old 
   MOV   R0,A                     ; is used to store
   MOV   @R0,SBUF                 ; the freshly received byte
   
;-- second, the transmit ISR 
SER_ISR_TX:
   JNB   TI,SER_ISR_END
   CLR   TI                       ;TI needs to be cleared "manually"
   MOV   A,TX_TAIL                ;check if tail pointer matches head
   CJNE  A,TX_HEAD,SER_ISR_TX1
   SETB  NEEDTI                   ;if so, buffer is empty -> set flag 
   SJMP  SER_ISR_END              ;   "Tx not in progress" and exit
SER_ISR_TX1:
   INC   A                        ;else advance tail pointer
   CJNE  A,#TX_BUFF_END+1,SER_ISR_TX2
   MOV   A,#TX_BUFF               ;  wrap around if needed
SER_ISR_TX2:
   XCH   A,TX_TAIL                ;store the advanced pointer and use the old
   MOV   R0,A                     
   MOV   SBUF,@R0                 ;  to pick the byte to be transmitted
SER_ISR_END:
   POP   ACC                      ;end of interrupt, restore registers etc.etc.
   MOV   R0,A
   POP   ACC
   POP   PSW
   RETI

   END

;===============================================================================
