Rarely do I get to write assembly code in my day job. Usually I’m doing mixed-signal controller board design and someone else is writing C code. If I’m getting paid to code, it’s usually also been in C. So I’m at maximum exhilaration when I both get to design a special-purpose PCB and code it up to squeeze every last bit of performance out of a microcontroller.
Two past PCB designs I did stand out for me in this fashion. Quite on the opposite end of the spectrum from the high-speed monster I just finished turning on, both were 4 layer PCBs or fewer and based around relatively slow (32MHz PIC24, USB1.1) controller boards: one for 2-axis coordinated motion and the other for multicolor LED light shows.
Microchip of course makes the PIC18 series, some members of which do add USB to this time-honored set of 8-bit peripherals and 16-bit-wide, compiler-friendly instructions. But while both these applications had to support a low-cost overall solution, their real-time demands weren’t well-suited to an 8-bit microcontroller.
Microchip’s step-up PIC24FJ32GB offering has been a nice minimum-component-count platform over the last few years for any low-cost USB application that can fit in only 8K of RAM for variable storage but needs enough processing power to keep up with several motor bridges or serial peripherals. It comes in two different packages: 28-pin dual-inline (-002 suffix, 14 pins on either side, either through-hole or surface-mount) or 44 pins with 11 on each side of a square surface-mount package (-004 suffix, either leadless or quad-flatpack). The amount of RAM seems small, but frequently in a drive application 8K is sufficient to buffer the incoming USB packets and act on them in some meaningful way before moving on; longer-term retention isn’t required.

2-axis Cutting

The first board of my favorites is one I designed for a leading brand of craft cutter. As a two-axis motion controller, it dispensed with stepper motor drive (for reasons of size, audible noise and cost) and introduced DC servo motor control into the market for cutting pre-designed intricate shapes and fonts out of colored/textured paper. I didn’t actually write any of the code on the servo processor that takes cut vectors over USB and does the motion control; that was all done by a very capable embedded C developer and algorithms expert.
What I did do after I sent the board off for fab is program a companion 8-bit PIC device on the board in what has become an anachronism, now that Microchip offers 16-bit controllers with both USB OTG support and dual quadrature encoders in the form of their EP series. But back in 2011, it was necessary to choose: add a USB-to-UART bridge (such as the popular FT232 series or Microchip’s equivalent MCP2200) to a dsPIC33 motor controller, or offload the quadrature encoder interrupt (QEI) processing from the PIC24 and allow it to query the position counts every 1ms over I2C from a dedicated microcontroller.
Since it was the lowest-cost alternative I used a PIC16LF1823 for the latter, and although I was just implementing existing QEI hardware blocks in software it was a fun exercise in interrupt latency reduction. I knew how many 125-ns cycles were burned for every possible ISR branch combination, since this drove the allowable line density on the encoder wheel. The bugs that manifested themselves whenever certain types of interrupts got dropped and the cuts designed to exacerbate them furthermore produced some interesting abstract shapes in the paper along the way.

LED light shows in a display case

The second board I did had to control red, green and blue LED illumination strips for up to 16 product showcase bins in a wooden cabinet. The support chip for this product was the LT8500 from Linear Tech, which provides exactly the 48 independent digital PWM channels that I needed for just a few dollars. The assembly code in the PIC24 acted as more than a bridge between the USB/RS-485 link and the LT8500: it maintained the state table in RAM and provided a protocol for quickly changing/ramping the colors upon a command trigger without any awkward latencies from one bin to the next. All color changes thus were synchronized by the LT8500 to within a few milliseconds, much better than the DMX-based prototype driven by a Microsoft box. Alas, a better career opportunity presented itself before this board went out for fab so I never got past the simulation phase.

Favorite Assembly Macros

I want to share a few favorite PIC24/dsPIC33 macros that came out of this project. The first one just does a copy up to and including the first zero-valued byte, i.e. strcpy(), which assembles down very compactly on this platform. The return value in W0 will always be 0:

.macro strcpy src,dst ; src and dst can be any register W1 through W15
 clr.b W0 ; clobber W0 since an ALU operation is needed to set flags
 ior.b W0,[\src++],[\dst++] ; keep copying, advancing both pointers
 bra nz, .-2 ; branch to the previous instruction if nonzero

So you can call this macro as follows:

mov #STRING,W1
mov #BUFFER,W2
strcpy W1,W2

The next PIC24 assembly macro is a bit more involved. It provides a “MOVe_Byte to High position” that is useful to abstract on this 16-bit processor. When packing halfwords into words the diverse instruction combinations in this macro tend to get executed a lot anyway, so we might as well encapsulate them into a single command that handles the cases for us.  The mov_bh macro (I would’ve defined it as “mov.bh” if macro names were allowed to contain the dot character) can be called with any valid syntax that the mov instruction supports, such as immediate/literal, register-to-register, etc., and we won’t see in the calling code any of the requisite swap instructions.  For example we want to be able to do any of the following:

mov #0xabcd,W1 ; W1:0xabcd
mov #0xfedc,W0 ; W1:0xabcd, W0:0xfedc
mov WREG,file ; W1:0xabcd, W0:0xfedc, file:0xfedc
mov_bhl 0x03,W1 ; W1:0x03cd, W0:0xfedc, file:0xfedc
mov_bh file,WREG ; W1:0x03cd, W0:0xdcdc, file:0xfedc
mov_bh W1,file ; W1:0x03cd, W0:0xdcdc, file:0xdcdc
mov_bh W0,W1 ; W1:0xdccd, W0:0xdcdc, file:0xdcdc
mov_bh W1,file ; W1:0xdccd, W0:0xdccd, file:0xcddc
swap W0 ; W1:0xdccd, W0:0xcddc, file:0xcddc
mov_bh WREG,file ; W1:0xdccd, W0:0xcddc, file:0xdcdc

The macro definition for mov_bh begins by first mapping the registers W0 (or WREG) through w15 that could get passed onto just their numeric equivalents.  This way we can both handle the WREG case as well as make sure that the register is a valid one in this range.

.equiv MOV_BH_REGISTER_W0, 0
.equiv MOV_BH_REGISTER_W1, 1
.equiv MOV_BH_REGISTER_W2, 2
.equiv MOV_BH_REGISTER_W3, 3
.equiv MOV_BH_REGISTER_W4, 4
.equiv MOV_BH_REGISTER_W5, 5
.equiv MOV_BH_REGISTER_W6, 6
.equiv MOV_BH_REGISTER_W7, 7
.equiv MOV_BH_REGISTER_W8, 8
.equiv MOV_BH_REGISTER_W9, 9
.equiv MOV_BH_REGISTER_W10, 10
.equiv MOV_BH_REGISTER_W11, 11
.equiv MOV_BH_REGISTER_W12, 12
.equiv MOV_BH_REGISTER_W13, 13
.equiv MOV_BH_REGISTER_W14, 14
.equiv MOV_BH_REGISTER_W15, 15
.equiv MOV_BH_REGISTER_wreg, 0
.equiv MOV_BH_REGISTER_w0, 0
.equiv MOV_BH_REGISTER_w1, 1
.equiv MOV_BH_REGISTER_w2, 2
.equiv MOV_BH_REGISTER_w3, 3
.equiv MOV_BH_REGISTER_w4, 4
.equiv MOV_BH_REGISTER_w5, 5
.equiv MOV_BH_REGISTER_w6, 6
.equiv MOV_BH_REGISTER_w7, 7
.equiv MOV_BH_REGISTER_w8, 8
.equiv MOV_BH_REGISTER_w9, 9
.equiv MOV_BH_REGISTER_w10, 10
.equiv MOV_BH_REGISTER_w11, 11
.equiv MOV_BH_REGISTER_w12, 12
.equiv MOV_BH_REGISTER_w13, 13
.equiv MOV_BH_REGISTER_w14, 14
.equiv MOV_BH_REGISTER_w15, 15

The next step is to handle the cases one by one.

.macro mov_bh src,dst ; mov register/file lower byte into upper byte
 .ifdef MOV_BH_REGISTER_\dst ; i.e. MOV_BH file,WREG or Ws,Wd
 .if MOV_BH_REGISTER_\dst ;
 swap \dst ;
 mov.b \src,\dst ;
 swap \dst ;
 .else ;
 swap W0 ;
 mov.b \src,\dst ;
 swap W0 ;
 .else ; i.e. MOV_BH file,file or Ws,file
 .ifdef MOV_BH_REGISTER_\src ;
 .if MOV_BH_REGISTER_\src ; i.e. MOV_BH W1,file through W15,file
 push W0 ;
 mov \src,W0 ;
 mov.b WREG,(1+(\dst)) ;
 pop W0 ;
 .else ; i.e. MOV_BH WREG,file or W0,file
 mov.b WREG,(1+(\dst)) ;
 .endif ;
 .else ; i.e. MOV_BH file,file (via W0)
 push W0 ;
 mov.b \src,WREG ;
 mov.b WREG,(1+(\dst)) ;
 pop W0 ;
.macro mov_bhl src,dst ; mov literal byte into upper byte
 .ifdef MOV_BH_REGISTER_\dst ; i.e. MOV_BH #lit8,Wd
 swap \dst ;
 mov.b #\src,\dst ;
 swap \dst ;
 .else ; i.e. MOV_BH #lit8,file
 push W0 ;
 mov.b #\src,W0 ;
 mov.b WREG,(1+(\dst)) ;
 pop W0 ;

That’s it for this retrospective of some of my favorite 16-bit microcontroller PCB designs. In the next post I’ll go back to 8-bit land with a MOS 6502 coding exercise.