完成整个代码框架搭建后,后续将对此代码框架进行填充。
首先需要初始化MCU的时钟源以及分频系数,同时再初始化串口,完成输出打印。此时将不采用中断来实现上述功能,在后续的专题中再进行中断的初始化等操作。
一、代码重定位
首先需要修改链接脚本文件,获取.data在flash中的存储起始位置与.data重定位后在ram中的起始位置,以及.data在ram中的结束位置;同时也获取到.bss段在ram中的位置。
1.1 链接脚本修改
/* Section for text (init最开始的代码段) */
.init :
{
/* Entry point and code */
/* KEEP的作用是保留.init段中的所有内容,SORT_NONE的作用则是不对此段进行优化排序;使得.init段按照源代码中的顺序保持不变 */
KEEP(*(SORT_NONE(.init)))
} >FLASH AT>FLASH
/* Section for text (code) */
.text :
{
/* code */
*(.text)
} >FLASH AT>FLASH
/* 设置数据段在ram中存放的位置 */
.valign :
{
. = ALIGN(4);
PROVIDE(_vma_data_start = .);
} >RAM AT>FLASH
/* 设置数据段在flash中存放的位置 */
.lalign :
{
. = ALIGN(4);
PROVIDE(_lma_data_start = .);
} >FLASH AT>FLASH
/* Section for data */
.data :
{
/* Initialized data */
*(.data)
. = ALIGN(4);
/* 获取数据段在ram中的结束位置 */
PROVIDE(_vma_data_end = .);
} >RAM AT>FLASH
/* Section for uninitialized data */
.bss :
{
. = ALIGN(4);
/* 获取bss段在ram中的开始位置 */
PROVIDE(_sbss = .);
/* Uninitialized data */
*(.bss)
. = ALIGN(4);
/* 获取bss段在ram中的结束位置 */
PROVIDE(_ebss = .);
} >RAM AT>FLASH
新增三个段描述,用于init代码段的描述、说明.data段分别存放在flash和ram的哪个位置处以及添加.bss段的位置说明,以便提供给代码使用。
因为在链接脚本中,每个段所处的位置都使用 > 和 AT> 符号来分别描述其处于内存地址与加载地址的某处;所以每个段都具备两个不同区域的地址,并且每个段的地址默认都是以自动递增的方式排列在一起。AT>所指的区域就是flash的存储区域。
其中.bss段存放的是未初始化的全局变量与静态变量,所以其值应该全为0;并且因为.bss段的数据全为0,因此在编译生成镜像时,此段数据将不会保存到生成的镜像中,仅使用位置符号来表示此段数据,这样做的好处在于可以缩小镜像本身的大小。(所以衍生处理的问题就是在运行镜像时,需要代码手动清零.bss段)
1.2 新增.init代码段
随着代码的增加,在进行编译链接时会出现编译器将汇编代码链接到了靠后的位置处;而启动时,mcu需要读取首地址,此时将出现跑飞的现象。
所以需要增加init代码段,专用于存放boot代码,使得mcu上电运行后,第一句代码应该为boot汇编代码。如下所示:
# 用于声明接下的代码存储在.init段中,此段为自定义段
.section .init
.global _start
_start:
# 加载data段的ram区域与flash区域的位置
la t0, _vma_data_start
la t1, _lma_data_start
la t2, _vma_data_end
# 如果data段的大小>0,则继续执行,将falsh中的data段拷贝到ram中
bgeu t0, t2, 2f
1.3 数据段搬运以及bss段清零
完成链接脚本的修改后,就可以根据链接脚本提供的段信息来进行代码重定位了。首先需要做的是搬运.data段从flash到ram,随即在ram中将.bss段区域清零。
代码如下图所示:
_start:
# 加载data段的ram区域与flash区域的位置
la t0, _vma_data_start
la t1, _lma_data_start
la t2, _vma_data_end
# 如果data段的大小>0,则继续执行,将falsh中的data段拷贝到ram中
bgeu t0, t2, 2f
1:
# 将flash中的data段数据拷贝到ram中
lw t3, 0(t1)
sw t3, 0(t0)
addi t0, t0, 4
addi t1, t1, 4
# 当t0>=t2时,说明data段已经拷贝完成,可以跳出循环继续执行
bltu t0, t2, 1b
2:
# 对bss段进行清零
la t0, _sbss
la t1, _ebss
# 如果bss段的大小>0,则继续执行,将ram中bss段区域内存进行清零
bgeu t0, t1, 2f
1:
# 将bss段内存清零
sw x0, 0(t0)
addi t0, t0, 4
# 当t0>=t1时,说明bss段内存已经清零完毕,可以跳出循环继续执行
bltu t0, t1, 1b
2:
# 设置栈指针地址
la sp, _stack_end
# 跳转到c代码执行
j start_kernel
通过对ram与flash中的数据段与bss段定位,然后将其进行拷贝与清零操作。上述代码中的注释已经说明了汇编代码中是如何进行搬运与清零的,这里将不再进行赘述。
二、时钟初始化
首先通过查看原理图,获取MCU的外部输入时钟是8MHz;然后在以此为基础进行倍频与分屏的设置。
当系统上电复位时,系统采用的时钟源是MCU内部的8MHz时钟源,此时钟精度不够,所以在初始化时,一般会将系统时钟源设置为外部高速时钟源。时钟树框图如下所示:
2.1 默认SYSCLK配置
其中SYSCLK为最后得到的系统时钟,在默认的情况下,它的产生路径由下图所示:
时钟配置寄存器中,默认的时钟源为HSI,所以最后得到输出的SYSCLK频率为8MHz。
2.2 修改SYSCLK配置
接下来将时钟源修改为精度更高并且更稳定的外部HSE时钟,并且将SYSCLK时钟频率配置为96MHz。其时钟配置框图如下所示:
在PREDIV1SRC选择处选择原始HSE作为时钟输入,同时在PLLSRC处也选择HSE作为时钟输入,接着在PLLMULL处进行12倍频,得到PLLCLK的输出频率为8 * 12 = 96MHz,最后选择PLLCLK作为SYSCLK的时钟源,系统得到96MHz的时钟频率。
实现代码如下所示:
#include "platform.h"
#include "types.h"
#include "clock.h"
/*********************************************************************
* @fn clock_hse_on
*
* @brief 使能HSE时钟,使得HSE有输出时钟进入MCU.
*
* @param on - 0,关闭HSE时钟;1,打开HSE时钟.
*
* @return none
*/
void clock_hse_on(int on)
{
if(on)
{
// 开启HSE振荡器
//RCC->CTLR |= RCC_HSE_Bypass;
//RCC->CTLR &= ~RCC_HSE_Bypass;
RCC->CTLR |= RCC_HSE_ON;
}
else
{
// 关闭HSE振荡器
RCC->CTLR &= ~RCC_HSE_ON;
RCC->CTLR &= ~RCC_HSE_Bypass;
}
}
/*********************************************************************
* @fn clock_pll_on
*
* @brief pll使能开关函数.
*
* @param pll - 用于选择PLL、PLL2或PLL3.
* on - 0,关闭PLL;1,打开PLL.
*
* @return none
*/
void clock_pll_on(int pll, int on)
{
switch(pll)
{
case 0:
if(on)
RCC->CTLR |= 1 << PLL_ON_CTL;
else
RCC->CTLR &= ~(1 << PLL_ON_CTL);
break;
case 1:
if(on)
RCC->CTLR |= 1 << PLL2_ON_CTL;
else
RCC->CTLR &= ~(1 << PLL2_ON_CTL);
break;
case 2:
if(on)
RCC->CTLR |= 1 << PLL3_ON_CTL;
else
RCC->CTLR &= ~(1 << PLL3_ON_CTL);
break;
default :
if(on)
RCC->CTLR |= 1 << PLL_ON_CTL;
else
RCC->CTLR &= ~(1 << PLL_ON_CTL);
return;
}
}
/*********************************************************************
* @fn clock_pll_cfg
*
* @brief 设置PLLSRC的时钟源以及pll的倍频系数.
*
* @param src - 时钟源选择:0,HSI;1,HSE.
* mul - 倍频系数,倍频的范围为3~18(且17除外).
*
* @return none
*/
void clock_pll_cfg(int src, int mul)
{
RCC->CFGR0 &= ~0x3f0000;
if(src)
{
RCC->CFGR0 |= mul | (1 << PLL_SRC_CTL);
}
else
{
RCC->CFGR0 |= mul;
RCC->CFGR0 &= ~(1 << PLL_SRC_CTL);
}
}
/*********************************************************************
* @fn clock_prediv1_cfg
*
* @brief 设置prediv1的时钟源以及prediv1的分频系数.
*
* @param src - 时钟源选择:0,HSE;1,PLL2.
* div - 分频系数,分频的范围为1~16.
*
* @return none
*/
void clock_prediv1_cfg(int src, int div)
{
RCC->CFGR2 &= ~((1 << PREDIV1_SRC_CTL) | 0x0f);
if(src)
{
RCC->CFGR2 |= div | (1 << PREDIV1_SRC_CTL);
}
else
{
RCC->CFGR2 |= div;
RCC->CFGR2 &= ~(1 << PREDIV1_SRC_CTL);
}
}
/*********************************************************************
* @fn clock_src_cfg
*
* @brief 设置最后sysclk的时钟源选择.
*
* @param src - 时钟源选择:0,HSI;1,HSI;2,PLL.
*
* @return none
*/
void clock_src_cfg(int src)
{
RCC->CFGR0 |= (src & 0x03);
}
/*********************************************************************
* @fn clock_hse_96Mhz
*
* @brief 设置sysclk时钟源为PLL,且PLL由HSE通过倍频后得到.
*
* @param none.
*
* @return none
*/
void clock_hse_96Mhz(void)
{
uint32_t tmp = 0;
// 关闭hse振荡器
clock_hse_on(0);
// 关闭PLL,为了设置PLL的分频系数
clock_pll_on(0, 0);
// 开启hse振荡器
clock_hse_on(1);
// 等待hse时钟稳定
do
{
tmp = (RCC->CTLR >> RCC_HSE_RDY) & 0x01;
} while (!tmp);
/* HCLK = SYSCLK */
RCC->CFGR0 |= (uint32_t)0x00;
/* PCLK2 = HCLK */
RCC->CFGR0 |= (uint32_t)0x00;
/* PCLK1 = HCLK / 2 */
RCC->CFGR0 |= (uint32_t)0x400;
// 选择HSE作为PREDIV1的时钟源,并且将PREDIV1进行1分频
clock_prediv1_cfg(0, RCC_PREDIV1_Div1);
// 选择PREDIV1作为PLLSRC的时钟源,并且将PLL进行12倍频
clock_pll_cfg(1, RCC_PLLMul_12_EXTEN);
// 开启PLL
clock_pll_on(0, 1);
// 等待PLL稳定
do
{
tmp = (RCC->CTLR >> PLL_ON_RDY) & 0x01;
} while (!tmp);
// PLL锁定成功后,切换sysclk时钟源为PLL
clock_src_cfg(RCC_PLL_CFG);
// 等待切换时钟源成功
do
{
tmp = (RCC->CFGR0 >> 2) & 0x03;
} while (tmp != 2);
}
上述代码的作用就是根据框图中的红色路线进行了时钟设置,特别需要注意的是每当使能一个时钟源或者PLL时,最好等待其稳定状态ready后,再进行下一步操作。
当系统时钟设置完毕后,后续就可以根据此时钟来初始化各个总线的时钟频率了;并且各总线下的外设也可以进行初始化设置了,所以下一章我们将进行串口设备的初始化。