??? 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 |