1. 系统延时
延时功能在计算机编程中十分常见,它可以暂停程序的执行一段时间,以实现各种应用需求。
1.1. 延时的作用
在MSPM0或其他微控制器的编程中,延时被广泛使用,主要有以下一些原因:
- 处理硬件:许多硬件都需要一些时间来响应某个命令。例如,如果在一个程序中你启动一个电动机然后立即检查其状态,你可能会得到一个错误的读数,因为电动机可能还没有足够的时间开始旋转。此时你需要使用 delay() 函数让系统等待一段时间,使得电动机有足够的时间响应。
- 用户交互:我们常常需要在用户交互中实现延迟效果。例如,在蜂鸣器播放音乐时,音符之间需要一段沉默的时间。或者,在闪烁LED灯的情况下,"开"和"关"状态之间需要延时以控制闪烁的速度。
- 节省能源:在一些应用中,比如电池供电的系统,如果不在需要的时候长期保持系统的高速运转,那么电池的寿命会大大缩短。在此情况下,我们可以让系统在一段时间后进入待机或低功耗模式,直到下一个处理周期到来。
- 定时操作:在许多项目中,我们常常需要实现一些特定时间点的操作。例如,在自动灌溉系统中,我们可能需要在每天的特定时间点进行灌溉。在间隔测量中,我们可能每隔一段时间采集一次数据。
尽管延时函数在很多情况下非常有用,但也需要注意其阻塞性质。过度依赖阻塞延时可能会导致程序对其他事件的响应不及时。为了更好的在MSPM0上进行多任务编程,我们还可以学习一些非阻塞延时的编程技术。
❓什么是阻塞延时?
阻塞延时是在程序执行过程中,当某个操作或函数需要一定时间才能完成时,程序会暂停执行直至该操作完成,这段时间程序被阻塞了。阻塞延时可能会导致程序运行速度变慢或出现假死现象。 举个例子,假设你想要煮开水来泡茶。通常情况下,你会将水壶放在炉灶上加热,等待水烧开后才能使用。在这个过程中,存在阻塞延时。 当你将水壶放在火上时,程序可以看作是“等待”水烧开的操作。在这个等待过程中,你不能立即得到热水来泡茶,需要耐心等待水煮沸。期间,你可能无法做其他与烧水无关的事情,因为你需要留意水壶,并等待时机。即便家里着火了,你也还是在等待烧水。
1.2. 在3507中实现延时的方式
可以有很多种延时的方式,这里给大家介绍三种方法:空代码延时
、TI工程自带的延时
、使用滴答定时器
。
1.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亮一定时间然后再灭一定时间,如此反复。
1.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
1.3. 滴答定时器介绍
SysTick定时器可用作标准的下行计数器,是一个24位向下计数器,有自动重新装载能力,可屏蔽系统中断发生器。Cortex-M0处理器内部包含了一个简单的定时器,所有基于M0内核的控制器都带有SysTick定时器,这样就方便了程序在不同的器件之间的移植。SysTick定时器可用于操作系统,提供系统必要的时钟节拍,为操作系统的任务调度提供一个有节奏的“心跳”。正因为所有的M0内核的芯片都有Systick定时器,这在移植的时候不像普通定时器那样难以移植。
简单来说,就是我们开启了滴答定时器后,我们设置一个初始的值比如100,滴答定时器就会向下100->99->98->97...的向下计数,直到0。而自动重新装载功能就是当计数值到0时,会自动重新会到我们之前设置的100,然后又开始重新向下计数。
RCU 通过 MCLK 作为 Cortex 系统定时器(SysTick)的外部时钟,即使用MCLK计时,MCLK默认为32MHz。通过对 SysTick 控制和状态寄存器的设置,即可控制或读取。关于系统时钟的介绍可参考用户手册的第128页。
SysTick定时器设定初值并使能之后,每经过1个系统时钟周期,计数值就减1,减到0时,SysTick计数器自动重新装载初值并继续计数,同时内部的COUNTFLAG标志位被置位,触发中断(前提开启中断)。
1.4. 滴答定时器配置
1.4.1 开启SYSCONFIG配置工具
在CCS中新建一个空白工程 empty。
在CCS的左侧工作区中找到并打开empty.syscfg文件。
在sysconfig中,左侧可以选择MCU的外设,我们找到并点击SYSTICK选项卡,在SYSTICK中点击ADD,就可以添加滴答定时器。
1.4.2 配置滴答定时器
滴答定时器是使用MCLK计时,而MCLK默认为32MHz,所以每计一次数花费的时间为 1÷32,000,000 = 0.00000003125 S.
MSPM0G3507默认使用内部时钟32MHz,而我们地猛星外部接了一个40MHz的高速晶振,后面可以通过这个晶振将芯片的主频升到最高的80MHz
假设我们设置要计数的值为32000,则计32000个数会花费的时间为:32000 x 0.00000003125 = 0.001 S = 1MS.
所以如果我们要设置1ms的滴答定时器就设置滴答定时器的计数值为32000,这样的话在滴答定时器从32000向下计数到0时的时间就正好是1ms。案例中我们开启了中断,关于中断的概念在后面再说。大概的意思就是中断其他任务,先执行滴答定时器的内容。
1.4.3. 其他配置
这里我们需要一个LED灯来展示延时的状态,通过延时,让LED一会亮一会灭。所以我们还需要配置LED的GPIO。
将之前在.syscfg文件中的配置保存。
可以通过快捷键
Ctrl+S
进行快速保存
1.5. 滴答定时器使用
我们配置好了滴答定时器之后,还要手动编写滴答定时器的中断服务函数,因为我们开启的滴答定时器的中断,当滴答定时器的计数值从我们设置的值减到0时,就会触发一次中断,触发中断就会执行中断服务函数。各个中断的中断服务函数名称已经被写死,不可修改,否则无法正常进入中断服务函数。关于中断服务函数的名称是什么,看下表:
中断函数名 | 中断说明 |
---|---|
Reset_Handler | 复位中断函数 |
NMI_Handler | 不可屏蔽中断函数 |
HardFault_Handler | 硬件故障中断函数 |
SVC_Handler | 特权中断函数 |
PendSV_Handler | 一种可挂起的、最低优先级的中断函数 |
SysTick_Handler | 滴答定时器中断函数 |
GROUP0_IRQHandler | GROUP0的中断函数 |
GROUP1_IRQHandler | GROUP1中断函数 |
TIMG8_IRQHandler | TIMG8的中断函数 |
UART3_IRQHandler | UART3的中断函数 |
ADC0_IRQHandler | ADC0的中断函数 |
ADC1_IRQHandler | ADC1的中断函数 |
CANFD0_IRQHandler | CANFD0的中断函数 |
DAC0_IRQHandler | DAC0的中断函数 |
SPI0_IRQHandler | SPI0的中断函数 |
SPI1_IRQHandler | SPI1的中断函数 |
UART1_IRQHandler | UART1的中断函数 |
UART2_IRQHandler | UART2的中断函数 |
UART0_IRQHandler | UART0的中断函数 |
TIMG0_IRQHandler | TIMG0 的中断函数 |
TIMG6_IRQHandler | TIMG6的中断函数 |
TIMA0_IRQHandler | TIMA0的中断函数 |
TIMA1_IRQHandler | TIMA1的中断函数 |
TIMG7_IRQHandler | TIMG7的中断函数 |
TIMG12_IRQHandler | TIMG12的中断函数 |
I2C0_IRQHandler | I2C0的中断函数 |
I2C1_IRQHandler | I2C1的中断函数 |
AES_IRQHandler | 硬件加速器的中断函数 |
RTC_IRQHandler | RTC实时时钟的中断函数 |
DMA_IRQHandler | DMA的中断函数 |
在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,这样我们的精确延时函数就写好了。
1.6. LED灯闪烁实验
前面我们学习了如何去使用滴答定时器,下面我们就用滴答定时器的延时去实现LED闪烁1s间隔。其实很简单就是先让LED引脚输出高电平,然后调用delay_1ms(1000),再让LED引脚输出低电平,再调用delay_1ms(1000),最后在while(1)函数里调用即可。
LED的配置步骤请参考
点亮LED灯
章节,这里不再重复配置。
#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
烧写我们的代码之后,可以看到开发板LED将会1s亮1s灭。
烧录步骤:配置下载仿真器->下载仿真器与开发板接线->编译->下载调试。具体烧录步骤请参考
环境搭建
章节或者点亮LED灯
章节。