让STM32F运行dll
因项目的一些实际问题,无奈之下开发了这种方式的程序。废话不多说,下面上干货。
背景:
STM32F429,外存16M,外扩SD卡,FatFS,USB(可选)
目标:STM32F429 运行SD卡中的DLL程序,可以直接通过USB或SD卡拷贝更新升级程序。
原理:在STM32F429中运行类似uboot的程序,找到SD卡中的elf文件,利用elf的特性,加载程序到外存,跳转到外存中运行。
需要的基础知识点:
1、FatFS
2、分散加载
3、ELF文件格式
4、C,ARM汇编基础,有最好,没有也不影响
5、RAM、ROM、外扩RAM硬件基础常识
6、其它一些帮助理解的知识(可有可无),有这些基础会帮助快速理解程序如何在单片机系统上运行:嵌入式操作系统、编译原理、UBoot、Bootloader、GCC、嵌入式OOP开发等等。
步骤:
1、先写个测试用的例程
2、再写个加载DLL的类Bootloader
注意:ARM的DLL与Window的DLL不是一回事,注意区分。 因CPU架构不同,指令格式也不同,Window下的DLL在ARM下是不能直接运行的!!!这里借用elf格式的文件(Keil会自动生成axf文件)实现DLL,可以使用GCC处理得到,也可以直接由Keil生成。
另外需要说明的是,通过外部RAM运行程序是一种无奈的选择,这会导致程序运行变慢!加载到内存运行也不是最优解(同样会变慢,只是比外存快一些,不过,你要是舍得花银子的话,当我没说)。原因是ARM架构本身可以同时处理指令和数据,都加载到内存或外存时,只能分两步来处理,这必然要比在指令在Flash中,数据在RAM中要慢。当然,有坏处,自然也有好处,而且好处也不少,至于是什么好处可以自行脑补。。。
测试例程如下:
//int argNum __attribute__((at(0X20000000)));
//int argValue __attribute__((at(0x20000004)));
int main(int argc,char **argv)
{
Stm32_Clock_Init(336,25,2,7);//设置时钟,168M,OTG= 48M
int * pargv;
pargv = (int *)0x20000004;
NVIC_SetVectorTable(*pargv,0);
USART1_Init(115200);
//显示开机动画
OLED.OLED_Init();
OLED.ShowAnimation();
cout<<"This is a dll test!\r\n"<<endl;
LED_Init();
while(1)
{
LED_GREEN = !LED_GREEN;
cout<<"Hello,I'm app!"<<endl;
Delayms(500);
}
return 0;
}
上面代码很简单,意思就是让LED灯500ms翻一下(闪烁),顺便打印一下字符,说明“App还活着"。
关键代码:(分散加载)
LR_IROM1 0xC0010000 0x00100000 { ; 外存地址
ER_IROM1 0xC0010000 0x00100000 { ; 加载地址
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x00030000 { ; RW data
.ANY (+RW +ZI)
}
}
我这里不处理内部摆放问题,仅为演示运行原理。好了,到这里,测试例程就OK了。
经测试,程序可以使用C写,也可以使用C++写,没有影响。
2、类Bootloader的加载程序
int argNum __attribute__((at(0X20000000)));
int argValue __attribute__((at(0x20000004)));
void LoaderDll(FIL *fp); //加载DLL
unsigned int exMem_Addr_Base; //加载的基地址
先模仿UBoot的做法,定义个约定好的地方,回头看下前面的例程,argValue这个参数可以直接用于重置复位地址。argNum:用于传递参数个数,argValue可以用于传递应用程序需要的参数或参数组
int main(void)
{
FATFS *fs;//逻辑磁盘工作区.
FIL *fp; //操作的文件指针
Stm32_Clock_Init(336,25,2,7);//设置时钟,168M,OTG= 48M
USART1_Init(115200);
printf("Boot is starting... \r\n");
Beep_Init();
LED_Init(); //初始化LED时钟
LED_GREEN = LED_ON;
SDRAM_Init();
my_mem_init(SRAMEX);
MPU_Config();
//初始化SD卡 & SDIO接口
if(SD_Init()) Beep(200);
//FatFS
fs = (FATFS*)malloc(sizeof(FATFS));
fres = f_mount(fs,"1:",1); //挂载SD卡
if(fres != FR_OK) Beep(200);
char *pPath ="1:/Test/ElfTest.axf";
fp = (FIL *)malloc(sizeof(FIL));
fres = f_open(fp, pPath, FA_READ);
if(fres != FR_OK)
{
Beep(200);
return 0;
}
//加载程序
LoaderDll(fp);
f_close(fp);
typedef int (*Fun)();
Fun fun;
//重定位入口点
argValue = exMem_Addr_Base;
// fun = (Fun)(mem_Addr_Base + 5); //OK 加载到内存,运行速度比较慢
fun = (Fun)(exMem_Addr_Base + 5); //OK 加载到外存,运行速度非常慢
//执行程序
fun();
return 0;
}
关键代码:
//加载程序到内存
for(int i=0;i<elf_ehdr->e_phnum;i++)
{
// printf("//===== 程序头部 %d 信息 ======//\n",i);
f_lseek(fp,elf_ehdr->e_phoff + i * elf_ehdr->e_phentsize);
index = 0;
fres = f_read(fp,elf_phdr,sizeof(Elf32_Phdr),&index);
if(index == 0) Beep(200);
if((elf_phdr->p_type == PT_LOAD)&&(elf_phdr->p_flags & PF_X))
{
//定位
f_lseek(fp,elf_phdr->p_offset);
//Copy segment
index = 0;
fres = f_read(fp,(void *)elf_phdr->p_paddr,elf_phdr->p_filesz,&index); //因STM32F4无MMU,故直接装载到实际物理地址
if(index == 0) Beep(200);
mem_Addr_Base = elf_phdr->p_paddr;
//填充bss段
if(elf_phdr->p_filesz < elf_phdr->p_memsz) //填充bss段
mymemset((elf_phdr->p_paddr + elf_phdr->p_filesz),0,(elf_phdr->p_memsz - elf_phdr->p_filesz));
// //更新到下一段
// mem_addr += elf_phdr->p_align;
}
//内存初始化
//...
}
OK,是不是非常简单?大体意思就是,初始化应用程序的运行环境,我这里简单加载了FatFS,SD卡,然后从SD卡中读入程序文件,最后跳转到App去运行。
到此处就可以测试收工了。运行结果如下图所示: