Hi all,
Although I‘ve been programming computers in various languages since the era of the AMSTRAD CPC (anyone remembers?), being an engineer, software development is not exactly the focus of my field of study.
Proper designing and development of a program is paramount for ease of troubleshooting and debugging when things don’t behave as expected especially when it comes to bigger and complex scripts.
Even though I realise that this is a huge subject and probably beyond the scope of this forum, it would be useful if more experienced programmers could offer some guidelines, advice or best practices regarding general programming approach with the focus on easy debugging. ( In addition to the useful info on the reference posts of this forum.)
Print every function. Anything that takes more than one page is too long.
Use lots of functions. Every function should have a name that clearly describes what it does, and should do only one thing. processData() is a lousy name. So is printTimeAndReadTemperaturesAndMakeGETRequest().
By using lots of functions, each one can be quite small. Small functions are easy to test. Once a function is KNOWN to work, with all input data, it can then be treated as an infallible black box.
Look at Robin2's serial input tutorial. The recvWithStartEndMarkers() conceptually looks quite complicated, but a little study shows that it really is quite simple. A few tests will show that recvWithStartEndMarkers() just works. So, if you have a program that reads serial data, and does so by calling recvWithStartEndMarkers(), then it is quite safe to assume that any problems with the program are NOT caused by recvWithStartEndMarkers().
Somewhere between 16 months of Analysis Paralysis and Rewriting All The Comments on an IBM project in 1993, and "Agile Programming" (whatever that actually is), I believe there is a friendly place we can live in.
My basic belief is "Prototype Everything": Interaction Design, Hardware, Code, Documentation, packaging.
I wrote a lot of PASCAL back in the day, and I have used Niklaus Wirth's concept of "Stepwise Refinement" ever since.
And don't get too bogged down in other people's ideas, and what they say is "The Right Way". Including mine.
I was fortunate to be able to help get Buckminister Fuller to come and talk with us at IBM. One thing he said has echoed in some space in my head ever since:
Some thought about design upfront is always worthwhile, but in the case of arduino sketches, they're never likely to be all that long. Sure, you may be using a bunch of libraries, but most arduinos simply don't have enough resources to build a really big system.
In which case, you can probably get your design work done in the shower, or any other place where you can't immediately put your hands on the keyboard.
Use top down or bottom up or functional decomposition (aka stepwise refinement) or most likely, some combination of these.
The most important thing in avoiding problems in bigger and complex scripts is testing as you go. I feel uncomfortable if I've written more than twenty lines of code and not tested my sketch. Indeed, if I've written a new three line function, guess what, I'm going to test.
This way, you can usually rely on the old method of "What did I change last?" to get you out of trouble. Sometimes, that isn't enough, but it's usually enough of a clue by four to help direct your analysis.
Print every function. Anything that takes more than one page is too long.
Use lots of functions. Every function should have a name that clearly describes what it does, and should do only one thing. processData() is a lousy name. So is printTimeAndReadTemperaturesAndMakeGETRequest().
That is so true! I discovered that the hard way over time..
One of the hardest type of programs that I had to debug was one that had extensive use of state machines (in the broader sense of the term)
The following snippet is an example of a bug that took me one week to spot.
The program uses 2 softSerial ports to talk to an ESP module and to another device and switches between them with the port.listen() command. (here it is within the function wifiActive(); that is not shown)
Having forgotten to add the line wifiActive(); below, (the MPU couldn't listen to wifi module and it thought that there was no connection) all the state variables were set wrong and subsequently other state dependent functions were run and everything was a mess . What made things real difficult, was that while the program run, the wifi port was sometimes activated by other parts of the code which made the symptom intermittent. Troubleshooting (by inserting serial.println lines at various places) kept giving misleading hints of the probable location of the problem. I think that at the end finding the source of the problem was more thanks to..luck!
if (millis()-timer1> 3000) {
timer1=millis();
#ifdef RTC
if (timeStatus()== timeNotSet) { AskTimeStamp(activeTransport); } // If Time not sync, sync with bus
#endif
#ifdef WIFI
wifiActive();
if (WiFi.status()!=WL_NO_SHIELD){
if (WiFi.status() == WL_CONNECTED || WiFi.status() == WL_IDLE_STATUS ) { setTransport(wifiNet);}
else {unsetTransport(wifiNet);
connect2Wifi(ssid,pass);
}
digitalWrite(wifiLed,isTransport(wifiNet));
}
#endif
Every concept can be (and usually is) abused but I like the concept of Agile Development because it is very difficult for non-computer folk to describe exactly what they want. So if you can show them something without too much effort they get a chance to say "Ahh ... nooo... that's not what I had in mind".
The nearest analogy I can think of for Arduino programming is what every sensible programmer does - develop code as a series of separate functions that can be tested separately before being brought together.
I have not needed to use conditional inclusion of lines of code (as in the example in Reply #9. I know it is very common - especially in the Arduino code that deals with different MCUs - but it seems to me to create scope for many sleepless nights.
Where I have needed some differences I have had a couple of alternative files that could be included - so that in either case there is clean code with no need for the reader to mentally skip parts.
Watcher:
The need for the #ifdef s is because the hardware has plugin options such as wifi module , RTC etc..
I understand that and the concept is widely used.
I just think it obfuscates things and I would avoid it if I could. And I suspect provision for alternative modules could be provided by less confusing means.
Agile Development because it is very difficult for non-computer folk to describe exactly what they want. So if you can show them something without too much effort they get a chance to say "Ahh ... nooo... that's not what I had in mind".
Very good point. I had trouble pushing the "Show an early Prototype to the customer" idea at IBM. Some IS (AKA IT) managers though it was "Unprofessional". But I was an Engineer, who had been hired by a nontraditional IT middle manager who literally told me, "I'm looking for a Hardware Guy to run amok among the Software people!".
Robin2:
I have not needed to use conditional inclusion of lines of code (as in the example in Reply #9. I know it is very common - especially in the Arduino code that deals with different MCUs - but it seems to me to create scope for many sleepless nights.
I have used it for debugging, since it's a great way to strip out all test code or re-add it instantly when it's necessary.
Other than than, if you only intend your code to run on a single controller with a single arrangement of hardware, you will have no need for conditional compilation. Just like it would never make sense to have something like an if( false ) block in your code, if your code always uses the same configuration of hardware and options you get no benefit from conditionals.
It's very necessary though if your going to write any code with general applicability that uses specialized features. Even between different AVRs, you can't trust that they have the same arrangement of registers and bits for a given function. Conditional compilation is the only way you can maximize the amount of function you get from a library written to run on as many processors as possible.
In general, it is possible to create a Hardware Abstraction Layer by other means than conditional compilation. But it is easy to see why that method was chosen for the Arduino library. It's convenient and conventional. It is also hard to imagine how other methods would address the self contained nature of the code - the lack of OS support. If it was necessary to produce object code that would run on multiple platforms, then a different approach would be mandatory (specifically, some kind of conditional execution). But that is not compatible with the necessity of limiting code size.