hw/char/pl011: Add support for loopback
This patch adds loopback for sent characters, sent BREAK,
and modem-control signals.
Loopback of send and modem-control is often used for uart
self tests in real hardware but missing from current pl011
model, resulting in self-test failures when running in QEMU.
This implementation matches what is observed in real pl011
hardware placed in loopback mode:
1. Input characters and BREAK events from serial backend
are ignored, but
2. Both TX characters and BREAK events are still sent to
serial backend, in addition to be looped back to RX.
Signed-off-by: Tong Ho <tong.ho@amd.com>
Signed-off-by: Francisco Iglesias <francisco.iglesias@amd.com>
Message-id: 20240227054855.44204-1-tong.ho@amd.com
Reviewed-by: Peter Maydell <peter.maydell@linaro.org>
Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
diff --git a/hw/char/pl011.c b/hw/char/pl011.c
index 855cb82..8753b84 100644
--- a/hw/char/pl011.c
+++ b/hw/char/pl011.c
@@ -49,10 +49,14 @@
}
/* Flag Register, UARTFR */
+#define PL011_FLAG_RI 0x100
#define PL011_FLAG_TXFE 0x80
#define PL011_FLAG_RXFF 0x40
#define PL011_FLAG_TXFF 0x20
#define PL011_FLAG_RXFE 0x10
+#define PL011_FLAG_DCD 0x04
+#define PL011_FLAG_DSR 0x02
+#define PL011_FLAG_CTS 0x01
/* Data Register, UARTDR */
#define DR_BE (1 << 10)
@@ -76,6 +80,13 @@
#define LCR_FEN (1 << 4)
#define LCR_BRK (1 << 0)
+/* Control Register, UARTCR */
+#define CR_OUT2 (1 << 13)
+#define CR_OUT1 (1 << 12)
+#define CR_RTS (1 << 11)
+#define CR_DTR (1 << 10)
+#define CR_LBE (1 << 7)
+
static const unsigned char pl011_id_arm[8] =
{ 0x11, 0x10, 0x14, 0x00, 0x0d, 0xf0, 0x05, 0xb1 };
static const unsigned char pl011_id_luminary[8] =
@@ -251,6 +262,89 @@
s->ibrd, s->fbrd);
}
+static bool pl011_loopback_enabled(PL011State *s)
+{
+ return !!(s->cr & CR_LBE);
+}
+
+static void pl011_loopback_mdmctrl(PL011State *s)
+{
+ uint32_t cr, fr, il;
+
+ if (!pl011_loopback_enabled(s)) {
+ return;
+ }
+
+ /*
+ * Loopback software-driven modem control outputs to modem status inputs:
+ * FR.RI <= CR.Out2
+ * FR.DCD <= CR.Out1
+ * FR.CTS <= CR.RTS
+ * FR.DSR <= CR.DTR
+ *
+ * The loopback happens immediately even if this call is triggered
+ * by setting only CR.LBE.
+ *
+ * CTS/RTS updates due to enabled hardware flow controls are not
+ * dealt with here.
+ */
+ cr = s->cr;
+ fr = s->flags & ~(PL011_FLAG_RI | PL011_FLAG_DCD |
+ PL011_FLAG_DSR | PL011_FLAG_CTS);
+ fr |= (cr & CR_OUT2) ? PL011_FLAG_RI : 0;
+ fr |= (cr & CR_OUT1) ? PL011_FLAG_DCD : 0;
+ fr |= (cr & CR_RTS) ? PL011_FLAG_CTS : 0;
+ fr |= (cr & CR_DTR) ? PL011_FLAG_DSR : 0;
+
+ /* Change interrupts based on updated FR */
+ il = s->int_level & ~(INT_DSR | INT_DCD | INT_CTS | INT_RI);
+ il |= (fr & PL011_FLAG_DSR) ? INT_DSR : 0;
+ il |= (fr & PL011_FLAG_DCD) ? INT_DCD : 0;
+ il |= (fr & PL011_FLAG_CTS) ? INT_CTS : 0;
+ il |= (fr & PL011_FLAG_RI) ? INT_RI : 0;
+
+ s->flags = fr;
+ s->int_level = il;
+ pl011_update(s);
+}
+
+static void pl011_put_fifo(void *opaque, uint32_t value);
+
+static void pl011_loopback_tx(PL011State *s, uint32_t value)
+{
+ if (!pl011_loopback_enabled(s)) {
+ return;
+ }
+
+ /*
+ * Caveat:
+ *
+ * In real hardware, TX loopback happens at the serial-bit level
+ * and then reassembled by the RX logics back into bytes and placed
+ * into the RX fifo. That is, loopback happens after TX fifo.
+ *
+ * Because the real hardware TX fifo is time-drained at the frame
+ * rate governed by the configured serial format, some loopback
+ * bytes in TX fifo may still be able to get into the RX fifo
+ * that could be full at times while being drained at software
+ * pace.
+ *
+ * In such scenario, the RX draining pace is the major factor
+ * deciding which loopback bytes get into the RX fifo, unless
+ * hardware flow-control is enabled.
+ *
+ * For simplicity, the above described is not emulated.
+ */
+ pl011_put_fifo(s, value);
+}
+
+static void pl011_loopback_break(PL011State *s, int brk_enable)
+{
+ if (brk_enable) {
+ pl011_loopback_tx(s, DR_BE);
+ }
+}
+
static void pl011_write(void *opaque, hwaddr offset,
uint64_t value, unsigned size)
{
@@ -266,6 +360,7 @@
/* XXX this blocks entire thread. Rewrite to use
* qemu_chr_fe_write and background I/O callbacks */
qemu_chr_fe_write_all(&s->chr, &ch, 1);
+ pl011_loopback_tx(s, ch);
s->int_level |= INT_TX;
pl011_update(s);
break;
@@ -295,13 +390,15 @@
int break_enable = value & LCR_BRK;
qemu_chr_fe_ioctl(&s->chr, CHR_IOCTL_SERIAL_SET_BREAK,
&break_enable);
+ pl011_loopback_break(s, break_enable);
}
s->lcr = value;
pl011_set_read_trigger(s);
break;
case 12: /* UARTCR */
- /* ??? Need to implement the enable and loopback bits. */
+ /* ??? Need to implement the enable bit. */
s->cr = value;
+ pl011_loopback_mdmctrl(s);
break;
case 13: /* UARTIFS */
s->ifl = value;
@@ -361,12 +458,21 @@
static void pl011_receive(void *opaque, const uint8_t *buf, int size)
{
+ /*
+ * In loopback mode, the RX input signal is internally disconnected
+ * from the entire receiving logics; thus, all inputs are ignored,
+ * and BREAK detection on RX input signal is also not performed.
+ */
+ if (pl011_loopback_enabled(opaque)) {
+ return;
+ }
+
pl011_put_fifo(opaque, *buf);
}
static void pl011_event(void *opaque, QEMUChrEvent event)
{
- if (event == CHR_EVENT_BREAK) {
+ if (event == CHR_EVENT_BREAK && !pl011_loopback_enabled(opaque)) {
pl011_put_fifo(opaque, DR_BE);
}
}