/** * \file main.c * \brief Firmware for the i2c-dimmer * \author Ronald Schaten & Thomas Stegemann * \version $Id: main.c,v 1.1 2007/07/29 17:19:50 rschaten Exp $ * * Permission to use, copy, modify, and distribute this software and its * documentation under the terms of the GNU General Public License is hereby * granted. No representations are made about the suitability of this software * for any purpose. It is provided "as is" without express or implied warranty. * See the GNU General Public License for more details. */ /** * \mainpage I2C-dimmer * * \section sec_intro Introduction * * I haven't done many microcontroller-projects till now, but more than one of * the few projects I did involved controlling LEDs by pulse width modulation * (PWM). Doing this for one or more LEDs is a stressful task for a little * microcontroller, but if you want to do some other more or less complicated * things while keeping LEDs at certain brightnesses is likely to ruin the * timings that are used in the PWM. Not to talk about the program code, which * gets more and more unreadable if you try to do several different things 'at * the same time'. * * For my next project I need to fade some LEDs again, so I was looking for an * easier way to do it. The plans include reading from memory cards, talking to * real time clocks and displaying text on an LCD, so I'm almost sure that I * won't be able to reliably control the five channels I'm going to use. * * The first plan was to use a ready-made chip. I looked around and the best * thing I could find was one made by Philips (PCA something, I forgot the * number) that can be controlled via I2C-bus. That part is able to control * eight LEDs, but apart from 'on' and 'off' you can set the LEDs only to two * different brightnesses. Those are variable, nevertheless, but it would be * impossible to light one LED at 20%, one at 50% and one at 80%. Another * drawback is that it is SMD-only, and my soldering-skills don't including * working with stuff that small. * * So the Idea was to set up a separate controller for LED-fading, that can be * externally controlled, ideally via I2C-bus since I intend to use several * other devices in my next project that can make use of the bus. So I set up * an ATtiny2313 on my breadboard, clocked it with an external 20MHz-crystal * and we tried to control as many LEDs as possible... * * \section sec_pwm Pulse width modulation * * \subsection sec_pwm1 The old way * * Controlling the brightness of LEDs by PWM is a common technique, I used it * myself in several projects. Till now I used to switch on all LEDs that * should light up at a level greater than zero, waited till the first of the * LEDs has to be switched off, switched it off, waited for the next one and so * on. After a certain time all LEDs are switched off, and I start again. * * I try to visualize that with a little picture: * * \code * . . . . .| . . * 1 *************************************************|************************ * 2 *************************************** |************************ * 3 ********* |********** * 4 | * 5 ***************************** |************************ * \endcode * * In this example, a full cycle of the PWM would need 50 units of time. The * first LED is switched on the full time (100%), the second for 40 of the 50 * units (80%), the third one for ten (20%) and the fifth one for 30 units * (60%). The fourth LED is off (0%). We see that after 50 units of time the * modulation starts again. * * The drawback of this technique is, that it's slow. And for each additional * channel you try to control, it gets even slower. We tried, but we weren't * able to control more than five LEDs in this way without them to start * flickering to a visible amount. * * We tried to create an array with all states of the process, so the PWM only * would have to loop through the array and set the outputs accordingly. But * that didn't work either, because the used microcontroller doesn't have * enough RAM to store the array. * * \subsection sec_pwm2 Thomas' idea * * After some tests that didn't work out too well, Thomas had a great idea how * to implement the PWM. It also works with an array for all states, but the * states of the modulation are not displayed for the same time. The first * state is displayed for one time-unit, the second one for two time-units, the * third one for four and so on. In this way the LEDs are turned on and off * more than once per cycle of the PWM, but that doesn't hurt. * * Let's try to paint a picture again: * * \code * . . . . . . | . * .. . . . . |.. . . * 1 * |* * 2 ** | ** * 3 *** |*** * 4 **** | **** * 5 * **** |* **** * 6 ****** | ****** * 7 ******* |******* * 8 ******** | **** * \endcode * * So here we see a PWM with eight channels that are able to display up to 64 * different brightnesses. Channel one is switched on for one unit of time, * channel two for two units and so on. The most interesting thing is on * channel five: the LED is switched on for one unit of time, switched off, and * switched on again for four units of time. * * Lets try a more complicated example -- with brighter LEDs, too: * * \code * . . . . . . | . * .. . . . . |.. . . * 1 * *******************************|* * 2 ** **************** | ** * 3 ******* **************** |******* * 4 *******************************| * 5 * **** **************** |* **** * 6 *************************************************************| ********** * 7 **************************************************************|*********** * 8 ************************ | **** * \endcode * * The channels 1 to 8 have the brightnesses 33, 18, 23, 32, 21, 63, 64 and 24. * * The advantage of this technique is that on the one hand you have to save a * limited number of states (six states in the example), and the looping * through the states is very simple: state n is sent to the output pins, then * we wait for 2^(n-1) time units, then the next state is sent. * * Each state represents the bit-pattern that has to be sent during one step. * In other words: one column out of the above picture at the start of a new * time period. So in this example, we have six states: 01010101, 01100110, * 01110100, 11100000, 11110110 and 01101001. The first one is displayed for * one unit of time, the second one for two units, the third one for four units * and so on... * * Using this technique has the advantage that adding more channels does almost * nothing in terms of system load. The only time that the algorithm has to do * actual calculations is when a new value has been delivered and has to be * converted into the states. So using this algorithm, it is possible * to show different brightnesses on all free pins of the controller. With an * ATtiny2313 that means that you can fade 13 different LEDs while still * talking I2C to communicate with other devices! * * \section sec_i2c I2C communication * * Speaking I2C is no rocket science, but since one has to do a lot of * bit-shifting when implementing it, I took a ready-made library. * * The one I used is written by Donald R. Blake, he was so * kind to put it under GPL and post it to avrfreaks.net. You can find the * original post in a thread called '8 * bit communication between AVR using TWI' and some additions in the * thread 'I2C * Slave on an ATtiny45'. * * Thanks for the great work, Donald! And for putting it under a free license. * * Since his package seems to be only available as a forum-attachment, and I'm * not sure for how long that will be, I included it into the tarball of this * project. * * \section sec_install Building and installing * * The firmware is built and installed on the controller with the included * makefile. You might need to need to customize it to match your individual * environment. * * Don't forget to set the fuses on the controller to make use of the external * crystal. This project is using a fine algorithm, but it still needs the full * power of 20MHz. The settings I used are included in the makefile, too. * * Oh, and if you want the slave to use an I2C-address different from 0x10: no * problem. Just change it in the code. * * \section sec_usage Usage * * You should be able to use this device in the same way you would use any * other I2C-slave: * * \subsection sec_usage_hardware Connecting it * * The controller needs to have the following pins connected in the circuit: * * * * Your I2C-data and -clock lines should be terminated by 4,7k-resistors to * pull up the lines. All the other pins can be used to connect LEDs. They are * arranged in this way: * * * * \subsection sec_usage_software Talking to it * * For my tests I used an ATmega8 as I2C-master with the library * written by Peter Fleury. You can find it on http://jump.to/fleury. Thanks to him for * putting it online! * * The typical send function looks like this: * * \code * #define I2C_DIMMER 0x10 * * void sendi2cBytes(uint8_t address, uint8_t brightness) { * // address: number of the LED to set (0..12) * // brightness: value between 0 and 127 * // start the communication... * i2c_start_wait((I2C_DIMMER << 1) + I2C_WRITE); * // write a byte with the address. we want the highest bit of the * // address to be 1, so the slave can be sure that this is an address. * i2c_write(address | 0x80); * // calculate the actual duration the LED should light up. we could do * // this on the slave's side, but we assume that the device is more * // flexible when it is done on the master side. * uint16_t duration = (brightness + 1) * (brightness + 1) - 1; * // calculate the low- and the high-byte and send it. note that we split * // the duration into 7-bit-values, not 8 bit! in this way the highest * // bit of the transferred bytes is always low, allowing the slave to * // recognize the transmitted bytes as values, not as addresses. * i2c_write(duration & 0x7f); // low byte * i2c_write((duration >> 7) & 0x7f); // high byte * // stop the communication... * i2c_stop(); * } * \endcode * * \section sec_drawbacks Drawbacks * * Till now, the device worked in all situations I tested it in. So far * everything is fine. * * I guess that, compared to the ready-made off-the-hook-parts that controls * LEDs via I2C, this module is a bit slow. I can't see any flickering in the * LEDs since they are still switched very fast (about every 6ms, which would * result in a 166Hz flickering -- too fast for me). * * \section sec_files Files in the distribution * * - \e Readme.txt: Documentation, created from the htmldoc-directory. * - \e htmldoc/: Documentation, created from main.c. * - \e refman.pdf: Documentation, created from main.c. * - \e main.c: Source code of the firmware. * - \e main_*.hex: Compiled version of the firmware. * - \e usiTwiSlave.c: I2C-library. * - \e usiTwiSlave.h: I2C-library. * - \e USI_TWI_Slave.zip: I2C-library (package). * - \e i2c-dimmer.doxygen: Support for creating the documentation. * - \e License.txt: Public license for all contents of this project, except * for the USB driver. Look in firmware/usbdrv/License.txt for further info. * - \e Changelog.txt: Logfile documenting changes in soft-, firm- and * hardware. * * \section sec_thanks Thanks! * * Once again, special credits go to Thomas Stegemann. He had the great * idea for the PWM-algorithm, and I am always astonished by the patience he * has to show me how to do anything complicated in a sick language like C... * * \section sec_license About the license * * My work is licensed under the GNU General Public License (GPL). A copy of * the GPL is included in License.txt. * * (c) 2007 by Ronald Schaten - http://www.schatenseite.de */ #include #include #include #include #include // keeping constants in program memory #include "usiTwiSlave.h" // i2c-routines by Donald R. Blake #define TWI_SLA 0x10 /**< i2c slave address */ #define CHANNEL_COUNT 13 /**< number of 'fadeable' channels */ #define PORT_COUNT 2 /**< the channels are distributed over two ports */ #define OUTPORT0 PORTB /**< output port 0 */ #define OUTDDR0 DDRB /**< set port 0 to be output */ #define OUTMASK0 0x5F /**< see channel_pin, channel_port */ #define OUTPORT1 PORTD /**< output port 0 */ #define OUTDDR1 DDRD /**< set port 0 to be output */ #define OUTMASK1 0x7F /**< see channel_pin, channel_port */ /** * We want to drive as many channels as possible. Unfortunately the usable pins * aren't 'in a row', so we have to determine which channel ends up on which * port and pin. */ /** this is used to determine the port that is used for output */ const uint8_t channel_port[CHANNEL_COUNT] PROGMEM = { 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1 }; /** this is used to determine the pin that is used for output */ const uint8_t channel_pin[CHANNEL_COUNT] PROGMEM = { 0x01, 0x02, 0x04, 0x08, 0x10, 0x40, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40 }; /* * This is a special treatment for the states lasting very long. If you simply * double the times for each state, you eventually end up having long pauses in * the modulation. We try to suppress this effect by not waiting for 8192 * cycles but better performing the shorter 4096-cycle twice. */ #define STATE_COUNT 14 /**< number of states for pwm */ #define STATE_START_COUNT 2 /**< number of state groups to be treated individually */ /** interval length of the states */ const uint16_t switch_timer[STATE_COUNT] PROGMEM = { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 4096 }; /** start interval of the state groups */ const uint8_t switch_timer_index[STATE_START_COUNT]= { 13, 0 }; /** contains the port assignments for each interval */ uint8_t switch_state[STATE_COUNT][PORT_COUNT]; uint8_t switch_state_new[STATE_COUNT][PORT_COUNT]; /** * Three bytes have to be received for a full command. This enum is used to * indicate what part of the command we are waiting for. */ typedef enum { WAIT_FOR_ADDRESS, /**< first byte is the address */ WAIT_FOR_VALUE_LOW, /**< second byte is the lower part of the value */ WAIT_FOR_VALUE_HIGH, /**< third byte is the higher part of the value */ } ReadCommandState; /** * Holds one command that is received via i2c. The command consists of an * address (number of output channel) and a 16-bit value. The state is used to * indicate what part of the next command we are waiting for. */ typedef struct { uint8_t address; /**< number of output channel (between 0 and CHANNEL_COUNT-1 */ uint16_t value; /**< value to be assigned to the channel (between 0 and 128*128-1 = 16383 */ ReadCommandState state; /**< what are we waiting for? */ } Command; /** the next command is built in this variable */ Command command = {0, 0, WAIT_FOR_ADDRESS}; /** * initialize timer */ void timer_start() { TCCR1A = 0x00; // no hardware-pwm /* CS12, CS11, CS10 (clock select bits) * 0 1 0 cpu-clock / 8 */ TCCR1B = (0 << WGM13) | (0 << WGM12) | (0 << CS12) | (1 << CS11) | (0 << CS10); // WGM1=4 sei(); } /** * Set brightness on one channel. * \param channel the channel to address (0 .. CHANNEL_COUNT) * \param brightness the value to set (0 .. 16383) */ void set_brightness(uint8_t channel, uint16_t brightness){ uint8_t i; // read port mask and port for this channel from program memory uint8_t mask= pgm_read_word(&channel_pin[channel]); uint8_t port= pgm_read_word(&channel_port[channel]); // set the bits in the output-states according to the brightness for (i= 0; i < STATE_COUNT; i++){ // walk through all states... if (brightness & 1) { // set the bit if it needs to be set in this state switch_state_new[i][port] |= mask; } else { // clear it otherwise switch_state_new[i][port] &= ~mask; } // shift the value to look at the next bit brightness >>= 1; } } /** * initialize hardware */ void init_ports(void){ OUTDDR0 |= OUTMASK0; OUTPORT0 &= ~OUTMASK0; // clear all masked bits OUTDDR1 |= OUTMASK1; OUTPORT1 &= ~OUTMASK1; // clear all masked bits } /** * set output * \param port port to set * \param state value to be sent to the port */ void set_port(int port, uint8_t state){ switch(port){ case 0: OUTPORT0 |= (state & OUTMASK0); // set bits OUTPORT0 &= (state | ~OUTMASK0); // clear bits break; case 1: OUTPORT1 |= (state & OUTMASK1); // set bits OUTPORT1 &= (state | ~OUTMASK1); // clear bits break; } } /** * Check if anything has been received via i2c and evaluate the received data. * The received data is set into the command variable according to the state of * the command we are waiting for. */ void evaluate_i2c_input(void) { uint8_t byte_received = 0; if (usiTwiDataInReceiveBuffer()) { // we have input byte_received = usiTwiReceiveByte(); switch(command.state){ case WAIT_FOR_ADDRESS: if (byte_received & 0x80) { // bit 7 is set -> address received command.address = (byte_received & 0x7f); command.state = WAIT_FOR_VALUE_LOW; } // do nothing if this byte didn't look like an address break; case WAIT_FOR_VALUE_LOW: if (!(byte_received & 0x80)) { // bit 7 is not set -> could be a value command.value = byte_received; command.state = WAIT_FOR_VALUE_HIGH; } else { // seems to be an address command.address = byte_received; command.state = WAIT_FOR_VALUE_LOW; } break; case WAIT_FOR_VALUE_HIGH: if (!(byte_received & 0x80)) { // bit 7 is not set -> could be a value command.value += (byte_received << 7); command.state = WAIT_FOR_ADDRESS; // we have a complete command set_brightness(command.address, command.value); } else { // seems to be an address command.address = byte_received; command.state = WAIT_FOR_VALUE_LOW; } break; } } } /** * Main-function. Initializes everything and contains the main loop which * controls the actual PWM output. * \return An integer. Whatever... :-) */ int main(void) { uint8_t state_number = 0; uint8_t state_start = 0; uint8_t port = 0; uint16_t timer = 0; // initialize output ports init_ports(); // set all channels to 0 uint8_t i; for(i= 0; i < CHANNEL_COUNT; i++) { set_brightness(i, 0); } // own TWI slave address usiTwiSlaveInit(TWI_SLA); // start timer timer_start(); // init watchdog wdt_enable(WDTO_15MS); // 15ms watchdog while (1) { // loop forever for (state_start = 0; state_start < STATE_START_COUNT; state_start++) { // treat state groups... for (state_number = switch_timer_index[state_start]; state_number < STATE_COUNT; state_number++) { // cycle through all steps... for (port = 0; port < PORT_COUNT; port++) { // set all output ports according to the current step... set_port(port, switch_state[state_number][port]); } // determine how long to wait for the next step timer = pgm_read_word(&switch_timer[state_number]); // restart timer TCNT1 = 0; while (timer > TCNT1) { // wait for the next step... meanwhile... wdt_reset(); // feed the watchdog evaluate_i2c_input(); // read i2c commands } } } for(state_number= 0; state_number < STATE_COUNT; state_number++) { for(port= 0; port < PORT_COUNT; port++) { switch_state[state_number][port]= switch_state_new[state_number][port]; } } } return 0; }