I would suggest if you currently have no plan to organize code, just pick something simple and work your way to something better.
Having a pair of h/c files for each peripheral and purpose seems to work well. Peripherals are already naturally organized in the datasheet and headers, so it also works well to use that as a separation point.
The h file is simply to inform anyone else what they can use from the corresponding c file, and provide any info to make use of them. If no one needs anything in the c file, no need for an h file. The compiler compiles one c file at a time, so treat every c file from the compiler perspective as if its the only file you know about. That's where headers come into play- you tell the compiler there is 'something' 'somewhere else' (via an include), and the compiler will believe you (the linker eventually will need more than just the paperwork, though).
From the original code Simon posted, a simple example would be to split out the tca0 things-
//=====================================
//tca0.h
//=====================================
#pragma once
#include <stdint.h> //we need it here (uint16_t), so use it here
uint16_t tca0_time();
void tca0_setup();
//=====================================
//tca0.c
//=====================================
#include "tca0.h" //only need in this case to make sure our version matches what we are telling others
#include <avr/io.h> //<xc.h> //no one else needs this, so it goes here, not in tca0.h header
#include <stdint.h> //we need it here, so use it here (that's why there are include guards)
#include <avr/interrupt.h> //no one else needs this, so it goes here, not in tca0.h header
#include <util/atomic.h> //no one else needs this, so it goes here, not in tca0.h header
static volatile uint16_t time; //private, we tell no one
ISR(TCA0_OVF_vect){
time++;
TCA0.SINGLE.INTFLAGS = 0x01;
}
uint16_t tca0_time(){
uint16_t ret;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE){ ret = time; }
return ret;
}
void tca0_setup(){
//setup tca0 (from setup.c code)
}
Now any other c file that needs the tca0 time, or needs to run tca0 setup, will just include tca0.h and they will have all the info they need. No one else needs to worry about potential atomic problems. If any changes needed to tca0, you already know which files to change- the c file with that name and the h file also if needed. With the 'public' tca0 vars/functions prefaced with tca0_, you will be informed where they are sourced every time you use them.
There are plenty of holes in the above code/explanations, but its a start.
Yep. I actually do parameters checking
One portion of 'parameter checking' can be done with enums-
//=====================================
//adc.h
//=====================================
#pragma once //actually, need old style inlcude guards with xc8-pic
#include <stdint.h> //needed here (uint16_t), so use here
typedef enum {
adc_AVSS = 0x3B,
adc_TEMP,
adc_DAC1,
adc_FVR1,
adc_FVR2
} adc_ch_t;
uint16_t adc_read(adc_ch_t);
//=====================================
//adc.c
//=====================================
#include "adc.h"
#include <stdint.h>
#include <xc.h>
uint16_t adc_read(adc_ch_t ch){
ADCON0bits.CHS = (uint8_t)ch;
//other stuff
return ADRES;
}
There is no way you can pass a channel that is incorrect, so no need to check if the channel is valid as the compiler already checked. Yes, you can cast to get around that, but by that point you know what you are doing and why and want no checking anyway.
My 2 cents.