Proton BASIC Compiler - Large Buffer Async Receiver Routine

  • Pic® Basic

  • Large Buffer Async Receiver Routine

    A serial buffer of 256 bytes is usually considered plenty big enough. But if a high speed serial link is being used it can be desirable to have a bigger buffer (limited by the RAM size of the device being used of course). The default for Les’s HRSIN buffering code for 8 bit PICs is 64 bytes but it can be up to 255. This article describes a method of receiving asynchronous data (RS232) using a buffer bigger than 255 bytes.

    It might help some members of our community to study the process of interrupt driven serial routines from scratch; they are not complicated but there are several things to consider.

    The USART (or EUSART) will receive serial characters at the chosen speed (say 9600 bits per second). The normal default is one start bit, eight data bits and one stop bit. This gives ten bits per character so at 9600 bits per second, this is 960 characters per second (9600/10). This means that worst case, our interrupt routine will have to handle 960 characters per second, each one taking just over one millisecond. Now this is not usually a problem but if you look at a bit rate of 115200 bits per second, things get a bit tighter. You can work it out but the interrupt routine will be seeing a new character every 87uS. So we need an interrupt routine that can handle this.

    Let’s split the process of receiving serial data and then extracting it into two parts. First the receive. We want to put successive characters (one byte each in size) into a ram-based buffer as they arrive. So let’s define a buffer, say 256 bytes long (there is a neat trick in using buffers that are 256 long as you’ll see in a minute):

    DIM BUFF[256] AS BYTE ; define our data buffer size

    So this buffer will store our async our data. We now want a pointer that will point into this buffer where we are to plant the next char to arrive:

    DIM IN_PTR AS BYTE ;pointer into buffer for received data

    The USART will have a Data Register called something like RCREG. So, when a char arrives and our interrupt is triggered, we do this:

    …...USART interrupt……
    BUFF[IN_PTR] = RCREG ;get char from USART and store in buffer
    INC IN_PTR ;step pointer on to next posn
    ; clear the interrupt and exit

    Simple isn’t it? But you should be worried that the IN_PTR will eventually run off the end of the buffer and start overwriting other things. But it won’t, because the pointer is defined as a BYTE and how many different values can be held in one byte? Correct - 256. So a moments thought should let you realise that the pointer will eventually reach 254, 255 then 0. So it will automatically cycle round to the start of our buffer again. Which means that we don’t have to keep checking if it has reached the end of the buffer. But this ONLY works for a 256 buffer and a BYTE pointer, remember that. (The same would apply to a 16 bit pointer and a 65536 byte buffer but I doubt that would ever be realised.)

    So we have produced a circular input buffer that is pretty simple to code and won’t take many machine cycles of interrupt code. Now let’s deal with the other half of the problem, getting the data off the buffer so we can do whatever we need to do with it. We need a new pointer:

    DIM OUT_PTR AS BYTE ;take-off pointer

    So to extract the next char from the buffer:

    OUR_DATA = BUFF[OUT_PTR] ;get next char from buffer
    INC OUT_PTR ;step the output pointer

    But wait, this code sits outside the interrupt structure. How will it know that there is one (or more) characters in the buffer? If the IN and OUT pointers are equal, there is no new data in the buffer. So:

    IF IN_PTR != OUT_PTR then ;any new data in buffer? (the in and out pointers are not equal)
    OUR_DATA = BUFF[OUT_PTR] ;take the next char from the buffer
    INC OUT_PTR ;this will cycle round like the input pointer

    And that is all there is to it, more or less. You might be thinking that Les’s routine seems much more complicated but he does a lot more as well. He allows buffer sizes other than 256; he checks to see if the buffer has been overrun because the whole buffer has now been filled due to the user not taking the data out fast enough; he seamlessly integrates his routine into the structure of your program; he provides time-outs so you can issue a call for input and his code will wait while checking for data; and more besides. With his method, he does all the work. With the method described here, you do all the work but you get a bigger buffer than 255. If you want time-outs, you'll have to do it yourself.

    So what if we want a buffer bigger than 256? (I am pretty sure that Les’s P24 version will allow bigger than 256 but here we are still dealing with 8 bit PICs). You might say “use word pointers and everything will be OK”. Well hang on a minute, let’s look at this in more detail:

    DIM IN_PTR AS WORD ;no longer BYTE pointers, now 16 bits long WORDs
    DIM OUT_PTR AS WORD ;…so they can reach a much bigger buffer
    DIM BUFF[1000] AS BYTE

    ..…..usart interrupt……
    BUFF[IN_PTR] = RCREG ;get char from usart and store in buffer
    INC IN_PTR ;step to next buffer posn

    You can see that disaster lurks after 1000 characters have arrived! We need to check for this as follows:

    IF IN_PTR = 1000 THEN IN_PTR = 0 ;reset pointer when we reach the end of the buffer

    Remember that a buffer of 1000 bytes has addresses than run from 0 to 999. So 1000 is the point to reset the pointer, address 1000 must not be written to (if you don’t see why think about it some more). And exactly the same applies to the take-off routine. And that would appear to be that, everything in our garden is now rosy, yes? Well no. Look at the comparison of the input and output pointers:

    ; pointers unequal, so we have new data….

    At first sight this looks OK. But we are comparing two WORDS (2 bytes each). On a 24F PIC that will complete in one instruction cycle and therefore be OK, but on an 8 bit PIC (say 18F series) it will take more than one instruction cycle. Why does this matter? Because while that comparison of the two words is taking place, outside of interrupts, an interrupt could occur from the USART as a character arrives and spoil the comparison. There are a few combinations of things that can go wrong but the result is generally that we miss a character, or worse. Here is the assembler code that the compiler produces for that comparison to illustrate what I mean (for 18F46K22):

    movf out_ptrH,W,0
    subwf in_ptrH,W,0
    btfss STATUS,2,0
    [email protected] _LBL__7
    movf out_ptr,W,0
    subwf in_ptr,W,0
    btfss STATUS,2,0
    [email protected] _LBL__7
    bcf _B#VR1,0,0

    I leave it to you to as an exercise to devise a fool-proof way of checking if new data has arrived. One simple solution is as follows: When the USART interrupt is triggered because a character has arrived, set a flag called GOTDATA (within the interrupt routine). This flag means “there is at least one new character in the buffer”. Problem solved? Well no. We can test this flag in the “outside interrupt” code and process the new character, but how do we reset the flag? It’s no good doing it each time we extract a character from the buffer because another character may have arrived while we were doing something else. We still need that comparison of the input and output pointers. So combining these two things:

    Look at the example code. This shows the process of disabling interrupts for the short time we compare the in and out pointers, then re-enabling interrupts. If they are equal, there is no new data so we can safely clear GOTDATA. This method assumes that interrupts are normally enabled.

    If you want to use both USARTs of a device, simply do all this again but be careful to use different names for the pointers and the buffers.

    I hope this short article has opened your eyes to the process of interrupt driven asynchronous data handling. You can do exactly the same thing for transmission of characters but the other way round. If you know about this already and have spotted any blunders on my part, I would love to hear from you! Good luck.

    Les added a few comments which I include here. Thanks Les:
      1. When implementing code on an 18F device that requires faster operations, the optimiser should be invoked for at least level 2. The optimiser on the 18F devices is 100% safe to use and the code savings are tremendous. The asm code posted will shrink with the optimiser set and the Dead_Code_Remove declare activated.
    ;  Async buffer routine where buffer can be more than 256 bytes.  (Limit is 16384 but this is unlikely to be needed!)
       FOSC = INTIO67                          ;Internal oscillator block, port function on RA6 and RA7
       PLLCFG = On                             ;Oscillator multiplied by 4
       PRICLKEN = On                           ;Primary clock is always enabled
       FCMEN = OFF                             ;Fail-Safe Clock Monitor disabled
       IESO = OFF                              ;Oscillator Switchover mode disabled
       PWRTEN = OFF                            ;Power up timer disabled
       BOREN = OFF                             ;Brown-out Reset disabled in hardware and software
       BORV = 285                              ;VBOR set to 2.85 V nominal
       WDTEN = OFF                             ;Watch dog timer is always disabled. SWDTEN has no effect.
       WDTPS = 1                               ;1:1
       CCP2MX = PORTB3                         ;CCP2 input/output is multiplexed with RB3
       PBADEN = OFF                            ;PORTB<5:0> pins are configured as digital I/O on Reset
       CCP3MX = PORTE0                         ;P3A/CCP3 input/output is mulitplexed with RE0
       HFOFST = OFF                            ;HFINTOSC output and ready status are delayed by the oscillator stable status
       T3CMX = PORTB5                          ;T3CKI is on RB5
       P2BMX = PORTC0                          ;P2B is on RC0
       MCLRE = EXTMCLR                         ;MCLR pin enabled, RE3 input pin disabled
       STVREN = On                             ;Stack full/underflow will cause Reset
       LVP = OFF                               ;Single-Supply ICSP disabled
       XINST = OFF                             ;Instruction set extension and Indexed Addressing mode disabled (Legacy mode)
       Debug = OFF                             ;Disabled
       Cp0 = OFF                               ;Block 0 (000800-001FFFh) not code-protected
         On_Interrupt int        
        Symbol RC2IF  PIR3.5                             ;RX2 int flag
        Symbol RC2IE  PIE3.5                             ;RS232 2 IE
        Symbol CREN   RCSTA2.4                           ;receiver enable 
        Symbol PEIE   INTCON.6                           ;peripheral int enable
        Symbol GIE      INTCON.7                              ;Global Int Enable
        Dim  writeptr  As Word                           ;pointer to put data into buffer
        Dim  readptr   As Word                           ;pointer to take ..   off   ..
        Dim  datachr   As Byte                           ;the next data char from buffer
        Dim  gotdata   As Bit                            ;flag to show that a char has arrived
        Symbol buffsz  1000                              ;wanted size of input buffer
        Dim  data_array[buffsz] As Byte                  ;the latest vsn of the compiler allows big values (vsn, earlier vsns may not?
    ; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Starts here~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                readptr = 0                              ;it is vital to ensure pointers are zero at start
                writeptr = 0
                gotdata = 0
                TXSTA2 = %10100100                       ;clk internal, 8 bit tx, tx enabled, async mode, syn done, BRGH=1, x, x
                RCSTA2 = %10110000                       ;serial port enabled, 16 bit, x, enable rcvr, no addr detect, x, x, x 
                BAUDCON2 = %00001000                     ;x, x, x, o/p idles high, BRG16 is 1, x, x, x
                SPBRG2 = 138                             ;115200  see p.278 of datasheet. Gives error of -0.08% for 115200 baud
                SPBRGH2 = 0                              ;top 8 bits of baud rate reg
                PEIE = 1                                 ;enable peripheral ints
                RC2IF = 0                                ;just to be sure that the int flag is clear
                RC2IE = 1                                ;enable interrupts from USART2    receiver        
                GIE = 1                                  ;off we go......
    ; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~Main program~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    ;  blah
    ;  blah
       If gotdata = 1 Then                               ;is there at least 1 new byte?
        GoSub get_chr                                    ;result comes back in datachr
        TXREG2 = datachr                                 ;echo the char back by putting it in the USART transmit buffer tp prove we're working ok           
    ;  blah
    ;  blah
    get_chr: datachr = data_array[readptr]               ;get first new byte
             Inc readptr                                 ;step take-off pointer
             If readptr = buffsz Then readptr = 0        ;reset pointer if end of buffer
             GIE = 0                                     ;stop ints
             If readptr = writeptr Then gotdata = 0      ;clear flag if no more data in buffer
             GIE = 1                                     ;re-enable interrupts
    ; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Interrupt rtn~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    int:      Context Save                               ;interrupt has happened...... assume there is only one source: USART RX
               If RC2IF = 1 Then                         ;usart has received a byte
                 data_array[writeptr] = RCREG2           ;plant new byte in buffer
                 Inc writeptr                            ;writeptr runs from 0 to 999
                 If writeptr = buffsz Then writeptr = 0  ;reset write ptr when beyond end of buffer
                 gotdata = 1                             ;signal new data (may be more than 1 but will be ok)
                 RC2IF = 0                               ;clear the USART interrupt flag       
             Context Restore                             ;restore regs and carry on