Storing Settings on PIC16F1454

As I was playing with PIC16F1454, I came to the point where some configurability would be in order. You know how it goes with PIC microcontrollers - just write it in EEPROM and you’re good. Unless there is no EEPROM like there is none for PIC16F1454.

Never mind, I had this issue before, so I can just copy my own code (ab)using program memory for the same purpose. Guess what? There are some issue with this too.

The first of all my old code was for different microprocessor. While principle is the same, it’s not an exact match. The second reason was changes to XC8. My old code doesn’t properly compile on XC8 2.00 - they changed how location is defined. The third (and the last) reason is high-endurance flash that PIC16F1454 supports. Unlike normal flash that’s rated for 10K writes, last 128 of this PICs program memory is rated to 100K. Albeit 10K is nothing to frown about, 100K is much nicer - especially if I end up changing data a lot.

Second and third reason share the same fix. Memory definition looks like this:

#define _SETTINGS_FLASH_RAW { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                              0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }
#define _SETTINGS_FLASH_LOCATION 0x1FE0

const uint8_t _SETTINGS_PROGRAM[] __at(_SETTINGS_FLASH_LOCATION) = _SETTINGS_FLASH_RAW;

This will use the last 32 bytes, starting at 0x1FE0. This address is conveniently 32 bytes before end, falling without issues within “the last 128 bytes” high-endurance category. Now, if you need more memory, just make the array bigger and move it more forward. Just remember to do so in 32-byte increments as this is the block size for flash erase operation. If you don’t reserve all that memory, you might end up erasing your code and we wouldn’t want that. I personally never had need for more than 32 bytes of memory (i.e. one flash page) but your use case might differ.

All settings can be held in structure. Here I will have two settings - Address and DisplayHeight:

typedef struct {
    uint8_t Address;
    uint8_t DisplayHeight;
} SettingsRecord;

SettingsRecord Settings;

To read these settings, we just need to copy our reserved data (seemingly in _SETTINGS_FLASH_RAW variable) into the structure:

uint8_t* settingsPtr = (uint8_t*)&Settings;
for (uint8_t i = 0; i < sizeof(Settings); i++) {
    *settingsPtr = _SETTINGS_PROGRAM[i];
    settingsPtr++;
}

Writing is a two step process. It starts by erasing the WHOLE 32-word/byte block. Following that, we get to write each byte separately:

bool hadInterruptsEnabled = (INTCONbits.GIE != 0);
INTCONbits.GIE = 0;
PMCON1bits.WREN = 1;  // enable writes

uint16_t address = _SETTINGS_FLASH_LOCATION;
uint8_t* settingsPtr = (uint8_t*)&Settings;

// erase
PMADR = address;         // set location
PMCON1bits.CFGS = 0;     // program space
PMCON1bits.FREE = 1;     // erase
PMCON2 = 0x55;           // unlock flash
PMCON2 = 0xAA;           // unlock flash
PMCON1bits.WR = 1;       // begin erase
asm("NOP"); asm("NOP");  // forced

// write
for (uint8_t i = 1; i <= sizeof(Settings); i++) {
    unsigned latched = (i == sizeof(Settings)) ? 0 : 1;
    PMADR = address;            // set location
    PMDATH = 0x3F;              // same as when erased
    PMDATL = *settingsPtr;      // load data
    PMCON1bits.CFGS = 0;        // program space
    PMCON1bits.LWLO = latched;  // load write latches
    PMCON2 = 0x55;              // unlock flash
    PMCON2 = 0xAA;              // unlock flash
    PMCON1bits.WR = 1;          // begin write
    asm("NOP"); asm("NOP");     // forced
    address++;                  // move write address
    settingsPtr++;              // move data pointer
}

PMCON1bits.WREN = 0;  // disable writes
if (hadInterruptsEnabled) { INTCONbits.GIE = 1; }

The first and last step is dealing with interrupts. During write interrupts must be disabled. Code will disable them before writing and re-enable them afterward if needed.

Erase is easy enough. Just set FREE bit in the PMCON1 register followed by magic incantation (0x55, 0xAA, WR=1) and wait for a millisecond or two. Do note that NOP instructions are mandatory due to how self-writing program memory works. It’s one of the rare instances where NOP actually serves a purpose in C code.

To write data, process is close enough. Load all the bytes you wish to write using PMADR and PMDAT registers to set address and data. All bytes except the last will have LWLO bit set and will just cause loading of data into latches. The last byte must have LWLO cleared, signaling we’re done with writing. After a millisecond or two, bytes are done.

Two things are slightly curious there. The first one is setting of PMDATH to 0x3F. This value is actually the same as for erased cell and this just means we’re not changing it’s value. Note that upper byte is not the part of high-endurance flash and only 6-bit value (words are 14-bits on this PIC). Thus we really shouldn’t use it. The second strange decision is to start loop from 1 instead of the more conventional 0. This is so that we can determine if we’re at the last byte without substracting one.

In any case, this is all you need to make your program memory work as a storage for your settings.


PS: Procedure is the same on PIC16F1454, PIC16F1455, PIC16F1459, and probably quite a few more.

PPS: Whole code is available in Git repository.

PPPS: There is quite useful application note from Microchip (AN1673A) dealing with high-endurance flash. Their code uses similar but slightly different approach. If you don’t like this code, maybe theirs will tickle your fancy.

Duplicating Non-Reentrant Functions

Illustration

As I was playing with PIC16454 using USB, I kept getting these warnings: Microchip/usb_device.c:277:: advisory: (1510) non-reentrant function "_USBDeviceInit" appears in multiple call graphs and has been duplicated by the compiler

This was due to function being called from both main function and from interrupt handler. Since function could be interrupted at any point in time, this was definitely a problem and compiler did find a valid solution. However, this was a bit suboptimal for my case.

Since I had issue with only a few functions, I decided to make use of Hybrid option is XC8 compiler stack options. With this option warnings were gone. Surprisingly, this also made my code smaller. Hybrid stack compiled into 7181 words while standard Compiled stack was 7398.

If you have reentrancy happening in just a few functions, Hybrid option might be good for you.*


* Some restrictions apply. Please contact your fellow developers if your compile lasts longer than 4 hours.

Case-insensitive ZFS

Don’t.

Well, this was a short one. :)

From the very start of its existence, ZFS supported case-insensitive datasets. In theory, if you share disk with Windows machine, this is what you should use. But reality is a bit more complicated. It’s not that setting doesn’t work. It’s more a case of working too well.

Realistically you are going to be running ZFS on some *nix machine and access it from Windows, it’ll be via Samba. As *nix API generally expects case-sensitivity, Samba will dynamically convert what it shares from the case-sensitive world into the case-insensitive one. If file system is case-insensitive, Samba will get confused and you will suddenly have issues renaming files that differ only in case.

For example, you won’t be able to rename test.txt into Test.txt. Before doing rename Samba will if the new file already exists (step needed if underlying system is case-insensitive) in order to avoid overwriting unrelated file. This second check will fail on case-insensitive dataset as ZFS will report Test.txt exists. Because of this check (that would be necessary on case-sensitive file system) Samba will incorrectly think that destination already exists and not allow the rename. Yep, any rename differing only in case will fail.

Now, this could be fixable. If Samba would recognize the file system is case-insensitive, it could skip that check. But what if you have case-sensitive file system mounted within case-insensitive dataset? Or vice-versa? Should Samba check on every access or cache results? For something that doesn’t happen on *nix often, this would be either flaky implementation or a big performance hit.

Therefore, Samba assumes that file system is case-sensitive. In 99% of cases, this is true. Unless you want to chase ghosts, just give it what it wants.

Using Null in the Face of CS0121

Sometime you might want to use null as a parameter but you get the annoying CS0121.

The call is ambiguous between the following methods or properties…

One could just adjust constructor but that would be an easy way out.

Proper way would be to use default operator. For example, if you want to use null string, you can use something like default(string):

var dummy = new Dummy(default(string));

If we’re using nullable reference types, just add question mark:

var dummy = new Dummy(default(string?));

And this simple trick selects the correct constructor every time.

Changing ZFS Key Location

Back when I was creating my original pool, I decided to use password prompt as my encryption key unlocking method. And it was good. But then I wanted to automate this a bit. I wanted my key to be read of USB drive.

To do that one can simply prepare a new key and point the pool toward it.

dd if=/dev/urandom of=^^/usb/key.dat^^ bs=32 count=1
zfs change-key -o keylocation=file://^^/usb/key.dat^^ -o keyformat=raw Pool

Of course, it’s easy to return it back to password prompt too:

zfs change-key -o keylocation=prompt -o keyformat=passphrase Pool

Simple enough.