简介基于STM32F407从零制作的简易示波器完整工程源码适合嵌入式初学者、单片机爱好者及需要参考ADC采集与LCD显示方案的开发者。资源共171个文件以73个C源码和65个头文件为主另含14张原理或实物PNG图片、Keil工程配置与hex烧录文件压缩包整体仅1.18MB结构清晰便于查阅。已有3000余人学习下载项目实现一路ADC定时采集、继电器切换放大电路倍数、独立按键与状态灯控制采样周期和电压范围并将波形实时绘制到3.2寸TFTLCD上同时计算周期、频率、峰峰值等示波器参数。软件采用主循环采集中断定时界面更新的架构代码分层明确可直接烧录验证也可作为STM32F407外设驱动、数据处理与波形显示类项目的移植参考。1. 一块F407能当示波器源代码级拆解一台简易示波器很多人第一次听到“基于STM32F407的简易示波器”都会愣一下一颗MCU不带FPGA不带专用ADC芯片真能把波形画出来答案是能而且不是玩具级别的能。F407那颗168MHz的内核、3个12位ADC、2个DMA控制器和丰富定时器资源恰好是“中等带宽、低成本的调试利器”的水桶配置。用它能测音频信号、PWM输出、电源纹波、传感器波形带宽做到几百kHz不加前端、几MHz加了前端也不虚。这篇文章不讲那种只能看个呼吸灯的演示程序而是把一套能投入日常调试的简易示波器方案从硬件选型、ADC采样、DMA缓冲、触发显示到软件校准拆开讲直接落到源代码上。适合正在做课程设计、实验室仪器改造或者想在产品里塞一个自检波形显示功能的人。看完你能独立搭一套也知道坑在哪、参数怎么调、性能天花板在哪。2. 硬件选型不只看芯片ADC、显示与信号调理的取舍2.1 内置ADC还是外置ADC先算带宽再谈“高速”F407内置3个独立ADC每个都能跑到2.4Msps兆采样点每秒。听起来不高先算一笔账示波器的有效带宽大约是采样率的1/10到1/5。2.4Msps意味着能比较靠谱地看到240kHz以内的信号勉强能“看出形状”到480kHz。这个能力已经覆盖了大量调试场景串口波形、I2C时序、PWM占空比、音频信号、电源开关噪声、传感器输出。如果你测的是能动辄10MHz的有源晶振那内置ADC直接不适用乖乖去选外置并行ADC或串行高速ADC比如常见的8位并行ADC接FSMC能做到50Msps以上但那已经不是“简易”的范畴了。我的判断标准很直白被测信号频率低于200kHz用F407内置ADC高于200kHz要么加采样保持电路要么换方案。别指望靠软件插值补出带宽那是自欺欺人。另外一个关键点F407的ADC是逐次逼近型SAR它的输入带宽和采样保持时间有限测高频信号幅度会衰减。你拿它测一个1MHz方波看到的边缘肯定圆了这是物理限制不是代码问题。内置ADC还有一个隐藏优势它和多通道扫描、DMA是天然配合的。用定时器触发ADC转换完自动进DMACPU全程不参与搬运这对后面做显示、UI、按键响应非常重要。外置ADC反而要自己折腾并行时序或SPI同步问题复杂度会上一个台阶。2.2 单通道还是多通道扫描模式与规则组的取舍F407的每个ADC有一组规则通道最多16个和一组注入通道最多4个。规则通道适合“一个接一个依次转换”对应的是多通道示波器注入通道适合“被打断也要优先转换”对应的是突发单点捕捉。简易示波器最常见的是单通道其次是双通道。做双通道时有个非常容易翻车的点两个通道共用一个ADC时切换通道需要稳定的建立时间通道间会串扰尤其当一个信号强一个信号弱时弱信号会被强信号“拖拽”。F407的做法是给两个通道分别配置采样时间强信号通道用短采样时间弱信号通道用长采样时间能把串扰压下去一些。但说实话实验室里用的那种正规双通道示波器是两个独立ADC前端F407的可比性有限。我一般建议真需要双通道且频率都不低直接上F407的两个独立ADC触发分别配置互不干扰只是看个大概就用一个ADC扫描两个通道软件分帧。别把“双通道”想得太美简易方案默认单通道稳定第一。如果用两个ADC两个转换结果要同步对应到同一时刻可以靠硬件触发或软件帧号对齐这也是一个小工程点后面会提。2.3 模拟前端不是玄学阻抗、衰减、钳位与驱动很多初版示波器做出来波形乱飞第一反应是怀疑ADC其实八成问题在模拟前端。F407的ADC输入阻抗不高内部采样电容会在转换瞬间吸取电流如果输入源阻抗太大比如直接接了一个高阻分压网络采样值就不稳定。常见做法是在ADC输入端加一级运放跟随器比如用双运放搭一个单位增益缓冲输入阻抗拉高到MΩ级输出阻抗压低到几十欧姆ADC采样电容充电就快。运放选型看带宽和压摆率测100kHz信号至少选1MHz增益带宽积的型号轨到轨输入输出更好否则接近电源轨时波形削顶。然后是无源衰减网络。探头最常见的1x/10x开关就是这个思路1x直通10x先衰减10倍。MCU片内ADC量程通常0到3.3V但你要测的可能是5V、12V甚至更高的信号。这时候必须加分压电阻。我一般用9:1分压对应3.3V满量程时能测33V前端再串一个限流电阻。分压之后信号幅度变小需要再放大一级这和示波器的垂直灵敏度档位对应。换句话说模拟前端至少两级衰减/增益可选再加缓冲。再往后必须有钳位保护用两个二极管把输入电压钳在0V到3.3V之间串联电阻限流防止接错线烧芯片。这一步不做你的F407随时可能因为探头碰到高压而报废这是真实踩过的坑不值得再踩第二次。F407的ADC引脚没有内部钳位能力输入超过VDD或低于VSS就会通过寄生二极管漏电严重时直接锁死或损坏。2.4 显示走直驱还是外部屏驱帧率与成本的平衡显示方案决定了整个系统的架构。F407常见有几种选择一是小尺寸TFT LCD屏直接并口/SPI驱动二是带控制器的大屏用RGB接口三是不带屏把波形数据通过串口或USB发给上位机显示。简易示波器的体验七成在刷新率上刷新率上不去看起来就是卡顿的PPT。小尺寸TFT直驱比如2.4寸、2.8寸带ILI9341控制器的屏用FSMC并行接口驱动可以把像素写入速度提上去一帧80x160分辨率的波形区域大约几毫秒就能刷完。SPI驱动则慢一些好处是省引脚坏处是刷全屏明显能看出扫描过程。还有一种常见做法是波形区只占屏幕一部分其他区域显示网格、菜单和参数每次只刷新波形区那块内存而不是整屏重绘速度能提升一倍以上。如果你更看重信号分析而不是屏幕显示把数据发给上位机在电脑上画波形是更省事的路。F407自带USB OTG用CDC类虚拟串口加上一个简单的帧协议就能把采样数据流式传到PC。这种方式绕开了MCU刷屏的算力瓶颈上位机画图可以用任何现成库还能做FFT、保存数据文件扩展性最好。硬件成本也最低不用买屏。3. 把采样和DMA写进代码触发、缓冲与时间轴的配合3.1 F407上ADCDMA的最小配置代码这节直接给代码。使用STM32标准外设库来写方便移植如果用的是HAL库逻辑一致把函数名换掉即可。核心动作有三步开ADC时钟、配置ADC规则通道、配置DMA循环模式。void ADC_Config(void) { ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM8, ENABLE); // ADC1 挂在 APB2 上默认 84MHz分频后给 ADC 时钟 21MHz ADC_InitStructure.ADC_Resolution ADC_Resolution_12b; ADC_InitStructure.ADC_ScanConvMode DISABLE; // 单通道不扫描 ADC_InitStructure.ADC_ContinuousConvMode ENABLE; // 连续模式等定时器触发 ADC_InitStructure.ADC_ExternalTrigConvEdge ADC_ExternalTrigConvEdge_Rising; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T8_TRGO; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfConversion 1; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_84Cycles); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); } void DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_Channel DMA_Channel_0; DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_Memory0BaseAddr (uint32_t)adc_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralToMemory; DMA_InitStructure.DMA_BufferSize ADC_BUF_SIZE; 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_InitStructure.DMA_FIFOMode DMA_FIFOMode_Disable; DMA_Init(DMA2_Stream0, DMA_InitStructure); DMA_Cmd(DMA2_Stream0, ENABLE); }逻辑说明ADC1选择12位分辨率、单通道、由TIM8的TRGO上升沿触发外部转换这样采样时刻完全由硬件定时器控制不会因为中断延迟抖动。DMA配置成循环模式ADC转换完成一次就自动搬运一次到adc_buf缓冲区填满后从头再写CPU不需要干预。adc_buf是用__align(32)修饰的16位数组大小通常设成1024或2048。参数说明ADC_SampleTime_84Cycles表示每个采样点保持84个ADC时钟周期采样时间越长输入阻抗要求越低但最高采样率越低。84MHz的APB2分频给ADC的21MHz时钟下84周期的采样时间大约4微秒一个点对应约238kSPS。要提速就减采样时间或提高ADC时钟分频自己按需调。3.2 用定时器触发ADC而不是软件打点把ADC配置成外部触发而不是连续转换好处在于采样间隔由硬件决定波形在时间轴上分布均匀。TIM8或TIM1这些高级定时器能输出TRGO事件触发ADC启动。void TIM8_Base_Config(uint32_t sample_rate) { TIM_TimeBaseInitTypeDef TIM_InitStructure; // 定时器时钟 168MHz分频后得到 1MHz 计数频率 TIM_InitStructure.TIM_Prescaler 168 - 1; TIM_InitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_InitStructure.TIM_Period (1000000 / sample_rate) - 1; TIM_InitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_InitStructure.TIM_RepetitionCounter 0; TIM_TimeBaseInit(TIM8, TIM_InitStructure); TIM_SelectOutputTrigger(TIM8, TIM_TRGO_Update); TIM_Cmd(TIM8, ENABLE); }逻辑说明定时器向上计数每次更新事件同时产生一个TRGO信号给ADCADC收到上升沿就开始一次转换。转换结束后DMA自动搬运结果整个过程都是硬件链路不需要进入中断。sample_rate参数很好理解1MHz除以预分频和周期就是你想要的每秒采样点数。参数说明这里的预分频168-1是为了把168MHz的系统时钟降到1MHz这样通过周期寄存器直接写采样率数值比如sample_rate100000就得到100kSPS可读性很好。需要注意一点ADC不是无限快的外部触发频率不能超过ADC自身的最大转换速率。如果你把采样率设到1MSPS但ADC配置的采样时间是84周期转换根本来不及完成会出现DMA搬走旧数据、新数据还没好的错位。所以调采样率的同时要回头看一眼ADC采样时间配置。3.3 中断、DMA与缓冲谁能动、谁在等简易示波器的数据链路有三段ADC产生数据、DMA搬运数据、CPU处理数据。很多人一上来就在ADC中断里读数据寄存器这是最容易把采样率做不高的写法。进入中断至少几百个周期中断里如果再做浮点运算或屏幕输出采样间隔会被扯得乱七八糟波形在横轴上忽宽忽窄。正确思路是让DMA把数据搬到内存CPU只在需要显示时去读缓冲区或者等DMA半满/全满中断再处理。F407的DMA可以配置传输完成中断还会给你“半传输”和“全传输”两个标志正好用来做双缓冲处理。void DMA2_Stream0_IRQHandler(void) { if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_HTIF0)) { // 前半段缓冲区已填满可以处理同时后半段还在填 process_buffer(adc_buf, ADC_BUF_SIZE / 2); DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_HTIF0); } if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TCIF0)) { // 后半段缓冲区已填满此时前半段是稳定的 process_buffer(adc_buf ADC_BUF_SIZE / 2, ADC_BUF_SIZE / 2); DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TCIF0); } }逻辑说明DMA在循环模式下不断往缓冲区写数据。当写到缓冲区一半时触发HT中断写满全部触发TC中断。两个中断交替发生每次处理的都是DMA当前不在写入的那半块内存天然避免了读写冲突。process_buffer里做波形处理和显示处理完直接返回不能在里面做等待操作。参数说明ADC_BUF_SIZE建议设置为2的幂比如1024或2048因为DMA的传输数量和内存对齐都和边界有关。缓冲区越大单帧数据越完整但处理延迟越高缓冲区太小又会频繁中断挤占CPU。这个值配合你的采样率来选保证一帧数据对应屏幕上1到2个完整周期的信号看起来最舒服。3.4 采样率、点数与时基三者的换算波形显示有一个基本关系屏幕横轴显示多少时间取决于采样点数和采样率的比值。这一帧数据对应的时间窗长度 ADC_BUF_SIZE / sample_rate。时基档位每格多少毫秒就是在调度这个关系。假设缓冲区2048点采样率200kSPS一帧就是10.24ms。如果屏幕波形区域横向分成100格每格大约0.1ms。这个方法反过来用更实用你希望看到100Hz信号在两个周期左右就先算好时间窗要20ms然后反推采样率。调档位的代码不复杂就是改TIM8的Period寄存器同时保持其他配置不动。有一个常见错误需要提前提醒改定时器周期时如果正在数据传输中当前帧的前后两段采样率会不一致画出来的波形会突兀地变形。解决办法是改完采样率后清掉DMA缓冲的“正在使用”标志强制从新起点开始采而不是接着旧数据画。4. 波形显示与触发简易示波器不简陋的两个关键4.1 波形显示整屏重绘、DMA刷屏与局部刷新屏幕刷新是体验感的大头。先用一组对比看方案差异显示方式帧率约特点适用场景SPI全屏重绘10~15 FPS简单能看到扫描过程学习验证FSMC并行接口波形区域局部刷新30~50 FPS流畅不闪正经的简易示波器DMA2D批量搬运显存60 FPS几乎不占CPU波形区较大、带显存的屏我实际做得最多的方案是“局部刷波形区”。屏幕固定区域显示网格、刻度、档位文字这些内容只在切档时重绘一次平时只更新波形区域的像素。做法是把一个和波形区等大的显存数组专门存波形每次DMA到一半时算好432个点的Y坐标写进这个显存再整体复制到LCD等待区。如果用的是带显存的控制器比如ILI9341可以直接往它的GRAM对应区域写不用重启整屏。这里有个容易偷懒又容易翻车的地方很多人图省事每次都全套调用一次清屏函数把整块屏全写成背景色再画线。显示一帧要做2次全屏操作帧率直接砍半还伴随明显的闪烁。把清屏范围限制在波形区效果立竿见影。4.2 边沿触发拿什么判断“这一帧该画了”波形稳定显示的前提是触发。没有触发的示波器屏幕上看到的是一堆乱跑的波形因为你每一帧的起始点不固定。触发的作用是等到信号满足某个条件比如电压从低到高穿过阈值再开始采集这一帧。uint16_t find_trigger_index(uint16_t *buf, uint16_t size, uint16_t level) { for (uint16_t i 1; i size; i) { // 寻找上升沿跨越阈值的点前一时刻低于阈值当前时刻不低于 if (buf[i - 1] level buf[i] level) { return i; // 返回触发点索引 } } return 0xFFFF; // 没找到返回无效标志 }逻辑说明每次DMA半满中断拿到一段数据后先在这个块里找第一个满足条件的索引把这个点作为波形显示的起点。这样每帧都从同一个电压相位开始画波形就“钉”在屏幕上了。参数说明level是触发阈值的ADC原始值和电压对应关系是 level 目标电压 / 3.3V × 4095。上升沿只看前一个点低于、当前点高于下降沿反过来即可。实际信号有噪声时这个简单判断会抖动可以在判断里加一个小迟滞窗口比如前一点比阈值低50码当前点比阈值高50码才判为触发抖动会明显减少。4.3 纵轴量程与横轴时基旋钮和档位的软件实现量程切换调的是信号进来后放大多少倍、显示时Y坐标怎么映射。Y坐标映射公式很简单先把ADC值换算成电压再按当前量程缩小最后映射到屏幕高度。uint16_t voltage_to_y(float voltage, float range_per_div, uint16_t screen_height) { float volts_per_pixel range_per_div * 4 / screen_height; uint16_t y (uint16_t)((voltage range_per_div * 2) / volts_per_pixel); if (y screen_height) y screen_height - 1; // 超出边界钳位 return screen_height - 1 - y; // 屏幕Y轴方向反转 }逻辑说明range_per_div是每格电压值比如0.5V/div乘以4格屏幕纵向显示8格的一半或全部按你的布局定得到满量程电压范围。把电压负半幅加进来再加一个平移量就能把波形居中或上下挪动。这个函数在后面自动校准时也会复用是显示模块的核心。参数说明voltage是前面从ADC原始值换算出的实际电压用float方便操作screen_height是波形区像素高度拿这个值参与运算可以做到任意屏幕尺寸通用。横轴时基的核心是改定时器周期。我这里建议把所有档位定义成一张表而不是每次用浮点运算去算0.1ms/div、0.2ms/div、0.5ms/div、1ms/div、2ms/div、5ms/div、10ms/div。切档时直接查表设置TIM8的Period同时把显示“每格对应多少点”的系数更新。这样逻辑最简单代码也最容易调试。4.4 数据打包到上位机虚拟串口与帧格式如果决定不装屏走虚拟串口通道那代码工作量会转移到上位机。我建议的帧格式很简单帧头数据长度采样率数据体校验。数据体可以直接丢ADC原始值上位机负责换算电压这样MCU端不需要做浮点运算。#define FRAME_HEADER 0xAA55 #define FRAME_MAX_LEN 2048 typedef struct { uint16_t header; uint16_t length; uint32_t sample_rate; uint16_t data[FRAME_MAX_LEN]; uint8_t checksum; } WaveFrame; uint8_t frame_checksum(uint8_t *data, uint16_t len) { uint8_t sum 0; for (uint16_t i 0; i len; i) sum data[i]; return sum; }逻辑说明数据结构体直接对应一帧波形数据。发送时先把结构体指针转成字节数组加上校验字节通过USB虚拟串口发出。上位机收到后先校验再解析出采样率和数据按时基画图。校验用简单的累加和就够了毕竟同轴传输丢包概率低主要防的是粘包和半包。参数说明length用16位最大支持65535个数据点够用如果以后要扩展高速数据流建议把帧头改成4字节同步字并加帧序号字段上位机能识别丢帧。USB虚拟串口的波特率概念是虚拟出来的实际吞吐由USB全速决定大约1MB/s的预算传200kSPS的16位数据绰绰有余。5. 示波器避坑采集逻辑没错波形却见不得人5.1 现象1波形上下飘像在“荡秋千”现象输入一个稳定的直流电压屏幕上的线不是平的而是缓慢上下漂移幅度可能有几十个码。原因ADC的参考电压VREF没有处理好。很多开发板直接把VREF接到了3.3V电源上而这个3.3V来自板载LDO或DCDC纹波和负载变化都会直接叠加到ADC结果里。另外滤波电容离MCU太远或者没接也会导致参考不稳。解决最可靠的办法是给ADC用独立参考源。如果不想加芯片就把VREF引脚单独走线加一个100nF和10uF的电容组合且电容尽量靠近引脚。同时检查是不是有电机、继电器或大电流外设在ADC采样时切换这一类瞬态干扰也会表现为低频漂移。软件上可以加一个滑动平均滤波器但要小心它会磨平快速变化的边沿只对慢信号有效。真正治本还是硬件上把参考电压做干净。5.2 现象2高频信号幅度只有实际的一半现象测1kHz方波幅度正常测100kHz方波峰峰值明显变小边缘也变成斜坡。原因这不是算法问题是模拟前端带宽不够。F407的ADC采样保持电路对高频信号的跟踪能力有限采样时间内输入电容没充到最终值就开始转换了。另一个常见原因是前端运放的增益带宽积选低了10倍放大配置下100kHz信号需要至少1MHz带宽的运放很多通用低速运放到几十kHz就开始衰减。解决适当缩短ADC采样时间让它更快地完成对采样电容的充电。但这只能改善一点点治本要看模拟前端。测量前端各级的带宽哪一级是最短板就换哪一级。运放换成增益带宽积在10MHz以上的型号输入缓冲拓扑不要用简单的同相放大注意补偿电容。如果想测更高频就需要加射频前端电路那就超出简易示波器的定位了。5.3 现象3触发失灵波形在屏幕上来回跑现象输入一个稳定方波触发阈值设在中点但波形有时稳有时飘还偶尔变成一团乱线。原因触发点找错了。前面代码里find_trigger_index遍历的是DMA缓冲区的“段”如果这一段的起始位置已经处于信号高电平阶段前半段可能找不到穿越点就会返回无效标志显示模块只能从头画相位自然对不上。还有一种情况是噪声比较大触发点每次抖动几百个码波形左右位置不稳定。解决处理buffer时如果找不到触发点不要画这一帧直接等待下一段。视觉上只是偶尔少画一帧但波形稳定性和连续性会好很多。触发点抖动问题给触发比较加迟滞窗口上一段数据里先找一次趋势方向连续两个点满足条件才确认触发。如果信号本身含噪严重可以在触发判断前先对原始数据做一个轻度中值滤波只影响触发点位置不改变原始数据。5.4 现象4切换量程后直流偏置突然变了个样现象用1V/div看一个信号基线在屏幕中央偏上一点切到0.2V/div基线跑到屏幕顶部甚至消失波形削顶。原因量程切换后如果前端是固定增益而软件只改映射系数那么信号超出当前量程是很正常的不叫偏置变化。但如果前端是有增益级的切换继电器或模拟开关后运放的输入失调电压也被放大了直流偏置点跟着变波形自然整体跑偏。解决给每个量程档做一次零点校准切换档位后先采集接地通道的数据算出当前档位的零点偏移码显示时减掉。这个校准不只是开机做一次建议每次切档都快速做一遍成本极低效果明显。硬件上运放选低失调电压的型号能从根本上减轻这个问题。模拟开关切换后要留几毫秒稳定时间再开始采样不能切完立即采。6. 量程切换与零点校准越用越顺手的自动校准函数简易示波器最容易被人诟病的一点就是“不准”。不准的根源基本有两个零点漂移和增益误差。零点漂移来自运放的输入失调电压增益误差来自分压电阻精度和运放增益精度。给系统加一个自动校准函数把这组误差修掉测量精度能上一个台阶而且代码不多。这个功能相当于给示波器装了一副“自动对焦眼镜”每次换档位自动校一遍使用者完全无感但波形从“大概看看”变成“敢读数”。校准思路不复杂。F407内部有一个连接到ADC的参考电压通道大约1.2V左右精度可以接受。利用它算增益系数先采内部参考通道得到码值N1再接一个稳定的外部参考比如直接用3.3V电源分压成2V得到N2两个码值相除和理论值的比值就是增益校正系数。零点校准更简单把输入端接地采一段数据平均后得到零点码值存在校准结构体里。显示时执行两个线性变换先减零点再乘增益系数。typedef struct { float gain_correction; uint16_t zero_offset; uint16_t raw_value; } Calibration_t; uint16_t apply_calibration(Calibration_t *cal, uint16_t raw_adc) { int32_t corrected (int32_t)raw_adc - cal-zero_offset; corrected (int32_t)(corrected * cal-gain_correction); if (corrected 0) corrected 0; if (corrected 4095) corrected 4095; return (uint16_t)corrected; }逻辑说明apply_calibration在每次显示前调用把原始ADC值从“有点漂的码值”修正成“和真实电压成线性关系的好码值”。先减零点再做增益修正顺序不能反否则零点偏移会被增益一起放大。参数说明zero_offset是接地时采到的平均值gain_correction是理论码值除以实际码值得到的系数比如理论上是2.00V对应2482个码实测只有2450增益系数就是2482/2450≈1.013。返回时做边界钳位避免超出12位ADC的范围。我第一次做示波器时直接拿开发板上的ADC采波形显示在屏幕上所有波形看起来都“头发丝一样细”一到直流档位就开始飘。后来花了一个晚上加了这组校准函数波形基线从±30个码稳定到±2个码以内测量误差也明显降下来。从那以后我养成了一个习惯凡是带ADC采样做显示的嵌入式项目都会预留一个自校准入口哪怕只是“开机测一次零点”这种最轻量的做法都能省掉大量调试时对不准信号的烦恼。这算是这个项目里投入产出比最高的一次改动希望帮到你。本文还有配套的精品资源点击获取