-
Dumb 8-channel Servo Breakout
09/14/2016 at 12:11 • 0 commentsThe first one is only a servo breakout -- it doesn't have any components on it other than the pin headers. It lets you connect servos to all 8 available gpio pins (leaving only rx and tx), so that you can build a walking robot like #Katka.
To save space, the servos use angled headers. You can power them separately, or (after closing the juper on the back) from the 5V pin of the board.
There is a link to the OSHPark order page, and the design files and gerbers are uploaded in the files section.
-
18-channel Servo Shield
09/14/2016 at 12:21 • 0 commentsThe second approach is to actually use an ATmega328p chip communicating over I2C with the ESP8266, to generate PWM for 18 different servos. I had the code already from my #Servo Controller, so all I needed was a PCB for it. I considered using a Pro Mini here too, but finally decided that a bare atmega is enough -- I don't need an external oscilator, because neither the servo signal nor I2C are sensitive to timing differences.
So I received the PCBs from OSHPark, and assembled the shield. And of course I had a short between the 3.3V pin and the ground. One of the vias on my design got moved by mistake, and touched the ground fill. Fortunately that was easily fixed with a knife.
Then I connected my USBASP programmer, and with great help from @Christoph flashed the blink sketch, set the fuses to the right values, tested it, and then flashed my program.
First tests show that it works, except that for some reason I need to lower the frequency of the I2C bus to 150kHz from the default 400kHz, otherwise the slave gets stuck with its SDA pulled down as soon as it receives the second byte of transmission. I will poke at it some more and hopefully figure out what's wrong there.
-
Shield Anatomy
09/15/2016 at 19:20 • 0 commentsSince this board is so packed (18 servo sockets take a lot of space), I couldn't fit all the labels on it. So a bit of a legend is necessary:
The MISO, MOSI, SCK, RST, 3V3 and GND pins are needed for programming the chip after it's assembled. I'm just connecting the USBASP programmer to them and use the Arduino IDE with the Lilypad board selected. Then you also need to set the fuses with Avrdude:
avrdude -c usbasp -p m328p -U lfuse:w:0xE2:m -U hfuse:w:0xDF:m
to switch it from 1Mhz to 8Mhz. That's it, you can disconnect the programmer and use your new shield. Oh, before you program it, remember to add the resistor for the RST pullup on the back of the board. 10kΩ should be fine.You have two options for powering the servos. You can either connect the servo power pin to the 5V pin next to it, and power them from the same source as your D1 Mini, or you can use a separate power.
The order of the servo sockets on the board is pretty much random -- whatever was easiest to route. I suppose I could fix it in software, but for now I didn't even figure out yet which socket is which.
Ah, I also figured out why it didn't work with larger frequencies of i2c bus with esp8266 -- the fault is on the side of the esp8266 micropython, and its lack of support for clock stretching. A pull request is submitted to fix it.
-
Released
10/09/2016 at 12:28 • 0 commentsI decided to make a formal release of the 18-channel servo shield.
I fixed all the problems I could find (including the short between power and ground), updated the firmware to have the servo sockets numbered in a sane order, and added jumpers on the bottom for changing the I²C address (so that up to 4 shields can be used at once without having to modify the firmware).
I also wrote simple libraries for MicroPython, NodeMCU and Arduino, with some examples.
I'm releasing the whole thing under a Creative Commons non-commercial license, because that's the license used by the PWM library that is included in there. I would love to release it under a license that allows commercial use (and to be able to order ready shields from China), however, that would require me to rewrite the firmware to not use that library (or get the original authors to change the license). Too bad I'm too lazy to do that.
-
Another Short
11/11/2016 at 11:50 • 0 commentsReleasing the design before actually receiving the boards and testing them turned out to be a mistake. Out of three boards that I got from OSHPark, two have a short between the servo power and ground. The third one seems fine.
So I opened the designs and looked through them carefully trace by trace -- but no short there. I opened the gerber files and looked through that -- no short either. The fact that two boards have a short but the third doesn't, suggests that there is some problem with some traces being too close to each other.
So I took a knife, and started cutting the traces on the board, trying to locate the short. I narrowed it down to the lower right quarter of the board -- specifically, to the ground fill on the bottom, between the servo socket pins.
Oops, seems like routing a "thin" trace between two 0.4mm pads doesn't leave enough room? This is a bit strange, because it always worked for me before -- perhaps I was just lucky.
Anyways, I went through the design and changed all the pads to 0.35mm, and also made some other traces passing through narrow spaces thinner. I also deleted the old designs from OSHPark, and added this new, corrected one. Ordered it, and we will see how that one works.
-
20-channel Servo Shield
11/30/2016 at 13:05 • 1 commentYes, I keep refining the design. No, I don't actually need all those servo shields. But it's a fun challenge. So I made another one, this time controlling up to 20 servos at once.
Note how all the servo sockets are on the edges. That means that if you use angled 3×10 male headers, you can actually stack those. And with the address selection on the analog pins, you can have up to 4 shields at once. That's 80 servos. Of course, if you just reprogram them to use a different address, you could have hundreds of them.
So where are the two extra servo channels coming from? Turns out that if your ATmega328p uses an internal oscilator and you don't have an external oscilator connected, you can use those pins as gpios.
I'm still conflicted about whether to actually order this board or not. One idea is to order it, rewrite the firmware in plain C, and submit this to the 1kB contest...
-
One-sided PCB
11/30/2016 at 14:15 • 0 commentsLooking at the design from the last log, I thought "Hmm, if only I had some more room on the sides for the ground and power lines, I could make this a single-sided PCB". Turns out I was wrong.
This is my best attempt at this. See that highlighted SDA line? If only it wasn't there, or I could fit it somehow next to the SCL line, everything would have been just fine. The track next to it would take it place, and the power rail could get a connection on top, where that track is now. Alas, there is no way to do that without a jumper cable.
Oh well, a jumper can't hurt, can it?
-
Pimped 20-channel PCB
11/30/2016 at 19:23 • 0 commentsOK, the one-sided board experiment was fun, but not very practical. Especially since all the fab houses out there make two-sided boards by default, so I don't save anything. So I went back to the two-sided board, applied some of the improvements I discovered while working on the one-sided one, cleaned up the traces a bit, added a custom PCB outline with rounded edges and an outline of the D1 Mini on the bottom silkscreen, and I think it's ready for ordering. But I will wait one more day, maybe I will spot some more things.
-
Firmware in Plain C
12/13/2016 at 20:46 • 0 commentsI thought that maybe I would be able to get the servo controller code below 1kB, and enter this project into the contest. And even if I don't, hey, it's a great opportunity to learn about AVR's TWI peripheral, so why not. Thus, for the last week I had the datasheet for ATmega328p and the application notes for TWI open on one of my workspaces all the time -- but apart from an occasional glance, I didn't do much with it. Finally, Sunday came and I felt motivated enough to begin.
The PWM code, which I stole from the #Stubby the (Teaching) Hexapod project initially, is written in C and assembly, so no changes were needed there. I just needed the I²C slave code. In addition, since the PWM is interrupt-driven, it would be best to not use interrupts in the communication code -- this way it won't interfere with PWM timings.
Turns out you can do that quite easily. You just keep checking the TWINT flag in a tight loop, and when it gets set, you do what you would do in an interrupt normally, then just clear the flag and resume. The theory is quite simple, and the slave code is actually much simpler than the master code (especially since I'm only handling writes, and don't care about reads). It still took me two days to get it working -- somehow I couldn't get the ATmega to even ACK its own slave address...
Frustrated, I swapped the D1 Mini board that I was using to test the I²C communication (MicroPython is super-convenient for that, as you can just type stuff at the console and have it execute live), for a Adafruit HUZZAH Feather, changed the pins from GPIO4 and GPIO5 to GPIO0 and GPIO2 (because they have pullups already), and... it still didn't work.
When I shared my frustration at the hacker channel, Christoph suggested I try with a slower clock. So I lowered the clock frequency from 400kHz to 100kHz, and it magically worked. That gave me an idea -- maybe the pullups are too weak for the faster speed? So I tried enabling the internal pullups on the ATmega (I know, I know), and it works at 400kHz now too.
Here's the code, if you are interested:
#define PWM_MAX_PINS 12 #define PWM_PERIOD 20000L // 50Hz #include "pwm.h" #include <avr/io.h> #define I2C_ADDRESS 0x10 #define I2C_BUFFER_SIZE 32 volatile unsigned char *ports[PWM_MAX_PINS] = { &PORTD, &PORTD, &PORTD, &PORTD, &PORTD, &PORTD, &PORTD, &PORTD, &PORTB, &PORTB, &PORTB, &PORTB, &PORTB, &PORTB, &PORTC, &PORTC, &PORTC, &PORTC, }; unsigned char pins[PWM_MAX_PINS] = { 1, 0, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 0, 1, 2, 3, }; int main(void) { unsigned char i2c_buffer[I2C_BUFFER_SIZE]; unsigned char i2c_cursor = 0; unsigned char servo; union { unsigned char bytes[2]; unsigned int value; } bytes2int; pwm_init(ports, pins, PWM_MAX_PINS, PWM_PERIOD); for (unsigned char i = 0; i < PWM_MAX_PINS; ++i) { pwm_set_phase_batch(i, 0); } pwm_apply_batch(); PORTC |= 1<<PC4 | 1<<PC5; // enable pullups TWAR = I2C_ADDRESS<<1; while (1) { TWCR = 1<<TWEN | 1<<TWINT | 1<<TWEA; while (!(TWCR & (1<<TWINT))) {} switch (TWSR & 0xF8) { case 0x60: // received address for write and acked i2c_cursor = 0; break; case 0x80: // received data and acked case 0x88: // received data and nacked if (i2c_cursor < I2C_BUFFER_SIZE) { i2c_buffer[i2c_cursor++] = TWDR; } case 0xa0: // stop or repeated start received if (i2c_cursor > 0) { servo = i2c_buffer[0]; for (unsigned char i = 1; i < i2c_cursor - 1; i += 2) { bytes2int.bytes[0] = i2c_buffer[i]; bytes2int.bytes[1] = i2c_buffer[i + 1]; pwm_set_phase_batch(servo, bytes2int.value); ++servo; if (servo >= PWM_MAX_PINS) { servo = 0; } } pwm_apply_batch(); } break; } } }
However, it turned out that all my effort is for nothing. While my I²C code is only about 250 bytes (already a quarter of the limit, if you think about it), the PWM code I'm using totals almost 3kB. I should have checked that before I started.So no contest for this particular project, at least for now -- unless I manage to write my own servo PWM code that is more optimized for size, which is always an option -- I could then even drop the non-commercial license, and let the China churn out some cheap servo controllers... We will see, for now I switched my attention to other projects.
-
Smaller PWM Code
12/18/2016 at 21:31 • 0 commentsI have been thinking about rewriting the PWM code in order to drop the inconvenient license, and possibly also bring the code under the 1kB mark, to enter it into the contest. Those are some notes from my ideas.
There are three tricks that I think could let me get this to work:
The first trick is that the servo signals don't need to agree in phase -- each of the signals is independent, so I'm completely free to decide when to start each of them. The only things that matter is the pulse width and the period of the whole signal. That means that I can generate the signals for the pins on each of the 3 ports separately -- first handle the B pins, then the C pins, and then the D pins. I would still have more than enough time for two more ports, if I needed them.
The second trick is to use a timer interrupt, but set the trigger to point to the next moment in time when one or more pins need to be set low, and then move that trigger to the next such moment.
The third trick is to do most of the processing outside of the interrupt, when a servo's position is changed -- calculate the port states and delays. The interrupt should only set the registers to values it takes from arrays.
I think that this approach is simple enough to have small code (for instance it has no sorting), but fast enough to be rather accurate (the delays will probably need an adjustment for the interrupt execution time). Together with a no-interrupt i2c slave, I hope to get some pretty stable signals.