I’m coming today with an interesting topic. It will be a pointer to a function. Unlike “regular” pointers, they are a bit harder. Because functions define more things than just a regular variable.

Pointer to a function

Pointers can point to any memory cell, and our program code is nothing more than successive instructions in Flash memory. So you can point to it.

The compiler in cooperation with the linker handles determining a function’s address very well.

A function has certain fixed elements. These are arguments and the returned result. We provide these things from the outside, but inside the function is always the same. After all, it resides in non-volatile memory – Flash.

In reality, the function code always uses the arguments in the same way, in the same order, and always returns the result the same way. Only the data “underneath” changes. The operations are always the same 🙂

So if we provide in code the address of a function together with the list of arguments it requires, then in fact we can point to any function. Having two functions with the same arguments means that we can call them “interchangeably”.

Function pointer structure

How to build a function pointer? I already mentioned to you that the compiler must know the entire required list of arguments and the “return value”. This will have to be attached to the pointer.

Another important element is informing the compiler that this is a pointer to a function. How? By adding the function call operator, that is, parentheses on the right side of the identifier.

Let’s take an example, the simplest function:

void MyFunction(void) { … }

Never mind what it does.

How to build a pointer to it? First, the name of the pointer and indicating that it will be a pointer.

(*FunPtr)

Let’s put it in parentheses right away so that operator binding order doesn’t win out. We have a pointer named FunPtr. We still don’t know what type or what it points to.

It is to be a pointer to a function, so we must add the function call operator. In the same way we wrote the function.

(*FunPtr)()

This is already a function pointer. Now the entire litany of arguments and the return type. The compiler MUST know this. It must know what EXACTLY the pointer points to. After all, we need to pass those arguments from the outside somehow.

Our function took nothing and returned nothing, so it’s simple.

void (*FunPtr)(void)

In the arguments, you provide only their types. In the order that the function accepts them. Now we can assign the function to the pointer.

The function name itself is converted to its address. That’s what is used here. The pointer name is also an address. So we use just the names. The correctness of the remaining elements is checked by the compiler “in the background”.

FunPtr = MyFunction;

From now on the FunPtr pointer points to our function. How to call the function via the pointer? With the function call operator 🙂

FunPtr();

If FunPtr is an address in memory, then the parentheses tell the compiler “call the function at this address”.

A more difficult pointer

Ok, that was trivial because we had no arguments. Let’s do something crazy. Let’s point to an example transfer function:

uint8_t ReadFromI2C(uint8_t Address, uint8_t Register, uint8_t *Data, uint16_t Size){…}

The function accepts in its arguments:

  • I2C device address
  • register address in the device
  • pointer to the array for receiving data
  • amount of data read (e.g., with auto-increment)

An error code is returned as uint8_t. If no error, then zero. If error, then something else.

How to build a pointer that points to such a function? Let’s go!

  1. We need an identifier

ReadFromI2CPtr

  1. Now let’s say that this is a pointer

(*ReadFromI2CPtr)

  1. What the pointed-to function returns

uint8_t (*ReadFromI2CPtr)

  1. The entire list of arguments in the same order as the function we want to point to (types only)

uint8_t (*ReadFromI2CPtr)(uint8_t, uint8_t, uint8_t*, uint16_t);

And done! Was it hard? I hope not 🙂

Passing a function pointer to a function as an argument

Often we will want to pass such a function pointer as an argument. In the next email we’ll deal with callbacks, and that’s exactly where function pointers are used.

How do we pass such a pointer? With a pointer to a variable, we provided the pointer type. How here?

This whole litany is the type of the pointer.

If we want to pass a function pointer as an argument, we must EXACTLY describe what kind of function it is.

As a reminder, a function is described by:

  • name (address in Flash)
  • types, order, and number of arguments
  • return type

For example, let’s take this more difficult function, or rather its prototype.

uint8_t ReadFromI2C(uint8_t Address, uint8_t Register, uint8_t *Data, uint16_t Size) { … }

We want to pass the address of this function as an argument. What will the prototype of the function that takes the address of such a function look like?

void MyFun2( ?? ) { … }

Think for a moment… You have to pass all information about the function being pointed to.

Done?

Do you know? 🙂

Since a function pointer must be precisely specified, it will look the same as if we were creating a standalone pointer.

void MyFun2( uint8_t (*FunctionPtr)(uint8_t, uint8_t, uint8_t*, uint16_t) ){ … }

This is ONE argument! A pointer to a function that returns uint8_t and takes 4 arguments according to the given types. Yes, I know… it looks terrifying.

Fortunately, using this function itself and passing to it the address of a function is simpler. What is the address of a function?

Its name! So the concrete call of this function with the nightmarish pointer argument will look simpler:

MyFun2( ReadFromI2C );

That’s it. The function name ReadFromI2C is its address.

Now the compiler looks and analyzes:

“Oookay… function MyFun2 (based on the prototype) takes the address of a function that returns uint8_t and takes arguments of type uint8_t, uint8_t, pointer to uint8_t and uint16_t. 

You passed me, dude, the address of some function ReadFromI2C, so let’s take a look at what it looks like (based on the prototype).

ReadFromI2C (based on the prototype) returns uint8_t and takes arguments of type uint8_t, uint8_t, pointer to uint8_t and uint16_t.

Everything matches! We can compile!”

If there is any mismatch, the compiler will tell you right away!

The key is to understand that a function pointer MUST be described very precisely. It must fully define the function prototype. I’ll remind you once again, because this is important:

  • name (address in Flash)
  • types, order, and number of arguments
  • return type

Most common mistakes

I see a few mistakes in attempts to use function pointers. The most important ones:

  1. Missing the function call operator

If the pointer points to a function, then in its declaration it must include the function call operator.

  1. Missing parentheses that “separate” the function call from the pointer

Parentheses, i.e., the function call operator, have HIGHER precedence than the pointer operator, i.e., the asterisk *

So when writing

uint8_t *ReadFromI2CPtr(uint8_t, uint8_t, uint8_t*, uint16_t);

We actually have a function prototype that returns a pointer, not a pointer declaration!

There must be parentheses that include the asterisk and the identifier (name) of the pointer.

uint8_t (*ReadFromI2CPtr)(uint8_t, uint8_t, uint8_t*, uint16_t);

Now the pointer has precedence, and only then the function call operator. Now it is a function pointer.

  1. Argument mismatch

We don’t have freedom with function pointers. More precisely, we do not create a “function pointer” as something general.

We create a very specific “pointer to a function that takes X1, X2, X3…Xn and returns Y.” Therefore, a mismatch of even one element X or Y will break compilation.

  1. Using parentheses when passing the address of a function

Parentheses are the function call operator. If we want to extract just the address of a function, we use ONLY THE NAME of the function. Without parentheses. Parentheses will cause that instead of passing the address… we will call the function.

The situation is similar to pointers to data: The name itself = address. The name with an asterisk is the value at that address.

For a function, the name itself = address. The name with parentheses is calling the function at that address.

Maybe I’m repeating myself, but this is key to understanding pointers 🙂 What is “just” an address, and what is “something useful” hidden under that address.,

Summary

As you can see, it’s a bit harder than a regular pointer to data. No mercy. In C, a lot of things rest on the programmer. Many things must be determined in advance. Among other reasons, that’s why C is efficient 🙂

Study this email carefully once again. Test in the online compiler assigning a function address to a pointer. Call the function using the pointer. Create some complicated function and try changing things so that errors show up.

You have to practice and experiment.

Why do I press so hard on this function pointer?

We’ll need it for so-called Callbacks. In common use, they utilize function pointers.

This will be an important topic, because callbacks are used quite intensively in microcontroller programming.

Be sure to let me know in the comments whether this article was understandable for you! If you don’t understand something, write! It’s important.

Do you want to learn the C language with microcontrollers in mind?

I created a course dedicated to microcontrollers. I teach C in it from scratch. Everything I discussed in this post (and much, much more) is included in the course syllabus. (The course is conducted in Polish.)

I gathered my experience from several years of embedded programming and I want to pass on the best possible knowledge to you. I participated in various projects: solo, startup, a mid-sized company, and a huge corporation.

In addition to the basics and syntax, I share a ton of best practices. I weave this in between explaining subsequent aspects of the C language.

An additional advantage is also that I show how to run a project well. I’ll show you how to deal with building abstraction layers. We’ll use structures, pointers, and callbacks. And of course splitting into files. That helps a lot.

Such separated layers are much easier to перенос between projects, and even between different microcontroller families.

Join the waiting list for the course and start learning together with the materials I prepared. After signing up, you will receive weekly emails about the C language (the newsletter is conducted in Polish): https://cdlamikrokontrolerow.pl

Podobne artykuły

.
Categories: STM32

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *