目录
前言
一、ecx_init:
参数说明
函数功能
二、ecx_statecheck:
参数说明
函数功能
三、ecx_send_processdata_group / ecx_send_processdata:
参数说明
函数功能
四、ecx_receive_processdata_group / ecx_receive_processdata:
参数说明
函数功能
五、ecx_esidump:
参数说明
函数功能
总结
前言
上次学习总结了ec_config.c文件里的函数接口,了解了SOEM在工作前的初始化工作,结合之前针对ec_base.c文件的学习,对SOEM主站的工作框架有了一定的认知,本次就针对ec_main.c文件进行学习。
一、ecx_init:
int ecx_init(ecx_contextt *context, const char *ifname)
{
//初始化EtherCAT邮箱内存池
ecx_initmbxpool(context);
//绑定硬件网卡,并初始化网卡的EtherCAT通信参数
return ecx_setupnic(&context->port, ifname, FALSE);
}
//context->port:网卡端口结构体,用于存储网卡的硬件句柄、MAC地址、通信状态等信息
//ifname:传入的网卡标识,代表要绑定哪一个网卡
//FALSE:表示“不使用双网卡冗余模式”
参数说明
context:上下文结构体,存储各项信息
ifname:要绑定的网卡标识,通常是网卡驱动的逻辑名称“eth0”
函数功能
该接口是SOEM主站初始化的核心入口,负责完成主站上下文初始化与网卡硬件绑定,为后续EtherCAT通信做准备。
二、ecx_statecheck:
// 入参:context-主站上下文;slave-从站编号;reqstate-目标状态;timeout-超时时间(ms)
// 返回值:从站最终的实际状态(成功则=reqstate,失败则=当前状态/0)
uint16 ecx_statecheck(ecx_contextt *context, uint16 slave, uint16 reqstate, int timeout)
{
// 局部变量定义
uint16 configadr, state, rval; // configadr-从站配置地址;state-从站当前状态;rval-原始状态值
ec_alstatust slstat; // AL状态结构体(存储AL状态+状态码)
osal_timert timer; // 超时定时器(SOEM封装的跨平台定时器)
// 第一步:合法性校验——如果指定的从站编号超过总从站数,直接返回0(无效)
if (slave > context->slavecount)
{
return 0;
}
// 第二步:启动超时定时器(计时timeout毫秒)
osal_timer_start(&timer, timeout);
// 第三步:获取目标从站的配置地址(EtherCAT从站的唯一标识地址)
configadr = context->slavelist[slave].configadr;
// 第四步:循环检查从站状态,直到达到目标状态或超时
do
{
// 分支1:slave<1 表示检查"所有从站"(广播检查)
if (slave < 1)
{
rval = 0;
// 广播读取所有从站的AL状态寄存器(ECT_REG_ALSTAT)
// BRD=Broadcast Read,0=广播地址,读取2字节到rval
ecx_BRD(&context->port, 0, ECT_REG_ALSTAT, sizeof(rval), &rval, EC_TIMEOUTRET);
// 字节序转换(网络序→主机序,适配不同CPU)
rval = etohs(rval);
}
// 分支2:slave≥1 表示检查"指定单个从站"
else
{
// 初始化状态结构体
slstat.alstatus = 0;
slstat.alstatuscode = 0;
// 精准读取单个从站的AL状态寄存器
// FPRD=Fast Process Data Read,按配置地址读取从站的AL状态(包含状态+状态码)
ecx_FPRD(&context->port, configadr, ECT_REG_ALSTAT, sizeof(slstat), &slstat, EC_TIMEOUTRET);
// 转换字节序,得到从站的AL状态
rval = etohs(slstat.alstatus);
// 保存从站的AL状态码(用于后续故障排查,如状态切换失败的原因)
context->slavelist[slave].ALstatuscode = etohs(slstat.alstatuscode);
}
// 关键:提取从站的实际状态——AL状态寄存器低4位是状态值(0x000f掩码过滤)
state = rval & 0x000f;
// 如果当前状态≠目标状态,短暂延时1ms(避免高频轮询占用CPU)
if (state != reqstate)
{
osal_usleep(1000); // 延时1000微秒=1毫秒
}
// 循环条件:1. 当前状态≠目标状态;2. 定时器未超时 → 继续循环检查
} while ((state != reqstate) && (osal_timer_is_expired(&timer) == FALSE));
// 第五步:将最终读取到的状态保存到主站上下文(方便后续查询)
context->slavelist[slave].state = rval;
// 第六步:返回从站最终的实际状态
return state;
}
参数说明
cintext:上下文结构体
slave:从站编号
reqstate:目标状态
timeout:超时时间
函数功能
该接口核心功能是在指定超时时间内,持续检查指定从站AL状态机,直到从站进入开发者要求的目标状态
三、ecx_send_processdata_group / ecx_send_processdata:
// 入参:context-主站上下文;group-从站组编号
// 返回值:WKC(工作计数器),0=发送失败,≥1=发送成功(具体值=成功响应的从站数)
int ecx_send_processdata_group(ecx_contextt *context, uint8 group)
{
// 局部变量定义
uint32 LogAdr; // 逻辑地址(PDO数据在从站的内存地址)
uint16 w1, w2; // 逻辑地址拆分的高低16位
int length; // 要发送的PDO总长度
uint16 sublength; // 分段传输时单段的长度
uint8 idx; // 发送缓冲区索引(STM32以太网发送buf的索引)
int wkc; // 工作计数器(返回值)
uint8 *data; // 指向输出PDO数据的指针
boolean first = FALSE; // 标记是否是第一个DC同步帧
uint16 currentsegment = 0; // 当前分段索引
uint32 iomapinputoffset; // 重叠IO映射时的输入数据偏移
uint16 DCO; // DC同步相关标识
// ========== 第一步:初始化核心变量 ==========
wkc = 0; // 初始化WKC为0(默认失败)
// 如果该组从站支持DC同步,标记first为TRUE(第一个帧要带DC同步指令)
if (context->grouplist[group].hasdc)
{
first = TRUE;
}
// 清空该组的邮箱状态(避免旧状态干扰)
ecx_clearmbxstatus(context, group);
// ========== 第二步:计算PDO数据长度(区分重叠/非重叠IO映射) ==========
/* For overlapping IO map use the biggest */
if (context->overlappedMode == TRUE)
{
// 重叠IO映射:输入/输出共享内存,取输出/输入长度的最大值作为帧长度
length = (context->grouplist[group].Obytes > context->grouplist[group].Ibytes)
? context->grouplist[group].Obytes
: context->grouplist[group].Ibytes;
// 加上邮箱状态长度(非周期数据)
length += context->grouplist[group].mbxstatuslength;
// 记录输入数据偏移(因为输出/输入共享内存,输入数据要存在偏移位置)
iomapinputoffset = context->grouplist[group].Obytes;
}
else
{
// 非重叠IO映射:总长度=输出长度+输入长度+邮箱状态长度
length = context->grouplist[group].Obytes +
context->grouplist[group].Ibytes +
context->grouplist[group].mbxstatuslength;
iomapinputoffset = 0; // 无偏移
}
// ========== 第三步:获取PDO逻辑起始地址 ==========
LogAdr = context->grouplist[group].logstartaddr;
// ========== 第四步:核心发送逻辑(长度>0才发送) ==========
if (length)
{
wkc = 1; // 长度有效,先置WKC为1(表示开始发送)
// ========== 分支1:LRW被阻塞(只能分开读/写) ==========
if (context->grouplist[group].blockLRW)
{
// 子分支1.1:如果有输入数据,先执行LRD(读)操作(虽然是发送函数,但先读再写)
if (context->grouplist[group].Ibytes)
{
currentsegment = context->grouplist[group].Isegment; // 输入分段起始索引
data = context->grouplist[group].inputs; // 指向输入数据缓冲区
length = context->grouplist[group].Ibytes; // 输入数据长度
LogAdr += context->grouplist[group].Obytes; // 调整逻辑地址到输入区域
// 分段传输循环:直到所有分段发完或长度为0
do
{
// 计算当前分段的长度
if (currentsegment == context->grouplist[group].Isegment)
{
sublength = (uint16)(context->grouplist[group].IOsegment[currentsegment++] - context->grouplist[group].Ioffset);
}
else
{
sublength = (uint16)context->grouplist[group].IOsegment[currentsegment++];
}
// 1. 获取发送缓冲区索引(STM32的以太网发送buf是数组,idx是当前要用的下标)
idx = ecx_getindex(&context->port);
// 2. 拆分逻辑地址为高低16位(适配EtherCAT帧格式)
w1 = LO_WORD(LogAdr);
w2 = HI_WORD(LogAdr);
DCO = 0;
// 3. 组装LRD指令的EtherCAT数据帧
ecx_setupdatagram(&context->port, &(context->port.txbuf[idx]), EC_CMD_LRD, idx, w1, w2, sublength, data);
// 4. 如果是第一个帧且支持DC,添加DC同步指令(FRMW=写从站寄存器)
if (first)
{
DCO = ecx_adddatagram(&context->port, &(context->port.txbuf[idx]), EC_CMD_FRMW, idx, FALSE,
context->slavelist[context->grouplist[group].DCnext].configadr,
ECT_REG_DCSYSTIME, sizeof(int64), &context->DCtime);
first = FALSE; // 标记已发送DC同步帧
}
// 5. 发送帧(STM32底层调用以太网驱动发送txbuf[idx]中的数据)
ecx_outframe_red(&context->port, idx);
// 6. 记录发送信息(方便后续接收时匹配)
ecx_pushindex(context, idx, data, sublength, DCO);
// 7. 更新长度、地址、数据指针,准备下一分段
length -= sublength;
LogAdr += sublength;
data += sublength;
} while (length && (currentsegment < context->grouplist[group].nsegments));
}
// 子分支1.2:如果有输出数据,执行LWR(写)操作(核心:发送输出PDO)
if (context->grouplist[group].Obytes)
{
data = context->grouplist[group].outputs; // 指向输出PDO数据(你要发给从站的控制指令)
length = context->grouplist[group].Obytes; // 输出数据长度
LogAdr = context->grouplist[group].logstartaddr; // 重置逻辑地址到输出区域
currentsegment = 0; // 重置分段索引
// 分段传输循环
do
{
// 计算当前分段长度
sublength = (uint16)context->grouplist[group].IOsegment[currentsegment++];
if ((length - sublength) < 0)
{
sublength = (uint16)length; // 最后一段可能不足,取剩余长度
}
// 1. 获取发送缓冲区索引
idx = ecx_getindex(&context->port);
// 2. 拆分逻辑地址
w1 = LO_WORD(LogAdr);
w2 = HI_WORD(LogAdr);
DCO = 0;
// 3. 组装LWR指令的EtherCAT数据帧(核心:把输出数据打包成帧)
ecx_setupdatagram(&context->port, &(context->port.txbuf[idx]), EC_CMD_LWR, idx, w1, w2, sublength, data);
// 4. DC同步(同上文)
if (first)
{
DCO = ecx_adddatagram(&context->port, &(context->port.txbuf[idx]), EC_CMD_FRMW, idx, FALSE,
context->slavelist[context->grouplist[group].DCnext].configadr,
ECT_REG_DCSYSTIME, sizeof(int64), &context->DCtime);
first = FALSE;
}
// 5. 发送帧(STM32以太网驱动发送数据)
ecx_outframe_red(&context->port, idx);
// 6. 记录发送信息
ecx_pushindex(context, idx, data, sublength, DCO);
// 7. 更新参数
length -= sublength;
LogAdr += sublength;
data += sublength;
} while (length && (currentsegment < context->grouplist[group].nsegments));
}
}
// ========== 分支2:LRW可用(同时读写,效率更高) ==========
else
{
// 确定数据指针(优先输出,无输出则用输入)
if (context->grouplist[group].Obytes)
{
data = context->grouplist[group].outputs; // 指向输出PDO(控制指令)
}
else
{
data = context->grouplist[group].inputs;
iomapinputoffset = 0; // 无输出时,输入偏移置0
}
// 分段传输循环(核心:组装LRW帧,同时发输出+收输入)
do
{
sublength = (uint16)context->grouplist[group].IOsegment[currentsegment++];
// 获取发送缓冲区索引
idx = ecx_getindex(&context->port);
// 拆分逻辑地址
w1 = LO_WORD(LogAdr);
w2 = HI_WORD(LogAdr);
DCO = 0;
// 组装LRW指令的EtherCAT数据帧(最核心:同时读写)
ecx_setupdatagram(&context->port, &(context->port.txbuf[idx]), EC_CMD_LRW, idx, w1, w2, sublength, data);
// DC同步
if (first)
{
DCO = ecx_adddatagram(&context->port, &(context->port.txbuf[idx]), EC_CMD_FRMW, idx, FALSE,
context->slavelist[context->grouplist[group].DCnext].configadr,
ECT_REG_DCSYSTIME, sizeof(int64), &context->DCtime);
first = FALSE;
}
// 发送帧
ecx_outframe_red(&context->port, idx);
// 记录发送信息(带偏移,适配重叠IO)
ecx_pushindex(context, idx, (data + iomapinputoffset), sublength, DCO);
// 更新参数
length -= sublength;
LogAdr += sublength;
data += sublength;
} while (length && (currentsegment < context->grouplist[group].nsegments));
}
}
// ========== 第五步:返回WKC ==========
return wkc;
}
int ecx_send_processdata(ecx_contextt *context)
{
return ecx_send_processdata_group(context, 0);
}
参数说明
context:上下文结构体
group:从站组组号
函数功能
该接口核心功能主要是实现EtherCAT主站发送过程数据(PDO)到指定从站组,通过EtherCAT协议封装成数据帧,发送到总线的从站,同时支持DC分布式时钟同步,分段传输,重叠IO映射等高级特性。ecx_send_processdata函数是对此函数的调用封装。
四、ecx_receive_processdata_group / ecx_receive_processdata:
// 入参:context-主站上下文;group-从站组编号(实际未直接使用,兼容接口);timeout-接收超时时间(ms)
// 返回值:WKC(成功=有效从站数,EC_NOFRAME=-1=超时/无帧,0=解析失败)
int ecx_receive_processdata_group(ecx_contextt *context, uint8 group, int timeout)
{
// 局部变量定义
uint8 idx; // 接收缓冲区索引(对应发送时的txbuf索引)
int pos; // 索引栈的位置(遍历栈用)
int wkc = 0, wkc2; // wkc=最终返回的WKC;wkc2=临时WKC/组编号占位
uint16 le_wkc = 0; // 网络字节序的WKC(需要字节序转换)
int valid_wkc = 0; // 标记是否解析到有效WKC(0=无效,1=有效)
int64 le_DCtime; // 网络字节序的DC时间(需要字节序转换)
ec_idxstackT *idxstack; // 指向索引栈(记录发送帧的信息)
ec_bufT *rxbuf; // 指向接收缓冲区(STM32以太网rxbuf)
// ========== 第一步:占位处理(避免编译器警告) ==========
/* just to prevent compiler warning for unused group */
wkc2 = group; // group参数未直接使用(因为索引栈已关联组),仅占位
// ========== 第二步:初始化核心指针 ==========
idxstack = &context->idxstack; // 绑定索引栈(发送时push的帧信息)
rxbuf = context->port.rxbuf; // 绑定STM32的以太网接收缓冲区
// ========== 第三步:从索引栈取出第一个发送帧的位置 ==========
/* get first index */
pos = ecx_pullindex(context); // 从栈顶取出第一个帧的位置(pos≥0表示有帧)
// ========== 第四步:核心循环:遍历所有发送的帧,接收并解析响应 ==========
/* read the same number of frames as send */
while (pos >= 0) // 只要栈里还有未处理的帧,就继续循环
{
// 1. 获取当前帧的缓冲区索引(对应发送时的idx)
idx = idxstack->idx[pos];
// 2. 等待接收响应帧(核心:STM32阻塞等待,直到收到帧或超时)
// 返回值:wkc2>EC_NOFRAME(-1)表示收到帧;否则超时/无帧
wkc2 = ecx_waitinframe(&context->port, idx, timeout);
// 3. 检查是否收到有效帧
if (wkc2 > EC_NOFRAME)
{
// ========== 分支1:响应帧是LRD(读)或LRW(读写)→ 解析输入PDO + WKC ==========
if ((rxbuf[idx][EC_CMDOFFSET] == EC_CMD_LRD) || (rxbuf[idx][EC_CMDOFFSET] == EC_CMD_LRW))
{
// 子分支1.1:包含DC同步数据(dcoffset>0)
if (idxstack->dcoffset[pos] > 0)
{
// a. 拷贝输入PDO数据到主站缓冲区(核心:把从站数据存到你能访问的内存)
// rxbuf[idx][EC_HEADERSIZE]:跳过帧头,取实际数据;length[pos]:数据长度
memcpy(idxstack->data[pos], &(rxbuf[idx][EC_HEADERSIZE]), idxstack->length[pos]);
// b. 解析WKC(网络字节序→主机序)
memcpy(&le_wkc, &(rxbuf[idx][EC_HEADERSIZE + idxstack->length[pos]]), EC_WKCSIZE);
wkc = etohs(le_wkc); // 字节序转换(适配STM32的小端序)
// c. 解析DC时间(同步主从站时钟)
memcpy(&le_DCtime, &(rxbuf[idx][idxstack->dcoffset[pos]]), sizeof(le_DCtime));
context->DCtime = etohll(le_DCtime); // 64位字节序转换
}
// 子分支1.2:无DC同步数据 → 仅拷贝输入PDO + 累加WKC
else
{
/* copy input data back to process data buffer */
// 核心:把从站返回的输入PDO数据拷贝到主站缓冲区(比如传感器数据)
memcpy(idxstack->data[pos], &(rxbuf[idx][EC_HEADERSIZE]), idxstack->length[pos]);
wkc += wkc2; // 累加临时WKC
}
valid_wkc = 1; // 标记解析到有效WKC
}
// ========== 分支2:响应帧是LWR(写)→ 仅解析WKC(无输入数据) ==========
else if (rxbuf[idx][EC_CMDOFFSET] == EC_CMD_LWR)
{
// 子分支2.1:包含DC同步数据
if (idxstack->dcoffset[pos] > 0)
{
// a. 解析WKC(LWR的WKC需要×2,对齐LRW的计数规则)
memcpy(&le_wkc, &(rxbuf[idx][EC_HEADERSIZE + idxstack->length[pos]]), EC_WKCSIZE);
/* output WKC counts 2 times when using LRW, emulate the same for LWR */
wkc = etohs(le_wkc) * 2;
// b. 解析DC时间
memcpy(&le_DCtime, &(rxbuf[idx][idxstack->dcoffset[pos]]), sizeof(le_DCtime));
context->DCtime = etohll(le_DCtime);
}
// 子分支2.2:无DC同步数据 → 仅累加WKC(×2)
else
{
/* output WKC counts 2 times when using LRW, emulate the same for LWR */
wkc += wkc2 * 2;
}
valid_wkc = 1; // 标记解析到有效WKC
}
}
// ========== 第五步:释放接收缓冲区 ==========
/* release buffer */
ecx_setbufstat(&context->port, idx, EC_BUF_EMPTY); // 标记缓冲区为空,可复用
// ========== 第六步:取出索引栈的下一个帧位置 ==========
/* get next index */
pos = ecx_pullindex(context);
}
// ========== 第七步:清空索引栈(释放资源) ==========
ecx_clearindex(context);
// ========== 第八步:返回最终结果 ==========
/* if no frames has arrived */
if (valid_wkc == 0) // 未解析到有效WKC(超时/无帧)
{
return EC_NOFRAME; // 返回-1,表示无有效帧
}
return wkc; // 返回有效WKC(成功=从站数,失败=小于从站数)
}
int ecx_receive_processdata(ecx_contextt *context, int timeout)
{
return ecx_receive_processdata_group(context, 0, timeout);
}
参数说明
略
函数功能
该接口核心功能主要负责主站接受指定从站组的过程数据(PDO),解析数据并更新WKC工作计数器。大致流程为在指定超时时间范围内,接收并解析对应从站组的EtherCAT响应帧,将从站返回的输入PDO数据写入到主站内存缓冲区,同时计算并返回WKC,来判断通信是否成功。
五、ecx_esidump:
// 入参:context-主站上下文;slave-从站编号;esibuf-主站缓冲区(存储读取的ESI数据)
// 返回值:无(ESI数据直接写入esibuf)
void ecx_esidump(ecx_contextt *context, uint16 slave, uint8 *esibuf)
{
// 局部变量定义
uint16 configadr, address, incr; // configadr-从站配置地址;address-EEPROM读取地址;incr-读取步长
uint64 *p64; // 64位数据指针(适配8字节读取)
uint16 *p16; // 16位数据指针(适配2字节读取,esibuf的操作指针)
uint64 edat; // 存储从EEPROM读取的64位数据
uint8 eectl = context->slavelist[slave].eep_pdi; // 保存原始EEPROM控制权状态
// ========== 第一步:切换EEPROM控制权到主站 ==========
ecx_eeprom2master(context, slave); /* set eeprom control to master */
// 作用:告诉从站“现在由主站控制EEPROM”,否则主站无法读取ESI文件
// ========== 第二步:初始化核心参数 ==========
configadr = context->slavelist[slave].configadr; // 获取从站的配置地址(唯一标识)
address = ECT_SII_START; // 设置EEPROM读取起始地址(0x0000,ESI文件起始位置)
p16 = (uint16 *)esibuf; // 将esibuf转为16位指针(方便按2/4步长操作)
// 第三步:确定读取步长(incr)——根据从站EEPROM支持的读取粒度
if (context->slavelist[slave].eep_8byte)
{
incr = 4; // 8字节读取(64位):每次读8字节,指针步长=4(因为p16是16位,4×2=8字节)
}
else
{
incr = 2; // 2字节读取(16位):每次读2字节,指针步长=2(2×2=4字节?实际是每次读2字节,步长2是兼容逻辑)
}
// ========== 第四步:核心循环:逐段读取ESI文件到缓冲区 ==========
do
{
// 1. 从从站EEPROM读取64位数据(核心:实际读取ESI内容)
// ecx_readeepromFP:Fast Process读取EEPROM,configadr=从站地址,address=EEPROM地址,超时时间EC_TIMEOUTEEP
edat = ecx_readeepromFP(context, configadr, address, EC_TIMEOUTEEP);
// 2. 将读取的64位数据写入主站缓冲区(esibuf)
p64 = (uint64 *)p16; // 临时转为64位指针
*p64 = edat; // 把读取的ESI数据写入缓冲区
// 3. 更新指针和地址,准备下一次读取
p16 += incr; // 缓冲区指针后移(按步长)
address += incr; // EEPROM读取地址后移(按步长)
// 循环条件:
// 1. address ≤ (EC_MAXEEPBUF >> 1):读取地址不超过缓冲区最大地址(避免溢出)
// 2. edat != 0xffffffff:读取的数据不是全1(ESI文件结束标记是全1)
} while ((address <= (EC_MAXEEPBUF >> 1)) && ((uint32)edat != 0xffffffff));
// ========== 第五步:恢复EEPROM控制权(如果原始是PDI控制) ==========
if (eectl)
{
ecx_eeprom2pdi(context, slave); /* if eeprom control was previously pdi then restore */
// 作用:读取完成后恢复从站EEPROM的原始控制权,避免影响从站正常工作
}
}
参数说明
context:上下文结构体
slave:从站编号
esibuf:主站缓冲区(存储读取的ESI数据)
函数功能
该接口核心功能主要实现将指定从站的ESI文件(存储在从站的EEPROM中)逐段读取到主站的内存缓冲区(esibuf),读取完成后恢复从站EEPROM控制权,为后续解析从站配置(如PDO映射)提供原始数据。
总结
本次主要是对SOEM中ec_main.c文件里的主要接口进行了学习总结,包括主站对从站的初始化,以及针对从站的PDO数据通信以及ESI文件的读取,这些在后续主站开发中都会详细的进行配置,也是为后续开发打下基础。
















暂无评论内容