☰
Proteus仿真STM32 ADC采样为0?三个配置细节一次解决
2026/10/5 6:11:49 网站建设 项目流程

把STM32F103C8放进Proteus 8.15里做ADC仿真,读回来的值永远是0,这个问题我估计十个搞STM32仿真的工程师里至少有五个都撞上过。我第一次遇到时,第一反应是芯片模型坏了,甚至想直接把整个仿真环境卸了重装。后来冷静下来,把代码、原理图、寄存器状态一项项查过去,才发现问题根本不在芯片,而在几个特别不起眼的配置细节上。

这篇文章就按我当时排查的顺序,把导致ADC采样为0的3个核心配置完整梳理一遍。每一节都包含代码示例、原理分析和Proteus环境下特有的坑。你如果现在正被这个问题卡住,不用急着换芯片,按下面的顺序检查代码和原理图,大概率能直接解决。

1. 先交代清楚:现象与排查思路

1.1 仿真环境到底长什么样

我说的这个场景很典型,你可以对号入座一下:

  • 软件版本:Proteus 8.15(8.15 SP1),芯片选STM32F103C8,LQFP48封装
  • 开发环境:Keil MDK 5.x,配合标准外设库(Standard Peripheral Library 3.5)
  • 代码逻辑:ADC1的通道0,也就是PA0引脚,软件触发单次转换,读回12位数据
  • 原理图接线:VDD接3.3V,VSS接地,VDDA接3.3V,VSSA接地。PA0接一个电位器的滑动端,电位器两端分别接3.3V和GND
  • 验证方式:在Keil里下载固件,通过Proteus的Virtual Terminal打印ADC值,或者直接把变量加到Proteus调试窗口里观察

正常情况下,转动电位器改变PA0的电压,ADC读数应该在0到4095之间变化。但现象是:不管你把电位器拧到哪,打印出来始终是0,连一点跳动都没有。

这种情况最容易让人误判成“Proteus的STM32F103C8模型有缺陷”或者“芯片型号不支持ADC”。实际上,Proteus对STM32F103C8的ADC外设模拟虽然不算完美,但基本功能是完整的。绝大多数情况下,问题出在代码配置或者原理图的细节上。

1.2 排查顺序比怀疑芯片更重要

我做排查时,给自己定了一条原则:沿着ADC工作的完整链路,从前往后走一遍。

一颗ADC要拿到正确的转换值,至少要满足三个前提。

第一个前提,ADC外设本身要有时钟。时钟都没打开,ADC模块就是死的,读出来只能是0。

第二个前提,采样引脚被正确配置成模拟输入,而且原理图上确实有模拟电压送到这个引脚。如果引脚配置错了,或者原理图根本没接通,读到的自然是无效值。

第三个前提,ADC的初始化结构体、规则通道配置、校准、启动转换、等待标志位,这一整套流程必须完整。缺一步,转换结果就可能不对。

这三个前提,正好对应我标题里说的“3个配置”。所以下文分三节来写,你排查的时候也按这个顺序来,既省时间,又不会漏点。

2. 配置一:ADC时钟别偷懒

2.1 ADC时钟源链路

在STM32F103里面,ADC1、ADC2挂在APB2总线上,所以ADC外设的模块时钟来自RCC的APB2外设时钟使能。很多人初始化时只写了这一句:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

写完这句就觉得ADC时钟来了。实际上,这句话只是把ADC1模块的寄存器时钟打开了,但ADC模块内部用于采样的时钟,也就是ADCCLK,还需要另外配置。ADCCLK来自PCLK2(APB2总线时钟)经过ADC预分频器分频后的输出,它的最大值不能超过14MHz。

PCLK2在大概率上是系统主频72MHz或者36MHz。如果你没有写RCC_ADCCLKConfig,那么ADCCLK会保持复位默认值,在Proteus的模型里,这个默认状态可能导致ADC无法正常工作,或者转换结果不可靠。

正确的做法是这样:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 72MHz / 6 = 12MHz

如果系统主频是72MHz,用Div6得到12MHz,符合规范。如果PCLK2只有36MHz,那么Div4得到9MHz,Div6得到6MHz,都没问题。总之先算出PCLK2的实际频率,再让ADCCLK落在14MHz以下。

这是很多人忽略的第一步。ADC寄存器访问不依赖ADCCLK,但ADC的采样和转换过程完全依赖它。没有ADCCLK,转换过程根本不会进行,Proteus里ADC数据寄存器就一直保持复位值0。

2.2 Proteus里最容易踩的时钟坑

再补充一个Proteus环境里特别容易踩的坑:有些开发板用的是25MHz外部晶振,工程代码里把HSE配成了25MHz,PLL也按25MHz来倍频。代码拿到Proteus里仿真时,原理图里如果只放了一个芯片、没有放置晶振元件,或者放置了但频率对不上,Proteus模型对HSE的支持就可能按内部RC来跑,整个时钟树全乱套。

时钟树一乱,APB2频率就不对,ADCCLK自然不对劲,ADC读数为0完全不奇怪。

我在做其他仿真时也遇到过类似情况,比如电机控制里的SVPWM仿真,当时换了好几个配置都没用,最后把时钟源统一改回内部时钟,或者按Proteus模型能正确支持的晶振频率重新计算PLL,问题才解决。

所以在Proteus里做ADC仿真,我的经验是:优先使用内部时钟(HSI),或者明确把外部晶振配置成Proteus能识别的频率。ADC对时钟稳定性的要求比对峰值频率的要求更敏感,搞一个稳定的ADCCLK,后面能省很多事。

这里也要提一句,ADC预分频器可选的配置有Div2、Div4、Div6、Div8,PCLK2为72MHz时的情况如下表:

预分频器配置ADCCLK结果是否满足≤14MHz
RCC_PCLK2_Div236MHz否
RCC_PCLK2_Div418MHz否
RCC_PCLK2_Div612MHz是
RCC_PCLK2_Div89MHz是

如果你拿不准当前PCLK2到底是多少,可以在调试窗口里看RCC->CFGR寄存器的值,或者用SystemCoreClock变量打印出来,然后再决定用几分频。

3. 配置二:GPIO引脚必须真·模拟输入

3.1 为什么引脚模式这么关键

第二个高频出问题的点,是GPIO引脚的模式配置。

STM32F103的GPIO端口针对不同外设功能,必须在初始化时配置成对应的模式。ADC输入引脚需要的是模拟输入模式,也就是GPIO_Mode_AIN。

有相当多的人会把ADC引脚配成GPIO_Mode_IPU(上拉输入),甚至GPIO_Mode_Out_PP(推挽输出)。这两种模式都会让ADC读不到真实电压。

上拉输入模式下,引脚内部会有一个上拉电阻把引脚电平拉高,外部电压会被影响,采样结果偏向满量程或者干脆不稳定。推挽输出模式更夸张,引脚被内部驱动器强制输出高低电平,外部模拟电压根本进不来,在Proteus模型里引脚走的是数字逻辑路径,ADC通道就拿不到有效的模拟电平,结果就是读数一直是0。

正确的GPIO初始化代码很简单:

GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure);

注意,GPIO_Mode_AIN模式下,引脚的输入输出数据寄存器都不会参与工作,施密特触发器也会被切断,模拟电压直接经过引脚内部的模拟开关送到ADC采样电容。这是ADC输入引脚的唯一正确配置方式。

还有一个容易忽略的坑:有些人习惯用编程器配置工具自动生成代码,结果PA0被同时初始化成了多个外设功能,或者跟其他数字引脚混在一起配置。代码看起来只是多了一行GPIO_Mode_Out_PP,但就这一行,就足以把整个ADC通道废掉。

3.2 原理图连接是另一层坑

代码层面的GPIO模式对了,还要看原理图上有没有真的把模拟电压送到这个引脚。

在Proteus原理图里,STM32F103C8是LQFP48封装,PA0引脚在芯片符号上可能显示为PA0,也可能显示为PA0-WKUP/USART2_CTS/ADC12_IN0这种复用名。反正都是同一个引脚,你只要确认自己接的是PA0,代码里读的通道是ADC_Channel_0,两者对应,就不会错。

但如果你把电位器接到了PA1,然后在代码里读ADC_Channel_0,那读回来的就是PA0的电压。PA0如果悬空,Proteus模型给它一个什么电压值就很随机,有时候是0,有时候是乱码。这个错误我在帮别人排查时遇到过好多次。

另外,Proteus里部分电源引脚是默认隐藏的。VDDA、VSSA这些模拟电源引脚,在芯片符号上可能是可见的,也可能是隐藏的,但你必须保证它们处于正确的连接状态。如果VDDA没接3.3V,ADC内部的模拟电路就没有电源和参考电压,采样结果一定不正常。

我有一个特别简单的验证方法:在Proteus原理图上放一个DC Voltmeter,表笔并联在PA0和GND之间。然后转动电位器,看电压表读数有没有变化。

如果电压有变化,说明外部信号已经送到了引脚,问题在代码侧。如果电压表恒定为0,说明原理图连接就有问题,先查连线,别去动代码。

3.3 引脚复用与通道映射

STM32F103C8的ADC通道映射需要记住,尤其是你换芯片型号的时候:

引脚ADC通道
PA0ADC12_IN0
PA1ADC12_IN1
PA2ADC12_IN2
PA3ADC12_IN3
PA4ADC12_IN4
PA5ADC12_IN5
PA6ADC12_IN6
PA7ADC12_IN7
PB0ADC12_IN8
PB1ADC12_IN9

如果你用的是PB0,那代码里要选择ADC_Channel_8,而不是ADC_Channel_0。这类映射关系在Proteus仿真里同样不会放宽,配置错了就是0。

4. 配置三:ADC初始化与转换流程必须完整

4.1 初始化结构体字段逐个过一遍

第三个配置点,是整个ADC模块的初始化。

标准外设库里用ADC_InitTypeDef这个结构体来配置ADC的工作模式。很多人从网上直接复制一个ADC_Init,却不知道每个字段的含义,结果漏了关键字段,或者字段值互相矛盾。

ADC_InitTypeDef的核心字段有这些:

  • ADC_Mode:ADC模式,单ADC场景用ADC_Mode_Independent
  • ADC_ScanConvMode:是否开启扫描模式,只扫一个通道时通常DISABLE
  • ADC_ContinuousConvMode:单次还是连续转换。单次采样用DISABLE,连续采样用ENABLE
  • ADC_ExternalTrigConv:外部触发方式,软件触发用ADC_ExternalTrigConv_None
  • ADC_DataAlign:数据对齐,12位ADC结果一般右对齐,即ADC_DataAlign_Right
  • ADC_NbrOfChannel:规则组的转换通道数量,只采一个通道就是1

这些字段里面,最容易出错的是ADC_NbrOfChannel。如果你只采一个通道,但写成了4,配置本身不会报错,转换逻辑却会按4个通道的序列去跑,读出来的值就完全不对。Proteus模型对这些细节模拟得很严格,这个问题在Proteus里特别容易暴露。

典型初始化代码:

ADC_InitTypeDef ADC_InitStructure; ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure);

还要注意,ADC_RegularChannelConfig是一个必须调用的函数。它负责把某个通道配置到规则转换序列的某个位置上。如果你初始化了ADC,却没有调用ADC_RegularChannelConfig,那规则通道序列就是空的,启动转换后自然没有结果。

ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);

这句代码的意思是:ADC1的规则序列第1个位置,放ADC_Channel_0这个通道,采样时间为55.5个ADC时钟周期。

4.2 启动顺序:使能、校准、转换

ADC的启动顺序,是另一个在Proteus仿真里特别容易翻车的点。

正确流程是这样的:

  1. 调用ADC_RegularChannelConfig配置规则通道
  2. 调用ADC_Cmd(ADC1, ENABLE)使能ADC
  3. 做校准。标准库提供了ADC_ResetCalibration和ADC_StartCalibration,两个都要执行
  4. 启动转换,ADC_SoftwareStartConvCmd(ADC1, ENABLE)
  5. 等待EOC标志位置位
  6. 读取ADC_GetConversionValue

很多人为了省事,跳过校准步骤。在真实芯片上跳过校准,有时候还能读到数据,但在Proteus模型里,校准步骤会被严格校验。你不校准,它就不给你正确的转换结果,甚至读出来的就是0。

校准部分的代码:

ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1));

校准完成后,再启动转换:

ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); uint16_t value = ADC_GetConversionValue(ADC1);

还有一个细节,启动转换前最好清一下EOC标志,或者确保上一次转换完全结束。在连续模式下,如果不注意,很容易读到旧值。Proteus仿真的时序和真实芯片有差异,这类“陈旧数据”问题在仿真里更容易出现,所以我习惯每次都重新调用ADC_SoftwareStartConvCmd,确保每次读取都是一次新的转换。

完整的读函数我一般写成这样:

uint16_t read_adc_value(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); return ADC_GetConversionValue(ADC1); }

注意,如果这个函数在调用前ADC没有被使能、没有校准过,它还是会返回0。所以初始化部分的顺序一定不能乱。

4.3 采样时间对结果的影响

ADC_RegularChannelConfig里最后一个参数是采样时间,可选值有1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5个ADC时钟周期。

这里有一个原则:信号源内阻越高,需要的采样时间越长。在Proteus里,如果信号源是电位器分压,电位器阻值比较大,采样时间又短,ADC内部的采样电容还没充满就开始转换,读出来的值就可能偏低或者波动。

Proteus仿真里的时钟精度和真实芯片毕竟有差异,所以我的习惯是采样时间选大一点,比如55.5甚至239.5个周期。仿真场景下,转换速度慢一点无所谓,数据的稳定性更重要。

5. 常见问题与实战排查实录

5.1 高频问题速查表

我把自己遇到过的、以及帮别人排查时看到的Proteus ADC=0问题整理成了一张速查表,按出现频率排序:

现象大概率原因处理方式
读数始终为0,电压表显示引脚有电压GPIO没配成AIN模式,或配成了输出/数字输入改成GPIO_Mode_AIN
读数始终为0,电压表也显示不了电压原理图上PA0没连到分压电路,或电源没接检查原理图连线,检查VDDA/VREF
读数固定为4095或非常高引脚内部上拉,或者电位器两端接反改用AIN模式,检查电位器接线
读数有变化但明显不稳定采样时间太短,或ADCCLK配置异常调长采样时间,检查预分频器
程序卡死在等待EOC的while循环ADC时钟没开,或ADC_Cmd没执行检查RCC时钟使能,确保调用了ADC_Cmd(ADC1, ENABLE)
换了芯片型号后读数从0变成乱码引脚映射不同,PA0不一定对应ADC通道0查对应型号的ADC通道映射表

这张表基本上覆盖了九成以上的情况。如果不在表里,也不要轻易怀疑Proteus有Bug,先从电源和原理图连接查起。

5.2 一次真实排查记录

有一次,一个做毕设的朋友让我帮忙联调一个工程。他的代码里ADC初始化、GPIO初始化、时钟配置看起来全跟我写的一样,但Proteus仿真里读数就是0。我远程看了半个小时也没看出问题,最后让他把原理图截图发过来,才发现他把电位器的中间引脚接到了PA1,代码里读的却是ADC_Channel_0(PA0)。

这个例子说明,排查时一定要先确认代码里配置的通道和原理图实际连接的引脚是同一个。任何一边出错,读到的都是别的东西。

还有一次是电源问题。一个朋友在原理图里忘了给VDDA接3.3V。Proteus默认不会主动把VDDA和VDD连在一起,如果你的芯片符号把VDDA单独引出来了,就需要手动接3.3V。结果整个模拟模块没有参考电压,ADC读数一定是0。

这种问题在实物板上反而不常见,因为实物板设计时通常会把VDDA和VDD用磁珠或0欧电阻连起来,但Proteus原理图用的是分散引脚,特别容易漏接。

5.3 用Proteus内置工具验证

排查时不要只盯着代码,Proteus本身提供的几个调试工具很好用。

第一个是DC Voltmeter,也就是直流电压表。放在被测引脚上直接读电压,这是验证外部信号有没有送到芯片的最快方法。

第二个是调试监视窗口。在Proteus的Debug菜单里选中微控制器芯片,可以查看内部寄存器的值。你重点看ADC_SR、ADC_DR这几个寄存器。如果ADC_DR一直为0,而ADC_SR里的EOC标志也没置位,说明转换流程没走通;如果EOC已经置位但DR是0,问题多半在通道配置或者校准上。这个信息量比只看电压表大得多。

第三个是Virtual Terminal,也就是虚拟串口终端。用UART打印转换结果。不过我一般先用变量监视的方式,因为它比串口更直观,不受波特率和UART外设本身配置的影响。等你确定ADC值正常了,再把UART加上,也不会引入额外变量。

5.3 一个更系统的排查顺序

如果你现在手里就是这个烂摊子,我给你一个可以直接照着做的顺序:

第一步,在代码里加一个全局变量adc_value,在ADC读值函数返回后赋值给它。然后在Proteus调试菜单里添加这个变量的监视。这样能直观看到ADC的原始读数。

第二步,用DC Voltmeter测PA0的电压。转动电位器,看电压是否在0到3.3V之间变化。没有变化,就去查原理图。

第三步,在调试窗口里看RCC->CFGR寄存器,确认PCLK2的值。再确认ADC预分频器寄存器 ADC->CR2 和 ADC->SQR1、ADC->SQR3 的配置是否符合预期。

第四步,检查ADC_SR寄存器。如果EOC置位,但DR是0,重点查校准步骤有没有执行,采样时间是不是太短。如果EOC一直不置位,重点查ADC有没有使能、规则通道有没有配置。

这套顺序走下来,基本能把问题定位到某一个具体环节。我在帮别人排查时,百分之九十的情况都能在这一套流程内解决。

6. 关于Proteus ADC仿真的几个心里话

写到这里,主体内容算是讲完了,还有一些经验性的东西想分享。

第一,不要一开始就把问题归因于Proteus的模型缺陷。STM32F103C8在Proteus里的外设模型虽然做不到100%和真芯片一致,但ADC这个基本功能,模型是完整的。绝大多数ADC=0的问题,归根结底是三个配置点里至少有一个没做好。按上面的顺序排查一遍,基本能解决九成以上的问题。

第二,Proteus仿真对代码规范性的要求,有时候比实物板还要高。实物板上漏了校准,可能照样出数据;漏了引脚AIN配置,可能因为内部结构也能凑合读到值。但Proteus模型会严格校验初始化和寄存器状态,反倒逼着你把外设初始化写得干干净净。从这个角度看,被Proteus折磨一次,也不全是坏事。

第三,如果你计划用多个ADC通道,或者做多通道扫描,务必搞清规则组序列的配置逻辑。ADC_RegularChannelConfig每次调用只配置一个通道,但通道在序列中的位置由第二个参数决定。很多人只改通道号,忘了改排列序号,结果采到的还是旧通道。Proteus对这种细节同样不会放过。

最后分享一个小技巧。我在仿真阶段,习惯把ADC采样结果映射到PWM占空比上,驱动一个小灯泡或者LED渐变亮灭。这样做的好处是,你不用盯着数字,一眼就能看出ADC有没有在工作。当你看到LED亮度随手电位器平滑变化时,说明整条链路基本通了。这个土办法,比任何调试手段都直观。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询