基于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位的数据,含义如下(bit2~bit5对于group1~4,比如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 汇编指令

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

注: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); 
} 
相关推荐
追梦少年时6 小时前
STM32-Flash闪存
stm32·单片机·嵌入式硬件·51单片机
weixin_452600697 小时前
《青牛科技 GC6125:驱动芯片中的璀璨之星,点亮 IPcamera 和云台控制(替代 BU24025/ROHM)》
人工智能·科技·单片机·嵌入式硬件·新能源充电桩·智能充电枪
weixin_452600698 小时前
【青牛科技】14W 高保真音频放大电路——D2030
科技·单片机·嵌入式硬件·音视频·电动工具·智能电表
小刘同学-很乖15 小时前
MQTT从入门到精通之MQTT Dashboard
spring boot·stm32·物联网·iot
YuCaiH16 小时前
【STM32】USART串口数据包
笔记·stm32·单片机·嵌入式硬件
Kasen's experience18 小时前
STM32 GPIO 配置
stm32·单片机·嵌入式硬件
知行电子-18 小时前
Proteus中数码管动态扫描显示不全(已解决)
单片机·proteus·嵌入式
学习路上_write19 小时前
FPGA/Verilog,Quartus环境下if-else语句和case语句RT视图对比/学习记录
单片机·嵌入式硬件·qt·学习·fpga开发·github·硬件工程
非概念19 小时前
stm32学习笔记----51单片机和stm32单片机的区别
笔记·stm32·单片机·学习·51单片机
jjjxxxhhh12320 小时前
FPGA,使用场景,相比于单片机的优势
单片机·嵌入式硬件·fpga开发