cisen / blog

Time waits for no one.
135 stars 20 forks source link

三极管 相关 #930

Open cisen opened 4 years ago

cisen commented 4 years ago

总结

b03533fa828ba61ef00da9864434970a314e59b3

二极管

与或非

https://blog.csdn.net/yedongnan001/article/details/79471329 与门、或门和非门是数字电路中常用的三个器件,这里以三极管为例说明他们的实现原理。

  1、与门

20180307150622902

  分析:当输入1为高电平+5V,输入2也为高电平+5V时,Q11基极通过R24接到+5V,发射极接地,形成回路,Q11导通,Q9发射极接+5V,基极通过R19以及导通的Q11接地,形成回路,从而Q9导通,导通后Q9的发射极和集电极压降为0.3V,则输出点电压为+4.7V,即为数字量1;当输入1为低时,Q11集电极和发射极未导通,Q9的发射极和基极无法形成回路,则Q9的集电极和发射极不能导通,输出被拉到地端,即输出数字量0;输入2为低时,则Q9的Ueb不可能大于0.7V,即Q9的发射极与集电极不能导通,输出点电平被钳位到地端,即低电平。

  2、或门

20180307150228440

  分析:当输入1或输入2为高电平+5V时,Q6的基极接到+5V,发射极接地,Q6的集电极和发射极导通,导通后压降Uce为0.3V,则输出点电压为4.7V,即输出点输出数字量1;当输入皆为低电平时,Ube <0.7V,Q6集电极和发射极不能导通,输出被拉到地端,输出为低电平。

  3、非门

20180307150254592

  分析:当输入为高电平+5V时,Q1基极与发射极间Ube> 0.7V,Q1导通,输出点电压为Q1的集电极和发射极之间的压降,即0.3V,即输出为数字量0;当输入为低时,Q1集电极和发射极之间未导通,输出电压为上拉的电压,+5V,即数字量1。

cisen commented 4 years ago

实现加减乘除

说明

计算机系统中,数值一律用补码来表示:因为补码可以使符号位和数值位统一处理,同时可以使减法按照加法来处理。

对补码做简单介绍:数值编码分为原码,反码,补码,符号位均为0正1负。

原码 -> 补码: 数值位取反加1

补码 -> 原码: 对该补码的数值位继续 取反加1

补码 的绝对值(称为真值):正数的真值就是本身,负数的真值是各位(包括符号位)取反加1(即变成原码并把符号位取反).

b -> -b : 各位(包括符号位)取反加1

解决方案

需要熟练掌握一些常见功能的位操作实现,具体为:

<1> 常用的等式:-n = ~(n-1) = ~n+1 <2> 获取整数n的二进制中最后一个1:n&(-n) 或者 n&~(n-1),如:n=010100,则-n=101100,n&(-n)=000100 <3> 去掉整数n的二进制中最后一个1:n&(n-1),如:n=010100,n-1=010011,n&(n-1)=010000 ## 加法运算 将一个整数用二进制表示,其加法运算就是:相异(^)时,本位为1,进位为0;同为1时本位为0,进位为1;同为0时,本位进位均为0. - **转换位二进制,递归相加完进位(carry)就完成加法** 所以,不计进位的和为sum = a^b,进位就是arr = a&b,(与sum相加时先左移一位,因为这是进位)。完成加法直到进位为0. ## 减法运算 a-b = a+(-b)  根据补码的特性,各位取反加1即可(注意得到的是相反数,不是该数的补码,因为符号位改变了) - **换成补码,照加法递归** (上面用二进制实现的加减法可以直接应用于负数) ## 乘法运算 原理上还是通过加法计算。**将b个a相加**,注意下面实际的代码。 ## 除法运算 除法运算是乘法的逆。看a最多能减去多少个b ## JS版 ```js /** * 向右移动小数点 ,相当于乘10的_pointcount次幂 * @param _number 操作的数字 * @param _pointcount 向右移动的位数 * @returns 移动以后的数字(number类型) */ var moveRight = function (_number, _pointcount) { if (Number(_number) !== Number(_number)) { return _number; } var flag = 1; if (_number < 0) { flag = -1; _number = _number * -1; } var numStr = _number.toString(); var strArr = numStr.split("."); var rightStr = strArr.length > 1 ? strArr[1] : ""; //在不足移动的位数后补0; if (rightStr.length < _pointcount) { var addZero = []; for (var i = rightStr.length; i < _pointcount; i++) { addZero.push("0"); } rightStr = rightStr + addZero.join(""); } return Number(strArr[0] + rightStr.substring(0, _pointcount) + "." + rightStr.substr(_pointcount)) * flag; }; /** * 向左移动小数点,相当于_number除以10的_pointcount次幂 * @param _number 操作的数字 * @param _pointcount 向左移动的位数 * @returns 移动以后的数字(number类型) */ var moveLeft = function (_number, _pointcount) { //判断是否为数字,非数字直接返回本身 if (Number(_number) !== Number(_number)) { return _number; } var flag = 1; if (_number < 0) { flag = -1; _number = _number * -1; } //number转换为string ,方便后续操作。 var numStr = _number.toString(); //按照小数点分隔字符串, var strArr = numStr.split("."); var num = 0, zeroArr = []; //生成移动位数+1的0 ,补到分隔后的第一个字符串中,兼容0.XXX的情况。开头多余的零会在调用Number方法时去掉。 while (num < _pointcount + 1) { zeroArr.push("0"); num++; } var leftStr = zeroArr.join("") + strArr[0]; var leftArr = leftStr.split("") //插入移动后的小数点。 leftArr.splice((leftStr.length - _pointcount), 0, '.'); var result = leftArr.join(""); //拼接原始值小数点后的数字 if (strArr.length > 1) { result += strArr[1]; } return Number(result) * flag; }; /** * 加法 * @param _num1 * @param _num2 * @param precision 精度 * @returns {*} */ var add = function (_num1, _num2, precision) { if (!precision) { precision = 2; } return moveLeft(moveRight(_num1, precision) + moveRight(_num2, precision), precision) }; /** * 减法 * @param minuend * @param reduction * @returns {*} */ var reduce = function (minuend, reduction, precision) { if (!precision) { precision = 2; } return moveLeft(moveRight(minuend, precision) - moveRight(reduction, precision), precision) }; /** * 乘法 * @param _num1 * @param _num2 * @returns {number} */ var multiplication = function (_num1, _num2, precision) { if (!precision) { precision = 2; } return moveLeft(moveRight(_num1, precision) * moveRight(_num2, precision), precision * 2) }; /** * 除法 * @param _num1 被除数 * @param _num2 除数 * @param precision 精度 整形,几位小数 * @returns {number} */ var division = function (_num1, _num2, precision) { if (!precision) { precision = 2; } return moveRight(_num1, precision) / moveRight(_num2, precision); }; console.log(moveRight(0.9999999,3)); console.log(moveLeft(88888888888,2)); console.log(add(1.01,7.079,8)); console.log(reduce(8.01,1,2)); console.log(multiplication(1,8.88,2)); console.log(division(16,2,2)) ```
cisen commented 4 years ago

https://blog.csdn.net/YPJMFC/article/details/78246971

我们知道,计算机最基本的操作单元是字节(byte),一个字节由8个位(bit)组成,一个位只能存储一个0或1,其实也就是高低电平。无论多么复杂的逻辑、庞大的数据、酷炫的界面,最终体现在计算机最底层都只是对0101的存储和运算。因此,了解位运算有助于提升我们对计算机底层操作原理的理解。

今天就来看看怎么不使用显式“ + - * /”运算符来实现加减乘除运算。

下面我们一个一个来看。

  1. 加法运算 先来个我们最熟悉的十进制的加法运算:

13 + 9 = 22 1 我们像这样来拆分这个运算过程:

不考虑进位,分别对各位数进行相加,结果为sum: 个位数3加上9为2;十位数1加上0为1; 最终结果为12;

只考虑进位,结果为carry: 3 + 9 有进位,进位的值为10;

如果步骤2所得进位结果carry不为0,对步骤1所得sum,步骤2所得carry重复步骤1、 2、3;如果carry为0则结束,最终结果为步骤1所得sum: 这里即是对sum = 12 和carry = 10重复以上三个步骤,(a) 不考虑进位,分别对各位数进行相加:sum = 22; (b) 只考虑进位: 上一步没有进位,所以carry = 0; (c) 步骤2carry = 0,结束,结果为sum = 22. 我们发现这三板斧行得通!

那我们现在还使用上面的三板斧把十进制运算放在二进制中看看是不是也行的通。

13的二进制为0000 1101,9的二进制为0000 1001:

不考虑进位,分别对各位数进行相加: sum = 0000 1101 + 0000 1001 = 0000 0100

考虑进位: 有两处进位,第0位和第3位,只考虑进位的结果为: carry = 0001 0010

步骤2carry == 0 ?,不为0,重复步骤1 、2 、3;为0则结束,结果为sum: 本例中, (a)不考虑进位sum = 0001 0110; (b)只考虑进位carry = 0; (c)carry == 0,结束,结果为sum = 0001 0110 转换成十进制刚好是22.

我们发现,适用于十进制的三板斧同样适用于二进制!仔细观察者三板斧,大家能不能发现其实第一步不考虑进位的加法其实就是异或运算;而第二部只考虑进位就是与运算并左移一位–;第三步就是重复前面两步操作直到第二步进位结果为0。

这里关于第三步多说一点。为什么要循环步骤1、 2、 3直到步骤2所得进位carry等于0?其实这是因为有的数做加法时会出现连续进位的情况,举例:3 + 9,我们来走一遍上述逻辑:

a = 0011, b = 1001;
start;

first loop;
1.1 sum = 1010
1.2 carry = 0010
1.3 carry != 0 , go on;

second loop;
2.1 sum = 1000;
2.2 carry = 0100;
2.3 carry != 0, go on;

third loop;
3.1 sum = 1100;
3.2 carry = 0000;
3.3 carry == 0, stop; result = sum;

end 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 如上面的例子,有的加法操作是有连续进位的情况的,所以这里要在第三步检测carry是不是为0,如果为0则表示没有进位了,第一步的sum即为最终的结果。

有了上面的分析,我们不难写出如下代码:

// 递归写法 int add(int num1, int num2){ if(num2 == 0) return num1; int sum = num1 ^ num2; int carry = (num1 & num2) << 1; return add(sum, carry); }

// 迭代写法 int add(int num1, int num2){ int sum = num1 ^ num2; int carry = (num1 & num2) << 1;
while(carry != 0){ int a = sum; int b = carry; sum = a ^ b; carry = (a & b) << 1;
} return sum; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 我们的计算机其实就是通过上述的位运算实现加法运算的(通过加法器,加法器就是使用上述的方法实现加法的),而程序语言中的+ - * /运算符只不过是呈现给程序员的操作工具,计算机底层实际操作的永远是形如0101的位,所以说位运算真的很重要!

  1. 减法运算 我们知道了位运算实现加法运算,那减法运算就相对简单一些了。我们实现了加法运算,自然的,我们会想到把减法运算11 - 6变形为加法运算11 + (-6),即一个正数加上一个负数。是的,很聪明,其实我们的计算机也是这样操作的,那有的人会说为什么计算机不也像加法器一样实现一个减法器呢?对的,这样想当然是合理的,但是考虑到减法比加法来的复杂,实现起来比较困难。为什么呢?我们知道加法运算其实只有两个操作,加、 进位,而减法呢,减法会有借位操作,如果当前位不够减那就从高位借位来做减法,这里就会问题了,借位怎么表示呢?加法运算中,进位通过与运算并左移一位实现,而借位就真的不好表示了。所以我们自然的想到将减法运算转变成加法运算。

怎么实现呢?

刚刚我们说了减法运算可转变成一个正数加上一个负数,那首先就要来看看负数在计算机中是怎么表示的。

+8在计算机中表示为二进制的1000,那-8怎么表示呢?

很容易想到,可以将一个二进制位(bit)专门规定为符号位,它等于0时就表示正数,等于1时就表示负数。比如,在8位机中,规定每个字节的最高位为符号位。那么,+8就是00001000,而-8则是10001000。这只是直观的表示方法,其实计算机是通过2的补码来表示负数的,那什么是2的补码(同补码,英文是2’s complement,其实应该翻译为2的补码)呢?它是一种用二进制表示有号数的方法,也是一种将数字的正负号变号的方式,求取步骤:

第一步,每一个二进制位都取相反值,0变成1,1变成0(即反码)。

第二步,将上一步得到的值(反码)加1。

简单来说就是取反加一!

关于补码更详细的内容可参维基百科-补码,这里不再赘述。

其实我们利用的恰巧是补码的可以将数字的正负号变号的功能,这样我们就可以把减法运算转变成加法运算了,因为负数可以通过其对应正数求补码得到。计算机也是通过增加一个补码器配合加法器来做减法运算的,而不是再重新设计一个减法器。

以上,我们很容易写出了位运算做减法运算的代码:

/*

(1) 计算绝对值得乘积 (2) 确定乘积符号(同号为证,异号为负) 1 2 有了这个思路,代码就不难写了:

/*

考虑我们现实生活中手动求乘积的过程,这种方式同样适用于二进制,下面我以13*14为例,向大家演示如何用手动计算的方式求乘数和被乘数绝对值的乘积。

这里写图片描述 从上图的计算过程可以看出,如果乘数当前位为1,则取被乘数左移一位的结果加到最终结果中;如果当前位为0,则取0加到乘积中(加0也就是什么也不做);

整理成算法步骤:

(1) 判断乘数是否为0,为0跳转至步骤(4) (2) 将乘数与1作与运算,确定末尾位为1还是为0,如果为1,则相加数为当前被乘数;如果为0,则相加数为0;将相加数加到最终结果中; (3) 被乘数左移一位,乘数右移一位;回到步骤(1) (4) 确定符号位,输出结果; 1 2 3 4 代码如下:

int multiply(int a, int b) {   //将乘数和被乘数都取绝对值  int multiplicand = a < 0 ? add(~a, 1) : a;    int multiplier = b < 0 ? add(~b , 1) : b;     //计算绝对值的乘积   int product = 0;   while(multiplier > 0) {     if((multiplier & 0x1) > 0) {// 每次考察乘数的最后一位     product = add(product, multiplicand);     }      multiplicand = multiplicand << 1;// 每运算一次,被乘数要左移一位     multiplier = multiplier >> 1;// 每运算一次,乘数要右移一位(可对照上图理解)   }    //计算乘积的符号   if((a ^ b) < 0) {     product = add(~product, 1);   }    return product; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 显而易见,第二种求乘积的方式明显要优于第一种。

  1. 除法运算 除法运算很容易想到可以转换成减法运算,即不停的用除数去减被除数,直到被除数小于除数时,此时所减的次数就是我们需要的商,而此时的被除数就是余数。这里需要注意的是符号的确定,商的符号和乘法运算中乘积的符号确定一样,即取决于除数和被除数,同号为证,异号为负;余数的符号和被除数一样。 代码如下:

/*

上面的代码之所以比较慢是因为步长太小,每次只能用1倍的除数去减被除数,所以速度比较慢。那能不能增大步长呢?如果能,应该怎么增大步长呢?

计算机是一个二元的世界,所有的int型数据都可以用[2^0, 2^1,…,2^31]这样一组基来表示(int型最高31位)。不难想到用除数的2^31,2^30,…,2^2,2^1,2^0倍尝试去减被除数,如果减得动,则把相应的倍数加到商中;如果减不动,则依次尝试更小的倍数。这样就可以快速逼近最终的结果。

2的i次方其实就相当于左移i位,为什么从31位开始呢?因为int型数据最大值就是2^31啊。

代码如下:

int divide_v2(int a,int b) {
// 先取被除数和除数的绝对值
int dividend = a > 0 ? a : add(~a, 1);
int divisor = b > 0 ? a : add(~b, 1);
int quotient = 0;// 商
int remainder = 0;// 余数
for(int i = 31; i >= 0; i--) { //比较dividend是否大于divisor的(1<<i)次方,不要将dividend与(divisor<<i)比较,而是用(dividend>>i)与divisor比较, //效果一样,但是可以避免因(divisor<<i)操作可能导致的溢出,如果溢出则会可能dividend本身小于divisor,但是溢出导致dividend大于divisor
if((dividend >> i) >= divisor) {
quotient = add(quotient, 1 << i);
dividend = substract(dividend, divisor << i);
}
}
// 确定商的符号
if((a ^ b) < 0){ // 如果除数和被除数异号,则商为负数
quotient = add(~quotient, 1);
}
// 确定余数符号
remainder = b > 0 ? dividend : add(~dividend, 1);
return quotient;// 返回商 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 好了,以上。