基于STM32的逻辑分析仪

文章目录

  • 一、逻辑分析仪体验
    • 1、使用示例
      • 1.1 逻辑分析仪
      • 1.2 开源软件PulseView
    • 2、核心技术
      • 2.1 技术方案
      • 2.2 信号采集与存储
      • 2.3 数据上传
    • 3、使用逻辑分析仪
    • 4、 SourceInsight 使用技巧
      • 4.1新建工程
      • 4.2 设置工程名及工程数据目录
      • 4.3 指定源码目录
      • 4.4 添加源码
      • 4.5 同步文件
      • 4.6 操作示例
        • 4.6.1 在工程中打开文件
        • 4.6.2 在文件中查看函数或变量的定义
        • 4.6.3 查找函数或变量的引用
        • 4.6.4 其他快捷键
  • 二、逻辑分析仪SUMP协议分析
    • 1、硬件结构
    • 2、SUMP协议
      • 2.1 上位机发出的命令及参数
      • 2.2 下位机发送的回复
        • 2.2.1 CMD_ID 的回复
        • 2.2.2 CMD_METADATA 命令的回复
        • 2.2.3 上报的采样数据
      • 2.3 上位机和下位机时序图
        • 2.3.1 扫描操作的时序图
        • 2.3.2 对时序图进行源码分析
          • (1)上位机
          • (2)下位机
        • 2.3.3 逻辑分析仪的设置操作
          • (1)设置采样数
          • (2)设置采样频率
          • (3)使能或禁止通道
          • (4)设置通道的触发条件
        • 2.3.4 设置操作的时序图
        • 2.3.5 采样操作的时序图
  • 三、实现逻辑分析仪
    • 1、软件设计方案
      • 1.1 如何实现最高的采样率
      • 1.2 汇编指令
      • 1.3 精确测量时间
        • 1.3.1 测量读GPIO操作的时间
        • 1.3.2 测量读写buffer的时间
        • 1.3.3 测量NOP指令的时间
        • 1.3.4 逻辑右移
        • 1.3.5 加法操作
        • 1.3.6 测量处理Tick中断函数的时间
    • 2、实现功能
      • 2.1 方案修订
      • 2.2 编写程序
        • 2.2.1 采集数据
        • 2.2.2 上报数据
      • 2.3 上机演示
    • 3、改进功能
      • 3.1 使用说明
      • 3.2 最终程序的结构
      • 3.3 提高采样率
      • 3.4 增加改进USB串口功能
        • 3.4.1 在STM32CubeMX 增加USB 串口功能
        • 3.4.2 USB 串口收发函数改造
        • 3.4.3 提高USB串口发送效率
      • 3.5 使用RLE提升重复数据的传输效率
        • 3.5.1 RLE功能
        • 3.5.2 上位机使能RLE
        • 3.5.3 代码解读

一、逻辑分析仪体验

1、使用示例

1.1 逻辑分析仪

逻辑分析仪是分析数字信号的仪器,简单地说就是采集引脚的高低电平,按照某些协议分析多个引脚的信息(比如I2C、SPI信号)。

有些逻辑分析仪也可以采集模拟信号,是简化版的示波器。

使用场景如下:

在这里插入图片描述

1.2 开源软件PulseView

软件下载:https://sigrok.org/wiki/Downloads

在这里插入图片描述

2、核心技术

2.1 技术方案

PC机软件:可以使用开源软件PulseView。

逻辑分析仪的方案有很多种,产品级别的方案如下:

在这里插入图片描述

它一般都使用FPGA进行数据采集(高速、并行),把结果保存在大容量的DRAM里。采集完毕后,再通过单片机上传到PC进行分析:这被称为buffer模式。这种模式支持的采样率比较高,但是采集的时长、数据量取决于DRAM大小。

还有stream模式,一边采集数据一边传输给PC,它支持的采样时间比较长,但是采样率受限于数据接口(网卡、USB口、串口)的传输效率。

很多开源的逻辑分析仪只需要一个带USB口的单片机即可实现,方案如下:

在这里插入图片描述

跟PC之间的接口,可以使用USB,也可以使用普通的串口。即使使用USB时,大多数情况下也是模拟为USB串口。

2.2 信号采集与存储

本教程使用瑞士军刀(STM32F103)制作一个demo版本的逻辑分析仪。

信号的采集主要涉及2部分:信号电平、高精度的时间。如下图所示,你要分析这些信号,就需要知道它们的电平变化时刻、电平值:
在这里插入图片描述

2.3 数据上传

该项目中,先使用串口上传数据,这个比较简单,但是效率低。然后再使用板子的USB口模拟一个串口来上传数据,这样数据传输效率比较高。

3、使用逻辑分析仪

启动PulseView,如下操作可以识别出逻辑分析仪:

在这里插入图片描述

点击如下图标,可以捕获数据:

在这里插入图片描述

4、 SourceInsight 使用技巧

4.1新建工程

运行source Insight,点击菜单“Project->New Project”,如下图所示:

在这里插入图片描述

4.2 设置工程名及工程数据目录

在弹出的 New Project 对话框中设置“New project name”(项目的名称),然后设置 Where do you want to store the project data file? (项目文件保存位置),点击 Browse 按 钮选择源码的目录即可,如下图:

在这里插入图片描述

4.3 指定源码目录

设置源码目录:Project Source Directory – the main location of your source files”() 点击红框左边“…”选择源码目录,点击OK,如下图所示:

在这里插入图片描述

4.4 添加源码

在新弹出的对话框中,点击“Add”或“Add All”。“Add”是手动选择需要添加的文 件,而“Add All”是添加所有文件。我们使用“Add All”,在弹出的提示框中选中 “Recursively add lower sub-directories”(递归添加下级的子目录)并点击OK。同样的 Remove File,Remove All是移除单个文件或者移除所有文件,如下图所示:

在这里插入图片描述

添加文件完成后会弹出下面窗口,点击“确定”即可:

在这里插入图片描述

此时界面会返回到主界面,如下图所示,点击“Close”:

在这里插入图片描述

4.5 同步文件

同步文件的意思是让Source Insight去解析源码,生成数据库,这样有助于以后阅读 源码。比如点击某个函数时就可以飞快地跳到它定义的地方。 先点击菜单“Project->Synchronize Files”,如下图错误!未找到引用源。所示:

在这里插入图片描述

在弹出的对话框中 选中“Force all files to be re-parsed”(强制解析所有文件),并点击 “Start”按钮开始同步,如下图所示:

在这里插入图片描述

在这里插入图片描述

4.6 操作示例

前面建立工程后,就会自动打开了工程。如果下次你想打开工程,启动Souce Insight 后,点击菜单“Project -> Open Porject”就可以在一个列表中选择以前建立的工程,如下 图所示:

在这里插入图片描述

4.6.1 在工程中打开文件

点击"P"图标打开文件列表,双击文件打开文件,也可以输入文件名查找文件,如下图 所示:

在这里插入图片描述

4.6.2 在文件中查看函数或变量的定义

打开文件后,按住ctrl键的同时,用鼠标点击函数、变量,就会跳到定义它的位置, 如下图所示:

在这里插入图片描述

4.6.3 查找函数或变量的引用

双击函数,右键点击弹出对话框选择“Lookup Reference”;或者双击函数后,使用 快捷键"ctrl+/"来查找引用,如下图所示:

在这里插入图片描述

4.6.4 其他快捷键

在这里插入图片描述

二、逻辑分析仪SUMP协议分析

根据使用流程分析上位机程序、下位机程序的交互过程,就可以弄清楚逻辑分析仪的 协议。逻辑分析仪的协议有很多种类型,我们使用的上位机程序,借用了“openbench logic-sniffer”逻辑分析仪的代码,它使用SUMP协议。SUMP协议网址:

https://www.sump.org/projects/analyzer/protocol/

1、硬件结构

下图是逻辑分析仪的最简单的硬件结构:

① 上位机、下位机之间使用串口通信

② 下位机使用GPIO采集数据

注意:商用的逻辑分析仪一般使用FPGA采集数据,使用USB跟上位机通信。

在这里插入图片描述

2、SUMP协议

2.1 上位机发出的命令及参数

上位机和下位机之间的交互过程为:

① 上位机发送命令、数据

② 下位机“可能”回复

完整的命令和参数说明如下:

命令命令值作用
CMD_RESET0x00复位下位机
CMD_ID0x02让下位机上报ID
CMD_METADATA0x04让下位机上报参数
CMD_SET_BASIC_TRIGGER_MASK00xC0使能某个通道的触发功能
示例数值:0x01 0x02 0x00 0x00 表示channel0, 9使能了触发功能
CMD_SET_BASIC_TRIGGER_VALUE00xC1设置通道的触发值
示例数值:0x01 0x00 0x00 0x00 表示channel 0的触发值为高电平 channel 9的触发值为低电平
CMD_SET_BASIC_TRIGGER_CONFIG00xC2最后一个字节的bit3为1表示启动触发功能
示例数值:0x00 0x00 0x00 0x08 最后一个字节的bit3表示启动触发 功能
CMD_SET_DIVIDER0x80根据用户设置的采样频率计算出分频系数
注意:
当采样频率大于100MHz时, 会"Enable demux mode", 让逻辑分析工作于200MHz, 分频系数=200MHz/采样频率 - 1 当采样频率小于100MHz时, 分频系数=100MHz/采样频率 -1 示例数值:0xf3 0x01 0x00 0x00 分配系数为: 0x01f3=499=100MHz/200KHz-1
CMD_CAPTURE_SIZE0x81使用1个命令发送READCOUNT DELAYCOUNT两个参数
示例数值:0x0c 0x00 0x0c 0x00 前2字节表示要采样的次数为 0x0c * 4 = 48
后2字节表示要延迟的次数为 0x0c * 4 = 48
CMD_SET_FLAGS0x82设置flag,
比如使用启动demux模式
根据用户选择的通道,使能group (见后面注释)
CMD_CAPTURE_DELAYCOUNT0x83示满足触发条件开始采样后, 延迟多少次采样,才保存数据
示例数值:0x0c 0x00 0x00 0x00 表示延迟次数为 0x0c * 4 = 48
CMD_CAPTURE_READCOUNT0x84表示要采样的次数
示例数值:0x0c 0x00 0x00 0x00
采样次数为 0x0c * 4 = 48

对于命令CMD_SET_FLAGS,它的数值为32位的数据,含义如下(bit2bit5对于group14,比如bit2为0表示group 1使能):

#define CAPTURE_FLAG_RLEMODE1 (1 << 15)

#define CAPTURE_FLAG_RLEMODE0 (1 << 14)

#define CAPTURE_FLAG_RESERVED1 (1 << 13)

#define CAPTURE_FLAG_RESERVED0 (1 << 12)

#define CAPTURE_FLAG_INTERNAL_TEST_MODE (1 << 11)

#define CAPTURE_FLAG_EXTERNAL_TEST_MODE (1 << 10)

#define CAPTURE_FLAG_SWAP_CHANNELS (1 << 9)

#define CAPTURE_FLAG_RLE (1 << 8)

#define CAPTURE_FLAG_INVERT_EXT_CLOCK (1 << 7)

#define CAPTURE_FLAG_CLOCK_EXTERNAL (1 << 6)

#define CAPTURE_FLAG_DISABLE_CHANGROUP_4 (1 << 5)

#define CAPTURE_FLAG_DISABLE_CHANGROUP_3 (1 << 4)

#define CAPTURE_FLAG_DISABLE_CHANGROUP_2 (1 << 3)

#define CAPTURE_FLAG_DISABLE_CHANGROUP_1 (1 << 2)

#define CAPTURE_FLAG_NOISE_FILTER (1 << 1)

#define CAPTURE_FLAG_DEMUX (1 << 0)
其中CAPTURE_FLAG_DISABLE_CHANGROUP_1-4 其中有等于1的则该group被禁止 若为0则该group为使能状态 并且只有当所有channel被禁止才为1 若有部分channel被禁止而已则该group还是为0(使能状态)

2.2 下位机发送的回复

2.2.1 CMD_ID 的回复

上位机发送CMD_ID命令(“0x02”)给下位机后,下位机要回复4个字节“1ALS”。

2.2.2 CMD_METADATA 命令的回复

上位机发送CMD_ID命令(“0x04”)给下位机后,下位机要回复很多参数给上位机。参数格式为“1字节的数据类别,多个字节的数据”,说明如下:

上报的数据类别上报的数据说明
0x01“100ASK_LogicalNucleo”名字
0x20大字节序的4字节最大采样通道数
0x21大字节序的4字节保存采样数据的buffer大小
0x22大字节序的4字节动态内存大小(未使用)
0x23大字节序的4字节最大采样频率
0x24大字节序的4字节协议版本
0x401 字节最大采样通道数
0x411 字节协议版本
0x00结束标记
2.2.3 上报的采样数据

对于我们借用的“openbench-logic-sniffer”逻辑分析仪协议,它上报的数据是:先上报最后一个采样的数据,最后上报第1个采样点的数据。

每一个采样的数据里,含有使能的 group 的数据,比如使能了 group1、group2、group4,没有使能 group3,那么上报的一个采样数据为 3 字节:第 1 字节对应 group1 的channel 0~7,第 2 字节对应 group2 的 channel 8~15,第 3 字节对应 group4 的 channel 24~31。

每一个字节数据里,bit0对应这个group里最低的通道的值

2.3 上位机和下位机时序图

2.3.1 扫描操作的时序图

要理解时序图,建议对比着上位机源码、下位机源码进行分析:

// 上位机

sigrok-util_src_patched\libsigrok\src\hardware\openbench-logic-sniffer\api.c,scan函数

// 下位机

LogicAnalyzer_F103\Core\Src\logicanalyzer.c,LogicalAnalyzerTask函数

启动PulseView,如下操作可以识别出逻辑分析仪:

在这里插入图片描述

其中涉及的操作、时序图,如下:

在这里插入图片描述

2.3.2 对时序图进行源码分析
(1)上位机

sacn函数

在这里插入图片描述

在这里插入图片描述

ols_get_metadata函数

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

对应如下:

在这里插入图片描述

(2)下位机

logicanalyzer.c

在这里插入图片描述

在这里插入图片描述

2.3.3 逻辑分析仪的设置操作

SUMP 协议的逻辑分析仪,最大支持32个采样通道,分为4组:group 1、group 2、group 3、group 4,每组含有8个通道:channel 0~channel 7属于group 1,以此类推。

(1)设置采样数

如下可以设置采样数:

在这里插入图片描述

(2)设置采样频率

在这里插入图片描述

(3)使能或禁止通道

可以选择是否使能某个channel:

在这里插入图片描述

当32 个通道都使能时,一次采样得到32位数据,bit0 对应通道0,bit31 对应通道31。

当group n里8个通道都禁止的话,那么一次采样就可以少传输1字节。比如group 3里的channel 16~channel 23 都被禁止后,一次采样就可以得到3字节数据,bit16原来对应channel 16,现在对应channel 24,以此类推。

注意:只有当1个group中的所有8个通道都被禁止才少传输1字节,若1个group中的所有通道未全部被禁止,则还是传输原字节数

(4)设置通道的触发条件

可以设置采样的触发条件(对于使能了触发的多个通道,只要有某个通道的值符合触发条件了,就会开始采样):

在这里插入图片描述

2.3.4 设置操作的时序图

在启动采样之前,上位机会把前面设置的参数发给下位机,代码如下:

在这里插入图片描述

ols_prepare_acquisition 函数在如下文件中:

// 上位机 ols_prepare_acquisition函数

sigrok-util_src_patched\libsigrok\src\hardware\openbench-logic-sniffer\protocol.c

分析它的代码,得到如下时序图:

在这里插入图片描述

上位机source insight代码如下:

在这里插入图片描述

下位机source insight代码如下:

在这里插入图片描述

2.3.5 采样操作的时序图

当用户点击如下按钮后,上位机会先设置逻辑分析仪,最后发送启动命令开始采集数据;采集完毕后,上位机会放送停止命令。时序图如下:

在这里插入图片描述

三、实现逻辑分析仪

1、软件设计方案

商用的逻辑分析仪一般使用 FPGA 采集数据,并且有比较大的内存(比如 512MB)。FPGA 可以使用非常高的速率采集数据,然后存放在内存里。

对于单片机,内存小,速度慢,我们需要压榨它的性能。

1.1 如何实现最高的采样率

逻辑分析仪采样数据的示例代码如下:

// 1. 关闭中断 
// 2. 循环 
while (1) 
{ // 2.1 读GPIO // 2.2 写buffer // 2.3 延时 
} 
// 3. 开中断

为了达到最高的采样率,循环里面的“读GPIO”、“写buffer”操作使用汇编指令;循环里的“延时”代码要去除。在这种情况下,我们再去测量一次循环的耗时,就可以算出最高采样率。

当使用更低的采样率时,在上述代码里插入“耗时”操作。

这都需要我们事先知道“读GPIO”、“写buffer”、“延时指令”的耗时。

1.2 汇编指令

为了简化程序,我们只使用PB8PB15这8个引脚。要去读取这几个引脚的数值,需要读取GPIOB_IDR寄存器。这个寄存器的说明在“2_官方资料\2.0_STM32F103xx参考手册(英文原版)【重要】.pdf”里,寄存器的bit815分别对应PB8PB15引脚,对应上位机的channel07。寄存器如下:

在这里插入图片描述

注:LDR 表示读取四个字节 LDRH 表示读取两个字节 LDRB 表示读取一个字节

读GPIO 的汇编指令代码为:

LDR R1, =0x40010C08

LDR R0, [R1]

写buffer 的汇编指令代码为:

LDR R1, =buffer地址

STR R0, [R1]

在汇编里,我们可以在循环之间插入几条NOP指令来实现延时,比如:

NOP

NOP

1.3 精确测量时间

先列出结果,精确测量的时间列表如下:

操作汇编指令耗时
读取GPIO//R1 为0x40010C08
LDR R0, [R1]
44ns
读内存//R1 为0x20000000
LDR R0, [R1]
15ns
写内存//R1 为0x20000000
STRB R0, [R1]
16ns
NOP 指令NOP15ns
逻辑右移LSR R0, #824ns
累加ADD R0, #123ns
Tick 中断处理10ns

我们需要测量读一次GPIO的精确时间,写一次buffer的精确时间,一条NOP指令执行的时间。

示例代码如下:

// 1. 关闭中断 
{ // 2.1 设置某个引脚为高电平 // 2.2 循环100万次读GPIO,或写Buffer,或执行NOP指令 // 2.3 设置某个引脚为低电平 
} 
// 3. 开中断 

我们借助外部工具监测GPIO引脚为高电平的时间,就可以算出每次操作的耗时。可以使用逻辑分析仪来测量GPIO为高电平的时间。

逻辑分析仪测量PA15,如下图接线:

在这里插入图片描述

1.3.1 测量读GPIO操作的时间

相关寄存器位置如下:

在这里插入图片描述

在汇编中引脚配置如下:

在这里插入图片描述

测试函数的代码为:

void MeaSureTime(void)
{/* 1.让引脚输出低电平 */HAL_GPIO_WritePin(GPIOA,GPIO_PIN_15,GPIO_PIN_RESET);HAL_Delay(100);/* 2.关中断 */__disable_irq();/* 3.1 让引脚输出高电平 */HAL_GPIO_WritePin(GPIOA,GPIO_PIN_15,GPIO_PIN_SET);/* 3.2 执行汇编指令 *///for(int i = 0; i < 10000; i++)asm_measure();/* 3.3 让引脚输出低电平 */HAL_GPIO_WritePin(GPIOA,GPIO_PIN_15,GPIO_PIN_RESET);/* 4.开中断 */__enable_irq();
}

在汇编里读GPIO的代码为:

				THUMBAREA    |.text|, CODE, READONLY; asm_measure handler
asm_measure    PROCEXPORT  asm_measure; 设置PA15输出高电平; 使用汇编让PA15输出高电平供测量LDR  R1, =0x40010810LDRH  R0,= [1 << 15]STR  R0,  [R1]; 读GPIO 手写100条指令LDR  R1, =0x40010c08LDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDR    LDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDR    LDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDR    LDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDRLDRH  R0, [R1]  ; 读GPIOB_IDR; 设置PA15输出低电平; 使用汇编让PA15输出低电平供测量LDR  R1, =0x40010810LDRH  R0,= [1 << 31]STR  R0,  [R1]BX  LR  ; 返回ENDP

读100次GPIO,耗时:4.39us;读1次GPIO耗时约为44ns。

在这里插入图片描述

1.3.2 测量读写buffer的时间

在汇编里读内存的代码为:

				 THUMBAREA	 |.text|, CODE, READONLY; asm_measure_r_mem handler
asm_measure_r_mem	PROCEXPORT  asm_measure_r_mem; 设置PA15输出高电平LDR  R1, =0x40010810LDR  R0, =(1<<15)STR  R0, [R1]LDR  R1, =0x20000000LDRH  R0, [R1]  ; 读内存(100); 设置PA15输出低电平LDR  R1, =0x40010810LDR  R0,= (1 << 31)STR  R0,  [R1]BX  LRENDP

读100次内存,耗时:1.54us;读1次内存耗时约为15ns。

在这里插入图片描述

在汇编里写内存的代码为:

				 THUMBAREA	 |.text|, CODE, READONLY; asm_measure_r_mem handler
asm_measure_r_mem	PROCEXPORT  asm_measure_r_mem; 设置PA15输出高电平LDR  R1, =0x40010810LDR  R0, =(1<<15)STR  R0, [R1]LDR	R1, =0x20000000STRB  R0, [R1]	; 写内存(100); 设置PA15输出低电平LDR  R1, =0x40010810LDR  R0,= (1 << 31)STR  R0,  [R1]BX  LRENDP

写100次内存,耗时:1.58us;读1次内存耗时约为16ns。

在这里插入图片描述

1.3.3 测量NOP指令的时间

在汇编里写内存的代码为:

				 THUMBAREA	 |.text|, CODE, READONLY; asm_measure_r_mem handler
asm_measure_r_mem	PROCEXPORT  asm_measure_r_mem; 设置PA15输出高电平LDR  R1, =0x40010810LDR  R0, =(1<<15)STR  R0, [R1]NOP(写100次); 设置PA15输出低电平LDR  R1, =0x40010810LDR  R0,= (1 << 31)STR  R0,  [R1]BX  LRENDP

执行100次NOP指令,耗时:1.49us;执行1次NOP指令耗时约为15ns。
在这里插入图片描述

1.3.4 逻辑右移

在这里插入图片描述

在汇编里写内存的代码为:

				 THUMBAREA	 |.text|, CODE, READONLY; asm_measure_r_mem handler
asm_measure_r_mem	PROCEXPORT  asm_measure_r_mem; 设置PA15输出高电平LDR  R1, =0x40010810LDR  R0, =(1<<15)STR  R0, [R1]LSR R0, #8(写100次); 设置PA15输出低电平LDR  R1, =0x40010810LDR  R0,= (1 << 31)STR  R0,  [R1]BX  LRENDP

执行100次“LSR R0, #8”指令,耗时:2.4us;执行1次指令耗时约为24ns。

在这里插入图片描述

1.3.5 加法操作

在汇编里写内存的代码为:

				 THUMBAREA	 |.text|, CODE, READONLY; asm_measure_r_mem handler
asm_measure_r_mem	PROCEXPORT  asm_measure_r_mem; 设置PA15输出高电平LDR  R1, =0x40010810LDR  R0, =(1<<15)STR  R0, [R1]ADD R0, #1(写100次); 设置PA15输出低电平LDR  R1, =0x40010810LDR  R0,= (1 << 31)STR  R0,  [R1]BX  LRENDP

执行100次“ADD R0, #1”指令,耗时:2.33us;执行1次指令耗时约为23ns。

在这里插入图片描述

1.3.6 测量处理Tick中断函数的时间

测量关闭中断情况下一段代码的执行时间(约为10s):

void MeaSureTime(void)
{/* 1.让引脚输出低电平 */HAL_GPIO_WritePin(GPIOA,GPIO_PIN_15,GPIO_PIN_RESET);HAL_Delay(100);/* 2.关中断 */__disable_irq();/* 3.1 让引脚输出高电平 */HAL_GPIO_WritePin(GPIOA,GPIO_PIN_15,GPIO_PIN_SET);/* 3.2 执行汇编指令 */for(int i = 0; i < 2277984; i++) /* 10s(执行一次asm_measure()的时间为4.39us 通过换算得到10s约需要执行2277984次) */asm_measure();/* 3.3 让引脚输出低电平 */HAL_GPIO_WritePin(GPIOA,GPIO_PIN_15,GPIO_PIN_RESET);/* 4.开中断 */__enable_irq();
}

在这里插入图片描述
在这里插入图片描述

测量开中断情况下,同一段代码的执行时间:
在这里插入图片描述

void MeaSureTime(void)
{/* 1.让引脚输出低电平 */HAL_GPIO_WritePin(GPIOA,GPIO_PIN_15,GPIO_PIN_RESET);HAL_Delay(100);/* 2.关中断 *///__disable_irq();/* 3.1 让引脚输出高电平 */HAL_GPIO_WritePin(GPIOA,GPIO_PIN_15,GPIO_PIN_SET);/* 3.2 执行汇编指令 */for(int i = 0; i < 2277984; i++) /* 10s(执行一次asm_measure()的时间为4.39us 通过换算得到10s约需要执行2277984次)*/asm_measure();/* 3.3 让引脚输出低电平 */HAL_GPIO_WritePin(GPIOA,GPIO_PIN_15,GPIO_PIN_RESET);/* 4.开中断 *///__enable_irq();
}

相差:10.1939508(开启中断) - 10.0943512(关闭中断) = 0.0995996s,对应 10000次 tick(一个Tick1ms执行一次 10s对应10000个Tick) 中断,每次tick 中断耗时:0.0995996/10000=0.00995996ms=9.95996us,约为 10us。

2、实现功能

2.1 方案修订

精确测量的时间列表如下:

操作汇编指令耗时
读取GPIO//R1 为0x40010C08
LDR R0, [R1]
44ns
读内存//R1 为0x20000000
LDR R0, [R1]
15ns
写内存//R1 为0x20000000
STRB R0, [R1]
16ns
NOP 指令NOP15ns
逻辑右移LSR R0, #824ns
累加ADD R0, #123ns
Tick 中断处理10us

对于如下代码:

// 1. 关闭中断 
// 2. 循环 
while (1) 
{ // 2.1 读GPIO、逻辑右移 // 2.2 写buffer、累加地址 // 2.3 延时 
} 
// 3. 开中断 

去掉延时,循环一次耗时 44+24+16+23=107ns,理论上最高的采样频率=1/107ns=9MHz。而 STM32F103C8 的内存为 20K,即使全部用来保存采样的数据,也只能保存20*1024/9000000=0.002 秒,没有任何实用价值。

即使降低采样频率,比如降到100KHz(I2C 频率一般为100KHz,再低的话就没有实用价值了),20K内存全部用来保存采样数据,能保存20*1024/100000=0.2048秒,也没有什l么使用价值。

瓶颈在于:用来保存采样数据的内存太小了。看看商用的逻辑分析仪,它的内存是巨大的:

在这里插入图片描述

在有限的内存里,我们需要提高内存的使用效率:不变的数据就不要保存了。新方案如下:

① 定义两个数组:uint8_t data_buf[5000]、uint8_t cnt_buf[5000]

② 以比较高的、频率周期性地读取GPIO的值(可以启动定时器,但不能使能中断,因为Tick中断会耗时)

③ 只有GPIO值发生变化了,才存入data_buf[i++];GPIO 值无变化时,cnt_buf[i-1]累加

④ 以后,根据data_buf、cnt_buf恢复各个采样点的数据,上报给上位机

假设data_buf大小为5000,能记录5000个变化的数据,这足够我们日常使用了。

其他考虑:使用新方案后,能记录很长时间的数据,在程序运行期间,要判断是否收到“上位机发来的CMD_XOFF停止命令”,所以:串口接收中断要打开。

2.2 编写程序

核心代码为“Core\Src\logicanalyzer.c”,主要有2大功能:

① 采集数据:读取GPIO数据、保存起来

② 上报数据:把数据发给上位机

当下位机收到“CMD_ARM_BASIC_TRIGGER”命令后,启动采集、上报:

在这里插入图片描述

2.2.1 采集数据

start 函数采集数据,功能为:

① 禁止中断:这是为了在采集数据时以最快的频率采集,不让中断干扰。 除了串口中断之外,其他中断都禁止。下位机只有tick中断、串口中断,所以只需要 禁止tick中断。 保留串口中断的原因在于:上位机可能发来命令停止采样。

② 等待触发条件:用户可能设置触发采样的条件

③ 触发条件满足后,延时一会:没有必要

④ 循环:以最高频率采样 退出的条件有三:收到上位机发来的停止命令、采集完毕、数据buffer已经满

⑤ 恢复中断

关键功能是采集、记录数据:

/* 记录第一个数据 */
data = (*data_reg) >> 8;
g_rxdata_buf[0] = data;
g_rxcnt_buf[0] = 1;
g_cur_sample_cnt = 1;
pre_data = data;/* 4. 以最高的频率采集数据 */
#ifdef USE_ASM_TO_SAMPLE
sample_function();
#else
while (1)
{        *pa15_reg = (1<<15); /* PA15输出高电平 *//* 4.1 读取数据 */data = (*data_reg) >> 8;/* 4.2 保存数据 */        g_cur_pos += (data != pre_data)? 1 : 0; /* 数据不变的话,写位置不变 */g_rxdata_buf[g_cur_pos] = data;         /* 保存数据 */g_rxcnt_buf[g_cur_pos]++;               /* 增加"相同的数据"个数 */g_cur_sample_cnt++;                     /* 累加采样个数 */pre_data = data;/* 4.3 串口收到停止命令 */if (get_stop_cmd)break;/* 4.4 采集完毕? */if (g_cur_sample_cnt >= g_convreted_sample_count)break;/* 4.5 buffer满? */if (g_cur_pos >= BUFFER_SIZE)break;/* 4.6 加入这些延时凑出1MHz,加入多少个nop需要使用示波器或逻辑分析仪观察、调整 */__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );__asm volatile( "nop" );*pa15_reg = (1UL<<31); /* PA15输出低电平 */
}
2.2.2 上报数据

采集数据时是以最大频率采集的,比如以 1MHz 采集。如果上位机要求的采样频率是 200KHz:1MHz/200KHz=5,采集到的数据量是上报数据量的5倍。我们只需要每隔5个数据 上报一个即可。

static void upload (void)
{int32_t i = g_cur_pos;uint32_t j;uint32_t rate = MAX_FREQUENCY / g_samplingRate;int cnt = 0;for (; i >= 0; i--){for (j = 0; j < g_rxcnt_buf[i]; j++){cnt++;  /* 我们以最大频率采样, 假设最大频率是1MHz* 上位机想以200KHz的频率采样* 那么在得到的数据里, 每5个里只需要上报1个*/if (cnt == rate) {uart_send(&g_rxdata_buf[i], 1, 10);cnt = 0;}}}
}

2.3 上机演示

为了采集数据,设置下位机的PB3输出周期为1ms、占空比为50%的PWM波。让PB8连接到PB3,PB8是channel 0,就可以再上位机观察channel 0的波形。 如下图接线(串口也要接):

在这里插入图片描述
在这里插入图片描述

3、改进功能

3.1 使用说明

PB8~PB15 是通道 0~7,可以用来连接要测试的引脚。注意:测量其他电子设备时要共地。

PB3 输出周期1ms、50%占空比的PWM波,可以用来验证功能是否正常。

如下图连接:

在这里插入图片描述

采样示例:

在这里插入图片描述

3.2 最终程序的结构

在这里插入图片描述

3.3 提高采样率

使用汇编采集数据,把最大采样频率提高一倍,达到2MHz。

在工程的“Core\Src\logicanalyzer.h”中定义“USE_ASM_TO_SAMPLE”,就可以使用汇编代码采集数据,达到2MHz的采样频率。

在这里插入图片描述

汇编代码在“Core\Src\operation.S”中,如下:

BUFFER_SIZE equ 2700 THUMB AREA    |.text|, CODE, READONLY ; sample_function handler 
sample_function    PROC EXPORT  sample_function IMPORT g_rxdata_buf IMPORT g_rxcnt_buf IMPORT g_cur_pos IMPORT g_cur_sample_cnt IMPORT get_stop_cmd IMPORT g_convreted_sample_count PUSH     {R4, R5, R6, R7, R8, R9, R10, R11, R12, LR} LDR R0, =g_rxdata_buf  ; 得到这些变量的地址,并不是得到它们的值 LDR R1, =g_rxcnt_buf   ; 得到g_rxcnt_buf变量的地址,并不是得到它的值 LDR R2, =g_cur_pos     ; 得到g_cur_pos变量的地址,并不是得到它的值 LDR R2, [R2]           ; 得到g_cur_pos变量的值 LDR R3, =g_cur_sample_cnt LDR R3, [R3] LDR R4, =get_stop_cmd LDR R5, =g_convreted_sample_count LDR R5, [R5] LDR R8, [R0]  ; pre_data LDR R10, =BUFFER_SIZE LDR  R6, =0x40010C08; 设置PA15的值备用 LDR R11, =0X40010810 LDR R12, =(1<<15) LDR LR, =(1<<31) 
Loop     ; 设置PA15输出高电平 STR R12, [R11] LDRH R7, [R6]  ; 读GPIOB_IDR LSR R7, #8    ; data = (*data_reg) >> 8; CMP R7, R8 ADDNE R2, #1  ; g_cur_pos += (data != pre_data)? 1 : 0; STRB R7, [R0, R2] ; g_rxdata_buf[g_cur_pos] = data;     MOV R8, R7        ; pre_data = data LDR R7, [R1, R2, LSL #2] ; R7 = g_rxcnt_buf[g_cur_pos] ADD R7, #1 STR R7, [R1, R2, LSL #2] ; g_rxcnt_buf[g_cur_pos]++; ADD R3, #1    ; g_cur_sample_cnt++; CMP R3, R5    ; if (g_cur_sample_cnt >= g_convreted_sample_count) break; BGE LoopDone LDR R7, [R4]  ; R7 = get_stop_cmd CMP R7, #0    ; if (get_stop_cmd) break; BNE LoopDone CMP R2, R10    ; if (g_cur_pos >= BUFFER_SIZE) break; BGE LoopDone NOP NOP         ; 延时, 凑出2MHz ; 设置PA15输出高电平 STR LR, [R11] B Loop LoopDone LDR R0, =g_cur_pos     ; 得到g_cur_pos变量的地址,并不是得到它的值 STR R2, [R0]           ; 保存g_cur_pos变量的值 LDR R0, =g_cur_sample_cnt STR R3, [R0]           ; 保存g_cur_sample_cnt变量的值POP     {R4, R5, R6, R7, R8, R9, R10, R11, R12, PC} ENDP     

3.4 增加改进USB串口功能

3.4.1 在STM32CubeMX 增加USB 串口功能

第1步:使能USB功能,如下图操作:

在这里插入图片描述

第2步:设置时钟,确保CPU频率为最大的72MHz,USB频率为48MHz,如下图:

在这里插入图片描述

第3步:添加USB串口第3方组件,如下图:

在这里插入图片描述

烧录程序后,使用 USB 线连接开发板和 PC,可以在 PC 的设备管理器看到新的串口设备:

在这里插入图片描述

注意:由于硬件设计问题,每次烧写程序后都要重新接插USB线。要调试USB 串口功能的话,每次启动调试之后也要重新接插USB线。

硬件问题在于 USB 口的使能引脚常拉高,导致无法让 PC 重新识别 USB 设备(只能重插):

在这里插入图片描述

3.4.2 USB 串口收发函数改造

当下位机通过USB口接收到数据时,它的如下函数被调用:

在这里插入图片描述

当下位机想通过USB口发送数据时,使用如下函数:

在这里插入图片描述

发送函数需要改造,如下:

在这里插入图片描述

代码如下:

/** * @brief  Data received over USB OUT endpoint are sent over CDC interface *         through this function. * *         @note *         This function will issue a NAK packet on any OUT packet received on *         USB endpoint until exiting this function. If you exit this function *         before transfer is complete on CDC interface (ie. using DMA controller) *         it will result in receiving more data while previous ones are still *         not sent. * * @param  Buf: Buffer of data to be received * @param  Len: Number of data received (in bytes) * @retval Result of the operation: USBD_OK if all operations are OK else USBD_FAIL */ 
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len) 
{ /* USER CODE BEGIN 6 */ for (uint32_t i = 0; i < *Len; i++) { circle_buf_write(&g_uart_rx_bufs, Buf[i]); } USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]); USBD_CDC_ReceivePacket(&hUsbDeviceFS); return (USBD_OK); /* USER CODE END 6 */ 
} /* USER CODE BEGIN PRIVATE_FUNCTIONS_IMPLEMENTATION */ 
uint8_t usb_send(uint8_t *datas, int len, int timeout) 
{ USBD_CDC_HandleTypeDef *hcdc = (USBD_CDC_HandleTypeDef*)hUsbDeviceFS.pClassData; while(1) { if (hcdc->TxState == 0) { break; } if (timeout--) { mdelay(1); } else { return HAL_BUSY; } } return CDC_Transmit_FS(datas, len); 
}
3.4.3 提高USB串口发送效率

虽然USB速度远高于UART,但是如果使用USB传输数据时是一个字节一个字节地传输,那么效率极低。我们需要根据USB的特性,一次尽可能传输更多数据。STM32F103的USB传输,一次能传输最多64字节的数据。上位机怎么知道当前数据传输完毕了呢?下位机可以 传输少于 64 字节的数据,上位机就知道当前传输完毕了(没传完你干嘛不传输 64 字节 呢?);如果下位机刚好要传输64字节的数据,那么USB驱动还要额外传输一个“零包” 给上位机,为了避免传输零包,我们尽量每次传输63字节。
在这里插入图片描述
代码如下:

/********************************************************************** 
* 函数名称: uart_save_in_buf_and_send 
* 功能描述: 使用USB传输时,一个一个字节地传输效率非常低,尽量一次传输64字节 
* 输入参数: datas - 保存有要发送的数据 
*            
len - 数据长度 
*            
timeout - 超时时间(ms) 
*            
flush - 1(即刻发送), 0(可以先缓存起来) 
* 输出参数: 无 
* 返 回 值: 无 ***********************************************************************/ 
static void uart_save_in_buf_and_send(uint8_t *datas, int len, int timeout, int flush) 
{ static uint8_t buf[64]; static int32_t cnt = 0; for (int32_t i = 0; i < len; i++) { buf[cnt++] = datas[i]; /* 先存入buf, 凑够63字节再发送 */ if (cnt == 63) { /* 对于USB传输,它内部发送64字节数据后还要发送一个零包 * 所以我们只发送63字节以免再发送零包 */ uart_send(buf, cnt, timeout); cnt = 0; } } /* 如果指定要"flush"(比如这是最后要发送的数据了), 则发送剩下的数据 */ if (flush && cnt) { uart_send(buf, cnt, timeout); cnt = 0; } 
}

3.5 使用RLE提升重复数据的传输效率

3.5.1 RLE功能

假设要传输9个相同的数据,比如9个0x12,那么常规方法就要发送9个0x12。如果规定发送的数据里,某一个数据表示“相同的数据个数”,后面跟着这个数据,不就只需要发送2个字节的数据了吗?比如“0x09 0x12”。我们怎么分辨一个数据是长度,还是数据本身?可以使用最高位来分辨:比如0x89表示要传输9个数据(SUMP协议里表示要传输10个数据),0x12表示数据本身。缺点是:数据里最高位必须清为0。

RLE: Run Length Encoding,在数据里嵌入长度。在传输重复的数据时可以提高效率。

SUMP协议里规定:

① 传输长度:最高位为1,去掉最高位的数值为n,表示有(n+1)个数据

② 传输数据:数据的最高位必须为0

例子1:对于8通道的数据,channel 7就无法使用了。要传输10个数据0x12时,只需要传输2字节:0x89 0x12。0x89的最高位为1,表示有(9+1)个相同的数据,数据为0x12。

例子2:对于32通道的数据,channel 31就无法使用了。要传输10个数据 0x12345678时,只需要传输8字节:0x09 0x00 0x00 0x80 0x78 0x56 0x34 0x12 “0x09 0x00 0x00 0x80"的最高位为1,表示有(9+1)个相同的数据,数据为"0x78 0x56 0x34 0x12”

3.5.2 上位机使能RLE

如下设置:

在这里插入图片描述

3.5.3 代码解读

代码如下:

/********************************************************************** * 函数名称: upload * 功能描述: 上报数据 * 输入参数: 无 * 输出参数: 无 * 返 回 值: 无 ***********************************************************************/ 
static void upload (void) 
{ int32_t i = g_cur_pos; uint32_t j; uint32_t rate = MAX_FREQUENCY / g_samplingRate; int cnt = 0; uint8_t pre_data; uint8_t data; uint8_t rle_cnt = 0; for (; i >= 0; i--) { for (j = 0; j < g_rxcnt_buf[i]; j++) { cnt++;   /* 我们以最大频率采样, 假设最大频率是1MHz * 上位机想以200KHz的频率采样 * 那么在得到的数据里, 每5个里只需要上报1个 */ if (cnt == rate)  { if (g_flags & CAPTURE_FLAG_RLE) { /* RLE : Run Length Encoding, 在数据里嵌入长度, 在传输重复的数据时可以提高效率 * 先传输长度: 最高位为1表示长度, 去掉最高位的数值为n, 表示有(n+1)个数据 * 再传输数据本身 (数据的最高位必须为0) * 例子1: 对于8通道的数据, channel 7就无法使用了 * 要传输10个数据 0x12时, 只需要传输2字节: 0x89 0x12 * 0x89的最高位为1, 表示有(9+1)个相同的数据, 数据为0x12 *  * 例子2: 对于32通道的数据, channel 31就无法使用了 * 要传输10个数据 0x12345678时, 只需要传输8字节: 0x09 0x00 0x00 0x80 0x78 0x56 0x34 0x12 * "0x09 0x00 0x00 0x80"的最高位为1, 表示有(9+1)个相同的数据, 数据为"0x78 0x56 0x34 0x12" */ data = g_rxdata_buf[i] & ~0x80; /* 使用RLE时数据的最高位要清零 */; if (rle_cnt == 0) { pre_data = data; rle_cnt = 1; } else if (pre_data == data) { rle_cnt++; /* 数据相同则累加个数 */ } else if (pre_data != data) { /* 数据不同则上传前面的数据 */ if (rle_cnt == 1) /* 如果前面的数据只有一个,则无需RLE编码 */ uart_save_in_buf_and_send(&pre_data, 1, 100, 0); else { /* 如果前面的数据大于1个,则使用RLE编码 */ rle_cnt = 0x80 | (rle_cnt - 1); uart_save_in_buf_and_send(&rle_cnt, 1, 100, 0); uart_save_in_buf_and_send(&pre_data, 1, 100, 0); } pre_data = data; rle_cnt = 1; } if (rle_cnt == 128) { /* 对于只有8个通道的逻辑分析仪, 只使用1个字节表示长度,最大长度为128 * 当相同数据个数累加到128个时, * 就先上传 */ rle_cnt = 0x80 | (rle_cnt - 1); uart_save_in_buf_and_send(&rle_cnt, 1, 100, 0); uart_save_in_buf_and_send(&pre_data, 1, 100, 0); rle_cnt = 0; } } else { /* 上位机没有起到RLE功能则直接上传 */ uart_save_in_buf_and_send(&g_rxdata_buf[i], 1, 100, 0); } cnt = 0; } } } /* 发送最后的数据 */ if ((g_flags | CAPTURE_FLAG_RLE) && rle_cnt) { if (rle_cnt == 1) uart_save_in_buf_and_send(&pre_data, 1, 100, 0); else { rle_cnt = 0x80 | (rle_cnt - 1); uart_save_in_buf_and_send(&rle_cnt, 1, 100, 0); uart_save_in_buf_and_send(&pre_data, 1, 100, 0); } } /* 为了提高USB上传效率,我们"凑够一定量的数据后才发送", * 现在都到最后一步了,剩下的数据全部flush、上传 */ uart_save_in_buf_and_send(NULL, 0, 100, 1); 
} 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1488016.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

string使用及模拟

前言 相信看过我博客的小伙伴都已经C的接触已经很久了&#xff0c;也没那么多废话。stl库直接走起&#xff0c;最开始、最简单的就是string。string就相当于是把C语言中的字符串“char[]”给升了级&#xff0c;像是顺序表一样多了记录长度和容量的大小&#xff0c;还加了很多的…

大模型面试:LLM+向量库的文档对话系统

面试题 1.1 为什么大模型需要外挂(向量)知识库&#xff1f;如何将外部知识注入大模型&#xff0c;最直接的方法&#xff1a;利用外部知识对大模型进行微调 回答 大模型需要外挂(向量)知识库的原因&#xff1a; 知识更新频率&#xff1a;大模型在训练时使用的知识是静态的&am…

免杀笔记 -->API的整理Shellcode加密(过DeFender)

最近更新频率明显下降我懒&#xff0c;那么今天就来记录一下我们的一些常用的API的整理以及ShellCode的加密。 1.WinAPI整理 问我为什么要整理&#xff1f; 就是用起来的时候要左翻右翻 &#xff1a;&#xff1a; 烦死了 1.VirtualAlloc VirtualAlloc(NULL,sizeof(buf),MEM_…

人工智能和计算机视觉领域国际学术会议submission

文章目录 1. AAAI 20252. CVPR 20253. ICCV 20254. IJCAI 20255. ICRA 20256. NeurIPS 20257. ACL 20258. ICLR 2025 1. AAAI 2025 人工智能促进协会&#xff08;AAAI&#xff09;是一个成立于1979年的非营利性科学组织&#xff0c;专注于深化对智能行为和思维机制的科学理解&…

通过 WSL 2 在Windows 上挂载 Linux 磁盘

原文查看 曾为了传输或者共享不同系统的文件频繁地在 Windows 和 Linux 系统之间切换&#xff0c;效率过低&#xff0c;所以尝试通过 WSL 2 在Windows 上挂载 Linux 磁盘。 先决条件 需要在Windows 10 2004 及更高版本&#xff08;Build 19041 及更高版本&#xff09;或 Win…

排查一次线程泄漏

背景&#xff1a;最近经常发生K8S健康检查到应用的心跳接口超时不通&#xff0c;然后发生了重启&#xff0c;第一时间进入pod内部使用任何jvm命令都会导致java进程重启&#xff08;也包括arthas工具使用不了&#xff09;&#xff0c;dump不下来&#xff0c;事故现场没法保留&am…

SpringBoot集成Sharding-JDBC实现分库分表

本文已收录于专栏 《中间件合集》 目录 版本介绍背景介绍拆分方式集成并测试1.引入依赖2.创建库和表3.pom文件配置3.编写测试类Entity层Mapper接口MapperXML文件测试类 4.运行结果 自定义分片规则定义分片类编写pom文件 总结提升 版本介绍 SpringBoot的版本是&#xff1a; 2.3.…

电子期刊制作攻略:从零开始,轻松入门

​随着互联网的快速发展&#xff0c;电子期刊已经逐渐成为人们获取信息和娱乐的重要途径。越来越多的人开始关注并投身于电子期刊的制作行业。那么&#xff0c;如何从零开始&#xff0c;轻松入门电子期刊制作呢&#xff1f; 1.首先点击FLBOOK在线制作制作电子杂志平台 2.点击开…

【odoo17】后端py方法触发右上角提示组件

概要 在前面文章中&#xff0c;有介绍过前端触发的通知服务。 【odoo】右上角的提示&#xff08;通知服务&#xff09; 此文章则介绍后端触发方法。 内容 直接上代码&#xff1a;但是前提一定是按钮触发&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; def bu…

无法解析插件 org.apache.maven.plugins:maven-war-plugin:3.2.3(已解决)

文章目录 1、问题出现的背景2、解决方法 1、问题出现的背景 最开始我想把springboot项目转为javaweb项目&#xff0c;然后我点击下面这个插件 就转为javaweb项目了&#xff0c;但是我后悔了&#xff0c;想要还原成springboot项目&#xff0c;点开项目结构关于web的都移除了&am…

HarmonyOS Next 省市区级联(三级联动)筛选框

效果图 完整代码 实例对象 export class ProvinceBean {id?: stringpid?: stringisSelect?: booleandeep?: objectextName?: stringchildren?: ProvinceBean[] }级联代码 import { MMKV } from tencent/mmkv/src/main/ets/utils/MMKV import { ProvinceBean } from ..…

【Git】merge合并分支

两个分支未修改同一个文件的同一处位置: Git自动合并 两个分支修改了同一个文件的同一处位置:产生冲突 例&#xff1a; 在master分支修改了main同时&#xff0c;feat分支也修改了相同的文件 合并的时候就会产生冲突 解决方法: Step1- 手工修改冲突文件&#xff0c;合并冲突内容…

【数据结构】单链表的增删改查

介绍 链表是有序的列表&#xff0c;但是它在内存中是如下存储的&#xff1a; ①链表以节点的方式进行存储&#xff0c;是链式存储的 ②每个节点包含 data 域、next 域&#xff1a;指向下一节点 ③链表的各个节点不一定是连续存放的 ④链表分为有头节点的链表和没有头节点的链表…

比肩 GPT-4o 的 Llama 3.1 本地部署快速体验的方法

比肩 GPT-4o 的 Llama 3.1 本地部署快速体验的方法 flyfish Llama 3.1模型简介 Llama 3.1是一系列大型语言模型&#xff0c;包含以下几种规模&#xff1a; 8B 参数&#xff1a;模型中有80亿个参数 70B 参数&#xff1a;模型中有700亿个参数 405B 参数&#xff1a;模型中有4…

深入理解MySQL锁机制与性能优化:详解记录锁、间隙锁、临键锁及慢SQL查询分析

1. 事务隔离和锁机制详解 记录锁 第一种情况,当我们对于唯一性的索引(包括唯一索引和主键索引)使用等值查询,精准匹配到一条记录的时候,这个时候使用的就是记录锁。 比如 where id = 1 4 7 10。 间隙锁 第二种情况,当我们查询的记录不存在,无论是用等值查询还是范围…

【vue教程】四. Vue 计算属性和侦听器

目录 本章涵盖知识点回顾计算属性&#xff08;Computed&#xff09;创建计算属性计算属性的多样性计算属性的数组过滤计算属性的复杂表达式 计算属性 vs 方法计算属性的实例演示 侦听器&#xff08;Watchers&#xff09;创建侦听器侦听器的高级用法侦听器的深度观察侦听器的立即…

无线遥控控制直流电机和无刷电机开关

一 目的 此文章记录了arduino与陶晶驰串口屏实现联动&#xff0c;点击屏幕双态开关远程控制arduino2560板载的直流电机和无刷电机开关。为手搓乒乓球发球机做准备。 二 接线 2.1串口屏接线 串口屏与Arduino UNO接线如下&#xff1a; ———————————————— RX …

pdf2image:将PDF文档转化为图像的Python魔法

标题&#xff1a;探索pdf2image&#xff1a;将PDF文档转化为图 像的Python魔法 背景 在数字时代&#xff0c;我们经常需要处理各种格式的文档&#xff0c;尤其是PDF文件。PDF以其跨平台的可读性和稳定性而广受欢迎。然而&#xff0c;有时我们需要将PDF文件转换成图像格式&am…

C++ - char*、const char*、char[]、string

const char* const char* 用来定义字符串常量。 char[ ] char型的字符数组是一种定长的数组&#xff0c;存储指定长度的字符序列&#xff0c;数组中的每个元素都是一个char类型的变量&#xff0c;如&#xff1a; char arr[] {h, a, l, l, o, \0}; char c arr[0]; // 访问…

CentOS怎么关闭自动锁屏?

禁止自动锁屏 有时候几分钟不用Centos&#xff0c;系统就自动锁屏了&#xff0c;这是一种安全措施&#xff0c;防止别人趁你不在时使用你的系统。但对于大部分人而言&#xff0c;这是没有必要的&#xff0c;尤其是Centos虚拟机&#xff0c;里面没啥重要的东西&#xff0c;每次…