ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32F103C6示波器源码实战:Keil5跑通ADC采样与TFT显示

STM32F103C6示波器源码实战:Keil5跑通ADC采样与TFT显示

简介:这份资源是面向嵌入式初学者与电子爱好者的STM32F103C6示波器项目源码,基于Keil5开发环境,帮助读者理解如何用Cortex-M3内核单片机实现模拟信号采集与实时传输。项目核心思路是ADC采集探头电压信号,经DMA自动搬运至内存以减轻CPU负担,再通过USB HID协议将数据实时发送到PC端显示波形,是学习信号采集与USB通信的典型实践案例。压缩包共969个文件,以572个c源文件、257个h头文件、51个s汇编文件为主,另含crf、icf、lib、uvprojx等工程与库文件,整体约11.72MB,目录涵盖Drivers驱动、Src源码、Inc头文件、Middlewares中间件及MDK-ARM工具链,结构完整便于按模块研读。目前已有922人学习下载,读者可据此掌握ADC+DMA+HID的完整链路,并在此基础上扩展滤波、存储与分析等功能。

1. 一块 F103C6 做示波器:为什么这版 Keil5 源码值得你花一个晚上跑通

手头有一块最小系统板,芯片是 STM32F103C6,Flash 只有 32KB、RAM 10KB,引脚也不多。很多人第一反应是「这点资源能干嘛」,但恰恰是这种资源受限的芯片,最适合拿来吃透 ADC 采样、定时器触发和 TFT 刷屏这条完整链路。这份示波器源码就是围绕 F103C6 搭的一套可运行工程,Keil5 打开就能编译,烧进去接上信号就能看到波形。它解决的不是「做一个商用示波器」,而是让你在一块便宜到可以随手焊坏的单片机上,把「模拟信号怎么变成屏幕上那条线」这件事从头到尾走一遍。适合刚学完 GPIO 和串口、想找个综合项目练手的人,也适合手上正好有 C6 板子、想验证自己硬件是否正常的熟手。下面我按实际拆包和复现的顺序讲,参数、坑点、改法都落到具体位置。

2. 拆开工程看结构:F103C6 的 32KB 到底怎么分配

2.1 目录里都有什么,哪些文件动不得

拿到源码包,第一件事不是急着编译,而是先看清楚目录结构。这类 Keil5 工程通常长这样:根目录下有一个.uvprojx工程文件,一个User文件夹放main.c和中断服务函数,一个Hardware或BSP文件夹放 LCD、ADC、定时器的驱动,还有一个Start或CMSIS文件夹放启动文件和内核头文件。F103C6 属于中容量产品,启动文件要选startup_stm32f103xb.s这一档,选错了链接阶段会报一堆未定义符号。

有几个文件是「动不得」的:启动文件、system_stm32f1xx.c里的时钟配置、以及stm32f1xx.h里的寄存器定义。这些是芯片能跑起来的地基,改错一个时钟树参数,现象就是程序卡在SystemInit里出不来,串口一行字都不打印。真正需要你关注的是main.c的主循环、ADC 的初始化函数、以及 LCD 的刷屏函数,这三块决定了示波器的采样率和刷新率。

我一般会先做一件事:把工程编译一遍,看 Code、RO-data、RW-data、ZI-data 四个数字。F103C6 的 Flash 是 32KB,RAM 是 10KB,如果编译出来 Code 超过 30KB 或者 ZI-data 逼近 10KB,后面加任何功能都会溢出。这份源码之所以能在 C6 上跑,就是因为它在缓冲区大小和显示区域上做了裁剪,这一点在后面的参数章节会具体说。

2.2 采样、触发、显示:三个模块怎么串起来

示波器的核心链路其实就三步:采样、触发、显示。采样靠 ADC,触发靠比较逻辑,显示靠 LCD 刷点。这份源码里,ADC 配置成规则通道连续转换,由定时器 TRGO 事件触发,这样采样间隔是确定的,不会因为主循环里干了别的活而抖动。定时器的更新频率决定了采样率,比如 72MHz 主频下预分频 72、重装载 100,得到的就是 10kHz 的触发频率。

触发逻辑是很多人第一次做示波器会忽略的地方。没有触发,波形在屏幕上会左右乱跑,根本没法看。源码里的做法是:在 ADC 中断里判断采样值是否越过设定的触发电平,越过了才开始往缓冲区写,写满一屏就停,主循环再把缓冲区刷到 LCD 上。这就是最简单的上升沿触发。缓冲区大小直接决定了一屏能显示多少个点,F103C6 的 RAM 只有 10KB,用uint16_t存 512 个点就是 1KB,加上 LCD 的显存和栈空间,基本就到头了。

显示部分,源码用的是并口或 SPI 的 TFT 屏,刷一列点的方式画波形。这里有个取舍:如果每采一个点就刷一次屏,刷新率会被 LCD 拖死;所以正确做法是采满一屏再统一刷。主循环里那个while(1)干的就是这件事——等缓冲区满,然后逐列画点,画完清标志,等下一屏。理解了这个数据流,后面调参数才知道该动哪里。

3. 在 Keil5 里把它跑起来:从新建工程到第一道波形

3.1 打开工程先改这三处配置

拿到.uvprojx直接双击打开,Keil5 会提示器件包。F103C6 需要安装Keil.STM32F1xx_DFP这个 Device Family Pack,没装的话器件列表里找不到 STM32F103C6,编译按钮是灰的。装好包之后,在 Options for Target 里确认三件事。

第一,Device 页选对型号,必须是 STM32F103C6,选成 C8 或 RB 虽然引脚兼容,但 Flash 和 RAM 大小不一样,链接脚本可能对不上。第二,Target 页的晶振频率填你板子上的实际值,最小系统板一般是 8MHz 外部晶振,填错了SystemInit算出来的主频就不对,定时器采样率跟着全错。第三,Debug 页选对你的下载器,ST-Link 就选 ST-Link Debugger,J-Link 就选 J-LINK,然后在 Settings 里确认能读到芯片 ID。

# 编译后如果报 "cannot open source input file stm32f1xx.h" # 说明头文件路径没加全,检查 Options -> C/C++ -> Include Paths # 至少要包含以下三个路径(按你实际目录替换) .\Start\CMSIS\Device\ST\STM32F1xx\Include .\Start\CMSIS\Include .\User

这段不是让你敲命令,而是提醒你:Keil 工程最常见的翻车就是头文件路径。源码包如果是从别人机器上打包的,路径里可能带着绝对路径,换台电脑就找不到。解决办法是在 Include Paths 里把带盘符的路径删掉,重新用相对路径加一遍。

3.2 采样率、缓冲区、触发电平三个参数怎么定

工程能编译只是第一步,真正决定示波器好不好用的是三个参数。我把它们和对应的代码位置列出来,你照着改就行。

参数典型值代码位置影响
定时器预分频71TIM2 初始化和重装载值共同决定采样率
定时器重装载99TIM2 初始化同上,采样率 = 72M/(71+1)/(99+1)
ADC 缓冲区长度512main.c 数组定义决定一屏点数,越大越占 RAM
触发电平2048触发判断处12 位 ADC 的中值,可调

采样率的算法是:主频除以(预分频+1)再除以(重装载+1)。上面这组值算出来是 10kHz,也就是每 100 微秒采一个点。对于音频以下的信号够用,想看更快的信号就得减小重装载值,但注意 ADC 本身有转换时间,F103 的 ADC 最快也就 1MHz 左右,别把定时器设得比 ADC 还快,那样采到的点是重复的。

缓冲区长度和 RAM 的关系要算清楚。512 个uint16_t是 1KB,如果 LCD 驱动自己还开了显存,比如 240x320 的屏用 16 位色,一屏就是 150KB,F103C6 根本放不下。所以这类小资源示波器通常用「只画波形区域、不整屏缓存」的方式,或者用 8 位色、小尺寸屏。你如果换了大屏,第一件事就是确认显存是不是开在芯片内部 RAM 里,是的话赶紧改成局部刷新。

触发电平设成 2048 是因为 12 位 ADC 的范围是 0 到 4095,中值对应 1.65V 左右。如果你的信号是 0 到 3.3V 摆幅,触发点设中间最稳;如果信号只在小范围波动,比如 1V 到 1.5V,那触发电平要相应调低,否则永远触发不了,屏幕就是一条直线。

3.3 烧录后没波形,先查这四步

烧进去屏幕亮了但没波形,别急着改代码,按这个顺序查。第一步,用万用表量 ADC 输入引脚,确认信号真的到了引脚上,很多人是杜邦线没插紧或者接错了通道。第二步,在 ADC 中断里翻转一个 GPIO,用示波器或逻辑分析仪看有没有翻转,有翻转说明采样在跑,没有就是定时器或 ADC 没启动。第三步,看触发电平是不是设得太高或太低,把信号直接接到 3.3V 或 GND,看屏幕有没有反应。第四步,检查 LCD 的刷屏函数,单独写个测试把屏幕刷成纯色,确认显示链路是通的。

这四步走完,基本能定位到是采样问题还是显示问题。我见过最多的翻车是 ADC 引脚和 LCD 引脚复用了同一个 GPIO,初始化的时候后初始化的把前面的覆盖了,现象就是屏幕正常但采样值永远是 0。查引脚复用表,把冲突的引脚错开就行。

4. 避坑与排查:F103C6 做示波器最容易栽的五个地方

4.1 现象:编译通过但一烧录就卡死

原因通常是栈溢出或者中断向量表偏移不对。F103C6 的 RAM 只有 10KB,如果局部数组开得太大,比如在函数里定义一个 512 字节的缓冲,栈直接爆掉,程序跑飞。解决方法是把大数组改成全局或静态,放到 ZI-data 区,同时把启动文件里的栈大小从默认的 0x400 调到 0x200 甚至更小,给堆留空间。

4.2 现象:波形能显示但抖动厉害

这是触发没做好或者采样和显示不同步。如果触发判断放在主循环里,采样中断和主循环抢缓冲区,波形就会左右跳。正确做法是触发和写缓冲都在 ADC 中断里完成,主循环只负责读一个「缓冲满」标志然后刷屏。另外,刷屏期间如果 ADC 还在写同一个缓冲区,也会撕裂,所以要么双缓冲,要么刷屏前先关中断。

4.3 现象:采样率标称 10kHz 但实际差很多

多半是定时器时钟源没配对。F103 的 TIM2 挂在 APB1 上,如果 APB1 预分频不为 1,定时器时钟会是 APB1 的两倍。很多人按 36MHz 算,实际是 72MHz,采样率直接翻倍。查RCC_GetClocksFreq的返回值,或者直接在定时器初始化后翻转 GPIO 用示波器量周期,这是最准的验证方法。

4.4 现象:换一块同型号板子就不工作

F103C6 有多个封装,引脚定义不一样。源码如果是按 LQFP48 写的,你换成 TSSOP20 的板子,很多引脚根本不存在,初始化这些引脚要么没反应要么报错。换板子第一件事是对着数据手册的引脚定义表,把 LCD 和 ADC 用到的引脚重新核对一遍,尤其是复用功能和重映射。

4.5 现象:屏幕花屏或颜色不对

LCD 驱动的初始化序列和屏幕控制器型号必须匹配。常见的小屏控制器有 ILI9341、ST7789、ST7735,初始化命令差别很大,用错序列就是花屏。源码里如果写死了某一种,你换屏就要改初始化数组。另外,颜色格式也要对,RGB565 和 RGB666 的字节顺序不同,接反了颜色会偏。先刷纯色测试,红绿蓝各刷一遍,颜色对了再调波形。

5. 进阶:把这份源码改成能测更高频信号的双缓冲版本

跑通基础版本之后,你会发现两个瓶颈:一是采样率上不去,二是刷屏时波形会闪。这两个问题可以用双缓冲加 DMA 解决。思路是开两个缓冲区,ADC 由定时器触发、DMA 搬运,填满一个缓冲区就切换,主循环刷另一个,互不干扰。F103C6 的 DMA 有 7 个通道,ADC1 对应 DMA1 通道 1,配置成循环模式,缓冲区大小设成 256,两个缓冲就是 512 个uint16_t,1KB,RAM 还扛得住。

// ADC + DMA 双缓冲关键配置(放在 ADC 初始化之后) #define BUF_LEN 256 uint16_t adc_buf[2][BUF_LEN]; // 双缓冲,共 1KB // DMA 配置:外设到内存,循环模式,半满和全满都产生中断 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)adc_buf; DMA_InitStructure.DMA_BufferSize = BUF_LEN * 2; // 两个缓冲连续存放 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_Init(DMA1_Channel1, &DMA_InitStructure); DMA_ITConfig(DMA1_Channel1, DMA_IT_TC | DMA_IT_HT, ENABLE); // 全满和半满中断 DMA_Cmd(DMA1_Channel1, ENABLE);

这段代码的逻辑是:DMA 把 ADC 数据自动搬到adc_buf,搬满一半触发 HT 中断,搬满全部触发 TC 中断。在中断里切换「当前该刷哪个缓冲」的标志,主循环根据标志刷屏。这样采样和显示彻底解耦,采样率只受 ADC 和定时器限制,刷屏慢一点也不会丢数据。参数上,BUF_LEN设 256 是折中,太小了中断太频繁,太大了 RAM 吃紧。中断优先级里 DMA 要比主循环高,但别设成最高,留一点给 systick。

验证方法很简单:输入一个已知频率的方波,比如 1kHz,看屏幕上是不是稳定显示两个周期。如果波形稳定不抖,说明双缓冲生效了;如果还是抖,检查 DMA 中断里有没有清标志位,F1 系列的 DMA 中断标志必须手动清,忘了清就会一直进中断,主循环根本跑不到刷屏。

从那以后我每次改采样相关的代码,都会先用 GPIO 翻转法量一遍实际采样率,再去看波形,不然调半天都是玄学。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进