在上一篇文章中,我们已经介绍了整个软件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; //
}
Core Clock中配置mcu的主时钟频率,本人使用的H7 mcu,在时钟树中配置的主频为400MHz。然后打开Trace Enable。
然后打断点调试(注意,如果有些行不能打断点,可能需要调整代码编译优化等级)。
记录两个端点的时刻,然后相减就是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)
。
最终测得波形比较准确。
总结
1、当制作一个延时函数的时候(无论什么方式),当延时足够小的时候,我们就很有必要考虑这个延时函数内部代码本身运行的耗时。
2、很多地方计算SysTick->LOAD的时候会最终减1,这个应该是参考了HAL库函数里的写法。但是如果是上文中的mcu,1个计数就是0.02us,可根据实际情况决定是否减1。