Сложная завязка
Предыстория...
В рамках моей работы над реверс-инжинирингом электронных eInk-ценников мне довелось столкнуться с интересной проблемой. Конкретная компания (Samsung Electro Mechanics/SoluM) перешла с использования сторонних чипов, происхождение которых мне удалось выявить (Marvell 88MZ100) на новый чип, который стала применять со своими ценниками следующего поколения.
Казалось, что это их собственный чип, разработанный силами компании именно для этой цели. Браться за реверс-инжиниринг такой штуки – дохлый номер. Друг дал мне несколько ценников с такими чипами – повозиться. Оказалось, они бывают двух типов: одни с сегментированным дисплеем на электронных чернилах, а другие – с обычным графическим дисплеем на электронных чернилах. Главный чип в обеих моделях один и тот же, поэтому первым делом я взялся за устройство с сегментированным дисплеем, поскольку оно проще, и на его примере легче разобраться с неизвестной системой. Было не вполне ясно, с чего начать, но, конечно же, как раз такие задачки всегда самые интересные!
Исследование
Глупо пытаться решить кроссворд, не прочитав вопросы к нему. Столь же глупо браться за реверс-инжиниринг устройства, не собрав сперва всю информацию, которая о нем уже имеется. Итак, что нам исходно известно? Протокол беспроводной передачи данных, вероятно, такой как обычно, поскольку ни одна компания не захочет мигрировать на новый либо поддерживать для своих клиентов сразу два протокола, не спеша выполняя миграцию. Старый протокол был 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. Подлинно интересно.
У нас есть ниточка!
Открыв еще один тег сегмента, продолжаем радоваться новостям: программирующая головка подписана четкими удобочитаемыми золотыми буквами! Представляется, что у головки есть SPI, УАПП, контакт для сброса, источник питания, земля, а также контакт под названием «test», вероятно, применяемый для перехода в режим заводских испытаний. Все любопытнее и любопытнее.
Логично, что древнейший представитель гипотетического семейства 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!
За работу!
Букварик по 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»), а также для работы с инструкцией movc
. DPTR
состоит из старшего регистра 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
, но, в отличие от ситуации с внешней памятью, теперь нам доступны не только считывание и запись. Мы можем выполнять повышение на единицу (inc
rement), понижение на единицу (dec
rement), сложение (add
) и большинство других ожидаемых операций. На самом деле, ВСЯ внутренняя RAM доступна косвенно через эти регистры-указатели. Самые нижние 128 байт 0x00-0x7f
также доступны и напрямую (адрес непосредственно закодирован в самой инструкции, точно, как при работе с SFR
. 16 байт памяти в диапазоне 0x20-0x2f
также побитово адресуемы при помощи команд побитовой обработки. В этой части удобно хранить переменные для булевых значений. Самые нижние 32 байта 0x00-0x1f
составляют 4 банка регистров R0
...R7
. В регистре состояния 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
нужно установить в 0x10, чтобы этот блок работал, и знаем, что бит 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
. Я без сомнения видел, что при установке этого бита появляющиеся значения меняются. По-видимому, это затрагивало не ВСЕ адреса SFR
, но многие. Я назвал этот регистр CFGPAGE
.
Теперь, сочтя, что с CFGPAGE
я разобрался, я вернулся к моей таинственной функции, которая обнуляла P1DIR
. Уже зная, что обнуляется в данном случае НЕ P1DIR
, а его странный кузен на другой странице SFR
, я попытался скопировать этот код в мою программу. И вы не поверите, я случайно наткнулся на код, отключающий WDT!!!
Исследовал код, окружающий эту функцию, поскольку обычно родственные функции в бинарниках расположены рядом друг с другом. Рядом действительно нашлось несколько функций, которые тоже обращали CFGPAGE
и получали доступ к расположенному рядом адресу P1DIR
. Поработав несколько часов методом проб и ошибок, я полностью разобрался в деталях работы сторожевого таймера. На 4-й странице конфигураций адрес BF
, по-видимому, управляет включением и сбросом сторожевого таймера; старший бит этого регистра включает или отключает в сторожевом таймере функцию сброса чипа. Я назвал его WDTCONF
. Адрес BA
(являющийся P1DIR
на конфигурационной странице 0) является регистром активации сторожевого таймера. Бит 0 здесь включает или отключает сам сторожевой таймер. Я назвал его WDTENA
.
Вплоть до этого момента я еще выяснял, как приручить сторожевой таймер. Потребовалось время, но в итоге я разобрался. В регистр BB
(теперь именуемый WDTPET
) можно записать ноль, чтобы приручить сторожевой таймер. Еще несколько минут мне потребовалось, чтобы выяснить, как сконфигурировать в сторожевом таймере задержку, поскольку в адресном пространстве явно зияла дыра между BB
и BF
. Счетчик имеет 24 бита в длину и перегружается при «приручении». Считывать его нельзя. Значение перезагрузки сохраняется в WDTRSTVALH
:WDTRSTVALM
:WDTRSTVALL
, расположенных по адресам BE
, BD
, BC
соответственно, на конфигурационной странице 4. Счетчик считает ВВЕРХ на частоте около 62 КГц, а при переполнении срабатывает. Таким образом, чтобы поставить увеличенную задержку, в эти регистры сброса нужно записать значение поменьше.
Более тонкие возможности
Программирование флеш-памяти
// вызов с отключенным irqs
voif flashDo(void) {
TRIGGER |= 8;
while (!(TCON2 & 0x08));
TCON2 &=~ 0x48;
SETTINGS &=~ 0x10;
}
void flashWrite(u8 pgNo, u16 ofst,
void *src, u16 len) {
u8 cfgPg, speed;
speed = CLKSPEED;
CLKSPEED = 0x21;
cfgPg = CFGPAGE;
CFGPAGE = 4;
SETTINGS = 0x18;
FWRTHREE = 3;
FPGNO = pgNo;
FWRDSTL = ofst;
FWRDSTH = ofst >> 8;
FWRLENL = len - 1;
FWRLENH = (len - 1) >> 8;
FWRSRCL = (u8)src;
FWRSRCH = ((u16)src) >> 8;
flashDo();
CFGPAGE = cfgPg;
CLKSPEED = speed;
}
void flashRead(u8 pgNo, u16 ofst,
void __xdata *dst, u16 len) {
u8 pgNo, cfgPg, speed;
speed = CLKSPEED;
CLKSPEED = 0x21;
cfgPg = CFGPAGE;
CFGPAGE = 4;
SETTINGS = 0x8;
FWRTHREE = 3;
FPGNO = pgNo;
FWRDSTL = (u8)dst;
FWRDSTH = ((u16)dst) >> 8;
FWRSRCL = ofst;
FWRSRCH = ofst >> 8;
FWRLENL = len - 1;
FWRLENH = (len - 1) >> 8;
flashDo();
CFGPAGE = cfgPg;
CLKSPEED = speed;
}
void flashErase(u8 pgNo) {
u8 __xdata dummy = 0xff;
u8 cfgPg, speed;
speed = CLKSPEED;
CLKSPEED = 0x21;
cfgPg = CFGPAGE;
CFGPAGE = 4;
SETTINGS |= 0x38;
FWRTHREE = 3;
FPGNO = pgNo;
FWRDSTL = 0;
FWRDSTH = 0;
FWRLENL = 0;
FWRLENH = 0;
FWRSRCL = (u8)&dummy;
FWRSRCH = ((u16)&dummy) >> 8;
flashDo();
CFGPAGE = cfgPg;
CLKSPEED = speed;
}
Я сосредоточился на образе OTA, поскольку он меньше основной прошивки. Одна деталь, которая определенно нужна в образе OTA – это возможность записи во флеш-память. Как это выглядит? Предполагается, что нам нужна какая-то функция, которая будет стирать флеш, поскольку флеш стирается блоками. Еще нужна функция записи, которая может записать страницу данных или менее. Нужна та или иная проверка записанных данных. Единственная деталь, которой отличаются реализации – как мы будем подавать данные, предназначенные для записи, во флеш-контроллер. Я не знал, как это должно выглядеть, но все оставшееся было достаточно легко найти. Верификация, вероятно, сводилась бы просто к вызову memcmp
или цикла. Операции стирания флеша изнашивают флеш-память, поэтому перед стиранием страницу нужно проверять, а затем выполнять операцию.
В поисках проверки перед стиранием быстро нашел функцию, которая создает область из 0x400
байт в XRAM
, полную байт 0xFF
. Затем область памяти CODE
сравнивается с этим буфером, и, если они не равны, то прерывания деактивируются, и затрагиваются некоторые SFR
на конфигурационной странице 4. Размер страницы во флеш-памяти явно составляет 1024 байт. Проверяя, в каких еще местах затрагиваются те самые SFR
, находим оставшийся флеш-код. Из контекста понятно, что и как делают эти регистры. В данном случае интересно, каким образом данные подаются в блок контроля флеш-памяти. В этом управляющем блоке явно есть блок DMA. В блок управления флеш-памятью подается адрес XDATA
, и данные поглощаются непосредственно оттуда. Как бы круто!
К тому моменту я еще не был уверен, как читать INFOBLOCK. По-видимому, код OTA его не касался, но откуда-то он ДОЛЖЕН был считываться – ведь в нем есть данные. Я проверил главный образ и заметил фрагмент кода, затрагивавший те же SFR
из флеш-памяти, но иным образом. Проведя еще кое-какой анализ, я смог воспроизвести правильное считывание INFOBLOCK. Любопытно, что тем же методом можно считывать и любой другой блок флеш-памяти, но это незачем делать, поскольку для считывания флеш-памяти нужно всего лишь прочитать область памяти CODE
. INFOBLOCK доступен только через блок управления флеш-памятью. Как при варианте записи во флеш-память, так и при варианте считывания из нее, управляющий блок использует прямой доступ к памяти (DMA) и записывает в XDATA
.
Один регистр DF
(FWRTHREE
) не поддавался никаким попыткам его объяснить. В него всегда шла запись со значением 0x03
, не знаю, почему. Мой код для доступа к флеш-памяти делает то же самое. Регистр D8
(FPGNO
) записывается с номером страницы флеш-памяти. Основные страницы флеш-памяти пронумерованы от 0 до 63, у INFOBLOCK номер 128. DA
:D9
(FWRSRCH
:FWRSRCL
) – это источник блока DMA в блоке управления флеш-памятью. Для записи во флеш он содержит адрес XDATA
, где мы находим данные для записи. Для считывания флеша ищется байтовое смещение на исходной странице, и с этого смещения начинается считывание. DC
:DB
(FWRDSTH
:FWRDSTL
) – это назначение для DMA в блоке управления флеш-памятью. Для записи во флеш он будет содержать байтовое смещение на странице назначения, и с этой точки начнется запись. Для считывания флеша используется адрес XDATA
, по которому записываются данные, полученные при считывании. DE
:DD
(FWRLENH
:FWRLENL
) – это длина тех данных, которые должен передать блок DMA, минус единица.
Запись во флеш-память как таковая запускается путем установки бита еще в одном SFR
. Различные биты в нем устанавливаются и для управления другим кодом, по-видимому, не связанным с флеш-памятью, так что я заключил, что этот регистр, наверное, инициирует различные действия. Я назвал этот регистр D7
на странице конфипгурации 4 TRIGGER
. Состояние завершения также проверяется в регистре, который, по-видимому, совместно используется и другим кодом. Этот регистр с конфигурационной страницы 4 у CF
я назвал TCON2
, а почему нет? Также был регистр на C7
, также используемый совместно с другим кодом, который, по-видимому, конфигурировал, какую именно операцию выполнять. Я назвал его SETTINGS
. 0x30
был записан в него логическим ИЛИ для стирания + записи, 0x18
для записи флеша, 0x08
для считывания флеша. Я догадался, что бит 0x08
означает «ожидается передача данных», 0x10
означает «во флеш», а 0x20
«стирай». Это логично, учитывая, какие значения мы видим, и какие операции здесь выполняются.
Считывание и запись во флеш удавались на славу, но стирание по-видимому не работало. Вместо стирания страницы с заданным кодом, почему-то все время стиралась та страница, на которой находился код, запрашивающий стирание. Очевидно, данная проблема заключалась не в том коде, что содержался на этом устройстве, это я что-то не так делал. Проверял, проверял и снова проверял, чтобы убедиться, что мой код совпадает с заводским. Совпадал. Что же не так? Работал несколько дней, пока не осознал, что заводской код работает на частоте 4МГц, а мой на 16МГц. Могла ли суть быть в этом? Оказалось, именно так! Я изменил мой код для стирания флеш-памяти, так, чтобы сохранялся текущий частотный делитель, и сбавил ход часов до 4МГц на срок стирания флеш-памяти. Прошло нормально, поскольку этот код уже работает с отключенными прерываниями.
Еще одна тонкость этого блока управления флеш-памяти заключается в том, что в нем, по-видимому, не предусмотрена простая операция «стереть». Я поразмышлял о назначении подходящих if-битов в регистре SETTINGS
, и тогда мне показалось логичным, что при установке в 0x20
или 0x30
должно произойти простое стирание. Единственный способ стереть подобное — произвести операцию «стирание+запись», при которой записывается как минимум один байт (поскольку в FWRLENH
:FWRLENL
никак не представить нулевую длину. Чтобы выполнить простое стирание, я просто запрашиваю запись единственного байта 0xFF
. Работает
SPI
В сущности, все SPI-драйверы одинаковы. Поступает байт на вход, возвращается байт на выход. Разумеется, у некоторых есть DMA, а некоторые управляются прерываниями, но 99% из них в небольших системах управляются программно, а где-то есть простая функция u8 spiByte(u8 byte);
.
Логично было далее заглянуть именно в SPI. Поскольку известно, что SSD1623L2
общается с SPI, а также мы знаем подробности организации такого общения, нужно просто посмотреть в код и выяснить, какая его часть должна делать эту операцию. Точно, как в судоку, учитывая, как много мы уже знаем, этот поиск труда не составит. Посмотрев в даташит SSD1623L2
видим, что у первого отправленного байта номер регистра записан в битах 1..6, а бит «записи» находится на позиции #7. Длина всех регистров – по 24 бита. Логично, что программист напишет код, который станет принимать в качестве параметра номер регистра, сдвигая его влево на единицу, возможно, логическое-или-в 0x80
, если затребована запись, а затем передавать три байта. Не все программисты действуют логично, но такое допущение неизмеримо помогает при реверс-инжиниринге. Просмотрев код, весьма просто заметить функции, по виду делающие именно это. Некоторые добавляют 0x80
, некоторые нет. Все они вызывают ту самую таинственную функцию для каждого байта. Итак, предполагаем, что некоторые выводят текст на экран, некоторые считывают. Давайте возьмемся за саму таинственную функцию.
На самом деле, здесь все проще простого. Она переключает CFGPAGE
на 4, затем записывает в регистр ED
значение 0x81
, записывает байт, который должен быть отправлен в EE
, записывает 0xA0
в EC
, делает задержку в 12 микросекунд, устанавливает бит 3 в EB
, считывает полученный байт из EF
, сохраняет 0x80
в ED
. Вот и все. Как все это осмыслить? Как и ранее, опираясь на уже известное.
0x80
и 0x81
отличаются всего одним битом, и мы устанавливаем его до начала эксплуатации SPI, а по окончании работы сбрасываем, так что это, видимо, «активирующий» бит какого-то рода. С другой стороны, значение 0xA0
буквально фонит конфигурацией какого-то рода. Регистр EB
по-прежнему тайна. Но, если я воспроизведу этот код, не записывая в него, все будет работать, поэтому делаю вывод, что не многое от этого регистра зависит. Определенно EE
это SPITX
, а EF
это SPIRX
. Я назвал ED
- SPIENA
и EC
- SPICFG
.
Осталось охарактеризовать, что делают биты в SPICFG
. Поработал немного методом проб и ошибок, вооружившись при этом логическим анализатором. Бит 7 должен быть установлен, бит 6 сброшен. Бит 5 начинает передачу байта SPI и самостоятельно сбрасывается, закончив с ней. Биты 3 и 4 задают тактовую частоту, можно выбрать из значений: 500КГц, 1МГц, 2МГц, 4МГц. 2 – это стандартный конфигурационный бит CPHA
для SPI, бит 1 это CPOL
. Бит 0 по-видимому нарушает RX. Предполагаю, он может конфигурировать блок для полудуплексного режима (в линии MOSI
). В общем, не так это и сложно.
Пин за пином, быстро находим конфигурацию GPIO и видим, что P0.0
это SCLK
, P0.1
это MOSI
и P0.2
это MISO
. Выискивая, где конфигурируются эти GPIO, мы также видим, как нужен бит CLKEN
для SPI: это бит 3. Чудесно – теперь у нас есть рабочий SPI!
Определяем температуру
volatile u8 __xdata mTempRet[2];
void TEMP_ISR(void) __interrupt (10)
{
uint8_t i;
i = CFGPAGE;
CFGPAGE = 4;
mTempRet[0] = TEMPRETH;
mTempRet[1] = TEMPRETL;
CFGPAGE = i;
IEN1 &=~ 0x10;
}
int16_t tempGet(void)
{
u16 temp, sum = 0;
u8 i;
CLKEN |= 0x80;
i = CFGPAGE;
CFGPAGE = 4;
TEMPCFG = 0x81;
TEMPCAL2 = 0x22;
TEMPCAL1 = 0x55;
TEMPCAL4 = 0;
TEMPCAL3 = 0;
TEMPCAL6 = 3;
TEMPCAL5 = 0xff;
TEMPCFG &=~ 0x08;
CFGPAGE = i;
IEN1 &=~ 0x10;
for (i = 0; i < 9; i++) {
// запуск
IEN1 |= 0x10;
// ожидание
while (IEN1 & 0x10);
if (i) { // сначала пропустить
sum += u8Bitswap(mTempRet[0]) << 2;
if (mTempRet[1] & 1)
sum += 2;
if (mTempRet[1] & 2)
sum += 1;
}
timerDelay(TICKS_PER_S / 1000);
}
// выключить
CLKEN &=~ 0x80;
return sum / 8;
}
Дисплеи с e-Ink обновляются иначе, в зависимости от текущей температуры, поэтому для их правильного обновления критично точно знать температуру окружающей среды. В зависимости от температуры выбираются правильные формы волн. Здесь нам пригодятся знания со стороны. Поэтому, если найдем, где волновые формы загружаются в контроллер дисплея, можно найти, где делается выбор. От этого места можно напрямую пройти до той точки, где измеряется температура, верно? Сделав так, выходим ровно на одну функцию, от вывода которой и зависит, какая волновая форма будет использоваться. Должно быть, эта и есть! Кстати: обычно температурные датчики прикрепляются к АЦП – почти никто не делает их в отдельном исполнении. По это [пока] не важно.
Все начинается с установки бита 7 в CLKEN
и заканчивается его сбросом, так что мы как минимум знаем, что именно так мы включаем и выключаем температурный датчик (или АЦП). Функция переключает CFGPAGE
на 4, затем записывает ряд значений в ряд регистров. Все значения константны. 0x81
-> рег. F7
, 0x22
-> рег. E7
, 0x55
-> рег. E6
, 0x00
-> рег. FC
, 0x00
-> рег. FB
, 0x03
-> рег. FE
, 0xFF
-> рег. FD
, затем биты 0x81
сбрасываются в F7
. После этого CFGPAGE
восстанавливается, а затем сбрасывается бит 4 в регистре A1
. Такова, по-видимому, начальная настройка. После того, как пятикратно произойдет определенная процедура, усредняются результаты всех операций кроме первой. После этого над полученным таким образом средним выполняется много математики, в частности, с использованием значений из INFOBLOCK – вероятно, это калибровочные значения. Затем возвращается результат. Давайте подробнее рассмотрим детали.
В процессе просто устанавливался бит 4 в регистре A1
, устанавливался глобальный бит и затем в режиме активного ожидания проводим время, пока бит не будет сброшен. Конкретные усредняемые значения, по-видимому, берутся от какого-то глобального. Это странно… Я поискал, где оно записывается, и нашел его в обработчике прерывания #10. По-видимому, так сбрасывался бит 4 в регистре A1
, затем происходило переключение на конфигурационную страницу 4, считываются значения из регистров F8
и F9
, и с ними проделываются какие-то странные вещи, а затем записывается это глобальное значение. Но что же делается с этими значениями?
Мне просто в глаза кололи константы 0x55
, 0xAA
, 0xCC
и 0x33
. Возможно ли такое? Мог ли кто-то настолько затупить, что… ну да. Это константы для хитрого способа обратить порядок битов в байте. Хитро, но только на более продвинутых процессорах. На 8051 такой подход очень неэффективен. Но… почему? Кажется, что, какой бы IP (указатель команд) они ни лицензировали для измерения температуры, он выдает результат, в котором биты расположены в обратном порядке. Почему эта проблема должна находить разрешение на программном уровне фирменного чипа – большой вопрос. В конце концов, обращать порядок битов на аппаратном уровне не сложнее, чем поменять порядок нескольких проводков… Что это дает? Не знаю. На самом деле, я этого так и не понял.
Почти никто не проектирует выделенный счетчик команд для температурного датчика, эту штуку просто подключают к АЦП. Как только мне удалось повторно реализовать этот код и убедиться, что работает он очень хорошо, я попытался поменять все эти регистры. Большинство из них влияли на коэффициент усиления температурного датчика, некоторые не давали никакого эффекта. Если бы это был обычный АЦП, мы ожидали бы, что некоторые биты переключали бы его на другой вид ввода и давали бы совершенно иное значение. К сожалению, этого не происходило. Он в самом деле выглядел как обычный температурный датчик. Это подтверждается еще и потому, что эти регистры больше нигде не затрагиваются. Чертовски странно, но ладно…
Поскольку запись почти во все из этих регистров происходит лишь однажды, и именно эти значения, а изменение их сказывается на измеряемом значении, я решил просто назвать их все значениями калибровки температуры. Следовательно, знакомимся с TEMPCAL1
(рег. E6
), TEMPCAL2
(рег. E7
), TEMPCAL3
(рег. FB
), TEMPCAL4
(рег. FC
), TEMPCAL5
(рег. FD
) и TEMPCAL6
(рег. FE
). Я назвал F7
TEMPCFG
, так как он используется неоднократно и, по-видимому, действительно управляет загрузкой калибровочного значения. Результаты выдаются в TEMPRETH
(рег. F8
) и TEMPRETL
(рег. F9
). Результаты имеют по 10 бит в длину, выровнены по верхнему концу 16-битного регистра результатов, с обращенным порядком бит.
Я также заметил, что бит 3 в TEMPCFG
устанавливается, когда заканчивается создание образца. Любопытно, что заводской код его не проверяет, полагаясь вместо этого на прерывание. Но, на самом деле, это пригодилось в расшифровке назначения регистра A1
. Как видите, классический 8051 ограничен 7 источниками прерываний, поскольку у нас 8 бит в регистре IEN
, а бит 7 зарезервирован для активации глобального прерывания. Итак, как же управлять прерываниями с номерами 7 и выше? На самом деле, тут как дикий запад, что хочешь, то и делаешь. Но здесь у нас есть аппаратный элемент, вызывающий прерывание номер 10, и, воспользовавшись битом, можно определить, когда оно было сделано. Это отлично подходит для эксперимента. в котором мы хотим узнать, как активируются и деактивируются прерывания выше 7. Нужно было просто как следует повозиться с этим кодом, пока не избавишься от прерывания, но образец при этом создается. Искать пришлось недолго. Должно быть, это A1
! Я назвал его IEN1
. Я не уверен, какова здесь функция бита 0, но биты 1 и выше контролируют активацию прерываний номер 7 и выше. Это мне удалось подтвердить позднее. Итак, готово – мы документировали еще одно периферийное устройство, тем самым обнаружив еще больше странностей…
I2C
На данном этапе я открыл более крупный ценник с e-Ink, оснащенный тем же чипом. Это была 2,9-дюймовая модель с графическим дисплеем на электронных чернилах и с NFC!!! Опять же, здесь пригодятся сторонние знания. Большинство устройств с NFC сами вам подробно сообщат, чем являются, если спросите вежливо. Это хорошо, поскольку чип на плате, обрабатывающий NFC, был слишком мал, чтобы как следует его обозначить. Просканировав его при помощи NFC и сверившись с ID устройства, узнаем, что это NXP NT3H1101 (архивная копия здесь для потомков). С этой очень удобной страницы можно скачать даташит – и сразу становится понятно, как должна протекать коммуникация с этим чипом. Полезная информация! (Тут вся информация полезная). Раздражает только, что I2C адрес этого устройства не фиксирован, но для него можно задать любое значение; правда, предусмотрено значение по умолчанию. Азбука реверс-инжиниринга: в 99,9% случаев значения, заданные по умолчанию, не меняются. Готов поспорить, заданный по умолчанию адрес I2C также не изменился!
Найти двоичный аналог для 0x55
довольно легко – не так часто встречается такое значение. По-видимому, все они делаются до вызовов к одной из двух функций. Логично, что они должны быть связаны с I2C. Причем, во всех случаях перед этими вызовами устанавливается бит 4 в CLKEN
, который затем сбрасывается. Теперь мы знаем, что I2C активируется через этот бит. Давайте посмотрим, что делают эти функции. Некоторые копируют данные из предоставленного параметра в самом начале, некоторые делают это в конце. В середине все они записывают некоторые глобальные вещи, устанавливают глобальный бит, сбрасывают бит 4 и устанавливают бит 5 в регистре 95
и дожидаются, пока он будет сброшен. Хм, работает примерно как температурный датчик. По-видимому, бит 2 в IEN1
активирует прерывание.
Давайте посмотрим, где находится тот обработчик прерываний, который затрагивает эти глобальные значения. Действительно, его номер прерываний 8, как и ожидалось. Он устанавливает CFGPAGE
в 0, а затем считывает регистр 91
. 3 самых младших бита игнорируются, а остальные биты используются в switch-case, чтобы решать, что делать. Этот код оказался немного путаным, поэтому я решил поэкспериментировать. Прикрепил логический анализатор к линиям, идущим к чипу NFC и быстро обнаружил, где здесь SDA
, а где SCL
. Это было легко, поскольку для данного чипа есть даташит.
Представляется, что сброс бита 4 в регистре 95
ни на чем не отражается, но установка бита 5 приводит к тому, что условие START на шине принимает значение «истина». Обусловливается прерывание. Если сделать то же самое при помощи встроенного обработчика и считав 5 старших бит в регистре 91
, видим, что они имеют значение 0x08
. Тогда адресный байт сохраняется с битом R/W (чтение/запись) в регистре 94
, и сбрасывается бит 3 в регистре 95
. Также следует отметить, что ВСЕ пути, ведущие через этот обработчик прерываний, приводят к сбросу бита 3 в регистре 95
. Полагаю, это «бит, требующий прерывания». Пока еще не во всем разобрался, но мы уже можем назвать некоторые регистры. Кажется, что все регистры I2C находятся на конфигурационной странице 0.
Я собираюсь вызвать 91
I2CSTATE
, поскольку именно I2C он и содержит и никогда не считывается по какой-либо иной причине. Я никогда не видел, чтобы менялись или вообще как-либо использовались три самых младших бита. I2CBUF
– так я назову 94
, поскольку данные прокачиваются через него по конвейеру, а 95
в дальнейшем будет именоваться I2CCTL
, поскольку чтобы делались дела, в него нужно что-то записать.
Разбираемся дальше, и обнаруживаем, что, когда адресный байт отправлен, можно получить одно из четырех значений состояния. Если посланный нами адресный байт требовал доступа на запись, то состояние станет 0x18
, если он был подтвержден (ACK), и 0x20
, если нет. Если посланный нами адресный байт требовал доступа на чтение, то состояние станет 0x40
, если он был подтвержден (ACK), и 0x48
, если нет. Обработка NAK (байт не подтвержден) весьма проста. При установке бита 5 в I2CCTL
условие STOP на шине принимает значение «истина».
Отправлять данные в режиме записи легко. Байт просто записывается в I2CBUF
. Если отправленный байт подтвержден (ACK), то состояние станет 0x28
а если нет – то 0x30
. Чтобы спровоцировать перезапуск, ставим бит 4 в I2CCTL
— работает. При завершении выполнения команды RESTART на шине состояние становится 0x10
.
Если мы захотим прочитать информацию, то, после отправки бита перезапуска и адресного байта в режиме считывания, как только увидим состояние 0x40
, мы сможем решить, как отреагировать на следующий байт, который мы получим — ACK или NAK. Чтобы подтвердить его (ACK), устанавливаем бит 2 в I2CCTL
, а чтобы не подтвердить (NAK) – сбрасываем этот бит. С возвратом обработчика байт будет получен. Когда это будет сделано, мы увидим состояние 0x50
, если подтвердили байт, и 0x58
, если не подтвердили его. Так или иначе, в I2CBUF
будет содержаться полученный байт.
Просмотрев код инициализации и повозившись с нашей копией, обнаруживаем, что бит 7 в I2CCTL
управляет, будет ли периферийное устройство провоцировать прерывания. Если не будет, то этот регистр инициализируется на 0x43
. Полагаю, таким образом блок конфигурируется для работы в ведущем режиме. Поскольку у меня нет образца кода для ведомого режима, дальше я этот вопрос не исследовал, но уверен, и ведомый режим поддерживается. Это можно сделать, но мне лениво :).
В регистр 96
также записывается информация во время инициализации, а затем больше не изменяется. Это хорошо коррелирует с одним битом информации, которого нам до сих пор не хватает – указывающим, как устанавливается тактовая частота. Поэкспериментировав с этим регистром (который теперь называется I2CSPEED
) видим, что у него существует сложная взаимозависимость с тактовой частотой, но, после нескольких десятков попыток я пришел к следующему: rate = 16MHz / ((dividerB ? 10 * (1 + dividerB) : 12) << dividerA)
, где dividerA это три самых младших бита I2CSPEED
, а dividerB – следующие 4. Старший бит, по-видимому, не используется.
То, что начальная настройка GPIO происходит поблизости от точки инициализации периферии, по-видимому подразумевает, что в данном случае важны пины P1.4
и P1.5
.
Все работало, но была одна тайна. Когда прерывание для данного блока было активировано (в IEN1
), бит 2 также ставился в регистре A2
. Поскольку IEN1
находится по адресу A1
, подозреваю, это связано с прерыванием. Я так и не выяснил как следует, что он делает, и никакой код кроме кода начальной настройки I2C его не использует. Я предварительно назвал его I2CUNKNOWN
, хотя, вероятнее, что он связан с прерываниями, чем с I2C. Как бы то ни было, теперь мой код может выполнять транзакции с I2C, работая как ведущий!
Обнаружение изменения пина
Прошивка ценника пробудилась, когда его сканировали устройством с функцией NFC. У бортового чипа NFC предусмотрен пин «обнаружения поля», подключенный к главному микроконтроллеру. Совпадение? Не думаю! Должен быть способ обнаружения изменений на пине. Он даже выводит чип из спящего режима (экономия энергии). Кроме того, некоторое время уходит на отрисовку электронными чернилами, и во время такого ожидания чип, пожалуй, должен продолжать спать. Дисплей просигнализирует об окончании отрисовки, изменив сигнал «BUSY». Итак… имеем два случая, в которых CPU должен обнаруживать изменение на пине и, скорее всего, речь не идет о цикле активного ожидания. Найти первый описанный случай было бы сложно – я еще не знал точно, где находится этот код для погружения в спячку. Второй случай, напротив, находился очень легко – я имею в виду, легко нашел код для отрисовки на экране. Опять же, здесь полезно опираться на имеющиеся знания. Я знал, какая команда отвечает за «обновление экрана» на практически всех существующих чипах для дисплеев с электронными чернилами. Просто ввел ее и посмотрел, что будет. Там было много кода, затрагивались многие SFR
. Я принялся экспериментировать с теми немногими, которые увидел. Сделал несколько обоснованных предположений: все пины должны быть в состоянии инициировать обнаружение изменений. Так бывает не всегда, но на обоснованное предположение обычно тянет. Я предположил, что о каких бы конфигурационных регистрах ни шла речь, они будут последовательными и работать с тремя портами. Также я предположил, что изменение пина должно обеспечивать прерывание, а не просто будить устройство. Логично, что количество конфигурационных регистров довольно предсказуемо. На каждый пин нам потребуются ENABLE, STATUS и, скорее всего, DIRECTION. Кроме того, регистры, относящиеся к обнаружению изменений GPIO, скорее всего, будут находиться поблизости от других регистров, конфигурирующих GPIO.
Исходя из этого, я провел несколько экспериментов, поскольку мог без труда переключать, как минимум, некоторые пины (например, TEST). Кроме того, потратил некоторое время, чтобы посмотреть, как развивается моя текущая карта SFR
. Еще не забыл посмотреть регистры BC
, BD
и BE
на конфигурационной странице 0. Несколько экспериментов показали, что они управляют подтяжкой каждого пина. Правда, никогда не видел никаких конфигураций, которые позволяли бы «потянуть пин вниз». Назвал их PxPULL
.
После нескольких экспериментов стало понятно, что здесь по три регистра на порт, и они управляют прерываниями при изменении пина. PxLVLSEL
(A3
, A4
, A4
) выбирает желаемый уровень (0 = высокий, 1 = низкий). PxINTEN
(A6
, A7
, A9
) обеспечивает отслеживание изменений пина на аппаратном уровне. PxCHSTA
(AA
, AB
, AC
) хранит статус обнаружения (бит установлен = что-то изменилось). Другие эксперименты показали, что номер прерывания при изменении пина – это 11. Работает хорошо, и мне даже удавалось разбудить чип из энергосберегающего режима (подробнее об этом ниже).
Второй DPTR
Регистры 84
и 85
таинственным образом приберегались на фоне всех операций подкачки в CFGPAGE
и хранили все 8 бит, записанные в них. Во многих вариантах 8051 именно там должен был бы находиться второй регистр DPTR
. Но, если так, то как же переключиться на него? Каждый делает это по-своему. Решил попробовать. Написал на ассемблере программу, чтобы по очереди обращать каждый бит в каждом регистре и проверять, совпадает ли запись целого DPTR
(специальная инструкция) с последующим считыванием DPL
и DPH
(обычный доступ к SFR
). Предсказуемо, что многие из этих вещей нельзя так просто переключать, не обвалив программу. Но, потренировавшись аккуратно пропускать то те, то другие, я вычленил бит 0 в 92
. Ну да… Именно он это и делает. Как и во многих 8051, я назвал этот регистр DPS
, что означает «выбор указателя данных». Регистры 84
и 85
я назвал, естественно, DPL1
и DPH1
.
Другие эксперименты.
Некоторые эксперименты показали, что два самых младших бита в PCON
(ожидание и сон) работают, как и положено в спящем режиме для 8051 (хотя, можно настроить и сон в энергосберегающем режиме). Я также отметил, что при установке бита 4 деактивируется XRAM
. Так экономится еще немного энергии в спящем режиме!
Интересны регистры в диапазоне B2
..B6
. По-видимому, они изменяются в зависимости от инструкций, выполняемых по месту их положения. Внимательно все рассмотрев, я осознал, что B4
:B5
это всегда актуальный PC
!!! Зачем это кому-то могло понадобиться – не знаю. Назвал их PCH
и PCL
. Они предназначены только для чтения. Но что насчет других регистров в этом диапазоне? B2
и B3
, по-видимому, связаны с условными переходами. При длинном переходе (например, когда выполняется ljmp
, lcall
или ret
), они, по-видимому, хранят точку назначения перехода. При коротких переходах (таких как sjmp
), B2
по-видимому выясняет смещение. Странные штуки, но бесполезные, поэтому я в них дальше не вникал. Остальные регистры назвал PERFMONx
.
Сон в энергосберегающем режиме
Люди есть люди, и ничто человеческое им не чуждо. Людям нравятся круглые числа. Нравится точность, даже если она не нужна. Это очень помогает при реверс-инжиниринге. Например, какой отклик вызывает у вас константа 0x380000
? Никакого? Пожалуй. А как насчет 0x36EE80
? За нее взгляд уже цепляется. Что, черт возьми, это означает? Переводите ее в десятеричную систему и видите: 3 600 000. Ну вот, это час, выраженный в миллисекундах. Такое значение может быть полезно, пожалуй, только в случае долгого сна в энергосберегающем режиме. Я уже устал считать, сколько вещей я «обратно спроектировал», опираясь на константы такого рода, проливающие свет на то, где реализуется сон!
Вот какие константы на этом устройстве передавались интересующей меня функции: 1 5000 2 000 5 000 10 000 3 600 000 1 800 000 0xffffffff. Вполне понятно, что это указание длительности в миллисекундах. Последняя – это, вероятно, заглушка для «вечно или почти вечно».
Здесь почти не было шансов понять, что делают большинство регистров, так как они используются кодом почти исключительно в спящем режиме. Некоторые были в SFR
, а некоторые в пространстве MMIO
. Мне удалось скопировать код и воспроизвести его. В частности, меня заинтересовало, что таймер сна может работать на двух скоростях: с частотой 32КГц и 1Гц. Это 24-битный таймер, при работе с которым кратчайший возможный сон длится примерно 30 мс, а самый долгий может продлиться примерно 194 дня! Подробнее – в SDK.
Радио
Радио обычно требует обширной конфигурации, поэтому в плотном пространстве SFR
для него слишком тесно. В большинстве 8051, оснащенных радио, для решения этой проблемы используется MMIO
. Ввод/вывод, отображаемый на память в 8051 обычно просто отображается на адресное пространство XRAM
. Просмотрев код по диагонали, я понял, что радио на этом чипе находится в MMIO:df00 — MMIO:dfff
.
Путь RX
Опять же, я решил начать с образа OTA. Он достаточно мал, и это упрощает анализ. Вскоре стало понятно, что образ OTA не посылает никаких радио-пакетов, а только получает их (подтверждения автоматически отправляются на аппаратном уровне, что типично для большинства чипов ZigBee). Но это хорошо! Благодаря этому нам достаточно проанализировать всего половину драйвера, а значит, задача вдвое упрощается!
Когда я стал искать, где код OTA получает данные, создавалось впечатление, что здесь есть буферная очередь. Что это такое: это очередь, содержащая отдельные байты, каждый из которых является указателем на список буферов. Код, который, как казалось, получает пакеты и обрабатывает полученные пакеты, вынимал буфер из очереди, обрабатывал его, после чего ставил его в другую очередь. Очень простая схема. В одной очереди хранятся буферы, полные полученных данных, в другой очереди хранятся пустые буферы, готовые принять новые полученные данные. Достаточно ясно.
Немного осмотревшись, быстро обнаруживаем, где обращение к очередям происходит иным образом: вывод буфера из очереди «порожних» и постановка в очередь полных. Это обработчик для прерывания #5! Сам обработчик прерываний был весьма прост, при условии, что установлен бит TCON2.2
, он сохранял 0xC6
в MMIO:df48
, выводил буфер из очереди, копировал в него байты и ставил в другую очередь. Но откуда он копировал байты? Откуда получал длину копии? И то, и другое бралось из буфера в XRAM
, в который он не писал! Эту тайну мне так и не удалось разгадать.
На этом поиски не закончились. Ключевая роль отводилась прерыванию 4. Его обработчик оказался еще проще. Он проверял бит 5 в MMIO:dfad
(назову его RADIO_IRQ4_pending
, и, если он установлен, он вызывает процедуру, не вызываемую нигде более. Эта процедура считывала SFR
FA
, проверяла, что значение в нем меньше или равно 128, читала MMIO:df98
, проверяла, что при увеличении на единицу оно станет равно предыдущему значению. Если что-либо из вышесказанного не выполнялось, то она сохраняла 0xC6
в MMIO:df48
, в противном случае выбиралась конфигурационная страница 4, первое считанное значение сохранялось в глобальной переменной, которая в дальнейшем обозначала длину. Это значение минус единица сохранялось в D5
, а указатель на буфер, откуда впоследствии копировались данные, сохранялся в D4
:D3
. Затем устанавливался бит 2 в TRIGGER
.
Здесь, опять же, помогает знание контекста. 127 – максимальное значение, которое может иметь действительный пакет 802.15.4, и в эту длину входит 2-байтный циклический избыточный код (CRC), но не входит длина самого байта. Поэтому, догадываюсь, что FA
это полученная длина (с учетом длины байта и CRC). Я назвал его RADIO_GOTLEN
. В таком случае логично, что MMIO:df48
(теперь названный RADIO_rxFirstByte
) может быть первым полученным байтом (байтом длины). С оставшимися регистрами все ясно: D5
это длина DMA для RX DMA (теперь называется RADIO_RXLEN
), D4
:D3
это разобранный на части указатель на место назначения RX DMA (RADIO_RXPTRH
и RADIO_RXPTRL
соответственно).
Тогда все сложилось. Прерывание номер 4 срабатывает, как только радио получит пакет во внутренний буфер. Бит 5, установленный в RADIO_IRQ4_pending
(это теперь он называется RADIO_IRQ4_pending
) сообщает нам, что это произошло. Мы приступаем к первичному обследованию пакета (убеждаемся, что длина его в разумных пределах), а затем запускаем DMA из внутреннего буфера в XRAM
, если все хорошо. Если нет, то записываем 0xC6
в MMIO:df48
. Логически это можно сравнить с «опорожнением RX FIFO», следовательно, этот регистр теперь называется RADIO_command
. Если с пакетом все было нормально, и операция DMA завершилась, то ставится бит 2 в TCON2
, и срабатывает прерывание 5. Здесь мы, опять же, записываем «опорожнение RX FIFO» в RADIO_command
. Это целесообразно, поскольку мы уже вытянули данные методом DMA. Затем данные копируются, и дело сделано!
В большинстве радиомодулей полученный циклический избыточный код на более высоких уровнях не предоставляется – его просто проверяют и дают в ответ единственный бит состояния со значением «да» или «нет». Как обычно, целесообразно предположить, что все работает «штатно». Проверяешь – действительно штатно. Большинство радиомодулей ZigBee вместо этого сообщают в этих двух байтах LQI (индикатор качества радиоканала) и RSSI (показатель уровня принимаемого сигнала), а не CRC. В данной модели радио работает примерно так же. Ну, почти. По-видимому, первый байт всегда 0xD0
, но второй, как кажется, действительно содержит LQI (в младших 7 битах) и состояние CRC (в бите 7). На самом деле, функционально это очень похоже на принцип работы радио Chipcon. Команда 0xC6
также означает «опорожнение RX FIFO» для радиоустройств chipcon (now TI)! Многие другие вещи не совпадают, но команды – НАПРОТИВ, и это помогло мне сориентироваться в других элементах этого радио-стека!
Подробнее о радио
Если рассмотреть, как код OTA инициирует радио, видно, что МНОЖЕСТВО регистров затрагиваются лишь однократно, в них записываются какие-то значения, которые кажутся совершенно случайными. Скорее всего, многие из них калибровочные. Любой регистр, запись в который происходит однократно (или неоднократно, но вносится одно и то же значение) – калибровочный. Я пропущу скучные подробности о затронутых регистрах, но расскажу о рабочем инициирующем коде, который есть в SDK.
Здесь, опять же, наблюдаем, как множество значений записывается в регистр RADIO_command
. Записанные значения совпадают с теми, которые мы ожидали бы увидеть, если бы работали со значениями команд chipcon, хотя, видим и некоторые значения, которых в радиомодулях chipcon нет. Итак, или это радио – редкостный бастард chipcon, или оба они происходят от общего предка. В любом случае, такая ситуация помогает понять еще некоторые команды, выдаваемые ими.
Воспроизведение инициирующего кода и написание обработчиков прерываний, подобных тем, что встроены в чип, дает нам рабочий бинарник, который может работать на прием и располагает к экспериментам. Заметив еще некоторые регистры, в которые пишет главная прошивка, я быстро определил, что MMIO:df88 — MMIO:df8f
это «мой длинный MAC-адрес», который будет использоваться на аппаратном уровне для фильтрации входящих пакетов. Аналогично, MMIO:df90 — MMIO:df91
устанавливает «собственный PAN ID (идентификатор личной сети)» для RX-фильтра. А MMIO:df92 — MMIO:df93
устанавливает «собственный короткий адрес». Это оборудование будет принимать и подтверждать (ACK) любой пакет, присланный нам на широковещательные адреса. MMIO:dfc0
задает радиоканал в стандартной нумерации 802.15.4 (11..26).
Поскольку радио будет подтверждать пакеты, я также смог обнаружить, что при корректировке MMIO:dfc9
настраивается сила передачи. Думаю, дело в регистре, устанавливающем мощность TX. Я также заметил, что, когда устанавливается канал в главной заводской прошивке, еще два регистра записываются поканальными значениями. В прошивке OTA всего один такой регистр. Относящийся к RX называется MMIO:dfcb
, а относящийся к TX -MMIO:dffd
. Достаточно легко, чтобы воспроизвести и понять. Тогда пришло время разобраться в TX!
Давайте отправим несколько байт!
Расшифровав путь получения данных, я перенес имена функций и регистров в мой дизассемблированный главный образ. Посмотрев, что еще осталось неразмеченным, увидим, где пролегает путь TX. В самом деле, здесь есть еще две буферные очереди: одна полна пустых TX-буферов, готовых к использованию, а другая – «использованных» TX-буферов, готовых к отправке. Я очень быстро нашел передающую функцию.
В 802.15.4 принято слушать радиоканал перед передачей. Такая операция называется CCA (оценкой незанятости канала). Прежде, чем что-либо делать с теми данными, которые мы собираемся отправить, рассмотрим цикл, читающий MMIO:df98
и проверяющий бит 0. Если он установлен, то функция отваливает, и таймер ставится на повторную попытку. Думаю, это и есть путь CCA. Если мы 128 раз видим в этом бите ноль, то считаем, что канал свободен.
Функция передачи как таковая оказалась удручающе проста: выбирается конфигурационная страница 4, желаемая длина (не включая байт длины или CRC), и все это записывается в CD
. Указатель на буфер в XRAM
записывается в CA
:C9
. Буфер начинается с байта длины. RADIO_command
загружается со значением 0xCB
. В радиомодулях chipcon такой команды нет, но, полагаю, она означает «загрузить TX FIFO». Затем ставится бит 1 в TRIGGER
. Предполагаю, так запускается доступ DMA к внутренней очереди радио TX FIFO. Затем MMIO:dfc8
устанавливается в 0xFF
, выполняется 255 попыток, чтобы дождаться окончания TX, при этом проверяется, чтобы был установлен бит 7 в MMIO:df9b
(теперь называется RADIO_curRfState
). Затем, после небольшой задержки, MMIO:dfc8
устанавливается в 0x7F
. Любопытно, что я даже не представляю, почему записывается MMIO:dfc8
. В моем коде я пытался обходиться без него, и все работало прекрасно.
Хвосты
Немного поэкспериментировав, я обнаружил некоторые фокусы, которых заводская прошивка не умеет. Бит 6 в RADIO_IRQ4_pending
ставится после того, как мы «TX» пакет, и истекает задержка, связанная с подтверждением (ACK). Если мы, в самом деле, получим ACK, то также будет установлен бит 4. Следовательно, можно легко определить (1) когда отправка пакета у нас действительно прошла и (2) получили ли мы ACK. Круто!
Также, если установлен бит 4 в RADIO_IRQ4_pending
, а бит 5 в RADIO_curRfState
не занят, это означает, что мы в процессе получения пакета. Нам необходимо выбрать RSSI вручную, для чего мы считываем MMIO:df84
(теперь RADIO_currentRSSI
). У него есть смещение примерно на 56 дБм.
Я также заметил, что бит 1 в TCON2
устанавливается по завершении TX DMA (но не обязательно процесса TX как такового). Бит 0 в TCON2
устанавливается, когда заканчивается инициализация радио.
Неразгаданные тайны
Измерение АЦП/батареи и движка шифрования AES
Логично, что должен быть какой-то способ измерить напряжение батареи, но я не нашел и следов какого-либо подобного кода. Без кода, использующего АЦП таким образом, шансы обнаружить этот способ исчезающее малы. Блока AES, в принципе, касается то же, что и АЦП. Я знаю, что в чипе есть блок для ускорения AES (необходимый для ZigBee). Но, поскольку действующий код его не использует, не вижу способа найти его.
Разное
Вещи, которые мы не можем найти, но и которые нас не особо волнуют, так как мы не можем купить этот чип: контроллер ИК LED, блок ШИМ, ЦАП. Оставлю читателю эти вещи для самостоятельных упражнений.
ZBS242/3 Распиновки, функции, SFR, материалы для скачивания
Скачать ZBS24x SDK.
- В затемненных ячейках указаны регистры с побитовой адресацией
- По диагонали заштрихованы регистры, которые не запасаются в банке
CFGPAGE
- По вертикали заштрихованы регистры, которых, по-видимому, вообще нет ни на одной из страниц
- Пустые клетки – это неизвестные регистры
- С буквы «r» начинаются названия РАДИО-регистров
Уроки для начинающего реверс-инженера
- Прежде, чем приступать к работе, хотя бы несколько часов или дней почитайте материалы.
- Полезно что-то знать заранее. Чем больше занимаешься реверс-инжинирингом, тем искуснее становишься.
- Очертания кода узнаваемы. Все драйверы SPI выглядят одинаково, драйверы I2C тоже выглядят похоже, все циклические буферы выглядят одинаково. Форма кода может многое подсказать о его содержании.
- Исходите из того, что авторы кода и проектировщики чипа – умственно здоровы (если не доказано обратное).
- Первый закон Ньютона применительно к разработке программного и аппаратного обеспечения: без серьезного внешнего воздействия все вещи делаются так, как делались всегда. Исходите из того, что большинство проектов похожи, и что однажды увиденное еще не раз вам встретится.
- Как правило, никто не меняет значений, заданных по умолчанию.
- Каждая крупица знаний помогает исключить те или иные варианты. Если вас что-то запутало, отложите это и проанализируйте какой-то другой элемент. Вернитесь к затыку попозже, когда уже будете знать больше.
- Даже самые странные константы что-то означают. Чем страннее номер, тем больше в нем смысла, скорее всего.
- Наработайте теорию, прежде, чем поспешно браться за дело. Эксперимент, не подкрепленный теорией, бессмысленный.
- Обязательно пытайтесь что-то делать. Теория без эксперимента никуда не годится.
- Делайте пометки по ходу ваших попыток и как только что-то узнаете, поскольку ваш бинарник с попытками вскоре превратится в непролазную кашу, и вы начнете что-то забывать.
Облачные серверы от Маклауд отлично подходят для
Зарегистрируйтесь по ссылке выше или кликнув на баннер и получите 10% скидку на первый месяц аренды сервера любой конфигурации!
Автор: owlofmacloud