Tuesday 12 September 2017

Take your PIC

The Microchip PIC microcontrollers are amazing.  I encountered them back in 1998, and they are game changers for digital electronics. They are cheap (and I mean throwaway cheap), easy to power (they have wide operating voltages, have very versatile I/O and are surprisingly powerful.

One of many incarnations of the PIC24. Photo by Acdx


The 'F' series (PIC16F, PIC18F, PIC24F) were electrically re-programmable. They also have eeprom, which means you can retain parameters and data after power-off. Best of all Microchip provided a free (rare at the time) subset of development tools, including sample code.

Dinosaurs- ultra-violet light erasable PICs. Photo from wikipedia


It now made no sense to use timer ICs like the CD4040, or even the iconic LM555 or multivibrators like the 74121. A PIC is much more precise, does not drift over time, and more consistent, especially for production units. For me, it even replaced small-scale FPGA. For an old assembly language jock (my first computer language was solder!), it was heaven-sent.

The PIC18F14K50 is one of the first USB microcontrollers. It came with a free subset of the MPLAB IDE and you can get it to work with a C compiler. Best of all there is a lot of C source code, for many types of USB Device, USB Hosts and USB OTG. The flash programmer Pickit 2 was cheap, especially the third-party versions.
The USB interface is the current de facto interface standard, and was well worth the investment in time from 15 years ago. Microchip's Low Pin Count Development Kit was cheap and can be deployed with minimal modifications.

Left: USB-RS485 interface 
Right: Low Pin Count Development Kit. 
The USB RS485 dongle is my design, a slightly modified derivative of the Microchip Low Pin Count Development Kit. It is based on the PIC18F14K50 microcontroller. The following work also applies to the Low Pin Count Development Kit. The advantage is that it is compatible with the existing Windows and Linux device drivers: both Windows and Linux will automatically recognize it as a garden-variety USB serial port. We thus avoid the need to write custom device drivers (although writing device drivers can be fun and profitable- perhaps the subject of another post).


PIC18F14K50-based RS485 USB dongle and the PICKIT2 programmer
The intention is to use the USB RS485 dongle, together with the Raspberry Pi to measure the battery voltage at my solar panel. This is gross overkill for an embedded voltmeter, but as we shall see, it expands into an Internet of Things device. And by having the device come up as a standard USB serial port, we again avoid the need for custom Linux and Windows device drivers.

It would be a strange serial port. When sent any valid character, the device will respond with:
byte1 byte2 'HCM' byte3 byte4 'LJP'. 

Where byte1 and byte2 are hexadecimal bytes from AN8 and similarly, byte3 and byte4 are hexadecimal bytes from AN9. The PIC's Analog-to-Digital Converter is 10-bits, so 2 bytes are required for each input.

USB RS485 dongle with added resistor divider circuit

USB RS485 dongle with Raspberry Pi, mounted as an IoT solar battery voltmeter

My version of MPLAB only runs on Windows (you might have better luck with the newer MPLAB X), so I usually run it from my Qemu Virtual Machine. I then copy the compiled hex file and program it using pk2cmd.

Screenshot of Slackware 14.2 Linux running MPLAB 8.3 Windows XP on a Qemu  Virtual Machine

The sample code I am using is the Microchip USB Device - CDC - Serial Emulator. There is an online version here, but I would recommend you download the Microchip version.

The PIC18F14K50 has 9 usable external analog input lines. We only need two, one for the battery and one for the solar panel. We use AN8 and AN9, just because they happen to be unused and easily soldered on the PCB.

We find the file main.c in the sample code and insert the following lines of C code to initialize the PIC:

void InitializeUSART(void)
{
    #if defined(__18CXX) // __18CXX *is* defined
            unsigned char c;
        #if defined(__18F14K50)
            TRISC |= 0x30; // Set up AN8 AN9 for analog in
            ANSELH = 0x03; // Enable AN8-AN9, RB4, RB5
                           // we use RB4 for MAX485 RE (receiver enable)
            ANSEL = 0x00;  //Disables A4-A7 (enables RC0-RC3 for USB LEDs)
            ADCON2 = 0x9D; // Right-justified output, 6TAD
            ADCON1 = 0x00; // Vdd & Vss as +ve & -ve voltage references
            ADCON0 = 0x21; // Select AN8 (CHS=1000) and turn on ADC

Next we find the function ProcessIO() and insert:

#if defined(__18CXX)
    #define mDataRdyUSART() PIR1bits.RCIF
    #define mTxRdyUSART()   TXSTAbits.TRMT
    #define mAN8Busy()   ADCON0bits.GO 
#elif defined(__C30__) || defined(__C32__)
    #define mDataRdyUSART() UART2IsPressed()
    #define mTxRdyUSART()   U2STAbits.TRMT
#endif

void ProcessIO(void)
{
  //Blink the LEDs according to the USB device status
  BlinkUSBStatus();
  // User Application USB tasks
  if((USBDeviceState < CONFIGURED_STATE)||(USBSuspendControl==1)) return;

  if (RS232_Out_Data_Rdy == 0)  // only check for new USB buffer if the old RS232 buffer is
  {               // empty.  This will cause additional USB packets to be NAK'd
    LastRS232Out = getsUSBUSART(RS232_Out_Data,64); //until the buffer is free.
    if(LastRS232Out > 0)
    {
      RS232_Out_Data_Rdy = 1;  // signal buffer full. Any amount of data will do
      RS232cp = 0;  // Reset the current position
      mLED_3_On();  // 2017-09-03 test code
      mLED_4_Off(); // 2017-09-03 test code
    }
    else
    {
      mLED_3_Off();  // 2017-09-03 test code
      mLED_4_Off();  // 2017-09-03 test code
    }
  }

  if(RS232_Out_Data_Rdy && !mAN8Busy() && RS232cp==0 ) // 2017-09-03 Received command, ADC is free
  {
    ADCON0 = 0x21; // 2017-09-03. Select AN8 (CHS=1000) and turn on ADC
    ADCON0bits.GO = 1; // Start conversion
    ++RS232cp;    // Indicate pending conversion
    mLED_3_On();  // 2017-09-03 test code
    mLED_4_On(); // 2017-09-03 test code
  }

  if(RS232_Out_Data_Rdy && RS232cp==1) // 2017-09-03 check if conversion done
  {
    if (!mAN8Busy())
    {
      USB_Out_Buffer[NextUSBOut++] = ADRESH; // Pick up results
      USB_Out_Buffer[NextUSBOut++] = ADRESL; // Pick up results
      USB_Out_Buffer[NextUSBOut++] = 'H'; // Pick up results
      USB_Out_Buffer[NextUSBOut++] = 'C'; // Pick up results
      USB_Out_Buffer[NextUSBOut++] = 'M'; // Pick up results
      USB_Out_Buffer[NextUSBOut] = 0;
      RS232cp++; // 2017-09-04 Signal ready for next command
      mLED_3_Off();  // 2017-09-03 test code
      mLED_4_On(); // 2017-09-03 test code
    }
  }
  if(RS232_Out_Data_Rdy && !mAN8Busy() && RS232cp==2 ) // 2017-09-04 Do AN9
  {
    ADCON0 = 0x25; // 2017-09-04. Select AN5 (CHS=1001) and turn on ADC
    ADCON0bits.GO = 1; // Start conversion
    ++RS232cp;    // Indicate pending conversion
    mLED_3_On();  // 2017-09-03 test code
    mLED_4_On(); // 2017-09-03 test code
  }

  if(RS232_Out_Data_Rdy && RS232cp==3) // 2017-09-03 check if conversion done
  {
    if (!mAN8Busy())
    {
      USB_Out_Buffer[NextUSBOut++] = ADRESH; // Pick up results
      USB_Out_Buffer[NextUSBOut++] = ADRESL; // Pick up results
      USB_Out_Buffer[NextUSBOut++] = 'L'; // Pick up results
      USB_Out_Buffer[NextUSBOut++] = 'J'; // Pick up results
      USB_Out_Buffer[NextUSBOut++] = 'P'; // Pick up results
      USB_Out_Buffer[NextUSBOut] = 0;
      RS232_Out_Data_Rdy = 0; // Signal ready for next command
      mLED_3_Off();  // 2017-09-03 test code
      mLED_4_On(); // 2017-09-03 test code
    }
  }

  if((USBUSARTIsTxTrfReady()) && (NextUSBOut > 0))
  { // Send results to USB
    putUSBUSART(&USB_Out_Buffer[0], NextUSBOut);
    NextUSBOut = 0;
    mLED_3_Off();  // 2017-09-03 test code
    mLED_4_Off(); // 2017-09-03 test code
  }

  CDCTxService();
}               //end ProcessIO

Now you might notice that a lot of the code has to do with serial IO, which is not necessary for our analog to digital conversion. This will come in hand later as we refine our PIC IoT to work without the Raspberry Pi. Without the Pi as the USB host we will need to transmit our result using the serial port.

We then compile the modified sample code into a hex file which we then program using the Pickit 2:
./pk2cmd -PPIC18F14K50 -Fanalog_pic.hex  -M

If the stars are all aligned and everything goes perfectly (more likely after hours of painful but ultimately satisfying debugging) you get the correct blinkenlights on your development kit, and the PIC comes up as '/dev/ttyACM0'. We proceed to the next part, the application program to read the 'serial' port.

We whip out our python interpreter:

root@aspireF15:/home/heong/mpg$python
Python 2.7.11 (default, Mar  3 2016, 13:35:30)
[GCC 5.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import serial
>>> port=serial.Serial('/dev/ttyACM0', timeout=1)
>>> port.write('A'); port.read(10)
1
'\x00\x00HCM\x00\x00LJP'

OK, we have a valid reply but is reading zeroes in out analog input. Time to connect AN8 and AN9 to a reference voltage, 5V.

>>> port.write('A'); port.read(10)
1
'\x03\xedHCM\x00\x00LJP'

With multimeter, AN8 measures 4.94V and the 5V rail measures 5.01V. Assuming 3ff(1023) is 5.01, so 3ed is 1005, we have 4.92V (1005 / 1023 * 5) which is pretty close!

Next we look to measuring the solar battery voltage, which we expect to be around 12V. We will need a voltage divider.

Use 10K resistor for Z1 and 3K resistor for Z2

If we use 10K and 3K resistors, this will allow for a 22V maximum at the solar panel. You should use 1% tolerance wire-wound resistors. I happened to have only 20% carbon film resistors, so I used those and hope the error can be calibrated away. Note the carbon resistors should be much worse over temperature and age. 

After some hurried programming with the solder programming language, we connect the USB pic to the battery positive and negative terminals (watch it- reversing the connection may fry your USB device or your laptop!) and we now get:

>>> port.write('A'); port.read(10)
1
'\x03\x18HCM\x00\xdfLJP'

0318 hex is decimal 792. If full-scale is hex 3ff (decimal 1023). Estimating full-scale as 5V x 13/3, and calculating 792/1023 * 5 * 13 / 3 or 16.8V. With voltmeter I get 15.19. Which is not brilliant, but reasonable.

This has been my longest post to date. I hope it did not look too difficult - it really isn't. There may be many new things like microcontrollers (PICs), USB, and python, but the sample code from the Low Pin Count Development Kit really does work out of the box. I do not know everything mentioned here 100%. I know just enough to get the project going, and that is the norm in these manic days of ever-diminishing cycle time.

In the next post I will link up the analog input USB PIC18F14K50 to my website, and this makes it part of the Internet of Things.

Good luck and happy trails.

No comments:

Post a Comment