ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Proteus仿真STM32 ADC采样全为0?从时钟、引脚、EOC三大配置彻底排查

Proteus仿真STM32 ADC采样全为0?从时钟、引脚、EOC三大配置彻底排查 说实话我第一次在Proteus 8.15里跑STM32F103C8的ADC例程时也被采样值永远为0这个现象折磨了半天。虚拟串口打印出来的电压值纹丝不动始终是0.00V当时我第一反应也是是不是芯片模型坏了甚至想把库里所有STM32型号都换一遍。冷静下来之后才意识到Proteus里的STM32模型对寄存器配置非常较真ADC采样为0绝大多数不是芯片问题而是三个配置环节出了问题——ADC时钟、模拟输入引脚模式、软件触发与EOC等待。这篇文章我就把自己排查的过程完整捋一遍顺手附上一套可以直接运行的例程和最终的排错清单。如果你也遇到过类似现象建议按这个顺序检查大概率能省下不少折腾时间。1. 我在Proteus里复现ADC全为0的现场1.1 搭建最小测试环境先把环境说清楚方便你对号入座。我使用的是Proteus 8.15 Professional芯片选择的是库里的STM32F103C8配套8MHz晶振、两个20pF负载电容、10k上拉复位电路电源部分把VDDA和VREF都接到了3.3VVSSA接GNDBOOT0下拉到地。信号源这边我用了一个POT-HG电位器两端分别接到3.3V和GND中间抽头接到PA0——也就是ADC1的通道0。另外接了一个Virtual Terminal虚拟串口方便在仿真里看打印结果串口映射用的是USART1的PA9和PA10。我最初测试的代码逻辑很简单上电后初始化ADC1、配置PA0为模拟输入、执行一次软件触发转换然后把ADC_DR寄存器里的原始值换算成电压值通过串口打印出来。结果运行起来之后虚拟终端上每一行都是0.000V。这种全0结果特别迷惑人因为它既不是乱码也不是随机跳动而是稳定地显示0。当时我试过换芯片型号、删除重建原理图、把串口改成LCD显示现象都一模一样。后来我才明白问题不在前端显示而在ADC外设本身根本没有正确工作。1.2 现象复现与初步判断在继续排查之前我先做了一件非常重要的事在代码里加了一个LED闪烁逻辑确认程序确实在正常执行。如果连LED都不闪那问题远不止ADC一处得先查电源、晶振、复位以及Proteus是否真的加载了正确固件。LED正常闪烁之后我把怀疑范围缩小到了ADC外设配置。此时我用了Proteus的调试功能在代码里添加了变量来观察ADC1-DR寄存器的值并逐步确认RCC的APB2外设时钟是否使能了ADC1和GPIOAADC1-CR2的ADON位是否置1ADC1-SR的EOC标志位是否置1最终ADC1-DR的值是否为0。排查结果显示问题出在配置顺序和细节上。以下三个配置是最常见的全0元凶我逐个拆开讲。2. 先查时钟ADC没拿到12MHz后面全是徒劳2.1 ADC时钟从哪里来为什么必须控制在14MHz以内STM32F103系列里ADC外设挂在APB2总线上它的时钟源是PCLK2再经过ADC预分频器分频后生成ADCCLK。也就是说ADCCLK PCLK2 / 分频系数分频系数可选2、4、6、8。这里有一个硬性约束ADC的时钟频率不能超过14MHz。STM32F103的ADC是逐次逼近型结构内部采样保持和比较逻辑的时序都基于ADCCLK频率一旦超限转换结果可能完全不可靠。Proteus的STM32模型对这一点模拟得相当严格配置超限时转换过程干脆不启动DR寄存器保持复位值0表现出来就是ADC采样全为0。2.2 正确分频的代码写法与验证方法默认情况下STM32F103如果使用外部8MHz晶振经过PLL倍频后SYSCLK是72MHzAPB2预分频配置为1分频所以PCLK2也是72MHz。要让ADCCLK不超过14MHz至少要用6分频72/612MHz正好满足要求。这也是标准外设库例程里普遍写ADC_PCLK2_Div6的原因。我建议在ADC初始化前先明确配置ADC时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6);注意RCC_ADCCLKConfig的作用仅仅是设置ADC预分频器并不负责PCLK2本身的分频。如果你偷懒没有调用RCC_PCLK2Config或者工程里的SystemInit压根没有把时钟配到72MHz那你实际得到的PCLK2可能是别的值。比如改用内部HSI 8MHz时钟时PCLK2默认也就是8MHz左右这时用6分频得到约1.33MHz虽然工作正常但采样速度很慢更怕的是有人用了Div2结果ADCCLK接近4MHz也未必会全0但配合其他问题就容易出幺蛾子。所以我的建议是在初始化之前调用一次RCC_GetClocksFreq把当前PCLK2的值打印出来心里有数。RCC_ClocksTypeDef RCC_Clocks; RCC_GetClocksFreq(RCC_Clocks); // 用串口打印 RCC_Clocks.PCLK2_Frequency // 若打印 72000000ADC分频用 Div6 最合适还有一个细节不能漏Proteus里晶振组件的频率要和MCU属性里的时钟设置一致。如果晶振组件是4MHz而代码按8MHz外部晶振初始化SystemInit很可能卡在等待HSE起振的循环里程序根本跑不到ADC初始化。这个坑我放到后面第5部分集中说因为它经常和全0现象同时出现。3. 再看引脚模拟输入模式和通道编号对不上就白读3.1 引脚复用到AIN模式的重要性很多新手容易忽略GPIO这一层以为ADC通道配置好之后引脚就会自动变成模拟输入。实际不是这样。STM32的引脚是有复用功能的。PA0这个引脚既可以做普通IO也可以复用成TIM2_CH1、WKUP还可以作为ADC1_IN0。它到底以什么身份工作取决于GPIO控制寄存器CRL里的模式位。如果想让ADC正确采集到外部模拟电压必须把该引脚配置成模拟输入模式也就是GPIO_Mode_AIN。如果你忘了配置GPIO或者只配置了通用输入浮空模式在真实芯片上ADC采样通常还能读出个不稳定值但在Proteus的仿真模型里模拟电压根本不会正确进入采样网络结果就是DR寄存器的值一直停在0。这一步对仿真尤其严格简直像考试里的必答项。正确代码如下GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure);别忘了使能GPIOA的时钟。它和ADC1的时钟都挂在APB2上可以一起打开RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);3.2 原理图端最容易忽略的接线细节代码配置没问题但原理图接错线照样全0。这里列几个我处理过的高频踩坑点通道和引脚对应关系记错。STM32F103C8的ADC1有10个外部通道通道编号和引脚的对应关系是固定的。PA0对应通道0PA1对应通道1以此类推推到PA7对应通道7PB0对应通道8PB1对应通道9。代码里ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ...)显然配的是PA0如果信号源接在PA1上读出来的只会是另一个悬空通道的值通常就是0。信号源没有形成完整回路。用DVSRC直流电压源给PA0提供2.5V时必须把电源的负极和STM32的GND接到同一个地网络。很多人只看到输出电压有2.5V但实际上信号源的参考地和芯片GND没有连通等效于PA0悬空读值当然不对。Proteus里的GND端子不止一个放的时候要确保它们都在同一条电源网络里。VREF引脚悬空或接错。这个在STM32F103C8仿真模型里非常常见。芯片的VREF决定了ADC转换的满量程电压必须接3.3V。如果VREF悬空或者接到GNDADC内部的参考电压就不正常结果是无论如何都读不出有效值。完整的电源接法应该是VDDA接3.3VVREF接3.3VVSSA接GND同时VDD和VSS也要接好。电位器接线不对。用POT-HG电位器时1脚和3脚分别接3.3V和GND中间抽头接PA0。如果只把两端接了3.3V中间抽头通过一个电阻接地那抽头电压就被固定在上拉状态转动旋钮也不会有变化。更极端的情况是电位器两端都没接中间抽头悬空读值会是随机或0。我在排查时就发现自己犯的恰恰是第2个坑信号源的正端接到了PA0但信号源的负端没有和GND连通导致PA0的电压始终悬浮。把地线补上之后ADC值立刻随电压变化了。仿真里面接地这种理所当然的步骤反而最容易被忽略。4. 最后看时序软件触发后不等EOC拿到的就是04.1 ADC转换不是瞬间完成的第三个配置问题出在时序上。ADC从触发到转换完成需要时间不是写一条指令就能立刻读出结果的。STM32F103的逐次逼近型ADC完成一次规则通道转换需要若干个ADCCLK周期具体数量取决于采样时间和转换位数。例如采样时间选择239.5周期加上12位转换本身的12.5个周期总共约252个ADCCLK。当ADCCLK是12MHz时一次转换大约耗时21微秒。在真实芯片上如果你触发转换后立刻去读DR寄存器读到的可能是上一轮的残留值如果上一轮根本没完成DR就是复位值0。Proteus模型对时序的模拟更保守第一次转换尚未完成时DR寄存器常常保持0于是很多人就误以为芯片坏了。4.2 等待EOC的完整初始化流程正确的做法是软件触发转换之后必须等待ADC状态寄存器SR里的EOC标志位置1再读取DR。完整流程应该是// 1. 使能ADC1时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 2. 配置PA0为模拟输入 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 配置ADC1 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); // 4. 配置规则组通道0采样时间选最长档 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); // 5. 使能ADC并校准 ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); // 6. 软件触发等待EOC再读取DR ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) RESET); uint16_t adc_value ADC_GetConversionValue(ADC1);这段代码的顺序不能乱。尤其要注意校准步骤虽然跳过校准不一定导致全0但仿真模型会保留这套流程如果校准没有完成就触发转换结果很容易不正常。另外ADC_ContinuousConvMode如果改成ENABLE也就是连续转换模式只需要触发一次就会不停转换。这时你依然要等第一次EOC置位才能读DR。很多人连续转换模式下直接循环读DR读出来的第一帧数据就是0然后把后面每次触发逻辑全打乱排查起来很费劲。4.3 单步调试时容易被卡住的实操坑这里分享一个我踩过的坑在Proteus里单步调试代码时如果一步步执行到ADC_SoftwareStartConvCmd再按单步执行进入while等待EOC的循环程序会一直卡在里面看起来像是死锁。原因很简单单步执行时仿真时间几乎不推进而ADC转换需要几十微秒的仿真时间。你在等待循环里单步执行EOC永远不会置位。解决办法有两个直接把断点打在while等待EOC之后的那一行然后用全速运行Run到断点让仿真时间推进足够长或者干脆在读取ADC之前加一个短延时比如1毫秒再用while (!EOC);做保护。从工程习惯上讲我还是更推荐等待EOC这种方式因为它最规范不会因为改了系统时钟就失效。但调试时了解这个时序特性能省很多时间。5. 差点误导我的另一个大坑Proteus加载了旧HEX5.1 程序文件路径不一致的典型症状这是最隐蔽、最容易让人把配置查一遍又一遍的问题。Proteus里的STM32组件不是一个自动运行固件的黑盒子它需要你显式指定目标HEX文件。如果你在Keil里改了代码编译生成了新的demo.hex但Proteus组件属性里的Program File还指向桌面上的old.hex那么仿真运行的仍然是旧代码。症状非常明显无论你怎么改ADC配置、GPIO模式、等待逻辑仿真结果完全不变还是老样子全0。因为程序文件压根没变你改的东西根本没被加载。我在排查到最后一步时才发现Keil编译输出目录默认是工程目录下的Objects文件夹而Proteus里的Program File还指向我几天前复制到桌面的hex。重新选择正确的hex路径之后ADC值立刻正常了。5.2 先用LED和串口证明代码真的在跑避免被这个问题坑最有效的办法是在确认ADC问题之前先给工程加一个活证据。比如在main函数开头把某个GPIO翻转起来接一个LED让它在仿真里闪烁或者直接在ADC初始化之前通过串口打印一个固定的标识字符串。如果仿真运行时LED正常闪、标识正常打印说明当前加载的代码确实是最新的如果这些都没有先检查HEX路径再说。具体操作是双击原理图中的STM32F103C8打开Edit Component对话框在Program File一栏重新Browse你的Keil输出HEX。顺便检查Advanced Properties里的Clock Frequency确保它与代码初始化时的外部晶振频率一致比如都填8MHz。这里顺带提一句如果晶振频率设置不一致SystemInit在等待HSE就绪时可能超时卡死程序跑不起来ADC自然全0。这个现象和HEX路径错乱叠加在一起非常容易让人误判为ADC配置有问题。我当时用了一个简单的排查顺序先看LED闪烁、再看串口版本号、最后才去翻ADC寄存器。事实证明这个顺序让我少走了很多弯路。仿真里程序根本没跑起来和ADC配置错了完全是两码事先把前者排除后者才有意义。6. 一套能直接抄的ADC读取例程与Proteus侧设置6.1 代码清单与关键注释这里给出一段完整的轮询方式读取ADC1通道0的例程方便你直接复现。代码基于标准外设库HAL库的思路也完全一样重点在于前面说的三步时钟分频正确、GPIO模拟输入、触发后等待EOC。#include stm32f10x.h #include stm32f10x_gpio.h #include stm32f10x_rcc.h #include stm32f10x_adc.h #include stm32f10x_usart.h // 简单的串口初始化PA9TX void USART1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Tx | USART_Mode_Rx; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE); } void USART1_SendByte(uint8_t byte) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, byte); } void USART1_SendString(char* str) { while (*str) USART1_SendByte((uint8_t)*str); } uint16_t ADC1_ReadChannel0(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) RESET); return ADC_GetConversionValue(ADC1); } int main(void) { uint16_t adc_value 0; char buffer[32]; USART1_Init(); USART1_SendString(STM32F103C8 ADC Test Start\r\n); // 使能GPIOA和ADC1时钟ADC时钟 PCLK2 / 6 72MHz / 6 12MHz RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); // PA0模拟输入 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // ADC1配置单通道、单次转换、软件触发 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); // 规则通道0采样时间选最长档降低外部高阻影响 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); // 校准 ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); while (1) { adc_value ADC1_ReadChannel0(); int volt_mv (int)((uint32_t)adc_value * 3300UL / 4095UL); sprintf(buffer, ADC%d, Voltage%d.%03dV\r\n, adc_value, volt_mv / 1000, volt_mv % 1000); USART1_SendString(buffer); // 简单延时避免虚拟终端刷新太快 for (volatile uint32_t i 0; i 2000000; i); } }这份代码在Proteus中的效果是如果PA0接的电压为0VADC输出接近0接3.3V时输出接近4095接1.65V时ADC值约2047电压显示约1.65V。6.2 Proteus侧需要做的设置原理图侧有几个关键点建议按下面核对芯片型号选STM32F103C8双击后在Program File中加载上述代码编译出的HEX晶振组件频率设为8MHzMCU属性里的Clock Frequency也设为8MHzVDDA和VREF接到3.3VVSSA接GND电位器POT-HG两端分别接到3.3V和GND中间抽头接PA0想观察串口输出放一个Virtual Terminal组件RX接PA9TX接PA10可选波特率设为115200仿真运行后改变电位器抽头位置或DVSRC电压值观察虚拟终端打印的ADC原始值和电压值。如果一切正常你会看到ADC值随输入电压线性变化。如果仍然全0进入下一步的定位清单。7. 最终定位清单从现象到根因的检查顺序7.1 七步定位表我在这次排查之后总结了一张表格之后每次遇到Proteus STM32 ADC全0问题都是按这个顺序走一遍检查项可能原因确认方法解决方案程序是否运行HEX路径错误、晶振频率属性不匹配LED是否闪烁、串口是否打印版本号重新加载最新HEX设置Clock Frequency为8M系统时钟SystemInit卡死或PCLK2频率与预期不符串口打印RCC_Clocks结构体检查晶振组件、调RCC_PCLK2Config和RCC_ADCCLKConfigADC时钟分频ADCCLK超过14MHz或分频过小计算PCLK2/分频系数72MHz下使用Div6或Div8GPIO模式PA0未配置为模拟输入GPIOA时钟未使能单步查看GPIOA-CRL寄存器配置GPIO_Mode_AIN使能RCC_APB2Periph_GPIOA通道编号信号源接错引脚、代码通道和引脚不对应对照通道表确认PA0Channel0修改接线或ADC_RegularChannelConfig参数VREF/电源VREF悬空、VDDA未接、信号源不共地用电压探针查看PA0实际电压VREF接3.3V信号源地与STM32 GND相连触发与等待未软件触发、触发后没等EOC、单步中变量看不全调试窗口查看ADC1-SR的EOC位按标准流程触发并while(EOCRESET)等待7.2 用Proteus调试窗口快速判断问题在哪个环节除了串口打印Proteus的调试窗口也能帮上大忙。运行仿真后在Debug菜单中打开外设寄存器查看界面或者在代码中加Watch变量直接观察ADC1-SR、ADC1-CR2和ADC1-DR三个关键寄存器的值。判断逻辑很简单如果ADC1-CR2的ADON位一直为0说明ADC_Cmd(ADC1, ENABLE)没生效先查RCC时钟使能和ADC_Init调用如果ADON1但EOC一直为0说明转换没完成重点查ADCCLK分频和软件触发语句是否执行以及在单步调试时有没有留出足够的仿真时间如果EOC1但DR0重点查GPIO模拟输入模式、引脚接线、VREF参考电压、信号源回路如果DR有值但电压不随电源变化重点查通道编号与引脚的对应关系。这套判断方法不依赖示波器也不用反复改代码适合在Proteus里快速定位。我后来真实调试板子时也用过类似的寄存器级排查思路一次就锁定了问题比盲改代码高效得多。从我那次踩坑到现在前后折腾了大半天。回头看真正的问题在于把仿真现象直接等同于芯片坏或者ADC外设不可用却没有按外设的时钟、引脚、触发时序逐一排查。Proteus对STM32模型虽然有些地方比真实芯片更严格但这反而逼着我把ADC的工作流程弄清楚了。以后再遇到采样全0我不会再急着换芯片而是先问自己三个问题时钟对吗引脚配了吗EOC等了吗
返回列表