12
\$\begingroup\$

We are currently using 32-bit PIC32 Microcontroller. It is working fine for our needs, but we are also exploring other microcontollers that can suite us better + we have other projects for which we are selecting MCU. For that purpose we have selected ARM based SAM DA microcontoller which is same 32-bit but is ARM based (more popular than PIC32 - industry wise).

Now for PIC32 we use MPLAB but for ARM cortex-M0, we will be using Atmel Studio. We will be using C-language in both the platforms. The thing that is of my concern is, we will be using two 32 bit microcontoller (from same company) but having different architectures. This will require us learn two different devices and will increase our "learning curve" + delivery time. But on the other hand, I also think since we will be using C-Language in both cases, the learning curve for ARM shouldn't be that heard and it is worth exploring that processor as well.

My main question is, how big of difference the architecture makes when we are programming in C-Language since it provides an abstraction of the internals of the micrcontroller. And what are the main differences in MPLAP and Atmel Studio, considering C-language programing.

\$\endgroup\$
3
  • 2
    \$\begingroup\$ If things are working with the PIC32, then what's the point of switching? Even if the code completely ports (it won't), there is still the new tool chain and IDE to get used to. What's the point? Switching for religious reasons or to be "ARM based" (or anything else based) is silly. You need to have a good reason, but you haven't shown us any. \$\endgroup\$ Dec 24, 2017 at 14:51
  • \$\begingroup\$ I did not ask about switching. I talked about choosing a different architecture for other projects as we are working on multiple projects + there is room for improvement in our existing design. Main point was about learning curve and challenges in working with two different architectures at the same time. \$\endgroup\$
    – TheTechGuy
    Dec 24, 2017 at 15:09
  • \$\begingroup\$ One thing I did find that Atmel Studio provides superior timing than MPLAB youtube video \$\endgroup\$
    – TheTechGuy
    Dec 24, 2017 at 15:13

5 Answers 5

21
\$\begingroup\$

This is quite an opinionated topic. I can speak for myself (AVR, ARM, MSP430).

Difference 1 (most significant) is in the peripherals. Each of the MCU has similar UART, SPI, timers etc. - just register names and bits are different. Most of the time it was the main issue I had to deal with when moving code between chips. Solution: write your drivers with a common API, so your application can be portable.

Difference 2 is the memory architecture. If you want to place constants in flash on an AVR you have to use special attributes and special functions to read them. In the ARM world you just dereference a pointer because there is a single address space (I don't know how small PICs handle it, but would assume that they are closer to AVR).

Difference 3 is interrupt declaration and handling. avr-gcc has the ISR() macro. ARM has just a function name (like someUART_Handler() - if you use CMSIS headers and startup code). ARM interrupt vectors can be placed anywhere (including RAM) and modified at runtime (very handy if you have for example two different UART protocols that can be switched). AVR has only the option to use vectors either in "main flash" or the "bootloader section" (so if you want to handle interrupts differently you have to use an if statement).

Difference 4 - sleep modes and power control. If you have the need for lowest power consumption, then you have to levarege all features of the MCU. This can differ a lot between MCU - some have more coarse power saving modes, some can enable/disable individual peripherals. Some MCUs have adjustable regulators so you can run them with lower voltage at slower speed etc. I don't see an easy way to achieve the same efficiency on an MCU (let's say) with 3 global power modes and another with 7 power modes and individual peripheral clock control.

The single most important thing when caring about portability is to clearly split your code to hardware-dependent (drivers) and hardware-independent (application) parts. You can develop and test the latter on a regular PC with a mock driver (eg. console instead of an UART). This saved me many times as 90% of the application code was complete before the prototype hardware came out of the reflow oven :)

In my opinion the good thing about ARM is the "monoculture" - availability of many compilers (gcc, Keil, IAR... to name a few), many free and oficially supported IDEs (at least for NXP, STM32, Silicon Labs, Nordic) , many debug tools (SEGGER - especially the Ozone, ULINK, OpenOCD...) and many chip vendors (I won't even start naming them). The PIC32 is mostly limited to Microchip (but it only matters if you don't like their tools.

When it comes to C code. It is 99% the same, an if statement is the same, a loop works in the same way. However you should care about the native word size. For example a for loop on an AVR is fastest if you use uint8_t for the counter, while on ARM uint32_t is the fastest type (or int32_t). ARM would have to check for 8-bit overflow every time if you used a smaller type.

Selecting an MCU and/or vendor in general is mostly about politics and logistics (unless you have very clear engineering constraints, for example: high temperature - use MSP430 or Vorago). Even if the application can run on anything and only 5% of the code (drivers) has to be developed and supported over product lifetime - it is still an extra cost for the company. All places I have worked in had a favorite vendor and MCU line (like "pick any Kinetis you want unless there is a very good reason to chose something different"). It also helps if you have other people to ask for help, so as a manager I would avoid having 5-person development department where everyone used a totally different chip.

\$\endgroup\$
4
  • 3
    \$\begingroup\$ “AVR is fastest if you use uint8_t for the counter, while on ARM uint32_t is the fastest type (or int32_t). ARM would have to check for 8-bit overflow every time if you used a smaller type.” you can use uint_fast8_t if you only need at least 8 bit. \$\endgroup\$
    – Michael
    Dec 24, 2017 at 20:17
  • \$\begingroup\$ @Michael - sure you can use the _fast types, but you can't count on the overflow behavior. In my gcc's stdint.h I have "typedef unsigned int uint_fast8_t", which basically is a uint32_t :) \$\endgroup\$
    – filo
    Dec 25, 2017 at 7:40
  • \$\begingroup\$ Trying to write an API which is efficient, universal, and complete is difficult given that different platforms have different abilities. The CPU probably matters less than the peripherals and the design decisions made with them. For example, some devices have allow various peripherals to be reconfigured at any time in at most a few microseconds, while others may require multiple steps spread out over hundreds of microseconds or even milliseconds. An API function which is intended for the former pattern may be usable within an interrupt service routine that runs at 10,000Hz, but... \$\endgroup\$
    – supercat
    Dec 25, 2017 at 17:43
  • \$\begingroup\$ ...could not support such usage on platforms that would require spreading out operations over hundreds of microseconds. I don't know why hardware designers don't seem to try very hard to support "quick operation at any time" API semantics, but many use a model that synchronizes individual operations rather than state so that if e.g. a request has been given to turn on a device and code realizes it doesn't need to be on, code must wait for the device to turn on before it can issue the request to turn off. Handling that smoothly in an API adds major complications. \$\endgroup\$
    – supercat
    Dec 25, 2017 at 17:48
12
\$\begingroup\$

I have used several MCUs from four different manufacturers. The main work every time again is to get familiar with the peripherals.

For example a UART itself is not too complex and I find my drivers port easily. But the last time it took me almost a day to get the clocks, I/O pins interrupt , enable etc. sorted out.

The GPIO can be very complex. Bit-set, bit-clear, bit-toggle, Special functions enable/disable, tri-state. Next you get interrupts: any-edge, rise, fall, level-low, level-high, self clearing or not.

Then there is I2C, SPI, PWM, Timers and two dozen more types of peripherals each with their own clock enables and every time the registers are different with new bits. For all of those it takes many many hours reading the datasheet how to set which bit under which circumstances.

The last manufacturer had lots of code example which I found unusable. Everything was abstracted. But when I traced it down, the code went through six! levels of function calls to set a GPIO bit. Nice if you have a 3GHz processor but not on an MCU of 48MHz. My code in the end was a single line:

GPIO->set_output = bit.

I have tried to use more generic drivers but I have given up. On an MCU you are always struggling with space and clock cycles. I found that the abstraction layer is the first to go out the window if you generate a specific waveform in an interrupt routine called at 10KHz.

So now I have everything working and I plan NOT to switch again unless for a very, very good reason.

All of the above must be amortized over how many products you sell and what you save. Selling a million: saving 0.10 to switch to a different type means you can spend 100.000 on software man-hours. Selling 1000 you have only 100 to spend.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Personally this is why I stick with assembler. Lovely binary, no abstraction. \$\endgroup\$
    – Ian Bland
    Dec 24, 2017 at 18:13
  • \$\begingroup\$ C's preprocessor can do pretty well with stuff, especially when combined with __builtin_constant intrinsics. If one defines constants for each I/O bit of the form (port number*32 + bit number), it's possible to write a macro for OUTPUT_HI(n) which will yield code equivalent to GPIOD->bssr |= 0x400; if n is a constant like 0x6A, but call a simple subroutine if n is not constant. That having been said, most vendor APIs I've seen range between mediocre and horrible. \$\endgroup\$
    – supercat
    Dec 25, 2017 at 17:52
8
\$\begingroup\$

This is more an opinion/comment than an answer.

You don't want and shouldn't be programming in C. C++, when used in the right way, is far superior. (OK, I have to admit, when used in the wrong way it is far worse than C.) That limits you to chips that have a (modern) C++ compiler, which is roughly evertything that is supported by GCC, including AVR (with some limitations, filo mentions the problems of a non-uniform address space), but excluding nearly all PICs (PIC32 could be supported, but I haven't seen any decent port yet).

When you are programming algorithms in C/C++ the difference between the choices you mention is small (except that an 8 or 16 bit chip will be at a severe disadvantage when you do a lot of 16, 32 or higher bit arithmetic). When you need the last ounce of performance, you will probably need to use assembler (either your own or code provided by the vendor or a third party). In that case you might want to re-consider the chip you selected.

When you are coding to the hardware you can either use some abstraction layer (often provided by the manufacturer) or write your own (based on the datasheet and/or example code). IME existing C abstractions (mbed, cmsis, ...) are often functionaly (almost) correct, but fail horribly in performance (check oldfarts rant about 6 layers of indirection for a pin set operation), usability and portability. They want to expose all functionality of the particular chip to you, which in nearly all cases you won't need and rather not care about, and it locks your code to that particular vendor (and probably that particular chip).

This is were C++ can do much better: when done properly, a pin set can go through 6 or more abstraction layers (because that makes a better (portable!) interface and shorter code possible), yet provide an interface that is target-independent for the simple cases, and still result in the same machine code as you would write in assembler.

A snippet of the coding style I use, which can either make you enthousiastic or turn away in horror:

// GPIO part of a HAL for atsam3xa
enum class _port { a = 0x400E0E00U, . . . };

template< _port P, uint32_t pin >
struct _pin_in_out_base : _pin_in_out_root {

   static void direction_set_direct( pin_direction d ){
      ( ( d == pin_direction::input )
         ? ((Pio*)P)->PIO_ODR : ((Pio*)P)->PIO_OER )  = ( 0x1U << pin );
   }

   static void set_direct( bool v ){
      ( v ? ((Pio*)P)->PIO_SODR : ((Pio*)P)->PIO_CODR )  = ( 0x1U << pin );    
   }
};

// a general GPIO needs some boilerplate functionality
template< _port P, uint32_t pin >
using _pin_in_out = _box_creator< _pin_in_out_base< P, pin > >;

// an Arduino Due has an on-board led, and (suppose) it is active low
using _led = _pin_in_out< _port::b, 27 >;
using led  = invert< pin_out< _led > >;

In reality there are some more layers of abstraction. Yet the final use of the led, let's say to turn it on, doesn't show the complexity or the details of the target (for an arduin uno or an ST32 blue pill the code would be identical).

target::led::init();
target::led::set( 1 );

The compiler isn't intimidated by all those layers, and because there are no virtual functions involved the optimizer sees through everything (some details, omitted, like enabling the peripheral clock):

 mov.w  r2, #134217728  ; 0x8000000
 ldr    r3, [pc, #24]   
 str    r2, [r3, #16]
 str    r2, [r3, #48]   

Which is how I would have written it in assembler - IF I had realized that the PIO registers can be used with offsets from a common base. In this case I probably would, but the compiler is far better at optimizing such things than I am.

So as far as I have an answer, it is: write an abstraction layer for your hardware, but do it in modern C++ (concepts, templates) so it doesn't harm your performance. With that in place, you can switch easily to another chip. You can even start developing on some random chip you have laying around, are familiair with, have good debugging tools for, etc. and postpone the final choice untill later (when you have more info about the required memory, CPU speed etc.).

IMO one of the falacies of embbeded development is choosing the chip first (it is a question often asked on this forum: which chip should I choose for .... The best answer is generally: it doesn't matter.)

(edit - response to "So performance wise, C or C++ would be at same level?")

For the same constructs, C and C++ are the same. C++ has much more constructs for abstraction (just a few: classes, templates, constexpr) which can, like any tool, be used for the good or for the bad. To make the discussions more interesting: not everyone agrees what is good or bad...

\$\endgroup\$
5
  • \$\begingroup\$ So performance wise, C or C++ would be at same level? I would think C++ will have more overload. Definitely you pointed me in the right direction, C++ is the way to go not C. \$\endgroup\$
    – TheTechGuy
    Dec 24, 2017 at 16:15
  • \$\begingroup\$ C++ templates forces compile-time polymorphism which can be zero (or even negative) cost in terms of performance, as the code is compiled for each specific use-case. This does tend to lend itself best to targeting speed (O3 for GCC), though. Run-time polymorphism, like virtual functions, can suffer much greater penalty, although arguable easier to maintain and in some cases good enough. \$\endgroup\$
    – Hans
    Dec 24, 2017 at 20:47
  • 1
    \$\begingroup\$ You claim that C++ is better, but then you go and use C-style casts. For shame. \$\endgroup\$
    – JAB
    Dec 25, 2017 at 2:15
  • \$\begingroup\$ @JAB I never felt much for the new-style casts, but I'll give them a try. But my current priority is on other parts of this library. The real problem is of course that I couldn't pass the pointers as template parameters. \$\endgroup\$ Dec 25, 2017 at 9:04
  • \$\begingroup\$ @Hans my cto (Compile Time Objects) style has a rather narrow use-case (close to the hardware, compile-time known situation), it is more a C-killer than a replacement for tranditional uses of virtual-based OO. A usefull bycatch is that the absence of indirection makes it possible to calculate the stack size. \$\endgroup\$ Dec 25, 2017 at 9:07
4
\$\begingroup\$

If I understand correctly, you want to know what architecture specific features of the platform "pop up" in your C language environment, making it more challenging to write maintainable, portable code on both platforms.

C is already quite flexible in that it is a "portable assembler". All platforms you've selected have GCC/commercial compilers available that have support for C89 and C99 language standards, meaning you can run similar code on all platforms.

There are a few considerations:

  • Some architectures are Von Neumann (ARM, MIPS), others are Harvard. The main limitations arise when your C program needs to read data from ROM, e.g. to print strings, have data defined as "const" or similar.

Some platforms/compilers can hide this "limitation" better than others. E.g. on AVR you need to use specific macros to read ROM data. On PIC24/dsPIC there are also dedicated tblrd instuctions available. However in addition some parts also have the "program space visibility" (PSVPAG) feature available that allows for mapping a page of the FLASH into RAM, making immediate data addressing available without tblrd. The compiler can do this quite effectively.

ARM and MIPS are Von Neumann, thereby have memory regions for ROM, RAM and peripherals packed onto 1 bus. You won't notice any difference between reading data from RAM or "ROM".

  • If you dive below C, and look at generated instructions for certain operations, you'll find some large differences around I/O. ARM and MIPS are RISC load-store register architecture. This means that data access on the memory bus must go through MOV instructions. This also means that any modification of a peripheral value will lead to a read-modify-write (RMW) operation. There are some ARM parts that support Bit-Banding, that map set/clr-bit registers in the I/O peripheral space. However you need to code this access up yourself.

On the other hand a PIC24 allows ALU operations to read&write data directly via indirect addressing (even with pointer modifications..). This has some characteristics from a CISC like architecture, so 1 instruction can do more work. This design may lead to more complex CPU cores, lower clocks, higher power consumption, etc. Fortunately for you the part is already designed. ;-)

These differences can mean is a PIC24 can be "more punchy" w.r.t. I/O operations than a similarly clocked ARM or MIPS chip. However, you may get a much higher clocker ARM/MIPS part for the same price/package/design constraints. I guess for practical terms, I think a lot of "learning the platform" is getting to grips what the architecture can and cannot do, how fast a few set of operations will be, etc.

  • Peripherals, clock management, etc. differ per family of parts. Strictly speaking, this will also change within the ARM eco-system between vendors, except for a few Cortex m bound peripherals like NVIC and SysTick.

These differences can be encapsulated somewhat by device drivers, but in the end embedded firmware has a high level of coupling with the hardware, so custom work can sometimes not be avoided.

Also, if you're leaving the ecosystems of Microchip/former Atmel, you may find that ARM parts require more setup to get them going. I mean in terms of; enabling clocks to peripherals, then configuring peripherals and "enabling" them, setup NVIC separately, etc. This is just part of the learning curve. Once you remember to do all of these things, in the right order, writing device drivers for all these microcontrollers will feel quite similar at some point.

  • Also, try to use libraries like stdint.h, stdbool.h, etc. if you aren't already. These integer types make the widths explicit, which makes code behaviour most predictable between the platforms. This may mean the use of 32-bit integers on an 8-bit AVR; but if your code needs it so be it.
\$\endgroup\$
3
\$\begingroup\$

Yes and no. From a programmers perspective you are ideally hiding the details of the instruction set. But that is to some extent already not relevant the peripherals which is the whole point of writing the program are not part of the instruction set. Now at the same time you cant just compare 4096Byte flash parts across those instruction sets, particularly if using C, the amount of consumption of the flash/memory is heavily determined by the instruction set and compiler, some should never see a compiler (cough PIC cough) due to how much waste of those resources are consumed by compiling. Others flash consumption is a smaller overhead. Performance is also an issue when using a high level language and performance matters in MCU applications, so it can make a difference between spending $3 per board for the mcu or $1. Build 1 million units thats 2Million dollars you simply gave away relative to a few tens to a hundred thousand dollars in software development time.

If it is about making the programming easier (at the overall cost of the product) you should be able to download a developers package for the mcu such that the instruction set architecture is something you never see, so if that is your primary concern, it is not a concern. It still costs you money as far as cost of the product to use these libraries, but, time to market might be shorter, I find the libraries take more time/work to use vs directly talking to the peripherals.

Bottom line the instruction sets are the least of your worries, move on to real issues.

\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.