SysTick介紹
定義:Systick,即滴答定時器,是內核中的一個特殊定時器,用于提供系統級的定時服務。該定時器是一個24位的遞減計數器,具有自動重載值寄存器的功能。當計數器到達自動重載值時,它會自動重新加載并開始新的計數周期。
實現簡單的 延時 | 操作系統的時基(如 FreeRTOS ) |
生成定時中斷以及進行精確定時和周期定時操作 | 軟件看門狗等系統調度操作 |
- 在STM32中,Systick通常以HCLK(AHB時鐘)或HCLK/8的頻率作為運行時鐘。?
?SysTick工作原理
在使用Systick定時器進行延時操作時,可以設定初值,并使能后,每經過一個系統時鐘周期,計數值就減1。當計數到0時,Systick計數器自動重裝初值(nus*72)并繼續計數,同時內部的COUNTFLAG標志會置1位(計數完成),觸發中斷(如果中斷使能)。這樣,可以在中斷處理函數中實現特定的延時邏輯。
時鐘源:HCLK
?SysTick寄存器
- STK_CTRL(控制及狀態寄存器)
位 | 名稱 | 類 型 | 默認值 | 描述 |
16 | COUNTFLAG | R | 0 | 如果在上次讀取本寄存器后, SysTick 已經數到了 0 ,則該位為 1(計數完成) 。如果讀取該位后,該位自動清零。 |
2 | CLKSOURCE | R/W | 0 | 0= 外部時鐘源( STCLK ), ST 公司將其應用為 8 分頻; 1= 內核時鐘( FCLK ), ST 公司將其應用為 1 分頻。 |
1 | TICKINT | R/W | 0 | 1 = SysTick 倒數到 0 時產生 SysTick 異常請求。(產生中斷); 0 =? 數到 0 時無動作。 |
0 | ENABLE | R/W | 0 | SysTick 定時器的使能位。 |
- ?STK_LOAD(重裝載數值寄存器)
位 | 名稱 | 類型 | 默認值 | 描述 |
23:0 | RELOAD | R/W | 0 | 當倒數至零時,將被重裝載的值 |
- ?STK_VAL(當前數值寄存器)
位 | 名稱 | 類型 | 默認值 | 描述 |
23:0 | CURRENT | R/W | 0 | 讀取時返回當前倒計數的值,寫它則使之清零,同時還會清除 在 SysTick 控制及狀態寄存器中的 COUNTFLAG 標志。 |
?Hal_Delay函數的底層邏輯
- HAL_Delay函數中(調用的是SysTick中的中斷服務函數):
- HAL_Init函數中(調用寄存器,初始化SysTick_Init()函數)
手擼延時函數 (操作寄存器)
1.聲明一個微秒的延時函數
步驟:(調用寄存器)
- 給重裝載寄存器設置計數的個數:72*nus;
- 清空當前寄存器;
- 給寄存器(CLR)分配系數:不分頻72MHz,1us記72個數;
- 打開定時器;
- 為使定時器正常的完成一個周期循環,進行判斷:定時器是否正確打開,計數完標志位是否置1;
- 關閉計數器。
void delay_us(uint32_t nus)
{uint32_t temp = 0;//滴答定時器需要配置三個寄存器//1.確定要數多少個數72SysTick->LOAD = 72 * nus;//2.清空當前計數SysTick->VAL = 0x00;//3,配置分頻系數SysTick->CTRL |= (1<<2); //將1的二進制向左移2位//4,開啟定時器SysTick->CTRL |=(1<<0);
// while(!(SysTick->CTRL & (1 << 16)));//再很多地方用到,定時可能被關掉,要判斷定時器是否正常。enabe?1;countflag?1do{temp = SysTick->CTRL;}while((temp & 0x01) && !(temp & (1<<16)));//4.關閉定時器SysTick->CTRL &= ~(1<<0);
// temp &= ~(1<<0);
}
2.聲明一個毫秒的延時函數,利用while循環?
void delay_ms(uint32_t nms)
{while(nms--)delay_us(1000);
}
?3.聲明一個延時秒的函數,利用毫秒函數
void delay_s(uint32_t ns)
{while(ns--)delay_ms(1000);
}
手擼操作系統的延時函數?
????????當有操作系統時SysTick是用來做時基,這時定時器不能頻繁的打開和關閉。所以,延時函數要用另一種寫法。
思路:看定時器的值,當前和下次的數差額,換算成時間。,ticks一共需要記得數:72*nus;利用一個參數tcnt當前已經記了多少個數。利用while循環來記錄記得數量,計算總和,如何超過定義的初值停止計數。
- 參數和原理:
- 流程圖:
- ?微秒函數代碼:
(根據流程圖寫代碼)
void delay_us(uint32_t nus)
{//1.畫流程提uint32_t ticks;uint32_t tcnt = 0,told,tnow;uint32_t relode = SysTick->LOAD;ticks = nus*72;told = SysTick->VAL;while(1){tnow = SysTick->VAL;if(tnow !=told){if(tnow < told)tcnt += told - tnow;elsetcnt += relode -(tnow - told);told = tnow;if(tcnt > ticks)break; } }
}
- ?毫秒和秒的函數和不帶操作系統時的函數相同。
項目:智能排隊控制系統?
項目需求
- ?紅外傳感器檢測有人通過并計數;
- ?計數值顯示在LCD1602;
- ?允許通過時,LED1閃爍,蜂鳴器不響,繼電器不閉合;
- ?不允許通過時,LED2閃爍,蜂鳴器響,繼電器閉合;
- ?每次允許通過5個人,之后轉為不允許通過,3秒后再轉為允許通過。
硬件清單
紅外避障模塊 | 繼電器 | 蜂鳴器 | LCD1602 | 開發板 | ST-Link |
小實驗:SysTick模擬多線程
?一般模擬多線程使用操作系統(FreeRots),但是本章利用SysTick定時器觸發中斷在中斷服務函數里來實現
方法一:(在中斷服務函數里直接寫)
方法二: (寫一個多任務的tasks.c文件)
- tasks.c文件代碼:
#include "tasks.h"
#include "stm32f1xx.h"
#include "led.h"
uint32_t task1_cnt = 0;
uint32_t task2_cnt = 0;uint8_t task1_flag = 0;
uint8_t task2_flag = 0;void systick_it(void){if(task1_cnt < 1000)task1_cnt ++;else{task1_flag = 1;task1_cnt = 0; //計數清零}if(task2_cnt < 1000)task2_cnt ++;else{task2_flag = 1;task2_cnt = 0; //計數清零}
}
上面聲明的函數類似于 中斷服務函數里的 回調函數:寫的是服務代碼。void task1(void){if(task1_flag == 0)return;else{led1_toggle(); task1_flag = 0; //一定要清除標志位,否則上面標志位一直是1,燈一直亮}
}void task2(void){if(task2_flag == 0)return;else{led2_toggle();task2_flag = 0; //一定要清除標志位,否則上面標志位一直是1,燈一直亮}
}
- 將自己寫的?回調函數(服務代碼)聲明到 void SysTick_Handler(void)里:
- ?tasks.h文件代碼
#ifndef __TASKS_H__
#define __TASKS_H__void systick_it(void);
void task1(void);
void task2(void);#endif
- main.c文件代碼
#include "sys.h"
#include "led.h"
#include "delay.h"
#include "tasks.h"int main(void)
{HAL_Init(); /* 初始化HAL庫 */stm32_clock_init(RCC_PLL_MUL9); /* 設置時鐘, 72Mhz ,將外部HSE倍頻到72Mhz*/ led_init(); /* LED初始化 */while(1){ task1();task2();}
}
?注意:
利用SysTick模擬多線程存在時間差,不是完全的實時操作系統,只能實現簡單的任務。
?模塊:紅外跟隨避障
模塊介紹
????????在智能車、機器人和自動化等領域避障技術是確保安全和高效運行的關鍵。紅外避障模塊作為一種常見的避障解決方案,因其非接觸、響應速度快和抗干擾能力強等優點而備受青睞。本文將詳細介紹紅外避障模塊的特點、工作原理、以及應用案例,幫助您更好地了解這一技術。
工作原理:
紅外避障模塊不斷發射紅外信號,當紅外信號:
- 有反射回來,OUT 輸出低電平,輸出指示燈(綠燈)亮。
- 沒反射回來,OUT 輸出高電平,輸出指示燈(綠燈)滅。
? ? ? ? 紅外避障模塊上的一對紅外線發射與接收管,發射管發射出一定頻率的紅外線,當檢測方向遇到障礙物時,紅外線反射回來被接收管接收,經過比較器(LM393)電路處理之后,信號輸出接口輸出低電平信號,同時綠色指示燈會亮起。
?????????因為黑色能夠吸收紅外線(紅外線不反射),而白色不行(紅外線反射),所以除了避障外可用作黑白線循跡、光電開關等等。
常見的用途:
- 機器人避障;
- 小車避障、跟隨;
- 流水線計數;
- 黑白線循跡。
工作參數及引腳介紹
- 工作參數:
工作電壓 | DC 3.3-5V |
工作溫度 | -10°C ~ +50°C |
檢測角度 | 35° |
檢測距離 | 2 ~ 30 CM可調(不同廠家略有差異),距離越近性能越穩定。 |
靈敏度 | 模塊中藍色的電位器用于調節靈敏度,順時針旋轉,靈敏度變高,檢測距離變長;逆時針越小,靈敏度變低,檢測距離變短。 |
- 接線如下:
紅外避障模塊 | STM32 | 備注 |
VCC | 3.3/5V | 電源正極 |
GND | GND | 電源負極 |
OUT | 任意GPIO口 | 數字輸出 |
?小實驗:紅外點燈
- 實驗目的:用手遮擋紅外傳感器,LED1點亮2s,之后熄滅。
- 硬件清單:
紅外傳感器 | 上官二號 | ST-Link |
- 硬件接線:
STM32 | 紅外避障模塊 |
5v | VCC |
G | GND |
GPIO口:PB4 | OUT |
- exti.c文件代碼:
原理:低電平觸發中斷,反轉led燈。
步驟:(中斷函數)
- 寫一個初始化中斷的函數:開啟GPIOA的時鐘,配置GPIO口;
- 寫一個中斷服務函數,里面調用一個GPIO口的公共服務函數;
- 調公共服務函數中的回調函數,編寫服務代買;
- 獲取中斷標志位函數;
- 設置中斷標志位的函數。
#include "exti.h"
#include "stm32f1xx.h"
#include "delay.h"
#include "led.h"//定義一個標志位
uint8_t vibrate_flag = False;void exti_init(void){//打開時鐘__HAL_RCC_GPIOA_CLK_ENABLE();//初始化GPIO口GPIO_InitTypeDef GPIO_Initstruct;GPIO_Initstruct.Mode =GPIO_MODE_IT_FALLING;GPIO_Initstruct.Pin = GPIO_PIN_4;GPIO_Initstruct.Pull = GPIO_PULLUP;GPIO_Initstruct.Speed = GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init(GPIOA,&GPIO_Initstruct);//在HAL_Init()函數中進行優先級分組,默認第四種,實際用二種HAL_NVIC_SetPriority(EXTI4_IRQn,2,0);HAL_NVIC_EnableIRQ(EXTI4_IRQn);
}//中斷服務函數,
void EXTI4_IRQHandler(void){//調用GPIO的中斷公共函數,不同外設不同HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_4);}
//回調函數(GPIO口只有一個),業務代碼:按鍵;消抖
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){//消抖,震動可忽略不計: delay_ms(20);if(GPIO_Pin == GPIO_PIN_4){ //判斷EXTI0是否是GPIO_PIN0if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_4) == GPIO_PIN_RESET){//判斷GPIO口是否是低電平
// led1_toggle();//設置中斷標志位vibrate_flag = Ture;}}
}
//讓主函數識別中斷標志位,定義一個獲取中斷標志位的函數
uint8_t vibrate_flag_get(void){uint8_t temp = vibrate_flag; //第一次調用這個函數,返回:True;第二次調用,返回:Falsevibrate_flag = False;return temp;
}//設置中斷標志位的值
void vibrate_flag_set(uint8_t value){vibrate_flag = value;
}
- exti.h文件代碼:
#ifndef __EXTI_H__
#define __EXTI_H__
#include "stdint.h"#define Ture 1
#define False 0void exti_init(void);
uint8_t vibrate_flag_get(void);
void vibrate_flag_set(uint8_t value);#endif
- main.c文件代碼:
#include "sys.h"
#include "led.h"
#include "delay.h"
#include "exti.h"int main(void)
{HAL_Init(); /* 初始化HAL庫 */stm32_clock_init(RCC_PLL_MUL9); /* 設置時鐘, 72Mhz */led_init(); /* LED初始化 */exti_init();while(1){ if(vibrate_flag_get() == Ture){led1_on();delay_ms(2000);led1_off();vibrate_flag_set(False); //避免在觸發一次中斷,LED燈亮的同時,受到外界的中斷觸發,標志位一直置為True,小燈一直亮。}}
}
模塊:LCD1602顯示模塊
LCD1602介紹
定義:LCD1602( Liquid Crystal Display 1602),一種常見的字符型液晶顯示模塊。它能夠顯示16列2行,共32個字符字符,每個字符都由5x8像素點陣構成,是一種專門用來顯示字母、數字、符號等的液晶顯示模塊。
分類:
顯示字符 | 屏幕顏色 | 作電壓 |
1602(16列2行)、2004(20列4行)、12864(128列64行) | 藍屏(白字)、黃綠屏(黑字/白字)、灰屏(黑字) | 5V、3.3V |
工作參數和引腳介紹?
- 工作參數
工作電壓 工作電流 工作溫度 設備壽命 5V 2.0mA -20°C ~ +70°C >100,000小時
- 引腳接線?
(注意:在接線完成之前,不要給板子供電,避免把LCD1602顯示模塊燒壞)
?引腳注解:
V0:液品顯示器對比度調整端,接正電源時對比度最高,接地時對比度最低,對比度過高時會產 生“鬼影”,使用時可以通過電位器或電阻調整對比度。電阻小了全是黑塊,電阻大了不顯示。
RW:讀寫信號線,高電平時進行讀操作,低電平時進行寫操作。
當 RS (0:指令寄存器)和 RW(0: 寫) 共同為低電平時可以寫入指令成者顯示地址;
當 RS 為低電平(0:指令寄存器)),RW 為高電平(1:讀)時可以讀出信號;
當 RS 為高電平(1:數據寄存器),RW 為低電平時(0: 寫)可以寫入數據。
E:使能端,當E端由高電平跳變成低電平時,液晶模塊執行命令。
?基本操作時序
- ?讀操作時序圖
- 寫操作時序圖?
我們以寫操作為例,分析一下時序圖。
- 當我們要寫指令的時候,RS 置為低電平,RW 置為低電平,E 置為低電平,然后將指令數據送到數據口 D0~D7,延時 tsp1,讓 LCD1602 準備接收數據,這時候將 E 拉高,產生一個上升沿,這時候指令就開始寫入 LCD1602,延時一段時間,將 E 置為低電平。
- 當我們要寫數據的時候,RS 置為高電平,RW 置為低電平,E 置為低電平,然后將指令數據送到數據口 D0~D7,延時 tsp1,讓 LCD1602 準備接收數據,這時候將 E 拉高,產生一個上升沿,這時候數據就開始寫入 LCD1602,延時一段時間,將 E 置為低電平。
對比一下可以發現,寫指令和寫數據在時序上只是 RS 的不同,寫指令 RS=0,寫數據 RS=1
- 時序時間參數如下:?
??LCD1602指令
指令1 | 清顯示,指令碼 01H,光標復位到地址 00H 位置。 |
指令2 | 光標復位,光標返回到地址00H。 |
指令3 | I/D:光標和顯示模式設置,光標移動方向,高電平右移,低電平左移; S:屏幕上所有文字是否左移或者右移。高電平表示有效,低電平則無效。 |
指令4 | 顯示開關控制。 ? D:控制整體顯示的開與關,高電平表示開顯示,低電平表示關顯示; ? C:控制光標的開與關,高電平表示有光標,低電平表示無光標; ? B:控制光標是否閃爍,高電平閃爍,低電平不閃爍。 |
指令5 | S/C:高電平時移動顯示的文字,低電平時移動光標; ?R/L:高電平時右移,低電平時左移。 |
指令6 | 功能設置命令 DL:高電平時為4位總線,低電平時為8位總線; ? N:低電平時為單行顯示,高電平時雙行顯示; ? F:低電平時顯示5x7的點陣字符,高電平時顯示5x10的點陣字符。 |
指令7 | 字符發生器RAM地址設置。 |
指令8 | DDRAM地址設置。 |
指令9 | 讀忙信號和光標地址,BF:為忙標志位,高電平表示忙,此時模塊不能接收命令或者數據,如果為低電平表示不忙。 |
指令10 | 寫數據。 |
指令11 | 讀數據。 |
??LCD1602顯存地址
- 寫入地址:
?寫入顯示地址時要求最高位 D7 為高電平(1):
- 寫入第一行第一個字符的地址:
00000000B(00H)(1行1列)+ 10000000B(80H)(最高位置1) = 10000000B(80H)。
- 寫入第二行第一個字符的地址:
01000000B(40H)(1行2列)+ 10000000B(80H)(最高位置1) = 11000000B(C0H)
- 寫入字符
?字符表如下,和 ASCII 碼相似:
例:數字1 = 高八位(0011)+低八位(xxxx0001) = 00110001(對應十進制ASCII表)
LCD1602啟動流程?
小實驗:LCD1602顯示內容
實驗目的 :
- 使用LCD1602顯示一個字符;
- 使用LCD1602顯示一個字符串。
硬件清單:
LCD1602 | 上官二號 | ST-Link | 電阻(可以不用) |
硬件接線:
LCD1602 | STM32 |
VSS | GND |
VDD | 5V |
V0 | GND |
RS | B1 |
RW | B2 |
E | B10 |
D0 | A0 |
D1 | A1 |
D2 | A2 |
D3 | A3 |
D4 | A4 |
D5 | A5 |
D6 | A6 |
D7 | A7 |
A | 3.3V |
K | GND |
-
實驗1:LCD1602顯示一個字符?
- lcd1602.c文件代碼:
步驟:
- 初始化lcd的函數:初始化GPIO口函數 和 初始化啟動過程函數;
- 根據 時序圖 編寫 寫指令 和 寫數據 的函數;(使用宏定義更加的方便,簡潔)
- 根據 寫地址 和 寫數據 的原理:編寫一個寫字符的函數。
#include "lcd.h"
#include "delay.h"//進行RS寄存器宏定義
#define RS_GPIO_Poter GPIOB
#define RS_GPIO_PIN GPIO_PIN_1
#define RS_LOW HAL_GPIO_WritePin(RS_GPIO_Poter,RS_GPIO_PIN,GPIO_PIN_RESET)
#define RS_HIGH HAL_GPIO_WritePin(RS_GPIO_Poter,RS_GPIO_PIN,GPIO_PIN_SET)//進行RW寄存器進行宏定義
#define RW_GPIO_Poter GPIOB
#define RW_GPIO_PIN GPIO_PIN_2
#define RW_LOW HAL_GPIO_WritePin(RW_GPIO_Poter,RW_GPIO_PIN,GPIO_PIN_RESET)
#define RW_HIGH HAL_GPIO_WritePin(RW_GPIO_Poter,RW_GPIO_PIN,GPIO_PIN_SET)//對E寄存器進行宏定義
#define E_GPIO_Poter GPIOB
#define E_GPIO_PIN GPIO_PIN_10
#define E_HIGH HAL_GPIO_WritePin(GPIOB,GPIO_PIN_10,GPIO_PIN_SET)
#define E_LOW HAL_GPIO_WritePin(GPIOB,GPIO_PIN_10,GPIO_PIN_RESET)void lcd_init(void){//初始化GPIO口lcd_gpio_init();//(開機)上電的初始化lcd_start_init();
}
//上電的初始化
void lcd_gpio_init(void){__HAL_RCC_GPIOA_CLK_ENABLE();__HAL_RCC_GPIOB_CLK_ENABLE();GPIO_InitTypeDef GPIO_Initstruct;GPIO_Initstruct.Pin = GPIO_PIN_1|GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_0|GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;GPIO_Initstruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_Initstruct.Pull = GPIO_PULLUP;GPIO_Initstruct.Speed = GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init(GPIOA,&GPIO_Initstruct);GPIO_Initstruct.Pin = RW_GPIO_PIN|RS_GPIO_PIN|E_GPIO_PIN;GPIO_Initstruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_Initstruct.Pull = GPIO_PULLUP;GPIO_Initstruct.Speed = GPIO_SPEED_FREQ_HIGH;HAL_GPIO_Init(GPIOB,&GPIO_Initstruct);}
//啟動初始化
void lcd_start_init(void){//延時15msdelay_ms(15);//寫指令38H(不檢測忙信號)lcd_write_cmd(0x38);//延時5msdelay_ms(5);//檢測忙信號//寫指令38H:顯示模式設置lcd_write_cmd(0x38);//寫指令08H:顯示關閉lcd_write_cmd(0x08);//寫指令01H:顯示清屏lcd_write_cmd(0x01);//寫指令06H:顯示光標移動設置lcd_write_cmd(0x06);//寫指令0CH:顯示開及光標設置lcd_write_cmd(0x0C);
}//根據時序圖來完成寫指令的函數
void lcd_write_cmd(char cmd){RS_LOW; //寫指令RW_LOW; //寫E_LOW; //將E置低電平GPIOA->ODR = cmd; //將指令寫入到GPIOA的輸出寄存器里的的低八位中delay_ms(5); //延時tsp2E_HIGH; //E置高電平delay_ms(5); //延時tpwE_LOW; //將E置低電平
}//根據時序圖來完成寫數據的函數
void lcd_write_date(char data){RS_HIGH; //寫數據RW_LOW; //寫E_LOW; //將E置低電平GPIOA->ODR = data; //將指令寫入到GPIOA的輸出寄存器里的的低八位中delay_ms(5); //延時tsp2E_HIGH; //E置高電平delay_ms(5); //延時tpwE_LOW; //將E置低電平
}//顯示一個字符的函數
void lcd_show_char(int colum,char data){//在哪里顯示?lcd_write_cmd(0x80+colum);//顯示內容?lcd_write_date(data);}
- lcd.h文件代碼:
#ifndef __LCD_H__
#define __LCD_H__
#include "stm32f1xx.h"void lcd_init(void);
void lcd_gpio_init(void);
void lcd_start_init(void);
void lcd_write_cmd(char cmd);
void lcd_write_date(char dataShow);void lcd_show_char(int colum,char data);#endif
- main.c文件代碼:
#include "sys.h"
#include "led.h"
#include "delay.h"
#include "lcd.h"int main(void)
{HAL_Init(); /* 初始化HAL庫 */stm32_clock_init(RCC_PLL_MUL9); /* 設置時鐘, 72Mhz */led_init(); /* LED初始化 */lcd_init();while(1){ lcd_show_char(0x0E,'x');}
}
實驗2:lcd顯示一個字符串
- 基本流程和上述顯示一個字符一樣;區別在lcd.c文件中顯示字符函數:
- mian.c文件代碼:
#include "sys.h"
#include "led.h"
#include "delay.h"
#include "lcd.h"int main(void)
{HAL_Init(); /* 初始化HAL庫 */stm32_clock_init(RCC_PLL_MUL9); /* 設置時鐘, 72Mhz */led_init(); /* LED初始化 */lcd_init();while(1){ lcd_show_string(1,0,"xys!!!");lcd_show_string(2,0,"good!!!");}
}
?出現的問題和改正:
- LCD屏幕第一行有黑塊
????????可能的問題:接線問題,沒燒錄代碼。
- 利用for循環遍歷字符串中的字符時,出現只打印一半的的情況
?輸出的結果:(不完整)
原因:
- 在于循環條件中錯誤地使用了動態變化的 strlen(string);
- 每次循環迭代時,string 指針遞增,導致 strlen(string) 的值逐漸減少,最終提前終止循環。
- ?例如,初始字符串長度為 11,但隨著指針后移,后續的 strlen 會依次返回 10、9、8…當循環變量 i 超過當前剩余長度時,循環終止,導致只打印部分字符。
解決辦法:預先計算字符串長度并保存在變量中,循環時使用固定的長度值。
?完結項目
狀態機?
定義:狀態機(State Machine)是一種用于描述系統行為或功能行為的數學模型。它通常包含一組狀態、一組轉換條件以及動作執行。狀態機通過在不同狀態之間進行轉換來模擬系統的行為。
主要特點:?
- ?有限狀態:狀態機通常具有有限數量的狀態。這些狀態可以是有序的、離散的或層次化的。
- 轉換條件:狀態之間的轉換是基于特定條件觸發的。當滿足某個條件時,狀態機會從當前狀態轉換到下一個狀態。
- ?動作執行:在狀態轉換過程中,狀態機可能會執行某些動作或操作。這些動作可以包括計算、數據更新、輸出信號等。
- 確定性和非確定性:狀態機可以是確定性的(每個條件唯一對應一個轉換)或非確定性的(一個條件可能導致多個可能的轉換)。
實現方式:狀態機的實現方式多種多樣,可以使用編程語言中的條件語句、循環結構或專門的狀態機庫來實現。此外,硬件設計領域中的有限狀態機(Finite State Machine, FSM)也是狀態機的一種重要應用。
舉例:
while(1)
{if(state == 開心){KTV();擼串();if(女朋友跟人跑了)state = 郁悶;}else if(state == 郁悶){抽煙();嫩模();if(交新女朋友了)state = 開心;else if(刷到良許直播)state = 打雞血;}else if(state == 打雞血){寫bug();改bug();if(成功入行了)state = 開心;}
}
本項目的狀態機?
項目框圖
?硬件接線
執行代碼
- tasks.c文件代碼:
在模擬系統中,采用多線程,來完成狀態的編寫,寫到滴答定時器的中斷服務函數中。
步驟:
- 聲明一個滴答定時器的回調函數(寫服務代碼);
- 服務代碼:狀態機的狀態(是否允許通行)、要執行的行為(led燈的亮滅、繼電器的開閉、蜂鳴器的響、lcd顯示字符串)、自動轉化條件(3s之后狀態的轉換);
- 聲明一個執行條件的函數:當通過5個人的時候,條件轉化(當有人通過時計數器計次,利用lcd顯示屏顯示當前的計數的次數;超過5人后狀態轉化)。
#include "tasks.h"
#include "stm32f1xx.h"
#include "led.h"
#include "beep.h"
#include "gate.h"
#include "lcd.h"
#include "exti.h"
#include "stdio.h"//定義一個枚舉來聲明狀態機的狀態
enum{PASS_STATE, //注意:這里有個逗號WAIT_STATE
}; //這里有個分號uint32_t led1_cnt = 0;
uint32_t led2_cnt = 0;
uint32_t wait_cnt = 0;uint8_t led1_task_flag = 0;
uint8_t led2_task_flag = 0;uint8_t state = PASS_STATE; //定義一個狀態:通過和不通過void systick_it(void){ //回調函數//如果處于允許通行的狀態if(state == PASS_STATE){//LED1以1s的頻率閃爍if(led1_cnt < 1000)led1_cnt ++;else{led1_toggle();led1_cnt = 0;}//LED2不閃led2_off();//蜂鳴器不響beep_off();//繼電器打開gate_on();
// lcd_show_string(1,1,"hello");}//如果處于不允許通行的狀態else if(state == WAIT_STATE){//LED1不閃,LED2以500ms的頻率閃爍led1_off();if(led2_cnt < 500){led2_cnt ++;}else{led2_toggle();led2_cnt = 0;}//蜂鳴器響beep_on();//繼電器關閉gate_off();//計時3s之后if(wait_cnt < 5000) // 5swait_cnt ++;else{wait_cnt = 0;//進入允許通行的狀態state = PASS_STATE; //LCD顯示狀態lcd_show_string(1,3,"...PASS...");}}
}//在主函數中調用實現檢測人數
uint32_t passager = 0;
void exti_task(void){//檢測有人通過if(ia_flag_get() == True && state == PASS_STATE){passager ++;//在lcd顯示屏上顯示當前的計數人數char message[16]; //創建一個字符數組,存儲字符串sprintf(message,"PASS...%02d/05",passager); //將字符串打印到字符數組中lcd_show_string(1,1,message);}//如果通過的人數超過5個if(passager >= 5){passager = 0;//進入不允許通行的狀態state = WAIT_STATE;//lcd顯示當前的狀態lcd_show_string(1,1,"....WAIT....");}
}
- main.c的文件代碼:
#include "sys.h"
#include "led.h"
#include "delay.h"
#include "tasks.h"
#include "gate.h"
#include "exti.h"
#include "lcd.h"
#include "beep.h"int main(void)
{HAL_Init(); /* 初始化HAL庫 */stm32_clock_init(RCC_PLL_MUL9); /* 設置時鐘, 72Mhz ,將外部HSE倍頻到72Mhz*/ led_init(); /* LED初始化 */gate_init();exti_init();lcd_init();beep_init();while(1){ exti_task();}
}
本項目的新知識的總結:
- 利用systick模擬操作系統,實現多線程的編程;
- 提前將避障紅外模塊,繼電器,蜂鳴器,led燈的代碼寫好;
- 利用枚舉來定義狀態機的狀態,相比宏函數更加的方便;
- 利用 &&運算符,進行兩個條件的限制;