新唐MS51从机模式下硬件IIC总线高速率读写的分析与实现
接触新唐MS51BA9AE其实很偶然,当然这个偶然也是存在中的必然,2020年以来的芯片缺货潮导致了ST的MCU几乎都全部沦陷,性价比非常高的STM8系列不仅价格高到离谱,还缺货根本买不到,无奈下改了设计投奔了新唐的N76E003-,再然后N76E003也涨价缺货了,就去觅了MS51BA9AE,好在后2者硬件再PCB上完全兼容,代码则几乎不需要修改,就用了几批次5k的样子,但是没什么卵用,MS51BA9AE一样沦陷了,后面我需要费点劲改成同封装国产航顺的Crotex-M0,这是后话。
MS51BA9AE的硬件IIC总线号称支持最高400kHz的总线速率,一开始用来模拟EEPROM的时候我是没什么担心的,一直到实际做下去才发现自己低估了这件事的难度,折腾了有2天没什么进展,甚至都不知道问题出在哪里。
整个设计是使用MS51的IIC总线来模拟串行EEPROM,设备会插到读卡器上进行读写,用来设置参数之类的东西,但是代码一跑起来的时候就一直没办法正常工作,示波器和逻辑分析仪看出来的图形有些奇奇怪怪的东西,却又不知道哪里来的,怀疑器件损坏,换了还是一样,甚至换成N76E003,情况也没什么大的变化,使用读卡器无法读出正确,使用我自己的代码做主机来读,效果也很是胡乱。
转头去新唐找了官方的BSP,编译、下载、运行,结果还是一样。
之后就是有些暗无天日的折腾,弄不清楚很不爽的那种折腾。在费了很大的周折,经过了大概十几个小时的各种操作之后,才把这几个奇怪的东西解释清楚,并且找到了解决方法,不喜欢看分析的可以拉到最后看结论。
首先我们来看官方BSP代码,BSP包可以到下面的地址下载,github上的貌似要新一些
https://www.nuvoton.com/export/resource-files/MS51_Series_BSP_Keil_V1.00.003.zip
https://github.com/OpenNuvoton/MS51BSP_KEIL
现在下载好BSP,BSP内关于IIC从机模式的示例在这个路径里:
.\MS51BSP_KEIL\MS51DA9AE_MS51BA9AE\SampleCode\RegBased\I2C_Slave\Keil
打开名字为I2C_Slave的Keil工程文件
首先我需要把从机地址改成0xA0,这是大多数EEPROM卡的从机地址
打开I2C_S.c,把第33行的
#define I2C_SLAVE_ADDRESS 0xA4
改成
#define I2C_SLAVE_ADDRESS 0xA0
编译,运行。
使用明华通用读卡器读卡,卡型号选择24C01A,读卡的结果如图:
可以看到除了第一和第二个字节之外,读取的数值全部是0xff,而例程内实际读取的是data_received数组内的值,这个数组在初始化之后保存的值是全0,所以正确的读出结果应该是全部为0x00。
为了找到问题所在,我修改了函数处理函数,引入一个数组来保存每次进入中断后的I2STAT寄存器的值,同时我设置了一个IO口,命名为INT,在进入中断的时候置INT=0退出中断前置INT=1,把SDA\SCL和INT连接到逻辑分析仪上寻找答案。
修改前BSP内的中断处理函数是这样的:
void I2C_ISR(void) interrupt 6
{
switch (I2STAT)
{
case 0x00:
STO = 1;
break;
case 0x60:
AA = 1;
break;
case 0x68:
P02 = 0;
while(1);
break;
case 0x80:
data_received[data_num] = I2DAT;
data_num++;
if (data_num == LOOP_SIZE)
{
data_num = 0;
AA = 0;
}
else
AA = 1;
break;
case 0x88:
data_received[data_num] = I2DAT;
data_num = 0;
AA = 1;
break;
case 0xA0:
AA = 1;
break;
case 0xA8:
I2DAT = data_received[data_num];
data_num++;
AA = 1;
break;
case 0xB8:
I2DAT = data_received[data_num];
data_num++;
AA = 1;
break;
case 0xC0:
AA = 1;
break;
case 0xC8:
AA = 1;
break;
}
SI = 0;
}
在I2C_S.c文件前部加入IO和数组定义:
sbit INT = P0^4;
unsigned char IICStatu[0x20]; //保存I2STAT的数组
unsigned char IICStatuPos; //写入IICStatud位置的指针
在初始化IIC接口函数Init_I2C_SLAVE内初始化INT引脚,加入一句P04_QUASI_MODE
现在函数变这样
void Init_I2C_SLAVE(void)
{
P13_OPENDRAIN_MODE; //set SCL (P13) is Open Drain mode, external pull up resister is necessary
P14_OPENDRAIN_MODE; //set SDA (P14) is Open Drain mode, external pull up resister is necessary
P04_QUASI_MODE;
IICStatuPos = 0; //IICStatu数组的指针归0
SDA = 1; //set SDA and SCL pins high
SCL = 1;
set_EIE_EI2C; //enable I2C interrupt by setting IE1 bit 0
set_IE_EA;
I2ADDR = I2C_SLAVE_ADDRESS; //define own slave address
set_I2CON_I2CEN; //enable I2C circuit
set_I2CON_AA;
}
修改中断处理函数,现在变成这样
void I2C_ISR(void) interrupt 6
{
unsigned char I2STATSave; //用于保存I2STAT
INT = 0; //进入中断后INT置0
I2STATSave = I2STAT; //保存本次I2STAT,也就是中断发生的原因
switch (I2STAT)
{
case 0x00:
STO = 1;
break;
case 0x60:
AA = 1;
break;
case 0x68:
P02 = 0;
while(1);
break;
case 0x80:
data_received[data_num] = I2DAT;
data_num++;
if (data_num == LOOP_SIZE)
{
data_num = 0;
AA = 0;
}
else
AA = 1;
break;
case 0x88:
data_received[data_num] = I2DAT;
data_num = 0;
AA = 1;
break;
case 0xA0:
AA = 1;
break;
case 0xA8:
I2DAT = data_received[data_num];
data_num++;
AA = 1;
break;
case 0xB8:
I2DAT = data_received[data_num];
data_num++;
AA = 1;
break;
case 0xC0:
AA = 1;
break;
case 0xC8:
AA = 1;
break;
}
SI = 0;
INT = 1;
IICStatu[IICStatuPos] = I2STATSave; //本次I2STAT存放进数组
IICStatuPos++;
IICStatuPos = IICStatuPos & 0x1f;
}
然后我们重新再用读卡器读一次,当然,结果还是一样不会有变化,除了第一第二个字节外,其他都读出为0xff。现在我们来看IICStatu数组的值:
在Keil观察窗口界面下数组一共被写入了6个字节,这很奇怪,因为读卡器实际上连续读了128个字节,加上起始条件、地址SLA+W、重复起始条件、SLA+R,读卡器应该进行了差不多130几个操作。
那么看下逻辑分析仪捕获了些什么
图上可以看出MCU被正确的寻址,进入了地址模式,然后接收了SLA+R,这之后正确地发送了第一个字节0x0,但是在发送第二个字节之后收到了NAK,于是退出了地址模式回到待机模式,后面读卡器的一连串操作实际上都被忽略了。
整个操作一共进入了6次中断,中断的原因分别的是:
1、0x60 接收到起始条件+SLA+W(自身地址+W),给主机应答了ACK,进入地址状态。
2、0x80 接收到主机写入的一个字节,给主机应答了ACK,这个字节实际是后面要操作的地址。
3、0xA0 接收到重复的起始条件
4、0xA8 在起始条件之后收到了SAL+R(自身地址+R),应答了ACK,准备发送数据
5、0xB8 向主机正确的发送了一个数据并收到了ACK应答,后续应该准备下一个数据。
6、0xC0 向主机正确的发送了一个数据并收到了NAK应答,退出地址状态变成未寻址。
放大看逻辑分析仪的波形,在第9个CLK的上升沿的位置,SDA的确是电平1,所以从机接收到了一个NAK,并且很奇怪的在第7个CLK与第8个之间有明显的间隔,参考上一个字节的波形,间隔是出现在第8和第9个CLK之间,主机从接收数据转到发送发送ACK,代码转换多费了时间,在这个位置CLK拉长是可以理解的,但是第7个CLK与第8个之间出现就有些奇怪了。
读卡器的读卡时序不可控,为了弄清楚问题,现在找了一个目标板,用运行自己代码的主机设备来读卡,在降低CLK频率到一定的程度时,终于,卡的读取正常了。
可以看到一连串的读取操作跟随了一连串的ACK,在每个ACK之前SDA都有个短暂的高电平,那是因为我的主机代码里在读了8bit数据之后,先拉低了SCL,再拉低SDA发ACK。SCL拉低之后MS51释放了SDA线准备接收ACK,所以出现一个短暂的高电平尖峰。
观察主机总线频率,自己的主机设备调整到能正常读取的情况下,SCL的频率大概是50kHz,明华读卡器则是在SLA+R之前使用大概50kHz的SCL,SLA+R之后的连续读写则变成了100kHz左右。那么号称支持400kHz总线数据的MS51为什么不能在100kHz下工作呢?又为什么会收到NAK应答呢?既然主机发送了NAK,那为什么后面还在继续发SCL读数据?
难道说,抓取到的总线波形里反应的并不全是主机的行为?
示波器抓取的波形看,又发现一个奇怪的地方。
将示波器的波形和逻辑分析仪的波形叠加,可以看到SCL在SLA+W的最后一位之后的下降沿之后,出线了大约8uS的其他电平跟加接近地电平的下凹,这在逻辑分析仪上是看不到的,但是在示波器上可以很明显的发现。脑子里转了很多种考虑,判断出现这种情况,应该这个电位不是由主机下拉造成,那既然不是主机下拉引起的,难道MS51做为从机在整个通讯过程中,对SCL施加了影响?在我以往的概念里,SCL由主机产生,从机难道不是应该跟随主机的SCL跳变来走,不去干涉SCL的状态的吗?
翻手册,找到这样一句话:
“SI由软件清0,在SI被清0之前,SCL低电平周期延长,传输暂停,该状态对于从机处理接收到的数据非常有用,可以确保准确处理前一数据再接收下一个数据。“
额,也就是说,在IIC总线规则里,从机发送了ACK之后,可以通过持续拉低SCL的方式暂停总线通讯,主机有义务在传输下一个字节之前判断SCL是否被释放,这个机制用来保证从机在处理刚接收到的数据时不会漏过下一个字节的传输,但是在平常写的模拟IIC主机代码里,大多数时候从来不去处理这一情形,包括通用读卡器的代码内也完全没有这个判断,所以对这种情况是全忽略,也完全没印象的。
所以,示波器看到的电平下陷,实际上是MS51在收到SLA+W之后,SI被置为1,同时总线控制器持续拉低SCL,一直到SI被软件清0为止,在波形上看这个下陷大概为8uS,对比逻辑分析仪INT引脚的低电平宽度,正好差不多是进入中断到退出中断之间的时长。
那么为什么通用读卡器100kHz读卡失败呢?我们用一张EEPROM卡,全写0之后读卡,看下波形和读取MS51模拟的卡片有什么区别呢?
通过对比读卡器读取IC卡和读取MS51的波形对比,现在我们很容易得出结论了,之所以会在第二个字节收到主机的NAK,实际上是因为第二个字节的开头第一个CLK被MS51的强制下拉拉低,这导致我们失去了一个CLK,直接造成了后面的CLK失步而应该收到ACK的CLK错误的被跳过变成了NAK。波形上可以看出,INT的低电平宽度持续了很长一段时间,这段时间代码处于中断处理函数内,SI持续=1,所以SCL被钳位于0。而读卡器并没有判断SCL的电平,而是自顾自直接开始了下一个时钟,在中断处理结束SI=0之后,MS51对SCL的钳位撤销,SCL可以恢复到高电平,但是这时候主机已经开始了下一个时钟,波形上看就像是一个CLK的低电平宽度被拉长了似的。
很明显,我们没办法调整成品读卡器的读卡时序,那就只有想办法让MS51在中断处理的过程,耗费的时间小于读卡器的CLK宽度。通过观察INT的低电平宽度,可以看到整个中断处理过程大约是10uS左右,这其实很快,但是对于1T的8051内核来说这似乎还不够快,我们打开反汇编窗口来观察下时间耗费在哪里。
首先很奇怪的是DPTR做了PUSH,既然我的中断函数里没有使用XDATA区域的变量,那么DPTR应该不需要做入栈和出栈,不过后面的代码就说明了为什么DPTR需要做PUSH,因为Keil C编译器在处理后面的switch语句时使用了C?CCASE函数,追进去后就可以看到两个POP指令分别是POP DPH和POP DPL,这是用来出栈调用前的下一个指令地址,然后根据ACC的值选择跳转到对应的地址位置用的,C?CCASE这个子程序并不用RET返回,而是直
接把CALL入栈的下一个地址POP出来,然后用JMP跳转的。
总体来说这样的结构很清楚,但是效率却很低,为了避免switch语句使用C?CCASE的低效,我们把switch改成if来实现,下面看看效果。
结果看波形可以知道,使用修改过的代码,中断的处理时间降低到283us左右,去掉INT脚置0指令还可以省下1个指令周期的时间,用自己的主机设备,调整SCL的频率到200kHz,在不判断SCL钳位的情况下直接读卡,可以很正常的工作,当然100kHz的明华读卡器是完全没问题了现在。
结论:
1、 SO=1的时候,MS51会钳位SCL到0,不注意这一点的话会看不懂波形
2、 Keil C的switch语句在这个应用里效率非常低,至少是低2-3倍的区别
3、 很多的读卡器或者主机代码,完全不处理SCL钳位的判断,自顾自读写。
4、 示波器和逻辑分析仪很重要。
修改过的代码已经上传,戳下面这里下载,目前测试下来在200kHz频率下读写非常稳定可靠。