本内容记录于韦东山老师的毕设级开源学习项目,含个人观点,请理性阅读。
个人笔记,没有套路,一步到位,欢迎交流!
00单片机的逻辑分析仪与商业版FPGA的逻辑分析仪异同

对比维度 | 自制STM32逻辑分析仪 | 商业版逻辑分析仪 | |
---|---|---|---|
采样率 | 通常较低(如1MHz,依赖STM32时钟配置) | 极高(可达8GHz或更高) | |
通道数量 | 较少(如8-16通道,受限于STM32引脚与资源) | 多(32-300通道以上,支持多总线同步采集) | |
存储深度 | 较浅(受STM32内存限制,如KB级) | 深(512MB或更大,支持长时间数据捕获) | |
触发功能 | 基础触发(如电平触发、简单逻辑组合) | 复杂触发(支持协议触发、毛刺检测、多条件组合触发) | |
协议支持 | 需依赖软件解码(如PulseView或自定义解析) | 内置多种协议解码(SPI、I2C、UART等),支持自动化分析 | |
实时性 | 依赖CPU处理能力,实时性较差 | 硬件加速处理,实时性高(如FPGA协处理) | |
成本 | 低成本(约数百元,依赖STM32开发板与配件) | 高成本(数万至数十万元) | |
适用场景 | 教育、开发调试、简单信号分析 | 工业级测试、复杂系统调试、高精度协议验证 | |
独立性 | 依赖上位机软件(如USB传输数据) | 独立运行(内置操作系统与存储,无需外接设备) | |
扩展性 | 可通过代码自定义功能,灵活性高 | 功能固化,但支持模块化扩展(如增加探头或协议库) | |
信号保真度 | 可能受STM32采样精度与噪声影响 | 高精度采样(支持电压等级分析,非仅逻辑0/1) |
01 上位机协议分析
1.PulseView++协议分析++
PulseView 作为一款开源逻辑分析仪软件,是属于sigrok开源软件组织的产品,其协议支持和通信机制主要围绕 硬件接口协议 和 软件解码协议 两方面展开。以下是详细分析:
一、硬件通信协议
PulseView 通过 Sigrok 底层库 (libsigrok
)与硬件设备通信,支持多种硬件接口协议,具体包括:
- SUMP协议/OLS( openbench-logic-sniffer**)**
也是韦东山老师的案例。
- FX2LAFW 协议
- 用于基于 CY7C68013A 或 CBM9002A 芯片的逻辑分析仪(如 UINIO-Logic-24MHz、Saleae 克隆版),通过 USB 接口传输数据,需配合 Zadig 工具安装驱动。
- 自定义固件协议
- 支持通过修改硬件固件(如改变 USB 设备标识符)适配不同硬件,例如树莓派 RP2040 板通过 Rust 编译固件实现协议兼容。
二、软件解码协议
PulseView 依赖 Sigrok 解码库 (libsigrokdecode
)实现协议解析,支持以下两类协议:
- 硬件通信协议解码
- 常见协议:包括 I²C、SPI、UART、CAN、SD、1-Wire、IrDA 等 90+ 种协议,覆盖数字信号的波形解析与数据提取。
- 自定义协议:用户可通过 Python 脚本扩展解码功能,例如支持UFCS快充协议等定制化需求。
- 数据文件格式支持
- VCD 文件:支持导入 Verilog 仿真生成的 VCD 波形文件,用于数字设计验证。
- 二进制数据 :通过
sigrok-cli
导出采样数据,支持 CSV、WAV 等格式
2.SUMP协议/OLS( openbench-logic-sniffer**)**
上位机发送的命令和数据有:
其中0x82:
cs#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)
cs
#define CMD_RESET 0x00
#define CMD_ARM_BASIC_TRIGGER 0x01
#define CMD_ID 0x02
#define CMD_METADATA 0x04
#define CMD_FINISH_NOW 0x05 /* extension of Demon Core */
#define CMD_QUERY_INPUT_DATA 0x06 /* extension of Demon Core */
#define CMD_QUERY_CAPTURE_STATE 0x07 /* extension of Demon Core */
#define CMD_RETURN_CAPTURE_DATA 0x08 /* extension of Demon Core */
#define CMD_ARM_ADVANCED_TRIGGER 0x0F /* extension of Demon Core */
#define CMD_XON 0x11
#define CMD_XOFF 0x13
#define CMD_SET_DIVIDER 0x80
#define CMD_CAPTURE_SIZE 0x81
#define CMD_SET_FLAGS 0x82
#define CMD_CAPTURE_DELAYCOUNT 0x83 /* extension of Pepino */
#define CMD_CAPTURE_READCOUNT 0x84 /* extension of Pepino */
#define CMD_SET_ADVANCED_TRIG_SEL 0x9E /* extension of Demon Core */
#define CMD_SET_ADVANCED_TRIG_WRITE 0x9F /* extension of Demon Core */
#define CMD_SET_BASIC_TRIGGER_MASK0 0xC0 /* 4 stages: 0xC0, 0xC4, 0xC8, 0xCC */
#define CMD_SET_BASIC_TRIGGER_VALUE0 0xC1 /* 4 stages: 0xC1, 0xC5, 0xC9, 0xCD */
#define CMD_SET_BASIC_TRIGGER_CONFIG0 0xC2 /* 4 stages: 0xC2, 0xC6, 0xCA, 0xCE */

02 下位机通信分析
1.对上位机的响应(流程分析):

1. 扫描连接设备阶段 

我们使用串口接收中断,不断循环检测接收到的命令或者数据:
cs
while (1)
{
if (uart_recv(&c, TIMEOUT_FOREVER) == 0)
{
cmd_buffer[cmd_index] = c;
switch (cmd_buffer[0])
{
-
连接设备时 :上位机(PulseView)会先发送复位命令(CMD_RESET) (命令码
0x00
),用于初始化下位机状态并清除缓存cscase CMD_RESET: { break; }
-
应答回复: 上位机(PulseView)会先发送CMD_ID 命令("0x02"),下位机要回复 4 个字节"1ALS"。
cscase CMD_ID: { /* 上报4个字节"1ALS" */ uart_send((uint8_t *)"1ALS", 4, TIMEOUT_DEFAULT); break; }
-
设备识别 :随后发送查询设备信息命令 (命令码
0x04
),要求下位机返回设备固件版本、支持的通道数和最大采样率等信息。获取下位机的默认配置(如内存大小、支持的触发模式),
下位机要回复的参数格式为"1 字节的数据类别,多个字节的数据",说明如下:
cscase CMD_METADATA: { uint32_t virtual_bufferSize = getBufferSize(); uint32_t maxFrequency = getMaxFrequency(); /* 上报参数 */ // 一个字节的发送,且最大等待时间TIMEOUT_DEFAULT=100ms //NAME send_byte(0x01, TIMEOUT_DEFAULT); send_string("100ASK_LogicalNucleo", TIMEOUT_DEFAULT); send_byte(0x00, TIMEOUT_DEFAULT); //SAMPLE MEM send_byte(0x21, TIMEOUT_DEFAULT); send_uint32(virtual_bufferSize, TIMEOUT_DEFAULT); //DYNAMIC MEM send_byte(0x22, TIMEOUT_DEFAULT); send_uint32(0, TIMEOUT_DEFAULT); //SAMPLE RATE send_byte(0x23, TIMEOUT_DEFAULT); send_uint32(maxFrequency, TIMEOUT_DEFAULT); //Number of Probes send_byte(0x40, TIMEOUT_DEFAULT); send_byte(8, TIMEOUT_DEFAULT); //Protocol Version send_byte(0x41, TIMEOUT_DEFAULT); send_byte(0x02, TIMEOUT_DEFAULT); //END send_byte(0x00, TIMEOUT_DEFAULT); break; } //大端序处理:将32位整数拆分为4字节,按高位优先(MSB first)传输 static int send_uint32(uint32_t val, int timeout) { uint8_t buffer[4]; // 用于存储整数的各个字节 buffer[3] = BYTE0(val); buffer[2] = BYTE1(val); buffer[1] = BYTE2(val); buffer[0] = BYTE3(val); uart_send(buffer, 4, timeout); return 0; } //为什么要这么处理呢? /* 1.大端序转换,统一字节顺序避免端序歧义(无论内存大端小端) 2.强制4字节长度避免粘包问题 */ static int send_string(char *str, int timeout) { return uart_send((uint8_t *)str, strlen(str), timeout); }
2. 参数配置阶段 

触发配置 ,上位机发送触发掩码和值,定义触发信号的逻辑条件:
-
使能触发 :通过
0xC0
命令设置通道使能cscase CMD_SET_BASIC_TRIGGER_MASK0: { cmd_index++; if(cmd_index < 5)//需要 5字节数据(1字节命令 + 4字节掩码),小于5,则跳过后 //续代码(continue),继续接收数据。当 cmd_index 达到5时,说明已接收完整数据。 continue; setTriggerMask(*(uint32_t *)(cmd_buffer + 1));//取首地址+1及后32位 break; }
-
触发条件 :通过
0xC1
系列命令设置触发值(如高\低电平触发)cscase CMD_SET_BASIC_TRIGGER_VALUE0: { cmd_index++; if(cmd_index < 5) continue; setTriggerValue(*(uint32_t *)(cmd_buffer + 1)); break; }
-
通道使能 :通过
0xC2
命令启动触发(如0x00 0x00 0x000x08中最后一个字节的bit3=1
表示启动触发)cscase CMD_SET_BASIC_TRIGGER_CONFIG0: { cmd_index++; if(cmd_index < 5) continue; uint8_t serial = (*((uint8_t*)(cmd_buffer + 4)) & 0x04) > 0 ? 1 : 0; uint8_t state = (*((uint8_t*)(cmd_buffer + 4)) & 0x08) > 0 ? 1 : 0; if(serial == 1) setTriggerState(0);//Not supported else setTriggerState(state); break; }
设置采样参数 ,上位机通过命令组配置以下参数:
采样次数与采样前舍去次数设置:
-
采样率 :通过
0x80
命令是根据设置的采样频率算出分频系数,例如X
=(100Mhz/200Khz)-1 》可得X=499》》0xf3 0x01 0x00 0x00
表示0000 01f3=499(其中采样频率<100Mhz时按100Mhz算 >100Mhz时按200Mhz算)cscase CMD_SET_DIVIDER: { cmd_index++; if(cmd_index < 5) continue; uint32_t divider = *((uint32_t *)(cmd_buffer + 1)); setSamplingDivider(divider); break; } static void setSamplingDivider (uint32_t divider) { int f = 100000000 / (divider + 1); if(f > MAX_FREQUENCY) f = MAX_FREQUENCY; g_samplingRate = f; }
当下位机buffer超过256k,采样次数与采样前舍去次数分开下发

-
采样次数: 通过
0x84
命令+32位数据表示 -
采样前舍去次数(延时次数): 通过
0x83
命令+32位数表示
cscase CMD_CAPTURE_DELAYCOUNT://83 { cmd_index++; if(cmd_index < 5) continue; uint32_t delayCount = *((uint32_t*)(cmd_buffer + 1)); setSamplingDelay(4 * delayCount); break; } case CMD_CAPTURE_READCOUNT://84 { cmd_index++; if(cmd_index < 5) continue; uint32_t readCount = *((uint32_t*)(cmd_buffer + 1)); setSampleNumber(4 * readCount); break; }
当下位机buffer小于256k,前两位表示采样次数后两位表示采样前舍去次数
-
采样次数和延时次数: 通过
0x81
命令+32位数表示cscase CMD_CAPTURE_SIZE: { cmd_index++; if(cmd_index < 5) continue; uint16_t readCount = *((uint16_t*)(cmd_buffer + 1)); uint16_t delayCount = * ((uint16_t*)(cmd_buffer + 3)); setSampleNumber(4 * readCount); setSamplingDelay(4 * delayCount); break; }
-
通道使能 :通过
0x82
命令启用/禁用特定通道cscase CMD_SET_FLAGS: { cmd_index++; if(cmd_index < 5) continue; setFlags(*(uint32_t *)(cmd_buffer + 1)); break; }
3. 触发与数据采集阶段 

-
启动采集 :发送运行命令CMD_ARM_BASIC_TRIGGER (命令码
0x01
),下位机开始采样,采样结束后上报数据。下位机要注意接收停止命令CMD_XOFF (命令码0x13)cscase CMD_ARM_BASIC_TRIGGER: { run(); break; } case CMD_XOFF: { //stop (); break; } static void run (void) { /* 采集数据 */ start(); /* 上报数据 */ upload(); }
协议可有的功能:
- 触发事件处理:
- 当触发条件满足时,下位机停止采样并缓存触发点前后的数据(预触发和触发后数据)
- 上位机通过读取状态命令(命令码
0x20
)轮询下位机状态,直到收到触发完成标志
- 数据回传 :上位机发送读取数据命令(命令码
0x20
)*,下位机按SUMP协议格式返回二进制数据块(包含时间戳和通道状态)*
注意:数据格式与传输特点
- 数据块结构 :SUMP协议要求数据以32位小端存储格式 传输(最大支持 32 个采样通道),每个采样点按通道顺序打包(例如通道0对应最低位),与STM32的变量存储方式一样,还有串口通信、USB协议都是规定低位先行......最后是数组元素在内存中是连续存储 的,且地址从低到高依次排列。
所以(uint32_t *)buff[1]一到四字节直接可用且分别对应 group1 的channel 0~7,group2、3、4等同理。- 传输顺序:它上报的数据是:先上报最后一个采样的数据,最后上报第 1 个采样点的数据。
- 数据流控制:上位机可能分批次请求数据(通过分段读取命令),避免单次传输过大导致缓冲区溢出
2.娱乐部分:
可以自行使用逻辑分析仪监控USB模拟的串口通信(用另一台PulseView检测),观察命令码和数据是否符合SUMP协议规范
注意USB模拟的串口在48Mhz以上的采样频率下会比较清晰

03 软件实现与性能压榨
1.采样频率优化------使用汇编语句
测量读 GPIO 操作、读写 buffer、NOP 指令的时间、逻辑右移、加法操作的时间测量方法类似如下(使用汇编语句):
结论:循环一次耗时 44+24+16+23=107ns,理论上最高的采样频率=1/107ns=9MHz。
测量处理 Tick 中断函数的时间:

汇编优化数据采集
cs
BUFFER_SIZE equ 3100 ; 注意这个数值要跟logicanalyzer.c中的BUFFER_SIZE保持一致
; 声明后续代码使用 Thumb 指令集
THUMB ;16 位指令集_大多数 Cortex-M 芯片默认使用 Thumb 模式
AREA |.text|, CODE, READONLY ;汇编伪指令_|.text|为标准代码段名------CODE 表示这是一个代码段,READONLY 表示只读
; sample_function handler
sample_function PROC ; 函数开始标记
EXPORT sample_function ;相当于C语言中的 extern 声明,但方向相反(这里是汇编导出给外部使用)
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} ; 函数入口保存寄存器------LR(Link Register)存储了函数执行完毕后应返回的地址
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 ;GPIOB_IDR地址用于读取PB8-PB15引脚状态
; 设置PA15的值备用
LDR R11, =0X40010810
LDR R12, =(1<<15)
LDR LR, =(1<<31)
Loop
; 设置PA15输出高电平
STR R12, [R11]
;read data
LDRH R7, [R6] ; 读GPIOB_IDR
LSR R7, #8 ; data = (*data_reg) >> 8;
CMP R7, R8 ;条件执行__与ADDNE条件码NE关联
ADDNE R2, #1 ; g_cur_pos += (data != pre_data)? 1 : 0;
STRB R7, [R0, R2] ; g_rxdata_buf[g_cur_pos] = data; ;目标地址 = R0 + R2
MOV R8, R7 ; pre_data = data
LDR R7, [R1, R2, LSL #2] ; R7 = g_rxcnt_buf[g_cur_pos] ;乘以4(即左移2位);目标地址 = 数组基地址(R1) + 下标(R2) * 元素大小(4)
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 ;基于零标志位(Z)判断相等或不等
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} ; 函数出口恢复寄存器;为什么用 PC 而不是 LR_因为 LR 可能在函数内部被修改(例如嵌套调用),而栈中保存的是原始的返回地址
ENDP ; 函数结束标记
2.内存保存采样数据优化
在有限的内存里,我们需要提高内存的使用效率:不变的数据就不要保存了。新方案
如下:
① 定义两个数组:uint8_t data_buf[BUFFER_SIZE]、uint8_t cnt_buf[BUFFER_SIZE]
① 以比较高的、频率周期性地读取 GPIO 的值
② 只有 GPIO 值发生变化了,才存入 data_buf[i++];GPIO 值无变化时,cnt_buf[i-1]累 加
③ 以后,根据 data_buf、cnt_buf 恢复各个采样点的数据,上报给上位机
cs
/* 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;
3.USB串口传输优化

优势或特点
-
++使用环形缓冲区+DMA持续写入数据++,不阻塞硬件接收。主程序按需读取数据,无需实时响应每个字节。缓冲区未满时,新数据持续写入;缓冲区满时,也可触发溢出处理。内存固定、无拷贝开销------天然的多任务运行!
csstatic 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 */ }
-
下位机想通过 USB 口发送数据时,要确保上次传输完成
cs
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);
}
硬件问题:
cs
/**********************************************************************
* 函数名称: 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;
}
}
4.使用 RLE 提升重复数据的传输效率

1.RLE规则(SUMP 协议里规定)
编码规则 :
- 长度字段 :最高位为1,低7位表示
(重复次数-1)
(如重复10次编码为0x89
)。 - 数据字段 :最高位置0(
data = g_rxdata_buf[i] & ~0x80
),避免与长度字段冲突。
2.代码实现
cs
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;
}
......................................................................................................
/* 发送最后的数据
*因为可能数据遍历完了,但没有达到发送数据的条件:(pre_data != data)、(rle_cnt == 128),
*即有有>=1个以上的数据没有上传
*/
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);
03 代码实现
1.解析命令
cs
void LogicalAnalyzerTask(void)
{
uint8_t cmd_buffer[5];
uint8_t cmd_index = 0;
uint8_t c;
while (1)
{
if (uart_recv(&c, TIMEOUT_FOREVER) == 0)
{
cmd_buffer[cmd_index] = c;
switch (cmd_buffer[0])
{
case CMD_RESET://00
{
break;
}
case CMD_ID://02
{
/* 上报4个字节"1ALS" */
uart_send((uint8_t *)"1ALS", 4, TIMEOUT_DEFAULT);
break;
}
case CMD_METADATA://04
{
uint32_t virtual_bufferSize = getBufferSize();
uint32_t maxFrequency = getMaxFrequency();
/* 上报参数 */
//NAME
send_byte(0x01, TIMEOUT_DEFAULT);
send_string("100ASK_LogicalNucleo", TIMEOUT_DEFAULT);
send_byte(0x00, TIMEOUT_DEFAULT);
//SAMPLE MEM
send_byte(0x21, TIMEOUT_DEFAULT);
send_uint32(virtual_bufferSize, TIMEOUT_DEFAULT);
//DYNAMIC MEM
send_byte(0x22, TIMEOUT_DEFAULT);
send_uint32(0, TIMEOUT_DEFAULT);
//SAMPLE RATE
send_byte(0x23, TIMEOUT_DEFAULT);
send_uint32(maxFrequency, TIMEOUT_DEFAULT);
//Number of Probes
send_byte(0x40, TIMEOUT_DEFAULT);
send_byte(8, TIMEOUT_DEFAULT);
//Protocol Version
send_byte(0x41, TIMEOUT_DEFAULT);
send_byte(0x02, TIMEOUT_DEFAULT);
//END
send_byte(0x00, TIMEOUT_DEFAULT);
break;
}
case CMD_ARM_BASIC_TRIGGER://01
{
run();
break;
}
case CMD_XON:
{
//start();
break;
}
case CMD_XOFF:
{
//stop ();
break;
}
case CMD_CAPTURE_SIZE://81
{
cmd_index++;
if(cmd_index < 5)
continue;
uint16_t readCount = *((uint16_t*)(cmd_buffer + 1));
uint16_t delayCount = * ((uint16_t*)(cmd_buffer + 3));
setSampleNumber(4 * readCount);
setSamplingDelay(4 * delayCount);
break;
}
case CMD_SET_DIVIDER: //80
{
cmd_index++;
if(cmd_index < 5)
continue;
uint32_t divider = *((uint32_t *)(cmd_buffer + 1));
setSamplingDivider(divider);
break;
}
case CMD_SET_BASIC_TRIGGER_MASK0://c0
{
cmd_index++;
if(cmd_index < 5)
continue;
setTriggerMask(*(uint32_t *)(cmd_buffer + 1));
break;
}
case CMD_SET_BASIC_TRIGGER_VALUE0://c1
{
cmd_index++;
if(cmd_index < 5)
continue;
setTriggerValue(*(uint32_t *)(cmd_buffer + 1));
break;
}
case CMD_SET_BASIC_TRIGGER_CONFIG0://c2
{
cmd_index++;
if(cmd_index < 5)
continue;
uint8_t serial = (*((uint8_t*)(cmd_buffer + 4)) & 0x04) > 0 ? 1 : 0;
uint8_t state = (*((uint8_t*)(cmd_buffer + 4)) & 0x08) > 0 ? 1 : 0;
if(serial == 1)
setTriggerState(0);//Not supported
else
setTriggerState(state);
break;
}
case CMD_SET_FLAGS://82
{
cmd_index++;
if(cmd_index < 5)
continue;
setFlags(*(uint32_t *)(cmd_buffer + 1));
break;
}
case CMD_CAPTURE_DELAYCOUNT://83
{
cmd_index++;
if(cmd_index < 5)
continue;
uint32_t delayCount = *((uint32_t*)(cmd_buffer + 1));
setSamplingDelay(4 * delayCount);
break;
}
case CMD_CAPTURE_READCOUNT://84
{
cmd_index++;
if(cmd_index < 5)
continue;
uint32_t readCount = *((uint32_t*)(cmd_buffer + 1));
setSampleNumber(4 * readCount);
break;
}
default:
{
}
}
cmd_index = 0;
memset(cmd_buffer, 0, sizeof(cmd_buffer));//清除原先buff
}
}
}
**2.**采集数据
- 必须严格按照设定的采样频率采集信号,确保时间分辨率(如1MHz采样率对应1μs时间精度)。
- 时序准确性是逻辑分析仪的核心指标,直接影响信号分析的可靠性
① 禁止中断:这是为了在采集数据时以最快的频率采集,不让中断干扰。
除了串口中断之外,其他中断都禁止。下位机只有 tick 中断、串口中断,所以只需要
禁止 tick 中断。
保留串口中断的原因在于:上位机可能发来命令停止采样。
② 等待触发条件:用户可能设置触发采样的条件
③ 触发条件满足后,延时一会:没有必要
④ 循环:以最高频率采样
退出的条件有三:收到上位机发来的停止命令、采集完毕、数据 buffer 已经满
⑤ 恢复中断
cs
static void start (void)
{
extern void sample_function();
uint8_t data;
uint8_t pre_data;
volatile uint16_t *data_reg = (volatile uint16_t *)0x40010C08; /* GPIOB_IDR用于读取PB8-PB15引脚状态。 */
volatile uint32_t *pa15_reg = (volatile uint32_t *)0X40010810; /* GPIOA_BSRR通过PA15引脚输出信号 */
//计算实际需要采集的样本数,因为我们直接使用max采样f运行
//上位机次数*(MAX采样f/)
g_convreted_sample_count = g_sampleNumber * (MAX_FREQUENCY / g_samplingRate);
get_stop_cmd = 0;//用户停止标志,初始化为0
//清空数据缓冲区位置和采样计数器。
g_cur_pos = 0;
g_cur_sample_cnt = 0;
(void)pre_data;
(void)pa15_reg;
/* 1. 除了串口中断,其他中断都禁止 */
Disable_TickIRQ();//关闭系统定时器中断(如SysTick),防止中断干扰实时采样。
memset(g_rxcnt_buf, 0, sizeof(g_rxcnt_buf));//清空计数缓冲区
/* 2. 等待触发条件 */
if (g_triggerState && g_triggerMask)//判读上位机设置的端口是否开启
{
while (1)//监测GPIO引脚状态,当满足预设的触发条件(高/低电平)时退出等待
{
data = (*data_reg) >> 8;
/* 有没有期待的高电平? */
if (data & g_triggerMask & g_triggerValue)
break;
/* 有没有期待的低电平? */
if (~data & g_triggerMask & ~g_triggerValue)
break;
/* 用户选择停止? */
if (get_stop_cmd)//若用户发送停止信号(get_stop_cmd=1),立即退出函数。
return;
}
}
/* 3. 这里可以延时g_sampleDelay个采样周期,但是没有必要 */
(void)g_sampleDelay;//没有用到
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//1Mhz运行
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输出低电平 */
}
#endif
/* 5. 使能被禁止的中断 */
Enable_TickIRQ();
}
**3.**上报数据
- 无需严格实时传输:上报频率可以与采样频率解耦,但需保证数据顺序和时序信息完整。
- 关键要求 :
- 数据顺序正确(先采样的数据先上报)。
- 每个数据点的时间戳或等效时间信息可被上位机还原(如通过固定采样率计算时间偏移)
采集数据时是以最大频率采集的,比如以 1MHz 采集。如果上位机要求的采样频率是
200KHz:1MHz/200KHz=5,采集到的数据量是上报数据量的 5 倍。我们只需要每隔 5 个数据
上报一个即可。
cs
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--)//外层循环遍历数据位置(i从g_cur_pos递减到0)
{
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;
}
}
}
/* 发送最后的数据
*因为可能数据遍历完了,但没有达到发送数据的条件:(pre_data != data)、(rle_cnt == 128),
*即有有>=1个以上的数据没有上传
*/
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);
}
细节:"幕后工作"
已经实现:
1.USB上传缓存cnt清0(收尾工作)
cs
uart_save_in_buf_and_send(NULL, 0, 100, 1);
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;
}
}
2.清除样本缓存(开幕工作)
在static void start (void)中已经实现:
cs
//清空数据缓冲区位置和采样计数器。
g_cur_pos = 0;
g_cur_sample_cnt = 0;
memset(g_rxcnt_buf, 0, sizeof(g_rxcnt_buf));//清空计数缓冲区
至于其他大多采取覆盖的形式:
比如:(在上传时我们也是根据索引变量采取后进先出的形式)
uint8_t g_rxdata_buf[BUFFER_SIZE];
还有USB串口用到的环形缓冲区......
这么一来下面这段自以为是改进的代码就显得赘述了
cs
case CMD_RESET://00
{
reset_globals(); // 调用全局变量重置函数
break;
}
void reset_globals() {
g_cur_pos = 0;
g_cur_sample_cnt = 0;
memset(g_rxdata_buf, 0, BUFFER_SIZE); // 可选:清空缓冲区
memset(g_rxcnt_buf, 0, BUFFER_SIZE * sizeof(uint32_t));
}
不得不惊叹到韦东山老师团队的厉害和大义!感谢他们为嵌入式人才培养做出的贡献!!!
04 到此,项目基本功能已经掌握!下一篇,我们来实现"单片机逻辑分析仪"的扩展功能。未完待续~
一首童年最燃的《再飞行》送给你