
简介STM32C8T6OLED波形显示工程压缩包是一款基于ARM Cortex-M0内核的mini版示波器实现方案适合嵌入式初学者与进阶开发者学习MCU驱动、ADC采样、信号处理及OLED图形渲染。包内共193个文件主要包含36个h头文件、34个c源文件、35个d及35个o等编译中间文件以及uvprojx工程文件、hex烧录文件、map映射文件和bat脚本等整体大小仅4.66MB结构清晰便于移植和二次开发。项目支持0.96和1.3寸OLED屏幕可通过SPI或I2C接口驱动结合STM32C8T6内置ADC与DMA实现实时波形采集与显示代码中涵盖滤波、峰值检测等信号处理算法并配有详细注释。已有1962人学习下载对于想要掌握STM32系列MCU开发、理解数字示波器原理或制作便携式测量工具的开发者具有直接参考价值也可作为课程设计与电子竞赛的实用模板。 最近在调试一块STM32F103C8T6的小板子顺便把手头那块0.96寸OLED利用起来做了一个简单的波形显示器。一开始只是想把ADC采集到的电压值在屏幕上画出来结果越做越像那么回事最后硬是折腾出了一个能实时刷新、能显示正弦波和方波的“迷你示波器”。整个过程踩了不少坑也把HAL库、DMA、I2C这些外设重新捋了一遍。这篇东西就把整个项目的思路、硬件接线、代码实现和排坑过程都记录下来给同样想用C8T6驱动OLED显示波形的人做个参考。这个项目适合刚学完STM32基础、想动手做点实用小工具的开发者也适合正在准备电赛或课程设计的学生。难度大概是中等偏上一点核心就两块一是把OLED驱动跑通并整理出适合画波形的底层函数二是让ADC高效采样并把数据映射到屏幕上。两块分开都不难合在一起就考验代码的组织能力了。1. 项目整体思路与方案选型1.1 为什么选STM32C8T6加OLEDSTM32F103C8T6是很多人入门STM32时用的第一颗芯片Cortex-M3内核主频72MHz64KB Flash20KB RAM。这在今天看参数很一般但做波形显示这种活绰绰有余。关键是它自带12位ADC采样速度最高能到1MHz左右对音频级别的波形显示完全够用。搭配0.96寸OLED一块屏才十几块钱128x64的分辨率虽然不高画个波形轮廓、显示几个参数还是没问题的。选OLED而不是LCD1602或者TFT主要看中三点。OLED是自发光不需要背光功耗低对比度极高在实验室里看起来非常清晰。其次OLED模块大多集成了SSD1306驱动芯片I2C接口只需要两根线接线上省事很多。再就是OLED的像素点是独立的可以精确控制每一个点画波形比字符型LCD灵活太多。这三点叠加OLED就成了这种小型仪表类项目的第一选择。1.2 波形显示的技术难点与破解思路刚接到这个需求时我脑子里第一个想法是这不就是把采到的值画在屏幕上吗真做起来才发现有三个难点要解决。第一个是采样与显示的时序协调。OLED通过I2C通信标准模式下只有100kbps就算跑到400kbps刷新一帧全屏数据也要几十毫秒。而ADC采样又是连续不断的如果在刷屏期间堵住采样波形就会出现断裂或者变形。所以必须让采集和显示互不干扰我最终的方案是用DMA循环采样数据往缓冲区里存CPU只管画屏两边各干各的。第二个难点是坐标映射。ADC是12位分辨率数值范围0到4095而OLED屏幕纵向只有64个像素。直接把原始值映射上去会导致波形要么顶天立地要么挤在中间一条线。需要先计算数据的最大值和最小值再做一个缩放和居中处理让波形始终铺满屏幕且不溢出。第三个难点是刷新速度。OLED全屏刷新一次要写1024字节每次都写全屏肯定卡成PPT。优化的思路是只更新波形区域并且把OLED的显存映射到MCU内部的一个buffer里先在内存里画好再一次性刷过去。这样和屏幕的交互次数大大减少刷一帧波形只需要写对应区域的数据。2. 硬件搭建与OLED驱动2.1 引脚接线与I2C配置硬件上我用的是STM32F103C8T6最小系统板加一块0.96寸I2C接口的OLED模块再加一个电位器用来模拟输入信号一个按键用来切换显示模式。接线表非常清晰OLED模块引脚STM32引脚说明VCC3.3V电源GNDGND地SCLPB6I2C1时钟线SDAPB7I2C1数据线电位器中间脚PA0ADC1通道0输入I2C的上拉电阻在OLED模块上已经集成好了不需要额外接。这里有一个关键点C8T6的硬件I2C外设其实有点娇气网上吐槽的人一大把但用HAL库基本不会出问题前提是初始化参数要写对。我用的时钟是100kHz标准模式地址是0x788位写模式也可以写成0x3C左移一位注意区分8位地址和7位地址。C8T6上I2C1对应的引脚是PB6和PB7这个别搞混。我见过有人把SCL接到PB3、SDA接到PB4想用软件模拟I2C也不是不行但后面又要复用定时器等外设冲突难免。建议优先用硬件I2C干净省心。2.2 HAL库OLED驱动从底层到启动流程用HAL库驱动SSD1306关键就两个函数写命令和写数据。SSD1306的I2C协议有一个固定套路先发控制字节0x00表示后面跟着的是命令0x40表示后面是数据。HAL库里对应的是HAL_I2C_Mem_Write这个函数第二个参数是设备地址第三个参数是控制字节。#define OLED_ADDR 0x78 void OLED_WriteCmd(uint8_t cmd) { HAL_I2C_Mem_Write(hi2c1, OLED_ADDR, 0x00, I2C_MEMADD_SIZE_8BIT, cmd, 1, 100); } void OLED_WriteData(uint8_t *data, uint16_t len) { HAL_I2C_Mem_Write(hi2c1, OLED_ADDR, 0x40, I2C_MEMADD_SIZE_8BIT, data, len, 100); }初始化SSD1306有一串固定的命令序列网上到处都能找到无非是开显示、设置时钟分频、设置电荷泵、设置地址模式等。这里有一个极其容易踩的坑SSD1306的电荷泵必须手动打开否则屏幕永远是一片黑。我最初折腾了半小时一直在查接线和地址最后发现就是漏了电荷泵那一条命令。驱动层还差一个关键机制显存buffer。我在RAM里开了一个128x64/8也就是1024字节的数组所有绘图操作都在这个数组里进行修改完后再整包发到OLED的GRAM。SSD1306内部有一块和屏幕像素一一对应的GRAM往GRAM写数据就能控制像素亮灭。这样做的好处是绘图时不需要频繁I2C通信要显示的时候一次推过去。uint8_t oled_buffer[128 * 64 / 8]; void OLED_Refresh(void) { OLED_WriteCmd(0x21); // 设置列地址范围 OLED_WriteCmd(0); OLED_WriteCmd(127); OLED_WriteCmd(0x22); // 设置页地址范围 OLED_WriteCmd(0); OLED_WriteCmd(7); OLED_WriteData(oled_buffer, sizeof(oled_buffer)); }这套结构画点、画线、画矩形都变得极其简单。比如画一个点先算出点在buffer里对应哪个字节的哪一位然后对该字节做置位或清零操作。显示波形的时候轮廓就由大量点连成线组成。3. 采样与波形绘制核心实现3.1 ADC采样与DMA传输接下来是数据采集部分。我用ADC1的通道0采集PA0引脚的模拟电压配置成12位分辨率采样时间尽量短一点我选择了1.5周期这样在72MHz时钟下单个通道的采样率能拉得比较高。为了提高数据稳定性我没有直接用单次采样的值而是用DMA循环模式连续采集。DMA会在后台把ADC转换结果搬运到数组里完全不需要CPU干预。这个设计非常关键等于给CPU腾出了全部精力去做绘图计算。#define SAMPLE_NUM 1024 uint16_t adc_buffer[SAMPLE_NUM]; void ADC_Init(void) { __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_DMA1_CLK_ENABLE(); DMA_HandleTypeDef hdma_adc; hdma_adc.Instance DMA1_Channel1; hdma_adc.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_adc.Init.PeriphInc DMA_PINC_DISABLE; hdma_adc.Init.MemInc DMA_MINC_ENABLE; hdma_adc.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_adc.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_adc.Init.Mode DMA_CIRCULAR; hdma_adc.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_adc); __HAL_LINKDMA(hadc, DMA_Handle, hdma_adc); hadc.Instance ADC1; hadc.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc.Init.ScanConvMode ADC_SCAN_DISABLE; hadc.Init.ContinuousConvMode ENABLE; hadc.Init.DiscontinuousConvMode DISABLE; hadc.Init.NbrOfConversion 1; HAL_ADC_Init(hadc); ADC_ChannelConfTypeDef sConfig {0}; sConfig.Channel ADC_CHANNEL_0; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_1CYCLES_5; HAL_ADC_ConfigChannel(hadc, sConfig); HAL_ADC_Start_DMA(hadc, (uint32_t *)adc_buffer, SAMPLE_NUM); }这里有几个细节值得说一下。DMA的模式必须用循环模式否则采满1024个点后DMA会停止传输后面的数据就固定不动了。如果用普通模式你需要在DMA传输完成中断里重新开启传输但那样会有一段空白时间影响波形连续性。PeriphDataAlignment和MemDataAlignment都要设置成半字因为12位ADC结果存在16位变量里正好合适我之前顺手填了字节对齐结果数据全乱了。DMA通道选择也要注意ADC1的请求是映射在DMA1的通道1上这个查参考手册的DMA请求映射表就能确认。C8T6的DMA1只有7个通道分配错了一律不工作。3.2 数据预处理与坐标映射原始ADC值拿到手之后不能直接上屏。因为波形可能很小比如只用了0到500的范围画出来就是紧贴屏幕底部的一条线。反过来如果信号超过参考电压波形又会削顶。所以每一帧数据都要做一次归一化处理。我的做法是从前几百个采样点里找出最大值和最小值然后计算垂直方向的比例因子。具体处理代码如下uint16_t min_val 4095, max_val 0; for (int i 0; i DISPLAY_POINTS; i) { if (adc_buffer[i] min_val) min_val adc_buffer[i]; if (adc_buffer[i] max_val) max_val adc_buffer[i]; } if (max_val min_val) max_val min_val 1; float scale 56.0f / (max_val - min_val);这里把屏幕纵向范围限制在8到60之间上下各留了一点边界避免波形顶部和底部贴死屏幕边缘看起来更舒服。横向方面屏幕是128像素宽我从中截取120个点用于显示左右各留4像素的边距最左边可以用来显示一个竖坐标轴最右边留白避免波形顶墙。还有一个容易忽略的点ADC采样得到的数组是在不断更新的如果在绘图的同时DMA还在往同一个数组里写数据可能画到一半数据变了显示出来就是断断续续的波形。我采用的策略是绘图前先拷贝一份数据快照到临时数组再用临时数组绘图。数据拷贝1024个uint16_t用memcpy就够了耗时可以忽略不计。3.3 波形绘制算法波形绘制的核心就是把这些点逐一连成线段。128x64分辨率下水平和垂直相邻像素的连线效果基本能呈现出光滑的曲线。画线我用的是经典的Bresenham算法HAL库和标准库都没有现成函数只能自己写。void OLED_DrawLine(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1) { int dx abs(x1 - x0), sx x0 x1 ? 1 : -1; int dy -abs(y1 - y0), sy y0 y1 ? 1 : -1; int err dx dy, e2; while (1) { OLED_DrawPixel(x0, y0); if (x0 x1 y0 y1) break; e2 2 * err; if (e2 dy) { err dy; x0 sx; } if (e2 dx) { err dx; y0 sy; } } }然后每两个相邻采样点之间调用一次画线函数就形成了完整波形。这里要提醒的是ADC的原始值是大小随电压变化而屏幕坐标是Y轴向下增大所以映射时要做一次上下翻转否则波形看起来是倒的。具体就是y Y_OFFSET (56 - (adc_buffer[i] - min_val) * scale)这样电压高的值对应的Y坐标更小符合示波器正电压向上的直觉。绘制完波形后还能顺便在屏幕角落显示几个关键参数比如当前最大值、最小值以及输入电压值。显示数字需要字库我在代码里内置了一个小的ASCII字库每个字符5x7像素占5个字节。这部分虽然代码量不大但实用性很强后面扩展其他功能也用得上。4. 常见问题与排查技巧4.1 OLED不亮或花屏这是我遇到最多的问题也是初学者最容易卡住的地方。现象分两种完全没反应或者显示乱码雪花点。完全没反应先量一下OLED的VCC引脚有没有3.3V电压再看SCL和SDA有没有波形。用逻辑分析仪抓一下I2C总线如果发现只有起始信号没有数据大概率是地址错了。SSD1306的I2C地址其实由模块上的电阻决定常见的是0x3C但有些模块焊的是0x3D。用HAL库时要清楚HAL_I2C_Mem_Write第二个参数要传8位地址也就是0x3C左移一位变成0x78。写了0x3C进去每次通信都会失败屏幕自然不亮。花屏的原因则多半是初始化序列不完整或者电荷泵没开。网上流传的各种初始化代码大同小异但有些为了省字节把关键命令砍掉了。我建议用官方SSD1306 datasheet里的配置流程一条命令都不要少。还有个隐蔽问题如果OLED供电电压不稳比如电池供电电压掉到3V以下屏幕也可能花屏或闪烁这时要检查电源而非代码。4.2 波形跳动、噪声大波形在屏幕上一直抖看起来不稳定大概率是参考电压或者采样时序的问题。C8T6的ADC参考电压引脚是VDDA最小系统板的VDDA通常直接接3.3V如果供电本身不干净采样值就会带上噪声。我加了两个措施ADC采样值做3次中值滤波然后对绘图用的数据再做一次滑动平均。虽然牺牲了一点点响应速度但波形线变得非常平滑。另外一个隐蔽问题来自电位器本身。如果输入信号源阻抗太高ADC内部采样电容充不满采样结果会偏低且不稳定。STM32的ADC建议外部信号源阻抗不超过10k欧姆用电位器分压时最好把阻值选小一点或者加一个运放缓冲。有的朋友用10k甚至100k电位器直接接PA0波形乱跳不要怪代码先检查信号源阻抗。4.3 刷新率上不去一开始我的波形显示只有每秒几帧肉眼看着就是一卡一卡的。分析后发现问题出在两个方面。一是使用软件模拟I2C时延时太狠每次传输都等了很久。换成硬件I2C并把时钟提到400kHz后刷新时间直接降了一倍。二是每次全屏刷新把整个1024字节的buffer都推给OLED。其实波形显示只需要屏幕下半部分把刷新范围限制在波形区域后一帧数据量减少到原来的三分之一左右刷新率明显提升。如果你的主循环里还在用HAL_Delay做延时刷新也要注意延时时间不要设置太长。我最终把主循环的延时控制在10ms以内这样ADC数据的更新频率和屏幕刷新率都能维持在比较舒服的水平。5. 再向前一步项目扩展与想法做完基础版本之后这套系统的可玩性其实还有很多。比如加入按键切换触发模式让波形平稳地停在屏幕上这就需要引入触发判据的逻辑在DMA缓冲区里查找一个上升沿或者下降沿然后从这个位置开始绘图。再比如把采样结果通过串口或者USB发给上位机结合Python的matplotlib画出更精细的波形这就有点虚拟示波器的味道了。如果对OLED的刷新率还不满意可以换用SPI接口的OLED模块速度能比I2C快好几倍。C8T6的SPI1可以跑到18MHz左右刷一屏的时间从几十毫秒压缩到几毫秒实时性会显著提升。当然SPI要占用更多引脚接线也稍微复杂些但作为硬件方向的进阶练习非常值得。最后分享一个提升幸福感的小技巧在OLED初始化之后加一个开机自检画面把所有像素点亮一遍再熄灭刚好大概一秒钟既验证了屏幕通信正常又有那么一点仪式感。这个项目折腾下来最大的感触是C8T6这种“老古董”芯片其实潜力很大很多时候我们觉得它不行只是没把DMA和中断的配合用好。等把底层外设玩明白了再去做更复杂的信号处理也就有了底子。本文还有配套的精品资源点击获取