PIC32 vs dsPIC vs ARM vs AVR,当我们用 C 语言编程时,架构是否重要?

电器工程 微控制器 图片 实验室 爱特梅尔工作室 建筑学
2022-01-08 17:55:16

我们目前使用的是 32 位 PIC32 微控制器。它可以很好地满足我们的需求,但我们也在探索其他更适合我们的微控制器 + 我们还有其他项目正在选择 MCU。为此,我们选择了基于 ARM 的 SAM DA微控制器,它是相同的 32 位但基于 ARM(比 PIC32 更受欢迎 - 行业方面)。

现在对于 PIC32,我们使用 MPLAB,但对于 ARM cortex-M0,我们将使用 Atmel Studio。我们将在这两个平台上使用 C 语言。我担心的是,我们将使用两个 32 位微控制器(来自同一家公司)但具有不同的架构。这将需要我们学习两种不同的设备,并将增加我们的“学习曲线”+交付时间。但另一方面,我也认为,由于我们将在这两种情况下使用 C 语言,因此不应该听到 ARM 的学习曲线,并且也值得探索该处理器。

我的主要问题是,当我们用 C 语言编程时,架构有多大的不同,因为它提供了微控制器内部的抽象。考虑到 C 语言编程,MPLAPAtmel Studio 的主要区别是什么。

4个回答

这是一个很有主见的话题。我可以为自己说话(AVR、ARM、MSP430)。

差异 1(最显着)在外围设备中。每个 MCU 都有类似的 UART、SPI、定时器等——只是寄存器名称和位不同。大多数时候,这是我在芯片之间移动代码时必须处理的主要问题。解决方案:使用通用 API 编写驱动程序,以便您的应用程序可以移植。

差异 2 是内存架构。如果您想在 AVR 上的闪存中放置常量,您必须使用特殊属性和特殊函数来读取它们。在 ARM 世界中,您只需取消引用一个指针,因为只有一个地址空间(我不知道小型 PIC 如何处理它,但会假设它们更接近 AVR)。

区别3是中断声明和处理。avr-gccISR()宏。ARM 只有一个函数名(如 someUART_Handler() - 如果您使用 CMSIS 标头和启动代码)。ARM 中断向量可以放置在任何地方(包括 RAM)并在运行时进行修改(例如,如果您有两个可以切换的不同 UART 协议,则非常方便)。AVR 只能选择在“主闪存”或“引导加载程序部分”中使用向量(因此,如果您想以不同的方式处理中断,则必须使用if语句)。

差异 4 - 睡眠模式和电源控制。如果您需要最低功耗,那么您必须充分利用 MCU 的所有功能。这在 MCU 之间可能有很大不同 - 有些具有更粗略的省电模式,有些可以启用/禁用单个外围设备。一些 MCU 具有可调节的稳压器,因此您可以以较低的电压以较慢的速度运行它们。我没有看到一种简单的方法可以在具有 3 种全局电源模式和另一种具有 7 种电源模式的 MCU(比如说)上实现相同的效率和单独的外设时钟控制。

关心可移植性时最重要的一点是将代码清晰地拆分为依赖于硬件(驱动程序)和独立于硬件(应用程序)的部分。您可以使用模拟驱动程序(例如,控制台而不是 UART)在普通 PC 上开发和测试后者。这为我节省了很多时间,因为 90% 的应用程序代码在原型硬件从回流炉出来之前就已经完成了 :)

在我看来,ARM 的好处是“单一文化”——许多编译器(gcc、Keil、IAR 等等)的可用性,许多免费和官方支持的 IDE(至少对于 NXP、STM32、Silicon Labs、 Nordic),许多调试工具(SEGGER - 特别是 Ozone、ULINK、OpenOCD...)和许多芯片供应商(我什至不会开始命名它们)。PIC32 主要限于 Microchip(但只有在您不喜欢他们的工具时才重要。

说到C代码。99% 相同,if语句相同,循环以相同方式工作。但是,您应该关心本机字的大小。例如,如果您使用计数器,forAVR 上的循环是最快的,而在 ARM上是最快的类型(或)。如果您使用较小的类型,ARM 每次都必须检查 8 位溢出。uint8_tuint32_tint32_t

选择 MCU 和/或供应商通常主要与政治和物流有关(除非您有非常明确的工程限制,例如:高温 - 使用 MSP430 或 Vorago)。即使应用程序可以在任何东西上运行,并且在产品生命周期内只需要开发和支持5% 的代码(驱动程序) ——这仍然是公司的额外成本。我工作过的所有地方都有一个最喜欢的供应商和 MCU 产品线(比如“选择任何你想要的 Kinetis,除非有充分的理由选择不同的东西”)。如果您有其他人寻求帮助,这也会有所帮助,因此作为经理,我会避免让每个人都使用完全不同的芯片的 5 人开发部门。

我使用了来自四个不同制造商的多个 MCU。每次的主要工作都是熟悉外围设备。

例如,UART 本身并不太复杂,我很容易找到我的驱动程序端口。但最后一次我花了将近一天的时间来整理时钟、I/O 引脚中断、启用等。

GPIO 可能非常复杂。位设置、位清除、位切换、特殊功能启用/禁用、三态。接下来你会得到中断:任何边沿、上升、下降、低电平、高电平、自清零与否。

然后是 I2C、SPI、PWM、定时器和另外两种类型的外设,每种外设都有自己的时钟使能,并且每次寄存器都有新的位不同。对于所有这些,阅读数据表如何在哪种情况下设置哪个位需要花费很多时间。

最后一家制造商有很多我发现无法使用的代码示例。一切都被抽象化了。但是当我追查它时,代码经历了六个!设置 GPIO 位的函数调用级别。如果你有一个 3GHz 的处理器,但没有一个 48MHz 的 MCU,那就太好了。最后我的代码是一行:

GPIO->set_output = bit.

我曾尝试使用更通用的驱动程序,但我放弃了。在 MCU 上,您总是在空间和时钟周期上苦苦挣扎。我发现,如果您在一个称为 10KHz 的中断例程中生成特定波形,那么抽象层是第一个出现的。

所以现在我一切正常,我打算不再切换,除非有非常非常好的理由。

以上所有内容都必须根据您销售的产品数量和节省的产品进行摊销。销售一百万:节省 0.10 以切换到不同的类型意味着您可以在软件工时上花费 100.000。卖出 1000 个,你只有 100 个可以花。

这更像是一个意见/评论而不是一个答案。

您不想也不应该使用 C 进行编程。如果以正确的方式使用C++,它的优势要大得多。(好吧,我不得不承认,当以错误的方式使用时,它比 C 差得多。)这将您限制在具有(现代)C++ 编译器的芯片上,这大致是 GCC 支持的一切,包括 AVR(与一些限制,filo 提到了地址空间不统一的问题),但几乎排除了所有 PIC(可以支持 PIC32,但我还没有看到任何像样的端口)。

当您在 C/C++ 中编程算法时,您提到的选择之间的差异很小(除了当您执行大量 16、32 或更高位算术时,8 位或 16 位芯片将处于严重劣势)。当您需要最后一盎司的性能时,您可能需要使用汇编程序(您自己的或供应商或第三方提供的代码)。在这种情况下,您可能需要重新考虑您选择的芯片。

当您对硬件进行编码时,您可以使用一些抽象层(通常由制造商提供)或编写自己的(基于数据表和/或示例代码)。IME 现有的 C 抽象(mbed、cmsis、...)通常在功能上(几乎)是正确的,但在性能(检查 oldfarts 咆哮关于引脚设置操作的 6 层间接)、可用性和可移植性方面非常失败。他们希望向您公开特定芯片的所有功能,在几乎所有情况下,您都不需要也不会关心这些功能,并且它将您的代码锁定给该特定供应商(可能还有该特定芯片)。

这是 C++ 可以做得更好的地方:如果做得好,一个 pin 集可以通过 6 个或更多抽象层(因为这使得更好的(可移植的!)接口和更短的代码成为可能),同时提供一个独立于目标的接口对于简单的情况仍然会产生与您在汇编程序中编写的相同的机器代码

我使用的编码风格的片段,它可以让你兴奋或惊恐地转身离开:

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

实际上还有更多的抽象层。然而,LED 的最终使用,比如说打开它,并没有显示目标的复杂性或细节(对于 arduin uno 或 ST32 蓝色药丸,代码将是相同的)。

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

编译器不会被所有这些层吓倒,并且由于不涉及任何虚函数,优化器可以查看所有内容(省略了一些细节,例如启用外围时钟):

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

这就是我在汇编器中编写它的方式——如果我意识到 PIO 寄存器可以与公共基址的偏移量一起使用。在这种情况下,我可能会,但编译器在优化这些事情方面比我好得多。

因此,据我所知,它是:为您的硬件编写一个抽象层,但要在现代 C++(概念、模板)中进行,这样就不会损害您的性能。有了它,您可以轻松切换到另一个芯片。您甚至可以开始在您已经放置、熟悉、拥有良好调试工具等的随机芯片上进行开发,并将最终选择推迟到以后(当您获得有关所需内存、CPU 速度等的更多信息时)。

IMO 嵌入式开发的谬误之一是首先选择芯片(这是这个论坛上经常被问到的一个问题:我应该选择哪种芯片......。最好的答案通常是:没关系。)

(编辑 - 对“所以性能明智,C 或 C++ 将处于同一水平?”的回应)

对于相同的构造,C 和 C++ 是相同的。C++ 有更多的抽象结构(只有几个:类、模板、constexpr),可以像任何工具一样用于好或坏。为了使讨论更有趣:不是每个人都同意什么是好是坏......

如果我理解正确,您想知道在您的 C 语言环境中“弹出”平台的哪些架构特定功能,这使得在两个平台上编写可维护、可移植的代码更具挑战性。

C 已经非常灵活,因为它是一个“便携式汇编程序”。您选择的所有平台都提供支持 C89 和 C99 语言标准的 GCC/商业编译器,这意味着您可以在所有平台上运行类似的代码。

有几个考虑:

  • 一些架构是冯诺依曼(ARM,MIPS),其他的是哈佛。当您的 C 程序需要从 ROM 读取数据时,主要限制出现,例如打印字符串,将数据定义为“const”或类似的。

一些平台/编译器可以比其他平台更好地隐藏这个“限制”。例如,在 AVR 上,您需要使用特定的宏来读取 ROM 数据。在 PIC24/dsPIC 上也有专用的 tblrd 指令可用。然而,除此之外,某些部件还具有“程序空间可见性”(PSVPAG)功能,允许将 FLASH 的页面映射到 RAM,从而无需 tblrd 即可使用即时数据寻址。编译器可以非常有效地做到这一点。

ARM 和 MIPS 是冯诺依曼,因此将 ROM、RAM 和外围设备的存储区域打包到 1 条总线上。您不会注意到从 RAM 或“ROM”读取数据之间的任何区别。

  • 如果您深入了解 C 语言,并查看为某些操作生成的指令,您会发现 I/O 有一些很大的不同。ARM 和 MIPS 是 RISC加载存储寄存器架构这意味着内存总线上的数据访问必须通过 MOV 指令。这也意味着外设值的任何修改都将导致读-修改-写 (RMW) 操作。有一些 ARM 部件支持 Bit-Banding,映射 I/O 外设空间中的 set/clr-bit 寄存器。但是,您需要自己编写此访问权限。

另一方面,PIC24 允许 ALU 操作通过间接寻址直接读写数据(即使使用指针修改......)。这具有类似 CISC 架构的一些特征,因此 1 条指令可以完成更多工作。这种设计可能会导致更复杂的 CPU 内核、更低的时钟、更高的功耗等。幸运的是,该部件已经设计好了。;-)

这些差异可能意味着 PIC24 在 I/O 操作方面比类似时钟的 ARM 或 MIPS 芯片“更强大”。但是,在相同的价格/封装/设计限制条件下,您可能会获得更高的时钟 ARM/MIPS 部件。我想就实际而言,我认为很多“学习平台”是要掌握架构能做什么和不能做什么,几组操作的速度等等。

  • 每个系列的外设、时钟管理等都不同。严格来说,这也将在供应商之间的 ARM 生态系统内发生变化,除了少数 Cortex m 绑定的外围设备,如 NVIC 和 SysTick。

这些差异可以通过设备驱动进行一定程度的封装,但最终嵌入式固件与硬件的耦合程度很高,因此有时无法避免定制工作。

此外,如果您要离开 Microchip/前 Atmel 的生态系统,您可能会发现 ARM 部件需要更多设置才能运行。我的意思是;启用外围设备的时钟,然后配置外围设备并“启用”它们,单独设置 NVIC 等。这只是学习曲线的一部分。一旦你记得按照正确的顺序做所有这些事情,为所有这些微控制器编写设备驱动程序在某些时候会感觉非常相似。

  • 此外,如果您还没有,请尝试使用 stdint.h、stdbool.h 等库。这些整数类型使宽度明确,这使得平台之间的代码行为最可预测。这可能意味着在 8 位 AVR 上使用 32 位整数;但是,如果您的代码需要它,那就这样吧。