ARM Cortex-M0+定点数运算实战:MLIB库移位与算术函数深度解析
2026/6/17 16:08:09 网站建设 项目流程

1. 项目概述

在嵌入式开发领域,尤其是面对ARM Cortex-M0+这类没有硬件浮点单元(FPU)的微控制器时,如何高效、精确地处理小数运算,是每个工程师都会遇到的经典难题。直接使用软件浮点库?性能开销巨大,实时性难以保证。用整数模拟?代码复杂,可读性差,且容易出错。这时候,定点数运算(Fixed-Point Arithmetic)就成了我们的“秘密武器”。它本质上是一种用整数来表示小数的方法,通过预先确定小数点的位置,将所有的浮点运算转化为整数运算和移位操作,从而在有限的硬件资源下,实现性能与精度的绝佳平衡。

我接触过不少项目,从电机驱动的FOC算法到音频信号处理,再到低功耗传感器数据滤波,定点数运算都是核心基石。而飞思卡尔(现恩智浦)为Cortex-M0+内核提供的MLIB(Math Library)库,则是将这套理论工程化、高效化的典范。它并非一个简单的函数集合,而是一套针对处理器指令集深度优化的数学工具箱。今天,我们就深入MLIB库的腹地,聚焦其最基础也最关键的移位运算算术运算函数,如MLIB_ShL,MLIB_ShR,MLIB_Sub,MLIB_VMac等。我会结合多年的踩坑经验,不仅告诉你这些函数怎么用,更会剖析它们为何这样设计,在什么场景下会出问题,以及如何避开那些手册里不会写的“暗礁”。无论你是刚开始接触定点数的新手,还是想优化现有代码的老手,相信这篇近万字的详解都能让你有所收获。

2. 定点数基础与MLIB库设计哲学

在直接上手函数之前,我们必须统一“语言”,即彻底理解Q格式定点数,并洞悉MLIB库背后的设计考量。这能让你在后续使用中,清楚地知道每一个比特的来龙去脉。

2.1 Q格式定点数:嵌入式中的“小数”语言

定点数的核心思想是“约定大于配置”。我们和编译器约定好,一个整数中的某一位是小数点。最常见的格式是Qm.n,其中m表示整数部分的位数(包括符号位),n表示小数部分的位数。对于MLIB库,它主要处理两种有符号定点数:

  1. Q1.15格式(Frac16):用16位有符号整数(int16_t)表示一个范围在[-1, 1)之间的小数。其最高位(bit 15)是符号位,接下来的15位(bit 14 到 bit 0)全部是小数位。这意味着它没有整数部分(除了符号),分辨率是 1 / 2^15 = 1/32768 ≈ 0.0000305。

    • 数值范围:-1 ≤ value < 1 (更准确地说,可表示的最小负数是-1.0,最大正数是 1 - 2^-15)。
    • 示例:0.5 用Q1.15表示就是 0.5 * 32768 = 16384 = 0x4000。-0.25 就是 -0.25 * 32768 = -8192,其二进制补码形式为 0xE000。
  2. Q1.31格式(Frac32):用32位有符号整数(int32_t)表示一个范围在[-1, 1)之间的小数。最高位(bit 31)是符号位,低31位(bit 30 到 bit 0)全是小数位。分辨率高达 1 / 2^31 ≈ 4.6566e-10。

    • 数值范围:-1 ≤ value < 1。
    • 示例:0.125 用Q1.31表示就是 0.125 * 2^31 = 268435456 = 0x10000000。

MLIB库提供的FRAC16()FRAC32()宏,就是帮我们完成这个从浮点数到定点数转换的“翻译官”。但这里有个关键点:输入给这些宏的浮点数,必须在[-1, 1)区间内,否则转换结果毫无意义,因为定点格式本身无法表示超出此范围的数。

2.2 MLIB库的设计取舍:性能、精度与安全

MLIB库的函数命名和功能设计,清晰地反映了嵌入式开发中的经典权衡:

  • _F16_F32后缀:明确区分操作数是16位还是32位定点数。这不仅仅是位宽不同,更关乎精度和动态范围。Frac32的精度远高于Frac16,但运算速度可能稍慢(在32位处理器上差异不大),且占用更多内存。在信号链中,我们常采用“Frac16输入 -> Frac32中间运算 -> Frac16输出”的模式来平衡精度和存储开销。
  • Sat后缀:这是“Saturation”(饱和)的缩写。带Sat的函数(如MLIB_ShLSat_F16)在发生溢出时,会将结果钳位(Clamp)到该数据类型能表示的最大正值(0x7FFF 对于 Frac16)或最小负值(0x8000 对于 Frac16),而不是任由其环绕(Wrap-around)。这是防止信号处理中因溢出导致灾难性失真的关键安全机制。例如,在音频处理中,一个溢出导致的环绕可能从最大音量瞬间跳变到最小音量,产生刺耳的爆破音。
  • 内联(Inline)实现:MLIB库的绝大多数函数都被声明为inline。这意味着函数调用在编译时会被直接展开为相应的汇编指令序列,完全消除了函数调用的开销(压栈、跳转、弹栈)。对于在循环中频繁调用的核心运算,这能带来显著的性能提升。这也是官方文档中提及“非ANSI-C兼容”的原因之一,因为inline关键字在早期C标准中并非总被支持。
  • “Effectivity”优先:文档中多次提到“Due to effectivity reason”。这里的“effectivity”可以理解为“效率”或“实效性”。MLIB库的终极目标是极致的运行时效率,而非严格的数学完备性或便捷性。因此,它不会在函数内部进行参数范围检查(如移位位数是否超限),也不会处理所有边界情况。它将保证输入正确的责任交给了开发者,以此换取每一个CPU时钟周期。

理解了这些,我们就能明白,使用MLIB库是一场与处理器的紧密合作。我们提供规范的数据,它回报以极致的速度。接下来,我们就进入实战环节,逐一拆解这些函数。

3. 移位运算函数深度解析

移位是定点数运算的灵魂。乘法、除法、数值缩放都离不开它。MLIB提供了方向明确且有无饱和处理的移位函数,我们需要根据场景谨慎选择。

3.1 双向移位:MLIB_ShBi_F16/F32

ShBi是“Shift Bidirectional”的缩写,即双向移位。它通过第二个参数的符号来决定移位方向。

函数原型:

Frac16 MLIB_ShBi_F16(register Frac16 f16In1, register Word16 w16In2); Frac32 MLIB_ShBi_F32(register Frac32 f32In1, register Word16 w16In2);

核心行为:

  • w16In2 > 0:逻辑左移。等价于f16In1 << w16In2,低位补0。
  • w16In2 < 0:算术右移。等价于f16In1 >> (-w16In2),高位用符号位填充。
  • w16In2 = 0:不移位,直接返回原值。

重要限制与原理:移位位数w16In2的绝对值必须小于数据位宽(Frac16是15,Frac32是31)。为什么?以Frac16左移为例,如果移位15位,那么符号位(bit15)会被移出,结果的有效符号位丢失,数值意义完全混乱。移位16位或更多,所有有效数据都会被移出,结果恒为0(对于正数)或-1(对于负数,因为算术右移补符号位),这显然不是我们想要的数学操作。MLIB为了效率,不会检查这个范围,输入超限会导致未定义行为。

示例与心算:假设f16In1 = 0x3000(Q1.15下约为0.375),w16In2 = -2

  1. 数值上,算术右移2位等于除以4。0.375 / 4 = 0.09375。
  2. 定点数上,0x3000 二进制为0011 0000 0000 0000
  3. 算术右移2位(高位补符号位0):0000 1100 0000 0000,即 0x0C00。
  4. 验证:0x0C00 = 3072 (十进制)。3072 / 32768 = 0.09375。结果正确。

无饱和处理的风险:MLIB_ShBi_F16不带饱和。左移可能导致溢出。例如,0x4000 (0.5) 左移1位变成 0x8000。在Q1.15看来,0x8000 是 -1.0,而不是预期的1.0。这就是溢出导致的符号反转,在控制系统中可能引发正反馈震荡,非常危险。

3.2 饱和双向移位:MLIB_ShBiSat_F16/F32

这是ShBi的安全版本。当左移导致溢出时,它会将结果饱和到该数据类型能表示的最大正值或最小负值。

函数原型:

Frac16 MLIB_ShBiSat_F16(register Frac16 f16In1, register Word16 w16In2); Frac32 MLIB_ShBiSat_F32(register Frac32 f32In1, register Word16 w16In2);

饱和行为详解:继续上面的危险例子:MLIB_ShBiSat_F16(0x4000, 1)

  1. 0x4000 (0.5) 左移1位,数学结果是1.0。
  2. Q1.15格式无法表示1.0(其上限是 1 - 2^-15)。
  3. 函数检测到正向溢出,于是将结果饱和到最大正值:0x7FFF (约0.99997)。 虽然损失了一些精度,但保证了结果的符号正确且数值在合理范围内,系统行为仍然是稳定的。

实操心得:何时用Sat版本?一个简单的原则:当你无法从数学上绝对保证运算不会溢出时,就使用带Sat的版本。尤其是在处理来自传感器、ADC或通信接口的外部数据时,这些数据可能超出你预期的范围。在控制环路(如PID控制器)中,饱和运算能防止积分项“wind-up”导致系统失控。虽然饱和运算会引入微小的非线性误差,但在绝大多数情况下,这比溢出导致的系统崩溃要安全得多。

3.3 单向移位:MLIB_ShL_F16/F32MLIB_ShR_F16/F32

这两个函数是单向的,分别用于纯左移和纯右移。第二个参数是无符号数 (UWord16),强调了移位方向是固定的。

函数原型:

Frac16 MLIB_ShL_F16(register Frac16 f16In1, register UWord16 u16In2); Frac32 MLIB_ShR_F32(register Frac32 f32In1, register UWord16 u16In2); // 其他类似

使用场景辨析:你可能会问,有了ShBi,为什么还需要ShLShR

  1. 语义清晰:当你的算法明确只需要左移(如放大信号)或只需要右移(如缩小信号)时,使用ShL/ShR能使代码意图更明确,提高可读性。
  2. 潜在优化:虽然MLIB可能用类似方式实现,但在某些架构或编译器优化下,明确方向的移位可能比带条件判断的双向移位有更优化的指令序列。
  3. 防止误用:使用ShL时,你传入一个负数移位量是没有意义的,这能在代码审查时更容易发现问题。

移位位数的范围限制同样适用ShLShRu16In2参数必须分别在 [0, 15] (Frac16) 或 [0, 31] (Frac32) 范围内。

3.4 饱和单向移位:MLIB_ShLSat_F16/F32

这是ShL的安全版本,原理与ShBiSat类似,只针对左移可能产生的溢出进行饱和处理。ShR(右移)只会让数值变小,不会溢出,因此没有提供ShRSat版本。

一个综合案例:动态范围缩放假设我们在处理一个音频样本块,需要根据一个动态增益gain(用Frac16表示)来调整音量。增益可能大于1(放大),也可能小于1(衰减)。我们可以用移位来近似乘法(当增益是2的幂次方时),或用移位配合查表、乘法实现更复杂的增益控制。

// 假设 audio_sample 是 Frac16 格式的音频样本 // gain_shift 是一个有符号整数,表示增益对应的2的幂次。例如,gain_shift=1 表示增益2倍,gain_shift=-1表示增益0.5倍 Frac16 apply_gain(Frac16 audio_sample, Word16 gain_shift) { Frac16 result; if(gain_shift >= 0) { // 放大,使用饱和左移防止溢出产生爆音 result = MLIB_ShLSat_F16(audio_sample, (UWord16)gain_shift); } else { // 衰减,使用右移(或ShBi带负参数) result = MLIB_ShR_F16(audio_sample, (UWord16)(-gain_shift)); } return result; }

4. 算术运算函数详解

移位是缩放,而加减乘除才是运算的核心。MLIB提供了基础的减法函数和实用的向量乘加函数。

4.1 减法运算:MLIB_Sub_F16/F32MLIB_SubSat_F16/F32

减法是最基础的算术运算之一。在定点数中,减法就是直接的整数减法,但需要对结果的小数点位置有清晰的认识。

函数原型:

Frac16 MLIB_Sub_F16(register Frac16 f16In1, register Frac16 f16In2); Frac32 MLIB_SubSat_F32(register Frac32 f32In1, register Frac32 f32In2); // 其他类似

运算规则:result = f16In1 - f16In2因为输入和输出都是相同的Q格式(Q1.15或Q1.31),所以直接做整数减法,结果自然就是相同Q格式的定点数差值。这是定点数运算的一个便利之处:相同格式的加减法,无需额外调整。

溢出分析:两个Q1.15数相减,结果的范围可能在 (-2, 2) 之间。而Q1.15只能表示 [-1, 1)。因此,当f16In1接近1 (0x7FFF) 且f16In2接近 -1 (0x8000) 时,数学结果接近2,会发生正向溢出。反之,当f16In1接近 -1 且f16In2接近1时,数学结果接近-2,会发生负向溢出

  • MLIB_Sub_F16:不处理溢出。发生溢出时,结果会环绕。例如,在16位有符号整数中,0x7FFF (32767) - 0x8000 (-32768) 的数学结果是65535,这超出了int16_t的范围。实际计算是 32767 - (-32768) = 32767 + 32768 = 65535。65535用16位无符号数是0xFFFF,用有符号int16_t解释就是 -1。所以一个接近2的值,溢出后变成了-1,误差极大。
  • MLIB_SubSat_F16:会处理溢出。对于上述情况,由于是正向溢出,它会将结果饱和到最大正值 0x7FFF。

注意事项:减法中的“坑”即使使用SubSat,也只能保证结果在 [-1, 1) 范围内。但有一个特殊情况:当f16In1f16In2非常接近时,结果会是一个绝对值很小的数,精度是足够的。然而,如果你需要的结果是差值放大后的信号(例如,在误差计算后需要乘以一个大的增益),那么即使差值本身没有溢出,后续的放大操作也可能导致溢出。这时就需要考虑使用更高精度的中间类型(如Frac32)来保存差值。

4.2 向量乘加运算:MLIB_VMac_F16/F32/F32F16F16

VMac是“Vector Multiply-Accumulate”的缩写,这是一个在数字信号处理(如滤波器、点积计算)中极其常用的操作。它一次性计算两个乘积的和。

函数原型:

// 全Frac32版本 Frac32 MLIB_VMac_F32(register Frac32 f32In1, register Frac32 f32In2, register Frac32 f32In3, register Frac32 f32In4); // 全Frac16版本 Frac16 MLIB_VMac_F16(register Frac16 f16In1, register Frac16 f16In2, register Frac16 f16In3, register Frac16 f16In4); // 混合精度版本:输入Frac16,输出Frac32 Frac32 MLIB_VMac_F32F16F16(register Frac16 f16In1, register Frac16 f16In2, register Frac16 f16In3, register Frac16 f16In4);

数学表达式:result = (fIn1 * fIn2) + (fIn3 * fIn4)注意:这里的乘法和加法都是定点数运算。

精度与溢出处理(核心难点):这是定点数运算中最需要小心的地方。我们以MLIB_VMac_F16为例:

  1. 乘法:两个Q1.15数(1位符号,15位小数)相乘,理论上会得到一个Q2.30格式的数(2位符号,30位小数)。但我们的目标是最终结果仍然是Q1.15。因此,乘积必须右移15位来重新归一化到Q1.15格式。MLIB库在内部帮我们完成了这个移位。
  2. 加法:两个经过移位归一化后的Q1.15数相加,结果范围可能在 [-2, 2) 之间,同样可能溢出。
  3. MLIB_VMac_F16不包���饱和处理。如果加法溢出,结果会环绕。

为什么需要MLIB_VMac_F32F16F16这是MLIB库提供的一个非常重要的精度扩展函数。它接受Frac16输入,但输出Frac32。

  1. Frac16乘法得到Q2.30中间结果。
  2. 这个中间结果被存储到Frac32(Q1.31)变量中。注意,这里存在一个格式转换:从Q2.30到Q1.31,小数位多了1位,这允许我们在加法前保留更高的精度。
  3. 两个Frac32中间结果相加,得到Frac32最终结果。 这样做的好处是:在进行连续的乘加运算(如FIR滤波器)时,可以先用高精度(Frac32)累加,最后再一次性舍入或饱和处理到Frac16输出,从而最小化累积误差。

实战场景:FIR滤波器单抽头计算假设我们有一个FIR滤波器,计算输出y[n]的一个项:y += coeff[i] * x[n-i]。其中coeff[i]x[n-i]都是Frac16。

// 方法1:使用全Frac16,快速但精度有限,易溢出 Frac16 tap_result_f16 = MLIB_VMac_F16(coeff[i], x_history[i], 0, 0); // 实际上就是一次乘法 y_f16 = MLIB_AddSat_F16(y_f16, tap_result_f16); // 假设有AddSat函数,需要循环累加 // 方法2:使用混合精度,精度高,不易溢出,适合高精度需求 Frac32 tap_result_f32 = MLIB_VMac_F32F16F16(coeff[i], x_history[i], 0, 0); y_acc_f32 += tap_result_f32; // y_acc_f32 是 Frac32 累加器 // 循环结束后,将 y_acc_f32 转换回 Frac16 y_f16 = MLIB_Round_F16(y_acc_f32, 16); // 假设用舍入函数

显然,在要求较高的滤波器中,方法2能提供更好的信噪比。

5. 核心环节:在真实工程中集成与使用MLIB

了解了单个函数后,我们需要把它们放到一个完整的工程上下文中去看。如何正确引入MLIB库?如何构建一个健壮的定点数处理流程?

5.1 环境配置与库的引入

MLIB库通常作为MCU SDK(如NXP的MCUXpresso SDK)的一部分提供。你需要:

  1. 在IDE(如Keil, IAR, MCUXpresso IDE)中,确保对应的MLIB库文件(通常是mlib.amlib.lib)被链接到你的工程中。
  2. 在需要使用MLIB函数的源文件中,包含头文件#include "mlib.h"
  3. 检查编译器优化等级。MLIB的大量内联函数在低优化等级(如-O0)下可能无法完全内联,会影响性能。建议在性能关键路径使用至少-O1或-O2优化。

5.2 构建健壮的定点数处理流程

一个典型的处理流程包括以下步骤,我将其总结为“定点数运算四步法”:

  1. 输入定标(Scaling):将物理世界的浮点数(如电压、温度)转换为定点数。这需要确定一个缩放因子(Scale)。例如,ADC采样值范围为0~3.3V,对应0~4095。你想用Q1.15表示-1V~1V。那么缩放因子就是:定点值 = (电压值 / 1.0) * 32768。注意检查输入是否超限,必要时进行钳位。

    #define VOLTAGE_SCALE (1.0f) // 物理量程:-1V ~ 1V #define ADC_MAX (4095) #define ADC_REF_VOLTAGE (3.3f) Frac16 convert_adc_to_fixed(uint16_t adc_raw) { // 1. 转换为浮点电压 (假设单极性ADC,0-3.3V) float voltage = ((float)adc_raw / ADC_MAX) * ADC_REF_VOLTAGE; // 2. 归一化到 [-1, 1] 范围(本例是0-3.3V映射到0-1,所以是单极性,需调整) // 假设我们想要的是以1.65V为中心的±1V范围 float normalized = (voltage - 1.65f) / VOLTAGE_SCALE; // 3. 钳位到 [-1, 1) 区间 if (normalized >= 1.0f) normalized = 0.999999f; if (normalized < -1.0f) normalized = -1.0f; // 4. 转换为Q1.15 return FRAC16(normalized); }
  2. 核心运算:使用MLIB函数进行所需的数学运算。这是最需要警惕溢出和精度损失的阶段。

    • 规划数据流:明确每个变量的Q格式。在信号链中,随着运算进行,数据的动态范围可能变化,需要适时进行缩放(移位)。
    • 优先使用高精度中间变量:对于复杂的多步计算(如a*b + c*d + e*f),应使用Frac32作为累加器,最后再降精度到Frac16。
    • 善用饱和运算:在可能溢出的环节(尤其是增益放大、加法、减法后),使用Sat版本函数。
  3. 结果处理:运算得到的定点数可能需要后处理。

    • 舍入(Rounding):如使用MLIB_Round函数,在降精度(如Frac32转Frac16)时,能获得比直接截断更好的精度。
    • 饱和(Saturation):如果核心运算用的是非饱和函数,在最终输出前,应手动进行饱和钳位。
    • 溢出标志检查:某些MLIB函数或处理器状态寄存器可能提供溢出标志,可以用于调试或高级容错处理。
  4. 输出反定标:将最终的定点数结果转换回物理量。

    float convert_fixed_to_voltage(Frac16 f16_val) { // 1. 将Q1.15转换为浮点数 float normalized = (float)f16_val / 32768.0f; // 2. 反归一化到电压值 float voltage = normalized * VOLTAGE_SCALE + 1.65f; return voltage; }

5.3 性能优化技巧

  1. 减少精度转换:频繁在Frac16和Frac32之间转换会消耗指令。尽量让一整段算法在统一精度下运行。
  2. 利用内联:MLIB函数本身是内联的,确保你的调用处于编译器能够并愿意内联的上下文中(如关闭函数分割、使用合理的优化等级)。
  3. 循环展开:对于处理数组的循环(如滤波器),在循环体内手动展开几次,可以减少循环开销,并给编译器更多的优化空间。但要注意代码体积的增大。
  4. 查表法替代复杂运算:对于某些非线性函数(如三角函数、指数),如果输入范围有限且精度要求可接受,预先计算好定点数查找表(LUT)并用MLIB的移位/加法来插值,速度远快于任何软件浮点实现。

6. 常见问题、调试技巧与避坑指南

即使理解了原理,实际使用中还是会遇到各种问题。下面是我总结的一些典型坑点和排查方法。

6.1 数值结果完全不对或异常大/小

  • 可能原因1:Q格式混淆。这是最常见的问题。你以为是Q1.15的数,实际上可能是Q1.31,或者根本就是个普通的整数。调试方法:在调试器中,将变量的十六进制值手动转换为浮点数验证。例如,看到变量a = 0x4000,如果是Frac16,它代表0.5;如果被误当作Frac32,它代表一个极小的数 (0x4000 / 2^31 ≈ 4.6566e-10)。
  • 可能原因2:移位位数超限。给ShL函数传入了大于15(对Frac16)的移位值。调试方法:检查所有移位操作的第二个参数,确保其值在有效范围内。可以添加断言(assert)进行运行时检查。
  • 可能原因3:溢出且未使用饱和。结果变成了一个符号相反的极大值。调试方法:在疑似溢出的运算步骤后,立即用饱和函数重新计算一次,比较结果。或者,在模拟环境中,用浮点版本算法并行运行,对比中间结果。

6.2 运算精度不符合预期

  • 可能原因1:累积误差。在长序列的运算中(如IIR滤波器),使用Frac16会导致精度迅速损失。解决方案:在反馈环路或累加器中,升级到Frac32。即使输入输出是Frac16,内部状态也应用Frac32保存。
  • 可能原因2:舍入方式不当。从高精度到低精度转换时,直接截断(C语言中的强制类型转换)会引入较大的统计偏差。解决方案:使用MLIB_Round函数进行四舍五入。它的原理通常是加0.5(在低精度格式下)后截断。
  • 可能原因3:乘法后的移位丢失。如果你自己用整数乘法和移位来模拟定点乘法,很容易忘记乘积需要右移n位(对Q1.15是15位)。MLIB函数在内部处理了这一点,但如果你自己实现,务必牢记。

6.3 程序运行速度慢

  • 可能原因1:编译器未内联MLIB函数。在调试模式或低优化等级下,编译器可能不会内联小函数。解决方案:检查反汇编代码,看MLIB函数调用是否是真的BL(分支链接)指令。如果是,请提高优化等级(如-O2),并确保函数定义在头文件中可用。
  • 可能原因2:频繁的精度转换。在循环中反复调用FRAC16()FRAC32()宏或进行类型转换。解决方案:将输入数据一次性批量转换为所需的定点格式,运算完成后再一次性转换回去。

6.4 调试与验证策略

  1. 单元测试法:为每个使用MLIB的算法模块编写单元测试。用已知的浮点输入和输出,验证定点版本的输出是否在误差允许范围内。这是保证算法移植正确的基石。
  2. 双路径执行法:在开发初期,可以让代码同时运行浮点版本和定点版本,并比较输出。一旦定点版本稳定,再移除浮点代码。这能帮你快速定位是算法逻辑错误还是定点化错误。
  3. 利用调试器观察:现代IDE的调试器可以定制数据显示格式。你可以为Frac16Frac32类型创建自定义的显示格式,使其直接显示为十进制小数,而不是十六进制数,这将极大提升调试效率。
  4. 边界条件测试:专门测试输入为最大值(0x7FFF)、最小值(0x8000)、0等边界情况,确保饱和、溢出处理符合预期。

最后,记住MLIB是一个工具,它强大而高效,但也需要谨慎和尊重。理解其背后的定点数原理,明确每一步运算的Q格式,在性能与安全之间做好权衡,你就能在资源受限的Cortex-M0+平台上,实现不亚于浮点运算的复杂算法。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询