逻辑分析仪解码脚本实例解析——UART
文章目录
- 0 前言
- 1 准备工作
- 1.1 下载DSView
- 1.2 DSLogic逻辑分析仪
- 2 理解DSView解码机制
- 2.1 运行DSView的demo
- 2.2 UART 帧解码步骤
- 3 解码脚本解析—— UART 解码器
- 3.1 找到解码脚本
- 3.2 import & 异常类 & 全局函数
- 3.2.1 校验位检查
- 3.2.2 抛出异常
- 3.3 解码器 Decoder 类
- 3.3.1 解码器属性配置
- 3.3.2 初始化函数和复位函数
- 3.4 解码结果显示函数
- 3.5 采样点数和位置的计算
- 3.5.1 计算UART一个bit对应的采样点数
- 3.5.2 计算UART一个bit中间点的索引
- 3.6 状态机
- 3.6.1 状态1:等待起始位
- 3.6.2 状态2:获取起始位
- 3.6.3 状态3:获取数据位
- 3.6.4 状态4:获取校验位
- 3.6.5 状态5:获取停止位
- 3.7 主循环 & 状态机调度(关键)
- 3.7.1 get_wait_cond()_计算阻塞的点数
- 3.7.2 inspect_sample()_状态机调度
- 3.7.3 inspect_edge()_边沿捕获处理函数
- 3.7.4 handle_break()_错误处理函数
0 前言
题主为了使用逻辑分析仪解码自己的私有协议,琢磨怎么自己实现一个解码器脚本
在网上几乎没找到相关的资料,所以自己找到了 DSLogic
的解码脚本,并研究了一下解码逻辑,然后改了个脚本出来。为了避免后面有需求的朋友花时间再研究一遍,所以写这个帖子。
这个帖子在写了一半的时候,发现已经有大佬做了非常棒的讲解,而且大佬也是用UART做示例的
原文链接: 逻辑分析仪协议解码教程
相比于大佬的帖子,本文会讲的更基础更细一些,在大模型的帮助下,代码注释也会更多一点
DSView
解码器是基于 sigrok
开源项目的 libsigrokdecode
sigrok
官方也提供了大量的资料,链接:sigrok开源项目
本文通过解析 DSView
的解码器脚本源码,帮助你使用 DSLogic
逻辑分析仪解码私有的通讯协议,内容如下:
🌟 DSView
逻辑分析仪的解码器入门
🌟 理解 DSView
的解码机制
🌟 实战解析 UART
解码器脚本
🌟 尝试完成私有协议的硬件解码任务
1 准备工作
1.1 下载DSView
下载链接:DreamSourceLab——Download
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 == 0elif 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) == 1elif parity_type == 'even':return (ones % 2) == 0
3.2.2 抛出异常
class SamplerateError(Exception):passclass ChannelError(Exception):pass
3.3 解码器 Decoder 类
本章以下所有的内容都属于脚本的核心: Decoder
类
3.3.1 解码器属性配置
注意:在改写自己的解码器的时候,必须要把id
改成其它的,否则进入软件的时候会报错
class Decoder(srd.Decoder):api_version = 3id = '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 = Noneself.samplenum = 0self.frame_start = -1self.frame_valid = Noneself.startbit = -1self.cur_data_bit = 0self.datavalue = 0self.paritybit = -1self.stopbit1 = -1self.startsample = -1self.state = 'WAIT FOR START BIT'self.databits = []self.break_start = Nonedef 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.0self.put(s - floor(halfbit), self.samplenum + ceil(halfbit), self.out_python, data)def putg(self, data):s, halfbit = self.samplenum, self.bit_width / 2.0self.put(s - floor(halfbit), s + ceil(halfbit), self.out_ann, data)def putp(self, data):s, halfbit = self.samplenum, self.bit_width / 2.0self.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.0self.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.0bitpos += bitnum * self.bit_widthreturn 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.samplenumself.frame_valid = Trueself.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 = Falsees = self.samplenum + ceil(self.bit_width / 2.0)self.putpse(self.frame_start, es, ['FRAME', 0,(self.datavalue, self.frame_valid)])# 回到状态1self.state = 'WAIT FOR START BIT' return# 复位本数据帧相关的变量# 将self.startsample标记为-1,用于后续状态3标识首个数据位self.cur_data_bit = 0self.datavalue = 0self.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 += 1if 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.datavaluebdata = 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 = signalif 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 = Falseself.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 = Falseself.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# 确定一个完整帧时间跨度内的样本数量,信号低电平持续至少该时长即为中断条件# 起始位宽度:固定为1bitframe_samples = 1 # 数据位宽度:根据配置确定,4bit-128bitframe_samples += self.options['num_data_bits'] # 校验位宽度:有校验则为1bit,无校验则为0bitframe_samples += 0 if self.options['parity_type'] == 'none' else 1# 停止位宽度:根据配置确定,0bit-2.5bitframe_samples += self.options['num_stop_bits']# 将UART一个数据帧的位长度转变为逻辑分析仪采样点数frame_samples *= self.bit_widthself.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.stateif 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.samplenumreturn# Signal went high. Was there an extended period with low signal?if self.break_start is None:return# 错误处理diff = self.samplenum - self.break_startif 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'