- SysTick
- 代码编写步骤
- 程序代码
- 运行效果
- RTC
- 程序代码
- 运行效果
- 注意
- 1. 代码问题
- 2. 闹钟设置问题
- TIM
- PWM
- 程序代码
- 运行效果
- 代码分析(设计思想)
- 注意
- 代码错误
- 为什么使用 GPIO 输入寄存器读取 TIM 输出比较模式输出的电平
- 其他
- 输入捕获
- 程序代码
- 运行效果
- PWM
SysTick
利用 SysTick 定时器编写倒计时程序,如初始设置为 2 分 30 秒,每秒在屏幕上输出一次时间,倒计时为 0 后,红灯亮,停止屏幕输出,并关闭 SysTick 定时器的中断。
代码编写步骤
- 确认 SysTick 重装载值(一次计时时长)(初始化 SysTick 的主要内容)
计算所需的重装载值需要了解 SysTick 使用的时钟频率,例如本次我们使用系统时钟,使用用户手册可以查询到频率为 48MHz:

在代码中,由 SystemCoreClock 定义:

计数器 24 位,最长计时次数为 224(即从 0xffffff 减至 0),时间为 224÷48000000 = 16777216÷48000000 ≈ 0.349525秒。
重装载值计算:
- 1 ms 中断间隔:1 ms = 1/1000 s = 48 MHz * 1/1000 s = 48000
- 100 ms 中断间隔:100 ms = 1/10 s = 48 MHz * 1/10 s = 4800000
- 确认 SysTick 优先级
因为 SysTick 属于内核外设,跟普通外设的中断优先级有些区别,并没有抢占优先级和子优先级的说法。内核外设的中断优先级由内核SCB这个外设的寄存器:SHPRx 来配置。
在 stm32L431 中,内核外设的中断优先级可编程为:0~15,只有16个可编程优先级,数值越小,优先级越高,一般设置 SysTick 的优先级为 15。
在系统定时器中,配置优先级为 (1UL << __NVIC_PRIO_BITS) - 1UL),其中宏 __NVIC_PRIO_BITS 为4,那计算结果就等于15, 即设置 SysTick 优先级在内核外设中是最低的。
// 设置系统定时器中断优先级
NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL);
- SysTick 初始化
- 初始化步骤:
- 禁止 SysTick 定时器(可以直接
SysTick->CTRL = 0; //清零,包括禁止中断以及计时
) - 写入重载值(
SysTick->LOAD = 重载值
) - 清除当前值(
SysTick->VAL = 任意值
) - 选择时钟(SysTick->CTRL)
- 设置 SysTick 中断优先级
- 启动 SysTick 定时器(SysTick->CTRL,包括使能中断和计时)
- 禁止 SysTick 定时器(可以直接
程序代码
- 主函数:
//main.c
//主函数
int main(void)
{//关总中断DISABLE_INTERRUPTS;wdog_stop();//"时分秒"缓存初始化(00:02:30)gTime[0] = 0; //时gTime[1] = 2; //分gTime[2] = 30; //秒gpio_init(LIGHT_RED,GPIO_OUTPUT,LIGHT_OFF);gpio_init(LIGHT_GREEN,GPIO_OUTPUT,LIGHT_OFF);gpio_init(LIGHT_BLUE,GPIO_OUTPUT,LIGHT_OFF); //初始化灯,全灭//初始化 SysTickSysTick->CTRL = 0; //清零,包括禁止中断以及计时SysTick->LOAD = SystemCoreClock * 0.1; //写入重载值,计时时长为0.1秒SysTick->VAL = 999UL; //清除当前值while(SysTick->VAL != 0); //等待 SysTick 当前值清零完成printf("SysTick 当前值:%d\n", SysTick->VAL);SysTick->CTRL |= (1UL << 2U); //选择内核时钟SysTick->CTRL |= (1UL << 1) | 1UL; //使能 SysTick 中断和计时//开总中断ENABLE_INTERRUPTS;printf("-----倒计时开始-----\n");printf("倒计时剩余:%d:%d:%d\n",gTime[0],gTime[1],gTime[2]);gpio_set(LIGHT_GREEN,LIGHT_ON); //设置绿灯亮//主循环部分for(;;){}
}
- 中断服务函数:
//isr.c
//中断服务程序
void SysTick_Handler()
{static uint8_t SysTickCount = 0;SysTickCount++; //Tick单元+1wdog_feed(); //看门狗“喂狗”if (SysTickCount >= 10){SysTickCount = 0;if(gTime[2] == 0){if(gTime[1] == 0){if(gTime[0] == 0){printf("-----倒计时结束-----\n");gpio_set(LIGHT_GREEN,LIGHT_OFF); //设置绿灯暗gpio_set(LIGHT_RED,LIGHT_ON); //设置红灯亮SysTick->CTRL = 0; //清零,包括禁止中断以及计时return;}gTime[0]--;gTime[1] = 59U;}gTime[1]--;gTime[2] = 59U;}else{ gTime[2]--;}printf("倒计时剩余:%d:%d:%d\n",gTime[0],gTime[1],gTime[2]);if((gTime[2] % 2) == 0){gpio_set(LIGHT_GREEN,LIGHT_ON); //设置绿灯亮}else{gpio_set(LIGHT_GREEN,LIGHT_OFF); //设置绿灯暗}}
}
运行效果
- 提示信息:
倒计时开始:

经验证,SysTick->VAL 确实是写入任何值都会将当前值清零。
倒计时结束:

- 小灯情况:
倒计时中:

绿灯闪烁(每秒一个变化)。
倒计时结束:

红灯常亮
RTC
利用 RTC 显示日期(年 月 日、时 分 秒),每秒更新。并设置某个时间的闹钟。闹钟时间到时,屏幕上显示有你的姓名的文字,并点亮绿灯。
程序代码
- 主函数:
//main.c
//主函数
int main(void)
{//关总中断DISABLE_INTERRUPTS;//用户外设模块初始化gpio_init(LIGHT_RED,GPIO_OUTPUT,LIGHT_OFF); //初始化红灯gpio_init(LIGHT_GREEN,GPIO_OUTPUT,LIGHT_OFF); //初始化绿灯gpio_init(LIGHT_BLUE,GPIO_OUTPUT,LIGHT_OFF); //初始化蓝灯RTC_Init(); //RTC初始化RTC_Set_Time(23,59,59); //设置时间为0:0:0RTC_Set_Date(24,6,1,1); //设置日期//使能模块中断RTC_PeriodWKUP_Enable_Int(); //使能唤醒中断RTC_Alarm_Enable_Int(A);//开总中断ENABLE_INTERRUPTS;RTC_Set_PeriodWakeUp(1); //配置WAKE UP中断,每秒中断一次RTC_Set_Alarm(A,2,0,0,5); //设置闹钟A//主循环for(;;) //for(;;)(开头){}
}
- 中断服务函数:
//isr.c
//======================================================================
//程序名称:RTC_WKUP_IRQHandler
//函数参数:无
//中断类型:RTC闹钟唤醒中断处理函数
//======================================================================void RTC_WKUP_IRQHandler(void){uint8_t hour,min,sec;uint8_t year,month,date,week;if(RTC_PeriodWKUP_Get_Int()) //唤醒中断的标志{RTC_PeriodWKUP_Clear(); //清除唤醒中断标志RTC_Get_Date(&year,&month,&date,&week); //获取RTC记录的日期RTC_Get_Time(&hour,&min,&sec); //获取RTC记录的时间gpio_set(LIGHT_GREEN,LIGHT_OFF); //绿灯暗if((sec % 2) == 0){gpio_set(LIGHT_RED,LIGHT_ON); //红灯亮}else{gpio_set(LIGHT_RED,LIGHT_OFF); //红灯暗}printf("%02d/%02d/%02d %02d:%02d:%02d 星期%d\n",year,month,date,hour,min,sec,week); }}
//======================================================================
//程序名称:RTC_Alarm_IRQHandler
//中断类型:RTC闹钟中断处理函数
//======================================================================
void RTC_Alarm_IRQHandler(void)
{gpio_set(LIGHT_RED,LIGHT_OFF); //红灯暗gpio_set(LIGHT_GREEN,LIGHT_ON); //绿灯亮if(RTC_Alarm_Get_Int(A)) //闹钟A的中断标志位{RTC_Alarm_Clear(A); //清闹钟A的中断标志位printf("闹钟A:32106100066 \n");}if(RTC_Alarm_Get_Int(B)) //闹钟A的中断标志位{RTC_Alarm_Clear(B); //清闹钟A的中断标志位printf("闹钟B:32106100066 \n");}
}
运行效果
- 提示信息:

- 小灯情况:
RTC启动:

红灯闪烁
闹钟到:

绿灯亮起
注意
1. 代码问题
在金葫芦的代码中有一个函数我觉得很有问题,十进制数转BCD码函数只转了个位,十位并没有转。

进行修改后:
// ===========================================================================
// 函数名称:RTC_DEC2BCD
// 函数参数:十进制数
// 函数返回:十进制数对应的BCD码格式
// 功能概要:将十进制数转化为对应的BCD码格式
// ===========================================================================
uint8_t RTC_DEC2BCD(uint8_t val)
{uint8_t bcdL = val % 10;uint8_t bcdH = ((val - bcdL) / 10) % 10;return ((uint8_t)((bcdH<<4)|bcdL));
}
2. 闹钟设置问题
在 STM32 的 RTC 配置中,必须先使能闹钟才能设置闹钟时间,主要原因涉及到 RTC 的工作机制和寄存器的保护措施。以下是详细解释:
RTC 工作机制和寄存器保护
- 防止无效写入:
- RTC 闹钟寄存器通常是受保护的,这样设计是为了防止在 RTC 闹钟未启用时进行无效的写操作。RTC 的许多寄存器在未使能时无法写入,以避免配置无效或误操作导致的系统错误。
- 确保配置生效:
- 使能闹钟之后,RTC 模块内部的状态机会进入相应的工作模式,准备好接受配置。这意味着在 RTC 闹钟未使能的情况下,写入的配置可能不会被正确应用或保存。
- 时钟同步问题:
- RTC 是基于独立的低速时钟(如 LSE 或 LSI)工作的。使能闹钟后,RTC 开始基于这个时钟进行计时,确保时间设置操作是在正确的时钟同步条件下进行的。否则,可能会导致时间设置不准确。
典型的 RTC 闹钟配置步骤
- 使能 RTC 时钟:
- 确保 RTC 时钟源已使能(例如 LSE、LSI 或 HSE 分频)。
- 使能 RTC 和进入初始化模式:
- 设置 RTC 控制寄存器,进入初始化模式。这一步通常是为了确保 RTC 在配置期间处于可控状态。
- 使能闹钟:
- 设置闹钟使能位,确保 RTC 进入闹钟配置模式。
- 配置闹钟时间:
- 在闹钟使能的情况下,写入闹钟时间和日期寄存器。此时写入操作是有效的且被接受的。
- 退出初始化模式并启动 RTC:
- 退出初始化模式,启动 RTC 以开始计时。
TIM
PWM
利用 PWM 脉宽调制,交替显示红灯的5个短闪和5个长闪。
程序代码
main.c
PWM输出初始化
//主函数
int main(void)
{//关总中断DISABLE_INTERRUPTS;//(1.5)用户外设模块初始化gpio_init(LIGHT_RED,GPIO_OUTPUT,LIGHT_OFF); //初始化红灯gpio_init(LIGHT_GREEN,GPIO_OUTPUT,LIGHT_OFF); //初始化绿灯gpio_init(LIGHT_BLUE,GPIO_OUTPUT,LIGHT_OFF); //初始化蓝灯pwm_init(PWM_USER,1000,4000,75.0,PWM_EDGE,PWM_MINUS); //PWM输出初始化printf("ARR = %d\nCCR = %d\n",TIM2->ARR,TIM2->CCR3);//开总中断ENABLE_INTERRUPTS;for(;;) {}
}
pwm.c
使能所需中断
//(5)更新中断使能TIM2->DIER |= TIM_DIER_CC3IE_Msk;TIM2->DIER |= TIM_DIER_UIE_Msk;//使能 NVIC 接收中断NVIC_EnableIRQ(TIM2_IRQn);
isr.c
中断服务函数,通过极性反转实现长短闪转换
uint16_t Int_count = 0;void TIM2_IRQHandler(void)
{//记录中断时计时器的值uint16_t cnt_value = TIM2->CNT;//保证仅处理由 UIF 和 CC3IF 触发的中断if(((TIM2->SR & 1)==1)||((TIM2->SR & (1<<3))!=0)){//清除中断标志TIM2->SR &= ~(uint16_t)1;TIM2->SR &= ~(uint16_t)(1<<3);printf("TIM2->CNT = %d\n",cnt_value);//判断中断时 TIM 输出波形的位置if(cnt_value == (uint16_t)TIM2->CCR3){printf("下降沿,");}if(cnt_value == 0){printf("上升沿,");}if(gpio_get(PWM_PIN2) == 1){//此时若TIM输出通道为高电平则使灯亮起printf("高电平(绿灯亮)\n");gpio_set(LIGHT_GREEN,LIGHT_ON);++Int_count; //在亮灯时记录亮灯中断次数}else{//此时若TIM输出通道为低电平则使灯变暗printf("低电平(绿灯暗)\n");gpio_set(LIGHT_GREEN,LIGHT_OFF);//在灯暗时,当亮灯次数达到5,则将TIM输出极性反转if(Int_count == 5){printf("-----极性反转为");if((TIM2->CCER & TIM_CCER_CC3P) == 0){TIM2->CCER |= TIM_CCER_CC3P_Msk; //设置为负极性printf("负极性-----\n");}else{TIM2->CCER &= ~TIM_CCER_CC3P_Msk; //设置为正极性printf("正极性-----\n");}Int_count = 0; //重新计数}}}
}
运行效果
初始打印重装载值(ARR)和捕获/比较寄存器值(CCR)

负极性:亮灯时间(CNT = 1000 --> CHT = 0(3999))长闪

正极性:亮灯时间(CNT = 0 --> CHT = 1000)短闪

实际开发板上小灯如提示信息上展示的一致。
代码分析(设计思想)
- 选择中心对齐模式
pwm.c:

stm32l431xxx.h


原始代码选择了中心对齐模式3
- 选择边沿对齐模式
pwm.c


- 选择输出比较模式:
pwm.c


这里选择了 PWM 模式 1
波形分析
假设使用中心对齐模式和 PWM 模式 1,则通道 3 输出的波形为:

为了保证(更好地实现)实验效果,我选择使用边沿对齐+PWM 模式 1,并将输出波形的周期设置为 1s,目标波形为:

因此目标频率为 1 Hz,计算公式:
系统频率 48000000 Hz / 预分频 48000 / 重装载值 2000 = 0.5 Hz
函数void pwm_init(uint16_t pwmNo,uint32_t clockFre,uint16_t period,double duty,uint8_t align,uint8_t pol)
其中参数clockFre
(时钟频率)= 系统频率/预分频,即需设置为 1000 Hz;参数period
为重装载值,即需设置为 2000。
最终调用函数的参数:
pwm_init(PWM_USER,1000,2000,75.0,PWM_EDGE,PWM_MINUS); //PWM输出初始化
参数含义:

注意
代码错误
金葫芦的源码写的很怪

首先是TIM2只有四条通道,哪来的通道5,且其引脚号与通道1相同,因此认定为代码写错了,将该行删去。


和上面一样通道5不存在,还有 GEC39(第39号引脚)也不是PTA5,而是通道3(ATB10),因此同样认定为代码写错了,将该行修改正确。
为什么使用 GPIO 输入寄存器读取 TIM 输出比较模式输出的电平
在STM32中,GPIO端口既有输入数据寄存器(IDR)也有输出数据寄存器(ODR),但这两个寄存器的用途有所不同:
- 输入数据寄存器(IDR):用于读取引脚的当前电平状态,不论引脚配置为输入或输出模式。
- 输出数据寄存器(ODR):用于设置输出引脚的电平,但并不直接反映引脚的实际电平,特别是在引脚被配置为输入模式或其他外部电路影响引脚电平时。
为什么读取GPIO输入寄存器(IDR)?
即使引脚配置为输出模式,读取输入数据寄存器(IDR)仍然是确认引脚实际电平状态的可靠方法。输出数据寄存器(ODR)只是用来写入设置引脚的目标电平,而不是反映引脚的实时电平状态。
实际电平状态的确认
定时器在输出比较模式下驱动引脚电平时,实际电平状态需要通过输入数据寄存器(IDR)来读取,这是因为:
- 输入数据寄存器(IDR)总是反映引脚的当前电平,无论引脚配置为输入还是输出。
- 输出数据寄存器(ODR)仅仅存储目标输出值,不反映引脚的当前实际状态。
示例代码
下面是如何在定时器中断服务程序中读取GPIO输入寄存器来获取引脚的实际电平状态的示例代码:
// 假设 TIM1_CH1 对应的 GPIO 引脚为 PA8
#define TIM1_CH1_PIN GPIO_Pin_8
#define TIM1_CH1_PORT GPIOAvoid TIM1_CC_IRQHandler(void) {// 检查 TIM1 通道1 的捕获/比较中断标志if (TIM_GetITStatus(TIM1, TIM_IT_CC1) != RESET) {// 清除中断标志TIM_ClearITPendingBit(TIM1, TIM_IT_CC1);// 读取捕获/比较寄存器的值uint32_t compare_value = TIM_GetCapture1(TIM1);// 读取 GPIO 引脚的状态BitAction pin_state = GPIO_ReadInputDataBit(TIM1_CH1_PORT, TIM1_CH1_PIN);// 根据捕获/比较值和引脚状态进行处理if (pin_state == Bit_SET) {// 引脚为高电平// 处理高电平的情况} else {// 引脚为低电平// 处理低电平的情况}}
}
总结
在STM32微控制器中,读取GPIO输入数据寄存器(IDR)是确认引脚当前实际电平状态的正确方法。输出数据寄存器(ODR)只是用于设置引脚目标电平,不反映实时状态。因此,即使在输出比较模式下,使用输入数据寄存器来读取引脚状态是确保得到准确电平信息的最佳方式。
其他
- 必须清除中断标志否则会一直触发中断
- 计时器在中断时不会停止
输入捕获
GEC39定义为输出引脚,GEC10定义为输入引脚,用杜邦线将两个引脚相连,验证捕捉实验程序Incapture-Outcmp-20211110,观察输出的时间间隔。
程序代码
main.c
outcmp_init(OUTCMP_USER,1000,2000,50.0,CMP_REV); //输出比较初始化incapture_init(INCAP_USER,375,1000,CAP_DOUBLE); //上升沿捕捉初始化
isr.c
//=====================================================================
//函数名称:INCAP_USER_Handler(输入捕捉中断处理程序)
//参数说明:无
//函数返回:无
//功能概要:(1)每次捕捉到上升沿或者下降沿触发该程序;
// (2)每次触发都会上传当前捕捉到的上位机程序
//=====================================================================
void INCAP_USER_Handler(void)
{//声明INCAP_USER_Handler中需要的变量static uint8_t flag = 0;DISABLE_INTERRUPTS; //关总中断//------------------------------------------------------------------//(在此处增加功能)if(cap_get_flag(INCAP_USER)){//在捕获到上升沿之后,输出此刻捕获的是上升沿和时间if(gpio_get(INCAP_USER)==1 && flag == 0){printf("%d分钟:%d秒:%d毫秒此刻是上升沿\r\n",gTime[0],gTime[1],gTime[2]);flag = 1;}//在捕获到下降沿之后,输出此刻捕获的是下降沿和时间else if(gpio_get(INCAP_USER)==0 && flag == 1){printf("%d分钟:%d秒:%d毫秒此刻是下降沿\r\n",gTime[0],gTime[1],gTime[2]);flag = 0;}cap_clear_flag(INCAP_USER); //清中断}//------------------------------------------------------------------ENABLE_INTERRUPTS; //关总中断
}
运行效果


根据初始化TIM输出比较引脚的参数对比,捕获的上升沿和下降沿是正确的。
金葫芦的代码太乱了,这个实验我就不做过多分析了