<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Microcontroller Tutorials</title>
	<atom:link href="https://www.teachmemicro.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.teachmemicro.com/</link>
	<description>Microcontroller Tutorials and Resources</description>
	<lastBuildDate>Thu, 06 Aug 2026 09:28:38 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.2</generator>

<image>
	<url>https://www.teachmemicro.com/wp-content/uploads/2019/04/blue-icon-65x65.png</url>
	<title>Microcontroller Tutorials</title>
	<link>https://www.teachmemicro.com/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>How to Scale IoT Networks from Local Prototypes to Enterprise Clouds</title>
		<link>https://www.teachmemicro.com/how-to-scale-iot-networks-from-local-prototypes-to-enterprise-clouds/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=how-to-scale-iot-networks-from-local-prototypes-to-enterprise-clouds</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Thu, 06 Aug 2026 09:28:38 +0000</pubDate>
				<category><![CDATA[Features]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=12438</guid>

					<description><![CDATA[<p>Building an Internet of Things project from scratch is a highly rewarding technical challenge. For many hobbyists and developers, the journey begins on a workbench with a simple Arduino, NodeMCU, or ESP32 board wired into a breadboard. Seeing raw sensor data appear on your screen validates hours of hard work and complex troubleshooting. The Australian &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/how-to-scale-iot-networks-from-local-prototypes-to-enterprise-clouds/">How to Scale IoT Networks from Local Prototypes to Enterprise Clouds</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Building an Internet of Things project from scratch is a highly rewarding technical challenge. For many hobbyists and developers, the journey begins on a workbench with a simple Arduino, NodeMCU, or ESP32 board wired into a breadboard. Seeing raw sensor data appear on your screen validates hours of hard work and complex troubleshooting. The Australian hardware landscape is evolving rapidly, with commercial initiatives expanding the use of Low Earth Orbit satellite connectivity for remote monitoring applications. However, there is an enormous gap between a successfully wired local project and a commercial network spanning large numbers of devices across the country. Understanding how to navigate this transition from local tests to enterprise-scale networks is vital for modern developers and IT teams alike.</p>
<p><span id="more-12438"></span></p>
<h2><strong>The Limits of the Local Workbench</strong></h2>
<p>Developing a basic prototype requires significant engineering effort to perfect both the hardware and software layers. Developers typically start small to test their concepts in a highly controlled environment before committing to larger infrastructure investments. This gradual approach helps reduce early risks while proving that the core concept works as intended under controlled conditions.</p>
<p>For instance, you might begin by setting up an ESP32 async web server to host a local dashboard and monitor sensor readings on your own private Wi-Fi network. This approach is a brilliant, cost-effective way to understand the fundamentals of microcontroller communications, data parsing, and event handling without relying on external hosting fees or complex cloud subscriptions.</p>
<p>But as your project matures, localised builds frequently encounter severe data bottlenecks. Field testing of distributed smart systems (such as prototype environmental sensors or dynamic waste management monitors) often reveals high packet loss and significant latency when you rely entirely on on-premise hardware. To overcome these limitations, organisations often utilise professional <a style="text-decoration: none;" href="https://www.ac3.com.au/what-we-do/cloud">cloud migration services</a> to transition their early-stage local databases into scalable, enterprise-grade environments. Small local dashboards simply do not possess the processing limits, bandwidth, or storage capacity required for continuous commercial production and large-scale data logging.</p>
<h2><strong>Managing the IoT Data Explosion</strong></h2>
<p>The sheer volume of information generated by modern IoT networks is enormous. As connected devices continue to expand across industries, they generate vast amounts of telemetry that require scalable storage and processing capabilities. Whether sensors are tracking agricultural soil moisture, fleet vehicle GPS coordinates, or factory machine temperatures, continuous data collection quickly exceeds the capabilities of standalone development hardware or local servers.</p>
<p>Relying on independent local servers is no longer a viable long-term strategy for growing businesses or ambitious developers. According to a recent Forbes analysis on enterprise IT, true scalability requires organisations to transition away from hacked-together solutions and operate fleets of connected devices via integrated cloud systems that natively handle massive data ingestion. Attempting to manage this level of scale on isolated hardware introduces severe performance bottlenecks and significant security vulnerabilities that could compromise an entire network.</p>
<h2><strong>Bridging the Gap with Professional Infrastructure</strong></h2>
<p>Enterprise cloud networks have become essential for supporting scalable hardware deployments as organisations move away from isolated infrastructure toward unified cloud environments. Many businesses recognise that maintaining independent network infrastructure internally becomes increasingly complex as deployments grow. Adopting managed infrastructure reduces operational overhead by shifting maintenance responsibilities to specialised cloud platforms.</p>
<p>Navigating this transition requires careful architectural planning, especially as cloud platforms continue to evolve. Changes to major cloud services have demonstrated the importance of designing flexible infrastructure that can adapt when technologies or platforms are discontinued. To minimise disruption during these transitions, organisations increasingly rely on experienced technical partners to migrate MQTT brokers, sensor databases, and security credentials into resilient cloud environments.</p>
<h2><strong>Key Steps for Commercial Deployment</strong></h2>
<p>Scaling a network requires a fundamental shift in how telemetry is structured and managed. When moving from a benchtop prototype to a commercial release, engineering teams must implement several critical architectural changes to ensure long-term stability:</p>
<ul>
<li>Implementing Cloud Load Balancers: Routing sensor telemetry through enterprise-grade load balancers is essential to prevent packet loss and reduce latency across thousands of concurrent connections.</li>
<li>Automated Security Patching: Managing manual updates across physical hardware gateways is impossible at scale. Cloud-based software solutions allow for seamless, automated security patches across vast geographic areas.</li>
</ul>
<ul>
<li>Multi-Cloud Redundancy: Relying on a single server point creates a high risk of failure. Scalable enterprise environments ensure continuous MQTT bridge connectivity, keeping devices online even during regional hardware outages.</li>
<li>Preparing for AI Workloads: Adopting advanced cloud networks natively supports the integration of machine learning tools. This capability turns raw sensor telemetry into actionable business intelligence without slowing down core network operations.</li>
</ul>
<h2><strong>Preparing for the Future of Connectivity</strong></h2>
<p>Cloud adoption continues to accelerate across Australia, supporting innovation, business growth, and the expansion of the technology sector. Taking a project from a single microcontroller node to a commercial enterprise network is a significant technical leap. By recognising the limitations of local prototypes and investing in scalable cloud infrastructure early, developers and businesses alike can build resilient, future-ready networks capable of supporting the next generation of connected devices.</p>
<p>The post <a href="https://www.teachmemicro.com/how-to-scale-iot-networks-from-local-prototypes-to-enterprise-clouds/">How to Scale IoT Networks from Local Prototypes to Enterprise Clouds</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Adding an External Antenna to ESP32: U.FL, PCB Modifications, and Placement</title>
		<link>https://www.teachmemicro.com/adding-antenna-to-esp32/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=adding-antenna-to-esp32</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Fri, 31 Jul 2026 00:36:54 +0000</pubDate>
				<category><![CDATA[ESP32 Tutorial]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=12389</guid>

					<description><![CDATA[<p>Adding an antenna to ESP32 can improve Wi-Fi and Bluetooth reliability when the board is installed far from an access point, placed inside an enclosure, or surrounded by components that interfere with its built-in PCB antenna. However, the modification involves more than soldering a piece of wire to the board. The antenna connection is part &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/adding-antenna-to-esp32/">Adding an External Antenna to ESP32: U.FL, PCB Modifications, and Placement</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Adding an antenna to ESP32 can improve Wi-Fi and Bluetooth reliability when the board is installed far from an access point, placed inside an enclosure, or surrounded by components that interfere with its built-in PCB antenna. However, the modification involves more than soldering a piece of wire to the board.</p>
<p>The antenna connection is part of a carefully designed radio-frequency path. The antenna, connector, coaxial cable, PCB trace, matching components, and surrounding ground plane all affect how much of the ESP32's transmitted power is actually radiated.</p>
<p>In this tutorial, we will look at the safest ways of adding an external antenna to an ESP32. This includes choosing a module with a factory-installed U.FL connector, changing an antenna selection jumper, modifying an existing PCB, selecting the correct antenna, and positioning it inside the finished project.</p>
<p><span id="more-12389"></span></p>
<p><strong>Important:</strong> The antenna arrangement differs between ESP32 boards. Before moving or removing any component, find the schematic for your exact development board and inspect the RF section under magnification.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<p><!-- Suggested image: Three ESP32 antenna configurations side by side: PCB antenna module, U.FL module, and development board with an antenna selector jumper. --></p>
<h2><strong>Why Add an External Antenna to an ESP32?</strong></h2>
<p>Most ESP32 development boards already contain a small meandered PCB antenna. For a board sitting openly on a desk, this antenna is often sufficient. Problems usually appear after the board is installed in the final product.</p>
<p>An external antenna may be useful when:</p>
<ul>
<li>The ESP32 is installed inside a metal enclosure.</li>
<li>A battery, LCD, motor, relay, or large PCB is close to the built-in antenna.</li>
<li>The device must communicate through several walls.</li>
<li>The ESP32 is located near the edge of the access point's coverage.</li>
<li>The product needs an antenna mounted outside its enclosure.</li>
<li>The orientation of the built-in PCB antenna is unsuitable for the installation.</li>
<li>A directional antenna is required for a fixed point-to-point link.</li>
</ul>
<p>An external antenna does not automatically guarantee longer range. A badly positioned external antenna connected through a poor cable can perform worse than the original PCB antenna. The entire wireless link must be considered.</p>
<p>A simplified link budget can be written as:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_fe449c7f36fc03606417804b0d84e286.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="P_R = P_T + G_T + G_R - L_{path} - L_{cable} - L_{mismatch}" /></span><script type='math/tex'>P_R = P_T + G_T + G_R - L_{path} - L_{cable} - L_{mismatch}</script></p>
<p>Here, <em>P<sub>R</sub></em> is the received power, <em>P<sub>T</sub></em> is the transmitted power, <em>G<sub>T</sub></em> and <em>G<sub>R</sub></em> are antenna gains, and the remaining terms represent path, cable, and impedance mismatch losses.</p>
<p>This equation explains why installing a higher-gain antenna may not help when a long, lossy cable or an incorrectly modified RF trace introduces additional losses.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Understand Which Type of ESP32 Board You Have</strong></h2>
<p><img data-dominant-color="c5c4c2" data-has-transparency="false" style="--dominant-color: #c5c4c2;" loading="lazy" decoding="async" class="aligncenter size-full wp-image-12391 not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2026/07/esp32-antenna-configurations.avif" alt="ESP32 Antenna Configuration" width="724" height="543" srcset="https://www.teachmemicro.com/wp-content/uploads/2026/07/esp32-antenna-configurations.avif 724w, https://www.teachmemicro.com/wp-content/uploads/2026/07/esp32-antenna-configurations-300x225.avif 300w" sizes="auto, (max-width: 724px) 100vw, 724px" /></p>
<p>The term “ESP32 board” can refer to the ESP32 chip, a shielded ESP32 module, or a complete development board. The antenna may be part of any one of these assemblies.</p>
<table>
<thead>
<tr>
<th>Board or Module Type</th>
<th>Antenna Arrangement</th>
<th>Recommended Approach</th>
</tr>
</thead>
<tbody>
<tr>
<td>ESP32 module with PCB antenna</td>
<td>Antenna is etched directly onto the module PCB</td>
<td>Use the existing antenna or replace the module with an external-antenna version</td>
</tr>
<tr>
<td>ESP32 module ending in U or UE</td>
<td>Factory-installed micro-coaxial connector</td>
<td>Connect a compatible external antenna</td>
</tr>
<tr>
<td>Development board with PCB antenna and U.FL connector</td>
<td>A small RF jumper selects one antenna path</td>
<td>Move the jumper to the external-antenna position</td>
</tr>
<tr>
<td>Board with no connector but an accessible RF feed</td>
<td>Modification requires isolating the existing antenna</td>
<td>Attach a short 50 Ω micro-coaxial cable at the RF feed</td>
</tr>
<tr>
<td>Custom PCB using a bare ESP32 chip</td>
<td>RF matching and antenna network are designed on the PCB</td>
<td>Use controlled-impedance layout and RF validation</td>
</tr>
</tbody>
</table>
<h3><strong>ESP32 Modules with External Antenna Connectors</strong></h3>
<p>Espressif sells several module families in separate PCB-antenna and external-antenna versions. Examples include:</p>
<table>
<thead>
<tr>
<th>PCB Antenna Version</th>
<th>External Antenna Version</th>
</tr>
</thead>
<tbody>
<tr>
<td>ESP32-WROOM-32E</td>
<td>ESP32-WROOM-32UE</td>
</tr>
<tr>
<td>ESP32-S3-WROOM-1</td>
<td>ESP32-S3-WROOM-1U</td>
</tr>
<tr>
<td>ESP32-C3-WROOM-02</td>
<td>ESP32-C3-WROOM-02U</td>
</tr>
</tbody>
</table>
<p>The letter <strong>U</strong> commonly indicates an external-antenna connector in Espressif module names. However, this naming convention should not be assumed for every third-party board. Always verify the full module number printed on the metal shield.</p>
<p>A module such as the ESP32-WROOM-32UE does not contain the familiar meandered PCB antenna. Its RF output is routed directly to the small connector on the module. This is normally the easiest and most reliable option for adding an external antenna.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>What Is a U.FL Connector?</strong></h2>
<p><img data-dominant-color="dfdcd3" data-has-transparency="false" style="--dominant-color: #dfdcd3;" loading="lazy" decoding="async" class="aligncenter size-full wp-image-12390 not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2026/07/rf-connector-reference-chart.avif" alt="RF Connector Reference chart" width="724" height="543" srcset="https://www.teachmemicro.com/wp-content/uploads/2026/07/rf-connector-reference-chart.avif 724w, https://www.teachmemicro.com/wp-content/uploads/2026/07/rf-connector-reference-chart-300x225.avif 300w" sizes="auto, (max-width: 724px) 100vw, 724px" /></p>
<p>U.FL is a miniature 50 Ω coaxial connector family made by Hirose. ESP32 sellers sometimes call the same connector an IPEX connector. More precisely, Espressif's first-generation external antenna connector is compatible with Hirose U.FL, I-PEX MHF I, and Amphenol AMC cable plugs.</p>
<p>These connectors are very small and should be handled carefully. The cable plug must be centered directly over the board receptacle and pressed vertically downward. When correctly connected, it should produce a small tactile click.</p>
<p>Do not press the connector at an angle. Doing so can bend the center contact or pull the receptacle away from the PCB.</p>
<p>Also be careful when ordering cables. I-PEX MHF I and MHF 4 are different connector families. An MHF 4 cable is smaller and should not be forced onto a first-generation U.FL or MHF I receptacle.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h3><strong>U.FL-to-SMA Pigtails</strong></h3>
<p>A common arrangement uses a short U.FL-to-SMA coaxial pigtail. The U.FL end connects to the ESP32 module, while the SMA end is mounted through the enclosure. A larger external whip antenna then screws onto the SMA connector.</p>
<p>This arrangement provides two advantages:</p>
<ul>
<li>The fragile U.FL connector remains protected inside the enclosure.</li>
<li>The external antenna can be replaced without repeatedly disconnecting the U.FL plug.</li>
</ul>
<p>Choose the SMA end carefully. SMA and reverse-polarity SMA connectors look similar but have different center contacts. Verify that the panel connector and antenna are compatible before ordering.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<p><!-- Suggested image: Close-up diagram comparing U.FL/MHF I, MHF 4, SMA, and RP-SMA connectors. --></p>
<h2><strong>Method 1: Use an ESP32 Module with a Factory U.FL Connector</strong></h2>
<p>The best method is to begin with a board or module designed for an external antenna. Examples include development boards populated with an ESP32-WROOM-32UE, ESP32-S3-WROOM-1U, or ESP32-C3-WROOM-02U.</p>
<h3><strong>Parts Required</strong></h3>
<ul>
<li>ESP32 development board with an external-antenna module</li>
<li>Compatible U.FL or MHF I cable assembly</li>
<li>2.4 GHz, 50 Ω antenna suitable for the module</li>
<li>Optional U.FL-to-SMA bulkhead pigtail</li>
<li>Plastic connector tool or fine nonmetallic tweezers</li>
</ul>
<h3><strong>Connection Procedure</strong></h3>
<ol>
<li>Disconnect USB power and any external power supply.</li>
<li>Confirm the connector type using the module datasheet.</li>
<li>Inspect the U.FL receptacle for contamination or damage.</li>
<li>Align the cable plug directly over the receptacle.</li>
<li>Press downward on the metal connector body rather than on the cable.</li>
<li>Route the cable so that it does not pull sideways on the connector.</li>
<li>Add strain relief to the cable several millimeters away from the receptacle.</li>
<li>Install the antenna before applying power.</li>
</ol>
<p>Do not attach adhesive, epoxy, or hot glue directly over the connector until the connection has been tested. Strain relief should hold the cable to the main PCB without pulling on the RF receptacle.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Method 2: Move the ESP32 Antenna Selection Jumper</strong></h2>
<p>Some development boards contain both a PCB antenna and a U.FL-style connector. Only one path should normally be active. A small zero-ohm resistor or solder bridge connects the ESP32's RF feed to the selected antenna.</p>
<p>This arrangement is particularly common on some ESP32 camera boards and compact IoT development boards.</p>
<p><!-- Suggested image: Enlarged three-pad antenna selector showing center RF feed, PCB antenna route, and U.FL route. --></p>
<h3><strong>How the Three-Pad Selector Works</strong></h3>
<p><img data-dominant-color="759574" data-has-transparency="false" style="--dominant-color: #759574;" loading="lazy" decoding="async" class="aligncenter size-full wp-image-12397 not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2026/07/pcb-antenna-jumper-selection.avif" alt="PCB antenna jumper selection" width="724" height="543" srcset="https://www.teachmemicro.com/wp-content/uploads/2026/07/pcb-antenna-jumper-selection.avif 724w, https://www.teachmemicro.com/wp-content/uploads/2026/07/pcb-antenna-jumper-selection-300x225.avif 300w" sizes="auto, (max-width: 724px) 100vw, 724px" /></p>
<p>A typical selector contains three pads:</p>
<ul>
<li>The center pad is connected to the ESP32 RF output.</li>
<li>One outside pad leads to the built-in PCB antenna.</li>
<li>The other outside pad leads to the U.FL connector.</li>
</ul>
<p>A zero-ohm resistor bridges the center pad to one of the two antenna paths. To select the external antenna, the resistor is removed from the PCB-antenna path and installed between the center pad and the connector path.</p>
<p>The arrangement is not identical on every board. The external path may be above, below, or beside the original jumper. Never assume that rotating the resistor by 90 degrees is correct without tracing the circuit.</p>
<h3><strong>Tools Required</strong></h3>
<ul>
<li>Fine-tip temperature-controlled soldering iron</li>
<li>Fine soldering tweezers</li>
<li>Flux</li>
<li>Solder wick</li>
<li>Magnification</li>
<li>ESD-safe work surface</li>
<li>Multimeter for checking shorts and continuity</li>
</ul>
<h3><strong>Modification Procedure</strong></h3>
<ol>
<li>Remove all power from the board.</li>
<li>Take a clear photograph of the original jumper position.</li>
<li>Identify the radio feed, PCB antenna route, and connector route from the schematic or PCB traces.</li>
<li>Apply a small amount of flux to the zero-ohm resistor.</li>
<li>Heat both ends evenly and lift the component with tweezers.</li>
<li>Clean the pads using solder wick if necessary.</li>
<li>Place the resistor between the RF feed pad and the external-connector pad.</li>
<li>Inspect the joint under magnification.</li>
<li>Check that the PCB antenna path is no longer connected.</li>
<li>Connect the external antenna before powering the ESP32.</li>
</ol>
<p>A solder blob can sometimes be used instead of reinstalling the zero-ohm resistor, but it must not bridge all three pads. Connecting the PCB antenna and external antenna simultaneously creates an uncontrolled RF junction that can cause severe impedance mismatch.</p>
<p>A multimeter can identify an accidental short or open circuit, but it cannot prove that the RF path has a 50 Ω characteristic impedance at 2.4 GHz. Proper RF performance must be evaluated through signal, throughput, or network-analyzer testing.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Method 3: Add a Coaxial Connection to a Board Without U.FL</strong></h2>
<p>This is the most difficult method and should normally be used only for experimentation. If a standard ESP32-WROOM module already contains the PCB antenna, the RF path is part of the module itself. The carrier board does not usually provide a convenient antenna feed that can be modified.</p>
<p>For this type of board, replacing the complete module or development board with an external-antenna version is usually safer than cutting into the module's PCB antenna.</p>
<h3><strong>When a Direct PCB Modification Is Possible</strong></h3>
<p>A modification may be possible when:</p>
<ul>
<li>The RF feed trace is visible and accessible.</li>
<li>The board contains an exposed matching network.</li>
<li>The existing PCB antenna can be disconnected without disturbing the matching components.</li>
<li>A nearby ground point is available for the coaxial shield.</li>
<li>The modification is being performed on a prototype rather than a certified production product.</li>
</ul>
<h3><strong>Why a Loose Wire Is Not Recommended</strong></h3>
<p>A common shortcut is to solder a random wire to the antenna feed. Although the wire may radiate some RF energy, it does not provide a controlled 50 Ω connection. The exposed section of wire, ground geometry, nearby objects, and solder joint all become part of the antenna.</p>
<p>The free-space wavelength at 2.4 GHz is approximately:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_d6d4c8846f3d015b890e99d150ee535a.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="\lambda = \frac{c}{f} \approx 125\text{ mm}" /></span><script type='math/tex'>\lambda = \frac{c}{f} \approx 125\text{ mm}</script></p>
<p>A free-space quarter wavelength is therefore approximately:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_d0544a6dc77d49991dc706d41a6ad392.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="\frac{\lambda}{4} \approx 31\text{ mm}" /></span><script type='math/tex'>\frac{\lambda}{4} \approx 31\text{ mm}</script></p>
<p>This does not mean that soldering a 31 mm wire to the ESP32 will produce a properly matched antenna. Practical antennas are affected by conductor width, ground plane dimensions, dielectric material, enclosure, loading, and nearby components.</p>
<h3><strong>Recommended Direct-Modification Approach</strong></h3>
<p>When a direct modification is unavoidable, use a short length of 50 Ω micro-coaxial cable rather than an unshielded wire.</p>
<ol>
<li>Locate the antenna feed and matching network.</li>
<li>Determine which side of the series component is connected to the radio.</li>
<li>Remove the jumper or cut the trace leading to the built-in antenna.</li>
<li>Solder the coaxial cable's center conductor to the radio-side RF feed.</li>
<li>Solder the coaxial shield to ground immediately beside the feed point.</li>
<li>Keep the exposed center conductor extremely short.</li>
<li>Secure the cable to the PCB so movement does not pull on the RF pad.</li>
<li>Terminate the other end at a proper U.FL, SMA, or other 50 Ω connector.</li>
</ol>
<p>The shield connection is as important as the center conductor. A long shield lead creates inductance and can make an otherwise short connection behave poorly at 2.4 GHz.</p>
<p>Do not scrape or cut the antenna trace while the board is powered. Avoid excessive heat because small RF pads can separate easily from the PCB.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Designing an External Antenna Connection on a Custom ESP32 PCB</strong></h2>
<p><img data-dominant-color="a1b09f" data-has-transparency="false" style="--dominant-color: #a1b09f;" loading="lazy" decoding="async" class="aligncenter size-full wp-image-12396 not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2026/07/good-vs-poor-rf-layout.avif" alt="good-vs-poor-rf-layout" width="724" height="543" srcset="https://www.teachmemicro.com/wp-content/uploads/2026/07/good-vs-poor-rf-layout.avif 724w, https://www.teachmemicro.com/wp-content/uploads/2026/07/good-vs-poor-rf-layout-300x225.avif 300w" sizes="auto, (max-width: 724px) 100vw, 724px" /></p>
<p>If you are creating a custom ESP32 board, the RF connection should be designed into the PCB rather than added afterward.</p>
<p>The RF trace between the matching network and antenna connector should have a characteristic impedance of approximately 50 Ω. The correct trace width depends on:</p>
<ul>
<li>PCB dielectric thickness</li>
<li>Copper thickness</li>
<li>Dielectric constant</li>
<li>Distance to the reference ground plane</li>
<li>Whether the trace is microstrip or coplanar waveguide</li>
<li>Spacing between the trace and surrounding ground copper</li>
</ul>
<p>Because these values depend on the PCB manufacturer's stack-up, copying the RF trace width from another board does not guarantee the correct impedance.</p>
<h3><strong>RF Layout Guidelines</strong></h3>
<ul>
<li>Keep the RF trace as short as possible.</li>
<li>Do not create branches or stubs in the antenna path.</li>
<li>Keep the trace width consistent.</li>
<li>Avoid routing the RF trace through vias.</li>
<li>Maintain a continuous ground reference beneath the trace.</li>
<li>Use ground vias along the sides of a coplanar RF trace.</li>
<li>Keep crystals, high-speed clocks, USB lines, UART lines, and switching regulators away from the antenna section.</li>
<li>Place the matching network close to the ESP32 RF output or antenna as directed by the reference design.</li>
<li>Keep copper and signal traces away from the connector area where required by the connector footprint.</li>
<li>Include a pi or CLC matching footprint even when the initial design uses zero-ohm or unpopulated components.</li>
</ul>
<p>The matching network gives you a way to compensate for the final PCB, connector, enclosure, and antenna during RF testing. The correct matching values normally require a vector network analyzer or equivalent RF measurement setup.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Choosing the Correct ESP32 External Antenna</strong></h2>
<p>The antenna must match the radio, connector, installation, and certification requirements of the exact ESP32 module.</p>
<h3><strong>Frequency</strong></h3>
<p>Common ESP32, ESP32-C3, and ESP32-S3 modules operate their Wi-Fi and Bluetooth radios in the 2.4 GHz band. A 2.4 GHz Wi-Fi or Wi-Fi/Bluetooth antenna is therefore required for these modules.</p>
<p>Do not select an antenna only because it physically fits the connector. An antenna intended only for GPS, cellular, LoRa, or sub-GHz operation may have very poor performance at 2.4 GHz.</p>
<p>For newer ESP32 variants supporting additional radio bands, use an antenna specified for every band that the design will use.</p>
<h3><strong>Impedance</strong></h3>
<p>The antenna and cable should have a nominal impedance of 50 Ω. This matches the RF system used by the ESP32 module and standard Wi-Fi antenna assemblies.</p>
<h3><strong>Antenna Gain</strong></h3>
<p>Higher gain does not mean that the antenna generates additional power. It concentrates radiation in particular directions while reducing it in others.</p>
<p>For example, a vertical high-gain whip may provide more horizontal coverage but create weaker areas directly above or below the antenna. A low-gain antenna may produce a more useful radiation pattern for a device whose orientation changes.</p>
<p>Certification must also be considered. Some Espressif external-antenna module datasheets specify the antenna used during certification and recommend that replacement antenna gain not exceed that value. The ESP32-WROOM-32UE and ESP32-S3-WROOM-1U documentation, for example, specifies a 2.4 GHz, 50 Ω antenna and lists 2.33 dBi as the certification antenna's maximum gain.</p>
<p>That value should not automatically be applied to every ESP32 module. Check the datasheet and certification documents for your exact part number. Using a different antenna type or higher gain may require additional EMC or regulatory testing.</p>
<h3><strong>External Whip Antenna</strong></h3>
<p>A screw-on whip antenna is useful when the antenna can be mounted outside the enclosure. It is easy to replace and can be oriented after installation.</p>
<h3><strong>Adhesive FPC Antenna</strong></h3>
<p>An adhesive flexible antenna can be installed inside a plastic enclosure. It should be mounted according to the antenna manufacturer's instructions. Some flexible antennas are designed to operate against a specific ground plane, while others require clearance from metal.</p>
<p>Do not fold, cut, or sharply bend the active antenna section. The coaxial cable may usually be routed around the enclosure, but the radiating element should remain in its intended shape.</p>
<h3><strong>PCB Antenna</strong></h3>
<p>A separate PCB antenna can be connected through a coaxial cable and mounted away from the ESP32 board. As with an FPC antenna, the antenna's ground-plane and clearance requirements must be followed.</p>
<h3><strong>Directional Antenna</strong></h3>
<p>A patch, panel, or Yagi-style antenna may be useful for a fixed point-to-point connection. It must be aimed correctly and may not be suitable for devices that move or need coverage in several directions.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>ESP32 Antenna Placement</strong></h2>
<p>Antenna placement is often more important than antenna gain. Moving an ordinary antenna away from metal and electrical noise can produce a greater improvement than replacing it with a higher-gain antenna in the same poor location.</p>
<h3><strong>Keep the Antenna Away from Metal</strong></h3>
<p>Metal close to an antenna can block radiation and change its resonant frequency. Avoid placing the antenna directly against:</p>
<ul>
<li>Metal enclosures</li>
<li>Large batteries</li>
<li>Heat sinks</li>
<li>LCD frames</li>
<li>Metal mounting plates</li>
<li>Large connectors</li>
<li>Shielding cans</li>
</ul>
<p>When using a metal enclosure, mount the antenna outside through a bulkhead connector. The coaxial cable can pass through the wall while the antenna remains in free space.</p>
<h3><strong>Keep the Antenna Away from Noise Sources</strong></h3>
<p>Switching regulators, DC motors, relays, high-speed displays, USB circuits, memory buses, and digital clocks can introduce interference. Place the antenna and its cable away from these circuits whenever possible.</p>
<p>Do not route the antenna cable parallel to high-current motor or relay wiring. If the paths must cross, crossing at approximately 90 degrees is generally preferable to running them together.</p>
<h3><strong>Respect the PCB Antenna Keepout Area</strong></h3>
<p>When using an ESP32 module's built-in PCB antenna, place the antenna end at the edge of the carrier PCB. Allowing the antenna to extend beyond the edge is usually the preferred arrangement.</p>
<p>If the antenna cannot extend past the board, there should be no copper, traces, or components under or beside the active antenna region. Espressif recommends providing at least 15 mm of clearance around the PCB antenna inside the finished enclosure.</p>
<p>The exact keepout geometry should come from the hardware design guide for the selected module.</p>
<h3><strong>Consider Polarization</strong></h3>
<p>A straight whip antenna normally has linear polarization. For communication with a typical access point whose antennas are vertical, starting with the ESP32 antenna vertically oriented is reasonable.</p>
<p>Rotating one antenna by 90 degrees relative to the other can reduce the received signal. Reflections inside buildings may reduce or change this effect, so the final orientation should still be tested experimentally.</p>
<h3><strong>Do Not Coil the Cable Around the Antenna</strong></h3>
<p>Extra coaxial cable should be routed away from the radiating element. Do not wrap it tightly around the whip or place a coil directly behind an adhesive antenna. The cable shield and nearby conductive objects can alter the antenna pattern.</p>
<h3><strong>Test the Finished Enclosure</strong></h3>
<p>An antenna that performs well on an open workbench may behave differently after the enclosure, battery, display, and wiring are installed. Always evaluate the complete assembled product.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<p><!-- Suggested image: Good and bad ESP32 antenna placement examples inside plastic and metal enclosures. --></p>
<h2><strong>Testing the ESP32 Antenna Modification</strong></h2>
<p>Test the original antenna before making the modification so that you have a baseline. Use the same ESP32 board, access point, location, power supply, orientation, and firmware for both tests.</p>
<p>Useful measurements include:</p>
<ul>
<li>Average RSSI</li>
<li>Packet loss</li>
<li>Connection and reconnection time</li>
<li>TCP or UDP throughput</li>
<li>Maximum usable distance</li>
<li>Performance through walls</li>
<li>Performance at different antenna orientations</li>
</ul>
<p>RSSI is measured in dBm. Values closer to zero represent a stronger received signal. Because RSSI changes with movement, reflections, and nearby people, compare averages rather than relying on one reading.</p>
<h3><strong>Arduino RSSI Test Sketch</strong></h3>
<p>The following sketch connects to an access point and prints individual RSSI readings followed by their average:</p>
<pre><pre><code class="language-cpp">#include &lt;WiFi.h&gt;

const char *ssid = &quot;YOUR_WIFI_SSID&quot;;
const char *password = &quot;YOUR_WIFI_PASSWORD&quot;;

const int sampleCount = 20;
const unsigned long sampleDelayMs = 500;

void setup() {
  Serial.begin(115200);
  delay(1000);

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  Serial.print(&quot;Connecting&quot;);

  unsigned long startTime = millis();

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(&quot;.&quot;);

    if (millis() - startTime &gt; 30000) {
      Serial.println(&quot;\nConnection timed out.&quot;);
      return;
    }
  }

  Serial.println(&quot;\nConnected.&quot;);
  Serial.print(&quot;IP address: &quot;);
  Serial.println(WiFi.localIP());
  Serial.print(&quot;BSSID: &quot;);
  Serial.println(WiFi.BSSIDstr());
}

void loop() {
  if (WiFi.status() != WL_CONNECTED) {
    Serial.println(&quot;Wi-Fi disconnected.&quot;);
    delay(2000);
    return;
  }

  long rssiTotal = 0;

  Serial.println(&quot;Collecting RSSI samples:&quot;);

  for (int i = 0; i &lt; sampleCount; i++) {
    int32_t rssi = WiFi.RSSI();
    rssiTotal += rssi;

    Serial.print(&quot;Sample &quot;);
    Serial.print(i + 1);
    Serial.print(&quot;: &quot;);
    Serial.print(rssi);
    Serial.println(&quot; dBm&quot;);

    delay(sampleDelayMs);
  }

  float averageRssi = (float)rssiTotal / sampleCount;

  Serial.print(&quot;Average RSSI: &quot;);
  Serial.print(averageRssi, 1);
  Serial.println(&quot; dBm\n&quot;);

  delay(3000);
}</code></pre></pre>
<p>Record several test runs with the original antenna and several more with the external antenna. Do not hold the board or antenna during one test and place it on a table during another. Your hand can affect the antenna and invalidate the comparison.</p>
<h3><strong>Use More Than RSSI</strong></h3>
<p>An improved RSSI reading does not always mean that the entire connection is better. Interference, packet retries, antenna pattern, and access-point behavior can affect actual data transfer.</p>
<p>For a more useful evaluation, transfer a large file or run a local TCP throughput test while monitoring packet loss and disconnections. Repeat the test in the location where the device will actually operate.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Troubleshooting an ESP32 External Antenna</strong></h2>
<h3><strong>The External Antenna Performs Worse</strong></h3>
<p>Check the following:</p>
<ul>
<li>The zero-ohm jumper may still select the PCB antenna.</li>
<li>Both antenna paths may accidentally be connected.</li>
<li>The U.FL plug may not be fully seated.</li>
<li>The antenna may be designed for the wrong frequency.</li>
<li>The antenna may be too close to metal.</li>
<li>The coaxial cable may be damaged or excessively long.</li>
<li>The antenna may have a null in the direction of the access point.</li>
<li>The connector or RF pad may have been damaged during soldering.</li>
</ul>
<h3><strong>Wi-Fi Works Only When the Board Is Close to the Router</strong></h3>
<p>This often indicates an open or badly mismatched antenna path. Inspect the selector resistor and U.FL connection. Confirm that the antenna is connected to the radio side of the matching network rather than to the isolated PCB-antenna side.</p>
<h3><strong>The Signal Changes When the Cable Is Touched</strong></h3>
<p>A large change when touching the cable can indicate that the cable shield is not properly grounded, the connector is loose, or an exposed section of conductor is acting as part of the antenna.</p>
<h3><strong>The U.FL Connector Detached from the PCB</strong></h3>
<p>Do not attempt to glue it back in place and expect the RF path to work. The center signal pad and surrounding ground pads must be electrically restored. If the pads have lifted, repair normally requires microscope-assisted micro-soldering and a careful RF connection.</p>
<h3><strong>The Board Resets While Transmitting</strong></h3>
<p>This problem is more likely related to the power supply than the antenna. Wi-Fi transmission produces short current peaks. Check the 3.3 V regulator, USB cable, supply wiring, and decoupling capacitors.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Frequently Asked Questions</strong></h2>
<h3><strong>Can I connect an antenna directly to an ESP32 GPIO?</strong></h3>
<p>No. The Wi-Fi and Bluetooth antenna uses the ESP32's RF output path, not a general-purpose input/output pin. Connecting an antenna to a GPIO will not improve radio range and may expose that GPIO to unwanted RF signals or static discharge.</p>
<h3><strong>Can I use the PCB antenna and external antenna at the same time?</strong></h3>
<p>Not by simply connecting both paths together. A passive split creates an impedance mismatch and divides transmitted power. A proper diversity or dual-antenna design requires an RF switch, matching network, and support from the selected ESP32 variant and firmware.</p>
<h3><strong>Can I solder an SMA connector directly to the board?</strong></h3>
<p>Only when the board has a correctly designed 50 Ω RF footprint and sufficient mechanical support. A full-size SMA connector is too large to hang from a small antenna feed pad. A short micro-coaxial pigtail connected to a panel-mounted SMA connector is usually safer.</p>
<h3><strong>Will a 9 dBi antenna give three times the ESP32 range?</strong></h3>
<p>Not necessarily. Gain is directional, cable loss reduces the improvement, and the other end of the connection must still receive and return data. Higher gain may also exceed the antenna conditions used for the module's regulatory certification.</p>
<h3><strong>Does the external antenna improve both Wi-Fi and Bluetooth?</strong></h3>
<p>On common ESP32 modules, Wi-Fi and Bluetooth share the 2.4 GHz RF path. A correctly selected external antenna can therefore affect both. The exact internal radio arrangement should still be confirmed in the datasheet for the selected ESP32 variant.</p>
<h3><strong>Can I operate an external-antenna ESP32 without an antenna?</strong></h3>
<p>Avoid intentionally transmitting for extended periods without an antenna. An open connector produces a severe impedance mismatch, greatly reduces communication performance, and may stress the RF output stage.</p>
<h3><strong>Is firmware configuration required after moving the antenna jumper?</strong></h3>
<p>Normally, no. A passive zero-ohm antenna selector changes the physical RF path and is not controlled by software. Boards using an electronic RF switch or antenna-diversity circuit may require GPIO or ESP-IDF configuration.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Summary</strong></h2>
<p>The safest way of adding an external antenna to an ESP32 is to use a module designed for it, such as an ESP32-WROOM-32UE or ESP32-S3-WROOM-1U. The factory connector preserves the module's controlled RF path and avoids cutting the built-in PCB antenna.</p>
<p>For boards containing both a PCB antenna and a U.FL connector, locate the antenna selector and move its zero-ohm jumper to the external path. Do not leave both antenna paths connected.</p>
<p>Directly modifying a PCB-antenna module is possible in some cases, but it requires isolating the existing antenna and attaching a properly grounded 50 Ω micro-coaxial cable. A loose wire is not a reliable substitute for a matched antenna connection.</p>
<p>Finally, pay close attention to placement. Keep the antenna away from metal, high-speed circuits, batteries, displays, and noisy wiring. Test the complete device in its final enclosure using RSSI, packet loss, and actual throughput rather than assuming that a larger antenna will automatically provide better range.</p>
<p>The post <a href="https://www.teachmemicro.com/adding-antenna-to-esp32/">Adding an External Antenna to ESP32: U.FL, PCB Modifications, and Placement</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>ESP32-S3 Analog Input Leakage Current: High-Impedance ADC Design</title>
		<link>https://www.teachmemicro.com/esp32-s3-analog-input-leakage-current/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=esp32-s3-analog-input-leakage-current</link>
					<comments>https://www.teachmemicro.com/esp32-s3-analog-input-leakage-current/#respond</comments>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Tue, 28 Jul 2026 03:13:06 +0000</pubDate>
				<category><![CDATA[ESP32 Tutorial]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=12350</guid>

					<description><![CDATA[<p>The ESP32-S3 analog-to-digital converter works well with low-impedance voltage sources. However, problems can appear when the analog signal comes from a high-value resistor divider, a resistive sensor, or another circuit that cannot supply much current. You may notice that the measured voltage is lower than expected, the first reading is different from the following readings, &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/esp32-s3-analog-input-leakage-current/">ESP32-S3 Analog Input Leakage Current: High-Impedance ADC Design</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>The ESP32-S3 analog-to-digital converter works well with low-impedance voltage sources. However, problems can appear when the analog signal comes from a high-value resistor divider, a resistive sensor, or another circuit that cannot supply much current.</p>
<p>You may notice that the measured voltage is lower than expected, the first reading is different from the following readings, or one ADC channel appears to affect another. These symptoms are often blamed on <strong>analog input leakage current</strong>. Leakage is part of the problem, but the ESP32-S3's switched-capacitor ADC input also creates a temporary load whenever a conversion starts.</p>
<p>In this article, we will examine how ESP32-S3 analog input leakage affects high-impedance circuits and how to design an ADC input that produces stable and repeatable readings.</p>
<p><span id="more-12350"></span></p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>How the ESP32-S3 ADC Input Works</strong></h2>
<p>The ESP32-S3 contains two 12-bit successive-approximation ADC units. Together, they provide 20 analog input channels:</p>
<table>
<thead>
<tr>
<th>ADC Unit</th>
<th>Channels</th>
<th>GPIO Pins</th>
</tr>
</thead>
<tbody>
<tr>
<td>ADC1</td>
<td>ADC1_CH0 to ADC1_CH9</td>
<td>GPIO1 to GPIO10</td>
</tr>
<tr>
<td>ADC2</td>
<td>ADC2_CH0 to ADC2_CH9</td>
<td>GPIO11 to GPIO20</td>
</tr>
</tbody>
</table>
<p>For most projects, ADC1 is the better choice. Espressif recommends ADC1, and ADC2 analog functions cannot be used normally while Wi-Fi is using the ADC2 peripheral.</p>
<p>A 12-bit conversion produces values from 0 to 4095. However, the raw result is not automatically equal to an exact voltage because the internal reference voltage varies from one chip to another. The ESP32-S3 reference is designed around 1.1 V, but its actual value can vary approximately from 1.0 V to 1.2 V. For this reason, calibrated voltage functions should be used whenever possible.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>What Is Analog Input Leakage Current?</strong></h2>
<p>An ideal ADC input would have infinite resistance and draw no current from the measured circuit. A real microcontroller pin contains protection structures, switches, multiplexers, transistors, parasitic capacitance, and internal analog circuitry. Small currents can therefore flow into or out of the pin.</p>
<p>When the source resistance is low, this current produces almost no visible error. When the source resistance is hundreds of kilohms or several megohms, even a tiny current can create a measurable voltage drop.</p>
<p>The approximate DC error caused by leakage is:</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_06ca51353687d5f7e2e937cd03c1a494.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt=" V_{error}=I_{leak}\times R_{source} " /></span><script type='math/tex'> V_{error}=I_{leak}\times R_{source} </script></p>
<p>For example, suppose an analog source has an equivalent resistance of 200 kΩ. The resulting errors would be:</p>
<table>
<thead>
<tr>
<th>Assumed Leakage Current</th>
<th>Voltage Error with 200 kΩ Source</th>
</tr>
</thead>
<tbody>
<tr>
<td>10 nA</td>
<td>2 mV</td>
</tr>
<tr>
<td>50 nA</td>
<td>10 mV</td>
</tr>
<tr>
<td>100 nA</td>
<td>20 mV</td>
</tr>
<tr>
<td>1 µA</td>
<td>200 mV</td>
</tr>
</tbody>
</table>
<p>The ESP32-S3 datasheet specifies a maximum high-level and low-level digital GPIO input current of 50 nA under its stated DC test conditions. However, this value should <strong>not</strong> be treated as a guaranteed ADC input leakage specification. The datasheet does not provide a separate worst-case analog leakage value covering ADC conversions, attenuation settings, temperature, board contamination, and all operating modes.</p>
<p>For a high-impedance design, it is safer to consider the 50 nA figure only as a pin-level reference. The complete ADC circuit should be tested across temperature and operating conditions, or isolated from the signal source using an analog buffer.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>The ADC Is a Dynamic Load</strong></h2>
<p><img data-dominant-color="edeef0" data-has-transparency="false" style="--dominant-color: #edeef0;" loading="lazy" decoding="async" class="aligncenter wp-image-12365 size-full not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2026/07/impedance-variation.avif" alt="Source impedance variation" width="718" height="235" srcset="https://www.teachmemicro.com/wp-content/uploads/2026/07/impedance-variation.avif 718w, https://www.teachmemicro.com/wp-content/uploads/2026/07/impedance-variation-300x98.avif 300w" sizes="auto, (max-width: 718px) 100vw, 718px" /></p>
<p>Leakage current explains steady DC error, but it does not explain every problem seen with high-impedance inputs. The ESP32-S3 uses a successive-approximation ADC, which includes an internal sampling network.</p>
<p>At the beginning of a conversion, an internal switch briefly connects the selected GPIO to a sampling capacitor. The input source must provide enough charge for this capacitor to approach the input voltage during the available acquisition period.</p>
<p>A simplified settling relationship is:</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_57871c5086b6249b2b61e5ea5d7553f0.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt=" V_{error}=V_{step}e^{-\frac{t_{acq}}{R_{source}C_{sample}}} " /></span><script type='math/tex'> V_{error}=V_{step}e^{-\frac{t_{acq}}{R_{source}C_{sample}}} </script></p>
<p>Here, <em>Rsource</em> is the source resistance, <em>Csample</em> is the effective internal sampling capacitance, and <em>tacq</em> is the acquisition time.</p>
<p>As the source resistance increases, the capacitor takes longer to charge. If the ADC starts its conversion before the voltage settles, the result can be lower or higher than the actual signal. This effect may appear even when the static leakage current is extremely small.</p>
<p>The internal attenuation network also changes the signal path. Consequently, source loading and conversion error may vary with the selected attenuation setting.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Why the First ADC Reading Can Be Wrong</strong></h2>
<p>The ADC input multiplexer connects several analog channels to the same converter. When software changes from one channel to another, the sampling network can retain a small amount of charge from the previous channel.</p>
<p>A low-impedance source quickly replaces this charge. A high-impedance source may require more time. The first reading can therefore be influenced by:</p>
<ul>
<li>The voltage previously measured on another ADC channel</li>
<li>Residual charge in the sampling network</li>
<li>The selected attenuation setting</li>
<li>The external filter capacitor</li>
<li>The time between channel selection and conversion</li>
</ul>
<p>A common software solution is to perform one dummy conversion after changing channels and discard its result. This helps, but it does not replace proper analog input design.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>High-Value Resistor Divider Example</strong></h2>
<p>Consider a circuit that measures a supply voltage of up to 15 V. To minimize battery drain, we use a 1 MΩ upper resistor and a 220 kΩ lower resistor.</p>
<p>The voltage presented to the ESP32-S3 is:</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_781a090fa519aa3a6b619eadb7a9f4e4.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="V_{ADC}=V_{IN}\frac{R_{BOTTOM}}{R_{TOP}+R_{BOTTOM}}" /></span><script type='math/tex'>V_{ADC}=V_{IN}\frac{R_{BOTTOM}}{R_{TOP}+R_{BOTTOM}}</script></p>
<p>At 15 V:</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_b42fce7284f9fb9a7c588fda9302878c.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="V_{ADC}=15\frac{220000}{1000000+220000}=2.705\text{ V}" /></span><script type='math/tex'>V_{ADC}=15\frac{220000}{1000000+220000}=2.705\text{ V}</script></p>
<p>This keeps the input below the ESP32-S3 datasheet's 2.9 V effective calibrated range for the highest attenuation setting.</p>
<p>The divider current at 15 V is:</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_3bad6b6c6d698ebaf0deeb2c1e473624.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="I_{DIV}=\frac{15}{1000000+220000}=12.3\mu\text{A}" /></span><script type='math/tex'>I_{DIV}=\frac{15}{1000000+220000}=12.3\mu\text{A}</script></p>
<p>The ADC does not see 1.22 MΩ as its source resistance. The Thevenin resistance at the divider output is:</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_add2bb1513c56f52f309512b1f7d9408.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="R_{TH}=R_{TOP}\parallel R_{BOTTOM}" /></span><script type='math/tex'>R_{TH}=R_{TOP}\parallel R_{BOTTOM}</script></p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_01bd6c320f788e257b00ef2f005be99b.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="R_{TH}=\frac{1000000\times220000}{1000000+220000}=180.3\text{ k}\Omega" /></span><script type='math/tex'>R_{TH}=\frac{1000000\times220000}{1000000+220000}=180.3\text{ k}\Omega</script></p>
<p>This is still a relatively high source impedance. An assumed leakage current of 100 nA would produce approximately:</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_5b0a0eb47806ec13eae54dde4dce27fd.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="V_{error}=100\text{ nA}\times180.3\text{ k}\Omega=18.0\text{ mV}" /></span><script type='math/tex'>V_{error}=100\text{ nA}\times180.3\text{ k}\Omega=18.0\text{ mV}</script></p>
<p>At the original 15 V input, the equivalent error is multiplied by the divider ratio:</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_2ac995f837aa778b04a40ae0f88177f6.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="V_{IN_error}=18.0\text{ mV}\times\frac{1220000}{220000}\approx100\text{ mV}" /></span><script type='math/tex'>V_{IN_error}=18.0\text{ mV}\times\frac{1220000}{220000}\approx100\text{ mV}</script></p>
<p>This calculation only estimates static leakage error. Sampling transients, resistor tolerance, ADC error, noise, temperature, and PCB leakage can add further error.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Adding a Capacitor to the ADC Input</strong></h2>
<p>Espressif recommends placing a 0.1 µF capacitor between the ADC pin and ground. The capacitor performs two useful functions:</p>
<ul>
<li>It filters high-frequency noise before it reaches the ADC.</li>
<li>It acts as a local charge reservoir for the ADC sampling capacitor.</li>
</ul>
<p><img data-dominant-color="f6f6f7" data-has-transparency="false" style="--dominant-color: #f6f6f7;" loading="lazy" decoding="async" class="aligncenter size-full wp-image-12364 not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2026/07/esp32-s3-adc-circuit.avif" alt="ESP32-S3 ADC Added capacitor" width="724" height="543" srcset="https://www.teachmemicro.com/wp-content/uploads/2026/07/esp32-s3-adc-circuit.avif 724w, https://www.teachmemicro.com/wp-content/uploads/2026/07/esp32-s3-adc-circuit-300x225.avif 300w" sizes="auto, (max-width: 724px) 100vw, 724px" /></p>
<p>For the 1 MΩ and 220 kΩ divider, the approximate time constant with a 0.1 µF capacitor is:</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_fc9f92edb00764728e1996ea4fce8204.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="\tau=R_{TH}C" /></span><script type='math/tex'>\tau=R_{TH}C</script></p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_18a3fdec57ae339d54c9785dd5cbc5a7.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="\tau=180.3\text{ k}\Omega\times0.1\mu\text{F}=18.0\text{ ms}" /></span><script type='math/tex'>\tau=180.3\text{ k}\Omega\times0.1\mu\text{F}=18.0\text{ ms}</script></p>
<p>A capacitor is normally considered nearly settled after about five time constants:</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_f08e5213fb1b9dd9e33eb4c4a1136fad.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="t_{settle}\approx5\tau=90\text{ ms}" /></span><script type='math/tex'>t_{settle}\approx5\tau=90\text{ ms}</script></p>
<p>If the divider is permanently connected, the capacitor remains charged and this delay is not usually needed before every measurement. If the divider is switched on only when a reading is required, the firmware should wait approximately 90 to 100 ms before sampling.</p>
<p>The capacitor and divider also create a low-pass filter. Its approximate cutoff frequency is:</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_7b1eed22ba7d7c6e86854fb49f3183a8.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="f_c=\frac{1}{2\pi R_{TH}C}" /></span><script type='math/tex'>f_c=\frac{1}{2\pi R_{TH}C}</script></p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_dd474d35e45eb768446289d4f8b57329.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="f_c=\frac{1}{2\pi(180.3\text{k}\Omega)(0.1\mu\text{F})}\approx8.8\text{ Hz}" /></span><script type='math/tex'>f_c=\frac{1}{2\pi(180.3\text{k}\Omega)(0.1\mu\text{F})}\approx8.8\text{ Hz}</script></p>
<p>This is suitable for battery voltage, temperature, and other slowly changing signals. It may be too slow for audio, vibration, or fast control signals.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Method 1: Reduce the Divider Resistance</strong></h2>
<p>The simplest way to reduce loading error is to use lower resistor values. A 100 kΩ and 22 kΩ divider has the same ratio as the 1 MΩ and 220 kΩ divider, but its Thevenin resistance is only about 18 kΩ.</p>
<table>
<thead>
<tr>
<th>Divider</th>
<th>Current at 15 V</th>
<th>Thevenin Resistance</th>
</tr>
</thead>
<tbody>
<tr>
<td>1 MΩ and 220 kΩ</td>
<td>12.3 µA</td>
<td>180.3 kΩ</td>
</tr>
<tr>
<td>100 kΩ and 22 kΩ</td>
<td>123 µA</td>
<td>18.0 kΩ</td>
</tr>
<tr>
<td>47 kΩ and 10 kΩ</td>
<td>263 µA</td>
<td>8.25 kΩ</td>
</tr>
</tbody>
</table>
<p>The lower-value divider consumes more current, but it is less affected by leakage, PCB contamination, and ADC sampling transients.</p>
<p>For mains-powered equipment, the additional current may not matter. For a battery-powered sensor that sleeps most of the time, a switched divider may provide a better compromise.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Method 2: Use a Switched Voltage Divider</strong></h2>
<p>A switched divider keeps the resistors disconnected while the ESP32-S3 is sleeping. The firmware enables the divider shortly before taking a measurement and disables it afterward.</p>
<p>A high-side P-channel MOSFET or a low-leakage analog load switch can be used to connect the upper resistor to the measured supply. After enabling the circuit, wait for the divider and filter capacitor to settle.</p>
<p>The switch itself must also be selected carefully. MOSFET off-state leakage, gate leakage, and contamination around the ADC node may become significant when using megohm resistors.</p>
<p>A low-side switch should generally be avoided for this application because it can leave the ADC pin connected to the measured voltage through the upper resistor while the lower resistor is disconnected. This can make the pin float, forward-bias protection structures, or create an unexpected current path.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Method 3: Buffer the Signal with an Op-Amp</strong></h2>
<p>For very high-impedance sensors, the best solution is usually a voltage follower.</p>
<p>The sensor or resistor network connects to the op-amp input, while the op-amp output drives the ESP32-S3 ADC. The op-amp presents a very high input resistance to the sensor and a low output resistance to the ADC.</p>
<p>Look for the following characteristics:</p>
<ul>
<li>Very low input bias current</li>
<li>Operation from a 3.3 V supply</li>
<li>Rail-to-rail input and output</li>
<li>Input common-mode range that includes the measured signal</li>
<li>Output swing that covers the required ADC voltage range</li>
<li>Low offset voltage and low drift</li>
<li>Stability when driving a capacitive load</li>
</ul>
<p>A small resistor, typically between 100 Ω and 1 kΩ, can be placed between the op-amp output and the ADC capacitor. This resistor isolates the amplifier from the capacitor and can improve stability.</p>
<p>Some sensor types have an impedance so high that they should never be connected directly to a microcontroller ADC. In such cases, an amplifier designed for extremely low input bias current is required.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Method 4: Improve PCB Layout and Cleanliness</strong></h2>
<p>At megohm impedance levels, the PCB becomes part of the analog circuit. Flux residue, dust, moisture, solder mask contamination, and fingerprints can create leakage paths comparable to the signal current.</p>
<p>Use these layout practices:</p>
<ul>
<li>Keep the high-impedance trace short.</li>
<li>Place the filter capacitor close to the ESP32-S3 pin.</li>
<li>Keep the trace away from switching regulators, clocks, USB, and antenna circuitry.</li>
<li>Avoid routing digital signals parallel to the ADC trace.</li>
<li>Clean flux residue from the board.</li>
<li>Use a ground plane around ordinary low-level analog traces.</li>
<li>For extremely high impedance, consider a driven guard around the sensitive node.</li>
</ul>
<p>Also make sure that internal pull-up and pull-down resistors are disabled on the ADC pin. These resistors are approximately tens of kilohms and will completely disturb a high-value divider.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Choosing the ADC Attenuation</strong></h2>
<p>The ESP32-S3 ADC supports several attenuation settings. Higher attenuation permits a larger voltage at the pin but usually produces more conversion error near the upper end of the range.</p>
<table>
<thead>
<tr>
<th>Arduino Setting</th>
<th>Approximate ESP32-S3 Range</th>
<th>Datasheet Effective Calibrated Range</th>
</tr>
</thead>
<tbody>
<tr>
<td><em>ADC_ATTEN_DB_0</em></td>
<td>Up to approximately 0.95 V</td>
<td>0 to 0.85 V</td>
</tr>
<tr>
<td><em>ADC_ATTEN_DB_2_5</em></td>
<td>Up to approximately 1.25 V</td>
<td>0 to 1.10 V</td>
</tr>
<tr>
<td><em>ADC_ATTEN_DB_6</em></td>
<td>Up to approximately 1.75 V</td>
<td>0 to 1.60 V</td>
</tr>
<tr>
<td><em>ADC_ATTEN_DB_11</em></td>
<td>Up to approximately 3.10 V</td>
<td>0 to 2.90 V</td>
</tr>
</tbody>
</table>
<p>The Arduino API continues to use the name <em>ADC_ATTEN_DB_11</em>, while newer ESP-IDF documentation may describe the highest attenuation as 12 dB.</p>
<p>For best accuracy, do not design the divider so that the maximum expected voltage is exactly at the upper ADC limit. Leave margin for supply variation, resistor tolerance, transients, and calibration error.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Arduino ESP32-S3 ADC Example</strong></h2>
<p>The following sketch reads a high-value 1 MΩ and 220 kΩ divider connected to GPIO4. GPIO4 is an ADC1 pin on the ESP32-S3.</p>
<p>The circuit should include a 0.1 µF ceramic capacitor from GPIO4 to ground.</p>
<pre><pre><code class="language-cpp">#include &lt;Arduino.h&gt;

const uint8_t ADC_PIN = 4;

const float R_TOP = 1000000.0f;
const float R_BOTTOM = 220000.0f;

const uint8_t SAMPLE_COUNT = 32;

uint32_t readAdcMillivolts()
{
    uint64_t total = 0;

    // Dummy conversion to help the ADC input settle.
    (void)analogRead(ADC_PIN);
    delayMicroseconds(500);

    for (uint8_t i = 0; i &lt; SAMPLE_COUNT; i++)
    {
        total += analogReadMilliVolts(ADC_PIN);
        delayMicroseconds(500);
    }

    return (uint32_t)(total / SAMPLE_COUNT);
}

float readInputVoltage()
{
    const uint32_t adcMillivolts = readAdcMillivolts();
    const float adcVoltage = adcMillivolts / 1000.0f;

    return adcVoltage * (R_TOP + R_BOTTOM) / R_BOTTOM;
}

void setup()
{
    Serial.begin(115200);

    analogReadResolution(12);
    analogSetPinAttenuation(ADC_PIN, ADC_ATTEN_DB_11);

    delay(100);
}

void loop()
{
    const uint32_t adcMillivolts = readAdcMillivolts();
    const float inputVoltage =
        (adcMillivolts / 1000.0f) *
        (R_TOP + R_BOTTOM) / R_BOTTOM;

    Serial.printf(
        &quot;ADC: %lu mV, Input: %.3f V\n&quot;,
        (unsigned long)adcMillivolts,
        inputVoltage
    );

    delay(1000);
}</code></pre></pre>
<p>The <em>analogReadMilliVolts()</em> function returns a calibrated voltage rather than an uncorrected raw ADC count. Averaging reduces random noise, while the dummy conversion helps after the input multiplexer or attenuation circuitry has changed state.</p>
<p>The code does not compensate for resistor tolerance, leakage, PCB contamination, or gain and offset error in the complete circuit. Final calibration should be performed using a trusted multimeter or voltage reference.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Two-Point Calibration</strong></h2>
<p>For better accuracy, apply two known input voltages and record the corresponding ESP32-S3 readings. The corrected voltage can then be represented as:</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_49285d1232c2a11d26264dee8f43deaa.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="V_{corrected}=mV_{measured}+b" /></span><script type='math/tex'>V_{corrected}=mV_{measured}+b</script></p>
<p>The gain is:</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_12a6f19256e56bf72976d94897ee7693.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="m=\frac{V_{actual2}-V_{actual1}}{V_{measured2}-V_{measured1}}" /></span><script type='math/tex'>m=\frac{V_{actual2}-V_{actual1}}{V_{measured2}-V_{measured1}}</script></p>
<p>The offset is:</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_486d713efdead5e330ce9b674e16985b.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="b=V_{actual1}-mV_{measured1}" /></span><script type='math/tex'>b=V_{actual1}-mV_{measured1}</script></p>
<p>Store the resulting gain and offset in flash or non-volatile storage. This calibration corrects consistent gain and offset errors, but it cannot fully correct unstable leakage, insufficient settling time, or temperature-dependent errors.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Sampling High-Impedance ADC Channels</strong></h2>
<p>Use the following sampling sequence when reading a high-impedance source:</p>
<ol>
<li>Enable the divider or sensor power, when applicable.</li>
<li>Wait for the external RC network to settle.</li>
<li>Select the ADC channel and attenuation.</li>
<li>Perform one dummy conversion.</li>
<li>Take multiple calibrated readings.</li>
<li>Average or median-filter the samples.</li>
<li>Disable the measurement circuit when finished.</li>
</ol>
<p>Do not sample faster than the source and filter network can settle. Increasing the sample rate does not improve a circuit whose source cannot recharge the ADC input between conversions.</p>
<p>If several ADC channels have very different voltages, consider reading each channel twice and discarding the first conversion. A buffer amplifier for each high-impedance channel provides a more reliable solution.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Wi-Fi and ADC Noise</strong></h2>
<p>ADC1 can be used while Wi-Fi is active, but wireless transmission causes rapidly changing current consumption. These current pulses can introduce noise into the power and ground networks.</p>
<p>For precision measurements:</p>
<ul>
<li>Use ADC1 instead of ADC2.</li>
<li>Add proper supply decoupling near the ESP32-S3.</li>
<li>Place the ADC capacitor close to the pin.</li>
<li>Avoid taking critical readings during Wi-Fi transmission.</li>
<li>Average several readings.</li>
<li>Consider temporarily stopping wireless activity during calibration or precision sampling.</li>
</ul>
<p>The ESP32-S3 datasheet's ADC characterization was performed with a 100 nF capacitor, a DC input signal, an ambient temperature of 25 °C, and Wi-Fi disabled. A real application may therefore show more noise than the published test results.</p>
<p><strong><div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div></strong></p>
<h2><strong>Common ESP32-S3 High-Impedance ADC Problems</strong></h2>
<table>
<thead>
<tr>
<th>Symptom</th>
<th>Likely Cause</th>
<th>Recommended Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td>Reading is consistently low</td>
<td>Leakage or insufficient acquisition settling</td>
<td>Lower source resistance, add a capacitor, or use a buffer</td>
</tr>
<tr>
<td>First reading is different</td>
<td>Residual charge from another channel</td>
<td>Discard the first conversion</td>
</tr>
<tr>
<td>Reading changes when touched</td>
<td>Input is floating or extremely high impedance</td>
<td>Add a defined bias path and buffer the signal</td>
</tr>
<tr>
<td>Channels affect each other</td>
<td>Multiplexer charge and insufficient settling</td>
<td>Slow the sampling sequence or buffer each channel</td>
</tr>
<tr>
<td>Voltage changes in humid conditions</td>
<td>PCB surface leakage</td>
<td>Clean the board and improve guarding</td>
</tr>
<tr>
<td>Large spikes during wireless activity</td>
<td>Supply or ground noise</td>
<td>Improve decoupling, layout, and sampling timing</td>
</tr>
<tr>
<td>Upper voltage range is inaccurate</td>
<td>Operating near the attenuation limit</td>
<td>Change the divider ratio and add voltage margin</td>
</tr>
</tbody>
</table>
<h2>ESP32-S3 High-Impedance ADC Design Checklist</h2>
<ul>
<li>Prefer GPIO1 to GPIO10 on ADC1.</li>
<li>Keep the maximum ADC voltage below the calibrated effective range.</li>
<li>Add approximately 0.1 µF between the ADC pin and ground.</li>
<li>Calculate the divider's Thevenin resistance.</li>
<li>Estimate leakage error using the required voltage accuracy.</li>
<li>Allow at least five external RC time constants after switching the circuit on.</li>
<li>Discard the first reading after changing channels.</li>
<li>Average multiple calibrated readings.</li>
<li>Disable internal pull-up and pull-down resistors.</li>
<li>Keep the ADC trace short and away from noisy signals.</li>
<li>Clean the PCB when using megohm impedances.</li>
<li>Use an op-amp buffer when the sensor impedance is very high.</li>
<li>Calibrate the complete circuit, not only the ADC.</li>
</ul>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Conclusion</strong></h2>
<p>The ESP32-S3 ADC input should not be treated as an ideal infinite-resistance voltmeter. Static leakage current can create voltage error when the source impedance is high, while the ADC's internal sampling network can temporarily load the signal during each conversion.</p>
<p>A 0.1 µF capacitor at the ADC pin is an important starting point and is recommended by Espressif. However, the capacitor must be combined with adequate settling time, suitable divider values, ADC1 channel selection, averaging, careful PCB layout, and calibration.</p>
<p>For battery monitoring and other slowly changing signals, a high-value divider with a local capacitor may provide acceptable performance. For extremely high-impedance sensors or precision measurements, a low-bias-current op-amp buffer is the more reliable solution.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>References</strong></h2>
<ul>
<li><a href="https://www.espressif.com/sites/default/files/documentation/esp32-s3_datasheet_en.pdf">Espressif ESP32-S3 Series Datasheet</a></li>
<li><a href="https://docs.espressif.com/projects/esp-hardware-design-guidelines/en/latest/esp32s3/">ESP32-S3 Hardware Design Guidelines</a></li>
<li><a href="https://docs.espressif.com/projects/arduino-esp32/en/latest/api/adc.html">Arduino-ESP32 ADC API</a></li>
<li><a href="https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-reference/peripherals/adc/adc_calibration.html">ESP32-S3 ADC Calibration Driver</a></li>
</ul>
<p>The post <a href="https://www.teachmemicro.com/esp32-s3-analog-input-leakage-current/">ESP32-S3 Analog Input Leakage Current: High-Impedance ADC Design</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.teachmemicro.com/esp32-s3-analog-input-leakage-current/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Tips to Avoid the Telegram Login Prompt Fees</title>
		<link>https://www.teachmemicro.com/tips-to-avoid-the-telegram-login-prompt-fees/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=tips-to-avoid-the-telegram-login-prompt-fees</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Fri, 24 Jul 2026 10:22:15 +0000</pubDate>
				<category><![CDATA[Features]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=12343</guid>

					<description><![CDATA[<p>Some Telegram users have been encountering a prompt lately asking them to pay a $1 SMS verification fee when they log in to their accounts. And no, not all receive this prompt. It seems to be showing up more depending on the location.  There are users who also see the prompt when their login looks &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/tips-to-avoid-the-telegram-login-prompt-fees/">Tips to Avoid the Telegram Login Prompt Fees</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><span style="font-weight: 400;">Some Telegram users have been encountering a prompt lately asking them to pay a $1 SMS verification fee when they log in to their accounts. And no, not all receive this prompt. It seems to be showing up more depending on the location. </span></p>
<p><span id="more-12343"></span></p>
<p><span style="font-weight: 400;">There are users who also see the prompt when their login looks a bit different than usual or under certain trigger conditions. Still, there are a few legit ways you can lower the odds of seeing that prompt for </span><a href="https://fasttg.com/article/2026-tg-deng-lu-1-mei-yuan-fei-yon"><b>Telegram登</b><b>录</b><b>短信</b><b>费问题</b></a><span style="font-weight: 400;">. If you want to know how, then this article is for you. </span></p>
<h2><strong>Telegram Login Prompts: Reasons Why You’re Seeing Them</strong></h2>
<p><span style="font-weight: 400;">It seems like the login fee prompt is usually connected to specific instances. Telegram may charge a fee in certain regions. That’s where international SMS delivery is pricier. Also, the app security can request additional verification steps. This happens when the system senses a potentially risky sign-in action. </span></p>
<p><span style="font-weight: 400;">The official explanation about the login fee prompt states that this is a one-time charge. If the user is using the same phone number and also the same device, or at least something close to the IP range, they can sign in to the same account again for a longer period after paying that $1 one time fee. There’s a chance that this could trigger again if you keep changing IPs or swapping devices.</span></p>
<h2><strong>How to Avoid Telegram Login Prompts</strong></h2>
<p><span style="font-weight: 400;">Not everyone gets the prompt. Instead, this depends on the account itself and how the login happens. Take these steps to avoid the telegram login prompts:</span></p>
<h3><strong>Use a different Telegram app (client)</strong></h3>
<p><span style="font-weight: 400;">One of the most suggested workarounds is to try another Telegram-compatible client. For Android, Telegram X is a good alternative. For iPhone users, try Nicegram. There's a good chance you will receive the login verification code without firing the SMS fee prompt. However, results are not guaranteed. </span></p>
<h3><strong>Avoid Swapping Devices or Networks</strong></h3>
<p><span style="font-weight: 400;">Telegram’s security may be messed up if you keep changing your login setup. If you bounce between devices a lot, connect through various </span><a href="https://en.wikipedia.org/wiki/Virtual_private_network"><span style="font-weight: 400;">VPN</span></a><span style="font-weight: 400;"> servers, or your IP address changes frequently, it can increase the odds of extra verification steps. This can trigger the login fee prompt.</span></p>
<h3><strong>Enroll a Recovery Email</strong></h3>
<p><span style="font-weight: 400;">Once you’re logged in, add a recovery email address for your Telegram account. A confirmed email gives Telegram another way to verify it really is you the next time you try to log in. In many cases, email confirmation is frequently used instead of SMS. This can help you avoid the login verification fees.</span></p>
<h3><strong>Turn On Passkeys For Later Logins</strong></h3>
<p><span style="font-weight: 400;">Telegram also supports passkeys. This allows you passwordless sign-ins. It uses the security tools that you already have on your device. Passkeys can make future sign-ins easier while also reducing your reliance on SMS verification. </span></p>
<h2><strong>Try Alternatives Before Paying the Fee</strong></h2>
<p><span style="font-weight: 400;">If the payment pop-up appears, it’s often smart to try the listed alternatives first before paying anything. Some users say the issue went away after switching clients or changing the way they log in, no payment needed. Also, there are claims that refunds might not be offered if you pay the fee but still can’t get the verification code. So, trying the free options first is a good way to prevent making the payment.</span></p>
<h2><strong>Conclusion</strong></h2>
<p><span style="font-weight: 400;">Even if the Telegram login fee doesn't affect everyone, it’s useful to understand why it appears so you can respond appropriately. Take note of the suggestions above. Instead of paying immediately, these tips might reduce the risk of incurring the SMS verification fee. Also, staying updated on Telegram’s newer login and security options can make account access feel easier.</span></p>
<p>The post <a href="https://www.teachmemicro.com/tips-to-avoid-the-telegram-login-prompt-fees/">Tips to Avoid the Telegram Login Prompt Fees</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>The Tech Founder&#039;s Guide to Automating Paid Search with APIs and Web Scraping</title>
		<link>https://www.teachmemicro.com/the-tech-founders-guide-to-automating-paid-search-with-apis-and-web-scraping/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=the-tech-founders-guide-to-automating-paid-search-with-apis-and-web-scraping</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Thu, 23 Jul 2026 12:22:11 +0000</pubDate>
				<category><![CDATA[Features]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=12337</guid>

					<description><![CDATA[<p>Tech founders who build custom electronics, IoT projects, and hardware startups often hit a wall when it comes to customer acquisition. Engineering a brilliant product is only half the battle. Scaling market visibility requires a completely different type of architecture, but it is one that modern developers are uniquely equipped to handle. Australia's digital advertising &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/the-tech-founders-guide-to-automating-paid-search-with-apis-and-web-scraping/">The Tech Founder&#039;s Guide to Automating Paid Search with APIs and Web Scraping</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Tech founders who build custom electronics, IoT projects, and hardware startups often hit a wall when it comes to customer acquisition. Engineering a brilliant product is only half the battle. Scaling market visibility requires a completely different type of architecture, but it is one that modern developers are uniquely equipped to handle.</p>
<p><span id="more-12337"></span></p>
<p>Australia's digital advertising market has seen massive growth, reaching a record $4.9 billion in the first quarter of 2026 alone. Search advertising remains the dominant digital channel, accounting for roughly 44 percent of that expenditure. With total ad spend forecast to hit US$16.88 billion by the end of the year, there are massive scaling opportunities for tech startups that know how to manipulate marketing data. Today, local founders are treating their advertising budgets like software deployments, using application programming interfaces and automated data pipelines to effectively steer commercial algorithms.</p>
<h2>Bridging the Gap Between Scraping and Advertising</h2>
<p>Major platforms have largely removed manual bid controls as of 2026, transitioning entirely to AI-focused campaign types. As a result, programmatic automation has become essential for properly guiding algorithmic ad spend. However, automation algorithms are strictly dependent on pristine data.</p>
<p>Because official developer endpoints do not always return visual search layouts or localised competitor context, tech founders must actively scrape search engines to extract real-time placement data and AI Overview intelligence. Just as developers can engineer custom SEO tracking tools to monitor local ranking shifts, similar scraping principles can be adapted to fuel high-return paid search campaigns.</p>
<p>Extracting this ad intelligence comes with technical hurdles. Search engines have deployed aggressive bot mitigation tactics recently, meaning developers must utilise headless browsers, proxy rotation, and sophisticated stealth plugins to avoid IP bans while gathering their market research.</p>
<h2>Architecting the Automated Campaign Pipeline</h2>
<p>It is one thing to scrape data, but deploying it for commercial gain is another challenge entirely. Search marketing experts note that faulty web scraping or broken conversion tracking will actively misdirect machine learning budgets, wasting crucial startup capital.</p>
<p>Many founders prefer to build the initial data extraction infrastructure internally but rely on external specialists for the actual execution. For example, a tech startup might supply the raw backend infrastructure, but then partner with a specialised <a style="text-decoration: none;" href="https://dnmdigital.com.au/ppc-agency/google-ads/">google ads agency Sydney</a> to deploy that intelligence into highly optimised regional campaigns. This ensures the extracted data is properly formatted for commercial bidding algorithms.</p>
<p>When architecting an automated ad pipeline, developers should focus on several core technical upgrades:</p>
<ul>
<li>Transitioning legacy scripts to the V8 JavaScript engine, which enables significantly faster execution and the ability to write modern JavaScript for advanced bidding logic.</li>
<li>Utilising robust frameworks like Playwright for web scraping. Playwright has largely overtaken Puppeteer for enterprise-grade scraping in 2026 due to its native support for isolated browser contexts across Python, Java, and .NET.</li>
<li>Leveraging the updated Google Ads API monthly release cycle to access incremental automation features without breaking existing code.</li>
<li>Integrating comprehensive performance metrics across Performance Max networks to give internal automation scripts richer conversion data.</li>
</ul>
<h2>The Financial Impact of Data-Driven Personalisation</h2>
<p>The ultimate goal of connecting custom data extraction to an ad platform is operational efficiency. Manual campaign monitoring used to consume over 15 hours a week for marketing teams. By deploying API-based automation tools, tech startups can reduce that routine workload down to just two to three hours a week, freeing up time for product development.</p>
<p>Beyond saving time, granular data allows for automated triggers that speak directly to user behaviour at an individual level. According to McKinsey research, implementing data-driven personalisation at scale can reduce acquisition costs by up to 50 percent while improving overall marketing spend efficiency by 10 to 30 percent. Recent case studies from digital agencies mirror this, demonstrating that pairing granular feed data with automated bidding strategies can increase conversions by over 1,700 percent within a 90-day period.</p>
<p>For today's tech founder, mastering customer acquisition is less about traditional copywriting and more about building robust data feedback loops. By bridging the gap between sophisticated web scraping, modern APIs, and expert campaign management, hardware startups can ensure every advertising dollar works harder to capture local market share.</p>
<p>The post <a href="https://www.teachmemicro.com/the-tech-founders-guide-to-automating-paid-search-with-apis-and-web-scraping/">The Tech Founder&#039;s Guide to Automating Paid Search with APIs and Web Scraping</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Introducing the PIC16F84A Instruction Trace Tool</title>
		<link>https://www.teachmemicro.com/introducing-the-pic16f84a-instruction-trace-tool/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=introducing-the-pic16f84a-instruction-trace-tool</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Thu, 23 Jul 2026 02:04:25 +0000</pubDate>
				<category><![CDATA[Features]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=12329</guid>

					<description><![CDATA[<p>Learning PIC assembly is easier when you can see what happens inside the microcontroller. The problem is that most tutorials show the instruction, the result, and maybe the affected register, but not the path the instruction takes inside the chip. That is what the new PIC16F84A Instruction Trace tool in PICSimulator.com is for. With this tool, &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/introducing-the-pic16f84a-instruction-trace-tool/">Introducing the PIC16F84A Instruction Trace Tool</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Learning PIC assembly is easier when you can see what happens inside the microcontroller. The problem is that most tutorials show the instruction, the result, and maybe the affected register, but not the path the instruction takes inside the chip. That is what the new <a href="https://picsimulator.com/instruction-trace.html"><strong>PIC16F84A Instruction Trace</strong></a> tool in PICSimulator.com is for.</p>
<p style="text-align: left;"><span id="more-12329"></span></p>
<p><img data-dominant-color="ebf0ee" data-has-transparency="false" style="--dominant-color: #ebf0ee;" loading="lazy" decoding="async" class="aligncenter size-large wp-image-12331 not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2026/07/pic-instruction-trace-1-1024x587.avif" alt="" width="618" height="354" srcset="https://www.teachmemicro.com/wp-content/uploads/2026/07/pic-instruction-trace-1-1024x587.avif 1024w, https://www.teachmemicro.com/wp-content/uploads/2026/07/pic-instruction-trace-1-300x172.avif 300w, https://www.teachmemicro.com/wp-content/uploads/2026/07/pic-instruction-trace-1-768x440.avif 768w, https://www.teachmemicro.com/wp-content/uploads/2026/07/pic-instruction-trace-1.avif 1470w" sizes="auto, (max-width: 618px) 100vw, 618px" /><br />
With this tool, you can enter a PIC assembly instruction such as:</p>
<pre><pre><code class="language-cpp">MOVLW 0x02</code></pre></pre>
<p>and see how the PIC16F84A fetches the instruction from program memory, places it into the instruction register, decodes the instruction bits, and routes the data through internal blocks such as the W register, RAM file registers, ALU, STATUS register, stack, program counter, and buses.</p>
<p>This is useful especially for beginners who are still trying to understand how PIC assembly instructions actually work.</p>
<h2>Contents</h2>
<ul>
<li>What the Instruction Trace Tool Does</li>
<li>Why This Is Useful</li>
<li>Opening the Tool</li>
<li>Example 1: Tracing <em>MOVLW 0x02</em></li>
<li>Example 2: Tracing <em>MOVWF 0x0C</em></li>
<li>Example 3: Tracing <em>CLRF 0x0C</em></li>
<li>Using the Animate Button</li>
<li>Understanding the Decoded Word</li>
<li>Conclusion</li>
</ul>
<h2>What the Instruction Trace Tool Does</h2>
<p>The PIC16F84A has only 35 instructions, which is one reason why it is a good microcontroller for beginners. However, each instruction still has its own binary format.</p>
<p>For example, <em>MOVLW 0x02</em> is not stored inside program memory as text. The PIC does not see the letters M-O-V-L-W. Instead, the assembler converts it into a 14-bit instruction word:</p>
<pre><pre><code class="language-cpp">11000000000010</code></pre></pre>
<p>The Instruction Trace tool shows this word visually. It also shows which bits represent the instruction opcode and which bits represent the operand.</p>
<p>For <em>MOVLW 0x02</em>, the lower 8 bits contain the literal value:</p>
<pre><pre><code class="language-cpp">00000010</code></pre></pre>
<p>This is the value that will be loaded into the W register.</p>
<h2>Why This Is Useful</h2>
<p>When programming in assembly, you are working close to the hardware. A single instruction may affect the W register, file registers, program counter, stack, or STATUS flags.</p>
<p>For beginners, this can be confusing because the effect of an instruction is not always obvious just by reading the code.</p>
<p>The trace tool helps by showing:</p>
<ul>
<li>the fetched 14-bit instruction word</li>
<li>the decoded opcode and operand fields</li>
<li>the active internal blocks</li>
<li>the data path used by the instruction</li>
<li>the final value of W, file registers, STATUS flags, or PC</li>
<li>the instruction cycle count</li>
<li>the result of the instruction</li>
</ul>
<p>Instead of only reading what an instruction does, you can now watch how the PIC processes it internally.</p>
<h2>Opening the Tool</h2>
<p>Go to:</p>
<p><a href="https://picsimulator.com/instruction-trace.html">https://picsimulator.com/instruction-trace.html</a></p>
<p>You will see an input field for the assembly instruction, state fields for initial register values, the internal PIC16F84A block diagram, and a decoded instruction word section.</p>
<p><img data-dominant-color="f7f9fa" data-has-transparency="false" style="--dominant-color: #f7f9fa;" loading="lazy" decoding="async" class="aligncenter wp-image-12332 size-full not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2026/07/pic-instruction-trace-0.avif" alt="" width="1519" height="145" srcset="https://www.teachmemicro.com/wp-content/uploads/2026/07/pic-instruction-trace-0.avif 1519w, https://www.teachmemicro.com/wp-content/uploads/2026/07/pic-instruction-trace-0-300x29.avif 300w, https://www.teachmemicro.com/wp-content/uploads/2026/07/pic-instruction-trace-0-1024x98.avif 1024w, https://www.teachmemicro.com/wp-content/uploads/2026/07/pic-instruction-trace-0-768x73.avif 768w" sizes="auto, (max-width: 1519px) 100vw, 1519px" /></p>
<p>To begin, type an instruction into the ASM command field.</p>
<h2>Example 1: Tracing MOVLW 0x02</h2>
<p>&nbsp;</p>
<p>Enter this command:</p>
<pre><pre><code class="language-cpp">MOVLW 0x02</code></pre></pre>
<p><em>MOVLW</em> means “move literal to W”. The value <pre><code class="language-cpp">0x02</code></pre> is the literal value.</p>
<p>The tool shows the instruction word:</p>
<pre><pre><code class="language-cpp">11000000000010</code></pre></pre>
<p>This is fetched from Flash Program Memory and moved into the Instruction Register.</p>
<p>The instruction is then decoded:</p>
<pre><pre><code class="language-cpp">opcode = 110000
k      = 00000010</code></pre></pre>
<p>Here, <em>k</em> is the literal value. Since <pre><code class="language-cpp">00000010</code></pre> is equal to <em>0x02</em>, the PIC loads <em>0x02</em> into the W register.</p>
<p>After execution:</p>
<pre><pre><code class="language-cpp">W = 0x02</code></pre></pre>
<p>In the block diagram, you should see the instruction move from program memory to the instruction register, then through the decode/control path, and finally to the W register.</p>
<h2>Example 2: Tracing MOVWF 0x0C</h2>
<p>Now enter:</p>
<pre><pre><code class="language-cpp">MOVWF 0x0C</code></pre></pre>
<p><em>MOVWF</em> means “move W to file register”.</p>
<p>Before running the trace, set the initial W value to something easy to see, for example:</p>
<pre><pre><code class="language-cpp">Initial W = 0x35</code></pre></pre>
<p>The instruction tells the PIC to copy the contents of W into file register address <pre><code class="language-cpp">0x0C</code></pre>.</p>
<p>After execution:</p>
<pre><pre><code class="language-cpp">File register 0x0C = 0x35</code></pre></pre>
<p>This is a good instruction for learning how the W register and RAM file registers work together. The W register is not a normal RAM location, but many PIC instructions use it as the main working register.</p>
<h2>Example 3: Tracing CLRF 0x0C</h2>
<p>Next, enter:</p>
<pre><pre><code class="language-cpp">CLRF 0x0C</code></pre></pre>
<p><em>CLRF</em> means “clear file register”.</p>
<p>This instruction writes <pre><code class="language-cpp">0x00</code></pre> to the selected file register. It also affects the Zero flag in the STATUS register.</p>
<p>After execution:</p>
<pre><pre><code class="language-cpp">File register 0x0C = 0x00
STATUS.Z = 1</code></pre></pre>
<p>This is because the result of the operation is zero.</p>
<p>This is one of the important things to remember when writing PIC assembly: some instructions do more than just change a register. They may also affect STATUS flags, which can then affect later conditional instructions.</p>
<h2>Using the Animate Button</h2>
<p>The tool also includes an <strong>Animate</strong> button.</p>
<p>Clicking this button steps through the instruction flow automatically. This is useful if you want to follow the operation in order:</p>
<ol start="1">
<li>Program Counter points to the instruction address.</li>
<li>Flash Program Memory outputs the instruction word.</li>
<li>Instruction Register receives the 14-bit word.</li>
<li>Decode and Control separates the opcode and operand fields.</li>
<li>The required internal blocks become active.</li>
<li>The result is written to W, RAM, STATUS, PC, or another destination.</li>
</ol>
<p>You can also use the step buttons to move through the trace manually.</p>
<h2>Understanding the Decoded Word</h2>
<p>At the bottom of the internal block diagram, the tool shows the decoded word.</p>
<p>This section is important because it connects assembly language with machine language.</p>
<p>For example:</p>
<pre><pre><code class="language-cpp">MOVLW 0x02</code></pre></pre>
<p>becomes:</p>
<pre><pre><code class="language-cpp">11000000000010</code></pre></pre>
<p>The first bits identify the instruction, while the remaining bits contain the literal value.</p>
<p>For file register instructions such as:</p>
<pre><pre><code class="language-cpp">MOVWF 0x0C</code></pre></pre>
<p>the decoded word contains the file register address. This lets you see where the address bits are located inside the instruction.</p>
<p>For bit-oriented instructions, the decoded word also shows the selected bit number.</p>
<h2>Conclusion</h2>
<p>The PIC16F84A Instruction Trace tool is designed to make PIC assembly less mysterious. Instead of memorizing instructions only from a table, you can now see how each instruction travels through the internal parts of the microcontroller.</p>
<p>This is especially helpful when learning instructions like <em>MOVLW</em>, <em>MOVWF</em>, <em>CLRF</em>, bit tests, jumps, calls, and instructions that affect the STATUS register.</p>
<p>Try entering different PIC16F84A assembly commands and watch how the internal blocks respond. Once you understand the path of each instruction, writing and debugging PIC assembly becomes much easier.</p>
<p>The post <a href="https://www.teachmemicro.com/introducing-the-pic16f84a-instruction-trace-tool/">Introducing the PIC16F84A Instruction Trace Tool</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Linked List Visualizer for Embedded Systems</title>
		<link>https://www.teachmemicro.com/linked-list-visualizer-for-embedded-systems/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=linked-list-visualizer-for-embedded-systems</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Tue, 21 Jul 2026 20:34:46 +0000</pubDate>
				<category><![CDATA[Tools]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=12317</guid>

					<description><![CDATA[<p>A linked list in embedded systems is a collection of data elements connected using pointers. Unlike an array, the elements do not need to occupy consecutive locations in memory. Each element stores its own data and a pointer that identifies the next element in the list. Linked lists are common in general-purpose software, but using &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/linked-list-visualizer-for-embedded-systems/">Linked List Visualizer for Embedded Systems</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>A <strong>linked list in embedded systems</strong> is a collection of data elements connected using pointers. Unlike an array, the elements do not need to occupy consecutive locations in memory. Each element stores its own data and a pointer that identifies the next element in the list.</p>
<p>Linked lists are common in general-purpose software, but using one in a microcontroller requires extra care. Dynamic memory allocation, memory fragmentation, pointer errors, and unpredictable execution time can all become important concerns when RAM is limited.</p>
<p>In this tutorial, I will explain how a linked list works, show how nodes are connected, and build a practical singly linked list in embedded C. You can also use the visualizer below to add, remove, and traverse nodes while watching the pointers change.</p>
<p><span id="more-12317"></span></p>
<h2><strong>Interactive Linked List Visualizer</strong></h2>
<p>Use the controls below to add nodes, remove nodes, and move through the list. Each box represents one node. The arrow leaving a node represents its <em>next</em> pointer.</p>
<p>Pay particular attention to the first node, called the <em>head</em>, and to the final node whose <em>next</em> pointer is <em>NULL</em>.</p>
        <div
            id="tmm-llv-1"
            class="tmm-llv-app"
            data-llv-instance="tmm-llv-1"
            data-llv-max-nodes="6"
            data-llv-initial-values=""
        >
            <section class="tmm-llv-card" aria-labelledby="tmm-llv-1-title">
                <noscript>
                    <div class="tmm-llv-noscript">
                        JavaScript is required for the interactive linked list visualizer.                    </div>
                </noscript>

                <div class="tmm-llv-topline">
                    <h3 id="tmm-llv-1-title">
                        Linked List                    </h3>
                    <p>
                        Singly linked nodes with head, tail, and next pointers.                    </p>
                </div>

                <div class="tmm-llv-stage">
                    <div
                        class="tmm-llv-list"
                        data-llv-el="list"
                        role="img"
                        aria-label="Singly linked list nodes with next pointers"
                    ></div>
                </div>

                <div class="tmm-llv-legend" aria-label="Pointer legend">
                    <span><strong class="tmm-llv-head-text">HEAD</strong>: first node</span>
                    <span><strong class="tmm-llv-tail-text">TAIL</strong>: last node</span>
                    <span><strong>next</strong>: pointer to the following node or NULL</span>
                </div>

                <div class="tmm-llv-controls">
                    <label class="tmm-llv-value-field" for="tmm-llv-1-value">
                        <span>Node value</span>
                        <input
                            id="tmm-llv-1-value"
                            data-llv-el="value-input"
                            type="text"
                            inputmode="text"
                            autocomplete="off"
                            maxlength="6"
                            placeholder="A or 0x41"
                        >
                    </label>
                    <button class="tmm-llv-button" type="button" data-llv-action="insert-head">
                        Insert Head                    </button>
                    <button class="tmm-llv-button" type="button" data-llv-action="insert-tail">
                        Insert Tail                    </button>
                    <button class="tmm-llv-button tmm-llv-button--secondary" type="button" data-llv-action="delete-head">
                        Delete Head                    </button>
                    <button class="tmm-llv-button tmm-llv-button--secondary" type="button" data-llv-action="delete-value">
                        Delete Value                    </button>
                    <button class="tmm-llv-button tmm-llv-button--secondary" type="button" data-llv-action="find">
                        Find                    </button>
                    <button class="tmm-llv-button tmm-llv-button--muted" type="button" data-llv-action="reset">
                        Reset                    </button>
                </div>

                <div class="tmm-llv-status" id="tmm-llv-1-status" data-llv-el="status">
                    <span>Head: <strong data-llv-el="head">NULL</strong></span>
                    <span>Tail: <strong data-llv-el="tail">NULL</strong></span>
                    <span>Nodes: <strong data-llv-el="count">0 / 6</strong></span>
                    <span>State: <strong data-llv-el="state">EMPTY</strong></span>
                </div>

                <p
                    class="tmm-llv-message"
                    id="tmm-llv-1-message"
                    data-llv-el="message"
                    aria-live="polite"
                >
                    List is empty. Insert a node to begin.                </p>

                <details class="tmm-llv-code" aria-labelledby="tmm-llv-1-code-title">
                    <summary id="tmm-llv-1-code-title">
                        Equivalent Embedded C                    </summary>
                    <button class="tmm-llv-button tmm-llv-button--copy" type="button" data-llv-action="copy-code">
                        Copy Code                    </button>
                    <pre><code data-llv-el="code">#define LIST_MAX_NODES 6

typedef struct Node
{
    uint8_t value;
    struct Node *next;
} Node;

typedef struct
{
    Node *head;
    Node *tail;
    uint8_t count;
} LinkedList;

bool list_push_front(LinkedList *list, Node *node, uint8_t value)
{
    if (list-&gt;count &gt;= LIST_MAX_NODES)
    {
        return false;
    }

    node-&gt;value = value;
    node-&gt;next = list-&gt;head;
    list-&gt;head = node;

    if (list-&gt;tail == NULL)
    {
        list-&gt;tail = node;
    }

    list-&gt;count++;
    return true;
}

bool list_push_back(LinkedList *list, Node *node, uint8_t value)
{
    if (list-&gt;count &gt;= LIST_MAX_NODES)
    {
        return false;
    }

    node-&gt;value = value;
    node-&gt;next = NULL;

    if (list-&gt;tail != NULL)
    {
        list-&gt;tail-&gt;next = node;
    }
    else
    {
        list-&gt;head = node;
    }

    list-&gt;tail = node;
    list-&gt;count++;
    return true;
}

bool list_pop_front(LinkedList *list, uint8_t *value)
{
    Node *old_head;

    if (list-&gt;head == NULL)
    {
        return false;
    }

    old_head = list-&gt;head;
    *value = old_head-&gt;value;
    list-&gt;head = old_head-&gt;next;

    if (list-&gt;head == NULL)
    {
        list-&gt;tail = NULL;
    }

    list-&gt;count--;
    return true;
}</code></pre>
                </details>
            </section>
        </div>
        
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>What Is a Linked List?</strong></h2>
<p>A linked list is a data structure made from individual elements called <strong>nodes</strong>. Each node normally contains two parts:</p>
<ul>
<li>The data stored by the node</li>
<li>A pointer to another node</li>
</ul>
<p>A basic singly linked-list node in C may look like this:</p>
<pre><pre><code class="language-cpp">typedef struct Node
{
    uint8_t value;
    struct Node *next;
} Node;</code></pre></pre>
<p>The <em>value</em> member stores the actual information. The <em>next</em> member stores the address of the following node.</p>
<p>Suppose three nodes contain the values 10, 20, and 30. Their relationship can be represented as:</p>
<pre><pre><code class="language-cpp">head
  |
  v
[10 | next] -&gt; [20 | next] -&gt; [30 | NULL]</code></pre></pre>
<p>The program starts at <em>head</em>, follows the first node’s <em>next</em> pointer, and continues until it reaches <em>NULL</em>.</p>
<p>The final node points to <em>NULL</em> because there is no node after it.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Linked List Versus Array</strong></h2>
<p>An array stores its elements next to one another in memory.</p>
<pre><pre><code class="language-cpp">uint8_t values[4] = {10, 20, 30, 40};</code></pre></pre>
<p>The elements occupy consecutive positions:</p>
<pre><pre><code class="language-cpp">Address:  0x2000  0x2001  0x2002  0x2003
Value:       10      20      30      40</code></pre></pre>
<p>This makes accessing a specific array element fast. The processor can calculate the address of <em>values[2]</em> directly.</p>
<p>Linked-list nodes may be located in different parts of memory:</p>
<pre><pre><code class="language-cpp">Node 1 at 0x2000
Node 2 at 0x2140
Node 3 at 0x2088</code></pre></pre>
<p>The nodes remain connected because each one stores the address of the next node.</p>
<div style="overflow-x: auto;">
<table>
<thead>
<tr>
<th>Feature</th>
<th>Array</th>
<th>Linked List</th>
</tr>
</thead>
<tbody>
<tr>
<td>Memory layout</td>
<td>Contiguous</td>
<td>Nodes may be separated</td>
</tr>
<tr>
<td>Direct element access</td>
<td>Fast by index</td>
<td>Requires traversal</td>
</tr>
<tr>
<td>Insert at beginning</td>
<td>May require shifting</td>
<td>Pointer update only</td>
</tr>
<tr>
<td>Remove from beginning</td>
<td>May require shifting</td>
<td>Pointer update only</td>
</tr>
<tr>
<td>Memory overhead</td>
<td>Data only</td>
<td>Each node requires a pointer</td>
</tr>
<tr>
<td>Cache locality</td>
<td>Usually good</td>
<td>Usually worse</td>
</tr>
<tr>
<td>Maximum size</td>
<td>Often fixed</td>
<td>Depends on node allocation</td>
</tr>
</tbody>
</table>
</div>
<p>A linked list is useful when elements must frequently be inserted or removed without shifting the remaining data. An array is usually better when fast indexed access and compact memory usage are more important.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>The Head Pointer</strong></h2>
<p>The <em>head</em> pointer identifies the first node in the list.</p>
<pre><pre><code class="language-cpp">Node *head = NULL;</code></pre></pre>
<p>When <em>head</em> is <em>NULL</em>, the list is empty.</p>
<p>After adding a node:</p>
<pre><pre><code class="language-cpp">Node first_node;

first_node.value = 10;
first_node.next = NULL;

head = &amp;first_node;</code></pre></pre>
<p>The head now stores the address of <em>first_node</em>.</p>
<pre><pre><code class="language-cpp">head -&gt; [10 | NULL]</code></pre></pre>
<p>The head is not a node itself. It is a pointer that tells the program where the list begins.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>How Linked-List Traversal Works</strong></h2>
<p>Unlike an array, a linked list cannot directly jump to an arbitrary element by index. The program must begin at the head and follow each <em>next</em> pointer.</p>
<pre><pre><code class="language-cpp">void linked_list_print(const Node *head)
{
    const Node *current = head;

    while (current != NULL)
    {
        printf(&quot;%u\n&quot;, current-&gt;value);
        current = current-&gt;next;
    }
}</code></pre></pre>
<p>The temporary pointer <em>current</em> begins at the first node. After processing that node, it becomes the address stored in <em>current-&gt;next</em>.</p>
<p>The traversal ends when <em>current</em> becomes <em>NULL</em>.</p>
<p>For a list containing four nodes, reaching the final node may require following three pointers. This means finding an element becomes slower as the list grows.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Creating a Basic Singly Linked List</strong></h2>
<p>A singly linked list allows movement in one direction only. Each node points to the node after it.</p>
<p>We can begin with this data structure:</p>
<pre><pre><code class="language-cpp">#include &lt;stdbool.h&gt;
#include &lt;stddef.h&gt;
#include &lt;stdint.h&gt;

typedef struct Node
{
    uint8_t value;
    struct Node *next;
} Node;

typedef struct
{
    Node *head;
    uint16_t count;
} LinkedList;</code></pre></pre>
<p>The <em>LinkedList</em> structure stores:</p>
<ul>
<li>A pointer to the first node</li>
<li>The number of nodes currently in the list</li>
</ul>
<h3><strong>Initializing the List</strong></h3>
<pre><pre><code class="language-cpp">void linked_list_init(LinkedList *list)
{
    list-&gt;head = NULL;
    list-&gt;count = 0U;
}</code></pre></pre>
<p>An initialized list contains no nodes, so the head is set to <em>NULL</em>.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Adding a Node at the Beginning</strong></h2>
<p>Adding a node at the beginning is one of the simplest linked-list operations.</p>
<p>Suppose the current list is:</p>
<pre><pre><code class="language-cpp">head -&gt; [20] -&gt; [30] -&gt; NULL</code></pre></pre>
<p>To insert a node containing 10:</p>
<ol>
<li>Make the new node point to the current head.</li>
<li>Change the head so it points to the new node.</li>
</ol>
<pre><pre><code class="language-cpp">bool linked_list_push_front(
    LinkedList *list,
    Node *node,
    uint8_t value)
{
    if ((list == NULL) || (node == NULL))
    {
        return false;
    }

    node-&gt;value = value;
    node-&gt;next = list-&gt;head;

    list-&gt;head = node;
    list-&gt;count++;

    return true;
}</code></pre></pre>
<p>After the operation:</p>
<pre><pre><code class="language-cpp">head -&gt; [10] -&gt; [20] -&gt; [30] -&gt; NULL</code></pre></pre>
<p>The existing nodes do not move in memory. Only two pointer values change.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Adding a Node at the End</strong></h2>
<p>Adding at the end requires finding the final node unless the list also stores a tail pointer.</p>
<pre><pre><code class="language-cpp">bool linked_list_push_back(
    LinkedList *list,
    Node *node,
    uint8_t value)
{
    Node *current;

    if ((list == NULL) || (node == NULL))
    {
        return false;
    }

    node-&gt;value = value;
    node-&gt;next = NULL;

    if (list-&gt;head == NULL)
    {
        list-&gt;head = node;
        list-&gt;count++;
        return true;
    }

    current = list-&gt;head;

    while (current-&gt;next != NULL)
    {
        current = current-&gt;next;
    }

    current-&gt;next = node;
    list-&gt;count++;

    return true;
}</code></pre></pre>
<p>If the list is empty, the new node becomes the head. Otherwise, the function traverses the list until it finds the node whose <em>next</em> pointer is <em>NULL</em>.</p>
<p>This operation takes longer as the list grows. Storing a separate tail pointer can make insertion at the end much faster.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Removing the First Node</strong></h2>
<p>To remove the first node:</p>
<ol>
<li>Save a pointer to the current head.</li>
<li>Move the head to the second node.</li>
<li>Disconnect the removed node.</li>
</ol>
<pre><pre><code class="language-cpp">Node *linked_list_pop_front(LinkedList *list)
{
    Node *removed;

    if ((list == NULL) || (list-&gt;head == NULL))
    {
        return NULL;
    }

    removed = list-&gt;head;
    list-&gt;head = removed-&gt;next;

    removed-&gt;next = NULL;
    list-&gt;count--;

    return removed;
}</code></pre></pre>
<p>If the original list is:</p>
<pre><pre><code class="language-cpp">head -&gt; [10] -&gt; [20] -&gt; [30] -&gt; NULL</code></pre></pre>
<p>the result becomes:</p>
<pre><pre><code class="language-cpp">head -&gt; [20] -&gt; [30] -&gt; NULL

removed -&gt; [10] -&gt; NULL</code></pre></pre>
<p>The function returns the removed node so the application can reuse it or release it through the appropriate memory-management method.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Removing a Node by Value</strong></h2>
<p>Removing a node from the middle requires keeping track of both the current node and the node before it.</p>
<pre><pre><code class="language-cpp">Node *linked_list_remove(
    LinkedList *list,
    uint8_t value)
{
    Node *current;
    Node *previous = NULL;

    if (list == NULL)
    {
        return NULL;
    }

    current = list-&gt;head;

    while (current != NULL)
    {
        if (current-&gt;value == value)
        {
            if (previous == NULL)
            {
                list-&gt;head = current-&gt;next;
            }
            else
            {
                previous-&gt;next = current-&gt;next;
            }

            current-&gt;next = NULL;
            list-&gt;count--;

            return current;
        }

        previous = current;
        current = current-&gt;next;
    }

    return NULL;
}</code></pre></pre>
<p>If the first node matches, the head changes. If a middle node matches, the previous node is connected directly to the node after the removed one.</p>
<p>For example:</p>
<pre><pre><code class="language-cpp">Before:

[10] -&gt; [20] -&gt; [30] -&gt; NULL

Remove 20:

[10] ---------&gt; [30] -&gt; NULL</code></pre></pre>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Finding a Value</strong></h2>
<pre><pre><code class="language-cpp">Node *linked_list_find(
    LinkedList *list,
    uint8_t value)
{
    Node *current;

    if (list == NULL)
    {
        return NULL;
    }

    current = list-&gt;head;

    while (current != NULL)
    {
        if (current-&gt;value == value)
        {
            return current;
        }

        current = current-&gt;next;
    }

    return NULL;
}</code></pre></pre>
<p>The function returns a pointer to the matching node or <em>NULL</em> when the value is not present.</p>
<p>Searching a linked list requires checking nodes one at a time. For large collections requiring frequent searches, another data structure may be more suitable.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Complete Dynamic Allocation Example</strong></h2>
<p>On a desktop computer, linked-list nodes are commonly created using dynamic allocation.</p>
<pre><pre><code class="language-cpp">#include &lt;stdbool.h&gt;
#include &lt;stdint.h&gt;
#include &lt;stdlib.h&gt;

typedef struct Node
{
    uint8_t value;
    struct Node *next;
} Node;

typedef struct
{
    Node *head;
    uint16_t count;
} LinkedList;

bool linked_list_add(
    LinkedList *list,
    uint8_t value)
{
    Node *node;

    if (list == NULL)
    {
        return false;
    }

    node = malloc(sizeof(Node));

    if (node == NULL)
    {
        return false;
    }

    node-&gt;value = value;
    node-&gt;next = list-&gt;head;

    list-&gt;head = node;
    list-&gt;count++;

    return true;
}

bool linked_list_remove_first(
    LinkedList *list,
    uint8_t *value)
{
    Node *removed;

    if ((list == NULL) ||
        (list-&gt;head == NULL))
    {
        return false;
    }

    removed = list-&gt;head;
    list-&gt;head = removed-&gt;next;

    if (value != NULL)
    {
        *value = removed-&gt;value;
    }

    free(removed);
    list-&gt;count--;

    return true;
}</code></pre></pre>
<p>This code works, but using <em>malloc()</em> and <em>free()</em> may not be the best option for every embedded system.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Why Dynamic Allocation Can Be a Problem</strong></h2>
<p>Microcontrollers often have limited RAM and may need to operate continuously for months or years. Repeated allocation and release can divide the available heap into small separated areas.</p>
<p>This condition is called <strong>memory fragmentation</strong>.</p>
<p>Suppose a device has several free memory regions:</p>
<pre><pre><code class="language-cpp">Free blocks: 16 bytes, 12 bytes, 20 bytes</code></pre></pre>
<p>The total free memory is 48 bytes, but an attempt to allocate one 32-byte block may fail because no individual free region is large enough.</p>
<p>Dynamic allocation can introduce other concerns:</p>
<ul>
<li>Allocation may fail at runtime.</li>
<li>Execution time may be difficult to predict.</li>
<li>Memory leaks can slowly consume the heap.</li>
<li>Double-free errors can corrupt memory.</li>
<li>Using freed nodes can cause unpredictable behavior.</li>
<li>Debugging heap corruption on a microcontroller can be difficult.</li>
</ul>
<p>Dynamic memory is not automatically forbidden in embedded software. It can be acceptable when allocation is controlled, occurs only during initialization, or runs on a system with enough memory and a suitable allocator.</p>
<p>However, a fixed node pool is often a safer choice for small microcontrollers and long-running firmware.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Using a Static Node Pool</strong></h2>
<p>A static pool reserves all possible nodes at compile time. The application takes nodes from the pool and returns them when they are no longer needed.</p>
<pre><pre><code class="language-cpp">#define NODE_POOL_SIZE 8U

typedef struct Node
{
    uint8_t value;
    struct Node *next;
} Node;

typedef struct
{
    Node nodes[NODE_POOL_SIZE];
    Node *free_list;
} NodePool;</code></pre></pre>
<p>The pool can use a second linked list to track available nodes.</p>
<h3><strong>Initializing the Pool</strong></h3>
<pre><pre><code class="language-cpp">void node_pool_init(NodePool *pool)
{
    uint8_t index;

    if (pool == NULL)
    {
        return;
    }

    for (index = 0U;
         index &lt; NODE_POOL_SIZE - 1U;
         index++)
    {
        pool-&gt;nodes[index].next =
            &amp;pool-&gt;nodes[index + 1U];
    }

    pool-&gt;nodes[NODE_POOL_SIZE - 1U].next =
        NULL;

    pool-&gt;free_list = &amp;pool-&gt;nodes[0];
}</code></pre></pre>
<p>After initialization, every available node is connected through the pool’s <em>free_list</em>.</p>
<h3><strong>Allocating a Node from the Pool</strong></h3>
<pre><pre><code class="language-cpp">Node *node_pool_allocate(NodePool *pool)
{
    Node *node;

    if ((pool == NULL) ||
        (pool-&gt;free_list == NULL))
    {
        return NULL;
    }

    node = pool-&gt;free_list;
    pool-&gt;free_list = node-&gt;next;

    node-&gt;next = NULL;

    return node;
}</code></pre></pre>
<p>The first available node is removed from the free list and returned to the caller.</p>
<h3><strong>Returning a Node to the Pool</strong></h3>
<pre><pre><code class="language-cpp">void node_pool_release(
    NodePool *pool,
    Node *node)
{
    if ((pool == NULL) || (node == NULL))
    {
        return;
    }

    node-&gt;next = pool-&gt;free_list;
    pool-&gt;free_list = node;
}</code></pre></pre>
<p>The released node is placed back at the beginning of the free list.</p>
<p>This approach has several useful properties:</p>
<ul>
<li>The maximum memory usage is known at compile time.</li>
<li>Allocation does not depend on the general-purpose heap.</li>
<li>No external fragmentation occurs inside the pool.</li>
<li>Allocation and release are fast and predictable.</li>
<li>Pool exhaustion can be detected when no free nodes remain.</li>
</ul>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Complete Static Linked List Example</strong></h2>
<p>The following example combines a linked list with a fixed node pool.</p>
<pre><pre><code class="language-cpp">#include &lt;stdbool.h&gt;
#include &lt;stddef.h&gt;
#include &lt;stdint.h&gt;

#define NODE_POOL_SIZE 8U

typedef struct Node
{
    uint8_t value;
    struct Node *next;
} Node;

typedef struct
{
    Node *head;
    uint16_t count;
} LinkedList;

typedef struct
{
    Node nodes[NODE_POOL_SIZE];
    Node *free_list;
} NodePool;

void node_pool_init(NodePool *pool)
{
    uint8_t index;

    for (index = 0U;
         index &lt; NODE_POOL_SIZE - 1U;
         index++)
    {
        pool-&gt;nodes[index].next =
            &amp;pool-&gt;nodes[index + 1U];
    }

    pool-&gt;nodes[NODE_POOL_SIZE - 1U].next =
        NULL;

    pool-&gt;free_list = &amp;pool-&gt;nodes[0];
}

Node *node_pool_allocate(NodePool *pool)
{
    Node *node;

    if (pool-&gt;free_list == NULL)
    {
        return NULL;
    }

    node = pool-&gt;free_list;
    pool-&gt;free_list = node-&gt;next;

    node-&gt;next = NULL;

    return node;
}

void node_pool_release(
    NodePool *pool,
    Node *node)
{
    node-&gt;next = pool-&gt;free_list;
    pool-&gt;free_list = node;
}

void linked_list_init(LinkedList *list)
{
    list-&gt;head = NULL;
    list-&gt;count = 0U;
}

bool linked_list_add(
    LinkedList *list,
    NodePool *pool,
    uint8_t value)
{
    Node *node;

    node = node_pool_allocate(pool);

    if (node == NULL)
    {
        return false;
    }

    node-&gt;value = value;
    node-&gt;next = list-&gt;head;

    list-&gt;head = node;
    list-&gt;count++;

    return true;
}

bool linked_list_remove_first(
    LinkedList *list,
    NodePool *pool,
    uint8_t *value)
{
    Node *removed;

    if (list-&gt;head == NULL)
    {
        return false;
    }

    removed = list-&gt;head;
    list-&gt;head = removed-&gt;next;

    if (value != NULL)
    {
        *value = removed-&gt;value;
    }

    list-&gt;count--;
    node_pool_release(pool, removed);

    return true;
}</code></pre></pre>
<p>The maximum number of active nodes is limited to <em>NODE_POOL_SIZE</em>. When all nodes are in use, <em>linked_list_add()</em> returns <em>false</em>.</p>
<p>This fixed limit is often desirable in embedded firmware because the system’s worst-case memory requirement is known before the program runs.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Example Application</strong></h2>
<pre><pre><code class="language-cpp">LinkedList event_list;
NodePool event_pool;
uint8_t event_value;

int main(void)
{
    node_pool_init(&amp;event_pool);
    linked_list_init(&amp;event_list);

    linked_list_add(
        &amp;event_list,
        &amp;event_pool,
        10U);

    linked_list_add(
        &amp;event_list,
        &amp;event_pool,
        20U);

    linked_list_add(
        &amp;event_list,
        &amp;event_pool,
        30U);

    while (1)
    {
        if (linked_list_remove_first(
                &amp;event_list,
                &amp;event_pool,
                &amp;event_value))
        {
            process_event(event_value);
        }

        run_other_tasks();
    }
}</code></pre></pre>
<p>Because nodes are added at the beginning, the values in this example are removed in reverse order: 30, 20, then 10.</p>
<p>This behavior is a stack, not a FIFO queue. To preserve insertion order, add new nodes at the end or store a tail pointer.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Linked List with Head and Tail Pointers</strong></h2>
<p>Adding a tail pointer makes it possible to insert at the end without traversing the entire list.</p>
<pre><pre><code class="language-cpp">typedef struct
{
    Node *head;
    Node *tail;
    uint16_t count;
} LinkedList;</code></pre></pre>
<p>Appending a node then becomes:</p>
<pre><pre><code class="language-cpp">bool linked_list_push_back(
    LinkedList *list,
    Node *node,
    uint8_t value)
{
    if ((list == NULL) || (node == NULL))
    {
        return false;
    }

    node-&gt;value = value;
    node-&gt;next = NULL;

    if (list-&gt;tail == NULL)
    {
        list-&gt;head = node;
        list-&gt;tail = node;
    }
    else
    {
        list-&gt;tail-&gt;next = node;
        list-&gt;tail = node;
    }

    list-&gt;count++;

    return true;
}</code></pre></pre>
<p>When removing the first node, remember to clear the tail when the final element is removed.</p>
<pre><pre><code class="language-cpp">Node *linked_list_pop_front(
    LinkedList *list)
{
    Node *removed;

    if ((list == NULL) ||
        (list-&gt;head == NULL))
    {
        return NULL;
    }

    removed = list-&gt;head;
    list-&gt;head = removed-&gt;next;

    if (list-&gt;head == NULL)
    {
        list-&gt;tail = NULL;
    }

    removed-&gt;next = NULL;
    list-&gt;count--;

    return removed;
}</code></pre></pre>
<p>This head-and-tail arrangement can be used to build a FIFO event queue.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Where Linked Lists Are Used in Embedded Systems</strong></h2>
<p>Linked lists can be useful when the number or ordering of elements changes while the program is running.</p>
<h3><strong>Event Queues</strong></h3>
<p>Each node can represent an event waiting to be processed.</p>
<pre><pre><code class="language-cpp">typedef struct Event
{
    uint16_t event_id;
    uint32_t timestamp;
    struct Event *next;
} Event;</code></pre></pre>
<h3><strong>Software Timers</strong></h3>
<p>Timer nodes may be sorted by expiration time. The application only needs to check the first timer to find the next event that will expire.</p>
<h3><strong>Network Packet Lists</strong></h3>
<p>Network stacks may connect packet buffers without copying all packet data into one large contiguous block.</p>
<h3><strong>Device and Driver Registration</strong></h3>
<p>Drivers, callbacks, or peripheral objects can be registered in a list during system initialization.</p>
<h3><strong>Memory Free Lists</strong></h3>
<p>A memory allocator can link unused blocks together. The static node pool shown earlier is itself an example of a free list.</p>
<h3><strong>RTOS Kernel Objects</strong></h3>
<p>Real-time operating systems commonly use linked lists internally for task scheduling, blocked-task lists, timer lists, and queues.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>When a Linked List Is Not the Best Choice</strong></h2>
<p>A linked list is not automatically better because elements can be inserted and removed easily.</p>
<p>Consider using an array, ring buffer, or another fixed structure when:</p>
<ul>
<li>The maximum number of elements is known.</li>
<li>Fast indexed access is required.</li>
<li>Memory overhead must be minimized.</li>
<li>Cache locality matters.</li>
<li>The data is processed in strict FIFO order.</li>
<li>Deterministic memory usage is a priority.</li>
</ul>
<p>For UART bytes, a ring buffer is normally more appropriate than a linked list. Every linked-list node would require a pointer in addition to the byte itself, creating unnecessary memory overhead.</p>
<p>A linked list becomes more attractive when elements are larger, insertions occur at different positions, or nodes already exist as application objects.</p>
<h2><strong>Memory Overhead in a Linked List</strong></h2>
<p>Each node requires space for at least one pointer.</p>
<p>Consider this structure on a 32-bit microcontroller:</p>
<pre><pre><code class="language-cpp">typedef struct Node
{
    uint8_t value;
    struct Node *next;
} Node;</code></pre></pre>
<p>The data uses only one byte, but the pointer requires four bytes. Alignment and padding may increase the total node size to eight bytes.</p>
<p>As a result, storing eight one-byte values may require approximately 64 bytes instead of eight bytes.</p>
<p>This is why linked lists are rarely a good choice for individual UART characters or tiny sensor readings.</p>
<p>They become more memory-efficient when each node contains a larger payload or when the node is embedded inside an existing object.</p>
<h2><strong>Intrusive Linked Lists</strong></h2>
<p>An intrusive linked list places the pointer directly inside the application object.</p>
<pre><pre><code class="language-cpp">typedef struct Sensor
{
    uint8_t address;
    uint16_t sample_rate;
    struct Sensor *next;
} Sensor;</code></pre></pre>
<p>The sensor object itself becomes a list node. No separate wrapper node is required.</p>
<p>This approach is common in embedded systems because it avoids additional allocation and gives the programmer direct control over memory.</p>
<p>The tradeoff is that the object becomes coupled to the list. A single <em>next</em> pointer also allows the object to belong to only one such list at a time unless more link fields are added.</p>
<h2><strong>Linked Lists and Interrupts</strong></h2>
<p>Modifying a linked list from an interrupt while the main program also modifies it can corrupt the list.</p>
<p>Consider adding a node at the head:</p>
<pre><pre><code class="language-cpp">node-&gt;next = list-&gt;head;
list-&gt;head = node;</code></pre></pre>
<p>This operation involves more than one memory access. If another execution context changes the list between those two statements, a node may be lost or connected incorrectly.</p>
<p>Depending on the system, shared-list operations may require:</p>
<ul>
<li>A short critical section</li>
<li>Temporarily disabling the relevant interrupt</li>
<li>An RTOS mutex</li>
<li>An RTOS queue</li>
<li>A carefully designed lock-free algorithm</li>
</ul>
<p>Adding <em>volatile</em> to a pointer does not make the complete linked-list operation atomic.</p>
<p>For communication between a UART interrupt and a main loop, a single-producer, single-consumer ring buffer is normally simpler and safer than a shared linked list.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Common Linked-List Mistakes</strong></h2>
<h3><strong>Losing the Head Pointer</strong></h3>
<p>Changing the head before saving the old value may make the original node unreachable.</p>
<h3><strong>Forgetting to Update the Previous Node</strong></h3>
<p>When removing a middle node, the previous node must point to the removed node’s successor.</p>
<h3><strong>Dereferencing NULL</strong></h3>
<p>Always check whether a pointer is <em>NULL</em> before accessing its members.</p>
<pre><pre><code class="language-cpp">if (current != NULL)
{
    value = current-&gt;value;
}</code></pre></pre>
<h3><strong>Using a Node After It Has Been Freed</strong></h3>
<p>A pointer does not automatically become invalid-looking after <em>free()</em>. It may still contain the old address even though that memory no longer belongs to the node.</p>
<pre><pre><code class="language-cpp">free(node);
node = NULL;</code></pre></pre>
<p>Setting the local pointer to <em>NULL</em> can help prevent accidental reuse, although other copies of the same pointer must also be handled correctly.</p>
<h3><strong>Creating a Cycle Accidentally</strong></h3>
<p>If a node points to an earlier node instead of eventually reaching <em>NULL</em>, traversal may never terminate.</p>
<pre><pre><code class="language-cpp">[10] -&gt; [20] -&gt; [30]
          ^         |
          |_________|</code></pre></pre>
<h3><strong>Returning a Node to the Pool Twice</strong></h3>
<p>Releasing the same node twice can corrupt the pool’s free list. The application must clearly define who owns each node.</p>
<h3><strong>Using Dynamic Allocation Without Handling Failure</strong></h3>
<p>Every allocation can fail. The return value of <em>malloc()</em> or a node-pool allocation function must be checked.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Singly Versus Doubly Linked Lists</strong></h2>
<p>A singly linked list stores only a pointer to the next node.</p>
<pre><pre><code class="language-cpp">typedef struct Node
{
    uint8_t value;
    struct Node *next;
} Node;</code></pre></pre>
<p>A doubly linked list stores both next and previous pointers.</p>
<pre><pre><code class="language-cpp">typedef struct Node
{
    uint8_t value;
    struct Node *next;
    struct Node *previous;
} Node;</code></pre></pre>
<p>A doubly linked list allows traversal in both directions and makes removal easier when the node address is already known. However, it requires another pointer in every node and more pointer updates during insertion and removal.</p>
<div style="overflow-x: auto;">
<table>
<thead>
<tr>
<th>Feature</th>
<th>Singly Linked List</th>
<th>Doubly Linked List</th>
</tr>
</thead>
<tbody>
<tr>
<td>Pointers per node</td>
<td>One</td>
<td>Two</td>
</tr>
<tr>
<td>Forward traversal</td>
<td>Yes</td>
<td>Yes</td>
</tr>
<tr>
<td>Backward traversal</td>
<td>No</td>
<td>Yes</td>
</tr>
<tr>
<td>Memory usage</td>
<td>Lower</td>
<td>Higher</td>
</tr>
<tr>
<td>Removal with node pointer</td>
<td>May need previous node</td>
<td>Direct pointer updates</td>
</tr>
<tr>
<td>Implementation complexity</td>
<td>Lower</td>
<td>Higher</td>
</tr>
</tbody>
</table>
</div>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Frequently Asked Questions</strong></h2>
<h3><strong>Are linked lists commonly used in embedded systems?</strong></h3>
<p>Yes, especially in operating systems, protocol stacks, event systems, driver registration, and memory managers. However, small bare-metal applications often prefer fixed arrays or ring buffers because they use less memory and are easier to analyze.</p>
<h3><strong>Should I use malloc for an embedded linked list?</strong></h3>
<p>It depends on the project. Dynamic allocation may be acceptable during initialization or on larger systems. For small or safety-sensitive microcontrollers, a fixed node pool usually provides more predictable behavior.</p>
<h3><strong>Is a linked list faster than an array?</strong></h3>
<p>Not in every operation. Inserting at the head of a linked list is fast, but finding the tenth element requires following the previous nine nodes. Arrays provide much faster indexed access and usually better memory locality.</p>
<h3><strong>Can linked-list nodes store structures?</strong></h3>
<p>Yes. A node can store sensor records, messages, tasks, timers, network packets, or other application-specific data.</p>
<h3><strong>What happens when a static node pool is full?</strong></h3>
<p>The allocation function returns <em>NULL</em>. The application must decide whether to reject the new item, report an error, reuse an older node, or increase the configured pool size.</p>
<h3><strong>Can I use a linked list inside an interrupt?</strong></h3>
<p>It is possible, but shared access must be designed carefully. Pointer updates can be interrupted halfway through, so critical sections or another synchronization method may be required.</p>
<h3><strong>Should UART data use a linked list?</strong></h3>
<p>Usually not. A fixed ring buffer is more compact and efficient for a stream of bytes. A linked list may be useful when each received item is a larger variable-size message rather than an individual byte.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Conclusion</strong></h2>
<p>A linked list connects separate data objects using pointers. The head identifies the first node, each node points to the next one, and the final node points to <em>NULL</em>.</p>
<p>The main linked-list operations are:</p>
<ul>
<li>Adding a node</li>
<li>Removing a node</li>
<li>Traversing the list</li>
<li>Searching for a value</li>
<li>Returning unused nodes to their allocator or static pool</li>
</ul>
<p>For embedded systems, the pointer logic is only part of the design. You must also consider RAM overhead, allocation failure, fragmentation, interrupt safety, and worst-case execution time.</p>
<p>The visualizer at the beginning of this tutorial shows the logical connections between nodes. In an actual microcontroller, those nodes may be stored at completely different memory addresses. The pointers are what preserve their order.</p>
<p>For small and predictable firmware, I recommend starting with a singly linked list backed by a fixed node pool. It keeps the flexibility of linked nodes while avoiding the uncertainty of repeated heap allocation.</p>
<p>The post <a href="https://www.teachmemicro.com/linked-list-visualizer-for-embedded-systems/">Linked List Visualizer for Embedded Systems</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>Ring Buffer Visualizer for Embedded Systems</title>
		<link>https://www.teachmemicro.com/ring-buffer-visualizer-for-embedded-systems/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=ring-buffer-visualizer-for-embedded-systems</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Tue, 21 Jul 2026 03:15:47 +0000</pubDate>
				<category><![CDATA[Tools]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=12313</guid>

					<description><![CDATA[<p>A ring buffer in embedded systems is a fixed-size memory area used to temporarily store data. You will often find one between a fast peripheral and a slower part of the program. A UART interrupt, for example, can place received bytes into a ring buffer while the main application processes those bytes whenever it has &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/ring-buffer-visualizer-for-embedded-systems/">Ring Buffer Visualizer for Embedded Systems</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>A <strong>ring buffer in embedded systems</strong> is a fixed-size memory area used to temporarily store data. You will often find one between a fast peripheral and a slower part of the program. A UART interrupt, for example, can place received bytes into a ring buffer while the main application processes those bytes whenever it has time.</p>
<p>A ring buffer is also called a <strong>circular buffer</strong>. The memory itself is still an ordinary C array. What makes it circular is the way its indexes return to zero after reaching the final array element.</p>
<p>This tutorial explains how an embedded ring buffer works, how its <em>head</em> and <em>tail</em> pointers move, and how you can implement one in C. You can also use the interactive visualizer below to see each operation happen.</p>
<p><span id="more-12313"></span></p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Interactive Embedded Ring Buffer Visualizer</strong></h2>
<p>Enter a character, decimal byte, or hexadecimal value and click <strong>Write Byte</strong>. The value is stored at the current head position. Click <strong>Read Byte</strong> to remove the oldest value from the tail position.</p>
<p>Watch what happens when either pointer reaches the final slot. Instead of moving outside the array, it wraps around to index zero.</p>
        <div
            id="tmm-rbv-2"
            class="tmm-rbv-app"
            data-rbv-instance="tmm-rbv-2"
            data-rbv-buffer-size="8"
        >
            <section class="tmm-rbv-card" aria-labelledby="tmm-rbv-2-buffer-title">
                <noscript>
                    <div class="tmm-rbv-noscript">
                        JavaScript is required for the interactive ring buffer visualizer.                    </div>
                </noscript>

                <div class="tmm-rbv-topline">
                    <h3 id="tmm-rbv-2-buffer-title">
                        Ring Buffer                    </h3>
                    <p>
                        Count-based FIFO storage for embedded UART bytes.                    </p>
                </div>

                <div class="tmm-rbv-buffer-area">
                    <div
                        class="tmm-rbv-buffer"
                        data-rbv-el="buffer"
                        role="img"
                        aria-label="Circular ring buffer slots with head and tail indicators"
                    ></div>
                    <div class="tmm-rbv-legend" aria-label="Pointer legend">
                        <span><strong class="tmm-rbv-head-text">HEAD</strong>: next write position</span>
                        <span><strong class="tmm-rbv-tail-text">TAIL</strong>: next read position</span>
                    </div>
                </div>

                <div class="tmm-rbv-controls">
                    <label class="tmm-rbv-value-field" for="tmm-rbv-2-value">
                        <span>Byte or character</span>
                        <input
                            id="tmm-rbv-2-value"
                            data-rbv-el="value-input"
                            type="text"
                            inputmode="text"
                            autocomplete="off"
                            maxlength="6"
                            placeholder="A or 0x41"
                        >
                    </label>
                    <button class="tmm-rbv-button" type="button" data-rbv-action="write">
                        Write Byte                    </button>
                    <button class="tmm-rbv-button tmm-rbv-button--secondary" type="button" data-rbv-action="read">
                        Read Byte                    </button>
                    <button class="tmm-rbv-button tmm-rbv-button--muted" type="button" data-rbv-action="reset">
                        Reset                    </button>
                </div>

                <div class="tmm-rbv-uart-row">
                    <label class="tmm-rbv-toggle" for="tmm-rbv-2-uart-toggle">
                        <input id="tmm-rbv-2-uart-toggle" data-rbv-el="uart-toggle" type="checkbox">
                        <span>Simulate UART RX</span>
                    </label>
                    <button class="tmm-rbv-button tmm-rbv-button--secondary" type="button" data-rbv-action="uart-start" disabled>
                        Start UART                    </button>
                    <button class="tmm-rbv-button tmm-rbv-button--muted" type="button" data-rbv-action="uart-stop" disabled>
                        Stop UART                    </button>
                    <span class="tmm-rbv-uart-label" data-rbv-el="uart-label">
                        UART RX idle.                    </span>
                </div>

                <div class="tmm-rbv-status" id="tmm-rbv-2-status" data-rbv-el="status">
                    <span>Head: <strong data-rbv-el="head">0</strong></span>
                    <span>Tail: <strong data-rbv-el="tail">0</strong></span>
                    <span>Used: <strong data-rbv-el="used">0 / 8</strong></span>
                    <span>State: <strong data-rbv-el="state">EMPTY</strong></span>
                </div>

                <p
                    class="tmm-rbv-message"
                    id="tmm-rbv-2-message"
                    data-rbv-el="message"
                    aria-live="polite"
                >
                    Buffer is empty. Write a byte to begin.                </p>

                <details class="tmm-rbv-code" aria-labelledby="tmm-rbv-2-code-title">
                    <summary id="tmm-rbv-2-code-title">
                        Equivalent Embedded C                    </summary>
                    <button class="tmm-rbv-button tmm-rbv-button--copy" type="button" data-rbv-action="copy-code">
                        Copy Code                    </button>
                    <pre><code data-rbv-el="code">#define BUFFER_SIZE 8

typedef struct
{
    uint8_t data[BUFFER_SIZE];
    volatile uint8_t head;
    volatile uint8_t tail;
    volatile uint8_t count;
} RingBuffer;

bool ring_buffer_write(RingBuffer *rb, uint8_t value)
{
    if (rb-&gt;count &gt;= BUFFER_SIZE)
    {
        return false;
    }

    rb-&gt;data[rb-&gt;head] = value;
    rb-&gt;head = (rb-&gt;head + 1U) % BUFFER_SIZE;
    rb-&gt;count++;

    return true;
}

bool ring_buffer_read(RingBuffer *rb, uint8_t *value)
{
    if (rb-&gt;count == 0U)
    {
        return false;
    }

    *value = rb-&gt;data[rb-&gt;tail];
    rb-&gt;tail = (rb-&gt;tail + 1U) % BUFFER_SIZE;
    rb-&gt;count--;

    return true;
}</code></pre>
                </details>
            </section>
        </div>
        
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>What Is a Ring Buffer in Embedded Systems?</strong></h2>
<p>A ring buffer is a <strong>first-in, first-out</strong> or FIFO data structure built using a fixed-size array. The first value written into the buffer is normally the first value read from it.</p>
<p>Consider this eight-byte array:</p>
<pre><pre><code class="language-cpp">uint8_t buffer[8];</code></pre></pre>
<p>A normal queue implementation might remove the first value and then shift every remaining value toward index zero. That approach works, but repeatedly moving data wastes processor time.</p>
<p>A ring buffer does not shift its contents. Instead, it keeps track of two indexes:</p>
<ul>
<li><strong>Head</strong> — the position where the next value will be written</li>
<li><strong>Tail</strong> — the position where the next value will be read</li>
</ul>
<p>When a value is written, the head advances. When a value is read, the tail advances. The data remains in the same physical array slots until those slots are reused.</p>
<p>This makes ring-buffer writes and reads fast and predictable. Both operations normally take constant time because no existing elements have to be moved.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Why Ring Buffers Are Useful in Embedded Applications</strong></h2>
<p>Microcontrollers often deal with events that do not happen at exactly the same speed as the main application.</p>
<p>For example, a UART peripheral may receive a byte while the processor is:</p>
<ul>
<li>Updating a display</li>
<li>Reading a sensor</li>
<li>Writing data to flash memory</li>
<li>Processing a communication packet</li>
<li>Executing another part of the main loop</li>
</ul>
<p>The UART receive interrupt should not perform a large amount of processing. It should normally retrieve the received byte quickly, store it somewhere safe, and then return.</p>
<p>A ring buffer provides that temporary storage:</p>
<pre><pre><code class="language-cpp">UART peripheral → RX interrupt → Ring buffer → Main application</code></pre></pre>
<p>The interrupt acts as the <strong>producer</strong> because it adds bytes. The main application acts as the <strong>consumer</strong> because it removes and processes them.</p>
<p>The same idea can be used for:</p>
<ul>
<li>UART receive and transmit queues</li>
<li>ADC sample storage</li>
<li>Sensor-data collection</li>
<li>Logging systems</li>
<li>CAN and SPI message queues</li>
<li>Audio samples</li>
<li>Keyboard or keypad input</li>
<li>Data moving between interrupt handlers and the main loop</li>
</ul>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>How the Head Pointer Works</strong></h2>
<p>The head points to the array position where the next byte will be stored.</p>
<p>Suppose the head is initially at index zero:</p>
<pre><pre><code class="language-cpp">head = 0;</code></pre></pre>
<p>Writing the character <em>'A'</em> stores it in <em>buffer[0]</em>. The head then moves to index one.</p>
<pre><pre><code class="language-cpp">buffer[head] = &#039;A&#039;;
head++;</code></pre></pre>
<p>If the next value is <em>'B'</em>, it is stored in <em>buffer[1]</em>, after which the head moves to index two.</p>
<p>The head always identifies the <em>next available write position</em>. It does not normally point to the most recently written value.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>How the Tail Pointer Works</strong></h2>
<p>The tail points to the oldest unread value.</p>
<p>If the tail is at index zero, reading from the buffer returns the value stored in <em>buffer[0]</em>. The tail then advances to index one.</p>
<pre><pre><code class="language-cpp">value = buffer[tail];
tail++;</code></pre></pre>
<p>The contents do not need to be shifted after the read. The application simply considers that slot available again.</p>
<p>Some implementations clear a slot after reading it because doing so makes debugging and visualization easier. Clearing the slot is not normally required for the ring-buffer algorithm itself.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Pointer Wraparound</strong></h2>
<p>The circular behavior appears when a pointer reaches the end of the array.</p>
<p>For an eight-element buffer, the valid indexes are zero through seven. If the head is at index seven, its next position must be index zero.</p>
<p>This can be written using the modulo operator:</p>
<pre><pre><code class="language-cpp">head = (head + 1U) % BUFFER_SIZE;</code></pre></pre>
<p>With <em>BUFFER_SIZE</em> equal to eight:</p>
<pre><pre><code class="language-cpp">(7 + 1) % 8 = 0</code></pre></pre>
<p>The same operation is used for the tail:</p>
<pre><pre><code class="language-cpp">tail = (tail + 1U) % BUFFER_SIZE;</code></pre></pre>
<p>No data is physically moved from the end of the array back to the beginning. Only the index wraps around.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Detecting an Empty or Full Ring Buffer</strong></h2>
<p>A ring buffer must be able to determine whether another read or write is allowed.</p>
<p>The interactive visualizer uses a <strong>count-based implementation</strong>. In addition to the head and tail, it stores the number of elements currently inside the buffer.</p>
<pre><pre><code class="language-cpp">uint8_t head;
uint8_t tail;
uint8_t count;</code></pre></pre>
<p>The buffer is empty when:</p>
<pre><pre><code class="language-cpp">count == 0</code></pre></pre>
<p>It is full when:</p>
<pre><pre><code class="language-cpp">count == BUFFER_SIZE</code></pre></pre>
<p>This method allows every array element to be used. An eight-element array can hold eight values.</p>
<p>Another common method avoids the count variable and reserves one array slot. In that version:</p>
<ul>
<li><em>head == tail</em> means the buffer is empty.</li>
<li>The buffer is full when advancing the head would make it equal to the tail.</li>
</ul>
<p>That approach is especially useful for a single-producer, single-consumer ring buffer because the producer can own the head while the consumer owns the tail. I will return to that version later in the tutorial.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Basic Count-Based Ring Buffer in C</strong></h2>
<p>The following implementation matches the behavior of the visualizer. It stores eight bytes and rejects new data when the buffer is full.</p>
<pre><pre><code class="language-cpp">#include &lt;stdbool.h&gt;
#include &lt;stdint.h&gt;

#define BUFFER_SIZE 8U

typedef struct
{
    uint8_t data[BUFFER_SIZE];
    uint8_t head;
    uint8_t tail;
    uint8_t count;
} RingBuffer;</code></pre></pre>
<h3><strong>Initializing the Buffer</strong></h3>
<p>Initialization places both pointers at index zero and sets the stored count to zero.</p>
<pre><pre><code class="language-cpp">void ring_buffer_init(RingBuffer *rb)
{
    rb-&gt;head = 0U;
    rb-&gt;tail = 0U;
    rb-&gt;count = 0U;
}</code></pre></pre>
<p>The actual data array does not have to be cleared because no slot is considered valid while the count is zero.</p>
<h3><strong>Writing a Byte</strong></h3>
<pre><pre><code class="language-cpp">bool ring_buffer_write(RingBuffer *rb, uint8_t value)
{
    if (rb-&gt;count &gt;= BUFFER_SIZE)
    {
        return false;
    }

    rb-&gt;data[rb-&gt;head] = value;
    rb-&gt;head = (rb-&gt;head + 1U) % BUFFER_SIZE;
    rb-&gt;count++;

    return true;
}</code></pre></pre>
<p>The function first checks whether the buffer is full. If space is available, the value is written at the head position. The head advances, and the element count increases.</p>
<p>The function returns <em>false</em> when no more data can be accepted. The caller can use this result to record an overflow, increment an error counter, or take another appropriate action.</p>
<h3><strong>Reading a Byte</strong></h3>
<pre><pre><code class="language-cpp">bool ring_buffer_read(RingBuffer *rb, uint8_t *value)
{
    if (rb-&gt;count == 0U)
    {
        return false;
    }

    *value = rb-&gt;data[rb-&gt;tail];
    rb-&gt;tail = (rb-&gt;tail + 1U) % BUFFER_SIZE;
    rb-&gt;count--;

    return true;
}</code></pre></pre>
<p>The read function checks for an empty buffer before accessing the array. It returns the oldest byte, advances the tail, and decreases the count.</p>
<h3><strong>Checking the Current State</strong></h3>
<pre><pre><code class="language-cpp">bool ring_buffer_is_empty(const RingBuffer *rb)
{
    return rb-&gt;count == 0U;
}

bool ring_buffer_is_full(const RingBuffer *rb)
{
    return rb-&gt;count == BUFFER_SIZE;
}

uint8_t ring_buffer_used(const RingBuffer *rb)
{
    return rb-&gt;count;
}

uint8_t ring_buffer_free(const RingBuffer *rb)
{
    return BUFFER_SIZE - rb-&gt;count;
}</code></pre></pre>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>A Simple Ring Buffer Example</strong></h2>
<p>Here is how the buffer can be used from a normal C program:</p>
<pre><pre><code class="language-cpp">RingBuffer rx_buffer;
uint8_t received_byte;

int main(void)
{
    ring_buffer_init(&amp;rx_buffer);

    ring_buffer_write(&amp;rx_buffer, &#039;A&#039;);
    ring_buffer_write(&amp;rx_buffer, &#039;B&#039;);
    ring_buffer_write(&amp;rx_buffer, &#039;C&#039;);

    if (ring_buffer_read(&amp;rx_buffer, &amp;received_byte))
    {
        /* received_byte contains &#039;A&#039; */
    }

    if (ring_buffer_read(&amp;rx_buffer, &amp;received_byte))
    {
        /* received_byte contains &#039;B&#039; */
    }

    while (1)
    {
        /* Main application */
    }
}</code></pre></pre>
<p>Even if the head wraps around while more values are written, the tail continues to return them in FIFO order.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Using a Ring Buffer for UART Reception</strong></h2>
<p>A UART receive ring buffer is one of the most common embedded applications.</p>
<p>The general flow is:</p>
<ol>
<li>The UART peripheral receives a byte.</li>
<li>The receive interrupt runs.</li>
<li>The interrupt reads the UART data register.</li>
<li>The byte is written into the ring buffer.</li>
<li>The interrupt returns.</li>
<li>The main loop later reads and processes the buffered byte.</li>
</ol>
<p>A simplified interrupt handler might look like this:</p>
<pre><pre><code class="language-cpp">void UART_RX_IRQHandler(void)
{
    uint8_t value = uart_read_data_register();

    if (!ring_buffer_write(&amp;rx_buffer, value))
    {
        uart_rx_overflow = true;
    }
}</code></pre></pre>
<p>The main loop can process the data without waiting inside the interrupt:</p>
<pre><pre><code class="language-cpp">int main(void)
{
    uint8_t value;

    ring_buffer_init(&amp;rx_buffer);
    uart_init();

    while (1)
    {
        while (ring_buffer_read(&amp;rx_buffer, &amp;value))
        {
            process_received_byte(value);
        }

        run_other_application_tasks();
    }
}</code></pre></pre>
<p>This arrangement lets the interrupt capture incoming data quickly while the main application performs the more time-consuming work.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>A Concurrency Warning About the Count Variable</strong></h2>
<p>The count-based implementation is easy to understand and works well for the visualizer. However, notice what happens in the UART example:</p>
<ul>
<li>The interrupt increases <em>count</em>.</li>
<li>The main loop decreases <em>count</em>.</li>
</ul>
<p>Both execution contexts modify the same variable. An interrupt occurring during a read-modify-write operation may produce a race condition, depending on the microcontroller, variable width, compiler, and generated instructions.</p>
<p>Adding <em>volatile</em> prevents the compiler from treating a shared variable as if it never changes unexpectedly. It does <strong>not</strong> automatically make an operation atomic, and it does not by itself solve every concurrency problem.</p>
<p>You can protect a count-based implementation with a short critical section. Another option is to use a single-producer, single-consumer design where the interrupt modifies only the head and the main loop modifies only the tail.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Single-Producer, Single-Consumer UART Ring Buffer</strong></h2>
<p>For a typical UART receive buffer, there is one producer and one consumer:</p>
<ul>
<li>The UART RX interrupt is the only code that changes the head.</li>
<li>The main loop is the only code that changes the tail.</li>
</ul>
<p>The implementation below reserves one array slot so that <em>head == tail</em> can unambiguously represent an empty buffer.</p>
<pre><pre><code class="language-cpp">#include &lt;stdbool.h&gt;
#include &lt;stdint.h&gt;

#define UART_BUFFER_SIZE 16U
#define UART_BUFFER_MASK (UART_BUFFER_SIZE - 1U)

typedef struct
{
    uint8_t data[UART_BUFFER_SIZE];
    volatile uint16_t head;
    volatile uint16_t tail;
} UartRingBuffer;</code></pre></pre>
<p>Because the masking optimization is used here, <em>UART_BUFFER_SIZE</em> must be a power of two.</p>
<h3><strong>Writing from the UART Interrupt</strong></h3>
<pre><pre><code class="language-cpp">bool uart_ring_buffer_write_isr(
    UartRingBuffer *rb,
    uint8_t value)
{
    uint16_t head = rb-&gt;head;
    uint16_t next =
        (head + 1U) &amp; UART_BUFFER_MASK;

    if (next == rb-&gt;tail)
    {
        return false;
    }

    rb-&gt;data[head] = value;
    rb-&gt;head = next;

    return true;
}</code></pre></pre>
<h3><strong>Reading from the Main Loop</strong></h3>
<pre><pre><code class="language-cpp">bool uart_ring_buffer_read(
    UartRingBuffer *rb,
    uint8_t *value)
{
    uint16_t tail = rb-&gt;tail;

    if (tail == rb-&gt;head)
    {
        return false;
    }

    *value = rb-&gt;data[tail];
    rb-&gt;tail =
        (tail + 1U) &amp; UART_BUFFER_MASK;

    return true;
}</code></pre></pre>
<p>This arrangement avoids a shared count variable. The producer publishes a new head only after writing the byte, while the consumer advances the tail only after retrieving it.</p>
<p>You should still check the requirements of your specific architecture. Index reads and writes should be naturally aligned and atomic for the chosen index type. Systems with multiple cores, DMA, an RTOS, cache coherency concerns, or more than one producer or consumer may require memory barriers, critical sections, or an operating-system synchronization primitive.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Modulo Versus Bit Masking</strong></h2>
<p>The general wraparound operation uses modulo:</p>
<pre><pre><code class="language-cpp">next = (index + 1U) % BUFFER_SIZE;</code></pre></pre>
<p>This works for any valid buffer size.</p>
<p>If the size is a power of two, the index can instead be wrapped using a mask:</p>
<pre><pre><code class="language-cpp">next = (index + 1U) &amp; (BUFFER_SIZE - 1U);</code></pre></pre>
<p>For example, valid indexes for a 16-byte buffer are zero through 15. The value <em>BUFFER_SIZE - 1</em> is therefore <em>0x0F</em>.</p>
<p>Modern compilers can often optimize a constant modulo operation automatically. I would therefore prioritize clear and correct code first, then inspect the generated code if performance is critical.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Ring Buffer Overflow and Underflow</strong></h2>
<h3><strong>Overflow</strong></h3>
<p>An overflow occurs when the producer attempts to write while no space is available.</p>
<p>The buffer can respond in different ways:</p>
<ul>
<li>Reject the new value</li>
<li>Overwrite the oldest unread value</li>
<li>Set an overflow flag</li>
<li>Increment a lost-byte counter</li>
<li>Apply flow control when the protocol supports it</li>
</ul>
<p>The visualizer rejects the new byte because this makes the loss of data obvious. Whether that is the correct behavior in a real project depends on the application.</p>
<p>For a command interface, losing an old or new character may invalidate the entire command. For continuous sensor history, overwriting the oldest sample may be acceptable.</p>
<h3><strong>Underflow</strong></h3>
<p>An underflow occurs when the consumer attempts to read an empty buffer.</p>
<p>A well-designed read function should detect this before accessing the array. Returning a Boolean result is useful because every possible byte value, including zero, may be valid data.</p>
<pre><pre><code class="language-cpp">uint8_t value;

if (ring_buffer_read(&amp;rx_buffer, &amp;value))
{
    process_received_byte(value);
}
else
{
    /* No data currently available */
}</code></pre></pre>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Choosing the Ring Buffer Size</strong></h2>
<p>The correct buffer size depends on how quickly data arrives and how long the consumer may be unable to process it.</p>
<p>A useful starting estimate is:</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_36c3ba47aec8bea6fe7cfe96cf26d71b.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="N_{\min} \ge R_{\text{bytes}}T_{\text{delay}} + M" /></span><script type='math/tex'>N_{\min} \ge R_{\text{bytes}}T_{\text{delay}} + M</script></p>
<p>Where:</p>
<ul>
<li><strong>N<sub>min</sub></strong> is the minimum number of required slots.</li>
<li><strong>R<sub>bytes</sub></strong> is the incoming data rate in bytes per second.</li>
<li><strong>T<sub>delay</sub></strong> is the longest expected consumer delay in seconds.</li>
<li><strong>M</strong> is an additional safety margin.</li>
</ul>
<p>For example, an 8-N-1 UART connection at 115200 baud uses approximately ten transmitted bits for every data byte: one start bit, eight data bits, and one stop bit.</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_13803e89537e1b130570a49228941c86.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="R_{\text{bytes}} \approx \frac{115200}{10} = 11520\text{ bytes/s}" /></span><script type='math/tex'>R_{\text{bytes}} \approx \frac{115200}{10} = 11520\text{ bytes/s}</script></p>
<p>If the main application can be delayed for 5 milliseconds:</p>
<p><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_79901569c7af45e0cd758adc090dd800.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="N_{\min} \ge 11520 \times 0.005 = 57.6\text{ bytes}" /></span><script type='math/tex'>N_{\min} \ge 11520 \times 0.005 = 57.6\text{ bytes}</script></p>
<p>A 64-byte buffer is the smallest convenient power-of-two choice, but it leaves almost no margin. A 128-byte buffer may be more appropriate if RAM permits and occasional longer delays are possible.</p>
<p>If the implementation reserves one slot, remember that a 64-element array stores only 63 bytes.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Ring Buffer Versus an Ordinary Linear Buffer</strong></h2>
<div style="overflow-x: auto;">
<table>
<thead>
<tr>
<th>Feature</th>
<th>Ring Buffer</th>
<th>Linear Buffer with Shifting</th>
</tr>
</thead>
<tbody>
<tr>
<td>Memory allocation</td>
<td>Fixed</td>
<td>Usually fixed</td>
</tr>
<tr>
<td>Write operation</td>
<td>Advances head</td>
<td>Adds at the end</td>
</tr>
<tr>
<td>Read operation</td>
<td>Advances tail</td>
<td>May shift remaining values</td>
</tr>
<tr>
<td>Wraparound</td>
<td>Yes</td>
<td>No</td>
</tr>
<tr>
<td>Read and write time</td>
<td>Normally constant</td>
<td>May increase when shifting</td>
</tr>
<tr>
<td>Common embedded use</td>
<td>UART, ADC, logging and streaming</td>
<td>Small packets and simple temporary storage</td>
</tr>
</tbody>
</table>
</div>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Common Ring Buffer Mistakes</strong></h2>
<h3><strong>Confusing the Head with the Last Written Position</strong></h3>
<p>In the implementation used here, the head is the <em>next</em> write position. After writing to index three, the head normally advances to index four.</p>
<h3><strong>Failing to Distinguish Full from Empty</strong></h3>
<p>If the head and tail can be equal in both states, another piece of information is required. Use a count, reserve one slot, or store a separate full flag.</p>
<h3><strong>Using an Index That Is Too Small</strong></h3>
<p>The index type must be able to represent every valid buffer position. An eight-bit index is sufficient for buffers up to 256 elements, but larger buffers require a wider type.</p>
<h3><strong>Assuming Volatile Makes Everything Thread-Safe</strong></h3>
<p><em>volatile</em> affects compiler optimization. It does not automatically protect a multi-step update from interrupts, multiple tasks, or another processor core.</p>
<h3><strong>Doing Too Much Work Inside the Interrupt</strong></h3>
<p>The receive interrupt should normally store the byte and return. Packet parsing, command execution, display updates, and other longer operations are better handled outside the ISR.</p>
<h3><strong>Ignoring Overflow</strong></h3>
<p>Even a correctly implemented ring buffer can overflow. Always decide how the application should detect and respond to lost data.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Frequently Asked Questions</strong></h2>
<h3><strong>Is a ring buffer the same as a circular queue?</strong></h3>
<p>The terms are often used interchangeably. Both normally describe FIFO storage that reuses a fixed array by wrapping its indexes. Ring buffer is particularly common terminology in embedded systems, drivers, communications, and streaming applications.</p>
<h3><strong>Does a ring buffer dynamically allocate memory?</strong></h3>
<p>It does not have to. Embedded implementations usually use a statically allocated array so memory usage is known at compile time and no heap allocation is required.</p>
<h3><strong>Can a ring buffer store structures instead of bytes?</strong></h3>
<p>Yes. The array can contain sensor samples, CAN frames, event structures, pointers, or other fixed-size objects.</p>
<pre><pre><code class="language-cpp">typedef struct
{
    uint32_t timestamp;
    int16_t temperature;
    uint16_t pressure;
} SensorSample;

SensorSample sample_buffer[16];</code></pre></pre>
<h3><strong>Should a full ring buffer reject or overwrite data?</strong></h3>
<p>It depends on the application. Communication buffers often reject new data and report an overflow. History buffers may intentionally overwrite the oldest entry so they always contain the most recent samples.</p>
<h3><strong>Can DMA use a ring buffer?</strong></h3>
<p>Yes, although the design becomes more hardware-specific. Many microcontrollers support circular DMA modes that repeatedly fill a memory region. The application must then track which portion of the region has been written and which portion has already been processed.</p>
<h3><strong>Can I use this implementation with an RTOS?</strong></h3>
<p>You can, but a ring buffer shared by multiple tasks may require a mutex, critical section, semaphore, or RTOS queue. A lock-free single-producer, single-consumer implementation should not automatically be treated as safe for multiple producers or consumers.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Conclusion</strong></h2>
<p>A ring buffer gives embedded applications an efficient way to temporarily store streaming data without shifting array elements or allocating more memory at runtime.</p>
<p>The basic idea is simple:</p>
<ul>
<li>The head identifies the next write position.</li>
<li>The tail identifies the next read position.</li>
<li>Both pointers return to zero after reaching the end.</li>
<li>Writes must detect a full buffer.</li>
<li>Reads must detect an empty buffer.</li>
</ul>
<p>The count-based approach used by the visualizer is useful for learning because the full and empty conditions are easy to see. For a real UART interrupt and main-loop arrangement, a single-producer, single-consumer implementation with separate head and tail ownership can reduce shared-state problems.</p>
<p>Try filling the visualizer, reading a few bytes, and then writing more values. The most important moment is when the head crosses the final index and returns to zero while the unread values remain in the correct FIFO order. Once that behavior is clear, the C implementation becomes much easier to understand.</p>
<p>The post <a href="https://www.teachmemicro.com/ring-buffer-visualizer-for-embedded-systems/">Ring Buffer Visualizer for Embedded Systems</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>How to Power an ESP32-S3 with AA Batteries</title>
		<link>https://www.teachmemicro.com/how-to-power-an-esp32-s3-with-aa-batteries/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=how-to-power-an-esp32-s3-with-aa-batteries</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Tue, 21 Jul 2026 00:36:29 +0000</pubDate>
				<category><![CDATA[ESP32 Tutorial]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=12295</guid>

					<description><![CDATA[<p>The ESP32-S3 is commonly powered through its USB port while programming and testing. However, USB power is not always practical for projects installed outdoors, mounted on walls, or placed far from an electrical outlet. In this tutorial, we will build an ESP32-S3 AA battery power supply using two AA cells and an MT3608 boost converter. &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/how-to-power-an-esp32-s3-with-aa-batteries/">How to Power an ESP32-S3 with AA Batteries</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>The ESP32-S3 is commonly powered through its USB port while programming and testing. However, USB power is not always practical for projects installed outdoors, mounted on walls, or placed far from an electrical outlet.</p>
<p>In this tutorial, we will build an <strong>ESP32-S3 AA battery power supply</strong> using two AA cells and an MT3608 boost converter. We will also add an optional battery-voltage monitor and use deep sleep to make the batteries last longer.</p>
<p><span id="more-12295"></span></p>
<p>The short answer is that an ESP32-S3 can run from AA batteries, but the batteries should not normally be connected directly to the development board. The voltage must remain stable, and the power circuit must handle the short current bursts produced when the ESP32-S3 uses Wi-Fi or Bluetooth.</p>
<blockquote><p>This tutorial assumes an ESP32-S3 development board similar to the Espressif ESP32-S3-DevKitC-1. Other ESP32-S3 boards may have different regulators, power LEDs, USB circuits, pin labels, and power-input arrangements. Check your board’s schematic before applying power through its 3V3 pin.</p></blockquote>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Can an ESP32-S3 Run on AA Batteries?</strong></h2>
<p>Yes. Two AA batteries can power an ESP32-S3 when used with a suitable voltage converter. The ESP32-S3 chip normally operates from a 3.3 V supply. Its acceptable supply range is approximately 3.0 V to 3.6 V. The power source must also handle the ESP32-S3’s brief current peaks when its radio begins transmitting.</p>
<p>A pair of alkaline AA batteries has a nominal voltage of:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_5e1a9e3affa6368dbf38f3a867844cfd.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="2 \times 1.5\text{ V} = 3.0\text{ V}" /></span><script type='math/tex'>2 \times 1.5\text{ V} = 3.0\text{ V}</script></p>
<p>At first glance, 3.0 V may appear close enough to the ESP32-S3’s operating voltage. The problem is that battery voltage does not remain fixed. A fresh alkaline cell may measure above 1.5 V without a load. Its voltage then gradually falls as the battery discharges. The terminal voltage can also drop suddenly when the ESP32-S3 draws a high-current pulse. Directly connecting two AA cells to the 3V3 pin may therefore work initially but become unreliable as the batteries discharge. The ESP32-S3 may reset when Wi-Fi starts, fail to boot, or stop operating long before the batteries are completely empty.</p>
<p>To avoid this problem, we will use an <strong>MT3608 boost converter</strong> adjusted to provide a regulated 3.3 V output.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>What Is the MT3608?</strong></h2>
<p>The MT3608 is a small step-up or <strong>boost converter</strong>. A boost converter takes a lower DC voltage and raises it to a higher regulated voltage.</p>
<p>Common MT3608 breakout modules include:</p>
<ul>
<li>An MT3608 switching-regulator IC</li>
<li>An inductor</li>
<li>A Schottky diode</li>
<li>Input and output capacitors</li>
<li>A multi-turn adjustment potentiometer</li>
<li>IN+, IN−, OUT+, and OUT− connection pads</li>
</ul>
<p>The MT3608 IC has an input-voltage range of approximately 2 V to 24 V and an adjustable output. Its switching frequency is 1.2 MHz. For this project, the module raises the changing voltage from two AA cells to a regulated 3.3 V:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_a017593452a0a1194a9cc141ebf45f55.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="V_{\text{BATTERY}} \longrightarrow \text{MT3608} \longrightarrow 3.3\text{ V}" /></span><script type='math/tex'>V_{\text{BATTERY}} \longrightarrow \text{MT3608} \longrightarrow 3.3\text{ V}</script></p>
<p>The MT3608 is inexpensive and widely available, which makes it practical for a prototype or hobby project.</p>
<h3><strong>The MT3608 Is Boost-Only</strong></h3>
<p>An important limitation is that the MT3608 can only raise voltage. It cannot reduce a voltage that is higher than the desired output. For example, an MT3608 should not be used to convert a three-cell alkaline battery pack from approximately 4.5 V down to 3.3 V. That would require a buck converter or buck-boost converter instead. Two alkaline or two NiMH AA cells remain below the desired 3.3 V output during normal use, so they are suitable for this circuit.</p>
<h3><strong>MT3608 Current Ratings</strong></h3>
<p>Many online listings describe an MT3608 module as a “2 A boost converter.” This does not necessarily mean that the module can continuously supply 2 A at its output under all conditions.</p>
<p>The available output current depends on:</p>
<ul>
<li>Input voltage</li>
<li>Output voltage</li>
<li>Converter efficiency</li>
<li>Inductor rating</li>
<li>Diode rating</li>
<li>PCB layout</li>
<li>Cooling</li>
<li>Battery condition and internal resistance</li>
</ul>
<p>At a low battery voltage, the converter must draw more current from the batteries than it delivers at its output.</p>
<p>Ignoring converter losses for a moment:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_9c7711005537dd32c6d3a3dc34c612e8.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="V_{\text{IN}}I_{\text{IN}} \approx V_{\text{OUT}}I_{\text{OUT}}" /></span><script type='math/tex'>V_{\text{IN}}I_{\text{IN}} \approx V_{\text{OUT}}I_{\text{OUT}}</script></p>
<p>After including converter efficiency:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_d5cd53bf6b6d6502e7d45a47a78dae4e.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="I_{\text{IN}} = \frac{V_{\text{OUT}}I_{\text{OUT}}}{V_{\text{IN}}\eta}" /></span><script type='math/tex'>I_{\text{IN}} = \frac{V_{\text{OUT}}I_{\text{OUT}}}{V_{\text{IN}}\eta}</script></p>
<p>For example, supplying 500 mA at 3.3 V from a 2.0 V battery pack at 85% efficiency would require approximately:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_2401054c657c8d8638c3d133b2658c46.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="I_{\text{IN}} = \frac{3.3 \times 0.5}{2.0 \times 0.85}" /></span><script type='math/tex'>I_{\text{IN}} = \frac{3.3 \times 0.5}{2.0 \times 0.85}</script></p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_08bbb5dba71e84cc759afcb7a28838c3.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="I_{\text{IN}} \approx 0.97\text{ A}" /></span><script type='math/tex'>I_{\text{IN}} \approx 0.97\text{ A}</script></p>
<p>The ESP32-S3 will not continuously draw 500 mA in a typical sensor project, but the batteries and converter must tolerate its short radio-current peaks.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>AA Battery Configurations for the MT3608</strong></h2>
<p>Here is what happens with common AA battery arrangements:</p>
<table>
<thead>
<tr>
<th>Battery configuration</th>
<th>Approximate nominal voltage</th>
<th>Suitable with MT3608?</th>
</tr>
</thead>
<tbody>
<tr>
<td>One alkaline AA</td>
<td>1.5 V</td>
<td>No; below the MT3608’s specified input range</td>
</tr>
<tr>
<td>Two alkaline AAs</td>
<td>3.0 V</td>
<td>Yes; boost to 3.3 V</td>
</tr>
<tr>
<td>Three alkaline AAs</td>
<td>4.5 V</td>
<td>No; the MT3608 cannot step down to 3.3 V</td>
</tr>
<tr>
<td>Four alkaline AAs</td>
<td>6.0 V</td>
<td>No; use a step-down converter</td>
</tr>
<tr>
<td>Two NiMH AAs</td>
<td>2.4 V</td>
<td>Yes; boost to 3.3 V</td>
</tr>
<tr>
<td>Three NiMH AAs</td>
<td>3.6 V</td>
<td>No; the pack may be above the required output</td>
</tr>
</tbody>
</table>
<p>Two AA cells are therefore the most convenient arrangement for this MT3608 circuit.</p>
<p><img data-dominant-color="d7d6d1" data-has-transparency="false" style="--dominant-color: #d7d6d1;" loading="lazy" decoding="async" class="aligncenter size-full wp-image-12303 not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2026/07/esp32-powering-options.avif" alt="ESP32-S3 Powering Options" width="836" height="471" srcset="https://www.teachmemicro.com/wp-content/uploads/2026/07/esp32-powering-options.avif 836w, https://www.teachmemicro.com/wp-content/uploads/2026/07/esp32-powering-options-300x169.avif 300w, https://www.teachmemicro.com/wp-content/uploads/2026/07/esp32-powering-options-768x433.avif 768w" sizes="auto, (max-width: 836px) 100vw, 836px" /></p>
<p>Because the MT3608 requires approximately 2 V at its input, the converter will eventually stop operating when a two-cell pack falls to about 1 V per cell.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Required Components</strong></h2>
<p>For the basic ESP32-S3 AA battery circuit, you will need:</p>
<ul>
<li>ESP32-S3 development board</li>
<li>MT3608 adjustable boost-converter module</li>
<li>Two-cell AA battery holder</li>
<li>Two matching AA batteries</li>
<li>On/off switch</li>
<li>470 µF electrolytic capacitor rated for at least 6.3 V</li>
<li>0.1 µF ceramic capacitor</li>
<li>Multimeter</li>
<li>Jumper wires or soldered connections</li>
</ul>
<p>For battery-voltage measurement, you will also need:</p>
<ul>
<li>Two 100 kΩ resistors</li>
<li>One optional 0.1 µF capacitor</li>
</ul>
<p>Use batteries of the same type, brand, capacity, and approximate age. Do not mix alkaline batteries with rechargeable batteries in the same holder.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Adjusting the MT3608 to 3.3 V</strong></h2>
<p>The MT3608 is an adjustable converter. It must be configured before it is connected to the ESP32-S3.</p>
<blockquote><p><strong>Do not connect the MT3608 output to the ESP32-S3 until you have measured and adjusted its output voltage.</strong> An incorrectly adjusted module can output a voltage high enough to damage the board.</p></blockquote>
<p>Follow these steps:</p>
<ol>
<li>Insert the two AA batteries into the holder.</li>
<li>Connect the battery holder’s positive wire to MT3608 IN+.</li>
<li>Connect the battery holder’s negative wire to MT3608 IN−.</li>
<li>Set your multimeter to DC voltage.</li>
<li>Measure between MT3608 OUT+ and OUT−.</li>
<li>Turn the adjustment potentiometer slowly while watching the multimeter.</li>
<li>Stop when the output reads approximately 3.30 V.</li>
<li>Disconnect the batteries before wiring the ESP32-S3.</li>
</ol>
<p>The potentiometer on many MT3608 modules is multi-turn. It may require several rotations before the output voltage visibly changes. Do not assume that every module adjusts in the same direction. Watch the multimeter while turning the potentiometer.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>ESP32-S3 AA Battery Wiring Diagram</strong></h2>
<p>After adjusting the MT3608 to 3.3 V, connect the complete circuit as follows:<br />
<img data-dominant-color="d5d2d1" data-has-transparency="false" style="--dominant-color: #d5d2d1;" loading="lazy" decoding="async" class="aligncenter size-full wp-image-12300 not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2026/07/ESP32-S3-AA-battery-circuit.avif" alt="ESP32-S3-AA-battery-circuit" width="849" height="539" srcset="https://www.teachmemicro.com/wp-content/uploads/2026/07/ESP32-S3-AA-battery-circuit.avif 849w, https://www.teachmemicro.com/wp-content/uploads/2026/07/ESP32-S3-AA-battery-circuit-300x190.avif 300w, https://www.teachmemicro.com/wp-content/uploads/2026/07/ESP32-S3-AA-battery-circuit-768x488.avif 768w" sizes="auto, (max-width: 849px) 100vw, 849px" /><br />
Observe the polarity of the electrolytic capacitor:</p>
<ul>
<li>Capacitor positive lead to MT3608 OUT+</li>
<li>Capacitor negative lead to ground</li>
</ul>
<p>The 470 µF capacitor helps supply short bursts of current when the ESP32-S3 radio begins transmitting. The 0.1 µF ceramic capacitor helps suppress higher-frequency noise. Install the capacitors close to the ESP32-S3 power pins rather than close to the battery holder. Long wires add resistance and inductance, reducing the capacitors’ ability to support fast current pulses.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Why We Connect the MT3608 to the 3V3 Pin</strong></h2>
<p>Most ESP32-S3 development boards include an onboard regulator that converts USB or 5 V input to 3.3 V.</p>
<p>One option would be to adjust the MT3608 to 5 V and connect it to the board’s 5V input. However, the power would pass through two conversion stages:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_11baa0169c342cec13b386afdb3ea105.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="\text{AA batteries} \longrightarrow 5\text{ V boost} \longrightarrow 3.3\text{ V regulator}" /></span><script type='math/tex'>\text{AA batteries} \longrightarrow 5\text{ V boost} \longrightarrow 3.3\text{ V regulator}</script></p>
<p>Each conversion stage wastes some energy.</p>
<p>By adjusting the MT3608 to 3.3 V and connecting it directly to the 3V3 pin, we bypass the development board’s onboard regulator:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_1865d1c2872fe8631aeb1f9bd7284218.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="\text{AA batteries} \longrightarrow 3.3\text{ V boost} \longrightarrow \text{ESP32-S3}" /></span><script type='math/tex'>\text{AA batteries} \longrightarrow 3.3\text{ V boost} \longrightarrow \text{ESP32-S3}</script></p>
<p>The disadvantage is that the board no longer has another regulator between the MT3608 and the ESP32-S3. The MT3608 output must therefore be adjusted accurately and checked before every major wiring change.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Important USB Power Warning</strong></h2>
<p>Disconnect the AA battery supply before connecting the ESP32-S3 to USB unless the board or external circuit includes proper power-source isolation.</p>
<p>Connecting USB power and an external 3.3 V supply at the same time may cause current to flow backward into one of the power sources.</p>
<p>A simple approach during development is:</p>
<ol>
<li>Switch off or disconnect the AA battery holder.</li>
<li>Connect USB and upload the sketch.</li>
<li>Disconnect USB.</li>
<li>Reconnect or switch on the battery supply.</li>
</ol>
<p>A more advanced design can use:</p>
<ul>
<li>Schottky-diode isolation</li>
<li>An ideal-diode controller</li>
<li>A load switch</li>
<li>A power multiplexer</li>
<li>A jumper that selects USB or battery power</li>
</ul>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Adding an ESP32-S3 Battery Voltage Monitor</strong></h2>
<p>The ESP32-S3 can measure the battery voltage using one of its ADC pins. The measurement must be taken from the battery side of the MT3608. Measuring the converter output would normally show approximately 3.3 V even as the batteries discharge.</p>
<p>For a two-cell AA pack, use the following voltage divider:<br />
<img data-dominant-color="d5d1cf" data-has-transparency="false" style="--dominant-color: #d5d1cf;" loading="lazy" decoding="async" class="aligncenter size-full wp-image-12301 not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2026/07/ESP32-S3-AA-battery-level-detection.avif" alt="ESP32 S3 AA battery detection" width="805" height="526" srcset="https://www.teachmemicro.com/wp-content/uploads/2026/07/ESP32-S3-AA-battery-level-detection.avif 805w, https://www.teachmemicro.com/wp-content/uploads/2026/07/ESP32-S3-AA-battery-level-detection-300x196.avif 300w, https://www.teachmemicro.com/wp-content/uploads/2026/07/ESP32-S3-AA-battery-level-detection-768x502.avif 768w" sizes="auto, (max-width: 805px) 100vw, 805px" /><br />
GPIO4 is an ADC-capable pin on the ESP32-S3 and corresponds to ADC1 channel 3.</p>
<p>The two equal resistors divide the battery voltage by two:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_d0add98ad55c0cdc88048b507a466899.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="V_{\text{ADC}} = V_{\text{BATTERY}} \times \frac{100\,\text{k}\Omega}{100\,\text{k}\Omega + 100\,\text{k}\Omega}" /></span><script type='math/tex'>V_{\text{ADC}} = V_{\text{BATTERY}} \times \frac{100\,\text{k}\Omega}{100\,\text{k}\Omega + 100\,\text{k}\Omega}</script></p>
<p>Therefore:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_d130dfdb8911f0c0c85c0791bdfeae02.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="V_{\text{BATTERY}} = V_{\text{ADC}} \times 2" /></span><script type='math/tex'>V_{\text{BATTERY}} = V_{\text{ADC}} \times 2</script></p>
<p>A fresh two-cell alkaline pack measuring 3.2 V produces approximately:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_ec4685acde79bad4f6c18310f6c88ac9.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="V_{\text{ADC}} = \frac{3.2}{2} = 1.6\text{ V}" /></span><script type='math/tex'>V_{\text{ADC}} = \frac{3.2}{2} = 1.6\text{ V}</script></p>
<p>An optional 0.1 µF capacitor from GPIO4 to ground can reduce noise and help stabilize the ADC reading. This is especially useful because the two 100 kΩ resistors create a relatively high-impedance source for the ADC.</p>
<p>The resistor divider continuously draws a small current:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_43b8c72aa5b0a3b28486faa3fbb85209.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="I = \frac{3.0\text{ V}}{200\,\text{k}\Omega}" /></span><script type='math/tex'>I = \frac{3.0\text{ V}}{200\,\text{k}\Omega}</script></p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_600f9c5dd5d57ff227ea07eadc9434ac.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="I = 15\,\mu\text{A}" /></span><script type='math/tex'>I = 15\,\mu\text{A}</script></p>
<p>This is small compared with the consumption of a normal ESP32-S3 development board. However, it can become significant in a highly optimized low-power design. On a custom PCB, the voltage divider can be enabled only when a measurement is required by using a MOSFET or analog switch.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>ESP32-S3 AA Battery and Deep-Sleep Code</strong></h2>
<p>The following Arduino sketch reads the AA battery-pack voltage and then places the ESP32-S3 into deep sleep for ten minutes.</p>
<pre><pre><code class="language-cpp">#include &lt;Arduino.h&gt;

#define BATTERY_ADC_PIN       4
#define SLEEP_TIME_SECONDS    600

const float R1 = 100000.0;
const float R2 = 100000.0;

float readBatteryVoltage()
{
  const int numberOfSamples = 32;
  uint32_t totalMillivolts = 0;

  for (int i = 0; i &lt; numberOfSamples; i++)
  {
    totalMillivolts += analogReadMilliVolts(BATTERY_ADC_PIN);
    delay(5);
  }

  float adcMillivolts =
      totalMillivolts / (float)numberOfSamples;

  float adcVoltage = adcMillivolts / 1000.0;

  // Reconstruct the original battery voltage.
  float batteryVoltage =
      adcVoltage * ((R1 + R2) / R2);

  return batteryVoltage;
}

void setup()
{
  Serial.begin(115200);
  delay(1000);

  analogReadResolution(12);
  analogSetPinAttenuation(BATTERY_ADC_PIN, ADC_11db);

  float batteryVoltage = readBatteryVoltage();

  Serial.print(&quot;AA battery pack voltage: &quot;);
  Serial.print(batteryVoltage, 2);
  Serial.println(&quot; V&quot;);

  /*
   * Read sensors or transmit data here.
   *
   * Keep this section as short as possible.
   * Wi-Fi, Bluetooth and sensors consume much
   * more current than deep sleep.
   */

  Serial.println(&quot;Entering deep sleep...&quot;);
  Serial.flush();

  esp_sleep_enable_timer_wakeup(
      (uint64_t)SLEEP_TIME_SECONDS * 1000000ULL
  );

  esp_deep_sleep_start();
}

void loop()
{
  // The ESP32-S3 restarts setup() after waking
  // from deep sleep.
}</code></pre></pre>
<p>The Arduino-ESP32 API provides <em>analogReadMilliVolts()</em>, which returns a calibrated voltage value rather than a raw ADC count.</p>
<p>The 11 dB attenuation setting increases the input range available to the ADC. The divided battery voltage should remain well below the ESP32-S3 ADC pin’s maximum level.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>How the Deep-Sleep Code Works</strong></h2>
<p>The ESP32-S3 executes <em>setup()</em> after power-up and after every deep-sleep wake-up.</p>
<p><img data-dominant-color="eff1f2" data-has-transparency="false" style="--dominant-color: #eff1f2;" loading="lazy" decoding="async" class="aligncenter size-full wp-image-12302 not-transparent" src="https://www.teachmemicro.com/wp-content/uploads/2026/07/esp32-power-timeline.avif" alt="" width="836" height="471" srcset="https://www.teachmemicro.com/wp-content/uploads/2026/07/esp32-power-timeline.avif 836w, https://www.teachmemicro.com/wp-content/uploads/2026/07/esp32-power-timeline-300x169.avif 300w, https://www.teachmemicro.com/wp-content/uploads/2026/07/esp32-power-timeline-768x433.avif 768w" sizes="auto, (max-width: 836px) 100vw, 836px" /></p>
<p>The program performs the following sequence:</p>
<ol>
<li>Configures the ADC.</li>
<li>Reads the AA battery-pack voltage.</li>
<li>Performs the required sensor or communication task.</li>
<li>Configures a ten-minute wake-up timer.</li>
<li>Enters deep sleep.</li>
<li>Restarts when the timer expires.</li>
</ol>
<p>The sleep duration is configured here:</p>
<pre><pre><code class="language-cpp">#define SLEEP_TIME_SECONDS 600</code></pre></pre>
<p>For a five-minute interval, change it to:</p>
<pre><pre><code class="language-cpp">#define SLEEP_TIME_SECONDS 300</code></pre></pre>
<p>For a one-hour interval:</p>
<pre><pre><code class="language-cpp">#define SLEEP_TIME_SECONDS 3600</code></pre></pre>
<p>Deep sleep powers down the ESP32-S3 CPUs and most digital peripherals while retaining selected RTC circuitry. The ESP32-S3 chip itself can reach a deep-sleep current of only a few microamps under datasheet conditions. A complete development board will normally consume more because it may also contain:</p>
<ul>
<li>A voltage regulator</li>
<li>USB-to-UART circuitry</li>
<li>USB interface circuitry</li>
<li>A power indicator LED</li>
<li>An addressable RGB LED</li>
<li>Flash memory</li>
<li>PSRAM</li>
<li>Pull-up resistors</li>
<li>Other support components</li>
</ul>
<p>The MT3608 module also consumes current while operating, even when the ESP32-S3 is asleep.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Is the MT3608 Good for Deep-Sleep Projects?</strong></h2>
<p>The MT3608 is suitable for learning, prototyping, and inexpensive battery-powered projects. However, it is not necessarily the best converter for an ultra-low-power sensor node.</p>
<p>The total sleep current includes:</p>
<ul>
<li>ESP32-S3 development-board sleep current</li>
<li>MT3608 operating current</li>
<li>Any indicator LED on the MT3608 module</li>
<li>Battery-monitor divider current</li>
<li>Sensor standby current</li>
<li>Leakage through connected peripherals</li>
</ul>
<p>Some generic MT3608 modules include an indicator LED that remains on continuously. Even a small LED current can become significant when the ESP32-S3 spends most of its time sleeping. For a project intended to run for many months or years, consider a boost converter designed specifically for low-quiescent-current operation.</p>
<p>The MT3608 remains useful for this tutorial because it is:</p>
<ul>
<li>Inexpensive</li>
<li>Widely available</li>
<li>Easy to adjust</li>
<li>Easy to mount on a breadboard or prototype board</li>
<li>Capable of supplying an ESP32-S3 when properly selected and tested</li>
</ul>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Estimating ESP32-S3 AA Battery Runtime</strong></h2>
<p>Battery runtime depends more on average current than on the highest current peak.</p>
<p>A basic runtime estimate is:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_654b267138c4f0c0834bfd5a80ecf6f1.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="\text{Runtime (hours)} = \frac{\text{Battery capacity (mAh)}}{\text{Average battery current (mA)}}" /></span><script type='math/tex'>\text{Runtime (hours)} = \frac{\text{Battery capacity (mAh)}}{\text{Average battery current (mA)}}</script></p>
<p>Suppose the ESP32-S3:</p>
<ul>
<li>Draws 120 mA while awake</li>
<li>Remains awake for five seconds</li>
<li>Wakes every ten minutes</li>
<li>Draws 0.3 mA from the 3.3 V rail while sleeping</li>
</ul>
<p>Its average current on the regulated 3.3 V rail is approximately:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_d4adf6f552dd7113dd7b70281a1f2c21.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="I_{\text{AVG}} = 120\left(\frac{5}{600}\right) + 0.3\left(\frac{595}{600}\right)" /></span><script type='math/tex'>I_{\text{AVG}} = 120\left(\frac{5}{600}\right) + 0.3\left(\frac{595}{600}\right)</script></p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_90fbb09b1d4daaaa2174040ae0e6aa6e.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="I_{\text{AVG}} \approx 1.30\text{ mA}" /></span><script type='math/tex'>I_{\text{AVG}} \approx 1.30\text{ mA}</script></p>
<p>Assuming an average battery voltage of 2.4 V and an MT3608 efficiency of 85%:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_60ae932dd66e7154db3613aa6164d6d1.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="I_{\text{BATTERY}} = \frac{3.3 \times 1.30}{2.4 \times 0.85}" /></span><script type='math/tex'>I_{\text{BATTERY}} = \frac{3.3 \times 1.30}{2.4 \times 0.85}</script></p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_452da26b987489d60c867e59fa7fa1b5.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="I_{\text{BATTERY}} \approx 2.1\text{ mA}" /></span><script type='math/tex'>I_{\text{BATTERY}} \approx 2.1\text{ mA}</script></p>
<p>Using a conservative usable battery capacity of 2000 mAh:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_c72abbcb6bfa2b7690a9f18b72cfe64f.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="\text{Runtime} = \frac{2000}{2.1}" /></span><script type='math/tex'>\text{Runtime} = \frac{2000}{2.1}</script></p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_da97dfea8763830023f188315b1552f3.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="\text{Runtime} \approx 953\text{ hours}" /></span><script type='math/tex'>\text{Runtime} \approx 953\text{ hours}</script></p>
<p>This is approximately 40 days.</p>
<p>However, this simplified calculation does not include the MT3608 module’s own operating current. Suppose the converter module and its indicator LED draw an additional 1 mA from the battery pack:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_52a6d6edcc84af6d56c47c599f8b53ee.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="I_{\text{TOTAL}} = 2.1\text{ mA} + 1.0\text{ mA}" /></span><script type='math/tex'>I_{\text{TOTAL}} = 2.1\text{ mA} + 1.0\text{ mA}</script></p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_a6824aee4a2390796c7a3eb001e2b14a.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="I_{\text{TOTAL}} = 3.1\text{ mA}" /></span><script type='math/tex'>I_{\text{TOTAL}} = 3.1\text{ mA}</script></p>
<p>The estimated runtime would then become:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_e1a451a43fbb361634f6d3a4cff388c4.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="\text{Runtime} = \frac{2000}{3.1}" /></span><script type='math/tex'>\text{Runtime} = \frac{2000}{3.1}</script></p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_7e76d3781b30bc36a3ff78ac59018ba5.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="\text{Runtime} \approx 645\text{ hours}" /></span><script type='math/tex'>\text{Runtime} \approx 645\text{ hours}</script></p>
<p>This is approximately 27 days.</p>
<p>This example shows why converter idle current matters in a deep-sleep project.</p>
<p>Actual runtime may be lower because of:</p>
<ul>
<li>MT3608 quiescent and indicator-LED current</li>
<li>Battery internal resistance</li>
<li>Low-temperature operation</li>
<li>Wi-Fi connection time</li>
<li>Sensor consumption</li>
<li>Development-board LEDs</li>
<li>Battery age</li>
<li>Reduced alkaline capacity at higher discharge currents</li>
<li>The MT3608’s approximately 2 V minimum input</li>
<li>Conversion losses that change with load and battery voltage</li>
</ul>
<p>The best approach is to measure the current of the complete circuit using the actual batteries, converter module, sensors, development board, and firmware.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Alkaline or Rechargeable AA Batteries?</strong></h2>
<p>Both alkaline and NiMH AA batteries can be used with the MT3608.</p>
<h3><strong>Alkaline AA Batteries</strong></h3>
<p>Alkaline batteries have a nominal voltage of 1.5 V per cell. Two cells produce a nominal 3.0 V, which the MT3608 can raise to 3.3 V. Alkaline cells are widely available and have a relatively low self-discharge rate. However, their voltage continuously falls during use, and their effective capacity decreases under heavier loads. The high current pulses produced by Wi-Fi may cause a larger temporary voltage drop when alkaline batteries are old, cold, or partially discharged.</p>
<h3><strong>NiMH Rechargeable AA Batteries</strong></h3>
<p>NiMH batteries typically have a nominal voltage of 1.2 V per cell. Two cells therefore provide approximately:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_8601b2b1661b215f75a0dbb2a6a61d3b.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="2 \times 1.2\text{ V} = 2.4\text{ V}" /></span><script type='math/tex'>2 \times 1.2\text{ V} = 2.4\text{ V}</script></p>
<p>The MT3608 raises this to 3.3 V. NiMH batteries generally handle repeated high-current pulses better than ordinary alkaline batteries and can be recharged many times. For a frequently used wireless sensor, NiMH cells may be more economical. Low-self-discharge NiMH batteries are especially useful when the device spends long periods asleep.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Improving ESP32-S3 AA Battery Life</strong></h2>
<h3><strong>Use Deep Sleep</strong></h3>
<p>Do not leave the ESP32-S3 running continuously if it only needs to take a measurement every few minutes. Deep sleep normally provides a much larger improvement than simply reducing the CPU frequency while the chip remains awake.</p>
<h3><strong>Keep Wi-Fi Sessions Short</strong></h3>
<p>Connecting to Wi-Fi and transmitting data consume considerably more current than sleeping.</p>
<p>A battery-powered application should normally:</p>
<ol>
<li>Wake up.</li>
<li>Read its sensors.</li>
<li>Connect to the network.</li>
<li>Transmit its data.</li>
<li>Disconnect or shut down the radio.</li>
<li>Return to deep sleep.</li>
</ol>
<h3><strong>Remove or Disable Power LEDs</strong></h3>
<p>A power LED drawing even 1 mA becomes significant when the ESP32-S3 spends most of its time in deep sleep. Inspect both the ESP32-S3 board and the MT3608 module for always-on LEDs. Some boards allow the LED resistor to be removed. Others require cutting a trace. Only modify a board after inspecting its schematic and PCB connections.</p>
<h3><strong>Turn Sensors Off While Sleeping</strong></h3>
<p>Some sensors continue drawing current even while the ESP32-S3 is asleep. A load switch, P-channel MOSFET, or regulator-enable pin can disconnect sensor power before deep sleep.</p>
<h3><strong>Keep the Wires Short</strong></h3>
<p>Long, thin wires add resistance. A high current pulse can produce a temporary voltage drop across them:</p>
<p style="text-align: center;"><span class='MathJax_Preview'><img src='https://www.teachmemicro.com/wp-content/plugins/latex/cache/tex_6b4299cfa2a51ebe04bd563cc1a30371.gif' style='vertical-align: middle; border: none; padding-bottom:2px;' class='tex' alt="V_{\text{DROP}} = I \times R" /></span><script type='math/tex'>V_{\text{DROP}} = I \times R</script></p>
<p>Keep the battery, MT3608, capacitor, and ESP32-S3 power connections short and secure.</p>
<h3><strong>Use a Low-Power Converter for Long Deployments</strong></h3>
<p>The MT3608 is useful for prototyping, but a low-quiescent-current fixed-output converter is a better choice when maximizing runtime is more important than minimizing component cost.</p>
<h3><strong>Move to a Custom PCB</strong></h3>
<p>Development boards are excellent for prototyping, but their USB interface, LEDs, regulator, and support circuitry are not always optimized for battery operation. A custom PCB using an ESP32-S3 module and a low-power converter can achieve much lower sleep current.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Troubleshooting the ESP32-S3 MT3608 Circuit</strong></h2>
<h3><strong>The ESP32-S3 Resets When Wi-Fi Starts</strong></h3>
<p>This is usually caused by the 3.3 V rail dropping during a radio-current burst.</p>
<p>Check the following:</p>
<ul>
<li>MT3608 output voltage under load</li>
<li>Battery condition</li>
<li>Battery-holder contacts</li>
<li>Wire length and thickness</li>
<li>Output-capacitor value and placement</li>
<li>Solder joints</li>
<li>MT3608 module quality</li>
</ul>
<p>Place the 470 µF capacitor close to the ESP32-S3. Measure the 3.3 V rail while the ESP32-S3 starts Wi-Fi rather than measuring it only when the board is idle.</p>
<h3><strong>The MT3608 Output Does Not Change</strong></h3>
<p>The adjustment potentiometer may require many turns before the voltage changes. Continue turning it slowly while monitoring the output with a multimeter. Do not connect the ESP32-S3 while experimenting with the adjustment.</p>
<h3><strong>The MT3608 Cannot Reach 3.3 V</strong></h3>
<p>Measure the battery voltage at IN+ and IN−. The input may have fallen below the MT3608’s operating range, or the battery voltage may collapse when a load is connected.</p>
<p>Also inspect:</p>
<ul>
<li>Battery polarity</li>
<li>Loose holder contacts</li>
<li>Incorrect input/output wiring</li>
<li>A damaged converter module</li>
<li>An excessive load on the output</li>
</ul>
<h3><strong>The Board Works with USB but Not with Batteries</strong></h3>
<p>Measure the following voltages:</p>
<ol>
<li>Battery-pack voltage without a load</li>
<li>Battery-pack voltage with the ESP32-S3 connected</li>
<li>MT3608 input voltage</li>
<li>MT3608 output voltage</li>
<li>ESP32-S3 3V3-pin voltage during startup</li>
</ol>
<p>A reading of 3.3 V with no load does not guarantee that the converter can maintain 3.3 V during startup or Wi-Fi transmission.</p>
<h3><strong>The Battery Reading Is Too High or Too Low</strong></h3>
<p>Verify the actual resistor values and voltage-divider wiring. The battery-positive connection must come from the battery side of the MT3608. The ADC pin must connect to the junction of the two resistors. Average several ADC samples and compare the result with a multimeter. A small calibration factor can be added after testing:</p>
<pre><pre><code class="language-cpp">batteryVoltage *= 1.03;</code></pre></pre>
<p>Use a correction factor based on measurements from your own board and resistor divider.</p>
<h3><strong>The Deep-Sleep Current Is Still High</strong></h3>
<p>The ESP32-S3 chip may be sleeping correctly while the rest of the circuit continues drawing current.</p>
<p>Check:</p>
<ul>
<li>ESP32-S3 power LED</li>
<li>MT3608 indicator LED</li>
<li>USB-to-UART circuitry</li>
<li>MT3608 operating current</li>
<li>Sensor standby current</li>
<li>Battery voltage divider</li>
<li>External pull-up resistors</li>
<li>Other connected modules</li>
</ul>
<p>Measure current at the battery holder so that the reading includes the entire circuit rather than only the ESP32-S3 board.</p>
<div class="clear"></div><div style="margin-top:20px; margin-bottom:20px;" class="divider divider-solid"></div>
<h2><strong>Conclusion</strong></h2>
<p>Powering an ESP32-S3 with AA batteries is possible using a properly adjusted MT3608 boost converter. Two alkaline or two NiMH AA cells provide a suitable input for the MT3608. The module raises the changing battery voltage to a regulated 3.3 V that can be applied to the ESP32-S3 board’s 3V3 pin. Before connecting the development board, always use a multimeter to adjust the MT3608 output to 3.3 V. Add a large capacitor close to the ESP32-S3 to support Wi-Fi current bursts, and avoid connecting USB and external 3.3 V power simultaneously unless the sources are properly isolated.</p>
<p>The MT3608 is a practical and affordable choice for prototypes. However, its operating current and any onboard indicator LED can reduce the runtime of a deep-sleep project. Applications intended to operate for many months should use a converter specifically designed for low-quiescent-current battery operation. Battery life will ultimately depend on how long the ESP32-S3 remains awake. By reading sensors quickly, keeping Wi-Fi sessions short, turning off peripherals, and entering deep sleep between measurements, an ESP32-S3 AA battery project can operate for weeks or potentially months.</p>
<p>The post <a href="https://www.teachmemicro.com/how-to-power-an-esp32-s3-with-aa-batteries/">How to Power an ESP32-S3 with AA Batteries</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>PCB Material Types and Selection: A Practical Guide for Design</title>
		<link>https://www.teachmemicro.com/pcb-material-types-and-selection-a-practical-guide-for-design/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=pcb-material-types-and-selection-a-practical-guide-for-design</link>
		
		<dc:creator><![CDATA[Roland Pelayo]]></dc:creator>
		<pubDate>Wed, 15 Jul 2026 05:29:29 +0000</pubDate>
				<category><![CDATA[Features]]></category>
		<guid isPermaLink="false">https://www.teachmemicro.com/?p=12268</guid>

					<description><![CDATA[<p>If you've ever designed a board only to find it failing after a few thermal cycles, you already know the pain. The PCB material you choose determines everything—signal integrity, mechanical reliability, thermal performance, and ultimately whether your product survives in the field or ends up on a rework bench. Choosing the right substrate from the &#8230;</p>
<p>The post <a href="https://www.teachmemicro.com/pcb-material-types-and-selection-a-practical-guide-for-design/">PCB Material Types and Selection: A Practical Guide for Design</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><span style="font-weight: 400;">If you've ever designed a board only to find it failing after a few thermal cycles, you already know the pain. The PCB material you choose determines everything—signal integrity, mechanical reliability, thermal performance, and ultimately whether your product survives in the field or ends up on a rework bench. Choosing the right substrate from the dozens of available options is one of the most consequential decisions in PCB development.</span></p>
<p><span style="font-weight: 400;"> </span><span id="more-12268"></span></p>
<h2><b>Why PCB Material Selection Matters</b></h2>
<p><span style="font-weight: 400;">Substrate material affects nearly every aspect of board performance. Here's what changes when you switch materials:</span></p>
<table>
<thead>
<tr>
<th><b>Performance Area</b></th>
<th><b>Impact of Material Choice</b></th>
</tr>
</thead>
<tbody>
<tr>
<td><span style="font-weight: 400;">Signal Integrity</span></td>
<td><span style="font-weight: 400;">Dielectric constant (Dk) and dissipation factor (Df) determine signal loss at high frequencies</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Thermal Reliability</span></td>
<td><span style="font-weight: 400;">Glass transition temperature (Tg) and CTE control survival through soldering and thermal cycling</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Mechanical Strength</span></td>
<td><span style="font-weight: 400;">Flexural modulus and peel strength determine board rigidity and pad adhesion</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Moisture Resistance</span></td>
<td><span style="font-weight: 400;">Moisture absorption percentage affects insulation resistance in humid environments</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Manufacturing Yield</span></td>
<td><span style="font-weight: 400;">Material processability directly impacts drilling, plating, and lamination success</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Cost Per Board</span></td>
<td><span style="font-weight: 400;">Material cost varies by 3× to 20× between standard and specialty substrates</span></td>
</tr>
</tbody>
</table>
<p><span style="font-weight: 400;">A material that works perfectly for a Bluetooth IoT sensor may fail catastrophically in an automotive ECU exposed to 125°C underhood temperatures. Understanding the material properties and how they interact with your design constraints is critical to getting it right the first time.</span></p>
<p><span style="font-weight: 400;"> </span></p>
<h2><b>Common PCB Material Types</b></h2>
<h3><b>FR-4</b></h3>
<p><a href="https://pcbandassembly.com/blog/fr4-guide/"><span style="font-weight: 400;">FR-4</span></a><span style="font-weight: 400;"> is a woven fiberglass-reinforced epoxy laminate that accounts for over 90% of PCBs manufactured worldwide. It offers a strong balance of electrical insulation, mechanical strength, and cost-effectiveness.</span></p>
<p><span style="font-weight: 400;">FR-4 comes in several grades defined by glass transition temperature (Tg), which determines the temperature at which the material begins to soften and lose mechanical integrity.</span></p>
<table>
<thead>
<tr>
<th><b>FR-4 Grade</b></th>
<th><b>Tg Range</b></th>
<th><b>Typical Applications</b></th>
</tr>
</thead>
<tbody>
<tr>
<td><span style="font-weight: 400;">Standard FR-4</span></td>
<td><span style="font-weight: 400;">130–140°C</span></td>
<td><span style="font-weight: 400;">Low-cost consumer goods, single or double reflow processes</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Medium-Tg</span></td>
<td><span style="font-weight: 400;">150–160°C</span></td>
<td><span style="font-weight: 400;">General industrial, standard lead-free soldering</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">High-Tg</span></td>
<td><span style="font-weight: 400;">170–180°C</span></td>
<td><span style="font-weight: 400;">High-reliability, thick boards (&gt;2mm), servers, automotive</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Very High-Tg</span></td>
<td><span style="font-weight: 400;">200°C+</span></td>
<td><span style="font-weight: 400;">Harsh environments, aerospace, downhole drilling</span></td>
</tr>
</tbody>
</table>
<p><span style="font-weight: 400;">Standard FR-4 is typically sufficient for most consumer electronics and general-purpose designs. High-Tg becomes necessary when boards undergo multiple lead-free reflow cycles or operate in elevated temperature environments.</span></p>
<p><b>When to use FR-4:</b><span style="font-weight: 400;"> General-purpose digital and analog designs, frequencies below 1–2 GHz, cost-sensitive projects, prototype runs.</span></p>
<p><b>When NOT to use FR-4:</b><span style="font-weight: 400;"> High-frequency RF (&gt;2 GHz), extreme temperature environments, high-power thermal management, applications requiring tight Dk tolerance.</span></p>
<p><span style="font-weight: 400;"> </span></p>
<h3><b>High-Frequency / RF Laminates</b></h3>
<p><span style="font-weight: 400;">For designs operating above 1–2 GHz, FR-4's dielectric losses become unacceptable. High-frequency laminates use specialized resin systems and reinforcements to provide stable, low-loss electrical properties.</span></p>
<table>
<thead>
<tr>
<th><b>Material</b></th>
<th><b>Dielectric Constant (Dk)</b></th>
<th><b>Dissipation Factor (Df)</b></th>
<th><b>Best For</b></th>
</tr>
</thead>
<tbody>
<tr>
<td><span style="font-weight: 400;">Rogers RO4350B</span></td>
<td><span style="font-weight: 400;">3.48 ± 0.05</span></td>
<td><span style="font-weight: 400;">0.0037</span></td>
<td><span style="font-weight: 400;">5G, automotive radar, base station</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Rogers RO4003C</span></td>
<td><span style="font-weight: 400;">3.38 ± 0.05</span></td>
<td><span style="font-weight: 400;">0.0027</span></td>
<td><span style="font-weight: 400;">General RF, LNA, filter networks</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">RT/duroid 5880</span></td>
<td><span style="font-weight: 400;">2.20 ± 0.02</span></td>
<td><span style="font-weight: 400;">0.0009</span></td>
<td><span style="font-weight: 400;">Satellite, mmWave, ultra-low loss</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Taconic RF-35</span></td>
<td><span style="font-weight: 400;">3.50 ± 0.10</span></td>
<td><span style="font-weight: 400;">0.0028</span></td>
<td><span style="font-weight: 400;">Cost-effective RF, antenna</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Arlon 25N</span></td>
<td><span style="font-weight: 400;">3.38 ± 0.05</span></td>
<td><span style="font-weight: 400;">0.0025</span></td>
<td><span style="font-weight: 400;">Broadband RF, instrumentation</span></td>
</tr>
</tbody>
</table>
<p><span style="font-weight: 400;">These materials cost 4× to 10× more than standard FR-4, so engineers often use hybrid stackups—RF laminates on signal layers and FR-4 for the rest—to balance performance and cost.</span></p>
<p><b>When to use RF laminates:</b><span style="font-weight: 400;"> Microwave and millimeter-wave circuits, high-speed digital (&gt;10 Gbps), sensitive RF front-ends, impedance-critical designs.</span></p>
<p><span style="font-weight: 400;"> </span></p>
<h3><b>Aluminum / Metal-Core PCB (MCPCB)</b></h3>
<p><a href="https://pcbandassembly.com/blog/a-complete-guide-to-aluminum-pcb/"><span style="font-weight: 400;">Aluminum PCBs</span></a><span style="font-weight: 400;"> use a thin dielectric layer bonded to an aluminum or copper base plate. The metal core conducts heat away from power components far more effectively than FR-4.</span></p>
<p><b>Typical thermal conductivity values:</b></p>
<table>
<thead>
<tr>
<th><b>Material</b></th>
<th><b>Thermal Conductivity (W/m·K)</b></th>
</tr>
</thead>
<tbody>
<tr>
<td><span style="font-weight: 400;">Standard FR-4</span></td>
<td><span style="font-weight: 400;">0.3–0.4</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">High-Tg FR-4</span></td>
<td><span style="font-weight: 400;">0.5–0.7</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Aluminum MCPCB</span></td>
<td><span style="font-weight: 400;">1.0–3.0</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Ceramic-filled MCPCB</span></td>
<td><span style="font-weight: 400;">3.0–9.0</span></td>
</tr>
</tbody>
</table>
<p><b>Primary applications:</b><span style="font-weight: 400;"> High-power LED lighting, motor drivers, power converters, automotive headlamps, switch-mode power supplies.</span></p>
<p><span style="font-weight: 400;">The trade-off is that MCPCBs cannot support plated through-holes in the metal core area, which limits routing density and layer count to 1–2 layers typically.</span></p>
<p><span style="font-weight: 400;"> </span></p>
<h3><b>Polyimide (Flex / Rigid-Flex)</b></h3>
<p><span style="font-weight: 400;">Polyimide substrates offer exceptional thermal stability (Tg &gt;300°C) and mechanical flexibility, making them the standard material for flexible and rigid-flex PCBs.</span></p>
<table>
<thead>
<tr>
<th><b>Property</b></th>
<th><b>Polyimide</b></th>
<th><b>FR-4</b></th>
</tr>
</thead>
<tbody>
<tr>
<td><span style="font-weight: 400;">Continuous Operating Temp</span></td>
<td><span style="font-weight: 400;">250–300°C</span></td>
<td><span style="font-weight: 400;">130–180°C</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Flexural Capability</span></td>
<td><span style="font-weight: 400;">Bendable</span></td>
<td><span style="font-weight: 400;">Rigid</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Moisture Absorption</span></td>
<td><span style="font-weight: 400;">2.5–3.0%</span></td>
<td><span style="font-weight: 400;">0.1–0.15%</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Relative Cost</span></td>
<td><span style="font-weight: 400;">3–5× FR-4</span></td>
<td><span style="font-weight: 400;">Baseline</span></td>
</tr>
</tbody>
</table>
<p><span style="font-weight: 400;">Polyimide is essential for applications requiring dynamic flexing, such as foldable devices, camera modules, and robotic joints. However, it absorbs significantly more moisture than FR-4, which must be accounted for in the assembly process—baking before reflow is mandatory.</span></p>
<p><span style="font-weight: 400;"> </span></p>
<h2><b>Key Material Properties Explained</b></h2>
<p><span style="font-weight: 400;">Understanding these properties is essential for making informed material selections.</span></p>
<h3><b>Glass Transition Temperature (Tg)</b></h3>
<p><span style="font-weight: 400;">Tg is the temperature at which the resin transitions from a rigid, glass-like state to a rubbery, softened state. Below Tg, the material maintains its mechanical and electrical integrity. Above Tg, the coefficient of thermal expansion increases dramatically (3–5×), which can stress plated through-holes and cause barrel cracking.</span></p>
<p><b>Rule of thumb:</b><span style="font-weight: 400;"> Select a material with Tg at least 25°C above your maximum operating and assembly temperatures.</span></p>
<h3><b>Dielectric Constant (Dk) and Dissipation Factor (Df)</b></h3>
<p><span style="font-weight: 400;">Dk determines signal propagation speed and impedance. A stable, consistent Dk across frequency and temperature is critical for high-speed and RF designs.</span></p>
<p><span style="font-weight: 400;">Df (also called loss tangent) measures how much signal energy is lost as heat in the dielectric. Lower Df means less signal attenuation.</span></p>
<table>
<thead>
<tr>
<th><b>Application</b></th>
<th><b>Dk Requirement</b></th>
<th><b>Df Requirement</b></th>
</tr>
</thead>
<tbody>
<tr>
<td><span style="font-weight: 400;">Digital logic (&lt;100 MHz)</span></td>
<td><span style="font-weight: 400;">3.5–5.0 (broad tolerance OK)</span></td>
<td><span style="font-weight: 400;">&lt;0.025</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">High-speed digital (&gt;1 Gbps)</span></td>
<td><span style="font-weight: 400;">±5% tolerance or better</span></td>
<td><span style="font-weight: 400;">&lt;0.010</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">RF / Microwave</span></td>
<td><span style="font-weight: 400;">±2% tolerance</span></td>
<td><span style="font-weight: 400;">&lt;0.005</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Millimeter-wave (30 GHz+)</span></td>
<td><span style="font-weight: 400;">±1% tolerance</span></td>
<td><span style="font-weight: 400;">&lt;0.002</span></td>
</tr>
</tbody>
</table>
<h3><b>Coefficient of Thermal Expansion (CTE)</b></h3>
<p><span style="font-weight: 400;">CTE describes how much the material expands when heated. The Z-axis (through-thickness) CTE is the most critical value because via barrels are constrained in this direction.</span></p>
<p><span style="font-weight: 400;">High Z-axis CTE mismatch between the laminate and copper plating is a leading cause of via barrel cracks during thermal cycling and lead-free soldering.</span></p>
<p><b>Target values:</b><span style="font-weight: 400;"> Z-axis CTE below Tg should be 40–60 ppm/°C for standard FR-4. High-reliability materials target 30–50 ppm/°C.</span></p>
<h3><b>UL 94 Flammability Rating</b></h3>
<p><span style="font-weight: 400;">Most electronics require UL 94 V-0 rated materials, which self-extinguish within 10 seconds of flame removal. This is the standard for consumer, industrial, automotive, and medical applications.</span></p>
<p><span style="font-weight: 400;">  </span></p>
<h2><b>Common Material Selection Mistakes</b></h2>
<h3><b>Over-specifying RF Materials</b></h3>
<p><span style="font-weight: 400;">Using Rogers RO4350B for a 500 MHz digital design is like buying a racing bicycle for a trip to the grocery store. It works, but you're paying 4× more than necessary. FR-4 performs adequately below 1–2 GHz for most applications.</span></p>
<p><b>Fix:</b><span style="font-weight: 400;"> Always check whether your design actually needs tight Dk tolerance. If your highest frequency component runs at 200 MHz and you're not doing impedance-critical routing, standard FR-4 will work fine.</span></p>
<p><span style="font-weight: 400;"> </span></p>
<h3><b>Ignoring CTE Mismatch in Hybrid Stackups</b></h3>
<p><span style="font-weight: 400;">Combining FR-4 with Rogers or polyimide in the same stackup creates CTE mismatches that can cause delamination or via cracking during thermal cycling. Each material expands at a different rate, stressing the interfaces between layers.</span></p>
<p><b>Fix:</b><span style="font-weight: 400;"> Work with your fabricator to understand available hybrid material combinations and verified bonding schedules. Not all material combinations are reliable.</span></p>
<p><span style="font-weight: 400;"> </span></p>
<h3><b>Neglecting Moisture Sensitivity</b></h3>
<p><span style="font-weight: 400;">Polyimide absorbs up to 3% moisture, while FR-4 absorbs only 0.1%. If you specify polyimide for a flex section without accounting for moisture in your assembly process, you risk delamination during reflow soldering.</span></p>
<p><b>Fix:</b><span style="font-weight: 400;"> Specify pre-bake requirements for moisture-sensitive materials. IPC-J-STD-033 provides guidelines for handling moisture-sensitive components and substrates.</span></p>
<p><span style="font-weight: 400;"> </span></p>
<h3><b>Specifying Unavailable Material Grades</b></h3>
<p><span style="font-weight: 400;">Many exotic laminates have minimum order quantities at distributors or long lead times. If you specify a rare IPC-4101 slash sheet without checking availability, you may delay your project by weeks.</span></p>
<p><b>Fix:</b><span style="font-weight: 400;"> Discuss material availability with your </span><a href="https://pcbandassembly.com/pcb-manufacturing/"><span style="font-weight: 400;">circuit board fabrication</span></a><span style="font-weight: 400;"> partner early in the design phase. A fab with broad material sourcing experience and robust circuit board fabrication capabilities can suggest equivalent or similar materials that are in stock. For a comprehensive overview of available laminates, see our complete guide to PCB material selection.</span></p>
<p><span style="font-weight: 400;"> </span></p>
<h2><b>Cost Comparison by Material Type</b></h2>
<p><span style="font-weight: 400;">Material cost is typically 10–20% of total PCB fabrication cost, but the choice of substrate has a significant impact on overall board price.</span></p>
<table>
<thead>
<tr>
<th><b>Material</b></th>
<th><b>Relative Cost (vs FR-4)</b></th>
<th><b>Typical Lead Time</b></th>
</tr>
</thead>
<tbody>
<tr>
<td><span style="font-weight: 400;">Standard FR-4</span></td>
<td><span style="font-weight: 400;">1.0× (baseline)</span></td>
<td><span style="font-weight: 400;">In stock</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">High-Tg FR-4</span></td>
<td><span style="font-weight: 400;">1.2–1.5×</span></td>
<td><span style="font-weight: 400;">In stock</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Halogen-free FR-4</span></td>
<td><span style="font-weight: 400;">1.3–1.6×</span></td>
<td><span style="font-weight: 400;">1–2 weeks</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">CEM-1 / CEM-3</span></td>
<td><span style="font-weight: 400;">0.7–0.9×</span></td>
<td><span style="font-weight: 400;">In stock</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Rogers RO4350B</span></td>
<td><span style="font-weight: 400;">4–6×</span></td>
<td><span style="font-weight: 400;">1–3 weeks</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Rogers RT/duroid 5880</span></td>
<td><span style="font-weight: 400;">8–12×</span></td>
<td><span style="font-weight: 400;">2–4 weeks</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Aluminum MCPCB</span></td>
<td><span style="font-weight: 400;">2–4×</span></td>
<td><span style="font-weight: 400;">1–2 weeks</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Polyimide (flex)</span></td>
<td><span style="font-weight: 400;">3–5×</span></td>
<td><span style="font-weight: 400;">1–2 weeks</span></td>
</tr>
</tbody>
</table>
<p><b>Cost-saving tip:</b><span style="font-weight: 400;"> For mixed-material designs, use the expensive laminate only on the signal layers that require it. A 4-layer hybrid stackup with Rogers on the outer layers and FR-4 core can save 40–60% compared to an all-Rogers board.</span></p>
<p><span style="font-weight: 400;"> </span></p>
<h2><b>Making the Right Choice</b></h2>
<p><span style="font-weight: 400;">The right PCB material depends on your specific combination of electrical, thermal, mechanical, and cost requirements. Here's a quick summary of which material to choose based on your dominant design constraint:</span></p>
<table>
<thead>
<tr>
<th><b>If Your Priority Is…</b></th>
<th><b>Start With…</b></th>
</tr>
</thead>
<tbody>
<tr>
<td><span style="font-weight: 400;">Lowest cost</span></td>
<td><span style="font-weight: 400;">Standard FR-4</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">High-frequency / low loss</span></td>
<td><span style="font-weight: 400;">Rogers RO4350B or RO4003C</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Extreme temperature</span></td>
<td><span style="font-weight: 400;">High-Tg FR-4 or Polyimide</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Heat dissipation</span></td>
<td><span style="font-weight: 400;">Aluminum MCPCB</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Flexible form factor</span></td>
<td><span style="font-weight: 400;">Polyimide</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">Regulated industry (medical/auto/aero)</span></td>
<td><span style="font-weight: 400;">High-Tg FR-4 with appropriate certifications</span></td>
</tr>
<tr>
<td><span style="font-weight: 400;">High-speed digital (&gt;10 Gbps)</span></td>
<td><span style="font-weight: 400;">Low-loss RF laminates (RO4003C, Megtron 6)</span></td>
</tr>
</tbody>
</table>
<p><span style="font-weight: 400;"> </span></p>
<h2><b>Frequently Asked Questions</b></h2>
<h3><b>What is the most commonly used PCB material?</b></h3>
<p><span style="font-weight: 400;">FR-4 is by far the most common PCB material, used in over 90% of all PCBs manufactured. It offers a good balance of electrical insulation, mechanical strength, and cost-effectiveness for frequencies below 1–2 GHz.</span></p>
<h3><b>How do I choose between FR-4 and Rogers materials?</b></h3>
<p><span style="font-weight: 400;">Choose FR-4 for general-purpose designs below 1–2 GHz, non-critical impedance control, and cost-sensitive projects. Choose Rogers or other RF laminates when your design operates above 2 GHz, requires tight Dk tolerance (±2% or better), or needs low loss tangent for signal integrity at high frequencies.</span></p>
<h3><b>What does Tg mean in PCB materials?</b></h3>
<p><span style="font-weight: 400;">Tg (glass transition temperature) is the temperature at which the PCB substrate begins to soften from a rigid, glass-like state to a rubbery state. Below Tg, the material maintains its mechanical and electrical properties. Above Tg, thermal expansion increases dramatically, which can stress plated through-holes.</span></p>
<h3><b>Is high-Tg FR-4 always better than standard FR-4?</b></h3>
<p><span style="font-weight: 400;">Not necessarily. High-Tg FR-4 costs 20–50% more than standard FR-4 and offers no advantage in designs that never experience elevated temperatures. Use high-Tg only when your board will undergo multiple lead-free reflow cycles or operate above 100°C continuously.</span></p>
<h3><b>Can I use aluminum PCB for RF designs?</b></h3>
<p><span style="font-weight: 400;">Aluminum MCPCBs are not suitable for RF or high-speed designs. The thick dielectric layer required for electrical isolation limits impedance control and introduces high losses at frequency. Aluminum-core boards are optimized for thermal management, not signal integrity.</span></p>
<h3><b>What materials are used for flexible PCBs?</b></h3>
<p><span style="font-weight: 400;">Polyimide (typically Kapton) is the standard material for flexible and rigid-flex PCBs. It offers excellent thermal stability (Tg &gt;300°C) and mechanical flexibility. PET and PEN are used in low-cost consumer applications but cannot withstand soldering temperatures.</span></p>
<h3><b>How do I verify my chosen material meets regulatory requirements?</b></h3>
<p><span style="font-weight: 400;">Check the material's UL 94 flammability rating (V-0 is standard for most electronics), RoHS compliance status, and any industry-specific requirements (IATF 16949 for automotive, ISO 13485 for medical). Request the laminate manufacturer's certification data sheet from your fabricator.</span></p>
<h3><b>What is a hybrid PCB stackup?</b></h3>
<p><span style="font-weight: 400;">A hybrid stackup uses two or more different laminate materials in the same board. The most common example is Rogers RO4350B on the outer (RF) signal layers with FR-4 cores for the remaining layers. This approach balances high-frequency performance with cost control.</span></p>
<p><span style="font-weight: 400;"> </span></p>
<h2><b>Conclusion</b></h2>
<p><span style="font-weight: 400;">Selecting the right PCB material doesn't have to be overwhelming. Start with FR-4 for general-purpose designs, and only move to specialty materials when your specific requirements demand it—RF performance above 2 GHz, extreme temperatures, thermal management, or flex applications.</span></p>
<p>The post <a href="https://www.teachmemicro.com/pcb-material-types-and-selection-a-practical-guide-for-design/">PCB Material Types and Selection: A Practical Guide for Design</a> appeared first on <a href="https://www.teachmemicro.com">Microcontroller Tutorials</a>.</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: www.teachmemicro.com @ 2026-08-06 17:36:38 by W3 Total Cache
-->