☰
python实现串口通信的示例代码
2026/10/27 1:55:39 网站建设 项目流程

下面这一段示例代码呢, 就是咱们用来实现串口通信的这段程序。

更新时间是2020年02月10日, 具体的时刻为10:45:33。

这篇文章主要是向大家介绍了用于实现串口通信的示例代码, 文章当中借助这些示例代码进行了非常详细的介绍内容, 这对于大家进行学习, 或者从事相关工作而言, 是具有相当重要的参考与学习价值的, 有需要的朋友们接下来就要伴随着小编的脚步一起来进行学习与实践操作了。

1 硬件设备

2 安装

在第一次安装的时候, 我们在进行包的导入操作的过程中, 发现了下载出现错误的情况。正确的应当是选择另外一个包来安装。在安装成功完成之后, 用户可以直接使用即可。

3 实现串口通信

3.第一步, 也就是发现端口这一项内容。

接下来是COM, 其中N等于1、2等等, 接下来是‘/dev/ttyS0'。作为一个初学者, 我可以给您提供以下两种方式来确定端口号。

方法一: 在终端里面输入。具体来说, 要在cmd里面输入一些内容。

python -m serial.tools.list_ports

输出结果:

COM5

1 ports found

方法二是搜索电脑里的设备管理器, 打开它之后再把烧录器插进去, 窗口就会自动弹出来。要是没有弹出, 那可能说明驱动程序没安装。即使驱动装了也弄不好, 那就重启一下电脑。大家都在干活的时候都清楚, 程序员会跟你说去重启机器或者把缓存清掉, 就是这两句话。还有可能原因也是驱动程序装错了。

方法三, 是直接去找一个具备端口扫描功能的上位机软件, 然后直接点击扫描按钮去执行就行。在大多数情况下, 当你把设备插进去之后, 这个上位机就会自动检测到你所连接设备的端口信息。

----->

注意, 当串口处于被占用的状态时, 也有可能导致失败出现, 例如你在编译器里有两个进程在运行下面的测试代码, 第二个进程就会因为端口被占用而发生失效的情况, 也有的上位机是因为同时打开了两个上位机的缘由造成的, 这在一些实验课上同学遇到过情况, 可以使用任务管理器来进行kill掉操作。

测试:

import serial

#Windows

ser = serial.Serial(port='COM5', baudrate=115200, timeout=0.5)

print(ser.name)

在控制台那里, 它所输出的结果是。

COM5

Process finished with exit code 0

就是,去写那个拿来建立一个名为ser的对象的代码部分的意思。

class PicSerial:

__ser = None # ser的单例

__isinit = False

@staticmethod

def get_available_port():

"""

检测可以使用的端口号

:return->str: 端口号的名称

"""

port = list(list_ports.comports())

if len(port) > 0:

port_name = port[0].device

print(port_name)

return port_name

# logging.info("Available port:", ports)

else:

print("There is no available port.")

# logging.error("There is no available port.")

def __new__(cls, *args, **kwargs):

if PicSerial.__ser is None:

cls.__ser = object.__new__(cls)

return cls.__ser

def __init__(self):

if not PicSerial.__isinit:

self.sername = self.get_available_port()

self.ser = serial.Serial(port=self.sername, baudrate=BAUDRATE)

PicSerial.__isinit = False

print("PicSerial init.")

3.进行数字2的操作, 即发出相关指令。

3.第2.1部分, 也就是关于那个协议的格式。

3.2.2 传送的方式

其实吧, 那个传送的办法, 是有的:

这是串行端口对象, 只传单个字节, 字符串, 字节数组加上字节数组长度。

因此直接采用数组来传递数据, 此处会碰到一个现象是列表会自动把十六进制的数值转换为整数形式。

因此, 你要实施转换的话, 能够直接把格式写成b“/x56/x00/x17/x00”, 如果你用不到去传递十进制数字的那个步骤, 也能够选择去转换成列表这种样式的东西, 直接运用函数map去对接内置的函数chr和变量x, 或者是直接运用函数map去对接内置的函数ord和那个变量x, 这也是一条可行的路子, 那么在进行读取这个操作的时候, 你也一定要格外留神一件事儿, 就是只要那个数据被放进了列表结构里面去, 系统就会把这个状态自动地转变为整型数值。

我认为这样做是非常愚蠢的, 但是我还是不得不采取这样的方式来进行书写。

#在PicSerial中

def isreply(self, cmd: bytes, option: str) -> bool:

"""

检测是否有回复

:return:回复的内容

:param cmd:

:param option:

:return: True则有回复

"""

if isinstance(cmd, bytes) and isinstance(option, str) and len(cmd) > 0 and len(option) > 0:

self.ser.write(cmd)

reply = self.ser.read(4)

reply = list(map(chr, list(reply)))

print("49h,The function'{}' is running. reply:{}".format(sys._getframe().f_code.co_name, reply))

if len(reply) >= 4 and reply[0] == 'v' and reply[1] == SERIAL_NUM and reply[2] == option and reply[3] == STATUS:

return True

return False

测试:

#在test文件中

class TestSerial(unittest.TestCase):

def test_isreply(self):

self.assertTrue(ser.isreply(GET_VERSION_CMD, VERSION))

self.assertFalse(ser.isreply('\x56\x00\x11\x00', VERSION))

self.assertFalse(ser.isreply(GET_VERSION_CMD, b'\x11'))

self.assertFalse(ser.isreply(123456, b'\x11'))

self.assertFalse(ser.isreply('', VERSION))

self.assertFalse(ser.isreply(b'', VERSION))

self.assertFalse(ser.isreply(GET_VERSION_CMD, ''))

self.assertFalse(ser.isreply(GET_VERSION_CMD, None))

self.assertFalse(ser.isreply(b'', ''))

self.assertFalse(ser.isreply(b'\x56\x00\xAA\x00', VERSION))

self.assertFalse(ser.isreply(GET_VERSION_CMD, '\xAA'))

#之后就省略不写了

if __name__ == '__main__':

unittest.main()

结果:

3.第3步, 我们来获取一下那个版本号的事情, 这个事情呢, 它其实就是hello world。

我们要根据那份协议的指引, 一步接着一部地去执行操作。

主 机 发:56 00 11 00 摄像头回:76 00 11 00 0B 56 43 30 37 30 33 20 31 2E 30 30 ( 1.00)

#在PicSerial中

def getversion(self) -> str:

"""

获取版本号

:return:

"""

cmd = GET_VERSION_CMD

option = VERSION

if self.isreply(cmd, option):

left = self.ser.readall()

print("75h,The function'{}' has responded.left{}".format(sys._getframe().f_code.co_name, left))

return self.ser.read(12).decode()[1:]

测试:

#在test文件中

def test_getversion(self):

self.assertEqual(ser.getversion(), 'VC0703 1.00')

结果的情况是这样子的, 我们通过测试。

3.4 复位

主机发出的内容包含56,紧接着后面跟着00, 再往后是26, 最后以00结尾。

摄像头进行回复, 具体的情况是数字76, 接着是数字00, 然后是数字26, 后面跟着两个连续的00。

#在PicSerial中

def reset(self):

"""

复位

:return:

"""

cmd = REST_CMD

option = RESET

if self.isreply(cmd, option):

if self.ser.read(1) == b'':

left = self.ser.readall()

print("75h,The function'{}' has responded.left{}".format(sys._getframe().f_code.co_name, left))

return True

return False

测试得到的结果和实际运行起来的最终结果是不一样的。

我花费了一些时间, 终于是把原因给找到了。在做单元测试的时候, 我一直都是去点击前面那个绿色的小箭头。我原本以为, 这样做的话, 就只是去运行当前测试函数的内容。但是呢? 我发现它竟然把其他的函数也给运行了。因此, 只有把我之前写的测试函数先注释掉。这样做之后,得到的结果就变得一样的。

测试通过。

3.5 照相

3.当前帧刷新这一步骤, 需要予以停止。

这一步操作一次就完全足够了, 因为在读取命令的时候可能会产生一些麻烦情况。

但是呢, 这个步骤是有它实际意义的, 就是在当你察觉到这张图片的尺寸非常大, 这个时候正常标准之下两个字节就能够完全表示清楚(当然这里要排除你的图片的面积属于那种特别巨大或者是特别清晰的那种极端情况), 还有另外一种可能, 就是突然之间数据源读不到东西变成空的了。

如果在这种状态下计算出来的数值非常大的话, 就可以选用这个函数来处理解决问题, 要是这个方法还不行的话, 最后就只能选择进行复位操作了。

def stoprefresh(self):

"""

停止刷新当前帧

:return:

"""

cmd = STOP_REFRESH_CMD

option = TAKE_PHOTO

self.ser.write(cmd)

if self.isreply(cmd, option) and self.ser.read(1) == b"\x00":

left = self.ser.readall()

print("87h,The function'{}' has responded.left{}".format(sys._getframe().f_code.co_name, left))

return True

return False

通过测试

def test_stoprefresh(self):

self.assertTrue(ser.stoprefresh())

3.5.2 获娶图片长度

def getlength_bytes(self) -> bytes:

"""

获取图片的长度

:return:

"""

cmd = GET_LENGTH_CMD

option_pic = '4'

self.ser.write(cmd)

if self.isreply(cmd, option_pic):

if self.ser.read(1) == b'\x04':

res = self.ser.read(4)

left = self.ser.readall()

print("103h,The function'{}' has responded.left{}".format(sys._getframe().f_code.co_name, left))

return res

return b'\x00\x00\x00\x00'

测试通过

def test_getlength(self):

self.assertEqual(ser.getlength(), b'\x00\x00\x12\x34')

3.5.3 恢复帧更新

def recover_refresh(self):

"""

恢复帧刷新

:return:

"""

cmd = RECOVER_REFRESH_CMD

option = TAKE_PHOTO

self.ser.write(cmd)

if self.isreply(cmd, option):

# 读出剩余的字节

left = self.ser.readall()

print("142h,The function'{}' has responded.left{}".format(sys._getframe().f_code.co_name, left))

return True

return False

测试并通过:

def test_recover_refresh(self):

self.assertTrue(ser.recover_refresh())

3.5.4 拍照

在这里卡了很长时间, 具体是什么原因导致长度不确定的情况, 目前还不明白, 每次读取的内容都没有被完整读完, 建议去查看一下代码。

下面代码只是演示

#下面代码只是演示不在最终版本中

def savephoto(self, cmd, option, len):

"""

保存图片

:param cmd:

:param option:

:param len: 照片的长度

:return:

"""

with open('write_pic/serialpic/photo.jpg', 'wb') as f:

if self.isreply(cmd, option):

print(self.ser.read(1))

countofread_complete_byte = 0 # 用于计算当前已经写入的长度

while countofread_complete_byte !

= len + 10:

# read()是有上限的,不可以把全部都读取

lines = self.ser.read(len + 10 - countofread_complete_byte)

countofread_complete_byte += lines.__len__()

f.write(lines)

print("142h,countofread_complete_byte:", countofread_complete_byte, "lines", lines.__len__())

left = self.ser.readall()

print("146h,少读内容:", left, "共", left.__len__(), "个字节")

res = self.ser.readall()

print(res)

现象:

当前的情况表现为运行过程持续无法停止, 必须通过手动触发stop命令来终止程序, 或者在完全没有执行stop操作的情况下, 系统会不间断地输出提示lines的内容为空。基于这一现象, 我们可以推测出read()函数的行为并不具备阻塞特性。

随着数据的接收, 图片字节的总量处于不断累积增加的态势。即便在每一次数据遍历结束时, 依据条件满足self.ser.read(len + 10 - _byte)这个规则去读取剩余的数据量后, 仍然存在未被处理完的残留内容。

我发现此时一共读出了四千零四十九个字节, 其中图片数据有四千零三十个字节, 首尾两部分加起来共有十个字节。那多出来的九个字节到底是什么呢? 火眼金睛的L发现了正确的开始位置位于上图中的红色方块处。之所以会出现这九个字节的多余数据, 是因为多出来的部分并不是当前这张图片的内容的一部分。

所以我们可以进行猜想, 程序当时没有停止运行的真实原因, 很可能是因为在上一张图片的数据还没有被完全读取完毕的时候, 我就手动停止了程序操作。这样一来,就留下了上一张尚未读完的数据内容。而在本次操作中, 系统恰好将这些之前残留、未曾处理完的数据继续读取到了。

字节串和字符串这两种数据类型,它们都是可以进行切片操作的,你可以把这个操作直接做一遍, 然后把切出来的结果给保存住。

def getphoto(self):

"""

拍照并且保存图片

:return:

"""

# self.reset()

# 1、停止帧刷新

# self.stoprefresh()

# 获取图片长度

# 返回字节长度用于整合命令,表示图片的总字节数

length = self.getlength_bytes()

# 返回整形,表示图片的总字节数

len = self.bytesToInt(length)

print("158h,len:", len)

# 拍照

cmd = GET_PHOTO_START_CMD + length + GET_PHOTO_END_CMD

print("159hcmd", cmd)

self.ser.write(cmd)

readall = self.ser.readall()

readall_len = readall.__len__()

differ = readall_len - len - 10

if differ !

= 0:

res = readall[differ + 5:-5]

print("172h:", res)

self.savephoto(res)

else:

res = readall[5:-5]

print("175h:", res)

self.savephoto(res)

# 关闭串口

self.ser.close()

# 恢复刷新

# self.recover_refresh()

成功输出结果:

为了能够方便地进行调试, 发送停帧的信号以及回复的帧数, 这些操作都是需要手动去发送的, 而接下来剩余的问题就是要把代码里面的注释给解除掉, 然后试着看看能不能成功地把这些部分组合成为一个新的函数, 结果发现有的命令读取的内容会变成空值, 因此我们可以根据这个情况做出推断, 肯定又是由于之前的那个命令里面, 还有一些数据没有被彻底读完、残留下来的字节, 才导致了读到东西被篡改的情况出现。

每一次执行完命令后看一看还有没有遗留字节,把剩余字节都取出来,然后的判断都不需要了。【读者看到的代码都是最新版本的,此处我添加了left和print到对应函数中】,处理结果打印到控制台:

def getphoto(self):

"""

拍照并且保存图片

:return:

"""

# 1、停止帧刷新

self.stoprefresh()

# 获取图片长度

# 返回字节长度用于整合命令,表示图片的总字节数

length = self.getlength_bytes()

# 返回整形,表示图片的总字节数

len = self.bytesToInt(length)

print("161h,The function'{}' is running. len:{}".format(sys._getframe().f_code.co_name, len))

# 拍照

cmd = GET_PHOTO_START_CMD + length + GET_PHOTO_END_CMD

print("165h,The function'{}' is running. cmd:{}".format(sys._getframe().f_code.co_name, cmd))

self.ser.write(cmd)

readall = self.ser.readall()

readall_len = readall.__len__()

differ = readall_len - len - 10

if differ !

= 0:

res = readall[differ + 5:-5]

print("161h,The function'{}' is running. res:{}".format(sys._getframe().f_code.co_name, res))

self.savephoto(res)

else:

res = readall[5:-5]

print("161h,The function'{}' is running. res:{}".format(sys._getframe().f_code.co_name, res))

self.savephoto(res)

# 恢复刷新

self.recover_refresh()

最终输出的图片结果是那个样子, 因为拍摄的对象是导线, 并且没有对焦成功。

# 这个测试应该怎么写? 有图片就输出并且可以打开就可以了惹?有人能教教我?

def test_getphoto(self):

pass

4 仔细想了想自己犯错的数量, 发现真的挺多的。

关于我觉得这个地方可能存在错误这件事, 我认为这个值到底具体是什么, 需要多设置断点去观察清楚才行。

在那一段用于演示的代码内部, 因为相关的协议明确写明是五个字节,所以我这边已经读取了4个字节之后再读那最后一个字节的话一定会得到0x00的结果, 但是后来我在打印那一行返回值的时候发现返回的是0x04, 这才能够为我的猜想以及阅读上一张图片的内容作下一个铺垫。

只要你写出来的逻辑代码会变得复杂起来, 而且这种复杂的程度超过了二十行以上的话, 那说明肯定出错了, 你是一定知道的。

在代码这一部分内容上面耗费了特别多的时间, 然后就是在进行反思的那一个时候发现了十六进制这个情况存在。这就感觉很让人费解, 觉得有点不可思议。

list='aabbccddee'

hexer=list.decode("hex")

print hexer

在进行打印日志的操作时, 务必让日志内容保持详尽的状态, 具体而言, 需要把每一个所涉及的变量其对应的长度、变量名称以及此时的函数名称都给罗列出来, 并且要将此刻目前正在运行的那个函数情况一并交代清楚。

还有一个问题是, 程序仍然没法自主停下来去打印某个线程的堆栈信息。

本文的全部内容就是这些, 希望这些内容能够对大家在进行学习方面起到一定的帮助作用, 同时也恳请大家多多地去支持脚本之家这样一个平台。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询