아마도 아두이노 IDE에 포함된 delay() 함수는 익숙하실 것입니다. 이 함수는 아두이노 계열 마이크로컨트롤러 제품군 전체에 블로킹 지연을 적용할 수 있는 간단한 함수입니다. 베어메탈 마이크로컨트롤러 프로그래밍으로 넘어오면, 이와 유사한 코드를 찾고 싶어지기 마련입니다. 그러나 아쉽게도, 8051의 "표준 라이브러리"에는 이러한 지연 함수가 포함되어 있지 않습니다.
이 글에서는 하드웨어 기반의 지연 기법을 간략하게 살펴본 뒤, 엄격하게 정의된 일련의 가정을 바탕으로 8051 Busy Bee 기반의 지연 구현 예제를 제시합니다.
8051 지연 함수가 없는 것처럼 보이는 이유
delay() 함수가 누락된 것처럼 보이는 가장 큰 이유는 유연성입니다. 아두이노 코드가 마이크로컨트롤러의 많은 부분을 제어하고 있다는 점을 이해할 필요가 있습니다. 클록 속도와 전용 타이머는 모두 미리 정의되어 있으며, 내부적으로 숨겨져 있습니다. 이러한 숨겨진 일관성 (hidden consistency) 덕분에 delay() 함수는 모든 플랫폼에서 동일하게 동작하여 프로그래밍을 간단하게 할 수 있습니다. 다른 방식으로 하려면 아두이노 프로그래머는 아키텍처와 주변장치의 동작 원리에 대해 많은 것을 알아야 합니다. 이는 초보자와 취미인 사람들이 쉽게 접근할 있도록 하려는 아두이노의 취지와 맞지 않습니다.
일반적인 8051 환경에서는 상황이 크게 다릅니다. 숨겨진 것은 아무것도 없으며 필요한 모든 주변장치는 사용자가 직접 구성해야 합니다. 클록 역시 사용자가 직접 구성해야 하며, 고속의 외부 클록, 내부 클록, 또는 32.768 kHz를 자유롭게 선택할 수 있으며 워치독 타이머만 사용해 마이크로컨트롤러를 주기적으로 깨우는 딥 슬립 모드를 선택할 수도 있습니다. 이러한 옵션은 delay()와 같은 함수의 동작을 왜곡하거나 심지어 멈춰버리게 만들 수 있습니다.
Busy Bee의 레퍼런스 매뉴얼을 준비한 후 본격적으로 시작하겠습니다.
기술 팁: 블로킹 (blocking)이라는 용어는 지연 시간 내내 마이크로컨트롤러의 메인 코드가 완전히 멈추어 아무 일도 하지 않는 것을 의미합니다. 일반적으로 짧은 지연이나 단순한 문제에서는 허용되지만 부적절한 동작으로 이어질 수 있습니다. 예를 들어, 블로킹 지연이 진행되는 동안 마이크로컨트롤러는 버튼 입력에 응답하지 못할 것입니다. 이러한 문제에 대한 대안에는 인터럽트나 논블로킹 지연 등이 있습니다.
지연 루틴을 구현하는 다양한 방법
마이크로컨트롤러에서 지연 함수를 구현하는 방법은 다양하며, 대표적인 예는 다음과 같습니다:
- NOP (No OPeration, 아무 작업도 수행하지 않는 명령어)를 활용해 정교하게 구현한 어셈블리 코드. 여기에서는 프로그래머가 각 어셈블리 명령어의 특성에 따라 마이크로컨트롤러의 클록 사이클을 계산합니다.
- 벡터 인터럽트를 사용하는 하드웨어 타이머. 지연과 관련된 동작이 ISR (Interrupt Service Routine, 인터럽트 서비스 루틴)에 포함되거나, 아두이노의 millis() 함수와 유사하게 인터럽트가 시스템 시간을 유지할 수 있습니다.
- 인터럽트 없이 자유롭게 계속 동작하는 하드웨어 타이머. 여기에서 하드웨어 타이머는 계속해서 동작합니다. 이는 모듈로 60 (modulo 60)으로 동작하는 벽 시계를 바라보는 것과 유사합니다. 예를 들어 현재 시간이 50초이고 20초의 지연이 필요하다면, 초침이 10초가 될 때까지 기다려야 합니다. 8051에서 이러한 솔루션은 선택한 타이머 유형에 따라 모듈로 256 또는 65536 연산이 될 수 있습니다. 이 동작의 "속도"는 시스템 클록과 타이머 프리스케일 구성에 따라 결정됩니다.
- 인터럽트 방식을 사용하지 않고 제어하는 하드웨어 타이머. 이 방법에서는 사용자 프로그램이 타이머를 정지, 알고 있는 값으로 미리 로드, 타이머를 활성화, 그런 다음 오버플로우가 발생하기를 기다립니다.
각 방법에는 장단점이 있으며, 개별 프로젝트 요구 사항, 프로그래머의 숙련도, 향후 유지보수성, 그리고 사용 가능한 하드웨어 자원에 따라 선택이 달라집니다. 예를 들어, 일정한 주기의 신호가 필요한 프로젝트의 경우 벡터 인터럽트를 사용하는 하드웨어 타이머가 매우 적합한 방법입니다. 이러한 정밀한 주기적 타이밍은 PID (Proportional Integral Derivative, 비례 적분 미분) 제어기나 DAC (Digital to Analog Converter, 디지털-아날로그 컨버터)를 이용한 파형 생성에 적합합니다.
블로킹 지연에 집중
이 글에서는 인터럽트 방식을 사용하지 않는 하드웨어 타이머 기반의 방법을 보여줄 것입니다. 마이크로컨크롤러의 하드웨어 타이머는 일반적으로 시계의 초침처럼 계속 증가합니다. 이 코드의 개발에 사용된 Silicon Labs의 EFM8BB1 (8051 기반 Busy Bee)은 4개의 16비트 범용 하드웨어 타이머를 포함한 다양한 타이머를 갖추고 있으며, 이들은 표준 8051과의 하위 호환성을 유지합니다.
16비트 타이머는 모듈로 216 연산을 수행합니다. 시계가 0부터 59까지 세고 다시 0으로 돌아가는 것처럼 16비트 타이머는 0부터 65535까지 세고 다시 0으로 되돌아갑니다. 하드웨어가 자동으로 인터럽트 플래그를 설정하기 때문에 이 오버플로우 이벤트는 특별합니다. 그림1의 단순화된 블록 다이어그램이 이러한 동작을 보여줍니다.
그림 1: Busy Bee timer #2의 단순화된 블록 다이어그램.
기술 팁: 주변장치의 인터럽트 플래그는 자동으로 인터럽트를 발생시키지 않습니다. 인터럽트 활성화 레지스터 (또는 확장 인터럽트 활성화 레지스터)에서 해당 인터럽트 활성화 비트가 설정되어 있어야만 인터럽트를 발생시킵니다.
이 인터럽트 플래그가 이 글의 끝에 첨부된 코드에서 설명하는 블로킹 지연의 핵심입니다. 지연 동작은 다음과 같은 절차로 수행됩니다:
- 프리스케일은 1:1 그리고 모드는 16비트 자동 리로드로 타이머를 설정합니다. 또한 sbit 연산자를 사용하여 구동 제어 (run control) 비트와 인터럽트 플래그 비트에 별칭을 지정합니다. 이 작업은 while(1) 슈퍼루프에 진입하기 전에 한 번만 수행됩니다.
- 타이머를 정지시키고 인터럽트 플래그를 클리어합니다.
- 원하는 지연 값을 타이머에 로드합니다.
- 타이머의 구동 제어 비트를 활성화합니다.
- 인터럽트 플래그가 발생하기 전까지 while 루프에서 (아무 동작도 하지 않고) 대기합니다. 이 플래그가 1로 설정될 때까지 main() 함수의 다른 동작은 모두 차단됩니다.
기술 팁: 기존의 8051은 레지스터를 비트 단위로 조작할 수 있는 독특한 아키텍처를 가지고 있습니다. 결과적으로 레지스터 내 특정 비트를 선택하기 위해 마스크를 사용할 필요가 없어 빠른 코드 실행이 가능합니다. 이는 Keil C51의 sbit 어셈블러 구문을 사용하여 구현됩니다.
이러한 하위 호환성은 EFM8BB1과 같은 8051 파생 제품에서도 유지되고 있습니다. 그러나 최신 제품들은 원래의 8051에 비해 훨씬 더 많은 주변장치와 관련 SFR을 포함하고 있습니다. 안타깝게도, 모든 레지스터가 비트 주소 지정이 가능한 것은 아닙니다. 0x0 또는 0x8로 끝나는 레지스터만 이 편리하고 빠른 sbit 연산을 사용할 수 있습니다. 레퍼런스 매뉴얼을 자세히 살펴보면 주소가 0xC8인 TMR2CN0 레지스터는 비트 주소 지정이 가능한 것을 확인할 수 있습니다.
블로킹 지연에 수반되는 호출 및 연산 오버헤드
이 글을 마무리하기에 앞서, 함수 호출과 리로드 값 계산에 수반되는 오버헤드도 감안해야 합니다. 예를 들어, 다음과 같은 작은 코드 한 줄이 상당히 큰 문제를 야기합니다:
tmr_load = -((n_us * 49) >> 1);
이 코드는 부동소수점을 사용하지 않고 마이크로초당 24.5 클록 틱을 처리하기 위해 영리한 방법을 사용하였습니다. 0.5 틱을 처리하기 위해, 49를 곱한 뒤 오른쪽 시프트 연산-상위 바이트를 한 번 시프트한 후, 캐리를 포함하여 하위 바이트를 한 번 더 시프트 연산-을 사용해 2로 나눕니다. 마지막 계산 단계는 216에서 리로드 값을 빼는 것으로, 간단히 표현하자면 결과 값을 부호 반전하는 것입니다. 다르게 표현하면, 모듈로 65536 환경에서 65536 = 0이기 때문에 65536 – x는 0 – x와 결과가 동일합니다. 이는 60 = 0 (모듈로 60)인 시계와 동일한 개념입니다.
코드의 그 다음 줄에서는 tmr_load 변수에 100을 더합니다. 이는 함수 호출에 따른 오버헤드와 tmr_load 계산에 소요되는 시간을 보정하기 위한 다소 조잡한 방법입니다. Busy Bee에는 하드웨어 곱셈기가 없기 때문에, 8비트 ALU (Arithmetic Logic Unit, 산술 논리 장치)로 8비트 × 16비트 곱셈을 수행하려면 시간이 많이 소요됩니다. 이 함수의 오버헤드로 인한 한 가지 문제는 짧은 지연 시간을 처리할 수 없다는 것입니다. 약 4 µs 정도의 오버헤드로 인해 이보다 더 짧게 지연시킬 수가 없습니다. 이러한 짧은 지연은 앞서 언급한 NOP 명령을 사용하는 어셈블리 코드를 직접 작성할 필요가 있습니다.
그림 2는 실제 결과를 보여주며, 핀이 5 µs 동안 하이, 5 µs 동안 로우 상태를 유지한 후 다시 하이 상태가 되는 것을 확인할 수 있습니다. 연관된 blk_ms_delay 함수에 대해서도 유사한 결과를 얻을 수 있었으며, Digilent Analog Discovery로 측정한 blk_ms_delay (2000)의 호출은 오차가 0.01ms 이내였습니다.
그림 2: 5 µs 동안 온, 5 µs 동안 오프 되도록 프로그래밍된 실제 신호의 오실로스코프 측정 결과.
결론
앞서 언급하였듯이, 아래의 첨부된 코드는 Busy Bee의 클록, 프리스케일, 그리고 타이머 설정에 따라 크게 달라집니다. 따라서 이 코드를 개발할 당시의 가정과 다른 조건이라면 그에 맞게 코드를 수정해야 합니다.
예를 들어, 전력 소모를 줄이기 위해 더 느린 오실레이터를 사용하도록 코드를 수정할 수도 있습니다. 또는 마이크로컨트롤러를 딥 슬립 상태로 만드는 다양한 기법들도 있습니다. 슬립 상태의 마이크로컨트롤러를 가능한 가장 빠른 속도로 깨워 슬립 상태로 되돌리기 전에 작업을 수행할 수도 있습니다.
이러한 유연성이 과거에 사용해 왔던 고급 프로그래밍보다 마이크로컨트롤러를 베어메탈 프로그래밍하는 것이 더 어려운 이유입니다. 동시에, 최대 성능을 끌어낼 수 있는 핵심이기도 합니다.
아래에 의견이나 제안을 자유롭게 남겨주기 바랍니다.
감사합니다.
APDahlen
/***************************************************************************************************
*
* 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);
}
}
영문 원본: How to Program a Blocking Delay Function in the 8051 Microcontroller

