Your code can't be 100% portable across all the architectures. BUt you can maxmize your chances & minimize your work.
1) Use only ANSI C. Enable strict ANSI checking if compiler supports it.
2) Carefully seperate hardware dependent low level functions & high level functions. Low level functions must be re-written for every target, but high level functions can be reused if they follow ANSI specs.
e.g. Accessing I2C eeprom using software I2C, you can write low level functions like i2c_start(), i2c_stop() etc for each target seperately. But you can write the high level functions like i2c_ee_read_byte(), i2c_ee_write_byte() etc. once only.
3) Don't use any compiler specific extensions. That means you may have to do some things harder way.
4) Avoid using target specific features. Like there is 'bit' type for 8051, but it may not be present for AVR or ARM. Also 8051 can access external RAM/ROM while PIC can't do that.
5) Interrupts are one of the things which have to be re-written for each target. Interrupts differ in all big & small respects in every target. Even if you write isr in C, still they won't be portable that easily.
6) Also note that when you are porting across 8,16 & 32-bit targets, your code won't be optimized. For example, for PIC/AVR, 8-bit vars are faster, but for ARM 32-bit vars are more faster. Also allocation of local data may be different. ARM, AVR use data-stack while PIC,8051 use overlays.
7) For some architectures c++ simply doesn't exist.
In my opinion it's more work that it is worth to write application which needs to be portable across so varied targets. Instead I prefer write seprate programs for each. That way it is more optimized, using full power of architecture in hand.
Hope that helpes