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. Все равно возвращается ноль :((
Я запустил его проект в симуляторе и проделал все те же манипуляции. Да, действительно, такой эффект имеет место быть. Но откуда и почему?
Ответ на этот вопрос можно найти в листинге компилятора, просмотрев ассемблерный код, генерируемый при трансляции Си-программы.
Посмотрев ассемблерный код я довольно быстро нашел причину такого поведения программы. И вспомнил, что однажды на эти грабли уже наступал сам. И когда я сам столкнулся с этой проблемой, то потратил довольно большое количество времени на поиск истинной причины. Но благодаря использованию возможностей отладчика µ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, но в первых главах книги автор в простой и доходчивой манере излагает общие принципы отладки. Те принципы, которые помогают сделать процесс отладки быстрым и безболезненным.
Удачи вам в этом нелегком деле!