文章目录
0 前言
题主为了使用逻辑分析仪解码自己的私有协议,琢磨怎么自己实现一个解码器脚本
在网上几乎没找到相关的资料,所以自己找到了 DSLogic 的解码脚本,并研究了一下解码逻辑,然后改了个脚本出来。为了避免后面有需求的朋友花时间再研究一遍,所以写这个帖子。
这个帖子在写了一半的时候,发现已经有大佬做了非常棒的讲解,而且大佬也是用UART做示例的
原文链接: 逻辑分析仪协议解码教程
相比于大佬的帖子,本文会讲的更基础更细一些,在大模型的帮助下,代码注释也会更多一点
DSView 解码器是基于 sigrok 开源项目的 libsigrokdecode
sigrok 官方也提供了大量的资料,链接:sigrok开源项目
本文通过解析 DSView 的解码器脚本源码,帮助你使用 DSLogic 逻辑分析仪解码私有的通讯协议,内容如下:
🌟 DSView 逻辑分析仪的解码器入门
🌟 理解 DSView 的解码机制
🌟 实战解析 UART 解码器脚本
🌟 尝试完成私有协议的硬件解码任务
1 准备工作
1.1 下载DSView
1.2 DSLogic逻辑分析仪
也可以暂时不用,因为 DSView 软件提供了demo,即使没有接 DSLogic 也可以运行。

2 理解DSView解码机制
2.1 运行DSView的demo
运行 DSView 的demo来理解其解码机制。
按照图片指示操作,在界面上来分析UART的解码过程。
- 切换到demo模式,并删除所有解码器

- 只保留UART通道

- 添加第一个
UART解码器

- 添加第二个
UART解码器

- 查看
UART解码结果

2.2 UART 帧解码步骤
通过观察上述解码结果,可以看出一个串口帧解码大概分为以下几个步骤:
1️⃣ 寻找跳变沿:上升沿 / 下降沿
2️⃣ 确定起始位状态:合法 / 非法
3️⃣ 确定数据位状态:0 / 1
4️⃣ 确定结束位状态:0 / 1
上述demo添加了两个 UART 解码器,即 0: UART 和 1: UART
直观的看下来, 1: UART 解码器增加了对 解码位置 和 每个bit值 的显示
3 解码脚本解析—— UART 解码器
上文提到,UART 有两个解码器,我们这里来分析一下 1: UART 解码器,因为其功能更多一些
以下脚本的解析并非按照代码原生的顺序,遇到关键的代码会特别标明
3.1 找到解码脚本
脚本位置在 DSView 安装目录下的 decode 文件夹内,可以看出解码脚本是用Python写的

3.2 import & 异常类 & 全局函数
总结下来,解码器实现的功能可以概括为以下两点:
🌧️ 找到关键信息位置,如起始位、数据位、校验位、终止位等的位置
🌧️ 解码数字信号,得到对应的信息或数据,并直观地显示在对应的位置
import sigrokdecode as srd # 流式协议解码库
from common.srdhelper import bitpack # 用于将解码得到的二进制比特转换为字节
from math import floor, ceil # 天花板函数和地板函数
'''
OUTPUT_PYTHON 格式:Packet:[<ptype>, <rxtx>, <pdata>]
以下是 <ptype> 类型及其对应的 <pdata> 值说明:
'STARTBIT':数据为起始位的整数值(0/1)。
'DATA':始终为包含两个元素的元组:
第1项:UART数据的整数值(有效范围0至511,因数据最多支持9位)。
第2项:各数据位及其ss/es编号的列表。
'PARITYBIT':数据为校验位的整数值(0/1)。
'STOPBIT':数据为停止位的整数值(0/1)。
'INVALID STARTBIT':数据为无效起始位的整数值(0/1)。
'INVALID STOPBIT':数据为无效停止位的整数值(0/1)。
'PARITY ERROR':数据为包含两项的元组,第一项为预期校验值,第二项为实际校验值。
'BREAK':数据固定为0。
'FRAME':数据为包含两项的元组,第一项为UART数据的整数值,第二项为布尔值,表示UART帧的有效性。
'''
3.2.1 校验位检查
支持四种校验方式:
🌧️ 0 校验:检查校验位是否为0
🌧️ 1 校验:检查校验位是否为1
🌧️ 奇校验:检查数据位,1出现的次数是奇数则校验通过
🌧️ 偶校验:检查数据位,1出现的次数是偶数则校验通过
# Given a parity type to check (odd, even, zero, one), the value of the
# parity bit, the value of the data, and the length of the data (5-9 bits,
# usually 8 bits) return True if the parity is correct, False otherwise.
# 'none' is _not_ allowed as value for 'parity_type'.
def parity_ok(parity_type, parity_bit, data, num_data_bits):
# Handle easy cases first (parity bit is always 1 or 0).
if parity_type == 'zero':
return parity_bit == 0
elif parity_type == 'one':
return parity_bit == 1
# Count number of 1 (high) bits in the data (and the parity bit itself!).
ones = bin(data).count('1') + parity_bit
# Check for odd/even parity.
if parity_type == 'odd':
return (ones % 2) == 1
elif parity_type == 'even':
return (ones % 2) == 0
3.2.2 抛出异常
class SamplerateError(Exception):
pass
class ChannelError(Exception):
pass
3.3 解码器 Decoder 类
本章以下所有的内容都属于脚本的核心: Decoder 类
3.3.1 解码器属性配置
注意:在改写自己的解码器的时候,必须要把id改成其它的,否则进入软件的时候会报错
class Decoder(srd.Decoder):
api_version = 3
id = '1:uart'
name = '1:UART'
longname = 'Universal Asynchronous Receiver/Transmitter'
desc = 'Asynchronous, serial bus.'
license = 'gplv2+'
inputs = ['logic']
outputs = ['uart']
tags = ['Embedded/industrial']
channels = (
{'id': 'rxtx', 'type': 209, 'name': 'RX/TX', 'desc': 'UART transceive line', 'idn':'dec_1uart_chan_rxtx'},
)
# 可选设定参数,出现在在选定编码器后弹出来的参数配置界面中
# id : 参数句柄,不出现在用户界面上
# desc : 描述信息,出现在用户界面上
# default : 默认值,出现在用户界面上
# valuse : 可选值,出现在用户界面上(如果不提供,用户可自由配置)
options = (
# 波特率配置项:默认115200,
{'id': 'baudrate', 'desc': 'Baud rate', 'default': 115200, 'idn':'dec_1uart_opt_baudrate'},
# 数据位数配置项:默认8位,可选范围4-128位
{'id': 'num_data_bits', 'desc': 'Data bits', 'default': 8,
'values': tuple(range(4,129,1)), 'idn':'dec_1uart_opt_num_data_bits'},
# 校验类型配置项:默认无校验,可选奇校验/偶检验/0校验/1校验
{'id': 'parity_type', 'desc': 'Parity type', 'default': 'none',
'values': ('none', 'odd', 'even', 'zero', 'one'), 'idn':'dec_1uart_opt_parity_type'},
# 校验检查配置项:默认启用校验检查
{'id': 'parity_check', 'desc': 'Check parity?', 'default': 'yes',
'values': ('yes', 'no'), 'idn':'dec_1uart_opt_parity_check'},
# 停止位配置项:默认1.0位,支持0.0-2.5位
{'id': 'num_stop_bits', 'desc': 'Stop bits', 'default': 1.0,
'values': (0.0, 0.5, 1.0, 1.5, 2.0, 2.5), 'idn':'dec_1uart_opt_num_stop_bits'},
# 比特序配置项:默认低位优先,可选lsb-first/msb-first
{'id': 'bit_order', 'desc': 'Bit order', 'default': 'lsb-first',
'values': ('lsb-first', 'msb-first'), 'idn':'dec_1uart_opt_bit_order'},
# 数据格式配置项:默认十六进制,支持ascii/dec/hex/oct/bin
{'id': 'format', 'desc': 'Data format', 'default': 'hex',
'values': ('ascii', 'dec', 'hex', 'oct', 'bin') ,'idn':'dec_1uart_opt_format'},
# 信号反转配置项:默认不反转,可选yes/no
{'id': 'invert', 'desc': 'Invert Signal?', 'default': 'no',
'values': ('yes', 'no'), 'idn':'dec_1uart_opt_invert'},
# 起止位显示配置项:默认不显示,可选yes/no
{'id': 'anno_startstop', 'desc': 'Display Start/Stop?', 'default': 'no',
'values': ('yes', 'no'), 'idn':'dec_1uart_anno_startstop'},
)
# 协议解码类型定义
annotations = (
('108', 'data', 'data'),
('7', 'start', 'start bits'),
('6', 'parity-ok', 'parity OK bits'),
('0', 'parity-err', 'parity error bits'),
('1', 'stop', 'stop bits'),
('1000', 'warnings', 'warnings'),
('209', 'data-bits', 'data bits'),
('10', 'break', 'break'),
)
# 显示解码结果的行
annotation_rows = (
# 'data'类别:标注为RX/TX,包含第0-4行(共5行)
('data', 'RX/TX', (0, 1, 2, 3, 4)),
# 'data-bits'类别:标注为Bits,仅包含第6行
('data-bits', 'Bits', (6,)),
# 'warnings'类别:标注为Warnings,仅包含第5行
('warnings', 'Warnings', (5,)),
# 'break'类别:标注为break,仅包含第7行
('break', 'break', (7,)),
)
# 二进制协议的解码结果
binary = (
('rxtx', 'RX/TX dump'),
)
idle_state = 'WAIT FOR START BIT'
3.3.2 初始化函数和复位函数
def __init__(self):
self.reset()
def reset(self):
self.samplerate = None
self.samplenum = 0
self.frame_start = -1
self.frame_valid = None
self.startbit = -1
self.cur_data_bit = 0
self.datavalue = 0
self.paritybit = -1
self.stopbit1 = -1
self.startsample = -1
self.state = 'WAIT FOR START BIT'
self.databits = []
self.break_start = None
def start(self):
self.out_python = self.register(srd.OUTPUT_PYTHON)
self.out_binary = self.register(srd.OUTPUT_BINARY)
self.out_ann = self.register(srd.OUTPUT_ANN)
self.bw = (self.options['num_data_bits'] + 7) // 8
3.4 解码结果显示函数
在完成协议解码后,需要用一个个注释块来显示解码结果,这里函数的目标是:
1️⃣ 找到注释块的 起始位置 和 终止位置,画出注释块
(所有位置都是用采样点索引来表示的,即找到起始采样点索引和终止采样点索引)
2️⃣ 在注释块上显示解码结果
def putx(self, data):
# s是起始采样点索引,halfbit是每一个bit对应的采样点数
s, halfbit = self.startsample, self.bit_width / 2.0
# 显示起始位和终止位:标注范围从当前位起始点前半个比特到当前采样点后半个比特
if self.options['anno_startstop'] == 'yes' :
self.put(s - floor(halfbit), self.samplenum + ceil(halfbit), self.out_ann, data)
# 不显示起始位和终止位:标注范围从帧起始点到当前采样点加上停止位长度(考虑配置的停止位数)
else :
self.put(self.frame_start, self.samplenum + ceil(halfbit * (1+self.options['num_stop_bits'])), self.out_ann, data)
def putpx(self, data):
s, halfbit = self.startsample, self.bit_width / 2.0
self.put(s - floor(halfbit), self.samplenum + ceil(halfbit), self.out_python, data)
def putg(self, data):
s, halfbit = self.samplenum, self.bit_width / 2.0
self.put(s - floor(halfbit), s + ceil(halfbit), self.out_ann, data)
def putp(self, data):
s, halfbit = self.samplenum, self.bit_width / 2.0
self.put(s - floor(halfbit), s + ceil(halfbit), self.out_python, data)
def putgse(self, ss, es, data):
self.put(ss, es, self.out_ann, data)
def putpse(self, ss, es, data):
self.put(ss, es, self.out_python, data)
def putbin(self, data):
s, halfbit = self.startsample, self.bit_width / 2.0
self.put(s - floor(halfbit), self.samplenum + ceil(halfbit), self.out_binary, data)
3.5 采样点数和位置的计算
3.5.1 计算UART一个bit对应的采样点数
DSLogic 可以达到 100MHz 及以上的采样率,halfbit 是每一个bit对应的采样点数
例:串口波特率为 115200 下,UART 一个 bit 对应逻辑分析仪在 100MHz 下采样 868 个点
halfbit = 100000000 / 115200 = 868
def metadata(self, key, value):
if key == srd.SRD_CONF_SAMPLERATE:
self.samplerate = value
# The width of one UART bit in number of samples.
self.bit_width = float(self.samplerate) / float(self.options['baudrate'])
3.5.2 计算UART一个bit中间点的索引
如上所说, UART 一个 bit 对应逻辑分析仪在其采样率下采若干个点。
如果想知道 UART 的某个 bit 是 1 还是 0 ,那么选择这个比特最中间的采样点是最可靠的。
因为显然,这个 bit 的两侧可能是跳变沿,其数据是不可靠的。
def get_sample_point(self, bitnum):
# 确定比特采样点的绝对样本编号
# 比特位置(bitpos)表示指定UART比特位中间点的样本编号。
# 0=起始位,1至x=数据位,x+1=奇偶校验位(若启用)或第一个停止位,以此类推
# 比特窗口内的采样点编号为0,1,...,(比特宽度-1)
# 因此比特窗口中间采样点的索引计算公式为:(比特宽度 - 1) / 2。
bitpos = self.frame_start + (self.bit_width - 1) / 2.0
bitpos += bitnum * self.bit_width
return bitpos
3.6 状态机
3.6.1 状态1:等待起始位
在状态1中,记录下起始位产生时的采样点索引值,并跳转到状态2。
def wait_for_start_bit(self, signal):
# Save the sample number where the start bit begins.
self.frame_start = self.samplenum
self.frame_valid = True
self.state = 'GET START BIT'
3.6.2 状态2:获取起始位
def get_start_bit(self, signal):
self.startbit = signal
# 起始位必须为0。若非如此,将报告错误,并回到状态1,重新等待起始位
if self.startbit != 0:
self.putp(['INVALID STARTBIT', 0, self.startbit])
self.putg([5, ['Frame error', 'Frame err', 'FE']])
self.frame_valid = False
es = self.samplenum + ceil(self.bit_width / 2.0)
self.putpse(self.frame_start, es, ['FRAME', 0,
(self.datavalue, self.frame_valid)])
# 回到状态1
self.state = 'WAIT FOR START BIT'
return
# 复位本数据帧相关的变量
# 将self.startsample标记为-1,用于后续状态3标识首个数据位
self.cur_data_bit = 0
self.datavalue = 0
self.startsample = -1
# 显示起始位
self.putp(['STARTBIT', 0, self.startbit])
if self.options['anno_startstop'] == 'yes':
self.putg([1, ['Start bit', 'Start', 'S']])
# 进入下一个状态:获取数据位
self.state = 'GET DATA BITS'
3.6.3 状态3:获取数据位
def get_data_bits(self, signal):
# 获取首个数据位,中间采样点的绝对索引值,用于生成后续数据位采样的索引值
if self.startsample == -1:
self.startsample = self.samplenum
# 原始信号输出到逻辑分析仪界面
self.putg([6, ['%d' % signal]])
# 获取该数据位起始位置的绝对索引值,和结束位置的绝对索引值,用于解码结果显示的起始位置和结束位置
s, halfbit = self.samplenum, int(self.bit_width / 2)
self.databits.append([signal, s - halfbit, s + halfbit])
# 数据位检查
# 只有当本帧所有的数据位都完成采集,才会一起处理,并在界面上显示最终解码结果
self.cur_data_bit += 1
if self.cur_data_bit < self.options['num_data_bits']:
return
# 将所有的数据位格式由二进制转换为16进制,并显示解码结果
bits = [b[0] for b in self.databits]
if self.options['bit_order'] == 'msb-first':
bits.reverse()
self.datavalue = bitpack(bits)
self.putpx(['DATA', 0, (self.datavalue, self.databits)])
self.putx([0, ['@%02X' % self.datavalue]])
# 二进制数据的转换与输出
b = self.datavalue
bdata = b.to_bytes(self.bw, byteorder='big')
self.putbin([0, bdata])
self.putbin([1, bdata])
# 清空 self.databits 列表,准备接收下一帧数据
self.databits = []
# 状态机切换
# 若配置了校验位(parity_type != 'none'),则切换到 GET PARITY BIT 状态
# 若未配置校验位,直接切换到 GET STOP BITS 状态,准备接收停止位
self.state = 'GET PARITY BIT'
if self.options['parity_type'] == 'none':
self.state = 'GET STOP BITS'
3.6.4 状态4:获取校验位
def get_parity_bit(self, signal):
self.paritybit = signal
if parity_ok(self.options['parity_type'], self.paritybit,
self.datavalue, self.options['num_data_bits']):
self.putp(['PARITYBIT', 0, self.paritybit])
self.putg([2, ['Parity bit', 'Parity', 'P']])
else:
# TODO: Return expected/actual parity values.
self.putp(['PARITY ERROR', 0, (0, 1)]) # FIXME: Dummy tuple...
self.putg([3, ['Parity error', 'Parity err', 'PE']])
self.frame_valid = False
self.state = 'GET STOP BITS'
3.6.5 状态5:获取停止位
# TODO: Currently only supports 1 stop bit.
def get_stop_bits(self, signal):
self.stopbit1 = signal
# Stop bits must be 1. If not, we report an error.
if self.stopbit1 != 1:
self.putp(['INVALID STOPBIT', 0, self.stopbit1])
self.putg([5, ['Frame error', 'Frame err', 'FE']])
self.frame_valid = False
self.putp(['STOPBIT', 0, self.stopbit1])
if self.options['anno_startstop'] == 'yes':
self.putg([2, ['Stop bit', 'Stop', 'T']])
# Pass the complete UART frame to upper layers.
es = self.samplenum + ceil(self.bit_width / 2.0)
self.putpse(self.frame_start, es, ['FRAME', 0,
(self.datavalue, self.frame_valid)])
self.state = 'WAIT FOR START BIT'
3.7 主循环 & 状态机调度(关键)
def decode(self):
# 如果没有指定波特率,则报错
if not self.samplerate:
raise SamplerateError('Cannot decode without samplerate.')
# 如果Invert Signal被配置为yes,则标记inv,后面处理的时候对输入信号翻转
inv = self.options['invert'] == 'yes'
cond_data_idx = None
# 确定一个完整帧时间跨度内的样本数量,信号低电平持续至少该时长即为中断条件
# 起始位宽度:固定为1bit
frame_samples = 1
# 数据位宽度:根据配置确定,4bit-128bit
frame_samples += self.options['num_data_bits']
# 校验位宽度:有校验则为1bit,无校验则为0bit
frame_samples += 0 if self.options['parity_type'] == 'none' else 1
# 停止位宽度:根据配置确定,0bit-2.5bit
frame_samples += self.options['num_stop_bits']
# 将UART一个数据帧的位长度转变为逻辑分析仪采样点数
frame_samples *= self.bit_width
self.break_min_sample_count = ceil(frame_samples)
cond_edge_idx = None
# 主循环
while True:
# self.wait的退出阻塞条件
conds = [] # conds为等待条件列表
cond_data_idx = len(conds)
conds.append(self.get_wait_cond(inv)) # 详见get_wait_cond()注释
cond_edge_idx = len(conds)
conds.append({0: 'e'}) # 向等待条件列表添加终止标记
# 阻塞,直到满足conds要求的阻塞条件被满足
# 条件有可能是:等待检测到边沿,也有可能是逻辑分析仪采集到一定数量的点数
(rxtx, ) = self.wait(conds)
# 已经获取到特定位置的采样点,调用相应的处理函数
# 在这里将实现状态机的调度
if cond_data_idx is not None and (self.matched & (0b1 << cond_data_idx)):
self.inspect_sample(rxtx, inv)
# 已经获取到了特定的边沿,调用相应的处理函数
# 在这里将实现错误处理
if cond_edge_idx is not None and (self.matched & (0b1 << cond_edge_idx)):
self.inspect_edge(rxtx, inv)
3.7.1 get_wait_cond()_计算阻塞的点数
def get_wait_cond(self, inv):
# 获取当前状态机的状态,该状态用于返回输入给Decoder.wait()的条件
state = self.state
# 当前状态是等待起始位:返回条件字典
# 键0表示起始位,值'r'(上升沿)或'f'(下降沿)
# 如果Invert Signal被配置为yes,则捕获下降沿,反之则捕获上升沿
# 当捕获到上升沿或下降沿时,Decoder.wait()退出阻塞
if state == 'WAIT FOR START BIT':
return {0: 'r' if inv else 'f'}
# 当前状态是获取起始位:bitnum = 0
# 在本函数后面的self.get_sample_point(bitnum)函数中,自带0.5个bit的延时
# 这代表:从捕获到上升沿/下降沿后的第0.5个bit,是起始位的判断位置
# 当到达这个位置的时候,Decoder.wait()退出阻塞
if state == 'GET START BIT':
bitnum = 0
# 当前状态是获取起始位:bitnum = 1(起始位) + 已经获取到的数据位数量
# 这是因为要依次获取每个数据位的采样位置(即每个bit的中心)
# 从捕获到上升沿/下降沿后的第1.5bit、2.5bit直到n.5bit都是数据位(n=数据位长度-1)
elif state == 'GET DATA BITS':
bitnum = 1 + self.cur_data_bit # self.cur_data_bit由0开始递增,直到数据位长度-1
# 当前状态是获取校验位:bitnum = 1(起始位) + 数据位长度
elif state == 'GET PARITY BIT':
bitnum = 1 + self.options['num_data_bits']
# 当前状态是获取停止位:bitnum = 1(起始位) + 数据位长度 + 校验位长度
elif state == 'GET STOP BITS':
bitnum = 1 + self.options['num_data_bits']
bitnum += 0 if self.options['parity_type'] == 'none' else 1
# 将UART bit长度转换为逻辑分析仪采样点的点数
# self.get_sample_point(bitnum)函数内部会自动加0.5个bit的采样点数,即bit的中间采样点位置
want_num = ceil(self.get_sample_point(bitnum))
# 返回从现在开始,需要等待多少采样点,才可以退出Decoder.wait()的阻塞
return {'skip': want_num - self.samplenum}
3.7.2 inspect_sample()_状态机调度
def inspect_sample(self, signal, inv):
# 信号翻转处理判断
if inv:
signal = not signal
# 状态机调度
state = self.state
if state == 'WAIT FOR START BIT':
self.wait_for_start_bit(signal)
elif state == 'GET START BIT':
self.get_start_bit(signal)
elif state == 'GET DATA BITS':
self.get_data_bits(signal)
elif state == 'GET PARITY BIT':
self.get_parity_bit(signal)
elif state == 'GET STOP BITS':
self.get_stop_bits(signal)
3.7.3 inspect_edge()_边沿捕获处理函数
def inspect_edge(self, signal, inv):
# 信号翻转处理判断
if inv:
signal = not signal
# 判断当前是否是起始位的电平状态
# self.break_start是UART起始位的第一个采样点
if not signal:
self.break_start = self.samplenum
return
# Signal went high. Was there an extended period with low signal?
if self.break_start is None:
return
# 错误处理
diff = self.samplenum - self.break_start
if diff >= self.break_min_sample_count:
self.handle_break()
self.break_start = None
3.7.4 handle_break()_错误处理函数
错误处理函数用于防止状态机卡在某个状态中,无法退出。
比如接收到了起始位,校验也通过,但是数据位迟迟没有到来。
def handle_break(self):
self.putpse(self.frame_start, self.samplenum,
['BREAK', 0, 0])
self.putgse(self.frame_start, self.samplenum,
[7, ['Break condition', 'Break', 'Brk', 'B']])
self.state = 'WAIT FOR START BIT'

1351

被折叠的 条评论
为什么被折叠?



