大家可能熟悉 Arduino IDE 中的 delay() 函数。这是一个简单的函数,它提供了一个适用于 Arduino 微控制器系列中所有成员的阻塞延迟。当你过渡到裸机微控制器编程时,你可能会发现自己在寻找类似的代码。不幸的是,你不太可能 在8051 “标准库”中找到这样一个延迟函数。
在本文中,我们将简要探讨硬件延迟方法,然后使用一组严格定义的假设提出 8051 Busy Bee 解决方案。
这种明显的 delay() 遗漏的最大原因可能是灵活性。了解 Arduino 代码控制了底层微控制器的很大一部分。时钟速度和专用计时器都是预定义的,隐藏在后台。这种隐藏的一致性允许简单的编程体验,delay() 函数在所有平台上执行相同。否则需要 Arduino 程序员对外设的体系结构和操作有大量的了解。这样的行为违背了 Arduino 为初学者和业余爱好者提供可访问性的目的。
在典型的 8051 环境中,情况就大不相同了。没有什么是隐藏的,你通常需要配置所有必要的外围设备。你还可以负责时钟,并可以自由选择高速外部,内部,32.678 kHz 甚至深度睡眠,只有看门狗计时器定期唤醒微控制器。这些选项中的每一个都会损坏甚至停止一个函数,如 delay() 按预期运行。
找到一份 Busy Bee 参考手册,让我们开始吧。
技术贴士 :术语阻塞意味着微控制器的主代码在整个延迟期间被阻塞(什么都不做)。对于小的延迟和简单的问题,这通常是可以接受的,但可能导致不可接受的操作。例如,当阻塞延迟正在进行时,微控制器将对按钮按下无响应。这个问题的替代方案包括中断和非阻塞延迟。
许多延迟选项是可用的
让我们首先认识到,在微控制器中构造延迟函数的方法有很多种。一个简短的列表可能包括:
-
以 NOP (什么都不做指令)为特征的精心构造的汇编代码。在这里,程序员将根据每个汇编命令的特征计算微控制器的时钟周期。
-
带有矢量中断的硬件定时器。与延迟相关的操作可以嵌入到中断服务例程( ISR )中,或者中断可以维持类似于 Arduino millis() 函数的系统时间。
-
免费运行硬件定时器而不中断。这里硬件定时器是不断运行的。这类似于看一个带模数60运算的挂钟。假设当前时间是50秒,而你想要一个20秒的延迟。然后你会一直等到秒针到达10秒。在 8051 中,这样的解决方案将根据选择的计时器类型对 256 或 25536 取模。操作的“速度”取决于系统时钟和定时器的预量程配置。
-
不使用中断的受控硬件定时器。使用这种方法,用户程序将停止定时器,预加载定时器到一个已知值,启用定时器,并等待溢出发生。
每种方法都有优点和缺点。选择在很大程度上取决于单个项目的需求、程序员的技能、未来的可维护性以及可用的硬件资源。例如,如果项目需要一致的心跳,带有矢量中断的硬件计时器就是一个非常好的解决方案。这种精确的周期性定时对于比例积分导数控制器或使用 DAC 生成波形来说是很好的。
阻塞延迟
在这篇文章中,我们将介绍一个使用无中断硬件计时器的解决方案。回想一下,微控制器的硬件计时器通常就像时钟的秒针一样计数。在此代码开发中使用的 Silicon Labs 的 EFM8BB1 8051 具有一个定时器集合,其中包括四个16位通用硬件定时器,向后兼容标准 8051。
16位定时器具有模数2^16运算。与从0到60计数的时钟一样,16位计时器在回滚到 0 之前也会从 0 计数到 65535。这个溢出事件是特殊的,因为硬件会自动设置一个中断标志。这个操作建议在高度简化的图1框图中。
图 1 : Busy Bee 定时器 #2 的简化框图。
技术贴士 :外设的中断标志不会自动启动中断。只有在中断或扩展中断使能电阻中设置了相关的中断使能位时,它才会这样做。
这个中断标志是本文所附代码中描述的阻塞延迟的关键。延时的操作描述如下:
-
将合适预售和模式的定时器分别配置为 1:1 和16位自动重载。还要使用sbit操作符别名运行控制和中断标志。这些操作在while(1)超级循环之前执行一次。
-
关闭定时器并清除中断标志。
-
用期望的延迟值加载计时器。
-
启用计时器的运行位。
-
在 while 循环中旋转(什么都不做),直到中断标志。此操作将阻塞所有其他 main() 代码,直到设置该标志。
技术提示 :最初的8051是一个独特的架构,能够在位级寄存器上操作。这导致了快速的代码,而不需要使用掩码来选择寄存器内的特定位。这是使用Keil C51特殊功能寄存器(Sbit)汇编器语句完成的。
在EFM8BB1等衍生产品中保留了这种向后能力。然而,较新的产品比最初的8051包含了更多的外设和相关的SFRs。不幸的是,并非所有的寄存器都是位可寻址的。只有以0x0或0x8结尾的寄存器才能使用这种方便快捷的位操作。仔细查看参考手册,可以发现地址为0xC8的TMR2CN0是位可寻址的。
调用和数字开销
在结束本文之前,我们需要考虑与调用函数和计算重载值相关的开销。例如,这一小段代码带来了一个严重的问题:
tmr_load = -((n_us * 49) >> 1);
它试图聪明地解释每微秒24.5个时钟节拍。它没有使用float类型来解释半比特,而是先乘以49,然后使用右移操作除以2;一个为高位,一个为低位,带进位。
最后一步是用216减去装弹值。简写就是对结果求反。换一种说法,在模65536环境中,65536 - x提供的答案与0 - x相同,因为65536 = 0(模65536)。这和60 = 0(模60)的时钟是一样的。
在下一行代码中,我们将100添加到tmr_load变量中。这是一个粗略的方法,用于计算函数调用开销以及计算tmr_load所需的时间。回想一下,Busy Bee没有硬件乘法器。因此,通过8位ALU执行8位乘16位乘法需要时间。
这个函数开销的一个牺牲品是接受小延迟的能力。它的开销大约为4us,因此不可能延迟更小的东西。如果需要这些小延迟,应该使用前面提到的带有NOP操作的手工编码汇编程序。
真实世界的结果如图2所示,我们看到一个引脚为5 us高,5 us低,然后又高。对于相关的blk_ms_delay也获得了类似的结果,其中blk_ms_delay(2000)的调用在0.01 ms内,由 Digilent 的 471-060 (Analog Discovery)测量。你是否同意阻塞函数提供了合理的性能?
图 2 :示波器测量现实世界的信号,编程5路开和5路关。
结论
如前所述,所附代码高度依赖于Busy Bee的时钟、预分频和定时器配置。你将需要修改代码,以解释任何偏离此代码开发中所做假设的条件。
也许你会修改代码,使用较慢的振荡器来节省能量。还有一些技巧可以让微控制器进入深度睡眠状态。然后,你可能希望以尽可能高的速度唤醒微控制器,以便在返回睡眠之前快速执行操作。
这种灵活性使得微控制器裸机编程比你过去可能使用的高级编程更加困难。这也是解锁性能的关键。
/***************************************************************************************************
*
* This code was developed on the EFM8BB1 featuring a 24.5 MHz internal clock.
*
* Note that the TMR2CN0 SFR is located at 0xC8. Since it ends in 0x08, it is one of the
* bit-addressable memory locations. This is convenient as it allows the Keil C51
* compiler to use the sbit construct.
*
* Be sure to include these statements:
*
* sbit TR2 = TMR2CN0^2; // Timer 2 run control
* sbit TF2 = TMR2CN0^7; // TMR2 16-bit overflow on the 0xFFFF to 0x0000 transition.
*
* Also, don't forget to configure timer 2:
*
* TMR2CN0 = 0x00; // default for timer 2: clear overload flag, 16-bit, timer off
* CKCON0 |= CKCON0_T2ML__SYSCLK; // use system clock
*
*/
/***************************************************************************************************
*
* @brief Microsecond blocking delay based on T2
*
* Given a 24.5 MHz system clock, this function provides a delay between 5 and 1000 us.
* The actual max value without an overflow error is 1337. However, it's easier and
* less error prone to remember 5 to 1000.
*
* Note that this function cannot be used for single us delays as it takes longer than that to
* perform the function calls and math to calculate the appropriate timer delay.
*
* An empirical correction is added to account for the calling overhead and delay calculations.
*
* This function assumes a 24.5 MHz system clock. It also assumes a 1:1 pre-scale or Timer 2.
* For fast computation:
* 1) multiply n_us by 49
* 2) divide by 2 using a shift right
*
* @param n_us Identifies the number of microseconds to delay.
*
* @warning There are no guard rails for overflow.
*
* @warning Delays less than 5 us will be extended to 5 us.
*
* @warning For improved performance replace the reload calculation with a lookup table.
*
*/
void blk_us_delay (uint16_t n_us){
uint16_t tmr_load;
if (n_us < 5){ // Extend small delays to 5 us
n_us = 5;
}
tmr_load = -((n_us * 49) >> 1);
tmr_load += 100; // Estimate accounting or the calling overhead
// and the machine cycles to perform the previous
// 8 by 16-bit multiplication.
TR2 = 0; // Stop timer
TF2 = 0; // Clear timer overflow flag
TMR2L = tmr_load;
TMR2H = tmr_load >> 8;
TR2 = 1; // Start timer
while (!TF2); // Wait for the overflow
TR2 = 0; // Stop timer to save power
}
/***************************************************************************************************
*
* @brief Millisecond blocking delay based on blk_us_delay which in turn is based on T2.
*
* Provides a delay between 1 and 65,534 ms.
*
* @param n_ms Identifies the number of milliseconds to delay.
*
* @warning For improved accuracy be sure to use a hardware timer with a large pre-scale value.
*
*/
void blk_ms_delay (uint16_t n_ms){
uint16_t i;
for(i = 0; i < n_ms; i++){
blk_us_delay (1000);
}
}