1. 项目概述

tiny-devices 是一个面向资源极度受限嵌入式平台(典型如 RAM < 4KB、Flash < 32KB 的 Cortex-M0+/M3/M4F 微控制器)设计的轻量级设备驱动框架。它并非传统意义上的“全功能驱动库”,而是一套 可裁剪、无依赖、零动态内存分配、纯 C99 实现 的底层外设抽象层(HAL-Lite),专为 tiny 生态系统构建—— tiny 是一个极简主义嵌入式运行时环境,强调确定性、可预测性和最小化二进制体积,常用于超低功耗传感器节点、BLE Mesh 终端、工业状态指示器等对成本与功耗极度敏感的场景。

其核心设计哲学可概括为三点:

  • Zero Overhead Abstraction :所有驱动接口在编译期完全内联或展开,无函数调用开销,无虚表,无运行时类型检查;
  • Stateless by Default :驱动实例不维护内部状态(如缓冲区、计数器、模式标志),所有状态由用户显式管理,避免隐式副作用与内存泄漏风险;
  • Hardware-Centric API :API 命名与参数设计直映硬件寄存器语义(如 spi_set_clk_divider() 而非 spi_set_baudrate() ),降低学习曲线与调试复杂度。

该框架不提供操作系统适配层(如 FreeRTOS 封装)、不包含中断服务例程(ISR)模板、不集成协议栈(如 I2C/SPI 的完整读写事务封装),而是将控制权完全交还给开发者,仅提供 原子级、可组合、可验证 的硬件操作原语。这使其天然适配裸机(Bare Metal)、CMSIS-RTOS v2、以及 tiny 自带的协程调度器 tiny_coop


2. 核心架构与设计原理

2.1 分层结构

tiny-devices 采用三层静态分层模型,各层之间通过头文件包含与宏定义实现编译期解耦:

层级 名称 职责 典型文件
L0 Hardware Abstraction Layer (HAL) 直接操作寄存器,屏蔽芯片厂商差异(如 STM32 vs NXP LPC) hal/stm32f0xx.h , hal/nrf52832.h
L1 Device Driver Core 提供通用设备操作接口(init/config/enable/disable),不依赖具体芯片 drivers/gpio.h , drivers/spi.h , drivers/uart.h
L2 Board Support Package (BSP) 板级引脚映射、时钟配置、电源管理策略 bsp/my_sensor_node.h

关键工程决策说明

  • 为何不提供 ISR 封装?
    在超低功耗应用中,中断响应时间必须严格可控。 tiny-devices 要求用户自行编写 ISR,并在其中直接调用 L1 驱动的 xxx_irq_handler() 函数(该函数为纯状态机逻辑,无阻塞、无 malloc)。此举避免了中断上下文中的隐式调度、锁竞争或堆分配失败风险。
  • 为何禁止动态内存?
    tiny 系统通常禁用 malloc/free 。所有驱动初始化函数(如 uart_init() )接受用户预分配的 struct uart_dev_s *dev 指针,该结构体仅含 4–16 字节(取决于外设),可置于 .bss 或静态数组中。例如:
static struct uart_dev_s my_uart = {0}; // 占用 8 字节
uart_init(&my_uart, UART1, 115200, UART_PARITY_NONE);

2.2 编译时配置机制

所有驱动行为均通过 C 预处理器宏控制,无运行时配置结构体。典型配置项如下表所示:

宏定义 默认值 作用 工程影响
TINY_DEV_GPIO_IRQ_ENABLE 0 启用 GPIO 中断支持(增加 ~120 字节代码) 若板载按钮需边沿触发,必须定义为 1
TINY_DEV_SPI_DMA_ENABLE 0 启用 SPI DMA 模式(依赖芯片 HAL 的 DMA 接口) STM32F0 平台不可用(无 DMA),LPC824 可启用
TINY_DEV_UART_RX_BUF_SIZE 0 UART 接收缓冲区大小(字节) 设为 0 则禁用 FIFO, uart_read() 变为轮询模式;设为 16 则启用双缓冲环形队列
TINY_DEV_I2C_TIMEOUT_MS 10 I2C 传输超时阈值(毫秒) 在 1MHz I2C 总线上,10ms 可覆盖最长 10000 字节传输

⚠️ 配置陷阱警示
TINY_DEV_UART_RX_BUF_SIZE 非零时, uart_init() 会要求传入 rx_buf rx_buf_size 参数。若传入 NULL 或尺寸不足,编译器将报错(通过 static_assert 实现),而非运行时崩溃——这是 tiny-devices “Fail Fast” 哲学的体现。


3. 主要驱动模块详解

3.1 GPIO 驱动( drivers/gpio.h

GPIO 是最基础的外设, tiny-devices 的 GPIO 驱动以 位带操作(Bit-Band) 原子寄存器写入 为核心,确保多任务/中断环境下引脚状态修改的绝对原子性。

关键 API 与参数解析
函数原型 作用 参数说明 典型用法
void gpio_init(const struct gpio_pin_s *pin, uint8_t mode) 初始化单个引脚 pin : 指向 gpio_pin_s 结构体(含 port/base_addr, pin_num, af_num); mode : GPIO_MODE_INPUT / GPIO_MODE_OUTPUT_PP / GPIO_MODE_AF_PP static const struct gpio_pin_s led_pin = {.port=GPIOA, .pin_num=5}; gpio_init(&led_pin, GPIO_MODE_OUTPUT_PP);
void gpio_write(const struct gpio_pin_s *pin, uint8_t val) 设置引脚电平 val : 0 (低)或 1 (高) gpio_write(&led_pin, 1); // 点亮 LED
uint8_t gpio_read(const struct gpio_pin_s *pin) 读取引脚电平 if (gpio_read(&btn_pin)) { ... } // 检测按键释放
void gpio_toggle(const struct gpio_pin_s *pin) 翻转引脚电平 gpio_toggle(&led_pin); // 无需读-改-写,硬件级原子操作

🔍 源码逻辑剖析 (以 STM32F0 为例):
gpio_toggle() 不使用 GPIOx->ODR ^= (1 << n) (非原子),而是利用 Cortex-M 内置的 Bit-Band Alias Region

#define BITBAND_SRAM_BASE 0x20000000
#define BITBAND_PERIPH_BASE 0x40000000
#define BITBAND_ALIAS(addr, bit) \
  ((addr & 0xF0000000) == 0x20000000 ? \
    (BITBAND_SRAM_BASE + ((addr - 0x20000000) * 32) + (bit * 4)) : \
    (BITBAND_PERIPH_BASE + ((addr - 0x40000000) * 32) + (bit * 4)))

static inline void gpio_toggle(const struct gpio_pin_s *pin) {
  volatile uint32_t *bb_addr = (volatile uint32_t*)BITBAND_ALIAS(
      (uint32_t)&pin->port->ODR, pin->pin_num);
  *bb_addr = ~(*bb_addr); // 单字节写入,硬件保证原子性
}

此实现比传统读-改-写快 3×,且在任意中断优先级下安全。

3.2 UART 驱动( drivers/uart.h

UART 驱动支持三种工作模式: 轮询(Polling) 中断接收(IRQ-RX) DMA 接收(DMA-RX) ,由 TINY_DEV_UART_RX_BUF_SIZE 宏自动选择。

模式对比与选型指南
模式 CPU 占用 实时性 适用场景 关键约束
Polling ( RX_BUF_SIZE=0 ) 高(持续查询 TXE/RXNE 标志) 差(发送/接收期间无法响应其他事件) 调试串口、单次 AT 指令交互 必须配合 uart_wait_tx_complete() 使用
IRQ-RX ( RX_BUF_SIZE>0 ) 低(仅在接收字节时进入 ISR) 优(中断延迟 < 1μs) 传感器数据流、命令行交互 需在 BSP 中定义 UART1_IRQHandler 并调用 uart_irq_handler()
DMA-RX ( DMA_ENABLE=1 ) 极低(CPU 仅在 DMA 完成中断中处理) 优(DMA 传输无 CPU 干预) 高速日志记录(如 921600bps)、固件升级 DMA 缓冲区必须 4 字节对齐,且长度为 2 的幂
核心 API 行为说明
// 初始化(根据 RX_BUF_SIZE 自动选择模式)
int uart_init(struct uart_dev_s *dev, USART_TypeDef *usart, 
               uint32_t baud, uint8_t parity);

// 发送(阻塞,直到全部字节移入移位寄存器)
int uart_write(struct uart_dev_s *dev, const uint8_t *buf, size_t len);

// 接收(非阻塞,返回实际读取字节数)
int uart_read(struct uart_dev_s *dev, uint8_t *buf, size_t len);

// 等待发送完成(Polling 模式必需,IRQ/DMA 模式可选)
void uart_wait_tx_complete(struct uart_dev_s *dev);

💡 工程实践技巧
tiny 协程环境中,常将 UART 接收封装为协程:

TINY_COOP_TASK(uart_reader) {
  static uint8_t rx_buf[32];
  while (1) {
    int n = uart_read(&my_uart, rx_buf, sizeof(rx_buf));
    if (n > 0) {
      parse_command(rx_buf, n); // 处理命令
    }
    tiny_coop_delay_ms(1); // 让出 CPU,避免忙等
  }
}

3.3 SPI 驱动( drivers/spi.h

SPI 驱动采用 主模式(Master Only) 设计,不支持从机模式(Slave),因 tiny 应用几乎均为传感器/Flash 控制器角色。其最大特色是 零拷贝双缓冲传输

双缓冲机制详解

当调用 spi_transfer() 时,驱动自动启用双缓冲(若芯片支持):

  • Buffer A:CPU 向其中写入待发送数据;
  • Buffer B:DMA 从中读取并发送,同时 CPU 可向 Buffer A 写入下一帧数据;
  • 驱动通过 SPI_CR2_TXEIE (发送缓冲区空中断)和 SPI_SR_BSY (忙标志)实现无缝切换。
// 双缓冲传输(需提前配置 TX/RX buffers)
int spi_transfer(struct spi_dev_s *dev, 
                 const uint8_t *tx_buf, uint8_t *rx_buf, size_t len);

// 纯发送(RX buffer 为 NULL,仍占用 MISO 线,但忽略接收值)
int spi_send(struct spi_dev_s *dev, const uint8_t *tx_buf, size_t len);

📌 关键参数配置
spi_init() mode 参数决定 CPOL/CPHA 组合:

mode CPOL CPHA 适用设备
0 0 0 SD Card (SPI Mode 0)
1 0 1 nRF24L01+
2 1 0 OLED SSD1306 (部分型号)
3 1 1 W25Q Flash (默认)

3.4 I2C 驱动( drivers/i2c.h

I2C 驱动严格遵循 7-bit 地址 + 无重复启动(No Repeated Start) 规范,不支持 10-bit 地址或 SMBus 扩展。其健壮性体现在三重超时防护:

  1. 总线空闲超时 :检测 SCL 拉低超过 TINY_DEV_I2C_TIMEOUT_MS ,执行总线恢复(SCL 时钟伸展 + SDA 释放);
  2. 地址应答超时 :发送地址后未收到 ACK,在第 9 个时钟周期强制终止;
  3. 数据应答超时 :每字节发送后未收到 ACK,立即返回错误。
原子化读写 API
// 写入寄存器(Write then Stop)
int i2c_write_reg(struct i2c_dev_s *dev, uint8_t addr, 
                   uint8_t reg, const uint8_t *data, size_t len);

// 读取寄存器(Start + Addr + Write Reg + Restart + Addr+Read + Stop)
int i2c_read_reg(struct i2c_dev_s *dev, uint8_t addr, 
                 uint8_t reg, uint8_t *data, size_t len);

// 连续读取(Start + Addr+Read + Stop,适用于温度传感器等)
int i2c_read_stream(struct i2c_dev_s *dev, uint8_t addr, 
                    uint8_t *data, size_t len);

⚙️ 硬件级优化
在 STM32 平台, i2c_write_reg() 利用 I2C_CR2_RELOAD 位实现寄存器地址与数据的 单次加载 ,避免多次写入 I2C_TXDR 导致的时序抖动,确保在 400kHz 速率下时序余量 > 15%。


4. 与 tiny 生态的深度集成

4.1 tiny_coop 协程调度器协同

tiny-devices 驱动本身无调度概念,但其 IRQ 模式天然适配 tiny_coop 的事件唤醒机制。典型模式为:

  1. UART ISR 中调用 uart_irq_handler() ,该函数将 dev->rx_count 原子递增;
  2. 协程中使用 tiny_coop_wait_event(&dev->rx_count, != 0) 挂起;
  3. rx_count 变化时,协程被唤醒,执行 uart_read() 消费数据。

此模式下,CPU 在无数据时完全休眠( WFI ),功耗降至 μA 级。

4.2 tiny_log 日志系统对接

所有驱动错误码(如 I2C_ERR_TIMEOUT , SPI_ERR_OVERRUN )均映射到 tiny_log LOG_LEVEL_ERROR ,并通过 TINY_LOG_DRIVER 标签输出。启用方式:

#define TINY_LOG_DRIVER 1
#include "tiny_log.h"
// 错误发生时自动输出: "[DRV] I2C: timeout on addr 0x68"

4.3 构建系统集成(CMake)

tiny-devices 提供标准 CMake 接口,支持细粒度裁剪:

# 在项目 CMakeLists.txt 中
add_subdirectory(third_party/tiny-devices)
tiny_devices_add_driver(
  TARGET my_firmware
  DRIVERS gpio uart spi
  CONFIGS TINY_DEV_UART_RX_BUF_SIZE=16
)

此命令自动链接对应驱动源文件、定义预处理器宏,并校验配置冲突(如 SPI_DMA_ENABLE=1 但目标芯片无 DMA)。


5. 典型应用场景与代码示例

5.1 低功耗环境传感器节点(STM32L071 + BME280)

#include "drivers/i2c.h"
#include "drivers/gpio.h"
#include "tiny_coop.h"

static struct i2c_dev_s bme_i2c = {0};
static struct gpio_pin_s bme_rst = {.port=GPIOA, .pin_num=0};

void bme280_init(void) {
  // 硬复位 BME280
  gpio_init(&bme_rst, GPIO_MODE_OUTPUT_PP);
  gpio_write(&bme_rst, 0);
  tiny_coop_delay_ms(2);
  gpio_write(&bme_rst, 1);
  tiny_coop_delay_ms(10);

  // 初始化 I2C(400kHz, 7-bit addr 0x76)
  i2c_init(&bme_i2c, I2C1, 400000, 0x76);
}

TINY_COOP_TASK(bme_reader) {
  uint8_t data[8];
  while (1) {
    // 读取温度/压力/湿度(寄存器 0xFA-0xFF)
    if (i2c_read_reg(&bme_i2c, 0x76, 0xFA, data, 8) == 0) {
      int32_t temp = (int32_t)(data[3] << 12) | (data[4] << 4) | (data[5] >> 4);
      TINY_LOG_INFO("Temp: %d.%02d°C", temp/100, temp%100);
    }
    tiny_coop_delay_ms(2000);
  }
}

5.2 高速 SPI Flash 编程器(NRF52840 + Winbond W25Q80)

#include "drivers/spi.h"
#include "drivers/gpio.h"

static struct spi_dev_s flash_spi = {0};
static struct gpio_pin_s flash_cs = {.port=GPIO0, .pin_num=12};

void flash_init(void) {
  gpio_init(&flash_cs, GPIO_MODE_OUTPUT_PP);
  gpio_write(&flash_cs, 1);
  spi_init(&flash_spi, SPI0, SPI_MODE_3, 20000000); // 20MHz, Mode 3
}

// 页编程(256 字节)
void flash_page_program(uint32_t addr, const uint8_t *data) {
  gpio_write(&flash_cs, 0);
  spi_send(&flash_spi, "\x02", 1); // Write Enable
  spi_send(&flash_spi, "\x06", 1);
  gpio_write(&flash_cs, 1);

  gpio_write(&flash_cs, 0);
  uint8_t cmd[4] = {0x02, (addr>>16)&0xFF, (addr>>8)&0xFF, addr&0xFF};
  spi_transfer(&flash_spi, cmd, NULL, 4);
  spi_transfer(&flash_spi, data, NULL, 256);
  gpio_write(&flash_cs, 1);
}

6. 调试与故障排查

6.1 常见问题速查表

现象 可能原因 解决方案
i2c_write_reg() 返回 -1 (超时) SDA/SCL 上拉电阻过大(>10kΩ)或缺失 检查原理图,更换为 4.7kΩ 上拉
uart_read() 始终返回 0 TINY_DEV_UART_RX_BUF_SIZE 0 ,但未调用 uart_irq_handler() 启用缓冲区或改用 uart_read_polling()
spi_transfer() 数据错位 SPI_MODE_X 与设备 datasheet 不匹配 对照设备手册的时序图,调整 mode 参数
编译报错 undefined reference to 'spi_dma_xfer' TINY_DEV_SPI_DMA_ENABLE=1 但未实现 hal/xxx_dma.c 禁用 DMA 或补全 DMA HAL 实现

6.2 硬件级调试技巧

  • SPI 信号观测 :使用 Saleae Logic 分析仪捕获 SCK/CS/MOSI ,验证 CPOL/CPHA 是否与 spi_init(mode) 一致;
  • I2C 总线扫描 :调用 i2c_scan_bus(&dev) 扫描 0x08–0x77 地址,确认设备是否在线;
  • GPIO 时序验证 :将 gpio_toggle() 插入关键路径,用示波器测量翻转周期,验证编译器优化等级(建议 -Os )。

7. 项目演进与社区实践

tiny-devices 当前版本(v0.4.2)已稳定支持 STM32F0/L0/G0、Nordic NRF52、NXP LPC824 三大平台。社区贡献的 BSP 覆盖 17 款开发板,包括 Adafruit Feather NRF52840、WeAct Studio STM32F411CEU6、Seeed Studio XIAO ESP32C3(通过 RISC-V 移植层)。

未来路线图聚焦于:

  • LPUART 超低功耗模式支持 :添加 uart_enter_lpuart_stop_mode() ,在 STOP2 模式下维持 32kHz LSE 时钟接收;
  • TrustZone 安全驱动扩展 :为 Cortex-M33 平台提供 secure_gpio_write() 等隔离 API;
  • Rust FFI 绑定生成 :通过 bindgen 自动生成 tiny_devices_sys crate,支持 Rust + tiny 混合开发。

在真实产线中,某工业振动传感器项目采用 tiny-devices 后,固件体积从 28KB(基于 CubeMX HAL)压缩至 9.3KB,待机电流从 12μA 降至 2.1μA,且通过 IEC 61000-4-2 ±8kV ESD 测试——这印证了其“精简即可靠”的工程信条。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐