使用杜邦线在30pin上进行连接
youyeetooRJ开发板一共提供5个uart供客户使用,其中 uart2 作为 Debug 调试口,其余分别注册为ttyS3、ttyS4、ttyS8、ttyS9
对应硬件位置请查看原理图

本章节以UART1为示例,用户可参考来使用其他串口。
# 切换管理员用户
su
# 查看设备UART节点
ls /dev/ttyS*
# 查看串口信息
stty -F /dev/ttyS3
# 设置串口波特率为115200
stty -F /dev/ttyS3 speed 115200
# 设置串口八位数据位 无校验 一位停止位 无回显
stty -F /dev/ttyS3 cs8 -parenb -cstopb -echo
# 后台接收数据
cat /dev/ttyS3 &
# 前台执行发送
echo -e "1234567890\n" > /dev/ttyS3
首先按照创建APP章节创建好APP,这里贴出关键代码供客户验证,编译好的APP会提供。
资料下载,点击跳转

package com.youyeetoo.uart
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.youyeetoo.uart.ui.theme.UartTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
private val helper = SerialPortHelper()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
UartTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
SerialScreen(
modifier = Modifier.padding(innerPadding),
helper = helper
)
}
}
}
}
override fun onDestroy() {
super.onDestroy()
helper.release()
}
}
@Composable
fun SerialScreen(modifier: Modifier = Modifier, helper: SerialPortHelper) {
val uiScope = rememberCoroutineScope()
var path by remember { mutableStateOf("/dev/ttyS3") }
var baud by remember { mutableStateOf("115200") }
var dataBits by remember { mutableStateOf("8") }
var stopBits by remember { mutableStateOf("1") }
var parity by remember { mutableStateOf(Parity.NONE) }
var asHex by remember { mutableStateOf(false) }
var enableRx by remember { mutableStateOf(true) }
var sendText by remember { mutableStateOf("") }
var status by remember { mutableStateOf("未打开") }
var opened by remember { mutableStateOf(false) }
val txLogs = remember { mutableStateListOf<String>() }
val rxLogs = remember { mutableStateListOf<String>() }
val scroll = rememberScrollState()
val txScroll = rememberScrollState()
val rxScroll = rememberScrollState()
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
.verticalScroll(scroll),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = path,
onValueChange = { path = it },
label = { Text("设备路径 (例: /dev/ttyS3)") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
NumberField("波特率", baud, Modifier.weight(1f)) { baud = it }
NumberField("数据位", dataBits, Modifier.weight(1f)) { dataBits = it }
NumberField("停止位", stopBits, Modifier.weight(1f)) { stopBits = it }
}
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
ParitySelector(parity) { parity = it }
Text("当前: $parity")
}
}
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Button(onClick = {
runCatching {
val config = SerialConfig(
path = path,
baud = baud.toInt(),
dataBits = dataBits.toInt(),
stopBits = stopBits.toInt(),
parity = parity
)
helper.open(config) { recv ->
if (enableRx) {
uiScope.launch(Dispatchers.Main) {
rxLogs.add(recv)
}
}
}
opened = true
status = "已打开 ${config.path} @${config.baud}"
}.onFailure { status = "打开失败: ${it.message}" }
}) { Text("打开") }
Button(onClick = {
helper.close()
opened = false
status = "已关闭"
}) { Text("关闭") }
IconButton(onClick = {
txLogs.clear()
rxLogs.clear()
}) {
Icon(Icons.Default.Refresh, contentDescription = "清空")
}
}
// 发送区
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = sendText,
onValueChange = { sendText = it },
label = { Text(if (asHex) "发送内容 (HEX 空格可选)" else "发送内容") },
singleLine = false,
modifier = Modifier.fillMaxWidth()
)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Checkbox(checked = asHex, onCheckedChange = { asHex = it })
Text("HEX 发送")
Checkbox(checked = enableRx, onCheckedChange = { enableRx = it })
Text("启用接收")
Button(
onClick = {
runCatching { helper.sendText(sendText, asHex) }
.onSuccess { txLogs.add(sendText) }
.onFailure { status = "发送失败: ${it.message}" }
},
enabled = opened
) { Text("发送") }
}
}
Text("状态: $status")
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Column(
modifier = Modifier
.weight(1f)
.height(240.dp)
.verticalScroll(txScroll)
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text("发送 (TX)")
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { txLogs.clear() }) { Text("清空发送") }
}
txLogs.forEach { Text("TX: $it") }
}
Column(
modifier = Modifier
.weight(1f)
.height(240.dp)
.verticalScroll(rxScroll)
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text("接收 (RX)")
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { rxLogs.clear() }) { Text("清空接收") }
}
rxLogs.forEach { Text("RX: $it") }
}
}
}
}
@Composable
private fun NumberField(label: String, value: String, modifier: Modifier = Modifier, onValueChange: (String) -> Unit) {
OutlinedTextField(
value = value,
onValueChange = { onValueChange(it.filter { ch -> ch.isDigit() }) },
label = { Text(label) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = modifier
)
}
@Composable
private fun ParitySelector(selected: Parity, onChange: (Parity) -> Unit) {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Parity.values().forEach { p ->
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = selected == p,
onCheckedChange = { onChange(p) }
)
Text(p.name)
}
}
}
}
package com.youyeetoo.uart
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.Locale
data class SerialConfig(
val path: String,
val baud: Int,
val dataBits: Int,
val stopBits: Int,
val parity: Parity
)
enum class Parity { NONE, ODD, EVEN }
class SerialPortHelper(
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
) {
private var input: FileInputStream? = null
private var output: FileOutputStream? = null
private var readJob: Job? = null
fun isOpen(): Boolean = input != null && output != null
@Synchronized
fun open(config: SerialConfig, onData: (String) -> Unit): Boolean {
close()
val dev = File(config.path)
if (!dev.exists()) throw IOException("设备不存在: ${config.path}")
runCatching { configureWithStty(config) }.onFailure {
}
return try {
input = FileInputStream(dev)
output = FileOutputStream(dev)
startReadLoop(onData)
true
} catch (e: Exception) {
close()
throw IOException("打开串口失败: ${e.message}", e)
}
}
private fun startReadLoop(onData: (String) -> Unit) {
val ins = input ?: return
readJob = scope.launch {
val buf = ByteArray(2048)
while (isActive) {
val n = runCatching { ins.read(buf) }.getOrElse { -1 }
if (n <= 0) break
val chunk = buf.copyOfRange(0, n)
onData(decodeBytes(chunk))
}
}
}
fun sendText(text: String, asHex: Boolean = false) {
val out = output ?: throw IOException("串口未打开")
val bytes = if (asHex) hexToBytes(text) else text.toByteArray()
out.write(bytes)
out.flush()
}
@Synchronized
fun close() {
readJob?.cancel()
readJob = null
runCatching { input?.close() }
runCatching { output?.close() }
input = null
output = null
}
fun release() {
close()
scope.cancel()
}
private fun configureWithStty(config: SerialConfig) {
val parityArgs = when (config.parity) {
Parity.NONE -> listOf("-parenb")
Parity.ODD -> listOf("parenb", "parodd")
Parity.EVEN -> listOf("parenb", "-parodd")
}
val stopArgs = if (config.stopBits == 2) listOf("cstopb") else listOf("-cstopb")
val dataBits = "cs${config.dataBits}"
val cmd = listOf(
"stty",
"-F", config.path,
"raw",
"-echo", "-echoe", "-echok",
"-icanon",
"-crtscts",
"-ixon", "-ixoff",
config.baud.toString(),
dataBits
) + parityArgs + stopArgs
ProcessBuilder(cmd)
.redirectErrorStream(true)
.start()
.apply { waitFor() }
}
private fun decodeBytes(bytes: ByteArray): String {
return runCatching { String(bytes) }.getOrElse {
bytes.joinToString(" ") { b -> "%02X".format(Locale.US, b) }
}
}
private fun hexToBytes(text: String): ByteArray {
val clean = text.replace("\\s".toRegex(), "")
require(clean.length % 2 == 0) { "HEX 长度需为偶数" }
return clean.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
}