把 shell 脚本里的「后台接收」-- 以 UART/CAN 双总线监听为例
#!/bin/bash
set -euo pipefail
shopt -s lastpipe
############################
# 1. 固定配置
############################
UART_DEV=(/dev/ttyAS{1..4})
UART_BAUD=(9600 115200 57600 921600)
UART_TX_LINE=(
"UART1_LOOP_9600"
"UART2_LOOP_115200"
"UART3_LOOP_57600"
"UART4_LOOP_921600"
)
CAN_BUS=(can0 can1)
CAN_FRAME=(
"12345678#AABBCCDDEEFF0011"
"87654321#FFEEDDCCBBAA9988"
)
############################
# 2. 清理函数
############################
cleanup() {
echo; echo ">>> 收到 exit,清理后台任务 ..."
jobs -p | xargs -r kill -9 2>/dev/null || true
exit 0
}
trap cleanup INT TERM EXIT
############################
# 3. UART 初始化
############################
init_uart() {
for i in {0..3}; do
stty -F "${UART_DEV[$i]}" "${UART_BAUD[$i]}" raw -echo \
ignbrk -icrnl -ixon -opost -isig -icanon
done
}
############################
# 4. 后台:接收
############################
# UART 接收
for i in {0..3}; do
{
while :; do
read -r -t1 line < "${UART_DEV[$i]}" && echo "[tty$((i+1))] RX $line"
done
} &
done
# CAN 接收
for bus in "${CAN_BUS[@]}"; do
{ candump -L "$bus" | while read -r f; do echo "[$bus] RX $f"; done; } &
done
############################
# 5. 主循环:定时发送
############################
main_loop() {
init_uart
while :; do
# UART 循环发送
for i in {0..3}; do
printf '%s\r\n' "${UART_TX_LINE[$i]}" > "${UART_DEV[$i]}"
echo "[tty$((i+1))] TX ${UART_TX_LINE[$i]}"
done
# CAN 扩展帧循环发送
for i in {0..1}; do
cansend "${CAN_BUS[$i]}" "${CAN_FRAME[$i]}"
echo "[${CAN_BUS[$i]}] TX ${CAN_FRAME[$i]}"
done
sleep 2
done
}
############################
# 6. 唯一交互:等待 exit
############################
echo ">>> UART/CAN 已启动,输入 exit 退出 <<<"
main_loop &
read -p "" cmd
[[ "$cmd" == "exit" ]] && cleanup
- 背景说明
最近写了一个全功能硬件自检脚本,需要 同时 做三件事: - 周期性 发送 数据到 UART + CAN + GPIO;
- 实时 接收 UART/CAN 的回环数据并打印;
- 用户敲
exit时瞬间 干净退出。
后台接收部分虽然只有几行,却最容易踩坑。本文把 UART 和 CAN 的接收逻辑逐行拆透,保证你下次拷代码时知道“为什么非得这么写”。
- 先给完整上下文(省流版)
# 1) 串口设备
UART_DEV=(/dev/ttyAS{1..4})
# 2) CAN 接口
CAN_BUS=(can0 can1)
- UART 接收——以
/dev/ttyAS1为例
{
while :; do
if read -r -t1 line < "${UART_DEV[0]}"; then
echo "[tty1] RX $line"
fi
done
} &
行号 代码 作用 坑点提示
① { ... } & 整段逻辑丢进 子 shell 后台任务,主脚本继续往下跑。 不加 {} 的话 & 只会把最后一条命令后台化。
② while : 死循环,保持“永远在线”。 如果写 while read,一旦串口没数据就直接退出,监听就断了。
③ read -r -t1 line < "${UART_DEV[0]}" -t1:最多等 1 秒,无数据立刻返回非 0;-r:禁用反斜杠转义;<:重定向文件描述符。 去掉 -t1 会导致 read 永远阻塞,脚本退出时杀不掉。
④ if ... then echo 只有真正读到内容才打印,避免空行刷屏。 不加判断会把空行也打出来。
⑤ echo "[tty1] RX $line" 统一前缀,方便肉眼 grep。 如果设备发 \r\n,$line 末尾会带 \r,可 line=${line%$'\r'} 去掉。
- CAN 接收——以
can0为例
{ candump -L can0 | while read -r frame; do echo "[can0] RX $frame"; done; } &
行号 代码 作用 坑点提示
① candump -L can0 -L 单行日志格式:timestamp id#data。 不加 -L 会多行输出,不好 read。
② while read -r frame 从管道里逐行读。 在子 shell 里,while 循环结束后 candump 也会被杀,干净。
③ echo "[can0] RX $frame" 加前缀,和 UART 日志对齐。 —
④ 整体再包一次 { ... } & 整个 candump+while 一起后台化。 如果不包,管道左右可能分到不同进程组,杀不干净。
- 为什么“子 shell +
&”是最佳实践?
- 并发:主脚本初始化/主循环不会被阻塞。
- 隔离:子 shell 的变量、重定向、工作目录与父脚本完全隔离。
- 易杀:
jobs -p | xargs -r kill -9
一条命令即可杀掉 所有 子 shell,避免残留 candump 进程。
- 生命周期时序图
启动脚本
│
├─ fork 子 shell 1 → UART 接收 (ttyAS1)
├─ fork 子 shell 2 → UART 接收 (ttyAS2)
├─ fork 子 shell 3 → UART 接收 (ttyAS3)
├─ fork 子 shell 4 → UART 接收 (ttyAS4)
├─ fork 子 shell 5 → candump + while (can0)
├─ fork 子 shell 6 → candump + while (can1)
│
└─ 父进程进入 main_loop,做 UART/CAN/GPIO 发送
↑
用户敲 exit → cleanup() → kill -9 所有子 shell
- FAQ 速查
问题 解决
串口读出来末尾带 ^M line=${line%$'\r'}
串口偶尔丢字节 在 stty 里加 raw -echo 关闭回显/规范模式
candump 日志太长刷屏 加 grep -v " 0000000" 过滤空帧
退出时 candump 僵死 确认整段逻辑用 {} 包起来,再 &
- 一行总结
把 接收逻辑 写成{ while read ... } &的三明治结构,
就能在 shell 里低成本实现 多总线并发监听 + 一键退出,
UART、CAN、SPI、I2C 统统适用。
更多推荐



所有评论(0)