Monday, February 13, 2017

Controlling 16 rc servos with the Adafruit 16-Channel 12-bit PWM/Servo Driver PCA9685 with AutoIT over I2C

In order to connect I2C devices to the PC I'm using the CH341A chip as USB to I2C converter (http://germanwarez.blogspot.ch/2016/02/controling-i2c-bus-with-autoit-with.html). The Adafruit is about USD15, I used a clone for about USD2 (search for PCA9685 on aliexpress).



16-Channel 12-bit PWM/Servo Driver - I2C interface - PCA9685

The PCA9685 is produced by NXP Semiconductors and comes with a datasheet in pdf format 16-channel, 12-bit PWM Fm+ I2C-bus LED controller and some more documents at their webpage for the PCA9685|NPX. The interesting features as servo controller are:
-Supply voltage: 2.3 to 5.5V
-I2C bus speed: up to 1 MHz (fast mode)
-16 outputs that can be set to ON or OFF or PWM (12 bit)
-Internal clock (25 MHz) and prescaler (duty cycle between 24 Hz and 1526 Hz, default is 200 Hz)
-Optionally external clock (up to 50 MHz)

-Auto-increment of registers, allowing to write the PWM data for all outputs in a single I2C bus transfer
-PWM is programmed over I2C, no load on the main CPU

The chip has a default address of 100 0000b or 40h. There are six solder pads to change the zeros into ones on my PCB, allowing to configure a maximum of 63 different addresses that can be actually used (address 111 0000 can't be used, and within the 63 addresses there are many reserved addresses that can be used only because the chip doesn't comply to the I2C standard by responding to reserved addresses as well). In order to control rc servos there are one or two things to be configured. It is the pre-scaler, and optionally the auto-increment.


The pre-scaler is configured by writing the desired value to the PRE_SCALE register at address FEh while the chip is sleep mode, and it's done like this:
  1. Set bit 4 of MODE 1 register (stop all PWM output, stop the internal oscillator)
  2. Write the pre-scale value to the PRE_SCALE register
  3. Clear bit 4 of MODE 1 register (start the internal oscillator)
  4. Wait 500 us
  5. Set bit 7 of MODE 1 register (enable all PWM output)
The calculation of the prescaler for a 20 ms (corresponding to 50 Hz) duty is simple:
PRE_SCALE = 25000000 Hz / (4096 * 50 Hz ) - 1

Auto-increment is configured by setting bit 5 in the MODE 1 register at address 00h, it is not implemented due to limitations in the I2C interface code.

That's it.

As the title suggests here's the AutoIT code "servotest.au3":
;PCA9685_TESTSUITE
;Schematic:
;PC -USB- CH341A -I2C- PCA9685 -wire- Oscilloscope

AutoItSetOption("MustDeclareVars", 1)
#include "CH341A_USB_to_I2C_helper.au3"

CH341AInit()
;init PCA9685 (no-auto-increment due to limited driver capabilities)
CH341AWrite(0x40, 0, 16)
CH341AWrite(0x40, 0xFE, 121) ;
CH341AWrite(0x40, 0x00, 0)
Sleep(500)
CH341AWrite(0x40, 0x00, 128) ;restart, not needed (but in the datasheet)
CH341AWrite(0x40, 0x00, 0)
;set servo1 to max, servo2 to middle and servo3 to max
;It takes 20ms to pass 4096 ticks
;It takes 205 ticks for 1 ms (min), 307 ticks for 1.5 ms (middle) and 410 ticks for 2ms (max)
Local $loByte, $hiByte, $hex
$hex = Hex(205,4)
$hiByte = Dec(StringLeft($hex,2))
$loByte = Dec(StringRight($hex,2))
CH341AWrite(0x40, 0x06, 0)
CH341AWrite(0x40, 0x07, 0)
CH341AWrite(0x40, 0x08, $loByte)
CH341AWrite(0x40, 0x09, $hiByte)

$hex = Hex(307,4)
$hiByte = Dec(StringLeft($hex,2))
$loByte = Dec(StringRight($hex,2))
CH341AWrite(0x40, 0x0A, 0)
CH341AWrite(0x40, 0x0B, 0)
CH341AWrite(0x40, 0x0C, $loByte)
CH341AWrite(0x40, 0x0D, $hiByte)

$hex = Hex(410,4)
$hiByte = Dec(StringLeft($hex,2))
$loByte = Dec(StringRight($hex,2))
CH341AWrite(0x40, 0x0E, 0)
CH341AWrite(0x40, 0x0F, 0)
CH341AWrite(0x40, 0x10, $loByte)
CH341AWrite(0x40, 0x11, $hiByte)
Exit
And here's the include file "CH341A_USB_to_I2C_helper.au3":
;CH341A_USB_to_I2C_helper
;functions:
;CH341AInit()
;  Initialize the CH341A chip for i2c
;  No return value
;CH341ARead(i2cAddr, i2cRegister)
;  Read the value of the given i2c chip and register address
;CH341AWrite(i2cAddr, i2cRegister, data)
;  Write data to the given i2c chip and register address

#include <WinAPISys.au3>

;device ID
Global $CH341Aid = 0
Global $CH341Adll = DllOpen("C:\Windows\System32\CH341DLL.DLL")

Func CH341AInit()
   ;open the dll
   If $CH341Adll  <> -1 Then
      ConsoleWrite("CH341AInit: DllOpen OK" & @CRLF)
   Else
      ConsoleWrite("CH341AInit: DllOpen error, could not open the dll." & @CRLF)
      ConsoleWrite("CH341AInit: FATAL ERROR. EXIT." & @CRLF)
      Exit
   EndIf

   ;open the device
   Local $aResult = DllCall($CH341Adll, "BOOL", "CH341OpenDevice", "ULONG", $CH341Aid)
   If  $aResult[0] <> -1 Then
      ConsoleWrite("CH341AInit: CH341OpenDevice OK" & @CRLF)
   Else
      ConsoleWrite("CH341AInit: CH341OpenDevice error: Could not open device " & @CRLF)
      ConsoleWrite("CH341AInit: FATAL ERROR. EXIT." & @CRLF)
      Exit
   EndIf

   ;set the I2C speed
   Local $iMode = 0
   Local $aResult = DllCall($CH341Adll, "BOOL", "CH341SetStream", "ULONG", $CH341Aid, "ULONG", $iMode)
   If  $aResult[0] Then
      ConsoleWrite("CH341AInit: CH341SetStream: OK, ")
      ConsoleWrite("Param: DeviceID="& $CH341Aid & ", Moe=" & $iMode & @CRLF)
   Else
      ConsoleWrite("CH341AInit: CH341SetStream: Error" & @CRLF)
      ConsoleWrite("CH341AInit: FATAL ERROR. EXIT." & @CRLF)
      Exit
   EndIf

EndFunc

Func CH341ARead($i2cAddr, $i2cRegister)
   Local $sBYTE_r4 = DllStructCreate ("BYTE[4]" )
   Local $pBYTE_r4 = DllStructGetPtr($sBYTE_r4)

   Local $aResult = DllCall($CH341Adll, "BOOL", "CH341ReadI2C", "BYTE", $CH341Aid, _
                      "BYTE", $i2cAddr,  "BYTE", $i2cRegister, "PTR",  $pBYTE_r4)
   If  $aResult[0] Then
      ConsoleWrite("CH341ReadI2C.: OK, ")
      ConsoleWrite("Param: $CH341Aid="& $CH341Aid & ", $i2cAddr=" & $i2cAddr & _
         ", $i2cRegister=" & $i2cRegister & _
         ", $sBYTE_r4=" & DllStructGetData($sBYTE_r4, 1) & " ")
      ConsoleWrite("(" & Dec(StringMid(BinaryMid(DllStructGetData($sBYTE_r4, 1), 1, 1), 3)) & ")" & @CRLF)

      Return DllStructGetData($sBYTE_r4, 1)
   Else
      ConsoleWrite("CH341ReadI2C.: Error" & @CRLF)
      ConsoleWrite("CH341ReadI2C.: Param: $CH341Aid="& $CH341Aid & ", $i2cAddr=" & $i2cAddr & _
         ", $i2cRegister=" & $i2cRegister & _
         ", $sBYTE_r4=" & DllStructGetData($sBYTE_r4, 1) & @CRLF)
      Return -1
   EndIf
EndFunc

Func CH341AWrite($i2cAddr, $i2cRegister, $data)
   Local $aResult = DllCall($CH341Adll, "BOOL", "CH341WriteI2C", "BYTE", $CH341Aid, _
                      "BYTE", $i2cAddr,  "BYTE", $i2cRegister,  "BYTE", $data)
   If  $aResult[0] Then
      ConsoleWrite("CH341WriteI2C: OK, ")
      ConsoleWrite("Param: $CH341Aid="& $CH341Aid & ", $i2cAddr=" & $i2cAddr & ", $i2cRegister=" & $i2cRegister & ", $data=" & $data & @CRLF)
      Return 0
   Else
      ConsoleWrite("CH341WriteI2C: Error" & @CRLF)
      ConsoleWrite("CH341WriteI2C: Param: $CH341Aid="& $CH341Aid & ", $i2cAddr=" & $i2cAddr & ", $i2cRegister=" & $i2cRegister & ", =$data" & $data & @CRLF)
      Return -1
   EndIf

EndFunc
This time no pictures.

No comments:

Post a Comment