本帖最后由 洋葱圈 于 2013-10-21 11:17 编辑
注意:文中有一些字体高亮由于粘贴问题都丢失了,想看原版的可到官网原网页看,提问请在本帖提问。 (原网页地址:http://www.lpld.cn/?p=306)
如果大家运行过前面几节课提到的例程,就会从串口调试助手看到许多启动信息,这些信息包含了当前固件库的版本号、单片机的内核时钟等信息。但是回到例程的app代码内,我们并没有找到这些内容的输出代码,那么它们跑到哪里去了呢?答案就是启动代码,也就是工程在运行到main()函数之前的那些代码。虽然这些代码是固件库所包含的代码,开发者不用更改,但是本着深入理解固件库和Kinetis启动流程的学习目的,我们还是要在学习UART的同时,来看看这些代码到底是怎样运行的。 启动代码Kinetis单片机在运行到用户的main()函数之前,还要运行一些叫做启动代码的东西来进行寄存器、中断向量表、系统时钟初始化等操作。其中我们经常看到的启动信息的输出就是在系统时钟初始化之后进行的。 首先要找到这些启动代码在什么位置,在第2课中我们提到过,在IAR开发环境中的CPU组下包含的所有代码都是硬件相关,这里就是Kinetis启动代码的大本营了。其中startup_K60.s是单片机的汇编启动代码文件,system_MK60DZ10.c是K60的系统初始化c语言代码,可以统称为这些是Kinetis单片机的启动代码。 启动代码运行流程详解startup_K60.s 在单片机的寻址空间的前1024个字节中存放着256个异常向量(exception vectors),其中我们只需要知道几个重要的即可。第一个向量是栈(Stack)指针,物理地址0×00000000;第二个向量是复位地址(Reset_Handler)指针,物理地址0×00000004;往后的所有向量都是系统中断和外设中断的地址指针了。 这些指针地址都是在startup_K60.s汇编文件内定义的,代码如下:
1 | __vector_table | 2 | DCD sfe(CSTACK) ; Top of Stack | 3 | DCD Reset_Handler ; Reset Handler | 4 | DCD NMI_Handler ; NMI Handler | 5 | DCD HardFault_Handler ; Hard Fault Handler | 6 | DCD MemManage_Handler ; MPU Fault Handler | 7 | DCD BusFault_Handler ; Bus Fault Handler | 8 | DCD UsageFault_Handler ; Usage Fault Handler |
Line 1:__vector_table是地址标号,代表以下代码的开始地址,它的值是由开发环境的链接文件决定的。IAR的话就是*.icf文件内定义的,打开\lib\iar_config_files\目录下的LPLD_K60DN512_FLASH.icf文件,你可以发现__VECTOR_TABLE符号已经被定义为了0×00000000。 Line 2:DCD是一个汇编伪指令,就是给指定的数据分配存储单元。例如这行,指定的数据是sfe(CSTACK),给该数据分配的存储地址就是0×00000000,该地址是上面的__vector_table标号决定的。CSTACK是*.icf文件内定义的一个块,用于存放栈数据,sfe(CSTACK)代表取这个块的最后一个地址的下一个地址,为什么要取末尾的地址呢,因为栈数据是从一段空间的底部逐渐往顶部存入的,而读取是从栈顶部开始的。 Line 3:包含此行以后的代码就是系统中断指针地址和外设中断指针地址了。 当单片机上电或者复位后,单片机首先将CSTACK地址所存的地址指针读入到SP寄存器(堆栈指针寄存器),然后将Reset_Handler地址所存的地址指针读入到PC寄存器(程序计数寄存器),接下来单片机就会开始运行Reset_Handler地址开始处的程序代码了。代码如下:
1 | Reset_Handler | 2 | LDR R0, =SystemInit ;执行系统初始化函数SystemInit() | 3 | BLX R0 | 4 | LDR R0, =main ;执行用户主函数main() | 5 | BX R0 |
Line 1:指明下一行的代码从地址Reset_Handler开始。 Line 2:将SystemInit函数地址给R0寄存器。 Line 3:跳转到R0所存的地址执行,即执行SystemInit()系统初始化函数。此时代码就已经跳转到system_MK60DZ10.c文件中的SystemInit()函数取执行了。 Line 4~5:同上,当执行完系统初始化函数后,紧接着执行用户app的main()函数,即用户自己的工程代码。 可见,当单片机上电或复位后,单片机的启动顺序是先初始化SP、PC寄存器,接下来就开始运行PC寄存器指向地址的代码了。 system_MK60DZ10.c 当单片机启动后,首先运行的代码就是该文件内的SystemInit()系统初始化函数。在这个函数中,系统干了这样几件事:1)时能全部IO口时钟、2)禁用看门狗、3)拷贝中断向量表和相关数据代码到RAM中、4)初始化相关总线时钟、5)打印系统初始化信息。 1)前面的时能IO口时钟是使单片机的所有IO口全部处于激活状态,如果在不先使能的状况下对相关IO口进行操作,就会触发系统硬件错误中断(HardFault_Handler)。 2)禁用看门狗模块会方便开发调试,以防在总线初始化或者调试过程中出现系统复位状况。 3)这一步是初学者比较难懂的部分,为什么要拷贝中断向量表、相关变量和函数到RAM中呢?大家都知道,RAM的读写速度要比ROM的读写速度快很多,而我们的中断向量表是从单片机存储空间的物理地址开头0×00000000处开始存储的,这部分属于ROM空间,为了让单片机能更快的响应中断事件,把中断向量表拷贝到RAM中运行是目前通行的做法。还有一些函数和变量也是要拷贝到RAM中的,这些变量指的是已经初始化的全局变量,函数指的是由__RAMFUNC关键字定义的函数,这些概念我们会在后面的小节中讲到。这部分用到的代码如下:
1 | //将中断向量表、需在RAM中运行的函数等数据拷贝到RAM中 | 2 | common_relocate(); |
Line 2:该函数是在\lib\common\目录下的relocate.c代码内实现的,大家可以自行参考注释。 4)这一部分的代码除了初始化各部分时钟,还包括读取各部分时钟的频率到相关全局变量内,以便固件库的其他模块进行调用。代码如下:
1 | //初始化各部分时钟:系统内核主频、总线时钟、FlexBus时钟、Flash时钟 | 2 | LPLD_PLL_Setup(CORE_CLK_MHZ); | 3 |
| 4 | //更新内核主频 | 5 | SystemCoreClockUpdate(); |
Line 2:调用MCG模块的库函数LPLD_PLL_Setup()对时钟进行初始化,其中宏定义CORE_CLK_MHZ是定义在k60_card.h内的,用户可以自行修改数值以改变系统内核频率。 Line 5:获取实际初始化后的内核频率,将频率值赋值到全局变量SystemCoreClock中。 5)终于说到本节课的主题了——UART!这一步首先初始化输出调试信息需要用到的UART串口模块。代码如下所示:
1 | term_port_structure.UART_Uartx = TERM_PORT; | 2 | term_port_structure.UART_BaudRate = TERMINAL_BAUD; | 3 | LPLD_UART_Init(term_port_structure); |
Line 1:配置串口号,这里采用宏定义TERM_PORT,该定义在k60_card.h内定义为UART5,也就是说默认采用UART5模块输出调试信息。 Line 2:配置波特率,这里采用宏定义TERMINAL_BAUD,该定义在k60_card.h内定义为115200。 Line 3:条用初始化函数进行初始化。 初始化完UART5就该输出调试信息了,代码如下:
1 | #ifdef DEBUG_PRINT | 2 | printf("\r\n"); | 3 | //以下都是printf的函数调用,请看实际代码 | 4 | #endif |
Line 1:这里采用宏定义DEBUG_PRINT来控制系统启动时是否输出这些调试信息。 至此,OSKinetis固件库的启动代码就基本讲完了,一些对于初学者比较晦涩难懂的概念,我们会在后面解释,你也可以百度谷歌这些概念,看看大家的解释。例如一些汇编指令、堆栈的区别、icf链接文件的格式等等。
|