Friday, August 28

Firmware Factory: Bit Fields vs Shift and Mask

Working with embedded systems usually involves writing code which will interface with hardware. This often means working on the register level. It doesn’t matter if we’re talking about a UART, an analog to digital converter, an LCD controller, or some other gizmo. Sooner or later, you’re going to have to break out the datasheets and figure out how to talk to an external device. To succeed at this you must become a master of bit manipulation.

Hardware designers don’t like wasting space, so modes, settings and other small pieces of information are often stored as packed bits. Our processors usually access things a byte (or a word) at a time, so what is the best way to handle this? Like so many other topics in software engineering, there are multiple ways to skin this cat. In C (and its derivatives) there are two major options: shift and mask, and bit fields.

ad-pll-ctrl

Consider this register from an Analog Devices ADAU1977, an Analog to Digital Converter (ADC). There is a lot going on here. We’ve got 6 separate data elements to deal with. Bit 7, is a read only bit, while the rest are read/write. The 3 least significant bits are handled as a single 3 bit value, used to select the master clock for the ADC. 

Bit Fields

C’s native interface for describing bitwise data is bit fields. Bit fields are defined just like structures, which means they make for very easy to read code. The disadvantage to bit fields is that they are non-portable. Using bit fields means placing a lot of trust that your compiler will do what you expect it to.

In bit fields, we would describe the PLL control register as follows:

struct 
{
 unsigned char clockSelect:3;
 unsigned char res2 :1;
 unsigned char clkSource :1;
 unsigned char res1 :1;
 unsigned char pllMute :1;
 unsigned char pllLock :1;
} PllControlRegister;

accessing the bit fields is just like a structure:

struct PllControlRegister myRegister;
myRegister.pllMute = 0;

Easy right? Well, the devil is in the details. Bit fields are one of the gotchas of non-portable code. Even the Linux kernel hackers have been bitten. If the project you’re working on is architecture (and compiler) specific, and you know how the compiler will work, you’re golden! However, if you have any future plans to re-use this code on a different system, you’re asking for trouble. Here’s why: The C standard doesn’t define how bit fields should be ordered. In fact, it says: “The order of allocation of bit-fields within a unit (high-order to low-order or low-order to high-order) is implementation-defined.”

intconGoing by that, we have no way to know if pllLock will end up in the most significant bit like we expect it to, or the least significant bit. Things get even more complicated when you throw in larger bit fields on 32 or 64 bit machines, endianness, and padding for fields that cross storage boundaries. If you’re interested in portable code, you’re better off with the other method, shift and mask, which I describe below. For now though, let’s talk about a real world case where bit fields are the better bet.

Microchip’s PIC line of processors use bit fields extensively in their own include files. For example, the INTCON (interrupt control) register on the PIC16F1829 microcontroller is defined as follows:

struct 
{
   unsigned IOCIF :1;
   unsigned INTF :1;
   unsigned TMR0IF :1;
   unsigned IOCIE :1;
   unsigned INTE :1;
   unsigned TMR0IE :1;
   unsigned PEIE :1;
   unsigned GIE :1;
}INTCONbits_t;

(the actual .h file has a union, but I’ve simplified things a bit here)

Microchip can use bit fields here because they know what the compiler will do – they wrote it! INTCON is also an internal register on the PIC itself, so no worries about writing portable code. The added bonus here is that PIC has bit set and clear opcodes that can operate over all of its RAM. The compiler is smart enough to translate any bit field operation to the appropriate bsf and bcf opcodes, which operate in a single cycle.

Shift and Mask

The safer way to handle operations like this is shifting and masking. As the name implies, this uses a shift operation to build up a mask, then we use that mask with a logical operator to set or clear a specific bit. We don’t want to perform all this work on the actual hardware, so the best bet is to use a shadow of the register in RAM. First read the hardware register into RAM, then make modifications on the RAM shadow, and finally write the RAM shadow back to the hardware register. This is where the term Read-Modify-Write comes from. I know this all sounds complex, but after using it a few times, shift and mask with a shadow register becomes second nature.

Looking back at the ADC register up at the top of the article, let’s say we wanted to set pllMute, which is bit 6. The logical OR operation can be used to set a bit. In this case you need to mask all the bits you want to change with a 1.

For simplicity’s sake, assume for these examples that PllControlRegister is a memory mapped I/O location, called  “pllCtlReg”.

unsigned char pllShadow;
unsigned char mask = 1;
pllShadow = pllCtlReg;     //Read the current value of pllCtlReg
//now build the mask
mask = mask << 6;          //Shift to get 0x40, or b0100 0000
pllShadow | = mask;         //Logical OR will SET the only bit 6 in our shadow
pllCtlReg = pllShadow;     //write the shadow back to the hardware

If we want to clear a bit, a logical AND is the way to go, but this time you need to mask the bits you want to change with 0s, all other bits will be set to 1.

unsigned char pllShadow;
unsigned char mask = 1;
pllShadow = pllCtlReg;      //Read the current value of pllCtlReg
//now build the mask
mask = mask << 6;           //Shift to get 0x40, or b0100 0000 
mask = ~mask;               //logical NOT gives us 0xBF, or b1011 1111 
pllCtrlReg &= mask;         //Logical AND will clear only bit 6 in our shadow.
pllCtlReg = pllShadow;      //write the shadow back to the hardware

If you need to toggle a bit, logical exclusive or (XOR, or the ^ operator) is the tool of choice. I’m just showing one of the most basic ways to perform shift and mask. There are plenty of ways to perform the same operations. You can use #defines, or preprocessor macros, or even functions to perform these same tasks. It all depends on what is most important for your application: memory space, or execution speed.


Filed under: Featured

No comments:

Post a Comment