#include "./SYSTEM/sys/sys.h"
void sys_nvic_set_vector_table(uint32_t baseaddr, uint32_t offset) // (设置中断向量表)
{
// Cortex-M处理器的VTOR寄存器只关心地址的0~8位,而9~31位(高23位)的值都被忽略
// 按位与运算保留偏移地址的0~8位二进制位,
// 这样就能够将中断向量表起始位置的地址偏移设置到 VTOR 寄存器中,从而完成中断向量表的偏移操作。
SCB->VTOR = baseaddr | (offset & (uint32_t)0xFFFFFE00);
// 实现方式:将指定的中断向量表的偏移地址 offset 和最低的9位(即偏移地址的0~8位)按位与运算,
// 再同变量 baseaddr 按位或操作,然后将结果赋值给VTOR寄存器。
}
static void sys_nvic_priority_group_config(uint8_t group) // (--局部函数-- 配置NVIC中断优先级分组,)
{
/*--补充知识--
NVIC_PRIORITYGROUP_4是一种基于中断控制器(NVIC)实现优先级分组的方案。
它是基于Cortex-M处理器中的硬件优先级的分组方法。
在NVIC_PRIORITYGROUP_4中,有4个优先级组,从0到3(优先级从高到低)。
在每个优先级组中,有16个优先级级别。其中,优先级0为最高级别,15为最低级别。
在同一个优先级组内,可以为每个中断设置不同的子优先级
抢占优先级和响应优先级都被赋予16个级别。抢占优先级用于中断之间的抢占,响应优先级用于中断服务函数内部的优先级控制。
中断向量表---------->参考手册p130页表54
*/
uint32_t temp, temp1;
temp1 = (~group) & 0x07; // 对变量 group 进行按位求反(~)再和二进制数 0x07 的按位与操作
temp1 <<= 8; // 将 temp1 左移 8 位。目的是将优先级分组的值放到 AIRCR 的第 15~13 位中
temp = SCB->AIRCR; // 读取 System Control Block(SCB)寄存器中的AIRCR(应用程序中断和复位控制寄存器)的值给temp
temp &= 0X0000F8FF; // 将 temp 与 0x0000F8FF 按位与操作,目的是将 AIRCR 的(中断优先级组)和(子优先级)置 0,以便重新配置
temp |= 0X05FA0000; // 将 temp 的值与 0x05FA0000 按位或操作,该数的意思是使用 NVIC_PRIORITYGROUP_4 方案进行优先级编码。在该方案中,优先级分组是 0b10
temp |= temp1; // 将 temp1 与 temp 按位或操作,将分组值放置到对应的位中。最终将前三位设置成为优先级分组值。
SCB->AIRCR = temp; // 将 temp 写入 (SCB)寄存器中的(AIRCR),从而完成配置 NVCI 中断优先级分组的全过程。
}
void sys_nvic_init(uint8_t pprio, uint8_t sprio, uint8_t ch, uint8_t group) // (NVIC中断控制器的初始化)
{
sys_nvic_priority_group_config(group); // 调用sys_nvic_priority_group_config函数,参数为group,从而完成配置 NVCI 中断优先级分组
uint32_t temp;
temp = pprio << (4 - group); // 将pprio的值左移(4-group)位,将结果存入temp中。目的是计算出主优先级
temp |= sprio & (0x0f >> group); // 将sprio的低4位存入temp中,使用0x0f右移(group)位再按位与操作,保留temp中的低4位。目的是计算出从优先级
temp &= 0xf; // 将temp与0xf按位与(&)操作,以保证temp中的值不大于15。
NVIC->ISER[ch / 32] |= 1 << (ch % 32); // 将NVIC->ISER[ch/32]寄存器中的针对相应中断线的位置1,使能相应的中断线;
NVIC->IP[ch] |= temp << 4; // 将计算出的优先级temp左移4位后写入NVIC->IP[ch]寄存器,以指定相应中断的优先级。
}
void sys_nvic_ex_config(GPIO_TypeDef *p_gpiox, uint16_t pinx, uint8_t tmode) //(NVIC外部中断配置)
{
uint8_t offset;
uint32_t gpio_num = 0;
uint32_t pinpos = 0, curpin = 0, pos = 0;
/*参数说明
offset用于存储引脚号在EXTICR寄存器中的位偏移量。
gpio_num存储对应的GPIO编号,
pinpos和curpin分别用于遍历和检查引脚号中的每一位,
pos用于生成位掩码,
*/
gpio_num = ((uint32_t)p_gpiox - (uint32_t)GPIOA) / 0X400; // 计算得到GPIO端口GPIOA在AHB总线中的地址与输入参数p_gpiox的地址之差,再除以0x400,即可得到对应的GPIO编号。
RCC->APB2ENR |= 1 << 0; // 使能AFIO时钟,在RCC->APB2ENR寄存器中将AFIO时钟位置1,以便访问AFIO寄存器
for (pinpos = 0; pinpos < 16; pinpos++) // 遍历引脚号中的每一位
{
pos = 1 << pinpos; // 使用pos生成位掩码,
curpin = pinx & pos; // 使用按位与(&)操作取出引脚号的当前位curpin
if (curpin == pos) // 如果当前位为1,则说明该引脚需要配置EXTI功能。
{
offset = (pinpos % 4) * 4; // 计算它在EXTICR寄存器中对应的位偏移量offset,
AFIO->EXTICR[pinpos / 4] &= ~(0x000F << offset); // 然后将AFIO->EXTICR[pinpos / 4]寄存器中指定位置的值清零
AFIO->EXTICR[pinpos / 4] |= gpio_num << offset; // 再将gpio_num按照偏移量offset的位偏移值存入该位置
EXTI->IMR |= 1 << pinpos; // 将EXTI->IMR寄存器中对应引脚的位置1,使能该引脚对应的EXTI中断(即使能line BITx的中断)。
/*另外,根据传入的触发方式tmode来配置EXTI对应线的触发方式*/
if (tmode & 0x01)
EXTI->FTSR |= 1 << pinpos; // 如果tmode的最低位为1,则使能下降沿触发;
if (tmode & 0x02)
EXTI->RTSR |= 1 << pinpos; // 如果tmode的次低位为1,则使能上升沿触发。
}
}
}
void sys_gpio_remap_set(uint8_t pos, uint8_t bit, uint8_t val) //(GPIO引脚重映射)
{
uint32_t temp = 0; // temp用于保存生成的位掩码,i用于遍历需要配置的位。
uint8_t i = 0;
RCC->APB2ENR |= 1 << 0; // 使能AFIO时钟,temp用于保存生成的位掩码,i用于遍历需要配置的位。
for (i = 0; i < bit; i++) // 根据传入的bit参数计算出需要配置的位掩码temp
{
temp <<= 1; // 参数bit表示有多少个位需要配置,因此需要生成一个低bit位都为1的掩码,
temp += 1; // 通过使用temp按位左移及按位或运算进行生成。
}
AFIO->MAPR &= ~(temp << pos); // 在AFIO->MAPR寄存器中将需要配置的位置清零,这可以通过temp左移pos位后取反(~)再与原值取按位与(&)操作实现
AFIO->MAPR |= (uint32_t)val << pos; // 最后,在AFIO->MAPR寄存器中将需要配置的位设置为新值。可以将新值左移pos位后再按位或(|)到原值中完成设置。
}
//(GPIO引脚设置)
// 结构体定义:
// typedef struct
// {
// volatile uint32_t MODER; // GPIO端口模式寄存器
// volatile uint16_t OTYPER; // GPIO端口输出类型寄存器
// uint16_t RESERVED0; // 保留,值为0x02
// volatile uint32_t OSPEEDR; // GPIO端口输出速度寄存器
// volatile uint32_t PUPDR; // GPIO端口上拉/下拉寄存器
// volatile uint16_t IDR; // GPIO端口输入数据寄存器
// uint16_t RESERVED1; // 保留,值为0x0A
// volatile uint16_t ODR; // GPIO端口输出数据寄存器
// uint16_t RESERVED2; // 保留,值为0x0E
// volatile uint16_t BSRR; // GPIO端口位设置/重置寄存器
// uint16_t RESERVED3; // 保留,值为0x12
// volatile uint16_t LCKR; // GPIO端口配置锁寄存器
// uint16_t RESERVED4; // 保留,值为0x16
// volatile uint16_t AFR[2]; // GPIO端口替代功能寄存器
// uint16_t RESERVED5[10]; // 保留,值为0x1C-0x28
// volatile uint32_t BRR; // GPIO位重置寄存器
// } GPIO_TypeDef;
void sys_gpio_set(GPIO_TypeDef *p_gpiox, uint16_t pinx, uint32_t mode, uint32_t otype, uint32_t ospeed, uint32_t pupd)
{
/*参数介绍
p_gpiox指向GPIOx(x为A、B、C等)的寄存器地址,
pinx代表要配置的引脚(可能是多个,用二进制位表示)
,mode表示引脚工作模式(输入、输出、复用等),
otype表示输出类型(开漏或推挽),
ospeed表示输出速率,
pupd表示上下拉模式。*/
uint32_t pinpos = 0, pos = 0, curpin = 0;
// pinpos用于循环检查GPIO口的每个引脚,pos用于检查引脚是否需要配置,curpin是检查后的结果。
uint32_t config = 0;
// config变量,初始值为0,用于保存某一个IO的设置
for (pinpos = 0; pinpos < 16; pinpos++) // 使用for循环,依次处理pinpos为0~15的引脚。
{
pos = 1 << pinpos; // 将1左移pinpos位,赋值给pos,即pos用于检查对应引脚是否要配置
curpin = pinx & pos; // 引脚配置信息存储在pinx中,使用位运算&得到curpin,即该引脚是否需要配置。
if (curpin == pos) // 如果curpin等于pos,则表示该引脚需要配置。
{
config = 0; // 如果需要配置,首先将config清零,表示该引脚的配置为默认模拟输入模式。
if ((mode == 0X01) || (mode == 0X02)) // 如果mode为普通输出模式或复用功能模式
{
config = ospeed & 0X03; // 将速度参数ospeed低2位和config的低2位进行或操作,得到bit0/1,表示MODE[1:0]的设置。
config |= (otype & 0X01) << 2; // 将otype低位和config的第2位进行或操作,得到CNF[0]的设置。
config |= (mode - 1) << 3; // 将mode减去1的值左移3位,再和config的第3位进行或操作,得到CNF[1]的设置。
}
else if (mode == 0) // 如果mode等于0则为普通输入模式。
{
if (pupd == 0) // 如果不带上下拉,即浮空输入模式。
{
config = 1 << 2; // 将config第2 3位设置为0 1,表示浮空输入模式。
}
else // 否则(即带上下拉输入模式)。
{
config = 1 << 3; // 将config的第2 3位设置为1 0,表示为上下拉输入模式
/*下面为根据pupd参数的低位,对GPIO口的引脚进行上拉或者下拉。*/
p_gpiox->ODR &= ~(1 << pinpos); // 将ODR的对应引脚位清零,
p_gpiox->ODR |= (pupd & 0X01) << pinpos; // 再根据pupd的低位,左移对应引脚位,进行或操作,设置为上拉或者下拉。
}
}
/*下面根据pinpos来判断引脚所在的CRH或者CRL寄存器。*/
if (pinpos <= 7) // 如果当前处理的引脚pinpos的位置在低8位,则对应寄存器是CRL寄存器。
{
p_gpiox->CRL &= ~(0X0F << (pinpos * 4)); // 首先把pinpos左移2位(相当于乘以4)得到(低8位)偏移量,然后将0X0F左移偏移量位,
// 得到要写入寄存器的掩码,再使用位反码来清除掩码指定的位
p_gpiox->CRL |= config << (pinpos * 4); // 将config左移偏移量位,并将结果写入CRL寄存器。
}
else // 如果当前处理的引脚pinpos的位置在高8位,则对应寄存器是CRH寄存器。
{
p_gpiox->CRH &= ~(0X0F << ((pinpos - 8) * 4)); // 同上,只不过偏移量应减去8(高8位)再进行计算
p_gpiox->CRH |= config << ((pinpos - 8) * 4); // 将config左移偏移量位,并将结果写入CRH寄存器。
}
}
}
}
void sys_gpio_pin_set(GPIO_TypeDef *p_gpiox, uint16_t pinx, uint8_t status) //(GPIO单个引脚输出状态)
{
/*参数介绍
p_gpiox指向GPIOx(x为A、B、C等)的寄存器地址,pinx代表要控制的引脚,status则表示引脚状态(0或1)。
*/
if (status & 0X01) // 判断status的最低比特位(即0或1),确定要执行的引脚操作
{
// 此时将pinx的值与掩码0xFFFF(即所有比特位均为1)进行按位或运算,将BSRR寄存器的低16位设置为1,即将对应引脚的电平设置为高电平。
p_gpiox->BSRR |= pinx;
}
else
{
// 此时将pinx的值与掩码0xFFFF(即所有比特位均为1)进行按位或运算,将BSRR寄存器的低16位设置为1,即将对应引脚的电平设置为高电平。
p_gpiox->BSRR |= (uint32_t)pinx << 16;
}
}
uint8_t sys_gpio_pin_get(GPIO_TypeDef *p_gpiox, uint16_t pinx) // (读取GPIO单个引脚状态)
{
/*参数介绍
p_gpiox指向GPIOx(x为A、B、C等)的寄存器地址
pinx代表要获取状态的引脚
*/
if (p_gpiox->IDR & pinx) // if判断语句判断IDR的某一比特位(即引脚电平数据)是否为1,确定该引脚的实际电平状态
{
return 1; // 意味着该引脚电平为高电平,返回1
}
else
{
return 0; // 如果该比特位为0,意味着该引脚电平为低电平,返回0。
}
}
void sys_wfi_set(void) // (进入低功耗状态)
{
__ASM volatile("wfi"); // 代码使用了汇编指令 wfi,该指令会将处理器置于 WFI 模式,并等待中断信号的触发。
}
void sys_intx_disable(void) // (关闭所有中断)
{
__ASM volatile("cpsid i"); // 代码使用了汇编指令 cpsid i,该指令会将处理器的中断使能位清零,禁止所有中断的触发。
}
void sys_intx_enable(void) // (开启所有中断)
{
__ASM volatile("cpsie i"); // 代码使用了汇编指令 cpsie i,该指令会将处理器的中断使能位置位,允许中断请求的触发。
}
void sys_msr_msp(uint32_t addr) // (设置栈顶地址)
{
// (Cortex Microcontroller Software Interface Standard)
__set_MSP(addr); // 代码使用了CMSIS库函数 __set_MSP(),该函数用于设置MSP寄存器的值为传入的addr参数。
}
void sys_standby(void) // (进入待机模式)
{
RCC->APB1ENR |= 1 << 28; // 使能RCC模块中的APB1总线上的PWR(Power Control)模块时钟,(将1 << 28左移28位,得到0x10000000表示PWR)
PWR->CSR |= 1 << 8; // 将PWR模块的CSR寄存器的第8位(PDDS)置为1,表示进入待机模式。
PWR->CR |= 1 << 2; // 将PWR模块的CR寄存器的第2位(ULP)置为1,表示选择ULP(Ultra Low Power)模式,并开启进入待机模式的操作。
PWR->CR |= 1 << 1; // 将PWR模块的CR寄存器的第1位(PDDS)置为1,PDDS(Power Down Deep Sleep)模式,并开启进入待机模式的操作。
SCB->SCR |= 1 << 2; // 将SCB(System Control Block)模块的SCR寄存器的第2位(SLEEPDEEP)置为1,表示进入深度睡眠状态。
sys_wfi_set(); // (进入低功耗状态)
}
void sys_soft_reset(void) // (系统软件复位)
{
// 0X05FA0000和(uint32_t)0x04的"或"运算,可以得到一个特定值0x05FA0004,表示执行系统软件复位。
// 将这个值赋给SCB的AIRCR寄存器,就会让处理器执行软件复位操作。
SCB->AIRCR = 0X05FA0000 | (uint32_t)0x04;
}
uint8_t sys_clock_set(uint32_t plln) // (时钟设置函数)
{
uint32_t retry = 0;
uint8_t retval = 0;
// plln参数,表示需要设置的时钟倍频系数。retry和retval分别用于记录重试次数和返回值。
// RCC是一个寄存器控制器模块
RCC->CR |= 0x00010000; // 使用"|="运算,将该寄存器的位16(HSEON)置为1,表示要开启外部高速时钟(HSE)。
while (retry < 0XFFF0)
{
__nop(); // 空指令语句__nop(),以等待外部高速时钟就绪。
if (RCC->CR & (1 << 17) && retry > 0X8000)
{
break; // 如果外部高速时钟稳定就绪,则会将时钟控制寄存器的位17(HSERDY)置为1,此时可以退出循环。
}
retry++;
}
if (retry >= 0XFFF0) // 外部高速时钟需要一定的时间才能稳定,因此设置了一个最大重试次数0XFFF0,如果超过该次数仍然无法就绪,
{
retval = 1; // retval为1表示时钟设置失败。
}
else // 如果外部高速时钟稳定就绪,就进入到时钟配置的核心部分。
{
RCC->CFGR = 0X00000400;
// 将时钟分频寄存器CFGR的值设置为0X00000400,表示AHB(Advanced High-performance Bus)和APB
// (Advanced Peripheral Bus)分别不分频,即AHB和APB时钟频率相同。
plln -= 2; // 传入的时钟倍频系数plln减去2,
RCC->CFGR |= plln << 18; // 并将其左移18位,然后使用"|="运算,将其设置为CFGR寄存器对应的位[29:24],表示设置PLL时钟的倍频系数。
RCC->CFGR |= 1 << 16; // 将其设置为CFGR寄存器对应的位[29:24],表示设置PLL时钟的倍频系数。
/*配置Flash存储器的延时周期*/
FLASH->ACR = 1 << 4; // 使用"或"运算将ACR寄存器的位4(LATENCY)置为1,表示设置Flash存储器的延时周期为1个时钟周期。
FLASH->ACR |= 2 << 0; // 使用"或"运算将ACR寄存器的位[2:0]设置为2,表示使用HCLK(AHB时钟)延时。
/*上述配置目的为:使得Flash存储器能够支持系统时钟在48MHz时的工作。*/
RCC->CR |= 1 << 24; // 使用"或"运算将CR寄存器的位24(PLLON)置为1,使能PLL时钟。
while (!(RCC->CR >> 25)) // 使用while循环等待时钟稳定,即等待CR寄存器的PLL锁定位(PLLRDY)变为1,表示PLL时钟稳定。
;
RCC->CFGR |= 2 << 0; // 使用while循环等待时钟稳定,即等待CR寄存器的PLL锁定位(PLLRDY)变为1,表示PLL时钟稳定。
while (((RCC->CFGR >> 2) & 0X03) != 2) // 使用while循环等待CFGR寄存器的位[3:2]变为2,表示PLL时钟作为系统时钟已经稳定。
;
}
return retval; // 最后,返回时钟设置的状态,如果返回值为0表示时钟设置成功,如果返回值为1表示设置失败。
}
void sys_stm32_clock_init(uint32_t plln) // (时钟初始化函数)
{
RCC->APB1RSTR = 0x00000000; // 将APB1的复位寄存器清零,即将其所有位设置为0,这样可以确保时钟初始化时这个总线上的所有设备都处于复位状态。
RCC->APB2RSTR = 0x00000000; // 将APB2的复位寄存器清零,即将其所有位设置为0,这样可以确保时钟初始化时这个总线上的所有设备都处于复位状态。
RCC->AHBENR = 0x00000014; // 设置AHB总线上的时钟使能寄存器,使能DMA、GPIOA和GPIOB外设。
RCC->APB2ENR = 0x00000000; // 将APB2的时钟使能寄存器清零,即将其所有位设置为0,这样可以确保时钟初始化时这个总线上的所有设备都未启用。
RCC->APB1ENR = 0x00000000; // 将APB1的时钟使能寄存器清零,即将其所有位设置为0,这样可以确保时钟初始化时这个总线上的所有设备都未启用。
RCC->CR |= 0x00000001; // 设置RCC时钟控制寄存器的开启位,使能外部高速时钟(HSE)。
RCC->CFGR &= 0xF8FF0000; // 清除RCC时钟配置寄存器的PLL、分频器和时钟选项位。
RCC->CR &= 0xFEF6FFFF; // 清除RCC时钟配置寄存器的PLL、分频器和时钟选项位。
RCC->CR &= 0xFFFBFFFF; // 清除RCC时钟控制寄存器的HSE禁止位
RCC->CFGR &= 0xFF80FFFF; // 清除RCC时钟配置寄存器的PLL倍频位
RCC->CIR = 0x009F0000; // 清空RCC时钟中断寄存器。
sys_clock_set(plln); // 用sys_clock_set函数设置PLL倍频系数和选择是否将PLL作为系统时钟。
#ifdef VECT_TAB_RAM
sys_nvic_set_vector_table(SRAM_BASE, 0x0); // 选择向量表在RAM中的地址,用于用户程序的跳转和中断处理。
#else
sys_nvic_set_vector_table(FLASH_BASE, 0x0); // 选择向量表在FLASH中的地址,用于用户程序的跳转和中断处理。
#endif
}