【第28期】查表法(Look-Up Table, LUT)与空间换时间

在嵌入式领域,我们经常面临一个资源不对等的矛盾:Flash 空间很大(通常几百 KB),但 CPU 算力很弱(几十 MHz)

如果你在单片机里写了 y = sin(x) 或者 temp = log(r), 编译器会生成几百行汇编代码来做泰勒级数展开。你的主循环周期瞬间就会被拉长。

查表法 (Look-Up Table, LUT) 就是解决这个问题的终极武器。

核心思想“与其现场算,不如提前抄答案。” 利用廉价的存储空间(Flash)来存储预先计算好的结果,换取 CPU 的极速响应。

1. 场景一:三角函数与波形生成

假设你要做一个 SPWM 逆变器或者步进电机 S 型加减速,需要频繁计算 sin(x)

笨办法:实时计算

#include <math.h> // 引入了庞大的数学库
float val = sin(angle * 3.14 / 180.0); // 浮点运算,耗时几十微秒,且需要 FPU 支持

查表法: 我们把 0~90 度的正弦值放大 1024 倍(定点化,避免浮点运算),提前算好存入数组。

// 放在 Flash (.rodata) 中,不占 RAM
const int16_t SinTable[91] = { 
    0, 17, 35, 53, ... 1024 // 对应 sin(0) ~ sin(90) * 1024
};

int16_t Fast_Sin(uint8_t angle) {
    // 利用对称性,只需存 0-90 度,涵盖 0-360 度
    if (angle <= 90) return SinTable[angle];
    if (angle <= 180) return SinTable[180 - angle];
    if (angle <= 270) return -SinTable[angle - 180];
    return -SinTable[360 - angle];
}

收益:运算时间从 ~50us 降到 ~0.1us (几次内存读取)。

2. 场景二:NTC 热敏传感器 (非线性映射)

NTC 电阻随温度变化的曲线是非线性的,对应的 Steinhart-Hart 方程包含自然对数 ln,单片机算起来非常吃力。

查表策略: ADC 读到的电压值(0~4095)与温度是一一对应的。我们可以生成一张 “ADC值 -> 温度” 的映射表。

问题:如果做一个 int TempTable[4096] 的全表,太占空间了(4096 * 2 = 8KB)。如果 Flash 只有 32KB,这就很奢侈。

优化策略:分段查表 + 线性插值 (Linear Interpolation)

我们不需要存每一个 ADC 值,我们每隔 64 个点存一个温度数据。

// 假设 ADC 步长为 64
// Table[0] 对应 ADC=0 的温度
// Table[1] 对应 ADC=64 的温度...
const int16_t NTC_Table[] = { -40, -10, 25, 60, ... }; 

int16_t Get_Temp(uint16_t adc_val) {
    uint8_t index = adc_val / 64;       // 算出在哪两个点之间 (整数部分)
    uint8_t remainder = adc_val % 64;   // 算出偏移量 (小数部分)
    
    int16_t y0 = NTC_Table[index];      // 左边的温度
    int16_t y1 = NTC_Table[index + 1];  // 右边的温度

    // 核心算法:线性插值 (y = y0 + slope * dx)
    // 假设两点之间是直线,算出中间值
    return y0 + (y1 - y0) * remainder / 64;
}

收益

  • 空间:表的大小缩小了 64 倍(仅需 100 多字节)。

  • 精度:虽然略有误差,但对于测温来说通常足够(误差 < 0.5度)。

  • 速度:依然只有简单的加减乘除,没有 log


3. 场景三:状态压缩与位操作

查表法不仅能查数值,还能查“逻辑”。

案例:流水灯花样。 你需要控制 8 个 LED 亮灭,有很多种花样模式。 与其写一堆 if (mode == 1) LED = 0x55;,不如直接查表:

const uint8_t LED_Patterns[] = {
    0x01, 0x02, 0x04, 0x08, // 流水左移
    0x10, 0x20, 0x40, 0x80,
    0x81, 0x42, 0x24, 0x18, // 两边向中间聚合
    0xFF, 0x00, 0xFF, 0x00  // 闪烁
};

void LED_Task() {
    static int i = 0;
    PORTA = LED_Patterns[i]; // 直接输出,毫无逻辑负担
    i = (i + 1) % sizeof(LED_Patterns);
}

这其实就是一种极简的微指令 (Microcode) 思想。


4. 查表法的工程原则

  1. 数据必须 const:一定要加 const 关键字,确保数组存放在 Flash (.rodata) 中。如果不加,启动代码会把它们搬运到 RAM (.data),瞬间撑爆你本来就不富裕的 RAM。

  2. 越界保护:查表意味着用输入作为索引。如果输入是外部传来的(比如串口数据),必须先检查是否超过了数组下标,否则会读取到随机乱码甚至引发 HardFault。

  3. 生成工具:对于大的表格(如 CRC 表、正弦表),不要手写。用 Python 脚本或 Excel 生成 CSV,然后复制进 C 代码中。


本章关键知识点

  1. 本质:用预处理(编译时计算)代替运行时计算。

  2. 权衡:如果计算太复杂(三角、对数、除法),或者逻辑太繁琐(花样控制),请优先考虑查表。

  3. 插值:是平衡表格大小和精度的关键技巧。

/***************************************************
 * 本文为作者《嵌入式开发基础与工程实践》系列文章之一。
 * 关注即可订阅后续内容更新,翻阅往期信息,采用异步推送机制,无需主动轮询。
 * 转发本文可视为一次网络广播,有助于更多节点接收该信息。
 ***************************************************/

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值