See also: https://blog.benjojo.co.uk/post/userspace-usb-drivers
The Flashforge Creator 3 Pro printer is a dual head 3d printer, which provides pretty good printing results just out of the box and works with Linux. Although at the moment of this writing it has been just three years after product launch, Flashforge does not list the printer anymore at their official webpage. But there is still some information available on archive.org: Flashforge Creator 3 Pro.
During various tests with different filaments, I experienced that some material would stick better to the build plate than other material. Most of the time ABS would stick very well, but with PLA there were issues. Naturally one tries to change slicer settings, start a new print to verify if the change made an improvement. In this iterative approach, one of the important settings for having PLA to stick to the first layer, is to let the cooling fan cool down the extruded material. I wasn't sure whether the fans would be spinning at all, since these were hidden inside the printing enclosure and it was hard to see. Furthermore, from the display status, the fan information was not updated either with the fan rotating speed.
What complicated matters even more was that with the official slicing software from Flashforge, Flashprint, there is a manual machine control window, which let one press buttons for “cooling fan control”. Interestingly with this feature it did not seem to be possible to control the left fan: Regardless of pressing the button for starting the left or right cooling fan here, only the right fan would actually spin. This behaviour made me initially think that this was the reason, why the filament would not stick when printing with the left extruder. I contacted Flashforge customer support for this, but after many fruitless discussions with Flashforge customer support, it was also clear that Flashforge wasn't willing to fix anything at this printer.
So after spending considerable time on it, I at least found out that when printing from a file, both fans would actually work as expected. Measurements with an oscilloscope confirmed, that the mainboard also correctly controls speed using PWM control (Something which did not work on a Creator Pro 2, for example. See https://github.com/moonglow/flashforge_fan_fix for more information.). At that point, I had already started to reverse engineer the printers' firmware, trying to understand why one fan did not spin with the manual control in Flashprint. And it took just a few more months to create a patch, which fixed the left cooling fan control.
The manual control in Flashprint looks like the following:
Please note the tiny little R and L character next to the fan symbol when clicking on the picture above. It is a mystery to me why Flashforge decided to place the button for the left cooling fan on the right and the button for the right cooling fan on the left. Probably overlooked with the software testers at Flashforge, since they never had a chance to test themselves. Maybe because they did not have the right hardware in front of them.
To see how the buttons should work, please see the following two video clips:
|
When I sent these videos to Flashforge customer, they were amazed, and asked, whether these were real. I told them that these were, with the only difference that the left video shows the printer with the non-patched firmware and the right videos shows the printer with the patch. As can be seen in the video, the non-patched firmware, always controls the right cooling fan, regardless of which button is clicked. With the patched firmware, it correctly controls the left or right cooling fan, depending on which button is clicked.
As explained earlier, Flashforge support was reluctant to do anything. But since I thought more people on the internet had a similar experience 1), I realized the only option was to reverse engineer the firmware and patch it. All in all, the experience was quite educational. Things like getting console access, finding relevant files, understanding ELF files, the linking process, Ghidra, Arm assembly, debugging with gdb, patching and much much more.
So if anyone is planning to undertake something similar or is maybe just curious to see how this was done, please continue reading. You may download the new installer from here:
To install, extract the archive to an empty FAT-formatted USB stick, switch off the printer and insert the USB stick and switch on. The installation should start automatically. When finished, the printer should indicate the firmware version is 1.4.1 with date 20241018. Please install at your own risk.
The main circuit board, mounted at the bottom, consists of two separate processors:
The main role of the Allwinner cpu, apart from providing a graphical user interface, is to prepare gcode commands from internal saved files and/or commands sent over the network to the printer and forward them to the motion controller. In the other direction, the motion controller sends data, like temperature information to the main cpu. This can be seen when connecting a serial cable to the corresponding RX/TX pins (J25) on the main board.
The Flashforge linux based system is made with an old version of buildroot (2016). For better understanding the system, ssh access would be helpful, but an ssh server (like dropbear) is not installed. However shell access is still possible using a serial cable at the circuit board at connector J2.
Instructions about how to get root access, can be found on the internet.2). You may also see following page to see what I used.
Please click on this link for more information about installing dropbear to get ssh working.
Once a terminal connection is made to the printer, serious reverse engineering can start. There are several great tools for reverse engineering. I highly recommend Ghidra. It is an open-source disassembler and can do a few things which make it so much easier to understand what is going on. It is able to create from the raw binary ELF file an abstract kind of C code. It automatically adds labels to variables and memory locations, is able to create function graphs which shows the flow of the code and much more.
The first step to understand the processes which take place upon powering up the printer is to see if there is some information provided at a serial port. With a baudrate of 115200 and connecting to J2, I could see the following excerpt (Please see here for more details and a full log)
U-Boot SPL 2017.01-rc2-00057-g32ab180-dirty (Jan 06 2021 - 10:39:41) DRAM: 64 MiB Trying to boot from MMC1 U-Boot 2017.01-rc2-00057-g32ab180-dirty (Jan 06 2021 - 10:39:41 +0800) Allwinner Technology CPU: Allwinner V3s (SUN8I 1681) Model: Lichee Pi Zero DRAM: 64 MiB MMC: SUNXI SD/MMC: 0 Setting up a 320x480 lcd console (overscan 0x0) ... mmc0(part 0) is current device reading script.bin 26972 bytes read in 23 ms (1.1 MiB/s) reading uImage 3026952 bytes read in 158 ms (18.3 MiB/s) ## Booting kernel from Legacy Image at 41000000 ... Image Name: Linux-3.4.39+ ...
From the output, one can see, it is using U-Boot to get into Linux-3.4.39+
Once the linux kernel is loaded into memory3), linux calls /etc/init.d/rcS which at its tail contains following conditional expression to start the file /opt/auto_run.sh
... if [ -f "/opt/auto_run.sh" ];then . /opt/auto_run.sh fi
Since the file /opt/auto_run.sh is present, it is called and does then the following:
In /opt/ffstartup.cfg finally is then a reference made to our target binary:
AppName = software/%VERSION%/creator3-arm AppArgs = 1;-qws
This contains a reference to the printer application which also handles the user interface. The application serves as a front-end, sending commands to the motion controller, to control the printer. The placeholder %VERSION% points to a version number formatted like: “1.4.0”. So the full path for the binary creator3-arm would be then:
/opt/software/1.4.0/creator3-arm
This is the file we need to inspect further and find if it contains any handling of commands to control the fans. For this we open the file in Ghidra.
When opening creator3-arm for the first time with ghidra we need to provide some initial data, helping it with the first analysis.
After clicking on Finish, the Flashforge_C3P Project should appear. Then start the Code Browser by clicking on the green Ghidra icon.
To import the creator3-arm file, click on: File → Import File… by selecting the binary and clicking on OK. After that it will ask if the file needs to be analyzed. Click on Yes and continue with configuring the analysis options: Enable the following additional options:
Then after showing a summary, the CodeBrowser window should appear with the disassembly and pseude c-code.
Then click on Analyze and wait for Ghidra to finish.
Starting creator3-arm with gdb, Setting up environment variables
https://wiki.osdev.org/ELF https://github.com/compilepeace/BINARY_DISSECTION_COURSE/blob/master/ELF/SECTION_HEADER_TABLE/SECTIONS_DESCRIPTION/SECTIONS_DESCRIPTION.md
When inspecting the creator3-arm file with the file command it reveals:
file creator3-arm creator3-arm: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.3, for GNU/Linux 2.6.31, BuildID[sha1]=9fa452071c8601d8b08ad7875bbc71ee0f6c4845, stripped
Then to get more information about the ELF sections, we use:
readelf --section-headers creator3-arm
which shows:
There are 29 section headers, starting at offset 0x332934: | ||||||||||
Section Headers: | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
[Nr] | Name | Type | Addr | Off | Size | ES | Flg | Lk | Inf | Al |
[ 0] | NULL | 00000000 | 000000 | 000000 | 00 | 0 | 0 | 0 | ||
[ 1] | .interp | PROGBITS | 00010134 | 000134 | 000013 | 00 | A | 0 | 0 | 1 |
… | … | … | … | … | … | .. | .. | . | . | . |
[ 5] | .dynsym | DYNSYM | 00012338 | 002338 | 004830 | 10 | A | 6 | 1 | 4 |
[ 6] | .dynstr | STRTAB | 00016b68 | 006b68 | 0081ae | 00 | A | 0 | 0 | 1 |
… | … | … | … | … | … | .. | .. | . | . | . |
[ 9] | .rel.dyn | REL | 0001f74c | 00f74c | 000160 | 08 | A | 5 | 0 | 4 |
[10] | .rel.plt | REL | 0001f8ac | 00f8ac | 001fe8 | 08 | A | 5 | 12 | 4 |
… | … | … | … | … | … | .. | .. | . | . | . |
[13] | .text | PROGBITS | 00024938 | 014938 | 1a863c | 00 | AX | 0 | 0 | 8 |
[14] | .fini | PROGBITS | 001ccf74 | 1bcf74 | 000008 | 00 | AX | 0 | 0 | 4 |
[15] | .rodata | PROGBITS | 001ccf80 | 1bcf80 | 13b5d4 | 00 | A | 0 | 0 | 8 |
… | … | … | … | … | … | .. | .. | . | . | . |
[24] | .data | PROGBITS | 0035232c | 33232c | 00046c | 00 | WA | 0 | 0 | 4 |
[25] | .bss | NOBITS | 00352798 | 332798 | 003044 | 00 | WA | 0 | 0 | 8 |
… | … | … | … | … | … | .. | .. | . | . | . |
[28] | .shstrtab | STRTAB | 00000000 | 332833 | 000101 | 00 | 0 | 0 | 1 | |
Key to Flags: W (write), A (alloc), X (execute) |
For clarity some sections have been left out. For reverse engineering the following sections might be interesting:
Questions I was confronted with were, rather how relocation happens upon linking process at startup, where in the file is it possible to put these patches and what can be done, if there is no space available (if the original code does not have any free area where to place the patched code.).
There are many great webpages and books available on this topic.4).
Information about assembly, linker and linker scripts was very valuable as well5). The process to create binary files from assembly files, stripping the library and ultimately patch firmware by overwriting it.
The debugger gdb, which makes it possible to test the software and see if patches have some effect is installed at the printer.
Then, after understanding enough about the system, I was finally able to create a fix for the M106 and M107 commands. This was done by reusing the existing implementations of the M104 and M108 commands, adapting them for M106 and M107. Because the gcode functions in the main application work more or less like a proxy, forwarding incoming gcode commands to the motion controller, the essential change consisted of letting pointers in the M104 and M108 functions point to other memory locations where the characters 'M106' and 'M107' were present.
Then all what was basically necessary was to overwrite the existing M106 and M107 functions.
Because the new M106 function was larger than the original, it could not be placed at the original memory location. There all the studying about ELF files paid out well, because I discovered that due to memory alignment considerations at the end of the .text segment, there was just enough space to place the new M106 function. The new M107 function also was larger, but I was able to put that at the original M106/M107 memory location.
When clicking a button in Flashprint to start or stop a fan, a network command is sent to the printer, which can be captured with tcpdump and analyzed with wireshark. Interestingly, Flashprint does actually differentiate between the left and right cooling fan by sending also the index to the printer. It is just looks like the printer does not interpret that parameter.
For further debugging, it is much easier to directly send a command to the printer than using Flashprint. We can use the tool netcat for this.
The command to set the right (index 0) or left (index 1) fan to full speed using netcat would be:
echo "~M106 S255 T0" | nc -N printer.localhost 8899 echo "~M106 S255 T1" | nc -N printer.localhost 8899
(Where printer.localhost is resolved into the address of the Flashforge 3d printer.)
The printer returns an acknowledgement:
CMD M106 Received.
From analysis of the rootfs, the main application (creator3-arm) would always be started with the command line options “1 -qws”:
creator3-arm 1 -qws
But after analyzing with ghidra, I saw, there is also a -debug option, which, when used, reveal many interesting messages on the terminal. These look like:
... serial/SerialObject.cpp serialSendCode 415 "G90" serial/SerialObject.cpp serialSendCode 415 "G92 A0 B0" serial/SerialObject.cpp serialSendCode 415 "M140 S50" serial/SerialObject.cpp serialSendCode 415 "M104 S200 T0" serial/SerialObject.cpp serialSendCode 415 "M104 S0 T1" Execute/onetimesclicked.cpp setOneTimesClicked 37 UI/buildprint.cpp on_stop_pressed 1038 UI/mainwindow.cpp dialogTwoButtonClose 732 Dialog/twobuttondialog.cpp TwoButtonDialog 25 title= "Cancel print job" text= "Are you sure?" okText= "Yes" backText= "No" Execute/onetimesclicked.cpp slot_oneTimesClicked 47 Execute/onetimesclicked.cpp setOneTimesClicked 37 Dialog/twobuttondialog.cpp on_ok_pressed 84 ...
We can see in the snippet above, that there is a function serialSendCode which sends various commands to the Nation N32G455 (ARM Cortex-M4). The interesting thing with this, is that we can see exactly what is sent to the N32G455.
Without deep understanding yet of the inner workings, I noticed the M104 function in comparison, did send more arguments to the Nation N32G455 chip. As an experiment, I changed the string “M104” in “M106” in the binary and noticed that when sending the M104 command using netcat, the main processor would send M106 instead to the Nation N32G455, with the arguments which I provided: The left fan started spinning for the first time!
With that, a possible fix was found: Function M104 could be cloned and used to handle M106.
Not much later, while inspecting with ghidra, I found the large switch-case statement where all gcode commands are evaluated. For M106 the relevant address is at 0x131620 and for M104 it is at 0x13170e:
... LAB_00131618: XREF[1]: 001315c8(j) 00131618 1c 2c cmp r4,#0x1c 0013161a 00 f0 26 81 beq.w LAB_0013186a 0013161e 32 dd ble LAB_00131686 00131620 6a 2c cmp r4,#0x6a @ r4 = 106 <--- M106 00131622 00 f0 55 81 beq.w LAB_001318d0 00131626 72 dd ble LAB_0013170e 00131628 6c 2c cmp r4,#0x6c 0013162a 00 f0 48 81 beq.w LAB_001318be 0013162e c0 f2 d4 80 blt.w LAB_001317da 00131632 70 2c cmp r4,#0x70 00131634 00 f0 33 81 beq.w LAB_0013189e 00131638 72 2c cmp r4,#0x72 0013163a 40 f0 95 80 bne.w LAB_00131768 ... LAB_0013170e XREF[1]: 00131626(j) 0013170e 68 2c cmp r4,#0x68 @ r4 = 104 <--- M104 00131710 00 f0 83 80 beq.w LAB_0013181a ...
And M106 is further handled at LAB_001318d0:
LAB_001318d0 XREF[1]: 00131622(j) 001318d0 28 46 mov param_1,r5 001318d2 08 99 ldr param_2,[sp,#param_5] 001318d4 f8 f7 2c fe bl FUN_0012a530 undefined FUN_0012a530() 001318d8 b0 fa 80 f0 clz param_1,param_1 001318dc 40 09 lsrs param_1,param_1,#0x5 001318de fa e6 b LAB_001316d6
At LAB_001318d0, parameters param_1 (Which goes to r0) and param_2 (to r1) are set, before calling the rest of M106 at FUN_0012a530. The calling convention for handling over parameters in c functions in arm architecture is not by placing them on the stack, but using r0, r1, r2 and r3 instead for the first 4 registers.
For comparison, the parameter preparation for M104 at LAB_0013181a takes one argument more and looks like:
LAB_0013181a XREF[1]: 00131710(j) 0013181a 41 46 mov param_2,r8 0013181c 28 46 mov param_1,r5 0013181e 08 9a ldr param_3,[sp,#param_5] 00131820 fb f7 bc f9 bl FUN_0012cb9c undefined FUN_0012cb9c() 00131824 b0 fa 80 f0 clz param_1,param_1 00131828 40 09 lsrs param_1,param_1,#0x5 0013182a 54 e7 b LAB_001316d6
This means that to be able to re-use the function M104, not only function M104 needs to be copied but also the parameter preparation for M104.
Like M106, the original M107 gcode command would ignore the index as argument. So neither “M107 T0”, nor “M107 T1” would make any difference. And instead it would always only stop the right fan.
So in order to fix this, with the understanding of the fix for the M106 function, we just need to clone an available function, which is able to handle an index as argument and modify this for M107. For this, the function M108 was used.
The arm binary needed to be patched at several locations. During debugging and development, a script was written which takes a csv file containing target addresses, references to .s assembly files and assembles them. And place the resulting binaries into the original file. The .csv file which was used for creating 1.4.1 looks like the following:
0x0100a4;"src/ELF_FileSiz_MemSiz.s";"Max out ELF .text section size from alignment provision" 0x1318d0;"src/m106_args_preparation.s";"Fix function argument handling for function M106" 0x330a88;"src/m106_fun.s";"Fix function M106" 0x1317da;"src/m107_args_preparation.s";"Fix function argument handling for function M107" 0x12a530;"src/m107_fun.s";"Fix function M107" 0x1d437c;"src/bump_up_version.s";"bump up software version from 1.4.0 to 1.4.1" 0x1d4370;"src/bump_up_date.s";"modify software creation date from 20230201 to 20241018"
Here column 1 contains the address where to place the binary file. This is the so-called VirtAddress of the code in memory and starts at 0x10000. To calculate the position where to place the code at the file, it is necessary to subtract 0x10000 from the address.
Column 2 contains the Gnu as assembly .s file which needs to be assembled. Most of the code was copied from Ghidra and rewritten by hand to be compatible to Gnu as. To resolve the labels into addresses, the opcode was taken and looked up with the online tool armconverter.
Column 3 contains an arbitrary description which is shown when running the script.
The script itself has the sha256 embedded of the 1.4.0 target file and will only continue when it matches. The heavy lifting is done with the following tools:
The latest official firmware from Flashforge (Februar 2023) is version 1.4.0.