# Tuyaopen/onchip Uart

> On-chip UART (serial port) for TuyaOpen using the tal_uart API: init a user UART, send/receive bytes, RX interrupt callback. 串口、UART、串口收发、串口通信、tal_uart、用户串口。

- Skill: `tuya/tuyaopen-onchip-uart` (Agent Skill)
- Install (CLI): `npx skillmds@latest add tuya/tuyaopen-onchip-uart`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tuya/tuyaopen-onchip-uart/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: tuya (https://skillmd.com/u/tuya)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tuya/tuyaopen-onchip-uart

---


# TuyaOpen On-Chip UART (user serial)

## ⛔ STOP — for a "serial / 串口" request, confirm WHICH serial before ANY code

When the user says anything like "用串口发 X" / "send X over serial" / "发给电脑" /
"用 USB 串口" without explicitly stating which kind, you **MUST ask and WAIT** before
writing any code or choosing an API. **Do NOT infer** — "发给电脑" / "USB 串口" do
**not** imply the log console.

Ask these two fixed options and wait for the user's choice — **prefer an interactive
multiple-choice prompt if your tooling supports one; otherwise list them as text**:

> 你要用哪种串口?
> 1. **调试日志口**（`PR_*`；T5AI 上就是 USB 转串口、电脑直接能收;零配置)
> 2. **独立用户串口**（`tal_uart`；独立 UART 实例 + 自定义引脚)

- Picked **1** → just `PR_NOTICE(...)`; no UART init, nothing recorded. Done.
- Picked **2** → continue with this skill (confirm instance + pins, record `onchip:uart<N>`).

❌ BAD: user says "用串口发 hello world 给电脑" → you write `PR_DEBUG("hello world")` without asking.
✅ GOOD: you ask the two options, wait for the answer, then write code only for the chosen path.

## RED LINE — user UART vs log console (do not confuse)

| | API | What it is |
|---|-----|------------|
| **User serial port** | `tal_uart_init` / `tal_uart_write` / `tal_uart_read` | A real UART peripheral you open on chosen pins. **This skill.** |
| **Debug / log output** | `PR_DEBUG` / `PR_INFO` / `PR_NOTICE` / `PR_ERR` (tal_log) | The logging console. NOT a user peripheral — do not model it as one. |

- **Which serial?** → see the **⛔ STOP** above: ask the two fixed options and wait;
  never assume the log console. `PR_*` = log console (not a peripheral, nothing recorded);
  `tal_uart` = dedicated user UART (this skill; confirm instance + pins, record `onchip:uart<N>`).
- **Pick the UART from `platform.json` — do NOT guess roles.** Read
  `.tuyaopen/ide/platform.json` → `peripherals.uart.spec.ports[]`. Each port carries:
  - `role` — its **power-on default purpose**: `"log"` (debug/log console),
    `"download"` (flash/download port), or `"general"` (free for app use). This is the
    default and is **reconfigurable in code**, but treat it as the guide.
  - `pinGroups` — its available TX/RX pin set(s).

  For a user UART, **prefer a `role:"general"` port**. **Never** use the log port
  (`role:"log"`). The `download` port is fine — its use is only
  transient during flashing, so it's free at runtime. Confirm the chosen instance with
  the user; don't infer roles from general board knowledge.

## On-chip — no Kconfig, no TDD registration

- On-chip peripherals are provided by the platform. **Do NOT** write `CONFIG_ENABLE_UART` (or any `CONFIG_ENABLE_SPI/I2C/GPIO`) — those are platform-selected. No `app_default.config` change is needed.
- There is no `board_register_hardware()` / TDD step for a user UART. You call `tal_uart_init()` directly.

## Step 1 — Confirm the instance and pins

A user UART is confirmed by **which UART instance** + **which TX/RX pins**.

**Whether you need pin mux is decided by the port's `pinGroups`** (from platform.json):

- Port with a **single** `pinGroups` entry → those are its fixed default pins.
  **Do NOT call `tkl_io_pinmux_config`** — just `tal_uart_init()` and use it. The vast
  majority of cases (and the SDK default `TUYA_UART_NUM_0`) are this. Don't mux "just in case".
- Port with **multiple** `pinGroups` → you must pick one; selecting a group needs mux.
  Only then `#include "tkl_pinmux.h"`. E.g. T5AI `UART2` exposes PIN31/30 **or** PIN41/40:

```c
/* ALTERNATE-PINS ONLY — for a multi-group port; omit entirely for single-group ports */
#include "tkl_pinmux.h"
tkl_io_pinmux_config(TUYA_IO_PIN_40, TUYA_UART2_RX);
tkl_io_pinmux_config(TUYA_IO_PIN_41, TUYA_UART2_TX);
```

If the chosen port has one pin group, **skip the block above and `tkl_pinmux.h` entirely**.

Record the confirmed UART in `.tuyaopen/used-peripherals.json` as `onchip:uart<N>`
before generating code — see the `hardware-vibe-coding` skill.

## Step 2 — Init + send/receive

Default-pin case (the common one) — **no pin mux, no `tkl_pinmux.h`**:

```c
#include "tal_api.h"

#define USR_UART_NUM   TUYA_UART_NUM_0   /* the UART the user confirmed; default pins → no mux */

void app_uart_init(void)
{
    TAL_UART_CFG_T cfg = {0};
    cfg.base_cfg.baudrate = 115200;
    cfg.base_cfg.databits = TUYA_UART_DATA_LEN_8BIT;
    cfg.base_cfg.stopbits = TUYA_UART_STOP_LEN_1BIT;
    cfg.base_cfg.parity   = TUYA_UART_PARITY_TYPE_NONE;
    cfg.rx_buffer_size    = 256;
    cfg.open_mode         = O_BLOCK;
    if (tal_uart_init(USR_UART_NUM, &cfg) != OPRT_OK) {
        PR_ERR("uart init failed");      /* PR_* = log console, fine to use */
        return;
    }

    const char *msg = "hello world\r\n";
    tal_uart_write(USR_UART_NUM, (const uint8_t *)msg, strlen(msg));
}

/* Polled receive + echo */
static void uart_rx_loop(void)
{
    uint8_t buf[256];
    for (;;) {
        int n = tal_uart_read(USR_UART_NUM, buf, sizeof(buf));
        if (n <= 0) { tal_system_sleep(10); continue; }
        tal_uart_write(USR_UART_NUM, buf, n);
    }
}
```

RX-by-interrupt instead of polling:

```c
static void uart_rx_cb(TUYA_UART_NUM_E port, void *buff, uint16_t len) { /* ... */ }
tal_uart_rx_reg_irq_cb(USR_UART_NUM, uart_rx_cb);
```

## API Reference

| Function | Description |
|----------|-------------|
| `tal_uart_init(port, &cfg)` | Init UART; `cfg.base_cfg` = baudrate/databits/stopbits/parity, `cfg.open_mode` (`O_BLOCK`), `cfg.rx_buffer_size` |
| `tal_uart_write(port, buf, len)` | Send bytes; returns write size or <0 |
| `tal_uart_read(port, buf, len)` | Read bytes; returns read size (>=0) or <0 |
| `tal_uart_get_rx_data_size(port)` | Bytes available in RX buffer |
| `tal_uart_rx_reg_irq_cb(port, cb)` | Register RX interrupt callback |
| `tal_uart_deinit(port)` | Release the UART |

`open_mode` flags: `O_BLOCK`, `O_ASYNC_WRITE`, `O_FLOW_CTRL`, `O_TX_DMA`, `O_RX_DMA`.

## Troubleshooting

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| Log output garbled / lost after init | Opened the log UART (`TUYA_UART_NUM_0`) as a user UART | Use a different instance; never reuse the console port |
| No data on non-default pins | Alternate pin group not muxed | `tkl_io_pinmux_config(PIN, TUYA_UARTn_TX/RX)` before `tal_uart_init` (default pins need no mux) |
| `tal_uart_read` always returns 0 | Polling with nothing received, or wrong baud | Check wiring/baud; add `tal_system_sleep` in poll loop |

## Reference example

SDK: `examples/peripherals/uart/` (UART init, write, polled read/echo, UART2 pin mux).

