汽车电子expert成长之路

本博客发布的个人原创精品----嵌入式系统技术文章,欢迎大家参考学习,并转发分享!

嵌入式软件开发之S12(X)系列MCU的far和near函数指针调用详解

1
阅读(317) 评论(0)

作者按:如果你在开发S12(X)系列MCU的bootloader或者使用NVM SSD时,用到函数指针实现bootloader到APP应用工程跳转或者使用函数指针调用重映射(remap/relocated)/拷贝到RAM的NVM SSD API时,遇到了“莫名”的程序跑飞,本文将告诉你根本原因和解决办法。


内容提要

引言

1. S12(X) CPU内核的跳转和函数调用和返回指令

2. S12(X) 系列MCU的CodeWarrior 5.x应用工程中的near和far类型指针的定义与区别

    2. 1. near类型指针定义

    2.2 far类型指针定义

    2.3 反汇编分析near和far对函数/函数指针的影响

3. S12(X) 系列MCU的CodeWarrior 5.x应用工程中使用函数指针调用函数的方法步骤和注意事项

    ①使用near类型函数指针调用

    ②使用far类型函数指针调用

总结


引言


函数指针是C语言指针中比较特别的一类,也是C语言高级应用的难点之一。顾名思义,函数指针就是指向函数的指针,由于函数定义时参数和返回值类型和多少的不同,函数千变万化,因此指向函数的函数指针必须事先定义,并且与最终指向的函数定义形参和返回值一致。


在嵌入式系统软件开发中,尤其是汽车ECU的bootloader开发时,我们常用到函数指针,以完成bootloader到应用程序APP的跳转以及数组类型的Flash SSD位置独立驱动的调用。


在S12(x)系列MCU中,由于其存储器存在分页访问机制,其未分页区中的数据和代码使用16-bit的near类型地址即可访问,而位于分页地址中的数据和代码,必须使用24-bit的far类型地址才能正确寻址和访问。 所以使用函数指针调用函数时,near和far类型将影响其运行结果。


本文先介绍S12(X) CPU内核的程序跳转和函数调用与返回指令,然后,再结合S12G128的CodeWarrior 5.x应用工程,介绍其应用工程中far与near函数指针/数据指针的区别和使用注意事项。解释由于函数指针使用不恰当,导致函数调用和返回指令不匹配引起的程序跑飞问题的根本原因,希望对大家有所帮助。


1. S12(X) CPU内核的跳转和函数调用和返回指令


在S12(X) 系列MCU使用的CPU内核--S12(X) CPU core中,有以下程序跳转和子函数(subroutine)调用及函数返回指令:

10.jpg

其中,

JMP为直接跳转指令,可以直接跳转到64KB地址空间范围,其不压栈返回地址,所以无法返回;


BSR和JSR都是子函数调用跳转指令,用于调用在64KB局部地址中或者相对地址小于64KB的子函数(16-bit地址),其会使用两个字节的系统堆栈压栈函数返回地址,与之对应的子函数返回指令为RTS,运行RTS时将出栈函数返回地址;


CALL是扩展存储器(expanded memory)子函数调用指令,其执行时,除了压栈16-bit的PC寄存器中的函数返回地址外,还将压栈P-Flash分页寄存器PPAGE,并更新子函数的PPAGE寄存器,所以,其适用于跨页函数调用,能够跨页识别和处理24-bit的逻辑地址。相应地,其返回指令为RTC,运行RTC时将出栈PPAGE寄存器和函数返回地址,从而保证跨页函数调用能够正确返回。

Tips:从以上介绍可知,JMP、BSR和JSR以及RTS指令的执行效率比CALL和RTC指令的执行效率要高,但后者的寻址空间可以大于64KB,适用于定义在分页地址中的函数调用和返回。


在进行应用程序开发时,一般情况下,CodeWarrior 5.x IDE工具链都能够更具函数定义和工程链接文件地址分配情况自动选择对应的正确指令,无需用户特别关心。


2. S12(X) 系列MCU的CodeWarrior 5.x应用工程中的near和far类型指针的定义与区别


在S12(X) 系列MCU的CodeWarrior 5.x应用工程中,可以使用near和far来修饰用户定义的指针和函数,其正确有效的定义格式如下:


2. 1. near类型指针定义


<数据类型> * __near  <指针名>; 或者省略关键词__near, 此时定义默认为near类型:

<数据类型> * <指针名>;


比如:定义指向字节类型数据的near指针:byte * __near   Ptr;或者 byte *Ptr;

  定义指向无参数无返回值类型函数的near函数指针:typedef void (* __near   FN_Ptr)(void);


2.2 far类型指针定义


<数据类型> * __far <指针名>;


比如:定义指向字节类型数据的far指针:byte * __far Far_Ptr;

 定义指向无参数无返回值类型函数的far函数指针:typedef void (* __far FN_Ptr)(void);


在S12(X) 系列MCU的CodeWarrior 5.x应用工程中,near(或者默认缺省)类型的指针占用2个字节地址,其寻址范围为2^16=64KB,仅限于访问S12(X)系列MCU的本地地址空间;而far类型的指针占用3个字节,其访问空间为2^24=16MB,可以包括任意S12(X)系列的逻辑地址空间;


①near(或者默认缺省)与far类型的字节数据指针定义及编译结果(map文件)

11.jpg

②无参数无返回值型near(或者默认缺省)函数指针定义及编译结果(map文件)

12.jpg

③无参数无返回值型far函数指针定义及编译结果(map文件)

13.jpg

Tips:在定义函数时,若不使用near或者far明确类型,定义在未分页区中的函数默认为near型,其地址为2个字节,比如下图中的Cpu_Delay100US()延迟函数和PT4_LED1_Toggle() 板载LED翻转函数;而定义在分页地址的函数,比如Cpu_DelayMS()延迟函数,其地址则是24-bit逻辑地址--0x0E_8042,为3个字节,这是有编译器在链接时,自动决定的:

14.jpg

2.3 反汇编分析near和far对函数/函数指针的影响


通过CodeWarrior 5.x IDE将以上测试函数反汇编后,可以看到,未分页区中的near型函数--PT4_LED1_Toggle() 使用RTS指令返回,

15.jpg

而位于分页区的far型函数--Cpu_DelayMS()使用RTC指令返回:


16.jpg


Tips:在CodeWarrior 5.x IDE的应用工程中,在任意C源文件(*.c)中,单击右键选择Disassemble即可将其进行反汇编,查看其编译结果(*.o.lst文件)--汇编指令代码:

17.jpg

①如果将放置在未分页区的PT4_LED1_Toggle() 板载LED翻转函数定义为far类型,则其地址依然为2个字节,但是反汇编其函数返回指令则会变为RTC,相应地,其调用指令也会变成CALL:

10.jpg

②若将放置在分页地址中的Cpu_DelayMS()延迟函数定义为near类型,则其地址依然为3个字节,但是反汇编其函数返回指令则会变为RTS,相应地,其调用指令也会变成BSR:

11.jpg

此时,main()函数和Cpu_DelayMS()延迟函数都被编译链接到page0xE的P-Flash中,所以在main()函数使用短跳转指令BSR调用Cpu_DelayMS()延迟函数也是可以被正确寻址和执行的,但如果在其他P-Flash 分页地址中调用此类使用near定义的分页地址函数,则会因为忽略分页地址(PPAGE)的压栈和出栈而运行出错或者造成程序跑飞。


Tips:通过以上分析,可知,near和far会影响变量和函数的存储地址长度,进而影响其寻址能力;在函数和函数指针定义时,near和far还会影响其调用和返回指令;


3. S12(X) 系列MCU的CodeWarrior 5.x应用工程中使用函数指针调用函数的方法步骤和注意事项


为了在RAM中运行这两个延迟函数和LED翻转函数,先使用数据指针将位于未分页区的Cpu_Delay100US()延迟函数、PT4_LED1_Toggle() 板载LED翻转函数以及位于分页区的Cpu_DelayMS()延迟函数拷贝到RAM中,通过3个预定义的全局数组;

10.jpg

11.jpg


Tips:由于Cpu_DelayMS()延迟函数位于分页区中,需要使用24-bit的逻辑地址才能正常访问,所以将其拷贝到RAM中时,需要使用3个字节长度的far数据指针,同时还需要将其地址进行转换:


直接获取的Cpu_DelayMS()延迟函数地址为far类型函数指针(*Far_Ptr1),其值存储在Far_Ptr1中,指向地址为0x80420E,是一个非法地址:

23.jpg


转换之后的far类型数据指针(*Far_Ptr2)指向的地址才是Cpu_DelayMS()延迟函数正确地址--0x0E8042:

24.jpg


调试时,将函数作为变量拖到数据窗口1(Data1),可以观察到Cpu_DelayMS()延迟函数的真实地址为0x0E8042:

25.jpg


使用如下宏定义,可以完成S12(X)系列MCU的far型数据指针与far型函数指针的相互转换:

26.jpg

 

然后,在将各个数组地址转换为对应的函数指针进行调用:


①使用near类型函数指针调用

27.jpg

此时,编译结果使用子函数跳转指令JSR,直接跳转到RAM中执行相应的函数代码,由于位于未分页区的Cpu_Delay100US()延迟函数和PT4_LED1_Toggle() 板载LED翻转函数使用RTS返回指令, 故可以正确执行并返回,而位于分页区的Cpu_DelayMS()延迟函数编译结果使用的是RTC指令,其与JSR指令不匹配,故可以正确跳转在RAM中执行该延迟函数,但无法正确返回到main()函数----在执行RTC指令时,将额外出栈PPAGE寄存器,从而导致返回时地址错误而出现程序跑飞(runaway)。


②使用far类型函数指针调用

28.jpg

此时,调用指令为CALL,且会调用系统函数_FCALL()完成分页地址地址压栈保护处理:

29.jpg

 

而此时,这3个函数在RAM中的地址为16-bit本地地址,以上对分页地址的压栈处理将破坏正常的系统堆栈,从而导致程序跑飞:

30.jpg

总结


通过以上分析,S12(X)系列MCU的CodeWarrior 5.x应用工程中,使用函数指针调用函数时,一定要先通过map文件定位所要调用的函数的编译链接地址位于分页区还是未分页区:若在分页区,其函数返回指令为RTC,此时我们需要使用far类型函数指针,通过CALL指令来调用;而如果被调用函数位于未分页区,其编译结果中,其函数返回指令为RTS,则应该使用near类型函数指针,通过JSR/BSR指令来调用。


即始终要保证函数调用指令与函数返回指令匹配:


函数编译链接地址

函数指针类型

函数调用指令

函数返回指令

是否压栈和出栈PPAGE寄存器

16-bit 未分页地址(near/默认缺省)

near类型函数指针

JSR/BSR

RTS

24-bit 分页地址(far)

far类型函数指针

CALL

RTC


一般情况下,不要认为使用near和far改变未分页地址和分页地址中的函数/变量/指针的默认类型定义,否则将造成数据/指令寻址错误,从而出现数据访问错误/总线出现非法地址或者运行非法指令,从而造成程序跑飞等异常。


回到文章最开始的问题,由于我们开发S12(X)系列MCU的bootloader或者使用NVM SSD时,APP应用工程的Startup函数和重映射(remap/relocated)/拷贝到RAM的NVM SSD API都在64KB的本地地址范围内,所以需要使用near类型函数指针而不是far类型函数指针,否则将造成程序跑飞。


为了方便大家学习和理解本文所讲知识,我已将本文中使用的测试工程,分享到以下百度云盘,如果需要可以自行下载:


链接:https://pan.baidu.com/s/1HkrYxBW_NJhys3C08MdCqg

密码:tod0


Tips:关于函数指针的C语言基础知识,大家可以参考以下百度百科链接:

https://baike.baidu.com/item/%E5%87%BD%E6%95%B0%E6%8C%87%E9%92%88/2674905?fr=aladdin