尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32C8T6+OLED波形显示完整教程:ADC采样与DMA搬运实战

STM32C8T6+OLED波形显示完整教程:ADC采样与DMA搬运实战 简介一套基于STM32C8T6的mini版示波器工程文件定位为嵌入式波形显示入门项目适合电子爱好者、单片机学习者及STM32入门开发者。工程利用芯片内置ADC完成模拟信号采样通过SPI或I2C接口驱动0.96/1.3寸OLED屏实现实时波形绘制代码覆盖数据采集、信号处理滤波/峰值检测与图形渲染完整链路并涉及中断或DMA等高效传输方式。压缩包共193个文件核心为C源文件与头文件另含Keil工程配置、hex/axf烧录文件及编译中间文件整体仅4.66MB下载后可直接打开工程查看或烧录验证。目前已有1962人学习下载。资源包含带详细中文注释的完整工程可帮助读者快速掌握ADC采样、OLED驱动、定时器调度及示波器底层实现思路也可作为后续扩展FFT或双通道显示的基础模板。 手头刚好有人丢过来一个压缩包名字就叫“STM32C8T6OLED波形显示.rar”里面是STM32F103C8T6也就是网上烂大街的C8T6最小系统板加0.96寸OLED屏做实时波形显示的完整工程。拆开看了看代码里用了HAL库I2C驱动SSD1306配合ADC采样和DMA搬运数据在OLED上滚动绘制正弦波、方波和电压变化曲线。这个包特别适合刚学完点灯、想往“采集显示”这条链路上走的同学也适合做信号检测、简易示波器、传感器数据可视化这类小项目时直接抄作业。这篇就围绕这个工程把从接线到代码移植、从波形绘制到问题排查的完整过程全拆开讲一遍。1. 整体设计思路为什么是C8T6配OLED1.1 硬件选型背后的逻辑先聊硬件选型。STM32F103C8T6这颗芯片在嵌入式圈子里基本属于入门必修课它是一颗Cortex-M3内核、主频最高72MHz、Flash 64KB、SRAM 20KB的小芯片。很多人在选型时会纠结它性能够不够实际上做OLED波形显示这种任务是绰绰有余的。ADC是12位、最多10个通道采样速度最高1MHz配合DMA几乎不占CPUOLED屏用的是SSD1306驱动芯片分辨率128x64I2C接口下刷新一帧也就几十毫秒的量级完全能跟上实时波形的需求。为什么选OLED而不是TFT LCD或LCD1602这里面有一个很实际的原因OLED的像素密度高可以直接画波形曲线不需要像LCD1602那样只能显示字符而TFT彩屏虽然能干的事更多但对新手来说接线麻烦SPI驱动也没OLED这么简单。OLED屏工作时每个像素自发光对比度高功耗低尤其适合电池供电的手持小设备。而且0.96寸这个尺寸下128x64的分辨率刚好能容纳一屏波形视觉上非常合适。再一个关键点是I2C接口。OLED屏常见的接口有I2C和SPI两种这个工程用的是I2C版本。I2C只占用两根线SCL、SDA极大节省GPIO资源。对于C8T6这种引脚本来就不算宽裕的芯片来说省出的引脚可以给按键、传感器、串口调试等用途。很多新手到这里会问“I2C这么慢刷新波形会不会卡顿”实测下来I2C在400kHz快速模式下刷新一个128x64的OLED单纯传像素数据大概需要128x64x8bits除以400kHz约等于165ms看着确实不快。但波形显示不需要传输全屏像素只需要更新新采样点的那一列其余部分沿用旧数据整体刷新率就能做到人眼觉得流畅的程度。所以I2C的带宽瓶颈并没有想象中严重这也是很多开源示波器项目选OLEDI2C的原因。1.2 波形显示方案的选择波形显示的核心问题是如何在有限的屏幕空间里把连续时间信号呈现出来。目前主流的做法有两种滚动模式和扫描模式。滚动模式是每次采样点到来时把整屏波形向左移动一列然后把新点画在屏幕最右边。这种方式就像医院心电图机的显示效果实时感很强也符合看波形时“信号从右往左流过屏幕”的习惯。扫描模式则是把屏幕分成若干列每个采样周期画一列画完一整屏后自动清屏重来。这种方式适合观察单次触发信号比如捕捉一个脉冲、一个冲击响应但实时连续观察时会出现清屏重画的断档感。这个工程采用的是滚动模式这也是大多数民用示波器类小项目的通用做法。原因很直接滚动模式下画面是连续变化的不需要频繁清屏闪烁感弱。而且它天然地支持“新数据不断进来旧数据不断移出”这种FIFO式的数据流模型配合DMA循环采样非常顺滑。软件架构上这个工程的逻辑链路也很清晰ADC连续采样 - DMA把结果搬到内存缓冲区 - 定时器触发采样完成中断或DMA传输完成中断 - 在中断里做一次简单的滤波和缩放 - 调用OLED绘图函数把新数据点画到屏幕上。整条链路不需要RTOS一个主循环加几个中断回调就能搞定特别适合入门阶段理解嵌入式系统中“硬件外设中断算法”的配合方式。2. 硬件连接与初始化细节2.1 引脚分配与接线表无论做什么项目第一步永远是看原理图和手册。C8T6最小系统板引脚引出方式各异但这个工程的接法比较通用我直接给出一份可直接照抄的接线表。模块引脚STM32引脚说明OLED VCC3.3V3.3VOLED供电注意不要接5VOLED GNDGNDGND共地OLED SCLPB6I2C1_SCL接上拉电阻OLED SDAPB7I2C1_SDA接上拉电阻信号输入PA0ADC1_IN0待测模拟信号板载LEDPC13PC13指示采样运行状态这里有一个容易踩的坑OLED模块的SCL和SDA一般已经通过板载电阻做了上拉但也有部分模块没有上拉或只上了一颗。I2C总线正常工作必须有上拉电阻如果上拉缺失屏幕初始化时会出现时好时坏、隔一段时间才亮、甚至完全不亮的情况。排查方法很简单——用万用表量一下SCL和SDA对地的电压正常情况下空闲时应该都是高电平。如果量出来是低电平大概率上拉电阻没接或者接错了位置。还有一个细节是OLED供电一定要用3.3V。很多人习惯性看到模块上标着VCC就直接接5V结果OLED虽然也能亮一下但长时间工作会发热屏幕对比度下降严重的直接烧掉SSD1306芯片。这个项目里MCU是3.3V供电I2C总线的上拉电平也是3.3V所以OLED供3.3V才是最稳妥的方案。2.2 系统时钟与基础外设初始化在HAL库框架下CubeMX是生成初始化代码的利器。新建工程时芯片型号选STM32F103C8TxRCC时钟配置选择外部晶振或内部晶振都可以如果是典型的8MHz外部晶振就用PLL倍频到72MHz。C8T6跑72MHz是这个芯片的额定主频稳定性没什么问题。需要注意的是GPIO引脚速度。I2C引脚在CubeMX里一般不用手动改速度默认就行但ADC输入引脚PA0建议设置为Analog模式。很多新手在这里有个误区以为ADC输入引脚要配成Input模式其实在STM32的GPIO配置里ADC模拟输入对应的是Analog模式这样才能让外部模拟信号直接进入内部采样保持电路。配置成普通的数字输入反而会引入额外的上下拉电阻或施密特触发器影响采样精度。时钟树配置完成后生成工程代码之后要做的就是在main.c里初始化I2C和ADC外设。I2C1的初始化模式选Fast Mode400kHz这个速率对SSD1306来说是全力跑大部分模块都能稳定工作。ADC1配置成Scan Mode关闭、Continuous Conversion Mode开启、12位分辨率、数据右对齐。DMA这一块很关键在CubeMX里给ADC1添加DMA请求Mode选择Circular循环模式Data Width选Half Word因为ADC转换结果寄存器的数据是16位实际只用低12位。循环模式的DMA是这个项目能够做到“CPU不参与数据传输”的核心。DMA在循环模式下会不断把ADC转换结果搬到指定的内存数组里搬满之后自动从头开始完全不需要CPU干预。这样主循环可以专心处理显示逻辑采样数据也不会丢失。唯一要留意的是DMA搬运和CPU读取同一块内存缓冲区时会有竞争问题工程里常用的做法是开DMA半传输中断和全传输中断在中断回调里处理数据或者在主循环里轮询当前DMA的传输进度只在安全窗口内读取数据。3. OLED驱动的实现要点3.1 SSD1306初始化与坐标体系OLED屏内部的核心是SSD1306驱动芯片它内部有1KB GDDRAMGraphic Display Data RAM对应128x64像素的显存。控制方式很直接往显存地址写1就点亮对应像素写0就熄灭。I2C命令流里需要区分控制字节0x00表示后续字节是命令0x40表示后续是数据。所有绘图操作本质上都归结为“往哪个内存地址写哪个值”。初始化序列是每个OLED项目都绕不开的部分。SSD1306上电后默认处于关显示状态必须通过一系列命令把它点亮。命令包括电荷泵开关、显示时钟分频比、复用率、显示偏移、起始行地址等。这个工程用的是网上广为流传的SSD1306初始化序列实测下来兼容性很好几乎能适配市面所有0.96寸I2C OLED模块。核心代码如下uint8_t oled_init_cmd[] { 0xAE, // 关闭显示 0xD5, 0x80, // 显示时钟分频/振荡器频率 0xA8, 0x3F, // 复用率 1/64 0xD3, 0x00, // 显示偏移 0 0x40, // 起始行地址 0 0x8D, 0x14, // 电荷泵开启 0x20, 0x02, // 页寻址模式 0xA1, // 段重映射左右反转 0xC8, // COM扫描方向上下反转 0xDA, 0x12, // COM引脚硬件配置 0x81, 0xCF, // 对比度 0xD9, 0xF1, // 预充电周期 0xDB, 0x40, // VCOMH选择 0xA4, // 全部使用GDDRAM内容 0xA6, // 正常显示不是反色 0xAF // 开启显示 };这套初始化命令没有太多需要改的地方唯一一个在项目中经常调整的是0x81后面跟的对比度值。不同批次的OLED屏相同对比度下的显示效果差异很大有的偏暗有的则正常。如果屏幕亮度太低先把0xCF改成0xFF或0x80试一下。SSD1306的坐标体系要特别注意。它把128x64的显存分成了8页每页8行像素渲染方式有点像打字机先定位到哪一页再定位到哪一列然后连续写入一字节数据这一字节的每一位对应这一页内从上到下的8个像素点。所以在画波形时不能像普通图形库那样直接往任意(x, y)坐标写点必须先计算目标Y坐标属于哪一页、页内偏移是多少然后做位运算。3.2 画点函数与汉字显示波形显示的基础是画点。工程里常见的实现方式如下// 在(x, y)处画一个点color1点亮color0熄灭 void OLED_DrawPoint(uint8_t x, uint8_t y, uint8_t color) { uint8_t page y / 8; uint8_t offset y % 8; if (x 128 || y 64) return; if (color) { OLED_GRAM[page][x] | (1 offset); } else { OLED_GRAM[page][x] ~(1 offset); } }这里面OLED_GRAM[8][128]就是用来模拟SSD1306显存的一块二维数组程序想做任何绘图操作时都先改这块内存最后一次性把整个GRAM刷新到屏幕刷屏函数如下void OLED_Refresh(void) { for (uint8_t page 0; page 8; page) { OLED_WR_CMD(0xB0 page); // 设置页地址 OLED_WR_CMD(0x00); // 设置列地址低4位 OLED_WR_CMD(0x10); // 设置列地址高4位 for (uint8_t col 0; col 128; col) { OLED_WR_DATA(OLED_GRAM[page][col]); } } }这个“离屏缓冲一次性刷新”的架构是OLED绘图的黄金规则。如果在画线过程中直接往屏幕写数据人眼看到的效果就是东一笔西一笔乱画还伴随严重的闪烁。而有了GRAM缓冲后所有绘图操作都是内存操作显示效果干净利落。汉字显示是本工程的另一个亮点。SSD1306本身不认识任何字符更没有字库所以显示汉字必须自己取模。网上的取模软件比如PCtoLCD2002支持把任意汉字转成16x16或更大规格的点阵数据。取模设置有几个参数要格外注意阴码/阳码、逐行式/逐列式、字节正序/反序。不同的取模方式对应不同的显示代码工程里如果更换字库经常会出现画面变成镜像或全乱码的情况根源往往就是取模方式不对。以16x16汉字为例一个字占32个字节先写左边16列的上半8行再写左边16列的下半8行然后处理右边这是逐列式的典型输出顺序。工程里的显示函数只需要把字模数据逐字节写入GRAM的对应位置即可void OLED_ShowChinese(uint8_t x, uint8_t y, const uint8_t *font) { for (uint8_t i 0; i 16; i) { for (uint8_t j 0; j 2; j) { uint8_t byte font[i * 2 j]; for (uint8_t bit 0; bit 8; bit) { if (byte (0x01 bit)) { OLED_DrawPoint(x i, y j * 8 bit, 1); } } } } }实际调试时有个小技巧数据显示不对时先在电脑上用字模软件打开点阵预览图和屏幕上的显示效果对照这样能快速判断是取模方向问题还是位序问题。取模问题占汉字显示故障的九成以上剩下的多是坐标拼写错误或字模数组越界。4. ADC采样与波形绘制的核心实现4.1 ADC DMA 定时器联动采集波形显示的质量主要取决于采样策略而不仅仅是代码本身。这个工程在ADC连续扫描的基础上用定时器触发ADC转换做到严格的等间隔采样。因为波形也好频率分析也好都要求采样数据在时间轴上均匀分布。如果单纯靠ADC连续转换数据之间的间隔可能因中断优先级、总线占用等因素而波动画出来的波形会有不规则的扭曲。用TIM2触发ADC是很经典的做法。CubeMX里把TIM2配成PWM输出模式频率设成想要的采样率比如20kHz。PWM的每个上升沿都会触发一次ADC转换转换完成后DMA自动把结果搬到缓冲区。这样整条链路完全由硬件控制CPU只负责在合适时机读取缓冲区的数据。CubeMX中TIM2的配置如下Prescaler设成71Counter Period设成49则PWM频率为72MHz / (711) / (491) 20kHz对应50us采样间隔。ADC的采样时间设置也很关键。STM32F103的ADC采样时间可以配置为1.5周期到239.5周期不等。如果信号源内阻比较高采样时间太短会导致采样电容充不满影响转换精度。做波形显示这种精度要求不算极端的场景一般把采样时间设成55.5周期或239.5周期。虽然采样率会相应下降但显示的波形会更平滑毛刺也更少。我之前调试时试过1.5周期采样结果波形上全是锯齿状噪声换了239.5周期后好很多。DMA缓冲区的设计需要仔细考虑。缓冲区大小决定了主循环里能以多长的“数据块”为单位来处理数据。如果设成512个uint16_tDMA半传输中断一次全传输中断一次各自代表一个分段点。这样在主循环里处理数据时可以安全地访问已经完成的那一半数据而另一段还在被DMA继续填充。#define ADC_BUFFER_SIZE 512 uint16_t adc_buffer[ADC_BUFFER_SIZE]; // DMA传输过半中断回调 void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { process_wave_data(adc_buffer, 0, ADC_BUFFER_SIZE / 2); } } // DMA传输完成中断回调 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc-Instance ADC1) { process_wave_data(adc_buffer, ADC_BUFFER_SIZE / 2, ADC_BUFFER_SIZE / 2); } }4.2 数据缩放与波形坐标变换ADC采集到的是12位原始值范围0到4095。OLED只有64行像素屏幕上的Y坐标范围是0到63。直接把原始值除以64放上去波形会占满整个屏幕并且贴顶贴底看起来很难受。常规做法是给波形预留上下边距假设波形的中心幅度对应2V那可以把Y坐标计算做成动态的也可以固定一个映射系数。这个工程里的方式比较简单实用先对ADC值做中值滤波或滑动平均降低毛刺然后把电压值映射到屏幕坐标uint8_t wave_y_from_voltage(uint16_t adc_value) { // 假设参考电压3.3V12位ADC最大值4095 float voltage (float)adc_value * 3.3f / 4095.0f; float y_float (1.0f - (voltage / 3.3f)) * 56.0f 4.0f; // 上下各留4像素边距 return (uint8_t)y_float; }其中“1.0 - voltage / 3.3f”是为了做Y轴翻转否则电压越高点在屏幕上的位置越靠下不符合人们看示波器的习惯。屏幕顶部对应高电平、底部对应低电平这样看波形最直观。关于X轴滚动模式下每个新数据点对应新的一列需要把整个GRAM左移一列。实现方式有两种一种是每次刷新时按列拷贝GRAM数据复杂度O(128x8)另一种是维护一个偏移量画新点时写到循环缓冲区对应的列地址上刷屏时把缓冲符映射到屏幕列。前者写法直观但费时间后者效率高。工程里如果追求显示流畅建议用第二种思路用DMA刷新整屏时按偏移量重排数据。4.3 波形绘制的完整流程把上面所有模块串起来波形显示的主循环逻辑大致如下系统上电初始化时钟、I2C、OLED、ADC、DMA、定时器。OLED显示开机界面比如显示“Wave Demo”和一个静态正弦波形让用户确认屏幕工作正常。启动定时器开始ADCDMA采集。主循环不断检查DMA缓冲区的半传输、全传输标志位。当检测到新的数据块就绪对数据做滤波、缩放处理然后把旧波形左移一列画一个新点。每隔N次采样清一次屏防止整屏都是扫描线的残留。一个核心的绘图逻辑是“移动画点”移动操作处理不好会导致拖影即旧波形没有被完全擦除。正确的顺序是先擦除最左边那一列对应的像素然后把GRAM里所有列左移一位最后在最右侧画新的波形点。擦除时不能直接清掉整列否则波形线会断裂应该只擦掉该列上原有波形点的那几个像素。void update_waveform(uint16_t new_adc_value) { // 擦除左边界列 for (uint8_t page 0; page 8; page) { OLED_GRAM[page][0] 0; } // 整屏左移一列 for (uint8_t page 0; page 8; page) { for (uint8_t col 0; col 127; col) { OLED_GRAM[page][col] OLED_GRAM[page][col 1]; } OLED_GRAM[page][127] 0; } // 计算新点坐标并画在最右侧 uint8_t y wave_y_from_voltage(new_adc_value); OLED_DrawPoint(127, y, 1); // 刷屏 OLED_Refresh(); }这一段代码的核心思想是“双缓冲滚动擦除”。直接修改GRAM数据避免每次采样都全屏清空重画又因为OLED_Refresh一次把GRAM完整刷到屏幕所以不会出现撕裂。实际显示效果是波形匀速向左移动画面非常顺滑。关于采样率和显示刷新率的关系需要做一个权衡。假设OLED的I2C刷新全屏一次要消耗约150ms如果20kHz采样每个采样点都刷新屏幕那刷屏压力太大波形也看不出连续滚动。一般策略是每采集到若干点才更新一次显示列本工程把这个“降采样倍数”设为10也就是说每10个ADC采样点映射到一个屏幕列。之后再把多个点的值做一个平均或取峰值用来代表这一列的波形幅度。这样既保证了波形足够平滑又让人眼能看到清晰的滚动。5. 常见问题与排查技巧实录5.1 典型故障速查表现象可能原因排查方式OLED完全不亮供电不对、I2C地址错误、初始化序列没跑量VCC是否为3.3V检查0x78/0x7A地址上电后全屏雪花点初始化时序不对、I2C通信不稳定降低I2C速度到100kHz测试检查上拉电阻波形显示为一条直线信号没接、ADC配置错误、DMA没启动检查PA0电压是否变化打印ADC原始值波形上下颠倒坐标映射Y轴方向反了调整y_float计算中的翻转逻辑波形毛刺特别多采样时间太短、电源纹波大、信号没滤波增大ADC采样时间给电源加0.1uF去耦电容波形右端有拖影擦除逻辑不完整确认左移前是否清空了目标列画面闪烁严重全屏刷新太频繁降低显示刷新频率或改用局部刷新OLED上电后不亮是最常见的故障很多人第一反应是板子坏了或屏幕坏了其实大概率是I2C地址没配对。SSD1306的I2C地址通常是0x787位地址0x3C或0x7A7位地址0x3D这个由模块背面的电阻决定。地址搞错表现为OLED初始化完全无响应查这个最直接的办法是写一个I2C扫描程序枚举所有7位地址看看哪个地址有ACK应答。5.2 电压换算和参考电压选择ADC电压换算还有一个很容易被忽略的坑STM32F103的ADC参考电压VREF默认接到VDDA也就是芯片供电电压3.3V。但这个3.3V并不是所有板子都是精确的3.3VLDO的输出可能在3.28到3.32之间浮动。如果直接用理论值3.3V去算电压显示出来的电压值跟万用表实测值会有一个系统偏差。精度要求高时最好的办法是用一个已知的精确电压比如用一个高精度基准源输出2.5V去校准反向推算真实的VREF值。工程代码里如果写成定点数计算在校准时只需修改一个常数即可。5.3 采样缓冲区越界与数据竞争DMA回调和主循环之间读取同一个缓冲区如果没有同步机制会出现数据错位甚至崩溃。这个工程的做法是标志位加双缓冲DMA传输完成中断里置位一个volatile标志主循环里检查到标志后再处理数据同时ADC DMA配置两个缓冲区交替使用保证CPU和DMA不会同时访问同一个缓冲区。这种方式比“锁”更轻量也更容易理解和调试。还有一点值得注意HAL_ADC_ConvHalfCpltCallback和HAL_ADC_ConvCpltCallback里的代码要尽量精简。中断服务函数执行时间过长会影响DMA搬数的节奏严重时产生溢出错误。我在实际测试中发现如果在这个回调里做太多浮点运算或调用耗时的OLED刷屏函数波形会出现明显的跳变和丢失。正确做法是回调里只设置标志位、更新计数真正的数据处理和显示都放到主循环做。5.4 关于OLED“连上电源就亮吗”的疑问有些朋友拿到OLED模块后直接接上VCC和GND发现屏幕不亮就怀疑模块坏了。实际上SSD1306上电后默认处于睡眠模式需要主控芯片发初始化命令才会点亮。所以OLED“连上电源就亮”这种说法并不严谨它是一种显示驱动芯片不是简单的背光屏。“接上电源就亮”的往往是LCD1602带背光的液晶模块OLED反而必须靠MCU跑完初始化序列才正常工作。这也是本工程里甚至不需要额外加开机检测逻辑的原因——如果初始化命令顺利下发屏幕上自然会出现波形或字符如果屏幕黑着直接可以判断是通讯问题而非屏幕供电问题。6. 项目完成后如何进一步扩展这个工程做完等于打通了“模拟信号采集 DMA数据传输 OLED图形显示”的整条技术链路后续扩展空间非常大。想做简易示波器可以加按键和编码器调节纵向增益、横向时间轴再把ADC采样率提升配合定时器触发做成多档位可调的波形观测工具。目前这个工程的采样率约20kHz能观察几kHz以内的信号这是入门级示波器的典型规格。如果换成STM32F303或G4系列内部带了更高性能的ADC和运放能实现更好的模拟前端性能。想做数据记录仪可以把采样数据点按时间存入Flash或SD卡配合RTC模块记录时间戳上位机通过串口读取后再绘制曲线。OLED屏此时只作为实时监视窗口真正的大数据量分析交给上位机。还想继续挖掘OLED的显示潜力的话可以做屏幕旋转、反色显示、简单菜单系统、二级状态页。C8T6的20KB SRAM足够放几个菜单页面和字库做一个小型仪表完全没问题。这个工程最大的价值在于它让你真正理解了“硬件触发 DMA 离屏渲染”这三板斧这套思路放在任何嵌入式图形界面项目里都通用。我在实际调试中的体会是做这类项目性子不能急别上来就堆代码先把接线和通讯调通再用一个简单的采样程序确认ADC数值稳定最后才做波形绘制。分阶段验证每一步都有明确的观测结果整体推进会非常顺。如果直接整套工程烧录进去发现波形不动排查起来反而无从下手。而当你亲眼看到正弦波在OLED上丝滑滚过时那种“整条链路都赢在掌控之中”的感觉远比代码本身更有成就感这也是做嵌入式最迷人的地方。本文还有配套的精品资源点击获取
返回列表