运算符%

作者: 鱼鹰谈单片机 | 来源:发表于2018-12-18 18:44 被阅读0次

    说完位运算,再说说其他的运算符。+ - * / 不用多说,应该都比较清楚,但是还是要注意的就是使用 / 进行整型变量的计算时,它不像平常一样可以得到小数的,而只有整数部分,并没有小数。还有就是各个运算符的顺序,如果不确定哪个先运算,不如加上括号()吧,不用担心效率的问题,因为加了括号只是告诉编译器该如何处理这条语句而已。另外使用#define定义一些表达式的时候也最好加上括号,因为你不能确定你这个宏定义会在什么地方使用,为了安全起见还是加上比较好,这些内容在宏定义小节将进行更详细的说明。
    现在来重点说说 % 取余运算。这个运算就很有意思了。如果从51单片机过来的,看到这个运算符最多的地方就是在为定时器赋值时将高低字节进行分离了,还有在数码管实验中将一个数分离成十进制的。这些都是很常见的应用。但是其实更广泛的应用不在此。
    编程的时候,很多时候都会要求一个数在某一个范围内进行反复循环,0100循环,05循环等等。一般的方法是使用if语句,当判断达到最大值的时候回到开始处。实际上使用这种方法也是可以的,但是如果有更简单更高效的方法你是否还会使用if语句呢。
    先说说使用 & 的方法吧。比如说我想让一个数在0~7内循环,该如何做呢?temp = (temp++)&0x07,如此就简单的实现了07循环。因为要实现07的循环,其实只要提取一个变量递增的低三位即可。不管这个变量如何变化,它的低三位始终都是在07循环变化的。同理,它也可以实现015、0~31变化。但是这个方法有局限,它只能按照连续bit位的最大值进行循环。
    现在再说 %,这个就厉害了,它不存在这个限制。可以在0任意数循环。比如05循环,只要temp = (temp++)%6(注意是6而不是5),那么temp就会在0~5之间循环了(这是我在看循环队列的时候看到的方法,当时很是震惊,关于循环队列更多的东西将在循环队列中讲解)。
    很神奇吧,更神奇的是使用它还可以计算两个变量之间的距离(因为距离时没有负数的,这里我称之为距离可能不是很好理解,慢慢来)我们知道不管是8bit数据,16bit、32bit、64bit,它始终有一个位数的限制,如何在有限的位数里面获得两个数据之间准确距离信息呢。以8bit为例,最大数为255,第一次读取为0x4,第二次读取是0x9,那么从0x04变化到0x09,变化了几次(递增数为1)? 9 – 4 = 5,如果变化后的数小于255当然好办,但是超过了255,又从0开始递增呢?这个时候又该如何。比如说一开始读取的是251,之后再读一次,变成了1,怎么算,251-1=250?肯定不对,1-250=-250,更不是?那到底变化了几次?252、253、254、255、0、1,这里可以看出是6,但是该怎么计算,又是否有一个公式可以在不改变原来数据变化的情况下将超过限制和没超过限制这两种情况的计算包含呢。有的。就是length = (num2 – num1 + max)%max。关于这个公式更详细内容请看循环队列小节。
    确实,使用%可以在任意数之间循环,但是她也有限制,就是目前来看只能实现递增1的情况,我想递减呢?就是说我想从9减为0,然后从9开始继续,又该如何。我的一个项目就需要这样的变化,怎么办,如果用if确实能够解决问题,但是直觉告诉我,肯定有简单方法实现,所以我就上网搜,但是可能我搜索的方法不对,始终没有搜到,所以我暂时搁置了。直到一天夜里,回想递增循环的情况,慢慢的思考其中的本质,再结合距离计算的公式,突然明悟了。就是num--; num = (num+max)%max;这样两条语句去实现。比如9~0,就是num--; num = (num+10)%10;
    那么怎么理解呢,按理说0再自减就是0xff,即255,再加10就是265,265%10 =5,怎么就变成了9呢?这和存储有关。255从有符号的角度来看,就是-1,-1+10等于9,9%10=9,没错,就是如此。但是我的变量声明不是有符号的,而是无符号的,怎么也没有出问题呢?这是因为溢出了,因为265在8bit情况下溢出就变成了9,所以计算也不会出现问题。所以这些计算机基础方面的东西一定要理解透彻清晰,才能更好的驾驭一门语言,你也会发现其中的东西真的很神奇。
    --------------------------------------------------------------2018/10/14 Osprey

    在运用的时候发现,其实当最大值就是溢出值时,可以简化这个公式。很多时候单片机都需要一个运行时间信息,一般的处理方法就是使用if语句进行延时,但是实际上不需要如此麻烦。
    比如说STM32单片机,16位定时器,分频时间根据你需要延时的最大时间进行设置。比如最大所需延时6s,16位定时器最大值65535,那么在定时器时钟频率72M情况下,分频系数设置为7200-1(分频系数越大,定时精度越小,所以要根据最大延时间确定分频系数),那么最大溢出时间为6,553,600 us,即6.5536 s,这样就可以让定时器一直处于运行状态,即使到达最大值也会重新从0开始计数,这样就可以不需要变量也可以准确获取时间(如果定时时间很长,即使使用最大分频65535也不能得到最大延时,又不想增加变量在溢出中断中进行计时怎么办,可以采用定时器主从模式,用一个定时器给另一个分频)
    既然定时器的寄存器CNT一直在0~65535循环变化,那么就可以通过获得该时间来实现延时的效果。根据前面的内容,使用 (time – CNT + 65535)%65535就可以获得从定时开始到现在的时间了(time为16位变量,该值为开始定时那一刻CNT的值)。
    比如现在要定时1s,那么首先获取当前的CNT值,然后等待,如

    time = CNT; // 更新当前时间
    while((CNT- time + 65536) % 65536 < 10,000);
    

    这样,当1s时间到的时候就可以跳出循环了,当然了可以使用if语句,这样就不会卡死在这里,只有时间到的情况下才会进入。如:

    if(( CNT- time + 65536) % 65536 > (10,000 – 1))
    {
        Time = CNT;    // 更新当前时间,用于下一次定时
        // 1 s 时间到,处理事件
    }
    

    但是你是否发现65536是一个很特殊的数字,定时器因为位数的限制,始终都在0~65535内循环,不需要使用 % 来让它循环,那么以上的判断语句就可以简化成这样

    time = CNT; // 更新当前时间
    while((uint16_t) (CNT- time + 65536) < 10,000);
    // 和
    if((uint16_t) (CNT- time + 65536) > (10,000 – 1))
    {
        Time = CNT;    // 更新当前时间,用于下一次定时
        // 1 s 时间到,处理事件
    }
    

    从效率上来说明显后者更高,不信的话可以自己测试。
    但是需要注意的是 (time –CNT + 65536) & 65535 和 (time –CNT + 65536) % 65536之间的区别,虽然这两种写法都可以使计算的值在0~65535之间循环,也等效,但是明显后者效率稍低一些,因为前面乃是位运算。但是呢,后者的好处就像前面说的,没有限制。
    另外再说一点,STM32F4系列单片机有一个DWT模块,在当systick时钟被操作系统(如ucos)使用的时候,可以使用它作为延时时钟,延时精度为系统时钟频率(纳秒级),而且是32位的,延时时间也够长,不用白不用啊!
    ------------------------------------------------------更新于2018/11/19 Osprey

    相关文章

      网友评论

        本文标题:运算符%

        本文链接:https://www.haomeiwen.com/subject/analkqtx.html