本內容記錄于韋東山老師的畢設級開源學習項目,含個人觀點,請理性閱讀。
個人筆記,沒有套路,一步到位,歡迎交流!
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
)實現協議解析,支持以下兩類協議:
- 硬件通信協議解碼
- 常見協議:包括 I2C、SPI、UART、CAN、SD、1-Wire、IrDA 等 90+ 種協議,覆蓋數字信號的波形解析與數據提取。
- 自定義協議:用戶可通過 Python 腳本擴展解碼功能,例如支持UFCS快充協議等定制化需求。
- 數據文件格式支持
- 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 MEMsend_byte(0x21, TIMEOUT_DEFAULT);send_uint32(virtual_bufferSize, TIMEOUT_DEFAULT);//DYNAMIC MEMsend_byte(0x22, TIMEOUT_DEFAULT);send_uint32(0, TIMEOUT_DEFAULT);//SAMPLE RATEsend_byte(0x23, TIMEOUT_DEFAULT);send_uint32(maxFrequency, TIMEOUT_DEFAULT);//Number of Probessend_byte(0x40, TIMEOUT_DEFAULT);send_byte(8, TIMEOUT_DEFAULT);//Protocol Versionsend_byte(0x41, TIMEOUT_DEFAULT);send_byte(0x02, TIMEOUT_DEFAULT);//ENDsend_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 supportedelsesetTriggerState(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。


匯編優化數據采集
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_bufIMPORT g_rxcnt_bufIMPORT g_cur_posIMPORT g_cur_sample_cntIMPORT get_stop_cmdIMPORT g_convreted_sample_countPUSH {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_dataLDR R10, =BUFFER_SIZELDR R6, =0x40010C08 ;GPIOB_IDR地址用于讀取PB8-PB15引腳狀態; 設置PA15的值備用LDR R11, =0X40010810LDR R12, =(1<<15)LDR LR, =(1<<31)
Loop ; 設置PA15輸出高電平STR R12, [R11]
;read dataLDRH R7, [R6] ; 讀GPIOB_IDRLSR 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 + R2MOV R8, R7 ; pre_data = dataLDR R7, [R1, R2, LSL #2] ; R7 = g_rxcnt_buf[g_cur_pos] ;乘以4(即左移2位);目標地址 = 數組基地址(R1) + 下標(R2) * 元素大小(4)ADD R7, #1STR 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 LoopDoneLDR R7, [R4] ; R7 = get_stop_cmdCMP R7, #0 ; if (get_stop_cmd) break;BNE LoopDone ;基于零標志位(Z)判斷相等或不等CMP R2, R10 ; if (g_cur_pos >= BUFFER_SIZE) break;BGE LoopDone ;大于或等于時跳轉NOPNOP ; 延時, 湊出2MHz; 設置PA15輸出高電平STR LR, [R11]B LoopLoopDoneLDR R0, =g_cur_pos ; 得到g_cur_pos變量的地址,并不是得到它的值STR R2, [R0] ; 保存g_cur_pos變量的值LDR R0, =g_cur_sample_cntSTR R3, [R0] ; 保存g_cur_sample_cnt變量的值POP {R4, R5, R6, R7, R8, R9, R10, R11, R12, PC} ; 函數出口恢復寄存器;?為什么用 PC 而不是 LR_因為 LR 可能在函數內部被修改(例如嵌套調用),而棧中保存的是原始的返回地址ENDP ; 函數結束標記
2.內存保存采樣數據優化
/* 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();/* 上報參數 */ //NAMEsend_byte(0x01, TIMEOUT_DEFAULT);send_string("100ASK_LogicalNucleo", TIMEOUT_DEFAULT);send_byte(0x00, TIMEOUT_DEFAULT);//SAMPLE MEMsend_byte(0x21, TIMEOUT_DEFAULT);send_uint32(virtual_bufferSize, TIMEOUT_DEFAULT);//DYNAMIC MEMsend_byte(0x22, TIMEOUT_DEFAULT);send_uint32(0, TIMEOUT_DEFAULT);//SAMPLE RATEsend_byte(0x23, TIMEOUT_DEFAULT);send_uint32(maxFrequency, TIMEOUT_DEFAULT);//Number of Probessend_byte(0x40, TIMEOUT_DEFAULT);send_byte(8, TIMEOUT_DEFAULT);//Protocol Versionsend_byte(0x41, TIMEOUT_DEFAULT);send_byte(0x02, TIMEOUT_DEFAULT);//ENDsend_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 supportedelsesetTriggerState(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時間精度)。
- ?時序準確性是邏輯分析儀的核心指標,直接影響信號分析的可靠性
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_SAMPLEsample_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. 上報數據
- 無需嚴格實時傳輸:上報頻率可以與采樣頻率解耦,但需保證數據順序和時序信息完整。
- ?關鍵要求:
- 數據順序正確(先采樣的數據先上報)。
- 每個數據點的時間戳或等效時間信息可被上位機還原(如通過固定采樣率計算時間偏移)
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 到此,項目基本功能已經掌握!下一篇,我們來實現“單片機邏輯分析儀”的擴展功能。未完待續~
一首童年最燃的《再飛行》送給你