简介这份资源是面向嵌入式初学者与电子爱好者的STM32F103ZET6简易示波器完整工程基于Cortex-M3内核芯片利用其72MHz主频与多通道ADC实现正弦波、方波等信号的采集与LCD波形显示适合作为单片机外设综合练习与课程设计参考。压缩包共272个文件约8.9MB以c与h源码、o与d编译中间文件、crf与lst链接列表、s启动文件及txt说明为主另含uvproj、uvopt等Keil工程文件与hex、axf烧录产物覆盖从源码到可执行固件的完整链路。目前已有4570人学习下载。读者可从中获得ADC采样、定时器控制采样率、SPI/I²C驱动LCD、CMSIS标准库配置等模块化实现思路并借助工程文件直接编译烧录对照波形调试过程理解微控制器资源调度与信号可视化方法对掌握嵌入式开发流程具有实际参考价值。1. 拆开一个 STM32F103ZET6 示波器程序包它到底能跑出什么波形前阵子帮一个做电源测试的朋友找便携采集方案他丢过来一个压缩包名字就叫「基于STM32F103ZET6示波器程序.rar」。我第一反应是这年头用 F103 做简易示波器的资料满天飞但真正能跑、能看、能改的没几个。拆开之后发现这套东西的定位很清晰——它不是要替代台式示波器而是给嵌入式工程师一个能塞进设备里的波形观察窗口。核心是 STM32F103ZET6 这颗 Cortex-M3 芯片主频 72MHz自带 12 位 ADC配合 TFT 屏和按键交互能采能显能调。适合谁做电机驱动、电源环路、传感器信号调理的从业者手头需要一台能长期挂在板子上的低成本监视器也适合刚学完单片机外设、想找个综合项目练手的同学。它解决的不是「高精度测量」而是「现场快速看波形趋势」这个具体诉求。2. 信号链拆解从模拟前端到屏幕像素的完整路径2.1 模拟前端与 ADC 采样时序这套程序最核心的部分不是显示而是采样。STM32F103ZET6 的 ADC1 挂在 APB2 总线上最高时钟 14MHz12 位分辨率下最快转换时间 1μs。但程序里不会跑这么快因为要留时间给 DMA 搬运和屏幕刷新。常见做法是配置 ADC 为连续转换模式由 TIM2 或 TIM3 的更新事件触发采样率通过定时器分频算出来。比如要采 10kHz 的正弦波按奈奎斯特至少 20kHz 采样率实际工程里取 10 倍以上也就是 100kHz 以上才看得清形状。定时器触发的好处是采样间隔严格等距不会因为中断响应抖动导致波形畸变。模拟前端部分程序包里通常配一个运放做阻抗变换和偏置。F103 的 ADC 输入范围是 0 到 3.3V如果测双极性信号得把信号抬到 1.65V 中点。常见电路是用电阻分压加跟随器或者直接用轨到轨运放搭一个加法器。这里有个血泪经验偏置电压不稳整个波形会上下漂看起来像信号在呼吸。所以基准源最好用 TL431 或者 REF3033别直接用 3.3V 分压那玩意随负载变。// ADC1 初始化关键片段TIM3 触发 DMA 循环搬运 void ADC_Config(void) { ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // PA0 作为模拟输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); // DMA1 通道1 搬运 ADC1 数据到缓冲区 DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adc_buffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize ADC_BUFFER_SIZE; // 比如 1024 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); DMA_Cmd(DMA1_Channel1, ENABLE); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; // 由外部触发 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T3_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_DMACmd(ADC1, ENABLE); ADC_ExternalTrigConvCmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); // 校准 ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); // TIM3 产生 100kHz 触发信号72MHz / (72 * 10) 100kHz TIM_TimeBaseStructure.TIM_Period 10 - 1; TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); TIM_Cmd(TIM3, ENABLE); }这段代码里几个参数值得盯住ADC_SampleTime_55Cycles5是采样保持时间信号源内阻大就加长否则采不准DMA_Mode_Circular让缓冲区循环覆盖屏幕读的时候直接读最新一段不用等中断TIM3 的Period和Prescaler决定了采样率改这两个值就能切档位。我一般会把采样率做成可调从 10kHz 到 500kHz 分几档用按键切换这样既能看工频干扰也能看开关电源的纹波。2.2 波形重建与 TFT 刷新策略采到数据只是第一步怎么把 1024 个点映射到 240x320 的 TFT 屏上才是显示部分的关键。程序里通常用 ILI9341 或 ST7789 驱动SPI 接口刷一屏大概几十毫秒。如果每采一次就全屏刷新波形会闪得没法看。常见做法是双缓冲一个缓冲区给 DMA 写另一个给屏幕读读完了交换。但 F103 的 RAM 只有 64KB双缓冲 1024 个半字就是 2KB加上屏幕显存勉强够用。更省的做法是只刷新变化区域但实现复杂这套程序里一般不做。波形映射的算法很简单找到缓冲区里的最大值和最小值算出峰峰值然后按屏幕高度做线性缩放。但这里有个玄学问题——如果信号一直在变峰峰值跟着变波形会上下跳。解决办法是加一个「自动量程」的滞回区间或者干脆固定垂直档位让用户手动调。我倾向于后者因为自动量程在测瞬态信号时会把小信号放大成噪声看起来全是毛刺。// 将 ADC 缓冲区映射到屏幕坐标并绘制 void Draw_Waveform(uint16_t *buf, uint16_t len) { uint16_t i; uint16_t max 0, min 4095; uint16_t y, y_prev; uint16_t x_prev 0; // 找峰峰值 for(i 0; i len; i) { if(buf[i] max) max buf[i]; if(buf[i] min) min buf[i]; } // 防止除零至少留 10 个 ADC 码的余量 if(max - min 10) { max min 10; } // 清屏或只清波形区这里用局部清屏 LCD_Fill(0, WAVE_Y_START, 240, WAVE_Y_END, BLACK); // 逐点画线x 方向压缩到 240 像素 for(i 0; i 240; i) { // 从缓冲区里等间隔取点len 可能大于 240 uint16_t idx i * len / 240; // 映射到 Y 坐标ADC 值越大屏幕 Y 越小因为屏幕原点在左上 y WAVE_Y_END - (buf[idx] - min) * (WAVE_Y_END - WAVE_Y_START) / (max - min); if(i 0) { LCD_DrawLine(x_prev, y_prev, i, y, GREEN); } x_prev i; y_prev y; } }这段代码里WAVE_Y_START和WAVE_Y_END是屏幕上的波形显示区域一般留出顶部状态栏和底部菜单栏。LCD_DrawLine是画线函数比画点快但要注意坐标边界别画到区域外面去。如果屏幕驱动带硬件加速比如 ILI9341 的 Memory Write 连续写可以先把整行像素拼好再一次性发送速度能快一倍。另外buf[idx]这种等间隔抽取在信号频率高的时候会混叠看起来像低频波这是采样定理的锅不是程序 bug。要避免就得提高采样率或者加抗混叠滤波器。3. 交互与参数配置按键、菜单和那些反直觉的默认值3.1 按键状态机与菜单结构这套程序一般配三到四个按键一个菜单键、一个加键、一个减键、一个确认键。按键处理用状态机比用延时消抖靠谱因为示波器刷新占着 CPU延时消抖会把主循环卡死。常见做法是定时器每 10ms 扫一次 IO记录按下和释放事件主循环里根据当前菜单层级做响应。菜单结构通常是两级一级选功能垂直档位、水平时基、触发模式二级调参数。这里有个坑如果按键扫描放在中断里而菜单刷新在主循环两个上下文同时改同一个变量会出玄学问题。我一般把按键事件放进一个环形队列主循环慢慢消费。触发模式是示波器的灵魂。这套程序里通常支持三种自动、正常、单次。自动模式下没触发信号也刷新屏幕上全是滚动的噪声正常模式下等触发条件满足才更新一屏波形稳定单次模式抓一个瞬态就停。触发条件一般是边沿触发上升沿或下降沿触发电平用 ADC 码值表示。调触发电平的时候屏幕上会有一条水平虚线告诉你当前触发点在哪。如果触发电平设得比信号峰峰值还高那就永远触发不了屏幕卡住不动——这不是死机是触发条件没满足。3.2 时基与垂直档位的换算关系时基决定了屏幕上显示多长时间的信号。假设采样率是 100kHz缓冲区 1024 个点那么一屏就是 10.24ms。如果屏幕横向 240 像素每个像素代表 42.7μs。用户看到的「时基」参数通常用「ms/div」表示比如 5ms/div一共 10 格就是 50ms。但程序内部是按采样点算的所以改时基实际上是改采样率或者改抽取比例。垂直档位同理ADC 满量程 3.3V 对应 4095如果垂直档位设成 1V/div屏幕 8 格就是 8V但 ADC 只能测 3.3V所以前面必须有衰减器或者放大器。程序里的垂直档位只是改显示比例不改变硬件增益这点要分清。参数典型值影响调整建议采样率10kHz~500kHz决定能看多快的信号按信号频率的 10 倍以上选缓冲区长度512~2048决定一屏点数越大越占 RAMF103 建议 1024触发电平0~4095决定波形从哪开始画设在信号幅值中间偏上垂直档位0.1V/div~2V/div决定波形高度先选大档位再往下调时基0.1ms/div~50ms/div决定波形疏密先选大时基找信号再缩小这张表里的参数不是孤立的。比如你把采样率降到 10kHz时基设成 0.1ms/div那一个像素对应的时间就太长了波形会挤成一团。反过来采样率 500kHz时基 50ms/div缓冲区根本装不下那么多点程序会做抽取波形看起来是好的但细节丢了。我一般会先让用户选一个「自动设置」程序根据输入信号的频率和幅值算一组参数然后再手动微调。自动设置的核心就是测频和测幅测频用定时器捕获或者过零检测测幅用峰峰值。4. 避坑与排查那些让你怀疑芯片坏了的瞬间4.1 波形显示为一条直线或满屏噪声现象上电后屏幕有背光菜单能操作但波形区要么一条水平线要么全是跳动的点。原因通常有三个一是模拟前端没供电运放没输出ADC 读到的是零或满量程二是 DMA 没启动缓冲区里全是初始值三是触发条件设错了自动模式下应该滚动但程序卡在正常模式。排查顺序先用万用表量 ADC 输入引脚的电压如果是 0V 或 3.3V 固定查前端如果电压随信号变查 DMA 配置里的DMA_BufferSize和DMA_Mode如果都正常把触发模式切到自动看有没有滚动。我遇到过最坑的一次是 DMA 通道选错了ADC1 对应 DMA1 通道1结果代码里写了通道2编译不报错运行就是没数据。4.2 采样率上不去波形混叠现象想测 50kHz 的开关纹波结果屏幕上显示的是一个低频正弦频率对不上。原因采样率不够或者定时器触发频率算错了。F103 的 ADC 最快 1MHz 左右但加上 DMA 和屏幕刷新实际能稳定跑 200kHz 就不错了。如果信号频率 50kHz采样率至少 500kHz这时候得把屏幕刷新降下来或者用抽取模式只显示一部分点。另一个常见错误是定时器分频算错比如Prescaler设成 72以为得到 1MHz实际上 APB1 定时器时钟是 36MHz如果系统时钟 72MHzAPB1 分频系数为 2所以 36MHz / 72 500kHz不是 1MHz。这个细节在参考手册的时钟树里不看就翻车。4.3 屏幕刷新撕裂或闪烁现象波形在屏幕上滚动时中间有一条横向撕裂线或者整体闪烁。原因SPI 刷屏和 DMA 采样抢总线或者刷屏没有做局部更新。F103 的 SPI 最高 18MHz刷 240x320 全屏要 2403202*8/18M ≈ 68ms这期间如果采样缓冲区被覆盖显示的就是新旧混合的数据。解决办法一是用双缓冲DMA 写 buf0 的时候屏幕读 buf1二是降低刷新率比如每 100ms 刷一次中间只更新变化区域三是把 SPI 时钟提到 36MHzF103 的 SPI1 在 APB2 上最高 72MHz 二分频。但 SPI 太快会导致屏幕花屏得看屏幕驱动芯片的手册ILI9341 一般能跑 20MHz 以上。4.4 按键反应迟钝或连击现象按一下加键参数跳好几格或者按了没反应得使劲按。原因按键扫描周期太长或者没有做释放检测。如果扫描周期 50ms人按一下至少 100ms程序会认为按了两次。解决办法扫描周期降到 10ms并且记录「按下沿」和「释放沿」只在按下沿触发一次动作。另外机械按键抖动时间一般 5~10ms10ms 扫描刚好能滤掉不用再加延时。如果按键接在中断引脚上记得在中断里关掉该引脚的中断处理完再开否则会反复进中断。4.5 程序烧录后不运行或运行一段时间死机现象烧录成功但屏幕不亮或者跑几分钟后卡死。原因一是堆栈溢出F103 默认堆栈 1KB如果局部数组太大比如uint16_t buf[2048]放在函数里直接爆栈二是中断优先级冲突ADC 中断和 DMA 中断抢同一个优先级导致死锁三是看门狗没喂。排查把大数组改成全局变量或静态变量检查NVIC_PriorityGroupConfig和各个中断的抢占优先级DMA 和 ADC 别设成一样如果开了独立看门狗主循环里记得喂狗但刷屏函数如果耗时太长喂狗间隔要算好。我一般会在主循环里翻转一个 IO用示波器看周期如果周期突然变长就是哪里卡住了。5. 进阶玩法把 F103 示波器变成能存数据的简易分析仪这套程序最值钱的地方不是显示而是它能改。F103ZET6 有 512KB Flash 和 64KB RAM除了跑示波器还能塞进去一个简单的数据记录功能。我一般会加一个 SD 卡或者串口上传把 ADC 缓冲区里的原始数据按二进制存下来然后在电脑上用 Python 画图分析。这样比盯着小屏幕看靠谱得多尤其是抓瞬态信号的时候。串口上传的代码很简单把 DMA 缓冲区里的半字转成字节加个帧头用 115200 波特率发出去一秒钟能传 10K 个点对于 100kHz 采样率来说传 1024 个点只要 0.1 秒完全跟得上。# 上位机解析串口传来的 ADC 数据并绘图 import serial import struct import matplotlib.pyplot as plt ser serial.Serial(COM3, 115200, timeout1) raw ser.read(2048) # 读 1024 个半字 data struct.unpack( H * 1024, raw) # 小端解析 # 转成电压3.3V / 4095 volts [v * 3.3 / 4095 for v in data] plt.plot(volts) plt.xlabel(Sample) plt.ylabel(Voltage (V)) plt.grid(True) plt.show()这段 Python 脚本里struct.unpack的格式字符串H*1024表示小端、1024 个无符号半字。如果 STM32 那边发的是大端改成。串口波特率 115200 下一帧 2048 字节大概 0.18 秒加上解析和绘图整体延迟不到一秒。如果想连续采集可以在 STM32 那边加一个简单的协议每帧开头两个字节 0xAA 0x55然后跟 1024 个半字上位机按帧同步。这样即使丢几个字节也能重新对齐。我习惯在 STM32 端用 DMA 串口发送把缓冲区指针直接给 USART1 的 DMA 通道CPU 完全不参与采样和上传互不干扰。另一个进阶方向是加 FFT。F103 没有硬件浮点但可以用定点数做 256 点 FFT大概几毫秒算完。把 ADC 数据加汉宁窗然后跑基 2 的 FFT把幅值谱画在屏幕上就能看谐波了。不过屏幕分辨率有限一般只显示前 64 个频点。这个功能对做电源的很有用能直接看开关频率和它的谐波分布。代码网上有现成的定点 FFT 库移植过来改改就行注意 Q 格式和溢出。从那以后我每次拿到这种「简易示波器」程序包都先不急着烧录而是把 ADC 配置和定时器时钟树对一遍再用信号发生器从低频到高频扫一遍确认采样率和显示都对得上。这套 F103 的方案虽然老但胜在便宜、资料多、改起来方便适合当个长期挂机的波形监视器。希望帮到你。本文还有配套的精品资源点击获取