当前位置: 首页 > news >正文

逻辑分析仪解码脚本实例解析——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的解码过程。

  1. 切换到demo模式,并删除所有解码器
    在这里插入图片描述
  2. 只保留UART通道
    在这里插入图片描述
  3. 添加第一个 UART 解码器
    在这里插入图片描述
  4. 添加第二个 UART 解码器
    在这里插入图片描述
  5. 查看 UART 解码结果
    在这里插入图片描述

2.2 UART 帧解码步骤

通过观察上述解码结果,可以看出一个串口帧解码大概分为以下几个步骤:

1️⃣ 寻找跳变沿:上升沿 / 下降沿
2️⃣ 确定起始位状态:合法 / 非法
3️⃣ 确定数据位状态:0 / 1
4️⃣ 确定结束位状态:0 / 1

上述demo添加了两个 UART 解码器,即 0: UART1: 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 的某个 bit1 还是 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'
http://www.dtcms.com/a/393004.html

相关文章:

  • 垃圾回收中的STW是什么?
  • redis未授权漏洞扫描器
  • LTE/EPC 架构
  • ANSYS学习
  • 【python】安装jieba库
  • tyza66的博客:专注软件开发、全栈开发与开源项目的技术分享
  • Redis最佳实践——购物车优化详解
  • Netty从0到1系列之Netty内存管理【下】
  • 【使用函数求余弦COS函数的近似值】2022-11-27
  • 前端违规页面车主信息优化说明
  • 成功安装了 Anaconda3。要启动它,您有以下几种主要方式:方式一:通过“开始菜单”启动(最直接的方法)1. 点击您电脑屏幕左下角的 “开始菜单”(Win
  • flex布局实现导航栏横向滚动切换
  • 改进过程缺乏数据驱动会带来哪些后果
  • 实验1.1点亮led灯
  • 林粒粒的视频笔记13-数据清洗
  • Java进阶教程,全面剖析Java多线程编程,线程出让,笔记09
  • 大模型微调之 用LoRA微调Llama2(附代码)——李宏毅2025大模型作业5笔记-上
  • Matplotlib地理数据可视化技术详解:Cartopy与Basemap实战指南
  • wordpress 图片不显示 后台无法登陆的问题之一
  • TFS-2023《Local-Global Fuzzy Clustering With Anchor Graph》
  • Spring —— AOP
  • 讲一下ZooKeeper的持久化机制
  • 【Java后端】深入理解 Spring Security:从原理到实战
  • LeetCode:31.K个一组翻转链表
  • openharmony之系统亮度范围定制
  • 一种利用串口51单片机远程升级 OTA
  • Redis三种集群模式
  • C++ map_set封装
  • NW836NW884美光固态闪存NW885NW913
  • STM32计算步进电机转速