资讯详情

资讯详情

基于STM32F407的简易示波器:ADC+DMA双缓冲采样与波形显示实战

简介这是一份面向STM32F407嵌入式学习者的简易示波器完整源码工程适合正在练习ADC采集、LCD显示与串口通信开发的读者。工程使用C语言编写可在Keil5.3环境下编译软件架构为主循环采集中断定时界面更新实际实现了一路ADC定时采集、继电器控制放大倍数、独立按键与状态灯控制、3.2寸TFTLCD显示、采集曲线绘制以及周期、频率、峰峰值等参数计算覆盖从底层采样到上层显示的关键环节。压缩包共171个文件以73个C源码和65个头文件为主体另含Keil工程配置文件、烧录hex、界面截图PNG和少量清理/辅助脚本整体仅1.18MB目录清晰便于对照阅读。目前已有3340人学习下载该工程。通过这份代码读者可以理清STM32F407的中断、定时器、ADC、LCD驱动与信号处理算法如何衔接适合作为课程设计或入门示波器项目的参考资料。 先把话说在前面这个项目我前前后后调了两周踩了不少坑最后做出来的东西虽然和商用示波器没得比但日常看个PWM波形、串口信号、电源纹波完全够用更重要的是把STM32的ADC、DMA、定时器、中断这些外设真正串起来了。这篇就把整个实现过程、源代码思路和调试心得一次说清楚给后面想做同类项目的朋友省点时间。基于STM32F407的简易示波器核心价值在于用一块几十块钱的开发板加一个TFT屏幕实现一个带宽几百kHz、采样率最高2.4Msps的入门级波形查看工具。听起来参数挺寒碜但对于学习嵌入式、调试单片机电路、观察低频信号来说这个方案的学习收益比买一台几千块的示波器高得多——你能看到示波器内部是怎么工作的采样、触发、显示、时基这些概念全都变成可读的代码。1. 项目定位与硬件选型思路1.1 为什么选STM32F407而不是其他芯片做简易示波器核心瓶颈在ADC采样率和数据处理能力。STM32F407内置3个12位逐次逼近型ADC理论采样率最高2.4Msps每秒240万次采样,在同类MCU里算很能打的。对比一下STM32F103的ADC最高1MspsF4直接翻倍多而且F4主频168MHzSRAM有192KB其中64KB是CCM RAM做数据缓冲和处理都从容不少。另一个关键点是F4有完整的DMA控制器和定时器触发ADC的硬件链路。这意味着采样的开始、数据的搬运可以不经过CPU干预CPU只负责把数据从缓冲区取出来绘图就行。如果用F1系列虽然也能做但采样率和数据处理能力会紧张不少波形刷新率上不去。还有一个实际考虑F407开发板非常普及网上资料多ADCDMA的参考例程一抓一大把。我用的是一块正点原子的探索者板子屏幕是4.3寸TFTLCD带电阻触摸板载的硬件资源刚好够用不需要额外搭建复杂的外围电路。1.2 硬件连接与单通道采样方案硬件连接非常直接用的是板载的PA1作为ADC输入通道也就是ADC1的通道1。信号从引脚进来之后经过一个简单的RC低通滤波1kΩ电阻加100pF电容截止频率约1.6MHz目的不是做高精度滤波而是挡掉高频噪声防止采样混叠。屏幕用的是FSMC接口的TFTLCD这是F407一个很实用的特性——FSMC外设可以把LCD映射到外部存储器的地址空间写屏幕就是往内存地址写数据速度比GPIO模拟时序快得多。开发板上4.3寸屏刚好支持FSMC接口分辨率800×480显示波形绰绰有余。按键方面我外接了4个独立按键接在PE0到PE3上分别控制时基切换采样时间窗口、触发模式切换、暂停/运行、屏幕亮度调节。其实后面代码里只实现了时基切换和暂停功能另外两个按键功能是预留的。2. 整体架构与核心设计思路2.1 采样链路定时器触发ADC DMA搬运这是整个项目最核心的链路设计。思路是定时器产生触发信号ADC收到触发后开始转换转换完成后由DMA把结果搬到内存全程不占用CPU。用定时器来触发的目的是让采样间隔精确可控这是示波器时基功能的硬件基础。具体配置大概是这样的链路TIM3产生PWM输出频率等于ADC采样率PWM的上升沿触发ADC1的定时器触发模式ADC1规则组对PA1进行采样转换为12位数据每次转换完成后DMA2的通道0搬运数据到内存缓冲区这里有个容易忽略的细节ADC的采样率并不是随便设的。根据奈奎斯特定理要观测的最高信号频率不能超过采样率的一半。我用的是2.4Msps理论最高采样率实际配置时采样周期设为3个ADC时钟周期采样时间3Cycles这样能留出余量处理其他任务。如果设置成更长的采样周期ADC精度会更高但采样率会下降需要根据实际观测的信号频率来权衡。数据缓冲用的是DMA的循环模式设置了一个2KB的缓冲区可以存1024个采样点。DMA循环模式的好处是硬件自动回绕CPU随时都能读取最新的数据但需要考虑数据一致性问题——你在CPU读数据的时候DMA可能正在写入同一个位置这样读出来的数据就是不完整的。解决办法是用双缓冲后面详细说。2.2 双缓冲设计与波形显示策略双缓冲是示波器软件设计的核心。原理很简单把缓冲区分成两个半区DMA正在写A半区的时候CPU就去读B半区并绘制波形DMA写满A后自动切到BCPU再回头处理A。这样两边各干各的互不干扰。代码实现上我用DMA的传输完成中断来检测半区和整区。具体做法定义采样缓冲区为uint16_t adc_buf[2048]前半区地址是adc_buf[0]后半区是adc_buf[1024]启用DMA的半传输中断HT和全传输中断TC在中断服务函数里设置标志位通知主循环这块数据可以用了这里有个很关键的性能考量中断服务函数里不要做任何波形绘制工作只设置标志位。绘制逻辑放在主循环里处理。因为绘制一帧波形要画几百个点、很多条线段放到中断里会卡死其他任务而且中断服务时间过长本身就不规范。波形显示方面我是把每一帧数据做了一个简单的归一化处理把12位的ADC值0-4095映射到LCD屏幕的纵向像素范围比如屏幕高度480像素就留出上下边距实际绘图区域400像素高横向则是固定把1024个采样点压缩到屏幕宽度的800个像素上。这种显示方式比较简单粗暴但够用。2.3 时基与采样率的关系时基功能是示波器的一个基础功能。在软件里实现时基切换本质上是改变定时器触发ADC的频率。我在代码里实现了4档时基档位采样率观测信号类型02.4Msps几百kHz的高速信号1300ksps几十kHz的信号240ksps几kHz的信号35ksps几百Hz的低频信号切换时基就是修改TIM3自动重装载值ARR。因为这个值是16位的最大65535而TIM3的输入时钟是84MHz所以最低采样率能到84MHz/65535约等于1.28ksps实际我设的5ksps已经够用了。要注意的是修改ARR应该在其更新事件发生后生效否则可能出现一次错误的采样周期实际测试中这个误差影响不大可以接受。3. 源代码关键模块实现3.1 ADC与DMA初始化配置初始化代码是整个项目的地基。我使用STM32标准外设库不是HAL代码比较直接。核心部分如下void ADC_DMA_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 1. 使能时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_DMA2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 2. 配置PA1为模拟输入 GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AN; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_NOPULL; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. ADC1配置独立模式12位分辨率 ADC_InitStructure.ADC_Resolution ADC_Resolution_12b; ADC_InitStructure.ADC_ScanConvMode DISABLE; ADC_InitStructure.ADC_ContinuousConvMode DISABLE; ADC_InitStructure.ADC_ExternalTrigConvEdge ADC_ExternalTrigConvEdge_Rising; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_T3_TRGO; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfConversion 1; ADC_Init(ADC1, ADC_InitStructure); // 4. 配置规则组通道1采样时间为3个周期 ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_3Cycles); // 5. DMA配置 DMA_InitStructure.DMA_Channel DMA_Channel_0; DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_Memory0BaseAddr (uint32_t)adc_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralToMemory; DMA_InitStructure.DMA_BufferSize ADC_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_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_FIFOMode DMA_FIFOMode_Disable; DMA_Init(DMA2_Stream0, DMA_InitStructure); // 6. 启用DMA半传输/全传输中断 DMA_ITConfig(DMA2_Stream0, DMA_IT_HT | DMA_IT_TC, ENABLE); NVIC_InitStructure.NVIC_IRQChannel DMA2_Stream0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); // 7. 启用ADC和DMA ADC_DMACmd(ADC1, ENABLE); DMA_Cmd(DMA2_Stream0, ENABLE); ADC_Cmd(ADC1, ENABLE); }两个细节值得解释一下ADC外部触发源选的是ADC_ExternalTrigConv_T3_TRGO意思是TIM3的TRGO事件触发ADC转换。TIM3_TRGO是在定时器更新事件时产生的而更新事件频率就是ARR1分频后的频率。另外一个要点是DMA的外设地址直接写(uint32_t)ADC1-DR这是ADC转换结果寄存器DMA从固定地址读取数据每读一次就对应一次ADC转换完成。3.2 定时器触发配置定时器配置这一段相对简单但要注意外部触发模式下ADC不会自动连续转换必须等定时器产生触发事件。void TIM3_Trigger_Init(uint32_t sample_rate) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; uint16_t prescaler 0; uint16_t period 0; // 计算分频和周期84MHz / ((prescaler1) * (period1)) sample_rate // 这里简化处理先固定分频为0通过调整period来改变采样率 // 84MHz / (period1) sample_rate // period 84MHz / sample_rate - 1 period (uint16_t)(84000000UL / sample_rate - 1); TIM_TimeBaseStructure.TIM_Prescaler prescaler; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period period; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); // 配置TIM3的TRGO为更新事件 TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update); // 启用定时器 TIM_Cmd(TIM3, ENABLE); }有个公式需要记住定时器时钟84MHz输出触发频率 84MHz / (period 1)。如果你设period为0就能得到84MHz的触发频率但这样ADC完全来不及转换实际极限就是上面说的2.4Msps左右。所以代码里要加一个限制采样率不能超过2.4Msps超过就直接钳位到2.4M。实际测试下来2.4Msps采样率下波形刷新率大概每秒50帧左右——因为一次要处理1024个点的数据还要画800个像素宽的波形FSMC虽然快但画线的for循环还是需要时间。这个刷新率观看实时波形没有明显的闪烁感基本可用。3.3 数据处理与波形绘制数据处理部分的工作量其实比采样配置还大。ADC采到的原始数据是12位无符号整数范围0到4095直接画在屏幕上效果不好需要做一次坐标变换。我写了一个简单的映射函数void Waveform_Process(void) { uint16_t i; static uint16_t disp_x 0; static uint16_t disp_y 0; // 清空波形绘制区域 LCD_ClearWindow(WAVE_X_START, WAVE_Y_START, WAVE_WIDTH, WAVE_HEIGHT); // 画网格 Draw_Grid(WAVE_X_START, WAVE_Y_START, WAVE_WIDTH, WAVE_HEIGHT); for (i 0; i ADC_BUF_SIZE; i 2) // 1024个点抽稀到512次绘制 { uint16_t x WAVE_X_START i * WAVE_WIDTH / ADC_BUF_SIZE; uint16_t y WAVE_Y_START (uint16_t)((4095 - adc_buf[i]) * WAVE_HEIGHT / 4096); if (i 0) { LCD_DrawPoint(x, y); } else { LCD_DrawLine(disp_x, disp_y, x, y); } disp_x x; disp_y y; } }这段代码里有几个工程上的取舍说一下。首先是抽稀1024个点直接画会有密集的短线而且相邻点几乎重叠显示效果反而不清晰抽稀到大约500个点视觉更干净。其次网格绘制不必每次刷新都做可以把网格画在底层缓冲区但为了代码简单我直接在每次绘制前都画一次网格代价是多了几十毫秒的绘制时间。这一步的性能优化空间其实很大比如可以把网格放到一个单独的图层上或者只在需要时重绘。但对于入门级实现来说牺牲一点性能换取代码的可读性是值得的。4. 调试实录问题排查与避坑指南4.1 常见问题速查表调试这款简易示波器期间我遇到了一批非常典型的问题能把它们一一解决掉说明对这套外设链路的理解就到位了。现象可能原因解决办法波形满屏乱跳DMA缓冲区和显示缓冲区共用数据一致性被破坏改用双缓冲或把DMA缓冲区的数据先memcpy一份再绘制ADC采样值始终为0PA1没有配置成模拟输入或者ADC通道号写错检查GPIO模式和ADC_RegularChannelConfig的通道号与引脚对应关系波形是纯直线没有开ADC的外部触发或定时器没启动确认ADC_ExternalTrigConv_T3_TRGO配置确认TIM3已经TIM_Cmd(TIM3, ENABLE)刷新率低、画面撕裂绘制代码太慢一帧没画完DMA又写完了一轮提高绘制效率或者把抽稀比例调高比如1024点抽到256点幅度看起来不准分压或缩放逻辑有问题检查电压基准F407的ADC参考电压是3.3V满量程对应3.3V需要把adc_buf[i] * 3.3 / 4096转成电压第一行的数据一致性问题绝对值得展开。刚开始我用单缓冲区DMA不断往里面写主循环同时从里面读结果画出波形会出现水平方向的随机断层。后来改成双缓冲DMA写完半区后中断通知CPU去读另外半区问题立刻消失。这是在示波器软件里最核心的一个架构决策。4.2 触发功能让波形稳住没有触发功能的示波器波形会到处乱跑看起来就像在闪烁抖动。商用示波器的触发电路是硬件实现的精度高。我们这里用软件实现了最基础的上升沿触发原理很简单设定一个触发电平阈值在采样缓冲区里查找从低于阈值到高于阈值的跳变点从这个点开始绘制波形。uint16_t Find_Trigger_Point(const uint16_t *buf, uint16_t len, uint16_t level) { uint16_t i; for (i 1; i len; i) { if (buf[i - 1] level buf[i] level) { return i; } } return 0; // 没找到触发点从头开始画 }实际效果是在稳定的周期信号下波形能基本稳定在屏幕上不再乱跑。但这个简易触发有两个明显缺陷一是如果信号本身噪声很大上升沿的判定位置会有微小抖动二是如果信号频率很低采样缓冲区里可能完全没有完整信号周期导致找不到触发点。对入门项目来说能到这个程度就不错了。如果你后续想做进阶版本可以考虑把触发方式升级为硬件触发。F407的ADC本身带模拟看门狗可以设定一个阈值超过阈值产生中断但精度和实时性还是有限。真正要硬件触发得外接比较器芯片那就是另一个量级的设计了。4.3 几个容易被忽视的细节最后说几个调试中花了不少时间排查的细节这些坑并不在课本上但实际做项目时一定会遇到第一个是DMA中断里访问标志位的问题。我一开始在DMA中断服务函数里直接设置一个全局变量标志主循环轮询这个标志去绘制按理说没什么问题。但后来发现如果主循环里面某个函数耗时过长比如画网格的函数一次要跑几十毫秒可能会导致错过下一次DMA中断标志波形会周期性卡顿。解决的办法是在DMA中断里只把数据复制出来用memcpy或直接循环拷贝1024个半字复制完立即返回绘制放到主循环里做。第二个是ADC采样时间的选择。F407的ADC采样时间可以设置为3到480个时钟周期采样时间越短源阻抗的影响越大。如果你用PA1引脚直接接信号源而没有加缓冲器比如运放跟随信号源内阻大的话采样时间短会导致采样值不准确。我实际测试时用了一个信号发生器50Ω内阻3个时钟周期的采样时间没问题但换到高阻信号源就出现幅度偏低的现象。建议在输入引脚前加一个电压跟随器这样采样时间可以设得保守一点。第三是版本兼容性问题。我最初用的是HAL库写代码很漂亮但调试时发现HAL库的ADCDMACIRCULAR模式的组合在某些早期版本有bug转换大字节序的数据时偶尔会丢失数据。后来换回标准外设库底层逻辑更可控问题就没有了。不是说HAL库不行而是这种高速采样的应用场景标准库的底层控制力更强也更容易理解数据流动的过程。写在最后的话整个项目做下来我个人最大的体会是示波器这个工具用起来是黑盒自己做一遍才能明白里面全是工程细节的取舍。采样率和精度怎么平衡触发方式怎么选缓冲区怎么设计每一点都是性能、复杂度、成本之间的权衡。很多时候没有绝对正确的答案只有适合你需求的方案。这个项目做完之后还可以往很多方向扩展增加FFT频谱分析功能F407的DSP指令集跑FFT非常合适、增加双通道、加SD卡存储波形数据、甚至做一个简单的任意波形发生器配合使用。源代码文件已经整理好了所有配置都在bsp_adc.c、bsp_tim.c、bsp_lcd.c这几个文件里有兴趣的朋友可以直接拿去改。我的建议是不要原封不动地烧录运行而是拿着这个框架自己重新写一遍初始化和绘制逻辑这样学到的东西比看十遍代码都多。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →