四、SysTick+(红外传感器+LCD1602显示器)模块+智能排队系统项目
2026/8/19 21:11:37 网站建设 项目流程

1. SysTick介绍

1.1什么是SysTick

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

Systick定时器的主要功能包括实现简单的延时、生成定时中断以及进行精确定时和周期定时操作。此外, Systick定时器还可以被用于其他目的,例如作为操作系统的时基(如FreeRTOS),或者用于软件看门狗等系统调度操作。在STM32中,Systick通常以HCLKAHB时钟)或HCLK/8作为运行时钟。

1.2 SysTick工作原理

在使用Systick定时器进行延时操作时,可以设定初值并使能后,每经过一个系统时钟周期,计数值就减1。 当计数到0时,Systick计数器自动重装初值并继续计数,同时内部的COUNTFLAG标志会置位,触发中断 (如果中断使能)。这样,可以在中断处理函数中实现特定的延时逻辑。

时钟来源是HCLK(72M),之后一分频或者八分频,VAL递减计数器,递减到0之后countflag置1(触发中断),重装载值再次进入到VAL中进行下一轮计数。

2. SysTick寄存器介绍

SysTick控制及状态寄存器(CTRL)

TICKINT:会不会产生中断的一个标志位。

ENABLE:置1时计时器开始工作。

SysTick重装载数值寄存器(LOAD)

SysTick当前数值寄存器(VAL):递减计数器

3.手撸延时函数

3.1HAL_Delay底层实验原理

随便打开我们之前做过的一个程序,发现我们之前用的延时函数都是delay_ms()延时函数。我们F12进去。

这个函数实际上是我们自己编写的(中文的注释),而不是ST公司自带的。 当然ST公司也给了延时函数。weak类型的,可以有我们进行修改重写。

那我们为什么还要进行其他的延时函数编写呢? 是因为ST公司自带的只能实现毫秒级别的延时,而我们有许多时候要求微秒的延时级别。

我们现在看HAL_Delay是如何用systick实现的。选中HAL_Delay ,依次ctrl c+f+v。

这里就是实现的一个底层代码。

3.2手撸原理

首先我们看delay.c中的函数 有delay_us、delay_ms、delay_s。单位直接都是1000的进制关系,我们只需写出us级别的延时函数,需要用到其他单位的时候,我们就进行while循环即可。

我们上文说了systick的三种寄存器,都需要我们来进行配置。

VAL--当前计数器的值

LOAD--重装载值--我们要计多少个数

countflag--延时完成 还有要进行几分频。

理一下思路:

1.确定要计多少个数 2.清空当前的计数 3.配置分频系数 4.开启定时器

1.之前说过滴答定时器的时钟来源是HCLK(72M、1秒会数72M个数)1s=1000ms=1Mμs。对应就是1μs读了72个数。所以72*nμs个数需要读,存放LOAD寄存器。SysTick->LOAD = 72 * nμs;

2.清除掉之前有可能残留下来的值,SysTick->VAL = 0x00

3.在CTRL中第2位段进行分频,SysTick->CTRL|=1 << 2;(用或等的方式吧某一位 置1,1左移两位,从第0位到第二位,或等保证了怎么移 都会使第二位置1)

4.开启定时器在CTRL中第0位段,SysTick->CTRL|=1 << 0;(1左移0位)

当计数完成后 countflag将置为1。在CTRL第16位段,SysTick->CTRL|=1 << 16.

在while循环的条件中,temp & 0x01(使能位一直为1),后者temp&(1<<16) 。我们要当countflag位置1时,该退出while循环,所以取个反!。当计数未结束时 后者取0 加个反为1,再和前者1 与的关系,while循环继续。计数结束时后者为1 取反为0,再与前者1 取与 总结果为0 ,退出while循环。

结束之后 ,我们该关闭定时器,使ENABLE那位至0。SysTick->CTRL&=~(1<<0);(使用与等,置第0位为0,因为0与任何位与 都是0)。

4.手撸带操作系统的延时函数

通常的滴答定时器会进行频繁的启动和关闭。但在操作系统(如FreeRTOS)中作为时基。就不能频繁的启动关闭。

while循环结束的条件就是tcnt(已经计的数)>ticks(需要计多少数)。通过不断的读取二者的值进行判断(递减计数器VAL左----右),由于读取间隔的问题,可能会出现图左下方的两种情况。在流程图中也有体现其分别的计算方法。

可以按照流程图的顺序来写。

小实验:SysTick模拟多线程(较难)

实验目的

使用SysTick模拟多线程,实现流水灯实验。

硬件清单

上官二号

ST-Link

像之前我们的流水灯实验,是单线程的。

一般我们要进行多线程,就要是移植一个操作系统,比如RTOS,但如果不移植的情况下,如何模拟一个多线程。就要用到systick(定时器)。现在我们基于这个实验,带操作系统的一个延时函数,做一个复制。

此处为systick的中断服务函数。

task.c的内容,在此之前要在BSP文件夹中分别新建task.c.h文件,然后在“品”和魔法棒中添加对应文件和头文件。

task.h 声明包含.c中的函数名称。

主函数

HAL_Init()打开并配置了 SysTick 定时器后,硬件每 1ms 产生一次中断;CPU 被打断后自动执行SysTick_Handler/systick_isr,执行完再回到mainwhile(1)

模块:红外传感器

红外跟随避障模块详解 | 良许嵌入式

如果红外接收管接收到了红外线,OUT引脚就输出一个低电平,否则就一直保持高电平。

原理跟振动传感器相似。

小实验:红外点灯

实验目的

用手遮挡红外传感器,LED1点亮2秒,之后熄灭。

硬件清单

红外传感器

上官二号

ST-Link

硬件接线

对应的.h文件声明也要修改。

模块:LCD1602显示器

LCD1602显示模块详解 | 良许嵌入式

LCD1602( Liquid Crystal Display 1602),一种常见的字符型液晶显示模块。虽然它接线多,但是LCD1602是每个嵌入式工程师的必经之路。它能够显示16列2行,共32个字符字符,每个字符都由5x8像素点阵构成,是一种专门用来显示字母、数字、符号等的液晶显示模块。

LCD1602 的基本的操作分为读操作写操作

其中读操作可以分为读状态和读数据;写操作可以分为写指令和写数据。

读状态:输入 RS=0、RW=1、E=1;输出:D0~7 读出为状态字;

读数据:输入 RS=1、RW=1、E=1;输出:D0~7 读出为数据;

写指令:输入 RS=0、RW=0、E=上升沿;写入控制指令;

写数据:输入 RS=1、RW=0、E=上升沿;写入需要显示的字符。

LCD1602 的读写操作、屏幕和光标的操作都是通过指令编程来实现的。

LCD1602 内部的控制器共有11条控制指令,如表所示:

指令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:读数据。

要在屏上显示,有两个部分需要确定,首先是显示的地址,其次是显示的内容。

显示的地址的设定呢,是由指令8(DDRAM地址)来确定的。我们可以看到D7的位置始终是1。

LCD1602 的控制器内置有 80 个 byte 的显存, 而 LCD1602 只有两行 × 16 个字符的显示区域,所以 显存中有些地址是无法对应上 LCD 屏的,下图为显存地址对应图:

+80H(0x80)不是数学上随便加个数字,而是在告诉 LCD:“这是一条‘设置地址’指令”。

+80H= 给地址命令加上“标志位 D7=1”,让控制器认出这是“设置显示地址”,而不是普通数据或其它指令。

接下来就是显示的内容。

这个模块的显示的内容是有限的,只能显示对应字符表中的内容。

例如我们要显示1,对应的输入就是00110001,对应的十进制数是49,对应的ASCII码就是1。

实际上是于ASCII码是一一对应的,所以在后续编写代码时,直接把对应的字符直接输入进去即可

LCD1602呢有一个初始化的流程,那么我们在写程序的时候呢也要把这个初始化考虑进去。

小实验:驱动LCD1602显示内容

实验目的

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

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

硬件清单

LCD1602

上官二号

ST-Link

硬件接线

首先我们还是要在Drivers-->BSP中建立对应的文件夹其中都包括对应的.c和.h文件。

依旧是老规矩,在“品”中添加对应的文件。和在魔法棒中的C/C++中添加对应的头文件。

这些都是必要的步骤。

之后来编写代码,之后编译,lcd1602.h将会出现。

在lcd1602.c中,先构想一下到底要写一些什么函数,首先每个模块都是有一个初始化init函数的,所以,接下来还要有对应的读和写,在讲时序图的时候说过,要让LCD1602显示什么内容的话,要首先把数据发送过去。那么写,包含写数据和写命令

既然有写,肯定要有一个读read。但在LCD 1602模块里,读数据寄存器用处不是非常大。所以一般来讲,我们不会读它的数据寄存器,或者说指令寄存器的,所以我们读read这种相关函数的话,就省略的不写了。

到这儿,我们初始化的部分就结束了。接下来进行写指令和写数据的函数封装。在时序图中我们也看过,写数据和写指令的区别就是RS这个引脚是置0还是1来选择寄存器,0就是指令寄存器,1就是数据寄存器。

那么在这个时序图的过程中呢,我们需要不断去设置引脚的状态,那我们就需要用到HAL_GPIO_WritePin这个函数,经常写这个函数的话就会变得很不方便,所以我们进行进一步的通过宏定义来进行封装。

通过时序图,写出写指令的代码。RS-0,RW-0。

Output Data Register(输出数据寄存器)

GPIOA->ODR = cmd= 把命令字节并行输出到 PA0~PA7,作为 LCD 的 低 8 位数据线电平。

把指令字节cmd的 8 位,一次性写到 GPIOA 的输出数据寄存器ODR里,从而出现在 PA0-PA7 引脚上,交给 LCD 的 DB0-DB7。

同样的,写数据的代码类似。

显示的地址和内容 show。

在我们的.h文件中全部进行一个声明。

main

现在是显示一个字符,接下来我们看看如何显示字符串。

char *string= 传入一个“字符指针”,指向字符串的首地址。

项目:智能排队控制系统

项目需求

1.红外传感器检测有人通过并计数;

2.计数值显示在LCD1602

3.允许通过时,LED1闪烁,蜂鸣器不响,继电器不闭合;

4.不允许通过时,LED2闪烁,蜂鸣器响,继电器闭合;

5.每次允许通过5个人,之后转为不允许通过,3秒后再转为允许通过

硬件清单继电器(模拟匣机)

蜂鸣器

红外避障模块

LCD1602

上官二号

ST-Link

硬件接线

先把项目工程的框架搭起来。这个项目的需求是稍微复杂的,所以我们会用到systick去模拟一个操作系统。在实验12的基础上进行修改。之后根据框图中所用到的模块,在Drivers->BSP中建立对应的文件夹。

之后在“品”中添加各个的.c文件,在魔法棒中C/C++添加头文件。

由于之前我们的继电器的实验的命名是alarm,在进入之后要将alarm换成gate。

可以ctrl+f,找到replace进行替换。.h文件开头处的为大写,需要手动敲一下。

gate继电器的引脚变成了GPIOB B6,所以在.c文件中要进行一个改动。

之后依次检查各个模块是否正常,编译运行看继电器是否正常。

接下来看LED模块是否正常。

beef蜂鸣器,我们此次实验接的是B5,所以对应的引脚代码也要修改一下。之后main中运行一下看是否正常。#include "beep.h"别忘加上。和beep_init()。

之后就是我们的红外传感器 对应的是exti.c。接到的是B4,所以引脚还是要确定一下。之后测试一下。

#include "exti.h"和exti_init()。

#include "lcd1602.h",lcd1602_init();

目前确保了模块和接线是没有问题的,后续就可以专注在逻辑上面的编写了。

具体项目在下面说完状态机的知识后继续。

状态机

状态机(State Machine是一种用于描述系统行为或功能行为的数学模型。它通常包含一组状态、一组转换条件以及动作执行。状态机通过在不同状态之间进行转换来模拟系统的行为。每个状态都代表系统的一个特定配置或情况,而转换规则则定义了从一个状态转换到另一个状态的条件和动作。

状态机广泛应用于多个领域,包括但不限于计算机科学、电子工程、通信、自动化和机器人技术等。在计算机科学中,状态机常用于描述软件系统的行为,特别是在硬件设计、编译器设计、网络协议以及用户界面交互等方面。

状态机的主要特点包括:

1.有限状态:状态机通常具有有限数量的状态。这些状态可以是有序的、离散的或层次化的。

2.转换条件:状态之间的转换是基于特定条件触发的。当满足某个条件时,状态机会从当前状态转换到下一个状态。

3.动作执行:在状态转换过程中,状态机可能会执行某些动作或操作。这些动作可以包括计算、数据更新、输出信号等。

4.确定性和非确定性:状态机可以是确定性的(每个条件唯一对应一个转换)或非确定性的(一个条件可能导致多个可能的转换)。

状态机的实现方式多种多样,可以使用编程语言中的条件语句、循环结构或专门的状态机库来实现。此外,硬件设计领域中的有限状态机(Finite State Machine, FSM)也是状态机的一种重要应用。

图中所演示的状态不多,如果以后状态多的话可以使用switch case语句。

switch (表达式) {
case 常量1:
语句块1;
break;
case 常量2:
语句块2;
break;
// ... 可以有多个case
default: // 可选,所有case都不匹配时执行
默认语句块;
break;
}

项目具体的实现

由于我们要用到systick模拟操作系统,来构建一个多任务的调度框架。

从底层原理看,它做了三件事:

  • 产生固定时间中断:配置SysTick定时器,让它每间隔固定时间(例如1ms)产生一次中断。

  • 充当“时间基准”:在中断服务函数中,对一个全局变量(如Tick)进行累加,这个变量就是整个模拟系统的“逻辑时间”,所有延时、超时都基于它。

  • 实现“任务切换”:在中断中,或者在主循环中检测时间到,然后保存当前任务的状态(寄存器),恢复下一个任务的寄存器,让CPU去执行另一个任务。这模拟了操作系统的上下文切换。

所以我们的项目代码呢,主要写在task.c当中。原本我们的task.c当中也有一部分内容,有两个task,其中task1是以1秒的速度将LED1进行翻转,task2是500毫秒的频率将LED2进行一个翻转。为了方便区分,我们将名字进行一个改动,将task1->led1_task,task2->led2_task。这样命名就很清晰,一下就可以看出哪个task是干嘛用的。包括最上方变量的定义和代码的其他地方也要同步改过来。编译一下出现task.h后,其中的也不要忘了改。

之后在main函数中测试一下,可以看到LED1,2按给定频率闪烁的现象。

用上我们上面说的状态机的知识,那么首先要进行状态变量的定义。一般使用枚举enum来将每个变量依次枚举出来。 在我们的项目中 有两个状态分别是允许通行和不允许。之后通过systick的回调函数来检测状态。

在我们写继续的代码的时候,最好是有一个流程图,把整个思路理清楚。通过注释一步步的去编写代码。

要调用sprintf要包含#include "stdio.h"这个头文件。

sprintf的标准用法就是:把格式化后的字符串写进一个字符数组里,不打印到屏幕。

嵌入式里 LCD 不能直接printf,所以先sprintf到缓冲区,再拿message去显示。

必须保证数组够大

sprintf不会检查message够不够长,写超了会踩内存。

这句最多写出:

PASS...05/05 \0

12个字符 + 结束符 '\0' = 13 字节

char message[16]16个字节装得下。规范写法更推荐:

sprintf(message, sizeof(message), "PASS...%02d/05", passenger);

snprintf会限制最多写sizeof(message)个字节,避免溢出。教学/小项目里sprintf很常见,知道这个风险即可。

sprintf(message, "PASS...%02d/05", passenger);

参数作用

message

结果写到这里

"PASS...%02d/05"

模板:固定文字 + 占位符

passenger

填进%02d的那个数

%02d是格式说明符,从右往左读最清楚:

部分含义

%

这里要替换成变量

d

按有符号十进制整数打印(对应int/uint32_t这类整数)

2

至少占 2 位

0

位数不够时左边补 0

所以:

passenger%d%2d%02d

3

3

3(前面空格)

03

12

12

12

12

这里用%02d,是为了和后面的/05对齐。

常见占位符(顺带记)

%d // 整数 sprintf(buf, "%d", 3); → "3"

%02d // 两位、不足补0 sprintf(buf, "%02d", 3); → "03"

%s // 字符串 sprintf(buf, "hi %s", "stm"); → "hi stm"

%f // 浮点 sprintf(buf, "%.1f", 3.14); → "3.1"

%x // 十六进制 sprintf(buf, "%02X", 10); → "0A"

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询