C: Structs and Pointers

Tags: C, Programming

This blog entry covers parts of a recent C programming exam I held for my course C programming at VAMK. I know some of my students read this blog, so it might be of help to them, or anyone else interested in some more, dare I say, 'advanced' C stuff. It is by no means intended to be a complete tutorial, but it might provide a starting point for further study.

Let's start with something simple. We have a 32-bit memory area, say an unsigned 32 bit integer, and we want to store 2 16-bit values in there. We could use bitwise operators/shift operators to do this, but it is much easier to map this region on top of a struct as follows:

	struct cell32{ 
        	uint16_t one; 
        	uint16_t two; 
	}; 

	int main(int argc, char *argv[]){

		uint32_t cell;
		struct cell32 *mycell = (struct cell32 *)&cell;

		mycell->one = 10;
		mycell->two = 60;

	return 0;
	}

Here, a struct with the two 16-bit entities is mapped over the 32-bit cell variable. In practice, we do have to be careful since the compiler can add padding bytes in order to properly align each field in the struct. In the example above, both fields are 16-bit integers (2 bytes long) and are also 2 byte aligned - there will be no padding added (assuming x86 architecture).

As an example, padding will be added when we write something like:

	struct cell32{ 
        	char     one; 
        	uint16_t two; 
	};

The size of the struct will be 4 bytes: the first field will be one byte aligned, the second one has to be 2 byte aligned - which means the compiler will add a padding byte to the first field.

So, where do we practically use something like this? Suppose you are writing drivers for embedded systems and you have a bunch of registers to set. In actuality, you have a memory address, and then a bunch of offsets which are individual registers for various functionalities. These are usually of the same size, so there will be no problem with padding.

Let's have a practical example. Suppose I have some GPIO to manage on a Freescale i.MX51 SOC. The GPIO1_BASE address is at 0x73F84000. Individual registers (such as the data register, direction register etc.) are put at 32-bit offsets from this base address. We can represent this as a struct:

	struct imx51_gpio{
 		uint32_t dr;     // 0x00,
 		uint32_t dir;    // 0x04,
 		uint32_t psr;    // 0x08,
 		uint32_t icr1;   // 0x0C,
 		uint32_t icr2;   // 0x10,
 		uint32_t imr;    // 0x14,
 		uint32_t isr;    // 0x18,
	};

We can then map this struct over the GPIO1_BASE address:

	struct imx51_gpio *imx51_gpio1 = (struct imx51_gpio *) GPIO1_BASE;

This way, each register becomes easily accessible.

Another useful purpose of a struct is to provide an abstract interface to individual implementations, for instance when writing device drivers. Ideally, each driver has a set of well defined operations (e.g., init, open, close, read, write, ...) which don't change from one driver to another. The way they are implemented however, does change (by the way, this is object oriented programming - see abstract classes and interfaces). How can we do this in C? As an example, have a look at the following struct:

	
   struct usb_driver {
        const char *name;

        int (*probe) (struct usb_interface *intf,
                      const struct usb_device_id *id);

        void (*disconnect) (struct usb_interface *intf);

	...
   };

This (partial - shortened for clarity) struct comes from the Linux kernel and provides the base for every USB driver. There can be properties to assign, such as the name in this case, but we can also see a couple of function pointers (probe and disconnect). These can be seen as abstract methods - each driver has to implement its own functionality for these, since each device can have different requirements. 

Each driver implementation provides some static functions which get assigned to these function pointers. For example:

   static struct usb_driver led_driver = {
	.name =		"usbled",
	.probe =	led_probe,
	.disconnect =	led_disconnect,
   };

Whereby led_probe and led_disconnect are the names of some static functions in the file where this led_driver is defined, taking care of all the initialization, cleanup, etc. required for this device. Afterwards, it can be made available to the Linux kernel using usb_register() on module load, and removed with usb_deregister() on module unload.This means that we have a generic interface for each device. The Linux kernel does not need to know anything about the specifics of each driver or specific function names to do the initialization; it can just call probe() for every USB device driver that gets registered, and the functionality will be device specific.

Oh, by the way, this last example initializes the struct using the set notation. It is used extensively in the Linux kernel. Its benefits include that it provides an easy way to initialise a struct without having to know the order in which elements are declared in the struct, etc.