
简介面向嵌入式学习者与电子工程师的一套STM32F103C6简易示波器完整工程源码基于Keil5开发运行于ARM Cortex-M3内核单片机重点演示ADC配合DMA高效采集模拟信号再通过USB HID协议实时发送到PC端显示与处理可迁移到波形采集、传感器监测、低速数据记录等场景。压缩包共969个文件大小约11.72MB以572个C源文件、257个头文件和51个汇编启动文件为主还包含Keil工程配置文件、静态库、中间件及说明文档整体按Drivers、Src、Inc、Middlewares等目录组织方便定位外设驱动、主程序、头文件与HID协议实现。已有922人学习下载。项目从ADC采样、DMA搬运到上位机USB通信的代码链路清晰适合初学者理解嵌入式数据采集与PC交互的全过程基于该源码可继续增加滤波、波形存储、触发控制或数据分析功能具备良好的二次开发价值。1. 为什么拿F103C6做示波器选型逻辑与能力边界先说结论STM32F103C6这颗芯片做出来的示波器不是让你拿去和台式示波器比参数的它的正确使用场景是嵌入式开发中的辅助调测工具。比如你在调I2C时序、看PWM输出波形、验证串口波特率是否准确、检查按键消抖是否有效这些场景下你不需要关心信号的第3个谐波长什么样只需要确认高电平对不对、频率大概是多少、有没有毛刺这就够了。很多人一听到单片机示波器就想到DSO138那种掌上示波器用F103ZET6甚至F407配2.4寸TFT彩屏能测到1MHz以上。但C6这颗料不一样它的定位是入门级、够用就好、成本压到最低。1.1 F103C6的资源盘点32KB Flash能塞下什么STM32F103C6T6的标配资源是72MHz主频、32KB Flash、10KB SRAM、2个12位ADC共用1个ADC外设、3个定时器、2个SPI、2个I2C、3个USART。注意两个数字32KB Flash和10KB SRAM。很多人第一反应是32KB能干啥但实际上一个精简示波器固件的代码量大约在15KB到25KB之间包含LCD驱动或者串口上位机协议只要你不塞进去一堆冗余的库函数这个容量是完全够用的。我用的是标准外设库Standard Peripheral Library不是HAL库因为SPL的体积更小、执行效率更高对于这种对时序敏感的应用场景更合适。HAL库当然也能跑但代码体积会膨胀到接近30KB留给业务逻辑的空间就很紧张了。1.2 为什么不用F103ZET6或F407内存与FSMC的取舍这里要讲一个很多人忽视的硬件约束F103C6没有FSMC灵活的静态存储控制器这意味着你不能直接驱动TFT彩屏的并行接口。ZET6有FSMC可以用地址线映射的方式刷屏CPU只需要写内存地址就能刷新LCD速度飞快。而C6不行你只能用SPI接口的屏或者干脆不接屏把波形数据通过串口发给上位机显示。我的选择是后者板子只负责采集显示交给PC端的上位机。这样做的好处非常明显省掉了LCD驱动代码Flash空间压力大幅下降上位机可以做更复杂的显示网格、光标测量、FFT板子可以做得非常小甚至可以用F103C6最小系统板加杜邦线搭起来坏处也很现实不能脱离电脑独立使用。但这对于一个调试工具来说不是问题——调试的时候本来就坐在电脑面前。1.3 能力边界能测多宽的信号F103C6的ADC最高采样率是1MHz实际上在72MHz主频下ADC时钟14MHz采样周期1.5个周期时能达到1Msps后面细说。根据奈奎斯特定理理论上能测的最高信号频率是500kHz但实际上因为触发抖动、采样间隔不均匀等因素实际可靠测量范围在200kHz以下。这个范围覆盖了什么大部分UART的波特率时序波形115200bps的位宽约8.7us对应约115kHz、I2C的100kHz标准模式、SPI的几百kHz低速模式、PWM输出的几kHz到几十kHz信号以及绝大多数传感器的模拟输出。够用了真的够用了。2. 硬件方案设计信号调理与采集前端的注意事项示波器的核心前端不是单片机而是信号调理电路。单片机的ADC输入范围是0到3.3V而且要求源阻抗尽量低但你要测的信号可能是-5V到5V的双极性信号或者幅值只有几十毫伏的微弱信号。这两者之间必须经过中间电路做转换。2.1 输入保护第一件要做的事我用一个非常简单的方案串联一个1kΩ电阻再接两个反并联的BAV99二极管到3.3V和GND。这个电路的作用是防止外部信号超过ADC输入范围时烧掉单片机引脚。原理很好理解当输入电压高于3.3V加一个二极管压降约3.9V时上面的二极管导通把电压钳位到3.9V当输入电压低于-0.7V时下面的二极管导通把电压钳位到负的一个二极管压降。1kΩ电阻起限流作用防止大的瞬态电流灌入芯片。这个保护电路不完美——它不能让ADC在超压后继续准确测量但它的价值在于在接线失误、信号接反、或者探针碰到高电压这些常见事故中保命。我做过的板子中有一套就是没加保护结果一个同事把12V接进去整个板子冒烟了。从此以后所有带外部输入的板子保护电路一律标配。2.2 偏置与增益把双极性信号搬到ADC窗内如果要测的是双极性信号比如音频信号在0V附近来回摆动直接接入ADC是测不了的因为单极性ADC不能处理负电压。常见的做法是用一个运放做加法器/电平抬升电路把信号整体向上平移1.65V3.3V的一半这样原来的-1.65V到1.65V范围就被映射到了0到3.3V。具体电路我用的是一片MCP6002双运放做的同相加法器输入信号经过一个RC低通滤波器后接运放同相端反相端接一个1.65V的基准电压用两个10kΩ电阻分压3.3V得到再加一个100nF电容滤波。运放输出直接接ADC引脚。增益设置要讲一点我用了一个10kΩ电位器做反馈电阻可以在1倍到5倍之间连续调节。这个小设计解决了一个实际问题——不同信号的幅值范围差异很大固定增益往往需要频繁更换电阻。电位器虽然精度不如固定电阻但作为调试用途完全够用。2.3 基准电压被忽略的精度瓶颈F103C6的ADC参考电压默认接VDDA3.3V电源但很多最小系统板的3.3V是AMS1117输出的噪声和纹波指标其实一般。如果你直接把VDDA接上去会看到波形上叠加了一层明显的锯齿状噪声这在示波器应用中非常致命。改进方法有两个用独立的LDO给VDDA供电用地分割把模拟地和数字地分开用外部基准芯片如REF3033提供稳定的3.3V给VREF我实际测试过用小板的AMS1117供电时ADC读数在稳定直流输入下会有±3到±5个LSB的跳动换了独立LDO供电后跳动降到±1到±2个LSB。对于8位级别的显示要求后者就够用了。注意这是12位ADC但实际有效位数能做到9到10位就非常好了。3. 固件架构ADC采样、DMA传输与触发逻辑的实现固件部分是整个项目的灵魂也是我花了最多时间调试的地方。核心需求很清晰以尽可能高的采样率连续采样把数据存到内存按触发条件截取一段波形然后通过串口发给上位机。3.1 采样率计算1Msps是怎么配置出来的STM32F103的ADC最高时钟是14MHz当ADC采样周期配置为1.5个周期、转换周期为1.5个周期时总共约3个周期理论上可以做到14MHz除以3约4.67Msps。但实际上因为转换不仅仅是采样周期还包括12个周期的逐次逼近转换所以最极限配置是1.512.514个ADC时钟周期14MHz除以14正好是1Msps。具体配置代码使用标准外设库RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC时钟 72MHz / 6 12MHz ADC_InitStructure.ADC_Resolution ADC_Resolution_12b; 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_1Cycles5); ADC_DMACmd(ADC1, ENABLE);注意我特意把ADC时钟配成了12MHz而不是14MHz留了一点裕量。实测在14MHz时钟下某些芯片个体采样结果会出现偶发的编码跳动降频后稳定性明显改善。这就是工程实践和理论计算的差别——理论值是一回事量产级稳定是另一回事。3.2 DMA双缓冲不丢数据的核心机制连续采样的数据量很大如果靠CPU在中断里搬运数据72MHz的执行时间根本来不及。正确做法是DMA直接把ADC转换结果搬到内存全程不经过CPU。我用了DMA的双缓冲模式也叫乒乓缓冲也就是定义两个缓冲区A和BDMA先填充A填满后触发中断并自动切到BCPU在中断里处理A的数据处理完了等B填满再交换。这样CPU和DMA并行工作数据不丢。#define SAMPLES_PER_FRAME 2048 uint16_t dma_buf[2][SAMPLES_PER_FRAME]; uint8_t dma_active_buf 0; void DMA1_Channel1_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC1)) { DMA_ClearITPendingBit(DMA1_IT_TC1); // 处理刚刚填满的缓冲区 process_waveform(dma_buf[dma_active_buf], SAMPLES_PER_FRAME); dma_active_buf ^ 1; } }DMA配置中一定要设置循环模式Circular Mode这样DMA会在两个缓冲区之间自动来回切换不需要在中断里手动修改DMA内存地址。3.3 触发逻辑波形的定时快门示波器的触发功能是区分玩具和工具的分水岭。没有触发波形在屏幕上就是乱跑的因为每次采样开始的时间点跟信号没有确定的关系。我的实现方案是软件触发DMA一直连续采样上位机发一个取波形命令后固件扫描缓冲区中的采样点找到第一个满足触发电平且方向正确的点比如上升沿即当前点低于触发电平、下一个点高于触发电平然后从这个点开始往前取若干点作为触发前数据往后取若干点作为触发后数据拼成一帧完整的波形。int8_t find_trigger_point(uint16_t *buf, uint16_t len, uint16_t threshold) { for (uint16_t i 1; i len; i) { if (buf[i-1] threshold buf[i] threshold) { return (int8_t)(i - TRIGGER_PRE_SAMPLES); } } return -1; // 未找到触发点 }这里的经验值触发前数据建议占波形窗口的20%左右触发后占80%这样可以看到触发点前面的信号形态类似模拟示波器的延迟扫描。如果找不到触发点就自动触发也就是直接返回缓冲区开头保证屏幕至少有个波形显示而不是一直黑屏。3.4 数据处理与参数提取拿到一帧波形数据之后固件需要计算几个关键参数峰峰值Vpp、最大值、最小值、周期和频率。频率计算我用的是过零检测法统计信号从负到正穿过平均值或零电平的次数用采样率除以穿过的周期数就得频率。这个算法简单快速但也容易受噪声干扰所以我加了一个迟滞带只有信号从低于平均值阈值再上升到高于阈值加一个滞回量比如5倍噪声幅度才计为一次过零。实测下来对于音频和中低频信号这个方法的误差在2%以内。峰峰值的计算更简单直接遍历缓冲区找最大最小值的差。但要注意这个方法是采样到的峰值如果信号的峰值很窄比如几十纳秒的毛刺常规采样率根本采不到这是所有采样示波器的固有限制不是代码能解决的。4. Keil5工程搭建从芯片包到烧录调试的完整流程这部分写给第一次用Keil5做STM32开发的读者。F103C6是老芯片了资料其实很多但在Keil5下的工程搭建有些细节还是容易踩坑。4.1 安装器件支持包Keil5和Keil4最大的区别是Keil5把器件支持包Device Family Pack和IDE分离了你必须先安装对应的PACK才能新建对应芯片的工程。到Keil官网下载Keil.STM32F1xx_DFP最新版本安装后才能在Device列表中看到STM32F103C6。装完PACK之后新建工程时选择STM32F103C6Keil会自动匹配启动文件和正确的Flash大小32KB。4.2 工程配置的三个关键项构建一个可用的F103工程有几个配置项必须设置对否则要么编译报错要么烧录后跑飞第一在C/C选项卡的Define框中填写STM32F10X_MD,USE_STDPERIPH_DRIVERSTM32F10X_MD是中等容量器件宏F103C6属于中等容量Flash范围16KB-128KB这个宏决定启动文件里使用的中断向量表版本。USE_STDPERIPH_DRIVER是允许使用标准外设库的头文件。两个宏都缺一不可漏掉任何一个都会报出一堆莫名其妙的头文件错误。第二调试器设置。如果你用的是ST-Link在Debug选项卡选择ST-Link Debugger然后进入Settings确认SW Device能识别到芯片。如果在设备列表里看不到芯片最常见的原因是没接VCC或者SWDIO/SWCLK接反了。F103C6的SWD接口就是PA13/PA14两个引脚不需要额外接RESET线但接上更保险。第三Flash Download选项卡。点击Settings在Flash Download页面里确认Programming Algorithm使用的是STM32F10x Med-density Flash 32K如果你的列表里只有High-density或者Low-density必须手动Add对应的算法否则会出现烧录时擦除失败或者跳过了部分Flash区域。4.3 标准外设库的选择与版本我用的是ST官方经典标准外设库V3.5版本不是HAL库。为什么这个项目需要精确控制ADC采样时序和DMA行为标准库把这层逻辑暴露得非常清楚每个寄存器的操作都透明。HAL库抽象更强但中间层多对于这么简单的外设配置反而显得笨重。标准库V3.5的压缩包解压后把Libraries目录整个复制到工程目录下然后在Keil工程里添加需要的源码文件。我一般只加这些stm32f10x_adc.cstm32f10x_dma.cstm32f10x_gpio.cstm32f10x_rcc.cstm32f10x_usart.cmisc.cNVIC配置相关其他的外设源文件不添加减少编译时间和代码体积。如果在使用某个外设库函数时报undefined symbol检查一下是不是对应的.c文件没加进工程。4.4 一个容易忽视的编译优化设置在Options for Target的C/C选项卡中Optimization等级我推荐设置为Level 1 (-O0)或Level 2 (-O1)不要用默认的Level 0。Level 0-O0的代码完全不做优化执行效率最低对于ADC采集中断响应来说可能会丢失采样点Level 3-O2/-O3优化最激进但有时会改变程序的行为尤其是涉及指针和中断共享变量的场景。实测这个项目用-O1最稳代码量减少约8%执行速度提升明显而且没有出现优化导致的逻辑错误。5. 实测效果与踩坑记录真实波形背后的排除过程现在到了最有价值的部分——我在调这个示波器的过程中踩过的坑以及怎么一步步定位并解决它们。5.1 波形严重失真的根因GPIO速度配置第一次上电测试输入一个1kHz的正弦波上位机显示的波形是严重畸形的——上升沿很缓像RC充电曲线到了平台又突然掉下来。我一开始以为是RC低通滤波器的截止频率设置得太低把信号都滤平了但算了一下截止频率是160kHz1kΩ 1nF1kHz信号不应该受影响。排查过程先量了运放输出端的信号是正常的。再量ADC引脚的信号发现波形已经畸变了。问题出在PA0引脚本身——F103的GPIO有多种输出速度和输入模式如果不配置或配置错误引脚的输入阻抗和采样电容的充电时间会严重影响高速ADC采样。查阅参考手册发现ADC输入引脚必须配置为模拟输入模式也就是GPIO_Mode_AIN。而我当时的配置代码是从LED例程复制过来的配置成了推挽输出模式。虽然这个模式不会让芯片损坏但引脚的驱动能力和输入路径完全不对导致ADC内部的采样电容无法在采样时间内完成充电信号就被拉斜了。改正方法GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; // 模拟输入 GPIO_Init(GPIOA, GPIO_InitStructure);改完之后波形立刻恢复正常这个耗时半天的排查让我记住了ADC引脚必须配置为模拟输入这不是一个可选项。5.2 采样率虚标的真相CPU频繁打断DMA传输另一个离谱的问题当我把ADC配置在最高采样率1Msps时理论上一次DMA传输2048个点只需要约2毫秒但实测一次波形更新周期达到了将近20毫秒而且波形显示有明显的卡顿感。用逻辑分析仪抓了DMA中断的触发频率发现中断周期确实在2毫秒左右但中断处理函数的执行时间却接近18毫秒。问题出在process_waveform函数里——我为了在上位机上显示频率在这个函数中做了过零检测、峰值计算和一个软件低通滤波这个滤波器用的是一个200阶的FIR每次处理2048个点就需要约40万次乘加运算。72MHz的Cortex-M3每秒约能执行7200万条指令40万次乘加的循环确实要吃掉好几个毫秒。但这个时间不应该有17毫秒这么多——问题在于我在中断里调用了printf来发送调试信息而printf的重定向实现是阻塞式等待串口发送完成。当上位机串口速率只有115200bps时发送几百个字符就需要约20毫秒DMA在这段时间内已经完成了两次缓冲区切换导致中断被反复抢占。解决思路分两步第一把printf从DMA中断里移出去改成在发送缓冲区中操作用串口中断异步发送第二把FIR滤波器从DMA中断中移到主循环中中断只负责标记数据就绪。这个修改之后同样的采样率设置实测采样率能稳定达到约980kSps波形更新达到每秒30帧以上。5.3 电源噪声波形基线不是平的第三坑来自电源。给F103C6用USB供电整板只有一个100uF电解电容做电源滤波测得基线上叠加了约20mV的50Hz及其谐波噪声波形看起来像一条毛茸茸的粗线。这个问题的根源在USB的5V到3.3V的LDO转换过程中DC-DC开关噪声和市电工频干扰通过地回路串入了模拟前端。我做了两个改进一是在ADC电源引脚VDDA对地加了一个10uF钽电容和一个100nF陶瓷电容并联靠近引脚放置这个组合覆盖了低频和高频的噪声。二是把模拟地和数字地单点连接在PCB上用一个0Ω电阻跨接避免模拟信号的回流经过数字逻辑的总线切换区域。改完后再测基线噪声从±20mV降到了±3mV以内波形清晰度明显提升。对于入门级示波器应用来说这个噪声水平已经可接受了。5.4 上位机通信的速率瓶颈与协议设计最后提一下上位机的数据传输。如果直接用115200bps的串口传2048个点的16位原始波形数据每次4160字节传一帧需要约360毫秒每秒只能刷新不到3帧。这个体验很痛苦。我的优化方案是波形数据压缩后传输。ADC是12位精度但显示只需要8位把每次采样的12位数据右移4位变成8位数据量直接减半。另外对波形窗口做抽帧处理——如果波形窗口内超过3000个有效采样点就每x个点取一个代表值比如最大值这样在保持波形轮廓的前提下大幅减少传输量。实际协议格式简化为0xAA 0x55 [data_len_high] [data_len_low] [波形数据...]帧头固定0xAA 0x55接收端通过帧头同步数据长度用两个字节表示上位机解析时先读帧头再读长度然后按长度读取数据。这套协议简单可靠实测在460800bps下波形刷新率能达到每秒20帧以上。6. 后续还能怎么扩展做完这个项目之后我还想到了几个可以继续迭代的方向一个是增加虚拟通道和数学运算功能。现在只采集了单通道但F103C6的ADC支持多通道扫描模式可以扩展到两通道同时采集这样就能看李萨如图形或者比较两个信号的相位关系。数学运算方面如果Flash空间还够可以加一些简单的RMS计算这对测量音频信号很有用。另一个是改用RTOS。现在的主循环是轮询式的如果后续要同时处理串口、采集、按键扫描等任务用一个简单的RTOS比如FreeRTOS管理任务会使代码结构清晰很多。当然这需要更大的Flash支持F103C6的32KB会比较紧张如果做大的功能扩展换个F103C8T664KB Flash或者F103RCT6256KB Flash会更从容。最后说一句关于上位机的体会。很多人做这类项目都喜欢花很多时间去打磨上位机的界面加缩放、加光标、加频谱。但我个人的经验是先把采集链路做到稳定、不丢数、采样率达标这是基础上位机的功能可以一步步加上去不要一开始就想着做得和商业示波器一样花哨。采集链路做不稳上位机做得再好看也只是一个空壳子。反过来采集链路扎实了哪怕上位机只是一个简单的TFT画线程序它也已经能帮你解决很多实际调试问题了。本文还有配套的精品资源点击获取