-
Firmware for the 20-servo Shield
12/19/2016 at 09:16 • 0 commentsIt took some work, and it took some compromises. I ended up not using the first trick I mentioned, of sending the signals for each port separately. I also ended up needing sorting anyways, but did that with a horrible O(n²) insertion sort -- anything to save some bytes. But it's here and it works. I even managed to squeeze in an array for remapping the channel numbers to what is actually written on the shield. The whole thing has 990 bytes and you can see it below, or in this repository: https://bitbucket.org/thesheep/d1-mini-20ch-servo/src
#include <avr/io.h> #include <avr/interrupt.h> #include <inttypes.h> #define I2C_ADDRESS 0x10 #define I2C_BUFFER_SIZE 32 static unsigned char i2c_buffer[I2C_BUFFER_SIZE]; static unsigned char i2c_cursor; static union { unsigned char bytes[2]; unsigned int value; } bytes2int; #define MAX_SERVO 20 static uint16_t servo_pulse[MAX_SERVO]; volatile static uint16_t servo_delay[MAX_SERVO + 1]; volatile static uint8_t servo_mask_b[MAX_SERVO]; volatile static uint8_t servo_mask_c[MAX_SERVO]; volatile static uint8_t servo_mask_d[MAX_SERVO]; volatile static uint8_t servo_event; // Remap servo numbers to pins. // 0 = PB0 1 = PB1 2 = PB2 3 = PB3 4 = PB4 5 = PB5 6 = PB6 7 = PB7 // 8 = PC0 9 = PC1 10 = PC2 11 = PC3 12 = PD0 13 = PD1 14 = PD2 15 = PD3 // 16 = PD4 17 = PD5 18 = PD6 19 = PD7 const static uint8_t servo_map[MAX_SERVO] = { 13, 10, 11, 12, 14, 15, 16, 6, 7, 17, 9, 8, 5, 4, 3, 2, 1, 0, 19, 18 }; ISR(TIMER1_COMPA_vect) { servo_event = 0; PORTB = 0b11111111; PORTC = 0b00001111; // leave out RESET, SDA, SCL, PC7 PORTD = 0b11111111; TCNT1 = 0; OCR1B = servo_delay[0]; } ISR(TIMER1_COMPB_vect) { PORTB &= servo_mask_b[servo_event]; PORTC &= servo_mask_c[servo_event]; PORTD &= servo_mask_d[servo_event]; servo_event += 1; OCR1B = servo_delay[servo_event]; } void servo_update() { uint16_t last_pulse = 0; for (uint8_t event = 0; event < 8; ++event) { uint16_t smallest_pulse = 0xffff; for (uint8_t servo = 0; servo < MAX_SERVO; ++servo) { if (servo_pulse[servo] > last_pulse && servo_pulse[servo] < smallest_pulse) { smallest_pulse = servo_pulse[servo]; } } servo_delay[event] = smallest_pulse; servo_mask_b[event] = 0xff; for (uint8_t servo = 0; servo < 8; ++servo) { if (servo_pulse[servo] <= smallest_pulse) { servo_mask_b[event] &= ~(1 << servo); } } servo_mask_c[event] = 0xff; for (uint8_t servo = 0; servo < 4; ++servo) { if (servo_pulse[8 + servo] <= smallest_pulse) { servo_mask_c[event] &= ~(1 << servo); } } servo_mask_d[event] = 0xff; for (uint8_t servo = 0; servo < 8; ++servo) { if (servo_pulse[12 + servo] <= smallest_pulse) { servo_mask_d[event] &= ~(1 << servo); } } last_pulse = smallest_pulse; } } int main() { // setup i2c slave PORTC |= 1<<PC4 | 1<<PC5; // enable pullups TWAR = I2C_ADDRESS<<1; // setup servos TCCR1A = 0x00; TCCR1B &= ~(1<<CS12 | 1<<CS11 | 1<<CS10); TCCR1B = 0x02; // prescaler 8 OCR1A = 20000; // period TIMSK1 = 1<<OCIE1A | 1<<OCIE1B; DDRB = 0xff; DDRC = 0x0f; DDRD = 0xff; for (int i = 0; i < MAX_SERVO; ++i) { servo_pulse[i] = 0; servo_delay[i] = 0xffff; } servo_delay[MAX_SERVO] = 0xffff; servo_update(); sei(); TCCR1C |= 1<<FOC1A; // trigger COMPA // run i2c slave 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; } break; case 0xa0: // stop or repeated start received if (i2c_cursor > 0) { unsigned char i2c_servo = i2c_buffer[0]; for (unsigned char i = 1; i < i2c_cursor - 1; i += 2) { if (i2c_servo >= 20) { i2c_servo = 0; } bytes2int.bytes[0] = i2c_buffer[i]; bytes2int.bytes[1] = i2c_buffer[i + 1]; servo_pulse[servo_map[i2c_servo]] = bytes2int.value; i2c_servo += 1; } servo_update(); } break; } } }
And output from avr-size:
avr-size --format=avr --mcu=atmega328p main.elf AVR Memory Usage ---------------- Device: atmega328p Program: 990 bytes (3.0% Full) (.text + .data + .bootloader) Data: 198 bytes (9.7% Full) (.data + .bss + .noinit)
(Yes, the Data section doesn't count, I checked that only the Program is getting flashed.)Yes, the whole program is in one large file, and only divided into two functions. Why? To save the few bytes. I also fixed the prescaler to 8, so I get one tick per µs, which makes all the computations a bit easier. There is no address selection, because that shield does it on the analog-only pins, and the ADC-reading code would add much too much to the program size. You can change the address by re-flashing new firmware, of course.
I'm also wasting the RAM quite liberally whenever that lets me save a few bytes of code memory. I don't care, I'm only using 10% of the available amount anyways.
-
20ch Servo Shield Assembled
01/20/2017 at 10:13 • 0 commentsIt took some time, but the 20-channel version of the D1 Mini servo shield is finally assembled. I still used an ATmega328p, because that's what I have on hand (the code is now small enough to work on an ATmega8). As you can see on the picture below, the angled headers make it stick out of the D1 Mini outline a little bit -- but now you can really stack them! Once I update the firmware to take into account the address selection jumpers, you will be able to stack four of those babies, giving you a total of 80 servos to control. Of course powering that many servos from the poor D1 Mini is a bad idea (unless most of them only move sometimes), so you can also cut the trace connecting the 5V power with the D1 Mini pin, and provide the power separately.
Of course, as soon as I got the PCB in my hands, I immediately got some ideas on how to improve it: connect the reset pin to the D1 Mini's reset pin, for easier programming and to make them both reset together, make the D1 Mini outline on the top of the board smaller, so it's more readable, etc. -- but those are small things, and I'm mostly happy with this version.
-
20ch Firmware Improvements
01/20/2017 at 21:25 • 0 commentsToday I spent several hours working on the firmware -- adding the address selection support and making the whole thing much robust.
The first thing I added, was to modify place where the servo events are updated. Until now, they were updated immediately after the I2C transaction finished -- as soon as we had the servo pulses. But that meant that if you are unlucky, it could happen while the timer interrupt fires, and the data seen by the interrupt would be inconsistent. We can't switch off the interrupts while updating the events, because it takes very long time, and our timings would be completely off then. So what can be done? Well, all the events typically happen in the first 2ms of the cycle. We have 18ms left to update the events (that's also why the glitch wasn't so easily visible). So now, when the I2C transaction finishes, a flag is set signalling that an update is needed. Then, while waiting for more I2C data, the loop checks that flag and whether the last event in the cycle has been already processed. As soon as the last even is processed (which we can recognize by the fact that it has delay of 0xffff), we update the events and clear the flag. Sounds easy, but it took me some hours to get right.
Unfortunately, that didn't eliminate all the glitches. The second problem happens when you have two events very close to each other -- so close, that processing of the first one finishes after the second one's scheduled time. You could expect the second event to be a bit late then, but it's actually much worse -- since the interrupts fire on equality, and not whenever the counter overflows the trigger, the interrupt for the second event is skipped entirely and never fires -- the pin is only updated in the next event, or never, if that event was the last one. Ouch. I spent some time optimizing the interrupt routine and moving stuff in it to make it a bit more robust, but it's never processed instantly, and the problem remains. In the end, I'm just looking for events that happen too soon after the previous one, and when that happens, merge them with that previous event. It introduces a few microseconds of a glitch, but that's better than the alternative.
For the address selection, I had to use the analog-only pins of the ATmegaXX8. I was already a bit tired when I started on it, so I stupidly copied the code from my #Mechatronic Ears project, forgetting that it's for an ATtiny85. And it mostly worked -- the AVR chips are very similar, after all, especially when using the correct header files with the right macros -- except that I masked wrong bits in the mux selection. It took me sever tries to track that down, but once I found it the fix was trivial.
The next bug is related to the internal pullups I'm enabling on the I2C lines. Turns out I had the masks for the PWM routines slightly wrong, and they were also toggling the pullups. The effects on the I2C transmission were... interesting, if not a bit nondeterministic. It helped that I got a really nice logic analyzer for Christmas, that also has an analog channel, and that I could see the voltage levels on the I2C lines acting weird, even when there was no data being transmitted. Fixing this while keeping the interrupt routines as fast as possible took several tries, but I managed to move all the operations into the event update routines in the end.
The last thing I added is sending a stop condition on the I2C bus whenever something unexpected happens on it. That lets me recover from errors in communication more easily.
-
PCA9685
02/23/2017 at 15:56 • 1 comment@Jonathan Beri asked in the comments why I didn't use PCA9685, but instead went with an ATmega and my own code. To be honest, this is mostly because this project is the continuation of my #Servo Controller project, in which I used a Pro Mini board.
But that got me thinking, and I went and checked if I can get some of those chips cheaply, and I went ahead and designed a PCB for them:
I was actually so happy with it, that I went ahead and ordered them from OSHPark. Today the boards arrived, and I assembled one and tested it:
It works perfectly fine. You only get 16 channels (that's why I could fit it on the standard D1 Mini shield), but you can stack 64 of those boards (the address selection jumpers are on the bottom). Pretty neat. -
Selling Prototypes on Tindie
02/25/2017 at 02:00 • 0 commentsI decided to give Tindie a try and sell the prototypes of servo shields that I've built. I have three of each of the 16-channel and 20-channel ones. You can get them here: https://www.tindie.com/stores/deshipu/
I'm basically just getting rid of them, since the project is complete and I don't really need so many servo controllers -- there are only so many robots I can build at once. On the other hand, it would be a waste to just throw them away or let them rot in my drawer -- so I'm selling them.
While they are all manually tested, keep in mind that they are still prototypes -- purple PCBs, hand soldering, possible hidden bugs -- I will support them the best that I can, but I can't replace them (because I only have 3 of each and I'm selling them all) if they stop working. I can fix bugs in the firmware for the 20-channel shield, but then you will need an ISP programmer (or any Arduino, in fact) to re-program them -- I can help with that, but it does take some time and effort to setup. The 16-channel servo uses an off-the-shelf chip, so there are not likely to be any bugs.
This is also an experiment in gauging the interest in this. If they get added to a lot of wishlists, I might consider making a larger batch of them.
-
Usage Examples
03/01/2017 at 15:43 • 0 commentsSo here is example code you can use to communicate with the 18-channel and 20-channel servo controllers, for different environments that you can run on the ESP8266:
MicroPython
import ustruct from machine import I2C, Pin i2c = I2C(scl=Pin(5), sda=Pin(4)) address = 0x10 servo = 4 position = 1500 i2c.writeto_mem(address, servo, ustruct.pack("<H", position))
NodeMCU
address = 0x10 servo = 4 position = 1500 sda = 2 scl = 1 i2c.setup(0, sda, scl, i2c.SLOW) i2c.start(0) i2c.address(0, address, i2c.TRANSMITTER) i2c.write(0, servo) i2c.write(0, bit.band(position, 0xff)) i2c.write(0, bit.rshift(position, 8)) i2c.stop()
Arduino
#include <Wire.h> void setup() { int address = 0x10; int servo = 4; int position = 1500; Wire.begin(); Wire.beginTransmission(address); Wire.write(servo); Wire.write(position & 0xff); Wire.write(position >> 8); Wire.endTransmission(); } void loop() { }
-
Version 1.0 of the 16-channel Shield on Sale
03/24/2017 at 17:48 • 0 commentsAfter the prototypes that I put on sale on Tindie sold within 2 days, I decided to try and make a larger number of the servo controller shields. I choose to do it with the 16-channel PCA9685 shields, because to be honest I don't trust my own C code enough to inflict it on unsuspecting people. In any case, you can now buy the production version of this on Tindie here:
https://www.tindie.com/products/deshipu/16-channel-servo-shield-for-d1-mini-version-10/
I ordered about 90 PCBs (the Elecrow's 10x10cm offer is the best bang for the buck), but so far I only had components to assemble 5 of them. More components ordered, and I will put them in stock as they arrive. Here's a photo of the ready PCBs:
I also wrote proper documentation manual for this board, it's available here: http://16-channel-servo-shield-for-d1-mini.readthedocs.io/
-
Assembling and Testing
04/04/2017 at 11:34 • 0 commentsThe package with the chips finally arrived, so I can assemble the remaining boards. Soldering the TSSOP packages by hand is not terribly difficult, but it can still be tricky. So I'm not trusting myself, and apart from visual inspection I decided to also do at least basic testing of the shields. On the other hand, I have to ship them with the headers unsoldered, so I can't simply plug them in and see if they work. Fortunately, I have some of those testing probes lying around:
With that, I can easily check that the devices show up in an i2c scan, and that they can wiggle the servo. It''s not a full test -- that would have taken way too much time -- but it does cover the most common failure modes. Since I'm only making a few dozens of them, and I can assemble them as they are being ordered, I don't think I need to make a more sophisticated testing rig just yet.