pcengines/apu2-documentation

details of i2c support on APU2

Opened this issue · 2 comments

sniku commented

First of all, thank you for continuing to work on the APUx platform and providing new BIOS updates regularly!

My goal is to get an OLED LCD screen to work on APU2 on FreeBSD.

I have spent a considerable amount of time trying to get i2c to work on *BSD without success. I somewhat succeeded on Linux, but only partially. I'll lay out everything I've learned and how far I've come and then, at the end, ask for help.

Hopefully, this post will be useful for others. i2c information for APU2 is sparse on the internet.

Hardware information

APU2/3/4 SOC has AMD A55E Hudson-1 FCH, which provides the PIIX4 device.
APUx motherboards have pins for i2c exposed, but the header isn't soldered.

I have a small 128x32 ssd1306 OLED display.

i2c support on Linux

Dmesg prints these three lines

[    7.382308] piix4_smbus 0000:00:14.0: SMBus Host Controller at 0xb00, revision 0
[    7.382334] piix4_smbus 0000:00:14.0: Using register 0x02 for SMBus port selection
[    7.383029] piix4_smbus 0000:00:14.0: Auxiliary SMBus Host Controller at 0xb20

On Linux, there's a kernel module that implements i2c for piix4. It's called i2c-piix4. Once this module is loaded, i2c can be listed with i2cdetect -l. On APU2 four Bus adapters are listed.

root@debapu:~# i2cdetect -l
i2c-3   smbus           SMBus PIIX4 adapter port 4 at 0b00      SMBus adapter
i2c-1   smbus           SMBus PIIX4 adapter port 2 at 0b00      SMBus adapter
i2c-4   smbus           SMBus PIIX4 adapter port 1 at 0b20      SMBus adapter
i2c-2   smbus           SMBus PIIX4 adapter port 3 at 0b00      SMBus adapter
i2c-0   smbus           SMBus PIIX4 adapter port 0 at 0b00      SMBus adapter

Note, all found devices are "SMBus adapter", not "I2C adapter" as I was expecting.

To detect what functionality the devices support, the following command is executed:

root@debapu:~# i2cdetect -F 0
Functionalities implemented by /dev/i2c-0:
**I2C                              no**
SMBus Quick Command              yes
SMBus Send Byte                  yes
SMBus Receive Byte               yes
SMBus Write Byte                 yes
SMBus Read Byte                  yes
SMBus Write Word                 yes
SMBus Read Word                  yes
SMBus Process Call               no
SMBus Block Write                yes
SMBus Block Read                 yes
SMBus Block Process Call         no
SMBus PEC                        no
**I2C Block Write                  no**
**I2C Block Read                   no**

i2cdetect says that those are not i2c adapters. Most/all i2c LCDs require I2C Block Write functionality to work.
I'm not sure if SMBus Block Write functionality can be used to drive i2c LCD. If it can, I would love to hear how to use it.

Interestingly, i2cset commands do work. These commands can be used to set individual pixels. A sample script driving the LCD, pixel by pixel, is pasted below.

Edit: Texas Instruments wrote a paper SMBus Compatibility With an I2C Device which explains the SMBus vs i2c differences.

i2cset commands work because they use "SMBus Write Byte" which is 1:1 compatible with "i2c Single-Byte Write". So in essence I'm using SMB commands to drive i2c device.

i2c OLED demo on Linux

Here's a short video demonstrating that some i2c functionality actually works on Linux.

20201226_231310.mp4

Here's the script used for the video.

modprobe i2c-dev

function display_off() {
i2cset -y 0 0x3c 0x00 0xAE  # Display OFF (sleep mode)
sleep 0.1
}

function init_display() {
i2cset -y 0 0x3c 0x00 0xA8  # Set Multiplex Ratio
i2cset -y 0 0x3c 0x00 0x3F    # value
i2cset -y 0 0x3c 0x00 0xD3  # Set Display Offset
i2cset -y 0 0x3c 0x00 0x00    # no vertical shift
i2cset -y 0 0x3c 0x00 0x40  # Set Display Start Line to 000000b
i2cset -y 0 0x3c 0x00 0xA1  # Set Segment Re-map, column address 127 ismapped to SEG0
i2cset -y 0 0x3c 0x00 0xC8    # Set COM Output Scan Direction, remapped mode. Scan from COM7 to COM0
#i2cset -y 0 0x3c 0x00 0xC0   # Set COM Output Scan Direction, remapped mode. Scan from COM7 to COM0
i2cset -y 0 0x3c 0x00 0xDA  # Set COM Pins Hardware Configuration
#i2cset -y 0 0x3c 0x00 0x12   # Alternative COM pin configuration, Disable COM Left/Right remap
#i2cset -y 0 0x3c 0x00 0x2    # Sequential COM pin configuration,  Disable COM Left/Right remap
#i2cset -y 0 0x3c 0x00 0x22   # Sequential COM pin configuration,  Enable Left/Right remap  (8pixels height)
i2cset -y 0 0x3c 0x00 0x32    # Alternative COM pin configuration, Enable Left/Right remap   (4pixels height)
#i2cset -y 0 0x3c 0x00 0x81 # Set Contrast Control
#i2cset -y 0 0x3c 0x00 0xCF   # value, 0x7F max.
i2cset -y 0 0x3c 0x00 0xA4  # display RAM content
i2cset -y 0 0x3c 0x00 0xA6  # non-inverting display mode - black dots on white background
i2cset -y 0 0x3c 0x00 0xD5  # Set Display Clock (Divide Ratio/Oscillator Frequency)
i2cset -y 0 0x3c 0x00 0x80    # max fequency, no divide ratio
i2cset -y 0 0x3c 0x00 0x8D  # Charge Pump Setting
i2cset -y 0 0x3c 0x00 0x14    # enable charge pump
i2cset -y 0 0x3c 0x00 0x20  # page addressing mode
i2cset -y 0 0x3c 0x00 0x20    # horizontal addressing mode
#i2cset -y 0 0x3c 0x00 0x21   # vertical addressing mode
#i2cset -y 0 0x3c 0x00 0x22   # page addressing mode
}

function display_on() {
i2cset -y 0 0x3c 0x00 0xAF  # Display ON (normal mode)
sleep 0.001
}

function reset_cursor() {
i2cset -y 0 0x3c 0x00 0x21  # set column address
i2cset -y 0 0x3c 0x00 0x00  #   set start address
i2cset -y 0 0x3c 0x00 0x7F  #   set end address (127 max)
i2cset -y 0 0x3c 0x00 0x22  # set page address
i2cset -y 0 0x3c 0x00 0x00  #   set start address
i2cset -y 0 0x3c 0x00 0x07  #   set end address (7 max)
}

display_off
init_display
display_on
reset_cursor

# fill screen
for i in $(seq 1024)
do
   i2cset -y 0 0x3c 0x40 0xff
done

reset_cursor

# clear screen
for i in $(seq 1024)
do
   i2cset -y 0 0x3c 0x40 0x0
done

reset_cursor

# draw a pattern
for i in $(seq 146)
do
    for i in 1 4 16 64 16 4 1
    do
        i2cset -y 0 0x3c 0x40 $i
    done
done

While this works, it's far from useful. The refresh rate of the display when driven pixel-by-pixel isn't acceptable.
While I could maybe accept one frame per ~3 seconds, none of the LCD drivers use this method, so I would have to code it from scratch, which is not fun :-)

i2c support on *BSD

I experimented on Linux only to get a better understanding of i2c support on APU. I can't use Linux for the project I'm currently working on.

On FreeBSD dmesg prints these two lines

intsmb0: <AMD FCH SMBus Controller> at device 20.0 on pci0
smbus0: <System Management Bus> on intsmb0

On FreeBSD there are several kernel modules for i2c, but none of them seem to support piix4.

root@freeApu:~ # kldstat
Id Refs Address                Size Name
 1   44 0xffffffff80200000  227ad00 kernel
 2    1 0xffffffff8247b000     3158 gpioiic.ko
 3    4 0xffffffff8247f000     a4f0 gpiobus.ko
 4    2 0xffffffff8248a000     5838 iicbb.ko
 5    5 0xffffffff82490000     afa0 iicbus.ko
 6    1 0xffffffff8249b000     3a28 iicsmb.ko
 7    3 0xffffffff8249f000     2ef0 smbus.ko
 8    1 0xffffffff824a3000   3bad38 zfs.ko
 9    2 0xffffffff8285e000     a448 opensolaris.ko
10    1 0xffffffff82869000     38d0 iic.ko
11    1 0xffffffff8286d000     a960 ig4.ko
12    1 0xffffffff82b21000     2698 intpm.ko
13    1 0xffffffff82b24000     16f0 nctgpio.ko
14    1 0xffffffff82b26000     2488 superio.ko
15    1 0xffffffff82b29000      4a0 gpioled.ko

root@freeApu:~ # i2c -s -v
dev: /dev/iic0, addr: 0x3fff, r/w: r, offset: 0x00, width: 8, count: 1
Error opening I2C controller (/dev/iic0) for scanning: No such file or directory

The /dev/iic* devices are missing. I haven't managed to achieve much more than that even though I tried many, many things.

Any help here would be greatly appreciated.

Changing direction

IF there's no "true" i2c support on APU, then perhaps there's another way of driving OLED displays?

  1. GPIO (i2c bit-banging through GPIO pins)
  2. Serial connection through the internal COM2 port ?
  3. SPI
  4. SMBus?
  5. get a real i2c adapter that can be connected to PCI or internal USB?

Let's go one by one
GPIO
I know that at least on Linux it's possible to hook up the i2c display I have to GPIO pins and drive it using "bit-banging". While this isn't the most optimal way of driving LCDs, perhaps it's good enough.
I haven't managed to get it to work on *BSD, but maybe I haven't tried hard enough (yet).

FreeBSD has gpioiic kernel module, which is "GPIO I2C bit-banging device driver".

The GPIO pins are detected on BSD 🎉

root@freeApu:~ # kldload nctgpio
root@freeApu:~ # gpioctl -l
pin 00: 0       GPIO00<IN,OD>
pin 01: 0       GPIO01<IN,OD>
pin 02: 0       GPIO02<IN,OD>
pin 03: 0       GPIO03<IN,OD>
pin 04: 0       GPIO04<IN,OD>
pin 05: 0       GPIO05<IN,OD>
pin 06: 0       GPIO06<IN,OD>
pin 07: 0       GPIO07<IN,OD>
pin 08: 0       GPIO10<IN,OD>
pin 09: 0       GPIO11<IN,OD>
pin 10: 0       GPIO12<IN,OD>
pin 11: 0       GPIO13<IN,OD>
pin 12: 0       GPIO14<IN,OD>
pin 13: 0       GPIO15<IN,OD>
pin 14: 0       GPIO16<IN,OD>
pin 15: 0       GPIO17<IN,OD>

But I'm not sure where to go from here.

Serial connection through the internal COM2 port
This is something that has been suggested on one of the numerous forum posts I've read through. I have not tried it.
Is it feasible?

SMBus
Is it possible to somehow use the SMBus functionality to drive LCD?

SPI
I'm only listing SPI for completeness. After reading the hardware schematics, I see that the SPI header on APU is only useful for BIOS recovery. Or am I wrong?

External I2C adapter
Is there a hardware i2c adapter that can be connected to mPCIe or internal USB? If there's one, I would be happy to buy it and not worry about the other methods. :-)

Any comments, corrections, or general help would be much appreciated :-)

What did support@pcengines.ch say? You should post their response here to add to this nice i2c writeup on the APU.
I have not tried playing with i2c on the APU2 myself sadly, but I expect PC Engines support to have the answers you seek.

@sniku I recall an attempt to connect LCD display to I2C/SMBus pins on APU. If SMBus write byte is compatible with I2C write byte, this is the best possible way to drive it, although it would be better to have a system driver or software written in C )or something else) to avoid going through all system wrappers around the i2c tools.

As for other solutions, you could for example use the FTDI FT232H and connect it to an internal USB 2.0 port on the board which would enable you a pure I2C interface. FT232H should have a decent support (FreeBSD should have libftdi or something), maybe it could help. Although that also means writing some code to handle the display (in python for example).

Another solution is to check for the display's model-specific driver if it exists anywhere for Linux or FreeBSD.