I2c是一种双向串行通讯标准,常用于嵌入式系统中。利用I2c总线可以利用有限的I/O接口来扩展多功能的外围设备。主要由SCL(时钟线)和SDA(数据线组成)。I2c总线上可以连接多个带有I2c接口的设备,每个设备都有自己唯一的地址。设备地址一般看该设备对应的手册。当总线空闲的时候SDA线和SCL线都为高电平,如果SCL处于高电平时SDL产生下降沿则认为起始位,如果SCL处于高电平SDA产生上升沿时则为停止位。
主要讲的是Stm32配置I2c协议成主发送从接收模式,我们之前看到的都是调用STM32的I2c的官方库函数来配置I2c,今天呢我们是自己配置寄存器来写一个I2c的库函数。
第一步:开启时钟外设看STM32对应的手册来配置相应的寄存器。我写的是STM32f303所以看对应手册开启时钟外设寄存器为RRC_APB1ENR寄存器。再次强调对应的芯片不同手册里面要配置的寄存器也不一样,一切要按照对应的手册写,这里只是提供一个参考。
///Reinitialize the I2C peripheral registers to their default reset values
void I2c::Initialize(uint8_t channel)
{
base = ( I2C_TypeDef * ) ( I2C1_BASE + ( ( channel - 1 ) * baseRegOffset ) );
if (_I2c1 == channel)
{
// I2C 1 clock enable
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
}
else if (_I2c2 == channel)
{
//I2C 2 clock enable
RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;
}
}
第二步:配置时钟频率(一般模式,快速模式,高速模式)。主要配置以下几个寄存器。把以下几个寄存器配置好了I2c的时序也相应的会产生。
///set I2C master Clock frequency
void I2c::ConfigClk(uint8_t SCLL, uint8_t SCLH, uint8_t PECSC, uint8_t SDADEL, uint8_t SCLDEL)
{
Disable();
//I2CCLK timing setting.
base->TIMINGR &= ~I2C_TIMINGR_SCLL_Msk;
base->TIMINGR &= ~I2C_TIMINGR_SCLH_Msk;
base->TIMINGR &= ~I2C_TIMINGR_PRESC_Msk;
base->TIMINGR &= ~I2C_TIMINGR_SDADEL_Msk;
base->TIMINGR &= ~I2C_TIMINGR_SCLDEL_Msk;
base->TIMINGR |= SCLL << I2C_TIMINGR_SCLL_Pos;
base->TIMINGR |= SCLH << I2C_TIMINGR_SCLH_Pos;
base->TIMINGR |= PECSC << I2C_TIMINGR_PRESC_Pos;
base->TIMINGR |= SDADEL << I2C_TIMINGR_SDADEL_Pos;
base->TIMINGR |= SCLDEL << I2C_TIMINGR_SCLDEL_Pos;
Enable();
}
设置频率时我这里给了一个例子可以参考,往相应的寄存器输入值,这个值也是在手册里面找的。不能自己乱写哈。
///Set clockspeed
///Mode of timings settings
///Standard mode Fast-mode Fast-mode Plus
void I2c::I2cClockSpeed(uint8_t speed)
{
//Standard mode100 kHz
if (100 == speed)
{
ConfigClk(0x13, 0xf, 1, 0x2, 0x4);
}
//Fastmode mode 400kHz
if (400 == speed)
{
ConfigClk(0x9, 0x3, 0, 0x1, 0x3);
}
//Fastmode Plus
if (500 == speed)
{
ConfigClk(0x6, 0x3, 0, 0x0, 0x01);
}
}
第三步:初始化对应的Gpio引脚,在这里要注意的是一般I2c都配置成开漏输出就可以了。
///Reinitialize the I2C peripheral registers to their default reset values
///I2Cx: where x can be 1, 2 to select the I2C peripheral
void I2c::ConfigPins(I2cPinConfig pinConfig)
{
Gpio sclPin;
Gpio sdaPin;
sclPin.Initialize(pinConfig.sclCh, pinConfig.sclPin);
sclPin.ConfigAltFunc(pinConfig.sclAltNum);
sclPin.ConfigMode(Gpio::_Alt);
sclPin.ConfigSpeed(Gpio::_HighSpeed);
sdaPin.Initialize(pinConfig.sdaCh, pinConfig.sdaPin);
sdaPin.ConfigAltFunc(pinConfig.sdaAltNum);
sdaPin.ConfigMode(Gpio::_Alt);
sdaPin.ConfigSpeed(Gpio::_HighSpeed);
sclPin.ConfigOutputType(Gpio::_OpenDrain);
sdaPin.ConfigOutputType(Gpio::_OpenDrain);
sclPin.ConfigInputType(Gpio::_NoPull);
sdaPin.ConfigInputType(Gpio::_NoPull);
}
第四步:我们可以看对应手册进行读写了。先看I2c作为主模式下是如何写的。我们可以看到写的流程图及产生以下代码:
写过程:
///sends one byte
void I2c::WriteByte(const uint8_t txData, uint8_t address)
{
//Configure slave address and configure direction to write
base->CR2 = ( base->CR2 & ~( I2C_CR2_SADD_Msk | I2C_CR2_RD_WRN ) ) | address;
//Set number of bytes to be write
base->CR2 = ( base->CR2 & ~I2C_CR2_NBYTES_Msk ) | ( 1 << I2C_CR2_NBYTES_Pos );
//Allow 12C module to send STOP automatically after all bytes are transferred
base->CR2 |= I2C_CR2_AUTOEND;
//Send a start condition to begin writing data
base->CR2 |= I2C_CR2_START;
while ( !( base->ISR & I2C_ISR_TXIS ) ) {}
base->TXDR = txData;
}
读过程:
///Recevice one byte
uint8_t I2c::ReadByte(uint8_t address)
{
uint8_t rxData;
//Configure slave address and configure direction to read
base->CR2 = (base->CR2 & ~I2C_CR2_SADD_Msk) | address | I2C_CR2_RD_WRN;
//Set number of bytes to be read
base->CR2 = (base->CR2 & ~I2C_CR2_NBYTES_Msk) | 1 << I2C_CR2_NBYTES_Pos;
//Allow 12C module to send STOP automatically after all bytes are transferred
base->CR2 |= I2C_CR2_AUTOEND;
//Send a start condition to begin receiving data
base->CR2 |= I2C_CR2_START;
while (0 == base->ISR & I2C_ISR_RXNE) {}
rxData = base->RXDR;
return rxData;
}
分析一下以上代码,结合上面的流程图就是先确定寻址找到从机地址确定读写方向,设置要读写的字节。我这里
给的例子是读写的一个字节所以我就给了一个1到CR2_NBYTES该寄存器里面。配置一个停止信号、一个start起始
信号和配置一个从机设备地址。有人看到这里可能会问ACK,NACK不要我们自己写吗,是的不用,我们主机只负
责发送一个start信号和一个停止信号给从机设备,从机设备硬件会自己给出应答包。最后我会放一张用逻辑分析仪
分析I2c读写的Eeprom过程图片方便更好理解。
当看到逻辑分析仪scl线和sda线由高电平被拉低就是代表通讯开始,从机接收成功会对应发送ACK。所以ACK应答不需要主机写。配置I2c的库函数到这里感觉一切要以对应的手册为基准。大概我对于I2c的理解就是这些了。下次我们写一下I2c读取Eeprom该注意的地方。