diff options
| author | Selene ToyKeeper | 2023-11-02 17:16:25 -0600 |
|---|---|---|
| committer | Selene ToyKeeper | 2023-11-02 17:16:25 -0600 |
| commit | 7cb4fe0944b839f28dfd96a88a772cd6a8b58019 (patch) | |
| tree | 8d3b203f1650edc28b1f67e1589e3bc870b33fa6 /fsm | |
| parent | added LICENSE (GPLv3) (diff) | |
| download | anduril-7cb4fe0944b839f28dfd96a88a772cd6a8b58019.tar.gz anduril-7cb4fe0944b839f28dfd96a88a772cd6a8b58019.tar.bz2 anduril-7cb4fe0944b839f28dfd96a88a772cd6a8b58019.zip | |
reorganized project files (part 1)
(just moved files, didn't change the contents yet,
and nothing will work without updating #includes and build scripts and stuff)
Diffstat (limited to 'fsm')
| -rw-r--r-- | fsm/COPYING | 674 | ||||
| -rw-r--r-- | fsm/adc.c | 573 | ||||
| -rw-r--r-- | fsm/adc.h | 112 | ||||
| -rw-r--r-- | fsm/chan-aux.c | 11 | ||||
| -rw-r--r-- | fsm/chan-aux.h | 25 | ||||
| -rw-r--r-- | fsm/chan-rgbaux.c | 35 | ||||
| -rw-r--r-- | fsm/chan-rgbaux.h | 72 | ||||
| -rw-r--r-- | fsm/channels.c | 357 | ||||
| -rw-r--r-- | fsm/channels.h | 141 | ||||
| -rw-r--r-- | fsm/eeprom.c | 112 | ||||
| -rw-r--r-- | fsm/eeprom.h | 52 | ||||
| -rw-r--r-- | fsm/events.c | 198 | ||||
| -rw-r--r-- | fsm/events.h | 221 | ||||
| -rw-r--r-- | fsm/main.c | 211 | ||||
| -rw-r--r-- | fsm/main.h | 10 | ||||
| -rw-r--r-- | fsm/misc.c | 312 | ||||
| -rw-r--r-- | fsm/misc.h | 68 | ||||
| -rw-r--r-- | fsm/pcint.c | 96 | ||||
| -rw-r--r-- | fsm/pcint.h | 15 | ||||
| -rw-r--r-- | fsm/ramping.c | 259 | ||||
| -rw-r--r-- | fsm/ramping.h | 167 | ||||
| -rw-r--r-- | fsm/random.c | 16 | ||||
| -rw-r--r-- | fsm/random.h | 12 | ||||
| -rw-r--r-- | fsm/spaghetti-monster.h | 75 | ||||
| -rw-r--r-- | fsm/spaghetti-monster.txt | 325 | ||||
| -rw-r--r-- | fsm/standby.c | 105 | ||||
| -rw-r--r-- | fsm/standby.h | 68 | ||||
| -rw-r--r-- | fsm/states.c | 105 | ||||
| -rw-r--r-- | fsm/states.h | 37 | ||||
| -rw-r--r-- | fsm/wdt.c | 197 | ||||
| -rw-r--r-- | fsm/wdt.h | 20 |
31 files changed, 4681 insertions, 0 deletions
diff --git a/fsm/COPYING b/fsm/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/fsm/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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 <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/fsm/adc.c b/fsm/adc.c new file mode 100644 index 0000000..31b250f --- /dev/null +++ b/fsm/adc.c @@ -0,0 +1,573 @@ +// fsm-adc.c: ADC (voltage, temperature) functions for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +// override onboard temperature sensor definition, if relevant +#ifdef USE_EXTERNAL_TEMP_SENSOR +#ifdef ADMUX_THERM +#undef ADMUX_THERM +#endif +#define ADMUX_THERM ADMUX_THERM_EXTERNAL_SENSOR +#endif + +#include <avr/sleep.h> + + +static inline void set_admux_therm() { + #if (ATTINY == 1634) + ADMUX = ADMUX_THERM; + #elif (ATTINY == 25) || (ATTINY == 45) || (ATTINY == 85) + ADMUX = ADMUX_THERM | (1 << ADLAR); + #elif (ATTINY == 841) // FIXME: not tested + ADMUXA = ADMUXA_THERM; + ADMUXB = ADMUXB_THERM; + #elif defined(AVRXMEGA3) // ATTINY816, 817, etc + ADC0.MUXPOS = ADC_MUXPOS_TEMPSENSE_gc; // read temperature + ADC0.CTRLC = ADC_SAMPCAP_bm | ADC_PRESC_DIV64_gc | ADC_REFSEL_INTREF_gc; // Internal ADC reference + #else + #error Unrecognized MCU type + #endif + adc_channel = 1; + adc_sample_count = 0; // first result is unstable + ADC_start_measurement(); +} + +inline void set_admux_voltage() { + #if (ATTINY == 1634) + #ifdef USE_VOLTAGE_DIVIDER // 1.1V / pin7 + ADMUX = ADMUX_VOLTAGE_DIVIDER; + #else // VCC / 1.1V reference + ADMUX = ADMUX_VCC; + #endif + #elif (ATTINY == 25) || (ATTINY == 45) || (ATTINY == 85) + #ifdef USE_VOLTAGE_DIVIDER // 1.1V / pin7 + ADMUX = ADMUX_VOLTAGE_DIVIDER | (1 << ADLAR); + #else // VCC / 1.1V reference + ADMUX = ADMUX_VCC | (1 << ADLAR); + #endif + #elif (ATTINY == 841) // FIXME: not tested + #ifdef USE_VOLTAGE_DIVIDER // 1.1V / pin7 + ADMUXA = ADMUXA_VOLTAGE_DIVIDER; + ADMUXB = ADMUXB_VOLTAGE_DIVIDER; + #else // VCC / 1.1V reference + ADMUXA = ADMUXA_VCC; + ADMUXB = ADMUXB_VCC; + #endif + #elif defined(AVRXMEGA3) // ATTINY816, 817, etc + #ifdef USE_VOLTAGE_DIVIDER // 1.1V / ADC input pin + // verify that this is correct!!! untested + ADC0.MUXPOS = ADMUX_VOLTAGE_DIVIDER; // read the requested ADC pin + ADC0.CTRLC = ADC_SAMPCAP_bm | ADC_PRESC_DIV64_gc | ADC_REFSEL_INTREF_gc; // Use internal ADC reference + #else // VCC / 1.1V reference + ADC0.MUXPOS = ADC_MUXPOS_INTREF_gc; // read internal reference + ADC0.CTRLC = ADC_SAMPCAP_bm | ADC_PRESC_DIV64_gc | ADC_REFSEL_VDDREF_gc; // Vdd (Vcc) be ADC reference + #endif + #else + #error Unrecognized MCU type + #endif + adc_channel = 0; + adc_sample_count = 0; // first result is unstable + ADC_start_measurement(); +} + + +#ifdef TICK_DURING_STANDBY +inline void adc_sleep_mode() { + // needs a special sleep mode to get accurate measurements quickly + // ... full power-down ends up using more power overall, and causes + // some weird issues when the MCU doesn't stay awake enough cycles + // to complete a reading + #ifdef SLEEP_MODE_ADC + // attiny1634 + set_sleep_mode(SLEEP_MODE_ADC); + #elif defined(AVRXMEGA3) // ATTINY816, 817, etc + set_sleep_mode(SLEEP_MODE_STANDBY); + #else + #error No ADC sleep mode defined for this hardware. + #endif +} +#endif + +inline void ADC_start_measurement() { + #if (ATTINY == 25) || (ATTINY == 45) || (ATTINY == 85) || (ATTINY == 841) || (ATTINY == 1634) + ADCSRA |= (1 << ADSC) | (1 << ADIE); + #elif defined(AVRXMEGA3) // ATTINY816, 817, etc + ADC0.INTCTRL |= ADC_RESRDY_bm; // enable interrupt + ADC0.COMMAND |= ADC_STCONV_bm; // Start the ADC conversions + #else + #error unrecognized MCU type + #endif +} + +// set up ADC for reading battery voltage +inline void ADC_on() +{ + #if (ATTINY == 25) || (ATTINY == 45) || (ATTINY == 85) || (ATTINY == 1634) + set_admux_voltage(); + #ifdef USE_VOLTAGE_DIVIDER + // disable digital input on divider pin to reduce power consumption + VOLTAGE_ADC_DIDR |= (1 << VOLTAGE_ADC); + #else + // disable digital input on VCC pin to reduce power consumption + //VOLTAGE_ADC_DIDR |= (1 << VOLTAGE_ADC); // FIXME: unsure how to handle for VCC pin + #endif + #if (ATTINY == 1634) + //ACSRA |= (1 << ACD); // turn off analog comparator to save power + ADCSRB |= (1 << ADLAR); // left-adjust flag is here instead of ADMUX + #endif + // enable, start, auto-retrigger, prescale + ADCSRA = (1 << ADEN) | (1 << ADSC) | (1 << ADATE) | ADC_PRSCL; + // end tiny25/45/85 + #elif (ATTINY == 841) // FIXME: not tested, missing left-adjust + ADCSRB = 0; // Right adjusted, auto trigger bits cleared. + //ADCSRA = (1 << ADEN ) | 0b011; // ADC on, prescaler division factor 8. + set_admux_voltage(); + // enable, start, auto-retrigger, prescale + ADCSRA = (1 << ADEN) | (1 << ADSC) | (1 << ADATE) | ADC_PRSCL; + //ADCSRA |= (1 << ADSC); // start measuring + #elif defined(AVRXMEGA3) // ATTINY816, 817, etc + VREF.CTRLA |= VREF_ADC0REFSEL_1V1_gc; // Set Vbg ref to 1.1V + // Enabled, free-running (aka, auto-retrigger), run in standby + ADC0.CTRLA = ADC_ENABLE_bm | ADC_FREERUN_bm | ADC_RUNSTBY_bm; + // set a INITDLY value because the AVR manual says so (section 30.3.5) + // (delay 1st reading until Vref is stable) + ADC0.CTRLD |= ADC_INITDLY_DLY16_gc; + set_admux_voltage(); + #else + #error Unrecognized MCU type + #endif +} + +inline void ADC_off() { + #ifdef AVRXMEGA3 // ATTINY816, 817, etc + ADC0.CTRLA &= ~(ADC_ENABLE_bm); // disable the ADC + #else + ADCSRA &= ~(1<<ADEN); //ADC off + #endif +} + +#ifdef USE_VOLTAGE_DIVIDER +static inline uint8_t calc_voltage_divider(uint16_t value) { + // use 9.7 fixed-point to get sufficient precision + uint16_t adc_per_volt = ((ADC_44<<5) - (ADC_22<<5)) / (44-22); + // shift incoming value into a matching position + uint8_t result = ((value / adc_per_volt) + + VOLTAGE_FUDGE_FACTOR + #ifdef USE_VOLTAGE_CORRECTION + + VOLT_CORR - 7 + #endif + ) >> 1; + return result; +} +#endif + +// Each full cycle runs ~2X per second with just voltage enabled, +// or ~1X per second with voltage and temperature. +#if defined(USE_LVP) && defined(USE_THERMAL_REGULATION) +#define ADC_CYCLES_PER_SECOND 1 +#else +#define ADC_CYCLES_PER_SECOND 2 +#endif + +#ifdef AVRXMEGA3 // ATTINY816, 817, etc +#define ADC_vect ADC0_RESRDY_vect +#endif +// happens every time the ADC sampler finishes a measurement +ISR(ADC_vect) { + + #ifdef AVRXMEGA3 // ATTINY816, 817, etc + ADC0.INTFLAGS = ADC_RESRDY_bm; // clear the interrupt + #endif + + if (adc_sample_count) { + + uint16_t m; // latest measurement + uint16_t s; // smoothed measurement + uint8_t channel = adc_channel; + + // update the latest value + #ifdef AVRXMEGA3 // ATTINY816, 817, etc + // Use the factory calibrated values in SIGROW.TEMPSENSE0 and SIGROW.TEMPSENSE1 + // to calculate a temperature reading in Kelvin, then left-align it. + if (channel == 1) { // thermal, convert ADC reading to left-aligned Kelvin + int8_t sigrow_offset = SIGROW.TEMPSENSE1; // Read signed value from signature row + uint8_t sigrow_gain = SIGROW.TEMPSENSE0; // Read unsigned value from signature row + uint32_t temp = ADC0.RES - sigrow_offset; + temp *= sigrow_gain; // Result might overflow 16 bit variable (10bit+8bit) + temp += 0x80; // Add 1/2 to get correct rounding on division below + temp >>= 8; // Divide result to get Kelvin + m = (temp << 6); // left align it + } + else { m = (ADC0.RES << 6); } // voltage, force left-alignment + + #else + m = ADC; + #endif + adc_raw[channel] = m; + + // lowpass the value + //s = adc_smooth[channel]; // easier to read + uint16_t *v = adc_smooth + channel; // compiles smaller + s = *v; + if (m > s) { s++; } + if (m < s) { s--; } + //adc_smooth[channel] = s; + *v = s; + + // track what woke us up, and enable deferred logic + irq_adc = 1; + + } + + // the next measurement isn't the first + adc_sample_count = 1; + // rollover doesn't really matter + //adc_sample_count ++; + +} + +void adc_deferred() { + irq_adc = 0; // event handled + + #ifdef USE_PSEUDO_RAND + // real-world entropy makes this a true random, not pseudo + // Why here instead of the ISR? Because it makes the time-critical ISR + // code a few cycles faster and we don't need crypto-grade randomness. + #ifdef AVRXMEGA3 // ATTINY816, 817, etc + pseudo_rand_seed += ADC0.RESL; // right aligned, not left... so should be equivalent? + #else + pseudo_rand_seed += (ADCL >> 6) + (ADCH << 2); + #endif + #endif + + // the ADC triggers repeatedly when it's on, but we only need to run the + // voltage and temperature regulation stuff once in a while...so disable + // this after each activation, until it's manually enabled again + if (! adc_deferred_enable) return; + + // disable after one iteration + adc_deferred_enable = 0; + + // what is being measured? 0 = battery voltage, 1 = temperature + uint8_t adc_step; + + #if defined(USE_LVP) && defined(USE_THERMAL_REGULATION) + // do whichever one is currently active + adc_step = adc_channel; + #else + // unless there's no temperature sensor... then just do voltage + adc_step = 0; + #endif + + #if defined(TICK_DURING_STANDBY) && defined(USE_SLEEP_LVP) + // in sleep mode, turn off after just one measurement + // (having the ADC on raises standby power by about 250 uA) + // (and the usual standby level is only ~20 uA) + if (go_to_standby) { + ADC_off(); + // if any measurements were in progress, they're done now + adc_active_now = 0; + // also, only check the battery while asleep, not the temperature + adc_channel = 0; + } + #endif + + if (0) {} // placeholder for easier syntax + + #ifdef USE_LVP + else if (0 == adc_step) { // voltage + ADC_voltage_handler(); + #ifdef USE_THERMAL_REGULATION + // set the correct type of measurement for next time + if (! go_to_standby) set_admux_therm(); + #endif + } + #endif + + #ifdef USE_THERMAL_REGULATION + else if (1 == adc_step) { // temperature + ADC_temperature_handler(); + #ifdef USE_LVP + // set the correct type of measurement for next time + set_admux_voltage(); + #endif + } + #endif + + if (adc_reset) adc_reset --; +} + + +#ifdef USE_LVP +static inline void ADC_voltage_handler() { + // rate-limit low-voltage warnings to a max of 1 per N seconds + static uint8_t lvp_timer = 0; + #define LVP_TIMER_START (VOLTAGE_WARNING_SECONDS*ADC_CYCLES_PER_SECOND) // N seconds between LVP warnings + + #ifdef NO_LVP_WHILE_BUTTON_PRESSED + // don't run if button is currently being held + // (because the button causes a reading of zero volts) + if (button_last_state) return; + #endif + + uint16_t measurement; + + // latest ADC value + if (adc_reset) { // just after waking, don't lowpass + measurement = adc_raw[0]; + adc_smooth[0] = measurement; // no lowpass, just use the latest value + } + #ifdef USE_LOWPASS_WHILE_ASLEEP + else if (go_to_standby) { // weaker lowpass while asleep + // occasionally the aux LED color can oscillate during standby, + // while using "voltage" mode ... so try to reduce the oscillation + uint16_t r = adc_raw[0]; + uint16_t s = adc_smooth[0]; + #if 0 + // fixed-rate lowpass, stable but very slow + // (move by only 0.5 ADC units per measurement, 1 ADC unit = 64) + if (r < s) { s -= 32; } + if (r > s) { s += 32; } + #elif 1 + // 1/8th proportional lowpass, faster but less stable + int16_t diff = (r/8) - (s/8); + s += diff; + #else + // 50% proportional lowpass, fastest but least stable + s = (r>>1) + (s>>1); + #endif + adc_smooth[0] = s; + measurement = s; + } + #endif + else measurement = adc_smooth[0]; + + // values stair-step between intervals of 64, with random variations + // of 1 or 2 in either direction, so if we chop off the last 6 bits + // it'll flap between N and N-1... but if we add half an interval, + // the values should be really stable after right-alignment + // (instead of 99.98, 100.00, and 100.02, it'll hit values like + // 100.48, 100.50, and 100.52... which are stable when truncated) + //measurement += 32; + //measurement = (measurement + 16) >> 5; + measurement = (measurement + 16) & 0xffe0; // 1111 1111 1110 0000 + + #ifdef USE_VOLTAGE_DIVIDER + voltage = calc_voltage_divider(measurement); + #else + // calculate actual voltage: volts * 10 + // ADC = 1.1 * 1024 / volts + // volts = 1.1 * 1024 / ADC + voltage = ((uint16_t)(2*1.1*1024*10)/(measurement>>6) + + VOLTAGE_FUDGE_FACTOR + #ifdef USE_VOLTAGE_CORRECTION + + VOLT_CORR - 7 + #endif + ) >> 1; + #endif + + // if low, callback EV_voltage_low / EV_voltage_critical + // (but only if it has been more than N seconds since last call) + if (lvp_timer) { + lvp_timer --; + } else { // it has been long enough since the last warning + #ifdef DUAL_VOLTAGE_FLOOR + if (((voltage < VOLTAGE_LOW) && (voltage > DUAL_VOLTAGE_FLOOR)) || (voltage < DUAL_VOLTAGE_LOW_LOW)) { + #else + if (voltage < VOLTAGE_LOW) { + #endif + // send out a warning + emit(EV_voltage_low, 0); + // reset rate-limit counter + lvp_timer = LVP_TIMER_START; + } + } +} +#endif + + +#ifdef USE_THERMAL_REGULATION +// generally happens once per second while awake +static inline void ADC_temperature_handler() { + // coarse adjustment + #ifndef THERM_LOOKAHEAD + #define THERM_LOOKAHEAD 4 + #endif + // reduce frequency of minor warnings + #ifndef THERM_NEXT_WARNING_THRESHOLD + #define THERM_NEXT_WARNING_THRESHOLD 24 + #endif + // fine-grained adjustment + // how proportional should the adjustments be? + #ifndef THERM_RESPONSE_MAGNITUDE + #define THERM_RESPONSE_MAGNITUDE 64 + #endif + // acceptable temperature window size in C + #define THERM_WINDOW_SIZE 2 + + // TODO? make this configurable per build target? + // (shorter time for hosts with a lower power-to-mass ratio) + // (because then it'll have smaller responses) + #define NUM_TEMP_HISTORY_STEPS 8 // don't change; it'll break stuff + static uint8_t history_step = 0; + static uint16_t temperature_history[NUM_TEMP_HISTORY_STEPS]; + static int8_t warning_threshold = 0; + + if (adc_reset) { // wipe out old data + // ignore average, use latest sample + uint16_t foo = adc_raw[1]; + adc_smooth[1] = foo; + + // forget any past measurements + for(uint8_t i=0; i<NUM_TEMP_HISTORY_STEPS; i++) + temperature_history[i] = (foo + 16) >> 5; + } + + // latest 16-bit ADC reading + uint16_t measurement = adc_smooth[1]; + + // values stair-step between intervals of 64, with random variations + // of 1 or 2 in either direction, so if we chop off the last 6 bits + // it'll flap between N and N-1... but if we add half an interval, + // the values should be really stable after right-alignment + // (instead of 99.98, 100.00, and 100.02, it'll hit values like + // 100.48, 100.50, and 100.52... which are stable when truncated) + //measurement += 32; + measurement = (measurement + 16) >> 5; + //measurement = (measurement + 16) & 0xffe0; // 1111 1111 1110 0000 + + // let the UI see the current temperature in C + // Convert ADC units to Celsius (ish) + #ifndef USE_EXTERNAL_TEMP_SENSOR + // onboard sensor for attiny25/45/85/1634 + temperature = (measurement>>1) + THERM_CAL_OFFSET + (int16_t)TH_CAL - 275; + #else + // external sensor + temperature = EXTERN_TEMP_FORMULA(measurement>>1) + THERM_CAL_OFFSET + (int16_t)TH_CAL; + #endif + + // how much has the temperature changed between now and a few seconds ago? + int16_t diff; + diff = measurement - temperature_history[history_step]; + + // update / rotate the temperature history + temperature_history[history_step] = measurement; + history_step = (history_step + 1) & (NUM_TEMP_HISTORY_STEPS-1); + + // PI[D]: guess what the temperature will be in a few seconds + uint16_t pt; // predicted temperature + pt = measurement + (diff * THERM_LOOKAHEAD); + + // convert temperature limit from C to raw 16-bit ADC units + // C = (ADC>>6) - 275 + THERM_CAL_OFFSET + TH_CAL; + // ... so ... + // (C + 275 - THERM_CAL_OFFSET - TH_CAL) << 6 = ADC; + uint16_t ceil = (TH_CEIL + 275 - TH_CAL - THERM_CAL_OFFSET) << 1; + int16_t offset = pt - ceil; + + // bias small errors toward zero, while leaving large errors mostly unaffected + // (a diff of 1 C is 2 ADC units, * 4 for therm lookahead, so it becomes 8) + // (but a diff of 1 C should only send a warning of magnitude 1) + // (this also makes it only respond to small errors at the time the error + // happened, not after the temperature has stabilized) + for(uint8_t foo=0; foo<3; foo++) { + if (offset > 0) { + offset --; + } else if (offset < 0) { + offset ++; + } + } + + // Too hot? + // (if it's too hot and not getting cooler...) + if ((offset > 0) && (diff > -1)) { + // accumulated error isn't big enough yet to send a warning + if (warning_threshold > 0) { + warning_threshold -= offset; + } else { // error is big enough; send a warning + // how far above the ceiling? + // original method works, but is too slow on some small hosts: + // (and typically has a minimum response magnitude of 2 instead of 1) + // int16_t howmuch = offset; + // ... so increase the amount, except for small values + // (for example, 1:1, 2:1, 3:3, 4:5, 6:9, 8:13, 10:17, 40:77) + // ... and let us tune the response per build target if desired + int16_t howmuch = (offset + offset - 3) * THERM_RESPONSE_MAGNITUDE / 128; + if (howmuch < 1) howmuch = 1; + warning_threshold = THERM_NEXT_WARNING_THRESHOLD - (uint8_t)howmuch; + + // send a warning + emit(EV_temperature_high, howmuch); + } + } + + // Too cold? + // (if it's too cold and still getting colder...) + // the temperature is this far below the floor: + #define BELOW (offset + (THERM_WINDOW_SIZE<<1)) + else if ((BELOW < 0) && (diff < 0)) { + // accumulated error isn't big enough yet to send a warning + if (warning_threshold < 0) { + warning_threshold -= BELOW; + } else { // error is big enough; send a warning + warning_threshold = (-THERM_NEXT_WARNING_THRESHOLD) - BELOW; + + // how far below the floor? + // int16_t howmuch = ((-BELOW) >> 1) * THERM_RESPONSE_MAGNITUDE / 128; + int16_t howmuch = (-BELOW) >> 1; + // send a notification (unless voltage is low) + // (LVP and underheat warnings fight each other) + if (voltage > (VOLTAGE_LOW + 1)) + emit(EV_temperature_low, howmuch); + } + } + #undef BELOW + + // Goldilocks? + // (temperature is within target window, or at least heading toward it) + else { + // send a notification (unless voltage is low) + // (LVP and temp-okay events fight each other) + if (voltage > VOLTAGE_LOW) + emit(EV_temperature_okay, 0); + } +} +#endif + + +#ifdef USE_BATTCHECK +#ifdef BATTCHECK_4bars +PROGMEM const uint8_t voltage_blinks[] = { + 30, 35, 38, 40, 42, 99, +}; +#endif +#ifdef BATTCHECK_6bars +PROGMEM const uint8_t voltage_blinks[] = { + 30, 34, 36, 38, 40, 41, 43, 99, +}; +#endif +#ifdef BATTCHECK_8bars +PROGMEM const uint8_t voltage_blinks[] = { + 30, 33, 35, 37, 38, 39, 40, 41, 42, 99, +}; +#endif +void battcheck() { + #ifdef BATTCHECK_VpT + blink_num(voltage); + #else + uint8_t i; + for(i=0; + voltage >= pgm_read_byte(voltage_blinks + i); + i++) {} + #ifdef DONT_DELAY_AFTER_BATTCHECK + blink_digit(i); + #else + if (blink_digit(i)) + nice_delay_ms(1000); + #endif + #endif +} +#endif + diff --git a/fsm/adc.h b/fsm/adc.h new file mode 100644 index 0000000..1bb67ed --- /dev/null +++ b/fsm/adc.h @@ -0,0 +1,112 @@ +// fsm-adc.h: ADC (voltage, temperature) functions for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#if defined(USE_LVP) || defined(USE_THERMAL_REGULATION) +// use raw value instead of lowpassed value for the next N measurements +// (2 = 1 for voltage + 1 for temperature) +volatile uint8_t adc_reset = 2; +#endif + +#ifdef USE_LVP +// default 5 seconds between low-voltage warning events +#ifndef VOLTAGE_WARNING_SECONDS +#define VOLTAGE_WARNING_SECONDS 5 +#endif +// low-battery threshold in volts * 10 +#ifndef VOLTAGE_LOW +#define VOLTAGE_LOW 29 +#endif +// battery is low but not critical +#ifndef VOLTAGE_RED +#define VOLTAGE_RED 33 +#endif +// MCU sees voltage 0.X volts lower than actual, add X/2 to readings +#ifndef VOLTAGE_FUDGE_FACTOR +#ifdef USE_VOLTAGE_DIVIDER +#define VOLTAGE_FUDGE_FACTOR 0 +#else +#define VOLTAGE_FUDGE_FACTOR 5 +#endif +#endif + +#ifdef TICK_DURING_STANDBY +volatile uint8_t adc_active_now = 0; // sleep LVP needs a different sleep mode +#endif +volatile uint8_t irq_adc = 0; // ADC interrupt happened? +uint8_t adc_sample_count = 0; // skip the first sample; it's junk +uint8_t adc_channel = 0; // 0=voltage, 1=temperature +uint16_t adc_raw[2]; // last ADC measurements (0=voltage, 1=temperature) +uint16_t adc_smooth[2]; // lowpassed ADC measurements (0=voltage, 1=temperature) +// ADC code is split into two parts: +// - ISR: runs immediately at each interrupt, does the bare minimum because time is critical here +// - deferred: the bulk of the logic runs later when time isn't so critical +uint8_t adc_deferred_enable = 0; // stop waiting and run the deferred code +void adc_deferred(); // do the actual ADC-related calculations + +static inline void ADC_voltage_handler(); +uint8_t voltage = 0; +#ifdef USE_VOLTAGE_CORRECTION + #ifdef USE_CFG + #define VOLT_CORR cfg.voltage_correction + #else + // same 0.05V units as fudge factor, + // but 7 is neutral, and the expected range is from 1 to 13 + uint8_t voltage_correction = 7; + #define VOLT_CORR voltage_correction + #endif +#endif +#ifdef USE_LVP +void low_voltage(); +#endif + +#ifdef USE_BATTCHECK +void battcheck(); +#ifdef BATTCHECK_VpT +#define USE_BLINK_NUM +#endif +#if defined(BATTCHECK_8bars) || defined(BATTCHECK_6bars) || defined(BATTCHECK_4bars) +#define USE_BLINK_DIGIT +#endif +#endif +#endif // ifdef USE_LVP + + +#ifdef USE_THERMAL_REGULATION +// try to keep temperature below 45 C +#ifndef DEFAULT_THERM_CEIL +#define DEFAULT_THERM_CEIL 45 +#endif +// don't allow user to set ceiling above 70 C +#ifndef MAX_THERM_CEIL +#define MAX_THERM_CEIL 70 +#endif +// Local per-MCU calibration value +#ifndef THERM_CAL_OFFSET +#define THERM_CAL_OFFSET 0 +#endif +// temperature now, in C (ish) +int16_t temperature; +#ifdef USE_CFG + #define TH_CEIL cfg.therm_ceil + #define TH_CAL cfg.therm_cal_offset +#else + #define TH_CEIL therm_ceil + #define TH_CAL therm_cal_offset + uint8_t therm_ceil = DEFAULT_THERM_CEIL; + int8_t therm_cal_offset = 0; +#endif +static inline void ADC_temperature_handler(); +#endif // ifdef USE_THERMAL_REGULATION + + +inline void ADC_on(); +inline void ADC_off(); +inline void ADC_start_measurement(); + +#ifdef TICK_DURING_STANDBY +inline void adc_sleep_mode(); +#endif + diff --git a/fsm/chan-aux.c b/fsm/chan-aux.c new file mode 100644 index 0000000..e04e6a2 --- /dev/null +++ b/fsm/chan-aux.c @@ -0,0 +1,11 @@ +// channel modes for single color aux LEDs +// Copyright (C) 2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +void set_level_aux(uint8_t level) { + indicator_led(!(!(level)) << 1); // high (or off) +} + +bool gradual_tick_null(uint8_t gt) { return true; } // do nothing + diff --git a/fsm/chan-aux.h b/fsm/chan-aux.h new file mode 100644 index 0000000..ff599b8 --- /dev/null +++ b/fsm/chan-aux.h @@ -0,0 +1,25 @@ +// channel modes for single color aux LEDs +// Copyright (C) 2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#define NUM_AUX_CHANNEL_MODES 1 + +// include / exclude field based on compile options +#ifdef USE_CHANNEL_MODE_ARGS + #define AUX_HAS_ARGS , .has_args = 0 +#else + #define AUX_HAS_ARGS +#endif + +#define AUX_CHANNELS \ + { \ + .set_level = set_level_aux, \ + .gradual_tick = gradual_tick_null \ + AUX_HAS_ARGS \ + } + +void set_level_aux(uint8_t level); + +bool gradual_tick_null(uint8_t gt); + diff --git a/fsm/chan-rgbaux.c b/fsm/chan-rgbaux.c new file mode 100644 index 0000000..19d18a6 --- /dev/null +++ b/fsm/chan-rgbaux.c @@ -0,0 +1,35 @@ +// channel modes for RGB aux LEDs +// Copyright (C) 2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +void set_level_auxred(uint8_t level) { + rgb_led_set(!(!(level)) * 0b000010); // red, high (or off) +} + +void set_level_auxyel(uint8_t level) { + rgb_led_set(!(!(level)) * 0b001010); // red+green, high (or off) +} + +void set_level_auxgrn(uint8_t level) { + rgb_led_set(!(!(level)) * 0b001000); // green, high (or off) +} + +void set_level_auxcyn(uint8_t level) { + rgb_led_set(!(!(level)) * 0b101000); // green+blue, high (or off) +} + +void set_level_auxblu(uint8_t level) { + rgb_led_set(!(!(level)) * 0b100000); // blue, high (or off) +} + +void set_level_auxprp(uint8_t level) { + rgb_led_set(!(!(level)) * 0b100010); // red+blue, high (or off) +} + +void set_level_auxwht(uint8_t level) { + rgb_led_set(!(!(level)) * 0b101010); // red+green+blue, high (or off) +} + +bool gradual_tick_null(uint8_t gt) { return true; } // do nothing + diff --git a/fsm/chan-rgbaux.h b/fsm/chan-rgbaux.h new file mode 100644 index 0000000..6ef5d89 --- /dev/null +++ b/fsm/chan-rgbaux.h @@ -0,0 +1,72 @@ +// channel modes for RGB aux LEDs +// Copyright (C) 2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later +#pragma once + +#define RGB_AUX_ENUMS \ + CM_AUXRED, \ + CM_AUXYEL, \ + CM_AUXGRN, \ + CM_AUXCYN, \ + CM_AUXBLU, \ + CM_AUXPRP, \ + CM_AUXWHT + +#define RGB_AUX_CM_ARGS 0,0,0,0,0,0,0 + +#define NUM_RGB_AUX_CHANNEL_MODES 7 + +// include / exclude field based on compile options +#ifdef USE_CHANNEL_MODE_ARGS + #define AUX_RGB_HAS_ARGS , .has_args = 0 +#else + #define AUX_RGB_HAS_ARGS +#endif + +#define RGB_AUX_CHANNELS \ + { \ + .set_level = set_level_auxred, \ + .gradual_tick = gradual_tick_null \ + AUX_RGB_HAS_ARGS \ + }, \ + { \ + .set_level = set_level_auxyel, \ + .gradual_tick = gradual_tick_null \ + AUX_RGB_HAS_ARGS \ + }, \ + { \ + .set_level = set_level_auxgrn, \ + .gradual_tick = gradual_tick_null \ + AUX_RGB_HAS_ARGS \ + }, \ + { \ + .set_level = set_level_auxcyn, \ + .gradual_tick = gradual_tick_null \ + AUX_RGB_HAS_ARGS \ + }, \ + { \ + .set_level = set_level_auxblu, \ + .gradual_tick = gradual_tick_null \ + AUX_RGB_HAS_ARGS \ + }, \ + { \ + .set_level = set_level_auxprp, \ + .gradual_tick = gradual_tick_null \ + AUX_RGB_HAS_ARGS \ + }, \ + { \ + .set_level = set_level_auxwht, \ + .gradual_tick = gradual_tick_null \ + AUX_RGB_HAS_ARGS \ + } + +void set_level_auxred(uint8_t level); +void set_level_auxyel(uint8_t level); +void set_level_auxgrn(uint8_t level); +void set_level_auxcyn(uint8_t level); +void set_level_auxblu(uint8_t level); +void set_level_auxprp(uint8_t level); +void set_level_auxwht(uint8_t level); + +bool gradual_tick_null(uint8_t gt); + diff --git a/fsm/channels.c b/fsm/channels.c new file mode 100644 index 0000000..cc78536 --- /dev/null +++ b/fsm/channels.c @@ -0,0 +1,357 @@ +// fsm-channels.c: Channel mode functions for SpaghettiMonster. +// Copyright (C) 2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "fsm-ramping.h" + + +#if NUM_CHANNEL_MODES > 1 +void set_channel_mode(uint8_t mode) { + if (mode == channel_mode) return; // abort if nothing to do + + uint8_t cur_level = actual_level; + + // turn off old LEDs before changing channel + set_level(0); + + // change the channel + channel_mode = mode; + + // update the LEDs + set_level(cur_level); +} +#endif // if NUM_CHANNEL_MODES > 1 + + +#ifdef USE_CALC_2CH_BLEND +// calculate a "tint ramp" blend between 2 channels +// results are placed in *warm and *cool vars +// brightness : total amount of light units to distribute +// top : maximum allowed brightness per channel +// blend : ratio between warm and cool (0 = warm, 128 = 50%, 255 = cool) +void calc_2ch_blend( + PWM_DATATYPE *warm, + PWM_DATATYPE *cool, + PWM_DATATYPE brightness, + PWM_DATATYPE top, + uint8_t blend) { + + #ifndef TINT_RAMPING_CORRECTION + #define TINT_RAMPING_CORRECTION 26 // 140% brightness at middle tint + #endif + + // calculate actual PWM levels based on a single-channel ramp + // and a blend value + PWM_DATATYPE warm_PWM, cool_PWM; + PWM_DATATYPE2 base_PWM = brightness; + + #if defined(TINT_RAMPING_CORRECTION) && (TINT_RAMPING_CORRECTION > 0) + uint8_t level = actual_level - 1; + + // middle tints sag, so correct for that effect + // by adding extra power which peaks at the middle tint + // (correction is only necessary when PWM is fast) + if (level > HALFSPEED_LEVEL) { + base_PWM = brightness + + ((((PWM_DATATYPE2)brightness) * TINT_RAMPING_CORRECTION / 64) + * triangle_wave(blend) / 255); + } + // fade the triangle wave out when above 100% power, + // so it won't go over 200% + if (brightness > top) { + base_PWM -= 2 * ( + ((brightness - top) * TINT_RAMPING_CORRECTION / 64) + * triangle_wave(blend) / 255 + ); + } + // guarantee no more than 200% power + if (base_PWM > (top << 1)) { base_PWM = top << 1; } + #endif + + cool_PWM = (((PWM_DATATYPE2)blend * (PWM_DATATYPE2)base_PWM) + 127) / 255; + warm_PWM = base_PWM - cool_PWM; + // when running at > 100% power, spill extra over to other channel + if (cool_PWM > top) { + warm_PWM += (cool_PWM - top); + cool_PWM = top; + } else if (warm_PWM > top) { + cool_PWM += (warm_PWM - top); + warm_PWM = top; + } + + *warm = warm_PWM; + *cool = cool_PWM; +} +#endif // ifdef USE_CALC_2CH_BLEND + + +#ifdef USE_HSV2RGB +RGB_t hsv2rgb(uint8_t h, uint8_t s, uint16_t v) { + RGB_t color; + + if (s == 0) { // grey + color.r = color.g = color.b = v; + return color; + } + + uint8_t region; + uint16_t fpart; + uint16_t high, low, rising, falling; + + // hue has 6 segments, 0-5 + region = ((uint16_t)h * 6) >> 8; + // find remainder part, make it from 0-255 + fpart = ((uint16_t)h * 6) - (region << 8); + + // calculate graph segments, doing integer multiplication + // TODO: calculate 16-bit results, not 8-bit + high = v; + low = ((uint32_t)v * (255 - s)) >> 8; + // TODO: use a cosine crossfade instead of linear + // (because it looks better and feels more natural) + falling = ((uint32_t)v * (255 - ((s * fpart) >> 8))) >> 8; + rising = ((uint32_t)v * (255 - ((s * (255 - fpart)) >> 8))) >> 8; + + // default floor + color.r = low; + color.g = low; + color.b = low; + + // assign graph shapes based on color cone region + switch (region) { + case 0: + color.r = high; + color.g = rising; + //color.b = low; + break; + case 1: + color.r = falling; + color.g = high; + //color.b = low; + break; + case 2: + //color.r = low; + color.g = high; + color.b = rising; + break; + case 3: + //color.r = low; + color.g = falling; + color.b = high; + break; + case 4: + color.r = rising; + //color.g = low; + color.b = high; + break; + default: + color.r = high; + //color.g = low; + color.b = falling; + break; + } + + return color; +} +#endif // ifdef USE_HSV2RGB + + +///// Common set_level_*() functions shared by multiple lights ///// +// (unique lights should use their own, +// but these common versions cover most of the common hardware designs) + +// TODO: upgrade some older lights to dynamic PWM +// TODO: 1ch w/ dynamic PWM +// TODO: 1ch w/ dynamic PWM and opamp enable pins? +// TODO: 2ch stacked w/ dynamic PWM +// TODO: 2ch stacked w/ dynamic PWM and opamp enable pins? + + +#ifdef USE_SET_LEVEL_1CH +// single set of LEDs with 1 power channel +void set_level_1ch(uint8_t level) { + if (level == 0) { + LOW_PWM_LVL = 0; + } else { + level --; // PWM array index = level - 1 + LOW_PWM_LVL = PWM_GET(low_pwm_levels, level); + } +} +#endif + + +#ifdef USE_SET_LEVEL_2CH_STACKED +// single set of LEDs with 2 stacked power channels, DDFET+1 or DDFET+linear +void set_level_2ch_stacked(uint8_t level) { + if (level == 0) { + LOW_PWM_LVL = 0; + HIGH_PWM_LVL = 0; + } else { + level --; // PWM array index = level - 1 + LOW_PWM_LVL = PWM_GET(low_pwm_levels, level); + HIGH_PWM_LVL = PWM_GET(high_pwm_levels, level); + } +} +#endif + + +#ifdef USE_SET_LEVEL_3CH_STACKED +// single set of LEDs with 3 stacked power channels, like DDFET+N+1 +void set_level_3ch_stacked(uint8_t level) { + if (level == 0) { + LOW_PWM_LVL = 0; + MED_PWM_LVL = 0; + HIGH_PWM_LVL = 0; + } else { + level --; // PWM array index = level - 1 + LOW_PWM_LVL = PWM_GET(low_pwm_levels, level); + MED_PWM_LVL = PWM_GET(med_pwm_levels, level); + HIGH_PWM_LVL = PWM_GET(high_pwm_levels, level); + } +} +#endif + + +#if defined(USE_TINT_RAMPING) && (!defined(TINT_RAMP_TOGGLE_ONLY)) +void set_level_2ch_blend() { + #ifndef TINT_RAMPING_CORRECTION + #define TINT_RAMPING_CORRECTION 26 // 140% brightness at middle tint + #endif + + // calculate actual PWM levels based on a single-channel ramp + // and a global tint value + //PWM_DATATYPE brightness = PWM_GET(pwm1_levels, level); + uint16_t brightness = PWM1_LVL; + uint16_t warm_PWM, cool_PWM; + #ifdef USE_STACKED_DYN_PWM + uint16_t top = PWM1_TOP; + //uint16_t top = PWM_GET(pwm_tops, actual_level-1); + #else + const uint16_t top = PWM_TOP; + #endif + + // auto-tint modes + uint8_t mytint; + uint8_t level = actual_level - 1; + #if 1 + // perceptual by ramp level + if (tint == 0) { mytint = 255 * (uint16_t)level / RAMP_SIZE; } + else if (tint == 255) { mytint = 255 - (255 * (uint16_t)level / RAMP_SIZE); } + #else + // linear with power level + //if (tint == 0) { mytint = brightness; } + //else if (tint == 255) { mytint = 255 - brightness; } + #endif + // stretch 1-254 to fit 0-255 range (hits every value except 98 and 198) + else { mytint = (tint * 100 / 99) - 1; } + + PWM_DATATYPE2 base_PWM = brightness; + #if defined(TINT_RAMPING_CORRECTION) && (TINT_RAMPING_CORRECTION > 0) + // middle tints sag, so correct for that effect + // by adding extra power which peaks at the middle tint + // (correction is only necessary when PWM is fast) + if (level > HALFSPEED_LEVEL) { + base_PWM = brightness + + ((((PWM_DATATYPE2)brightness) * TINT_RAMPING_CORRECTION / 64) * triangle_wave(mytint) / 255); + } + // fade the triangle wave out when above 100% power, + // so it won't go over 200% + if (brightness > top) { + base_PWM -= 2 * ( + ((brightness - top) * TINT_RAMPING_CORRECTION / 64) + * triangle_wave(mytint) / 255 + ); + } + // guarantee no more than 200% power + if (base_PWM > (top << 1)) { base_PWM = top << 1; } + #endif + + cool_PWM = (((PWM_DATATYPE2)mytint * (PWM_DATATYPE2)base_PWM) + 127) / 255; + warm_PWM = base_PWM - cool_PWM; + // when running at > 100% power, spill extra over to other channel + if (cool_PWM > top) { + warm_PWM += (cool_PWM - top); + cool_PWM = top; + } else if (warm_PWM > top) { + cool_PWM += (warm_PWM - top); + warm_PWM = top; + } + + TINT1_LVL = warm_PWM; + TINT2_LVL = cool_PWM; + + // disable the power channel, if relevant + #ifdef LED_ENABLE_PIN + if (warm_PWM) + LED_ENABLE_PORT |= (1 << LED_ENABLE_PIN); + else + LED_ENABLE_PORT &= ~(1 << LED_ENABLE_PIN); + #endif + #ifdef LED2_ENABLE_PIN + if (cool_PWM) + LED2_ENABLE_PORT |= (1 << LED2_ENABLE_PIN); + else + LED2_ENABLE_PORT &= ~(1 << LED2_ENABLE_PIN); + #endif +} +#endif // ifdef USE_TINT_RAMPING + + +#ifdef USE_GRADUAL_TICK_1CH +void gradual_tick_1ch() { + GRADUAL_TICK_SETUP(); + + GRADUAL_ADJUST_1CH(low_pwm_levels, LOW_PWM_LVL); + + // did we go far enough to hit the next defined ramp level? + // if so, update the main ramp level tracking var + if ((LOW_PWM_LVL == PWM_GET(low_pwm_levels, gt))) + { + GRADUAL_IS_ACTUAL(); + } +} +#endif + + +#ifdef USE_GRADUAL_TICK_2CH_STACKED +void gradual_tick_2ch_stacked() { + GRADUAL_TICK_SETUP(); + + GRADUAL_ADJUST(low_pwm_levels, LOW_PWM_LVL, PWM_TOP); + GRADUAL_ADJUST_1CH(high_pwm_levels, HIGH_PWM_LVL); + + // did we go far enough to hit the next defined ramp level? + // if so, update the main ramp level tracking var + if ( (LOW_PWM_LVL == PWM_GET(low_pwm_levels, gt)) + && (HIGH_PWM_LVL == PWM_GET(high_pwm_levels, gt)) + ) + { + GRADUAL_IS_ACTUAL(); + } +} +#endif + + +#ifdef USE_GRADUAL_TICK_3CH_STACKED +void gradual_tick_3ch_stacked() { + GRADUAL_TICK_SETUP(); + + GRADUAL_ADJUST(low_pwm_levels, LOW_PWM_LVL, PWM_TOP); + GRADUAL_ADJUST(med_pwm_levels, MED_PWM_LVL, PWM_TOP); + GRADUAL_ADJUST_1CH(high_pwm_levels, HIGH_PWM_LVL); + + // did we go far enough to hit the next defined ramp level? + // if so, update the main ramp level tracking var + if ( (LOW_PWM_LVL == PWM_GET(low_pwm_levels, gt)) + && (MED_PWM_LVL == PWM_GET(med_pwm_levels, gt)) + && (HIGH_PWM_LVL == PWM_GET(high_pwm_levels, gt)) + ) + { + GRADUAL_IS_ACTUAL(); + } +} +#endif + + diff --git a/fsm/channels.h b/fsm/channels.h new file mode 100644 index 0000000..218f4f5 --- /dev/null +++ b/fsm/channels.h @@ -0,0 +1,141 @@ +// fsm-channels.h: Channel mode functions for SpaghettiMonster. +// Copyright (C) 2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +// always enable channel modes, even if there is only one +#define USE_CHANNEL_MODES + +// typedefs +typedef void SetLevelFunc(uint8_t level); +typedef SetLevelFunc * SetLevelFuncPtr; + +typedef bool GradualTickFunc(uint8_t gt); +typedef GradualTickFunc * GradualTickFuncPtr; + +// TODO: implement custom 3H handlers +typedef void ChannelArgFunc(); +typedef ChannelArgFunc * ChannelArgFuncPtr; + +typedef struct Channel { + SetLevelFuncPtr set_level; + #ifdef USE_SET_LEVEL_GRADUALLY + GradualTickFuncPtr gradual_tick; + #endif + #ifdef USE_CUSTOM_3H_HANDLERS + // TODO: implement custom 3H handlers + ChannelArgFuncPtr ramp_channel_arg; + #endif + #ifdef USE_CHANNEL_MODE_ARGS + bool has_args; + //uint8_t arg; // is in the config struct, not here + #endif +} Channel; + +Channel channels[]; // values are defined in the hwdef-*.c + +// TODO: size-optimize the case with only 1 channel mode? +// (the arrays and stuff shouldn't be needed) + +#if NUM_CHANNEL_MODES > 1 + #define USE_CHANNEL_MODES + // current multi-channel mode + uint8_t channel_mode = DEFAULT_CHANNEL_MODE; +#else + #define channel_mode 0 +#endif + +#ifdef USE_CUSTOM_CHANNEL_3H_MODES +// different 3H behavior per channel? +// TODO: move to progmem +// TODO: move to Anduril, not FSM +StatePtr channel_3H_modes[NUM_CHANNEL_MODES]; +#endif + +//#ifdef USE_CHANNEL_MODE_TOGGLES +#if NUM_CHANNEL_MODES > 1 +// user can take unwanted modes out of the rotation +// bitmask +#ifdef USE_CFG + #define channel_mode_enabled(n) ((cfg.channel_modes_enabled >> n) & 1) + #define channel_mode_enable(n) cfg.channel_modes_enabled |= (1 << n) + #define channel_mode_disable(n) cfg.channel_modes_enabled &= ((1 << n) ^ 0xff) +#else + uint16_t channel_modes_enabled = CHANNEL_MODES_ENABLED; + #define channel_mode_enabled(n) ((channel_modes_enabled >> n) & 1) + #define channel_mode_enable(n) channel_modes_enabled |= (1 << n) + #define channel_mode_disable(n) channel_modes_enabled &= ((1 << n) ^ 0xff) + #endif +#endif + +#ifdef USE_CHANNEL_MODE_ARGS + #ifndef USE_CFG + // one byte of extra data per channel mode, like for tint value + uint8_t channel_mode_args[NUM_CHANNEL_MODES] = { CHANNEL_MODE_ARGS }; + #endif + // which modes respond to their "arg", and which don't? + //const uint8_t channel_has_args = CHANNEL_HAS_ARGS; + //#define channel_has_args(n) ((CHANNEL_HAS_ARGS >> n) & 1) + // struct member + #define channel_has_args(n) (channels[n].has_args) +#endif + +#if NUM_CHANNEL_MODES > 1 +void set_channel_mode(uint8_t mode); +#endif + +#ifdef USE_CALC_2CH_BLEND +void calc_2ch_blend( + PWM_DATATYPE *warm, + PWM_DATATYPE *cool, + PWM_DATATYPE brightness, + PWM_DATATYPE top, + uint8_t blend); +#endif + +#ifdef USE_HSV2RGB +typedef struct RGB_t { + uint16_t r; + uint16_t g; + uint16_t b; +} RGB_t; +RGB_t hsv2rgb(uint8_t h, uint8_t s, uint16_t v); +#endif // ifdef USE_HSV2RGB + + +#ifdef USE_SET_LEVEL_1CH +// TODO: remove this +void set_level_1ch(uint8_t level); +#endif + +#ifdef USE_SET_LEVEL_2CH_STACKED +// TODO: remove this +void set_level_2ch_stacked(uint8_t level); +#endif + +#ifdef USE_SET_LEVEL_3CH_STACKED +// TODO: remove this +void set_level_3ch_stacked(uint8_t level); +#endif + +#if defined(USE_TINT_RAMPING) && (!defined(TINT_RAMP_TOGGLE_ONLY)) +// TODO: remove this +void set_level_2ch_blend(); +#endif + +#ifdef USE_GRADUAL_TICK_1CH +// TODO: remove this +void gradual_tick_1ch(); +#endif + +#ifdef USE_GRADUAL_TICK_2CH_STACKED +// TODO: remove this +void gradual_tick_2ch_stacked(); +#endif + +#ifdef USE_GRADUAL_TICK_3CH_STACKED +// TODO: remove this +void gradual_tick_3ch_stacked(); +#endif + diff --git a/fsm/eeprom.c b/fsm/eeprom.c new file mode 100644 index 0000000..66cdd78 --- /dev/null +++ b/fsm/eeprom.c @@ -0,0 +1,112 @@ +// fsm-eeprom.c: EEPROM API for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "fsm-eeprom.h" + +#ifdef USE_EEPROM +#ifdef EEPROM_OVERRIDE +uint8_t *eeprom; +#else +uint8_t eeprom[EEPROM_BYTES]; +#endif + +uint8_t load_eeprom() { + #if defined(LED_ENABLE_PIN) || defined(LED2_ENABLE_PIN) + delay_4ms(2); // wait for power to stabilize + #endif + + cli(); + // check if eeprom has been initialized; abort if it hasn't + uint8_t marker = eeprom_read_byte((uint8_t *)EEP_START); + if (marker != EEP_MARKER) { sei(); return 0; } + + // load the actual data + for(uint8_t i=0; i<EEPROM_BYTES; i++) { + eeprom[i] = eeprom_read_byte((uint8_t *)(EEP_START+1+i)); + } + sei(); + return 1; +} + +void save_eeprom() { + #if defined(LED_ENABLE_PIN) || defined(LED2_ENABLE_PIN) + delay_4ms(2); // wait for power to stabilize + #endif + + cli(); + + // save the actual data + for(uint8_t i=0; i<EEPROM_BYTES; i++) { + eeprom_update_byte((uint8_t *)(EEP_START+1+i), eeprom[i]); + } + + // save the marker last, to indicate the transaction is complete + eeprom_update_byte((uint8_t *)EEP_START, EEP_MARKER); + sei(); +} +#endif + +#ifdef USE_EEPROM_WL +uint8_t eeprom_wl[EEPROM_WL_BYTES]; +uint8_t * eep_wl_prev_offset; + +uint8_t load_eeprom_wl() { + #if defined(LED_ENABLE_PIN) || defined(LED2_ENABLE_PIN) + delay_4ms(2); // wait for power to stabilize + #endif + + cli(); + // check if eeprom has been initialized; abort if it hasn't + uint8_t found = 0; + uint8_t * offset; + for(offset = 0; + offset < (uint8_t *)(EEP_WL_SIZE - EEPROM_WL_BYTES - 1); + offset += (EEPROM_WL_BYTES + 1)) { + if (eeprom_read_byte(offset) == EEP_MARKER) { + found = 1; + eep_wl_prev_offset = offset; + break; + } + } + + if (found) { + // load the actual data + for(uint8_t i=0; i<EEPROM_WL_BYTES; i++) { + eeprom_wl[i] = eeprom_read_byte(offset+1+i); + } + } + sei(); + return found; +} + +void save_eeprom_wl() { + #if defined(LED_ENABLE_PIN) || defined(LED2_ENABLE_PIN) + delay_4ms(2); // wait for power to stabilize + #endif + + cli(); + // erase old state + uint8_t * offset = eep_wl_prev_offset; + for (uint8_t i = 0; i < EEPROM_WL_BYTES+1; i ++) { + eeprom_update_byte(offset+i, 0xFF); + } + + // save new state + offset += EEPROM_WL_BYTES+1; + if (offset > (uint8_t *)(EEP_WL_SIZE-EEPROM_WL_BYTES-1)) offset = 0; + eep_wl_prev_offset = offset; + // marker byte + // FIXME: write the marker last, to signal completed transaction + eeprom_update_byte(offset, EEP_MARKER); + offset ++; + // user data + for(uint8_t i=0; i<EEPROM_WL_BYTES; i++, offset++) { + eeprom_update_byte(offset, eeprom_wl[i]); + } + sei(); +} +#endif + diff --git a/fsm/eeprom.h b/fsm/eeprom.h new file mode 100644 index 0000000..440d2b3 --- /dev/null +++ b/fsm/eeprom.h @@ -0,0 +1,52 @@ +// fsm-eeprom.h: EEPROM API for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <avr/eeprom.h> + +// set this higher to enable normal eeprom functions +#ifndef EEPROM_BYTES +#define EEPROM_BYTES 0 +#endif + +// set this higher to enable wear-levelled eeprom functions +#ifndef EEPROM_WL_BYTES +#define EEPROM_WL_BYTES 0 +#endif + +#ifdef USE_EEPROM +// this fails when EEPROM_BYTES is a sizeof() +//#if EEPROM_BYTES >= (EEPSIZE/2) +//#error Requested EEPROM_BYTES too big. +//#endif +#ifdef EEPROM_OVERRIDE +uint8_t *eeprom; +#else +uint8_t eeprom[EEPROM_BYTES]; +#endif +uint8_t load_eeprom(); // returns 1 for success, 0 for no data found +void save_eeprom(); +#define EEP_START (EEPSIZE/2) +#endif + +#ifdef USE_EEPROM_WL +#if EEPROM_WL_BYTES >= (EEPSIZE/4) +#error Requested EEPROM_WL_BYTES too big. +#endif +uint8_t eeprom_wl[EEPROM_WL_BYTES]; +uint8_t load_eeprom_wl(); // returns 1 for success, 0 for no data found +void save_eeprom_wl(); +#define EEP_WL_SIZE (EEPSIZE/2) +#endif + +#if EEPSIZE > 256 +#define EEP_OFFSET_T uint16_t +#else +#define EEP_OFFSET_T uint8_t +#endif + +// if this marker isn't found, the eeprom is assumed to be blank +#define EEP_MARKER 0b10100101 + diff --git a/fsm/events.c b/fsm/events.c new file mode 100644 index 0000000..6987ae2 --- /dev/null +++ b/fsm/events.c @@ -0,0 +1,198 @@ +// fsm-events.c: Event-handling functions for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <util/delay_basic.h> + + +void append_emission(Event event, uint16_t arg) { + uint8_t i; + // find last entry + for(i=0; + (i<EMISSION_QUEUE_LEN) && (emissions[i].event != EV_none); + i++) { } + // add new entry + if (i < EMISSION_QUEUE_LEN) { + emissions[i].event = event; + emissions[i].arg = arg; + } else { + // TODO: if queue full, what should we do? + } +} + +void delete_first_emission() { + uint8_t i; + for(i=0; i<EMISSION_QUEUE_LEN-1; i++) { + emissions[i].event = emissions[i+1].event; + emissions[i].arg = emissions[i+1].arg; + } + emissions[i].event = EV_none; + emissions[i].arg = 0; +} + +void process_emissions() { + while (emissions[0].event != EV_none) { + emit_now(emissions[0].event, emissions[0].arg); + delete_first_emission(); + } +} + +// Call stacked callbacks for the given event until one handles it. +uint8_t emit_now(Event event, uint16_t arg) { + for(int8_t i=state_stack_len-1; i>=0; i--) { + uint8_t err = state_stack[i](event, arg); + if (! err) return 0; + } + return 1; // event not handled +} + +void emit(Event event, uint16_t arg) { + // add this event to the queue for later, + // so we won't use too much time during an interrupt + append_emission(event, arg); +} + +void emit_current_event(uint16_t arg) { + emit(current_event, arg); +} + +void empty_event_sequence() { + current_event = EV_none; + ticks_since_last_event = 0; + // when the user completes an input sequence, interrupt any running timers + // to cancel any delays currently in progress + // This eliminates a whole bunch of extra code: + // before: if (! nice_delay_ms(ms)) {break;} + // after: nice_delay_ms(ms); + interrupt_nice_delays(); +} + +uint8_t push_event(uint8_t ev_type) { // only for use by PCINT_inner() + // don't do this here; do it in PCINT_inner() instead + //ticks_since_last_event = 0; // something happened + + // only click events are sent to this function + current_event |= B_CLICK; + + // handle button presses + if (ev_type == B_PRESS) { + // set press flag + current_event |= B_PRESS; + // increase click counter + if ((current_event & B_COUNT) < (B_COUNT)) { + current_event ++; + } + return 1; // event pushed, even if max clicks already reached + // (will just repeat the max over and over) + } + // handle button releases + else if (ev_type == B_RELEASE) { + // clear the press flag + current_event &= (~B_PRESS); + // if a "hold" event just ended, set the timeout flag + // to indicate that the event is done and can be cleared + if (current_event & B_HOLD) { current_event |= B_TIMEOUT; } + return 1; // event pushed + } + + return 0; // unexpected event type +} + + +// explicitly interrupt these "nice" delays +volatile uint8_t nice_delay_interrupt = 0; +inline void interrupt_nice_delays() { nice_delay_interrupt = 1; } + +// like delay_ms, except it aborts on state change +// return value: +// 0: state changed +// 1: normal completion +uint8_t nice_delay_ms(uint16_t ms) { + /* // delay_zero() implementation + if (ms == 0) { + CLKPR = 1<<CLKPCE; CLKPR = 0; // full speed + _delay_loop_2(BOGOMIPS*95/100/3); + return 1; + } + */ + while(ms-- > 0) { + if (nice_delay_interrupt) { + return 0; + } + + #ifdef USE_DYNAMIC_UNDERCLOCKING + #ifdef USE_RAMPING + uint8_t level = actual_level; // volatile, avoid repeat access + if (level < QUARTERSPEED_LEVEL) { + clock_prescale_set(clock_div_4); + _delay_loop_2(BOGOMIPS*DELAY_FACTOR/100/4); + } + //else if (level < HALFSPEED_LEVEL) { + // clock_prescale_set(clock_div_2); + // _delay_loop_2(BOGOMIPS*95/100/2); + //} + else { + clock_prescale_set(clock_div_1); + _delay_loop_2(BOGOMIPS*DELAY_FACTOR/100); + } + // restore regular clock speed + clock_prescale_set(clock_div_1); + #else + // underclock MCU to save power + clock_prescale_set(clock_div_4); + // wait + _delay_loop_2(BOGOMIPS*DELAY_FACTOR/100/4); + // restore regular clock speed + clock_prescale_set(clock_div_1); + #endif // ifdef USE_RAMPING + #else + // wait + _delay_loop_2(BOGOMIPS*DELAY_FACTOR/100); + #endif // ifdef USE_DYNAMIC_UNDERCLOCKING + + // run pending system processes while we wait + handle_deferred_interrupts(); + + // handle events only afterward, so that any collapsed delays will + // finish running the UI's loop() code before taking any further actions + // (this helps make sure code runs in the correct order) + // (otherwise, a new state's EV_enter runs before the old state's + // loop() has finished, and things can get weird) + process_emissions(); + } + return 1; +} + +#ifdef USE_DYNAMIC_UNDERCLOCKING +void delay_4ms(uint8_t ms) { + while(ms-- > 0) { + // underclock MCU to save power + clock_prescale_set(clock_div_4); + // wait + _delay_loop_2(BOGOMIPS*98/100); + // restore regular clock speed + clock_prescale_set(clock_div_1); + } +} +#else +void delay_4ms(uint8_t ms) { + while(ms-- > 0) { + // wait + _delay_loop_2(BOGOMIPS*398/100); + } +} +#endif +/* +uint8_t nice_delay_4ms(uint8_t ms) { + return nice_delay_ms((uint16_t)ms << 2); +} +*/ + +/* +uint8_t nice_delay_s() { + return nice_delay_4ms(250); +} +*/ + diff --git a/fsm/events.h b/fsm/events.h new file mode 100644 index 0000000..575af1b --- /dev/null +++ b/fsm/events.h @@ -0,0 +1,221 @@ +// fsm-events.h: Event-handling functions for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <avr/pgmspace.h> + + +// timeout durations in ticks (each tick 1/62th s) +#ifndef HOLD_TIMEOUT +#define HOLD_TIMEOUT 24 +#endif +#ifndef RELEASE_TIMEOUT +#define RELEASE_TIMEOUT 18 +#endif + +// return codes for Event handlers +// Indicates whether this handler consumed (handled) the Event, or +// if the Event should be sent to the next handler in the stack. +#define EVENT_HANDLED 0 +#define EVENT_NOT_HANDLED 1 +#define MISCHIEF_MANAGED EVENT_HANDLED +#define MISCHIEF_NOT_MANAGED EVENT_NOT_HANDLED + +// typedefs +typedef uint8_t Event; +typedef struct Emission { + Event event; + uint16_t arg; +} Emission; + +Event current_event; +// at 0.016 ms per tick, 255 ticks = 4.08 s +static volatile uint16_t ticks_since_last_event = 0; + +// maximum number of events which can be waiting at one time +// (would probably be okay to reduce this to 4, but it's higher to be safe) +#define EMISSION_QUEUE_LEN 16 +// was "volatile" before, changed to regular var since IRQ rewrites seem +// to have removed the need for it to be volatile +// no comment about "volatile emissions" +Emission emissions[EMISSION_QUEUE_LEN]; + +void append_emission(Event event, uint16_t arg); +void delete_first_emission(); +void process_emissions(); +uint8_t emit_now(Event event, uint16_t arg); +void emit(Event event, uint16_t arg); +void emit_current_event(uint16_t arg); +void empty_event_sequence(); +uint8_t push_event(uint8_t ev_type); // only for use by PCINT_inner() + + +// TODO: Maybe move these to their own file... +// ... this probably isn't the right place for delays. +#ifndef DELAY_FACTOR + // adjust the timing of delays, lower = shorter delays + // 90 = 90% delay, 10% for other things + #define DELAY_FACTOR 92 +#endif +inline void interrupt_nice_delays(); +uint8_t nice_delay_ms(uint16_t ms); +//uint8_t nice_delay_s(); +void delay_4ms(uint8_t ms); + + +/* Event structure + * Bit 7: 1 for a button input event, 0 for all others. + * If bit 7 is 1: + * Bits 0,1,2,3: Click counter. Up to 15 clicks. + * Bit 4: 1 for a "press" event, 0 for a "release" event. + * Bit 5: 1 for a "hold" event, 0 otherwise. + * Bit 6: 1 for a "timeout" event, 0 otherwise. + * If bit 7 is 0: + * Sort of ad-hoc, shown in #defines below. + */ + +// event masks / bits +#define B_SYSTEM 0b00000000 +#define B_CLICK 0b10000000 +#define B_TIMEOUT 0b01000000 +#define B_HOLD 0b00100000 +#define B_PRESS 0b00010000 +#define B_RELEASE 0b00000000 +#define B_COUNT 0b00001111 +#define B_FLAGS 0b11110000 + +// Event types +#define EV_none 0 + +// Events which aren't button presses +#define EV_debug (B_SYSTEM|0b01111111) +#define EV_enter_state (B_SYSTEM|0b00001000) +#define EV_leave_state (B_SYSTEM|0b00001001) +#define EV_reenter_state (B_SYSTEM|0b00001010) +#define EV_tick (B_SYSTEM|0b00000001) +#ifdef TICK_DURING_STANDBY +#define EV_sleep_tick (B_SYSTEM|0b00000011) +#endif +#ifdef USE_LVP +#define EV_voltage_low (B_SYSTEM|0b00000100) +#endif +#ifdef USE_THERMAL_REGULATION +#define EV_temperature_high (B_SYSTEM|0b00000101) +#define EV_temperature_low (B_SYSTEM|0b00000110) +#define EV_temperature_okay (B_SYSTEM|0b00000111) +#endif + +// Button press events + +// shouldn't normally happen, but UI might empty_event_sequence() while button +// is down so a release with no recorded prior hold could be possible +#define EV_release (B_CLICK|B_RELEASE|0) + +#define EV_click1_press (B_CLICK|B_PRESS|1) +#define EV_click1_release (B_CLICK|B_RELEASE|1) +#define EV_click1_complete (B_CLICK|B_TIMEOUT|1) +#define EV_1click EV_click1_complete +#define EV_click1_hold (B_CLICK|B_HOLD|B_PRESS|1) +#define EV_click1_hold_release (B_CLICK|B_HOLD|B_RELEASE|B_TIMEOUT|1) +#define EV_hold EV_click1_hold + +#define EV_click2_press (B_CLICK|B_PRESS|2) +#define EV_click2_release (B_CLICK|B_RELEASE|2) +#define EV_click2_complete (B_CLICK|B_TIMEOUT|2) +#define EV_2clicks EV_click2_complete +#define EV_click2_hold (B_CLICK|B_HOLD|B_PRESS|2) +#define EV_click2_hold_release (B_CLICK|B_HOLD|B_RELEASE|B_TIMEOUT|2) + +#define EV_click3_press (B_CLICK|B_PRESS|3) +#define EV_click3_release (B_CLICK|B_RELEASE|3) +#define EV_click3_complete (B_CLICK|B_TIMEOUT|3) +#define EV_3clicks EV_click3_complete +#define EV_click3_hold (B_CLICK|B_HOLD|B_PRESS|3) +#define EV_click3_hold_release (B_CLICK|B_HOLD|B_RELEASE|B_TIMEOUT|3) + +#define EV_click4_press (B_CLICK|B_PRESS|4) +#define EV_click4_release (B_CLICK|B_RELEASE|4) +#define EV_click4_complete (B_CLICK|B_TIMEOUT|4) +#define EV_4clicks EV_click4_complete +#define EV_click4_hold (B_CLICK|B_HOLD|B_PRESS|4) +#define EV_click4_hold_release (B_CLICK|B_HOLD|B_RELEASE|B_TIMEOUT|4) + +#define EV_click5_press (B_CLICK|B_PRESS|5) +#define EV_click5_release (B_CLICK|B_RELEASE|5) +#define EV_click5_complete (B_CLICK|B_TIMEOUT|5) +#define EV_5clicks EV_click5_complete +#define EV_click5_hold (B_CLICK|B_HOLD|B_PRESS|5) +#define EV_click5_hold_release (B_CLICK|B_HOLD|B_RELEASE|B_TIMEOUT|5) + +#define EV_click6_press (B_CLICK|B_PRESS|6) +#define EV_click6_release (B_CLICK|B_RELEASE|6) +#define EV_click6_complete (B_CLICK|B_TIMEOUT|6) +#define EV_6clicks EV_click6_complete +#define EV_click6_hold (B_CLICK|B_HOLD|B_PRESS|6) +#define EV_click6_hold_release (B_CLICK|B_HOLD|B_RELEASE|B_TIMEOUT|6) + +#define EV_click7_press (B_CLICK|B_PRESS|7) +#define EV_click7_release (B_CLICK|B_RELEASE|7) +#define EV_click7_complete (B_CLICK|B_TIMEOUT|7) +#define EV_7clicks EV_click7_complete +#define EV_click7_hold (B_CLICK|B_HOLD|B_PRESS|7) +#define EV_click7_hold_release (B_CLICK|B_HOLD|B_RELEASE|B_TIMEOUT|7) + +#define EV_click8_press (B_CLICK|B_PRESS|8) +#define EV_click8_release (B_CLICK|B_RELEASE|8) +#define EV_click8_complete (B_CLICK|B_TIMEOUT|8) +#define EV_8clicks EV_click8_complete +#define EV_click8_hold (B_CLICK|B_HOLD|B_PRESS|8) +#define EV_click8_hold_release (B_CLICK|B_HOLD|B_RELEASE|B_TIMEOUT|8) + +#define EV_click9_press (B_CLICK|B_PRESS|9) +#define EV_click9_release (B_CLICK|B_RELEASE|9) +#define EV_click9_complete (B_CLICK|B_TIMEOUT|9) +#define EV_9clicks EV_click9_complete +#define EV_click9_hold (B_CLICK|B_HOLD|B_PRESS|9) +#define EV_click9_hold_release (B_CLICK|B_HOLD|B_RELEASE|B_TIMEOUT|9) + +#define EV_click10_press (B_CLICK|B_PRESS|10) +#define EV_click10_release (B_CLICK|B_RELEASE|10) +#define EV_click10_complete (B_CLICK|B_TIMEOUT|10) +#define EV_10clicks EV_click10_complete +#define EV_click10_hold (B_CLICK|B_HOLD|B_PRESS|10) +#define EV_click10_hold_release (B_CLICK|B_HOLD|B_RELEASE|B_TIMEOUT|10) + +#define EV_click11_press (B_CLICK|B_PRESS|11) +#define EV_click11_release (B_CLICK|B_RELEASE|11) +#define EV_click11_complete (B_CLICK|B_TIMEOUT|11) +#define EV_11clicks EV_click11_complete +#define EV_click11_hold (B_CLICK|B_HOLD|B_PRESS|11) +#define EV_click11_hold_release (B_CLICK|B_HOLD|B_RELEASE|B_TIMEOUT|11) + +#define EV_click12_press (B_CLICK|B_PRESS|12) +#define EV_click12_release (B_CLICK|B_RELEASE|12) +#define EV_click12_complete (B_CLICK|B_TIMEOUT|12) +#define EV_12clicks EV_click12_complete +#define EV_click12_hold (B_CLICK|B_HOLD|B_PRESS|12) +#define EV_click12_hold_release (B_CLICK|B_HOLD|B_RELEASE|B_TIMEOUT|12) + +#define EV_click13_press (B_CLICK|B_PRESS|13) +#define EV_click13_release (B_CLICK|B_RELEASE|13) +#define EV_click13_complete (B_CLICK|B_TIMEOUT|13) +#define EV_13clicks EV_click13_complete +#define EV_click13_hold (B_CLICK|B_HOLD|B_PRESS|13) +#define EV_click13_hold_release (B_CLICK|B_HOLD|B_RELEASE|B_TIMEOUT|13) + +#define EV_click14_press (B_CLICK|B_PRESS|14) +#define EV_click14_release (B_CLICK|B_RELEASE|14) +#define EV_click14_complete (B_CLICK|B_TIMEOUT|14) +#define EV_14clicks EV_click14_complete +#define EV_click14_hold (B_CLICK|B_HOLD|B_PRESS|14) +#define EV_click14_hold_release (B_CLICK|B_HOLD|B_RELEASE|B_TIMEOUT|14) + +#define EV_click15_press (B_CLICK|B_PRESS|15) +#define EV_click15_release (B_CLICK|B_RELEASE|15) +#define EV_click15_complete (B_CLICK|B_TIMEOUT|15) +#define EV_15clicks EV_click15_complete +#define EV_click15_hold (B_CLICK|B_HOLD|B_PRESS|15) +#define EV_click15_hold_release (B_CLICK|B_HOLD|B_RELEASE|B_TIMEOUT|15) + diff --git a/fsm/main.c b/fsm/main.c new file mode 100644 index 0000000..066188c --- /dev/null +++ b/fsm/main.c @@ -0,0 +1,211 @@ +// fsm-main.c: main() function for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "fsm-main.h" + +#if PWM_CHANNELS == 4 +#ifdef AVRXMEGA3 // ATTINY816, 817, etc +#error 4-channel PWM not currently set up for the AVR 1-Series +#endif +// 4th PWM channel requires manually turning the pin on/off via interrupt :( +ISR(TIMER1_OVF_vect) { + //bitClear(PORTB, 3); + PORTB &= 0b11110111; + //PORTB |= 0b00001000; +} +ISR(TIMER1_COMPA_vect) { + //if (!bitRead(TIFR,TOV1)) bitSet(PORTB, 3); + if (! (TIFR & (1<<TOV1))) PORTB |= 0b00001000; + //if (! (TIFR & (1<<TOV1))) PORTB &= 0b11110111; +} +#endif + +// FIXME: hw_setup() shouldn't be here ... move it entirely to hwdef files +#if (ATTINY == 25) || (ATTINY == 45) || (ATTINY == 85) +static inline void hw_setup() { + #if !defined(USE_GENERIC_HWDEF_SETUP) + hwdef_setup(); + #else + // configure PWM channels + #if PWM_CHANNELS >= 1 + DDRB |= (1 << PWM1_PIN); + TCCR0B = 0x01; // pre-scaler for timer (1 => 1, 2 => 8, 3 => 64...) + TCCR0A = PHASE; + #if (PWM1_PIN == PB4) // Second PWM counter is ... weird + TCCR1 = _BV (CS10); + GTCCR = _BV (COM1B1) | _BV (PWM1B); + OCR1C = 255; // Set ceiling value to maximum + #endif + #endif + // tint ramping needs second channel enabled, + // despite PWM_CHANNELS being only 1 + #if (PWM_CHANNELS >= 2) || defined(USE_TINT_RAMPING) + DDRB |= (1 << PWM2_PIN); + #if (PWM2_PIN == PB4) // Second PWM counter is ... weird + TCCR1 = _BV (CS10); + GTCCR = _BV (COM1B1) | _BV (PWM1B); + OCR1C = 255; // Set ceiling value to maximum + #endif + #endif + #if PWM_CHANNELS >= 3 + DDRB |= (1 << PWM3_PIN); + #if (PWM3_PIN == PB4) // Second PWM counter is ... weird + TCCR1 = _BV (CS10); + GTCCR = _BV (COM1B1) | _BV (PWM1B); + OCR1C = 255; // Set ceiling value to maximum + #endif + #endif + #if PWM_CHANNELS >= 4 + // 4th PWM channel is ... not actually supported in hardware :( + DDRB |= (1 << PWM4_PIN); + //OCR1C = 255; // Set ceiling value to maximum + TCCR1 = 1<<CTC1 | 1<<PWM1A | 3<<COM1A0 | 2<<CS10; + GTCCR = (2<<COM1B0) | (1<<PWM1B); + // set up an interrupt to control PWM4 pin + TIMSK |= (1<<OCIE1A) | (1<<TOIE1); + #endif + + // configure e-switch + PORTB = (1 << SWITCH_PIN); // e-switch is the only input + PCMSK = (1 << SWITCH_PIN); // pin change interrupt uses this pin + #endif // ifdef USE_GENERIC_HWDEF_SETUP +} +#elif (ATTINY == 1634) || defined(AVRXMEGA3) // ATTINY816, 817, etc +static inline void hw_setup() { + // this gets tricky with so many pins... + // ... so punt it to the hwdef file + hwdef_setup(); +} +#else + #error Unrecognized MCU type +#endif + + +//#ifdef USE_REBOOT +static inline void prevent_reboot_loop() { + // prevent WDT from rebooting MCU again + #ifdef AVRXMEGA3 // ATTINY816, 817, etc + RSTCTRL.RSTFR &= ~(RSTCTRL_WDRF_bm); // reset status flag + #else + MCUSR &= ~(1<<WDRF); // reset status flag + #endif + wdt_disable(); +} +//#endif + + +int main() { + // Don't allow interrupts while booting + cli(); + + //#ifdef USE_REBOOT + // prevents cycling after a crash, + // whether intentional (like factory reset) or not (bugs) + prevent_reboot_loop(); + //#endif + + hw_setup(); + + #if 0 + #ifdef HALFSPEED + // run at half speed + // FIXME: not portable (also not needed) + CLKPR = 1<<CLKPCE; + CLKPR = 1; + #endif + #endif + + #ifdef USE_DEBUG_BLINK + //debug_blink(1); + #endif + + // all booted -- turn interrupts back on + PCINT_on(); + WDT_on(); + ADC_on(); + sei(); + + // in case any spurious button presses were detected at boot + #ifdef USE_DELAY_MS + delay_ms(1); + #else + delay_4ms(1); + #endif + + // fallback for handling a few things + #ifndef DONT_USE_DEFAULT_STATE + push_state(default_state, 0); + nice_delay_interrupt = 0; + #endif + + // call recipe's setup + setup(); + + // main loop + while (1) { + // if event queue not empty, empty it + process_emissions(); + + // if loop() tried to change state, process that now + StatePtr df = deferred_state; + if (df) { + set_state(df, deferred_state_arg); + deferred_state = NULL; + //deferred_state_arg = 0; // unnecessary + } + + // enter standby mode if requested + // (works better if deferred like this) + if (go_to_standby) { + #ifdef USE_RAMPING + set_level(0); + #else + #if PWM_CHANNELS >= 1 + PWM1_LVL = 0; + #endif + #if PWM_CHANNELS >= 2 + PWM2_LVL = 0; + #endif + #if PWM_CHANNELS >= 3 + PWM3_LVL = 0; + #endif + #if PWM_CHANNELS >= 4 + PWM4_LVL = 255; // inverted :( + #endif + #endif + standby_mode(); + } + + // catch up on interrupts + handle_deferred_interrupts(); + + // turn delays back on, if they were off + nice_delay_interrupt = 0; + + // give the recipe some time slices + loop(); + + } +} + + +void handle_deferred_interrupts() { + /* + if (irq_pcint) { // button pressed or released + // nothing to do here + // (PCINT only matters during standby) + } + */ + if (irq_adc) { // ADC done measuring + adc_deferred(); + // irq_adc = 0; // takes care of itself + } + if (irq_wdt) { // the clock ticked + WDT_inner(); + // irq_wdt = 0; // takes care of itself + } +} + diff --git a/fsm/main.h b/fsm/main.h new file mode 100644 index 0000000..2e2a111 --- /dev/null +++ b/fsm/main.h @@ -0,0 +1,10 @@ +// fsm-main.h: main() function for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +int main(); +// needs to run frequently to execute the logic for WDT and ADC and stuff +void handle_deferred_interrupts(); + diff --git a/fsm/misc.c b/fsm/misc.c new file mode 100644 index 0000000..bc10ea1 --- /dev/null +++ b/fsm/misc.c @@ -0,0 +1,312 @@ +// fsm-misc.c: Miscellaneous function for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#ifdef USE_DYNAMIC_UNDERCLOCKING +void auto_clock_speed() { + uint8_t level = actual_level; // volatile, avoid repeat access + if (level < QUARTERSPEED_LEVEL) { + // run at quarter speed + // note: this only works when executed as two consecutive instructions + // (don't try to combine them or put other stuff between) + clock_prescale_set(clock_div_4); + } + else if (level < HALFSPEED_LEVEL) { + // run at half speed + clock_prescale_set(clock_div_2); + } else { + // run at full speed + clock_prescale_set(clock_div_1); + } +} +#endif + +#if defined(USE_BLINK_NUM) || defined(USE_BLINK_DIGIT) +#define BLINK_SPEED 1000 +uint8_t blink_digit(uint8_t num) { + //StatePtr old_state = current_state; + + // "zero" digit gets a single short blink + uint8_t ontime = BLINK_SPEED * 2 / 12; + if (!num) { ontime = BLINK_ONCE_TIME; num ++; } + + #ifdef BLINK_CHANNEL + // channel is set per blink, to prevent issues + // if another mode interrupts us (like a config menu) + uint8_t old_channel = channel_mode; + #endif + + for (; num>0; num--) { + // TODO: allow setting a blink channel mode per build target + #ifdef BLINK_CHANNEL + set_channel_mode(BLINK_CHANNEL); + #endif + set_level(BLINK_BRIGHTNESS); + #ifdef BLINK_CHANNEL + channel_mode = old_channel; + #endif + nice_delay_ms(ontime); + + #ifdef BLINK_CHANNEL + set_channel_mode(BLINK_CHANNEL); + #endif + set_level(0); + #ifdef BLINK_CHANNEL + channel_mode = old_channel; + #endif + nice_delay_ms(BLINK_SPEED * 3 / 12); + } + + #ifdef BLINK_CHANNEL + set_channel_mode(old_channel); + #endif + + return nice_delay_ms(BLINK_SPEED * 8 / 12); +} +#endif + +#ifdef USE_BLINK_BIG_NUM +uint8_t blink_big_num(uint16_t num) { + uint16_t digits[] = { 10000, 1000, 100, 10, 1 }; + uint8_t started = 0; + for (uint8_t digit=0; digit<sizeof(digits)/sizeof(uint16_t); digit++) { + uint16_t scale = digits[digit]; + if (num >= scale) { + started = 1; + } + if (started) { + uint8_t digit = 0; + while (num >= scale) { + num -= scale; + digit ++; + } + if (! blink_digit(digit)) return 0; + } + } + + return nice_delay_ms(1000); +} +#endif +#ifdef USE_BLINK_NUM +uint8_t blink_num(uint8_t num) { + #if 1 + uint8_t hundreds = num / 100; + num = num % 100; + uint8_t tens = num / 10; + num = num % 10; + #else // can be smaller or larger, depending on whether divmod is used elsewhere + uint8_t hundreds = 0; + uint8_t tens = 0; + for(; num >= 100; hundreds ++, num -= 100); + for(; num >= 10; tens ++, num -= 10); + #endif + + #if 0 + // wait a moment in the dark before starting + set_level(0); + nice_delay_ms(200); + #endif + + if (hundreds) blink_digit(hundreds); + if (hundreds || tens) blink_digit(tens); + return blink_digit(num); +} +#endif + +#ifdef USE_INDICATOR_LED +void indicator_led(uint8_t lvl) { + switch (lvl) { + #ifdef AVRXMEGA3 // ATTINY816, 817, etc + + case 0: // indicator off + AUXLED_PORT.DIRSET = (1 << AUXLED_PIN); // set as output + AUXLED_PORT.OUTCLR = (1 << AUXLED_PIN); // set output low + #ifdef AUXLED2_PIN // second LED mirrors the first + AUXLED2_PORT.DIRSET = (1 << AUXLED2_PIN); // set as output + AUXLED2_PORT.OUTCLR = (1 << AUXLED2_PIN); // set output low + #endif + break; + case 1: // indicator low + AUXLED_PORT.DIRCLR = (1 << AUXLED_PIN); // set as input + // this resolves to PORTx.PINxCTRL = PORT_PULLUPEN_bm; + *((uint8_t *)&AUXLED_PORT + 0x10 + AUXLED_PIN) = PORT_PULLUPEN_bm; // enable internal pull-up + #ifdef AUXLED2_PIN // second LED mirrors the first + AUXLED2_PORT.DIRCLR = (1 << AUXLED2_PIN); // set as input + // this resolves to PORTx.PINxCTRL = PORT_PULLUPEN_bm; + *((uint8_t *)&AUXLED2_PORT + 0x10 + AUXLED2_PIN) = PORT_PULLUPEN_bm; // enable internal pull-up + #endif + break; + default: // indicator high + AUXLED_PORT.DIRSET = (1 << AUXLED_PIN); // set as output + AUXLED_PORT.OUTSET = (1 << AUXLED_PIN); // set as high + #ifdef AUXLED2_PIN // second LED mirrors the first + AUXLED2_PORT.DIRSET = (1 << AUXLED2_PIN); // set as output + AUXLED2_PORT.OUTSET = (1 << AUXLED2_PIN); // set as high + #endif + break; + + #else // MCU is old tiny style, not newer mega style + + case 0: // indicator off + DDRB &= 0xff ^ (1 << AUXLED_PIN); + PORTB &= 0xff ^ (1 << AUXLED_PIN); + #ifdef AUXLED2_PIN // second LED mirrors the first + DDRB &= 0xff ^ (1 << AUXLED2_PIN); + PORTB &= 0xff ^ (1 << AUXLED2_PIN); + #endif + break; + case 1: // indicator low + DDRB &= 0xff ^ (1 << AUXLED_PIN); + PORTB |= (1 << AUXLED_PIN); + #ifdef AUXLED2_PIN // second LED mirrors the first + DDRB &= 0xff ^ (1 << AUXLED2_PIN); + PORTB |= (1 << AUXLED2_PIN); + #endif + break; + default: // indicator high + DDRB |= (1 << AUXLED_PIN); + PORTB |= (1 << AUXLED_PIN); + #ifdef AUXLED2_PIN // second LED mirrors the first + DDRB |= (1 << AUXLED2_PIN); + PORTB |= (1 << AUXLED2_PIN); + #endif + break; + + #endif // MCU type + } +} + +/* +void indicator_led_auto() { + if (actual_level > MAX_1x7135) indicator_led(2); + else if (actual_level > 0) indicator_led(1); + else indicator_led(0); +} +*/ +#endif // USE_INDICATOR_LED + +#ifdef USE_BUTTON_LED +// TODO: Refactor this and RGB LED function to merge code and save space +void button_led_set(uint8_t lvl) { + switch (lvl) { + + #ifdef AVRXMEGA3 // ATTINY816, 817, etc + + case 0: // LED off + BUTTON_LED_PORT.DIRSET = (1 << BUTTON_LED_PIN); // set as output + BUTTON_LED_PORT.OUTCLR = (1 << BUTTON_LED_PIN); // set output low + break; + case 1: // LED low + BUTTON_LED_PORT.DIRCLR = (1 << BUTTON_LED_PIN); // set as input + // this resolves to PORTx.PINxCTRL = PORT_PULLUPEN_bm; + *((uint8_t *)&BUTTON_LED_PORT + 0x10 + BUTTON_LED_PIN) = PORT_PULLUPEN_bm; // enable internal pull-up + break; + default: // LED high + BUTTON_LED_PORT.DIRSET = (1 << BUTTON_LED_PIN); // set as output + BUTTON_LED_PORT.OUTSET = (1 << BUTTON_LED_PIN); // set as high + break; + + #else + + case 0: // LED off + BUTTON_LED_DDR &= 0xff ^ (1 << BUTTON_LED_PIN); + BUTTON_LED_PUE &= 0xff ^ (1 << BUTTON_LED_PIN); + BUTTON_LED_PORT &= 0xff ^ (1 << BUTTON_LED_PIN); + break; + case 1: // LED low + BUTTON_LED_DDR &= 0xff ^ (1 << BUTTON_LED_PIN); + BUTTON_LED_PUE |= (1 << BUTTON_LED_PIN); + BUTTON_LED_PORT |= (1 << BUTTON_LED_PIN); + break; + default: // LED high + BUTTON_LED_DDR |= (1 << BUTTON_LED_PIN); + BUTTON_LED_PUE |= (1 << BUTTON_LED_PIN); + BUTTON_LED_PORT |= (1 << BUTTON_LED_PIN); + break; + + #endif // MCU type + } +} +#endif + +#ifdef USE_AUX_RGB_LEDS +void rgb_led_set(uint8_t value) { + // value: 0b00BBGGRR + uint8_t pins[] = { AUXLED_R_PIN, AUXLED_G_PIN, AUXLED_B_PIN }; + for (uint8_t i=0; i<3; i++) { + uint8_t lvl = (value >> (i<<1)) & 0x03; + uint8_t pin = pins[i]; + switch (lvl) { + + #ifdef AVRXMEGA3 // ATTINY816, 817, etc + + case 0: // LED off + AUXLED_RGB_PORT.DIRSET = (1 << pin); // set as output + AUXLED_RGB_PORT.OUTCLR = (1 << pin); // set output low + break; + case 1: // LED low + AUXLED_RGB_PORT.DIRCLR = (1 << pin); // set as input + // this resolves to PORTx.PINxCTRL = PORT_PULLUPEN_bm; + *((uint8_t *)&AUXLED_RGB_PORT + 0x10 + pin) = PORT_PULLUPEN_bm; // enable internal pull-up + break; + default: // LED high + AUXLED_RGB_PORT.DIRSET = (1 << pin); // set as output + AUXLED_RGB_PORT.OUTSET = (1 << pin); // set as high + break; + + #else + + case 0: // LED off + AUXLED_RGB_DDR &= 0xff ^ (1 << pin); + AUXLED_RGB_PUE &= 0xff ^ (1 << pin); + AUXLED_RGB_PORT &= 0xff ^ (1 << pin); + break; + case 1: // LED low + AUXLED_RGB_DDR &= 0xff ^ (1 << pin); + AUXLED_RGB_PUE |= (1 << pin); + AUXLED_RGB_PORT |= (1 << pin); + break; + default: // LED high + AUXLED_RGB_DDR |= (1 << pin); + AUXLED_RGB_PUE |= (1 << pin); + AUXLED_RGB_PORT |= (1 << pin); + break; + + #endif // MCU type + } + } +} +#endif // ifdef USE_AUX_RGB_LEDS + +#ifdef USE_TRIANGLE_WAVE +uint8_t triangle_wave(uint8_t phase) { + uint8_t result = phase << 1; + if (phase > 127) result = 255 - result; + return result; +} +#endif + +#ifdef USE_REBOOT +void reboot() { + // put the WDT in hard reset mode, then trigger it + cli(); + #if (ATTINY == 25) || (ATTINY == 45) || (ATTINY == 85) + WDTCR = 0xD8 | WDTO_15MS; + #elif (ATTINY == 1634) + // allow protected configuration changes for next 4 clock cycles + CCP = 0xD8; // magic number + // reset (WDIF + WDE), no WDIE, fastest (16ms) timing (0000) + // (DS section 8.5.2 and table 8-4) + WDTCSR = 0b10001000; + #elif defined(AVRXMEGA3) // ATTINY816, 817, etc + CCP = CCP_IOREG_gc; // temporarily disable change protection + WDT.CTRLA = WDT_PERIOD_8CLK_gc; // Enable, timeout 8ms + #endif + sei(); + wdt_reset(); + while (1) {} +} +#endif + diff --git a/fsm/misc.h b/fsm/misc.h new file mode 100644 index 0000000..8de6b29 --- /dev/null +++ b/fsm/misc.h @@ -0,0 +1,68 @@ +// fsm-misc.h: Miscellaneous function for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#ifdef USE_DYNAMIC_UNDERCLOCKING +void auto_clock_speed(); +#endif + +// shortest time (in ms) the light should blink for to indicate a zero +#ifndef BLINK_ONCE_TIME + #define BLINK_ONCE_TIME 10 +#endif + +#if defined(USE_BLINK_NUM) || defined(USE_BLINK_DIGIT) + #ifndef BLINK_BRIGHTNESS + #define BLINK_BRIGHTNESS (MAX_LEVEL/6) + #endif + #if defined(USE_CFG) && defined(DEFAULT_BLINK_CHANNEL) + #define BLINK_CHANNEL cfg.blink_channel + #elif defined(DEFAULT_BLINK_CHANNEL) + #define BLINK_CHANNEL DEFAULT_BLINK_CHANNEL + #endif + uint8_t blink_digit(uint8_t num); +#endif + +#ifdef USE_BLINK_NUM +//#define USE_BLINK +uint8_t blink_num(uint8_t num); +#endif + +/* +#ifdef USE_BLINK +uint8_t blink(uint8_t num, uint8_t speed); +#endif +*/ + +#ifdef USE_INDICATOR_LED +// FIXME: Remove this, replace with button_led() +// lvl: 0=off, 1=low, 2=high +void indicator_led(uint8_t lvl); +#endif + +#ifdef USE_BUTTON_LED +// lvl: 0=off, 1=low, 2=high +void button_led_set(uint8_t lvl); +#endif + +// if any type of aux LEDs exist, define a shorthand flag for it +#if defined(USE_INDICATOR_LED) || defined(USE_AUX_RGB_LEDS) || defined(USE_BUTTON_LED) +#define HAS_AUX_LEDS +#endif + +#ifdef USE_AUX_RGB_LEDS +// value: 0b00BBGGRR +// each pair of bits: 0=off, 1=low, 2=high +void rgb_led_set(uint8_t value); +#endif + +#ifdef USE_TRIANGLE_WAVE +uint8_t triangle_wave(uint8_t phase); +#endif + +#ifdef USE_REBOOT +void reboot(); +#endif + diff --git a/fsm/pcint.c b/fsm/pcint.c new file mode 100644 index 0000000..131d0c3 --- /dev/null +++ b/fsm/pcint.c @@ -0,0 +1,96 @@ +// fsm-pcint.c: PCINT (Pin Change Interrupt) functions for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <avr/interrupt.h> +#include <util/delay_basic.h> + +uint8_t button_is_pressed() { + uint8_t value = ((SWITCH_PORT & (1<<SWITCH_PIN)) == 0); + button_last_state = value; + return value; +} + +inline void PCINT_on() { + #if (ATTINY == 25) || (ATTINY == 45) || (ATTINY == 85) + // enable pin change interrupt + GIMSK |= (1 << PCIE); + // only pay attention to the e-switch pin + #if 0 // this is redundant; was already done in main() + PCMSK = (1 << SWITCH_PCINT); + #endif + // set bits 1:0 to 0b01 (interrupt on rising *and* falling edge) (default) + // MCUCR &= 0b11111101; MCUCR |= 0b00000001; + #elif (ATTINY == 1634) + // enable pin change interrupt + #ifdef SWITCH2_PCIE + GIMSK |= ((1 << SWITCH_PCIE) | (1 << SWITCH2_PCIE)); + #else + GIMSK |= (1 << SWITCH_PCIE); + #endif + #elif defined(AVRXMEGA3) // ATTINY816, 817, etc) + SWITCH_ISC_REG |= PORT_ISC_BOTHEDGES_gc; + #else + #error Unrecognized MCU type + #endif +} + +inline void PCINT_off() { + #if (ATTINY == 25) || (ATTINY == 45) || (ATTINY == 85) + // disable all pin-change interrupts + GIMSK &= ~(1 << PCIE); + #elif (ATTINY == 1634) + // disable all pin-change interrupts + GIMSK &= ~(1 << SWITCH_PCIE); + #elif defined(AVRXMEGA3) // ATTINY816, 817, etc) + SWITCH_ISC_REG &= ~(PORT_ISC_gm); + #else + #error Unrecognized MCU type + #endif +} + +//void button_change_interrupt() { +#if (ATTINY == 25) || (ATTINY == 45) || (ATTINY == 85) || (ATTINY == 1634) + #ifdef PCINT_vect + ISR(PCINT_vect) { + #else + ISR(PCINT0_vect) { + #endif +#elif defined(AVRXMEGA3) // ATTINY816, 817, etc) + ISR(SWITCH_VECT) { + // Write a '1' to clear the interrupt flag + SWITCH_INTFLG |= (1 << SWITCH_PIN); +#else + #error Unrecognized MCU type +#endif + + irq_pcint = 1; // let deferred code know an interrupt happened + + //DEBUG_FLASH; + + // as it turns out, it's more reliable to detect pin changes from WDT + // because PCINT itself tends to double-tap when connected to a + // noisy / bouncy switch (so the content of this function has been + // moved to a separate function, called from WDT only) + // PCINT_inner(button_is_pressed()); +} + +// should only be called from PCINT and/or WDT +// (is a separate function to reduce code duplication) +void PCINT_inner(uint8_t pressed) { + button_last_state = pressed; + + // register the change, and send event to the current state callback + if (pressed) { // user pressed button + push_event(B_PRESS); + emit_current_event(0); + } else { // user released button + // how long was the button held? + push_event(B_RELEASE); + emit_current_event(ticks_since_last_event); + } + ticks_since_last_event = 0; +} + diff --git a/fsm/pcint.h b/fsm/pcint.h new file mode 100644 index 0000000..cd7ba02 --- /dev/null +++ b/fsm/pcint.h @@ -0,0 +1,15 @@ +// fsm-pcint.h: PCINT (Pin Change Interrupt) functions for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +volatile uint8_t irq_pcint = 0; // pin change interrupt happened? +//static volatile uint8_t button_was_pressed; +#define BP_SAMPLES 32 +volatile uint8_t button_last_state; +uint8_t button_is_pressed(); +inline void PCINT_on(); +inline void PCINT_off(); +void PCINT_inner(uint8_t pressed); + diff --git a/fsm/ramping.c b/fsm/ramping.c new file mode 100644 index 0000000..adc8acb --- /dev/null +++ b/fsm/ramping.c @@ -0,0 +1,259 @@ +// fsm-ramping.c: Ramping functions for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#ifdef USE_RAMPING + +#ifdef HAS_AUX_LEDS +inline void set_level_aux_leds(uint8_t level) { + #ifdef USE_INDICATOR_LED_WHILE_RAMPING + // use side-facing aux LEDs while main LEDs are on + if (! go_to_standby) { + #ifdef USE_INDICATOR_LED + indicator_led((level > 0) + (level > DEFAULT_LEVEL)); + #endif + #ifdef USE_BUTTON_LED + button_led_set((level > 0) + (level > DEFAULT_LEVEL)); + #endif + } + #else // turn off front-facing aux LEDs while main LEDs are on + #if defined(USE_INDICATOR_LED) || defined(USE_AUX_RGB_LEDS) + if (! go_to_standby) { + #ifdef USE_INDICATOR_LED + indicator_led(0); + #endif + #ifdef USE_AUX_RGB_LEDS + rgb_led_set(0); + #ifdef USE_BUTTON_LED + button_led_set((level > 0) + (level > DEFAULT_LEVEL)); + #endif + #endif + } + #endif + #endif +} +#endif // ifdef HAS_AUX_LEDS + +#ifdef USE_AUX_RGB_LEDS_WHILE_ON +// TODO: maybe move this stuff into FSM +#include "anduril/aux-leds.h" // for rgb_led_voltage_readout() +inline void set_level_aux_rgb_leds(uint8_t level) { + if (! go_to_standby) { + if (level > 0) { + rgb_led_voltage_readout(level > USE_AUX_RGB_LEDS_WHILE_ON); + } else { + rgb_led_set(0); + } + // some drivers can be wired with RGB or single color to button + // ... so support both even though only one is connected + #ifdef USE_BUTTON_LED + button_led_set((level > 0) + (level > DEFAULT_LEVEL)); + #endif + } +} +#endif // ifdef USE_AUX_RGB_LEDS_WHILE_ON + + +void set_level(uint8_t level) { + #ifdef USE_JUMP_START + // maybe "jump start" the engine, if it's prone to slow starts + // (pulse the output high for a moment to wake up the power regulator) + // (only do this when starting from off and going to a low level) + // TODO: allow different jump start behavior per channel mode + // FIXME: don't jump-start during factory reset + // (it seems to cause some eeprom issues on KR4 + // when doing a click with a loose tailcap) + if ((! actual_level) + && level + && (level < JUMP_START_LEVEL)) { + set_level(JUMP_START_LEVEL); + delay_4ms(JUMP_START_TIME/4); + } + #endif + + #ifdef HAS_AUX_LEDS + set_level_aux_leds(level); + #endif + + #ifdef USE_AUX_RGB_LEDS_WHILE_ON + set_level_aux_rgb_leds(level); + #endif + + if (0 == level) { + set_level_zero(); + } else { + // call the relevant hardware-specific set_level_*() + SetLevelFuncPtr set_level_func = channels[channel_mode].set_level; + set_level_func(level - 1); + } + + if (actual_level != level) prev_level = actual_level; + actual_level = level; + + #ifdef USE_SET_LEVEL_GRADUALLY + gradual_target = level; + #endif + + #ifdef USE_DYNAMIC_UNDERCLOCKING + auto_clock_speed(); + #endif +} + +#ifdef USE_LEGACY_SET_LEVEL +// (this is mostly just here for reference, temporarily) +// single set of LEDs with 1 to 3 stacked power channels, +// like linear, FET+1, and FET+N+1 +// (default set_level_*() function for most lights) +void set_level_legacy(uint8_t level) { + if (level == 0) { + #if PWM_CHANNELS >= 1 + PWM1_LVL = 0; + #endif + #if PWM_CHANNELS >= 2 + PWM2_LVL = 0; + #endif + #if PWM_CHANNELS >= 3 + PWM3_LVL = 0; + #endif + #if defined(PWM1_CNT) && defined(PWM1_PHASE_RESET_OFF) + PWM1_CNT = 0; + #endif + #if defined(PWM2_CNT) && defined(PWM2_PHASE_RESET_OFF) + PWM2_CNT = 0; + #endif + #if defined(PWM3_CNT) && defined(PWM3_PHASE_RESET_OFF) + PWM3_CNT = 0; + #endif + #ifdef LED_OFF_DELAY + // for drivers with a slow regulator chip (eg, boost converter), + // delay before turning off to prevent flashes + delay_4ms(LED_OFF_DELAY/4); + #endif + // disable the power channel, if relevant + #ifdef LED_ENABLE_PIN + LED_ENABLE_PORT &= ~(1 << LED_ENABLE_PIN); + #endif + #ifdef LED2_ENABLE_PIN + LED2_ENABLE_PORT &= ~(1 << LED2_ENABLE_PIN); + #endif + } else { + // enable the power channel, if relevant + #ifdef LED_ENABLE_PIN + #ifdef LED_ON_DELAY + uint8_t led_enable_port_save = LED_ENABLE_PORT; + #endif + + #ifndef LED_ENABLE_PIN_LEVEL_MIN + LED_ENABLE_PORT |= (1 << LED_ENABLE_PIN); + #else + // only enable during part of the ramp + if ((level >= LED_ENABLE_PIN_LEVEL_MIN) + && (level <= LED_ENABLE_PIN_LEVEL_MAX)) + LED_ENABLE_PORT |= (1 << LED_ENABLE_PIN); + else // disable during other parts of the ramp + LED_ENABLE_PORT &= ~(1 << LED_ENABLE_PIN); + #endif + + // for drivers with a slow regulator chip (eg, boost converter), + // delay before lighting up to prevent flashes + #ifdef LED_ON_DELAY + // only delay if the pin status changed + if (LED_ENABLE_PORT != led_enable_port_save) + delay_4ms(LED_ON_DELAY/4); + #endif + #endif + #ifdef LED2_ENABLE_PIN + #ifdef LED2_ON_DELAY + uint8_t led2_enable_port_save = LED2_ENABLE_PORT; + #endif + + LED2_ENABLE_PORT |= (1 << LED2_ENABLE_PIN); + + // for drivers with a slow regulator chip (eg, boost converter), + // delay before lighting up to prevent flashes + #ifdef LED2_ON_DELAY + // only delay if the pin status changed + if (LED2_ENABLE_PORT != led2_enable_port_save) + delay_4ms(LED2_ON_DELAY/4); + #endif + #endif + + // PWM array index = level - 1 + level --; + + #if PWM_CHANNELS >= 1 + PWM1_LVL = PWM_GET(pwm1_levels, level); + #endif + #if PWM_CHANNELS >= 2 + PWM2_LVL = PWM_GET(pwm2_levels, level); + #endif + #if PWM_CHANNELS >= 3 + PWM3_LVL = PWM_GET(pwm3_levels, level); + #endif + + #ifdef USE_DYN_PWM + uint16_t top = PWM_GET(pwm_tops, level); + #if defined(PWM1_CNT) && defined(PWM1_PHASE_SYNC) + // wait to ensure compare match won't be missed + // (causes visible flickering when missed, because the counter + // goes all the way to 65535 before returning) + // (see attiny1634 reference manual page 103 for a warning about + // the timing of changing the TOP value (section 12.8.4)) + // (but don't wait when turning on from zero, because + // it'll reset the phase below anyway) + // to be safe, allow at least 32 cycles to update TOP + while(actual_level && (PWM1_CNT > (top - 32))) {} + #endif + // pulse frequency modulation, a.k.a. dynamic PWM + PWM1_TOP = top; + #endif // ifdef USE_DYN_PWM + #if defined(PWM1_CNT) && defined(PWM1_PHASE_RESET_ON) + // force reset phase when turning on from zero + // (because otherwise the initial response is inconsistent) + if (! actual_level) { + PWM1_CNT = 0; + #if defined(PWM2_CNT) && defined(PWM2_PHASE_RESET_ON) + PWM2_CNT = 0; + #endif + #if defined(PWM3_CNT) && defined(PWM3_PHASE_RESET_ON) + PWM3_CNT = 0; + #endif + } + #endif + } + #ifdef USE_DYNAMIC_UNDERCLOCKING + auto_clock_speed(); + #endif +} +#endif + + +#ifdef USE_SET_LEVEL_GRADUALLY +inline void set_level_gradually(uint8_t lvl) { + gradual_target = lvl; +} + + +// call this every frame or every few frames to change brightness very smoothly +void gradual_tick() { + uint8_t gt = gradual_target; + if (gt < actual_level) gt = actual_level - 1; + else if (gt > actual_level) gt = actual_level + 1; + + // call the relevant hardware-specific function + GradualTickFuncPtr gradual_tick_func = channels[channel_mode].gradual_tick; + bool done = gradual_tick_func(gt - 1); + + if (done) { + uint8_t orig = gradual_target; + set_level(gt); + gradual_target = orig; + } +} +#endif // ifdef USE_SET_LEVEL_GRADUALLY + + +#endif // ifdef USE_RAMPING + diff --git a/fsm/ramping.h b/fsm/ramping.h new file mode 100644 index 0000000..c4b7d48 --- /dev/null +++ b/fsm/ramping.h @@ -0,0 +1,167 @@ +// fsm-ramping.h: Ramping functions for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#ifdef USE_RAMPING + +// actual_level: last ramp level set by set_level() +uint8_t actual_level = 0; +// the level used before actual +uint8_t prev_level = 0; + +void set_level(uint8_t level); +//void set_level_smooth(uint8_t level); +void set_level_zero(); // implement this in a hwdef + +#ifdef USE_SET_LEVEL_GRADUALLY +// adjust brightness very smoothly +uint8_t gradual_target; +inline void set_level_gradually(uint8_t lvl); +void gradual_tick(); + +// reduce repetition with macros +#define GRADUAL_TICK_SETUP() \ + PWM_DATATYPE target; + +// tick to a specific value +#define GRADUAL_ADJUST_SIMPLE(TARGET,PWM) \ + if (PWM < TARGET) PWM ++; \ + else if (PWM > TARGET) PWM --; + +// tick to a specific value, except when immediate 0 to 255 is needed +#define GRADUAL_ADJUST_STACKED(TARGET,PWM,TOP) \ + if ( ((PWM == 0) && (TARGET == TOP)) \ + || ((PWM == TOP) && (TARGET == 0))) \ + PWM = TARGET; \ + else GRADUAL_ADJUST_SIMPLE(TARGET,PWM) + +// tick the top layer of the stack +#define GRADUAL_ADJUST_1CH(TABLE,PWM) \ + target = PWM_GET(TABLE, gt); \ + if (PWM < target) PWM ++; \ + else if (PWM > target) PWM --; + +// tick a base level of the stack +// (with support for special DD FET behavior +// like "low=0, high=255" --> "low=255, high=254") +#define GRADUAL_ADJUST(TABLE,PWM,TOP) \ + target = PWM_GET(TABLE, gt); \ + if ((gt < actual_level) \ + && (PWM == 0) \ + && (target == TOP)) PWM = TOP; \ + else \ + if (PWM < target) PWM ++; \ + else if (PWM > target) PWM --; + +#endif // ifdef USE_SET_LEVEL_GRADUALLY + +// auto-detect the data type for PWM tables +// FIXME: PWM bits and data type should be per PWM table +// FIXME: this whole thing is a mess and should be removed +#ifndef PWM1_BITS + #define PWM1_BITS 8 + #define PWM1_TOP 255 + #define STACKED_PWM_TOP 255 +#endif +#if PWM_BITS <= 8 + #define STACKED_PWM_DATATYPE uint8_t + #define PWM_DATATYPE uint8_t + #define PWM_DATATYPE2 uint16_t + #ifndef PWM_TOP + #define PWM_TOP 255 + #endif + #define STACKED_PWM_TOP 255 + #ifndef PWM_GET + #define PWM_GET(x,y) pgm_read_byte(x+y) + #endif +#else + #define STACKED_PWM_DATATYPE uint16_t + #define PWM_DATATYPE uint16_t + #ifndef PWM_DATATYPE2 + #define PWM_DATATYPE2 uint32_t + #endif + #ifndef PWM_TOP + #define PWM_TOP 1023 // 10 bits by default + #endif + #ifndef STACKED_PWM_TOP + #define STACKED_PWM_TOP 1023 + #endif + // pointer plus 2*y bytes + //#define PWM_GET(x,y) pgm_read_word(x+(2*y)) + // nope, the compiler was already doing the math correctly + #ifndef PWM_GET + #define PWM_GET(x,y) pgm_read_word(x+y) + #endif +#endif +#define PWM_GET8(x,y) pgm_read_byte(x+y) +#define PWM_GET16(x,y) pgm_read_word(x+y) + +// use UI-defined ramp tables if they exist +#ifdef PWM1_LEVELS +PROGMEM const PWM1_DATATYPE pwm1_levels[] = { PWM1_LEVELS }; +#endif +#ifdef PWM2_LEVELS +PROGMEM const PWM2_DATATYPE pwm2_levels[] = { PWM2_LEVELS }; +#endif +#ifdef PWM3_LEVELS +PROGMEM const PWM3_DATATYPE pwm3_levels[] = { PWM3_LEVELS }; +#endif +#ifdef PWM4_LEVELS +PROGMEM const PWM4_DATATYPE pwm4_levels[] = { PWM4_LEVELS }; +#endif +#ifdef PWM5_LEVELS +PROGMEM const PWM5_DATATYPE pwm5_levels[] = { PWM5_LEVELS }; +#endif + +// convenience defs for 1 LED with stacked channels +// FIXME: remove this, use pwm1/2/3 instead +#ifdef LOW_PWM_LEVELS +PROGMEM const PWM_DATATYPE low_pwm_levels[] = { LOW_PWM_LEVELS }; +#endif +#ifdef MED_PWM_LEVELS +PROGMEM const PWM_DATATYPE med_pwm_levels[] = { MED_PWM_LEVELS }; +#endif +#ifdef HIGH_PWM_LEVELS +PROGMEM const PWM_DATATYPE high_pwm_levels[] = { HIGH_PWM_LEVELS }; +#endif + +// 2 channel CCT blending ramp +#ifdef BLEND_PWM_LEVELS +// FIXME: remove this, use pwm1/2/3 instead +PROGMEM const PWM_DATATYPE blend_pwm_levels[] = { BLEND_PWM_LEVELS }; +#endif + + +// pulse frequency modulation, a.k.a. dynamic PWM +// (different ceiling / frequency at each ramp level) +// FIXME: dynamic PWM should be a per-channel option, not global +#ifdef PWM_TOPS +PROGMEM const PWM_DATATYPE pwm_tops[] = { PWM_TOPS }; +#endif + +// FIXME: jump start should be per channel / channel mode +#ifdef USE_JUMP_START + #ifndef JUMP_START_TIME + #define JUMP_START_TIME 8 // in ms, should be 4, 8, or 12 + #endif + #ifndef DEFAULT_JUMP_START_LEVEL + #define DEFAULT_JUMP_START_LEVEL 10 + #endif + #ifdef USE_CFG + #define JUMP_START_LEVEL cfg.jump_start_level + #else + #define JUMP_START_LEVEL jump_start_level + uint8_t jump_start_level = DEFAULT_JUMP_START_LEVEL; + #endif +#endif + +// RAMP_SIZE / MAX_LVL +// cfg-*.h should define RAMP_SIZE +//#define RAMP_SIZE (sizeof(stacked_pwm1_levels)/sizeof(STACKED_PWM_DATATYPE)) +#define MAX_LEVEL RAMP_SIZE + + +#endif // ifdef USE_RAMPING + diff --git a/fsm/random.c b/fsm/random.c new file mode 100644 index 0000000..91fd929 --- /dev/null +++ b/fsm/random.c @@ -0,0 +1,16 @@ +// fsm-random.c: Random number generator for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#ifdef USE_PSEUDO_RAND +uint8_t pseudo_rand() { + static uint16_t offset = 1024; + // loop from 1024 to 4095 + offset = ((offset + 1) & 0x0fff) | 0x0400; + pseudo_rand_seed += 0b01010101; // 85 + return pgm_read_byte(offset) + pseudo_rand_seed; +} +#endif + diff --git a/fsm/random.h b/fsm/random.h new file mode 100644 index 0000000..49aa0cf --- /dev/null +++ b/fsm/random.h @@ -0,0 +1,12 @@ +// fsm-random.h: Random number generator for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#ifdef USE_PSEUDO_RAND +uint8_t pseudo_rand(); +// TODO: test without "volatile", in case it's not needed +volatile uint8_t pseudo_rand_seed = 0; +#endif + diff --git a/fsm/spaghetti-monster.h b/fsm/spaghetti-monster.h new file mode 100644 index 0000000..77431f8 --- /dev/null +++ b/fsm/spaghetti-monster.h @@ -0,0 +1,75 @@ +// spaghetti-monster.h: UI toolkit / microkernel for e-switch flashlights. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +/* + * SpaghettiMonster: Generic foundation code for e-switch flashlights. + * Other possible names: + * - FSM + * - RoundTable + * - Mostly Harmless + * - ... + */ + +#include "tk-attiny.h" + +#include <avr/eeprom.h> +#include <avr/power.h> + +// include project definitions to help with recognizing symbols +#include "fsm-events.h" +#include "fsm-states.h" +#include "fsm-adc.h" +#include "fsm-wdt.h" +#include "fsm-pcint.h" +#include "fsm-standby.h" +#include "fsm-channels.h" +#include "fsm-ramping.h" +#include "fsm-random.h" +#ifdef USE_EEPROM +#include "fsm-eeprom.h" +#endif +#include "fsm-misc.h" +#include "fsm-main.h" + +#if defined(USE_DELAY_MS) || defined(USE_DELAY_4MS) || defined(USE_DELAY_ZERO) || defined(USE_DEBUG_BLINK) +#define OWN_DELAY +#include "tk-delay.h" +#endif + +#ifdef USE_DEBUG_BLINK +#define DEBUG_FLASH PWM1_LVL = 64; delay_4ms(2); PWM1_LVL = 0; +void debug_blink(uint8_t num) { + for(; num>0; num--) { + PWM1_LVL = 32; + delay_4ms(100/4); + PWM1_LVL = 0; + delay_4ms(100/4); + } +} +#endif + +// Define these in your SpaghettiMonster recipe +// boot-time tasks +void setup(); +// single loop iteration, runs continuously +void loop(); + +// include executable functions too, for easier compiling +#include "fsm-states.c" +#include "fsm-events.c" +#include "fsm-adc.c" +#include "fsm-wdt.c" +#include "fsm-pcint.c" +#include "fsm-standby.c" +#include "fsm-channels.c" +#include "fsm-ramping.c" +#include "fsm-random.c" +#ifdef USE_EEPROM +#include "fsm-eeprom.c" +#endif +#include "fsm-misc.c" +#include "fsm-main.c" + diff --git a/fsm/spaghetti-monster.txt b/fsm/spaghetti-monster.txt new file mode 100644 index 0000000..434e1bc --- /dev/null +++ b/fsm/spaghetti-monster.txt @@ -0,0 +1,325 @@ +Spaghetti Monster: A UI toolkit library for flashlights +------------------------------------------------------- + +This toolkit takes care of most of the obnoxious parts of dealing with +tiny embedded chips and flashlight hardware, leaving you to focus on the +interface and user-visible features. + +For a quick start, look at the example UIs provided to see how things +are done. They are probably the most useful reference. However, other +details can be found here or in the FSM source code. + + +Why is it called Spaghetti Monster? + + This toolkit is a finite state machine, or FSM. Another thing FSM + stands for is Flying Spaghetti Monster. Source code tends to weave + into intricate knots like spaghetti, called spaghetti code, + particularly when the code isn't using appropriate abstractions for + the task it implements. + + Prior e-switch light code had a tendency to get pretty spaghetti-like, + and it made the code difficult to write, understand, and modify. So I + started from scratch and logically separated the hardware details from + the UI. This effectively put the spaghetti monster in a box, put it + on a leash, to make it behave and stay out of the way while we focus + on the user interface. + + Also, it's just kind of a fun name. :) + + +General concept: + + Spaghetti Monster (FSM) implements a stack-based finite state machine + with an event-handling system. + + Each FSM program should have a setup() function, a loop() function, + and at least one State: + + - The setup() function runs once each time power is connected. + + - The loop() function is called repeatedly whenever the system is + otherwise idle. Put your long-running tasks here, preferably with + consideration taken to allow for cooperative multitasking. + + - The States on the stack will be called whenever an event happens. + States are called in top-to-bottom order until a state returns an + "EVENT_HANDLED" signal. Only do quick tasks here. + + +Finite State Machine: + + Each "State" is simply a callback function which handles events. It + should return EVENT_HANDLED for each event type it does something + with, or EVENT_NOT_HANDLED otherwise. + + Transitions between states typically involve mapping an Event to a new + State, such as this: + + // 3 clicks: go to strobe modes + else if (event == EV_3clicks) { + set_state(strobe_state, 0); + return EVENT_HANDLED; + } + + It is strongly recommended that your State functions never do anything + which takes more than a few milliseconds... and certainly not longer + than 16ms. If you do this, the pending events may pile up to the + point where new events get thrown away. So, do only quick tasks in + the event handler, and do your longer-running tasks in the loop() + function instead. Preferably with precautions taken to allow for + cooperative multitasking. + + If your State function takes longer than one WDT tick (16ms) once in a + while, the system won't break. Several events can be queued. But be + sure not to do it very often. + + Several state management functions are provided: + + - set_state(new_state, arg): Replace the current state on the stack. + Send 'arg' to the new state for its init event. + + - push_state(new_state, arg): Add a new state to the stack, leaving + the current state below it. Send 'arg' to the new state for its + init event. + + - pop_state(): Get rid of (and return) the top-most state. Re-enter + the state below. + + +Event types: + + Event types are defined in fsm-events.h. You may want to adjust these + to fit your program, but the defaults are: + + State transitions: + + - EV_enter_state: Sent to each new State once when it goes onto + the stack. The 'arg' is whatever you define it to be. + + - EV_leave_state: Sent to a state immediately before it is removed + from the stack. + + - EV_reenter_state: If a State gets pushed on top of this one, and + then it pops off, a re-enter Event happens. This should handle + things like consuming the return value of a nested input handler + State. + + Time passing: + + - EV_tick: This happens once per clock tick, which is 16ms or + 62.5Hz by default. The 'arg' is the number of ticks since + entering the state. When 'arg' exceeds 65535, it wraps around + to 32768. + + - EV_sleep_tick: This happens every 0.5s during standby, if + enabled at compile time. The 'arg' is the number of ticks since + entering the state. When 'arg' exceeds 65535, it wraps around + to 32768. + + LVP and thermal regulation: + + - EV_voltage_low: Sent whenever the input power drops below the + VOLTAGE_LOW threshold. Minimum of VOLTAGE_WARNING_SECONDS + between events. + + - EV_temperature_high: Sent whenever the MCU's projected temperature + is higher than therm_ceil. Minimum of one second between events. + The 'arg' indicates how far the temperature exceeds the limit. + + - EV_temperature_low: Sent whenever the MCU's projected temperature + is lower than (therm_ceil - THERMAL_WINDOW_SIZE). Minimum of + one second between events. The 'arg' indicates how far the + temperature exceeds the limit. + + Button presses: + + Button events can be referred to either by pre-defined symbols, or + by teasing out the flags manually. The structure of a button + event is as follows: + + - Bit 7: 1 for button events, 0 otherwise. + + - Bit 6: 1 for a "timeout" event (signals the end of a + sequence), or 0 otherwise. + + - Bit 5: 1 for a "hold" event, 0 otherwise. This flag is only + necessary because, without it, it would be impossible to + distinguish between "click, click, timeout" and "click, hold, + release". + + - Bit 4: 1 if button is currently pressed, 0 otherwise. Button + release events look just like button press events, except this + is not set. + + - Bits 0,1,2,3: Counter for how many clicks there have been. + The first click is 1, second is 2, and it goes up to 15 clicks + in a row. Clicks after 15 are coded as 15. + + The pre-defined button event symbols are like the following: + + - EV_click1_press: The user pressed the button, but no time has + passed since then. + + - EV_click1_release: The user pressed and released the button, + but no time has passed since then. + + - EV_click1_complete: The user clicked the e-switch, released + it, and enough time passed that no more clicks were detected. + (a.k.a. EV_1click) + + - EV_click1_hold: The user pressed the button, and continued + holding it long enough to count as a "hold" event. This event + is sent once per timer tick as long as the button is held, and + the 'arg' value indicates how many timer ticks since the + button state went from 'press' to 'hold'. + + - EV_click1_hold_release: The button was released at the end of + a "hold" event. This is the end of the input sequence, + because no timeout period is used after a hold. + + It's worth noting that a "hold" event can only happen at the + end of an input sequence, and the sequence will reset to empty + after the hold is released. + + If the user pressed the button more than once, events follow the + same pattern. These are the same as above, except with a full + short-press and release first. + + - EV_click2_press + - EV_click2_release + - EV_click2_complete (a.k.a. EV_2clicks) + - EV_click2_hold + - EV_click2_hold_release + + Each of the above patterns continues up to 15 clicks. + + To match entire categories of events, use the bitmasks provided. + For example, to match button events where the button is down or + the button is up, the code would look like this: + + if ((event & (B_CLICK | B_PRESS)) == (B_CLICK | B_PRESS)) { + // button is down (can be a press event or a hold event) + } + else if ((event & (B_CLICK | B_PRESS)) == (B_CLICK)) { + // button was just released + } + + In theory, you could also define your own arbitrary event types, and + emit() them as necessary, and handle them in State functions the same + as any other event. + + +Cooperative multitasking: + + Since we don't have true preemptive multitasking, the best we can do + is cooperative multitasking. In practice, this means: + + - Declare global variables as volatile if they can be changed by an + event handler. This keeps the compiler from caching the value and + causing incorrect behavior. + + - Don't put long-running tasks into State functions. Each State + will get called at least once every 16ms for a clock tick, so they + should not run for longer than 16ms. + + - Put long-running tasks into loop() instead. + + - For long delay() calls, use nice_delay_ms(). This allows the MCU + to process events while we wait. It also automatically aborts if + it detects a state change, and returns a different value. + + In many cases, it shouldn't be necessary to do anything more than + this, but sometimes it will also be a good idea to check the + return value and abort the current task: + + if (! nice_delay_ms(mydelay)) break; + + - In general, try to do small amounts of work and then return + control to other parts of the program. Keep doing small amounts + and yielding until a task is done, instead of trying to do it all + at once. + + +Persistent data in EEPROM: + + To save data which lasts after a battery change, use the eeprom + functions. Define an eeprom style (or two) at the top, define how + many bytes to allocate, and then use the relevant functions as + appropriate. + + - USE_EEPROM / USE_EEPROM_WL: Enable the eeprom-related functions. + With "WL", it uses wear-levelling. Without, it does not. Note: + Wear levelling is not necessarily better -- it uses more ROM, and + it writes more bytes per save(). So, use it only for a few bytes + which change frequently -- not for many bytes or infrequent + changes. + + - EEPROM_BYTES N / EEPROM_WL_BYTES N: Allocate N bytes for the + eeprom data. + + - load_eeprom() / load_eeprom_wl(): Load the stored data into the + eeprom[] or eeprom_wl[] arrays. + Returns 1 if data was found, 0 otherwise. + + - save_eeprom() / save_eeprom_wl(): Save the eeprom[] or eeprom_wl[] + array data to persistent storage. The WL version erases all old + values and writes new ones in a different part of the eeprom + space. The non-WL version updates values in place, and does not + overwrite values which didn't change. + + Note that all interrupts will be disabled during eeprom operations. + + +Useful #defines: + + A variety of things can be #defined before including + spaghetti-monster.h in your program. This allows you to tweak the + behavior and set options to fit your needs: + + - FSM_something_LAYOUT: Select a driver type from tk-attiny.h. This + controls how many power channels there are, which pins they're on, + and what other driver features are available. + + - USE_LVP: Enable low-voltage protection. + + - VOLTAGE_LOW: What voltage should LVP trigger at? Defaults to 29 (2.9V). + + - VOLTAGE_FUDGE_FACTOR: Add this much to the voltage measurements, + to compensate for voltage drop across the reverse-polarity + diode. + + - VOLTAGE_WARNING_SECONDS: How long to wait between LVP events. + + - USE_THERMAL_REGULATION: Enable thermal regulation + + - DEFAULT_THERM_CEIL: Set the temperature limit to use by default + when the user hasn't configured anything. + + - USE_RAMPING: Enable smooth ramping helpers. + + - RAMP_LENGTH: Pick a pre-defined ramp by length. Defined sizes + are 50, 75, and 150 levels. + + - USE_DELAY_4MS, USE_DELAY_MS, USE_DELAY_ZERO: Enable the delay_4ms, + delay_ms(), and delay_zero() functions. Useful for timing-related + activities. + + - HOLD_TIMEOUT: How many clock ticks before a "press" event becomes + a "hold" event? + + - RELEASE_TIMEOUT: How many clock ticks before a "release" event + becomes a "click" event? Basically, the maximum time between + clicks in a double-click or triple-click. + + - USE_BATTCHECK: Enable the battcheck function. Also define one of + the following to select a display style: + + - BATTCHECK_VpT: Volts, pause, tenths. + - BATTCHECK_4bars: Blink up to 4 times. + - BATTCHECK_6bars: Blink up to 6 times. + - BATTCHECK_8bars: Blink up to 8 times. + + - ... and many others. Will try to document them over time, but + they can be found by searching for pretty much anything in + all-caps in the fsm-*.[ch] files. diff --git a/fsm/standby.c b/fsm/standby.c new file mode 100644 index 0000000..5def07c --- /dev/null +++ b/fsm/standby.c @@ -0,0 +1,105 @@ +// fsm-standby.c: standby mode functions for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <avr/interrupt.h> +#include <avr/sleep.h> + +#include "fsm-adc.h" +#include "fsm-wdt.h" +#include "fsm-pcint.h" + +// low-power standby mode used while off but power still connected +#define standby_mode sleep_until_eswitch_pressed +void sleep_until_eswitch_pressed() +{ + #ifdef TICK_DURING_STANDBY + WDT_slow(); + #else + WDT_off(); + #endif + + ADC_off(); + + // make sure switch isn't currently pressed + while (button_is_pressed()) {} + empty_event_sequence(); // cancel pending input on suspend + + PCINT_on(); // wake on e-switch event + + #ifdef TICK_DURING_STANDBY + // detect which type of event caused a wake-up + irq_adc = 0; + irq_wdt = 0; + irq_pcint = 0; + while (go_to_standby) { + #else + go_to_standby = 0; + #endif + + // configure sleep mode + #ifdef TICK_DURING_STANDBY + // needs a special sleep mode during measurements + if (adc_active_now) adc_sleep_mode(); + else + #endif + set_sleep_mode(SLEEP_MODE_PWR_DOWN); + + sleep_enable(); + #ifdef BODCR // only do this on MCUs which support it + sleep_bod_disable(); + #endif + sleep_cpu(); // wait here + + // something happened; wake up + sleep_disable(); + + #ifdef TICK_DURING_STANDBY + // determine what woke us up... + if (irq_pcint) { // button pressed; wake up + go_to_standby = 0; + } + if (irq_adc) { // ADC done measuring + #ifndef USE_LOWPASS_WHILE_ASLEEP + adc_reset = 1; // don't lowpass while asleep + #endif + adc_deferred_enable = 1; + adc_deferred(); + //ADC_off(); // takes care of itself + //irq_adc = 0; // takes care of itself + } + if (irq_wdt) { // generate a sleep tick + WDT_inner(); + } + } + #endif + + // don't lowpass immediately after waking + // also, reset thermal history + adc_reset = 2; + + // go back to normal running mode + // PCINT not needed any more, and can cause problems if on + // (occasional reboots on wakeup-by-button-press) + PCINT_off(); + // restore normal awake-mode interrupts + ADC_on(); + WDT_on(); +} + +#ifdef USE_IDLE_MODE +void idle_mode() +{ + // configure sleep mode + set_sleep_mode(SLEEP_MODE_IDLE); + + sleep_enable(); + sleep_cpu(); // wait here + + // something happened; wake up + sleep_disable(); +} +#endif + diff --git a/fsm/standby.h b/fsm/standby.h new file mode 100644 index 0000000..957e2e1 --- /dev/null +++ b/fsm/standby.h @@ -0,0 +1,68 @@ +// fsm-standby.h: standby mode functions for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +// deferred "off" so we won't suspend in a weird state +// (like... during the middle of a strobe pulse) +// set this to nonzero to enter standby mode next time the system is idle +volatile uint8_t go_to_standby = 0; + +#ifdef TICK_DURING_STANDBY +#ifndef STANDBY_TICK_SPEED +#define STANDBY_TICK_SPEED 3 // every 0.128 s +/* + * From the Attiny85 manual: + * 0: 16 ms + * 1: 32 ms + * 2: 64 ms + * 3: 0.128 s + * 4: 0.256 s + * 5: 0.512 s + * 6: 1.0 s + * 7: 2.0 s + * 32: 4.0 s + * 33: 8.0 s + * (other values may have unexpected effects; not sure why the final bit is + * separated from the others, in the "32" position instead of "8", but that's + * how it is) + */ +#endif + +#if (STANDBY_TICK_SPEED == 1) +#define SLEEP_TICKS_PER_SECOND 31 +#define SLEEP_TICKS_PER_MINUTE 1800 + +#elif (STANDBY_TICK_SPEED == 2) +#define SLEEP_TICKS_PER_SECOND 16 +#define SLEEP_TICKS_PER_MINUTE 900 + +#elif (STANDBY_TICK_SPEED == 3) +#define SLEEP_TICKS_PER_SECOND 8 +#define SLEEP_TICKS_PER_MINUTE 450 + +#elif (STANDBY_TICK_SPEED == 4) +#define SLEEP_TICKS_PER_SECOND 4 +#define SLEEP_TICKS_PER_MINUTE 225 + +#elif (STANDBY_TICK_SPEED == 5) +#define SLEEP_TICKS_PER_SECOND 2 +#define SLEEP_TICKS_PER_MINUTE 113 + +#elif (STANDBY_TICK_SPEED == 6) +#define SLEEP_TICKS_PER_SECOND 1 +#define SLEEP_TICKS_PER_MINUTE 57 + +#endif +#endif + +#define standby_mode sleep_until_eswitch_pressed +void sleep_until_eswitch_pressed(); + +#ifdef USE_IDLE_MODE +// stops processing until next click or timer tick +// (I think) +void idle_mode(); +#endif + diff --git a/fsm/states.c b/fsm/states.c new file mode 100644 index 0000000..4b94ce9 --- /dev/null +++ b/fsm/states.c @@ -0,0 +1,105 @@ +// fsm-states.c: State-handling functions for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "fsm-states.h" +#include "fsm-adc.h" + +// TODO: if callback doesn't handle current event, +// pass event to next state on stack? +// Callback return values: +// 0: event handled normally +// 1: event not handled +// 255: error (not sure what this would even mean though, or what difference it would make) +// TODO: function to call stacked callbacks until one returns "handled" + +void _set_state(StatePtr new_state, uint16_t arg, + Event exit_event, Event enter_event) { + // call old state-exit hook (don't use stack) + if (current_state != NULL) current_state(exit_event, arg); + // set new state + current_state = new_state; + // call new state-enter hook (don't use stack) + if (new_state != NULL) current_state(enter_event, arg); + + // since state changed, stop any animation in progress + interrupt_nice_delays(); +} + +int8_t push_state(StatePtr new_state, uint16_t arg) { + if (state_stack_len < STATE_STACK_SIZE) { + // TODO: call old state's exit hook? + // new hook for non-exit recursion into child? + state_stack[state_stack_len] = new_state; + state_stack_len ++; + // FIXME: use EV_stacked_state? + _set_state(new_state, arg, EV_leave_state, EV_enter_state); + return state_stack_len; + } else { + // TODO: um... how is a flashlight supposed to handle a recursion depth error? + return -1; + } +} + +StatePtr pop_state() { + // TODO: how to handle pop from empty stack? + StatePtr old_state = NULL; + StatePtr new_state = NULL; + if (state_stack_len > 0) { + state_stack_len --; + old_state = state_stack[state_stack_len]; + } + if (state_stack_len > 0) { + new_state = state_stack[state_stack_len-1]; + } + // FIXME: what should 'arg' be? (maybe re-entry should be entry with arg+1?) + _set_state(new_state, 0, EV_leave_state, EV_reenter_state); + return old_state; +} + +uint8_t set_state(StatePtr new_state, uint16_t arg) { + // FIXME: this calls exit/enter hooks it shouldn't + // (for the layer underneath the top) + pop_state(); + return push_state(new_state, arg); +} + +void set_state_deferred(StatePtr new_state, uint16_t arg) { + deferred_state = new_state; + deferred_state_arg = arg; +} + +#ifndef DONT_USE_DEFAULT_STATE +// bottom state on stack +// handles default actions for LVP, thermal regulation, etc +uint8_t default_state(Event event, uint16_t arg) { + if (0) {} // this should get compiled out + + #ifdef USE_LVP + else if (event == EV_voltage_low) { + low_voltage(); + return EVENT_HANDLED; + } + #endif + + #if 0 + #ifdef USE_THERMAL_REGULATION + else if (event == EV_temperature_high) { + high_temperature(); + return 0; + } + + else if (event == EV_temperature_low) { + low_temperature(); + return 0; + } + #endif + #endif + + // event not handled + return EVENT_NOT_HANDLED; +} +#endif + diff --git a/fsm/states.h b/fsm/states.h new file mode 100644 index 0000000..156e6cf --- /dev/null +++ b/fsm/states.h @@ -0,0 +1,37 @@ +// fsm-states.h: State-handling functions for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "fsm-adc.h" + +// typedefs +typedef uint8_t State(Event event, uint16_t arg); +typedef State * StatePtr; + +// top of the stack +volatile StatePtr current_state; + +// stack for states, to allow shared utility states like "input a number" +// and such, which return to the previous state after finishing +#define STATE_STACK_SIZE 8 +StatePtr state_stack[STATE_STACK_SIZE]; +uint8_t state_stack_len = 0; + +void _set_state(StatePtr new_state, uint16_t arg, + Event exit_event, Event enter_event); +int8_t push_state(StatePtr new_state, uint16_t arg); +StatePtr pop_state(); +uint8_t set_state(StatePtr new_state, uint16_t arg); + +// if loop() needs to change state, use this instead of set_state() +// (because this avoids race conditions) +volatile StatePtr deferred_state; +volatile uint16_t deferred_state_arg; +void set_state_deferred(StatePtr new_state, uint16_t arg); + +#ifndef DONT_USE_DEFAULT_STATE +uint8_t default_state(Event event, uint16_t arg); +#endif + diff --git a/fsm/wdt.c b/fsm/wdt.c new file mode 100644 index 0000000..64f006e --- /dev/null +++ b/fsm/wdt.c @@ -0,0 +1,197 @@ +// fsm-wdt.c: WDT (Watch Dog Timer) functions for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <avr/interrupt.h> +#include <avr/wdt.h> + +// *** Note for the AVRXMEGA3 (1-Series, eg 816 and 817), the WDT +// is not used for time-based interrupts. A new peripheral, the +// Periodic Interrupt Timer ("PIT") is used for this purpose. + +void WDT_on() +{ + #if (ATTINY == 25) || (ATTINY == 45) || (ATTINY == 85) + // interrupt every 16ms + //cli(); // Disable interrupts + wdt_reset(); // Reset the WDT + WDTCR |= (1<<WDCE) | (1<<WDE); // Start timed sequence + WDTCR = (1<<WDIE); // Enable interrupt every 16ms + //sei(); // Enable interrupts + #elif (ATTINY == 1634) + wdt_reset(); // Reset the WDT + WDTCSR = (1<<WDIE); // Enable interrupt every 16ms + #elif defined(AVRXMEGA3) // ATTINY816, 817, etc + RTC.PITINTCTRL = RTC_PI_bm; // enable the Periodic Interrupt + while (RTC.PITSTATUS > 0) {} // make sure the register is ready to be updated + RTC.PITCTRLA = RTC_PERIOD_CYC512_gc | RTC_PITEN_bm; // Period = 16ms, enable the PI Timer + #else + #error Unrecognized MCU type + #endif +} + +#ifdef TICK_DURING_STANDBY +inline void WDT_slow() +{ + #if (ATTINY == 25) || (ATTINY == 45) || (ATTINY == 85) + // interrupt slower + //cli(); // Disable interrupts + wdt_reset(); // Reset the WDT + WDTCR |= (1<<WDCE) | (1<<WDE); // Start timed sequence + WDTCR = (1<<WDIE) | STANDBY_TICK_SPEED; // Enable interrupt every so often + //sei(); // Enable interrupts + #elif (ATTINY == 1634) + wdt_reset(); // Reset the WDT + WDTCSR = (1<<WDIE) | STANDBY_TICK_SPEED; + #elif defined(AVRXMEGA3) // ATTINY816, 817, etc + RTC.PITINTCTRL = RTC_PI_bm; // enable the Periodic Interrupt + while (RTC.PITSTATUS > 0) {} // make sure the register is ready to be updated + RTC.PITCTRLA = (1<<6) | (STANDBY_TICK_SPEED<<3) | RTC_PITEN_bm; // Set period, enable the PI Timer + #else + #error Unrecognized MCU type + #endif +} +#endif + +inline void WDT_off() +{ + #if (ATTINY == 25) || (ATTINY == 45) || (ATTINY == 85) + //cli(); // Disable interrupts + wdt_reset(); // Reset the WDT + MCUSR &= ~(1<<WDRF); // Clear Watchdog reset flag + WDTCR |= (1<<WDCE) | (1<<WDE); // Start timed sequence + WDTCR = 0x00; // Disable WDT + //sei(); // Enable interrupts + #elif (ATTINY == 1634) + cli(); // needed because CCP, below + wdt_reset(); // Reset the WDT + MCUSR &= ~(1<<WDRF); // clear watchdog reset flag + CCP = 0xD8; // enable config changes + WDTCSR = 0; // disable and clear all WDT settings + sei(); + #elif defined(AVRXMEGA3) // ATTINY816, 817, etc + while (RTC.PITSTATUS > 0) {} // make sure the register is ready to be updated + RTC.PITCTRLA = 0; // Disable the PI Timer + #else + #error Unrecognized MCU type + #endif +} + +// clock tick -- this runs every 16ms (62.5 fps) +#ifdef AVRXMEGA3 // ATTINY816, 817, etc +ISR(RTC_PIT_vect) { + RTC.PITINTFLAGS = RTC_PI_bm; // clear the PIT interrupt flag +#else +ISR(WDT_vect) { +#endif + irq_wdt = 1; // WDT event happened +} + +void WDT_inner() { + irq_wdt = 0; // WDT event handled; reset flag + + static uint8_t adc_trigger = 0; + + // cache this here to reduce ROM size, because it's volatile + uint16_t ticks_since_last = ticks_since_last_event; + // increment, but loop from max back to half + ticks_since_last = (ticks_since_last + 1) \ + | (ticks_since_last & 0x8000); + // copy back to the original + ticks_since_last_event = ticks_since_last; + + // detect and emit button change events (even during standby) + uint8_t was_pressed = button_last_state; + uint8_t pressed = button_is_pressed(); + if (was_pressed != pressed) { + go_to_standby = 0; + PCINT_inner(pressed); + } + // cache again, in case the value changed + ticks_since_last = ticks_since_last_event; + + #ifdef TICK_DURING_STANDBY + // handle standby mode specially + if (go_to_standby) { + // emit a sleep tick, and process it + emit(EV_sleep_tick, ticks_since_last); + process_emissions(); + + #ifndef USE_SLEEP_LVP + return; // no sleep LVP needed if nothing drains power while off + #else + // stop here, usually... except during the first few seconds asleep, + // and once in a while afterward for sleep LVP + if ((ticks_since_last > (8 * SLEEP_TICKS_PER_SECOND)) + && (0 != (ticks_since_last & 0x0f))) return; + + adc_trigger = 0; // make sure a measurement will happen + adc_active_now = 1; // use ADC noise reduction sleep mode + ADC_on(); // enable ADC voltage measurement functions temporarily + #endif + } + else { // button handling should only happen while awake + #endif + + // if time since last event exceeds timeout, + // append timeout to current event sequence, then + // send event to current state callback + + // callback on each timer tick + if ((current_event & B_FLAGS) == (B_CLICK | B_HOLD | B_PRESS)) { + emit(EV_tick, 0); // override tick counter while holding button + } + else { + emit(EV_tick, ticks_since_last); + } + + // user held button long enough to count as a long click? + if (current_event & B_PRESS) { + // during a "hold", send a hold event each tick, with a timer + if (current_event & B_HOLD) { + emit_current_event(ticks_since_last); + } + // has button been down long enough to become a "hold"? + // (first frame of a "hold" event) + else { + if (ticks_since_last >= HOLD_TIMEOUT) { + ticks_since_last_event = 0; + current_event |= B_HOLD; + emit_current_event(0); + } + } + } + + // event in progress, but button not currently down + else if (current_event) { + // "hold" event just ended + // no timeout required when releasing a long-press + if (current_event & B_HOLD) { + //emit_current_event(ticks_since_last); // should have been emitted by PCINT_inner() + empty_event_sequence(); + } + // end and clear event after release timeout + else if (ticks_since_last >= RELEASE_TIMEOUT) { + current_event |= B_TIMEOUT; + emit_current_event(0); + empty_event_sequence(); + } + } + + #ifdef TICK_DURING_STANDBY + } + #endif + + #if defined(USE_LVP) || defined(USE_THERMAL_REGULATION) + // enable the deferred ADC handler once in a while + if (! adc_trigger) { + ADC_start_measurement(); + adc_deferred_enable = 1; + } + // timing for the ADC handler is every 32 ticks (~2Hz) + adc_trigger = (adc_trigger + 1) & 31; + #endif +} + diff --git a/fsm/wdt.h b/fsm/wdt.h new file mode 100644 index 0000000..abf34c5 --- /dev/null +++ b/fsm/wdt.h @@ -0,0 +1,20 @@ +// fsm-wdt.h: WDT (Watch Dog Timer) functions for SpaghettiMonster. +// Copyright (C) 2017-2023 Selene ToyKeeper +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#define TICKS_PER_SECOND 62 + +void WDT_on(); +inline void WDT_off(); + +volatile uint8_t irq_wdt = 0; // WDT interrupt happened? + +#ifdef TICK_DURING_STANDBY + #if defined(USE_INDICATOR_LED) || defined(USE_AUX_RGB_LEDS) + // measure battery charge while asleep + #define USE_SLEEP_LVP + #endif +#endif + |
