文章目录
- 一、逻辑分析仪体验
- 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_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 如何实现最高的采样率
逻辑分析仪采样数据的示例代码如下:
// 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指令执行的时间。
示例代码如下:
// 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 指令 | NOP | 15ns |
逻辑右移 | LSR R0, #8 | 24ns |
累加 | ADD R0, #1 | 23ns |
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);
}