25

I am a C developer for an embedded system. YouTube has recently started recommending "C++ for embedded systems" talks. Having watched some of them, they pique my interest, but none of them answer the question they leave me with.

These talks (especially Modern C++ in Embedded Systems by Michael Caisse) advocate for a development process whereby, instead of:

  1. writing and edit code
  2. debugging it to confirm it works (or, more likely, debugging it to see what's wrong and where to go from here)
  3. repeat until working

...one should avoid the debugger completely, trusting that the choice of language and good practice makes bugs less likely, which then eliminates the need for the debugger.

But as someone who writes firmware for a microcontroller that controls analogue circuitry, many of my problems are found when hardware shows unexpected behaviour and I find I can only investigate this behaviour (especially the timing of events) by throwing breakpoints all over my code and waiting to see events happen out of order, or not happen at all.

This will then either reveal a mis-configured register, or unexpected behaviour by one of the microcontroller's peripherals, which was not obvious from the device manual and necessitates a small code re-design. These talks have my attention, but I cannot see how these techniques that are supposed to help people like me, actually help me with hardware issues.

Can abstractions and good code practice (which I'm all for) eliminate the need for the debugger (something I see as necessary for addressing hardware bugs)?

  • 7
    I think this question could be improved by replacing the literal term "need for a debugger" by "need for debugging by tools like a debugger or logging" - I guess is that is what you actually meant. – Doc Brown Nov 1 at 17:26
  • 1
    Debuggers are the primary SE tool to catch code and logic errors, bad execution states during development of an embedded application. But afterwards, Extensive testing of embedded systems, especially under electrical, protocol/application, and environmental stress is used to root out firmware design faults under marginal conditions. In that context, The debugger is an essential diagnostic tool, but the testing is done (ideally) under, real world, live conditions. – crasic Nov 2 at 4:19
  • 2
    You'll always need to test your program, and for any non-trivial program you'll find that at least some of your tests fail, at which point you'll want to find out why they failed and how to modify your code so that it does the right thing instead... at which point you'll need to debug. You don't necessarily need a debugger to debug, though -- in many situations, you can sufficiently analyze your code's behavior by adding temporary debug-prints/logging, and later reading the generated output, if you prefer to do it that way. – Jeremy Friesner Nov 3 at 3:43
  • 3
    I would regard the behavior of the hardware to be something external to your software (although internal to the system), and handling an unexpected behavior to be a requirement that was not known in advance. I don't see any way that "choice of language and good practice" can possibly avoid debugging the system in such cases. I think the talks you're listening to are geared to a different audience. – David K Nov 3 at 13:42
  • 6
    You'll always find people bringing theory and how with good practices this and that, but having coded since the Atari 800: you need a debugger, will most likely always need one in your lifetime. The number of promises about design and methodology I heard in the last 30 years is ridiculous. Things don't work until they work and you need a debugger to speed up troubleshooting. In embedded systems or working with hardware in general, there are so many things that can't be debugged.. at least use the tools available to make work easier. And yes, good practices help, of course. – Thomas Nov 3 at 19:54
34

I think you are misrepresenting the message of the "Modern C++ in Embedded Systems" video. The point is that there are people in the embedded world that write code and then test it by running the code in the debugger to verify that it does what they think it does. He argues that a better alternative is to use abstractions so that the compiler can verify that certain assumptions about the code hold.

This method still allows to use the debugger to find bugs, especially hardware problems. You should just not use the debugger to understand code, it should be understandable and correct by writing it that way.

The advantage of using higher abstractions to validate assumptions is that there are certain types of bugs, e.g. having a function f(int mode, int value) which is called as f(value, mode), that can be completely avoided. Michael Caisse argues that using the right tools, e.g. strong types in C++, alleviates this and should therefore be used.

  • 6
    Exactly. Stepping through your program with a debugger should, in general, not be necessary to understand your own code - if it is, your code is spaghetti and should be shot (read: uses insufficient abstractions!). Of course you still must do this if something's gone wrong! – Lightness Races with Monica Nov 3 at 18:12
  • 2
    "You should just not use the debugger to understand code, it should be understandable and correct by writing it that way". This is wishful thinking - did you ever try to find a bug in other people's code? – Trantor Nov 4 at 13:56
  • 1
    This isn't anything particular to embedded computing. Lots of devs just write code for all types of things and "test" it by trying it to see if it works. Success is gauged not by the lack of bugs, but at least simply by them not manifesting during the "test". – J... Nov 4 at 18:21
  • 1
    @LightnessRaceswithMonica: arguably "something has gone wrong" is synonymous with "you don't understand your code". At least, it didn't have the effect you expected, and so there's something you don't understand. The only thing worse than it being that I don't understand my own code, is if I do understand my own code and the problem is a compiler/hardware bug... So yeah, if I start out not understanding my own code by default, then something has gone badly wrong. – Steve Jessop Nov 4 at 23:44
  • 2
    @Trantor You're missing the point. This obviously does not apply to code that is already written and must be dealt with, it applies to active development. Getting things right at a higher level of abstraction prevents errors from cascading and multiplying. Designing code well and then executing on that design will always give a better result that writing any old crap with no idea of whether it will work, and hoping you can catch all the issues with a debugger. – Matthew Read Nov 5 at 0:03
35

No, not at all !

Abstractions and good practices can of course reduce the risks of errors. For example:

  • language abstractions let the compiler generate code, that you would have to write yourself otherwise. For example, the C++ object model ensures that object constructed are destroyed as they supposed to be, without extra care on your shoulders;

  • these abstractions allow to build safer constructs that you can use in your code, such as RAII, or smart pointers that considerably alleviate the tasks related to memory management;

  • a rich container library and a powerful algorithm library further avoid that you have to write a lot of error prone code yourself by using already tested and highly optimized implementations.

But all this will only reduce probability of bugs. It will never eliminate bugs completely. So you will continue to use the debugger and log files to chase them.

  • 11
    And don't forget that RAII is not only about memory management: there's tons of resource types you want to automatically release in the correct order, for which in C you would have to resort to the goto ladder of manual unwinding. Example of such resources: interrupt flag, mutexes, states of peripherals. A good example of what you get rid of with RAII (the goto ladder) can be seen in this function in a Linux driver. – Ruslan Nov 2 at 11:41
  • 2
    @Ruslan of course ! I've put them in the same paragraph since both are about resource management, but the "that ...memory management" clause was for the smart pointers only. Sorry for the ambiguity. – Christophe Nov 2 at 11:55
  • 7
    I would add that the only thing that can avoid debugging would be a constructive mathematical proof of the algorithm and the use of certified tools and hardware. If not, then bugs are unavoidable. – Jean-Baptiste Yunès Nov 2 at 18:09
  • I'm not sure that "you can never write bug-free code" leads to "you must use a debugger", unless you're using an extremely broad definition of the word. I've worked on platforms that had no debugger whatsoever, and had to use print statements over serial, patterns on LEDs, or even oscilloscope readouts to diagnose and fix bugs. – MooseBoys Nov 4 at 4:45
  • 1
    @MooseBoys Do you really think the OP was limiting the scope of the question to a debugger application, rather than debugging techniques in general? – Barmar Nov 4 at 17:07
22

This question basically boils down to "can you write bug free code the first time every time?” The answer is always going to be no.

Yes, there are practices that can help, you can isolate modules. You can compile both for the embedded and desktop, then test and develop on the desktop. You can create hardware abstraction layers that help isolate those modules so you can test and debug them easier on PC.

There is certainly value in reducing the usage of debuggers on embedded platforms, as they are typically much slower than on PC and your REPL is therefore much slower.

But, eventually something will come up that requires a debugger of some sort. Sometimes that's a JTAG debugger, sometimes it's an oscilloscope, or a blinking LED.

  • 7
    It's possible to write bug-free code without using a debugger. I don't think anyone's making the assertion that it can always be done bug free the first time. – Robert Harvey Nov 1 at 17:32
  • 4
    @RobertHarvey: if you don't get it right the first time, how are you going to fix it? Not all bugs can be figured out solely through integration testing. The OPs language doesn't just exclude debuggers like gdb, but debuggers in the abstract such as printf logging or blinking leds. – whatsisname Nov 1 at 19:20
  • 5
    The OP focuses on the debugger specifically, and doesn't specifically exclude those other forms of debugging except by not mentioning them. – Robert Harvey Nov 1 at 19:25
  • 6
    Indeed I think the answer to this question depends on what constitutes a debugger. Clearly debuggers are not needed at all if you only classify general-purpose debugging tools designed for use with arbitrary programs a debuggers; it's perfectly possible to develop and debug entirely with "printf debugging" or blinking LEDs or whatever. – R.. Nov 2 at 15:35
  • 4
    And if a "debugger" can be interpreted as "human who does the debugging"... ;-) – R.. Nov 2 at 15:37
10

There are two basic types of software bugs:

  1. The code doesn't do what you intended.
  2. What you intended was the wrong thing to do.

The choice of languages etc may (or may not) have an impact on the first type of bug, but it has no effect at all on the second. Note, by "what you intended" I mean the real-world observable behaviour of the software, not internal design decisions.

For a real-world embedded system, the likelihood that you fully understood everything that the real world can throw at your software is, realistically, zero. So expect to go bug-hunting!

5

If everything you do is perfect, you don't need a debugger. Nobody is perfect.

There is a major class of bugs that I have seen in my career which can be described as the author thought they knew what the code was doing, but it in fact did something else. When this happens, you need a tool which shows precisely what the computer did, rather than what you thought it did. That tool is the debugger (or a suite of related tools, like hyper-paranoid levels of logging).

A framework which literally prevents you from confusing the left motor and the right motor is likely to be too restrictive to do anything interesting in. If you customize a generalized framework enough to reach this point, you have a decent sized body of code which is going to need a debugger. Indeed I ran into a case like this recently which was solved with the simultaneous application of a debugger, good software documentation, and some Lego models. I would not have wanted to solve the problem with any one of those fundamental tools missing.

There have been programmers which do the no-debugger approach. Donald Knuth was famous for thinking a program through from start to finish, and only then begin writing the code. From what I understand, his code was remarkably bug free, often compiling and running the first time! But I am certain he would appreciate a debugger for when your firmware causes a PCI-e exception to get thrown due to a timeout!

1

Debuggers, while a useful tool for many things, are by definition primarily for... de-bugging. So your question comes down to whether good practice and reliance on third party code can ever completely eliminate bugs.

[...] trusting that the choice of language and good practice makes bugs less likely, which then eliminates the need for the debugger.

As you said, even if you trust that your languages/frameworks and good practices make bugs less likely, you haven't eliminated all bugs, but reduced the likelihood of their occurrence. Without a debugger (or some similar approach such as logging), how will you diagnose those bugs that still occur?

Further, if everyone trusts their languages and frameworks 100%, how will defects in the languages/libraries themselves be discovered? Open any mainstream project on GitHub and see how many issues are reported.

Good practice can certainly reduce software defects, but even the best practices and tools will never eliminate the utility of a debugger.

I think your answer is in your own comment:

[...] many of my problems are found when hardware shows unexpected behaviour

The problem with bugs is, we never see them coming!

0

Isolating two specific use cases, in the context of embedded development:

As a development tool the debugger is essential for testing code and execution states in a sterile and controlled environment.

As a diagnostic tool the debugger is essential for diagnosing and understanding the failure modes of an embedded firmware.

So no, it cannot be eliminated, at least not completely.

Embedded devices interact with real world conditions, so the final testing of an embedded firmware is done under real world and not debugger induced stresses (environmental and application stress testing). There are "software bugs" which are purely the development realm of the software engineer (SE), and "system bugs" which are bugs which occur when the firmware interacts with the real world conditions of its embedded application.


During embedded development the SE in collaboration with the electronic engineer (EE), and mechanical engineer (ME) and project manager (PM) will define the nominal operating conditions and expected function of your firmware feature. In this development activity the debugger and the ICE/SWD device is invaluable as to enable

  • System Monitoring
  • Code Error Detection
  • Controlled Test Condition

Compared to logging, which on an embedded system can have many side effects and complexities, this is a particularly unintrusive way to develop and test with confidence that the nominal conditions are close to real world and all the immediate, "software" bugs are eliminated.


After the firmware is nominally complete, a qualification and test cycle is required. Unlike pure software there is usually a physical real world component to an embedded device and the effectiveness of the associated firmware. The environment can affect thing like

  • Rates of inputs and interrupts
  • Quality of data from external sensors
  • Operating conditions and base error rates of various protocols and I/O interfaces
  • Other environment dependent conditions.

All of this will serve to stress your embedded firmware to a point where all of you side logic and error condition checks will be stress tested. You are in rough seas, so to speak, compared to your development environment...

So an embedded device test cycle will combine

  • Environmental stress (vibration, thermal, electrical)
  • I/O Stress (external protocols and interfaces)
  • Application stress (demanding performance)
  • Any other applicable stress

In order to stress test the system, and in turn, the embedded firmware.

In that context the debugger, in combination with the ICE/SWD is an invaluable diagnostic tool to

  • Understand the nature of a bug or glitch
  • Place blame on the EE for screwing up the hardware
  • Diagnose and monitor the system after a weakness has shown up

So, even here, no, the debugger is an invaluable tool.

  • 1
    "As a development tool the debugger is essential for testing code and execution states in a sterile and controlled environment." No, it's not essential, one can certainly trigger test sequences through any sort of I/O port, with a UART being very popular. In fact it's not clear that a debugger is even useful for testing, sure it lets you look at variables that you didn't preplan to read, but structured output is far better for capturing, performing further analysis, and creating records of the test results. – Ben Voigt Nov 4 at 14:42
  • @BenVoigt how would you develop or validate the UART driver? UART irq change the device state, a SWD just pauses it. The debugger and SWD/ICE is what the manufacturer validates to read out the internal state of your device, any external tool or firmware capacity for doing readouts would need to be verified and benchmarked, likely using the debugger . Depends on your requirements and tolerance for issues of course. GPIO is also useful for fantastic timing measurements, but it's hard to read out a lot of data, a UART requires CPU time to read out any significant data (115200bd is 100us a byte). – crasic Nov 5 at 16:00
  • I develop a UART driver based on the microcontroller documentation. I validate it by counting errors from incoming invalid packets, and reading them using commands in valid packets. Way too much repetitive work to do that in an interactive debugger. The debugger really is only useful for debugging. – Ben Voigt Nov 5 at 16:09
  • @BenVoigt But if the error count is not what you expect, what will you do to interogate the system? What if there is a cpu error? The swd will let you examine all status registers. I am not suggesting you step through to understand your code I am saying that hardware errors can exist too. Even CPU errors where documentation is wrong and errata is needed. I believe you may be misinterpreting my answer or I didn't understand the question – crasic Nov 5 at 16:13
  • It definitely is valuable as a debugging and diagnostic tool. It's a horrible test and validation tool. – Ben Voigt Nov 5 at 23:16

Not the answer you're looking for? Browse other questions tagged or ask your own question.