Learning to Write Professional Software Code Style

Futurist

Joined Apr 8, 2025
748
@WBahn

I stand corrected, seems even C89 stated that same equivalence, so thank you for the correction, I'm going to have a word with Copilot about this...

Now, why does the OP get different results from each form? there must be something odd going on...
 
Last edited:

Thread Starter

Embededd

Joined Jun 4, 2025
153
Can you @WBahn tell me which C standard I should actually follow? In post #19 it was suggested to look at MISRA, but I think that’s mainly for automotive software. For my case, should I stick with C99 or ANSI C?

Basically, which standard would be best for me to follow?
 

Futurist

Joined Apr 8, 2025
748
I’m attaching an image

View attachment 356549
What MCU is this? is it a dev board? what is the dev environment you're using? do you know which language version it supports? who wrote the compiler?

Ber careful too, it's possible that the declaration wasn't the only code difference, so be careful and prove to yourself that simply changing that declaration causes the difference.
 

Thread Starter

Embededd

Joined Jun 4, 2025
153
What MCU is this? is it a dev board? what is the dev environment you're using? do you know which language version it supports? who wrote the compiler?
The MCU I’m using is an ATmega8A, as I mentioned in the code. It’s not a development board I’ve placed all the components on a breadboard. I’m using Microchip Studio (formerly Atmel Studio) as my development environment. the compiler is avr-gcc
 

WBahn

Joined Mar 31, 2012
32,823
Which one?
The one I cited is the current one, namely C23 (though all I have is n3096, which is the last publicly available draft before the standard). The same grammar syntax is used all the way back to K&R.

I just downloaded n3220, which is the first working draft of the next standard, but it is supposedly identical to the C23 standard except for a fix to one footnote in one appendix. It is still in agreement with this and I can't imagine something like this ever changing. In n3220, the clause is 6.7.11, para 15.
 

WBahn

Joined Mar 31, 2012
32,823
@WBahn

I stand corrected, seems even C89 stated that same equivalence, so thank you for the correction, I'm going to have a word with Copilot about this...
It'll just complement you on your amazing attention to detail and tell you that you are absolutely right (even if you aren't).

Now, why does the OP get different results from each form? there must be something odd going on...
C implementations for MCUs are frequently not standards-compliant.

As an aside, it's been shown that it is actually impossible to implement a C compiler that is in absolute, strict compliance with the standard (and I think this has since been extended to at least imply that it is probably true for all languages). It's been many years since I read the original paper and I couldn't track it down in the couple minutes I was willing to devote to the attempt.
 

Futurist

Joined Apr 8, 2025
748
It'll just complement you on your amazing attention to detail and tell you that you are absolutely right (even if you aren't).



C implementations for MCUs are frequently not standards-compliant.

As an aside, it's been shown that it is actually impossible to implement a C compiler that is in absolute, strict compliance with the standard (and I think this has since been extended to at least imply that it is probably true for all languages). It's been many years since I read the original paper and I couldn't track it down in the couple minutes I was willing to devote to the attempt.
Well any language standard has to define the syntax and the semantics and must itself be expressed in some language (a metalanguage, English BNF etc).

So I can see how "strict compliance" is a challenge.

Why are MCU oriented C compilers often not compliant though?
 

WBahn

Joined Mar 31, 2012
32,823
Can you @WBahn tell me which C standard I should actually follow? In post #19 it was suggested to look at MISRA, but I think that’s mainly for automotive software. For my case, should I stick with C99 or ANSI C?

Basically, which standard would be best for me to follow?
MISRA was originally developed with automotive applications in mind, but it is not automotive-specific. It is widely accepted in many industries as a solid framework for best-practices.

As for which version of the language standard to use, that is often not something that you have complete control over. You can't write C99 code if the only tools for your platform are C89. Also, many embedded environments rely heavily on implementation-specific extensions to the language. So, at the end of the day, you can only do the best you can.

I would say that you should avoid, to the degree possible, using undefined or implementation-defined constructs. Become very familiar with the standard by referring to it often. There is a LOT of good information there, but it is not something that you can digest casually or all at once.

You are balancing a couple of competing things. By using new features of the language introduced in later versions, you are making your code less portable because it (or pieces of it being reused in other applications) won't compile under older versions. But, those new features were introduced for a reason and by not using them out of fear of portability issues, you rob yourself of the ability to leverage them where they make sense. So I would say that you should use the features of the language that really make sense and add clear value, but avoid using them just because they are there and you can.
 

WBahn

Joined Mar 31, 2012
32,823
Well any language standard has to define the syntax and the semantics and must itself be expressed in some language (a metalanguage, English BNF etc).

So I can see how "strict compliance" is a challenge.

Why are MCU oriented C compilers often not compliant though?
When you are writing programs that run on bare metal, you need to be able to work down very close to the hardware. But C is a high-level language that intentionally abstracts much of the hardware details away. So C compilers for embedded systems need to be extended to provide the necessary level of control and access. A lot of this is buried in the header files and macro definitions, but not all of it.
 

Thread Starter

Embededd

Joined Jun 4, 2025
153
I’m posting an improved version of my code for feedback and review. I’ve really tried not to leave any gaps this time and put effort into making it clean.

Do you think this can be considered a fixed module just for checking the LCD, or do you feel there’s still room for more improvement?

I’d really appreciate your thoughts on overall coding style.

C:
/*
* LCD_Test.c
*
* Purpose:
* Verify LCD connectivity and functionality with ATmega8A
* in 4-bit interface mode by displaying the text "LCD Test".
* Atmega8
* Microchip Studio
* C language
* Created: 29-09-2025 17:12:04
* Author : Developer
*/

#include <avr/io.h>
#define F_CPU (12000000UL)
#include <util/delay.h>

// Control pin mapping
#define RS   (PC0)
#define EN   (PC1)

// Data and control ports
#define LCD_PORT     (PORTD)
#define LCD_DDR      (DDRD)
#define CTRL_PORT    (PORTC)
#define CTRL_DDR     (DDRC)

/* LCD Data Line Mapping
   D4 → PD2
   D5 → PD3
   D6 → PD4
   D7 → PD5
*/
#define LCD_DATA_SHIFT   (2)
#define LCD_DATA_MASK    ((0x0F) << (LCD_DATA_SHIFT))
#define LCD_PORT_MASK    (~(LCD_DATA_MASK))

/* Nibble masks */
#define LCD_HIGH_NIBBLE  (0xF0)
#define LCD_LOW_NIBBLE   (0x0F)

/* LCD Commands */
#define CLEAR_DISPLAY    (0x01)
#define DISPLAY_ON       (0x0C)
#define FOUR_BIT_MODE    (0x28)
#define ENTRY_MODE       (0x06)

/* LCD Initialization sequence commands */
#define INIT_8BIT_MODE   (0x33)
#define INIT_4BIT_MODE   (0x32)

/* Start address of each line */
#define LINE_ONE (0x00)
#define LINE_TWO (0x40)

// Function prototypes
void HW_Config(void);
void LCD_CMD(unsigned char Command);
void LCD_DATA(unsigned char Data);
void LCD_INIT(void);
void LCD_PULSE(void);
void LCD_Message(char *str);


/*****
*
*  main  -  Entry point of program
*
*****/
int main(void)
{
    char *Name = "LCD Test";

    HW_Config();     // Configure MCU I/O for LCD
    LCD_INIT();      // Run LCD init sequence
    LCD_Message(Name);

    while (1);       // Nothing else to do; static display
}


/*****
*
*  HW_Config  -  Configure microcontroller hardware for LCD
*
*****/
void HW_Config(void)
{
    (LCD_DDR) |= (LCD_DATA_MASK);      
    (CTRL_DDR) |= ((1 << (RS)) | (1 << (EN)));
    _delay_ms(50);                  
}


/*****
*
*  LCD_INIT  -  Initialize the LCD in 4-bit mode
*
*****/
void LCD_INIT(void) {
    _delay_ms(50);          
    LCD_CMD(INIT_8BIT_MODE);
    _delay_ms(5);
    LCD_CMD(INIT_4BIT_MODE);
    _delay_ms(5);
    LCD_CMD(FOUR_BIT_MODE);  
    LCD_CMD(DISPLAY_ON);    
    LCD_CMD(ENTRY_MODE);    
    LCD_CMD(CLEAR_DISPLAY);  
    _delay_ms(5);
}


/*****
*
*  LCD_CMD  -  Send command to LCD
*
*****/
void LCD_CMD(unsigned char cmd) {
    (LCD_PORT) = ((LCD_PORT) & (LCD_PORT_MASK)) |
                 (((cmd) & (LCD_HIGH_NIBBLE)) >> (4 - (LCD_DATA_SHIFT)));
    (CTRL_PORT) &= ~(1 << (RS));
    LCD_PULSE();

    (LCD_PORT) = ((LCD_PORT) & (LCD_PORT_MASK)) |
                 (((cmd) & (LCD_LOW_NIBBLE)) << (LCD_DATA_SHIFT));
    (CTRL_PORT) &= ~(1 << (RS));
    LCD_PULSE();
}


/*****
*
*  LCD_DATA  -  Send character data to LCD
*
*****/
void LCD_DATA(unsigned char data) {
    (LCD_PORT) = ((LCD_PORT) & (LCD_PORT_MASK)) |
                 (((data) & (LCD_HIGH_NIBBLE)) >> (4 - (LCD_DATA_SHIFT)));
    (CTRL_PORT) |= (1 << (RS));  
    LCD_PULSE();

    (LCD_PORT) = ((LCD_PORT) & (LCD_PORT_MASK)) |
                 (((data) & (LCD_LOW_NIBBLE)) << (LCD_DATA_SHIFT));
    (CTRL_PORT) |= (1 << (RS));
    LCD_PULSE();
}


/*****
*
*  LCD_PULSE  -  Generate enable pulse
*
*****/
void LCD_PULSE(void) {
    (CTRL_PORT) |= (1 << (EN));
    _delay_us(1);            
    (CTRL_PORT) &= ~(1 << (EN));
    _delay_us(50);          
}


/*****
*
*  LCD_Message  -  Print a string on LCD
*
*****/
void LCD_Message(char *ptr)
{
    while (*ptr)
    {
        LCD_DATA(*ptr++);
    }
}
 

BobTPH

Joined Jun 5, 2013
11,514
I believe there is a difference between the initialization with a string literal and one inside brackets. A string literal is a constant, it cannot be written to, while the bracket notation creates an array variable that can be written to. This could result in a difference in execution if the compiler put the literal in non-writable memory as it s allowed to.
 

Futurist

Joined Apr 8, 2025
748
When you are writing programs that run on bare metal, you need to be able to work down very close to the hardware. But C is a high-level language that intentionally abstracts much of the hardware details away.
What hardware details are you referring to? registers? memory? interrupts? privileges? these exist on almost every digital processor since the earliest days.

So C compilers for embedded systems need to be extended to provide the necessary level of control and access. A lot of this is buried in the header files and macro definitions, but not all of it.
If a language needs true extensions then that's because the language is ill suited to the problem space. But again what extensions might an MCU application need that any other platform doesn't?
 

WBahn

Joined Mar 31, 2012
32,823
What hardware details are you referring to? registers? memory? interrupts? privileges? these exist on almost every digital processor since the earliest days.



If a language needs true extensions then that's because the language is ill suited to the problem space. But again what extensions might an MCU application need that any other platform doesn't?
This is why you can't write an operating system entirely in C.

For example, what code would you write in C in order to store the value 42 into the EX register on an x86-64 processor?
 

Futurist

Joined Apr 8, 2025
748
This is why you can't write an operating system entirely in C.

For example, what code would you write in C in order to store the value 42 into the EX register on an x86-64 processor?
I don't think one can, not based on what I see in the language standard. But that problem isn't confined to MCUs.

You wrote earlier:

WBahn said:
So C compilers for embedded systems need to be extended to provide the necessary level of control and access.
But that's the case for non-embedded systems too, that's what I was pointing out. The only reason we have standards is to a) ensure that different vendors compilers perform equivalently and b) provide a degree of portability across targets.

The C standards are perhaps the most lax of all programming languages, so much is implementation defined that a) and b) above are often not achieved.
 

WBahn

Joined Mar 31, 2012
32,823
I believe there is a difference between the initialization with a string literal and one inside brackets. A string literal is a constant, it cannot be written to, while the bracket notation creates an array variable that can be written to. This could result in a difference in execution if the compiler put the literal in non-writable memory as it s allowed to.
But the string literal form is syntactic sugar.

The declaration

Code:
char s[] = "abc", t[3] = "abc";
defines "plain" char array objects s and t whose elements are initialized with character string literals. This declaration is identical to

Code:
char s[] = { ’a’, ’b’, ’c’, ’\0’ },
     t[] = { ’a’, ’b’, ’c’ };
The contents of the arrays are modifiable.

The above isn't an example I came up with out of thin air -- it is taken directly from the C23 standard and is the exact same example that appears in the ANSI C (C89) standard and all the ones in between.

The distinction you are getting at isn't in what's on the right hand side of the declaration, but rather what is on the left. The example goes on to state

On the other hand, the declaration

Code:
char *p = "abc";
defines p with type ‘‘pointer to char’’ and initializes it to point to an object with type ‘‘array of char’’ with length 4 whose elements are initialized with a character string literal. If an attempt is made to use p to modify the contents of the array, the behavior is undefined.
 

WBahn

Joined Mar 31, 2012
32,823
I don't think one can, not based on what I see in the language standard. But that problem isn't confined to MCUs.

You wrote earlier:



But that's the case for non-embedded systems too, that's what I was pointing out. The only reason we have standards is to a) ensure that different vendors compilers perform equivalently and b) provide a degree of portability across targets.

The C standards are perhaps the most lax of all programming languages, so much is implementation defined that a) and b) above are often not achieved.
When you use C to write a program that runs on a PC, you have an operating system there between your program and the hardware. So you have the luxury of being able to use a language that doesn't allow you dink around at the hardware level (in exchange for a level of abstraction that is much more human-friendly).

When you use C to write a program that runs on bare metal, you don't have that luxury, so you need a language that lets you get down into the nitty-gritty details of the hardware. Historically, that usually meant writing in assembly language for that processor (or, at best, that family of processors). The resulting code is highly non-portable, often not even to other processors within the same family, at least not without some modification.

When working on bare metal, you can't escape this. You can't define a language that is processor-unaware. C language implementations intended for bare-metal use (referred to as "freestanding" implementations, also known as nonhosted implementations) are, by nature, a compromise. You want to have the convenience of the abstraction that a high-level language offers for most of the code, but need to provide constructs that give the necessary access to the hardware, since you don't have the services that are provided by the operating environment in a hosted application.

The C language does provide a rather kludgy way to accomplish this by providing the 'asm' keyword that allows you to inject assembly language directly into the translator output. Of course, the moment you do this, you have thrown away portability because you are now locked to running the program on a processor that understands that assembly language code. This gives you a way of dealing with small amounts of processor-specific stuff, but it is not the way that anyone should want to write an entire program. So the language allows for extensions specifically for this purpose.

The standard navigates this space by defining the notions not only of conforming hosted and freestanding implementations, but also of conforming and strictly conforming programs. One of the requirements is that any conforming implementation must accept any strictly conforming program, but the definition of a strictly conforming program for a freestanding implementation confines the use of library functions to a subset of about a dozen of the standard headers. A strictly conforming program must also not use anything that exceeds the minimum implementation limits, which would require, for example, that variables of type 'int' must be 16 bits.
 

Thread Starter

Embededd

Joined Jun 4, 2025
153
So, since my LCD code that I posted in #52 didn’t get any further comments, I’m assuming my LCD driver code is now complete. Next, I’d like to create similar test drivers for UART and I²C, which I plan to use in my project. I might start with UART first.
 

Futurist

Joined Apr 8, 2025
748
When you use C to write a program that runs on a PC, you have an operating system there between your program and the hardware. So you have the luxury of being able to use a language that doesn't allow you dink around at the hardware level (in exchange for a level of abstraction that is much more human-friendly).

When you use C to write a program that runs on bare metal, you don't have that luxury, so you need a language that lets you get down into the nitty-gritty details of the hardware. Historically, that usually meant writing in assembly language for that processor (or, at best, that family of processors). The resulting code is highly non-portable, often not even to other processors within the same family, at least not without some modification.

When working on bare metal, you can't escape this. You can't define a language that is processor-unaware. C language implementations intended for bare-metal use (referred to as "freestanding" implementations, also known as nonhosted implementations) are, by nature, a compromise. You want to have the convenience of the abstraction that a high-level language offers for most of the code, but need to provide constructs that give the necessary access to the hardware, since you don't have the services that are provided by the operating environment in a hosted application.

The C language does provide a rather kludgy way to accomplish this by providing the 'asm' keyword that allows you to inject assembly language directly into the translator output. Of course, the moment you do this, you have thrown away portability because you are now locked to running the program on a processor that understands that assembly language code. This gives you a way of dealing with small amounts of processor-specific stuff, but it is not the way that anyone should want to write an entire program. So the language allows for extensions specifically for this purpose.

The standard navigates this space by defining the notions not only of conforming hosted and freestanding implementations, but also of conforming and strictly conforming programs. One of the requirements is that any conforming implementation must accept any strictly conforming program, but the definition of a strictly conforming program for a freestanding implementation confines the use of library functions to a subset of about a dozen of the standard headers. A strictly conforming program must also not use anything that exceeds the minimum implementation limits, which would require, for example, that variables of type 'int' must be 16 bits.
Well I largely agree with you, but bear in mind Windows, its kernel mode components and device drivers are all written in C, that's what makes it portable, so clearly that code does not require an OS because it is an OS.

I've never been a fan of embedding assembler code, I have often wondered if some of the hardware stuff could be abstracted for use by a language.

For example the stack pointer is common to almost all modern processors so could be exposed as a native language abstraction and that would facilitate the management of stack frames (which underpin schedulers for example) wholly from the language with no need to resort to machine specific assembler.

LLVM's IR language is interesting too, that is an abstract assembly language that hides the processor in all but the most esoteric scenarios. Clang is based on LLVM and can therefore expose intrinsics that are supported by the LLVM IR like saturated arithmetic operations and fixed point arithmetic.

LLVM doesn't itself expose the stack pointer but it can be exposed as an LLVM IR function. LLVM has massive optimization abilities, so the target specific generated code is often better than a human assembler programmer could write.
 
Last edited:

Thread Starter

Embededd

Joined Jun 4, 2025
153
I’m posting this code to test whether UART transmission and reception are working correctly on my ATmega8A using a USB–TTL converter. The code initializes UART at 9600 baud, and then continuously echoes back any character received. This helps confirm that both TX (sending) and RX (receiving) are functioning as expected.

C:
/*
* UART_Echo_Test.c
* Purpose:
*   This code tests UART functionality on ATmega8A by performing
*   an echo test — any character received on UART is transmitted back.
*
* Hardware:
*   - MCU: ATmega8A
*   - Clock: 12 MHz external crystal
*   - UART Pins: TXD (PD1), RXD (PD0)
*
* Toolchain:
*   - Language: C
*   - Compiler: AVR-GCC (via Microchip Studio)
*
* Author : Developer
* Created: 29-09-2025
*/

#define F_CPU       12000000UL   // MCU clock frequency (12 MHz)
#define BAUD        9600         // Desired baud rate
#define UBRR_VAL    ((F_CPU / (16UL * BAUD)) - 1) // Datasheet formula

#include <avr/io.h>
#include <util/delay.h>

/* UART register bit definitions for readability */
#define RX_COMPLETE  (1 << RXC)
#define TX_READY     (1 << UDRE)
#define RX_ENABLE    (1 << RXEN)
#define TX_ENABLE    (1 << TXEN)
#define UCSRC_SELECT (1 << URSEL)
#define UCSZ_8BIT    ((1 << UCSZ0) | (1 << UCSZ1))

/**
* UART_Init
* Initializes UART with 8 data bits, 1 stop bit, no parity.
*/
void UART_Init(void) {
    UBRRH = (unsigned char)(UBRR_VAL >> 8);
    UBRRL = (unsigned char)(UBRR_VAL & 0xFF);

    UCSRB = RX_ENABLE | TX_ENABLE;                    // Enable RX and TX
    UCSRC = UCSRC_SELECT | UCSZ_8BIT;                 // Select UCSRC, 8-bit frame
}

/**
* UART_RxChar
* Waits until a character is received and returns it.
*/
unsigned char UART_RxChar(void) {
    while (!(UCSRA & RX_COMPLETE)) {
        // Block until RXC is set
    }
    return UDR;
}

/**
* UART_TxChar
* Waits until buffer is ready and transmits one character.
*/
void UART_TxChar(char ch) {
    while (!(UCSRA & TX_READY)) {
        // Block until UDRE is set
    }
    UDR = ch;
}

/**
* UART_SendString
* Sends a null-terminated string.
*/
void UART_SendString(const char *str) {
    while (*str) {
        UART_TxChar(*str++);
    }
}

int main(void) {
    UART_Init();
    _delay_ms(100);  // Allow UART to stabilize

  //  UART_SendString("UART Echo Test Ready\r\n");

    while (1) {
        unsigned char c = UART_RxChar();  // Wait for character
        UART_TxChar(c);                   // Echo it back
    }
}
When I type a character in the terminal (for example, u), I see the same u echoed back. If I type j, I see j, and if I type i, I see i on the terminal.

1759321876858.png
 
Top