返回列表 发新帖我要提问本帖赏金: 50.00元(功能说明)

[AT32F403/403A] 基于AT32F403ACCT7实现EtherCAT模块的FOE固件升级功能

[复制链接]
460|3
dffzh 发表于 2026-7-31 13:39 | 显示全部楼层 |阅读模式
, 固件, 模块, , ,
#技术资源# #申请原创#

一般情况下,我们开发的带MCU的EtherCAT模块应该都是要支持对MCU固件进行FOE升级的(一种远程升级固件的功能),也就是测试端、工厂端或者客户端在维修时,可以很方便的对模块进行固件升级。今天咱就以雅特力的AT32F403ACCT7芯片为例与大家聊聊FOE升级这件事,看看怎么样开发一个支持FOE升级的bootloader程序。
其实所谓的FOE,是EtherCAT协议里面的特有内容,全称为File Access Over EtherCAT,就是基于EtherCAT协议对MCU固件进行升级的一种文件传输协议,其固件文件的后缀为.efw(EtherCAT Firmware (File))格式,在通过codesys或者TwinCAT等上位机进行升级操作时,即会让你选择指定的efw格式文件:
1.png
2.png
而FOE升级的原理其实也是通过EtherCAT协议栈进行的,在协议栈代码里有几个文件,就是专门用于FOE的,包括ecatfoe.c,foeappl.c和bootmode.c源文件以及相应的头文件,而其他文件则会通过调用里面的接口的方式进行实际操作:
1.png
其中ecatfoe.c主要实现了FOE升级的主服务接口FOE_ServiceInd,这个接口会在邮箱通信里面被调用:
1.png
2.png
而foeappl.c主要实现了FOE升级的与状态机对应的相关具体操作,包括读写操作和接收数据操作等:
1.png
而bootmode.c主要实现了对MCU的flash进行擦除和写入数据的操作,这里面的代码就与MCU平台相关了,也是我们实现FOE固件升级的核心开发部分。

当然,EtherCAT协议栈默认是失能FOE升级的,我们在通过SSC工具配置协议栈代码的时候可以使能BOOTSTRAPMODE_SUPPORTED:
1.png
另外,我们还需要开启FOE_SUPPORTED来支持FOE功能:
1.png
配置完成后,在后续生成的协议栈代码里即会有所体现:
1.png
当然,在从站模块的ESI文件(xml文件)里需要配置一下以使能FOE,然后上位机里面的“下载”按钮就能操作了:
1.png
2.png
3.png
接下来我们就详细说说支持FOE升级的bootloader程序实现过程。
第一步当然是要把通过SSC工具配置好的EtherCAT协议栈导入工程,主要包括之前截图的那些文件,然后在定义的ecat_slave_main初始化接口里的HW_Init()里面实现与MCU硬件平台相关的配置,包括MCU和ESC芯片的SPI通信配置等。然后在main的while循环里调用MainLoop来执行协议栈代码。这一块和APP程序里的协议栈操作基本类似,就不详述了。
第二步就是要在foeappl.c里面实现读请求,写请求和写固件数据的接口代码。
其中FOE_Read接口是主站下发读文件数据的请求,我们这里可以不用实现;

FOE_Write接口是主站下发的写固件数据的请求,就是当我们在主站上位机上导入efw文件操作开始升级时,主站会把文件的名称和文件名长度下发给从站,这个时候我们可以加上逻辑代码,就是如果文件名称或者文件名长度不符合要求,就返回报错,停止升级,这是非常有必要的,可以防止更新错误的efw文件,参考代码如下:
  1. /////////////////////////////////////////////////////////////////////////////////////////
  2. /**
  3. \param     pName         Pointer to the name of the file
  4. \param     nameSize      Length of the file name
  5. \param     password      Password for the file read
  6. \return okay, busy or an error code
  7.             0 (okay)
  8.             ECAT_FOE_ERRCODE_NOTDEFINED (error)
  9.             ECAT_FOE_ERRCODE_NOTFOUND (error)
  10.             ECAT_FOE_ERRCODE_ACCESS    (error)
  11.             ECAT_FOE_ERRCODE_DISKFULL (error)
  12.             ECAT_FOE_ERRCODE_ILLEGAL (error)
  13.             ECAT_FOE_ERRCODE_PACKENO (error)
  14.             ECAT_FOE_ERRCODE_EXISTS    (error)
  15.             ECAT_FOE_ERRCODE_NOUSER    (error)

  16.             (no busy response shall be returned by this function.
  17.             If the slave stack requires some time to handle the incoming data the function FOE_Data() should return busy)

  18. \brief    The function starts a file writing

  19. *////////////////////////////////////////////////////////////////////////////////////////
  20. UINT16 FOE_Write(UINT16 MBXMEM * pName, UINT16 nameSize, UINT32 password)
  21. {
  22.         u8* pFileName = (u8*)pName;
  23.         u8* pMemName = (u8*)aFirmwareDownloadHeader;
  24.        
  25. #if BOOTSTRAPMODE_SUPPORTED
  26.     if ( ( nameSize >= 3 )
  27.         &&( pFileName[0] == pMemName[0] )
  28.         &&( pFileName[1] == pMemName[1] )
  29.         &&( pFileName[2] == pMemName[2] )
  30.             //&&( pFileName[3] == pMemName[3] )
  31.         )
  32.     {
  33.         if ( bBootMode )
  34.         {
  35.             BL_StartDownload(0,password);
  36.                         DownloadType=0;
  37.             return 0;
  38.         }
  39.         else
  40.             return ECAT_FOE_ERRCODE_BOOTSTRAPONLY;
  41.     }       
  42.     else
  43.     if ( bBootMode )
  44.         return ECAT_FOE_ERRCODE_NOTINBOOTSTRAP;
  45.     else
  46. #endif
  47. #if FOE_SAVE_FILES
  48.     if ( nameSize < MAX_FILE_NAME_SIZE )
  49.     {
  50.         /* for test every file name can be written */
  51.         MBXSTRCPY(aFileName, pName, nameSize);
  52.         MBXSTRCPY(aFileName+nameSize, "\0", 1); //string termination


  53.         nFileWriteOffset = 0;
  54.         nFileSize = 0;
  55.         return 0;
  56.     }
  57.     else
  58.         return ECAT_FOE_ERRCODE_DISKFULL;
  59. #else
  60.         DataValue = 0x00;
  61.         nFileSize = 0;
  62.         return 0;
  63. #endif
  64. }
其中aFirmwareDownloadHeader的定义如下,即为待升级文件的前面几个字符:
  1. #define  APP_NAME     "FOE_"  
  2. #define  MAX_FILE_NAME_SIZE   100
  3. const char  aFirmwareDownloadHeader[MAX_FILE_NAME_SIZE] = APP_NAME;
正常时,则主要通过调用BL_StartDownload接口来执行flash擦除操作。

另外,如果不像被轻易升级,我们可以在程序里增加密码口令,即FOE_Write接口里的第三个形参password,只有当操作升级时输入的密码和程序里写的密码一致时,才允许升级,否则报错返回,停止升级,如下图所示,是升级时需要输入的密码,如果程序里没有配置,则不用管:
1.png
FOE_Data接口则负责将二进制的固件数据通过里面的BL_Data接口写入MCU的flash里面,参考代码如下:
  1. /////////////////////////////////////////////////////////////////////////////////////////
  2. /**
  3. \param     pData         Received file data
  4. \param         Size          Length of received file data

  5. \return okay, busy or an error code
  6.             0 (okay)
  7.             FOE_MAXBUSY-100 (0%)    (busy)
  8.             FOE_MAXBUSY (100%) (busy)
  9.             ECAT_FOE_ERRCODE_NOTDEFINED (error)
  10.             ECAT_FOE_ERRCODE_NOTFOUND (error)
  11.             ECAT_FOE_ERRCODE_ACCESS    (error)
  12.             ECAT_FOE_ERRCODE_DISKFULL (error)
  13.             ECAT_FOE_ERRCODE_ILLEGAL (error)
  14.             ECAT_FOE_ERRCODE_PACKENO (error)
  15.             ECAT_FOE_ERRCODE_EXISTS    (error)
  16.             ECAT_FOE_ERRCODE_NOUSER    (error)

  17. \brief    The function is called to write the next part of a file

  18. *////////////////////////////////////////////////////////////////////////////////////////

  19. UINT16 FOE_Data(UINT16 MBXMEM * pData, UINT16 Size)
  20. {
  21. #if BOOTSTRAPMODE_SUPPORTED
  22.     if ( bBootMode )
  23.     {
  24.         return BL_Data(DownloadType,pData, Size);
  25.     }
  26.     else
  27. #endif
  28. #if FOE_SAVE_FILES
  29.         if((nFileWriteOffset + Size) > MAX_FILE_SIZE)
  30.             return ECAT_FOE_ERRCODE_DISKFULL;

  31.         if ( Size )
  32.         {

  33. #if MBX_16BIT_ACCESS
  34.                 MBXMEMCPY(&aFileData[(nFileWriteOffset >> 1)], pData, Size);
  35. #else
  36.                 MBXMEMCPY(&aFileData[nFileWriteOffset], pData, Size);
  37. #endif

  38.         }
  39. #endif
  40.         if ( Size == (u16ReceiveMbxSize - MBX_HEADER_SIZE - FOE_HEADER_SIZE) )
  41.         {
  42.             /* FoE-Data services will follow */
  43.             nFileWriteOffset += Size;
  44.             return 0;
  45.         }
  46.         else
  47.         {
  48.             /* last part of the file is written */
  49.             nFileSize = nFileWriteOffset + Size;
  50.             nFileWriteOffset = 0;

  51.             return FOE_ACKFINISHED;
  52.         }


  53. }
其中全局变量nFileSize是记录文件数据大小的;
BL_Data接口即操作具体的写flash过程;
这里有一点需要强调一下,也非常有用,默认的BL_Data接口是没有第一个参数DownloadType的,这个我们可以自行增加,就是我们可以通过这个来判断当前是升级MCU还是FPGA固件,当然,你必须在FOE_Write接口里根据文件名称来配置DownloadType为相应的值0或者1,然后在BL_Data里面操作,如果你的板子是支持通过MCU来给FPGA升级固件的话。
最后我们就是在bootmode.c里面实现与MCU硬件的交互了,也就是擦除flash和写flash。
我之前是用AT32F403ACCT7芯片来调试的,所以里面的flash操作接口就主要来源于at32f403a_407_flash.c文件。

其中BL_StartDownload里面负责擦除flash数据,毕竟更新数据之前要先erase:
  1. /////////////////////////////////////////////////////////////////////////////////////////
  2. /**
  3. \param    password    download password
  4. \brief Dummy BL_StartDownload function
  5. *////////////////////////////////////////////////////////////////////////////////////////
  6. void BL_StartDownload(UINT8 ID,UINT32 password)
  7. {
  8.         if(ID==0)//MCU
  9.         {       
  10.                 iFlashWriteOffset=DOWNLOAD_ADDRESS;
  11.                 iFileTotalSize=0;
  12.                 bsp_EraseCpuFlashSector(FLASH_ERASE_START_ADDR);
  13.         }       
  14. //        else if(ID==1) //FPGA
  15. //        {

  16. //        }       
  17. }
BL_Data负责写入数据到flash,另外,为了看到升级过程,我们可以通过RUN灯来指示:
  1. /////////////////////////////////////////////////////////////////////////////////////////
  2. /**
  3. \param    pData    Data pointer
  4. \param    Size    Data Length
  5. \return    FoE error code
  6. \brief Dummy BL_Data function
  7. *////////////////////////////////////////////////////////////////////////////////////////
  8. UINT16 BL_Data(UINT8 ID,UINT16 *pData,UINT16 Size)
  9. {
  10.         if(ID==0)//MCU
  11.         {
  12.                 bsp_WriteCpuFlash(iFlashWriteOffset,(uint8_t*)pData,Size);
  13.                 iFlashWriteOffset=iFlashWriteOffset+(Size);
  14.                 iFileTotalSize+=Size;
  15.         }       
  16. //        else if(ID==1)//FPAG
  17. //        {

  18. //        }
  19.         LedRun((*pData)&0x0001);
  20.     return 0;
  21. }
另外,上面接口里操作flash的函数bsp_EraseCpuFlashSector和bsp_WriteCpuFlash,我们可以定义在bsp_flash.c里面:
  1. uint8_t bsp_EraseCpuFlashSector(uint32_t FLASH_Sector)
  2. {
  3.         uint8_t  Ret;
  4.         uint8_t  i;
  5.         uint32_t eraseSectorAddr;
  6.        
  7.         __set_PRIMASK(1);                 
  8.         flash_unlock();
  9.        
  10.         /* erase 64 sectors */
  11.         eraseSectorAddr = FLASH_Sector;
  12.         for(i=0; i<FLASH_ERASE_SECTOR_SUM; i++)
  13.         {
  14.             Ret = flash_sector_erase(eraseSectorAddr);
  15.             eraseSectorAddr += FLASH_SECTOR_SIZE;
  16.         }
  17.        
  18.           flash_lock();
  19.         __set_PRIMASK(0);         
  20.         return Ret;
  21. }

  22. uint8_t bsp_WriteCpuFlash(uint32_t _ulFlashAddr, uint8_t *_ucpSrc, uint32_t _ulSize)
  23. {
  24.         uint32_t i;

  25.         if (_ulFlashAddr + _ulSize > FLASH_BASE_ADDR + FLASH_SIZE)
  26.         {
  27.                 return 1;
  28.         }

  29.         if (_ulSize == 0)
  30.         {
  31.                 return 0;
  32.         }

  33.         __set_PRIMASK(1);                  /* ¹ØÖÐ¶Ï */
  34.         flash_unlock();

  35.           /* Clear pending flags (if any) */
  36.         flash_flag_clear(FLASH_ODF_FLAG | FLASH_PRGMERR_FLAG | FLASH_EPPERR_FLAG );

  37. //        if (ucRet == FLASH_REQ_ERASE)
  38. //        {
  39. //                FLASH_EraseSector(bsp_GetSector(_ulFlashAddr), VoltageRange_3);
  40. //        }

  41.         for (i = 0; i < _ulSize; i++)
  42.         {
  43.                 flash_byte_program(_ulFlashAddr++, *_ucpSrc++);
  44.         }

  45.           flash_unlock();
  46.           __set_PRIMASK(0);                 
  47.         return 0;
  48. }
而这两个接口里面调用的函数便是固件库里的接口了,逐层调用,完成功能。

以上我们主要完成了固件下载操作,然后我们还得实现一下常规的app和bootloader之间的跳转操作,以保证程序正常运行。在bootloader里面,我们需要定义app的起始地址:
  1. #define  APP_ADDR     0x08020000
然后照葫芦画瓢,实现Jump2App接口:
  1. void Jump2App(void)
  2. {
  3.         uint8_t i;
  4.        
  5.         void(*jump2app)(void) ;
  6.         //
  7.         if(((*(volatile uint32_t *)APP_ADDR) & 0x2FFE0000) == 0x20000000)
  8.         {
  9.                 jump2app=(void(*)())*(volatile uint32_t *)(APP_ADDR + 4);  
  10.                
  11.                 __set_PRIMASK(1);
  12.                 for(i = 0; i < 8; i++) {
  13.                                 NVIC->ICER[i] = 0xFFFFFFFF;
  14.                                 NVIC->ICPR[i] = 0xFFFFFFFF;
  15.                 }
  16.                 __set_PRIMASK(0);
  17.                 SysTick->CTRL = 0;
  18.                 SysTick->LOAD = 0;
  19.                 SysTick->VAL = 0;       
  20.                 MSR_MSP(*(volatile uint32_t *)APP_ADDR);
  21.                 jump2app();
  22.                
  23.         }
  24.         else
  25.         {
  26.             NVIC_SystemReset();
  27.         }       
  28. }

  29. __asm void MSR_MSP(u32 addr)
  30. {
  31.         MSR MSP, r0
  32.         BX r14
  33. }
然后在main的while循环里按需要操作即可:
  1. while(1)
  2. {
  3.         HW_EscReadWord( EscAlControl_foe, ESC_AL_CONTROL_OFFSET);
  4.         EscAlControl_foe = SWAPWORD(EscAlControl_foe);
  5.     if(((EscAlControl_foe&STATE_BOOT)==STATE_BOOT)&&(EscAlControl_foe!=0xFFFF))
  6.         {
  7.                 i=0;
  8.                 //enter boot for foe
  9.                 if(boot_flag && (boot_cnt++>=0x4FFF))
  10.                 {
  11.                                 boot_flag = 0;
  12.                                 boot_cnt = 0;
  13.                         AL_ControlInd(STATE_BOOT, 0);
  14.                 }
  15.         }
  16.         else
  17.         {
  18.                 if(i++>0x2FFF)
  19.                 {
  20.                         i=0;       
  21.                         Jump2App();
  22.                 }       
  23.         }
  24.        
  25.         //ethercat process
  26.         MainLoop();
  27. }
然后在程序正常运行的时候,如果app程序收到进入bootstrap模式的命令,则MCU执行系统复位,回到bootloader程序,开始执行下一步的FOE升级操作:
  1. /*foe update thread*/
  2. static void foe_thread(void)
  3. {
  4.         if((nAlStatus & STATE_MASK)==STATE_BOOT)
  5.         {
  6.                 if(++BootTime > BOOT_SWITCH_DELAY)
  7.                 {
  8.                         NVIC_SystemReset();
  9.                 }       
  10.         }
  11.         else
  12.         {
  13.                 BootTime=0;
  14.         }
  15. }
当然,APP程序里和开发环境里都要设置偏移地址,即APP程序是从APP_ADDR位置开始运行的,如下所示:
  1. #define FOE_UPDATE_ENABLE
  2. #define NVIC_VECTTAB_FLASH       ((uint32_t)0x08000000)
  3. #define APP_VECTOR_OFFSET_ADDR  ((uint32_t)0x00020000)
  4. #ifdef FOE_UPDATE_ENABLE
  5. nvic_vector_table_set(NVIC_VECTTAB_FLASH,APP_VECTOR_OFFSET_ADDR);
  6. #endif
1.png
然后最终发布的固件就是将bootloader.bin文件和app.bin文件合并在一起的一个bin文件,你可以通过J-Flash软件进行merge,或者其他小工具也可以:
1.png
如果你用的是其他MCU平台,操作方法也是一样的,主要修改的部分就是和MCU相关的flash接口部分,然后你就可以玩转FOE升级模块MCU程序了:
1.png
2.png
有了FOE远程升级功能,即使客户现场出现问题,远程即可搞定,再也不用千里迢迢跑去客户现场了~~避免了被客户围观的各种尴尬~~

打赏榜单

21小跑堂 打赏了 50.00 元 2026-08-20
理由:恭喜通过原创审核!期待您更多的原创作品~~

评论

这篇文章介绍了如何基于 AT32F403ACCT7,利用 EtherCAT 的 FoE(File Access over EtherCAT)协议实现远程固件升级。文章给出了一条完整的“主站下发 .efw → FoE ...  发表于 2026-8-20 17:15
yiyigirl2014 发表于 2026-8-4 10:14 | 显示全部楼层
AX58100 负责 EtherCAT 报文解析、PDO/SDO/FOE 底层处理;AT32F403 通过 SPI 接口和 AX58100 通信,实现 FOE 上层业务、固件存储、Flash 升级。
wangwu1976@ 发表于 2026-8-25 11:22 | 显示全部楼层
学习了
您需要登录后才可以回帖 登录 | 注册

本版积分规则

562

主题

4015

帖子

27

粉丝
快速回复 在线客服 返回列表 返回顶部
0