多圖殺貓。實驗環境為Windows。
咳咳,本次實驗感覺環境好難配啊,實驗倒是感覺起來容易一些(果然有攻略就是舒服)。
實驗準備 - 點燈
1. STM32CubeMX
接下來把文末下載區的幾個全部先下載下來備用。
首先安裝主程序STM32CubeMX,就一路下一步下一步下一步就好了,看得懂的比如說安裝目錄之類的改一下就好了,不需要防備360安全套餐、百度全家桶等。
打開程序之后界面長這樣。
萌新上來什么都不懂,跟著教程直接新建工程,選擇了核心板的型號STM32F103C8之后直接點OK。
點擊Project選項卡中的setting,進入到項目設置。填寫完項目名稱、位置以及工具鏈。在圖中,我選的是MDK-ARM V5,下載列表中有相關工具下載(Keil)。
全部設置好之后,點擊下方OK按鈕。說時遲那時快,只聽砰的一聲,一個討厭的警示框跳出來了。告訴我們,有個依賴沒有滿足。
此時可以直接點擊Yes開啟自動下載模式,全自動安裝,你值得擁有!
但是由于各種原因斷在70M這個位置兩次之后我心灰意冷選擇放棄,各種查找資料之后發現還可以進行本地安裝。
首先是從Help選型卡中的Install New Libraries進入庫管理界面。
此時,左下角有一個** From Local ... **按鈕,將下載的V1.3.0包以及V1.3.1補丁順序打入即可。安裝完畢后可以看到安裝的是當前最新版本。
接下來為了點亮小燈,需要找一個引腳輸出高電平形成電流。在此,我選擇將PA9接口置為GPIO_Output模式。多說一句,實際上這個地方的配置不是必須的,因為這里的配置最終也是由MX轉譯成為C的代碼,而在Keil中才是真正的編譯,即可以在代碼中直接編寫相應的流程。不過,話說回來,能自動干的東西為啥要碼代碼。
而后,點擊代碼生成按鈕。
生成結束后后會在項目的文件夾下生成一個由設置所決定的工程文件夾。而Open Project目前點了還沒有作用,因為還沒有裝相應的軟件。
至此,STM32CubeMX的流程就結束了。
2. Keil
STM32CubeMX生成出了整個工程的目錄,但是需要一個軟件將其打開。我選擇使用Keil v5(其實是先選的v5,上面的Project Setting中有設置)。
Keil的安裝就沒有上面那個這么麻煩,直接下一步下一步下一步,注冊碼也有注冊機直接可以搞定。過程一如參考資料里的安裝教程。
話接上集,直接點擊Open Project按鈕,打開項目工程文件。中間還會需要安裝一個依賴包,繼續同意即可。
可以從圖中看到,STM32CubeMX已經幫我們生成好了一系列的基礎文件,從中進行修改以滿足我們的要求即可。
由于我們在MX中已經有進行過設置,修改了PA9為輸出引腳,在代碼中就得到了體現。下方即為生成的GPIO初始化代碼。
main函數中調用了GPIO初始化函數之后,PA9就開始持續輸出高電平。而為了讓小燈閃爍,自然不能一直高電平下去,而是需要輸出一個矩形波。
至此,代碼已經全部寫完。而在下載之前,需要在Flash -> Configure Flash Tools -> Utilities中,將Use Debug Driver的勾去掉,轉而使用ST-LINK V/2。
按F7進行模塊編譯,按F8下載到板子吧!
值得一提的是,下載之后,并不會立刻有反應。而是需要斷電重啟或者使用板子上帶的RST按鈕進行重置之后程序才會正式開始運行。
注意注意,我圖中是將小燈直接連入GND以及VCC,但是有的小燈比較脆弱,這么做會直接燒掉。所以穩妥的做法應該在連接電路中再加一個電阻。s
實驗步驟
0. 連接圖
按照Lab3攻略上說的連好圖,先假裝自己連對了確實連對了。
其中,ST-LINK接四根線3.3V、GND、SWDIO、SWCLK分別對應STM32板子上的3.3V、GND、DIO、DCLK。此為燒錄用的線路。而PA9、PA10為串口通信所用的線路。所以圖中使用了兩個USB口。
面包板上線的連接方式為從引腳出來之后經過按鈕到GND。
1. UART串口輸出
首先進入STM32CubeMX。按照實驗中的要求,在右側芯片設置中,將PA12、PA11定為輸入(接按鈕),PA10、PA9分別定為TX、RX(接電腦串口)。
同時,在左側的配置中,將USART1的模式定為Half-Duplex。這步所對應生成的代碼與實驗攻略中的代碼略有差別。但是在不指定模式的情況下,PA9以及PA10會被認為是GPIO_Output而與PA11一起進行初始化,而不是TX、RX口。所以在此選擇一個模式。
配置完畢,接下來是代碼生成,點擊按鈕靜靜等待即可。
少女祈禱中。。。
代碼生成完之后,基本的函數已經都有了。但是還需要自己手動填寫一些代碼,如UART0_Init()等。而stm32f1xx_hal_msp.c中所需填寫的函數與Half-Duplex模式一致,所以不需要進行大幅度改動。只需要將攻略的幾行代碼填入即可。
void UART0_Init(UART_HandleTypeDef* UartHandle){
UartHandle->Instance = USART1;
UartHandle->Init.BaudRate = 9600;
UartHandle->Init.WordLength = UART_WORDLENGTH_8B;
UartHandle->Init.StopBits = UART_STOPBITS_1;
UartHandle->Init.Parity = UART_PARITY_NONE;
UartHandle->Init.HwFlowCtl = UART_HWCONTROL_NONE;
UartHandle->Init.Mode = UART_MODE_TX_RX;
HAL_UART_Init(UartHandle);
}
int main(void) {
...
UART_HandleTypeDef UartHandle;
UART0_Init(&UartHandle);
while (1) {
HAL_UART_Transmit(&UartHandle, (uint8_t*)"press\r\n", 7, 500);
HAL_Delay(100);
}
}
HAL_UART_Transmit有4個參數,第一個參數是串口的句柄,第二個參數是一個二進制數組(char*),第三個參數是要發送的數據長度,第四個是發送超時的判定時間。
2. 接入開關
在0中已經將線連好。
3. 檢測按鈕被按下
由于按鈕接地,所以,當按鈕被按下時,PA11應該可以檢測到一個低電平的輸入。編寫程序的時候,可以根據這一點,讀取PA11的引腳值,并據此判斷。
但是,在實驗過程中結果并不理想。測試結果如下。
注意到按鈕的狀態是不斷變化的,而我并沒有去接觸按鍵。這種情況可能是由于電路本身不精密所導致的。~~~~(Update:做了第4項實驗之后,發現同樣的接法PA12沒問題,看來主要是因為按鈕的問題才導致了抖動)(Update: 突然覺得可能是面包板的問題)
硬件難改,但實驗也不能這么停止。仿照計算機組成實驗的思想,嘗試著使用按鍵防抖動的方法檢測按鍵是否被按下。具體方法是檢測按鍵被穩定按下某幾個周期之后,才表示真正檢測到了按鍵事件。
但是實際測試結果并不理想,由圖可以看出。我依然沒有按任何的按鍵,而串口卻一直有輸出。
思考了一下,發現可能是由于兩次檢測時間間隔太短,導致跳變的引腳值被記錄多次。此時,使用延時檢測即可,即兩次檢測之間間隔拉大。
下方代碼為最終版本,即加了anti_jitter即HAL_Delay的結果。
#define MAX_BITCOUNT 0xff
#define CHECK_DELAY 10
void anti_jitter(int *bitcount, int state){
*bitcount <<= 1;
*bitcount &= MAX_BITCOUNT;
*bitcount += state & 1;
}
int main(void)
{
int total, bitcount;
char str[64];
int send = 0;
// something for initialization.
total = 0; bitcount = MAX_BITCOUNT ;
while (1) {
int cnt;
GPIO_PinState state;
state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_11);
HAL_Delay(CHECK_DELAY);
anti_jitter(&bitcount, state);
if (bitcount == 0){
if (!send){
send = 1;
total++;
cnt = sprintf(str, "Press 11 %d times\r\n", total);
HAL_UART_Transmit(&UartHandle, (uint8_t*)str, cnt, 500);
}
}else{
send = 0;
}
}
}
最終代碼能夠做到無誤檢測,同時在按鍵被按下之后,響應時間較短。
4. PA12中斷響應
PA12引腳的下降沿觸發將會觸發中斷,進入函數EXTI15_10_IRQHandler,此時在函數中調用HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_12)表示查看PA12的值,如果符合條件,則觸發HAL_GPIO_EXTI_Callback函數。
在callback函數中,將檢測標志位置1即可被while循環中的if識別并輸出。代碼如下。
// ---------- stm32f1xx_it.c ----------
void EXTI15_10_IRQHandler(void){
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_12);
}
// ---------- main.c ----------
void NVIC_Init(){
// 使能中斷
HAL_NVIC_SetPriority(EXTI15_10_IRQn,0,0);
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
}
int PA12count = 0, PA12flag = 0;
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
if (GPIO_Pin == GPIO_PIN_12){
PA12flag = 1;
PA12count ++;
}else{
UNUSED(GPIO_Pin);
}
}
int main(void) {
int total, bitcount;
char str[64];
int send = 0;
NVIC_Init();
......
while (1) {
int cnt;
......
if (PA12flag == 1){
PA12flag = 0;
cnt = sprintf(str, "Press 12 %d times\r\n", PA12count);
HAL_UART_Transmit(&UartHandle, (uint8_t*)str, cnt, 500);
}
}
}
void MX_GPIO_Init(void) {
......
GPIO_InitStruct.Pin = GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
......
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
5. 定時器中斷
定時器中斷的實現思路與引腳涉及的中斷基本一致。不同的是需要設置中斷觸發的時間。因為不同于外部中斷,時鐘中斷是內部觸發,所以需要預先設定好觸發時間。
而后,同樣的,需要覆寫中斷觸發函數TIM3_IRQHandler,而后在其中對時鐘進行判斷后觸發HAL_TIM_PeriodElapsedCallback。并在callback中真正處理邏輯。
// ---------- stm32f1xx_hal_msp.c ----------
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim){
__TIM3_CLK_ENABLE();
}
void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* htime){
__TIM3_CLK_DISABLE();
}
// main.c
void NVIC_Init(){
...
HAL_NVIC_SetPriority(TIM3_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM3_IRQn);
}
TIM_HandleTypeDef TIM_Handle;
int TIMflag = 0;
void TIM3_IRQHandler(void){
HAL_TIM_IRQHandler(&TIM_Handle);
}
void TIM_Init(){
TIM_Handle.Instance = TIM3;
TIM_Handle.Init.Prescaler = 8000;
TIM_Handle.Init.CounterMode = TIM_COUNTERMODE_UP;
TIM_Handle.Init.Period = 199;
HAL_TIM_Base_Init(&TIM_Handle);
HAL_TIM_Base_Start_IT(&TIM_Handle);
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
TIMflag = 1;
}
int main(void){
......
total = 0; bitcount = MAX_BITCOUNT;
while (1) {
int cnt;
GPIO_PinState state;
HAL_Delay(CHECK_DELAY);
state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_11);
anti_jitter(&bitcount, state);
if (bitcount == 0){
if (send != 2){
send = 1;
}
}else if (send == 2){
send = 0;
}
if (TIMflag == 1){
TIMflag = 0;
if (send == 1){
send = 2;
total++;
cnt = sprintf(str, "Press 11 %d times\r\n", total);
HAL_UART_Transmit(&UartHandle, (uint8_t*)str, cnt, 500);
}
if (PA12flag == 1){
PA12flag = 0;
cnt = sprintf(str, "Press 12 %d times\r\n", PA12count);
HAL_UART_Transmit(&UartHandle, (uint8_t*)str, cnt, 500);
}
}
}
}
6. 自行車碼表
碼表有兩個模式,里程模式以及速度模式,兩個模式有區別也有聯系。
首先是最基礎的里程模式,該模式只對應PA12按鈕的事件。在每次PA12被按下(輪子走了一圈)的時候算好里程即可,或者使用圈數*輪子周長的方式也行。在本程序中,將輪子周長表示為3.14m。
其次是速度模式。速度模式不能單單使用總里程/總時間,這樣得到的總速度是沒有意義的,而近似實時的速度計算法應該是通過計算在一定時間內所行駛的里程數推算出短時間內的速度。在本程序中,使用1.6s作為計算的時間長度,即8個周期。
代碼清單
main.c
#define MAX_BITCOUNT 0xff
#define CHECK_DELAY 10
#define PERIMETER 3.14
UART_HandleTypeDef UartHandle;
TIM_HandleTypeDef TIM_Handle;
int cnt;
int total, bitcount, dispMode = 0;
int PA12count = 0, PA12flag = 0;
int TIMflag = 0, TIMcount = 0;
float RouteTick[10];
char str[64];
int send = 0;
void UART0_Init(){
UartHandle.Instance = USART1;
UartHandle.Init.BaudRate = 9600;
UartHandle.Init.WordLength = UART_WORDLENGTH_8B;
UartHandle.Init.StopBits = UART_STOPBITS_1;
UartHandle.Init.Parity = UART_PARITY_NONE;
UartHandle.Init.HwFlowCtl = UART_HWCONTROL_NONE;
UartHandle.Init.Mode = UART_MODE_TX_RX;
HAL_UART_Init(&UartHandle);
}
void NVIC_Init(){
HAL_NVIC_SetPriority(EXTI15_10_IRQn,0,0);
HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);
HAL_NVIC_SetPriority(TIM3_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM3_IRQn);
}
void anti_jitter(int *bitcount, int state){
*bitcount <<= 1;
*bitcount &= MAX_BITCOUNT;
*bitcount += state & 1;
}
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
if (GPIO_Pin == GPIO_PIN_12){
PA12flag = 1;
PA12count ++;
}else{
UNUSED(GPIO_Pin);
}
}
void EXTI15_10_IRQHandler(void){
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_12);
}
void TIM3_IRQHandler(void){
HAL_TIM_IRQHandler(&TIM_Handle);
}
void TIM_Init(){
TIM_Handle.Instance = TIM3;
TIM_Handle.Init.Prescaler = 8000;
TIM_Handle.Init.CounterMode = TIM_COUNTERMODE_UP;
TIM_Handle.Init.Period = 199;
HAL_TIM_Base_Init(&TIM_Handle);
HAL_TIM_Base_Start_IT(&TIM_Handle);
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
TIMflag = 1;
TIMcount++;
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
UART0_Init();
NVIC_Init();
TIM_Init();
total = 0; bitcount = MAX_BITCOUNT;
dispMode = 0;
while (1) {
int cnt;
GPIO_PinState state;
HAL_Delay(CHECK_DELAY);
state = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_11);
anti_jitter(&bitcount, state);
if (bitcount == 0){
send = 1;
}else{
if (send == 2){
send = 0;
}
}
if (send == 1){
send = 2;
dispMode = 1 - dispMode;
}
if (TIMflag == 1){
int head = (TIMcount - 1) % 8;
int nexHead = (head + 1) % 8;
float route = PA12count * PERIMETER;
RouteTick[head] = route;
TIMflag = 0;
if (dispMode == 0){
cnt = sprintf(str, "You traveled %.2f m\r\n", route);
}else{
float inThisTime = RouteTick[head] - RouteTick[nexHead];
cnt = sprintf(str, "You traveled in %.2f m/s\r\n", inThisTime / 1.6);
}
HAL_UART_Transmit(&UartHandle, (uint8_t*)str, cnt, 500);
}
}
}
void MX_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOA_CLK_ENABLE();
/*Configure GPIO pins : PA11 PA12 */
GPIO_InitStruct.Pin = GPIO_PIN_11;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
stm32f1xx_hal_msp.c
void HAL_MspInit(void)
{
__HAL_RCC_AFIO_CLK_ENABLE();
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);
/* System interrupt init*/
/* MemoryManagement_IRQn interrupt configuration */
HAL_NVIC_SetPriority(MemoryManagement_IRQn, 0, 0);
/* BusFault_IRQn interrupt configuration */
HAL_NVIC_SetPriority(BusFault_IRQn, 0, 0);
/* UsageFault_IRQn interrupt configuration */
HAL_NVIC_SetPriority(UsageFault_IRQn, 0, 0);
/* DebugMonitor_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DebugMonitor_IRQn, 0, 0);
/* SysTick_IRQn interrupt configuration */
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
}
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim){
__TIM3_CLK_ENABLE();
}
void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* htim){
__TIM3_CLK_DISABLE();
}
void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{
GPIO_InitTypeDef GPIO_InitStruct;
if(huart->Instance==USART1)
{
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_USART1_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_9;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_InitStruct.Pin = GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
}
void HAL_UART_MspDeInit(UART_HandleTypeDef* huart)
{
if(huart->Instance==USART1)
{
__HAL_RCC_USART1_CLK_DISABLE();
__HAL_RCC_USART1_FORCE_RESET();
__HAL_RCC_USART1_RELEASE_RESET();
HAL_GPIO_DeInit(GPIOA, GPIO_PIN_9|GPIO_PIN_10);
}
}
7. PA11的按鈕也采用中斷方式檢測,主程序只檢測標識做串口發送
有了PA12的經驗,修改PA11按鈕的檢測方式也比較容易。(假設PA11沒有抖動問題)
首先,需要在初始化PA11的時候,將其設置為中斷觸發,我設置的是下降沿觸發。
GPIO_InitStruct.Pin = GPIO_PIN_11;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
而后,在中斷處理函數內添加對PA11的處理。
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){
if (GPIO_Pin == GPIO_PIN_12){
PA12flag = 1;
PA12count ++;
}else if(GPIO_Pin == GPIO_PIN_11){
PA11flag = 1;
}else{
UNUSED(GPIO_Pin);
}
}
void EXTI15_10_IRQHandler(void){
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_11);
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_12);
}
最后,只需要在主循環中對PA11flag進行檢測即可。
if (PA11flag == 1){
PA11flag = 0;
dispMode = 1 - dispMode;
}
當然,由于PA11flag只是涉及到狀態的改變,可以直接在中斷觸發函數中進行模式的切換。
參考資料:
- 第3次實驗的指導文檔 by 楊凱
- [分享] [練習]以STM32CubeMX+Keil 成功點燈 -- 比較 STM32 與 Arduino 的差異
- STM32CubeMX的固件庫的離線安裝方法
- keil mdk v5.11 官方最新版 這個鏈接是為了安裝教程,沒有下載過
- STM32F103 Blink LED (using Keil and STMCubeMX)
- STM32學習筆記之EXTI(外部中斷)
- stm32庫函數詳解