很久以前,写过基于51单片机的i2c通信,具体是读写EEPROM。但是当时没能好好总结,只是记录了些代码,现回头去来看,真是一件头疼的事情。
对于stm32,其硬件i2c有着一些bug,此外对于i2c这种通用的串行通信协议,从源头掌握和使用显然更加靠谱一些,当然,对于arm,还是直接操作寄存器来得方便的多。
参考文献:
[1] utotao-基于51单片机的I2C与E2PROM通信
[2] I2C实践-LM75A温度传感器
[3] STM32F1 模拟I2C驱动DAC(LTC2605)程序
采用串行总线技术可以使系统的硬件设计大大简化、系统的体积减小、可靠性提高。同时,系统的更改和扩充更为容易。
常用的串行扩展总线有: I2C (Inter IC BUS)总线 、单总线(1-WIRE BUS)、SPI(Serial Peripheral Interface)总线及Microwire/PLUS 等。
I2C 总线是 PHLIPS 公司推出的一种串行总线,是具备多主机系统所需的包括总线裁决和高低速器件同步功能的高性能
串行总线。
I2C 总线叧有两根双向信号线。一根是数据线 SDA,另一根是时钟线 SCL。
每个接到 I2C 总线上的器件都有唯一的地址。主机和其它器件间的数据传送可以是由主机发送数据到其它器件,这时主机即为发送器,总线上接收数据的器件则为接收器。在多主机系统中,可能同时有几个主机企图启动总线传送数据,为了避免混乱, I2C 总线要通过总线仲裁,以决定由哪一台主机控制总线。
1)时钟有效性
I2C 总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定, 在时钟线上的信号为低电平期间,数据线上的高电平戒低电平状态才允许变化。
2)起始信号和终止信号
START: SCL 线为高电平期间,SDA 线由高电平向低电平的变化表示起始信号;
STOP: SCL 线为高电平期间,SDA 线由低电平向高电平的变化表示终止信号。
3) 应答与非应答
每一个字节必须保证是 8 位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有 9 位)。
由于某种原因从机不对主机寻址信号应答(即非应答)时(如从机正在进行实时性的处理工作而无法接收总线上的数据),它必须将数据线置于高电平,而由主机产生一个终止信号以结束总线的数据传送。
如果从机对主机进行了应答,但在数据传送一段时间后无法继续接收更多的数据时,从机可以通过对无法接收的第一个数据字节的“非应答”通知主机,主机则应収出**终止信号(STOP)**以结束数据的继续传送。
当主机接收数据时,它收到最后一个数据字节后,必须向从机収出一个结束传送 的信号。这个信号是由对从机的“非应答”来实现的。然后,从机释放 SDA 线,以允许主机产生终止信号。
I2C 总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。
case1: 主机向从机发送数据,数据传送方向在整个传送过程中不变:
注:有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。
A 表示应答, A 非表示非应答(高电平)。S 表示起始信号,P 表示终止信号。
case2: 主机在第一个字节后,立即从从机读数据
以上两点小结:
case3: 在传送过程中,当需要改变传送方向时,起始信号和从机地址都被重复产生一次,但两次读/写方向位正好反相。
显然,本次温度读取依据数据手册可知是case3。
I2C 总线协议有明确的规定: 采用 7 位的寻址字节(寻址字节是起始信号后的第一个字节)。
D7~D1 位组成从机的地址。D0 位是数据传送方向位,为“0”时表示主机向从机写数据,为“1”时表示主机由从机读数据。
主机发送地址时,总线上的每个从机都将这 7 位地址码不自己的地址进行比较,如果相同,则认为自己正被主机寻址,根据 R/T 位将自己确定为发送器或接收器。
从机的地址由固定部分和可编程部分组成。在一个系统中可能希望接入多个相同的从机,从机地址中可编程部分决定了可接入总线该类器件的最大数目。如一个从机的 7 位寻址位有 4 位是固定位,3 位是可编程位,这时仅能寻址 8 个同样的器件,即可以有 8 个同样的器件接入到该 I2C 总线系统中。
接线示意图:
2)lm75a内部寄存器
3)温度计算:加权计算
4)lm读取温度时序图:
编程部分读取温度函数就是依据这张时序图来的~
1. 主机发送ob1001A2A1A0R/W
2. lm75a应答
3. 主机发送0b000000D1D0
4. lm75a应答
5. 主机发送ob1001A2A1A0R/W
6. 从机应答
7. 读取数据
1)一系列宏定义
#define I2CPORT GPIOB //定义IO接口
#define I2C_SCL GPIO_Pin_6 //定义IO接口
#define I2C_SDA GPIO_Pin_7 //定义IO接口
#define SCL_H GPIO_SetBits(GPIOB,GPIO_Pin_6)
#define SCL_L GPIO_ResetBits(GPIOB,GPIO_Pin_6)
#define SDA_H GPIO_SetBits(GPIOB,GPIO_Pin_7)
#define SDA_L GPIO_ResetBits(GPIOB,GPIO_Pin_7)
#define IIC_WRITE 0 //数据方向,写入
#define IIC_READ 1 //数据方向,接收
#define SDA_OUT(){GPIO_InitTypeDef GPIO_InitStructure;\
GPIO_InitStructure.GPIO_Pin = I2C_SDA;\
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;\
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;\
GPIO_Init(I2CPORT, &GPIO_InitStructure);}
#define SDA_IN(){GPIO_InitTypeDef GPIO_InitStructure;\
GPIO_InitStructure.GPIO_Pin = I2C_SDA;\
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; \
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;\
GPIO_Init(I2CPORT, &GPIO_InitStructure);}
这个地方使用宏定义定义了两个函数,纯属想多了解一下宏定义的优缺点,参考:浅谈宏定义(#define语句)
2)初始化i2c
//初始化IIC
void i2c_init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE ); //使能GPIOB时钟
GPIO_InitStructure.GPIO_Pin = I2C_SCL;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(I2CPORT, &GPIO_InitStructure);
GPIO_SetBits(I2CPORT,I2C_SCL); //PB6,PB7 输出高
}
3)start信号
void i2c_start(void){
// I2C_SCL_OUTH();
SDA_OUT();
SDA_H;
SCL_H;
delay_us(5);
SDA_L;
delay_us(5);
SCL_L;
//printf("start on...\r\n");
}
4)stop信号
void i2c_stop(void){
SDA_OUT();
SDA_L;
SCL_L;
delay_us(5);
SCL_H;
SDA_H;
delay_us(5);
//printf("stop...!\r\n");
}
5)应答信号
void i2c_ack(void){
SCL_L;
SDA_OUT();
SDA_L;
delay_us(5);
SCL_H;
delay_us(5);
SCL_L;
//printf("generate ack!\r\n");
}
6)不应答信号
void i2c_noack(void){
SCL_L;
SDA_OUT();
SDA_H;
delay_us(5);
SCL_H;
delay_us(5);
SCL_L;
//printf("generate no ack!\r\n");
}
7)等待应答信号
u8 i2c_wait_ack(void){
u8 tempcount;
delay_us(5);
SDA_IN();//检测SDA,如果为0表示有应答,否则等一会表示无应答
SDA_H;
SCL_H;
delay_us(5);
while(GPIO_ReadInputDataBit(I2CPORT,I2C_SDA)){
tempcount++;
if(tempcount > 250){
//printf("no ackking ...\r\n");
//i2c_stop();
return 1;//无应答信号,终止传输
}
}
//printf("yes, ackking ...\r\n");
//检测到Slave应答信号
SCL_L;
return 0;
}
9)发送一个字节
/*
* 此处需要格外注意,
* 初始状态的时候需要使得SCL为低电平,然后读取第一个位
*/
void i2c_send_byte(u8 b_data){
unsigned char i;
SDA_OUT();
SCL_L;
for( i = 0;i< 8;i++){
delay_us(10);
if(b_data & 0x80){
SDA_H;
// printf("the send bit is 1!\r\n");
}
else{
// printf("the send bit is 0!\r\n");
SDA_L;
}
b_data <<= 1;
SCL_H;
delay_us(5);
SCL_L;
delay_us(5);
}
}
10)读取一个字节
u8 i2c_read_byte(void){
u8 data,i;
SDA_IN();
for(i = 0;i< 8;i++){
SCL_L;
delay_us(10);
SCL_H;
delay_us(10);
data <<= 1;
if(GPIO_ReadInputDataBit(I2CPORT,I2C_SDA)){
data++;
printf("receive data bit is 1!\r\n");
}
delay_us(3);
}
// if(!ackflag)
// i2c_ack();
// else
// i2c_noack();
return data;
}
1)写入从机地址以及从机寄存器地址
u8 iic_write_addr(unsigned char slave_addr,unsigned char register_addr)
{
i2c_start();
i2c_send_byte(slave_addr);
i2c_wait_ack();
i2c_send_byte(register_addr);
i2c_wait_ack();
return 0;
}
2)读取数据
void i2c_Read(unsigned char id, unsigned char addr, unsigned char *p, unsigned int len)
{
int i;
printf("test temperatur!\r\n");
iic_write_addr(id|IIC_WRITE,addr);
i2c_start();
i2c_send_byte(id|IIC_READ);
i2c_wait_ack();
for (i=0;i
3)读取温度:
way1:
uint16_t I2C_LM75read(void)
{
u16 readDATA=0x0000;
u8 tempH=0x00;
u8 tempL=0x00;
i2c_start();
i2c_send_byte(0x9E);
i2c_wait_ack();//等待地址确认,从机应答
i2c_send_byte(0x00);
i2c_wait_ack();//等待地址确认,从机应答
i2c_start();
i2c_send_byte(0x9F);
i2c_wait_ack();//等待地址确认,从机应答
tempH=i2c_read_byte();
i2c_ack();
tempL=i2c_read_byte();
i2c_noack();
i2c_stop();
readDATA=(((u16)tempH<<8 )| tempL)>>5;
return readDATA;
}
way2:
void LM75A_GetTemp(u8 *Tempbuffer){//读温度
u8 temp_buff[2];
u8 t=0,a=0;
i2c_start();
i2c_send_byte(0x9E);
i2c_wait_ack();//等待地址确认,从机应答
i2c_send_byte(0x00);
i2c_wait_ack();//等待地址确认,从机应答
i2c_start();
i2c_send_byte(0x9F);
i2c_wait_ack();//等待地址确认,从机应答
temp_buff[0] =i2c_read_byte();
i2c_ack();
temp_buff[1] =i2c_read_byte();
i2c_noack();
i2c_stop();
t = temp_buff[0]; //处理温度整数部分,0~125度
*Tempbuffer = 0; //温度值为正值
if(t & 0x80){ //判断温度是否是负(MSB表示温度符号)
*Tempbuffer = 1; //温度值为负值
t = ~t; t++; //计算补码(原码取反后加1)
}
if(t & 0x01){ a=a+1; } //从高到低按位加入温度积加值(0~125)
if(t & 0x02){ a=a+2; }
if(t & 0x04){ a=a+4; }
if(t & 0x08){ a=a+8; }
if(t & 0x10){ a=a+16; }
if(t & 0x20){ a=a+32; }
if(t & 0x40){ a=a+64; }
Tempbuffer++;
*Tempbuffer = a;
a = 0;
t = temp_buff[1]; //处理小数部分,取0.125精度的前2位(12、25、37、50、62、75、87)
if(t & 0x20){ a=a+12; }
if(t & 0x40){ a=a+25; }
if(t & 0x80){ a=a+50; }
Tempbuffer++;
*Tempbuffer = a;
}
way3:
float readTemperature(void){
unsigned char temp[2]={0};
float temp_value=0.0;
unsigned int temp_high=0;
unsigned int temp_low=0;
unsigned int low=0;
unsigned int tempforh=0;
unsigned int judge_posneg=0;
i2c_Read(0x9E,0x00,temp,2);
temp_high=temp[0];
temp_low=temp[1];
low=temp_low>>5;
tempforh=temp_high*8+low;
judge_posneg=(temp_high & 0x80)>>7;
if(judge_posneg==0){
temp_value=tempforh*0.125;
return temp_value;
}else {
tempforh=(tempforh^0x7FF)+1;
temp_value=tempforh*(-0.125);
return temp_value;
}
}
Q:如何使用ASM实现I2C通信?