智能车制作

标题: 【跟我学OSKinetis】第4课-从启动代码开始看!UART! [打印本页]

作者: 洋葱圈    时间: 2013-10-21 11:11
标题: 【跟我学OSKinetis】第4课-从启动代码开始看!UART!
本帖最后由 洋葱圈 于 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链接文件的格式等等。



作者: 洋葱圈    时间: 2013-10-21 11:12
本帖最后由 洋葱圈 于 2013-10-21 11:15 编辑

UART模块讲解

UART模块是通用异步收发器的英文缩写(Universal Asynchronous Receiver/Transmitter),这是个正式的名字,还有个小名儿叫串口。串口的功能我们就不多说了,教科书、网络上的介绍多的是。它是一个非常简单的模块,上手极快,但是对于Kinetis的串口来说,它又有许多高级的功能、比如DMA传输、FIFO等等。本节课我们只讲如何用OSKinetis固件库来使用UART模块的简单收发功能,包括中断的使用。其实上面的启动代码部分已经简单涉及到了,聪明的你会发现它的初始化方式是和GPIO是一样的。

如果大家想要详细了解UART模块的寄存器及其他高级功能,同样可以阅读K60P144M100SF2RM.pdf的“Chapter 51 Universal Asynchronous Receiver/Transmitter (UART)”一节。

UART固件库编程思路

查询模式:

中断模式:

UART例程详解轮询方式发送接收-LPLD_SerialComm

首先看非中断发送接收的使用方法,打开例程“04-(UART)LPLD_SerialComm”,用户程序代码main()内首先调用了自定义函数uart_init()对UART进行初始化,看下其实现代码:


01
UART_InitTypeDef uart5_init_struct;
02
void uart_init(void)
03
{
04
uart5_init_struct.UART_Uartx = UART5; //使用UART5
05
uart5_init_struct.UART_BaudRate = 9600; //设置波特率9600
06
uart5_init_struct.UART_RxPin = PTE9;  //接收引脚为PTE9
07
uart5_init_struct.UART_TxPin = PTE8;  //发送引脚为PTE8
08
//初始化UART
09
LPLD_UART_Init(uart5_init_struct);
10
}


Line 1:首先在函数外定义一个全局UART初始化结构体变量。思路和GPIO初始化一样。

Line 4:配置结构体的UART_Uartx成员变量,选择UART5串口模块。K60一共有6个串口,分别是UART0~UART5。

Line 5:配置结构体的UART_BaudRate成员变量,设置波特率为9600。

Line 6~7:分别配置UART的接收和发送引脚,这里的参数是由K60的复用引脚来决定的,每个串口模块有不同的发送接收复用引脚,你可以随意配置,但是如果配置了不存在的复用引脚,会初始化失败。如果你不配置这个成员变量,固件库会初始化默认的引脚。具体参数请看在线文档中对UART_InitTypeDef结构体的描述(点击查看

Line 8:最后调用UART初始化函数进行初始化,参数就是刚刚配置的结构体变量。

初始化结束完毕后,例程中调用了发送函数发送了一个字符串:


1
LPLD_UART_PutCharArr(UART5, "Input something:\r\n", 20);


Line 1:该函数的第一个参数是选择串口号,第二个参数是字符串,即字节型数组,第三个参数是字节数组的长度。此时K60会从他的UART5的PTE8引脚输出这些信息,使用RUSH Kinetis开发板的童鞋可以直接将USB线插到开发板母板的USB接口上,通过串口调试助手查看信息。

接下来看while循环内的代码:


1
recv = LPLD_UART_GetChar(UART5);
2
LPLD_UART_PutChar(UART5, recv);


Line 1:调用串口接收字节函数,接收串口5的数据,并赋值给变量recv。这个函数的特点是只有有接收到数据后,才会返回,否则一直停留不跳出该函数

Line 2:调用串口发送字节函数,从串口5发送字节recv。这个函数和while循环上面的那个不一样,仅仅是发送一个字节变量。

最终的调试结果是,当你每在串口调试助手发送一个字节,就会在接收界面收到一个同样的数据,结果如下图所示。

[attach]51263[/attach]


这次同样是插到RUSH Kinetis开发板的串口转USB接口上,为什么没有输出启动信息呢波特率没有设置为115200?不对,我们初始化时用的是9600。秘密就在k60_card.h代码内,原来在这个工程的配置中,我们将PRINT_ON_OFF宏定义定义为了0,这样就不会在启动时输出调试信息了!

中断方式接收-LPLD_SerialInterrupt

接下来这个例程采用中断方式接收串口数据,打开例程“05-(UARTint)LPLD_SerialInterrupt”,首先看下其uart_init()的实现代码:


01
uart5_init_struct.UART_Uartx = UART5; //使用UART5
02
uart5_init_struct.UART_BaudRate = 9600; //设置波特率9600
03
uart5_init_struct.UART_RxPin = PTE9;  //接收引脚为PTE9
04
uart5_init_struct.UART_TxPin = PTE8;  //发送引脚为PTE8
05
uart5_init_struct.UART_RxIntEnable = TRUE;  //使能接收中断
06
uart5_init_struct.UART_RxIsr = uart_isr;  //设置接收中断函数
07
//初始化UART
08
LPLD_UART_Init(uart5_init_struct);
09
//使能UART中断
10
LPLD_UART_EnableIrq(uart5_init_struct);


同样的地方就不重复解释了,看看不一样的地方。

Line 5:配置UART_RxIntEnable成员变量为真(TRUE),意思是使能UART模块的接收中断功能。

Line 6:设置中断接收函数为uart_isr,大家要记住,使能一个模块的中断,就必须配置相关的中断函数,否则初始化会失败或者导致硬件错误。

Line 10:调用UART中断使能函数,使能UART5的不可屏蔽中断。

接下来直接看接收中断函数的代码:


1
void uart_isr(void)
2
{
3
int8 recv;
4
recv = LPLD_UART_GetChar(UART5);
5
LPLD_UART_PutChar(UART5, recv);
6
}


Line 3:首先定义一个8位的字节变量recv用于接收,根据开发环境的配置不同,有的地方问题字节变量时有符号型的,因此定义为int8,有的认为是无符号型的,因此定义为uint8,定义为哪种变量都行,只要是8位的。

Line 4:还是调用接收字节函数,接收一个8位字节,这里函数就不会一直停留等待了,因为是中断信号告诉我们可以接收字节了,函数内部只要直接获取数据就可以了。

Line 5:输出这个字节。

程序最终的调试结果和上一个例程是一样的,但是如果你在上面代码Line 4的位置打一个断点,就会发现,当你通过串口调试助手发送字节的时候,程序会进入这个断点。证明了串口5成功进入了接收中断。大家也可以照猫画虎,写一个发送中断的程序,发送中断是在用户发送完成一个字节后才触发中断

UART拾遗补缺关于printf()函数

在所有例程中,我们通过串口输出调试信息都是用printf()这个函数,如果大家写过C/C++代码,那么肯定对他的用法已经很熟悉了(不知道这个函数的用法请找度娘)。但关键是,printf()是从哪个串口输出信息,波特率又是多少呢?没错,在前面的system_MK60DZ10.c一小节已经提到了,调试信息输出的串口初始化是在SystemInit()函数内进行的。你要更改k60_card.h代码内的TERM_PORT和TERMINAL_BAUD宏定义来更改串口号和波特率即可。那么输出引脚如何设置呢,如果你不更改SystemInit()内的代码,那么默认引脚就是在线文档中些的那些。如果你想更改其他引脚,那么按照正常的方法就可以更改,例程中已经介绍过了。

关于椠渀开挀栀愀爀()、out_char()等函数

可能有些人对第一个例程“01-LPLD_HelloWorld”中的这两个函数还抱有疑问。当时使用它们的时候也没有进行串口初始化操作啊,怎么就能输入输出字符了呢?其实这两个函数也是调用了TERM_PORT和TERMINAL_BAUD宏定义来使用UART库函数发送和接收字符,你只要查看这两个函数的定义即可发现。嬠/p]串口调试常见问题

与其说是常见问题,不如说是新手解惑。


作者: Neozoic    时间: 2013-10-21 12:15
支持支持,太棒了!
作者: kanwoe    时间: 2013-10-21 13:20

作者: weltry    时间: 2014-3-28 21:24
大哥  V2的库可以直接拷贝relocate.c使用吗?
作者: 洋葱圈    时间: 2014-3-29 22:50
weltry 发表于 2014-3-28 21:24
大哥  V2的库可以直接拷贝relocate.c使用吗?

这个是V3库代码的教程,V2库你可以自己试试,不试怎么知道。

作者: prothesman    时间: 2014-4-8 16:25
站位,留存,常来看看,下载学习,储备知识,:P:P:P
作者: wmslecz    时间: 2014-4-10 22:43
果段来 占位...
作者: m__dd    时间: 2014-4-11 23:22
本帖最后由 m__dd 于 2014-4-11 23:24 编辑

关于K60,栈顶(MSP)为何是0x2000FFF8,而不是0x20010000
/*将SP指针指向RAM的最顶端*/
define exported symbol __BOOT_STACK_ADDRESS = __region_RAM2_end__ - 8;        //0x2000FFF8;

是不是因为:
  1、__BOOT_STACK_ADDRESS定义的变量本身占4字节,所以是0x2000FFFC
  2、考虑到8字节对齐,而不能是0x2000FFFC,而是0x2000FFF8

不好意思,已经给您留言,不要烦哦
作者: 洋葱圈    时间: 2014-4-12 20:18
m__dd 发表于 2014-4-11 23:22
关于K60,栈顶(MSP)为何是0x2000FFF8,而不是0x20010000
/*将SP指针指向RAM的最顶端*/
define exported ...

__BOOT_STACK_ADDRESS不是一个变量,而是一个符号,仅仅代表一个地址。
0x20010000是RAM的最后一个字节地址的下一个地址,你说的没错,堆栈要8字节对齐,所以要-8,就是0x2000FFF8。
其实在V3库中的icf文件已经不用__BOOT_STACK_ADDRESS这个符号了,而是直接用sfe(CSTACK)找到栈顶。

作者: 夏奇啦    时间: 2014-4-13 12:36
正在用,很不错的底层库
作者: 机器人没名字    时间: 2014-4-13 12:55
LPLD的支持支持
作者: m__dd    时间: 2014-4-13 22:11
洋葱圈 发表于 2014-4-12 20:18
__BOOT_STACK_ADDRESS不是一个变量,而是一个符号,仅仅代表一个地址。
0x20010000是RAM的最后一个字节地 ...

哦,对,0X20010000只是最后一个字节的下一地址,已超出RAM区。

我是在__INITIAL_SP地址处定义了一个数组,然后将数组名强制转换成函数指针。(KEIL5)如下:
__attribute__((section(".__INITIAL_SP"))) volatile  static  INT32U  __initial_sp[1] __attribute__((used));

谢谢您的回答!

作者: 1174544639    时间: 2014-4-30 12:03
拉普兰德大锅,来顶你
作者: zhangxin1992109    时间: 2014-8-4 19:17
我想问一下这个SystemCoreClockUpdate();函数是不是可有可无啊????我不理解这个。。删了好像也没影响
作者: 阿达的礼物    时间: 2016-12-18 14:19
洋葱圈 发表于 2013-10-21 11:12
UART模块讲解UART模块是通用异步收发器的英文缩写(Universal Asynchronous Receiver/Transmitter),这是 ...

优秀


作者: zmhzc111234    时间: 2017-1-14 15:54
太棒了,学习到了好多





欢迎光临 智能车制作 (http://dns.znczz.com/) Powered by Discuz! X3.2