We're continuing our PiicoDev Servo Driver design journey! In today's episode we write a Hello, World class for the PCA9685 PWM Driver and make a few mistakes along the way. Everything works out well in the end, and we (re)learn some valuable lessons in troubleshooting.

Transcript

Welcome back to Part Two of our series, Design a Product with Us. Last week, we scoped the chip which was the PCA9685 PWM driver chip. We've got it on a breakout board here, just assembled the PWM headers for a Servo. I've connected it to our Raspberry Pi Pico using the PiicoDev connector, just a breakout lead that goes to the header, and we've got a Servo and external battery power so that we can drive the Servo.

We're just going to focus on getting a "Hello World" running on this chip from scratch, so we'll buff up that MicroPython and write, I guess, a simple class to control this PWM driver. I'm not going to build it from the ground up as a PiicoDev project, or rather start how any other maker would by creating an I2C object and sending some bytes over the bus. The stuff that makes it compatible with PiicoDev is more of like administrative wrapping stuff that we can probably do later at the end, but for now we'll just get a "Hello World" running.

So I have a blank working environment in PyCharm. We have the data sheet for the chip. Armed with nothing more than these things, we're going to drive a Servo. Let's get started. We'll begin like every great project does, but let's create a new file, PCA9685. We'll need to init, I know that we'll pass in an I2C bus or an I2C object, so we'll need that. And the default address for the chip is hex 40. We know this because Adafruit have been very generous with how they have laid out this board. There's an address jumper, remember there's six address pins on this device and here are the jumpers. And Adafruit have laid it out such that we can just infer the address from this. We have a one followed by six open jumpers, so that's a one followed by six zeros, which is hex 40. So our class will take in that self.

I2C is equal to I2C. It'll take in that I2C object, self.address will be set to that address value. Now, what? Well, we have to actually invoke our class in some kind of script and I'll just work in the same file for now. We can pull things apart into separate documents later, but for now I think it makes sense to just keep everything together. So, we can write our script. We're working on a Raspberry Pi Pico, so of course from machine import we'll need to import I2C and we'll also need pin. The Pico expansion board for Raspberry Pi Pico uses pins or GPIO 8 and 9 for the Pico connector. So, we'll need to define those pins. SDA is pin 8 and SCL is pin 9. And we'll create our I2C object, that's with C equals I2C. The bus number we're working with is bus zero and SDA equals SDA, SCL equals SCL. Done. Now, we're going to initialize the driver. We'll create an instance of this class, so we'll just say, I guess we'll just call it the driver equals PCA 96.85 and we need to pass in the I2C definition. And actually, that's everything because the address has a default value.

So, to begin with we have a class definition that doesn't actually do anything on the I2C bus yet. It just creates an instance that has an I2C definition and some address and we've just set up our microcontroller with the I2C bus and created an instance of that class called driver. Now, we have to actually interact with the physical hardware. Now, we turn to the data sheet to see how we're going to control this thing. What can we actually do with this chip? Chapter 7.3 registered definitions. This is very standard. We just have basically a summary of all the registers and really it's these first five registers, maybe even the first two registers, that are interesting.

Every other register after that is just controlling some characteristic for the driver it's controlling one of the channels. So, I think our first port of call is going to be this mode register one. A lot of data sheets will often have like an example program or example pseudo code to show you some typical application. You know, they'll say write this value to this register to power on the chip, right this value to this register to configure it the way you might want to configure it. Typically, unfortunately, this data sheet doesn't have that, so we have to be a bit more careful.

If there are any datasheet writers out there, I love it when you include typical application programs in the appendix of your data sheet. Wow, so useful! So here we have the mode one register and there's some really interesting stuff in here. We have a restart bit, but this looks like it's just the status of the restart logic when you power on a chip. It'll go through a power on reset and this is probably just a flag for that. We have the external clock which is default zero to use internal clock, that's appropriate, we don't want to include any circuits that we don't have to. Interesting, that register auto increment is disabled on startup.

We'll probably come back to that later and when we start the device up we are in sleep mode. So, I guess the first thing we need to do is wake up the device. The oscillator is off by default and then there's a few other flags for whether it will respond to sub-address calls, not interested in that right now. So, step one let's wake up the chip, or rather step one let's enable the oscillator. Everything else here looks fine, accept the sleep bit and the auto increment bit. I think we want that to be enabled later on, so enable oscillator, auto increment, and we have self.i squared C so we have access to all. the PWM and we can control the duty cycle by setting the on time and the off time

The I²C methods like right to mem self.i2c dot right to mem, have two arguments: the soft address and the register. We are looking at register zero and want to write a 1 and 0. Michael from the future here, there is a very insidious bug on the screen right now. See if you can figure out what it might be. I press Ctrl R to run the script and nothing happens. This is actually good because the command would have failed if we didn't have everything else correct. This means we were able to configure our microcontroller, initialize our class, and do a successful transaction on the bus. We wrote a 1 and 0 to register zero and setup is complete.

A device oscillator is enabled and we are ready to start driving our servo. Every other register on the device is the PWM Control Data. We have a lot of replicated registers where we have LED zero on and off. It looks like for every PWM channel, there are four bytes to control its behavior. We scroll down to example one, which is as close as the data sheet gets to a really helpful example code. For every PWM channel and LED channel, we have two control parameters: the on time and the off time. Together, they will set the duty cycle, which is how we control a servo. Our oscillator is constantly counting from 0 to 40.95 and is driving the PWM. We can control the duty cycle by setting the on time and the off time.

We have a counter that rolls over to zero when it reaches 4095. This is like one control window for Pulse Width Modulation. When we set our on time, we're setting the number that the counter is at when our signal goes high or our LED goes on. When we set the off time, we're setting the value of the counter for when that waveform goes off. By changing these two parameters, we can control the duty cycle of the waveform.

It seems like the on time is not really necessary for our application, we just want to control Duty Cycles. So, we only need to do that with one parameter, and we can fix it to always be zero. Then, we can control our duty cycle by changing the off time, which would change our pulse width.

We have our control scheme, and we need two numbers: the on time and the off time. They are each defined by two bytes, so we need our four control bytes to set our PWM duty cycle. But, we don't actually know what frequency this is running at. Servers will only accept a pretty narrow range of frequencies, and we have no idea what the actual frequency of this waveform is.

So, we're going to need to set the oscillator or the prescaler. At the bottom of the register map is register FE, and that is the prescaler for the internal oscillator. We need to make sure that we have a compatible frequency. Most servos run between 50 to a couple of hundred Hertz, and so we're going to need to load a value into that register to set the waveform to about something in that range. Luckily, the datasheet provides us a formula. We have the value that we need to load in given by this formula. So, let's just fill it in.

We don't get around to importing math, so we need to round the oscillator clock which is 25 megahertz (25E6) divided by 4096 multiplied by our desired update rate. I'm going to go for 200 Hertz times 200 Hertz and then take one off that. It helps if you don't make a typo or a second time.

We need to load a value of 30 into the prescaler register (Fe). It's super important to note that the prescaler register can only be set when the Sleep bit of mode 1 register is set to logic one. We have to actually set the frequency and the prescaler before we take the device out of sleep mode.

We can take this and put it above our enable call, run the script and it seems to have worked. Now, we need to load a value into the high and low time registers and drive our server. For that, we're going to need a method which I'm going to call PWM. It's a class, so we have self and we want to drive a specific channel of this motor driver. I'll put in a channel argument and for on time, I'll just set that to default to zero and for off time, I can default that to 4095.

We have our two numbers and we want to package them up into a four byte message. My server is connected to Channel Zero, so I can start at address six and we have on low high off low high which is little endian. We want to package in data, so we're going to need to import and pack the data.

We can use the u-struct module to unpack and pack data. We have the format string for pack, which is 'little ndn and then HH for two 16-bit numbers'. We have the data that we want to be packed, which is 'on' and 'off'. This will take our two numbers and package it into four bytes in little endian. We can then write this data to register six and auto increment through 7, 8 and 9.

We can use self.i2c.write_to_mem to write the data. We have self.address, 0x06 and the data. We know that Channel Zero starts from address six, so we don't have to hard code this to the channel we're using today. We can say that Channel One is four bytes down the line and Channel Two is another four bytes down the line. This way, we can write 'on' and 'off' data to any of the channels.

We should now be ready to go. We can make an infinite loop and say 'driver.pwm Channel Zero' and 'off' could be 2000, which is about 50 duty cycle. We can then sleep for a second and say 'driver.pwm 0' and 1000, which is a 25 duty cycle. We will need to import 'sleeper Mass' and we should get a servo that moves back and forth.

Finally, we need to remember that a byte string should look like this: b'\x1e'. This resolves as some number when we use int.from_bytes. If we have one byte, this resolves as one number.

We get some hot garbage from whatever the hell I've written, so that's one problem. We still have nothing but at least we're writing meaningful data to our chip. Great start! Let's just check out sanity real quick and start the program off with a reset of mode 1 register. So that would be Bridge the zero zero and we want zero zero. Then we want to put a device to sleep to set so that we can set the oscillator because here we were assuming that it was asleep but we need to make it a sleep so we need one two three four one zero. And it looks like this line here where I had one zero before I thought I was doing the auto increment I'm not sure what's happened there but that's all this is doing is putting it to sleep.

So we're going to do a hard reset and we're going to put it to sleep or disable oscillator then we're going to pre-scale the oscillator to 200 Hertz. And this I mean this is clearly wrong because here I use the same code to disable the oscillator so to enable the oscillator and auto increment so we only want to set bit five on bit four we want to set off so we want to set this to hex 2 0. Yeah I guess there is nothing for it but another Moment of Truth. There have been about like three or four off camera moment of truths but let's give this one a bell and there you have it we have we have a Hello World. We can drive this device it's not much at the moment we're using let's turn this off. It's not much at the moment we're using these naughty little byte strings to just write constant values. It would probably it would absolutely be better practice to read the contents of a register.

We are going to set the bits we want to set and then write it back using masking and bit logic. This is enough to start with. Now, I'm going to implement some real quality of life improvements so that we don't have to do silly things like use byte strings, etc. We can probably time lapse this out and I'll see you in a few minutes.

One of the quality of life improvements is internal read and write methods. We don't want to have to constantly call self.i2c.write_to_mem, self.address, the register, and the value. In the case of the reset mode, we just want to write to register zero the value 0. So, we have a method for write that wraps the write_to_mem method and a similar method for read which wraps the read_from method.

I have checked that things are still working and we are writing the correct data. This means that we can remove the commented out commands and make the code much more readable.

We may need to tune the frequency as the server is constantly vibrating. The PWM values may be fine, but we may be sending the wrong frequency. In the course of fixing that, we may as well write a real frequency setting method.

I think that's probably a good place to wrap up. We have a pretty clear direction of some housekeeping improvements to make helpful functions for things like setting the frequency and maybe even a method into something that's more of a Servo angle method. This is because we want to control a Servo angle, not have to think in terms of the raw pulse width variable.

In this episode, I probably didn't do the best troubleshooting that I could have. Looking back, I should have busted out a logic level analyzer, perhaps should have looked at some waveforms on a scope, or even just reading back values from registers to make sure they were being set correctly would have been a really good start in debugging. But hey, I've made these mistakes so you don't have to.

If you've made it this far through the episode, you're clearly one of the top Factory fans. This bit's for you. We're going to put firmware development to the side for a little while and work on the PCB in the next episode. Now we've already got a couple of ideas of how we're going to do this and we're going to share them in a forum post so you can weigh in whether you think they're good inventive ideas or terrible standard breaking ideas. I really look forward to hearing your thoughts and this could be your chance to help us make this product exactly how it ought to be.

As always, if you have any questions leave us a comment below or on our forums and I look forward to seeing you in the next Factory episode where we will begin designing PCB. Catch you next time!

Comments


Loading...
Feedback

Please continue if you would like to leave feedback for any of these topics:

  • Website features/issues
  • Content errors/improvements
  • Missing products/categories
  • Product assignments to categories
  • Search results relevance

For all other inquiries (orders status, stock levels, etc), please contact our support team for quick assistance.

Note: click continue and a draft email will be opened to edit. If you don't have an email client on your device, then send a message via the chat icon on the bottom left of our website.

Makers love reviews as much as you do, please follow this link to review the products you have purchased.