embeddedarduino-unocpu-registersatmega

[Arduino][Register manipulation] How to make your own Serial.print() with registers?


(Using VSCode with PlatformIO IDE and Arduino UNO Rev. 3, witch have a ATmega328P microprocessor)

Hi, I started learning about embedded systems' firmware development using register manipulation. My first goal is to make a "Hello World" program:

Serial.begin(9600);
Serial.print("Hello world");

But directly setting register values to reproduce the same result as the code above.

Following the USART section of ATmega328P, I develop:

#include <Arduino.h>

// USART initialization parameters
#define FOSC 8000000 // Clock speed

unsigned int UBRRnAsynchronousNormalMode(int desiredBaud){
  return (FOSC/(16*desiredBaud))-1;
}

void serialBegin(unsigned int UBRRn){
  // Setting baud rate
  UBRR0H = (unsigned char) (UBRRn>>8);
  UBRR0L = (unsigned char) UBRRn;

  // Defining frame format
  UCSR0C = (1<<USBS0) | (3<<UCSZ00);

  // Enabling transmiter
  UCSR0B = (1<<TXEN0) | (1<<RXEN0);
}

void serialPrint(unsigned char data){
  while(!(UCSR0A & (1<<UDRE0)));
  UDR0 = data;
}

void setup() {
  serialBegin(UBRRnAsynchronousNormalMode(9600));
  serialPrint('1');
}

The Serial Monitor says "Terminal on COM5 | 9600 8-N-1", but my print results in:

enter image description here

Sounds like a common baud rate disparity problem, but as I understand, I set my rate to 9600 (same as indicated by the Serial Monitor). Have I made an error in the code, or missed some step to reproduce a Serial.print()?


Solution

  • Understanding the numerical limits of types is essential when doing any form of programming, but during old 8 bit MCU programming in particular.

    This function is wrong:

    unsigned int UBRRnAsynchronousNormalMode(int desiredBaud){
      return (FOSC/(16*desiredBaud))-1;
    }
    

    FOSC is defined as 8000000 and that number, an "integer constant", has a type like everything else. C first tries to see if it can make it type int. But since AVR has 16 bit int, the largest number is 65535. So instead it tries to fit it inside a 32 bit long, which is ok. So the integer constant 8000000 has type long.

    Meaning that on AVR, your equation is equivalent to ((long)FOSC/(16*desiredBaud))-1. But here both 16 and desiredBaud are 16 bit int. And 16 * 9600 = 153600, so you get an integer overflow.

    In comments you describe that FOSC/16/desiredBaud fixed the problem. This is because the / operators associate operands from left to right, so this is guaranteed to be equivalent to (FOSC/16)/desiredBaud. And in the sub expression FOSC/16, one operand is long. An implicit type promotion happens before calculation (see Implicit type promotion rules). So it is calculated on long and the result is long And since the result is long, "result"/desiredBaud gets implicitly promoted to long too. So it worked by luck... don't rely on luck.

    So what to actually do to fix this? Follow best practices:

    The code could be rewritten as:

    
    #include <stdint.h>
    
    #define FOSC 8000000ul
    
    uint16_t UBRRnAsynchronousNormalMode (uint32_t desiredBaud){
      return (FOSC / (16ul * desiredBaud)) - 1ul;
    }