先贴代码:
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公开函数