STM32 GPIO模拟多路I2C访问
I2C/IIC/2-WIRE(相同的总线不同的命名方式)访问时序的实现可以通过硬件功能模块的配置控制实现,也可以通过GPIO模拟时序实现。
多路I2C设备的连接,如果每个I2C设备的地址不同,可以通过一路I2C总线连接各个设备,以发送协议的地址激活相应的I2C设备进行响应。
对于I2C设备地址相同的多个设备,则需要用多路并行I2C进行访问,通常硬件功能模块资源有限,就要通过GPIO模拟多路I2C总线实现。
这里的范例代码实现9路I2C总线模拟,因为模拟时序占用MCU时间,所以9路并不是同时进行访问,而是在要访问某一路I2C设备前,将内部代码逻辑对应控制的通道进行切换,所以是轮流而非并行的访问。
管脚的配置
首先建立基础工程并配置时钟后配置9路I2C的管脚,每路I2C配置为Open Drain的输出方式,可以设置为内部上拉,如果电路有外部上拉这里也可以设置为浮空。如: 保存后生成基础工程代码。
多路I2C代码实现
GPIO模拟多路I2C代码的实现分为几个部分
引入微秒延时函数
用于实现I2C时序延时的微秒延时函数,参见 STM32 HAL us delay(微秒延时)的指令延时实现方式及优化 。在代码里定义如下函数:
__IO float usDelayBase;
void PY_usDelayTest(void)
{
__IO uint32_t firstms, secondms;
__IO uint32_t counter = 0;
firstms = HAL_GetTick()+1;
secondms = firstms+1;
while(uwTick!=firstms) ;
while(uwTick!=secondms) counter++;
usDelayBase = ((float)counter)/1000;
}
void PY_Delay_us_t(uint32_t Delay)
{
__IO uint32_t delayReg;
__IO uint32_t usNum = (uint32_t)(Delay*usDelayBase);
delayReg = 0;
while(delayReg!=usNum) delayReg++;
}
void PY_usDelayOptimize(void)
{
__IO uint32_t firstms, secondms;
__IO float coe = 1.0;
firstms = HAL_GetTick();
PY_Delay_us_t(1000000) ;
secondms = HAL_GetTick();
coe = ((float)1000)/(secondms-firstms);
usDelayBase = coe*usDelayBase;
}
void PY_Delay_us(uint32_t Delay)
{
__IO uint32_t delayReg;
__IO uint32_t msNum = Delay/1000;
__IO uint32_t usNum = (uint32_t)((Delay%1000)*usDelayBase);
if(msNum>0) HAL_Delay(msNum);
delayReg = 0;
while(delayReg!=usNum) delayReg++;
}
在main.c源文件的系统时钟配置代码段后运行 PY_usDelayTest()和PY_usDelayOptimize()两个函数:
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
PY_usDelayTest();
PY_usDelayOptimize();
则可以使用微秒延时函数PY_Delay_us_t(x);
定义通道选择参数
I2C通道选择影响GPIO端口和管脚的对应关系,做如下的选择参数定义:
GPIO_TypeDef * I2C_SCL_PORT = GPIOB;
uint16_t I2C_SCL_PIN = GPIO_PIN_6;
GPIO_TypeDef * I2C_SDA_PORT = GPIOB;
uint16_t I2C_SDA_PIN = GPIO_PIN_7;
#define IIC1 1
#define IIC2 2
#define IIC3 3
#define IIC4 4
#define IIC5 5
#define IIC6 6
#define IIC7 7
#define IIC8 8
#define IIC9 9
void I2C_SEL(uint8_t I2Cn)
{
switch(I2Cn)
{
case IIC1:
I2C_SCL_PORT = GPIOB;
I2C_SCL_PIN = GPIO_PIN_6;
I2C_SDA_PORT = GPIOB;
I2C_SDA_PIN = GPIO_PIN_7;
break;
case IIC2:
I2C_SCL_PORT = GPIOB;
I2C_SCL_PIN = GPIO_PIN_4;
I2C_SDA_PORT = GPIOA;
I2C_SDA_PIN = GPIO_PIN_15;
break;
case IIC3:
I2C_SCL_PORT = GPIOA;
I2C_SCL_PIN = GPIO_PIN_8;
I2C_SDA_PORT = GPIOB;
I2C_SDA_PIN = GPIO_PIN_15;
break;
case IIC4:
I2C_SCL_PORT = GPIOB;
I2C_SCL_PIN = GPIO_PIN_13;
I2C_SDA_PORT = GPIOB;
I2C_SDA_PIN = GPIO_PIN_12;
break;
case IIC5:
I2C_SCL_PORT = GPIOB;
I2C_SCL_PIN = GPIO_PIN_10;
I2C_SDA_PORT = GPIOB;
I2C_SDA_PIN = GPIO_PIN_2;
break;
case IIC6:
I2C_SCL_PORT = GPIOB;
I2C_SCL_PIN = GPIO_PIN_0;
I2C_SDA_PORT = GPIOA;
I2C_SDA_PIN = GPIO_PIN_7;
break;
case IIC7:
I2C_SCL_PORT = GPIOA;
I2C_SCL_PIN = GPIO_PIN_5;
I2C_SDA_PORT = GPIOA;
I2C_SDA_PIN = GPIO_PIN_4;
break;
case IIC8:
I2C_SCL_PORT = GPIOA;
I2C_SCL_PIN = GPIO_PIN_2;
I2C_SDA_PORT = GPIOA;
I2C_SDA_PIN = GPIO_PIN_1;
break;
case IIC9:
I2C_SCL_PORT = GPIOC;
I2C_SCL_PIN = GPIO_PIN_15;
I2C_SDA_PORT = GPIOC;
I2C_SDA_PIN = GPIO_PIN_14;
break;
default: break;
}
}
定义公共I2C管脚输入输出函数
通过I2C_SEL()函数可以切换 I2C_SCL_PORT,I2C_SCL_PIN,I2C_SDA_PORT, I2C_SDA_PIN的对应关系,从而I2C公共输入输出函数定义为:
void SCL_OUT_H(void)
{
HAL_GPIO_WritePin(I2C_SCL_PORT, I2C_SCL_PIN, GPIO_PIN_SET);
}
void SCL_OUT_L(void)
{
HAL_GPIO_WritePin(I2C_SCL_PORT, I2C_SCL_PIN, GPIO_PIN_RESET);
}
void SDA_OUT_H(void)
{
HAL_GPIO_WritePin(I2C_SDA_PORT, I2C_SDA_PIN, GPIO_PIN_SET);
}
void SDA_OUT_L(void)
{
HAL_GPIO_WritePin(I2C_SDA_PORT, I2C_SDA_PIN, GPIO_PIN_RESET);
}
uint8_t SDA_IN(void)
{
return HAL_GPIO_ReadPin(I2C_SDA_PORT, I2C_SDA_PIN);
}
定义公共I2C时序访问函数
通过微秒延时函数的应用,以及I2C输出输入管脚的控制,实现公共I2C时序访问函数。注意us_num定义用于控制访问速度。
#define us_num 10
void I2C_Init(void)
{
I2C_SEL(IIC1);
SDA_OUT_H();
SCL_OUT_H();
I2C_SEL(IIC2);
SDA_OUT_H();
SCL_OUT_H();
I2C_SEL(IIC3);
SDA_OUT_H();
SCL_OUT_H();
I2C_SEL(IIC4);
SDA_OUT_H();
SCL_OUT_H();
I2C_SEL(IIC5);
SDA_OUT_H();
SCL_OUT_H();
I2C_SEL(IIC6);
SDA_OUT_H();
SCL_OUT_H();
I2C_SEL(IIC7);
SDA_OUT_H();
SCL_OUT_H();
I2C_SEL(IIC8);
SDA_OUT_H();
SCL_OUT_H();
I2C_SEL(IIC9);
SDA_OUT_H();
SCL_OUT_H();
}
void I2C_Start(void)
{
PY_Delay_us_t(us_num) ;
SDA_OUT_H();
SCL_OUT_H();
PY_Delay_us_t(us_num/2) ;
SDA_OUT_L();
PY_Delay_us_t(us_num/2) ;
SCL_OUT_L();
}
void I2C_Stop(void)
{
SCL_OUT_L();
PY_Delay_us_t(us_num) ;
SDA_OUT_L();
PY_Delay_us_t(us_num) ;
SCL_OUT_H();
PY_Delay_us_t(us_num) ;
SDA_OUT_H();
PY_Delay_us_t(us_num) ;
}
void I2C_Write_Ack(void)
{
PY_Delay_us_t(us_num/2) ;
SDA_OUT_L();
PY_Delay_us_t(us_num/2) ;
SCL_OUT_H();
PY_Delay_us_t(us_num) ;
SCL_OUT_L();
SDA_OUT_H();
}
uint8_t I2C_Read_Ack(void)
{
uint8_t status=0;
SCL_OUT_L();
PY_Delay_us_t(us_num/2) ;
SDA_OUT_H();
PY_Delay_us_t(us_num/2) ;
status = SDA_IN();
SCL_OUT_H();
PY_Delay_us_t(us_num) ;
SCL_OUT_L();
SDA_OUT_L();
return status;
}
void I2C_Send_Byte(uint8_t txd)
{
for(uint8_t i=0;i<8;i++)
{
PY_Delay_us_t(us_num/2) ;
if((txd&0x80)>>7) SDA_OUT_H();
else SDA_OUT_L();
txd<<=1;
PY_Delay_us_t(us_num/2) ;
SCL_OUT_H();
PY_Delay_us_t(us_num) ;
SCL_OUT_L();
}
SDA_OUT_L();
}
uint8_t I2C_Read_Byte(unsigned char rdack)
{
uint8_t rxd=0;
for(uint8_t i=0;i<8;i++ )
{
SCL_OUT_L();
PY_Delay_us_t(us_num/2) ;
SDA_OUT_H();
PY_Delay_us_t(us_num/2) ;
SCL_OUT_H();
rxd<<=1;
if(SDA_IN()) rxd++;
PY_Delay_us_t(us_num) ;
}
SCL_OUT_L();
SDA_OUT_H();
if (rdack) I2C_Write_Ack();
return rxd;
}
使用方式
首先调用I2C初始化函数I2C_Init(),其实就是将各路I2C通道的输出置为上拉高电平状态。 然后调用I2C通道选择函数I2C_SEL()选择要操作的I2C通道,如选择通道1为I2C_SEL(IIC1)。 接着就可以调用I2C_Send_Byte()和I2C_Read_Byte()进行写一个字节和读一个字节的访问。 使用者还需要根据具体I2C设备的协议时序要求,定义函数组合多次写字节和读字节以实现数据访问目的,通常包括写I2C 7位地址,写寄存器地址,写或读数据等。也可以根据需要,将单字节访问函数扩展为多字节访问函数。
SCL的可调特性
9路I2C访问占用了18个GPIO管脚。实际上,由于SCL是单向输出管脚,因此可以将9路I2C的时钟管脚SCL共用一个GPIO(如同一路I2C连接多路不同I2C地址设备场景),这样9路I2C的模拟就只需要用到10个GPIO(每路模拟I2C的SDA管脚必须是单独的)。需要注意SCL采用一拖多的情况下,时序会有所恶化,也就是会降低I2C的最高可访问速度。所以如果要每路I2C都跑得快,则每路I2C的SCL连接到单独的GPIO。
–End–
|