本文介绍在正点原子的STM32H743开发板上,使用SOEM方案实现EtherCAT主站通讯,本文记录从零基础学习路线。由于本人才疏学浅,如有错误,还请指正。
下文蓝色带下划线为参考资料的网址链接,蓝色字体为参考资料名称。
在了解EtherCAT之前,建议对于TCP/IP的概念有一些了解,可以观看正点原子的介绍视频:【正点原子】手把手教你学lwIP_哔哩哔哩_bilibili(建议看完前7讲)
对于EtherCAT概念的了解,可以观看:【工业通讯】EtherCAT&EtherNet/IP基础知识讲解合集_哔哩哔哩_bilibili
本项目移植过程中,参考的文献如下:
[1]王惠娇. 基于嵌入式平台的EtherCAT主站实现研究[D].郑州大学,2018.
[2]毛令. 基于STM32的嵌入式EtherCAT主站的研究与实现[D].北方民族大学,2022
[1]刘豪志. 基于嵌入式Linux的EtherCAT主站设计及伺服控制系统研究[D].南京航空航天大学,2020.
以上绍视频和文献,对于ETH和EtherCAT帧结构、寻址模式、通讯模式、同步模式等知识点讲解比较清晰,这里就不展开介绍。我们就直入主题,介绍在STM32H743基于SOEM实现EtherCAT通讯的移植和调试过程。
SOEM的代码的官网下载链接:GitHub -OpenEtherCATsociety/SOEM: Simple Open Source EtherCAT Master
SOEM库采用分层设计,并且提供了一个抽象层,将SOEM协议栈与具体操作系统和硬件分开。抽象层由OSAL和OSHW两个模块组成,OSAL是操作系统抽象层,OSHW 是硬件抽象层,移植的主要内容就是对OSAL和OSHW具体API实现,在新的操作系统和硬件平台上的重写。
SOEM的层级架构如下图所示:
抽象层可以分为操作系统抽象层OSAL和硬件抽象层OSHW两部分。
OSAL模块侧重于操作系统的抽象和时间机制。由osal.h和osal.c组成。定义了基本数据类型,时间相关结构以及时间和定时器相关API和线程相关API。其中的时间和定时器相关的API是必须实现的,线程相关的API不是必须实现的。
OSHW模块侧重于为上层提供网络服务。由oshw.h/oshw.c和nicdrv.h/nicdrv.c四个文件组成。oshw.h/.c主要实现网络大小端和本地大小端的转换,nicdrv.h/.c是网络驱动,主要实现 EtherCAT帧的发送和接收。
不同平台上nicdrv模块的逻辑结构基本相同,需要实现的是三个底层API:
(1)网口的初始化 (2)MAC层帧发送 (3)MAC层帧接收
SOEM的中间层,是EtherCAT协议 栈的具体实现,包含BASE模块、MAIN模块、CONFIG模块、CONFIGDC模块、COE等。各层的功能如下表所示。
层 |
功能 |
BASE |
将工业应用数据组装成 EtherCAT 帧 |
MAIN |
顺序寻址、广播方式、配置地址、逻辑地址方式对从站读写读写从站 EEPROM |
CONFIG |
提供邮箱模式的非过程数据读写和三缓冲模式的过程数据 PDO读写初始化从站控制器的寄存器,配置从站FMMU |
CONFIGDC |
提供分布式时钟,实现主从站之间时钟同步 |
COE |
CANOpen Over EtherCAT |
FOE |
File Over EtherCAT |
SOE |
Sercos Over EtherCAT |
APP应用层并不属于SOEM库,应用层调用SOEM提供的服务,实现具体的应用,由用户按所需自行编辑。
本项目的移植流程参考文献:[1]王惠娇. 基于嵌入式平台的EtherCAT主站实现研究[D].郑州大学,2018.
网上也有人提供了程序源码,如以下的链接是基于STM32H743实现:
EasyControl ethercat总线 精简主从站方案 (amobbs.com 阿莫电子论坛
基于stm32和soem的EtherCAT主站代码分享 (amobbs.com 阿莫电子论坛
移植SOEM到嵌入式平台的过程就是重写SOEM抽象层的过程。在STM32H743的开发板上,通过RMII外接一个PHY芯片,型号是LAN8720A。STM32H743的以太网口电路详见官方资料《STM32H7开发指南-HAL库版本_V1.0 66.1.1》介绍。
使用RMII接口,其电路图如下:
以太网口的电路,具体可参考网上博主的教程:基于STM32构建EtherCAT主站(SOEM方案)3_stm32 ethercat_拉松的博客
STM32H743具有400MHz的CPU频率,可是实现微秒级别精度的系统时钟。为了避免使用tick机制实现系统时钟的中断干扰,采用硬件定时器级联,将TIM3和TIM2二者级联起来,其中TIM2为32位计时器,TIM3为16位计时器,将二者连起来实现48比特系统时间。
设置TIM2频率1MHz,1us计数一次。配置TIM2输出一个1Hz的时钟信号,作为TIM3的输入端。这样,TIM2的计数值就是微秒数,TIM5的计数值是秒数。
代码如下:
//计时器 通过读取CNT值实现计时 避免中断的影响 TIM2 TIM3
//设置TIM2主模式,输入时钟1mhz,产生1hz的TRGO信号
//设置TIM3为从模式,时间源为TIM2更新信号
void PhyTim_EtherCAT_Init(void)
{
//-----------------TIM2初始化------------------//
TIM_ClockConfigTypeDef sClockSourceConfig;
TIM_MasterConfigTypeDef sMasterConfig; //定时器主模式配置
TIM_SlaveConfigTypeDef sSlaveConfig; //定时器从模式配置
TIM2_Handler.Instance = TIM2; //通用定时器2
TIM2_Handler.Init.Prescaler=199; //分频
TIM2_Handler.Init.CounterMode=TIM_COUNTERMODE_UP; //向上计数器
TIM2_Handler.Init.Period=1000000-1; //自动装载值
TIM2_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;//时钟分频因子
TIM2_Handler.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; //自动重装载 写入新值后,计数器完成当前旧的计数后,再开始新的计数周期
HAL_TIM_Base_Init(&TIM2_Handler);
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; //使用内部时钟源 外部触发预分频器 关闭预分频器 TIM2->SMCR
HAL_TIM_ConfigClockSource(&TIM2_Handler, &sClockSourceConfig); //定时器时钟配置
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_ENABLE; //TIM2设置为主模式
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE; // 选择更新事件作为触发输出 (TRGO)
HAL_TIMEx_MasterConfigSynchronization(&TIM2_Handler, &sMasterConfig);
HAL_TIM_Base_Start(&TIM2_Handler); //使能定时器2
//-----------------TIM3初始化------------------//
TIM3_Handler.Instance = TIM3; //通用定时器3
TIM3_Handler.Init.Prescaler = 0; //不分频
TIM3_Handler.Init.CounterMode = TIM_COUNTERMODE_UP; //向上计数器
TIM3_Handler.Init.Period = 50000; //自动装载值
TIM3_Handler.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; //时钟分频因子
HAL_TIM_Base_Init(&TIM3_Handler);
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_ITR1; //定时器时钟源选择
HAL_TIM_ConfigClockSource(&TIM3_Handler, &sClockSourceConfig);
sSlaveConfig.SlaveMode = TIM_SLAVEMODE_EXTERNAL1; //外部时钟模式1 由所选触发信号 (TRGI) 的上升沿提供计数器时钟 TIM3->SMCR[2;0]=111
sSlaveConfig.InputTrigger = TIM_TS_ITR1; // 输入触发:选择 ITR0 作为输入源
sSlaveConfig.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING;// 触发极性:上升沿
sSlaveConfig.TriggerPrescaler = TIM_TRIGGERPRESCALER_DIV1;// 触发预分频:无
sSlaveConfig.TriggerFilter = 0x0; // 滤波:不需要任何滤
HAL_TIM_SlaveConfigSynchronization(&TIM3_Handler, &sSlaveConfig); //定时器从模式配置
HAL_TIM_Base_Start(&TIM3_Handler);
}
//定时器底册驱动,开启时钟,设置中断优先级
//此函数会被HAL_TIM_Base_Init()函数调用
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM6) //0.5ms TIM6
{
__HAL_RCC_TIM6_CLK_ENABLE();
HAL_NVIC_SetPriority(TIM6_DAC_IRQn,3,1);
HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);
}
else if(htim->Instance==TIM7) //2ms TIM7
{
__HAL_RCC_TIM7_CLK_ENABLE();
HAL_NVIC_SetPriority(TIM7_IRQn,4,1);
HAL_NVIC_EnableIRQ(TIM7_IRQn);
}
else if(htim->Instance==TIM2) // TIM2
{
__HAL_RCC_TIM2_CLK_ENABLE();
}
else if(htim->Instance==TIM3) // TIM3
{
__HAL_RCC_TIM3_CLK_ENABLE();
}
}
//读取时间
uint32_t GetSec(void) //获取时间/秒
{
return TIM3->CNT;
}
uint32_t GetUSec(void) //获取时间/微妙
{
return TIM2->CNT;
}
参考资料:STM32定时器同步模式 四种_tim_masterconfigtypedef_luowei_memory的博客
STM32H743的ETH介绍详见 《STM32H7开发指南-HAL库版本_V1.0 66.1.1》,正点原子的教程也有介绍:【正点原子】手把手教你学lwIP_哔哩哔哩_bilibili
nicdrv负责Ethernet帧的打包和解包,需要实现EtherCAT帧通过MAC层发送出去,并将从MAC层接收到的EtherCAT帧送给nicdrv。Nicdrv模块的实现是EtherCAT移植的关键。
参考网上博主的介绍,相应的代码更改为H743即可。
基于STM32构建EtherCAT主站(SOEM方案)2_stm32 ethercat_拉松的博客;
基于STM32构建EtherCAT主站(SOEM方案)5_拉松的博客
在调试的时候,报错找不到从站,经过调试时PHY初始化的时候,滤波模式没有设置好(F767为混杂模式设置),修正后的代码如下:
//LAN8720初始化
int32_t Lan8720_Init(void)
{
//===============================================================MAC====================================================================
u32 sn0 = *((vu32*)(0x1FF0F420)); //获取STM32的唯一ID的前24位作为MAC地址后三字节
u8 macaddress[6]; //MAC地址
//MAC地址设置(高三字节固定为:2.0.0,低三字节用STM32唯一ID)
macaddress[0]=2; //高三字节(IEEE称之为组织唯一ID,OUI)地址固定为:2.0.0
macaddress[1]=0;
macaddress[2]=0;
macaddress[3]=(sn0>>16)&0XFF; //低三字节用STM32的唯一ID
macaddress[4]=(sn0>>8)&0XFF;
macaddress[5]=sn0&0XFF;
//硬件复位
__set_PRIMASK(1); //关闭所有中断,复位过程不能被打断!
PhyPCF8574_WriteBit(ETH_RESET_IO,1); //硬件复位
delay_ms(100);
PhyPCF8574_WriteBit(ETH_RESET_IO,0); //复位结束
delay_ms(100);
__set_PRIMASK(0); //开启所有中断
NETMPU_Config(); //MPU保护设置
ETH_Handler.Instance=ETH; //ETH
ETH_Handler.Init.MACAddr=macaddress; //MAC地址
ETH_Handler.Init.MediaInterface=HAL_ETH_RMII_MODE; //RMII接口
ETH_Handler.Init.RxDesc=DMARxDscrTab; //发送描述符
ETH_Handler.Init.TxDesc=DMATxDscrTab; //接收描述符
ETH_Handler.Init.RxBuffLen=ETH_MAX_PACKET_SIZE; //接收长度
//初始化ETH
HAL_ETH_Init(Ð_Handler);
memset(&TxConfig, 0 , sizeof(ETH_TxPacketConfig));
TxConfig.Attributes = ETH_TX_PACKETS_FEATURES_CSUM | ETH_TX_PACKETS_FEATURES_CRCPAD; //Tx数据包硬件功能
TxConfig.ChecksumCtrl = ETH_CHECKSUM_IPHDR_PAYLOAD_INSERT_PHDR_CALC; //指定校验和插入控件:启用 IP 标头校验和和有效负载校验和计算和插入
TxConfig.CRCPadCtrl = ETH_CRC_PAD_INSERT; //指定CRC和Pad插入和替换控件
//===============================================================PHY====================================================================
uint32_t idx, duplex, speed = 0;
int32_t PHYLinkState; //Lan8720状态
ETH_MACConfigTypeDef MACConf; //ETH MAC 配置结构定义
ETH_MACFilterConfigTypeDef filterDef; //ETH MAC滤波器结构定义
HAL_ETH_GetMACFilterConfig(Ð_Handler,&filterDef); //获取 ETH MAC 过滤器配置
filterDef.PromiscuousMode = ENABLE; //过滤器使能PM模式
HAL_ETH_SetMACFilterConfig(Ð_Handler,&filterDef); //设置ETH MAC 过滤器
//将内存缓冲区分配给 DMA Rx 描述符
for(idx = 0; idx < ETH_RX_DESC_CNT; idx ++)
{
HAL_ETH_DescAssignMemory(Ð_Handler, idx, Rx_Buff[idx], NULL);
}
//===============================================================LAN8720=================================================================
uint32_t regvalue = 0, addr = 0;
u32 timeout=0; //超时计时
u32 regval=0; //lan8720addr 只有一个网口 默认使用0
u32 phylink=0;
HAL_StatusTypeDef rtn; //定义HAL状态结构
HAL_ETH_SetMDIOClockRange(Ð_Handler); //配置ETH MDIO接口的时钟范围
//获取lan8720地址
lan8720addr = 32;
for(addr = 0; addr <= lan8720addr; addr ++)
{
rtn = HAL_ETH_ReadPHYRegister(Ð_Handler, addr, LAN8720_SMR, ®value);
if( rtn != HAL_OK){continue;}
if((regvalue & LAN8720_SMR_PHY_ADDR) == addr)
{
lan8720addr = addr;
break;
}
}
if(LAN8720_WritePHY(LAN8720_BCR,LAN8720_BCR_SOFT_RESET)>=0) //LAN8720软件复位 判断LAN8720寄存器读写是否正常
{
//等待软件复位完成
if(LAN8720_ReadPHY(LAN8720_BCR,®val)>=0)
{
while(regval&LAN8720_BCR_SOFT_RESET)
{
if(LAN8720_ReadPHY(LAN8720_BCR,®val)<0)
{
PHYLinkState=LAN8720_STATUS_READ_ERROR;
break;
}
delay_ms(10);
timeout++;
if(timeout>=LAN8720_TIMEOUT) break; //超时跳出,5S
}
}
else
{
PHYLinkState=LAN8720_STATUS_READ_ERROR;
}
}
else
{
PHYLinkState=LAN8720_STATUS_WRITE_ERROR;
}
//===============================================================SET=================================================================
do
{
delay_ms(100);
PHYLinkState = LAN8720_GetLinkState(); //读取链接状态
}while(PHYLinkState <= LAN8720_STATUS_LINK_DOWN);
//打印连接状态
printf("ETH工作状态: ");
switch (PHYLinkState)
{
case LAN8720_STATUS_100MBITS_FULLDUPLEX:
duplex = ETH_FULLDUPLEX_MODE;
speed = ETH_SPEED_100M;
printf(" 100Mb/s FullDuplex \r\n");
break;
case LAN8720_STATUS_100MBITS_HALFDUPLEX:
duplex = ETH_HALFDUPLEX_MODE;
speed = ETH_SPEED_100M;
printf(" 100Mb/s HalfDuplex \r\n");
break;
case LAN8720_STATUS_10MBITS_FULLDUPLEX:
duplex = ETH_FULLDUPLEX_MODE;
speed = ETH_SPEED_10M;
printf(" 10Mb/s FullDuplex \r\n");
break;
case LAN8720_STATUS_10MBITS_HALFDUPLEX:
duplex = ETH_HALFDUPLEX_MODE;
speed = ETH_SPEED_10M;
printf(" 10Mb/s HalfDuplex \r\n");
break;
default:
duplex = ETH_FULLDUPLEX_MODE;
speed = ETH_SPEED_100M;
printf(" 100Mb/s FullDuplex__ \r\n");
break;
}
HAL_ETH_GetMACConfig(Ð_Handler, &MACConf); //获取 MAC 和 MTL 子系统的配置
MACConf.DuplexMode = duplex; //ETH工作模式 全双工/半双工
MACConf.Speed = speed; //ETH传输速度
MACConf.TransmitQueueMode = ETH_TRANSMITTHRESHOLD_128; //ETH传输模式 128字节
HAL_ETH_SetMACConfig(Ð_Handler, &MACConf); //MAC 配置
HAL_ETH_Start_IT(Ð_Handler); //开启ETH中断
HAL_ETH_BuildRxDescriptors(Ð_Handler); //将上次接收的数据包的 Rx Desc返回给 DMA
}
上一节参考链接介绍,已经将Ether CAT主站协议库SOEM成功移植到嵌入式平台STM32H743上,本章从EtherCAT主站实现的有效性、可靠性、稳定性出发,对嵌入式平台的EtherCAT主站驱动从站进行测试分析。
本项目调试所使用的伺服驱动器为英威腾的DA200,对于该伺服的介绍乐意查看以下链接:DA200交流伺服驱动器
实验平台及接线如下图所示,参考《DA200系列交流伺服驱动器中文说明书V2.2_3.2.1节》
参考说明书《英威腾SV-DA200交流伺服驱动器_EtherCAT技术指南V2.62》的第2章,在ServoPlorer软件上设置以下参数:
参数 |
名称 |
数值 |
P0.01 |
电机代码 |
2300 |
P0.03 |
控制模式选择 |
8 [EtherCAT 模式] |
P4.08 |
EtherCAT 同步类型 |
2: DCMode |
P4.07 |
EtherCAT同步周期 |
2: 1ms |
P4.09 |
EtherCAT 故障检测时间 |
100ms |
使用EtherCAT控制伺服电机,具体的配置可以总结为以下过程:
EtherCAT初始化过程 |
相关函数 |
配置初始化 |
ec_init() ec_config_init(FALSE) INVT_DA200_setup() |
分配分布式时钟 |
ec_configdc() |
配置过程数据映射 |
ec_config_map(&IOmap) |
请求所有从站进入到安全模式 |
|
其中的INVT_DA200_setup()为从站伺服器的PDO映射,参考《英威腾SV-DA200交流伺服驱动器_EtherCAT技术指南》介绍,具体的操作步骤为:
英威腾 PDO 映射的操作步骤代码如下:
// 英威腾 PDO 映射的操作步骤
int INVT_DA200_setup(uint16 slvcnt)
{
EtherCAT_write8(slvcnt, 0x1C12, 00, 0); // 1 停止PDO分配功能(将0x1C12和0x1C13的子索引0设置为0)
EtherCAT_write8(slvcnt, 0x1600, 00, 0); // 2 停止PDO映射功能(将0x1600~0x1603和0x1A00~0x1A03的子索引0全部设置为0)
EtherCAT_write32(slvcnt, 0x1600, 01, 0x60400010); // 3 设置PDO映射对象(0x1600~0x1603和0x1A00~0x1A03)的映射入口
EtherCAT_write32(slvcnt, 0x1600, 02, 0x607A0020); // 3 设置PDO映射对象(0x1600~0x1603和0x1A00~0x1A03)的映射入口
EtherCAT_write8(slvcnt, 0x1600, 00, 2); // 4 设置PDO映射对象(0x1600~0x1603和0x1A00~0x1A03)映射入口的数值
EtherCAT_write16(slvcnt, 0x1C12, 01, 0x1600); // 5 设置PDO分配对象(设置0x1C12和0x1C13的子索引1);
EtherCAT_write8(slvcnt, 0x1C12, 00, 1); // 6 重新打开PDO分配功能(将0x1C12和0x1C13的子索引0设置为1)
/**********************************************************************************************************/
EtherCAT_write8(slvcnt, 0x1C13, 00, 0); // 1 停止PDO分配功能(将0x1C12和0x1C13的子索引0设置为0)
EtherCAT_write8(slvcnt, 0x1A00, 00, 0); // 2 停止PDO映射功能(将0x1600~0x1603和0x1A00~0x1A03的子索引0全部设置为0)
EtherCAT_write32(slvcnt, 0x1A00, 01, 0x60410010); // 3 设置PDO映射对象(0x1600~0x1603和0x1A00~0x1A03)的映射入口
EtherCAT_write32(slvcnt, 0x1A00, 02, 0x60640020); // 3 设置PDO映射对象(0x1600~0x1603和0x1A00~0x1A03)的映射入口
EtherCAT_write8(slvcnt, 0x1A00, 00, 2); // 4 设置PDO映射对象(0x1600~0x1603和0x1A00~0x1A03)映射入口的数值
EtherCAT_write16(slvcnt, 0x1C13, 01, 0x1A00); // 5 设置PDO分配对象(设置0x1C12和0x1C13的子索引1);
EtherCAT_write8(slvcnt, 0x1C13, 00, 1); // 6 重新打开PDO分配功能(将0x1C12和0x1C13的子索引0设置为1)
EtherCAT_write8(slvcnt, 0x6060, 00, 8); //设置操作模式 :周期性同步位置模式
return 0;
}
此外,还要实现EtherCAT状态机,状态机用于描述从站应用的状态和状态改变。状态改变请求通常由主站发起,从站响应。具体状态跳转方式如下图:
状态机的实现代码如下所示:
//EtherCAT状态机
void EtherCAT_ctrl_state(void)
{
switch (outputs1->controlword) //获取当前状态 0x6041状态字
{
case 0: //Switch on disabled
outputs1->controlword = 6; //Shutdown
break;
case 6: //Ready to switch on
outputs1->controlword = 7; //Disable voltage + Quick stop
break;
case 7: //Switched on
outputs1->targetPostion = inputs1->actualPostion; //查询【6064h: Position actual value】来获取电机实际位置反馈
outputs1->controlword = 0x0f; // Enable operation
break;
case 0x0f: //Operation enabled
if( OpenReady == 0 )
OpenReady = 1;
else
OpenReady = 0;
break;
default:
outputs1->controlword = 6; //Shutdown
break;
}
}
对于EtherCAT状态设置主要是对6041h状态字的查询和设置,如下图所示:
使用周期性同步位置模式控制电机转动,在该模式下,插补周期与 EtherCAT同步周期相同(本文设置为1ms)。
1、 设置 6060 h为 8,选择 Cyclic Synchronous Position Mode;
2、 设置 6040 h以使能驱动器,发送 0x0F;
3、 逐次设置 607A h为目标位置(绝对位置),进行位置控制。
具体的操作方法如下图所示:
通过逐次设置 607A h为目标位置(绝对位置),进行位置控制,实现电机的转动。
以上是本人使用SOEM方案在STM32H743实现EtherCAT主站通讯,的整个过程,如有错误,还请指教交流。