1. 项目概述

Gauge_asukiaaa 是一个面向嵌入式平台(特别是 Arduino 生态)的轻量级数值映射与插值库,其核心目标并非通用信号处理,而是 为模拟仪表盘(gauge)类人机界面提供高精度、低开销的输入-显示值转换服务 。该库不依赖浮点运算单元(FPU),全程采用整数算术实现,适用于 ATmega328P(Arduino Uno)、ESP32、STM32F1/F4 等资源受限的 MCU。其设计哲学是“以最小代码体积换取最大显示保真度”——在无外部校准工具的前提下,仅凭若干离散采样点即可构建非线性刻度映射关系,显著提升物理传感器读数在模拟表盘上的视觉准确性。

项目名称 Gauge_asukiaaa 中的 asukiaaa 为作者标识,符合 Arduino 社区常见命名惯例;而 Gauge 直指其核心应用场景:驱动指针式仪表、LED 环形进度条、OLED/SPI TFT 上的弧形刻度盘等需非均匀刻度渲染的 UI 元件。MIT 许可证赋予其在商业与开源项目中自由集成的权利,无专利或版权风险。

2. 核心原理:梯度驱动的分段线性插值

2.1 为什么需要梯度插值而非简单比例缩放?

传统 map() 函数仅支持线性映射:

int linear_mapped = map(raw_value, min_raw, max_raw, min_gauge, max_gauge);

该方法在以下场景严重失真:

  • 传感器非线性输出 :如热敏电阻(NTC)阻值-温度曲线呈指数衰减;
  • 机械仪表非线性响应 :指针偏转角度与驱动电流非严格正比(受游丝弹性、磁路饱和影响);
  • 人眼感知非线性 :相同物理增量在刻度密集区(如 0–10%)比稀疏区(90–100%)更易被察觉。

Gauge_asukiaaa 通过 用户定义的控制点(control points)及其梯度(gradient) 构建分段线性函数,本质是 Piecewise Linear Interpolation(PLI)的工程化实现。其数学模型为:

对于输入值 x ∈ [x_i, x_{i+1}] ,输出 y 按下式计算:
y = y_i + (x - x_i) × (y_{i+1} - y_i) / (x_{i+1} - x_i)

其中 (x_i, y_i) 为第 i 个控制点, i ∈ [0, N-2] N 为总点数。梯度 g_i = (y_{i+1} - y_i) / (x_{i+1} - x_i) 被预计算并存储为整数比(避免运行时除法),这是性能优化的关键。

2.2 控制点设计的工程实践

库要求用户至少提供 3 个控制点 N ≥ 3 ),形成两个及以上线段。典型配置示例如下:

物理量(输入) 表盘刻度(输出) 工程意义
0 0 零点校准(冷端补偿/机械归零)
512 50 中间线性区基准点
1023 100 满量程点(ADC 最大值映射)

此配置生成两段线性映射:

  • [0, 512] → [0, 50] :梯度 g₀ = 50/512 ≈ 0.0977
  • [512, 1023] → [50, 100] :梯度 g₁ = 50/511 ≈ 0.0978

若传感器在低温区响应迟钝(如 -20°C 时 ADC 值仅变化 10 点对应 5°C 温升),可在该区间密布控制点:

// 低温区高密度采样(提升分辨率)
{.x = 0,   .y = -40},   // -40°C
{.x = 12,  .y = -35},   // -35°C(Δx=12, Δy=5)
{.x = 38,  .y = -30},   // -30°C(Δx=26, Δy=5)
// ...

梯度自动适配为 5/12≈0.417 5/26≈0.192 ,确保小温差在表盘上获得足够指针偏转。

3. API 接口详解与源码逻辑

3.1 核心数据结构: GaugePoint Gauge

库定义两个关键结构体,全部位于头文件 Gauge.h 中:

// 控制点结构体:存储一对 (input, output) 坐标
struct GaugePoint {
  int16_t x;  // 输入值(如 ADC 原始读数),有符号 16 位
  int16_t y;  // 输出值(如表盘角度/百分比),有符号 16 位
};

// 仪表映射器主类
class Gauge {
private:
  const GaugePoint* points_;    // 指向控制点数组的常量指针
  uint8_t num_points_;          // 控制点总数
  int32_t* gradients_;          // 预计算梯度数组(int32_t 存储 dy*dx' 形式)
  int16_t* deltas_x_;           // 各段 Δx 数组(x_{i+1} - x_i)
  int16_t* deltas_y_;           // 各段 Δy 数组(y_{i+1} - y_i)

public:
  // 构造函数:传入控制点数组及数量
  Gauge(const GaugePoint* points, uint8_t num);

  // 主要映射函数:输入 raw_value,返回映射后值
  int16_t convert(int16_t raw_value) const;

  // 辅助函数:获取当前梯度数组(用于调试)
  const int32_t* getGradients() const { return gradients_; }
};
关键设计解析:
  • gradients_ 的存储格式 :为规避运行时除法,梯度 g_i = dy_i / dx_i 被存储为 dy_i * SCALE_FACTOR ,其中 SCALE_FACTOR 为编译期常量(默认 1000 )。实际计算时:
    y = y_i + (x - x_i) * g_i / SCALE_FACTOR
    用整数乘加替代除法,精度损失可控( SCALE_FACTOR=1000 时最大误差 < 0.1%)。
  • deltas_x_ deltas_y_ :预存各段跨度,避免每次 convert() 时重复计算 x_{i+1}-x_i ,节省 2×(N-1) 次减法。
  • 内存布局优化 :所有数组均声明为 const private 成员,编译器可将其置于 Flash( .rodata 段),RAM 占用仅 sizeof(Gauge) (约 12 字节)+ 动态分配的梯度/跨度数组(通常 < 100 字节)。

3.2 主要成员函数: convert() 的执行流程

convert(int16_t raw_value) 是库的核心,其执行逻辑高度优化,时间复杂度 O(N),空间复杂度 O(1):

int16_t Gauge::convert(int16_t raw_value) const {
  // 步骤1:边界处理(超限直接返回端点值)
  if (raw_value <= points_[0].x) return points_[0].y;
  if (raw_value >= points_[num_points_-1].x) 
    return points_[num_points_-1].y;

  // 步骤2:线性搜索定位所在区间(因 N 通常 ≤ 10,比二分查找更高效)
  uint8_t i = 0;
  while (i < num_points_-1 && raw_value > points_[i+1].x) i++;

  // 步骤3:执行分段线性插值
  int32_t dx = raw_value - points_[i].x;                    // 当前段内偏移
  int32_t dy_scaled = dx * gradients_[i];                   // dy_scaled = dx * (dy_i * SCALE)
  int16_t dy = (int16_t)(dy_scaled / SCALE_FACTOR);         // 实际 dy(整数除法)
  return points_[i].y + dy;
}
性能关键点:
  • 无浮点运算 :全程 int16_t / int32_t 运算, AVR-GCC convert() 编译后约 80–120 字节机器码,执行时间 < 20 μs(16MHz Uno)。
  • 无动态内存分配 gradients_ deltas_x_ deltas_y_ 在构造函数中由 new 分配,但用户可重载 operator new 指向静态缓冲区,彻底消除堆碎片风险。
  • 缓存友好 :连续访问 points_ gradients_ 等数组,利于 MCU 数据缓存(如 ESP32 的 IRAM)。

4. 典型应用示例与工程集成

4.1 基础用法:ADC 电压表映射

假设使用 STM32F103C8T6(ADC 12-bit,Vref=3.3V)驱动 OLED 表盘,需将 0–4095 映射到 0–100% ,但因分压电阻温漂,实测 0V→0 , 1.65V→48 , 3.3V→100

#include <Gauge.h>

// 定义控制点(按 x 升序排列!)
const GaugePoint voltage_points[] = {
  {0,     0},   // 0V → 0%
  {2048, 48},   // 1.65V (2048/4095*3.3V) → 48%
  {4095, 100}   // 3.3V → 100%
};

Gauge voltage_gauge(voltage_points, 3);

void setup() {
  Serial.begin(115200);
  // 初始化 ADC、OLED 等...
}

void loop() {
  int16_t adc_raw = analogRead(A0);           // 获取原始 ADC 值
  int16_t display_percent = voltage_gauge.convert(adc_raw); // 映射为百分比
  oled_draw_gauge(display_percent);          // 自定义绘制函数
  delay(100);
}

4.2 与 FreeRTOS 集成:多传感器并发映射

在 ESP32 上同时处理温度(NTC)、湿度(DHT22)、压力(BMP280)三路传感器,每路需独立映射:

#include <Gauge.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// 各传感器控制点(简化示例)
const GaugePoint temp_points[] = {{0, -40}, {1023, 125}}; // -40°C to 125°C
const GaugePoint humi_points[] = {{0, 0}, {1023, 100}};   // 0% to 100%
const GaugePoint pres_points[] = {{0, 300}, {1023, 1100}}; // 300hPa to 1100hPa

Gauge temp_gauge(temp_points, 2);
Gauge humi_gauge(humi_points, 2);
Gauge pres_gauge(pres_points, 2);

// 任务:周期性采集并映射
void sensor_task(void* pvParameters) {
  while(1) {
    int16_t temp_raw = read_ntc_adc();   // 假设函数
    int16_t humi_raw = read_dht_humi();  // 假设函数
    int16_t pres_raw = read_bmp_press(); // 假设函数

    // 并发映射(无共享状态,线程安全)
    int16_t temp_disp = temp_gauge.convert(temp_raw);
    int16_t humi_disp = humi_gauge.convert(humi_raw);
    int16_t pres_disp = pres_gauge.convert(pres_raw);

    // 发送至显示任务队列...
    vTaskDelay(pdMS_TO_TICKS(500));
  }
}

void app_main() {
  xTaskCreate(sensor_task, "sensor", 2048, NULL, 5, NULL);
}

4.3 HAL 库深度集成:STM32 ADC DMA + Gauge

利用 STM32 HAL 的 ADC 多通道 DMA 采集,将原始数据流实时映射:

#include "stm32f4xx_hal.h"
#include <Gauge.h>

extern ADC_HandleTypeDef hadc1;
extern DMA_HandleTypeDef hdma_adc1;

// 双通道控制点(CH1: 温度, CH2: 电流)
const GaugePoint ch1_points[] = {{0, 0}, {4095, 150}};
const GaugePoint ch2_points[] = {{0, 0}, {4095, 20}};
Gauge ch1_gauge(ch1_points, 2);
Gauge ch2_gauge(ch2_points, 2);

uint16_t adc_buffer[2]; // DMA 双通道缓冲区

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
  if (hadc->Instance == ADC1) {
    // DMA 传输完成,adc_buffer[0]=CH1, adc_buffer[1]=CH2
    int16_t temp_mapped = ch1_gauge.convert(adc_buffer[0]);
    int16_t curr_mapped = ch2_gauge.convert(adc_buffer[1]);

    // 更新 GUI 变量(如 LVGL 的 lv_bar_set_value)
    update_temperature_bar(temp_mapped);
    update_current_bar(curr_mapped);
  }
}

5. 高级配置与调优技巧

5.1 编译期参数定制

库支持通过 #define 调整关键参数,需在包含 Gauge.h 前定义:

宏定义 默认值 作用 工程建议
GAUGE_SCALE_FACTOR 1000 梯度缩放因子 提高精度选 10000 (需 int32_t 容纳 dx*dy*10000
GAUGE_MAX_POINTS 10 支持最大控制点数 根据 Flash 剩余空间调整, ATmega328P 建议 ≤ 8
GAUGE_USE_PROGMEM false 控制点数组存入 Flash AVR 平台设为 true ,节省 RAM

示例(AVR 优化):

#define GAUGE_SCALE_FACTOR 10000
#define GAUGE_MAX_POINTS 8
#define GAUGE_USE_PROGMEM true
#include <Gauge.h>

// 控制点存入 Flash
const GaugePoint points[] PROGMEM = {{0,0}, {512,50}, {1023,100}};

5.2 梯度精度与误差分析

梯度计算引入的量化误差是主要误差源。以 SCALE_FACTOR=1000 为例:

  • 真实梯度 g = 50/511 ≈ 0.097847
  • 存储梯度 g_stored = round(50/511 * 1000) = 98
  • 相对误差 |98/1000 - 50/511| / (50/511) ≈ 0.15%

降低误差策略

  • 增大 SCALE_FACTOR 10000 时误差降至 0.015% ,但 dx*g_stored 可能溢出 int32_t (需验证 max(dx)*max(g_stored) < 2^31 )。
  • 选择更优控制点 :避免 dx_i 过小(如 x_i=100, x_{i+1}=101 ),因其放大 g_i 的舍入误差。

5.3 内存占用与性能实测(Arduino Uno)

组件 占用(字节) 说明
Gauge 对象 12 const GaugePoint* (2B) + uint8_t (1B) + 3× int32_t* (6B) + padding (3B)
控制点数组(3点) 12 3 × sizeof(GaugePoint)
梯度/跨度数组(2段) 20 2 × (int32_t + int16_t + int16_t)
总计(Flash) ~200 包含代码、常量、静态数组
总计(RAM) 32 对象 + 动态数组(不含控制点)

convert() 执行时间: 16.2 μs (实测,16MHz AVR),满足 50kHz 采样率需求。

6. 故障排查与最佳实践

6.1 常见问题诊断表

现象 可能原因 解决方案
convert() 返回异常大值(如 32767 控制点 x 未按升序排列;或 raw_value 超出 points_[0].x points_[N-1].x 范围 使用 std::sort 预排序;添加 Serial.println() 打印 raw_value points_ 边界
映射结果阶梯状跳变 SCALE_FACTOR 过小导致 dy 截断;或控制点过少 增大 SCALE_FACTOR ;增加中间控制点(如每 10% 电压加一点)
编译报错 undefined reference to 'operator new' AVR 平台未启用 new 运算符 platformio.ini 添加 build_flags = -lstdc++ ;或改用静态分配(见 6.2)

6.2 静态内存分配(推荐用于裸机系统)

规避 new 依赖,将梯度数组置于全局静态区:

// 静态缓冲区(大小 = (N-1) × sizeof(int32_t))
static int32_t grad_buffer[GAUGE_MAX_POINTS-1];
static int16_t dx_buffer[GAUGE_MAX_POINTS-1];
static int16_t dy_buffer[GAUGE_MAX_POINTS-1];

class StaticGauge : public Gauge {
public:
  StaticGauge(const GaugePoint* points, uint8_t num) 
    : Gauge(points, num) {
    // 强制使用静态缓冲区(需修改 Gauge.cpp 中构造函数)
    gradients_ = grad_buffer;
    deltas_x_ = dx_buffer;
    deltas_y_ = dy_buffer;
  }
};

StaticGauge my_gauge(points, 3); // 零堆内存使用

6.3 硬件协同校准流程

  1. 物理标定 :用标准源(如精密电源、恒温槽)施加已知输入 x_i ,记录 ADC 值;
  2. 软件录入 :将 (x_i, y_i) 写入 points[] 数组;
  3. 在线验证 :串口输出 raw_value convert() 结果,对比标准值;
  4. 迭代优化 :在误差大区域插入新控制点,重新编译烧录。

此流程将硬件非线性、PCB 布线噪声、参考电压漂移等系统误差一并补偿,远超单纯软件查表。

7. 与其他映射方案对比

方案 精度 RAM 占用 Flash 占用 实时性 适用场景
map() (Arduino) 低(纯线性) 0 ~20B 极高 快速原型,线性传感器
查表法(LUT) 高(任意形状) 高(O(N)) 中(O(N)) 极高 高速应用,Flash 充足
Gauge_asukiaaa 中高(分段线性) 极低 (O(1)) (O(N)) (O(N)) 资源受限 MCU,需非线性校准
浮点多项式拟合 高(平滑曲线) 中(需 float lib) 高(系数+算法) 低(float 运算慢) 高性能 MCU,数学建模

Gauge_asukiaaa 在精度、资源、开发效率三角中取得最优平衡——它不追求理论最优,而是为嵌入式工程师提供“够用、可靠、省心”的工业级校准工具。

Logo

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

更多推荐