- 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); //初始化燈,全滅
//初始化 SysTick
SysTick->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單元+1
wdog_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:0
RTC_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 GPIOA
void 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輸出比較引腳的引數對比,捕獲的上升沿和下降沿是正確的。
金葫蘆的程式碼太亂了,這個實驗我就不做過多分析了