简介:这份资源是面向嵌入式初学者与电子竞赛备赛者的STM32F103C8T6简易示波器完整工程Demo,围绕ADC采样、定时器触发、串口通信与波形显示等核心环节,帮助读者理解如何用Cortex-M3内核微控制器搭建一套可运行的数字示波器原型,适合具备一定C语言与单片机基础、希望从理论走向实操的学习者。压缩包共298个文件,约8.56MB,以c与h源码、Keil工程文件(uvprojx、uvoptx)、编译产物(o、d、axf、hex)及少量bat脚本、htm说明为主,覆盖从工程配置到固件烧录的完整链路。目前已有2916人学习下载,说明该案例在同类项目中具有较高参考价值。读者可从中获得可直接编译运行的工程框架、ADC与定时器协同采样的实现思路、串口数据传输与上位机显示的处理逻辑,以及硬件电路与调试排错的相关线索,便于对照学习并二次开发。
1. 需求先立住:为什么用 STM32F103C8T6 做一台“够用”的简易示波器
手上只有一块最小系统板、一块 TFT 屏和几根杜邦线,却要交出一台能测波形、能调时基、能看幅值的示波器,这是很多嵌入式课设和毕设里最真实的起点。选 STM32F103C8T6 不是因为它强,恰恰是因为它“刚好够”:72MHz 主频、12 位 ADC、多路定时器加 DMA,配合一块 320×240 的并口或 SPI 屏,就能把采样、触发、显示这条链路跑通。这套简易示波器 Demo 要解决的核心问题,是在低成本 MCU 上把“采样—存储—触发—绘制”做成一条稳定闭环,适合正在做嵌入式课程设计、想理解 ADC 与定时器协同、又不想被复杂模拟前端拖住的人。先把需求边界划清楚,后面所有设计才有落点。
2. 总体方案设计:采样链路、触发机制与显示刷新怎么分工
2.1 从信号到像素的四段式结构
一台简易示波器拆开看,无非四件事:把模拟电压变成数字、按固定节奏存进缓冲区、找到波形起点、把缓冲区画到屏幕上。STM32F103C8T6 的资源决定了这四段不能互相抢时间,所以结构上我一般按“定时器触发 ADC、DMA 搬运、主循环做触发判断与绘制”来分。定时器负责节拍,ADC 负责转换,DMA 负责把转换结果悄悄塞进内存,CPU 只在缓冲区填满后介入。这样采样期间 CPU 几乎不参与,留给屏幕刷新的时间才够用。
具体分工如下:TIM2 或 TIM3 作为采样时钟源,通过更新事件触发 ADC1 的规则通道转换;ADC1 配置为连续转换加 DMA 循环模式,把结果写入一个长度为 N 的uint16_t缓冲区;主循环检测半满或全满标志后,做软件触发判断,再把一段数据映射成屏幕坐标。这里的关键认知是:采样率由定时器决定,不由 ADC 自己决定,这是后面时基调节能成立的前提。
2.2 采样率与时基的换算关系
时基旋钮背后其实是一个除法。假设定时器时钟 72MHz,预分频PSC、自动重装ARR,则采样率fs = 72MHz / ((PSC+1)*(ARR+1))。屏幕上横向 240 个像素点,若一屏显示T个采样点,则每格时间 =T / fs / 格数。我通常把一屏固定显示 240 点,通过改ARR来切换时基档位,这样绘制逻辑不用动,只换采样节奏。
| 时基档位 | 采样率 fs | ARR 典型值 | 适用场景 |
|---|---|---|---|
| 1ms/div | 约 10kSPS | 7199 | 低频信号、电源纹波 |
| 100us/div | 约 100kSPS | 719 | 音频、PWM 观察 |
| 10us/div | 约 1MSPS | 71 | 中频方波、脉冲 |
| 1us/div | 受 ADC 上限约束 | 需评估 | 仅作示意,注意混叠 |
提示:STM32F103 的 ADC 最高采样率约 1MSPS(采样时间取最短时),再往上调 ARR 只会让转换来不及完成,读到的值是上一次的残留,波形会“粘住”不动,这是新手最容易误判为程序卡死的地方。
2.3 触发方式的选择:为什么先做上升沿软件触发
硬件触发需要比较器和外部中断配合,对 Demo 来说成本偏高。我一般先做软件触发:在 DMA 缓冲区里从前往后找第一个满足“前一点低于阈值、当前点高于阈值”的位置,把它作为波形起点,从该点开始取 240 点送显示。这样每次刷新波形都从同一个相位开始,屏幕上才不会左右乱跑。阈值默认取缓冲区中值,也可以做成可调。等这套跑稳了,再考虑加迟滞比较,避免噪声导致触发点抖动。
3. 关键模块实现:ADC+DMA 采样、触发定位与屏幕绘制
3.1 ADC 与 DMA 的初始化配置
采样链路的初始化顺序不能乱:先开时钟,再配 GPIO 为模拟输入,然后配 ADC 和 DMA,最后配定时器并启动。下面这段是核心配置,省略了 GPIO 时钟使能等样板代码,重点看 ADC 与 DMA 的绑定关系。
// ADC1 + DMA1_Channel1 循环采样,TIM3 更新事件触发 void adc_dma_init(uint16_t *buf, uint32_t len) { RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; RCC->AHBENR |= RCC_AHBENR_DMA1EN; // PA0 模拟输入 GPIOA->CRL &= ~(0xF << 0); DMA1_Channel1->CPAR = (uint32_t)&ADC1->DR; // 外设地址:ADC 数据寄存器 DMA1_Channel1->CMAR = (uint32_t)buf; // 内存地址:采样缓冲区 DMA1_Channel1->CNDTR = len; // 传输数量 DMA1_Channel1->CCR = DMA_CCR_MSIZE_0 // 内存 16 位 | DMA_CCR_PSIZE_0 // 外设 16 位 | DMA_CCR_MINC // 内存地址自增 | DMA_CCR_CIRC; // 循环模式,缓冲区自动回卷 DMA1_Channel1->CCR |= DMA_CCR_EN; ADC1->CR1 = ADC_CR1_SCAN; // 单通道也开扫描位,便于扩展 ADC1->CR2 = ADC_CR2_CONT // 连续转换 | ADC_CR2_DMA // 使能 DMA | ADC_CR2_EXTSEL_1 | ADC_CR2_EXTSEL_0 // TIM3 TRGO 触发 | ADC_CR2_EXTTRIG; // 允许外部触发 ADC1->SMPR2 = ADC_SMPR2_SMP0_2; // 通道 0 采样时间 41.5 周期 ADC1->SQR1 = 0; // 规则序列长度 1 ADC1->SQR3 = 0; // 第一个转换通道为 0 ADC1->CR2 |= ADC_CR2_ADON; // 开启 ADC }这段代码里有两个容易忽略的点。第一,DMA_CCR_CIRC让缓冲区循环回卷,主循环永远能读到最近len个点,不需要手动重启 DMA。第二,ADC_CR2_EXTSEL选的是 TIM3 的 TRGO 事件,所以定时器必须配置成“更新事件产生 TRGO”,否则 ADC 收不到触发,DMA 一个数都不会搬。采样时间SMP0_2对应 41.5 周期,是为了让高阻抗信号源有足够建立时间,如果信号源阻抗低,可以改短以换取更高采样率。
3.2 定时器触发节奏的配置
定时器这边只做一件事:按设定频率产生更新事件,并把它映射到 TRGO。下面以 TIM3 为例,arr由时基档位查表得到。
void tim3_trigger_init(uint16_t psc, uint16_t arr) { RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; TIM3->PSC = psc; TIM3->ARR = arr; TIM3->CR2 = TIM_CR2_MMS_1; // 更新事件作为 TRGO 输出 TIM3->CR1 = TIM_CR1_CEN; // 启动定时器 }MMS_1这个位组合的含义是“更新事件映射到 TRGO”,是 ADC 外部触发能工作的关键。很多人把定时器配好了、ADC 也开了,波形却是一条直线,问题往往就出在CR2没设,TRGO 一直是低电平。参数上,psc一般固定为 71,把定时器时钟降到 1MHz,这样arr直接就是微秒数,换算时基时心算就能完成,调试阶段省很多事。
3.3 触发点定位与波形绘制
缓冲区填满后,主循环先找触发点,再把 240 个点映射到屏幕。映射时要注意 ADC 是 12 位(0~4095),屏幕纵向只有 240 像素左右,必须做缩放,并且要留出上下边距,否则波峰波谷会贴边被裁掉。
// 在 buf 中寻找上升沿触发点,返回起始索引;找不到则返回 0 uint32_t find_trigger(uint16_t *buf, uint32_t len, uint16_t thresh) { for (uint32_t i = 1; i < len; i++) { if (buf[i-1] < thresh && buf[i] >= thresh) { return i; } } return 0; // 未触发,从头画,屏幕会滚动,属正常现象 } // 把 240 个采样点画到屏幕,y 轴做缩放并留边距 void draw_wave(uint16_t *buf, uint32_t start, uint32_t n) { for (uint32_t x = 0; x < n; x++) { uint16_t v = buf[(start + x) % BUF_LEN]; // 4095 -> 屏高 200,上下各留 20 像素 uint16_t y = 220 - (uint32_t)v * 200 / 4095; lcd_draw_point(x, y, COLOR_GREEN); } }find_trigger返回的是缓冲区内的绝对索引,绘制时用取模回卷,保证起点靠近缓冲区尾部时也能连续取点。draw_wave里的220 - ...是因为屏幕坐标原点在左上角,电压越高 y 越小,这个减法别写反,写反了波形会上下颠倒,看起来像“信号被翻转”,其实是坐标映射错了。绘制前记得清屏或只清波形区域,全屏清会导致明显闪烁,我一般只清上一帧波形覆盖的矩形区域。
3.4 幅值与频率的粗略测量
Demo 阶段不必上 FFT,用最朴素的方法就够交差:幅值取一屏内的最大值减最小值,换算成电压(V = adc * 3.3 / 4095);频率则通过统计两次上升沿触发点之间的采样点数,用f = fs / 间隔点数估算。这两个值显示在屏幕角落,答辩时能直接指着说“这里读出来是 1kHz、1.6V 峰峰值”。要注意频率测量只在信号周期明显大于采样间隔时可信,接近奈奎斯特频率时读数会严重偏离,这一点在报告里写清楚,反而是加分项。
4. 避坑与排查:这类 Demo 最容易翻车的五个地方
4.1 波形是一条直线或完全不动
现象是屏幕上有网格但波形贴在某一条水平线上,调时基也没反应。原因通常有两个:ADC 根本没被触发,或者 DMA 没搬数据。排查顺序是先看ADC1->DR在调试器里是否变化,不变则查 TIM3 的CR2是否设了MMS;若DR在变但缓冲区不变,查 DMA 通道是否使能、CNDTR是否在递减。解决方法是把触发链路逐级点亮:先手动软件触发一次 ADC 确认转换正常,再开定时器触发,最后接 DMA,一级一级排,比盯着整段代码猜快得多。
4.2 波形左右乱跑、无法稳定
现象是每次刷新波形起点都不一样,像在“游走”。原因是没做触发,或者触发阈值设在了噪声区。解决方法是启用find_trigger,并把阈值设到信号幅度的中间偏上位置;如果信号本身噪声大,加一点迟滞——要求连续两个点都高于阈值才算触发,能明显压住抖动。这一步做完,波形“定住”的效果立竿见影,是答辩时最直观的改进点。
4.3 调高采样率后波形失真或幅度变小
现象是时基往快档调,波形幅度反而缩水、形状变尖。原因是 ADC 采样时间设得太短,信号源内阻加采样电容还没建立到位就采样了。解决方法是把SMPR2的采样周期调长,或者降低信号源阻抗。这里有个血泪经验:示波器前端如果直接接信号源而没有缓冲运放,高阻信号在快档下必然失真,报告里如实写明“本 Demo 未加模拟前端,快档精度受限”,比硬说“全档位精确”更让人信服。
4.4 屏幕刷新慢、波形闪烁严重
现象是波形一帧一帧地跳,肉眼可见闪烁。原因是每帧全屏清屏加逐点绘制,SPI 屏带宽被吃满。解决方法是只清波形区域、用批量写像素代替单点写、把绘制放在 DMA 传输的空档期做。如果屏幕支持,把波形画到一块内存显存再整块刷,效果更好。这个优化不改变功能,但直接决定演示时“像不像一台仪器”。
4.5 缓冲区越界导致偶发死机
现象是运行一段时间后程序跑飞或屏幕花屏。原因是触发点靠近缓冲区末尾时,start + x越界访问。解决方法是所有取点统一走% BUF_LEN回卷,并且缓冲区长度取 2 的幂,让编译器把取模优化成位与。这个坑很隐蔽,因为越界读不一定立刻崩,往往在演示到一半时才发作,提前用取模封死最省心。
5. 验收与答辩:怎么证明这台简易示波器“真的能用”
验收不要一上来就讲代码结构,先让仪器自己说话。我一般准备三样东西:一个已知频率和幅值的信号源(函数发生器或另一块 MCU 的 PWM 加 RC 滤波)、一张对照表、一段录屏。演示时先接 1kHz 方波,调时基到能看见两三个周期,指出屏幕上的幅值读数和频率读数,再和信号源设定值对照,误差在可接受范围内就算通过。接着换一个低频正弦,展示触发稳定后波形不漂移。最后故意把时基调到最快档,说明精度边界在哪里——主动交代局限,比被问出来强。
验收标准可以按下面这张表逐项打勾,答辩时照着念都行:
| 验收项 | 通过标准 | 验证方法 |
|---|---|---|
| 采样链路 | 缓冲区数据随输入变化 | 调试器观察 buf 内容 |
| 触发稳定 | 波形起点固定,不左右游走 | 连续观察 10 秒 |
| 时基切换 | 至少 3 档可切换且波形随之伸缩 | 逐档演示 |
| 幅值测量 | 与信号源设定值误差在 10% 内 | 对照读数 |
| 频率测量 | 中低频段误差在 10% 内 | 对照读数 |
| 长时间运行 | 连续运行 10 分钟不死机不花屏 | 计时观察 |
答辩时最容易被追问的是“采样率和时基怎么对应”“触发是怎么实现的”“精度受什么限制”。前两个问题用第 2、3 章的公式和代码就能答,第三个问题恰恰是展示工程判断力的地方:把 ADC 采样时间、信号源阻抗、无模拟前端这三条讲清楚,评委反而会觉得你懂边界。还有一个小技巧,把find_trigger的阈值做成一个可调变量,现场用按键改阈值,演示“触发点从抖动到稳定”的过程,比任何 PPT 都有说服力。
从那以后我每次带这类采样加显示的 Demo,都强制自己先把“定时器触发 ADC、DMA 搬运、软件触发定位”这三段分别用调试器点亮一遍,再合起来跑,因为合起来出问题时,你根本分不清是哪一段在骗你。希望这套结构能帮到你,照着搭一遍,再按自己的屏幕和信号源改参数,交一份能自圆其说的方案不难。
本文还有配套的精品资源,点击获取