Tags
I need to store information about pH calibration within the Ladybug Blue Lite’s (LBL) nRF51822 Flash. The values I store include:
- a word for the write check. The write check is a known value the program checks to check if the values for the pH calibration have been stored before – or if the values written are corrupted (this assumes if the write check bytes are corrupted, the bytes holding the pH calibration info is also corrupted). The value I will use is 0x01020304
- a word for the pH 4 millivolts read when the probe is in the pH 4 calibration solution. This should be +/- 415 mV. I cover why in earlier posts on the pH circuit….As well as why calibrating the probe to the LBL circuits is important….
- a word for the pH 7 millivolts read when the probe is in the pH 7 calibration solution. This also should be +/- 415 mV.
- a word to hold the date the calibration was taken. pH probes should be regularly checked. By storing the calibration date, the smartphone can notify the hydro farmer it is time to calibrate.
Unions are a wonderful way to map a bunch-o-bytes to the same memory location as formatted values. Examples where this is useful is storing data to Flash as well as sending data over a connection (e.g.: BLE).
Thanks to Those That Went Before
Ron Sousa provided us with excellent videos within the embedded section of Contextual Electronics that gave me a much more practical view on when to use a Union in firmware.
The Goal
I am sure twiddling data structures is very familiar to most of you. I struggle with setting up Unions and Structs in a way that makes it easiest to access bytes based on different views. The goal of this post is to highlight how a Union (plus structs) can provide easier access to the two most common views: 1) a bunch of sequential bytes that need to go from one place to the other (e.g.: RAM to Flash, a micro controller to a Smartphone…) 2) data types (like a pH value) that map to bytes in memory that most likely need to be swapped in some fashion to go from a bunch of bytes to an int32_t (for example) pH value.
The Data Structure for pH Calibration
Measuring the pH means using a pH probe (and circuitry like that on the LBL) that will not give the “ideal” values. Calculating the pH means mapping mV values to pH values where the ideal step (slope) between two pH values is 59.16mV. Most likely a pH probe will not get exactly 0 mV for pH 7 or 3*59.16 (lower pH values register positive voltage values, pH 4 is 3 away from pH 7) = 177.5mV for pH 4. Once the actual values of a pH probe’s reading when the probe is emerged in a calibration solution, the slope measurement of 59.16 = (177.5 – 0)/(7-4) = 177.5/3 = 59.16 mV can be adjusted so subsequent pH measurements when in the nutrient bath can accommodate for the difference in slope. For example, I measured a pH of 165 mV for pH4 and -10 mV for pH 10. The slope is: (165 – -10)/(7-4) = 175/3 = 58.3 mV.
Here are the Union and struct typedefs I use:
typedef struct {
uint32_t write_check;
int32_t pH4;
int32_t pH7;
uint8_t month;//eg: 1 for January, 12 for December
uint8_t day;//eg: 30 for the 30th day of the month
uint16_t year;//eg: 2015
}pH_cal_t;
typedef union {
uint8_t bytes[16];
pH_cal_t pH_cal;
}pH_data_t;
Here is part of the test code I wrote:
pH_data.pH_cal.write_check = 0x01020304;
pH_data.pH_cal.pH4 = 165;
pH_data.pH_cal.pH7 = -10;
pH_data.pH_cal.day = 20;
pH_data.pH_cal.month = 11;
pH_data.pH_cal.year = 2015;
pstorage_wait_handle = block_0_handle.block_id; //Specify which pstorage handle to wait for.
pstorage_wait_flag = 1;
pstorage_store(&block_0_handle, pH_data.bytes, 16, 0);
while(pstorage_wait_flag) { power_manage(); }
SEGGER_RTT_WriteString(0,“data written:\n”);
for (int i = 0; i < 16; i++)
{
if (i > 0) SEGGER_RTT_WriteString(0,“:”);
SEGGER_RTT_printf(0,“%02X”,pH_data.bytes[i]);
}
SEGGER_RTT_WriteString(0,“\n”);
SEGGER_RTT_WriteString(0,“\n”);
SEGGER_RTT_printf(0,“write_check: %0X\n”,pH_data.pH_cal.write_check);
SEGGER_RTT_printf(0,“pH 4 calibration: %d\n”,pH_data.pH_cal.pH4);
SEGGER_RTT_printf(0,“pH 7 calibration: %d\n”,pH_data.pH_cal.pH7);
SEGGER_RTT_printf(0,“day : %d\n”,pH_data.pH_cal.day);
SEGGER_RTT_printf(0,“month : %d\n”,pH_data.pH_cal.month);
SEGGER_RTT_printf(0,“year : %d\n”,pH_data.pH_cal.year);
The code uses the nRF51’s persistent storage manager (pstorage) APIs to read and write from flash. There turned out to be some “gotchas” reading and writing to flash that took away my enthusiasm to talk directly to the NVMC registers. I’ll probably cover my flash read/write exploration in a future post.
Here is what is printed out to the terminal screen:
04:03:02:01:A5:00:00:00:F6:FF:FF:FF:0B:14:DF:07
write_check: 1020304
pH 4 calibration: 165
pH 7 calibration: -10
day : 20
month : 11
year : 2015
The side by side comparison of the pH_data.bytes view of the data and the pH_data.pH_cal view helped me better see how the bytes are internally stored versus how the program’s data type interpret them. For example, pH 4 calibration is 4 millivolts. It is the second word with a LSB of 0xA5 followed by 0’s for the other three bytes. The int32_t representation is 0x000000A5 = 165.
Using Unions and structs to go between different data views – the view the micro controller’s storage has on the data versus a data type’s interpretation of the bytes that are stored – made it much easier to understand what the bytes are all about.
That’s it for now. Thanks for reading this far.