EFM8的I2C从机编程
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电平。
更多推荐



所有评论(0)