IIC软件模拟---从机的实现
一、前言
-
在网上找了半天,都没怎么看到很多关于IIC软件模拟,写从机的,一咬牙就自己尝试写了。在这个过程中学到了许多新东西(都是踩坑踩出来的),我记忆力差防止后面忘记了趁着梳理知识点,做个记录总结。
-
IIC通信中,从机与主机之间的通信,无非在于2点,接收数据和发送数据,而这就在时序上有着严密的要求。这篇文章主要以从机的视角出发,如何根据SCL上高低电平的变化来正确的实现接收和发送数据。
二、正文
- 关于SCL的控制,永远由主机来控制SCL电平的变换。
- 外部中断用途介绍:
SCL的上升沿---接收数据
SDA的上升沿---检测停止信号
SDA的下降沿 ---检测启动信号
- 关于枚举根据自己需求定义:
typedef enum{
IDLE = x1,
ADDR = x2,
RECEIVING = x3,
SENDING = x4
}Slave_State;
Slave_State state;
- 基础函数:
void IIC_W_SDA(uint8_t value)
{
GPIO_WriteBit(SDA_GPIO,SDA_PIN,(BitAction)value);
}
uint8_t IIC_R_SDA()
{
return GPIO_ReadInputDataBit(SDA_GPIO,SDA_PIN);
}
void IIC_W_SCL(uint8_t value)
{
GPIO_WriteBit(SCL_GPIO,SCL_PIN,(BitAction)value);
}
uint8_t IIC_R_SCL()
{
return GPIO_ReadInputDataBit(SCL_GPIO,SCL_PIN);
}
- 配置IO口模式
//配置SDA为输出
void SDA_Out(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = SDA_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; // 开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(SDA_GPIO, &GPIO_InitStructure);
}
//配置SDA为输入
void SDA_In(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = SDA_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(SDA_GPIO, &GPIO_InitStructure);
}
2.1 数据的接收:
- 读操作:当SCL:1,读取SDA上电平,这就需要从机捕捉SCL上的电平变化,那么如何捕捉电平的变换呢?
这就需要用到IO口的外部中断来实现:捕捉SCL的上升沿
//SCL
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource8); //IO口与中断线的映射(选择)
EXTI_InitStruct8.EXTI_Line=EXTI_Line8; //配置中断触发模式
EXTI_InitStruct8.EXTI_Mode=EXTI_Mode_Interrupt;
EXTI_InitStruct8.EXTI_Trigger=EXTI_Trigger_Rising;
EXTI_InitStruct8.EXTI_LineCmd=ENABLE;
EXTI_Init(&EXTI_InitStruct8);
EXTI_ClearITPendingBit(EXTI_Line8);//清除初始化造成的标志位
- 每次上升沿中断,能获取到的就是SDA线上的高低电平,也就相当于1bit,而一个字节8bit组成,因此需要中断8次才能获取一个完成的字节数据。
- 这就需要1个静态变量(recv_byte )来存储每一次的高低电平,1个静态变量(bit_count)记录接收的位数。
uint8_t sda = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9);
recv_byte <<= 1;
recv_byte |= sda;
bit_count++;
if(bit_count == 8){ //接收完成1个字节数据
......
}
- 在此说明:关于起始、停止信号的检测及数据的接收都是在外部中断函数中进行的,为了更加清楚的讲述,下面将其进行分开讲解。
void EXTI9_5_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line8) != RESET){//数据接收
EXTI_ClearITPendingBit(EXTI_Line8);
if(state == ADDR || state == RECEIVING){ //state状态机
//此处进行数据1bit的接收
uint8_t sda = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9);
Rx_byte <<= 1;
Rx_byte |= sda;
bit_count++;
if(bit_count == 8){ //接收完成1个字节数据
if(state == ADDR){
//判断地址是否正确。
//判断是读还是写:
// ....详情看下面....
}else if(state == RECEIVING){ //存储数据
buffer[i++] = Rx_byte;
}
}
}
}
if(EXTI_GetITStatus(EXTI_Line9) != RESET) { //判断起始及停止信号
EXTI_ClearITPendingBit(EXTI_Line9);
// ....详情看下面....
}
}
-
在IIC主从通信之间,每一次通话的发起都会由起始信号发起,停止信号结束,当SCL为高电平时,SDA由1-->0,为起始信号,SDA由0-->1为停止信号。
-
所以从机还需要捕捉SDA的上下降沿,这也就要用到IO口的外部中断。
//SDA
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource9); //IO口与中断线的映射(选择)
EXTI_InitStruct9.EXTI_Line=EXTI_Line9; //配置中断触发模式
EXTI_InitStruct9.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct9.EXTI_Trigger=EXTI_Trigger_Rising_Falling;
EXTI_InitStruct9.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct9);
EXTI_ClearITPendingBit(EXTI_Line9);//清除初始化造成的标志位
-
SDA的下降沿时,SCL保持为高电平则为起始信号:
-
SDA的上升沿时,SCL仍然保持高电平则为停止信号:
-
此处由于上升沿和下降沿都会触发中断产生,所以要区分具体是哪种类型触发的中断,就需要结合上一次的SDA的状态来判断,例如下图的last_sda(保存SDA上次的状态):
1.若last_sda :1,sda : 0 则判断为下降沿,这时scl:1则可以确定检测到起始信号。
2. 若last_sda :0,sda : 1 则判断为上升沿,这时scl:1则可以确定检测到停止信号。
//起始停止信号的判断
if(EXTI_GetITStatus(EXTI_Line9) != RESET) { //判断起始及停止信号
uint8_t scl = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_8);
uint8_t sda = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9);
static uint8_t last_sda = 1;
EXTI_ClearITPendingBit(EXTI_Line9);
// START 条件:SDA 下降沿,且 SCL 为高
if(last_sda == 1 && sda == 0 && scl == 1){
state = ADDR;
bit_count = 0;
Rx_byte = 0;
}else if(last_sda == 0 && sda == 1 && scl == 1) { //STOP
state = IDLE;
bit_count = 0;
Rx_byte = 0;
current_index = 0;
}
last_sda = sda;
}
- 在IIC的通信中,一段完整的通信格式:起始信号--地址(7bit)+读写(1bit)--应答--数据--停止信号。
- 这么多不同类型的数据,就需要状态机来记录当前过程的状态例如下段代码中的state;
- 在检测到起始信号后,会将状态置为state=ADDR,表示接下来接收的数据应为地址
if(last_sda == 1 && sda == 0 && scl == 1){
state = ADDR;
bit_count = 0;
Rx_byte = 0;
}
- 接收地址:若地址正确,则根据读写位,将状态state更改位RECEIVING、SENDING、若地址不匹配则改为IDLE;
if(state == ADDR){
if((Rx_byte>>1) == 地址){
if(Rx_byte & 0x01){
state = SENDING;
send_flag = SENDING;//发送数据标志位
}else {
state = RECEIVING;
}
IIC_SendAck(0);
}else { //地址不正确
IIC_SendAck(1);
state = IDLE;
}
}else if(state == RECEIVING) {
//数据存储
}
- 应答:在接收一个字节数据后,需要发送应答
在SCL为低电平时,将SDA拉低,下一个SCL为低电平时,从机释放SDA(SDA拉高)
/* 发动应答 */
void IIC_SendAck(uint8_t Ackbit)
{
SDA_Out();//IO口切换为输出
while(IIC_R_SCL()==1);//等待SCL低电平
IIC_W_SDA(Ackbit);//Ackbit:0 拉低SDA,准备ACK 1:非应答
while( IIC_R_SCL()==0); //等待主机升高SCL
//while(IIC_R_SCL()==1);//等待SCL低电平---为正确时序段释放SDA做准备
SDA_In(); //释放SDA,回到输入模式(上拉输入)
EXTI_ClearITPendingBit(EXTI_Line8); //清除SCL上升沿触发的中断标志位
EXTI_ClearITPendingBit(EXTI_Line9); //清除将IO口改为输入模式时置为的中断标志位
}
2.2 数据的发送:
- 数据发送:数据发送是将每个字节数据1bit按SCL节奏(SCL:0)发送到SDA上,这就需要考虑SCL电平的变化:
- 数据的发送:SCL稳定在低电平时,改变SDA的高低电平。
- 发送完一个字节后数据,需要留一个SCL信号读取主机返回的响应。
*关于发送数据时,检测SCL电平的变化,用while最合适,用上升沿,下降沿不好用。
//发送数据
void Send_Host_data(uint8_t *arry,uint8_t len)
{
uint8_t i;
SDA_Out();//IO口切换为输出
for(i=0;i<len;i++){
uint8_t j;
for(j=0;j<8;j++){ //单个字节的读取
while(IIC_R_SCL()==1);//等待SCL稳定在低电平
IIC_W_SDA((arry[i] & (0x80 >> j)));
while( IIC_R_SCL()==0); //等待主机升高SCL
}
//接收应答
while( IIC_R_SCL()==1); //等待下一个低电平
IIC_W_SDA(1);//释放SDA的控制权,SDA空闲
while(IIC_R_SCL()==0);//等待SCL拉高来接收应答
//读取SDA上的电平
}
send_flag = IDLE;//清除标志位
EXTI_ClearITPendingBit(EXTI_Line8); //清除SCL上升沿触发的中断标志位
EXTI_ClearITPendingBit(EXTI_Line9); //清除将IO口改为输入模式时置为的中断标志位
}
2.3 主函数逻辑
主函数里面主要进行初始化配置,及根据状态机state状态来发送数据
int main(void)
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
delay_init();
Initial_Configuration()
while(1)
{
if(state == SENDING){
if(send_flag){
if(数据类型1){
Send_Host_data(sendbuffer,len);
} else if(数据类型2)){
Send_Host_data(sendbuffer,len);
} ...
}
}
}
}
三、总结
-
关于写IIC的从机,需要非常注意时序,错一个,就会导致接收数据不对,调试过程中最好借助类似与示波器的工具。
-
同时要注意IO电平切换是的延时,否则中间几个电平变换,最后就执行了一个电平。
-
SCL的电平变换由主设备控制节奏。---这个也是我最开始写从机时思考的问题。
-
从机需要捕捉SDA和SCL的电平变化(外部中断实现),来实现数据的接收,停止及启动信号的检测。
-
IO口开漏输出模式下同时可以读取IO电平。
四、声明
在这个过程中,我也查看过其他博主的文章做参考找灵感,在此表示感谢!本文意在将此次自己所学的进行知识梳理,同时也希望能给其他同样正在写IIC从机这个方面的人提供一些参考,文章整理不易,引用时希望指明出处,谢谢。
浙公网安备 33010602011771号