Learning to Write Professional Software Code Style

Futurist

Joined Apr 8, 2025
753
I know a few companies that have the policy that code should not be commented (except for standardized information at the top of each file), except as absolutely necessary (meaning that each comment has to be accepted during code review as being extremely valuable/important). When I first encountered that, I was quite taken aback. But the rationale actually makes sense, provided it is enforced during code reviews. Essentially, it adopts the position that code should be written so that it is sufficiently self-documenting, requiring very good use of variable and procedure names and structural organization that makes each code block's relevance to the task being performed extremely apparent. It zealously emphasizes readability over performance.
I worked at a place that had a rule - no comments - and I was very frustrated by that. You see I often used to write code by first writing a set of comment blocks, explaining in English what the code was (going) to do. I would actually be doing high level design as I wrote these comments and could then later "fill in" the empty spaces with real code, already thought through at a high abstract level.

That place I worked at had first rate programmers, the interviews for the role took two days full time and most of the coders were high caliber (perhaps the most intelligent teams of programmers I've ever worked with) many were ex-Microsoft and had deep insights, part of the interview was a pair-programming session where I was paired up (seated right at his desk) with an experienced coder to work with him on a task I knew little about, the goal was to gauge my ability to adapt quickly to an unfamiliar problem (and even language) and constructively work with whomever I was paired with.

But I never enjoyed the work, the restriction on comments (among other things) was a drag for me because I felt (and still feel) that code can sometimes be unintelligible without some human level explanation or description. Writing code that is "obvious in intent" is a burden that can conflict with intelligibility.

Perhaps with AI we might reach a new level of sophistication where the comments too can be analyzed and issues called out by tools. For example an AI tool could easily detect that a block of code has changed fundamentally and report that the associated comment has not been changed, perhaps an INFO message or something.

So far as @Embededd is concerned I'd encourage him to write as many comments as he wants (after all they have zero influence over code behavior) since he's learning a lot of new stuff and restricting how he expresses his thoughts isn't helpful. Over time he can review his commenting style and adapt and improve, I've often written code with very wordy comments but later, once the code is stable, go back and review and rephrase the comments.
 
Last edited:

WBahn

Joined Mar 31, 2012
32,840
I worked at a place that had a rule - no comments - and I was very frustrated by that. You see I often used to write code by first writing a set of comment blocks, explaining in English what the code was (going) to do. I would actually be doing high level design as I wrote these comments and could then later "fill in" the empty spaces with real code, already thought through at a high abstract level.

That place I worked at had first rate programmers, the interviews for the role took two days full time and most of the coders were high caliber (perhaps the most intelligent teams of programmers I've ever worked with) many were ex-Microsoft and had deep insights, part of the interview was a pair-programming session where I was paired up (seated right at his desk) with an experienced coder to work with him on a task I knew little about, the goal was to gauge my ability to adapt quickly to an unfamiliar problem (and even language) and constructively work with whomever I was paired with.

But I never enjoyed the work, the restriction on comments (among other things) was a drag for me because I felt (and still feel) that code can sometimes be unintelligible without some human level explanation or description. Writing code that is "obvious in intent" is a burden that can conflict with intelligibility.

Perhaps with AI we might reach a new level of sophistication where the comments too can be analyzed and issues called out by tools. For example an AI tool could easily detect that a block of code has changed fundamentally and report that the associated comment has not been changed, perhaps an INFO message or something.

So far as @Embededd is concerned I'd encourage him to write as many comments as he wants (after all they have zero influence over code behavior) since he's learning a lot of new stuff and restricting how he expresses his thoughts isn't helpful. Over time he can review his commenting style and adapt and improve, I've often written code with very wordy comments but later, once the code is stable, go back and review and rephrase the comments.
I generally agree. The "no comments" policy always struck me as an overreaction just as much as the "everything must be commented" policies that seemed to be common a few decades back. Both are examples of going overboard and throwing the baby out with the bathwater.

The most effective way to develop code, for me, is to design the program using a structured pseudocode style patterned after how formal legal documents are outlined (because the indenting of that style matches well with the indenting of well-structured code). It lends itself to iterative problem decomposition which allows me to expand the solution as deep as needed and to be selective -- sections that require greater development can go deeper while sections that are very straightforward can stay shallow. As soon as I've reached a point where I'm confident I can write code that implements that task, I stop. Then, much as you describe in your first paragraph, I proceed to fill in the code for each step, leaving the text to serve as a comment for that block of code, which might be a single line or might be a significant block of code that is performing an obvious task in an obvious way, provided the reader has that high-level comment to establish the context for the code.

Now, the vast majority of code I write is (or was, when I was still working) peripheral to my actual job. I wasn't a programmer, I was a design engineer who happened to write a fair amount of software to aid in the design work. As such, I have little experience working on a software development team or working on large software projects. Most of my programs were simple enough (a few hundred to several thousand lines of code) that I could write them on-the-fly, sometimes by writing little test programs to develop code for particularly complicated sections. So I tend to write programs in a way that is somewhat akin to a one-person Agile process in which I generally start with a very top-level driver that does little more than run and calls stubbed-out functions that my do nothing more than return immediately, or perhaps print out an "I got run" message. I then incrementally add features (the Agile world's "stories"), fleshing out other stubs only to the degree needed to interact with the functionality I'm focused on (for instance, a function that is supposed to determine the centroid of a signal might get hardcoded to always return 1.234, regardless of the data).

That approach generally works well for me and the way my mind works, but I know that I can do better (and that I'm being lazy and choosing not to, most of the time).

About thirty years ago I got tasked with teaching the intro C course (in an EE curriculum, not CS) at the last minute, so I just replicated the prior instructor's syllabus with new examples and exercises. That syllabus required that students submit pseudocode (or a flowchart) of their program along with the program. It was pretty obvious that what students were doing was writing the program first, and then writing up the pseudocode to match it afterwards, which completely defeats the purpose. But, I felt like hypocrite dinging them for it because that was exactly what I was doing when I wrote up the solutions. That forced me to ponder the situation and I decided that I had to do one of two things, either get rid of the expectation that they turn in pseudocode, or restructure things so that the pseudocode they turned in was actually done before they wrote the code. Since the focus of the course was on engineering problem solving, I wanted to force them to actually engage in a structured problem solving process, but at the same time I didn't want to force them to do so in a way that actually detracted from solving the problem efficiently and effectively. So the next semester I did a test. Knowing that students generally will not do something until the night before it's due, I had them submit their pseudocode three days before the program itself was due. Then, their code was required to closely match the pseudocode, with the requirement that any deviations had to be documented as to why the pseudocode was deficient and how the modified design addressed the deficiency. I was banking on the expectation that, since no code was required for the first turn-in, that most students would write their pseudocode without actually writing any code. Some students, of course, still wrote the program and then developed the pseudocode from it; not much I could do about that. But it was obvious that most didn't, and the code they turned in later bore little resemblance, at first, to the pseudocode they had submitted, which resulted in a lot of extra time spent documenting the differences (and I was pretty harsh in terms of expecting that documentation to be complete, since the goal was to encourage them to take things seriously enough up front to not have to document very many changes).

At the same time, I forced myself to do the same thing that I was expecting the students to do. I had a good idea how long it took me to solve comparable problems, since I would time myself whenever working a problem in order to gauge whether it was a reasonable assignment for them. So I decided that that semester, I would tackle every problem by developing the pseudocode completely first, before writing a single line of code, and then write the code afterward. What I discovered was that, on average, it took me somewhat more than half the total amount of time to develop the pseudocode. At first, I was dismayed by this because over half my time was gone and I still had to write all the code. But then, it ended up taking less than a quarter of the original time to implement the code, so I was seeing a significant reduction in overall time. But the truly astonishing thing was that (and, keep in mind, we are talking about C programs here) every single program, except one, compiled and ran perfectly on the first attempt. The one that didn't was missing a semicolon and that was it. All semester long and just a single syntax error and zero logic errors out of over fifty programs. I would never have anticipated that.

So what might explain it? I gave it quite a bit of thought and talked to quite a few people and we all agreed that the answer is actually pretty straightforward. When you write a program on the fly, you are having to deal with the algorithmic thinking part simultaneously with the implementation part (i.e., thinking about the logic and the syntax at the same time). Humans are not good at multitasking. By developing the pseudocode first, you are completely focused on the algorithm, including the corner cases. When you then start writing the code, you are free from being concerned about the logic and can focus on correctly implementing the code, including all the syntax details.

It's very powerful and effective, and I seldom do it in practice. Why? Because I'm lazy and want to get right into playing with the code. Human nature. It actually takes more time, but it doesn't feel like it. But when things get a bit more complicated, I'm quick to set the computer aside and go back to this approach for the portion of the program that is being troublesome.
 

Futurist

Joined Apr 8, 2025
753
I generally agree. The "no comments" policy always struck me as an overreaction just as much as the "everything must be commented" policies that seemed to be common a few decades back. Both are examples of going overboard and throwing the baby out with the bathwater.

The most effective way to develop code, for me, is to design the program using a structured pseudocode style patterned after how formal legal documents are outlined (because the indenting of that style matches well with the indenting of well-structured code). It lends itself to iterative problem decomposition which allows me to expand the solution as deep as needed and to be selective -- sections that require greater development can go deeper while sections that are very straightforward can stay shallow. As soon as I've reached a point where I'm confident I can write code that implements that task, I stop. Then, much as you describe in your first paragraph, I proceed to fill in the code for each step, leaving the text to serve as a comment for that block of code, which might be a single line or might be a significant block of code that is performing an obvious task in an obvious way, provided the reader has that high-level comment to establish the context for the code.

Now, the vast majority of code I write is (or was, when I was still working) peripheral to my actual job. I wasn't a programmer, I was a design engineer who happened to write a fair amount of software to aid in the design work. As such, I have little experience working on a software development team or working on large software projects. Most of my programs were simple enough (a few hundred to several thousand lines of code) that I could write them on-the-fly, sometimes by writing little test programs to develop code for particularly complicated sections. So I tend to write programs in a way that is somewhat akin to a one-person Agile process in which I generally start with a very top-level driver that does little more than run and calls stubbed-out functions that my do nothing more than return immediately, or perhaps print out an "I got run" message. I then incrementally add features (the Agile world's "stories"), fleshing out other stubs only to the degree needed to interact with the functionality I'm focused on (for instance, a function that is supposed to determine the centroid of a signal might get hardcoded to always return 1.234, regardless of the data).

That approach generally works well for me and the way my mind works, but I know that I can do better (and that I'm being lazy and choosing not to, most of the time).

About thirty years ago I got tasked with teaching the intro C course (in an EE curriculum, not CS) at the last minute, so I just replicated the prior instructor's syllabus with new examples and exercises. That syllabus required that students submit pseudocode (or a flowchart) of their program along with the program. It was pretty obvious that what students were doing was writing the program first, and then writing up the pseudocode to match it afterwards, which completely defeats the purpose. But, I felt like hypocrite dinging them for it because that was exactly what I was doing when I wrote up the solutions. That forced me to ponder the situation and I decided that I had to do one of two things, either get rid of the expectation that they turn in pseudocode, or restructure things so that the pseudocode they turned in was actually done before they wrote the code. Since the focus of the course was on engineering problem solving, I wanted to force them to actually engage in a structured problem solving process, but at the same time I didn't want to force them to do so in a way that actually detracted from solving the problem efficiently and effectively. So the next semester I did a test. Knowing that students generally will not do something until the night before it's due, I had them submit their pseudocode three days before the program itself was due. Then, their code was required to closely match the pseudocode, with the requirement that any deviations had to be documented as to why the pseudocode was deficient and how the modified design addressed the deficiency. I was banking on the expectation that, since no code was required for the first turn-in, that most students would write their pseudocode without actually writing any code. Some students, of course, still wrote the program and then developed the pseudocode from it; not much I could do about that. But it was obvious that most didn't, and the code they turned in later bore little resemblance, at first, to the pseudocode they had submitted, which resulted in a lot of extra time spent documenting the differences (and I was pretty harsh in terms of expecting that documentation to be complete, since the goal was to encourage them to take things seriously enough up front to not have to document very many changes).

At the same time, I forced myself to do the same thing that I was expecting the students to do. I had a good idea how long it took me to solve comparable problems, since I would time myself whenever working a problem in order to gauge whether it was a reasonable assignment for them. So I decided that that semester, I would tackle every problem by developing the pseudocode completely first, before writing a single line of code, and then write the code afterward. What I discovered was that, on average, it took me somewhat more than half the total amount of time to develop the pseudocode. At first, I was dismayed by this because over half my time was gone and I still had to write all the code. But then, it ended up taking less than a quarter of the original time to implement the code, so I was seeing a significant reduction in overall time. But the truly astonishing thing was that (and, keep in mind, we are talking about C programs here) every single program, except one, compiled and ran perfectly on the first attempt. The one that didn't was missing a semicolon and that was it. All semester long and just a single syntax error and zero logic errors out of over fifty programs. I would never have anticipated that.

So what might explain it? I gave it quite a bit of thought and talked to quite a few people and we all agreed that the answer is actually pretty straightforward. When you write a program on the fly, you are having to deal with the algorithmic thinking part simultaneously with the implementation part (i.e., thinking about the logic and the syntax at the same time). Humans are not good at multitasking. By developing the pseudocode first, you are completely focused on the algorithm, including the corner cases. When you then start writing the code, you are free from being concerned about the logic and can focus on correctly implementing the code, including all the syntax details.

It's very powerful and effective, and I seldom do it in practice. Why? Because I'm lazy and want to get right into playing with the code. Human nature. It actually takes more time, but it doesn't feel like it. But when things get a bit more complicated, I'm quick to set the computer aside and go back to this approach for the portion of the program that is being troublesome.
That's certainly a very thorough and decent approach to the teaching duty, telling students to do something just because, is never a good idea.

Now on the subject of pseudo code, its interesting how modern, professional software designers simply don't bother with this anymore, certainly in work connected with UI and OO and web apps.

Most of the people I encounter in places like Microsoft no longer concern themselves with this, the tooling is being specifically developed to reduce the need for stuff like flowcharts and pseudo code. For example working with .Net today is a breeze, it is possible to work directly at the code level, the tooling and languages are being designed and enhanced to facilitate working purely at the code level.

You can see this in action just by watching a tutorial video on stuff like Blazor or LINQ in .Net, unless one works with these kinds of technologies it won't be apparent just how workable this approach is.
 

WBahn

Joined Mar 31, 2012
32,840
That's certainly a very thorough and decent approach to the teaching duty, telling students to do something just because, is never a good idea.

Now on the subject of pseudo code, its interesting how modern, professional software designers simply don't bother with this anymore, certainly in work connected with UI and OO and web apps.

Most of the people I encounter in places like Microsoft no longer concern themselves with this, the tooling is being specifically developed to reduce the need for stuff like flowcharts and pseudo code. For example working with .Net today is a breeze, it is possible to work directly at the code level, the tooling and languages are being designed and enhanced to facilitate working purely at the code level.

You can see this in action just by watching a tutorial video on stuff like Blazor or LINQ in .Net, unless one works with these kinds of technologies it won't be apparent just how workable this approach is.
I can see that. Almost all of my programs are console programs (scientific computing one one kind or another) without the need for a GUI. But, the extremely limited exposure I have had to writing GUI-based applications would make me agree that using a pseudocode/flowchart based approach for the whole thing would be difficult and not very useful. Using it for the snippets and functions that get invoked by the program might be useful, at least for ones that are sufficiently complex. But most of the code wouldn't fall into that category.

But it still leaves me concerned about the broader picture. Are people that are brought up in that kind of development environment really learning how to develop solutions, or are they primarily being trained how to be successful monkeys at using the tools someone else wrote for them? That's always been an issue in almost all technical fields and we increasingly become victims of our own technological success. We need to rely on the advancement of the tools to continue doing more advanced stuff, but we become increasing divorced from understanding the fundamentals upon which those tools are based. I believe that will come back to haunt us -- in fact, I think it is doing so already and that it will only get worse as time goes by.
 

Futurist

Joined Apr 8, 2025
753
I can see that. Almost all of my programs are console programs (scientific computing one one kind or another) without the need for a GUI. But, the extremely limited exposure I have had to writing GUI-based applications would make me agree that using a pseudocode/flowchart based approach for the whole thing would be difficult and not very useful. Using it for the snippets and functions that get invoked by the program might be useful, at least for ones that are sufficiently complex. But most of the code wouldn't fall into that category.

But it still leaves me concerned about the broader picture. Are people that are brought up in that kind of development environment really learning how to develop solutions, or are they primarily being trained how to be successful monkeys at using the tools someone else wrote for them? That's always been an issue in almost all technical fields and we increasingly become victims of our own technological success. We need to rely on the advancement of the tools to continue doing more advanced stuff, but we become increasing divorced from understanding the fundamentals upon which those tools are based. I believe that will come back to haunt us -- in fact, I think it is doing so already and that it will only get worse as time goes by.
You're an oldie like me, I share that skepticism about jumping right in to coding, although I do it often enough myself.

I think the way languages abstract today is far better than forty years ago, for example with something like LINQ (in .Net) one quickly learns to think in sequences, streams of items and how to filter and order and transform the items. Perhaps not (yet) readily available in the low level MCU world though.

Stuff like LINQ can eliminate loops, making the code cleaner and easier to relate to, the common loop constructs have led many of us to think in terms of loops, and that's not a great idea for most problems, its even regarded as an "old fashioned" abstraction these days.

For example if we pulled a sequence of user items from some database, and were tasked with ordering them by seniority, then by second name and then transform each one to the persons passport photograph, this is a no-brainer with something like LINQ, we'd write:

C#:
var images = user_stream.Where(u => u.FullTime).OrderBy(u => u.Rank).ThenBy(u => u.LastName).Select(LookupImage);
No loops and in fact even simple manipulations of small collections can be manipulated this way, no overhead at all, the "images" variable refers to a sequence of images whereas we began by consuming a sequence of user items (these could be structures like we see in C).

In the IDE we can hover over any of that and get clear summaries of what it is, making it easy to write/iterate/run. So in days gone by one might have represented that statement with some kind if pseudo code that expressed loops, whereas today the very idea of a loop is no longer fundamental, functional languages are even more expressive, but again not well suited to MCU code.
 

Thread Starter

Embededd

Joined Jun 4, 2025
156
So to make my code modular, I’ve started splitting things into separate modules like LCD.c, UART.c, I2C.c, DS1307.c with their header files and of course main.c . Basically, when I think about portability, my idea is that the code should be able to run on any platform.

But in my situation, if I move from an ATmega to an ARM, then all the source files I’ve written would need to be re-done, because the register configurations are completely different between the two. So my question is: what exactly do I need to do in order to write portable code that won’t need a complete rewrite when I change hardware?

@WBahn @MrChips @Futurist @BobTPH @Ya’akov @drjohsmith @Dave Lowther
 
Last edited:

Ya’akov

Joined Jan 27, 2019
10,235
I wasn't referring specifically to commenting, that like other issues of style is often also governed by chosen standards though.

If one is delivering source code to a customer then yes, they might well have requirements there but in cases where source is not a deliverable then it is the team's standards that dictate.

The fact is there are always standards to adhere to sometimes they are just unstated (like when working in isolation).

Standards are an ever present reality and so ideally should be explicit and shared, the value of such standards is to protect the company's investment in creating source code, it has long term value in that code written ten years ago by people long since gone is as readable today to a team as it was ten years ago.

Having said all that, comments are a special case because they must always be manually maintained when code changes, I'm sure we've all seen large codebases where heavily worked on code has comments stemming from day one and are now out of step with the lates code, outdated, inaccurate comments are worse than none at all.
Consistency certainly is important in any code base. It makes returning to old code not so much like rewriting it.

So I think we agree on that aspect.
 

Ya’akov

Joined Jan 27, 2019
10,235
I worked at a place that had a rule - no comments - and I was very frustrated by that. You see I often used to write code by first writing a set of comment blocks, explaining in English what the code was (going) to do. I would actually be doing high level design as I wrote these comments and could then later "fill in" the empty spaces with real code, already thought through at a high abstract level.

That place I worked at had first rate programmers, the interviews for the role took two days full time and most of the coders were high caliber (perhaps the most intelligent teams of programmers I've ever worked with) many were ex-Microsoft and had deep insights, part of the interview was a pair-programming session where I was paired up (seated right at his desk) with an experienced coder to work with him on a task I knew little about, the goal was to gauge my ability to adapt quickly to an unfamiliar problem (and even language) and constructively work with whomever I was paired with.

But I never enjoyed the work, the restriction on comments (among other things) was a drag for me because I felt (and still feel) that code can sometimes be unintelligible without some human level explanation or description. Writing code that is "obvious in intent" is a burden that can conflict with intelligibility.

Perhaps with AI we might reach a new level of sophistication where the comments too can be analyzed and issues called out by tools. For example an AI tool could easily detect that a block of code has changed fundamentally and report that the associated comment has not been changed, perhaps an INFO message or something.

So far as @Embededd is concerned I'd encourage him to write as many comments as he wants (after all they have zero influence over code behavior) since he's learning a lot of new stuff and restricting how he expresses his thoughts isn't helpful. Over time he can review his commenting style and adapt and improve, I've often written code with very wordy comments but later, once the code is stable, go back and review and rephrase the comments.
I have often used "levels" of comments where indentation indicates the urgency to read. That is, comments intended to elucidate a surprising behavior would live in the first level, constant and variable definitions in the second, documentation extracts in the third and "working notes", intended entirely to support the current effort, in the fourth.

The idea being that reading the code it wasn't necessary to look at every comment to be sure the important information was conveyed.

I worked primarily in perl, and it's pod (Plain Old Documentation) markup and perldoc utility was an excellent way to make versions of the code for review that included formatted for review. I spent a fair amount of time writing scripts for presentation of the code and internal documentation.

It's been a long time since I worked on a large programming project.
 

WBahn

Joined Mar 31, 2012
32,840
So to make my code modular, I’ve started splitting things into separate modules like LCD.c, UART.c, I2C.c, DS1307.c with their header files and of course main.c . Basically, when I think about portability, my idea is that the code should be able to run on any platform.

But in my situation, if I move from an ATmega to an ARM, then all the source files I’ve written would need to be re-done, because the register configurations are completely different between the two. So my question is: what exactly do I need to do in order to write portable code that won’t need a complete rewrite when I change hardware?

@WBahn @MrChips @Futurist @BobTPH @Ya’akov @drjohsmith @Dave Lowther
The modularity aspect isn't so much to make the code so that it is portable between platforms -- as you've noted, that is difficult with MCU code. Think of it more like plug-and-play for projects on the same platform. If tomorrow's project needs an LCD, then you include LCD.h and now have access to all of the functions needed to use that LCD. But if a project doesn't need it, you don't include it. It's the same with the standard libraries -- if you need functions from the math library, you include math.h, otherwise you don't.

But you will likely add different LCD modules that you want to support, so you either needed different files for different modules, or you can put the code for all of the modules into a single file and use #define macros and #ifdef blocks to "activate" the module you are using right now. Your goal is then to develop an LCD interface protocol for your projects that is consistent at the level of the code using the module, and take care of the differences in the LCD.c code.

You can do the same thing to hide the differences between MCU families, as well.
 

Thread Starter

Embededd

Joined Jun 4, 2025
156
The modularity aspect isn't so much to make the code so that it is portable between platforms -- as you've noted, that is difficult with MCU code. Think of it more like plug-and-play for projects on the same platform. If tomorrow's project needs an LCD, then you include LCD.h and now have access to all of the functions needed to use that LCD. But if a project doesn't need it, you don't include it. It's the same with the standard libraries -- if you need functions from the math library, you include math.h, otherwise you don't.

But you will likely add different LCD modules that you want to support, so you either needed different files for different modules, or you can put the code for all of the modules into a single file and use #define macros and #ifdef blocks to "activate" the module you are using right now. Your goal is then to develop an LCD interface protocol for your projects that is consistent at the level of the code using the module, and take care of the differences in the LCD.c code.

You can do the same thing to hide the differences between MCU families, as well.
So if I’m using the same MCU (say ATmega8A) but I change the external device for example switching from DS1307 to DS3231, or from one LCD to another then my main.c can stay the same, since it just calls the high-level functions like RTC_ReadTime() or LCD_Print(). I’d only need to modify or replace the driver file (like DS1307.c -> DS3231.c), as long as the function interface remains consistent.

And for portability if later I switch from ATmega8A to a different MCU family (like ARM or PIC) then ideally I should be able to reuse the same main.c and just re-implement the low-level driver that handles the hardware registers.

Am I understanding this correctly?
 

WBahn

Joined Mar 31, 2012
32,840
So if I’m using the same MCU (say ATmega8A) but I change the external device for example switching from DS1307 to DS3231, or from one LCD to another then my main.c can stay the same, since it just calls the high-level functions like RTC_ReadTime() or LCD_Print(). I’d only need to modify or replace the driver file (like DS1307.c -> DS3231.c), as long as the function interface remains consistent.

And for portability if later I switch from ATmega8A to a different MCU family (like ARM or PIC) then ideally I should be able to reuse the same main.c and just re-implement the low-level driver that handles the hardware registers.

Am I understanding this correctly?
For the most part. A lot depends on how much effort you are willing to put into things. You could imagine having a guide file, such as devices.h, which you use to define which specific parts you are using in that design. For instance, you might have

#define DEVICES_LCD_1602
#define DEVICES_MCU_ATMEGA328

Then, your single devices_lcd.c file includes devices.h and then uses #ifdef statement to select the code portions that will be compiled.

This requires some discipline on your part, but it might pay off handsomely down the road by enabling you to quickly port projects to new combinations of devices and to add new devices to your stable.

But, of course, nothing ever goes as smoothly as it seems it should, so don't assume that everything will play nice as you make changes until you've gotten some experience in doing it.
 

Thread Starter

Embededd

Joined Jun 4, 2025
156
For the most part. A lot depends on how much effort you are willing to put into things. You could imagine having a guide file, such as devices.h, which you use to define which specific parts you are using in that design. For instance, you might have

#define DEVICES_LCD_1602
#define DEVICES_MCU_ATMEGA328

Then, your single devices_lcd.c file includes devices.h and then uses #ifdef statement to select the code portions that will be compiled.

This requires some discipline on your part, but it might pay off handsomely down the road by enabling you to quickly port projects to new combinations of devices and to add new devices to your stable.

But, of course, nothing ever goes as smoothly as it seems it should, so don't assume that everything will play nice as you make changes until you've gotten some experience in doing it.
Just wanted to let you know my modular drivers are all working fine. I’ve got LCD, UART, and I2C running separately, and I tested them with a simple main file.

So basically, each module works on its own, and now I can call them from main
C:
/*
* main.c
* Purpose:
*   Simple modular test for LCD and I2C drivers.
*   - Displays "System Ready" on LCD (stays visible)
*   - Continuously checks DS1307 ACK over I2C
*   - Sends ACK/NACK to UART
*
* Hardware:
*   MCU: ATmega8A @ 12 MHz external crystal
*   LCD: 16x2 (4-bit mode)
*   DS1307 RTC at I2C address 0x68
*/

#include "config.h"
#include "UART.h"
#include "I2C.h"
#include "LCD.h"

#define DS1307_ADDR  0x68

int main(void)
{
    UART_Init();
    I2C_Init();
    LCD_Init();

    // Show startup message once
    LCD_String("System Ready");
    UART_SendString("System Ready\r\n");

    _delay_ms(1000); // Wait briefly for user to see

    // Main test loop
    while (1)
    {
        if (I2C_Start())
        {
            if (I2C_WriteAddress(DS1307_ADDR << 1))
                UART_SendString("I2C: ACK\r\n");
            else
                UART_SendString("I2C: NACK\r\n");

            I2C_Stop();
        }
        else
        {
            UART_SendString("I2C: START FAIL\r\n");
        }

        _delay_ms(1000); // repeat every second
    }
}
Terminal Output

1759578295579.png
 

Thread Starter

Embededd

Joined Jun 4, 2025
156
For the most part. A lot depends on how much effort you are willing to put into things.

But, of course, nothing ever goes as smoothly as it seems it should, so don't assume that everything will play nice as you make changes until you've gotten some experience in doing it.
Okay, so I think I’ve got a pretty good idea on what modular, readable, and portable code means now but I’m still not totally sure what people mean when they talk about maintainable code. How’s that different? What exactly makes code "maintainable" in real projects?
 

Futurist

Joined Apr 8, 2025
753
Okay, so I think I’ve got a pretty good idea on what modular, readable, and portable code means now but I’m still not totally sure what people mean when they talk about maintainable code. How’s that different? What exactly makes code "maintainable" in real projects?
It's subjective, a key factor though is simply standards adherence, by adhering to the same standards all the other code does, that goes a long way toward maintainability, in fact that's one of the reasons for standards.

It should be possible for any team member to look at any other team members code and see more or less what they would have written themselves.

Things that hurt maintainability include:

  • poor variable/function names
  • hard coded values
  • stuff that "shows off" the writers skills or knowledge
  • emphasis on happy paths
  • inconsistencies across multiple functions and source files.
 

WBahn

Joined Mar 31, 2012
32,840
Okay, so I think I’ve got a pretty good idea on what modular, readable, and portable code means now but I’m still not totally sure what people mean when they talk about maintainable code. How’s that different? What exactly makes code "maintainable" in real projects?
Perhaps an example might shed some light on the concept.

The first programming class I took was as a junior in high school and we had to write a high-low game (in BASIC) for the final project. It was a trivial program requirement -- you just had to make up some silly banter and have the program generate a random number and then get input from the user and tell them whether the their guess was higher or lower until they either got it right or ran out of guesses. This was a course aimed at kids that needed one more math credit to graduate -- the guy that graduated last in my class was taking it the same semester I did. But it was the only programming course offered, and I (and some others) quickly went well beyond the formal course. So I did a 2-D character-graphics game in which a random location was picked on the screen and the user had so much energy to take shots at the hidden target. The more energy they used, the further away the target could be and still be scored a hit. At max energy, they got two shots, at min energy, they got ten. Each miss put an asterisk at the aim point and two arrows indicating direction to the real location.

My printout was about five pages long, and the game ran pretty slowly. So I spent a lot of time optimizing it for speed and eventually got the code down to about a page and a half, nearly a page of which was the instructions that were printed out at the beginning. It ran very fast. The teacher used it as a demo for future classes and during my senior year a student found a bug in its behavior. As soon as I saw it, I knew what the problem was and how to fix it. But I could figure out how to implement the fix in the optimized code because I couldn't spot the exact place where the mistake was happening. I went back to the original unoptimized code and found the mistake and fixed it within a few minutes. But I never was able to fix the optimized version because the code was so dense that I could follow it, even though I had written it. Now, I suspect that if I had that code today (and I really wish I still did!) that I could fix it pretty quickly, but at the time my debugging skills just weren't up to the task.

The bottom line is that the original version was hugely more maintainable than the optimized version. This is a common tradeoff that is made -- performance verses maintainability. But, with today's processing power, even in the MCU world, it is a tradeoff that is seldom justified (but, there are certainly times when it is). Unfortunately, programmers like to be cute and clever and figure out tricks. These may improve the performance, but they usually trash maintainability -- and the improved performance is seldom actually needed.
 

simozz

Joined Jul 23, 2017
170
I worked in a company where the team didn't like comments. Well, it was actually prohibited to comment the code...

In addition, they used to acronymize variables/registers/wires names, which made even more difficult to understand the why..

Last but not least, no block diagrams at all.

Analyzing their code was a nightmare.
 

WBahn

Joined Mar 31, 2012
32,840
I worked in a company where the team didn't like comments. Well, it was actually prohibited to comment the code...

In addition, they used to acronymize variables/registers/wires names, which made even more difficult to understand the why..

Last but not least, no block diagrams at all.

Analyzing their code was a nightmare.
Sounds like a too-dogmatic approach that was imposed by someone based on what they had been taught or wanted to be true without any consideration of the real world.

Adopting a no-comments style rule is fine and can work, but it needs to have allowances for exceptions and the entirety of the rest of the rules and workflow need to be designed to be in harmony with it.
 

simozz

Joined Jul 23, 2017
170
It was just the way they always worked and adopted during the years as a small team.

The Verification engineer lead was particularly reluctant to comments.

I remember looking at its code for a functional model where variables names were "data0 data1 data2" and so on instead of naming what actually the arguments meaning was.
No empty lines neither spaces allowed.. Of course you couldn't disagree about it...

Many times I felt like reading a text without commas/periods/paragraphs.. And I am totally reluctant to this style.
 

Futurist

Joined Apr 8, 2025
753
It was just the way they always worked and adopted during the years as a small team.

The Verification engineer lead was particularly reluctant to comments.

I remember looking at its code for a functional model where variables names were "data0 data1 data2" and so on instead of naming what actually the arguments meaning was.
No empty lines neither spaces allowed.. Of course you couldn't disagree about it...

Many times I felt like reading a text without commas/periods/paragraphs.. And I am totally reluctant to this style.
One of the enjoyable aspects of personal projects is that one is free of such constraints, no need to adhere to silly rules.
 

WBahn

Joined Mar 31, 2012
32,840
One of the enjoyable aspects of personal projects is that one is free of such constraints, no need to adhere to silly rules.
The flip side is that you don't get to benefit from the non-silly rules, either because you aren't aware of the good ones, or the fact that even the good ones that we know are good and useful still requite a level of externally-imposed discipline to get us to follow them.

A really good example, though unrelated to software, is the notion of the CTK -- Consolidated Took Kit -- that became widely used (and mandated) by aircraft maintainers in the 1980s time frame. This required that every tool used on the flight line or in a shop be individually marked (typically via engraving) with a tool kit number and that the toolkit have a specific place for each tool such that a quick visual inspection revealed whether or not every tool was there. The kits then had to be inventoried and signed out prior any work being done and then inventoried and signed in by someone else after work was done.

Setting up a new kit was a royal pain in the ass. It often took weeks to engrave every tool (including individual Allen wrenches and drill bits) and to painstakingly make the cutouts (typically in painted foam rubber sheets). Then there was the hassle of inventorying the box anytime it was used. At the shop level, this was twice since we could inventory it at the beginning of shift and again at end of shift. We had three main toolboxes we used most days, and so we generally had the first person in the shop inventory all three and then someone else inventory and close all three at the end of shift (which was immediately followed by the next shift opening and inventorying them). But on the flight line, this process was repeated for every job. If I checked out a box to work on Plane A, when I was done I had to check the box back in and check it back out before I proceeded to Plane B.

Was it a hassle? Yes.

Did it pay off? Big time. When the Air Force introduced the CTK mandates, FOD (foreign object damage) rates dropped by about 90% immediately.

Were there subtle benefits that went beyond the FOD prevention? Yes, and they more than paid for the time and effort to set up and run the system.

How many of us routinely spend significant amounts of time trying to find tools that we have misplaced, even when we "know" exactly where we last put them? How many of us have ended up going to the hardware store and buying a new tool even though we know we already have several of that same tool? How often have we wasted time scrounging through a tool box looking for the right size wrench or socket because they are all jumbled together (only to sometimes find, after multiple passes through all of them that are in the box, that the one we need isn't there -- taking use back to the prior point)?

With a CTK, you know exactly where the tool is. You can count on it being there. If you need a bigger wrench or socket, it is immediately to the right of the one you just tried. These small-scale but ongoing improvements in efficiency (and reductions in frustration) more than pay for the headaches of setting them up. Also, in a home or typical work environment, there's really no need for the kind of rigid ongoing inventory control that a maintenance facility needs, so we really are talking about one-time up-front costs. There's also no need to engrave each tool, just to make the "each-tool-has-one-obvious-home" set up.

Despite this, I've had grand plans to set up my tools (or at least the bulk of them) this way for nearly forty years and still haven't done it. I've managed to make two tiny CTKs (each with a half dozen items in it) at home. Why? Because it IS a major pain to set one up and there are always other more-pressing things to do. Despite the fact that I absolutely know that I would have significantly more time to do those things in the future if I wasn't always playing the tool-hunt game.

I did manage to set up a small kit at work that had the majority of the tools we needed, and it significantly sped up working in the lab. But, since I didn't have the authority to impose on everyone that they had to put tools away, I ended up rounding up tools about once a week and putting them were they belonged. It was worth it to me, personally, because I wanted them to usually be where I expected them. Once someone else took over the lab, the first thing they did was throw away my foam cutouts and just dump the tools in the drawers, and almost immediately everyone was playing tool-hunt on a regular basis.

So, this is something that generally requires a word-from-on-high imposition of discipline to benefit from.

The same is true in software style guidelines. Most people lack the self-discipline to adhere to good, consistent style even when they fully appreciate the value of doing so.

EDIT: Fix typos.
 
Last edited:
Top