Another fun find while thrifting. This time, it's a little battery-operated fan that shows messages with lights.
So I got it, of course, mainly to see what makes it tick.
Disassembly
The back came off with a bit of prying. Three AAA batteries power a DC motor that spins the fan.
The front has a dome over the plastic fan blade, and then the fan blade holder holds the PCB.
The PCB has a little ribbon cable with 7 LEDs built into it. That ribbon slots into the fan blade.
The dome is press-fit over the fan blades and onto the fan blade holder, which contains the PCB. Then, that whole assembly is press-fit onto the motor shaft. The trick is that there's a spring coming up from base, and the spring is connected to the battery. As such, the DC power to the PCB is coming from the shaft and the spring.
PCB front
PCB back
There is a little slot in the PCB back where power gets interrupted now and then.
I think what's happening is that the DC power maintains a reasonable voltage in a capacitor somewhere and that fills up as the motor spins. Periodically, there's a pulse generated that might kill circuit power, were it not for the cap. The pulse that occurs is also input to the circuitry to know when the fan has hit a particular rotational point, so it has positional feedback.
The 24C08
The people who made this left me two good things.
First, the EPROM chip's label was left visible. In other toys, they sometimes bury these chips in a potting substance, or grind the identifying information off of the top layer of the chip.
That's a HK 24C08 EPROM. It can hold up to 8Kbits of memory, or 1 Kbyte. It's addressable by I2C. The pins on it are power, ground, SDA, SCL, Write Protect, and one meaningful Address line. There are two NC pins.
The address line allows you to have two of these EPROMs on the same I2C bus, if you want. In this case, the pin is connected to GND, so that makes it address 0.
Note that there appears to be a little capacitor stuck between pins 7 and 8 on the device. Pin 8 is Vcc, and pin 7 is /WC (Write Control). One 24C08 datasheet says, "Write operations are disabled to the entire memory array when write control (/WC) is driven high. Write operations are enabled when write control (/WC) is either driven low or left floating."
In this case, since there's a cap between Vcc and /WC, it would seem that it's "hard wired" as write-protected.
I2C SDA/SCL connection
The next nice thing they did was that they built the device with a usable Micro USB port, whose data lines connect directly to the SDA and SCL lines of the 24C08.
While the device had a Micro USB port, you can't plug it directly into a computer USB port and expect it to do anything. The USB interface doesn't work that way, i.e., it doesn't do direct I2C communications.
I took a Micro USB cable, chopped it, and separated the wires.
Once the wire ends were stripped, I just toned out the connections with a DMM, checking for a connection from each wire to a pin on the 24C08. In my case, the black and red connected properly to GND/Vss (4) and Vcc (8). The green wire turned out to be my SDA (data) line (5), and the white one connected to SCL (clock) (6).
Arduino
I fired up an old Kosmoduino board, which is an Arduino Nano, pre-wired to various controls and devices.
My initial interest was to use the Kosmos buttons. I found that pin 7 would give me the Button 1 signal, and pin 8 would be Button 2. I could check regularly in the main loop() function, and if either would go LOW, I could have the sketch do something in response, like read or write EPROM data.
The green board shown here is just an easy way for me to connect pins. There are screw-down terminals on the opposite side, so I didn't have to solder the USB cord's wires.
For the Arduino Nano, native I2C communications are done on pins A4 (SDA, yellow here connecting to green on the other end) and A5 (SCL, white here).
Arduino has an at24c08 library. I'm using the one from Stefan Stromberg. (This is one of many at24cxx libraries for eproms of different storage sizes.) Its basic examples yielded quick results.
You start by managing the sketch libraries and installing the AT24C library.
Then, in the sketch, you have to include the library
#include <at24c08.h>
and then you declare the eprom object and its address as a global.
AT24C08 eprom(AT24C08_ADDRESS_0);
From that point, you can simply call eprom.get() to read a byte from the device. This is much easier than trying to figure out the addressing patterns manually. If you read the datasheet for the 24C08, there's a little dance you have to do, sending a START, pretend-writing to and address, then sending another START, then reading a byte, then STOP. All of that rigamarole is handled for you in the eprom AT24C library.
Read operations
I made the main loop check button 1 for a LOW signal, and when that was seen, it would read.
Initially, I tried just reading a few specific bytes, but I progressed quickly to the point of having it report a page (256 bytes) of memory at a time in binary form (highest bit, mask 0x80, down to lowest bit, mask 0x01). I then adjusted the output to be more legible, using "#" for 1 values, and "." for 0 values.
After a few test iterations, a pattern emerged. As it turns out, if you read the values in descending address order, you can see the letters' pixels. Example shown here is the "REDUCE USE 5-8PM" message:
104 ####.### F7
103 ...#.... 10
102 ...#.... 10
101 ...#.... 10
100 ####.### F7
99 ####.### F7
98 #..#...# 91
97 #..#...# 91
96 #..#...# 91
95 #......# 81
94 ####.### F7
93 #....... 80
92 #....... 80
91 #....... 80
90 #....... 80
89 ####.### F7
88 ...#...# 11
87 ...#...# 11
86 ...#...# 11
85 .....##. 06 <- above this, message "SMUD CAN HELP", 13 chars
84 ...#...# 11 <- rendering code
83 .#.....# 41 <- line count
82 ######## FF
81 ...#...# 11
80 ..##...# 31
79 .#.#...# 51
78 #...###. 8E
77 ######## FF
76 #..#...# 91
75 #..#...# 91
74 #..#...# 91
73 #......# 81
72 ######## FF
71 #......# 81
70 #......# 81
69 .#....#. 42
68 ..####.. 3C
67 .####### 7F
66 #....... 80
65 #....... 80
64 #....... 80
63 .####### 7F
62 .######. 7E
61 #......# 81
60 #......# 81
59 #......# 81
58 .#....#. 42
57 ######## FF
56 #..#...# 91
55 #..#...# 91
54 #..#...# 91
53 #......# 81
52 ........ 00
51 ........ 00
50 ........ 00
49 ........ 00
48 ........ 00
47 .####### 7F
46 #....... 80
45 #....... 80
44 #....... 80
43 .####### 7F
42 #...###. 8E
41 #..#...# 91
40 #..#...# 91
39 #..#...# 91
38 .##....# 61
37 ######## FF
36 #..#...# 91
35 #..#...# 91
34 #..#...# 91
33 #......# 81
32 ........ 00
31 ........ 00
30 ........ 00
29 ........ 00
28 ........ 00
27 #..##### 9F
26 #..#...# 91
25 #..#...# 91
24 #..#...# 91
23 .##....# 61
22 ...#.... 10
21 ...#.... 10
20 ...#.... 10
19 ...#.... 10
18 ...#.... 10
17 .##.###. 6E
16 #..#...# 91
15 #..#...# 91
14 #..#...# 91
13 .##.###. 6E
12 ######## FF
11 ...#...# 11
10 ...#...# 11
9 ...#...# 11
8 ....###. 0E
7 ######## FF
6 ......#. 02
5 ...###.. 1C
4 ......#. 02
3 ######## FF <- above this, message "REDUCE USE 5-8PM", 16 chars
2 ...#...# 11 <- rendering code
1 .#.#.... 50 <- column count (80)
0 ......#. 02
Write operations
I made the sketch look for a button 2 LOW signal. When seen, it would to trigger a Write operation. I fully expected this to fail, because of the capacitor tying Vcc to /WC. But, for whatever reason, I was able to write to the EPROM without error! Maybe the cap has failed, or it wasn't installed correctly. I thought I would have to de-solder it or otherwise kill it, but I was able to write!
So, I started writing values to various lines to change the message and do other experiments.
Changing pixels was pretty easy.
Preamble byte 1: column count
Preceding each message phrase, there was additional information. For each, you would see two bytes. And, there was also a value in address 0 that didn't fit the pattern.
It turns out that for each message, the first byte is a "column count". Each letter is rendered as 5 columns of 7 pixels. So for a 16-char message, there are 80 "columns". Thus, for the message "REDUCE USE 5-8PM" in addresses 3..82 (80 bytes), the count in address 1 is 0x50 (80 decimal).
There's an upper limit to this value. You can't render more than 0x5a columns (90 decimal columns, 18 characters). If you try to set this value to something that is too large, or doesn't divide by 5 properly, the message rendering will fail overall.
Preamble byte 2: rendering code
The second byte is a rendering code. For example, Address 2 contained 00010001. In fact, for the fan I got, it used values 00010001 for all six of the message rendering codes. The default behavior was to draw the message gradually, left-to-right, wait, and then erase gradually, right-to-left.
With some experimentation, I found that the nibble codes for these rendering code bytes act as follows:
0000 LR RL (same behavior as 0001)
0001 LR RL
0010 RL LR
0011 OI IO
0100 IO OI
0101 C-LR LR-C
0110 R-LC C-LR
0111 Inst Inst
LR = left to right
RL = right to left
IO = inside to outside
OI = outside to inside
C-LR = Center to Left and Right
LR-C = Left and Right to Center
Inst = show the whole message in one instant without slowly drawing
A rendering code byte has two nibbles (high and low). As an example, if the high nibble is 0110, and the low nibble is 0011, it will draw the message from the Right and Left edges in to the Center, and then erase from the inner ring to the outer ring.
Message count
The only thing left to figure out was byte 0. It was set to 6 initially, and that simply meant "there are 6 messages to display".
7 bits, not 8
The fan has only 7 LEDs going around, not 8. That didn't really make sense, given the letter patterns in the EPROM made use of all eight bits. After some experimentation with bit values, I found that the bit at position 0x08 actually is ignored during rendering. For example, the value 0b11110111 renders the same as 0b11111111.
Below are examples where I've changed the word HELP to other values, just to test.
Easier control
I modified my Arduino sketch to use the Serial port as an input mechanism, rather than using the buttons.
The sketch reads text from Serial into a character buffer. As I do command text processing, I read from that buffer instead of reading directly from Serial.
There are two main operations: R and W. The commands are as follows:
Command: R0
Read page 0 (255..0) and report EPROM byte contents in the form address, binary value, hex value.
Command: R1
Same as R0, but page 1 (511..256)
The device does not appear to use pages 2 or 3 of the data for messages. Those pages can be read, and I'm not sure if they're important (e.g., operational code, or other configuration settings I don't know about).
Command: W decimalAddr binary7
This command lets you write a byte to a decimal address. If the binary value only has 7 digits, it's assumed to be part of a letter, and split accordingly, inserting a 0 in the 0x08 bit position. For example, if you say W 24 1010111, it will turn the binary value into 1010 0111 (0xA7) and write that to address 24. This 7-bit format makes is easier to visualize the character bits that will actually appear.
Command: W decimalAddr binary8
In the case where you have to write a full 8-bit value (e.g., rendering control codes, or column count information), you enter the binary value with all 8 bits, and it use them as provided without inserting the 0x08 bit.
For example, W 24 10100111 is equivalent to the 7-bit variant W 24 1010111.
Coding trickiness
A few notes on coding.
Debouncing
Since I started by using buttons for input, I added delay() calls as a cheap way to prevent re-reading the button too soon after I'd seen a LOW signal the first time.
Datatype: byte vs int
In early revs, I was calling eprom.put(addr,val) with an int datatype for val. This would write two bytes, not one. And, because I was writing bytes in reverse address order, it caused an overwrite of 0 in certain positions.
For example, if you use an int type and do:
then it would do something odd, like put 0 into positions 80 and 79, and 1 into 78, because it was actually writing two bytes each time.
The way to prevent the 0-overwrite behavior is to use the right data type, or do an explicit case, like this:
Serial read buffering
As with any character buffer management, be careful with buffer overflow possibilities. Far too many examples are provided out there, where someone in Arduino land provides an example of char buf[32] and then lets people write off the end of it. That will wreck up your sketch behavior, and it also teaches bad security coding practices to young programmers.
While none of my commands will expect more than 32 bytes, I keep a running count of characters read per line, and prevent buffering if the count goes too high.
Next Steps
- Come up with some clever messages of my own.
- Communicate with the Arduino Serial interface using something other than Serial Monitor, and pipe a whole set of messages (including message count at Addr 0, line counts, rendering codes, and message pixel columns) as a compiled bundle.
- Find more of these devices, because they're fun!
Final notes
As it turns out, I didn't really need to do any disassembly of the device. The Micro USB port is externally available, underneath the fan blade that doesn't have the LEDs. At this point, I can connect the fan to the Arduino (hacked USB cable), run Write and Read commands using Arduino's Serial Monitor, do Read operations to verify that the data got written, unplug, and test.
If, however, the /WC line were preventing me from proceeding, I would have had to de-solder the capacitor between pins 7 and 8. (For some devices, I may have also had to bodge wire pin 7 to GND/Vss pin 4 in order to write.
I have not found ways to do any of the following:
- Change the rendering speed
- Render around the entire circle
- Avoid the whitespace breaks between characters
- Render any other nice things found in other spinners (e.g., the date/time/temperature).
No comments:
Post a Comment