TS-9370 FPGA
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 |
- ↑ 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.