logo elektroda
logo elektroda
X
logo elektroda

PIC12F683 - two-channel dimmer and encoder support on 128 bytes of RAM

p.kaczmarek2  1 87 Cool? (+3)
📢 Listen (AI):

TL;DR

  • A PIC12F683 project builds a two-channel LED dimmer with a rotary encoder and pushbutton, despite only 128 bytes of RAM and one hardware PWM output.
  • Software PWM runs in GPIO code, the encoder uses a state machine to filter bad transitions, and the button toggles which dimmer channel is active.
  • Timer0 interrupts drive the dimming loop at about 50 kHz, and DIMMER_MAX was reduced to 50 to raise the output frequency.
  • The controller stores the last brightness in internal EEPROM, so settings survive power loss instead of resetting to default.
  • An AOD454 MOSFET drove LED strips directly from 5 V GPIO and stayed acceptable thermally, reaching 65 °C at 5.1 A.
Generated by the language model.

Here is an account of building a two-channel dimmer based on an 8-bit PIC12F683 microcontroller and parts from electrical junk. The project will be written in C using the SDCC compiler. A single encoder will be used to control the brightness levels, and pressing the encoder will toggle the bar being controlled at the time. The PWM will be realised programmatically on a timer and interrupt, as the PIC presented here has only one hardware channel for pulse generation, and my assumptions require at least two strips to be controlled.

This topic is part of my series on PIC microcontrollers and the SDCC compiler. For other parts, please see the topics below:
Part 1 - Setting up the operating environment
https://www.elektroda.pl/rtvforum/topic3635522.html#18304424
Part 2 - Blink LED, IO pins, digital inputs and outputs
https://www.elektroda.pl/rtvforum/topic3647884.html#18389188
Part 3 - Oscillator settings. Internal oscillator, external oscillator, quartz resonator, PLL
https://www.elektroda.pl/rtvforum/topic3657704.html
Part 4 - Timers, interrupts
https://www.elektroda.pl/rtvforum/topic3676645.html#18580858
Part 5 - Seven-segment display operation
https://www.elektroda.pl/rtvforum/topic3676650.html#18580877
Part 6 - MM5450 LED display driver
https://www.elektroda.pl/rtvforum/topic3845301.html
Related topics about PIC12F:
PIC12F683 and SDCC - tutorial - we create a simple dimmer (read catalogue notes)
Christmas WS2812 animations on PIC12F683 - how many LEDs will 128 bytes of RAM handle?

Introduction
This is another project on a tiny eight-bit - this time based on an encoder. I've already shown a dimmer in the past, so this time I decided to add such variety. In addition, my previous presentation was based on a microC, where some of the mechanisms are ready, and today I am creating from 0 in SDCC. Maybe this will interest someone, but still a glimpse of the MCU used.


2048 bytes of Flash for instructions and fixed data, 128 bytes of RAM for variables.... that's not a lot of memory, but it should be enough for a dimmer. The worse thing is that we only have one PWM, and I want to handle two strips:

I will undoubtedly implement the PWM programmatically, via GPIO operations.

Part preparation
I try to base my builds on parts from electrical junk, so I started with my collection of selected circuit boards:

A module from a car radio caught my eye. There is a display, a controller and encoder:

The controller is a PD6340A. I could not find its datasheet online, although I did find a diagram of the radio. It is connected to the main controller by some strange two-pin protocol.

I soldered the encoder using flux and braid - I apply flux to the solder, then press the braid to the solder with a hot tip. The braid collects the solder from the pads. Then I pull the component out, still helping myself with the soldering iron, as not all the binder will always collect.

I then specified the encoder leads.
Here we have:
- the encoder proper - that is, the "infinite potentiometer", whose outputs are three pins - ground and two signals, these three legs. I have whittled them down and added wires:

- an extra button - simply pressing the encoder short circuits the circuit


Effect of encoder on oscilloscope
I pulled up both outputs (extreme legs) of the encoder to the power supply via 10 kΩ resistors. I connected the middle leg to ground. I connected the oscilloscope probes to the outputs. Let's observe what happens on them.
Turning clockwise:

The low state first appears on channel one (yellow waveform).
Turning left:

The low state first appears on channel two (blue waveform).
If there is no movement, we still have a high state on both outputs.
We already have a stopping point - it still needs to be detected. Let's assume that we sample fast enough. When there is no movement, both digital pins will return 11, but when movement starts we get 10 or 01, depending on which way the encoder is turned.
So many more of these states - as we have 2 pins (2 bits), we have 2^2 = 4 possible states. Additionally, we want to examine the state before and after, so we have two snapshots of 2 bits each, for a total of 2^3 = 8 possibilities.
We decompose according to the oscilloscope transitions for the movement (let's assume) to the right:
- 0b00 -> 0b01
- 0b01 -> 0b11
- 0b11 -> 0b10
- 0b10 -> 0b00
Any other transition means movement in the opposite direction, except for no change.
This can already be entered in the code...

First steps with the encoder
First we include the header of the PIC used and set its configuration - internal oscillator, no watchdog.
Code: C / C++
Log in, to see the code

Then we boot the PIC, disable the anolog pins and comparators, initialise the pin mode (two inputs - TRISIO), set the internal oscillator (OSCCON), enable the built-in pull up resistors (OPTION_REG and WPU - to set the default high state on the encoder outputs) and initialise the variables with their initial values.
Code: C / C++
Log in, to see the code

Now it's time for the actual encoder logic. Here I take advantage of the fact that the two pins are right next to each other in the GPIO register. I simply shift it by 4 to get to the fourth bit and then do a logic product with bit 0b11 to just get the two values. I then compare them as I described earlier. Finally, I determine whether there was a clockwise or counterclockwise movement and based on that at that point I just turn the dimmer on or off.
Code: C / C++
Log in, to see the code



Dimmer base
The encoder is working, the dimmer can already be realised. The best way would be to do it on a hardware PWM, but the PIC12F683 only has one such output, and I ultimately want two. For this reason, I made a simple software PWM. I count the executions of the loop (from 0 to 100) and compare the current index with the dimmer value, which the encoder can also edit. In this way I get a smooth fill adjustment:
Code: C / C++
Log in, to see the code







Fixes - higher frequency
At this stage the first downside of software PWM came out. My DIMMER_MAX is too large - 100% fill requires too many steps. A hardware PWM would have been better, but there's only one such module here, so not enough for me. I implemented the most obvious fix, i.e. reducing DIMMER_MAX to 50, which had the effect of doubling the frequency of the generated signal.
Code: C / C++
Log in, to see the code



Button and dimmer distribution
Many encoders additionally have a button contained within them. This is the simplest microswitch, no major philosophy here. I have delegated one GPIO pin for it in input mode with a built-in pull-up resistor. This allows it to be forced into a default high state. Pressing the button shorts it to ground. When the button is pressed I flip the selected dimmer - now the program handles two separate dimmers. There is no support for contact oscillation (debouncing) in the code yet, but a small value capacitor such as 100 nF can be given between GPIO and ground.
I describe the status of the dimmers with separate variables - I have not been tempted to make an array for now, although with more dimmers it would definitely be appropriate to use one.
Code: C / C++
Log in, to see the code

I've tested the program and that's enough to operate two dimmers without contact vibration problems.

Tests with LED
Watching waveforms on an oscilloscope is one thing, but real tests with LEDs are no substitute. Maybe single LEDs will suffice for now. The PIC's GPIO can easily drive a regular LED:








Stability correction needed
Brief tests with single LEDs, however, showed that practice is not as elegant as theory after all. Occasionally, fast rotation generated erroneous readings, making it somewhat difficult to set the highest or lowest brightness.
Undoubtedly, there is a need to make this controller more secure and add tracking of the current and previous state.
I decided to use the tried and tested solution from the Arduino. Still without interrupts, but more reliable:
https://github.com/brianlow/Rotary/blob/master/Rotary.cpp
Code: C / C++
Log in, to see the code

This method uses a state machine and examines whether the encoder has made a full movement one way or the other just according to these states. In the event of a wrong step, it appropriately 'rolls back' the state to the corresponding earlier step. Only specific final states indicate movement - here with the DIR_CW and DIR_CCW bytes attached.
The implementation of the states here, in turn, boils down to holding their very indices in an array - for example, being in the R_START state we are in the first row of the array, now we have a reading from two GPIOs (4 possible values) and with these indices we specify a column from the first row, and the value from this column is the index of the new state (and possibly the DIR movement code).
The array is chosen to handle all possible transitions between states, so it contains as many as 7*4=28 possibilities.
It is worth looking, for example, at the second row - index R_CW_FINAL - there you can see that if something goes wrong, you do not get DIR_ information, but return to R_START.

After these changes, I can no longer call up incorrect reads in any way.


Flipping the dimmer to timer
The second necessary fix is to move the dimmer operation to a hardware timer. Such a timer can run at a frequency we specify and call an interrupt where we handle the software PWM creation. I can't just use an off-the-shelf PWM here, because this PIC only has one hardware PWM (based on CCP1).
What needs to be done to start Timer0 with an interrupt?
First we set bit 5 (Timer 0 clock source) and bits 2-1 (prescaler) in OPTION_REG:

We then enable general interrupts and Timer 0 overflow interrupts in INTCON:

You still need to set the value of TMR0, which is the one that will be counted down. The interrupt will be called when 0xFF - > 0x00 of this value is overflowed.

Personally, I wanted a reasonably fast interrupt, so I selected a 1:1 prescaler, so since Timer0 goes at Fosc/4, TMR0 is incremented at 2 MHz. Now all we need to do is select the value of TMR0 so that our interrupt calls at about 50 kHz. This is because I have 50 degrees of brightness (DIMMER_MAX) and I want about 1 kHz at the output. I can always reduce DIMMER_MAX significantly if I need to and relieve the CPU.
The last thing to remember is to reset TMR0 to the desired value in the interrupt and to clear the timer interrupt flag.
All code:
Code: C / C++
Log in, to see the code




Transistor selection
Often when dismantling various electrical junk, I solder and put away transistors. Before soldering, I check their parameters to collect what I can use. One day a couple of D454s, actually AOD454s, ended up in the inverter from the monitor backlight.

This is an N-type channel MOSFET with a fairly high drain current (12 A at Vgs=10) and low resistance in the open state. The PIC12F would control it directly from its pins (more precisely: via a resistor), so the gate voltage would be 5 V. The datasheet note shows that at Vgs=5 V, Rds(on) is 47 mΩ, which is an acceptable value. Not every transistor can be driven from such a low voltage. For the detailed behaviour of the transistor, refer to the graphs in the datasheet note.

This is, of course, a considerable simplification, but in such a simple case my choice worked.

Tests with LED strip
I wanted to check how much my transistor would heat up. Choosing a transistor along with a gate resistor is not a trivial matter and far beyond the subject of this short PIC presentation so I wasn't expecting sensational results, but I still had to see if it would work at all.
The transistor was soldered onto a small piece of copper-coated laminate, so heat dissipation was quite reduced, but maybe that's better - you can always improve performance.
I tested everything with a laboratory power supply at 12 volts, attaching more LED strips to increase the load.

By the way, I advise you against testing LED strips this way, even the rollers bend from the heat, the adhesive layer under the strip also degrades.

At a load of 0.66 A, the transistor is cold. This 40 °C is only due to reflection in the binder.

At a load of 1.85 A the transistor only starts to heat up, it reaches just under 30 °C.

At a load of 2.9 A, the transistor exceeds 35 °C.

At a load of 5.1 A, the transistor reaches 65 °C.

Probably if only some heatsink were added, the temperature would drop even further.
The results are fully satisfactory to me.


Write to internal EEPROM
One shortcoming remains. The dimmer now forgets its setting when it is powered off. Most of the time this is not a problem, as we tend to assume that the PIC is powered all the time, but some people may have occasional power outages and it is not desirable for the dimmer to forget its setting then.
In the case of very short power outages, one could combine and give extra capacitors on the PIC's power line, but this is unlikely to make sense in the long run.
Fortunately, our tiny PIC has a whole 256 bytes of non-volatile EEPROM:

The embedded EEPROM is controlled by several registers with names starting with EE:

EEDAT stores the data byte, EEADR its address. EECON1 controls memory operations.
Write and read operations are also described in the data note.

Reading is the simplest - simply set the address, RD bit and then read the data.
Writing is similar, but slightly more difficult - we enable the write, disable the interrupts, and then send the magic 0x55 0xAA values, and only then start the write and wait for it to succeed.
I only need to do the read once after the MCU starts, I do the write with each change.
Code: C / C++
Log in, to see the code


Optimising EEPROM consumption
Recording with each change is not an optimal approach. By rotating the encoder we will have a lot of these changes, after all, with 50 brightness levels here we have a good 50 records. This will wear out the EEPROM faster, although with its robustness, it can be solidly argued that it really does take a lot of such 'dimming' cycles to damage it:

Nevertheless, I will give a simple way how this could be improved. You simply need to reduce the number of writes - you could, for example, save when the user finishes an operation. The question arises how to detect this - probably the easiest way is to reset the cycle counter to a given fixed value when the brightness level changes, and keep decreasing it in the background as long as it is not zero. When it reaches zero after decreasing, then we make a save.
A similar procedure is useful (and perhaps more useful) when writing directly to Flash memory, which may have fewer predicted erase/write cycles.

Summary
This time the PIC12F683 was fully up to the task at hand, and it would probably even be possible to drive more LED strips this way - we are only limited by the amount of free GPIOs. With 8 pins, including ground and power, we are left with 6 candidates to use. Ostensibly there is the MCLR, but you can disable its RESET role and use it as GP3. GP3 can only be an input, but for a button it will do. So 6 pins, including 3 for the encoder - I'm unlikely to skip that. That leaves 3 pins for LED strips. It would be possible to make an RGB version. The selected transistor is also doing well. 65 °C at 5 A is rather good. One could now think of some 3D printed housing and we have a functional dimmer.

About Author
p.kaczmarek2
p.kaczmarek2 wrote 14318 posts with rating 12209 , helped 648 times. Been with us since 2014 year.

Comments

androot 11 Apr 2026 13:12

Isn't it simpler to interrupt on the edge from one input and check the state of the other to distinguish the direction? [Read more]

%}