寫這個文章是用來學習的,記錄一下我的學習過程。希望我能一直堅持下去,我只是一個小白,只是想好好學習,我知道這會很難,但我還是想去做!
本文寫于:2025.04.03
STM32開發板學習——第12節: [5-2]對射式紅外傳感器計次&旋轉編碼器計次
- 前言
- 開發板說明
- 引用
- 解答和科普
- 一、外部中斷代碼
- 二、旋轉編碼器計次
- 問題
- 總結
前言
? ?本次筆記是用來記錄我的學習過程,同時把我需要的困難和思考記下來,有助于我的學習,同時也作為一種習慣,可以督促我學習,是一個激勵自己的過程,讓我們開始32單片機的學習之路。
? ?歡迎大家給我提意見,能給我的嵌入式之旅提供方向和路線,現在作為小白,我就先學習32單片機了,就跟著B站上的江協科技開始學習了.
? ?在這里會記錄下江協科技32單片機開發板的配套視頻教程所作的實驗和學習筆記內容,因為我之前有一個開發板,我大概率會用我的板子模仿著來做.讓我們一起加油!
? ?另外為了增強我的學習效果:每次筆記把我不知道或者問題在后面提出來,再下一篇開頭作為解答!
開發板說明
? ?本人采用的是慧凈的開發板,因為這個板子是我N年前就買的板子,索性就拿來用了。另外我也購買了江科大的學習套間。
? ?原理圖如下
1、開發板原理圖
2、STM32F103C6和51對比
3、STM32F103C6核心板
視頻中的都用這個開發板來實現,如果有資源就利用起來。另外也計劃實現江協科技的套件。
下圖是實物圖
引用
【STM32入門教程-2023版 細致講解 中文字幕】
還參考了下圖中的書籍:
STM32庫開發實戰指南:基于STM32F103(第2版)
數據手冊
解答和科普
一、外部中斷代碼
DO數字輸出端,隨便接一個GPIO,當我們的擋光片或者編碼器在對射式紅外傳感器中間經過時,這個DO就會輸出電平跳變的信號,就會觸發STM32 PB14口的中斷,我們在中斷函數中,執行變量++的程序,然后主循環里調用OLED顯示這個變量。
第一步,配置RCC,把我們這里涉及的外設的時鐘都打開
第二步,配置GPIO。選擇我們的端口為輸入模式
第三步,配置AFIO,選擇我們用的這一路GPIO,連接到后面的EXTI
第四步, 配置EXTI,選擇邊沿觸發方式,比如上升沿、下降沿或者雙邊沿
還有選擇觸發響應方式,可以選擇中斷響應和事件響應
第五步,配置NVIC,給我們這個中斷選擇一個合適的優先級,最后通過NVIC,外部中斷信號就能進入CPU了。
這樣CPU才能收到中斷信號,才能跳轉到中斷函數里執行中斷程序。
配置時鐘
void CounterSensor_Init(void) //外部中斷配置
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //開啟APB2的GPIOB的外設時鐘RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //開啟APB2的AFIO的時鐘//接著還有EXTI和NVIC兩個外設:這兩個外設時鐘一直都是打開著的,不需要我們再開啟時鐘了
EXTI和NVIC這兩個外設的時鐘是一直都制開著的,不需要我們再開啟時鐘了.
NVIC是內核的外設,內核的外設都是不需要開啟時鐘的,和CPU一起住在皇宮里的。
配置GPIO
GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU; //上拉輸入默認高電平GPIO_InitStructure.GPIO_Pin=GPIO_Pin_8;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOB,&GPIO_InitStructure);
對于中斷來說:要選擇浮空輸入、上拉輸入、或者下拉輸入。這其中的一個模式(可以看參考文檔)
配置AFIO
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource8);
當執行完這個函數后,AFIO的第8個數據選擇器就撥好了,其中輸入端被撥到了GPIOA外設上,對應的就是PA8引腳,輸出端固定連接的是EXIT的第8個中斷線路,這樣PA8引腳的電平信號就可以順利通過AFIO,進入到后級EXTI電路了。
配置EXTI
對于標志位,有的比較緊急,在指標值位后會觸發中斷,在中斷函數里,如果你想查看標志位和清除標志位,就用下面兩個函數;
所以你現在主程序里查看和清除標志位,就用上面兩個函數;
如果想在中斷函數里查看和清除標志位,就用下面兩個函數。
都是讀寫寄存器,只不過下面兩個函數只能讀寫與中斷有關的標志位,并且對中斷是否允許做出來判斷,而上面的這兩個函數只是一般的讀寫標志位,沒有額外的處理,能不能觸發中斷的標志位都能讀取,所以建議主程序用上面兩個,中斷程序里用下面兩個。只不過是進行了區分。
EXTI_InitTypeDef EXTI_InitStructure;EXTI_InitStructure.EXTI_Line=EXTI_Line8 ;EXTI_InitStructure.EXTI_LineCmd=ENABLE;EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;EXTI_InitStructure.EXTI_Trigger= EXTI_Trigger_Falling;EXTI_Init(&EXTI_InitStructure);
現在的配置是:將EXTI的第8個線路配置為中斷模式,下降沿觸發,然后開啟中斷,這樣PA8的電平信號就能通過EXTI通向下一級NVIC了。
配置NVIC
NVIC是內核外設,所以它的庫函數是被ST發配到雜項這里來了,打開misc.h文件
1、分組
2、配置一下初始化參數
定義不在本文件在stm32f10x.h文件
中斷通道列表
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //只能用一組,放在模塊中進行分組,要確保每個模塊分組都選的同一個NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel= EXTI15_10_IRQn ; NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;NVIC_Init(&NVIC_InitStructure);
外部中斷函數配置完成
外部信號從GPIO到AFIO,再到EXTI,再到NVIC,最終通向CPU,這樣才能讓CPU由主程序跳轉到中斷程序執行。中斷程序應該放到哪里呢?
中斷函數
在STM32中,中斷函數都是固定的,每個中斷通道都對應一個中斷函數,中斷函數的名字可以參考一下啟動文件,
這就是中斷函數的格式,中斷函數都是無參無返回值的,中斷函數名字不要寫錯了,寫錯了就進不了中斷了,最好是直接從啟動文件復制過來,這樣就不會有問題。
在中斷函數里,一般都是先進行一個中斷標志位的判斷,確保是我們想要的中斷源觸發的這個函數,因為這個函數EXTI5和EXTI9都能進來,所以要先判斷是不是我們想要的EXTI8進來的,
這個時候就需要去exti.h看一下,
if(EXTI_GetITStatus(EXTI_Line8)==SET){}
判斷一下是不是返回值為SET,如果是的話,我們就可以執行中斷程序了,最后中斷程序結束后,一定要調用一下清除中斷標志位的函數,因為只要中斷標志位置1了,程序就會跳轉到中斷函數,如果你不清除中斷標志位,那它就會一直申請中斷,這樣程序就會一直響應中斷,執行中斷函數,那么程序就卡死在中斷函數里了,所以我們每次中斷函數結束后,都應該清除一下中斷標志位。
void EXTI9_5_IRQHandler (void)
{if(EXTI_GetITStatus(EXTI_Line8)==SET){EXTI_ClearITPendingBit(EXTI_Line8);}}
測試能不能進入中斷
將 Dialog修改為DARMSTM.DLL Parameter修改為-pSTM32F103C8(這里請根據你的芯片填寫型號)
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"
#include "OLED.h"
#include "CountSensor.h"int main(void)
{OLED_Init();OLED_ShowString(1,1,"Hello STM32 MCU");OLED_ShowString(2,1,"Count:");CounterSensor_Init();while(1){OLED_ShowNum(2,7,CounterSensor_Get(),5);}
}
Key.C和H
#include "stm32f10x.h" // Device headeruint16_t CounterSensor_Count;void CounterSensor_Init(void) //外部中斷配置
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //開啟APB2的GPIOB的外設時鐘RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //開啟APB2的AFIO的時鐘//接著還有EXTI和NVIC兩個外設:這兩個外設時鐘一直都是打開著的,不需要我們再開啟時鐘了GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU; //上拉輸入默認高電平GPIO_InitStructure.GPIO_Pin=GPIO_Pin_8;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOA,&GPIO_InitStructure);GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource8);EXTI_InitTypeDef EXTI_InitStructure;EXTI_InitStructure.EXTI_Line=EXTI_Line8 ;EXTI_InitStructure.EXTI_LineCmd=ENABLE;EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;EXTI_InitStructure.EXTI_Trigger= EXTI_Trigger_Falling;EXTI_Init(&EXTI_InitStructure);NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //只能用一組,放在模塊中進行分組,要確保每個模塊分組都選的同一個NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel= EXTI9_5_IRQn ; NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;NVIC_Init(&NVIC_InitStructure);}uint16_t CounterSensor_Get(void)
{return CounterSensor_Count;
}
void EXTI9_5_IRQHandler (void)
{if(EXTI_GetITStatus(EXTI_Line8)==SET){CounterSensor_Count++;EXTI_ClearITPendingBit(EXTI_Line8);}}
#ifndef __COUNT_SENSOR_H
#define __COUNT_SENSOR_Hvoid CounterSensor_Init(void);
uint16_t CounterSensor_Get(void);
#endif
實現現象
按鍵觸發外部中斷,實現計數加1,下降沿觸發,感覺按鍵必須得加入消抖程序。
按鍵觸發外部中斷計數加1
二、旋轉編碼器計次
旋轉編碼器,下面這兩個A,B相的輸出引腳,分別接到了STM32的PB0和PB1兩個引腳。
正向旋轉時,A,B相輸出的是這樣的波形,反向旋轉時,輸出是下面的波形,如果把一相的下降沿用作觸發中斷,在中斷時刻讀取另一相的電平,正轉就是高電平,反轉就是低電平,這樣就能區分旋轉方向了,這不過有一點小瑕疵;
A,B相都觸發中斷,只有在B相下降沿和A相低電平時,才判斷為正轉,在A相下降沿和B相低電平時,才判斷為反轉,這樣就能保證正轉和反轉都是轉到位了,才執行數字加減的操作,同時也演示一下兩個中斷的初始化代碼。
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"
#include "OLED.h"
#include "Encoder.h"int16_t Num;int main(void)
{OLED_Init();Encoder_Init();OLED_ShowString(1,2,"Hello STM32 MCU");OLED_ShowString(2,1,"Num:");while(1){Num+=Encoder_Get(); //Get是旋轉編碼器產生的正負脈沖數,所以這個返回值+=Num,就能對Num進行加減操作了OLED_ShowSignedNum(2,5,Num,5);}
}
Encoder.c和h
#include "stm32f10x.h" // Device headerint16_t Encoder_Count;void Encoder_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //開啟APB2的GPIOB的外設時鐘RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //開啟APB2的AFIO的時鐘//接著還有EXTI和NVIC兩個外設:這兩個外設時鐘一直都是打開著的,不需要我們再開啟時鐘了GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU; //上拉輸入默認高電平GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0|GPIO_Pin_1;GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;GPIO_Init(GPIOB,&GPIO_InitStructure);GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0);GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource1); //EXTI_InitTypeDef EXTI_InitStructure;EXTI_InitStructure.EXTI_Line=EXTI_Line0|EXTI_Line1 ; //EXTI_InitStructure.EXTI_LineCmd=ENABLE;EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;EXTI_InitStructure.EXTI_Trigger= EXTI_Trigger_Falling;EXTI_Init(&EXTI_InitStructure);NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //只能用一組,放在模塊中進行分組,要確保每個模塊分組都選的同一個NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel= EXTI0_IRQn ; NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;NVIC_Init(&NVIC_InitStructure);NVIC_InitStructure.NVIC_IRQChannel= EXTI1_IRQn ; NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;NVIC_InitStructure.NVIC_IRQChannelSubPriority=2;NVIC_Init(&NVIC_InitStructure);}int16_t Encoder_Get(void) //返回變化值用于外部加減一個變量
{int16_t Temp;Temp=Encoder_Count;Encoder_Count=0;return Temp;
}void EXTI0_IRQHandler(void)
{if(EXTI_GetITStatus(EXTI_Line0)==SET){if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)){Encoder_Count--;}EXTI_ClearITPendingBit(EXTI_Line0);}
}void EXTI1_IRQHandler(void)
{if(EXTI_GetITStatus(EXTI_Line1)==SET){if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0)){Encoder_Count++;}EXTI_ClearITPendingBit(EXTI_Line1);}
}
#ifndef __ENCODER_H
#define __ENCODER_Hvoid Encoder_Init(void);
int16_t Encoder_Get(void);#endif
實驗現象
旋轉編碼器計次
**第一個就是,在這個中斷函數里。最好不要執行耗時過長的代碼,:中斷函數要簡短快速。別剛進中斷就執行一個Delay多少毫秒這樣的代碼, 因為中斷是處理突發的事情。如果你為了一個突發的事情待著中斷里不出來了, 那主程序就會受到嚴重的阻塞。
另外就是。最好不要在中斷函數和主函數調用相同的函數或者操作同一個硬件, 如果你既在主程序里調用OLED。又在中斷里調用OLED, OLED就會顯示錯誤。
在主程序中,OLED剛顯示一半,然后就進中斷了,結果中斷還是OLED顯示函數,那OLED就挪到其他地方顯示了,這時還沒有問題,但當中斷結束后,需要繼續原來的顯示,這時就出現問題了, == 因為硬件的顯示位置被挪到其他位置了==,所以再回來的時候,繼續顯示的內容就會跟著跑到其他地方去,這就會造成問題,雖然中斷有保護現場和恢復現場,但這只能保證CPU程序能正常返回不出問題,對于外部硬件的話,并沒有在進入中斷時,進行現場保護,所以中斷返回后,就出現問題了。
最后不要在主函數和中斷函數里,操作可能產生沖突的硬件,在實現功能的時候,可以在中斷里操作變量或者標志位,當中斷返回時,我再對這個變量進行顯示和操作。
這樣既能夠保證中斷函數的簡單快捷,又能保證不產生沖突的硬件操作,這就是中斷程序設計的注意事項。可以多用用變量或者標志位,來減少代碼之間的耦合性,讓部分代碼相互獨立,僅使用變量、標志位或者函數作為接口,這樣能讓程序更加清晰,代碼更加強健。
問題
1、按鍵中斷能不能消抖
總結
本節課主要是了學習了中斷的代碼實現,顯示進行配置NVIC的整個路線打通,第一步,配置RCC,把我們這里涉及的外設的時鐘都打開第二步,配置GPIO。選擇我們的端口為輸入模式第三步,配置AFIO,選擇我們用的這一路GPIO,連接到后面的EXTI第四步, 配置EXTI,選擇邊沿觸發方式,比如上升沿、下降沿或者雙邊沿還有選擇觸發響應方式,可以選擇中斷響應和事件響應第五步,配置NVIC,給我們這個中斷選擇一個合適的優先級,最后通過NVIC,外部中斷信號就能進入CPU了。
然后還有配置兩個中斷時的操作,也是打來有的地方也是可以|,有的需要單獨配置,再寫一行打開所需要的時鐘。