LInterp.h是一个预处理器脚本,用于使用 Arduino IDE(或任何其他)C 编译器声明和初始化大型插值/翻译/查找数组。它声明一个数组并从一组提供的映射坐标自动生成其元素,然后消失,不留下任何代码、数据、定义、类或其他任何东西。这对于 RAM 有限的 Arduino 板来说是理想的,因为数组可以在PROGMEM
(程序空间)内存更大,并且不需要微控制器代码来生成数组。LInterp对 Arduino 板或引导加载程序一无所知,并且完全独立于硬件。它实际上解决了生成潜在的大型变换数组的问题因此编译器可以计算数组元素的值并在一次传递中初始化数组。对于最初(由作者)作为 UNIX shell 脚本编写的具有 40 年历史的回收件来说还不错,以避免使具有类似用户内存限制的 VAX 崩溃。
LInterp在 Arduino 微控制器平台中最明显的用途是为连接到模拟输入的非线性传感器或换能器生成线性变换。通过在设备产生的范围内“映射”一组模拟输入值(例如作为输入引脚上的电压),LInterp生成一个逆变换数组,其元素是用户指定的线性函数上的均匀间隔点在相同的设备范围内。如果数组元素之间的间隔被选择为大于底层设备输入比例(例如ADC范围),那么通过相邻数组元素之间的插值可以比仅使用最近索引元素更准确地转换输入值。LInterp总是在数组的末尾添加一个额外的数组元素,以将上面最后提供的纵坐标映射点与有效的数组元素绑定以进行插值(即使数组被声明为平面)。
C 语言预处理器是一个原始的文本处理代码,它通过扩展和替换标记定义直到它们评估为常量值来工作。它没有循环、变量或任何运行时上下文的概念,只能进行简单的整数运算。幸运的是,这足以允许从一组映射坐标和输入/输出缩放参数生成线性插值数组。#define
在使用#include
指令调用脚本之前,我们将这些常量作为 Arduino 草图中的语句提供给预处理器。脚本完成后,它会删除与其关联的所有定义,因此可以定义一个新集合并再次调用脚本以生成另一个数组。LInterp脚本中的所有标记都以LI_
并且在完成后将是未定义的,因此微控制器代码所需的任何数组参数定义都应在源文件的较早部分进行,并且LInterp令牌定义引用它们。声明的数组名由编译器单独保留。该#define
语句具有以下语法:
#define token value /* assigns
value
to
token
*/
或者
#define token /* asserts that
token
exists from this point onwards in the source file */
以下列表描述了控制LInterp脚本的标记。在标记表示数值的情况下,类型显示在括号中。默认值显示在方括号中。
LI_ARR
[ LInterp
] – 数组的名称。脚本完成后由编译器保留。
LI_CAL
( int
) – 阵列基础“校准”比例。
LI_INT
( int
) [1] – 的插值间隔LI_CAL
。
LI_OFS
( float
) [0] – 数组元素常量偏移值。缩放后添加(参见LI_SCL
)
LI_P
n
– 数组映射纵坐标在n =0 到n =32 的范围内。必须声明至少四个坐标。如果需要超过 32 个定义,则必须将定义添加到LInterp脚本(请参阅参考资料LInterp.h
)。
LI_RAM
– 强制在板 RAM 中声明数组,即使pgmspace.h
存在。
LI_RNG
( int
) – 映射纵坐标比例。LI_P
n
LI_SCL
( float
) [1.0] - 数组元素比例系数,=(最后一个元素值)-(第一个元素值)(见LI_OFS
)
LI_TYP
– 指定数组的整数类型,例如byte
、int
或long
。元素将由编译器计算为 (float) 并(LI_TYP)
在分配给数组之前四舍五入。
LI_VAR
[ const
] – 在 RAM 中声明数组时的常量/动态切换。定义LI_VAR
(无值)以允许在声明后写入数组。
Arduino 平台中LInterp脚本的唯一外部依赖是宏令牌。如果在调用脚本之前未包含系统头文件, LInterp将声明并初始化 RAM 中的数组为常量(默认情况下)或可修改(如果已定义)。PROGMEM
LI_VAR
要计算数组大小和元素间距,预处理器必须将纵坐标映射点值乘以和除以提供的缩放和插值间隔因子,并且它必须仅使用整数算术来执行此操作,因为它没有可用的浮点类型. 记住整数除法中的 999/1000 = 0(下溢),数组定义的大小和操作顺序对于获得正确的数组维度很重要。LInterp还在计算中将提供的常量转换为长整数,以防止整数乘法溢出。在为LInterp配置定义列表时,应注意以下数值注意事项:
纵坐标地图比例
纵坐标地图定义LI_P0
..及其比例因子应使用方便的测量派生比例,该比例允许地图值表示为数组定义所需精度的整数。LI_P
N
LI_RNG
数组基础(插值)比例
我们希望我们的数组跨越的整数比例,并定义我们的插值间隔大小,由 定义LI_CAL
。它等于纵坐标地图比例因子LI_RNG
乘以一个常数,但作为一个单独的因子提供,以允许对由仪器或数学上下文强加的数组使用现有的“自然”索引缩放。
纵坐标地图点阵列基本位置
纵坐标映射点( n > 0) 通常不直接对应于数组索引。包含(使得(int) i( n ) < (float) i( n ) < (int) i( n ) + 1)的数组元素由下式给出LI_P
n
LInterp[i]
LI_P
n
我( n ) = ( * ) / ( * )LI_P
n
LI_CAL
LI_RNG
LI_INT
其中算术运算按使用长整数显示的顺序执行。
数组大小计算
使用上面的数组索引计算,数组的大小由第一个 ( LI_P0
) 和最后一个 ( ) 映射点给出,即 (# of elements) = i( N ) – i( 0 ) + 1。同样,算术运算符在没有事先简化或分组的情况下按所示顺序应用。显示的额外元素是添加的最终数组元素,边界在上面。LI_P
N
LI_P
N
足够的纵坐标映射
如果规则间隔的纵坐标图包括连续纵坐标之间的映射函数的拐点(峰、谷或鞍点),则生成数组元素的线性内插器无法获得此信息。增加映射点频率以解决此类特征。
不规则间距纵坐标映射集
纵坐标映射集 { } 表示被转换的映射函数的规则间隔。映射点 (P I , S I )的不规则间隔映射集 {P n , S n }也是正则集,因此定义= P I * ( S I – S i-1 ) 和 S av为平均值集合内的间隔 (S I – S i-1 ) 的值产生一个规则间隔的映射,其中 ( S 0 – S (-1) ) = S av和= [P n scale] * S av 。请记住,区间和 SLI_P
n
LI_P
i
LI_RNG
av必须表示为LInterp的整数。
插值范围限制
LInterp脚本包含足够的插值元素声明,以在连续提供的映射纵坐标之间提供多达 32 个插值数组元素。尝试声明超过此值将产生致命的编译错误和“超出插值限制”消息。声明这么多插值的尝试通常表示数组定义中的缩放错误或映射函数的缓慢增加(低梯度)间隔(例如图形的 P8 到 P9)。由于在数组中使用许多插值会降低该纵坐标间隔中变换的精度,因此最好在此处声明更多纵坐标映射点(请参阅上面关于不规则映射集的说明)或增加插值间隔LI_INT
. 如果认为绝对有必要增加LInterp脚本中的插值声明的数量,请按照LInterp.h
文件中显示的说明进行操作。
坐标映射范围限制
LInterp脚本包括在多达 32 个纵坐标映射点之间进行插值的定义。如果需要更多,该LInterp.h
文件包含有关如何添加更多映射点定义的说明。请注意,文件中有几个位置需要这些添加,不要与插值范围声明混淆(见上文)。
访问LInterp生成的数组取决于它的声明位置。如果它已在 RAM 中声明,则它可以像使用指针 ( *(array+index)
) 或下标 ( array[index]
) 表示法的任何其他 C 数组声明一样访问。如果数组在 RAM 中声明并LI_VAR
定义了令牌,则数组元素可能会被微控制器代码更改。如果数组是在程序空间内存中声明的PROGMEM
(pgmspace.h
这些函数具有一般形式
(a
rray_element_type
) pgm_read_
array_element_type
(int
pos
);
其中array_element_type是标准数值类型,例如byte
、或,并且是程序空间存储器中要读取的位置。要访问LInterp生成的程序空间数组中的第i个元素,我们提供用加i指定的数组名称for 。为了将来的可移植性和代码可读性,将函数调用嵌入宏定义中是谨慎的,例如int
float
long
pos
LI_ARR
pos
#define MyArray( i ) pgm_read_float( LInterp + i )
其中使用了默认数组名称LInterp
,并且在这种情况下数组元素类型为浮点数。MyArray()
宏可以像代码中的函数调用一样使用,其参数计算为数组元素索引。有关pgmspace.h
函数的更多信息,请参阅www.arduino.cc上的PROGMEM
库参考。
如果LInterp生成的平移数组是平坦的(即LI_INT
= 1),因此映射函数的每个值都有一个数组元素,则上述宏可以直接使用映射函数的边界检查值作为数组索引. 如果LInterp数组已指定插值间隔(即LI_INT
> 1),因此必须在数组值之间插值映射函数值,则必须使用插值函数来获得正确的变换值。例如:
float DeviceToLin ( int dev_value ) {
/* lower bound check */
if ( dev_value < ArrOffset ) dev_value = ArrOffset;
/*upper bound check */
else if ( dev_value > ArrLimit ) dev_value = ArrLimit;
dev_value -= ArrOffset;
int index = dev_value / ArrInterp;
/* lower interpolation bound */
float lwr_bound = MyArray ( index );
/*upper interpolation bound */
float upr_bound = MyArray ( index + 1 );
return( lwr_bound + ( upr_bound – lwr_bound ) * ( dev_value % ArrInterp ) / ArrInterp );
}
withArrInterp
等于LI_INT
用于创建数组的插值间隔,dev_value
具有与 相同的比例LI_CAL
,是对应于单位ArrLimit
的数组结束位置,是对应于单位 ( not )的数组偏移量(如果有)。LInterp数组生成器确保最后提供的纵坐标映射点以有效数组元素为界。请记住在涉及数组边界的任何计算中使用长整数,并检查编译器的整数溢出警告。LI_P
N
LI_CAL
ArrOffset
LI_P0
LI_CAL
LI_OFS
LI_P
N
由于 Arduino 环境中最常见的LInterp应用程序是模拟设备输出转换,LinDev.h
因此提供了一个大纲头文件,用于针对特定设备定义进行定制。这将纵坐标图、LInterp定义、设备线性变换函数和输出函数封装在与 Arduino 项目草图文件分开的单个头文件中,从而有效地虚拟化设备。以下示例显示了将此头文件用作独立的 Arduino 草图。
例如,我们考虑使用LInterp来线性化连接到(任何)Arduino 板的模拟输入的非线性模拟设备的部分输出范围。对数电位器(或对数电位器)是一种可变电阻器,它产生的电阻随着扫描旋转角的增加而呈指数增加。如果我们将电阻的一端接地,另一端连接到我们的模拟参考电压电源Vref (来自单独供电的 Arduino 板),则扫描端子将产生非线性电压增加,随着电位器主轴角度的增加,锅从零旋转到Vref在完全旋转。使用一个 100K 电位器来限制从模拟参考电源汲取的电流,将电位器固定在一张纸板上并添加一个带有方向指针的旋钮,我们可以在其整个旋转范围(通常为 270度,或 27 度间隔)。如果我们随后在每个标记位置测量电位器电压,我们就有了电位器的常规纵坐标映射集。然后,我们可以将电位器扫描终端连接到 Arduino 板的 A0 模拟输入,并使用VrefanalogRead()
除以特定 Arduino 板的 ADC 电平中的模拟通道全宽来读取电位器电压。
现在,假设我们想要一个 Arduino 代码函数,它以 0 到 10 之间的实数形式返回电位器方向角。我们可以将电位器电压analogRead()
按比例缩放(10 除以模拟参考电压),但返回值胜出'不匹配我们在锅周围标记的等间隔刻度,因为锅不是线性的。我们需要使用一个变换函数“线性化”电位器输出。为了我们的示例,我们还指定(出于某种原因)我们希望罐变换在刻度上的位置 3 和 7 之间是线性的,但在此范围之外保持非线性(但与其平滑连续)。让我们也忽略这样一个事实,即我们知道底池是对数的(因此我们可以使用数学函数对其进行线性化),并且需要在四个标记的底池位置之间获得更好的精度,而不仅仅是它们之间的线性近似。如果我们还测量刻度上每个电位器位置之间的电位器电压,我们总共会得到九个映射点来指定LI_P0
我们LI_P8
的变换数组。由于这些必须是整数,因此我们指定它们并LI_RNG
以毫伏为单位,后者等于模拟参考电压Vref. 所有对数罐应生成示例草图中所示的相同图,因为它们的指数电阻曲线符合对数罐对数的 ANSI 标准。
我们现在根据底池产生的值范围选择我们的变换数组大小,作为 ADC 电平接收。查看纵坐标图,我们所需的线性电位器范围涵盖大约 444 个 ADC 电平,如果声明为“平面”数组,则将占用 1780 个字节。如果我们采用LogPotRes
(比如说)5 个 ADC 级别的插值大小,我们可以将其减少到 356 字节。这是明智的,因为我们的纵坐标图太粗糙而无法有效使用 (100 / 444 =) 0.23% 的分辨率精度。因此,我们设置LI_INT
为ADC 电平LogPotRes
中LI_CAL
的模拟通道全宽。最后,我们将数组比例设置LI_SCL
为 10,匹配我们的旋钮比例,LI_OFS
设置为 3,作为我们指定数组开始的所有数组元素值的旋钮比例偏移量LI_P0
. 我们还将保留LI_P0
and的副本LI_P8
(以 mV 为单位,作为长整数)以在变换函数中定位我们的线性罐部分的开始和结束。我们现在使用指令调用LInterp脚本#include
,之后我们的数组被分配、初始化并且对我们的代码可见,并且所有LI_
定义都被删除。
使用连接到 Arduino 板的串行端口,我们现在可以从setup()
函数中检查声明的数组,就好像它是程序代码中的常规数组声明一样。请注意,第一个元素等于我们对 的定义LI_OFS
,而最后一个元素(恰好)大于LI_OFS + LI_SCL
。该函数ReadPot()
对电位器模拟通道的多个读数取平均值,并对电位器电压进行边界检查。如果这超出了我们的线性变换范围,则电压仅根据相关的旋钮间隔范围进行缩放并返回;否则根据我们的DeviceToLin()
函数示例对其进行数组转换。启动该loop()
功能后,旋钮位置应在 3 到 7 范围内正确报告,并且大约在此范围之外。
ovf
,inf
以及nan
在数组末尾附近返回的值
将模拟设备电平缩放为数组索引时,请注意与原始纵坐标映射参考电压的差异。如果 ADC 或模拟设备参考电压相对于映射参考电压发生漂移,则直接缩放到阵列索引的 ADC 电平可能会超过阵列的末端。出于这个原因,为您的模拟设备和 Arduino 板 ADC(通过AREF引脚)使用公共外部参考电压源始终是一个好主意。如果设备电压漂移是您的 Arduino 应用程序中不可避免的限制,请将模拟通道用作设备电压监视器,并在您的变换阵列索引计算中对其进行缩放。同样,假设标称值为LI_RNG
当使用设备派生的数组索引访问时,而不是精确测量的值可能会导致数组太短。(技术说明:请记住,ADC 在满量程时总是返回比 Vref 小 1LSB 。)
数组声明产生一个数组元素 (= ) LI_OFS
选择导致整数除法下溢错误的缩放范围LI_RNG
会使LI_CAL
整个数组消失。(请参阅纵坐标缩放和整数算术)
数组边界处的缺失/重叠值
当使用如上例中在设备范围的一部分上声明的平移/插值数组时,必须注意数组的边界是如何由转换函数代码确定的。这应该使用相同的整数值(如 long ints)来完成,该整数值用于定义相对于LI_CAL
而不是边界检查计算的数组索引的数组,这可能会引入截断错误。
LInterp脚本可以被定制以在预处理器的有限能力所施加的严重限制内产生一个编码非线性输出函数的输出数组。这可以使用以下形式的宏定义来实现
#define LI_OFS( curr_map_pt, next_map_pt, element ) ( /*... arithmetic operations */ )
#define LI_SCL( curr_map_pt, next_map_pt, element ) ( /*... arithmetic operations */ )
并用这些确切的语句替换(一个或两个)LI_OFS
和LI_SCL
在文件顶部的元素生成器中:LInterp.h
LI_OFS( LI_LEV, LI_NXT, LI_ELT )
LI_SCL( LI_LEV, LI_NXT, LI_ELT )
以及将数组声明初始化器列表中显示的第一个元素更改为LI_OFS(0, 1, 0)
(或硬编码的#define 数字常量)如果LI_OFS
更改为宏。数字标记(不是变量)LI_LEV
并LI_NXT
指定和映射条目之间的数字标记为每个插值区间(如果有)定义,从 1 开始,以适合范围内的多个完整区间结束。令牌总是等于+ 1 并且是必要的,因为预处理器不能将 1 加到令牌上。请注意,上面显示的第一个数组元素是一种特殊情况,其唯一元素值为 0,可以通过条件测试来检测和处理,例如LI_P
n
LI_P(
n
+1)
LI_ELT
LI_NXT
LI_LEV
( ( element== 0 )? .. : .. )
在宏中使用三元运算符。这三个数字标记是LInterp脚本中预处理器唯一可用的上下文信息。
幸运的是,LI_OFS
andLI_SCL
宏仅在编译过程中进行评估,其中传递的数字标记评估为整数,并且允许浮点算术运算以及类型转换和强制转换。这提供了仅在这三个“状态”标记的上下文中计算输出函数的有限能力。映射点分布现在成为输出函数的域,由于每个映射间隔的插值分配将根据其范围而变化,这一事实变得复杂。鉴于元素生成器仍将计算LI_SCL
与插值数组元素相乘的线性小数值,输出函数只需定义为LI_LEV
如果映射点之间的线性近似足以满足输出函数的精度。作为一个稍微简单的例子,抛物线输出函数可以定义为
#define LI_SCL(p,q,r) (
scale_constant
* p * p )
并留下LI_OFS
一个常量定义。可以是一个实数,作为另一个语句的标记提供。输出函数宏定义的一个更有用的示例是更改由纵坐标映射中的上下边界条目定义的变换数组的单个区域的线性输出函数:scale_constant
#define
#define LI_OFS(p,q,r) ( (p < LWR) * OFS1 + (p >= LWR && p < UPR ) * OFS2 + ( p >= UPR ) * OFS3 )
#define LI_SCL(p,q,r) ( ( p < LWR || p >= UPR ) * SCL1 + ( p >= LWR && p < UPR ) * SCL2 )
其中表达式标记是用户提供的#define 常量,选择偏移量以使三个线性部分在纵坐标映射条目处连续,编号为和。然后可以使用相同的数组仅基于输入索引透明地生成不同的输出函数,而不是使用三个数组和编码输入边界测试来在它们之间进行选择。OFS
i
UPR
LWR
LI_ELT
如果事先不知道其范围,宏插值元素编号标记(对应于)在某种程度上是无用的。这可以从LI_S()
宏中获得并用于为当前数组元素生成一个实数小数范围因子:
elt_factor
= ( (float) element /
(long) LI_S( curr_map_pt, next_map_pt ) )
其中element
从 1 到LI_S()
第 0 个元素定义为前一个映射间隔的最后一个元素,或数组的第一个元素。
请重命名包含您的自定义设置的头文件,因为突变的LInterp变种有伪装成标准规格版本从实验室逃脱并在市民中引起恐慌的习惯。特别是,请不要因有关预处理器脚本的问题而惹恼 Arduino 支持人员或论坛版主,这是一种神秘且正确失传的(黑色)艺术。可悲的是,您会在 GitHub 或 StackOverflow 等 C 编程论坛(后者以错误的预处理器脚本编写的常见结果命名)中找到充足的帮助。
LInterp通过许多硬件平台的进化过程进入 Arduino 环境,从 Beowulf 集群到 GPU 板,无疑已经被重新发明了很多次。LInterp 之类的脚本很可能是C 预处理器的 ANSI 标准对其功能保持如此高度限制的历史原因。提前向任何认为他们的推导值得赞扬的人道歉;但是作者证明所提供的LInterp脚本和相关代码完全是他们自己的作品,没有任何版权责任(并且有证据,可能在磁带上)。它本着免费开源公共分发的“copyLeft”精神提供。作者对其使用的任何结果不承担任何责任。
快乐插值,
dxb
(古代码)
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
全部0条评论
快来发表一下你的评论吧 !