STM32 SYSTICK定时器常见问题
2024-08-29 来源:elecfans
我们知道,STM32库函数里通常使用来自内核的系统定时器SYSTICK作为时基,实现计数延时。一般来讲,ST公司提供的库函数里将SYSTICK定时器配置为1ms的定时器中断,每产生1ms中断则相关中断事件计数变量加一。具体应用中我们经常会调用那个Delay()函数以实现计数定时,做延时或超时管理。
有人在阅读ST提供的LL库里的这个延时函数时,发现代码里对延时参数总是做了个加1操作,代码如下:
上图中红色代码,程序进来后就对给定的延时参数做了个加1操作,这不将1ms延时变成2ms了吗?
其实,这个地方已经有做了注释,就是为了保证有1个最小的延时等待,函数参数给定1ms的延时,经过这样加1操作后就能保证至少1ms的实际延时,极限情况的确可能达到2ms,但不会超过2ms。但如果这里不做加1操作,函数参数给定1ms的延时,实际延时最短的情况的极限是0,最长的极限是1ms.
我们不妨借助下面图形一起看看,可能更直观点。
假设调用Delay函数时给定延时参数为3,首先读到的滴答中断计数值为5,显然在上图的绿色区除了两个边界点外的任一时刻读到的数据都是5。如果先不对延时参数加1,那么实际延时就落在(2ms,3ms)之间;如果对延时参数先加1,那么实际延时就落在(3ms,4ms)之间。换言之,拟定延时n个ms,若做加一操作,实际延时落在(n,n+1)ms开区间;若不做加1操作,实际延时落在(n-1,n)ms开区间。
那这个的加1操作是必须的吗?我不这么认为,加不加1没有原则性问题。当我们把该定时器的溢出中断时间定好后,精度就定了,以1ms中断为例,我们就得接受1ms内的误差。这个地方知道怎么回事就好,毕竟中断代码的编写、延时参数的拟定都是我们自己定的。
上面代码是ST提供的LL库里关于那个延时函数的写法,看看HAL库的写法是不是一样的,不妨顺便看看。【下面截图便是Hal_Delay()函数代码】
显然,在HAL库里的Delay函数里进来后也首先做了加1操作,跟LL库的做法一样。
上面HAL库延时函数代码里的Tickstart是调用Delay()函数时首次读到的滴答中断计数变量的值,wait是延时值,Hal_GetTick()函数动态读取到的滴答中断计数变量的值【uwTick】。对于上面while语句里面的uwTick与Tickstart的比较的写法,有人可能会产生疑问。当Hal_GetTick()函数读取到的滴答中断累加值uwTick小于或等于Tickstart时还能得出正确的比较结果吗?
比方像下面这种情况,某无符号整型变量循环累加计数,现在需求得Val2与Val1的差值以测试信号宽度。
假定上面数据的计数周期为T,且Val2与Val1的间隔不超过1T。此时二者的间隔用数学表达式就是:Val2+T-Val1。这里的代码似乎没有考虑这个溢出周期的问题?可是那个uwTick完全可能出现小于tickstart的情况啊。
这是怎么回事呢?我也一度很怀疑这个写法,当时也做了些简单测试。对于测试结果,现在回想起来当时也没做冷静的分析而做出了错误的判断。
回过头来想,按理说这个代码不该有问题。毕竟是老代码了,何况之前也没有人反映这个地方有问题。难道哪里误会了?
因为当前代码用到的几个变量都是32位无符号数,刚开始测试时也是基于32位数做的,结果动不动就很大,不容易判断正误。我干脆将相关数据全部改为8位无符号数,这样测试起来就方便些。
我先准备了下面的0~255的循环计数表格。显然计数溢出周期为256。
现在要在任意一个不大于1个计数周期内求取任意2个数据之间隔,即求这两个无符号数之差。假设计算上图中第1行数据5与第2行数据3的间隔值。这里用StartValue表示5,NowValue表示3,Result表示结果,都定义为8位无符号类型。
参照库代码写法,代码该这样写:Result1=NowValue – StartValue;
按照我的想法,代码该这样写: Result2= NowValue + 256 – StartValue;
经过测试,结果是一样的,也不难验证是正确的。
对于这个结果,意外,但也意料之中。因为我在添加那个256周期值时心里就在犯嘀咕,有所警觉了。我现在是两个8位数的加减,加个256按理是不会影响结果的,这样推理下来,两行代码的结果就本该一样。
我们再回头看看上面HAL库函数中延时函数:
这里tickstart和wait以及函数HAL_GetTick()读到的变量是uwTick值都是32位无符号变量,显然,uwTick与tickstart的延时间隔也不会超过0xfffffff。
如果uwTick小于或等于tickstart,通常我们需要加上溢出周期,这个周期值等于32位数据模值,用16进制表示就是1后面跟8个0。经过测试发现下面代码的两种写法在微处理器的计算结果也是一样的。【uwTick对应下面截图代码中的NowValue, uwTick对应StartValue】
因为都定义成32位数据宽度了,结果很大,但是正确的。很明显上图中A行代码更简洁。看来,库代码这里这样写是没问题的。
上面两次测试除了位宽差别外,其它都一样。怎么感觉求算不超过1个计量周期宽度内任意两个数据的间隔值不用考虑那个溢出周期呢?
其实,只要是任意两个相同无符号数据类型数据做减法,且数据所能计量的最大值为其满量程值【比如8位对应255,16位对应65535】,求算任意一个周期内两个数据的间隔值时,使用上面A行代码写法是没有问题的,但这并不等于说不用考虑溢出周期的问题。
以现在测试为例。A行程序代码在微处理里完成的就是数学表达式NowValue 加上 溢出周期值 减去 StartValue的结果,我们不妨将A行代码的计算过程做个翻译解读就看得出来。只是B行代码里再多加1个溢出周期值也不影响计算结果。【注:前面两个测试的溢出周期值刚好等于各自所用数据宽度的模值。】
但实际应用中,数据变量的计数周期往往并不等于所用数据宽度的模值。此时前面A行代码的写法就行不通了,而要用B行代码写法。即A行代码写法只能用在特定场合,B行代码写法具有普适性。
我们不妨看看下面表格。这个表格跟前面0~255的表格类似,只是换成由一系列0~99的数据周期性排列而成。
同样,我们求第一行的数据5与第2行的数据3之间的间隔数,这里的数据都定义成uint8类型。显然这里计数周期为100,而非256,此时的代码就不能采用前面A行代码写法。
我们可以看看验证结果:【显然第一种写法的结果是错的,不难判断正确结果是98】
我们继续看一个实战性的例子,看一个有关使用TIMER测量脉冲宽度的例子。
下图斜线表示计数器计数方向,现在利用TIMER的捕获功能测量下面一段脉冲的高电平宽度。
假设TIMER为16位,计数器向上单向计数。于脉冲的上沿和下沿事件分别捕捉到两个计数值Val1和Val2,二者都定义成16位无符号整型变量。
结合图形可以看出,脉冲虽然跨越了溢出点,但脉冲宽度没有超过1个计数周期,Val2的值小于Val1。此时我们求算t1的宽度,正常都会这样写:
t1 = Val2+ ARR+1 - Val1;
当然,如果说你把ARR刚好设置为0xfffff, 此时程序代码就可以简化成 t1 = Val2 - Val1。
OK,关于STM32片内Systick定时器延时中断应用的两个小疑问就聊到这里。问题虽小,但可以牵扯出不少东西供探究。。