stonegray's site

Hacking the Tassimo (Part 1)



I want to yell “Hey Siri, make me a coffee” and have a coffee start pouring. For this, we need to:

Note that above the actual goal of coffee, this is also a fun project, so my approach is inefficient at achieving the end goal. I expect there to be much easier ways of approaching this than I’ve chosen here.

⚠️ DANGER ⚠️

The Tassimo uses a non-isolated power supply. Every connection on the board is at mains potential. Use caution when handling and appropriate precautions when connecting to external devices like your computer.


The mainboard is a standard 2-layer ENIG PCB. Mostly SMT with THT connectors and components. It has the following markings:

Marking Image Marking Layer Location Notes
1 x “06936496” Front Sticker Near debug header
2 x “335-1190” Back Soldermask Near debug header Has circular logo with squares nearby
3 x “E.I.S. GmbH” “ Back Soldermask Near 6-pin connector
4 x “1001-0059.12” Back Copper Near 6-pin connector

The heart of the Tassimo is this Phillips/NXP LPC2103F, a surprisingly large 32-bit ARMv7 microcontroller with 8kB of flash.


The main control board has the following connections, from top to bottom:


The unpopulated 6 pin connector was immediately interesting as it was likely a UART. By testing the pins using a multimeter we’re able to determine the pinout. By quickly checking which pins are connected to the chip we were able to find the pinout. The UART runs at 38400 baud, 8N1, and prints the following on boot:


While brewing a test coffee, it prints out in tab-seperated data. These numbers appear to correlate with stages of the brewing process, and are likely raw sensor data.

0	  11	 596	  23	  83	   1	   0	7970	 426	   0	 450	    5	
... 1090 more lines

Pin 6 was initially only labeled as “IO44 P0.14/DCD1/SCK1/EINT1” based on our continuity tests and the microcontroller datasheet. We determined it’s function by checking that it was floated high, carried no data on boot, had no external pullups. This is typical of an active low input. Using a resistor to prevent damage to chip in the event we were wrong, we pulled it down on boot which gave us access to an interactive prompt on UART0.

This prompt is the ISP. According to the documentation there are 10 commands that include the ability to dump the firmware. There is very limited support for code protection. While you can prevent using read commands from the ISP and disable code, there appears to be no way to prevent uploading code.

1# Send characters for autobaud and sync:
2echo "?SynchronizedOK" > /dev/ttyS0
4# Send ISP unlock command. This magic number is found on page 236 of Phillips UM10161.
5echo "U 23130" > /dev/ttyS0
6# Device responds with "0", meaning CMD_SUCCESS

With this access, we can try reading out the code protection bits. They are stored at flash addres 0x1FC per the datasheet. If that value is equal to magic number 0x87654321, (dec 2271569471) code protection is enabled.

1R 508 4              # Read four bytes from address 508 / 0x1FC
20                    # Device reports CMD_SUCESSS
4$;=NV;6UM            # Uuencoded data from device 

And after adding the required header we can decode it with uudecode:

1$ uudecode -o /dev/stdout < test2.uu | xxd
200000000: 6ddb b66d                                m..m

Turns out the approach of “read the code protection bits” wasn’t even neccessary. Just the fact that we can read this without it throwing an error indicates that this code protection bit isn’t set.

This means we should definitely be able to dump the firmware, and should be able to access the JTAG port as well.

Reading a few bytes by hand through this interface isn’t too bad, but dumping the entire memory will take a while. We would need to verify checksums, send OK messages every 20 lines, all at a sluggish 9600 baud*. Most importantly, there’s a low margin of error: if we make a mistake extracting the firmware, we’d likely brick out coffeemaker when we try to flash it. Getting a known-good firmware and NVRAM dump gives a “checkpoint”; if we break anything, it should let us get back to where we started.

Changing the baud is possible once connected, however our 16M xtal is not listed on the supported speeds table. For safety, we will be using the default RC oscillator, calibrated from serial, 9600.


Using some 3rd-party programming software that supports our chip, we were able to download and verify the firmware image from the chip.

1$ sha256sum LPC2103-20230117-044849.hex
286a129559b6e653555a6b355a44706d48be5ac22723959de8d5a7159d347687c  LPC2103-20230117-044849.hex
4$ wc -c LPC2103-20230117-044849.hex
592190 LPC2103-20230117-044849.hex

As a sanity check, reading 0x1FC, the address for the code protection enable, returned the same data we manually extracted. 🎉


Now that we have a copy of the firmware, our next task is to disassemble it using Ghidra in order to check if we can use the serial port to control the device. This microcontroller can be decoded as ARM:LE:32:v7:default, using the Intel Hex format.

Unfortunately there appears to be no SWD file or easy way of adding all system registers for this chip, so we need to map the memory manually. The most important, and arguably only required change, is to make sure that Flash is not writable in order to correctly disassemble.


Once we’ve configured a reasonable memory map, we can start disassembling. Before we get too far, we should check that we’re able to write the chip. To do this we’ll start by making a small change that’s easy to observe, then flash the chip to see if we encounter any checksums or DRM surprises.

During boot, we observed printing what appears to be a fixed string “E3304_005UDP” onto UART0. As long as it doesn’t have any other purpose, this looks like a good target to modify. Searching the binary shows that this string is stored, unterminated, at 0x0F1. With only a few Xrefs it appears that it is in fact only used to print, so it should be safe to modify.


Note: we later resized this string to include the 0x45/“E” above. It just loads it in a funny way.

After making the change, we’re able to export the modified binary from Ghidra and load it into our flashing utility. After pulling the “IO44 P0.14/DCD1/SCK1/EINT1” pin low using the brown wire to the JTAG port’s GND, we’ll boot into ISP mode. Resetting the board by repurposing the builtin brew button as a reset, we can flash and reboot the board into our now modified firmware.


Connecting again to the UART at 38400 / 8N1 gives us the following output shortly after boot, confirming that we’re able to modify the firmware.


Being able to modify the firmware is nice, but coffee is nicerer. Next up, can we use the UART to brew a coffee?

Enabling the UART

Our initial connection to the UART showed only data output, and no activity when we sent data. Our technical refernece for the chip shows UART0 located at 0xE000C000. Jumping to that address in Ghidra shows us very limted access, only a few functions.


Unfortunately, it’s hard to know if one of these does in fact recieve data until we understand the memory structure better. (The function names are made up)

Note: There is no SVD file available for the Phillips/NXP LPC2103, however if SVD files are available, using an SVD loader may be a better option.

Since Ghidra doesn’t know which registers are which, we need some way to tell it in order to make that UART memory space readable. While in some cases you may need to enter them by hand from the technical reference manuals, I found some header files from public repos that contain the UART stucture and the required defines.

1#define UFCR_FIFO_ENABLE    (1 << 0)    // FIFO Enable
2#define UFCR_RX_FIFO_RESET  (1 << 1)    // Reset Receive FIFO
3#define UFCR_TX_FIFO_RESET  (1 << 2)    // Reset Transmit FIFO
4#define UFCR_FIFO_TRIG1     (0 << 6)    // Trigger @ 1 character in FIFO
5#define UFCR_FIFO_TRIG4     (1 << 6)    // Trigger @ 4 characters in FIFO
6#define UFCR_FIFO_TRIG8     (2 << 6)    // Trigger @ 8 characters in FIFO
7#define UFCR_FIFO_TRIG14    (3 << 6)    // Trigger @ 14 characters in FIFO

Ghidra can read these defines by simply importing them as C source, but doesn’t understand which #defines go with which register, so we’ll need to convert these into an enum.

Using Vim, we’ll visual select all of the defines together and convert them into an enum format, then wrap them in brackets and add the enum name.

" Remove "#define ", replace with "\t"
:`<,`>s/#define /\t/g

" Add an = before the para:

" Add commas:

" add the enum names by hand, then:
" Remove prefixes: (<tab>UIIR_NO_INT=xxx to <tab>NO_INT=xxx)

This gives us nice and readable code, and processing the ~123 lines is a breeze as all the per-line changes are automated.

1enum e_UIIR {
2    NO_INT   = (1 << 0),    // NO INTERRUPTS PENDING
3    MS_INT   = (0 << 1),    // MODEM Status
4    THRE_INT = (1 << 1),    // Transmit Holding Register Empty
5    RDA_INT  = (2 << 1),    // Receive Data Available
6    RLS_INT  = (3 << 1),    // Receive Line Status
7    CTI_INT  = (6 << 1),    // Character Timeout Indicator
8    ID_MASK  = 0x0E

After quickly verifying this against the UART register map and saving this header file, we can now import it into Ghidra and assign types, giving us a much more readable output:


Copyright notice: Image contains only non-code inferred memory space

Note: This approach doesn’t handle addresses which read and write in different formats well. For example 0x#E000C008 when written is always the FIFO control register, and when read is the interrupt ID register. This could be resolved by using the Memory Map instead of a struct.

Unfortunately those lonely looking interrupt ID registers don’t bode well for bidirectional communication. In order to confirm, let’s take a look at those references to nd disassemble the functions that reference it.

Starting with void initUart0():

 1void initUART0(void){
 2	PINSEL0 = PINSEL0 | 0b00000101;
 4	// Writing ENABLE_DIV_LATCH_ACCESS changes union
 5	// uartRegs.rx_tx_buffer into the divisor latch LSB:
 8	// U0DLL (Divisor Latch LSB )
 9	uartRegs.rx_tx_buffer.dll = (div_latch_reg_value) 0x18;
11	// Revert to being TX/RX buffer:
12	uartRegs.lcr = CHAR_LEN_8;
14	uartRegs.dlm = 1;
15	return;

This would have been challenging to understand without clearly mapping this memory as the union type of uartRegs.tx_tx_buffer changes from rbr/thr into dll and back, while it writes the Divisor Latch LSB:

Nothing there, let’s check the other function that accesses the UART, void uartWritePointerFunction(char):

 1void uartWritePointerFunction(char charToPrint){
 3	e_ULSR modemStatus;
 5	// Read UART0_LSR
 6	modemStatus = uartRegs.lsr;
 8	// While transmit holding register, spin reading LSR reg:
 9	while ((modemStatus & ULSR_THRE) == 0){
10		// Read UART0_LSR
11		modemStatus = uartRegs.lsr;
12	}
14	// Write Transmit Buf Register
15	uartRegs.rx_tx_buffer.thr = charToPrint;
16	return;

It looks like our initial suspicion was correct: Any code reading from the UART would need to access 0xE000C000/uartRegs.rx_tx_buffer.rbr, and we’ve disassembled each function that references that address.

With UART1 being completely unused, this indicates that on the factory firmware, it’s impossible to send any commands or data to the device using the UART. This means we’d need to write our own handler to accept any UART data, which is non trivial.

tan(): Since we only care about doing a single action, we could likely abuse the UART interrupt to make it brew on any byte recieved in only a couple armv7 instructions: set the brew pin high, clear uart buffer, then return. Then simply register that as the ISR handler for the UART 😎

With the UART unavailable, our next best bet to brew a coffee looks like the JTAG.

Enabling the JTAG

Unfortunately, initial attempts to access the JTAG have been unsuccessful. It appears one of the JTAG lines, TCK, is also used for pin 4 on the barcode scanner connector. Having a copy of the firmware opens up a lot of options, and we should be able to figure out how to get JTAG access.

Because we’ve already verified that the code protection register is not set, the most likely cause is either the DBGSEL pin, or reassignment of the JTAG pins. A quick check with a multimeter shows pulling up the DBGSEL pin is successful, and our device has no barcode scanner connected which could cause bus contention on the TCK pin, at least electrically. This narrows it down to pin reassignment as the most likely cause for nonfunctional JTAG.


To confirm, we can patch our program to loop infinitely, to ensure no GPIO is ever initialized. Based on my lack of ARM7 knowledge I simply used the b instruction, the ARMv7 eqivilent of x86’s jmp, to loop infinitely to the reset vector.


After rebooting into our patched firmware, we can successfully connect and read out the IDCODE register from our JTAG port.

Open On-Chip Debugger 0.12.0
Info : JTAG tap: auto0.tap tap/device found: 0x4f1f0f0f (mfg: 0x787 (<unknown>), part: 0xf1f0, ver: 0x4)

This matches what is reported to be the IDCODE for an LPC2xxx platform, however the manufacturer provides no information so we’re unable to verify.

With our JTAG now working, we can we can set breakpoints in GDB based on our disassembled code to stop just before we make any changes to IO, then step through (remember si not s) until we hit the instruction that breaks our JTAG.

(gdb) break *0x77A4
Breakpoint 5 at 0x77a4
(gdb) continue

Breakpoint 5, 0x000077a4 in ?? ()
(gdb) si
0x000077a6 in ?? ()
(gdb) si
0x000077a8 in ?? ()
(gdb) si
... (etc)

The device can get through all of it’s setup code, but we lose the JTAG connection once we get into the big loop that handles the actual brewing. It’s likely possible to patch this out to reassign the pin, or enable JTAG after the barcode is read.


After a quick proof-of-concept showing we can bitbang JTAG using the ESP32, we should take a quick detour and figure out how to add an ESP32 into the coffee maker; since keeping a JTAG adapter attached to the coffee maker at all times isn’t ideal.

Adding ESP32

The venerable ESP32 seems like the obvious choice for this project. Inexpensive, great toolchain, and plenty fast enough for any interface, even JTAG. It’s also supported by esphome, which makes integrating with my home automation system easy. I’m guessing Tassimo didn’t expect aftermarket JTAG OTA update coprocessor when they were calculating the power budgets for this device, so checking that there’s sufficient power available is our first step.

The Tassimo uses an LNK305 LinkSwitch-TN power supply, set for a little over 5.2 volts, following a schematic nearly identical to that recommended in the datasheet. This supply feeds the front LEDs, and a 3V3 linear regulator to provide voltage for everything else.

Unfortunately, with a rated power of 175mW @ 86-265VAC, this supply has insufficient power to run the WiFi radio on the ESP32. While we can reduce our power consumption on the ESP32, it’s easy enough to add a cheap <$1 AC/DC module so we don’t have to worry about it.


This is a perfect use for dubiously safe sub-dollar AC-DC supplies, as thanks to the Tassimo’s non-isolated power supply we’re treating the entire thing as live AC anyway. I picked these up for a little under 10 cents on Taobao.

After connecting everything and a quick functional test, we can close up the machine for the final time; we can do everything else remotely.


And one more flashing after I forgot to flash the factory firmware after my jmp 0x00-ifieid version for JTAG testing.


Next Steps

With the interfaces documented, it’s now just a matter of uploading the corrected patches over WiFi to the ESP32 which can JTAG flash the main microcontroller into being more amicable to remote brewing.

#homeassistant   #reversing   #hacks   #hardware   #tassimo   #ghidra   #armv7