• PCB Day Best Day

    Jarrett02/08/2018 at 16:29 0 comments

    Have not finished populating the board or doing any tests yet. The board looks good, though! Final soldermask colour will probably be black, but this particular boardhouse wanted extra for that, and I expect to have to respin this, so I want the cheapest price possible.

  • Holding and Shipping

    Jarrett01/11/2018 at 19:14 0 comments

    Lack of updates hasn't meant lack of progress here. Since I last wrote, two things have happened:

    I've built a mock-up for one potential solution to my battery holder problem.

    First, I laser-cut a shape into some acrylic:

    Then, I cut and sanded an angle into an acrylic tube:

    It seems to slide onto the cutout pretty easily, and then rotating the tube so that the long end is wedged into the side cuts, and locks into place.

    It seems to work decently well. The tube has an inner diametre of 12mm, just over the LR44 battery's 11.7mm, so they fit with only a little bit of rattle.

    The other thing I've done is sent off a PCB for manufacturing. Not actually OSHPark, unfortunately, so I don't expect to see it until the end of the month at the earliest.

  • Measured frequency

    Jarrett12/29/2017 at 19:58 3 comments

    Revisiting this project log, I had some ballpark guesstimates on timing, and it wasn't looking as good as I'd hoped. So I attacked the problem again, this time in the lab.

    First of all, I simplified the main loop of code to this:

                LATAbits.LATA5 = 1;
                for(i = 0; i < VPIXELS; i++) {
                    setChannel(blob, i, blueMap[i]);
                }
                LEDMap(blob);
                LATAbits.LATA5 = 0;
                for(i = 0; i < 2; i++) {
                    __delay_ms(10);
                }

     That sets port A5 high while we're actually doing work and gives me something to trigger on.

    Remember that setChannel is there to process data from storage into an array of the format that the driver chip takes, and LEDMap sends the data to the LED driver over SPI.

    I'm expecting setChannel to take a long time, and LEDMap to be fairly quick, given that SPI is done in hardware at clockspeed/4.

    Here's what the whole loop looks like (yellow is PA5, blue is SDO/ SPI data):

    That doesn't look right! Why is SDO all the way to the left?

    We go to LEDMap, where that data is sent:

    void LEDMap(uint8_t *blob)
    {
        uint8_t data = 0;
        
        XLAT = 0;
        
        for(int i = TABLESIZE - 1; i >= 0; i--) {
            data = *(blob + i);
            SPIWrite(data);
        }
        
        XLAT = 1;
        __delay_ms(10);
        XLAT = 0;
    }
    

     Oh yeah, that makes sense. __delay_ms(10) is a little excessive. I meant to tune that down as far as the driver would allow, but it slipped my mind completely.

    Putting the scope on XLAT proves that nicely:

    So two things come out of this that are very interesting. The more important takeaway is that, if the XLAT timing is reduced to almost nothing, each loop of main() should take around 750us.

    The other is that a 10ms delay is nowhere near 10ms. Close to 40ms, in fact. Why is that?

    The clue comes from the poor Microchip documentation for built in functions. Historically, before their (pretty excellent) redo of their framework, there was an important constant called _XTAL_FREQ.

    Some built-in functions, such as the delay functions, required it to know what speed the crystal was running at, and generate accurate(ish) timings.

    With the framework redesign, as far as I can tell, those delay functions are still the recommended method. You need to specify _XTAL_FREQ yourself, however.


    In my system.h file, I have written:

    #define SYS_FREQ        12000000L
    #define FCY             SYS_FREQ/4
    #define _XTAL_FREQ SYS_FREQ

    Hmmm. So, future note: _XTAL_FREQ must equal FCY, not SYS_FREQ.

    Okay, back to the task at hand. Or at least, some fun facts,

    Each byte is taking around 5us to send.

    The whole block to load up the LED driver with data is about 200us to send.

    Okay, so at a 750us period, I can refresh one LED driver 1333 times per second. The final board has two drivers, so I'm looking at 666Hz.

    Ideally, as the globe spins, I'd have a horizontal resolution that is equal to my vertical resolution. I have 20 LEDs going up each edge, so multiply that by Pi, and I require 63-ish refreshes required for each revolution of the disc.

    Treating it like a movie screen, that's a little over 10FPS at each physical point on the globe. That's not really enough for animation, but for a static light/dark image, I think it's right on the edge of being okay.

    I'm not certain I can get my motor spinning that slowly, however.

    But there are two optimisations that can still be done. Currently testing one out, and I think it'll be pretty fantastic if it pans out.

  • Mechanicals are firming up

    Jarrett12/18/2017 at 03:07 0 comments

    As mentioned earlier, some minor mechanical redesign was in order.

    The driven pulley was moved to prevent cantilevering, and I switched to D-profile shaft to allow some extra laser-cut pieces to couple the PCB.

    Cut it all out and assembly time.

    Resulting in massive lossy GIFs:

    I'm actually super stoked about the speed  and how stable the whole thing is. Excuse the handheld shaky-cam.

    A couple minor issues:

    The large pulley is still a little bit close to the upper bearing, it's either rubbing or in danger of rubbing. There's a ton of room below it, so what needs to be changed are the motor mounting slots. A little bit lower. Additionally, the belt is at full tension while the motor carriage all the way to the right. So it works, but it would be nice to have a little bit more clearance.

    So, in summary:

    Down 3mm, to the right 3mm.

  • A brief note on peripherals, speed, and memory

    Jarrett12/04/2017 at 20:31 0 comments

    Let's dive into how the code works, presently.

    The main loop looks like this:

        while(1)
        {
            
            if (frame != angleInt && angleInt < HPIXELS) {
                frame = angleInt;
                
                DisableInterrupts();
                for(i = 0; i < VPIXELS; i++) {
                    setChannel(blob, i, blueMap[frame][i]);
                    setChannel(blob, 23 - i, greenMap[frame][i]);
                }
                LEDMap(blob);
                EnableInterrupts();
                
            }
        }

     This is a little different than the final design, but the principle is similar. The testbench has one LED driver running two banks of 10 LEDs each. The final design will use 20 LEDs driven by two ICs. That should mean the main difference is that two LEDMap calls will be made.

    Currently, the channel assignment code looks like this:

    void setChannel(uint8_t *blob, uint8_t channel, uint16_t value) {
        uint8_t lvalue;
        uint8_t rvalue;
        uint8_t newVal;
        uint8_t byteAddr;
        
        if(channel % 2 == 0) {
            byteAddr = (channel * 3) >> 1;
            lvalue = (uint8_t)(value & 0xFF);
            rvalue = (uint8_t)(value >> 8);
            
            *(blob + byteAddr) = lvalue;
            newVal = (*(blob + byteAddr + 1)) & 0xF0;
            newVal = rvalue | newVal;
            
            *(blob + byteAddr + 1) = newVal;
        } else {
            byteAddr = (((channel - 1) * 3) >> 1) + 1;
            
            lvalue = (uint8_t)(value << 4) & 0xF0;
            rvalue = (uint8_t)(value >> 4);
            
            newVal = (*(blob + byteAddr)) & 0x0F;
            newVal = lvalue | newVal;
            
            *(blob + byteAddr) = newVal;
            *(blob + byteAddr + 1) = rvalue;
        }
    }
    
    void LEDMap(uint8_t *blob)
    {
        uint8_t data = 0;
        
        XLAT = 0;
        
        for(int i = TABLESIZE - 1; i >= 0; i--) {
            data = *(blob + i);
            SPIWrite(data);
        }
        
        XLAT = 1;
        __delay_ms(10);
        XLAT = 0;
    }

    setChannel takes in a 16-bit value and address and puts it into a table of 12-bit values, using some magic that offsets everything into the proper address. The LED driver uses 12-bits for each channel, sent sequentially. There's a pretty glaring huge problem with this: It takes a ton of time to go through each row, many times per rotation. It has to be run for each pixel. In my final board, that will be 20 LEDs per driver, two drivers, and I'm hoping to get at least the same resolution on the vertical as horizontal  (so 20 * Pi ~= 63 LED changes per rotation).

    With a very rough test using AVR GCC with the Godbolt compiler (I know it's not the right architecture), we get:

    ~80 instructions * 24 channels * 2 banks = 3840 instructions for setChannel 

    ~20 instructions * 36 channels * 2 banks = 1440 instructions  for LEDMap

    The PIC internal clock is 32MHz, but the instruction clock divides that by four:

    8E6 / (3840 + 1440 instructions) = 1515 Hz for the entire loop.

    Divide that again by 63 virtual horizontal pixels, and you get the full rotation of the PCB being maximum about 24 RPM. That's not nearly good enough!

    The bulk of the processor time here is taken up by copying the currently active frame from input storage data into the output array, aligning it properly into 12-bit channels. What that means is that I can stick using 8-bit data and get more data out of my storage, at the cost of brightness resolution. We've discovered that it's also at the cost of unacceptable processor time, so I'll give the other method a shot, and try storing everything as a 36-byte table, representing 24 12-bit channels.

    That brings us into a topic I've skipped talking about so far. The PIC family of microcontrollers is interesting because it is absolutely huge, with all kinds of different peripherals and weird combinations.

    The one I've been using for testing, and likely the same (or similar) one I'll be using in the final model is the PIC16F1619. It is one of only 4 different models that have a poorly marketed and even more poorly documented Angular Timer. It's a fantastic peripheral, though, that does exactly what I need. You give it a periodic signal (eg. Hall Effect sensor triggered by a magnet), and it will divide that time up spit out an interrupt at regular intervals, depending on how many interrupts you want per period.

    So that's taken a load off my...

    Read more »

  • Firming up the code

    Jarrett11/28/2017 at 03:51 0 comments

    My test jig is becoming invaluable, as I wait for more materials to arrive on the slow boat from China.

    Project files are being posted here as I go, for those that missed it.

    It makes my workbench look like an 80s cyberpunk fantasy.

    The Python tool I've written to convert images to a POV-friendly format has been great.

    Give it a command like:

    python convert.py -i World_map.png -f c -v -c blueMap -x 152 -y 20 -o blue

    And it spits out generated C code, CSV files, or images after conversion. For more info, type: 

    python convert.py --help

    It'll print out a handy menu of all the options.

    The C generated output gets stored as a big chunk of memory and transferred out via SPI to the LED controller. Interestingly, the compiler chokes at any more than 6080 bytes of contiguous memory, even though it only uses up 78% of total memory. That'll be a memory paging issue, but I think it'll be fine. That is two displays (green and blue), 20 pixels high, and 152 pixels around.

    For my test jig, I did much smaller - 10 pixels high and 50 across, and then waved it back and forth in front of camera a bunch.

    Hey... that doesn't look like anything familiar.

    Oh wait. No, I guess that's about right.

    Here's the original:

    (Greyscale values inverted because green must be 0xFF)

    The final

    Version at 20 pixels high will look more like this:

    Recognisable, but it would still be nice if it were finer. Maybe revision 2.

  • Scrap it and do it again

    Jarrett11/10/2017 at 18:39 0 comments

    I hinted in the previous log that there were some changes in the pipeline.

    My CAD drawings use thinner acrylic than the stuff I actually ended up using. This, in addition to deficiencies of the design, led to some issues that require another revision.

    Here is the current design:

    Due to the acrylic sizing, the bearing blocks on the right have a fair amount of slop. This isn't helped by cheap bearings, either. For the initial startup, the driven shaft goes all the way to the top set of bearing blocks, preventing the cantilever effect from causing issues.

    As soon as that was sliced to hold the PCB, however, the cantilevering got much worse.

    All three pieces of acrylic that make up each pulley are thicker, leading to all tolerances being a little tighter than how it looks in the design. When the pulley is on and tensioned, the forces are pulling the driven shaft(1).  That's causing the shaft to cantilever towards the motor(2). When that happens, the sidewalls of the pulley start rubbing the bearing block(3), in addition to causing a ton of vibrating as the PCB is off-centre.

    So that means that as soon as my PCB was actually attached, the system was hard to start, unstable when it did, and then high enough friction that my motor burned out.

    So it's back to the drawing board, for part of it.

    In this rearrangement that took all of 30 seconds, it should be easy to see what changes need to be made. The motor carriage will be shifted up a little, to allow the pulley to rest in between two bearing blocks. This should completely eliminate any cantilevering, unless the shaft itself is bending(which would be a Bad Thing).

    Additionally, instead of trying to sandwich bearings with acrylic of the wrong size, I'll rest them in an open hole, held in by shaft collars at the appropriate places. That'll make more sense when I clean up the design.

  • Spin up and first fit

    Jarrett11/08/2017 at 19:14 0 comments

    When we last left our intrepid hero - I mean the laser cutter, obviously - we were waiting on properly sized belts. I have received and installed one and spun up the machine. 200mm was a good size. It works, in this configuration!

    The motor draws about 10.5W, and seems to go at a good clip. I should measure it at some point.

    I still haven't found a silver bullet for my PCB coupling issues. The goal for today is to attempt one of the possible methods and see if it works. It wasn't written in the previous log, but I also picked up some nylon RC clevis pins. The bore is smaller than my 3mm shaft, however, and I couldn't find anything more appropriate.

    Instead of finding a 3mm drill bit and drilling it out, here's a neat trick that's much more versatile:

    By grinding a flat onto hardened rod, you can turn it into very mediocre drill bit, with exactly the required diameter.

    And then you can drill it into the nylon pins.

    Here's the machine, completely assembled. That cardboard disc is a stand-in for my PCB, with a pretty similar thickness.

    Spun up:

    Yeah, I'm quite happy with the speed.

    But it's now time to scrap this revision and rebuild it from scratch. Stay tuned!

  • Hjalp! Mechanical plan and call for discussion

    Jarrett11/05/2017 at 22:14 4 comments

    Here's a mock up of the PCB. It'll  be spun axially by 3mm shafts on the top and bottom.

    And herein lies the second question:

    What's a good method for affixing the shafts to the PCB?

    The current working plan is to use a shaft collar with a 1.6mm thick slit carved into the face to hold the edge of the PCB.

    This method has a couple problems:

    How do I cut the slit?

    It's not an off-the-shelf solution.

    There are a couple other solutions I've been toying with:

    These are two identical plates, clearance holes on one side, and threaded holes on the other.

    I don't know where to get them, or what they're called. Colour doesn't matter, but shape is important. These ones, and others I have seen, come as cable clamps for some DB15-style connectors.

    Because they're just support pieces for connectors, however, they don't seem to be able to be purchased individually, or have dimensions associated with them. $4 for the entire package is cost prohibitive, too.

    Another possible method:

    Hobby RC plane clevis pins.

    There are a few issues with that, too.

    There isn't really anything that fits both the shaft and the PCB thickness. They're cheaply made and inconsistently sized. They're also too long and eat up my vertical board space.

    So what say you, internet? Any suggestions? Alternate ideas, or good sources for things?

  • Here be code

    Jarrett11/04/2017 at 02:33 0 comments

    If you don't need or want a simple C driver for this chipset, I'd skip this post!

    #define OE          LATBbits.LATB5
    #define XLAT        LATCbits.LATC2
    
    //12 bits per channel = 1.5 bytes * 24 channels
    #define TABLESIZE 36
    #define CHANNELS  24
    
    void main(void)
    {
        unsigned int i;
        uint8_t mapInc = 0;
        uint8_t blob[TABLESIZE];
    
        while(1)
        {
            //Zero out buffer
            for(i = 0; i < TABLESIZE; i++) {
                blob[i] = 0x00;
            }
            
            //Colourspace is 0xFFF levels
            //Dividing/multiplying by four to speed up the fade
            for(i = 0; i < 0x1000 / 4; i++) {
                setChannel(blob, mapInc, i * 4);
                LEDMap(blob);
            }
            
            mapInc = (mapInc + 1) % 24;
        }
    }
    
    
    void setChannel(uint8_t *blob, uint8_t channel, uint16_t value) {
        uint8_t lvalue;
        uint8_t rvalue;
        uint8_t newVal;
        uint8_t byteAddr;
        
        if(channel % 2 == 0) {
            byteAddr = (channel * 3) / 2;
            lvalue = (uint8_t)(value & 0xFF);
            rvalue = (uint8_t)(value >> 8);
            
            *(blob + byteAddr) = lvalue;
            newVal = (*(blob + byteAddr + 1)) & 0xF0;
            newVal = rvalue | newVal;
            
            *(blob + byteAddr + 1) = newVal;
        } else {
            byteAddr = (((channel - 1) * 3) / 2) + 1;
            
            lvalue = (uint8_t)(value << 4) & 0xF0;
            rvalue = (uint8_t)(value >> 4);
            
            newVal = (*(blob + byteAddr)) & 0x0F;
            newVal = lvalue | newVal;
            
            *(blob + byteAddr) = newVal;
            *(blob + byteAddr + 1) = rvalue;
        }
    }
    
    void LEDMap(uint8_t *blob)
    {
        uint8_t data = 0;
        
        XLAT = 0;
        
        for(int i = TABLESIZE - 1; i >= 0; i--) {
            data = *(blob + i);
            SPIWrite(data);
        }
        
        XLAT = 1;
        __delay_ms(1);
        XLAT = 0;
    }

    I cut out all the PIC initialisation stuff, but the only gotcha there is that the SPI must transmit on the active to idle clock transition.