Linux 系统 Python 与 MCU(STM32)串口通信指南与规范
1. 引言
本文档旨在为 Linux 系统下的 Python 与 MCU(STM32)串口通信提供指南与规范。通过本文档,你可以了解如何在 Linux 系统中使用 Python 与 MCU 进行串口通信,包括如何安装必要的库、如何配置串口参数以及如何进行数据传输,同时,本文档还提供了一些规范,以便于你更为高效地进行数据传输。
2. 背景知识
2.1. 串口如何传输数据
串口通信是一种常见的通信方式,它通过串行数据线(TX 和 RX)和地线(GND)进行数据传输。在串口通信中,数据以字节为单位进行传输,每个字节由8位的二进制数据组成(采用8个数据位,无校验位,1个停止位)。同时,由于不包含时钟线,所以串口通信需要通过波特率(Baud Rate)来确定数据传输速率,波特率越高,数据传输速率越快,但是也越容易出错。因此,在串口通信中,波特率的选择需要根据实际情况进行权衡。在具体使用时,通信双方需要事先约定好波特率、数据位、校验位和停止位等参数,以便于正确地进行数据传输。
串口通信的一个典型的数据传输过程:
- TX 拉低电平,表示开始传输数据;
- TX 依次发送数据位(8位);
- TX 发送停止位(1位);
- TX 拉高电平,表示传输结束。
2.2. 内存存储方式
在正式开始传输我们想要的任意大小的数据之前,我们需要了解一些关于内存的知识。 在计算机中,内存是一块非常大的存储器,通常在我们程序运行的时候动态分配。通常,内存的最小单位是字节(Byte),一个字节由8位二进制数据组成。而不同的处理器又会将4个字节(32位)或者8个字节(64位)作为一个数据单元,也就是我们所称的32位处理器或者64位处理器。而对于一个变量,它可以是8位、16位、32位、64位等,在被存储在内存时,会因为处理器的不同而有不同的存储方式。在这里,我们主要介绍两种存储方式:大端存储和小端存储。
大端模式:即高位字节排放在内存地址低地址端,低位字节排放在内存的高地址端。
小端模式:即低位字节排放在内存地址低地址端,高位字节排放在内存的高地址端。
比如,对于一个32位数据0x12345678,它在内存中的存储方式如下:
- 大端模式
0x12 | 0x34 | 0x56 | 0x78
- 小端模式
0x78 | 0x56 | 0x34 | 0x12
2.3. 如何组合数据
2.3.1. 内存中的一连串数据
假定如下类型的数据:
int16_t data[5] = {1, 2, 3, 4, 5}; // 5个16位数据
在这个例子中,我们定义了一个长度为5的int16数组,对于他们所占据的10字节内存空间,如下图所示:
| 地址 | 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x09 |
|---|---|---|---|---|---|---|---|---|---|---|
| 内容 | 0x00 | 0x01 | 0x00 | 0x02 | 0x00 | 0x03 | 0x00 | 0x04 | 0x00 | 0x05 |
以单个变量为界,又可以表示为:
| 地址 | data[0] | data[1] | data[2] | data[3] | data[4] |
|---|---|---|---|---|---|
| 内容 | 0x0001 | 0x0002 | 0x0003 | 0x0004 | 0x0005 |
而对于为小端模式的stm32,其内存存储方式如下:
| 地址 | 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x09 |
|---|---|---|---|---|---|---|---|---|---|---|
| 内容 | 0x01 | 0x00 | 0x02 | 0x00 | 0x03 | 0x00 | 0x04 | 0x00 | 0x05 | 0x00 |
以单个变量为界,又可以表示为:
| 地址 | data[0] | data[1] | data[2] | data[3] | data[4] |
|---|---|---|---|---|---|
| 内容 | 0x0100 | 0x0200 | 0x0300 | 0x0400 | 0x0500 |
2.3.2. 内存中多种类型数据的组合
假定如下类型的数据:
struct {
int16_t a;
int32_t b;
int16_t c;
} data = {1, 2, 3}; // 1个16位数据,1个32位数据,1个16位数据
在这个例子中,我们定义了一个结构体,其中包含了一个16位数据,一个32位数据,和一个16位数据。对于他们所占据的8字节内存空间,如下图所示:
| 地址 | 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 |
|---|---|---|---|---|---|---|---|---|
| 内容 | 0x00 | 0x01 | 0x00 | 0x00 | 0x00 | 0x02 | 0x00 | 0x03 |
以单个变量为界,又可以表示为:
| 地址 | a | b | c |
|---|---|---|---|
| 内容 | 0x0001 | 0x00000002 | 0x0003 |
而对于为小端模式的stm32,其内存存储方式如下:
| 地址 | 0x00 | 0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 |
|---|---|---|---|---|---|---|---|---|
| 内容 | 0x01 | 0x00 | 0x02 | 0x00 | 0x00 | 0x00 | 0x03 | 0x00 |
以单个变量为界,又可以表示为:
| 地址 | a | b | c |
|---|---|---|---|
| 内容 | 0x0100 | 0x02000000 | 0x0300 |
2.4. Python 与 MCU 串口通信
2.4.1 stm32如何进行符合数据传输
在此处,我们需要约定一种数据传输的规范,以便于在 Python 与 MCU 之间进行数据传输。在本文档中,我们约定如下的数据传输规范:
typedef struct packet_t{
uint8_t header; // 数据包头
uint8_t type; // 数据类型
uint16_t data1; // 数据1
float32_t data[2]; // 数据2、3
uint8_t tail; // 数据包尾
} packet;
对于stm32,由于为一个完整的数据包,内部数据相邻排布,而串口通信需要逐个发送与接收字节,因此我们约定,直接对其内存空间逐字节发送与接收,发送环节可以使用如下代码实现:
packet data = {0x01, 0x02, 0x03, {0.1, 0.2}, 0x04}; // 数据初始化
for(int i = 0; i < sizeof(packet); i++){
HAL_UART_Transmit(&huart1, (uint8_t *)&data + i, 1, 1000); // 逐字节发送
}
接收数据时,以中断方式接收,接收到一个完整的数据包后,再进行数据解析,具体如下:
uint8_t buffer[10]; // 缓冲区
packet data; // 数据包
uint8_t index = 0; // 缓冲区索引
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart){
if(huart->Instance == USART1){
buffer[index++] = data; // 接收数据
if(index >= sizeof(packet)){ // 接收完整数据包
memcpy(&data, buffer, sizeof(packet)); // 数据解析
index = 0; // 重置索引
}
}
}
注意,此处使用了memcpy函数,该函数用于将内存中的数据拷贝到另一个内存地址中,为了数据的完整性,我们需要将接收到的数据拷贝到另一个内存地址中,以便于后续的数据解析,故不能在每次接收到数据包后直接追加到数据包中。
2.4.2. Python 如何进行数据传输
对于上位机,由于更为完善的操作系统,以及Python的强大库支持,我们可以直接使用Python的struct库来进行数据的打包与解包,具体如下:
- 数据打包与发送
import struct
import serial
format = "<BBHffB" # 数据包格式
# 数据包定义
data = struct.pack(format, 0x01, 0x02, 0x03, 0.1, 0.2, 0x04)
# 串口初始化
ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=1)
# 数据发送
ser.write(data)
- 数据接收与解析
import struct
import serial
class Packet:
def __init__(self, header, type, data1, data2, data3, checksum):
self.header = header
self.type = type
self.data1 = data1
self.data2 = data2
self.data3 = data3
self.tail = tail
data = Packet(1, 2, 3, 0.1, 0.2, 4)
# 串口初始化
ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=1)
# 循环接收数据
while True:
# 检查缓冲区大小是否符合数据包大小
if ser.in_waiting >= struct.calcsize(format):
# 读取数据
data = ser.read(struct.calcsize(format))
此处可以进一步通过类的封装,实现更可读性更高的代码:
import struct
import serial
format = "<BBHffB" # 数据包格式
class Packet:
def __init__(self, header, type, data1, data2, data3, tail):
self.header = header
self.type = type
self.data1 = data1
self.data2 = data2
self.data3 = data3
self.tail = tail
def pack(self) -> bytes:
return struct.pack(format, self.header, self.type, self.data1, self.data2, self.data3, self.tail)
def unpack(self, data: bytes):
self.header, self.type, self.data1, self.data2, self.data3, self.tail = struct.unpack(format, data)
# 串口初始化
ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=1)
# 数据包定义
data = Packet(1, 2, 3, 0.1, 0.2, 4)
while True:
if ser.in_waiting >= struct.calcsize(format):
data.unpack(ser.read(struct.calcsize(format)))
这里使用了struct库,该库用于将Python的数据类型转换为C语言的数据类型,以便于进行数据的打包与解包。在这里,我们使用struct.pack函数将数据打包为字节流,然后通过串口发送;在接收数据时,我们使用struct.unpack函数将字节流解包为Python的数据类型,以便于后续的数据处理。而这两个函数的第一个参数是一个格式化字符串,用于指定数据的类型与顺序。具体的格式化字符串如下:
2.4.2.1. 格式化字符串的字节顺序,大小和对齐方式
在默认情况下,C 类型将以所在机器的原生格式和字节顺序来表示,并在必要时通过跳过填充字节来正确地对齐(根据 C 编译器所使用的规则)。 选择此行为是为了使已打包结构体的字节与对应的 C 结构体的内存布局完全对应。 使用原生字节顺序和填充还是标准格式取决于应用程序本身。
或者,根据下表,格式字符串的第一个字符可用于指示打包数据的字节顺序,大小和对齐方式: | 字符 | 字节顺序 | 大小 | 字节顺序 | | —- | —- | —- | ——– | | @ | 按原字节 | 按原字节 | 按原字节 | | = | 按原字节 | 标准大小 | 无 | | < | 小端 | 标准大小 | 无 | | > | 大端 | 标准大小 | 无 |
如果第一个字符不是其中之一,则假定为 @ (也就是默认值)。
例如,数字 1023 (十六进制的 0x3ff) 具有以下字节表示形式:
- 大端序 (>)
0x03 0xff - 小端序 (<)
0xff 0x03
Python 示例:
>>> import struct
>>> struct.pack('>H', 1023)
b'\x03\xff'
>>> struct.pack('<H', 1023)
b'\xff\x03'
原生字节顺序可能为大端序或小端序,具体取决于主机系统。 例如,Intel x86, AMD64 (x86-64) 和 Apple M1 是小端序的;IBM z 和许多旧式架构则是大端序的。 请使用 sys.byteorder 来检查你的系统字节顺序。
>>> import sys
>>> sys.byteorder
'little'
本机大小和对齐方式是使用 C 编译器的 sizeof 表达式来确定的。 这总是会与本机字节顺序相绑定。
标准大小仅取决于格式字符;请参阅 格式字符 了解更多信息。
请注意 @ 和 = 之间的区别:两个都使用本机字节顺序,但后者的大小和对齐方式是标准化的。
形式 ! 代表网络字节顺序总是使用在 IETF RFC 1700 中所定义的大端序。
没有什么方式能指定非本机字节顺序(强制字节对调);请正确选择使用 < 或 >。
2.4.2.2. 格式化字符串的格式字符
格式字符具有以下含义;C 和 Python 值之间的按其指定类型的转换应当是相当明显的。 标准大小列是指当使用标准大小时以字节表示的已打包值大小;也就是当格式字符串以 <, >, ! 或 = 之一开头的情况。 当使用本机大小时,已打包值的大小取决于具体的平台。
| 字符 | C 类型 | Python 类型 | 标准大小 | 备注 |
|---|---|---|---|---|
| x | 空字节 | 无 | 在打包时填充为NUL字符 |
|
| c | char |
长度为1的str |
1 | |
| b | signed char |
int |
1 | |
| B | unsigned char |
int |
1 | |
| ? | _Bool |
bool |
1 | 注1 |
| h | short |
int |
2 | |
| H | unsigned short |
int |
2 | |
| i | int |
int |
4 | |
| I | unsigned int |
int |
4 | |
| l | long |
int |
4 | |
| L | unsigned long |
int |
4 | |
| q | long long |
int |
8 | |
| Q | unsigned long long |
int |
8 | |
| n | ssize_t |
int |
||
| N | size_t |
int |
||
| e | float |
float |
2 | 注2 |
| f | float |
float |
4 | |
| d | double |
float |
8 | |
| s | char[] |
str |
||
| p | char[] |
str |
||
| P | void * |
int |
其中,粗体字母表示常用的格式字符。可以简单用下图表示:
| 字符 | 大小(字节) | 类比 C 类型 | 是否有符号 | 范围 |
|---|---|---|---|---|
| b | 1 | int8_t | √ | -128 ~ 127 |
| B | 1 | uint8_t | 0 ~ 255 |
|
| h | 2 | int16_t | √ | -32768 ~ 32767 |
| H | 2 | uint16_t | 0 ~ 65535 |
|
| i | 4 | int32_t | √ | -2e31 ~ 2e31 - 1 |
| I | 4 | uint32_t | 0 ~ 2e32 - 1 |
|
| l | 4 | int32_t | √ | -2e31 ~ 2e31 - 1 |
| L | 4 | uint32_t | 0 ~ 2e32 - 1 |
|
| f | 4 | float | √ |
参考资料
附录
代码示例
-
_Bool类型是 C99 标准中引入的布尔类型。 请注意,_Bool类型在 C 中的大小是不确定的,因此在 Python 中使用?时,它的大小是不确定的,应当谨慎使用。 ↩ -
IEEE 754 binary16 "半精度"类型是在 IEEE 754标准 中定义的 16 位浮点数类型。 它包含一个符号位,5 个指数位和 11 个精度位(明确存储 10 位),可以完全精确地表示大致范围在6.1e-05到6.55e+04之间的数字。此类型并不被 C 编译器广泛支持,应当谨慎使用。 ↩