Наверняка каждый попадал в ситуацию, когда пытаешься отладить какую-нибудь цифровую схему, а она ну ни как не работает должным образом. Особенно неудобно в Arduino IDE, где тебе для отладки доступны лишь вывод в COM-порт какой либо информации или светодиодик 🙂 Кто пробовал — тот знает. И обязательно кто-нибудь из наблюдающих за твоими тщетными попытками наладить работу бросает фразу: «Тебе тут логический анализатор нужен». В какой-то момент времени мне это надоело и я решил выяснить что это за зверь, чем он лучше осциллографа и действительно ли мне он необходим.
Что бы узнать, что это такое, первым делом ищем описание во всемирной паутине. Логический анализатор (англ. Logic Analyzer) — электронный прибор, который может записывать и отображать последовательности цифровых сигналов. Он используется для тестирования и отладки цифровых электронных схем, например, при проектировании компонентов компьютеров и управляющих электронных устройств. В отличие от осциллографов, логические анализаторы имеют значительно больше входов (обычно от 16 до нескольких сотен), но при этом часто способны показывать лишь два уровня сигнала («0») и («1»), к которым иногда добавлено состояние «Z».
Ну что же, наверняка стоящая штука. Понятное дело, что покупка фирменного изделия (USBee, Saleae и т.п.) — не наш метод. Закажем клон в Китае (вот такой). А пока заказанная посылка была в пути (да и время позволяло), я решил попробовать реализовать функции логического анализатора на Ардуино. Как говорит мой приятель Иванович: «Используй то, что под рукою и не ищи себе другого» 🙂
Задумка «в крупную клетку» выглядела так: к входным пинам Ардуино подключаем анализируемые линии, быстро-быстро читаем состояние этих пинов собирая данные (чем больше данных — тем лучше) и выводим полученный результат в неком удобоваримом виде на экран. Всего делов то! Я к этому моменту был уже немного «подкован» и знал, что для того что бы получилось «быстро-быстро» digitalRead(pin) — явно не подходит. Почему? Потому что МЕ-ДЛЕ-ННО!!! Вот тут небольшая статья по этому поводу. Так что остается самый быстрый и удобный способ — прямое чтение пинов соответствующего порта. На тот момент мне казалось, что реализация моего проекта «Toy Logic Analyzer» — буквально «вечер-другой» работы. Вот как я представлял себе «изюминку» этого анализатора (оставалось только дорисовать «рюшечки»):
void loop() { Serial.println(PINC); //выводим состояние битов порта }
Но, по мере погружения в проект я понял, что все не так просто. Сразу же встал вопрос насчет «Чем больше данных-тем лучше». Для поиграться/пощупать, мне вполне будет достаточно 4-х каналов. Только нужно определиться с пинами подключения анализируемых линий. Посмотрим на распиновку Ардуино:
Наиболее подходящими мне показались порты D (Pin8..Pin11) и C (A0..A3). Во первых, пины идут подряд и во вторых — являются младшими битами соответствующего порта, что очень даже удобно. Свой окончательный выбор я остановил на порту C. При таком варианте, прочитав состояние порта и обнулив старшие 4-ре бита, мы получаем значения логических уровней на каждом из подключенных каналов (далее по тексту я буду использовать термин СОСТОЯНИЕ ШИНЫ). Читая таким образом состояние шины через некие промежутки времени мы будем получать числовые значения BUS_DATA:
Каналов четыре, каждый канал представлен состоянием «0» или «1», в итоге мы получаем некую комбинацию чисел в диапазоне от 0 до 0xF. Вполне очевидно, что набор чисел не очень удобен для наглядного представления об изменениях на каналах. Как же должны выглядеть эти данные в человеческом виде? Для первого замера мы получаем значение 0хA, в двоичном представлении это b1010. При выводе результатов для каждого канала мы рисуем верхнюю черточку — если «1» и нижнюю — если «0», а переходы соединяем вертикальными линиями.
Солидные программы примерно так и выводят результаты своих замеров, попутно декодируя данные, разбирая протоколы и еще куча плюшек.. Но у нас же запланирован МЕГАдешевый и МЕГАпростой вариант :), поэтому у нас результат будет представлен несколько упрощенно (как минимум, без вертикальных линий), примерно вот так:
Небольшое отступление в HARDWARE часть 🙂
Честно признаюсь, что практически до финальной версии я подключал анализируемые линии прямо к пинам Arduino. Знаю, что так делать не стоит. Поэтому в конце концов я сделал простенькую плату с защитой пинов в виде токоограничивающих резисторов и защитных стабилитронов. Схема анализатора:
Печатная плата:
Ну и собственно готовое изделие:
Ну, не знаю… Как по мне, плата придает некую «солидность» этой поделке 🙂
Сохранение данных замера. Компрессия.
Считанные данные нужно где-то хранить. Соответственно, нам нужен буфер или массив. От его размера напрямую зависит обьем анализируемых данных. Самый быстрый компонент для хранения данных — это ОЗУ (RAM или память). Памяти у Atmega328 всего два килобайта. Мало, очень мало. Попробуем ужаться. Для хранения одного замера с 4-х каналов необходимо 4-ре бита или пол-байта. Значит в одной ячейке массива (1 байт) могут разместиться два замера. Т.е., массив на 512 байт способен уместить 1024 замера. Как же это сделать?
В массив будем производить запись ДВУХ считанных замеров (текущий и предыдущий).
Array[x]=((prev_data<<4) | cur_data)
Как видно из рисунка-примера, замер №1 (0xA) и замер №2(0xA) в массив будут записаны как 0xAA. Все. Отсюда, кстати, следует небольшое условие: количество замеров должно быть четным. Тогда размер массива получается простым делением количества замеров на два.
Кстати, при реализации проекта я несколько раз впадал в ступор путая НОМЕР ЗАМЕРА и ПОЗИЦИЮ В БУФЕРЕ.
Естественно, любой буфер имеет некий конечный размер, поэтому запись данных будет происходить по кругу: пишем сформированные данные в буфер пока не достигли конца, далее переход в начало и так далее.
Система триггеров.
Понятное дело, что просто сохранять какие-то данные не интересно. Обычно требуется записывать данные начиная с какого-нибудь события (Event) — специфичного набора состояний входных линий. Еще это называют «запись по триггеру».
Какие же события будем отлавливать? Я для себя выделил такие:
— переход канала из «0» -> «1» (RISE)
— переход канала из «1» -> «0» (FALL)
— любое изменение на любом канале (BURST)
— определенное состояние шины (PATTERN).
Чтобы упростить себе жизнь, я решил, что первые два триггера (RISE и FALL) я реализую только для одного канала (типа МАСТЕР-канал), если понадобиться, то можно будет просто перекоммутировать клипсу-зажим на нужную линию. Вполне очевидно, что для поиска изменений состояния шины потребуются две переменные: текущее состояние (cur_data) и предыдущее (prev_data).
Триггер BURST (любое изменение на шине).
Для приведенного примера первые два замера абсолютно идентичны, а вот на третьем произошло изменение состояния канала Ch2. Это наверное, самый простой в плане реализации триггер. Просто проверяем каждый раз данные на предмет того, что текущее состояние шины не равно предыдущему 🙂
if (prev_data != cur_data)
Триггер RISE (переход из «0» в «1»).
Как проверить, что состояние канала поменялось с «0» на «1»?
В первом приближении я делал такими проверками «Если первый битик предыдущего замера был равен «0» И первый битик текущего замера равен «1» — то считаем что условие триггера выполнено.
//первый вариант if (((prev_data & 1)==0) && (cur_data & 1)==1)
Однако после общения с моим приятелем Александром и получением от него дельных ЦУ, данная проверка стала выглядеть так:
//второй вариант if (~prev_data & cur_data & 1)
Помимо лаконичности такой проверки, надеюсь, что скорость выполнения тоже возросла 🙂 Я рассуждал примерно так (поправьте если ошибаюсь):
1 вариант.
— выделяем младший бит из prev_data
— сравниваем его с 0
— выделяем младший бит из cur_data
— сравниваем его с 1
— делаем логическое AND с полученными результатами
ИТОГО: 5 операций
2 вариант.
— инвертируем prev_data
— побитовое сложение с cur_data
— выделяем младший бит из результата
ИТОГО: 3 операции
Триггер FALL (переход из «1» в «0»).
Несложно догадаться, что для триггера FALL проверка будет такая:
if (prev_data & ~cur_data & 1)
Триггер PATTERN (определенное состояние на шине).
Оказалось — самый сложный триггер. Для данного режима нужно указать маску по которой будет происходить сравнение. Причем, помимо определенных «0» или «1», можно указать, что для данного канала состояние не важно. Например, для маски 0?1? — подходят следующие варианты: 0010; 0011;0110;0111;
Первый вариант реализации этой проверки выглядел вот таким образом:
volatile byte pattern_mask[4]; // массив-маска для события // Значения в массиве-маске: 0 - и есть ноль, 1 - 1, и 2 - это неопределенное состояние volatile byte pattern_mask[4]; // массив-маска для события byte flag = 0; // Результат сравнения. 1 - совпало for (byte i=0; i<4;i++) // 4 канала – 4 цикла { if (pattern_mask[i] < 2) // значение текущего символа маски меньше 2, значит это НЕ символ-игнорирования { if (((cur_data >> i)&1) == pattern_mask[i]) // Сдвигаем значение cur_data на i шагов вправо, потом умножаем на 1 тем самым обнуляя все биты кроме первого, затем сравниваем с соотв. значением маски { flag = 1; // значение совпало, устанавливаем флаг } else { flag = 0; // не совпало – не чего ловить – выходим из цикла break; } } else flag = 1; } // конец цикла i
Получилось хоть и работоспособно, но очень громоздко. А это сказывается на равномерности считывания данных.
И тут опять же, благодаря вмешательству Александра… была произведена СУЩЕСТВЕННАЯ оптимизация этой проверки. А именно,
вместо 4-х байтового массива patterm_mask заводим 2 переменные: pattern_mask и pattern_value.
pattern_value хранит собственно сам паттерн состояния шины, который ищется. Если бит должен игнорироваться, то в нем «0», например:
ищем 0000 — pattern_value=0x0
ищем 0101 — pattern_value=0x5
ищем xx10 — pattern_value=0x2
pattern_mask должна хранить «1» в тех разрядах, которые сравниваются, и «0» в тех, которые игнорируются, используя пример выше:
ищем 0000 — pattern_mask=0xf
ищем 0101 — pattern_mask=0xf
ищем xx10 — pattern_value=0x3
В этом случае весь вышеприведенный код проверки заменяется одной строчкой:
if ((cur_data & pattern_mask) == pattern_value)
Просто СУПЕР! Я реально офигел от такого лаконичного и красивого решения!
Вывод результатов.
Как считывать состояние шины мы уже разобрались, как будем хранить результаты — тоже определились. Какие события будем отслеживать — выделили. Теперь что же с этими результатами делать? В каком виде их выводить? С какого момента?
При отладке полезно знать, что предшествовало появлению того или иного триггера, так называемая история.
Для случая на картинке, до появления условия описанного триггером, мы все равно записываем считанные данные в массив. На десятом замере условия триггера (Event) выполнилось. Высчитываем значение StartIndex. И далее мы «тупо» производим запись состояний шины в массив (уже не делая проверку на триггер) до момента начала истории (StartIndex).
А вот выводить на экран результаты сканирования нужно от начала истории и до конца…
Таким образом, мы получаем некое понимание того, что было на шине пока не появился триггер.
Вот как будет выглядеть реальная осциллограмма замера:
В нашем варианте анализатора конец истории и начало замера указывается маленькой стрелочкой вверх «^». Дабы придать солидность выводимым результатам, я нарисовал снизу некое подобие шкалы разметки. Тут же присутствует расчетная цена одного деления (Time:) (я использовал штатную ардуиновскую millis(). Сохраняем значение millis() в начале замера и в конце. Высчитываем сколько же по времени длился собственно замер. Ну и далее делим это время на количество замеров).
Если есть необходимость, то результаты замера можно посмотреть в виде дампа:
Правда вывод дампа начинается не с истории, а с начала буфера.
Управление.
Реализовано в диалоговом режиме. Обмен командами происходит через терминал. Доступны следующие команды:
Практически все команды получились однобуквенными. Если бы не режим PATTERN, то все было бы сверхтривиально. Команды R,F,B,P — запуск сканирования с указанным триггером. Для триггера P дополнительно нужно указывать четыре параметра для каждого канала («0», «1», «?» (или «х»)). Для того, чтобы остановить ожидание появления триггера, просто подаем новую команду и она отменит старую. Команда S (Show buffer)- повторный вывод осциллограммы. От отладки осталась команда D (Dump out) — вывод результатов в виде дампа. Перечень доступных команд можно всегда получить если ввести H (или ?).
В настройках терминала необходимо установить скорость 115200 и режим «Новая строка».
========================================
Основной скелет программы теперь выглядит так:
— ждем команду указание триггера;
— очищаем буфер;
— сканируем шину, записываем данные в буфер и попутно отслеживаем появление заданного триггера;
— при появлении данных в CОМ-порту, прерываем сканирование;
— если условие триггера выполнилось — пишем данные в буфер, при этом сохранив историю состоянии шины до сработки триггера;
— выводим результаты на экран в виде осциллограммы;
=========================================
Оценка скорости выборки.
В какой-то момент времени меня очень заинтересовала скорость захвата данных полученного логического анализатора… Я попробовал несколько вариантов измерения:
— с помощью замеров времени выборки (с помощью millis(). Читай выше);
— при каждом считывании состояния шины я инвертировал значение на одном из пинов (debugPin) и замерял частоту переключения;
— внешним генератором;
Последний способ опишу (просто у меня остался скрин этого варианта).
На внешнем генераторе установил частоту 496 Hz. И подобрал размер буфера таким образом, чтобы целиком уместилась одна «ступенька» в замер. Вышло что количество замеров равно 240. Дальнейшие рассуждения:
Частота 496 Гц — значит период 1/496 = 2,016 мс
Половина периода — 1,008 мс
В эту половину периода укладывается 240 замеров, значит частота дискретизации 240/1,008= 238,08 кГц
Следовательно период дискретизации 4,2 мкс
Все остальные способы оценки давали близкие результаты (около 4 мкс), что очень даже хорошо.
Примеры работы.
4-х битный счетчик 74ls161
Получилось весьма правдоподобно, буквально пару неточностей 🙂
Захват протокола I2C:
=========================================
Достоинства.
— «дешево и сердито». (Кроме Arduino вообще ничего не нужно);
— 100% OpenSource. (Бери исходники и делай с ними все, что посчитаешь нужным);
— позволяет без затрат оценить необходимость анализатора для ваших нужд;
— достаточно гибкая система настраиваемых триггеров;
— вполне приемлемая скорость оцифровки (по меркам Ардуино). («Медленный» обмен данными через СОМ-порт происходит ТОЛЬКО после захвата данных);
— дополнительный вывод результатов в виде дампа значений;
— легкая переносимость на другие платформы (например на STM32F103C8T6 );
Недостатки описывать не буду :), так как считаю, что само название «Toy Logic Analyzer» (рус. «Игрушечный логический анализатор») в значительной мере их наличие оправдывает.
Забрать весь материал по этому проекту можно тут.
Всем спасибо за уделенное время на прочтение и удачи!
0 комментариев на «“Лаборатория юного радиолюбителя. «Toy Logic Analyzer»”»
Респект за крайне подробную статью.
а теперь не троллинга ради а из интереса великого:
0. Чем вас не устроил вариант sump logic analyser ОТ gillham? (вариант FUN считаю уважительной причиной)
1. вы используете 4 бита. почему бы во вторые 4 бита не поместить значение счетчика «сколько тактов продержалось состояние» тогда для медленных процессов вы сможете записать до 512*16 состояний.
Доброго дня!
По прядку…
0) Честно говоря, про этот вариант (sump logic analyser ОТ gillham) я узнал только что от вас 🙂 Я видел только подобный проект с экраном от Nokia.
1) Ну да, неплохая идея. Я вначале нечто подобное обдумывал, но в конце концов решил, что реализованный вариант более предпочтителен. Для медленных процессов можно просто сделать несколько замеров по определенным триггерам. Кстати, вам никто не мешает все переделать 🙂 Если есть желание