无符号数减法在嵌入式开发中的应用

1. 背景

在嵌入式软件开发中我们对无符号数使用有特别的偏好 -‘遵从能省则省的原则,无符号能表示更大的数字’,这是由于嵌入式开发相对于其它资源仍然较为紧缺,不可避免会使用无符号数的减法,当”a-b;a<b”无论是过程还是结果你都可能不存在疑问而且这个结果也能被很好的理解;但当”a-b;a<b”无论是结果还是过程可能存在一些疑惑,例如“a、b都为无符号数,但这个结果是有符号还是无符号?”。a-b仍较为抽象,下面以一个更加实际的遇到的问题进行展开:

在嵌入式开发中我们经常会遇到计时这类开发需求,常规做法是使用一个硬件计时器,这个硬件定时器会以设定频率计数(受限于硬件定时器性能这个计数器的最大值一般不会太大)计数值从零到最大值,然后溢出又从零开始计数到最大值,依次从零到最大值反复循环计数;当我们需要测量一段代码的执行耗时,只需要在执行代码前获取定时器当前计数值t1,这段代码执行结束后再次获取定时器当前计数值t2,这段代码的耗时dt即为t2-t1,大多数情况t2-t1会得到预期的结果,但这里面存在一种特殊情况:当t2<t1时你会怎么完成这段耗时代码的计算?

 

为了解决“t2-t1且t2<t1”这种特殊情况,你是否是使用以下代码:

uintx_t dt=0;if(t2<t1){    dt = 0xffff-t2+t1 ; //这里假定计数器的最大值为0xffff  }else{     dt = t2-t1 ;}
    当前上面计算耗时的结果是完全正确的,但这个步骤显得略微复杂,我们能不能简化上述的表示式呢?下面代码也可实现同样的功能:
 uintx_t dt = t2-t1 ;//uintx_t 表示与t1/t2同类型
    这里将上面数行计算t2-t1代码直接压缩到了1行,两者结果完全一样,都没有任何的异常结果的输出。或许你可能有疑问第二个表达式没有处理任何溢出为什么这两者结果会是一样?
     为更好理解第二个表达式,我们对“t2-t1且t2<t1”表达式分别进行计时3个单位直接进行测试,
测试1源码:
#include "stdio.h"#include "stdint.h"int main(){    {        uint8_t t1 = 0xff-1 ,t2 = 1;        printf("uint8:%u-%u=%urn",t2,t1,t2-t1);    }    {        uint16_t t1 = 0xffff-1 ,t2 = 1;        printf("uint16:%u-%u=%urn",t2,t1,t2-t1);    }    {        uint32_t t1 = 0xffffffff-1 ,t2 = 1;        printf("uint32:%u-%u=%urn",t2,t1,t2-t1);    }
测试2源码(在测试1基础上将结果输出为有符号):
#include "stdio.h"#include "stdint.h"int main(){    {        uint8_t t1 = 0xff-1 ,t2 = 1;        printf("uint8:%u-%u=%drn",t2,t1,t2-t1);    }    {        uint16_t t1 = 0xffff-1 ,t2 = 1;        printf("uint16:%u-%u=%drn",t2,t1,t2-t1);    }    {        uint32_t t1 = 0xffffffff-1 ,t2 = 1;        printf("uint32:%u-%u=%drn",t2,t1,t2-t1);    }
}
测试2运行结果:

图片

测试3源码(在测试1基础上将结果输出为强制转换成t1、t2同类型):
#include "stdio.h"#include "stdint.h"int main(){    {        uint8_t t1 = 0xff-1 ,t2 = 1;        printf("uint8:%u-%u=%urn",t2,t1,(uint8_t)(t2-t1));    }    {        uint16_t t1 = 0xffff-1 ,t2 = 1;        printf("uint16:%u-%u=%urn",t2,t1,(uint16_t)(t2-t1));    }    {        uint32_t t1 = 0xffffffff-1 ,t2 = 1;        printf("uint32:%u-%u=%urn",t2,t1,(uint32_t)t2-t1);    }
}
测试3运行结果:
图片
在测试1中虽然都是计算耗时3个单位,但是测试1的第一个结果和第二个结果虽然不一样但是从有符号数的角度这个结果是能够被理解的,测试1的第三个结果虽然满足计时的需要,但是理解上造成了一些困难—“小数减去大数其结果为什么是正数?”。测试2仅仅将计算结果转换成了有符号数,更加方便对第一个结果和第二个结果进行理解。测试3在测试1的基础上将测试结果全部强制转换成使用无符号的数据类型,其结果全部满足计时需求。

2. 无符号数减法原理

虽然我们经常使用了无符号数进行减法运算,但其实无符号数不能直接进行减法运算,无符号数进行减法运算前会将其转换成有符号数进行运算,以下为一段验证在PC上测试源码:

#include "stdio.h"#include "stdint.h"
int main(){ uint8_t a = 1; uint16_t b = 1; uint32_t c = 1; printf("sizeof(uint8):%u sizeof(-uint8)=%urn",sizeof(a),sizeof(-a)); printf("sizeof(uint16):%u sizeof(-uint16)=%urn",sizeof(b),sizeof(-b)); printf("sizeof(uint32):%u sizeof(-uint32)=%urn",sizeof(c),sizeof(-c));}

图片

从上面结果看到无论无符号数是两个字节还是一个字节,在进行减法时都会被转换成四字节数。无符号数在进行减法计算时其定义域已经扩充到int32上,输出结果的值域同样被扩充到了int32上,当我们搞清楚这个表达式的定义域和值域空间后,在结合W001 – 处理器是如何将整数减法运算转换成整数加法运算的?,其结果就能被很好地理解了。
为了进一步加深对无符号数减法的理解,我们可以反汇编代码进行分析理解,以下是一段在coterx-m4f上的无符号数的反汇编源码:

 

在上述反汇编中,能够看到即使是uint8大小的数也会被加载到一个32位的寄存器(32位处理器中这个寄存器为32位)中,然后全部计算都是基于32位进行展开,从函数的角度这个原始定义域即使为8位但都会被转换成32位开始计算,当然这个值域默认情况下也是32位的,这也是我们在使用uint8或者uint16进行减法可能会产生难以排查bug的根源。
上面我们对无符号数减法可能产生什么样的错误、如何得到预期的答案以及为什么会产生这样的非预期答案,但在测试2uint32中已按有符号数输出了结果,但是结果仍产生了不可理解的正数。

3.测试2中uint32无符号减法为什么没有产生负数结果

通过上面原理分析我们已经知道,在计算过程中会优先转成成有符号int32类型(有符号),但测试2的uint32数据已经超出了int32类型所能超过范围,这样导致-t1其实已经变成了一个正数,结果变成了两个正数相加,所以这种情况下没有产生负数,对结果的没有进行值域限制结果仍然是正常的。

#include "stdio.h"#include "stdint.h"int main(){    {        uint8_t t1 = 0xff-1 ,t2 = 1;        printf("uint8:%u-%u=%drn",t2,t1,t2-t1);    }    {        uint16_t t1 = 0xffff-1 ,t2 = 1;        printf("uint16:%u-%u=%drn",t2,t1,t2-t1);    }    {        uint32_t t1 = 0xffffffff-1 ,t2 = 1;        printf("uint32:%u-%u=%drn",t2,t1,t2-t1);    }
}

    测试2运行结果:

图片

原创文章,作者:guozi,如若转载,请注明出处:https://www.sudun.com/ask/88932.html

(0)
guozi的头像guozi
上一篇 2024年6月4日
下一篇 2024年6月4日

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注