4. 系统延时
延时功能在计算机编程中十分常见,它可以暂停程序的执行一段时间,以实现各种应用需求。
4.1 延时的作用
在MSPM0或其他微控制器的编程中,延时被广泛使用,主要有以下一些原因:
- 处理硬件:许多硬件都需要一些时间来响应某个命令。例如,如果在一个程序中你启动一个电动机然后立即检查其状态,你可能会得到一个错误的读数,因为电动机可能还没有足够的时间开始旋转。此时你需要使用 delay() 函数让系统等待一段时间,使得电动机有足够的时间响应。
- 用户交互:我们常常需要在用户交互中实现延迟效果。例如,在蜂鸣器播放音乐时,音符之间需要一段沉默的时间。或者,在闪烁LED灯的情况下,"开"和"关"状态之间需要延时以控制闪烁的速度。
- 节省能源:在一些应用中,比如电池供电的系统,如果不在需要的时候长期保持系统的高速运转,那么电池的寿命会大大缩短。在此情况下,我们可以让系统在一段时间后进入待机或低功耗模式,直到下一个处理周期到来。
- 定时操作:在许多项目中,我们常常需要实现一些特定时间点的操作。例如,在自动灌溉系统中,我们可能需要在每天的特定时间点进行灌溉。在间隔测量中,我们可能每隔一段时间采集一次数据。
尽管延时函数在很多情况下非常有用,但也需要注意其阻塞性质。过度依赖阻塞延时可能会导致程序对其他事件的响应不及时。为了更好的在MSPM0上进行多任务编程,我们还可以学习一些非阻塞延时的编程技术。
❓什么是阻塞延时?
阻塞延时是在程序执行过程中,当某个操作或函数需要一定时间才能完成时,程序会暂停执行直至该操作完成,这段时间程序被阻塞了。阻塞延时可能会导致程序运行速度变慢或出现假死现象。 举个例子,假设你想要煮开水来泡茶。通常情况下,你会将水壶放在炉灶上加热,等待水烧开后才能使用。在这个过程中,存在阻塞延时。 当你将水壶放在火上时,程序可以看作是“等待”水烧开的操作。在这个等待过程中,你不能立即得到热水来泡茶,需要耐心等待水煮沸。期间,你可能无法做其他与烧水无关的事情,因为你需要留意水壶,并等待时机。即便家里着火了,你也还是在等待烧水。
4.2 在3507中实现延时的方式
可以有很多种延时的方式,这里给大家介绍三种方法:空代码延时
、TI工程自带的延时
、使用滴答定时器
。
4.2.1 空代码延时
空代码延时(No-Operation Delay),通常简称为NOP延时,是一种在程序设计中使用的技巧,其目的是为了在代码中插入一个特定的延迟时间,而无需执行任何有意义的操作。
在硬件层面,NOP指令(No-Operation Instruction)是许多处理器指令集中的一个指令,当执行该指令时,处理器不会进行任何操作,但它会消耗一个或多个时钟周期。在软件层面,NOP延时通常用于以下几种情况:
时序控制: 在某些硬件相关的编程中,精确的时序控制非常重要。通过插入NOP指令,可以确保其他硬件操作有足够的时间来完成。
代码对齐: 在某些情况下,为了优化性能或满足硬件要求,可能需要将代码对齐到特定的边界。NOP指令可以用来填充空间,以实现这种对齐。
调试: 在调试程序时,开发者可能会插入NOP指令来暂时“冻结”程序,以便观察程序的某个特定状态。
占位符: 在开发过程中,开发者可能暂时不知道要在某个位置放置什么代码,此时可以用NOP作为占位符。
下面是一个简单的示例,展示了如何在C语言中使用空代码延时:
void delay(void)
{
for (int i = 0; i < 1000000; i++)
{
// 这里的循环体是空的,起到NOP延时的作用
}
}
2
3
4
5
6
7
在这个例子中,delay函数中的循环将执行很多次,但实际上并没有执行任何操作,除了消耗时间。这种方法的缺点是它不是精确的延时,因为它的持续时间取决于处理器的速度和其他系统负载。
在上一章节的点亮LED灯的代码中,用的就是空代码延时的方式,控制LED亮一定时间然后再灭一定时间,如此反复。
4.2.2. TI工程自带的延时
TI的工程模板中,给我们准备好了一个可以根据主控频率进行计算时间的延时函数:delay_cycles(cycles)
。
它的源码如下:
/**
* @brief Alias for DL_Common_delayCycles
*/
#define delay_cycles(cycles) DL_Common_delayCycles(cycles)
/**
* @brief Consumes the number of CPU cycles specified.
*
* @param[in] cycles Floor number of cycles to delay.
* Specifying zero will result in the maximum
* possible delay. Note that guarantees at least
* this number of cycles will be delayed,
* not that exactly this number of cycles will be
* delayed. If a more precise number of cycle delay value
* is needed, GPTimer is recommended.
*
* Typical variance from this function is 10 cycles or
* less assuming that the function is located in flash and
* that caching is enabled. Disabling caching may result in
* wait-states when fetching from flash.
* Other variance occurs due:
* - Amount of register stacking/unstacking around API entry/exit
* - Value of cycles relative to 4-cycle loop counter
* - Placement of code on a 2- or 4-byte aligned boundary
*/
void DL_Common_delayCycles(uint32_t cycles);
void DL_Common_delayCycles(uint32_t cycles)
{
/* this is a scratch register for the compiler to use */
uint32_t scratch;
/* There will be a 2 cycle delay here to fetch & decode instructions
* if branch and linking to this function */
/* Subtract 2 net cycles for constant offset: +2 cycles for entry jump,
* +2 cycles for exit, -1 cycle for a shorter loop cycle on the last loop,
* -1 for this instruction */
__asm volatile(
#ifdef __GNUC__
".syntax unified\n\t"
#endif
"SUBS %0, %[numCycles], #2; \n"
"%=: \n\t"
"SUBS %0, %0, #4; \n\t"
"NOP; \n\t"
"BHS %=b;" /* branches back to the label defined above if number > 0 */
/* Return: 2 cycles */
: "=&r"(scratch)
: [ numCycles ] "r"(cycles));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
通过汇编语言的NOP指令,通过主频计算执行代码的时间周期,去实现的延时。而时间周期TI团队也已经为大家计算好了,可以直接在工程中调用delay_cycles(主频hz);
函数即可。
例如要实现1秒的延时:
//工程中的主频默认是32MHz,如果有修改请使用修改后的主频
delay_cycles(32000000);
2
要实现1毫秒的延时:
//工程中的主频默认是32MHz,如果有修改请使用修改后的主频
delay_cycles(32000000/1000);
2
要实现1微秒的延时:
//工程中的主频默认是32MHz,如果有修改请使用修改后的主频
delay_cycles(32000000/1000/1000);
//或者使用下面这一句
delay_cycles(32000000/1000000);
2
3
4
4.2.3. 使用滴答定时器
滴答定时器是几乎所有arm内核的芯片都带有的一个功能,可以通过使用滴答定时器来实现精准延时,本章节的案例就是以滴答定时器作为模板介绍。
4.2 滴答定时器介绍
SysTick定时器可用作标准的下行计数器,是一个24位向下计数器,有自动重新装载能力,可屏蔽系统中断发生器。Cortex-M0处理器内部包含了一个简单的定时器,所有基于M0内核的控制器都带有SysTick定时器,这样就方便了程序在不同的器件之间的移植。SysTick定时器可用于操作系统,提供系统必要的时钟节拍,为操作系统的任务调度提供一个有节奏的“心跳”。正因为所有的M0内核的芯片都有Systick定时器,这在移植的时候不像普通定时器那样难以移植。
RCU 通过 MCLK 作为 Cortex 系统定时器(SysTick)的外部时钟,即使用MCLK计时,MCLK默认为32MHz。通过对 SysTick 控制和状态寄存器的设置,即可控制或读取。关于系统时钟的介绍可参考用户手册的第128页。
SysTick定时器设定初值并使能之后,每经过1个系统时钟周期,计数值就减1,减到0时,SysTick计数器自动重新装载初值并继续计数,同时内部的COUNTFLAG标志位被置位,触发中断(前提开启中断)。
4.3 滴答定时器配置
4.3.1 准备一个空白工程
将SDK路径下的G3507空白工程复制到SDK的根目录下:
必须复制到SDK的根目录下,不然无法找到文件
在Keil中打开我们复制的空白工程 empty。
设置图形化配置文件保存路径:
源:
cmd.exe /C "$P../../../../../../tools/keil/syscfg.bat '$P' empty.syscfg"
改:
cmd.exe /C "$P../../tools/keil/syscfg.bat '$P' empty.syscfg"
设置工程头文件路径:
改为:
设置工程链接器:
改为:
../../source/ti/driverlib/lib/keil/m0p/mspm0g1x0x_g3x0x/driverlib.a
完成之后编译,可以看到没有报错了。
4.3.2 开启SYSCONFIG配置工具
在Keil的主界面打开empty.syscfg文件,在empty.syscfg文件打开的情况下,再打开SYSCONFIG的GUI界面。
在sysconfig中,左侧可以选择MCU的外设,我们找到并点击SYSTICK选项卡,在SYSTICK中点击ADD,就可以添加滴答定时器。
4.3.3 配置滴答定时器
滴答定时器是使用MCLK计时,而MCLK来源与主频的32MHz,所以每计一次数花费的时间为 1÷32,000,000 = 0.00000003125 S. 假设我们设置要计数的值为32000,则计32000个数会花费的时间为:32000 x 0.00000003125 = 0.001 S = 1MS. 所以如果我们要设置1ms的滴答定时器就设置滴答定时器的计数值为32000。案例中我们开启了中断,关于中断的概念在后面再说。大概的意思就是中断其他任务,先执行滴答定时器的内容。
将以上配置保存,然后到Keil中编译更新。
4.3.4 其他配置
这里我们需要一个LED灯来展示延时的状态,通过延时,让LED一会亮一会灭。所以我们还需要配置LED的GPIO。
4.4 滴答定时器使用
我们配置好了滴答定时器之后,还要手动编写滴答定时器的中断服务函数,因为我们开启的滴答定时器的中断,当滴答定时器的计数值从我们设置的值减到0时,就会触发一次中断,触发中断就会执行中断服务函数。各个中断的中断服务函数名称已经被写死,不可修改,否则无法正常进入中断服务函数。关于中断服务函数的名称是什么,可以在工程的.s文件中找到各个中断的中断服务函数名称。
在empty.c文件中编写如下代码:
#include "ti_msp_dl_config.h"
volatile unsigned int delay_times = 0;
//搭配滴答定时器实现的精确ms延时
void delay_ms(unsigned int ms)
{
delay_times = ms;
while( delay_times != 0 );
}
int main(void)
{
SYSCFG_DL_init();
while (1)
{
}
}
//滴答定时器中断服务函数
void SysTick_Handler(void)
{
if( delay_times != 0 )
{
delay_times--;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
🔅C语言扩展
在C语言中,volatile 是一个关键字,用来告诉编译器不要对它所修饰的变量做任何优化,因为这个变量的值可能会随时被意想不到的因素改变,比如硬件中断、多线程操作等。volatile 告诉编译器不要将对这个变量的读写操作优化掉,每次访问都要从变量地址中读取或写入。在多线程或与硬件相关的编程中,volatile 经常用来声明那些程序之外其他实体可能会修改的变量,以确保每次访问都能获取最新的值,避免编译器优化导致的意想不到的问题。
以上代码中执行的逻辑为:开启了滴答定时器后,滴答定时器每隔1ms进入中断服务函数中,中断服务函数里一直判断变量 delay_times是否不为0,当不为0时将会一直自减到0。delay_ms()函数的执行效果就是改变变量delay_times的值,让它不为0,然后死等到变量delay_times为0则结束。因为有中断在不断的以1ms的时间间隔将变量delay_times 自减到0,这样我们的精确延时函数就写好了。
4.5 LED灯闪烁实验
前面我们学习了如何去使用滴答定时器,下面我们就用滴答定时器的延时去实现LED闪烁1s间隔。其实很简单就是先让LED引脚输出高电平,然后调用delay_1ms(1000),再让LED引脚输出低电平,再调用delay_1ms(1000),最后在while(1)函数里调用即可。
#include "ti_msp_dl_config.h"
volatile unsigned int delay_times = 0;
//搭配滴答定时器实现的精确ms延时
void delay_ms(unsigned int ms)
{
delay_times = ms;
while( delay_times != 0 );
}
int main(void)
{
SYSCFG_DL_init();
while (1)
{
//LED引脚输出高电平
DL_GPIO_setPins(LED1_PORT, LED1_PIN_22_PIN);
delay_ms(1000);
//LED引脚输出低电平
DL_GPIO_clearPins(LED1_PORT, LED1_PIN_22_PIN);
delay_ms(1000);
}
}
void SysTick_Handler(void)
{
if( delay_times != 0 )
{
delay_times--;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
编译通过:0报错0警告
确认编译成功后,我们将代码烧录开发板中即可。这里我使用J-LINK连接开发板。
J-LINK | 开发板 |
---|---|
5V | 5V |
CLK | CLK |
SWD | DIO |
GND | GND |
Keil中J-LINK下载的配置
配置完J-LINK后,重新编译,然后点击下载:
编译下载之后,可以看到开发板LED将会1s亮1s灭。