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

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

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


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

饕餮 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;				// 开启定时器 1
    SM0 = 0;
    SM1 = 1;				// 串口工作方式 1
    REN = 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);		// 检测是否发送完毕,发送完毕后自动置 1
                TI=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;        // 串口方式 1
    TMOD = 0x21;        // 定时器 1 使用方式 2 自动重载,定时器 0 用作按键扫描
    TH1 = 0xFD;    		//9600 波特率对应的预设数,定时器方式 2 下,TH1=TL1
    TL1 = 0xFD;
    TH0 = 0x4C;			//50ms
    TL0 = 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;			//10ms
    TL0 = 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;							// 开启定时器 1
    TH0 = 0x4C;							//50ms
    TL0 = 0x00;
    TR0 = 1;
    ET0 = 1;
    SM0 = 0;
    SM1 = 1;							// 串口工作方式 1
    EA = 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;			//10ms
    TL0 = 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值