To continue this little weblog series on bits, I’m going to go into bit fractions.
Yeah, right… there is no such thing, of course – unless you’re into probabilities or fuzzy logic, perhaps.
What I actually want to do, is describe “analog output” on the ATmega, using the Arduino library’s analogWrite() function. And throw in some bit manipulations along the way, to stay somewhat on topic.
The little secret with analogWrite() is that it doesn’t do what its name suggests. The ATmega has no way of generating an analog signal, i.e. a voltage level between 0 and VCC.
Instead, a pulse is generated, with a varying duty cycle. I.e. the “on” and the “off” times of the pulse will be different, the ratio of these times being proprtional to the 0..255 value passed as argument to analogWrite(). With “0”, the signal will be 100% off, with “255” the signal will be 100% on. With “128” the pulse will be on the same amount of time as off. With “1”, it will be on very briefly, and then off most of the time, and so on…
The neat thing is that you connect it to an incandescent lamp, or a motor, then the effect will be that these will light/turn at less than their full power, due to the time it takes for these devices to try and follow the pulse. So the effect is similar to a fractional adjustment: you can dim / slow down these devices by using analogWrite().
It even works with LEDs, although these turn on and off very fast. In this case, the reason is that our eyes can’t follow such fast changes, and so we perceive the result as dimmed as well. A whole industry was once created around this “persistence of vision” property of our eyes – it’s called TV…
Here’s a sketch which uses this pulsed output to control the brightness of a LED connected to DIO3 (i.e. D6):
Note that I didn’t have to define pin 6 as an output, analogWrite() does that.
What the above does, is ramp up gradually from 0 to 255, and then repeat:
Suppose we want it to fade in and out instead:
Try implementing this yourself.
Note that you’re going to need at least 9 bits of information to do this: 8 for the brightness level and 1 to keep track of whether you’re currently in the up ramp or in the down ramp:
Here’s one way to do it, using some bit trickery:
A few notes:
- I’ve changed the “level” variable from an 8-bit byte to a 16-bit word
- bit 8 toggles from 0 to 1 and back every 256 level counts
- it’ll be 1 when level is 256..511, 768..1023, etc
- when it’s 1, we flip the bits, i.e. 0 becomes 255, 1 becomes 254, etc
- the analogWrite() function ignores all upper bits
If you think that was an obscure call to analogWrite(), try this one:
analogWrite(6, level ^ -((level >> 8) & 1));
Maybe you can decypher it when written slightly differently?
analogWrite(6, level ^ -bitRead(level, 8));
(hint: bitRead() always returns either 0 or 1)
It’s all pretty geeky stuff, and let’s hope you’ll never have to deal with code such as this again, but the point of this story is that there’s no magic. You just have to know what each operator does, and how to translate an integer from decimal to binary notation and back.
I’ll summarize my intuitive interpretation of bit operators below:
- “X | Y” = take X and copy all the 1’s of Y into it
- “X & Y” = take X and copy all the 0’s of Y into it
- “X ^ Y” = take X and flip all the bits where Y has 1’s
- “~ X” = flip all the bits of X
- “– X” = arithmetic minus (same as “(~X) + 1” !)
- “! X” = 1 if X is zero, 0 otherwise
- “X << N” = multiply X by 2, N times
- “X >> N” = divide X by 2, N times
Some tricks based on this:
- “~ 0” = all bits set to 1 (same as “-1” !)
- “~ 0 << N” = all bits 1, but N lowest bits set to 0
- “bit(N) – 1” = a constant with N lowest bits set to 1
- “X & (bit(N) – 1)” = the N lowest bits of X, the rest is 0
- “X & ~ (bit(N) – 1)” = X, but with the N lowest bits set to 0
- “!! X” = 0 if X is zero, 1 otherwise
An useful rule when writing logical expressions is: when in doubt, parenthesize! – see C operator precedence.
Sooo… use bit(), bitRead(), bitWrite(), bitSet(), and bitClear() wherever you can, since it usually makes the code easier to read. But there’s no need to get lost if you see ^&|~!’s in your expression – just slow down and decode such expressions step by step!