STM32 LCD实时波形显示:ADC/DMA采样与局部刷新实战

发布时间:2026/11/4 3:18:46

STM32 LCD实时波形显示:ADC/DMA采样与局部刷新实战 简介这份基于STM32的LCD实时波形绘制工程面向学习嵌入式硬件与单片机显示技术的开发者尤其适合已掌握GPIO、ADC基础、希望打通采集—处理—显示链路的进阶练习者。项目以3.5寸LCD为输出端围绕LCD初始化、像素格式转换、波形坐标缩放与动态刷新展开并给出缓冲区分行或逐点写入、中断与轮询刷新、DMA减轻CPU负担等实现思路代码采用较清晰的模块化与面向对象组织便于更换屏幕尺寸或调整分辨率、刷新率与定时器参数。压缩包共136个文件约3.08MB包含35个.h头文件、33个.c源文件及Keil工程配置、HEX与AXF编译产物另附STM32标准外设库中ADC、TIM、RCC、FLASH等驱动与LCD驱动源码便于直接打开工程对照学习。目前已有4531人学习下载。读者可从中获得一套可运行的LCD波形显示参考实现理解颜色数据组织、采样值归一化与实时刷屏的取舍也能借鉴其工程结构与性能优化切入点作为课程设计、数据采集可视化原型或二次开发的起点。1. 用一块几十块的屏替代示波器STM32 实时绘制波形图能解决什么调电源纹波、看传感器输出、抓串口时序很多人第一次想的是买台示波器可一台入门设备的价格够买几十套 STM32 最小系统板加一块 2.4 寸 LCD。于是“基于 STM32 的 LCD 实时绘制波形图”这个需求在嵌入式圈子里反复被问起单片机自己采、自己算、自己画到底能不能顶上一台简易示波器答案是可以但边界很清楚——它适合低频、慢变、需要长时间盯着看的信号比如温度曲线、称重传感器抖动、电机霍尔波形、音频包络。不适合高频、需要精确幅度校准的场合。**适合哪些人**手上有 STM32F103/F407 一类开发板、会点 C、想给自制设备加一个“能看到信号”的窗口的从业者。坑主要集中在数据链路和刷屏策略上而不是画线算法本身下面按链路一层层拆。2. 数据链路从 ADC 采样到屏幕像素的四个环节波形图看起来是“画”出来的实际上是“搬”出来的。一条完整的链路是定时器触发 ADC → DMA 把结果搬进内存 → CPU 在缓冲区里做映射和抽点 → LCD 按列刷新。任何一环拖后腿屏幕上的波形就会抖、会撕裂、会“跑”。这一章把前三个环节拆开把参数怎么定讲透。2.1 定时器触发 DMA 搬运把采样和画图解耦最常见的错误写法是在 ADC 转换完成中断里画点。采样率一上到 10kHz中断里跑一次 LCD 打点哪怕是并口就是几微秒起步中断频率直接吃掉大半 CPU主循环连串口都收不完整。正确做法是让硬件自己跑定时器的更新事件作为 TRGO 输出触发 ADC 规则组转换ADC 每次转换完成发 DMA 请求DMA 把ADC1-DR搬进内存数组全程 CPU 不参与。CPU 只在 DMA 半传输和传输完成两个中断里被叫醒一次一次处理半个缓冲区。/* 采样前端TIM3 以 20kHz 更新事件输出 TRGO硬件触发 ADC1 规则组 */ #define ADC_BUF_LEN 512 /* 必须是 2 的幂方便用位与取模 */ static uint16_t g_adc_buf[ADC_BUF_LEN] __attribute__((aligned(4))); static void SampleChain_Init(void) { /* 1. 时钟先全开ADC 走 APB2DMA 走 AHBTIM3 走 APB1 */ RCC-APB2ENR | RCC_APB2ENR_ADC1EN; RCC-AHBENR | RCC_AHBENR_DMA1EN; RCC-APB1ENR | RCC_APB1ENR_TIM3EN; /* 2. 采样时基72MHz / (PSC1) / (ARR1) 20kHz */ TIM3-PSC 0; /* 不分频计数时钟 72MHz */ TIM3-ARR 3599; /* 3600 个计数一个周期 - 20kHz */ TIM3-CR2 TIM_CR2_MMS_1; /* MMS010更新事件作为 TRGO 输出 */ /* 3. ADC1外部触发选 TIM3_TRGO右对齐12 位单通道 */ ADC1-SQR1 0; /* 规则组长度 1 */ ADC1-SQR3 0; /* 通道 0按实际接线改 */ ADC1-SMPR2 ADC_SMPR2_SMP0_2; /* 采样时间 41.5 周期 */ ADC1-CR2 ADC_CR2_EXTSEL_2 | ADC_CR2_EXTSEL_1 /* TIM3_TRGO */ | ADC_CR2_EXTTRIG /* 允许外部触发 */ | ADC_CR2_DMA /* 转换完成发 DMA 请求 */ | ADC_CR2_ADON; ADC1-CR2 | ADC_CR2_ADON; /* 老型号要写两次才唤醒 */ /* 4. DMA1 通道1循环模式半字宽度指向 ADC1-DR */ DMA1_Channel1-CPAR (uint32_t)ADC1-DR; /* 外设地址固定 */ DMA1_Channel1-CMAR (uint32_t)g_adc_buf; /* 内存地址 缓冲区首地址 */ DMA1_Channel1-CNDTR ADC_BUF_LEN; /* 循环模式下自动重装 */ DMA1_Channel1-CCR DMA_CCR_MINC /* 内存地址自增 */ | DMA_CCR_CIRC /* 循环永不停止 */ | DMA_CCR_HTIE /* 半传输中断 */ | DMA_CCR_TCIE /* 传输完成中断 */ | DMA_CCR_PSIZE_0 /* 外设 16 位 */ | DMA_CCR_MSIZE_0 /* 内存 16 位 */ | DMA_CCR_PL_0; /* 优先级中 */ DMA1_Channel1-CCR | DMA_CCR_EN; TIM3-CR1 | TIM_CR1_CEN; /* 启动后整条链路自动跑 */ }这段代码里有四个参数值得逐个抠。ARR3599决定了采样率改采样率只改这一个数别去动 PSCPSC 保持 0 能让时基误差最小。SMPR 采样时间要根据信号源阻抗选信号源内阻大比如接了个 100k 分压就必须拉长到 239.5 周期否则采出来全是漂的这种问题在温度采集上特别常见很多人以为是 ADC 精度不够其实是采样电容没充够。CNDTR是 DMA 一轮搬多少个数取 512 而非 500是为了后面用idx (LEN-1)代替取模运算中断里少几十个周期。CIRC 循环位必须开关了以后 DMA 搬完一轮就停波形只画一屏就定格这是新手最常踩的一个坑。链路跑起来之后CPU 端的入口就只剩两个中断回调半传输前 256 点满和传输完成后 256 点满。回调里不要画图只置一个标志或者把索引递交给主循环把耗时的工作全部挪到while(1)里做。2.2 双缓冲为什么必须波形撕裂的成因与帧率预算只用一个缓冲区就会遇到经典的撕裂DMA 正在往 buf 后半段写CPU 同时在读前半段做映射读到一半 DMA 指针绕回开头把前面的数据覆盖了屏幕上就出现“新波形左半边接着旧波形右半边”的断层。解决方式是乒乓结构——把 512 点的缓冲区看作前后两个 256 点的半区DMA 写后半区时 CPU 只读前半区DMA 写前半区时 CPU 只读后半区靠半传输和传输完成两个中断切换。/* 乒乓读中断只递索引主循环处理“刚写满”的那一半 */ static volatile uint8_t s_half_ready 0; /* bit0前半满, bit1后半满 */ void DMA1_Channel1_IRQHandler(void) { if (DMA1-ISR DMA_ISR_HTIF1) { /* 前半区写满 */ DMA1-IFCR DMA_IFCR_CHTIF1; s_half_ready | 0x01; } if (DMA1-ISR DMA_ISR_TCIF1) { /* 后半区写满 */ DMA1-IFCR DMA_IFCR_CTCIF1; s_half_ready | 0x02; } } static void Wave_Task(void) { if (s_half_ready 0x01) { s_half_ready ~0x01; Render_Half(g_adc_buf[0], ADC_BUF_LEN / 2); /* 读前半区 */ } if (s_half_ready 0x02) { s_half_ready ~0x02; Render_Half(g_adc_buf[ADC_BUF_LEN / 2], ADC_BUF_LEN / 2); } }注意s_half_ready是volatile中断里做的是“或”而不是赋值否则两个中断挨得近时会丢标志。Render_Half里做的事就是把 256 个采样点映射成屏幕列并刷出去。接下来算一笔帧率预算这决定了缓冲区该开多大。假设采样率 20kHz、屏幕宽度 240 列一屏能装 240 个点那么理论上填满一屏需要 12ms也就是最高 83 帧。但实际瓶颈在刷屏如果用的是 SPI 接口的 ILI9341 一类屏单列刷新清旧点、画新点、连线大约要 15~20 微秒240 列就是 4ms 左右加上映射计算一帧 8~10ms 是可以做到的。若采样率只有 1kHz一屏装 240 点要 240ms那波形是“一帧一帧跳”的这时候反而应该做数据抽点把采样率降到刚好匹配屏幕刷新率或者把时基拉开显示更长的窗口。一句话原则采样率 × 屏幕列数 得到的每帧时间要落在刷屏时间的 1~3 倍之间低于这个范围是浪费高于这个范围是卡顿。2.3 坐标映射定点化把 12 位码值压进屏幕高度采样值是 0~4095屏幕可用高度比如是 220 行上下各留 10 行边距。每一帧都要做 256 次映射如果每次写浮点除法F103 没有 FPU一次除法几百个周期光映射就吃掉几毫秒。做法是把斜率提前算成 Q16 定点数运行时只做一次乘法和一次右移。/* 12 位采样值 - 屏幕行号y 越大越靠下所以底部对应码值 0 */ #define Y_TOP 10 #define Y_BOTTOM 230 static int32_t s_slope_q16; /* 每 1 个 ADC 码对应多少像素Q16 格式 */ void Map_Init(uint16_t vref_mv, uint16_t vmax_mv) { /* 只显示 0 ~ vmax_mv 这一段量程超出部分被裁剪掉 */ uint32_t span_code (uint32_t)vmax_mv * 4095u / vref_mv; if (span_code 0) span_code 1; s_slope_q16 ((int32_t)(Y_BOTTOM - Y_TOP) 16) / (int32_t)span_code; } static inline uint16_t Map_ToY(uint16_t code) { int32_t y Y_BOTTOM - (((int32_t)code * s_slope_q16) 16); if (y Y_TOP) y Y_TOP; /* 上限裁剪防止越界写显存 */ if (y Y_BOTTOM) y Y_BOTTOM; return (uint16_t)y; }Map_Init里的vmax_mv就是“满量程显示多少毫伏”它是整个波形显示的唯一标尺。比如参考电压 3300mV、想看 0~3.3V 全量程span_code就是 4095想看 0~1V 的细节span_code变成 1240同样的码值波动就会在屏幕上放大三倍多这是把 LCD 当“放大镜”用的关键。裁剪那两行不能省——信号一旦过冲y会算到屏幕外直接写显存就是花屏这个 bug 一旦出现很难查因为花屏位置会随信号漂移。inline加在Map_ToY上是必须的256 次函数调用在 F103 上也是几百个周期的开销。3. 驱动层落地打点、画线与窗口刷新数据准备好了下一步是把它变成屏幕上的像素。这一层的差别直接决定波形是“流畅”还是“一顿一顿”。先算带宽再选封装最后做局部刷新。3.1 SPI 还是并口先算一笔带宽账一块 320×240 的 16 位色屏一帧全屏是 320×240×2 153600 字节。SPI 接口在 36MHz 时钟下理论 4.5MB/s全屏刷一遍要 34ms也就是满打满算 29 帧实际扣掉命令开销和片选间隙能跑到 20 帧就不错了。并口FSMC 驱动一次写 16 位一个读-改-写周期几十纳秒全屏刷一遍 3~5ms差了一个数量级。接口方式典型时钟/时序全屏理论耗时适用场景SPI 软件模拟几 MHz200ms 以上只做静态显示不做波形SPI 硬件 DMA18~36MHz34~68ms单列局部刷新240 列约 5~10ms8080 并口GPIO 推数十 ns 级10ms 左右中等占用引脚多FSMC / 外部总线总线时序3~5ms多通道、高刷新率首选结论很直白**做实时波形全屏刷新是绝路必须走局部刷新。**SPI 屏也能做波形前提是只刷发生变化的那几列而不是每帧清屏重画。如果手头是 SPI 屏又要画多条通道那就把列宽降下来——320 列里只用 200 列显示波形剩下的放文字。3.2 从打点到画线三级封装的取舍驱动封装一般分三级打点、画线段、画波形。打点是最底层只有一个“设窗口 写一像素”的动作画线是 Bresenham画波形不用通用画线而是用“竖线连接法”——因为波形的 x 坐标是单调递增的每列只需要从上一列的 y 到这一列的 y 画一条竖线比通用 Bresenham 快很多而且不会出现斜线的锯齿断裂。/* 一级打点。所有绘图最终都落到这里 */ void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { LCD_SetWindow(x, y, x, y); LCD_WriteData16(color); } /* 二级竖直线段波形绘制的主力一条命令窗口内连续写 */ void LCD_DrawVLine(uint16_t x, uint16_t y0, uint16_t y1, uint16_t color) { uint16_t n; if (y0 y1) { uint16_t t y0; y0 y1; y1 t; } n y1 - y0 1; LCD_SetWindow(x, y0, x, y1); while (n--) LCD_WriteData16(color); /* 并口可用 DMA 批量推 */ } /* 三级波形单列。擦旧 画新 连线三件事一次窗口搞定 */ void Wave_DrawColumn(uint16_t x, uint16_t y_new, uint16_t y_prev, uint16_t fg, uint16_t bg) { LCD_DrawVLine(x, Y_TOP, Y_BOTTOM, bg); /* 先整列清背景 */ if (y_prev 0xFFFF) { /* 首列没有前一点 */ LCD_DrawVLine(x, y_new, y_new, fg); return; } /* 用竖线把前后两点连起来信号跳变时也不会断 */ LCD_DrawVLine(x, y_prev y_new ? y_prev : y_new, y_prev y_new ? y_new : y_prev, fg); }Wave_DrawColumn里“先整列清背景”这一步是整篇文章里最容易被优化掉、也最容易被优化错的。有人为了省时间只擦旧点附近三个像素结果信号幅度突变时屏幕上留下残影拖尾越跑越脏。整列清一次是 221 行的写入量在并口下不到 20 微秒完全值得。y_prev 0xFFFF这个哨兵值用来标记“这一列是窗口的第一列”避免画出从屏幕顶部到当前点的竖线——这个 bug 我第一次做的时候调了整整一个下午。3.3 局部刷新让每一列只被写一次真正的实时波形不是每帧重画整屏而是像老式示波器的余辉一样一列一列往前推。做法是维护一个写指针x_cursor每来一个新采样点就画一列然后x_cursor走到最右边就绕回 0同时把 0 列整列清掉形成“滚动 覆盖”的效果。/* 滚动刷新新点永远画在游标处游标绕回时那一列自然被覆盖 */ static uint16_t s_x_cursor 0; static uint16_t s_y_prev 0xFFFF; void Wave_Push(uint16_t code) { uint16_t y Map_ToY(code); if (s_x_cursor 0) s_y_prev 0xFFFF; /* 绕回断掉连线 */ Wave_DrawColumn(s_x_cursor, y, s_y_prev, COLOR_FG, COLOR_BG); s_y_prev y; s_x_cursor; if (s_x_cursor WAVE_W) s_x_cursor 0; /* WAVE_W 通常取 240 或 300 */ }这段逻辑有个隐藏的取舍滚动模式下屏幕左边是“最新的数据”还是“最旧的数据”上面的写法里游标从左到右推进所以左边是旧数据、右边是新数据绕回时新数据会从左边覆盖旧数据——视觉上像是波形在“原地刷新”而不是“向左滚动”。想要示波器那种向左滚的效果就把游标反向走或者干脆用memmove的方式每来一列整屏数据左移一列新数据画在最右边。后者每次要重画整屏代价大只有在并口屏上才划算。SPI 屏老老实实用原地覆盖并口屏可以上滚动。还有一个常见做法是“脏列标记”——只在数据变化超过阈值比如 2 个像素时才刷新那一列静止信号下 CPU 占用能降到接近 0。这个优化在只看缓变信号时非常好用但要注意一旦启用屏幕上的旧波形不会被擦掉看起来像“余辉”需要配合定期的整屏清除。4. 实时性调优让波形不抖的四个参数波形画出来了但抖、闪、跳是下一个要解决的问题。抖动基本来自三处采样时基不准、中断优先级打架、像素数少于采样点。这一章逐个拆。4.1 采样率、时基与触发方式先决定“一屏看多久”时基是波形显示里最像“玄学”的参数。同样一个 50Hz 的正弦采样率设 20kHz、一屏 240 点窗口长度只有 12ms连一个完整周期都看不到采样率设 2kHz、一屏 240 点窗口 120ms能看到 6 个周期但每个周期只有 40 个点波形会有明显的折角。正确的算法是反着来先确定“一屏要显示多长时间”时基再倒推采样率。比如想看 100ms 的窗口、屏幕 240 列那么每列代表 416 微秒采样率取 1/416us ≈ 2.4kHz 就够了如果信号里有 1kHz 以上的成分按奈奎斯特至少要 2 倍工程上取 5~10 倍那就得把采样率提到 10kHz然后靠抽点把 1000 个点压成 240 列。抽点在 4.3 节展开。触发方式决定了波形“站不站得住”。自由运行模式下每帧的起始相位随机屏幕上看到的就是一条左右乱窜的波形。解决办法是加一个边沿触发在一段缓冲区里找第一个上升沿前一点小于阈值、当前点大于阈值从那个位置开始往屏幕上画。这样每一帧都从同一个相位开始波形就是“定住”的。触发电平取信号峰峰值的中点附近用代码自动算/* 上升沿触发返回触发点在缓冲区中的下标找不到返回 0 */ static uint16_t Trigger_FindRising(const uint16_t *buf, uint16_t len, uint16_t level) { for (uint16_t i 1; i len; i) { if (buf[i - 1] level buf[i] level) return i; /* 找到了从这一点开始画 */ } return 0; /* 没触发退化成自由运行 */ } /* 自动电平取缓冲区最大最小值的中间避免手调 */ static uint16_t Trigger_AutoLevel(const uint16_t *buf, uint16_t len) { uint16_t vmin 0xFFFF, vmax 0; for (uint16_t i 0; i len; i) { if (buf[i] vmin) vmin buf[i]; if (buf[i] vmax) vmax buf[i]; } return (uint16_t)(((uint32_t)vmin vmax) / 2); }Trigger_FindRising的循环上限是len而不是len-1是为了防止i1越界level用自动电平能省掉一个电位器但要注意信号本身有直流偏置时自动电平会把触发点定在偏置上波形看起来还是会轻微左右晃——这时候要么加迟滞要求上升沿超过 level 2% 量程要么在映射阶段先减去直流均值。迟滞的实现只需把条件改成buf[i-1] level - hyst buf[i] level hyst。4.2 中断优先级谁可以打断谁必须提前定死采样链路一旦用上 DMA 定时器中断就不再只有 LCD 一个了。典型的冲突是DMA 半传输中断正在处理数据串口接收中断插进来跑了 200 微秒等回来的时候 DMA 已经把半个缓冲区覆盖了。表现出来就是波形上随机出现一小段“错位”的锯齿。规则很简单**采样链路的 DMA 中断优先级必须高于所有通信类中断低于任何硬件故障类中断。**在 Cortex-M 的 NVIC 里给 DMA1_Channel1 设抢占优先级 0 或 1串口设 3SysTick 设 15。同时把 DMA 中断里的工作量压到最小——只置标志不做映射。映射和刷屏全部放到主循环主循环里再关一次全局中断把数据memcpy到工作缓冲区拷贝 512 个半字只要几微秒代价可以接受。/* 主循环里取一份快照避免渲染期间 DMA 又写回同一块内存 */ static uint16_t s_snapshot[ADC_BUF_LEN / 2]; static void Take_Snapshot(const uint16_t *src) { __disable_irq(); /* 关中断拷贝开中断 */ for (uint32_t i 0; i ADC_BUF_LEN / 2; i) s_snapshot[i] src[i]; __enable_irq(); }关中断的时间要控制在 10 微秒以内超过就会影响串口收字节。512 个半字的拷贝在 72MHz 的 M3 上大约 3~5 微秒勉强合格缓冲区开到 2048 就危险了这时候应该改用双缓冲指针交换而不是拷贝。4.3 抽点与峰值保持屏幕像素比采样点少的时候当一屏的采样点数远大于屏幕列数比如 1000 个点画 240 列直接隔 N 个取一个会丢掉窄脉冲——一个只持续 2 个采样点的毛刺抽点后完全消失你以为信号很干净实际上有故障。正确做法是峰值保持把落在同一列里的所有采样点取最大值和最小值在这一列上画一条从 min 到 max 的竖线。这样任何毛刺都会以竖线的形式显示出来宁可“看起来脏”也不能漏掉。/* 峰值保持抽点把 src 的 len 个点压成 dst_w 列每列保留 min/max */ void Decimate_PeakHold(const uint16_t *src, uint16_t len, uint16_t *out_min, uint16_t *out_max, uint16_t dst_w) { uint16_t step_q8 (uint16_t)(((uint32_t)len 8) / dst_w); /* 每列步长Q8 */ for (uint16_t x 0; x dst_w; x) { uint16_t start (uint16_t)(((uint32_t)x * step_q8) 8); uint16_t end (uint16_t)(((uint32_t)(x 1) * step_q8) 8); uint16_t vmin 0xFFFF, vmax 0; if (end len) end len; for (uint16_t i start; i end; i) { if (src[i] vmin) vmin src[i]; if (src[i] vmax) vmax src[i]; } out_min[x] vmin; out_max[x] vmax; } }step_q8用 Q8 定点表示每列跨多少个采样点避免浮点。注意end的边界裁剪——当len不是dst_w的整数倍时最后一列会越界这个越界不会立刻崩溃但会读到数组外的随机值表现为屏幕最右边一列总是乱跳。渲染时把out_min[x]和out_max[x]都映射成 y然后调用LCD_DrawVLine画一条竖线毛刺就保住了。代价是波形看起来比实际“粗”静态信号下会显示成一团 2~3 像素厚的带状——这是正常的不是 bug。5. 避坑与排查五个现场翻车记录下面这几条都是我在实际项目里踩过的按“现象 → 原因 → 解决”写遇到类似症状可以直接对号入座。5.1 显示层花屏、残影、刷新慢**现象一屏幕上偶尔出现一条斜着贯穿全屏的亮线位置每次都不一样。**原因几乎总是坐标越界Map_ToY返回了负值或者超过屏幕高度的值被强转成uint16_t后变成一个巨大的数窗口设置命令收到这个值后把显存地址算飞了。解决是把映射函数的上下裁剪写死并且在LCD_SetWindow里再兜一层if (x LCD_W || y LCD_H) return;。这层防御看着冗余但它把“偶发花屏”变成了“偶发丢点”后者肉眼根本看不出来。**现象二波形跑一段时间后屏幕上留下越来越厚的拖影。**原因是刷新时只擦了旧点附近没有清整列。信号幅度大范围跳变时旧点和新点之间的区域没被覆盖累积成残影。解决就是回到 3.2 节的Wave_DrawColumn每列先整列刷背景再画前景。如果嫌整列清屏慢可以退一步清min(y_prev,y_new)-2到max(y_prev,y_new)2这一段比整列省一半时间又不会留残影。**现象三SPI 屏刷到 240 列时明显卡顿CPU 占用率打到 90%。**原因是每列刷新都重新发了一遍“设置窗口”命令一列四条命令加数据命令开销比数据还大。解决办法是合并窗口波形是连续列的可以一次性设置一个 16 列宽的窗口把 16 列的数据打包进一个 DMA 缓冲区推出去。代价是要多做一次内存组包在 F103 上大约 30 微秒但省下的 SPI 命令时间是这个的好几倍。5.2 数据层抖动、丢点、量程不对**现象四波形整体在左右方向上轻微晃动像喝了酒。**如果已经加了触发还是晃八成是触发点落在了缓冲区末尾附近导致后面补的是下一轮的数据相位对不上。解决办法是把触发搜索范围限制在缓冲区的前 3/4并且要求触发点之后至少还有dst_w个点可用不够就丢弃这一帧。另一个隐蔽原因是采样时基不是整数分频——ARR算出来带小数被截断实际采样率和设定值差千分之几长时间看会缓慢漂移。这种漂移很难查建议在初始化时把实际分频值打印出来核对一遍。**现象五接 3.3V 信号时波形顶到屏幕上边就削平了。**先查参考电压如果Map_Init里传的vref_mv是 3300但实际 VDDA 只有 3250满量程就会差一点。再查采样时间信号源内阻大、采样时间短ADC 采到的值会系统性偏低表现为波形整体压低且顶部变圆。还有一种情况是输入超过 VDDA被内部保护二极管钳位波形顶部削平的同时 ADC 输入引脚会有微弱漏电流长时间这么接会损伤引脚。量程标定的正确做法是接一个已知电压比如用基准源或电池跑一遍把vref_mv反算出来而不是照抄手册。6. 进阶从“看得见”到“读得出”加一层参数测量波形画出来只解决了一半问题真要用起来还得能读数。在渲染链路后面挂一个轻量的测量层对每一帧的快照算几个特征值成本很低但价值很高。测量项算法注意点峰峰值max - min要跳过触发点之前的无效段平均值累加后除以点数用uint32_t累加512 点不会溢出有效值平方和累加后开方开方用整数牛顿迭代别引入 libm频率找相邻两个上升沿的下标差下标差 × 采样周期噪声大时要加迟滞占空比高电平点数 / 总点数阈值取自动电平频率测量的实现要点是“下标差”而不是“数周期”f fs / (idx2 - idx1)其中fs是采样率。这个方法在低频下误差大下标差可能有 ±1 的量化误差信号周期越长越准。想要更准就用线性插值触发点落在两个采样点之间按斜率插出小数部分能把误差降到 1/10 个采样周期。/* 用插值提高触发点精度在 lastlevelcur 的区间里线性插值 */ static float Trigger_SubSample(const uint16_t *buf, uint16_t idx, uint16_t level) { float a (float)buf[idx - 1]; float b (float)buf[idx]; if (b a) return (float)idx; return (float)(idx - 1) (level - a) / (b - a); /* 返回小数下标 */ }有了小数下标两个触发点相减得到的就是带小数的周期数频率精度直接上一个台阶。这个技巧在测 50Hz 工频、测电机转速时特别有用因为整数下标的量化误差在低频段占比很大。最后说一个我自己的习惯。早期做这类项目我总喜欢先把画图调通看着波形出来就觉得成了结果一到现场就发现数据是错的、量程是错的、触发是晃的只能从头返工。从那以后我每次做采样显示类的东西都强制走一遍“先存后画”的流程先把原始数据通过串口整块 dump 到上位机确认数值、量程、时基全对再打开 LCD 渲染。这一步多花半小时能省掉后面几天的排查。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/11/4 3:18:46

LE Audio核心技术解析与应用实践

1. LE Audio技术背景与核心价值蓝牙技术联盟在2020年正式发布的LE Audio标准,标志着无线音频传输进入全新时代。作为传统Classic Audio的革新版本,LE Audio基于低功耗蓝牙(BLE)架构重新设计了整个音频协议栈。我在参与某跨国耳机项…

2026/11/4 3:18:46

前端性能优化实战:从原理到工程实践

1. 性能优化为何成为前端工程师的必修课第一次接手公司核心产品的前端改版时,我遇到了一个令人尴尬的场景:在演示新版页面时,点击按钮后足足等待了3秒才出现响应。会议室里PM皱起的眉头和CTO敲击桌面的手指,至今记忆犹新。这次经历…

2026/11/4 3:18:46

Linux下MySQL C客户端头文件、库文件与多架构链接排错

简介:面向在 Linux 环境下做 C/C 开发、需要对接 MySQL 数据库的开发者,尤其是苦于缺少 mysql-devel 包、或不便在目标机上安装 MySQL 客户端与服务器的场景。资源是一套可直接随工程携带的 MySQL 连接头文件与库文件集合,同时提供 32 位和 6…

2026/11/4 4:23:48

1.73亿token账单拆解:缓存命中率如何影响AI编程助手成本

1. 一个月的账单摆在面前,先别急着掏计算器9 月刚过完,我把 WorkBuddy 后台的用量面板拉出来,看到那个数字的时候,第一反应是"这玩意儿是不是统计错了"——1.73 亿 token。不是 173 万,是 1.73 亿。我盯着屏…

2026/11/4 4:23:48

C++内联函数:性能优化与实现原理详解

1. 从宏到内联函数:C性能优化的关键一步在C开发中,函数调用开销一直是性能敏感场景的痛点。传统函数调用需要保存现场、传递参数、跳转指令并建立新栈帧,这种机制虽然清晰但代价高昂。早期C程序员常用宏定义来规避这种开销,但宏带…

2026/11/4 4:23:48

具身智能在海鲜处理中的多模态感知与自适应控制

1. 项目概述:具身智能的物理世界突围战当波士顿动力的机器人还在实验室里表演后空翻时,中国团队已经让机械臂学会了处理活体龙虾。五一视界最新发布的51Claw具身智能系统,正试图用海鲜处理这个看似狭窄的切口,验证一个更宏大的命题…

2026/11/4 4:23:48

Folia 下载慢?网盘镜像与 AUR 源双路线提速方案

Folia 这个项目在圈子里火起来之后,我身边不少朋友第一反应是去官网拉包,结果进度条卡在百分之几不动,等了半天最后超时重来。这事儿其实挺典型的——一个托管在海外代码平台上的开源项目,国内直连下载大文件,体验往往…

2026/11/4 4:18:48

无人机遥感图像处理链路:去噪、辐射校正与智能裁剪

1. 无人机遥感图像为什么“不能直接用”——从航拍到可交付成果的处理链路拿到无人机落下来的原始影像,绝大多数人第一反应是直接拖进软件里拼图,然后被结果吓一跳:田块边界糊成一团,同一片作物在相邻两张图上颜色一深一浅&#x…

2026/11/3 7:47:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/3 14:24:53

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/4 0:03:20

AI辅助技术栈评估:提升研发决策效率60%

1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年…

2026/11/4 0:03:20

从生成Token到直接决策:Jev如何重塑AI Agent工程实践

1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你…

2026/11/4 0:03:20

32G统一内存跑大模型:内存账、TPS实测与端云分流

想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统…

2026/11/3 7:47:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/3 14:24:53

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑