News:

Let's find out together what makes a PIC Tick!

Main Menu

Here is an example TX buffer

Started by TimB, May 11, 2021, 04:18 PM

Previous topic - Next topic

TimB


Hi All

Recently I needed an interrupt driven USART as the TXing of data was impacting my code speed.

In case its of use here it is chopped down in a demo


    Device = 18F13K22

    Declare Xtal = 64

    Declare Float_Display_Type = Fast                                       ' Enable the faster, and more accurate, floating point display library routine
'
' Setup USART1
'
    Declare Hserial1_Baud = 38400                                          ' Set the Baud rate for USART1
    Declare HRSOut1_Pin = PORTC.6                                           ' Setup the pin used for TX on the USART1

'---------------------------------------------------------------------------------
' Setup the config fuses for internal oscillator
'
Config_Start
    FOSC = IRC          ' Internal RC oscillator
    PLLEN = On ;Oscillator multiplied by 4
    PCLKEN = On         ' Primary clock enabled
    FCMEN = Off         ' Fail-Safe Clock Monitor disabled
    IESO = Off          ' Oscillator Switchover mode disabled
    PWRTEN = On         ' PWRT enabled
    BOREN = SBORDIS     ' Brown-out Reset enabled in hardware only (SBOREN is disabled)
    BORV = 19           ' VBOR set to 1.9 V nominal
    WDTEN = Off         ' WDT is controlled by SWDTEN bit of the WDTCON register
    WDTPS = 32768       ' Watchdog Timer Postscale. 1:32768
    HFOFST = Off        ' The system clock is held Off until the HFINTOSC is stable.
    MCLRE = On          ' MCLR pin enabled, RA3 input pin disabled
    STVREN = On         ' Stack full/underflow will cause Reset
    LVP = Off           ' Single-Supply ICSP disabled
    BBSIZ = Off         ' 512W boot block size
    XINST = Off         ' Instruction set extension and Indexed Addressing mode disabled
    Debug = Off         ' Background debugger disabled, RA0 and RA1 configured as general purpose I/O pins
    Cp0 = Off           ' Block 0 not code-protected
    CP1 = Off           ' Block 1 not code-protected
    CPB = Off           ' Boot block not code-protected
    CPD = Off           ' EEPROM not code-protected
    WRT0 = Off          ' Block 0 not write-protected
    WRT1 = Off          ' Block 1 not write-protected
    WRTC = Off          ' Configuration registers not write-protected
    WRTB = Off          ' Boot block not write-protected
    WRTD = Off          ' EEPROM not write-protected
    EBTR0 = Off         ' Block 0 not protected from table reads executed in other blocks
    EBTR1 = Off         ' Block 1 not protected from table reads executed in other blocks
    EBTRB = Off         ' Boot block not protected from table reads executed in other blocks
Config_End

    On_Hardware_Interrupt    GoTo USART_INTERRUPT
   
   
GoTo Main

    $define InterruptsOFF   INTCONbits_GIE = 0
    $define InterruptsOn   INTCONbits_GIE = 1
   
    Dim bTXStartBuffPntr As Byte
    Dim bTXEndBuffPntr As Byte
    Dim cTX1BuffSize As 30
    Dim aTX1RingBuffer[cTX1BuffSize] As Byte
   
    Dim bInterruptTemp As Byte

; Code for the buffered HRSOUT replacement
    #Disable HRSOut

__hrsout1__: 

    InterruptsOFF   
    bInterruptTemp = WREG
    aTX1RingBuffer[bTXEndBuffPntr] = bInterruptTemp       ; Load the data into the buffer           
    Inc bTXEndBuffPntr                               ; Inc the pointer
    If bTXEndBuffPntr >= cTX1BuffSize Then           ; Check for roll round
        bTXEndBuffPntr = 0
    EndIf
    PIE1bits_TXIE = 1                                ; Always enable TX interrupts
    InterruptsOn
   
    Return
   


;*=============================================================*
;   Usart interrupt
;   Hanldes the TXing of data
;*=============================================================*

USART_INTERRUPT:
    Context Save
 
        ; Code to send the data from the TX buffer
       
    If PIE1bits_TXIE = 1 Then                                              ; Only do the rest if the TX1 interrupts are enabled, its our way to control the system
        If PIR1bits_TXIF = 1 Then                                          ; And ff this is a TX interrupt 
         
            TXREG = aTX1RingBuffer[bTXStartBuffPntr]                      ; Take the data from the array at the start pointer and send it out
            Inc bTXStartBuffPntr                                          ; Move the pointer to the next byte in the buffer               
           
            If bTXStartBuffPntr >= cTX1BuffSize Then                      ; Check and handle roll round
                bTXStartBuffPntr = 0
            EndIf

            If bTXStartBuffPntr = bTXEndBuffPntr Then                     ; If all the data is sent turn of TX interrupts
                PIE1bits_TXIE = 0
            EndIf                                                               
        EndIf
    EndIf

  Context Restore
 


Proc Initialise()
   
    OSCCON  = %01110000                                             ; Set the internal OSC to 64mhz
    OSCCON2 = %00000100
    OSCTUNE = %01000000   
   
    DelayMS 250                                                     ; Delay for the osc to stabalise
   
    bTXStartBuffPntr = 0                                             ; Make sure the ring buffer pointers are zeroed
    bTXEndBuffPntr = 0

    INTCONbits_PEIE = 1                                             ; Enable interrupt prioritys
    INTCONbits_GIE = 1                                              ; Turn on interrupts
   
EndProc


    Dim fMyFLoat As Float
   
   
Main:
   
    Initialise()
    fMyFLoat = 1.2345
   
    Do   
        HRSOutLn Dec4 fMyFLoat
        HRSOutLn "This is a test!"
       
        DelayMS 250
    Loop
   

John Drew

Good one Tim,
How about putting it in the WIKI?
I'll do the same with my modified version of Les's read buffer. Make a nice pair.
Cheers
John

Giuseppe

Thanks TimB for sharing your code

top204

#3
Excellent use of the compiler Tim.

If I ever get the time to write a book for the compilers, I'll add a section that covers how to circumvent the internal libraries of the compiler and create new BASIC code ones, and the constants it brings out in the main program for timings and pins used and if the command is in the program's listing etc...

Some of you may have noticed in some of the libraries I have created, the #IfSym and #IfnSym and #EndIfSym directives and the _SYSCOM_xxxxxx constants they check? These are simplified, compiler only, directives and are something that the compiler brings into the program's listing to select if a Pin has been Declared, and what Port.Pin it is, or Baud, or if a particular command has been used in the program etc, so the new BASIC code library can enable or disable or manipulate the code, as the internal library does.

This is not information for the compiler manuals because it is too low level and if not used with care can actually cause problems in code operation. It is for a seperate book for "Advanced Compiler Usage". I created the mechanism so I could create library code for new devices, and test it thoroughly, before implementing it in the compiler's source code.

tumbleweed

The problem I've always had with using TX interrupts is you have to anticipate the largest total size of the messages you have to queue up and allocate a buffer that large.

__hrsout1__ should probably check to make sure there's room in the buffer before adding new chars, and would have to wait if it's full.

Right now, the only thing that's saving your bacon in the example 'do-loop' is the 'delayMS 250' between sending the 20+ chars.

top204

#5
With buffers, as long as the data is being sent out or read in faster, or the same speed, that the code is operating, they will rotate around themselves. Buffer over-run is always a problem, even on mainframe computers.

When this is a possibility, I always produce code that has multi-buffer use, so while one is being filled, the others are being read or written etc... However, with the new devices, they have 8K and more of RAM, so teh buffer can be quite large, and the buffer array made a Heap type so it sits above all the program's normal variable RAM because it is being accessed indirectly.

I've had to do multi-buffering a few times with audio from a FAT system because the time it takes to find and read the sectors of a file cause clicks in the output audio, so a multi-buffer mechanism stops this. Also, if writing to slow memory, a multi-buffer mechanism is a must so that while one buffer is writing to the slow device, the other buffers are being filled with data from the program. Then they are rotated with a flag variable when ready for new data.

TimB

Quote from: tumbleweed on May 12, 2021, 11:28 AM__hrsout1__ should probably check to make sure there's room in the buffer before adding new chars, and would have to wait if it's full.


The example was deliberately written to be as simple as possible. I could have implemented a check to see if the tail was about to overrun the nose. But if you get yourself into a situation where you could then the buffer is not doing its job. The whole point was to prevent you waiting to send data and missing other important work.

Do some mental maths to think about what you intend to send add a bit then alter one line "Dim cTX1BuffSize As 30"
Change the size of the buffer to cover all bases.

Tim

 

tumbleweed

I appreciate that you're trying to make it simple.
The trouble is that it's not, and if the user doesn't realize the limitations then it's going to cause more problems.

The example loop should probably read more like
   Do  
        HRSOutLn Dec4 fMyFLoat
        HRSOutLn "This is a test!"
      
        // wait here long enough to ensure that all the characters you wanted to send
        // have been transmitted or else things are going to go south fast
        // make sure you keep track of the total numbers of chars and never send
        // more than 30 at a time without waiting.
        // you can keep making the output buffer larger, but if this part of the code is
        // faster than the HRSOut statements above it will eventually fail.
    Loop

TimB


The send loop is not part of the code. It was just to demo it.

I have faith that 99% of Positron coders are capable of figuring out that if you over fill a buffer it will cause issues.

As I said it was written to be as simple as possible. No obscuring the way it works. People can build on it.

I could if wanted add code to fill the buffer with "buffer overrun" if it gets too full  ;)

top204

Totally agree Tim.

It is simply showing the mechanism behind making a buffered USART transmitter, and if buffer over-run is to be eliminated by the user, it takes a few comparisons to wait for the buffer to have some space before the data gets entered into it.




keytapper

#10
Quote from: TimB on May 11, 2021, 04:18 PMHi All
Recently I needed an interrupt driven USART as the TXing of data was impacting my code speed.
In case its of use here it is chopped down in a demo

Back to my keyboard, I got triggered of this idea and I'm trying to implement it, as it is for the RX.
I'm stuck to set the HSerIn entry point, because when I set __hrsin1_with_timeout__ the compiler will complain about duplicated label.

Probably would be nicer to use a subroutine instead, but I'm willing to learn some wizardry  ;D about how to overload a standard command, as I could see for printing on a I2C bus.

EDIT

I finally read some include, there's one HSerIn interrupt driven, but for 18F MCU. Perhaps I'll hack it a bit for 16F enhanced.
Ignorance comes with a cost

top204

Add the line below to the start of the program's listing. It will stop the compiler's library subroutines for USART1 being created, so they can be replaced in the main program:

#Disable HRSIn, HRSInTo, HRSIn_RCREG_Read, HRSOut              ' Disable the compiler's library subroutines for HRsin, HRsout, HSerin and HSerout


keytapper

Thank you,
I found the included examples of the compiler. Just it was mentioned applicable for 18F MCU. So for my purpose I'll keep an eye what may happen to a 16F.
Ignorance comes with a cost

top204

If it is an enhanced 14-bit core device, it can be done, but taking the mechanism that makes indirect linear RAM into account on them, as the compiler does with its indirect commands and functions with the enhanced 14-bit core devices.

However, if it is a standard 14-bit core device, it will not be very efficient because of the very fragmented RAM they have. So an array may stray into another RAM bank and the low-level code for loading and reading it using the FSR register needs to cater for the RAM banks and a calculation as to where in RAM a section of the array is located, as the compiler does with its array access mechanisms on the standard 14-bit core devices. It works well, but is not very efficient because of their architecture.

keytapper

Quote from: top204 on Apr 17, 2024, 09:43 AMIf it is an enhanced 14-bit core device, it can be done, but taking the mechanism that makes indirect linear RAM into account on them, as the compiler does with its indirect commands and functions with the enhanced 14-bit core devices.

I made it!!!
Greatly appreciated your directions.
#Disable HSerIn, Hrsinto, Hrsin_Rcreg_Read

Dim GEN               As Byte System
Dim GENH              As Byte System
Dim PP0               As Byte System
Dim PP0H              As Byte System
Dim PP1               As Byte System
Dim PP1H              As Byte System
Dim PP7               As Byte System
Dim PP7H              As Byte System
Dim idx_In            As Byte System
Dim idx_Out           As Byte System
Dim FSR1_Save         As Word System
Dim Timeout_Int       As GEN
Dim Timeout_IntH      As GENH
Dim Timeout_Value     As Timeout_Int.Word
Dim FSR0_Save_Int     As PP0
Dim FSR0_Save_IntH    As PP0H
Dim FSR0_Save         As FSR0_Save_Int.Word
Dim Inside_Loop_Int   As PP1
Dim Inside_Loop_IntH  As PP1H
Dim Inside_Loop       As Inside_Loop_Int.Word
Dim Outside_Loop_Int  As PP7
Dim Outside_Loop_IntH As PP7H
Dim Outside_Loop      As Outside_Loop_Int.Word

#ifSym __SYSCOM_HRSIN1_TO_REQ_
__Hrsin1_with_Timeout__:
    Outside_Loop = Timeout_Value
    Clear Inside_Loop
Getbuf_Outside:
    DelayCS 2
Getbuf_Inside:
    DelayCS 1
    Movf      idx_In,w
    Subwf     idx_Out,w
    Bnz       GetByte
    WREG = 255
    Addwf     Inside_Loop,f
    Addwfc    Inside_LoopH,f
    Addwfc    Outside_Loop,f
    Addwfc    Outside_LoopH,f
    Btfss     STATUSbits_C
    Ret
    Incfsz    Inside_Loop,w
    Bz        skip_it
    Incfsz    Inside_LoopH,w
skip_it:
    Bra       Getbuf_Outside
    Inside_Loop = ((59 * Xtal) / 4)
    Bra       Getbuf_Inside
#endIfSym
#ifSym __SYSCOM_HRSIN1_REQ_
__Hrsin1__:
    While idx_In = idx_Out
        #ifdef Watchdog_Req
        Clrwdt
        #endif
    Wend
#endIfSym
#ifSym __SYSCOM_HSERIAL1_HELPERS_REQ_
GetByte:
    Inc  idx_Out
    If  idx_Out >= RXBUFSIZE Then
        Clear  idx_Out
    EndIf
    FSR0_Save = FSR0
    FSR0 = AddressOf(rxbuffer)
    FSR0 = FSR0 + idx_Out
    WREG = INDF0
    PP0 = WREG
    FSR0 = FSR0_Save
    Set STATUSbits_C
    Return
#endIfSym
I sorry to say that I opted for a different approach, because even procedures can accomplish my intent.
Ignorance comes with a cost