lowRISC / sonata-system

A full micro-controller system utilizing the CHERIoT Ibex core, part of the Sunburst project funded by UKRI
Apache License 2.0
23 stars 14 forks source link

Potential pinmux design #148

Open GregAC opened 1 month ago

GregAC commented 1 month ago

@alees24 @marnovandermaas please note below.

Here's an outline of a design spec for the pinxmux work. Feel free to suggest different ways to build this, just wanted to get my view of how it would all work written down and thought it'd be useful to have some discussion of the design before we implement it.

The pinmux will allow multiple blocks within Sonata to control a single physical pin. It does not enable dynamic any to any muxing functionality (where any IO from any block can control any pin). Rather a physical pin will have a small number of block IOs it could connect to. E.g. a particular pin could choose between a GPIO, CSn of SPI block 1, RX of UART 1 or SDA of I2C 1. There will be two parts to the pinmux:

Each physical pin will provide the following interface:

input  pin_output_i;        // Value to output on the pin
input  pin_output_enable_i; // Pin driven to output value when set, otherwise pin_output_i is ignored
output pin_input_o;         // Input value from the pin (always valid will be equal to pin_output_i when pin_output_enable_i is set)

Enable programmatic access to pull-up/pull-down configurations could be considered but isn't a critical part of core functionality.

The software interface for the pinmux will consist of a number of registers each filled with copies of an n-bit field (fixed n across all registers). Each field corresponds to a particular physical pin. Writing to that field sets which block controls that pin. The meaning of the index for each field will vary from pin to pin depending on what block IO is available to connect to that physical pin. We may wish to reserve index 0 to mean unconnected (so output enable low and input routed nowhere)

When a pin is connected to a block there are three possible types:

Which type is used is part of the static configuration

Static configuration

The static configuration has two major sections

Block Defintions - Split into two sub-sections, block types and block instances.

A block type provides the name of a block and of the IO that block (giving a name and type for each IO). Block instances specify how many of each block type is available.

An example YAML configuration for the block definition could look like this:

- block: spi
  ios:
    - pin: cs_n
      type: output
    - pin: sck
      type: output
    - pin: tx
      type: output
    - pin: rx
      type: input
- block: uart
  ios:
    - pin: tx
      type: output
    - pin: rx
      type: input
- block: rpi_gpio
  ios:
    # This is repetitive, may want special syntax to deal with but perhaps not in the first version
    - pin: io[0]
      type: in_out
    - pin: io[1]
      type: in_out
    # Continue on for all pins on RPI hat header

- block: mb_gpio
  ios:
    - pin: io[0]
      type: in_out
    - pin: io[1]
      type: in_out
    - pin: io[2]
      type: in_out
    # Continue on for all pins on mikroBUS

...

- instances:
  - spi: 5 # 5 SPI blocks
  - uart: 4 # 4 UART blocks
  - rpi_gpio: 1 # 1 RPI GPIO header

Connection Map - Specifies which block IO a physical pin can choose between

- pin: rph_g0 # RPI Hat pin 0
  block_ios:
    - spi[0].cs_n
    - uart[0].tx
    - rpi_gpio.io[0]

- pin: rph_g5 # RPI Hat pin 0
  block_ios:
    - spi[0].rx
    - uart[0].rx
    - rpi_gpio.io[5]

...

- pin: mb3 # mikroBUS Click pin 3
  block_ios:
    - spi[0].rx
    - uart[0].rx
    - mb_gpio.io[3]

...

Note that a physical pin can only have a connection to at most one pin in a block. This enables per block indexing for the mux selection field (e.g. say rph_g0 was connected to spi[0].cs_n and spi[0].sck we cannot just have indexes for SPI/UART/RPI GPIO).

With that configuration the pinmux generator will produce RTL that looks something like this:

module pinmux (
  // Clock, reset and tl register interface for config

  output pin_rph_g0_output_o,
  output pin_rph_g0_output_enable_o,
  input  pin_rph_g0_input_i,

  // More RPI hat pins

  output pin_rph_g5_output_o,
  output pin_rph_g5_output_enable_o,
  input  pin_rph_g5_input_i,

  // Many more pin interfaces

  output pin_mb3_output_o,
  output pin_mb3_output_enable_o,
  input  pin_mb3_input_i,

  // Many more pin interfaces

  input  [4:0] spi_csn_n,
  input  [4:0] spi_sck,
  input  [4:0] spi_tx,
  output [4:0] spi_rx

  input  [3:0] uart_tx,
  output [4:0] uart_rx,

  input  [27:0] rpi_gpio_io_output_i,
  input  [27:0] rpi_gpio_io_output_enable_i,
  output [27:0] rpi_gpio_io_input_o,

  input  [10:0] mb_gpio_io_output_i,
  input  [10:0] mb_gpio_io_output_enable_i,
  output [10:0] mb_gpio_io_input_o

  // Many more blocks
);

  // Outputs - Blocks IO is muxed to choose which drives the output and output
  // enable of a physical pin
  logic [2:0] rph_g0_output_sel;

  prim_onehot_mux #(
    .Width(1),
    .Inputs(3)
  ) u_rph_g0_output_mux (
    .in_i({spi_csn_n[0], uart_tx[0], rpi_gpio_io_output_i[0]}),
    .sel_i(rph_g0_output_sel),
    .out_o(pin_rph_g0_output_o)
  );

  prim_onehot_mux #(
    .Width(1),
    .Inputs(3)
  ) u_rph_g0_output_enable_mux (
    .in_i({1'b1, 1'b1, rpi_gpio_io_output_enable_i[0]}),
    .sel_i(rph_g0_output_sel),
    .out_o(pin_rph_g0_output_enable_o)
  );

  logic [0:0] rph_g5_output_sel;

  prim_onehot_mux #(
    .Width(1),
    .Inputs(1)
  ) u_rph_g5_output_mux (
    .in_i({rpi_gpio_io_output_i[5]}),
    .sel_i(rph_g5_output_sel),
    .out_o(pin_rph_g5_output_o)
  );

  prim_onehot_mux #(
    .Width(1),
    .Inputs(3)
  ) u_rph_g5_output_enable_mux (
    .in_i({rpi_gpio_io_output_enable_i[5]}),
    .sel_i(rph_g5_output_sel),
    .out_o(pin_rph_g5_output_enable_o)
  );

  logic [2:0] mb3_output_sel;

  prim_onehot_mux #(
    .Width(1),
    .Inputs(3)
  ) u_mb3_output_mux (
    .in_i({mb_gpio_io_output_i[3]}),
    .sel_i(mb3_output_sel),
    .out_o(pin_mb3_output_o)
  );

  prim_onehot_mux #(
    .Width(1),
    .Inputs(3)
  ) u_mb3_output_enable_mux (
    .in_i({mb_gpio_io_output_enable_i[3]}),
    .sel_i(mb3_output_sel),
    .out_o(pin_mb3_output_enable_o)
  );

  // Inputs - Physical pin inputs are muxed to particular block IO
  logic [0:0] rpi_gpio_io_input_sel [28];

  prim_onehot_mux #(
    .Width(1),
    .Inputs(1)
  ) u_gpio_io_0_input_mux (
    .in_i({pin_rph_g0_input_i})
    .sel_i(rpi_gpio_io_input_sel[0]),
    .out_o(rpi_gpio_io_input_o[0])
  );

  prim_onehot_mux #(
    .Width(1),
    .Inputs(1)
  ) u_gpio_io_5_input_mux (
    .in_i({pin_rph_g5_input_i})
    .sel_i(rpi_gpio_io_input_sel[5]),
    .out_o(rpi_gpio_io_input_o[5])
  );

  logic [0:0] mb_gpio_io_input_sel [10];

  prim_onehot_mux #(
    .Width(1),
    .Inputs(1)
  ) u_mb_gpio_io_3_input_mux (
    .in_i({pin_mb3_input_i})
    .sel_i(mb_gpio_io_input_sel[3]),
    .out_o(mb_gpio_io_input_o[3])
  );

  logic [1:0] spi_rx_input_sel [5];

  prim_onehot_mux #(
    .Width(1),
    .Inputs(2)
  ) u_spi_rx_0_input_mux (
    .in_i({pin_rph_g5_input_i, pin_mb3_input_i})
    .sel_i(spi_rx_input_sel[5]),
    .out_o(spi_rx[0])
  );

  logic [1:0] uart_rx_input_sel [4];

  prim_onehot_mux #(
    .Width(1),
    .Inputs(2)
  ) u_uart_rx_0_input_mux (
    .in_i({pin_rph_g5_input_i, pin_mb3_input_i})
    .sel_i(uart_rx_input_sel[0]),
    .out_o(uart_rx[0])
  );
endmodule

It does not include the logic that handles the register interface. Note the use of the onehot_mux module with one-hot encoded selector signals. The register interface will need to map writes to the pin selector fields to appropriate updates to these one-hot encoded selector signals. This utlizes more flops but provides the best timing through the muxes. One physical pin field update can need multiple selector signals to be updated. For instance should the pin selector for rph_g0 be written to point to GPIO rather than SPI we need to update the rph_g0_output_sel selector and the rpi_gpio_io_input_sel selector. We get the index 0 disconnect index for free in this setup. When an index of 0 is written the relevant selector signals have 0 written to them which will disable output enable and route the input to nowhere.

Another consideration is what happens when multiple pins are mapped to the same block IO. We could simply say this is a software problem, we'll get odd hardware behaviour as the one-hot muxing will mean the block receives the OR of multiple input pins. Otherwise we could implement detection for this issue in the RTL and disallow the field write but for the first version I'd tend towards saying it's a software problem.

Finally we may want some special case behaviour for I2C. In the current Sonata setup multiple physical pins are wired into the same I2C bus controlled by a single I2C block. We could have a 'I2C chain' concept. And pinmux will allow you to add and remove physical pins from being chained into one I2C block.

We will also want the tool to generate a table that tells us for each physical pin what indexes connect it to what block IO.

GregAC commented 1 month ago

We do need to consider if the onehot mux offers the most efficient implementation for FPGA. The pinmux could end up consuming significant logic resources due to all of the muxing.

Say you have 3 inputs to select from a single 6-LUT (the architecture on the Artix 7 FPGAs) can implement the whole mux with a one-hot select and you get the output 0 when select is 0 functionality.

A 6-LUT could implement a 4 input mux with a integer select (i.e. a 2-bit select) however that doesn't give you the output 0 functionality. Unless you hardwire one of the inputs to 0 and you're back to the same thing.

At 6 inputs, you'd need 3 LUTs to implement the one hot version, (composing 2x3 input one-hot muxes with a final OR) but I think you can do it with 2 for the integer select version. One 4 input mux fed with the bottom 2 bits of the select followed by a second 3 input mux that chooses between the first mux and the other two inputs, depending on the full select signal. This can have the bonus feature of output on 0 if the unused 6 and 7 select indexes are used.

This may mean what to choose depends upon the size of the mux. We don't have loads of time to experiment here but worth building things such that it is easy to do so.

marnovandermaas commented 1 month ago

Thanks for this proposal Greg. I'm tempted to write a dedicate Python script for this, unless anyone knows of a tool that can already to most of this.

In terms of the register interface, I would like to give each pin a dedicated 8 bits. This means you can update four pins with a single 32-bit write. As you suggest writing 0 disconnects both the output and the input, so the software should zero all of the registers before starting its configuration. This is what I would propose for the encoding:

If both 7 and 6 are zero then the value of the pin selection is ignored. If both 7 and 6 are one then also the output enable is connected to the block for full control. Since there is only one selection field it does prohibit pins from being an output of one block and an input from a separate block, but I don't see a need for this in our current design. I've kept 5 as reserved bit in case we want to extend it in the future and because I think a maximum of 32 different connections per pin is more than sufficient.

GregAC commented 1 month ago

What is the use of the output enable and input connected bits? The output enable is inherent in the pin block selection. It could be used for overriding the output enable from a block but are there any use cases for this (when connected to GPIO you get output enable control anyway, when connected to I2C it's vital the I2C block gets control over output enable).

Happy to have the pins each get a dedicated 8 bits with a 5 bit selection field but not sure we've got a compelling use case for the top 3 bits yet so they should all just be reserved.

GregAC commented 1 month ago

I'm tempted to write a dedicate Python script for this, unless anyone knows of a tool that can already to most of this.

Yes this was my intent, it will need a dedicated tool.

GregAC commented 1 month ago

Oh and it'd be worth building this so it uses the OpenTitan prim_pad_wrapper interface as it's way to interface with the pads, see the generic implementation here: https://github.com/lowRISC/opentitan/blob/master/hw/ip/prim_generic/rtl/prim_generic_pad_wrapper.sv

We wouldn't need to support the full capabilities of the wrapper, just use the bits that gives us the same functionality as the pin_X_output_o/pin_X_output_enable_o/pin_X_input interface I proposed above.

We'd then have the pinmux live in sonata_system wiring the pad wrapper interface out to the top-level. Then have something that looks like the OT padring (https://github.com/lowRISC/opentitan/blob/master/hw/top_earlgrey/rtl/padring.sv) in the FPGA top-level.

marnovandermaas commented 1 month ago

What is the use of the output enable and input connected bits? The output enable is inherent in the pin block selection. It could be used for overriding the output enable from a block but are there any use cases for this (when connected to GPIO you get output enable control anyway, when connected to I2C it's vital the I2C block gets control over output enable).

Happy to have the pins each get a dedicated 8 bits with a 5 bit selection field but not sure we've got a compelling use case for the top 3 bits yet so they should all just be reserved.

Ah yes, you're right, the output/input/in_out is specified by the block pins in the YAML description. So lets make bits 7-5 reserved and bits 4-0 selection.

nbdd0121 commented 1 month ago

Do we want the ability for an input pin to be fed to multiple blocks?

marnovandermaas commented 1 month ago

Do we want the ability for an input pin to be fed to multiple blocks?

I don't think this is required for us, but I also think that our current proposal does allow this.

alees24 commented 1 month ago

Some initial thoughts:

marnovandermaas commented 1 month ago

Thanks @alees24, here's my take based on what you said.

When the output is disabled, the pin is set to high-impedance. This is the same behaviour for I2C and GPIO I believe. For the driving of the I2C input when multiple pins are connected to the same I2C host, you need a way of specifying whether to & or | the signals (this leads into your last point).

Agreed leaving USB device out of this for now.

For input and in_out, I propose we have a dflt field. For input this decides what happens when it is disconnected. For in_out the disconnected state is high impedance, but the default field tells us whether to & or | a field if multiple pins are connected to the same IO input. For output this default is assumed to be high-impedance. Here's how it would look in the block definitions:

- block: uart
  ios:
    - name: tx
      type: output
    - name: rx
      type: input
      dflt: 1
- block: i2c
  ios:
    - name: scl
      type: in_out
      dflt: 1
    - name: sda
      type: in_out
      dflt: 1

I also changed pin to name in the block IOs to distinguish them from the pins defined in the connection map.

alees24 commented 1 month ago

Thanks for clarifying that. I had overlooked the availability of 'in_out' in the example; that helps. Presumably there is a reason that you've shortened 'default' to 'dflt' which I'm afraid I would argue is really not clear/somewhat unreadable? Perhaps we need a clearer alternative.

marnovandermaas commented 1 month ago

I thought it looked nice being the same length as name and type, but I agree it is unclear. I will make it default instead.

marnovandermaas commented 1 month ago

I have an alternative proposal for combining in_out IOs. Each in_out can specify its combine policy, which can be no, and or or. no just means that this IO cannot be combined with others, like GPIO. and is how I2C IOs should be anded together so that if one device pulls the bus low the whole bus goes low, or means the opposite where the default state is low and the bus goes high if any of the devices pulls the bus up.

nbdd0121 commented 1 month ago

It feels this is just an alternative spelling to push-pull and open-drain?