资讯动态

STM32F103 ADC多通道+DMA+OLED显示完整工程实现

发布时间:2026/9/15 16:22:15 来源:尧图企业网站定制
简介这是一份基于STM32F103微控制器的AD多通道采集与OLED显示完整工程资源面向嵌入式系统初学者与开发者适用于传感器网络、数据记录、信号分析等常见场景解决从多路模拟信号采集、ADC配置到OLED实时显示的开发需求。包内共433个文件约13.43MB以79个C源码和87个H头文件为主涵盖ADC初始化、数据读取、OLED驱动、数据处理等完整功能模块同时包含uvprojx等工程配置文件、hex烧录文件及编译中间产物方便直接打开工程或对照学习。已有386人浏览学习。通过该资源读者可深入理解STM32CubeMX、HAL库配置方式掌握多通道采样、中断处理、滤波算法以及I2C/SPI控制OLED的完整流程同时兼顾电源管理与异常处理设计思路无论入门还是项目复用都极具参考价值。1. 一块屏、八路采样这个标题背后的真实需求把AD多通道采集、STM32F103和OLED三个词拼在一起最常见的现实场景就出来了一块F103最小系统板上接了电位器、电流互感器二次侧或热敏电阻分压网络要求在调试台上不连电脑就能看到多路电压值。最常见做法是用STM32CubeMX把ADC配置成多通道加DMA循环采集再用I2C挂一块0.91寸OLED屏上直接输出每一路的实时电压省掉上位机和串口线。新手常犯的错是裸轮询HAL_ADC_Start加HAL_ADC_PollForConversion采完一路再切下一路数据是能出但CPU空转、通道切换有间隙采集几路之后主循环节奏全乱。标题里这组词组合起来真正值钱的是多通道和显示之间的衔接方式——DMA把采集结果连续填进缓冲区主循环只管取数、格式化、刷屏。这套路是仪表、电池监测、电源调试这类场景的通用骨架看完本文你得到的是一份能直接抄走的工程模板。2. STM32F103 的 ADC 多通道采集原理扫描规则组与 DMA 搬运2.1 规则组扫描模式通道顺序由配置决定STM32F103 的 ADC1 有 16 个外部通道但数据寄存器 DR 只有一个。所谓多通道采集本质上是用规则组Regular Group按顺序把多个通道依次接入转换器每一个通道的结果在转换完成后写入同一个 DR如果程序不及时读走下一个通道的转换结果会直接覆盖掉旧值。开启扫描模式Scan Mode后规则组的转换顺序完全由 CubeMX 里添加通道的顺序决定。也就是说 IN0、IN1、IN2 按 rank 1、2、3 排列转换结果就按这个顺序依次出现跟引脚物理编号无关。例如在 CubeMX 的 ADC1 配置面板里依次添加 IN0、IN1、IN2…IN7转换完一轮后 DR 里出现的顺序必然是 IN0 最先、IN7 最后。如果通道配置顺序写成 IN3、IN1、IN0那么缓冲区里前三项对应的是 IN3、IN1、IN0 的值——这是后面读数组判断电压最关键的前提错了整屏数据全部张冠李戴。规则组还有一个容易忽略的限制扫描模式下如果不开连续转换一轮扫完就会停住下一次触发才重新开始。配合 DMA 循环模式时一般把连续转换Continuous Conversion打开让 ADC 自己一轮接一轮跑DMA 持续把 DR 里的结果搬到内存三者形成一个不受 CPU 干预的数据流水线。2.2 DMA 循环搬运让 ADC 自己往数组里写数据多通道采集场景里中断方式不是不能用但每转换完一个通道就进一次中断8 通道就是 8 次中断/轮中断服务程序里如果再做数据处理CPU 占用率会非常难看。DMA 的价值是把从 DR 读数据这个动作从 CPU 手里拿走由 DMA 控制器直接完成而且 STM32F103 的 DMA 外设支持循环模式Circular地址自动回卷天然适合周期性刷新一批采样值的采集模型。对应的 DMA 关键参数只有四个DirectionPeripheral To Memory从外设到内存ModeCircular循环模式Peripheral IncrementDisable外设地址固定始终是 ADC DR 寄存器Memory IncrementEnable内存地址递增按顺序填数组外设地址固定、内存地址递增这一对组合容易理解源始终是同一个 DR目标是数组的连续地址。数据宽度必须设置成 Half Word16 位因为 F103 的 ADC 是 12 位分辨率转换结果存放在 16 位 DR 里。如果误设成 WordDMA 每搬一次会跨过 4 个字节缓冲区里的数据就错位了。#define ADC_CH_NUM 8 volatile uint16_t adc_buf[ADC_CH_NUM]; // 启动 DMA 采集adc_buf 会被不断刷新 HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, ADC_CH_NUM);这段代码里 adc_buf 用了 uint16_t 数组类型和 DMA 数据宽度对齐volatile是为了防止编译器把主循环里的读取优化成只读一次缓存这一点对老手也要再强调一次——DMA 在后台改这个数组CPU 侧不加 volatile优化开 O2 后很容易读到陈旧数据。启动函数只需要调用一次Circular 模式下它会一直在后台跑不需要反复 Start_Stop。2.3 采样时间与总转换耗时先算清楚再设参数ADC 的转换时间不是随便拍的。F103 的 ADC 时钟最高 14MHz一般从 APB272MHz分频得到可选 2、4、6、8 分频。12 位转换本身固定占 12.5 个 ADC 时钟周期加上用户可选的采样周期单通道总转换时间公式为Tconv (采样周期 12.5) / ADC时钟频率例如 APB2 为 72MHz、ADC 分频 6 得到 12MHz采样周期设为 71.5那么单通道耗时就是(71.5 12.5) / 12MHz 7us8 通道一轮大概 56us。如果采样周期缩到 1.5单通道降到约 1us速度提升了 7 倍但输入源阻抗稍大比如 10k 以上分压网络时采样结果会偏低因为采样电容没来得及充满电压就被切走了。采样周期的选择原则不复杂信号源阻抗大电位器、热敏电阻分压就往大里设比如 239.5信号源是运放输出这种低阻抗驱动可以往小里设换取速度。多通道采集还有一个隐性约束——如果把 8 个通道全部设成 239.5 周期每轮转换 168us对应刷新率约 6kHz对显示类和大多数传感器应用都够用却不适合需要同步高速采样的场景。工程上更常见的折中是快的通道设短周期慢速传感器通道单独设长周期混合配置省总时间。启动多通道 DMA 采集前还需要做一次片上校准HAL_ADCEx_Calibration_Start(hadc1);这一行放在 HAL_ADC_Start_DMA 之前作用是修正芯片内部的电容失调误差。F103 的 ADC 校准不需要外部基准直接在初始化后跑一次即可工厂里管这个叫去零点漂移。跳过去不调也不会完全不工作但实测中低量程段的线性度会差一点逢上精密采集就是隐患。3. 用 STM32CubeMX 配置 ADC 多通道 DMA I2C 最小工程3.1 引脚分配与时钟树先把基础工程建对打开 STM32CubeMX选择 STM32F103C8T6在 Pinout 视图里依次做三件事把 PB6、PB7 分配到 I2C1分别是 SCL 和 SDA把 PA0 到 PA7 分配到 ADC1 的 IN0 到 IN7然后进 Clock Configuration 把系统时钟设到 72MHz。ADC 时钟在 RCC 的 APB2 分频里选 6 分频12MHz这个值的意义在于离最高 14MHz 不远同时又给采样周期留了一定余量。需要注意的是硬件 I2C1 在 F103 上默认就挂在 PB6/PB7但很多开发板把 OLED 接到了别的引脚比如 PA5/PA7 或软件模拟 I2C。CubeMX 里如果选硬件 I2C1就必须用 PB6/PB7除非你改配置到 I2C2PB10/PB11。OLED 的 I2C 从机地址默认是 0x3C7 位地址写坐标是 0x78这块后面写驱动时还会用到。3.2 ADC 参数面板逐项对照这张表直接照着填进入 ADC1 的 Parameter Settings按以下表格逐项配置这些参数直接决定 DMA 能否把 8 路数据按预期顺序灌进数组配置项取值说明Scan Conversion ModeEnabled开启扫描规则组多通道按序转换Continuous Conversion ModeEnabled连续转换配合 DMA 循环实现不间断采集Discontinuous Conversion ModeDisabled不使用间断模式否则一轮只转换部分通道DMA Continuous RequestEnabled让 DMA 持续接收 ADC 请求老版本在 DMA Settings 里Number Of Conversion8规则组的通道数量不是最大通道号External Trigger Source软件触发F103 用 TIM 触发也能跑这里用软件启动最简洁ADC Clock Prescaler6 分频APB2 72MHz 分频得到 12MHz 的 ADC 时钟通道列表里依次添加 IN0 到 IN7采样时间建议先统一设 239.5等实物跑起来再根据信号源阻抗微调。这里有几个初学者容易踩的细节Number Of Conversion 设成 8 之后通道列表里必须有 8 个条目多了少了 CubeMX 会直接报错软件触发模式下不用在代码里做任何触发配置只要调用了 HAL_ADC_Start_DMA硬件自动启动第一次转换。3.3 DMA 配置与生成后的两行关键代码在 ADC1 的 DMA Settings 里添加 DMA Request方向选 Peripheral To Memory模式选 Circular数据宽度 Peripheral 和 Memory 都选 Half Word。Memory Increment 默认就是 Enabled外设地址递增保持 Disabled不要手痒把它打开——ADC 的数据寄存器是固定地址打开了 DMA 会去读一串不存在的地址。生成 MDK 工程后CubeMX 已经把初始化代码写好了但在 while 之前需要手动补两行uint16_t adc_buf[8]; // 放在文件顶部注意加 volatile // 在 MX_ADC1_Init() 之后、主循环之前执行 HAL_ADCEx_Calibration_Start(hadc1); HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, 8);HAL_ADC_Start_DMA 的第三个参数传 8指的是 DMA 缓冲区按元素计数不是字节数。因为 ADC 的数据宽度是 Half Word所以实际搬运 16 字节。很多只显示第一路的奇怪问题就出在这个参数上传成 16 会让 DMA 工作到缓冲区边界之外。3.4 OLED 接线与硬件 I2C 的备选方案0.91 寸 OLED 是 4 针或 6 针 I2C 接口接线方式很固定VCC 接 3.3VGND 接 GNDSCL 接 PB6SDA 接 PB7。如果你的板子把 OLED 接到了任意 GPIO 上那 CubeMX 里就别选 I2C1直接全配成 GPIO 输出代码里顺序翻转 SCL/SDA 模拟时序——这个方案在 F103 上其实更常见因为硬件 I2C 在这个型号上出过不少兼容性的坑很多工程师干脆默认用软件模拟 IIC 驱动 OLED。软件模拟 I2C 的好处是引脚随便选、时序自己控制但主循环里被打断时容易产生毛刺。硬件 I2C 则要留意总线上是不是存在地址冲突——0.91 寸 OLED 通常只有一个从机如果挂第二块 I2C 器件确认地址不能一样。实机调试时如果 OLED 不亮先用示波器量 SCL/SDA 有没有波形比反复改代码快得多。3.5 下载前检查BOOT0、SWD 引脚与 DAP 下载失败PCB 打样回来第一次下载时最常卡在识别不到芯片。这里有个容易误会的点BOOT1 引脚在正常 Flash 启动模式下不用管BOOT0 必须被拉低有的人看到下载失败就去拨 BOOT1没意义。如果 ST-LINK 提示 DAP 下载失败按顺序检查SWDIO/SWCLK 是否被复用到其他功能、BOOT0 是否确实是低、目标板供电是否稳定。按住复位键再点下载等进度条出现再松手——这个技巧能解决相当一部分首次连接失败的尴尬。4. HAL 库驱动 OLED 并实时显示多通道数值4.1 I2C 协议中的控制字节命令和数据怎么区分OLED 驱动芯片 SSD1306 的 I2C 传输格式和普通传感器不太一样。每一次写操作主机先发从机地址 0x780x3C 左移一位再发一个控制字节控制字节为 0x00 表示后续是命令0x40 表示后续是显存数据。也就是说命令和数据的区分不在地址位而是靠这个控制字节完成有别于 EEPROM 那种靠寄存器地址区分读写对象的方式。void OLED_WR_Byte(uint8_t dat, uint8_t cmd) { HAL_I2C_Mem_Write(hi2c1, 0x78, cmd, I2C_MEMADD_SIZE_8BIT, dat, 1, 100); }上面这个函数用 HAL 库的 Mem_Write 把控制字节当内存地址用第一次写从机地址第二次写内部地址即控制字节第三次写数据。网上流传最广的江协科技 OLED 驱动源码里OLED_WR_Byte 就是这个模式HAL 库工程直接摘过来即可。若选用软件模拟 I2C就把 HAL_I2C 换成 GPIO 翻转加延时无非是 SCL 拉高拉低之间插入约 5us 的延时把 8 位数据逐位送出去。4.2 SSD1306 显存模型128x32 是怎么被映射成 4 页的0.91 寸 OLED 分辨率是 128x32内部显存按页组织每页 8 个像素高对应 32 像素高正好分成 4 页Page 0 到 Page 3每页横向 128 个字节。这意味着整屏显存只有4 x 128 512字节显示一个字符时不需要操作像素点而是修改对应页内某个字节的位。OLED_ShowString 这类函数内部都维护着一个 OLED_GRAM[4][128] 数组先往数组里写像素再把整个数组刷进屏幕。全屏刷新在 400kHz I2C 下大约 13ms100kHz 下要 50ms 左右对于电压显示足够快。江协科技的模板驱动里就是全量刷屏的思路在 128x32 这种小屏上完全够用不需要像大屏那样做分页 DMA。4.3 主循环里做三件事取数、算电压、刷字符有了 DMA 在后台填 adc_buf主循环就能专心做显示。0.91 寸屏幕一行刚好 16 个 ASCII 字符8x6 字体128/8168 个通道拼成 4 行显示正好一屏放满。char line[24]; uint8_t i; while (1) { for (i 0; i 4; i) { float v_left adc_buf[i * 2] * 3.3f / 4095.0f; float v_right adc_buf[i * 2 1] * 3.3f / 4095.0f; sprintf(line, CH%d:%.2f CH%d:%.2f, i * 2 1, v_left, i * 2 2, v_right); OLED_ShowString(0, i * 8, line); } HAL_Delay(100); }这段代码用 sprintf 把两个通道的电压拼进一行OLED_ShowString 的第一个参数是列坐标第二个是行像素坐标与页地址无关——驱动函数内部会把 y 坐标转成对应页和页内偏移。电压计算公式里的 4095 来自 12 位 ADC 的满量程3.3V 是参考电压。如果供电不是精确的 3.3V把 3.3f 换成实测值或者用精密基准源接一个通道做反推校准这是后面第 5 章要展开的话题。HAL_Delay(100) 把显示刷新率压到 10Hz人眼读数不卡同时给 DMA 留足了采样窗口。不需要为显示做双缓冲0.91 寸的 512 字节显存本身就在驱动层维护主循环只读 adc_buf两者互不干扰。4.4 别忘了初始化顺序上电时序和首屏OLED 驱动初始化要保证芯片上电稳定后再送命令。常规做法是在 OLED_Init 里先延时 100ms然后依次发送关显示0xAE、时钟分频、复用率、显示偏移、起始行、对比度、扫描方向等一串命令最后开显示0xAF。这些命令序列在 SSD1306 datasheet 里有标准模板不需要自己发明摘用现成驱动的初始化数组最稳。如果 OLED 上电后屏幕是黑屏但 I2C 波形正常排查方向基本就两个初始化命令里漏了 0xAF或者地址不匹配。注意 0x91 寸 OLED 也有用 SH1106 芯片的SH1106 的初始化序列和 SSD1306 大体兼容但列地址是 0x00 到 0x7F 带 2 列偏置细节差异会导致边缘显示异常这种问题离线调试时最容易忽略。5. 把能显示做到测得准滤波、局部刷新与常见坑排查5.1 采样稳定性先把数值跳动问题解决ADC 显示值如果在小数点后两位反复跳先别急着改代码。最优先确认几个硬件因素PA0-PA7 引脚有没有悬空引脚用长的杜邦线接信号源相当于天线、模拟地与数字地有没有单点接地、供电是不是开关电源直供。硬件上忍不了的噪声软件上做中值或者均值滤波。uint16_t get_median_value(uint8_t ch) { uint16_t buf[5]; uint8_t i, j; uint16_t tmp; memcpy(buf, (uint16_t *)adc_buf ch, sizeof(buf)); // 注意数组是 uint16_t // 简单冒泡排序取中值 for (i 0; i 4; i) for (j i 1; j 5; j) if (buf[i] buf[j]) { tmp buf[i]; buf[i] buf[j]; buf[j] tmp; } return buf[2]; }中值滤波对尖峰脉冲干扰的抑制效果比均值好代码也容易理解每次取同一通道的连续 5 个样本排序后取中间值。这个滤波函数放在显示循环里调用等于是以牺牲一点响应速度为代价换来了读数的稳定。如果通道数多更高效的方案是把中值滤波挪到 DMA 中断里做滑窗但对显示类应用没必要。5.2 局部刷新别让 OLED 刷新拖慢采样节奏全量刷 512 字节在小屏上本来就不慢但如果采样率和显示刷新率都要提升就要改用局部刷新。SSD1306 支持按页设置列地址范围只重发本页变化的字符区域。显示第 2 行文本时只需更新 Page 1 对应列地址范围的字节I2C 上的数据量大幅减少。void OLED_Refresh_Page(uint8_t page, uint8_t col_start, uint8_t col_end) { uint8_t c; OLED_WR_Byte(0xB0 page, OLED_CMD); // 设置页地址 Page0~3 OLED_WR_Byte(col_start 0x0F, OLED_CMD); // 列低地址 OLED_WR_Byte(0x10 | (col_start 4), OLED_CMD); // 列高地址 for (c col_start; c col_end; c) OLED_WR_Byte(OLED_GRAM[page][c], OLED_DATA); // 只发送这一页的数据 }这个函数替代全量刷新后主循环里的显示代码就从OLED_Refresh()改成按需调用OLED_Refresh_Page(1, 0, 63)。局部刷新的代价是函数调用变复杂要对每个字符区域预先计算占用的列范围。对于 8 位 ASCII 字符一行文本就是 128 列半行刷新也够实际收益主要来自后续想加曲线或进度条时。5.3 五个必查故障点现象大概率原因处理只有第一路有数值其余固定不变DMA 的 Memory Increment 未开启CubeMX 里勾上 Memory Increment重新生成数值跳到合理范围之外ADC 引脚悬空或信号源阻抗过大未接传感器时把引脚接 GND调长采样时间OLED 花屏或乱码I2C 速率过高或供电噪声400kHz 降到 100kHz看波形是否有毛刺刷屏时采样值偶发跳变DMA 写入和 CPU 读数组冲突先拷贝一份数组再滤波或短临界区整屏偏移一列或左右不对称用的是 SH1106 但按 SSD1306 驱动确认屏幕芯片型号改用对应列偏移命令这五个问题覆盖了我接触过的绝大多数 F103OLED 采集工程的返工场景。最后一个问题最容易坑人——同样写着 0.91 寸 OLEDSSD1306 和 SH1106 的列寻址细节不同买到便宜屏之前先确认芯片能省掉一晚上的排错时间。把 I2C 速率降下来、滤波加上、局部刷新做出来这套采集显示系统的稳定性和响应速度基本就到了头剩下的工作就是把参考电压从 3.3V 换成独立基准源把电压计算精度往 1mV 级别推。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价