22 嵌入式操作系统 | 串口命令行调试(socat / stty / printf / hexdump)

嵌入式操作系统 | 串口命令行调试(socat / stty / printf / hexdump)

本课程开源地址(Gitee) :https://gitee.com/fujianxinxi/qianrushixitongyingyongkaifa.git

课件、示例代码与验收脚本都在该仓库,可直接 git clone 或下载 ZIP 使用。
模块六 · 2 学时 · 环境:VMware + Ubuntu 20.04

一句话概述:一行 C 代码都不写,全部用命令行把串口调通:用 socat 造一对虚拟串口,用 stty 查看/设置串口参数,用 printf 发数据、hexdump 收数据,再用 exec 3<> 双工打开串口,手工完成一次 Modbus RTU 读写并用 crc_check.py 核对 CRC 与温度;最后故意制造故障,练排查能力。

本节目标

  1. 能说出串口参数(波特率 / 数据位 / 校验位 / 停止位),会用 stty 查看和设置它们,并知道每条对应程序里的哪个选项
  2. 会用 socat + printf + hexdump 完成"串口能发能收"的验证,会用 exec 3<> 在同一个 fd 上收发
  3. 会手工发一帧 Modbus RTU 请求、看懂 7 字节响应,并用 crc_check.py 核对 CRC、换算温度;出现异常能按现象查原因

本节课的意义 :串口出问题(收到乱码、收不到数据、数据被抢走)时,第一反应应该是拿几条命令去验证物理链路和参数,而不是先改程序。这些命令在现场调试、工控设备对接时天天用。

一、讲解内容

1.1 知识讲解

串口是什么。 设备之间最常用的一种连接方式:两边按同样的"约定"收发字节。这个约定就是四个参数,常写成 9600-8-N-1:

参数 取值 含义
波特率 9600 / 115200 ... 每秒传多少位,两边必须一致,不一致就是乱码
数据位 8 一个字符 8 位
校验位 N(None) 不做奇偶校验(Modbus RTU 就是 8-N-1 或 8-E-1)
停止位 1 一帧结束的标志

串口在 Linux 里就是一个"设备文件"。 真串口叫 /dev/ttyS0(主板串口)、/dev/ttyUSB0(USB 转串口),打开、读写就是普通文件操作。虚拟机里没有真串口怎么办------用 socat 造一对虚拟串口:

text 复制代码
socat -d -d pty,raw,echo=0,link=/tmp/ttyV0 pty,raw,echo=0,link=/tmp/ttyV1
              ▲ 一端给我们调                     ▲ 另一端给对端设备

raw = 不做任何字符加工(二进制协议必备),echo=0 = 关掉内核回显。对我们来说 /tmp/ttyV0 就是串口:写进去的数据会从 /tmp/ttyV1 出来,反之亦然。

stty 是 termios 的命令行前端------这是本课最重要的一张对照表(下节课写 C 程序时,代码里那几行就是这张表):

命令行(本课用) 程序里(下节课用) 作用
stty -F /dev/xxx -a tcgetattr() 看当前所有参数
stty -F /dev/xxx 9600 cfsetispeed/cfsetospeed 设波特率
stty -F /dev/xxx cs8 `c_cflag = CS8`
stty -F /dev/xxx -cstopb c_cflag &= ~CSTOPB 1 位停止位
stty -F /dev/xxx -parenb c_cflag &= ~PARENB 无校验
stty -F /dev/xxx raw -echo cfmakeraw() 原始模式 + 关回显

-F 后面跟设备名,意思就是"对这个设备操作",不是对当前终端。不加 -F 改的是你自己的终端,一定要小心。

为什么要 raw + -echo。 串口默认是"熟模式(cooked)":内核会做行编辑(按回车才交数据)、把 \r 变成 \n、把 Ctrl+C 当信号、并且把你写出去的数据回显回来。这些对二进制协议全是灾难。所以:

  • 上课用的 socat 已经带了 raw,echo=0,所以你 stty -a 看到的已经是 -icanon -echo;
  • 真串口默认是 icanon echo ,所以程序里必须写 cfmakeraw();命令行里必须写 stty ... raw -echo;
  • 反过来,你也可以故意 stty -F /tmp/ttyV1 echo 把回显打开------马上就能看到"自己发出去的数据又回到了自己手里"(下面第 9 步有实验,这是排查时最常见的坑)。

命令行收发的三件套。

目的 命令 说明
发数据 printf 'abc\r\n' > /tmp/ttyV0 用 printf,不要用 echo :echo 对 \x、\r 的处理各系统不一致,printf 可靠
收数据 `timeout 5 head -c 7 /tmp/ttyV1 hexdump -C`
兜底 timeout 5 ... 收不到数据时不会永远卡住,5 秒自动结束------排查时永远加上 timeout
双工 exec 3<>/tmp/ttyV0 之后 >&3 发、<&3 收 同一个 fd 才能"我发完你回我",两个重定向会打开两次设备。注意 :bash 打开设备不带 O_NOCTTY ,在没有控制终端的环境(脚本 / setsid / 服务)里会把串口抢成你的控制终端,读回来是 0 字节------所以这几条命令要在真实终端里敲(见 1.3 最后两行)

Modbus RTU 帧结构(工业上最常见的串口协议,我们读一个保持寄存器):

位置 请求帧 01 03 00 00 00 01 84 0A 响应帧 01 03 02 00 FA 38 07
1 01 从机地址 01 从机地址
2 03 功能码(读保持寄存器) 03 功能码
3 00 起始地址高字节 02 后面的数据有几个字节
4 00 起始地址低字节 00 数据高字节
5 00 要读几个(高字节) FA 数据低字节(0x00FA = 250)
6 01 要读几个(低字节) 38 CRC 低字节
7 84 CRC 低字节 07 CRC 高字节
8 0A CRC 高字节 ---

CRC 怎么算:用现成的小工具 (本课提供 crc_check.py,CRC16/MODBUS:初值 0xFFFF、反射多项式 0xA001,低字节在前):

shell 复制代码
python3 crc_check.py --crc 01 03 00 00 00 01      # 算请求帧的 CRC → 84 0A
python3 crc_check.py 01 03 02 00 FA 38 07         # 校验响应帧 → 通过,温度 25.0 C

手工发帧时先用它把请求帧的 CRC 算出来,就不用背 84 0A 了;收到响应也用它核对。

读上来的数据怎么变温度。 寄存器里是整数,本例约定"放大 10 倍":读到 250 就是 25.0 C。真实设备的换算规则看它的说明书。

1.2 操作步骤(一条命令一步,看现象)

第 1 步:造一对虚拟串口(终端 A,保持运行)

shell 复制代码
sudo apt install -y socat          # 课时 15 装过就不用再装
socat -d -d pty,raw,echo=0,link=/tmp/ttyV0 pty,raw,echo=0,link=/tmp/ttyV1

现象(打印两个 PTY 编号并建好软链接,然后停在这里不退出,这是正常的):

text 复制代码
socat[3553] N PTY is /dev/pts/4
socat[3553] N PTY is /dev/pts/5
socat[3553] N starting data transfer loop with FDs [5,5] and [7,7]

第 2 步:确认"串口"真的在(终端 B)

shell 复制代码
ls -l /tmp/ttyV0 /tmp/ttyV1

现象(软链接指向 socat 造出来的 pts 设备;这一步是排查的第一条命令:文件不在,就是 socat 没开):

text 复制代码
lrwxrwxrwx 1 root root 10 /tmp/ttyV0 -> /dev/pts/4
lrwxrwxrwx 1 root root 10 /tmp/ttyV1 -> /dev/pts/5

第 3 步:用 stty 看当前参数(不改任何东西,只看)

shell 复制代码
stty -F /tmp/ttyV0 -a

现象(关键几行,本环境实测):

text 复制代码
speed 38400 baud; rows 0; columns 0; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>;
...
-parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts
...
-isig -icanon iexten -echo echoe echok -echonl -noflsh -xcase -tostop -echoprt

怎么读这份输出:

  • speed 38400 baud:当前"波特率"(socat 默认给的,说明默认值不一定是我们想要的)
  • cs8 -cstopb -parenb:8 位数据、1 位停止位、无校验 ------ 已经对了
  • -icanon -echo:不是熟模式、不回显 ------ 这是 socat 的 raw,echo=0 帮我们设好的

第 4 步:用 stty 把参数设成 9600-8-N-1

shell 复制代码
stty -F /tmp/ttyV0 9600 cs8 -cstopb -parenb raw -echo
stty -F /tmp/ttyV0 -a | head -1

现象(第一行变成 9600,其余不变):

text 复制代码
speed 9600 baud; rows 0; columns 0; line = 0;

第 5 步:第一次收发验证------字符串能不能过去

终端 B 先"听"(收 7 个字节就结束,最多等 5 秒):

shell 复制代码
timeout 5 head -c 7 /tmp/ttyV1 | hexdump -C

终端 C 再"说":

shell 复制代码
printf 'hello\r\n' > /tmp/ttyV0

现象(终端 B 打出 7 个字节,右边还能看出 hello 和回车换行 0d 0a)------能到这里就说明串口通了:

text 复制代码
00000000  68 65 6c 6c 6f 0d 0a                           |hello..|
00000007

第 6 步:把参数改回去/改成别的,看会不会乱码(重要结论)

shell 复制代码
stty -F /tmp/ttyV1 115200          # 故意让两端的"波特率"不一样
timeout 5 head -c 7 /tmp/ttyV1 | hexdump -C   # 终端 B 继续听
printf 'hello\r\n' > /tmp/ttyV0               # 终端 C 再发

现象与结论(本课必须讲清楚这一点 ):在 socat 造的虚拟串口上数据照样能收到,不会乱码------因为虚拟串口的背后是内存,根本不关心波特率。真串口上两边波特率不一致才会出现乱码/丢字节。所以:

  • 用虚拟串口练的是命令和排查手法;
  • 真设备的波特率不一致,只能靠"两边参数对一遍"来解决 (这就是第 3 步那条 stty -a 的用途)。

第 7 步:命令行发二进制帧(Modbus 请求帧)

shell 复制代码
printf '\x01\x03\x00\x00\x00\x01\x84\x0a' > /tmp/ttyV0

终端 B 用 timeout 3 head -c 8 /tmp/ttyV1 | hexdump -C 听,现象:

text 复制代码
00000000  01 03 00 00 00 01 84 0a                           |........|
00000008

说明命令行完全可以收二进制,\x01 这种写法由 printf 直接送出原始字节(hexdump -C 会把不可打印字节显示成点)。

第 8 步:启动 Modbus 从机(终端 B,必须先开)

从机脚本用课程资料里的 modbus_slave.py(Python 标准库实现,模拟一个温度传感器,每答一次温度升 0.5):

shell 复制代码
python3 -u modbus_slave.py /tmp/ttyV1

现象:

text 复制代码
Modbus 从机已打开 /tmp/ttyV1,等待主机请求...(Ctrl+C 退出)

为什么加 -u:不加的话 Python 输出会攒在缓冲区里,重定向到文件或在后台跑时看不到打印,容易误以为程序没反应。

第 9 步:在同一 fd 上"发一帧、收一帧"(本课的高潮)

shell 复制代码
exec 3<>/tmp/ttyV0                                  # 读写方式打开串口,挂到 fd 3
printf '\x01\x03\x00\x00\x00\x01\x84\x0a' >&3       # 从 fd 3 发出请求帧
timeout 2 head -c 7 <&3 | hexdump -C                # 从同一个 fd 3 读 7 字节响应
exec 3<&-                                           # 用完关掉

现象(实测:从机回的正是温度 250):

text 复制代码
00000000  01 03 02 00 fa 38 07                           |.....8.|
00000007

终端 B 的从机也同步打印了一来一回:

text 复制代码
收到请求:01 03 00 00 00 01 84 0A
发送响应:01 03 02 00 FA 38 07

这一步有两个坑(老师现场实测出来的,记下来):

  1. 这几条命令要在真实终端里敲。 bash 的 exec 3<> 打开设备不带 O_NOCTTY :如果当前 shell 没有控制终端(脚本、setsid/nohup 起的脚本、systemd 服务里),虚拟串口会被它"认领"成控制终端,于是 head -c 7 <&3 立刻读到 0 字节(EOF) ------而同一时刻从机日志明明写着 发送响应。这也正是下一课 C 程序里一定写 open(dev, O_RDWR | O_NOCTTY) 的原因。
  2. 如果确实要在没有控制终端的场合做这件事,用带 O_NOCTTY 的 Python 一行命令代替(printf/head/cat/stty 这些命令自己打开设备时都带 O_NOCTTY,所以只有 shell 的 exec 3<> 有这个坑):
shell 复制代码
python3 -u -c "
import os, time
fd = os.open('/tmp/ttyV0', os.O_RDWR | os.O_NOCTTY)
os.write(fd, bytes.fromhex('010300000001840a'))
time.sleep(0.5)
print(' '.join('%02X' % b for b in os.read(fd, 7)))
os.close(fd)
"

现象(实测):输出 01 03 02 00 FA 38 07。

第 10 步:用 crc_check.py 核对 CRC、把寄存器值换算成温度

shell 复制代码
python3 crc_check.py 01 03 02 00 FA 38 07

现象(实测):

text 复制代码
输入字节:01 03 02 00 FA 38 07
CRC 校验通过(0x0738)
功能码 03、从机 01:寄存器值 = 250,温度 = 25.0 C

故意改坏一个字节,看工具能不能发现(排查时用它判断"帧是不是被改过/错位"):

shell 复制代码
python3 crc_check.py 01 03 02 00 FA 38 08
text 复制代码
输入字节:01 03 02 00 FA 38 08
CRC 校验失败:算出 0x0738,帧里是 0x0838(字节错位或数据被改动)

第 11 步:故意制造 4 种故障,练排查(每个都做一遍,记现象)

故障 怎么做 现象 结论
对端不在 终端 B 按 Ctrl+C 停掉从机,再执行第 9 步 timeout 等满 2 秒,什么都没输出 "收不到"先查对端是否在跑
回显没关 stty -F /tmp/ttyV1 echo,再从 fd 3 发 printf 'abc\r' >&3,然后 `timeout 2 head -c 5 <&3 hexdump -C` 收到 61 62 63 5e 4d(即 abc^M)------自己的数据被弹回来了
残留数据污染 先发 4 个字节(printf 'abc\r' > /tmp/ttyV0)再启动从机、跑第 9 步 从机打印 收到请求:61 62 63 0D 01 03 00 00 / CRC 不对,丢弃,主机一直收不到响应 帧错位多半是上一次实验的残留字节混进来了:重开 socat,或先把缓冲读空
一端两个人用 两个终端同时 cat /tmp/ttyV0,再发数据 两个 cat 都能打开(虚拟串口不报 busy),但数据被其中一个抢走,另一个永远等不到 串口是独占资源,约定一端只给一个程序用

第 12 步:收工。 终端 A 按 Ctrl+C 停掉 socat,然后 ls -l /tmp/ttyV0 会报"没有那个文件"------虚拟串口随 socat 一起消失。

1.3 常见错误与排查

排查顺序(背下来,现场调试就按这个顺序走):

text 复制代码
① 设备在不在      ls -l /tmp/ttyV0            → 不在:socat 没开
② 参数对不对      stty -F /tmp/ttyV0 -a       → 两端对一遍(真设备关键)
③ 能不能发        printf 'hello\r\n' > /dev/xxx
④ 能不能收        timeout 5 head -c 7 /dev/yyy | hexdump -C
⑤ 字节对不对      hexdump -C 看原始字节,别只看"有没有数据"
⑥ 帧对不对        python3 crc_check.py <收到的字节>
现象 原因 解决
No such file or directory: /tmp/ttyV0 socat 没启动 / 已退出 先做第 1 步,ls -l /tmp/ttyV0 确认软链接在
命令一直卡着不回 用 cat 收数据、对端又没发 永远加 timeout :`timeout 5 head -c N /dev/xxx
收到 5E 4D(^M)或收到自己发的原样数据 对端回显 开着(echo 没关) stty -F /dev/xxx raw -echo;程序里 cfmakeraw()
收到乱码 / 字节数不对 两端参数不一致(尤其真串口波特率) 两边都执行 stty -a 对一遍;确认 9600-8-N-1
从机一直说 CRC 不对,主机收不到回复 帧错位:上次实验的残留字节混进来了 重开 socat;或先把缓冲读空(timeout 1 cat /dev/xxx > /dev/null)
数据"时有时无" 同一端被两个程序同时打开,数据被抢走 一端只给一个程序用;fuser -v /tmp/ttyV0(或 lsof /tmp/ttyV0)看谁占着
printf 发出去对方收到一堆怪东西 用 echo 发的,转义没生效 改用 printf,\x0A 这种十六进制转义只有 printf 可靠
后台跑的 Python 从机"没反应" print 被缓冲,输出没刷出来 用 python3 -u modbus_slave.py ...(-u 关缓冲)
stty 一改,自己的终端乱了 忘了 -F,改的是当前终端 一定写 -F 设备名;真乱了敲 stty sane 恢复
第 9 步 head -c 7 <&3 读到 0 字节,但从机日志说"已发送响应" 当前 shell 没有控制终端 ,bash 的 exec 3<>(不带 O_NOCTTY)把串口抢成了控制终端 在真实终端里执行;或改用第 9 步第 2 条那个带 O_NOCTTY 的 Python 命令
程序报 tcsetattr: Interrupted system call 系统调用被信号打断(errno == EINTR) 标准做法:errno == EINTR 时重试 (课时 23 的 serial_open()、write_frame() 就是这么写的)

二、学生练习

本节命令速查表:

命令 作用 示例
socat -d -d pty,raw,echo=0,link=/tmp/ttyV0 pty,raw,echo=0,link=/tmp/ttyV1 造一对虚拟串口(终端 A) 见第 1 步
ls -l /tmp/ttyV0 /tmp/ttyV1 确认"串口"在 ---
stty -F 设备 -a 看串口参数 stty -F /tmp/ttyV0 -a
stty -F 设备 9600 cs8 -cstopb -parenb raw -echo 设成 9600-8-N-1 ---
printf '...' > 设备 发数据(支持 \x01 写二进制) printf 'hello\r\n' > /tmp/ttyV0
`timeout 5 head -c N 设备 hexdump -C` 收 N 字节并按十六进制看
exec 3<>设备 / >&3 / <&3 同一 fd 双工收发 见第 9 步
python3 crc_check.py 01 03 02 00 FA 38 07 校验 CRC、换算温度 ---
python3 -u modbus_slave.py /tmp/ttyV1 启动 Modbus 从机(对端) ---
编号 任务 提示 预期结果
L22.1 造虚拟串口并确认 第 1、2 步 ls -l 看到两个软链接
L22.2 看参数、把两端都设成 9600-8-N-1 第 3、4 步 stty -a 里 speed 9600、cs8 -cstopb -parenb
L22.3 用命令发一句 hello 并另一端收 第 5 步 hexdump 出现 68 65 6c 6c 6f 0d 0a
L22.4 手工发 Modbus 请求帧并收 7 字节响应 第 9 步(别忘了 exec 3<>) 收到 01 03 02 00 FA 38 07
L22.5 用工具核对 CRC 和温度 第 10 步 "CRC 校验通过,温度 25.0 C"
L22.6 故意改坏一个字节再校验 第 10 步 "CRC 校验失败:算出 0x0738,帧里是..."
L22.7 复现"回显没关"的现象 第 11 步第 2 行 收到 61 62 63 5e 4d(abc^M)
L22.8 复现"残留数据导致帧错位" 先发 abc 再从机启动 从机打印 CRC 不对,丢弃

三、作业

  • 必做:完成 L22.3、L22.4、L22.5,提交三张截图(stty -a 的输出 / 手工收发的 hexdump / crc_check.py 的校验结果),并回答:① raw 和 -echo 分别解决什么问题?② 请求帧 01 03 00 00 00 01 84 0A 里每个字节是什么意思?③ Modbus 的 CRC 低字节在前还是高字节在前?④ 为什么虚拟串口两边"波特率不一致"也不乱码?那真串口会怎样?⑤ 第 9 步为什么必须"在真实终端里敲"?O_NOCTTY 起什么作用?
  • 选做:① 不查工具,自己用 printf 手工把"读 2 个寄存器"的请求帧组出来(提示:功能码 03、起始地址 0x0000、数量 0x0002,用 crc_check.py --crc 算 CRC,然后看从机会回什么);② 把"排查顺序"那张六步写成一个小抄贴在自己笔记本上,下节课用
  • 提交:截图 + 文字回答,命名 课时22-作业截图.png

四、本节小结与下节预告

本节一行 C 代码都没写,却把串口这套东西摸清了:socat 造虚拟串口、stty 看/设参数(和 termios 一一对应)、printf/hexdump/timeout 收发数据、exec 3<> 双工打开、手工完成一次 Modbus 读写并用工具核对 CRC。更重要的是那条六步排查顺序------以后串口调不通,先按它走一遍。

下节课把这些命令写成一个真正的程序:用 uloop 打开串口,定时 发 Modbus 请求,事件驱动 收响应和做超时保护------顺便把"一问一答、sleep 干等"的阻塞写法改成不阻塞的写法。


附录:怎么运行 + 完整代码

本课不用写代码,附录 B、C 的两个 Python 脚本是"对端工具+校验工具",直接用课程的现成文件即可。行尾请用 LF。

附录 A|怎么运行(三个终端,注意先后顺序)

shell 复制代码
# ===== 终端 A:造一对虚拟串口(保持运行,别关)=====
sudo apt install -y socat
socat -d -d pty,raw,echo=0,link=/tmp/ttyV0 pty,raw,echo=0,link=/tmp/ttyV1

# ===== 终端 B:对端(先开!)=====
ls -l /tmp/ttyV0 /tmp/ttyV1                      # ① 设备在不在
stty -F /tmp/ttyV0 -a                            # ② 参数对不对(看 speed/cs8/-parenb)
cd ~/lab/课时22-串口命令行调试
python3 -u modbus_slave.py /tmp/ttyV1            # ④ 启动从机,停在这里等请求

# ===== 终端 C:主机侧,全部用命令 =====
stty -F /tmp/ttyV0 9600 cs8 -cstopb -parenb raw -echo   # ③ 设成 9600-8-N-1
printf 'hello\r\n' > /tmp/ttyV0                          # ③ 能不能发(终端 B 可加一条命令收)
python3 crc_check.py --crc 01 03 00 00 00 01             # ⑥ 先算出请求帧的 CRC
exec 3<>/tmp/ttyV0                                       # 读写打开串口,挂到 fd 3
printf '\x01\x03\x00\x00\x00\x01\x84\x0a' >&3            # 发请求帧(8 字节)
timeout 2 head -c 7 <&3 | hexdump -C                     # 收响应帧(7 字节)
exec 3<&-                                                # 关掉 fd 3
python3 crc_check.py 01 03 02 00 FA 38 07                # ⑥ 核对 CRC + 换算温度

为什么"对端先开":串口不像 TCP 有连接握手,收不到数据时无法区分"对端没开"还是"帧不对"。先把对端跑起来,出错就只剩"帧不对"一种可能------这就是排查的思路。

附录 B|文件 crc_check.py(命令行算 CRC / 校验帧的小工具)

python 复制代码
#!/usr/bin/env python3
"""课时 22 的"命令行小工具":算 Modbus CRC、校验响应帧、换算温度

用法一(算请求帧的 CRC):
    python3 crc_check.py --crc 01 03 00 00 00 01
    -> CRC = 0x0A84,帧 = 01 03 00 00 00 01 84 0A

用法二(校验收到的 7 字节响应帧,并算温度):
    python3 crc_check.py 01 03 02 00 FA 38 07
    -> CRC 校验通过,温度 = 25.0 C

也支持从管道读(十六进制,空格分隔):
    echo "01 03 02 00 FA 38 07" | python3 crc_check.py
"""
import sys


def crc16_modbus(data: bytes) -> int:
    """CRC16/MODBUS:初值 0xFFFF、反射多项式 0xA001"""
    crc = 0xFFFF
    for b in data:
        crc ^= b
        for _ in range(8):
            crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
    return crc


def parse_bytes(args) -> bytes:
    """把命令行参数或标准输入里的十六进制字节攒成 bytes"""
    if not args:
        args = sys.stdin.read().replace(",", " ").split()
    out = bytearray()
    for a in args:
        a = a.strip()
        if a.lower().startswith("0x"):
            a = a[2:]
        if not a:
            continue
        if len(a) > 2:                      # 允许 "01030200FA3807" 这种连写
            for i in range(0, len(a) - 1, 2):
                out.append(int(a[i:i + 2], 16))
        else:
            out.append(int(a, 16))
    return bytes(out)


def main() -> int:
    args = sys.argv[1:]
    only_crc = False
    if args and args[0] in ("--crc", "-c"):
        only_crc = True
        args = args[1:]

    data = parse_bytes(args)
    if not data:
        print(__doc__)
        return 1

    print("输入字节:" + " ".join(f"{b:02X}" for b in data))

    if only_crc:
        c = crc16_modbus(data)
        frame = data + bytes([c & 0xFF, c >> 8])       # Modbus:低字节在前
        print(f"CRC = 0x{c:04X}(低字节在前:{c & 0xFF:02X} {c >> 8:02X})")
        print("完整帧:" + " ".join(f"{b:02X}" for b in frame))
        return 0

    # 校验整个帧:前 n-2 字节算 CRC,和最后两字节比
    if len(data) < 4:
        print("帧太短(至少 4 字节),没法校验")
        return 1
    body = data[:-2]
    got = data[-2] | (data[-1] << 8)
    c = crc16_modbus(body)
    if c != got:
        print(f"CRC 校验失败:算出 0x{c:04X},帧里是 0x{got:04X}(字节错位或数据被改动)")
        return 2
    print(f"CRC 校验通过(0x{got:04X})")

    if data[1] == 0x03 and data[2] == 2:                # 读保持寄存器、2 个字节数据
        raw = (data[3] << 8) | data[4]
        print(f"功能码 03、从机 {data[0]:02X}:寄存器值 = {raw},温度 = {raw / 10.0:.1f} C")
    else:
        print(f"从机 {data[0]:02X}、功能码 {data[1]:02X}、数据 " +
              " ".join(f"{b:02X}" for b in data[2:-2]))
    return 0


if __name__ == "__main__":
    sys.exit(main())

附录 C|文件 modbus_slave.py(Modbus RTU 从机,模拟温度传感器)

python 复制代码
#!/usr/bin/env python3
"""课时 22 / 23 的"测试工具":Modbus RTU 从机(模拟一个温度传感器)

用法:python3 -u modbus_slave.py /tmp/ttyV1 [起始温度]
      (另一端给主机:课时 22 用命令行发帧,课时 23 用 C 程序发帧)
"""
import os
import struct
import sys


def crc16_modbus(data: bytes) -> int:
    crc = 0xFFFF
    for b in data:
        crc ^= b
        for _ in range(8):
            crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
    return crc


dev = sys.argv[1] if len(sys.argv) > 1 else "/tmp/ttyV1"
temp = float(sys.argv[2]) if len(sys.argv) > 2 else 25.0

fd = os.open(dev, os.O_RDWR | os.O_NOCTTY)
print(f"Modbus 从机已打开 {dev},等待主机请求...(Ctrl+C 退出)")

buf = b""
try:
    while True:
        data = os.read(fd, 256)
        if not data:
            continue
        buf += data
        while len(buf) >= 8:
            frame, buf = buf[:8], buf[8:]
            print("收到请求:" + " ".join(f"{b:02X}" for b in frame))

            if crc16_modbus(frame[:6]) != (frame[6] | (frame[7] << 8)):
                print("  CRC 不对,丢弃")
                continue
            if frame[1] != 0x03:
                print(f"  功能码 {frame[1]:#04x} 不支持,丢弃")
                continue

            value = int(round(temp * 10))            # 25.0 C -> 250
            resp = bytes([frame[0], 0x03, 2, (value >> 8) & 0xFF, value & 0xFF])
            resp += struct.pack("<H", crc16_modbus(resp))
            os.write(fd, resp)
            print("发送响应:" + " ".join(f"{b:02X}" for b in resp))
            temp += 0.5                              # 每答一次温度升 0.5,方便观察
except KeyboardInterrupt:
    print("\n从机退出")
finally:
    os.close(fd)

进阶(选做):把从机的 os.read 换成 select.select([fd], [], [], 1),就变成"有数据才处理"------这正是下节课 C 程序里 uloop 帮我们做的事。

相关推荐
阿钱真强道2 小时前
20 嵌入式操作系统 | ubus:把自己的程序状态暴露出去
c语言·进程通信·ubus·ubusd·blobmsg
承渊政道7 小时前
Linux系统学习【进程信号详细解析——认识、产生、保存以及捕捉信号】
linux·学习·ubuntu·ssh·vs code·进程信号
牢姐与蒯9 小时前
Linux信号(一).信号产生
linux·运维·服务器·ubuntu
zhangrelay7 天前
《机器人控制器设计与编程》课程习题资料-2026
linux·笔记·学习·ubuntu
Linux-lucky7 天前
39-41-Linux学习之旅之redis缓存基础与NFS基础
linux·运维·mysql·ubuntu
QQ14220784497 天前
2012 年的老 ThinkPad E430 改装 Ubuntu 24.04全程翻车排查实录
ubuntu·项目复盘
阿钱真强道7 天前
06 嵌入式操作系统 | vim 与软件管理:apt / 源码编译
ubuntu·vim·嵌入式·apt·源码编译
牢姐与蒯7 天前
Linux进程间通信(三).基于匿名管道的进程池的实现
linux·运维·服务器·ubuntu
Linux-lucky7 天前
32-38-Linux学习之旅之MySQL综合
linux·运维·学习·mysql·ubuntu