Electronic – PIC interrupt based soft UART timing trouble

assemblyinterruptspictimeruart

I've tried to implement a software UART on a PIC18F452 using TIMER0 interrupts and I can't get the timing working.

I'm using MPLAB ASM for compilation and the PICkit2 for programming.

LIST P=18F452
include <P18F452.inc>
CONFIG WDT=OFF, LVP=OFF, OSC=HS, CP0=OFF

variable cyclesPerBaud=1            ; 1 cycle(s) per baud
variable cyclesInt=0xFFFF           ; counter in 16 bit timer
variable freq = 20000000            ; clock frequency 20Mhz
variable baud = 9600                    ; baud rate
variable cyclesMain=33              ; cycles in Main program branch (intr->checkBitCounter->startStopBit->transfer)
variable cyclesIdle=16              ; cycles in Idle branch (intr->Idle)
variable offset = (freq/(4*baud))/(cyclesPerBaud)
variable initOffset = cyclesInt - offset
variable durrOffset = cyclesInt - offset + cyclesMain
variable hurrOffset = cyclesInt - offset + cyclesIdle

cblock 0x20
    char                    ; character to send
    bitCounter      ; bits left to send
    startStopBit    ; checks to send start of stop bit
    buffer              ; buffer where char is rotated
    cycle                   ; the current cycle in cyclesPerBaud starting high
endc

org 0000h
    goto main
org 0008h
    goto intr

main
bcf OSCCON, SCS     ; use primary clock
bsf RCON, IPEN      ; enable priority levels

; interrupt config:
bsf INTCON, GIEH    ; enable high priority interrupts
bcf INTCON, GIEL    ;   disable low priority interrupts
bsf INTCON, TMR0IE; enable TMR0 interrupts
bcf INTCON, INT0IE; dis. ext. interrupts
bcf INTCON, RBIE    ; dis. rb port change int.
bcf INTCON, TMR0IF; clear the TMR0 intr. flag bit
bcf INTCON, INT0IF; 
bcf INTCON, RBIF    ;

bsf INTCON2, TMR0IP; set TMR0 high priority

; timer 0 config:   
bcf T0CON, TMR0ON   ; temp. disable timer
bcf T0CON, T08BIT   ; 16 bit counter
bcf T0CON, T0CS     ; T0CKI pin input as source
bcf T0CON, T0SE     ; switch on falling edge
bsf T0CON, PSA      ; disable prescaler
bcf T0CON, T0PS2    ; doesn't really matter
bcf T0CON, T0PS1    ; -||-
bcf T0CON, T0PS0    ; -||-

; init output registers and variables
clrf TRISD
clrf LATD
movlw 0xFF
movwf PORTD             ; our output port set to high (serial 0 - but not sure about that)
movlw b'01111111' ; symbol to send
movwf char
movlw cyclesPerBaud
movwf cycle             ; unused
clrf bitCounter     ; zeros
clrf startStopBit   ; zeros
clrf buffer             ; zeros
; eof init 
movlw LOW initOffset    ; dunno why but HIGH has to be reversed with LOW
movwf TMR0H             
movlw HIGH initOffset   
movwf TMR0L             
bsf T0CON, TMR0ON   ; turn on timer

loop 
goto loop                   ; waiting for the     interrupt

intr                ; main branch: 8 cycles
btfss INTCON, TMR0IF
    retfie
bcf T0CON, TMR0ON

; had to comment this section out - didn't want to work with it - dunno why
;   decf cycle, F
;   btfss STATUS, Z
;       goto idle
;   movlw cyclesPerBaud
;   movwf cycle

checkBitCounter             ; all branches until 'tmr_ret': 16 cycles
movf bitCounter, F
btfss STATUS, Z
    goto rotateBuffer

;startStopBit

movf startStopBit, F
btfss STATUS, Z
    goto stopCopy
movlw 0x8
movwf bitCounter
movf char, W
movwf buffer
rlncf buffer, F
movlw 0x00

transfer
movwf PORTD
decf bitCounter, F
btfsc STATUS, Z
    incf startStopBit, F

tmr_ret         ; 5 cycles (+8 from intr = 13 cycles)
movlw LOW durrOffset    ; dunno why but HIGH has to be reversed with LOW
movwf TMR0H             
movlw HIGH durrOffset   
movwf TMR0L 

bsf T0CON, TMR0ON
retfie

stopCopy
clrf startStopBit
movlw 0xFF
nop
goto transfer

rotateBuffer
rrncf buffer, F
movf buffer, W
nop
nop
nop
nop
goto transfer   

idle        ; 5 cycles (+7 from intr = 12 cycles)
movlw LOW hurrOffset    ; dunno why but HIGH has to be reversed with LOW
movwf TMR0H             
movlw HIGH hurrOffset   
movwf TMR0L 

bsf T0CON, TMR0ON
retfie

end

The PIC is sending stuff on PORTD0, as it's supposed to, but it's not the data it's supposed to be sending. When I peak what is transmitted (with Realterm), the byte being sent is 11011111 or 10111111 and sometimes 11111111 instead of 01111111.

Also, since I started using 'variables' in the code I noticed that I had to copy the lower part of timing offsets with HIGH and high bytes with LOW into the TMR0H:L registers – anybody knows why it works that way? Maybe I'm confusing the significance of these registers: I used them before as if the TMR0H register would hold the more significant 8 bits of the 2 byte counter – is that correct?

Best Answer

RECEIVING

The normal approach for implementing a software asynchronous receiver is to have a timer tick that runs continuously at 3x or 5x the baud rate (note: odd numbers are better than even numbers). Watch the input to be low on two consecutive ticks. Once that is observed, start sampling the input on every third tick, until you have sampled it nine more times. If the input is high on the ninth time, you've received a properly-framed character. If it's low on the ninth time, you have a framing error.

* * * S - - 0 - - 1 - - 2 - - 3 - - 4 - - 5 - - 6 - - 7 - - S
-----______000000111111222222333333444444555555666666777777----
--______000000111111222222333333444444555555666666777777-------

The above timing diagram shows how things should work. The top line shows what happens on each timer tick (each non-blank is a timer tick; the * is a tick waiting for the start bit, a S is checking to ensure that the line is still low after having received what looked like a start bit. The numbers 0-7 represent the sampling of bits 0-7, and S is the stop bit. The bottom two lines show the incoming data in the cases where one just barely catches the beginning of a start bit (on the third asterisk), or where one just barely misses catching it on one interrupt (so the line's been low for a third of a bit time by the time the start bit is noticed). Note that even with that level of uncertainty, the source is guaranteed to be sending bit 0 when it's sampled, and likewise for the rest of the bits.

One software approach is to sample the line at the indicated interrupts and ignore it on the ones marked with dashes. An alternative is to blindly sample the line on every interrupt into a 32-bit shift register, using something like:

  rrf   headtail,w
  bcf   _tail,3
  btfsc _INPUTPORT,_INPUTBIT
   bsf  _STATUS,_tail,3
  rrf   buff2,w
  movff buff1,buff2
  movff buff0,buff1
  movwf buff0
  rrf   headtail,f

The last four bits received from the port will be in headtail bits 3..0. The 24 bits before those will be in buff0..buff2, with each one holding every third bit. The bits before that will be in headtail bits 7..4. If one does that, one can check whether headtail holds the bit pattern 00xxxx1x. If it does, copy buff1 to wherever you want the incoming data, copy buff1 to the spot you want the data, OR headtail with 11000011, and full buff0..buff2 with FF. Otherwise do nothing. This approach may be a tiny bit slower than selectively loading or ignoring the data input, but may be better able to recover from framing errors.

TRANSMISSION

Transmission is easier than reception. Arrange for a piece of code to run once per bit time (if your tick is at 3x the baud rate, run the code every third tick). The simplest way to set up the code is to use a couple of bytes to hold the data that should be sent out the port, including start and stop bits. There are a variety of approaches one may use. A rather versatile one would be:

  ; At start of interrupt
  btfss  TransmitBuffL,0
   bcf   OUTPUT
  btfsc  TransmitBuffL,0
   bsf   OUTPUT

  ; Later, after having handled reception:
  btfss  TransmitBitsLeft,7 ; Assume this counts down to -1 (i.e. 255)
   decfsz TransmitTicks,f
   goto  noTransmit ; Transmit if no bits left, or no ticks
  movlw  3
  movwf  TransmitTicks ; Handle bit transmission every third interrupt
  decf   TransmitBitsLeft,f
  rrf    TransmitBuffH,f
  rrf    TransmitBuffL,f
NoTransmit:

One may prepare a byte for transmission when TransmitTicks bit 7 is set. When that is the case, place the bit sequence to be set into TransmitBuffH and TransmitBuffL, set TransmitTicks to the length of the first bit (typically 3), and TransmitBitsLeft to the total length of the word including start and stop bits. Something like:

TxByte: ; Sends byte in W.  Assumes TransmitBitsLeft has high bit set
  movwf   TransmitBuffL,f
  movlw   3
  movwf   TransmitTicks
  movwf   TransmitBuffH,f  ; LSB (stop bit) is set.  Upper bits don't matter.
  movlw   9 ; Total frame length (incl. start and stop) minus one
  movwf   TransmitBitsLeft
  bcf     _STATUS,_CARRY ; Start bit should be low
  rlf     TransmitBuffL,f ; Stick start bit in front of what we're sending
  rlf     TransmitBuffH,f

This approach can send out normal data frames using the style indicated. If one needs more stop bits, one may make sure TransmitBuffH has enough bits set, and increase TransmitBitsLeft appropriately. If one wants to send out a BREAK, one may set TransmitBuffL to 2 and TransmitBitsLeft to 1, set TransmitTicks to the desired length of the break signal (in ticks). To idle the line for a certain length of time, set TransmitBuffL to 1, TransmitBitsLeft to 0, and set TransmitTicks to the desired idle time (in ticks).