PIC32 vs dsPIC vs ARM vs AVR, ¿importa la arquitectura cuando estamos programando en lenguaje C de todos modos? [cerrado]

Actualmente estamos utilizando un microcontrolador PIC32 de 32 bits. Está funcionando bien para nuestras necesidades, pero también estamos explorando otros microcontroladores que puedan adaptarse mejor a nosotros + tenemos otros proyectos para los que estamos seleccionando MCU. Para ese propósito, hemos seleccionado el microcontrolador SAM DA basado en ARM, que es el mismo de 32 bits pero está basado en ARM (más popular que PIC32, en la industria).

Ahora para PIC32 usamos MPLAB pero para ARM cortex-M0, usaremos Atmel Studio. Usaremos lenguaje C en ambas plataformas. Lo que me preocupa es que usaremos dos microcontroladores de 32 bits (de la misma compañía) pero con arquitecturas diferentes. Esto requerirá que aprendamos dos dispositivos diferentes y aumentará nuestra "curva de aprendizaje" + tiempo de entrega. Pero, por otro lado, también creo que dado que usaremos C-Language en ambos casos, la curva de aprendizaje para ARM no debería ser tan escuchada y vale la pena explorar ese procesador también.

Mi pregunta principal es qué tan grande es la diferencia que hace la arquitectura cuando estamos programando en C-Language, ya que proporciona una abstracción de las partes internas del microcontrolador. Y cuáles son las principales diferencias entre MPLAP y Atmel Studio , considerando la programación en lenguaje C.

Si las cosas funcionan con el PIC32, ¿cuál es el punto de cambiar? Incluso si el código se adapta por completo (no lo hará), todavía queda la nueva cadena de herramientas y el IDE al que acostumbrarse. ¿Cuál es el punto de? Cambiar por motivos religiosos o para estar "basado en ARM" (o cualquier otra cosa) es una tontería. Necesitas tener una buena razón, pero no nos has mostrado ninguna.
No pregunté sobre el cambio. Hablé sobre elegir una arquitectura diferente para otros proyectos, ya que estamos trabajando en varios proyectos y hay margen de mejora en nuestro diseño existente. El punto principal fue sobre la curva de aprendizaje y los desafíos de trabajar con dos arquitecturas diferentes al mismo tiempo.
Una cosa que descubrí es que Atmel Studio proporciona una sincronización superior que el video de YouTube de MPLAB

Respuestas (5)

Este es un tema bastante opinable. Puedo hablar por mí mismo (AVR, ARM, MSP430).

La diferencia 1 (la más significativa) está en los periféricos. Cada uno de los MCU tiene UART, SPI, temporizadores, etc. similares; solo los nombres de registro y los bits son diferentes. La mayoría de las veces era el problema principal con el que tenía que lidiar al mover código entre chips. Solución: escriba sus controladores con una API común, para que su aplicación pueda ser portátil.

La diferencia 2 es la arquitectura de la memoria. Si desea colocar constantes en flash en un AVR, debe usar atributos especiales y funciones especiales para leerlas. En el mundo ARM, simplemente elimina la referencia de un puntero porque hay un solo espacio de direcciones (no sé cómo lo manejan los PIC pequeños, pero supondría que están más cerca de AVR).

La diferencia 3 es la declaración y el manejo de interrupciones. avr-gcctiene la ISR()macro. ARM solo tiene un nombre de función (como someUART_Handler(), si usa encabezados CMSIS y código de inicio). Los vectores de interrupción ARM se pueden colocar en cualquier lugar (incluida la RAM) y modificar en tiempo de ejecución (muy útil si tiene, por ejemplo, dos protocolos UART diferentes que se pueden cambiar). AVR solo tiene la opción de usar vectores en "flash principal" o en la "sección del cargador de arranque" (por lo que si desea manejar las interrupciones de manera diferente, debe usar una ifdeclaración).

Diferencia 4: modos de suspensión y control de potencia. Si necesita el menor consumo de energía, debe aprovechar todas las funciones de la MCU. Esto puede diferir mucho entre MCU: algunos tienen modos de ahorro de energía más gruesos, algunos pueden habilitar/deshabilitar periféricos individuales. Algunas MCU tienen reguladores ajustables para que pueda ejecutarlas con un voltaje más bajo a una velocidad más lenta, etc. No veo una manera fácil de lograr la misma eficiencia en una MCU (digamos) con 3 modos de potencia globales y otra con 7 modos de potencia y control de reloj periférico individual.

Lo más importante cuando se preocupa por la portabilidad es dividir claramente su código en partes dependientes del hardware (controladores) e independientes del hardware (aplicación). Puede desarrollar y probar este último en una PC normal con un controlador simulado (por ejemplo, una consola en lugar de un UART). Esto me salvó muchas veces ya que el 90 % del código de la aplicación estaba completo antes de que el prototipo de hardware saliera del horno de reflujo :)

En mi opinión, lo bueno de ARM es el "monocultivo": disponibilidad de muchos compiladores (gcc, Keil, IAR... por nombrar algunos), muchos IDE gratuitos y con soporte oficial (al menos para NXP, STM32, Silicon Labs, Nordic), muchas herramientas de depuración (SEGGER, especialmente Ozone, ULINK, OpenOCD...) y muchos proveedores de chips (ni siquiera comenzaré a nombrarlos). El PIC32 se limita principalmente a Microchip (pero solo importa si no te gustan sus herramientas.

Cuando se trata de código C. Es 99% lo mismo, una ifdeclaración es la misma, un ciclo funciona de la misma manera. Sin embargo, debe preocuparse por el tamaño de la palabra nativa. Por ejemplo, un forbucle en un AVR es más rápido si lo usa uint8_tpara el contador, mientras que en ARM uint32_tes el tipo más rápido (o int32_t). ARM tendría que verificar el desbordamiento de 8 bits cada vez que usara un tipo más pequeño.

La selección de una MCU y/o proveedor en general se trata principalmente de política y logística (a menos que tenga restricciones de ingeniería muy claras, por ejemplo: alta temperatura, use MSP430 o Vorago). Incluso si la aplicación puede ejecutarse en cualquier cosa y solo el 5% del código (controladores) debe desarrollarse y admitirse durante la vida útil del producto , sigue siendo un costo adicional para la empresa. Todos los lugares en los que he trabajado tenían un proveedor favorito y una línea de MCU (como "elija cualquier Kinetis que desee a menos que haya una muy buena razón para elegir algo diferente"). También ayuda si tiene otras personas a las que pedir ayuda, por lo que, como gerente, evitaría tener un departamento de desarrollo de 5 personas en el que todos usaran un chip totalmente diferente.

“AVR es el más rápido si usa uint8_t para el contador, mientras que en ARM uint32_t es el tipo más rápido (o int32_t). ARM tendría que verificar el desbordamiento de 8 bits cada vez que usara un tipo más pequeño”. puede usar uint_fast8_t si solo necesita al menos 8 bits.
@Michael: seguro que puede usar los tipos _fast, pero no puede contar con el comportamiento de desbordamiento. En mi stdint.h de gcc tengo "typedef unsigned int uint_fast8_t", que básicamente es un uint32_t :)
Tratar de escribir una API que sea eficiente, universal y completa es difícil dado que las diferentes plataformas tienen diferentes capacidades. La CPU probablemente importa menos que los periféricos y las decisiones de diseño que se toman con ellos. Por ejemplo, algunos dispositivos han permitido reconfigurar varios periféricos en cualquier momento en unos pocos microsegundos como máximo, mientras que otros pueden requerir múltiples pasos repartidos en cientos de microsegundos o incluso milisegundos. Una función API que está diseñada para el patrón anterior puede usarse dentro de una rutina de servicio de interrupción que se ejecuta a 10,000 Hz, pero...
...no podría admitir dicho uso en plataformas que requerirían distribuir las operaciones en cientos de microsegundos. No sé por qué los diseñadores de hardware no parecen esforzarse mucho en admitir la semántica API de "operación rápida en cualquier momento", pero muchos usan un modelo que sincroniza operaciones individuales en lugar de estados, de modo que si, por ejemplo, se ha dado una solicitud a enciende un dispositivo y el código se da cuenta de que no necesita estar encendido, el código debe esperar a que el dispositivo se encienda antes de poder emitir la solicitud de apagado. Manejar eso sin problemas en una API agrega complicaciones importantes.

He usado varias MCU de cuatro fabricantes diferentes. El trabajo principal una vez más es familiarizarse con los periféricos.

Por ejemplo, un UART en sí no es demasiado complejo y encuentro fácilmente el puerto de mis controladores. Pero la última vez me tomó casi un día arreglar los relojes, la interrupción de los pines de E/S, habilitar, etc.

El GPIO puede ser muy complejo. Bit-set, bit-clear, bit-toggle, activar/desactivar funciones especiales, tri-state. A continuación, obtiene interrupciones: cualquier borde, subida, bajada, nivel bajo, nivel alto, autolimpieza o no.

Luego están I2C, SPI, PWM, Timers y dos docenas más de tipos de periféricos, cada uno con sus propias habilitaciones de reloj y cada vez que los registros son diferentes con nuevos bits. Para todos ellos, lleva muchas horas leer la hoja de datos sobre cómo configurar qué bit en qué circunstancias.

El último fabricante tenía muchos ejemplos de código que encontré inutilizables. Todo estaba abstraído. Pero cuando lo rastreé, ¡el código pasó por seis! niveles de llamadas de función para establecer un bit GPIO. Es bueno si tiene un procesador de 3 GHz pero no en una MCU de 48 MHz. Mi código al final era una sola línea:

GPIO->set_output = bit.

He intentado usar controladores más genéricos pero me he dado por vencido. En una MCU siempre estás luchando con el espacio y los ciclos de reloj. Descubrí que la capa de abstracción es la primera en desaparecer si genera una forma de onda específica en una rutina de interrupción llamada a 10 KHz.

Así que ahora tengo todo funcionando y planeo NO volver a cambiar a menos que sea por una muy, muy buena razón.

Todo lo anterior se debe amortizar sobre cuántos productos vendas y lo que ahorres. Vender un millón: ahorrar 0,10 para cambiar a un tipo diferente significa que puede gastar 100.000 en horas-hombre de software. Vendiendo 1000 te quedan solo 100 para gastar.

Personalmente, es por eso que me quedo con el ensamblador. Encantador binario, sin abstracción.
El preprocesador de C puede funcionar bastante bien con cosas, especialmente cuando se combina con intrínsecos constantes incorporados. Si uno define constantes para cada bit de E/S de la forma (número de puerto*32 + número de bit), es posible escribir una macro para OUTPUT_HI(n)la cual producirá un código equivalente a GPIOD->bssr |= 0x400;si nes una constante como 0x6A, pero llame a una subrutina simple si nes no constante Habiendo dicho eso, la mayoría de las API de proveedores que he visto oscilan entre mediocres y horribles.

Esto es más una opinión/comentario que una respuesta.

No quieres ni deberías estar programando en C. C++, cuando se usa de la manera correcta , es muy superior. (Está bien, debo admitir que cuando se usa de manera incorrecta es mucho peor que C). Eso lo limita a chips que tienen un compilador C++ (moderno), que es aproximadamente todo lo que admite GCC, incluido AVR (con algunas limitaciones, filo menciona los problemas de un espacio de direcciones no uniforme), pero excluyendo casi todos los PIC (se podría admitir PIC32, pero aún no he visto ningún puerto decente).

Cuando está programando algoritmos en C/C++, la diferencia entre las opciones que menciona es pequeña (excepto que un chip de 8 o 16 bits tendrá una gran desventaja cuando haga mucha aritmética de 16, 32 o más bits). Cuando necesite el último gramo de rendimiento, probablemente necesitará usar ensamblador (ya sea el suyo propio o el código proporcionado por el proveedor o un tercero). En ese caso, es posible que desee volver a considerar el chip que seleccionó.

Cuando está codificando para el hardware, puede usar alguna capa de abstracción (a menudo proporcionada por el fabricante) o escribir la suya propia (según la hoja de datos y/o el código de ejemplo). Las abstracciones C existentes de IME (mbed, cmsis, ...) a menudo son funcionalmente (casi) correctas, pero fallan terriblemente en el rendimiento (verifique que Oldfarts despotrica sobre 6 capas de direccionamiento indirecto para una operación de conjunto de pines), usabilidad y portabilidad. Quieren exponerle toda la funcionalidad del chip en particular, que en casi todos los casos no necesitará y más bien no le importará, y bloquea su código para ese proveedor en particular (y probablemente ese chip en particular).

Aquí es donde C ++ puede hacerlo mucho mejor: cuando se hace correctamente, un conjunto de pines puede pasar por 6 o más capas de abstracción (porque eso hace posible una interfaz mejor (¡portátil!) y un código más corto), pero proporciona una interfaz que es independiente del objetivo para los casos simples , y aun así dar como resultado el mismo código de máquina que escribiría en ensamblador .

Un fragmento del estilo de codificación que uso, que puede entusiasmarte o alejarte horrorizado:

// 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 > >;

En realidad hay algunas capas más de abstracción. Sin embargo, el uso final del led, digamos para encenderlo, no muestra la complejidad o los detalles del objetivo (para un arduin uno o una pastilla azul ST32, el código sería idéntico).

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

El compilador no se siente intimidado por todas esas capas, y debido a que no hay funciones virtuales involucradas, el optimizador ve todo (algunos detalles, omitidos, como habilitar el reloj periférico):

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

Así es como lo habría escrito en ensamblador, SI me hubiera dado cuenta de que los registros PIO se pueden usar con compensaciones desde una base común. En este caso, probablemente lo haría, pero el compilador es mucho mejor que yo para optimizar esas cosas.

Por lo que tengo una respuesta, es: escriba una capa de abstracción para su hardware, pero hágalo en C++ moderno (conceptos, plantillas) para que no perjudique su rendimiento. Con eso en su lugar, puede cambiar fácilmente a otro chip. Incluso puede comenzar a desarrollar en algún chip aleatorio que tenga, con el que esté familiarizado, tenga buenas herramientas de depuración, etc. y posponga la elección final hasta más tarde (cuando tenga más información sobre la memoria requerida, la velocidad de la CPU, etc.).

En mi opinión, una de las falacias del desarrollo integrado es elegir primero el chip (es una pregunta que se hace a menudo en este foro: qué chip debo elegir... La mejor respuesta es generalmente: no importa).

(editar - respuesta a "Entonces, en cuanto al rendimiento, ¿C o C ++ estarían al mismo nivel?")

Para las mismas construcciones, C y C++ son iguales. C ++ tiene muchas más construcciones para la abstracción (solo algunas: clases, plantillas, constexpr) que pueden, como cualquier herramienta, usarse para bien o para mal. Para hacer las discusiones más interesantes: no todos están de acuerdo en lo que es bueno o malo...

Entonces, en cuanto al rendimiento, ¿C o C ++ estarían al mismo nivel? Creo que C++ tendrá más sobrecarga. Definitivamente me indicaste la dirección correcta, C++ es el camino a seguir, no C.
Las plantillas de C++ fuerzan el polimorfismo en tiempo de compilación que puede tener un costo cero (o incluso negativo) en términos de rendimiento, ya que el código se compila para cada caso de uso específico. Sin embargo, esto tiende a prestarse mejor a la velocidad de orientación (O3 para GCC). El polimorfismo en tiempo de ejecución, al igual que las funciones virtuales, puede sufrir una penalización mucho mayor, aunque podría decirse que es más fácil de mantener y, en algunos casos, lo suficientemente bueno.
Afirmas que C++ es mejor, pero luego vas y usas moldes de estilo C. Para vergüenza.
@JAB Nunca sentí mucho por los moldes de nuevo estilo, pero los probaré. Pero mi prioridad actual está en otras partes de esta biblioteca. El problema real es, por supuesto, que no pude pasar los punteros como parámetros de plantilla.
@Hans mi estilo cto (Compile Time Objects) tiene un caso de uso bastante estrecho (cerca del hardware, situación conocida en tiempo de compilación), es más un C-killer que un reemplazo para los usos tradicionales de OO basado en virtual. Una captura incidental útil es que la ausencia de direccionamiento indirecto hace posible calcular el tamaño de la pila.

Si entiendo correctamente, desea saber qué características específicas de la arquitectura de la plataforma "aparecen" en su entorno de lenguaje C, lo que hace que sea más difícil escribir código portátil y mantenible en ambas plataformas.

C ya es bastante flexible en el sentido de que es un "ensamblador portátil". Todas las plataformas que ha seleccionado tienen compiladores comerciales/GCC disponibles que son compatibles con los estándares de lenguaje C89 y C99, lo que significa que puede ejecutar un código similar en todas las plataformas.

Hay algunas consideraciones:

  • Algunas arquitecturas son Von Neumann (ARM, MIPS), otras son Harvard. Las principales limitaciones surgen cuando su programa C necesita leer datos de la ROM, por ejemplo, para imprimir cadenas, tener datos definidos como "const" o similar.

Algunas plataformas/compiladores pueden ocultar esta "limitación" mejor que otros. Por ejemplo, en AVR necesita usar macros específicas para leer datos de ROM. En PIC24/dsPIC también hay disponibles instrucciones tblrd dedicadas. Sin embargo, además, algunas partes también tienen disponible la función de "visibilidad del espacio del programa" (PSVPAG) que permite mapear una página de FLASH en RAM, haciendo que el direccionamiento de datos inmediato esté disponible sin tblrd. El compilador puede hacer esto con bastante eficacia.

ARM y MIPS son Von Neumann, por lo que tienen regiones de memoria para ROM, RAM y periféricos empaquetados en 1 bus. No notará ninguna diferencia entre leer datos de RAM o "ROM".

  • Si se sumerge por debajo de C y observa las instrucciones generadas para ciertas operaciones, encontrará grandes diferencias en torno a la E/S. ARM y MIPS son arquitectura de registro de almacenamiento de carga RISC . Esto significa que el acceso a los datos en el bus de memoria debe pasar por instrucciones MOV. Esto también significa que cualquier modificación de un valor periférico conducirá a una operación de lectura, modificación y escritura (RMW). Hay algunas partes de ARM que admiten Bit-Banding, que asignan registros set/clr-bit en el espacio periférico de E/S. Sin embargo, debe codificar este acceso usted mismo.

Por otro lado, un PIC24 permite operaciones de ALU para leer y escribir datos directamente a través de direccionamiento indirecto (incluso con modificaciones de puntero...). Esto tiene algunas características de una arquitectura similar a CISC, por lo que 1 instrucción puede hacer más trabajo. Este diseño puede conducir a núcleos de CPU más complejos, relojes más bajos, mayor consumo de energía, etc. Afortunadamente para usted, la pieza ya está diseñada. ;-)

Estas diferencias pueden significar que un PIC24 puede ser operaciones de E/S "más impactantes" que un chip ARM o MIPS con reloj similar. Sin embargo, puede obtener una parte ARM/MIPS mucho más alta por las mismas restricciones de precio/paquete/diseño. Supongo que, en términos prácticos, creo que gran parte del "aprendizaje de la plataforma" consiste en comprender lo que la arquitectura puede y no puede hacer, qué tan rápido será un conjunto de operaciones, etc.

  • Los periféricos, la gestión del reloj, etc. difieren según la familia de piezas. Estrictamente hablando, esto también cambiará dentro del ecosistema ARM entre proveedores, a excepción de algunos periféricos vinculados a Cortex m como NVIC y SysTick.

Estas diferencias se pueden encapsular un poco con los controladores de dispositivos, pero al final el firmware incorporado tiene un alto nivel de acoplamiento con el hardware, por lo que a veces no se puede evitar el trabajo personalizado.

Además, si está dejando los ecosistemas de Microchip/antiguo Atmel, es posible que descubra que las piezas ARM requieren más configuración para que funcionen. Quiero decir en términos de; habilitar relojes para periféricos, luego configurar periféricos y "habilitarlos", configurar NVIC por separado, etc. Esto es solo parte de la curva de aprendizaje. Una vez que recuerde hacer todas estas cosas, en el orden correcto, escribir controladores de dispositivos para todos estos microcontroladores se sentirá bastante similar en algún momento.

  • Además, intente usar bibliotecas como stdint.h, stdbool.h, etc. si aún no lo ha hecho. Estos tipos de enteros hacen que los anchos sean explícitos, lo que hace que el comportamiento del código sea más predecible entre las plataformas. Esto puede significar el uso de números enteros de 32 bits en un AVR de 8 bits; pero si su código lo necesita, que así sea.

Si y no. Desde la perspectiva de los programadores, lo ideal es ocultar los detalles del conjunto de instrucciones. Pero eso ya no es relevante hasta cierto punto, los periféricos, que es el punto central de escribir el programa, no son parte del conjunto de instrucciones. Ahora, al mismo tiempo, no puede simplemente comparar partes flash de 4096 bytes en esos conjuntos de instrucciones, particularmente si usa C, la cantidad de consumo de flash/memoria está fuertemente determinada por el conjunto de instrucciones y el compilador, algunos nunca deberían ver un compilador (tos PIC tos) debido a la cantidad de desperdicio de esos recursos que se consumen al compilar. El consumo de flash de otros es una sobrecarga menor. El rendimiento también es un problema cuando se usa un lenguaje de alto nivel y el rendimiento es importante en las aplicaciones de MCU, por lo que puede marcar la diferencia entre gastar $3 por placa para el mcu o $1.

Si se trata de facilitar la programación (al costo total del producto), debería poder descargar un paquete de desarrolladores para el mcu, de modo que la arquitectura del conjunto de instrucciones sea algo que nunca vea, por lo que si esa es su principal preocupación, es no es una preocupación. Todavía le cuesta dinero en cuanto al costo del producto usar estas bibliotecas, pero, el tiempo de comercialización puede ser más corto, creo que las bibliotecas toman más tiempo/trabajo para usar en lugar de hablar directamente con los periféricos.

En pocas palabras, los conjuntos de instrucciones son la menor de sus preocupaciones, pase a los problemas reales.