1. SysTick介绍
1.1什么是SysTick?
Systick,即滴答定时器,是内核中的一个特殊定时器,用于提供系统级的定时服务。该定时器是一个24位的 递减计数器,具有自动重载值寄存器的功能。当计数器到达自动重载值时,它会自动重新加载并开始新的计 数周期。
Systick定时器的主要功能包括实现简单的延时、生成定时中断以及进行精确定时和周期定时操作。此外, Systick定时器还可以被用于其他目的,例如作为操作系统的时基(如FreeRTOS),或者用于软件看门狗等系统调度操作。在STM32中,Systick通常以HCLK(AHB时钟)或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,执行完再回到main的while(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);
| 参数 | 作用 |
|---|---|
| 结果写到这里 |
| 模板:固定文字 + 占位符 |
| 填进 |
%02d是格式说明符,从右往左读最清楚:
| 部分 | 含义 |
|---|---|
| 这里要替换成变量 |
| 按有符号十进制整数打印(对应 |
| 至少占 2 位 |
| 位数不够时左边补 0 |
所以:
passenger | %d | %2d | %02d |
|---|---|---|---|
3 |
|
|
|
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"