GPIO工作时序模拟_DWT实现μs级精确延时

时序模拟

  • 工作时序

    在使用各种器件时,非常高频的操作就是使用GPIO来模拟这些器件所需的各种工作时序,以I2C通信为例,进行I2C启动的时序为:

    SCL时钟信号为高电平,SDA数据信号由高电平置低,产生下降沿

    我们在使用软件模拟I2C时,就会用到如下的代码:

    void SDA_Output( uint16_t val )
    {
    	if ( val )
    		GPIOB->BSRR |= GPIO_PIN_7;
    	else
    		GPIOB->BRR |= GPIO_PIN_7;
    }
    void SCL_Output( uint16_t val )
    {
    	if ( val )
    		GPIOB->BSRR |= GPIO_PIN_6;
    	else
    		GPIOB->BRR |= GPIO_PIN_6;
    }
    
    void I2CStart(void)//产生启动信号
    {
    	SDA_Output(1);//数据线为高电平
    	delay(DELAY_TIME);
    	SCL_Output(1);//时钟线为高电平
    	delay(DELAY_TIME);
    	SDA_Output(0);//数据线由高置低产生下降沿
    	delay(DELAY_TIME);
    	SCL_Output(0);
    	delay(DELAY_TIME);
    }
    

产生工作时序的代码实现

  • 由上文代码可见,产生工作时序的操作可以分为两个重要部分:

  • 控制GPIO的输出

    也就是控制电平高低

    一般来说,先配置好所需引脚,为了方便修改与移植,一般不用CubeMX来配置器件要连接的引脚,而是直接写在函数中,用宏定义来标识所用引脚

    #define MY_PORT	GPIOA
    #define MY_PIN	GPIO_PIN_0
    //这样,只要修改宏定义,就能更改所用引脚
    void DHT11_PP_OUT(void)
    {
    	GPIO_InitTypeDef GPIO_InitStruct;
    	GPIO_InitStruct.Pin = MY_PIN;
    	GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;	
    	GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    	HAL_GPIO_Init(MY_PORT, &GPIO_InitStruct);
    }
    

    然后,就可以写一个函数(如上文),或者直接用HAL_GPIO_WritePin来控制电平高度

    为了编写更简单,一般把调用函数控制电平的语句再用宏定义起一个“别名”来方便调用

    #define MY_PULL_1 HAL_GPIO_WritePin(MY_PORT, MY_PIN, GPIO_PIN_SET)
    #define MY_PULL_0 HAL_GPIO_WritePin(MY_PORT, MY_PIN, GPIO_PIN_RESET)
    #define MY_ReadPin HAL_GPIO_ReadPin(MY_PORT, MY_PIN)
    
    MY_PULL_1;//给引脚置1,发送高电平
    
  • 延时函数排障

    这就是极其容易出错的部分:如果器件对工作时序有特殊的要求,例如要求μs级的持续电平,而延时精度不够,这就会导致器件接收到的信号并非它能“听懂”的指令,从而产生极其隐蔽的错误,整个程序会卡在器件的初始化阶段(因为没有接收到正确的启动时序),你会误认为器件坏了

延时函数的分类

  • 为了进行延时,有几种不同的原理

  • CPU空转延时

    就是让CPU执行空循环,执行这些空白指令的用时即被延时的时间,当然这种简单粗暴的方法随着主频的提升变得不可靠,而且容易被编译器优化掉(例如I2C使用的一节中就必须调节编译器优化等级才能正常使用),在STM32的开发中一般不再使用了

  • SysTick/HAL_Delay

    借助STM32系统自带的中断SysTick进行延时操作,精度为1ms,具体原理不再赘述

    这种延时操作简单,例如只需要HAL_Delay(50);即可延时50ms

  • TIM中断

    与上文介绍的两种阻塞式中断(在延时过程中无法执行其他操作)不同,利用硬件定时器的中断可以在延时过程中进行其他操作,实时性更强,而且精度极高,但配置麻烦,且占用硬件资源

DWT

DWT概述

  • 由上文的介绍,是否有一种延时的方法,既极其精准,能够实现μs级的延时,又使用简单,无需像TIM一样进行复杂的配置呢

  • DWT简介

    这就可以借助DWT(Data Watchpoint and Trace,数据观察点与跟踪)

    这是ARM Cortex-M 系列处理器内核(如 Cortex-M3、M4、M7 等)中集成的一个调试与性能分析单元,它在不占用 CPU 资源、不影响主程序逻辑的前提下,提供高精度的运行时信息,我们可以使用其中的周期计数器CYCCNT单元进行计时操作

  • 启用DWT

    作为一个调试模块,DWT默认是关闭的,需要手动配置打开

    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    //使能Trace功能(通过 CoreDebug 寄存器)
    
    DWT->CYCCNT = 0;
    //清零CYCCNT,这一步也可以不做
    
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    //使能 CYCCNT 计数器
    

DWT相关寄存器

  • 周期计数器CYCCNT

    每经过一个 CPU 时钟周期,自动+1,若系统主频 = 72 MHz 则每个周期约为13.89 ns,可见精度极高

    计数器有32 位(可溢出),因此(72MHz下)不应用于超过59s的延时

  • 控制寄存器CTRL

    用于使能 CYCCNT 等功能

  • 其他寄存器用于进行单片机的高级统计,如异常次数等,此处用不上,不再介绍

DWT延时代码实现

  • 将下列代码添加到你的工程中,记得修改对应单片机型号的头文件与当前CPU主频,即可使用DWT进行延时操作

  • delay.h
    #ifndef __DELAY_H
    #define __DELAY_H
    
    #include <stdint.h>
    
    void delay_us(uint32_t us);
    void delay_ms(uint32_t ms);
    void DWT_Init(void);
    
    #endif
    
  • delay.c
    #include "delay.h"
    #include "stm32f10x.h"//换成对应型号
    
    void DWT_Init(void) 
    {
    	if (!(CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk)) 
    	{
    		CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    		DWT->CYCCNT = 0;
    		DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    	}
    }
    
    void delay_us(uint32_t us) 
    {
    	uint32_t start = DWT->CYCCNT;
    	uint32_t cycles = us * 72;//72MHz
    	while ((DWT->CYCCNT - start) < cycles){
    	}
    }
    
    void delay_ms(uint32_t ms) {
    	while(ms--) {
    		delay_us(1000);
    	}
    }
    

posted on 2026-01-03 15:21  无术师  阅读(12)  评论(0)    收藏  举报