【STM32】软件I2C控制频率

在上一篇文章中,我们已经介绍了整个软件I2C的实现原理,但是也遗留了一个问题,那就是I2C速率的控制,其实就是控制SCL信号的频率。

微秒级延时

在上篇文章中,我们使用了SysTick进行延时,具体如下:

    typedef enum
    {
        Standard_Mode = 100 * 1000,
        Fast_Mode = 400 * 1000,
        Fast_Mode_Plus = 1000 * 1000,
    } I2C_SW_Speed_Mode_e; //i2c速率 unit Hz

static void i2c_delay(I2C_SW_Speed_Mode_e speed_mode)
{
    uint32_t temp;
    SysTick->LOAD = SystemCoreClock / 8 / (speed_mode * 2);
    SysTick->VAL = 0X00; //
    SysTick->CTRL = 0X01; //
    do
    {
        temp = SysTick->CTRL; //
    }
    while((temp & 0x01) && (!(temp & (1 << 16)))); //
    SysTick->CTRL = 0x00; //
    SysTick->VAL = 0X00; //
}

关于SysTick延时的原理,可以参考这篇文章

HAL库下的systick 底层配置 HAL_Delay实现原理 微秒级延时(非中断)以及一些重写延时的小坑 关于HAL_Delay的使用问题_hal_inctick配置__zs_dawn的博客-CSDN博客

STM32入门:Systick(嘀嗒定时器)学习_我是混子我怕谁的博客-CSDN博客

在这里就说明一下Load的值怎么来的。

首先,Systick使用的是系统时钟的8分频,例如系统时钟为400MHz,8分频后为50MHz。

那么1/50000000秒即1/50微秒产生一个周期的振动。

所以,需要计数50次才会产生1微秒的时钟。也就是计数1次产生0.02微秒,精度就是0.02us。

延时验证

那么这个延时准确吗?我们怎么验证呢?网上一般都会有两种方式:

  • 使用示波器观察(电平翻转延时)
  • keil中使用st-link单步调试

在这里,我推荐使用keil中使用st-link单步调试的方案,至于为什么,我们下文再做解释。

首先,我们重写了一个函数

void delay_us(uint32_t delay)
{
    uint32_t temp;
    SysTick->LOAD = SystemCoreClock / 8 / 1000000 * delay;
    SysTick->VAL = 0X00; //
    SysTick->CTRL = 0X01; //
    do
    {
        temp = SysTick->CTRL; //
    }
    while((temp & 0x01) && (!(temp & (1 << 16)))); //
    SysTick->CTRL = 0x00; //
    SysTick->VAL = 0X00; //
}

【STM32】软件I2C控制频率_第1张图片

【STM32】软件I2C控制频率_第2张图片

Core Clock中配置mcu的主时钟频率,本人使用的H7 mcu,在时钟树中配置的主频为400MHz。然后打开Trace Enable。

然后打断点调试(注意,如果有些行不能打断点,可能需要调整代码编译优化等级)。

【STM32】软件I2C控制频率_第3张图片

记录两个端点的时刻,然后相减就是delay_us(1);语句运行的耗时。

0.02621103s - 0.02620955s = 0.00000148s = 1.48us
0.02623537s - 0.02623389s = 0.00000148s = 1.48us
0.02620512s - 0.02620364s = 0.00000148s = 1.48us

实测了3次,平均耗时为1.48us。那么与实际值还是有一定偏差呢,对于这个现象个人觉得是正常的,因为delay_us函数中除了延时代码,还有其它代码也有需要耗时的。那是不是其他耗时就是1.48us-1.00us=0.48us呢?

当我们delay_us(2);时,测试结果如下;

0.02617987s - 0.02617736s = 0.00000251s = 2.51us
0.02619555s - 0.02619303s = 0.00000252s = 2.52us
0.02618346s - 0.02618095s = 0.00000251s = 2.51us

好像也不是固定的。。。。所以除了一个固定计数延时,还有一个不固定的其他代码耗时。但是如果只关注微秒级别的值,那么0.xxus确实可以忽略不计。

但是如果是对于1MHz的时钟频率,半周期为0.50us,那这个代码本身运行耗时是没有办法忽略不计的。这也是为什么配置时钟频率越高的时候,实际偏差越大。

高频率时钟延时

上文说到,1MHz频率的时钟,半周期为0.50us,而我们1us计数的偏差都有0.48us,所以我们使用微秒级别的延时是远远不够的。那么不能直接使用上文的delay_us函数。

又由于其他代码自身有延时,所以我们的实际计数时间得减去一个偏置值,这个偏置值是一个经验值,和编译优化等级强相关。

#define CODE_BASE_DELAY_US  (0.40f)
#define I2C_SPEED_HZ_TO_DELAY_TIME_US(speed) (1000000.0/(speed*2)-CODE_BASE_DELAY_US)

    typedef enum
    {
        Standard_Mode = 100 * 1000,
        Fast_Mode = 400 * 1000,
        Fast_Mode_Plus = 1000 * 1000,
    } I2C_SW_Speed_Mode_e; //i2c速率 unit Hz

static void i2c_delay(I2C_SW_Speed_Mode_e speed_mode)
{
    float delay = I2C_SPEED_HZ_TO_DELAY_TIME_US(speed_mode);
	
    uint32_t temp;
    SysTick->LOAD = SystemCoreClock / 8 / 1000000 * delay;
    SysTick->VAL = 0X00; //
    SysTick->CTRL = 0X01; //
    do
    {
        temp = SysTick->CTRL; //
    }
    while((temp & 0x01) && (!(temp & (1 << 16)))); //
    SysTick->CTRL = 0x00; //
    SysTick->VAL = 0X00; //
    //printf("delay %0.3f us\r\n",delay);
}

结果

本人的编译优化等级选择的是-Ofast,然后#define CODE_BASE_DELAY_US (0.40f)

最终测得波形比较准确。
【STM32】软件I2C控制频率_第4张图片
【STM32】软件I2C控制频率_第5张图片

总结

1、当制作一个延时函数的时候(无论什么方式),当延时足够小的时候,我们就很有必要考虑这个延时函数内部代码本身运行的耗时。
2、很多地方计算SysTick->LOAD的时候会最终减1,这个应该是参考了HAL库函数里的写法。但是如果是上文中的mcu,1个计数就是0.02us,可根据实际情况决定是否减1。

你可能感兴趣的:(STM32,stm32,嵌入式硬件,单片机)