STM32的SysTick

SysTick介绍

定义:Systick,即滴答定时器,是内核中的一个特殊定时器,用于提供系统级的定时服务。该定时器是一个24位的递减计数器,具有自动重载值寄存器的功能。当计数器到达自动重载值时,它会自动重新加载并开始新的计数周期。

滴答定时器的主要功能

实现简单的 延时
操作系统的时基(如 FreeRTOS
生成定时中断以及进行精确定时周期定时操作
软件看门狗等系统调度操作
  • STM32中,Systick通常以HCLKAHB时钟)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.确定要数多少个数72
    SysTick->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?1
    do{
        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;
            else
                tcnt += relode -(tnow - told);

            told = tnow;
            
            if(tcnt > ticks)
                break; 
        } 
    }
}
  •  毫秒和秒的函数和不带操作系统时的函数相同。

项目:智能排队控制系统 

项目需求

  1.  红外传感器检测有人通过并计数;
  2.  计数值显示在LCD1602;
  3.  允许通过时,LED1闪烁,蜂鸣器不响,继电器不闭合;
  4.  不允许通过时,LED2闪烁,蜂鸣器响,继电器闭合;
  5.  每次允许通过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模拟多线程存在时间差,不是完全的实时操作系统,只能实现简单的任务。

 模块:红外跟随避障

模块介绍

        在智能车、机器人和自动化等领域避障技术是确保安全和高效运行的关键。红外避障模块作为一种常见的避障解决方案,因其非接触、响应速度快和抗干扰能力强等优点而备受青睐。本文将详细介绍红外避障模块的特点、工作原理、以及应用案例,帮助您更好地了解这一技术。

工作原理:

红外避障模块不断发射红外信号,当红外信号

  1. 有反射回来,OUT 输出低电平,输出指示灯(绿灯)亮。
  2. 没反射回来,OUT 输出高电平,输出指示灯(绿灯)灭。

        红外避障模块上的一对红外线发射与接收管,发射管发射出一定频率的红外线,当检测方向遇到障碍物时,红外线反射回来被接收管接收,经过比较器(LM393)电路处理之后,信号输出接口输出低电平信号,同时绿色指示灯会亮起。

         因为黑色能够吸收红外线(红外线不反射),而白色不行(红外线反射),所以除了避障外可用作黑白线循迹、光电开关等等。

常见的用途:

  1. 机器人避障;
  2. 小车避障、跟随;
  3. 流水线计数;
  4. 黑白线循迹。

工作参数及引脚介绍

  • 工作参数:
工作电压DC 3.3-5V
工作温度-10°C ~ +50°C
检测角度35°
检测距离2 ~ 30 CM可调(不同厂家略有差异),距离越近性能越稳定。
灵敏度模块中蓝色的电位器用于调节灵敏度,顺时针旋转,灵敏度变高,检测距离变长;逆时针越小,灵敏度变低,检测距离变短。
  • 接线如下:
红外避障模块STM32备注
VCC3.3/5V电源正极
GNDGND电源负极
OUT任意GPIO口数字输出

 小实验:红外点灯

  • 实验目的:用手遮挡红外传感器,LED1点亮2s,之后熄灭。
  • 硬件清单:

红外传感器

上官二号

ST-Link

  • 硬件接线:
STM32红外避障模块
5vVCC
GGND
GPIO口:PB4OUT
  • 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_PIN0
        if(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;第二次调用,返回:False
    vibrate_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  0

void 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

工作参数和引脚介绍 

  • 工作参数
    工作电压工作电流工作温度设备寿命
    5V2.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地址设置。
指令8DDRAM地址设置。
指令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显示内容

实验目的 :

  1. 使用LCD1602显示一个字符;
  2. 使用LCD1602显示一个字符串。

硬件清单:

LCD1602上官二号ST-Link电阻(可以不用)

硬件接线:

LCD1602STM32
VSSGND
VDD5V
V0GND
RSB1
RWB2
EB10
D0A0
D1A1
D2A2
D3A3
D4A4
D5A5
D6A6
D7A7
A3.3V
KGND
  • 实验1:LCD1602显示一个字符 

  • lcd1602.c文件代码:

步骤:

  1. 初始化lcd的函数初始化GPIO口函数 初始化启动过程函数;
  2. 根据 时序图 编写 写指令写数据 的函数;(使用宏定义更加的方便,简洁)
  3. 根据 写地址 和 写数据 的原理:编写一个写字符的函数。
#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){
    //延时15ms
    delay_ms(15);
    //写指令38H(不检测忙信号)
    lcd_write_cmd(0x38);
    //延时5ms
    delay_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);        //延时tsp2
    E_HIGH;             //E置高电平
    delay_ms(5);        //延时tpw
    E_LOW;              //将E置低电平
}

//根据时序图来完成写数据的函数
void lcd_write_date(char data){
    RS_HIGH;             //写数据
    RW_LOW;              //写
    E_LOW;               //将E置低电平
    GPIOA->ODR = data;   //将指令写入到GPIOA的输出寄存器里的的低八位中
    delay_ms(5);         //延时tsp2
    E_HIGH;              //E置高电平
    delay_ms(5);         //延时tpw
    E_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)是一种用于描述系统行为或功能行为的数学模型。它通常包含一组状态、一组转换条件以及动作执行。状态机通过在不同状态之间进行转换来模拟系统的行为。

主要特点: 

  1.  有限状态:状态机通常具有有限数量的状态。这些状态可以是有序的、离散的或层次化的。
  2. 转换条件:状态之间的转换是基于特定条件触发的。当满足某个条件时,状态机会从当前状态转换到下一个状态。
  3.  动作执行:在状态转换过程中,状态机可能会执行某些动作或操作。这些动作可以包括计算、数据更新、输出信号等。
  4. 确定性和非确定性:状态机可以是确定性的(每个条件唯一对应一个转换)或非确定性的(一个条件可能导致多个可能的转换)。

实现方式:状态机的实现方式多种多样,可以使用编程语言中的条件语句、循环结构或专门的状态机库来实现。此外,硬件设计领域中的有限状态机(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)    // 5s
            wait_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灯的代码写好;
  • 利用枚举来定义状态机的状态,相比宏函数更加的方便;
  • 利用 &&运算符,进行两个条件的限制;

### STM32 SysTick 定时器使用教程 #### 一、SysTick定时器概述 SysTick定时器是一个简单易用的倒计数定时器,集成于ARM Cortex-M系列处理器内核之中。该定时器能够提供系统级的时间管理功能,在操作系统调度、任务延迟等方面有着广泛应用[^1]。 #### 二、主要特性 - **高精度**:基于核心频率工作,可以实现微秒级别的精准延时; - **低资源消耗**:仅需少量CPU干预即可完成配置与启动; - **硬件支持**:自动重装载机制使得无需软件介入就能持续触发中断事件; #### 三、寄存器结构说明 SysTick拥有四个关键寄存器来控制其行为模式: | 寄存器名称 | 功能描述 | | --- | --- | | `CTRL` (Control and Status Register) | 控制及状态设置,决定是否开启/关闭定时器及其运行方式等属性| | `LOAD` (Reload Value Register)| 设置每次溢出前的最大计数值| | `VAL`(Current Value Register) | 当前剩余未计满次数值显示| | `CALIB`(Calibration Value Register)| 提供校准参数| 其中最重要的是`CTRL`寄存器,它决定了整个定时过程的核心逻辑[^2]。 #### 四、初始化流程 为了使能并正确配置SysTick定时器,通常按照以下步骤执行: - 配置加载值(load value),即设定一次完整的计数周期长度; - 清零当前计数值(current value register),确保从头开始计算; - 启动定时器,允许产生相应的中断信号; 下面给出一段简单的C语言代码作为实例演示如何利用STM32 HAL库来进行基本的SysTick定时器编程: ```c #include "stm32f1xx_hal.h" void SystemClock_Config(void); static void MX_GPIO_Init(void); int main(void){ // 初始化HAL库 HAL_Init(); // 系统时钟配置 SystemClock_Config(); // GPIO端口初始化 MX_GPIO_Init(); /* 开启systick */ if(HAL_OK != HAL_SYSTICK_Config(SystemCoreClock / 1000)){ Error_Handler(); } while (1){ // 主程序循环体... } } ``` 上述代码片段实现了每毫秒触发一次SysTick中断的效果,这有助于构建稳定可靠的任务调度框架[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值