This commit is contained in:
2025-09-28 23:33:51 +08:00
parent ee6f94aae4
commit e369f6c284
15 changed files with 4043 additions and 0 deletions

8
1gpio/user/inc/gpio.h Normal file
View File

@@ -0,0 +1,8 @@
#ifndef _GPIO_H
#define _GPIO_H
#include "main.h"
void user_gpio_setup(void);
#endif

17
1gpio/user/inc/main.h Normal file
View File

@@ -0,0 +1,17 @@
#ifndef _MAIN_H
#define _MAIN_H
#include <stdint.h>
// 包含libopencm3内核的相关头文件
#include <libopencm3/cm3/systick.h> // 系统滴答定时器相关定义
#include <libopencm3/cm3/scb.h> // 系统控制块相关定义
// 包含STM32 H系列微控制器的相关头文件
#include <libopencm3/stm32/rcc.h> // 复位和时钟控制相关定义
#include <libopencm3/stm32/pwr.h> // 电源控制相关定义
#include <libopencm3/stm32/gpio.h> // 通用输入输出相关定义
#include <libopencm3/stm32/flash.h> // 闪存控制相关定义
#include <libopencm3/stm32/h7/nvic.h> // 中断向量控制器相关定义
#endif

11
1gpio/user/inc/systick.h Normal file
View File

@@ -0,0 +1,11 @@
#ifndef _SYSTICK_H
#define _SYSTICK_H
#include "main.h"
extern volatile uint32_t systick;
void user_delay_ms(uint32_t ms);
void systick_init(uint32_t ticks);
#endif

47
1gpio/user/src/gpio.c Normal file
View File

@@ -0,0 +1,47 @@
#include "gpio.h"
/**
* @brief 初始化用户GPIO引脚配置
*
* @details 该函数用于初始化和配置用户所需的GPIO引脚,主要包括以下配置:
* 1. 使能GPIOE时钟,配置PE3引脚为推挽输出模式,速度为2MHz,初始状态为低电平
* 2. 使能备份区域寄存器访问通过设置PWR_CR1_DBP位
* 3. 使能GPIOC时钟,配置PC13引脚为输入模式,并启用下拉电阻
* 这些配置通常用于设置LED控制引脚和按钮输入引脚等常见外设接口。
*
* @note 该函数应在系统初始化阶段调用,确保GPIO引脚在使用前已正确配置。
* PE3引脚配置为输出模式,可用于驱动LED或其他输出设备。
* PC13引脚配置为输入模式并启用下拉电阻,适合连接按钮或开关等输入设备。
* PWR_CR1_DBP位的设置允许访问备份寄存器,这在某些应用中可能是必要的。
*
* @warning 在调用此函数前,应确保系统时钟已经正确配置。
* 修改GPIO配置可能会影响连接到这些引脚的外设功能。
* 在多任务系统中,如果多个任务访问同一GPIO,可能需要添加同步机制。
*
* @see rcc_periph_clock_enable(), gpio_mode_setup(), gpio_set_output_options(), gpio_clear()
*/
void user_gpio_setup(void)
{
// 使能GPIOE端口的时钟,必须先使能时钟才能配置和使用该端口的引脚
rcc_periph_clock_enable(RCC_GPIOE);
// 配置GPIOE端口的引脚3为输出模式,不使用上拉或下拉电阻
gpio_mode_setup(GPIOE, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO3);
// 设置GPIOE端口引脚3的输出选项推挽输出类型,速度为2MHz
gpio_set_output_options(GPIOE, GPIO_OTYPE_PP, GPIO_OSPEED_2MHZ, GPIO3);
// 将GPIOE端口引脚3设置为低电平,也就是灯灭
gpio_clear(GPIOE, GPIO3);
// 使能备份域访问,通过设置电源控制寄存器1的DBP位
// 这是访问RTC、备份寄存器等备份域外设的必要步骤
// PC13-PC15是备份域的GPIO引脚,需要使能备份域访问才能配置这些引脚
PWR_CR1 |= PWR_CR1_DBP;
// 使能GPIOC端口的时钟,必须先使能时钟才能配置和使用该端口的引脚
rcc_periph_clock_enable(RCC_GPIOC);
// 配置GPIOC端口的引脚13为输入模式,并启用下拉电阻
gpio_mode_setup(GPIOC, GPIO_MODE_INPUT, GPIO_PUPD_PULLDOWN, GPIO13);
}

174
1gpio/user/src/main.c Normal file
View File

@@ -0,0 +1,174 @@
#include "main.h"
#include "gpio.h"
#include "systick.h"
static void system_clock_setup(void);
/**
* @brief 主函数,程序入口点
*
* @details 该函数是程序的入口点,负责系统的初始化和主循环。主要执行以下步骤:
* 1. 设置中断优先级分组为16个优先级,无子优先级
* 2. 初始化系统时钟配置
* 3. 初始化SysTick定时器,设置为每毫秒产生一次中断
* 4. 初始化用户GPIO引脚配置
* 5. 进入主循环,检测按钮状态并控制LED
* 主循环中检测PC13引脚按钮的状态,当检测到按钮按下时,
* 进行消抖处理,然后切换PE3引脚LED的状态,并等待按钮释放。
*
* @note 该函数不会返回,除非发生严重错误。
* 主循环中的按钮检测包含了简单的消抖处理,通过延时和再次检测实现。
* 程序使用了轮询方式检测按钮状态,适用于简单的嵌入式应用。
*
* @warning 在修改此函数时,应确保系统初始化顺序正确,特别是时钟配置。
* 按钮消抖延时时间(20ms)可能需要根据实际硬件特性进行调整。
* 如果使用RTOS,此函数结构需要重新设计以适应任务调度。
*
* @see scb_set_priority_grouping(), system_clock_setup(), systick_init(),
* user_gpio_setup(), user_delay_ms(), gpio_get(), gpio_toggle()
*/
int main(void)
{
// 设置中断优先级分组为16个主优先级,无子优先级
// 这意味着每个中断都有独立的优先级,没有子优先级用于同一优先级中断的排序
scb_set_priority_grouping(SCB_AIRCR_PRIGROUP_GROUP16_NOSUB);
// 初始化系统时钟配置
system_clock_setup();
// 初始化SysTick定时器,参数1000U表示每秒产生1000次中断,即每1ms一次
systick_init(1000U);
// 初始化用户GPIO引脚配置
// 根据gpio的代码,这包括配置PE3为输出LED和PC13为输入按钮
user_gpio_setup();
// 主循环,程序将在此无限循环
while (1)
{
// 检测PC13引脚按钮的状态,如果为高电平(按钮按下)则执行以下代码
if(gpio_get(GPIOC, GPIO13))
{
// 延时20ms,用于按钮消抖
// 按钮在按下和释放时会产生机械抖动,延时可以避免误检测
user_delay_ms(20);
// 再次检测按钮状态,确认按钮确实被按下(不是抖动)
if(gpio_get(GPIOC, GPIO13))
{
// 切换PE3引脚LED的状态
// 如果LED当前是亮的状态,则熄灭;如果是灭的状态,则点亮
gpio_toggle(GPIOE, GPIO3);
}
// 等待按钮释放
// 这是一个空循环,一直检测直到按钮被释放PC13变为低电平
// 这样可以防止在按钮按住期间多次触发LED切换
while(gpio_get(GPIOC, GPIO13));
}
}
// 理论上不会执行到这里,因为上面是无限循环
// 这行代码主要是为了满足C语言标准中main函数应有返回值的要求
return 0;
}
/**
* @brief 系统时钟配置函数
*
* @details 该函数用于初始化和配置系统的时钟树,包括PLL锁相环、系统时钟分频、Flash等待状态
* 和电源管理设置。函数执行以下主要步骤:
* 1. 初始化GPIOH和SYSCFG时钟,配置GPIOH1引脚为输出模式
* 2. 设置PLL配置结构体的各个参数,包括时钟源选择、PLL分频系数等
* 3. 配置系统时钟分频参数,确定各个总线的时钟频率
* 4. 设置Flash等待状态,确保Flash访问速度与系统时钟频率匹配
* 5. 配置电压缩放比例和电源模式
* 6. 应用PLL配置,完成系统时钟设置
* 这些配置参数与STM32CubeMX工具生成的配置可以一一对应,便于调试和验证。
*
* @note 该函数应在系统启动初期调用,确保系统时钟正确配置后再进行其他初始化。
* 函数中配置的GPIOH1引脚可能用于指示时钟配置状态或调试目的。
* PLL配置基于25MHz的外部高速晶振(HSE),通过PLL1产生系统时钟。
* PLL2和PLL3可用于为特定外设提供时钟。
* 系统时钟配置完成后,CPU将运行在较高频率(约480MHz),具体取决于PLL1配置。
*
* @warning 修改此函数中的时钟配置参数时,应确保不超过芯片的最大工作频率限制。
* 不正确的时钟配置可能导致系统不稳定或无法正常工作。
* 修改时钟配置可能会影响依赖于精确时序的外设(如UART、SPI等)。
* 在多任务系统中,修改系统时钟可能需要重新调整定时器和调度器的配置。
*
* @see rcc_periph_clock_enable(), gpio_mode_setup(), gpio_set_output_options(),
* gpio_set(), rcc_clock_setup_pll()
*/
static void system_clock_setup(void)
{
// 初始化PLL配置结构体,所有成员清零
struct rcc_pll_config pll_config = {0};
// 使能GPIOH端口时钟,用于配置GPIOH1引脚
// OSC_IN是GPIOH0,OSC_OUT是GPIOH1
rcc_periph_clock_enable(RCC_GPIOH);
// 使能SYSCFG时钟,用于系统配置
rcc_periph_clock_enable(RCC_SYSCFG);
// 配置GPIOH1引脚为输出模式,启用上拉电阻
gpio_mode_setup(GPIOH, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO1);
// 设置GPIOH1引脚为推挽输出,速度为50MHz
gpio_set_output_options(GPIOH, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO1);
// 短暂延时,确保GPIO配置稳定
for (unsigned i = 0; i < 20; i++)
{
__asm__("nop"); // 空操作指令,用于延时
}
// 将GPIOH1引脚设置为高电平
gpio_set(GPIOH, GPIO1);
// 设置系统时钟源为PLL
pll_config.sysclock_source = RCC_PLL;
// 设置PLL时钟源为外部高速晶振(HSE)
pll_config.pll_source = RCC_PLLCKSELR_PLLSRC_HSE;
// 设置HSE频率为25MHz
pll_config.hse_frequency = 25000000U;
// 配置PLL1参数,这一部分可以和CubeMX进行一一对应
// 系统时钟频率 = HSE / M * N / P = 25MHz / 5 * 192 / 2 = 480MHz
pll_config.pll1.divm = 5;
pll_config.pll1.divn = 192;
pll_config.pll1.divp = 2;
pll_config.pll1.divq = 2;
pll_config.pll1.divr = 2;
// 配置PLL2参数
pll_config.pll2.divm = 32;
pll_config.pll2.divn = 129;
pll_config.pll2.divp = 2;
pll_config.pll2.divq = 2;
pll_config.pll2.divr = 2;
// 配置PLL3参数
pll_config.pll3.divm = 32;
pll_config.pll3.divn = 129;
pll_config.pll3.divp = 2;
pll_config.pll3.divq = 2;
pll_config.pll3.divr = 2;
// 配置系统时钟分频参数,还是可以和CubeMX进行一一对应
pll_config.core_pre = RCC_D1CFGR_D1CPRE_BYP; // CPU核心时钟不分频
pll_config.hpre = RCC_D1CFGR_D1HPRE_DIV2; // AHB总线时钟2分频
pll_config.ppre1 = RCC_D2CFGR_D2PPRE_DIV2; // APB1总线时钟2分频
pll_config.ppre2 = RCC_D2CFGR_D2PPRE_DIV2; // APB2总线时钟2分频
pll_config.ppre3 = RCC_D1CFGR_D1PPRE_DIV2; // APB3总线时钟2分频
pll_config.ppre4 = RCC_D3CFGR_D3PPRE_DIV2; // APB4总线时钟2分频
// 设置Flash等待状态为4个周期,确保在480MHz系统时钟下能正确访问Flash
pll_config.flash_waitstates = FLASH_ACR_LATENCY_4WS;
// 配置电压缩放和电源模式
pll_config.voltage_scale = PWR_VOS_SCALE_0; // 电压缩放级别0,最高性能模式
pll_config.power_mode = PWR_SYS_LDO; // 使用LDO(低压差线性稳压器)电源模式
pll_config.smps_level = 0; // SMPS(开关模式电源)级别
// 应用PLL配置,完成系统时钟设置
rcc_clock_setup_pll(&pll_config);
}

145
1gpio/user/src/systick.c Normal file
View File

@@ -0,0 +1,145 @@
#include "systick.h"
/**
* @brief 系统毫秒计数器
*
* @details 这是一个全局变量,用于记录系统运行的毫秒数。
* 该变量在SysTick定时器中断服务程序(sys_tick_handler)中递增,
* 通常用于实现系统计时和延时功能如user_delay_ms函数
* 变量被声明为volatile,确保编译器不会优化掉对它的访问,
* 因为它可以在中断上下文中被修改,而主程序可能需要读取它的值。
*
* @note 该变量的初始值为0,表示系统启动后经过的毫秒数。
* 变量的递增频率取决于SysTick定时器的配置,通常为每毫秒递增一次。
* 在32位系统上,该变量约每49.7天会溢出一次从2^32-1回到0
*
* @warning 在多任务或多线程环境中访问此变量时,可能需要添加适当的同步机制,
* 如禁用中断或使用原子操作,以避免竞态条件。
* 不应直接修改此变量的值,除非有特殊需求并了解其影响。
*
* @see sys_tick_handler(), user_delay_ms(), systick_init()
*/
volatile uint32_t systick = 0;
/**
* @brief 执行WFI(Wait For Interrupt)指令,使处理器进入低功耗状态
*
* @details 该函数使用内联汇编执行ARM Cortex-M处理器的WFI指令。
* WFI指令会使处理器进入低功耗状态,直到下一个中断发生时才会唤醒处理器。
* 该函数被声明为静态内联(always_inline),确保总是被内联展开以减少函数调用开销。
* 使用volatile关键字防止编译器优化掉看似"无用"的指令。
*
* @note 该函数不会改变处理器状态,只是暂停执行直到中断发生。
* 在中断发生时,处理器会恢复执行WFI指令之后的代码。
* 该函数通常用于在等待事件或延时期间降低系统功耗。
*
* @warning 使用该函数时,确保系统中存在能够唤醒处理器的中断源,
* 否则处理器可能会一直保持低功耗状态。
*/
static inline __attribute__((always_inline)) void __WFI(void)
{
__asm volatile("wfi");
}
/**
* @brief 实现毫秒级延时的函数
*
* @details 该函数使用SysTick定时器实现精确的毫秒级延时。函数记录当前的SysTick值作为起始时间,
* 然后进入循环等待,直到经过的时间达到指定的毫秒数。在等待期间,CPU会通过WFI指令
* 进入低功耗状态,以降低系统功耗。该函数适用于裸机系统中的简单延时需求。
*
* @param ms 要延时的毫秒数,取值范围为1到(2^32-1)毫秒。
*
* @note 该函数是阻塞式的,在延时期间CPU无法执行其他任务。
* 函数依赖于全局变量systick的正确更新,通常systick在SysTick中断服务程序中递增。
* SysTick定时器应配置为每毫秒产生一次中断。
* 函数内部使用了WFI指令降低功耗,但实际延时的精度取决于中断的频率和响应时间。
*
* @warning 如果系统中没有中断或中断被禁用,使用此函数可能导致系统死锁。
* 在实时性要求高的系统中,应谨慎使用阻塞式延时函数。
* 最大延时时间受限于32位无符号整数的最大值(约49.7天)。
*
* @see systick, __WFI()
*/
void user_delay_ms(uint32_t ms)
{
// 记录开始延时的时刻systick的当前值
uint32_t start = systick;
// 循环等待,直到经过的时间达到指定的毫秒数
while (systick - start < ms)
{
// 进入低功耗模式,等待中断唤醒,有助于在延时时降低CPU功耗
__WFI();
}
}
/**
* @brief 初始化SysTick定时器
*
* @details 该函数用于初始化和配置ARM Cortex-M的SysTick定时器,使其能够以指定的频率产生中断。
* 函数执行以下配置步骤:
* 1. 设置SysTick时钟源为AHB总线时钟
* 2. 根据指定的中断频率计算并设置重载值
* 3. 清除当前计数值
* 4. 设置中断优先级
* 5. 使能中断和计数器
* 配置完成后,SysTick定时器将开始工作,并在每次计数到0时产生中断。
*
* @param ticks 指定SysTick定时器的中断频率,单位为Hz每秒中断次数
* 例如,传入1000表示定时器每1秒产生1000次中断,即每1毫秒产生一次中断。
*
* @note 该函数依赖于rcc_get_bus_clk_freq()函数获取CPU时钟频率。
* SysTick中断优先级被设置为15最低优先级,在需要更高实时性的应用中可能需要调整。
* 函数使用了库函数封装的方式操作SysTick寄存器,而不是直接寄存器访问。
* 重载值的计算方式为:(CPU时钟频率 / ticks) - 1,确保ticks值不能导致计算结果超过24位最大值(16,777,215)。
*
* @warning 如果ticks参数过大,可能导致重载值小于1,此时定时器可能无法正常工作。
* 在调用此函数前,应确保CPU时钟已经正确配置。
* 此函数会覆盖任何之前的SysTick配置,包括中断处理函数的注册。
*
* @see sys_tick_handler(), systick, user_delay_ms()
*/
void systick_init(uint32_t ticks)
{
// 设置SysTick时钟源为AHB总线时钟
systick_set_clocksource(STK_CSR_CLKSOURCE_AHB);
// 计算重载值总线频率除以定时器周期数减1
systick_set_reload((rcc_get_bus_clk_freq(RCC_CPUCLK) / ticks) - 1UL);
// 清除SysTick当前值寄存器被注释掉的代码
//STK_CVR = 0UL;
// 使用函数方式清除SysTick计数器
systick_clear();
// 设置SysTick中断优先级为15最低优先级
nvic_set_priority(NVIC_SYSTICK_IRQ, 15);
// 使用函数方式配置SysTick控制寄存器被注释掉的代码
// STK_CSR = STK_CSR_CLKSOURCE | STK_CSR_TICKINT | STK_CSR_ENABLE;
// 分别使能SysTick中断和计数器
systick_interrupt_enable();
systick_counter_enable();
}
/**
* @brief SysTick定时器中断服务函数
*
* @details 该函数是SysTick定时器的中断服务程序(ISR),每当SysTick定时器计数到0时触发。
* 函数的主要功能是递增全局变量systick,该变量通常用于系统计时和延时功能。
*
* @note 该函数应保持简短高效,因为它是中断服务程序,执行时间过长可能影响系统实时性。
* systick变量通常定义为全局变量,用于记录系统运行的毫秒数假设SysTick配置为1ms中断一次
* 在多任务系统中,此函数可能需要添加任务切换相关的代码。
*
* @warning 此函数不应被用户代码直接调用,它是由硬件自动调用的中断服务程序。
* 在函数内部应避免调用可能导致长时间阻塞的函数或复杂的计算。
* 如果systick变量在多线程/多任务环境中被访问,可能需要添加适当的同步机制。
*
* @see systick, systick_init(), user_delay_ms()
*/
void sys_tick_handler(void)
{
systick++;
}