Close
0%
0%

Servo Breakout for WeMos D1 Mini

Connect servos to your ESP8266 dev board.

Similar projects worth following
Servo shields for the WeMos D1 Mini boards.

I thought I will try to make some shields for the WeMos D1 Mini, because it's one of my favorite ESP8266 boards. There is already an official motor controller shield (though I can't get it to work reliably), so I decided I will make some servo shields.

Buy it on Tindie.

wemos-servo8.zip

Gerbers for the 8-channel board.

Zip Archive - 170.95 kB - 08/23/2016 at 13:00

Download

wemos-servo8.fzz

Fritzing design of the 8-channel board.

x-fritzing-fzz - 31.74 kB - 08/23/2016 at 13:00

Download

  • Assembling and Testing

    Radomir Dopieralski04/04/2017 at 11:34 0 comments

    The 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.

  • Version 1.0 of the 16-channel Shield on Sale

    Radomir Dopieralski03/24/2017 at 17:48 0 comments

    After 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/

  • Usage Examples

    Radomir Dopieralski03/01/2017 at 15:43 0 comments

    So 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() {
    }
    

  • Selling Prototypes on Tindie

    Radomir Dopieralski02/25/2017 at 02:00 0 comments

    I 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.

  • PCA9685

    Radomir Dopieralski02/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.

  • 20ch Firmware Improvements

    Radomir Dopieralski01/20/2017 at 21:25 0 comments

    Today 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.

  • 20ch Servo Shield Assembled

    Radomir Dopieralski01/20/2017 at 10:13 0 comments

    It 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.

  • Firmware for the 20-servo Shield

    Radomir Dopieralski12/19/2016 at 09:16 0 comments

    It 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

    Read more »

  • Smaller PWM Code

    Radomir Dopieralski12/18/2016 at 21:31 0 comments

    I 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.

  • Firmware in Plain C

    Radomir Dopieralski12/13/2016 at 20:46 0 comments

    I 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...

    Read more »

View all 18 project logs

  • 1
  • 2

    Populate the PCB -- solder the ATmega328p, the reset resistor (10kΩ), the I2C pullup resistors (10kΩ, optional), capacitor (10-1000µF, optional). Solder the pin headers for the servos and power, and an extra pin or wire for the reset.

  • 3

    Connect your ISP programmer to the 3v3, GND, MISO, MOSI, SCK and Reset pins as follows:

    If you are making the 2ch version, the pin names are printed on the PCB. Get the source code from https://bitbucket.org/thesheep/d1mini-18ch-servo/src for the 18-channel version or https://bitbucket.org/thesheep/d1-mini-20ch-servo/src for the 20-channel version. To flash the 18-channel version, use Arduino IDE. To flash the 20-channel version, run "make flash" in the firmware directory.

View all 5 instructions

Enjoy this project?

Share

Discussions

Thomas wrote 01/21/2017 at 07:58 point

Maybe I've worked too long in the field of electrical reliability to put too much trust in the mechanical fitness of a two rows of 8 pin headers to support the mass of an assembly of a rather dense assembly [shield-PCB+48 pin headers+16 3pin connectors+attached cables]. It might be advisable to provide mounting options that take away mechanical strain and stress from the pin headers. Also such an assembly easily draws a lot of current, even if no servo is ever stalled (0.2A x 16). The requirements of an adequate power supply (with sufficiently designed ground) might also be a good thing to be getting aware of.

  Are you sure? yes | no

Radomir Dopieralski wrote 01/21/2017 at 09:24 point

There is a connector for a separate power supply for the servos, and 99% of the bottom layer is a ground fill. As for mechanical ruggedness, you can always put the shields on the bottom, and the D1 Mini (which is relatively light) on top. But the shield is quite small and feels pretty solid. I agree it would be nice to have mounting holes, unfortunately there isn't much room left for that.

  Are you sure? yes | no

Thomas wrote 01/21/2017 at 15:13 point

You've got a point. I'm just not a big fan of "shields"  for different reasons.

By the way, an ESP-14 can control up to 15 servos without external controller. This should work even if the integrated STM8S003F3 has a 3.3V power supply.

Edit1 in response to @Radomir Dopieralsk: http://www.watterott.com/index.php?page=product&info=4831&dl_media=6623&x0993b=33c134530245f3b2921d85e84fa5d152

Edit2 in response to @Radomir Dopieralsk: if you need the ESP8266 serial interface for controlling the MicroPython console you'd be down to controlling just 13 servos (maybe 14 with some tricks).

  Are you sure? yes | no

Radomir Dopieralski wrote 01/21/2017 at 15:25 point

ESP-14? Is that another module with the ESP8266 on it? How does it do without an external controller, if you can only use 11 gpio pins on the ESP8266, and that's if you re-use the serial pins... Do you have a link?

  Are you sure? yes | no

Radomir Dopieralski wrote 01/21/2017 at 15:50 point

Ah, I see, so you program the stm8 chip inside. That's nice, but I want to use the esp8266 to run micropython on it, stm8 doesn't work for me.

  Are you sure? yes | no

Radomir Dopieralski wrote 01/21/2017 at 16:21 point

Just reply to the message one level above...

Looking at the pinout of the esp14 module, I don't see any esp8266 pins broken out -- so I don't think you can program the esp8266 on it, only the stm8.

  Are you sure? yes | no

Thomas wrote 01/21/2017 at 18:26 point

The ESP8266 can be programmed the usual way through pins 10 and 11 when pin 14 is low. The intent of the hardware design is to use the ESP8266 UART for communication with the STM8S, but it's also possible to use a simple serial protocol to transfer the data for servo control from E_GPIO0 to any STM8S GPIO.

Another option would be to make the STM8S listen to ESP8266 TX and filter out servo set commands from the data stream. There are a few more options. and depending on on-line console communication requirements, trade-offs and time spent on optimizations, anywhere from 13 to 17 servos can be controlled without using port expanders (like shift registers).

Please refer to the discussion here for another take on using the ESP-14: https://hackaday.io/project/16097-eforth-for-cheap-stm8s-value-line-gadgets#j-discussions-title

Thanks for the message level hint :-)

  Are you sure? yes | no

Jonathan Beri wrote 01/20/2017 at 16:48 point

Are you thinking about a PCA9685-based shield as well?

  Are you sure? yes | no

Radomir Dopieralski wrote 01/20/2017 at 17:16 point

No, I used those with Feather HUZZAH, and I'm not impressed. They only have 16 channels, the I2C registers are a bit complex (enough to need a library for it) and they are actually for driving LEDs, not servos. With a simple atmega I have everything they would give me, plus 4 more channels, and it's cheaper.

  Are you sure? yes | no

Jonathan Beri wrote 01/20/2017 at 18:47 point

I've used it pretty happily in other servo projects but those are fair points. Are you able to achieve better resolution and/or faster frequency with the atmega? 

  Are you sure? yes | no

Radomir Dopieralski wrote 01/20/2017 at 18:53 point

The frequency I'm using is fixed at 50Hz, because that's what the servos want. I can do 1µs resolution at that frequency, which with my servos translates to 0.01° angle (except for a corner case that I just found where two servos are in really similar positions -- then it's more like 5µs).

Don't get me wrong, the PCA9685 is a great piece of silicon and I would use it -- if I didn't write the code to do it without it first. For Tote there is an extra advantage -- I only need one chip for both the robot behavior and the PWM -- though I will be moving away from that anyways.

  Are you sure? yes | no

esot.eric wrote 01/23/2017 at 01:22 point

so, bitbanged PWM on 20 channels with 1us resolution? and presumably serial of some sort? Impressive!

  Are you sure? yes | no

Radomir Dopieralski wrote 01/23/2017 at 11:16 point

Well... I found recently a bug where if two channels are too close to each other, one of them wouldn't work -- so I had to modify it, so that you only have 1µs resolution when all the channels are at least 10µs apart from each other. When two or more get too close, they get merged into the same signal. So in the worst case, where all the channels are within 10µs of the first one, you only get 10µs resolution.

The i2c communication happens without interrupts, so fortunately it doesn't affect the timers.

  Are you sure? yes | no

Radomir Dopieralski wrote 01/20/2017 at 18:54 point

By the way, making a PCA9685-based shield should be extremely straightforward -- it would basically be just all the connections.

  Are you sure? yes | no

Radomir Dopieralski wrote 01/20/2017 at 20:50 point

Hmm, it's even more straightforward than I thought:


  Are you sure? yes | no

Mike Causer wrote 10/19/2016 at 01:17 point

Time to put my stash of SG90s to good use.

  Are you sure? yes | no

Craig Hissett wrote 09/14/2016 at 17:00 point

I need at least two of these in my life!

Awesome work matey!

  Are you sure? yes | no

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates