📝 9 Sep 2022
PinePhone Hacking with Pinebook Pro and BLÅHAJ
UPDATE: PinePhone is now officially supported by Apache NuttX RTOS (See this)
Last week we spoke about creating our own Operating System for Pine64 PinePhone…
Our PinePhone OS will be awfully quiet until we implement UART Input and Output. (For the Serial Debug Console)
Today we’ll learn about the UART Controller for the Allwinner A64 SoC inside PinePhone…
Transmit and receive UART Data the Polling Way
Also the Interrupt-Driven Way
Enabling UART Interrupts
Handling UART Interrupts
And how we implemented PinePhone’s UART Driver for Apache NuttX RTOS.
Let’s dive into our NuttX Porting Journal and learn how we made PinePhone chatty over UART…
Allwinner A64 UART Controller Registers
Our operating system will print some output on PinePhone’s Serial Debug Console as it runs. (And receive input too)
To do that, we’ll talk to the UART Controller on the Allwinner A64 SoC…
Flip the A64 User Manual to page 562 (“UART”) and we’ll see the UART Registers. (Pic above)
PinePhone’s Serial Console is connected to UART0 at Base Address 0x01C2
8000
Which we define like so: a64_memorymap.h
/* A64 UART0 Base Address */
#define A64_UART0_ADDR 0x1C28000
/* A64 UART0 IRQ */
#define A64_UART0_IRQ 32
(A64_UART0_IRQ
is defined here)
(We’ll talk about A64_UART0_IRQ
in a while)
Check that PinePhone is connected to our computer with the USB Serial Debug Cable (pic above) at 115.2 kbps…
Let’s read and write UART Data the easier (inefficient) way, via Polling…
A64 UART Receive and Transmit Registers UART_RBR and UART_THR
Page 563 of the Allwinner A64 User Manual tells us the UART Registers for reading and writing UART Data (pic above)…
Receiver Buffer Register (RBR)
(At Offset 0x00
)
Transmit Holding Register (THR)
(Also at Offset 0x00
)
Let’s write some UART Data…
The Transmit Holding Register (THR) is at address 0x01C2
8000
. (Since Offset is 0)
We’ll write our output data to 0x01C2
8000
, byte by byte, and the data will appear in the Serial Console: a64_serial.c
// Send one byte to PinePhone Allwinner A64 UART
static void a64_uart_send(struct uart_dev_s *dev, int ch)
{
// Write to UART Transmit Holding Register (UART_THR)
// Offset: 0x0000
uint8_t *uart_thr = (uint8_t *)
(A64_UART0_ADDR + 0x0);
// Bits 7 to 0: Transmit Holding Register (THR)
// Data to be transmitted on the serial output port . Data should only be
// written to the THR when the THR Empty (THRE) bit (UART_LSR[5]) is set.
// If in FIFO mode and FIFOs are enabled (UART_FCR[0] = 1) and THRE is set,
// 16 number of characters of data may be written to the THR before the
// FIFO is full. Any attempt to write data when the FIFO is full results in the
// write data being lost.
*uart_thr = ch;
}
So this code…
a64_uart_send(NULL, 'H');
a64_uart_send(NULL, 'E');
a64_uart_send(NULL, 'Y');
Will print this to PinePhone’s Serial Console…
HEY
Will this work if we send a huge chunk of text?
Nope, we’ll overflow the Transmit FIFO Buffer!
The pic below shows what happens if we print too much… The overflow characters will get dropped. (Hence the solitary “f
”)
To fix this, we wait for the UART Port to be ready before we transmit. We’ll see how in the next section.
What’s uart_dev_s
?
That’s the convention that NuttX RTOS expects for UART Drivers.
We may drop the parameter if we’re not on NuttX.
Why we wait for the UART Port before we transmit
Let’s check if the UART Port is ready to accept output data for transmission.
We read Bit 5 of the Line Status Register (UART_LSR) at Offset 0x14
: a64_serial.c
// Return true if Transmit FIFO is not full for PinePhone Allwinner A64 UART
static bool a64_uart_txready(struct uart_dev_s *dev)
{
// Read from UART Line Status Register (UART_LSR)
// Offset: 0x0014
const uint8_t *uart_lsr = (const uint8_t *)
(A64_UART0_ADDR + 0x14);
// Bit 5: TX Holding Register Empty (THRE)
// If the FIFOs are disabled, this bit is set to "1" whenever the TX Holding
// Register is empty and ready to accept new data and it is cleared when the
// CPU writes to the TX Holding Register.
// If the FIFOs are enabled, this bit is set to "1" whenever the TX FIFO is
// empty and it is cleared when at least one byte is written
// to the TX FIFO.
return (*uart_lsr & 0b100000) != 0; // Transmit FIFO is ready if THRE=1 (Bit 5)
}
Now we can print to the Serial Console without dropping characters…
// Wait for UART Port to be ready
while (!a64_uart_txready(NULL)) {}
// Send one byte of data
a64_uart_send(NULL, 'A');
Busy Wait in an Empty Loop? That’s wasteful ain’t it?
Yes we’re wasting CPU Cycles waiting for UART.
That’s why NuttX and other Operating Systems will insist that we implement UART with Interrupts (instead of Polling).
We’ll cover this in a while.
Also note that PinePhone’s UART Port has a Transmit FIFO Buffer of 16 characters.
Our UART Driver doesn’t check for the available space in the Transmit FIFO Buffer.
For efficiency, we should probably fix this: a64_serial.c
// Return true if Transmit FIFO is empty for PinePhone Allwinner A64 UART
static bool a64_uart_txempty(struct uart_dev_s *dev)
{
// Transmit FIFO is empty if Transmit FIFO is not full (for now)
return a64_uart_txready(dev);
}
Moving on from UART Transmit to Receive…
A64 UART Registers UART_RBR and UART_THR
Now that PinePhone can talk to us, let’s make sure we can talk back!
Anything that we type into PinePhone’s Serial Console will appear in the Receiver Buffer Register (RBR), byte by byte.
The Receiver Buffer Register is at address 0x01C2
8000
. (Since Offset is 0). This how we read it: a64_serial.c
// Receive data from PinePhone Allwinner A64 UART
static int a64_uart_receive(struct uart_dev_s *dev, unsigned int *status)
{
// Status is always OK
*status = 0;
// Read from UART Receiver Buffer Register (UART_RBR)
// Offset: 0x0000
const uint8_t *uart_rbr = (const uint8_t *)
(A64_UART0_ADDR + 0x00);
// Bits 7 to 0: Receiver Buffer Register (RBR)
// Data byte received on the serial input port . The data in this register is
// valid only if the Data Ready (DR) bit in the UART Line Status Register
// (UART_LCR) is set.
//
// If in FIFO mode and FIFOs are enabled (UART_FCR[0] set to one), this
// register accesses the head of the receive FIFO. If the receive FIFO is full
// and this register is not read before the next data character arrives, then
// the data already in the FIFO is preserved, but any incoming data are lost
// and an overrun error occurs.
return *uart_rbr;
}
(We may drop the dev
and status
parameters if we’re not on NuttX)
But don’t read the UART Input yet! We need to wait for the UART Input to be available…
Let’s check if there’s UART Input ready to be read from the UART Port.
We read Bit 0 of the Line Status Register (UART_LSR) at Offset 0x14
: a64_serial.c
// Return true if Receive FIFO is not empty for PinePhone Allwinner A64 UART
static bool a64_uart_rxavailable(struct uart_dev_s *dev)
{
// Read from UART Line Status Register (UART_LSR)
// Offset: 0x0014
const uint8_t *uart_lsr = (const uint8_t *)
(A64_UART0_ADDR + 0x14);
// Bit 0: Data Ready (DR)
// This is used to indicate that the receiver contains at least one character in
// the RBR or the receiver FIFO.
// 0: no data ready
// 1: data ready
// This bit is cleared when the RBR is read in non-FIFO mode, or when the
// receiver FIFO is empty, in FIFO mode.
return (*uart_lsr) & 1; // DR=1 if data is ready
}
Now we’re ready to read UART Input…
// Wait for UART Input to be ready
while (!a64_uart_rxavailable(NULL)) {}
// Read one byte of data
int status;
int ch = a64_uart_receive(NULL, &status);
Again… This looks like a waste of CPU Cycles?
Indeed, UART Input won’t work well on multitasking operating systems unless we do it with Interrupts. (Coming up in a sec!)
Is it safe to do UART Output when our PinePhone OS is booting?
Yep we may call a64_uart_send
and a64_uart_txready
when our OS is booting.
For Arm64 Assembly we have something similar: This Arm64 Assembly Macro is super helpful for printing debug messages in our Arm64 Startup Code…
Don’t we need to set the Baud Rate for the UART Port?
Right now we don’t initialise the UART Port because U-Boot has kindly done it for us. (At 115.2 kbps)
We’ll come back to this in a while.
Earlier we saw UART with Polling, and how inefficient it can get. Now we talk about UART with Interrupts and how we…
Attach a UART Interrupt Handler
Enable UART Interrupts
Handle UART Interrupts
Does NuttX use UART Polling or Interrupts?
NuttX uses both Polling-based UART and Interrupt-driven UART. NuttX OS writes System Logs (syslog
) the UART Polling way…
sinfo("This is printed on UART with Polling\n");
And NuttX Apps print App Messages the UART Interrupt Way…
printf("This is printed on UART with Interrupts\n");
So if we don’t see any App Messages in NuttX, check that the UART Interrupts are OK.
Shared Peripheral Interrupts for Allwinner A64’s Generic Interrupt Controller
PinePhone’s UART Controller will trigger an Interrupt for Transmit and Receive Events when…
Transmit Buffer becomes empty
Received Data becomes available
The Allwinner A64 User Manual (page 211, “GIC”) reveals that UART0 Interrupts will be triggered at Interrupt Number 32. (Pic above)
Let’s attach our Interrupt Handler to handle the UART Interrupts: a64_serial.c
// UART0 IRQ Number for PinePhone Allwinner A64 UART
#define UART_IRQ 32
// Attach Interrupt Handler for PinePhone Allwinner A64 UART
static int a64_uart_attach(struct uart_dev_s *dev)
{
// Attach UART Interrupt Handler
int ret = irq_attach(
UART_IRQ, // Interrupt Number
a64_uart_irq_handler, // Interrupt Handler
dev // NuttX Device
);
// Set Interrupt Priority in
// Generic Interrupt Controller version 2
arm64_gic_irq_set_priority(
UART_IRQ, // Interrupt Number
0, // Interrupt Flags
IRQ_TYPE_LEVEL // Trigger Interrupt on High
);
// Enable UART Interrupt
if (ret == OK) {
up_enable_irq(UART_IRQ);
} else {
sinfo("error ret=%d\n", ret);
}
return ret;
}
a64_uart_irq_handler is our UART Interrupt Handler, we’ll explain in a while.
What’s irq_attach?
// Attach UART Interrupt Handler
int ret = irq_attach(
UART_IRQ, // Interrupt Number
a64_uart_irq_handler, // Interrupt Handler
dev // NuttX Device
);
On NuttX, we call irq_attach to attach an Interrupt Handler to the UART Controller.
What’s arm64_gic_irq_set_priority?
// Set Interrupt Priority in
// Generic Interrupt Controller version 2
arm64_gic_irq_set_priority(
UART_IRQ, // Interrupt Number
0, // Interrupt Flags
IRQ_TYPE_LEVEL // Trigger Interrupt on High
);
Arm64 Interrupts are managed on PinePhone by the Generic Interrupt Controller in Allwinner A64…
The code above calls the Generic Interrupt Controller to set the priority of the UART Interrupt.
Later when we’re done with UART Interrupts, we should detach the Interrupt Handler: a64_serial.c
// Detach Interrupt Handler for PinePhone Allwinner A64 UART
static void a64_uart_detach(struct uart_dev_s *dev)
{
// Disable UART Interrupt
up_disable_irq(UART_IRQ);
// Detach UART Interrupt Handler
irq_detach(UART_IRQ);
}
A64 UART Interrupt Enable Register UART_IER
UART Interupts won’t happen until we enable UART Interrupts.
Page 565 of the Allwinner A64 User Manual tells us the UART Register for enabling UART Interrupts (pic above)…
Interrupt Enable Register (UART_IER)
(At Offset 0x04
)
This is how we enable (or disable) UART Receive Interrupts: a64_serial.c
// Enable or disable Receive Interrupts for PinePhone Allwinner A64 UART
static void a64_uart_rxint(struct uart_dev_s *dev, bool enable)
{
// Write to UART Interrupt Enable Register (UART_IER)
// Offset: 0x0004
uint8_t *uart_ier = (uint8_t *)
(A64_UART0_ADDR + 0x04);
// Bit 0: Enable Received Data Available Interrupt (ERBFI)
// This is used to enable/disable the generation of Received Data Available Interrupt and the Character Timeout Interrupt (if in FIFO mode and FIFOs enabled). These are the second highest priority interrupts.
// 0: Disable
// 1: Enable
if (enable) { *uart_ier |= 0b00000001; }
else { *uart_ier &= 0b11111110; }
}
And this is how we enable (or disable) UART Transmit Interrupts: a64_serial.c
// Enable or disable Transmit Interrupts for PinePhone Allwinner A64 UART
static void a64_uart_txint(struct uart_dev_s *dev, bool enable)
{
// Write to UART Interrupt Enable Register (UART_IER)
// Offset: 0x0004
uint8_t *uart_ier = (uint8_t *)
(A64_UART0_ADDR + 0x04);
// Bit 1: Enable Transmit Holding Register Empty Interrupt (ETBEI)
// This is used to enable/disable the generation of Transmitter Holding Register Empty Interrupt. This is the third highest priority interrupt.
// 0: Disable
// 1: Enable
if (enable) { *uart_ier |= 0b00000010; }
else { *uart_ier &= 0b11111101; }
}
Earlier we’ve attached a64_uart_irq_handler
as our Interrupt Handler for UART Interrupts…
// Attach UART Interrupt Handler
int ret = irq_attach(
UART_IRQ, // Interrupt Number
a64_uart_irq_handler, // Interrupt Handler
dev // NuttX Device
);
Let’s look inside the Interrupt Handler.
When UART triggers an Interrupt, it stores the cause of the Interrupt in the Interrupt Identity Register (UART_IIR), Offset 0x08
.
Bits 0 to 3 of the Interrupt Identity Register are…
Binary 0010
if the Transmit Holding Register is empty
(Hence we should transmit more data)
Binary 0100
if there’s Receive Data available
(Hence we should read the data received)
This is how we handle these conditions in our Interrupt Handler: a64_serial.c
// Interrupt Handler for PinePhone Allwinner A64 UART
static int a64_uart_irq_handler(int irq, void *context, void *arg)
{
// Get the UART Device
struct uart_dev_s *dev = (struct uart_dev_s *)arg;
UNUSED(irq);
UNUSED(context);
DEBUGASSERT(dev != NULL && dev->priv != NULL);
// Read UART Interrupt Identity Register (UART_IIR)
// Offset: 0x0008
const uint8_t *uart_iir = (const uint8_t *) (A64_UART0_ADDR + 0x08);
// Bits 3 to 0: Interrupt ID
// This indicates the highest priority pending interrupt which can be one of the following types:
// 0000: modem status
// 0001: no interrupt pending
// 0010: THR empty
// 0100: received data available
// 0110: receiver line status
// 0111: busy detect
// 1100: character timeout
// Bit 3 indicates an interrupt can only occur when the FIFOs are enabled and used to distinguish a Character Timeout condition interrupt.
uint8_t int_id = (*uart_iir) & 0b1111;
// 0100: If received data is available...
if (int_id == 0b0100) {
// Receive the data
uart_recvchars(dev);
// 0010: If THR is empty (Transmit Holding Register)...
} else if (int_id == 0b0010) {
// Transmit the data
uart_xmitchars(dev);
}
return OK;
}
Let’s talk about uart_recvchars
and uart_xmitchars
…
What’s uart_xmitchars
?
// 0010: If THR is empty (Transmit Holding Register)...
if (int_id == 0b0010) {
// Transmit the data
uart_xmitchars(dev);
If the Transmit Holding Register is empty, our Interrupt Handler calls uart_xmitchars
to transmit more data.
uart_xmitchars
is a NuttX System Function that calls a64_uart_send
to transmit data to UART, while buffering the UART Output Data.
(We’ve seen a64_uart_send
earlier)
uart_xmitchars
will also call a64_uart_txready
to check if the UART Port is ready to accept more data, before transmitting the data.
Now for the other direction…
What’s uart_recvchars
?
// 0100: If received data is available...
if (int_id == 0b0100) {
// Receive the data
uart_recvchars(dev);
If Received Data is available, our Interrupt Handler calls uart_recvchars
to read the Received Data.
uart_recvchars
is a NuttX System Function that calls a64_uart_receive
to receive data from UART, while buffering the UART Input Data.
(We’ve seen a64_uart_receive
earlier)
uart_recvchars
will also call a64_uart_rxavailable
to check if Received Data is actually available, before reading the data.
And that’s how we transmit and receive UART Data with Interrupts!
Did we forget something?
Rightfully we should initialise the UART Baud Rate: a64_serial.c
// Setup PinePhone Allwinner A64 UART
static int a64_uart_setup(struct uart_dev_s *dev)
{
// TODO: Set the Baud Rate
return 0;
}
PinePhone’s U-Boot Bootloader has kindly set the Baud Rate for us (115.2 kbps), so we skip this for now. More about the bootloader…
Later when need to set the UART Baud Rate for other UART Ports, the steps are explained here…
What about UART Shutdown?
To shutdown a UART Port, we disable the interrupts: a64_serial.c
// Shutdown PinePhone Allwinner A64 UART
static void a64_uart_shutdown(struct uart_dev_s *dev)
{
// Disable the Receive and Transmit Interrupts
a64_uart_rxint(dev, false);
a64_uart_txint(dev, false);
}
The Shutdown Function will never be called for UART0 because it’s always active as the Serial Console.
But for UART1 to UART4, the Shutdown Function will be called when our NuttX App closes the UART.
Anything else?
One last thing: For NuttX we need to implement a simple I/O Control Handler ioctl
: a64_serial.c
// I/O Control for PinePhone Allwinner A64 UART
static int a64_uart_ioctl(struct file *filep, int cmd, unsigned long arg)
{
int ret = OK;
switch (cmd)
{
case TIOCSBRK: /* BSD compatibility: Turn break on, unconditionally */
case TIOCCBRK: /* BSD compatibility: Turn break off, unconditionally */
default:
{
ret = -ENOTTY;
break;
}
}
return ret;
}
We’re almost done with our PinePhone UART Driver for NuttX!
How do we create a PinePhone UART Driver for NuttX?
We’ve implemented all the UART Operations for our PinePhone UART Driver…
a64_uart_setup
: Initialise UART Driver
a64_uart_shutdown
: Shutdown UART Driver
a64_uart_attach
: Attach Interrupt Handler
a64_uart_detach
: Detach Interrupt Handler
a64_uart_ioctl
: I/O Control
a64_uart_receive
: Receive Data
a64_uart_rxint
: Enable / Disable Receive Interrupt
a64_uart_rxavailable
: Is Received Data Available
a64_uart_send
: Transmit Data
a64_uart_txint
: Enable / Disable Transmit Interrupt
a64_uart_txready
: Is UART Ready to Transmit
a64_uart_txempty
: Is Transmit Buffer Empty
NuttX expects us to wrap the UART Operations into a uart_ops_s
Struct like so: a64_serial.c
// Serial driver UART operations for PinePhone Allwinner A64 UART
static const struct uart_ops_s g_uart_ops =
{
.setup = a64_uart_setup,
.shutdown = a64_uart_shutdown,
.attach = a64_uart_attach,
.detach = a64_uart_detach,
.ioctl = a64_uart_ioctl,
.receive = a64_uart_receive,
.rxint = a64_uart_rxint,
.rxavailable = a64_uart_rxavailable,
#ifdef CONFIG_SERIAL_IFLOWCONTROL
.rxflowcontrol = NULL,
#endif
.send = a64_uart_send,
.txint = a64_uart_txint,
.txready = a64_uart_txready,
.txempty = a64_uart_txempty,
};
We should start our UART Driver like this: a64_serial.c
// UART0 is console and ttyS0
#define CONSOLE_DEV g_uart0port
#define TTYS0_DEV g_uart0port
// Performs the low level UART initialization early in
// debug so that the serial console will be available
// during bootup. This must be called before arm_serialinit.
void a64_earlyserialinit(void)
{
// NOTE: This function assumes that low level hardware configuration
// -- including all clocking and pin configuration -- was performed
// earlier by U-Boot Bootloader.
// Enable the console UART. The other UARTs will be initialized if and
// when they are first opened.
CONSOLE_DEV.isconsole = true;
a64_uart_setup(&CONSOLE_DEV);
// Omitted: Init UART1 to UART4, if required
(g_uart0port
contains the UART Operations g_uart_ops
)
Then we expose UART0 as /dev/console
and /dev/ttyS0
: a64_serial.c
// Register serial console and serial ports. This assumes
// that imx_earlyserialinit was called previously.
void arm64_serialinit(void)
{
// Register UART0 as /dev/console
int ret = uart_register("/dev/console", &CONSOLE_DEV);
if (ret < 0) { _err("Register /dev/console failed, ret=%d\n", ret); }
// Register UART0 as /dev/ttyS0
ret = uart_register("/dev/ttyS0", &TTYS0_DEV);
if (ret < 0) { _err("Register /dev/ttyS0 failed, ret=%d\n", ret); }
// Omitted: Register UART1 to UART4 as /dev/ttyS1 to /dev/ttyS4
// TTY Numbering is always Sequential:
// If UART1 is disabled, then UART2 becomes /dev/ttyS1
And we’re done with our PinePhone UART Driver for NuttX!
Let’s watch our UART Driver in action!
Follow these steps to build NuttX and copy to Jumpdrive microSD…
Insert the microSD into PinePhone and power it on. We should see…
Starting kernel ...
HELLO NUTTX ON PINEPHONE!
- Ready to Boot CPU
- Boot from EL2
- Boot from EL1
- Boot to C runtime for OS Initialize
nx_start: Entry
up_allocate_heap: heap_start=0x0x400c4000, heap_size=0x7f3c000
arm64_gic_initialize: TODO: Init GIC for PinePhone
arm64_gic_initialize: CONFIG_GICD_BASE=0x1c81000
arm64_gic_initialize: CONFIG_GICR_BASE=0x1c82000
arm64_gic_initialize: GIC Version is 2
up_timer_initialize: up_timer_initialize: cp15 timer(s) running at 24.00MHz, cycle 24000
up_timer_initialize: _vector_table=0x400a7000
up_timer_initialize: Before writing: vbar_el1=0x40227000
up_timer_initialize: After writing: vbar_el1=0x400a7000
uart_register: Registering /dev/console
uart_register: Registering /dev/ttyS0
work_start_highpri: Starting high-priority kernel worker thread(s)
nx_start_application: Starting init thread
lib_cxx_initialize: _sinit: 0x400a7000 _einit: 0x400a7000 _stext: 0x40080000 _etext: 0x400a8000
nsh: sysinit: fopen failed: 2
nshn:x _msktfaarttf:s :C PcUo0m:m aBnedg innonti nfgo uInddle L oNouptt
Shell (NSH) NuttX-10.3.0-RC2
nsh>
(Yeah the output is slightly garbled, here’s the workaround)
Now that we handle UART Interrupts, NuttX Shell works perfectly OK on PinePhone…
nsh> uname -a
NuttX 10.3.0-RC2 fc909c6-dirty Sep 1 2022 17:05:44 arm64 pinephone
nsh> help
help usage: help [-v] [<cmd>]
. cd dmesg help mount rmdir true xd
[ cp echo hexdump mv set truncate
? cmp exec kill printf sleep uname
basename dirname exit ls ps source umount
break dd false mkdir pwd test unset
cat df free mkrd rm time usleep
Builtin Apps:
getprime hello nsh ostest sh
nsh> hello
task_spawn: name=hello entry=0x4009b1a0 file_actions=0x400c9580 attr=0x400c9588 argv=0x400c96d0
spawn_execattrs: Setting policy=2 priority=100 for pid=3
Hello, World!!
nsh> ls /dev
/dev:
console
null
ram0
ram2
ttyS0
zero
What about other UART Ports? (Besides UART0)
We’re adding support for other UART Ports, like UART3 for PinePhone’s 4G LTE Modem…
The changes have been upstreamed to NuttX Mainline…
How do we enable a UART Port? Like UART3?
Head over to the NuttX Build Configuration…
make menuconfig
Then select…
To configure the Baud Rate, Bits, Parity and Stop Bits…
(The default Baud Rate / Bits / Parity / Stop Bits work OK with the PinePhone LTE Modem on UART3: 115.2 kbps / 8 Bits / No Parity / 1 Stop Bit)
Today we talked about PinePhone UART and how we created the NuttX UART Driver.
There’s plenty to be done for NuttX on PinePhone, please lemme know if you would like to join me 🙏
Please check out the other articles on NuttX for PinePhone…
Many Thanks to my GitHub Sponsors for supporting my work! This article wouldn’t have been possible without your support.
Got a question, comment or suggestion? Create an Issue or submit a Pull Request here…
lupyuen.github.io/src/serial.md
Which Allwinner A64 UART Ports are used in PinePhone?
According to the PinePhone Schematic, the following UART Ports in Allwinner A64 are connected…
UART0: Serial Console
Pins PB8 (TX) and PB9 (RX)
(Assigned as /dev/ttyS0)
UART1: Bluetooth Module (Realtek RTL8723CS)
Pins PG6 (TX), PG7 (RX), PG8 (RTS) and PG9 (CTS)
(TODO: Assign as /dev/ttyS1)
UART2: Unused
Pins PB0 and PB1
(Wired to Light Sensor STK3311 and Compass Sensor AK09911)
UART3: 4G LTE Modem (Quectel EG25-G)
Pins PD0 (TX) and PD1 (RX)
(TODO: Assign as /dev/ttyS3)
UART4: 4G LTE Modem (Quectel EG25-G)
Pins PD4 and PD5
(Wired to RTS and CTS, not really a UART)
TODO: Disable UART2 and /dev/ttyS2 so that UART3 maps neatly to /dev/ttyS3. (See this)