VIP用户可以用PC浏览器下载pdf,非VIP用户请电邮14518918@qq.com索取图文并茂的pdf或源码。

一、I2C从机编程
安装完Simplicity Studio 5,可以在这个目录下,找到EFM8LB1原厂评估板适配的所有外设的原厂例程:D:\SiliconLabs\SimplicityStudio\v5\developer\sdks\8051\v4.3.1\examples\EFM8LB1_SLSTK2030A。但官方例程的I2C从机代码,D:\SiliconLabs\SimplicityStudio\v5\developer\sdks\8051\v4.3.1\examples\EFM8LB1_SLSTK2030A\SMBus\SlaveMultibyteHWACK,我瞟了一眼,很简洁,该用到的中断标志位都用到了;但实战价值不高,不如我“偷师”前同事的代码好用。
然而,万万没想到,我抄来的代码,几年下来,看似I2C主机读写都一切正常,但仍然有bug。这就有点操蛋了,我一方面抱怨同事无私共享的代码是在给咱埋坑,一方面咱自己一不不认真学习官方demo代码,二不不认真研读官方用户手册,真是有点那个“升米恩,斗米仇”的意思了。源码的bug在于,认为当I2C主机写时序发送STOP时,会落入case SMB_SRSTO: //0x10这个分支,当I2C主机读时序发送STOP时,会落入case SMB_STSTO: //0x50这个分支。但事实上,无论是I2C主机读还是写,只要发送STOP,都只会落入case SMB_SRSTO: //0x10这个分支;而case SMB_STSTO: //0x50这个分支,实际上是总线发送了一个异常STOP触发的,并不能被想当然地理解为是I2C主机读时序发送STOP时引发的。
我是真拿同事代码跑过I2C从机,还拿用户手册核对过SMB0CN0寄存器的每个bit位的含义,我当时以为我已经懂了代码。结果还真不是,几年后的今天加了测试代码才知道,当主机发起正常的I2C读时序,根本不会进入case SMB_STSTO: //0x50这个分支。Deepseek分析,早在从机接收到NACK时,TXMODE标志位就已经被清零,再等到STOP这里,SMB0CN0&0xF0就只会有STO=1,所以会跑到(STO1)的0x10这个分支去,而不是落入(TXMODE1)&&(STO1)这个0x50的分支了。这里,我们可以试想一下,一个I2C随机读时序,当从机上报最后一个bit的SDA=0,此刻主机不想读下一个数据了就应该发NACK通知从机释放总线,单主机没有发NACK而是发了一个STOP,此刻就会落入(TXMODE1)&&(STO==1)这个0x50的异常分支,那从机就应该主动复位SMB硬核以释放SDA总线,免得挂死I2C总线。
让我们再次看看Deepseek总结的表格,关于SMB0CN0寄存器高4位的几种重要状态,它决定了在SMB_ISR中固件该如何应对主机的各段I2C时序:

SMB0CN0 & 0xF0	Bit7 MASTER	Bit6 TXMODE	Bit5  STA	Bit4 STO	含义	必要性
0x20	0	0	1	0	地址匹配 + 主机要写或要读	✅ 必须
0x00	0	0	0	0	收到数据字节(接收模式)	✅ 必须
0x40	0	1	0	0	已发送数据字节(发送模式)	✅ 必须
0x10	0	0	0	1	检测到STOP(当时是Receiver)	✅ 必须
0x50	0	1	0	1	检测到STOP(当时是Transmitter)	⚠️ 建议保留

所以,起手就可以先定义一堆SMB0CN0高4位相关的宏:

#define SMB_SRADD 0x20   // 地址匹配(主机写从机地址)  
#define SMB_SRDAT 0x00   // 收到数据(主机写数据)  
#define SMB_STDAT 0x40   // 发送数据(主机读数据)  
#define SMB_SRSTO 0x10   // STOP(主机读写结束)  
#define SMB_STSTO 0x50   // STOP(异常STOP)  

然后再定义一个I2C从机状态机的枚举类型,以状态机来分辨当前主机的I2C时序已经进行到那个阶段。动用I2C状态机,明显好处是方便标识I2C时序的流转。比如,当从机收到第一个8bit数时,如果当前的I2C状态机vI2cState==I2C_STATE_ADDR_W,这说明之前收到了START+SlaveAddr(0),那么这里从机收到的第一个8bit数就应该是RegOffsetAddr了,那么很自然地就应把收到的第一个8bit数据,赋值给RegOffsetAddr,并置I2C状态机vI2cState=I2C_STATE_W_OFFSET表示主机写了个RegOffsetAddr下来。当从机收到第二个8bit数据时,由于当前I2C状态机vI2cState=I2C_STATE_W_OFFSET,所以能判断出第二个8bit数应该是DataWrite[0]了,那么很自然地就应把收到的第二个8bit数赋值给RAM_arr[RegOffsetAddr]。还有一个地方,SFF-8472 MSA的采样值,是2个Byte,在给采样值变量赋值时,需要先判断I2C状态机vI2cState 是否等于I2C_STATE_INIT,如果不等于,说明主机正在访问I2C从机,那么此刻MCU最好不要对A2的DDM变量赋值,免得MCU刚更新完一个采样值的MSB,还没有来得及更新LSB,就被主机读走了newMSB+oldLSB,这会引发数据紊乱的,试想前一刻是0x00FF,现在是0x0100,结果主机读回去的值是0x01FF,是不是就错了嘛?

//I2C从设备状态机序号  
enum Enum_I2CSTatemachine  
{  
    I2C_STATE_INIT = 0,     // 0,初始态  
    I2C_STATE_ADDR_R,       // 1,START+SlaveAddress(1)  
    I2C_STATE_ADDR_W,       // 2,START+SlaveAddress(0)  
    I2C_STATE_W_OFFSET,     // 3,START+SlaveAddress(0)+RegisterOffset  
    I2C_STATE_W_DATA,       // 4,START+SlaveAddress(0)+RegisterOffset+WriteData[n]  
    I2C_STATE_R_DATA,       // 5,START+SlaveAddress(1)+或接着ReadData[n]+ACK  
    I2C_STATE_STOP,         // 6,START+SlaveAddress(0)+ ... +STOP  
    I2C_STATE_NACK          // 7,START+SlaveAddress(1)+ReadData[n]+NACK  
};  
enum Enum_I2CSTatemachine data vI2cState;//I2C从机状态机  

再定义一些I2C从机相关的全局变量:

INT8U data SlaveAddr;		//SMB从机地址  
INT8U data RegOffsetAddr;	//从机寄存器偏移地址  
INT8U data RegData;		//从机收到的数据  
INT8U data RegData_arr[7];	//待发送数组数据,实际A0对应下标0,A2对应下标2,A4对应下标4   

再定义一些I2C从机的RAM缓存的全局变量,是联合体变量,其中A0Lower、

A2Lower、A2Table01h是SFF-8472 MSA规定的区域,A2Table03h是我自定义的工作区域。
union _UN_A0_LOWER_  xdata uA0Lower;    //SFF8472协议A0上半页128B联合体  
union _UN_A2_LOWER_  xdata uA2Lower;    //SFF8472协议A2上半页128B联合体  
union _UN_A2_TABLE3_ xdata uA2Table01h; //A2下半页A2Table01 128B数组对应USER EEPROM  
union _UN_A2_TABLE3_ xdata uA2Table03h; //A2下半页A2Table03 128B联合体MCU主控  

这里拿I2C主机发起的单字节随机写时序为例,下图可观察到SMBCN0的各个bit置1的情况,以及自拟的I2C从机状态机的流转情况。

这里拿I2C主机发起的单字节随机读时序为例,下图可观察到SMBCN0的各个bit置1的情况,以及自拟的I2C从机状态机的流转情况。

要不说美帝国科技发展迅猛,只因别人不卷,datasheet都写得事无巨细。下面的I2C Slave State Diagram,更是犹如行云流水,沁我心脾。简单说,就是I2C时序上的每过一次8bit或每过一次ACK,就会触发一次I2C中断。注意下面的I2C时序是立即写和立即读时序,故没有向从机写入偏移地址的操作;而我们通常是要实现随机写和随机读,是需要向从机先写入偏移地址再发起数据写入或数据读出;但两者核心的中断标志位是一致的。

好了,前面铺垫了那么多,现在让我们来看看EFM8LB12的I2C从机的中断服务程序。我个人认为,这是最简洁的I2C从机代码,比STM32/GD32的复杂又晦涩的I2C中断标志位,理解起来真的容易很多。不得不感概,这就是SilicaonLabs原厂的技术魅力!

//SMB中断服务程序  
void SMBus_ISR(void) interrupt SMBUS0_IRQn  
{  
    if (0 == SMB0CN0_ARBLOST)   // 总线仲裁未丢失  
    {  
        switch (SMB0CN0 & 0xF0) //解析SMBus控制和状态标志位  
        {  
        case SMB_SRADD: //0x20,地址匹配(主机写从机地址)  
            SMB0CN0_STA = 0;           // Clear SMB0CN0_STA bit  
            SlaveAddr = (SMB0DAT & 0xFE);//所以最后一个bit=0  
            if ((SlaveAddr == SLAVE_ADDR_A0) || (SlaveAddr == SLAVE_ADDR_A2)) // If received address matched  
            {  
                SMB0CN0_ACK = 1;              //发送响应ACK,拉低SDA  
                if ((SMB0DAT & 0x01) == 0x01) //如果收到的从机地址bit0=1代表主机读  
                {  
                    SMB0DAT = RegData_arr[(SlaveAddr & 0x07)]; //立即发送数据  
                    RegOffsetAddr++;  
                    RegData_arr[(SlaveAddr & 0x07)] = Get_Next_Val(SlaveAddr, RegOffsetAddr);//准备好下一个待发送数据  
                    vI2cState = I2C_STATE_ADDR_R; //I2C从机状态机=I2C_STATE_ADDR_R  
                }  
                else                          //如果收到的从机地址bit0=0代表主机写  
                {  
                    vI2cState = I2C_STATE_ADDR_W; //I2C从机状态机=I2C_STATE_ADDR_W  
                }  
            }  
            else                       //如果收到的从机地址不是A0/A2  
            {  
                // address matched, but illegal  
                SMB0CN0_ACK = 0;     //发送不响应NACK,释放SDA  
            }  
            //喂狗  
            WDT0_feed();  
            break;  
  
        case SMB_STDAT: //0x40,主机读数据  
            if (SMB0CN0_ACK)      //如果主机发送ACK,需要继续读  
            {  
                SMB0DAT = RegData_arr[(SlaveAddr & 0x07)]; //预取数据  
  
                RegOffsetAddr++;  
                RegData_arr[(SlaveAddr & 0x07)] = Get_Next_Val(SlaveAddr, RegOffsetAddr);  
                vI2cState = I2C_STATE_R_DATA; //I2C从机状态机=I2C_STATE_R_DATA  
            }  
            break;  
  
        case SMB_SRDAT: //0x00,主机写数据,注意第一个数据是偏移地址  
            if (I2C_STATE_ADDR_W == vI2cState)    // First byte is RegOffsetAddr  
            {  
                RegOffsetAddr = SMB0DAT;   // Save RegOffsetAddr  
                SMB0CN0_ACK = 1;           //发送响应ACK,拉低SDA  
                RegData_arr[SlaveAddr & 0x07] = Get_Next_Val(SlaveAddr, RegOffsetAddr); //预取数据  
                vI2cState = I2C_STATE_W_OFFSET; //I2C从机状态机=I2C_STATE_W_OFFSET  
                uA2Table03h.sStr.vDriverMappingOffset = RegOffsetAddr; //记录主机下发的寄存器地址起始值  
                uA2Table03h.sStr.vDriverMappingLength = 0;             //主机写Driver映射区的字节长度清零  
                uA2Table03h.sStr.vDriverMappingWritenFlag = 0;         //主机写Driver映射区的标志位清零  
                uA2Table03h.sStr.vDriverMappingTableIndex = 0;         //主机写Driver映射区的页码序号清零  
            }  
            else                                // Second byte is data,then save data  
            {  
                RegData = SMB0DAT;  
                SMB0CN0_ACK = 1;           //发送响应ACK,拉低SDA  
                Update_Buff(SlaveAddr, RegOffsetAddr, RegData);  
                RegOffsetAddr++;  
                RegData_arr[SlaveAddr & 0x07] = Get_Next_Val(SlaveAddr, RegOffsetAddr); //预取数据  
                //将收到的数据存入缓存vDriverMappingBuffer[],然后主机写Driver映射区的字节长度vDriverMappingLength++  
                vDriverMappingBuffer[uA2Table03h.sStr.vDriverMappingLength++] = RegData;  
                vI2cState = I2C_STATE_W_DATA; //I2C从机状态机=I2C_STATE_W_DATA  
            }  
            break;  
  
        case  SMB_SRSTO: //0x10,主机读写结束  
            SMB0CN0_STO = 0;           // SMB0CN0_STO Driver be cleared by software  
            if (I2C_STATE_W_DATA == vI2cState) //之前状态机是主机写从机  
            {  
                //uA2Lower.sStr.PowerLeveling +=0x01;//测试代码  
                //前面有写A0Lower/A2Lower[00..5F]/A2.Table00h或01h/A2Table03h,STOP之后应置位写FLASH标志位  
                if (g_byFlashUpdate)   
                {  
                    vSFPMappingWritenFlag = 1;  
                }  
                  
                //主机写A2A2[60..7F],或A2Table04h..A2Table77h  
                if (SLAVE_ADDR_A2 == SlaveAddr)  
                {  
                    //如果主机写的是A2[60..7F],应该进入Timer0_ISR立即处理以缩短主机等待时间  
                    if ((0x60 <= RegOffsetAddr) && (0x7F >= RegOffsetAddr))  
                    {  
                        //软件置TF0=1,强制触发一次Timer0中断  
                        TCON |= TCON_TF0__BMASK;  
                    }  
                    //如果主机写A2Table04h..A2Table77h这是外设芯片映射区,应该进入Timer0_ISR立即处理以缩短主机等待时间  
                    if ((0x80 <= RegOffsetAddr) && (0x03 < uA2Lower.sStr.TableIndex))   
                    {  
                        //主机写Driver映射区的标志位置1,到主循环中引发MCU转写Driver的操作  
                        uA2Table03h.sStr.vDriverMappingWritenFlag = 1;  
                        //记录主机写Driver映射区的页码序号=当前TableIndex  
                        uA2Table03h.sStr.vDriverMappingTableIndex = uA2Lower.sStr.TableIndex;  
                        //软件置TF0=1,强制触发一次Timer0中断  
                        //TCON |= TCON_TF0__BMASK;  
                    }  
                }  
                vI2cState = I2C_STATE_STOP; //I2C从机状态机=I2C_STATE_STOP  
            }  
            else //之前状态机不是主机写从机数据,那大概率是主机读从机  
            {  
                //uA2Lower.sStr.PowerLeveling +=0x10;//测试代码  
                g_byFlashUpdate = 0;  
                vI2cState = I2C_STATE_INIT; //I2C从机状态机=I2C_STATE_INIT  
            }  
            break;  
  
        case  SMB_STSTO: //0x50,异常STOP,需复位SMBus模块和状态机  
        // 请注意,主机读结束,是不会进入这个分支的,原因是NACK就把TXMODE标志位清零了,  
        // 然后再到主机读时序的STOP这里,SMB0CN0 & 0xF0 = 0x10会跑上面那个分支去了。  
            SMB0CN0_STO = 0;                   // 清中断标志位  
            SMB0CF &= ~SMB0CF_ENSMB__ENABLED;  // 禁止SMBus  
            SMB0CF |=  SMB0CF_ENSMB__ENABLED;  // 启用SMBus  
            //uA2Lower.sStr.PowerLeveling +=0x10;//测试代码  
            g_byFlashUpdate = 0;               //清g_byFlashUpdate  
            vI2cState = I2C_STATE_INIT;        //I2C从机状态机=I2C_STATE_INIT  
        break;  
  
        default:         //其他未定义状态,需复位SMBus模块和状态机  
            SMB0CF &= ~SMB0CF_ENSMB__ENABLED;  // 禁止SMBus  
            SMB0CF |=  SMB0CF_ENSMB__ENABLED;  // 启用SMBus  
            SMB0CN0_STA = 0;   // 手动清除STA  
            SMB0CN0_STO = 0;   // 手动清除STO  
            SMB0CN0_ACK = 1;   // 发送响应ACK  
            //uA2Lower.sStr.PowerLeveling +=0x20;//测试代码  
            g_byFlashUpdate = 0;  
            vI2cState = I2C_STATE_INIT; //I2C从机状态机=I2C_STATE_INIT  
            break;  
        }//end of  switch (SMB0CN0 & 0xF0)  
    }  
    else   // 总线仲裁丢失  
    {  
        SMB0CN0_STA = 0;   // 手动清除STA  
        SMB0CN0_STO = 0;   // 手动清除STO  
        SMB0CN0_ACK = 1;   // 发送响应ACK  
    }  
          
    SMB0CN0_SI  = 0;   // 清除SMBus中断标志位  
}  

这里,还调用了两个辅助函数,Get_Next_Val()和Update_Buff(),用于对RAM缓存进行读写:

//从RAM数组取所需数据  
INT8U Get_Next_Val(INT8U bySlaveAddr, INT8U vI2cOffset)  
{  
    INT8U data byVal;  
  
    switch (bySlaveAddr)  
    {  
    case SLAVE_ADDR_A0:  
        if (128 > vI2cOffset) //A0上半页  
        {  
            byVal = uA0Lower.pbyBuf[vI2cOffset];  
        }  
        break;  
    case SLAVE_ADDR_A2:  
        if (128 > vI2cOffset) //A2上半页  
        {  
            //主机回读A2密码设置和密码输入区  
            if ((vI2cOffset >= 119) && (vI2cOffset <= 126))  
            {  
                byVal = g_PasswordLevel;  //密码区不可读,返回密级值  
            }  
            else  
            {  
                byVal = uA2Lower.pbyBuf[vI2cOffset];  
            }  
        }  
        else                   //A2下半页  
        {  
            switch (uA2Lower.sStr.TableIndex)  
            {  
            case 0: //USER EEPROM  
            case 1: //  
                byVal = uA2Table01h.pbyBuf[vI2cOffset - 128];  
                break;  
            case 3: //MCU Control  
                if (2 == g_PasswordLevel) //用户已输入PW2,使A0Table3可读写  
                    byVal = uA2Table03h.pbyBuf[vI2cOffset - 128];  
                else  
                    byVal = 0xFF;  
                break;  
            default://其他table未定义,啥也不是  
                byVal = 0xFF;  
                break;  
            }//end of switch(TableIndex)  
        }//end of else  
        break;  
    case SLAVE_ADDR_A4:  
        byVal = uA4.pbyBuf[vI2cOffset];  
        break;  
  
    default:  
        byVal = 0xFF;  
        break;  
    }//end of switch (bySlaveAddr)  
  
    return byVal;  
}  
  
//用主机写下来的RAM数据,更新到对应的RAM数组并置写FLASH标志位  
void Update_Buff(INT8U bySlaveAddr, INT8U vI2cOffset, INT8U byDat)  
{  
    switch (bySlaveAddr)  
    {  
    case SLAVE_ADDR_A0:  
        if (128 > vI2cOffset) //A0上半页  
        {  
            if (96 > vI2cOffset) //A0[0..95]是OEMPassword密级写保护  
            {  
                if (0 < g_PasswordLevel) //用户已输入PW1或PW2,使A0[0..95]可写  
                {  
                    uA0Lower.pbyBuf[vI2cOffset] = byDat;  
                    g_byFlashUpdate = 0xA0; //准备下一步就要保存到FLASH  
                }  
            }  
            else  
            {  
                uA0Lower.pbyBuf[vI2cOffset] = byDat;  
                g_byFlashUpdate = 0xA0; //准备下一步就要保存到FLASH  
            }  
        }  
        break;  
    case SLAVE_ADDR_A2:  
        if (128 > vI2cOffset) //A2上半页  
        {  
            if (96 > vI2cOffset) //A2[0..95]是OEMPassword密级写保护  
            {  
                if (0 < g_PasswordLevel) //用户已输入PW1或PW2,使A0[0..95]可写  
                {  
                    uA2Lower.pbyBuf[vI2cOffset] = byDat;  
                    g_byFlashUpdate = 0xA0; //准备下一步就要保存到FLASH  
                }  
            }  
            else if ((110 == vI2cOffset) || (111 == vI2cOffset) || ((127 == vI2cOffset))) //A0[110/111/127]无写保护任意可写  
            {  
                uA2Lower.pbyBuf[vI2cOffset] = byDat;  
            }  
            else if ((119 <= vI2cOffset) && (126 >= vI2cOffset)) //A0[119..126]是密码区  
            {  
                uA2Lower.pbyBuf[vI2cOffset] = byDat;  
                g_byUpHostPWFlag |= (INT8U)(1U << (vI2cOffset - 119));  
            }  
        }  
        else                   //A2下半页  
        {  
            switch (uA2Lower.sStr.TableIndex)  
            {  
            case 0: //USER EEPROM,无密码可写  
            case 1:  
                uA2Table01h.pbyBuf[vI2cOffset - 128] = byDat;  
                g_byFlashUpdate = 0xA0; //准备下一步就要保存到FLASH  
                break;  
            case 3: //MCU control  
                if (2 == g_PasswordLevel) //用户已输入PW2,使A0Table3可读写  
                {  
                    uA2Table03h.pbyBuf[vI2cOffset - 128] = byDat;  
                    //欲保存Table3/80h/81h到FLASH,需用户设置uA2Table03h.sStr.ControlByte='S'  
                }  
                break;  
            default://其他table未定义,啥也不是  
                break;  
            }//end of switch(TableIndex)  
        }//end of else  
        break;  
    case SLAVE_ADDR_A4:  
        uA4.pbyBuf[vI2cOffset] = byDat;  
        break;  
    default://其他非A0/A2从机,啥也不是  
        break;  
    }//end of switch (bySlaveAddr)  
}  

注意,I2C总线仲裁丢失是一个小分支,我们需要将关注重点,集中在对(SMB0CN0 & 0xF0)的判断分支上,也就是前面提到的Deepseek总结的表格,关于SMB0CN0寄存器高4位的几种重要状态那个表格。
注意,case SMB_SRADD: //0x20这个分支,是从机地址匹配引发的,无论是主机读从机(START+SlaveAddress(1))还是主机写从机(START+SlaveAddress(0)),都会落入这个分支。落进来之后,代码是通过分别接收到的SlaveAddress的bit0的值,来判断主机接下来是要读从机还是要写从机的。如果是要读从机,则从机应立即将事先准备好的8bit数据RegData_arr[(SlaveAddr & 0x07)],赋值给SMB0DAT寄存器,这样在接下来的9个SCL,I2C从机硬核就可以把SMB0DAT寄存器值逐bit上报给主机。这个赋值SMB0DAT的动作一定要快,这是限制I2C从机带宽的重要瓶颈。完了还需把偏移地址还需要自加1即RegOffsetAddr++1,并准备好下一个待发送数据RegData_arr[(SlaveAddr & 0x07)] = Get_Next_Val(SlaveAddr, RegOffsetAddr)。
注意case SMB_STDAT: //0x40这个分支,是主机读从机数据引发的。注意从机上报的第一个数据是在ReSTART+SlaveAddress(1)引发的case SMB_SRADD: //0x20这个分支里面,就已经赋值给了SMB0DAT。等主机打完9个SCL把第一个数据读走,就会引发case SMB_STDAT: //0x40这个分支,此刻MCU也应立即将事先已经准备好下一个待发送数据RegData_arr[(SlaveAddr & 0x07)],赋值给SMB0DAT寄存器。完了还需把偏移地址还需要自加1即RegOffsetAddr++1,并准备好下一个待发送数据RegData_arr[(SlaveAddr & 0x07)] = Get_Next_Val(SlaveAddr, RegOffsetAddr)。注意注意,由于限制I2C从机带宽的重要瓶颈,就是应付主机读,所以事先准备好下一个待发送数据,就显得非常重要。这就是为什么推荐直接从RAM获取待发送数据,而不推荐从FLASH去获取待发送数据的原因,于是我的习惯就是开一片RAM缓存(实际上是数组和结构体union成的联合体),来做I2C从机的数据交互载体。
注意case SMB_SRDAT: //0x00这个分支,是主机写数据到从机引发的。注意主机写出的第一个数据是偏移地址RegOffsetAddr,后面写出的数据才是匹配到偏移地址的寄存器值。
注意case SMB_SRSTO: //0x10这个分支,是主机读或主机写结束均会引发的。很多事情要在STOP的之后才能开始处理。比如主机写了A0Lower区域,MSA规定需要保存到EEPROM,那么在STOP分支,就要置位一个标志位vSFPMappingWritenFlag=1,完了退回到while(1)主循环中去做耗时很长的数据保存操作。比如主机写了A2[119…126]这8Byte的密码区,就要置位Timer0的TF0中断标志位,退出I2C_ISR之后就会立即进入Timer0_ISR,去处理需要高速响应的密码核对或修改事宜。比如主机写了A2[110].bit6,它是SoftTxDisable,MSA要求在SoftTxDisable改变的100ms内,硬件必须关断或使能光发射机,如果while(1)里面有一个长时间操作一直占着CPU,在100ms内没法去响应SoftTxDisable,那么也应该在Timer0_ISR中去处理这个SoftTxDisable。所以在STOP分支里面,为了快速响应主机写部分特殊区域而做出相应处理,有强制触发一次Timer0中断的代码TCON |= TCON_TF0__BMASK;由于TF0这个中断标志位是RW的,所以软件置1也能触发Timer0中断,从而得以进入Timer0_ISR()去处理一些需要快速响应的事务,以缩短主机的等待时间。


// Timer0中断服务程序 (ISR)  
void Timer0_ISR(void) interrupt TIMER0_IRQn  
{  
    // 注意,TF0进入ISR后硬件会自动清零中断标志位,所以ISR中无需手工再写1清0中断标志位  
    TCON &= ~TCON_TF0__BMASK;  
      
    // 停止Timer0计数,避免重入  
    TCON &= ~TCON_TR0__BMASK;    
  
    uA2Lower.sStr.PowerLeveling ++;//如果A2Lower[6Fh]有自加1,则说明Timer0_ISR被触发  
  
    //如果密码区A2[119..126]有任意byte被更新,就需要立即处理密码相关事务  
    if (g_byUpHostPWFlag)  
        //如果A2[123..126]有任意byte被更新,则将重新核定密级g_PasswordLevel,  
        //密级=1是输了用户级密码,=2是输入了工厂级密码,=3是输入了Bootloader密码  
        CheckPassword();  
  
    //如果用户输入了UserPassword之后又更改了A2[119..122],就拿A2[119..122]赋值给UserPassword  
    ChangeUserPassword();  
  
    if (1 == uA2Table03h.sStr.vDriverMappingWritenFlag) //主机写了需要转录到外设的映射区  
    {  
        uA2Table03h.sStr.vDriverMappingWritenFlag = 0;//标志位清零,避免重入  
        //把被主机改写的RAM区(映射到Driver的寄存器片段,从vDriverMappingOffset开始的vDriverMappingLength个字节),写进外挂Driver寄存器  
    }  
}  

为了满足SMB协议,SCL低电平持续时间太长,就超时退出。在初始化SMB单元时,就启用了SCL低电平超时检测功能,SMB0CF |= SMB0CF_SMBTOE__SCL_TO_ENABLED;此代码将自动把SCL与Timer3关联起来,当SCL高电平时,Timer3被强制重载(保持不溢出);当SCL低电平时,Timer3就开始计数;如果SCL低电平时间过长,Timer3就会溢出从而触发中断Timer3中断。Timer3在初始化时,被配置成溢出时间=25ms。所以这里又有一个Timer3_ISR()函数:

//Timer3中断服务程序,由SCL低电平时间超时触发  
void TIMER3_ISR(void) interrupt TIMER3_IRQn  
{  
        SMB0CF &= ~SMB0CF_ENSMB__ENABLED;  // 禁止SMBus  
        SMB0CF |=  SMB0CF_ENSMB__ENABLED;  // 启用SMBus  
        TMR3CN0 &= ~TMR3CN0_TF3H__BMASK; // Clear Timer3 interrupt-pending flag  
}  

至于EFM8的I2C从机编程相关的管脚初始化、SMB硬核初始化、Timer初始化、中断初始化、Crossbar初始化,RAM到FLASH的数据互转等,都是比较常规的操作,请自行阅读代码,我都有中文注释,辅之以AI,只要有心学习,您肯定能快速理解的。
这是Keil工程源码:

二、固件项目
本项目基于Keil C51的uVision V5.38.0.0。
common.h有#include必要的其他头文件,有定义结构体、联合体、枚举等复杂数据类型,有申明定义在common.c中的全局变量,有申明定义在其他c文件中的函数名,所以,本项目的所有c文件,都可以简单地#include common.h,就能整活儿了。
SFP_main.c,是main()函数所在地,完成了外设初始化,就进入while(1)主循环。固件版本号就再开头。
IRQ.c,是中断服务函数所在地。包括Timer0、Timer3、Timer4、SMB、PCA等ISR都在这里处理。
Config.c,是各种外设初始化函数所在地。
Control.c,是控制外设的函数所在地。
History.txt,是必不可少的更新记录,救命的,务必详细。

三、上位机软件
上位机软件是基于CVI IDE自编的myI2C.exe V1.0.0.69。

1,固件下载
先搜索CH341这个USB转I2C主机,后搜索EFM8LB12的I2C Bootloader地址0xF0。

然后点击Tab2里面的“EFM8OneKeyRun”按键,选中磁盘上的固件可烧录文件《project.hex》,然后开始自动下载固件,结束后固件将自动运行,上位机将自动搜索到A0/A2从机地址。手动点击“SearchAllSlaveI2C”按键,也可搜索到A0/A2从机地址。

2,固件升级
一样的,点击Tab2里面的“EFM8OneKeyRun”按键,选中磁盘上的固件可烧录文件《project.hex》,然后上位机首先会输入JMP密码迫使使MCU跳转到I2C Bootloader,出现0xF0从机地址后就开始自动下载固件,结束后固件将自动运行,上位机将自动搜索到A0/A2从机地址。

3,EEPROM回读
切换到Tab1,点击“ReadE2”按键,应有按照SFF-8472 MSA解析出来的从机上报信息,比如温度和电压采样值,比如PN/SN等:

4,OEM密码输入
往Tab1界面输入A2的OEM密码,点击“SetPW”按键将OEM密码写入从机的A2[7B…7E],然后点击“Read-L”按键,将回读并显示A2[00…7F]到表格,可见密码区回读值已经是OEM密级值0x01了,而且A2[6F]也从0加到1。

5,数据写入
由于此刻已经获取到OEM权限,可以修改A2[00…5F]了。可以双击表格的[00]单元,把原先的55改成00,回车写入从机。再到Tabl1点击“ReadE2”按键,应看到Temperature_H_Alarm阈值从原先的85变成了0,而且A2Checksum的LED亮红灯示警。完了把表格的[00]单元改回55,一切恢复原样。

6,工厂密码输入,得以访问私密区域A2Table03h
只有输入了工厂密码,获取到工厂密级,才能回读到A2Table03h的值。可见密码区回读值已经是FTY密级值0x02了,而且A2[6F]也从1加到2。

上位机改变A2Table03h[FA]=0x61,MCU会自清零并翻转P1.2电平。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐