FOE协议与下位机程序实现过程之前文章有提到,这里不做介绍了。这里主要介绍1、QT上位机通过FOE读写下位机的数据;2、QT上位机读写ESC的EEPROM。
SOEM源码中和foe相关的文件为ethercatfoe.c、ethercatfoe.h。主要包含了下面三个函数。
/* 读请求 参数:请求文件名、密钥、本地用于存储的内存的大小、本地文件的地址、每次通信超时时间 */
int ec_FOEread(uint16 slave, char *filename, uint32 password, int *psize, void *p, int timeout)
/* 写请求 参数:请求文件名、密钥、文件大小、本地文件的地址、每次通信超时时间*/
int ec_FOEwrite(uint16 slave, char *filename, uint32 password, int psize, void *p, int timeout)
/* 定义回调函数 参数:回调函数的地址 */
int ec_FOEdefinehook(void *hook)
FOE读的整体实现思路如下:
清空邮箱,防止现有数据影响接下来的判断。
向下位机发送读请求帧,包含了文件名、密钥等的数据。
读取下位机的回复的数据帧;
将数据帧的内容拷贝到本地内存中,判断内存是否越界,计算已接受数据的大小
如果定义了回调函数,则会调用回调函数。回调函数包含了从机编号、包计数、已接收文件总长度这三个参数
循环,直到接收完成。
/** FoE read, 代码片段.
*
* @param[in] context = context struct
* @param[in] slave = 从机编号.
* @param[in] filename = 请求的文件名.
* @param[in] password = 密钥.
* @param[in,out] psize = 本地存储文件的内存的大小.
* @param[out] p = 本地存储文件的内存的地址
* @param[in] timeout = 超时时间us
* @return Workcounter from last slave response
*/
int ecx_FOEread(ecx_contextt *context, uint16 slave, char *filename, uint32 password, int *psize, void *p, int timeout)
{
.......
/* 将邮箱清空,为FOE传输做准备 */
ec_clearmbx(&MbxIn);
wkc = ecx_mbxreceive(context, slave, (ec_mbxbuft *)&MbxIn, 0);
ec_clearmbx(&MbxOut);
......
/* 封装数据,发送读请求 */
wkc = ecx_mbxsend(context, slave, (ec_mbxbuft *)&MbxOut, EC_TIMEOUTTXM);
if (wkc > 0)
{
do
{
.......
/* 一系列判断 */
if(aFOEp->OpCode == ECT_FOE_DATA)
{
/* 获取数据的长度和包编号 */
segmentdata = etohs(aFOEp->MbxHeader.length) - 0x0006;
packetnumber = etohl(aFOEp->PacketNumber);
/* 判断包编号和总计接收的长度是否超过了内存 */
if ((packetnumber == ++prevpacket) && (dataread + segmentdata <= buffersize))
{
/* 数据转存,内存偏移,接收计数累加 */
memcpy(p, &aFOEp->Data[0], segmentdata);
dataread += segmentdata;
p = (uint8 *)p + segmentdata;
/* 这个maxdata是邮箱的大小,也是每包数据的最大长度,相等说明不是最后一包数据,继续接收*/
if (segmentdata == maxdata)
{
worktodo = TRUE;
}
........
/* 回复ack给下位机,如果有回调函数则调用回调 */
wkc = ecx_mbxsend(context, slave, (ec_mbxbuft *)&MbxOut, EC_TIMEOUTTXM);
if (wkc <= 0)
{
worktodo = FALSE;
}
if (context->FOEhook)
{
context->FOEhook(slave, packetnumber, dataread);
}
..........
}
FOE写的整体实现思路如下:
清空邮箱,防止现有数据影响接下来的判断。
向下位机发送写请求帧,包含了文件名、密钥等的数据。
读取下位机的回复的应答帧;
判断应答帧的包计数,如果定义了回调函数,则会调用回调函数,回调函数包含了从机编号、包计数、已接收文件总长度这三个参数
继续发送数据包
循环,直到发送完成。
/** FoE write, 代码片段.
*
* @param[in] context = context struct
* @param[in] slave = 从站编号.
* @param[in] filename = 待发送的文件名.
* @param[in] password = 密钥.
* @param[in] psize = 待发送的文件的大小.
* @param[out] p = 待发送文件的地址
* @param[in] timeout = Timeout per mailbox cycle in us, standard is EC_TIMEOUTRXM
* @return Workcounter from last slave response
*/
int ecx_FOEwrite(ecx_contextt *context, uint16 slave, char *filename, uint32 password, int psize, void *p, int timeout)
{
........
/* 清空邮箱为FOE通信做准备 */
ec_clearmbx(&MbxIn);
wkc = ecx_mbxreceive(context, slave, (ec_mbxbuft *)&MbxIn, 0);
ec_clearmbx(&MbxOut);
......
/* 封装数据发送写请求 */
wkc = ecx_mbxsend(context, slave, (ec_mbxbuft *)&MbxOut, EC_TIMEOUTTXM);
if (wkc > 0)
{
do
{
worktodo = FALSE;
/* clean mailboxbuffer */
ec_clearmbx(&MbxIn);
/* 读取从站回复 */
wkc = ecx_mbxreceive(context, slave, (ec_mbxbuft *)&MbxIn, timeout);
if (wkc > 0) /* succeeded to read slave response ? */
{
if ((aFOEp->MbxHeader.mbxtype & 0x0f) == ECT_MBXT_FOE)
{
switch (aFOEp->OpCode)
{
case ECT_FOE_ACK:
{
packetnumber = etohl(aFOEp->PacketNumber);
if (packetnumber == sendpacket)
{
/* 如果定义了回调函数,可以调用回调函数 */
if (context->FOEhook)
{
context->FOEhook(slave, packetnumber, psize);
}
tsize = psize;
if (tsize > maxdata)
{
tsize = maxdata;
}
if(tsize || dofinalzero)
{
worktodo = TRUE;
dofinalzero = FALSE;
segmentdata = tsize;
psize -= segmentdata;
/* 判断是否为最后一帧 */
if (!psize && (segmentdata == maxdata))
{
dofinalzero = TRUE;
}
...........
/* 封装数据,发送数据帧 */
wkc = ecx_mbxsend(context, slave, (ec_mbxbuft *)&MbxOut, EC_TIMEOUTTXM);
if (wkc <= 0)
{
worktodo = FALSE;
}
}
}
else
{
/* FoE error */
wkc = -EC_ERR_TYPE_FOE_PACKETNUMBER;
}
break;
}
}
用与定义回调函数,就是将我们回调函数入口地址传递给一个变量。我们自己定义的回调函数需要为下面的形式
(*FOEhook)(uint16 slave, int packetnumber, int datasize);
首先,通过看SOEMd的源码发现,在调用ec_FOEread的时候,我们需要先申请一片内存,将内存地址传递进去,用来存放读取的文件,那么当文件很大的时候,我们申请的内存就需要足够大。同样的调用ec_FOEwrite的时候,我们也需要申请一片内存,将要写的数据全部读取到内存中,然后将内存的地址传递给ec_FOEwrite,当文件很大的时候,我们就需要定义的内存足够大。这种实现方式也算是用空间换时间吧,将文件一次性放到内存中,然后再统一操作数据。还有一点就是ec_FOEread和ec_FOEwrite我们只需要调用一次,将参数传递进去后,就开始进行一个while循环,知道我们所有的操作结束。
本次是用QT来进行FOE数据传输的,在数据传输部分,个人偏向于边读便发送,或者边写便发送的方式。另外在数据收发的过程中,还要保证界面继续刷新,因此需要对ec_FOEread和ec_FOEwrite进行修改。
/* 对比源码修改的部分 */
if (packetnumber == sendpacket)
{
/* 在收到ack 说明写请求成功,发送自定义信号量,并延时10ms 来更新界面 psize是剩余的 文件大小 */
if(psize != 0)
emit foewrite(psize);
Sleep(10);
/***********************************************************/
tsize = psize;
if (tsize > maxdata)
{
tsize = maxdata;
}
if(tsize || dofinalzero)
{
worktodo = TRUE;
dofinalzero = FALSE;
segmentdata = tsize;
psize -= segmentdata;
/* if last packet was full size, add a zero size packet as final */
/* EOF is defined as packetsize < full packetsize */
if (!psize && (segmentdata == maxdata))
{
dofinalzero = TRUE;
}
FOEp->MbxHeader.length = htoes((uint16)(0x0006 + segmentdata));
FOEp->MbxHeader.address = htoes(0x0000);
FOEp->MbxHeader.priority = 0x00;
/* get new mailbox count value */
cnt = ec_nextmbxcnt(context->slavelist[slave].mbx_cnt);
context->slavelist[slave].mbx_cnt = cnt;
FOEp->MbxHeader.mbxtype = ECT_MBXT_FOE + MBX_HDR_SET_CNT(cnt); /* FoE */
FOEp->OpCode = ECT_FOE_DATA;
sendpacket++;
FOEp->PacketNumber = htoel(sendpacket);
/* 屏蔽掉了下面发送地址偏移的代码,因此每次发送的数据地址都是固定的,只需要定义一 个固定的地址用于存放数据,每次在发送前将数据填充到该地址就可以了,这样我们就不 需要一个很大的内存了 */
memcpy(&FOEp->Data[0], p, segmentdata);
// p = (uint8 *)p + segmentdata;
/* 对比源码修改部分 */
segmentdata = etohs(aFOEp->MbxHeader.length) - 0x0006;
packetnumber = etohl(aFOEp->PacketNumber);
/* 这里就不需要判断接收数据总大小是否大于申请的内存的大小了,因为 p 的地址不偏移了,每次接收完数据后,根据自定义的信号量去固定的内存地址拷贝已读取到的数据并保存到文件,不需要太大的内存了 */
// if ((packetnumber == ++prevpacket) && (dataread + segmentdata <= buffersize))
if ((packetnumber == ++prevpacket) && (segmentdata <= maxdata))
{
memcpy(p, &aFOEp->Data[0], segmentdata);
dataread += segmentdata;
/* 发送数据的地址不偏移 */
// p = (uint8 *)p + segmentdata;
if (segmentdata == maxdata)
{
worktodo = TRUE;
}
/* 发送自定义信号量,更新界面 dataread已接收的文件大小 */
emit foeread(dataread);
Sleep(10);
FOEp->MbxHeader.length = htoes(0x0006);
FOEp->MbxHeader.address = htoes(0x0000);
这里只是简单的介绍了一下程序的实现思路,可能有点绕。
测试的例程在连接从站后,直接请求从站的状态从Init跳转到了Bootstrap。在Bootstrap的时候值能通过FOE进行通信。还有一个地方需要注意,在Bootstrap状态的时候,通信的邮箱是boot邮箱,虽然地址、大小一般都和标准邮箱(COE用的邮箱)一样,但是还是需要进行配置的。boot邮箱的配置在EEPROM中,因此在进入Bootstrap的时候会去EEPROM中读取boot邮箱的配置。如果和标准邮箱的配置一样且标准邮箱已经配置过了,就不需要再进行配置了,否则,需要对boot邮箱进行配置。下图就是XML中Boot邮箱的地址、大小的配置信息。需要再SSC-Tool中勾选了BOOTSTRAPMODE_SUPPORTED选项,xml中才会产生。
FOE主站源码:https://github.com/IJustLoveMyself/csdn-example/tree/main/example4
配合测试的STM32F405从站源码:https://github.com/IJustLoveMyself/csdn-example/tree/main/example5