/* This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ /** @file main.c * @author King Kévin * @date 2016 * @brief show the time on a LED strip * * @mainpage LED clock * The LED clock is an add-on for round wall clocks. * LEDs need to be attached to the border of the clock. * The micro-controller will then show how much time of the day passed using light. * * The time will be shown as arc progress bars, instead of hands pointing at the current time. * The hours passed since the beginning of the midday are shown using blue LEDs. * The minutes passed sine the beginning of the hour are shown using green LEDs. * The (gamma corrected) brightness of the last LED shows how much of the hours or minutes has passed. * Whichever progress is higher will be shown on top of the other. * For example if it's 6:45, the first half of the circle will be blue, and an additional quarter will be green. * The seconds passed since the beginning of the minute are shown using a running red LED, similar to the seconds hand. * The red color might be added on top of the blue, or green color, then showing as violet or yellow. * * The LEDs are controlled using a STM32F1XX micro-controller (based on an ARM Cortex-M3 32-bit processor). * The board needs to include a 32.678 kHz oscillator for the Real-Time-Clock (RTC). * Preferably use a blue pill board. * The board needs to be powered by an external 5 V power supply (e.g. using the USB port). * To set the time connect using serial over the USB port (providing the CDC ACM profile) or USART1 port (TX and RX are on pin PA9 and PA10) and enter "time HH:MM:SS". * Optionally connect a 3 V coin battery on the VBAT pin for the RTC to keep the correct time in case the main power supply gets disconnected. * To know the charge of the coin cell connect its positive terminal to ADC channel 1 on pin PA1 through a 10 kOhm resistor. * The voltage of the battery will be shown over serial while the board is booting. * * For the LEDs use a 1 meter LED strip with 60 red-green-blue WS2812b LEDs. * Tape the LED strip along the border/edge of the clock. * Ideally the wall clock has a diameter of 32 cm for the 1 m LED strip to completely fit. * Connect the 5 V power rail of the LED strip to the 5 V pin of the board. * Connect the DIN signal line of the LED strip to the MISO pin of the micro-controller on PA6. * SPI is used to efficiently shift out the LED color values to the WS2812b LEDs. * A custom clock is provided for this operation using channel 3 of timer 3 on pin PB0. * Simply connect this clock to the SPI CLK input on pin PA5. * * The brightness of LEDs is dependant on the ambient luminosity. * To measure the ambient luminosity a 5528 photo-resistor is used. * Connect one leg of the photo-resistor to ADC channel 0 on pin PA0 and the other to ground. * Connect one leg of a 1 kOhm resistor to ADC channel 0 on pin PA0 and the other to a 3.3 V pin. * * If the board does not provide a 32.678 kHz oscillator for the internal RTC it is also possible to use an external RTC such as the Maxim DS1307. * The time is then read over I2C and incremented using the square wave output. * Working example code is under the DS1307_4096Hz_timer tag. */ /* standard libraries */ #include // standard integer types #include // standard I/O facilities #include // standard utilities #include // standard streams #include // error number utilities #include // string utilities #include // mathematical utilities /* STM32 (including CM3) libraries */ #include // real-time control clock library #include // general purpose input output library #include // vector table definition #include // Cortex M3 utilities #include // interrupt utilities #include // external interrupt utilities #include // timer utilities #include // ADC utilities #include // real time clock utilities /* own libraries */ #include "global.h" // board definitions #include "usart.h" // USART utilities #include "usb_cdcacm.h" // USB CDC ACM utilities #include "led_ws2812b.h" // WS2812b LEDs utilities /** @defgroup main_flags flag set in interrupts to be processed in main task * @{ */ volatile bool button_flag = false; /**< flag set when board user button has been pressed/released */ volatile bool time_flag = false; /**< flag set when time changed */ volatile bool photoresistor_flag = false; /**< flag set when ambient luminosity is measured */ /** @} */ #define TICKS_PER_SECOND 256 /**< the number of ticks in one second */ /** @defgroup main_ticks ticks per time units * @note these are derived from TICKS_PER_SECOND * @note I have to use type variables because defines would be stored in signed integers, leading to an overflow it later calculations * @{ */ /** number of ticks in one second */ const uint32_t ticks_second = TICKS_PER_SECOND; /** number of ticks in one minute */ const uint32_t ticks_minute = 60*TICKS_PER_SECOND; /** number of ticks in one hour */ const uint32_t ticks_hour = 60*60*TICKS_PER_SECOND; /** number of ticks in one midday (12 hours) */ const uint32_t ticks_midday = 12*60*60*TICKS_PER_SECOND; /** @} */ /** @defgroup battery_adc ADC used to measure battery voltage * @{ */ #define BATTERY_ADC_CHANNEL ADC_CHANNEL1 /**< ADC channel */ #define BATTERY_PORT GPIOA /**< port on which the battery is connected */ #define BATTERY_PORT_RCC RCC_GPIOA /**< timer port peripheral clock */ #define BATTERY_PIN GPIO1 /**< pin of the port on which the battery is connected */ /** @} */ /** @defgroup photoresistor_adc ADC used to ambient luminosity * @{ */ #define PHOTORESISTOR_ADC_CHANNEL ADC_CHANNEL0 /**< ADC channel */ #define PHOTORESISTOR_PORT GPIOA /**< port on which the battery is connected */ #define PHOTORESISTOR_PORT_RCC RCC_GPIOA /**< timer port peripheral clock */ #define PHOTORESISTOR_PIN GPIO0 /**< pin of the port on which the battery is connected */ /** @} */ /** RGB values for the WS2812b clock LEDs */ uint8_t clock_leds[WS2812B_LEDS*3] = {0}; /** user input command */ char command[32] = {0}; /** user input command index */ uint8_t command_i = 0; /** gamma correction lookup table (common for all colors) */ uint8_t gamma_correction_lut[256] = {0}; /** photo-resistor measurement of ambient luminosity */ volatile uint16_t photoresistor_value = 0; /** factor to dim LED of the clock, depending on the ambient luminosity */ float clock_brithtness = 0; int _write(int file, char *ptr, int len) { int i; if (file == STDOUT_FILENO || file == STDERR_FILENO) { for (i = 0; i < len; i++) { if (ptr[i] == '\n') { // add carrier return before line feed. this is recommended for most UART terminals usart_putchar_nonblocking('\r'); // a second line feed doesn't break the display cdcacm_putchar('\r'); // a second line feed doesn't break the display } usart_putchar_nonblocking(ptr[i]); // send byte over USART cdcacm_putchar(ptr[i]); // send byte over USB } return i; } errno = EIO; return -1; } void led_on(void) { #if defined(SYSTEM_BOARD) || defined(BLUE_PILL) gpio_clear(LED_PORT, LED_PIN); #elif defined(MAPLE_MINI) gpio_set(LED_PORT, LED_PIN); #endif } void led_off(void) { #if defined(SYSTEM_BOARD) || defined(BLUE_PILL) gpio_set(LED_PORT, LED_PIN); #elif defined(MAPLE_MINI) gpio_clear(LED_PORT, LED_PIN); #endif } void led_toggle(void) { gpio_toggle(LED_PORT, LED_PIN); } /** @brief switch off all clock LEDs * @note LEDs need to be set separately */ static void clock_clear(void) { // set all colors of all LEDs to 0 for (uint16_t i=0; i=WS2812B_LEDS*255 || led_minute>=WS2812B_LEDS*255) { // a calculation error occurred return; } // show hours and minutes on LEDs if (led_hour>led_minute) { // show hours in blue (and clear other LEDs) for (uint16_t led=0; led=0xff) { // full hours clock_leds[led*3+2] = 0xff; } else { // running hours clock_leds[led*3+2] = led_hour; } led_hour -= clock_leds[led*3+2]; } // show minutes in green (override hours) for (uint16_t led=0; led0; led++) { clock_leds[led*3+0] = 0; if (led_minute>=0xff) { // full minutes clock_leds[led*3+1] = 0xff; } else { // running minutes clock_leds[led*3+1] = led_minute; } led_minute -= clock_leds[led*3+1]; clock_leds[led*3+2] = 0; } } else { // show minutes in green (and clear other LEDs) for (uint16_t led=0; led=0xff) { // full minutes clock_leds[led*3+1] = 0xff; } else { // running minutes clock_leds[led*3+1] = led_minute; } led_minute -= clock_leds[led*3+1]; clock_leds[led*3+2] = 0; } // show hours in blue (override minutes) for (uint16_t led=0; led0; led++) { clock_leds[led*3+0] = 0; clock_leds[led*3+1] = 0; if (led_hour>=0xff) { // full hours clock_leds[led*3+2] = 0xff; } else { // running hours clock_leds[led*3+2] = led_hour; } led_hour -= clock_leds[led*3+2]; } } // don't show seconds on full minute (better for first time setting, barely visible else) if (time%ticks_minute==0) { return; } uint16_t led_second = (WS2812B_LEDS*(time%ticks_minute))/ticks_minute; // get LED for seconds uint8_t brightness_second = (255*(time%ticks_second))/ticks_second; // get brightness for seconds // set seconds LED clock_leds[led_second*3+0] = brightness_second; //clock_leds[led_second*3+1] = 0; // clear other colors (minutes/hours indication) //clock_leds[led_second*3+2] = 0; // clear other colors (minutes/hours indication) // set previous seconds LED led_second = ((led_second==0) ? WS2812B_LEDS-1 : led_second-1); // previous LED clock_leds[led_second*3+0] = 0xff-brightness_second; //clock_leds[led_second*3+1] = 0; // clear other colors (minutes/hours indication) //clock_leds[led_second*3+2] = 0; // clear other colors (minutes/hours indication) // adjust brightness for (uint16_t i=0; i255 ? 512-i-1 : i); // get fade brightness for (uint8_t hour=0; hour<12; hour++) { // set all hour colors uint16_t led = WS2812B_LEDS/12*hour; // get LED four hour mark clock_leds[led*3+0] = brightness; // set brightness clock_leds[led*3+1] = brightness; // set brightness clock_leds[led*3+2] = brightness; // set brightness } clock_leds_set(); // set the colors of all LEDs ws2812b_transmit(); // transmit set color // delay some time for the animation for (uint32_t j=0; j<40000; j++) { __asm__("nop"); } } } /** @brief process user command * @param[in] str user command string (\0 ended) */ static void process_command(char* str) { /* split command */ const char* delimiter = " "; char* word = strtok(str,delimiter); if (!word) { goto error; } /* parse command */ if (0==strcmp(word,"help")) { printf("available commands:\n"); printf("time [HH:MM:SS]\n"); } else if (0==strcmp(word,"time")) { word = strtok(NULL,delimiter); if (!word) { //printf("current time: %02d:%02d:%02d\n", rtc_read_hours(), rtc_read_minutes(), rtc_read_seconds()); } else if (strlen(word)!=8 || word[0]<'0' || word[0]>'2' || word[1]<'0' || word[1]>'9' || word[3]<'0' || word[3]>'5' || word[4]<'0' || word[4]>'9' || word[6]<'0' || word[6]>'5' || word[7]<'0' || word[7]>'9') { goto error; } else { rtc_set_counter_val(((word[0]-'0')*10+(word[1]-'0')*1)*ticks_hour+((word[3]-'0')*10+(word[4]-'0')*1)*ticks_minute+((word[6]-'0')*10+(word[7]-'0')*1)*ticks_second); // set time in RTC counter printf("time set\n"); } } else { goto error; } return; // command successfully processed error: puts("command not recognized. enter help to list commands"); } /** @brief program entry point * this is the firmware function started by the micro-controller */ int main(void) { SCB_VTOR = (uint32_t) 0x08002000; // relocate vector table because of the bootloader rcc_clock_setup_in_hse_8mhz_out_72mhz(); // use 8 MHz high speed external clock to generate 72 MHz internal clock usart_setup(); // setup USART (for printing) cdcacm_setup(); // setup USB CDC ACM (for printing) setbuf(stdout, NULL); // set standard out buffer to NULL to immediately print setbuf(stderr, NULL); // set standard error buffer to NULL to immediately print // setup LED rcc_periph_clock_enable(LED_RCC); // enable clock for LED gpio_set_mode(LED_PORT, GPIO_MODE_OUTPUT_2_MHZ, GPIO_CNF_OUTPUT_PUSHPULL, LED_PIN); // set LED pin to 'output push-pull' led_off(); // switch off LED to indicate setup started // setup button #if defined(BUTTON_RCC) && defined(BUTTON_PORT) && defined(BUTTON_PIN) && defined(BUTTON_EXTI) && defined(BUTTON_IRQ) rcc_periph_clock_enable(BUTTON_RCC); // enable clock for button gpio_set_mode(BUTTON_PORT, GPIO_MODE_INPUT, GPIO_CNF_INPUT_FLOAT, BUTTON_PIN); // set button pin to input rcc_periph_clock_enable(RCC_AFIO); // enable alternate function clock for external interrupt exti_select_source(BUTTON_EXTI, BUTTON_PORT); // mask external interrupt of this pin only for this port exti_set_trigger(BUTTON_EXTI, EXTI_TRIGGER_BOTH); // trigger on both edge exti_enable_request(BUTTON_EXTI); // enable external interrupt nvic_enable_irq(BUTTON_IRQ); // enable interrupt #endif // setup RTC rtc_auto_awake(RCC_LSE, 32768/TICKS_PER_SECOND-1); // ensure RTC is on, uses the 32.678 kHz LSE, and the prescale is set to our tick speed, else update backup registers accordingly rtc_interrupt_enable(RTC_SEC); // enable RTC interrupt on "seconds" nvic_enable_irq(NVIC_RTC_IRQ); // allow the RTC to interrupt // generate gamma correction table (with fixed gamma value) for (uint16_t i=0; i0) { // there is a command to process command[command_i] = 0; // end string command_i = 0; // prepare for next command process_command(command); // process user command } } else if (c!='\r') { // user command input command[command_i] = c; // save command input if (command_i=ticks_midday*2) { // one day passed rtc_set_counter_val(rtc_get_counter_val()%ticks_midday); // reset time counter } clock_set_time(rtc_get_counter_val()); // set time //adc_start_conversion_regular(ADC1); // start measuring ambient luminosity } while (photoresistor_flag) { // new photo-resistor value has been measured photoresistor_flag = false; // reset flag action = true; // action has been performed float photoresistor_voltage = photoresistor_value*1.2/ref_value; // calculate voltage from value if (photoresistor_voltage<1.7) { // high ambient luminosity clock_brithtness = 1; // set highest brightness } else if (photoresistor_voltage>2.1) { // low ambient luminosity clock_brithtness = 0.3; // set low brightness } else { // intermediate ambient luminosity clock_brithtness = 0.3+(0.7*(1-(photoresistor_voltage-1.7)/0.4)); // set variable brightness } //printf("%f, %f\n", photoresistor_voltage, clock_brithtness); } if (action) { // go to sleep if nothing had to be done, else recheck for activity action = false; } else { __WFI(); // go to sleep } } return 0; } #if defined(BUTTON_ISR) && defined(BUTTON_EXTI) /** @brief interrupt service routine called when button is pressed of released */ void BUTTON_ISR(void) { exti_reset_request(BUTTON_EXTI); // reset interrupt button_flag = true; // perform button action } #endif /** @brief interrupt service routine called when tick passed on RTC */ void rtc_isr(void) { rtc_clear_flag(RTC_SEC); // clear flag time_flag = true; // notify to show new time } /** @brief interrupt service routine called when ADC conversion completed */ void adc1_2_isr(void) { photoresistor_value = adc_read_regular(ADC1); // read measured photo-resistor value (clears interrupt flag) photoresistor_flag = true; // notify new ambient luminosity has been measured }