001 使用单片机实现的逻辑分析仪——吸收篇

本内容记录于韦东山老师的毕设级开源学习项目,含个人观点,请理性阅读。

个人笔记,没有套路,一步到位,欢迎交流!

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)与硬件设备通信,支持多种硬件接口协议,具体包括:

  1. SUMP协议/OLS( openbench-logic-sniffer**)**

也是韦东山老师的案例。

  1. FX2LAFW 协议
    • 用于基于 CY7C68013A 或 CBM9002A 芯片的逻辑分析仪(如 UINIO-Logic-24MHz、Saleae 克隆版),通过 USB 接口传输数据,需配合 Zadig 工具安装驱动。
  2. 自定义固件协议
    • 支持通过修改硬件固件(如改变 USB 设备标识符)适配不同硬件,例如树莓派 RP2040 板通过 Rust 编译固件实现协议兼容。

二、软件解码协议

PulseView 依赖 Sigrok 解码库libsigrokdecode)实现协议解析,支持以下两类协议:

  1. 硬件通信协议解码
    • 常见协议:包括 I²C、SPI、UART、CAN、SD、1-Wire、IrDA 等 90+ 种协议,覆盖数字信号的波形解析与数据提取。
    • 自定义协议:用户可通过 Python 脚本扩展解码功能,例如支持UFCS快充协议等定制化需求。
  2. 数据文件格式支持
    • 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),用于初始化下位机状态并清除缓存

    cs 复制代码
                    case CMD_RESET:
                    {
                        break;
                    }
  • 应答回复: 上位机(PulseView)会先发送CMD_ID 命令("0x02"),下位机要回复 4 个字节"1ALS"。

    cs 复制代码
                    case CMD_ID:
                    {
                        /* 上报4个字节"1ALS" */                
                        uart_send((uint8_t *)"1ALS", 4, TIMEOUT_DEFAULT);
                        break;
                    }
  • 设备识别 :随后发送查询设备信息命令 (命令码0x04),要求下位机返回设备固件版本、支持的通道数和最大采样率等信息。获取下位机的默认配置(如内存大小、支持的触发模式),
    下位机要回复的参数格式为"1 字节的数据类别,多个字节的数据",说明如下:

    cs 复制代码
                    case 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命令设置通道使能

    cs 复制代码
    				case 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系列命令设置触发值(如高\低电平触发)

    cs 复制代码
    				case CMD_SET_BASIC_TRIGGER_VALUE0:
    				{
    					cmd_index++;
    					if(cmd_index < 5)
    						continue;
    					setTriggerValue(*(uint32_t *)(cmd_buffer + 1));
    					break;
    				}
  • 通道使能 :通过0xC2命令启动触发(如0x00 0x00 0x00 0x08中最后一个字节的bit3=1表示启动触发)

    cs 复制代码
    				case 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算)

    cs 复制代码
    				case 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位数表示

    cs 复制代码
    				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;
    				}

当下位机buffer小于256k,前两位表示采样次数后两位表示采样前舍去次数

  • 采样次数和延时次数: 通过0x81命令+32位数表示

    cs 复制代码
    				case 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命令启用/禁用特定通道

    cs 复制代码
    				case 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)

    cs 复制代码
    				case 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持续写入数据++,不阻塞硬件接收。主程序按需读取数据,无需实时响应每个字节。缓冲区未满时,新数据持续写入;缓冲区满时,也可触发溢出处理。内存固定、无拷贝开销------天然的多任务运行!

    cs 复制代码
    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 */
    }
  • 下位机想通过 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 到此,项目基本功能已经掌握!下一篇,我们来实现"单片机逻辑分析仪"的扩展功能。未完待续~

一首童年最燃的《再飞行》送给你

相关推荐
Invinciblenuonuo17 分钟前
STM32八股【5】----- TIM定时器
stm32·单片机·嵌入式硬件
selenia886025 分钟前
如何成功点亮LED灯并实现闪烁效果
单片机·嵌入式硬件
禾川兴 132424006881 小时前
国产芯片解析:龙讯HDMI Splitter系列:多屏共享高清
单片机·fpga开发·适配器模式
猫猫的小茶馆3 小时前
【PCB工艺】软件是如何控制硬件的发展过程
开发语言·stm32·单片机·嵌入式硬件·mcu·51单片机·pcb工艺
威视锐科技5 小时前
软件定义无线电36
网络·网络协议·算法·fpga开发·架构·信息与通信
JINX的诅咒6 小时前
CORDIC算法:三角函数的硬件加速革命——从数学原理到FPGA实现的超高效计算方案
算法·数学建模·fpga开发·架构·信号处理·硬件加速器
柒月玖.6 小时前
基于AT89C52单片机的轮胎压力监测系统
单片机·嵌入式硬件·mongodb
多多*7 小时前
Java设计模式 简单工厂模式 工厂方法模式 抽象工厂模式 模版工厂模式 模式对比
java·linux·运维·服务器·stm32·单片机·嵌入式硬件
Qwertyuiop20168 小时前
搭建开源笔记平台:outline
笔记·开源
云山工作室9 小时前
基于FPGA的智能垃圾分类装置(论文+源码)
单片机·fpga开发·毕业设计·毕设