基于PIC18F47-Q18开发板的10位ADC信号转8位PWM输出适配问题
看起来你遇到的核心问题是10位ADC值到8位PWM的适配逻辑错误,再加上代码里几个关键的配置/逻辑bug,导致LED亮度循环四次。我来一步步帮你梳理和解决:
问题根源分析
ADC到PWM的缩放错误:
10位ADC的输出范围是01023,而8位PWM占空比寄存器(`PWM4DCH`)的范围是0255。你直接把ADRESL(10位值的低8位)赋值给PWM4DCH,相当于把10位值拆成了4个256的区间,每个区间的PWM值重复,所以LED会四次循环亮灭——这就是你看到的现象。ADC转换等待逻辑完全写反:
你代码里的while (ADCON0bits.GO ==0);是错误的!GO/DONE位在转换开始时被置1,转换完成后自动清0。正确的等待逻辑应该是等待GO位为1的状态结束,也就是while (ADCON0bits.GO == 1);。你现在的代码会在转换还没完成时就读取ADC值,得到的都是旧数据或错误值。Timer2被反复开关,导致PWM输出中断:
PWM模块依赖Timer2作为时基,必须让Timer2持续运行才能输出稳定的PWM波形。你在主循环里反复启停Timer2,会让PWM输出断断续续,进一步加剧LED的异常闪烁。可能的ADC对齐位设置错误:
你提到ADC是右对齐,但代码里ADCON0 = 0x84,查PIC18F47-Q18手册可知,ADCON0的bit1是ADFM(ADC结果对齐位),1是右对齐,0是左对齐。0x84是0b10000100,ADFM位是0——这意味着你实际设置的是左对齐,和你描述的ADRESL/ADRESH的存储逻辑完全相反,这会导致ADC值读取完全错误!
解决方案步骤
1. 修正ADC配置与读取逻辑
- 确保ADC右对齐:把
ADCON0设置为0x86(0b10000110),这样ADFM=1,ADRESL存储低8位,ADRESH存储高2位,符合你的预期。 - 修正转换等待逻辑:改成等待
GO位为1的状态结束。
2. 正确缩放10位ADC值到8位PWM
把10位ADC值(01023)缩放为8位PWM值(0255)最高效的方法是右移2位(等价于除以4,因为1023/4≈255.75,刚好对应8位的最大值255)。代码实现可以是:
// 合并10位ADC值 uint16_t adc_val = ((uint16_t)ADRESH << 8) | ADRESL; // 缩放为8位PWM值 PWM4DCH = adc_val >> 2;
或者直接用位运算(更高效,适合单片机):
PWM4DCH = (ADRESL >> 2) | (ADRESH << 6);
这样当电位器转到最小时(ADC值0),PWM占空比0,LED最暗;电位器转到最大时(ADC值1023),PWM占空比255,LED最亮,完全符合你的需求。
3. 让Timer2持续运行
在初始化代码里打开Timer2(T2CONbits.T2ON = 1),之后不要再在主循环里反复开关它。PWM需要稳定的时基才能输出连续波形。
修正后的完整代码
// CONFIG1L #pragma config FEXTOSC = OFF #pragma config RSTOSC = HFINTOSC_64MHZ // CONFIG1H #pragma config CLKOUTEN = OFF #pragma config CSWEN = OFF #pragma config FCMEN = OFF // CONFIG3L #pragma config WDTCPS = WDTCPS_31 #pragma config WDTE = OFF #define clock OSCCON1 // define the timer control bits #define freq OSCFRQ #define enable_timer T2CONbits.T2ON // define the timer control bits #define interrupt_enable PIE4bits.TMR2IE #define PB1 PORTBbits.RB4 #define PB2 PORTCbits.RC5 #define light PORTAbits.RA7 void main(void) { clock = 0x60; // use High Mhz clock freq = 0x03; // Use 8Mhz clock T2CLKCON = 0x01; // Make Timer2 Fosc/4 ANSELA = 0x01; // set up port A,B,C D/A TRISA = 0x01; // set up port A,B,C I/O ANSELB = 0x00; TRISB = 0xFF; ANSELC = 0x00; TRISC = 0xFF; T2PR = 0xC7; // period of the PWM PWM4DCH = 0x00; // 初始占空比0 PWM4DCL = 0x00; // 8位PWM的话,PWM4DCL可以忽略(因为只用到高8位) RA4PPS = 0x08; // set RA4 to be PWM output T2CON = 0x20; // set pre scale value of 1:4 PWM4CONbits.PWM4EN = 1; // enable the PWM PWM4CONbits.POL = 1; // reverse Polarity of PWM enable_timer = 1; // 初始化时就打开Timer2,持续运行 while (1) { ADCON0 = 0x86; // ADC使能 + 右对齐(ADFM=1) + CHS=0 ADPCH = 0x00; // Selected channel is RA0 ADCON0bits.GO = 1; // start ADC conversion while (ADCON0bits.GO == 1); // 等待转换完成(GO位从1变0) // 缩放10位ADC值到8位PWM uint16_t adc_val = ((uint16_t)ADRESH << 8) | ADRESL; PWM4DCH = adc_val >> 2; ADCON0bits.ADON = 0; // Turn off ADC enable bit } return; }
额外说明
- 关于你尝试的合并ADC值再赋值的代码:
PWM4DCH = (ADRESH<<8 | ADRESL);是错误的,因为PWM4DCH是8位寄存器,16位值会被自动截断低8位,和直接赋值ADRESL效果完全一样,所以解决不了问题。 - 后续改用中断的思路是对的,等你熟悉中断后,可以把ADC转换放在ADC完成中断里,把Timer2溢出中断作为PWM的时基,这样代码会更高效。
备注:内容来源于stack exchange,提问作者Ivan

