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

news2025/4/7 15:54:55

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

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

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:

#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)
#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. 扫描连接设备阶段

我们使用串口接收中断,不断循环检测接收到的命令或者数据:
  	while (1)
	{  
      if (uart_recv(&c, TIMEOUT_FOREVER) == 0)
        {
			cmd_buffer[cmd_index] = c;	
            switch (cmd_buffer[0])
            {
  • 连接设备时:上位机(PulseView)会先发送复位命令(CMD_RESET)​​(命令码0x00),用于初始化下位机状态并清除缓存
                    case CMD_RESET:
                    {
                        break;
                    }
  • 应答回复:上位机(PulseView)会先发送CMD_ID 命令(“0x02”),下位机要回复 4 个字节“1ALS”。
                    case CMD_ID:
                    {
                        /* 上报4个字节"1ALS" */                
                        uart_send((uint8_t *)"1ALS", 4, TIMEOUT_DEFAULT);
                        break;
                    }
  • 设备识别:随后发送查询设备信息命令​(命令码0x04),要求下位机返回设备固件版本、支持的通道数和最大采样率等信息。获取下位机的默认配置(如内存大小、支持的触发模式),
     下位机要回复的参数格式为“1 字节的数据类别,多个字节的数据”,说明如下:
                    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命令设置通道使能
    				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系列命令设置触发值(如高\低电平触发)
    				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表示启动触发)
    				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算)
    				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位数表示
    				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位数表示
    				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命令启用/禁用特定通道
    				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)
    				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 中断函数的时间:

汇编优化数据采集


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 恢复各个采样点的数据,上报给上位机
        /* 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持续写入数据,不阻塞硬件接收。主程序按需读取数据,无需实时响应每个字节。缓冲区未满时,新数据持续写入;缓冲区满时,也可触发溢出处理。内存固定、无拷贝开销——天然的多任务运行!
    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 口发送数据时,要确保上次传输完成
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);
}

硬件问题:

/**********************************************************************
 * 函数名称: 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.代码实现

                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.解析命令

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 已经满
⑤ 恢复中断

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 个数据
上报一个即可。
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(收尾工作)

 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)中已经实现:

//清空数据缓冲区位置和采样计数器。
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串口用到的环形缓冲区……

这么一来下面这段自以为是改进的代码就显得赘述了

​
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 到此,项目基本功能已经掌握!下一篇,我们来实现“单片机逻辑分析仪”的扩展功能。未完待续~

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2329937.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

11-产品经理-创建产品

在“产品”-“仪表盘”内&#xff0c;可以查看系统中关于产品及相关需求的统计。 在“产品”-“产品列表”页面&#xff0c;可以按项目集、项目查看其关联产品。还可以添加产品、编辑产品线、或者导出产品列表。 产品看板&#xff0c;通过看板方式查看产品、产品计划和产品下的…

低代码开发平台:飞帆制作网页并集成到自己的网页中

应用场景&#xff1a; 有时&#xff0c;我们的网页使用了某个模版&#xff0c;或者自己写的 html、css、javascript 代码。只是网页中的一部分使用飞帆来制作。这样的混合网页如何实现呢&#xff1f; 其实很容易&#xff0c;来体验一下飞帆提供的功能&#xff01; 还记得这个…

语法: result=log (x);

LOG( ) 语法: resultlog (x); 参数: x是一个浮点数; 返回值: result等于返回值,是一个浮点数; 功能: 该函数是用来计算浮点数x的自然对数(即ln x);如果x小于或等于0,或x太大,则行为没有定义; 注意:存在error挂起; 如果在编写程序里包含了errno.h头文件,则范围和等级…

Hibernate核心方法总结

Session中的核心方法梳理 1、save方法 这个方法表示将一个对象保存到数据库中&#xff0c;可以将一个不含OID的new出来的临时对象转换为一个处于Session缓存中具有OID的持久化对象。 需要注意的是&#xff1a;在save方法前设置OID是无效的但是也不会报错&#xff0c;在save方…

IntelliJ IDEA Maven 工具栏消失怎么办?

一、问题现象与背景 在使用 IntelliJ IDEA&#xff08;简称 IDEA&#xff09;开发 Maven 项目时&#xff0c;偶尔会遇到右侧或侧边栏的 Maven 工具栏&#xff08;显示依赖、生命周期等信息的窗口&#xff09;突然消失的情况。这可能影响开发者快速操作 Maven 构建、依赖管理等…

消息队列(kafka 与 rocketMQ)

为什么要使用消息队列?作用1: 削峰填谷(突发大请求量问题)作用2: 解耦(单一原则)作用3: 异步(减少处理时间) 如何选择消息队列(kafka&RocketMQ)成本功能性能选择 rocketMQ是参考kafka进行实现的为什么rocketMQ与kafka性能差距很大呢?kafka 的底层数据储存实现rocketMQ 的…

【STM32】Flash详解

【STM32】Flash详解 文章目录 【STM32】Flash详解1.Flash闪存概念1. 1核心区别&#xff1a;NOR Flash vs. NAND Flash1.2 为什么常说的“Flash”多指 NAND Flash&#xff1f;1.3技术细节对比(1) 存储单元结构(2) 应用场景(3) 可靠性要求 1.4总结 2.STM32内部的Flash2.1为什么是…

CV - 目标检测

物体检测 目标检测和图片分类的区别&#xff1a; 图像分类&#xff08;Image Classification&#xff09; 目的&#xff1a;图像分类的目的是识别出图像中主要物体的类别。它试图回答“图像是什么&#xff1f;”的问题。 输出&#xff1a;通常输出是一个标签或一组概率值&am…

node-modules-inspector 可视化node_modules

1、node_modules 每个vue的项目都有很多的依赖&#xff0c;有的是dev的&#xff0c;有的是生产的。 2、使用命令pnpx node-modules-inspector pnpx node-modules-inspector 3、node_modules可视化 4、在线体验 Node Modules Inspector 5、github地址 https://github.com/a…

远程服务器下载llama模型

适用于有防火墙不能直接从HF上下载的情况 然后&#xff0c;你可以克隆 Llama-3.1-8B-Instruct 模型&#xff1a; git clone https://你的用户名:你的访问令牌hf-mirror.com/meta-llama/Llama-3.1-8B-Instruct用户名&#xff0c;令牌来自huggingface官网 注意&#xff1a;要提…

2011-2019年各省地方财政金融监管支出数据

2011-2019年各省地方财政金融监管支出数据 1、时间&#xff1a;2007-2019年 2、来源&#xff1a;国家统计局、统计年鉴 3、指标&#xff1a;行政区划代码、地区、年份、地方财政金融监管支出 4、范围&#xff1a;31省 5、指标说明&#xff1a;地方财政在金融监管方面的支出…

Java大厂面试题 -- JVM 优化进阶之路:从原理到实战的深度剖析(2)

最近佳作推荐&#xff1a; Java大厂面试题 – 深度揭秘 JVM 优化&#xff1a;六道面试题与行业巨头实战解析&#xff08;1&#xff09;&#xff08;New&#xff09; 开源架构与人工智能的融合&#xff1a;开启技术新纪元&#xff08;New&#xff09; 开源架构的自动化测试策略优…

存储引擎 / 事务 / 索引

1. 存储引擎 MySQL 中特有的术语。 &#xff08;Oracle 有&#xff0c;但不叫这个名字&#xff09; 是一种表存储 / 组织数据的方式 不同的存储引擎&#xff0c;表存储数据的方式不同 1.1 查看存储引擎 命令&#xff1a; show engines \g&#xff08;或大写&#xff1a;G…

RabbitMQ运维

RabbitMQ运维 一.集群1.简单介绍2.集群的作用 二.搭建集群1.多机多节点搭建步骤 2.单机单节点搭建步骤 3.宕机演示 三.仲裁队列1.简单介绍2.Raft协议Raft基本概念主节点选举选举过程 3.仲裁队列的使用 四.HAProxy负载均衡1.安装HAProxy2.HAProxy的使用 一.集群 1.简单介绍 Ra…

Ansible 实战:Roles,运维的 “魔法函数”

一、介绍 你现在已经学过tasks和handlers&#xff0c;那么&#xff0c;最好的playbook组织方式是什么呢&#xff1f;答案很简单&#xff1a;使用roles&#xff01;roles基于一种已知的文件结构&#xff0c;能够自动加载特定的vars_files、tasks以及handlers。通过roles对内容进…

关于JVM和OS中的指令重排以及JIT优化

关于JVM和OS中的指令重排以及JIT优化 前言&#xff1a; 这东西应该很重要才对&#xff0c;可是大多数博客都是以讹传讹&#xff0c;全是错误&#xff0c;尤其是JVM会对字节码进行重排都出来了&#xff0c;明明自己测一测就出来的东西&#xff0c;写出来误人子弟… 研究了两天&…

在CPU服务器上部署Ollama和Dify的过程记录

在本指南中&#xff0c;我将详细介绍如何在CPU服务器上安装和配置Ollama模型服务和Dify平台&#xff0c;以及如何利用Docker实现这些服务的高效部署和迁移。本文分为三大部分&#xff1a;Ollama部署、Dify环境配置和Docker环境管理&#xff0c;适合需要在本地或私有环境中运行A…

【计网】TCP 协议详解 与 常见面试题

三次握手、四次挥手的常见面试题 不用死记&#xff0c;只需要清楚三次握手&#xff0c;四次挥手的流程&#xff0c;回答的时候心里要记住&#xff0c;假设网络是不可靠的 问题(1)&#xff1a;为什么关闭连接时需要四次挥手&#xff0c;而建立连接却只要三次握手&#xff1f; 关…

7.4 SVD 的几何背景

一、SVD 的几何解释 SVD 将矩阵分解成三个矩阵的乘积&#xff1a; ( 正交矩阵 ) ( 对角矩阵 ) ( 正交矩阵 ) (\pmb{正交矩阵})\times(\pmb{对角矩阵})(\pmb{正交矩阵}) (正交矩阵)(对角矩阵)(正交矩阵)&#xff0c;用几何语言表述其几何背景&#xff1a; ( 旋转 ) ( 伸缩 )…

C++的多态-上

目录 多态的概念 多态的定义及实现 1.虚函数 2. 多态的实现 2.1.多态构成条件 2.2.虚函数重写的两个例外 (1)协变(基类与派生类虚函数返回值类型不同) (2)析构函数的重写(基类与派生类析构函数的名字不同) 2.3.多态的实现 2.4.多态在析构函数中的应用 2.5.多态构成条…