硬汉嵌入式论坛

 找回密码
 立即注册
查看: 2469|回复: 10
收起左侧

[技术讨论] STM32F1使用TIM输出比较+DMA时遇到的问题

[复制链接]

1

主题

3

回帖

6

积分

新手上路

积分
6
发表于 2023-5-15 22:06:57 | 显示全部楼层 |阅读模式
本帖最后由 hurry 于 2023-5-16 12:42 编辑

新人第一次发帖,如有不妥请批评指正。
我想使用定时器的输出比较和DMA做时间和数量可控的脉冲,基于HAL库,以下是我的思路:
CubeMX配置:
定时器
img1.png
另外中断和DMA配置:
img2.png
img3.png
--------------------------------------------------------------------------------------------
代码思路
  • 创建一个大小为N的DMA缓存BUF用来给DMA运到捕获/比较寄存器CCRx,由于定时器是到达比较值就翻转所以一次DMA的最大输出脉冲数为N/2;
  • 清空TIM的计数值CNT,设置第一个比较值BUF[0]到CCRx寄存器(当定时器CNT等于BUF[0],发生第一次DMA传输);
  • 使用 HAL_TIM_OC_Start_DMA(htimx, channel, BUF[1], N-1) 来启动DMA并开启定时器;
  • DMA传输完成后进入DMA传输完成中断(此时最后一个比较值还没触发,DMA只是搬运到CCRx,计数值CNT还没等于CCRx),通过开启定时器的捕获/比较通道的中断TIM_IT_CCx使得最后一次比较完成时也就是CNT=CCRx=BUF[N]时再次中断,在这个中断里关闭捕获/比较通道的中断再使用HAL_TIM_OC_Stop_DMA(htimx, channel)关闭DMA传输,如果脉冲数超过N/2可以继续重复步骤(3)开启DMA传输数据;


代码实现

这个函数用来开始一次DMA传输和定时器
[C] 纯文本查看 复制代码
StepperDriverState_t StepperDriverMoveSteps(stepperDriver_t *hsd, long steps)
{
    StepperAssert(hsd);
    if (StepperDriverGetEnableStatus(hsd) == 0) return STEPPER_DRIVER_ERROR;
    if (hsd->curStep > 0) return STEPPER_DRIVER_BUSY;
    /* 方向处理 */
    if (steps > 0)
    {
        StepperDriverSetDir(hsd, 1);
        hsd->curStep = steps;
        hsd->curDir = 1;
    }
    else if (steps < 0)
    {
        StepperDriverSetDir(hsd, -1);
        hsd->curStep = -steps;
        hsd->curDir = -1;
    }
    else return STEPPER_DRIVER_ERROR;
    /* 脉冲数计算 */
    StepperLog("channel:%d, startStep:%d\n", hsd->stepCtrl.channel, steps);
    if (hsd->curStep*2 > STEPPER_DATAMAP_SIZE) /* 超过DMA缓存的部分存起来,分段多次传输 */
    {
        hsd->planStep = hsd->curStep - STEPPER_DATAMAP_SIZE/2;
        hsd->curStep = STEPPER_DATAMAP_SIZE/2;
    }
    /* 定时器设置 */
    __HAL_TIM_SET_COUNTER(hsd->stepCtrl.htim, 0);
    __HAL_TIM_SET_COMPARE(hsd->stepCtrl.htim, hsd->stepCtrl.channel, hsd->stepMap[0]);
    PinDEBUG2(1);
    HAL_TIM_OC_Start_DMA(hsd->stepCtrl.htim,
                          hsd->stepCtrl.channel,
                          (uint32_t*)&hsd->stepMap[1],
                          hsd->curStep*2-1);
    LOG_DBG(LOGTAG, "Pulse start :Cnt%d, CMP:%d\n",hsd->stepCtrl.htim->Instance->CNT, hsd->stepCtrl.htim->Instance->CCR1);
    return STEPPER_DRIVER_OK;
}

在中断中的处理
[C] 纯文本查看 复制代码
void StepperDriverPulseFinishedCallback(stepperDriver_t *hsd, TIM_HandleTypeDef *htim)
{
    if (htim != hsd->stepCtrl.htim)
        return;
    PinDEBUG1(1);//Debug1引脚设置高电平,表示进入中断

    if (__HAL_TIM_GET_ITSTATUS(htim, timitIndex[hsd->stepCtrl.channel>>2]) == RESET)
    {
        //DMA传输完成,第一次中断进入到这里
        __HAL_TIM_ENABLE_IT(htim, timitIndex[hsd->stepCtrl.channel>>2]);
        hsd->endPulse = 1; //开启了捕获/比较中断,定时器中断马上调用了一次回调,使用标志位跳过
        PinDEBUG1(0); //Debug1引脚设置低电平,表示退出中断
        return; 
    }
    if (hsd->endPulse == 1)
    {
        //跳过因开启中断而导致立即进入的中断
        hsd->endPulse = 0;
#if 0 //解决起始输输出高电平的临时方案
        // 如果定时器在启动时是低电平,因为传输奇数个数据这里应该是高电平
        if (hsd->stepCtrl.gpio->IDR & hsd->stepCtrl.pinBit != hsd->stepCtrl.pinBit)
        {
            LOGS("DMA Cplt , Voltage is Low\n");
            goto end; //低电平直接结束输出
        }
        else
        {
            LOGS("Waiting Next OC IT\n");
        }
#endif 
        PinDEBUG1(0); //Debug1引脚设置低电平,表示退出中断
        return; 
    }
    //最后一个输出比较结束,第三次进入回调
    end:
    PinDEBUG2(0); //debug2引脚设置低电平,高电平的持续时间就是本次定时器脉冲的总时长
    __HAL_TIM_DISABLE_IT(htim, timitIndex[hsd->stepCtrl.channel>>2]);
    HAL_TIM_OC_Stop_DMA(hsd->stepCtrl.htim, hsd->stepCtrl.channel);
    
    hsd->stepCnt += hsd->curStep*hsd->curDir; // 计算累计步数
    if (hsd->planStep > 0) //还有计划的步数没走完
    {
        if (hsd->planStep*2 > STEPPER_DATAMAP_SIZE)
        {
            hsd->planStep -= STEPPER_DATAMAP_SIZE/2;
            hsd->curStep = STEPPER_DATAMAP_SIZE/2;
        }
        else
        {
            hsd->curStep = hsd->planStep;
            hsd->planStep = 0;
        }

        __HAL_TIM_SET_COUNTER(hsd->stepCtrl.htim, 0);
        __HAL_TIM_SET_COMPARE(hsd->stepCtrl.htim, hsd->stepCtrl.channel, hsd->stepMap[0]);
        PinDEBUG2(1); //debug2引脚高电平代表开始DMA和定时器
        HAL_TIM_OC_Start_DMA(hsd->stepCtrl.htim,
                            hsd->stepCtrl.channel,
                            (uint32_t*)&hsd->stepMap[1],
                            hsd->curStep*2-1);
    }
    else
    {
        StepperLog("channel:%d stepCnt:%lld\n",hsd->stepCtrl.channel, hsd->stepCnt);
        hsd->planStep = 0;
        hsd->curStep = 0;
        hsd->curDir = 0;
    }
    PinDEBUG1(0); //Debug1引脚设置低电平,表示退出中断
}



测试
发生100个脉冲测试
img4.png
第一个红框
img5.png
第二个红框
img6.png
第三个红框
img7.png
现象:
  • 脉冲数是100个,分为两段,一共翻转了200次
  • 脉冲的数量和顺序都是对的
  • 定时器开始时是低电平

经过了多次验证代码是没有问题的......

直到我编写并使用停止当前输出脉冲后就开始出现问题了
[C] 纯文本查看 复制代码
void StepperDriverStopMove(stepperDriver_t *hsd)
{
    uint16_t waitStep;
    if (hsd->endPulse == 1 || hsd->curStep == 0) return;
    HAL_TIM_OC_Stop_DMA(hsd->stepCtrl.htim, hsd->stepCtrl.channel);
    waitStep = (__HAL_DMA_GET_COUNTER(hsd->stepCtrl.htim->hdma[dmaIndex[hsd->stepCtrl.channel>>2]])+1)/2;
    if(waitStep && hsd->curStep)  //DMA还有数据等待传输,当前计划的脉冲不为零
    {
        hsd->stepCnt += (hsd->curStep - waitStep)*hsd->curDir; //计算累计脉冲数
        hsd->planStep = 0; //清空未完成的计划
        hsd->curDir = 0;
        hsd->curStep = 0;
        StepperLog("stepper stop pos:%lld,wait:%d\n", hsd->stepCnt,waitStep);
    }
}

反复很多次使用之后初始电平从原来的低电平变为了高电平
现象

img8.png

上面生成的测试数据为 stepMap[0] = 1,stepMap[1] = 1 + 10 , stepMap[3] = 11 + 10......
原来第一个脉冲长度为10us也就是第一次触发到第二次触发之间是高电平,
现在是1us,因为开启定时器到第一次触发比较之间的时间是1us,判断出初始电平可能不对了?

把测试数据更改为一个100us的脉冲再次测试:
正常情况
img9.png

异常出现后
img10.png
目前怀疑在DMA为完成传输的情况下使用HAL_TIM_OC_Stop_DMA后可能会造成这个现象。
目前的临时解决方法是在DMA传输完成中断里面判断是否为高电平,如果是低电平就直接结束,不等待最后一个比较值触发,不然会多一个脉冲,
实际验证个方法只能减小出现问题的概率,不能根除初始电平翻转的问题

附件为STM32F103ZE的CubeMX和Keil工程,相关文件为
UserDriver/Stepper_Driver.c  Line94、Line132、Line 193 (应用代码)
UserApp/board_base.cpp  Line135 (中断回调)
UserApp/task_main.c Line205、Line255 (串口指令)
复现方法:
[C] 纯文本查看 复制代码
串口1/3/4发送命令
ARM:D; (把stepMap的数据替换为测试数据)
MC:2,ms,1000; (表示PA2(TIM5 CH3)产生1000个脉冲)
MC:2,stop; (表示在产生脉冲时强行停止)

反复多次产生然后停止就有概率看到现象
把MC:2后面的2换为1可以在PA0(TIM2 CH1)上输出脉冲
临时解决方案在Stepper_Driver.c 211行,经过测试产生异常的概率会减小但是还是会有,求大佬解答为何初始电平会变化
STM32F103ZE_RTOS_Project0515.rar (1.35 MB, 下载次数: 8)
回复

使用道具 举报

1万

主题

7万

回帖

11万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
115679
QQ
发表于 2023-5-16 00:52:43 | 显示全部楼层
我做脉冲控制,一般使用的TIM UP更新事件 + DMA触发任意GPIO做脉冲控制,如果做停止,我是设置全高或者全低

STM32H7视频教程第16期:DMA双缓冲实现32路脉冲并行同步控制(2022-05-26)
https://www.armbbs.cn/forum.php? ... d=112560&fromuid=58
(出处: 硬汉嵌入式论坛)


回复

使用道具 举报

1

主题

3

回帖

6

积分

新手上路

积分
6
 楼主| 发表于 2023-5-16 12:40:10 | 显示全部楼层
eric2013 发表于 2023-5-16 00:52
我做脉冲控制,一般使用的TIM UP更新事件 + DMA触发任意GPIO做脉冲控制,如果做停止,我是设置全高或者全低 ...

get到思路了,有没有F1相关的代码可以参考呢
回复

使用道具 举报

1万

主题

7万

回帖

11万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
115679
QQ
发表于 2023-5-16 15:32:15 | 显示全部楼层
hurry 发表于 2023-5-16 12:40
get到思路了,有没有F1相关的代码可以参考呢

F1系列没做例子,也可以方便的实现,你可以试试,使用UP更新事件触发DMA执行,发送数据给GPIO引脚输出寄存器。

QQ截图20230516153237.png
回复

使用道具 举报

0

主题

3

回帖

3

积分

新手上路

积分
3
发表于 2024-9-14 17:54:12 | 显示全部楼层
我也遇到了这个问题,看了下代码,楼主你的思路和我一致,我解决这个问题的办法是在每次HAL_TIM_OC_Stop_DMA之后,将CCMR1寄存器中的Mode改为 TIM_OCMODE_FORCED_INACTIVE 模式,在每一次HAL_TIM_OC_Start_DMA之前将CCMR1中的Mode改为 TIM_OCMODE_TOGGLE,这样可以保证在任意时刻停止传输时,对应输出通道一定是低电平,下次启动时一定是从低电平开始跳变的,附上修改OC触发模式的代码

[C] 纯文本查看 复制代码
static void change_mtr_tim_ocmode(TIM_HandleTypeDef *htim, uint32_t new_mode) {
    uint32_t tmpccmrx;
    /* Get the TIMx CCMR1 register value */
    tmpccmrx = htim->Instance->CCMR1;
    /* Reset the Output Compare Mode Bits */
    tmpccmrx &= ~TIM_CCMR1_OC1M;
    tmpccmrx &= ~TIM_CCMR1_CC1S;
    /* Select the Output Compare Mode */
    tmpccmrx |= new_mode;
    /* Write to TIMx CCMR1 */
    htim->Instance->CCMR1 = tmpccmrx;
}
回复

使用道具 举报

1万

主题

7万

回帖

11万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
115679
QQ
发表于 2024-9-15 09:45:28 | 显示全部楼层
agkody 发表于 2024-9-14 17:54
我也遇到了这个问题,看了下代码,楼主你的思路和我一致,我解决这个问题的办法是在每次HAL_TIM_OC_Stop_DM ...

谢谢分享。
回复

使用道具 举报

0

主题

35

回帖

35

积分

新手上路

积分
35
发表于 2024-9-16 15:58:26 | 显示全部楼层
这个我试过,标准库+DMA+定时器输出比较通道,F1可以实现12路梯形加减速,12路一起启动填充DMA发送数组是个瓶颈,25K脉冲就差不多到极限了,而且最后停止会有一个非常短的脉冲,宽带大约2us,这个用中断方式也会有,但是中断方式比这个安全可靠。另外硬汉大佬这个定时器更新中断内启动DMA来翻转GPIO也试了一下,不知道和中断里直接翻转IO相比有什么优势,频率同样上不去,操作也没变简单。
回复

使用道具 举报

1万

主题

7万

回帖

11万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
115679
QQ
发表于 2024-9-17 01:37:52 | 显示全部楼层
死不低头 发表于 2024-9-16 15:58
这个我试过,标准库+DMA+定时器输出比较通道,F1可以实现12路梯形加减速,12路一起启动填充DMA发送数组是个 ...

实际上中断可以处理的过来的话,中断确实最简单易用的。

DMA的话,就是节省CPU利用率。另外路数多的话,可以FMC DMA多路脉冲,同样的实现方法。
回复

使用道具 举报

0

主题

3

回帖

3

积分

新手上路

积分
3
发表于 2024-9-18 09:23:47 | 显示全部楼层
死不低头 发表于 2024-9-16 15:58
这个我试过,标准库+DMA+定时器输出比较通道,F1可以实现12路梯形加减速,12路一起启动填充DMA发送数组是个 ...

你遇到的这个问题我也同时遇到了,我用的是STM32G474做的测试,主频170M,DMA传输中断优先级设置为0。我的操作是在每次DMA完成传输中断中将会停止DMA传输,并且强行下拉定时器输出通道,问题就出在最后一个脉冲上:因为DMA完成传输中断触发于最后一次CCRx的填装时刻而不是最后一次CCRx=Cnt的时刻,而且DMA的传输完成中断触发会有一定时间的延迟,这两种情况就导致最后一个脉冲会被HAL_TIM_OC_Stop_DMA函数强行提前中止。在我的测试中,最后一个脉冲的正脉宽低于3.5us时中断不会截断原本正脉宽的输出,只有大于3.5us时才会被中断强行截断;我的解决方案是,在每次加减速曲线生成的脉冲数组末尾追加一个远大于3.5us的负脉宽,相当于n个脉冲DMA传输2n+1次,用于确保脉冲群的完整性。

至于频率问题,用DMA方式输出不定周期的脉冲频率实测是可以做到2.5MHz的可控频率可控脉冲数量的:

0.4us周期的双脉冲测试

0.4us周期的双脉冲测试

0.4us周期的四脉冲测试

0.4us周期的四脉冲测试

2us周期的双脉冲测试

2us周期的双脉冲测试

可变频率的脉冲测试

可变频率的脉冲测试

file:///C:/Users/Admin/Desktop/%E6%B5%8B%E8%AF%95%E5%9B%BE1.jpg
回复

使用道具 举报

1万

主题

7万

回帖

11万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
115679
QQ
发表于 2024-9-19 09:47:58 | 显示全部楼层
agkody 发表于 2024-9-18 09:23
你遇到的这个问题我也同时遇到了,我用的是STM32G474做的测试,主频170M,DMA传输中断优先级设置为0。我 ...

谢谢分享。
回复

使用道具 举报

0

主题

35

回帖

35

积分

新手上路

积分
35
发表于 2024-9-28 13:32:19 | 显示全部楼层
agkody 发表于 2024-9-18 09:23
你遇到的这个问题我也同时遇到了,我用的是STM32G474做的测试,主频170M,DMA传输中断优先级设置为0。我 ...

你这个2.5M频率,是算好了整个数组存起来再发送,还是边发送边计算填充,或者分成几个小数组分段发送,如果有多路输出要边加速边填充我觉得很难把速度搞上去,加速阶段要填充的数据多了还是有一定的计算量的,假设要实现1M频率,就要1秒计算2M个值,1ms要计算2K个
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|小黑屋|Archiver|手机版|硬汉嵌入式论坛

GMT+8, 2025-5-12 00:13 , Processed in 0.291996 second(s), 27 queries .

Powered by Discuz! X3.4 Licensed

Copyright © 2001-2023, Tencent Cloud.

快速回复 返回顶部 返回列表