
简介这是一份面向嵌入式初学者与电子爱好者的STM32F103ZET6简易示波器程序源码包基于Cortex-M3内核芯片实现正弦波、方波等信号的采集与波形显示适合用于学习ADC采样、LCD驱动与定时器配置等典型外设开发。压缩包共272个文件约8.9MB以c与h源码文件为核心配合o、d、crf等编译中间文件以及lst、txt、s等辅助说明与启动文件另含uvproj、uvopt工程文件、hex烧录文件与map映射文件可直接在Keil环境下打开编译。资源已有4570人学习下载热度较高。项目完整呈现了从ADC模拟信号采集、数据处理到SPI/I²C接口驱动LCD显示波形的实现链路并附带配置文件与说明文档便于读者理解采样率控制、屏幕刷新与固件组织方式是嵌入式系统入门与信号可视化实践的实用参考。1. 一块 F103 板子跑出波形这个示波器程序到底能干什么如果你手头正好有一块 STM32F103ZET6 的开发板屏幕、按键、ADC 输入这些外设都焊好了却一直只跑过点灯和串口打印那这份示波器程序值得你花一个下午拆开看。它做的事情很直接把模拟信号从 ADC 引脚采进来在本地屏幕上实时画出波形同时把采样率、触发方式、电压档位这些参数做成可调项。整套逻辑跑在裸机环境里不依赖操作系统编译烧录后就能脱离电脑独立工作。它适合两类人。一类是想找一个完整、能跑、代码量可控的 STM32 综合练手项目的开发者因为示波器这个场景天然把 ADC、DMA、定时器、屏幕驱动、按键交互全串起来了比单个外设 Demo 有嚼头。另一类是手上已经有 F103ZET6 板子、想把它改造成简易测量工具的人比如测个传感器输出、看个 PWM 占空比、抓一段缓慢变化的电压曲线。需要说清楚的是它是“简易示波器”不是替代台式示波器的方案带宽和采样率受限于 F103 的 ADC 和屏幕刷新能力定位在低频信号观察和教学验证。这份资源是源码包形式拿到手就是一套可以编译的工程。下面我按“先看清结构、再跑起来、最后动手改”的顺序把它拆一遍。2. 工程结构与技术选型从启动文件到屏幕刷新链路2.1 目录骨架与各文件夹职责拿到一个 STM32 工程我第一件事不是打开 main.c而是先看目录怎么分的。这套程序的结构是典型的裸机工程布局大致长这样Project/ ├── CORE/ # 内核相关启动文件与内核头文件 ├── STM32F10x_FWLib/ # 标准外设库源码 ├── HARDWARE/ # 板级驱动屏幕、ADC、按键等 │ ├── ADC/ │ ├── LCD/ │ ├── KEY/ │ └── TIMER/ ├── USER/ # 主逻辑 │ ├── main.c │ ├── stm32f10x_it.c # 中断服务函数 │ └── system_stm32f10x.c └── OBJ/ # 编译输出CORE和STM32F10x_FWLib基本不用动属于地基。真正要读的是HARDWARE和USER两块前者是外设驱动后者是示波器的主循环和中断逻辑。这种分法的好处是改屏幕型号或者换 ADC 通道时只动HARDWARE里对应文件夹主逻辑不受影响。提示不同来源的工程目录名可能略有差异但“库文件 / 硬件驱动 / 用户逻辑”这三层划分基本一致认准这个结构就不会迷路。2.2 技术选型为什么是标准库而不是 HAL这套程序用的是 STM32 标准外设库Standard Peripheral Library不是现在更流行的 HAL 库。原因很实际F103 这一代芯片用标准库的代码更贴近寄存器执行路径短中断响应和 DMA 配置写起来直观对于示波器这种对时序敏感的场景少一层抽象就少一分不确定性。HAL 库的优点是跨系列移植方便但代价是代码体积和调用层级都上去了在 F103 这种资源有限的片子上标准库反而更顺手。核心外设的职责分工是这样的外设作用关键点ADC1采集模拟信号配合 DMA 搬运避免 CPU 轮询丢点DMA把 ADC 结果搬到内存缓冲区循环模式双缓冲或半满中断TIM触发 ADC 采样控制采样率定时器触发比软件触发稳定FSMC驱动 TFT 屏幕并口屏幕用 FSMC 刷得快EXTI按键输入用于切换档位、触发方式选型里最关键的一条是“定时器触发 ADC DMA 搬运”。如果让 CPU 在循环里手动读 ADC采样间隔会被屏幕刷新和按键扫描拖得忽长忽短画出来的波形抖动明显。用硬件触发加 DMA采样节奏由定时器决定CPU 只管处理已经采好的数据波形稳定性会好很多。2.3 数据流一个采样点从引脚到屏幕的路径把链路理清楚后面看代码就不会乱。一个采样点的完整旅程是这样的定时器按设定频率产生触发事件ADC 收到触发转换当前引脚电压结果写入数据寄存器DMA 检测到 ADC 数据就绪自动把值搬到内存缓冲区adc_buf[]缓冲区采满一批后触发中断或主循环轮询标志位主逻辑对这批数据做触发判断、电压换算换算后的点交给屏幕驱动画到对应像素位置。这条链路里第 3 步的 DMA 是性能关键第 5 步的触发判断是“波形能不能稳住”的关键。很多人第一次做示波器波形能出来但一直左右滚动问题基本都出在触发逻辑上后面避坑章节会细说。3. 跑起来编译、烧录与第一次看到波形3.1 环境准备与工程打开先把工具链备齐。这套工程是 Keil MDK 的工程格式.uvprojx所以你需要Keil MDK-ARM版本 5 即可对应 F103 系列的器件支持包Device Family Pack一个 ST-Link 或 J-Link 下载器串口调试助手可选用来打印调试信息。装好之后直接双击工程文件打开。打开后先别急着编译检查两个地方一是Options for Target里的器件型号是否选到 STM32F103ZE二是Debug选项卡里下载器是否识别到了芯片。这两处不对后面全是白忙。3.2 关键编译配置时钟与下载设置F103ZET6 的外部晶振通常是 8MHz工程里system_stm32f10x.c会根据宏定义决定倍频到 72MHz。确认一下stm32f10x.h里的HSE_VALUE和你板子上的晶振一致不一致会导致串口波特率、定时器周期全部偏掉这种问题排查起来很费时间。/* system_stm32f10x.c 中的时钟配置片段 */ #define SYSCLK_FREQ_72MHz 72000000 static void SetSysClockTo72(void) { /* 使能外部高速晶振 */ RCC-CR | ((uint32_t)RCC_CR_HSEON); /* 等待 HSE 就绪超时则停在这里便于排查 */ while((RCC-CR RCC_CR_HSERDY) 0) {} /* 配置 PLLHSE 作为源9 倍频得到 72MHz */ RCC-CFGR | (uint32_t)(RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL9); /* 使能 PLL 并等待锁定 */ RCC-CR | RCC_CR_PLLON; while((RCC-CR RCC_CR_PLLRDY) 0) {} /* 切换系统时钟到 PLL 输出 */ RCC-CFGR (uint32_t)((uint32_t)~(RCC_CFGR_SW)); RCC-CFGR | (uint32_t)RCC_CFGR_SW_PLL; while ((RCC-CFGR (uint32_t)RCC_CFGR_SWS) ! RCC_CFGR_SWS_PLL) {} }这段代码的作用是把系统时钟从内部 8MHz 切到外部晶振倍频后的 72MHz。PLLMULL9就是 9 倍频8MHz 乘 9 正好 72MHz。如果你的板子晶振不是 8MHz这个倍频系数要跟着改否则定时器算出来的采样率全是错的。几个while等待循环是死等卡在这里说明晶振没起振先查硬件焊接和负载电容。3.3 烧录后第一次上电屏幕和信号接入编译通过、烧录成功后板子重新上电。正常情况下屏幕会先显示界面框架然后开始画波形。如果屏幕是白的或者花屏先别怀疑程序八成是屏幕驱动型号对不上去HARDWARE/LCD里确认初始化序列和你的屏幕控制器是否匹配。信号接入这一步要注意量程。ADC 引脚一般只能承受 0 到 3.3V直接接超过这个范围的信号会烧引脚。测更高电压必须加分压电阻或者前端调理电路。第一次测试建议用板子自己的 3.3V 经过一个电位器分压拧电位器看波形上下移动这样最安全也最直观。/* main.c 主循环的简化结构 */ int main(void) { /* 初始化各外设时钟、屏幕、ADC、DMA、定时器、按键 */ SystemInit(); LCD_Init(); ADC_DMA_Init(); TIM_Trigger_Init(); KEY_Init(); while(1) { /* 等待一批采样完成 */ if (adc_batch_ready) { adc_batch_ready 0; /* 触发判断找到波形稳定显示的起点 */ trigger_index Find_Trigger(adc_buf, BUF_SIZE); /* 换算电压并绘制 */ Draw_Waveform(adc_buf, trigger_index); } /* 扫描按键处理档位和触发方式切换 */ Key_Process(); } }主循环的逻辑很清晰等数据、找触发点、画波形、处理按键。adc_batch_ready这个标志位由 DMA 传输完成中断置位主循环只负责消费。这种“中断置标志、主循环处理”的写法在裸机里很常见好处是中断服务函数尽量短重活留给主循环不会因为画图耗时影响采样。4. 核心模块拆解ADC 采样、触发判断与屏幕绘制4.1 ADC 加 DMA 的连续采样配置采样是整个示波器的源头配不好后面全白搭。核心思路是让 ADC 在定时器触发下连续转换DMA 在后台把结果搬走CPU 完全不参与单点搬运。/* ADC 与 DMA 初始化关键片段 */ void ADC_DMA_Init(void) { ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; /* 打开 ADC1 和 DMA1 时钟 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); /* DMA 配置从 ADC 数据寄存器搬到内存缓冲区 */ DMA_InitStructure.DMA_PeripheralBaseAddr (u32)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (u32)adc_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize 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_Init(DMA1_Channel1, DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); /* ADC 配置独立模式外部触发右对齐 */ ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T2_CC2; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 1; ADC_Init(ADC1, ADC_InitStructure); /* 规则通道 0采样时间 55.5 周期 */ ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); /* 打开 ADC 的 DMA 请求 */ ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); /* ADC 校准别省这一步 */ ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); }几个参数值得单独说。DMA_Mode_Circular让 DMA 采满缓冲区后自动回到开头这样采样永不停止主循环随时能取最近一批数据。ADC_ExternalTrigConv_T2_CC2表示用定时器 2 的捕获比较通道 2 来触发转换采样率就由这个定时器的频率决定。ADC_SampleTime_55Cycles5是采样保持时间信号源内阻大的话要适当加长否则采到的值偏低。注意ADC 校准那两行别删。不校准的话零点有偏移画出来的波形整体上下平移测小信号时误差很明显。4.2 触发判断让波形不再左右乱跑波形能画出来但一直横向滚动是新手做示波器最常见的翻车点。根因是每批数据的起始点相位不固定屏幕从缓冲区第一个点开始画自然每次都不一样。解决办法是做触发判断在缓冲区里找一个满足条件的点作为绘制起点。/* 上升沿触发找到信号从阈值下方穿到上方的位置 */ u16 Find_Trigger(u16 *buf, u16 size) { u16 i; u16 threshold (TRIGGER_LEVEL_HIGH TRIGGER_LEVEL_LOW) / 2; for (i 1; i size / 2; i) { /* 前一点在阈值下方当前点在阈值上方即为上升沿 */ if (buf[i-1] threshold buf[i] threshold) { return i; /* 返回触发点下标作为绘制起点 */ } } return 0; /* 没找到触发点就从头画波形会滚动 */ }threshold是触发电平由用户通过按键调节对应屏幕上那条水平触发线。循环只扫缓冲区前半段是为了保证触发点后面还有足够的数据可以画满一屏。如果扫完都没找到符合条件的点说明信号幅度没跨过触发电平或者触发方式选错了比如信号是下降沿为主却选了上升沿触发这时返回 0波形就会滚动这也是判断触发是否生效的一个直观信号。触发方式一般提供三种上升沿、下降沿、自动。自动模式下即使没触发也强制刷新适合观察无规律信号上升沿和下降沿模式则要求必须找到触发点才更新波形更稳但信号不满足条件时会停住。4.3 屏幕绘制把采样值映射成像素坐标绘制这一步要把 12 位 ADC 值0 到 4095映射到屏幕的纵向像素范围。假设屏幕绘图区高度是DRAW_HEIGHT像素映射关系是线性的。/* 将 ADC 值换算成屏幕 Y 坐标并画点 */ void Draw_Waveform(u16 *buf, u16 start) { u16 x, y; u16 prev_y 0; for (x 0; x DRAW_WIDTH; x) { /* 取触发点之后的数据越界则回绕 */ u16 idx (start x) % BUF_SIZE; u16 adc_val buf[idx]; /* 线性映射ADC 值域映射到屏幕高度注意 Y 轴方向翻转 */ y DRAW_BOTTOM - (u32)adc_val * DRAW_HEIGHT / 4096; /* 和上一个点连线避免点太稀出现断点 */ if (x 0) LCD_DrawLine(x - 1, prev_y, x, y); prev_y y; } }映射公式里DRAW_BOTTOM是绘图区底部的 Y 坐标减去换算值是因为屏幕坐标系原点在左上角Y 越大越靠下而电压越高应该画得越靠上所以要翻转。除以 4096 对应 12 位 ADC 的满量程。用LCD_DrawLine把相邻两点连起来比单纯画点视觉上连续得多尤其在采样点少于屏幕宽度时。如果屏幕刷新慢导致波形闪烁常见做法是只重绘波形区域不整屏清屏或者用一块内存做显存、画完一次性刷过去。F103 的 FSMC 刷屏速度还行但整屏清屏加逐点绘制还是会有可见延迟优化时优先考虑缩小重绘范围。5. 避坑与排查那些让波形出不来的常见问题5.1 屏幕花屏或全白现象是上电后屏幕没有正常界面要么全白要么彩色条纹。原因通常是 LCD 初始化序列和实际屏幕控制器不匹配或者 FSMC 的时序参数配得太快屏幕跟不上。解决方法是先确认屏幕型号去HARDWARE/LCD里找对应的初始化代码如果型号对但还花屏把 FSMC 读写时序里的地址建立、数据保持时间适当加大宁可慢一点也要先点亮。5.2 波形滚动停不下来现象是波形能显示但一直横向移动调触发档位也没用。原因有两个一是触发判断函数没找到符合条件的点一直返回 0二是触发电平设得超出了信号实际幅度范围。解决方法是先把触发方式切到自动模式确认信号本身正常再切回边沿触发用按键把触发电平调到信号幅度的中间位置观察是否稳定。如果信号本身幅度就很小先检查前端有没有衰减。5.3 采样率上不去或波形失真现象是调高采样率后波形反而变差或者高频成分明显丢失。原因是 ADC 采样时间设得太短信号源内阻又大采样保持电容没充到位。解决方法是把ADC_SampleTime从 55.5 周期往上调同时确认信号源输出阻抗不要太高必要时加一级电压跟随器做缓冲。另外要记住采样定理想看清某个频率的信号采样率至少要是它的两倍以上实际工程里一般取五到十倍。5.4 编译报错找不到器件或库文件现象是换电脑或者换 Keil 版本后编译一堆红。原因是器件支持包没装或者工程里库文件的相对路径变了。解决方法是先在 Keil 的 Pack Installer 里装上 F1 系列的支持包再检查Options for Target里的Include Paths是否指向了正确的库目录。路径里尽量别用中文和空格这也是血泪经验。5.5 按键没反应或误触发现象是按键按下去没反应或者没按也乱跳。原因是按键消抖没做或者 GPIO 上下拉配置和实际电路对不上。解决方法是软件上加 10 到 20 毫秒延时消抖硬件上确认按键是接地上拉还是接电源下拉把 GPIO 模式配成对应的输入方式。如果用了外部中断还要注意中断触发边沿设置。6. 二次开发换屏幕、加功能与验证方法6.1 换一块屏幕要动哪些地方换屏幕是这套程序最常见的改造需求。核心是把HARDWARE/LCD里的驱动换掉但接口要保持一致。建议先定义一组统一的绘图接口比如LCD_Init、LCD_DrawPoint、LCD_DrawLine、LCD_Clear然后针对新屏幕实现这几个函数主逻辑一行都不用改。/* 统一的绘图接口换屏幕时只改实现不改调用 */ void LCD_DrawPoint(u16 x, u16 y, u16 color); void LCD_DrawLine(u16 x1, u16 y1, u16 x2, u16 y2); void LCD_Clear(u16 color); void LCD_Init(void);如果新屏幕分辨率变了DRAW_WIDTH和DRAW_HEIGHT这两个宏要跟着改映射公式里的除数也要同步。分辨率提高后刷新压力变大可以考虑降低波形刷新频率或者只刷新变化区域。6.2 加一个频率测量功能在现有采样链路基础上加频率测量并不难。思路是对触发点之间的间隔做统计连续找到两个上升沿触发点它们之间的采样点数乘以采样周期就是信号周期取倒数就是频率。/* 粗略频率测量统计两个上升沿之间的采样点数 */ u32 Measure_Freq(u16 *buf, u16 size, u32 sample_rate) { u16 first 0, second 0; u16 threshold (TRIGGER_LEVEL_HIGH TRIGGER_LEVEL_LOW) / 2; u16 i, found 0; for (i 1; i size; i) { if (buf[i-1] threshold buf[i] threshold) { if (found 0) { first i; found 1; } else { second i; break; } } } if (second first) return sample_rate / (second - first); /* 周期点数换算成频率 */ return 0; }sample_rate是当前定时器设定的采样率单位 Hz。这个方法测低频信号够用但信号频率接近采样率一半时误差会变大因为一个周期内的采样点太少。想测更高频率要么提高采样率要么改用测周法配合输入捕获。6.3 改造后的验证习惯每次改完驱动或者采样逻辑我一般按固定顺序验证一遍先用信号发生器或者板载 PWM 输出一个已知频率的方波确认波形形状和频率读数对得上再把幅度调小看小信号下触发是否还稳最后连续运行十几分钟看会不会因为缓冲区溢出或者内存越界跑飞。这套流程能挡掉大部分低级错误。从那以后我每次动采样相关的代码都强制先用已知信号跑一遍再往下写功能不然等一堆功能叠上去再回头查根本分不清是哪一层出的问题。希望这份拆解能帮你把这块 F103 板子上的示波器跑起来也顺手改成自己想要的样子。本文还有配套的精品资源点击获取