TS-9370 FPGA

From embeddedTS Manuals

FPGA Registers

The TS-9370's FPGA is connected to the CPU over the FlexSPI bus. This provides 32-bit access to the FPGA, mapped at 0x2800_0000.

Offset Description
0x0000 Model/Rev Info[1]
0x0020 IRQ core
0x0040 FPGA GPIO block #0
0x0080 FPGA GPIO block #1
0x00C0 FPGA GPIO block #2
  1. See the #FPGA Revisions section for how to interpret these registers.

FPGA GPIO Instances

Note: Unlike GPIO into the CPU, at present the FPGA GPIOs do not support interrupts.

Each of the three GPIO blocks can manage up to 32 IO lines. The 32-bit registers controlling each block are defined as follows:

Offset Read Function Write Function
0x00 Output Enables Set OE Bits
0x04 Reserved Clear OE Bits
0x08 Output Data Set Data Bits
0x0c Input Data Clear Data Bits
0x10 Reserved Reserved
0x14 Reserved Reserved
0x18 Reserved Reserved
0x1c Reserved Reserved

FPGA PWM

This system includes PWM that supports 10-bit duty/period, a 66.666666mhz input clock, and 12 values of input clock shift.

Linux supports this API through the /sys/ interface using file I/O. First export the pwm channel to enable it:

# Export PWM channel 0
echo 0 > /sys/class/pwm/pwmchip0/export
File Description
/sys/class/pwm/pwmchip0/pwm0/period Period in nanoseconds. Must be bigger than the duty cycle or writes will fail. Can only change when the pwm is disabled.
/sys/class/pwm/pwmchip0/pwm0/duty_cycle Duty cycle in nanoseconds. Can change at any time, must be less than period.
/sys/class/pwm/pwmchip0/pwm0/enable When 1, pwm is outputting. When 0, outputs idle state of the PWM.
/sys/class/pwm/pwmchip0/pwm0/polarity When "normal", idle high and duty cycle low. When "inversed", idle low and duty cycle high. A valid period must be set before this can be changed.

For example, for a 50hz signal with 25% duty cycle:

# Set Period to 20ms
echo 20000000 > /sys/class/pwm/pwmchip0/pwm0/period
# Set duty cycle to 5ms
echo 5000000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
# Enable PWM and output 50hz signal
echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable

# Duty cycle can be changed while it is enabled
echo 1000000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle

The Linux PWM API will attempt to arrive at the exact period at the cost of the duty cycle resolution. For the most possible duty cycle resolution use one of the max period ns values from the table below.

Shift PWM Input Frequency (Hz) Duty Cycle units (ns) Max Period (ns) Max Period (Hz)
0 66666666 15 15345 65167
1 33333333 30 30690 32583
2 16666666 60 61380 16291
3 8333333 120 122760 8145
4 4166666 240 245520 4072
5 2083333 480 491040 2036
6 1041666 960 982080 1018
7 520833 1920 1964161 509
8 260416 3840 3928330 254
9 130208 7860 7856660 127
10 65104 15360 15713320 63
11 32552 30720 31426640 31

If period is set to one of these values, the full 10 bits of duty cycle is available. Past that, the Linux API will use the closest available value. Debug output can be enabled with:

echo "file pwm-ts.c +p" > /sys/kernel/debug/dynamic_debug/control

If this is enabled, the kernel can output additional information after setting a frequency:

echo 0 > /sys/class/pwm/pwmchip0/export
# 10ms period:
echo 10000000 > /sys/class/pwm/pwmchip0/pwm0/period
# 5ms duty cycle:
echo 5000000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle
echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable
dmesg | tail

This will output:

[   75.758146] ts-pwm 500001a8.mikro_pwm: cycle=1293661 shift=10 cnt=773
[   75.758184] ts-pwm 500001a8.mikro_pwm: shift=10 cnt=773 duty_cnt=387

The last value in cnt indicates how much resolution is available for the duty cycle at this given period. In the best case there are 10 bits (0-2047) to specify duty cycle, but this above example is 0-773 to arrive at this particular period. You can determine the duty cycle increments with period / cnt. From the above example:

10000000 / 773 = 12936.61

The duty cycle can then be configured in increments of 12936ns. Smaller values will round to the closest value.

This PWM will allow a max speed of 79.2MHz / 3 = 26.4MHz, but this will sacrifice all of the available duty cycle except an on/50%/off. The slowest speed is highest divisor at 38hz.

While the Linux driver is recommended for most users, the PWM core is located at 0x28000400, each PWM are 0x20 registers apart.

Address Bits Description
0x00 (CONFIG) 31:2 Reserved
1 PWM Inversion (0 = Idle high, duty cycle low), (1 = Idle low, duty cycle high)
0 PWM Enable (1 = Enable PWM output, 0 = Drive default state)
0x04 (PERIOD) 31:10 Reserved
9:0 PWM Period (0–1023 steps)
0x08 (DUTY) 31:10 Reserved
9:0 PWM Duty Cycle (0–1023 steps)
0x0C (SHIFT) 31:12 Reserved
11:0 Shift (Clock frequency = 66666666 / (1 >> shift))

FPGA Circuit Breaker

This board includes:

  • 1x High side switches capable of sourcing 200mA
  • 4x Low side switches capable of sinking 500mA each, or 1A total.

If these sink/source values are exceeded, the "DIO_FAULT#" IO will trip to prevent damage. Any detected edge on DIO_FAULT# will trigger a latch in the FPGA, and the "circuit_breaker_fault" will assert. As soon as this signal is asserted, all of the high side and low side switches will disable, regardless of the data out value in the GPIO.

The circuit_breaker_fault will stay high until the fault condition is cleared. The fault condition is cleared by creating a rising edge on clear_fault.

For example:

# Set EN_LS_OUT_1 high
gpioset 6 17=1

If this then exceeds the current limit:

# Read circuit_breaker_fault
gpioget 6 21

This pin can be read as an interrupt or polled. If it returns 1, then the circuit breaker has tripped. Once the issue causing the overcurrent condition is cleared, the clear_fault IO must be pulsed. The circuit_breaker_fault signal is cleared on the rising edge.

# Set clear_fault low
gpioset 6 15=0
# Set clear_fault high
gpioset 6 15=1

# re-read circuit_breaker_fault, and it should now return 0 if the condition has not re-tripped.
gpioget 6 21

FPGA XBAR

The FPGA XBAR controller allows rerouting some fpga pins between other pins. For example, the FPGA has connections to many of the CPU UARTs, SPI, and has several internal cores. This lets the users control which controllers are connected to certain pins. For example, the daughtercard header is all GPIO by default. The xbar allows routing the CPU's UART away from a RS-485 transceiver, and instead to this daughter card header. This does not make the daughter card header RS-485 levels, but just changes which controller drives these pins.

Under Linux the pin mapping can be controlled 2 ways, either through a device tree modification, or through debugfs in userspace.

For Userspace:


cd "/sys/kernel/debug/pinctrl/28002000.pinctrl-tsxbar-pinctrl/"

# echo a list of every output pin
cat pingroups

# echo a list of every function available:
cat pinmux-functions

# To see functions on a given pin:
cat pinmux-functions | grep MIKRO_TXD
# This returns:
##function 3: GPIO0_IO14, groups = [ MIKRO_TXD ]
##function 4: UART8_TXD, groups = [ MIKRO_TXD ]

# By default, MIKRO_TXD is a uart.  To select the GPIO:
echo "MIKRO_TXD GPIO0_IO14" > pinmux-select
# To select the UART again:
echo "MIKRO_TXD UART8_TXD" > pinmux-select


For the device tree:


In the device tree, find the compatible = "technologic,ts9370-xbar"; node, and add pin mappings underneath that. See pinfunc-ts9370.h in the same path as the device tree for the available options. For example, this is how to define the mikrobus UART pins as GPIO or UART:

		fpga_xbar: pinctrl@28002000 {
			compatible = "technologic,ts9370-xbar";
			reg = <0x2000 0x200>;

			pinctrl_uart_mikrobus: mikrobusuartgrp {
				xbar,pins = <
					TS9390_PAD_MIKRO_TXD__UART8_TXD
					TS9390_PAD_UART8_RXD__MIKRO_RXD
				>;
			};

			pinctrl_gpio_mikrobus: mikrobusgpiogrp {
				xbar,pins = <
					TS9390_PAD_MIKRO_TXD__GPIO0_IO14
					TS9390_PAD_MIKRO_RXD__GPIO0_IO15
				>;
			};
		};

The mikrobus UART is the default, but this is how it would be explicitly mapped in the device tree:

&lpuart8 {
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_uart8 &pinctrl_uart_mikrobus>;
	status = "okay";
};

The XBAR controller supports these muxing options:

Alt 0 Alt 1 Alt 2 Alt 3 Alt 4 Alt 5 Alt 6 Alt 7
Pin #00 (UART6_TXD), Default BT_RXD
N/A UART6_TXD N/A N/A N/A N/A N/A N/A
Pin #01 (HIGHZ), Default BT_TXD
N/A N/A N/A N/A N/A N/A N/A N/A
Pin #02 (HIGHZ), Default BT_RTS
N/A N/A N/A N/A N/A N/A N/A N/A
Pin #03 (UART6_RTS), Default BT_CTS
N/A UART6_RTS N/A N/A N/A N/A N/A N/A
Pin #04 (DC_1), Default DP_19P2MHZ_CLK
N/A DC_1 N/A N/A N/A N/A N/A N/A
Pin #05 (HIGHZ), Default DP_OSC_CLK
N/A N/A N/A N/A N/A N/A N/A N/A
Pin #06 (UART8_TXD), Default MIKRO_TXD
GPIO0_IO14 UART8_TXD N/A N/A N/A N/A N/A N/A
Pin #07 (GPIO0_IO15), Default MIKRO_RXD
GPIO0_IO15 N/A N/A N/A N/A N/A N/A N/A
Pin #08 (LPSPI4_CLK), Default MIKRO_SPI_CLK
GPIO0_IO16 LPSPI4_CLK N/A N/A N/A N/A N/A N/A
Pin #09 (lpspi4_cs_mux_1), Default MIKRO_SPI_CS#
GPIO0_IO17 lpspi4_cs_mux_1 N/A N/A N/A N/A N/A N/A
Pin #10 (GPIO0_IO18), Default MIKRO_SPI_MISO
GPIO0_IO18 N/A N/A N/A N/A N/A N/A N/A
Pin #11 (LPSPI4_MOSI), Default MIKRO_SPI_MOSI
GPIO0_IO19 LPSPI4_MOSI N/A N/A N/A N/A N/A N/A
Pin #12 (GPIO0_IO20), Default MIKRO_RESET#
GPIO0_IO20 N/A N/A N/A N/A N/A N/A N/A
Pin #13 (GPIO0_IO21), Default MIKRO_AN
GPIO0_IO21 N/A N/A N/A N/A N/A N/A N/A
Pin #14 (PWM0_OUT), Default MIKRO_PWM
GPIO0_IO22 PWM0_OUT N/A N/A N/A N/A N/A N/A
Pin #15 (GPIO0_IO23), Default MIKRO_INT
GPIO0_IO23 N/A N/A N/A N/A N/A N/A N/A
Pin #16 (GPIO2_IO27), Default DC_1
GPIO2_IO27 UART7_TX LPSPI4_CLK N/A N/A N/A N/A N/A
Pin #17 (GPIO2_IO28), Default DC_3
GPIO2_IO28 N/A N/A N/A N/A N/A N/A N/A
Pin #18 (GPIO2_IO29), Default DC_5
GPIO2_IO29 UART7_RTS LPSPI4_MOSI N/A N/A N/A N/A N/A
Pin #19 (GPIO2_IO30), Default DC_7
GPIO2_IO30 lpspi4_cs_mux_2 N/A N/A N/A N/A N/A N/A
Pin #20 (GPIO2_IO31), Default DC_9
GPIO2_IO31 N/A N/A N/A N/A N/A N/A N/A
Pin #21 (HIGHZ), Default DIO_1_IN
N/A N/A N/A N/A N/A N/A N/A N/A
Pin #22 (HIGHZ), Default DIO_2_IN
N/A N/A N/A N/A N/A N/A N/A N/A
Pin #23 (HIGHZ), Default DIO_3_IN
N/A N/A N/A N/A N/A N/A N/A N/A
Pin #24 (HIGHZ), Default DIO_4_IN
N/A N/A N/A N/A N/A N/A N/A N/A
Pin #25 (GPIO2_IO17), Default EN_LS_OUT_1
GPIO2_IO17 N/A N/A N/A N/A N/A N/A N/A
Pin #26 (GPIO2_IO18), Default EN_LS_OUT_2
GPIO2_IO18 N/A N/A N/A N/A N/A N/A N/A
Pin #27 (GPIO2_IO19), Default EN_LS_OUT_3
GPIO2_IO19 N/A N/A N/A N/A N/A N/A N/A
Pin #28 (GPIO2_IO20), Default EN_LS_OUT_4
GPIO2_IO20 N/A N/A N/A N/A N/A N/A N/A
Pin #29 (GPIO2_IO16), Default EN_HS_SW
GPIO2_IO16 N/A N/A N/A N/A N/A N/A N/A
Pin #30 (UART7_TXD), Default PRIM_485_TXD
N/A UART7_TXD N/A N/A N/A N/A N/A N/A
Pin #31 (UART7_RTS), Default PRIM_485_TXEN
N/A UART7_RTS N/A N/A N/A N/A N/A N/A
Pin #32 (HIGHZ), Default PRIM_485_RXD
N/A N/A N/A N/A N/A N/A N/A N/A
Pin #33 (UART3_RTS), Default SEC_485_TXEN
N/A UART3_RTS N/A N/A N/A N/A N/A N/A
Pin #34 (HIGHZ), Default SEC_485_RXD
N/A N/A N/A N/A N/A N/A N/A N/A
Pin #35 (UART3_TXD), Default SEC_UART_TX
N/A UART3_TXD N/A N/A N/A N/A N/A N/A
Pin #36 (GPIO0_IO0), Default EN_GREEN_LED#
GPIO0_IO0 N/A N/A N/A N/A N/A N/A N/A
Pin #37 (GPIO0_IO2), Default EN_RED_LED#
GPIO0_IO2 N/A N/A N/A N/A N/A N/A N/A
Pin #38 (HIGHZ), Default PUSH_SW#
N/A N/A N/A N/A N/A N/A N/A N/A
Pin #39 (PLL_CLK_OUT), Default CODEC_CLK
N/A PLL_CLK_OUT N/A N/A N/A N/A N/A N/A
Pin #40 (GPIO0_IO4), Default NIM_RESET#
GPIO0_IO4 N/A N/A N/A N/A N/A N/A N/A
Pin #41 (GPIO0_IO5), Default NIM_CTS#
GPIO0_IO5 N/A N/A N/A N/A N/A N/A N/A
Pin #42 (GPIO0_IO6), Default NIM_PWR_ON#
GPIO0_IO6 N/A N/A N/A N/A N/A N/A N/A
Pin #43 (HIGHZ), Default NIM_STATUS
N/A N/A N/A N/A N/A N/A N/A N/A
Pin #44 (UART5_TXD), Default NIM_TXD
GPIO0_IO10 UART5_TXD N/A N/A N/A N/A N/A N/A
Pin #45 (GPIO0_IO11), Default NIM_RXD
GPIO0_IO11 N/A N/A N/A N/A N/A N/A N/A
Pin #46 (HIGHZ), Default NO_SCAP_CHRG#
N/A N/A N/A N/A N/A N/A N/A N/A
Pin #49 (SECOND_PORT_RXD_3V), Default UART3_RXD
N/A SECOND_PORT_RXD_3V N/A N/A N/A N/A N/A N/A
Pin #50 (HIGHZ), Default UART3_CTS
N/A N/A N/A N/A N/A N/A N/A N/A
Pin #51 (NIM_RXD), Default UART5_RXD
N/A NIM_RXD N/A N/A N/A N/A N/A N/A
Pin #52 (BT_TXD), Default UART6_RXD
N/A BT_TXD N/A N/A N/A N/A N/A N/A
Pin #53 (BT_RTS), Default UART6_CTS
N/A BT_RTS N/A N/A N/A N/A N/A N/A
Pin #54 (PRIM_485_RXD_3V), Default UART7_RXD
N/A PRIM_485_RXD_3V DC_3 SECOND_PORT_RXD_3V N/A N/A N/A N/A
Pin #55 (DC_7), Default UART7_CTS
N/A DC_7 N/A N/A N/A N/A N/A N/A
Pin #56 (MIKRO_RXD), Default UART8_RXD
N/A MIKRO_RXD N/A N/A N/A N/A N/A N/A
Pin #57 (MIKRO_SPI_MISO), Default lpspi4_miso_mux_1
N/A MIKRO_SPI_MISO N/A N/A N/A N/A N/A N/A
Pin #58 (DC_3), Default lpspi4_miso_mux_2
N/A DC_3 N/A N/A N/A N/A N/A N/A
Pin #59 (GPIO0_IO3), Default EN_BLUE_LED
GPIO0_IO3 N/A N/A N/A N/A N/A N/A N/A

While the existing drivers should be used for any iomux interaction, this is the register documentation for interacting directly with the core.

Address Bits Read/Write Description
0x0 31:24 RW PIN[n+3] FUNC_SEL
23:16 RW PIN[n+2] FUNC_SEL
15:8 RW PIN[n+1] FUNC_SEL
7:0 RW PIN[n+0] FUNC_SEL

This repeats for 0x0+ceil(N_PINS/4).

For each FUNC_SEL register:

Bits Description
3 HIGH_Z_EN, 1 = enable high-z, regardless of lower bits, 0 drive peripheral values
2:0 Value of 0-7 selects a function that drives oe+data

FPGA SPI MUX

The #FPGA XBAR maps signals that have single drivers, but sharing the SPI bus multiple places requires driving the correct MISO signal back to the FPGA based on which device is selected. To support sharing the CPU SPI bus, the FPGA implements a mux compatible with Linux's spi-mux. This is supported with existing drivers in our kernels.

While this could use GPIO for chip selects in Linux, this has impacts on performance. Most SPI devices require the chip select not just for selecting the active device, but framing all communication. By muxing the bus, we can use the real hardware chip select, and support DMA with the chip select framing.

The FPGA still routes a common SPI clock, MOSI, to each device. The MUX selects which MISO is sent back to the CPU, and where the hardware chip select is routed.

The FPGA GPIO bank 2 IO 7, 8, 9 make but spimux0, spimux1, spimux2. These allow selecting 8 possible locations for the SPI. In the FPGA XBAR, this is the signals:

lpspi4_miso_mux[7:0]
lpspi4_cs_mux[7:0]

Not all of these are brought out in the default FPGA:

SPI MUX Location
000 Onboard ADC
001 Mikrobus SPI
010 Daughtercard header SPI
011 N/A
100 N/A
101 N/A
110 N/A
111 N/A

FPGA Updates

For most Linux users, the FPGA can be updated with:

curl -sSL http://files.embeddedts.com/ts-arm-sbc/ts-9370-linux/fpga/update-fpga.sh | sh

Then reboot.

This FPGA supports multiple application loads. On startup, it always starts the first image which is a bootloader that is capable of rewriting the FPGA's flash. If there is no update, it sets a register that switches the FPGA to the second image which contains the common functionality like GPIO, IRQs, etc.

U-boot checks checks for a valid 'fit' image in the eMMC boot 1 partition (mmcblk0boot1) at offset 0x280000. If it finds this valid update, then before switching out of the FPGA bootloader it rewrites the second application load. This take approximately 40 seconds. After writing, it then erases the region of mmcblk0boot1 containing the update, and then sets the register in the bootloader that updates the FPGA.

This update process is designed so the typical case of updating the application load is safe even in the case of sudden power loss. The FPGA bootloader is in a separate erase block of the FPGA's internal flash, so if we are interrupted in the middle of erasing/writing flash it only affects the application load. On the next start up, since the update has not been completed and still exists on mmcblk0boot1, it will restart the update and try again until it succeeds.