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)与硬件设备通信,支持多种硬件接口协议,具体包括:
- 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:
#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 0x000x08中最后一个字节的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 到此,项目基本功能已经掌握!下一篇,我们来实现“单片机逻辑分析仪”的扩展功能。未完待续~
一首童年最燃的《再飞行》送给你
相关文章:
001 使用单片机实现的逻辑分析仪——吸收篇
本内容记录于韦东山老师的毕设级开源学习项目,含个人观点,请理性阅读。 个人笔记,没有套路,一步到位,欢迎交流! 00单片机的逻辑分析仪与商业版FPGA的逻辑分析仪异同 对比维度自制STM32逻辑分析仪商业版逻…...
es基本概念
Elasticsearch 的架构与基本概念 Elasticsearch(简称 ES)是一个开源的分布式搜索和分析引擎,基于 Apache Lucene 构建。它被广泛用于全文搜索、日志分析、实时数据分析等场景。以下是其架构概述及其基本概念的详细解释。 Elasticsearch 的架…...
可以使用费曼学习法阅读重要的书籍
书本上画了很多线,回头看等于没画出任何重点。 不是所有的触动都是有效的。就像你曾经看过很多好文章,当时被触动得一塌糊涂,还把它们放进了收藏夹,但一段时间之后,你就再也记不起来了。如果让你在一本书上画出令自己…...
11-产品经理-创建产品
在“产品”-“仪表盘”内,可以查看系统中关于产品及相关需求的统计。 在“产品”-“产品列表”页面,可以按项目集、项目查看其关联产品。还可以添加产品、编辑产品线、或者导出产品列表。 产品看板,通过看板方式查看产品、产品计划和产品下的…...
JavaScript学习教程,从入门到精通,JavaScript 基础语法全面指南(5)
JavaScript 基础语法全面指南 一、JavaScript 基本概念 JavaScript 是一种轻量级的解释型或即时编译型的编程语言,主要用于网页开发,为网页添加交互功能。 1.1 JavaScript 的特点 解释型语言:不需要编译,由 JavaScript 引擎直…...
低代码开发平台:飞帆制作网页并集成到自己的网页中
应用场景: 有时,我们的网页使用了某个模版,或者自己写的 html、css、javascript 代码。只是网页中的一部分使用飞帆来制作。这样的混合网页如何实现呢? 其实很容易,来体验一下飞帆提供的功能! 还记得这个…...
语法: result=log (x);
LOG( ) 语法: resultlog (x); 参数: x是一个浮点数; 返回值: result等于返回值,是一个浮点数; 功能: 该函数是用来计算浮点数x的自然对数(即ln x);如果x小于或等于0,或x太大,则行为没有定义; 注意:存在error挂起; 如果在编写程序里包含了errno.h头文件,则范围和等级…...
Hibernate核心方法总结
Session中的核心方法梳理 1、save方法 这个方法表示将一个对象保存到数据库中,可以将一个不含OID的new出来的临时对象转换为一个处于Session缓存中具有OID的持久化对象。 需要注意的是:在save方法前设置OID是无效的但是也不会报错,在save方…...
IntelliJ IDEA Maven 工具栏消失怎么办?
一、问题现象与背景 在使用 IntelliJ IDEA(简称 IDEA)开发 Maven 项目时,偶尔会遇到右侧或侧边栏的 Maven 工具栏(显示依赖、生命周期等信息的窗口)突然消失的情况。这可能影响开发者快速操作 Maven 构建、依赖管理等…...
消息队列(kafka 与 rocketMQ)
为什么要使用消息队列?作用1: 削峰填谷(突发大请求量问题)作用2: 解耦(单一原则)作用3: 异步(减少处理时间) 如何选择消息队列(kafka&RocketMQ)成本功能性能选择 rocketMQ是参考kafka进行实现的为什么rocketMQ与kafka性能差距很大呢?kafka 的底层数据储存实现rocketMQ 的…...
【STM32】Flash详解
【STM32】Flash详解 文章目录 【STM32】Flash详解1.Flash闪存概念1. 1核心区别:NOR Flash vs. NAND Flash1.2 为什么常说的“Flash”多指 NAND Flash?1.3技术细节对比(1) 存储单元结构(2) 应用场景(3) 可靠性要求 1.4总结 2.STM32内部的Flash2.1为什么是…...
CV - 目标检测
物体检测 目标检测和图片分类的区别: 图像分类(Image Classification) 目的:图像分类的目的是识别出图像中主要物体的类别。它试图回答“图像是什么?”的问题。 输出:通常输出是一个标签或一组概率值&am…...
node-modules-inspector 可视化node_modules
1、node_modules 每个vue的项目都有很多的依赖,有的是dev的,有的是生产的。 2、使用命令pnpx node-modules-inspector pnpx node-modules-inspector 3、node_modules可视化 4、在线体验 Node Modules Inspector 5、github地址 https://github.com/a…...
远程服务器下载llama模型
适用于有防火墙不能直接从HF上下载的情况 然后,你可以克隆 Llama-3.1-8B-Instruct 模型: git clone https://你的用户名:你的访问令牌hf-mirror.com/meta-llama/Llama-3.1-8B-Instruct用户名,令牌来自huggingface官网 注意:要提…...
Spring 中的 @Autowired 和 @Resource
🧩 一、Autowired 和 Resource 的基本作用 注解来源作用AutowiredSpring 提供(org.springframework.beans.factory.annotation.Autowired)按类型 自动注入ResourceJDK 提供(javax.annotation.Resource)默认按名称 注入…...
自定义汇编语言(Custom Assembly Language) 和 Unix Git
1. 什么是自定义汇编语言(Custom Assembly Language)? 汇编语言(Assembly Language)是一种低级编程语言,它直接与 CPU 指令集(Instruction Set Architecture, ISA)对应,…...
Python菜鸟教程(小程序)
目录 一.简易计算器 二.学生成绩分级 三.密码设置 四.作业选择 点赞收藏,评论支持 一.简易计算器 print(-------使用的运算符-------\n) print(1.加号) print(2.减号) print(3.乘号) print(4.除号) Aint(input(请输入第一个数: )) Bint(input(请输入第二个数: )) Fi…...
2011-2019年各省地方财政金融监管支出数据
2011-2019年各省地方财政金融监管支出数据 1、时间:2007-2019年 2、来源:国家统计局、统计年鉴 3、指标:行政区划代码、地区、年份、地方财政金融监管支出 4、范围:31省 5、指标说明:地方财政在金融监管方面的支出…...
Java大厂面试题 -- JVM 优化进阶之路:从原理到实战的深度剖析(2)
最近佳作推荐: Java大厂面试题 – 深度揭秘 JVM 优化:六道面试题与行业巨头实战解析(1)(New) 开源架构与人工智能的融合:开启技术新纪元(New) 开源架构的自动化测试策略优…...
SQL LIKE 语句详解
SQL LIKE 语句详解 引言 SQL(Structured Query Language)是用于管理关系数据库的标准语言。在SQL中,LIKE 语句是一种常用的字符串匹配操作符,用于在查询中搜索包含特定模式的字符串。本文将详细介绍 LIKE 语句的用法、规则以及注意事项。 一、什么是 LIKE 语句? LIKE …...
现代Web应用中的时光机器:深入解析撤销/重做功能的艺术与科学
引言:数字世界的安全网 在现实世界中,我们拥有橡皮擦、撤销键和后悔药(比喻意义上)。数字世界同样需要这样的安全保障。研究表明: **85%**的用户会在完成复杂表单时犯至少一个错误 提供撤销功能的界面可将用户满意度…...
存储引擎 / 事务 / 索引
1. 存储引擎 MySQL 中特有的术语。 (Oracle 有,但不叫这个名字) 是一种表存储 / 组织数据的方式 不同的存储引擎,表存储数据的方式不同 1.1 查看存储引擎 命令: show engines \g(或大写:G…...
Go语言-初学者日记(八):构建、部署与 Docker 化
🧱 一、go build:最基础的构建方式 Go 的构建工具链是出了名的轻量、简洁,直接用 go build 就能把项目编译成二进制文件。 ✅ 构建当前项目 go build -o myapp-o myapp 指定输出文件名默认会构建当前目录下的 main.go 或 package main &a…...
【ESP32】ESP32物联网应用:MQTT控制与状态监测
ESP32物联网应用:MQTT控制与状态监测 引言 在物联网时代,远程监测和控制设备已经成为现实生活中常见的需求。本文将介绍如何使用ESP32微控制器配合MQTT协议,实现一个简单而强大的物联网应用:远程状态监测和设备控制。我们将以巴…...
RabbitMQ运维
RabbitMQ运维 一.集群1.简单介绍2.集群的作用 二.搭建集群1.多机多节点搭建步骤 2.单机单节点搭建步骤 3.宕机演示 三.仲裁队列1.简单介绍2.Raft协议Raft基本概念主节点选举选举过程 3.仲裁队列的使用 四.HAProxy负载均衡1.安装HAProxy2.HAProxy的使用 一.集群 1.简单介绍 Ra…...
Ansible 实战:Roles,运维的 “魔法函数”
一、介绍 你现在已经学过tasks和handlers,那么,最好的playbook组织方式是什么呢?答案很简单:使用roles!roles基于一种已知的文件结构,能够自动加载特定的vars_files、tasks以及handlers。通过roles对内容进…...
CVAT安装和使用(Windows)
必要组件安装 WSL2 Docker Git Chrome Install WSL2 (Windows subsystem for Linux) refer to this official guide. WSL2 requires Windows 10, version 2004 or higher. After installing WSL2, install a Linux Distribution of your choice. 安装 WSL2(适用…...
Spring 中的 bean 生命周期
🌱 一、什么是 Bean 生命周期? 在 Spring 容器中,一个 Bean 从“创建 → 初始化 → 使用 → 销毁”,经历了完整的生命周期。 Spring 提供了 多个扩展点 让你可以在这些阶段做事情,比如注入资源、日志记录、连接资源、清…...
关于JVM和OS中的指令重排以及JIT优化
关于JVM和OS中的指令重排以及JIT优化 前言: 这东西应该很重要才对,可是大多数博客都是以讹传讹,全是错误,尤其是JVM会对字节码进行重排都出来了,明明自己测一测就出来的东西,写出来误人子弟… 研究了两天&…...
微软推出首款量子计算芯片Majorana 1
全球首款拓扑架构量子芯片问世,2025年2月20日,经过近20年研究,微软推出了首款量子计算芯片Majorana 1,其宣传视频如本文末尾所示。 微软表示,开发Majorana 1需要创造一种全新的物质状态,即所谓的“拓扑体”…...





