One sketch, several hardware editions? Possible?

Hi folks,

Is there any way with Arduino to write a single sketch that can handle several different hardware platforms? I know the basics of coding, but I'm not a professional programmer, and while I've searched for answers to this I'm not sure if I'm even using the proper terminology.

The code I'm writing has to use #define statements to setup the hardware limitations, but I will be writing this for 2-3 different platforms, each of which has subtly different capabilities.

Is there any way to have a common core for the Sketch, and just vary the 2-3 constants which need to change for the various editions?

Thanks

Myx

Sure. For example, the "Blink" example runs on all hardware.
In fact, many of the examples are hardware independent at the "user sketch" level - that's one of the advantages of using the Arduino core and libraries.

For more important differences, there are a set of symbols that are pre-defined by the compile environment whenever you build a sketch. So you can do:

#if defined(ARDUINO_ARCH_AVR)
  #if defined(__ATMEGA328P__)
    // stuff for 328 Ardunios
  #elif defined(__ATMEGA1284P__)
    // stuff defined for Wildfire or Bobuino
  #endif
#elif defined(ARM_MATH_CM4)
  // fancy DSP instructions!
#else ...

It is a good idea to isolate this stuff as much as possible. I have some C code from the mids 80s, when the language and libraries were a lot less standardized, and it's so full of SYSV/BSD/SOLARIS/IBMPC conditionals that it is really difficult to read.

westfw gave a good answer while I was writing this, but since there is a little bit of different information, I'll post it anyway:

There are various macros that can be used to identify which microcontroller, board, or architecture is being compiled for. Arduino has standardized macros for board and architecture.

The board macro looks like ARDUINO_{build.board}, where {build.board} is defined in boards.txt. For example, the macro for the Uno is ARDUINO_AVR_UNO:

The architecture macro looks like ARDUINO_ARCH_{architecture name}, where the architecture name is defined by the architecture folder name of the hardware package. For example, when compiling for an AVR, the ARDUINO_ARCH_AVR macro will be defined.

There will usually also be more low level macros defined by the toolchain. Generally, I prefer to use these in my code over the Arduino-specific macros. For example, I would prefer to use AVR_ATmega328P instead of ARDUINO_AVR_UNO, unless I specifically wanted to identify the Uno, which I have never found the need for. There are multiple boards that use the ATmega328P so you would have some messy preprocessor code trying to cover all of them using the board ID macro and you would likely still miss some obscure 3rd party boards. Often there will also be a low level macro for the architecture. In the case of the AVR, there is both __AVR and AVR. You can find a list of the AVR-specific macros here:
https://www.microchip.com/webdoc/avrlibcreferencemanual/using_tools_1using_avr_gcc_mach_opt.html
There will likely be similar for other architectures, but I have not found a similar helpful list for the non-AVR architectures I'm worked with, so I usually end up digging around through source code for those.

So you just need to write your code something like this

// non-architecture-specific code here
#if defined(__AVR__)
// AVR-specific code here
#elif defined(ESP8266)
// ESP8266-specific code here
#else
#error The architecture of the currently selected board is not supported
#endif
// non-architecture-specific code here

References:

as example, my project works on Uno, Mega, esp8266 and M0. it can use Ethernet or different WiFi libraries. it can use SD or SPIFFS. it can use A1 or I2C ADC...