Keil C51. Ручное использование флага переноса в арифметических операциях

Александр Бельченко
17 ноября 2004
Этот небольшой практический совет показывает как вручную манипулировать флагом переноса в арифметических операциях. Такая «мануальная терапия» может быть полезна при работе с двухбайтными регистрами таймеров 0 и 1.

В стандартной архитектуре MCS-51 два таймера 0 и 1 могут работать в режиме таймера с перезагрузкой, но только в однобайтном режиме. Если необходимо запустить таймер в 16-битном режиме, то при каждом переполнении необходимо будет перезагружать таймер вручную.

Например, для получения прерываний от таймера 0 каждые 10 мс, при каждом переполнении таймера (т.е. в прерывании) его нужно перегружать значением (65536 - 9216) = 0xDC00. Дополнительно нужно учесть сколько времени будет затрачено на саму операцию перезагрузки, а также на вход в прерывание. В типичном случае код для перезагрузки значения таймера будет выглядеть так (пример взят из Application Note 105):

#include <reg51.h>
#define TIMER0_COUNT 0xDC11 /* 10000h - ((11,059,200 Hz / (12 * FREQ)) - 17) */

static void timer0_isr (void) interrupt 1 using 1
{
    unsigned i;
    
    /*------------------------------------------------
    Stop Timer 0, adjust the timer 0 counter so that
    we get another interrupt in 10ms, and restart the
    ------------------------------------------------*/
    TR0 = 0; /* stop timer 0 */
    i = TIMER0_COUNT + TL0 + (TH0<<8);
    TL0 = i;
    TH0 = i >> 8;
    TR0 = 1;
    
    ...
}

Как видно, для учитывания времени, прошедшего с момента возникновения прерывания и до того момента, как значения таймера будет перезагружено новой величиной, применяется простой прием: значение для перезагрузки прибавляется к текущему значению таймера. Время, которое тратится на арифметические операции, учитывается в константе перезагрузки — в вышеприведенном примере на это отведено 17 машинных тактов. Если взглянуть на тот ассемблерный код, который будет сгенерирован для этого примера, то можно увидеть примерно такое:

0000 C28C              CLR     TR0          ; TR0 = 0; /* stop timer 0 */
0002 AF8C              MOV     R7,TH0       ; i = TIMER0_COUNT + TL0 + (TH0<<8);
0004 EF                MOV     A,R7
0005 FE                MOV     R6,A
0006 AD8A              MOV     R5,TL0
0008 ED                MOV     A,R5
0009 2411              ADD     A,#011H
000B FD                MOV     R5,A
000C E4                CLR     A
000D 34DC              ADDC    A,#0DCH
000F CD                XCH     A,R5
0010 2400              ADD     A,#00H
0012 FF                MOV     R7,A
0013 ED                MOV     A,R5
0014 3E                ADDC    A,R6
;---- Variable 'i' assigned to Register 'R6/R7' ----
0015 8F8A              MOV     TL0,R7       ; TL0 = i;
0017 F58C              MOV     TH0,A        ; TH0 = i >> 8;
0019 D28C              SETB    TR0          ; TR0 = 1;

Бегло подсчитав количество тактов, за которое будет выполнен этот код, у меня получился 21 такт. В кейловском примере стоит цифра 17 — возможно, что компилятор в версии 5 был поумнее.

Для тех, кто писал подобный код на ассемблере, получаемый код может показаться весьма неоптимальным — по крайней мере я так считаю. На чистом ассемблере я бы записал эту операцию так:

    CLR     TR0
    MOV     A, #0x08
    ADD     A, TL0
    MOV     TL0, A
    MOV     A, #0xDC
    ADDC    A, TH0
    MOV     TH0, A
    SETB    TR0

Если захотите, то подобный кусок кода можете вставить в свой обработчик прерывания в качестве ассемблерной вставки. Но целью этой статьи было показать, как максимально приблизиться к желаемому результату средствами языка C51.

Самый существенный момент здесь — это то, что необходимо произвести сложение двухбайтовых чисел на 8-битном контроллере. Т.е. при сложении старших байт нужно учитывать флаг переноса. Для обращения к флагу переноса Carry из си-кода можно использовать имя CY (бит с таким именем определяется в заголовочном файле reg51.h или специфичном для конкретного микроконтроллера).

Для ручной реализации двухбайтового сложения можно использовать такой код:

    TR0 = 0;

    TL0 += 0x0A;
    TH0 += 0xDC + (unsigned char)CY;
    
    TR0 = 1;

Обратите внимание на явное приведение бита CY к типу unsigned char. Посмотрим на ассемблерный эквивалент:

0004 C28C              CLR     TR0
0006 740A              MOV     A,#00AH
0008 258A              ADD     A,TL0
000A F58A              MOV     TL0,A
000C E4                CLR     A
000D 33                RLC     A
000E 24DC              ADD     A,#0DCH
0010 258C              ADD     A,TH0
0012 F58C              MOV     TH0,A
0014 D28C              SETB    TR0

Как видно лишний код (оверхэд) составил всего 2 команды или 2 такта, что уже можно считать приемлемым результатом. По сравнению с первым примером время выполнения этого кода уменьшилось в 2 раза, а объем кода — в полтора раза.