STM32F103 OLED示波器实现:ADC采样与滚动刷新的内存优化

发布时间:2026/9/15 14:30:09
STM32F103 OLED示波器实现:ADC采样与滚动刷新的内存优化 简介基于STM32F103的OLED简易示波器完整工程面向嵌入式爱好者、大学生及DIY玩家用于学习ADC信号采集与OLED波形显示。项目采用“二维数据一维化”滚动刷新策略仅保留当前显示行数据显著降低内存占用适合资源受限的STM32开发。RAR资源包共55个文件包括C源码、头文件、Keil工程文件、编译生成的hex/axf以及烧录与调试配置整体仅746KB小巧完整。工程目录划分清晰HARDWARE、SYSTEM、USER、OBJ等模块便于对照学习已有1546人学习下载。资源内包含ADC采集、OLED初始化与绘图、串口调试等程序模块并特别展示如何在中断中实时刷新波形从代码结构到滚动显示算法都可作为课程设计或DIY项目的参考模板帮助理解嵌入式系统中的内存优化与实时处理技巧且整体可编译运行适合快速上手。1. 示波器也要算内存账这套 OLED 方案为什么能跑在 F103 上见到实物之前我也以为 STM32F103 做示波器只是个玩具Cortex-M3 主频 72MHz内置 ADC 采样率撑死 1MbpsRAM 只有 20KB还要塞进一块 OLED 屏的显存和滚动波形缓冲。真拆开这套简易oled示波器的源码结构发现它根本没走大数组存波形的老路——HARDWARE/adc.c只负责把 ADC 连续采样值交给 DMAoled.c里没有整帧波形缓冲而是把二维波形数据降成一维的当前行状态边采边绘滚动刷新。这样做最直接的好处是 RAM 占用被压到几 KB 以内F103C8T6 这种 20KB SRAM 的芯片也能流畅跑起来。对刚接触嵌入式信号采集的人来说这套代码是理解 ADC、DMA、OLED 显存三者怎么协作的很好样本对老手来说这里更值得拆的是采样率怎么标定触发怎么做噪声从哪来这些实际调板问题。下文按硬件链路、显存策略、采样控制、验证排错的顺序展开。2. 二维数据一维化OLED 滚动刷新的内存账本2.1.1 传统示波器缓冲模型在 F103 上为什么立不住通用示波器的模型是持续采样按需显示ADC 以固定速率采样结果写入环形缓冲显示端再从缓冲里抽取一帧数据绘制。这个模型在 F103 上最大的问题是内存。以 128×64 的 OLED 为例一屏波形需要 128 个采样点每个点 2 字节一帧只要 256 字节看起来不大但要实现滚动连续显示至少需要两帧以上的缓冲做双缓冲切换再加上触发前后各一段数据的预触发存储缓冲区大小会到 1KB~2KB。听起来还是能接受可别忘了 F103 的 RAM 还要分给堆栈、printf 重定向、OLED 显存、DMA 描述符。实际工程里 RAM 占用很快就会见底尤其是如果你还想把波形暂停后放大查看那就必须存更多历史点。这套项目的做法是把屏幕拆成 64 行像素。OLED 控制器常见如 SSD1306本身带 1KB 的 GDDRAM显示刷新由控制器自己完成MCU 只需要更新 GDDRAM 内容。示波器要展示的波形其实是时间轴 × 幅值轴的二维关系传统做法是维护一个time_buf[128]每个元素记录该时间点对应的幅值绘制时再遍历这个数组把幅值换算成屏幕 Y 坐标逐点画出来。这套方案不需要time_buf它维护的是一个line_buf[64]每个元素记录当前这一行的波形在哪些列上有点。2.1.2 逐行动态刷新的关键把 Y 轴变成行号OLED 显示过程本质上是逐行扫描。以常见的 128×64、SSD1306 为例GDDRAM 分成 8 个 Page每 Page 8 像素高共 64 像素MCU 写显存时按 Page 寻址。这套源码里的oled.c就利用了这一点绘制波形时对于每一个采样点算出它在屏幕上的 Y 坐标再算出它属于哪个 Page、在该 Page 内的位掩码然后以列 掩码的方式直接写 GDDRAM。注意这里的关键不是先把一帧波形画到显存里再整体刷新而是采到一个点就把这个点的状态写进对应的 Page 行显示随采样实时推进。// 伪代码表示该项目绘制波形的核心思路 void DrawPoint_on_OLED(uint8_t x, uint8_t y) { uint8_t page y / 8; // 所在 Page0~7 uint8_t bit y % 8; // Page 内位偏移 OLED_SetCursor(x, page); // 设置列和页地址 uint8_t val OLED_ReadRAM(x, page); // 读回当前 GDDRAM 值 OLED_WriteRAM(val | (1 bit)); // 置位对应像素 }这段代码展示的是对 GDDRAM 做读-改-写的逐点操作。因为 OLED 控制器不支持仅写某一位必须先读回当前字节再改。OLED_SetCursor的x是列地址0~127page是页地址0~7两个参数把写入位置定位到具体 8×1 的格子OLED_ReadRAM是 I2C/SPI 读 GDDRAM 的操作很多驱动为了省事直接跳过这一步结果就是画点会把同页的旧内容覆盖掉波形出现残影。注意这套源码里frontoled.h和oled.c是分离的前者声明对外接口后者实现底层读写时序readme.txt里也标注了这种分层意图。读回这一步依赖具体屏幕控制器的读写时序SSD1306 在 I2C 模式下需要先发 0x80命令/数据标识再操作SPI 模式下用 D/C 引脚区分。2.1.3 滚动刷新左移一列比重绘整屏便宜得多波形要从右往左滚动最简单的做法是每来一个新采样点就把整屏 128 列全部重写一遍。但这样有严重的性能浪费SSD1306 的 GDDRAM 写操作在 I2C 400kHz 下写满一屏 1024 字节大约需要 20ms期间 ADC 还在持续采样。用这套源码的滚动方案每来一个新点只把最右边一列画上去再把整屏内容左移一列。注意这个左移不是读回整屏再移位写回而是用 SSD1306 的水平滚动指令或者直接改显示起始列地址实现——如果硬件支持连数据搬运都省了。下表整理了三种常见波形显示方案的 RAM 和总线开销对比这是选型时最容易忽略的部分方案显存操作额外 RAM每点 CPU 开销适用场景全屏重绘每点重写全部 128 列无高静态波形、拍照式显示二维数组缓冲 定时刷新刷新时整帧写入帧缓冲 256B 以上中需要回看、缩放一维行状态 滚动显示只写当前列 滚动行状态 64B ~ 128B低实时滚动本项目采用这套源码走的正是第三种line_buf[64]相当于把当前屏上哪些位置亮了的二维矩阵压缩成每一行是否有数据的一维状态数组。代价是你无法回溯历史波形只能看实时滚动。对入门学习而言这是可以接受的取舍——毕竟低频信号如 1kHz 以内的正弦波滚动显示完全够用而省下的 RAM 可以留给 DMA 缓冲和堆栈。2.1.4 双缓冲在这套方案里为什么是多余的很多人看代码时会问ADC 采完直接画屏会不会出现画到一半新数据来了的撕裂这里要区分采样速度和显示速度。ADC 采样率即使只有 10kHz每 100us 产生一个数据而 OLED 画一个点至少需要几十 us 的总线时间。直接画会导致采样数据丢失——ADC 的 DR 寄存器被覆盖。真正的瓶颈在 ADC 侧不在显示侧。解决思路是把 ADC 的数据交给 DMA由 DMA 把采样结果搬到一个环形缓冲里主循环再从缓冲取数据进行显示。这样显示速度慢一点不要紧DMA 保证采样不丢数据。双缓冲在这里没必要采样端和显示端通过环形缓冲解耦缓冲深度按显示一个点的时间内 DMA 会搬多少个数据来算。按 10kHz 采样、每画一个点耗时 200us 计算缓冲深度 8~16 就足够。这套源码没有显式给出环形缓冲的实现但既然adc.c里配置了 DMA实际逻辑必然是DMA 搬运 主循环取数的模式很多移植者喜欢把这套逻辑放进定时器中断里那是另一个可以跑通的路径代价是中断频率高、CPU 占用大。正确的做法是DMA 中断只在半满和全满时触发一次主循环批量取数。3. ADC 采集链路与 OLED 驱动从引脚到像素3.1 规则组 DMAADC 的中断频率问题看adc.c时最该注意的不是初始化顺序而是它采用了规则组连续采样 DMA 搬运的经典组合这是减少 CPU 中断次数的关键。如果每采一个样本进一次中断F103 在 72MHz 下处理一次 ADC 中断大约要几十个周期当采样率超过 20kHz 时 CPU 就会被中断淹没主循环几乎没有时间刷屏。DMA 的做法是 ADC 转换完成后硬件自动把结果搬到内存一次 DMA 传输完成后才中断一次。void ADC1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; DMA_InitTypeDef DMA_InitStructure; ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; // PA0 作为模拟输入 GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adc_buf; // 采样缓冲 DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize ADC_BUF_LEN; // 缓冲长度 DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; // 循环模式 DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel1, DMA_InitStructure); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; // 单通道不需要扫描 ADC_InitStructure.ADC_ContinuousConvMode ENABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发连续转换 DMA_Cmd(DMA1_Channel1, ENABLE); }几个参数要解释清楚。ADC_SampleTime_55Cycles5决定单次采样时间ADC 时钟按 12MHzAPB2 72MHz 的 6 分频计算55.5 周期对应约 4.6us 的采样窗口调小到ADC_SampleTime_1Cycles5可以让单次转换缩短到 1us 左右但采样电容充电时间不足输入阻抗高时会导致测量值偏小。DMA_Mode_Circular配合DMA_BufferSize很关键缓冲写满后 DMA 自动回绕到起始地址不用手动重置。DMA_PeripheralDataSize_HalfWord是因为 STM32 的 ADC 输出是 12 位存在 16 位寄存器里如果错配成 Byte高 8 位数据会丢失波形顶值会变 0x00FF。这里的adc_buf建议申请成uint16_t数组元素个数按采样率和主循环处理速度匹配常见做法是 128 或 256。这里还要提醒ADC_ExternalTrigConv_None表示完全软件触发ADC 转换完成后立刻开始下一次转换采样率由 ADC 时钟和采样周期决定近似 12MHz / (55.512.5) ≈ 176kHz。这个速率远超 OLED 的显示速度所以 DMA 缓冲一定会周期性满缓冲长度决定主循环最多可以拖延多久才丢数据。如果实际测出来的波形频率明显偏低或出现周期性断点优先检查 DMA 半满中断是否配置正确而不是怀疑 ADC 坏了。3.2 OLED 的接口选型这个项目的 SPI 与 I2C 取舍这套 OLED 驱动源码里oled.c没有明确的时序宏开关但frontoled.h和实际接线能看出它默认用的是四线 SPI 还是 I2C 取决于你的屏幕版本。这里说一个很多移植者踩过的坑SSD1306 控制器的 I2C 地址固定为 0x3C 或 0x3D取决于 SA0 引脚电平而 SPI 模式有四线DC CS SCK MOSI和七线额外 RES两种接法。F103 硬件 SPI1 的 SCK 最大 18MHzOLED 控制器 SPI 时序通常要求 SCK 空闲为高、数据在上升沿采样CPOL1, CPHA1或空闲为低、上升沿采样CPOL0, CPHA0——不同厂家的屏可能不同不能用一套参数通吃。遇到花屏先试SPI_CPOL_High | SPI_CPHA_Edge和SPI_CPOL_Low | SPI_CPHA_Edge两套组合比反复查硬件接线靠谱。void OLED_WriteCommand(uint8_t cmd) { OLED_DC_LOW(); // 命令模式 OLED_CS_LOW(); // 片选使能 SPI_I2S_SendData(SPI1, cmd); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); OLED_CS_HIGH(); // 片选释放 }注意while等待的是TXE标志而不是BSYTXE只代表数据已移入移位寄存器不代表发送完成。如果数据量大的时候出现漏字换成等待BSY标志释放。OLED_CS_LOW()到HIGH()之间的片选拉低时间必须大于一个 SPI 时钟周期有些国产屏要求 CS 低电平至少保持 50nsSPI 速度跑满 18MHz 时一个位周期约 55ns勉强够用如果你把系统时钟超频到 128MHzSPI 分频配置不变时实际速率可能超标此时要在两个命令之间插空操作。初始化时序上SSD1306 上电后必须先发关闭显示0xAE、设置显示时钟分频0xD5 0x80、设置多路复用比0xA8 0x3F、设置显示偏移0xD3 0x00、开启电荷泵0x8D 0x14、设置内存寻址模式0x20 0x00水平寻址、设置列地址范围0x21 0x00 0x7F、设置页地址范围0x22 0x00 0x07最后开启显示0xAF。这里最容易出问题的是内存寻址模式如果用垂直寻址0x20 0x01写数据时地址会自动跨页跳转画完第一列 8 行后跳到第二列而不是你直觉里的逐行——很多花屏现象根源就在这一条命令。该项目的oled.c初始化函数是按水平寻址来写的移植到别的屏幕时先确认这一句没被改掉。3.3 坐标映射ADC 原始值怎样变成屏幕上的 YADC 输出是 0~4095 的原始值OLED 的 Y 坐标只有 0~63中间隔着 64 级量化。简单的映射是y 63 - (adc_val * 64 / 4096)取反是因为 OLED 的 Y 轴向下为正而示波器习惯上幅值向上。这个映射只有 6 位有效分辨率意味着 4096 个 ADC 等级摊到 64 个像素上每个像素代表 64 个 ADC 等级信号稍微有一点噪声显示出来就变成上下跳动的粗线。解决方法是把 ADC 值做分段映射 死区判断。先把 ADC 值右移 6 位除以 64得到一个 0~63 的粗值这是它所在的水平带再判断当前采样值是否与上一次显示的 Y 相差超过一个阈值比如 ±2 个像素超过才更新屏幕点。这样做能滤掉小幅噪声但代价是真正的小信号变化也会被忽略。更好的做法是先对 ADC 数据做滑动平均再从 12 位映射到屏幕 Y。该项目的oled.c里没有明显的滤波函数你可以在主循环取数时加一句简单的adc_filtered (adc_filtered * 7 adc_new) / 8一阶低通的截止频率约为采样率 / (2 * PI * 8)对 10kHz 采样而言约 200Hz既能保留波形形状又能滤掉大部分毛刺。4. main 里的状态机从原始采样到完成波形4.1 系统初始化顺序有讲究main.c的初始化顺序是时钟、GPIO、DMA、ADC、OLED、串口最后是主循环。这个顺序不能随意调换原因在依赖关系OLED 初始化需要 SPI 外设和 GPIO 配置就绪ADC 初始化需要 DMA 通道已配置否则ADC_DMACmd使能时 DMA 可能处于未就绪状态ADC 转换完成的数据没有搬运目标而 DMA 需要时钟使能后才能操作寄存器。int main(void) { delay_init(); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); OLED_Init(); OLED_Clear(); ADC1_Init(); USART1_Init(115200); while (1) { uint16_t val Get_ADC_DMA_Value(); // 从 DMA 缓冲取最新采样 Draw_Waveform_On_OLED(val); // 映射并画到 OLED 上 Delay_us(50); // 人为限速控制滚动速度 } }这里的核心是Get_ADC_DMA_Value函数的实现方式。直接返回adc_buf[0]是错的——DMA 在循环模式下永远在写这个数组你读到的是当前 DMA 指针位置的旧值。正确做法是维护一个读索引每次读取 DMA 写指针之前的数据。由于 DMA 的CNDTR寄存器会递减循环模式下回到初始值后重新装载你可以用ADC_BUF_LEN - DMA_GetCurrDataCounter(DMA1_Channel1)得到当前写到了第几个元素再决定读哪个。如果读索引追赶上了写索引说明主循环太慢缓冲溢出此时应该丢弃旧数据而非覆盖否则显示的波形会出现时间轴扭曲。Delay_us(50)这行限速很重要。不加这个延时主循环会以最大速度刷屏OLED 的滚动速度会快到人眼无法辨认加了延时等于间接控制了水平扫描速度。假设每次循环显示一个采样点延时 50us 加上 SPI 写屏时间实际每点约 100us滚动全屏 128 列需要约 12.8ms对应约 78 帧/秒看起来就比较舒服。想做水平时间可调的示波器改这个延时值就对了——如果你想看更高频的信号把这个延时改小想看低频信号漂移加大延时。4.2 波形刷新状态机的三种状态OLED_Clear整屏清一次要写 1024 字节在 I2C 下要 20ms 以上导致波形出现闪烁。解决办法是把清屏改成只擦除上一列。按这个思路项目的主循环可以改造成如下的线性状态机这也是很多实际可用的 OLED 示波器固件的常见做法STATE_SCROLL当前列已画完把整个显示区域左移一列。STATE_SAMPLE从 DMA 缓冲取新采样点计算 Y 坐标。STATE_DRAW在最后一列画点更新行状态数组。void Draw_Waveform_StateMachine(uint16_t adc_val) { static uint8_t draw_col 127; // 当前绘制列 static uint8_t last_col_buf[8]; // 上一列 8 个 Page 的状态 uint8_t y (uint8_t)(63 - (adc_val 6)); uint8_t page y / 8; uint8_t bit y % 8; if (draw_col 127) { OLED_ColumnScroll(draw_col, page, bit); // 滚动只影响当前列 } else { OLED_ClearColumn(0, last_col_buf); // 清掉最左列 } OLED_DrawPoint(draw_col, page, bit); draw_col (draw_col 127) ? (draw_col 1) : 0; }这个写法把滚动和落点分开了OLED_ColumnScroll负责把整屏内容左移OLED_DrawPoint画新点OLED_ClearColumn处理移出屏幕的数据。注意last_col_buf必须保存上一列的每个 Page 的旧数据否则滚动后清屏会把新画上的点也一起清掉。这套状态机的优点是不需要完整帧缓冲任何时刻只关心当前列和上一列的状态和前面说的一维化策略一致。4.3 触发没有触发波形永远在乱飘不带触发的示波器显示正弦波时波形会像一条蛇一样左右游走因为每次采样起点在信号周期上的位置是随机的。标准的解决方案是上升沿触发检测到信号从低于阈值变到高于阈值的瞬间才开始绘制新的一列。这样每个周期的起点对齐波形稳定。uint8_t Trigger_Detect(uint16_t adc_val) { static uint8_t trig_state 0; // 0: 低电平1: 已触发 if (trig_state 0 adc_val TRIGGER_LEVEL) { trig_state 1; return 1; } if (trig_state 1 adc_val TRIGGER_LEVEL) { trig_state 0; } return 0; }这个士斯密特触发的简化版本有个 bug当信号本身在阈值附近振荡时会产生多次触发波形会被频繁重置。解决方法是加入滞回比较——高触发阈值和低触发阈值分开比如高阈值为 0.6×VREF低阈值为 0.4×VREF。信号从低于 0.4VREF 涨到高于 0.6VREF 才触发从高于 0.6VREF 跌到低于 0.4VREF 才复位。滞回区间能吸收一部分噪声但真正的噪声毛刺仍然可能误触发此时可以把触发判断放到采样数据里做 3 点中值滤波代价是触发点延迟一个采样周期对观察波形形状没有影响。这个项目的源码没有单独实现触发但main.c里判断阈值后绘制的方式是触发的一种朴素实现想要稳定的显示效果建议按上面的补全。5. 采样率标定、噪声排查和验证手段5.1 用定时器触发 ADC 得到准确的采样率前面提到ADC_SampleTime_55Cycles5对应约 176kHz 采样率但加上 ADC 校准误差和时钟偏差实际采样率可能偏离理论值 3%~5%。靠软件延时控制采样率在低频时误差更大。一个更可控的方法是用定时器产生 TRGO 事件触发 ADC这样采样率由定时器的 PSC 和 ARR 精确决定与主频偏差无关。void TIM2_ADC_Trig_Init(void) { TIM_TimeBaseInitTypeDef TIM_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_InitStructure.TIM_Period 72 - 1; // 1MHz 计数得到 1us 周期 TIM_InitStructure.TIM_Prescaler 72 - 1; // 72MHz / 72 1MHz TIM_InitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_InitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_InitStructure); TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); TIM_Cmd(TIM2, ENABLE); }定时器设置为更新事件输出 TRGOADC 的外部触发源选择ADC_ExternalTrigConv_T2_TRGO。此时采样率等于定时器更新频率72MHz / (72 × 72) ≈ 13.89kHz。按奈奎斯特定理这个采样率能观察的最高信号频率约 6.9kHz对入门示波器来说是合理范围。调整TIM_Period即可改变采样率不需要改 ADC 代码。这套源码没有实现定时器触发但readme.txt里提到了 ADC 采样实际移植时建议优先采用定时器触发的方式因为它让采样率成为一个显式的可调参数写文档和调波形都更明确。5.2 波形毛刺和锯齿的四种来源OLED 示波器显示出来的波形如果带毛刺或者有规律的锯齿优先查这四类问题第一电源噪声。F103 的 ADC 参考电压是 VREF如果你的板子直接把 VREF 接到 3.3V LDO 输出而 LDO 纹波本身就有 50mV 级别ADC 结果必然带噪声。解决办法是 VREF 与 VREF- 之间加 0.1uF 和 10uF 电容或者干脆用独立基准源。第二输入引脚悬空。PA0 作为 ADC 输入但没有外接信号时引脚电平由内部漏电流决定会显示近似随机数的跳动波形。调试时可以先把 PA0 接到 GND 和 3.3V分别观察显示的直线是否平直。第三DMA 与 CPU 竞争 AHB 总线导致部分采样点被延迟读写。把 DMA 优先级设为 High同时注意不要在 DMA 中断服务函数里做耗时操作尤其避免调用OLED_Clear这类整屏写操作。第四OLED 本身的刷新噪声。SSD1306 内部电荷泵会产生周期性干扰如果 ADC 输入信号和 OLED 的 SCL/SCK 线靠得太近屏幕刷新时会把串行时钟噪声耦合进信号线。布线时让 ADC 输入走模拟地隔离区域与数字信号线保持 5mm 以上距离。代码层面oled.c里写 GDDRAM 时频繁开关 SPI 片选也会产生地弹噪声。可以改成连续写多个字节再拉高 CS减少电平跳变的次数。配合前面说的一阶低通滤波能把大部分数字噪声压下去代价是信号的高频成分被衰减对 1kHz 以内的正弦波影响不大。5.3 验证方法串口回传 Python 画波形第一板跑通后怎么判断显示出的波形是真实的信号而不是代码 bug你需要把 ADC 数据通过串口发回 PC用 Python 画一张参考波形对比。115200 波特率 8N1 格式下每帧数据如果只发 2 字节原始数据加换行符最大有效数据速率约 11.5KB/s对应约 5.7k 采样点/秒。也就是说你不可能把 13.89kHz 的采样数据全量实时发出来只能抽帧——每 N 个数据取一个发出去或者发一小段 128 点的窗口。import serial import matplotlib.pyplot as plt ser serial.Serial(COM12, 115200, timeout1) raw [] for _ in range(512): line ser.readline().strip() if line: raw.append(int(line, 16)) plt.plot(raw) plt.title(STM32 OLED Scope - ADC Raw Data) plt.xlabel(Sample Index) plt.ylabel(ADC Code) plt.show()对应 MCU 侧发数据时要注意printf重定向到串口后%d格式化会占用大量 CPU 时间在 ADC 采样过程中调用 printf 会拖慢主循环导致采样不均匀。常见做法是先关中断、把整数转成十六进制字符串放进固定缓冲区再开中断、启动 DMA 发送。串口接收端看到的波形如果与 OLED 上一致说明 ADC 采集链路和映射公式没问题如果 OLED 上波形被压扁或削顶检查y 63 - (adc_val * 64 / 4096)的整数溢出——adc_val是 16 位无符号整数乘以 64 后最大值为 262080已经超出uint8_t范围必须用uint16_t或uint32_t中间变量否则结果会被截断成 64 的倍数波形变成一块块的台阶。这个问题在移植到 8 位 MCU 时尤其容易踩int是 16 位的旧 C51 工程和 32 位的 F103 行为完全不同代码里所有涉及乘法的地方都要检查中间变量类型。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询