-
1Using PWM with Arduino
Alright, we have two goals in this tutorial, just like in the parallel SDK and Lua tutorials: we're going to learn how to use PWM to fade an LED, and we're going to use I2C to communicate with a chip. Make sure to read the details section first if these terms don't make sense to you; I go into a detailed description of how PWM and I2C work.
PWM is very easy in the Arduino environment. Arduino has a built-in function called analogWrite() which creates a PWM output on pins which support it. Again, for all these examples I will be using an Adafruit HUZZAH Feather, but they will work just fine with any ESP board. I recommend avoiding the ESP-01 for development. Use something like the ESP-12F instead, it's much easier. On the Feather, we will be using GPIO 0 and 2, which are the two on-board LEDs. On other boards, just check the documentation to see what pins have LEDs on them. You can, of course, attach an external resistor and LED if you want.
The analogWrite() function takes two parameters: the pin we want to use for PWM output, and the duty cycle value. The duty cycle is a 10-bit value, from 0-1023. To get a specific percentage, just multiply the percentage you want by 1024 (the number of values). If we wanted 30% duty cycle, for example, we would use:
Note, however, that 30% duty cycle is not the same as 30% brightness. The relationship between current and brightness is non-linear in LEDs. This means most of the dynamic range of an LED's brightness is near the low end of the 10-bit scale. There are many ways to scale the output to make it look smoother to the human eye. We will take a quick look at this later on.
We're going to use a very simple state machine to fade our LEDs up and down. The state machine can have two states: increasing or decreasing. The duty cycle increases from 0 to 1023. When it hits 1023 the state changes to decreasing, and it heads back down to 0. This loops indefinitely. It's one of the simplest ways to fade an LED! Let's take a look at the code.
-
2The "Simple Fade"
Our first test program will be using a very simple state machine algorithm, which, as described, simply keeps track of the fade direction and modifies the duty cycle as required.
/* * This sketch is written by MrAureliusR (Alexander Rowsell) * It it released under the terms of the Mozilla Public License v2 (https://www.mozilla.org/en-US/MPL/2.0/) * It is part of the ESP8266 Tutorial Series on Hackaday! Make sure to read the accompanying article. * * We will be using the built-in analogWrite function to do our PWM for us! * */ enum {DECREASING = 0, INCREASING = 1}; int dir = DECREASING, stepSize = 1, curStep = 0, led = 2, delayTime = 10; void setup() { Serial.begin(115200); pinMode(led, OUTPUT); } void loop() { if(curStep == 0) dir = INCREASING; else if(curStep >= 1024) dir = DECREASING; if(dir == INCREASING) { analogWrite(led, curStep); curStep += stepSize; if(curStep >= 1024) curStep = 1024; } else if (dir == DECREASING) { analogWrite(led, curStep); curStep -= stepSize; if(curStep <= 0) curStep = 0; } Serial.print("Current step: "); Serial.println(curStep); delay(delayTime); }
To make things simple, we declare a few variables for things like the LED pin, the current duty cycle, the amount to change the duty cycle in each loop, and how long to delay at the end of the loop. I've used an enum instead of #defines for increasing and decreasing. This is a good habit to get into, but it doesn't really matter which you use for such a small program.
We also start the serial output up, so we can check the progress of the fade, though this can be commented out and is mostly just for debugging. Even beginners should be able to grasp what is happening in this code. If you have questions or are confused, feel free to ask in the discussion below, or join us on Freenode IRC in ##esp8266.
Next, we'll take a look at a little trick to make the fade look a bit smoother.
-
3The "Smooth Fade"
The human eye does not perceive light output linearly in relation to LED current. What this means is to get twice the perceived brightness, you often need to use about four times the current. LEDs function most efficiently below their max current, so making the most of the dynamic range we have is essential. Mike Harrison of mikeselectricstuff explains this really well in this video. The simplest way to do this is to take the square of the output we want. The square root of 1024 is 32, but I take the input up to 50 which gives us a bit more time at max brightness. Compare this with the previous sketch and see which one looks better to you!
/* * This sketch is written by MrAureliusR (Alexander Rowsell) * It it released under the terms of the Mozilla Public License v2 (https://www.mozilla.org/en-US/MPL/2.0/) * It is part of the ESP8266 Tutorial Series on Hackaday! Make sure to read the accompanying article. * * We will be using the built-in analogWrite function to do our PWM for us! * */ enum {DECREASING = 0, INCREASING = 1}; int dir = DECREASING, stepSize = 1, curStep = 0, outStep = 0, led = 2, delayTime = 50; void setup() { Serial.begin(115200); pinMode(led, OUTPUT); } void loop() { if(curStep == 0) dir = INCREASING; else if(curStep >= 50) dir = DECREASING; if(dir == INCREASING) { outStep = pow(curStep, 2); if(outStep >= 1024) outStep = 1024; analogWrite(led, outStep); curStep += stepSize; if(curStep >= 50) curStep = 50; } else if (dir == DECREASING) { outStep = pow(curStep, 2); if(outStep >= 1024) outStep = 1024; analogWrite(led, outStep); curStep -= stepSize; if(curStep <= 0) curStep = 0; } Serial.print("Current step: "); Serial.print(curStep); Serial.print("\tOut step: "); Serial.println(outStep); delay(delayTime); }
Again we have serial debugging just so we can easily see where in the cycle we are. Now that we've mastered PWM, let's move on to the slightly more complex I2C serial bus. Make sure you've fully read the details section so you have a basic understanding of how the I2C bus works.
-
4I2C Comms with the Wire Library
As discussed in the details section, I2C is a multi-master, multi-slave bus. However, in practice, it's almost always a single master system with one or more slaves. Arduino makes I2C comms quite simple; they provide a library called Wire which takes care of all the low-level stuff for us.
Today we'll be demonstrating the DS3231 real time clock chip, made by Dallas and now owned by Maxim. It's very important to know how a chip works before we try and communicate with it. This chip, like many I2C devices, has a set of registers which you access directly via reads and writes to certain addresses. (Don't get these addresses confused with the address of the chip itself on the I2C bus -- these are different things!) In the case of the DS3231, we have registers which define the current date and time, a set of registers for the two alarms, and some system registers which control things like whether the clock is running, what kinds of outputs we want on the auxiliary pins, that sort of thing. Here's the register map from the datasheet:
So the register addresses start at zero, and go up to 0x12 (hexadecimal 12, decimal 18). We're really only concerned with addresses 0x0-0x6, the time and date registers. The DS3231, like many real time clocks, stores the time and date as binary-coded decimal. This means that you read the hexadecimal number as a decimal number. For example, the time 12:24 would be encoded as 0x12 0x24, instead of the value of those digits, which would be 0x0C 0x18. I suppose at one point there was a good reason for this, but for us it just makes things difficult because we need to convert between the two. I've gone with the speed over size method of using a lookup table. If you want to read more about this, there's a wealth of information on BCD out there.
In this example, we're going to ask the user for the time and date over serial, and then program those values into the DS3231. The power-on defaults for the control registers are fine; they have the chip set to run the clock and that's all we really want at this point. After this, it will read the time every second and print it to the serial terminal so we can be sure it's all working.
For convenience, we've defined the 7-bit address of the DS3231 as DS3231addr. Below this is the BCD table: if you put in 10, you get 0x10 out. There are more elegant solutions, but this one is simple and bulletproof. Following that, we have the simplest function possible for the decimal to BCD conversion:
uint8_t dec2bcd(uint8_t dec) { return bcdtbl[dec]; }
You'll notice we're using a strange variable type you may not have seen before. These types are defined in the header file called <stdint.h>, and they are used when we want a variable that is a certain number of bits wide. There are a bunch of options for signed and unsigned numbers. In our case, we want an unsigned 8-bit number, so we use uint8_t (unsigned integer, 8 bits-wide type). The reason we do this is to ensure that when we send data to the DS3231, it's all in 8-bit chunks at a time. The Wire library might take care of this under the hood, but just to be safe I always specify bit sizes for serial communications. This is also why our bcdtbl is declared as a uint8_t.
The setup function simply asks the user for the date and time, using the Serial.parseInt() function. This waits for the user to type a number, followed by the enter key (technically any non-digit character will work). It then gives us a nicely formatted integer, so we don't have to deal with converting the ASCII text the serial terminal gives us into an integer. Remember that "10" and 10 are different things! The characters for "10" are actually hexadecimal numbers 0x31 and 0x30, which is not what we want. This is where Arduino shines for both new and old programmers: you can cut out tedious tasks like this and just get to coding. I personally believe you should also be able to do what the library does, but that's way beyond our current aims.
Note that we pass the result from Serial.parseInt() directly into dec2bcd(). We could do this in two lines, but doing it all in one is much cleaner and simpler. Passing the result of one function into another is a great trick to keep your code smaller and neater.
After we have all the values from the user, we then get ready to pass these to the DS3231. This is where the Wire library comes in. I recommend checking the official Arduino documentation for the Wire library if you want to see a more detailed description of these functions. Before we do anything with the Wire library, we have to initialise it with a call to Wire.begin(). This will set the GPIOs for the I2C to the correct output type of open-drain. Recall that by default this uses GPIO 4 & 5. These are marked as SCL and SDA on the Adafruit HUZZAH.
Notice that we pass Serial.parseInt() directly to dec2bcd(). The astute among you might notice that Serial.parseInt returns an int, which is technically a int32_t, incompatible with the uint8_t that dec2bcd() requires. Normally you would use a "cast" here to change the type, but in our case we know the user has entered only a 2-digit number. If the user entered a value more than 255, it could cause problems. Oddly, Arduino doesn't give us a warning about this: most compilers would. If you're interesting in learning about casts in C, I highly recommend the book C in a Nutshell! After calling Wire.begin(), we can at any point call Wire.beginTransmission(). This function takes the 7-bit address of the device we want to communicate with as an argument. It then waits for the program to buffer data to send to the device. Data is written to the buffer using Wire.write(). After you have buffered all the data you want to send, you can then initiate the transmission with Wire.endTransmission(). Arduino takes a different approach than most I2C libraries, in that it buffers all the data before starting the transmission. This could potentially affect the way your program runs, but in most cases it works just fine.
In our case, the data we want to write starts with the address of the register we want to start writing at. We want to start at register 0, so the first piece of data we send is simply 0x00. After that, we send each piece of data, and the internal register pointer automatically increments. This means we can just send data in a row without having to adjust the register pointer in between each byte of data. This is how almost all I2C devices work.
When reading from an I2C device, the Arduino Wire library also uses a buffering system. You initiate a transmission in the same way as before, using Wire.beginTransmission(). We have to tell it where to start reading from, so again we write the byte 0x00. Then, we use the Wire.requestFrom() function, which takes the address of the device, the number of bytes we want to read, and whether or not to send a STOP condition after the request. Finally, we use the Wire.endTransmission() function to start the communication process. After this, we can read the bytes out of the data buffer using Wire.read(). In our example program below, we simply use a for loop to print all the bytes to the screen in hex format.
#include <Wire.h> #include <stdint.h> /* * This sketch is written by MrAureliusR (Alexander Rowsell) * It it released under the terms of the Mozilla Public License v2 (https://www.mozilla.org/en-US/MPL/2.0/) * It is part of the ESP8266 Tutorial Series on Hackaday! Make sure to read the accompanying article. * * I2C Communication * In this example, we will be communicating with a DS3231 real time clock and temperature sensor * */ #define DS3231addr 0x68 uint8_t bcdtbl[100] = { 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9,\ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19,\ 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29,\ 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,\ 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,\ 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,\ 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,\ 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,\ 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,\ 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99 }; uint8_t recvByte; uint8_t dec2bcd(uint8_t dec) { return bcdtbl[dec]; } void setup() { Serial.begin(115200); // start serial communication Serial.setTimeout(30000); Serial.println("Please enter the year in the YY format:"); uint8_t year = dec2bcd(Serial.parseInt()); Serial.println("Please enter the month in the MM format:"); uint8_t month = dec2bcd(Serial.parseInt()); Serial.println("Please enter the day of the month in DD format:"); uint8_t dom = dec2bcd(Serial.parseInt()); Serial.println("Please enter the day of the week from 1-7 (1 is Sunday, 7 is Saturday)"); uint8_t dow = dec2bcd(Serial.parseInt()); Serial.println("Please enter the hour, from 0-23:"); uint8_t hour = dec2bcd(Serial.parseInt()); Serial.println("Please enter the minute, from 0-59:"); uint8_t minute = dec2bcd(Serial.parseInt()); Serial.println("Please enter the seconds from 0-59 or hit enter for 0:"); uint8_t seconds = dec2bcd(Serial.parseInt()); Wire.begin(); // start I2C Wire.beginTransmission(DS3231addr); Wire.write(0); // start at register 0 Wire.write(seconds); Wire.write(minute); Wire.write(hour & ~0x40); // send hour with 24h bit Wire.write(dow); Wire.write(dom); Wire.write(month); Wire.write(year); Wire.endTransmission(); // send data } void loop() { // put your main code here, to run repeatedly: Wire.beginTransmission(DS3231addr); Wire.write(0); Wire.requestFrom(DS3231addr, 7, true); Wire.endTransmission(); for(int i = 0; i < 7; i++) { recvByte = Wire.read(); Serial.print(recvByte, HEX); Serial.print(" "); } Serial.println(); delay(1000); }
And that's it! Feel free to play around with the code. Explore some of the other registers on the DS3231, or try and convert the code to work with a different RTC chip. As always, if you need help, check the Getting Help section in the project details! If you spot any errors, please do let me know and I will correct them.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.