[Перевод] Реверс-инжиниринг неизвестного микроконтроллера

hymwuuihqwrlnbev7f_xuirbvyw.png

Сложная завязка


Предыстория…


В рамках моей работы над реверс-инжинирингом электронных eInk-ценников мне довелось столкнуться с интересной проблемой. Конкретная компания (Samsung Electro Mechanics/SoluM) перешла с использования сторонних чипов, происхождение которых мне удалось выявить (Marvell 88MZ100) на новый чип, который стала применять со своими ценниками следующего поколения.

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

Исследование


4f6beab008a28ac3ba5eb253d1312ef1.jpg

Глупо пытаться решить кроссворд, не прочитав вопросы к нему. Столь же глупо браться за реверс-инжиниринг устройства, не собрав сперва всю информацию, которая о нем уже имеется. Итак, что нам исходно известно? Протокол беспроводной передачи данных, вероятно, такой как обычно, поскольку ни одна компания не захочет мигрировать на новый либо поддерживать для своих клиентов сразу два протокола, не спеша выполняя миграцию. Старый протокол был ZigBee-подобным на 2,4 Ггц, поэтому новый, вероятно, такой же. Вот фото платы с обеих сторон.


Итак, что же мы видим? Во-первых, крутой образчик оптимизации издержек. Они заламинировали экран с электронными чернилами прямо на печатную плату! Кому нужна проводящая стеклянная задняя панель, когда есть печатная плата? Передняя панель сделана из проводящего пластика. Но это не важно.

Видны две антенны, обе, судя по их размеру — на 2,4 ГГц. Ожидаемо, поскольку у устройств предыдущего поколения также было по две антенны на 2,4 ГГц. Видим два чипа. Большой и малый. У крупного (обозначенного «SEM9010») по-видимому много контактов идет к дисплею и ни одного — к антеннам. Очевидно это контроллер дисплея.

Малый (обозначенный «SEM9110») — это, по-видимому, мозг, отвечающий за все операции. Он подключен к антеннам, кристаллу, отвечающему за хронометраж, а также к ключевым точкам, которые здесь очевидно для заводского программирования.

Здесь 12 контактных площадок: одна подключена к положительному выводу батареи, одна к земле, назначение остальных 10 — загадка. Поискав название чипа онлайн, не нахожу ничего дельного — определенно, их собственная разработка. Но кто проектирует собственный чип для столь простого применения? Может просто ребрендинг? Впряглись, работаем!

Любопытно, что тут помог поиск по картинкам Google. Бывает, этот инструмент приходится донельзя кстати при реверс-инжиниринге. В данном случае он приводит нас к этому самородку (архивная копия здесь для потомков). Это вопрос с StackExchange — интересуются, как работают эти электронные ценники. Вопрос интересен потому, что на размещенной здесь фотографии печатная плата выглядит практически идентично нашей. Чипы тоже ровно такие же, но метки на них другие! Вероятно, плата сделана еще до того, как SoluM приступила к ребрендингу этих чипов.

Тот чип, который я счел контроллером дисплея, обозначен SSD1623L2. Действительно, это контроллер сегментированного дисплея на электронных чернилах, поддерживает до 96 сегментов. Поискав онлайн, нахожу даташит пре-релизной версии 0.1 (архивная копия здесь для потомков). Это хорошо! Если бы знали, как до этого достучаться, то могли бы подобрать код, который оно понимает, причем, как только этот код увидим, вот и все!

Оказывается, что главный микроконтроллер — ZBS242. Ну, ладно. С этим микроконтроллером я не знаком. Еще немного поищем в Интернете — и поиски выводят нас на ссылку (архивная копия здесь для потомков), по которой также упоминается тот самый ответ с StackExchange. Страница корейская, но на ней видно, что у этого чипа ядро 8051, а также довольно предсказуемое периферийное оснащение: УАПП, SPI, I2C, АЦП, ЦАП, компаратор, температурный датчик, 5-канальный ШИМ, контроллер 3-ch триак-контроллер, ИК-передатчик, функция сканирования ключей, функция RF-Wake, разнос антенн, радиомодуль, совместимый с ZigBiee, а также MAC. На картинке видно, что здесь также есть внутренний RC-генератор частотой 32 кГц, который, как заявлено, в спящем режиме может потреблять всего 1 uA. Думаю, именно эта компания сделала наш чип для Samsung. Интересно…

Еще поищем по картинкам и обнаружим, что озадачивший нас кристалл SEM9110 также был снят в упор (архивная копия здесь для потомков). Заявлено, что это ZBS243. Полагаю, это означает, что здесь целое семейство чипов: ZBS24x. Подлинно интересно.

У нас есть ниточка!


066e673f48e77b531decfcb10564e395.jpg

Открыв еще один тег сегмента, продолжаем радоваться новостям: программирующая головка подписана четкими удобочитаемыми золотыми буквами! Представляется, что у головки есть SPI, УАПП, контакт для сброса, источник питания, земля, а также контакт под названием «test», вероятно, применяемый для перехода в режим заводских испытаний. Все любопытнее и любопытнее.
7b9f762fb02653c82f5050b010059880.jpg

Логично, что древнейший представитель гипотетического семейства ZBS24x будет обозначен «ZBS240». Может, поиск по такому запросу даст нам что-нибудь интересное? Поискав по «ZBS240» и отфильтровав шлак, находим еще одну интересную страницу по-корейски  (архивная копия здесь для потомков). Создается впечатление, будто эта компания делает заказные групповые программаторы по требованию. Осмотревшись на их сайте, находим мануал (архивная копия здесь для потомков) по их программирующему устройству, и даже можем скачать утилиту для ПК, чтобы работать с таким устройством. У этой утилиты даже есть инструмент для обновления прошивки на устройстве. Я посмотрел, можно ли по этой информации догадаться, как программировать устройство, но прошивка оказалась зашифрована. По-видимому, утилита, работающая на стороне ПК, просто посылает данные через последовательный USB-порт, так что и здесь никакой полезной информации. Грустно… 

Поискав еще немного, находим даже более интересную страницу (архивная копия здесь для потомков). Что это? Это в продаже??! Определенно уже нет, верно? Я просто на всякий случай написал в эту компанию на мыло. Молчание… В качестве жеста отчаяния я поинтересовался у друга из Гонконга, не знает ли он в Корее кого-нибудь, кто мог бы связаться с этими ребятами, поскольку из их сайта следует, что в качестве платежа они принимают только перевод из корейского банка. Я просто изумился, когда он постучал мне в ответ и сказал, действительно, может достать мне это устройство через посредника, найденного в Корее! Через несколько дней девайс доставили почтой DHL!

До него можно достучаться!


Как с ним контактировать


Работает! Я могу читать чип и писать в него. Мне потребовалось некоторое время, чтобы исследовать программирующий инструмент. По-видимому, у чипа 64 КБ флеш-памяти и «информационный блок» размером 1KB, который, полагаю, используется для хранения калибровочных значений, MAC-адресов и т.п. Некоторые следы я смог перехватить, вооружившись чудесным логическим анализатором Saleae Logic,  наблюдая, как программатор делает свое дело. Мои находки можно скачать здесь. В этом архиве вы найдете следы считывания, стирания и записи в пространства INFOBLOCK и CODE. На самом деле, протокол ОЧЕНЬ прост! Тактовая частота может быть какой угодно в диапазоне от 100 КГц до 8 МГц.

Протокол ISP: разделка до косточек 


Все начинается с установки линий в нужное состояние: SCLK нижняя, MOSI верхняя, RESET верхняя, SS верхняя. Такое состояние выдерживается 20 мс. Далее RESET становится нижней на 32 мс. Затем как минимум 4 такта процессора отправляются на линию SCK с частотой 500 КГц. Затем наблюдается еще одна задержка в 10 мс, пока RESET не переводится вверх. Теперь можно установить задержку в 100 мс перед началом коммуникации. После этого может быть совершено любое количество транзакций. Несколько базовых правил: должно быть как минимум 5us между переходом SS вниз и отправкой байта, минимум 2us между концом байта и переходом SS вверх, а кратчайший период, который SS может провести в верхнем состоянии, составляет 2,5us. Следовательно, каждый байт отправляется в состоянии: SS нижнее, байт отправляется в режиме SPI 0, SS вверху. Да, конечно, SS перещелкивается для каждого байта.

Все транзакции имеют по три-четыре байта в длину. Первый байт указывает тип транзакции, самый нижний бит задает направление транзакции: ноль означает запись на устройство, единица — считывание с устройства. Команды 0x02/0x03 используются для инициирования сеансов связи. Программатор отправляет трехбайтную запись:  02 BA A5, а затем выполняет считывание, сначала отправляя команду на считывание и «адрес»:  03 BA, ведущий посылает FF, тем временем получая A5. Если это работает, то коммуникация считается установленной.

Команды 0x12/0x13 используются для считывания/записи регистров специального назначения (SFR) в CPU (это я выяснил сложнее, но в данном случае порядок не столь важен). Чтобы выбрать INFOBLOCK, SFR 0xD8 должен быть установлен в 0x80, для выбора главной области флэш-памяти он должен быть установлен в 0x00. Чтобы записать значение vv в регистр rr, нужны данные SPI 12 rr vv. Чтобы удостовериться, что значение было прочитано, его можно считать обратно, сначала отправив команду на считывание и «адрес»:  13 rr, после чего ведущий отправляет FF, тем временем получая vv.

Считывать флэш-память просто. Для этого применяется 0x09, четырехбайтная команда. После байта команды посылается адрес, сначала старший байт, потом младший. Затем ведущий отправляет FF, получая тем временем байт, который был считан. Ну да. Нужна отдельная команда на считывание каждого байта. Писать тоже просто. Для этого применяется команда 0x08. Это четырехбайтная команда. После байта команды посылается адрес, сначала старший байт, потом младший, а затем байт, который нужно записать. На запись каждого байта также требуется отдельная команда. Перед записью не забудьте операцию стирания. Чтобы стереть INFOBLOCK, требуется всего лишь одна 4-байтная команда:  48 00 00 00. Стирание в главной флэш-памяти осуществляется при помощи команды 88 00 00 00.

Итак, теперь вы знаете достаточно, чтобы тривиально запрограммировать вашу ZBS24x!

За работу!


lq5zpherrcf7hqtttmhdaxsy6he.png

Букварик по 8051


Если вы уже поднаторели в использовании 8051, можете смело пропускать этот раздел.

8051 — это старый микроконтроллер, спроектированный intel еще в седую древность. Работать с ним — страшная морока, но он по-прежнему применяется довольно часто, поскольку его дешево лицензировать (фактически — бесплатно). В чем же морока? У 8051 несколько отдельных пространств памяти. CODE — это область памяти, отведенная под код. Ее максимальный размер 64KB (16-битный адрес). В наиболее современных образцах это флеш-память. Код может считывать отсюда байты, при помощи специальной инструкции movc («MOVe from Code»). XRAM — это «внешняя» память. То есть, внешняя относительно ядра. В ней можно хранить разные штуки, но больше она почти ни на что не годится. Вот так: единственные операции, которые можно выполнять в этой памяти — это запись и считывание. Ее максимальный размер — 64KB (16-битный адрес). Как работает адресная память 8-битного адреса с адресом шириной 16-бит? Оказывается, очень медленно. Команда movx («MOVe to/from eXternal») обращается к памяти такого типа, но как указать 16-битный адрес? Для этого используется специальный регистр под названием DPTR («Data PoinTeR»), а также для работы с инструкцией movcDPTR состоит из старшего регистра DPH и младшего регистра DPL. Следовательно, записав в каждый из них по половине адреса, можно адресовать внешнюю память и память кода. Как вы догадываетесь, этот процесс быстро начинает буксовать, так как, например, чтобы скопировать участок из внешней памяти во внешнюю память, потребуется многократно тасовать значения между DPL и DPH. По этой причине в некоторых более продвинутых вариантах 8051 имеется множество регистров DPTR, но не все, и не все они реализованы одинаково.

В Intel добавили более скоростной способ доступа к подмножеству внешней памяти. В данном случае идея такова: использовать регистры R0 и R1 как регистры-указатели. Но они размером по 8 бит, откуда же берутся другие 8 бит в адресе? Они из регистра P2 (который также управляет портом 2 для контактов GPIO). Очевидно, такая практика мешает использовать порт 2 для… понимаете…GPIO. Есть способы, позволяющие сгладить эту ситуацию, но я сейчас не об этом. Таким образом, доступный нам объем памяти ограничен 256 байтами (если только не изменить динамически порт 2, чего вы, пожалуй, делать не захотите). Обычно эта память называется PDATA. Подобные обращения к памяти также делаются при помощи инструкции movx. Далее на очереди у нас SFR — различные конфигурационные регистры, при помощи которых конфигурируется периферия. К этой области памяти можно обращаться только напрямую. Такова ситуация: адрес должен быть закодирован прямо в инструкции, не будет никакого доступа через какой-либо регистр-указатель. Есть 128 байт SFR. В представленной таблице приведены списки SFR, доступных в соответствии со стандартом 8051. Серые ячейки содержат SFR, к чьим битам можно обращаться к каждому по отдельности при помощи команд побитовой обработки. Это удобно при атомарном задании контактов портов, или при активации/деактивации источников прерываний, или при проверке некоторых статусов.

Внутренняя память на 8051 немного сложна. На всех современных 8051 она имеется в количестве 256 байт. Последние 128 байт 0x80-0xff доступны только косвенно через регистры R0 и R1, но, в отличие от ситуации с внешней памятью, теперь нам доступны не только считывание и запись. Мы можем выполнять повышение на единицу (increment), понижение на единицу (decrement), сложение (add) и большинство других ожидаемых операций. На самом деле, ВСЯ внутренняя RAM доступна косвенно через эти регистры-указатели. Самые нижние 128 байт 0x00-0x7f также доступны и напрямую (адрес непосредственно закодирован в самой инструкции, точно, как при работе с SFR. 16 байт памяти в диапазоне 0x20-0x2f также побитово адресуемы при помощи команд побитовой обработки. В этой части удобно хранить переменные для булевых значений. Самые нижние 32 байта 0x00-0x1f составляют 4 банка регистров R0R7. В регистре состояния PSW есть биты, позволяющие выбирать, какой банк сейчас используется, но в реальности, поскольку во внутренней области с памятью обычно дефицит, код по большей части использует всего один банк памяти.

8051 — это машина, в основном предназначенная для работы с одним операндом. То есть: в большинстве операций аккумулятор используется как один из источников и, возможно, как место назначения. Регистры также можно использовать при множестве операций (но не со всеми), а некоторые операции допускают непрямой доступ к внутренней RAM, как описано выше. Стек — пустой восходящий, адресуемый SFR, он называется sp и находится только во внутренней RAM, его максимальный размер ограничен 256 байтами, а в реальности гораздо меньше. 

Любой образ 8051 в ROM начинается с таблицы векторов, содержащей переходы к начальному коду, который требуется запускать, а также к обработчикам прерываний. В 8051 исторически сложилось, что вектор сброса находится по адресу 0x0000, а обработчики прерываний начинаются с адреса 0x0003 и далее через каждые 8 байт. Поскольку инструкция reti применяется только для возврата от прерываний, с ее помощью можно легко обнаруживать, является ли конкретная функция обработчиком прерывания.

Заправьте всем этим канал вашего компилятра для C и затянитесь как из трубочки!  

Подходящий компилятор C для этой архитектуры существует: это C51 от Keil. Но он недешев. Также есть опенсорсный компилятор: SDCC. Он так себе, зато бесплатный. Занимаясь этим проектом, я нашел в нем лишь два великолепных бага, которые преодолевались только в обход; это совсем не плохо для опенсорсного проекта.

Приступим к анализу

void prvTxBitbang(u8 val)
                  __naked {
  __asm__(
    "  setb  PSW.5       \n"
    "  jbc   _EA, 00004$ \n"
    "  clr   PSW.5       \n"
    "00004$:             \n"
    "  clr   C           \n"
    "  mov   A, DPL      \n"
    "  rlc   A           \n"
    "  mov   DPL, A      \n"
    "  mov   A, #0xff    \n"
    "  rlc   A           \n"
    "  mov   DPH, A      \n"
    "  mov   B, #11      \n"
    "00001$:             \n"
    "  mov   A, DPH      \n"
    "  rrc   A           \n"
    "  mov   DPH, A      \n"
    "  mov   A, DPL      \n"
    "  rrc   A           \n"
    "  mov   DPL, A      \n"
    "  jnc   00002$      \n"
    "  setb  _P1_0       \n"
    "  sjmp  00003$      \n"
    "00002$:             \n"
    "  clr   _P1_0       \n"
    "  nop               \n"
    "  nop               \n"
    "00003$:             \n" 
    "  nop               \n"
    "  nop               \n"
    "  nop               \n"
    "  djnz  B, 00001$   \n"
    "  mov   C, PSW.5    \n"
    "  mov   _EA, C      \n"
    "  ret               \n"
  );  }

Легко начать с конфигурации GPIO. Как правило, вам попадутся несколько совпадающих разрядов, которые будут устанавливаться или стираться в нескольких регистрах подряд. Это логично, поскольку при активации или деактивации обычно приходится использовать пин как функцию (от GPIO), задавать его в качестве ввода или вывода и устанавливать или читать его значение. Код такого рода должен вам встретиться уже в самом начале работы. Посмотрим, что же найдется… находим, что стандартные регистры P0,  P1 и P2 действительно используются именно так, как следует обращаться с регистрами GPIO. Посмотрев, какие регистры записаны вокруг них, и что затем происходит с битами в них (считываются ли они (ввод) либо записываются (вывод)), можно предположить, что регистры AD,  AE,  AF предназначены для «выбора функции» — и представляется, что GPIO, для которых устанавливаются соответствующие биты, не используются как gpio, а все GPIO, действительно используемые как GPIO, начинают работать таким образом только после того, как соответствующий бит в одном из этих регистров будет сброшен. Я назвал их PxFUNC, где x — номер порта. Затем можно заключить, что B9,  BA,  BB контролируют направление. Всякий раз, когда в одном из них установлен бит, соответствующий GPIO только считывается, а когда бит сброшен — соответствующий GPIO предназначен только на запись. Следовательно, мы понимаем, что эти регистры контролируют направление GPIO. Я назвал их PxDIR, где x — номер порта. Итак, теперь, теоретически, я мог бы контролировать GPIO. Если бы я только знал, какие из них что делают… 

Решил просто пробовать все их подряд, пока не найду тот, который управляет «контактной площадкой TEST» на головке программатора, либо, может быть, контактными площадками «URX» и «UTX». Да любыми, на самом деле… Я обнаружил, что порт 1 пин 0 (P1.0) это «TEST»,  P0.6 это «UTX», а P0.7 это «URX». Имея управляемую GPIO, можно упростить себе жизнь, но только до той поры, пока с отладкой удастся справляться, переключая разные GPIO, и пока вам это не надоест. У меня было время в этом напрактиковаться!  

У нас есть printf!


Этой функцией я воспользовался, чтобы методом дрыгоножества (bit-bang) превратить контактную площадку «TEST» в обычный последовательный порт 8n1, а вывод собрал при помощи моего логического анализатора. Возился с ним до тех пор, пока он не стал давать такую скорость передачи информации (в бодах), которую выдерживал мой кабель-переходник между USB и последовательным интерфейсом. У меня уже была реализация printf на ассемблере для 8051. За какой-то час я напрактиковался выводить сложные отладочные строки из этого импровизированного последовательного порта. Неплохо для начала, определенно, только так и необходимо действовать, чтобы эффективно двигаться вперед!  

К этому моменту я вывел в окне значения всех SFR, чтобы как минимум ориентироваться, каковы эти значения. С дальнейшими изысканиями еще оставались кое-какие проблемы. Начну с того, что сторожевой таймер (WDT), по-видимому, устанавливался только по умолчанию и сбрасывал чип после одной секунды выполнения, поэтому все мои эксперименты должны были укладываться в секунду или менее. Я еще не знал, как управлять WDT, поэтому какое-то время мирился с этим ограничением. Как бы то ни было, ведь одна секунда — это множество циклов!  

Расширяем доступ


Теперь, имея возможность надежно выполнять код и выводить результаты, я решил разобраться, где элементы управления тактами. Почти во всех регистрах есть как минимум один регистр, который управляет различными скоростями (как минимум, скоростью работы CPU) и другой регистр, управляющий тактовой частотой (или сбросом) различных модулей. Находят их обычно так: первый обычно записывается ОЧЕНЬ рано при начальной нагрузке, и после этого его почти не трогают (если вообще трогают). У второго обычно бывает установлен бит (идут такты) или сброшен бит прежде, чем мы приступим к конфигурированию периферийного устройства. Мы не знаем, где конфигурируется различная периферия, но обычно набор SFR с близкими номерами соответствует периферийному устройству. Итак, посмотрим. Определенно здесь есть регистр, подходящий под это описание: B7. Видим, что в нем устанавливается по одному биту за раз, прежде, чем будут записаны несколько SFR с похожими номерами, и биты в нем будут сброшены после того, как обращения к нескольким SFR с похожими номерами прекратятся. Мы также видим, что исходно он записывается как 0x2F, так что здесь мы имеем дело с периферийными устройствами, которые включены заранее. Поскольку представляется, что биты задаются до операций, которые мы расцениваем как инициализацию периферийных устройств, я назову этот регистр CLKEN. Я поиграл с изменением битов в этом регистре, и создалось впечатление, будто при их сбросе ничего не происходит. В принципе, это логично, поскольку никакой периферии я не использую.

Другой регистр, записанный поблизости (грамотный код обычно инициализирует все тактовые операции вместе), в который затем повторно ничего не записывается — это 8E. Он записывается в 0x21. Я предположил, что это может быть связано со скоростью. Поэкспериментировал. По-видимому, 4 самых младших бита никак не отражаются на работе, поэтому не представляю, почему они устанавливаются в 0b0001, но вот следующие три бита, наверное, весьма существенно меняют скорость CPU (насколько я могу судить по скорости моей УАПП, подвергнутой дрыгоножеству). Старший бит, по-видимому, немного менял частоту, я предположил, что он отвечает за переключение между внутренней RC-цепью и внешним кристаллом. Три бита, которые, как я предположил, работали как делитель частоты, задают скорость работы часов, по-видимому, равную 16MГц / (1 + значение). Я назвал этот регистр CLKSPEED. Следовательно, наибольшая скорость достигается при значении 0x01, а наименьшая при 0xf1

Заставляем таймеры работать


Многие производители надстраивают в 8051 всевозможные вещи, поэтому стандартизации здесь совсем мало. Однако, большинство не трогают нормальное оснащение 8051, например, таймер 0 и таймер 1. Обратите внимание: это не непреложное правило. Например, TI значительно меняет таймеры в своих чипах серии CC. Я заметил, что в этом чипе обращения к тем регистрам, которые обычно должны конфигурировать стандартные таймеры 8051, по-видимому, происходят близко, а обработчик прерываний #1, видимо, также их затрагивает. Возможно ли? Стандартные таймеры? Я попробовал и… сработало. Полностью стандартные, по-видимому, точно соответствующие исходной спецификации. Я проверил регистр CLKEN и обнаружил, что необходимо устанавливать бит 0 (маска 0x01), чтобы таймеры работали. Убедился, что стандартный регистр IEN0 также работает как ожидалось, а номера 1 и 3 действительно управляют прерываниями для таймера 0 и таймера 1! По-видимому, частота работы таймеров составляет в точности 1/12 от 16МГц, точно, как следовало бы ожидать в стандартном 8051, работающем при 16 МГц. До сих пор я не нашел, как изменить эту частоту. То, что мы узнали, теперь открывает нам регистры TL0,  TH0,  TL1,  TH1,  TMOD,  TCON! У нас теперь есть рабочие прецизионные таймеры!

Я не поленился проверить, действительно ли в стандарте 8052 (сиквел 8051) реализован таймер 2. Нет, не реализован. 

А может быть УАПП?

void uartInit(void) {
    //ставим часы
    CLKEN |= 0x20;
 
    // устанавливаем пины
    P0FUNC |= (1 << 6) | (1 << 7);
    P0DIR &=~ (1 << 6);
    P0DIR |= (1 << 7);
 
    // конфигурируем
    UARTBRGH = 0x00;
    UARTBRGL = 0x89;
    UARTSTA = 0x12;
}
 
void uartTx(u8 ch) {
    while (UARTSTA_1));
    UARTSTA_1 = 0;
    UARTBUF = ch;
}

Было несколько строк в модуле OTA. Логично, что они должны к чему-то относиться, верно? Может быть, к отладочному последовательному порту? Это бы хорошо сочеталось с платой, на которой есть ключевые точки «UTX» и «URX». Этот код был немного запутанным, но создавалось впечатление, будто он запасает байты в каком-то буфере. Код определенно выглядел как для стандартного кольцевого буфера. Я посмотрел, куда считывается этот буфер. Оказалось, в обработчик для прерывания #0. Ооо, интересно. А может ли это быть обработчик прерываний УАПП? Казалось, код проверяет бит #1 в области, напоминающей регистр состояния (регистр 98), и, если он был установлен, то считывал байт из нашего кольцевого буфера и записывал его в регистр 99. Если другой бит (#0) был установлен в вышеупомянутом регистре состояния, то он считывал регистр 99 и вставлял результат в… другой кольцевой буфер. Ну, это чертовски соответствует как раз тем действиям, которые я ожидал бы увидеть от обработчика прерываний УАПП! Что делаем дальше?  

У каждого кольцевого буфера два указателя: один на считывание, и один на запись. Логично, что они должны инициализироваться еще до того, как буфер для чего-либо будет использоваться. Поэтому, если мы найдем, где инициализируются эти индексы, то мы, вероятно, найдем, где устанавливается УАПП, верно? Определенно выглядит так. В той функции, что инициализирует УАПП, видим, что GPIO P0.6 и P0.7 устанавливаются в режим функции,   P0.7 ставится на ввод, а P0.6 — на вывод. Еще два регистра:  9A и 9B are записываются с 0x00 и 0x89 соответственно. Тот регистр, который по моей версии работает с состояниями (регистр 98) записывается как 0x10, а затем биты 0 и 1 в нем сбрасываются. Затем в CLKEN ставится бит 5, а в IEN0 ставится бит 0. Вот, в принципе, и все, что нам нужно!  

Итак, мы называем регистр 99 UARTBUF, а регистр 98 становится UARTSTA. Мы знаем, что UARTSTA нужно установить в 0×10, чтобы этот блок работал, и знаем, что бит 0 означает, у УАПП есть свободный байт в очереди TX FIFO, а бит 1 означает, что у УАПП есть для нас байт в очереди RX FIFO. Мы знаем, что в CLKEN бит 5 активировал часы для УАПП, и что номер прерывания 0 соответствует обработчику прерываний УАПП. Это просто кладезь информации. Зная это, я смог сделать у меня в коде рабочий драйвер УАПП и отправить исходящее сообщение на нужный контакт «UTX», который, как мы теперь знаем, располагается по адресу порт 0 пин 6 (P0.6). Мы также узнали, что ключевая точка «URX» подключена к P0.7, и что это линия RX в УАПП. УАПП отправлял данные со скоростью 115 200 бит/сек, 8n1, и его никоим образом не затрагивал регистр CLKSPEED. Итак, что же это еще за два других таинственных регистра, которые дают эти магические значения?  

Я попробовал повозиться с двумя оставшимися регистрами,  9A и 9B. Быстро выяснилось, для чего они нужны. Это делители частот. Я подставил несколько значений, чтобы посмотреть, как они влияют на частоту передачи данных в бодах. Оказалось, все просто. 9A (далее именуемый UARTBRGL) был младшим байтом, а 9B (далее именуемый UARTBRGH) был старшим байтом (верхние 4 бита, по-видимому, игнорируются). Частота передачи данных в бодах — вычисляется просто как 16MГц / (UARTBRGH:UARTBRGL + 1). Это отлично объясняет значения, казавшиеся магическими — они соответствуют 115 200 бод.

По-видимому, небольшой баг связан с тем, что биты состояния можно сбрасывать программно, не затрагивая  FIFO, поэтому, если вы случайно сбросите бит, означающий «есть свободное пространство в TX FIFO» (UARTSTA.1), то прерывание никогда не наступит, и бит останется в нижнем состоянии.

Любопытно, что эти локации совпадают с правильными адресами 8051 для SCON и SBUF, которые являются в 8051 регистрами последовательного порта. Биты 0, 1 и 2 в UARTSTA действительно подходят под описания SCON из 8051, но на этом сходство исчерпано. УАПП из 8051 требует установить биты 7 и 6 в SCON в 0 и 1, только так он станет нормальным УАПП. Этот чип в данном случае требует 0 и 0. Более того, в УАПП 8051 обычно нет делителя бодовых частот, вместо которого используется таймер 1.

Сторожевой таймер и «смотри-ка!»


К этому моменту лимит исполнения в 1 секунду, гарантированный задаваемой по умолчанию конфигурацией сторожевого таймера, уже начинал меня раздражать. Я решил докопаться, где и как конфигурируется сторожевой таймер. Обычно сторожевой таймер конфигурируется в рамках собственной функции, и она невелика. Разумеется, не скажу, что так бывает всегда, но чаще всего выглядит именно так. У меня нашлось несколько кандидатов, и я попытался скопировать из каждого по очереди записи регистров в мою тестовую программу, но сторожевой таймер не поддавался. Мне требовалось исправно сбрасывать чип каждую секунду.

Занимаясь именно этим, я заметил очень странную функцию. По-видимому, она считывала регистр под номером FF, что-то туда писала, затем сбрасывала P1DIR, писала в какой-то другой регистр, а затем восстанавливала оригинальное значение в регистре FF. Странность была в том, что она устанавливала ВСЕ контакты порта 1 на вывод. Это нонсенс. В других моделях на порту 1 множество контактов сконфигурировано на вход. Кроме того, такие регистры обычно оперируются побитово, при помощи инструкций anl(логическое И) и orl(логическое ИЛИ). Такая грубая запись сразу на весь регистр выглядела отталкивающе. Что же такого в регистре FF, что нужно резервно скопировать и восстановить? Выглядело это очень странно!  

Решил провести расследование. Когда выводил в консоль значение регистра FF, получался ноль, что меня, конечно, не устраивало. Обыскал всю прошивку и заметил, что практически повсюду в ней идет запись, потом резервное копирование, а затем восстанавливается исходное значение. Я также заметил, что запись почти всегда происходит со значением 0x04 и редко с 0x00. Считывание этого регистра происходило только при резервном копировании для дальнейшего восстановления, никаких иных действий над этим значением не совершалось. На какой функционал это указывает? В принципе, именно так обычно работают элементы управления банкингом при банкинге памяти! Когда информации у вас больше, чем вмещается в адресное пространство, приходится переключаться. Такой паттерн доступа (резервное копирование перед изменением, и затем восстановление) типичен для таких практических ситуаций. Но что они могут запасать? Может ли такое быть? Неужто эти безумцы перегружают само пространство памяти SFR?!

Я написал программу, которая могла вывести на экран значения всех SFR, все 128. Затем я обращал бит 0x04 в FF SFR и снова выводил все пространство SFR. Далее программа оборачивала этот бит обратно и снова выводила все значения. Боже Всемогущий! Так и есть! Бит 2 в регистре FF действительно запасает пространство SFR. Я без сомнения видел, что при установке этого бита появляющиеся значения меняются. По-видимому, это затрагивало не 

© Habrahabr.ru