将 DMA 用于 PWM 时的性能优势

如何解决将 DMA 用于 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 不需要轮询/等待 ADC 样本,或者使用 DMA 通过 I2C 或 SPI 传输长数据流。 但是,使用 DMA 更新 TIM3->CCR2 寄存器而不是通过以下方式手动修改 TIM3->CCR2 寄存器是否具有显着的性能优势:

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

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

解决方法

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

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

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

下一步是弄清楚 CPU 时间从何而来。有两种可能:

a) 如果您的任务是系统中优先级最高的任务(包括优先级高于所有 IRQ 等);那么其他所有任务都将成为您时间消耗的牺牲品。在这种情况下,您单枪匹马地破坏了实时操作系统的任何麻烦(可能更好的是使用更快/更有效的非实时操作系统来优化“平均情况”而不是优化“最坏情况”,并使用 DMA,并使用功能较弱/更便宜的 CPU,以降低“以美元为单位的成本”获得更好的最终结果。

b) 如果您的任务不是系统中优先级最高的任务,则上面显示的代码已损坏。具体来说,IRQ(可能还有任务切换/抢占)会在 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 通道可以配置为“自动重复”,并且数据块可能是一个循环缓冲区)。那样的话,“所有 CPU 时间的 88.2% 因为你没有使用 DMA 被浪费”将是“几乎使用的 CPU 时间为零,因为 DMA 控制器几乎在做所有事情”;并且整个过程不会受到大多数时序问题的影响(IRQ 或更高优先级的任务抢占不能影响 DMA 控制器的时序)。

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

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

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

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

,

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

如果处理器确实有更好的事情要做(或者您正在使用电池运行并希望节省电量),那么您需要计算处理器在它需要做的每件事之间等待的时间。

就您而言,您使用的是操作系统上下文切换而不是“等待”。您可以通过测量其他线程的性能来计算切换写入到 pwm 切换回周期的成本。

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

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

显然,这在很大程度上取决于您的确切系统。没有可以给出的一般性答案。您的测试与您的真实系统越接近,您得到的答案就越准确。

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

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

应该是:

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

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

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 <select id="xxx"> SELECT di.id, di.name, di.work_type, di.updated... <where> <if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 <property name="dynamic.classpath" value="tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams['font.sans-serif'] = ['SimHei'] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -> systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping("/hires") public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate<String
使用vite构建项目报错 C:\Users\ychen\work>npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-