【代码】使用GPIO模拟I2C总线实现I2C存储

先贴代码:

i2cstoragelib.h

#ifndef i2cstoragelib
#define i2cstoragelib

#include "main.h"



#define SCL_H() HAL_GPIO_WritePin(SCL_GPIO_Port,SCL_Pin,GPIO_PIN_SET)
#define SCL_L() HAL_GPIO_WritePin(SCL_GPIO_Port,SCL_Pin,GPIO_PIN_RESET)

#define SDA_H() HAL_GPIO_WritePin(SDA_GPIO_Port,SDA_Pin,GPIO_PIN_SET)
#define SDA_L() HAL_GPIO_WritePin(SDA_GPIO_Port,SDA_Pin,GPIO_PIN_RESET)

#define SDA_READ() HAL_GPIO_ReadPin(SDA_GPIO_Port,SDA_Pin)



#define L_I2C_DELAY() osDelay(2)

#define L_I2C_WRITE_REQUEST_OK 0 
#define L_I2C_WRITE_ERROR_ADDRESS_OUT_OF_RANGE 1
#define L_I2C_WRITE_ERROR_UNEXPECTED_ACK 2 

#define I2C_STATE_BUSY 1
#define I2C_STATE_IDLE 0;


void sda_init_output();
void sda_init_input();
int16_t i2c_write_byte(uint16_t address_to_write,uint8_t byte_to_write);
uint8_t i2c_read_byte(uint16_t address_to_read);

extern uint8_t I2C_CURRENT_STATE;



#endif

i2cstoragelib.c

#include "i2cstoragelib.h"
#include "cmsis_os.h"

uint8_t I2C_CURRENT_STATE;

void sda_init_output()
{
	
	 /*Configure GPIO pin : SDA_Pin */
	GPIO_InitTypeDef GPIO_InitStruct = {0};
  GPIO_InitStruct.Pin = SDA_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(SDA_GPIO_Port, &GPIO_InitStruct);
	SDA_L();
}

void sda_init_input()
{
	 /*Configure GPIO pin : SDA_Pin */
	GPIO_InitTypeDef GPIO_InitStruct = {0};
  GPIO_InitStruct.Pin = SDA_Pin;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(SDA_GPIO_Port, &GPIO_InitStruct);
}



//the initial state is : SDA=0 SCL=0
//if sending data,SDA must change when SCL=0
//if starting or stopping process , SDA should change when SCL=1, which breaks the rule :) .

void i2c_stop_process()
{
	sda_init_output();
	SCL_H(); //init signal
	L_I2C_DELAY();
	
	SDA_H(); //change SDA
	L_I2C_DELAY();
	
	SCL_L(); //reset to SDA=0 SCL=0
	L_I2C_DELAY();
	SDA_L();
	L_I2C_DELAY();
	
}


void i2c_start_process()
{
	sda_init_output();
	
	SDA_H(); // init signal
	L_I2C_DELAY();
	SCL_H();
	L_I2C_DELAY();
	
	SDA_L(); //change SDA
	L_I2C_DELAY();
	
	SCL_L();  //reset to SDA=0 SCL=0
	L_I2C_DELAY();
	
}


void i2c_send_bit(uint8_t bit)
{
	if(bit) //change SDA when SCL = 0
	{
		SDA_H();
	}else
	{
		SDA_L();
	}
	L_I2C_DELAY();
	
	SCL_H(); //change SCL to 1 to send data;
	L_I2C_DELAY();
	
	SCL_L(); //reset 
	L_I2C_DELAY();
	SDA_L();
	L_I2C_DELAY();
	
}




GPIO_PinState i2c_get_ack(uint32_t timeout)  //get the acknowledge
{
	SDA_H();
	L_I2C_DELAY();
	SCL_H();//switch SCL to 1 to get the response 
	L_I2C_DELAY();
	
	uint16_t cnt = 1;
	GPIO_PinState response = GPIO_PIN_SET;
	
	while(cnt <= timeout)
	{
		if(!SDA_READ())
		{
			response = GPIO_PIN_RESET;
			break;
		}
		osDelay(1);
		cnt++;
	}
	
	SCL_L();//reset
	L_I2C_DELAY();
	
	return response;
}

void i2c_send_byte(uint8_t byte_to_send)
{
	uint8_t number_switcher = 0b10000000 ;
	while(number_switcher)
	{
		if((byte_to_send & number_switcher) == number_switcher)
		{
			i2c_send_bit(1);
		}
		else
		{
			i2c_send_bit(0);
		}
		number_switcher >>= 1;
	}
	
}











int16_t i2c_write_byte(uint16_t address_to_write,uint8_t byte_to_write)
{
	
	while(I2C_CURRENT_STATE == I2C_STATE_BUSY)
	{
		osDelay(1);
	}
	
	I2C_CURRENT_STATE = I2C_STATE_BUSY;
	
	if(address_to_write>=8192)
	{
		return L_I2C_WRITE_ERROR_ADDRESS_OUT_OF_RANGE;
	}
	
	
	//This is the default setting based on the schematic
	//here is the simulate code
	i2c_start_process();
	
	//1010 000 X
	//first, send this to the I2C bus, which means
	//device type identifier , hardware address , and read/write mode
	i2c_send_byte(0b10100000);//write mode
	
	//switch sda to input mode and get the response
	sda_init_input();
	uint8_t response = i2c_get_ack(250);
	//if something went wrong, abort the process.
	if(response)
	{
		i2c_stop_process();
		return L_I2C_WRITE_ERROR_UNEXPECTED_ACK;
	}
	
	
	//address! 
	uint8_t address_p1 = address_to_write >> 8;
	uint8_t address_p2 = address_to_write << 8 >> 8;
	
	// now send the address period1 (8 of 16 bits total. 0~8191)
	sda_init_output();
	i2c_send_byte(address_p1);
	
	//switch sda to input mode and get the response
	sda_init_input();
	response = i2c_get_ack(250);
	//if something went wrong, abort the process.
	if(response)
	{
		i2c_stop_process();
		return L_I2C_WRITE_ERROR_UNEXPECTED_ACK;
	}
	
	// now send the address period2 (8 of 16 bits total. 0~8191)
	sda_init_output();
	i2c_send_byte(address_p2);
	
	//switch sda to input mode and get the response
	sda_init_input();
	response = i2c_get_ack(250);
	
	//if something went wrong, abort the process.
	if(response)
	{
		i2c_stop_process();
		return L_I2C_WRITE_ERROR_UNEXPECTED_ACK;
	}
	
	// now send the byte to write
	sda_init_output();
	i2c_send_byte(byte_to_write);
	
	//switch sda to input mode and get the response
	sda_init_input();
	response = i2c_get_ack(250);
	
	//if something went wrong, abort the process.
	if(response)
	{
		i2c_stop_process();
		return L_I2C_WRITE_ERROR_UNEXPECTED_ACK;
	}
	
	
	
	//all done. Now stop the process.
	i2c_stop_process();
	
	I2C_CURRENT_STATE = I2C_STATE_IDLE;
	
	return L_I2C_WRITE_REQUEST_OK;
	
	
}

uint8_t i2c_read_byte(uint16_t address_to_read)
{
	
	while(I2C_CURRENT_STATE == I2C_STATE_BUSY)
	{
		osDelay(2);
	}
	I2C_CURRENT_STATE = I2C_STATE_BUSY;
	
	if(address_to_read>=8192)
	{
		return L_I2C_WRITE_ERROR_ADDRESS_OUT_OF_RANGE;
	}
	
	
	//This is the default setting based on the schematic
	//here is the simulate code
	i2c_start_process();
	
	//1010 000 X
	//first, send this to the I2C bus, which means
	//device type identifier , hardware address , and read/write mode
	i2c_send_byte(0b10100000); // write mode
	
	//switch sda to input mode and get the response
	sda_init_input();
	uint8_t response = i2c_get_ack(250);
	//if something went wrong, abort the process.
	if(response)
	{
		i2c_stop_process();
		return L_I2C_WRITE_ERROR_UNEXPECTED_ACK;
	}
	
	
	//address!
	uint8_t address_p1 = address_to_read >> 8;
	uint8_t address_p2 = address_to_read << 8 >> 8;
	
	// now send the address period1 (8 of 16 bits total. 0~8191)
	sda_init_output();
	i2c_send_byte(address_p1);
	
	//switch sda to input mode and get the response
	sda_init_input();
	response = i2c_get_ack(250);
	
	//if something went wrong, abort the process.
	if(response)
	{
		i2c_stop_process();
		return L_I2C_WRITE_ERROR_UNEXPECTED_ACK;
	}
	
	// now send the address period2 (8 of 16 bits total. 0~8191)
	sda_init_output();
	i2c_send_byte(address_p2);
	
	//switch sda to input mode and get the response
	sda_init_input();
	response = i2c_get_ack(250);
	
	//if something went wrong, abort the process.
	if(response)
	{
		i2c_stop_process();
		return L_I2C_WRITE_ERROR_UNEXPECTED_ACK;
	}
	
	
	
	//Now stop the process to save the address then we can read the data from the chip by the address
	i2c_stop_process();
	
	osDelay(5);
	
	//Now start reading
	i2c_start_process();
	
	
	//1010 000 X
	//first, send this to the I2C bus, which means
	//device type identifier , hardware address , and read/write mode
	i2c_send_byte(0b10100001); // read mode
	
	//switch sda to input mode and get the response
	sda_init_input();
	response = i2c_get_ack(250);
	//if something went wrong, abort the process.
	if(response)
	{
		i2c_stop_process();
		return L_I2C_WRITE_ERROR_UNEXPECTED_ACK;
	}
	
	
	//Now get the data
	uint8_t byte_to_get = 0;
	for(int32_t i=7;i>=0;i--)
	{
		byte_to_get += i2c_get_ack(25) << i;
		L_I2C_DELAY();
	}
	
	
	//Now stop the process
	sda_init_output(); //reset SDA state
	
	i2c_send_bit(1);
	
	i2c_stop_process();
	
	
	I2C_CURRENT_STATE = I2C_STATE_IDLE;
	return byte_to_get;
	
	
	
	
	
}

需要在主函数中先初始化(使用这三行):

SCL_L();
SDA_L();
I2C_CURRENT_STATE=I2C_STATE_IDLE;

注:需要调用的话,项目需要使用stm32cubemx生成,并且必须使用FreeRTOS,因为使用了osDelay函数,也可以使用别的操作系统,只需要修改对应的delay函数即可。

初始化完成后,即可在FreeRTOS任务中调用这两个函数:

int16_t i2c_write_byte(uint16_t address_to_write,uint8_t byte_to_write);
uint8_t i2c_read_byte(uint16_t address_to_read);

由于我的ide不能输入中文,所以使用了英语注释。

因为I2C总线需要使用SDA引脚进行输入和输出,所以需要让SDA对应的GPIO端口能够随时切换输入输出模式,具体代码如下:

图1:SDA输入输出切换

注意在初始化输出模式时要启用上拉电阻,因为对于STM32芯片而言,SDA接受的回复数据是低电平,如果不启用上拉电阻,就会分不清SDA接受到的低电平信号是否为从设备发出;其中SDA_H()代表使SDA输出高电平信号,其他如XXX_H(),XXX_L()以此类推。

I2C协议简述:

总而言之,最基本的原则是当SDA端口输出数据时,SDA的信号变化需要在SCL为低电平时完成,而当处理器发出开始或停止信号时需要通过让SDA的信号在SCL为高电平时变化完成;当SDA在接受回复时,处理器需要发出SCL的时钟信号,当SCL为高电平时,从设备将发出对应信号,此时进行接收即可。

实现思路如下:

  先立下前提:发送数据前后,SDA和SCL均处于低电平状态;然后定义函数实现i2c开始信号和停止信号的发出,先初始化信号让SCL和SDA处于正确的状态(先令SDA变化到正确的状态,再将SCL置为高电平),然后改变SDA,最后再将SCL和SDA切换为低电平状态(先将SCL置零再将SDA置零),代码如下:

图2:开始停止信号

  接着定义函数实现i2c发送单位数据:

先将SDA切换为要发送的一位信号,然后将SCL置一以确认发送数据,再将SCL和SDA置零(先将SCL置零再将SDA置零),具体实现如下:

                                                                图3:I2C发送单位数据

随后通过调用上面定义的i2c_send_bit函数实现发送一个字节并进行必要的测试,通过移位运算和按位与实现取待发送字节的每一位,具体实现如下:

图4:I2C发送字节数据

  接着定义函数实现接受单位数据:

假定已经将SDA切换为输入模式,以防万一先将SDA置一,然后将SCL置一,再通过SDA_READ函数(是HAL_GPIO_ReadPin的宏定义)获取SDA的信号,这里需要使用循环多次获取信号直至超时;最后重置SCL信号,代码如下:

                                                        图5:I2C读取SDA单位信号

下面是实现指定地址写入读取:

  由于操作系统的不同任务可能会同时使用I2C总线,所以需要定义一个状态I2C_CURR

ENT_STATE表示I2C总线是否被占用中,当写入或读取函数调用时先检查I2C_CURR

ENT_STATE,若未在占用则将I2C_CURRENT_STATE设置为“占用中”,然后再开始对应的操作,完成操作后再将I2C_CURRENT_STATE设置为“空闲”。

  根据AT24C64D的文档与开发板原理图,得知该从设备AT24C64D标识为1010,硬件地址为000;再根据I2C协议文档以及AT24C64D的文档,得知具体流程:

在处理器发出开始信号后,要先发送设备标识、设备地址、以及R/W信号(0读1写),即要写入就要先按顺序发送10100000,要读取即发送10100001。

对于写入一个字节,处理器要释放SDA端口的控制权,然后AT24C64D会通过SDA端口发送一个低电平的回复信号,然后处理器需要发送数据地址(由文档得知AT24C64D的地址范围是0~8191,即13位地址,但处理器每次只能连续8位数据,所以需要连续发送两个字节代表地址,且这十六位数据的前三位为无效位),然后处理器要释放SDA端口的控制权以接收AT24C64D通过SDA端口发送的回复信号,随后发送写入的一个字节数据,最后处理器发出停止信号。

对于指定地址读入一个字节,前面的流程和写入流程类似,只是处理器不需要发送要写入的数据,直接发出停止信号,然后处理器再发出开始信号,再发送10100001表示读取,然后处理器要释放SDA端口的控制权以接收AT24C64D通过SDA端口发送的回复信号,接着处理器连续接收8位SDA信号,最后处理器发送停止信号即可。

根据思路编写函数:

图6:I2C公开函数

 

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值