如何在多家厂商芯片上运行相同的 C 语言代码

在过去的两年里,我一直在用 Zephyr RTOS 来磨练我的技能。对我来说最有趣的部分之一是能够维护一个代码库,并为来自多个供应商的芯片编译它。我最近做了一个物联网传感器群演示,其中有不同的节点运行 Nordic、NXP 或 Espressif 芯片。当来自 DigiKey 的 Josh 和 Kelsie 看到它时,他们邀请我们在传感器会议的 DigiKey 展位上展示它。

Golioth 可以轻松地将基于微控制器的物联网设备连接到云端,使数据在云端可用,并促进远程设备管理。这个项目的研究部分集中在如何以一种与硬件无关的方式处理 Zephyr 实时操作系统应用程序代码。我将详细介绍我们是如何做到这一点的,但如果你想现在就看到代码,请查看我们发布的开源物联网天气fleet仓库

相同硬件的三个不同版本

因为这只是一个演示,所以传感器数据非常简单: 以远程配置,间隔报告温度读数。当然,一旦启动并运行,将传感器数据变成任何其他类型的数据都是很简单的。

由于近年来芯片短缺的困扰,我想展示的不仅仅是在一个芯片家族内部转移的能力,而是在完全不同的供应商之间转移的能力。我降落在 Nordic nRF9160 (这里是一个 SPARKFUN THING PLUS - NRF9160),NXP i.MX RT1062 (这里是一个 RT1060-EVKB评估板,和一个 Espressif ESP32 (这里是一个 Adafruit Huzzah32 )。

如何在所有这些不同的硬件上运行相同的代码呢? Zephyr 使用 Kconfig 和 Devicetree 以一种不会使模型变复杂的方式提取硬件。这包括引脚复用和一个智能的传感器模型,所以我们所需要做的就是为每个变体制作两个文件。以下是 NXP 板上的传感器和引脚分配:

/ {
    aliases {
        weather = &bme280;
    };
};

&lpi2c1 {
	status = "okay";

	bme280: bme280@76 {
		status = "okay";
		compatible = "bosch,bme280";
		reg = <0x76>;
	};
};

您可以看到,我选择了 i2c1 总线与 Bosch BME280 传感器。如果语法现在不完全清楚,不要担心,只需查看顶部为传感器分配别名的地方。这使得C代码可以引用一个名为weather 的传感器。它不知道这是什么类型的传感器,也不需要知道…Zephyr会处理所有这些,并为传感器读取提供一个通用的“通道”:

*// Create a pointer to our sensor*
const **struct** **device** *weather_dev = DEVICE_DT_GET(DT_ALIAS(weather));

*// Perform a sensor reading and access the temperature data*
sensor_sample_fetch(weather_dev);
sensor_channel_get(weather_dev, SENSOR_CHAN_AMBIENT_TEMP, &tem);

所以问题来了,是的,我们使用了不同的微控制器,但我们也使用了不同的传感器!

不同的传感器,同样的C代码

在芯片短缺的时代,不用重写固件就能转换完全不同的传感器,这是一种难以置信的解放。英飞凌DPS310温度传感器(此处显示的是AdafruitDPS310断口)代替Bosch BME280(如图所示是MikroE Weather Click )。

唯一改变的是Devicetree覆盖文件,它告诉构建我们使用的是哪个传感器(以及i2c总线使用哪些引脚):

/ {
    aliases {
        weather = &dps310;
    };
};

&i2c1 {
	status = "okay";
	clock-frequency = <I2C_BITRATE_STANDARD>;
	pinctrl-0 = <&i2c1_default>;

	dps310: dps310@77 {
		status = "okay";
		compatible = "infineon,dps310";
		reg = <0x77>;
	};
};

&pinctrl {
	i2c1_default: i2c1_default {
		group1 {
			psels = <NRF_PSEL(TWIM_SDA, 0, 26)>,
				<NRF_PSEL(TWIM_SCL, 0, 27)>;
		};
	};
};

注意,创建了相同的别名 (weather) ,但为该名称分配了不同的传感器。同样,Zephyr 将负责提取,为 DPS310 而不是 BME28 0启用和构建适当的驱动程序库。

看看数据滚滚而来吧

一旦构建数据看起来是一样的。唯一真正的区别是,英飞凌传感器的精度是六位数,而博世传感器只有两位数。否则,fleet 数据将被记录在 Golioth 服务器上,并准备在您希望的任何云平台上查询、可视化和使用。

该演示还包括改变整个 fleet 设置的能力,比如读取数据的频率。当然,如果你需要调整固件的工作方式,所有这些设备都可以接收无线 (OTA) 固件更新。试试吧,Golioth 的 Dev Tier 对你的前50台设备是免费的。

当数据进入服务器时,我们可以立即将其可视化。上图中,我们使用 Grafana 来绘制从三个不同电路板接收到的温度读数。

参见传感器演示

传感器会议现在正在Santa Clara 举行。整个 Golioth 团队都很高兴我们的硬件,演示是 DigiKey 展台的一部分。抬头看看物联网天气 fleet 发送实时读数!

资源

刷到这篇真的狠狠共情了!前两年芯片短缺最凶的时候,我手里的项目硬生生换了三回主控,从STM32换到GD32,最后又切到ESP32,光是底层驱动和硬件适配就重写了三遍,业务逻辑改得七零八落,那段时间真的改代码改到吐。

看到楼主用Zephyr实现跨Nordic、NXP、乐鑫三家完全不同的芯片,跑同一份C代码,真的眼前一亮!之前我对硬件抽象的理解,还停留在自己封装一层HAL接口,换芯片就补全接口实现,但是传感器一换型号、外设总线一改,还是要大动干戈。没想到Zephyr用设备树+Kconfig直接把硬件全给抽离了,给传感器起个统一别名,业务代码里根本不用管底层是BME280还是DPS310,连I2C的引脚、总线配置都全在设备树里搞定,C代码一行不用动,这简直是把硬件适配的工作量直接砍没了啊。

之前也浅试过Zephyr,但是总被设备树和Kconfig的门槛劝退,毕竟用惯了STM32CubeMX点两下就生成初始化代码,总觉得手写设备树太麻烦。但现在看楼主这个demo,真的觉得这点学习成本太值了——现在做物联网项目,芯片缺货、方案迭代太常见了,能做到换主控、换传感器都不用动业务代码,不管是应对供应链风险,还是后期维护,都太香了。

还有楼主提到的远程配置和OTA升级,也是狠狠戳中痛点。之前做过一个小批量的传感器节点项目,设备散出去之后,客户要改数据上报间隔,我愣是跑了好几个现场挨个升级,要是早用上这套方案,远程就能搞定,也不至于那么折腾。

不过也有点小疑问想问问楼主,这套方案在量产项目里踩过坑吗?比如不同厂商的芯片,驱动的兼容性稳不稳定?还有像一些资源比较小的MCU,比如M0内核的,跑Zephyr会不会很吃资源?有没有同好也用Zephyr做过量产的,也可以聊聊踩过的坑呀!

感谢分享!经历过前几年芯片短缺的开发者,看到这篇文章一定会深有感触。以前换个芯片几乎等于重写项目,而你展示的这种“Fleet”(机群)管理模式,真正让固件具备了抗风险能力。

另外,使用 Golioth 做统一的云端管理也是点睛之笔。既然硬件是异构的,通过 OTA 统一推送不同编译目标的固件包时,你是如何管理这些不同变体(Variants)的版本控制的?期待后续能分享更多关于 CI/CD 自动构建这块的心得。

这种在不同芯片中运行同一段C代码的方法还是很巧妙的,在看到标题后,看帖子内容前,我也设想了一下遇到这样的应用场景会如何处理。我的方法与贴中方法是不同的,我的设想是,先将每一种芯片的同一功能的C代码跑通后用预定义的方式#ifndef…#define#endif的方式来写。选用某个片子,就#define…对于的选项,其实这样还不是同一段代码,还是有区别。总结来说还是不如帖子中的方法巧妙。从帖子中,我也学到一招。

从我的角度上看,只要能做好硬件适配层的接口定义和维护,基本上就能做到适配不同芯片了(顶多业务层根据不同芯片的某些模块的小特性微调一下参数)。在这块zephyr和rtthread都做得还不错。其他的,目前接触到的都差点意思。

感谢楼主的分享,楼主主要是使用Zephyr RTOS设备树的概念进行不同传感器,不同MCU之间模块的重用,看着楼主是做的非常不错的。
关于如何在不同厂家芯片上运行相同的代码一般都是基于分层思想实现,具体如下
首先呢,与传感器或者MCU外设相关的配置(时钟、引脚、中断等)放在相关文件中。
其次呢,传感器相关的驱动函数放在相关文件中(如,不同厂家的EEPROM/传感器的读写数据函数);
最后,就调用相关驱动函数,也就是应用函数放在一个文件中。
上述方案就是典型的底层/驱动/应用层分层,这样既能保证代码的整洁,又能使得在不同MCU或者芯片之间能够基本做到无缝修改,叫做拿来即可使用。

你提的问题非常关键,IoT firmware engineering 的核心问题。

OTA 推送不同编译目标的固件包时,如何管理这些 Variants?

因为 IoT 真实情况是:

一个 fleet 里可能有:

设备 MCU
Node1 nRF9160
Node2 ESP32
Node3 i.MX RT1062

你提到的 CI/CD 自动构建其实才是最重要的。OTA 本身并不负责管理不同编译目标(variants),真正关键的是 构建阶段和发布流程(CI/CD) 。

推荐下面infineon FOTA应用笔记:

在 Infineon FOTA 架构中:

  • OTA系统只负责 分发和升级镜像
  • Variant 管理由 CI/CD 构建流程生成多个 firmware artifact
  • OTA server 根据 设备 metadata 匹配对应固件 并下发。

希望对你有帮助

这个类似的问题我们现在也在遇到。针对不同的芯片,针对不同的硬件设计,引用不同的外设模块等。

我开始也计划采用类似楼主的方式,使用类似设备树的配置文件来实现。不过后来放弃了。

主要还是考虑我们团队的人员水平。他们的接受能力对于新的方案可能需要一定的消化时间,另外,我也发现如果做成统一方案,对于一些器件很难发挥他的最大作用。也导致了性能高,功能全的芯片方案需要均衡能力到普通的芯片。

所以,现在还是以分层设计为基础,重新依据硬件编写底层驱动的方案。稳定,可靠!也好写KPI!嘿嘿

谢谢楼主的分享,你基于Zephyr RTOS的实战极具价值,单一代码库适配多厂商芯片、不同传感器,依托Devicetree机制实现硬件与代码解耦,既破解了芯片短缺下的适配痛点,其与Golioth的协同模式,也为运营商基于FTTO/FTTR-B拓展中小企业物联网服务提供了优质技术参考,实操性与借鉴意义突出。