RISC-V 32架构实践专题五(从零开始写操作系统-板级时钟初始化)

本文介绍了如何构建代码框架,包括初始化MCU的时钟源、分频系数和串口,以及通过修改链接脚本定位数据和bss段。还详细讲解了如何配置时钟,如HSE和PLL以达到更高的系统时钟频率。后续章节将涉及中断初始化和串口设备的初始化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        完成整个代码框架搭建后,后续将对此代码框架进行填充。

        首先需要初始化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后,再进行下一步操作。

        当系统时钟设置完毕后,后续就可以根据此时钟来初始化各个总线的时钟频率了;并且各总线下的外设也可以进行初始化设置了,所以下一章我们将进行串口设备的初始化。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值