简介
一个Windows程序一般是由一个exe和多个dll文件组成,在exe程序运行时调用dll的二进制代码,这样做相对所有的程序都写到一个exe文件里边有有几个优势:
1.增加代码的复用,比如DuiLib.dll一个界面库,微信客户端使用这个dll,百度网盘客户端也使用这个这个dll。
2.单个工程的文件少,简单,减少编译时间。
3.方便团队工作的分工.
4.效率高,体积小,相比一般脚本语言,脚本的速度太慢.
5.方便程序升级.相比静态库做升级时,exe程序也要重新编译,而dll升级,其它文件不需要重新编译.
6.和引导的区别:引导只是完成从程序的下载,然后跳到从程序去运行,然后不再运行引导程序了,从程序的编程对象还是STM32库函数和寄存器,和引导程序有很大一部分驱动程序是冗余的.Dll的这种架构编程是通过调用结构体函数指针,来实现业务功能,这和单片机标准库和寄存器完全无关,只和业务结构体函数指针接口相关,dll和主Hex完全没有冗余,这样就做到电路板的结构更变(指令集不变),dll在二进制的机器代码层面都不需要做改变.
那么能否将Windows的dll这种架构也可以应用到STM32单片机开发中呢?答案是可以的。Keil编译的STM32程序一般是以Hex或者Bin结尾的。STM32使用DLL时意味着单片机存在着两个或者以上的Hex文件. 负责主动调用的主Hex相当于EXE文件,被动调用的从Hex文件相当于Dll文件。主Hex文件存放Flash的0x8000000开始的前几个扇区,RAM可以分配在前48K,从0x20000000开始,第一个Dll存放可以存放在Flash的后面的一个扇区,如从0x8020000开始的第5个扇区,RAM可以分配8K,从2000C000开始.第二个Dll存放可以存放在Flash的最后扇区,从0x8040000开始,RAM可以分配8K,从2000C000开始.主Hex文件通过读取从Hex Dll文件的第一条指令确定从Hex文件的函数入口,然后跳到函数入口完成从Hex Dll的全局变量和静态变量的初始化,以及从Hex的接口函数指针的初始化!
STM32程序结构
STM32程序包括机器指令和变量数据,机器指令存储在Flash里边,只可以读取,不能写.变量存储在RAM里边,可以进行读取操作,也可以写操作。一般变量都有初始值,这是在Keil编译代码的时候分配的。Keil编译器编译代码后会生成一个Hex文件,这个Hex文件的前面部分存放机器指令,后面部分存放变量的初始值,烧录程序时会把机器代码连同变量的初始值写到Flash。单片机上电之后一般会跳到main函数去执行代码,但是在跳到main函数之前会先执行StartUP.s启动文件的代码,在StartUp.S文件一般会有一个__Main函数,这个是个系统函数和main函数不同,它会把Flash的变量初始值拷贝到RAM变量对应的位置,从而在main之前就完成了变量的初始化,然后跳到main函数去执行用户编写的代码。
STM32 DLL文件结构及Keil工程设置
主Hex的启动文件StartUp.S会自动调用库函数__Main完成全局变量,静态变量的初始化,然后跳到用户自定的Main函数.Dll不使用自带的__Main函数完成变量的初始化,那就需要自己写代码完成变量的初始化,这个是在主Hex加载Dll前完成.Dll的启动文件相对主Hex文件要简单很多,只有9行汇编代码,用于指示初始化函数的入口,变量的开始结束位置.
主Hex完成Dll的全局 静态变量初始化
void InitDllVar()
{
unsigned int HexVarStartAddr = *(unsigned int*)0x8020004; //变量初始值在Flash的开始地址
unsigned int HexVarEndAddr = *(unsigned int*)0x8020008; //变量初始值在Flash的结束地址
unsigned char* pRamStartAddr = (unsigned char*)0x2000C000;
memcpy(pRamStartAddr,(unsigned char*)HexVarStartAddr,(HexVarEndAddr-HexVarEndAddr));
}
主hex加载dll流程:
InitAppVar(); //初始化dll全局 局部变量
InitAppParam();
gETree = InitApp(&gLib); //加载业务dll初始化入口
LockAppParam();
Dll启动文件StartUp.S
AREA RESET, Code, READONLY,ALIGN=4
IMPORT InitDll
IMPORT |Load$$ER_IROM1$$Limit|
IMPORT |Load$$RW_IRAM1$$Limit|
DCD InitDll
DCD |Load$$ER_IROM1$$Limit| ; 变量初始值的开始位置
DCD |Load$$RW_IRAM1$$Limit| ; 变量初始值的结束位置
DCD 365 ; dll存在标志
END
这段汇编代码相对与主Hex要简单很多,和单片机体系结构无关,只是申明了当前工程的程序大小 变量所使用的RAM大小,在单片机中是这样存储的:
由于Dll是由主的Hex文件被调用的,所以Dll存放的位置没在第一扇区0x8000000的位置,RAM也没在默认的0x20000000的开始位置,这两个位置可以根据自己的需求设置,我的设置如图所示.
Dll函数接口结构体
Dll的功能函数由一个函数结构体暴露给主Hex,使用一个结构体包含Dll的函数,主Hex使用一个结构体针指向从Hex 的结构体即可.
Dll函数接口
typedef struct _ActLib
{
//步进
void (*RunSM)(char sm_id,int nLen);
void (*BrakeSM)(char sm_id);
void (*RunABS)(char sm_id,int step);
int (*GetAbsPos)(unsigned char sm_id);
void (*SetAbsOrg)(unsigned char sm_id);
void (*DisableSM)(unsigned char sm_id);
char (*IsSMStop)(char sm_id);
void (*StopSM)(unsigned char sm_id);
char (*GetDir)(char sm_id);
void (*SetSMSpeed)(char ID,int nSpeed);
void (*SetSoftNLimt)(char sm_id,char sensor);
void (*SetSoftPLimt)(char sm_id,char sensor);
//输入传感器,输出
char (*IsSensorOn)(char SensorNum);
char (*IsSensorOff)(char SensorNum);
void (*SetSensorLogic)(char Id,char Logic);
void (*WriteIO)(char dm_id,char status);
void (*NextTo)(unsigned char NextStep);
void (*SetActionError)(unsigned int nErrorCode);
unsigned short (*IsActionOk)(unsigned char ActionID);
unsigned short (*IsActionRun)(unsigned char ActionID);
void (*SetActionOk)(void);
void (*SetResult)(char id,unsigned short result);
unsigned char (*StartAction)(char ActionId);
void (*AddAction)(int nActionNum,void (*OpApp)(),const char* pActionName);
void (*AddSlaveAction)(int nActionNum,char SlaveActionNum,char BoardID,const char* pActionName);
void (*AddActionParam)(unsigned char ID,char ParamPos,char paramtype,const char* pParamName);
void (*SetActionParam)(char ActionId,char* ActionParam,char Len);
void (*ExitAction)(char ActionId);
unsigned char (*GetByteParam)(int pos);
//事件
void (*WaitSMStop)(unsigned char SmId,unsigned char StopStep);
void (*WaitSensorOn)(unsigned char SensorId,unsigned char OnStep);
void (*WaitSensorOff)(unsigned char SensorId,unsigned char OffStep);
void (*WaitAction)(unsigned char ActionId,unsigned char OkStep,unsigned char ErrorStep);
void (*JustWaitAction)(unsigned char ActionId,unsigned char OkStep,unsigned char ErrorStep);
void (*WaitTimeOut)(unsigned int Time,unsigned char OutStep);
void (*WaitRunLength)(unsigned char SmId,signed long nLen,unsigned char StopStep);
void (*SetErrorInfo)(const char* error_text);
void (*SetSubError)(void);
void (*FindSensor)(unsigned char MotorId,signed int nLen,unsigned char SnId,unsigned char OkStep,unsigned char ErrorStep);
void (*LeaveSensor)(unsigned char MotorId,signed int nLen,unsigned char SnId,unsigned char OkStep,unsigned char ErrorStep);
}ActLib;
主Hex的结构体指针声明:
ActLib* L;
从Hex结构体定义和初始化:
ActLib mActLib;
mActLib.RunSM = RunSM;
mActLib.BrakeSM = BrakeSM;
mActLib.SetSMSpeed = SetSMSpeed;
mActLib.RunABS = RunABS;
....
主Hex加载Dll以及初始化函数接口的结构体
void InitDll(ActLib* pLib)
{
L = pLib;
}
主Hex文件使用Dll库函数:
STEP_START(MoveBelt):
if(L->IsSensorOn(SN_PUSHEMS_DONE))
{
L->SetErrorInfo("分拣衔接有信封");
break;
}
if(L->IsSensorOn(SN_BELT_ORG))
{
L->LeaveSensor(SM_BELT,BeltAngle(360),SN_BELT_ORG,STEP2,STEP4);
}
else
{
L->FindSensor(SM_BELT,BeltAngle(360),SN_BELT_ORG,STEP3,STEP5);
}
lSTEP2:
L->WaitSensorOn(SN_BELT_ORG,STEP3);
L->WaitSMStop(SM_BELT,STEP5);
lSTEP3:
L->StopSM(SM_BELT);
L->SetActionOk();
lSTEP4:
L->LeaveSensor(SM_BELT,BeltAngle(360),SN_BELT_ORG,STEP2,STEP_ERROR);
lSTEP5:
L->FindSensor(SM_BELT,BeltAngle(360),SN_BELT_ORG,STEP3,STEP_ERROR);
lSTEP_ERROR:
L->SetErrorInfo("皮带电机丢步");
STEP_END
Dll更新
Dll的更新一般是主Hex接收来自串口的Dll的二进制数据,然后把DLL数据写到指定的扇区。也可以JLink把Dll烧录到特定的扇区。