Reverse Engineering the Gravity 800 Smoker Controller

I recently released FreeFall 800, open source firmware for the Gravity 800 smoker/grill from Masterbuilt. The project started because I needed to open the controller to fix a broken wire. That is when I noticed that inside was a standard ESP32 module, with a plainly visible programming header. This made me very surprised to find that there were little to no details online about the controller’s internals. In this post I aim to document everything I managed to reverse engineer about the Gravity 800 controller board, and will discuss some of the very strange choices the designers made.

Gravity 800 Controller Board. (I apologize for the image quality in this post, I thought my phone camera was better)

Gravity 800 Controller Board. (I apologize for the image quality in this post, I thought my phone camera was better)

At a high level the Gravity 800 controller board has the following major components:

  • ESP32 WiFi microcontroller module
  • Four character, 16 segment LCD display with backlight
  • Five button and knob encoder physical inputs
  • Piezo buzzer for temperature and timer alarms
  • Grill lid open switch
  • Thermocouple ADC for smoker chamber temperature, with thermistor for cold junction measurement
  • Four thermistor temperature probe ADC inputs
  • 12-volt blower fan motor driver

Unfortunately on first investigation, other than the MCU which is specifically an ESP32-WROOM-32UE module, and the LCD driver which appears to be a CS1621, the rest of the ICs are either unmarked or do not have specific enough markings for me to find data sheets or documentation. This meant that not only did I need to reverse the PCB connectivity to build FreeFall 800, I also ended up having to reverse engineer the serial protocols, at least enough to emulate them.

I will go through the reverse engineering process in detail below, but in the end I did end up with a full schematic of the Gravity 800 controller board. Some component values are missing (mainly unmarked capacitors and inductors, since I didn’t want to desolder them), but references should be accurate. A KiCAD project with the schematic is published in my reverse engineering repository here: prbs23/gravity-800-reverse-engineering.

Full schematic of Gravity 800 controller board

Full schematic of Gravity 800 controller board

Power Supply

The easiest place to start reversing the board was the power input. Not surprisingly, there does not appear to be anything too interesting going on with the power supply.

Power Supply Schematic

Power Supply Schematic

On the input side we have a reverse voltage protection diode and a couple of capacitors to produce the 12 volt rail that both feeds the 3.3 volt regulator and is used directly for the blower fan and buzzer. I was not able to identify what specific voltage regulator is being used, but it appears to be a pretty standard adjustable switching regulator configured to output 3.3V.

Microcontroller

After tracing the power supply, I moved on to the other part of the board that looked familiar: the ESP32 microcontroller. Specifically the controller uses the 8MB flash variant of the ESP32-WROOM-32UE module. In addition to the integrated WiFi and Bluetooth radios, the module has 32 GPIO that can also be used with various UART, SPI, PWM, and ADC peripherals.

Obviously the first thing to do was to connect to the programming header and dump the firmware. The programming header was conveniently labeled “Programming” and had the following pin out:

Pin Description
1 RX
2 TX
3 GPIO0/BOOT
4 GND
5 3.3V

Like all ESP32s the programming header does double duty as a serial debug interface while the processor is running. You can boot into the programming boot loader by tying GPIO0/BOOT low while applying power to the controller. In the first (of several) annoying decisions made by the designers, the reset (EN) of the ESP32 is simply pulled high on the board, and not exposed on the programming header. This means every time we want to program the part, it’s necessary to power cycle the board. That must have been annoying for their firmware developers.

Dumping the firmware was as simple as installing espflash and running:

espflash read-flash --port /dev/ttyUSB0 --baud 115200 --chip esp32 0 0x800000 firmware_dump.bin

There is no flash protection or encryption enabled on the controller, so I now had the full flash image. I did end up doing some basic decompilation and tracing of the firmware in Ghidra and Claude. However, other than some specific thermocouple calibration functions (discussed more later), I did not find much else in the firmware that was worth the effort of tracing in detail.

The serial debug output did not have anything particularly helpful for reverse engineering. However it does expose some details of the infrastructure the mobile app is built on top of. Once connected to a WiFi network, this is a snippet of the (hopefully anonymized) messages that were logged:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
(4890) wifi:new:<11,0>, old:<1,0>, ap:<255,255>, sta:<11,0>, prof:1
(5690) wifi:state: init -> auth (b0)
(5700) wifi:state: auth -> assoc (0)
(5700) wifi:state: assoc -> run (10)
(5740) wifi:connected with XXXX_SSID_XXXX, aid = 7, channel 11, BW20, bssid = xx:xx:xx:xx:xx:xx
(5740) wifi:security: WPA2-PSK, phy: bgn, rssi: -72
(5750) wifi:pm start, type: 2

event_info.disconnected:11,event_id=4
(5840) wifi:AP's beacon interval = 102400 us, DTIM period = 2
F_Rd_AdRegt[4]: 6291455
(7270) BH: b5 80 00 00 00 
(7360) Main: DRAM:101540
(8660) esp_netif_handlers: sta ip: ddd.ddd.ddd.ddd, mask: 255.255.254.0, gw: ddd.ddd.ddd.ddd
event_info.disconnected:63,event_id=7
(8660) WiFi: SYSTEM_EVENT_STA_GOT_IP
(8670) WiFi: Got IP: ddd.ddd.ddd.ddd

(8670) AWS: start aws
keyAD=4092,coseblue=0,upStep=0,showback=0
(8960) Main: wifi_shown_contr=0,b4byte3bit1=0
(9280) BH: b5 86 00 00 00 
(9670) AWS: Connecting to AWS...
rssi:-73

(10360) Main: DRAM:59132
F_Rd_AdRegt[4]: 8388607
(11280) BH: b5 86 00 00 00 
(12810) AWS: Connected to AWS ok
(12810) AWS: SubscrTopic:$aws/things/<XXX_DEVICE_ID_XXX>/jobs/get/+
(13280) BH: b5 8e 00 00 00 
(13330) AWS: SubscrTopic:$aws/things/<XXX_DEVICE_ID_XXX>/shadow/get/rejected
(13360) Main: DRAM:59392
keyAD=4093,coseblue=0,upStep=0,showback=0
(13960) Main: wifi_shown_contr=0,b4byte3bit1=0
F_Rd_AdRegt[4]: 8388607
(15280) BH: b5 8e 00 00 00 
(15480) AWS: SubscrTopic:$aws/things/<XXX_DEVICE_ID_XXX>/shadow/get/accepted
(15990) AWS: SubscrTopic:$aws/things/<XXX_DEVICE_ID_XXX>/shadow/update/document
rssi:-75

(16360) Main: DRAM:61320
(16500) AWS: SubscrTopic:$aws/things/<XXX_DEVICE_ID_XXX>/shadow/update/rejected
(16810) AWS: SubscrTopic:$aws/things/<XXX_DEVICE_ID_XXX>/shadow/update/delta
(17280) BH: b5 9e 00 00 00 
F_Rd_AdRegt[4]: 8388607
(17920) AWS: desired------
(17930) AWS: Update Status:$aws/things/<XXX_DEVICE_ID_XXX>/shadow/update   Msg:{
	"state":	{
		"reported":	{
			"model":	"C:G:P26:1:D",
			"vers":	"3.0.01",
			"update":	0,
			"trigger":	0,
			"mac":	"xxxxxxxxxxxx",
			"pwrOn":	false,
			"fah":	true,
			"engaged":	true,
			"doorOpn":	true,
			"mainTemp":	999,
			"heat":	{
				"t2":	{
					"heating":	false,
					"intensity":	0,
					"trgt":	225,
					"max":	700,
					"min":	150
				}
			},
			"probes":	null,
			"errors":	[0, 0, 0, 0, 0],
			"timers":	null,
			"ssid":	"XXXX_SSID_XXXX",
			"RSSI":	-75
		},
		"desired":	null
	}
}
(17970) AWS: Update Status length=507
(18000) AWS: IoT_data_tsend 0
(18000) AWS: Request Get:$aws/things/<XXX_DEVICE_ID_XXX>/shadow/get
(18220) AWS: get accept callback

.... Repeat from keyAD line

Serial log output from factory Gravity 800 firmware

The most obvious thing we can see from this log is that they are using the AWS IoT Core managed MQTT message broker. Both from the log messages, and also some light decompiling it appears they are likely using the Espressif esp-aws-iot library for doing this. Interestingly, this trace was captured while the controller was in the “off” state. So even when “off” the controller is still sending the full device state to AWS and Masterbuilt, including your WiFi router SSID and RSSI.

Pinout

MCU Schematic

MCU Schematic

While this was interesting, it was not particularly helpful for my reverse engineering effort. The next step was tracing all the pins of the ESP32 module to find what they were connected to. This is the full list of pin connections I ended up with after fully reverse engineering:

Pin Name Direction Destination Function
25 GPIO0 Input Programming Header Boot mode
34 U0RXD Input Programming Header UART RX
35 U0TXD Output Programming Header UART TX
10 GPIO25 Input Lid switch Lid switch (closed=low)
27 GPIO16 Output Buzzer Buzzer pulse (4kHz)
28 GPIO17 Output Fan Driver Fan enable (active high)
30 GPIO18 Input Knob Encoder Encoder A
29 GPIO5 Input Knob Encoder Encoder B
31 GPIO19 Input Knob Encoder Encoder Push Button (unused)
16 GPIO13 Output Display Controller Backlight Enable (active low)
33 GPIO21 Output Display Controller Display CS (active low)
36 GPIO22 Output Display Controller Display WR (SCK)
37 GPIO23 Output Display Controller Display SDA
12 GPIO27 Output Thermocouple ADC Thermocouple ADC SCK
26 GPIO4 Bidir Thermocouple ADC Thermocouple ADC SDA
8 GPIO32 Input UART ADC ADC UART RX
7 GPIO35 Output UART ADC ADC UART TX

One thing that I was surprised NOT to find while probing the ESP32 pins was any of the control push buttons. Given the number of free pins I was expecting them to be directly connected, but after some tracing I found that the push buttons appeared to be multiplexed, and connected to what I have been calling the UART ADC.

UART ADC Subsystem

Tracing the push buttons I ended up looking at the U3 chip on the controller, an unmarked 16-pin SOIC. This chip is connected to the ESP32, through what appeared to be a 9600 baud UART interface. While both TX and RX are connected between ICs, the ESP32 factory firmware never appears to send anything, and U3 automatically starts sending out a 16 byte message every 36 milliseconds when power is applied.

Other than power and some decoupling, the remaining pins of U3 are connected to: The four temperature probe thermistor inputs, a board mounted NTC thermistor (which I eventually determined was used for the cold thermocouple cold junction temperature), and… the push buttons for the controls. This all points towards U3 being an ADC; however, it is pretty rare for an ADC to use 9600 baud UART instead of SPI or I2C. Of the few UART ADCs I could find, none matched the description of U3. My best guess is that it’s a cheap MCU programmed to behave as an ADC for the ESP32. Why would they not just use a SPI or I2C ADC? Why not just use the ADC inputs of the ESP32?

Weird design decisions aside, I captured a couple of trace samples of the 16 byte messages the “UART ADC” was sending out:

Byte # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Frame 1 E1 0F FD 0F FA 0F FB 0F FB 0F FA 08 03 00 00 1E
Frame 2 E1 0F FC 0F FB 0F FB 0F FB 0F FB 08 03 00 00 1F
Frame 3 E1 0F FC 0F FB 0F FC 0F FA 0F FB 08 03 00 00 1F
Three captured frames from the UART ADC

Every message starts with a 0xE1 start byte, followed by what appears to be 7 (odd?) two byte ADC sample values, and ending with a check byte. Bits [15:12] of each sample value are zero, so everything is pointing towards this being a 12 bit ADC. The check byte appears to be calculated as: check_byte = sum(frame[0:15]) % 256. This would make the frame format look something like:

UART ADC Frame Format

UART ADC Frame Format

The seven inputs of the ADC are mapped as follows:

Pin Channel Input
15 CH0 Multiplexed push buttons
14 CH1 Thermistor probe 1
13 CH2 Thermistor probe 2
12 CH3 Thermistor probe 3
11 CH4 Thermistor probe 4
10 CH5 Thermocouple cold junction thermistor
9 CH6 Unused, tied to GND

This schematic shows the whole UART ADC subsystem, with button multiplexing and thermistor conditioning.

UART ADC Subsystem Schematic

UART ADC Subsystem Schematic

Multiplexed Push Buttons

Despite having plenty of free IO on the ESP32, and there only being 5 push buttons on the board, the designers chose to multiplex all the buttons through a single ADC input on the UART ADC. The ADC input is pulled up with a 7.5kΩ resistor, and then all 5 buttons pull the input down with a different strength resistor. The table below lists the pull down strengths, expected output voltage, as well as ADC thresholds I ended up using.

Button Pulldown Nominal Voltage ADC Value Positive Threshold Negative Threshold Window Size
None N/A 3.3V 4095 N/A 3971 124
Timer 100kΩ 3.069V 3810 3931 3452 379
Up 22kΩ 2.461V 3054 3412 2375 1037
Temp 5.1kΩ 1.336V 1657 2335 1312 1023
Enter 2.2kΩ 0.748V 928 1272 624 648
Down 510Ω 0.210V 260 574 0 594

To prevent button confusion, in my FreeFall 800 firmware, I chose my thresholds such that there is a 40 ADC code window between each button’s window where I do not detect any button. Due to the structure used, detecting multiple buttons pushed at once is sort of possible, but supporting it would narrow the ADC value windows for single button presses. I have not done the math to figure out which combinations are uniquely detectable.

Thermistors

The other five (used) inputs to the UART ADC all come from NTC thermistors, which use the same structure: A voltage divider with fixed pullup and the thermistor as the pull down, followed by an RC low pass filter.

The probe inputs use a 47kΩ pull up resistor, and the thermistors have a nominal 230kΩ resistance at 25°C. From the decompiled factory firmware, it appears to use a fixed β value of 4331, so the temperature can be resolved using the following formula:

$$ T_0 = 25.0{\degree}\text{C}, R_0 = 230k\Omega, \beta = 4331, R_{high} = 47\text{k}\Omega $$ $$ R_{probe} = -\frac{R_{high} \cdot {adc\_value}}{{adc\_value} - 4096} $$ $$ T_{probe} = \frac{1}{\frac{1}{T_0 + 273.15} + \frac{1}{\beta} * \log(\frac{R_{probe}}{R_0})} - 273.15 $$
Equation for calculating probe temperature from raw ADC value

The final thermistor input is a board mounted NTC thermistor that is used as a cold junction temperature measurement for the thermocouple temperature calculation. This thermistor has a nominal 10kΩ resistance at 25°C, and uses the same input structure with a 10kΩ pull up resistor. The β value used by the factory firmware in this case is 4104. Because the same input structure is used, the same equation can be used to calculate temperature.

From my testing, the physical implementation of this cold junction thermistor has some design problems. The most obvious is that it is not located anywhere near the actual thermistor junction. A bad design decision, but not the biggest issue. The bigger problem is they used a tiny 0603 SMT thermistor, which, with a nominal 165μA passing through it, self heats to about 5-10°C above the board nominal temperature. This error then gets inferred into the thermocouple measurement, giving a constant offset.

Thermocouple ADC

Unlike the temperature probes, the Gravity 800 uses a thermocouple to measure the smoker chamber temperature. This is handled by an independent single input differential ADC IC that has the reference IC4 on the controller board. Much like the UART ADC this chip did not have any useful identifying markings, and also had a weird serial interface.

Thermocouple ADC Schematic

Thermocouple ADC Schematic

The thermocouple itself is connected to the ADC with a fairly standard 1MΩ biasing and low pass filter circuit. On first inspection, it appeared that the serial interface to the ESP32 was a unidirectional SPI-like interface, where the ESP32 would periodically clock a block of data out of the ADC. But after trying some initial testing under this hypothesis, the serial data line proved to use more complex bidirectional signaling.

Oscilloscope trace capture of single thermocouple ADC transaction. Green trace ESP32 side of R105, blue trace ADC side of R105.

Oscilloscope trace capture of single thermocouple ADC transaction. Green trace ESP32 side of R105, blue trace ADC side of R105.

By probing the SDA signal on either side of resistor R105 and looking at the edge rate on either side, I was able to determine which side of the interface was driving SDA. From that observation the sequence appears to be:

  1. ADC pulls SDA low
  2. ESP32 sends 27 clocks, during which the ADC shifts out 27 bits of sample data
  3. On the next clock cycle the ADC stops driving SDA and the ESP32 starts driving
  4. ESP32 drives 8 clock cycle value of 0x56
  5. One cycle interface turn around where ESP stops and ADC starts driving SDA again.
  6. ESP32 clocks 9 more cycles of status data out of ADC.
  7. Wait for ADC to pull SDA low again.

From my analysis this is the most accurate representation of the protocol:

Thermocouple ADC serial protocol

Thermocouple ADC serial protocol

However, on the first transaction after coming out of reset, the factory does something slightly different. Presumably this is initializing something in the ADC. Instead of sending a fixed 0x56 command, it sends a 16 bit value of: 0xcb 0x04

First Thermocouple ADC serial transaction after reset

First Thermocouple ADC serial transaction after reset

Once I was able to get a 22-bit value out of the ADC, that appeared to change with temperature, I just needed to figure out how to map it to a temperature. This is where I needed to reverse engineer the factory firmware, because I had no real information on how the ADC measurements mapped to temperatures. Most of the firmware I did not fully annotate or trace, but I did identify one function which appeared to handle the calibration and correction of the thermocouple measurement.

The factory firmware appears to convert the ADC value into degrees fahrenheit with a linear scaling factor (0.00390543072717 °F/LSB), adds the cold junction temperature from the board mounted NTC, and then applies a piecewise linear correction factor. The correction function as well as a plot of its correction is shown below. Because the factory firmware does all its calculations in fahrenheit, I am showing both their coefficients in fahrenheit, and the coefficients used by FreeFall 800, which does its math in celsius.

$$ T_{corF}(T_{uncorF}) = T_{uncorF} + \begin{cases} 0 & \text{if } T_{uncorF} \leq 50{\degree}F \\ 30 \times \frac {T_{uncorF} - 50} {250 - 50} & \text{if } 50{\degree}F \leq T_{uncorF} \leq 250{\degree}F \\ 30 & \text{if } 250{\degree}F \leq T_{uncorF} \leq 400{\degree}F \\ 30 + (41-30) \times \frac {T_{uncorF} - 400} {600 - 400} & \text{if } 400{\degree}F \leq T_{uncorF} \leq 600{\degree}F \\ 41 & \text{if } 600{\degree}F \leq T_{uncorF} \\ \end{cases} $$ $$ T_{corC}(T_{uncorC}) = T_{uncorC} + \begin{cases} 0 & \text{if } T_{uncorC} \leq 10{\degree}C \\ 16.667 \times \frac {T_{uncorC} - 10} {107 - 10 } & \text{if } 10{\degree}C \leq T_{uncorC} \leq 107{\degree}C \\ 16.667 & \text{if } 107{\degree}C \leq T_{uncorC} \leq 204{\degree}C \\ 16.667 + (22.778-16.667) \times \frac {T_{uncorC} - 204} {316 - 204} & \text{if } 204{\degree}C \leq T_{uncorC} \leq 316{\degree}C \\ 22.778 & \text{if } 316{\degree}C \leq T_{uncorC} \\ \end{cases} $$
Piecewise linear correction function extracted from factory firmware.
Thermocouple correction curve

Thermocouple correction curve

It is unclear where this correction curve comes from, it does not match any thermocouple calibration curve I was able to find. So I have to assume this is correcting for some non-linearity in the sampling circuitry, not the thermocouple itself. Additionally, since this is using a relatively coarse linear piecewise function, I suspect it is possible to get more accurate temperatures by fitting a continuous function to calibration data. However, I cannot quantify if this is worth it, since I have not yet collected the required data to test it.

This correction function is applied to the scaled and offset ADC value. The full thermocouple conversion function is shown below:

$$ T_{scaleF} = 0.00390543072717{\degree}F $$ $$ T_{scaleC} = 0.00216968373732{\degree}C $$ $$ T_{offset} = 0 $$ $$ T = T_{cor}(({ADC Sample} * T_{scale}) + T_{coldJ} + T_{offset}) $$
Full thermocouple temperature conversion from factory firmware.

Absent better calibration and correction data, this is also temperature calculation that I implemented in FreeFall 800.

Display

The last major component of the controller board is the LCD display and controller (U31). Unlike most of the other components, I was actually able to find a datasheet for the LCD controller that matched its pin out: CS1621. The English CS1621 datasheet is missing some serial protocol details, but I found that the HT1621 appears equivalent and has a better datasheet. This made reverse engineering this part much easier than the ADCs, the main thing to figure out was the mapping of segment buffer bits to specific LCD segments.

LCD display and controller schematic

LCD display and controller schematic

The serial interface on the CS1621 is almost SPI, and fortunately it is possible to use the SPI peripheral in the ESP32 for the way it is used on this board. Technically the interface is made up of chip select (pin 9), bidirectional serial data (pin 12), and then pins 10 and 11 are inverted read and write enables. To write a data bit to the controller the WR pin is pulled low, and then data is latched on the rising edge. To read a bit from the controller the same is done on the RD pin. However if you only need to write to the controller, you can treat the WR pin as a SPI clock signal. Since the RD pin is not connected to the ESP32, clearly this is what the designers did. (I will note they probably should have a pull up on RD, but it seems to work okay un-driven)

Excerpt from CS1621 datasheet showing data and command transaction formats

Excerpt from CS1621 datasheet showing data and command transaction formats

Segment Mapping

The display content is updated by using the “WRITE mode” command to update the display buffer. The factory firmware actually writes the display buffer twice in a single command, first it sends 16 bytes of 0s to clear the buffer, and then 16 bytes of actual segment state data. I am not sure this is required now that I have the datasheet, but I wrote the FreeFall 800 LCD driver before I found an English version of the datasheet so I just replicated what the factory firmware did. The table below shows the full mapping of segments:

COM0 COM1 COM2 COM3
SEG0 CH3_E CH3_F CH3_H CH3_A1
SEG1 CH3_D1 CH3_K CH3_G1 CH3_I
SEG2 CH3_L CH3_G2 CH3_J CH3_A2
SEG3 CH2_D2 CH2_M CH2_C CH2_B
SEG4 CH2_L CH2_G2 CH2_J CH2_A2
SEG5 CH2_D1 CH2_K CH2_G1 CH2_I
SEG6 CH2_E CH2_F CH2_H CH2_A1
SEG7 CH3_D2 CH3_M CH3_C CH3_B
SEG8 CH1_E CH1_F CH1_H CH1_A1
SEG9 CH1_D1 CH1_K CH1_G1 CH1_I
SEG10 CH1_L CH1_G2 CH1_J CH1_A2
SEG11 CH1_D2 CH1_M CH1_C CH1_B
SEG12 CH0_E CH0_F CH0_H CH0_A1
SEG13 CH0_D1 CH0_K CH0_G1 CH0_I
SEG14 CH0_L CH0_G2 CH0_J CH0_A2
SEG15 CH0_D2 CH0_M CH0_C CH0_B
SEG16 BT HT2 SMK HT1
SEG17 MP1 MP2 MP3 MP4
SEG18 TEMP TIME RLD COL

The segment names map the the screen layout like the following:

  RLD        TIME        TEMP        MP1        MP2        MP3        MP4

 --A1-- --A2--      --A1-- --A2--         --A1-- --A2--      --A1-- --A2--     HT1
| \    |   /  |    | \    |   /  |       | \    |   /  |    | \    |   /  |
F  H   I  J   B    F  H   I  J   B  COL  F  H   I  J   B    F  H   I  J   B
|   \  | /    |    |   \  | /    |       |   \  | /    |    |   \  | /    |    SMK
 --G1-- --G2--      --G1-- --G2--         --G1-- --G2--      --G1-- --G2--
|   /  | \    |    |   /  | \    |       |   /  | \    |    |   /  | \    |
E  K   L  M   C    E  K   L  M   C  COL  E  K   L  M   C    E  K   L  M   C    HT2
| /    |   \  |    | /    |   \  |       | /    |   \  |    | /    |   \  |
 --D1-- --D2--      --D1-- --D2--         --D1--  -D2--      --D1-- --D2--     BT
.             .    .             .       .             .    .             .
+---- CH3 ----+    +---- CH2 ----+       +---- CH1 ----+    +---- CH0 ----+

Initialization and Configuration

When the controller comes out of reset it needs to be configured for the connected LCD. This is done with a sequence command of 12 bit command writes, where each command starts with a 0b100 command prefix. The configuration command sequence that the Gravity 800 factory firmware (and thus FreeFall 800) uses is:

Command Word Function
1. 0x852 Configure bias generator for 1/3 bias voltage with 4 commons.
2. 0x830 Enable internal RC oscillator with 250Khz frequency.
3. 0x800 Disable oscillator and bias generator.
4. 0x80a Disable watchdog timer.
5. 0x802 Enable oscillator generator.
6. 0x806 Enable bias generator

Backlight

The LCD backlight is driven through a high side P-channel MOSFET, connected to GPIO13 (module pin 16) of the ESP32. Because of the high side switch circuit used the backlight is enabled when GPIO13 is pulled low, and disabled when high. Annoyingly they did not include a pull up resistor on the MOSFET gate, so until the ESP32 powers on and configures GPIO13 as an output the backlight will partially turn on.

Miscellaneous

The rest of the controller is a variety of miscellaneous passive circuits

Knob Encoder

Encoder Knob Schematic

Encoder Knob Schematic

The knob is connected to a pretty standard 2 switch mechanical rotary encoder, with pull up and series resistors driving pins GPIO18 and GPIO5. Weirdly, the board has the pads and resistors to support a push button encoder, with the push button connected to GPIO19, but the actual mounted encoder is not pushable. In theory you could swap the encoder for a pushable one and have an extra user input option.

Lid Switch

Lid Switch Schematic

Lid Switch Schematic

The grill lid switch uses the same pull up plus series resistor input structure for driving GPIO25.

Fan Driver

Fan Driver Schematic

Fan Driver Schematic

The blower fan is driven from the 12 volt supply using a high side 20P03 30V/20A P-Channel MOSFET. The gate of the 20P03 is driven from a second low side N-Channel MOSFET, so that the 3.3V ESP32 can switch the 12V motor using GPIO17. The motor output itself has a LC filter and clamp diode to protect the rest of the controller.

Buzzer

Buzzer Schematic

Buzzer Schematic

Finally, the piezo buzzer is also driven directly from the 12 volt supply, and is connected to GPIO16 of the ESP32 through a low side N-Channel MOSFET. The factory firmware always drives the buzzer with a 4kHz frequency. Presumably you could use other frequencies, but for the time being I have kept with the same configuration.

Future Work

At this point pretty much everything on the Gravity 800 controller board is fully understood. There are a few things I still want to do in this area though:

  • Reverse engineer more of the Gravity series controller boards. When I published the FreeFall 800 repository, one common piece of feedback I got was people wanting to run it on the Gravity XT controller. This is a substantially different controller with a full graphical LCD; but from the FCC filings, it appears to still use a (newer) ESP32 module. If I can get my hands on an XT, or any other model, gravity series controller I would be interested in reverse engineering them too.

  • More information on the thermocouple or UART ADCs. I have enough information to get data out of these ADCs, but if anyone has any more information about these parts, or parts with similar looking interfaces I would love to know!

  • There is still more work to do on FreeFall 800! I am currently looking into adding MQTT support, improved configuration and calibration, among other things to FreeFall 800. Additionally, in the process of writing this post and doing some deeper research into the LCD controller, I now suspect that there’s a more efficient way I could be updating the LCD.


See also