Code can be written to be portable between microcontrollers. It is a good idea to do this unless your application is either very simple or you're in a big hurry :)
The idea is to separate the higher-level procedures (your main program flow) from the low-level hardware manipulation (turning on pins, configuring registers, etc).
For a simple example, take a circuit where you want to control an LED. You might set up your code by having a whole separate module for the LEDs: (I don't work with AVRs; please forgive any errors)
// For simplicity, this assumes all LEDs are on Port D.
void ledInit(uint8_t ledPin)
DDRD |= (1 << ledPin); // Configure the pin as an output
PORTD &= ~(1 << ledPin); // Set the pin low
void ledOn(uint8_t ledPin)
PORTD |= (1 << ledPin);
void ledOff(uint8_t ledPin)
PORTD &= ~(1 << ledPin);
Then, in your mainline code, you would only access the LED via these commands. If you needed more access, like a toggle command, you would add it to the led.c file. The main code isn't even aware of what happens inside the LED module.
This makes it much easier to move your mainline code between microcontrollers. Basically, you would only need to re-write the led.c file. You wouldn't have to change your mainline code at all.
This logic can also be used for timers, communications ports (SPI, I2C, USART...), etc.
In a large project, I find it best to actually have three layers of code:
- The mainline code, which orchestrates everything
- Mid-level, which provides functions and names (#defines) to the mainline code. This translates between the intention of the mainline code and the low-level functions required to make things happen, and
- Low-level (driver), which actually flips the correct bits.
In this case, the low-level functions are only seen by the mid-level block. The mid-level block is exposed to the mainline code.
This may seem overly complex, but it doesn't really take much more work. The benefit happens if you have to change bigger implementation details.
For example, say you've designed your program to use an SPI peripheral, but your new design actual needs to use I2C. You can now change the mid-level code block to properly call the low-level I2C commands instead of the low-level SPI commands. You wouldn't have to change either the high-level or the low-level code.
That being said, I wouldn't recommend this while you're still learning the basics :)