I'm trying to use 8-bit ADC to control the brightness of a LED via PWM. The goal is to have a smooth fade in to the brightness set by the trim pot so I eventually want to introduce linearization correction as outlined in this thread, but for now I just want to see it working without that.
If I use the 1:16 Pre-scaler with PR2 = 24 values from Mister E's wonderful Multicalc, everything works fine:

Code:
' For production version of this code, comment out the line below re:
' LCD debugging and also update config fuses to have code protect on
#DEFINE USE_LCD_FOR_DEBUG ; comment out for non-debug use
' ***************************************************************
' Pin Connections
' ***************************************************************
' VDD -> pin 1 -> +5V
' RA5/Rc -> pin 2 -> EUSART receive
' RA2 -> pin 5 -> 1kohm -> 2N2222A transistor -> LEDs
' RA1 -> pin 6 -> Trim pot input
' RA0/Tx -> pin 7 -> EUSART transmit (LCD)
' VSS -> pin 8 -> GND
DEFINE OSC 16 ; Set oscillator 16Mhz
' ***************************************************************
' EUSART Settings for Tx/Rc (e.g. LCD)
' ***************************************************************
' > use Mister E PIC Multi-Calc application to get register/DEFINE settings
' > as the values are dependent on the OSC and desired baud rate
DEFINE HSER_RCSTA 90h ' Enable serial port & continuous receive
DEFINE HSER_TXSTA 24h ' Enable transmit, BRGH = 1
DEFINE HSER_CLROERR 1 ' Clear overflow automatically
DEFINE HSER_SPBRG 160 ' 9600 Baud @ 16MHz, -0.08%
' ***************************************************************
' Device Fuses
' ***************************************************************
' PIC chip data sheets can be found here: C:\Program Files\Microchip\MPASM Suite
#CONFIG
__config _CONFIG1, _FOSC_INTOSC & _WDTE_ON & _PWRTE_ON & _MCLRE_OFF & _CP_OFF & _CPD_OFF
__config _CONFIG2, _PLLEN_OFF & _STVREN_ON & _BORV_LO & _LVP_OFF
#ENDCONFIG
' ***************************************************************
' Initialization
' ***************************************************************
OSCCON = %01111000 ' 16MHz internal osc
pause 100
APFCON.2 = 0 ; Tx on RA0 for LCD display
APFCON.7 = 1 ; Rc on RA5
APFCON.0 = 0 ; CCP1 on RA2
' Some LCD serial modules need inverted data, some do not
' Enable the line below if needed, but for SparkFun SerLCD it should be
' commented out
'BAUDCON.4 = 1 ; Transmit inverted data to the Tx pin
' From Mister E's Multi-Calc for EUSART:
' *****************************************************************
SPBRGH = 1
BAUDCON.3 = 1 ' Enable 16 bit baudrate generator
' *****************************************************************
TRISA = %00100010 ' Make all pins output except for RA1 (trim pot input)
' and RA5 (EUSART Rc)
ANSELA = %00000010 ; Analog on PORTA.1 (AN1) only
FVRCON = 0 ' Fixed Voltage Reference is disabled
ADCON0 = %00000101 ' ADC (analog-to-digital) is enabled on AN1 (RA1) only
PAUSEUS 20 ; wait for the analog switch 'glitch' to die down
ADCON1 = %00110000 ; Left-justified results in 8-bits; Frc as timer
#IFDEF USE_LCD_FOR_DEBUG
LCD_INST CON 254 ' instruction
LCD_CLR CON 1 ' Clear screen
LCD_L1 CON 128 ' LCD line 1
LCD_L2 CON 192 ' LCD line 2
LCD_BR_CMD CON 124 ' command character for adjusting backlight brightness
LCD_BR_LVL CON 140 ' 140==40%
#ENDIF
ADCInVal VAR BYTE ; stores ADCIN result read from trim pot
compVal VAR BYTE ; stores last-changed ADC value
CounterA var BYTE ' Just a BYTE Temporary working variable
DataW var WORD ' Just a WORD Temporary working variable
RawData var WORD [16] ' Array holding ADC Result
LEDBrVal VAR BYTE
i VAR BYTE
#IFDEF USE_LCD_FOR_DEBUG
pause 1000
HSEROUT [LCD_INST, LCD_CLR, "LCD Init"]
pause 5
HSEROUT [LCD_INST, LCD_L2, " SerLCD_V2_5"]
pause 2000
#ENDIF
' Should only need to do this one time to adjust backlight brightness
'#IFDEF USE_LCD_FOR_DEBUG
' HSEROUT [LCD_BR_CMD, LCD_BR_LVL]
' PAUSE 5
'#ENDIF
' ***************************************************************
' Set up PWM on CCP1
' ***************************************************************
CCP1CON = %00001100 ; Use CCP1 in PWM mode
' Set duty cycle registers initially to 0
CCP1CON.4 = 0
CCP1CON.5 = 0
CCPR1L = 0
' Use Mister E's PICMultiCalc_1.3.1.exe application (Windows only)
' to determine prescaler and PR2 values for given OSC frequency (e.g. 16Mhz)
' and duty cycle (use 100% to see highest actual value)
' ************************
' CCP1 uses TMR2
' ************************
T2CON = %00000110 ; Timer2 on with 1:16 prescaler
PR2 = 24 ; For 16Mhz OSC the desired output freq of 10,000Hz
; is achieved with this PR2 value (8-bit resolution
; with 1:16 prescaler)
; MAX DUTY = 100
MaxDuty VAR BYTE ; According to Darrel:
; MaxDuty = (PR2 + 1) * 4
MaxDuty = (PR2 + 1) * 4 ; 100
MinDuty CON 25 ; Minimum brightness for this application
MaxADCVal CON 255 ; 255 for 8-bit; 1023 for 10-bit
FadeInPause CON 100 ; Pause during LED fade in
GOSUB Do_ADC
compVal = ADCInVal ; set initial compare value
GOSUB Map_ADCInVal_to_PWM_Duty
#IFDEF USE_LCD_FOR_DEBUG
HSEROUT [LCD_INST, LCD_CLR]
pause 5
HSEROUT ["LEDBrVal=",DEC LEDBrVal," "]
pause 500
#ENDIF
' Fade in LED to set brightness
FOR i = 0 to LEDBrVal
CCP1CON.4 = i.0
CCP1CON.5 = i.1
CCPR1L = i >> 2
pause FadeInPause
#IFDEF USE_LCD_FOR_DEBUG
HSEROUT [LCD_INST, LCD_CLR]
pause 5
HSEROUT ["i=",DEC i," "]
#ENDIF
NEXT i
Main:
GOSUB Do_ADC
PAUSE 100
If (compVal > (ADCInVal + 1)) or (compVal < (ADCInVal - 1)) THEn
compVal = ADCInVal
GOSUB Map_ADCInVal_to_PWM_Duty
gosub ChngLEDBrightness
#IFDEF USE_LCD_FOR_DEBUG
HSEROUT [LCD_INST, LCD_CLR]
pause 5
HSEROUT ["new ADCInVal", LCD_INST, LCD_L2, DEC ADCInVal, " "]
#ENDIF
ENDIF
GOTO Main
Do_ADC:
' Stuff 16 Element WORD Array full of ADC values
' ----------------------------------------------
For CounterA = 0 to 15
ADCON0 = %00000101 ' Select Channel, Turn-On A/D
' 7=0 Unused
' 6=0 Unused
' 5=0 )
' 4=0 )
' 3=0 ) selects AN1
' 2=0 )
' 1=0 Go-done Bit
' 0=1 switch-On ADC Module
Pauseus 50 ' Wait for channel to setup
ADCON0.1 = 1 ' Start conversion
While ADCON0.1=1:Wend ' Wait for conversion
' DataW.HighByte=ADRESH
' DataW.LowByte=ADRESL
DataW = ADRESH ' Read variable from ADC and save
RawData(CounterA) = DataW
Next CounterA
' Sort ADC Input
' --------------
CounterA = 0
GetADCSortLoop:
If RawData(CounterA + 1) < RawData(CounterA) then
DataW = RawData(CounterA)
RawData(CounterA) = RawData(CounterA+1)
RawData(CounterA+1) = DataW
If CounterA > 0 then CounterA = CounterA - 2
endif
CounterA=CounterA+1
If CounterA < 15 then goto GetADCSortLoop
' Quanticise, discarding top and bottom FOUR elements
' ----------------------------------------------------
DataW = 0
For CounterA = 4 to 11
DataW = DataW+RawData(CounterA)
Next CounterA
ADCInVal = DataW>>3 ' Divide Result by EIGHT
' PAUSEUS 50 ' Wait for A/D channel acquisition time
' ADCON0.1 = 1 ' Start conversion
' WHILE ADCON0.1 = 1 ' Wait for it to complete
' WEND
' ADCInVal = ADRESH
return
'*********** Map Vref to duty range ************************************
Map_ADCInVal_to_PWM_Duty:
' Arduino Map function to emulate:
' ===============================
' map(value, fromLow, fromHigh, toLow, toHigh)
' long map(long x, long in_min, long in_max, long out_min, long out_max)
' {
' return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
' }
' TODO: If switching from onboard trim pot to ext trim pot and there isn't one
' one connected to the 3-pin connector, may need to make in_min greater
' than 0
LEDBrVal = (compVal - 0) * (MaxDuty - MinDuty)/(MaxADCVal - 0) + MinDuty
' LEDBrVal = (compVal ** 39322) + MinDuty ' Same as 100 + VrefInValue * 0.600006 but likely faster than 100+VrefInValue*6/10
' #IFDEF USE_LCD_FOR_DEBUG
' HSEROUT [LCD_INST, LCD_CLR]
' pause 5
' HSEROUT ["compVal=",DEC compVal," ",LCD_INST, LCD_L2]
' pause 500
' HSEROUT ["MotorDuty=",DEC MotorDuty," "]
' #ENDIF
RETURN
'*********** Set duty registers ****************************************
ChngLEDBrightness:
CCP1CON.4 = LEDBrVal.0
CCP1CON.5 = LEDBrVal.1
CCPR1L = LEDBrVal >> 2
RETURN
With the above, if I have the trim pot wiper arm all the way to the right it fades in the LED to 100 duty cycle and I can vary the brightness via the duty cycle by changing the trim pot (Min: 25, Max: 100) via the Map_ADCInVal_to_PWM_Duty function.
But if I try to implement more discrete PWM steps as discussed in the thread linked above, I get weird results.

Code:
' For production version of this code, comment out the line below re:
' LCD debugging and also update config fuses to have code protect on
#DEFINE USE_LCD_FOR_DEBUG ; comment out for non-debug use
' ***************************************************************
' Pin Connections
' ***************************************************************
' VDD -> pin 1 -> +5V
' RA5/Rc -> pin 2 -> EUSART receive
' RA2 -> pin 5 -> 1kohm -> 2N2222A transistor -> LEDs
' RA1 -> pin 6 -> Trim pot input
' RA0/Tx -> pin 7 -> EUSART transmit (LCD)
' VSS -> pin 8 -> GND
DEFINE OSC 16 ; Set oscillator 16Mhz
' ***************************************************************
' EUSART Settings for Tx/Rc (e.g. LCD)
' ***************************************************************
' > use Mister E PIC Multi-Calc application to get register/DEFINE settings
' > as the values are dependent on the OSC and desired baud rate
DEFINE HSER_RCSTA 90h ' Enable serial port & continuous receive
DEFINE HSER_TXSTA 24h ' Enable transmit, BRGH = 1
DEFINE HSER_CLROERR 1 ' Clear overflow automatically
DEFINE HSER_SPBRG 160 ' 9600 Baud @ 16MHz, -0.08%
' ***************************************************************
' Device Fuses
' ***************************************************************
' PIC chip data sheets can be found here: C:\Program Files\Microchip\MPASM Suite
#CONFIG
__config _CONFIG1, _FOSC_INTOSC & _WDTE_ON & _PWRTE_ON & _MCLRE_OFF & _CP_OFF & _CPD_OFF
__config _CONFIG2, _PLLEN_OFF & _STVREN_ON & _BORV_LO & _LVP_OFF
#ENDCONFIG
' ***************************************************************
' Initialization
' ***************************************************************
OSCCON = %01111000 ' 16MHz internal osc
pause 100
APFCON.2 = 0 ; Tx on RA0 for LCD display
APFCON.7 = 1 ; Rc on RA5
APFCON.0 = 0 ; CCP1 on RA2
' Some LCD serial modules need inverted data, some do not
' Enable the line below if needed, but for SparkFun SerLCD it should be
' commented out
'BAUDCON.4 = 1 ; Transmit inverted data to the Tx pin
' From Mister E's Multi-Calc for EUSART:
' *****************************************************************
SPBRGH = 1
BAUDCON.3 = 1 ' Enable 16 bit baudrate generator
' *****************************************************************
TRISA = %00100010 ' Make all pins output except for RA1 (trim pot input)
' and RA5 (EUSART Rc)
ANSELA = %00000010 ; Analog on PORTA.1 (AN1) only
FVRCON = 0 ' Fixed Voltage Reference is disabled
ADCON0 = %00000101 ' ADC (analog-to-digital) is enabled on AN1 (RA1) only
PAUSEUS 20 ; wait for the analog switch 'glitch' to die down
ADCON1 = %00110000 ; Left-justified results in 8-bits; Frc as timer
#IFDEF USE_LCD_FOR_DEBUG
LCD_INST CON 254 ' instruction
LCD_CLR CON 1 ' Clear screen
LCD_L1 CON 128 ' LCD line 1
LCD_L2 CON 192 ' LCD line 2
LCD_BR_CMD CON 124 ' command character for adjusting backlight brightness
LCD_BR_LVL CON 140 ' 140==40%
#ENDIF
ADCInVal VAR WORD ; stores ADCIN result read from trim pot
compVal VAR WORD ; stores last-changed ADC value
CounterA var BYTE ' Just a BYTE Temporary working variable
DataW var BYTE ' Just a WORD Temporary working variable
RawData var BYTE [16] ' Array holding ADC Result
LEDBrVal VAR WORD
i VAR WORD
'#IFDEF USE_LCD_FOR_DEBUG
' pause 1000
' HSEROUT [LCD_INST, LCD_CLR, "LCD Init"]
' pause 5
' HSEROUT [LCD_INST, LCD_L2, " SerLCD_V2_5"]
' pause 2000
'#ENDIF
' Should only need to do this one time to adjust backlight brightness
'#IFDEF USE_LCD_FOR_DEBUG
' HSEROUT [LCD_BR_CMD, LCD_BR_LVL]
' PAUSE 5
'#ENDIF
' ***************************************************************
' Set up PWM on CCP1
' ***************************************************************
CCP1CON = %00001100 ; Use CCP1 in PWM mode
' Set duty cycle registers initially to 0
CCP1CON.4 = 0
CCP1CON.5 = 0
CCPR1L = 0
' Use Mister E's PICMultiCalc_1.3.1.exe application (Windows only)
' to determine prescaler and PR2 values for given OSC frequency (e.g. 16Mhz)
' and duty cycle (use 100% to see highest actual value)
' ************************
' CCP1 uses TMR2
' ************************
T2CON = %00000110 ; Timer2 on with 1:16 prescaler
PR2 = 127 ; For 16Mhz OSC the desired output freq of 1954Hz
; is achieved with this PR2 value (9-bit resolution
; with 1:16 prescaler)
; MAX DUTY = 100
MaxDuty VAR WORD ; According to Darrel:
; MaxDuty = (PR2 + 1) * 4
MaxDuty = (PR2 + 1) * 4 ; 100
MinDuty CON 100 ; Minimum brightness for this application
MaxADCVal CON 255 ; 255 for 8-bit; 1023 for 10-bit
FadeInPause CON 50 ; Pause during LED fade in
GOSUB Do_ADC
compVal = ADCInVal ; set initial compare value
GOSUB Map_ADCInVal_to_PWM_Duty
#IFDEF USE_LCD_FOR_DEBUG
HSEROUT [LCD_INST, LCD_CLR]
pause 5
HSEROUT ["LEDBrVal=",DEC LEDBrVal," "]
pause 750
#ENDIF
' Fade in LED to set brightness
FOR i = 0 to LEDBrVal
CCP1CON.4 = i.0
CCP1CON.5 = i.1
CCPR1L = i >> 2
pause FadeInPause
#IFDEF USE_LCD_FOR_DEBUG
HSEROUT [LCD_INST, LCD_CLR]
pause 5
HSEROUT ["i=",DEC i," "]
#ENDIF
NEXT i
Main:
GOSUB Do_ADC
PAUSE 100
If (compVal > (ADCInVal + 1)) or (compVal < (ADCInVal - 1)) THEn
compVal = ADCInVal
GOSUB Map_ADCInVal_to_PWM_Duty
gosub ChngLEDBrightness
#IFDEF USE_LCD_FOR_DEBUG
HSEROUT [LCD_INST, LCD_CLR]
pause 5
HSEROUT ["new ADCInVal", LCD_INST, LCD_L2, DEC ADCInVal, " "]
#ENDIF
ENDIF
GOTO Main
Do_ADC:
' Stuff 16 Element WORD Array full of ADC values
' ----------------------------------------------
For CounterA = 0 to 15
ADCON0 = %00000101 ' Select Channel, Turn-On A/D
' 7=0 Unused
' 6=0 Unused
' 5=0 )
' 4=0 )
' 3=0 ) selects AN1
' 2=0 )
' 1=0 Go-done Bit
' 0=1 switch-On ADC Module
Pauseus 50 ' Wait for channel to setup
ADCON0.1 = 1 ' Start conversion
While ADCON0.1=1:Wend ' Wait for conversion
' DataW.HighByte=ADRESH
' DataW.LowByte=ADRESL
DataW = ADRESH ' Read variable from ADC and save
RawData(CounterA) = DataW
Next CounterA
' Sort ADC Input
' --------------
CounterA = 0
GetADCSortLoop:
If RawData(CounterA + 1) < RawData(CounterA) then
DataW = RawData(CounterA)
RawData(CounterA) = RawData(CounterA+1)
RawData(CounterA+1) = DataW
If CounterA > 0 then CounterA = CounterA - 2
endif
CounterA=CounterA+1
If CounterA < 15 then goto GetADCSortLoop
' Quanticise, discarding top and bottom FOUR elements
' ----------------------------------------------------
DataW = 0
For CounterA = 4 to 11
DataW = DataW+RawData(CounterA)
Next CounterA
ADCInVal = DataW>>3 ' Divide Result by EIGHT
' PAUSEUS 50 ' Wait for A/D channel acquisition time
' ADCON0.1 = 1 ' Start conversion
' WHILE ADCON0.1 = 1 ' Wait for it to complete
' WEND
' ADCInVal = ADRESH
return
'*********** Map Vref to duty range ************************************
Map_ADCInVal_to_PWM_Duty:
' Arduino Map function to emulate:
' ===============================
' map(value, fromLow, fromHigh, toLow, toHigh)
' long map(long x, long in_min, long in_max, long out_min, long out_max)
' {
' return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
' }
' TODO: If switching from onboard trim pot to ext trim pot and there isn't one
' one connected to the 3-pin connector, may need to make in_min greater
' than 0
LEDBrVal = (compVal - 0) * (MaxDuty - MinDuty)/(MaxADCVal - 0) + MinDuty
' LEDBrVal = (compVal ** 39322) + MinDuty ' Same as 100 + VrefInValue * 0.600006 but likely faster than 100+VrefInValue*6/10
' #IFDEF USE_LCD_FOR_DEBUG
' HSEROUT [LCD_INST, LCD_CLR]
' pause 5
' HSEROUT ["compVal=",DEC compVal," ",LCD_INST, LCD_L2]
' pause 500
' HSEROUT ["MotorDuty=",DEC MotorDuty," "]
' #ENDIF
RETURN
'*********** Set duty registers ****************************************
ChngLEDBrightness:
CCP1CON.4 = LEDBrVal.0
CCP1CON.5 = LEDBrVal.1
CCPR1L = LEDBrVal >> 2
RETURN
With this code, I only see ADC values ranging from 0-31. The Map_ADCInVal_to_PWM_Duty function seems to only give me a max of 150, whereas it should be 512.
Can anyone help steer me onto the right path?
Bookmarks