微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

在 ADC 缓冲区中重新排列列和行的优雅且快速!方法

如何解决在 ADC 缓冲区中重新排列列和行的优雅且快速!方法

摘要 我正在寻找一种优雅而快速方法来“重新排列”我的 ADC 缓冲区中的值以进行进一步处理。

简介: 在 ARM Cortex M4 处理器上,我使用 3 个 ADC 通过 DMA 和“双缓冲技术”对模拟值进行采样。当我得到“半缓冲区完成中断”时,一维数组中的数据排列如下:

Ch1S1,Ch2S1,Ch3S1,Ch1S2,Ch2S2,Ch3S2,Ch1S3 ..... Ch1Sn-1,Ch2Sn-1,Ch3Sn-1,Ch1Sn,Ch2Sn,Ch3Sn 其中 Sn 代表 Sample#,CHn 代表通道号。 当我做 2x Oversampling n 等于 16 时,通道数实际上是 9,在上面的例子中是 3

或以二维形式编写

Ch1S1,Ch1S3 ...
Ch1Sn-1,Ch3Sn

其中行代表 n 个样本,列代表通道 ...

我正在使用 CMSIS-DSP 来计算所有矢量内容,例如移位、缩放、乘法,一旦我“整理”了通道。这部分相当快。

问题: 但是我用于将一维缓冲区数组“重塑”为每个通道的累积值的代码非常糟糕且缓慢:

    for(i = 0; i < ADC_BUFFER_SZ; i++) {
       for(j = 0; j < MEAS_ADC_CHANNELS; j++) {
          if(i) *(ADC_acc + j) += *(ADC_DMABuffer + bP);    // sum up all elements
          else *(ADC_acc + j) = *(ADC_DMABuffer  + bP);     // initialize new on first run
          bP++;
       }
     }

在这个过程之后,我得到一个一维数组,每个通道有一个(累积的)U32 值,但是这个代码很慢:每个通道 16 个样本的 ~4000 个时钟周期/9 个通道或每个样本 ~27 个时钟周期。为了存档更高的采样率,这需要比现在快很多倍。

问题: 我正在寻找的是:一些优雅的方式,使用 CMSIS-DPS 函数来存档与上述相同的结果,但速度要快得多。我的直觉告诉我,我的想法是错误的,CMSIS-DSP 库中必须有一个解决方案,因为我很可能不是第一个偶然发现这个话题的人,也很可能不会是最后一个。所以我要求朝正确的方向推动一点,我猜这可能是“工作盲目”的严重情况......

我正在考虑将点积函数“arm_dot_prod_q31”与一个填充有数组的数组一起用于累积任务,因为我找不到可以简单地总结一维数组的 CMSIS 函数?但这并不能解决“重塑”问题,我仍然必须复制数据并创建新缓冲区来为“arm_dot_prod_q31”调用准备向量...... 除此之外,使用点积感觉有点尴尬,我只想总结数组元素......

我还想过将 ADC 缓冲区转换为 16 x 9 或 9 x 16 矩阵,但后来我找不到任何可以轻松(=快速且优雅)访问行或列的内容,这让我不得不使用另一个解决的问题,最终需要创建新的缓冲区并复制数据,因为我缺少一个可以将矩阵与向量相乘的函数......

也许有人给我提示,指出我正确的方向? 非常感谢和欢呼!

解决方法

ARM 是一种风险设备,因此 27 个周期大致等于 27 条指令,IIRC。您可能会发现需要更高的时钟频率才能满足时序要求。你在运行什么操作系统?您可以访问缓存控制器吗?您可能需要将数据缓冲区锁定到缓存中以获得足够高的性能。此外,在系统允许的情况下,将您的总和和原始数据物理地保存在内存中。

我不相信您的性能问题完全是您逐步处理数据数组的结果,但这里有一种比您使用的方法更简化的方法:

int raw[ADC_BUFFER_SZ];
int sums[MEAS_ADC_CHANNELS];

for (int idxRaw = 0,int idxSum = 0; idxRaw < ADC_BUFFER_SZ; idxRaw++)
{
    sums[idxSum++] += raw[idxRaw];
    if (idxSum == MEAS_ADC_CHANNELS) idxSum = 0;
}

请注意,我没有测试过上面的代码,也没有尝试编译它。算法很简单,你应该能很快上手。

在您的代码中编写指针数学不会使其更快。编译器将为您将数组表示法转换为有效的指针数学。你绝对不需要两个循环。

也就是说,我经常使用指针进行迭代:

int raw[ADC_BUFFER_SZ];
int sums[MEAS_ADC_CHANNELS];

int *itRaw = raw;
int *itRawEnd = raw + ADC_BUFFER_SZ;
int *itSums = sums;
int *itSumsEnd = itSums + MEAS_ADC_CHANNELS;

while(itRaw != itEnd)
{
    *itSums += *itRaw;
    itRaw++;
    itSums++;
    if (itSums == itSumsEnd) itSums = sums;
}

但几乎从来没有,当我与数学家或科学家合作时,测量/计量设备开发通常就是这种情况。与迭代器形式相比,向非 C 审阅者解释数组符号更容易。

另外,如果我有一个使用短语“for each...”的算法描述,我倾向于使用 for 循环形式,但是当描述使用“while ...”时,那么 of当然,我可能会使用 while... 形式,除非我可以通过将其重新排列为 do..while 来跳过一个或多个变量赋值语句。但我经常尽可能接近原始描述,直到我通过所有测试标准,然后出于代码卫生目的重新安排循环。当您可以轻松地让他们相信您实现了他们所描述的内容时,与领域专家争论他们的数学是错误的会更容易。

始终先做对,然后衡量并决定是否进一步磨练代码。几十年前,嵌入式系统的一些 C 编译器在优化一种循环方面比另一种循环做得更好。我们曾经不得不密切关注他们生成的机器代码,并且经常养成避免那些最坏情况的习惯。这在今天并不常见,几乎可以肯定您的 ARM 工具链并非如此。但您可能需要研究编译器优化功能的工作原理并尝试不同的方法。

尽量避免在与指针数学相同的行上进行值数学。这只是令人困惑:

   *(p1 + offset1) += *(p2 + offset2); // Can and should be avoided.
   *(p1++) = *(p2++); // reasonable,especially for experienced coders/reviewers.
   p1[offset1] += p2[offset2]; // Okay. Doesn't mix math notation with pointer notation.
   p1[offset1 + A*B/C] += p2...; // Very bad.
   // But...
   int offset1 += A*B/C; // Especially helpful when stepping in the debugger.
   p1[offset1]... ; // Much better.

因此是前面提到的迭代器形式。它可能会减少代码行数,但不会降低复杂性,而且肯定会增加在某些时候引入错误的几率。

纯粹主义者可能会争辩说,p1[x] 实际上是 C 中的指针表示法,但数组表示法几乎具有跨语言的通用绑定规则,即使不是完全通用的。意图是显而易见的,即使对于非程序员也是如此。虽然上面的例子非常简单,大多数 C 程序员在阅读其中任何一个都没有问题,但是当涉及的变量数量和数学复杂性增加时,将值数学与指针数学混合很快就会出现问题。你几乎永远不会为了任何重要的事情去做,所以为了一致性起见,请养成一起避免它的习惯。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。