
把STM32F103C8放进Proteus 8.15里做ADC仿真读回来的值永远是0这个问题我估计十个搞STM32仿真的工程师里至少有五个都撞上过。我第一次遇到时第一反应是芯片模型坏了甚至想直接把整个仿真环境卸了重装。后来冷静下来把代码、原理图、寄存器状态一项项查过去才发现问题根本不在芯片而在几个特别不起眼的配置细节上。这篇文章就按我当时排查的顺序把导致ADC采样为0的3个核心配置完整梳理一遍。每一节都包含代码示例、原理分析和Proteus环境下特有的坑。你如果现在正被这个问题卡住不用急着换芯片按下面的顺序检查代码和原理图大概率能直接解决。1. 先交代清楚现象与排查思路1.1 仿真环境到底长什么样我说的这个场景很典型你可以对号入座一下软件版本Proteus 8.158.15 SP1芯片选STM32F103C8LQFP48封装开发环境Keil MDK 5.x配合标准外设库Standard Peripheral Library 3.5代码逻辑ADC1的通道0也就是PA0引脚软件触发单次转换读回12位数据原理图接线VDD接3.3VVSS接地VDDA接3.3VVSSA接地。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来自PCLK2APB2总线时钟经过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得到9MHzDiv6得到6MHz都没问题。总之先算出PCLK2的实际频率再让ADCCLK落在14MHz以下。这是很多人忽略的第一步。ADC寄存器访问不依赖ADCCLK但ADC的采样和转换过程完全依赖它。没有ADCCLK转换过程根本不会进行Proteus里ADC数据寄存器就一直保持复位值0。2.2 Proteus里最容易踩的时钟坑再补充一个Proteus环境里特别容易踩的坑有些开发板用的是25MHz外部晶振工程代码里把HSE配成了25MHzPLL也按25MHz来倍频。代码拿到Proteus里仿真时原理图里如果只放了一个芯片、没有放置晶振元件或者放置了但频率对不上Proteus模型对HSE的支持就可能按内部RC来跑整个时钟树全乱套。时钟树一乱APB2频率就不对ADCCLK自然不对劲ADC读数为0完全不奇怪。我在做其他仿真时也遇到过类似情况比如电机控制里的SVPWM仿真当时换了好几个配置都没用最后把时钟源统一改回内部时钟或者按Proteus模型能正确支持的晶振频率重新计算PLL问题才解决。所以在Proteus里做ADC仿真我的经验是优先使用内部时钟HSI或者明确把外部晶振配置成Proteus能识别的频率。ADC对时钟稳定性的要求比对峰值频率的要求更敏感搞一个稳定的ADCCLK后面能省很多事。这里也要提一句ADC预分频器可选的配置有Div2、Div4、Div6、Div8PCLK2为72MHz时的情况如下表预分频器配置ADCCLK结果是否满足≤14MHzRCC_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.3VADC内部的模拟电路就没有电源和参考电压采样结果一定不正常。我有一个特别简单的验证方法在Proteus原理图上放一个DC Voltmeter表笔并联在PA0和GND之间。然后转动电位器看电压表读数有没有变化。如果电压有变化说明外部信号已经送到了引脚问题在代码侧。如果电压表恒定为0说明原理图连接就有问题先查连线别去动代码。3.3 引脚复用与通道映射STM32F103C8的ADC通道映射需要记住尤其是你换芯片型号的时候引脚ADC通道PA0ADC12_IN0PA1ADC12_IN1PA2ADC12_IN2PA3ADC12_IN3PA4ADC12_IN4PA5ADC12_IN5PA6ADC12_IN6PA7ADC12_IN7PB0ADC12_IN8PB1ADC12_IN9如果你用的是PB0那代码里要选择ADC_Channel_8而不是ADC_Channel_0。这类映射关系在Proteus仿真里同样不会放宽配置错了就是0。4. 配置三ADC初始化与转换流程必须完整4.1 初始化结构体字段逐个过一遍第三个配置点是整个ADC模块的初始化。标准外设库里用ADC_InitTypeDef这个结构体来配置ADC的工作模式。很多人从网上直接复制一个ADC_Init却不知道每个字段的含义结果漏了关键字段或者字段值互相矛盾。ADC_InitTypeDef的核心字段有这些ADC_ModeADC模式单ADC场景用ADC_Mode_IndependentADC_ScanConvMode是否开启扫描模式只扫一个通道时通常DISABLEADC_ContinuousConvMode单次还是连续转换。单次采样用DISABLE连续采样用ENABLEADC_ExternalTrigConv外部触发方式软件触发用ADC_ExternalTrigConv_NoneADC_DataAlign数据对齐12位ADC结果一般右对齐即ADC_DataAlign_RightADC_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仿真里特别容易翻车的点。正确流程是这样的调用ADC_RegularChannelConfig配置规则通道调用ADC_Cmd(ADC1, ENABLE)使能ADC做校准。标准库提供了ADC_ResetCalibration和ADC_StartCalibration两个都要执行启动转换ADC_SoftwareStartConvCmd(ADC1, ENABLE)等待EOC标志位置位读取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 ADC0问题整理成了一张速查表按出现频率排序现象大概率原因处理方式读数始终为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_0PA0。这个例子说明排查时一定要先确认代码里配置的通道和原理图实际连接的引脚是同一个。任何一边出错读到的都是别的东西。还有一次是电源问题。一个朋友在原理图里忘了给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这个基本功能模型是完整的。绝大多数ADC0的问题归根结底是三个配置点里至少有一个没做好。按上面的顺序排查一遍基本能解决九成以上的问题。第二Proteus仿真对代码规范性的要求有时候比实物板还要高。实物板上漏了校准可能照样出数据漏了引脚AIN配置可能因为内部结构也能凑合读到值。但Proteus模型会严格校验初始化和寄存器状态反倒逼着你把外设初始化写得干干净净。从这个角度看被Proteus折磨一次也不全是坏事。第三如果你计划用多个ADC通道或者做多通道扫描务必搞清规则组序列的配置逻辑。ADC_RegularChannelConfig每次调用只配置一个通道但通道在序列中的位置由第二个参数决定。很多人只改通道号忘了改排列序号结果采到的还是旧通道。Proteus对这种细节同样不会放过。最后分享一个小技巧。我在仿真阶段习惯把ADC采样结果映射到PWM占空比上驱动一个小灯泡或者LED渐变亮灭。这样做的好处是你不用盯着数字一眼就能看出ADC有没有在工作。当你看到LED亮度随手电位器平滑变化时说明整条链路基本通了。这个土办法比任何调试手段都直观。