Pointers and functions

Spring 2020

The questions below are due on Monday February 17, 2020; 11:59:00 PM.
 
You are not logged in.

If you are a current student, please Log In for full access to the web site.
Note that this link will take you to an external site (https://shimmer.csail.mit.edu) to authenticate, and then you will be redirected back to this page.
Back to Exercise 02

Losing My Edge Detect

1) Using Pointers with Functions

In our simplest way to create functions, we pass in some variables and then return zero or one variable. For example, the following function takes in two inputs and returns one output.

int multiply(int x, int y) {
    return x*y;
}

Importantly in C/C++, the inputs to functions are by default by value, meaning when we call the function, the values we pass in get copied and used rather than the initial locations in memory. So even if we had code which utilized the multiply function from above as follows:

int hey = 5;
int hi = 17;
int bye = multiply(hey,hi);

When multiply is called, the values contained within hey and hi are copied into the scope of the function for use1. This should make sense.

What do you do if you want to return more than one output or possibly affect more than one variable? How do you return an array? In all of these cases using pointers as we discussed in Exercise 01 can be really useful. Because a pointer variable contains an address, when we pass that address into a function, we can have direct access to that variable's value from within the function and not just the value of the variable.

As a simple canonical example, imagine we want to swap two values, or in other words, we want a function that will take in two values and swap them, returning two values. Here's how to do that:

void setup(){
  Serial.begin(115200);
}

void loop() {
  int first = 47;
  int second = 59;
  char print_buffer[200] = {0}; //init to nulls;
  sprintf(print_buffer,"First: %d, Second: %d", first,second);
  Serial.println(print_buffer);
  swap(&first, &second);
  sprintf(print_buffer,"First: %d, Second: %d", first,second);
  Serial.println(print_buffer);
}

int swap(int* x, int* y) {
  int temp = *x;
  *x = *y;
  *y = temp;
}

Let's unpack the code. The swap function takes in two arguments, but the arguments are actually pointers. So swap is expecting some addresses to be passed in.

In the loop we declare and initialize two ints, first and second. So our memory map might look like this (again, realizing that nothing requires that first and second be adjacent to each other in memory):

Placeholder for Diagram 5607be0b635ad175553d52e20eac081a
The initial memory map

But here's the trick: when we call swap in loop, we don't pass the values of first and second, we instead pass the addresses of those variables. When swap gets called it will place its local variables somewhere else in memory, and its local pointer variable x points to address 0x7530 (30000 in decimal, which we can also write as 0d30000) and y points to address 0x7534 = 0d30004. So we might have the following memory map:

Placeholder for Diagram 7e77441345c213ef88c94e22c8c23159
Now the function gets called and x and y are created in memory.

In the next line, int temp = \*x;, we dereference x, which holds address 30000, and reach out to grab the value that's being held at address 30000, which is 0x2F=0d47,2 which is then assigned to local variable temp. Another way of saying this step is temp = value pointed to by x. The memory map now looks like:

Placeholder for Diagram 5f4e8f1d529961fbbfa7313d8baa47ea
A new variable called temp is created within the scope of the function (and in memory). We initially give temp the value pointed to by x

Next we dereference y to grab the value stored at the address pointed to by y. The address pointed to by y is 0x7534 = 0d30004, and so we grab the value at that location, which is 0x3B=0d59. We then assign that value to the variable pointed at by x. Since x holds the address 0x7530=0d30000, we place 0d59 into that address, which is first's address. So now first holds 0d59=0x3B. Again, this is equivalent to "value pointed at by x = value pointed at by y. The memory map now looks like:

Placeholder for Diagram 7608b3a739e9bcfba4d25712f38b7597
Our variable first is now holding what is in second. We now need to finish the job.

Note that we are not changing the address held in x; to do that, we would have written something like x = y;.

Finally, we dereference y to assign the value temp to the address held in y, or "value pointed at by y = temp. Since y points to 0x7534=0d30004, the value of temp, which is 0x2F to that memory address, which is the address of second. holds the address of the variable second, now so does x:

Placeholder for Diagram 8746c47343abaa86bfae517cca46d815
Everything is Swapped

And voila, we have swapped two variables without returning anything! Study what is going on here. It is confusing at first, especially coming from a Python background, but all we're doing is exchanging the values in two memory locations.

2) Writing a Function

You'll recall that previously we've been dealing with switches and been building up state machines around them so that we can respond appropriately to our interactions with them. We'd like to now write a generalized button finite state machine built around a reusable function and an arbitrary number of global variables that each hold a button state. Each button FSM will have its state encapsulated in a single integer variable provided by the user. The state can be one of two values. The values of the state are:

  • 1: Button is currently unpushed
  • 0: Button is currently pushed

The system will obey the following state transition diagram and exhibit the following outputs:

Placeholder for Diagram 2703842d7e43befc96e7212be2525c15
State machine example with only changing transitions indicated.
  • If a button is in state 1 and it gets pushed, it should output a 1, and transition to state 0
  • If a button is in state 0 and it gets released (unpushed), it should output a -1, and transition to state 1
  • In all other cases, the system should maintain its current state and output 0

In effect, we are detecting the "edges" of the button values with this state machine, so we're going to create a function responsible for handling the FSM transitions called edge_detect. The function should be able to work on arbitrarily large numbers of button state machines (lots of buttons), provided the proper variables are used for it. The ability to work with an arbitrarily large number of state machines will come from its usage of pointers as inputs. It'll have three inputs:

  • input which corresponds the current input to this button SM (from digitalRead)
  • button_state an integer pointer corresponding to the current state of a particular button state machine
  • output an integer pointer corresponding to the current output of a particular button state machine.

In deployment this function would operate in the following manner:

const int PIN_1 = 16;
const int PIN_2 = 5;
int state_1;
int state_2;
const int LOOP_PERIOD = 30;
unsigned long timer;

void setup(){
  Serial.begin(115200);
  pinMode(PIN_1,INPUT_PULLUP);
  pinMode(PIN_2,INPUT_PULLUP);
  timer=millis();
  state_1 = 1; //initialize
  state_2 = 1; //initialize
}

void loop(){
  int input_1 = digitalRead(PIN_1);
  int input_2 = digitalRead(PIN_2);
  int output_1;
  int output_2;
  edge_detect(input_1,&state_1,&output_1);
  edge_detect(input_2,&state_2,&output_2);
  char print_buffer[300]={0};
  sprintf(print_buffer,"%d %d %d %d", output_1, output_2, state_1, state_2);
  Serial.println(print_buffer);
  //state logic below
  while (millis()-timer<LOOP_PERIOD);
  timer = millis();
}

Running this code for a little bit would result in the following annoted Serial monitor output. You should be able to see that even though we use one single function, we are able to properly isolate the transitions and outputs of the two separate state machines:

0 0 1 1
0 0 1 1
1 0 0 1  <---pushed button1 here
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
-1 0 1 1  <----released button1 here
0 0 1 1
0 0 1 1
0 0 1 1
0 0 1 1
0 0 1 1
1 0 0 1   <-----pushed button1 here
0 0 0 1
0 0 0 1
0 0 0 1
0 0 0 1
-1 0 1 1 <----released button 1 here
0 0 1 1
0 0 1 1
0 0 1 1
0 0 1 1
0 1 1 0   <----pushed button 2 here
0 0 1 0
0 0 1 0
0 0 1 0
0 0 1 0
0 -1 1 1   <---released button 2 here
0 0 1 1
0 0 1 1
0 0 1 1
0 0 1 1
0 0 1 1
0 1 1 0  <----pushed button 2 here
0 0 1 0
0 0 1 0
0 0 1 0
0 0 1 0
0 0 1 0
0 0 1 0
0 -1 1 1   <----released button 2 here
0 0 1 1
0 0 1 1
0 0 1 1
0 0 1 1
0 0 1 1

Implement a version of edge_detect below that will behave as specified above. The outputs of the checker are similar to the transcript above in terms of what is printed.

void edge_detect(int input, int* button_state, int* output) { //TODO: Your code here }

Back to Exercise 02

 
Footnotes

1the notable exception to this are arrays which are passed in by reference, but this is also the result of an array in C++ being nothing more than a pointer (click to return to text)

2The 0d stands for decimal value, the 0x stands for hex (click to return to text)