简介:这份资源是面向嵌入式初学者与单片机开发者的STM32F103C6示波器项目源码,基于Keil5工程组织,核心思路是利用ADC采集模拟电压信号、通过DMA自动搬运转换结果以减轻CPU负担,再借助USB HID协议将数据实时上传至PC端显示波形,适合作为信号采集与实时传输方向的学习案例。压缩包共969个文件,以572个c源文件、257个h头文件为主,另含51个汇编文件、34个crf与28个icf等编译链接配置,以及uvprojx工程、ioc配置、说明文档和Middlewares中间件目录,整体约11.72MB,工程结构完整、模块划分清晰。目前已有922人学习下载。读者可据此理解ADC+DMA+HID的完整数据链路,掌握Keil5工程配置与驱动组织方式,并在此基础上扩展滤波、存储与波形分析等功能。
1. 一块 F103C6 最小系统板,怎么就成了桌面上的简易示波器
手头只有一块 stm32F103C6 最小系统板和一根 USB 线,却想抓一下串口波形、看看 PWM 占空比、量个传感器输出的毛刺——买台入门示波器要几百块,等快递还要几天。这个项目的思路很直接:把 STM32 自带的 12 位 ADC 跑起来,配合定时器触发采样,把数据存进内存,再通过屏幕或串口把波形画出来,一块十几块钱的单片机就变成了能看波形的工具。它适合正在学 STM32 外设、想搞懂 ADC + DMA + 定时器联动的人,也适合手头缺仪器、需要临时看个慢速信号(音频以下、几十 kHz 以内)的开发者。源码是 Keil5 工程,打开就能编译下载,改采样率、改触发方式都在几个寄存器配置里,二次开发门槛不高。
2. 项目定位与技术选型:为什么是 F103C6 + ADC + DMA 这套组合
2.1 硬件资源盘点:C6 到底够不够用
STM32F103C6 属于 F103 系列里的低配型号,Flash 32KB、SRAM 10KB,比常见的 C8T6 少一半。很多人第一反应是“这点内存能跑示波器?”——关键看你怎么用。示波器的核心开销是采样缓冲区,如果按 12 位精度、单通道、1024 点缓冲算,也就占 2KB(每点 2 字节),10KB 的 SRAM 完全放得下,甚至能开双缓冲做乒乓采集。真正吃资源的是屏幕刷新和波形绘制,如果外接 OLED 或 TFT,刷屏本身会占掉不少 CPU 时间,所以这个项目在架构上把“采集”和“显示”拆成两条线:采集靠 ADC + DMA 在后台自动搬运,CPU 不参与;显示在主循环里做,画完一帧再处理下一帧。这样即使 C6 主频只有 72MHz,也能跑出稳定的采样节奏。
外设方面,F103C6 有一个 12 位 ADC,支持规则通道和注入通道,规则通道最多 16 个外部输入。项目里通常只用其中一两个通道,比如 PA0 或 PA1,接被测信号。ADC 的时钟来自 PCLK2 分频,最高不能超过 14MHz,所以 72MHz 主频下一般设 6 分频,得到 12MHz。采样时间可配,从 1.5 个周期到 239.5 个周期,采样时间越长,输入阻抗要求越低,但转换速度越慢。这些参数在代码里都是宏定义,改起来直观。
提示:C6 的 SRAM 只有 10KB,如果开双缓冲 2×1024 点,再加上栈和全局变量,基本就到顶了。想加 FFT 或更长的缓冲,要么换 C8T6,要么把采样深度降到 512 点。
2.2 采样链路设计:定时器触发 + ADC + DMA 的闭环
示波器最怕采样间隔不均匀,波形会抖。用软件循环里手动启动 ADC 转换,间隔受中断和循环影响,抖动很大。这个项目的做法是让 TIM 产生固定频率的触发信号,去触发 ADC 转换,ADC 转换完成后通过 DMA 把数据搬到内存,全程不需要 CPU 干预。链路是这样的:
- TIM2 或 TIM3 配置为 PWM 模式或输出比较模式,产生周期性的 TRGO 事件;
- ADC 配置为外部触发模式,触发源选对应的定时器 TRGO;
- ADC 开启 DMA 请求,每次转换完成自动搬运;
- DMA 配置为循环模式,搬满缓冲区后自动回到开头,或者配置为正常模式,搬满后产生中断,在中断里置标志。
这样采样率就由定时器的频率决定。比如 TIM2 挂在 APB1 上,时钟 72MHz,设预分频 PSC=0,自动重装 ARR=71,那更新频率就是 72MHz/(71+1)=1MHz,也就是每秒 100 万次触发。但 ADC 最快转换时间也要 1μs 左右(12MHz ADC 时钟,采样时间 1.5 周期 + 转换 12.5 周期 ≈ 14 个周期 ≈ 1.17μs),所以实际采样率上限在 800kS/s 左右。对于看音频信号(20kHz 以内)绰绰有余,看几十 kHz 的 PWM 也能凑合。
2.3 显示与交互方案:屏幕还是串口
项目正文没写具体显示方式,但常见做法有两种:一种是接 0.96 寸 OLED(I2C 或 SPI),在单片机里画波形;另一种是把数据通过串口发给上位机,用电脑软件画。两种方案各有取舍。
OLED 方案的好处是脱机运行,一根线都不用接电脑,适合现场调试。缺点是屏幕分辨率低(128×64),一屏只能画 128 个点,需要做抽点或滚动显示。而且 OLED 刷屏慢,I2C 尤其慢,刷一屏要几十毫秒,会限制波形刷新率。串口方案的好处是显示能力强,上位机可以用 Python 或 LabVIEW 画,还能做测量和存储。缺点是必须连着电脑,不够便携。
这个项目既然是 Keil5 工程,大概率是 OLED 方案,因为串口方案通常要配一个上位机软件,而标题里只提了单片机源码。代码里应该有一个 OLED 驱动文件,负责初始化屏幕和画点、画线。如果你手头是 TFT 屏,改一下驱动就行,绘图逻辑不用动。
2.4 工程结构:Keil5 里都有哪些文件
一个典型的 Keil5 工程目录大概长这样:
Project/ ├── CMSIS/ # 内核头文件 ├── FWLIB/ # 标准外设库 │ ├── inc/ │ └── src/ ├── USER/ │ ├── main.c # 主循环、初始化 │ ├── stm32f10x_it.c # 中断服务函数 │ └── system_stm32f10x.c ├── HARDWARE/ │ ├── ADC/ │ ├── TIM/ │ ├── DMA/ │ └── OLED/ └── Project.uvprojx # Keil 工程文件标准外设库版本一般是 V3.5.0,这是 F103 最常用的库。如果你习惯用 HAL 库,也可以自己移植,但标准库在 F103 上资料最多,出问题好查。HARDWARE 文件夹里每个外设一个子目录,里面是 .c 和 .h,这种分法在正点原子、野火等教程里很常见,好处是移植方便,换个芯片只用改底层。
3. 跑起来:从 Keil5 编译到第一次看到波形
3.1 环境准备与工程打开
先把 Keil5 装好,注意要装 MDK 和对应的器件支持包。F103C6 属于 STM32F1 系列,装 Keil.STM32F1xx_DFP 这个包就行。装完之后打开工程文件,在 Options for Target 里检查几项:
- Device 选的是 STM32F103C6;
- Target 里的晶振频率填 8MHz(外部晶振)或 72MHz(系统时钟);
- Output 里勾上 Create HEX File,方便用串口下载;
- Debug 里选 ST-Link 或 J-Link,取决于你用的下载器。
如果编译报错说找不到 stm32f10x.h,说明头文件路径没加对。在 C/C++ 选项卡的 Include Paths 里把 CMSIS、FWLIB/inc、USER、HARDWARE 下各子目录都加进去。这一步是新手最容易翻车的地方,路径少一个就报一堆未定义。
3.2 关键配置:采样率、触发方式和缓冲区
采样率由定时器决定,代码里一般会封装成一个函数:
// TIM2 初始化,产生 TRGO 触发 ADC // 采样率 = 72MHz / ((psc+1) * (arr+1)) void TIM2_Init(u16 psc, u16 arr) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseStructure.TIM_Period = arr; // 自动重装值 TIM_TimeBaseStructure.TIM_Prescaler = psc; // 预分频 TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); // 选择更新事件作为 TRGO 输出 TIM_SelectOutputTrigger(TIM2, TIM_TRGOSource_Update); TIM_Cmd(TIM2, ENABLE); }这段代码里 psc 和 arr 决定了触发频率。比如要 100kS/s,就设 psc=0、arr=719,因为 72MHz/720=100kHz。改采样率就改这两个参数,不用动 ADC 和 DMA 的配置。注意 TIM2 是 16 位定时器,arr 最大 65535,所以最低采样率能到 72MHz/65536≈1.1kHz,再低就得加大 psc。
ADC 配置里关键的一项是外部触发源:
// ADC1 初始化,外部触发由 TIM2 TRGO 驱动 void ADC1_Init(void) { ADC_InitTypeDef ADC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); // PA0 作为模拟输入 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 单通道 ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 非连续,靠触发 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T2_TRGO; 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_ExternalTrigConvCmd(ADC1, ENABLE); // 使能外部触发 ADC_DMACmd(ADC1, ENABLE); // 使能 DMA 请求 ADC_Cmd(ADC1, ENABLE); // ADC 校准 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); }这里 ADC_ExternalTrigConv 选的是 T2_TRGO,对应 TIM2 的触发输出。采样时间设的是 55.5 个周期,加上转换的 12.5 个周期,总共 68 个周期,ADC 时钟 12MHz 的话一次转换约 5.67μs,对应最高采样率约 176kS/s。如果想更快,把采样时间改成 1.5 周期,总周期 14 个,转换时间约 1.17μs,采样率能到 850kS/s,但输入阻抗要低一些,否则采样电容充不满,读数会偏。
DMA 配置负责把 ADC 数据搬到缓冲区:
// DMA1 通道1 搬运 ADC1 数据 void DMA1_Init(u16 *buffer, u16 len) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (u32)buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = 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_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel1, &DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); }DMA_Mode 设成 Circular 就是循环覆盖,缓冲区满了自动从头开始,适合连续采集。如果要做单次捕获,改成 DMA_Mode_Normal,然后在 DMA 传输完成中断里处理数据。缓冲区长度 len 一般取 512 或 1024,取 2 的幂方便做触发对齐。
3.3 主循环与波形刷新
主循环里做两件事:判断缓冲区是否更新,然后画波形。如果是循环 DMA,可以定期读缓冲区;如果是正常模式,就在 DMA 中断里置标志。画波形时要把 12 位 ADC 值映射到屏幕坐标:
// 把 ADC 值映射到 OLED 的 Y 坐标 // adc_val: 0~4095, y_range: 屏幕高度 u8 MapToScreen(u16 adc_val, u8 y_range) { // 12 位 ADC 满量程 4095,映射到 0~y_range return y_range - (u8)((u32)adc_val * y_range / 4095); } // 主循环里刷新波形 while(1) { if(g_adc_ready) { g_adc_ready = 0; for(u8 i = 0; i < 128; i++) { u8 y = MapToScreen(g_adc_buf[i * (BUF_LEN / 128)], 64); OLED_DrawPoint(i, y, 1); } OLED_Refresh(); } }这里做了抽点,因为缓冲区 1024 点,屏幕只有 128 列,所以每 8 个点取一个。抽点会丢高频细节,但屏幕分辨率就这样,想看细节得靠串口传数据。OLED_Refresh 是整屏刷新,I2C 屏大概要 20~30ms,所以刷新率在 30~50fps 左右,看慢速信号够用。
注意:如果发现波形左右抖动,多半是触发没对齐。可以在软件里做边沿触发,找到第一个上升沿的位置,从那里开始画,波形就稳了。
4. 核心模块拆解:ADC 采样、DMA 搬运和波形绘制怎么配合
4.1 ADC 采样精度与速度的取舍
12 位 ADC 理论分辨率是 4096 分之一,但实际有效位数受噪声影响,可能只有 9~10 位。想提高精度,可以过采样:连续采 4 次求平均,噪声降低一半,但采样率也降到四分之一。代码里加一个简单的滑动平均:
// 4 点滑动平均,降低噪声 u16 MovingAverage(u16 new_val) { static u16 buf[4] = {0}; static u8 idx = 0; u32 sum = 0; buf[idx] = new_val; idx = (idx + 1) & 0x03; for(u8 i = 0; i < 4; i++) sum += buf[i]; return sum >> 2; }这个函数每来一个新采样值就更新一次,输出是最近 4 次的平均。代价是波形会稍微变钝,高频成分被削弱。如果被测信号本身很干净,可以不加;如果信号毛刺多,加了会好看很多。
采样速度方面,前面算过,最快 850kS/s 左右。但实际能跑多快还取决于 DMA 和内存带宽。F103 的 DMA 和 CPU 共享总线,如果 CPU 同时在刷屏,DMA 搬运可能会被拖慢。所以高采样率时最好让屏幕刷新慢一点,或者用双缓冲,采集和显示分开。
4.2 DMA 双缓冲与单缓冲的差别
单缓冲就是一块内存,DMA 循环写,CPU 随时读。问题是 CPU 读的时候 DMA 可能正在写同一块区域,数据会撕裂。表现是波形上偶尔出现一个跳变点,像毛刺但不是信号本身的。解决办法是双缓冲:DMA 写缓冲 A 的时候,CPU 读缓冲 B;DMA 写满 A 后自动切到 B,并产生中断,CPU 在中断里切换读取目标。
标准库的 DMA 没有内置双缓冲模式(那是 HAL 库的 HAL_DMAEx_MultiBufferStart),但可以手动实现:用两个缓冲区,DMA 传输完成中断里切换内存地址。代码大概这样:
u16 buf_a[BUF_LEN], buf_b[BUF_LEN]; u16 *g_read_buf; void DMA1_Channel1_IRQHandler(void) { if(DMA_GetITStatus(DMA1_IT_TC1)) { DMA_ClearITPendingBit(DMA1_IT_TC1); // 切换读取缓冲区 if(DMA1_Channel1->CMAR == (u32)buf_a) { DMA1_Channel1->CMAR = (u32)buf_b; g_read_buf = buf_a; } else { DMA1_Channel1->CMAR = (u32)buf_a; g_read_buf = buf_b; } g_adc_ready = 1; } }这样 CPU 读的永远是 DMA 刚写完的那块,不会撕裂。代价是多占一倍内存,C6 上如果 BUF_LEN=512,两块就是 2KB,还能接受。
4.3 波形绘制:从点阵到连续曲线
OLED 画波形有两种方式:画点或画线。画点简单,但点稀疏时看起来是断的。画线要在相邻两点之间插值,代码稍复杂但效果好。常见做法是 Bresenham 画线算法,或者简单点,在两点之间填竖线:
// 在两点之间画竖线,形成连续波形 void DrawWaveLine(u8 x0, u8 y0, u8 x1, u8 y1) { if(x1 <= x0) return; for(u8 x = x0; x < x1; x++) { // 线性插值 u8 y = y0 + (u8)((s32)(y1 - y0) * (x - x0) / (x1 - x0)); OLED_DrawPoint(x, y, 1); } }这个函数在 x0 到 x1 之间逐列画点,y 值线性插值。比直接画点连续,计算量也不大。如果屏幕支持画线函数,直接用 OLED_DrawLine 更好。
绘制时还要注意坐标翻转。ADC 值越大,电压越高,但屏幕 Y 坐标是向下增大的,所以要做一次翻转,否则波形是倒的。前面 MapToScreen 里用 y_range - ... 就是干这个。
4.4 触发与同步:让波形不再左右跑
没有触发的示波器,波形会随信号相位左右滚动,根本没法看。触发的作用是找到一个固定参考点,每次从那里开始画。最简单的触发是边沿触发:设定一个阈值,当信号从低于阈值变到高于阈值时,认为触发发生,从那个点开始记录。
软件触发在缓冲区里找触发点:
// 在缓冲区里找上升沿触发点 // 返回触发点在缓冲区中的索引,找不到返回 0 u16 FindTrigger(u16 *buf, u16 len, u16 threshold) { for(u16 i = 1; i < len; i++) { if(buf[i-1] < threshold && buf[i] >= threshold) return i; } return 0; }找到触发点后,画波形时从触发点开始取数据,这样每次画的起点都是同一个相位,波形就稳了。阈值可以设成量程的一半,或者用自动阈值:先找缓冲区的最大值和最小值,取中间值。
提示:如果信号频率和采样率不成整数倍,触发点每次位置会差一个点,波形还是会轻微抖。这是正常的,专业示波器也有这个现象,叫“触发抖动”。
5. 避坑与排查:调这个项目时最容易翻车的几个地方
5.1 波形全是噪声,看不到信号
现象:屏幕上一片杂乱的点,或者一条粗带,完全看不出波形。
原因:最常见的是 ADC 输入引脚悬空。悬空的 ADC 引脚会拾取环境噪声,读数在 0 到 4095 之间乱跳。另一个原因是采样时间太短,输入阻抗高时采样电容充不满,读数偏低且不稳。
解决:先把 PA0 接到 GND,看读数是不是接近 0;接到 3.3V,看是不是接近 4095。如果不对,检查 ADC 初始化和校准。如果对,再接待测信号。采样时间先设长一点(比如 55.5 或 239.5 周期),稳定后再往短了调。
5.2 采样率上不去,波形挤在一起
现象:想采 100kHz 的信号,但屏幕上只显示几个周期,或者波形明显失真。
原因:采样率不够,不满足奈奎斯特采样定理。要采 100kHz 信号,采样率至少 200kS/s,实际工程上取 5~10 倍,也就是 500kS/s 到 1MS/s。但前面算过,F103 的 ADC 最快也就 850kS/s 左右,而且还要留余量给 DMA 和显示。
解决:降低被测信号频率,或者用等效采样(多次采集不同相位拼合)。如果非要采高频,只能换更快的 ADC 或换芯片。另外检查定时器配置,确认 TRGO 频率确实是想要的采样率,可以用示波器量一下 TIM 的触发输出引脚。
5.3 编译通过但下载后没反应
现象:Keil 里编译 0 错误 0 警告,下载也成功,但屏幕不亮或没波形。
原因:可能是启动文件选错了。C6 和 C8 的启动文件不一样,C6 的 Flash 是 32KB,启动文件应该是 startup_stm32f10x_ld.s(ld 是 low density)。如果错选了 md(medium density,对应 64KB 或 128KB),中断向量表可能对不上,程序跑飞。
解决:在 Keil 的 Options for Target -> Device 里确认选的是 STM32F103C6,然后在工程文件列表里检查启动文件是不是 ld 版本。另外检查系统时钟配置,C6 外部晶振一般是 8MHz,PLL 倍频到 72MHz,如果晶振频率不对,时钟会偏,串口波特率也会错。
5.4 DMA 传输完成中断不进
现象:配置了 DMA 传输完成中断,但中断服务函数从来没执行过。
原因:最常见的是 DMA 中断优先级没配,或者 NVIC 没使能。标准库里 DMA 中断属于 DMA1_Channel1_IRQn,要单独使能。另一个原因是 DMA 模式设成了循环模式,循环模式下传输完成中断只在缓冲区满时触发一次,如果没清标志,后续不再触发。
解决:检查 NVIC 配置:
NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);然后在 DMA 初始化里使能传输完成中断:DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE)。中断服务函数里记得清标志,否则会反复进中断。
5.5 屏幕刷新太慢,波形卡顿
现象:波形能显示,但刷新率很低,看起来一顿一顿的。
原因:OLED 整屏刷新太慢,尤其是 I2C 接口。128×64 的屏,I2C 速率 400kHz 的话,刷一屏要 20ms 以上,加上画波形的计算,帧率可能只有 20fps。
解决:改用 SPI 屏,速率能到 10MHz 以上,刷屏快很多。或者只刷新波形区域,不整屏刷新。还可以用局部刷新,只更新变化的列。如果这些都不行,就降低刷新率,反正看慢速信号不需要高帧率。
6. 二次开发:改采样深度、加测量功能、换屏幕的实操思路
拿到这个工程后,最常改的三个方向是采样深度、测量功能和显示方式。先说采样深度。默认可能是 512 或 1024 点,想看得更细就得加长。但 C6 的 SRAM 只有 10KB,加长缓冲区要算好余量。一个技巧是用 u8 存采样值的高 8 位,牺牲精度换深度。12 位 ADC 右对齐,取高 8 位就是 8 位精度,缓冲区直接省一半。对于看波形形状来说,8 位精度通常够用,除非你要测很小的电压变化。
// 把 12 位 ADC 值压缩成 8 位存储 u8 adc_8bit = (u8)(adc_val >> 4);这样 2048 点的缓冲区只占 2KB,C6 也能跑。画波形时再左移 4 位还原成 12 位量程,或者直接按 8 位映射到屏幕。
加测量功能是另一个常见需求。示波器最基本的是测频率、峰峰值、平均值。频率可以用过零检测:数一秒钟内信号穿过零点的次数,除以 2 就是频率。峰峰值就是缓冲区最大值减最小值。平均值就是求和除以点数。这些计算放在主循环里,每帧算一次,开销很小。
// 计算峰峰值和平均值 void Measure(u16 *buf, u16 len, u16 *vpp, u16 *vavg) { u16 max = 0, min = 4095; u32 sum = 0; for(u16 i = 0; i < len; i++) { if(buf[i] > max) max = buf[i]; if(buf[i] < min) min = buf[i]; sum += buf[i]; } *vpp = max - min; *vavg = sum / len; }频率测量稍微麻烦点,因为要处理噪声。简单做法是设一个滞回阈值,比如平均值上下各留 5% 的余量,信号穿过上阈值算一次上升沿,穿过下阈值算一次下降沿,两个上升沿之间的点数就是周期。用定时器频率一除就是频率。
换屏幕是最容易的,因为绘图逻辑和屏幕驱动是分开的。只要新屏幕有画点函数,把 OLED_DrawPoint 替换掉就行。比如换成 TFT 屏,把画点、刷新、初始化三个函数改掉,主循环里的波形绘制代码一行不用动。这也是为什么工程里要把 HARDWARE 分目录,就是为了换硬件时改动最小。
最后说一个我自己的习惯:每次改完采样率或缓冲区大小,一定先用一个已知频率和幅度的信号源(比如函数发生器输出 1kHz 方波)验证一遍,看频率读数对不对、幅度对不对。如果不对,先查定时器配置和 ADC 校准,别急着怀疑代码逻辑。从那以后我每次调采样参数都强制走一遍这个验证,省了很多来回折腾的时间。希望帮到你。
本文还有配套的精品资源,点击获取