从头开始写STM32F103C8T6驱动库(二)——编写系统初始化程序,配置时钟树
系列文章目录
Github开源地址
从头开始写STM32F103C8T6驱动库(一)——STM32CubeMX创建并调整工程结构
从头开始写STM32F103C8T6驱动库(二)——编写系统初始化程序,配置时钟树
从头开始写STM32F103C8T6驱动库(三)——编写GPIO驱动
从头开始写STM32F103C8T6驱动库(四)——编写延时函数,详解Systick
文章目录
- 系列文章目录
- 1.重写SystemInit函数
- 介绍STM32启动方式
- 介绍系统时钟树
- 编写配置时钟树代码
- 2.关闭JTAG-DP,启用SW-DP
- 3.测试
1.重写SystemInit函数
介绍STM32启动方式
首先提问大家一个问题就是,单片机的程序是从哪里开始执行的?
有的小伙伴可能会说,当然是从main函数开始执行了。
这对也不对,因为main函数执行用户程序起点而不是单片机执行的起点。而单片机的起点是哪里呢?
我们可以在STM32芯片手册中的这一段找到答案。
代码区始终从地址0x0000 0000开始(通过ICode和DCode总线访问),而数据区(SRAM)始终从地址0x2000 0000开始(通过系统总线访问)。
也就是说STM32单片机在上电那一刻是从地址0x0000 0000开始执行的再通过选择启动方式,我们默认是从主闪存存储器启动,也就是0x0800 0000启动
而这也正好对应了,下载器默认的下载地址。
而我们知道了单片机是从0x0000 0000开始执行,再通过选择启动方式为主闪存存储器启动的0x0800 0000之后呢,之后是执行什么?
之后就是执行编译器编译下载到单片机当中的程序了。
那就有同学会说是不是就是执行main函数了?
其实还不是执行main函数,而是执行一段汇编代码,这段代码也就是stm32的启动文件startup_stm32f103xb.s
我们来简单看一下这段汇编文件,首先就是对系统内存的堆栈内存分配。
Stack_Size EQU 0x400AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp; <h> Heap Configuration
; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>Heap_Size EQU 0x200AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limitPRESERVE8THUMB
分配给系统栈0x400也就是1KB
分配给系统堆0x200也就是512字节
; Vector Table Mapped to Address 0 at ResetAREA RESET, DATA, READONLYEXPORT __VectorsEXPORT __Vectors_EndEXPORT __Vectors_Size__Vectors DCD __initial_sp ; Top of StackDCD Reset_Handler ; Reset HandlerDCD NMI_Handler ; NMI HandlerDCD HardFault_Handler ; Hard Fault HandlerDCD MemManage_Handler ; MPU Fault HandlerDCD BusFault_Handler ; Bus Fault HandlerDCD UsageFault_Handler ; Usage Fault HandlerDCD 0 ; ReservedDCD 0 ; ReservedDCD 0 ; ReservedDCD 0 ; ReservedDCD SVC_Handler ; SVCall HandlerDCD DebugMon_Handler ; Debug Monitor HandlerDCD 0 ; ReservedDCD PendSV_Handler ; PendSV HandlerDCD SysTick_Handler ; SysTick Handler; External InterruptsDCD WWDG_IRQHandler ; Window WatchdogDCD PVD_IRQHandler ; PVD through EXTI Line detectDCD TAMPER_IRQHandler ; TamperDCD RTC_IRQHandler ; RTCDCD FLASH_IRQHandler ; FlashDCD RCC_IRQHandler ; RCCDCD EXTI0_IRQHandler ; EXTI Line 0DCD EXTI1_IRQHandler ; EXTI Line 1DCD EXTI2_IRQHandler ; EXTI Line 2DCD EXTI3_IRQHandler ; EXTI Line 3DCD EXTI4_IRQHandler ; EXTI Line 4DCD DMA1_Channel1_IRQHandler ; DMA1 Channel 1DCD DMA1_Channel2_IRQHandler ; DMA1 Channel 2DCD DMA1_Channel3_IRQHandler ; DMA1 Channel 3DCD DMA1_Channel4_IRQHandler ; DMA1 Channel 4DCD DMA1_Channel5_IRQHandler ; DMA1 Channel 5DCD DMA1_Channel6_IRQHandler ; DMA1 Channel 6DCD DMA1_Channel7_IRQHandler ; DMA1 Channel 7DCD ADC1_2_IRQHandler ; ADC1_2DCD USB_HP_CAN1_TX_IRQHandler ; USB High Priority or CAN1 TXDCD USB_LP_CAN1_RX0_IRQHandler ; USB Low Priority or CAN1 RX0DCD CAN1_RX1_IRQHandler ; CAN1 RX1DCD CAN1_SCE_IRQHandler ; CAN1 SCEDCD EXTI9_5_IRQHandler ; EXTI Line 9..5DCD TIM1_BRK_IRQHandler ; TIM1 BreakDCD TIM1_UP_IRQHandler ; TIM1 UpdateDCD TIM1_TRG_COM_IRQHandler ; TIM1 Trigger and CommutationDCD TIM1_CC_IRQHandler ; TIM1 Capture CompareDCD TIM2_IRQHandler ; TIM2DCD TIM3_IRQHandler ; TIM3DCD TIM4_IRQHandler ; TIM4DCD I2C1_EV_IRQHandler ; I2C1 EventDCD I2C1_ER_IRQHandler ; I2C1 ErrorDCD I2C2_EV_IRQHandler ; I2C2 EventDCD I2C2_ER_IRQHandler ; I2C2 ErrorDCD SPI1_IRQHandler ; SPI1DCD SPI2_IRQHandler ; SPI2DCD USART1_IRQHandler ; USART1DCD USART2_IRQHandler ; USART2DCD USART3_IRQHandler ; USART3DCD EXTI15_10_IRQHandler ; EXTI Line 15..10DCD RTC_Alarm_IRQHandler ; RTC Alarm through EXTI LineDCD USBWakeUp_IRQHandler ; USB Wakeup from suspend
__Vectors_End
建立中断向量表
不知道大家刚刚学习单片机中断的时候有没有疑惑过,为什么中断服务函数不用函数声明。
我当时就十分不解为什么中断服务函数没有函数声明呢?那发生中断时系统怎么知道去哪执行中断服务函数呢?
而启动文件当中这段内容就是建立中断向量表的过程,实际上就是告诉系统,当发生中断时要去哪里执行。
而函数声明的过程实际上也是告诉系统函数的地址,当调用函数时去哪里执行。(函数名实际上就是地址)所以建立中断向量表实际上也就是相当于函数声明了。
; Reset handler
Reset_Handler PROCEXPORT Reset_Handler [WEAK]IMPORT __mainIMPORT SystemInitLDR R0, =SystemInitBLX R0LDR R0, =__mainBX R0ENDP
而这一段大概就是声明了两个函数SystemInit和__main
这里面的__main就是大家所写的那个main函数,而SystemInit
就是在system_stm32f1xx.c中的一个函数,由此可见在执行用户的main函数之前还要执行一个SystemInit函数,而这个函数就是系统初始化函数,进行一些时钟配置等等。
介绍系统时钟树
/*** @brief Setup the microcontroller system* Initialize the Embedded Flash Interface, the PLL and update the * SystemCoreClock variable.* @note This function should be used only after reset.* @param None* @retval None*/
void SystemInit (void)
{
#if defined(STM32F100xE) || defined(STM32F101xE) || defined(STM32F101xG) || defined(STM32F103xE) || defined(STM32F103xG)#ifdef DATA_IN_ExtSRAMSystemInit_ExtMemCtl(); #endif /* DATA_IN_ExtSRAM */
#endif/* Configure the Vector Table location -------------------------------------*/
#if defined(USER_VECT_TAB_ADDRESS)SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#endif /* USER_VECT_TAB_ADDRESS */
}
大家打开system_stm32f1xx.c文件找到SystemInit函数实现,这是STM32CubeMX生成的,我们需要将此函数进行改写。
首先介绍一下STM32的时钟树
在STM32F103C8T6中有四个系统时钟源分别为
1. 高速外部时钟(HSE):以外部晶振作时钟源,晶振频率可取范围为4~16MHz,我们一般采用8MHz的晶振。
2. 高速内部时钟(HSI):由内部RC振荡器产生,频率为8MHz,但不稳定。
3. 低速外部时钟(LSE):以外部晶振作时钟源,主要提供给实时时钟模块,所以一般采用32.768KHz。
4. 低速内部时钟(LSI):由内部RC振荡器产生,也主要提供给实时时钟模块,频率大约为40KHz。
我们选择信号稳定的高速外部时钟(HSE),假设我们的晶振频率是8MHz。而STM32F103C8T6最大频率不是72MHz吗怎么用8MHz呢?
我们还需要将晶振输出的信号通过锁相环PLL进行9倍频输出给各个外设桥而8*9=72所以外设桥的工作频率实际上就是72MHz了
编写配置时钟树代码
了解完了时钟树而这些要怎么通过代码实现呢?
由RCC的CR寄存器复位值为0x0000 XX83,可知在没配置时钟树之前系统的时钟源使用的是高速内部时钟(HSI)
1. 首先先将外部高速时钟使能,也就是我们的外部晶振,将RCC寄存器的第16位HSEON位写入1来使能外部高速时钟。
RCC->CR |= RCC_CR_HSEON;
这里我们使用STM32官方提供的stm32f103xb.h文件,这里面包括了STM32F103C8T6所有的寄存器地址宏定义,可以非常方便的配置寄存器,也增加了代码可读性。
*(uint32_t*)0x40021000 |= 0x01<<16;
若编写这样的代码,虽然编译之后这和上面的代码是完全一样的但是这样的代码可以说是毫无可读性,所以我们采用官方提供的寄存器宏定义来配置寄存器,增加可读性,也方便后期维护。
2. 循环等待第17位硬件置1
while(!(RCC->CR&(RCC_CR_HSERDY)));
3. 设置HSE时钟作为PLL输入时钟。
RCC->CFGR |= RCC_CFGR_PLLSRC;
4. 设置PLL 9倍频输出
RCC->CFGR |= RCC_CFGR_PLLMULL9;
5. 配置AHB不分频72MHZ,APB2不分频72MHZ,APB1二分频36MHZ。
由于CFGR寄存器复位值为0x0000 0000所以我们保持PPRE2和HPRE位不动将PPRE1写入100即可
RCC->CFGR |= RCC_CFGR_PPRE1_2;
取自《STM32F10xxx闪存编程手册》
https://www.st.com/resource/en/programming_manual/cd00283419-stm32f10xxx-flash-memory-microcontrollers-stmicroelectronics.pdf
6. 设置系统时钟周期与闪存访问的比率 48 MHz < SYSCLK ≤ 72 MHz
FLASH->ACR |= 0x32;
7. 使能PLL并等待PLL是能成功
RCC->CR |= RCC_CR_PLLON;
while(!(RCC->CR&(RCC_CR_PLLRDY)));
8. 设置PLL为系统时钟并等待设置成功
RCC->CFGR |= RCC_CFGR_SW_1;
while((RCC->CFGR&0x0f)!=0x0a);
2.关闭JTAG-DP,启用SW-DP
到此系统时钟树已经配置完毕了*,但是我们还有一件事要做,就是关闭JTAG-DP,启用SW-DP
因为我常用SW接口进行调试下载,而JTAG接口我用不到而有占用着引脚,所以我们将JTAG关闭开启SW接口就好,如果你使用的是JATG则不用执行这一步
9. 首先我们开启辅助功能IO时钟,再配置AFIO_MAPR的SWJ_CFG位
关闭JTAG-DP,启用SW-DP
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;
AFIO->MAPR |= AFIO_MAPR_SWJ_CFG_1;
整体代码
/*** @brief Setup the microcontroller system* Initialize the Embedded Flash Interface, the PLL and update the * SystemCoreClock variable.* @note This function should be used only after reset.* @param None* @retval None*/
void SystemInit (void)
{//HSE振荡器开启RCC->CR |= RCC_CR_HSEON;//等待外部时钟就绪while(!(RCC->CR&(RCC_CR_HSERDY)));//选择外部高速时钟作为时钟源RCC->CFGR |= RCC_CFGR_PLLSRC;//PLL九倍频输出RCC->CFGR |= RCC_CFGR_PLLMULL9;///AHB不分频72MHZ;APB272MHZ不分频;APB1二分频36MHZRCC->CFGR |= RCC_CFGR_PPRE1_2;//设置系统时钟周期与闪存访问的比率 48 MHz < SYSCLK ≤ 72 MHzFLASH->ACR |= 0x32;//使能PLLRCC->CR |= RCC_CR_PLLON;//等待PLL使能成功while(!(RCC->CR&(RCC_CR_PLLRDY)));//选择PLL为系统时钟RCC->CFGR |= RCC_CFGR_SW_1;//等待系统时钟设置成功while((RCC->CFGR&0x0f)!=0x0a);//开启辅助功能IO时钟RCC->APB2ENR |= RCC_APB2ENR_AFIOEN;//关闭JTAG-DP,启用SW-DPAFIO->MAPR |= AFIO_MAPR_SWJ_CFG_1;
}
3.测试
我们编译调试一下如果可以正确执行到main函数那就是配置成功了。
从头开始写STM32F103C8T6驱动库(二)——编写系统初始化程序,配置时钟树相关推荐
- 从头开始写STM32F103C8T6驱动库(四)——编写延时函数,详解Systick
系列文章目录 Github开源地址 从头开始写STM32F103C8T6驱动库(一)--STM32CubeMX创建并调整工程结构 从头开始写STM32F103C8T6驱动库(二)--编写系统初始化程序 ...
- 从头开始写STM32F103C8T6驱动库(一)——STM32CubeMX创建并调整工程结构
系列文章目录 Github开源地址 从头开始写STM32F103C8T6驱动库(一)--STM32CubeMX创建并调整工程结构 从头开始写STM32F103C8T6驱动库(二)--编写系统初始化程序 ...
- 基于STM32F103C8T6的充电桩计费系统(程序+原理图+PCB+论文)
本设计: 基于STM32F103C8T6的充电桩计费系统(程序+原理图+PCB+论文) 原理图:Altium Designer 程序编译器:keil 5 编程语言:C语言 编号C0019 下载链接 [ ...
- C语言学习之编写一个C程序,运行时输人abc三个值,输出其中值最大者。
编写一个C程序,运行时输人abc三个值,输出其中值最大者. #include <stdio.h> void main(){int a,b,c,max;printf("请输入三个数 ...
- saltstack编写系统初始化状态
saltstack编写系统初始化状态 目录结构 Selinux 防火墙 chrony时间同步 kernel文件描述符 history历史记录 timeout 连接超时 yum源 基础命令安装 安装sa ...
- 编写一个C程序,运行时输出以下图形:
编写一个C程序,运行时输出以下图形: **** **** **** **** 代码示例: #include <stdio.h>int main() {for (int i = 0; ...
- 如何查看Linux系统下程序运行时使用的库?
Linux系统下程序运行会实时的用到相关动态库,某些场景下,比如需要裁剪不必要的动态库时,就需要查看哪些动态库被用到了. 以运行VLC为例. VLC开始运行后,首先查看vlc的PID,比如这次查到的V ...
- SD/TF卡驱动(二)--------SD卡程序初始化流程以及读写
说明: ①测试的SD卡为高容量卡,支持SD卡2.0协议,容量为16G ②采用GPIO模拟SPI时序的方式对SD卡进行驱动,很方便移植到没有硬件SPI或者SDIO的MCU,对于这类MCU,只需要将对应的 ...
- c语言将程序写为动态库,VS下生成C程序静态库(LIB)及动态库(DLL)的方法
一.前言 工作中有时候因为分工合作的原因需要让别人调用自己写的代码去完成某项功能,但是又不想让别人看到具体的实现过程,只是提供一个API形式的接口供别人调用:又或者是其他的一些原因,有必要学习静态库及 ...
最新文章
- 新手理解之NHibernate是什么?
- java培训就是害人的_[Java教程]粗心害死人啊,我的天。
- java读写excel文件
- python常用格式化_python的常用三种格式化方法
- spring cloud简介之最好参考
- 爬虫调用百度翻译API
- CSS标签选择器(二)
- Arduino驱动MAX30102踩坑记
- 使用liteide开发go遇到的问题
- 利用elasticsearch实现搜索引擎
- model.most_similar
- Deepest Station
- LED灯亮灭模拟小星星第一句
- 逾 200 家港企参与! GoGBA大湾区发展日(广州)圆满举行
- 使​​用Hashicorp Vault管理PKI并颁发证书
- Discuz手机模板:NVBING5-APP手机版
- 特斯拉自动驾驶功能更新:将上线红绿灯识别自动停车
- 工作几年了,还没成为“算法人上人”?
- vue子组件mounted不执行_vue 页面回退mounted函数不执行的解决方案
- JS函数制作倒数计时器