sigrok-protocol-decoders/

Если вы читали пост Знакомимся с 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 или драйверов для новых логических анализаторов, мультиметров и осциллографов. В общем, если вы искали открытый проект для самореализации, рекомендую.

EnglishRussianUkrainian