News:

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

Main Menu

Free Bootloader for 18F27Q43

Started by JonW, Today at 08:03 PM

Previous topic - Next topic

JonW

Hi All

I'm working on a 27Q43 Dev for Charlie and thought a bootloader would be a great
addition, so I have ported Les's 18F26K40 Tiny bootloader code over to the 18F27Q43 and
added some comments so I don't forget. It's a bit bigger than Les's, partly
because the write structure changed in the Q devices and partly because my coding
isn't as polished, but it's 404 bytes on a 128kB chip so I'm not losing sleep.

The protocol side needed nothing at all. The handshake, the block format and the
checksum are Les's, untouched. Everything behind them had to be rewritten,
because the Q devices dropped the EECON style NVM completely. TBLPTR becomes
NVMADR, NVMCON2 becomes NVMLOCK, the control bits become a 3 bit command in
NVMCON1, and WR becomes GO in NVMCON0, bit 0 not bit 7, which caught me out. The
K40 lets you drop each byte into the write latches as it arrives off the wire and
do one erase and write per block. The Q43 has no latches to dribble into, so the
block gets buffered in RAM and then written. I used word writes with NVMCMD 'b100,
write and post increment, so the address only gets loaded once per block and the
loop just feeds NVMDAT. There is a page write path as well but it needs the
address of the buffer RAM bank, and the datasheet only describes that as "the
bank following the last occupied GPR bank", which isn't something I fancied
betting a working bootloader on.

Now the bit that actually matters, and I think the reason nobody has done this
since the request went up in 2020.

Les's 264 bytes reserved at the top looks like a round number and it isn't. It's
8 bytes for the relocated reset instructions plus four 64 byte erase rows, and
that puts the 8 byte slot at 0xFEF8, in the row below the code at 0xFF00. Tiny
sends those relocated instructions as an ordinary write block, so the bootloader
gets asked to erase and program the page holding that slot while it is running a
few bytes away. With Les's arrangement the erase costs nothing but the slot.

I didn't spot that. I reserved 1024, being a nice multiple of the Q43's 256 byte
erase page, which puts the slot at the head of a page the code also lives in.
First upload, the bootloader erased the page it was executing from. The CPU
stalls during an erase and comes back fetching from flash that isn't there any
more, so it dies mid instruction and nothing after that happens. No error,
nothing hangs, and the part still enumerates next power up because the reset
vector and the second half of the bootloader survive. It simply never finishes an
upload. The read back told the story straight away, application written fine,
bootloader tail intact, first 256 bytes blank.

So the rule is reserved size = 8 plus a whole number of erase units, with the
slot in the unit below the code. On the Q43 that means 264, 520, 776 and so on,
never a round multiple of 256. 520 is the smallest that fits, slot at 0x1FDF8,
code at 0x1FE00.

piccodes.ini:

$14, C, 18F27Q43 128Kb PROGMEM & 1024B EE, $20000, $400, 520, 128,

The $13 entry already in there for the 47Q43 has identical geometry, so you can
use that and leave the file alone if you prefer.

One thing worth knowing that the source comment doesn't tell you. Les's four Nops
are annotated as the first four mnemonics from the user program, but Tiny doesn't
copy them, it builds the slot: two NOPs then a constructed GOTO to the
application entry. So the exit jump has to go to the start of the slot, not the
last word in it.

I've also added a check that refuses any block that would touch the bootloader's
own code. It does nothing on a working setup, every legitimate block passes, but
the placement is only correct if two numbers in two different files agree and
nothing checks that they do. With the check, a wrong number is a failed upload
instead of a dead part. On the K40 it would be redundant and I'm not suggesting
Les needs it, it's purely for the Q erase geometry.

It does flash and EEPROM. EEPROM is DFM at 0x380000, byte writes, erase before
write is automatic. Config writes are consumed and acknowledged but deliberately
not written. A bad fuse word out of a bootloader needs the PICkit to recover
anyway, and since the bootloader's own fuses are the ones in force, your
application's Config_Start block has to agree with the bootloader's rather than
being applied.

A few other Q43 things that cost me time and might save you some:

MVECEN = OFF, or nothing at 0x0008 ever runs. The Q43 wants the vectored table by
default and Positron puts the handler on the legacy vector.

ANSELx powers up all ones, so the UART RX pin is dead until you clear it. That on
its own will make a perfectly good bootloader invisible to the PC.

U1RXIF is PIR4 bit 0 but TMR0IF is PIR3 bit 7, they aren't together. The
bootloader polls U1FIFO.RXBE instead and sidesteps the question.

PPS output doesn't override TRIS on this part like it did on the K40.

And Les, worth a look when you get a minute, the .ppi has the fuse as
RSTOSC = HFINTOSC_64MHZHFINTOSC, which looks like the generator has glued the
name onto the front of the description. The assembler wants HFINTOSC_64MHZ and
throws Error[176] on the long one. Same story on HFINTOSC_1MHZ.

Tested on the bench, flash and EEPROM both verified against a PICkit read back,
application uploaded and running. Happy to post the source if anyone wants it.

The zip below contains the bootloader source, a demo program that's been tested on hardware with EEPROM writes, and the full tiny bootloader files with the PICCODES already edited and ready to go.

Enjoy  ;)


Thanks to Les for the original and for the compiler; none of this would exist
otherwise.


'****************************************************************************
'*  Multi-Tiny+ Serial Bootloader for a PIC18F27Q43 device.                 *
'*                                                                          *
'*  Derived from Les Johnson's 18F26K40 bootloader.  The TinyMultiBootloader+*
'*  protocol (family "C") is preserved byte for byte - only the NVM back    *
'*  end and the UART are rewritten, because the Q43 shares neither with     *
'*  the K40.                                                                *
'*                                                                          *
'*  Board  : MWAVE 27Q43 rev 1.0, CP2102 on RC6/RC7                         *
'*  Clock  : 64MHz internal, straight from the RSTOSC fuse                  *
'*  Baud   : 115200 (see cBaudDivisor for 19200)                            *
'*                                                                          *
'*  piccodes.ini entry this firmware expects:                               *
'*    $14, C, 18F27Q43 128Kb PROGMEM & 1024B EE, $20000, $400, 520, 128,    *
'*                                                                          *
'*  Writes PFM and EEPROM.  Configuration writes are consumed and ignored   *
'*  by design - a bad fuse word written from here leaves the board needing  *
'*  the PICkit to recover.                                                  *
'****************************************************************************
    Declare Warnings = Off
    Device = 18F27Q43
    Declare Xtal = 64
    Declare Library_Core = Off                  ' No compiler library core - everything here is explicit

$define cIdTypePIC      0x14                    ' Must match the piccodes.ini ID
$define cMax_Flash      _code                   ' 131072 bytes
$define cEepromSize     _eeprom                 ' 1024 bytes
$define cEepromBaseU    0x38                    ' DFM lives at 0x380000

'---------------------------------------------------------------------------
' Bootloader placement.  Get this wrong and the bootloader erases itself.
'
' TinyMultiBootloader+ moves the application's reset instructions up to the
' reserved address and sends them as an ordinary write block.  So the
' bootloader WILL be asked to erase and program the page containing that
' slot.  The slot must therefore sit in an erase page that holds nothing
' else of the bootloader.
'
' Les's 264 bytes on the K40 is exactly that, not a round number:
'     264 = 8 (the relocated instructions) + 256 (four 64-byte K40 rows)
' which puts the slot at 0xFEF8, in the row BELOW the code at 0xFF00.
'
' Same arrangement here, with the Q43's 256-byte page:
'     code slot  0x1FDF8 .. 0x1FDFF    - 8 bytes, tail of page 0x1FD00
'     boot code  0x1FE00 .. 0x1FFFF    - two whole pages, 512 bytes
'     reserved   520 bytes total       - this is the piccodes.ini figure
'
' Erasing page 0x1FD00 now costs nothing but the slot itself.
'
' Legal sizes are 264, 520, 776, 1032 ... - 8 plus whole erase pages.  The
' body is around 380 bytes with the guard, so 264 (256 of code) is too
' small and 520 is the smallest that fits.  That leaves roughly 130 bytes
' spare: check the end address in the listing after any change, because
' overrunning 0x1FFFF is the one mistake here the assembler cannot catch
' for you in a way that matches what piccodes.ini has been told.
'---------------------------------------------------------------------------
$define cErasePageSize  256                     ' bytes (128 words)
$define cBootCodeStart  $eval (cMax_Flash - 512)        ' 0x1FE00, page aligned
$define cBootVectorSlot $eval (cBootCodeStart - 8)      ' 0x1FDF8
$define cBootloaderSize $eval (cMax_Flash - cBootVectorSlot)   ' 520
$define cBootCodeLowWord 0xFE00                 ' low 16 bits of cBootCodeStart
$define cBootCodeUpper   0x01                   ' upper byte of cBootCodeStart

'---------------------------------------------------------------------------
' Baud rate divisor for U1BRG, with U1BRGS = 1 (4 samples/bit):
'   Baud = Fosc / (4 * (U1BRG + 1))
'
'   115200 -> 138 gives 115107.9  (-0.08%)
'     19200 -> 832 gives  19207.7 (+0.04%)   <- needs the 16-bit BRG, see below
'
' With U1BRGS = 0 (16 samples/bit): Baud = Fosc / (16 * (U1BRG + 1))
'     19200 -> 207 gives  19230.8 (+0.16%)   <- same value Les used on the K40
'---------------------------------------------------------------------------
$define cBaudDivisor    138                     ' 115200 at 64MHz, BRGS = 1
$define cBaudHighSpeed  1                       ' U1BRGS bit

'---------------------------------------------------------------------------
' NVMCMD codes - NVMCON1[2:0].  DS40002147, Table 10-4.
'---------------------------------------------------------------------------
$define cNVM_NoOp       0x00                    ' also "read", harmless at rest
$define cNVM_WriteInc   0x04                    ' write + post increment NVMADR
$define cNVM_ErasePage  0x06                    ' erase the 256-byte page

'---------------------------------------------------------------------------
' Variables
'---------------------------------------------------------------------------
    Dim bWriteBuffer[128] As Byte               ' one transfer block
    Dim bCRC             As Byte                ' running checksum
    Dim bByteCount       As Byte                ' bytes still to receive
    Dim bBlockSize       As Byte                ' saved copy of the above
    Dim bWordCount       As Byte                ' words still to program

    Dim bFlags           As Byte                ' the upper address byte
    Dim tEepromWrite     As bFlags.6            ' set if EEPROM is the target
    Dim tConfigWrite     As bFlags.7            ' set if Config is the target

    Dim bLastPageU       As Byte                ' \ page already erased,
    Dim bLastPageH       As Byte                ' / as NVMADR[21:8]
    Dim wGuard           As Word                ' low 16 bits of the block address

    Dim bTimeoutCounter1 As Byte                ' \
    Dim bTimeoutCounter2 As Byte                ' |  timeout on a received byte
    Dim bTimeoutCounter3 As Byte                ' /

    Dim wFSR0 As FSR0L.Word                     ' 8-bit FSR0L\H as one 16-bit SFR

'---------------------------------------------------------------------------
' Reset vector.
'
' TinyMultiBootloader+ relocates the application's first four instruction
' words up to cBootloader_Address and replaces the reset vector with this
' jump.  On timeout the bootloader jumps back to those four words, which
' carry the application into its own start-up.
'---------------------------------------------------------------------------
    GoTo BootloaderStart

    Org cBootVectorSlot                         ' 0x1FDF8
    Nop                                         ' \
    Nop                                         ' | the application's original
    Nop                                         ' | first four instructions
    Nop                                         ' /
                                                ' 8 bytes exactly, so the label
                                                ' below lands on 0x1FE00
BootloaderStart:
'---------------------------------------------------------------------------
' Port and UART1.
'
' The Q43 powers up with every ANSELx bit set.  RC7 must be digital or the
' receiver sees nothing - this alone is enough to make the PC application
' report "no device found" while the rest of the firmware runs perfectly.
'
' Unlike the K40, enabling the Q43's UART does NOT take the pins over from
' TRIS, so both TRIS bits are set explicitly.
'---------------------------------------------------------------------------
    ANSELC   = 0                                ' PORTC all digital
    TRISC.6  = 0                                ' TX out
    TRISC.7  = 1                                ' RX in

    PPS_Unlock()                                ' PPS1WAY = OFF, so this is repeatable
    U1RXPPS = ((2 << 3) | 7)                    ' RC7 -> U1RX  (port C = 2, pin 7)
    RC6PPS  = PPS_Fn_TX1                        ' U1TX -> RC6

    U1BRGL  = cBaudDivisor                      ' \ baud generator
    U1BRGH  = 0                                 ' /
    U1CON0  = ((cBaudHighSpeed << 7) | (1 << 5) | (1 << 4))  ' BRGS, TXEN, RXEN, MODE = async 8-bit
    U1CON1  = (1 << 7)                          ' U1ON - serial port on

    NVMCON1 = cNVM_NoOp                         ' keep NVMCMD clear when idle
    bLastPageU = 0xFF                           ' \ no page erased yet -
    bLastPageH = 0xFF                           ' / 0xFFFF is not a real page

'---------------------------------------------------------------------------
' Handshake.  The PC sends $C1; we answer with the piccodes ID.  Anything
' else, or silence, and we hand over to the application.
'---------------------------------------------------------------------------
    GoSub ReceiveByteInto_WREG
    If WREG <> 0xC1 Then GoTo Bootloader_Exit
    U1TXB = cIdTypePIC

'---------------------------------------------------------------------------
' Main protocol loop.  Identical in shape to the K40 version:
'
'   ->  "C"  (ready) or "N" (checksum failed, resend)
'   <-  upper address / flag byte
'   <-  high address byte
'   <-  low address byte
'   <-  byte count
'   <-  byte count data bytes
'   <-  checksum byte (all received bytes must sum to zero, modulo 256)
'---------------------------------------------------------------------------
    Do
        Clrwdt
        WREG = "C"                              ' ready for the next block
ContinueLoop:
        U1TXB = WREG
        bCRC = 0

        GoSub ReceiveByteInto_WREG              ' upper address byte
        bFlags  = WREG                          ' bits 6 and 7 are the target flags
        NVMADRU = WREG
        GoSub ReceiveByteInto_WREG              ' high address byte
        NVMADRH = WREG
        GoSub ReceiveByteInto_WREG              ' low address byte
        NVMADRL = WREG

        GoSub ReceiveByteInto_WREG              ' how many data bytes follow
        bByteCount = WREG
        bBlockSize = WREG                       ' keep a copy, the loop below eats it

        '-------------------------------------------------------------------
        ' Collect the whole block in RAM before touching flash.  The K40
        ' could dribble bytes straight into the write latches as they
        ' arrived; the Q43 cannot, because the page erase has to happen
        ' before the first word is programmed and the erase would destroy
        ' anything already written.
        '-------------------------------------------------------------------
        wFSR0 = AddressOf(bWriteBuffer)
RcvOct:
        GoSub ReceiveByteInto_WREG
        POSTINC0 = WREG
        Djnz bByteCount, RcvOct

        GoSub ReceiveByteInto_WREG              ' checksum byte
        If bCRC <> 0 Then                       ' every byte summed must give zero
            WREG = "N"                          ' bad block - ask for a resend
            GoTo ContinueLoop
        EndIf

        '-------------------------------------------------------------------
        ' Dispatch on the flag bits.  Config writes are deliberately dropped
        ' but still acknowledged, so the PC application completes normally.
        '-------------------------------------------------------------------
        If tConfigWrite = 1 Then Continue       ' ignore Config, ask for the next block
        If tEepromWrite = 1 Then GoTo Write_Eeprom

'---------------------------------------------------------------------------
' Program Flash Memory.
'
' First, the self-preservation check.  The placement above is arranged so
' that no legitimate block ever reaches the bootloader's own code, but
' geometry alone is a thin thing to bet the board on: one wrong figure in
' piccodes.ini and the first erase takes out the page the CPU is currently
' fetching from, mid-instruction, with no way back except the PICkit.
'
' So any block that would touch 0x1FE00 or above is dropped and still
' acknowledged.  The legitimate relocation block at 0x1FDF8 is 8 bytes long
' and ends exactly AT 0x1FE00, which is why the test is > and not >=.
'---------------------------------------------------------------------------
        If NVMADRU >= cBootCodeUpper Then
            wGuard.Byte0 = NVMADRL
            wGuard.Byte1 = NVMADRH
            If NVMADRU > cBootCodeUpper Then Continue       ' past the top of flash
            If wGuard >= cBootCodeLowWord Then Continue     ' starts in the bootloader
            If (wGuard + bBlockSize) > cBootCodeLowWord Then Continue  ' runs into it
        EndIf

'---------------------------------------------------------------------------
' Erase is per 256-byte page and the transfer block is 128 bytes, so two
' blocks land in each page.  Erasing on every block would wipe the first
' half when the second arrived, so the page currently erased is remembered
' and the erase only happens when the incoming address crosses into a new
' one.  This relies on the PC application sending blocks in ascending
' address order, which it does.
'---------------------------------------------------------------------------
        If NVMADRU <> bLastPageU Then GoTo Do_Erase
        If NVMADRH <> bLastPageH Then GoTo Do_Erase
        GoTo Write_Flash
Do_Erase:
        bLastPageU = NVMADRU
        bLastPageH = NVMADRH
        NVMCON1 = cNVM_ErasePage                ' NVMADR[7:0] is ignored by the erase
        GoSub NVM_Unlock_And_Go

Write_Flash:
        '-------------------------------------------------------------------
        ' Word writes with post-increment: NVMADR was set from the block
        ' header and steps on by two after every word, so the address only
        ' has to be loaded once.
        '-------------------------------------------------------------------
        bWordCount = bBlockSize >> 1            ' two bytes per program word
        wFSR0   = AddressOf(bWriteBuffer)
        NVMCON1 = cNVM_WriteInc
Flash_Word_Loop:
        NVMDATL = POSTINC0                      ' low byte of the word
        NVMDATH = POSTINC0                      ' high byte of the word
        GoSub NVM_Unlock_And_Go
        Djnz bWordCount, Flash_Word_Loop
        NVMCON1 = cNVM_NoOp                     ' park the command register
        Continue

'---------------------------------------------------------------------------
' Data Flash (EEPROM).
'
' The DFM is byte-wide with an automatic erase before each write, so there
' is no page handling at all.  The PC sends the offset in the high and low
' address bytes; the upper byte is the flag byte, so the real 0x38 upper
' address has to be supplied here.
'---------------------------------------------------------------------------
Write_Eeprom:
        NVMADRU = cEepromBaseU                  ' 0x380000 + offset
        wFSR0   = AddressOf(bWriteBuffer)
        NVMCON1 = cNVM_WriteInc
Eeprom_Byte_Loop:
        NVMDATL = POSTINC0
        GoSub NVM_Unlock_And_Go
        Djnz bBlockSize, Eeprom_Byte_Loop
        NVMCON1 = cNVM_NoOp
    Loop

'***************************************************************************
'*  Subroutines                                                            *
'***************************************************************************

'---------------------------------------------------------------------------
' NVM_Unlock_And_Go
'
' Runs whatever command is already sitting in NVMCON1.  The unlock is two
' writes to NVMLOCK followed by setting GO, with interrupts off.  GO is
' set-only - the hardware clears it when the operation finishes, and the
' CPU is stalled for the duration of a flash erase or write anyway.
'
' CHECK THIS ONE IN THE LISTING: press F2 and confirm the compiler has not
' placed anything between the two NVMLOCK writes and the GO. A bank-select
' is expected and harmless, anything else is not.
'---------------------------------------------------------------------------
NVM_Unlock_And_Go:
    Clrwdt
    INTCON0.7 = 0                               ' GIE off for the unlock sequence
    NVMLOCK   = 0x55                            ' \ required unlock
    NVMLOCK   = 0xAA                            ' /
    NVMCON0.0 = 1                               ' GO - start the operation
    Repeat : Until NVMCON0.0 = 0                ' hardware clears it when done
    Return

'---------------------------------------------------------------------------
' ReceiveByteInto_WREG
'
' Blocking receive with a timeout of roughly a second, built from three
' nested byte counters exactly as the K40 version is.  Falls through to
' Bootloader_Exit when nothing arrives.
'
' U1FIFO.RXBE (receive buffer empty) is polled rather than the interrupt
' flag, so this does not care which PIRx the Q43 happens to put U1RXIF in.
' Every received byte is added to the running checksum.
'---------------------------------------------------------------------------
ReceiveByteInto_WREG:
    Clrwdt
    bTimeoutCounter1 = ((_xtal / 2) + 1)        ' 64MHz -> 33, about 1 second
Repeat1:
    bTimeoutCounter2 = 0
Repeat2:
    bTimeoutCounter3 = 0
Repeat3:
    If U1FIFO.1 = 0 Then GotByte                ' RXBE clear = a byte is waiting
    Clrwdt
    Djnz bTimeoutCounter3, Repeat3
    Djnz bTimeoutCounter2, Repeat2
    Djnz bTimeoutCounter1, Repeat1
    GoTo Bootloader_Exit                        ' timed out - run the application

GotByte:
    WREG = U1RXB                                ' reading U1RXB clears U1RXIF
    bCRC = bCRC + WREG
    Return

'---------------------------------------------------------------------------
' Hand over to the application.
'---------------------------------------------------------------------------
' If no application has been loaded yet the slot still holds the four Nops
' this file put there, so execution slides straight back into the bootloader
' and it waits for the PC again.  That is the correct idle behaviour, but it
' does mean "nothing happens" looks identical to "no application present".
Bootloader_Exit:
    NVMCON1 = cNVM_NoOp                         ' make certain no command is armed
    U1CON1  = 0                                 ' U1ON off, release the UART
    GoTo(cBootVectorSlot)                       ' the relocated reset instructions

'***************************************************************************
'*  Configuration fuses                                                    *
'*                                                                         *
'*  These are what the PICkit programs, and the application inherits them  *
'*  unchanged - the bootloader never writes Config.  So they must suit the *
'*  application too, not just this file.                                   *
'*                                                                         *
'*  MVECEN = OFF matches MWAVE_27Q43_START.bas.  If you ever switch the    *
'*  application to the vectored interrupt table you must reflash this      *
'*  bootloader with MVECEN = ON as well.                                   *
'*                                                                         *
'*  WDTE = OFF, unlike Les's K40 file.  With the watchdog on, every        *
'*  application flashed through this bootloader inherits it and must clear *
'*  it. The MCLR button on the board is the recovery path instead.         *
'***************************************************************************
Config_Start
    FEXTOSC  = OFF                  ' No external primary oscillator
    RSTOSC   = HFINTOSC_64MHZ       ' 64MHz HFINTOSC, CDIV 1:1, straight from reset
                                    ' NB: the .ppi prints this as
                                    ' HFINTOSC_64MHZHFINTOSC - its generator
                                    ' concatenated the name with the start of
                                    ' the description. The assembler wants the
                                    ' short form. Same trap on HFINTOSC_1MHZ.
    CLKOUTEN = OFF                  ' CLKOUT function disabled
    PR1WAY   = OFF                  ' PRLOCKED can be set and cleared repeatedly
    CSWEN    = On                   ' Writing to NOSC and NDIV allowed
    FCMEN    = OFF                  ' Fail-Safe Clock Monitor disabled
    MCLRE    = EXTMCLR              ' MCLR pin is MCLR
    PWRTS    = PWRT_64              ' Power-up timer 64ms
    MVECEN   = OFF                  ' Legacy 0x0008 / 0x0018 interrupt vectors
    IVT1WAY  = OFF                  ' IVTLOCKED can be cleared and set repeatedly
    LPBOREN  = OFF                  ' Low-power BOR disabled
    BOREN    = SBORDIS              ' Brown-out Reset on, SBOREN ignored
    BORV     = VBOR_2P45            ' VBOR 2.45V
    ZCD      = OFF                  ' ZCD disabled
    PPS1WAY  = OFF                  ' PPSLOCKED can be set and cleared repeatedly
    STVREN   = On                   ' Stack full/underflow causes a Reset
    LVP      = On                   ' Low voltage programming enabled
    XINST    = OFF                  ' Extended instruction set disabled
    WDTCPS   = WDTCPS_31            ' 1:65536, software control of WDTPS
    WDTE     = OFF                  ' WDT disabled, SWDTEN ignored
    WDTCWS   = WDTCWS_7             ' Window always open, software control
    WDTCCS   = SC                   ' Software control of the WDT clock
    BBSIZE   = BBSIZE_512           ' Boot block 512 words (unused, BBEN is off)
    BBEN     = OFF                  ' Boot block disabled
    SAFEN    = OFF                  ' SAF disabled
    Debug    = OFF                  ' Background debugger disabled
    WRTB     = OFF                  ' Boot block not write protected
    WRTC     = OFF                  ' Config registers not write protected
    WRTD     = OFF                  ' Data EEPROM not write protected
    WRTSAF   = OFF                  ' SAF not write protected
    WRTAPP   = OFF                  ' Application block not write protected
    Cp       = OFF                  ' Code protection disabled
Config_End