【STM32】软件I2C
I2C简介
I2C总线是一种串行、半双工的总线,主要用于近距离、低速的芯片之间的通信。I2C总线有两根双向的信号线,一根数据线SDA用于收发数据,一根时钟线SCL用于通信双方时钟的同步。
- 在一个i2c通讯总线中,可连接多个i2c通讯设备(分为主机和从机)。
- 主机有权发起和结束一次通信,从机只能被动呼叫。当总线上有多个主机同时启动总线时,i2c也具备冲突检测和仲裁的功能来防止错误产生。
- 每个连接到i2c总线上的器件都有一个唯一的地址(7bit或者10bit),且每一个器件都可以作为主机也可以作为从机(但同一时刻只能有一个主机)。
- 串行的8位双向数据传输速率在标准模式下可达100Kbit/s,快速模式下可达400Kbit/s,高速模式下可达3.4Mbit/s。
I2C协议
有几点需要重点说明,下面会详细介绍每一个环节
- SLAVE_ADDRESS有7bit格式或者10bit格式。
- 除开起始和终止信号,每次发送或者读取一字节数据后,都需要等待ack或者发送ack。
- 寄存器地址或者值也肯能占多字节,先发送高字节的数据。
起始和终止信号
起始信号:当SCL在高电平期间SDA从高电平向低电平切换
终止信号:当SCL在高电平期间SDA从低电平向高电平切换
数据有效性
SDA数据线在SCL的每一个时钟周期传输一位数据。
-
SCL高电平期间:SDA表示的数据有效,此时SDA的电平要稳定,SDA为高电平时表示数据“1”,为低电平时表示数据“0”。
-
SCL低电平期间:SDA的数据无效,一般在这个时候SDA进行电平切换,为下一次表示数据做好准备。
数据和地址按8位进行传输,先传输数据的高位,每次传输的字节数不受限制.
I2C地址及数据方向
I2C总线上的每一个设备都有自己的独立地址,主机发起通讯时,通过SDA信号线发送设备地址(SLAVE_ADDRESS)来查找从机.I2C协议规定设备地址可以是7位或者10位,实际中7位地址应用比较广泛.紧跟设备地址的一个数据位用来表示数据传输方向,第8位或第11位.
- 数据方向位为"1":表示主机由从机读数据
- 数据方向位为"0":表示主机向从机写数据
10bit slave_address要分两个字节传输,高字节需要补上标志位"11110".
I2C设备寄存器地址和数据
上文中,我们默认设备寄存器地址和数据都是一个字节,但是实际项目中寄存器地址可能是两字节,数据可能是两字节或四字节.这种情况下其实和单字节类似,只不过是将多字节拆分为单字节传输,中间还是需要ACK.
举例,16位寄存器地址和32位数据的读时序:
Burst模式
burst模式其实就是连续模式,连续写或者读.
还是只传输一个地址,但是值可以是多个,写入的地址则是在原地址上递增.
写寄存器时序,就不举例了.就是多接收数据,在不要数据的时候发送NACK,否则还是继续发送ACK.理论上,burst的长度是没有限制,但是实际I2C设备是有限制的,具体就需要看I2C设备的说明了.
应答和非应答信号
I2C的数据或者地址传输都带响应.响应包括"ACK"和"NACK"两种信号.作为数据接收端时,当收到I2C传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送"ACK"信号,发送方会继续发送下一个数据;若接收方希望结束数据传输,则向对方发送"NACK"信号.发送方接收到信号后会产生一个停止信号,结束信号传输.
注意一点,在等待应答信号前,需要释放SDA的控制权,将SDA总线置为高电平.
数据发送端会释放SDA的控制权,由数据接收端控制SDA,给发送端传输应答或非应答信号
-
SDA为高电平:表示非应答信号(NACK)
-
SDA为低电平:表示应答信号(ACK)
软件I2C GPIO引脚配置
这里先说一下,为什么要使用软件模拟i2c,硬件上不是已经实现了i2c吗?我们在stm32中直接将引脚配置成i2c,然后使用hal库不就可以实现i2c通信吗?
理论上是的,但是据说,STM32芯片I2C有bug,所以很少人愿意冒险。
所以,要使用软件模拟I2C的时候,GPIO需要配置为开漏输出,至于为什么这样配置,可以参考(2条消息) I2C 开漏输出与上拉电阻_jiangdf的博客-CSDN博客
软件I2C的实现
软件设计的几个目标
- GPIO配置由外部实现(其实也想放在这里实现的,但是使能GPIO时钟有点绕)
- 支持7位或10位SLAVE_ADDRESS
- 支持多字节寄存器地址或数据
- 支持多个I2C速率
先看头文件
/*
* @Date: 2023-06-12 11:03:57
* @LastEditors: zdk
* @LastEditTime: 2023-06-16 14:30:05
* @FilePath: \haptics_evb_h7_bootloaderd:\01Project\haptic\code_new\haptics_evb_h7\haptics_evb_h7\Core\Inc\i2c_sw.h
*/
#ifndef I2C_SW_H
#define I2C_SW_H
#ifdef __cplusplus
#define
extern "c"
{
#endif
#include "gpio.h"
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#define I2C_WRITE (0x00)
#define I2C_READ (0x01)
#define I2C_10BIT_SLAVE_ADDRESS_INDICATOR (0b11110)
typedef enum
{
Standard_Mode = 100 * 1000,
Fast_Mode = 400 * 1000,
Fast_Mode_Plus = 1000 * 1000,
} I2C_SW_Speed_Mode_e; //i2c速率 unit Hz
typedef enum
{
One_Byte = 1,
Two_Byte = 2,
Four_Byte = 4
} Data_Width_e;//数据长度(寄存器地址或者值的数据长度)
typedef enum
{
Slave_Address_7Bit,
Slave_Address_10Bit,
} Slave_Address_Width_e;//i2c slave address的位宽
typedef struct
{
uint16_t scl_pin;
uint16_t sda_pin;
GPIO_TypeDef* scl_port;
GPIO_TypeDef* sda_port;
I2C_SW_Speed_Mode_e speed_mode;
uint16_t slave_addr;
Slave_Address_Width_e slave_address_width;
Data_Width_e slave_reg_addr_width;
Data_Width_e slave_reg_value_width;
} I2C_SW_Handle_t;
/**
* @description: 检测slave address的i2c设备是否存在
* @param {I2C_SW_Handle_t*} handle
* @return {*}
*/
bool i2c_device_exist(I2C_SW_Handle_t* handle);
/**
* @description: 读寄存器
* @param {I2C_SW_Handle_t*} handle
* @param {uint32_t} reg_addr
* @param {uint32_t} len
* @param {uint32_t*} buf
* @return {*}
*/
int i2c_reg_read(I2C_SW_Handle_t* handle, uint32_t reg_addr, uint32_t len, uint32_t* buf);
/**
* @description: 写寄存器
* @param {I2C_SW_Handle_t*} handle
* @param {uint32_t} reg_addr
* @param {uint32_t} len
* @param {uint32_t*} buf
* @return {*}
*/
int i2c_reg_write(I2C_SW_Handle_t* handle, uint32_t reg_addr, uint32_t len, const uint32_t* buf);
#ifdef __cplusplus
#define
}
#endif
#endif
再看源文件,源文件中有多个本地函数可以介绍一下
/**
* @description: 延时函数,用于控制i2c频率
* @param {I2C_SW_Speed_Mode_e} speed_mode
* @return {*}
*/
static void i2c_delay(I2C_SW_Speed_Mode_e speed_mode);
/**
* @description: 将时钟信号拉低
* @param {I2C_SW_Handle_t*} handle
* @return {*}
*/
static void scl_l(I2C_SW_Handle_t* handle);
/**
* @description: 将时钟信号拉高
* @param {I2C_SW_Handle_t*} handle
* @return {*}
*/
static void scl_h(I2C_SW_Handle_t* handle);
/**
* @description: 将数据信号拉低
* @param {I2C_SW_Handle_t*} handle
* @return {*}
*/
static void sda_l(I2C_SW_Handle_t* handle);
/**
* @description: 将数据信号拉高
* @param {I2C_SW_Handle_t*} handle
* @return {*}
*/
static void sda_h(I2C_SW_Handle_t* handle);
/**
* @description: 读取数据信号的电平
* @param {I2C_SW_Handle_t*} handle
* @return {*}
*/
static char sda_read(I2C_SW_Handle_t* handle);
/**
* @description: 产生i2c起始信号
* @param {I2C_SW_Handle_t*} handle
* @return {*}
*/
static void i2c_start(I2C_SW_Handle_t* handle);
/**
* @description: 产生i2c终止信号
* @param {I2C_SW_Handle_t*} handle
* @return {*}
*/
static void i2c_stop(I2C_SW_Handle_t* handle);
/**
* @description: 产生i2c应答信号
* @param {I2C_SW_Handle_t*} handle
* @return {*}
*/
static void i2c_ack(I2C_SW_Handle_t* handle);
/**
* @description: 产生i2c非应答信号
* @param {I2C_SW_Handle_t*} handle
* @return {*}
*/
static void i2c_no_ack(I2C_SW_Handle_t* handle);
/**
* @description: 等待i2c应答信号
* @param {I2C_SW_Handle_t*} handle
* @return {*}
*/
static bool i2c_wait_ack(I2C_SW_Handle_t* handle);
/**
* @description: i2c发送一字节数据
* @param {I2C_SW_Handle_t*} handle
* @param {char} data
* @return {*}
*/
static void i2c_send_byte(I2C_SW_Handle_t* handle, char data);
/**
* @description: i2c读取一字节数据
* @param {I2C_SW_Handle_t*} handle
* @return {*}
*/
static char i2c_recv_byte(I2C_SW_Handle_t* handle);
/**
* @description: 发送slave address信息
* @param {I2C_SW_Handle_t*} handle
* @param {char} rw 读或者写方向
* @return {*}
*/
static bool i2c_send_slave_address_with_wait_ack(I2C_SW_Handle_t* handle, char rw);
/**
* @description: 发送要读或写的地址信息
* @param {I2C_SW_Handle_t*} handle
* @param {uint32_t} address
* @return {*}
*/
static bool i2c_send_one_reg_address_with_wait_ack(I2C_SW_Handle_t* handle, uint32_t address);
/**
* @description: 接收寄存器地址值信息
* @param {I2C_SW_Handle_t*} handle
* @param {uint32_t*} value
* @param {bool} no_ack
* @return {*}
*/
static bool i2c_recv_one_reg_value_with_ack(I2C_SW_Handle_t* handle, uint32_t* value, bool no_ack);
/**
* @description: 发送寄存器值信息
* @param {I2C_SW_Handle_t*} handle
* @param {uint32_t} value
* @return {*}
*/
static bool i2c_send_one_reg_value_with_wait_ack(I2C_SW_Handle_t* handle, uint32_t value);
static void i2c_delay(I2C_SW_Speed_Mode_e speed_mode)
{
uint32_t temp;
SysTick->LOAD = SystemCoreClock / 8 / (speed_mode * 2) - 1;
SysTick->VAL = 0X00; //
SysTick->CTRL = 0X01; //
do
{
temp = SysTick->CTRL; //
}
while((temp & 0x01) && (!(temp & (1 << 16)))); //
SysTick->CTRL = 0x00; //
SysTick->VAL = 0X00; //
}
static void scl_l(I2C_SW_Handle_t* handle)
{
HAL_GPIO_WritePin(handle->scl_port, handle->scl_pin, GPIO_PIN_RESET);
}
static void scl_h(I2C_SW_Handle_t* handle)
{
HAL_GPIO_WritePin(handle->scl_port, handle->scl_pin, GPIO_PIN_SET);
}
static void sda_l(I2C_SW_Handle_t* handle)
{
HAL_GPIO_WritePin(handle->sda_port, handle->sda_pin, GPIO_PIN_RESET);
}
static void sda_h(I2C_SW_Handle_t* handle)
{
HAL_GPIO_WritePin(handle->sda_port, handle->sda_pin, GPIO_PIN_SET);
}
static char sda_read(I2C_SW_Handle_t* handle)
{
return HAL_GPIO_ReadPin(handle->sda_port, handle->sda_pin);
}
static void i2c_start(I2C_SW_Handle_t* handle)
{
scl_h(handle);
sda_h(handle);
i2c_delay(handle->speed_mode);
sda_l(handle);
i2c_delay(handle->speed_mode);
scl_l(handle);
i2c_delay(handle->speed_mode);
}
static void i2c_stop(I2C_SW_Handle_t* handle)
{
scl_h(handle);
i2c_delay(handle->speed_mode);
sda_l(handle);
i2c_delay(handle->speed_mode);
sda_h(handle);
i2c_delay(handle->speed_mode);
}
static void i2c_ack(I2C_SW_Handle_t* handle)
{
scl_l(handle);
i2c_delay(handle->speed_mode);
sda_l(handle);
i2c_delay(handle->speed_mode);
scl_h(handle);
i2c_delay(handle->speed_mode);
scl_l(handle);
i2c_delay(handle->speed_mode);
sda_h(handle);
i2c_delay(handle->speed_mode);
}
static void i2c_no_ack(I2C_SW_Handle_t* handle)
{
sda_h(handle);
i2c_delay(handle->speed_mode);
scl_h(handle);
i2c_delay(handle->speed_mode);
scl_l(handle);
i2c_delay(handle->speed_mode);
}
static bool i2c_wait_ack(I2C_SW_Handle_t* handle)
{
sda_h(handle);
i2c_delay(handle->speed_mode);
scl_h(handle);
i2c_delay(handle->speed_mode);
int retry_cnt = 10;
while(retry_cnt--)
{
if(!sda_read(handle))
{
scl_l(handle);
i2c_delay(handle->speed_mode);
return true;
}
}
scl_l(handle);
i2c_delay(handle->speed_mode);
return false;
}
static void i2c_send_byte(I2C_SW_Handle_t* handle, char data)
{
for(int offset = 0; offset < 8; offset++)
{
scl_l(handle);
i2c_delay(handle->speed_mode);
if(data & 0x80)
sda_h(handle);
else
sda_l(handle);
i2c_delay(handle->speed_mode);
scl_h(handle);
i2c_delay(handle->speed_mode);
data <<= 1;
}
scl_l(handle);
i2c_delay(handle->speed_mode);
}
static char i2c_recv_byte(I2C_SW_Handle_t* handle)
{
sda_h(handle);
i2c_delay(handle->speed_mode);
char value = 0;
for(int offset = 0; offset < 8; offset++)
{
scl_h(handle);
i2c_delay(handle->speed_mode);
value <<= 1;
if(sda_read(handle))
value++;
scl_l(handle);
i2c_delay(handle->speed_mode);
}
return value;
}
static bool i2c_send_slave_address_with_wait_ack(I2C_SW_Handle_t* handle, char rw)
{
if(handle->slave_address_width == Slave_Address_7Bit)
{
i2c_send_byte(handle, (handle->slave_addr << 1) | rw);
if(!i2c_wait_ack(handle))
return false;
}
else if(handle->slave_address_width == Slave_Address_10Bit)
{
uint8_t h_addr = (I2C_10BIT_SLAVE_ADDRESS_INDICATOR << 7)
| ((handle->slave_addr >> 8 & 0x03) << 2) | rw;
i2c_send_byte(handle, h_addr);
if(!i2c_wait_ack(handle))
return false;
uint8_t l_addr = handle->slave_addr & 0xff;
i2c_send_byte(handle, l_addr);
if(!i2c_wait_ack(handle))
return false;
}
return true;
}
static bool i2c_send_one_reg_address_with_wait_ack(I2C_SW_Handle_t* handle, uint32_t address)
{
for(int j = 0; j < handle->slave_reg_addr_width; j++)
{
uint8_t offset = (handle->slave_reg_addr_width - 1 - j) * 8;
uint8_t temp_reg_addr = (address >> offset) & 0xff;
i2c_send_byte(handle, temp_reg_addr);
if(!i2c_wait_ack(handle))
return false;
}
return true;
}
static bool i2c_recv_one_reg_value_with_ack(I2C_SW_Handle_t* handle, uint32_t* value, bool no_ack)
{
for(int k = 0; k < handle->slave_reg_value_width; k++)
{
uint8_t offset = (handle->slave_reg_value_width - 1 - k) * 8;
*value |= (i2c_recv_byte(handle) << offset);
if(no_ack)
i2c_no_ack(handle);
else
i2c_ack(handle);
}
return true;
}
static bool i2c_send_one_reg_value_with_wait_ack(I2C_SW_Handle_t* handle, uint32_t value)
{
for(int k = 0; k < handle->slave_reg_value_width; k++)
{
uint8_t offset = (handle->slave_reg_value_width - 1 - k) * 8;
i2c_send_byte(handle, (value >> offset) & 0xff);
if(!i2c_wait_ack(handle))
return false;
}
return true;
}
我们可以看到,比其他博客多了好几个函数,那些都是因为需要兼容多字节寄存器地址或者数据使用的.其中i2c_delay打算另外写一篇文章详细介绍(但是目前好像只在100MHz的时候比较准)
对外接口就是这几个:
bool i2c_device_exist(I2C_SW_Handle_t* handle)
{
bool ret = 0;
i2c_start(handle);
ret = i2c_send_slave_address_with_wait_ack(handle, I2C_WRITE);
i2c_stop(handle);
return ret;
}
int i2c_reg_read(I2C_SW_Handle_t* handle, uint32_t reg_addr, uint32_t len, uint32_t* buf)
{
i2c_start(handle);
if(!i2c_send_slave_address_with_wait_ack(handle, I2C_WRITE))
goto fail;
if(!i2c_send_one_reg_address_with_wait_ack(handle, reg_addr))
goto fail;
i2c_start(handle);
if(!i2c_send_slave_address_with_wait_ack(handle, I2C_READ))
goto fail;
for(int i = 0; i < len; i++)
{
i2c_recv_one_reg_value_with_ack(handle, &buf[i], len - 1 == i);
}
i2c_stop(handle);
return 0;
fail:
i2c_stop(handle);
return -1;
}
int i2c_reg_write(I2C_SW_Handle_t* handle, uint32_t reg_addr, uint32_t len, const uint32_t* buf)
{
i2c_start(handle);
if(!i2c_send_slave_address_with_wait_ack(handle, I2C_WRITE))
goto fail;
if(!i2c_send_one_reg_address_with_wait_ack(handle, reg_addr))
goto fail;
for(int i = 0; i < len; i++)
{
if(! i2c_send_one_reg_value_with_wait_ack(handle, buf[i]))
goto fail;
}
i2c_stop(handle);
return 0;
fail:
i2c_stop(handle);
return -1;
}
那么以上一个软件I2C功能就完成了.
软件I2C的使用
首先,在STM32CubeMX中配置GPIO.
然后,实例化软件I2C句柄对象.
I2C_SW_Handle_t* handle;
handle = malloc(sizeof(I2C_SW_Handle_t));
handle->scl_pin = TEMP_I2C_SCL_Pin;
handle->scl_port = TEMP_I2C_SCL_GPIO_Port;
handle->sda_pin = TEMP_I2C_SDA_Pin;
handle->sda_port = TEMP_I2C_SDA_GPIO_Port;
handle->slave_addr = TMP117_I2C_ADDRESS;
handle->slave_address_width=Slave_Address_7Bit;
handle->speed_mode = Standard_Mode;
handle->slave_reg_addr_width = One_Byte;
handle->slave_reg_value_width = Two_Byte;
最后,调用接口
if(i2c_device_exist(handle))
printf("hello i2c_address=0x%02x is exist\r\n", handle->slave_addr);
uint32_t device_id = 0;
if(0 == i2c_reg_read(handle, 0x0f, 1, &device_id))
printf("device_id=0x%04x \r\n", device_id);
uint32_t temp = 0;
while(1)
{
if(0 == i2c_reg_read(handle, 0x00, 1, &temp))
{
printf("read_value=0x%04x read_temp=%0.5f\r\n", temp, (int32_t)temp * 0.0078125);
}
}