| ??? 03/01/06 12:28 Read: times |
#110967 - interrupt driven serial: RFC |
I moved this to a new thread to the main message board as I believe it IS '51-relevant.
For Jon's serial kickstart document, I have written and commented the following routine. As seen in the previous discussion, there have been errors in it, so further scrutiny, discussion, corrections, comments, etc. (also on the comments and the "cleanness" (as it is intended for newbies)) are requested and welcome. Thanks, Jan Waclawek
;===============================================================================
;--- 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
;===============================================================================
|
| Topic | Author | Date |
| interrupt driven serial: RFC | 01/01/70 00:00 | |
| It must be perfect! | 01/01/70 00:00 | |
| I don't believe | 01/01/70 00:00 | |
| Validation | 01/01/70 00:00 | |
| well ... I've looked at it ... | 01/01/70 00:00 | |
| request for comment on comment | 01/01/70 00:00 | |
| let's take this a little at a time ... | 01/01/70 00:00 | |
| OK then give yours | 01/01/70 00:00 | |
| Intel Original | 01/01/70 00:00 | |
| on byte buffer | 01/01/70 00:00 | |
| c version | 01/01/70 00:00 | |
| Coloring | 01/01/70 00:00 | |
| html syntax colouring | 01/01/70 00:00 | |
| to C or not to C | 01/01/70 00:00 | |
| yeah but thats the point | 01/01/70 00:00 | |
| How true for "real C" | 01/01/70 00:00 | |
excellent idea | 01/01/70 00:00 | |
| asm, in the same style | 01/01/70 00:00 | |
| This may not work well ... | 01/01/70 00:00 | |
| How? | 01/01/70 00:00 |



