← На главную

Основы написания декодеров для Sigrok на языке Python

Если вы читали пост Знакомимся с Sigrok и логическим анализатором DSLogic, то помните, что для Sigrok можно писать декодеры протоколов (protocol decoders, в документации к Sigrok часто используется сокращение PD) на Python. Однако в посте ничего не говорится о том, как их, собственно, писать. Пришло время заполнить этот пробел.

Примечание: Если вы пропустили статью Как стать контрибьютором в open source проект – идеи для первого патча и прочие рекомендации, может быть не лишено смысла ознакомиться с ней.

Основные сведения

В этом контексте нельзя не сказать пару слов о процессе разработки Sigrok. Каким образом новые декодеры (или патчи к существующим) попадают в него? Для этого нужно предложить патчи в рассылке sigrok-devel. Притом мейнтейнеры предпочитают патчи в виде веток на GitHub, а не в файлов .patch. В качестве конкретного примера рассмотрим написанный мной декодер протокола TFT-дисплеев на базе ST7735. Соответствующее письмо в рассылку можно найти здесь.

Вы обратили внимание, что я сказал патчи, во множественном числе? Патчей действительно нужно несколько:

  • Патч для репозитория sigrok-dumps, добавляющий .sr файл с примером декодируемого протокола. Пример моего патча: 1ea7b9af.
  • Патч для репозитория libsigrokdecode, добавляющий сам декодер на Python. Пример: f62e32bc.
  • Наконец, патч для sigrok-test, добавляющий регрессионные тесты на написанный декодер. Пример: 4aa3a4fd.

Получить файл .sr не сложно, это делается банально с помощью sigrok-cli или PulseView. Заметьте, что пример следует сократить до минимального. Обрезать лишнее можно в PulseView с помощью курсоров. Также стоит иметь ввиду, что в идеале записанный пример должен покрывать все, или хотя бы большинство возможностей декодера (например, все декодируемые команды). В связи с этим может потребоваться написать кастомную прошивку для вашей любимой отладочной платы.

В общем, эта часть работы тривиальна. Поэтому далее мы сосредоточимся на написании декодера и тестов к нему.

Разработка декодера

На время разработки декодера нужно как-то заставить sigrok-cli и PulseView его видеть. Проще всего сделать это с помощью символьной ссылки:

cd /usr/share/libsigrokdecode/decoders sudo ln -s /path/to/libsigrokdecode/decoders/st7735 ./st7735

Декодер состоит из двух файлов. Файл __init__.py в основном содержит лицензию и краткое описание декодера:

## ## This file is part of the libsigrokdecode project. ## ## Copyright (C) 2018 Aleksander Alekseev <afiskon@gmail.com> ## ## This program is free software; you can redistribute it and/or modify ## it under the terms of the GNU General Public License as published by ## the Free Software Foundation; either version 2 of the License, or ## (at your option) any later version. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program; if not, see <http://www.gnu.org/licenses/>. ## ''' This decoder decodes the ST7735 TFT controller protocol. Details: http://www.displayfuture.com/Display/datasheet/controller/ST7735.pdf ''' from .pd import Decoder

Самое же интересное содержится в файле pd.py. Рассмотрим его по частям.

import sigrokdecode as srd MAX_DATA_LEN = 128 # Command ID -> name, short description META = { 0x00: {'name': 'NOP ', 'desc': 'No operation'}, 0x01: {'name': 'SWRESET', 'desc': 'Software reset'}, 0x04: {'name': 'RDDID ', 'desc': 'Read display ID'}, # ... (ПРОПУЩЕНО) ... }

Здесь просто объявляются константы, используемые далее по коду. Переменная META, как не сложно понять, служит для отображения кода команды в ее название и краткое описание. Эта информация была получена из даташита ST7735.

class Ann: BITS, CMD, DATA, DESC = range(4) class Decoder(srd.Decoder): api_version = 3 id = 'st7735' name = 'ST7735' longname = 'Sitronix ST7735' desc = 'Sitronix ST7735 TFT controller protocol.' license = 'gplv2+' inputs = ['logic'] outputs = ['st7735'] channels = ( {'id': 'cs', 'name': 'CS#', 'desc': 'Chip-select'}, {'id': 'clk', 'name': 'CLK', 'desc': 'Clock'}, {'id': 'mosi', 'name': 'MOSI', 'desc': 'Master out, slave in'}, {'id': 'dc', 'name': 'DC', 'desc': 'Data or command'} ) annotations = ( ('bit', 'Bit'), ('command', 'Command'), ('data', 'Data'), ('description', 'Description'), ) annotation_rows = ( ('bits', 'Bits', (Ann.BITS,)), ('fields', 'Fields', (Ann.CMD, Ann.DATA)), ('description', 'Description', (Ann.DESC,)), )

Класс Decoder представляет собой непосредственно наш декодер. Класс должен иметь несколько обязательных полей, содержащие уникальный идентификатор протокола, его имя, краткое описание, информацию о лицензии. Поля inputs и outputs определяют, какие данные декодер принимает на вход и выдает на выход. Это нужно по той причине, что Sigrok позволяет писать декодеры, работающие поверх других декодеров. Например, нередко нужно написать декодер, работающий поверх декодера SPI или I2C. В данном примере декодер работает непосредственно с логическими сигналами, не полагаясь на другие декодеры. Поле channels определяет, какие сигналы нужны декодеру на вход. Поля annotations и annotation_rows говорят, какие данные выводит наш декодер, притом последнее поле нужно для объединения этих данных в группы.

def __init__(self): self.reset() def reset(self): self.accum_byte = 0 self.accum_bits_num = 0 self.bit_ss = -1 self.byte_ss = -1 self.current_bit = -1 def start(self): self.out_ann = self.register(srd.OUTPUT_ANN) def put_desc(self, ss, es, cmd, data): if cmd == -1: return if META[cmd]: self.put(ss, es, self.out_ann, [Ann.DESC, ['%s: %s' % (META[cmd]['name'].strip(), META[cmd]['desc'])]]) else: # Default description: dots = '' if len(data) == MAX_DATA_LEN: data = data[:-1] dots = '...' data_str = '(none)' if len(data) > 0: data_str = ' '.join(['%02X' % b for b in data]) self.put(ss, es, self.out_ann, [Ann.DESC, ['Unknown command: %02X. Data: %s%s' % (cmd, data_str, dots)]])

Имена методов __init__, reset и start говорят сами за себя. Метод put_desc, используемый далее по коду, присваивает диапазону входных данных текстовое описание. Делается это через вызов унаследованного от родительского класса srd.Decoder метода put. Для идентификации начала и конца входных данных, которым мы хотим сопоставить какой-то выход декодера, используются целые числа, называемые start sample и end sample. В коде их часто сокращают до ss и es.

Наконец, основным методом является decode:

def decode(self): current_cmd = -1 current_data = [] desc_ss = -1 desc_es = -1 self.reset() while True: # Check data on both CLK edges. (cs, clk, mosi, dc) = self.wait({1: 'e'}) if cs == 1: # Wait for CS = low, ignore the rest. self.reset() continue if clk == 1: # Read one bit. self.bit_ss = self.samplenum if self.accum_bits_num == 0: self.byte_ss = self.samplenum self.current_bit = mosi if (clk == 0) and (self.current_bit >= 0): # Process one bit. self.put(self.bit_ss, self.samplenum, self.out_ann, [Ann.BITS, [str(self.current_bit)]]) # MSB-first self.accum_byte = (self.accum_byte << 1) | self.current_bit self.accum_bits_num += 1 if self.accum_bits_num == 8: # Process one byte. # DC = low for commands. ann = Ann.DATA if dc else Ann.CMD self.put(self.byte_ss, self.samplenum, self.out_ann, [ann, ['%02X' % self.accum_byte]]) if ann == Ann.CMD: self.put_desc(desc_ss, desc_es, current_cmd, current_data) desc_ss = self.byte_ss desc_es = self.samplenum # For cmds without data. current_cmd = self.accum_byte current_data = [] else: if len(current_data) < MAX_DATA_LEN: current_data += [self.accum_byte] desc_es = self.samplenum self.accum_bits_num = 0 self.accum_byte = 0 self.byte_ss = -1 self.current_bit = -1 self.bit_ss = -1

Данные для декодирования приходят через вызов унаследованного метода wait. Притом, передав этому методу аргумент {1: 'e'}, мы говорим, что хотим получать данные только по переднему или заднему фронту на первом канале, которому у нас соответствует CLK (нумерация каналов идет с нуля). Также помимо e (edge) можно указать r (rising edge) или f (falling edge), если нас интересует не любой фронт, а только передний или только задний. В принципе, можно и не указывать ничего, получая вообще все входные данные, какие есть. Но в этом случае скорость работы декодера будет оставлять желать лучшего.

Так или иначе, благодаря вызову wait переменным cs, clk, mosi и dc присваиваются единички и нолики в соответствии со значениями на каналах. Понять, по какому смещению относительно начала данных мы находимся, можно благодаря унаследованному полю samplenump. Остальное – дело техники. Единички и нолики выводятся с аннотацией Ann.BITS, из них собираются байты, выводимые с аннотациями Ann.CMD или Ann.DATA, а байты декодируются в текстовое описание, выводимое при помощи объявленного выше метода put_desc.

Напомню, как выглядит результат:

Протокол ST7735 в PulseView

На приведенной картинке фиолетовым цветом изображены биты (Ann.BITS). Под ними идут байты, зеленые соответствуют командам (Ann.CMD), а синие – аргументам команд (Ann.DATA). Наконец, под байтами выводится текстовое описание команды (Ann.DESC).

Вот и весь код! Стоит, впрочем, сказать пару слов об отладке. Иной раз трудно понять, какие данные приходят в тот или иной метод, особенно если вы пишете декодер, работающий поверх другого декодера. Вот, к примеру, шаблон ничего не делающего декодера, работающего поверх декодера SPI:

import sigrokdecode as srd class Ann: CMD, DATA, DESC = range(3) class Decoder(srd.Decoder): # see https://sigrok.org/wiki/Protocol_decoder_API api_version = 3 id = 'st7735' # this is shown in `sigrok-cli -L` name = 'ST7735' # this is shown in PulseView longname = 'ST7735 TFT controller' # shown in `sigrok-cli -L` desc = 'ST7735 TFT controller protocol decoder' license = 'gplv2+' inputs = ['spi'] outputs = ['st7735'] channels = ( ) optional_channels = ( ) annotations = ( ('command', 'Command'), # Ann.CMD ('data', 'Data'), # Ann.DATA ('description', 'Description'), # Ann.DESC ) annotation_rows = ( ('fields', 'Fields', (Ann.CMD, Ann.DATA,)), ('description', 'Description', (Ann.DESC,)), ) options = ( ) # see examples in spiflash decoder def __init__(self): self.reset() def reset(self): pass # do nothing, yet # This function is called before the beginning of the decoding. # This is the place to register() the output types, check the # user-supplied PD options for validity, and so on def start(self): self.out_ann = self.register(srd.OUTPUT_ANN) def putx(self, data): self.put(self.ss, self.es, self.out_ann, data) # This is a function that is called by the libsigrokdecode # backend whenever it has a chunk of data for the protocol # decoder to handle # Arguments: # ss = startsample, the absolute samplenumber of the # first sample in this chunk of data # es = endsample, the absolute samplenumber of the # last sample in this chunk of data # data = a list containing the data to decode. Depends on # whether the decoder decodes raw samples or is # stacked onto another decoder def decode(self, ss, es, data): print("decode: ss = %s, es = %s, data = %s" % (ss, es, data) ) ptype, mosi, miso = data self.ss, self.es = ss, es if ptype == 'CS-CHANGE': # end_current_transaction() pass if ptype != 'DATA': return # Handle commands here self.putx([Ann.DESC, ['Unknown command: 0x%02X' % mosi]])

Обратите внимание на отладочный вывод с помощью print, а также на то, что данные здесь приходят не через вызов wait, а напрямую в метод decode, имеющий другую сигнатуру. Теперь можно сказать:

pulseview --log-to-stdout ~/temp/st7735-debug.sr | tee ~/temp/pv.log

Отладочный вывод декодера будет виден в консоли.

Покрываем код текстами

Итак, будем считать, что с декодером мы разобрались. Осталось добавить тесты на него. На этом шаге лучше не полагаться на установленные бинарные пакеты Sigrok, а честно собрать их из веток master. Тем более, что половину репозиториев мы и так же склонировали. Делается это не сложно и занимает пару минут:

mkidir -p /home/eax/sigrok-dev/src cd /home/eax/sigrok-dev/src git clone 'git://sigrok.org/libsigrok' git clone 'git://sigrok.org/sigrok-cli' git clone 'git://sigrok.org/libsigrokdecode' cd libsigrok ./autogen.sh CFLAGS='-O0 -g' CXXFLAGS='-O0 -g' ./configure \ --prefix=/home/eax/sigrok-dev/install \ --disable-java --disable-python make make install cd ../libsigrokdecode ./autogen.sh CFLAGS='-O0 -g' CXXFLAGS='-O0 -g' ./configure \ --prefix=/home/eax/sigrok-dev/install make make install cd ../sigrok-cli ./autogen.sh CFLAGS='-O0 -g' CXXFLAGS='-O0 -g' ./configure \ --prefix=/home/eax/sigrok-dev/install make make install cd ..

Fun fact! Все зависимости я лично подтянул очень просто – собрал пакеты из AUR, но не стал их устанавливать. Даже если вы пользуетесь дистрибутивом, отличным от Arch Linux, вы можете подсмотреть список зависимостей в AUR.

Собирать PulseView при разработке декодеров, строго говоря, не требуется, но если очень хочется:

git clone git://sigrok.org/pulseview cd pulseview mkdir build cd build PKG_CONFIG_PATH=/home/eax/sigrok-dev/install/lib/pkgconfig/ \ cmake -G Ninja \ -DCMAKE_INSTALL_PREFIX:PATH=/home/eax/sigrok-dev/install \ -DCMAKE_BUILD_TYPE=Debug \ -DCMAKE_CXX_FLAGS='-O0 -g -fext-numeric-literals' .. ninja ninja install

Если вы хотите использовать Sigrok и PulseView из веток master, допишите в файл ~/.bashrc:

export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:~/sigrok-dev/install/lib/" export PATH="~/sigrok-dev/install/bin:$PATH"

Возвращаемся к тестами:

git clone git://sigrok.org/sigrok-test cd sigrok-test ./autogen.sh PKG_CONFIG_PATH=$HOME/sigrok-dev/install/lib/pkgconfig \ ./configure \ --with-decodersdir=$HOME/sigrok-dev/libsigrokdecode/decoders make

Проверяем, что тесты проходят:

LD_LIBRARY_PATH=$HOME/sigrok-dev/install/lib \ ./decoder/pdtest -r -v -a

Чтобы сгенерировать ожидаемый вывод для нашего нового декодера, пишем в decoder/test/st7735/test.conf:

test st7735_basic protocol-decoder st7735 channel cs=0 channel dc=2 channel mosi=3 ? channel clk=4 input display/st7735/st7735.sr output st7735 annotation match st7735_basic.output

Говорим:

LD_LIBRARY_PATH=$HOME/sigrok-dev/install/lib \ ./decoder/pdtest -f st7735

Будет получен файл decoder/test/st7735/st7735_basic.output с ожидаемым выводом. Для запуска отдельного теста говорим:

LD_LIBRARY_PATH=$HOME/sigrok-dev/install/lib \ ./decoder/pdtest -r st7735

Эксперимента ради можно отредактировать файл .output и убедиться, что в этом случае тесты не пройдут:

Testcase: st7735/st7735_basic/annotation Test output mismatch: - 24003758-24003780 st7735: command: "11" + 24003758-24003780 st7735: command: "01"

Заинтересованные читатели могут использовать информацию из этого раздела не только для разработки декодеров, но и для разработки кишок Sigrok и PulseView. Здесь вам также помогут заметки Памятка по отладке при помощи GDB, Краткий обзор статических анализаторов кода на C/C++, Профилирование кода на C/C++ в Linux и FreeBSD и далее по ссылкам.

Заключение

Кое-какие дополнительные сведения можно найти на официальной wiki проекта:

Также не побрезгуйте почитать код других декодеров из libsigrokdecode, там полно примеров. За помощью всегда можно обратиться в уже упомянутую рассылку sigrok-devel. А еще сообщество разработчиков Sigrok довольно активно в IRC, на канале #sigrok во FreeNode.

Как видите, Sigrok имеет относительно невысокий порог вхождения. Стать контрибьютором в него очень просто – берете случайную железку и пишите для используемого ею протокола декодер на Python. А когда и если это перестанет быть интересным, можно заняться разработкой PulseView или драйверов для новых логических анализаторов, мультиметров и осциллографов. В общем, если вы искали открытый проект для самореализации, рекомендую.