{"id":24016090,"url":"https://github.com/mgoblin/electronichourglasskit","last_synced_at":"2025-02-25T19:40:49.830Z","repository":{"id":271251683,"uuid":"912828358","full_name":"mgoblin/ElectronicHourGlassKit","owner":"mgoblin","description":"This repository contains programming examples for electronic hourglass kit.","archived":false,"fork":false,"pushed_at":"2025-01-28T19:28:44.000Z","size":4797,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-28T20:29:23.386Z","etag":null,"topics":["8051","8051-microcontroller","electronical-kit","hourglass","hourglass-kit","stc","stc-mcu","stc15"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mgoblin.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-01-06T13:30:28.000Z","updated_at":"2025-01-28T19:28:48.000Z","dependencies_parsed_at":"2025-01-06T15:36:06.515Z","dependency_job_id":"086459ca-a2c0-460e-9d43-2b1e89902617","html_url":"https://github.com/mgoblin/ElectronicHourGlassKit","commit_stats":null,"previous_names":["mgoblin/electronichourglasskit"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mgoblin%2FElectronicHourGlassKit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mgoblin%2FElectronicHourGlassKit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mgoblin%2FElectronicHourGlassKit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mgoblin%2FElectronicHourGlassKit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mgoblin","download_url":"https://codeload.github.com/mgoblin/ElectronicHourGlassKit/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240737541,"owners_count":19849542,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["8051","8051-microcontroller","electronical-kit","hourglass","hourglass-kit","stc","stc-mcu","stc15"],"created_at":"2025-01-08T08:39:30.412Z","updated_at":"2025-02-25T19:40:49.818Z","avatar_url":"https://github.com/mgoblin.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Table of contents\n* [Electronic hourglass kit programming examples  ](https://github.com/mgoblin/ElectronicHourGlassKit#electronic-hourglass-kit-programming-examples)\n    * [What is electronic hourglass kit](https://github.com/mgoblin/ElectronicHourGlassKit#what-is-electronic-hourglass-kit)\n    * [Additional hardware](https://github.com/mgoblin/ElectronicHourGlassKit#additional-hardware)\n* [Fast track](https://github.com/mgoblin/ElectronicHourGlassKit#fast-track)\n* [Deep dive](https://github.com/mgoblin/ElectronicHourGlassKit#deep-dive) \n    * [Build examples prerequisites](https://github.com/mgoblin/ElectronicHourGlassKit#build-examples-prerequisites)\n        * [Platformio (mandatory)](https://github.com/mgoblin/ElectronicHourGlassKit#platformio-mandatory)\n        * [SDCC compiler (optional)](https://github.com/mgoblin/ElectronicHourGlassKit#sdcc-compiler-optional)\n        * [gcc compiler (optional)](https://github.com/mgoblin/ElectronicHourGlassKit#gcc-compiler-optional)\n        * [clang compiler (optional)](https://github.com/mgoblin/ElectronicHourGlassKit#clang-compiler-optional)\n        * [Scons (optional)](https://github.com/mgoblin/ElectronicHourGlassKit#scons-optional)\n        * [stcgal (optional)](https://github.com/mgoblin/ElectronicHourGlassKit#stcgal-optional)\n    * [How to build examples](https://github.com/mgoblin/ElectronicHourGlassKit#how-to-build-examples)\n        * [Build from Platformio IDE](https://github.com/mgoblin/ElectronicHourGlassKit#build-from-platformio-ide)\n        * [Build from command line using Platfromio core](https://github.com/mgoblin/ElectronicHourGlassKit#build-from-command-line-using-platfromio-core)\n        * [Build from command line using only SCons](https://github.com/mgoblin/ElectronicHourGlassKit#build-from-command-line-using-only-scons)\n    * [Upload firmware](https://github.com/mgoblin/ElectronicHourGlassKit#upload-firmware)\n        * [Upload from Platformio IDE](https://github.com/mgoblin/ElectronicHourGlassKit#upload-from-platformio-ide)\n        * [Upload from command line using Platfromio core](https://github.com/mgoblin/ElectronicHourGlassKit#upload-from-command-line-using-platfromio-core)\n        * [Upload from command line](https://github.com/mgoblin/ElectronicHourGlassKit#upload-from-command-line)\n* [Electrical schema](https://github.com/mgoblin/ElectronicHourGlassKit#electrical-schema)\n* [LED driving algorithms](https://github.com/mgoblin/ElectronicHourGlassKit#led-driving-algorithms)\n    * [Stage 0. Install/configure sources and tools](https://github.com/mgoblin/ElectronicHourGlassKit#stage-0-installconfigure-sources-and-tools)\n    * [Stage 1. Turn single LED on](https://github.com/mgoblin/ElectronicHourGlassKit#stage-1-turn-single-led-on)\n    * [Stage 2. Turn single LED on using timer](https://github.com/mgoblin/ElectronicHourGlassKit#stage-2-turn-single-led-on-using-timer)\n    * [Stage 3. Turn on line 0 consists of L1-L5 LEDs](https://github.com/mgoblin/ElectronicHourGlassKit?tab=readme-ov-file#stage-3-turn-on-line-0-consists-of-l1-l5-leds)\n    * [Stage 4. Turn on all LEDs](https://github.com/mgoblin/ElectronicHourGlassKit?tab=readme-ov-file#stage-4-turn-on-all-leds)\n    * [Stage 5. Desсribe page and iterate through](https://github.com/mgoblin/ElectronicHourGlassKit?tab=readme-ov-file#stage-5-des%D1%81ribe-page-and-iterate-through)\n    * [Stage 6: Move the page display inside the timer](https://github.com/mgoblin/ElectronicHourGlassKit?tab=readme-ov-file#stage-6-move-the-page-display-inside-the-timer)\n    * [Stage 7. Animate picture inside main function](https://github.com/mgoblin/ElectronicHourGlassKit?tab=readme-ov-file#stage-7-animate-picture-inside-main-function)\n    * [Stage 8. Changing animation speed](https://github.com/mgoblin/ElectronicHourGlassKit?tab=readme-ov-file#stage-7-animate-picture-inside-main-function)\n    * [Stage 9. New animation pattern](https://github.com/mgoblin/ElectronicHourGlassKit?tab=readme-ov-file#stage-9-new-animation-pattern)\n* [Similar DIY projects](https://github.com/mgoblin/ElectronicHourGlassKit?tab=readme-ov-file#similar-diy-projects)\n    * [Ceptimus firmware](https://github.com/mgoblin/ElectronicHourGlassKit?tab=readme-ov-file#ceptimus-firmware)\n    * [Rick-100 firmware](https://github.com/mgoblin/ElectronicHourGlassKit?tab=readme-ov-file#rick-100-firmware)\n* [Next ideas](https://github.com/mgoblin/ElectronicHourGlassKit?tab=readme-ov-file#next-ideas)\n    * [Other ways to changing animation speed](https://github.com/mgoblin/ElectronicHourGlassKit?tab=readme-ov-file#other-ways-to-changing-animation-speed)\n    * [Using idle mode to reduce power consumption](https://github.com/mgoblin/ElectronicHourGlassKit?tab=readme-ov-file#using-idle-mode-to-reduce-power-consumption)\n    * [Storing pages into the EEPROM](https://github.com/mgoblin/ElectronicHourGlassKit?tab=readme-ov-file#storing-pages-into-the-eeprom)\n    * [Creating pages and animation visual editor](https://github.com/mgoblin/ElectronicHourGlassKit?tab=readme-ov-file#creating-pages-and-animation-visual-editor)\n\n\n# Electronic hourglass kit programming examples  \n\nThis repository contains programming examples for electronic hourglass kit.\n\nExamples written on C language and compiled with [SDCC](https://sdcc.sourceforge.net/)\n\n\u003e [!CAUTION]\n\u003e There is no way to download and save original firmware of the microcontroller.After first firmware upload orignal firmware was removed. \n\n\n## What is electronic hourglass kit\n\nElectronic houglass kit is 57 LEDs driven by STC15W204S (or STC15W201S)\n\n[Electronic houglass kit](https://www.icstation.com/hourglass-shaped-flashing-light-kits-simple-lamp-electronics-soldering-practice-stem-teaching-kits-p-12309.html)\n\n![Electronic houglass kit image](http://www.icstation.com/images/uploads/12309_1.jpg)\n\n## Additional hardware\n\nTo upload firmware you need USB-2-TTL adapter. I use CHG340g.\n\n![CHG340g](https://github.com/mgoblin/STC-programmator/blob/main/images/ch340g.jpeg)\n\nSTC programmator is optional, but recommended for use.\n[Additional info ](https://github.com/mgoblin/STC-programmator)    \n\n# Fast track\n\nAll examples are prebuilded in firmware folder. You need stage8.hex or stage9.hex in your choice.\n\nTo upload firmare follow this steps\n1. Install [stcgal](https://github.com/grigorig/stcgal)\n2. Dont turn on power via other connectors. Connect USB-2-TTL adapter to hourglass kit board and plug it into USB. USB-2-TTL Rx to hourglass kit board Tx, USB-2-TTL Tx to hourglass kit board Rx, 5V to 5V. If you not use programmator do not connect board GND pin to USB-2-TTL in this step. \n4. Clone this code repository and open terminal in the cloned repository root directory\n5. Run command \n```bash\n    stcgal firmware/stage9.hex\n```\n6. Waiting for stcgal output \n``` Waiting for MCU, please cycle power: ``` and connect USB-2-TTL adapter GND to hourglass kit board GND. Waiting for stcgal output \n``` Disconnected! ```\n7. Disconnect USB-2-TTL adapter and turn on power. As alternative way you can connect to hourglass kit board only GND and VCC pins of USB-2-TTL adapter.\n8. Enjoy :)  \n\n# Deep dive\n\nBuild tested in by me on Debian 12. You can build source code using Platformio IDE (recommended way) or from command line.\n\n## Build examples prerequisites\n\nThis examples was developed in [Platformio IDE](https://platformio.org/) using C programming language.\n\nYou can build source code using Platformio IDE (recommended way) or from command line.\n\n### Platformio (mandatory)\n\nFollow instructions from [Platformio IDE](https://platformio.org/).\nRun Platformio IDE and install 'Intel MCS-51 (8051)' and 'Native' platforms.\n\n### SDCC compiler (optional)\n\nSDCC is a C-compiler for small devices including STC microchips.\nIts installed with Platformio Intel MCS-51 (8051) platform.\nBut you can install it separately for using in the command line.\nOn Debian SDCC can be installed via apt. \n\nInstall need superuser permissions.\n```bash\napt-get install sdcc\n```\n\n### gcc compiler (optional)\n\nGCC compiler used for build unit tests. I wonder why you dont install gcc yet. \n\n### clang compiler (optional)\n\nClang compiler used for build unit tests.\n\n### Scons (optional)\n[SCons](https://scons.org/) is build tool used inside Platfromio. \nSCons based on Python\n\nBut you can install it separately for using in the command line.\n\n```bash\npipx install scons\n```\n\n\n### stcgal (optional)\nStcgal is a firmware upload tool. Its installed inside and used by Platfromio.\nStcgal is written on Python and can be install separately from Platfromio. \n\n```bash\npipx install stcgal\n```\n\n## How to build examples\n\nThere are three ways to build the code.\n\n### Build from Platformio IDE\nSimpliest way to build firmware is to use Platformio IDE.\n\nRun Platformio IDE.\n\n![Build firmware](images/Build_from_IDE.png)\n\nYou can also run local library tests.\n\n![Run tests](images/Run_tests.png)\n\n\n### Build from command line using Platfromio core\n\nTo build firmware from root directory run in terminal \n\n```bash\npio run -e STC15W204S\n```\n\nFor tests run \n```bash\npio test -e Native\n```\n\n### Build from command line using only SCons\nTo build firmware from root directory run in terminal\n```bash\nscons\n```\n\n## Upload firmware\n\n### Upload from Platformio IDE\n\n1. Connect USB-2-TTL adapter to hourglass kit board except GND\n2. Select upload menu item from Platformio STC15W204S environment.\n![Upload](images/Firmware_upload.png) and waiting for ```Cycling power: done``` output\n3. Connect USB-2-TTL adapter GND pin to hourglass kit board except GND pin. \n\n### Upload from command line using Platfromio core\n\n1. Connect USB-2-TTL adapter to hourglass kit board except GND\n2. In the terminal from root folder run command\n```bash\npio run -t upload -e STC15W204S\n```\nand waiting for ```Cycling power: done``` output\n\n3. Connect USB-2-TTL adapter GND pin to hourglass kit board except GND pin.\n\n### Upload from command line\n\nBefore upload firmware should be builded.\n\n1. Connect USB-2-TTL adapter to hourglass kit board except GND\n2. In the terminal from root folder run command\n```bash\nstcgal \u003cfile\u003e\n```\nwhere file file is name of firmware hex file.\n\n3. Waiting for ```Cycling power: done``` output\n4. Connect USB-2-TTL adapter GND pin to hourglass kit board except GND pin.\n\n# Electrical schema\n\nThe hourglass board has 57 LEDs, any combination of which the microcontroller can light, using just 12 I/O pins: the whole chip only has sixteen pins, and some of those are used for power, and sensing the push button.\n\nTake a look at the circuit diagram\n\n![Schema](images/Electrical_schema.jpg)\n\nLEDs are grouped into 12 lines. Line numeration started from 0.\nEach line excepth the last have 5 LEDs. Last line with number 11 have 2 LEDs.\nInside the line LEDs grouped by columns. There are 5 columns with numbers 0..4.\n\nLEDs are driven by P3 and P1 pins. P1[0..6] and P30, P31, P33, P36, P37 used.\nP1 select line and P3 select column.\n\nP32 is used to connect push button.\n\nLEDs doesnt have current-limiting resistors. MCU have total maximum current ~90mA. One MCU pin have maximum current ~20mA. Pin current is enough to drive LED, but in one moment only one LED should be turn on. The LEDs are multiplexed.\n\nEven lines 0, 2, 4...10 are direct and odd lines 1, 3, 5.. 11 are reverse. \n\nTo turn on LED in direct line P1x pin should have LOW value (logical zero) and P3x should have HIGH value (logical 1).\nTo turn on LED in reverse line P1x pin should have HIGH value (logical 1) and P3.x pin should have LOW value (logical 0).\n\nTo control this kind of matrix, the I/O pins have to be 'tri-state' type.\nThe microcontroller must be able to power each pin high, power the same pin low, or put that same pin into a high impedance ‘input’ state, where it effectively blocks any significant current from flowing in or out of it.\n\nThe STC15 chips achieve the tri-state operation (and more) for their I/O pins, by having PxM0 and PxM1 special function registers. That registers allow to control pins \nmode. There are four modes\n\n| P1M1[0..7] | P1M0[0..7] | Mode                         |\n|------------|------------|------------------------------|\n|  0         |  0         |  quasi bidirectional         |\n|  0         |  1         |  push-pull                   |\n|  1         |  0         |  input-only (high-impedance )|\n|  1         |  1         |  open Drain                  |\n\n| P1M3[0..7] | P3M0[0..7] | Mode                         |\n|------------|------------|------------------------------|\n|  0         |  0         |  quasi bidirectional         |\n|  0         |  1         |  push-pull                   |\n|  1         |  0         |  input-only (high-impedance )|\n|  1         |  1         |  open Drain                  |\n\nWe need push pull mode to turn LED on/off and input only otherwise.\n\nFor example for L1: line 0 driven by P10 and column 0 driven by P30. \nLine 0 is direct. L1 driven by P10 and P30.\n\nSet the following values:\n\n* P1M1 bit 0 to 0\n* P1M0 bit 0 to 1\n* P10 to 0\n* P3M1 bit 0 to 0\n* P3M0 bit 0 to 1\n* P30 to 1 \n\nOther bits of P1M1 = 1, P1M0 = 0\n\nFor L8: line is 1 and column 2. Line 1 is reverse. L8 driven by P11 and P33.\nSet the following values:\n\n* P1M1 bit 1 to 0\n* P1M1 bit 0 to 1\n* P11 to 1\n* P3M1 bit 3 to 0\n* P3M0 bit 3 to 1\n* P33 to 0\n\n# LED driving algorithms\n\nC or assembler can be used to programming STC MCU. This examples is written on C programming language.\n\nThere are two popular C language compilers for 8051 MCU architecture: Keil and SDCC.\nKeil is commertial, SDCC is free opensource compiler. Both have custom extensions. Keil C-language extentions doesnt compatible with SDCC, but may be easy translated.\n\nThis examples developed with STC15204S and STC15W201S compatibility in mind. \nTherefore maximum firmware size is less 1024 bytes. \n\nExamples split to stages. Stages demonstrate the growth of functionalty from simple to more complex levels. \n\nStages source code is placed in stages subfolders.\n\n## Stage 0. Install/configure sources and tools\n\nInstall and configure tools and build firmware from empty C language program. \n\n## Stage 1. Turn single LED on. \n\n***Step 1. Add STC hardware library.***  \nPlatfromio maintains centralized library registry. \n\nTo programming STC MCU add STC15 hardware library. This library includes MCU registers definition and covinient routines to manipulate with MCU resources (frequency, timers and so on). \n\nAdd to platfrom.io file lines\n```ini\nlib_deps = https://github.com/mgoblin/STC15lib.git#0.9.0\n```\n\n***Step 2. Create local library 'leds1' to turn led on/off*** \nA library is a way to manage complexity and separation of concerns.\nLocal libraries in platformio are located in the libs folder.\n\nAs an example of good firmware structure LEDs manipulation lib was created. Main method use library.\n\nLeds1 library declares in leds1.h to functions:\n - ```void leds_off()``` to turn all leds off\n - ```void led_1_on()``` to turn on LED1.\n\n The STC hardware library makes implementation of the leds1 library functions very clear and straightforward.\n\n ```C\nvoid leds_off()\n{\n    // All P3 and P1 pins should be in input only mode.\n    // Pins in nnput only mode have high impedance and low current consuption.  \n    pin_port_input_only_init(P3);\n    pin_port_input_only_init(P1);\n}\n ```\n\n See [STC hardware library pin module docs](https://mgoblin.github.io/STC15lib/docs/html/group__pin.html) for additional details.\n\n***Step 3. Fast led on/off***  \nAll details of LED state manipulation is under the hood in the leds1 library. Main method of firmware is simple\n\n```C\nvoid main()\n{\n    while (1)\n    {\n        // Put all LEDs off \n        leds_off();\n        f_delay_ms(LED_BLINK_DELAY_MS);\n        \n        // Put L1 on\n        led_1_on();\n        f_delay_ms(LED_BLINK_DELAY_MS);\n    }\n}\n```\nRapid switching of the LED on and off is perceived by the human eye as the LED being constantly on.\n\n\u003e[!NOTE]\n\u003e LED1 and some other LEDs are biased by USB-2-TTL adapter. After firmware upload diconnect Rx and Tx pins from USB-2-TTL adapter.   \n\n## Stage 2. Turn single LED on using timer.\n\nIn the stage1 led on/of code was placed in the main function and fast blinking was implemented using delays.\n\nIn stage2 code is moved to timer interrupt service routine.\n\n***Step1. Initialize and start timer 0.***\n\nAs a first step timer is initialized and started in main function and main function go to endles waiting.\n\n```C\nvoid main()\n{\n    timer0_mode0_1T_init();\n    timer0_mode0_start(TIMER_TICKS);\n\n    while (1) {}\n}\n```\n\nTimer interrupt routine is declared as \n```C\nvoid timerISR() __interrupt(1) __naked\n```\n\nSDCC keyword `__interrupt` means that this is interrupt handler and argument 1 is interrupt number. Timer0 has interrupt number 1.\n\nThe second SDCC keyword `__naked` means that compiler shouled not generate prolog and epilog with stack pop/push for register save/restore and other auxiliary code. This allows you to reduce the firmware size, but requires explicit code to rerun from interrup`reti`.\n\nWe will not use `__naked` interrupt handlers in the next stages because it need manual controlling save/restore registers. But its possible.\n\n***Step2. Implement timer0 interrupt handler.***\n\nThe second of the programming techniques is using bit flag variable inside interrupt handler (interrupt serive routine).\n\n```C\n__bit is_led_1_on = 0; // L1 On/Off bit flag \n```\n\nSTC15 have special SRAM bit addressable area and SDCC compiler have keyword `__bit` for bit flag type.\n\nAlternative way to implement \"memoizing\" L1 state is check P3.0 and P1.0 pin values and mode. See the pseudo code below.\n\n```C\nbool is_led1_on()\n{\n    return P10 == 0 \u0026\u0026 P3.0 == 1 \u0026\u0026 \u003ccheck P10 and P30 mode is push pull\u003e\n}\n```\n\nThis way doesnt need additional SRAM, but increase code size.\n\nWe have the requirement to frimware size less then 1K and we dont exhause SRAM. Therefore bit flag is more preferable way to implement \"memoizing\" L1 state in this firmware.\n\n## Stage 3. Turn on line 0 consists of L1-L5 LEDs. \n\nStage 3 demostrate how to turn on line 0. Line 0 consists of L1, L2, L3, L4, L5. Line 0 is even line.\n\n***Step 1. Initialize pins mode and timer 0***\n\nLets see the main function code. Its obviousю\n\n```C\nvoid main()\n{\n    // Initialzie P3 pins\n    pin_port_pull_push_init(P3);\n    P3 = LOW;\n\n    // Initialzie P1 pins\n    pin_port_input_only_init(P1);\n    pin_push_pull_init(P1, 0);\n    P1 = LOW;\n\n    // Initialize and start timer 0 for L1-L5 turn on\n    timer0_mode0_1T_init();\n    timer0_mode0_start(TICKS_COUNT);\n\n    while (1) {}\n}\n```\n\nAll P3 pins set to push pull mode and LOW (logical 0) value.\nOnly P10 pin initialized to push pull mode, other P1 pins is in input only mode.  \nThis combinataion of P1 and P3 modes is enabling put L1-L5 leds on/off by changing only P3 pins value. \n\n\n***Step 2. Turn L1-L5 on***\n\nTimer 0 interrupt handler is responsible for cyclically fast sequentialy on/off one of L1-L5.\n\nThe P3 values ​​corresponding to the L1-L5 on/off states are stored in the P3_pins array. The current LED index is stored in the pins_idx variable.\n\nThe timer 0 interrupt handler sets the P3 value and increments pins_idx. In the case of L5 on, pins_idx is cleared to the initial value of 0.\n\n```C\nvoid timerISR() __interrupt(1)\n{\n    P3 = P3_pins[pins_idx];\n    pins_idx == P3_PINS_COUNT - 1 ? pins_idx = 0 : pins_idx++;\n}\n```\n\nThe timer 0 interrupt period is set small enough to give the impression that all LEDs are on.\n\n## Stage 4. Turn on all LEDs.\n\nTo turn on all leds main function doesnt changed, only timer 0 interrupt handler should be modified.\n\nFirst of all `uint8_t led_column` and `uint8_t led_line` variables added to store current column and line.\n\nThe timer0 interrupt handler code is obvious, all details are moved to utility functions.\n\n```C\nvoid timerISR() __interrupt(1)\n{\n    turn_led_on(led_column, led_line);\n\n    if (is_line_iterated(led_column))\n    { \n        led_column = 0;\n        next_line();\n    } \n    else \n    {\n        led_column++;\n    }\n}\n```\n\n***Step1. Turn led on by column and line***\n\nLets see `turn_led_on` function code. \n\n```C\nvoid turn_led_on(uint8_t column, uint8_t line)\n{\n    uint8_t line_idx = line \u003e\u003e 1; // get P1 pin number into line_idx variable\n\n    // Set P1.\u003cline_idx\u003e pin to push pull mode \n    pin_port_input_only_init(P1);\n    pin_push_pull_init(P1, line_idx);\n\n    if ((line \u0026 0x01) == 0) // Is line odd or even?\n    {\n        // Line is even. P1 values need to be inverted\n        P3 = P3_pins[column];\n        P1 = ~P1_pins[line_idx];\n    }\n    else \n    {\n        // Line is odd. P3 values need to be inverted\n        P3 = ~P3_pins[column]; \n        P1 = P1_pins[line_idx];\n    }\n}\n```\n\nP1 pin number is equals to line/2.  \n\nFor even line LED P3 pin should have HIGH value (encoded in P3_pins[column]) and P1 should have LOW value. HIGH values for P1 are encoded in P1_pins[line_idx], therefore P1_pins[line_idx] value bits should be inverted. \n\nFor odd lines P3[column] should be inverted to get P3 value and P1_pins[line_idx] not.\n\n***Step 2. Change and store column and line for the next interrupt handler call***\n\nLast part of interrupt handler is about changing line and column for the next handler call.\n\nIf all leds in then cureent line iteratred we need increment line nubmer and set column to 0 to start iterate next line, otherwise only increments column number.   \nIf we interate all lines line set to 0.\n\nFunction `is_line_iterated` implementation is very simple.\n\n```C\nbool is_line_iterated(uint8_t column) { return column == P3_PINS_COUNT - 1; }\n```\n\nAnd the next_line function is cyclically increment line number.\n\n```C\nvoid next_line()\n{\n    led_line = led_line == (P1_PINS_COUNT \u003c\u003c 1) - 1 ? 0 : led_line + 1;\n}\n```\n\n## Stage 5. Desсribe page and iterate through\n\nSo far so good, now it's time to finish the experiments and start the final implementation of the firmware.\n\nFirst part of puzzle is how to describe the page? The second part is how to display page and the last puzzle part is how to change pages.\n\nAt this stage, the first and second parts of the puzzle will be solved.\n\n***Step 1. Abstractions and design***\n\nAnimation is the series of pages. \n\nThe page represents the on/off state of all 57 LEDs. \n\nPage display algorithm is: \n\n- get L1 state (on/off)\n- if L1 state is on - turn on only this LED, other LEDs are off\n- if L1 state is off - turn all LEDs off\n- get next LED state\n- if all 57 LEDs are iterated start over\n\nOk, how to describe \"only this LED is on  or all LEDs off\" in terms of electrical houglass kit? There is a state of P1M0, P1M1, P1, P3M0, PM31 and P3 registers.\n\nHow to implement the algorithm for displaying a page? And how to abstract the calculation of the next state of the LEDs from the application of the calculated state?\n\nOne possible answer is the iterator pattern.\n\nIterator is initialized with page description and has method next that return next state for displaying page. The state is a struct with P1M0, P1M1, P1, P3M0, PM31 and P3 fields. Iterator is endless, calling next to iterate page LEDs  over and over.\n\n\n***Step 2. Page description format***\n\nEach LED state is encoded as a bit. 1 - LED turn on state and 0 LED is off state.\nPage is a set of bits. bit 0 is encode L1, bit 1 - L2 and so on, bit 56 endcode L57 state. Therefore page could be declare as a follows:\n\n```C\ntypedef uint64_t ehgk_page_t;\n```\nFirst 57 bits (0-56) encode LED states and high bits from 57 to 63 are unused and always 0.\n\nSome ofter used page templates could be declared as\n\n```C\n#define EMPTY_PAGE (uint64_t)0\n#define ALL_LEDS_PAGE (uint64_t) 0x1FFFFFFFFFFFFF\n```\n\nTwo page manipulation macros are defied as \n\n```C\n#define ehgk_page_add_led(p, led)       (p |= led)\n#define ehgk_page_delete_led(p, led)    (p \u0026= ~led)\n```\n\nBut for initial describe the page bitwise or operation will be used.\nFor convenience LED enum is declared as \n\n```C\ntypedef enum led_t\n{\n    L1  = (uint64_t) 0x1,\n    L2  = (uint64_t) 0x2,\n    L3  = (uint64_t) 0x4,\n    L4  = (uint64_t) 0x8,\n    L5  = (uint64_t) 0x10,\n    L6  = (uint64_t) 0x20,\n    L7  = (uint64_t) 0x40,\n    L8  = (uint64_t) 0x80,\n    ...\n    L56 = (uint64_t) 0x80000000000000,\n    L57 = (uint64_t) 0x100000000000000,\n} led_t;\n```\n\nUsing led_t enum page could be described as follows:\n\n```C\nehgk_page_t page = L1 | L2; // Page with only L1 and L2 turn on.\n```\n\n***Step 3. Page iterator***\n\nPage iterator should have two methods\n- init for intiialize iterator with page \n- next for getting next state\n\nThis methods are declared as follows:\n\n```C\nvoid ehgk_iterator_init(ehgk_page_t page);\nehgk_iter_result_t* ehgk_iterator_next();\n```\n\nwhere ehgk_iter_result_t is\n\n```C\ntypedef struct ehgk_iter_result_t\n{\n    uint8_t p3;\n    uint8_t p1;\n    uint8_t p3m0;\n    uint8_t p3m1;\n    uint8_t p1m0;\n    uint8_t p1m1;\n} ehgk_iter_result_t;\n```\n\nThe iterator itself is declared as\n\n```C\ntypedef struct ehgk_iterator_t\n{\n    ehgk_page_t page;\n    uint64_t led_mask;\n    uint8_t column_idx;\n    uint8_t line_idx;\n} ehgk_iterator_t;\n```\n\nwhere page is store page_t definition, led_mask stores iterated LED position, columnt_idx and line_idx stores iterated LED column and line numbers.\n\nIterator module implementation is contains declaration of variables\n\n```C\n/**\n * Module iterator instance\n */\nehgk_iterator_t iterator;\n\n/**\n * Module last iteration result instance\n */\nehgk_iter_result_t iter_result;\n\n```\n\nThe implementation of iterator methods is discussed in the next step.\n\n***Step 4. Puts all together into the library***\n\nThe implementation of iterator methods is reused in this and next stages. Therefore, it makes sense to move them to a separate library. This library is placed to lib/ehgk_page folder.\n\nLibrary files are:\n| File                  | Description                                   |\n|-----------------------|-----------------------------------------------|\n| ehgk_page.h           | page_t and page manipulation macroses         |\n| ehgk_page_iterator.h  | iterator structures and methods declarations  |\n| ehgk_page_iterator.c  | iterator methods implementation               |\n\nLets see iterator mehtods implementation.\n\nInit method is simple. Its store page value into internal iterator state, make initial led mask value corresponding to L1 mask (bit0 is 1) and reset column and line values.\n\n```C\nvoid ehgk_iterator_init(ehgk_page_t page)\n{\n   //Save page and reset iterator state\n   iterator.page = page;\n   iterator.led_mask = 1;\n   iterator.column_idx = 0;\n   iterator.line_idx = 0;\n}\n```\n\nMethod next is more complex. Its consists of two parts\n - Generate P1 and P3 pins values and modes \n - Prepare to the next iteraction. Increment internal state.\n\nThe first part fills iter_result variable. The second part shift left led_mask and increment column and line indexes.   \nBoth parts used algorithms from previous stage. The iterate_result calculation algorithm takes into account the need to leave contact P32 always in the input only mode.  \n\n***Step 5. Unit tests***\n\nOne of the advantages of separating the computation of state and its applying is possibility to unit testing of ehgk_page library.\n\nThere are few approaches to microcontroller unit testing\n- testing on embedded target (on real mcu deive)\n- testing on emulator\n- testing on native platfrom (for example linux) \n\nFirst to approaches is unavailable or hard in STC platform.\n\nNative tests are intended for the project components that are independent of physical hardware. \n\nehgk_page library is independed from hardware, because it not use direct MCU registers getting/setting. Library can be build with gcc or clang for native plaform (in my case linux ) with unit test library dependency. [Unity](http://throwtheswitch.org/) used as unit test library. \n\nPlatformio has Unity testing support \"from the box\". All you need place unit tests code in test folder. But this is not big deal to build and run tests from command line.\n\nThe first approach start plaformio from command line\n\n```bash\npio test -e Native\n```\n\nThe second approach is build tests to executable binary and run it \n\nBoth approaches supported. \n\nUnit tests sources entry point is test/main.c\n\n```C\n#include \u003cunity.h\u003e\n#include \"ehgk_page_test.h\"\n#include \"ehgk_page_iterator_test.h\"\n\nvoid setUp(void) {\n    // set stuff up here\n}\n\nvoid tearDown(void) {\n    // clean stuff up here\n}\n\nint main( int argc, char **argv) {\n    UNITY_BEGIN();\n\n    RUN_TEST(test_ehgk_page_value_is_empty_after_init);\n    RUN_TEST(test_ehgk_page_add_led1);\n    RUN_TEST(test_ehgk_page_add_led2);\n    RUN_TEST(test_ehgk_page_add_leds_1_2);\n\n    RUN_TEST(test_ehgk_page_delete_led1_from_empty_page);\n    RUN_TEST(test_ehgk_page_delete_led1);\n    RUN_TEST(test_ehgk_page_delete_led2);\n    RUN_TEST(test_ehgk_page_delete_led_1_2);\n\n    RUN_TEST(test_iterator_init);\n    RUN_TEST(test_iterate_once_empty_page);\n    RUN_TEST(test_iterate_once_one_led_page);\n    \n    RUN_TEST(test_iterate_line0_columns_empty_page);\n    RUN_TEST(test_iterate_line0_columns_L1_page);\n    RUN_TEST(test_iterate_line0_columns_L2_page);\n    RUN_TEST(test_iterate_line0_columns_L3_page);\n    RUN_TEST(test_iterate_line0_columns_L4_page);\n    RUN_TEST(test_iterate_line0_columns_L5_page);\n\n    RUN_TEST(test_iterate_line0_columns_L2_L3_page);\n\n    RUN_TEST(test_iterate_line1_empty_page);\n    RUN_TEST(test_iterate_line1_all_columns_page);\n    RUN_TEST(test_iterate_all_lines_empty_page);\n\n    UNITY_END();\n}\n```\n\nIts imports untiy.h header and Run test cases. Testcases split into to suites - page tests and iterator tests.    \nTest suite is C source file with test cases. Test case is a C function that use Unity macroses to asserting expected and actual values. \n\n***Step6. Main program***\n\nNow that all the \"magic\" of calculating the LED state is encapsulated in the ehgk_page library, we can start developing the main program.\n\nThe main program simply turns on all the LEDs excluding outer ones.\n\n![Stage5 image](images/stages/stage5/stage5.jpg)\n\n```C\n/*======================== STC15 HAL headers ====================================*/\n#include \u003cpin.h\u003e\n/*========================End of STC15 HAL headers ==============================*/\n\n#include \u003cehgk_page_iterator.h\u003e\n\nextern ehgk_iterator_t iterator;\nextern ehgk_iter_result_t iter_result;\n\nvoid main()\n{\n    ehgk_iterator_init(\n        L2 | L3 | L4 | L5 | L6 | \n         L9 | L10 | L11 | L12 |\n          L15 | L16 | L17 |\n             L20 | L21 |\n                L24 |\n\n                L28 |\n                L29 |\n                L30 |\n                \n                L34 |\n             L37 | L38 |\n          L41 | L42 | L43 |\n         L46 | L47 | L48 | L49 |\n        L52 | L53 | L54 | L55 |L56\n    ) ;\n\n    while (1)\n    {\n        ehgk_iterator_next();\n\n        P1 = iter_result.p1;\n        P3 = iter_result.p3;\n    \n        P1M0 = iter_result.p1m0;\n        P1M1 = iter_result.p1m1;\n        P3M0 = iter_result.p3m0;\n        P3M1 = iter_result.p3m1;\n    }\n}\n```\n\nFor the firmware size minimization extern variable `iter_result` declared. This variable holds last call result of `ehgk_iterator_next()`.  \n\n## Stage 6: Move the page display inside the timer\n\nIn the previous stage main program routine was apply page state. Continuous calculation and page refresh takes too much CPU time. Moving this functionality to the timer enhances firmware. \n\n```C\n/*======================== STC15 HAL headers ====================================*/\n#include \u003cpin.h\u003e\n#include \u003ctimer0_mode0.h\u003e\n/*========================End of STC15 HAL headers ==============================*/\n\n#include \u003cehgk_page_iterator.h\u003e\n\nextern ehgk_iterator_t iterator;\nextern ehgk_iter_result_t iter_result;\n\nvoid timerISR() __interrupt(1)\n{\n    ehgk_iterator_next();\n\n    P1 = iter_result.p1;\n    P3 = iter_result.p3;\n\n    P1M0 = iter_result.p1m0;\n    P1M1 = iter_result.p1m1;\n    P3M0 = iter_result.p3m0;\n    P3M1 = iter_result.p3m1;\n}\n\nvoid main()\n{\n    ehgk_iterator_init(\n        L2 | L3 | L4 | L5 | L6 | \n         L9 | L10 | L11 | L12 |\n          L15 | L16 | L17 |\n             L20 | L21 |\n                L24 |\n\n                L28 |\n                L29 |\n                L30 |\n                \n                L34 |\n             L37 | L38 |\n          L41 | L42 | L43 |\n         L46 | L47 | L48 | L49 |\n        L52 | L53 | L54 | L55 |L56\n    );\n\n    \n    timer0_mode0_1T_init();\n    timer0_mode0_direct_start(0xF5, 0x00);\n\n    while (1)\n    {\n    }\n}\n```\n\n## Stage 7. Animate picture inside main function\n\nNow we can describe and statically displaying one page. Lets start animation.\n\n***Step 1. Keeping page sequence***\n\nA page animation is simply an array of pages. It is declared in the separate header file `pages_definition.h`. \n\nThere are two page animations. The first one is main animation of upper and lower triangles and the second one is LED29-LED30 auxialiary animation for the sand flow.    \nThe main animation have 29 pages and sand flow animation have only two pages. The main animation is very similar but not the same as ogiginal firmwares. I deliberately did not repeat the algorithm for turning on and off the LEDs from the original firmware, but you can easily make the necessary changes to the main animation\n\n***Step 2. Animate pages***\n\nHere we step back and implements page displaing and swaping pages in main function. This decision take smaller firmware size, but not such elegant as using timer to page displaying. \n\nFirst lets see function to display page for some time \n\n```C\nvoid displayPage(uint16_t iteration_delay_ticks)\n{\n    // Iterate through page LEDs and on/of LEDs according to page definition\n    for(uint16_t i = 0; i \u003c iteration_delay_ticks; i++)\n    {\n        ehgk_iterator_next();\n\n        P1 = iter_result.p1;\n        P3 = iter_result.p3;\n\n        P1M0 = iter_result.p1m0;\n        P1M1 = iter_result.p1m1;\n        P3M0 = iter_result.p3m0;\n        P3M1 = iter_result.p3m1;\n    }\n}\n```\n\nTime to dispalyaing page get in the function parameter and using as iterations count.\n\nThe main funtion is straigforward and easy readable\n\n```C\nvoid main()\n{\n    while (1)\n    {\n        // Iterate through pages\n        for(uint8_t page_idx = 0; page_idx \u003c PAGES_COUNT; page_idx++)\n        {\n            // Select next page to display\n            ehgk_iterator_init(pages[page_idx]);\n            // Display current page\n            displayPage(ORDINAL_PAGE_DELAY);\n            \n            // Animate sand flow\n            for(uint8_t idx = 0; idx \u003c L29_L30_PAGES_COUNT; idx++)\n            {\n                ehgk_iterator_init(pages[page_idx] | l29_l30_pages[idx]);\n                displayPage(SAND_FLOW_DELAY);\n            }\n        }\n    }\n}\n```\nThe firware size is 965 bytes. Good enough.\n\nFirmware video (clickable)    \n[![Stage 7 video](images/stages/stage7/stage7.jpg)](https://rutube.ru/video/adc7e955e35f0e294c2e2894f9f63e6f/)\n\n## Stage 8. Changing animation speed\n\nThis is last puzzle part to emulate original firmware - button from electronic hourglass kit. Pressing the button changes the animation speed.\n\nThe button is connected to pin P3.2. Pin P3.2 has a general purpose and also interrupt 0 (INT0).\n\n***Step 1. Button push handler stub***\n\nTo enable INT0, you must first enable the microcontroller interrupts. INTO can be thrown on only falling edge or on rasing and falling edges. STC hardware libraty routines are used for INT0 configruation.\n\n```C\nvoid int0_ISR() __interrupt(0)\n{\n    ...\n}\n\nvoid main()\n{\n    // Configure button press handler\n    enable_mcu_interrupts();\n    enable_int0_interrupt();\n    set_int0_interrupt_trigger(ONLY_FALLING_EDGE);\n    ...\n}\n```\n\n***Step 2. Changing animation speed using clock frequency divider***\n\nThe speed of page animation is determined by the values ​​of the `displayPage` function argument.\n\nThe first idea that comes to mind is to create variable for store delay value and change this variable inside INT0 handler. \n\nBut there is another way - to speed up/slow down the MCU as a whole. This way dont need variables, MCU has special frequency divider register CLK_DIV. Low three bits of CLK_DIV are encode MCU frequency divider value in range 1..128.    \nIncrementing CLK_DIV speed down MCU on 2^n. By default 'lowest speed' value is set using `set_frequency_divider_scale` call.\n\nButton press handler source code is below:\n\n```C\nvoid int0_ISR() __interrupt(0)\n{\n    // Three low CLK_DIV bits are frequency divider scaler \n    if(CLK_DIV == MIN_CPU_FREQ_DIVIDER) \n    { \n        CLK_DIV = MAX_CPU_FREQ_DIVIDER; \n    } \n    else \n    { \n        CLK_DIV--;\n    }\n}\n```\n\n## Stage 9. New animation pattern\n\nCreating new aniamations now is easy.   \nAs an example new pattern is imlemented.\n\nFirmware video (clickable)    \n[![See pattern video](images/stages/stage9/stage9.jpg)](https://rutube.ru/shorts/8395865b3d4660c745af35dafddf4a17/)\n\nThis pattern have only one animation (no L9-L30 animaption). And the page displaying and changing code is similar to the previuos one.\n\n```C\nvoid main()\n{\n    // Configure button handler\n    enable_mcu_interrupts();\n    enable_int0_interrupt();\n    set_int0_interrupt_trigger(ONLY_FALLING_EDGE);\n\n    // Set animation speed\n    set_frequency_divider_scale(MAX_CPU_FREQ_DIVIDER);\n\n    while (1)\n    {\n        // Iterate through pages\n        for(uint16_t page_idx = 0; page_idx \u003c PAGES_COUNT; page_idx++)\n        {\n            // Select next page to display\n            ehgk_iterator_init(pages[page_idx]);\n            // Display page\n            displayPage(ORDINAL_PAGE_DELAY);\n        }\n    }\n}\n```\n\nMajor changes is in the `pages_definition.h` header. There is a place where pages are described - pages array. Animation contains 15 pages and firmware size is 791 byte.\n\n# Similar DIY projects\n\nI am not first who reprogramming electronical hourglass kit.I would like to express my gratitude to the authors of the following projects\n\n## Ceptimus firmware\n\n[LED hourglass kit programming](https://ceptimus.co.uk/index.php/2022/06/22/led-hourglass-kit-programming/)\n\nThis project was initial point of my journey with electonic hourglass kit.    \nThe project has a good and detailed description of electrical shceme and LED control principle.\n\n## Rick-100 firmware\n\n[Github repo](https://github.com/Rick-100/STC-hourglass-kit)\n\nThis implementation use EEPROM to store page animations. The source code has detailed comments. Pages visual editor is available.\n\n# Next ideas\n\nIn my humble opinion, every firmware is not final and can be improved. Below some improvments ideas are described.\n\n## Other ways to changing animation speed\n\n- Store page displaying delay into the variable and changing variable value on button pressed. \n- Moving page displaying code to timer0 interrupt, moving page swapping to timer2 interrupt. \n \n## Using idle mode to reduce power consumption\n\nNow the MCU is constantly executing code and consuming power. The MCU spends most of its time in delay cycles.\n\nMCU idle mode can be used to sleep betwwen page swappings. This approach need to moving most of code to the timers interrupts. In the idle mode power consumption is reduced.\n\n## Storing pages into the EEPROM\n\nIn the current firmware, page definitions are stored in the microcontroller's flash memory along with the page manipulation code.\n\nSTC15 MCU have EEPROM memory. Its a good place to store page definitions. Moving page definitions to EEPROM frees up flash memory space and the firmware code can be larger.\n\nSTC15W201S have 1k flash and 4k EEPROM, STC15W204S have 4k flash and 1k EEPROM.\n\nFor STC15W204S, page definition storage can be done jointly in EEPROM and flash memory.\n\n## Creating pages and animation visual editor\n\nPage defintion is not complex with array and bitwise OR inside page. But visual page and animaption editor is more easy.\n\n\nThanx for reading. With best regards, Michael.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmgoblin%2Felectronichourglasskit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmgoblin%2Felectronichourglasskit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmgoblin%2Felectronichourglasskit/lists"}