单片机与上位机串口通信:原理、应用与实践

注:本文为 “单片机与上位机串口通信” 相关文章合辑。

略作重排,未整理去重。
如有内容异常,请看原文。


单片机与上位机的串行通信

饕餮 tt 于 2019 - 12 - 06 14:47:19 发布

写在前面

本文主要记录单片机通过 TXD、RXD 与上位机进行数据交换的过程。首先介绍 51 单片机中与串口通信有关的各种寄存器。

一、51 单片机串口通信相关寄存器

上位机向单片机发送数据时,单片机接收到的数据会存入 SBUF 发送/接收寄存器。该寄存器较为特殊,兼具发送和接收时存放数据的功能。若执行 data = SBUF,则会将 SBUF 接收到的上位机数据存入 data;若执行 SBUF = data,则会将单片机想要发送的数据(即 data 中的数据)送入 SBUF,再通过串口发送到上位机。

接收数据时,单片机会产生串口中断,否则单片机无法知晓何时接收完一位数据。该中断的服务程序为 interrupt 4,标志位是 RI。因此,进入串口中断服务程序时,务必将 RI 清零,否则程序会一直进入该中断服务程序。控制串口中断的寄存器是 SCON,其每一位如下:

在这里插入图片描述

SM0、SM1 这两位与 TMOD 中控制定时器 0、1 的 M0、M1 类似,用于控制串口工作方式。通过改变 SM0、SM1 的值,可使串行口工作在 4 种方式:

SM0SM1波特率
00 f o s c / 12 f_{osc}/12 fosc/12 (主振频率 / 12)
01可变
10 f o s c / 32 f_{osc}/32 fosc/32 (主振频率 / 32)、 f o s c / 64 f_{osc}/64 fosc/64
11可变

由于这 4 种工作方式的内容较多,若读者有疑惑,可自行查阅资料。

二、定时器产生固定波特率

查阅众多同学的博客后发现,很多人不清楚方式 1 和 3 中为何要给 TH1、TL1(这里以定时器 1 为例)一个固定的初值。实际上,这个固定初值是前辈们计算得出的,若要自行计算也是可行的,公式如下(戴胜华教授《单片机原理与应用》):

公式

上式中的 SMOD1 由电源寄存器 PCON 的第七位控制。假设规定串行口工作在方式 1 或 3 的一个初值,原本波特率为 9600,将 SMOD 置 1 后,波特率会翻倍变为 19200,这比较容易理解。

注意:串行口工作方式 0、1、2、3 与定时器工作方式 0、1、2、3 不同,需加以区分。

串行口工作在方式 0、1、2、3 中的任意一种,都可通过使定时器 1 工作在方式 2 来产生相应的波特率。

以下是常用波特率对应的定时器初值:

在这里插入图片描述
例如,若要产生 9600 波特率,当使用串口工作方式 1 或 3(此时 SMOD = 0),并且选用定时器 1 工作在方式 2(8 位自动重装载模式)时,波特率的计算公式为:

BaudRate = 2 SMOD 32 × f o s c 12 × ( 256 − TH1 ) \text{BaudRate} = \frac{2^\text{SMOD}}{32} \times \frac{f_{osc}}{12 \times (256 - \text{TH1})} BaudRate=322SMOD×12×(256TH1)fosc

其中:

  • BaudRate 是所需的波特率 (9600)
  • SMODPCON 寄存器中的波特率倍增位 (此处假设为 0)
  • f o s c f_{osc} fosc 是晶振频率
  • TH1 是定时器 1 高八位寄存器的预设初值

假设晶振频率 f o s c f_{osc} fosc 为 11.0592 MHz (即 11,059,200 Hz),波特率为 9600 bps,且 SMOD = 0,代入公式得:

9600 = 2 0 32 × 11059200 12 × ( 256 − TH1 ) 9600 = \frac{2^0}{32} \times \frac{11059200}{12 \times (256 - \text{TH1})} 9600=3220×12×(256TH1)11059200

化简计算过程:

9600 = 1 32 × 921600 256 − TH1 9600 = \frac{1}{32} \times \frac{921600}{256 - \text{TH1}} 9600=321×256TH1921600

9600 = 28800 256 − TH1 9600 = \frac{28800}{256 - \text{TH1}} 9600=256TH128800

256 − TH1 = 28800 9600 256 - \text{TH1} = \frac{28800}{9600} 256TH1=960028800

256 − TH1 = 3 256 - \text{TH1} = 3 256TH1=3

因此,TH1 寄存器所需的初值为:

TH1 = 256 − 3 = 253 \text{TH1} = 256 - 3 = 253 TH1=2563=253

将十进制值 253 转换为十六进制为 0xFD

结论:
0xFD 这个初值装载到 TH1 寄存器中。当定时器 1 配置为方式 2 并启动后,即可为串口提供所需的时钟,产生 9600 波特率。

关于定时器 1 工作方式 2:

  • 这是一种 8 位自动重装载模式。
  • 低八位 TL1 作为计数器进行递增计数。
  • 高八位 TH1 用于保存计数初值(即重装载值 0xFD)。
  • TL10xFF 计数溢出回到 0x00 时,硬件会自动将 TH1 中的值(0xFD)重新装载到 TL1 中,TL1 随即从 0xFD 开始下一个计数周期。这个溢出事件的频率被用作串口通信的波特率时钟源。

三、串口通信相关寄存器功能

回到 SCON 寄存器,其中 REN 为允许接收控制位,置 0 则禁止串口接收数据,置 1 则允许。
TB8 和 RB8 用于方式 2 和 3 中发送和接收数据的第 9 位,此处不再详述,使用时可进一步查阅资料。
TI 是发送中断标志位,发送完毕会自动置 1,发送数据前需先清零
TI,发送完后可根据 TI 判断是否发送完毕。
RI 是接收中断标志位,可根据其值判断单片机是否接收完上位机发送的数据。

总结一下,串口发送和接收涉及的寄存器及相应位包括:

PCON 中的 SMOD,SCON 中的 SM0、SM1、REN、TI、RI,TH0、TL0(TH1、TL1),
TMOD 中的 M1、M0(控制定时器的工作方式),
IE 中的 EA、ES(允许总中断、允许串口中断),
TCON 中的 TR0(TR1)。

寄存器 / 位描述
PCON 中的 SMOD控制波特率翻倍。
SCON 中的 SM0、SM1控制串口工作方式。
SCON 中的 REN允许接收控制位,置 1 允许接收数据。
SCON 中的 TI发送中断标志位,发送完毕自动置 1,发送前需清零。
SCON 中的 RI接收中断标志位,接收完毕自动置 1,需手动清零。
TH0、TL0定时器 0 的高八位和低八位。
TH1、TL1定时器 1 的高八位和低八位,用于波特率控制。
TMOD 中的 M1、M0控制定时器的工作方式。
IE 中的 EA、ES允许总中断、允许串口中断。
TCON 中的 TR0、TR1定时器 0 和定时器 1 的运行控制位。

四、单片机与上位机通信过程

单片机接收上位机数据工作过程

  1. 定时器产生一定波特率。
  2. 单片机与上位机通过 TXD、RXD 开始通信。
  3. 单片机允许串口中断,允许接收数据。
  4. 单片机接收到数据,进入串口中断服务程序,并将 RI 置 1,软件将 RI 清零,读取 SBUF。

单片机发送数据到上位机工作过程

  1. 定时器产生一定波特率。
  2. 单片机与上位机通过 TXD、RXD 开始通信。
  3. 单片机赋值给 TI。
  4. 单片机发送数据给上位机。
  5. 上位机接收到数据。

五、程序示例

以下两段程序参考了郭天祥《新概念 51 单片机 C 语言教程》以及其他同学的博客。

示例 1:接收并回传数据(郭天祥代码示例)

#include<reg52.h>
typedef unsigned char uint8;
typedef unsigned int uint16;
uint8 flag,a,i;
uint8 code table []="I get";
void init (){TMOD = 0x20;			// 定时器 1 工作在方式 2,八位自动重装TH1 = 0xfd;TL1 = 0xfd;TR1 = 1;				// 开启定时器 1SM0 = 0;SM1 = 1;				// 串口工作方式 1REN = 1;				// 接收允许EA = 1;					// 开总中断ES = 1;					// 开串口中断
}
void main (){init ();while (1){if (flag){ES = 0;				// 暂时关闭串口中断,防止在处理数据时再次发生串口中断for (i=0;i<6;i++){SBUF=table [i];	// 将 I get 放入发送寄存器while (!TI);		// 检测是否发送完毕,发送完毕后自动置 1TI=0;			// 将发送完毕的标志位清零}SBUF=a;				// 将接受到的值发回给主机while (!TI);TI=0;ES=1;				// 重新打开串口中断flag=0;}}
}
void ser () interrupt 4 {			// 串口中断服务程序RI = 0;						// 中断标志位a = SBUF;					// 将接收到的数据存入 a 中flag=1;
}

示例 2:按键发送字符串(结合按键,按一下发送一行字符的代码示例)

#include <reg51.h>
typedef unsigned char uint8;
typedef unsigned int uint16;
#define key_state0 0
#define key_state1 1
#define key_state2 2
sbit key = P3^2;
uint8 key_value;
bit flag;
uint8 Buf []="hello world!\n";
void delay (uint16 n)
{while (n--);
}
/* 波特率为 9600*/
void UART_init (void)
{SCON = 0x50;        // 串口方式 1TMOD = 0x21;        // 定时器 1 使用方式 2 自动重载,定时器 0 用作按键扫描TH1 = 0xFD;    		//9600 波特率对应的预设数,定时器方式 2 下,TH1=TL1TL1 = 0xFD;TH0 = 0x4C;			//50msTL0 = 0x00;TR1 = 1;			// 开启定时器,开始产生波特率TR0 = 1;ET0 = 1;EA  = 1;
}
/* 发送一个字符 */
void UART_send_byte (uint8 dat)
{SBUF = dat;       	// 把数据放到 SBUF 中while (TI == 0);	// 未发送完毕就等待TI = 0;    			// 发送完毕后,要把 TI 重新置 0
}
/* 发送一个字符串 */
void UART_send_string (uint8 *buf)
{while (*buf != '\0'){UART_send_byte (*buf++);}
}
void scankey (){static uint8 key_state;switch (key_state){case key_state0:if (!key) key_state = key_state1;break;case key_state1:if (!key){UART_send_string (Buf);delay (20000);key_state = key_state2;}else {key_state = key_state0;}break;case key_state2:if (key){key_state = key_state0;}break;default:break;}
}
void main ()
{UART_init ();while (1){if (flag){scankey ();}}
}
void timer0_isr () interrupt 1 using 0 {TH0 = 0xDC;			//10msTL0 = 0x00;flag = 1;
}

上述郭天祥的代码仅接收了上位机发送的一位数据,经修改后得到以下代码,可接收多位数据并根据上位机送来的数据控制流水灯,两段功能综合在一起,注释部分为接收多位数据的代码。

#include <reg52.h>
#define key_state0 0
#define key_state1 1
#define key_state2 2
typedef unsigned char uint8;
typedef unsigned int uint16;
sbit key = P3^2;
//uint8 table [8];
uint8 key_value;
uint8 flag,i,dat;
bit flag1;								// 控制是否开始流水
//uint8 num;
void init (){TMOD = 0x21;						// 定时器 1 工作在方式 2,八位自动重装TH1 = 0xfd;TL1 = 0xfd;TR1 = 0xfd;							// 开启定时器 1TH0 = 0x4C;							//50msTL0 = 0x00;TR0 = 1;ET0 = 1;SM0 = 0;SM1 = 1;							// 串口工作方式 1EA = 1;								// 开总中断ES = 1;								// 开串口中断
}
void scankey (){static uint8 key_state;switch (key_state){case key_state0:if (!key) key_state = key_state1;break;case key_state1:if (!key){REN = ~REN;				// 允许 / 禁止接收上位机数据key_state = key_state2;}else {key_state = key_state0;}break;case key_state2:if (key){key_state = key_state0;}break;default:break;}
}
void main (){init ();P1 = 0xff;while (1){if (!REN) P1 = 0xff;				// 不接收上位机数据时,关闭所有灯if (flag){ES = 0;						// 暂时关闭串口中断,防止在处理数据时再次发生串口中断//for (i=0;i<8;i++){		// 回传多位数据// SBUF=table [i];		// 发送一位//while (!TI);			// 检测是否发送完毕,发送完毕后自动置 1// TI=0;				// 将发送完毕的标志位清零// }SBUF = dat;while (!TI);TI = 0;ES=1;						// 重新打开串口中断flag=0;//num=0;					// 清零接收计数}}
}
void ser () interrupt 4 {					// 串口中断服务程序if (RI){RI = 0;							// 中断标志位//table [num++] = SBUF;dat = SBUF;						// 将接收到的数据存入 dat 中P1 = SBUF;						// 将收到的 16 进制数赋给 P1//if (num == 8) 					// 收满 8 位数据,开始回传flag=1;}
}
void timer0_isr () interrupt 1 using 0 {TH0 = 0xDC;			//10msTL0 = 0x00;scankey ();
}

目前程序中,发送代码里的 while (!TI) 会一直占用单片机。按照之前按键扫描延时尽量不用 delay 的原则,这里的 while 等待可能存在问题,但不确定是否是个人多虑,还需进一步深入学习才能确定。

若有错误,欢迎评论指正。

2020/3/19 日补充

为什么串口的波特率与定时器有关?

近期再次回顾这篇博客时,思考了串口波特率与定时器的联系。为何要用定时器 1 控制波特率,而不能用定时器 2?经查阅资料发现,51 单片机串口的波特率与定时器 1 的溢出率有关,这在计算波特率的公式中有所体现。定时器的溢出率即定时器的溢出速率,大致可理解为定时器溢出一次的时间。若晶振频率为 11.0592MHz,时钟周期为 1 / 11.0592 1/11.0592 1/11.0592,机器周期为 12 / 11.0592 12/11.0592 12/11.0592,则单片机定时器 + 1 的时间为 12 / 11.0592 12/11.0592 12/11.0592 μs,溢出率 = 溢出一次的时间 = 计数次数 × 机器周期。因此,通过改变定时器的初值,可改变定时器的溢出率,进而改变串口的波特率。

顺便解释一下波特率,它指串口每秒能接收的比特数(bit)。由于串口是按顺序一位一位发送数据,波特率为 9600 表示串口每秒能接收 9600 位数据。若上位机的波特率大于 9600,通信会失败,因为单片机来不及接收这么多数据。所以,串口通信要求上下位机的波特率一致,以保证数据传送不出错。

六、总结

本文介绍了单片机与上位机通过串口进行数据交换的基本原理和实现方法。通过合理配置寄存器、设计通信协议以及编写相应的程序代码,可以实现可靠的数据传输。在实际应用中,还需注意波特率匹配、数据缓存、错误处理等问题,以确保通信的稳定性和可靠性。


详细介绍如何从 0 开始写一个数据通信将数据从单片机发送到上位机(或者虚拟示波器)进行数据或图像显示,以及常见问题或注意事项解答

慕羽★于 2020-05-09 16:33:38 发布

本文主要内容

本文详细介绍如何从零开始实现数据通信,将数据从单片机发送到上位机(或虚拟示波器)进行数据或图像显示。文中还探讨了在编写通信协议时可能遇到的问题及注意事项,以匿名上位机为例,适合新手和初学者。

一、准备工作

1. 通信协议或帧格式

  • 必须了解上位机或虚拟示波器的通信协议或帧格式。例如,匿名上位机的通信帧格式如下:

    图片来自匿名通讯协议v7.00

    图片来自匿名通讯协议v7.00

  • 例如,垆边月晓开发的20通道数字示波器通信协议:

    在这里插入图片描述

  • 只有明确通信格式,双方才能进行有效通信,类似于语言交流或电报通信。

2. 确定单片机的大小端模式

  • 大端模式:高字节保存在低地址,低字节保存在高地址。

  • 小端模式:高字节保存在高地址,低字节保存在低地址。

  • 常见单片机的大小端模式:

    • KEIL C51:大端模式

    • KEIL MDK:小端模式

    • SDCC-C51、AVRGCC:小端模式

    • PC:小端模式,大部分ARM:小端模式

  • 51单片机一般为大端模式,32单片机一般为小端模式。

  • 确定大小端模式是必要的,因为不同模式下数据通信程序会有所不同。

3. 确保单片机串口正常工作

  • 通过串口发送简单数据,使用串口助手或其他软件(如匿名上位机)验证数据是否正确。

  • 串口问题可能导致数据校验失败,例如内部振荡器频率设置错误或波特率设置错误。

二、编写数据通信程序

1. 定义数据数组

  • 定义一个 uint8 类型的数组用于存放待发送的数据,容量一般 100 个字节足够:

    uint8 data_to_send[100];
    

2. 确定函数参数类型和个数

  • 根据要发送的数据类型和数量确定函数参数。例如,发送 4 个 uint16 类型数据:

    void ANO_DT_Send_F1(uint16 _a, uint16 _b, uint16 _c, uint16 _d);
    

3. 填充通信帧的固定部分

  • 根据通信帧格式,将帧头、帧类型和数据长度等信息放入数组:

    uint8 _cnt = 0;
    uint8 sumcheck = 0; // 和校验
    uint8 addcheck = 0; // 附加校验
    uint8 i = 0;data_to_send[_cnt++] = 0xAA;
    data_to_send[_cnt++] = 0xFF;
    data_to_send[_cnt++] = 0xF1;
    data_to_send[_cnt++] = 8; // DATA 区数据长度
    

4. 数据拆分(根据单片机大小端模式)

  • 数据拆分宏定义:

    #define BYTE0(dwTemp) (*(char *)(&dwTemp))
    #define BYTE1(dwTemp) (*((char *)(&dwTemp) + 1))
    #define BYTE2(dwTemp) (*((char *)(&dwTemp) + 2))
    #define BYTE3(dwTemp) (*((char *)(&dwTemp) + 3))
    
  • 小端模式单片机

    data_to_send[_cnt++] = BYTE0(_a);
    data_to_send[_cnt++] = BYTE1(_a);
    
  • 大端模式单片机

    data_to_send[_cnt++] = BYTE1(_a);
    data_to_send[_cnt++] = BYTE0(_a);
    

5. 计算和校验与附加校验

  • 和校验:从帧头开始,对每一字节进行累加,只取低 8 位。

  • 附加校验:在计算和校验的同时,对和校验值进行累加,只取低 8 位。

  • 示例代码:

    for (i = 0; i < data_to_send[3] + 4; i++) {sumcheck += data_to_send[i];   // 和校验addcheck += sumcheck;          // 附加校验
    }data_to_send[_cnt++] = sumcheck;
    data_to_send[_cnt++] = addcheck;
    

6. 通过串口发送数据

  • 使用串口发送函数将数组中的数据依次发送到上位机或虚拟示波器:

    uart_putbuff(DEBUG_UART, data_to_send, _cnt);
    

三、完整的数据通信程序示例

1. 小端模式单片机,通过 F1 帧发送 4 个 uint16 类型数据

uint8 data_to_send[100];void ANO_DT_Send_F1(uint16 _a, uint16 _b, uint16 _c, uint16 _d) {uint8 _cnt = 0;uint8 sumcheck = 0;uint8 addcheck = 0;uint8 i = 0;data_to_send[_cnt++] = 0xAA;data_to_send[_cnt++] = 0xFF;data_to_send[_cnt++] = 0xF1;data_to_send[_cnt++] = 8; // 数据长度data_to_send[_cnt++] = BYTE0(_a);data_to_send[_cnt++] = BYTE1(_a);data_to_send[_cnt++] = BYTE0(_b);data_to_send[_cnt++] = BYTE1(_b);data_to_send[_cnt++] = BYTE0(_c);data_to_send[_cnt++] = BYTE1(_c);data_to_send[_cnt++] = BYTE0(_d);data_to_send[_cnt++] = BYTE1(_d);for (i = 0; i < data_to_send[3] + 4; i++) {sumcheck += data_to_send[i];addcheck += sumcheck;}data_to_send[_cnt++] = sumcheck;data_to_send[_cnt++] = addcheck;uart_putbuff(DEBUG_UART, data_to_send, _cnt);
}

2. 大端模式单片机,通过 F1 帧发送 4 个 uint16 类型数据

uint8 data_to_send[100];void ANO_DT_Send_F1(uint16 _a, uint16 _b, uint16 _c, uint16 _d) {uint8 _cnt = 0;uint8 sumcheck = 0;uint8 addcheck = 0;uint8 i = 0;data_to_send[_cnt++] = 0xAA;data_to_send[_cnt++] = 0xFF;data_to_send[_cnt++] = 0xF1;data_to_send[_cnt++] = 8; // 数据长度data_to_send[_cnt++] = BYTE1(_a);data_to_send[_cnt++] = BYTE0(_a);data_to_send[_cnt++] = BYTE1(_b);data_to_send[_cnt++] = BYTE0(_b);data_to_send[_cnt++] = BYTE1(_c);data_to_send[_cnt++] = BYTE0(_c);data_to_send[_cnt++] = BYTE1(_d);data_to_send[_cnt++] = BYTE0(_d);for (i = 0; i < data_to_send[3] + 4; i++) {sumcheck += data_to_send[i];addcheck += sumcheck;}data_to_send[_cnt++] = sumcheck;data_to_send[_cnt++] = addcheck;uart_putbuff(DEBUG_UART, data_to_send, _cnt);
}

3. 小端模式单片机,通过 F2 帧发送 4 个 int16 类型数据

uint8 data_to_send[100];void ANO_DT_Send_F2(int16 _a, int16 _b, int16 _c, int16 _d) {uint8 _cnt = 0;uint8 sumcheck = 0;uint8 addcheck = 0;uint8 i = 0;data_to_send[_cnt++] = 0xAA;data_to_send[_cnt++] = 0xFF;data_to_send[_cnt++] = 0xF2;data_to_send[_cnt++] = 8; // 数据长度data_to_send[_cnt++] = BYTE0(_a);data_to_send[_cnt++] = BYTE1(_a);data_to_send[_cnt++] = BYTE0(_b);data_to_send[_cnt++] = BYTE1(_b);data_to_send[_cnt++] = BYTE0(_c);data_to_send[_cnt++] = BYTE1(_c);data_to_send[_cnt++] = BYTE0(_d);data_to_send[_cnt++] = BYTE1(_d);for (i = 0; i < data_to_send[3] + 4; i++) {sumcheck += data_to_send[i];addcheck += sumcheck;}data_to_send[_cnt++] = sumcheck;data_to_send[_cnt++] = addcheck;uart_putbuff(DEBUG_UART, data_to_send, _cnt);
}

4. 大端模式单片机,通过 F2 帧发送 4 个 int16 类型数据

uint8 data_to_send[100];void ANO_DT_Send_F2(int16 _a, int16 _b, int16 _c, int16 _d) {uint8 _cnt = 0;uint8 sumcheck = 0;uint8 addcheck = 0;uint8 i = 0;data_to_send[_cnt++] = 0xAA;data_to_send[_cnt++] = 0xFF;data_to_send[_cnt++] = 0xF2;data_to_send[_cnt++] = 8; // 数据长度data_to_send[_cnt++] = BYTE1(_a);data_to_send[_cnt++] = BYTE0(_a);data_to_send[_cnt++] = BYTE1(_b);data_to_send[_cnt++] = BYTE0(_b);data_to_send[_cnt++] = BYTE1(_c);data_to_send[_cnt++] = BYTE0(_c);data_to_send[_cnt++] = BYTE1(_d);data_to_send[_cnt++] = BYTE0(_d);for (i = 0; i < data_to_send[3] + 4; i++) {sumcheck += data_to_send[i];addcheck += sumcheck;}data_to_send[_cnt++] = sumcheck;data_to_send[_cnt++] = addcheck;uart_putbuff(DEBUG_UART, data_to_send, _cnt);
}

5. 小端模式单片机,通过 F3 帧发送 2 个 int16 类型和 1 个 int32 类型数据

uint8 data_to_send[100];void ANO_DT_Send_F3(int16 _a, int16 _b, int32 _c) {uint8 _cnt = 0;uint8 sumcheck = 0;uint8 addcheck = 0;uint8 i = 0;data_to_send[_cnt++] = 0xAA;data_to_send[_cnt++] = 0xFF;data_to_send[_cnt++] = 0xF3;data_to_send[_cnt++] = 8; // 数据长度data_to_send[_cnt++] = BYTE0(_a);data_to_send[_cnt++] = BYTE1(_a);data_to_send[_cnt++] = BYTE0(_b);data_to_send[_cnt++] = BYTE1(_b);data_to_send[_cnt++] = BYTE0(_c);data_to_send[_cnt++] = BYTE1(_c);data_to_send[_cnt++] = BYTE2(_c);data_to_send[_cnt++] = BYTE3(_c);for (i = 0; i < data_to_send[3] + 4; i++) {sumcheck += data_to_send[i];addcheck += sumcheck;}data_to_send[_cnt++] = sumcheck;data_to_send[_cnt++] = addcheck;uart_putbuff(DEBUG_UART, data_to_send, _cnt);
}

6. 大端模式单片机,通过 F3 帧发送 2 个 int16 类型和 1 个 int32 类型数据

uint8 data_to_send[100];void ANO_DT_Send_F3(int16 _a, int16 _b, int32 _c) {uint8 _cnt = 0;uint8 sumcheck = 0;uint8 addcheck = 0;uint8 i = 0;data_to_send[_cnt++] = 0xAA;data_to_send[_cnt++] = 0xFF;data_to_send[_cnt++] = 0xF3;data_to_send[_cnt++] = 8; // 数据长度data_to_send[_cnt++] = BYTE1(_a);data_to_send[_cnt++] = BYTE0(_a);data_to_send[_cnt++] = BYTE1(_b);data_to_send[_cnt++] = BYTE0(_b);data_to_send[_cnt++] = BYTE3(_c);data_to_send[_cnt++] = BYTE2(_c);data_to_send[_cnt++] = BYTE1(_c);data_to_send[_cnt++] = BYTE0(_c);for (i = 0; i < data_to_send[3] + 4; i++) {sumcheck += data_to_send[i];addcheck += sumcheck;}data_to_send[_cnt++] = sumcheck;data_to_send[_cnt++] = addcheck;uart_putbuff(DEBUG_UART, data_to_send, _cnt);
}

四、匿名上位机的相关配置

本部分内容参考自匿名通信协议 V7.00,使用其他上位机的读者可跳过此部分。

在这里插入图片描述

在这里插入图片描述

五、上位机显示效果示例

以下代码展示了如何发送 3 个变量的数据:

int16 s1 = 0, s2 = 0;
int32 s3 = 0;
while (1) {ANO_DT_Send_F3(s1, s2, s3);s1 += 1;if (s1 > 100) s1 = 0;s2 = 50 * sin(100 * s1) + 5;s3 = 50 * sin(100 * s1 + 10 * s2);pca_delay_ms(500); // 延时 500 ms 发送一次
}

以下界面说明数据通过了和校验与附加校验:

在这里插入图片描述

在这里插入图片描述

变量 s1

在这里插入图片描述

变量 s2

在这里插入图片描述

变量 s3

在这里插入图片描述

变量 s1、s2、s3 共同显示:

在这里插入图片描述

六、总结

通过上述步骤,您可以根据实际需求编写数据通信程序。

希望本文对大家有所帮助,欢迎大家在评论区交流。


单片机如何通过串口与上位机进行数据交换

getapi 于 2025-04-19 11:03:12 发布

一、硬件连接

确保单片机和上位机之间的串口连接正确:

  • 信号线连接
    • TX(发送端):单片机的 TX 引脚连接到上位机的 RX 引脚。
    • RX(接收端):单片机的 RX 引脚连接到上位机的 TX 引脚。
  • 电平匹配:若单片机工作电压与上位机电平标准不同,需使用电平转换芯片(如 MAX232)。

二、波特率设置

波特率是串口通信的关键参数,决定了数据传输速率。常见的波特率有 9600、115200 等。单片机和上位机的波特率必须一致,否则会导致数据传输错误。

三、单片机端编程

初始化串口

void UART_Init(void) {// 配置 GPIO 引脚为复用功能(TX 和 RX)// 配置 USART 外设(波特率、数据位、停止位、校验位等)// 启用 USART 中断(可选)
}

发送数据

void UART_SendChar(char ch) {while (!(USARTx->SR & USART_SR_TXE)); // 等待发送缓冲区为空USARTx->DR = (ch & 0xFF);            // 发送一个字节
}void UART_SendString(const char *str) {while (*str) {UART_SendChar(*str++);}
}

接收数据

char UART_ReceiveChar(void) {while (!(USARTx->SR & USART_SR_RXNE)); // 等待接收缓冲区非空return (char)(USARTx->DR & 0xFF);     // 读取接收到的数据
}void UART_ReceiveString(char *buffer, int maxLength) {int i = 0;char ch;while (i < maxLength - 1) {ch = UART_ReceiveChar();if (ch == '\r' || ch == '\n') break; // 遇到换行符结束接收buffer[i++] = ch;}buffer[i] = '\0'; // 字符串结束符
}

四、上位机端编程

使用串口助手

  • 打开串口助手软件,选择正确的串口号和波特率。
  • 发送数据给单片机,观察单片机返回的数据。

使用 Python 进行串口通信

import serial# 初始化串口
ser = serial.Serial('COM3', 9600, timeout=1)# 发送数据
ser.write(b'Hello MCU')# 接收数据
data = ser.readline().decode('utf-8').strip()
print(f"Received: {data}")# 关闭串口
ser.close()

五、数据格式设计

为了保证数据的可靠性和易解析性,通常需要设计一套简单的通信协议:

  • 帧头和帧尾:例如,每帧数据以 0xAA 开头,以 0x55 结尾。
  • 数据长度:指定数据的长度,便于接收方解析。
  • 校验机制:如添加 CRC 校验或简单的奇偶校验,确保数据完整性。

六、注意事项

  • 波特率匹配:确保单片机和上位机的波特率一致。
  • 数据缓存:避免因缓冲区溢出导致数据丢失。
  • 错误处理:处理通信中的噪声、丢包等问题。
  • 流控制:对于大数据量传输,可以启用硬件流控(RTS/CTS)或软件流控(XON/XOFF)。

七、总结

单片机通过串口与上位机进行数据交换的核心在于:

  1. 硬件连接正确。
  2. 双方波特率一致。
  3. 编写可靠的发送和接收代码。
  4. 设计合理的通信协议。

通过以上步骤,您可以轻松实现单片机与上位机之间的高效数据交换。


via

  • 51 单片机串口通信详解 - CSDN 博客
    https://blog.csdn.net/qq_36415628/article/details/103418519

  • 详细介绍如何从 0 开始写一个数据通信,将数据从单片机发送到上位机(或者虚拟示波器)进行数据或图像显示,以及常见问题或注意事项解答 - CSDN 博客
    https://blog.csdn.net/qq_44339029/article/details/106004997

  • 单片机如何通过串口与上位机进行数据交换 - CSDN 博客
    https://blog.csdn.net/getapi/article/details/147347648

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/78943.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

996引擎-人物模型(UIModel):创建内观时装备偏移问题

996引擎-人物模型(UIModel):创建内观时装备偏移问题 创建 人物模型(UIModel)问题参考资料创建 人物模型(UIModel) 90、91 是自定义剑甲的穿戴位置,因为需求只需要显示剑甲,所以下面创建人物模型时,只给了剑甲的id、特效。 function Controller:updateUI()-- 自定义收拾…

Python小程序:上班该做点摸鱼的事情

系统提醒 上班会忘记一些自己的事&#xff0c;所以你需要在上班的的时候突然给你弹窗&#xff0c;你就知道要做啥了 源码 这里有一个智能家居项目可以看看(开源) # -*- coding:utf-8 -*- """ 作者:YTQ 日期: 2025年04日29 21:51:24 """ impor…

centos安装部署配置kafka

1、解压到目录 tar -zxvf kafka_2.13-2.8.2.tgz -C /usr/local/kafka2.进入目录 cd /usr/local/kafka/kafka_2.13-2.8.23.查看版本&#xff08;验证是否已解压&#xff09; bin/kafka-topics.sh --version4.修改配置&#xff0c;注意&#xff1a;此配置中有一个默认的zookee…

深⼊理解指针(7)

1.函数指针变量的创建 在x86环境下&#xff1a; 我们发现&#xff1a;以函数是有地址的&#xff0c;函数名就是函数的地址&#xff0c;当然也可以通过& 函数名 的⽅式获得函数的地址。 如果我们要将函数的地址存放起来&#xff0c;就得创建函数指针变量咯&#xff0c;函数…

AdaBoost算法的原理及Python实现

一、概述 AdaBoost&#xff08;Adaptive Boosting&#xff0c;自适应提升&#xff09;是一种迭代式的集成学习算法&#xff0c;通过不断调整样本权重&#xff0c;提升弱学习器性能&#xff0c;最终集成为一个强学习器。它继承了 Boosting 的基本思想和关键机制&#xff0c;但在…

《PyTorch documentation》(PyTorch 文档)

PyTorch documentation(PyTorch 文档) PyTorch is an optimized tensor library for deep learning using GPUs and CPUs. (PyTorch是一个优化的张量库,用于使用GPU和CPU进行深度学习。) Features described in this documentation are classified by release status: (此…

Android学习总结之算法篇六(数组和栈)

括号匹配 public static boolean isValid(String s) {// 创建一个栈用于存储左括号Stack<Character> stack new Stack<>();// 遍历字符串中的每个字符for (char c : s.toCharArray()) {if (c ( || c [ || c {) {// 如果是左括号&#xff0c;将其压入栈中stack…

遗传算法(Genetic Algorithm,GA)

遗传算法&#xff08;Genetic Algorithm&#xff0c;GA&#xff09;是一种受生物进化理论启发的优化算法&#xff0c;通过模拟自然选择和遗传机制来搜索复杂问题的最优解。 ​​核心原理​​ ​​自然选择与适者生存​​&#xff1a;适应度高的个体更有可能繁殖&#xff0c;将…

消防应急物资智能调用立库:豪越科技助力消防“速战速决”

在消防救援的战场上&#xff0c;时间就是生命&#xff0c;每一秒都关乎着人民群众的生命财产安全。然而&#xff0c;在过去的紧急救援中&#xff0c;应急物资无法及时到位的情况时有发生&#xff0c;成为制约救援效率的关键难题&#xff0c;给救援工作带来了巨大的困境。 想象一…

【MySQL】数据类型和表的操作

目录 一. 常用的数据类型 1.数值类型 1.1 整形类型 1.2 浮点型类型 2.字符串类型 char和varchar的区别 如何选择char和varchar 3.日期类型 4.二进制类型 二. 表的操作 1.查看所有表 2.表的创建 3.查看表的结构 4.表的修改 4.1 添加新的列 4.2 修改表中现有的列 4…

涨薪技术|0到1学会性能测试第43课-apache status模块监控

前面的推文我们认识了apache目录结构与配置知识,今天我们继续来看下apache监控技术,究竟是怎么做性能监控的。后续文章都会系统分享干货,带大家从0到1学会性能测试。 Apache监控技术 关于apache监控通常会有两种方法: 一是:使用apache自带的status监控模块进行监控; 二是…

关于 MCP 的理论知识学习

文章目录 1. 写在最前面2. 基本概念2.1 Why MCP2.1.1 大模型访问的局限2.1.2 过渡阶段—Function Call2.1.3 当前阶段— MCP 3. 碎碎念4. 参考资料 1. 写在最前面 最近有一项任务是写旧版本迁移到新版本的支持文档&#xff0c;文档的编写是借助于 cursor 帮忙写的。但是实现的…

C++学习之路,从0到精通的征途:List类的模拟实现

目录 一.list的介绍 二.list的接口实现 1.结点 2.list结构 3.迭代器 &#xff08;1&#xff09;begin &#xff08;2&#xff09;end 4.修改 &#xff08;1&#xff09;insert &#xff08;2&#xff09;push_back &#xff08;3&#xff09;push_front &#xff0…

【游戏ai】从强化学习开始自学游戏ai-2 使用IPPO自博弈对抗pongv3环境

文章目录 前言一、环境设计二、动作设计三、状态设计四、神经网路设计五、效果展示其他问题总结 前言 本学期的大作业&#xff0c;要求完成多智能体PPO的乒乓球对抗环境&#xff0c;这里我使用IPPO的方法来实现。 正好之前做过这个单个PPO与pong环境内置的ai对抗的训练&#…

计算机考研精炼 操作系统

第 14 章 操作系统概述 14.1 基本概念 14.1.1 操作系统的基本概念 如图 14 - 1 所示&#xff0c;操作系统是计算机系统中的一个重要组成部分&#xff0c;它位于计算机硬件和用户程序&#xff08;用户&#xff09;之间&#xff0c;负责管理计算机的硬件资源&#xff0c;为用户和…

什么是基尔霍夫第一定律

基尔霍夫第一定律&#xff08;Kirchhoffs First Law&#xff09;&#xff0c;也称为基尔霍夫电流定律&#xff08;Kirchhoffs Current Law&#xff0c;简称 KCL&#xff09;&#xff0c;是电路分析中最基础的定律之一。它描述了电路中电流的守恒特性&#xff0c;适用于任何集总…

解决 RN Switch 组件在安卓端样式很丑的问题

解决此种问题的方式有很多 可以导入原生库react-native-switch 切图 (会缺少动画) 使用 js 组件 这里使用 js 绘制组件&#xff08;原生体验&#xff09;解决此类问题 Switch.tsx import React, { useEffect, useRef, useState } from react; import { Animated, Pressabl…

【AI】【MCP】搭建私人王炸MCP自动化工作流

目录 一、什么是MCP 二、MCP大集合 三、准备工作 3.1 安装node.js 3.2 安装vscode 3.3 安装cline插件 3.3.1 安装 3.3.2 配置Cline 四、配置MCP服务 4.1 Search-mcp服务 4.2 playwright-mcp 服务 前言&#xff1a;梦想组合&#xff0c;轻松办公&#xff0c;告别手动&a…

Git 实操:如何使用交互式 Rebase 移除指定提交(真实案例分享)

在日常开发中&#xff0c;有时候我们提交了一些不想保留的记录&#xff0c;比如测试代码、错误的功能提交等。 ⚠️ 在操作 4. 强制推送到远程仓库前的注意事项 强制推送&#xff08;git push --force 或 git push -f&#xff09;确实很强大但也危险&#xff0c;因为它会重写…

11.Excel:函数

一 函数是什么 函数是定义好的公式。 单元格内输入sum然后tab&#xff0c;框选要求和的范围&#xff0c;然后回车键。 补充&#xff1a;公式。 公式以开头&#xff0c;可以用于计算&#xff0c;返回数值。 分别点击各个数值&#xff0c;中间用加号连接。这样很不方便&#xff…