Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.
Category: Software
General-purpose computers doing something specific
The TLC5916 data sheet clearly shows that you write the configuration code (which controls the LED current) by shifting seven bits in, then raising LE during the 8th SCK pulse while simultaneously shifting the 8th bit.
That makes no sense whatsoever: you couldn’t use standard SPI hardware in a chained configuration, because you’d have to blip LE while shifting.
In fact, the chip doesn’t work that way. You set the config code in Special Mode just like you set the LED driver bits in Normal Mode: shift ’em all in, then blip LE to latch ’em into the parallel holding register.
Here’s the code to make it happen…
DisableSPI(); // manual SPI control
digitalWrite(PIN_DISABLE_DISPLAY,HIGH); // initial condition
digitalWrite(PIN_LATCH_DO,LOW);
PulsePin(PIN_SCK); // 1
digitalWrite(PIN_DISABLE_DISPLAY,LOW);
PulsePin(PIN_SCK); // 2
digitalWrite(PIN_DISABLE_DISPLAY,HIGH);
PulsePin(PIN_SCK); // 3
digitalWrite(PIN_LATCH_DO,HIGH); // sets Special Mode
PulsePin(PIN_SCK); // 4
digitalWrite(PIN_LATCH_DO,LOW);
PulsePin(PIN_SCK); // 5
//-- Send brightness level
EnableSPI(); // turn on SPI hardware
SendRecSPI(Brightness);
SendRecSPI(Brightness);
SendRecSPI(Brightness);
SendRecSPI(Brightness);
SendRecSPI(Brightness);
PulsePin(PIN_LATCH_DO); // latch new shift reg contents into drivers
//-- put LED drivers back in Normal Mode
DisableSPI();
digitalWrite(PIN_DISABLE_DISPLAY,HIGH); // initial condition
digitalWrite(PIN_LATCH_DO,LOW);
PulsePin(PIN_SCK); // 1
digitalWrite(PIN_DISABLE_DISPLAY,LOW);
PulsePin(PIN_SCK); // 2
digitalWrite(PIN_DISABLE_DISPLAY,HIGH);
PulsePin(PIN_SCK); // 3
digitalWrite(PIN_LATCH_DO,LOW); // sets Normal Mode
PulsePin(PIN_SCK); // 4
digitalWrite(PIN_LATCH_DO,LOW);
PulsePin(PIN_SCK); // 5
digitalWrite(PIN_DISABLE_DISPLAY,LOW); // turn the LEDs on again
The SendRecSPI() function does exactly what you’d expect:
byte SendRecSPI(byte Dbyte) { // send one byte, get another in exchange
SPDR = Dbyte; // assume it's OK to send a new byte
while (! (SPSR & (1 << SPIF))) { // wait for shift to finish
continue;
}
return SPDR; // SPIF will be cleared
}
I don’t know. Maybe the chip also works the way they show in the datasheet, but I doubt it’s worth finding out.
I’m using Texas Instruments TLC5916 constant-current LED driver chips for my my friend’s Totally Featureless Clock. An 8-bit configuration value sets the output current, with the external resistor defining the maximum value as described there.
The problem is that the current-versus-config-value curve has a non-monotonic discontinuity in the middle, where the Current Multiplier bit switches from 0 to 1. I don’t know in which alternate universe this design decision made sense, but right here and now…
It. Does. Not.
Why it’s a problem: the LED brightness tracks room illumination as seen by a CdS photoresistor, averaged over maybe half a minute. The brightness changes very slowly, so the jump when it goes from 0x7f to 0x80 is really eyecatching. At least to me, anyway.
Eyeballometric measurement of the curve shows the current at 0x80 pretty much matches the current at 0x60, soooo let’s just shove the second quarter of the curve (between 0x40 and 0x7f) downward until it meets the high-current value at 0x80.
I’m using hardware-assisted SPI for a project, copied in my own boilerplate code, assigned the bits, and… it didn’t work.
Jammed hard with mysterious symptoms. Looked like a stack crash, looked like the hardware was broken, looked like a lot of things.
The final hint, found by stuffing Serial.print() statements in all the usual spots, was that the SPCR register mysteriously changed from the desired 0x71 to 0x61, without any of my code doing the writing.
Turns out that the Fine Manual has this to say:
Bit 4 – MSTR: Master/Slave Select
[snippage] If -SS is configured as an input and is driven low while MSTR is set, MSTR will be cleared, and SPIF in SPSR will become set. The user will then have to set MSTR to re-enable SPI Master mode.
I planned to use Arduino Pin 10 (PWM10) as the signal to latch the output shift registers, but because I’m developing the code on an Arduino Pro before making the circuit board, I hadn’t gotten around to initializing that pin… which, as you might expect, is also the -SS pin.
With the pin not set up as an output, it defaults to an input. My cut-n-paste code blipped the pin high and left it low to simulate latching the ‘595 shift registers… but, for input pins, writing a value simulates what would happen when an external signal drives the pin.
Soooo, I had inadvertently set -SS low, which turned off Master mode, which meant the hardware wasn’t going to send the next byte, which means SPIF wasn’t going to automatically go high when I dropped a byte in SPDR. The code, of course, waited until SPIF was clear before loading SPDR, then hung waiting for it to go high again.
As always, stupid errors are easy to fix after figuring things out, but ouch did it take a while…
Moral of the story: always initialize all the I/O pins! (But you knew that, right?)
The general idea is a simple light sensor that can cope with typical indoor illumination, brightening and dimming the digits on a clock so that it’s both visible in the daylight and doesn’t light up the room at night.
This circuit produces a voltage that varies more-or-less inversely with the photoresistance R. The “decade resistor” DR acts as a range selector: the output voltage will be 2.5 V when DR = R.
Yesterday I doodled about the voltage-vs-resistance curves and how DR works, showing the equations that spit out V when you know R, which is handy from a circuit-analysis standpoint (if you can say that with a straight face for a two-resistor circuit).
What you want is the equation that spits out R when you know V, because you can actually measure V. Rearranging the equation in the doodle above produces that equation
R = DR * (5 – V) / V
Actually, what you really want is log R, because your sensation of brightness varies logarithmically with the illumination intensity: each doubling of intensity makes the scenery twice as bright. So you could find R, apply a floating-point math package to it, and come up with log R.
There’s a better way.
CdS Resistance vs Voltage
Stand yesterday’s graph on its ear and flip it side-to-side to get this view of the same data points. It’s plotted with a log scale on the Y axis, because the resistance varies over such a huge range.
The dots represent the values of R produced by the equation above with DR = 1 kΩ. Those are the actual resistance values, at least according to the circuit model.
The midsection of those dots is pretty straight, so the diagonal line second from the bottom is a straight line “curve fit” with intercepts at
(0 V, log 10000)
and
(5 V, log 100)
The y = mx + b equation fitting that line is
log R = (-2/5) * V + 4
where the (-2/5) comes from the slope of the line:
(log 10000 – log 100) / (0 – 5)
and the (+ 4) comes from the intercept at (log 10000), as set by the value of DR. In fact, the intercept is 1+ (log DR), because it’s always a factor of 10 higher than the value of DR.
Now, what’s nice about that is the equation spits out log R directly, with just some multiply-divide-add action.
Changing DR by factors of 10 produces the other lines, so (as before) switching DR gives you a low-budget, wide-dynamic-range output.
DR need not be a power of 10, of course. The dashed line near the middle is DR = 3 kΩ, which puts the more-or-less linear region between about 20 kΩ (fairly dim) and 500 Ω (rather bright). That’s a useful range in my house and it might be close enough for my friend.
The dashed line approximating those points has an intercept at 1 + log 3000 = 4.5, so the overall equation is
log R = (-2/5) * V + 4.5
The approximation gets progressively worse below, say, V = 0.5 and above V = 4.5, so the outline of the algorithm is:
V < 0.5 = pretty dark: lowest digit intensity
V between 0.5 and 4.5: puzzle over log R
V > 4.5 = lots o’ light: max digit intensity
The Arduino ADC produces a 10-bit integer: 0 through 1023. Call that Vb (“V binary”), which you can scale to V like this
V = (5/1024) * Vb
Plugging that into the equation produces
log R = (-2/5) * (5/1024) * Vb + 4.5
log R = (-2/1024) * Vb + 4.5
The useful limits on Vb for the linear approximation are
V = 0.5 -> Vb = 1024 * 0.5/5 = 102
V = 4.5 -> Vb = 1024 * 4.5/5 = 922
Checking those limits against the actual formula for R
Vb = 102 -> log R = 4.3 (instead of 4.43)
Vb = 922 -> log R = 2.7 (instead of 2.52)
That’s about what you’d expect from the graph: the line is lower than the dots on the dim end (left) and higher on the bright end (right). On the other paw, getting log R without the floating-point math package makes up for that.
Now, given that you’re going to use a table lookup anyway, you don’t need any arithmetic on Vb at all. Shove all the stuff surrounding Vb to the other side of the equation
(log R – 4.5) * (-1024 / 2) = Vb
(4.5 – log R) * 512 = Vb
Precompute the left side for useful values of R, fill in the corresponding bit pattern to get the desired brightness, then index into the table with the measured Vb: shazam, log R -> brightness bits in one step!
If you’re exceedingly lucky, the brightness bits will be more-or-less in a binary sequence, in which case you can just right-shift Vb to get that number of bits and send ’em directly to the LED drivers. No table needed: one shift and you’re done!
So, for example, suppose you want eight brightness levels controlled by three Brightness Bits BB
BB = Vb >> 7
What’s not to like?
Maybe you already knew that and were wondering why it took me so long to get there…
[Update: Remember, that struct is bass-ackwards. You want the most-significant fields on the bottom, rather than the top, so the bits fill them in properly. This is how I found out that’s true…]
Coerce the bits into an unnatural union with an unsigned long int and create a variable:
union WWVB_code_ {
uint32_t WWVB_ul;
struct WWVB_bits_ WWVB_bits;
};
union WWVB_code_ ReceivedBits;
Then set a few bits to find out how the compiler arranges things:
Soooo, rats, the DST bits are on the left and the Minute bits are on the right. That doesn’t tell you how they’re actually laid out in memory, but if you’re doing the interrupt handler in C, then you just stuff the incoming MSB-first bits from the radio directly into the right side of the int and let them slide leftward.
If you count ’em up, you’ll find that the commented-out bits allow the remainder to fit into an unsigned long int, which is all of 32 bits on the Arduino. You can actually use an unsigned long long int to get 64 bits, but it seems Arduino bit fields can’t extend beyond 32 bits.
There are ways around that, particularly seeing as how I’m using a simpleminded interpreter to parse the incoming bits, but I’ll doodle about that later.
Insert the usual caveats about portability and interoperability and maintainability and … well, you know why bits fields are such a bad idea.
The pgm_read_whatever() functions/macros require the address of the variable stored in PROGMEM (the program Flash ROM), not the variable’s value. More on that in the Arduino doc there.
So, for example, this does not work, no matter how easy it might be to type:
I just figured out how to use the WordPress “sourcecode” formatting and applied it to my software-related posts. It produces much nicer results than the manual formatting I was using, mostly by preventing long lines from jamming into the right column.
The catch: WordPress imposes a round trip from my original text to the screen encoding and back, which sometimes randomly mangles special symbols. Angle brackets and double-quotes, in particular, take serious damage.
If you happen to remember a favorite chunk of code in a previous post, please take a look at it and see if I missed any of the obvious text-replacement errors. Trawling through the Software category should turn up most of the posts.
As is always the case with program listings, the errors will be really obvious to everyone except me.