公司动态

嵌入式串口通信:从ASCII到二进制,数据表示与传输协议详解

📅 2026/8/17 23:04:07
嵌入式串口通信:从ASCII到二进制,数据表示与传输协议详解
1. 问题缘起从一次调试中的“理所当然”说起最近在调试一块基于英飞凌XMC4500的工控板需要将传感器采集到的温度值通过串口发送到上位机显示。这听起来是个再基础不过的任务读取ADC值转换成实际的摄氏度然后通过UART发送出去。我像往常一样写了个简单的转换函数把整数温度值用sprintf格式化成字符串然后调用UART_Transmit发送。上位机串口助手也如期收到了数据但显示出来的却是一堆乱码或者是一些完全不对的字符。我的第一反应是波特率不对或者数据位、停止位设置错了。但反复检查了XMC4500的UART配置和串口助手的设置完全匹配。我又尝试发送一个固定的字符串比如Hello上位机能正确显示。问题就出在数字上。当我发送数字25时串口助手以十六进制模式查看收到的是0x32和0x35。我瞬间明白了我发送的“25”这个字符串其ASCII码正是0x32字符‘2’和0x35字符‘5’。但我的串口助手默认以“字符模式”即ASCII文本模式显示它忠实地把0x32和0x35解码并显示成了字符“2”和“5”这看起来是对的但它显示的是字符而不是我心里想的那个数值。这引出了一个更深层的问题我的同事问“我们通过串口发送一个十进制数25是不是单片机只能把它转成‘2‘和‘5‘的ASCII码0x32,0x35发出去难道不能直接发0x1925的十六进制吗上位机那边又该怎么识别” 这个问题触及了嵌入式串口通信中一个非常核心但常被初学者混淆的概念串口发送的是字节流而“十进制”、“ASCII码”、“十六进制”只是这些字节流在不同上下文下的不同解释编码或表示形式。单片机本身并不关心你发送的是“十进制数”还是“ASCII码”它只负责把内存中指定地址的二进制数据一个字节一个字节地搬运到串口发送寄存器。所谓的“转换”发生在我们准备要发送的数据阶段以及上位机接收后解析数据的阶段。2. 核心概念厘清字节流、编码与表示法要彻底理解这个问题我们必须跳出“串口发送十进制数”这个模糊的说法从数据在计算机系统中的本质流通过程来看。2.1 数据在内存中的真相一切都是二进制在XMC4500这类单片机的内存中所有数据无论你认为它是整数、浮点数还是一个字符最终都是以二进制形式存在的。当我们定义一个变量int temperature 25;时编译器会在内存中分配一段空间例如4个字节并将数值25的二进制补码形式对于32位整数就是0x00000019存放进去。此时内存里并没有“十进制”或“ASCII”的标签只有一堆二进制位。2.2 发送的本质字节搬运工串口外设UART的工作简单而纯粹当你调用发送函数如UART_Transmit(data, 1)时你实际上是告诉DMA或者CPU“请把内存地址data处的一个字节8位二进制数据拷贝到串口的发送数据寄存器TDR里”。然后硬件会自动将这个字节转换成串行比特流按照设定的波特率、数据位、停止位等参数通过TX引脚发送出去。关键在于你传入UART_Transmit函数的这个data它的值是什么发送出去的就是什么。如果你传入的是变量temperature的地址并且一次发送4个字节那么发出去的就是0x00, 0x00, 0x00, 0x19假设是小端序。如果你传入的是字符串“25”的首地址发出去的就是0x32, 0x35。2.3 接收端的“解码”困境与协议约定数据通过物理线路传到上位机PC的串口上位机的串口驱动同样将其还原为一个个字节放入接收缓冲区。此时这一串字节对于PC来说只是一串0x??的数字。它代表什么含义完全取决于发送方和接收方事先的约定这就是通信协议的一部分。约定为文本ASCII如果双方约定传输的是人类可读的文本信息那么发送方就需要将数值转换成对应的ASCII字符序列。接收方如串口调试助手在“字符模式”下会将这些字节解释为ASCII码并显示成对应的字符。这就是为什么发送0x32, 0x35会显示“25”。这种方式的优点是直观、易调试任何串口工具都能直接看缺点是需要额外的转换开销调用printf、sprintf等且传输效率较低数值255需要3个字节‘2’、‘5’、‘5’。约定为原始二进制数据如果双方约定传输的是原始二进制数据比如直接传输传感器的ADC原始值、浮点数、或自定义结构体那么发送方就直接发送内存映像。接收方则需要知道数据的类型int, float、长度和字节序然后按照同样的规则去解析这些字节。串口调试助手的“十六进制显示”模式就是把这些原始字节以十六进制数的形式展示出来。这种方式效率高但可读性差且需要严格的格式约定否则解析全是乱码。所以回答最初的问题XMC4500的串口输出十进制数并不是“只能”转成ASCII码。你可以选择发送它的原始二进制形式更高效也可以选择转换成ASCII码字符串更易读。选择哪一种取决于你的通信协议和与上位机的约定。3. 实战两种发送方式的代码实现与对比下面我们以XMC4500的DAVE IDE开发环境为例分别展示如何实现这两种发送方式。假设我们要发送一个16位整数sensor_value 1025。3.1 方式一转换为ASCII字符串人类可读文本这是最常用、最易调试的方式。我们使用标准库函数如sprintf将数字格式化为字符串。#include stdio.h // 需要包含标准IO库 #include “uart.h” // 假设你的UART发送函数在此头文件中声明 void send_value_as_ascii(uint16_t value) { char buffer[10]; // 预留足够空间 int len sprintf(buffer, “%d\r\n”, value); // 将数字转换为十进制字符串并添加回车换行 // 注意sprintf会返回写入的字符数不包括结尾的‘\0‘ for(int i 0; i len; i) { UART_Transmit(buffer[i], 1); // 逐个字符字节发送 } // 或者如果UART支持发送字符串可能有一个更高效的函数 // UART_SendString(buffer); }发生了什么sprintf(buffer, “%d\r\n“, 1025)执行后buffer数组里存放的字节序列是{‘1‘, ‘0‘, ‘2‘, ‘5‘, ‘\r‘, ‘\n‘, ‘\0‘}。对应的ASCII码是{0x31, 0x30, 0x32, 0x35, 0x0D, 0x0A, 0x00}。我们发送了前6个字节不包括结尾的‘\0‘。上位机串口助手以文本模式打开接收到0x31, 0x30, 0x32, 0x35, 0x0D, 0x0A将其解释为字符“1”、“0”、“2”、“5”以及回车换行于是在显示窗口新的一行显示“1025”。优点极度友好任何串口调试助手甚至一个简单的终端程序都能直接显示。易于拼接可以轻松与其他文本信息组合如“Temp: 25C\r\n”。跨平台兼容性极佳文本协议是通用协议。缺点与坑点性能开销sprintf是一个比较耗时的函数尤其在资源紧张的MCU上频繁调用会影响实时性。它涉及整数除法、模运算等。内存碎片如果使用动态内存在MCU上不推荐或频繁创建缓冲区需注意管理。浮点数支持虽然%f可用但会显著增加代码体积需要拉入浮点格式库。线程安全标准库的printf/sprintf可能不是线程安全的在多任务环境中需小心。经验之谈在实时性要求高的场合避免在中断服务程序ISR中使用sprintf或printf。可以考虑使用更轻量的整数转字符串函数或者提前将固定文本部分做成常量。3.2 方式二发送原始二进制数据机器友好这种方式直接将变量在内存中的表示形式发送出去。这要求收发双方对数据格式有精确的约定。void send_value_as_binary(uint16_t value) { // 约定以小端序Little-Endian发送一个16位无符号整数 uint8_t data_bytes[2]; data_bytes[0] (uint8_t)(value 0xFF); // 低字节 data_bytes[1] (uint8_t)((value 8) 0xFF); // 高字节 // 可以一次发送整个数组如果UART驱动支持 UART_Transmit(data_bytes, 2); // 或者为了更清晰地表明这是一个“数据包”可以添加帧头帧尾 // uint8_t packet[] {0xAA, 0x55, data_bytes[0], data_bytes[1], 0x55, 0xAA}; // UART_Transmit(packet, sizeof(packet)); }发生了什么value 1025其十六进制为0x0401二进制为00000100 00000001。在小端序机器如ARM Cortex-M上低地址存放低字节。所以data_bytes[0] 0x01,data_bytes[1] 0x04。发送出去的字节流就是0x01, 0x04。上位机如果以十六进制模式查看会看到01 04。如果以文本模式查看0x01和0x04是控制字符SOH和EOT可能会显示为乱码或不可见。优点效率极高没有转换开销直接内存拷贝速度飞快。带宽节省传输数值1025只需2字节而ASCII方式需要4-6字节。适合复杂数据可以方便地打包发送结构体、浮点数、数组等。缺点与巨大坑点可读性为零没有协议解析直接看就是天书。协议必须严格必须约定好字节序Endianness、数据类型大小、帧结构是否有包头包尾、校验和。调试困难你需要一个能解析自定义二进制协议的专用上位机软件或者非常熟悉十六进制查看。字节序问题这是最大的坑。XMC4500ARM是小端序。如果你的上位机程序运行在x86小端序PC上且正确解析那没问题。但如果你的上位机是Java、C#默认大端序网络序或者另一个不同架构的嵌入式设备就必须进行字节序转换。血的教训我曾在一个项目中下位机STM32小端序直接发送一个32位浮点数给一个用C#.NET大端序处理二进制数据时需要特别注意写的上位机。两边都没做字节序转换导致上位机解析出来的数值完全不对排查了很久才发现是字节序的锅。最佳实践是在自定义二进制协议中强制规定使用网络序大端序发送前统一转换。对于16位整数使用htons()主机序转网络序类函数接收方则用ntohs()转换回来。如果MCU环境没有这些标准函数就自己实现一个交换字节的函数。4. 上位机侧解析串口调试助手的正确打开方式理解了发送端的原理我们再看接收端。串口调试助手是我们最常用的工具它的不同模式对应着不同的解析策略。字符模式或文本模式工作方式将接收到的每一个字节当作一个ASCII码去查ASCII表然后显示对应的字符。如果字节值在32-126之间可打印字符就显示字符如果是0-31、127控制字符则可能显示为空格、方框或执行特定动作如0x0D, 0x0A导致换行。适用场景接收方发送的是ASCII文本数据。例如我们的sprintf方式发送的数据。你的操作当你的单片机程序发送的是格式化的字符串时就选这个模式。十六进制显示模式Hex Display工作方式不进行任何ASCII解码直接将每个字节的值以两位十六进制数的形式显示出来字节之间通常用空格分隔。例如收到0x01, 0x04, 0x41显示为01 04 41。适用场景接收方发送的是原始二进制数据。用于调试二进制协议、查看原始数据流、校验数据是否正确。你的操作当你的单片机程序发送的是原始二进制数据如直接发送uint16_t的字节时必须切换到这个模式才能看到真实数据。此时如果你看到01 04并且你知道这是小端序的16位整数你就能心算出它代表0x0401即十进制1025。十六进制发送模式Hex Send工作方式你在发送框里输入01 02 AA调试助手不会把它当成字符串“01 02 AA”去发送对应的ASCII码0x30,0x31,0x20...而是直接发送字节0x01, 0x02, 0xAA。适用场景模拟发送二进制数据包给下位机用于测试。一个常见误区很多新手在调试时单片机以二进制发送上位机用字符模式看是乱码然后他们想用二进制模式回复却在发送框输入“ABCD”并勾选十六进制发送以为发的是‘A‘,‘B‘,‘C‘,‘D‘的ASCII码实际上发出去的是十六进制数0xAB和0xCD如果输入不合法可能出错导致通信失败。切记十六进制发送框里输入的是字节的十六进制值而不是字符。5. 进阶讨论效率、协议与实用技巧在实际项目中我们很少只发送一个孤零零的数字。通常需要构建一个包含多种信息的数据帧。5.1 如何设计一个简单实用的混合协议一个健壮的协议往往结合了文本的可读性和二进制的效率。例如一个常见的环境监控数据帧可以这样设计帧格式STX[设备ID][温度][湿度][光照][校验和]ETXSTX: 帧开始标志如0xAA。设备ID: 1字节二进制表示设备地址。温度: 2字节有符号整数二进制网络序单位0.1°C。值250表示25.0°C。湿度: 2字节无符号整数二进制网络序单位0.1%RH。光照: 4字节浮点数二进制IEEE754格式网络序单位Lux。校验和: 1字节从设备ID到光照数据的累加和取低8位。ETX: 帧结束标志如0x55。这种协议核心数据温度、湿度、光照用二进制保证了精度和效率而帧头帧尾用固定的二进制值便于帧同步。上位机解析时先寻找0xAA然后按约定长度取出后续字节校验最后按约定的数据类型和字节序解析出具体数值。5.2 关于“十进制输出”的再思考我们回到标题“串口输出十进制”。现在可以明确所谓“输出十进制”通常指的是人类期望看到十进制数字的表示形式。实现这一期望有两种途径在下位机完成转换在MCU端使用printf/sprintf将二进制数转换为十进制数字的字符表示ASCII码然后发送。这是“输出十进制”最常见的意思。在上位机完成转换在MCU端发送原始的二进制数。在上位机程序如C#、Python、LabVIEW中接收到字节流后按照约定的格式解析出二进制数然后在软件界面上将这个二进制数以十进制数字的格式显示出来。此时串口传输的并不是“十进制”但最终用户看到的是十进制结果。选择建议人机交互HMI场景如果主要是人在看串口调试助手或者传输给一个简单的文本显示器采用方式一ASCII文本。简单粗暴通用性强。机机交互M2M场景如果是两个设备之间高速、频繁地交换数据采用方式二二进制协议。效率优先但需要精心设计协议和编写解析代码。混合场景在项目初期调试阶段可以用ASCII协议快速验证功能。进入稳定期后为了性能和可靠性可以切换到二进制协议。很多成熟的工业协议如Modbus RTU就是基于二进制的。5.3 XMC4500上的性能考量与优化XMC4500是ARM Cortex-M4内核性能不错但依然需要注意优化。避免在中断中使用sprintf如前所述这个函数很重。如果必须在中断中发送调试信息可以考虑预先格式化好到缓冲区或者使用更轻量的函数。网上有很多“整数转字符串”的优化实现例如针对固定位数的整数可以用查表法。使用DMAXMC4500的UART支持DMA。对于发送定长的二进制数据包配置DMA可以极大解放CPU。即使是发送ASCII字符串如果字符串是已知的如固定的命令也可以放在Flash中用DMA发送。重定向printf到UART这是一个方便调试的技巧。通过重写_write等系统调用可以将标准库的printf输出重定向到串口。这样你就可以在代码中直接printf(“Value: %d\r\n“, sensor_val);。但要注意这背后的实现还是使用了sprintf之类的格式化函数在性能关键路径要慎用。最后分享一个调试二进制协议时的小技巧在发送二进制数据包的同时可以并行开启一个“调试串口”用另一个UART端口以ASCII文本形式打印出你正在发送的二进制数据的十六进制值。这样你在主串口用二进制模式收发业务数据在调试串口用文本模式实时看到数据的十六进制转储两者对比排查问题一目了然。