Rust Jupyter Kernel and FTDI (Part II)

for hardware interactive development (HID)

circuit4u
5 min readOct 11, 2020
UM232H + TM1637

Sometime ago, I wrote about driving TM1637 7-segment display with Parallax Propeller MCU and its SPIN language. (link)

Propeller provides a nice HID environment. Basically you write a few lines of SPIN code, and then press “F10” on keyboard. The code takes effect almost immediately. This becomes very useful when you are trying to figure out how to run a new piece of hardware.

Now this time, let’s see if we can do the same with FTDI (FT232H) IC driving hardware directly without any MCU in between.

FT232H/FT2232H MPSSE mode for I2C

Newer FTDI ICs such as FT232H or FT2232H came with MPSSE mode built-in, which can implement common data protocols such as SPI, I2C and more.

Because TM1637’s data protocol is very similar to I2C, we will focus on the I2C functions of MPSSE in this article. We will demonstrate SPI functions (much easier) in the future with TM1638 for example.

The diagram above depicts the hardware connections between a FT232H module (UM232H digikey link) and a TM1637 display module. These are the usual +3.3V and GND, plus two I2C signals: SCK and SDA.

Notice that the DI (AD1) and DO (AD2) of FT232H are shorted together to become one bi-directional SDA signal of I2C.

FTDI provides an additional MPSSE library on top of its D2XX library. However we will skip that extra layer, but focusing on D2XX functions directly to run MPSSE. It has two benefits:

  • It gets you closer to knowing the “technical details” of MPSSE engine.
  • Many languages, such as Rust, Go, Julia, Python have wrappers over FTDI D2XX library which already includes functions calls Ft_Write, Ft_Read, and Ft_SetBitMode.

The Rust code in this article can be easily ported to other languages.

The information to configure and run I2C with D2XX functions is spread across multiple FTDI application notes (AN411 link) and source code files (link).

Intuitively, you can think of that there is a MCU embedded inside FT232H. You send it various opcode + payload bytes via FT_Write(), after which FT232H reads/writes GPIO pins, shifts bytes/bits in/out at certain clock rate, at rising/falling clock edges.

Here is a reference of all these “opcodes” in one APP note (AN108 link).

Fortunately we only need a few of these opcodes to get I2C running.

The following assumes that you have already installed Rust kernel for Jupyter notebook.

Why Jupyter notebook for HID? Jupyter notebook is just like a REPL, any code can be executed and takes effect immediately. It also supports many language kernels, such as Python, Julia and Rust etc.

For the impatient, here is the whole Rust code Jupyter notebook.

I will briefly describe each sections below.

Initialize I2C

With D2XX library, you can open FTDI device directly (index 0, to pick the first one) or with a unique “serial number”. The serial number can be customized with “FT_Prog” (download link).

This is a big advantage over VCP. The user no longer worries about which COM port the operating system “randomly” assigns to the FTDI device.

After FT232H is open, you can configure MPSSE into I2C mode with set_bit_mode function. The other possible bit modes are all listed (here).

The following string of bytes are sent with D2XX Ft_Write function. It consists of three separate commands:

  • opcode = 0x8A: 60MHz main clock, not 12MHz
  • opcode = 0x8C: 3-phase clock for I2C
  • opcode = 0x86; payload = 200 (2 bytes): divide 60MHz down to make 100kHz I2C clock rate. It’s less obvious because of 3-phase clock. (See formula in C# source code example)

The next opcode 0x80 is the most used. It sets the GPIO high or low and as input or output. The two-byte payload consists of: “value, mask”.

For example 0x80, 0b0000_0011, 0b0000_0011 sets the lower two bits to be output and both high.

I2C Start Condition

The start condition of I2C is implemented with a SDA falling edge while SCK is held high.

The sequence of opcode 0x80 in start_i2c function generates a start condition

The commands are sent with separate FT_Write , so as to have enough delay in between the GPIO toggle, for the I2C slave to detect the start condition.

I2C Stop Condition

The stop condition is flagged by SDA rising edge while SCK is held high.

Similarly, stop_i2c function implements that.

Send one byte and read ACK

After I2C master sends out one byte, I2C slave responds with one ACK bit by pulling SDA low. FT232H, as I2C master, needs to read this single ACK bit back.

This is a bit cumbersome to implement. Let’s go through the whole send_byte function, opcode by opcode:

  • opcode = 0x19+ payload = 0, 0, byte_to_send: send one byte out on negative edge and LSB first
stealing this nice table from https://iosoft.blog/2018/12/05/ftdi-python-part-3/
  • opcode = 0x80; payload = 0, SCK: set SCK output low, but SDA as input (master releasing the I2C bus)
  • opcode = 0x22; payload =0: data input; bit mode; only 1 bit input
  • opcode = 0x87: transfer data back over USB immediately

The whole stream of bytes is sent in one FT_Write function call, so that there is minimal delay between the first 8 clock cycles and the 9th one to read I2C slave’s ACK bit.

Read bytes and ACK/NACK

Now comes time when I2C slave sends data back to the I2C master (FT232H), which needs to ACK or NACK to the I2C slave.

For example FT232H reads temperature, relative humidity data from SHT21 sensor. (in this gist: FT2232H with SHT21 sensor https://gist.github.com/circuit4u-medium/40c45d6fe0e51b64f0e6fe2219a14ab1)

Run TM1637 to display digits

In this case, we only write data to the TM1637 display module for display. In theory, we can read back over I2C bus the key scan bytes, if the corresponding IO lines connects to a keypad array.

With the I2C send_byte function defined, we can now experiment quickly with all kinds of TM1637 commands directly.

We can finally have the fun of HID without MCU firmware.

(go back to play with Rust Jupyter notebook)

--

--

circuit4u

memento of electronics and fun exploration for my future self