基于STM32的逻辑分析仪

文章目录

  • 一、逻辑分析仪体验
    • 1、使用示例
      • [1.1 逻辑分析仪](#1.1 逻辑分析仪)
      • [1.2 开源软件PulseView](#1.2 开源软件PulseView)
    • 2、核心技术
      • [2.1 技术方案](#2.1 技术方案)
      • [2.2 信号采集与存储](#2.2 信号采集与存储)
      • [2.3 数据上传](#2.3 数据上传)
    • 3、使用逻辑分析仪
    • [4、 SourceInsight 使用技巧](#4、 SourceInsight 使用技巧)
      • 4.1新建工程
      • [4.2 设置工程名及工程数据目录](#4.2 设置工程名及工程数据目录)
      • [4.3 指定源码目录](#4.3 指定源码目录)
      • [4.4 添加源码](#4.4 添加源码)
      • [4.5 同步文件](#4.5 同步文件)
      • [4.6 操作示例](#4.6 操作示例)
        • [4.6.1 在工程中打开文件](#4.6.1 在工程中打开文件)
        • [4.6.2 在文件中查看函数或变量的定义](#4.6.2 在文件中查看函数或变量的定义)
        • [4.6.3 查找函数或变量的引用](#4.6.3 查找函数或变量的引用)
        • [4.6.4 其他快捷键](#4.6.4 其他快捷键)
  • 二、逻辑分析仪SUMP协议分析
    • 1、硬件结构
    • 2、SUMP协议
      • [2.1 上位机发出的命令及参数](#2.1 上位机发出的命令及参数)
      • [2.2 下位机发送的回复](#2.2 下位机发送的回复)
        • [2.2.1 CMD_ID 的回复](#2.2.1 CMD_ID 的回复)
        • [2.2.2 CMD_METADATA 命令的回复](#2.2.2 CMD_METADATA 命令的回复)
        • [2.2.3 上报的采样数据](#2.2.3 上报的采样数据)
      • [2.3 上位机和下位机时序图](#2.3 上位机和下位机时序图)
  • 三、实现逻辑分析仪
    • 1、软件设计方案
      • [1.1 如何实现最高的采样率](#1.1 如何实现最高的采样率)
      • [1.2 汇编指令](#1.2 汇编指令)
      • [1.3 精确测量时间](#1.3 精确测量时间)
        • [1.3.1 测量读GPIO操作的时间](#1.3.1 测量读GPIO操作的时间)
        • [1.3.2 测量读写buffer的时间](#1.3.2 测量读写buffer的时间)
        • [1.3.3 测量NOP指令的时间](#1.3.3 测量NOP指令的时间)
        • [1.3.4 逻辑右移](#1.3.4 逻辑右移)
        • [1.3.5 加法操作](#1.3.5 加法操作)
        • [1.3.6 测量处理Tick中断函数的时间](#1.3.6 测量处理Tick中断函数的时间)
    • 2、实现功能
      • [2.1 方案修订](#2.1 方案修订)
      • [2.2 编写程序](#2.2 编写程序)
        • [2.2.1 采集数据](#2.2.1 采集数据)
        • [2.2.2 上报数据](#2.2.2 上报数据)
      • [2.3 上机演示](#2.3 上机演示)
    • 3、改进功能
      • [3.1 使用说明](#3.1 使用说明)
      • [3.2 最终程序的结构](#3.2 最终程序的结构)
      • [3.3 提高采样率](#3.3 提高采样率)
      • [3.4 增加改进USB串口功能](#3.4 增加改进USB串口功能)
        • [3.4.1 在STM32CubeMX 增加USB 串口功能](#3.4.1 在STM32CubeMX 增加USB 串口功能)
        • [3.4.2 USB 串口收发函数改造](#3.4.2 USB 串口收发函数改造)
        • [3.4.3 提高USB串口发送效率](#3.4.3 提高USB串口发送效率)
      • [3.5 使用RLE提升重复数据的传输效率](#3.5 使用RLE提升重复数据的传输效率)
        • [3.5.1 RLE功能](#3.5.1 RLE功能)
        • [3.5.2 上位机使能RLE](#3.5.2 上位机使能RLE)
        • [3.5.3 代码解读](#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_RESET 0x00 复位下位机
CMD_ID 0x02 让下位机上报ID
CMD_METADATA 0x04 让下位机上报参数
CMD_SET_BASIC_TRIGGER_MASK0 0xC0 使能某个通道的触发功能 示例数值:0x01 0x02 0x00 0x00 表示channel0, 9使能了触发功能
CMD_SET_BASIC_TRIGGER_VALUE0 0xC1 设置通道的触发值 示例数值:0x01 0x00 0x00 0x00 表示channel 0的触发值为高电平 channel 9的触发值为低电平
CMD_SET_BASIC_TRIGGER_CONFIG0 0xC2 最后一个字节的bit3为1表示启动触发功能 示例数值:0x00 0x00 0x00 0x08 最后一个字节的bit3表示启动触发 功能
CMD_SET_DIVIDER 0x80 根据用户设置的采样频率计算出分频系数 注意: 当采样频率大于100MHz时, 会"Enable demux mode", 让逻辑分析工作于200MHz, 分频系数=200MHz/采样频率 - 1 当采样频率小于100MHz时, 分频系数=100MHz/采样频率 -1 示例数值:0xf3 0x01 0x00 0x00 分配系数为: 0x01f3=499=100MHz/200KHz-1
CMD_CAPTURE_SIZE 0x81 使用1个命令发送READCOUNT DELAYCOUNT两个参数 示例数值:0x0c 0x00 0x0c 0x00 前2字节表示要采样的次数为 0x0c * 4 = 48 后2字节表示要延迟的次数为 0x0c * 4 = 48
CMD_SET_FLAGS 0x82 设置flag, 比如使用启动demux模式 根据用户选择的通道,使能group (见后面注释)
CMD_CAPTURE_DELAYCOUNT 0x83 示满足触发条件开始采样后, 延迟多少次采样,才保存数据 示例数值:0x0c 0x00 0x00 0x00 表示延迟次数为 0x0c * 4 = 48
CMD_CAPTURE_READCOUNT 0x84 表示要采样的次数 示例数值: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字节 协议版本
0x40 1 字节 最大采样通道数
0x41 1 字节 协议版本
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 如何实现最高的采样率

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

c 复制代码
// 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 指令 NOP 15ns
逻辑右移 LSR R0, #8 24ns
累加 ADD R0, #1 23ns
Tick 中断处理 10ns

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

示例代码如下:

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

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

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

1.3.1 测量读GPIO操作的时间

相关寄存器位置如下:

在汇编中引脚配置如下:

测试函数的代码为:

c 复制代码
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的代码为:

c 复制代码
				THUMB
                AREA    |.text|, CODE, READONLY

; asm_measure handler
asm_measure    PROC
                 EXPORT  asm_measure
    
	; 设置PA15输出高电平
    ; 使用汇编让PA15输出高电平供测量
	LDR  R1, =0x40010810
	LDRH  R0,= [1 << 15]
	STR  R0,  [R1]

    ; 读GPIO 手写100条指令
    LDR  R1, =0x40010c08
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR    
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR    
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR    
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR
    LDRH  R0, [R1]  ; 读GPIOB_IDR

	; 设置PA15输出低电平
    ; 使用汇编让PA15输出低电平供测量
	LDR  R1, =0x40010810
	LDRH  R0,= [1 << 31]
	STR  R0,  [R1]

	BX  LR  ; 返回
	
                 ENDP

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

1.3.2 测量读写buffer的时间

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

c 复制代码
				 THUMB
				 AREA	 |.text|, CODE, READONLY

; asm_measure_r_mem handler
asm_measure_r_mem	PROC
				  EXPORT  asm_measure_r_mem
	 ; 设置PA15输出高电平
	 LDR  R1, =0x40010810
	 LDR  R0, =(1<<15)
	 STR  R0, [R1]

	 LDR  R1, =0x20000000
	 LDRH  R0, [R1]  ; 读内存(100次)
         
     ; 设置PA15输出低电平
	 LDR  R1, =0x40010810
	 LDR  R0,= (1 << 31)
	 STR  R0,  [R1]

	 BX  LR
         		ENDP

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

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

c 复制代码
				 THUMB
				 AREA	 |.text|, CODE, READONLY

; asm_measure_r_mem handler
asm_measure_r_mem	PROC
				  EXPORT  asm_measure_r_mem
	 ; 设置PA15输出高电平
	 LDR  R1, =0x40010810
	 LDR  R0, =(1<<15)
	 STR  R0, [R1]

     LDR	R1, =0x20000000
	 STRB  R0, [R1]	; 写内存(100次)
         
     ; 设置PA15输出低电平
	 LDR  R1, =0x40010810
	 LDR  R0,= (1 << 31)
	 STR  R0,  [R1]

	 BX  LR
         		ENDP

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

1.3.3 测量NOP指令的时间

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

c 复制代码
				 THUMB
				 AREA	 |.text|, CODE, READONLY

; asm_measure_r_mem handler
asm_measure_r_mem	PROC
				  EXPORT  asm_measure_r_mem
	 ; 设置PA15输出高电平
	 LDR  R1, =0x40010810
	 LDR  R0, =(1<<15)
	 STR  R0, [R1]

	 NOP(写100次)
         
     ; 设置PA15输出低电平
	 LDR  R1, =0x40010810
	 LDR  R0,= (1 << 31)
	 STR  R0,  [R1]

	 BX  LR
         		ENDP

执行100次NOP指令,耗时:1.49us;执行1次NOP指令耗时约为15ns。

1.3.4 逻辑右移

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

c 复制代码
				 THUMB
				 AREA	 |.text|, CODE, READONLY

; asm_measure_r_mem handler
asm_measure_r_mem	PROC
				  EXPORT  asm_measure_r_mem
	 ; 设置PA15输出高电平
	 LDR  R1, =0x40010810
	 LDR  R0, =(1<<15)
	 STR  R0, [R1]

	 LSR R0, #8(写100次)
         
     ; 设置PA15输出低电平
	 LDR  R1, =0x40010810
	 LDR  R0,= (1 << 31)
	 STR  R0,  [R1]

	 BX  LR
         		ENDP

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

1.3.5 加法操作

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

c 复制代码
				 THUMB
				 AREA	 |.text|, CODE, READONLY

; asm_measure_r_mem handler
asm_measure_r_mem	PROC
				  EXPORT  asm_measure_r_mem
	 ; 设置PA15输出高电平
	 LDR  R1, =0x40010810
	 LDR  R0, =(1<<15)
	 STR  R0, [R1]

	 ADD R0, #1(写100次)
         
     ; 设置PA15输出低电平
	 LDR  R1, =0x40010810
	 LDR  R0,= (1 << 31)
	 STR  R0,  [R1]

	 BX  LR
         		ENDP

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

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

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

c 复制代码
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();
}


测量开中断情况下,同一段代码的执行时间:

c 复制代码
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 指令 NOP 15ns
逻辑右移 LSR R0, #8 24ns
累加 ADD R0, #1 23ns
Tick 中断处理 10us

对于如下代码:

c 复制代码
// 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已经满

⑤ 恢复中断

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

c 复制代码
/* 记录第一个数据 */
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个数据 上报一个即可。

c 复制代码
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"中,如下:

c 复制代码
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口发送数据时,使用如下函数:

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

代码如下:

c 复制代码
/** 
  * @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字节。

代码如下:

c 复制代码
/********************************************************************** 
* 函数名称: 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 代码解读

代码如下:

c 复制代码
/********************************************************************** 
 * 函数名称: 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); 
} 
相关推荐
-Springer-20 分钟前
STM32 学习 —— 个人学习笔记5(EXTI 外部中断 & 对射式红外传感器及旋转编码器计数)
笔记·stm32·学习
LS_learner38 分钟前
树莓派(ARM64 架构)Ubuntu 24.04 (Noble) 系统 `apt update` 报错解决方案
嵌入式硬件
来自晴朗的明天1 小时前
16、电压跟随器(缓冲器)电路
单片机·嵌入式硬件·硬件工程
钰珠AIOT2 小时前
在同一块电路板上同时存在 0805 0603 不同的封装有什么利弊?
嵌入式硬件
代码游侠2 小时前
复习——Linux设备驱动开发笔记
linux·arm开发·驱动开发·笔记·嵌入式硬件·架构
代码游侠13 小时前
学习笔记——设备树基础
linux·运维·开发语言·单片机·算法
xuxg200515 小时前
4G 模组 AT 命令解析框架课程正式发布
stm32·嵌入式·at命令解析框架
CODECOLLECT16 小时前
京元 I62D Windows PDA 技术拆解:Windows 10 IoT 兼容 + 硬解码模块,如何降低工业软件迁移成本?
stm32·单片机·嵌入式硬件
BackCatK Chen17 小时前
STM32+FreeRTOS:嵌入式开发的黄金搭档,未来十年就靠它了!
stm32·单片机·嵌入式硬件·freertos·低功耗·rtdbs·工业控制