【嵌入式】STM32实现SPI双机通信的一些细节(3)问题汇总
【嵌入式】STM32实现SPI双机通信的一些细节(3)问题汇总
Q1:从机移位寄存器已全部为主机数据时,是否需要主机时钟信号驱动来将数据移送至接收缓冲区?
背景
主机:初始化时只使能接收中断,需要被发送的数据准备好后,使能发送中断,立即发送;
从机:初始化时使能发送、接收中断,此时会立刻触发一次发送中断,在发送中断中执行 SPI_I2S_SendData(User_SPI, SPI_Slave.response);按照我的理解,从机的时钟由主机控制,因此虽然执行了这个函数,也只是将数据放入发送缓冲区等待主机发送;
主要流程:
- 主机首先执行帧创建函数,将需要发送的数据准备好;
- 调用发送函数,实际上就是使能发送中断;
- 发送中断被立刻触发,将帧数据发送出去,从机响应;
问题描述
当从机的移位寄存器已经全部为主机的数据时,是否需要主机的时钟信号驱动移位寄存器将数据移送至接收缓冲区?
为了调试程序,我在主机发送中断里的SPI_I2S_SendData处打了一个断点,当主机发出第一个字节时会再次触发该中断,但此时并没有触发从机的接收中断。
啊!!!!!!我傻了。昨天还反复告诉自己发送中断是在数据从发送缓冲区移入移位寄存器时触发,这触发的时候移位寄存器还没动呐!当然不会触发从机的中断啊!又是一次在写博客的时候发现了问题,看来以后真得坚持总结。回过头来,想了一下自己刚刚提的问题,移位寄存器移出至接收缓冲区需要主机的SCLK驱动么?应该是不需要的吧?当完成第一个字节的数据交换后,
从上图可以看到,在主从机交换数据到当前字节的最后一位时,RXNE标志位在SCLK下降沿到来之前就被置位了,按照我自己的理解是,这代表着最后一位数据一交换就会自动将数据移至接收缓冲区并触发中断,哪怕此时SCLK的下降沿没有来。这个目前看也无法设计一个比较好的方案验证它。
Q2:接收中断与下一个发送中断
背景
SPI全双工通信本质上是数据的交换,例如:主机在向从机写数据的同时,也会读取从机的数据,这就要求从机本身要发送相应的数据给主机。何不利用这一个字节做些事情呢?这是我设计双机通信的时候考虑的一个内容。
还是以主机向从机写数据为例(发送一个完整的数据帧,其中包含帧信息以及实际需写的数据):从机接收主机的帧数据进行解包阶段返回给从机FEED字节,当接收到最后一个字节即校验和后,如果当前帧有效,返回ACK字节,无效则返回FAIL字节。这三个字节具体是什么内容是完全根据自己的想法设计的,选择几个不太容易遇到的字节即可例如0XFE,0XEE等等。
重点:
主机和从机的首个待发送字节一旦放入移位寄存器进行交换,就会触发SPI发送中断,从而将下一个字节放入发送缓冲区;当数据交换完成后再触发首个字节的接收中断。如下图:
(这里有一个我的假设,6中的下一轮发送中断一定在5中的接收中断之后触发,这个需要验证一下才能知道,因为它们是属于同一个中断,所以后触发的会等到前一个执行完成后才会执行后一个中断。这意味着什么?意味着我可以在接收中断中修改从机的参数,进而影响从机下一个发送中断中所要返回的响应字节 它的重要性在于,可用来设计从机接收到主机的完整帧后的响应机制。如下图(手写更方便……)
从上面手写的示意图中可以看到一个非常重要的机制:主机的发送中断中,将SUM放入发送缓冲区后,还需要发送几个无效字节去驱动从机将最终的解包结果返回给主机。
问题描述
当从机接收到SUM后确定帧是完整后,可能由于数据写入的函数太耗时间,导致该接收中断执行时间过长,后面的发送中断处于等待状态,主机继续交换数据却发现没有接收到ACK,最终超时认定数据帧发送失败。当然这只是个假设,需要进行实际的测试:将数据写入任务取消,只修改从机响应码,看能否成功影响改变发送中断中放入缓冲区的响应字节,如果成功,意味着的确是该任务过于耗时,需要考虑其他的方案进行数据的存储。
20190416_20:04测试结果:未出现超时情况,从机能够返回ACK指令。又进行了几次测试,未发现超时现象,但在中断处理函数中执行数组复制工作不是一个好的方案,是否可以采用DMA的方式进行数据的写工作?
Q3:如何正确关闭SPI
背景
关闭SPI主要是针对主机,对于我的双机通信使用场景中很少会关闭SPI,但是会需要在完成一次数据发送任务后,退出SPI工作状态等待下一次发送任务。易知,在**SPI之前需要准备好实际的帧数据,那么意味着**SPI本质上就是使能它的发送中断,发送缓冲区为空立即触发数据发送。
主机完成发送任务最后的处理流程可参见《STM32中文参考手册》如下图:
问题描述
那么当接收到ACK字节后,如何正确地停止SPI?
根据上述内容进行分析,当主机接收到从机的ACK时,发送缓冲区中已有一个字节的数据了,那么实际上就是一旦确认接收到的是ACK/FAIL都需要立即失能发送中断,以确保将发送缓冲区中最后一个字节发送完后不会再次触发发送中断,而接收中断本身就是面对的从机待命状态返回的无效字节,所以不用管它。这样一来,主机完成发送后就可以等待下一次发送数据准备好,使能新的发送中断进行发送了。
如果需要关闭SPI,则按照上图中的顺序进行关闭。在我的使用场景中先按照上述步骤失能发送中断使其暂停发送,然后再关闭SPI。
Q4:比较合适的通信机制
在最开始设计时,我考虑了很多复杂的情况,例如将指令类型分为单次读写指令和周期读写指令等等,后来发现对于我的使用场景,周期读写不是很重要,也不适合使用SPI来实现。后来商讨后决定只实现最简单的读写功能,大体的机制描述如下:
主机写
Master
- 正式开始通信前,应当准备好需要写入从机的数据,并构建一个完整的数据帧;
- 使能主机发送中断立即开始发送;
- 发送完整帧数据后,等待从机返回的ACK/FAIL指令;
- 接收到确定指令或者超时未接受到,则清空当前发送状态信息,复位为待命状态,等待下一个发送任务;
Slave
- 首先需要保证从机处于就绪状态,发送缓冲区始终非空,以随时可以响应主机的发送;
- 接收主机的数据帧进行解包,在最后的校验和校验之前,从机只会从READY状态转为COMMANDING状态,只有在校验和通过后才会根据收到的数据帧更新从机状态;
- 对于主机写,在解包过程中会根据功能码字节直接将数据存入一个缓冲池,当校验和通过后才会将缓冲池中的数据写入从机真正的数据区;
- 此外,在确认帧完整还需要返回一个ACK字节,并且立刻回到就绪状态;在整个过程中需要保证发送缓冲区非空,即每次触发发送中断时都需要向发送缓冲区中写入数据,大部分时候都是写的一个无意义占位字节,FEED;
主机读
Master
- 正式开始通信前,应当准备好需要写入从机的数据,并构建一个完整的数据帧;
- 使能主机发送中断立即开始发送;
- 发送完整帧数据后,等待从机返回的ACK/FAIL指令;
- 未接收到确定指令或者超时未接受到,则清空当前发送状态信息,复位为待命状态,等待下一个发送任务;
- 接收到ACK后,通信机制中保证从机在发送完ACK字节的下一个字节就是返回的“主机读”的响应数据帧;这里需要强调,从机在返回主机所需数据时,也是构造一个完整的数据帧进行发送的;
- 主机进行解包,过程中出现的任何ACK或FAIL都会终止当前任务立即回到主机待命状态;这里有一个不足之处:如果主机在接收到从机帧数据时出错,从机还是在继续发送数据,并且当主机发送下一帧数据时,从机无法识别,这里需要改进 。
Slave
- 首先需要保证从机处于就绪状态,发送缓冲区始终非空,以随时可以响应主机的发送;
- 接收主机的数据帧进行解包,在最后的校验和校验之前,从机只会从READY状态转为COMMANDING状态,只有在校验和通过后才会根据收到的数据帧更新从机状态;
- 对于主机读,当确认帧完整后,返回ACK字节并立即进行帧数据的准备;
- 此后由主机驱动的发送会使从机将剩余数据逐字节发送,发送完毕后会立即回到就绪状态,这里就算从机角度的需要改进之处,目前我在从机发送帧数据时,接收到的数据是不进行处理的,这里还需要增加一个解包流程,以防止出现新的主机数据帧;
Q5:从机会出现冻结状态,如何处理
从机的时钟是由主机提供的,也就是说从机是否收发也完全是主机决定的。那么就会出现一种情况,如果主机在发送一帧指令的过程中,突然掉电重启进入待命状态,而从机的SPI状态则冻结在主机掉电时的解包流程中,就像SPI通信遇到了断点。而当主机发送新的一帧给从机的时候,从机立刻苏醒继续执行解包任务,而由于解包的流程已经进入了例如payload存储状态,则会将新的一个指令帧当作payload存入缓存中,导致主机指令无法响应的情况。所以,应当在从机的接收函数中,过滤每一个字节,无论从机当前处于什么状态都要进行帧头的检测。
void SPI_RecvDataS(u8 data)
{
//每一个字节都要检查帧头
if(data==HEAD)
SPI_Structure.SPI_RevCmdHolder.hdDetected=1; //检测到帧头标志量置1
//检测到帧头的下一个字节
else if(SPI_Structure.SPI_RevCmdHolder.hdDetected)
{
//帧头后必须是MS,如果不是则认定上一个数据只是普通的字节不是帧头
if(data==SPI_Structure.SPI_RevCmdHolder.MS)
SPI_Structure.cmdLog=1;
SPI_Structure.SPI_RevCmdHolder.hdDetected=0;
}
//解包流程控制
switch(SPI_Structure.cmdLog)
{
case 0:
SPI_Structure.response=FEED;
if(data==HEAD)
SPI_Structure.cmdLog=1;
break;
case 1:
if(data==SPI_Structure.SPI_RevCmdHolder.MS)
{
break;
}
else if(data==HEAD)
break;
else
SPI_Structure.cmdLog=0;
break;
case 2:
//以下均为解包流程控制
switch(data)
{
case READONLY:
SPI_Structure.SPI_RevCmdHolder.func=READONLY;
break;
case WRITEONLY:
SPI_Structure.SPI_RevCmdHolder.func=WRITEONLY;
break;
default:
SPI_Structure.state=READY;
SPI_Structure.response=FAIL;
SPI_CmdHolderReset(&SPI_Structure.SPI_RevCmdHolder);
SPI_Structure.cmdLog=0;
return;
}
SPI_Structure.SPI_RevCmdHolder.sum+=data;
SPI_Structure.cmdLog=3;
break;
case 3:
break;
case 4:
break;
case 5:
break;
case 6:
break;
}