可执行文件的加载运行,是计算机操作系统一个很古老、很基础的问题,这个问题早已经很好的被解决了,基本上以ELF(Executable and Linkable Format)为标准。我们在计算机上理所当然的启动可执行文件,没有觉得有什么稀奇的。如果我们“理所当然”的期望其他操作系统也支持可执行文件加载运行,心里的落差是相当巨大的。
在嵌入式软件领域,应用的场景差异很大,软件的组成部分不像个人电脑或者服务器那样有区分明确的系统软件和应用软件,通常有三种情形:1、以嵌入式Linux(安卓、WinCE或者Vxworks)等成熟的操作系统为平台,运行应用领域的应用软件;2、微型操作系统FreeRTOS、UCOS内核作为领域应用软件的支持库,和应用软件一起编译成二进制文件在板子上运行;3、没有操作系统,应用软件把需要做的所有事情全做了。在IOT应用领域,网络通信是必不可少的基础,所以1、2的情况居多,结合成本因素,第2种情况最为普遍。在有网络通信的基础上,远程升级维护的需求突显重要,系统和应用一起编译的现状令人苦恼。
网络上关于 FreeRTOS或者ESP32实现ELF文件加载运行的信息较少,有一篇博文Elf Loader (ourembeddeds.github.io)把ELF在MCU上加载运行的原理和方法做了清晰的介绍。
按照文章所讲的方法做,也可以达成目标,不过程序的开发调试要点工作量。我们采取了开发平台软件和板上系统软件各做一部分的方式实现ELF文件的加载运行。具体就是在电脑上调用python 程序将 elf文件进行一次预处理,提取入口地址、程序段(.text)地址和容量、数据段(.data和.rodata)地址和容量、以及bss段的地址和容量,保存为新的二进制文件,有PLC的基础系统软件加载到ESP32的内存中运行。处理各段的内容的python代码:
def tobin(self):
fo = open("plc.bin","wb")
a = bytearray(4)
n = elf.elf32_Ehdr.e_entry
a[0] = n&0xFF
a[1] = (n>>8)&0xFF
a[2] = (n>>16)&0xFF
a[3] = (n>>24)&0xFF
fo.write(a)
ss =['.rom.data','.sram1.text' , '.psram.data']
for s in ss:
sec = elf.getSectionByName(s)
if sec is None:
continue
n = sec.sh_addr
a[0] = n&0xFF
a[1] = (n>>8)&0xFF
a[2] = (n>>16)&0xFF
a[3] = (n>>24)&0xFF
fo.write(a)
n = sec.sh_size
a[0] = n&0xFF
a[1] = (n>>8)&0xFF
a[2] = (n>>16)&0xFF
a[3] = (n>>24)&0xFF
fo.write(a)
elf.f.seek(sec.sh_offset, 0)
fo.write(elf.f.read(n))
fo.close()
各段的内存起始地址和占用空间,PLC基础系统软件和PLC的应用程序需要保持一致。在PLC基础系统软件中预留出对应的存储空间,在PLC应用程序中使用这些存储空间。这些需要在编译链接的过程中明确指定。
MEMORY
{
rom_seg (RX) : org = 0x00000000, len = 0x80000
iram_seg (RX) : org = 0x400A0000, len = 0x10000
dram_seg (RW) : org = 0x3FFD8000, len = 0x10000
eh_seg : org = 0xc0000000, len = 0x10000
}
PLC基础系统软件和PLC的应用程序之间有相互调用的函数入口需要在加载的时候明确指定。
void* fp_app[]={
__init,
__run
};
void entry(void* p){
PLC_VARS* vp = (PLC_VARS*)p;
void** ftbl;
if(p==NULL){
return;
}
vp->common_ticktime = common_ticktime__;
vp->vars = vars;
vp->vars_n = sizeof(vars)/sizeof(VARP);
vp->inputList = inputVarList;
vp->input_n = sizeof(inputVarList)/sizeof(VARP);
vp->outputList = outputVarList;
vp->output_n = sizeof(outputVarList)/sizeof(VARP);
vp->pous = pous;
vp->pous_n = sizeof(pous)/sizeof(VARP);
vp->prgs = prgs;
vp->prg_sz = prg_sz;
vp->prgs_n = sizeof(prgs)/sizeof(VARP);
vp->cfg = global_cfg;
vp->cfg_n = sizeof(global_cfg)/sizeof(char*);
ftbl = (void**)vp->fp_os;
FB_init = ftbl[0];
FB_body = ftbl[1];
get_time_fp = ftbl[2];
remind_fp = ftbl[3];
retain_fp = ftbl[4];
append_chain_fp= ftbl[5];
vp->fp_app = fp_app;
}
完成这些基础的工作以后,PLC应用程序的编译就可以和PLC基础系统软件分开,需要更新PLC应用的时候,编译PLC应用程序就好,然后通过网络或者下载接口把二进制文件传到板子上,由PLC基础系统软件加载运行即可。即便包含了MQTT 接收、发送模块的PLC应用程序,容量也在64K字节以内。
编译的输出为:
[1/6] Building C object CMakeFiles/plc.dir/plc_main.c.obj
[2/6] Building C object CMakeFiles/plc.dir/Config0.c.obj
[3/6] Building C object CMakeFiles/plc.dir/VARIABLES.c.obj
[4/6] Building C object CMakeFiles/plc.dir/main.c.obj
[5/6] Building C object CMakeFiles/plc.dir/POUS.c.obj
[6/6] Linking C executable plc
[+] Section Header Table:
# Name Type Addr Offset Size ES Flg Lk Inf Al
[ 1] .rom.data PROGBITS 0x0 0x1000 0xb3a 0 3 0 0 4
[ 2] .sram1.text PROGBITS 0x400a0000 0x3000 0x22fc 0 7 0 0 4
[ 3] .psram.data PROGBITS 0x3ffd8000 0x2000 0x558 0 3 0 0 8
[ 4] .psram.bss NOBITS 0x3ffd8558 0x2558 0x68 0 3 0 0 8
[ 15] .bss NOBITS 0x3ffd85c0 0x2558 0x9e8 0 3 0 0 8
当前的容量大约为10K字节,用网络远程传输就可以在大约2秒内完成,所期望的远程升级维护特性就比较实用。