在首次工作经历中,接到了用软件模拟IIC从机的任务。使单片机起到一个监视器的作用,将主机与从机间的通信数据读取出来,并用串口分段打印出来的任务。
与普通的软件模拟IIC主机模式相比,软件模拟IIC从机模式,要处理的问题会更加复杂。但本实验只涉及到了实现从机模式的只读状态,故难度也会有所降低。
一开始时,我先是想用IO口直接读取SDA和SCL线上的数据信号,但查阅了其他文章后发现。如果直接用IO口去接收高低电平信号存在的弊端过多,具体如下。
-
实时性要求高:I2C 通信依赖于精确的时序。使用 I/O 口读取数据要求软件能够以足够高的速率轮询 I/O 状态,以确保不会错过任何通信事件。
-
软件复杂性:需要编写复杂的软件逻辑来处理 I2C 协议的时序,包括起始条件、数据位、应答位、停止条件等。
-
中断处理困难:在 I2C 通信过程中,通常需要使用中断来处理数据的接收和发送。使用 I/O 口直接读取数据信号将难以实现高效的中断驱动通信。
-
硬件支持限制:许多微控制器提供硬件 I2C 支持,包括自动处理 ACK 位、发送和接收数据等。不使用这些硬件功能将浪费硬件资源。
-
功耗问题:轮询 I/O 口的状态会增加 CPU 的工作量,从而增加功耗。硬件支持的 I2C 通常具有更优的功耗管理。
-
速度限制:软件模拟 I2C 通信很难达到微控制器硬件 I2C 支持的速度。
-
错误处理:硬件 I2C 通常具有错误检测机制,如超时检测、仲裁丢失等。软件模拟需要实现这些错误检测机制,增加了开发难度。
-
可移植性差:直接使用 I/O 口实现 I2C 通信的代码依赖于特定的硬件特性,这降低了代码的可移植性。
-
电磁兼容性问题:硬件 I2C 接口通常设计有特定的电磁兼容性(EMC)特性,而软件模拟可能没有这些特性,导致通信不稳定。
-
维护和调试难度:软件模拟 I2C 通信的代码通常难以维护和调试,特别是在通信速率较高或系统复杂度较大的情况下。
故在借阅其他文章后选择使用中断的方式对IIC数据进行解析,并用串口对其进行打印。具体思路如下。
使用中断来解析IIC时序:
1.中断计数器:用来记录SCL触发上升沿中断的次数,如果计够8次,则一字节数据接收完成。计到第九位时,计数器清零,开始接收下一字节数据。
2.起始信号:当SDA触发下降沿中断时,且此时SCL处于高电平时则为起始信号,并在检测到起始信号时将计数器清空。
3.接收数据:当检测到起始信号后,计数器开始对SCL上升沿中断继续计数,当计够八次时,完成一字节数据的接收,将一字节数据存入一个数组内。
4.停止信号:当SCL为高电平时,SDA触发上升沿中断时,则说明产生停止信号,并在检测到停止信号后向数组内插入标志信号。
5.串口分段打印:整IIC通讯结束后,用串口将数据分段发送到电脑。在检测到停止位信号后,向数组内出入一段标志信号(该标志不能与SDA数据冲突)。当检测到标志信号后,用串口向电脑发送换行符。在上位机中选用文本接收模式。并用snprintf函数将文本模式下接收到的SDA数据改为HEX数据。实现串口分段打印。
例程代码如下:
#include "stm32f10x.h" // Device header
#include "MYI2C/myi2c.h"
#include "SERIAL/serial.h"
#include "Delay.h"
// 用于存储接收到的数据的数组
uint16_t i2c_rx_buffer_index = 0; //数组索引值
uint8_t i2c_rx_buffer[256]={0}; //存放数据数组
uint8_t detected = 0; //信号标志位
uint8_t scl_interrupt_count = 0; //SCL计数器
//使用GPIOB的PB6作为SDA,PB7作为SCL
#define SDA_GPIO_PORT GPIOB
#define SDA_GPIO_PIN GPIO_Pin_0
#define SCL_GPIO_PORT GPIOB
#define SCL_GPIO_PIN GPIO_Pin_1
void MI2C_SLAVE_init(void) //初始化函数
{
NVIC_InitTypeDef NVIC_InitStructure; //NVIC句柄
EXTI_InitTypeDef EXTI_InitStructure; //EXTI句柄
GPIO_InitTypeDef GPIO_InitStructure; //GPIO句柄
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //使能GPIO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //打开复用时钟
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_1|GPIO_Pin_0; //初始化GPIO
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING; //改成浮空输入模式//
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource0); //初始化EXTI0
EXTI_InitStructure.EXTI_Line = EXTI_Line0; //选择通道0接收SDA跳变
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //选用中断触发模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling; //选择双边沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //使能EXTI
EXTI_Init(&EXTI_InitStructure);
// SCL为上升沿中断
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1); //初始化EXTI1
EXTI_InitStructure.EXTI_Line = EXTI_Line1; //选择通道1接收SCL跳变
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; //选用中断触发模式
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; //选择上升沿跳变
EXTI_InitStructure.EXTI_LineCmd = ENABLE; //使能EXTI
EXTI_Init(&EXTI_InitStructure);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //配置NVIC为分组2
//SDA NVIC
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //初始化NVIC
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
//SCL NVIC
NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
void EXTI0_IRQHandler(void) //SDA中断处理函数
{
if (EXTI_GetITStatus(EXTI_Line0) == SET) //判断有无发生中断
{
if (GPIO_ReadInputDataBit(SCL_GPIO_PORT, SCL_GPIO_PIN) == Bit_SET) //判断当发生中断时,SCL是否为高电平
{
if (GPIO_ReadInputDataBit(SDA_GPIO_PORT, SDA_GPIO_PIN) == Bit_RESET) //如果此时SDA线为低电平,则说明检测到起始信号
{
detected = 1; //将detected置1,说明此时检测到起始信号
scl_interrupt_count = 0; //清空SCL计数器
}
else //否则,说明检测到停止信号
{
detected = 2; //将detected置2,说明此时检测到停止信号
i2c_rx_buffer[i2c_rx_buffer_index++] = 0xBF; //当检测到停止信号时,向数组内插入0xBF作为标志位,用于判断是否发生换行符
}
}
EXTI_ClearITPendingBit(EXTI_Line0); //清除中断标志位
}
}
void EXTI1_IRQHandler(void) //SCL上升沿中断处理函数
{
if (EXTI_GetITStatus(EXTI_Line1) == SET) //判断有无发生中断
{
if (detected == 1) //如果触发SCL上升沿中断,且此时检测到起始信号,此时开始接收数据
{
if (scl_interrupt_count < 8) //判断SCL计数值是否小于八
{
uint8_t bit = GPIO_ReadInputDataBit(SDA_GPIO_PORT, SDA_GPIO_PIN) == Bit_SET ? 1 : 0; //接收数据
i2c_rx_buffer[i2c_rx_buffer_index] |= (bit << (7 - scl_interrupt_count)); //将数据存入数组
scl_interrupt_count++; //SCL计数器加1
}
else //当SCL计数器值大于8时,说明一个字节数据接收完成
{
i2c_rx_buffer_index++; //将数组索引值加1,准备接收下一字节数据
scl_interrupt_count = 0; //重置计数器,准备接收下一字节
}
}
EXTI_ClearITPendingBit(EXTI_Line1); //清除SCL中断标志
}
}
void serial_init(void) //usart初始化函数
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate=9600;
USART_InitStructure.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode=USART_Mode_Tx;
USART_InitStructure.USART_Parity=USART_Parity_No;
USART_InitStructure.USART_StopBits=USART_StopBits_1;
USART_InitStructure.USART_WordLength=USART_WordLength_8b;
USART_Init(USART1,&USART_InitStructure);
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
USART_Cmd(USART1,ENABLE);
}
void USART_SendByte(USART_TypeDef *USARTx,uint8_t byte) // USART发送一个字节的函数
{
while (!(USARTx->SR & USART_SR_TXE));
USARTx->DR = byte;
while (!(USARTx->SR & USART_SR_TC));
}
void USART_SendBuffer(USART_TypeDef *USARTx,uint8_t *buffer, uint16_t length) // USART发送缓冲区数据的函数
{
for (uint16_t i = 0; i < length; i++)
{
if (buffer[i] == 0xBF) // 检查是否为0xBF,若是,则发送换行符
{
USART_SendByte(USARTx, '\r'); // 发送回车符
USART_SendByte(USARTx, '\n'); // 发送换行符
}
else //将文本模式下的数据转换成HEX数据
{
char hexStr[4];
snprintf(hexStr, sizeof(hexStr), "%02X", buffer[i]); // 将字节转换为HEX字符串
USART_SendByte(USARTx, hexStr[0]); // 发送HEX字符串的高位字符
USART_SendByte(USARTx, hexStr[1]); // 发送HEX字符串的低位字符
USART_SendByte(USARTx, ' '); // 发送空格作为分隔符
}
}
}
int main(void) //主函数
{
serial_init(); //初始化USART
MI2C_SLAVE_init(); //初始化I2C
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //初始化GPIO_10
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10) == Bit_RESET); //等待VCC上电
Delay_ms(10); //延时10模式
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10) == Bit_SET) //如果延时10ms后,VCC依旧属于高电平,则说明此时上电状态平稳。(主要用于处理上电后会出现杂波的问题)
{
while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10) == Bit_SET); //等待VCC掉电。当掉电结束后,说明I2C通信已经完成
USART_SendBuffer(USART1, i2c_rx_buffer,i2c_rx_buffer_index); //当I2C通讯完成后,将数据用串口发送到电脑
}
}
以上便是第一次工作总结。这途中遇到了许许多多的问题。大部分都是因为基础知识不够扎实,需要不断学习提升。