RIOT SAUL 指南:使用传感器/执行器抽象层统一读取温度等物理数据
RIOT SAUL 指南使用传感器/执行器抽象层统一读取温度等物理数据【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT本文基于 RIOT 官方 C 语言教程doc/guides/c_tutorials/saul.md与其配套示例examples/guides/saul展开系统讲解 RIOT 的 SAUL[S]ensor [A]ctuator [U]ber [L]ayer传感器/执行器抽象层体系从工程 Makefile 配置、头文件引入到通过 SAUL 注册表查找温度传感器、读取并打印物理数据最终跑通make flash/make term全流程。读完本文你将掌握用统一 API 访问不同硬件传感器的方法并能直接对照仓库源码理解 SAUL 注册表与phydat_t数据结构的底层工作原理。SAUL 是什么为什么需要一层抽象在上一章教程中我们学会了直接操作 GPIO 与硬件打交道。但不同板卡上传感器/执行器的可用情况差异很大有的板卡带温度传感器有的带加速度计有的什么都没有。如果每次换一块板子都要重写驱动调用逻辑代码的可维护性会变得很差。RIOT 的解决方案就是SAULSensors/Actuators Abstraction Layer。从源码注释看它的定位非常明确drivers/include/saul.hSAUL is a generic actuator/sensor interface in RIOT. Its purpose is to enable unified interaction with a wide range of sensors and actuators through a set of defined access functions and a common data structure.即通过一组统一定义的访问函数和一个公共数据结构让上层应用以完全相同的方式访问五花八门的传感器与执行器无论底层是 I²C、SPI 还是 GPIO 模拟接口。SAUL 体系的三个关键设计点驱动必须注册每个实现 SAUL 接口的设备驱动都要把自己注册到中心的SAUL 注册表registry中。设备从此可以被查找、列出和访问。设备具备名字 类型每个设备暴露一个名称和一种设备类别class/type可用于自动化搜索与匹配——例如自动把光照传感器与 RGB LED 的颜色关联起来。自动初始化 统一 shell 命令通过auto_init机制自动初始化预配置的传感器/执行器并通过一个统一的 shell 命令访问所有可用设备见 sys/shell/cmds/saul_reg.c。一个值得注意的约束drivers/include/saul.hSAUL 驱动依赖线程上下文读取时往往会有短暂阻塞因此不能在中断上下文中发起 SAUL 注册表的请求。第一步在 Makefile 中启用 SAUL 模块SAUL 是一个可选模块使用前需要在应用的Makefile中显式声明。教程给出的最小配置如下USEMODULE saul USEMODULE saul_default USEMODULE ztimer USEMODULE ztimer_msec配套示例 examples/guides/saul/Makefile 给出了完整的工程配置逐行含义如下# name of your application APPLICATION saul_example # Change this to your board if you want to build for a different board BOARD ? arduino-feather-nrf52840-sense # This has to be the absolute path to the RIOT base directory: # If you are following the tutorial, your RIOT base directory will # most likely be something like RIOTBASE ? $(CURDIR)/RIOT # instead of this RIOTBASE ? $(CURDIR)/../../.. # Comment this out to disable code in RIOT that does safety checking # which is not needed in a production environment but helps in the # development process: DEVELHELP ? 1 # This board requires a start sleep to actually catch the printed output USEMODULE shell # Add the SAUL module to the application USEMODULE saul USEMODULE saul_default # Enable the milliseconds timer. USEMODULE ztimer USEMODULE ztimer_msec # Change this to 0 show compiler invocation lines by default: QUIET ? 1 include $(RIOTBASE)/Makefile.include各配置项的作用配置项作用APPLICATION应用名即最终生成的固件名这里为saul_exampleBOARD目标板卡教程默认arduino-feather-nrf52840-sense该板载有温度等传感器可按需更换RIOTBASERIOT 基础目录的绝对路径示例在examples/guides/下故用$(CURDIR)/../../..回退到仓库根目录DEVELHELP开启开发期安全检查越界检查等生产环境可注释掉USEMODULE shell提供交互式 shell方便通过命令行观测输出USEMODULE saulSAUL 抽象层核心模块USEMODULE saul_default启用默认设备集合与自动注册见下文依赖分析USEMODULE ztimer/ztimer_msec毫秒级定时器供ztimer_sleep使用QUIET设为 1 时隐藏编译器调用细节从构建系统的依赖解析sys/Makefile.dep可以确认saul_default的真实依赖关系ifneq (,$(filter saul_default,$(USEMODULE))) DEFAULT_MODULE auto_init_saul DEFAULT_MODULE saul_init_devs USEMODULE saul USEMODULE saul_reg endif也就是说只要加入saul_default构建系统会自动补上三样东西saulSAUL 抽象层本体saul_regSAUL 注册表模块sys/saul_reg/saul_reg.cauto_init_saul与saul_init_devs板载 SAUL 设备的自动初始化逻辑。启动时自动调用saul_init_devs()声明见 drivers/include/saul.h把板卡上预配置的每个传感器/执行器逐一登记到注册表中——这正是教程示例里直接就能搜到温度传感器的前提。第二步引入必要的头文件在main.c顶部加入如下头文件#include stdio.h #include board.h #include saul_reg.h #include ztimer.h各自的用途stdio.h提供printf/puts标准输出函数board.h板卡相关配置时钟、引脚、外设描述符等ztimer.h毫秒定时器 API用于ztimer_sleep(ZTIMER_MSEC, ...)延时saul_reg.hSAUL 注册表接口sys/include/saul_reg.h声明了saul_reg_find_type、saul_reg_read等核心函数以及saul_reg_t结构体。此时main.c的骨架如下#include stdio.h #include board.h #include saul_reg.h #include ztimer.h int main(void) { }第三步通过注册表查找传感器saul_reg_find_type按类型搜索设备要拿到一个传感器RIOT 提供了saul_reg_find_type函数——它会在注册表中搜索第一个与给定类别描述匹配的设备。本例要读取温度因此传入SAUL_SENSE_TEMP/* Define our temperature sensor */ saul_reg_t *temperature_sensor saul_reg_find_type(SAUL_SENSE_TEMP);设备类别传感器/执行器分类体系SAUL_SENSE_TEMP并不是随意起的名字。在 drivers/include/saul.h 中所有设备类别被划分为两大顶级分类enum { SAUL_CAT_UNDEF 0x00, /** device class undefined */ SAUL_CAT_ACT 0x40, /** Actuator device class */ SAUL_CAT_SENSE 0x80, /** Sensor device class */ };类别 ID 是一个 8 位无符号整数最高 2 位表示分类传感器0x80/ 执行器0x40低 6 位表示该分类内的具体类型。由此可通过掩码提取分类SAUL_CAT_MASK 0xc0SAUL_ID_MASK 0x3fdrivers/include/saul.h。SAUL_SENSE_TEMP即SAUL_CAT_SENSE | SAUL_SENSE_ID_TEMP。完整的常用类别定义包括传感器类SAUL_SENSE_*SAUL_SENSE_ANY通配、SAUL_SENSE_BTN按键、SAUL_SENSE_TEMP温度、SAUL_SENSE_HUM湿度、SAUL_SENSE_LIGHT光照、SAUL_SENSE_ACCEL加速度、SAUL_SENSE_MAG磁力、SAUL_SENSE_GYRO陀螺仪、SAUL_SENSE_COLOR颜色、SAUL_SENSE_PRESS气压、SAUL_SENSE_UV紫外线、SAUL_SENSE_DISTANCE距离、SAUL_SENSE_CO2、SAUL_SENSE_TVOC、SAUL_SENSE_PROXIMITY接近、SAUL_SENSE_CURRENT电流、SAUL_SENSE_VOLTAGE电压、SAUL_SENSE_PH、SAUL_SENSE_POWER等。执行器类SAUL_ACT_*SAUL_ACT_LED_RGBRGB LED、SAUL_ACT_SERVO舵机、SAUL_ACT_MOTOR电机、SAUL_ACT_SWITCH开关、SAUL_ACT_DIMMER调光器、SAUL_ACT_VOLTAGE电压输出、SAUL_ACT_CURRENT电流输出等。完整列表见 drivers/include/saul.h。此外还有SAUL_CLASS_ANY 0xff作为任意设备的通配符。当某个设备同时属于多个类别时它必须为每个类别各暴露一个驱动并在注册表中分别登记一条记录。检查查找结果C 没有异常必须判空saul_reg_find_type并不保证传感器一定存在板卡可能没有温度传感器或未配置。因此必须检查返回值是否为NULL/* Exit if we cant find a temperature sensor */ if (!temperature_sensor) { puts(No temperature sensor found); return 1; } else { /* * Otherwise print the name of the temperature sensor * and continue the program */ printf(Temperature sensor found: %s\n, temperature_sensor-name); }这里用到了saul_reg_t结构体的name字段。完整定义见 sys/include/saul_reg.htypedef struct saul_reg { struct saul_reg *next; /** pointer to the next device */ void *dev; /** pointer to the device descriptor */ const char *name; /** string identifier for the device */ saul_driver_t const *driver; /** the devices read callback */ } saul_reg_t;可见注册表本质上是一个单向链表next指向下一台设备dev指向底层设备描述符name是设备字符串标识driver指向该设备实现的 SAUL 驱动接口含read/write函数指针与type类别见 drivers/include/saul.h。saul_reg_find_type的实现sys/saul_reg/saul_reg.c就是遍历这个链表、逐个比对tmp-driver-typesaul_reg_t *saul_reg_find_type(uint8_t type) { saul_reg_t *tmp saul_reg; while (tmp) { if (tmp-driver-type type) { return tmp; } tmp tmp-next; } return NULL; }除了按类型查找注册表还提供saul_reg_find_name按名字、saul_reg_find_type_and_name按类型名字、saul_reg_find_nth按序号等查找函数声明见 sys/include/saul_reg.h。此时完整代码为#include stdio.h #include board.h #include saul_reg.h #include ztimer.h int main(void) { /* We sleep for 5 seconds to allow the system to initialize */ ztimer_sleep(ZTIMER_MSEC, 5000); puts(Welcome to SAUL magic!); /* Define our temperature sensor */ saul_reg_t *temperature_sensor saul_reg_find_type(SAUL_SENSE_TEMP); /* Exit if we cant find a temperature sensor */ if (!temperature_sensor) { puts(No temperature sensor found); return 1; } else { /* * Otherwise print the name of the temperature sensor * and continue the program */ printf(Temperature sensor found: %s\n, temperature_sensor-name); } }开头的ztimer_sleep(ZTIMER_MSEC, 5000)等待 5 秒让系统完成初始化auto_init 注册传感器确保此时注册表中已有设备可用。到这里程序已经能在板卡上找到温度传感器了。第四步读取传感器数据SAUL 的核心价值在这一步体现读取任何传感器都只需调用统一的saul_reg_read把结果存入一个phydat_t结构体/* We start an infinite loop to continuously read the temperature */ while (1) { /* Define a variable to store the temperature */ phydat_t temperature; /* * Read the temperature sensor * and store the result in the temperature variable * saul_reg_read returns the dimension of the data read (1 in this case) */ int dimension saul_reg_read(temperature_sensor, temperature);phydat_t物理数据的统一容器phydat_t是贯穿整个 RIOT 的物理数据容器定义见 sys/include/phydat.htypedef struct { int16_t val[PHYDAT_DIM]; /** the 3 generic dimensions of data */ uint8_t unit; /** the (physical) unit of the data */ int8_t scale; /** the scale factor, 10^*scale* */ } phydat_t;关键设计点固定 3 维PHYDAT_DIM 3sys/include/phydat.h。一维数据如温度只使用val[0]二维/三维数据如加速度计的 X/Y/Z依次填入。固定维度带来约几个字节的内存开销但换来全系统统一的数据结构。一维数据恰好覆盖 3 轴加速度计、陀螺仪、颜色传感器以及 RGB LED 这类天然三维的应用场景。unit单位物理单位枚举如UNIT_TEMP_C摄氏度、UNIT_TEMP_F、UNIT_TEMP_K、UNIT_LUX勒克斯、UNIT_G_FORCE重力加速度、UNIT_DPS度/秒、UNIT_GAUSS、UNIT_PPM、UNIT_BOOL等完整列表见 sys/include/phydat.h。scale缩放因子以 10 的幂表示即10^scale。int16_t数值 缩放因子组合出的动态范围非常可观。更重要的是RIOT 特意不用 float存储物理量——因为嵌入式系统甚至 8 位 MCU资源极度受限而多数 ADC 类传感器的精度只有 12~14 bitint16_t完全够用。saul_reg_read的返回语义sys/include/saul_reg.h返回读取到的数据维度数 [1-3]温度传感器通常返回 1返回-ENODEV设备无效返回-ENOTSUP设备不支持读操作返回-ECANCELED设备出错。其底层实现sys/saul_reg/saul_reg.c只是空指针检查后直接转发给驱动的read回调int saul_reg_read(saul_reg_t *dev, phydat_t *res) { if (dev NULL) { return -ENODEV; } return dev-driver-read(dev-dev, res); }检查读取结果C 语言没有异常机制所以读取后必须检查返回值。saul_reg_read成功时返回正数维度因此只需判断是否 0/* If the read was successful (1 Dimensions), print the temperature */ if (dimension 0) { puts(Error reading temperature sensor); return 1; }用phydat_dump打印数据并延时RIOT 提供phydat_dump函数可直接把phydat_t结构按人类可读格式含单位换算打印到控制台/* Dump the temperature to the console */ phydat_dump(temperature, dimension); /* Sleep for 1 seconds */ ztimer_sleep(ZTIMER_MSEC, 1000);phydat_dump的原型为void phydat_dump(phydat_t *data, uint8_t dim)sys/include/phydat.h第二个参数指定要打印的维度数。除phydat_dump外phydat 模块还提供了phydat_fit将 32 位整数值按比例缩放进int16_t范围并更新 scale见 sys/include/phydat.h与phydat_to_json输出 JSON 格式便于网络传输见 sys/include/phydat.h等实用工具可满足更复杂的应用场景。最终完整代码#include stdio.h #include board.h #include saul_reg.h #include ztimer.h int main(void) { /* We sleep for 5 seconds to allow the system to initialize */ ztimer_sleep(ZTIMER_MSEC, 5000); puts(Welcome to SAUL magic!); /* Define our temperature sensor */ saul_reg_t *temperature_sensor saul_reg_find_type(SAUL_SENSE_TEMP); /* Exit if we cant find a temperature sensor */ if (!temperature_sensor) { puts(No temperature sensor found); return 1; } else { /* * Otherwise print the name of the temperature sensor * and continue the program */ printf(Temperature sensor found: %s\n, temperature_sensor-name); } /* We start an infinite loop to continuously read the temperature */ while (1) { /* Define a variable to store the temperature */ phydat_t temperature; /* * Read the temperature sensor * and store the result in the temperature variable * saul_reg_read returns the dimension of the data read (1 in this case) */ int dimension saul_reg_read(temperature_sensor, temperature); /* If the read was successful (1 Dimensions), print the temperature */ if (dimension 0) { puts(Error reading temperature sensor); return 1; } /* Dump the temperature to the console */ phydat_dump(temperature, dimension); /* Sleep for 1 seconds */ ztimer_sleep(ZTIMER_MSEC, 1000); } }此代码与仓库中的 examples/guides/saul/main.c 完全一致可直接对照验证。第五步编译烧录与运行观测编译并烧录到板卡make flash打开串口终端查看输出make term一切正常的话控制台会每隔一秒打印一次温度形如2024-10-14 15:31:29,610 # Data: 24.50 °C 2024-10-14 15:31:30,134 # Data: 24.50 °C 2024-10-14 15:31:31,134 # Data: 24.50 °C 2024-10-14 15:31:32,134 # Data: 24.50 °C 2024-10-14 15:31:33,135 # Data: 24.50 °C 2024-10-14 15:31:34,135 # Data: 24.50 °C 2024-10-14 15:31:35,136 # Data: 24.50 °C可以看到phydat_dump已经自动完成了数值与单位的换算示例中为摄氏度带两位小数这正是phydat_t中scale与unit字段协作的结果。进阶用 shell 命令直接操控 SAUL 设备除了在 C 代码中调用注册表 APIRIOT 还提供了一组内置 shell 命令实现于 sys/shell/cmds/saul_reg.c需在 Makefile 中启用shell与saul_reg相关模块。示例 Makefile 中已加入USEMODULE shell因此烧录后可直接在make term的交互终端里操作列出注册表中所有设备 saul ID Class Name #0 TEMP temperaturesaul命令不带参数时列出全部设备实现见 sys/shell/cmds/saul_reg.c每行输出设备 ID、类别经saul_class_print转为字符串与名称。读取指定设备或全部设备 saul read 0 Reading from #0 (temperature|TEMP) Data: 24.50 °C saul read all向可写设备写入数据 saul write device id value 0 [value 1 [value 2]]该命令支持最多 3 个维度值对应PHYDAT_DIM常用于操控 LED、舵机等执行器若设备不支持写操作会返回-ENOTSUP并提示 device is not writable见 sys/shell/cmds/saul_reg.c。这些命令的底层同样是saul_reg_read/saul_reg_write/saul_reg_find_nth与 C 代码调用的是同一套注册表 API——这正是 SAUL统一访问思想的直接体现。总结通过本教程你已完成从零到一使用 SAUL 的完整闭环在Makefile中加入saul、saul_default、ztimer、ztimer_msec模块必要时加shell引入saul_reg.h、phydat.h经saul_reg.h间接包含、ztimer.h、board.h头文件用saul_reg_find_type(SAUL_SENSE_TEMP)在注册表中按类型查找传感器并判空用saul_reg_read读取数据到phydat_t结构检查返回维度再用phydat_dump打印用make flash/make term编译运行观察每秒一次的温度输出。更进一步你还理解了 SAUL 的底层机制注册表是saul_reg_t组成的单向链表saul_reg_find_type通过遍历比较driver-type完成查找phydat_t用3 维int16_t数值 单位 10 的幂缩放因子统一承载物理数据而saul_default模块通过构建依赖自动引入auto_init_saul与saul_init_devs让板载传感器在启动阶段自动登记进注册表。SAUL 的价值在于无论底层硬件是哪种总线、哪颗芯片应用层代码都只需要面对saul_reg_t和phydat_t两个抽象这为 RIOT 上的跨板卡应用开发环境监测、可穿戴设备、智能家居节点等打下了统一、可移植的基础。若项目运行结果与预期不符可对照 examples/guides/saul 目录下的完整工程逐行检查。【免费下载链接】RIOTRIOT - The friendly OS for IoT项目地址: https://gitcode.com/GitHub_Trending/riot/RIOT创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考