I was coding for Atmel ATmega 32 chips. Now I have shifted to the ARM Cortex-M family.
Is there any difference in coding between AVR and ARM, or does it depend on the compiler? How I would learn programming for ARM MCUs?
Electrical Engineering Stack Exchange is a question and answer site for electronics and electrical engineering professionals, students, and enthusiasts. It only takes a minute to sign up.
Sign up to join this communityI was coding for Atmel ATmega 32 chips. Now I have shifted to the ARM Cortex-M family.
Is there any difference in coding between AVR and ARM, or does it depend on the compiler? How I would learn programming for ARM MCUs?
There is some similarity (obviously ANSI C is pretty much ANSI C) but the differences in hardware will result in a great increase in complexity in most cases when you use an ARM or other 32-bit processor. It's not that you can't do simple programs that directly access hardware on an ARM but usually there are hardware abstraction layers and middleware and maybe an RTOS to contend with in order to get a program running that was the reason you wanted to use an ARM in the first place.
A few things are probably a bit simpler- but there are also memory alignment and memory barrier issues to deal with. And threading issues if you use an RTOS (which is pretty much what you deal with anyway when you use interrupts but with different terminology). The bus structure is far more complex. You'll likely want to consider using DMA or other complex peripherals.
You'll have to adapt to the naming conventions used by the middleware provider if you want to use their USB stack, Ethernet stack etc. It is said that
"There are only two hard things in Computer Science: cache invalidation, naming things, and off-by-one errors"
Here is an ASF (Atmel Software Framework) example of writing to a port (blinky):
in main.c:
ioport_set_pin_level(LED1_GPIO, IOPORT_PIN_LEVEL_HIGH);
in ioport.h
static inline void ioport_set_pin_level(ioport_pin_t pin, bool level)
{
arch_ioport_set_pin_level(pin, level);
}
in ioport_pio.h
__always_inline static void arch_ioport_set_pin_level(ioport_pin_t pin,
bool level)
{
Pio *base = arch_ioport_pin_to_base(pin);
if (level) {
base->PIO_SODR = arch_ioport_pin_to_mask(pin);
} else {
base->PIO_CODR = arch_ioport_pin_to_mask(pin);
}
}
Also in ioport.h:
/** \brief IOPORT levels */
enum ioport_value {
IOPORT_PIN_LEVEL_LOW, /*!< IOPORT pin value low */
IOPORT_PIN_LEVEL_HIGH, /*!< IOPORT pin value high */
};
struct Foo MyFoo = { .x = 7, .y = 8 };
is perfectly reasonable even on small embedded systems, but it's not ANSI C89.
\$\endgroup\$
In short: yes and no.
The C language will be mostly the same regardless of architecture, although there will be some variations depending on the specific compiler that you use.
However, using a different hardware architecture and its corresponding toolchain will inherently give you vastly different ways to use the hardware's registers, interrupt vectors, etcetera. Nothing a few tutorials won't help you with, though. Good luck!
An important point to watch out for is that different compilers will attempt to "optimize" code with differing levels of aggressiveness, in ways that would be allowable under the C Standard but would make the compilers unsuitable for processing certain kinds of code unless it includes compiler-specific directives which would not have been necessary when using other compilers that "optimize" less aggressively.
For example, given something like:
// Simple polling function to acquire some data and store it, using the
// return value to indicate success. Note that there is no reason why
// this function should need to make "dest" volatile.
extern uint32_t volatile data_ready;
extern uint16_t volatile data_source;
int get_data(uint16_t *dest){
if (!data_ready) return 0;
*dest=data_source;
return 1;
}
// Set up and trigger outside interrupt, DMA, or other such process
// to output data and wait for that process to complete.
extern uint32_t volatile out_length;
extern uint16_t volatile * volatile out_ptr;
void write_data(uint16_t *buff, uint32_t len)
{
out_ptr = buff;
out_length = len;
do {} while(out_length);
}
// Sample code using the above. Note that if "buff" were volatile,
// its address could not be passed to "get_data" unless get_data took
// a volatile pointer, and making "buff" volatile would compel test()
// to do a couple of unnecessary extra stores to it.
uint16_t buff;
void test(void)
{
while (!get_data(&buff));
buff+=1;
write_data(&buff, 1);
while (!get_data(&buff));
buff+=1;
write_data(&buff, 1);
}
ARM gcc 6.3.0 will ignore the value stored into "buff" by the first call to get_data
if invoked at -O2 or higher, since it sees that it will get overwritten by
the second call. It will do this even if the -fno-strict-aliasing
option is used. To make code compatible with gcc, it would be necessary to either make buff
volatile (which would in turn require that any functions that accept pointers to it qualify those pointers as volatile
), use optimization level 0 or 1, or insert a __asm
directive to force the compiler to recognize that stores to volatile locations can have side-effects on other objects.
The authors of gcc take the attitude that the code as written is broken since the Standard does not require that all compilers--even those not intended for use in embedded or systems programming, or targeting systems where no physical means of causing side-effects would exist--treat volatile accesses as having potential side-effects on other objects. I personally think it's the gcc authors' attitude that's broken, but I'll leave such judgements up to the reader.
PS--If the functions were written as macros:
#define get_data(dest) (data_ready ? 0 : ((*dest=data_source),1))
#define write_data(buff,len) do {\
out_ptr=buff; out_length=len; do {} while(out_length); \
} while(0)
gcc would break the code even at optimization level 1.
volatile
are observable and may not be eliminated or reordered relative to each other.
\$\endgroup\$
buff
in arbitrary order (allowing for consolidation), followed by accesses to out_ptr
and out_length
, followed by one or more accesses to buff
in arbitrary order, followed by more accesses to out_ptr
and out_length
. Generating correct code requires that the accesses to buff
be ordered with respect to out_length
. Generating optimal code requires that they be unordered with respect to each other. I would suggest that an implementer who is exercising good judgment should make it possible...
\$\endgroup\$
buff
in arbitrary order (allowing for consolidation), followed by accesses to out_ptr
" - that explicitly relies on sequencing of a non-volatile access and a volatile access. I really can't find fault with GCC here.
\$\endgroup\$
Proper C code should be portable if you don’t depend on machine- or compiler specifics. For example if you want an integer of a certain size you should use the stdint.h header and don’t trust in e.g. int
being 16 or 32 bit. Also don’t trust in the alignment staying the same.
Of course the peripherals, memory addresses etc. between different CPUs can differ greatly.