Part of what makes embedded systems fun for me is that they normally interact with the physical world. The physical world contains real parameters which we measure using transducers, signal conditioning circuits and so on, such that ultimately we end up with a variable in our embedded code that purports to represent this real world parameter. For example, we might have this:
uint16_t pressure; /* Pressure */
Don’t laugh. I have seen this a million times. What’s wrong with this you ask? Well, when dealing with real world variables, it is crucial that you as the author of the code make crystal clear at least four things about the real world variable:
- What the parameter actually is.
- The units of the variable.
- The resolution of the variable, otherwise known as the value of a LSB.
- The dynamic range of the variable.
Identifying the parameter
Many embedded systems measure a multitude of real world parameters, many of which may be of the same ‘type’ (for example, ‘pressure’). Thus while it is blindingly obvious to you when you write the code which pressure you are thinking of, it most certainly isn’t to someone coming into the code cold. Even if there is only one measured pressure in your system, the chances are there are various flavors of it. For example, a common architecture when measuring real world variables is to:
- Have the raw value. This is typically the value resulting from converting the latest ADC reading. This raw value is then:
- Median filtered so as to eliminate egregious outliers. The median value is then:
- Low pass filtered so as to remove Gaussian noise.
There are thus at least three representations of the pressure in the system I have described. My preference is that the variable name identify which instance you are dealing with. However, if for various reasons this makes for unwieldy names then at the very least make sure the comment that accompanies the variable declaration spells it out. For example:
uint16_t pressure_co2_median; /* Median filtered CO2 pressure */
Failure to specify the units drives me up the wall. For example there are literally dozens of units of pressure, there are at least four units of temperature and a huge number of units for speed. While it is of course obvious to you the author that you are measuring pressure in lbs/square inch, you are likely to find that the rest of world that works on the SI system probably aren’t even aware that such an arcane unit exists. The bottom line: specify your units. My example now becomes:
uint16_t pressure_co2_median; /* Median filtered CO2 pressure. Units: bar */
Related to this is the requirement that the units are consistent with the variable name. For example, most of us would criticize a declaration that looks like this:
uint16_t pressure_co2_median; /* Median filtered CO2 pressure. Units: Celsius */
This is clearly wrong. However, what about this declaration:
uint16_t symbol_rate; /* Symbol rate reported by demodulator. Units: bps */
What’s confusing about this declaration is that a symbol rate should have units of symbols per second. For the special case, where there is one bit per symbol, the symbol rate does happen to equal the bit rate, which is usually measured in bits per second or bps. Thus upon seeing a declaration such as this, I’m left in a quandary, as the following are all possibilities:
- I’m dealing with the special case where the symbol rate and bit rate are the same.
- The author was sloppy in his commenting and meant to write ‘sps’ rather than ‘bps’ and so the variable genuinely does reflect a symbol rate.
- The author was sloppy in his variable naming, such that the variable actually represents a bit rate with units of bps.
That’s a lot of confusion to sow for failure to ensure that the variable name and its units are consistent.
Closely allied with units is the resolution or scaling. In a nutshell what does 1LSB of the variable represent? Again I find that this all too important parameter is deemed obvious by the author of the code. While it is common that 1 LSB = 1 unit, systems that need to maximize dynamic range will often use a different scaling. For example: 1 LSB = 0.0125 bar. The bottom line: if you don’t specify what 1LSB represents you are really doing future readers of your code a major disservice. It’s common to incorporate the resolution and units together, such that our example now becomes:
uint16_t pressure_co2_median; /* Median filtered CO2 pressure. 1 LSB = 0.0125 bar */
The last thing that should be reported is the expected dynamic range of the variable. This covers a multitude of issues:
- What is the legal range of values? For example, consider the popular LM35 series of temperature sensors. These devices are inherently designed to report temperatures above zero Celsius. As such negative temperatures are not expected when using this sensor. Other sensors will of course have other physical limits which it’s important to report.
- What is the sensor offset? For example, absolute pressure sensors will typically have a non zero output when exposed to atmospheric pressure.
Thus it is essential that you specify the expected range. If you do so, the resolution can be implicit. However I still like to make it explicit. For example:
uint16_t pressure_co2_median; /* Median filtered CO2 pressure. Range 0x0000 - 0x3FFF = 1.000 - 204.7875 bar. 1 LSB = 0.0125 bar. */
I urge you to take a look at your code and see if you are doing this for your real world variables. If you aren’t then consider making some changes. Future generations will thank you.