STM32DUINO V3.0 Core API 升级可能造成的问题及对策 Arduino 官方发布了一套通用API 框架希望减少差异一统各硬件平台比如AVRSTM32。官方repo 链接arduino/ArduinoCore-API。stm32duino 在V2.12 版本后发布了V3.0全面迁移到了这套Core API但是目前我的建议是先不要跟着升级尤其是用V2 版本的老项目至少也需要仔细测试评估过再说。原因存在一批可能导致不兼容的API 变更有用户报告说编译后程序体积变大了4.8kB(15.3%)导致Flash 里塞不下串口和SPI 类全面虚函数化对运行时性能可能有少量影响V3.0.0 版本迁移没处理干净留下了一些BUG而V3.0.1 版本尚未发布这次升级并没有带来明显的功能性收益升级收益新增STM32C5xx 系列硬件支持并引入C5xx 系列的HALv2 库变更和问题1、引入Arduino Core APIstm32duino 自己fork 了一份core api而不是直接引入arduino 官方的东西链接在stm32duino/ArduinoCore-API。理论上迁移到core api 以后能提高用户程序的可移植性和库的通用性但是我觉得收益很小因为不通用的部分主要是真实的硬件架构和外设差异这些东西不太可能完美的靠api 层覆盖。比方说怎么在没有DMA 的硬件上使用DMA现有各arduino 平台在通用性上已经做的差不多了。2、外设抽象类这是最显著的变化core api 里定义了几个外设抽象类HardwareSerialHardwareSPIHardwareI2C2.1、HardwareSerial其中HardwareSerial顶掉了旧的串口实现类的名字于是以后Serial对象的类型要变更为Uart。旧的v2 版程序里的对象定义都要改// v2HardwareSerialSerial1(PA9,PA10);HardwareSerialSerial3(PB11,PB10,NC,NC);// v3UartSerial1(PA9,PA10);UartSerial3(PB11,PB10,NC,NC);因为Uart继承了HardwareSerial所以旧代码里接收HardwareSerial对象指针或引用的部分不用改比如voidwait_for_serial(HardwareSerials){while(s.available()0){yield();}}理论上旧代码要改的应该也不多Uart类的接口和旧的HardwareSerial完全兼容但是我觉得更好的做法应该是用HardwareUART作为抽象基类留着HardwareSerial保持兼容。实际上我对这整套抽象基类的设计都很不喜欢太土味OOP 了徒增虚函数开销只实现了约束接口的作用不如用concept。2.2 HardwareSPI传统的Arduino SPI 是个仅作为SPI 主机侧使用的接口内部实现非常简单用软件轮询驱动通讯。现在SPIClass成为了HardwareSPI的实现类接口变成虚函数还增加了SPI 从机侧和硬件中断接口。下面是抽象类定义classHardwareSPI{public:virtual~HardwareSPI(){}virtualuint8_ttransfer(uint8_tdata)0;virtualuint16_ttransfer16(uint16_tdata)0;virtualvoidtransfer(void*buf,size_t count)0;// Transaction FunctionsvirtualvoidusingInterrupt(intinterruptNumber)0;virtualvoidnotUsingInterrupt(intinterruptNumber)0;virtualvoidbeginTransaction(SPISettings settings)0;virtualvoidendTransaction(void)0;// SPI Configuration methodsvirtualvoidattachInterrupt()0;virtualvoiddetachInterrupt()0;virtualvoidbegin()0;virtualvoidend()0;};旧的setter 彻底删掉了现在只能经过beginTransaction传入SPI 硬件配置。SPISettings类也被标准化成单纯的配置参数容器而AVR 平台上SPISettings的旧实现是内置了配置参数到寄存器值的转换不存储原始配置参数不提供getter只缓存了寄存器值用于快速复用配置。不知道新的core api 要怎么处理这个行为冲突。毕竟AVR 平台是arduino 生态的老家不太可能放着不管。classSPISettings{public:constexprSPISettings(uint32_tclock,BitOrder bitOrder,SPIMode dataMode,SPIBusMode busModeSPI_CONTROLLER):clockFreq(clock),dataMode(dataMode),bitOrder(bitOrder),busMode(busMode){}constexprSPISettings(uint32_tclock,BitOrder bitOrder,intdataMode,SPIBusMode busModeSPI_CONTROLLER):clockFreq(clock),dataMode((SPIMode)dataMode),bitOrder(bitOrder),busMode(busMode){}// Default speed set to 4MHz, SPI mode set to MODE 0 and Bit order set to MSB first.constexprSPISettings():clockFreq(4000000),dataMode(SPI_MODE0),bitOrder(MSBFIRST),busMode(SPI_CONTROLLER){}booloperator(constSPISettingsrhs)const;booloperator!(constSPISettingsrhs)const;uint32_tgetClockFreq()const;SPIModegetDataMode()const;BitOrdergetBitOrder()const;SPIBusModegetBusMode()const;private:uint32_tclockFreq;SPIMode dataMode;BitOrder bitOrder;SPIBusMode busMode;friendclassHardwareSPI;};另外一个问题是枚举命名变更SPI 设备模式枚举的名字把MASTER改成了CONTROLLERSLAVE改成了PERIPHERAL// V2// Device modetypedefenum{SPI_MASTER,/* Device is master */SPI_SLAVE/* Device is slave */}SPIDeviceMode;// V3// Platforms should define SPI_HAS_PERIPHERAL_MODE if SPI peripheral// mode is supported, to allow applications to check whether peripheral// mode is available or not.typedefenum{SPI_CONTROLLER0,SPI_PERIPHERAL1,}SPIBusMode;这个问题不大SPI 库一直都是默认以主模式运行根本不支持从模式上面V3 的注释也说了硬件平台可以自己决定要不要支持从模式如果支持就定义宏SPI_HAS_PERIPHERAL_MODE应用代码应该检查这个标志。政治正确的问题没什么好说的。与这个改名同步Arduino 现在也在倡议把信号线名称MISO改成CIPO, 也就是从MasterInSlaveOut 改为*C**ontrollerInPeripheralOut。MOSISS 也一样。另一种说法是把MOSI 改成SDO (SerialDataOut)MISO 改成SDI (SerialDataIn)省略主从关系默认以主设备为中心看数据流向。怎么感觉这样改反而是完全剥夺了从设备的权益[doge]。2.3、HardwareI2C和SPI 库一样core api 提供了硬件I2C 抽象类Wire 或TwoWire 现在要继承它并实现那些虚函数。不过Wire 库本身就比较庞大多一层虚函数影响比SPI 库更小。下面是抽象类定义classHardwareI2C:publicStream{public:virtualvoidbegin()0;virtualvoidbegin(uint8_taddress)0;virtualvoidend()0;virtualvoidsetClock(uint32_tfreq)0;virtualvoidbeginTransmission(uint8_taddress)0;virtualuint8_tendTransmission(boolstopBit)0;virtualuint8_tendTransmission(void)0;virtualsize_trequestFrom(uint8_taddress,size_t len,boolstopBit)0;virtualsize_trequestFrom(uint8_taddress,size_t len)0;virtualvoidonReceive(void(*)(int))0;virtualvoidonRequest(void(*)(void))0;};I2C 还保留了setClock接口而不是要求在beginTrasmission时传入配置参数emmm有点恶心。另外注意Wire 库里begin函数有多个重载voidbegin(uint8_t,boolgeneralCallfalse,boolNoStretchModefalse);voidbegin(int,boolgeneralCallfalse,boolNoStretchModefalse);voidbegin()override{begin(false);}voidbegin(uint8_taddress)override{begin(address,false,false);}其中从core api 继承的begin(uint8_t)可能和begin(uint8_t, bool, bool)冲突因为后者的两个bool 参数都有默认值。而如果传入的地址是字面量整数就ok因为整数字面量默认类型是int会优先匹配到begin(int, bool, bool)这个重载。uint8_taddr0x3c;begin(addr);// 出错同时匹配到两个重载begin(0x3c);// 没问题至于有这个int重载的原因是为了兼容I2C 标准里的11bit 扩展地址用uint8_t只能传入一般的7bit 地址。显然arduino 官方的core api 设计没考虑到这个问题。3、变更的枚举和常量值引脚模式枚举里面OUTPUT_OPEN_DRAIN和INPUT_ANALOG的值互换了代码里没直接使用整数值的话不影响。// V2#defineINPUT0x0#defineOUTPUT0x1#defineINPUT_PULLUP0x2#defineINPUT_PULLDOWN0x3#defineINPUT_ANALOG0x4#defineOUTPUT_OPEN_DRAIN0x5// V3typedefenum{INPUT0x0,OUTPUT0x1,INPUT_PULLUP0x2,INPUT_FLOATINGINPUT,/* 0x0 */INPUT_PULLDOWN0x3,OUTPUT_OPENDRAIN0x4,/* ← 2.12 是 0x5 */OUTPUT_OPEN_DRAINOUTPUT_OPENDRAIN,INPUT_ANALOG0x5,/* ← 2.12 是 0x4 */}PinMode;串口模式从uint8_t类型变成了uint16_t数值也发生变化。旧代码里没直接写数值就没太大影响不过如果旧代码里把配置常量用uint8_t存储过不改代码的话就装不下新的uint16_t常量了。// V2// Define config for Serial.begin(baud, config);// below configs are not supported by STM32//#define SERIAL_5N1 0x00//#define SERIAL_5N2 0x08// ...//#define SERIAL_6N2 0x0A#defineSERIAL_8N10x06#defineSERIAL_8N20x0E// ...#defineSERIAL_8O20x3E// V3#defineSERIAL_5N1(SERIAL_STOP_BIT_1|SERIAL_PARITY_NONE|SERIAL_DATA_5)#defineSERIAL_6N1(SERIAL_STOP_BIT_1|SERIAL_PARITY_NONE|SERIAL_DATA_6)// ...#defineSERIAL_8S2(SERIAL_STOP_BIT_2|SERIAL_PARITY_SPACE|SERIAL_DATA_8)4、浮点转字符串打印性能退化V2 版本Print.h里提供的printFloat是个函数模板templateclassTsize_tprintFloat(T,uint8_t){/* .. */}如果程序里只需要打印单精度浮点float模板就只会实例化成一份printfFloat(float, uint8_t)但是如果一份固件里同时用了打印float和double就只能再多实例化一份双精度版本printFloat(double, uint8_t)两个版本并存显然这样会导致代码体积膨胀。可能是出于这种考虑V3 版本没有这个模板只提供一份双精度版实现哪怕只用单精度浮点float也要转换成double再打印。这样最大的问题是在STM32F4 之类有单精度硬件浮点单元FPU 的芯片上旧的代码生成的单精度版本可以用硬件浮点处理速度更快代码体积更小如果强行用双精度那就只能像没有FPU 一样用软件处理浮点导致性能退化。至于M0 系列的芯片本来就没有硬件FPU旧代码可能会分别实例化出一份单精度和双精度的实现导致体积膨胀所以换成用V3 以后可能会省一点空间。最佳方案可能是编译时检测目标芯片是否配置了FPU然后再决定要不要提供一份有硬件加速的单精度实现。5、String 类从双精度浮点double 构造时要吃掉大量栈空间String(float/double)构造函数里的临时缓冲从固定char buf[33]改成了按FLT/DBL_MAX_10_EXP算出来的大小(api/String.h:61-62定义FLT_MAX_DECIMAL_PLACES 10、DBL_MAX_DECIMAL_PLACES 10):// String.cppString::String(doublevalue,unsignedchardecimalPlaces){staticsize_tconstDOUBLE_BUF_SIZEDBL_MAX_10_EXPDBL_MAX_DECIMAL_PLACES111;// 308 10 3 321charbuf[DOUBLE_BUF_SIZE];// 2.12 这里是 char buf[33];...}所以输入double时缓冲区尺寸从33 字节膨胀到了321 字节这还是挺可观的尤其是对那些内存只有2K、4K 的M0 芯片而言。不过String里与浮点转换有关的函数用了两种实现第一种就是这个构造函数另外还有String::concat(double)这个函数把double转换成字符串并拼接到已有的String对象后面它的缓冲区没膨胀。所以doubled3.14;String s{d};// 一下吃掉321 字节缓冲区空间String ss;ssd;// 调用concat 拼接正常对策PlatformIO 锁定库版本可以在platformio.ini锁定库版本到V2.12如下[env:app] platform platformio/ststm3219.7.1 ; platform 版本也要锁 platform_packages platformio/framework-arduinoststm324.21200.0 ; 锁定stm32duino 框架版本 ; 还有这两个库最新版会检测stm32duino 框架版本V3 以下不能编译所以也要锁定到V3 之前的版本 lib_deps stm32duino/STM32duino Low Power 1.5.0 stm32duino/STM32duino RTC 1.9.0总结可以看出来官方这套core api 还相当不成熟官方源码里还留着一堆TODO 和问号比如在HardwareSerial.h里 名为XXX 的某人问道// XXX: Those constants should be defined as const int / enums?// XXX: shall we use namespaces too?#defineSERIAL_PARITY_EVEN(0x1ul)#defineSERIAL_PARITY_ODD(0x2ul)#defineSERIAL_PARITY_NONE(0x3ul)#defineSERIAL_PARITY_MARK(0x4ul)你问我我问谁。总之这套core api 并没有增加什么功能收益我只看到了一堆兼容性隐患和草率的设计决策。而且官方repo 里攒了99 个issue但是上一次提交更新已经是去年的事了。前途堪忧怕不是已经死了。所以还是那句话目前不推荐使用V3 版stm32duino除非st 接手core api 然后积极更新但那也要看他们什么时候能发V3.0.1没准直接反悔回退了。