Total structure
This blog post is also as PDF document available.
After building the mechanics and the functional tests of the hardware in Part 1, let's start today with the programming of a game variant and a look behind the scenes. Let us take a look at how a servo works and what contribution our servo driver module PCA9685 makes. You will also learn how to build your own MicroPython module from the information from the data sheet. So welcome to a new episode from the series.
MicroPython on the ESP32 and ESP8266
today
The Joy Ball Wizzard (Part 2)
I have put together an overview of the required parts for the mechanics and the hardware here. A detailed description for the assembly can be found together with the circuit diagram of the electronics and the hardware description, in Part 1.
Material for mechanics
4 |
Poplar plywood of 8mm; 180 x210mm |
2 |
Wooden bar 15x19mm; 180mm as a support |
4 |
Plywood strips 8mm; 15mmx180mm beveled on one side |
2 |
Leather strips approx. 1mm thick; 30x180mm as a hinge |
4 |
Wooden dowel 6mm Ø |
1 |
flat printed circuit board 150x180mm |
2 |
Plastic or aluminum angle 15x25mm; approx. 100mm long |
4 |
Spax screws 3x10mm |
12 |
Spax screws 3x20mm |
1 |
Steel ball 8..10mm |
various |
Soldering nails 8mm and 10mm |
approx. 2m |
Silver plated copper wire |
some |
Contact adhesive and wood glue |
2 |
2-wire cable approx. 30cm |
2 |
2-pole pin header |
2 |
|
2 |
Screws or pins optional: with ball bearing |
Tool
Screwdriver, stitch, tread saw, drill 2mm Ø + 6mm Ø, small flat pliers, soldering device
Illustration 1: Plat structure with servos
Hardware
Illustration 2: Joy Ball Wizzard circuit
There is a greater representation Here as a PDF document.
How to control a servo?
Other question: Why does an electric motor simply stand in a certain rotary position?
A servomotor, for short servo, consists of an electric motor that drives a lever arm via gearbox reduction. Why this does not carry out a constant rotary movement is because a potentiometer is coupled to the axis of rotation, i.e. an adjustable resistance. The rotary movement stops when the signal from the servo input line matches the signal caused by the potentiometer.
However, no variable DC voltage is transmitted via the input line (yellow or orange) of the servo, but impulses of certain length. The pulse duration is between 0.3ms (right -hand stop or 0 °) and 2.3ms (left stop or 180 °) and is repeated in the rhythm of 20ms. Within this period, one could therefore, with just one timer through a cleverly staggered time control via software, to control up to eight IO-pins servomotors independently of one another. This is done by the microcontroller with the ATMega328 in this form via a corresponding library. I have already implemented something like this in AVR assembler. Exciting thing!
Illustration 3: Servo pulse sequence
Our servo control with the chip PCA9685 works in a similar way by switching on and off per channel within 4096 possible times. We specify the times by writing their numbers in the appropriate registers. The PCA9685 does the rest. The building block actually comes from the LED corner, but is also ideal for controlling servomotors. That data sheet offers NXP for download. On this basis, I programmed a MicroPython module with the class PCA9685. We will deal with this in a moment, after I go into a few more details about the chip.
The PCA9685 is controlled via I2C and offers 6 address lines A0 to A5 for addressing, which can be curled by soldering bridges and thus allow a very flexible selection of a hardware address from 62 possible. We use the basic address 0x40. The inputs are 5V resistant, so the module can also be operated on an ATMega328 microcontroller. Each of the 16 outputs is controlled by two registers in time behavior. Because a value of 0 to 4095 is possible for each switching process, we need two bytes to store it. The PWM frequency is derived from the internal 25 MHz clock and can be set together for all outputs between 24Hz and 1526Hz. The pulse width of the output signal (AKA Duty Cycle) can be programmed in a gradation from 0 (0%) to 4095 (100%) to 4095 (100%). Because the start time of the starting powder and its waste time can be set independently, not only the pulse duration with a resolution of 1 / (PWM_Frequency * 4096) can be adjusted, but the pulse can also be moved freely within the period. For the potential expansion of the game through additional servo, the module is therefore first step, especially since the GPIO pins of the ESP8266 are used otherwise.
I did a very uncomfortable discovery in the ESP32 with the firmware versions 1.17 and 1.18. There, the PWM control only works from 700Hz for an inexperienced reason. This is completely off the row for servos and the reason why I chose the PCA9685.
The PCA9685 class
Let's move on to the PCA9685 class. The class definition begins with the keyword as usual class and the name PCA9685, the body of the class is introduced. A number of constant definitions follow, which I do not go into individually. It is the name of registers and flags. All start with a capital letter, the identifiers in the program can be recognized as a name for constants.
Each class needs a so-called constructor, according to the construction plan of which objects of the class can be derived. The constructor in a MicroPython class is a method with the fixed name __init __ (). Later, on the call, then instead of __init__ the name of the class is used here, so here PCA9685.
def __init__(self,I2C, hwad=None, oe=None, freq=None):
self.I2C=I2C
IF oe is need None:
self.oe=Pin code(oe,Pin code.OUT,value=1)
Else:
self.oe=None
IF hwad is None:
self.hwad=Hwad
Else:
self.hwad=hwad
self.ResetDevice()
IF freq is need None:
self.freq=freq
Else:
self.freq=Servo_freq
self.begin(self.freq)
self.Setautoinc()
print("Constructor PCA9685, HWADR =",hex(self.hwad))
The parameter and that Prefix self represents a reference to the object generated later. So that the remaining parameters I2C, hwadr, oe and freq the methods of the class are available, the objects handed over are handed over to instance variables, that start also with the opening credits self. These variables are within an object that is derived from the class, global, but in relation to several objects of the same class local.
That None references a simple but also interesting data types for which there is only one instance that nothing. In our parameter list, this means that for example for hwadr, nothing is handed over by default if the parameter does not receive any value in instantiation. The reference is not assigned to a (numerical) value, but the reference to the instance of nothing. This procedure allows us to react flexibly to certain situations through the IF constructs. In our special case, the class user does not have to know which hardware address of the PCA9685 or which PWM frequency is necessary for a servo. This is pre-assigned by the constant and this value is used in the constructor if there are no wishes.
An instance servo of the class PCA9586 would now be created most simply as follows. Of course, this assumes that the I2C object I2C has been previously instantiated.
servo = Servo(I2C)
The method resetDevice() does two things. It checks whether a PCA9685 device is on the bus and resets all such modules. The General Call Address 0x00 is used as a hardware address and the reset code 0x06 is sent as second byte. The method should report 2 as a return code, this is the number of those received on the bus ACK bits. The procedure is described on pages 7 (below) and 8 (above) of the Data sheets.
>>> from machine import Pin code, I2C
>>> from PCA9685 import Servo
>>> sclpine=Pin code(5)
>>> Sdapin=Pin code(4)
>>> I2C=I2C(scl=sclpine,sda=Sdapin,freq=400000)
>>> servo=Servo(I2C,oe=2)
>>> ResetDevice()
It looks like this on the bus pipes SCL (yellow) and SDA (blue):
Illustration 4: Reset command on the I2C bus
The resting levels on both lines are logical 1 or electrically 3.3V, caused by the pull-up resistors. A transfer is initiated by the start condition. This is a falling flank on the SDA line, while SCL is on high. Then the Master (ESP8266) puts the MSbit of the hardware address on SDA (here 0) and pulls SCL on low. With the following rising flank on SCL, it signals the slave (PCA9685) that the database is valid and to be taken over. This is repeated with the other 7 bits. During a ninth clock, the slave can now pull the SDA line to low. This is referred to as the Acknowledge (ACK) and tells the master that the byte sent was accepted. However, the ACK is not a confirmation that the byte has arrived in an unadulterated manner. With a NACK, not acknowleded, SDA remains HIGH, the slave says that he has received all the necessary bytes, or cannot receive any signs at the moment. After the hardware address, the master sends the reset code 0x06, which is also confirmed by the slave with ACK. Until the bytes are processed, the slave now pulls the SCL line to low. After the line has gone to high, the master informs the slave the end of the transmission through a stop condition. This is the rising flank on SDA while SCL is HIGH. Now both lines are back in rest.
The regular hardware address of the PCA9685 must be used for the remaining writing and reading arms on the bus. We create three basic methods for this purpose.
def writereg(self,reg,vala):
buf=bytearar(2)
buf[0]=reg
buf[1]=vala
self.I2C.writeto(Hwad,buf)
In order to write a value in a register of the PCA9685, we need three data bytes, the hardware address HWADR, the registration number reg and of course the data value val. The method i2c.writeto() demands the hardware address and a buffer that is based on the bytes protocol. This doesn't apply to the handed over the handed over reg and val. We therefore create a bytearray-object with two elements and assign the registration number and the data value. Then we send everything over the line.
The three bytes are acknowledged with ACK (Acknowledge = OK) from the PCA9685 (SDA = 0). The 7-bit hardware address 0x40 is pushed by a bit to the left, the write bit (SDA = 0) is inserted as an LSBit and then the two bytes are clocked from the buffer on the line. From the oscillogram we also recognize that the effective clock rate is not 400kHz but only 150kHz (9 clock impulses in 60µs).
Illustration 5: Describe the 0x06 tab with 0xA4
def readreg(self,reg):
buf=bytearar(1)
buf[0]=reg
self.I2C.writeto(Hwad,buf)
buf=self.I2C.readfom(Hwad,1)
return buf[0]
The reading command for a register takes place in two stages. First, the tab address is sent in a writing command with the hardware address. After that, the hardware address, this time with a reading bit as an LSB, is sent again and as a result of this, the PCA9685 answers with the content of the register that the ESP8266 now acknowledges with ACK. It knows that only one byte comes back, so the reception buffer is only 1 byte long.
def Writetoregs(self,reg,buf):
buffer=bytearar(1)
buffer[0]=reg
buffer+=buf
NOA=self.I2C.writeto(Hwad,buffer)
return NOA # Number of Acks
If several bytes are to be sent, for example to write the start and end time for PWM pulses, then we simply put the registration number in the Bytearray together with the handed over. The PCA9685 is configured in such a way that it independently counts the reception registration number with every data-byte received (self.setAutoInc() in the constructor).
A special procedure is necessary if the PCA9685 is to be reset during operation after we have put it asleep. That makes the routine doRestart() according to the requirements in data sheet on page 15.
Quote:
To restart all of the previously active PWM channels with a few I2C-bus cycles do the
following steps:
- Read MODE1 Register.
- Check that bit 7 (Restart) is a logic 1. if it is, clear bit 4 (sleep). Allow Time for
oscillator to stabilizer (500µs).
- Write Logic 1 to Bit 7 of MODE1 Register. All PWM Channels wants to restart and the
RESTART bit will clear.
def start start(self):
vala=self.readreg(Mode1)
IF vala & Restart:
vala &= 255-Sleep
self.writereg(Mode1,vala)
sleep_us(500)
vala |= Restart
self.writereg(Mode1,vala)
The connection is explained as follows. Setting the Sleep-Bits in register Mode1 send the PCA9685 to sleep. This can be done by the method setSleep() happened to which we will come later. At the same time, the bit will also be Restart in Mode1 set. The positions of the servos are now frozen and no longer react to new values that we send to the PCA9685. However, new values are written in the position register. The algorithm in doRestart() unlock the lock by Sleep-Bit extinguishes, the oscillator moves up and finally that Restart-Bit by writing a 1 deletes. If no change in the position register has been made in the meantime, the servo remains in its position. But if you have sent a new position, it will be approached.
Our goal is to make the PCA9685 to send tax impulses to the servos. That does the method writePulse(). It takes the number of the servo, as well as the numbers of the time discs for switching on and switching off.
# Times relative in 0 .. 4096 counts
def writ powder(self,NBR,on,off):
assert 0 <= NBR <= 15
off=min(off, 4095)
on=min(Max(0,on),off)
buffer=bytearar(5)
buffer[0]=NBR*4+LED base
buffer[1]=on & 0xff
buffer[2]=(on>>8) & 0xff
buffer[3]=off & 0xff
buffer[4]=(off>>8) & 0xff
NOA=self.I2C.writeto(Hwad,buffer)
return NOA
With assert let us check the servo number and then limit the start and end values to the range between 0 and 4095. A buffer with 5 elements is fed with the register address and the position values dissolved in bytes. The position register start at LED base, that's the register 0x06. Four registers are to be fed up, the notation is Little Endian, so the LSB comes first. Then everything is ready, the hardware address and the buffer go over the line. A return value of 5 tells us that 5 bytes from the PCA9685 were acknowledged with ACK.
Use two more methods writePulse(). The method writePulseFromWidth() also works with the relative time discs 0 to 4095. Servo number and start time are passed on, the time to switch off must be calculated, the plausibility control takes over writePulse().
# Times relative in 0 .. 4096 counts
def Write pulse fromwidth(self, NBR, on, Width):
off=on+Width
NOA=self.writ powder(NBR,on,off)
return NOA
writePulseTimeSlice() is the method of choice for our game. With the parameter run let us enter the time duration for the impulse that is placed on the servo control line. It can take 0.3ms up to 2.3ms. The start time is marginal. From the set PWM frequency the period duration is calculated in microseconds and thus the length of a relative timer. The relative values then go to the method writePulse().
# Times relative in µs; Servo-Freq in Hz
def Write PulseTimeslice(self,NBR,begin,run):
timelimit=(1/self.freq)*1000000
Timeslice=timelimit/4096 # µs/cnt
assert 0 <= begin <= timelimit
assert run >= 0
assert begin+run <= timelimit
from=intimately(begin/Timeslice)
until=intimately((begin+run)/Timeslice)
NOA=self.writ powder(NBR,from,until)
return NOA
A few service routines complete the PCA9685 class.
def Setautoinc(self,AI=True):
vala=self.readreg(Mode1)
IF AI:
self.writereg(Mode1,vala | Autoinc)
Else:
self.writereg(Mode1,vala & (~Autoinc) & 0xff)
In MicroPython it is only possible with tricks to get bit of a bytes. So we have to take it in hand. Two procedures lead to the goal. servo.AutoInc, for example, has the value 32 = 0B00100000. For the negation ~ servo.AutoInc we expect the value 0b1101111 = 223, but get -33 = -0b100001. but 255 - servo.Autoinc provides what we want and as well ~ servo.AutoInc & 0xff. The latter can be explained from the fact that -33 = 0xffffffddf. By unwound with 0xFF we receive 0xDF = 0B11011111.
So we read the Mode1 register and write it with set or deleted AutoInc bit back.
def setsleep(self,Fashion=True):
vala=self.readreg(Mode1)
IF Fashion:
vala = vala | Sleep
Else:
vala = vala & (~Sleep & 0xff)
self.writereg(Mode1,vala)
To change the PWM frequency, it is necessary to set the Sleep-Bit beforehand (default) and then delete (with mode=False). That is exactly what setSleep().
start() if the PWM frequency for our servos relies on the default value of 50 Hz (without parameters in the call) or the frequency is set to the transferred value. The value is written in the Prescaler register. We wait 500µs until the new setting works stably.
def begin(self,freq=50):
IF self.oe is need None:
self.oe.value(0)
self.frequency(freq)
sleep_us(500)
For the calculation of the Prescal value, the page 25 of the Data sheets the following formula:
PS = intimately (PCA_System cycle / (4096 * PWM-frequency)) - 1
We also receive the note:
This is exactly what the frequency() method does. If called with parameter, this value is written into the PRE_SCALE register 0xFE, if allowed. Called without parameter we get the currently set value of the PWM frequency.The PRE_SCALE register can only be set when the SLEEP bit of MODE1 register is set to to
logic 1.
def frequency(self,freq=None):
IF freq is need None:
assert 24 <= freq <=1526
self.setsleep()
self.freq=freq
PS=(Xtal//(4096*freq))-1
self.writereg(Pre_scale,PS)
self.setsleep(False)
Else:
return Xtal/((self.readreg(Pre_scale)+1)*4096)
The entire code of the module pca9685.py you can download, like the other modules.
The game program
It is best to start with the upload of the four modules ads1115.py, oled.py, SSD1306.PY and pca9685.py In the flash of the ESP8266. Then we want to see how the program joyball.py is working.
The imports bring us the methods from the classes into our name room.
import OS,sys # System and file instructions
from machine import Pin code, reset, I2C
from time import sleep, Ticks_ms
from PCA9685 import Servo
from ads1115 import Ads1115
from OLED import OLED
import ESP
ESP.Osdebug(None)
import GC
GC.collect()
The I2C bus is used three times, so we declare its instance in the main program. And not in every single module.
sclpine=Pin code(5)
Sdapin=Pin code(4)
I2C=I2C(scl=sclpine,sda=Sdapin,freq=400000)
The outcome for Output Enable -OE the PCA9685 and the sensor inputs are determined.
oepin=Pin code(2,Pin code.OUT) # D4
border=Pin code(12,Pin code.IN,Pin code.Pull_up) # D6
gate=Pin code(14,Pin code.IN,Pin code.Pull_up) # D5
target=Pin code(13,Pin code.IN,Pin code.Pull_up) # D7
new start=Pin code(0,Pin code.IN,Pin code.Pull_up) # D3
The instances for the engine control, for the ad conversion of the joystick voltages and for the OLED output are generated.
servo=Servo(I2C)
joy=Ads1115(I2C,0x48)
D=OLED(I2C,128,32)
The declaration of some variables is the definition of the interrupt service routine (aka ISR), bump(), which is called when the ball touches the sensors.
specification=2000
point=specification
time factor=50
# Declaration of global variables
trigger=True
gameover=False
begin=0
Various many points can be deducted for contacts with the sensors. To prevent another IRQ (Interrupt Request) from being triggered immediately, we first disable the handler, i.e. the routine bump(), by setting the parameter handler to the big nothing. trigger we put on True, so that the main loop can react to the event. trigger and points must be specified as globally, because otherwise the change in the values does not Scope the function can work outside.
def bump(pin code):
global point,trigger
IF pin code==gate:
point-=10
gate.IRQ(handler=None)
IF pin code==border:
point-=10
border.IRQ(handler=None)
trigger=True
The function goToOrigin() does everything that is necessary to start a round of the game. The ISR is not set to the big one to return the ball to the start, and no IRQ should be triggered. The display informs us that the return takes a few bars. Then we put the edge trigger on true, output the start-up aid and wait for the click of the joystick button.
Once this is done, we fill up the points account, or not, then it continues with the previous score. The start report is issued, the sensors are sharpened and the time measurement started. As soon as the ball touches the edge or a gate, the entrance on the ESP8266 goes from 3.3V to 0V. The falling flank triggers the interruption request and the routine bump() is called.
With the start of the main program, we measure the value of the supply voltage. This can be important if we as the energy source do not use the 12V connector power supply, but a battery. We get the measured value with the welcome report on the display. After 5 seconds goToOrigin() for an orderly further start.
joy.setcannel(2)
Uservo=2*joy.getvoltage()
D.clearall()
D.writer("Joy-Ball-Wizzard",0,0,False)
D.writer("Wellcome!",0,1,False)
D.writer("U-Servo: {:. 2f} V".format(Uservo),0,2,True)
sleep(5)
Gotoorigin()
In the main loop, we first set the ADC channel to 0, fetch the X-value from the joystick, which we reduce to 12 bits, narrow down and transform it to the time range of the servo. The same happens with channel 1 for the Y value. Then the servos are assigned the corresponding impulses via the PCA9685.
while 1:
joy.setcannel(0) #
X=Max(min(joy.GetConvresult()>>3,3400),0)
pulsex=joy.transform(-X,-3400,0,300,2100)
joy.setcannel(1) #
y=Max(min(joy.GetConvresult()>>3,3400),0)
pulsey=joy.transform(y,0,3400,350,2350)
servo.Write PulseTimeslice(4,0,pulsex)
servo.Write PulseTimeslice(7,0,pulsey)
D.writer("Score: {}".format(point),0,2)
After the output of the score, we treat special events. trigger is True when an IRQ has been triggered. In this case we wait 50ms and then switch the sensors sharply again.
IF trigger:
sleep(0.05)
gate.IRQ(handler=bump,trigger=Pin code.Irq_falling)
border.IRQ(handler=bump,trigger=Pin code.Irq_falling)
If the joystick button has been pressed during a round of play, this triggers a restart of the ESP8266 and all values are reset. However, the game can only really restart if the program as boot.py was uploaded to the flash. You can find information on this in of these instructions.
A game round is over when the ball ends up in the target bracket and target so goes to 0. We sit gameover on True and find the game time. If we were faster than 15 seconds, the remaining seconds are 50 as plus points. According to the corresponding deductions when crossing the time limit. The score then results from the sum of the points from the start depot and the times. The score is communicated and with a click of the joystick button it goes into the next round.
IF target.value()==0:
gameover=True
length of time=intimately((Ticks_ms() -begin)/1000)
timepoints=(15-length of time)*time factor
score=point+timepoints
point=Max(0,point)
print(point, timepoints, score)
D.clearall()
D.writer("Game Over",0,0, False)
pnts=strand(score)
D.writer("Score:"+pnts,0,1,False)
D.writer("New Game-> Button",0,2)
while need new start.value()==0:
passport
sleep(0.3)
Gotoorigin()
Outlook
Now it is up to you what else you do from the game. The field can be changed by replacing the top plate with the board. Further gates can be inserted. You can also carry out experiments with the control by changing the values for the time discs. And one or more other servos can be used to create variable obstacles. Just let your imagination run wild.
I wish you good luck and lots of fun. In the next episode, we will move the control from the joystick to wireless heavy power control. The connection is then established via WLAN and the UDP protocol. Let yourself be surprised until then!
2 comments
Pontifex42
Habe nun die Konstruktion abgeschlossen. Probedruck erfolgreich, funktioniert recht zufriedenstellend. Foto des ersten Drcuks ist dabei.
Das Publishing bei Thingiverse ist leider buggy as hell. Wenn es da Schwierigkeiten beim finden und downloaden gibt, bitte Bescheid sagen, ich ziehe dann nach printables um.
Pontifex42
Ich habe auch für dieses Projekt wieder ein paar 3D-Teile konstruiert, die das arbeiten mit Holz ersparen.
Zu finden hier:
https://www.thingiverse.com/thing:5454459
Da fehlt noch die obere Platte, ausserdem werde ich noch etwas auf Materialersparnis optimieren.
Bitte noch 3 Tage Geduld.