# Combined contents of all articles on [blog.hirnschall.net](https://blog.hirnschall.net/) **descritption:** Engineering projects on electronics, mechanical engineering, and software — from embedded systems and PCB design to FEM simulation and C++. **author:** [Sebastian Hirnschall](https://blog.hirnschall.net/about/) **sitemap:** [sitemap list of html pages](https://blog.hirnschall.net/sitemap.xml) **llms.txt:** [List of all articles as individual markdown with desctiption](https://blog.hirnschall.net/llms.txt) --- ## [All Posts — Blog Archive](https://blog.hirnschall.net/archive/) meta description: Browse every article on the blog: FEM simulation, C++, ESP8266, 3D printing, PCB design, and engineering projects — sorted by category. date published: 07.01.2020 (DD.MM.YYYY format) date last modified: 22.04.2026 (DD.MM.YYYY format) --- # [All Posts — Blog Archive](https://blog.hirnschall.net/archive/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Browse every article on the blog: FEM simulation, C++, ESP8266, 3D printing, PCB design, and engineering projects — sorted by category. meta title: All Posts — Blog Archive date published: 07.01.2020 (DD.MM.YYYY format) date last modified: 22.04.2026 (DD.MM.YYYY format) --- Table of contents ----------------- This site contains a collection of all the articles published on this blog. They are grouped by category rather than chronologically. If you are looking for something specific, you can also use the search bar in the header at the top of the page. ### 1   PCB/RF Design and Simulation * [Gerber To Step Converter (for FEM Simulation)](https://blog.hirnschall.net/gerber-to-step/) * [Custom 4x SATA Backplane](https://blog.hirnschall.net/4x-sata-backplane/) + [Signal Integrity Deep Dive](https://blog.hirnschall.net/4x-sata-backplane/#design) + [FEM Simulation of the finished PCB (3D full wave solver)](https://blog.hirnschall.net/4x-sata-backplane/#em-simulation) --- ### 2   Mechanical Engineering and Simulation * [Auxetic Material Design, Simulation, and 3D Print](https://blog.hirnschall.net/auxetic-material/) * [Symbolic FSAE Physics Solver](https://blog.hirnschall.net/everything-aero-concept/) + [Calculator, Examples, and Docs](https://blog.hirnschall.net/everything-aero/) + [Article](https://blog.hirnschall.net/everything-aero-concept/) * [Circular and Non-Circular Gears for 3D Printing](https://blog.hirnschall.net/gears-for-3d-printing/) * [Computing Eigenfrequencies Using FEM (Netgen/NGSolve)](https://blog.hirnschall.net/calculating-eigenfrequencies-using-fem/) * [Video light stand](https://blog.hirnschall.net/diy-video-light-stand/) --- ### 3   C++/Embedded Software * [A Type-Safe CAN Transport Layer with Code Generation](https://blog.hirnschall.net/can-transport-layer/) * [Programming an Advanced Calculator in C++](https://blog.hirnschall.net/programming-an-advanced-cpp-calculator/) --- ### 4   Arduino, ESP, and 3D Printing Projects * [3D Printed Rubber Band Gun — 6 Shot Pistol with Rotating Drum](https://blog.hirnschall.net/rubber-band-gun/) * [DIY Motorized Camera Slider (with Timelapse Functionality and webui)](https://blog.hirnschall.net/diy-motorized-slider/) * [Useless box with multiple switches](https://blog.hirnschall.net/diy-useless-box/) * [Automatic Chop Sticks](https://blog.hirnschall.net/automatic-chop-sticks/) * [DIY smart window blinds (Alexa and Homekit)](https://blog.hirnschall.net/diy-smart-window-blinds/) --- ### 5   Creative Programming (with p5.js) * [Perfectly Looping Procedural Animations (higher dimensional perlin noise)](https://blog.hirnschall.net/perfect-looping-gif/) * [Flow simulation (flow of a vector field)](https://blog.hirnschall.net/flow-simulation-js/) * [Maze generation](https://blog.hirnschall.net/maze-generation-js/) --- ### 6   Concepts and Ideas * [High precision CNC mill (appartment friendly, for steel/aluminium)](https://blog.hirnschall.net/cnc-mill-concept/) ### 7   References & Guides * [ESP8266 complete guide (reference)](https://blog.hirnschall.net/esp8266/) * [ESP32 complete guide (reference)](https://blog.hirnschall.net/esp32/) * [Perlin noise: What is it and how to use it](https://blog.hirnschall.net/perlin-noise/) * [How This Blog is Built - Github Pages & Cloudflare Edge](https://blog.hirnschall.net/same-domain-analytics/) + [Static Site Generation](https://blog.hirnschall.net/same-domain-analytics/#static-site-generation) + [LLM-Friendly Artifacts: llms.txt and Per-Page Markdown](https://blog.hirnschall.net/same-domain-analytics/#llms) + [Same Domain Analytics](https://blog.hirnschall.net/same-domain-analytics/#same-domain-analytics) + [Architecture — Github Pages and Cloudflare's Edge](https://blog.hirnschall.net/same-domain-analytics/#architecture) * [Online Calculators and Tools: Electronics & Physics](https://blog.hirnschall.net/tools/) --- ## [About — Sebastian Hirnschall, Engineer in Vienna](https://blog.hirnschall.net/about/) meta description: Technical maths student in Vienna documenting real projects: FEM simulation, ESP8266, PCB design, 3D printing, and C++. All code and files open source. date published: 24.03.2026 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [About](https://blog.hirnschall.net/about/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Technical maths student in Vienna documenting real projects: FEM simulation, ESP8266, PCB design, 3D printing, and C++. All code and files open source. meta title: About — Sebastian Hirnschall, Engineer in Vienna date published: 24.03.2026 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- about me -------- Hi! I am currently studying technical maths in Vienna and work part-time at Cerbsim. At the moment, I am most interested in PDEs, Numerics of PDEs and differential geometry. The applied nature of these fields makes them a natural extension of my other hobbies: mechanical engineering, 3d printing, electrical engineering, and software development. You can also find some of my seminar papers and notes from university [here](https://blog.hirnschall.net/downloads/). If you'd like to get in touch, feel free to reach out. Email: [contact [at] hirnschall [dot] net](javascript:linkTo_UnCryptMailto('nbjmup;dpoubduAijsotdibmm/ofu');) about this blog --------------- I started this blog in 2020 to share my favorite projects and thoughts on various technical topics. I just really like to document things. ### open source I document not just to share progress but also to keep track of my thoughts. Sharing knowledge and open sourcing everything is especially important to me as I learned so many topics by watching other peoples content or following along with their projects. Same goes for 3D printing. As long as it was patented the technology was basically unavailable to average people. Almost every post on this blog is open source and published [under a Creative Commons license](https://blog.hirnschall.net/license/). If a project I am working on is not on the blog it is probably on [GitHub](https://github.com/shirnschall). ### attribution For many projects of mine, I talk to friends and colleagues who help me discuss my ideas or outright contribute to the project. However, I generally avoid naming them in the post explicitly. The reason is not that I would not like to give them credit or tell you about how I like working with them, but that I want to avoid the impression of affiliation or endorsement where there is none. I do not like name dropping. Many of my friends and colleagues are known in their respective fields, and mentioning them here would add more weight to this project than is appropriate. If possible I try to link to their page for the project instead. Of course, if they want to be mentioned I am very happy about it. projects I am proud of ---------------------- This section lists some of the projects I am particularly proud of. Not necessarily the most impactful or flashiest ones. * **Netgen contribution:** I contributed two small features to Netgen's workplane object. Namely Arc/ArcTo and Revolve. * **3D Models:** From time to time I get asked to help with creating a 3D model for some simulation or project. You can find two such models [here](https://jschoeberl.github.io/talk-pdesoft/wta/Sydney.html) and [here](https://blog.hirnschall.net/about/). ![3D shell of the Sydney Opera House](https://blog.hirnschall.net/about/resources/img/opera.jpg) Figure 1: 3D shell of the Sydney Opera House * **Lecture Notes:** I had the opportunity to help turn handwritten lecture notes into a nice latex. You can find the result [here](https://www.tuwien.at/index.php?eID=dumpFile&t=f&f=1457291&token=9e374070d403491da4f237053af9f1b8603a6eea). * **Seminar talk:** The seminar talk I gave with Rafael and Christoph on the topic of computing eigenfrequencies using FEM (LOPSD, LOBPCG, etc.) is available on the NGSolve YouTube channel [here](https://youtu.be/35Yh8kGbMLA?si=LkUqX8yW9qpUWgkJ). * **3D Printer:** Built with all metal parts using hand tools, using the e3d toolchanging system, three independent z axes, duet 3 controller, wobble-x, and a heated enclosure. I started designing my printer before voron or ratrig was really a thing. Many design choices were painful to implement as they were not yet common. All in all a super fast, extremely reliable printer with real multi material capabilities that I am very proud of. I designed it in CATIA. * **Useless box:** Although it is a simple project, I made both versions (with one and with multiple switches) as a present for a friend. You can find the project [here](https://blog.hirnschall.net/diy-useless-box/). ![Beautiful pink useless box](https://blog.hirnschall.net/about/resources/img/useless_box.jpg) Figure 2: Beautiful pink useless box * **High Performance Computing:** Together with Rafael and Christoph I did a very interesting project for ["Introduction to Scientific Computing"](https://jschoeberl.github.io/IntroSC/intro.html). The results are one repo for [HPC](https://github.com/DHL-ASC/HPC) and one for [linear algebra](https://github.com/DHL-ASC/DHL-LinAlg). In the end we beat numpy in terms of speed for matrix-matrix and matrix-vector multiplication. The package is on pypi. ![Speed comparison of DHL-Linalg and numpy](https://blog.hirnschall.net/about/resources/img/hpc.jpg) Figure 3: Speed comparison of DHL-Linalg and numpy * **Two and a half years of Formula Student:** Many great memories and I learned so much. But most importantly I made incredible friends. ![Me sitting in the Formula Student car (EDGE15)](https://blog.hirnschall.net/about/resources/img/fs.jpg) Figure 4: Me sitting in the Formula Student car (EDGE15) --- ## [Auxetic Materials Explained: From FEM Simulation to 3D Print](https://blog.hirnschall.net/auxetic-material/) meta description: Auxetic materials expand when stretched. We design a re-entrant structure, simulate it with FEM, and 3D print it in TPU. date published: 21.03.2026 (DD.MM.YYYY format) date last modified: 14.04.2026 (DD.MM.YYYY format) --- # [Design, Simulate (FEM), and 3D Print Auxetic Materials](https://blog.hirnschall.net/auxetic-material/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Auxetic materials expand when stretched. We design a re-entrant structure, simulate it with FEM, and 3D print it in TPU. meta title: Auxetic Materials Explained: From FEM Simulation to 3D Print date published: 21.03.2026 (DD.MM.YYYY format) date last modified: 14.04.2026 (DD.MM.YYYY format) --- Motivation ---------- Normally, when you stretch any normal material it will get thinner and longer. The negative ratio of strain in the direction of stretching to the perpendicular direction is called the Poisson's ratio \(\nu\). \(\nu\) is typically a positive value. Auxetic materials however have a negative Poisson's ratio, meaning they expand in the direction perpendicular to the applied stress when stretched. But how is this possible? One way to achieve this behavior is to design the material with a specific microstructure that allows for this counterintuitive deformation. In this article we will design such a material, simulate it using FEM (NGSolve) and then 3D print it out of TPU. Designing the Microstructure ---------------------------- There are many possible ways to design such a structure. We will use a rather simple model consisting of a periodic arrangement of re-entrant structures [1]. Looking at a single cell, we can already see how the middle arms will straighten under load, pushing them apart. For a structure like this, this hinge-like rotation of the internal arms dominates compared to pure material stretching causing it to expand. To understand the mechanism, let's now look at such a unit cell: Figure 1: Design of the unit cell Next, we can pattern this unit cell to create a larger structure. After extruding it into 3D we end up with fig. 2. Figure 2: 3D Model Of The Full Structure Simulation ---------- To simulate the behavior of the meta material, we will use the finite element method (FEM) with NGSolve. The full code for this example can be found on the [NGSolve usermeeting page](https://ngsolve.github.io/ngs24blitz/SHirnschall/metamaterial_thick.html). We will not go into detail here but provide a brief overview. ### Mesh First we need to mesh the geometry. In this case, we have already created a geometry that will mesh nicely. For the actual 3D print, the outside of corners will get a chamfered edge with radius equal to the nozzle radius. This is to improve the print quality as the printer cannot reproduce sharp outside corners. We do not include these chamfers in the simulation as it would require small elements to resolve the tiny radii (even when curving the mesh). In this example we end up with \(36282\) elements. ### Solution and Limitations For this example we will use a linear elasticity model, and invert the matrix directly. As boundary conditions we will fix the left side of the model and apply a force in \(X\) direction on the right side. The resulting deformation is shown in fig. 3. For larger deformations we see the limitations of this approach. The material starts to bend outwards which is clearly unrealistic. This happens because linear elasticity does not account for large deformations and geometric nonlinearities. For small deformations however, we see the expected "negative Poisson's ratio" behavior. The material expands in the direction perpendicular to the applied force when stretched. As the visualization is interactive, the "Open Controls" in the top right corner can be used to change the deformation amount. Figure 3: Deformation of the auxetic material under load [The NGSolve website also showcases a different material with similar properties using a NeoHooke material and newton for the simulation.](https://docu.ngsolve.org/ngs24/tutorials/03_occ.html#metamaterials) Fig. 4 shows the deformed material next to the undeformed one. We can see how the matial expands vertically when stretched (horizontally). ![Undeformed material](https://blog.hirnschall.net/auxetic-material/resources/img/normal.jpg) (a) Undeformed material ![Stretched material](https://blog.hirnschall.net/auxetic-material/resources/img/stretched.jpg) (b) Stretched material Figure 4: Deformation of the auxetic material under load 3D Printing ----------- Finally, we can 3D print the model and test it out in real life. We will use TPU for this as it is a flexible material that can stretch and show the auxetic behavior. The model was somewhat optimized for 3D printing. As mentioned, outside corners are chamfered to improve tool path generation. Furthermore, the wall thickness was chosen to be \(2x\) the nozzle diameter. This way the printer can print each cell as a closed loop, reducing retractions and oozing, issues we want to avoid when printing soft tpu. Models with thinner walls were also printed from PLA but they did not work that well. The printed poorly and were too stiff. The video below shows the printed model in action. It can be stretched and behaves like we have already seen in the simulation. Practical Uses -------------- Auxetic materials have a wide range of potential applications due to their unique properties. They can be used in areas such as: * **Aerospace and satellites:** Auxetic structures can be used for deployable components. Their ability to expand laterally when stretched makes them useful for compact structures that need to unfold reliably in space. Since the deformation is achieved through bending rather than sliding contact, they also avoid issues like cold welding that can occur in vacuum. * **Medical devices**: Auxetic stents are a well-documented application. Their negative Poisson's ratio means they expand radially when stretched longitudinally, allowing them to anchor securely in blood vessels or airways without requiring a separate expansion mechanism. * **Protective gear**: Auxetic materials can absorb and dissipate energy more effectively than conventional materials, making them ideal for use in protective clothing, helmets, and padding. Conclusion ---------- Overall I really like this project. It covers the design, simulation, and manufacturing of such an auxetic material. The result is a really nice show piece. While this project is intentionally kept simple, let's discuss its limitations for completeness. ### Limitations One limitation of the simple design used is that it's Poisson's ratio is anisotropic, meaning it varies with direction. If we stretch it diagonally for example, it behaves like a conventional material. More complex designs from e.g. topology optimization can help to alleviate this issue. The simulation we used is simple, fast, and shows the behavior of the auxetic material under load. However, as discussed, there are limitations to this approach. --- ## [ESP32 WiFi Provisioning — Captive Portal & NVS Credentials](https://blog.hirnschall.net/esp32-iot-wifi-provisioning/) meta description: To avoid hardcoding credentials, the ESP starts in AP mode and hosts a site where we can enter the WiFi credentials. Once done, it connects to the WiFi. date published: 27.04.2026 (DD.MM.YYYY format) date last modified: 27.04.2026 (DD.MM.YYYY format) --- # [ESP32: WiFi Provisioning with a Captive Portal](https://blog.hirnschall.net/esp32-iot-wifi-provisioning/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: To avoid hardcoding credentials, the ESP starts in AP mode and hosts a site where we can enter the WiFi credentials. Once done, it connects to the WiFi. meta title: ESP32 WiFi Provisioning — Captive Portal & NVS Credentials date published: 27.04.2026 (DD.MM.YYYY format) date last modified: 27.04.2026 (DD.MM.YYYY format) --- Introduction ------------ Most IoT projects need to connect to a WiFi network, but hardcoding the SSID and password in the firmware is inflexible. This post covers a self-contained provisioning pattern where the ESP32 hosts its own access point and a minimal web interface on first boot, allowing WiFi credentials to be entered without modifying or reflashing the firmware. On every subsequent boot the stored credentials are used to connect normally. A hardware reset button clears the stored credentials so the process can be repeated. The full working example code can be found at the end of the post. This post builds on two other posts in the ESP32 reference: [Preferences](https://blog.hirnschall.net/esp32-preferences/) for storing credentials in NVS and [WebServer](https://blog.hirnschall.net/esp32-webserver/) for serving the provisioning page. This post is part of a complete ESP32 reference you can find [here](https://blog.hirnschall.net/esp32/). Overall Flow ------------ On every boot the firmware follows this sequence. First, it checks whether the reset button is held. If it is, it clears the stored credentials from NVS. Then it checks whether credentials exist in NVS. If they do not, it enters provisioning mode. If they do, it connects to WiFi and starts normal operation. In provisioning mode the ESP32 starts its own WiFi access point, runs a DNS server that redirects all traffic to itself, and serves a small HTML form where the user can enter an SSID and password. When the form is submitted the credentials are written to NVS and the ESP32 reboots. On the next boot the credentials exist and the normal connection flow runs instead. If the stored credentials are wrong, for example because the WiFi password changed, the ESP32 will try to connect on every boot but never succeed. Holding the reset button on boot clears the credentials and returns to provisioning mode. If we wanted to we could also clear the stored credentials after a certain number of failed connection attempts, but I usually avoid this in case the WiFi is just temporarily down. Libraries --------- We will use the following three libraries in this project. They are all included in the ESP32 Arduino core, so no additional installation is required. We just need to install the ESP32 core in the boards manager. ``` #include #include #include #include ``` `DNSServer.h` is needed for the captive portal. When a phone connects to the ESP32 access point it sends DNS queries to resolve any hostname. The DNS server responds to all queries with the ESP32's own IP address, which causes the phone's OS to detect a captive portal and automatically open the provisioning page in a browser. `WebServer.h` and `Preferences.h` are used for serving the provisioning page and storing the WiFi credentials, respectively. As mentioned above we have separate, more detailed, posts on both libraries ([WebServer](https://blog.hirnschall.net/esp32-webserver/), [Preferences](https://blog.hirnschall.net/esp32-preferences/)). Configuration ------------- To start things off, we define the access point name, password, reset button pin, and the ESP32's IP address in AP mode at the top of the file. For this example we choose `192.168.4.1` as the IP address and GPIO 0 for the "reset" button. Note that the reset button for the WiFi credentials is not the same as the reset button for the ESP32 itself. ``` const char* AP_SSID = "ESP32-Setup"; const char* AP_PASSWORD = "configure"; // min 8 characters, or "" for open network const int RESET_PIN = 0; // GPIO0 is the BOOT button on most dev boards const IPAddress AP_IP(192, 168, 4, 1); const IPAddress AP_SUBNET(255, 255, 255, 0); ``` The WiFi reset button is connected between the GPIO pin and GND, using the internal pull-up resistor. On most ESP32 development boards, GPIO0 is the BOOT button and is already wired this way, making it a convenient choice. Storing and Loading Credentials ------------------------------- We can use a struct to hold the credentials and store it as a single bytes entry in NVS using the Preferences library, as covered in the [Preferences post](https://blog.hirnschall.net/esp32-preferences/). We will choose a fixed (maximum) size of 64 characters for both the SSID and password fields. Additionally, we create helper functions to load, save, and clear the credentials in NVS, which we will use later. ``` struct Credentials { char ssid[64]; char password[64]; }; Preferences prefs; Credentials credentials; bool loadCredentials() { prefs.begin("wifi", true); bool exists = prefs.getBytesLength("creds") == sizeof(credentials); if (exists) { prefs.getBytes("creds", &credentials, sizeof(credentials)); } prefs.end(); return exists; } void saveCredentials() { prefs.begin("wifi", false); prefs.putBytes("creds", &credentials, sizeof(credentials)); prefs.end(); } void clearCredentials() { prefs.begin("wifi", false); prefs.clear(); prefs.end(); Serial.println("Credentials cleared"); } ``` Provisioning Mode ----------------- In provisioning mode we start the access point, configure the DNS server, and register two routes on the web server. One handles requests for the root "/" and serves an HTML form where we can enter the WiFi credentials. The second handler is for the "Save and Reboot" button shown on the main HTML page. It will send a request to "/save" which the ESP32 uses to store the credentials and reboot. Additionally, we redirect any request to an unknown path to the root handler. This is what triggers the captive portal. The phone's OS probes captive.apple.com, connectivitycheck.gstatic.com, etc.. By redirecting all paths to the root handler, we make sure that the requests made by the phone all return our provisioning page, which causes the captive portal popup to show on the phone. Fig. 1 shows the page that will be displayed when in provisioning mode. The actual HTML for the provisioning page is included in the full example at the end of the post. For readability's sake it is replaced with a placeholder in the listing in this section. ![WiFi Provisioning Page displayed by the ESP32](https://blog.hirnschall.net/esp32-iot-wifi-provisioning/resources/img/login.jpg) Figure 1: WiFi Provisioning Page displayed by the ESP32 Let's now go through the code step by step. `handleRoot` is simple. It returns the provisioning HTML. `handleSave` is more interesting. It checks if an ssid was provided. If not, it returns a 400 error with error message "SSID required". If it was provided, we store the GET argument for both the password and SSID in NVS and reboot. ``` WebServer server(80); DNSServer dns; //PROVISIONING_HTML is shown in the full example at the end of the post const char* PROVISION_HTML = R"(HTML GOES HERE)"; void handleRoot() { server.send(200, "text/html", PROVISION_HTML); } void handleSave() { if (!server.hasArg("ssid") || server.arg("ssid").isEmpty()) { server.send(400, "text/plain", "SSID required"); return; } server.arg("ssid").toCharArray(credentials.ssid, sizeof(credentials.ssid)); server.arg("password").toCharArray(credentials.password, sizeof(credentials.password)); saveCredentials(); server.send(200, "text/html", "

Saved. Rebooting...

"); server.handleClient(); // ensure response is sent before rebooting delay(1000); ESP.restart(); } void startProvisioning() { Serial.println("Starting provisioning mode"); WiFi.mode(WIFI_AP); WiFi.softAPConfig(AP_IP, AP_IP, AP_SUBNET); WiFi.softAP(AP_SSID, AP_PASSWORD); Serial.print("AP IP: "); Serial.println(WiFi.softAPIP()); // redirect all DNS queries to our IP, this triggers the captive portal on phones dns.start(53, "*", AP_IP); server.on("/", handleRoot); server.on("/save", HTTP_POST, handleSave); server.onNotFound(handleRoot); // redirect unknown paths to the form server.begin(); Serial.println("Provisioning server started"); while (true) { dns.processNextRequest(); server.handleClient(); } } ``` The DNS server listens on port 53 and responds to all hostname queries with the AP IP address. Again, `server.onNotFound(handleRoot)` redirects any path the phone's OS requests, such as connectivity check URLs, back to the form, which is what triggers the captive portal popup. Setup and Main Loop ------------------- In `setup` we check the reset button first, then check for stored credentials, and branch accordingly. ``` void setup() { Serial.begin(115200); pinMode(RESET_PIN, INPUT_PULLUP); // hold button on boot to clear credentials if (digitalRead(RESET_PIN) == LOW) { delay(50); // debounce if (digitalRead(RESET_PIN) == LOW) { clearCredentials(); Serial.println("Reset detected. Cleared credentials."); } } if (!loadCredentials()) { startProvisioning(); // does not return } // credentials exist, connect to WiFi Serial.print("Connecting to "); Serial.println(credentials.ssid); WiFi.mode(WIFI_STA); WiFi.begin(credentials.ssid, credentials.password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("Connected. IP: "); Serial.println(WiFi.localIP()); // normal operation starts here } void loop() { // application code } ``` One thing to note is that `startProvisioning` contains an infinite loop and thus never returns. We therefore do not need to set a flag or branch after it. Once the credentials are saved and the ESP32 reboots, `loadCredentials` returns `true` and the provisioning branch is never entered again. The debounce check on the reset button uses a short `delay`. This is one of the places where `delay` is OK as it runs once on boot before any other initialization and does not block anything. Usage ----- On first boot or after a reset, the ESP32 creates a WiFi network named `ESP32-Setup`. We can connect to it from a phone or laptop using the password `configure`. On most phones a browser will open automatically showing the setup form. If it does not, we navigate to `192.168.4.1` manually. We enter the SSID and password of the network we want the ESP32 to join and press Save. The ESP32 reboots and connects to the entered network. To reprovision, we hold the reset button while powering on or pressing the hardware reset. The stored credentials are cleared and the device returns to provisioning mode on the next boot. Full Example ------------ The listing below contains the complete code for the WiFi provisioning example discussed above. ``` #include #include #include #include const char* AP_SSID = "ESP32-Setup"; const char* AP_PASSWORD = "configure"; // min 8 characters, or "" for open network const int RESET_PIN = 0; // GPIO0 is the BOOT button on most dev boards const IPAddress AP_IP(192, 168, 4, 1); const IPAddress AP_SUBNET(255, 255, 255, 0); struct Credentials { char ssid[64]; char password[64]; }; Preferences prefs; Credentials credentials; bool loadCredentials() { prefs.begin("wifi", true); bool exists = prefs.getBytesLength("creds") == sizeof(credentials); if (exists) { prefs.getBytes("creds", &credentials, sizeof(credentials)); } prefs.end(); return exists; } void saveCredentials() { prefs.begin("wifi", false); prefs.putBytes("creds", &credentials, sizeof(credentials)); prefs.end(); } void clearCredentials() { prefs.begin("wifi", false); prefs.clear(); prefs.end(); Serial.println("Credentials cleared"); } WebServer server(80); DNSServer dns; const char* PROVISION_HTML = R"( WiFi Setup

WiFi Setup

)"; void handleRoot() { server.send(200, "text/html", PROVISION_HTML); } void handleSave() { if (!server.hasArg("ssid") || server.arg("ssid").isEmpty()) { server.send(400, "text/plain", "SSID required"); return; } server.arg("ssid").toCharArray(credentials.ssid, sizeof(credentials.ssid)); server.arg("password").toCharArray(credentials.password, sizeof(credentials.password)); saveCredentials(); server.send(200, "text/html", "

Saved. Rebooting...

"); server.handleClient(); // ensure response is sent before rebooting delay(1000); ESP.restart(); } void startProvisioning() { Serial.println("Starting provisioning mode"); WiFi.mode(WIFI_AP); WiFi.softAPConfig(AP_IP, AP_IP, AP_SUBNET); WiFi.softAP(AP_SSID, AP_PASSWORD); Serial.print("AP IP: "); Serial.println(WiFi.softAPIP()); // redirect all DNS queries to our IP, this triggers the captive portal on phones dns.start(53, "*", AP_IP); server.on("/", handleRoot); server.on("/save", HTTP_POST, handleSave); server.onNotFound(handleRoot); // redirect unknown paths to the form server.begin(); Serial.println("Provisioning server started"); while (true) { dns.processNextRequest(); server.handleClient(); } } void setup() { Serial.begin(115200); pinMode(RESET_PIN, INPUT_PULLUP); // hold button on boot to clear credentials if (digitalRead(RESET_PIN) == LOW) { delay(50); // debounce if (digitalRead(RESET_PIN) == LOW) { clearCredentials(); Serial.println("Reset detected. Cleared credentials."); } } if (!loadCredentials()) { startProvisioning(); // does not return } // credentials exist, connect to WiFi Serial.print("Connecting to "); Serial.println(credentials.ssid); WiFi.mode(WIFI_STA); WiFi.begin(credentials.ssid, credentials.password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("Connected. IP: "); Serial.println(WiFi.localIP()); // normal operation starts here } void loop() { // application code } ``` Where & What to Buy ------------------- Below are links1 to the ESP modules and development boards I personally tend to use. If you are unsure about which one to choose, [check out this comparison post!](https://blog.hirnschall.net/esp8266-vs-esp32/) | ESP-12F | NodeMCU ESP8266 | ESP32-WROOM-32 | ESP32-S3 | | --- | --- | --- | --- | | [amazon.com](https://amzn.to/41YJ965) [amazon.de](https://amzn.to/3Olf3GC) | [amazon.com](https://amzn.to/4smbFcB) [amazon.de](https://amzn.to/4eeUhD6) | [amazon.com](https://amzn.to/4tItyDJ) [amazon.de](https://amzn.to/41V075k) | [amazon.com](https://amzn.to/4tEXG2G) [amazon.de](https://amzn.to/4c26zgR) | | #CommissionsEarned | | | | --- ## [Automatic Chopsticks — Arduino Project for Under $10](https://blog.hirnschall.net/automatic-chop-sticks/) meta description: Build motorized chopsticks with Arduino and a single servo. A beginner electronics project with parts list, wiring diagram, and assembly steps. Under $10. date published: 16.11.2024 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [Motorized Automatic Chopsticks (Arduino Controlled)](https://blog.hirnschall.net/automatic-chop-sticks/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Build motorized chopsticks with Arduino and a single servo. A beginner electronics project with parts list, wiring diagram, and assembly steps. Under $10. meta title: Automatic Chopsticks — Arduino Project for Under $10 date published: 16.11.2024 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- Introduction ------------ As a friend of mine is struggeling to eat using chopsticks (even with [one of these helpers](https://amzn.to/4euiYYZ)) I decided to design and build a pair of fully automatic chopsticks for her. Must-have Features ------------------ Although this a simple project, we'll start with a list of must-have features before designing the required parts: ### Arduino compatible Again, we'll keep the project cheap and simple by using an atmega328p with the Arduino bootloader, alowing us to use the Arduino IDE and the Arduino libraries to program the chip. ### Spoon like As most people are quite falimiliar with using a spoon, we will use an old spoon as a handle for our automatic chopsticks. ### Premium feel This is a present and as such should feel high quality. I think using metal chopsticks instead of cheap wooden ones will go a long way in making them look nice. ### Automatic closing and opening Ofcourse, the automatic chopsticks should open and close automatically once food is detected. Design considerations (what is important) ----------------------------------------- ### Distance sensor As we want the chopsticks to close automatically once food is detected we will use a distance sensor to detect the food. Using a VL53L0X should be straight forward with the adafruit arduino library. Furthermore the range of 0mm to 100mm is a perfect fit for our project. ### Servo motor We will use a small 9g servo to move the chopsticks. However, attaching the chopsticks directly to the motor is a bad idea. To see this consider the following situation: In order for the chopsticks to grab different size items, we need the motor to fully close. This leads to the following problem, if a food item prevents the chopsticks from fully closing the motor stalls. If we disable the motor once it stalls the food will fall out. If we do not, the motor will overheat and die. The solution to this problem is using a metal spring as illustrated in fig. 1. below. The motor always moves, and reaches the closed position. The motor is stronger than the spring. Thus, once something blocks the sticks from fully closing the spring is stretched, applying tention and holding the food in place. ![fully closed](https://blog.hirnschall.net/automatic-chop-sticks/resources/img/close-sticks.jpg) (a) fully closed ![fully closed with food](https://blog.hirnschall.net/automatic-chop-sticks/resources/img/half-close-sticks.jpg) (b) fully closed with food Figure 1: Adding a spring to prevent motor stalling when food is blocking the sticks movement ### Simplicity I want this project to be simple, affordable and easy to assemble. Aditionally, as there is not much time left to assemble this present I want to use standard components I already have at hand. No exotic bearings or batteries. ### Battery As this is a handheld device we will have to use a small battery I am thinking about a 9V block battery. However, the microcontroller, the distance sensor, and the servo use 5V. We will have to add a DC-DC voltage regulator to step the 9V down to usable 5V using e.g. an MP1584EN. Please note that the MP1584EN has to be adjusted using a multimeter and the set screw on the MP1584EN before use. ### Electronics (custom PCB) Again, as there is not much time left I will use a piece af perf-board to assemble the electronics. However, it is also possible to order a custom PCB for around $5 online. The schematic can be seen in fig. 2. and in the download section below. ![Schematic for automatic chopsticks](https://blog.hirnschall.net/automatic-chop-sticks/resources/img/schematic.jpg) Figure 2: Schematic for automatic chopsticks ### Programming the atmega328p In order to save space will not include an isp header on the board. Instead we will insert the microcontroller in a spare arduino uno board, programm it and install the programmed chip in the custom board. You can download the code here. ### Handle I wnat to use an old spoon as a handle. I think it is best to cut off the spoon handle, drill a 3mm hole at the end and attach it to the 3d printed parts using an m3x6 screw. Building your own fully automatic chopsticks -------------------------------------------- To build your own automatic chopsticks you will need the following parts: As some components cannot be puchased individualy online you can try to buy them at a local electronics reseller. However, as these are all commonly used parts you can also keep them for later projects. | Component | Quantity | Where to buy | | --- | --- | --- | | [3d printed parts set](https://blog.hirnschall.net/automatic-chop-sticks/) | × 1 | [Printables.com](https://www.printables.com/model/1076247-automatic-chop-sticks-motorized) | | [atmega328p](https://blog.hirnschall.net/automatic-chop-sticks/) | × 1 | * [Amazon.com](https://amzn.to/3Cwy1UE) * [Amazon.de](https://amzn.to/3YRjhaf) | | [9V to 5V DCDC (MP1584EN)](https://blog.hirnschall.net/automatic-chop-sticks/) | × 1 | * [Amazon.com](https://amzn.to/3ZaYYWI) * [Amazon.de](https://amzn.to/3Z9Sh7r) | | [Distance sensor (VL53L0X)](https://blog.hirnschall.net/automatic-chop-sticks/) | × 1 | * [Amazon.com](https://amzn.to/3OdkzHS) * [Amazon.de](https://amzn.to/3ZdFTSP) | | [9V battery](https://blog.hirnschall.net/automatic-chop-sticks/) | × 1 | * [Amazon.com](https://amzn.to/3ZaflTu) * [Amazon.de](https://amzn.to/3UU5AX0) | | [9V battery clip](https://blog.hirnschall.net/automatic-chop-sticks/) | × 1 | * [Amazon.com](https://amzn.to/40RpSnH) * [Amazon.de](https://amzn.to/40XabeG) | | [9g servo](https://blog.hirnschall.net/automatic-chop-sticks/) | × 1 | * [Amazon.com](https://amzn.to/4hL5SJH) * [Amazon.de](https://amzn.to/3CxlfFv) | | #CommissionsEarned | | | ### Cost Although not all parts can be purchased individually, we will only consider the cost of components used in this build. Leftover parts can be used in other projects. ### Assembly Guide Assembly is straightforward. Start by printing the provided parts. While they are printing, we can solder the PCB per the schematic available for download below. As this is a simple circuit, we can use a piece of perf-board or design a custom PCB. I opted for the perf-board due to time restraints. Next, the DCDC (MP1584EN) is attached to the 9V battery using the battery clip to adjust the DCDC output voltage. To do so, connect a multimeter to the DCDC's output and turn the adjustment potentiometer (small screw) on the MP1584EN until the multimeter reads close to 5V. Once this is done, the battery, DCDC, servo, and PCB can be mounted to the 3d printed frame. Now, before attaching the servo arm to the servo, turn on the device and ensure the servo turns when the sensor is triggered. As discussed above, we want to use a spring between the chopsticks and the servo. Use DIN 912 m3 screws to attach the two rotors to the 3d printed base. Take a small pull spring and attach it to the servo arm. Take two strings and tie them to the other end of the spring. With the servo in the "no food detected" position, attach the servo arm to the servo and connect the strings to the rotors. Lastly, glue the four chopsticks mount to the rotors and use a spring from inside a pen to push the chopsticks open. Résumé ------ All in all, I am super excited about this project. The chopsticks work even better than I imagined. --- ## [Gerber to STEP with Copper — Free Tool for EM Simulation](https://blog.hirnschall.net/gerber-to-step/) meta description: PCB STEP exports omit copper traces - unusable for EM simulation. Convert Gerber files to a simulation-ready STEP with full copper geometry (free tool). date published: 21.03.2026 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [Gerber to STEP: Preparing PCBs for FEM Simulation](https://blog.hirnschall.net/gerber-to-step/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: PCB STEP exports omit copper traces - unusable for EM simulation. Convert Gerber files to a simulation-ready STEP with full copper geometry (free tool). meta title: Gerber to STEP with Copper — Free Tool for EM Simulation date published: 21.03.2026 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- Motivation ---------- One of the main difficulties when simulating a PCB using FEM is converting the PCB design into a simulation ready geometry. Especially when using open source tools. When using tools like KiCad the STEP export does not include copper features like traces and pads. While this is perfectly fine for mechanical design, they are required for meshing and simulation. The fabrication data (gerber files) contains the information needed to create the physical PCB, but converting it to a 3D model that is usable for meshing/simulation is non trivial. In this post we will take a look at how we can use free and open source tools to convert gerber files to STEP files that are defeatured and ready for meshing. After the mesh is created we can simulate the PCB (e.g. a patch antenna) to see current flow, reflection coefficients (S-parameters), near and far field patterns, etc. The figure below shows the two PCBs we will work with in this post. On the left, fig. 1a a real square patch antenna and on the right, fig. 1b an antenna in the form of the cerbsim logo. ![Square Patch Antenna](https://blog.hirnschall.net/gerber-to-step/resources/img/square_patch.jpg) (a) Square Patch Antenna ![Logo Patch Antenna](https://blog.hirnschall.net/gerber-to-step/resources/img/cerbsim.jpg) (b) Logo Patch Antenna Figure 1: PCB Designs Workflow -------- To convert from gerber to STEP we will use a free browser based tool I have worked on as a side project at work. It runs locally in the browser so no files are uploaded to any server. We will discuss how it works in the section below. To get the STEP files we can use the following workflow: * Export gerber files from you CAD software (e.g. KiCad). * Export drill files in GerberX2 format (Excellon is not supported). * Create a zip archive containing only the layers needed in the simulation: Copper layers, edge cuts, PTH and NPTH files. Do not include unnecessary, unused or empty files. E.g. If there are no non plated through holes, do not include the NPTH file! * Open the [Gerber-To-STEP webapp](https://app.sim-pl.com/app?appId=4) and sign in (e.g. via GitHub) * "Upload" the zip file containing the gerber files. * If Gerber X2 files are used, layers are assigned automatically. Otherwise, we need to assign them manually. * Verify the preview on the left looks good, then click download in the lower right corner. After downloading the STEP file, we can import it into our meshing or FEM software and start simulating. Fig. 2 shows the resulting geometry on the left (fig. 2a) and the corresponding mesh on the right (fig. 2b). Note that internal layers are also included and meshed. ![STEP](https://blog.hirnschall.net/gerber-to-step/resources/img/step.jpg) (a) STEP ![Mesh](https://blog.hirnschall.net/gerber-to-step/resources/img/mesh.jpg) (b) Mesh Figure 2: Resulting STEP and Mesh ### Common Pitfalls When converting Gerber files to STEP for simulation, a few common issues can lead to failed imports or meshing problems: * **Wrong drill file format** Only Gerber X2 drill files are supported. Excellon files will cause issues during the conversion process. * **Including unnecessary layers** Files such as silkscreen, solder mask, or assembly layers are not needed for simulation and must not be included. Only include copper, edge cuts, and drill files. * **Empty or unused files in the archive** Empty layers (e.g. NPTH with no holes) will cause processing issues or slow down the conversion. Only include files that contain actual data. * **Incorrect layer assignment** If Gerber X2 metadata is missing, layers must be assigned manually. Incorrect assignments (e.g. swapping top and bottom copper) will cause issues. * **Overly complex geometry** Small features such as text, logos, or decorative copper details increase mesh complexity without adding value to the simulation. This can significantly increase step generation- and computation time or even prevent meshing. * **Broken or non-manifold geometry** Poorly defined board outlines or overlapping features can result in invalid STEP geometry. Always verify the preview before exporting. * **DRC errors in the original design** Design rule violations (e.g. overlapping traces, unconnected pads, bad outline) can lead to issues during conversion. Ensure the original PCB design is clean and error-free. How It Works ------------ The gerber to STEP converter is built using python and runs in the browser using the open source [ngapp](https://cerbsim.github.io/ngapp/) framework. Internally [Netgen](https://ngsolve.org/) and [Open Cascade](https://github.com/Open-Cascade-SAS/OCCT) are used to draw the geometry based on the gerber files. To eliminate small edges that stem from rounding errors in the gerber files splines are fitted and small adjacent edges are merged. Subsequently all overlapping faces are unified and a STEP file suitable for simulation is created. The web app itself runs locally in the browser so no files are uploaded or shared. The downside to this is that running a full python environment in the browser is slower than running it natively. Fig. 3 shows the web app after loading a design. In the top left a 2D preview of the selected layer is displayed. Below it a 3D preview of the whole PCB. On the right the detected and assigned layers are listed. ![Gerber to STEP Web App Preview](https://blog.hirnschall.net/gerber-to-step/resources/img/webapp.jpg) Figure 3: Gerber to STEP Web App Preview Loading the files should not take long. If the progress bar stalls, make sure you have only uploaded the files discussed above. Drill files must be in GerberX2 format, no "empty" files, no silkscreen etc. For simulation purposes it is a good idea to simplify the geometry where possible (defeaturing). This means no text in the copper layer because it looks cool! Try It ------ If you want to try it out, head over to the [Gerber-To-STEP webapp](https://app.sim-pl.com/app?appId=4) and sign in via e.g. GitHub. The example files used in this post can be found in the download section below. Conclusion ---------- Working directly with the fabrication data, we can rebuild the full PCB geometry including traces, pads, and internal layers and make it usable for meshing without a lot of manual cleanup. Using the Gerber to STEP web app makes it easy to locally convert Gerber files to STEP format for simulation. All features relevant for FEM are included while small edges are removed. We are therefore not tied to a specific toolchain. You can use whatever PCB design software you prefer and combine it with both open-source and commercial simulation tools. --- ## [Type-Safe CAN Layer in C++ — No Bit Packing Bugs](https://blog.hirnschall.net/can-transport-layer/) meta description: Eliminate manual bit packing errors in CAN with compile-time validation and code generation. Full C++ implementation with filter and routing safety built in. date published: 01.03.2026 (DD.MM.YYYY format) date last modified: (DD.MM.YYYY format) --- # [A Type-Safe CAN Transport Layer with Code Generation](https://blog.hirnschall.net/can-transport-layer/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Eliminate manual bit packing errors in CAN with compile-time validation and code generation. Full C++ implementation with filter and routing safety built in. meta title: Type-Safe CAN Layer in C++ — No Bit Packing Bugs date published: 01.03.2026 (DD.MM.YYYY format) date last modified: (DD.MM.YYYY format) --- Motivation ---------- If you have ever spent an afternoon chasing a CAN bug that turned out to be a wrong bit offset or a missing filter, you know how frustrating and time consuming it can be. The code compiles, the system runs, and yet the behavior is wrong in ways that are difficult to track. The main problem is that working with CAN at a low level forces the user to manually manage details that are tedious, repetitive and error prone. Constructing messages requires bit packing into raw data buffers. Non standard field sizes such as 3 bit integers require manual masking and type casting. Filters must be configured correctly or messages will silently disappear. At the same time there is nothing preventing a node from sending or receiving messages it should never touch. These issues are most often not really obvious when reading the code. The implementation looks fine, which makes debugging hard. Problems only show up during system/integration testing or on real hardware, where observability is limited and iteration is slow. As the system grows, this approach does not scale. Adding or modifying a message requires updating multiple nodes and keeping them in sync. A small inconsistency between two nodes can lead to failures that are hard to detect and hard to reproduce. This post presents a different approach. Instead of treating these problems as runtime concerns, we move them into compile time. By generating a type safe transport layer from a single CAN map, entire classes of bugs such as incorrect packing, invalid message usage, and missing handlers can be eliminated before the code ever runs. The full implementation is available on [GitHub](https://github.com/shirnschall/can-transport-layer-modm). Solution -------- The solution to this problem is to introduce a single source of truth for the entire CAN system: a CAN map. This map defines all messages, their structure, and which nodes are allowed to send and receive them. From this CAN map file, the complete transport layer is auto generated. The generated transport layer sits between the application layer (user code) and the hardware abstraction layer (CAN driver). Its purpose is to remove all manual handling of message layout, filtering, and access control, replacing it with a type safe interface that is enforced at compile time. For this to work well we have the following requirements for the generated code: * Messages as plain C++ structs using standard types. The user should be able to work with them like any other data structure, without thinking about bit offsets or packing rules. * Serialization and deserialization must be handled automatically. Converting between structured data and raw CAN frames should be correct by construction and require no user involvement. * Ensure at compile time that a node is only able to send messages it is defined as a sender for, and only receive messages it is subscribed to. Any violation should result in a compilation error rather than undefined behavior at runtime. * Message handling should also be enforced. For every message a node subscribes to, a corresponding handler function must exist. If a handler is missing, the code should fail to compile. * All in all, every issue mentioned above should be a compile-time error. From the user's perspective, interaction with the system becomes straightforward. Send: To send a message, the user creates an instance of a generated message struct, assigns values to its fields, and passes it to a send function. The transport layer takes care of serialization and transmission. Receive: To receive messages, the user implements handler functions for the messages they are subscribed to. Incoming frames are automatically deserialized and dispatched to the correct handler. The application interacts only with typed data, never with raw byte buffers. The result is a system where correctness is enforced by the type system, and where the complexity of CAN communication is contained entirely within generated code. Hard to debug runtime issues become compile-time errors. ### Architecture To understand where the transport layer fits into the system, it helps to look at the overall software architecture. At the top sits the application layer. This is where the user implements the actual logic of the system. It operates purely on typed data structures and does not deal with CAN specifics such as message IDs, byte layouts, or filters. Below the application layer is the transport layer. It is responsible for converting between the application level data structures and the underlying CAN representation. On one side, it exposes a clean and type safe interface to the application. On the other side, it interacts with the CAN driver using raw frames. The transport layer handles several responsibilities. It serializes structured data into CAN frames and deserializes incoming frames back into typed messages. It enforces which messages a node is allowed to send or receive. It also configures hardware filters so that only relevant messages reach the software layer. Below the transport layer sits the hardware abstraction layer which provides access to the CAN peripheral and related hardware features. The transport layer relies on this layer for transmitting and receiving frames but does not depend on its internal implementation. At the bottom is the CAN bus itself, which connects all nodes in the system. The structure described above is shown in fig. 1 below. It shows how the application layer interacts exclusively (for CAN) with the generated transport layer, which in turn interfaces with the hardware abstraction layer and the CAN bus. ![CAN transport layer architecture diagramm](https://blog.hirnschall.net/can-transport-layer/resources/img/architecture.svg) Figure 1: Software Architecture CAN-Map ------- At the core of this approach is the CAN map. It defines the complete communication contract of the system in a single place. Every message, its structure, and the responsibilities of each node are described here, making it the single source from which all code is generated. Implementing it is not hard as long as the structure is well-defined. For this project I decided to go with yaml but we could also switch to a database as the project gets more complex. The key points the CAN-map has to include are * CAN bus name * CAN-map version * List of nodes on the bus * Who is allowed to send each message (namespaces) * Who is allowed to receive each message (subscribers) * Message name * Message ID * Message data length (dlc) * Fields including type and size By explicitly listing senders and subscribers, the CAN map defines which nodes are allowed to interact with each message. This information is later used to enforce access restrictions at compile time. A minimal example is shown below. In this configuration, the SensorNode publishes two messages while the ECU subscribes to them. From this single description, the generator can derive message types, serialization logic, node specific interfaces, and hardware filter configuration. ``` can: can0 version: 1 nodes: - ECU - SensorNode - Diagnosis namespaces: - name: SensorNode messages: - name: StrainData id: 0x201 dlc: 7 subscribers: - ECU fields: - { name: fx, type: int32, bits: 16 } - { name: fy, type: int32, bits: 16 } - { name: fz, type: int32, bits: 16 } - { name: valid, type: uint8, bits: 1 } - name: Temperature id: 0x202 dlc: 2 subscribers: - ECU fields: - { name: value, type: float, bits: 16, scale: 0.1 } ``` Once the CAN map is defined, there is no need to manually keep different parts of the system in sync. Any change to a message or node configuration is made in one place and updated automatically through code generation. Message Structs --------------- The message structs themselves are also not complicated. The CAN-map explicitly states all the information we need. Each message becomes a struct containing its fields, along with static metadata such as the message identifier and data length. The message names are included for debugging. What we want to achieve here is that the user sees CAN messages as plain C++ structs. From his perspective, a CAN message now behaves like any other data structure. Fields can be read and written directly, without worrying about offsets, masks, or casting. Instead of constructing raw byte arrays, the user creates and manipulates strongly typed objects, and the system ensures that they are transmitted correctly. Below are the generated structs for the example above. ``` namespace can0 { enum class Node : uint8_t { ECU, SensorNode, Diagnosis, }; namespace SensorNode { struct StrainData { static constexpr uint16_t id = 513; static constexpr uint8_t dlc = 7; #ifdef CANMAP_INCLUDE_MESSAGE_NAMES static constexpr const char* name = "SensorNode::StrainData"; #endif int32_t fx; // packed: 16 bits int32_t fy; // packed: 16 bits int32_t fz; // packed: 16 bits uint8_t valid; // packed: 1 bits }; struct Temperature { static constexpr uint16_t id = 514; static constexpr uint8_t dlc = 2; #ifdef CANMAP_INCLUDE_MESSAGE_NAMES static constexpr const char* name = "SensorNode::Temperature"; #endif float value; // packed: 16 bits static constexpr float value_scale = 0.1f; }; } // namespace SensorNode } // namespace can0 ``` As we can see, each CAN-bus has its own namespace and so does each node. Now that we have defined message structs, we need to ensure only these structs can be used when working with the transport layer. If any other type is used we want the compiler to throw an error. We can achieve this by using C++ 20 concepts. First, we register each generated struct such that CanMessage is true. For any other type CanMessage will be false. The generated functions can then use this concept to restrict their input to valid can messages. Using a wrong type will result in a compile-time error. ``` template struct IsCanMessage : std::false_type {}; template concept CanMessage = IsCanMessage::value; template<> struct IsCanMessage : std::true_type {}; template<> struct IsCanMessage : std::true_type {}; ``` Next, we define a list of types and a way to check if a type is contained in that list. This is done using C++ template parameter packs. **TypeList** is a list of types and **TypeListContains** checks if a type is contained in a TypeList. Both are evaluated at compile time. Building on this, we define the allowed Rx and Tx messages of each node. For every node, the CAN map specifies which messages it is allowed to transmit and which it can receive. This information is encoded into two TypeLists contained in the **NodeTraits** struct. At this point we can also start to add optional messages like **Diagnosis::Request** and **Diagnosis::DebugResponse** which add diagnosis functionality to the system without any user input. This diagnosis can be used for many things like checking compatible versions or built in git hashes (using CMake) to get the current software version. ``` template struct TypeList {}; template struct TypeListContains; template struct TypeListContains, T> : std::false_type {}; template struct TypeListContains, T> : std::conditional_t< std::is_same_v, std::true_type, TypeListContains, T>> {}; template struct NodeTraits; template<> struct NodeTraits { using TxMessages = TypeList< >; using RxMessages = TypeList< SensorNode::StrainData, SensorNode::Temperature, Diagnosis::Request, Diagnosis::DebugResponse >; }; template<> struct NodeTraits { using TxMessages = TypeList< SensorNode::StrainData, SensorNode::Temperature >; using RxMessages = TypeList< Diagnosis::Request, Diagnosis::DebugResponse >; }; ``` The important detail here and also the reason this code looks unnecessarily complex is that everything is evaluated at compile time using templates. This is important as it prevents runtime/logic errors in the code. Serialization and Bit Packing ----------------------------- Next, let's look at bit packing and serialization. What we need to do, is convert the message structs c++ type members into the message frames 8 bytes according to the CAN-map specification. Again, we will do so using templates. As we have these templates specialized for each message type, we can ensure that the serialization is done correctly at compile time. We will therefore just take a look at the implementation of one message type. ``` template inline void serialize(const Message& ,uint8_t*); template inline void deserialize(Message& , const uint8_t*); template<> inline void serialize(const can0::SensorNode::StrainData& msg, uint8_t* data) { std::memset(data, 0, can0::SensorNode::StrainData::dlc); packSigned<0,16>(data, msg.fx); packSigned<16,16>(data, msg.fy); packSigned<32,16>(data, msg.fz); pack<48,1>(data, msg.valid); } template<> inline void deserialize(can0::SensorNode::StrainData& msg, const uint8_t* data) { msg.fx = unpackSigned<0,16>(data); msg.fy = unpackSigned<16,16>(data); msg.fz = unpackSigned<32,16>(data); msg.valid = unpack<48,1>(data); } ``` The **pack**/**unpack** functions are done using templates to keep timing predictable. Nothing CAN-specific happens here, we just mask and shift the values into the correct byte positions. If you have worked with CAN before you will probably have written something similar. They are not auto-generated and you can swap in your own implementation without touching anything else. The signed variants are omitted here but can be found on [GitHub](https://github.com/shirnschall/can-transport-layer-modm). ``` template inline void pack(uint8_t* data, uint32_t value) { static_assert(Width > 0 && Width <= 32, "Width must be between 1 and 32"); constexpr uint32_t mask = (Width == 32) ? 0xFFFFFFFFu : ((1u << Width) - 1u); value &= mask; constexpr size_t byteOffset = Offset / 8; constexpr size_t bitOffset = Offset % 8; uint64_t tmp = 0; std::memcpy(&tmp, data + byteOffset, sizeof(uint64_t)); tmp &= ~(static_cast(mask) << bitOffset); tmp |= (static_cast(value) << bitOffset); std::memcpy(data + byteOffset, &tmp, sizeof(uint64_t)); } template inline uint32_t unpack(const uint8_t* data) { static_assert(Width > 0 && Width <= 32, "Width must be between 1 and 32"); constexpr uint32_t mask = (Width == 32) ? 0xFFFFFFFFu : ((1u << Width) - 1u); constexpr size_t byteOffset = Offset / 8; constexpr size_t bitOffset = Offset % 8; uint64_t tmp = 0; std::memcpy(&tmp, data + byteOffset, sizeof(uint64_t)); return static_cast((tmp >> bitOffset) & mask); } ``` CAN Manager Base Class ---------------------- Now that we have all the necessary functions implemented, we can create a CAN Manager base class. This class will act as a base class for managing CAN messages. Each node will get its own CAN Manager that inherits from this base class. This is where CRTP (Curiously Recurring Template Pattern) comes in. The base class takes the derived class as a template parameter, allowing it to cast itself to the derived type and call the correct message handler directly. No virtual functions, no vtable lookups, everything resolved at compile time. The key benefit here is that a missing handler becomes a compiler error rather than a silent fallback to a base class default. The base class is built on top of modm, an open-source C++ Hardware Abstraction Layer and driver library for embedded targets (like e.g. stm). modm abstracts hardware peripherals like FDCAN, GPIO, and timers behind consistent C++ interfaces, which is what the FdcanPeriph, RxPin, and TxPin template parameters refer to throughout this section. If you're using a different HAL, the base class is the only thing that needs adapting; the generator, the CAN map, and all application code stay the same. Note that this implementation has comments and some modm specific code removed for clarity and readability. The full code is on [GitHub](https://github.com/shirnschall/can-transport-layer-modm). Let's start with the template parameter list, the constructor, destructor, copy constructor, assignment operator, and the hardware initialization. As mentioned we will use CRTP so the last template argument is the derived class. The constructor initializes the FDCAN device from modm and configures the filters. One interesting aspect is **instance**. It is used as the modm CAN error callback only works for static instances. ``` template< Node ThisNode, typename FdcanPeriph, typename RxPin, typename TxPin, uint32_t NominalBitrate, uint32_t DataBitrate, modm::percent_t Tolerance, typename CanManagerDerived // For CRTP, not used in base class > class CanManagerBase{ public: CanManagerBase(uint32_t interruptPriority = INTERRUPT_PRIORITY_DEFAULT, typename FdcanPeriph::Mode startupMode = FdcanPeriph::Mode::Normal, bool overwriteOnOverrun = OVERWRITE_ON_OVERRUN_DEFAULT){ instance() = this; // Register instance for static callback access initHardware(interruptPriority, startupMode, overwriteOnOverrun); configureFilters(); } ~CanManagerBase(){ // Unregister on destruction if (instance() == this) { instance() = nullptr; } } //Prevent copying (only one instance per template instantiation) CanManagerBase(const CanManagerBase&) = delete; CanManagerBase& operator=(const CanManagerBase&) = delete; void initHardware(uint32_t interruptPriority, typename FdcanPeriph::Mode startupMode, bool overwriteOnOverrun){ // Connect GPIO pins to FDCAN peripheral // Initialize FDCAN peripheral with template parameters // Enable CAN-FD mode // Enable automatic retransmission //set callback for error handling, // use static function to call member function since modm requires a function pointer FdcanPeriph::setErrorCallback(&CanManagerBase::errorCallbackStatic); } ``` ### Send **send** checks the node traits template defined earlier to see if ThisNode is a valid sender for the message that we want to send. It does so using the TypeListContains template. If CANMAP\_INCLUDE\_MESSAGE\_NAMES is defined, we can raise a more descriptive error message. Using static\_assert ensures we get a compile-time error when we try to send a message that is not listed in the node traits for ThisNode. The transmit function itself tries to send the message with retry counter if the CAN device is ready. ``` template void send(const Message& msg) { #if defined(CANMAP_INCLUDE_MESSAGE_NAMES) static_assert( TypeListContains< typename NodeTraits::TxMessages, Message>::value, "This node is not allowed to transmit " + std::string(Message::name)); #else static_assert( TypeListContains< typename NodeTraits::TxMessages, Message>::value, "This node is not allowed to transmit this message"); #endif modm::can::Message frame( static_cast(Message::id), Message::dlc); //configure for e.g. CAN-FD serialize(msg, frame.data); transmit(frame); } void transmit(const modm::can::Message& frame){ uint8_t retry_count = 0; while (retry_count < CAN_RETRY_CNT_DEFAULT){ // if CAN is ready, send with retry counter modm::delay_us(1); ++retry_count; } // Failed after all retries ++error_counters_.tx_timeout; } ``` ### Receive To receive messages the user calls **processMessages**. If a message is available on the CAN, **handleFrame** is called which in turn calls **dispatch** with the frame and the current nodes valid Rx messages as a TypeList. **dispatch** iterates through the TypeList of valid Rx messages for this node (resolved at compile time, dispatched at runtime). If the received message ID matches, it deserializes the frame into the correct struct and calls the **onMessage** handler in the derived class via CRTP. Since the CAN frame is now a message struct, **onMessage** can use the struct type as a template parameter. If the current node is not subscribed to the received message's ID, it will call the **onUnknown** implementation. ``` uint8_t processMessages(size_t maxNMessages = MAX_NUMBER_OF_MSG_TO_PROCESS_DEFAULT) { uint8_t processed = 0; modm::can::Message rx_msg; // Read messages from FIFO // call handleFrame(rx_msg) for each message // increment processed return processed; } void handleFrame(const modm::can::Message& frame) { dispatch(typename NodeTraits::RxMessages{}, frame); } template void dispatch(TypeList, const modm::can::Message& frame){ if (frame.getIdentifier() == Head::id) { Head msg{}; deserialize(msg, frame.data); //use CRTP to call the derived class's onMessage specialization static_cast(this)->onMessage(msg); return; } if constexpr (sizeof...(Tail) > 0) { dispatch(TypeList{}, frame); } else { onUnknown(frame); } } template void onMessage(const Message& msg); //needs to be implemented by user. specialization on a per message level is advised. compile time error if not implemented. void onUnknown(const modm::can::Message& frame){ ++error_counters_.unknown_id; (void)frame; } ``` ### Filters CAN controllers use hardware filters to decide which message IDs are passed up to software and which are discarded at the peripheral level. Without them, every node would receive every message on the bus and have to sort it out in software, something that we want to avoid as it can be resource-intensive. Configuring them manually is a common source of bugs: a missing filter means a subscribed message is silently dropped, while an overly broad filter lets in messages the node has no handler for. Here the filters are derived automatically from the node's RxMessages list, so they are always consistent with what the node is actually subscribed to. We use a TypeList to call **configureSingleFilter** iteratively for each message ID. ``` void configureFilters() { filter_index_ = 0; configureFilterList( typename NodeTraits::RxMessages{} ); } // Base case: empty list template void configureFilterList(List) { // No messages to filter } // Recursive case: process head, then tail template void configureFilterList(TypeList) { configureSingleFilter(Head::id); if constexpr (sizeof...(Tail) > 0) { configureFilterList(TypeList{}); } } void configureSingleFilter(uint16_t id) { // Configure FDCAN standard ID filter // All messages go to FIFO0 with exact match (0x7FF mask) FdcanPeriph::setStandardFilter( filter_index_++, FdcanPeriph::FilterConfig::Fifo0, modm::can::StandardIdentifier(id), modm::can::StandardMask(0x7FF) // Exact match ); } uint8_t filter_index_{0}; ``` ### Error Handling and Diagnostics The rest of the code is error handling/counting, diagnostics, and several helper functions to access the FDCAN device's member functions/variables. The user can also provide an error callback that is called whenever a CAN error occurs. This is optional but can be useful for debugging. This code is modm specific and can be adapted or extended as needed. ``` struct ErrorCounters { uint32_t rx_overrun{0}; uint32_t tx_timeout{0}; uint32_t unknown_id{0}; uint32_t error_warning{0}; uint32_t error_passive{0}; uint32_t bus_off{0}; }; // Static callback function - unique per template instantiation static void errorCallbackStatic() { if (instance()) { instance()->handleModmCanErrorCallback(); } } static CanManagerBase*& instance(){ static CanManagerBase* inst = nullptr; return inst; } const ErrorCounters& getErrorCounters() const { return error_counters_; } void resetErrorCounters() { error_counters_ = {}; } bool isMessageAvailable() const{ return FdcanPeriph::isMessageAvailable(); } inline typename FdcanPeriph::BusState getMODMBusState() const{ return FdcanPeriph::getBusState(); } inline uint8_t getMODMTransmitErrorCounter() const{ return FdcanPeriph::getTxErrorCounter(); } inline uint8_t getMODMReceiveErrorCounter() const{ return FdcanPeriph::getRxErrorCounter(); } void handleModmCanErrorCallback() { // get error state from modm, increment ErrorCounters // add call to user-defined callback if needed } ErrorCounters error_counters_{}; }; ``` Node CAN Manager ---------------- Each node gets its own CAN manager. This way the user can simply use e.g. "can0::ECU" when programming the ECU. So, let's take a look at how this works for the ECU as an example. Again, comments are removed for clarity and readability. ``` namespace can0 { template class ECU : public CanManagerBase< Node::ECU, FdcanPeriph, RxPin, TxPin, NominalBitrate, DataBitrate, Tolerance, ECU // CRTP > { public: using Base = CanManagerBase< Node::ECU, FdcanPeriph, RxPin, TxPin, NominalBitrate, DataBitrate, Tolerance, ECU >; ECU(AppState& app_state, uint32_t interruptPriority = INTERRUPT_PRIORITY_DEFAULT) : Base(interruptPriority), app_state_(app_state){} // Message handlers - implement these in your .cpp file void onMessage(const SensorNode::StrainData& msg); void onMessage(const SensorNode::Temperature& msg); void onMessage(const Diagnosis::Request& msg); void onMessage(const Diagnosis::DebugResponse& msg); private: AppState& app_state_; }; } // namespace can0 ``` This code is much simpler than the base class. Its only job is to provide type-safe message handling for the ECU node. For this we declare the message handlers for each message type the Node can receive. If the user does not implement a handler for a particular message type, the compiler will generate an error. This way the user cannot forget to handle a message the Node is supposed to handle. The class has several modm specific template parameters that allow for hardware initialization. They could be removed if not needed. The node CAN manager also stores a pointer to the AppState. This way we can access and modify the application state within its message handlers and access the results from inside the main loop (also through the AppState pointer). All in all, the user just needs to instantiate a node CAN manager for the node he is currently programming and implement the necessary message handlers. It is not possible to receive a message the node is not subscribed to. What the User Actually Does --------------------------- We now give a short example of how the user can use the CAN transport layer. First, define the application state and instantiate the CAN manager for this node: ``` // AppState holds the live application state the handlers need to read from and write to struct AppState { uint32_t strain_event_counter{0}; float temperature_threshold{80.0f}; // set from main loop bool overtemperature{false}; }; AppState app_state; can0::ECU can_manager(app_state); ``` Next, the user implements the message handlers for each message the ECU is subscribed to. Here he can work with the message struct as discussed, using msg.value or msg.valid without caring about packing, bit sizes, etc. ``` void can0::ECU::onMessage(const can0::SensorNode::StrainData& msg) { if (!msg.valid) return; // handler does real work, uses and updates app state as needed ++app_state_.strain_event_counter; } void can0::ECU::onMessage(const can0::SensorNode::Temperature& msg) { // check against a threshold that may have been updated from the main loop app_state_.overtemperature = msg.value > app_state_.temperature_threshold; } ``` Finally, the user calls processMessages() in the main loop to process incoming messages and access the updated app state. ``` while (true) { can_manager.processMessages(); if (app_state.overtemperature) { // react - this flag was set from inside onMessage } // main loop can also write into app_state, handlers will see the updated values app_state.temperature_threshold = compute_threshold(); } ``` If the user forgets to implement a message handler, the compiler will catch it! Code Generation --------------- The generator itself is a straightforward Python script that reads the YAML CAN-map and outputs the C++ headers described above. It is not particularly interesting on its own. The interesting thing is what it produces, not how it produces it. What matters is that the CAN-map is the single source of truth. Any time a message is added, removed, or modified, regenerating the headers guarantees that every node's message structs, serialization, node traits, and hardware filters are all updated consistently. There is no manual step where one node gets updated and another does not. The generator also validates the CAN-map before generating any code - catching issues like DLC mismatches, unknown subscribers, or missing fields before they ever reach the compiler. Besides the C++ headers, the generator also produces a DBC file for each CAN bus, which can be loaded directly into tools like SavvyCAN for monitoring and debugging on the PC side. The full generator and all generated output for the example above can be found on [GitHub](https://github.com/shirnschall/can-transport-layer-modm). Conclusion ---------- What we end up with is a CAN transport layer where wrong message IDs, incorrect packing, missing handlers, and misconfigured filters are all compile-time errors rather than runtime surprises. The YAML file is the single source of truth. Add a message, regenerate, and every node is updated consistently. The main tradeoff worth being aware of is that the heavy use of templates increases compile times, and the generated code is tied to the modm HAL in its current form. Porting to a different HAL requires adapting the base class but not the generator or the application code. Other than that, it is hard to argue that manually managing all of this is preferable. The generated code does strictly more, with fewer opportunities for mistakes. --- ## [Custom 4-Port SATA Backplane — Design, Simulate, Build](https://blog.hirnschall.net/4x-sata-backplane/) meta description: Design, simulate, and build your own 4-drive SATA backplane PCB. Differential pairs, S-parameters, 3D FEM analysis, and full GitHub files for JLCPCB date published: 29.03.2026 (DD.MM.YYYY format) date last modified: 26.04.2026 (DD.MM.YYYY format) --- # [Design, Simulate (3D FEM), and Build a 4x SATA Backplane](https://blog.hirnschall.net/4x-sata-backplane/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Design, simulate, and build your own 4-drive SATA backplane PCB. Differential pairs, S-parameters, 3D FEM analysis, and full GitHub files for JLCPCB meta title: Custom 4-Port SATA Backplane — Design, Simulate, Build date published: 29.03.2026 (DD.MM.YYYY format) date last modified: 26.04.2026 (DD.MM.YYYY format) --- Motivation ---------- When trying to build a truly silent home server, choosing a backplane is hard. They are typically either built into a case which does not isolate vibrations and sound the way they should or they are way too expensive and hard to find. So, let's design our own, custom 4x SATA backplane. The difficult part is designing the PCB itself. Once this is done it will be extremely cheap and also not hard to solder. As the design is already done, you can download the gerber files (fabrication data) for the backplane on [GitHub](https://github.com/Hirnschall-Technologies/4x-sata-backplane) and order it on [JLCPCB](https://jlcpcb.com/). The exact order details can be found on github as well. In this post we will go over each design decision, signal integrity, electromagnetic interference, and manufacturing considerations. Fig. 1 shows the assembled backplane. Note that the red wires in the back are a prototype issue which is fixed in the current, released version. ![Assembled 4x SATA Backplane](https://blog.hirnschall.net/4x-sata-backplane/resources/img/assembled.jpg) Figure 1: Assembled 4x SATA Backplane Requirements ------------ Before we get to the actual PCB design, let's write down a list with features we want. As the project is quite simple, we will provide the list without too much discussion. * **4x SATA ports**: While I would prefer a larger backplane, 4 ports is sort of a sweet spot. The molex connector can support the power requirements of 4 drives. Furthermore, the 4x backplane is much smaller and thus cheaper to manufacture and as the minimum order quantity is 5, we can just order 2 backplanes for a total of 8 drives which is more than enough for a home server. Five 4x backplane pcbs cost around 25$. * **Single molex connector to power the drives**: The molex connector is rated for \(8A\) on the 12V rail. As one SATA drive has a maximum power consumption of around \(1.75A\) at 12V, four drives draw up to \(7A\) [1]. Thus we should use no more than 4 drives on one molex connector. * **Drive activity indicator**: Each SATA port should have an activity indicator LED. We will have to check the standard to see how to implement this. * **Staggered spin-up support**: A typical SATA drive will draw more current when it spins up the disks than in normal operation. To avoid a large current when spinning up all drives at once, the pc tries to spin up the drives one after another. The backplane must not interfere with this process. Again, we will have to check the standard to see how to implement this. We will go into more detail on how to implement pin 11 of the SATA power connector to support this feature in the section below. * **Vibration damping for the drives**: The backplane will need some sort of cage to hold the drives in place and prevent the connectors from breaking. We have to implement some sort of vibration damping on either a per drive basis or for the whole backplane. This is purely for noise reduction as NAS drives themselves are rated for operation without damping in an enclosure with many other drives (depends on exact drive used). * **Ability to mount a fan for cooling**: It would be nice to be able to mount a 120mm fan to the cage directly. The most important requirement, however, is that the backplane does not increase error rates. Design Considerations --------------------- Now that we have a list of requirements, we can start talking about design considerations. As SATA is "high speed", we have to keep an eye on signal integrity and electromagnetic interference. ### Frequency and Wavelength SATA III operates at a data rate of 6 Gbps, which corresponds to a frequency of 3 GHz. However, the frequency we need to design for is not 3 GHz! The actual frequency lies in the rise and fall times of the signals. We see this when using fourier analysis for the rectangular waveform. A fast rise and fall time results in higher frequency components, which can lead to electromagnetic interference and signal integrity issues. The highest frequency component of the signal we will consider is called the knee frequency and given by $$\begin{align} f\_{knee} := \frac{0.35}{t\_r} \end{align}$$ where \(t\_r\) is the rise time of the signal (\(20\%\text{ to }80\%\)). For SATA III, the rise time is typically around 50 ps, which gives us a knee frequency of around 7 GHz [2]. This means that we need to design our backplane to handle frequencies up to 7 GHz to ensure signal integrity and minimize electromagnetic interference. For fr4 with \(\varepsilon\_r = 4.4\), this leads to a wavelength $$\begin{align} \lambda = \frac{c}{f\_{knee} \sqrt{\varepsilon\_r}} \approx 0.02 m \end{align}$$ where \(c\) is the speed of light. As a rule of thumb, the trace will start to behave like a transmission line when its length exceeds \(\lambda/10=0.002 m\), which it clearly will. ### Differential Signaling SATA uses differential signaling to improve signal integrity and reduce electromagnetic interference. Here two wires carry the signal with opposite polarity (simplified: one wire carries the signal, the other carries the inverted signal). This works in the following way: The receiver looks at the difference between the two signals, which will cancel out any noise that is common to both signals. So, if a electromagnetic wave induces a voltage on both traces (which it should as we route them next to each other), the receiver will compute the difference between the two signals, which will cancel out the common mode noise and allow the receiver to correctly interpret the original signal. For this to work, we will route the traces as **differential pairs**. The ECAD software typically has a tool to do exactly this. Note: There are several discussions about the necessity to actually route the differential pair next to each other. We will not add to this here and go with the standard approach of routing them next to each other. **In this design:** Primary concern. ### Delay (length) Matching As discussed above, the idea is to cancel out interference by computing the difference between the two signals. However, if the signals have a different propagation delay, this can lead to timing issues and reduced signal integrity. Consider the following example: We emit a signal on both traces. Then the pair has a corner. Obviously, the signal will take longer to propagate on the outer (longer) trace than the inner one. We now have a timing mismatch. The two signals do not arrive at the same time. If the receiver now computes the difference between the two signals, they do not align properly, causing issues with signal integrity. To mitigate this issue we have to make sure the signal takes the same amount of time to propagate on both traces. This is often called length matching, as the two traces are made to have the same physical length. Length is often used as a practical proxy for propagation delay. Strictly speaking, however, we must match the actual propagation delay, not just the physical length. Continuing with the example shows exactly where delay matching must be applied and what goes wrong if we apply it at the wrong spot. If a voltage is induced on both traces after the first corner, the receiver will still cancel out the common mode noise. However, if we now match the delay before the receiver to realign the signals, we also realign the noise (together with the signal) such that it no longer arrives at the receiver at the same time on both traces. The noise is thus now no longer common mode and it is therefore not canceled out by the difference computation. To avoid this issue, we must match length/delay close to the point where the mismatch occurs. The sata standard specifies this **intra-pair skew** to be \(\leq 10 ps\) [3]. **In this design:** Primary concern. ### Propagation Delay Another thing to mention is the propagation delay, which is the time it takes for a signal to travel along a trace. The reason why length matching is technically not correct is that the signals may travel at different speeds through the two traces. The easiest way for this to happen is if we were to route one trace on an internal and one on an external layer (which we are obviously not doing). However, this is not the only reason the signal might travel at different speeds. Consider the way a PCB is actually built. We have copper layers and inbetween glass fiber prepreg layers. The prepreg is a composite made from woven glass fibers and resin. Depending on if the trace is above glass fiber or above resin will affect the propagation speed. The weave is typically oriented in \(0^\circ\) and \(90^\circ\). So, if we route our pair horizontally, One trace could be above a fiber strand and the other above resin. To combat this issue, we may align the layout to \(45^\circ\) and route the traces at \(\pm 45^\circ\). This way the differential pair is on average equally above both materials. Another possibility is to rotate the whole PCB in CAD by around \(10^\circ\). We have mentioned this for completeness sake but we will not consider this in this specific PCB design for SATA. **In this design:** Not critical. ### Crosstalk Crosstalk is the unwanted coupling of signals between adjacent traces. If we route two traces (not differential pair) next to each other, the signal in one can couple into the other, causing obvious issues. The best way to avoid this issue is to maintain adequate spacing between traces. More is better but we will go with the rule of thumb: * **Differential Pair Spacing**: For differential pairs with intra trace spacing \(w\) we will maintain a spacing of \(5w-7w\) between the pairs. * **Single-Ended Trace Spacing**: For single-ended traces with trace width \(w\), we will maintain a spacing of around \(2-5w\). In general, we will avoid routing traces in parallel over a large distance as it will increase the amount of crosstalk. If we have to cross other signal traces we should do so at right angles. We take a more critical look at trace spacing and its effect on crosstalk in section 5 below where we simulate the finished PCB and a spacing example using FEM. **In this design:** Primary concern. ### Return Currents In a transmission line, return currents flow through the ground plane to complete the circuit. The current will take the path of least impedance. For fast digital signals, due to capacitance effects, the return current will flow directly under the signal trace. It will not necessarily take the shortest path through the ground plane. This is something we have to keep in mind when routing. We do not want to split the reference plane (ground directly under the signal trace) and thus interfere with the return current paths. Again, consider the following example: We route a trace on the internal layer below the signal trace. As we have created a discontinuity in the reference plane, the return current has to take a detour around the slit. It will travel along the slit back and forth, creating in essence a slit antenna. If the length of the slit approaches \(\lambda/4\), it will start to act as an antenna and radiate energy. Even if we do not match \(\lambda/4\), we have still increased the loop area which is proportional to the radiation efficiency. **In this design:** Primary concern. ### Impedance Matching (Stackup) As we are dealing with a transmission line, we need to ensure that the impedance of the trace matches the impedance of the source and load. This helps to minimize reflections and maximize power transfer. For SATA the differential impedance is specified as 100 ohms [3]. As we want to order from JLCPCB we will use a 4-layer board even though we only need two layers. The reason is that a two layer board is constructed differently from a multi layer board. A typical pcb is \(1.6 mm\) thick. Thus on a two layer board the top and bottom layers will be \(1.6 mm\) apart. For multi layer boards however, a thick core is used then copper layers are separated by a thin dielectric (prepreg) layer. For the stackup we will use (JLC04161H-7628) the top layer is only \(0.2104 mm\) apart from the In1 layer. This allows us to use much thinner traces to achieve the desired impedance (think about capacitance as a function of area and distance). We start by choosing an acceptable trace spacing. Then we can use the [JLC impedance calculator](https://jlcpcb.com/pcb-impedance-calculator/) to compute differential pair spacing/width to achieve the desired \(100 \Omega\). For our stackup: * **Trace Spacing**: We choose the standard \(8 mil\) (\(0.2032 mm\)) trace spacing for the differential pair. * **Trace Width**: We compute the trace width to be \(0.2205 mm\) to get the desired impedance of \(100 \Omega\). **In this design:** Primary concern. ### Impedance Mismatch Now that we have computed the required trace dimensions, we have to ensure that the impedance remains constant throughout the transmission line. One possible issue are corners or vias that can cause impedance variations. Let's start with corners. The problem here is that the cross-section of the trace changes at the corner, which can lead to impedance variations. For the speeds we are concerned about this effect is probably negligible. However, a \(45^\circ\) is better than a \(90^\circ\) corner and an arc (fillet) is better than a \(45^\circ\) corner. We will go with the arc. Vias also cause an impedance mismatch. For this project we do not need to use vias in data lines so this is not an issue we have to consider. Connectors and other passive components can also cause impedance mismatches. So can the pads they are attached to. We will keep pads and footprints as small as possible. **In this design:** Not critical. ### Reference Plane and Vias As mentioned above, the return current flows in the reference plane below the signal trace. If we were to use vias in our signal traces, the signal would change its reference plane. We therefore need to place ground vias next to the signal wire to allow the return current to change planes as the signal does. Thus, we will always use vias in pairs. If we use a via, we will place a ground via next to it. **In this design:** Avoided by layout. ### Stubs Stubs are short sections of trace that branch off from the main transmission line. They cause unwanted reflections and if they are longer than \(\lambda/20\) to \(\lambda/40\) (again, rule of thumb) signal integrity issues. We will avoid stubs in our design and, if we have to use them, keep them as short as possible. A non obvious cause of stubs are vias when routing on internal planes. If we were to go from one internal layer to another, the via would still go from the top of the pcb to the bottom. It would not be connected to the top and bottom layer but it would create a stub. Depending on the frequency/wavelength we are working with and the length of the stub this could be an issue. If we determine that via stubs are an issue, we can opt for back drilling. The manufacturer would then drill away the unwanted part of the via, removing the stub. Again, we will route on the top and bottom layers and there is no reason to use vias in this design. But it is something to keep in mind when working with high-frequency signals. **In this design:** Avoided by layout. ### Via Fencing Via fencing is a technique used to reduce electromagnetic interference by placing a row of ground vias around the signal traces. This creates a sort of "fence" that helps to contain the electromagnetic fields and reduce crosstalk between adjacent traces. We will not use via fencing in this design. We want the differential traces to couple to the reference plane and to each other, nothing else. Placing ground vias around the signal traces can surely help, however, if they are too close to the signal traces, they can cause unwanted coupling, they could cause standing waves etc. We would have to carefully consider the placement and spacing of these vias. For this design, I will therefore avoid using via fencing and go with adequate spacing instead. Another technique to consider is PCB edge via stitching. Here ground vias are placed around the PCB edge, connecting the top to the bottom plane and creating sort of a fence. The reasoning is the following: If we use vias on signal traces, they will radiate perpendicular to the via itself, causing a wave to travel longitudinally in the dielectric with the planes above and below as a waveguide. The wave will then exit through the edges of the PCB. If we place vias there, the wave cannot exit. We could also get the edge plated by the manufacturer. Again, an interesting approach but in my opinion not necessary for this design. We do not use vias on the signal traces and thus we do not need to contain waves emitted by them. **In this design:** Not critical. ### Solder Mask Many RF designs remove the solder mask from the signal traces as it is not invisible to high frequency signals. It will affect the impedance and increase dielectric losses. Controlling solder mask thickness is also not easy during manufacturing. However, to avoid corrosion we would probably have to go with ENIG (Electroless Nickel Immersion Gold). Furthermore, removing the solder mask from a PCB that goes inside a PC is not a good idea as it increases the risk of shorting traces. Lastly, the solder mask will not be an issue at the frequency range we are in. Thus, we will keep the solder mask on the PCB. **In this design:** Not critical. ### Sata Specifics There are a few sata specific considerations for our design. #### Pin 11 Staggered Spin Up and Drive Activity Pin 11 of the SATA power connector is used for staggered spin-up and the drive activity indicator LED. If pin 11 is pulled low, staggered spin up is disabled.To support staggered spin-up, we will connect pin 11 to the 5V rail through a pull-up resistor (R1). This allows the drive to spin up when the sata controller tells it to, allowing for staggered spin-up of multiple drives. We do this similarly to [3, 4] Fig. 2 shows the schematic for this connection. **1\_ACT** is the connection to pin 11 of disk 1. ![Pin 11 Schematic](https://blog.hirnschall.net/4x-sata-backplane/resources/img/pin11.jpg) Figure 2: Pin 11 Schematic #### AC coupling Capacitors SATA uses AC coupling capacitors (typically \(100 nF\)) to isolate the DC component of the signal while allowing the AC component to pass through. This way each side can set its own DC bias. In our design we will not add any additional capacitors as they are already present on the host side. #### Hot Plug Capability SATA supports hot plugging, which is primarily enabled by the staggered pin lengths of the standard power connector, ensuring that ground and pre-charge pins make contact before the main power and signal pins. Since this backplane uses standard SATA power connectors, it inherits this sequencing behavior. In typical desktop systems, power supplies are designed to directly handle drive insertion and the associated inrush currents, so this design behaves electrically similar to a passive Molex-to-SATA adapter. However, as discussed on [GitHub](https://github.com/Hirnschall-Technologies/4x-sata-backplane/issues/1) and [Section 6.14.3 of the SATA specification [3]](https://sata-io.org/system/files/specifications/SerialATA_Revision_3_5_Gold.pdf), fully compliant hot-plug implementations may include additional measures such as controlled inrush current and signaling considerations. These are not implemented here, meaning that while hot plugging works in practice, this design does not provide the robustness of enterprise-grade backplanes. PCB Design ---------- Now that we have gone over the design considerations, we can start designing the PCB. We will use KiCad for this project. The design files are available on GitHub. ![SATA Power Routing](https://blog.hirnschall.net/4x-sata-backplane/resources/img/power.jpg) Figure 3: SATA Power Routing Figure 3 shows the routing for the schematic shown in fig. 2. We use planes for power distribution, thick traces and multiple vias. Furthermore, we make sure to offset the activity LED from the hard drive so that it is actually visible. ![Sata Data Routing](https://blog.hirnschall.net/4x-sata-backplane/resources/img/sata.jpg) Figure 4: Sata Data Routing Figure 4 is more interesting. We apply everything discussed in section 3. The signal traces are routed as differential pairs, the corners have fillets, delay matching is done close to the corners, and the two differential pairs have ample spacing between them. We have a solid ground plane on In1 directly under the signal traces. The top layer has no co-planar plane, just the differential pairs. Furthermore, no vias in the signal traces were required. To be more exact, the spacing between the differential pairs is around \(3.1mm\) which is roughly \(15w\). The intra-pair skew is kept to \(0.001mm\) and the two differential pairs have a length mismatch (inter-pair skew) of around \(6mm\) between them. This inter-pair skew is not critical and not specified in the SATA specification. EM Simulation ------------- Before we finish with the mechanical design for this project, I'd like to simulate the PCB itself and also how spacing affects cross-talk. To do so, we can use 3D FEM to compute the impedances and analyze the resulting S parameters. First, we will convert the fabrication output (gerber files) from KiCad to a simulation ready 3D model using the [free Gerber-to-STEP converter](https://blog.hirnschall.net/gerber-to-step/). We will then solve Maxwell's equations for the 3D model using FEM (full wave field solver) with PEC (perfect electrical conductor) boundary conditions for the copper layers and PML (Perfectly Matched Layer) to avoid reflections on the edge of the airbox. The simulation itself is done using [CENOS RF Software](https://cenos-platform.com/products/radio-frequency-simulation-software/). It does everything we need for this project and it is quite simple to use. Furthermore, it is very cheap for students. But first, let's look at the effect of spacing on crosstalk. ### Spacing and Crosstalk To see the effect of spacing on crosstalk, we will simulate a simple PCB with three traces (fig. 5). Each trace has a feed on one side and is terminated with \(50 \Omega\) on the other side. The traces are matched impedance for \(50 \Omega\), \(\varepsilon\_r=4.92\),\(\sigma=0.362\cdot10^{-3}\),\(h=0.2104mm\),\(w=0.3493mm\),\(l=20mm\). The middle trace (1) is treated as the aggressor. The other two traces are viewed as victims. The left one (2) has an edge-to-edge spacing of \(2w\) and the right one (3) has an edge-to-edge spacing of \(5w\) to the middle trace. The full 3D geometry including the airbox (but not the PML region) is shown in fig. 5. Figure 5: 3D Model used to simulate effect of spacing on crosstalk (S21 and S31) #### S-Parameters (Single-Ended) Once the simulation is set-up we can compute the impedances and **S-parameters**. S-parameters measure the relationship between incident and reflected waves at each port. E.g. S21 is the ratio of power coming out of port 2 to the power incident on port 1. Thus, they can be used to analyze not only reflections but also cross-talk [5]. For this, we will sweep from \(3 GHz \) to \(10 GHz \). Fig. 6 shows the resulting S-parameters and the resulting delta in the frequency domain. Note that the y-axis of the plot is in dB. Low values indicate good signal integrity. For the \(2w\) spacing, we see a significant increase (around 10 dB) in crosstalk compared to the \(5w\) spacing. ![Crosstalk vs Trace Spacing (\(2w\) vs \(5w\))](https://blog.hirnschall.net/4x-sata-backplane/resources/img/sparameter.jpg) Figure 6: Crosstalk vs Trace Spacing (\(2w\) vs \(5w\)) #### S-Parameters (Differential) Let's now treat the middle and the left trace as a differential pair and look at the effect on crosstalk in the right trace. Note that we have not changed any spacing for this experiment. The geometry is the same as in the previous example. However, the crosstalk is again, as expected, reduced. ![Crosstalk for Single Ended vs Differential](https://blog.hirnschall.net/4x-sata-backplane/resources/img/sparameterdiff.jpg) Figure 7: Crosstalk for Single Ended vs Differential As the spacing on the actual backplane is even larger than in this example we expect even better performance and thus no issues. #### Interpreting S-Parameters What we have computed in the simulation above is NEXT (Near-End Crosstalk). As a rule of thumb, we aim for \(\leq 5\%\) of crosstalk for single ended signals and for \(\leq 0.3\%\) for high speed serial signals [6]. S-parameters are typically given in dB and represent \(S\_{dB}:=20\log\_{10}(V\_{out}/V\_{in})\). So, if we end up with \(S21=-30dB\) this means that feed two sees \(\approx 3.2\%\) of the voltage swing at port 1 as noise. As a quick reference: $$\begin{align} -20dB &\approx 10\% \\ -30dB &\approx 3.2\% \\ -40dB &\approx 1\% \\ -50dB &\approx 0.3\% \\ -60dB &\approx 0.1.\% \end{align}$$ If we compare the dB results in the plots above (fig. 6 and 7) at the nyquist frequency (\(=3GHz\)), to our rule of thumb, we see that they match nicely. To determine the acceptable amount of crosstalk, we need to consider the allowed SNR (Signal-to-Noise Ratio) for our application as well as the maximum allowed attenuation. If we combine both, we can determine the maximum allowable crosstalk from all sources for our design. Given the large spacing of \(\approx 15w\) for our backplane, we expect to be well below \(-50dB\) for the crosstalk which fits our rule of thumb. Besides crosstalk, S-parameters are used to describe signal transmission, reflection, and coupling. As these are meaningful for the full pcb analysis we will go over them now. Consider two parallel traces. Each trace has a port on each end and we name them such that port 1 is next to port 2, port 3 is next to port 4. Trace 1 has ports 1 and 3 and trace 2 has ports 2 and 4. Then we have the following important S-parameters: * **Reflection (Return Loss):** \(S11\), \(S22\), \(\ldots\) represent the return loss at ports 1, 2, etc., respectively. They indicate how much of the signal is reflected back to the source due to impedance mismatches. A lower value (more negative in dB) indicates better impedance matching and less signal reflection. * **Insertion Loss:** \(S31\) and \(S42\) represent signal attenuation along each trace. They indicate how much of the signal is transmitted from one port to the other. A lower value (more negative in dB) indicates higher loss and less signal transmission. * **NEXT:** \(S21\) and \(S12\) represent the near-end crosstalk (NEXT) between the two traces. They indicate how much of the signal from one trace is coupled into the other trace at the near end (the end closest to the source). A lower value (more negative in dB) indicates less crosstalk and better isolation between the traces. * **FEXT:** \(S41\) and \(S32\) represent the far-end crosstalk (FEXT) between the two traces. They indicate how much of the signal from one trace is coupled into the other trace at the far end (the end closest to the load). A lower value (more negative in dB) indicates less crosstalk and better isolation between the traces. As S is a matrix, the parameters described above are also valid with the indices swapped. They then represent the same behavior but with the ports in reverse order. ### SATA Backplane Finally, we can simulate the actual backplane. We will look at the S-parameters for the differential pairs and the crosstalk between them. The geometry is the relevant section for one drive from the actual provided gerber files. We will consider the left pair (**pair1**) as aggressor and the right pair (**pair2**) as victim. Fig. 8 shows the section used for this simulation. Figure 8: 3D Model of the backplane section used in this simulation, including the air box but not the PML region #### Simulation Setup Each end of each trace gets a port. The port is modeled as a lumped element with voltage tap, not a full wave port (more on this below). This full 8-port setup allows us to capture the full S-matrix including FEXT and insertion loss. We do not use termination resistors in this simulation. Non-excited ports are set to an open circuit condition (zero current). This way we directly extract the full open circuit Z-matrix. To confirm this setup, we will comput \(Z\_{diff}\) which we know. It should be \(100 \Omega\). Once we have computed \(Z\_{diff}\), we can do a mesh dependency analysis. To do so, we use a smaller section of the pcb and simulate it with increasingly fine mesh. We will do the same for the size of the air-box, the pml region, and port pin placement. Placing the port pin directly on the physical PCB edge introduced a noticable amount of artificial reactance (\(30 \Omega\)). This was fixed by moving the port pin \(0.5mm\) away (inwards) from the PCB boundary. Fig. 9 below shows the relative impedance change from one mesh size to the next. As we no longer see meaningful change below \(w/3\), we will use \(\text{maxh}=w/3\) when doing the full PCB. ![Relative \(Z_{diff}\) change between different mesh sizes](https://blog.hirnschall.net/4x-sata-backplane/resources/img/meshdep.jpg) Figure 9: Relative \(Z\_{diff}\) change between different mesh sizes #### Mixed Mode S-Parameters Once we have computed the Z matrix and S-parameters we can convert the S-parameters to mixed mode parameters to analyze the differential pairs. This conversion is done as post processing. The naming convention for these parameters is based on the differential and common modes of the signals. E.g. \(S\_{dd}\) is the differential-differential parameter and \(S\_{cd}\) is the common-differential parameter. #### Zdiff, Sdd11 and Sdd33 As discussed above, we compute \(Z\_{diff}\) from Sdd11 and Sdd33 and compare the result to the expected \(Z\_{diff} = 100 \Omega\). We want to see a flat curve matching the design specification, and we do not want to see large imaginary parts in the result. Fig. 10 shows the resulting \(Z\_{diff}\). **What problems look like:** * Real part oscillates significantly or moves away from the expected value (we expect a flat curve). * Large imaginary parts (we expect \(\approx 0\)). **PCB design flaws we would spot in this plot:** * Trace width variations * Stackup issues * Bad via placement * Slit in reference plane (return current has to detour) ![Computed \(Z_{diff}\) from Sdd11 and Sdd33 of both differential pairs](https://blog.hirnschall.net/4x-sata-backplane/resources/img/zdiff.jpg) Figure 10: Computed \(Z\_{diff}\) from Sdd11 and Sdd33 of both differential pairs #### Crosstalk (NEXT, FEXT) Next, we will analyze the crosstalk between the differential pairs. Fig. 11 shows the NEXT and FEXT values. As expected they are very low, indicating good isolation between the pairs. This is not surprising, given the spacing of \(\approx 3.1mm\). Keep in mind that the backplane is connected to a SATA cable as well, so we have to be in spec including the cable and connector. **What problems look like:** * Values larger than expected (compute using SNR and attenuation as described above) * Large imaginary parts. **PCB design flaws we would spot in this plot:** * Inter-pair spacing too small * Reference plane slit such that return currents have to take the same path for both pairs. * Bad via placement such that return currents have to take the same path for both pairs. * Parallel run length too long ![Crosstalk between the two differential pairs (NEXT and FEXT)](https://blog.hirnschall.net/4x-sata-backplane/resources/img/next_fext.jpg) Figure 11: Crosstalk between the two differential pairs (NEXT and FEXT) #### Mode Conversion (Sdc and Scd) Mode conversion describes how much of a differential signal is converted into a common mode signal (Sdc) and vice versa (Scd). Ideally both are zero. Any conversion is a loss of signal integrity and an increase in EMI. In a perfectly symmetric differential pair the two contributions cancel by symmetry, so elevated values are a direct indicator of asymmetry somewhere in the layout. **What problems look like:** * Large values or frequency specific spikes * Sdc and Scd differ significantly **PCB design flaws we would spot in this plot:** * Asymmetric routing * Intra-pair skew * Slit in reference plane such that the return current of one trace has to detour * Vias without reference (ground) via close * Connector footprint asymmetric ![Mode Conversion (Sdc and Scd)](https://blog.hirnschall.net/4x-sata-backplane/resources/img/modeconversion.jpg) Figure 12: Mode Conversion (Sdc and Scd) #### Insertion Loss Insertion loss describes how much signal power is lost as the differential signal travels from one end of the pair to the other (Sdd21 and Sdd43). It includes both conductor losses and dielectric losses, both of which increase with frequency. A well-behaved insertion loss curve is smooth and monotonically decreasing with frequency. Any deviation from this indicates a localized discontinuity in the transmission path. **What problems look like:** * Notches and dips in the curve * Ripples (standing wave patterns) **PCB design flaws we would spot in this plot:** * Stubs and via stubs * Bad via placement * Reference plane slit * Sharp 90-degree bends * Impedance mismatches causing standing waves ![Insertion Loss (Sdd21 and Sdd43)](https://blog.hirnschall.net/4x-sata-backplane/resources/img/insertionloss.jpg) Figure 13: Insertion Loss (Sdd21 and Sdd43) #### Limitations of our Simulation The main limitation of this approach is that we do not model the connectors themselves in 3D. In practice the connector will be the largest impedance discontinuity in the system. So the simulation reflects the PCB traces only. We also do not use a proper wave port model which introduces some artificial reactance near the port. Furthermore, the copper features are modeled as PEC (Perfect Electric Conductor) and have no thickness (shell). This means we do not account for conductor loss in our simulation and the fringing fields at the trace edges are affected. This means that the real insertion loss is likely much higher than simulated. This is all OK, but we have to keep it in mind when interpreting the results. Mechanical Design ----------------- The mechanical design is much simpler than the electrical one. We will use rubber standoffs to mount the backplane itself to the case/chassis. The standoffs are quite soft and will isolate any vibrations. Then we will use a 3d printed cage to hold the drives in place and avoid stain on the connector. To avoid slides and a thus lot of 3D printing we will use DIN912 M3x6 screws to hold the drives in place. The screws screw fully into the lower four holes of the HDD and the nice round head of the screws will slide in a slot in the 3d printed cage, securing the drive in place. The cage can be bolted to the PCB using the rubber stand offs. Furthermore it will get slots for airflow between the drives and two holes that facilitate fan mounting. During the design process, we can use the step export from KiCad (also on GitHub). Below is an image of the finished backplane with cage. You can see how the din 912 screws are used to secure the drives. ![Finished Backplane with Cage and Drives](https://blog.hirnschall.net/4x-sata-backplane/resources/img/mechanical.jpg) Figure 14: Finished Backplane with Cage and Drives Conclusion ---------- In this article we have designed a custom 4x SATA backplane. We went over the design considerations for high-speed signals and how to apply them to our specific design. The design files are available on GitHub and you can order the backplane from JLCPCB. We have set up a full 3D FEM simulation of the backplane and found that it performs well. We have also analyzed the crosstalk between traces and how it is reduced by spacing and by using differential signaling. We have solved the two main shortcomings of commercial solutions outlined at the beginning: cost and noise isolation. Nothing now stands in the way of a fully custom, [truly silent server case! (click here)](https://blog.hirnschall.net/silent-home-server/) Overall, I am quite happy with how this project turned out. Below is a video of me plugging a HDD into the backplane, it works really nicely: --- ## [Silent Home Server Case — Custom Aluminium Build](https://blog.hirnschall.net/silent-home-server/) meta description: A silent home server case from aluminium sheets and extrusions — 8 hot-swap drives, automotive sound damping, rubber-isolated mounts, forced airflow. date published: 15.04.2026 (DD.MM.YYYY format) date last modified: 15.04.2026 (DD.MM.YYYY format) --- # [Building a Silent Home Server Case from Aluminium](https://blog.hirnschall.net/silent-home-server/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: A silent home server case from aluminium sheets and extrusions — 8 hot-swap drives, automotive sound damping, rubber-isolated mounts, forced airflow. meta title: Silent Home Server Case — Custom Aluminium Build date published: 15.04.2026 (DD.MM.YYYY format) date last modified: 15.04.2026 (DD.MM.YYYY format) --- Motivation ---------- Ever since I built my first server and had to sleep in the same room with it, I wanted to build a truly silent home server. And even though the server now lives in the Herrenzimmer (its own room) and noise is not such a big concern anymore, the idea stuck with me ever since. So, let's build a custom server case that is both quiet and nice looking. This project concludes the build we started with [building a custom 4x backplane.](https://blog.hirnschall.net/4x-sata-backplane/) Design Considerations --------------------- For this build to be actually useful, I want it to be reproducible along with some other requirements: * The server should be quiet * It has to fit two 4x backplanes (eight 3.5-inch drives total) * Adequate cooling for drives, HBA, NIC, and the CPU * I want the build to be a weekend project with hand tools (no sheetmetal bending!) * To keep the case small, we will choose an mATX mainboard Design ------ As we have already set which backplane we will use ([a custom one](https://blog.hirnschall.net/4x-sata-backplane/)), we can focus on the case itself. For it to be reproducible, we will choose standard size 1mm thick cut aluminium sheets. To keep things quiet, we will line the entire interior with 1cm thick automotive sound dampening foam. We will not use common acoustic foam that is normally mounted on walls. It is not suitable for this application. For cooling, we will build the case to force front to back airflow. The backplanes drive cage supports mounting a 120mm fan. So, from front to back we will have the drives, two fans, the mainboard. We will mount the power supply above the PCIe slots. As long as we choose 1u expansion cards, this will fit nicely. We will orient the PSU with its fan facing the PCIe cards. As PSU fans are typically intake fans, this will help to suck air away from the expansion cards and exhaust it out the back of the case. Build ----- ### Case The case will be built from six 1mm thick aluminium sheets and four 2020 aluminium extrusions. We will mount the top, bottom, left, and right panel to the slots in the extrusions using t-nuts. To mount the front and back panel we can tap an m6 thread in the ends of the extrusions. Fig. 1a shows the case and with the mainboard and one backplane installed (test fitting). To mount the mainboard and backplanes, we will use u profiles (20mm wide and 10mm thick). This way we can place acoustic foam between the outer panel and the u profiles (more on the foam later). The mainboard will sit on m3 brass standoffs. The backplanes will be mounted using m3 rubber isolation standoffs. This way we isolate any vibrations coming from the drives from the case itself. Another nice bonus is that we can slide the profiles in and out as they slide into the extrusion's slots. One such rail with rubber standoffs mounted is shown in fig. 1b below. ![Mainboard and backplane test-fit](https://blog.hirnschall.net/silent-home-server/resources/img/case.jpg) (a) Mainboard and backplane test-fit ![U-Profile with rubber standoffs](https://blog.hirnschall.net/silent-home-server/resources/img/standoffs.jpg) (b) U-Profile with rubber standoffs Figure 1: Case and Mainboard Mounting Fig. 2 shows the connected backplanes with attached fans and how the profiles supporting them slide out as a whole subassembly. ![Backplane Connection to HBA as a Subassembly](https://blog.hirnschall.net/silent-home-server/resources/img/slide_out.jpg) Figure 2: Backplane Connection to HBA as a Subassembly ### Cooling Like a rack server, we will push air through the case from front to back. To do so, we add a 3D printed bracket between the front panel and the case. We do the same on the back. This will allow enough air to enter and exit the case without the need for high static pressure (and high noise) fans. To prevent air recirculation inside the case we can add a simple cardboard wall around the fans (splitting the case in a front and back part). The 3D-printed air inlet/outlet is shown in fig. 3 below. If we choose the top and side panels 4cm longer than the bottom one, we can cover the bracket. Air will then enter through the bottom. Figure 3: 3D Model of the inlet geometry/bracket (interactive 3D) #### Fans For fans, we went with the new Noctua NF-A12x25 G2 120mm fans or the older, slightly cheaper, NF-P12 redux as an alternative. Either way, any 120mm fan will fit. The Noctua ones are quiet and have good static pressure. We will use two of them, one for each backplane. Mounting them between the motherboard and the backplanes gives a clean separation between the front intake and the rear exhaust. ### Acoustics To make the case silent we will try to block the three main ways of sound transmission: * **Structure-borne vibration:** The drive or fan physically vibrates, that vibration travels through its mounting points into the case frame and panels, and the panels then act as loudspeakers radiating sound into the room. * **Sound transmitting through the panels:** Sound waves generated by the drive/fan travel through the air inside the case and then pass through the case panels into the room. The panel's ability to resist this depends on its mass and stiffness. * **Sound transmitting through the air (airborne noise):** Sound takes the path of least resistance. Any gap, vent hole, or unsealed cable cutout acts as an acoustic leak and can radiate noise. #### Structure-Borne Vibration We use soft rubber standoffs to mechanically decouple the backplanes from the case. This way vibrations from both the drives and fans are isolated and do not transmit to the case and into the room. #### Sound Transmission through Panels To block noise coming from the drives and fans we will line the inside of the case completely with 1cm thick automotive sound dampening foam. We do not want to treat a room, we want to treat a surface. We therefore do not want to use acoustic foam panels that are typically used for wall mounting in rooms. While the wall mounted foam is open cell, the automotive sound dampening foam is closed cell and much heavier. The closed cell automotive foam is the correct choice to stop sound from transmitting through a panel due to its mass and viscoelastic damping properties. For this build we do not add a second rigid layer to the inside of the foam although it would probably improve the acoustic performance quite a bit (constrained layer damping - CLD). #### Airborne Noise This one is the hardest to get right. Noise will transmit through any opening in the case. Unfortunately we cannot seal the box completely as airflow is required for cooling. What we will do instead is use the 3d printed spacers and the acoustic foam to remove any line of sight from the outside to the noise emitting components. The 3D printed spacers create an offset gap so sound has to turn a corner to exit and that corner is lined with acoustic foam. If this is still not enough, we can add a "sound maze" (fig. 4) to further reduce noise transmission. As this will also affect the cooling however, we will try it without the maze first. ![Sketch of a Sound Maze to further reduce noise transmission](https://blog.hirnschall.net/silent-home-server/resources/img/sound_maze.jpg) Figure 4: Sketch of a Sound Maze to further reduce noise transmission ### Sheet Dimensions For my setup, using an mATX motherboard, I used panels and extrusions of the following size (fig. 5): * **Top and bottom panel:** 35cm x 40cm * **Left and right panel:** 40cm x 20cm * **Front and back panel:** 35cm x 20cm * **4x aluminium extrusions:** 40cm long * **4x aluminium u profiles:** 2cm wide x 1cm height x 40cm long ![Sheet Dimensions](https://blog.hirnschall.net/silent-home-server/resources/img/sheet_dimensions.jpg) Figure 5: Sheet Dimensions Panels of this size will leave the 3d printed spacers visible. If you want to hide them you can add 4cm (2cm per spacer) to the length of the top panel. So the panel ends up as 35cm x 44cm. You can also do so for the side panels. However, it will cover the air inlets on the side. This is probably ok for most setups as the bottom is still open. ### PSU Cutout To accurately cut the PSU opening, we will use a template. To do so we first stick painters tape to the actual psu. We can then trim the edges and transfer the tape to the panel we want to cut. We can then fine tune where we want to cut on the inside. You can see the tape on the PSU and the final template on the actual aluminium sheet in fig. 6. ![Painters tape on PSU](https://blog.hirnschall.net/silent-home-server/resources/img/psu_marking.jpg) (a) Painters tape on PSU ![Template on aluminium sheet](https://blog.hirnschall.net/silent-home-server/resources/img/psu_cutout.jpg) (b) Template on aluminium sheet Figure 6: PSU Cutout using painters tape as template ### Feet To make this case really stand out, we can add nice aluminium feet like on old HiFi systems. The ones I used also have a nice rubber bottom, further helping to reduce vibrations and noise. The feet are 40mmx20mm and simply screw into the bottom sheet metal panel using a nut and a bolt. I offset mine from the edge by about 1cm so that the nut on the inside does not interfere with the 2020 extrusions. Where to Buy ------------ Below is a list of all non standard components used in this build. Things like extrusions or aluminium sheets are best sourced locally to avoid shipping costs. | Component | Quantity | Where to buy | | --- | --- | --- | | m3 rubber standoffs | × 8 | [amazon.com](https://amzn.to/41eKUfa) [amazon.de](https://amzn.to/3O8Frn3) | | Acoustic dampening foam | × 1 | [amazon.com](https://amzn.to/3OsiBXH) [amazon.de](https://amzn.to/4ccKdJI) | | HiFi Feet | × 4 | [amazon.com](https://amzn.to/4tPyBCh) [amazon.de](https://amzn.to/4crHScy) | | Sata Backplane | × 2 | [Blog Article](https://blog.hirnschall.net/4x-sata-backplane/) [GitHub](https://github.com/hirnschall-technologies/4x-sata-backplane) | | #CommissionsEarned | | | Note on quantities: The quantity listed is the actual number of pieces needed. If you need e.g. four feet, and the amazon listing is for a pack of four, you only need to order one pack. Conclusion ---------- Overall, I am extremely happy with how the case turned out. It looks nice, and the build process was easier than expected. Going into the details of sound transmission was quite interesting as well. At the time of writing, the 3D printed spacers are not yet fitted, so a final acoustic verdict is still pending. That said, the previous iteration of this build, with larger openings and the wrong foam type, was already almost inaudible in normal use. With proper automotive damping foam lining every panel, rubber-isolated drive mounts, and no line of sight from the outside to the noise sources, this version should perform at least on par and likely better. If it is not quiet enough, adding a sound maze to the air inlets and outlets or bonding a second rigid layer on top of the foam (constrained layer damping, CLD) are the logical next steps. --- ## [CNC Mill Concept — Precision Mill Design Documentation](https://blog.hirnschall.net/cnc-mill-concept/) meta description: Design documentation for a fixed CNC milling machine targeting ≤ 10 µm accuracy in an apartment: structure, damping, sensing, electronics, and CAN software. date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- # [CNC Mill Concept](https://blog.hirnschall.net/cnc-mill-concept/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Design documentation for a fixed CNC milling machine targeting ≤ 10 µm accuracy in an apartment: structure, damping, sensing, electronics, and CAN software. meta title: CNC Mill Concept — Precision Mill Design Documentation date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- Introduction ------------ This documentation describes the design intent, system architecture, and planned implementation of a fixed CNC milling machine optimised for precision, low noise, and apartment-compatible operation. The machine targets ≤ 10 µm accuracy and repeatability while staying within residential floor load limits — no concrete bed, no heavy mineral cast. The structure of this documentation follows the causal chain from physical reality, to measurement, to estimation, to supervision and execution. The software architecture and CAN-FD transport layer used in the distributed sensor nodes are closely related to the implementation described in the [type-safe CAN transport layer post](https://blog.hirnschall.net/can-transport-layer/). Table of Contents ----------------- The documentation is split into the following sub-pages: ### 1[Goals, Philosophy & System Architecture](https://blog.hirnschall.net/cnc-mill-concept/goals-philosophy-system-architecture/) * [Goals](https://blog.hirnschall.net/cnc-mill-concept/goals-philosophy-system-architecture/#goals) * [Constraints](https://blog.hirnschall.net/cnc-mill-concept/goals-philosophy-system-architecture/#constraints) * [Design Philosophy](https://blog.hirnschall.net/cnc-mill-concept/goals-philosophy-system-architecture/#design-philosophy) * [Explicit Non-Goals](https://blog.hirnschall.net/cnc-mill-concept/goals-philosophy-system-architecture/#non-goals) * [System Architecture](https://blog.hirnschall.net/cnc-mill-concept/goals-philosophy-system-architecture/#system-architecture) * [Communication Topology](https://blog.hirnschall.net/cnc-mill-concept/goals-philosophy-system-architecture/#communication-topology) --- ### 2[Mechanical Structure & Passive Damping](https://blog.hirnschall.net/cnc-mill-concept/mechanical-structure-and-passive-damping/) * [Frame and Primary Structure](https://blog.hirnschall.net/cnc-mill-concept/mechanical-structure-and-passive-damping/#frame) * [X Gantry Structure](https://blog.hirnschall.net/cnc-mill-concept/mechanical-structure-and-passive-damping/#gantry) * [Force Loop Definition](https://blog.hirnschall.net/cnc-mill-concept/mechanical-structure-and-passive-damping/#force-loop) * [Table and Y-Axis Support](https://blog.hirnschall.net/cnc-mill-concept/mechanical-structure-and-passive-damping/#table) * [Carbon Fibre CLD](https://blog.hirnschall.net/cnc-mill-concept/mechanical-structure-and-passive-damping/#cld-cf) * [Epoxy Granite](https://blog.hirnschall.net/cnc-mill-concept/mechanical-structure-and-passive-damping/#epoxy-granite) * [Elastomeric Isolation](https://blog.hirnschall.net/cnc-mill-concept/mechanical-structure-and-passive-damping/#elastomeric-isolation) --- ### 3[Piezo Shunt Damping](https://blog.hirnschall.net/cnc-mill-concept/piezo-shunt-damping/) * [Piezo Damping Elements](https://blog.hirnschall.net/cnc-mill-concept/piezo-shunt-damping/#piezo-elements) * [Shunt Networks](https://blog.hirnschall.net/cnc-mill-concept/piezo-shunt-damping/#shunt-networks) * [Instrumentation of Shunts](https://blog.hirnschall.net/cnc-mill-concept/piezo-shunt-damping/#instrumentation) * [Adaptive Shunt Tuning](https://blog.hirnschall.net/cnc-mill-concept/piezo-shunt-damping/#adaptive-tuning) * [Interaction with Other Damping Strategies](https://blog.hirnschall.net/cnc-mill-concept/piezo-shunt-damping/#interaction) --- ### 4[Enclosure & Acoustics](https://blog.hirnschall.net/cnc-mill-concept/enclosure-and-acoustics/) * [Enclosure Construction](https://blog.hirnschall.net/cnc-mill-concept/enclosure-and-acoustics/#construction) * [Acoustic Strategy](https://blog.hirnschall.net/cnc-mill-concept/enclosure-and-acoustics/#acoustic-strategy) * [Mechanical Integration](https://blog.hirnschall.net/cnc-mill-concept/enclosure-and-acoustics/#mechanical-integration) * [Openings and Airflow](https://blog.hirnschall.net/cnc-mill-concept/enclosure-and-acoustics/#openings) --- ### 5[Vibration, Sensing & Metrology](https://blog.hirnschall.net/cnc-mill-concept/vibration-sensing-and-metrology/) * [Piezo Surface Microphones](https://blog.hirnschall.net/cnc-mill-concept/vibration-sensing-and-metrology/#piezo-microphones) * [Accelerometers](https://blog.hirnschall.net/cnc-mill-concept/vibration-sensing-and-metrology/#accelerometers) * [Sensor Placement and Coverage](https://blog.hirnschall.net/cnc-mill-concept/vibration-sensing-and-metrology/#sensor-placement) * [Laser-Based Tool Measurement](https://blog.hirnschall.net/cnc-mill-concept/vibration-sensing-and-metrology/#laser-tool-measurement) * [Thermal Stabilization](https://blog.hirnschall.net/cnc-mill-concept/vibration-sensing-and-metrology/#thermal-stabilization) --- ### 6[Compliance, State Estimation & Supervision](https://blog.hirnschall.net/cnc-mill-concept/compliance-state-estimation-supervision/) * [Compliance Model](https://blog.hirnschall.net/cnc-mill-concept/compliance-state-estimation-supervision/#compliance-model) * [Machine State Classification](https://blog.hirnschall.net/cnc-mill-concept/compliance-state-estimation-supervision/#state-classification) * [Supervision Logic](https://blog.hirnschall.net/cnc-mill-concept/compliance-state-estimation-supervision/#supervision-logic) * [State Transitions and Hysteresis](https://blog.hirnschall.net/cnc-mill-concept/compliance-state-estimation-supervision/#state-transitions) --- ### 7[Control Electronics, Motion & Electrical](https://blog.hirnschall.net/cnc-mill-concept/electronics-motion-and-electrical/) * [ECU (Master PCB)](https://blog.hirnschall.net/cnc-mill-concept/electronics-motion-and-electrical/#ecu) * [Sensor PCB Capabilities](https://blog.hirnschall.net/cnc-mill-concept/electronics-motion-and-electrical/#sensor-pcb-capabilities) * [Motion Configuration](https://blog.hirnschall.net/cnc-mill-concept/electronics-motion-and-electrical/#motion-configuration) * [Electrical Installation](https://blog.hirnschall.net/cnc-mill-concept/electronics-motion-and-electrical/#electrical-installation) * [Emergency Stop](https://blog.hirnschall.net/cnc-mill-concept/electronics-motion-and-electrical/#estop) * [Pneumatics](https://blog.hirnschall.net/cnc-mill-concept/electronics-motion-and-electrical/#pneumatics) --- ### 8[Software Architecture & CAN](https://blog.hirnschall.net/cnc-mill-concept/software-architecture-can/) * [Design Invariants](https://blog.hirnschall.net/cnc-mill-concept/software-architecture-can/#design-invariants) * [Software Stack](https://blog.hirnschall.net/cnc-mill-concept/software-architecture-can/#software-stack) * [CAN Map Specification](https://blog.hirnschall.net/cnc-mill-concept/software-architecture-can/#can-map-spec) * [CAN Manager](https://blog.hirnschall.net/cnc-mill-concept/software-architecture-can/#can-manager) * [CanManager Implementation](https://blog.hirnschall.net/cnc-mill-concept/software-architecture-can/#canmanager-implementation) * [Software Tests](https://blog.hirnschall.net/cnc-mill-concept/software-architecture-can/#software-tests) --- ### 9[Deferred & Future Work](https://blog.hirnschall.net/cnc-mill-concept/deferred-and-future-work/) * [Motion and Feedback Upgrades](https://blog.hirnschall.net/cnc-mill-concept/deferred-and-future-work/#motion-feedback) * [Drive Electronics](https://blog.hirnschall.net/cnc-mill-concept/deferred-and-future-work/#drive-electronics) * [Advanced Compensation Strategies](https://blog.hirnschall.net/cnc-mill-concept/deferred-and-future-work/#advanced-compensation) * [Manufacturing and Implementation](https://blog.hirnschall.net/cnc-mill-concept/deferred-and-future-work/#manufacturing) --- ## [Force & Moment Equilibrium Calculator — 2D Statics Solver](https://blog.hirnschall.net/force-moment-equilibrium/) meta description: Symbolic calculator for 2D static equilibrium problems (FSAE-friendly). Enter points with coordinates and known forces, get all unknown reactions solved in the browser. date published: 14.05.2026 (DD.MM.YYYY format) date last modified: 14.05.2026 (DD.MM.YYYY format) --- # [A Force and Moment Equilibrium Calculator](https://blog.hirnschall.net/force-moment-equilibrium/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Symbolic calculator for 2D static equilibrium problems (FSAE-friendly). Enter points with coordinates and known forces, get all unknown reactions solved in the browser. meta title: Force & Moment Equilibrium Calculator — 2D Statics Solver date published: 14.05.2026 (DD.MM.YYYY format) date last modified: 14.05.2026 (DD.MM.YYYY format) --- About ----- The tool below solves 2D static equilibrium problems. Given a set of points with known coordinates and either known or unknown force components, it solves the three equilibrium equations (\(\sum F\_x = 0\), \(\sum F\_y = 0\), \(\sum M\_z = 0\)) symbolically and returns each unknown force. It was originally built for FSAE rules quiz usage. Similar to [Everything Aero](https://blog.hirnschall.net/everything-aero/) and the [Gas Cycle Calculator](https://blog.hirnschall.net/gas-cycle-calculator/), this tool uses SymPy in the browser to solve the resulting system of equations symbolically. ### How to use it * Enter one point per line. Each line is a comma-separated list of `key=value` pairs describing one force application point. E.g. "x=0, y=0, fx=10, fy=0" * Each point may have the following values: `x`, `y`, `fx`, `fy`, `f`, `angle` * Force at a point can be provided in one of two equivalent ways: + Cartesian: `fx=...` and/or `fy=...`. + Magnitude + angle: `f=...`, `angle=...` (angle measured CCW from the positive x-axis). * Forces or coordinates that are not provided become unknowns the solver will determine. * Units can be multiplied into values. Supported: lengths `mm`, `cm`, `dm`, `m`, `km`, `in`, `ft`, `yd`, `mile`; forces `N`, `kN`, `lbf`; angles `rad`, `deg`. E.g. `x=210*mm`, `f=2*kN`, `angle=30*deg`. * Bare numbers are in m, N, deg. * Click **Solve**. Results are rendered as LaTeX: per-point reaction vectors \(F\_i\), their magnitudes \(\|F\_i\|\), coordinates, and moments \(M\_{z,i}\). The solver builds the full equilibrium system (translational and rotational) and hands it to SymPy. It solves for all unknown force components and coordinates symbolically and returns the values. The calculator runs a full Python environment in the browser. It needs a few seconds to start up and install packages with pip (in the browser environment only). SymPy is lazily loaded on the first **Solve** press, so that first solve takes a few seconds. If the tool fails to load, reload the page and check the console (press F12). Calculator ---------- Loading Python ... Examples -------- Let's take a look at how to use the calculator on an FS rules quiz question. ### Example: FS-Quiz Question 157 [FS-Quiz Question 157](https://fs-quiz.eu/question/157) describes a beam loaded with several vertical forces and supported at two points. We need to find the reaction forces at the supports. Every loaded point and every support gets one input line. Loaded points have their applied force as `fy`; supports have `fx=0` (the rollers/pins only carry vertical load) and leave `fy` unknown so the solver fills it in. ``` x=0, y=0, fx=0, fy=-40 x=.21, y=0, fx=0, fy=-60 x=.36, y=0, fx=0, fy=-35 x=.535, y=0, fx=0, fy=-55 x=.11, y=0, fx=0 x=.48, y=0, fx=0 ``` Fig. 1 shows the input and the resulting reaction forces at the two supports. The correct solution (\(91.1\) N) is shown as \(F\_5\) as we have entered the point that is ask for at position 5. Note that we can use arithmetic expressions in the input values (e.g. `x=(210+150)*mm`). ![Input and solution for FS-Quiz Question 157 (2D static equilibrium), showing the reaction forces at the two supports](https://blog.hirnschall.net/force-moment-equilibrium/resources/img/fs-quiz-157.jpg) Figure 1: Input and solution for FS-Quiz Question 157 (2D static equilibrium), showing the reaction forces at the two supports How It Works ------------ The web UI is built with [ngapp](https://cerbsim.github.io/ngapp) and runs Python entirely in the browser. Each input line declares one point with coordinates and (optionally) a force. The unit suffixes (`mm`, `kN`, `deg`, …) are implemented as additional equations/knowns. SymPy assembles and solves the resulting system symbolically and the result is rendered as LaTeX with MathJax. ### Equilibrium Equations For a planar rigid body in static equilibrium, the net force and the net moment around any point both vanish. With \(N\) force application points at positions \((x\_i, y\_i)\) and force components \((F\_{x,i}, F\_{y,i})\), the solver writes: * **Force equilibrium**: $$\sum\_{i=1}^{N} F\_{x,i} = 0 \qquad \sum\_{i=1}^{N} F\_{y,i} = 0$$ * **Moment equilibrium** around each point \(i\): $$\sum\_{j \ne i} \big[(x\_j - x\_i)\, F\_{y,j} - (y\_j - y\_i)\, F\_{x,j}\big] = 0$$ Writing the moment balance around every point is redundant in 2D (one moment equation plus the two force equations is enough), but it lets the solver use whichever set is easiest given the knowns and helps SymPy converge without the user having to pick a pivot. ### Force from Magnitude and Angle When a force is given as a magnitude \(F\) and an angle \(\alpha\) measured CCW from the positive x-axis, the solver adds: $$F\_{x,i} = F \cos\alpha \qquad F\_{y,i} = F \sin\alpha$$ so the rest of the system stays in Cartesian components. --- ## [ESP32 vs ESP8266: Which One Should You Actually Use?](https://blog.hirnschall.net/esp8266-vs-esp32/) meta description: Side-by-side ESP32 vs ESP8266 — specs, GPIO, ADC, deep sleep, price. Which chip wins in 2026, and the cases where the ESP8266 still beats it. date published: 10.04.2026 (DD.MM.YYYY format) date last modified: 26.04.2026 (DD.MM.YYYY format) --- # [ESP8266 vs ESP32: Which One Should You Actually Use?](https://blog.hirnschall.net/esp8266-vs-esp32/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Side-by-side ESP32 vs ESP8266 — specs, GPIO, ADC, deep sleep, price. Which chip wins in 2026, and the cases where the ESP8266 still beats it. meta title: ESP32 vs ESP8266: Which One Should You Actually Use? date published: 10.04.2026 (DD.MM.YYYY format) date last modified: 26.04.2026 (DD.MM.YYYY format) --- Introduction ------------ The ESP8266 and ESP32 are the two most widely used Wi-Fi microcontrollers for hobby and professional IoT projects. Both are made by Espressif, both run on 3.3V logic, both work with the Arduino IDE, and both cost only a few euros. If you have read any of the other articles in this series, you already know the ESP8266 well. The ESP32 is its direct successor. Released in 2016, two years after the ESP8266, it was designed to fix nearly every limitation the ESP8266 has. This article compares the two chips across every relevant dimension: processing power, connectivity, GPIO count, power consumption, price, and known limitations. It also covers when the ESP8266 is still the right choice. Even if it is the older and weaker chip, there are situations where it genuinely wins. No matter how you decide, we have detailed references for both chips: * [Complete ESP8266 reference](https://blog.hirnschall.net/esp8266/) * [Complete ESP32 reference](https://blog.hirnschall.net/esp32/) ### TL;DR **The ESP32 outclasses the ESP8266 in almost every measurable way.** It has a faster dual-core processor, more than four times the RAM, Bluetooth, far more GPIO pins, a much better ADC, and lower deep-sleep power consumption. The ESP8266 however has a lower active power draw and is cheaper. If you are starting a new project and cost is not the deciding factor, use the ESP32. If you are building a simple WiFi-only sensor node on a tight budget, especially one that transmits frequently and spends most of its time awake, the ESP8266 remains a good choice. Below are links to my personal favorites for both chips and development boards: | ESP-12F | NodeMCU ESP8266 | ESP32-WROOM-32 | ESP32-S3 | | --- | --- | --- | --- | | [amazon.com](https://amzn.to/41YJ965) [amazon.de](https://amzn.to/3Olf3GC) | [amazon.com](https://amzn.to/4smbFcB) [amazon.de](https://amzn.to/4eeUhD6) | [amazon.com](https://amzn.to/4tItyDJ) [amazon.de](https://amzn.to/41V075k) | [amazon.com](https://amzn.to/4tEXG2G) [amazon.de](https://amzn.to/4c26zgR) | | #CommissionsEarned | | | | When to Use Which ----------------- The table below covers the most common project scenarios. In general: the ESP8266 is the right pick when WiFi-only connectivity and low cost are the only requirements. The ESP32 is the right pick for everything else. It handles all the same scenarios the ESP8266 covers, and adds Bluetooth, more pins, better analog inputs, and a significantly stronger processor. For any new project where you are not constrained by price or existing ESP8266 infrastructure, the ESP32 is the safer long-term choice. | Use case | ESP8266 | ESP32 | Notes | | --- | --- | --- | --- | | WiFi-only sensor node | ● | ● | Either works. ESP8266 is cheaper; ESP32 has better deep-sleep current. | | Bluetooth required (BLE or Classic) | ○ | ● | ESP8266 has no Bluetooth. ESP32 only. | | Battery-powered, mostly sleeping | ● | ●● | ESP32 deep sleep is 10µA vs 20µA on ESP8266. ESP32 wins here. | | Battery-powered, frequent transmissions | ●● | ● | ESP8266 active draw (~80mA) is lower than ESP32 (~160-240mA). High duty cycle favours ESP8266. | | Multiple sensors / peripherals | ○ | ● | ESP8266 has only 11 freely usable GPIO and 1 ADC channel. ESP32 has 34 GPIO and 18 ADC channels. | | Analog readings (ADC) | ○ | ● | ESP8266 ADC has up to 20% error and degrades further during WiFi activity. ESP32 ADC is 12-bit across 18 channels. | | Audio (I2S) | ~ | ● | Both have I2S, but ESP32's dual-core processor makes audio processing practical. | | CAN bus / Ethernet MAC | ○ | ● | ESP32 only. | | Tight budget / high volume | ●● | ● | ESP8266 is typically $1-3 cheaper per unit. Matters at scale. | | New project, no constraints | ○ | ●● | ESP8266 is effectively in maintenance mode. ESP32 is the strategic choice for anything new. | **Recommended modules:** For the ESP8266, the [ESP-12F1](https://amzn.to/3Olf3GC) is the bare module used in custom PCB designs, while the [NodeMCU ESP82661](https://amzn.to/4eeUhD6) is the easiest starting point for breadboard prototyping. For the ESP32, the bare [ESP32-WROOM-32 module1](https://amzn.to/41V075k) is the standard choice for PCB integration, and the [ESP32-S3 DevKitC1](https://amzn.to/4c26zgR) development board is the equivalent for prototyping. Specs at a Glance ----------------- The table below lists the key datasheet figures for both chips. All ESP8266 numbers are taken from the Espressif ESP8266 datasheet [1] and are consistent with the measurements documented in the [ESP8266 reference article](https://blog.hirnschall.net/esp8266-reference/) in this series. ESP32 numbers are from the ESP32 datasheet [2]. | Specification | ESP8266 | ESP32 | | --- | --- | --- | | CPU | Tensilica Xtensa LX106, single-core | Tensilica Xtensa LX6, dual-core | | Clock speed | 80 MHz (overclockable to 160 MHz) | 160 MHz / 240 MHz | | SRAM | 80 KB user-data RAM, 32 KB instruction cache | 520 KB | | RTC memory | 512 B (inside system RAM) | 16 KB | | Flash | External, up to 16 MB (typically 1-4 MB on modules) | External, up to 16 MB (typically 4 MB on modules) | | Supply voltage | 2.8-3.6 V | 2.3-3.6 V | | WiFi | 802.11 b/g/n, 2.4 GHz, HT20 | 802.11 b/g/n, 2.4 GHz, HT20/HT40 | | Bluetooth | None | Bluetooth 4.2 Classic + BLE | | GPIO (total / freely usable) | 17 / 11 | 34 / 34 | | ADC | 1 channel, 10-bit, 0-1 V input range | 18 channels, 12-bit | | DAC | None | 2 channels, 8-bit | | PWM channels (software) | 8 | 16 | | SPI / I2C / I2S / UART | 2 / 1 / 2 / 2 | 4 / 2 / 2 / 3 | | CAN | None | 1 | | Ethernet MAC | None | 10/100 Mbps | | Touch sensors | None | 10 | | Active current (avg) | ~80 mA | ~160-240 mA | | Deep sleep current | 20 µA | ~10 µA | | Hibernation current | N/A | 5 µA | | Power-off current | N/A | 1 µA | | Operating temperature | -40°C to +125°C | -40°C to +125°C | | Typical price (bare module) | ~$1.50-3 | ~$3-6 | Processing Power ---------------- The ESP8266 uses a single-core Tensilica LX106 processor running at 80 MHz by default, with the option to overclock to 160 MHz. It has 80 KB of user-data RAM and 32 KB of instruction cache. More than enough for typical embedded tasks, but limited when running the WiFi stack alongside complex application code. The ESP32 uses a dual-core Tensilica LX6 processor running at up to 240 MHz, with 520 KB of SRAM. That is more than six times the memory and roughly three times the single-core processing speed of the ESP8266. The performance gap matters most in one specific scenario: running the WiFi stack alongside your own code. On the ESP8266, both compete for the same single core. The WiFi stack is not a background process. It needs regular time slices to handle connections, and if your code holds the CPU too long without yielding, the watchdog timer will reset the chip. This means tasks like sensor processing, web serving, and communication all have to be written with explicit yields (delays etc.), and anything computationally intensive can be difficult to run alongside WiFi reliably. On the ESP32, the WiFi stack typically runs on one core while your application code runs on the other. The result is that complex tasks like parsing data, handling multiple concurrent connections, or running a local web server simply work without the timing considerations required on the ESP8266. | Specification | ESP8266 | ESP32 | | --- | --- | --- | | **CPU** | Tensilica Xtensa LX106, single-core | Tensilica Xtensa LX6, dual-core | | **Clock speed** | 80 MHz (overclockable to 160 MHz) | 160 MHz / 240 MHz | | **SRAM** | 80 KB user-data RAM, 32 KB instruction cache | 520 KB | | **RTC memory** | 512 B | 16 KB | Connectivity ------------ Both chips support 802.11 b/g/n WiFi on the 2.4 GHz band. The ESP32 additionally supports HT40 (40 MHz channel width), which can roughly double WiFi throughput if wider channels are available. For most home automation projects the difference in WiFi performance is not noticeable, but it matters for applications that transfer large amounts of data. The bigger connectivity gap is Bluetooth. **The ESP8266 has no Bluetooth.** The ESP32 supports both Bluetooth Classic (4.2) and Bluetooth Low Energy (BLE). This makes the ESP32 the only option for projects that communicate with smartphones, fitness sensors, BLE beacons, or any device that uses Bluetooth rather than WiFi. Beyond wireless, the ESP32 adds two wired interfaces the ESP8266 lacks entirely: a CAN bus controller and a 10/100 Mbps Ethernet MAC interface (an external PHY chip is still required for Ethernet, but the MAC is integrated). These are relevant for industrial and automotive projects where CAN is the standard communication bus, or for applications that need a wired network connection. Note that for serious CAN bus projects, a dedicated microcontroller such as an STM32 with a proper CAN abstraction layer is usually the better choice. See the [Type-Safe CAN Layer in C++](https://blog.hirnschall.net/can-transport-layer/) article for a practical implementation. GPIOs and Peripherals --------------------- The ESP8266 has 17 pins in total, of which only 11 are freely usable as GPIO. The remaining pins are reserved for boot mode selection, the serial flash interface, or have other constraints that make them unreliable for general use. Several pins must be in a specific state at boot, which means they cannot always be freely driven by external hardware. This is a constraint that requires some planning in circuit design. A detailed breakdown of which pins are usable and which are constrained is covered in the [ESP8266 GPIO reference article](https://blog.hirnschall.net/esp8266-reference/). The ESP32 has 34 GPIO pins with far fewer constraints. It also supports pin multiplexing, which allows most pins to be assigned to different peripherals in firmware. This means I2C, SPI, and UART are not fixed to specific pins but can be mapped as needed. The table below shows the significant difference in peripherals between the two chips. | Peripheral | ESP8266 | ESP32 | | --- | --- | --- | | **ADC** | 1 channel, 10-bit, 0–1 V input range, up to 20% error | 18 channels, 12-bit | | **DAC** | None | 2 channels, 8-bit | | **Touch sensors** | None | 10 capacitive GPIO pins, no external components needed | | **PWM** | 8 channels (software) | 16 channels (software) | | **SPI** | 2 | 4 | | **I2C** | 1 | 2 | | **I2S** | 2 (limited by single-core CPU) | 2 (practical with dual-core CPU) | | **UART** | 2 (one output-only) | 3 | | **IR remote** | None | TX/RX, up to 8 channels | | **Pulse counter** | None | Hardware, full quadrature decoding | | **CAN** | None | 1 | | **Ethernet MAC** | None | 10/100 Mbps (external PHY required) | For projects that involve more than one or two sensors, or that require usable analog readings, the ESP8266's peripheral limitations become a hard constraint. The single ADC channel is a particular pain point. Many projects need to read multiple analog sensors simultaneously, which is simply not possible on the ESP8266 without external multiplexers or external ADCs. For everything else, connecting 5V devices to the ESP8266's 3.3V GPIO requires level shifting, which is covered in the [ESP8266 5V devices article](https://blog.hirnschall.net/esp8266-5v-devices/). The same applies to the ESP32. Built-in Sensors ---------------- The ESP8266 has no built-in sensors while the ESP32 includes three (two working): | Sensor | ESP8266 | ESP32 | | --- | --- | --- | | **Hall effect sensor** | None | This sensor is not working correctly and is considered unreliable. [3] | | **Capacitive touch** | None | 10 GPIO pins, no external components needed, can wake from deep sleep | | **Internal temperature** | None | Measures die temperature only, not ambient temperature | None of these sensors are essential for most projects, but the touch sensor in particular are a nice to have that removes the need for dedicated touch controller ICs in projects that use touch input. Power Consumption ----------------- Power consumption is the one area where the comparison between the two chips is not straightforward. The common assumption that the ESP8266 is better for battery-powered projects is not universally true, and depends on how the project uses the chip. ### Power modes Both chips support multiple power modes. The table below lists the current draw for each: | Mode | ESP8266 | ESP32 | | --- | --- | --- | | Active (WiFi on) | ~80 mA avg, up to 430 mA peak | ~160-240 mA avg | | Modem sleep (CPU on, WiFi off) | ~15 mA | ~20 mA | | Light sleep | ~0.9 mA | ~0.8 mA | | Deep sleep | 20 µA | ~10 µA | | Power-down (RTC off) | 5 µA | N/A | | Hibernation | N/A | 5 µA | | Power-off | N/A | 1 µA | ### Active power: ESP8266 wins When the WiFi radio is on and transmitting, the ESP8266 draws significantly less current than the ESP32. Roughly half on average. For projects where the chip spends most of its time awake and connected, this translates directly into longer battery life. A simple sensor that wakes up every 10 seconds, connects to WiFi, sends a reading, and stays connected will run longer on an ESP8266 than on an ESP32 with an equivalent battery. ### Deep sleep: ESP32 wins The interesting result is in deep sleep. **The ESP32's deep sleep current (10 µA) is lower than the ESP8266's (20 µA).** The ESP32 also adds hibernation mode at 5 µA and power-off mode at 1 µA. Both are unavailable for the ESP8266. For a sensor node that wakes up briefly every few minutes to take a reading and transmit it, then returns to deep sleep, the time spent awake is short and the time spent sleeping dominates the battery budget. In this scenario, which is probably the most common for battery-powered IoT devices, the ESP32 can actually outlast the ESP8266 on the same battery, despite having higher active power draw. The full breakdown of power modes, sleep current measurements, voltage regulator selection, and battery runtime formulas for the ESP8266 is covered in detail in the [ESP8266 on battery article](https://blog.hirnschall.net/esp8266-on-battery/). The same principles apply to the ESP32. ### Deep sleep wakeup One important difference is in how the two chips handle deep sleep wakeup: on the ESP8266, waking from deep sleep requires an external connection from GPIO16 to the RST pin, and the chip performs a full reset on wake where all data in RAM is lost (unless saved in RTC memory, manual work). The ESP32 has multiple wakeup sources including a timer, touch pins, and external GPIO, and retains data in its RTC memory (16 KB) across deep sleep cycles. This makes the ESP32 significantly easier to work with in battery-powered designs that need to preserve state between sleep cycles. Price ----- The ESP8266 is cheaper. This is its clearest and most consistent advantage. Bare ESP-12F modules typically cost around $1.50-3 each depending on where you buy them. ESP32-WROOM-32 modules typically cost $3-6. Development boards (NodeMCU-style) cost somewhat more in both cases due to the added USB interface and voltage regulator circuitry. The price gap has narrowed over the years, and for one-off projects or small quantities the difference is rarely a big concern. Where it matters is in high-volume production: if you are building 500 units of a simple WiFi sensor and the ESP8266 does the job, choose the ESP8266. For prototyping or small runs, the ESP32's is just easier to work with and much less hassle for an additional $2-3. ESP8266 Known Limitations ------------------------- The ESP32 was designed as a direct successor to the ESP8266, which means most of its improvements exist specifically to address problems that ESP8266 users ran into. Understanding these limitations explains why the ESP32 exists in the form it does, and helps set realistic expectations for anyone still using the older chip. * **ADC accuracy:** The ESP8266's ADC has up to 20% measurement error according to the datasheet. This is not a minor calibration issue. It means analog readings can be off by a fifth of the full scale. The error worsens further when the WiFi radio is active, as the RF circuitry affects the ADC. Turning off WiFi before taking analog readings can help, but adds latency and complexity. The ESP32's 12-bit ADC does not have this problem. * **Deep sleep resets the chip:** Waking from deep sleep on the ESP8266 is equivalent to a hardware reset. All data in RAM is lost. Any state that needs to survive a sleep cycle must be written to flash or to the small RTC memory (512 bytes) before sleeping. The ESP32 retains 16 KB of RTC memory across deep sleep, which is enough for most practical state-keeping needs. * **RTC inaccuracy in deep sleep:** The ESP8266's real-time clock drifts noticeably during deep sleep, making it unreliable for precise timed wakeups. For applications that need accurate sleep durations, the actual sleep time must be measured and corrected in firmware. The ESP32's RTC is more accurate. * **No true multitasking:** The ESP8266 has no RTOS by default in the Arduino environment, and even when one is used, there is only one core. The WiFi stack must be given enough CPU time via `delay()` or `yield()` calls, or the watchdog timer will reset the chip. Writing code that does anything time-consuming while maintaining a stable WiFi connection requires planning. The ESP32's dual-core architecture eliminates this problem. * **Limited GPIO and single ADC:** As covered above, 11 freely usable GPIO pins and a single ADC channel is enough for simple projects, but becomes a hard constraint as soon as a project grows. * **deepSleep() reliability:** `ESP.deepSleep()` sometimes does not enter deep sleep correctly. Adding `delay(100)` immediately after the call is a well-known workaround, but it is the kind of chip-level quirk that you actively have to keep track of. None of these limitations are real dealbreakers you cannot work around. But they require workarounds and impose design constraints that simply do not exist on the ESP32. Programming and Ecosystem ------------------------- Both chips are supported by the same set of development tools: * **Arduino IDE:** The most common starting point for both chips. Both have Arduino core libraries maintained by the community, and the vast majority of Arduino-compatible sensor and peripheral libraries work on both. Setting up Arduino IDE for the ESP8266 is covered in the [ESP8266 Arduino IDE article](https://blog.hirnschall.net/esp8266-arduino-ide/). * **ESP-IDF:** Espressif's official IoT Development Framework. More powerful than the Arduino IDE for large or production projects, with full FreeRTOS support and access to all chip features. Officially supported on both chips, though the ESP32 documentation and examples are significantly more complete. * **MicroPython:** Python-based firmware available for both chips. Scripts are largely portable between the two, with minor differences in pin numbering and available peripherals. * **PlatformIO:** Supported for both, and an alternative to the Arduino IDE for larger projects with dependency management and build automation. One important caveat: **ESP8266 and ESP32 Arduino code is not drop-in compatible.** Pin numbers, some API calls, and library implementations differ between the two cores. Code written for the ESP8266 will usually need adjustments to run on an ESP32, particularly anything that uses specific GPIO numbers, the ADC, deep sleep, or WiFi connection handling. The changes are typically small though. In terms of community resources, the ESP8266 has a slight historical advantage. It has been around longer and has a larger base of existing tutorials and forum threads. In practice this difference is not that significant, as the ESP32 ecosystem has matured and most questions have well-documented answers. ESP32 Family Comparison ----------------------- The ESP32 is not a single chip but a growing family. The original ESP32 (dual-core LX6, WiFi + BT 4.2) remains widely used, but Espressif has released several variants that extend the family in different directions: * **ESP32-S2:** Single-core, WiFi only, no Bluetooth. Lower cost than the original ESP32, with native USB support. A sensible choice when Bluetooth is not needed and cost matters. * **ESP32-S3:** Dual-core LX7, WiFi + BLE 5.0, with hardware acceleration for neural network inference. Suited for edge AI and machine learning applications. * **ESP32-C3:** RISC-V single-core, WiFi + BLE 5.0. Very low cost, native USB. A direct replacement for the ESP8266 in many scenarios, with Bluetooth added. * **ESP32-H2:** RISC-V, no WiFi, but supports BLE 5.0, Zigbee, and Thread (Matter). Designed for smart home mesh networking rather than direct internet connectivity. **The ESP8266 is in maintenance mode.** Espressif continues to provide security patches and basic support, but no new variants are planned and active development has effectively stopped. For any new design, the ESP32 family is the strategic choice — it has a longer support horizon, a broader feature set, and a growing ecosystem of variants that cover almost any IoT use case. Further Reading --------------- This article is part of a complete ESP8266 guide. The articles below cover the topics referenced throughout this comparison in full detail: * [ESP8266 Complete Guide — WiFi for Arduino Projects](https://blog.hirnschall.net/esp8266/) * [ESP8266 GPIO Reference: Pinout, Boot Modes and Circuit](https://blog.hirnschall.net/esp8266-reference/) * [Run ESP8266 on Battery — Deep Sleep and Power Modes](https://blog.hirnschall.net/esp8266-on-battery/) * [ESP8266 PCB Layout — Antenna Keep-Out and Grounding](https://blog.hirnschall.net/esp8266-pcb-design/) * [ESP8266 Level Shifting — Connect 5V Devices Safely](https://blog.hirnschall.net/esp8266-5v-devices/) * [Flash ESP8266 Without USB Adapter — Use an Arduino](https://blog.hirnschall.net/esp8266-arduino-ide/) * [ESP8266 Web Server — 20 Lines Arduino Code Example](https://blog.hirnschall.net/esp8266-webserver/) --- ## [CNC Mill Concept — Software Architecture & CAN](https://blog.hirnschall.net/cnc-mill-concept/software-architecture-can/) meta description: Firmware for distributed sensor nodes on STM32G4: YAML-driven CAN map code generation, type-safe CanManager transport, deterministic serialization, and DBC export. date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- # [CNC Mill Concept: Software Architecture & CAN](https://blog.hirnschall.net/cnc-mill-concept/software-architecture-can/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Firmware for distributed sensor nodes on STM32G4: YAML-driven CAN map code generation, type-safe CanManager transport, deterministic serialization, and DBC export. meta title: CNC Mill Concept — Software Architecture & CAN date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- Introduction ------------ This post is part of the [CNC Mill Concept hub](https://blog.hirnschall.net/cnc-mill-concept/). It covers the firmware architecture for the distributed sensor nodes: the design invariants, the layered software stack, the YAML-driven CAN map and code generation workflow, the type-safe CanManager transport layer, and the testing strategy. The type-safe CAN transport layer concept described here has already been implemented in the [CAN transport layer post](https://blog.hirnschall.net/can-transport-layer/). Note: this section is work in progress. Code snippets are conceptual and for reference only. Design Invariants ----------------- 1. Sensors return CAN message types defined in the CAN map. 2. Only CAN-map messages may be transmitted. 3. Any CAN map change affecting message layout must break compilation. 4. No raw CAN frame sending. 5. Strict separation of sensor acquisition and transport. 6. Prefer static allocation for fixed-size objects. 7. No dynamic allocation in real-time paths. 8. CAN map is the single source of truth for message layout. 9. Serialization must be deterministic and compiler-independent. Software Stack -------------- Fig. 1 shows the full software stack from user application down to hardware. ![Software stack: application layer (control logic, sensor framework, diagnosis), transport layer (CanManager, generated CAN map), low-level modm drivers, and STM32G4 hardware.](https://blog.hirnschall.net/cnc-mill-concept/software-architecture-can/resources/img/software-stack.jpg) Figure 1: Software stack: application layer (control logic, sensor framework, diagnosis), transport layer (CanManager, generated CAN map), low-level modm drivers, and STM32G4 hardware. ### Application Layer * Control logic * State estimation * Supervision * Diagnosis handling * No knowledge of serialization ### Sensor Framework Shared across all sensor PCBs. Responsibilities: * Call `init()` * Periodically call `update()` * Poll `hasDataChanged()` * Forward typed CAN message to CAN manager * Node supervision * Heartbeat handling ``` sensor.update(); if(sensor.hasDataChanged()) { auto msg = sensor.getProcessed(); canManager.send(msg); } ``` ### Transport Layer CAN manager: * Uses `Message::id` * Uses `Message::dlc` * Calls generated `serialize()` * Calls generated `deserialize()` * Never accesses individual fields * Never performs bit shifting * Never constructs payload manually ### Node-Specific CAN Manager The CAN manager will later be templated on node identity: ``` using Node = Nodes::ECU; CanManager canManager; ``` Node identity will be defined in the CAN map YAML and code-generated. Future specialization will allow: * Compile-time filter configuration * Compile-time dispatch selection * Elimination of unused message handling * No runtime branching based on node type Implementation is deferred. ### Low-Level Drivers * Hardware abstraction via modm * FDCAN peripheral configuration * DMA / timers / SPI access Compile-Time Enforcement ------------------------ Only message types registered in the CAN map may be transmitted. ``` template concept CanMessage = IsCanMessage::value; ``` CAN manager API: ``` template void send(const Message& msg); ``` If a message is not registered, compilation fails. Any change to field layout in YAML modifies the generated header and forces downstream recompilation. CAN Map Specification --------------------- The CAN map is the single source of truth for all messages on the CAN-FD bus. Messages are declared in YAML and code is generated. Messages are grouped by logical node namespace. ### YAML Specification ``` can: can0 version: 1 namespaces: - name: SensorNode messages: - name: StrainData id: 0x201 dlc: 8 fields: - { name: fx, type: int32, bits: 16 } - { name: fy, type: int32, bits: 16 } - { name: fz, type: int32, bits: 16 } - { name: valid, type: uint8, bits: 1 } ``` The YAML file defines: output header name, version, namespaces, message ID, DLC, field names, bit width, type, optional scale (float), and optional enum values. ### Strict DLC Rule Total bits are tightly packed. Required bytes: ``` required_bytes = ceil(total_bits / 8) ``` Validation rule: ``` required_bytes == dlc ``` Unused bits inside the final byte are allowed. Unused bytes are not allowed. ### Nodes and Subscribers The CAN map defines all nodes explicitly. YAML must contain a `nodes` list and a `subscribers` list per message. ``` nodes: - ECU - SensorNode - Diagnosis namespaces: - name: SensorNode messages: - name: StrainData id: 0x201 dlc: 7 subscribers: - ECU - Diagnosis ``` Rules: * `subscribers` is mandatory * Publisher is NOT implicitly a subscriber * If no subscribers are defined, generation fails * Subscriber names must exist in `nodes` list * Generation validates spelling and consistency Future generator extension will use this to produce node-specific filter tables, dispatch specialization, and compile-time elimination of unused message handlers. ### Generated C++ Message Generator produces `can0.hpp`: ``` namespace SensorNode { struct StrainData { static constexpr uint16_t id = 0x201; static constexpr uint8_t dlc = 8; #ifdef CANMAP_INCLUDE_MESSAGE_NAMES static constexpr const char* name = "SensorNode::StrainData"; #endif int32_t fx; // packed: 16 bits int32_t fy; // packed: 16 bits int32_t fz; // packed: 16 bits uint8_t valid; // packed: 1 bit }; } ``` The struct represents physical values only. Packing is handled separately. ### Scaled Float Support YAML: ``` - name: temperature type: float bits: 16 scale: 0.1 ``` Generated struct: ``` float temperature; // packed: 16 bits, scale: 0.1 static constexpr float temperature_scale = 0.1f; ``` Serialization: ``` int32_t raw = static_cast( std::round(msg.temperature / temperature_scale)); packSigned(data, raw); ``` Float is never transmitted as IEEE-754. It is transported as a scaled integer. ### Enum Support YAML: ``` - name: state type: uint8 bits: 3 enum: 0: Idle 1: Running 2: Error ``` Generated struct: ``` enum class State : uint32_t { Idle = 0, Running = 1, Error = 2, }; State state; ``` Serialization uses the underlying integer value. ### Serialization Model Generator produces `serialize()` and `deserialize()`: ``` template<> inline void serialize(const Message& msg, uint8_t* data); template<> inline void deserialize(Message& msg, const uint8_t* data); ``` All packing uses `pack_utils.hpp`. Properties: * Width ≤ 32 bits per field * Offset + Width ≤ 64 * Little-endian deterministic layout * No `reinterpret_cast` * No compiler bitfields ### DBC Generation Generator also produces `can0.dbc`, including message definitions, signal scaling, enum value mappings, and a CAN map version comment. Used for SavvyCAN or other tools. CAN Manager ----------- The CAN manager is the transport layer between application logic and FDCAN. It is strictly transport-only. It does not: * Construct payloads * Interpret physical values * Perform bit shifting * Access individual fields ### Transmit Path Public API: ``` template void send(const Message& msg); ``` Transmit sequence: 1. Compile-time check: `Message` satisfies `CanMessage` concept. 2. Allocate modm CAN frame. 3. Set frame ID to `Message::id`. 4. Set frame length to `Message::dlc`. 5. Call generated `serialize(msg, frame.data)`. 6. Submit frame to FDCAN driver. ``` template void CanManager::send(const Message& msg) { modm::can::Message frame( static_cast(Message::id), Message::dlc); frame.setExtended(false); serialize(msg, frame.data); transmit(frame); } ``` The CAN manager never touches payload fields directly. ### Receive Path Reception is type-safe and compile-time constrained: 1. FDCAN interrupt receives raw frame. 2. CAN manager matches frame ID against generated message list. 3. Matching message type is instantiated. 4. `deserialize()` reconstructs typed message. 5. Typed message is dispatched. Future implementation will use compile-time node specialization: ``` template struct NodeDispatch; template<> struct NodeDispatch { static void handle(const SensorNode::StrainData& msg); static void handle(const Diagnosis::Request& msg); }; ``` CAN manager will forward deserialized messages to: ``` NodeDispatch::handle(msg); ``` This avoids runtime switch statements, dynamic dispatch, and branch-heavy receive logic. Only messages subscribed by the node will generate handlers. No raw buffer handling is exposed to the application layer. ### Reception Filtering * Only CAN IDs defined in the CAN map are accepted * Unknown IDs: + ignored + or counted as diagnostic error + never forwarded as raw frame ### Responsibilities * Initialize FDCAN * Configure filters * Handle TX queue * Handle RX interrupt * Perform serialize/deserialize * Monitor bus state (error passive / bus-off) * Provide error counters for diagnosis Raw frame APIs must remain private. ### Determinism * No dynamic allocation in send/receive path * Bounded execution time * No blocking in interrupt context * No protocol logic inside CAN manager CanManager Implementation ------------------------- ### Design Goals 1. Complete abstraction of FDCAN hardware from application code. 2. Zero-cost compile-time type checking of message permissions. 3. Automatic hardware initialization via template parameters. 4. Automatic filter configuration from CAN map metadata. 5. No dynamic allocation in transmission or reception paths. 6. Deterministic bounded execution time. 7. Separation of transport logic from application logic. ### Software Stack Fig. 2 shows the CanManager stack from user application down to hardware, with the YAML-to-generated-code flow and the send/receive data flows. ![CanManager software stack: user application works with CAN map structs via send() and processMessages()](https://blog.hirnschall.net/cnc-mill-concept/software-architecture-can/resources/img/can-impl-software-stack.jpg) Figure 2: CanManager software stack: user application works with CAN map structs via send() and processMessages() ### Layer Descriptions #### User Application Layer * Direct field access: `msg.fx = 123` * Type-safe API: `can.send(msg)` * Automatic message processing: `can.processMessages()` * No bit manipulation required * No hardware knowledge needed #### CanManager (Transport Layer) * **send(msg)** — type-checked message transmission with automatic serialization * **processMessages()** — polls hardware FIFOs, deserializes, and dispatches to handlers * **Constructor** — automatic hardware initialization and filter configuration #### Generated Code Layer * **can0.hpp** — message struct definitions with metadata * **can0\_serialize.hpp** — deterministic pack/unpack functions * **NodeTraits** — compile-time permissions (TxMessages, RxMessages) #### modm HAL Layer * FDCAN peripheral drivers * GPIO configuration * Interrupt handling * Platform-specific implementations #### Hardware Layer * STM32G4 FDCAN peripheral * CAN bus interface * 1 Mbit/s nominal bitrate * 5 Mbit/s data phase (CAN-FD) ### Data Flows #### Send Flow (TX) 1. User creates message struct 2. Calls `can.send(msg)` 3. CanManager checks compile-time permissions 4. Automatic serialization via generated code 5. modm transmits frame 6. Hardware sends to CAN bus #### Receive Flow (RX) 1. CAN bus receives frame 2. Hardware stores in RX FIFO 3. User calls `can.processMessages()` 4. CanManager polls FIFO 5. Matches ID to message type 6. Automatic deserialization 7. Calls user's `onMessage(msg)` handler #### Initialization Flow 1. GPIO pin connection 2. FDCAN peripheral initialization 3. CAN-FD mode configuration 4. Reads RxMessages from NodeTraits 5. Configures hardware filters automatically ### Key Abstraction Principle What the user sees: * CAN map structs (`SensorNode::StrainData`) * Normal C++ types (`int32_t`, `float`, `uint8_t`) * Simple API (`send()`, `processMessages()`) What is hidden: * `modm::can::Message` * Bit shifting and masking * Hardware register access * Filter configuration * Serialization logic Diagnosis --------- * Diagnosis messages are part of the CAN map * Design principles: + central controller requests diagnostic data + no ad-hoc diagnostic frames + deterministic bounded execution + no dynamic allocation in real-time path * Version reporting: + firmware embeds Git commit hash + CAN map version embedded + board version embedded + returned via diagnosis request Memory Strategy --------------- * Prefer static allocation * No dynamic allocation in real-time paths * Heap usage allowed during initialization * Allocation failures must be handled explicitly Software Tests -------------- Testing is mandatory for serialization correctness and protocol stability. Testing is split into compile-time validation, host-side unit tests, and integration tests. ### Compile-Time Validation The generator enforces: * Unique CAN IDs * Strict DLC rule (`ceil(bits/8) == dlc`) * Enum value range validity * Float requires scale C++ enforces: * `CanMessage` concept restriction * Message registration * Struct regeneration on CAN map change Any change to YAML must trigger full recompilation. ### Serialization Unit Tests (Host) Host-side tests verify bit packing correctness, bit offsets, signed extension correctness, scaled float conversion, enum conversion, boundary conditions, and zero initialization of unused bits. ``` Message msg{}; msg.fx = 123; uint8_t buffer[Message::dlc]; serialize(msg, buffer); Message decoded{}; deserialize(decoded, buffer); assert(decoded.fx == msg.fx); ``` Round-trip serialization must be lossless within scaling precision. ### Edge Case Tests For each field: minimum value, maximum value, zero, negative (if signed), and float rounding boundary. Float tests must verify: ``` abs(original - decoded) <= scale ``` ### Integration Tests Integration test stub verifies: * Message ID matches expected * DLC matches expected * Serialization matches DBC definition * No padding beyond required bytes * CAN manager rejects non-registered types Optional: * Loopback FDCAN test * Stress test at full bus rate * Bus-off recovery behavior ### Regression Safety The CAN map is versioned. Any change to bit width, field order, scaling, or enum definition changes generated headers and forces rebuild. Host tests must run in CI before deployment. ### Test Philosophy * Deterministic serialization * No undefined behavior * No compiler-dependent layout * Explicit validation of protocol contract --- ## [Perlin Noise Explained — How It Works & When to Use](https://blog.hirnschall.net/perlin-noise/) meta description: How Perlin noise generates smooth randomness — gradient noise vs value noise, computational cost, and practical use in terrain generation and animation. date published: 27.08.2020 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [Perlin noise: What is it, how to use it, and why it's better than value noise.](https://blog.hirnschall.net/perlin-noise/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: How Perlin noise generates smooth randomness — gradient noise vs value noise, computational cost, and practical use in terrain generation and animation. meta title: Perlin Noise Explained — How It Works & When to Use date published: 27.08.2020 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- What is Perlin Noise -------------------- Perlin noise is a type of gradient noise that can be used to generate "smooth" randomness in one or more dimensions. This is why it is often used in the movie and special effects industry for procedural texture generation. It was developed by Ken Perlin in 1983. He was later awarded an Academy Award for Technical Achievement for creating the algorithm. ![Visualization of 1d perlin noise](https://blog.hirnschall.net/perlin-noise/resources/img/perlin-noise-1d.png) (a) plot of Perlin Noise ![Visualization of 1d random variables](https://blog.hirnschall.net/perlin-noise/resources/img/random-1d.png) (b) plot of random numbers Figure 1.1: Comparing Perlin Noise to random numbers (p5js random() and noise()) As you can see in fig 1.1 \(noise(x),x\in\mathbb{R}\) is a continuous function. In other words, Perlin noise can be used to generate a more "controlled" form of randomness. ### How Gradient Noise Works To generate the random, continuous function \(noise(x):\mathbb{R}^n\to\mathbb{R}^n\)  we start with a set of random vectors (gradients) $$g\_u\in\mathbb{R}^n,u\in\mathbb{Z}^n.$$ As the name gradient noise implies we now set $$noise'(u)=g\_u$$ $$noise(u)=0.$$ We can now interpolate to find \(noise(x),\forall x\in\mathbb{R}^n\). ![How 1d gradient noise works](https://blog.hirnschall.net/perlin-noise/resources/img/noise-1d-gradients.png) Figure 2.1: Resulting interpolant for one-dimensional noise As you can see in fig. 2.1 the resulting function \(noise(x)\) is zero if \(x=u\). To get a better result we take a linear combination of \(M\) weighted noise functions with different frequencies $$Noise(x)=\sum\_{i=0}^{M-1}a\_i\cdot noise(f\_i\cdot x)$$ where \(a\_i\) is a weight (amplitude) and \(f\_i\) is a frequency. These terms are called octaves. [ Animation of linear combinations of perlin noise ](resources/vid/linearcombination/linearcombination.webm) ### Octaves used in this animation: ![perlin noise octave 1](https://blog.hirnschall.net/perlin-noise/resources/img/noise-o1.png) ![perlin noise octave 2](https://blog.hirnschall.net/perlin-noise/resources/img/noise-o2.png) ![perlin noise octave 3](https://blog.hirnschall.net/perlin-noise/resources/img/noise-o3.png) ![perlin noise octave 4](https://blog.hirnschall.net/perlin-noise/resources/img/noise-o4.png) ### Result: ![perlin noise combination of 4 octaves](https://blog.hirnschall.net/perlin-noise/resources/img/noise-total.png) ### How Value Noise Works The apparently "often confused with" value noise works in a similar way to gradient noise. We start with a grid of random points $$g\_u\in\mathbb{R}^n,u\in\mathbb{Z}^n.$$ In contrast to gradient noise we demand that $$noise(u)=g\_u.$$ As with gradient noise, we then use the interpolant to calculate \(noise(x),x\in\mathbb{R}^n\). To get a better result we can use a linear combination of \(M\) different octaves (with different weights, \(a\_i\) and frequencies \(f\_i\)). $$Noise(x)=\sum\_{i=0}^{M-1}a\_i\cdot noise(f\_i\cdot x)$$ ### Gradiant Noise vs Value Noise In short: As gradient (Perlin) noise emphasizes frequencies around and above the grid spacing it will, in general, lead to a visually more appealing result. One problem with value noise can be its random nature. The result might look ugly if too many gridpoints have a similar value. For more information on gradient noise vs value noise take a look at [this answer on StackExchange.](https://math.stackexchange.com/q/184153) ### Computational complexity As we have seen above, to evaluate \(noise(x),x\in\mathbb{R}^n\) (n-dimensional Perlin noise), we have to interpolate between the \(2^n\) nearest grid points. We will later see that this can be done by calculating the dot-product of the gradient assigned to each of these points and \(x\). Afterward, we interpolate linearly between the results and apply an ease curve. As all of these operations scale with complexity \(\mathcal{O}(n)\) the algorithm scales with complexity \(\mathcal{O}(2^n)\) for \(n\) dimensions. When to use Perlin Noise? ------------------------- Perlin noise is typically used to make things look more realistic or live like. As most things in nature do not change instantaneous, using a normal random number generator is not a good option. As we have seen Perlin noise is a continuous ("smooth") function and therefore the result looks much more natural. For more flexibility, we may use different dimensions of Perlin noise. You can see some example use cases below: | Dimension | Visualization | Example usage | | --- | --- | --- | | 1 | 1d perlin noise | 1d Perlin noise can be used to make a straight line look hand-drawn or make movement look more realistic (no instant speed changes, no perfectly straight lines, etc.) | | 2 | 2d perlin noise | 2d perlin noise for terrain generation in minecraft 2d Perlin noise is often used to generate terrain, textures, or flowfields. | | 3 | 3d perlin noise | 3d perlin noise for cave generation in minecraft 3d Perlin noise can be used to generate caves (like those in Minecraft) or for animating textures etc. that use 2d noise | How to use Perlin Noise ----------------------- ### Implementation If the language you are using does not have Perlin noise as a built-in function you can either use [Ken Perlin's reference implementation](https://mrl.nyu.edu/~perlin/noise/) to implement it yourself or check [github.com](https://github.com/search?q=perlin+noise) if someone else already did all the work. If you are using unity you can use Mathf.PerlinNoise(float x, float y). ### Using Perlin noise (for animations) In contrast to \(random()\), \(noise(t)\) will return the same value for a given \(t\) no matter how often we call it. To get different results we increment \(t\). We can control the smoothness by how quickly we increment \(t\). ![perlin noise timestep expanation](https://blog.hirnschall.net/perlin-noise/resources/img/timesteps.png) Figure 3.1: Timesteps [1] If we need more than one noise value \(x:=noise(t\_1)\) and \(y:=noise(t\_2)\) we can use a large offset between \(t\_1\) and \(t\_2\). You can see how this works in fig. 3.2 below: ![multiple different perlin noise results](https://blog.hirnschall.net/perlin-noise/resources/img/different-results.png) Figure 3.2: Different values [1] ### Mapping the result Depending on the implementation the noise function will return a value \(x\) in a given intervall\([a,b],a [...] EEPROM.begin(sizeof(settings)); ``` Note: You can download the complete example below. ### How to Read from EEPROM ### EEPROM.get The simplest way to read data from EEPROM is using the EEPROM.get() function. ``` unsigned int addr = 0; EEPROM.get(addr, settings); //read data from array in ram and cast it into struct called settings ``` ### EEPROM.read If necessary, we can also read individual bytes from EEPROM using EEPROM.read(): ``` unsigned int addr = 0; for(int i=0; i`. The library listens on the local network using mDNS and allows the Arduino IDE to push firmware to the device over WiFi exactly as it would over a USB connection. The device appears in the IDE under Tools → Port as a network port once it is running and connected to the same network as the pc running the Arduino IDE. The ArduinoOTA library provides several functions, `setHostname()`, `setPassword()`, and the callbacks, that must all be called before `ArduinoOTA.begin()`. Calling them afterwards does not work. Let's quickly go through each of the functions provided by ArduinoOTA before we take a look at a full working example: ### setHostname `ArduinoOTA.setHostname()` sets the name the ESP advertises on the network. This is what appears in the Arduino IDE port list and what the IDE uses to locate the device. Without a hostname, the device falls back to a default name derived from its MAC address, which can make it difficult to identify out ESP when multiple OTA devices are on the same network. The snippet below sets the hostname to "esp32-ota". The same hostname is also registered with mDNS, so our ESP32 becomes reachable at `esp32-ota.local` from any device on the network, not just the Arduino IDE. ``` ArduinoOTA.setHostname("esp32-ota"); ``` ### setPassword `ArduinoOTA.setPassword()` requires the Arduino IDE to provide a matching password before accepting a firmware upload. Without a password set, anyone on the same network can push arbitrary firmware to the ESP. We will allways set a password when working with OTA. ``` ArduinoOTA.setPassword("otapassword"); ``` The password uses MD5 as a hashing function. MD5 is not really considered safe anymore as it has been broken for some time now. It is however good enough to prevent anyone from uploading firmware. In production, we might want to use some other way to verify the authenticity of the firmware. This is something we touch on in the HTTP OTA section at the end of this post. When uploading from the Arduino IDE with a password set, the IDE prompts for the password on first use of that network port. It stores the password per port, so subsequent uploads to the same device do not prompt again. ### Callbacks ArduinoOTA provides four callbacks that trigger at different stages of an update. In the examples below we will register them as lambda functions before calling `ArduinoOTA.begin()`. None are required for OTA to function, but the error callback is worth setting up since it is the primary diagnostic tool when something goes wrong. `ArduinoOTA.onStart()` fires when the IDE initiates an upload. The callback receives no arguments. We can call `ArduinoOTA.getCommand()` inside it to determine whether the update is a firmware flash (`U_FLASH`) or a filesystem update. If the application has a filesystem mounted, this is the right place to unmount it before the write begins. ``` ArduinoOTA.onStart([]() { String type = ArduinoOTA.getCommand() == U_FLASH ? "firmware" : "filesystem"; Serial.println("OTA start: " + type); }); ``` `ArduinoOTA.onEnd()` fires when the transfer completes successfully, just before the device reboots into the new firmware. This is the right place to flush any state or close open resources that should not be interrupted mid-update. ``` ArduinoOTA.onEnd([]() { Serial.println("OTA complete. Rebooting..."); }); ``` `ArduinoOTA.onProgress()` fires repeatedly during the transfer. It receives the number of bytes transferred so far and the total size. During development this is useful to confirm the upload is progressing; on a deployed device with no serial connection it serves no practical purpose. ``` ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { Serial.printf("Progress: %u%%\n", progress * 100 / total); }); ``` `ArduinoOTA.onError()` fires when the update fails. It receives an `ota_error_t` value. The five error codes cover: authentication failure (`OTA_AUTH_ERROR`), failure to begin the OTA write (`OTA_BEGIN_ERROR`), failure to establish the connection (`OTA_CONNECT_ERROR`), failure during data transfer (`OTA_RECEIVE_ERROR`), and failure to finalise the write (`OTA_END_ERROR`). In practice, `OTA_AUTH_ERROR` means a wrong password was provided, and `OTA_BEGIN_ERROR` almost always means the compiled binary is too large for the OTA partition. ``` ArduinoOTA.onError([](ota_error_t error) { Serial.printf("OTA error [%u]: ", error); if (error == OTA_AUTH_ERROR) Serial.println("Auth failed"); else if (error == OTA_BEGIN_ERROR) Serial.println("Begin failed"); else if (error == OTA_CONNECT_ERROR) Serial.println("Connect failed"); else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive failed"); else if (error == OTA_END_ERROR) Serial.println("End failed"); }); ``` In the examples above, we used serial output. If the ESP is in some unreachable location or simply tricky to get to, we might want to display the callback info in the webui hosted on the ESP itself or use the built in LED. ### begin `ArduinoOTA.begin()` starts the OTA service. We call it in `setup()` after the WiFi connection is established and after all configuration calls and callbacks have been registered. Once called, the device begins advertising itself on the network and is ready to accept uploads. ``` ArduinoOTA.begin(); ``` Note that `begin()` starts mDNS internally using the hostname set with `setHostname()`. There is no need to call `MDNS.begin()` separately. If the application also runs a web server that uses mDNS, as in the [ESP32 web server post](https://blog.hirnschall.net/esp32-webserver/), calling `MDNS.begin()` a second time after `ArduinoOTA.begin()` will not cause an error but is redundant. ### handle `ArduinoOTA.handle()` processes incoming OTA connections. We call it at the top of every `loop()` iteration. When no OTA activity is pending it returns immediately, so its overhead at idle is negligible. ``` void loop() { ArduinoOTA.handle(); // application code } ``` The critical constraint is that `handle()` must be reached on every loop iteration without a long interruption. If the `loop()` blocks, e.g. because of a large `delay()` or a `while` loop waiting on a sensor, the OTA connection may time out, failing the upload. The Arduino IDE reports this as a generic upload error with no indication that a blocking loop is the cause, which makes it one of the more frustrating issues to diagnose the first time it happens. We can fix this by replacing blocking waits with `millis()`-based timing. Instead of holding the CPU for a period, we check when the last task ran and whether the interval time has passed on each iteration. ``` void loop() { ArduinoOTA.handle(); static unsigned long lastTask = 0; if (millis() - lastTask >= 1000) { // sensor reads, state updates, etc. lastTask = millis(); } } ``` Note: `handle()` can only work if the WiFi connection is active. If the application disconnects from WiFi and does not reconnect, OTA will not be reachable until the connection is back up again. A reconnect loop on WiFi disconnect is probably a good idea for any device that relies on OTA updates. ### Uploading from the Arduino IDE Once the sketch is running on the ESP32 with ArduinoOTA initialised and the device connected to WiFi, open the Arduino IDE and navigate to Tools → Port. The device appears as a network port labelled with the hostname and its IP address, for example `esp32-ota at 192.168.1.42`. Select it as you would a USB port and click Upload. On the first upload to a password-protected device, the IDE prompts for the OTA password. Enter the password set with `setPassword()`. The IDE stores it for that port so subsequent uploads do not prompt again. If the device does not appear in the port list, the most common causes are: the device and the computer are not on the same network, mDNS is blocked by a firewall or network policy, or the device is in a blocking state and `handle()` is not being reached. To debug the issue, we can connect to the Serial output, check the webui output or use the LED given the OTA callbacks. ### Full ArduinoOTA Example The following is a complete working sketch combining everything we discussed above. It connects to WiFi, configures ArduinoOTA with a hostname and password, registers all four callbacks, and runs a non-blocking `millis()`-based loop alongside the OTA handler. We can use this sketch as a good starting point for any project that needs OTA. ``` #include #include const char* WIFI_SSID = "MyWiFi"; const char* WIFI_PASSWORD = "MyPassword"; const char* OTA_PASSWORD = "otapassword"; const char* OTA_HOSTNAME = "esp32-ota"; void setup() { Serial.begin(115200); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("Connected. IP: "); Serial.println(WiFi.localIP()); ArduinoOTA.setHostname(OTA_HOSTNAME); ArduinoOTA.setPassword(OTA_PASSWORD); ArduinoOTA.onStart([]() { String type = ArduinoOTA.getCommand() == U_FLASH ? "firmware" : "filesystem"; Serial.println("OTA start: " + type); }); ArduinoOTA.onEnd([]() { Serial.println("OTA complete. Rebooting..."); }); ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { Serial.printf("Progress: %u%%\n", progress * 100 / total); }); ArduinoOTA.onError([](ota_error_t error) { Serial.printf("OTA error [%u]: ", error); if (error == OTA_AUTH_ERROR) Serial.println("Auth failed"); else if (error == OTA_BEGIN_ERROR) Serial.println("Begin failed"); else if (error == OTA_CONNECT_ERROR) Serial.println("Connect failed"); else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive failed"); else if (error == OTA_END_ERROR) Serial.println("End failed"); }); ArduinoOTA.begin(); Serial.println("OTA ready. Hostname: " + String(OTA_HOSTNAME)); } void loop() { ArduinoOTA.handle(); static unsigned long lastTask = 0; if (millis() - lastTask >= 1000) { // application code here lastTask = millis(); } } ``` HTTP OTA -------- ArduinoOTA requires the device and the computer performing the upload to be on the same local network. This works during development but not for a device deployed at a remote location, behind NAT, or embedded in a product that ships to an end user. For those cases, HTTP OTA is the appropriate approach. HTTP OTA reverses the direction of the transfer. Rather than the IDE pushing firmware to the device, the device fetches a firmware binary from a URL and flashes itself. The ESP32 Arduino core provides `HTTPUpdate.h` for this. Because the device initiates the connection outbound, it works over the internet from behind any network configuration without requiring inbound port forwarding or a fixed IP address. The binary is the compiled firmware exported from the Arduino IDE using Sketch → Export Compiled Binary, which produces a `.bin` file in the sketch folder. That file is hosted on an HTTP or HTTPS server. The device fetches it with `httpUpdate.update()`, writes it to the inactive OTA partition, and reboots into it on success. HTTP OTA can be made automatic by adding a version check. The device periodically polls a lightweight endpoint that returns the current firmware version number. If the version is newer than the one running on the device, the update is fetched and applied. This is the standard pattern for managing a fleet of deployed devices without physical access to any of them. Automatic updates require careful implementation. A misconfigured server, a corrupted binary, or a compromised endpoint can push bad firmware to every device in a fleet simultaneously. The minimum requirements for any production setup are: the binary must be served over HTTPS with a valid certificate, the device must verify the binary against a checksum or signature before applying it, and the rollback mechanism built into the ESP32 bootloader must be enabled so a failed update falls back to the previous firmware rather than leaving the device in an unbootable state. The ESP32 Arduino core documentation covers `HTTPUpdate.h` and the rollback API in detail. Where & What to Buy ------------------- Below are links1 to the ESP modules and development boards I personally tend to use. If you are unsure about which one to choose, [check out this comparison post!](https://blog.hirnschall.net/esp8266-vs-esp32/) | ESP-12F | NodeMCU ESP8266 | ESP32-WROOM-32 | ESP32-S3 | | --- | --- | --- | --- | | [amazon.com](https://amzn.to/41YJ965) [amazon.de](https://amzn.to/3Olf3GC) | [amazon.com](https://amzn.to/4smbFcB) [amazon.de](https://amzn.to/4eeUhD6) | [amazon.com](https://amzn.to/4tItyDJ) [amazon.de](https://amzn.to/41V075k) | [amazon.com](https://amzn.to/4tEXG2G) [amazon.de](https://amzn.to/4c26zgR) | | #CommissionsEarned | | | | --- ## [CNC Mill Concept — Goals, Philosophy & System Architecture](https://blog.hirnschall.net/cnc-mill-concept/goals-philosophy-system-architecture/) meta description: Design goals for a CNC mill targeting ≤ 10 µm accuracy in an apartment, the philosophy behind the structural and control choices, and the five-subsystem architecture. date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- # [CNC Mill Concept: Goals, Philosophy & System Architecture](https://blog.hirnschall.net/cnc-mill-concept/goals-philosophy-system-architecture/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Design goals for a CNC mill targeting ≤ 10 µm accuracy in an apartment, the philosophy behind the structural and control choices, and the five-subsystem architecture. meta title: CNC Mill Concept — Goals, Philosophy & System Architecture date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- Introduction ------------ This post is part of the [CNC Mill Concept hub](https://blog.hirnschall.net/cnc-mill-concept/). It covers the top-level goals and constraints that shaped every subsequent design decision, the philosophy that governs tradeoffs throughout the machine, and the system architecture that partitions responsibility across the five main subsystems. Goals ----- The machine targets fixed-bed CNC milling of aluminium and steel. The primary requirements are: * **Machining accuracy and repeatability**: ≤ 10 µm. * **Apartment-compatible operation**: low airborne noise emission and low structure-borne vibration transmitted to the building. * **High observability**: the machine state must be measurable, not assumed. * **Graceful degradation**: the preferred failure response is feed reduction or spindle adaptation, not part scrap. Constraints ----------- The residential context imposes hard limits that rule out common heavy-machine approaches: * **Floor load**: residential building limit of approximately 150–200 kg/m². No concrete or mineral-cast bed. * **Total machine mass**: must remain controlled — mass is not a free variable for stiffness. * **Safety**: no reliance on software for safety-critical functions. * **Noise**: noise-sensitive environment is a primary constraint, not an afterthought. * **Availability**: downtime is acceptable; accuracy is prioritised over uptime. Design Philosophy ----------------- A small number of principles govern the design wherever competing approaches exist: * **Accuracy over brute stiffness**: geometry and load path optimisation are preferred over added mass. * **Observability over assumption**: if the machine state cannot be measured, the design is incomplete. * **Time-scale separation**: fast dynamics (vibration, chatter), slow dynamics (deflection, compliance), and supervisory decisions (feed and spindle adaptation) are handled at distinct layers and must not be conflated. * **Fail-safe by construction**: the safety system is hardware-only and independent of all firmware. * **KISS**: complexity is only introduced where it provides a measurable benefit. Explicit Non-Goals ------------------ The following are out of scope for the current phase. They may be revisited later but are not considered when making current design tradeoffs: * Custom servo drives or inverters. * Glass scale feedback. * Active vibration cancellation. * Fully temperature-controlled machine enclosure. * Predictive health monitoring. Documentation Scope ------------------- This concept documentation covers design intent, architectural decisions, subsystem placement and purpose, and the boundaries between mechanical, electrical, control, and safety layers. It does not include CAD geometry, manufacturing drawings, assembly procedures, or firmware implementation details. System Architecture ------------------- The machine decomposes into five subsystems with clearly separated responsibilities. Keeping these boundaries strict is what allows each layer to be reasoned about independently. ### Subsystems * **Motion control — Duet 3 6HC**: kinematics, motion planning, step generation, and G-code execution. No supervision or state estimation. * **Host — Raspberry Pi**: Duet services, web interface, plugins, logging and visualisation (Grafana). No real-time control. * **Real-time supervision — ECU (master PCB)**: sensor fusion, machine state estimation, feed and spindle supervision. No trajectory planning. * **Distributed sensing — sensor PCBs**: zonal architecture with local signal preprocessing and event-based reporting over CAN-FD. No global decision-making. * **Safety chain**: hardware-only, electrically independent, directly actuates power contactors and pneumatics. Overrides all other systems. ### Communication Topology * **Pi ↔ Duet**: native Duet architecture (services, UI, plugins). * **ECU ↔ Pi**: SPI or UART for configuration, data logging, and visualisation. * **ECU ↔ Duet**: GPIO signals for low-latency macro triggering. * **ECU ↔ Sensor PCBs**: CAN-FD, event-driven with heartbeat monitoring. * **ECU ↔ Spindle servo drive**: RS-485. * **Safety chain**: electrically independent, directly actuates power and pneumatics. ### Architecture Diagram Fig. 1 shows the full system with all subsystems and communication links. ![System architecture: Duet motion controller, ECU supervisor, distributed sensor PCBs, Raspberry Pi host, and hardware safety chain with their communication links.](https://blog.hirnschall.net/cnc-mill-concept/goals-philosophy-system-architecture/resources/img/system-architecture.jpg) Figure 1: System architecture: Duet motion controller, ECU supervisor, distributed sensor PCBs, Raspberry Pi host, and hardware safety chain with their communication links. --- ## [Eigenfrequencies with FEM — Tuning Fork Example in NGSolve](https://blog.hirnschall.net/calculating-eigenfrequencies-using-fem/) meta description: Calculate eigenfrequencies and modes using FEM and Rayleigh quotient minimization, with LOBPCG implementation in NGSolve applied to a tuning fork. date published: 15.5.2021 (DD.MM.YYYY format) date last modified: 26.04.2026 (DD.MM.YYYY format) --- # [Computing Eigenfrequencies using the Finite Element Method](https://blog.hirnschall.net/calculating-eigenfrequencies-using-fem/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate eigenfrequencies and modes using FEM and Rayleigh quotient minimization, with LOBPCG implementation in NGSolve applied to a tuning fork. meta title: Eigenfrequencies with FEM — Tuning Fork Example in NGSolve date published: 15.5.2021 (DD.MM.YYYY format) date last modified: 26.04.2026 (DD.MM.YYYY format) --- Introduction ------------ In order to analyze the natural frequencies of a given part, a 3D model with realistic material parameters can be used. As the eigenfrequencies and eigenmodes are solutions to the generalized eigenvalue-problem $$A\eta = (2\pi f)^2 M\eta$$ we can approximate the solutions on a finite element space using the finite element method (FEM or FEA). Although there are multiple different ways to solve this equation, we will use iterative methods to minimize the rayleigh-quotient. This makes sense as we are interested in the smallest eigenvalues and both \(A\) and \(M\) can be very large matrices (making an LU-factorization infeasible). As an example, we will analyze the eigenfrequencies and eigenmodes of a tuning fork. If you are interested in more details you can watch our talk below or download the corresponding pdf. Modal Analysis of a Tuning Fork ------------------------------- ### Finite Element Space (Mesh) As mentioned above, we will try to approximate solutions to the generalized eigenvalue problem $$A\eta = (2\pi f)^2 M\eta$$ within a finite element space. We will therefore start by constructing a mesh for our geometry. ![Tuning fork geometry and resulting mesh for FEM/FEA](https://blog.hirnschall.net/calculating-eigenfrequencies-using-fem/resources/img/mesh.png) Figure 1.1: Tuning fork geometry and resulting mesh If we now use one hat function \(\phi\_i\) per mesh vertex \(x\_i\) such that $$ \phi\_i(x\_j)=\delta\_{ij} $$ as basis for a space \(V\_h\subset H^1\_0\), we can represent each element of \(V\_h\) as a linear combination of basis functions. This also applies to the approximation $$\eta^{(h)}=\sum\_{j=1}^n \eta\_j\phi\_j$$ of \(\eta\) within \(V\_h\). Note: In practice, we will use the hat functions mentioned above in combination with higher-order basis functions to get a better approximation. ![resulting mesh on unit square for FEM/FEA](https://blog.hirnschall.net/calculating-eigenfrequencies-using-fem/resources/img/mesh-square.png) (a) Mesh on the unit square ![basis function on unit square for FEM/FEA](https://blog.hirnschall.net/calculating-eigenfrequencies-using-fem/resources/img/basis-square.png) (b) One basis function \(\phi\_i\) Figure 1.2: Visualization of one basis function \(\phi\_i\) on a mesh on the unit square ### Model of Linear Elasticity As our tuning fork is made out of metal, which deforms linearly (up to a point) we can use the model of linear elasticity to define the bilinear form \(a\) with Lamé parameters \(\mu\) and \(\lambda\) as $$ a:=\int\_{\Omega} 2\mu\varepsilon(u):\varepsilon(v)+\lambda \operatorname{div}(u)\operatorname{div}(v)dH^n $$ where \(\varepsilon(u):= \frac{1}{2}\left(\nabla u +\nabla u^T\right)\) and \(C:D:=\sum\_{ij}C\_{ij}D\_{ij}=tr(C:D^T)\). And the bilinear form \(m\) as $$ m:=\int\_{\Omega}\rho uvdH^n. $$ As both \(a\) and \(m\) are bilinear we can define the stiffness-matrix \(A\) and mass-matrix \(M\) using the basis-functions \(\phi\) as $$ \displaylines{(A\_{ij}):=a(\phi\_i,\phi\_j)\\ (M\_{ij}):=m(\phi\_i,\phi\_j)}. $$ Note that both \(A\) and \(M\) are material dependent. ### Calculating Eigenfrequencies and Eigenmodes Now that we have constructed a mesh, a finite element space as well as the stiffness-matrix \(A\) and mass-matrix \(M\) we can use the LOPCG method (A. Knyazev 2000) to approximate the smallest eigenvalue and corresponding mode shape by minimizing the rayleigh-quotient. You can see the LOPCG algorithm below: --- The LOPCG method (A. Knyazev 2000) Input: starting vector \(u^{(0)}\), tolerance \(\tau\) select \(u^{(0)}\), and set \(p^{(0)}=0\); for (\(i=0,\dots,\)MaxIterations):     \(r:= Au^{(i)}-\lambda^{(i)}Mu^{(i)}\);     \(\omega^{(i)}:= T^{-1}r\);     if (\(||{r}||\_{T^{-1}}<\tau\)):         break;     Use the Rayleigh–Ritz method on the trial subspace \(\operatorname{span}**\omega^{(i)},u^{(i)},p^{(i)}**\);     \(\lambda^{(i+1)}:=\tilde{\lambda}^{(i+1)}\);     \(u^{(i+1)}:=\alpha^{(i)}\omega^{(i)}+\tau^{(i)}u^{(i)}+\gamma^{(i)}p^{(i)}\);     \(p^{(i+1)}:=\alpha^{(i)}\omega^{(i)}+\gamma^{(i)}p^{(i)}\); Output: The approximation \(\lambda^{(k)}\) and \(u^{(k)}\) to the smallest eigenvalue \(\lambda\) and corresponding eigenvector. --- For this example, we have implemented the LOBPCG method (block version of LOPCG) using python/NGSolve. You can download the code in the download section below. ### Resulting Eigenfrequencies and Eigenmodes Here we have used Dirichlet boundary conditions on the handle of the tuning fork. You can see the resulting frequencies and mode shapes below: ![Eigenmode of a tuning fork using FEM/FEA](https://blog.hirnschall.net/calculating-eigenfrequencies-using-fem/resources/img/f414hz.png) (a) \(414\text{Hz}\) ![Eigenmode of a tuning fork using FEM/FEA](https://blog.hirnschall.net/calculating-eigenfrequencies-using-fem/resources/img/f439hz.png) (b) \(440\text{Hz}\) ![Eigenmode of a tuning fork using FEM/FEA](https://blog.hirnschall.net/calculating-eigenfrequencies-using-fem/resources/img/f637hz.png) (c) \(637\text{Hz}\) ![Eigenmode of a tuning fork using FEM/FEA](https://blog.hirnschall.net/calculating-eigenfrequencies-using-fem/resources/img/f798hz.png) (d) \(798\text{Hz}\) Figure 2.3: The eigenmodes corresponding to the \(4\) smallest eigenfrequencies ### Animation ![Animation of the eigenmode of a tuning fork corresponding to 440Hz eigenfrequency](https://blog.hirnschall.net/calculating-eigenfrequencies-using-fem/resources/vid/tuning-fork-animation-440hz.gif) Figure 2.4: Animation of the eigenmode corresponding to \(440\text{Hz}\) Seminar Talk (Video) -------------------- If you are interested in more details you can watch the talk below: Note: To make the talk more YouTube friendly we have simplified several aspects and we have omitted the somewhat technical proof for the rate of convergence of PINVIT (A. V. Knyazev and Neymeyr 2003). Abstract -------- We compare different iterative methods (including LOBPCG (A. Knyazev et al. 2007)) for computing eigenfrequencies and the corresponding eigenmodes in a finite element space. We analyze both numerical complexity and convergence of each algorithm and provide reference implementations using NGSolve. The most suitable method is then used to analyze a clamped-free beam and a tuning fork with realistic material properties. We compare our results to the Euler-Bernoulli beam-theory and measurements done on a real world model. --- ## [Free Engineering Calculators — Electronics & Physics](https://blog.hirnschall.net/tools/) meta description: Free online calculators for electronics and physics — solve for any unknown given the other variables. Symbolic FSAE solver, and more. date published: 01.01.2025 (DD.MM.YYYY format) date last modified: 22.04.2026 (DD.MM.YYYY format) --- # [Online Calculators and Tools](https://blog.hirnschall.net/tools/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Free online calculators for electronics and physics — solve for any unknown given the other variables. Symbolic FSAE solver, and more. meta title: Free Engineering Calculators — Electronics & Physics date published: 01.01.2025 (DD.MM.YYYY format) date last modified: 22.04.2026 (DD.MM.YYYY format) --- Introduction ------------ Many projects/exercises require the same simple calculations over and over again. We want to provide a collection of simple online calculators that can solve for any unknown given that the user provides all but one unknown. Furthermore, each page includes a section about the problem itself and which formula we use to solve for which unknown. If you want more information, check out our blog at [blog.hirnschall.net](https://blog.hirnschall.net) Table of contents ----------------- Use the links below to jump to the tool you are interested in: ### 1   Capacitors * [Discharge - RC (\(\tau\)) or Constant Current](https://blog.hirnschall.net/tools/capacitor-discharge/) * [Stored Energy](https://blog.hirnschall.net/tools/capacitor-stored-energy/) * [Impedance - Reactance](https://blog.hirnschall.net/tools/capacitor-impedance/) * [In Parallel - Total Capacitance](https://blog.hirnschall.net/tools/capacitors-in-parallel/) * [In Series - Total Capacitance](https://blog.hirnschall.net/tools/capacitors-in-series/) * [Parallel Plate Capacitor](https://blog.hirnschall.net/tools/parallel-plate-capacitor/) * [RC Time Constant - optional charge/discharge at given time \(t\)](https://blog.hirnschall.net/tools/rc-time-constant/) ### 2   Inductors * [Impedance - Reactance](https://blog.hirnschall.net/tools/inductor-impedance/) * [Stored Energy](https://blog.hirnschall.net/tools/inductor-stored-energy/) * [In Parallel - Total Inductance](https://blog.hirnschall.net/tools/inductors-in-parallel/) * [In Series - Total Inductance](https://blog.hirnschall.net/tools/inductors-in-series/) * [RL Time Constant - optional energize/de-energize at given time \(t\)](https://blog.hirnschall.net/tools/rl-time-constant/) ### 3   General Electronics * [Voltage Divider - Solve for \(V\_{out}\), \(V\_{in}\), \(R\_1\), or \(R\_2\)](https://blog.hirnschall.net/tools/voltage-divider/) * [Battery Runtime - from Capacity and Current](https://blog.hirnschall.net/tools/battery-runtime/) * [Wheatstone Bridge - Balance and \(V\_{out}\)](https://blog.hirnschall.net/tools/wheatstone-bridge/) * [Maxwell Bridge - Inductance and Series Resistance](https://blog.hirnschall.net/tools/maxwell-bridge/) * [Schering Bridge - Capacitance, ESR, and Dissipation Factor](https://blog.hirnschall.net/tools/schering-bridge/) * [LC Resonance Frequency](https://blog.hirnschall.net/tools/lc-resonance-frequency/) ### 4   Formula Student (FSAE) * [A symbolic VD and Aero equation solver built on reusable physics models](https://blog.hirnschall.net/everything-aero/) * [A symbolic Gas Cycle Calculator. T-V and P-V Plots.](https://blog.hirnschall.net/gas-cycle-calculator/) * [A symbolic 2D Force and Moment Equilibrium Calculator](https://blog.hirnschall.net/force-moment-equilibrium/) ### 5   FEM * [Online Gerber To Step Converter](https://blog.hirnschall.net/gerber-to-step/) * [Online Mesh Generator (Netgen)](https://app.sim-pl.com/app?appId=3) --- ## [Capacitors in Parallel Calculator — Total Capacitance](https://blog.hirnschall.net/tools/capacitors-in-parallel/) meta description: Calculate the total capacitance of capacitors in parallel. Add as many capacitors as needed. Includes formula explanation and practical use cases. date published: 01.04.2025 (DD.MM.YYYY format) date last modified: 22.04.2025 (DD.MM.YYYY format) --- # [Capacitors in Parallel Calculator](https://blog.hirnschall.net/tools/capacitors-in-parallel/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate the total capacitance of capacitors in parallel. Add as many capacitors as needed. Includes formula explanation and practical use cases. meta title: Capacitors in Parallel Calculator — Total Capacitance date published: 01.04.2025 (DD.MM.YYYY format) date last modified: 22.04.2025 (DD.MM.YYYY format) --- Calculator ---------- Enter the capacitance of each capacitor in parallel. Add more capacitors with the button below. The total capacitance is calculated from all filled fields. * Capacitor 1 (C1): * F mF µF nF pF * Capacitor 2 (C2): * F mF µF nF pF Enter at least two capacitance values. Add Capacitor Calculate Capacitors in Parallel — Explanation ------------------------------------ When capacitors are connected in parallel, the total capacitance is simply the sum of all individual capacitances: \[ C\_{\text{total}} = C\_1 + C\_2 + \cdots + C\_n \] This is the opposite of the series case — parallel connection always yields a total capacitance greater than any individual capacitor. The reason is straightforward: all capacitors in parallel share the same voltage \( V \) across their plates. The total charge stored is therefore the sum of the charges on each capacitor: $$ \begin{align} Q\_{\text{total}} &= Q\_1 + Q\_2 + \cdots + Q\_n \\ &= C\_1 V + C\_2 V + \cdots + C\_n V \\ &= (C\_1 + C\_2 + \cdots + C\_n) \cdot V \end{align} $$ Dividing both sides by \( V \) gives \( C\_{\text{total}} = C\_1 + C\_2 + \cdots + C\_n \) directly. This mirrors the formula for resistors in series — again, a useful analogy. When to Use Capacitors in Parallel ---------------------------------- Parallel combinations are common in practice for two reasons. First, to reach a target capacitance that is not available as a standard component value — for example, combining a 10 µF and a 4.7 µF to get 14.7 µF. Second, to reduce effective ESR (equivalent series resistance): multiple capacitors in parallel divide the ESR, which matters in switching power supplies and high-frequency bypass applications. Once the equivalent capacitance is known, plug it directly into the [RC time constant](https://blog.hirnschall.net/tools/rc-time-constant/) or [LC resonance frequency](https://blog.hirnschall.net/tools/lc-resonance-frequency/) calculator. Related Tools ------------- * [Capacitors in Series Calculator](https://blog.hirnschall.net/tools/capacitors-in-series/) — the complementary case; total C is always less than any individual capacitor. * [RC Time Constant Calculator](https://blog.hirnschall.net/tools/rc-time-constant/) — use the equivalent parallel C in an RC circuit. * [LC Resonance Frequency Calculator](https://blog.hirnschall.net/tools/lc-resonance-frequency/) — use the equivalent parallel C to set the resonant frequency. * [Inductors in Parallel Calculator](https://blog.hirnschall.net/tools/inductors-in-parallel/) — the inductive equivalent; parallel inductors use the reciprocal formula. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [3D Printed Rubber Band Gun — 6 Shot Pistol with Rotating Drum](https://blog.hirnschall.net/rubber-band-gun/) meta description: DIY 3D printed rubber band pistol with a 6-shot rotating drum mechanism. Covers the design, print settings, and assembly. Free STL download included. date published: 12.04.2026 (DD.MM.YYYY format) date last modified: 12.04.2026 (DD.MM.YYYY format) --- # [3D Printed Rubber Band Gun with Multiple Shots](https://blog.hirnschall.net/rubber-band-gun/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: DIY 3D printed rubber band pistol with a 6-shot rotating drum mechanism. Covers the design, print settings, and assembly. Free STL download included. meta title: 3D Printed Rubber Band Gun — 6 Shot Pistol with Rotating Drum date published: 12.04.2026 (DD.MM.YYYY format) date last modified: 12.04.2026 (DD.MM.YYYY format) --- Introduction ------------ Let's build a 3D printed rubber band gun that has multiple shots, is easy to print, and even more fun to use! I don't think we have to explain the motivation behind this project any further so let's get started with the design requirements/what we want to have in the finished design. You can find the model [at Printables](https://www.printables.com/model/11228-rubber-band-gun-6-shots)! Design Requirements ------------------- To make this project actually stand out I want the following things to be included in the design: * **Pistol**: I want it to be a handgun. Otherwise it will be too much. * **Multiple shots**: I want the gun to have multiple shots. You load it once and then it is semi-automatic. * **Easy to print**: We will use every trick in the book to make this thing print fast and without support. * **Safe to use**: We have to make sure that every part under tension is oriented in a way that it does not break when being used. A rubber band in the eye is NOT fun! While the list is not that long, each part is quite important. So, let's dive into the details! Design Details -------------- Now that the design constraints have been set, we can go through the details and how we can achieve them. To do so, we will go over the key points in the design with pictures. ### General Principle In general, a rubber band gun always works in the same way. We stretch a rubber band from the end of the barrel to some sort of lever that is held in place. Once we pull the trigger, that lever is set free to rotate/swing away and release the rubber band. You can see this in fig. 1 below: ![General principle of a rubber band gun](https://blog.hirnschall.net/rubber-band-gun/resources/img/with-rubber-band.jpg) Figure 1: General principle of a rubber band gun ### Multiple Shots To make the gun semi automatic, we can use a rotating drum mechanism. If we use not one lever but multiple levers fixed to a wheel, we can rotate the wheel a fraction of a turn to release just one rubber band. To make this work, we need a trigger to do two important things. It needs to release the drum and at the same time it needs to prevent full rotation. I.e. it has to allow the drum to rotate a 1/6th of a turn with each trigger pull. When we release the trigger this stop must also be released so that we can shoot the next stop. This part is called disconnector. The part that actually holds and releases the drum when the trigger is pulled is called sear. Fig. 2 shows the assembly of the trigger mechanism. The trigger itself is connected to the sear with a connecting rod. The sear rotates around an m2 pin when the trigger is pulled. At the same time the back of the sear catches the next tooth of the drum, preventing full rotation. This way each trigger pull releases just one rubber band. We use an additional rubber band to tension the trigger. The grip has protrusions that prevent the trigger from traveling too far. ![Trigger assembly](https://blog.hirnschall.net/rubber-band-gun/resources/img/trigger-assembly.jpg) Figure 2: Trigger assembly ### Safety and Easy to Print We have to make sure that every part is only experiencing load perpendicular to layer lines. Otherwise we risk the part breaking which could be dangerous. Besides the barrel, the drum is the only part that is experiencing any meaningful load. We will therefore print it laying flat. However, the axle sticks out both sides to center the drum. We will split the drum into two parts and glue them together in the assembly process. You can see how this works in fig. 3 below: ![Drum](https://blog.hirnschall.net/rubber-band-gun/resources/img/wheel.jpg) (a) Drum ![Drum axle](https://blog.hirnschall.net/rubber-band-gun/resources/img/wheel-axle.jpg) (b) Drum axle Figure 3: Trigger assembly The printed and assembled drum is shown in fig. 4 below: ![Drum assembly](https://blog.hirnschall.net/rubber-band-gun/resources/img/wheel-iso.jpg) Figure 4: Drum assembly Assembly -------- We can now finally assemble the project! The 3D printed files are available on [Printables](https://www.printables.com/model/11228-rubber-band-gun-6-shots). ### Hardware In addition to the printed parts, you will need the following hardware: * 8× M4×10 screws * 3× metal pins 2mm diameter × 25mm long * 2× metal pins 2mm diameter × 10mm long Standard 2mm nails cut to length work as a substitute for the pins. ### Print Settings We can print all parts from PLA as it is easy to print and heat or long UV exposure is not a concern for this project. The parts are designed to print easily and mostly without support. Only the sear has a small overhang that requires support. We will use 0.2mm layer height and the default slicer settings. As mentioned, the drum is printed as two separate pieces to avoid support material while still printing in the correct orientation for strength. We will use super glue to assemble the drum as shown in fig. 4. Make sure to let the glue cure fully before using the drum in the gun. Conclusion ---------- Overall I am super happy with how this project turned out. It was originally designed as a gift for one of my best friends. The mechanism works really nicely and is complicated, yet simple enough to be interesting for non technical people as well. --- ## [Battery Runtime Calculator — mAh, Current, and Runtime](https://blog.hirnschall.net/tools/battery-runtime/) meta description: Calculate battery runtime from capacity and current draw. Supports power-based loads, converter efficiency, and depth of discharge. Solves for runtime, capacity, or current. date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- # [Battery Runtime Calculator](https://blog.hirnschall.net/tools/battery-runtime/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate battery runtime from capacity and current draw. Supports power-based loads, converter efficiency, and depth of discharge. Solves for runtime, capacity, or current. meta title: Battery Runtime Calculator — mAh, Current, and Runtime date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- Calculator ---------- Provide any two of capacity, current, and runtime — the third is solved. Battery voltage enables Wh output and power-based input. Efficiency and depth of discharge are optional corrections. * Battery capacity: * mAh Ah * Load current: * µA mA A * Runtime: * h min days --- * Battery voltage — optional: * V mV * Load power — optional (replaces current): * mW W * Converter efficiency — optional: * % * Depth of discharge — optional: * % Provide any two of capacity, current, and runtime to calculate. Calculate Battery Runtime — Explanation ----------------------------- The runtime of a battery-powered device follows directly from the relationship between stored charge and current consumption. A battery rated at capacity \( C \) (in mAh or Ah) can deliver an average current \( I \) for a time: \[ t = \frac{C}{I} \] A 2000 mAh battery powering a 100 mA load will last 20 hours in the ideal case. The formula is the same regardless of chemistry — the mAh rating is the fundamental quantity that defines how much charge the battery holds. When the load is specified as a power draw rather than a current, we first convert using the battery voltage \( V \): \[ I = \frac{P}{V}, \qquad t = \frac{C \cdot V}{P} \] This is useful when the datasheet of a module gives a power figure rather than a current figure, or when the load operates from a regulated voltage that differs from the battery voltage. Converter Efficiency -------------------- Most battery-powered systems include a voltage regulator or DC-DC converter between the battery and the load. These converters are not lossless: a boost converter running at 85% efficiency requires the battery to supply 100/85 ≈ 1.18 times the power the load actually consumes. The calculator accounts for this by dividing the load current by the efficiency factor before computing the runtime: \[ I\_{\text{battery}} = \frac{I\_{\text{load}}}{\eta} \] Ignoring converter losses is one of the most common sources of error when estimating battery life. A system that looks like it should run for 10 hours at full load may actually run for 8.5 hours once a typical 85% efficient boost converter is factored in. Depth of Discharge ------------------ Most battery chemistries should not be discharged to zero. Doing so shortens cycle life significantly. The depth of discharge (DoD) sets what fraction of the nominal capacity is actually used: * **Li-Ion / LiPo** — typically 80–90% DoD to preserve cycle life; 100% is possible but accelerates aging. * **Lead-acid** — typically 50% DoD; deeper discharges can permanently sulfate the plates. * **LiFePO\(\_4\)** — tolerates deeper cycling than standard Li-Ion; 80–90% is common. * **NiMH** — relatively tolerant; 80–100% DoD is workable, though memory effect applies to older cells. When DoD is entered, the calculator reduces the effective capacity to \( C \cdot \text{DoD} \) before computing runtime or required capacity. For sizing a battery pack with longevity in mind, enter the DoD along with the required runtime to find the nominal capacity needed. Purpose of the Calculator ------------------------- Any two of capacity, current, and runtime determine the third. The rearranged formulas are: * To solve for runtime (how long a known battery lasts at a given load): \[ t = \frac{C}{I} \] * To solve for capacity (sizing a battery pack for a required runtime): \[ C = I \cdot t \] * To solve for current (finding the maximum load a battery can sustain for a given runtime): \[ I = \frac{C}{t} \] When efficiency and DoD are provided, both are applied before solving, and the result reflects the real-world figure rather than the idealized one. Practical Caveats ----------------- The calculation assumes a constant average current draw. Real devices rarely have a flat profile: a microcontroller sleeping most of the time with periodic radio bursts has a very different average than its peak consumption. For duty-cycled loads, compute the time-weighted average current and use that as the input. Battery capacity also degrades with temperature. Most lithium cells deliver their rated capacity only at around 20–25 °C. At 0 °C available capacity can drop to 70–80% of the nominal value; at -20 °C it can fall below 50%. For outdoor or automotive applications, it is worth applying an additional temperature derating factor on top of the DoD. For lead-acid batteries, the Peukert effect is relevant: capacity decreases at higher discharge rates, so the simple formula overestimates runtime under heavy loads. For lithium chemistries the Peukert effect is minor and can usually be ignored in first-order estimates. Related Tools ------------- * [Voltage Divider Calculator](https://blog.hirnschall.net/tools/voltage-divider/) — design the resistive divider for battery voltage monitoring at a microcontroller ADC input. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [CNC Mill Concept — Control Electronics, Motion & Electrical](https://blog.hirnschall.net/cnc-mill-concept/electronics-motion-and-electrical/) meta description: ECU and distributed sensor PCBs on STM32G4 with CAN-FD, Duet 3 6HC open-loop stepper motion with HTD belts, and a DIN/EN 60204-1 electrical installation. date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- # [CNC Mill Concept: Control Electronics, Motion & Electrical](https://blog.hirnschall.net/cnc-mill-concept/electronics-motion-and-electrical/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: ECU and distributed sensor PCBs on STM32G4 with CAN-FD, Duet 3 6HC open-loop stepper motion with HTD belts, and a DIN/EN 60204-1 electrical installation. meta title: CNC Mill Concept — Control Electronics, Motion & Electrical date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- Introduction ------------ This post is part of the [CNC Mill Concept hub](https://blog.hirnschall.net/cnc-mill-concept/). It covers the control electronics architecture, distributed sensor PCB capabilities, the motion system, and the electrical installation and pneumatics. Control Electronics and PCB Architecture ---------------------------------------- ### Purpose * Provide a clear separation between: + motion control + process supervision + sensing and data acquisition + safety * Enable extensible, modular sensing and supervision * Avoid tight coupling between motion execution and adaptive logic ### ECU (Master PCB) * Acts as real-time supervisory controller * Responsibilities: + sensor fusion + machine state estimation + process supervision decisions * Interfaces: + CAN-FD: - communication with distributed sensor PCBs + RS-485: - communication with spindle servo drive + GPIO: - low-latency triggering of Duet macros + SPI or UART: - communication with Raspberry Pi * Does not: + generate motion trajectories + directly drive axes + participate in safety chain ### Distributed Sensor PCBs * Zonal architecture: + multiple PCBs placed near sensors * Connected to ECU via CAN-FD * Responsibilities: + sensor signal acquisition + local filtering + FFT and envelope extraction + threshold detection * Communication behavior: + event-driven data transmission + periodic heartbeat messages * Fault handling: + ECU detects missing heartbeats + missing data treated as sensor failure * Debug and validation: + raw data access via USB or SPI ### Communication Architecture * CAN-FD used for: + robust, deterministic sensor data exchange * SPI / UART used for: + configuration + logging + visualization * GPIO used for: + low-latency supervisory actions + feed and spindle-related macros ### Raspberry Pi Integration * Runs Duet services and web interface * Hosts plugins for: + configuration of ECU and sensor PCBs + data logging + visualization (e.g. Grafana) * Not used for: + real-time control + safety-critical functions Sensor PCB Capabilities ----------------------- ### General Architecture Two identical distributed sensor PCBs are used. * Mounted close to structural measurement locations * Based on an STM32G4 (CAN-FD capable) * CAN-FD communication to the main ECU * Optional synchronization line for deterministic simultaneous sampling * Careful separation of analog and digital domains All nodes share the same CAN-FD bus. The ECU is located at one physical end of the bus. ### CAN Topology Fig. 1 shows the CAN-FD bus topology with both sensor nodes and their connected sensors. ![CAN-FD bus topology: ECU at one end, Sensor Node A (left X-beam) and Sensor Node B (right X-beam), with strain gauge, accelerometer, microphone, and temperature sensor connections per node.](https://blog.hirnschall.net/cnc-mill-concept/electronics-motion-and-electrical/resources/img/can-topology-overview.jpg) Figure 1: CAN-FD bus topology: ECU at one end, Sensor Node A (left X-beam) and Sensor Node B (right X-beam), with strain gauge, accelerometer, microphone, and temperature sensor connections per node. ### Strain Gauge Interface * Supports multiple full-bridge configurations * External 24-bit ADC recommended for dynamic strain measurement * Differential low-noise instrumentation front-end * Bridge excitation provided by PCB * Shielded differential wiring to remote DMS Typical usage: * 2 × X-beam full bridges ### Accelerometer Interface * SPI interface for digital 3-axis accelerometers * Deterministic sampling capability * Remote mounting via short shielded cable or rigid daughterboard * Optional synchronization between PCBs Typical usage: * 1 × spindle plate accelerometer * 1 × tower accelerometer ### Microphone Interface (Piezo Surface Microphone) * High-impedance charge amplifier front-end * Anti-alias filtering * Analog input to ADC * Shielded cable required Typical usage: * 1 × spindle housing microphone * 1 × tower microphone ### Temperature Sensor Interface * SPI/I²C thermocouple frontend or RTD interface * Alternatively precision analog temperature input * Slow sampling rate sufficient Typical usage: * 1 × spindle housing temperature * 1 × tower plate temperature ### Piezo Communication Interface * SPI or I²C master interface * Used to communicate with external piezo shunt PCBs * No high-voltage circuitry on the sensor PCB * Digital control and monitoring only Motion System ------------- ### Purpose * Provide precise, repeatable axis motion * Keep motion execution simple and robust in the initial build * Allow future upgrades without redesigning the machine structure ### Current Motion Configuration * Motion controller: + Duet 3 6HC * Motors: + stepper motors on all linear axes * Motor drive: + steppers driven directly by Duet * Mechanical transmission: + HTD belt between motor and leadscrew * Purpose of belt coupling: + mechanical filtering of motor vibration + increased effective steps per millimeter + flexibility in gear ratio selection * Leadscrews: + directly coupled to axis motion * Feedback: + open-loop stepper operation in current phase ### Design Considerations * Motion system prioritized for: + simplicity + predictability + compatibility with supervision layer * Motion execution kept independent from: + adaptive supervision logic + compliance estimation * Any feed or spindle adaptation performed via: + Duet macros + supervisory requests only ### Deferred Motion Upgrades * Closed-loop servo drives * Dual-loop control using external feedback * Glass scale integration * Custom servo inverter development ### Scope Limitations * No real-time modification of trajectories by ECU * No active damping or compensation within motion controller * Motion system behavior assumed deterministic for supervision purposes Electrical Installation and Pneumatics -------------------------------------- ### Purpose * Ensure operator and bystander safety * Provide deterministic, fail-safe shutdown behavior * Comply with applicable DIN / EN machine safety principles * Keep safety independent from software and firmware ### Electrical Installation * Electrical system designed in accordance with: + DIN / EN 60204-1 principles * Separate grounded electrical cabinet * Segregation of: + mains power + motor power + control signals + sensor signals * Use of: + main contactors + appropriate fusing + protective earth bonding * All safety-relevant wiring implemented in hardware ### Emergency Stop (E-Stop) * Emergency stop system implemented as hardware-only * Normally-open main contactor * E-stop directly interrupts: + mains power to drives + control power where required * No software involvement in E-stop behavior * E-stop overrides: + Duet + ECU + Raspberry Pi ### Pneumatics * Pneumatic system used for: + tool clamping + auxiliary machine functions * Depressurization valve: + normally open + wired directly into shutdown circuit * Behavior on: + E-stop + power loss * Result: + automatic venting of pneumatic system + loss of pressure as safe state * Pneumatic control logic not software-dependent for safety ### Fail-Safe Behavior * Loss of electrical power results in: + drives de-energized + pneumatic system depressurized * Safety chain behavior is deterministic and testable * No single software fault can inhibit safe shutdown ### Scope Limitations * No software-based safety logic * No safety functions implemented in Duet, ECU, or Raspberry Pi * Safety system not affected by firmware updates --- ## [Ideal Gas Cycle Calculator — Symbolic P-V & T-V Solver](https://blog.hirnschall.net/gas-cycle-calculator/) meta description: Symbolic calculator for ideal gas cycle problems (FSAE-friendly). Enter state-change types and knowns, get solved P-V and T-V diagrams in the browser. date published: 13.05.2026 (DD.MM.YYYY format) date last modified: 13.05.2026 (DD.MM.YYYY format) --- # [An Ideal Gas Cycle Calculator](https://blog.hirnschall.net/gas-cycle-calculator/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Symbolic calculator for ideal gas cycle problems (FSAE-friendly). Enter state-change types and knowns, get solved P-V and T-V diagrams in the browser. meta title: Ideal Gas Cycle Calculator — Symbolic P-V & T-V Solver date published: 13.05.2026 (DD.MM.YYYY format) date last modified: 13.05.2026 (DD.MM.YYYY format) --- About ----- The tool below calculates and plots an ideal gas cycle given the type of each state change and enough known variables. It was originally built for FSAE rules quiz usage. Similar to [Everything Aero](https://blog.hirnschall.net/everything-aero/) and the [Force & Moment Equilibrium calculator](https://blog.hirnschall.net/force-moment-equilibrium/), this tool uses SymPy in the browser to solve the resulting system of equations symbolically. ### How to use it * Enter one input "equation" per line. * Input all state change types (e.g. `type1-2 = isobaric`). Supported types are `isobaric`, `isochoric`, `isothermal`, and `adiabatic`. States are numbered 1, 2, ... and the cycle closes when the last state change returns to state 1. * Add all additional known variables for pressure, volume, and temperature. E.g. `p1=100000`, `v1=1`, `t1=300`. The index corresponds to the state number. * For adiabatic legs the heat capacity ratio \(\gamma\) defaults to an unknown and is solved for. To pin it, add `gammaA-B=1.4` where `A` and `B` are the state indices of that leg (e.g. `gamma1-2=1.4`). * The tool uses SI units. * Optionally, `bar`, `l` (liters) and `C` (degrees Celsius) can be used (multiplied) to convert units. E.g. `p1=2*bar`, `t1=37*C`. Scientific notation is also supported (e.g. `p1=2e5`). * Click **Solve** and hover over the diagrams to read state values. The solver determines which equations to use and solves for all unknowns. Results are rendered with Plotly as a P-V and a T-V diagram. The calculator runs a full Python environment in the browser. It needs a few seconds to start up and install packages with pip (in the browser environment only). SymPy and Plotly are lazily loaded on the first **Solve** press, so that first solve takes a few seconds. If the tool fails to load, reload the page and check the console (press F12). Calculator ---------- Loading Python ... Examples -------- Let's take a look at how to use the calculator for different FS rules quiz questions. Both examples below use the same four-state cycle (alternating isothermal and isochoric transitions), but ask for a different unknown. The original problem statements are linked from each example. ### Example 1: FS-Quiz Question 66 [FS-Quiz Question 66](https://fs-quiz.eu/question/66) states \(p\_1 = 7\) bar, \(V\_1 = 0.6\) m³, \(V\_2 = 3.3\) m³, \(T\_2 = 800\)°C, and \(p\_3 = 0.6\) bar along with the type of state change for each point. We need to find the pressure in point 4 (\(p\_4\)). The input therefore becomes the information given in the question. ``` type1-2 = isothermal type2-3 = isochoric type3-4 = isothermal type4-1 = isochoric p1=7*bar v1=.6 v2=3.3 t2=800*C p3=.6*bar ``` Hovering over point 4 on the P-V diagram shows \(p\_4 = 3.3 \cdot 10^5\) Pa, i.e. \(p\_4 = 3.3\) bar. Fig. 1 shows the input and the resulting P-V and T-V plots along with the solution for \(p\_4\). ![Input and solution for FS-Quiz Question 66 (ideal gas cycle), with point 4 hovered on the P-V diagram](https://blog.hirnschall.net/gas-cycle-calculator/resources/img/fs-quiz-66.jpg) Figure 1: Input and solution for FS-Quiz Question 66 (ideal gas cycle), with point 4 hovered on the P-V diagram ### Example 2: FS-Quiz Question 155 [FS-Quiz Question 155](https://fs-quiz.eu/question/155) gives \(p\_1 = 6.5\) bar, \(V\_1 = 0.7\) m³, \(V\_2 = 2.1\) m³, \(p\_4 = 1.2\) bar, and \(T\_1 = 800\) K. We need to find \(T\_3\). Again, the input for the solver is everything stated in the exercise: ``` type1-2 = isothermal type2-3 = isochoric type3-4 = isothermal type4-1 = isochoric p1=6.5*bar v1=.7 v2=2.1 p4=1.2*bar t1=800 ``` Note that `t1=800` has no `*C` as it is given in Kelvin directly. Hovering point 3 on the T-V diagram shows \(T\_3 \approx 147.7\) K which is the correct solution to question 155 as shown in fig. 2. ![Input and solution for FS-Quiz Question 155 (ideal gas cycle), with point 3 hovered on the T-V diagram](https://blog.hirnschall.net/gas-cycle-calculator/resources/img/fs-quiz-155.jpg) Figure 2: Input and solution for FS-Quiz Question 155 (ideal gas cycle), with point 3 hovered on the T-V diagram How It Works ------------ The web UI is built with [ngapp](https://cerbsim.github.io/ngapp) and runs python entirely in the browser. Each declared state change type is mapped to a single equation in the solver. The unit suffixes (`bar`, `C`) are implemented as additional equations/knowns. SymPy assembles and solves the resulting system symbolically, and Plotly plots the cycle on a P-V and a T-V diagram. Hovering a point shows its state values in SI units in the tool-tip. ### State Change Equations The isothermal, isobaric, and isochoric relations follow from the ideal gas law \(pV = nRT\) under one conserved quantity. The adiabatic case (no heat exchange) instead follows from \(pV^\gamma = \text{const}\), where \(\gamma\) is the heat capacity ratio. The relations between adjacent states \(i\) and \(j\) are: * **Isothermal** (\(T\_i = T\_j\)): $$p\_i V\_i = p\_j V\_j$$ * **Isobaric** (\(p\_i = p\_j\)): $$\dfrac{V\_i}{T\_i} = \dfrac{V\_j}{T\_j}$$ * **Isochoric** (\(V\_i = V\_j\)): $$\dfrac{p\_i}{T\_i} = \dfrac{p\_j}{T\_j}$$ * **Adiabatic** (no heat exchange): $$p\_i V\_i^\gamma = p\_j V\_j^\gamma \quad\text{and}\quad T\_i V\_i^{\gamma-1} = T\_j V\_j^{\gamma-1}$$ --- ## [ESP8266 PCB Layout — Antenna Keep-Out & Grounding](https://blog.hirnschall.net/esp8266-pcb-design/) meta description: ESP8266 PCB rules from Espressif's guidelines — 15mm antenna keep-out, decoupling, and copper pour rules to keep WiFi working on your board. date published: 09.01.2023 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [ESP8266: PCB Design Guidlines](https://blog.hirnschall.net/esp8266-pcb-design/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: ESP8266 PCB rules from Espressif's guidelines — 15mm antenna keep-out, decoupling, and copper pour rules to keep WiFi working on your board. meta title: ESP8266 PCB Layout — Antenna Keep-Out & Grounding date published: 09.01.2023 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- Introduction ------------ In recent years the ESP8266 has become one of my go-to choices for smart home/IoT devices or projects requiring WiFi. It is cheap [(around $1.5 on Amazon)1](https://amzn.to/3r9XX2D), has WiFi, and can be programmed using the Arduino IDE, making it an easy replacement for Arduinos if we need WiFi connectivity. This post outlines the most important guidlines when designing a pcb for an ESP8266 module. It is part of [a complete ESP8266 reference you can find here.](https://blog.hirnschall.net/esp8266/) PCB design ---------- Espressif provides a [hardware design guide](https://www.espressif.com/sites/default/files/documentation/esp8266_hardware_design_guidelines_en.pdf) for free. Noticeable takeaways are that we want to place the antenna of the esp module outside of our PCB or, if this is not possible, at least 15mm away from other components (keep-out zone). Meanwhile, [10] only shows a 5mm keep-out zone, as depicted in Fig. 6.1 (c) below. According to [1], we also want to avoid using resistors larger than 2.2k near the esp module as they might be affected by the WiFi. Up to 2.2kOhm seem to work without issues. ![esp8266 esp-07 and esp-12pcb design best prectice with antenna outside pcb area](https://blog.hirnschall.net/esp8266-pcb-design/resources/img/pcb-design-1.png) (a) Antenna outside PCB area ![esp8266 esp-07 and esp-12pcb design best prectice with pcb cut out](https://blog.hirnschall.net/esp8266-pcb-design/resources/img/pcb-design-2.png) (b) Antenna over PCB cut-out ![esp8266 esp-07 and esp-12pcb design best prectice with keep-out zone](https://blog.hirnschall.net/esp8266-pcb-design/resources/img/pcb-design-3.png) (c) Antenna over keep-out zone (red) Figure 6.1: PCB design for ESP8266 as shown in [10,14] More Info --------- This post is part of a complete ESP8266 reference/guide. You can find more information on how to use your ESP8266 module effectively at [blog.hirnschall.net/esp8266/](https://blog.hirnschall.net/esp8266/). Where & What to Buy ------------------- Below are links1 to the ESP modules and development boards I personally tend to use. If you are unsure about which one to choose, [check out this comparison post!](https://blog.hirnschall.net/esp8266-vs-esp32/) | ESP-12F | NodeMCU ESP8266 | ESP32-WROOM-32 | ESP32-S3 | | --- | --- | --- | --- | | [amazon.com](https://amzn.to/41YJ965) [amazon.de](https://amzn.to/3Olf3GC) | [amazon.com](https://amzn.to/4smbFcB) [amazon.de](https://amzn.to/4eeUhD6) | [amazon.com](https://amzn.to/4tItyDJ) [amazon.de](https://amzn.to/41V075k) | [amazon.com](https://amzn.to/4tEXG2G) [amazon.de](https://amzn.to/4c26zgR) | | #CommissionsEarned | | | | --- ## [Capacitor Discharge Calculator — RC and Constant Current](https://blog.hirnschall.net/tools/capacitor-discharge/) meta description: Calculate capacitor discharge time, voltage, current, and charge over time. Supports RC exponential discharge and constant current discharge. Solve for time or voltage. date published: 01.04.2025 (DD.MM.YYYY format) date last modified: 22.04.2025 (DD.MM.YYYY format) --- # [Capacitor Discharge Calculator](https://blog.hirnschall.net/tools/capacitor-discharge/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate capacitor discharge time, voltage, current, and charge over time. Supports RC exponential discharge and constant current discharge. Solve for time or voltage. meta title: Capacitor Discharge Calculator — RC and Constant Current date published: 01.04.2025 (DD.MM.YYYY format) date last modified: 22.04.2025 (DD.MM.YYYY format) --- Calculator ---------- Fill in the known variables. Leave exactly one of \( t \) or \( V(t) \) empty — the calculator solves for it. Provide \( R \) and/or \( \tau \) for RC exponential discharge, or \( I \) for constant current discharge. * Initial voltage (V0): * V mV kV * Capacitance (C): * F mF µF nF pF * Resistance (R) — RC mode: * Ω kΩ MΩ * Time constant (τ) — RC mode: * s ms µs min --- * Time (t): * s ms µs min * Target voltage V(t) — leave empty to solve for V(t): * V mV kV --- * Constant current (I) — constant current mode: * A mA µA Fill in the known variables to calculate. Calculate Capacitor Discharge — Explanation --------------------------------- This calculator covers two physically distinct discharge scenarios: exponential RC discharge and constant current discharge. RC Exponential Discharge ------------------------ When a charged capacitor discharges through a resistor, the voltage, current, and charge all decay exponentially: \[ V(t) = V\_0 \cdot e^{-t/\tau} \] \[ I(t) = \frac{V\_0}{R} \cdot e^{-t/\tau} \] \[ Q(t) = C \cdot V\_0 \cdot e^{-t/\tau} \] where \( \tau = R \cdot C \) is the time constant. All three quantities share the same exponential envelope — only the scaling factor differs. If C is a network of capacitors, use the [capacitors in series](https://blog.hirnschall.net/tools/capacitors-in-series/) or [capacitors in parallel](https://blog.hirnschall.net/tools/capacitors-in-parallel/) calculator to find the equivalent value first. To find the time at which the voltage reaches a target value \( V(t) \), rearrange: \[ t = -\tau \cdot \ln\!\left(\frac{V(t)}{V\_0}\right) \] The Time Constant τ ------------------- \( \tau = R \cdot C \) sets the speed of the discharge. After one time constant the voltage has dropped to \( 1/e \approx 36.8\% \) of \( V\_0 \). After five time constants the capacitor is considered fully discharged for most practical purposes: \[ \begin{align} t = 1\tau &\Rightarrow V = 36.8\%\ V\_0 \\ t = 2\tau &\Rightarrow V = 13.5\%\ V\_0 \\ t = 3\tau &\Rightarrow V = 5.0\%\ V\_0 \\ t = 4\tau &\Rightarrow V = 1.8\%\ V\_0 \\ t = 5\tau &\Rightarrow V = 0.7\%\ V\_0 \end{align} \] Constant Current Discharge -------------------------- When a capacitor is discharged by a constant current source — such as a current-regulated load or a charge pump — the voltage decreases linearly rather than exponentially: \[ V(t) = V\_0 - \frac{I \cdot t}{C} \] Rearranged to solve for the time to reach a target voltage: \[ t = \frac{C \cdot (V\_0 - V(t))}{I} \] The capacitor fully discharges at \( t\_{\text{max}} = C \cdot V\_0 / I \). To find the total energy stored before discharge begins, use the [capacitor energy calculator](https://blog.hirnschall.net/tools/capacitor-stored-energy/). Related Tools ------------- * [RC Time Constant Calculator](https://blog.hirnschall.net/tools/rc-time-constant/) — compute τ, R, or C for the same RC circuit. * [Capacitor Energy Calculator](https://blog.hirnschall.net/tools/capacitor-stored-energy/) — find the energy stored before discharge begins. * [Capacitors in Series](https://blog.hirnschall.net/tools/capacitors-in-series/) / [Capacitors in Parallel](https://blog.hirnschall.net/tools/capacitors-in-parallel/) — if C is a network, compute the equivalent capacitance first. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [CNC Mill Concept — Deferred & Future Work](https://blog.hirnschall.net/cnc-mill-concept/deferred-and-future-work/) meta description: Out of scope for the current CNC mill build: closed-loop servos, glass scales, custom drives, 4th and 5th axis, thermal compensation, and manufacturing docs. date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- # [CNC Mill Concept: Deferred & Future Work](https://blog.hirnschall.net/cnc-mill-concept/deferred-and-future-work/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Out of scope for the current CNC mill build: closed-loop servos, glass scales, custom drives, 4th and 5th axis, thermal compensation, and manufacturing docs. meta title: CNC Mill Concept — Deferred & Future Work date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- Introduction ------------ This post is part of the [CNC Mill Concept hub](https://blog.hirnschall.net/cnc-mill-concept/). It explicitly documents features and concepts that are **not part of the current build**, to prevent scope creep and preserve design intent for later development phases. Motion and Feedback Upgrades ---------------------------- * Closed-loop servo drives on linear axes * Dual-loop control using external feedback * Glass scale integration for linear axes * Replacement of stepper-based motion system Drive Electronics ----------------- * Custom servo inverter development * Evaluation of GaN, SiC, or IGBT-based power stages * Unified drive hardware for multiple machine projects Additional Axes --------------- * 4th axis integration * 5th axis integration * Simultaneous multi-axis machining capability Advanced Compensation Strategies -------------------------------- * Thermal compensation of machine structure * Advanced force-based compensation * Higher-order compliance models * Integration of additional observers Manufacturing and Implementation -------------------------------- * Detailed CAD models * Manufacturing drawings * Assembly procedures * Calibration procedures * Validation and acceptance testing Documentation Expansion ----------------------- * Implementation-level documentation * Firmware and software architecture documentation * Maintenance documentation * Operating procedures --- ## [How to Calculate the Optimal 3D Printed Lens Hood (Canon EF/OpenSCAD)](https://blog.hirnschall.net/canon-lens-hood/) meta description: How long can a 3D printed lens hood be before it vignettes? AOV maths, tulip and tubular hood geometry, and a parametric OpenSCAD tool for Canon EF/EF-S. date published: 25.05.2026 (DD.MM.YYYY format) date last modified: 25.05.2026 (DD.MM.YYYY format) --- # [3D Printing Lens Hoods: The Maths Behind a Parametric Tool](https://blog.hirnschall.net/canon-lens-hood/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: How long can a 3D printed lens hood be before it vignettes? AOV maths, tulip and tubular hood geometry, and a parametric OpenSCAD tool for Canon EF/EF-S. meta title: How to Calculate the Optimal 3D Printed Lens Hood (Canon EF/OpenSCAD) date published: 25.05.2026 (DD.MM.YYYY format) date last modified: 25.05.2026 (DD.MM.YYYY format) --- Motivation ---------- If you are into both photography and 3D printing, you will sooner or later think about printing lens hoods for your lenses. After all, they are not that cheap and just a plastic cylinder. However, after measuring and drawing the mounting system, the real problem becomes apparent: how long can the hood be before we get vignetting? Answering this question is not easy, as the manufacturer provides little information about the lens and basically none for the commercially available hoods. In this article we will explore the maths behind camera AOV, how long an optimal lens hood can be before it blocks light from hitting the sensor, and the differences between tubular and tulip hoods. Furthermore, we implement a parametric [OpenSCAD tool hosted on Thingiverse for basically all Canon EF and EF-S lenses (both tubular and tulip)](https://www.thingiverse.com/thing:7357205). Naming Conventions ------------------ For this project we will position the coordinate system at the first lens vertex. \(z\) will point towards the image plane, \(y\) will point upwards, and \(x\) will point to the right. Some projects, especially in computer graphics, have \(y\) pointing downwards. As long as we know the used convention, it does not matter. Furthermore, we will measure angles in degrees, not radians. FOV Cone Apex Position ---------------------- Looking online, we can quickly find that the camera sees a cone with angle of view \(\varphi\). Its apex is positioned at the entrance pupil. However, after talking to Bill Claff, I learned that this is apparently still a simplification. ### Off-Axis Parallel (Marginal) Rays In addition to the chief ray, we need to consider off-axis parallel rays (marginal rays) that may hit the sensor at different angles. If we were to construct the hood using the AOV and entrance pupil as the apex position, we would block these rays. This is something we do not want. ### Vignetting If we make the hood too large or long, we will start to see vignetting, i.e. the corners of the image will be darker than the center as the hood starts blocking light from the corners in. Optical Data and Ray Tracing ---------------------------- As mentioned in the motivation, the data we need is not provided by the manufacturer directly. There is, however, [the Photons to Photos Optical Bench](https://www.photonstophotos.net/GeneralTopics/Lenses/OpticalBench/OpticalBench.htm), which we can use to compute basically all the necessary optical data from the lens patent information. The optical bench tool does this by doing ray tracing using Snell's law of refraction. More information on this approach can be found in the [Photons to Photos optics primer](https://www.photonstophotos.net/GeneralTopics/Lenses/Optics_Primer/Optics_Primer.htm). For our use case we will do some additional postprocessing using python. Tulip vs. Tubular Hoods ----------------------- The two main types of lens hoods are tubular and tulip-shaped. Both have their place, but in general a tulip is closer to the optimal shape. Let's look at both designs, their advantages, and when to use which. We start with the simpler, tubular design. ### Tubular Hoods There are several reasons you might choose a tubular hood: * **Rotational symmetry**: When the lens hood mounting flange rotates during use, e.g. on lenses that do not have internal focusing like the EF-S 18-55mm kit lens, we must use a rotationally symmetric hood. * **Long focal length**: For lenses with a long focal length, the AOV becomes narrow and thus the theoretically possible hood becomes extremely long. If we cap the length at some reasonable value, we end up with a tubular hood. #### Maximum Hood Length As the lens is rotationally symmetric, the image is a circle with radius \(r\). So, we can compute the AOV and compensate for off-axis parallel rays by tracing them from their starting position at the image plane with height \(y\_{IP}=r\). Once we have found the AOV and the cone apex position, we can compute the maximum hood length starting at the hood mounting flange. For this we first have to compute the distance from the first lens vertex to the hood mounting flange as $$\begin{align} R &:= P +(I -44-L\_{measured}) - \tau + \tilde{d} \end{align}$$ where \(P\) is the entrance pupil location, \(I\) is the sensor location, \(44\) is the distance between the sensor and the EF mounting flange, \(L\_{measured}\) is the measured distance from the hood mounting flange to the EF mounting flange, \(\tau\) is an additional safety margin, and \(\tilde{d}\) is the distance between the chief ray and the outermost parallel off-axis ray in the \(z\)-direction. This is illustrated in fig. 1. ![Distance from the first lens vertex to the hood mounting flange](https://blog.hirnschall.net/canon-lens-hood/resources/img/recess.jpg) Figure 1: Distance from the first lens vertex to the hood mounting flange Now, given the AOV \(\varphi\) and the inner diameter of the hood \(D\_{inner}\), we can compute the maximum hood length as $$\begin{align} L\_{\max} = \frac{D\_{inner}}{2} \tan(90-\varphi/2) - R. \end{align}$$ Looking at fig. 2 we see where the above equation comes from. ![Maximum tubular hood length in the x-z plane](https://blog.hirnschall.net/canon-lens-hood/resources/img/hood-x-z.jpg) Figure 2: Maximum tubular hood length in the x-z plane ### Tulip Hoods Compared to a tubular hood design, the tulip shape conforms more closely to the actual image the camera sees. The main reasons to use this design are: * **Better performance**: In theory, this design should block more unwanted light than a tubular hood. How much this actually improves performance in practice is something I am not sure about. * **Wide-angle lenses**: For wide-angle lenses a tubular hood is basically useless. A tulip shape blocks unwanted light in the \(y\) direction while not blocking the wide angle of view in the \(x\) direction. #### Hood Shape and Length The construction is somewhat similar to the tubular hood. However, we will differentiate between \(x\) and \(y\) directions. Furthermore, we will not use the image circle radius as a starting position for the chief and off-axis rays, but rather the half sensor size, again in both \(x\) and \(y\) directions. To make things even more complicated, we will do so for both APS-C and full-frame sensors. After we have computed the AOV \(\varphi\) and the distance between the chief ray and the outermost parallel off-axis ray \(\tilde{d}\), we can start to construct the actual FOV cone in 3D CAD. Note that we will use the subscripts \(x\) and \(y\) to distinguish between the two directions. Once the FOV cone is constructed, we can subtract it from a tubular hood with a user-defined length \(L\_{\max,\text{user}}\). This way \(L\_{\max,\text{user}}\) is the maximum length for the tulip hood. As a final touch, we can cut the tips of the tulip hood to avoid thin, hard-to-print walls that would result from cutting the tubular hood at a sharp angle. To do so we can compute the correct height to cut as $$\begin{align} R &= P +(I -44-L\_{measured}) - \tau + \tilde{d}\_y \\ h\_x &= \frac{D\_{inner} \tan(90-\varphi\_x/2)}{2}\\ d\_y &= \frac{h\_x}{\tan(90-\varphi\_y/2)}. \end{align}$$ Note: In practice we will compute \(h - R\), for \(h\in\lbrace h\_x, h\_y\rbrace \) directly by intersecting the outermost marginal ray with the \(z\) axis. Again, the figure below (fig. 3) illustrates the equations above. ![Tulip hood tip clipping in the y-z plane](https://blog.hirnschall.net/canon-lens-hood/resources/img/hood-y-z.jpg) Figure 3: Tulip hood tip clipping in the y-z plane Hood Mounting Geometry ---------------------- Unfortunately, the hood mounting geometry seems to not be standardized among Canon lenses (as far as I know). There are two- and three-lug designs. For now, the user will have to measure the lug diameter themselves. The video shows what we have to measure. For two lenses measuring takes about three minutes. OpenSCAD -------- For this project I have decided to go with OpenSCAD for the 3D modeling. This way we can upload a parametric model to Thingiverse and have it be customizable without download. However, OpenSCAD, especially 2019.5 used by Thingiverse, has aged poorly when compared to e.g. a modern OpenCascade wrapper. For this project the lack of a revolve function for less than \(360^\circ\) was a hiccup. To fix this, we can work around it by doing \(360^\circ\) revolves and intersecting them with a circle segment of the desired angle. In OpenSCAD, this can be achieved as shown below. ``` module angle_wedge(angle, r) { polygon(concat( [[0, 0]], [for (a = [0 : 10 : angle]) [r * cos(a), r * sin(a)]], [[r * cos(angle), r * sin(angle)]] )); } module partial_rotate_extrude(angle) { intersection() { rotate_extrude() children(); translate([0,0,-500]) linear_extrude(1000) angle_wedge(angle, 1000); } } ``` Hoods Printed so Far -------------------- So far, I have printed and tested hoods for the following lenses: * [Canon EF 70-300mm F4-5.6 IS II USM](https://www.thingiverse.com/thing:7357680) * [Canon EF 28-80mm F3.5-5.6 II](https://www.thingiverse.com/thing:7357679) * [Canon EF-S 18-55mm F3.5-5.6 IS II](https://www.thingiverse.com/thing:7357678) * [Canon EF 50mm F1.8](https://www.thingiverse.com/thing:7357641) Conclusion ---------- For my first project dealing with real-world optics, the parametric hood design worked out great! Furthermore, I got the chance to talk with and learn from other people who know much more than me when it comes to this topic, which is always the biggest upside. For now the tool only supports Canon EF/EF-S lenses as that's what I own. The hood shape calculation is not dependent on the specific lens model or manufacturer; the hood mounting geometry is. If a hood mount does not rotate, the tulip shape seems to be optimal. If the user specifies a small \(L\_{\max,\text{user}}\), the shape will degenerate into a tubular hood by design. Lastly, we have successfully compensated for marginal rays. In practice we take the outermost marginal ray's free-space segment (before it enters the first element), extend it, and intersect it with the \(z\) axis. The intersection point is the effective cone apex for that field angle. This sidesteps the entrance pupil entirely, along with the field-angle dependence of its apparent position. --- ## [Symbolic FSAE Aero, Vehicle Dynamics and Physics Solver](https://blog.hirnschall.net/everything-aero/) meta description: Open-source symbolic physics calculator for aero and vehicle dynamics (FSAE). Enter knowns and assumptions, let the solver do the rest! date published: 13.05.2026 (DD.MM.YYYY format) date last modified: 13.05.2026 (DD.MM.YYYY format) --- # [A Symbolic Physics Solver for FSAE](https://blog.hirnschall.net/everything-aero/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Open-source symbolic physics calculator for aero and vehicle dynamics (FSAE). Enter knowns and assumptions, let the solver do the rest! meta title: Symbolic FSAE Aero, Vehicle Dynamics and Physics Solver date published: 13.05.2026 (DD.MM.YYYY format) date last modified: 13.05.2026 (DD.MM.YYYY format) --- About ----- Everything Aero is a symbolic aerodynamics and vehicle-dynamics calculator designed for Formula Student / Formula SAE Rules Quiz preparation and concept checking. It is open source and can easily be expanded to other fields/domains. If you want to contribute missing equations, please add an [issue or PR on GitHub](https://github.com/shirnschall/equation_manager)! Instead of hard-coded calculators, this tool uses a large library of physics-based equations (aero forces, load transfer, tyre models, drag, moments, etc.) and automatically assembles and solves the relevant system based on the inputs you provide. How to use it: * Each symbol needs to be appended with a numeric index (e.g. rho0, v0, A0, C\_L0). All available symbols and formulas are listed below the calculator. * Enter known values or constraints as equations (e.g. v0 = 30, C\_L0 = -3.2) * Optionally add assumptions that might lead to an inconsistent system (e.g. "assume no lateral weight transfer" can be added as "W\_lat0=0") * Specify which variables you want to solve for. This is used when dropping equations for over-constrained systems and when formatting the output. * There are many possible aerodynamic F\_z available. To do vehicle dynamics, tell the solver which normal force to use for the generic F\_z e.g. "F\_z0=F\_z\_df0" to use only the downforce. For e.g. Reynolds number scaling calculations, add "Re0=Re1". The solver determines which equations apply, builds a consistent system, and returns the solution along with the equations actually used. The calculator itself runs a full python environment in the browser. It therefore needs a few seconds to start and install packages with pip (in the browser environment only!). Furthermore sympy is loaded after the first **solve** button press. So the first press will take a few seconds. All known symbols and equations are listed below. If the tool fails to load, reload the page and check the console (press F12 key). Calculator ---------- Loading Python ... Usage ----- The calculator offers three input fields: Equations, Assumptions, and Looking for. Each field accepts one input per line. After entering your inputs, click **Solve** to run the symbolic solver. An example input is shown in fig. 1 below. ![Example input fields](https://blog.hirnschall.net/everything-aero/resources/img/example_input.png) Figure 1: Example input fields ### Conceptual Overview Everything Aero does not follow a fixed calculation order. Instead, it collects all known symbols and equations, automatically activates all solvable equation templates, solves the resulting system symbolically, and relaxes assumptions if the system is over-constrained. You only need to provide enough information — not a specific workflow. ### Indexed Variables All symbols must be indexed. Each index represents a separate operating point. Typical use cases include \(v\_0\), \(v\_1\) for two different speeds, \(\rho\_0\), \(\rho\_1\) for different atmospheric conditions, or \(F\_{z0}\), \(F\_{z1}\) for before and after a setup change. If your problem involves only one condition, use index `0` everywhere. Note: if any symbol is missing an index, the solver will not run. ``` v0 rho0 F_z_total1 ``` ### Input Fields #### Equations The Equations field is used for known numerical values, known relationships from the problem statement, and custom equations not already built into the solver. All equations entered here are treated as hard constraints — if the system is inconsistent, no solution will be found. ``` v0 = 30 rho0 = 1.225 Re0 = Re1 ``` #### Assumptions Assumptions are soft constraints that may override physics. This field is optional. If assumptions conflict with physical equations, the solver will try to preserve equations related to variables in the Looking for field and relax assumption equations if necessary. This mirrors how assumptions are treated in real engineering problems. ``` W_lat0 = 0 ``` #### Looking for This field is optional but strongly recommended. It serves two purposes: it highlights variables in the LaTeX solution output, and it tells the solver which equations to prioritize when resolving conflicts. ``` F_z_total_cornering_fl F_z_total_cornering_fr ``` ### Solve Once inputs are entered, click **Solve**. Scalar-only problems solve instantly; symbolic functions may take a few seconds. If no solution is found, check indexing first. ### Custom Equations Custom equations can be added directly in the Equations field, and new symbols are allowed. The example below solves [FS-Quiz Question 366](https://fs-quiz.eu/question/366) and correctly finds \(v\_1 = 86.6666\). ``` Re0 = Re1 nu0 = nu1 rho0 = rho1 ``` ![Custom equation input for FS-Quiz Question 366](https://blog.hirnschall.net/everything-aero/resources/img/custom_eq_input.png) Figure 2: Custom equation input for FS-Quiz Question 366 ### Troubleshooting If nothing happens after pressing Solve, the most likely cause is that not every symbol has an index correctly appended to it. Add the correct index and solve again. How It Works ------------ Everything Aero is built around a generic symbolic equation manager. Instead of hard-coding calculation steps, the solver dynamically assembles and solves a system of equations based on a large library of equation templates and which variables are present or provided. ### High-Level Flow When you submit inputs, the solver extracts all symbols and creates indexed variables, then activates any equation templates that are solvable given the known symbols. A symbolic system is assembled and solved using SymPy. If the system is over-constrained, assumptions are relaxed iteratively until a solution is found. The result may include functions, not just scalar values. ### Architecture The calculator is split into three independent parts: the equation engine (generic and reusable), equation templates (domain-specific physics), and the web UI built with [ngapp](https://cerbsim.github.io/ngapp). This separation makes it straightforward to add new equations, create calculators for other domains, or extend the physics without touching the solver. Deployment and documentation are handled automatically via GitHub Actions. ### Equation Templates All physics equations are defined as templates and registered with the equation manager. A Reynolds number template looks like this: ``` aero_eq_manager.add_equation_template( equations_to_add=["Re{{i}}=L{{i}} * rho{{i}} * v{{i}} / eta{{i}}"], relevant_vars=[ ("Re", "Reynolds number [-]"), ("L", "Characteristic length [m]"), ("rho", "Air density [kg/m³]"), ("v", "Velocity [m/s]"), ("eta", "Dynamic viscosity [Pa·s]") ], vars_to_check=[(["Re", "L"], 1)], name="Reynolds Number" ) ``` `equations_to_add` is a list of strings in `lhs = rhs` format. Symbols use `{{i}}` and `{{j}}` as index placeholders, and multi-index equations are expanded automatically. `relevant_vars` is a list of `(symbol, description)` tuples used for documentation and UI clarity. Descriptions should be precise to avoid input errors. `vars_to_check` is a list of `([symbols], n)` tuples that gate template activation: a template is only added to the system if at least `n` of the listed symbols already exist. This prevents unsolvable equations from being included and keeps the solver fast. ### Assumption Handling If the system is over-constrained, equations involving assumption symbols are selectively removed in an iterative process. Variables listed in Looking for are preserved where possible, and relaxation continues until a solution is found or all options are exhausted. ### Output Solutions are symbolic and displayed in LaTeX, with variables from the Looking for field highlighted. This allows further analysis such as differentiation or evaluation at specific parameter values. FS Rules Quiz Quick Start ------------------------- This page shows how to use Everything Aero to solve typical FS Rules Quiz problems quickly and reliably. If the problem can be solved with equations, this tool can usually handle it with minimal setup. ### Mental Model Everything Aero is not a step-by-step calculator. You do not choose formulas manually, decide calculation order, or worry about solving intermediate variables. You only write down what the problem gives you, add any explicit assumptions, and tell the solver what you want to find. The solver figures out the rest. ### Every Symbol Must Have an Index Every variable must have an index appended. If the problem describes only one condition, use index `0` throughout. The following is correct: ``` v0 = 30 rho0 = 1.225 ``` Writing `v = 30` or `rho = 1.225` without an index will prevent the solver from running. ### Typical FS-Quiz Workflow #### Step 1: Write down given values Copy everything the problem gives you numerically into the **Equations** field. ``` v0 = 30 rho0 = 1.225 C_L0 = -3.0 A0 = 1.2 m0 = 280 ``` #### Step 2: Add stated assumptions If the problem states things like "assume no lateral load transfer", "neglect drag", or "ignore aerodynamic moments", input them in **Assumptions**. Assumptions are treated as soft constraints and can be relaxed if needed. ``` W_lat0 = 0 ``` #### Step 3: Ask the question Put exactly what the question asks for into **Looking for**. This tells the solver what to highlight in the output and what to prioritize if equations conflict. This field is optional but strongly recommended. ``` Re0 F_z_total_cornering_fl v1 ``` ### Example 1: Reynolds Number A car drives at 30 m/s with a characteristic length of 0.3 m, air density 1.225 kg/m³, and dynamic viscosity 1.8×10⁻⁵ Pa·s. We want the Reynolds number. Equations: ``` v0 = 30 L0 = 0.3 rho0 = 1.225 eta0 = 1.8e-5 ``` Looking for: ``` Re0 ``` The solver returns \(Re\_0 = 612500.0\). ### Example 2: Wind Tunnel Scaling A 50% scale model must match the Reynolds number of the full car. Air properties are identical and full-scale speed is 30 m/s. We want the required tunnel speed. Equations: ``` Re0 = Re1 L0 = 1.0 L1 = 0.5 v0 = 30 rho0 = rho1 eta0 = eta1 ``` Looking for: ``` v1 ``` The solver returns \(v\_1 = 60.0\). ### Final Advice When in doubt: put everything the problem gives you in **Equations**, put stated assumptions in **Assumptions**, and put the question in **Looking for**. Let the solver do the rest. Known Symbols ------------- | | | | --- | --- | | **A** | Flow area [m²] | | **A\_ref** | Aerodynamic reference area [m²] | | **C\_L** | Lift coefficient [-] | | **C\_d** | Drag coefficient [-] | | **C\_rr** | Rolling resistance coefficient [-] | | **C\_y** | Side force coefficient [-] | | **Delta\_F\_z** | Change in tyre normal load [N] | | **Delta\_Fz\_drag\_f** | Front axle normal load change from drag moment [N] | | **Delta\_Fz\_drag\_fl** | Front-left normal load change from drag moment [N] | | **Delta\_Fz\_drag\_fr** | Front-right normal load change from drag moment [N] | | **Delta\_Fz\_drag\_r** | Rear axle normal load change from drag moment [N] | | **Delta\_Fz\_drag\_rl** | Rear-left normal load change from drag moment [N] | | **Delta\_Fz\_drag\_rr** | Rear-right normal load change from drag moment [N] | | **Delta\_Fz\_lat** | Total lateral normal load transfer [N] | | **Delta\_Fz\_lat\_f** | Front axle lateral load transfer [N] | | **Delta\_Fz\_lat\_fl** | Front-left load change from lateral effects [N] | | **Delta\_Fz\_lat\_fr** | Front-right load change from lateral effects [N] | | **Delta\_Fz\_lat\_r** | Rear axle lateral load transfer [N] | | **Delta\_Fz\_lat\_rl** | Rear-left load change from lateral effects [N] | | **Delta\_Fz\_lat\_rr** | Rear-right load change from lateral effects [N] | | **Delta\_Fz\_long** | Total longitudinal normal load transfer [N] | | **Delta\_Fz\_long\_f** | Front axle normal load change from longitudinal effects [N] | | **Delta\_Fz\_long\_fl** | Front-left load change from longitudinal effects [N] | | **Delta\_Fz\_long\_fr** | Front-right load change from longitudinal effects [N] | | **Delta\_Fz\_long\_geom** | Longitudinal load transfer via suspension geometry [N] | | **Delta\_Fz\_long\_r** | Rear axle normal load change from longitudinal effects [N] | | **Delta\_Fz\_long\_rl** | Rear-left load change from longitudinal effects [N] | | **Delta\_Fz\_long\_rr** | Rear-right load change from longitudinal effects [N] | | **Delta\_Fz\_long\_total** | Net longitudinal load transfer [N] | | **Delta\_delta\_z\_tire** | Change in tyre deflection [m] | | **F\_bump** | Bump stop force [N] (active only if delta\_z\_bump > 0) | | **F\_centripetal** | Required centripetal (lateral) force [N] | | **F\_d** | Aerodynamic drag force [N] | | **F\_f\_max** | Maximum available friction force magnitude [N] | | **F\_rr** | Rolling resistance force [N] | | **F\_slope** | Longitudinal force due to road slope [N] | | **F\_spring** | Suspension spring force [N] | | **F\_tan** | Resultant tangential force [N] | | **F\_thrust\_req** | Required thrust force [N] | | **F\_x\_drive** | Driven longitudinal force [N] | | **F\_x\_inertia** | Longitudinal inertial force (vehicle frame) [N] | | **F\_x\_max** | Maximum longitudinal tyre force [N] | | **F\_x\_tire** | Longitudinal tyre force [N] | | **F\_x\_tire\_max** | Maximum available longitudinal tyre force [N] | | **F\_y\_aero** | Aerodynamic side force [N] | | **F\_y\_available** | Available lateral force [N] | | **F\_y\_inertia** | Lateral inertial force [N] | | **F\_y\_max** | Maximum lateral force [N] | | **F\_z** | Generic normal force [N] | | **F\_z\_arb\_left** | ARB vertical force at left wheel [N] | | **F\_z\_arb\_right** | ARB vertical force at right wheel [N] | | **F\_z\_df** | Aerodynamic downforce [N] | | **F\_z\_df\_f** | Front axle aerodynamic downforce [N] | | **F\_z\_df\_fl** | Front-left aerodynamic downforce [N] | | **F\_z\_df\_fr** | Front-right aerodynamic downforce [N] | | **F\_z\_df\_r** | Rear axle aerodynamic downforce [N] | | **F\_z\_df\_rl** | Rear-left aerodynamic downforce [N] | | **F\_z\_df\_rr** | Rear-right aerodynamic downforce [N] | | **F\_z\_f** | Front axle normal force [N] | | **F\_z\_fl** | Front-left wheel normal force [N] | | **F\_z\_fr** | Front-right wheel normal force [N] | | **F\_z\_r** | Rear axle normal force [N] | | **F\_z\_rl** | Rear-left wheel normal force [N] | | **F\_z\_rr** | Rear-right wheel normal force [N] | | **F\_z\_static** | Static normal force [N] | | **F\_z\_static\_df\_drag\_fl** | Front-left normal force: static + downforce + drag moment [N] | | **F\_z\_static\_df\_drag\_fr** | Front-right normal force: static + downforce + drag moment [N] | | **F\_z\_static\_df\_drag\_rl** | Rear-left normal force: static + downforce + drag moment [N] | | **F\_z\_static\_df\_drag\_rr** | Rear-right normal force: static + downforce + drag moment [N] | | **F\_z\_static\_df\_f** | Front axle normal force: static + downforce [N] | | **F\_z\_static\_df\_fl** | Front-left normal force: static + downforce [N] | | **F\_z\_static\_df\_fr** | Front-right normal force: static + downforce [N] | | **F\_z\_static\_df\_r** | Rear axle normal force: static + downforce [N] | | **F\_z\_static\_df\_rl** | Rear-left normal force: static + downforce [N] | | **F\_z\_static\_df\_rr** | Rear-right normal force: static + downforce [N] | | **F\_z\_static\_f** | Static front axle normal force [N] | | **F\_z\_static\_fl** | Static front-left normal force [N] | | **F\_z\_static\_fr** | Static front-right normal force [N] | | **F\_z\_static\_r** | Static rear axle normal force [N] | | **F\_z\_static\_rl** | Static rear-left normal force [N] | | **F\_z\_static\_rr** | Static rear-right normal force [N] | | **F\_z\_susp** | Vertical force transmitted by suspension at wheel [N] | | **F\_z\_susp\_total** | Total suspension vertical force at wheel [N] | | **I\_z** | Yaw moment of inertia [kg·m²] | | **L** | Characteristic length [m] | | **L\_ISA** | ISA temperature lapse rate [K/m] | | **MR** | Suspension motion ratio (spring / wheel) [-] | | **M\_pitch\_aero** | Total aerodynamic pitching moment about CG [N·m] | | **M\_pitch\_df** | Pitching moment caused by downforce distribution [N·m] | | **M\_pitch\_drag** | Pitching moment caused by drag force [N·m] | | **M\_yaw\_aero** | Aerodynamic yawing moment about CG [N·m] | | **M\_z** | Sum of yaw moments about CG [N·m] | | **P\_required** | Required propulsion power [W] | | **P\_trac** | Tractive power at the wheels [W] | | **P\_x** | Longitudinal power at wheels [W] | | **R** | Specific gas constant for air [J/(kg·K)] | | **R\_turn** | Corner radius [m] | | **Re** | Reynolds number [-] | | **T** | Ambient temperature [K] | | **T\_0** | Sea level temperature [K] | | **T\_arb** | Anti-roll bar torque equivalent [N·m] | | **W** | Vehicle weight (gravitational force) [N] | | **a\_CG** | CG distance to front axle [m] | | **a\_brake\_aero** | Maximum braking deceleration with aerodynamic downforce [m/s²] | | **a\_lat** | Lateral acceleration [m/s²] | | **a\_lat\_max** | Maximum lateral acceleration (friction-limited) [m/s²] | | **a\_long** | Longitudinal acceleration (positive forward) [m/s²] | | **a\_long\_max** | Maximum longitudinal acceleration (friction-limited) [m/s²] | | **a\_x\_max** | Maximum traction-limited longitudinal acceleration [m/s²] | | **b\_CG** | CG distance to rear axle [m] | | **balance\_df** | Aerodynamic balance (front fraction) [-] | | **balance\_lat** | Lateral load transfer balance (front fraction) [-] | | **beta\_sideslip** | Vehicle sideslip angle at CG [rad] | | **delta\_avg** | Average front steering angle [rad] | | **delta\_inner** | Inner wheel steering angle [rad] | | **delta\_outer** | Outer wheel steering angle [rad] | | **delta\_z\_bump** | Bump stop compression [m] | | **delta\_z\_clearance** | Bump stop clearance [m] | | **delta\_z\_heave** | Chassis heave displacement [m] | | **delta\_z\_spring** | Spring compression [m] | | **delta\_z\_susp** | Suspension vertical deflection [m] | | **delta\_z\_tire** | Tyre vertical deflection [m] | | **delta\_z\_wheel** | Wheel vertical displacement [m] | | **delta\_z\_wheel\_f** | Front axle wheel vertical displacement [m] | | **delta\_z\_wheel\_left** | Left wheel vertical displacement [m] | | **delta\_z\_wheel\_r** | Rear axle wheel vertical displacement [m] | | **delta\_z\_wheel\_right** | Right wheel vertical displacement [m] | | **eta** | Dynamic viscosity [Pa·s] | | **eta\_aero** | Aerodynamic efficiency (lift-to-drag ratio) [-] | | **f\_load** | Dimensionless load influence factor [-] | | **friction\_util** | Friction utilisation ratio (≤1 = within grip) [-] | | **g** | Gravitational acceleration [m/s²] | | **h** | Geometric height of point [m] | | **k\_arb** | Anti-roll bar stiffness [N/m] | | **k\_bump** | Bump stop stiffness [N/m] | | **k\_roll\_f** | Front axle roll stiffness [N·m/rad] | | **k\_roll\_r** | Rear axle roll stiffness [N·m/rad] | | **k\_spring** | Suspension spring stiffness [N/m] | | **k\_tire** | Tyre vertical stiffness [N/m] | | **k\_vert** | Effective vertical stiffness at wheel [N/m] | | **k\_wheel** | Suspension wheel rate [N/m] | | **l\_wb** | Wheelbase [m] | | **m** | Vehicle mass [kg] | | **mu** | Tyre-road friction coefficient [-] | | **mu\_0** | Reference friction coefficient [-] | | **mu\_load\_dep** | Load-dependent friction coefficient [-] | | **mu\_long** | Longitudinal friction coefficient [-] | | **nu** | Kinematic viscosity [m²/s] | | **p** | Static pressure [Pa] | | **p\_0** | Sea level standard atmospheric pressure [Pa] | | **phi\_roll\_f** | Front roll stiffness distribution [-] | | **r\_dot** | Yaw acceleration [rad/s²] | | **rho** | Density at altitude h [kg/m³] | | **rho\_0** | Sea level standard density [kg/m³] | | **s\_coast** | Coast-down distance [m] | | **t\_coast** | Time to coast from v to v\_final [s] | | **t\_track** | Track width [m] | | **theta\_anti** | Anti-dive / anti-squat angle [rad] | | **theta\_bank** | Bank angle [rad] | | **theta\_pitch** | Pitch angle (small-angle) [rad] | | **theta\_slope** | Road slope angle (positive uphill) [rad] | | **v** | Vehicle speed relative to air [m/s] | | **v\_final** | Final speed [m/s] | | **v\_max\_corner** | Maximum cornering speed (friction-limited) [m/s] | | **v\_x** | Longitudinal velocity [m/s] | | **v\_y** | Lateral velocity component [m/s] | | **x\_CG** | Center of gravity position [m] | | **x\_COP** | Center of pressure position [m] | | **x\_rel\_aero** | Longitudinal lever arm from CG to COP [m] | | **z\_CG** | Center of gravity height [m] | | **z\_COP** | Aerodynamic center height [m] | | **z\_chassis** | Chassis reference vertical position [m] | | **z\_rel\_aero** | Vertical lever arm from CG to COP [m] | | **z\_wheel** | Wheel center vertical position [m] | Known Equations --------------- Bernoulli Equation (two points) $$ \begin{aligned} g\_{i} h\_{i} \rho\_{i} + p\_{i} + \frac{\rho\_{i} v\_{i}^{2}}{2} &= g\_{j} h\_{j} \rho\_{j} + p\_{j} + \frac{\rho\_{j} v\_{j}^{2}}{2} \end{aligned} $$ Continuity Equation for Incompressible Flow $$ \begin{aligned} \frac{v\_{i}}{v\_{j}} &= \frac{A\_{j}}{A\_{i}} \end{aligned} $$ Reynolds Number $$ \begin{aligned} Re\_{i} &= \frac{L\_{i} \rho\_{i} v\_{i}}{\eta\_{i}} \end{aligned} $$ Kinematic Viscosity Definition $$ \begin{aligned} \nu\_{i} &= \frac{\eta\_{i}}{\rho\_{i}} \end{aligned} $$ Aerodynamic Drag Force $$ \begin{aligned} F\_{d i} &= \frac{A\_{ref i} C\_{d i} \rho\_{i} v\_{i}^{2}}{2} \end{aligned} $$ Aerodynamic Lateral Force $$ \begin{aligned} F\_{y aero i} &= \frac{A\_{ref i} C\_{y i} \rho\_{i} v\_{i}^{2}}{2} \end{aligned} $$ Aerodynamic Yawing Moment $$ \begin{aligned} M\_{yaw aero i} &= F\_{y aero i} x\_{rel aero i} \end{aligned} $$ Vehicle Speed from Velocity Components $$ \begin{aligned} v\_{i} &= \sqrt{v\_{x i}^{2} + v\_{y i}^{2}} \end{aligned} $$ Vehicle Slip Angle at CG $$ \begin{aligned} \beta\_{sideslip i} &= \operatorname{atan}{\left(\frac{v\_{y i}}{v\_{x i}} \right)} \end{aligned} $$ Longitudinal Force Equilibrium $$ \begin{aligned} - F\_{d i} - F\_{rr i} + F\_{x drive i} - F\_{x inertia i} &= 0 \end{aligned} $$ Rolling Resistance Force $$ \begin{aligned} F\_{rr i} &= C\_{rr i} F\_{z i} \end{aligned} $$ Longitudinal Power from Force $$ \begin{aligned} P\_{x i} &= F\_{x drive i} v\_{i} \end{aligned} $$ Yaw Moment Equation of Motion $$ \begin{aligned} M\_{z i} &= I\_{z i} r\_{dot i} \end{aligned} $$ Maximum Longitudinal Tyre Force $$ \begin{aligned} F\_{x max i} &= F\_{z i} \mu\_{long i} \end{aligned} $$ Axle Normal Force from Wheel Loads $$ \begin{aligned} F\_{z f i} &= F\_{z fl i} + F\_{z fr i} \\ F\_{z r i} &= F\_{z rl i} + F\_{z rr i} \end{aligned} $$ Ideal Gas Law $$ \begin{aligned} p\_{i} &= R\_{i} T\_{i} \rho\_{i} \end{aligned} $$ ISA Temperature Lapse Rate (Assumption) $$ \begin{aligned} T\_{i} &= - L\_{ISA i} h\_{i} + T\_{0 i} \end{aligned} $$ Coast-Down Time (Drag Only) $$ \begin{aligned} t\_{coast i} &= \frac{2.0 m\_{i} \log{\left(\frac{v\_{i}}{v\_{final i}} \right)}}{A\_{ref i} C\_{d i} \rho\_{i} v\_{i}} \end{aligned} $$ Coast-Down Distance (Drag Only) $$ \begin{aligned} s\_{coast i} &= \frac{2.0 m\_{i} \left(- v\_{final i} + v\_{i}\right)}{A\_{ref i} C\_{d i} \rho\_{i}} \end{aligned} $$ Required Thrust vs Speed $$ \begin{aligned} F\_{thrust req i} &= F\_{d i} + F\_{x inertia i} \end{aligned} $$ Required Power vs Speed $$ \begin{aligned} P\_{required i} &= F\_{thrust req i} v\_{i} \end{aligned} $$ Continuity Equation (Mass Conservation) $$ \begin{aligned} A\_{i} \rho\_{i} v\_{i} &= A\_{j} \rho\_{j} v\_{j} \end{aligned} $$ CG Distance Definitions (Front / Rear) $$ \begin{aligned} a\_{CG i} &= x\_{CG i} \\ b\_{CG i} &= l\_{wb i} - x\_{CG i} \end{aligned} $$ Tyre Vertical Stiffness Definition $$ \begin{aligned} k\_{tire i} &= \frac{F\_{z i}}{\delta\_{z tire i}} \end{aligned} $$ Tyre Deflection Change from Load Change $$ \begin{aligned} \Delta\_{\delta z tire i} &= \frac{\Delta\_{F z i}}{k\_{tire i}} \end{aligned} $$ Wheel Vertical Position from Tyre Deflection $$ \begin{aligned} z\_{wheel i} &= - \delta\_{z tire i} + z\_{chassis i} \end{aligned} $$ Suspension Motion Ratio (Displacement) $$ \begin{aligned} \delta\_{z spring i} &= MR\_{i} \delta\_{z wheel i} \end{aligned} $$ Suspension Spring Force $$ \begin{aligned} F\_{spring i} &= \delta\_{z spring i} k\_{spring i} \end{aligned} $$ Suspension Force at Wheel $$ \begin{aligned} F\_{z susp i} &= \frac{F\_{spring i}}{MR\_{i}} \end{aligned} $$ Wheel Vertical Displacement Decomposition $$ \begin{aligned} \delta\_{z wheel i} &= \delta\_{z susp i} + \delta\_{z tire i} \end{aligned} $$ Suspension Deflection from Wheel Load $$ \begin{aligned} \delta\_{z susp i} &= \frac{F\_{z i} MR\_{i}}{k\_{spring i}} \end{aligned} $$ Effective Wheel Rate $$ \begin{aligned} k\_{wheel i} &= \frac{k\_{spring i}}{MR\_{i}^{2}} \end{aligned} $$ Anti-Roll Bar Torque from Wheel Displacement $$ \begin{aligned} T\_{arb i} &= k\_{arb i} \left(\delta\_{z wheel left i} - \delta\_{z wheel right i}\right) \end{aligned} $$ Anti-Roll Bar Vertical Wheel Forces $$ \begin{aligned} F\_{z arb left i} &= \frac{T\_{arb i}}{t\_{track i}} \\ F\_{z arb right i} &= - \frac{T\_{arb i}}{t\_{track i}} \end{aligned} $$ Wheel Displacement from Heave and Pitch $$ \begin{aligned} \delta\_{z wheel f i} &= a\_{CG i} \theta\_{pitch i} + \delta\_{z heave i} \\ \delta\_{z wheel r i} &= - b\_{CG i} \theta\_{pitch i} + \delta\_{z heave i} \end{aligned} $$ Bump Stop Engagement Deflection $$ \begin{aligned} \delta\_{z bump i} &= - \delta\_{z clearance i} + \delta\_{z susp i} \end{aligned} $$ Bump Stop Force (Linear, Engaged Only) $$ \begin{aligned} F\_{bump i} &= \delta\_{z bump i} k\_{bump i} \end{aligned} $$ Total Suspension Force Including Bump Stop $$ \begin{aligned} F\_{z susp total i} &= F\_{bump i} + F\_{z susp i} \end{aligned} $$ Geometric Longitudinal Load Transfer (Anti-Dive / Anti-Squat) $$ \begin{aligned} \Delta\_{Fz long geom i} &= F\_{x tire i} \tan{\left(\theta\_{anti i} \right)} \end{aligned} $$ Net Longitudinal Load Transfer Including Geometry $$ \begin{aligned} \Delta\_{Fz long total i} &= - \Delta\_{Fz long geom i} + \Delta\_{Fz long i} \end{aligned} $$ Effective Vertical Stiffness (Tyre + Suspension) $$ \begin{aligned} \frac{1}{k\_{vert i}} &= \frac{1}{k\_{wheel i}} + \frac{1}{k\_{tire i}} \end{aligned} $$ Roll Stiffness Distribution (Front Fraction) $$ \begin{aligned} \phi\_{roll f i} &= \frac{k\_{roll f i}}{k\_{roll f i} + k\_{roll r i}} \end{aligned} $$ Ackermann Inner Wheel Steering Angle $$ \begin{aligned} \delta\_{inner i} &= \operatorname{atan}{\left(\frac{l\_{wb i}}{R\_{turn i} - \frac{t\_{track i}}{2}} \right)} \end{aligned} $$ Ackermann Outer Wheel Steering Angle $$ \begin{aligned} \delta\_{outer i} &= \operatorname{atan}{\left(\frac{l\_{wb i}}{R\_{turn i} + \frac{t\_{track i}}{2}} \right)} \end{aligned} $$ Average Steering Angle (Bicycle Equivalent) $$ \begin{aligned} \delta\_{avg i} &= \frac{\delta\_{inner i}}{2} + \frac{\delta\_{outer i}}{2} \end{aligned} $$ Turn Radius from Steering Angle $$ \begin{aligned} R\_{turn i} &= \frac{l\_{wb i}}{\tan{\left(\delta\_{avg i} \right)}} \end{aligned} $$ Available Lateral Force on Banked Surface (for flat roads, theta\_bank=0) $$ \begin{aligned} F\_{y available i} &= F\_{z i} \left(\mu\_{i} \cos{\left(\theta\_{bank i} \right)} + \sin{\left(\theta\_{bank i} \right)}\right) \end{aligned} $$ Banked Turn Lateral Force Limit $$ \begin{aligned} F\_{centripetal i} &= F\_{y available i} \end{aligned} $$ Aerodynamic Downforce Axle Split $$ \begin{aligned} F\_{z df f i} &= F\_{z df i} balance\_{df i} \\ F\_{z df r i} &= F\_{z df i} \left(1 - balance\_{df i}\right) \end{aligned} $$ Aerodynamic Efficiency (L/D) $$ \begin{aligned} \eta\_{aero i} &= \frac{C\_{L i}}{C\_{d i}} \end{aligned} $$ Barometric Formula $$ \begin{aligned} p\_{i} &= p\_{0 i} e^{- \frac{g\_{i} h\_{i} \rho\_{0 i}}{p\_{0 i}}} \end{aligned} $$ Density Variation with Altitude $$ \begin{aligned} \rho\_{i} &= \rho\_{0 i} e^{- \frac{g\_{i} h\_{i} \rho\_{0 i}}{p\_{0 i}}} \end{aligned} $$ Aerodynamic Lever Arms relative to CG $$ \begin{aligned} x\_{rel aero i} &= - x\_{CG i} + x\_{COP i} \\ z\_{rel aero i} &= - z\_{CG i} + z\_{COP i} \end{aligned} $$ Aerodynamic Pitching Moment about CG $$ \begin{aligned} M\_{pitch aero i} &= F\_{d i} z\_{rel aero i} - F\_{z df i} x\_{rel aero i} \end{aligned} $$ Aerodynamic Balance from COP Location $$ \begin{aligned} balance\_{df i} &= \frac{l\_{wb i} - x\_{COP i}}{l\_{wb i}} \end{aligned} $$ Pitching Moment from Downforce Distribution $$ \begin{aligned} M\_{pitch df i} &= F\_{z df i} \left(x\_{CG i} - x\_{COP i}\right) \end{aligned} $$ Pitching Moment from Drag Force $$ \begin{aligned} M\_{pitch drag i} &= F\_{d i} \left(z\_{CG i} - z\_{COP i}\right) \end{aligned} $$ Longitudinal Inertial Force $$ \begin{aligned} F\_{x inertia i} &= a\_{long i} m\_{i} \end{aligned} $$ Total Longitudinal Load Transfer (mass based) $$ \begin{aligned} \Delta\_{Fz long i} &= \frac{F\_{x inertia i} z\_{CG i}}{l\_{wb i}} \end{aligned} $$ Longitudinal Load Transfer Axle Split $$ \begin{aligned} \Delta\_{Fz long f i} &= - \Delta\_{Fz long i} \\ \Delta\_{Fz long r i} &= \Delta\_{Fz long i} \end{aligned} $$ Longitudinal Load Transfer Per Wheel $$ \begin{aligned} \Delta\_{Fz long fl i} &= \frac{\Delta\_{Fz long f i}}{2} \\ \Delta\_{Fz long fr i} &= \frac{\Delta\_{Fz long f i}}{2} \\ \Delta\_{Fz long rl i} &= \frac{\Delta\_{Fz long r i}}{2} \\ \Delta\_{Fz long rr i} &= \frac{\Delta\_{Fz long r i}}{2} \end{aligned} $$ Lateral Inertial Force $$ \begin{aligned} F\_{y inertia i} &= a\_{lat i} m\_{i} \end{aligned} $$ Lateral Acceleration in a Turn $$ \begin{aligned} a\_{lat i} &= \frac{v\_{i}^{2}}{R\_{turn i}} \end{aligned} $$ Total Lateral Load Transfer $$ \begin{aligned} \Delta\_{Fz lat i} &= \frac{F\_{y inertia i} z\_{CG i}}{t\_{track i}} \end{aligned} $$ Lateral Load Transfer Axle Distribution $$ \begin{aligned} \Delta\_{Fz lat f i} &= \Delta\_{Fz lat i} balance\_{lat i} \\ \Delta\_{Fz lat r i} &= \Delta\_{Fz lat i} \left(1 - balance\_{lat i}\right) \end{aligned} $$ Lateral Load Transfer Per Wheel (clockwise positive turn) $$ \begin{aligned} \Delta\_{Fz lat fl i} &= \frac{\Delta\_{Fz lat f i}}{2} \\ \Delta\_{Fz lat fr i} &= - \frac{\Delta\_{Fz lat f i}}{2} \\ \Delta\_{Fz lat rl i} &= \frac{\Delta\_{Fz lat r i}}{2} \\ \Delta\_{Fz lat rr i} &= - \frac{\Delta\_{Fz lat r i}}{2} \end{aligned} $$ Vehicle Weight Definition $$ \begin{aligned} W\_{i} &= g\_{i} m\_{i} \end{aligned} $$ Static Normal Force (Total) $$ \begin{aligned} F\_{z static i} &= W\_{i} \end{aligned} $$ Static Normal Force Per Wheel $$ \begin{aligned} F\_{z static fl i} &= \frac{F\_{z static f i}}{2} \\ F\_{z static fr i} &= \frac{F\_{z static f i}}{2} \\ F\_{z static rl i} &= \frac{F\_{z static r i}}{2} \\ F\_{z static rr i} &= \frac{F\_{z static r i}}{2} \end{aligned} $$ Drag-Induced Normal Load Shift Per Wheel $$ \begin{aligned} \Delta\_{Fz drag fl i} &= \frac{\Delta\_{Fz drag f i}}{2} \\ \Delta\_{Fz drag fr i} &= \frac{\Delta\_{Fz drag f i}}{2} \\ \Delta\_{Fz drag rl i} &= \frac{\Delta\_{Fz drag r i}}{2} \\ \Delta\_{Fz drag rr i} &= \frac{\Delta\_{Fz drag r i}}{2} \end{aligned} $$ Normal Force: Static + Downforce (Axle) $$ \begin{aligned} F\_{z static df f i} &= F\_{z df f i} + F\_{z static f i} \\ F\_{z static df r i} &= F\_{z df r i} + F\_{z static r i} \end{aligned} $$ Normal Force: Static + Downforce (Per Wheel) $$ \begin{aligned} F\_{z static df fl i} &= F\_{z df fl i} + F\_{z static fl i} \\ F\_{z static df fr i} &= F\_{z df fr i} + F\_{z static fr i} \\ F\_{z static df rl i} &= F\_{z df rl i} + F\_{z static rl i} \\ F\_{z static df rr i} &= F\_{z df rr i} + F\_{z static rr i} \end{aligned} $$ Normal Force: Static + Downforce + Drag Moment (Per Wheel) $$ \begin{aligned} F\_{z static df drag fl i} &= \Delta\_{Fz drag fl i} + F\_{z static df fl i} \\ F\_{z static df drag fr i} &= \Delta\_{Fz drag fr i} + F\_{z static df fr i} \\ F\_{z static df drag rl i} &= \Delta\_{Fz drag rl i} + F\_{z static df rl i} \\ F\_{z static df drag rr i} &= \Delta\_{Fz drag rr i} + F\_{z static df rr i} \end{aligned} $$ Static Normal Force Axle Split $$ \begin{aligned} F\_{z static f i} &= \frac{W\_{i} \left(l\_{wb i} - x\_{CG i}\right)}{l\_{wb i}} \\ F\_{z static r i} &= \frac{W\_{i} x\_{CG i}}{l\_{wb i}} \end{aligned} $$ Aerodynamic Downforce (Total) $$ \begin{aligned} F\_{z df i} &= \frac{A\_{ref i} C\_{L i} \rho\_{i} v\_{i}^{2}}{2} \end{aligned} $$ Aerodynamic Downforce Per Wheel $$ \begin{aligned} F\_{z df fl i} &= \frac{F\_{z df f i}}{2} \\ F\_{z df fr i} &= \frac{F\_{z df f i}}{2} \\ F\_{z df rl i} &= \frac{F\_{z df r i}}{2} \\ F\_{z df rr i} &= \frac{F\_{z df r i}}{2} \end{aligned} $$ Drag-Induced Normal Load Shift (Axle) $$ \begin{aligned} \Delta\_{Fz drag f i} &= \frac{M\_{pitch drag i}}{l\_{wb i}} \\ \Delta\_{Fz drag r i} &= - \frac{M\_{pitch drag i}}{l\_{wb i}} \end{aligned} $$ Drag-Induced Normal Load Shift (Per Wheel) $$ \begin{aligned} \Delta\_{Fz drag fl i} &= \frac{\Delta\_{Fz drag f i}}{2} \\ \Delta\_{Fz drag fr i} &= \frac{\Delta\_{Fz drag f i}}{2} \\ \Delta\_{Fz drag rl i} &= \frac{\Delta\_{Fz drag r i}}{2} \\ \Delta\_{Fz drag rr i} &= \frac{\Delta\_{Fz drag r i}}{2} \end{aligned} $$ Maximum Friction Force $$ \begin{aligned} F\_{f max i} &= F\_{z i} \mu\_{i} \end{aligned} $$ Load-Dependent Friction Coefficient (Generic Model) $$ \begin{aligned} \mu\_{load dep i} &= f\_{load i} \mu\_{0 i} \end{aligned} $$ Maximum Longitudinal Acceleration (Friction-Limited) $$ \begin{aligned} a\_{long max i} &= \frac{F\_{z i} \mu\_{i}}{m\_{i}} \end{aligned} $$ Maximum Lateral Acceleration (Friction-Limited) $$ \begin{aligned} a\_{lat max i} &= \frac{F\_{z i} \mu\_{i}}{m\_{i}} \end{aligned} $$ Maximum Cornering Speed (Friction-Limited) $$ \begin{aligned} v\_{max corner i} &= \sqrt{\frac{F\_{z i} R\_{turn i} \mu\_{i}}{m\_{i}}} \end{aligned} $$ Resultant Tangential Force $$ \begin{aligned} F\_{tan i} &= \sqrt{F\_{x tire i}^{2} + F\_{y inertia i}^{2}} \end{aligned} $$ Friction Circle Utilisation $$ \begin{aligned} friction\_{util i} &= \frac{F\_{tan i}}{F\_{z i} \mu\_{i}} \end{aligned} $$ Friction Ellipse Model $$ \begin{aligned} \frac{F\_{x tire i}^{2}}{F\_{x tire max i}^{2}} + \frac{F\_{y inertia i}^{2}}{F\_{y max i}^{2}} &= 1 \end{aligned} $$ Tractive Power Definition $$ \begin{aligned} P\_{trac i} &= F\_{x tire i} v\_{i} \end{aligned} $$ Braking Deceleration with Aerodynamic Downforce $$ \begin{aligned} a\_{brake aero i} &= \frac{\mu\_{i} \left(F\_{z df i} + F\_{z static i}\right)}{m\_{i}} \end{aligned} $$ Centripetal Force Requirement $$ \begin{aligned} F\_{centripetal i} &= \frac{m\_{i} v\_{i}^{2}}{R\_{turn i}} \end{aligned} $$ Road Slope Longitudinal Force $$ \begin{aligned} F\_{slope i} &= g\_{i} m\_{i} \sin{\left(\theta\_{slope i} \right)} \end{aligned} $$ Maximum Longitudinal Acceleration (Traction-Limited) $$ \begin{aligned} a\_{x max i} &= \frac{F\_{x tire max i}}{m\_{i}} \end{aligned} $$ --- ## [Maxwell Bridge Calculator — Inductance and Series Resistance](https://blog.hirnschall.net/tools/maxwell-bridge/) meta description: Calculate unknown inductance and series resistance from a balanced Maxwell-Wien bridge. Enter R2, R3, R4, C4 at null; L_x, R_x, and Q are computed. date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- # [Maxwell Bridge Calculator](https://blog.hirnschall.net/tools/maxwell-bridge/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate unknown inductance and series resistance from a balanced Maxwell-Wien bridge. Enter R2, R3, R4, C4 at null; L_x, R_x, and Q are computed. meta title: Maxwell Bridge Calculator — Inductance and Series Resistance date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- Calculator ---------- Enter the four bridge components at balance. The unknown inductance \( L\_x \) and its series resistance \( R\_x \) are calculated from the balance conditions. Providing the test frequency also yields the quality factor \( Q \). * R2 (ratio arm): * Ω kΩ MΩ * R3 (ratio arm): * Ω kΩ MΩ * R4 (standard arm, variable): * Ω kΩ MΩ * C4 (standard arm, parallel with R4): * µF nF pF --- * Test frequency (f) — optional: * Hz kHz Enter all four bridge components to calculate. Calculate Maxwell Bridge — Explanation ---------------------------- The Maxwell bridge (more precisely the Maxwell-Wien bridge) is an AC bridge circuit used to measure the inductance and series resistance of an unknown inductor. It places the unknown inductor \( L\_x \) with its winding resistance \( R\_x \) in one arm, balances it against a known capacitor in an adjacent arm, and reads off \( L\_x \) and \( R\_x \) directly from the values of the calibrated components at null. The bridge has four arms. The unknown arm contains \( L\_x \) in series with \( R\_x \). The opposite arm (the standard arm) contains a known resistor \( R\_4 \) in parallel with a known capacitor \( C\_4 \). The two remaining arms are plain resistors \( R\_2 \) and \( R\_3 \). An AC source drives one diagonal and a null detector (galvanometer or oscilloscope) monitors the other. Balance Conditions ------------------ At balance, no current flows through the detector. Setting the complex impedance products of opposite arm pairs equal and separating real and imaginary parts gives two independent equations: \[ L\_x = R\_2 \cdot R\_3 \cdot C\_4 \] \[ R\_x = \frac{R\_2 \cdot R\_3}{R\_4} \] These two conditions are independent of frequency, which is one of the key practical advantages of the Maxwell bridge: the source frequency does not need to be known precisely. The ratio arms \( R\_2 \) and \( R\_3 \) always appear as a product, so only their product \( R\_2 R\_3 \) matters — in practice one is often kept fixed while the other is adjusted in decade steps to set the range. The quality factor of the measured inductor at the test frequency \( f \) is: \[ Q = \frac{\omega L\_x}{R\_x} = \omega C\_4 R\_4 \] where \( \omega = 2\pi f \). \( Q \) can be read directly from the standard arm components once the bridge is balanced. Valid Q Range ------------- The Maxwell bridge works well for medium-Q inductors, typically in the range \( 1 \leq Q \leq 10 \). Outside this range, balance becomes difficult to achieve because the real and imaginary balance conditions become nearly dependent, making the null hard to locate. For high-Q inductors (\( Q > 10 \)) the Hay bridge is the better choice: it uses a capacitor in series rather than parallel with \( R\_4 \), which suits high-Q measurements naturally. The calculator outputs a warning when the computed Q falls outside the useful range. Purpose of the Calculator ------------------------- In a physical measurement, the bridge is brought to null by adjusting the variable components (typically \( R\_4 \) for the resistive balance and \( R\_3 \) or \( C\_4 \) for the reactive balance). Once the detector reads zero, the component values at that point are entered here to recover \( L\_x \) and \( R\_x \). No frequency measurement is needed for \( L\_x \) and \( R\_x \); frequency is only required if Q is also wanted. The formulas can also be rearranged for component selection when designing a bridge for a target inductance range. Fixing \( C\_4 \) and choosing \( R\_2 R\_3 = L\_x / C\_4 \) sets the inductance scale, and choosing \( R\_4 = R\_2 R\_3 / R\_x \) sets the resistance scale. Related Tools ------------- * [Schering Bridge Calculator](https://blog.hirnschall.net/tools/schering-bridge/) — the capacitive equivalent: measures unknown capacitance and dissipation factor. * [Wheatstone Bridge Calculator](https://blog.hirnschall.net/tools/wheatstone-bridge/) — DC bridge for measuring unknown resistance. * [Inductor Impedance Calculator](https://blog.hirnschall.net/tools/inductor-impedance/) — compute \( X\_L \) and \( |Z| \) from the measured \( L\_x \) and \( R\_x \). * [Inductor Energy Calculator](https://blog.hirnschall.net/tools/inductor-stored-energy/) — compute the energy stored in the measured inductor at a given current. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [CNC Mill Concept — Mechanical Structure & Passive Damping](https://blog.hirnschall.net/cnc-mill-concept/mechanical-structure-and-passive-damping/) meta description: Welded steel box-section frame, gantry geometry, force loop from tool to frame, and four passive damping strategies on the spindle tower and X-carriage beams. date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- # [CNC Mill Concept: Mechanical Structure & Passive Damping](https://blog.hirnschall.net/cnc-mill-concept/mechanical-structure-and-passive-damping/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Welded steel box-section frame, gantry geometry, force loop from tool to frame, and four passive damping strategies on the spindle tower and X-carriage beams. meta title: CNC Mill Concept — Mechanical Structure & Passive Damping date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- Introduction ------------ This post is part of the [CNC Mill Concept hub](https://blog.hirnschall.net/cnc-mill-concept/). It covers the primary structural decisions for the machine frame and gantry, the explicit force loop that defines which elements are load-bearing, and the four passive damping strategies applied at different locations throughout the machine. Frame and Primary Structure --------------------------- * Welded steel box-section construction * Two rectangular frames: + lower frame + upper frame * Frames connected by four vertical steel legs * Box sections sized for stiffness via geometry, not mass * Welding limited to areas where deformation is acceptable ### Frame Bracing * Steel tension members used for frame bracing * Implementation: + threaded steel rods + or steel tubes with internal threads * Bracing applied on both diagonals * Members preloaded to pull frame into square * Bracing is: + non-moving + serviceable + not part of damping strategy X Gantry Structure ------------------ * Two parallel horizontal box-section beams * Beams span the upper frame * Beams support: + spindle tower + X-axis motion * Beams connected to linear bearing blocks on upper frame rails * Beam spacing sized to accommodate: + spindle plate + Z-axis rails + spindle housing ### Spindle Tower * Constructed from steel plates (approx. 10 mm thickness) * Plates bolted together (not welded) * Alignment procedure: + tower aligned perpendicular to table + fasteners torqued + holes drilled and reamed + dowel pins inserted to fix alignment * Tower rigidly connects spindle plate to X gantry beams Force Loop Definition --------------------- * Explicit force path during cutting: + tool + spindle + spindle housing + spindle mounting plate + spindle tower + X gantry beams + frame * Structural elements outside this loop are not considered load-bearing Table and Y-Axis Support ------------------------ * Steel table plate (approx. 10–15 mm thickness) * Four linear bearing blocks mounted under table * Bearing blocks positioned near table corners * Primary loading of bearing blocks in Z direction * Two Y-axis rail support beams: + bolted to lower frame + parallel to frame sides * Rail support beams are removable and alignable ### Optional Table Stiffening * Shallow steel ribs or profiles may be added under table plate * Attachment method: + adhesive bonding + bolting * No welding to table plate Passive Mechanical Damping and Stiffness Enhancement ---------------------------------------------------- ### Carbon Fibre Constrained Layer Damping (Structural) * Carbon fiber plates bonded to steel plates * Applied only to load-bearing steel plates with high modal strain * Explicit locations: + spindle tower plates (both sides) + optional X-carriage side plates * Purpose: + increase structural damping + suppress bending modes * Not intended to significantly increase stiffness * Carbon fiber plates not used as mounting surfaces ### 3M Damping Tape (Non-Structural) * Viscoelastic damping tape applied to thin sheet metal * Explicit locations: + enclosure inner sheet-metal panels + thin covers and noise-radiating panels * Purpose: + suppress panel ringing + reduce airborne noise * Explicitly not applied to: + precision interfaces + rails + force-loop components ### Epoxy Granite * Epoxy-based mineral composite used as internal damping filler * Composition: + epoxy resin + low-cost mineral aggregates * Filling strategy: + perimeter fill preferred + hollow core retained to reduce mass * Explicit locations: + X-carriage box-section beams: yes + Y-axis rail support beams: optional, evaluative * Purpose: + structural damping + acoustic attenuation * Stiffness contribution considered negligible ### Elastomeric Isolation (PU) * Thin polyurethane layer introduced at selected interfaces * Explicit location: + between spindle housing and spindle mounting plate * Purpose: + isolate high-frequency vibration + reduce noise transmission * PU layer: + preloaded by bolting + not part of stiffness or compliance tuning * Not used at: + linear guide interfaces + force-loop structural joints --- ## [CNC Mill Concept — Enclosure & Acoustics](https://blog.hirnschall.net/cnc-mill-concept/enclosure-and-acoustics/) meta description: Apartment-compatible acoustic enclosure for the CNC mill: MDF outer panels, automotive foam, and a damped sheet metal liner forming a mass-spring-mass sandwich. date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- # [CNC Mill Concept: Enclosure & Acoustics](https://blog.hirnschall.net/cnc-mill-concept/enclosure-and-acoustics/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Apartment-compatible acoustic enclosure for the CNC mill: MDF outer panels, automotive foam, and a damped sheet metal liner forming a mass-spring-mass sandwich. meta title: CNC Mill Concept — Enclosure & Acoustics date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- Introduction ------------ This post is part of the [CNC Mill Concept hub](https://blog.hirnschall.net/cnc-mill-concept/). It covers the acoustic enclosure design: construction layup, inner liner treatment, the mass-spring-mass acoustic strategy, mechanical integration with the machine frame, and airflow constraints. Purpose ------- * Reduce airborne noise emissions * Contain flood coolant * Provide operator protection * Do not participate in the mechanical force loop Enclosure Construction ---------------------- * Fully enclosed machine housing * Construction from outside to inside: + MDF outer panels (mass) + automotive interior acoustic foam + thin inner sheet-metal liner ### Inner Liner * Sheet metal sealed against coolant ingress * Acts as washable and durable inner surface * Treated with viscoelastic damping tape (e.g. 3M) * Purpose: + suppress panel ringing + reduce tonal noise radiation Acoustic Strategy ----------------- * Mass-spring-mass principle: + MDF (mass) + foam (spring / absorber) + sheet metal (inner mass) * Avoidance of large undamped flat panels * No reliance on active noise cancellation Mechanical Integration ---------------------- * Enclosure mechanically decoupled from machine frame where possible * Enclosure does not contribute to stiffness or damping of the force loop * Enclosure panels removable for service Openings and Airflow -------------------- * Openings minimized * Any required airflow paths: + acoustically baffled + not line-of-sight for noise propagation Design Constraints ------------------ * Enclosure designed for apartment-compatible operation * Noise reduction prioritized over cosmetic appearance --- ## [Voltage Divider Calculator — Solve for V_out, V_in, R1, or R2](https://blog.hirnschall.net/tools/voltage-divider/) meta description: Calculate voltage divider output. Solve for V_out, V_in, R1, or R2 given any three. Divider ratio, current, power, and optional load effect included. date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- # [Voltage Divider Calculator](https://blog.hirnschall.net/tools/voltage-divider/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate voltage divider output. Solve for V_out, V_in, R1, or R2 given any three. Divider ratio, current, power, and optional load effect included. meta title: Voltage Divider Calculator — Solve for V_out, V_in, R1, or R2 date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- Calculator ---------- Provide any three of \( V\_{\text{in}} \), \( R\_1 \), \( R\_2 \), \( V\_{\text{out}} \) — the fourth is solved. The divider ratio, current, and power are always shown. \( R\_L \) is optional: if provided, the loaded output voltage is also calculated. * Input voltage (Vin): * V mV kV * Top resistor (R1): * Ω kΩ MΩ * Bottom resistor (R2): * Ω kΩ MΩ * Output voltage (Vout): * V mV kV --- * Load resistance (RL) — optional: * Ω kΩ MΩ Provide any three of V\_in, R1, R2, V\_out to calculate. Calculate Voltage Divider — Explanation ----------------------------- A voltage divider consists of two resistors connected in series between an input voltage and ground. The output is taken from the junction between them. Because the same current flows through both resistors, the voltage splits in proportion to the resistance values: \[ V\_{\text{out}} = V\_{\text{in}} \cdot \frac{R\_2}{R\_1 + R\_2} \] Where \( R\_1 \) is the top resistor (between \( V\_{\text{in}} \) and the output node) and \( R\_2 \) is the bottom resistor (between the output node and ground). The ratio \( R\_2 / (R\_1 + R\_2) \) is always between 0 and 1, so \( V\_{\text{out}} \) is always less than \( V\_{\text{in}} \). A voltage divider can only scale down, not up. The current through the divider and total power dissipated are: \[ I = \frac{V\_{\text{in}}}{R\_1 + R\_2}, \qquad P = V\_{\text{in}} \cdot I = \frac{V\_{\text{in}}^2}{R\_1 + R\_2} \] Higher total resistance reduces quiescent current and standby power, which matters in battery-powered designs. The tradeoff is that high-resistance dividers are more sensitive to loading, as described below. Loading Effect -------------- The formula above assumes no current is drawn from the output. In practice, any load connected to \( V\_{\text{out}} \) forms a parallel combination with \( R\_2 \), reducing the effective bottom resistance and pulling the output voltage down: \[ R\_{2,\text{eff}} = \frac{R\_2 \cdot R\_L}{R\_2 + R\_L}, \qquad V\_{\text{out,loaded}} = V\_{\text{in}} \cdot \frac{R\_{2,\text{eff}}}{R\_1 + R\_{2,\text{eff}}} \] As a rule of thumb, the loading error stays below about 10% when \( R\_L \geq 10 \cdot R\_2 \). For a stiff divider that changes little under load, keep the divider resistors well below the expected load impedance. The optional \( R\_L \) field in the calculator shows the exact loaded output and the percentage drop for any combination. Note that the Wheatstone bridge is essentially two voltage dividers sharing the same supply, with the measurement taken as the difference between their outputs. See the [Wheatstone bridge calculator](https://blog.hirnschall.net/tools/wheatstone-bridge/) for that case. Purpose of the Calculator ------------------------- Any three of \( V\_{\text{in}} \), \( R\_1 \), \( R\_2 \), \( V\_{\text{out}} \) determine the fourth. The rearranged formulas are: * To solve for \( V\_{\text{out}} \) (checking what a known divider produces): \[ V\_{\text{out}} = V\_{\text{in}} \cdot \frac{R\_2}{R\_1 + R\_2} \] * To solve for \( V\_{\text{in}} \) (finding what input produces a target output): \[ V\_{\text{in}} = V\_{\text{out}} \cdot \frac{R\_1 + R\_2}{R\_2} \] * To solve for \( R\_1 \) (selecting the top resistor given a known \( R\_2 \) and target ratio): \[ R\_1 = R\_2 \cdot \frac{V\_{\text{in}} - V\_{\text{out}}}{V\_{\text{out}}} \] * To solve for \( R\_2 \) (selecting the bottom resistor given a known \( R\_1 \) and target ratio): \[ R\_2 = R\_1 \cdot \frac{V\_{\text{out}}}{V\_{\text{in}} - V\_{\text{out}}} \] In all cases the divider ratio, quiescent current, and dissipated power are shown as additional outputs. Resistor Selection ------------------ Choosing absolute resistor values involves a tradeoff between loading stiffness and power consumption. Lower resistance values make the divider output less sensitive to load current variations but increase the standing current drawn from the supply. Higher values reduce quiescent power but make the output more susceptible to loading and to noise picked up on the output node. In most microcontroller ADC reference dividers, resistor values in the 10 kΩ to 100 kΩ range strike a reasonable balance. For voltage monitoring with high-impedance op-amp inputs, values up to 1 MΩ are workable. For power-supply feedback networks where a stable reference voltage is critical, lower values (1 kΩ to 10 kΩ) are more common to swamp out any leakage currents. Related Tools ------------- * [Wheatstone Bridge Calculator](https://blog.hirnschall.net/tools/wheatstone-bridge/) — two voltage dividers sharing a supply; measures unknown resistance from the differential output. * [RC Time Constant Calculator](https://blog.hirnschall.net/tools/rc-time-constant/) — adding a capacitor across R2 turns the divider into an RC low-pass filter. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [ESP32 Complete Guide — WiFi for Arduino Projects](https://blog.hirnschall.net/esp32/) meta description: Everything you need to add WiFi to Arduino: module comparison, wiring, code examples, power saving, web server, preferences, and ethernet — one reference hub. date published: 11.04.2026 (DD.MM.YYYY format) date last modified: 14.05.2026 (DD.MM.YYYY format) --- # [ESP32 - Complete Guide:
Add WiFi to your Arduino Projects](https://blog.hirnschall.net/esp32/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Everything you need to add WiFi to Arduino: module comparison, wiring, code examples, power saving, web server, preferences, and ethernet — one reference hub. meta title: ESP32 Complete Guide — WiFi for Arduino Projects date published: 11.04.2026 (DD.MM.YYYY format) date last modified: 14.05.2026 (DD.MM.YYYY format) --- Introduction ------------ The ESP32 is Espressif's successor to the ESP8266. It is cheap [(around $5 on Amazon)1](https://amzn.to/41V075k), has WiFi and Bluetooth, and can be programmed using the Arduino IDE. Compared to the ESP8266 it offers faster processing, more GPIO pins, more memory, and built-in support for CAN, I2S, and other peripherals, making it a great choice for more demanding IoT projects. While the ESP32 can be used with the original AT firmware as a WiFi module for other microcontrollers, we will concentrate on writing our own firmware in this post. This lets us use the ESP32 as a standalone microcontroller, replacing an Arduino entirely. ESP8266 vs ESP32 - A Comparison ------------------------------- If you have not yet decided if the ESP32 or the cheaper ESP8266 is right for your project, [this comparison will help you make an informed decision](https://blog.hirnschall.net/esp8266-vs-esp32/). Table of Contents ----------------- To keep this reference readable, we have split it into multiple separate posts. Use the links below to jump to the section you are interested in: ### 1[Storing Data between Reboots with Preferences](https://blog.hirnschall.net/esp32-preferences/) * [Namespaces](https://blog.hirnschall.net/esp32-preferences/#namespaces) * [Supported Data Types](https://blog.hirnschall.net/esp32-preferences/#types) * [Basic Usage](https://blog.hirnschall.net/esp32-preferences/#basic-usage) * [Default Values When Reading Data](https://blog.hirnschall.net/esp32-preferences/#first-boot) * [Deleting Data](https://blog.hirnschall.net/esp32-preferences/#delete) * [Storing Strings and Bytes](https://blog.hirnschall.net/esp32-preferences/#strings-and-bytes) * [Full Example](https://blog.hirnschall.net/esp32-preferences/#full-example) --- ### 2[A basic Web Server](https://blog.hirnschall.net/esp32-webserver/) * [WiFi Setup](https://blog.hirnschall.net/esp32-webserver/#wifi) * [Registering Routes](https://blog.hirnschall.net/esp32-webserver/#routes) * [Handler Functions](https://blog.hirnschall.net/esp32-webserver/#handlers) * [Handling 404 Errors](https://blog.hirnschall.net/esp32-webserver/#not-found) * [Handling Requests](https://blog.hirnschall.net/esp32-webserver/#handle-client) * [Sensor Data as JSON](https://blog.hirnschall.net/esp32-webserver/#json) * [GET Arguments](https://blog.hirnschall.net/esp32-webserver/#get-arguments) * [mDNS - Access by Hostname](https://blog.hirnschall.net/esp32-webserver/#mdns) * [Full Example](https://blog.hirnschall.net/esp32-webserver/#full-example) --- ### 3[WiFi Provisioning - Captive Portal & NVS Credentials](https://blog.hirnschall.net/esp32-iot-wifi-provisioning/) * [Overall Flow](https://blog.hirnschall.net/esp32-iot-wifi-provisioning/#flow) * [Storing and Loading Credentials](https://blog.hirnschall.net/esp32-iot-wifi-provisioning/#credentials) * [Provisioning Mode](https://blog.hirnschall.net/esp32-iot-wifi-provisioning/#provisioning) * [Usage](https://blog.hirnschall.net/esp32-iot-wifi-provisioning/#usage) * [Full Example](https://blog.hirnschall.net/esp32-iot-wifi-provisioning/#full-example) --- ### 4[Over-The-Air Updates with ArduinoOTA (Flashing over WiFi)](https://blog.hirnschall.net/esp32-ota-update/) * [Partition Table](https://blog.hirnschall.net/esp32-ota-update/#partition-table) * [ArduinoOTA](https://blog.hirnschall.net/esp32-ota-update/#arduinoota) * [HTTP OTA](https://blog.hirnschall.net/esp32-ota-update/#http-ota) * [Full Example (ArduinoOTA)](https://blog.hirnschall.net/esp32-ota-update/#full-example-arduinoota) --- ### 5[HTTP GET & POST Requests - HTTPClient.h and InfluxDB Example](https://blog.hirnschall.net/esp32-http-request-influxdb/) * [HTTP GET Requests](https://blog.hirnschall.net/esp32-http-request-influxdb/#http-get) * [HTTP POST Requests](https://blog.hirnschall.net/esp32-http-request-influxdb/#http-post) * [Practical Example: Logging Sensor Data to InfluxDB](https://blog.hirnschall.net/esp32-http-request-influxdb/#influxdb-example) --- ### 6[Ethernet with W5500 and ETH.h (Arduino core)](https://blog.hirnschall.net/esp32-w5500-ethernet/) * [Two TCP Stacks Problem](https://blog.hirnschall.net/esp32-w5500-ethernet/#two-stacks) * [Requirements (Arduino IDE Setup)](https://blog.hirnschall.net/esp32-w5500-ethernet/#requirement) * [Hardware Setup](https://blog.hirnschall.net/esp32-w5500-ethernet/#hardware) * [SPI Setup](https://blog.hirnschall.net/esp32-w5500-ethernet/#spi-setup) * [ETH.begin()](https://blog.hirnschall.net/esp32-w5500-ethernet/#eth-begin) * [Event Handling](https://blog.hirnschall.net/esp32-w5500-ethernet/#event-handling) * [DHCP](https://blog.hirnschall.net/esp32-w5500-ethernet/#dhcp) * [Web Server over Ethernet](https://blog.hirnschall.net/esp32-w5500-ethernet/#webserver) * [mDNS - Access by Hostname](https://blog.hirnschall.net/esp32-w5500-ethernet/#mdns) * [Full Example](https://blog.hirnschall.net/esp32-w5500-ethernet/#full-example) --- ### 7[ESP8266 vs ESP32 - A Comparison](https://blog.hirnschall.net/esp8266-vs-esp32/) Where & What to Buy ------------------- Below are links1 to the ESP modules and development boards I personally tend to use. If you are unsure about which one to choose, [check out this comparison post!](https://blog.hirnschall.net/esp8266-vs-esp32/) | ESP-12F | NodeMCU ESP8266 | ESP32-WROOM-32 | ESP32-S3 | | --- | --- | --- | --- | | [amazon.com](https://amzn.to/41YJ965) [amazon.de](https://amzn.to/3Olf3GC) | [amazon.com](https://amzn.to/4smbFcB) [amazon.de](https://amzn.to/4eeUhD6) | [amazon.com](https://amzn.to/4tItyDJ) [amazon.de](https://amzn.to/41V075k) | [amazon.com](https://amzn.to/4tEXG2G) [amazon.de](https://amzn.to/4c26zgR) | | #CommissionsEarned | | | | --- ## [Flash ESP8266 Without USB Adapter — Use an Arduino](https://blog.hirnschall.net/esp8266-arduino-ide/) meta description: Program any ESP8266 using a spare Arduino as USB-to-serial bridge. Wiring, Arduino IDE setup, and the GPIO0-to-GND trick for firmware upload mode. date published: 09.01.2023 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [ESP8266: Programming with the Arduino IDE](https://blog.hirnschall.net/esp8266-arduino-ide/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Program any ESP8266 using a spare Arduino as USB-to-serial bridge. Wiring, Arduino IDE setup, and the GPIO0-to-GND trick for firmware upload mode. meta title: Flash ESP8266 Without USB Adapter — Use an Arduino date published: 09.01.2023 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- Introduction ------------ In recent years the ESP8266 has become one of my go-to choices for smart home/IoT devices or projects requiring WiFi. It is cheap [(around $1.5 on Amazon)1](https://amzn.to/3r9XX2D), has WiFi, and can be programmed using the Arduino IDE, making it an easy replacement for Arduinos if we need WiFi connectivity. While the ESP8266 can be used with the original AT firmware as a WiFi module for other microcontrollers like an Arduino, we will concentrate on writing our own replacement firmware in this post. Our firmware lets us replace the Arduino with an ESP module, simplifying programming and assembly and reducing cost. This post shows how to setup the Arduino IDE to work with the ESP8266 module and how we can use a spare Arduino as a USB to serial converter for programming the ESP module. It is part of [a complete ESP8266 reference you can find here.](https://blog.hirnschall.net/esp8266/) Programming ESP8266 with Arduino IDE ------------------------------------ An esp8266 is a cheap and easy way to add WiFi connectivity to your Arduino projects. However, an esp8266 can replace the Arduino entirely for smaller projects, making the project way cheaper, much smaller, and easier to program. As the esp module lacks a serial to USB converter, we'll use an Arduino to program it. ### Arduino IDE setup As mentioned above, we'll use the arduino IDE to program the esp8266. ([Download Arduino IDE here](https://www.arduino.cc/en/main/software)) We first have to add the esp board. To do so, navigate to File>Preferences and add: ``` http://arduino.esp8266.com/stable/package_esp8266com_index.json ``` ![adding esp8266 board module source to arduino ide](https://blog.hirnschall.net/esp8266-arduino-ide/resources/img/arduino-ide-settings.png) Figure 8.1: Adding an additional Boards Manager URL Next, we need to install the esp board using the boards manager (Tools>Board:"">Boards Manager...). Search for esp8266 and press install. ![installing esp8266 board module to arduino ide](https://blog.hirnschall.net/esp8266-arduino-ide/resources/img/arduino-ide-install-board.png) Figure 8.2: Installing the esp8266 board Ok, after selecting "generic esp8266  module" as seen in the figure below we are all set and can continue with wiring everything up! ![selecting esp8266 board module in the arduino ide](https://blog.hirnschall.net/esp8266-arduino-ide/resources/img/arduino-ide-esp-board-1.png) Figure 8.3: Selecting the esp8266 board ### Connect ESP8266 to Arduino We can enter programming mode by pulling the esp's gpio 0 pin low on boot. That is, disconnect the esp from GND, connect the esp's gpio 0 to GND, and reconnect the esp module to GND. Suppose you have connected the Arduino and opened a serial monitor on the connected port beforehand. In that case, you should see a message from the esp indicating that it has entered programming mode. It is also crucial that we connect the Arduino's reset pin to ground. Fig. 8.4 below shows a pushbutton labeled "FLASH" to connect gpio 0 to ground during the boot process. You do not have to use a button to do so. I usually use a spare jumper wire to make the connection and disconnect it from GND if I want to boot the esp normally. ![wiring diagram for programming an esp8266 esp-01, esp-07 and esp-12 module with Arduino ide](https://blog.hirnschall.net/esp8266-arduino-ide/resources/img/wiring.png) Figure 8.4: Schematic for programming an esp8266 using an Arduino uno This sketch uses an esp8266 esp12 board. If you want to program an esp module: * Use the minimal circuit shown in [Fig. 2.3](https://blog.hirnschall.net/esp8266-reference/#minimal-circuit12) and add the Rx, Tx, and gpio0 connections as shown in Fig. 8.4 above. * Connect the Arduino's reset pin to ground (GND). * To program a different module, check the datasheet to determine which pin is gpio 0. ### Programming the ESP8266 We can now hit upload like we would programming an Arduino board. Once finished, disconnect the gpio 0 pin and restart the esp module. More Info --------- This post is part of a complete ESP8266 reference/guide. You can find more information on how to use your ESP8266 module effectively at [blog.hirnschall.net/esp8266/](https://blog.hirnschall.net/esp8266/). Where & What to Buy ------------------- Below are links1 to the ESP modules and development boards I personally tend to use. If you are unsure about which one to choose, [check out this comparison post!](https://blog.hirnschall.net/esp8266-vs-esp32/) | ESP-12F | NodeMCU ESP8266 | ESP32-WROOM-32 | ESP32-S3 | | --- | --- | --- | --- | | [amazon.com](https://amzn.to/41YJ965) [amazon.de](https://amzn.to/3Olf3GC) | [amazon.com](https://amzn.to/4smbFcB) [amazon.de](https://amzn.to/4eeUhD6) | [amazon.com](https://amzn.to/4tItyDJ) [amazon.de](https://amzn.to/41V075k) | [amazon.com](https://amzn.to/4tEXG2G) [amazon.de](https://amzn.to/4c26zgR) | | #CommissionsEarned | | | | --- ## [CNC Mill Concept — Vibration, Sensing & Metrology](https://blog.hirnschall.net/cnc-mill-concept/vibration-sensing-and-metrology/) meta description: Vibration and sensing for the CNC mill: piezo microphones, MEMS accelerometers, strain gauge bridges over CAN-FD, and a flexure-mounted sub-100 nm laser tool probe. date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- # [CNC Mill Concept: Vibration, Sensing & Metrology](https://blog.hirnschall.net/cnc-mill-concept/vibration-sensing-and-metrology/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Vibration and sensing for the CNC mill: piezo microphones, MEMS accelerometers, strain gauge bridges over CAN-FD, and a flexure-mounted sub-100 nm laser tool probe. meta title: CNC Mill Concept — Vibration, Sensing & Metrology date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- Introduction ------------ This post is part of the [CNC Mill Concept hub](https://blog.hirnschall.net/cnc-mill-concept/). It covers the vibration sensing strategy, sensor types and placement, the distributed sensor PCB architecture, and the laser-based tool measurement subsystem. Vibration and Sensing Strategy ------------------------------ ### Purpose * Observe machine dynamic and quasi-static behavior * Detect chatter and dynamic instability * Provide inputs for compliance estimation and process supervision * Favor redundant, inexpensive sensing over sparse instrumentation ### Piezo Surface Microphones (Sensing Only) * High-impedance piezo elements used exclusively for sensing * No electrical shunt damping attached * Used for: + high-bandwidth vibration measurement + chatter detection * Non-directional measurement Explicit placement: * multiple microphones on spindle tower plates * one microphone mounted directly on the spindle housing * additional microphones permitted elsewhere as needed Notes: * microphones are distinct from piezo damping elements * microphones are not bonded at modal antinodes by requirement ### Accelerometers * Triaxial MEMS accelerometers * Directional vibration measurement (X, Y, Z) * Used for: + modal analysis and validation + directional vibration insight + quasi-static tilt estimation Explicit placement: * spindle tower * spindle mounting plate * optional additional locations where useful Dual-use strategy: * high-rate data: + vibration analysis + frequency-domain features * strongly low-pass-filtered data: + tilt estimation relative to gravity + quasi-static deflection observation ### Other Sensors (Contextual) * Temperature sensors: + monitor structural and spindle-related temperatures + provide context for drift and boundary-condition changes * Sensors are not used for real-time compensation in this phase ### Integration * Sensors connected to distributed sensor PCBs * Local preprocessing performed on sensor PCBs: + filtering + FFT or envelope extraction * Event-driven reporting to ECU via CAN-FD * Raw data access available for debugging and validation Sensor Placement and Measurement Coverage ----------------------------------------- ### Sensor PCB Placement Two identical sensor PCBs are used in the machine. * **PCB A** — mounted on the left X-beam near the spindle tower. * **PCB B** — mounted on the right X-beam near the spindle tower. Both boards use identical hardware and firmware. The distributed placement reduces analog wiring length for strain gauges and improves signal integrity. An optional synchronization line between the boards allows deterministic simultaneous sampling. ### X-Beam Instrumentation #### Strain Gauges (DMS) * One full bridge on the left beam (top and bottom surface) * One full bridge on the right beam (top and bottom surface) * Dynamic capable measurement Purpose: * Measure beam bending * Support dynamic load estimation * Enable structural excitation analysis and compliance observation ### Spindle Plate Instrumentation #### Accelerometer #1 * Mounted directly on the spindle plate * Used for high-frequency vibration measurement * Supports low-frequency tilt estimation ### Spindle Housing Instrumentation #### Microphone #1 * Mounted on the spindle housing * Used for bearing and chatter acoustic monitoring #### Temperature Sensor #1 * Mounted on the spindle housing * Used to monitor spindle thermal growth ### Tower Instrumentation #### Microphone #2 * Mounted on the tower structure * Used for structural acoustic monitoring #### Accelerometer #2 * Mounted on the tower body * Measures tower structural vibration #### Temperature Sensor #2 * Installed in a drilled, thermally coupled pocket inside the tower plate * Used for structural temperature monitoring ### Measurement Coverage The sensing system provides coverage of: * Beam static and dynamic bending * Spindle plate vibration and tilt * Bearing and chatter acoustics * Tower structural vibration * Spindle thermal growth * Tower structural temperature * Digital monitoring of piezo shunt circuits Metrology --------- ### Purpose * Establish accurate knowledge of tool geometry * Reduce uncertainty from tool runout and length variation * Enable high repeatability without relying on conservative margins * Decouple accuracy from spindle and tool holder quality ### Laser-Based Tool Measurement * Dedicated laser emitter and receiver * Emitter and receiver housed separately * Optical aperture implemented as a slit: + width: 5–10 µm * Measurement performed with spindle rotating ### Measurement Procedure * Spindle moves tool into laser beam * Initial detection: + threshold-based (not binary light/no-light) * Secondary fine measurement: + sensor housing mounted on precision flexure + flexure actuated independently of machine motion * Flexure motion resolution: + sub-100 nm repeatability * Tool remains rotating during measurement ### Measured Quantities * Tool length * Effective tool diameter * Radial runout envelope * Repeatable reference position for tool geometry ### Thermal Stabilization * Laser emitter and sensor actively heated * Target temperature: fixed elevated setpoint (e.g. 50 °C) * Measurement only enabled once thermal equilibrium is reached * Thermal history not used for compensation * Stability prioritized over absolute temperature accuracy ### Calibration Strategy * All absolute values obtained through calibration * Measurement system optimized for: + repeatability + consistency * Absolute accuracy derived from: + known calibration artifacts + reference tools ### Integration * Measurement system treated as a metrology subsystem * Not part of: + force loop + vibration control * Results communicated to: + Raspberry Pi (tool library management) + Duet (tool offsets) * Compatible with: + manual tool changes + future automated tool handling ### Scope Limitations * No real-time tool deflection measurement * No thermal growth compensation of tool during cutting * No wear prediction or life estimation --- ## [Capacitor Impedance Calculator — Reactance, |Z|, and Phase Angle](https://blog.hirnschall.net/tools/capacitor-impedance/) meta description: Calculate capacitive reactance, impedance magnitude, and phase angle. Solve for X_C, capacitance, or frequency given the other two. Optional series resistance. date published: 01.04.2025 (DD.MM.YYYY format) date last modified: 22.04.2025 (DD.MM.YYYY format) --- # [Capacitor Impedance Calculator](https://blog.hirnschall.net/tools/capacitor-impedance/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate capacitive reactance, impedance magnitude, and phase angle. Solve for X_C, capacitance, or frequency given the other two. Optional series resistance. meta title: Capacitor Impedance Calculator — Reactance, |Z|, and Phase Angle date published: 01.04.2025 (DD.MM.YYYY format) date last modified: 22.04.2025 (DD.MM.YYYY format) --- Calculator ---------- Fill in any two of \( f \), \( C \), \( X\_C \) — the third is solved. \( R \) is optional: if provided, \( |Z| \) and \( \phi \) are computed as additional outputs. \( \phi \) can also be used as an input together with \( R \) to replace one of the three main variables. * Frequency (f): * Hz kHz MHz * Capacitance (C): * F mF µF nF pF * Capacitive reactance (XC): * Ω kΩ MΩ --- * Resistance (R) — optional: * Ω kΩ MΩ * Phase angle (φ) — optional: * deg rad Fill in any two of f, C, X\_C to calculate. Calculate Capacitive Reactance — Explanation ---------------------------------- A capacitor resists changes in voltage. At AC, this opposition is called capacitive reactance \( X\_C \) and depends on both the capacitance and the signal frequency: \[ X\_C = \frac{1}{2\pi f C} \] Where: * \( X\_C \) is the capacitive reactance (in Ohms, \( \Omega \)), * \( f \) is the frequency (in Hertz, \( \text{Hz} \)), * \( C \) is the capacitance (in Farads, \( \text{F} \)). Unlike resistance, reactance is frequency-dependent: \( X\_C \) decreases as frequency increases. At DC (\( f = 0 \)) a capacitor blocks current entirely — \( X\_C \to \infty \). At very high frequencies it approaches a short circuit — \( X\_C \to 0 \). This is the basis for using capacitors as high-pass or low-pass filter elements. In an LC circuit, resonance occurs at the frequency where \( X\_C \) equals the inductive reactance — see the [LC resonance frequency calculator](https://blog.hirnschall.net/tools/lc-resonance-frequency/). Purpose of the Calculator ------------------------- Given any two of \( f \), \( C \), \( X\_C \), the calculator solves for the third. The rearranged formulas are: * To solve for \( X\_C \): \[ X\_C = \frac{1}{2\pi f C} \] * To solve for \( C \): \[ C = \frac{1}{2\pi f X\_C} \] * To solve for \( f \): \[ f = \frac{1}{2\pi X\_C C} \] Full Impedance with Series Resistance ------------------------------------- A real capacitor or RC circuit also has a series resistance \( R \). The total impedance magnitude and phase angle are: \[ |Z| = \sqrt{R^2 + X\_C^2} \] \[ \phi = -\arctan\!\left(\frac{X\_C}{R}\right) \] The phase angle \( \phi \) is always negative for a capacitor, meaning the current leads the voltage. It ranges from \( 0° \) (purely resistive, \( X\_C \ll R \)) to \( -90° \) (purely capacitive, \( X\_C \gg R \)). If \( \phi \) and \( R \) are known instead of \( X\_C \), the reactance can be recovered: \[ X\_C = -R \cdot \tan(\phi) \] Related Tools ------------- * [LC Resonance Frequency Calculator](https://blog.hirnschall.net/tools/lc-resonance-frequency/) — find the frequency where \( X\_C \) equals \( X\_L \). * [RC Time Constant Calculator](https://blog.hirnschall.net/tools/rc-time-constant/) — time-domain complement to the impedance view of the same RC circuit. * [Capacitors in Series](https://blog.hirnschall.net/tools/capacitors-in-series/) / [Capacitors in Parallel](https://blog.hirnschall.net/tools/capacitors-in-parallel/) — if C is a network, compute the equivalent capacitance first. * [Inductor Impedance Calculator](https://blog.hirnschall.net/tools/inductor-impedance/) — the inductive equivalent: \( X\_L = 2\pi f L \), positive phase angle. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [ESP8266 Web Server — 20 Lines Arduino Code Example](https://blog.hirnschall.net/esp8266-webserver/) meta description: ESP8266 web server using ESP8266WebServer.h — connect to WiFi, respond to requests, and read GET parameters. Working Arduino code in 20 lines. date published: 09.01.2023 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [ESP8266: Setting up a simple web server with Arduino IDE](https://blog.hirnschall.net/esp8266-webserver/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: ESP8266 web server using ESP8266WebServer.h — connect to WiFi, respond to requests, and read GET parameters. Working Arduino code in 20 lines. meta title: ESP8266 Web Server — 20 Lines Arduino Code Example date published: 09.01.2023 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- Introduction ------------ In recent years the ESP8266 has become one of my go-to choices for smart home/IoT devices or projects requiring WiFi. It is cheap [(around $1.5 on Amazon)1](https://amzn.to/3r9XX2D), has WiFi, and can be programmed using the Arduino IDE, making it an easy replacement for Arduinos if we need WiFi connectivity. This post is a short example of how to setup a basic web server on your ESP8266 module using the Arduino IDE. It is part of [a complete ESP8266 reference you can find here.](https://blog.hirnschall.net/esp8266/) A basic web server example -------------------------- There are many ways/libraries to set up a basic web server on the ESP. Each with its pros and cons. For this example, we will choose my personal favorite (ESP8266WebServer.h). First, we must configure the WiFi settings and include the necessary libraries. In this case, we will connect to an existing WiFi named "MyWiFi" with the password "123456" and a random mac address we generated, e.g. [here](https://dnschecker.org/mac-address-generator.php). ``` //--------------------------------------------------- #include const char *ssid = "MyWiFi"; //wifi name const char *password = "123456"; //wifi password uint8_t mac[6] {0x5C, 0x26, 0x19, 0x40, 0x86, 0x62}; //mac address //--------------------------------------------------- #include ESP8266WebServer server(80); //listen to port 80 ``` Next, we connect to WiFi, log to the serial console and start the web server inside the setup function. Furthermore, we tell the ESP8266WebServer instance to call the " MyFunction " function if the root directory "/" is accessed. Suppose our ESP has the IP XXX.XXX.XXX.XXX, and we navigate to http://XXX.XXX.XXX.XXX/ on the same WiFI, the function "MyFunction" will be executed. ``` void setup() { Serial.begin(115200); //start the serial output Serial.println(); Serial.println("Starting up"); Serial.print("Connecting to "); Serial.println(ssid); // Print local IP address and start web server Serial.println(""); wifi_set_macaddr(0, const_cast(mac)); WiFi.mode(WIFI_STA);//disable ap advertising WiFi.begin(ssid, password);//connect to wifi while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(500); } Serial.println("WiFi connected."); Serial.println("IP address: "); Serial.println(WiFi.localIP()); Serial.println("------------------------------"); server.on ( "/", MyFunction ); server.begin(); //start the web server Serial.println("web server started"); Serial.println("Startup finished"); Serial.println("****************"); } ``` The loop is quite minimalistic. Although other code can be added, we might want to avoid using "delay()" and use millis() with a global variable instead. ``` void loop() { server.handleClient(); } ``` Lastly, we take a look at the "MyFunction" callback function. ``` void MyFunction() { Serial.println("New request for \"/\""); if(server.hasArg("getTemp") && server.arg("getTemp").toInt() > 0){ server.send ( 200, "application/json", "{\"temperature\": " + String(MyTempVariable) + "}" ); } else{ server.send ( 200, "text/html", String(SomeDefaultVariable) ); } } ``` In this example, the server waits for a connection on "/". We can use the functions "server.hasArg() and server.arg() to check if a GET argument was provided and to get its value. If we access http://XXX.XXX.XXX.XXX/, the server will return a text/html response with the text stored inside "SomeDefaultVariable". If we provide the GET argument "getTemp" with value 100 by accessing http://XXX.XXX.XXX.XXX/?getTemp=100 the server will return a text/json response with the value of "MyTempVariable". Of course, we could also control the gpio pins before or after sending a response from inside "MyFunction". In reality, we would probably read the temperature sensor first before replying with the json, including temperature data. More Info --------- This post is part of a complete ESP8266 reference/guide. You can find more information on how to use your ESP8266 module effectively at [blog.hirnschall.net/esp8266/](https://blog.hirnschall.net/esp8266/). Where & What to Buy ------------------- Below are links1 to the ESP modules and development boards I personally tend to use. If you are unsure about which one to choose, [check out this comparison post!](https://blog.hirnschall.net/esp8266-vs-esp32/) | ESP-12F | NodeMCU ESP8266 | ESP32-WROOM-32 | ESP32-S3 | | --- | --- | --- | --- | | [amazon.com](https://amzn.to/41YJ965) [amazon.de](https://amzn.to/3Olf3GC) | [amazon.com](https://amzn.to/4smbFcB) [amazon.de](https://amzn.to/4eeUhD6) | [amazon.com](https://amzn.to/4tItyDJ) [amazon.de](https://amzn.to/41V075k) | [amazon.com](https://amzn.to/4tEXG2G) [amazon.de](https://amzn.to/4c26zgR) | | #CommissionsEarned | | | | --- ## [Perfectly Looping Procedural Animations Using Higher-Dimensional Noise](https://blog.hirnschall.net/perfect-looping-gif/) meta description: Random noise doesn't loop cleanly. By sampling higher-dimensional Perlin noise on a closed path, we generate perfectly looping procedural animations. date published: 03.03.2021 (DD.MM.YYYY format) date last modified: 04.04.2026 (DD.MM.YYYY format) --- # [Periodic Randomness using Higher-Dimensional Noise](https://blog.hirnschall.net/perfect-looping-gif/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Random noise doesn't loop cleanly. By sampling higher-dimensional Perlin noise on a closed path, we generate perfectly looping procedural animations. meta title: Perfectly Looping Procedural Animations Using Higher-Dimensional Noise date published: 03.03.2021 (DD.MM.YYYY format) date last modified: 04.04.2026 (DD.MM.YYYY format) --- Motivation ---------- Looping animations are everywhere: generative art, shaders, UI animations, game assets, and GIFs. But creating perfectly looping procedural animations is surprisingly difficult. A common approach is to animate some parameter over time: * scroll a texture * change a random seed * animate a value with Perlin noise * move through procedural space This usually works, until the animation restarts. Then you get a visible jump. The beginning and end don't match, because procedural randomness is typically not periodic. The idea to fix this is simple, we need to create periodic randomness. But how do we create randomness that loops perfectly? Perlin Noise Example -------------------- To get a feel for how we can implement periodic noise and how it works we will go through an example using perlin noise in p5js. We will generate a simple 2D terrain and create a perfectly looping gif. The technique shown is applicable to other procedural animation/generation as well (not just perlin noise gifs). If you are not familiar with perlin noise, you can take a look at this [article explaining what perlin-noise is](https://blog.hirnschall.net/perlin-noise/) and how we can use it in procedural generation. ### Naive Approach Lets start with this bad looping gif of a 2d "map" scrolling from right to left: [ 2d perlin noise terrain generation animation ](resources/vid/bad-looping.webm) Figure 1.1: Naive Approach to Looping Animations You can clearly see the cut where the gif starts from the beginning which is something we don't want. You can see side by side which section of perlin noise we are using to create the current animation frame and the corresponding frame in fig. 1.2. [ 2d perlin noise terrain generation animation following a straight line path through noise space. The cut happens because the beginning and end of the path are different. ](resources/vid/how-bad-looping-animations-work.webm) Figure 1.2: How Bad Looping Animations Work ### Closed Paths The reason why the animation in fig. 1.1 doesn't loop smoothly is because it follows a straight line path through the noise space. Thus the beginning and end of the path are different. To alleviate this problem we can move the section of noise we work with in a circle instead of a straight line. By moving on a closed path the noise inherently becomes periodic and we won't have any cuts when the animation restarts. You can see the result below: [ 2d perlin noise terrain generation animation following a circular (closed) path to avoid cuts when the animation restarts ](resources/vid/how-bad-looping-gisf-without-cut-work.webm) Figure 1.3: Looping Animation on a Closed Path Although fig. 1.3 shows that there is no longer an obvious cut when the video restarts, we see another obvious problem. The animation itself has changed. It is no longer scrolling but rather orbiting. We are now scrolling sideways. ### Higher-Dimensional Noise To overcome this problem we will take the idea of moving in a circle to three dimensions or in general, if we are using \(n\)-dimensional perlin noise in the original animation we'll now use \(n+1\) dimensions. So what we'll do is map our flat canvas to the outside of a cylinder in 3d space. By doing so we know there won't be any cuts as we are still going in a circle but the animation will stay the same! You can think of this as if we were to wrap the canvas in fig. 1.1 into a hollow cylinder. We can do this by mapping the each point \((x,y)\) to \((x',y',z')\): $$x'=\sin\left(\frac{x\cdot 2\pi}{U}\right)\cdot R$$ $$y'=y$$ $$z'=\begin{cases} \sqrt{R^2-x'^2} & \text{if $\frac{6\pi}{4} < (\frac{x\cdot 2\pi}{U}$ mod $2\pi) <\frac{2\pi}{4}$} \\ -\sqrt{R^2-x'^2} & \text{otherwise} \end{cases} $$ Where \(U\) is the circumference and \(R\) is the radius of the cylinder. By changing the size of the cylinder we can adjust the length of the final animation. Note that in this example, to stick with the naming of the 2D case, \(y\) represents the height of the cylinder. This might be counter intuitive as we expect \(z\) to represent the height. As (in p5js at least) \(noise(x)=noise(-x)\) we will offset the center of our cylinder from \(0\). This can be done by adding \(R\) to each coordinate. So, we'll use \(noise(R+x',R+y',R+z')\) instead of \(noise(x,y)\). You can see the section of noise we are using for the final animation in fig. 1.4 below. However it is hard to visualize 3d noise correctly.. [ Higher dimensional perlin noise section used to create perfectly looping animations. The section of noise is moving in a circle in 3d space which creates periodic noise without changing the animation itself. ](resources/vid/how-perfect-looping-works.webm) Figure 1.4: Section of 3D Perlin Noise used to Create Periodic Animations (perfectly looping) And the final animation without any jumps or cuts: [ perfectly looping terrain generation with 3d perlin noise ](resources/vid/perfect-looping-with-scrolling.webm) Figure 1.5: Perfectly looping gif Conclusion ---------- By sampling higher-dimensional noise along a closed path we can create perfectly looping procedural animations. This technique can be applied to any procedural animation that relies on randomness and is not inherently periodic. All in all I am quite happy with the results. I think the idea of using an extra dimension is quite neat. --- ## [CNC Mill Concept — Piezo Shunt Damping](https://blog.hirnschall.net/cnc-mill-concept/piezo-shunt-damping/) meta description: Electromechanical damping for the CNC mill spindle tower using PZT elements bonded at modal antinodes, R-L shunt networks, and adaptive resistance tuning. date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- # [CNC Mill Concept: Piezo Shunt Damping](https://blog.hirnschall.net/cnc-mill-concept/piezo-shunt-damping/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Electromechanical damping for the CNC mill spindle tower using PZT elements bonded at modal antinodes, R-L shunt networks, and adaptive resistance tuning. meta title: CNC Mill Concept — Piezo Shunt Damping date published: 25.03.2026 (DD.MM.YYYY format) date last modified: 19.05.2026 (DD.MM.YYYY format) --- Introduction ------------ This post is part of the [CNC Mill Concept hub](https://blog.hirnschall.net/cnc-mill-concept/). It covers the electromechanical damping strategy for the machine: piezo ceramic shunt damping applied to the spindle tower, the shunt network design, instrumentation for power dissipation measurement, and the optional adaptive tuning loop. Purpose and Scope ----------------- * Piezo elements used exclusively for **structural damping** * Function: + convert mechanical strain energy into electrical energy + dissipate energy in passive or semi-passive shunt networks * Piezo damping elements are **not used as sensors** * Sensing using piezo surface microphones is handled separately Piezo Damping Elements ---------------------- * Type: + piezo ceramic disks (PZT) * Mounting: + bonded directly to steel structure * Placement strategy: + bonded at **modal antinodes** of the spindle tower + placement determined by: - modal analysis - experimental hammer testing * Number: + multiple elements distributed over dominant bending modes * Explicit locations: + spindle tower side plates * Not placed on: + spindle housing + spindle mounting plate + non-load-bearing panels Shunt Networks -------------- * Each piezo element connected to an electrical shunt * Shunt types: + resistive (R) + resistive-inductive (R-L), if required * Baseline damping: + passive + independent of software * Failure behavior: + defaults to fixed, known-good resistance Instrumentation of Shunts ------------------------- * Shunt circuits instrumented with: + piezo voltage measurement + piezo current measurement * ECU computes: + instantaneous electrical power + averaged dissipated power * Power data: + logged + visualized + used for tuning and validation Adaptive Shunt Tuning --------------------- * Shunt resistance adjustable via digital potentiometer * Tuning strategy: + slow supervisory loop + time scale: minutes * Algorithm characteristics: + small bounded resistance steps + comparison of dissipated power at neighboring settings + hysteresis and min/max limits * Purpose: + compensate for: - temperature changes - boundary-condition changes - structural aging * Adaptive tuning: + optional + fully disable-able + not required for baseline damping Interaction with Other Damping Strategies ----------------------------------------- * Piezo shunt damping complements: + structural CLD (carbon fiber) + internal damping (epoxy granite) * Piezo damping targets: + specific structural modes * Not intended to: + replace passive damping + provide active vibration cancellation --- ## [Inductors in Parallel Calculator — Equivalent Inductance](https://blog.hirnschall.net/tools/inductors-in-parallel/) meta description: Calculate the equivalent inductance of inductors in parallel. Enter any number of values; result is always less than the smallest individual inductor. date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- # [Inductors in Parallel Calculator](https://blog.hirnschall.net/tools/inductors-in-parallel/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate the equivalent inductance of inductors in parallel. Enter any number of values; result is always less than the smallest individual inductor. meta title: Inductors in Parallel Calculator — Equivalent Inductance date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- Calculator ---------- Enter the inductance of each inductor in parallel. Add more inductors with the button below. The total inductance is calculated from all filled fields. * Inductor 1 (L1): * H mH µH nH * Inductor 2 (L2): * H mH µH nH Enter at least two inductance values. Add Inductor Calculate Inductors in Parallel — Explanation ----------------------------------- When inductors are connected in parallel, the total inductance is found from the sum of reciprocals: \[ \frac{1}{L\_{\text{total}}} = \frac{1}{L\_1} + \frac{1}{L\_2} + \cdots + \frac{1}{L\_n} \] For two inductors this simplifies to the product-over-sum form: \[ L\_{\text{total}} = \frac{L\_1 \cdot L\_2}{L\_1 + L\_2} \] The result is always smaller than the smallest individual inductor in the group. This is the opposite of capacitors in parallel, where values add directly, and it mirrors exactly how resistors in parallel behave. The derivation follows from Kirchhoff's current law. All inductors in parallel share the same voltage \( V \) across their terminals, so the current through each one is: \[ I\_k = \frac{1}{L\_k} \int V \, dt \] The total current is the sum of all branch currents: \[ I\_{\text{total}} = \sum\_k I\_k = \left(\sum\_k \frac{1}{L\_k}\right) \int V \, dt \] Since \( L\_{\text{total}} \) is defined by \( I\_{\text{total}} = \frac{1}{L\_{\text{total}}} \int V \, dt \), we get the reciprocal sum formula directly. Note on mutual coupling: the formula above assumes no magnetic coupling between the inductors (\( M = 0 \)). If the inductors are physically close together their magnetic fields interact, and the effective inductance will differ from the calculated value. In most practical layouts with separate, shielded components the coupling is negligible, but it is worth keeping in mind when placing inductors near each other on a board. When to Use Inductors in Parallel --------------------------------- Parallel combinations of inductors are less common than series combinations, but there are two situations where they appear. The first is handling higher current: placing two identical inductors in parallel halves the effective inductance but also halves the current through each component, which allows the combination to carry twice the current within each inductor's rating. The winding resistance (DCR) also halves, reducing copper losses at high current. The second situation is reaching a non-standard inductance value. If the target inductance is not available as a single component, two parallel inductors can sometimes hit it more closely than a series pair, particularly when the target is well below typical catalogue values. For reaching a higher inductance from available parts, the [inductors in series calculator](https://blog.hirnschall.net/tools/inductors-in-series/) is usually the better starting point. Once the equivalent inductance is known, it can be used directly in the [RL time constant calculator](https://blog.hirnschall.net/tools/rl-time-constant/), the [inductor impedance calculator](https://blog.hirnschall.net/tools/inductor-impedance/), or the [LC resonance frequency calculator](https://blog.hirnschall.net/tools/lc-resonance-frequency/). Related Tools ------------- * [Inductors in Series Calculator](https://blog.hirnschall.net/tools/inductors-in-series/) — values add directly; total L is always greater than any individual inductor. * [Capacitors in Parallel Calculator](https://blog.hirnschall.net/tools/capacitors-in-parallel/) — the capacitive analogue, where parallel connection adds values directly. * [Inductor Impedance Calculator](https://blog.hirnschall.net/tools/inductor-impedance/) — use the equivalent parallel L to find \( X\_L \) at a given frequency. * [RL Time Constant Calculator](https://blog.hirnschall.net/tools/rl-time-constant/) — use the equivalent parallel L in an RL circuit. * [LC Resonance Frequency Calculator](https://blog.hirnschall.net/tools/lc-resonance-frequency/) — use the equivalent parallel L to find the resonant frequency. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [ESP32 Ethernet with W5500 — ETH.h Arduino Code Example](https://blog.hirnschall.net/esp32-w5500-ethernet/) meta description: Add wired Ethernet to an ESP32 with a W5500 module — using ETH.h (not Ethernet.h, which silently breaks on ESP32), DHCP, and mDNS. date published: 11.04.2026 (DD.MM.YYYY format) date last modified: 11.04.2026 (DD.MM.YYYY format) --- # [ESP32: Adding Ethernet with the W5500 module](https://blog.hirnschall.net/esp32-w5500-ethernet/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Add wired Ethernet to an ESP32 with a W5500 module — using ETH.h (not Ethernet.h, which silently breaks on ESP32), DHCP, and mDNS. meta title: ESP32 Ethernet with W5500 — ETH.h Arduino Code Example date published: 11.04.2026 (DD.MM.YYYY format) date last modified: 11.04.2026 (DD.MM.YYYY format) --- Introduction ------------ For projects where a reliable wired network connection matters, industrial sensors, local dashboards, devices that need to stay connected without worrying about WiFi interference, the W5500 is my go to choice for adding Ethernet to an ESP32. It connects over SPI, is well supported, and as of ESP32 Arduino core v3.x requires no additional libraries. Given its onboard TCP/IP stack it is also quite fast. This post covers the complete setup using the USR-ES1 W5500 module ([amazon.com1](https://amzn.to/4mlfMUW), [amazon.de1](https://amzn.to/4mlfGwy)) and an ESP32-S3 DevKitC-1 ([amazon.com1](https://amzn.to/4tEXG2G), [amazon.de1](https://amzn.to/4c26zgR)). The same approach works for any W5500 breakout board. This post is part of [a complete ESP32 reference you can find here.](https://blog.hirnschall.net/esp32/) The Two TCP Stacks Problem -------------------------- The W5500 has a hardwired TCP/IP stack built into the chip itself. The ESP32 also has its own TCP/IP stack (lwIP). When we connect a W5500 to an ESP32 we therefore have two independent TCP stacks in the same system, and which one handles our network traffic matters. The traditional Arduino `Ethernet.h` library targets the W5500's onboard stack. On the ESP32 this means the W5500 and the ESP32's network layer end up isolated from each other. `WebServer.h`, `HTTPClient.h`, mDNS, and every other ESP32 networking library go through lwIP. They have no visibility into the W5500's stack. This does not produce a compiler error. The code compiles, the module initializes, and then things silently fail: the device resets in a loop, DHCP never completes, or requests are simply never received. Unfortunately this is quite hard to debug. A good, working, approach for the ESP32 is to use `ETH.h`, which is part of the ESP32 Arduino core. With `ETH.h`, lwIP handles all TCP/IP processing and the W5500's onboard stack is not used. The result is that every ESP32 networking library works exactly as it does over WiFi, just through the W5500 instead. For the same reason we prefer the W5500 over alternatives like the ENC28J60. The W5500 is well supported by the ESP32 core, widely available, and works reliably with this setup. The ENC28J60 is a different architecture and requires different drivers. For an ESP32 project I am unaware of any good reason to use it over the W5500. Requirements ------------ W5500 support via `ETH.h` requires ESP32 Arduino core v3.x or later. No additional libraries are needed. Everything is included in the core. Earlier versions of the core do not support the `ETH_PHY_W5500` driver or the `ETH.begin()` signature used here. To check or install the correct core version, open the Arduino IDE, go to **Tools → Board → Boards Manager**, search for `esp32` by Espressif, and install or update to v3.x. as shown in fig. 1. ![ESP32 Arduino core v3.x is required for W5500 support](https://blog.hirnschall.net/esp32-w5500-ethernet/resources/img/esp32-core.jpg) Figure 1: ESP32 Arduino core v3.x is required for W5500 support Hardware -------- The USR-ES1 is a compact W5500 breakout module that exposes the SPI interface and the CS, INT, and RST pins on a standard 2.54mm header. It runs on 3.3V and is directly compatible with the ESP32-S3 DevKitC-1 without level shifting. The wiring used in this example is shown below. The SPI pins are chosen to avoid conflicts with other peripherals on the board. The RST pin is not connected as the W5500 handles its own reset on power-up and the driver accepts `-1` to skip it. ``` // W5500 SPI pins #define ETH_SPI_SCK 12 #define ETH_SPI_MISO 13 #define ETH_SPI_MOSI 11 #define ETH_PHY_CS 10 #define ETH_PHY_IRQ 14 #define ETH_PHY_RST -1 // not connected ``` SPI Setup --------- Rather than using the default shared SPI instance, we create a dedicated `SPIClass` for the W5500. This is good practice whenever multiple SPI peripherals are present. It avoids bus contention and makes the pin assignment explicit. If we want to use additional SPI peripherals like sensors or external ADCs we can connect them to SPI3. On the ESP32-S3, SPI pins are remappable via the GPIO matrix so the HSPI/VSPI distinction of the original ESP32 is less rigid. Creating a named instance and passing it to `ETH.begin()` is the approach we'll go with regardless. ``` SPIClass ethSPI(HSPI); // in setup(): ethSPI.begin(ETH_SPI_SCK, ETH_SPI_MISO, ETH_SPI_MOSI, ETH_PHY_CS); ``` ETH.begin() ----------- `ETH.begin()` initializes the W5500 driver and starts the lwIP network interface. The parameters are: ``` ETH.begin(ETH_PHY_W5500, 0, ETH_PHY_CS, ETH_PHY_IRQ, ETH_PHY_RST, ethSPI); ``` Going through each argument: * `ETH_PHY_W5500`: Selects the W5500 driver. * `0`: PHY address, not used by the W5500. Both 0 and 1 work. * `ETH_PHY_CS`: Chip select pin. * `ETH_PHY_IRQ`: Interrupt pin for incoming data. The W5500 uses this to signal incoming data. * `ETH_PHY_RST`: Reset pin, set to -1 if not connected. * `ethSPI`: The SPI instance to use for communication. `ETH.begin()` must be called after `ethSPI.begin()` and after registering the event handler. Event Handling -------------- The ESP32 network stack is event-driven. Rather than polling for connection status, we register a callback with `Network.onEvent()` that is called when the network state changes. We use a global `eth_connected` flag to track whether the link is up and an IP address has been assigned. ``` static bool eth_connected = false; void onEvent(arduino_event_id_t event, arduino_event_info_t info) { switch (event) { case ARDUINO_EVENT_ETH_START: Serial.println("ETH Started"); ETH.setHostname("esp32-eth0"); break; case ARDUINO_EVENT_ETH_CONNECTED: Serial.println("ETH Connected"); break; case ARDUINO_EVENT_ETH_GOT_IP: Serial.print("ETH Got IP: "); Serial.println(ETH.localIP()); eth_connected = true; break; case ARDUINO_EVENT_ETH_DISCONNECTED: Serial.println("ETH Disconnected"); eth_connected = false; break; case ARDUINO_EVENT_ETH_STOP: Serial.println("ETH Stopped"); eth_connected = false; break; default: break; } } // in setup(), before ETH.begin(): Network.onEvent(onEvent); ``` The hostname must be set inside the `ARDUINO_EVENT_ETH_START` handler. This is the only point at which the network interface exists but has not yet negotiated a connection, making it the correct place to configure it. `eth_connected` becomes `true` only after `ARDUINO_EVENT_ETH_GOT_IP` fires. At this point, DHCP has completed and the device has a usable IP address. Any code that uses the network (e.g., web server, HTTP client, mDNS) should be guarded by this flag. To do so, we will use the `eth_connected` variable inside `loop()`. DHCP ---- DHCP is handled automatically by lwIP once `ETH.begin()` is called and the physical link is established. There is nothing to configure. When the cable is connected and a DHCP server is available on the network, `ARDUINO_EVENT_ETH_GOT_IP` will fire and `ETH.localIP()` returns the assigned address. If DHCP never completes, `ETH Got IP` never appears on the serial output. The most common causes for this are: * **Using the wrong library:** If the code compiles but DHCP silently fails, the most likely cause is that some third-party Ethernet library is interfering with the ESP32 core's network stack. Remove any installed libraries that include their own `ETH.h` or W5500 driver and just use the ESP32 core v3.x built-in version on its own. * **Wrong IRQ pin:** The W5500 uses the IRQ pin to signal incoming data including DHCP responses. If this pin is configured incorrectly or the connection is bad, the driver can miss responses. Double-check the wiring and the pin definition. * **No cable or no DHCP server:** Verify this with `ARDUINO_EVENT_ETH_CONNECTED`. If this event never fires, the physical link is not established. If it fires but `GOT_IP` does not, the DHCP server is not responding. Disabling WiFi and Bluetooth ---------------------------- If the project uses only Ethernet and does not need WiFi or Bluetooth, we can disable both to reduce power consumption and free up resources. ``` WiFi.mode(WIFI_OFF); btStop(); ``` These calls go at the start of `setup()`, before `ETH.begin()`. For a project that uses ethernet anyway, this is good practice. Web Server over Ethernet ------------------------ Since `ETH.h` integrates with lwIP, `WebServer.h` works identically over Ethernet as it does over WiFi. We do not have to configure anything Ethernet-specific. The only difference is that `server.handleClient()` should be called only when `eth_connected` is `true` as shown below. ``` void loop() { if (eth_connected) { server.handleClient(); } } ``` For a full explanation of route registration, handler functions, JSON endpoints, and GET arguments see the [ESP32 web server post](https://blog.hirnschall.net/esp32-webserver/). mDNS ---- mDNS also works identically to the WiFi case. We include `ESPmDNS.h` and call `MDNS.begin()` once the network is up. Since we have already set the hostname in the event handler, we use the same name here for consistency. ``` #include // in setup(), after ETH.begin(): if (MDNS.begin("esp32-eth0")) { Serial.println("mDNS responder started"); } ``` The device is now reachable at `http://esp32-eth0.local` from any device on the same network. Full Example ------------ The following is a complete minimal example combining everything above. It initializes the W5500 over HSPI, handles DHCP using events, disables WiFi and Bluetooth, and runs a simple web server once the connection is established. ``` #include #include #include #include #include // SPI instance for W5500 SPIClass ethSPI(HSPI); // W5500 pins #define ETH_SPI_SCK 12 #define ETH_SPI_MISO 13 #define ETH_SPI_MOSI 11 #define ETH_PHY_CS 10 #define ETH_PHY_IRQ 14 #define ETH_PHY_RST -1 // not connected WebServer server(80); static bool eth_connected = false; void onEvent(arduino_event_id_t event, arduino_event_info_t info) { switch (event) { case ARDUINO_EVENT_ETH_START: Serial.println("ETH Started"); ETH.setHostname("esp32-eth0"); break; case ARDUINO_EVENT_ETH_CONNECTED: Serial.println("ETH Connected"); break; case ARDUINO_EVENT_ETH_GOT_IP: Serial.print("ETH Got IP: "); Serial.println(ETH.localIP()); eth_connected = true; break; case ARDUINO_EVENT_ETH_DISCONNECTED: Serial.println("ETH Disconnected"); eth_connected = false; break; case ARDUINO_EVENT_ETH_STOP: Serial.println("ETH Stopped"); eth_connected = false; break; default: break; } } void handleRoot() { server.send(200, "text/plain", "hello from esp32 over ethernet!"); } void handleNotFound() { String message = "File Not Found\n\n"; message += "URI: "; message += server.uri(); message += "\nMethod: "; message += (server.method() == HTTP_GET) ? "GET" : "POST"; message += "\nArguments: "; message += server.args(); message += "\n"; for (uint8_t i = 0; i < server.args(); i++) { message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; } server.send(404, "text/plain", message); } void setup() { WiFi.mode(WIFI_OFF); btStop(); Serial.begin(115200); delay(500); ethSPI.begin(ETH_SPI_SCK, ETH_SPI_MISO, ETH_SPI_MOSI, ETH_PHY_CS); Network.onEvent(onEvent); ETH.begin(ETH_PHY_W5500, 0, ETH_PHY_CS, ETH_PHY_IRQ, ETH_PHY_RST, ethSPI); if (MDNS.begin("esp32-eth0")) { Serial.println("mDNS responder started"); } server.on("/", handleRoot); server.onNotFound(handleNotFound); server.begin(); Serial.println("HTTP server started"); } void loop() { if (eth_connected) { server.handleClient(); } delay(10); } ``` Conclusion ---------- All in all, adding a wired Ethernet connection to an ESP32 project has gotten quite easy as long as we know which library to use. With this sorted, we can simply use a W5500 module and use the esp32 as normal, just that it has a reliable wired ethernet connection. Where & What to Buy ------------------- Below are links1 to the ESP modules and development boards I personally tend to use. If you are unsure about which one to choose, [check out this comparison post!](https://blog.hirnschall.net/esp8266-vs-esp32/) | ESP-12F | NodeMCU ESP8266 | ESP32-WROOM-32 | ESP32-S3 | | --- | --- | --- | --- | | [amazon.com](https://amzn.to/41YJ965) [amazon.de](https://amzn.to/3Olf3GC) | [amazon.com](https://amzn.to/4smbFcB) [amazon.de](https://amzn.to/4eeUhD6) | [amazon.com](https://amzn.to/4tItyDJ) [amazon.de](https://amzn.to/41V075k) | [amazon.com](https://amzn.to/4tEXG2G) [amazon.de](https://amzn.to/4c26zgR) | | #CommissionsEarned | | | | --- ## [Downloads — Notes and University Papers](https://blog.hirnschall.net/downloads/) meta description: Download Notes and University Papers. date published: 24.03.2026 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [](https://blog.hirnschall.net/downloads/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Download Notes and University Papers. meta title: Downloads — Notes and University Papers date published: 24.03.2026 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- University/School ----------------- ### 2022 [* Stabilized P1/P1 elements for incompressible flow. Bachelor thesis. Vienna University of Technology, 2022.](https://blog.hirnschall.net/downloads/files/bachelor-thesis-hirnschall.pdf) This thesis discusses both Stokes’ and Navier-Stokes’ equations to compute the flow of incompressible isothermal Newtonian fluids using different space discretization methods. Special attention is given to the usage of lowest order P1/P1 element pairings to reduce the number of degrees of freedom when working on fine meshes. The suggested elements are easy to implement and directly available in the open source FEM-library NGSolve [19]. A stabilization term is used to explicitly weaken the divergence-free condition and overcome the fact that a combination of lowest order elements for both the pressure and the velocity fe-space do not meet the LBB-condition. To show the advantages and disadvantages of this method, we use both traditional P2/P1 Taylor-Hood elements, as well as H(div)-conforming elements, as a refer- ence. We combine this space discretization with a suitable, stiffly-accurate, IMEX- scheme to approximate turbulent flow. Finally, numerical results are presented. They agree with our expectations. ### 2021 [* Computing Eigenfrequencies using Finite Element Methods. Numerics Seminar Paper. Vienna University of Technology, 2021.](https://blog.hirnschall.net/downloads/files/computing-eigenfrequencies-using-fem.pdf) We compare different iterative methods (including LOBPCG (A. Knyazev et al. 2007)) for computing eigenfrequencies and the corresponding eigenmodes in a finite element space. We analyze both numerical complexity and convergence of each algorithm and provide reference implementations using NGSolve. The most suitable method is then used to analyze a clamped-free beam and a tuning fork with realistic material properties. We compare our results to the Euler-Bernoulli beam-theory and measurements done on a real world model. ### 2019 [* CG-Verfahren für dünnbesetzte Matrizen. German. Numerik Projekt 2. Vienna University of Technology, 2019.](https://blog.hirnschall.net/downloads/files/cg-verfahren-fuer-duennbesetzte-matrizen.pdf) Das Projekt beschäftigt sich mit dem Lösen linearer Gleichungssysteme der Form Ax = b. Dabei werden verschiedene iterative Verfahren vorgestellt und deren Aufwand verglichen. Außerdem wird eine effiziente Methode gezeigt, dünnbesetzte Matrizen zu speichern. Es folgen Plots zur Veranschaulichung der Konvergenzgeschwindigkeit der Verfahren. [* Numerische integration (auf unendlichen Intervallen). German. Numerik Projekt 1. Vienna University of Technology, 2019.](https://blog.hirnschall.net/downloads/files/numerische-integration-auf-unendlichen-intervallen.pdf) Vordergründiges Ziel dieser Arbeit ist es, exponentiell abfallende Funktionen auf dem unbeschränkten Intervall [0,∞) numerisch zu integrieren und verschiedene Vorgehensweisen zu vergleichen. Zunächst werden zwei unterschiedliche Möglichkeiten um auch das Integral über unbeschränkte Intervalle zu approximieren vorgestellt und untersucht wie sich verschiedener Eigenschaften der zu integrierenden Funktion auf das Konvergenzverhalten des näherungsverfahrens auswirken. Anschließend werden die beiden gewählten Methoden miteinander verglichen. Das letzte Kapitel befasst sich mit der Frage, inwiefern sich das Konvergenzverhalten durch die Wahl unterschiedlicher Gewichtsfunktionen ändert. Dabei wird verglichen wie schnell der Fehler der zuvor gewählten Vorgehensweisen für zwei häufig verwendete Gewichtsfunktionen (exp(−x) und exp(−x²) konvergieren. ### 2017 [* Funktionsweise und Schwachstellen von kryptographischen Hashfunktionen. German. VWA. BRG Gröhrmühlgasse, 2017.](https://blog.hirnschall.net/downloads/files/funktionsweise-und-schwachstellen-von-kryptographischen-hashfunktionen.pdf) Vordergründiges Ziel dieser Arbeit ist es, die Funktionsweise von Hashfunktionen zu beleuchten und zu untersuchen, auf welche Schwachstellen sich Angriffe stützen. Obwohl die Verwendung von Hashalgorithmen für den Nutzer meist nicht ersichtlich ist, finden sie dank ihrer nützlichen Eigenschaften in sehr vielen Bereichen der Informatik Anwendung. Zunächst werden die für das Verständnis von Hashfunktionen benötigten Grundlagen kurz angeschnitten und der Unterschied zu kryptographischen Hashfunktionen erläutert. Anschließend wird die Funktionsweise von drei der wichtigsten Hashalgorithmen (MD4, MD5, SHA) untersucht. Die letzten Kapitel befassen sich mit der Frage, inwiefern die hohe Geschwindigkeit moderner Hashalgorithmen eine Schwachstelle darstellt. Dabei werden drei repräsentative Angriffe auf Passwortlisten nachgestellt (Bruteforce, Markow-Kette, gezielter Angriff). Diese unterscheiden sich grundlegend, nützen jedoch alle die enorme Geschwindigkeit der verwendeten Hashfunktion aus. Other ----- ### 2020 [* Designing Circular and Non-Circular Gears for FDM 3D-Printing. Wiener Neustadt, 2020.](https://blog.hirnschall.net/downloads/files/gears.pdf) --- ## [ESP8266 Power Consumption — Deep Sleep & Battery Life](https://blog.hirnschall.net/esp8266-on-battery/) meta description: ESP8266 draws ~80 mA active and as little as 20 µA in deep sleep. Full breakdown by mode, GPIO16 wake wiring, and formulas to estimate real battery runtime. date published: 09.01.2023 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [ESP8266: Power Saving and Running on Battery](https://blog.hirnschall.net/esp8266-on-battery/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: ESP8266 draws ~80 mA active and as little as 20 µA in deep sleep. Full breakdown by mode, GPIO16 wake wiring, and formulas to estimate real battery runtime. meta title: ESP8266 Power Consumption — Deep Sleep & Battery Life date published: 09.01.2023 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- Introduction ------------ In recent years the ESP8266 has become one of my go-to choices for smart home/IoT devices or projects requiring WiFi. It is cheap [(around $1.5 on Amazon)1](https://amzn.to/3r9XX2D), has WiFi, and can be programmed using the Arduino IDE, making it an easy replacement for Arduinos if we need WiFi connectivity. This post conentrates on setting up the ESP8266 module to save power and thus work on battery for a very long time. It is part of [a complete ESP8266 reference you can find here.](https://blog.hirnschall.net/esp8266/) ESP8266 power saving -------------------- As mentioned in [the esp8266 reference](https://blog.hirnschall.net/esp8266-reference/), the esp8266 uses an average of around 80mA under normal operation. Assuming we want to use AA batteries with, let's say 2500mAh, the esp could run about 32 hours. Such a short runtime is insufficient for most applications. Fortunately, we can disable several internal elements to save power. Table 4.1 below shows what current we can expect in the different power modes. | Power Mode | CPU | System Clock | WiFi modem | RTC | Power consumption | | --- | --- | --- | --- | --- | --- | | Active (RF on) | on | on | on | on | | | Modem-sleep | on | on | off | on | 15mA | | Light-sleep | paused | off | off | on | 0.9mA | | Deep-sleep | off | off | off | on | 20µA | | Power-down | off | off | off | off | 5µA | Table 4.1: ESP8266 power modes and their power consumption A notable limitation of the ESP8266's design is that waking up from deep-sleep resets the microcontroller, and all data in RAM is lost. Waking from deep sleep can be done manually by pulling RST low or using a timer. If we want to use a timer to wake the ESP, we must connect GPIO 16 to RST. We can use the circuit described in [1] to delay and extend the pulse emitted by GPIO16, as shown in Fig. 4.1 below. ![esp8266 esp-12 and esp-07 and esp-01 reset pin gpio 16 circuit diagram sketch for deep sleep](https://blog.hirnschall.net/esp8266-on-battery/resources/img/reset-gpio16.png) Figure 4.1: Waking the ESP from deep-sleep using a timer [1] Using Arduino to program the ESP (see section 4), we enter the different sleep modes as follows. ``` #include WiFi.forceSleepBegin(2500000); //modem sleep for 2.5s ESP.deepSleep(5000000); //deep-sleep for 5s delay(100); //added after deepSleep to ensure the ESP goes to sleep properly ESP.deepSleep(0); //deep-sleep until manual wakeup delay(100); //added after deepSleep to ensure the ESP goes to sleep properly ``` Note that the maximum deep sleep duration is 71 minutes, and the actual sleep time can vary by 2%. Entering light sleep is a bit more complicated, as shown in [3]. To optimize the power consumption further, we need to minimize the time spent being active. We will follow the steps described in [4,5,6,7]. The main points to optimize are * keep WiFi off for as long as possible (e.g., while reading sensor data) and * minimize time spent with WiFi enabled (e.g., searching WiFi, getting IP, etc.). ### Static IP We will configure a static IP to disable DHCP negotiation, which takes time. Furthermore, we will disable network persistence to stop the ESP reading previous WiFi settings from flash (this can take over 1s). The code is relatively short: ``` IPAddress ip( 192, 168, 0, 25 ); //choose a suitable IP IPAddress gateway( 192, 168, 0, 1 ); //gateway/router ip IPAddress subnet( 255, 255, 255, 0 ); //subnet for your network WiFi.persistent( false ); //prevent the ESP from reading WiFi settings from memory WiFi.mode( WIFI_STA ); //WiFi mode. STA to connect to existing network WiFi.config( ip, gateway, subnet ); //configure WiFi WiFi.begin( WLAN_SSID, WLAN_PASSWD ); //connect to WiFi with this SSID and password ``` ### Disabeling WiFi while reading sensor data As the WiFi is disabled during deep sleep, we will use the "WAKE\_RF\_DISABLED" argument to keep the WiFi disabled when waking from deep sleep. ``` WiFi.disconnect( true ); //ensures the ESP enters deep sleep correctly delay( 1 ); //ensures the ESP enters deep sleep correctly ESP.deepSleep( SLEEPTIME, WAKE_RF_DISABLED );// use WAKE_RF_DISABLED to keep WiFi off when waking up ``` The next time the ESP wakes from deep sleep, it will restart with the WiFi disabled. We will turn it on manually once we want to transmit data. To turn on the WiFi, we can use the following code ``` WiFi.mode( WIFI_STA ); WiFi.begin( WLAN_SSID, WLAN_PASSWD ); ``` ### Avoiding network scans using RTC memory The third optimization step is a bit more advanced. When connecting to WiFi normally, the ESP scans the network to find the right channel and BSSID. Assuming the channel has not changed while the ESP module was in deep sleep, we do not have to wait for the AP to advertise the network. The only problem is that data in RAM is lost during deep sleep. However, as shown in Table 4.1 above, the RTC stays on during deep sleep. It has 768B of its own memory, of which only 256B are reserved. We can use the remaining RTC memory to store the necessary WiFi connection information such that it survives deep sleep. Once the ESP wakes up, we check whether the information is valid. If it is, we connect without a network scan. If it is not, we ignore it and connect as usual. The code to do this is [available here, [4]](https://www.bakke.online/index.php/2017/06/24/esp8266-wifi-power-reduction-avoiding-network-scan/). ### Voltage regulator The last thing to consider is our choice of a voltage regulator. Once the ESP goes into deep sleep, it only uses about 20µA. Therefore, we want to choose a voltage regulator that uses minimal power itself if we draw tiny currents. Here are some examples of bad choices * the AMS1117 used on many boards as it requires a minimum current draw of 10mA and * the MCP170x as it supports a max current draw of only 250mA. We recommend the following voltage regulators instead: * XC6220B301 or XC6220B331 with a bias current of 8µA or the * HT7830 or HT7833 with only 4µA bias current. More Info --------- This post is part of a complete ESP8266 reference/guide. You can find more information on how to use your ESP8266 module effectively at [blog.hirnschall.net/esp8266/](https://blog.hirnschall.net/esp8266/). Where & What to Buy ------------------- Below are links1 to the ESP modules and development boards I personally tend to use. If you are unsure about which one to choose, [check out this comparison post!](https://blog.hirnschall.net/esp8266-vs-esp32/) | ESP-12F | NodeMCU ESP8266 | ESP32-WROOM-32 | ESP32-S3 | | --- | --- | --- | --- | | [amazon.com](https://amzn.to/41YJ965) [amazon.de](https://amzn.to/3Olf3GC) | [amazon.com](https://amzn.to/4smbFcB) [amazon.de](https://amzn.to/4eeUhD6) | [amazon.com](https://amzn.to/4tItyDJ) [amazon.de](https://amzn.to/41V075k) | [amazon.com](https://amzn.to/4tEXG2G) [amazon.de](https://amzn.to/4c26zgR) | | #CommissionsEarned | | | | --- ## [Inductors in Series Calculator — Equivalent Inductance](https://blog.hirnschall.net/tools/inductors-in-series/) meta description: Calculate the equivalent inductance of inductors in series. Values add directly; total L is always greater than any individual inductor. date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- # [Inductors in Series Calculator](https://blog.hirnschall.net/tools/inductors-in-series/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate the equivalent inductance of inductors in series. Values add directly; total L is always greater than any individual inductor. meta title: Inductors in Series Calculator — Equivalent Inductance date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- Calculator ---------- Enter the inductance of each inductor in series. Add more inductors with the button below. The total inductance is calculated from all filled fields. * Inductor 1 (L1): * H mH µH nH * Inductor 2 (L2): * H mH µH nH Enter at least two inductance values. Add Inductor Calculate Inductors in Series — Explanation --------------------------------- When inductors are connected in series, the total inductance is the sum of all individual values: \[ L\_{\text{total}} = L\_1 + L\_2 + \cdots + L\_n \] The result is always greater than any single inductor in the string. This is the simplest combination rule for any passive component, and it mirrors resistors in series exactly. It is also the opposite of capacitors in series, where the reciprocal formula applies and the total is always less than the smallest value. The derivation is straightforward. All inductors in series carry the same current \( I \), so the voltage across each one is \( V\_k = L\_k \frac{dI}{dt} \). The total voltage is: \[ V\_{\text{total}} = \sum\_k V\_k = \left(\sum\_k L\_k\right) \frac{dI}{dt} \] Since \( L\_{\text{total}} \) is defined by \( V\_{\text{total}} = L\_{\text{total}} \frac{dI}{dt} \), the sum formula follows directly. Note on mutual coupling: the formula above assumes no magnetic coupling between inductors (\( M = 0 \)). When two inductors are wound or placed so their magnetic fields interact, the effective inductance changes. For two coupled inductors in series, the result depends on the winding orientation: \[ L\_{\text{total}} = L\_1 + L\_2 \pm 2M \] where \( M \) is the mutual inductance. Aiding fields (fluxes in the same direction) add \( 2M \), giving a higher total than the simple sum. Opposing fields subtract \( 2M \), giving a lower total. The calculator assumes \( M = 0 \), which is the correct starting point for well-separated, unshielded components. Special Case: Equal Inductors ----------------------------- When all \( n \) inductors have the same value \( L \), the total simplifies to: \[ L\_{\text{total}} = n \cdot L \] Two identical 100 µH inductors in series give 200 µH, three give 300 µH, and so on. This is useful when a target inductance is a whole-number multiple of a standard catalogue value. When to Use Inductors in Series ------------------------------- Series combinations are the more common case in practice. The main reason is reaching a higher inductance than is available from a single component. Wirewound inductors have a limited range of off-the-shelf values, and combining two in series is often simpler than a custom winding. Series inductors are also used to add small parasitic-suppression inductors in line with a main inductor in power supply layouts, where a few extra nanohenries of series inductance can help damp ringing without changing the main filter design significantly. The total DCR of inductors in series adds directly, unlike the parallel case where DCR halves. For high-current applications where copper loss matters, the [inductors in parallel calculator](https://blog.hirnschall.net/tools/inductors-in-parallel/) is usually the better route. Once the equivalent series inductance is known, use it in the [RL time constant calculator](https://blog.hirnschall.net/tools/rl-time-constant/), the [inductor impedance calculator](https://blog.hirnschall.net/tools/inductor-impedance/), or the [LC resonance frequency calculator](https://blog.hirnschall.net/tools/lc-resonance-frequency/). Related Tools ------------- * [Inductors in Parallel Calculator](https://blog.hirnschall.net/tools/inductors-in-parallel/) — total L is always less than any individual inductor; halves DCR at the cost of halved inductance. * [Capacitors in Series Calculator](https://blog.hirnschall.net/tools/capacitors-in-series/) — the capacitive analogue, where series connection uses the reciprocal formula. * [Inductor Impedance Calculator](https://blog.hirnschall.net/tools/inductor-impedance/) — use the equivalent series L to find \( X\_L \) at a given frequency. * [RL Time Constant Calculator](https://blog.hirnschall.net/tools/rl-time-constant/) — use the equivalent series L in an RL circuit. * [LC Resonance Frequency Calculator](https://blog.hirnschall.net/tools/lc-resonance-frequency/) — use the equivalent series L to set the resonant frequency. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [ESP8266 Complete Guide — WiFi for Arduino Projects](https://blog.hirnschall.net/esp8266/) meta description: Complete ESP8266 reference: module variants, wiring, code examples, deep sleep battery life, web server, EEPROM, and PCB layout — all in one place. date published: 07.01.2023 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [ESP8266 - Complete Guide:
Add WiFi to your Arduino Projects](https://blog.hirnschall.net/esp8266/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Complete ESP8266 reference: module variants, wiring, code examples, deep sleep battery life, web server, EEPROM, and PCB layout — all in one place. meta title: ESP8266 Complete Guide — WiFi for Arduino Projects date published: 07.01.2023 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- Introduction ------------ In recent years the ESP8266 has become one of my go-to choices for smart home/IoT devices or projects requiring WiFi. It is cheap [(around $1.5 on Amazon)1](https://amzn.to/3r9XX2D), has WiFi, and can be programmed using the Arduino IDE, making it an easy replacement for Arduinos if we need WiFi connectivity. While the ESP8266 can be used with the original AT firmware as a WiFi module for other microcontrollers like an Arduino, we will concentrate on writing our own replacement firmware in this post. Our firmware lets us replace the Arduino with an ESP module, simplifying programming and assembly and reducing cost. Although the information in this article can be found in the ESP's datasheet, on several other sources online, or by measuring an ESP module, we want to mention the ESP8266 reference provided by Stefan Frings [1]. ESP8266 vs ESP32 - A Comparison ------------------------------- If you have not yet decided if the ESP32 or the cheaper ESP8266 is right for your project, [this comparison will help you make an informed decision](https://blog.hirnschall.net/esp8266-vs-esp32/). Table of contents ----------------- To make this long article more readable, we have split it in to multiple seperate posts. Use the links below to jump to the section you are interested in: ### 1[Technical Specification](https://blog.hirnschall.net/esp8266-reference/) * [ESP-01 and ESP-1](https://blog.hirnschall.net/esp8266-reference/#esp01) + [Different modules](https://blog.hirnschall.net/esp8266-reference/#esp01) + [Minimal Circuit](https://blog.hirnschall.net/esp8266-reference/#minimal-circuit) + [Pinout](https://blog.hirnschall.net/esp8266-reference/#pinout) + [Reset pin differences](https://blog.hirnschall.net/esp8266-reference/#reset-pin) * [ESP-07 and ESP-12](https://blog.hirnschall.net/esp8266-reference/#esp12) + [Different modules](https://blog.hirnschall.net/esp8266-reference/#esp12) + [Minimal Circuit](https://blog.hirnschall.net/esp8266-reference/#minimal-circuit12) + [Pinout](https://blog.hirnschall.net/esp8266-reference/#pinout12) + [Reset pin differences](https://blog.hirnschall.net/esp8266-reference/#reset-pin12) --- ### 2[Limitations of the ESP8266 module](https://blog.hirnschall.net/esp8266-reference/#limitations) --- ### 3[Using the ESP8266 on a battery](https://blog.hirnschall.net/esp8266-on-battery/) * [Power saving techniques](https://blog.hirnschall.net/esp8266-on-battery/#lowpower) + [Different power modes compared](https://blog.hirnschall.net/esp8266-on-battery/#lowpower) + [Static IP](https://blog.hirnschall.net/esp8266-on-battery/#static-ip) + [Disabeling WiFi while reading sensor data](https://blog.hirnschall.net/esp8266-on-battery/#disable-wifi) + [Avoiding network scans using RTC memory](https://blog.hirnschall.net/esp8266-on-battery/#network-scans) * [Choosing a voltage regulator](https://blog.hirnschall.net/esp8266-on-battery/#voltage-regulator) --- ### 4[Connecting 5V devices](https://blog.hirnschall.net/esp8266-5v-devices/) * [Connecting to other 5V devices](https://blog.hirnschall.net/esp8266-5v-devices/#connecting-5v-devices) * [Is the ESP8266 5V tolerable?](https://blog.hirnschall.net/esp8266-5v-devices/#5v-tolerable) --- ### 5[PCB design guidelines](https://blog.hirnschall.net/esp8266-pcb-design/) --- ### 6[Programming ESP8266 with the Arduino IDE](https://blog.hirnschall.net/esp8266-arduino-ide/) * [Arduino IDE setup](https://blog.hirnschall.net/esp8266-arduino-ide/#arduino_ide_setup) * [Connecting ESP8266 to Arduino](https://blog.hirnschall.net/esp8266-arduino-ide/#connect_to_arduino) * [Programming the esp8266](https://blog.hirnschall.net/esp8266-arduino-ide/#flash-program) --- ### 7[A basic web server](https://blog.hirnschall.net/esp8266-webserver/) --- ### 8[EEPROM emulation with the Arduino IDE](https://blog.hirnschall.net/esp8266-eeprom/) * [EEPROM.begin()](https://blog.hirnschall.net/esp8266-eeprom/#begin) * [Calculating Required EEPROM Size](https://blog.hirnschall.net/esp8266-eeprom/#calc_size) * [How to Read from EEPROM](https://blog.hirnschall.net/esp8266-eeprom/#read) + EEPROM.get() + EEPROM.read() * [How to Write to EEPROM](https://blog.hirnschall.net/esp8266-eeprom/#write) + EEPROM.put() + EEPROM.write() * [EEPROM.commit()](https://blog.hirnschall.net/esp8266-eeprom/#commit) * [EEPROM.end()](https://blog.hirnschall.net/esp8266-eeprom/#end) * [Erase EEPROM](https://blog.hirnschall.net/esp8266-eeprom/#erase) --- [### 9   ESP8266 vs ESP32 - A Comparison](https://blog.hirnschall.net/esp8266-vs-esp32/) --- ### 10   Projects using ESP8266 * [Camera Slider with Timelapse capability and WEBUI (< $ 100)](https://blog.hirnschall.net/diy-motorized-slider/) * [Smart Window Blinds for Alexa & Homekit (< $ 10)](https://blog.hirnschall.net/diy-smart-window-blinds/) Where & What to Buy ------------------- Below are links1 to the ESP modules and development boards I personally tend to use. If you are unsure about which one to choose, [check out this comparison post!](https://blog.hirnschall.net/esp8266-vs-esp32/) | ESP-12F | NodeMCU ESP8266 | ESP32-WROOM-32 | ESP32-S3 | | --- | --- | --- | --- | | [amazon.com](https://amzn.to/41YJ965) [amazon.de](https://amzn.to/3Olf3GC) | [amazon.com](https://amzn.to/4smbFcB) [amazon.de](https://amzn.to/4eeUhD6) | [amazon.com](https://amzn.to/4tItyDJ) [amazon.de](https://amzn.to/41V075k) | [amazon.com](https://amzn.to/4tEXG2G) [amazon.de](https://amzn.to/4c26zgR) | | #CommissionsEarned | | | | --- ## [ESP32 WebServer.h Example — Arduino Web Server in 30 Lines](https://blog.hirnschall.net/esp32-webserver/) meta description: ESP32 web server with WebServer.h — serve HTML, expose sensor data as JSON, read GET parameters, and access via mDNS. Full example included. date published: 11.04.2026 (DD.MM.YYYY format) date last modified: 26.04.2026 (DD.MM.YYYY format) --- # [ESP32 Web Server — WebServer.h Guide with JSON & mDNS](https://blog.hirnschall.net/esp32-webserver/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: ESP32 web server with WebServer.h — serve HTML, expose sensor data as JSON, read GET parameters, and access via mDNS. Full example included. meta title: ESP32 WebServer.h Example — Arduino Web Server in 30 Lines date published: 11.04.2026 (DD.MM.YYYY format) date last modified: 26.04.2026 (DD.MM.YYYY format) --- Introduction ------------ The ESP32 is Espressif's successor to the ESP8266. Faster, more capable, and with both WiFi and Bluetooth built in. Like the ESP8266, it can be programmed using the Arduino IDE and is a great choice for IoT projects that need a lightweight web interface. This post covers setting up a web server on the ESP32 using the `WebServer.h` library, which shares almost identical API with the `ESP8266WebServer.h` library covered in [this post](https://blog.hirnschall.net/esp8266-webserver/). If you are already familiar with the ESP8266 version, the transition is straightforward. This post is part of [a complete ESP32 reference you can find here.](https://blog.hirnschall.net/esp32/) The WebServer Library --------------------- The ESP32 Arduino core ships with the `WebServer.h` library. No additional installation is required. The equivalent library for the ESP8266 is `ESP8266WebServer.h`. Both share the same API — the only difference is the include and the class name. This means that porting code between the two platforms is usually a matter of changing a single line. ``` // ESP32 #include WebServer server(80); // ESP8266 #include ESP8266WebServer server(80); ``` The argument passed to the constructor is the port the server listens on. Port 80 is the default HTTP port and means you can access the server in a browser without specifying a port number explicitly. WiFi Setup ---------- Before starting the web server, the ESP32 must connect to a WiFi network. Include `WiFi.h` and call `WiFi.begin()` with your network credentials. A simple blocking connection loop is good for most projects. We wait until `WiFi.status() == WL_CONNECTED` which means the connection is done. ``` #include const char* ssid = "MyWiFi"; const char* password = "MyPassword"; void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("Connected. IP: "); Serial.println(WiFi.localIP()); } ``` Once connected, `WiFi.localIP()` returns the IP address assigned by your router. We will use this address to access the web server from a browser on the same network. Basic Server Setup - server.begin() ----------------------------------- We instantiate the server at the global scope so it is accessible from both `setup()` and `loop()`. We then call `server.begin()` inside `setup()` after the WiFi connection is established to start the server. In the `loop()` function, we call `server.handleClient()` to process incoming requests. ``` #include #include const char* ssid = "MyWiFi"; const char* password = "MyPassword"; WebServer server(80); void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("Connected. IP: "); Serial.println(WiFi.localIP()); server.begin(); Serial.println("HTTP server started"); } void loop() { server.handleClient(); } ``` At this point the server is running but has no routes registered. So, if we try to access it, we will get no response. The server simply does not know what to do yet. The next step is to register handler functions for the paths we want to serve. Registering Routes - server.on() -------------------------------- ### Routes Let's say the ESP32 has the IP address `192.168.0.100`. When we navigate to `http://192.168.0.100/` in the browser we access the root path `/`. If we however access `http://192.168.0.100/status`, we access the path (route) `/status`. ### Handler Registration What we need to do is tell the server which function we want to call for each path/route that is accessed. This is done with the `server.on()` function. The first argument is the path, the second is the handler function that should be called when that path is accessed. We register the routes inside `setup()` before calling `server.begin()`. In the example below we register `handleRoot` to be called whenever `http://192.168.0.100/` is accessed and `handleStatus` to be called whenever `http://192.168.0.100/status` is accessed. Note that both handleRoot and handleStatus are functions that need to be defined before we use them. We discuss the handler itself in the next section. ``` server.on("/", handleRoot); server.on("/status", handleStatus); server.begin(); ``` We can also register a route inline using a lambda function, which can be convenient for short handlers. This is shown below: ``` server.on("/hello", []() { server.send(200, "text/plain", "hello from esp32!"); }); ``` The first argument to `server.on()` is the path, the second is the handler. For named functions, the handler is a function pointer with no arguments and no return value. Handler Functions ----------------- Inside the handler function we use `server.send()` to send a response to the client. The three arguments are the HTTP status code, the content type, and the response body. If everything went well, we send 200 (OK) status code, if the request is not found, we send 404 (Not Found) and so on. ``` void handleRoot() { server.send(200, "text/plain", "hello from esp32!"); } ``` To serve HTML instead of plain text, change the content type to `text/html` and pass an HTML string as the body. For json we can use `application/json`. ``` void handleRoot() { String html = ""; html += "ESP32"; html += "

Hello from ESP32

"; html += ""; server.send(200, "text/html", html); } ``` For longer HTML pages, building the string with concatenation is tedious. A common pattern is to use a raw string literal, which allows multi-line strings without escaping. ``` void handleRoot() { const char* html = R"( ESP32

Hello from ESP32

)"; server.send(200, "text/html", html); } ``` Handling 404 - server.onNotFound() ---------------------------------- By default, unregistered paths return an empty response. It is good practice to register a `onNotFound` handler that returns a proper 404 response. This is also useful during development for debugging — the handler below echoes back the requested URI, method, and any arguments sent with the request. ``` void handleNotFound() { String message = "File Not Found\n\n"; message += "URI: "; message += server.uri(); message += "\nMethod: "; message += (server.method() == HTTP_GET) ? "GET" : "POST"; message += "\nArguments: "; message += server.args(); message += "\n"; for (uint8_t i = 0; i < server.args(); i++) { message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; } server.send(404, "text/plain", message); } ``` We register it with `server.onNotFound()` before calling `server.begin()`. ``` server.onNotFound(handleNotFound); ``` Handling Requests - server.handleClient() ----------------------------------------- The web server is non-blocking. It does not run in a background thread — instead, `server.handleClient()` must be called regularly in `loop()` to process incoming requests. Each call checks for a new client connection, reads the request, calls the appropriate handler, and sends the response. ``` void loop() { server.handleClient(); } ``` If `loop()` is busy with other work like reading sensors, writing to a database, driving outputs, make sure `server.handleClient()` still gets called frequently enough. Long blocking operations in `loop()` will make the web server unresponsive. A common pattern is to move slow work to a timer and keep `loop()` as fast as possible. ``` void loop() { server.handleClient(); static unsigned long lastRead = 0; if (millis() - lastRead >= 1000) { // read sensors, do slow work lastRead = millis(); } } ``` Serving Sensor Data as JSON --------------------------- A common use case is to expose sensor data as a JSON endpoint so that a browser or other client can fetch the data asynchronously. As mentioned above, the content type for JSON is `application/json`. ``` float temperature = 0.0; float humidity = 0.0; void handleData() { String json = "{"; json += "\"temperature\":" + String(temperature, 2) + ","; json += "\"humidity\":" + String(humidity, 2); json += "}"; server.send(200, "application/json", json); } ``` We register the route the same way as we did for the other handlers before. ``` server.on("/data", handleData); ``` We can now access the latest sensor data by accessing `http://192.168.0.100/data` assuming the ESP32 has the IP address `192.168.0.100`. If we have multiple sensors or want to check connectivity before reading, we can guard the response with a validity check and return error codes as appropriate. So, if the sensor data is not ready, we send a 503 (Service Unavailable) status code with the message "sensor not ready". If the data is ready, we send the JSON response with the sensor data as before. ``` void handleData() { if (!sensorReady) { server.send(503, "application/json", "{\"error\":\"sensor not ready\"}"); return; } String json = "{"; json += "\"temperature\":" + String(temperature, 2) + ","; json += "\"humidity\":" + String(humidity, 2); json += "}"; server.send(200, "application/json", json); } ``` GET Arguments ------------- GET arguments are parameters passed in the URL. For example, `http://192.168.0.100/data?sensor=0` passes the argument `sensor` with the value `0`. This is useful if we want to serve different data depending on what the client requests. Inside a handler, we can check if an argument is present using `server.hasArg()` and get its value using `server.arg()`. ``` void handleData() { if (!server.hasArg("sensor")) { server.send(400, "text/plain", "Missing argument: sensor"); return; } int sensorId = server.arg("sensor").toInt(); String json = "{"; json += "\"sensor\":" + String(sensorId) + ","; json += "\"temperature\":" + String(readTemperature(sensorId), 2); json += "}"; server.send(200, "application/json", json); } ``` We can now request data for a specific sensor by accessing `http://192.168.0.100/data?sensor=0` or `http://192.168.0.100/data?sensor=1` and so on. Multiple arguments can be passed at once by separating them with `&`, e.g. `http://192.168.0.100/data?sensor=0&unit=celsius`. Each can be accessed individually using `server.arg()`. ``` int sensorId = server.arg("sensor").toInt(); String unit = server.arg("unit"); ``` mDNS - Access by Hostname ------------------------- A much nicer way to access the ESP32 than by its IP address is to use mDNS. Here we register a local domain for the ESP32 that does not change regardless of the IP address assigned by the router. This way we can access the ESP32 at e.g. `http://esp32.local` instead of using the IP. For this we can include `ESPmDNS.h` and call `MDNS.begin()` with your chosen hostname after the WiFi connection is established. We can then access our server at `http://hostname.local` as long as we are in the same network. ``` #include if (MDNS.begin("esp32")) { Serial.println("mDNS responder started"); } ``` Full Example ------------ The following is a complete minimal example combining everything covered above. It connects to WiFi, registers a root handler that serves an HTML page, a `/data` endpoint that returns sensor readings as JSON, a 404 handler, and starts the server with mDNS. In the code below, we use a bit of javascript to update the displayed sensor data every 2 seconds by fetching the `/data` endpoint. This way we can always display the latest sensor readings without needing to refresh the page. This is prurely optional. ``` #include #include #include const char* ssid = "MyWiFi"; const char* password = "MyPassword"; WebServer server(80); float temperature = 23.5; float humidity = 55.0; void handleRoot() { const char* html = R"( ESP32

ESP32 Web Server

Loading...

)"; server.send(200, "text/html", html); } void handleData() { String json = "{"; json += "\"temperature\":" + String(temperature, 2) + ","; json += "\"humidity\":" + String(humidity, 2); json += "}"; server.send(200, "application/json", json); } void handleNotFound() { String message = "File Not Found\n\n"; message += "URI: "; message += server.uri(); message += "\nMethod: "; message += (server.method() == HTTP_GET) ? "GET" : "POST"; message += "\nArguments: "; message += server.args(); message += "\n"; for (uint8_t i = 0; i < server.args(); i++) { message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; } server.send(404, "text/plain", message); } void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("Connected. IP: "); Serial.println(WiFi.localIP()); if (MDNS.begin("esp32")) { Serial.println("mDNS responder started"); } server.on("/", handleRoot); server.on("/data", handleData); server.onNotFound(handleNotFound); server.begin(); Serial.println("HTTP server started"); } void loop() { server.handleClient(); static unsigned long lastRead = 0; if (millis() - lastRead >= 1000) { // replace with actual sensor reads // temperature = sensor.readTemperature(); // humidity = sensor.readHumidity(); lastRead = millis(); } } ``` Where & What to Buy ------------------- Below are links1 to the ESP modules and development boards I personally tend to use. If you are unsure about which one to choose, [check out this comparison post!](https://blog.hirnschall.net/esp8266-vs-esp32/) | ESP-12F | NodeMCU ESP8266 | ESP32-WROOM-32 | ESP32-S3 | | --- | --- | --- | --- | | [amazon.com](https://amzn.to/41YJ965) [amazon.de](https://amzn.to/3Olf3GC) | [amazon.com](https://amzn.to/4smbFcB) [amazon.de](https://amzn.to/4eeUhD6) | [amazon.com](https://amzn.to/4tItyDJ) [amazon.de](https://amzn.to/41V075k) | [amazon.com](https://amzn.to/4tEXG2G) [amazon.de](https://amzn.to/4c26zgR) | | #CommissionsEarned | | | | --- ## [Capacitor Energy Calculator — Solve for E, C, or V](https://blog.hirnschall.net/tools/capacitor-stored-energy/) meta description: Calculate the energy stored in a capacitor. Solve for energy, capacitance, or voltage given the other two. Stored charge shown as additional output. date published: 01.04.2025 (DD.MM.YYYY format) date last modified: 22.04.2025 (DD.MM.YYYY format) --- # [Capacitor Energy Calculator](https://blog.hirnschall.net/tools/capacitor-stored-energy/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate the energy stored in a capacitor. Solve for energy, capacitance, or voltage given the other two. Stored charge shown as additional output. meta title: Capacitor Energy Calculator — Solve for E, C, or V date published: 01.04.2025 (DD.MM.YYYY format) date last modified: 22.04.2025 (DD.MM.YYYY format) --- Calculator ---------- This calculator can solve for energy \( E \), capacitance \( C \), or voltage \( V \) when you provide the other two. The stored charge \( Q \) is always shown as an additional output. * Capacitance (C): * F mF µF nF pF * Voltage (V): * V mV kV * Energy (E): * J mJ µJ kJ Please fill in exactly two variables. Calculate Energy Stored in a Capacitor — Explanation ------------------------------------------ When a voltage \( V \) is applied across a capacitor with capacitance \( C \), charge accumulates on the plates and energy is stored in the electric field between them. The energy stored is: \[ E = \frac{1}{2} C V^2 \] Where: * \( E \) is the stored energy (in Joules, \( \text{J} \)), * \( C \) is the capacitance (in Farads, \( \text{F} \)), * \( V \) is the voltage across the capacitor (in Volts, \( \text{V} \)). The \( V^2 \) dependence is the key insight: doubling the voltage quadruples the stored energy. This is why high-voltage capacitors are so effective in pulsed energy applications like camera flashes or defibrillators. To model how that energy is delivered over time as the capacitor discharges, use the [capacitor discharge calculator](https://blog.hirnschall.net/tools/capacitor-discharge/). The stored charge \( Q \) on the plates follows directly from the definition of capacitance: \[ Q = C \cdot V \] Substituting this into the energy formula gives two equivalent forms: \[ E = \frac{Q^2}{2C} = \frac{1}{2} Q V \] All three forms are equivalent — which one to use depends on what variables are known. Purpose of the Calculator ------------------------- Given any two of the three variables \( E \), \( C \), and \( V \), the calculator solves for the missing one. The rearranged formulas are: * To solve for \( E \) (energy): \[ E = \frac{1}{2} C V^2 \] * To solve for \( C \) (capacitance): \[ C = \frac{2E}{V^2} \] * To solve for \( V \) (voltage): \[ V = \sqrt{\frac{2E}{C}} \] In all cases, the stored charge \( Q = C \cdot V \) is shown as an additional output. If C needs to be derived from plate geometry first, use the [parallel plate capacitor calculator](https://blog.hirnschall.net/tools/parallel-plate-capacitor/). Related Tools ------------- * [Capacitor Discharge Calculator](https://blog.hirnschall.net/tools/capacitor-discharge/) — compute voltage and current as stored energy is released. * [Parallel Plate Capacitor Calculator](https://blog.hirnschall.net/tools/parallel-plate-capacitor/) — derive C from plate geometry and dielectric constant. * [Capacitors in Series](https://blog.hirnschall.net/tools/capacitors-in-series/) / [Capacitors in Parallel](https://blog.hirnschall.net/tools/capacitors-in-parallel/) — find the equivalent C for a capacitor network. * [Inductor Energy Calculator](https://blog.hirnschall.net/tools/inductor-stored-energy/) — the inductive equivalent: \( E = \frac{1}{2}LI^2 \). More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [C++ Expression Calculator — Variables & Parsing](https://blog.hirnschall.net/programming-an-advanced-cpp-calculator/) meta description: Build a C++ calculator that parses variables, parentheses, unary operators, and mathematical constants — full recursive descent parser with source code. date published: 04.09.2024 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [Programming an advanced C++ calculator](https://blog.hirnschall.net/programming-an-advanced-cpp-calculator/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Build a C++ calculator that parses variables, parentheses, unary operators, and mathematical constants — full recursive descent parser with source code. meta title: C++ Expression Calculator — Variables & Parsing date published: 04.09.2024 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- Motivation ---------- Ever since I started programming, I wanted to write my own calculator program. However, all tutorials and how-tos I could find online were about simple calculators that scan two numbers and either add or multiply them. This is not what I want, and it's probably not what you want either! Let's take a look at how we can build a c++ calculator that can process an input string (e.g. \(5+2\*(3-1)\)) and compute the result of a somewhat complicated calculation, including parentheses. As always, you can download the complete source code for this c++ calculator below. ### Goals As I have already mentioned above, the goal is to take a string or char array as an input and return a float (preferably a double). Furthermore, the code should run on a microcontroller like an ATMega328 (Arduino) or an [ESP8266](https://blog.hirnschall.net/esp8266/) with little to no modifications. How a calculator program works ------------------------------ Although there are many different ways to make a calculator in c++, I think our approach is especially elegant. We'll do the following: 1. parse the input string to find numbers and binary operations (\(\*,/,-,+\)) 2. recursively call our solve function if we find brackets 3. return result as a floating-point number Starting with a string \(eq\) our first goal is to find and store numbers and operators in a way that makes it easy to compute the correct result. To do so, we'll use three arrays * \(numbers\): stores each number we find in \(eq\) * \(multIndex\): stores the numbers-index of each number that is to the left of a \(\*\) operator * \(plusIndex\): stores the numbers-index of each number that is to the left of a \(+\) operator Let's take a look at the example input "\(8\*3+(2-1)\*5+7\)" and the three resulting vectors (=arrays) to better understand how this is going to work: Note: As we'll later see, we have to parse \(eq\) from right to left. ![how a c++ calculator console application works with multiplication](https://blog.hirnschall.net/programming-an-advanced-cpp-calculator/resources/img/calculator.png) As multiplication is done before addition, we'll look at the \(multIndex\) vector first. All we have to do now is to find numbers with their index \(i\) stored in \(multIndex\) and multiply them with the number with index \(i-1\). We'll then store the result of \(numbers[i]\*numbers[i-1]\) in \(numbers[i-1]\) for later use. After this step, our example looks like this: ![how a c++ calculator console application works with addition](https://blog.hirnschall.net/programming-an-advanced-cpp-calculator/resources/img/multiplication.png) We can now add all numbers with index in \(plusIndex\) and \(numbers[0]\) together to get the correct result. If we encounter either a division or a subtraction we can replace them in the following way: * replace \(a/b\) with \(a\*\frac{1}{b}\) * replace \(a-b\) with \(a+(-1)\*b\) By parsing \(eq\) from right to left we make sure we know \(b\) as soon as we reach the operator, enabling us to replace \(b\) with either \(-b\) or \(\frac{1}{b}\) as shown above. Furthermore, we'll include some checks to handle "bad" or incorrect inputs. If we do find a mistake we return \(\text{nan}\) (="Not a Number"). C++ Implementation ------------------ ### Finding Numbers and Operators I have prepared a minimalist vector class for this project and later use on Arduino. You can find it in the downloads section below or use std::vector instead: If you don't want to miss my upcoming Arduino calculator build make sure to leave your email below! ``` Vector numbers; Vector plusIndex; Vector multIndex; ``` To accommodate for subtraction and division, we parse from right to left: ``` for (char i = end-1; i >= start; --i) { ``` As C uses ASCII characters it is easy to check if a char is either a digit or the decimal point. If it is, we'll store it in a temporary string tmp: ``` if((eq[i]>47 && eq[i]<58) || eq[i]==46){ if(tmpc>=MAX_NUMBER_LENGTH) return std::nanf(""); tmp[tmpc++]=eq[i]; } ``` If we reach a character that is not part of a number we check if it is a known operator, starting with \(+\). If it is, we can convert the tmp string into a number: ``` else if(eq[i] == '+'){ if(tmpc>0){ reversed=reverseString(tmp,tmpc); if(!reversed) return std::nanf(""); plusIndex.push(numbers.push(strtof(reversed,nullptr))); free(reversed); tmpc=0; } //handling wrong or weird inputs else if(i==end-1){ return std::nanf(""); } //these two extra cases are necessary because a calculation like a++--+b is valid and equal to a+b else if(plusIndex.size() == 0 || (plusIndex.size() > 0 && numbers.size() != *plusIndex.at(plusIndex.size() - 1))){ plusIndex.push(numbers.size()); } } ``` Because we are parsing from right to left, tmp is reversed. We can correct this with the following function: ``` char* reverseString(const char* string,char length){ auto tmp = (char*)malloc((length+1)*sizeof(char)); if(!tmp) return nullptr; tmp[length]='\0'; for (int i = 0; i < length; ++i) { tmp[i]=string[length-1-i]; } return tmp; //make sure to free the returned pointer } ``` As we have discussed above we'll replace \(a-b\) with \(a+(-1)\*b\). In other words, we multiply the last number we found with \(-1\) and use the same code as with \(+\) above. ``` else if(eq[i] == '-'){ if(tmpc>0){ reversed=reverseString(tmp,tmpc); if(!reversed) return std::nanf(""); plusIndex.push(numbers.push(-strtof(reversed,nullptr))); free(reversed); tmpc=0; } //handling wrong or weird inputs else if(i==end-1){ return std::nanf(""); } //these two extra cases are necessary because a calculation like a++--+b is valid and equal to a+b else if(plusIndex.size() == 0 || (plusIndex.size() > 0 && numbers.size()!= *plusIndex.at(plusIndex.size() - 1))){ *numbers.at(numbers.size() - 1)*=-1; plusIndex.push(numbers.size()); }else{ *numbers.at(numbers.size() - 1)*=-1; } } ``` Multiplication and division work in a similar way: ``` else if(eq[i]=='*'){ if(tmpc>0){ reversed=reverseString(tmp,tmpc); if(!reversed) return std::nanf(""); multIndex.push(numbers.push(strtof(reversed,nullptr))); free(reversed); tmpc=0; }else if(i==end-1 || i==start){ return std::nanf(""); } //this case is for a*-b. because - is pushed into the plusIndex array we need to remove it. else if(plusIndex.size()>0 && *plusIndex.at(plusIndex.size()-1) == numbers.size()){ plusIndex.pop(); multIndex.push(numbers.size()); } else{ multIndex.push(numbers.size()); } }else if(eq[i]=='/'){ if(tmpc>0){ reversed=reverseString(tmp,tmpc); if(!reversed) return std::nanf(""); multIndex.push(numbers.push((float)1/strtof(reversed,nullptr))); free(reversed); tmpc=0; }else if(i==end-1 || i==start){ return std::nanf(""); } //this case is for a/-b. because - is pushed into the plusIndex array we need to remove it. else if(plusIndex.size()>0 && *plusIndex.at(plusIndex.size()-1) == numbers.size()){ plusIndex.pop(); (*numbers.at(numbers.size() - 1)) = 1 / (*numbers.at(numbers.size() - 1)); multIndex.push(numbers.size()); }else{ (*numbers.at(numbers.size() - 1)) = 1 / (*numbers.at(numbers.size() - 1)); multIndex.push(numbers.size()); } } ``` If we divide by zero C returns \(inf\). ### Finding Parentheses If we find a closing bracket ')' we'll look for a matching open bracket '('. Once we find a matching pair we can recursively call \(solve(eq,\text{index of '('}+1,\text{index of ')'})\): ``` else if(eq[i]==')'){ //try to find a matching '(': char numClosingBrackets=0; char foundMatching=0; for(char j=i-1;j>=start;--j){ if(eq[j]==')') ++numClosingBrackets; else if(eq[j]=='(' && numClosingBrackets>0) --numClosingBrackets; else if(eq[j]=='(' && numClosingBrackets==0){ //matching '(' found if(!foundMatching) { numbers.push(solve(eq, j + 1, i,vars)); i = j;//skip the part between () in parsing foundMatching = 1; } } } if(!foundMatching) return std::nanf(""); } } ``` After we are done parsing the whole string we have to convert tmp to a number one last time: ``` if(tmpc>0){ reversed=reverseString(tmp,tmpc); if(!reversed) return std::nanf(""); numbers.push(strtof(reversed,nullptr)); free(reversed); tmpc=0; } ``` ### Computing the result Now all that's left to do is computing the result. We'll start with multiplication and division. As we changed each division into a multiplication, it does not matter if we go from right to left or from left to right. We'll go from right to left and always replace the left number with the result. As we are using pointers we have to dereference them using the \*-operator: ``` if(numbers.size()==0) return std::nanf(""); if(multIndex.size() > 0) { for (char i = multIndex.size()-1;i>=0 ; --i){ //check if '*' is associated with two numbers: if(*multIndex.at(i)>= numbers.size()) return std::nanf(""); (*numbers.at(*multIndex.at(i)-1)) *= (*numbers.at(*multIndex.at(i))); } } ``` As we have always replaced the left number with the result, we can now add all numbers that are to the right of a \(+\)-operator. We also have to include the leftmost number. ``` float result=*numbers.at(0); for (char i=0;i< plusIndex.size(); ++i){ result+=*numbers.at(*plusIndex.at(i)); } return result; } ``` And we are done! Well, now that we have a simple calculator program working, let's take a look at how we can implement some more advanced features. Adding advanced features ------------------------ Although not absolutely necessary, we always want to avoid computationally complex code. So, if possible, we would rather not: * resize an array * have unnecessarily large arrays (store data more than once) * remove elements from the middle of an array (we would much rather ignore existing elements if we can) * have nested loops (this can result from badly structured arrays, if possible, we want linear complexity) * write complicated code with many ifs, else ifs Let's now take a look at two different implementations of a power function to get a better understanding of what we might want to avoid when expanding our basic c++ calculator. ### Binary Operators like power (\(a^b\)) Let the input string \(eq\) contain \(\text{a^b}\). #### 1. possible implementation: As we are parsing from right to left, we know \(b\) once we reach the '^' char. We now set a flag (e.g. \(\text{powFound}=\text{true}\)) and continue parsing. Every time we reach an operator, we check if the flag is set. If it is, we replace \(b\) with the value of \(a^b\) as we now know both \(a\) and \(b\). Pros: * No array manipulations * No nested for loops Cons: * If case for every operator we are using \(\implies\) “the same” code in multiple different places * Not really the same logic as multiplication and addition #### 2. possible implementation: We add a new array \(\text{powIndex}\). If we encounter a '^' char, we push the numbers-array’s current length into both the \(\text{powIndex}\) and the \(\text{multIndex}\) array. Now, before we do the multiplication step we can compute \(a^b\), replace \(a\) with the result, and \(b\) with \(1\). We then continue with multiplication as usual. Pros: * We do not need to change existing code * No array manipulations * No nested for loops * No if else * Logically the same as addition and multiplication * All code in one location * We can use the same logic for other binary operators Cons: * We need a new array * We are doing one unnecessary multiplication per pow As the number of '^' chars in a single calculation is very low, I would definitely prefer the second approach to implement operators in this basic c++ calculator. The code is fairly simple: ``` else if(eq[i]=='^'){ if(tmpc>0){ reversed=reverseString(tmp,tmpc); if(!reversed) return std::nanf(""); multIndex.push(numbers.push(strtof(reversed,nullptr))); powIndex.push(numbers.size()); free(reversed); tmpc=0; }else if(i==end-1 || i==start){ return std::nanf(""); }else{ multIndex.push(numbers.size()); powIndex.push(numbers.size()); } } ``` ``` if(powIndex.size() > 0) { for (char i = powIndex.size()-1;i>=0; --i){ //check if '*' is associated with two numbers: if(*powIndex.at(i)>= numbers.size()) return std::nanf(""); (*numbers.at(*powIndex.at(i)-1)) = pow((*numbers.at(*powIndex.at(i))),(*numbers.at(*powIndex.at(i)-1))); (*numbers.at(*powIndex.at(i))) = 1; } } ``` ### Unary Operations like \(\sin(a)\) Adding unary operators or functions is very easy. As we have mentioned numerous times above, we always know the number to the right of an operator by the time we reach the operator. So, all we need to do is check if we find a new operator and replace the last number we found. You can see an example with sine and arcsine below: ``` else{ //unary operators: //trig functions work with rad not deg! if(i>2 && eq[i]=='n' && eq[i-1]=='i' && eq[i-2]=='s' && eq[i-3]=='a'){ if(numbers.size()) *numbers.at(numbers.size()-1) = asin(*numbers.at(numbers.size()-1)); i-=3; if(plusIndex.size()>0 && *plusIndex.at(plusIndex.size()-1) == numbers.size()) { plusIndex.pop(); } }else if(i>1 && eq[i]=='n' && eq[i-1]=='i' && eq[i-2]=='s'){ if(numbers.size()) *numbers.at(numbers.size()-1) = sin(*numbers.at(numbers.size()-1)); i-=2; if(plusIndex.size()>0 && *plusIndex.at(plusIndex.size()-1) == numbers.size()) { plusIndex.pop(); } }else return std::nanf(""); } ``` ### Constants like \(\pi\) Implementing constants is almost the same as implementing unary functions. Instead of changing the last number in \(\text{numbers}\) we push the value of the constant we found. Example implementation of \(\pi\) as a constant: ``` //constants else if(i>0 && eq[i]=='i' && eq[i-1]=='p'){ if(numbers.size()) numbers.push(M_PI); i-=1; } ``` Depending on the number of constants we want to add, we might want to store them in a matrix and use a loop to check which one we found. ### Variables like \(\text{ans}\) It can be quite useful to use the last result or to save a result for later use. We'll first change the prototype of our solve function to receive a pointer to where the variables are stored: ``` float solve(const char* eq,char start,char end,const float* vars = nullptr); ``` We can then use almost the same source code as above: ``` else if(i>1 && eq[i]=='s' && eq[i-1]=='n' && eq[i-2]=='a'){ if(vars) numbers.push(vars[0]); else numbers.push(std::nanf("")); i-=2; } ``` By implementing variables in this way, we have to be very careful that the specified array is the correct size! We also have to map each variable we want to use to an index (e.g. \(\text{ans}\) is stored at index \(0\)). If you have any questions or find a mistake feel free to comment on GitHub or use the contact form in the top right corner of the screen to message me directly! Further Ideas ------------- Now that you have your basic c++ calculator working, try implementing: * fractions. Instead of returning a double or float, you could work with your own fraction class to get exact results. (Tip: take a look at continued fractions for typecasting a double to a fraction) * better support for wrong inputs. Try either return an error or fix the input. * this function in a c++ calculator class As always feel free to share your changes on GitHub! --- ## [ESP32 Preferences — Save Data Across Reboots (NVS)](https://blog.hirnschall.net/esp32-preferences/) meta description: Persist data on the ESP32 across reboots with the Preferences library. Key-value storage in NVS, read-only namespace, with full code example. date published: 26.04.2026 (DD.MM.YYYY format) date last modified: 07.05.2026 (DD.MM.YYYY format) --- # [ESP32: Save Data Across Reboots with Preferences](https://blog.hirnschall.net/esp32-preferences/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Persist data on the ESP32 across reboots with the Preferences library. Key-value storage in NVS, read-only namespace, with full code example. meta title: ESP32 Preferences — Save Data Across Reboots (NVS) date published: 26.04.2026 (DD.MM.YYYY format) date last modified: 07.05.2026 (DD.MM.YYYY format) --- Introduction ------------ The ESP32 Arduino core includes the Preferences library as the recommended way to store data persistently across reboots and power loss. It replaces the EEPROM library, which on the ESP8266 required manual address management and explicit commit calls. If you are coming from the ESP8266, the [ESP8266 EEPROM article](https://blog.hirnschall.net/esp8266-eeprom/) covers that approach. This post focuses entirely on the Preferences library and how to use it effectively on the ESP32. This post is part of a complete ESP32 reference you can find [here](https://blog.hirnschall.net/esp32/). NVS — How It Works ------------------ Preferences stores data in a dedicated region of the ESP32's flash memory called NVS (Non-Volatile Storage). NVS is a separate flash partition. It is not the same region used for program code or SPIFFS/LittleFS. Its location and size are defined in the partition table. Unlike EEPROM emulation on the ESP8266, which maps a fixed block of flash directly to a RAM buffer, NVS uses a key-value store with built-in wear leveling. Flash memory has a limited number of write cycles per sector. Wear leveling distributes writes across multiple sectors so that no single sector is written repeatedly. This makes NVS significantly more suitable for data that changes frequently, such as counters or frequently updated settings. NVS also handles power loss more gracefully. A write operation that is interrupted by a reset or power failure will not corrupt existing data. The partially written entry is discarded and the previous value is retained. Namespaces ---------- Data in NVS is organized into namespaces. A namespace is a named container that holds a set of key-value pairs. Think of it as a named section in a configuration file. Multiple namespaces can coexist in NVS without conflict, even if they use the same key names. Namespace and key names are case sensitive strings with a maximum length of 15 characters [3]. This limit is important. Silently truncating a 16-character name will cause the wrong key to be read or written without error. We have to keep names short and unambiguous. Only one namespace can be open at a time. Opening a second namespace requires closing the first with `end()`. Available Functions - Supported Types ------------------------------------- The Preferences library offers `put` and `get` functions to store and retrieve data from Flash. These functions are different per type and data size we want to store. The following are available [1]: | `put` | `get` | has unsigned version | C++ type | | --- | --- | --- | --- | | putChar | getChar | ● | int8\_t/uint8\_t | | putShort | getShort | ● | int16\_t/uint16\_t | | putInt | getInt | ● | int32\_t/uint32\_t | | putLong | getLong | ● | int32\_t/uint32\_t | | putLong64 | getLong64 | ● | int64\_t/uint64\_t | | putFloat | getFloat | ○ | float\_t | | putDouble | getDouble | ○ | double\_t | | putBool | getBool | ○ | bool | | putString | getString | ○ | const char \* | | putBytes | getBytes | ○ | const void \* | Functions for default types offer unsigned versions where it makes sense. If we want to store an `int` we'd use `putInt`. To store an `unsigned int` we'd use `putUInt`. So, if the function is `putX`, the unsigned version is available as `putUX`. Same goes for the `get` functions. Before we dive into full usage examples, let's look at the function signatures for both `put` and `get`. ### put syntax With the exception of string and byte, which we will go into later, the syntax for all putX functions is similar. We will use `putChar` as an example: ``` size_t putChar(const char *key, int8_t value) ``` The first argument is the key name, and the second is the value to store.The return value is 0 on errors and 1 otherwise. ### get syntax Similarly, the syntax for getX functions is also the same across all types (except for string and byte). Again, we will use `getChar` as an example: ``` int8_t getChar(const char *key, int8_t defaultValue = 0) ``` The first argument is the key name, and the second is the default value to return if the key does not exist. The return value is the stored value or the default if not found. Basic Usage ----------- As the Preferences library is included in the ESP32 Arduino core, we do not need to install anything besides the ESP32 core from the Boards Manager. Once installed, we incldue the header and create a Preferences object as shown below. ``` #include Preferences prefs; ``` ### Opening a Namespace To open a namespace we use `begin()`, specifying the namespace name and whether to open it in read-only mode. Passing `true` as the second argument opens the namespace read-only, which prevents accidental writes and is something we will do when we do not want to write data. The example below uses the namespace called "settings". ``` prefs.begin("settings", false); // open read-write prefs.begin("settings", true); // open read-only ``` ### Writing Values We write a value using the appropriate `putX()` method, passing the key name and the value. ``` prefs.putInt("counter", 42); prefs.putFloat("threshold", 23.5); prefs.putBool("enabled", true); ``` ### Reading Values We read a value using the corresponding `getX()` method, passing the key name and a default value. The default is returned if the key does not yet exist in NVS. We can use this feature on e.g. first boot before any value has been written. ``` int counter = prefs.getInt("counter", 0); float threshold = prefs.getFloat("threshold", 20.0); bool enabled = prefs.getBool("enabled", false); ``` ### Closing a Namespace We close the namespace with `end()` when done. This releases the handle and allows another namespace to be opened. ``` prefs.end(); ``` Default Values and isKey() -------------------------- The second argument to any `getX()` call is the default value returned when the key does not exist. This is the cleanest way to handle first boot as we do not need any special initialization code. ``` int count = prefs.getInt("count", 0); // returns 0 on first boot ``` If we want to explicitly check whether a key exists before reading, we use `isKey()`. ``` if (prefs.isKey("count")) { // key exists, safe to read int count = prefs.getInt("count", 0); } else { // first boot, key not yet written } ``` In most cases the default value in `getX()` is sufficient and `isKey()` is not needed. It is most useful when the distinction between a missing key and a key set to zero matters for the application. Deleting Data ------------- There are two main ways to delete data from NVS. We can remove either a single key-value pair or all key-value pairs in the current namespace. Both options require the namespace to be open in read-write mode. Neither affects other namespaces. ### Single key-value pair We can delete a single key-value pair with `remove()`. ``` prefs.remove("counter"); ``` ### All key-value pairs If we want to delete all key-value pairs in the current namespace, we can call `clear()` as shown below. This is useful for resetting all settings to defaults on e.g. a factory reset operation. ``` prefs.clear(); ``` Storing Strings and Bytes ------------------------- Strings are stored and retrieved using `putString()` and `getString()`. Again, the first argument is the key and the second argument is the value. Both char arrays and Arduino Strings can be used as values. When reading with a default value, the return value is an Arduino String. Alternatively, a char array can be passed as the second argument to `getString()`, in which case the value is copied into the provided buffer. We will stick to the Arduino String approach in the example below for simplicity. ``` prefs.putString("ssid", "MyWiFi"); prefs.putString("password", "MyPassword"); String ssid = prefs.getString("ssid", ""); String password = prefs.getString("password", ""); ``` For storing arbitrary binary data or a complete struct, we can use `putBytes()` and `getBytes()`. This is equivalent to the `EEPROM.put()` and `EEPROM.get()` approach from the ESP8266. We pass a pointer to the data and its size in bytes. The example below shows how we can store wifi credentials and application settings together in a struct. This way we can read and write everything in a single call without needing to manage multiple keys. ``` struct Settings { char ssid[32]; char password[64]; float threshold; bool enabled; }; Settings settings = {"MyWiFi", "MyPassword", 23.5, true}; //example data // write prefs.putBytes("cfg", &settings, sizeof(settings)); // read prefs.getBytes("cfg", &settings, sizeof(settings)); ``` Using `sizeof()` ensures the correct number of bytes is always written and read regardless of the struct layout or field sizes. This is probably the most convenient approach when storing multiple related values that belong together. ### Checking Stored Size Both String and Byte offers a way to check the size of a stored value before reading. We can do so using `getBytesLength()` and `getStringLength()`. This is useful to verify that the stored size matches the current struct size. E.g. after a firmware update that adds fields to the struct. Below we check if the struckt size has changed since the last write. If it has, we can assume the layout is different and use defaults instead of reading incompatible data. ``` size_t storedSize = prefs.getBytesLength("cfg"); if (storedSize == sizeof(settings)) { prefs.getBytes("cfg", &settings, sizeof(settings)); } else { // struct layout has changed, use defaults } ``` Multiple Namespaces ------------------- Namespaces are useful for separating different parts of an application. For example, network credentials can live in one namespace and application settings in another. Since only one namespace can be open at a time, we open, use, and close each one in sequence. The main reason to use multiple namespaces is to allow for keys with the same name to exist without conflict. For example, we could have a `version` key in both the `network` and `app` namespaces without any issues. ``` prefs.begin("network", false); prefs.putString("ssid", "MyWiFi"); prefs.putString("password", "MyPassword"); prefs.end(); prefs.begin("app", false); prefs.putFloat("threshold", 23.5); prefs.putBool("enabled", true); prefs.end(); ``` Full Example ------------ The following example stores WiFi credentials and application settings using a struct. On first boot the defaults are used and written to NVS. On subsequent boots the stored values are read back. ``` #include #include Preferences prefs; struct Settings { char ssid[32]; char password[64]; float threshold; bool enabled; }; Settings settings; void loadSettings() { prefs.begin("cfg", true); // read-only if (prefs.getBytesLength("settings") == sizeof(settings)) { prefs.getBytes("settings", &settings, sizeof(settings)); Serial.println("Settings loaded from NVS"); } else { // first boot or struct size changed — use defaults strncpy(settings.ssid, "MyWiFi", sizeof(settings.ssid)); strncpy(settings.password, "MyPassword", sizeof(settings.password)); settings.threshold = 23.5; settings.enabled = true; Serial.println("Using default settings"); } prefs.end(); } void saveSettings() { prefs.begin("cfg", false); // read-write prefs.putBytes("settings", &settings, sizeof(settings)); prefs.end(); Serial.println("Settings saved to NVS"); } void setup() { Serial.begin(115200); loadSettings(); WiFi.begin(settings.ssid, settings.password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); Serial.print("Connected. IP: "); Serial.println(WiFi.localIP()); // modify and save a setting settings.threshold = 25.0; saveSettings(); } void loop() { } ``` Migration from EEPROM --------------------- If you are moving existing ESP8266 EEPROM code to the ESP32, the main differences are as follows. On the ESP8266 we call `EEPROM.begin(size)` and manage addresses manually, writing and reading bytes at specific offsets. On the ESP32 with Preferences, addresses do not exist. They are replaced by keys. There is no `commit()` call because each `putX()` writes directly to NVS. There is no fixed size to declare upfront. The struct approach using `putBytes()` and `getBytes()` maps most directly to the `EEPROM.put()` and `EEPROM.get()` pattern. If your ESP8266 code stores a struct at address zero, the equivalent on the ESP32 is `putBytes("cfg", &myStruct, sizeof(myStruct))`. The rest of the application code that reads from and writes to the struct is unchanged. The EEPROM library is still available on the ESP32 for compatibility, but new projects should probably use Preferences. Where & What to Buy ------------------- Below are links1 to the ESP modules and development boards I personally tend to use. If you are unsure about which one to choose, [check out this comparison post!](https://blog.hirnschall.net/esp8266-vs-esp32/) | ESP-12F | NodeMCU ESP8266 | ESP32-WROOM-32 | ESP32-S3 | | --- | --- | --- | --- | | [amazon.com](https://amzn.to/41YJ965) [amazon.de](https://amzn.to/3Olf3GC) | [amazon.com](https://amzn.to/4smbFcB) [amazon.de](https://amzn.to/4eeUhD6) | [amazon.com](https://amzn.to/4tItyDJ) [amazon.de](https://amzn.to/41V075k) | [amazon.com](https://amzn.to/4tEXG2G) [amazon.de](https://amzn.to/4c26zgR) | | #CommissionsEarned | | | | --- ## [RC Time Constant Calculator — Solve for τ, R, or C](https://blog.hirnschall.net/tools/rc-time-constant/) meta description: Calculate the RC time constant. Solve for τ, resistance, or capacitance given the other two. Optionally calculate charging or discharging voltage at any time t. date published: 01.04.2025 (DD.MM.YYYY format) date last modified: 22.04.2025 (DD.MM.YYYY format) --- # [RC Time Constant Calculator](https://blog.hirnschall.net/tools/rc-time-constant/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate the RC time constant. Solve for τ, resistance, or capacitance given the other two. Optionally calculate charging or discharging voltage at any time t. meta title: RC Time Constant Calculator — Solve for τ, R, or C date published: 01.04.2025 (DD.MM.YYYY format) date last modified: 22.04.2025 (DD.MM.YYYY format) --- Calculator ---------- Provide any two of \( R \), \( C \), \( \tau \) — the third is solved. Optionally provide a time \( t \) and an initial or supply voltage to calculate the charging or discharging voltage at that time. * Resistance (R): * Ω kΩ MΩ * Capacitance (C): * F mF µF nF pF * Time constant (τ): * s ms µs min --- * Time (t) — optional: * s ms µs min * Supply voltage Vs (charging) — optional: * V mV kV * Initial voltage V0 (discharging) — optional: * V mV kV Provide any two of R, C, τ to calculate. Calculate RC Time Constant — Explanation ------------------------------ The RC time constant \( \tau \) characterises how quickly a capacitor charges or discharges through a resistor: \[ \tau = R \cdot C \] Where \( R \) is in Ohms and \( C \) is in Farads, giving \( \tau \) in seconds. It is the single most important parameter of any RC circuit — it sets the speed of every transition. Charging -------- When a discharged capacitor is connected to a supply voltage \( V\_s \) through a resistor, the voltage across the capacitor rises exponentially: \[ V(t) = V\_s \cdot \left(1 - e^{-t/\tau}\right) \] After one time constant the capacitor has charged to \( 1 - 1/e \approx 63.2\% \) of \( V\_s \). After five time constants it is considered fully charged: \[ \begin{align} t = 1\tau &\Rightarrow V = 63.2\%\ V\_s \\ t = 2\tau &\Rightarrow V = 86.5\%\ V\_s \\ t = 3\tau &\Rightarrow V = 95.0\%\ V\_s \\ t = 4\tau &\Rightarrow V = 98.2\%\ V\_s \\ t = 5\tau &\Rightarrow V = 99.3\%\ V\_s \end{align} \] Discharging ----------- When a fully charged capacitor discharges through a resistor, the voltage decays exponentially: \[ V(t) = V\_0 \cdot e^{-t/\tau} \] After one time constant the voltage has dropped to \( 1/e \approx 36.8\% \) of \( V\_0 \). The decay is the mirror image of the charging curve. To compute the exact voltage, current, and charge at a specific time, use the [capacitor discharge calculator](https://blog.hirnschall.net/tools/capacitor-discharge/). Purpose of the Calculator ------------------------- Given any two of \( R \), \( C \), \( \tau \), the calculator solves for the third: * To solve for \( \tau \): \[ \tau = R \cdot C \] * To solve for \( R \): \[ R = \frac{\tau}{C} \] * To solve for \( C \): \[ C = \frac{\tau}{R} \] Optionally, providing \( t \) and \( V\_s \) or \( V\_0 \) calculates the charging or discharging voltage at that time. Both can be provided simultaneously to compare charging and discharging in the same circuit. If C is a combination of capacitors, compute the equivalent value first with the [capacitors in series](https://blog.hirnschall.net/tools/capacitors-in-series/) or [capacitors in parallel](https://blog.hirnschall.net/tools/capacitors-in-parallel/) calculator. Related Tools ------------- * [Capacitor Discharge Calculator](https://blog.hirnschall.net/tools/capacitor-discharge/) — compute voltage, current, and charge over time for an RC discharge. * [Capacitor Impedance Calculator](https://blog.hirnschall.net/tools/capacitor-impedance/) — frequency-domain view of the same RC circuit. * [Capacitors in Series](https://blog.hirnschall.net/tools/capacitors-in-series/) / [Capacitors in Parallel](https://blog.hirnschall.net/tools/capacitors-in-parallel/) — if C is a network, compute the equivalent capacitance first. * [RL Time Constant Calculator](https://blog.hirnschall.net/tools/rl-time-constant/) — the inductive equivalent: \( \tau = L/R \), current as the state variable. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [DIY WiFi Smart Blinds for $10 — Alexa & HomeKit](https://blog.hirnschall.net/diy-smart-window-blinds/) meta description: Automate window blinds with an ESP8266 for under $10. Includes full schematic, Arduino code, and Homebridge setup for Alexa and Apple HomeKit integration. date published: 14.08.2020 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [DIY smart window blinds that work with Homekit and Alexa](https://blog.hirnschall.net/diy-smart-window-blinds/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Automate window blinds with an ESP8266 for under $10. Includes full schematic, Arduino code, and Homebridge setup for Alexa and Apple HomeKit integration. meta title: DIY WiFi Smart Blinds for $10 — Alexa & HomeKit date published: 14.08.2020 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- Yes, I LOVE Alexa. Unfortunately, I could not find a product to upgrade my existing window blinds to work with Alexa. So, let's go through the design process and how you can upgrade your own blinds to be smart. Design considerations --------------------- As always, I like to start with a list of features around which we can design the product. The smart window blinds upgrade has to be: * small, unobtrusive and quiet * compatible with Alexa over wifi * cheap * work with my existing window blinds * ideally also work with Apple Homekit as I am using an iPhone Parts ----- At first, I wanted to use a stepper motor to open the blinds, but it was too big and way too noisy. Instead, we'll use a small 9g servo combined with an esp wifi module. We'll need the following parts for one window: | Component | Quantity | Where to buy | | --- | --- | --- | | [esp8266 (12F or 01 recommended)](https://www.amazon.com/gp/product/B07SDMHYY8/ref=as_li_tl?ie=UTF8&tag=bloghirnsch0b-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B07SDMHYY8&linkId=a2780fd2e9b5d7abc644a1da280efb41) | × 1 | * [Amazon.com](https://www.amazon.com/gp/product/B07SDMHYY8/ref=as_li_tl?ie=UTF8&tag=bloghirnsch0b-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B07SDMHYY8&linkId=a2780fd2e9b5d7abc644a1da280efb41) * [Amazon.de](https://www.amazon.de/gp/product/B07DBC7KWN/ref=as_li_tl?ie=UTF8&tag=bloghirnschal-21&camp=1638&creative=6742&linkCode=as2&creativeASIN=B07DBC7KWN&linkId=081fbb3bc96753cce6325506821beb93) | | [5V to 3.3V step-down converter](https://www.amazon.com/gp/product/B07FSLGPR8/ref=as_li_tl?ie=UTF8&tag=bloghirnsch0b-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B07FSLGPR8&linkId=ac000a2f4948cc4f2f1dfa7037a9c289) | × 1 | * [Amazon.com](https://www.amazon.com/gp/product/B07FSLGPR8/ref=as_li_tl?ie=UTF8&tag=bloghirnsch0b-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B07FSLGPR8&linkId=ac000a2f4948cc4f2f1dfa7037a9c289) * [Amazon.de](https://www.amazon.de/gp/product/B07KQC571X/ref=as_li_tl?ie=UTF8&tag=bloghirnschal-21&camp=1638&creative=6742&linkCode=as2&creativeASIN=B07KQC571X&linkId=9bd3f9b04081b9d0d62a78465b67c674) | | [9g servo](https://www.amazon.com/gp/product/B07F7VJQL5/ref=as_li_tl?ie=UTF8&tag=bloghirnsch0b-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B07F7VJQL5&linkId=3b4c47aa9a71196a01ae6acf3a500920) | × 1 | * [Amazon.com](https://www.amazon.com/gp/product/B07F7VJQL5/ref=as_li_tl?ie=UTF8&tag=bloghirnsch0b-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B07F7VJQL5&linkId=3b4c47aa9a71196a01ae6acf3a500920) * [Amazon.de](https://www.amazon.de/gp/product/B07FQMTLD4/ref=as_li_tl?ie=UTF8&tag=bloghirnschal-21&camp=1638&creative=6742&linkCode=as2&creativeASIN=B07FQMTLD4&linkId=db000d4162c8900a36cc04758b6e2976) | | [cheap USB phone charger](https://www.amazon.com/gp/product/B017TXGM4I/ref=as_li_tl?ie=UTF8&tag=bloghirnsch0b-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B017TXGM4I&linkId=9754e53935d8b8e1814cd329d7ab39d5) | × 1 | * [Amazon.com](https://www.amazon.com/gp/product/B017TXGM4I/ref=as_li_tl?ie=UTF8&tag=bloghirnsch0b-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B017TXGM4I&linkId=9754e53935d8b8e1814cd329d7ab39d5) * [Amazon.de](https://www.amazon.de/gp/product/B07VQ5JJQK/ref=as_li_tl?ie=UTF8&tag=bloghirnschal-21&camp=1638&creative=6742&linkCode=as2&creativeASIN=B07VQ5JJQK&linkId=9ae72758d8da406e69359ec5266b285a) | | #CommissionsEarned | | | We will also (temporarily) need an Arduino to program the esp and one raspberry pi acting as a hub for all Alexa/Homekit projects combined. | Component | Quantity | Where to buy | | --- | --- | --- | | [Arduino Uno](https://www.amazon.com/gp/product/B008GRTSV6/ref=as_li_tl?ie=UTF8&tag=bloghirnsch0b-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B008GRTSV6&linkId=1c6cb7b1c28b255f152991f42a7f985c) | × 1 | * [Amazon.com](https://www.amazon.com/gp/product/B008GRTSV6/ref=as_li_tl?ie=UTF8&tag=bloghirnsch0b-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B008GRTSV6&linkId=1c6cb7b1c28b255f152991f42a7f985c) * [Amazon.de](https://www.amazon.de/gp/product/B008GRTSV6/ref=as_li_tl?ie=UTF8&tag=bloghirnschal-21&camp=1638&creative=6742&linkCode=as2&creativeASIN=B008GRTSV6&linkId=0145d74f9e9dedd4686aeed60d74ac70) | | [Raspberry Pi](https://www.amazon.com/gp/product/B07BC7BMHY/ref=as_li_tl?ie=UTF8&tag=bloghirnsch0b-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B07BC7BMHY&linkId=44f53ae493089027ad093aaa72a8aec6) | × 1 | * [Amazon.com](https://www.amazon.com/gp/product/B07BC7BMHY/ref=as_li_tl?ie=UTF8&tag=bloghirnsch0b-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B07BC7BMHY&linkId=44f53ae493089027ad093aaa72a8aec6) * [Amazon.de](https://www.amazon.de/gp/product/B07DDCRFP6/ref=as_li_tl?ie=UTF8&tag=bloghirnschal-21&camp=1638&creative=6742&linkCode=as2&creativeASIN=B07DDCRFP6&linkId=ef7191994b0abba5e69b8a3a45c2af7d) | | [microSD Card (for Raspberry Pi)](https://www.amazon.com/gp/product/B06XWN9Q99/ref=as_li_tl?ie=UTF8&tag=bloghirnsch0b-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B06XWN9Q99&linkId=2c0e53839c4246a7b9f8eee40a553a72) | × 1 | * [Amazon.com](https://www.amazon.com/gp/product/B06XWN9Q99/ref=as_li_tl?ie=UTF8&tag=bloghirnsch0b-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B06XWN9Q99&linkId=2c0e53839c4246a7b9f8eee40a553a72) * [Amazon.de](https://www.amazon.de/gp/product/B08GY9NYRM/ref=as_li_tl?ie=UTF8&tag=bloghirnschal-21&camp=1638&creative=6742&linkCode=as2&creativeASIN=B08GY9NYRM&linkId=ad861100ee87aaca36d1e3ef4920248c) | | #CommissionsEarned | | | ### Cost As most smart home accessories require some sort of hub, we will not consider the raspberry pi's cost. So, all in all, this project is about $6 to $7 per window. Schematic --------- As the esp8266 uses 3.3V and the USB charger outputs 5V, we'll use a cheap step down converter. The servo itself uses 5V. You can see how to connect everything in the schematic below. You can (as always) download everything you need to build this project yourself in the download section. ![smart window blinds schematic](https://blog.hirnschall.net/diy-smart-window-blinds/resources/img/sketch.png) Figure 3.1: Schematic for smart window blinds Assembly -------- ### Enclosure To make this project really stand out, I decided to 3d print a custom enclosure. This is not required as you can use any plastic box and make the necessary cutouts with a sharp knife. (Tip: heat the knife up using a lighter to cut through plastic easily). ### Wiring I used speaker-wire as it is very thick (thanks to all audiophiles), long and cheap. We can cut through a cheap USB cable we no longer need and connect the red (+) and black (-) wire to the speaker-wire. ![finished smart window blinds electronics inside 3d printed enclosure](https://blog.hirnschall.net/diy-smart-window-blinds/resources/img/IMG_3053.png) ![finished smart window blinds electronics inside 3d printed enclosure](https://blog.hirnschall.net/diy-smart-window-blinds/resources/img/IMG_3079.png) Figure 4.1: Window blinds electronics inside 3d printed enclosure Software -------- ### Homebridge (Alexa and Homekit integration) To use our own product with Alexa and Homekit, we'll use Homebridge on a raspberry pi. Please take a look at for detailed instructions on how to install Homebridge. We will also use the homebridge-alexa plugin and the http-window-blind plugin. http-plugins allow us to easily add DIY smart home products using http and an esp. ### ESP code We can take a look at the http-window-blind plugin to see how the esp is supposed to respond to requests. The plugin will use a get variable named "pos" to pass a value between 0 and 100 to the esp, and it expects the response to be json. E.g. {"position":"20"} where 20 is 20% open. Implementing this is straight forward. Upon an http-request, we map the "pos" value to an angle between 0 and 180 degrees and set the servo position accordingly. You can download the code for this project below. Now all that's left is to program the esp using an Arduino. We can do this with the Arduino IDE, as seen in this post: [How to program an esp8266 with Arduino.](https://blog.hirnschall.net/how-to-program-an-esp-with-arduino/) Automation ---------- As the new window blinds work with both Alexa and Homekit, we can use an apple tv for automation. I like my blinds to open and close at sunrise/sunset. Another great feature is automatically closing them when I leave and open them when I arrive at home. Résumé ------ I am really happy with this project. One of the main goals was to automatically close the blinds as soon as it gets dark outside to prevent people from seeing inside when the lights are on. This works great, and it is a great project to show off :) After all, we automated our window blinds ourselves! [ DIY automated window blinds in action! ](resources/vid/showoff.webm) Figure 7.1: DIY smart window blinds in action --- ## [RL Time Constant Calculator — Solve for τ, L, or R](https://blog.hirnschall.net/tools/rl-time-constant/) meta description: Calculate the RL time constant τ = L/R. Solve for time constant, inductance, or resistance. Optionally compute energizing or de-energizing current at a given time. date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- # [RL Time Constant Calculator](https://blog.hirnschall.net/tools/rl-time-constant/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate the RL time constant τ = L/R. Solve for time constant, inductance, or resistance. Optionally compute energizing or de-energizing current at a given time. meta title: RL Time Constant Calculator — Solve for τ, L, or R date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- Calculator ---------- Provide any two of \( R \), \( L \), \( \tau \) — the third is solved. Optionally provide a time \( t \) and a supply voltage or initial current to calculate the energizing or de-energizing current at that time. * Resistance (R): * Ω kΩ MΩ * Inductance (L): * H mH µH nH * Time constant (τ): * s ms µs min --- * Time (t) — optional: * s ms µs min * Supply voltage Vs (energizing) — optional: * V mV kV * Initial current I0 (de-energizing) — optional: * A mA Provide any two of R, L, τ to calculate. Calculate RL Time Constant — Explanation ------------------------------ The RL time constant \( \tau \) characterises how quickly the current in an RL circuit rises or falls in response to a voltage step: \[ \tau = \frac{L}{R} \] Where \( L \) is in Henrys and \( R \) is in Ohms, giving \( \tau \) in seconds. A larger inductance stores more energy and takes longer to change its current; a larger resistance dissipates energy faster and shortens the transition. The time constant is the single number that governs every transient in the circuit. The unit check is worth doing once: \( [L/R] = [\text{H}/\Omega] = [\text{H} \cdot \text{A} / \text{V}] = [\text{Wb} / \text{V}] = [\text{V} \cdot \text{s} / \text{V}] = [\text{s}] \). This is the RL counterpart of the RC time constant \( \tau = RC \); see the [RC time constant calculator](https://blog.hirnschall.net/tools/rc-time-constant/) for the capacitive version. Energizing ---------- When a voltage \( V\_s \) is applied to a series RL circuit with zero initial current, the current rises exponentially toward its steady-state value \( I\_{\max} = V\_s / R \): \[ I(t) = \frac{V\_s}{R} \left(1 - e^{-t/\tau}\right) \] At \( t = 0 \) the inductor looks like an open circuit — it carries no current yet and all of \( V\_s \) appears across it. As current builds up, the back-EMF of the inductor falls and the resistor takes an increasing share of the voltage. After five time constants the current is within 1% of \( I\_{\max} \) and the inductor looks like a plain wire carrying DC: \[ \begin{align} t = 1\tau &\Rightarrow I = 63.2\%\ I\_{\max} \\ t = 2\tau &\Rightarrow I = 86.5\%\ I\_{\max} \\ t = 3\tau &\Rightarrow I = 95.0\%\ I\_{\max} \\ t = 4\tau &\Rightarrow I = 98.2\%\ I\_{\max} \\ t = 5\tau &\Rightarrow I = 99.3\%\ I\_{\max} \end{align} \] De-energizing ------------- When the supply is removed from an inductor carrying an initial current \( I\_0 \), the current decays exponentially through the discharge path (typically a freewheeling diode or the source resistance): \[ I(t) = I\_0 \cdot e^{-t/\tau} \] After one time constant the current has fallen to \( 1/e \approx 36.8\% \) of \( I\_0 \). The inductor resists the drop and will develop whatever voltage across itself is needed to keep current flowing — this is the source of the inductive voltage spike that appears when an inductive load is switched off without a suppression path. The energy stored at \( I\_0 \) is \( E = \frac{1}{2}LI\_0^2 \); see the [inductor energy calculator](https://blog.hirnschall.net/tools/inductor-stored-energy/) to quantify it. Purpose of the Calculator ------------------------- Given any two of \( R \), \( L \), \( \tau \), the calculator solves for the third: * To solve for \( \tau \): \[ \tau = \frac{L}{R} \] * To solve for \( R \): \[ R = \frac{L}{\tau} \] * To solve for \( L \): \[ L = \tau \cdot R \] Optionally, providing \( t \) and \( V\_s \) calculates the energizing current at that time, and providing \( t \) and \( I\_0 \) calculates the de-energizing current. Both can be provided together to compare the two in the same circuit. If L is a combination of inductors, compute the equivalent value first with the [inductors in series](https://blog.hirnschall.net/tools/inductors-in-series/) or [inductors in parallel](https://blog.hirnschall.net/tools/inductors-in-parallel/) calculator. Related Tools ------------- * [RC Time Constant Calculator](https://blog.hirnschall.net/tools/rc-time-constant/) — the capacitive equivalent: \( \tau = RC \), voltage across capacitor as the state variable. * [Inductor Impedance Calculator](https://blog.hirnschall.net/tools/inductor-impedance/) — frequency-domain view of the same RL circuit. * [Inductor Energy Calculator](https://blog.hirnschall.net/tools/inductor-stored-energy/) — quantify the energy stored at a given current. * [Inductors in Series](https://blog.hirnschall.net/tools/inductors-in-series/) / [Inductors in Parallel](https://blog.hirnschall.net/tools/inductors-in-parallel/) — compute the equivalent inductance of a network before using it here. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [Maze Generator in JavaScript — Random Walker OOP](https://blog.hirnschall.net/maze-generation-js/) meta description: Build a maze generator in JavaScript using the random walker algorithm. Annotated source code you can drop into a p5.js sketch or any canvas project. date published: 30.05.2021 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [Coding Challenge #2: Maze Generation](https://blog.hirnschall.net/maze-generation-js/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Build a maze generator in JavaScript using the random walker algorithm. Annotated source code you can drop into a p5.js sketch or any canvas project. meta title: Maze Generator in JavaScript — Random Walker OOP date published: 30.05.2021 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- In this weeks challenge we'll take a look at maze generation with javascript. But why would you do that? As always the intent of this challenge is to learn something new in an interesting way. This week we focus on getting more comfortable with classes and object oriented programming. How Maze Generation Works ------------------------- There are several different ways to generate a maze. Some more complicated than others. As this is our second challenge we'll use a simple algorithm which gets even easier as it is related to [Challenge #1: Random walker](https://blog.hirnschall.net/random-walker-js/). Lets start with a square checker board. To generate the maze we will trace the path the random walker takes until it reaches the end point. For this to result in a maze we have to change the walker in several ways: * it has to visit every cell * it is not allowed to revisit cells if there is an alternative * if all adjacent cells have been visited it has to go back until a unvisited cell becomes available Getting Started With p5js ------------------------- We'll use p5.js, a great javascript library for drawing and animation. ### The Cells To make things easier each cell should be its own object and keep track of the following: * its location (x,y) * if it has been visited * which way the walker was going (for drawing walls) We are also using a global variable called "w" for the cells width/height. Using global variable is generally not a good Idea, but it is ok for smaller projects like this one. ``` function Cell(x, y) { this.x = x; this.y = y; this.wall = [1, 1, 1, 1]; this.visited = 0; ``` As you can see, member variables are always named with a starting "this.". To draw a cell it also needs a "show" function. Sometimes also called "display" or something similar. It is called each frame to display or update the cell. It has to: * draw a square at the correct position if the cell has been visited * draw walls on each side the walker was not going through I also want the start at the top left and the finish at the bottom right to have a different color.(green and red) Memberfunction names also start with a "this.": ``` this.show = function () { var x = this.x * w; var y = this.y * w; if (this.visited) { if(!this.x && !this.y) fill(50,200,50); else if(this.x == cols-1 && this.y == rows-1) fill(200,50,50); else fill(150); } else { noFill(); } noStroke(); rect(x, y, w, w); stroke(255); if (this.wall[0]) //h line { line(x, y, x + w, y); } if (this.wall[1]) //h line { line(x + w, y, x + w, y + w); } if (this.wall[2]) //h line { line(x, y + w, x + w, y + w); } if (this.wall[3]) //h line { line(x, y, x, y + w); } } } ``` We first check if the cell has been visited, if it is the start cell with position x=0 and y=0 or if it is the finish cell with position x=cols-1 and y=rows-1. We can then draw a rectangle with the correct color and check which walls should be painted using the "line" function. ### The Walker We store all cells in a matrix "grid[x][y]". We will take a closer look at how to initialize this array in the next section. As we are only using a singe walker I have decided to build it right into the main draw loop. Each frame it will do the following: * check if all adjacent cells have been visited * if not choose one at random * otherwise go the the cell it has been before * update the walls array of adjacent cells We start by checking if all adjacent cells have been visited. If we find one that has not we store it in a new array called "next". ``` var next = []; if (currentCell.x > 0 && !grid[currentCell.x - 1][currentCell.y].visited) { next.push(grid[currentCell.x - 1][currentCell.y]); } if (currentCell.y > 0 && !grid[currentCell.x][currentCell.y - 1].visited) { next.push(grid[currentCell.x][currentCell.y - 1]); } if (currentCell.x < cols - 1 && !grid[currentCell.x + 1][currentCell.y].visited) { next.push(grid[currentCell.x + 1][currentCell.y]); } if (currentCell.y < rows - 1 && !grid[currentCell.x][currentCell.y + 1].visited) { next.push(grid[currentCell.x][currentCell.y + 1]); } ``` We can now check the length of "next" to see if there are cells available for the walker to move on. In case you wonder where currentCell.x and currentCell.y in the listing above comes from, you can see how we update it at the end of the next listing. If there are cells available, we push the current position in an array called "stack". The walker then generates a random number between 0 and next.length. As the random function generates a real number we use the floor function to round it to the next integer. ``` if (next.length) //if there are cells available { stack.push(currentCell); //push to stack ++stackc; var nextIndex = random(next.length); if (nextIndex == next.length) nextIndex -= 1; var nextCell = next[floor(nextIndex)]; if (nextCell.x > current[0] && nextCell.y == current[1]) { nextCell.wall[3] = 0; currentCell.wall[1] = 0; } else if (nextCell.x < current[0] && nextCell.y == current[1]) { nextCell.wall[1] = 0; currentCell.wall[3] = 0; } else if (nextCell.x == current[0] && nextCell.y > current[1]) { nextCell.wall[0] = 0; currentCell.wall[2] = 0; } else if (nextCell.x == current[0] && nextCell.y < current[1]) { nextCell.wall[2] = 0; currentCell.wall[0] = 0; } current[0] = nextCell.x; current[1] = nextCell.y; } ``` If no cells are available we check "stack" to find the previous position. If "stack" has length 0 we are done! ``` else { //no unvisited cells reachable if (stack.length) { current[0] = stack[stackc - 1].x; current[1] = stack[stackc - 1].y; stack.splice(stackc - 1, 1); --stackc; } else { return; } } ``` ### Initialization You can see how we initialize the grid array down below: ``` current = [0, 0]; cols = floor(canvasWidth / w); rows = floor(canvasHeight / w); for (var x = 0; x < cols; ++x) { grid[x] = new Array(rows); // add 2d array for (var y = 0; y < rows; ++y) { var cell = new Cell(x, y); grid[x][y] = cell; //grid[x][y].show(); } } ``` Further Ideas ------------- You could try to: * experiment with different colors * make the walker its own class * use multiple walkers with different starting points to create a more complex  maze. (Hint: be careful with initialization and end condition) * use if(mouseIsPressed){ line(pmouseX, pmouseY, mouseX, mouseY); } to solve the maze after generating it. --- ## [ESP8266 Pinout — ESP-01, ESP-07, ESP-12E/F Reference](https://blog.hirnschall.net/esp8266-reference/) meta description: ESP8266 pinout for ESP-01, ESP-01S, ESP-07, and ESP-12E/F. Boot mode pins (GPIO0, GPIO2, GPIO15), reset circuit, and minimal wiring for each. date published: 09.01.2023 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [ESP8266: Pinout, Minimal Circuit and Technical Specifications](https://blog.hirnschall.net/esp8266-reference/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: ESP8266 pinout for ESP-01, ESP-01S, ESP-07, and ESP-12E/F. Boot mode pins (GPIO0, GPIO2, GPIO15), reset circuit, and minimal wiring for each. meta title: ESP8266 Pinout — ESP-01, ESP-07, ESP-12E/F Reference date published: 09.01.2023 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- Introduction ------------ In recent years the ESP8266 has become one of my go-to choices for smart home/IoT devices or projects requiring WiFi. It is cheap [(around $1.5 on Amazon)1](https://amzn.to/3r9XX2D), has WiFi, and can be programmed using the Arduino IDE, making it an easy replacement for Arduinos if we need WiFi connectivity. This post concentrates on the technical specifications of different ESP8266 modules and the minimal circuit required to use each ESP module. It is part of [a complete ESP8266 reference you can find here.](https://blog.hirnschall.net/esp8266/) Technical Specification ----------------------- The information in this chapter is primarily taken from the datasheets of the different modules [8,9,10,11,12]. ### Power: * Supply Voltage: 2.8-3.6V * avg. current: 80mA * Max current: 430mA * avg. Light Sleep current: 0.4-1.8mA * avg. Deep Sleep current: 20µA ### Memory: * RAM: 80kB * Cache: 32kB * Flash Memory: up to 16MB external flash * Max Program size: 1MB ### Connectivity: * Digital I/O: 11 programmable pins, 12mA max. * Analog in: 1 pin, 0-1V, 10 bit resolution * Serial: 1 SPI, 1 IIC, 1 UART, 1 UART (output only) * WiFi: 802.11 b/g/n 2.4GHz, WPA/WPA2 PSK * IPv4 and IPv6 (with Arduino Core 2.5.0 or later) * UDP and TCP, max 5 simultaneous connections * Ping: 4-10ms (up to 200 on crowded channels) * Soft-AP, up to 4 simultaneous connections ESP-01 and ESP-1 ---------------- The ESP-01 and ESP-1 modules are among the cheapest options. They feature eight exposed pins (2.54mm pin header), of which only two are usable GPIO pins. The modules differ in the following way: | Module | Flash memory | LEDs | Other | | --- | --- | --- | --- | | AI-Thinker ESP-01 | 512kB (blue) or 1MB (black) | Power (red), GPIO2 (green) | | | AZ-Delivery ESP-01S | 1MB | GPIO2 (blue) | additional pull-up resistor on EN pin | | DOIT ESP-1 (ESP82855) | 1MB | TxD (blue) | | Table 2.1: ESP-01, ESP-01S and ESP-1 modules compared ### Minimal Circuit The minimal circuit required to run an ESP-01 is shown in Fig. 2.1 below. Pins 2, 4, and 6 need to be connected to VCC, while pin 7 has to be connected to ground. Pulling pin 3 low during boot enables the firmware upload mode. As always, a decoupling capacitor is placed close to the microcontroller. ![esp8266 esp-01 minimal standalone circuit diagram sketch](https://blog.hirnschall.net/esp8266-reference/resources/img/esp-01-minimal.png) Figure 2.1: ESP-01 minimal circuit ### Pinout | Pin No. | Name | Description | Behavior on boot | | --- | --- | --- | --- | | 1 | GPIO3 (RxD) | Serial input/GPIO | pulled high | | 2 | VCC | +3.3V, uses up to 430mA | pulled high | | 3 | GPIO0 | low on boot to enter programming mode, high or floating for normal boot | | | 4 | RESET | low to reset, see Fig. 2.2 | | | 5 | GPIO2 (TxD1) | normal gpio | serial output, not allowed to be low | | 6 | CHIP\_EN | high to enable chip, low to shut down | | | 7 | GND | ground | | | 8 | GPIO1 (TxD) | serial output or normal gpio | serial output, not allowed to be low | Table 2.2: ESP-01, ESP-01S and ESP-1 pin names and behavior on boot ### Reset Pin differences Furthermore, the modules differ slightly in how the reset pin circuit is realized. Figure 2.2 below shows how the module's pin 1 (labeled "1") is connected to the ESP8266's reset pin (labeled "RST"). ![esp8266 esp-01 reset pin circuit diagram sketch](https://blog.hirnschall.net/esp8266-reference/resources/img/reset-01-blue.png) (a) ESP-01 blue ![esp8266 esp-01 reset pin circuit diagram sketch](https://blog.hirnschall.net/esp8266-reference/resources/img/reset-01-black.png) (b) ESP-01 black ![esp8266 esp-01s reset pin circuit diagram sketch](https://blog.hirnschall.net/esp8266-reference/resources/img/reset-01S.png) (c) ESP-01S and ESP-1 Figure 2.2: ESP-01, ESP-01S and ESP-1 reset pin comparison [1] ESP-07 and ESP-12 ----------------- In contrast to the ESP-01 module, the ESP-07 and ESP-12 modules have additional flash memory and more exposed pins. While the ESP-07 has 16 exposed pins, the ESP-12E and F modules have 22 pins, of which six are unusable. | Module | Flash memory | LEDs | Other | | --- | --- | --- | --- | | ESP-07 | 1MB | Power (red), GPIO2 (blue) | Built in ceramic antenna, external antenna can be used (remove capacitor!) | | ESP-07S | 1MB | no LEDs | requires external antenna | | ESP-12S | 4MB | GPIO2 (blue) | built in antenna only | | ESP-12E ESP-12F | 4MB | GPIO2 (blue) | built in antenna only 6 additional, unusable, pins | Table 2.3: ESP-07 and ESP-12 modules compared ### Minimal Circuit Although the minimal circuit required to run the ESP-07 and ESP-12E/F is similar to ESP-01's, the pinout has changed. Pins 1, 3, and 8 need to be connected to VCC, while pin 7 has to be connected to ground. Additionally, pin 16 (gpio15) has to be pulled LOW during boot. Pulling pin 18 (gpio0) low during boot enables the firmware upload mode. Again, a decoupling capacitor is placed close to the microcontroller. ![esp8266 esp-12, esp-07 minimal standalone circuit diagram sketch](https://blog.hirnschall.net/esp8266-reference/resources/img/esp-12-minimal.png) Figure 2.3: ESP-07 and ESP-12 minimal circuit ### Pinout | Pin No. | Name | Description | Behavior on boot | | --- | --- | --- | --- | | 1 | RESET | low to reset, see Fig. 2.4 | | | 2 | ADC | analog input, 0-1V | | | 3 | CHIP\_EN | high to enable chip, low to shut down | | | 4 | GPIO16 | gpio (no PWM), wakeup timer output when using deep sleep | emits high pulse | | 5 | GPIO14 (SCK) | normal gpio or serial clock (SPI) | pulled high | | 6 | GPIO12 (MISO) | normal gpio or master input slave output (SPI) | pulled high | | 7 | GPIO13 (MOSI) | normal gpio or master output slave input (SPI) | pulled high | | 8 | VCC | +3.3V, uses up to 430mA | pulled high | | 9 | | already used to connect to built-in flash memory | | | 10 | | 11 | | 12 | | 13 | | 14 | | 15 | GND | ground | | | 16 | GPIO15 (CS) | normal gpio or chip select (SPI) | has to be pulled low | | 17 | GPIO2 (TxD1) | normal gpio, connected to blue LED (illuminate on low) | serial output, not allowed to be low | | 18 | GPIO0 | low on boot to enter programming mode, high or floating for normal boot | | | 19 | GPIO4 | normal gpio | | | 20 | GPIO5 | normal gpio | | | 21 | GPIO3 (RxD) | Serial input/GPIO | pulled high | | 22 | GPIO1 (TxD) | serial output or normal gpio | serial output, not allowed to be low | Table 2.4: ESP-07 and ESP-12 pin names and behavior on boot ### Reset Pin differences Furthermore, the modules differ slightly in how the reset pin circuit is realized. Figure 2.2 below shows how the module's pin 1 (labeled "1") is connected to the ESP8266's reset pin (labeled "RST"). ![esp8266 esp-07 reset pin circuit diagram sketch](https://blog.hirnschall.net/esp8266-reference/resources/img/reset-07.png) (a) ESP-07 ![esp8266 esp-07s reset pin circuit diagram sketch](https://blog.hirnschall.net/esp8266-reference/resources/img/reset-07s.png) (b) ESP-07S and ESP12S ![esp8266 esp-12e and esp-12f reset pin circuit diagram sketch](https://blog.hirnschall.net/esp8266-reference/resources/img/reset-12ef.png) (c) ESP-12E and ESP-12F Figure 2.4: ESP-07, ESP-07S, ESP-12S, ESP-12E, and ESP-12F reset pin comparison [1] Limitations ----------- Although the ESP8266 is a great chip, it has several drawbacks. * The analog input has up to 20% error [8]. Turning off WiFi before reading an analog signal can help reduce the error, as discussed in [1]. * Waking up from deep sleep requires a reset of the chip, causing data in RAM to be lost. * The RTC is inaccurate in deep-sleep. * ESP.deepSleep() sometimes does not enter deep sleep correctly [1,6]. Using delay(100) after ESP.deepSleep() helps to mitigate this problem. * The ESP has no real multitasking, so we must give the WiFi enough time to do the required tasks. This is done in between calls to loop() or during delay() or yield() calls, as discussed in [13]. If not, the ESP's watchdog resets the module. More Info --------- This post is part of a complete ESP8266 reference/guide. You can find more information on how to use your ESP8266 module effectively at [blog.hirnschall.net/esp8266/](https://blog.hirnschall.net/esp8266/). Where & What to Buy ------------------- Below are links1 to the ESP modules and development boards I personally tend to use. If you are unsure about which one to choose, [check out this comparison post!](https://blog.hirnschall.net/esp8266-vs-esp32/) | ESP-12F | NodeMCU ESP8266 | ESP32-WROOM-32 | ESP32-S3 | | --- | --- | --- | --- | | [amazon.com](https://amzn.to/41YJ965) [amazon.de](https://amzn.to/3Olf3GC) | [amazon.com](https://amzn.to/4smbFcB) [amazon.de](https://amzn.to/4eeUhD6) | [amazon.com](https://amzn.to/4tItyDJ) [amazon.de](https://amzn.to/41V075k) | [amazon.com](https://amzn.to/4tEXG2G) [amazon.de](https://amzn.to/4c26zgR) | | #CommissionsEarned | | | | --- ## [Wheatstone Bridge Calculator — Balance and Output Voltage](https://blog.hirnschall.net/tools/wheatstone-bridge/) meta description: Calculate the Wheatstone bridge balance condition or output voltage. Solve for any unknown resistor or compute V_out for an unbalanced bridge. date published: 01.04.2025 (DD.MM.YYYY format) date last modified: 22.04.2025 (DD.MM.YYYY format) --- # [Wheatstone Bridge Calculator](https://blog.hirnschall.net/tools/wheatstone-bridge/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate the Wheatstone bridge balance condition or output voltage. Solve for any unknown resistor or compute V_out for an unbalanced bridge. meta title: Wheatstone Bridge Calculator — Balance and Output Voltage date published: 01.04.2025 (DD.MM.YYYY format) date last modified: 22.04.2025 (DD.MM.YYYY format) --- Calculator ---------- Provide any three of \( R\_1 \)–\( R\_4 \) to solve for the fourth using the balance condition. Provide all four plus \( V\_s \) (\(A\) to \(C\)) to calculate the output voltage \( V\_{out} \) of an unbalanced bridge. * R1: * Ω kΩ MΩ * R2: * Ω kΩ MΩ * R3: * Ω kΩ MΩ * R4: * Ω kΩ MΩ * Supply voltage (Vs) — optional: * V mV kV Provide any three of R1-R4 to calculate. Calculate Wheatstone Bridge — Explanation ------------------------------- A Wheatstone bridge consists of four resistors arranged in a diamond configuration with a supply voltage \( V\_s \) across one diagonal and a measurement point across the other. It is used to measure an unknown resistance precisely, or to detect small resistance changes in sensor applications such as strain gauges and RTDs. Balance Condition ----------------- The bridge is balanced when no current flows through the galvanometer — i.e. when both branches have the same voltage ratio: \[ \frac{R\_1}{R\_2} = \frac{R\_3}{R\_4} \] Which is equivalently written as: \[ R\_1 \cdot R\_4 = R\_2 \cdot R\_3 \] At balance, the output voltage \( V\_{out} = 0 \). Solving for any one resistor given the other three: \[ \begin{align} R\_1 &= \frac{R\_2 \cdot R\_3}{R\_4} \\ R\_2 &= \frac{R\_1 \cdot R\_4}{R\_3} \\ R\_3 &= \frac{R\_1 \cdot R\_4}{R\_2} \\ R\_4 &= \frac{R\_2 \cdot R\_3}{R\_1} \end{align} \] Unbalanced Bridge — Output Voltage ---------------------------------- When the bridge is not balanced, a voltage appears across the output. For a high-impedance load (no current drawn from the output): \[ V\_{out} = V\_s \cdot \left(\frac{R\_3}{R\_1 + R\_3} - \frac{R\_4}{R\_2 + R\_4}\right) \] The sign of \( V\_{out} \) indicates which branch has the higher voltage. This formula assumes no loading. If a low-impedance load is connected, the output voltage will be lower due to the bridge's output impedance. Use in Sensor Applications -------------------------- In practice one or more of the resistors is a sensor — a strain gauge, thermistor, or RTD — whose resistance changes with a physical quantity. The bridge is first balanced at a reference condition. Any deviation from balance then produces a \( V\_{out} \) proportional to the resistance change, which can be amplified and measured. This is the principle behind load cells, pressure sensors, and precision temperature measurement. Related Tools ------------- * [Maxwell Bridge Calculator](https://blog.hirnschall.net/tools/maxwell-bridge/) — AC bridge for measuring unknown inductance and series resistance. * [Schering Bridge Calculator](https://blog.hirnschall.net/tools/schering-bridge/) — AC bridge for measuring unknown capacitance and dissipation factor. * [Voltage Divider Calculator](https://blog.hirnschall.net/tools/voltage-divider/) — a Wheatstone bridge is two voltage dividers sharing a supply. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [ESP8266 Level Shifting — Connect 5V Devices Safely](https://blog.hirnschall.net/esp8266-5v-devices/) meta description: ESP8266 GPIO is 3.3V — connect 5V sensors and Arduinos safely using voltage dividers. Includes circuit diagrams and resistor value calculations. date published: 09.01.2023 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [ESP8266: Connecting to other 5V Devices](https://blog.hirnschall.net/esp8266-5v-devices/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: ESP8266 GPIO is 3.3V — connect 5V sensors and Arduinos safely using voltage dividers. Includes circuit diagrams and resistor value calculations. meta title: ESP8266 Level Shifting — Connect 5V Devices Safely date published: 09.01.2023 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- Introduction ------------ In recent years the ESP8266 has become one of my go-to choices for smart home/IoT devices or projects requiring WiFi. It is cheap [(around $1.5 on Amazon)1](https://amzn.to/3r9XX2D), has WiFi, and can be programmed using the Arduino IDE, making it an easy replacement for Arduinos if we need WiFi connectivity. This post shows how we can connect 5V devices to the 3.3V ESP8266 module for, e.g., serial communication or programming. It is part of [a complete ESP8266 reference you can find here.](https://blog.hirnschall.net/esp8266/) Connecting ESP8266 to other 5V devices -------------------------------------- If we want to connect the ESP module to other 5V devices (e.g., for programming), we will use a voltage divider to reduce the 5V to 3.3V. Using the voltage devider formula $$V\_{R1} = V\_{in}\frac{R\_1}{R\_2}$$ we see that the common resistor values \(R\_1=1k\Omega\) and \(R\_2=2.2k\Omega\) reduce 5V down to 2.27V. We do not need to adjust the output of the 3.3V chip (e.g., the esp). The circuit diagram for this setup is depicted in Fig. 5.1 below. ![connecting esp8266 esp-01, esp-07 and esp-12 to 5v devices e.g. Arduino circuit diagram sketch](https://blog.hirnschall.net/esp8266-5v-devices/resources/img/5v-esp.png) Figure 5.1: Connecting ESP8266 to a 5V device (e.g. serial programmer) Is the ESP8266 5V tolerable? ---------------------------- There is some debate online whether the ESP8266 is 5V tolerable or not. Do we really need a voltage divider when programming or connecting the ESP to other 5V devices? The datasheet clearly states a maximum allowed voltage of 3.6V. Therefore, it is best practice to use a 3.3V power supply (a linear regulator or a DCDC converter) and a voltage divider when connecting to 5V devices over serial. However, I have personally powered ESP modules using 5V in the past, and I have programmed them many times without using a voltage divider without issues. So, is it recommended? No. Does it work? Probably? Most of the time? Does it shorten the ESP's lifespan? Probably also yes. More Info --------- This post is part of a complete ESP8266 reference/guide. You can find more information on how to use your ESP8266 module effectively at [blog.hirnschall.net/esp8266/](https://blog.hirnschall.net/esp8266/). Where & What to Buy ------------------- Below are links1 to the ESP modules and development boards I personally tend to use. If you are unsure about which one to choose, [check out this comparison post!](https://blog.hirnschall.net/esp8266-vs-esp32/) | ESP-12F | NodeMCU ESP8266 | ESP32-WROOM-32 | ESP32-S3 | | --- | --- | --- | --- | | [amazon.com](https://amzn.to/41YJ965) [amazon.de](https://amzn.to/3Olf3GC) | [amazon.com](https://amzn.to/4smbFcB) [amazon.de](https://amzn.to/4eeUhD6) | [amazon.com](https://amzn.to/4tItyDJ) [amazon.de](https://amzn.to/41V075k) | [amazon.com](https://amzn.to/4tEXG2G) [amazon.de](https://amzn.to/4c26zgR) | | #CommissionsEarned | | | | --- ## [Capacitors in Series Calculator — Total Capacitance](https://blog.hirnschall.net/tools/capacitors-in-series/) meta description: Calculate the total capacitance of capacitors in series. Add as many capacitors as needed. Includes formula explanation and special cases. date published: 01.04.2025 (DD.MM.YYYY format) date last modified: 22.04.2025 (DD.MM.YYYY format) --- # [Capacitors in Series Calculator](https://blog.hirnschall.net/tools/capacitors-in-series/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate the total capacitance of capacitors in series. Add as many capacitors as needed. Includes formula explanation and special cases. meta title: Capacitors in Series Calculator — Total Capacitance date published: 01.04.2025 (DD.MM.YYYY format) date last modified: 22.04.2025 (DD.MM.YYYY format) --- Calculator ---------- Enter the capacitance of each capacitor in series. Add more capacitors with the button below. The total capacitance is calculated from all filled fields. * Capacitor 1 (C1): * F mF µF nF pF * Capacitor 2 (C2): * F mF µF nF pF Enter at least two capacitance values. Add Capacitor Calculate Capacitors in Series — Explanation ---------------------------------- When capacitors are connected in series, the total capacitance is always less than the smallest individual capacitor. Each capacitor adds an additional gap for the electric field to cross, which reduces the overall ability to store charge. The formula for \( n \) capacitors in series is: \[ \frac{1}{C\_{\text{total}}} = \frac{1}{C\_1} + \frac{1}{C\_2} + \cdots + \frac{1}{C\_n} \] Or equivalently: \[ C\_{\text{total}} = \frac{1}{\dfrac{1}{C\_1} + \dfrac{1}{C\_2} + \cdots + \dfrac{1}{C\_n}} \] This is the same form as resistors in parallel, which is a useful analogy if you already have that formula memorized. The reason for the reciprocal relationship is that in series, all capacitors carry the same charge \( Q \), so the total voltage is the sum of the individual voltages: \[ V\_{\text{total}} = V\_1 + V\_2 + \cdots + V\_n = \frac{Q}{C\_1} + \frac{Q}{C\_2} + \cdots + \frac{Q}{C\_n} \] Dividing both sides by \( Q \) gives the series formula directly. Special Case: Two Capacitors ---------------------------- For exactly two capacitors in series the formula simplifies to the product-over-sum form, which is often faster to compute by hand: \[ C\_{\text{total}} = \frac{C\_1 \cdot C\_2}{C\_1 + C\_2} \] Special Case: Equal Capacitors ------------------------------ When all \( n \) capacitors have the same value \( C \), the total simplifies further: \[ C\_{\text{total}} = \frac{C}{n} \] For example, three 30 µF capacitors in series give a total of 10 µF. Series combinations are also a practical way to reach a non-standard C value for a specific [RC time constant](https://blog.hirnschall.net/tools/rc-time-constant/) or [LC resonant frequency](https://blog.hirnschall.net/tools/lc-resonance-frequency/). Related Tools ------------- * [Capacitors in Parallel Calculator](https://blog.hirnschall.net/tools/capacitors-in-parallel/) — the complementary case; total C is always greater than any individual capacitor. * [RC Time Constant Calculator](https://blog.hirnschall.net/tools/rc-time-constant/) — use the equivalent series C in an RC circuit. * [LC Resonance Frequency Calculator](https://blog.hirnschall.net/tools/lc-resonance-frequency/) — use the equivalent series C to tune the resonant frequency. * [Inductors in Series Calculator](https://blog.hirnschall.net/tools/inductors-in-series/) — the inductive equivalent; series inductors add directly. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [Inductor Impedance Calculator — Reactance, |Z|, and Phase Angle](https://blog.hirnschall.net/tools/inductor-impedance/) meta description: Calculate inductive reactance, impedance magnitude, and phase angle. Solve for X_L, inductance, or frequency given the other two. Optional series resistance. date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- # [Inductor Impedance Calculator](https://blog.hirnschall.net/tools/inductor-impedance/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate inductive reactance, impedance magnitude, and phase angle. Solve for X_L, inductance, or frequency given the other two. Optional series resistance. meta title: Inductor Impedance Calculator — Reactance, |Z|, and Phase Angle date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- Calculator ---------- Fill in any two of \( f \), \( L \), \( X\_L \) — the third is solved. \( R \) is optional: if provided, \( |Z| \) and \( \phi \) are computed as additional outputs. \( \phi \) can also be used as an input together with \( R \) to replace one of the three main variables. * Frequency (f): * Hz kHz MHz * Inductance (L): * H mH µH nH * Inductive reactance (XL): * Ω kΩ MΩ --- * Resistance (R) — optional: * Ω kΩ MΩ * Phase angle (φ) — optional: * deg rad Fill in any two of f, L, X\_L to calculate. Calculate Inductive Reactance — Explanation --------------------------------- An inductor stores energy in a magnetic field and resists any change in the current flowing through it. When an alternating current passes through an inductor, the magnetic field is constantly building and collapsing, which induces a back-EMF that opposes the driving voltage. The faster the current changes (i.e. the higher the frequency), the stronger this opposition. We quantify it as inductive reactance \( X\_L \): \[ X\_L = 2\pi f L \] Where: * \( X\_L \) is the inductive reactance (in Ohms, \( \Omega \)), * \( f \) is the frequency (in Hertz, \( \text{Hz} \)), * \( L \) is the inductance (in Henrys, \( \text{H} \)). The relationship is linear: doubling the frequency doubles \( X\_L \), and doubling the inductance doubles \( X\_L \) by the same factor. This is the opposite of a capacitor, where reactance falls as frequency rises. At DC (\( f = 0 \)) an ideal inductor has zero reactance and acts as a plain wire. At very high frequencies \( X\_L \) grows without bound, which is why inductors are used as chokes: they pass DC and low-frequency signals while increasingly blocking higher frequencies. The [capacitor impedance calculator](https://blog.hirnschall.net/tools/capacitor-impedance/) shows the complementary picture from the capacitive side. In an LC circuit, resonance occurs at the frequency where \( X\_L = X\_C \). Below that frequency the circuit is capacitive, above it the circuit is inductive. The [LC resonance frequency calculator](https://blog.hirnschall.net/tools/lc-resonance-frequency/) solves for that crossover point directly. Purpose of the Calculator ------------------------- Given any two of \( f \), \( L \), \( X\_L \), the calculator solves for the third. The rearranged formulas are: * To solve for \( X\_L \) (the most common case, checking how much a known inductor blocks at a given frequency): \[ X\_L = 2\pi f L \] * To solve for \( L \) (selecting an inductor to achieve a target reactance at a given frequency): \[ L = \frac{X\_L}{2\pi f} \] * To solve for \( f \) (finding the frequency at which a known inductor reaches a target reactance): \[ f = \frac{X\_L}{2\pi L} \] If L is made up of several inductors, compute the equivalent value first with the [inductors in series](https://blog.hirnschall.net/tools/inductors-in-series/) or [inductors in parallel](https://blog.hirnschall.net/tools/inductors-in-parallel/) calculator. Full Impedance with Series Resistance ------------------------------------- Every real inductor has winding resistance \( R \) from the copper wire. At low frequencies where \( X\_L \ll R \), this resistance dominates and the inductor looks mostly resistive. At higher frequencies \( X\_L \) takes over. The two contributions combine as perpendicular components in the complex impedance plane, giving a magnitude: \[ |Z| = \sqrt{R^2 + X\_L^2} \] and a phase angle: \[ \phi = \arctan\!\left(\frac{X\_L}{R}\right) \] The phase angle is always positive for an inductor, meaning the voltage leads the current. It runs from \( 0° \) (purely resistive, \( X\_L \ll R \)) up to \( +90° \) (purely inductive, \( X\_L \gg R \)). This is the mirror image of a capacitor, where \( \phi \) is always negative. The ratio of reactance to resistance is also the quality factor \( Q \) of the inductor: \[ Q = \frac{X\_L}{R} = \tan(\phi) \] A high \( Q \) means little resistive loss relative to the energy stored per cycle. In filter and resonator design, \( Q \) directly sets the bandwidth and insertion loss of the circuit. If \( \phi \) and \( R \) are known from a measurement (for example from an impedance analyser), the reactance can be recovered as: \[ X\_L = R \cdot \tan(\phi) \] The RL circuit also has a characteristic time constant that governs how quickly current builds up after a voltage step. That time-domain view is covered in the [RL time constant calculator](https://blog.hirnschall.net/tools/rl-time-constant/). Related Tools ------------- * [Capacitor Impedance Calculator](https://blog.hirnschall.net/tools/capacitor-impedance/) — the capacitive equivalent: \( X\_C = \frac{1}{2\pi f C} \), negative phase angle. * [LC Resonance Frequency Calculator](https://blog.hirnschall.net/tools/lc-resonance-frequency/) — find the frequency where \( X\_L \) equals \( X\_C \). * [RL Time Constant Calculator](https://blog.hirnschall.net/tools/rl-time-constant/) — time-domain view of the same RL circuit. * [Inductors in Series](https://blog.hirnschall.net/tools/inductors-in-series/) / [Inductors in Parallel](https://blog.hirnschall.net/tools/inductors-in-parallel/) — compute the equivalent inductance of a network before using it here. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [Parallel Plate Capacitor Calculator — Solve Any Variable](https://blog.hirnschall.net/tools/parallel-plate-capacitor/) meta description: Solve for capacitance, plate area, separation, or permittivity given the other three. Online calculator with formula explanation for each variable. date published: 23.11.2024 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [Parallel Plate Capacitor Calculator](https://blog.hirnschall.net/tools/parallel-plate-capacitor/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Solve for capacitance, plate area, separation, or permittivity given the other three. Online calculator with formula explanation for each variable. meta title: Parallel Plate Capacitor Calculator — Solve Any Variable date published: 23.11.2024 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- Calculator ---------- This calculator can solve for any of the four variables \( C \), \( A \), \( d \), or \( \varepsilon\_r \) when you provide the other three. * Area of the plates (A): * m² mm² cm² * Distance between plates (d): * m cm mm * Dielectric constant (εr): * Capacitance (C): * F pF nF µF mF * Dielectric constant (ε0): * F/m Please fill in exactly three variables. Calculate Parallel Plate Capacitor - Explanation -------------------------------------- A parallel plate capacitor is a type of capacitor consisting of two conductive plates separated by a dielectric material. The capacitance \( C \) of such a capacitor is determined by the following formula: \[ C = \frac{\varepsilon\_0 \varepsilon\_r A}{d} \] Where: * \( C \) is the capacitance (in Farads, \(F\)), * \( \varepsilon\_0 \) is the dielectric constant of vacuum (\( 8.854 \times 10^{-12} \, \text{F/m} \)), * \( \varepsilon\_r \) is the relative dielectric constant of the material between the plates, * \( A \) is the area of one of the plates (in square meters, \( m^2 \)), and * \( d \) is the distance between the plates (in meters, \( m \)). Purpose of the Calculator ------------------------- This calculator can solve for any of the four variables \( C \), \( A \), \( d \), or \( \varepsilon\_r \) when you provide the other three. It uses the following rearranged formulas to solve for each missing variable. * To solve for \( C \) (capacitance): \[ C = \frac{\varepsilon\_0 \varepsilon\_r A}{d} \] * To solve for \( A \) (area): \[ A = \frac{C \cdot d}{\varepsilon\_0 \varepsilon\_r} \] * To solve for \( d \) (distance): \[ d = \frac{\varepsilon\_0 \varepsilon\_r A}{C} \] * To solve for \( \varepsilon\_r \) (relative dielectric constant): \[ \varepsilon\_r = \frac{C \cdot d}{A \cdot \varepsilon\_0} \] The capacitance found here can be used directly in the [RC time constant](https://blog.hirnschall.net/tools/rc-time-constant/), [LC resonance frequency](https://blog.hirnschall.net/tools/lc-resonance-frequency/), or [capacitor energy](https://blog.hirnschall.net/tools/capacitor-stored-energy/) calculator. Related Tools ------------- * [Capacitor Energy Calculator](https://blog.hirnschall.net/tools/capacitor-stored-energy/) — compute energy stored with the C you just calculated. * [RC Time Constant Calculator](https://blog.hirnschall.net/tools/rc-time-constant/) — use the computed C in an RC circuit. * [LC Resonance Frequency Calculator](https://blog.hirnschall.net/tools/lc-resonance-frequency/) — use the computed C to set an LC resonant frequency. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [Gears for 3D Printing — Circular & Non-Circular Design](https://blog.hirnschall.net/gears-for-3d-printing/) meta description: Design circular and non-circular gears for FDM using a circular tooth profile — simpler than involute, uniform teeth on any gear outline. Includes PDF. date published: 30.05.2021 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [Drawing circular and non-circular gears for FDM 3d-printing](https://blog.hirnschall.net/gears-for-3d-printing/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Design circular and non-circular gears for FDM using a circular tooth profile — simpler than involute, uniform teeth on any gear outline. Includes PDF. meta title: Gears for 3D Printing — Circular & Non-Circular Design date published: 30.05.2021 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- Gear profile ------------ Under normal circumstances, involute gears are the way to go as they offer many advantages when compared to other gear profiles. However, if non-circular gears are required drawing an involute gear profile can become complicated as not all teeth are the same size. As FDM 3d printed parts are (in general) not as strong as metal parts and therefore torque and transmission efficiency is not as important, we can try to use a different gear profile. If we use circular teeth we ensure that all teeth have the same size regardless of the gear outline. ![Circular gear profile](https://blog.hirnschall.net/gears-for-3d-printing/resources/img/gear-profile.png) Figure 1: Circular gear profile This gear profile is therefore very easy to construct, as you can see in fig. 2 below. ![How to draw a circular gear](https://blog.hirnschall.net/gears-for-3d-printing/resources/img/drawing-a-gear.png) Figure 2: How to construct a circular gear Calculating the correct tooth size ---------------------------------- To calculate the required tooth size \(r\) we have to solve a simple system of equations using a computer algebra program like maple for example. As the required system of equations is dependant on the gear shape, please take a look at the linked PDF paper for more details on how to assemble it. Examples included in the PDF ---------------------------- The linked PDF paper contains several detailed examples of how to calculate both circular and non circular gears using the proposed gear profile. Listed below are the results of all included examples: ### Circular gears with given gear ratio and centerdistance ![Two circular gears with given center distance](https://blog.hirnschall.net/gears-for-3d-printing/resources/img/ex1.png) Figure 3: Circular gears with given gear ratio and centerdistance ### Internal ring gear ![Two circular gears with given center distance](https://blog.hirnschall.net/gears-for-3d-printing/resources/img/ex2.png) Figure 3: Internal ring gear ### Planetary gearbox (epicyclic gear train) ![Two circular gears with given center distance](https://blog.hirnschall.net/gears-for-3d-printing/resources/img/ex3.png) Figure 3: Planetary gearbox (epicyclic gear train) ### Rectangular/square gear with given side length ![Two circular gears with given center distance](https://blog.hirnschall.net/gears-for-3d-printing/resources/img/ex4-1.png) (a) Corners are not teeth ![Two circular gears with given center distance](https://blog.hirnschall.net/gears-for-3d-printing/resources/img/ex4-2.png) (b) Corners are teeth Figure 4: Rectangular/square gear with given side length \(r=22/12\) ![Two circular gears with given center distance](https://blog.hirnschall.net/gears-for-3d-printing/resources/img/ex4-3.png) (a) Corners are not teeth ![Two circular gears with given center distance](https://blog.hirnschall.net/gears-for-3d-printing/resources/img/ex4-4.png) (b) Corners are teeth Figure 5: Rectangular/square gear with given side length \(r=22/24\) ### Rectangular and circular gears with given gear ratio and center-distance ![Two circular gears with given center distance](https://blog.hirnschall.net/gears-for-3d-printing/resources/img/ex5.png) Figure 6: Rectangular and circular gears with given gear ratio and center-distance ### Elliptical gear with given size and number of teeth ![Two circular gears with given center distance](https://blog.hirnschall.net/gears-for-3d-printing/resources/img/ex6-1.png) (a) ![Two circular gears with given center distance](https://blog.hirnschall.net/gears-for-3d-printing/resources/img/ex6-2.png) (b) Figure 7: Elliptical gear with given size and number of teeth ### Elliptical gear and square gear that fit together ![Two circular gears with given center distance](https://blog.hirnschall.net/gears-for-3d-printing/resources/img/ex7.png) Figure 8: Elliptical gear and square gear that fit together --- ## [Inductor Energy Calculator — Solve for E, L, or I](https://blog.hirnschall.net/tools/inductor-stored-energy/) meta description: Calculate the energy stored in an inductor. Solve for energy, inductance, or current given the other two. Flux linkage shown as additional output. date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- # [Inductor Energy Calculator](https://blog.hirnschall.net/tools/inductor-stored-energy/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate the energy stored in an inductor. Solve for energy, inductance, or current given the other two. Flux linkage shown as additional output. meta title: Inductor Energy Calculator — Solve for E, L, or I date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- Calculator ---------- This calculator solves for energy \( E \), inductance \( L \), or current \( I \) when you provide the other two. The flux linkage \( \lambda \) is always shown as an additional output. * Inductance (L): * H mH µH nH * Current (I): * A mA * Energy (E): * J mJ µJ kJ Please fill in exactly two variables. Calculate Energy Stored in an Inductor — Explanation ------------------------------------------ When current flows through an inductor, a magnetic field builds up around the windings. The energy required to establish that field is stored within it and returned to the circuit when the current falls. The amount of energy stored depends on both the inductance and the current: \[ E = \frac{1}{2} L I^2 \] Where: * \( E \) is the stored energy (in Joules, \( \text{J} \)), * \( L \) is the inductance (in Henrys, \( \text{H} \)), * \( I \) is the current through the inductor (in Amperes, \( \text{A} \)). The \( I^2 \) dependence is the key point: doubling the current quadruples the stored energy, just as doubling the voltage quadruples the energy in a capacitor. In practice this matters most in switching power supplies, where the inductor in a boost or buck converter stores energy during one phase of the switching cycle and releases it during the next. The peak current through the inductor sets how much energy is available per cycle, and therefore the maximum power the converter can deliver. The flux linkage \( \lambda \) is the magnetic analogue of stored charge in a capacitor. It is defined as: \[ \lambda = L \cdot I \] and has units of Weber (Wb). Substituting into the energy formula gives two equivalent forms: \[ E = \frac{\lambda^2}{2L} = \frac{1}{2} \lambda I \] These are useful when flux linkage rather than current is the known quantity, as in some motor and transformer analyses. Purpose of the Calculator ------------------------- Given any two of \( E \), \( L \), and \( I \), the calculator solves for the missing one. The rearranged formulas are: * To solve for \( E \) (checking how much energy a known inductor carries at a given current): \[ E = \frac{1}{2} L I^2 \] * To solve for \( L \) (sizing an inductor to store a target energy at a given peak current): \[ L = \frac{2E}{I^2} \] * To solve for \( I \) (finding the current needed to store a target energy in a known inductor): \[ I = \sqrt{\frac{2E}{L}} \] In all cases the flux linkage \( \lambda = L \cdot I \) is shown as an additional output. If the inductance comes from a combination of inductors, compute the equivalent value first with the [inductors in series](https://blog.hirnschall.net/tools/inductors-in-series/) or [inductors in parallel](https://blog.hirnschall.net/tools/inductors-in-parallel/) calculator. Note on saturation: the formula assumes a linear, non-saturating core. Real inductors have a saturation current above which the effective inductance drops sharply, and beyond that point the stored energy no longer follows \( \frac{1}{2}LI^2 \) with the nominal \( L \). Always check the datasheet saturation current rating when operating near peak current. Comparison with Capacitor Energy -------------------------------- Inductors and capacitors both store energy, but in different fields and with different governing variables. The structural parallel is exact: | | Inductor | Capacitor | | --- | --- | --- | | Stores | magnetic field energy | electric field energy | | Driving variable | \( I \) (current) | \( V \) (voltage) | | Energy formula | \( \tfrac{1}{2}LI^2 \) | \( \tfrac{1}{2}CV^2 \) | | Charge analogue | \( \lambda = LI \) (flux linkage, Wb) | \( Q = CV \) (charge, C) | See the [capacitor energy calculator](https://blog.hirnschall.net/tools/capacitor-stored-energy/) for the capacitive equivalent. Related Tools ------------- * [Capacitor Energy Calculator](https://blog.hirnschall.net/tools/capacitor-stored-energy/) — the capacitive equivalent: \( E = \frac{1}{2}CV^2 \). * [Inductor Impedance Calculator](https://blog.hirnschall.net/tools/inductor-impedance/) — find the inductive reactance \( X\_L \) and impedance at a given frequency. * [RL Time Constant Calculator](https://blog.hirnschall.net/tools/rl-time-constant/) — how quickly current builds up in an RL circuit after a voltage step. * [Inductors in Series](https://blog.hirnschall.net/tools/inductors-in-series/) / [Inductors in Parallel](https://blog.hirnschall.net/tools/inductors-in-parallel/) — compute the equivalent inductance of a network. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [Schering Bridge Calculator — Capacitance, ESR, and Dissipation Factor](https://blog.hirnschall.net/tools/schering-bridge/) meta description: Calculate unknown capacitance and ESR from a balanced Schering bridge. Enter C2, R3, R4, C4 at null; C_x, R_x, dissipation factor, and loss angle are computed. date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- # [Schering Bridge Calculator](https://blog.hirnschall.net/tools/schering-bridge/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate unknown capacitance and ESR from a balanced Schering bridge. Enter C2, R3, R4, C4 at null; C_x, R_x, dissipation factor, and loss angle are computed. meta title: Schering Bridge Calculator — Capacitance, ESR, and Dissipation Factor date published: 11.05.2026 (DD.MM.YYYY format) date last modified: 11.05.2026 (DD.MM.YYYY format) --- Calculator ---------- Enter the four bridge components at balance. The unknown capacitance \( C\_x \) and its series resistance \( R\_x \) (ESR) are calculated from the balance conditions. Providing the test frequency also yields the dissipation factor \( D \) and quality factor \( Q \). * C2 (standard capacitor): * µF nF pF * R3 (ratio arm): * Ω kΩ MΩ * R4 (standard arm, variable): * Ω kΩ MΩ * C4 (standard arm, parallel with R4): * µF nF pF --- * Test frequency (f) — optional: * Hz kHz Enter all four bridge components to calculate. Calculate Schering Bridge — Explanation ----------------------------- The Schering bridge is an AC bridge circuit used to measure the capacitance and loss of an unknown capacitor. It places the unknown capacitor \( C\_x \) with its equivalent series resistance \( R\_x \) (ESR) in one arm, balances it against a lossless standard capacitor \( C\_2 \) in the adjacent arm, and reads off \( C\_x \) and \( R\_x \) from the values of the other two bridge components at null. It is widely used in high-voltage testing of cables, bushings, and insulating materials, where the dissipation factor is a key indicator of dielectric quality. The bridge has four arms. The unknown arm contains \( C\_x \) in series with \( R\_x \). The opposite arm contains the standard capacitor \( C\_2 \), which is assumed lossless. The two remaining arms are \( R\_3 \) (a ratio resistor) and the standard arm \( R\_4 \) in parallel with \( C\_4 \). An AC source drives one diagonal; a null detector monitors the other. Balance Conditions ------------------ Setting the complex impedance products of opposite arm pairs equal and separating real and imaginary parts gives: \[ C\_x = C\_2 \cdot \frac{R\_4}{R\_3} \] \[ R\_x = R\_3 \cdot \frac{C\_4}{C\_2} \] Like the Maxwell bridge, these conditions are independent of frequency — the source frequency does not need to be known precisely to measure \( C\_x \) and \( R\_x \). The dissipation factor, however, does depend on frequency: \[ D = \tan\delta = \omega C\_x R\_x = \omega R\_4 C\_4 \] where \( \omega = 2\pi f \). \( D \) can be read directly from the standard arm components once the bridge is balanced, provided the test frequency is known. The quality factor \( Q = 1/D \) is the inverse of the dissipation factor. For a good-quality capacitor — film, ceramic, or mica types — \( D \) is very small (below 0.01), meaning \( Q \) is large. Electrolytic capacitors have much higher losses, with \( D \) values of 0.05 to 0.2 or more at low frequencies. Dissipation Factor and Loss Angle --------------------------------- The dissipation factor \( D = \tan\delta \) describes the fraction of energy dissipated per cycle relative to the energy stored. The loss angle \( \delta \) is the complement of the phase angle: a perfect capacitor has \( \delta = 0° \) (current leads voltage by exactly 90°); a real capacitor with some ESR has \( \delta > 0° \), meaning the current leads by slightly less than 90°. The relationship is: \[ \delta = \arctan(D) = \arctan(\omega C\_x R\_x) \] In high-voltage insulation testing, \( \tan\delta \) (often written as the power factor for small angles where \( \tan\delta \approx \sin\delta \)) is the primary diagnostic quantity. A rising \( \tan\delta \) over time or with voltage indicates degrading insulation. Purpose of the Calculator ------------------------- In a physical measurement, the bridge is brought to null by adjusting \( R\_4 \) (which controls the capacitive balance, setting \( C\_x \)) and \( C\_4 \) (which controls the resistive balance, setting \( R\_x \) and \( D \)). Once the detector reads zero, those component values are entered here to recover \( C\_x \), \( R\_x \), \( D \), \( Q \), and \( \delta \). Frequency is only required for the dissipation factor and loss angle; \( C\_x \) and \( R\_x \) come from the component ratios alone. For component selection, the formulas rearrange to: \( R\_3 = C\_2 R\_4 / C\_x \) and \( C\_4 = C\_2 R\_x / R\_3 \). Choosing \( C\_2 \) and \( R\_3 \) to match the expected range of \( C\_x \) keeps \( R\_4 \) in a convenient adjustment range. Related Tools ------------- * [Maxwell Bridge Calculator](https://blog.hirnschall.net/tools/maxwell-bridge/) — the inductive equivalent: measures unknown inductance and series resistance. * [Wheatstone Bridge Calculator](https://blog.hirnschall.net/tools/wheatstone-bridge/) — DC bridge for measuring unknown resistance. * [Capacitor Impedance Calculator](https://blog.hirnschall.net/tools/capacitor-impedance/) — compute \( X\_C \) and \( |Z| \) from the measured \( C\_x \) and \( R\_x \). * [Capacitor Energy Calculator](https://blog.hirnschall.net/tools/capacitor-stored-energy/) — compute the energy stored in the measured capacitor at a given voltage. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [LC Resonance Frequency Calculator — Solve for f, L, or C](https://blog.hirnschall.net/tools/lc-resonance-frequency/) meta description: Calculate the resonant frequency of an LC circuit. Solve for frequency, inductance, or capacitance given the other two. Angular frequency shown as additional output. date published: 01.04.2025 (DD.MM.YYYY format) date last modified: 22.04.2025 (DD.MM.YYYY format) --- # [LC Resonance Frequency Calculator](https://blog.hirnschall.net/tools/lc-resonance-frequency/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Calculate the resonant frequency of an LC circuit. Solve for frequency, inductance, or capacitance given the other two. Angular frequency shown as additional output. meta title: LC Resonance Frequency Calculator — Solve for f, L, or C date published: 01.04.2025 (DD.MM.YYYY format) date last modified: 22.04.2025 (DD.MM.YYYY format) --- Calculator ---------- Provide any two of \( f\_0 \), \( L \), \( C \) — the third is solved. The angular resonant frequency \( \omega\_0 \) is always shown as an additional output. * Resonant frequency (f0): * Hz kHz MHz * Inductance (L): * H mH µH nH * Capacitance (C): * F mF µF nF pF Provide any two of f₀, L, C to calculate. Calculate LC Resonant Frequency — Explanation ----------------------------------- An LC circuit consists of an inductor \( L \) and a capacitor \( C \) connected together. At the resonant frequency, the energy oscillates between the electric field of the capacitor and the magnetic field of the inductor — this is also the frequency where capacitive reactance \( X\_C \) equals inductive reactance \( X\_L \) (see the [capacitor impedance calculator](https://blog.hirnschall.net/tools/capacitor-impedance/) to compute \( X\_C \) at any frequency). The resonant frequency is: \[ f\_0 = \frac{1}{2\pi\sqrt{LC}} \] Where: * \( f\_0 \) is the resonant frequency (in Hertz, \( \text{Hz} \)), * \( L \) is the inductance (in Henrys, \( \text{H} \)), * \( C \) is the capacitance (in Farads, \( \text{F} \)). Purpose of the Calculator ------------------------- Given any two of the three variables, the calculator solves for the third. The rearranged formulas are: * To solve for \( f\_0 \): \[ f\_0 = \frac{1}{2\pi\sqrt{LC}} \] * To solve for \( L \): \[ L = \frac{1}{(2\pi f\_0)^2 \cdot C} \] * To solve for \( C \): \[ C = \frac{1}{(2\pi f\_0)^2 \cdot L} \] If the required C is not a standard component value, combine capacitors to reach it using the [capacitors in series](https://blog.hirnschall.net/tools/capacitors-in-series/) or [capacitors in parallel](https://blog.hirnschall.net/tools/capacitors-in-parallel/) calculator. Angular Resonant Frequency -------------------------- The angular frequency \( \omega\_0 \) is often more convenient in circuit analysis and filter design: \[ \omega\_0 = 2\pi f\_0 = \frac{1}{\sqrt{LC}} \] It is expressed in radians per second (rad/s). Many filter and impedance formulas use \( \omega\_0 \) directly, avoiding the repeated \( 2\pi \) factor. Applications ------------ LC circuits are used as tuned filters, oscillators, and impedance matching networks. In radio receivers, a variable capacitor is tuned to set \( f\_0 \) equal to the desired station frequency. In switching power supplies, the LC output filter is designed so that \( f\_0 \) is well below the switching frequency, attenuating the ripple. In RF design, LC tanks set the operating frequency of oscillators and amplifiers. Related Tools ------------- * [Capacitor Impedance Calculator](https://blog.hirnschall.net/tools/capacitor-impedance/) — compute \( X\_C \) at any frequency; at resonance \( X\_C = X\_L \). * [Capacitors in Series](https://blog.hirnschall.net/tools/capacitors-in-series/) / [Capacitors in Parallel](https://blog.hirnschall.net/tools/capacitors-in-parallel/) — combine capacitors to reach a non-standard C for a target frequency. * [Inductor Impedance Calculator](https://blog.hirnschall.net/tools/inductor-impedance/) — compute \( X\_L \) at any frequency; at resonance \( X\_L = X\_C \). * [Inductors in Series](https://blog.hirnschall.net/tools/inductors-in-series/) / [Inductors in Parallel](https://blog.hirnschall.net/tools/inductors-in-parallel/) — combine inductors to reach a non-standard L for a target frequency. More calculators: [blog.hirnschall.net/tools/](https://blog.hirnschall.net/tools/). --- ## [DIY Motorized Camera Slider — WiFi Control, Under $100](https://blog.hirnschall.net/diy-motorized-slider/) meta description: Build a WiFi-controlled motorized DSLR camera slider for under $100. Combines 3D-printed parts with metal rails — full BOM, wiring, and control code. date published: 02.08.2022 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [Best DIY motorized DSLR slider (with WiFi)](https://blog.hirnschall.net/diy-motorized-slider/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Build a WiFi-controlled motorized DSLR camera slider for under $100. Combines 3D-printed parts with metal rails — full BOM, wiring, and control code. meta title: DIY Motorized Camera Slider — WiFi Control, Under $100 date published: 02.08.2022 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- Introduction ------------ Today we are going to design and build the best motorized DIY camera slider out there. Including all the bells and whistles like wifi, programmability, and dual moving gantries for an extra compact form factor. If you are not interested in the design process you can skip to the assembly guide and required parts list here. Must-have Features ------------------ As always, let's start with a list of must-have features before designing the required parts: ### Timelaps capability (motorized) To capture stunning hyperlapses (moving timelapses), the slider has to have a motor and the ability to control the camera, the number of photos, movement of the camera, etc. To set all these parameters, we will use a web app on our phone (via wifi). ### WiFi For me, the goto microcontroller with wifi capability has to be [the esp8266 12f module](https://blog.hirnschall.net/esp8266/). It is small, cheap, has a lot of memory, and is easy to work with. If it has enough IO and memory it should be a great fit. ### Small form factor A traditional slider can move the camera as far as it is long. However, using two moving gantries (like the edelkrone slider for example), one for the camera and one for the tripod mount, the camera can move almost twice as far as the slider length. Using this design the slider can be much smaller and even fit into a backpack. One downside of this construction is that the tripod mount gantry has to be very stiff to not flex under the weight of the camera (as the slider acts as a lever). We will use a piece of 70mmx4mm aluminum profile to avoid this problem. ### Arduino compatible Using an esp8266 we can use the Arduino programming language and the Arduino ide, making this project more accessible. Programming can also be done using an Arduino [as described here](https://blog.hirnschall.net/esp8266/). ### Upgradability I think it would be nice to be able to add a 3 axis gimbal in the future. The slider should be able to handle the added weight and I also want to control the gimbal via the web app. However, this is not a top priority feature and might not be feasible. Design considerations (what is important) ----------------------------------------- ### Guiderails Since we will have two moving gantries I think it is best to use 2020 v-slot aluminum extrusions. They are cheap and the two gantries can easily be mounted using v-slot rollers. ### Sag The number one error I have seen on other slider projects is sag. If the chosen guide rails are too weak the slider is unusable as the camera will not move on a straight line. However, as we will be using short 2020 aluminum extrusions this is not a problem. ### Choosing a suitable Motor If we choose a motor with too little torque, we will not be able to use the slider at an angle. This is one of the mistakes I made last time. #### Calculating the required torque As you can see in fig. 2.1, the torque of a motor is given by \(T=r\cdot F\_T\) where r is the length of a lever connected to the motor shaft and \(F\_T\) is the force applied. Combined with fig. 2.2 and 2.3 it is easy to see how to calculate the required torque \(T = r\cdot F\_T=R\cdot F\_A\) F\_T is specified by on the datasheet for your motor and \(F\_A\) can be can be calculated using the weight of the slider including your camera \(G\) (\(F\_G=G\cdot9.81\)) and $$F\_A=\frac{\cos^{-1}(\varphi)}{F\_G}$$ where phi is the max angle you would like to use your slider at. You can see the connection between \(F\_A,F\_G\) and \(F\_N\) in fig. 2.3. As we are using bearings on the gantry we are not taking friction into account. Instead we will use a motor with a slightly higher torque rating than calculated here. So $$T>R\cdot F\_G.$$ #### Pancake Stepper Motor I would like to use a nema 17 pankace stepper motor as they are cheap, readily available and easy to work with. A stepper motor can also be used to position the camera accurately for moving timelapses. We have already seen how to calculate the required Torque. My camera weighs 480g, the slider 1446g (including the motor) and I would like to use the slider at an angle of up to 90deg. Lets assume a maximum weight of 2kg. Thus I have to choose a motor with at least 0.157Nm of torque. Unfortionately a standard nema 17 pancake motor with 0.13Nm torque is not sufficient. This is because the rated torque is the holding torque of the motor. The actual torque while moving is much lower. Thus the motor might be able to hold the slider in place but it would not be able to move it. To circumvent this problem we'll use a 40:1 worm gear to drive the belt connecting both gantries. This might be a problem for filming as we reduce the maximum speed the camera can move at by a factor of 40, but as I mainly want to capture hyperlapses this is not a problem for me. #### Gearing As a generic nema 17 pancake stepper has not enough torque for our purposes, we will use a gearbox to trade rotational speed for torque. For moving timelapses it is probably best to use a worm gear with a \(40:1\) gear ratio. In contrast to e.g. a planetary gearbox, a worm gear cannot be back-driven. Thus the motor can be disabled when the camera is not moving, saving power. Let's now calculate the torque this system can deliver [1]: Assuming a machine efficiency \(\eta=0.3\), motor torque \(T\_M\) and rotational speed \(\omega\_M\) we can calculate the speed (\(\omega\_W\)) and torque (\(T\_W\)) when using the worm gear as follows: $$ T\_W = \eta\frac{40}{1}T\_M = 12T\_M $$ $$ \omega\_W = \frac{\omega\_M}{40} $$ ### Power supply As most people already own a power bank, I would like to use one to power the slider if possible. Let's first check if this is a feasible approach. #### Calculating Battery power Let's say the power bank can supply 2.4A at 5V using 1 USB port and 3A when both USB ports are used simultaneously. As we want to use a stepper motor at 12V we will also have to use a boost converter for which we assume an efficiency of about \(75-95\%\). Thus the power bank can supply $$ A^{(1)}\_{out}=2.4\*5/12\*0.75 = 0.75A $$ at \(12V\) using one USB port and $$ A^{(2)}\_{out}=3\*5/12\*0.75 = 0.93A $$ at \(12V\) using both USB ports at once. Using a lab bench power supply we can confirm that the provided power is sufficient to run our motor. #### Calculating Battery Runtime The power bank I am using is rated for \(20000mAh\). This means it has enough capacity to supply \(20A\) for one hour. As we can only draw a current of up to \(2.4A\) from the power bank, it will last for \(8.33\) hours. This should be more than enough for our purposes. ### Electronics (custom PCB) Designing a pcb to fit our needs is not very interesting so here are the results [(available on github)](https://github.com/shirnschall/hs-slider-project): ![custom slider controller pcb schematic](https://blog.hirnschall.net/diy-motorized-slider/resources/img/pcb-schematic.jpg) (a) Slider-Controller Schematic ![custom slider controller pcb board](https://blog.hirnschall.net/diy-motorized-slider/resources/img/pcb-board.jpg) (b) Slider-Controller Board Figure 1: Custom Slider-Controller PCB NOTE: add a 16V 1000uF capacitor to the 5V to 12V boost converter. You can also add one to the input. ### 3d printed parts To make this slider actually usable in day-to-day work, we have to combine 3d printed parts where possible with metal parts where required. This is especially noticable on the outer moving gantry. Here a fully 3d printed part is to flexible and cannot support the weight of the slider with attached dlsr. We therefore have to use a thick 6mm aluminium plate in combination with 3d printed parts to get teh best of both worlds. All required 3d models are available for free at [printables.com](https://www.printables.com/model/251563-motorized-camera-slider-with-wifi) with the technical drawings for the metal plate available on [GitHub](https://github.com/shirnschall/hs-slider-project). Building your own Slider ------------------------ To build your own slider you will need the following parts: As some components cannot be puchased individualy online you can try to buy them at a local electronics reseller. However, as these are all commonly used parts you can also keep them for later projects. | Component | Quantity | Where to buy | | --- | --- | --- | | [3d printed parts set](https://www.printables.com/model/251563-motorized-camera-slider-with-wifi) | × 1 | [Printables.com](https://www.printables.com/model/251563-motorized-camera-slider-with-wifi) | | [Custom PCB](https://github.com/shirnschall/hs-slider-project/blob/master/pcb-gerber-files/2488982E_Y12.rar) | × 1 | * [Gerber Files](https://github.com/shirnschall/hs-slider-project/blob/master/pcb-gerber-files/2488982E_Y12.rar) * [JLCPCB.com](https://cart.jlcpcb.com/quote) | | [esp8266 12F](https://amzn.to/3BysY3q) | × 1 | * [Amazon.com](https://amzn.to/3BysY3q) * [Amazon.de](https://www.amazon.de/gp/product/B07DBC7KWN/ref=as_li_tl?ie=UTF8&tag=bloghirnschal-21&camp=1638&creative=6742&linkCode=as2&creativeASIN=B07DBC7KWN&linkId=081fbb3bc96753cce6325506821beb93) | | [5V to 3.3V step-down converter](https://www.amazon.com/gp/product/B07FSLGPR8/ref=as_li_tl?ie=UTF8&tag=bloghirnsch0b-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B07FSLGPR8&linkId=ac000a2f4948cc4f2f1dfa7037a9c289) | × 1 | * [Amazon.com](https://www.amazon.com/gp/product/B07FSLGPR8/ref=as_li_tl?ie=UTF8&tag=bloghirnsch0b-20&camp=1789&creative=9325&linkCode=as2&creativeASIN=B07FSLGPR8&linkId=ac000a2f4948cc4f2f1dfa7037a9c289) * [Amazon.de](https://amzn.to/3cSG6Gd) | | [5V to 12V boost converter](https://amzn.to/3Sxd5R5) | × 1 | * [Amazon.com](https://amzn.to/3Sxd5R5) * [Amazon.de](https://amzn.to/3PSBQVZ) | | [16V 1000uF Capacitor](https://amzn.to/3cTtVJj) | × 2 | * [Amazon.com](https://amzn.to/3cTtVJj) * [Amazon.de](https://amzn.to/3ShQ5VT) | | [a4988 stepper driver](https://amzn.to/3d4LrKY) | × 1 | * [Amazon.com](https://amzn.to/3d4LrKY) * [Amazon.de](https://amzn.to/3JmBlkD) | | 5V relay (Tianbo TR5V M-S-Z 05VDC) | × 1 | | | [5V transistor (bc337)](https://amzn.to/3OSFcXL) | × 1 | * [Amazon.com](https://amzn.to/3OSFcXL) * [Amazon.de](https://amzn.to/3PTlP29) | | [10k resistor](https://amzn.to/3zqqBNr) | × 2 | * [Amazon.com](https://amzn.to/3zqqBNr) * [Amazon.de](https://amzn.to/3cOpvTY) | | [USB Type-B port](https://amzn.to/3vxTwOA) | × 2 | * [Amazon.com](https://amzn.to/3vxTwOA) * [Amazon.de](https://amzn.to/3PRUHjS) | | [2.5mm 3pin port (pj208)](https://amzn.to/3PS5MBJ) | × 1 | * [Amazon.com](https://amzn.to/3PS5MBJ) * [Amazon.de](https://amzn.to/3QgSXRg) | | [pancake Nema17 stepper motor](https://blog.hirnschall.net/diy-motorized-slider/) | × 1 | * [Amazon.com](https://amzn.to/3QgMLZ9) * [Amazon.de](https://amzn.to/3vuhyKc) | | 40:1 worm gear set (5mm bore) | × 1 | * ebay * [Amazon.de](https://amzn.to/3zP9Dtp) | | [5mm×100mm shaft](https://amzn.to/3bmwljg) | × 1 | * [Amazon.com](https://amzn.to/3bmwljg) * [Amazon.de](https://amzn.to/3oOruL2) | | [685ZZ 5x11x5mm bearing](https://amzn.to/3Pj2LZK) | × 2 | * [Amazon.com](https://amzn.to/3Pj2LZK) * [Amazon.de](https://amzn.to/3C3Zs5R) | | [micro limit switch (endstop)](https://amzn.to/3PkHjDT) | × 1 | * [Amazon.com](https://amzn.to/3PkHjDT) * [Amazon.de](https://amzn.to/3JM0Dc6) | | 2020 V-Slot extrusions (I used 500mm) | × 2 | | | [V-Slot roller](https://amzn.to/3vuGZv1) | × 12 | * [Amazon.com](https://amzn.to/3vuGZv1) * [Amazon.de](https://amzn.to/3bmxkQG) | | m5x25 din912 screw | × 12 | | | m5 thin nut | × 12 | | | m5 washer | × 18 | | | m3x12 din912 screw | × 4 | | | m3x6 din912 screw | × 14 | | | m3x8 din912 screw | × 22 | | | m3 nut | × 4 | | | [m3 t-slot nut](https://amzn.to/3BAnCVd) | × 16 | * [Amazon.com](https://amzn.to/3BAnCVd) * [Amazon.de](https://amzn.to/3zsU8Ga) | | [m3x5.7 threaded insert](https://amzn.to/3zQEGFf) | × 8 | * [Amazon.com](https://amzn.to/3zQEGFf) * [Amazon.de](https://amzn.to/3bnasR1) | | gt2 timing belt | × 1 | | | belt pulley | × 1 | | | belt idler (5mm bore) | × 1 | | | #CommissionsEarned | | | ### Cost Although not all parts can be purchased individually, we will only consider the cost of components used in this build. Leftover parts can be used in other projects. ### Assembly Guide (Video) Résumé ------ All in all, I am very happy with how this project turned out. It is not only much cheaper than commercially available sliders, but it is also so good that I am happy using it over an off-the-shelf slider (even if money was no object). One thing I noticed, however, is that the compact design using two moving gantries requires a very sturdy tripod which is kind of a downside. Of course, the slider would be twice the size otherwise. --- ## [Flow Field Simulation in JS — Particles & Perlin Noise](https://blog.hirnschall.net/flow-simulation-js/) meta description: Simulate particle flow fields in JavaScript using Perlin noise to drive particle direction. Annotated p5.js source to build visualizations like this one. date published: 24.07.2020 (DD.MM.YYYY format) date last modified: 21.03.2026 (DD.MM.YYYY format) --- # [Coding Challenge #2: Flowfield Simulation](https://blog.hirnschall.net/flow-simulation-js/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Simulate particle flow fields in JavaScript using Perlin noise to drive particle direction. Annotated p5.js source to build visualizations like this one. meta title: Flow Field Simulation in JS — Particles & Perlin Noise date published: 24.07.2020 (DD.MM.YYYY format) date last modified: 21.03.2026 (DD.MM.YYYY format) --- This week we'll simulate the flow of particles through a vectorfield. As always we'll use p5js as it makes drawing and animating easy. If you are not yet comfortable with classes in javascript, you can take a look at our last [Coding Challenge #2](https://blog.hirnschall.net/maze-generation-js/) where we took a look at the basics. If you are interested in the mathematical background of this weeks challenge make sure to check out the Mathematical Background section below. How flow simulation works ------------------------- Ok, so we want to simulate the flow of particles through a vecotfield. But what is a Vectorfield? A vectorfield maps each point \((x,y)\) to a vector. To simulate the flow of particles we'll do the following: * place a particle inside the vectorfield * map the particles position \((x,y)\) to a vector (using the vectorfield) * move the particle in the direction the vector points in While this might sound difficult it is actually quite easy. Lets take a quick look at how to generate a vectorfield before we start with the code. If we assign a random vector to each point \((x,y)\) the resulting "flow" will look very bad. What we want is a more like a river where the direction of flow cannot change instantaneous. We can do this by using perlin noise. If you are not familiar with perlin noise take a look at [this article I wrote](https://blog.hirnschall.net/perlin-noise/). In short, perlin noise is a more controlled form of randomness. So, the plan is to use 2d perlin nosie to generate a random number between \(0\) and \(1\) based on the position \((x,y)\) of the particle and then map the result to a value from \(0\) to \(360\). We can interpret this value as the direction of the vector (angle). Now that that's out of the way lets take a look at the implementation: Getting started with p5js ------------------------- ### Particle class As we want multiple particles we'll start with particle class that.. * can keep track of its position * has a memberfunction to update its position * has a memberfunction to draw itself ``` function Particle(x, y) { this.x = x; this.y = y; this.v = [-1,0]; this.show = function () { fill(200) ellipse(this.x,this.y,5,5); } this.update = function(){ this.v=[cos(noise(this.x/s,this.y/s)*TWO_PI),sin(noise(this.x/s,this.y/s)*TWO_PI)]; this.x += this.v[0]*particlespeed; this.y += this.v[1]*particlespeed; //check if particle has reached left end if(this.x<-10){ this.x=canvasWidth+10; this.y=random(canvasHeight); } } } ``` The show function is very simple so lets start with the update function. To map the noise value to a direction we multiply it by \(2\pi\) which is \(360\) degrees in radians. We then take the cosine to get the \(x\)-value and the sine to get the \(y\)-value of a vector with length one that is pointing in the desired direction. We can then add this "velocity" vector to the current position. If the particle reaches the left end of the canvas we will reset its position. ### Draw function All that is left to do now is to add the particles. We'll do so every 10 frames at a random \(y\)-position between \(0\) and the height of the canvas and at the very right edge of the canvas. As we need to update and display the particles later we store them in an array called "particles": ``` function draw(){ if(particlesc < maxparticles && frameCount%10==0) particles[particlesc++] = new Particle(canvasWidth+10,random(canvasHeight)); ``` To update and display the particles we use ``` for(var i=0;i #include const char* ssid = "your-ssid"; const char* password = "your-password"; void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nConnected"); } void loop() { if (WiFi.status() == WL_CONNECTED) { HTTPClient http; http.begin("http://jsonplaceholder.typicode.com/posts/1"); int httpCode = http.GET(); if (httpCode > 0) { Serial.printf("HTTP %d\n", httpCode); if (httpCode == HTTP_CODE_OK) { String payload = http.getString(); Serial.println(payload); } } else { Serial.printf("Request failed: %s\n", HTTPClient::errorToString(httpCode).c_str()); } http.end(); } delay(10000); } ``` The `HTTPClient` object is declared inside `loop()` so it is constructed and destroyed on every iteration. This is intentional for simplicity in a single-request sketch; for repeated requests to the same host, see the connection reuse section. The WiFi credentials are hardcoded here to keep the example focused. For a deployed device we usually want to enter them at runtime instead, which is covered in [WiFi provisioning with a captive portal](https://blog.hirnschall.net/esp32-iot-wifi-provisioning/). HTTP POST Request ----------------- A POST request sends a body to the server along with the request. We can use it to submit sensor readings, trigger actions, or call an API that requires a payload. The two most common content types are URL-encoded form data and JSON. Let's take a look at both! ### Available Functions Everything from the GET section applies here as well. The additional functions specific to POST are listed below. * `int POST(String payload)` Sends the POST request with the given payload and returns the HTTP response code, or a negative error code on failure. Overloads also accept a `uint8_t*` byte buffer with a length argument, or a `Stream*`. * `int POST(uint8_t *payload, size_t size)` Same as above but takes a raw byte buffer and its size. Useful when the payload is not a String, e.g. when sending binary data. * `void addHeader(const String &name, const String &value)` We will use this function to set the Content-Type of the request body. Common values are "application/x-www-form-urlencoded" and "application/json". The server uses this to know how to parse the payload. ### POST with Form-Encoded Body Form encoding is the simplest way to send key-value pairs. The body looks exactly like a URL query string: `key1=value1&key2=value2`. Most web frameworks and many REST APIs accept this format without any additional configuration on the server side. ``` #include #include const char* ssid = "your-ssid"; const char* password = "your-password"; void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nConnected"); } void loop() { if (WiFi.status() == WL_CONNECTED) { HTTPClient http; http.begin("http://api.example.com/readings"); http.addHeader("Content-Type", "application/x-www-form-urlencoded"); float temperature = 23.4; float humidity = 61.0; String payload = "temperature=" + String(temperature) + "&humidity=" + String(humidity); int httpCode = http.POST(payload); if (httpCode > 0) { Serial.printf("HTTP %d\n", httpCode); } else { Serial.printf("Request failed: %s\n", HTTPClient::errorToString(httpCode).c_str()); } http.end(); } delay(10000); } ``` Note that if a value contains characters like `&` or `=`, they need to be percent-encoded before concatenation, since those characters are delimiters in the form encoding format. ### POST with JSON Body Sending JSON is the better choice when the payload has structure, when the server is a REST API that expects JSON, or when the response also needs to be parsed. We will use ArduinoJson to build the outgoing document and deserialise the response. The ArduinoJson `JsonDocument` acts as both the serialisation buffer and the deserialization target. This sounds a bit technical, but in practice it means that we will size it to cover both the outgoing and incoming documents. Alternatively, we can use the [ArduinoJson Assistant](https://arduinojson.org/v7/assistant/) as it gives exact sizes for a known schema. ### Full Example The example below sends a temperature and humidity reading as a JSON object, then parses the server's JSON response. The placeholder API at `jsonplaceholder.typicode.com` echoes back the POSTed body, which lets us verify the round-trip without a real server. ``` #include #include #include const char* ssid = "your-ssid"; const char* password = "your-password"; void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nConnected"); } void loop() { if (WiFi.status() == WL_CONNECTED) { HTTPClient http; http.begin("http://jsonplaceholder.typicode.com/posts"); http.addHeader("Content-Type", "application/json"); // Build the JSON payload JsonDocument doc; doc["temperature"] = 23.4; doc["humidity"] = 61.0; doc["sensor_id"] = "living_room"; String payload; serializeJson(doc, payload); int httpCode = http.POST(payload); if (httpCode > 0) { Serial.printf("HTTP %d\n", httpCode); if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_CREATED) { String response = http.getString(); // Parse the JSON response JsonDocument responseDoc; DeserializationError error = deserializeJson(responseDoc, response); if (!error) { const char* sensorId = responseDoc["sensor_id"]; float temp = responseDoc["temperature"]; Serial.printf("Echoed back: sensor=%s, temp=%.1f\n", sensorId, temp); } else { Serial.printf("JSON parse error: %s\n", error.c_str()); } } } else { Serial.printf("Request failed: %s\n", HTTPClient::errorToString(httpCode).c_str()); } http.end(); } delay(10000); } ``` Note that `HTTP_CODE_CREATED` (201) is the correct success code for a POST that creates a resource. Checking only for 200 will miss valid responses from many REST APIs. Reusing the Connection ---------------------- Every call to `http.begin()` without connection reuse enabled opens a fresh TCP connection and performs the full three-way handshake. For a single occasional request this is fine. For a sketch that POSTs a sensor reading frequently to the same host, the overhead can add up. Each handshake takes up to a few hundred milliseconds depending on network conditions. Calling `http.setReuse(true)` before `begin()` tells the library to send a `Connection: keep-alive` header and to leave the TCP socket open after `end()`. The next request to the same host reuses that socket directly. ``` #include #include const char* ssid = "your-ssid"; const char* password = "your-password"; HTTPClient http; // Declared outside loop() to persist across iterations void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\nConnected"); http.setReuse(true); } void loop() { if (WiFi.status() == WL_CONNECTED) { http.begin("http://api.example.com/readings"); int httpCode = http.GET(); if (httpCode > 0) { Serial.printf("HTTP %d\n", httpCode); if (httpCode == HTTP_CODE_OK) { Serial.println(http.getString()); } } else { Serial.printf("Request failed: %s\n", HTTPClient::errorToString(httpCode).c_str()); } http.end(); // Returns connection to pool, does not close socket } delay(5000); } ``` The `HTTPClient` object is declared at file scope so it survives across `loop()` iterations. `http.end()` still needs to be called after each request. It finalises the response, but with `setReuse(true)` it does not close the underlying socket. Note that the server can still choose to close the connection from its side, in which case the library will transparently reconnect on the next `begin()`. Connection reuse only helps when requests go to the same host. If the target URL changes between iterations, the library opens a new connection regardless. Practical Example: Writing to InfluxDB -------------------------------------- As a practical usage example for the HTTP POST request we will send sensor data to an InfluxDB instance. InfluxDB is a time-series database which means it is a optimized for storing sequences of measurements indexed by time. Unlike a "normal" (relational) database, every entry is automatically timestamped and queries are designed around time ranges, aggregations, and rates of change. TL;DR: Time-series DBs are very good to store sensor data from diffrent nodes without thinking about synchronisation etc. We can then use e.g. Grafana to display the data in a custom dashboard. InfluxDB is one of the most widely used time series databases and runs well on a Raspberry Pi. In this post however, we will not cover InfluxDB/Grafana setup and installation. ### Line Protocol InfluxDB accepts data in a format called line protocol. Each line represents one measurement at one point in time. The format is: ``` measurement[,tag_key=tag_value] field_key=field_value[,field_key=field_value] [timestamp] ``` The measurement name comes first, followed by optional tags, then fields, then an optional timestamp. Tags are indexed metadata. I.e. things like sensor ID or location that you want to filter or group by. Fields are the actual measured values. If the timestamp is omitted, InfluxDB uses the server's current time when the data arrives. In practice, we will generally omit the timestamp. A concrete example with two temperature readings and a door state would loop like this: ``` oven_sensors tc0=23.50,tc1=24.10,door0=0 ``` Multiple fields for the same measurement are separated by commas and sent in a single line. There is no schema to define upfront as fields are created automatically the first time they appear. From the ESP32 we can send line protocol data to InfluxDB using an HTTP POST request. The ESP32 Arduino core includes `HTTPClient.h` which handles this directly. No additional library is needed. ### InfluxDB v1 In InfluxDB v1 the write endpoint is `/write` with the database name as a query parameter. No authentication is required by default. ``` #include const char* INFLUXDB_HOST = "http://192.168.1.100:8086"; const char* INFLUXDB_DB = "sensors"; const char* INFLUXDB_MEASUREMENT = "oven_sensors"; void writeToInfluxDB(float tc0, float tc1, int door0) { HTTPClient http; String url = String(INFLUXDB_HOST) + "/write?db=" + INFLUXDB_DB; String data = String(INFLUXDB_MEASUREMENT); data += " door0=" + String(door0); data += ",tc0=" + String(tc0, 2); data += ",tc1=" + String(tc1, 2); http.begin(url); http.addHeader("Content-Type", "text/plain"); int httpCode = http.POST(data); if (httpCode == 204) { Serial.println("Data written successfully"); } else { Serial.print("Error: "); Serial.println(httpCode); } http.end(); } ``` As we can see, we just add the line protocol data for our example oven sensors as discussed above to the data variable before we use `http.POST(data)`. A successful write returns HTTP 204 (No Content). Any other status code indicates an error. We check for 204 explicitly rather than a generic success range. ### InfluxDB v2 InfluxDB v2 uses a different endpoint and replaces the database concept with organizations and buckets. Authentication uses a Bearer token passed in the request header. The line protocol payload is identical. ``` const char* INFLUXDB_HOST = "http://192.168.1.100:8086"; const char* INFLUXDB_ORG = "myorg"; const char* INFLUXDB_BUCKET = "sensors"; const char* INFLUXDB_TOKEN = "mytoken123"; void writeToInfluxDB(float tc0, float tc1, int door0) { HTTPClient http; String url = String(INFLUXDB_HOST) + "/api/v2/write?org=" + INFLUXDB_ORG + "&bucket=" + INFLUXDB_BUCKET + "&precision=s"; String data = String(INFLUXDB_MEASUREMENT); data += " door0=" + String(door0); data += ",tc0=" + String(tc0, 2); data += ",tc1=" + String(tc1, 2); http.begin(url); http.addHeader("Content-Type", "text/plain"); http.addHeader("Authorization", String("Token ") + INFLUXDB_TOKEN); int httpCode = http.POST(data); if (httpCode == 204) { Serial.println("Data written successfully"); } else { Serial.print("Error: "); Serial.println(httpCode); } http.end(); } ``` The three differences from v1 are the endpoint path (`/api/v2/write`), the query parameters (`org` and `bucket` instead of `db`), and the `Authorization` header. Everything else including the line protocol payload is unchanged. The token is hardcoded above for clarity. In production we keep it out of the source and load it from NVS at boot instead, using the [Preferences library](https://blog.hirnschall.net/esp32-preferences/). The same place the host and bucket can live so a single firmware image works across multiple deployments. Skipping Invalid Sensor Readings -------------------------------- Sensors can return NaN when they are disconnected or produce an invalid reading. Including a NaN value in a line protocol write will cause InfluxDB to reject the entire line. We can check each reading before appending it to the data string using the `isnan()` function. ``` String data = String(INFLUXDB_MEASUREMENT); data += " door0=" + String(door0); // digital, always valid if (!isnan(tc0)) data += ",tc0=" + String(tc0, 2); if (!isnan(tc1)) data += ",tc1=" + String(tc1, 2); if (!isnan(tc2)) data += ",tc2=" + String(tc2, 2); if (!isnan(tc3)) data += ",tc3=" + String(tc3, 2); ``` Note that at least one field must always be present in a line protocol write. In the example above `door0` is always included since it is a digital read that cannot be NaN. If all fields in a measurement could potentially be invalid, we need to check whether the data string has any fields before sending. Multiple Sensor Nodes --------------------- When multiple ESP32 nodes write to the same InfluxDB instance, we let the server timestamp every point on arrival rather than having each node send its own timestamp. This avoids synchronizing clocks across nodes, which would otherwise be necessary to keep their timestamps comparable. The tradeoff is absolute accuracy: on a slow link, or for a node that batches its writes, the server time can sit a few seconds after the measurement was actually taken. For the relative timing between nodes this is usually not a problem, since every point passes through the same server clock. We will use a tag to identify each node, which also lets Grafana plot them as separate series. ``` // tag identifies which node this data came from String data = String(INFLUXDB_MEASUREMENT) + ",node=oven1"; data += " tc0=" + String(tc0, 2); ``` The tag (`node=oven1`) comes immediately after the measurement name with no space, separated by a comma. In Grafana, grouping by the `node` tag gives a separate line per sensor node on the same chart. If the nodes have synchronized time via NTP, we can append a Unix timestamp to each line instead and InfluxDB will use that. This can be more accurate but requires NTP to be running reliably on each node. Guarding Against No Connection ------------------------------ Attempting an HTTP write when the network is not connected will block for the full timeout duration before failing. For an ESP32 connected over [Ethernet via a W5500 module](https://blog.hirnschall.net/esp32-w5500-ethernet/) we check the `eth_connected` flag. For WiFi we check `WiFi.status()`. ``` // Ethernet if (!eth_connected) { Serial.println("Not connected, skipping write"); return; } // WiFi if (WiFi.status() != WL_CONNECTED) { Serial.println("Not connected, skipping write"); return; } ``` Where & What to Buy ------------------- Below are links1 to the ESP modules and development boards I personally tend to use. If you are unsure about which one to choose, [check out this comparison post!](https://blog.hirnschall.net/esp8266-vs-esp32/) | ESP-12F | NodeMCU ESP8266 | ESP32-WROOM-32 | ESP32-S3 | | --- | --- | --- | --- | | [amazon.com](https://amzn.to/41YJ965) [amazon.de](https://amzn.to/3Olf3GC) | [amazon.com](https://amzn.to/4smbFcB) [amazon.de](https://amzn.to/4eeUhD6) | [amazon.com](https://amzn.to/4tItyDJ) [amazon.de](https://amzn.to/41V075k) | [amazon.com](https://amzn.to/4tEXG2G) [amazon.de](https://amzn.to/4c26zgR) | | #CommissionsEarned | | | | --- ## [Simulating Real Projects — Structural & Electromagnetic FEM](https://blog.hirnschall.net/fem/) meta description: From computing eigenfrequencies of a tuning fork to full-wave EM simulation of a PCB — practical finite element analysis, applied to real engineering problems. date published: 05.04.2023 (DD.MM.YYYY format) date last modified: 05.04.2026 (DD.MM.YYYY format) --- # [Simulating Real Projects with FEM](https://blog.hirnschall.net/fem/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: From computing eigenfrequencies of a tuning fork to full-wave EM simulation of a PCB — practical finite element analysis, applied to real engineering problems. meta title: Simulating Real Projects — Structural & Electromagnetic FEM date published: 05.04.2023 (DD.MM.YYYY format) date last modified: 05.04.2026 (DD.MM.YYYY format) --- Introduction ------------ The Finite Element Method is not limited to one engineering domain — the same mathematical framework applies to structural mechanics, fluid dynamics, and electromagnetics. What changes is the physics being modelled. The posts collected here apply FEM to real projects and real problems: Parts you can hold, PCBs you can order, simulations you can verify with a measurement. The emphasis throughout is on validation — comparing simulation results against real measurements rather than stopping at the simulation itself. For readability the list is split into electrical and mechanical. ### 1   Mechanical * [Auxetic Materials: Design, Simulation, and 3D Print (TPU)](https://blog.hirnschall.net/auxetic-material/) * [Computing Eigenfrequencies with PINVIT, LOPCG, and LOBPCG](https://blog.hirnschall.net/calculating-eigenfrequencies-using-fem/) --- ### 2   Electrical * [4x SATA Backplane: Design, Simulation (3D, full wave), and Build](https://blog.hirnschall.net/4x-sata-backplane/) * [Gerber to STEP conversion for FEM](https://blog.hirnschall.net/gerber-to-step/) --- ### 3   University Projects (PDF) * [Stabilized P1/P1 elements for incompressible flow - Bachelor Thesis](https://blog.hirnschall.net/downloads/files/bachelor-thesis-hirnschall.pdf) * [Computing Eigenfrequencies using Finite Element Methods - Seminar Paper](https://blog.hirnschall.net/downloads/files/computing-eigenfrequencies-using-fem.pdf) --- ## [Symbolic Engineering Solver: Auto-Solve Physics Problems (FSAE)](https://blog.hirnschall.net/everything-aero-concept/) meta description: Define equation templates, provide knowns, let the solver derive the unknowns. Vehicle dynamics and aero built-in, applicable to any physics system. date published: 07.03.2026 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- # [A Symbolic Equation Solver for Formula Student Problems](https://blog.hirnschall.net/everything-aero-concept/) author: [Sebastian Hirnschall](https://blog.hirnschall.net/about/) meta description: Define equation templates, provide knowns, let the solver derive the unknowns. Vehicle dynamics and aero built-in, applicable to any physics system. meta title: Symbolic Engineering Solver: Auto-Solve Physics Problems (FSAE) date published: 07.03.2026 (DD.MM.YYYY format) date last modified: 03.04.2026 (DD.MM.YYYY format) --- Motivation ---------- What if you could solve Formula Student rules quiz problems without thinking about them? While this may sound too good to be true, it has to work in theory and as it turns out, it also does in practice! Each year the Formula Student rules quiz is a challenge. The questions can be daunting and if you do not want to end up on the FSG waiting list you better come prepared. The questions themselves can be quite different from year to year. However, the core concept remains consistent. If you know the physics based equations/formulas required in the exercises you should be able to solve most of them. This brings up an interesting idea. What if we build a very large system of equations using all physics based principles we know together with the information provided in the problem. If we then solve for any unknowns that are solvable we should get the solution to the problem. Approach -------- To get this to work we need to do the following: * Define the physics-based equations and constraints. * Handle the information provided in the problem. * Deal with assumptions that may violate real physics based equations, making the system unsolvable. * Build some sort of solver to solve the system symbolically. * Display all unknowns that we solved for together with the equations used to the user. * Furthermore, if e.g. a minimum is asked for, the solver should output the function we then differentiate. Again, it needs to solve symbolically. * Have some sort of user interface (preferably a web interface) to input constraints and display the results. To do this efficiently and be able to easily create multiple tools for different fields, e.g. aerodynamics, vehicle dynamics, motor, etc. We will use multiple git repositories and integrate them into each other as a submodule. The main blocks will be * An equation library - responsible for providing the physics based equations and constraints * An equation manager - responsible for assembling the system of equations and solving * A user web interface - responsible for taking user input and displaying the results Let's tackle these challenges one by one. Equation Templates ------------------ To define the equations we will use a template system. Each equation template will define the actual equations, the variables used in the equations along with a description, which variables need to be known in order to have any chance or solving the equations, and the name of the equations (used for nice output). Below you can see what this looks like for e.g. Bernoulli's equation. Note the placeholders **{{i}}** and **{{j}}**. They will be replaced by actual indices when assembling the system. ``` equations_to_add=[ "p{{i}} + 1/2 * rho{{i}} * v{{i}}**2 + rho{{i}} * g{{i}} * h{{i}} = p{{j}} + 1/2 * rho{{j}} * v{{j}}**2 + rho{{j}} * g{{j}} * h{{j}}" ], relevant_vars=[ ("p", "Static pressure at point [Pa]"), ("rho", "Fluid density at point [kg/m³]"), ("v", "Flow velocity at point [m/s]"), ("g", "Gravitational acceleration [m/s²]"), ("h", "Geometric height of point [m]") ], vars_to_check=[(["p", "rho", "v"], 2)], name="Bernoulli Equation (two points)" ``` The way we have set up the equation templates, you do not need in depth programming knowledge to create new ones. Furthermore chatgpt/grok is quite good at adding new equations. Equation Manager ---------------- The equation manager will keep track of the system of equations being assembled.It will also handle solving the system. In order to solve the system we will use sympy. Sympy will solve the system symbolically and provide all unknowns, including functions. The only downside we have to consider is that this is quite slow and unfeasible for systems with too many unknowns that remain free. We are already set-up to prevent this issue. Each equation template include a list of **vars\_to\_check**. This list of tuples will be used to check if an equation template should be added to the system. If not at least \(n\) of the listed variables are already in the system we will not add the template. We do so iteratively until no new templates are added. ### Assumptions One thing to consider is dealing with assumptions. If the exercise states that we assume "no lateral weight shift" this means \(W\_{lat}=0\). However, adding this equation to the system will violate real physics based equations, leading to an unsolvable system. We can deal with this problem in the following way: If an assumption is present, we try to solve the system with the assumption included. If the system has no solution, we iteratively drop equations (not including the assumption itself) until the system becomes solvable. ### Input We will handle all inputs as real equations in the solver. This way the user can input something like \(rho\_1=1.2\) to input data provided in the problem but he can also add actual new equations like \(F=ma\). What this allows us to do is define multiple variables for e.g. downforce depending on which effects we want to consider. Then, the user can set a generic \(F\_z=F\_{z,drag}\) to the desired specific unkown. If we now want to compute the maximum velocity we can drive a corner at, the equation template uses the generic \(F\_z\) and we have specialized it to use \(F\_{z,drag}\) as per the exercise. User Interface -------------- To make this app accessible we can use [ngapp](https://cerbsim.github.io/ngapp/). ngapp allows us to run python code locally in the browser without the need to install anything. We can also build and deploy using github actions and github pages. One thing to note is the somewhat slow start of the web app. This is due to the fact that the environment needs to be initialized and we need to install dependencies via pip on load. ### Input We will add three input fields (Input, Looking-for, and Assumptions). One equation per line. ### Output We will render the output as Latex using MathJax. We will highlight and group the variables listed in **Looking-for**. Furthermore, we will output the equations that were included in the system together with their names for traceability. This way we know if the solution is correct and we can tell other team members which equations we used. At a minimum, we end up with an automatic, interactive formulary. Example ------- Let's now take a look at an example, [FS-Quiz Question 366](https://fs-quiz.eu/question/366). "Your team is offered to do a wind tunnel test in order to validate your CFD-simulation and improve the aerodynamic development process The resulting wind tunnel test model is a 60% scale model, featuring all the details of the actual car. At which airspeed (in kph) should you run the wind tunnel in order to directly compare the results with the 80% scale CFD simulation 65kph?" The correct answer is \(v\_1 = 86.6666\). As mentioned, we do not care about the problem itself. We simply input the given information and let the solver do the work. For this example, the input used is $$ \begin{aligned} v\_0 &= 65 \\ L\_0 &= 0.8 \\ L\_1 &= 0.6 \\ \nu\_0 &= \nu\_1 \\ \rho\_0 &= \rho\_1 \\ Re\_0 &= Re\_1. \end{aligned} $$ We used custom equations to tell the solver that both states have the same viscosity, density, and Reynolds number. The solver correctly outputs $$ \begin{aligned} v\_1 &= 86.6666 \end{aligned} $$ along with the equations used to get to this solution. You can find more examples or try the solver below. GitHub ------ This project is open source and available on github: * [WebApp - https://github.com/shirnschall/everything\_aero/](https://github.com/shirnschall/everything_aero/) * [Equations Library - https://github.com/shirnschall/everything\_rulesquiz](https://github.com/shirnschall/everything_rulesquiz) * [Equations Manager - https://github.com/shirnschall/equation\_manager](https://github.com/shirnschall/equation_manager) Try it & more Information ------------------------- The calculator itself is a web application built with Python and ngapp. It is hosted at For more detailed information and several examples refer to the [documentation at blog.hirnschall.net/everything-aero/](https://blog.hirnschall.net/everything-aero/). Conclusion ---------- All in all the solver works as intended. You do not need to think about the problem. You simply input the given information and let the solver do the work. This approach is not limited to aerodynamics and vehicle dynamics. It can be extended to any domain that can be described by equations. It is quite easy to set up solvers for different fields. Simply fork the webapp repository (everything\_aero), add a new equations file, and tell the solver to use the new file. Adding and maintaining equations is straightforward as well and can be done by almost anyone with knowledge of the field (no programming skills required). The next step is to expand and refine the equation library. Building a consistent and comprehensive set of equations will require contributions from domain experts. If you want to contribute, feel free to fork the repositories and submit pull requests! On the other hand, you can fork the project and make it your own as well.