1.1i2c总线知识
1.1.1 i2c总线物理拓扑结构
i2c总线在物理连接上非常简单,分别由sda(串行数据线)和scl(串行时钟线)及上拉电阻组成。通信原理是通过对scl和sda线高低电平时序的控制,来产生i2c总线协议所需要的信号进行数据的传递。在总线空闲状态时,这两根线一般被上面所接的上拉电阻拉高,保持着高电平。
1.1.2 i2c总线特征
i2c总线上的每一个设备都可以作为主设备或者从设备,而且每一个设备都会对应一个唯一的地址(可以从i2c器件的数据手册得知),主从设备之间就通过这个地址来确定与哪个器件进行通信,在通常的应用中,我们把cpu带i2c总线接口的模块作为主设备,把挂接在总线上的其他设备都作为从设备。
i2c总线上可挂接的设备数量受总线的最大电容400pf 限制,如果所挂接的是相同型号的器件,则还受器件地址位的限制。
i2c总线数据传输速率在标准模式下可达100kbit/s,快速模式下可达400kbit/s,高速模式下可达3.4mbit/s。一般通过i2c总线接口可编程时钟来实现传输速率的调整,同时也跟所接的上拉电阻的阻值有关。
i2c总线上的主设备与从设备之间以字节(8位)为单位进行双向的数据传输。
1.1.3 i2c总线协议
i2c协议规定,总线上数据的传输必须以一个起始信号作为开始条件,以一个结束信号作为传输的停止条件。起始和结束信号总是由主设备产生。总线在空闲状态时,scl和sda都保持着高电平,当scl为高电平而sda由高到低的跳变,表示产生一个起始条件;当scl为高而sda由低到高的跳变,表示产生一个停止条件。在起始条件产生后,总线处于忙状态,由本次数据传输的主从设备独占,其他i2c器件无法访问总线;而在停止条件产生后,本次数据传输的主从设备将释放总线,总线再次处于空闲状态。如图所示:
在了解起始条件和停止条件后,我们再来看看在这个过程中数据的传输是如何进行的。前面我们已经提到过,数据传输以字节为单位。主设备在scl线上产生每个时钟脉冲的过程中将在sda线上传输一个数据位,当一个字节按数据位从高位到低位的顺序传输完后,紧接着从设备将拉低sda线,回传给主设备一个应答位,此时才认为一个字节真正的被传输完成。当然,并不是所有的字节传输都必须有一个应答位,比如:当从设备不能再接收主设备发送的数据时,从设备将回传一个否定应答位。数据传输的过程如图所示:
在前面我们还提到过,i2c总线上的每一个设备都对应一个唯一的地址,主从设备之间的数据传输是建立在地址的基础上,也就是说,主设备在传输有效数据之前要先指定从设备的地址,地址指定的过程和上面数据传输的过程一样,只不过大多数从设备的地址是7位的,然后协议规定再给地址添加一个最低位用来表示接下来数据传输的方向,0表示主设备向从设备写数据,1表示主设备向从设备读数据。如图所示:
1.1.4 i2c总线操作
对i2c总线的操作实际就是主从设备之间的读写操作。大致可分为以下三种操作情况:
第一,主设备往从设备中写数据。数据传输格式如下:
第二,主设备从从设备中读数据。数据传输格式如下:
第三,主设备往从设备中写数据,然后重启起始条件,紧接着从从设备中读取数据;或者是主设备从从设备中读数据,然后重启起始条件,紧接着主设备往从设备中写数据。数据传输格式如下:
第三种操作在单个主设备系统中,重复的开启起始条件机制要比用stop终止传输后又再次开启总线更有效率。
1.2i2c总线硬件接口电路示例
1.2.1i2c总线硬件接口电路示例一
这个电路是基于lpc2368 arm7芯片进行设计的,使用其内部的i2c接口作为主设备,使用adt75和sc16is740作为两个从设备的i2c总线应用。
adt75是一个带i2c接口的温度传感器器件,数据手册上对其地址的描述如下:
由此,其地址跟a0、a1、a2引脚的接法有关,我们这里的实例是将a0、a1、a2全部接到高电平上,因此其地址是:1001111(即0x4f),又因根据协议再给地址添加一个最低位(方向位,默认给写方向),因此最后这个温度传感器作为从设备的地址是:10011110(即0x9e)。
sc16is740是一个具有i2c或者spi接口的扩展uart的器件(通过第8脚来决定使用i2c还是spi接口,我们这里要求使用i2c接口,因此将第8脚接到高电平)。根据数据手册,我们同样的可以知道地址跟a0、a1的接法有关,我们这里的a0接高电平,a1接低电平。因此这个器件作为从设备的地址是:10010010(即0x92)。
1.2.2i2c总线硬件接口电路示例二
这个电路是mini2440开发板上i2c总线接口的应用。我们可以看到,sda和scl线上接了一个10k的上拉排阻。at24c08是一个容量为8kbit的eeprom存储器件(注意是8kbit,也就是1kb) ,根据数据手册中器件地址部分的描述,at24c08的地址是:1010+a2a1a0+方向位,其中1010是eeprom的类型识别符;仅仅使用a2来确定总线访问本器件的从设备地址,这里接的低电平,所以为0;a1和a0是器件内部页地址,在对器件擦除或者编程时使用,虽然这里也接的低电平,但器件内部并不使用引脚的输入值,也就是说a1和a0的值是由软件进行设定的。
1.3脱离操作系统的i2c总线驱动示例(以电路示例一为例)
1.3.1lpc2368中i2c接口寄存器描述
lpc2368中有三个i2c总线接口,分别表示为i2c0、i2c1和i2c2,每个i2c接口都包含7个寄存器。它们分别是:
i2c控制置位寄存器(i2conset):8位寄存器,各位不同的设置是对i2c总线不同的控制。
位
符号
描述
复位值
1:0
-
保留,用户软件不要向其写入1。从保留位读出的值未被定义
na
2
aa
声明应答标志。为1时将为需要应答的情况产生一个应答
0
3
si
i2c中断标志。当i2c状态改变时该位置位
0
4
sto
总线停止条件控制。1发出一个停止条件,当总线检测到停止条件时,sto自动清零
0
5
sta
总线起始条件控制。1进入主模式并发出一个起始条件
0
6
i2en
总线使能控制。1为使能
0
7
-
保留,用户软件不要向其写入1。从保留位读出的值未被定义
na
i2c控制清零寄存器(i2conclr):8位寄存器,对i2conset寄存器中的相应为清零。
位
符号
描述
复位值
1:0
-
保留,用户软件不要向其写入1。从保留位读出的值未被定义
na
2
aac
声明应答标志清零位。向该位写入1清零i2conset寄存器中的aa位
0
3
sic
中断标志清零位。向该位写入1清零i2conset寄存器中的si位
0
4
-
保留,用户软件不要向其写入1。从保留位读出的值未被定义
na
5
stac
起始条件清零位。向该位写入1清零i2conset寄存器中的sta位
0
6
i2enc
总线禁能控制。写入1清零i2conset寄存器中的i2en位
0
7
-
保留,用户软件不要向其写入1。从保留位读出的值未被定义
na
i2c状态寄存器(i2stat):8位只读寄存器,用于监控总线的实时状态(可能存在26种状态)。
位
符号
描述
复位值
2:0
-
这3个位不使用且总是为0
0
7:3
status
这些位给出i2c接口的实时状态,不同的值代表不同的状态,状态码请参考数据手册
0x1f
i2c数据寄存器(i2dat):8位寄存器,在si置位期间,i2dat中的数据保持稳定。
位
符号
描述
复位值
7:0
data
该寄存器保留已经接收到或者准备要发送的数据值
0
i2c从地址寄存器(i2adr):8位寄存器,i2c总线为从模式时才使用。主模式中该寄存器无效。
位
符号
描述
复位值
0
gc
通用调用使能位
0
7:1
address
从模式的i2c器件地址
0x00
sch占空比寄存器(i2sclh):16位寄存器,用于定义scl高电平所保持的pclk周期数。
位
符号
描述
复位值
15:0
sclh
scl高电平周期选择计数
0x0004
scl占空比寄存器(i2scll):16位寄存器,用于定义scl低电平所保持的pclk周期数。
位
符号
描述
复位值
15:0
scll
scl低电平周期选择计数
0x0004
在前面的i2c总线特征中我们提到过,i2c总线的速率通过可编程时钟来调整,即必须通过软件对i2sclh和i2scll寄存器进行设置来选择合适的数据频率和占空比。 频率由下面的公式得出(fpclk是pclk的频率)。
1.3.2lpc2368中i2c总线操作
在1.1.4中我们已经讲过了对i2c总线的操作,但那只是从协议和时序上的描述,那我们如何从软件上去体现出来呢?接下来我们就讨论这个问题。
对i2c总线上主从设备的读写可使用两种方法,一是使用轮询的方式,二是使用中断的方式。轮询方式即是在一个循环中判断i2c状态寄存器当前的状态值来确定总线当前所处的状态,然后根据这个状态来进行下一步的操作。中断方式即是使能i2c中断,注册i2c中断服务程序,在服务程序中读取i2c状态寄存器的当前状态值,再根据状态值来确定下一步的操作。
不管使用哪种方法,看来i2c状态寄存器的值是至关重要的。这些状态值代表什么意思呢?下面我们描述一些常用的状态值(详细的状态值含义请参考数据手册)。
0x08: 表明主设备向总线已发出了一个起始条件;
0x10: 表明主设备向总线已发出了一个重复的起始条件;
0x18: 表明主设备向总线已发送了一个从设备地址(写方向)并且接收到从设备的应答;
0x20: 表明主设备向总线已发送了一个从设备地址(写方向)并且接收到从设备的非应答;
0x28: 表明主设备向总线已发送了一个数据字节并且接收到从设备的应答;
0x30: 表明主设备向总线已发送了一个数据字节并且接收到从设备的非应答;
0x40: 表明主设备向总线已发送了一个从设备地址(读方向)并且接收到从设备的应答;
0x48: 表明主设备向总线已发送了一个从设备地址(读方向)并且接收到从设备的非应答;
0x50: 表明主设备从总线上已接收一个数据字节并且返回了应答;
0x58: 表明主设备从总线上已接收一个数据字节并且返回了非应答;
1.3.3示例代码
一、轮询方式读写总线:
对于代码中从设备内部寄存器的操作请参考该设备的数据手册。例如,要读取温度传感器的温度值只需要调用:i2c0_readregister(channel_temperature, adt75a_temp, &value),如果读取成功,则value中的数据就是通过i2c总线读取温度传感器中的温度数据。
二、中断方式读写总线:
这里的从设备地址定义、i2c控制寄存器宏定义和i2c初始化与上面轮询中的类似,只是要在初始化函数中加上中断申请的代码,中断服务程序名称为:i2c0_exception。这里不再贴出以上代码了,这里只贴出关键性的代码。
/*定义i2c状态标志*/
typedef enum
{
i2c_idle = 0,
i2c_started = 1,
i2c_restarted = 2,
i2c_repeated_start = 3,
i2c_data_ack = 4,
i2c_data_nack = 5
} i2c_status_flag;
/*定义i2c数据传输缓冲区大小和传输超时大小*/
#define i2c_bufsize 0x200
#define i2c_timeout 0x00ffffff
/*定义i2c当前状态标志*/
volatile i2c_status_flag i2c_flag;
/*i2c当前的模式,0为主发送器模式,1为主接收器模式*/
volatile uint32 i2cmastermode = 0;
/*分别定义i2c接收和发送缓冲区、要发送或要接收的字节数、实际发送或接收的字节数*/
volatile uint8 i2creadbuf[i2c_bufsize], i2cwritebuf[i2c_bufsize];
volatile uint32 i2creadlength, i2cwritelength;
volatile uint32 i2c_rd_index, i2c_wr_index;
/****************************************************************************
** function name: i2c0_exception
** descriptions : i2c0中断服务程序
** input : 无
** output : 无
** created date : 2011-03-24
*****************************************************************************/
void i2c0_exception(void)
{
volatile uint32 stat_value;
stat_value = i20stat;
switch(stat_value)
{
case 0x08:
/*发出了一个起始条件,接下来将发送从地址然后清零si位和sta位*/
i2c_flag = i2c_started;
i20dat = i2cwritebuf[i2c_wr_index];
i2c_wr_index++;
i20conclr = i2c_sta | i2c_si;
break;
case 0x10:
/*一个重复的起始条件发送完成,接下来要将发送从地址然后清零si位和sta位*/
i2c_flag = i2c_restarted;
if(i2cmastermode == 1)
{
/*注意i2cwritebuf中的第0位是设备从地址和写方向位,因这里是读操作,故将第0位的方向位变为读*/
i20dat = i2cwritebuf[0] | 0x01;
}
i20conclr = i2c_sta | i2c_si;
break;
case 0x18 /*(注:sla+w表示从设备地址+写方向)*/
/*发送sla+w后已接收到ack,接下来开始发送数据字节到数据寄存器然后清零si位*/
if(i2c_flag == i2c_started)
{
i2c_flag = i2c_data_ack;
i20dat = i2cwritebuf[i2c_wr_index];
i2c_wr_index++;
}
i20conclr = i2c_si;
break;
case 0x28:
/*此状态表明已发送i2dat中的字节且接收到ack,接下来继续发送下一个字节*/
case 0x30:
/*已发送i2dat中的字节且接收到非ack,接下来可能发出停止条件或重启起始条件*/
if(i2c_wr_index != i2cwritelength)
{
/*实际发送的字节数与要发送的不相等则继续发送,但可能是最后一次*/
i20dat = i2cwritebuf[i2c_wr_index];
i2c_wr_index++;
if(i2c_wr_index != i2cwritelength)
{
i2c_flag = i2c_data_ack;
}
else
{
/*如果实际发送与要发送的相等了,表明主发送端数据发送完成*/
i2c_flag = i2c_data_nack;
if(i2creadlength != 0)
{
/*如果主发送端有等待接收的字节,则切换为主接收模式,重启起始条件*/
i2c_flag = i2c_repeated_start;
i20conset = i2c_sta | i2c_si;
}
}
}
else
{
/*如果实际发送与要发送的相等了,表明主发送端数据发送完成*/
i2c_flag = i2c_data_nack;
if(i2creadlength != 0)
{
/*如果主发送端有等待接收的字节,则表明需切换为主接收模式,重启起始条件*/
i2c_flag = i2c_repeated_start;
i20conset = i2c_sta;
}
}
i20conclr = i2c_si;
break;
case 0x40:
/*此状态表明已发送sla+r后已接收到ack*/
i20conclr = i2c_si;
break;
case 0x50:
/*此状态表明已接收数据字节后已接收到ack*/
case 0x58:
/*此状态表明已接收数据字节后已接收到非ack*/
i2creadbuf[i2c_rd_index] = i20dat;
i2c_rd_index++;
if(i2c_rd_index != i2creadlength)
{
/*如果实际接收的字节与要接收的不相等,则继续接收*/
i2c_flag = i2c_data_ack;
}
else
{
/*否则接收完毕*/
i2c_rd_index = 0;
i2c_flag = i2c_data_nack;
}
i20conclr = i2c_aa | i2c_si;
break;
case 0x20:
/*此状态表明已发送sla+w后已接收到非ack*/
case 0x48:
/*此状态表明已发送sla+r后已接收到非ack*/
i2c_flag = i2c_data_nack;
i20conclr = i2c_si;
break;
default:
i20conclr = i2c_si;
break;
}
vicvectaddr = 0x00;
}
/****************************************************************************
** function name: i2c0_start
** descriptions : 设置i2c0总线传输起始条件
** input : 无
** output : 返回true/false, false为设置超时
** created date : 2011-03-24
*****************************************************************************/
bool i2c0_start(void)
{
uint32 timeout = 0;
bool retval = false;
/*设置配置寄存器sta位开始条件*/
i20conset = i2c_sta | i2c_si;
i20conclr = i2c_si;
/*等待起始条件完成*/
while(1)
{
if(i2c_flag == i2c_started)
{
retval = true;
break;
}
if(timeout >= i2c_timeout)
{
retval = false;
break;
}
timeout++;
}
return retval;
}
/****************************************************************************
** function name: i2c0_stop
** descriptions : 设置i2c0总线传输停止条件
** input : 无
** output : 返回true
** created date : 2011-03-24
*****************************************************************************/
bool i2c0_stop(void)
{
/*设置配置寄存器sto位停止条件和清除si标志*/
i20conset = i2c_sto;
i20conclr = i2c_si;
/*等待停止条件完成*/
while(i20conset & i2c_sto);
return true;
}
/****************************************************************************
** function name: i2c0_engine
** descriptions : 完成i2c0总线从开始到停止的传输,传输过程在中断服务程序中进行
** input : 无
** output : 返回true/false
** created date: 2011-03-24
*****************************************************************************/
bool i2c0_engine(void)
{
i2c_flag = i2c_idle;
i2c_rd_index = 0;
i2c_wr_index = 0;
if(i2c0_start() != true)
{
i2c0_stop();
return false;
}
while(1)
{
if(i2c_flag == i2c_data_nack)
{
i2c0_stop();
break;
}
}
return true;
}
从上面代码中看,如果要使用i2c总线启动一次数据传输只需要先初始化好发送或接收缓冲区,然后调用i2c0_engine()函数即可。如下代码所示:
/****************************************************************************
** function name: i2c0_readwritetransmission
** descriptions : i2c总线数据读写传输
** input : read_buf-读数据缓冲区
read_len-读数据长度
write_buf-写数据缓冲区
write_len-写数据长度
** output : 数据读写传输是否成功
** created date : 2011-03-24
*****************************************************************************/
bool i2c0_readwritetransmission(uint8 **read_buf, uint32 read_len, uint8 *write_buf, uint32 write_len)
{
uint32 i;
bool result = false;
/*数据传输长度检查*/
if(read_len > i2c_bufsize || write_len > i2c_bufsize)
{
return false;
}
/*清空i2c接收和发送缓冲区内容*/
for(i = 0; i < i2c_bufsize; i++)
{
i2creadbuf[i] = 0;
i2cwritebuf[i] = 0;
}
/*确定i2c总线模式(0为主发送模式,1为主接收模式)*/
i2cmastermode = (read_len == 0)? 0 : 1;
i2creadlength = read_len;
i2cwritelength = write_len;
/*要写入i2c从设备的数据(第一个字节包含从设备地址和方向位)*/
for(i = 0; i 0 && result == true)
{
uint8 *buf = (uint8 *)malloc(read_len * sizeof(uint8));
for(i = 0; i < read_len; i++)
{
uf[i] = i2creadbuf[i];
}
*read_buf = buf;
}
return result;
}
1.4linux下i2c子系统框架
在linux下要使用i2c总线并没有像无系统中的那样简单,为了体现linux中的模块架构,linux把i2c总线的使用进行了结构化。这种结构分三部分组成,他们分别是:i2c核心部分、i2c总线驱动部分和i2c设备驱动。结构图如下:
由此看来,在linux下驱动i2c总线不像单片机中那样简单的操作几个寄存器了,而是把i2c总线结构化、抽象化了,符合通用性和linux设备模型。
/*i2c从设备地址*/
#define sc16is740_addr 0x92 /*i2c转uart设备*/
#define adt75a_addr 0x9e /*温度传感器设备*/
#define adt75a_temp 0x00 /*温度传感器内部寄存器*/
/*从设备选择标识*/
#define channel_gprs 0
#define channel_temperature 1
/*定义i2c控制寄存器各位操作宏*/
#define bit(x) (1 << x)
#define i2c_en bit(6)
#define i2c_sta bit(5)
#define i2c_sto bit(4)
#define i2c_si bit(3)
#define i2c_aa bit(2)
/*用作超时计数*/
#define safety_counter_limit 3000
/******************************************************************
** function name: i2c0_init
** descriptions : i2c0初始化
** input : 无
** output : 无
** created date : 2011-03-24
*******************************************************************/
void i2c0_init(void)
{
/*设置p0.0,p0.1为i2c0接口的sda和scl功能*/
pinsel0 |= (0x03 << 0) | (0x03 << 2);
/*设置i2c0接口功率/时钟控制位*/
pconp |= (0x01 << 7 );
/*清空i2c0配置寄存器的各位*/
i20conclr = (0x01 << 2) | (0x01 << 3) | (0x01 << 5) | (0x01 << 6);
/*使能i2c0为主发送器模式*/
i20conset = (0x01 safety_counter_limit)
{
return false; /*超时退出*/
}
}
addresssendsafetycounter ++;
if (addresssendsafetycounter > safety_counter_limit)
{
return false; /*超时退出*/
}
} while (i20stat != 0x18); /*在前面已经描述了0x18的含义*/
/*发送从设备内部寄存器地址,根据数据手册描述该内部地址要左移3位*/
i20dat = registeraddress safety_counter_limit)
{
return false; /*超时退出*/
}
}
/*发送从设备地址(方向位为读,注意与上0x01将地址最低位变为1即为读方向)*/
if(channel == channel_gprs)
i20dat = sc16is740_addr | 0x01;
else if(channel == channel_temperature)
i20dat = adt75a_addr | 0x01;
i20conclr = i2c_sta | i2c_si;
/*等待从设备地址发送完成*/
loopsafetycounter = 0;
while (~i20conset & i2c_si)
{
loopsafetycounter ++;
if (loopsafetycounter > safety_counter_limit)
{
return false; /*超时退出*/
}
}
/*开始准备读取数据*/
i20conclr = i2c_si | i2c_aa;
/*等待数据接收*/
loopsafetycounter = 0;
while (~i20conset & i2c_si)
{
loopsafetycounter ++;
if (loopsafetycounter > safety_counter_limit)
{
return false; /*超时退出*/
}
}
/*数据接收*/
*pdata = i20dat;
/*发送i2c停止条件*/
i20conset = i2c_sto;
i20conclr = i2c_si;
/*等待停止条件发送完成*/
loopsafetycounter = 0;
while (i20conset & i2c_sto)
{
loopsafetycounter ++;
if (loopsafetycounter > safety_counter_limit)
{
return false; /*超时退出*/
}
}
return true;
}
/****************************************************************************
** function name: i2c0_writeregister
** descriptions : 从i2c0总线上写从设备的数据
** input : 从设备选择标识、从设备内部寄存器地址、要写入的数据字节
** output : 写入是否成功
** created date : 2011-03-28
*****************************************************************************/
bool i2c0_writeregister(uint32 channel, uint8 registeraddress, uint8 data)
{
uint32 loopsafetycounter = 0;
uint32 addresssendsafetycounter = 0;
/*使用循环判断i2c状态寄存器i20stat 的值*/
do
{
/*向总线发送i2c起始条件*/
i20conset = i2c_sta | i2c_si;
i20conclr = i2c_si;
/*等待起始条件发送完成*/
loopsafetycounter = 0;
while (~i20conset & i2c_si)
{
loopsafetycounter ++;
if (loopsafetycounter > safety_counter_limit)
{
return false; /*超时退出*/
}
}
/*发送从设备地址*/
if(channel == channel_gprs)
i20dat = sc16is740_addr;
else if(channel == channel_temperature)
i20dat = adt75a_addr;
i20conclr = i2c_sta | i2c_si;
/*等待从设备地址发送完成*/
loopsafetycounter = 0;
while (~i20conset & i2c_si)
{
loopsafetycounter ++;
if (loopsafetycounter > safety_counter_limit)
{
return false; /*超时退出*/
}
}
addresssendsafetycounter ++;
if (addresssendsafetycounter > safety_counter_limit)
{
return false; /*超时退出*/
}
} while (i20stat != 0x18);
/*发送从设备内部寄存器地址*/
i20dat = registeraddress safety_counter_limit)
{
return false; /*超时退出*/
}
}
/*发送i2c停止条件*/
i20conset = i2c_sto;
i20conclr = i2c_si;
/*等待停止条件发送完成*/
loopsafetycounter = 0;
while (i20conset & i2c_sto)
{
loopsafetycounter ++;
if (loopsafetycounter > safety_counter_limit)
{
return false; /*超时退出*/
}
}
return true;
}
双室双温电冰箱不停机的应急检修
芯片中的晶体管是如何安上去的呢
5G+赋能垂直行业的创新应用
基于传感器的LED闪烁抑制技术是怎样的
除了BAT,苹果华为三星纷纷加入,2018年AI芯片六大趋势揭晓
嵌入式Linux内核I2C子系统详解
韩国KT公司在台城洞全面铺设5G基础设施,在小学里开设相关课程
【渗漏治理】换热器内漏的原因及处理方法
马斯克挑战手机操作系统,靠什么来破局?
嵌入式开发学习主要可以往哪些线路发展
中国电子展上,福州经济技术开发区展示如何用创新使能终端
鸿海与纬创都暂停印度工厂运营
了解下五大嵌入式操作系统
3月份新能源汽车销量出现回暖趋势,传统势力未走出泥潭
这个电容式触摸传感器开关的LED控制电路比较特别
三星推出新款OLED电视 第一批样品将于2019年中期上市
20位DAC是精度为1ppm的精密电压源中最简单的部分
连接器触点可以承载的电流类型
迷你风力发电机
教你ZedBoard开发板如何玩转linaro系统