26 Process Thinking and Incremental Coding
Some people can hammer out complicated code and have it work first time, every time. They are rare and this chapter is not for them. As a mechanical engineer I like to be able to visualize all the elements I’m working with in a physical framework. It helps me to see my way through a process to get to my goals. C code is easy to think of physically because all the elements correspond directly to chunks of hardware in your microcontroller. Stay in touch with what’s happening in your code by thinking about your machine and how it changes step by step over time.
Analog Circuits are Continuous in Time
Picture them as multiple different traces on a plotter, like the plots of EKG’s you see on a medical show. They change with time, sometimes quickly, sometimes dropping to a flatline, but they always have a value. Your microcontroller can only grab discrete values from the analog inputs at specific times. Think of them as dots along those continuous time traces. The more dots you can collect along those traces, the better you can represent the analog truth by connecting the dots. A lot can happen in the gaps if you go a long time between dots, so don’t miss it.
You need to store enough information from those dots to know a little bit of recent history about your analog system. There’s a limited supply of memory boxes to store that information, so plan your data representation carefully. (This is true in multiple GB computers, and especially true in microcontrollers where memory is measured in KB.)
Variables and Arguments as Boxes
Different variable types require memory boxes of different sizes and shapes (e.g. 8 bit char vs 64 bit double) to hold their values. Thinking about when these boxes are built, demolished, and filled can help understand your results.
A global variable is a box in memory that is created when you compile the code. The same box is always there at the same location in memory, nailed to your desk. Every part of your code can see the box, put things in and take things out, so you can’t rely on it being unchanged by other functions. Be sure to fill it with a sensible value by initializing at the declaration. The initialization only happens once at the start of the run.
An argument variable is a temporary box in memory passed to the function. When a function is called, the calling code builds the box on the stack (temporary memory) and puts a value in the box. This happens on the fly while the code is running, and the memory location can change every time the function is called. Only the code inside the function can see the box, and the box moves around on the desk between function calls. The calling code fills the box, but will never see the contents of the box again, so arguments sent to a function can be changed inside the function with no effect. When the function returns, the calling code demolishes the box to free up stack space and the value is gone forever. The argument is passed as a copy because it is easy and efficient for the processor (“pass by value”).
A local variable is a temporary box like an argument, created on the stack when the function is called based on the declaration inside the function code. Only the code inside the function can see the box and change the values. Be sure to initialize the box with a sensible value, or you might get the junk that was leftover in that memory location by previous functions. This initialization will happen every time the function is called. Variables defined as local to a block of code work the same way, with temporary boxes that are only visible inside that block of code, like the i in for(int i = 0; i < 10; i++){} is only visible to the code inside the curly braces.
A static variable is a handy hybrid in C. It is a permanent box nailed to the desk just like a global variable, but is only visible to the code inside the function where it is declared. If you put something in the box, it will still be there at the same location in memory next time the function is called, because no other function can see the box. Be sure to initialize it with a sensible value. This initialization will take place only once at the start of the run, even if it looks like it might happen every time because the declaration sits inside the function code.
Arrays and Pointers
When you declare an array, a big box of memory is allocated, just like for simple variables, enough to hold many values. If you pass an array as an argument, only the memory address of that block is passed to the function. The function now knows where that array box is on the desk and it can change the contents of the box. It is not working with a copy like it would be for simple arguments. Any changes the function makes to the array are made on the original. The old values are gone, and the changes will still be there when the function returns to the calling code. The array is passed as a memory address (pointer, “pass by reference”) because it is easy and efficient for the processor to avoid setting aside a new chunk of memory and copying all the contents over from the old chunk. Array arguments let you get many new values back from a returning function.
An array with one element will have a box identical to the box for a simple variable so we should also be able to pass a simple variable as if it is an array. By passing a pointer to a variable you are telling the function where the box is on the desk, and giving it the ability to change the contents of the box. The changes will remain after the function returns, just like the changes in an array. Passing pointers is another way of getting more than one value back from a returning function.
Big Tasks break down into Small Tasks
In a microcontroller application, all the normal operations happen in the loop() function, repeating over and over. It needs to repeat quickly, so that every important part of the task gets some attention before things go wrong. Just how quickly depends on the application (drones crash faster than heating systems), but all of the small task functions need to finish quickly, even if that means doing only a little bit of the task each time through the loop.
Building Functionality Incrementally
Unless you are a flawless coder, you will need to build and test each element of your code step by step. Sometimes it is easiest to build and test a function in isolation, then move that function into your larger sketch, either by simply copying, or by making it part of a library for use in lots of different sketches.
- first make it work!
- then make it Portable and Robust as individual functions
- then make it Small and Fast, but only if needed
Start by building the basic algorithm in a loop() and making it work well. Test early and often to detect errors one at a time. The longer you go between tests, the more errors you will make, and the harder it will be to untangle them. This is still true for me after almost 40 years of writing C code, so it is probably true for you as well. (Video 6:15)
Copy code from the loop() to create an independent function for each of the small tasks. Each function should do only one thing. Generalize the functionality to be useful in different circumstances, and check the arguments to make sure they are reasonable and won’t break your sketch. (Video 7:34)
Build your code in manageable pieces. Make it clear and easy to read with descriptive names and comments. You will need them when you come back to reuse or repair your code.
Recipes instead of Formulas
If you knew the secret formula, you could analyze KFC to confirm a match to the 11 different herbs and spices, but you would need a lot more information to make some that tasted the same. Achieving the results you want will be easier and more productive by applying simple steps in a process to build towards a result a little bit at a time.
This is very true in engineering science courses where memorizing an equation may help you plug and play for identical format questions, while failing the exam when you need to explain why something happens.
It is even more critical when you are programming embedded systems. Unless you understand the ideas behind every calculation and decision step you program, your code will fail. The big advantage when programming is you can treat each small element separately, building them up and combining them together in sequence to get where you are going.
If it isn’t clear what each variable in an assignment statement is about, and just how they are being combined, then you are probably trying to do too much on a single line, and should take smaller steps.