Keil C51. Расширения языка Си.
Ключевое слово using: тонкости использования

Александр Бельченко
2 апреля 2004
Иногда мне приходят письма от читателей сайта с просьбой помочь в решении той или иной проблемы, возникающей при работе с пакетом Keil C51 — с Си-компилятором или симулятором-отладчиком. Конечно же, я не могу знать всех нюансов работы этого пакета и ответить на все приходящие вопросы. Впрочем, часто всплывают вопросы, с которыми я и сам когда-то сталкивался в своей работе. На такие вопросы я стараюсь отвечать по мере сил и наличия свободного времени.

Сегодня я хочу рассказать вам об одном нюансе использования компилятора Keil C51. Этот нюанс связан с использованием различных банков регистров R0-R7 и ключевого слова using. Однако, обо всем по порядку.

Посмотрим на проблему в общих чертах

В конце февраля пришло письмо от читателя моего сайта Алексея Старостина. Вот его текст с формулировкой проблемы.

Добрый день, Александр!
Если Вас не затруднит проясните мне один момент. Написал я в кейле проект. Устройство принимает код от считывателя проксимити и передает в компьютер. Прием кода и преобразование расположены в отдельном модуле и состоит из двух подпрограмм. Одна принимает отдельный байт, а вторая всю информацию и соответственно вызывает первую. Принятая информация складывается в буфер в виде глобального массива. Хотел я это все задействовать в обработчике внешнего прерывания. Все вроде написал правильно (по крайней мере с точки зрения Си), однако при обработке прерывания, обработчик вызывает вторую подпрограмму, но данные из первой подпрограммы во вторую (в буфер) не передаются (в переменных не видно). Не передаются также в локальные переменные, а только в глобальные. Вот такая неприятность. То ли дебагер в кейле не правильно работает, то ли я что-то не так написал.

Имея только такие исходные данные и ничего более, я мог только исходить из своего опыта работы с отладчиком-симулятором µVision2. И честно признаюсь: таких глюков в симуляторе я никогда не встречал. Поэтому я ответил Алексею, что скорее всего ошибка в программе, а не отладчике, и попросил его детальнее обрисовать проблему.

Итак, имеется следующая схема вызовов подпрограмм:

обработчик прерывания
                    |
                    L--> подпрограмма 2
                                    |
                                    L--> подпрограмма 1

Проблема заключается в том, что данные, возвращаемые подпрограммой 1, должны через подпрограмму 2 попасть в обработчик прерывания.

Вроде бы все просто, но на каком-то этапе в этой цепи передачи данных происходит сбой. На каком именно? Для ответа на этот вопрос явно не хватает данных.

На мою просьбу показать детали Алексей ответил следующее:

Да, Вы все поняли правильно. Но ошибки я не вижу. Смотрел дизасемблированные данные - вроде все как надо, но в буфер ничего не приходит. Одним словом мистика. Если Вас не затруднит глянте мой проектец. Устройство считывает данные в формате clock&data и по запросу с компа пересылает их. Все вроде просто как автомат Калашникова, однако не пашет. Я вообщето переписал проект - прием данных полностью запихнул в обработчик прерывания, но очень хочется разобраться.

Пытаемся разобраться

Разобраться захотелось и мне. Мой первый наставник, у которого я работал, выйдя из института молодым специалистом, любил повторять: «Чудес не бывает!». Возможно он был в этом прав не на все 100%, но в случае с программой Алексея дело обстояло именно так.

Итак, что мы видим в программе? Я приведу только существенные для понимания моменты, опуская ненужные детали.

В файле int_ext.c находится обработчик прерывания int1, который вызывает подпрограмму считывания данных GetData():

void int1(void) interrupt 2 using 1
{
    ...
    GetData();
    ...
}

В отдельном файле clock_data.c содержатся те две подпрограммы, которые ответственны за прием данных — GetData() и GetDigit(). Причем подпрограмма GetData() вызывает функцию GetDigit() до тех пор, пока не будет принят весь пакет, или не возникнет ошибка приема.

Ошибка приема может возникнуть если в течении одной секунды входные данные от считывателя не поступают (т.е. истекает таймаут приема) или если контроль четности при приеме каждой цифры дает ошибку. Принятые данные складываются в глобальный массив-буфер InBUF[].

unsigned char GetDigit(void)
{
    ...
    return результат_приема_цифры_или_код_ошибки;
}

unsigned char GetData(void)
{
    while(сообщение_не_принято)
    {
        InBUF[i] = GetDigit();
        if ( ошибка_приема )
            return ошибка;
    }
    return все_ОК;
}

Вот как описывает Алексей поведение программы в отладчике:

Проверял я следующим образом: в дебагере вызывал прерывание INT1, затем доходил до строки в подпрограмме GetDigit():
while(CLOCK)
    if(fTime_out)
        return ERROR;   // Ждем смены бита,
                        // если таймаут, то ошибка
устанавливал бит fTime_out (типа время истекло), и по идее в InBUF[0] должно было появится 0хFF - код ошибки. Но появлялся ноль. Ситуация не меняется, даже если в программе GetDigit() в начале явно написать return 0xFF. Все равно возвращается ноль :((

Я запустил его проект в симуляторе и проделал все те же манипуляции. Да, действительно, такой эффект имеет место быть. Но откуда и почему?

Ответ на этот вопрос можно найти в листинге компилятора, просмотрев ассемблерный код, генерируемый при трансляции Си-программы.

У меня уже сложилась устойчивая привычка периодически просматривать получающийся ассемблерный код после трансляции Си-кода. Для того, чтобы Си-компилятор включал в свой листинг результат трансляции в команды ассемблера, необходимо включить соответствующую опцию в настройках проекта. А именно: Option for Target — Listing — опция C Compiler Listing включена — опция Assembly Code включена.

Посмотрев ассемблерный код я довольно быстро нашел причину такого поведения программы. И вспомнил, что однажды на эти грабли уже наступал сам. И когда я сам столкнулся с этой проблемой, то потратил довольно большое количество времени на поиск истинной причины. Но благодаря использованию возможностей отладчика µVision2 в итоге нашел.

Вкратце скажу, что в этом «глюке» виновато ключевое слово using в объявлении функции-обработчика прерывания int1.

Причина найдена: взглянем на нее внимательно

Проблема достаточно простая, хотя ее решение и неочевидно на первый взгляд.

Если посмотреть на ассемблерные эквиваленты функций GetData и GetDigit, то можно увидеть, что значение, возвращаемое функцией GetDigit передается через регистр R7 (это стандартный ход для компилятора Keil C51). Глюк кроется в том, что функция GetData вызывается из подпрограммы

void int1(void) interrupt 2 using 1
Здесь тонкий момент — ключевое слово using 1, которое указывает, что для работы подпрограммы используется 1й банк регистров (для основного тела программы по умолчанию используется нулевой банк). Вроде бы и ничего страшного.

Но! Злобный глюк сделал свое черное дело. Функция GetData описана в отдельном модуле и никак не указано компилятору, что она будет вызываться из прерывания, и что активным банком регистров будет 1й. Поэтому компилятор скомпилировал и оптимизировал код из расчета, что вся работа производится в нулевом банке. А оптимизация состояла в том, что возвращемые данные из функции GetDigit сохраняются в InBUF[i] следующими командами:

0009 1100        R     ACALL   GetDigit
000B 7400        R     MOV     A,#LOW InBUF
000D 2500        R     ADD     A,i
000F F8                MOV     R0,A
0010 A607              MOV     @R0,AR7

Обратите внимание на последнюю ассемблерную инструкцию. Здесь AR7 — это прямой адрес регистра R7. Как я уже говорил, компилятор считает, что идет работа с нулевым банком регистров, поэтому AR7 равен 0x07. На самом деле при входе в прерывание производится переключение банка регистров (с нулевого на первый), поэтому должно быть 0x0F. Вот оно!

Теперь мы знаем где спрятался глюк. И вас, наверное, уже мучает вопрос: а как с этим бороться?

Как избежать подобного эффекта в своих программах?

Первым решением может стать следующее: добавить к определению функции GetData ключевую фразу using 1. Но это решение в лоб, далекое от элегантности и мы таким путем не пойдём.

Освежив в памяти знания о директивах компилятора (перечитав параграф «Control Directives» в главе 2 документа «Cx51 Compiler» файл c51.pdf стр.20) можно увидеть два других возможных пути.

Путь первый. Радикальный

Запретить компилятору использовать обращение по адресу регистра вместо обращения к собственно регистру. Для этого используется директива NOAREGS.

Непосредственно перед функцией GetData указать следующее:

#pragma NOAREGS

Это позволит вызывать функцию GetData из любой точки программы, не заботясь о том, какой сейчас активный банк регистров. Для надежности можно указать #pragma NOAREGS и перед GetDigits.

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

    MOV A, R7
    MOV @R0, A

компилятор начинает лепить какие-то хитрые комбинации с использованием ассемблерной инструкции XCH. Как результат — на пустом месте увеличивается код и время выполнения.

Путь второй. Хитрый и тонкий

Хитрость этого метода заключается в том, что если мы знаем, что некая функция всегда будет запускаться только из некоторой конкретной подпрограммы и всегда банк регистров будет 1й, то мы можем использовать директиву REGISTERBANK и указать компилятору с каким банком регистров на самом деле идет работа.

Для этого непосредственно преед функцией GetData необходимо указать следующее:

#pragma RB(1)

что заставит компилятор правильно вычислять адрес регистров при прямом обращении к ним, как к ячейкам памяти.

Тонкость этого метода заключается в том, что необходимо помнить о том, что такая функция должна вызываться только из тех функций, в которых установлен активный банк регистров 1й. Иначе бодро и весело мы наступим на тот же самый глюк, о котором я расказывал в этой статье.

Заключение

В этой статье я постарался показать не только тонкости использования ключевого слова using, но и обрисовать процесс поиска и локализации «мистических» ошибок.

Я благодарен Алексею за его вопрос и пример кода, которые он мне предоставил. Как я уже говорил, я и сам однажды столкнулся в своей программы с этим эффектом. Но, получив шваброй по лбу, запомнил, что на нее не нужно наступать дважды, и подобных тонких мест на подсознательном уровне старался избегать. Программа же Алексея оказалось очень простой и вместе с тем очень показательной. Так что я рад, что его вопрос заставил меня снова вспомнить о той «швабре» и написать эту статью.

Ну а всем тем, кто, возможно, однажды столкнется с подобным «мистическим и необъяснимым» поведением в своей программе, я хочу посоветовать начинать анализ ситуации с локализации ошибки, внимательно отслеживая в симуляторе за перемещением данных от функции к функции.

В отладке программ мне часто помогают советы из книги Джона Роббинса «Отладка Windows-приложений» (Москва: ДМК, 2001). И хотя значительная часть книги относится к отладке программ на языке Си для платформы Windows, но в первых главах книги автор в простой и доходчивой манере излагает общие принципы отладки. Те принципы, которые помогают сделать процесс отладки быстрым и безболезненным.

Удачи вам в этом нелегком деле!