User Tools

This is an old revision of the document!


Embedded C code doesn't have to be ugly

Preface

A contractor engineer we employed years ago was tasked with implementing a UVC-compliant functionality for the SUB2r camera we were working on. Around a year ago it became clear that this implementation was becoming increasingly unstable with the addition of new exciting capabilities and it was at that time that I had a chance to open the source files. Only to get disgusted at the dreadful state of the code. Proverbial “spaghetti code” was surely based on text in those files!

That UVC implementation was heavily based on an example implementation provided by the hardware manufacturer which was taken more or less verbatim by that said engineer and slightly masaged around to do “what the customer asked” (oh, there was one other major “improvement” - that engineer has made sure that there was a big banner with his name at the beginning of each and every file).

It was obvious that before continuing developing new code there was an obligation to pay the technical debt accumulated over all this time… This text is the observations made, lessons learnt, and guidelines emerged from that project of bringing the C code into the XXI century.

0. Understand the code you are modifying

This one is not specific to the language of choice but is a more general best practice about anything an engineer of any kind should always keep in mind when working on a project: understand the problem and the current implementation before venturing into changing things.

It may not be obvious, especially at first, why certain decisions have been made that resulted in the choices made for the code you are about to modify. For example: the original implementation was producing MPEG frames programmatically, which required a synchronization between threads (which I'm not going to dig into right now). That, now unnecessary, code was left behind and built on top of. Which eventually resulted in stability issues caused by race conditions introduced by adding more code without understanding why the code was what it was in the first place.

Needless to say that debugging race conditions in embedded code is about as much “fun” as anyone should ever indure. Ever.

Even if unit tests are not something that pop to mind when working on firmware (embedded) code, one must always make sure to fully understand the code before modifying it.

1. Use *YOUR* favorite IDE

The default SDK provided an IDE based around Eclipse. My personal preference is MS Visual Studio with its slew of features that I can no longer live without. Namely:

  • My favorite keyboard shortcuts (A mouse? It often just slows me down when I'm working with text!)
  • Code browsing database
  • “Find all references”
  • Jump to declaration/definition (including a “quick view”)
  • View call hierarchy (both “from” and “to” up and down the call stack)
  • Refactoring support
  • Intelligent renaming (which includes renaming identifiers in comments and disabled code blocks)
  • (A very intelligent!) syntax error check while editing text
  • Code block comment/uncomment with a keyboard shortcut
  • Style suggestions
  • Clang-style formatting support (via .clang-format file)

In the end all that was needed to allow MSVS to work with the code was to add a single “settings” file named CppProperties.json to reside in the same directory as the rest of the source files and use the "Open Folder" feature to start editing code.

The plan is to eventually add “actions” to that setup so that it is possible to build without switching to that Eclipse's IDE. But for now this is what the CppProperties.json file looks like (with a future-use compiler flag for when the code is eventually transitioned into C++, but that part can easily be changed back to c99 or even a more advanced c17 if needed):

CppProperties.json
  1. {
  2. "configurations": [
  3. {
  4. "name": "IoT-Release",
  5. "inheritEnvironments": [
  6. "linux_arm"
  7. ],
  8. "includePath": [
  9. "${env.FX3_INSTALL_PATH}/firmware/u3p_firmware/inc",
  10. "${projectRoot}",
  11. "${env.ARMGCC_INSTALL_PATH}/arm-none-eabi/include",
  12. "${env.ARMGCC_INSTALL_PATH}/lib/gcc/arm-none-eabi/${env.ARMGCC_VERSION}/include",
  13. "${workspaceRoot}\\**"
  14. ],
  15. "defines": [
  16. "__CYU3P_TX__=1"
  17. ],
  18. "intelliSenseMode": "linux-gcc-arm",
  19. "compilerSwitches": "-std=c++17"
  20. }
  21. ]
  22. }

N.B.: There are quite a few compiler options for the actual compilation but as they don't affect the syntax analysis (like -fabi-version=0 -fno-exceptions -fno-rtti) I opted for not including them in the settings file for now.

2. Language standard

Many smart individuals are working on improving the languages we use, providing better ways to express our goals and ideas in code, improving the code's readability and maintainability, reducing chances of making stupid mistakes. Allowing compilers to generate faster, smaller, more optimized code.

As such (barring external dependencies) I always advise on using the latest stable language standard supported by the toolchain that your organization is comfortable with. And yes, that means that engineers must continually improve their grasp of the language and be on top of the latest stable standard to efficiently take advantage of the improvements provided by that standard.

3. #define (and const) vs. enum

So much has been said about the many advantages of pushing as much work as possible away from the preprocessor and into the compiler that is amazes me to still see tons of #define's in the modern code where language standard features would provide numerous benefits over the plain old text-based code processing that knows nothing of the code structure or the data types used.

Consider a list of unique identifiers used for something, say event IDs. Specifying those with the preprocessor puts the burden of making sure the IDs are unique onto the developer. And let's be honest - the potential for silly mistakes1) is enormous here. The solution? Simple: use unnamed enums!

enum{
    event_start   // acquire resources and begin processing
  , event_end     // stop the processing and free up the resources
  , event_pause   // stop processing but keep the resources
  , event_resume
};

As an added benefit it helps with the code annotation2) which, being on the same line as the code itself, prevents comments to get shifted around and get attached to an unrelated line of code3).

Same goes for bitmap values, quite honestly:

typedef enum UsbIsoSyncAttr{
    transfer_type_iso  = (0x01 << 0)
  , sync_type_async    = (0x01 << 2)
  , sync_type_adaptive = (0x02 << 2)
  , sync_type_sync     = (0x03 << 2)
} UsbIsoSyncAttr;

That very suggestion (using enum's) is also applicable for places which are normally thought of a “const variables' space” but avoided to save some RAM runtime space4). Using enum's of a single value makes sure the value is calculated during compile time, is appropriately type-cast, and doesn't get an address in RAM associated with it during runtime execution:

enum{ s2r_uvc_stream_buf_size = s2r_ep_bulk_video_pkts_count * s2r_ep_bulk_video_pkt_size };

4. Bit manipulations vs. structured data

5. Globals

6. Code blocks' nesting

7. Code comments

1)
including mistypes, code merges, code copy-paste during refactoring(!)
2)
if needed, of course, which is definitely not every line of code, despite what some people think
3)
ensuring hours of some poor soul's chasing around the code base to answer an obvious “WTF?” question that usually arises from such shifts
4)
whether that saves space or not is a separate topic

This website uses cookies. By using the website, you agree with storing cookies on your computer. Also, you acknowledge that you have read and understand our Privacy Policy. If you do not agree, please leave the website.

More information