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

elegant (and fast!) way to rearrange columns and rows in an ADC buffer

摘要: 我正在寻找一种优雅而快速的方法来“重新排列”我的 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, Ch2S1, Ch3S1,
Ch1S2, Ch2S2, Ch3S2,
Ch1S3 ...
Ch1Sn-1, Ch2Sn-1, Ch3Sn-1,
Ch1Sn, Ch2Sn, Ch3Sn

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

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

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

    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 个样本/9 个通道的约 4000 个时钟周期或每个样本约 27 个时钟周期。为了存档更高的采样率,这需要比现在快很多倍。

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

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

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

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

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

我不相信你的 perf 问题完全是你单步执行数据数组的结果,但这里有一个比你正在使用的方法更精简的方法:

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;
}

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

此外,如果我有一个算法描述使用短语“for each...”,我倾向于使用 for 循环形式,但是当描述使用“while ...”时,那么我当然可能会使用 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 程序员阅读其中任何一个都没有问题,但当涉及的变量数量和数学的复杂性增加时,将值数学与指针数学混合很快就会出现问题。你几乎永远不会为任何不平凡的事情做这件事,所以为了保持一致性,养成一起避免它的习惯。