The newest revision of the Open Hardware Laser Scanner board is finally alive! Upgrading to a 4-layer design packed with extra safety features was 100% the right call. Of course, getting here wasn't without its hurdles. Here is a quick rundown of the crazy bugs I ran into getting this revision off the ground:
The Rogue Power Supply: My Vevor DC power supply completely failed and suddenly started dumping 48V into my 24V rail (I'm pretty sure that violates a regulation or two). Luckily, my TVS diode did exactly what it was supposed to do and saved the board. In my confusion ("wait, why is this broken?"), I measured 24V, removed the diode to test, and accidentally hit the unprotected board with 48V. Incredibly, the board survived! Note to self: always trust the TVS diode.
The Silent TMC2209s (An ESP-IDF Gotcha): My TMC2209 stepper drivers still weren't communicating, which was actually one of the main reasons I spun this new board in the first place. It turns out the culprit was a sneaky change in the ESP-IDF toolchain that altered the UART protocol. Previously, the protocol would let the transmission line float after a successful transfer; now, it actively ties it to 0V. Because of the high drive strength of the ESP32-S3, the standard 1K resistor used between TX and RX for single-wire UART isn't enough anymore. Bumping that resistor up to ~5.4K ohms fixed the issue completely.
A Hot Camera Mistake: I learned the hard way that the camera FFC connector I used was a top-contact style, not bottom-contact. I plugged the ribbon cable in backwards. Fortunately, the camera survived the ordeal, though it got alarmingly hot before I caught the mistake
Minor Schematic Gremlins: The schematic for a basic button was incorrect. A minor inconvenience compared to the rest, but fixed nonetheless.
All in all, the V2 board is a massive step up. The 4-layer stackup makes routing so much cleaner, and having those extra safety features already paid off. Onward to the next step of the build!
Board was purchased from PCBway. We thank PCBway for the sponsorship of the previous board and they will be featured in upcoming video. For now it was easier to simply purchase instead of relying on sponsorship.
Building a high-speed polygon laser scanner from scratch is a fantastic engineering flex, but reproducing one has always been a barrier for the average maker. Previously, if you wanted to build this specific ESP32/FPGA-driven polygon scanner, you had to source and modify a custom Felix 3D printer frame. It was a brilliant proof-of-concept, but hardly accessible.
Now, the project has undergone a massive overhaul, and the entry barrier just hit the floor: the new hardware is designed as a direct, drop-in extension for the ubiquitous Vevor CNC3018 Pro. You replace the controller board but keep using everything else; power source, spindle, fan, encasing, frame.
By targeting one of the cheapest and most widely available desktop CNC machines on the market, anyone can now turn a standard 3018 frame into a high-speed, closed-loop laser scanning platform. Here is a look under the hood at what makes this new iteration so impressive.
Consolidation: 4 Layers and No More Ribbon Cables
The previous architecture relied on a complex two-board setup for the control electronics: a compute module and an extension board. Those have now been beautifully merged into a single, unified 4-layer PCB. Moving to 4 layers allowed for proper, unbroken ground planes—an absolute necessity when you have an ESP32-S3 blasting Wi-Fi, a Lattice uP5k FPGA switching at high frequencies, and stepper motor drivers all crammed onto an 88x70mm footprint.
Interestingly, the designer leaned heavily on AI to assist with the board’s power management and component selection. From sizing the correct PTC resettable fuses for motor inrush currents to calculating thermal via arrays for the FPGA’s exposed pad, AI was used as a sounding board to bulletproof the power tree.
The FFC Lifeline: Keeping the Optics Pristine
While the brains of the operation have been completely overhauled, the project made a very smart design choice: the laser head remains completely separate.
The delicate optical assembly—housing the polygon mirror, the high-speed laser, and the photodiode—sits on its own dedicated board. It hasn't changed at all. Integration is not possible as the laserdriver has to be close to the laser and opamps close to the photo diode.Instead of trying to integrate everything, the new 3018 control board connects to this pristine optical head using a clean, 20-pin Flat Flexible Cable (FFC). This connector can only be used so many times (up to 20 times) but is backward compatible and cheaply available.
Integrated Vision: Ditching the Pi
In the older setup, if you wanted machine vision for alignment or scanning, you had to tether a Raspberry Pi to the rig. That’s now a thing of the past.
The new board integrates a direct interface for an OV2640 camera module. By offloading the rigid, microsecond-level laser timing to the FPGA, the ESP32-S3 has the breathing room to handle the camera’s 24MHz data bus directly. It’s a leaner, cheaper, and significantly more elegant solution.
24 Volts and Future-Proofing
To get better performance out of the stepper motors and support beefier laser modules, the board's power architecture has been bumped up from 12V to 24V. Vehor also supplies 24V DC power packs and not 12V.
But the best part of this being a 3018 board drop-in replacement? It actually fits perfectly inside the stock Vevor CNC3018 Pro plastic enclosure and connectors. The hardware has even been provisioned to drive the stock 3018 spindle and cooling fans. While the software to run the spindle isn't quite finished yet, the silicon is there and waiting for a pull request.
By taking an advanced optical project and mapping it onto commodity CNC hardware, this project just moved from "cool prototype" to a platform that makers can actually build and iterate on.
Sometimes in hardware design, you have to take a step back to move forward! I was ready to finalize my effors with a video using facet correction to make a PCB, but I've hit a wall: my stepper drivers are dead. After doing some digging and testing multiple setups, I found the culprit. The drivers were only surviving on a wing, a prayer, and a few hacks.To do this right, I'm retiring the V1 board and starting fresh.
Here is the roadmap for the new design:
Drop the Pi4 for the camera: Using the ESP32-S3 directly for a rock-solid camera connection.
Better Optics: Designing proper mounts for the cylinder lenses.
Better Wiring: Natively fixing the TMC2209 connection bugs.
Better Chassis: Ditching the Felix frame for a CNC 3018 as this is more widely available and fully integrating the camera into the machine.
Realistically, this means physical video updates are on pause, though I’ll try to share some 3D renders of the new setup soon. But honestly? I'm thrilled. The facet correction concept is officially proven in the camera setup, so I know this redesign will be entirely worth the effort.
If you've been following along with the Prism Laser Scanner, you know that moving from a wobbly galvanometer to a high-speed rotating prism introduces a whole new nightmare of optical aberrations. In my previous logs, we could clearly see massive orthogonal errors (pyramidal tilt between facets) and scan line jitter (timing differences). This is due to that each facet pair is not perfectly planar and at 90 degrees, also the mounting is not exactly at 90 degrees.
I am thrilled to report that the main technical challenge seems fixed. We can now correct for facet distortion both along (scan) and orthogonal (orth) to the scanline and create perfect exposures. The hardware might be cheap and DIY, but the correction algorithm now guarantees industrial-grade results. It allows you to use all four facets and not use additional lenses and still have top results. More proof will follow, just look at the commit history https://github.com/hstarmans/hexastorm/commits/master/ and results below to gauge the effort required.
Here is how we got there.
1. The Median Average Reference
Previously, the calibration relied on picking a single facet as the "master" reference. The problem? If that specific facet was an outlier, it skewed the entire baseline. Furthermore, the coefficients of one facet were fixed to zero, so we didn't use all coefficients.
I achieved a major stability upgrade by ditching the single-facet reference. The system now calculates a virtual "median average" across all facets. This spatial median acts as a perfect, idealized virtual facet that we use to anchor the correction math, preventing any single highly-skewed facet from throwing off the calibration table.
2. Proving Statistical Stability
To prove this wasn't just a one-off lucky measurement, I automated a baseline calibration cycle to take 10 consecutive measurements. The results show that our underlying hardware timing is remarkably stable. A challenge with my previous log is that you just look at one result. How repeatable is this?
Here is the raw statistical payload using 10 measurements per facet. Notice the standard deviations (std_um): our orthogonal jitter is hovering between 0.25 and 0.8 microns, and scan jitter is around 0.5 to 1.0 microns. We have a spot diameter of 40 microns and eccentricity of around 1.5. The spot is dependent on how many neutral density filters. I use if i use all, I measure around 25 microns. Also, vibration artifacts can be removed further with cylinder optics.
Facet
Scan Shift (mm)
Orth Shift (mm)
Angle (°)
Scan Std (µm)
Orth Std (µm)
Scan Spread PtP (µm)
Spot Size (µm)
0
0.0229
0.0109
-86.5079
1.063
0.257
3.118
40.366
1
-0.0452
-0.0306
-86.3956
1.020
0.244
2.973
43.464
2
0.0032
-0.0109
-86.5607
0.476
0.257
1.853
40.266
3
-0.0032
0.0569
-86.3327
0.476
0.809
1.853
41.549
3. Verification: The 2D Dot Grid Simulation
To visually and mathematically prove the correction works, I wrote a routine that measures the 2D error by projecting a dot grid pattern.
Because my global shutter camera is stationary, we can't physically move the stage to expose a 2D area. I create a SVG with a dot grid, send it to the slicer using calbiration and projects it onto the camera sensor line by line. It then simulates the physical movement of the stage by stacking these individual 1D exposures into a single 2D image using a custom LaserStackSimulator class.
The routine evaluates the hardware twice: once without correction, and once with the active calibration profile fed into the slicer. It verifies if the optical correction forces the dots from all 4 facets to align perfectly. There is an algorithm which detects the centroids of the final dot pattern an matches them. It then computes the average offset.
The Results
The log output speaks for itself:
15:28:48 - INFO - Uncorrected 2D Errors (um) [Scan, Orth]: [[20.073, 11.351], [-46.93, -28.717], [2.655, -11.351], [-2.655, 59.795]]
15:28:48 - INFO - Corrected 2D Errors (um) [Scan, Orth]: [[-1.139, 0.704], [-2.443, 1.537], [2.555, -2.106], [1.667, -1.246]]
15:28:48 - INFO - --- 2D Calibration Summary ---
15:28:48 - INFO - Overall MAE Before: 22.94 um | MAE After: 1.67 um
15:28:48 - INFO - Overall Max Error Before: 59.80 um | Max Error After: 2.56 um
15:28:48 - INFO - Post-Correction Max Scan: 2.56 um | Max Orth: 2.11 um
15:28:48 - INFO - SUCCESS: 2D System calibrated to within +/- 30.0 microns.
Without correction, dots were landing up to ~80 microns away from their target (see -28 and +60 error for orthogonal error). After applying the dynamic calibration matrix, the maximum error was crushed down to 2.56 microns. The Mean Absolute Error dropped to just 1.67 microns. This engine now projects flawlessly. Results are wicked good, most likely your development chemistry will be a bigger limit than the laser head. There is a wide range of applications among other laser induced forward transfer.
Caveats & Next Steps
While the algorithm is sound, bringing this into the physical world has a few remaining challenges:
Real-World Exposures: I still need to run test images on actual PCBs and UV-sensitive cyanotype paper to see how the sub-pixel digital correction translates to chemical exposure.
Physical Modularity: Right now, the laser head needs to physically switch between the camera (for calibration) and the machine bed (for exposure). In the future it should be in one machine but it is now in different setups.
Alignment Sensitivity: The calibration matrix is incredibly sensitive to changes in the pointing of the laser bundle. Even a microscopic bump while moving the head from the camera to the machine might alter the facet timing or invalidate the calibration matrix. The errors don't change a lot but it is easy to get a different labeling of the facets. This sensitivity should be reduced if we switch between setups.
In the final, integrated design, the machine will likely need to perform a rapid calibration matrix scan in situ right before an exposure begins. This should be easy as the ESP32 can connect with a camera and global shutter suffices.
The math has been conquered, More to come as we start burning actual PCBs!
We all love laser scanners, but moving from a wobbly galvanometer to a high-speed rotating prism introduces a whole new nightmare of aberrations. In my previous blog posts, you clearly see that there is an orthogonal error between the scan lines. In addition, there are small timing differences (jitter). So far, no one has been able to handle, let alone quantify this.
My four-facet prism is spinning at 3000 RPM. While the motor controller is keeping the facet timing stable to within 0.2% (where the mean is 5ms per facet), each facet has a slightly different timing signature, which actually allows me to detect which facet is active.
To get usable data, I needed to map the imperfections of every single facet. "Close enough" wasn't going to cut it.
The Setup and The Hack
My optical setup involves a spinning prism directing a laser onto an OV2311 global shutter camera (2MP, with nice square 3.0μm pixels). To see what was happening, I set the camera exposure long (~1000ms) and modulated the laser with a sparse repetitive pattern: a single pulse followed by a long gap [1] * 1 + [0] * 39. This turned the scan lines into distinct strings of dots, freezing the scanner's behavior in time and space. The images are multiple exposures of a facet imposed on top of each other. You can see that the projection is extremely stable. All the smearing or ghost pixels are gone.
The Metrology
Using OpenCV, I grabbed images from all four facets. I detected the dots and fitted ellipses to find their precise sub-pixel centroids. By defining Facet 0 as the "golden" reference, I calculated the relative horizontal and vertical shifts for the other three faces.
The results gave me a perfect digital signature of the prism's physical defects:
Facet
Timing Shift (Scan μm)
Mechanical Tilt (Orth μm)
Spot Diameter (μm)
Eccentricity
0 (Ref)
0.0
0.0
23.023
1.307
1
18.171
4.177
22.35
1.464
2
13.45
77.707
24.801
1.577
3
44.907
48.889
22.826
1.363
The data is revealing. While the motor timing is stable, the timing synchronization (Scan) varies by over 44 microns between facets. More dramatically, we see massive pyramidal errors (orth-shift). Facets 1, 2, and 3 are all tilted significantly compared to Facet 0, shifting the scan line vertically by nearly 78μm. This is exactly what we were seeing before!
We also see high eccentricity (~1.6). This is expected as I don't use cylinder optics yet but a simple aspherical lens. Still, I am surpised. If the laser spot is 30 microns, we should be able to get much better results; previously, we never really got beyond 75 microns.
Verification
To prove the calibration model works, I did a lot. I repeated the measurements. I switched facets to see if the algorithm is stable. I moved the camera around the laser line to see if the result is consistent. This all proved to be the case. Finally, I generated a composite verification image by stacking the raw captures from all four facets ( a single image is seen all the way on the end).
Hollow colored circles (cyan, green, red, yellow) are the detected centroids of the actual laser spots.
Small red 'x' marks are where my calibration model predicts those spots should be after applying the calculated X and Y shifts.
As you can see, the red crosses land dead-center in the detected circles. With this calibration table, I can finally close the loop and feed these precise offsets into the slicer. As a final remark, LLMs are really accelerating the technology innovation here. I really think I am 3 to 4 times faster. Also some problems like, my pi's wifi is not working. Are fixed almost immediately, as the LLM states switch of your power management. This speeds up development 10-fold. As I would previously simply accept and not debug it.
All facets superimposed: Dots show the center of detected spots, while the 'x' marks show the model's approximation.Detailed analysis of Facet 0 used as the reference baseline.
Five months have passed since my last cross-scan test results.
Jitter and Scanline Error Resolution
The extended delay was primarily due to addressing extremely poor initial results from the jitter test, i.e. error along scanline. Resolving this critical issue required a deep dive and specific hardware/software adjustments. I will not outline all of the code optimizations but want to highlight two main changes:
1. Mirror Position Adjustment: The physical position of the mirror was moved, allowing the laser's position to be measured at the start of the scanline instead of the end. 2. Photodiode Trigger Algorithm Change: A key change was made to the algorithm governing the photodiode trigger timing. The algorithm was proposed by a LLM (https://github.com/hstarmans/hexastorm/blob/master/src/hexastorm/blocks/photodiode_debounce.py). These fixes now allow for successful photodiode detection and consistent measurement. The table below shows the resulting timing statistics for each facet (there are four). It's hard to stratify samples. I now set a minimum i.e. 32 which can result in more samples for other facets. Each row corresponds with a different facet. They are ordered sequentially.
Samples
Mean (ms)
Standard Deviation (ms)
32
5.00133
0.00076
451
5.00226
0.00037
34
4.99797
0.00036
458
4.99818
0.00031
Experimental Notes
Operational Parameters:
Laser Power: 130/255, both channels of laser driver Exposures: Four exposures are used per line. Facets: All facets were used, testing confirmed that using a single facet did not improve the overall results.
Lines seem to wide in the image, spacing should equal line width, I probably need to add an erosion operation.
Focusing Procedure:
Achieving accurate laser focus is critical. A white sheet of paper is placed on the exposure plane, and the laser is focused using a single channel of the laser diode driver at 133 a.u (digipot) to draw a test line. Successful focus is immediately confirmed by drawing a sharp, visible line (burning)
Future work
I need to pin facet data to certain facet, so I can use measurements to correct for each facet.
In the following images, the scan direction is along the x-axis direction. Lines are projected parallel to this direction. As such, the error in the y-axis represents the cross scan error. Substrate is solar-fotopapier, 20 blatt, format 14x19 cm. Using a scissor I cut a good sized rectangle (4x4 cm) Each line is exposed four times at a rate of 3000 RPM, i.e. 12000 facets per minute. Laser current is assumed to be 500 mA.
Using all four facets
Using a single facet
With a single facet lines are much sharper and 75 micron seems achievable. Stitching between lanes is a challenge as well. Odd lane (backward) have an other offset than even lanes (stage moves forward). Gravis pointed out that potentially, an algorithm, which takes the facet number into account could correct for the cross scan error. This is easy to imagine and can be executed, it is just hard to do so. Patterns is created via python, see crosscantest in https://github.com/hstarmans/hexastorm/blob/master/src/hexastorm/interpolator/patterns/create.py . In the pictures, on the left axis you see the lane width. There are ten lines per group where width and spacing are equal to the number denoted e.g. 50 microns. On the x-axis the distance between 0 and 10, is 10 mm. It just measures actual distance along x axis.
The polygon mirror (PM) is often used for fast scanning applications due to its superior scanning speed and large scanning angle. However, PM-based laser scanning systems are prone to cross-scan errors, restricting scanning precision. The facet tilt and scanhead dynamics are considered as two primary sources contributing to cross-scan errors. A treaty of cross scan error in polygon mirrors can be found here https://www.acin.tuwien.ac.at/file/publications/iat/pre_post_print/2024_cong_aim.pdf .
Luckily, cross scan error is zero in a laser prism scanner with two cylindrical lenses.
In a prism scanner, the laser diode is first collimated with an aspherical lens. The bundle is then focused by a cylindrical lens in a direction parallel to the prism. The bundle is deflected by refracting it through a tilted transparent plate. The bundle is finally focused by a second cylindrical lens orthogonal to the scan line. This has two advantages; the bundle is circularized (cylinder lenses have different focal lengths) and the cross scan error is removed (explained hereafter). Scanhead dynamics introduce noise into the system BUT the refracted laser bundle remains parallel to the incoming laser bundle as this is always the case for a planar prism. As such, the second cylinder lens removes the cross scan caused by vibrations as all parallel rays are focused in the same point. This is a fundamental advantage of prism laser scanning, minimal cross scan error. In polygon mirror systems this is solved by active compensation via a galvo scanner, see https://repositum.tuwien.at/handle/20.500.12708/213517. In a polygon mirror system the outgoing rays are not parallel to the incoming rays so the cylindrical lens trick doesn't work as well.
Below we see a side view of the new laser head and a camera with neutral density filters. If you look carefully, you can see a two cylinder lenses. The one between the laser and the prism is the hardest to see.
Cross scan measurement is shown below, a line which is around 12 pixels wide implying around 36 microns error. Camera has a pixel size of 3 micron, see https://hackaday.io/project/21933-prism-laser-scanner/log/217210-cross-scan-error-measurements-new-bearing two cylinder lenses, one channel, current 80 a.u. (scale 1-255), 4 facets, polygon speed 2000 rpm, exposure time is set to minimize overexposure and have a stable image
An exposure without cylinder lenses is shown below. Here you can clearly see the individual lines produced by the facets which are removed by the cylinder lens.
The boards sponsored by PCBWay are shown below. Progress in the last days has been good. Following things are working: prism motor (spins), laserdiode (can turn on & pogo pin system works), TMC2209 communications work, ESP32 and ICEUP5K (FPGA) work.
Progress has been as follows:
- 9 december: received boards
- 14 december boards populated & compute board working
numbers of some parts where wrong, which required extra shipment from mouser
- 19 december extension board populated (after receipt parts)
I encountered a bug.
- 30 december bug has been resolved
Two issues: - schema error (5V connected to 3.3V, this has been mitigated with soldering tricks)
- stepper motors communicate via UART1 which can create issues with UART0 (micropython repl)
this is a bug upstream but can be mitigated by a trick in the boot.py script.
- 2 january Destroyed laptop (applied 12V over USB input)
- 6 january Mounted motor, encountered issues moved to different board
- 7,8 january Changed code, ESP32 communicates with FPGA, spinning of prism work - 9 january Laser works (forward diode voltage of 2.5V) - 13 january Laser can be calibrated while on (pogo sytem works good) Laser diode prism test --> pass - 14 january Stability test / Mounting on machine laser reaches stability criterion mirror position without cylinder lens does work mirror position for cylinder does not work (cylinder lens might work, mirror position not optimal, needs testing) - 18 january x and y motor work, homing switches work spi unreliable (fails every 100_000 writes) - 21 january spi issue fixed; (bad soldered bord + incorrect setting) - 22 January test exposure works (resoldered x-dir pin) - 2 february micropython code optimized, check added to ensure micropython can feed data fast enough to the laserhead - 13 february all buttons are functional, only print remains now
Current challenges remaining - fine tuning optics & test exposure with microdot webserver