stm32软件模拟i2c通讯读取lm75a温度

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/Utotao/article/details/88782490

很久以前,写过基于51单片机的i2c通信,具体是读写EEPROM。但是当时没能好好总结,只是记录了些代码,现回头去来看,真是一件头疼的事情。
对于stm32,其硬件i2c有着一些bug,此外对于i2c这种通用的串行通信协议,从源头掌握和使用显然更加靠谱一些,当然,对于arm,还是直接操作寄存器来得方便的多。


参考文献:
[1] utotao-基于51单片机的I2C与E2PROM通信
[2] I2C实践-LM75A温度传感器
[3] STM32F1 模拟I2C驱动DAC(LTC2605)程序


1、I2C协议

1.1 i2c串行总线概述

采用串行总线技术可以使系统的硬件设计大大简化、系统的体积减小、可靠性提高。同时,系统的更改和扩充更为容易。
常用的串行扩展总线有: I2C (Inter IC BUS)总线单总线(1-WIRE BUS)SPI(Serial Peripheral Interface)总线Microwire/PLUS 等。
I2C 总线是 PHLIPS 公司推出的一种串行总线,是具备多主机系统所需的包括总线裁决和高低速器件同步功能的高性能
串行总线。
I2C 总线叧有两根双向信号线。一根是数据线 SDA,另一根是时钟线 SCL。
在这里插入图片描述
在这里插入图片描述
每个接到 I2C 总线上的器件都有唯一的地址。主机和其它器件间的数据传送可以是由主机发送数据到其它器件,这时主机即为发送器,总线上接收数据的器件则为接收器。在多主机系统中,可能同时有几个主机企图启动总线传送数据,为了避免混乱, I2C 总线要通过总线仲裁,以决定由哪一台主机控制总线。

Question:I2C如何实现数据的双向传输?
在这里插入图片描述

1.2 i2c总线的数据传送

1)时钟有效性
I2C 总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定在时钟线上的信号为低电平期间,数据线上的高电平戒低电平状态才允许变化
在这里插入图片描述
2)起始信号和终止信号
START: SCL 线为高电平期间,SDA 线由高电平向低电平的变化表示起始信号;
STOP: SCL 线为高电平期间,SDA 线由低电平向高电平的变化表示终止信号。

  • 起始和终止信号都是由主机发出的,在起始信号产生后,总线就处于被占用的状态;在终止信号产生后,总线就处于空闲状态;
  • 连接到 I2C 总线上的器件,若具有 I2C 总线的硬件接口,则很容易检测到起始和终止信号;
  • 接收器收到一个完整的数据字节后,有可能需要完成一些其它工作,如处理内部中断服务等,可能无法立刻接收下一个字节,这时接收器件可以将SCL 线拉成低电平,从而使主机处于等待状态。直到接收器件准备好接收下一个字节时,再释放 SCL 线使之为高电平,从而使数据传送可以继续进行。这里可以参开ARM9的i2c中断接收数据过程。
    在这里插入图片描述

3) 应答与非应答
每一个字节必须保证是 8 位长度。数据传送时,先传送最高位(MSB),每一个被传送的字节后面都必须跟随一位应答位(即一帧共有 9 位)。

由于某种原因从机不对主机寻址信号应答(即非应答)时(如从机正在进行实时性的处理工作而无法接收总线上的数据),它必须将数据线置于高电平,而由主机产生一个终止信号以结束总线的数据传送。
如果从机对主机进行了应答,但在数据传送一段时间后无法继续接收更多的数据时,从机可以通过对无法接收的第一个数据字节的“非应答”通知主机,主机则应収出**终止信号(STOP)**以结束数据的继续传送。
当主机接收数据时,它收到最后一个数据字节后,必须向从机収出一个结束传送 的信号。这个信号是由对从机的“非应答”来实现的。然后,从机释放 SDA 线,以允许主机产生终止信号。

1.3 数据帧格式

I2C 总线上传送的数据信号是广义的,既包括地址信号,又包括真正的数据信号。

  • 在起始信号后必须传送一个从机的地址(7 位),第 8 位是数据的传送方向位(R/T),用“0”表示主机发送数据(T),“1”表示主机接收数据(R)
  • 每次数据传送总是由主机产生的终止信号结束;
  • 若主机希望继续占用总线进行新的数据传送,则可以不产生终止信号,马上再次发出起始信号对另一从机迚行寻址。

case1: 主机向从机发送数据,数据传送方向在整个传送过程中不变:
在这里插入图片描述
注:有阴影部分表示数据由主机向从机传送,无阴影部分则表示数据由从机向主机传送。
A 表示应答, A 非表示非应答(高电平)。S 表示起始信号,P 表示终止信号。

case2: 主机在第一个字节后,立即从从机读数据
在这里插入图片描述
以上两点小结:
在这里插入图片描述
case3: 在传送过程中,当需要改变传送方向时,起始信号和从机地址都被重复产生一次,但两次读/写方向位正好反相。
在这里插入图片描述
显然,本次温度读取依据数据手册可知是case3

1.3 总线寻址

I2C 总线协议有明确的规定: 采用 7 位的寻址字节(寻址字节是起始信号后的第一个字节)。
在这里插入图片描述
D7~D1 位组成从机的地址。D0 位是数据传送方向位,为“0”时表示主机向从机写数据,为“1”时表示主机由从机读数据。
主机发送地址时,总线上的每个从机都将这 7 位地址码不自己的地址进行比较,如果相同,则认为自己正被主机寻址,根据 R/T 位将自己确定为发送器或接收器。

从机的地址由固定部分和可编程部分组成。在一个系统中可能希望接入多个相同的从机,从机地址中可编程部分决定了可接入总线该类器件的最大数目。如一个从机的 7 位寻址位有 4 位是固定位,3 位是可编程位,这时仅能寻址 8 个同样的器件,即可以有 8 个同样的器件接入到该 I2C 总线系统中。

1.4 lm75a温度传感器简介

1)管脚介绍:
在这里插入图片描述

接线示意图:
在这里插入图片描述
2)lm75a内部寄存器
在这里插入图片描述
3)温度计算:加权计算
在这里插入图片描述
4)lm读取温度时序图:
编程部分读取温度函数就是依据这张时序图来的~
在这里插入图片描述

 1. 主机发送ob1001A2A1A0R/W
 2. lm75a应答
 3. 主机发送0b000000D1D0
 4. lm75a应答
 5. 主机发送ob1001A2A1A0R/W
 6. 从机应答
 7. 读取数据

2、i2c模拟

2.1 i2c协议驱动

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;
}
2.2 lm75a驱动

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<len;i++)
  {
		*p++ = i2c_read_byte();
    if (i!=(len-1))
      i2c_ack();      	   
  }       
  i2c_noack();        
  i2c_stop();
}

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通信?

猜你喜欢

转载自blog.csdn.net/Utotao/article/details/88782490