将 DMA 用于 PWM 时的性能优势

Performance benefit when using DMA for PWM

我在 STM32F411RE 微控制器上有一段代码作为 FreeRTOS 任务运行:

static void TaskADCPWM(void *argument)
{
    /* Variables used by FreeRTOS to set delays of 50ms periodically */
    const TickType_t DelayFrequency = pdMS_TO_TICKS(50);
    TickType_t LastActiveTime;

    /* Update the variable RawAdcValue through DMA */
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)&RawAdcValue, 1);

#if PWM_DMA_ON
    /* Initialize PWM CHANNEL2 with DMA, to automatically change TIMx->CCR by updating a variable */
    HAL_TIM_PWM_Start_DMA(&htim3, TIM_CHANNEL_2, (uint32_t*)&RawPWMThresh, 1);
#else
    /* If DMA is not used, user must update TIMx->CCRy manually to alter duty cycle */
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
#endif

    while(1)
    {
        /* Record last wakeup time and use it to perform blocking delay the next 50ms */
        LastActiveTime = xTaskGetTickCount();
        vTaskDelayUntil(&LastActiveTime, DelayFrequency);
        
        /* Perform scaling conversion based on ADC input, and feed value into PWM CCR register */
#if PWM_DMA_ON
        RawPWMThresh = (uint16_t)((RawAdcValue * MAX_TIM3_PWM_VALUE)/MAX_ADC_12BIT_VALUE);
#else
        TIM3->CCR2 = (uint16_t)((RawAdcValue * MAX_TIM3_PWM_VALUE)/MAX_ADC_12BIT_VALUE);
#endif

    }
}

上述任务使用 RawAdcValue 值通过 DMA 或手动更新 TIM3->CCR2 寄存器。 RawAdcValue 通过 DMA 定期更新,存储在该变量中的值是 12 位宽。

我理解使用 DMA 如何有利于读取上面的 ADC 样本,因为 CPU 不需要 poll/wait ADC 样本,或者使用 DMA 通过 I2C 传输长数据流或 SPI。 但是,使用 DMA 更新 TIM3->CCR2 寄存器 而不是手动修改 TIM3->CCR2 寄存器是否有显着的性能优势:

TIM3->CCR2 &= ~0xFFFF;
TIM3->CCR2 |= SomeValue;

通过 DMA 或非 DMA 更新 CCR 寄存器的主要区别是什么?

首先,请记住过早的优化是无数问题的根源。您需要问的问题是“处理器还需要做什么?”。如果处理器没有更好的事情要做,那么只需轮询并节省一些编程工作。

如果处理器确实有更好的事情要做(或者你 运行 没有电池并且想省电)那么你需要计算处理器在它需要做的每件事之间等待的时间.

在您的情况下,您正在使用操作系统上下文切换来代替“等待”。您可以通过测量其他线程的性能来计算 switch-write-to-pwm-switch-back 周期的成本。

设置一个有两个线程的系统。在一个线程中执行一些您知道其性能的任务,例如,一些固定的计算或处理器基准测试。现在设置另一个线程来完成上面的计时器业务。测量第一个线程的性能。

接下来建立一个类似的系统,只有第一个线程加上执行 PWM 的 DMA。衡量性能变化,你有你答。

显然这完全取决于您的具体系统。没有可以给出的一般答案。您的测试越接近您的真实系统,您得到的答案就越准确。

PS:您的 PWM 将使用上述代码出现故障。将两次写入替换为一次写入:

TIM3->CCR2 &= ~0xFFFF;
TIM3->CCR2 |= SomeValue;

应该是:

TIM3->CCR2 = ((TIM3->CCR2 & ~0xFFFF) | SomeValue);

让我们首先假设您需要实现“每秒 N 个样本”。例如。对于音频,这可能是每秒 44100 个样本。

对于 PWM,您需要在每个样本中多次更改输出状态。例如;对于音频,这可能意味着每个样本写入 CCR 大约四次,或每秒“4*44100 = 176400”次。

现在看看 vTaskDelayUntil() 做了什么 - 很可能它设置了一个定时器并进行了一次任务切换,然后(当定时器到期时)你得到一个 IRQ,然后是第二个任务切换。每次更改 CCR 时,总开销可能会增加 500 CPU 个周期。您可以将其转换为百分比。例如。 (继续音频示例),“每秒 176400 CCR 更新 * 每次更新 500 个周期 = 每秒约 8820 万周期的开销”,然后,对于 100 MHz CPU,您可以做到“8820 万 / 1 亿 = 88.2% 的 CPU 时间是因为您没有使用 DMA 而浪费的。

下一步是找出 CPU 时间的来源。有两种可能:

a) 如果你的任务是系统中最高优先级的任务(包括比所有 IRQ 都更高的优先级等);那么所有其他任务都将成为您时间消耗的牺牲品。在这种情况下,你单枪匹马地破坏了实时 OS 的任何烦恼(最好只使用 faster/more 高效的非实时 OS 来优化“平均case”,而不是优化“最坏情况”,并使用 DMA,并使用更少的 powerful/cheaper CPU,以降低“$”的成本获得更好的最终结果。

b) 如果您的任务不是系统中优先级最高的任务,则上面显示的代码已损坏。具体来说,IRQ(可能还有任务 switch/preemption)会在 vTaskDelayUntil(&LastActiveTime, DelayFrequency); 之后立即发生,从而导致 TIM3->CCR2 = (uint16_t)((RawAdcValue * MAX_TIM3_PWM_VALUE)/MAX_ADC_12BIT_VALUE); 在错误的时间发生(比预期晚很多)。在病态情况下(例如,磁盘或网络等其他事件恰好以类似的相关频率发生 - 例如,“CCR 更新频率”的一半)这很容易变得完全无法使用(例如,因为打开输出通常会延迟更多超出预期并且关闭输出不是)。

然而...

所有这些都取决于您实际需要的每秒样本数(或者更好的是,每秒有多少 CCR 更新)。出于某些目的(例如,在改变太阳能电池板角度以全天跟踪太阳位置的系统中控制电动机的速度);也许您每分钟只需要 1 个样本,使用 CPU 引起的所有问题都会消失。对于其他目的(例如 AM 无线电传输)DMA 可能也不够好。

警告

不幸的是,我无法在网上找到HAL_ADC_Start_DMA()HAL_TIM_PWM_Start()HAL_TIM_PWM_Start_DMA()的任何文档,并且不知道参数是什么或DMA 实际是如何使用的。当我第一次写这个答案时,我只是依赖于一个可能是错误假设的“可能假设”。

通常,对于 DMA,您有许多数据块(例如,对于音频,可能有 176400 个值的块 - 足以在“每个样本 4 个值,每秒 44100 个样本”下播放一整秒的声音) );在进行传输时,CPU 可以自由地做其他工作(而不是浪费)。对于连续操作,CPU 可能会在 DMA 传输发生时准备下一个数据块,当 DMA 传输完成时,硬件将生成一个 IRQ,IRQ 处理程序将为下一个块启动下一个 DMA 传输值(或者,可以将 DMA 通道配置为“自动重复”,并且数据块可以是循环缓冲区)。这样一来,“由于您没有使用 DMA 而浪费的所有 CPU 时间的 88.2%”将是“几乎为零 CPU 的时间,因为 DMA 控制器几乎完成了所有工作”;并且整个事情不会受到大多数时序问题的影响(IRQ 或更高优先级的任务抢占不会影响 DMA 控制器的时序)。

这就是我假设代码在使用 DMA 时正在执行的操作。具体来说,我假设 DMA 每隔“N 纳秒”就会从一大块原始值中获取下一个原始值,并使用下一个原始值(表示脉冲宽度)将定时器的阈值设置为从 0 开始的值到 N 纳秒。

事后看来;代码更有可能将 DMA 传输设置为“每次传输 1 个值,并连续自动重复”。在这种情况下,DMA 控制器将以(可能很高的)频率不断地将 RawPWMThresh 中的任何值泵送到定时器,然后 while(1) 循环中的代码将更改中的值RawPWMThresh 频率(可能低得多)。例如(继续音频示例);它可能就像“每个样本 16 个值(通过 DMA 控制器),每秒 44100 个样本(通过 while(1) 循环)”。在这种情况下;如果某些东西(不相关的 IRQ 等)在 vTaskDelayUntil() 之后导致意外的额外延迟;那么这不是一个巨大的灾难(DMA 控制器只是重复现有值更长的时间)。

如果是的话;那么真正的区别可能是“每个样本 X 值,每秒 20 个样本”(使用 DMA)与“每个样本 1 个值,每秒 20 个样本”(没有 DMA);无论如何,开销是相同的,但 DMA 的输出质量要好得多。

但是;在不知道代码实际做什么的情况下(例如,在不知道 DMA 通道的频率以及定时器的预分频器之类的东西是如何配置的情况下)从技术上讲,在使用 DMA 时,“每个样本的 X 值,每秒 20 个样本”实际上是“每个样本 1 个值,每秒 20 个样本”(X == 1)。在那种情况下,使用 DMA 几乎毫无意义(none 我最初假设的性能优势;以及几乎 none 我事后想假设的“输出质量”优势,除了“如果在 vTaskDelayUntil() 之后出现意外的额外延迟,则重复旧值”)。