STM32学习之旅④ USART串口和上位机通信



目录:

一、认识其本质


(一)串口

  • 串口是串行接口 (Serial Interface)的简称,它是指数据一位一位地顺序传送,其特点是通信线路简单,只要一对传输线就可以实现双向通信(可以直接利用电话线作为传输线),从而大大降低了成本,特别适用于远距离通信,但传送速度较慢。一条信息的各位数据被逐位按顺序传送的通讯方式称为串行通讯。串行通讯的特点是:数据位的传送,按位顺序进行,最少只需一根传输线即可完成;成本低但传送速度慢。串行通讯的距离可以从几米到几千米;根据信息的传送方向,串行通讯可以进一步分为单工、半双工和全双工三种。

(二)协议

  • 所谓协议,就是通信双方约定好的规定,通信双方只有遵守这个规定才能够完成任务。举个栗子就是周幽王烽火戏诸侯,双方约定好以烽火为信号进行通信,但是愚蠢的周幽王为博美人褒姒一笑破坏了这个规定,最后付出的代价是惨重的。可见,通信双方只有遵守协议才能够完成通信。

(三)时序

  • 时序就是协议的实际化,它实质上是一些列的脉冲信号,通信双方将信息按照预先定好的规定(协议)转换成一系列的脉冲信号,通过总线发送给接收方,接收方再将接收到的数据按照规定进行解析,从而得到发送方发送过来的数据。

(四)上位机

  • 上位机和下位机其实是一个相对的概念,上位机指的是可以直接发出操控命令的计算机,一般指PC机,能够显示各种信号变化(液压,水位,温度等),能够将信息直接传递给人。下位机是直接控制设备获取设备状况的计算机,一般是PLC/单片机single chip microcomputer/slave computer/lower computer之类的,下位机需要PC机来对其进行控制。

二、所需材料



三、USART的介绍


  • stm32有丰富的通讯外设,USART(Universal Synchronous Asynchronous Receiver Transmitter)、SPI(Serial Peripheral interface)、I2c(Inter-Integrated Circuit)、CAN(Controller Area Network),因为stm32有完整的且强大的固件库,这使得配置串口的难度大大降低了,和用软件IO口模拟通信时序相比,硬件的支持可以大大提高通信的速率、大大降低出错的概率,从而提高了通信的质量和效率。用IO口模拟USART难度较大,它对延时要求比较苛刻,且出错的概率较大,所以一般很少用IO口模拟USART。IO口模拟I2c比较常见,由于I2c的最高通信速度只有3.4M/s,单片机的IO口速度可以完美驾驭。由于SPI多用于一些较高速的通信,例如LCD、OLED、TFT显示器的写入,EEPROM (Electrically Erasable Programmable read only memory)的写入和读取,用IO口模拟效果不是很理想,所以建议使用硬件自带接口。

  • 关于USART,以下是官方的介绍
    这里写图片描述


四、USART串口的配置


  • 先来看一下stm32的系统结构

这里写图片描述

  • 通过对stm32几个模块的操作,我们可以发现stm32外设配置的一些基本套路:打开相应的时钟->配置相应的引脚功能->声明对应的结构体->利用相应的Init函数进行初始化

  • 打开打开USATT1、GPIOA、AFIO的时钟

void usart_config()
{
    /*打开USATT1、GPIOA、AFIO的时钟*/
    RCC_APB2PeriphClockCmd( RCC_APB2Periph_USART1 \
    | RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
    
    /*配置对应的串口引脚*/
    usart_release_gpio_init();

    /*配置串口中断*/
    usart_para_config();
    
    USART_ClearFlag(USART1,USART_FLAG_TC); //清除发送完成标志位
    NVIC_Config();                         //初始化NVIO
    USART_Cmd(USART1, ENABLE);             //使能串口1
}

  • 配置相应的IO口,将其设为复用推挽输出和浮空输入
void usart_release_gpio_init()
{
    GPIO_InitTypeDef GPIO_InitStruct;
    
    /*配置PA9为复用推外输出*/
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    /*配置PA10为浮空输入*/
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    
    GPIO_Init(GPIOA, &GPIO_InitStruct);
}


  • 配置NVIC(Nested Vectored Interrupt Controller),即内嵌向量中断控制器,它是用来配置中断抢占优先级和从优先级(响应优先级)的

  • 关于抢占优先级和响应优先级区别:

  1. 高优先级的抢占优先级是可以打断正在进行的低抢占优先级中断的。

  2. 抢占优先级相同的中断,高响应优先级不可以打断低响应优先级的中断。

  3. 抢占优先级相同的中断,当两个中断同时发生的情况下,哪个响应优先级高,哪个先执行。

  4. 如果两个中断的抢占优先级和响应优先级都是一样的话,则看哪个中断先发生就先执行;

void NVIC_Config(void)
{
    NVIC_InitTypeDef NVIC_InitStructure; //NVIC 初始化结构体声明
    
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; //设置串口1 中断
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //抢占优先级0
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //子优先级为0
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能
    
    NVIC_Init(&NVIC_InitStructure);
}
  • 配置串口协议
void usart_para_config(void)
{
    USART_InitTypeDef USART_InitStruct;
    
    USART_InitStruct.USART_BaudRate = 115200;
    USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
    USART_InitStruct.USART_WordLength = USART_WordLength_8b;//8
    USART_InitStruct.USART_Parity = USART_Parity_No;        //N
    USART_InitStruct.USART_StopBits = USART_StopBits_1;     //1

    USART_Init(USART1, &USART_InitStruct);
    
    USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); //使能接收中断
}

五、发送函数


(一)单字节发送


  • 在main函数中调用USART_SendData(USART1, 0x08);这个函数就能够完成单字节的发送了
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "timer.h"
#include "usart.h"
int main()
{
    SystemInit();                //初始化系统,系统时钟设定为72MHz
    systick_init(72);            //配置systick,中断时间设置为72000/72000000 = 1us
    usart_config();
    while(1)
    {
        USART_SendData(USART1, 0x08);
        delay_ms(100);
    }
}
  • 打开串口助手就能够看到串口发来的数据了

这里写图片描述


(二)数据流发送

  • 数据流简单来说就是一串连续的信息序列,一串序列中有若干个字节,每个字节分别对应着通信双方预先约定好的数据含义,例如第一位代表地址、第二位代表数据流向、最后一位代表结束标志、其余位代表数据。数据流的长度可长可短,由通信双方确定,但通信的过程中不能够变化。

  • 定义一个协议栈
typedef struct
{
    u8 head;
    u8 tail;
    u8 direction;
    u8 data[4];
}send_stack;
send_stack tx_stack;
void tx_stack_init()
{
    tx_stack.head = 0xaa;     //协议栈头,起始位,1010 1010b
    tx_stack.direction = 0x09;//数据流方向,0x09表示从单片机发出
    memset(tx_stack.data, 0, sizeof(tx_stack.data));//把tx_stack.data[]全部初始化为零
    tx_stack.tail = 0xdd;     //协议栈尾,结束位,1101 1101b,栈头和栈尾最好能互补
}
  • 将协议栈内的数据依次发出
void usart_senddata()
{
    u8 i;
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    USART_SendData(USART1, tx_stack.head);
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    USART_SendData(USART1, tx_stack.direction);
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    for(i = 0; i < 4; i++)
    {
        USART_SendData(USART1, tx_stack.data[i]);
        while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    }
    USART_SendData(USART1, tx_stack.tail);
    while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
  • 打开串口助手可以看到串口发来的数据流

这里写图片描述



六、接收函数


  • 接收函数和发送函数类似,先定义接收协议栈
typedef struct
{
    u8 head;
    u8 tail;
    u8 direction;
    u8 data[4];
    u8 lock_flag;
    u8 data_pt;
}receive_stack;
receive_stack rx_stack;
void rx_stack_init()
{
    rx_stack.head = 0x00;         //协议栈头,起始位
    rx_stack.direction = 0x00;    //数据流方向,0x09表示从单片机发出
    memset(rx_stack.data, 0, sizeof(rx_stack.data));//把tx_stack.data[]全部初始化为零
    rx_stack.tail = 0x00;         //协议栈尾,结束位
    rx_stack.data_pt = 0x00;
    rx_stack.lock_flag = UNLOCK;
}
  • 接收数据需要借助中断来完成
void USART1_IRQHandler(void)
{
    u8 receive_data;
    if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //判断读寄存器是否非空
    {
        receive_data = USART_ReceiveData(USART1);         //接收单个字节的串口数据
        if(rx_stack.lock_flag == UNLOCK)                   //如果接收协议栈未锁柱
        {
            if(receive_data == 0xaa)
            {
                rx_stack.head = receive_data;
            }
            else if(receive_data == 0xf9)
            {
                rx_stack.direction = receive_data;
            }
            else if(receive_data == 0xdd)
            {
                rx_stack.tail = receive_data;
                if(rx_stack.data_pt >= 4)// && (rx_stack.tail == 0xdd))
                {
                    rx_stack.data_pt = 0;
                    rx_stack.lock_flag = LOCK;
                }
            }
            else
            {
                rx_stack.data[rx_stack.data_pt] = receive_data;
                rx_stack.data_pt++;
                if(rx_stack.data_pt > 4)// && (rx_stack.tail == 0xdd))
                {
                    rx_stack.data_pt = 0;
                    rx_stack.lock_flag = LOCK;
                }
            }
        }
        USART_ClearITPendingBit(USART1, USART_IT_RXNE);//清除接受中断标志
    }
}
  • 将接收到的数据流进行解析,用灯的亮灭将控制命令现实化
void ptr_handle(u8 *stack)
{
    u8 *stack_pt;
    stack_pt = stack;
    if(*stack_pt == 0xff)
    {
        key0.key_change_bit = CHGE_IN;
        if((key0.led_on_off % 2) == 1)
        {
            
        }
        else
        {
            key0.led_on_off = key0.led_on_off >=3 ? 0 : key0.led_on_off + 1;
        }
    }
    else
    {
        key0.key_change_bit = CHGE_IN;
        if((key0.led_on_off % 2) == 1)
        {
            key0.led_on_off = key0.led_on_off >=3 ? 0 : key0.led_on_off + 1;
        }
        else
        {
          
        }
    }
    stack_pt ++;
    if(*stack_pt == 0xff)
    {
        key1.key_change_bit = CHGE_IN;
        if((key1.led_on_off % 2) == 1)
        {
            
        }
        else
        {
            key1.led_on_off = key1.led_on_off >=3 ? 0 : key1.led_on_off + 1;
        }
    }
    else
    {
        key1.key_change_bit = CHGE_IN;
        if((key1.led_on_off % 2) == 1)
        {
            key1.led_on_off = key1.led_on_off >=3 ? 0 : key1.led_on_off + 1;
        }
        else
        {
          
        }
    }
    stack_pt ++;
    if(*stack_pt == 0xff)
    {
        key2.key_change_bit = CHGE_IN;
        if((key2.led_on_off % 2) == 1)
        {
            
        }
        else
        {
            key2.led_on_off = key2.led_on_off >=3 ? 0 : key2.led_on_off + 1;
        }
    }
    else
    {
        key2.key_change_bit = CHGE_IN;
        if((key2.led_on_off % 2) == 1)
        {
            key2.led_on_off = key2.led_on_off >=3 ? 0 : key2.led_on_off + 1;
        }
        else
        {
          
        }
    }
    stack_pt ++;
    if(*stack_pt == 0xff)
    {
        key3.key_change_bit = CHGE_IN;
        if((key3.led_on_off % 2) == 1)
        {
            
        }
        else
        {
            key3.led_on_off = key3.led_on_off >=3 ? 0 : key3.led_on_off + 1;
        }
    }
    else
    {
        key3.key_change_bit = CHGE_IN;
        if((key3.led_on_off % 2) == 1)
        {
            key3.led_on_off = key3.led_on_off >=3 ? 0 : key3.led_on_off + 1;
        }
        else
        {
          
        }
    }
    rx_stack.lock_flag = UNLOCK;
}
  • 主函数要做的,就是循环判断是否有灯的状态需要改变,每次接收到上位机发来的命令后把当前的状态发送到上位机
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "timer.h"
#include "gpio_config.h"
#include "usart.h"
int main()
{
    SystemInit();                //初始化系统,系统时钟设定为72MHz
    systick_init(72);            //配置systick,中断时间设置为72000/72000000 = 1us
    led_gpio_init();
    usart_config();
    tx_stack_init();
    while(1)
    {
        if (key0.key_change_bit == CHGE_IN)
        {
            if((key0.led_on_off % 2) == 1)
            {
                LED0_ON;                      //打开LED0
                tx_stack.data[0] = 0xff;
            }
            else
            {
                LED0_OFF;                     //关闭LED0
                tx_stack.data[0] = 0x00;
            }
            key0.key_change_bit = NO_CHGE;
        }
        if (key1.key_change_bit == CHGE_IN)
        {
            if((key1.led_on_off % 2) == 1)
            {
                LED1_ON;                      //打开LED1
                tx_stack.data[1] = 0xff;
            }
            else
            {
                LED1_OFF;                     //关闭LED1
                tx_stack.data[1] = 0x00;
            }
            key1.key_change_bit = NO_CHGE;
        }
        if (key2.key_change_bit == CHGE_IN)
        {
            if((key2.led_on_off % 2) == 1)
            {
                LED2_ON;                      //打开LED2
                tx_stack.data[2] = 0xff;
            }
            else
            {
                LED2_OFF;                     //关闭LED2
                tx_stack.data[2] = 0x00;
            }
            key2.key_change_bit = NO_CHGE;
        }
        if (key3.key_change_bit == CHGE_IN)
        {
            if((key3.led_on_off % 2) == 1)
            {
                LED3_ON;                      //打开LED3
                tx_stack.data[3] = 0xff;
            }
            else
            {
                LED3_OFF;                     //关闭LED3
                tx_stack.data[3] = 0x00;
            }
            key3.key_change_bit = NO_CHGE;
            usart_senddata();
        }
        if(rx_stack.lock_flag == LOCK)
        {
            ptr_handle(rx_stack.data);       //运行协议解析函数
        }
    }
}

七、串口打印,重定向printf函数


  • 使用printf函数,需要包含其头文件stdio.h,即标准输入输出头文件,std是standard的缩写,是标准的意思;i是input,输入;o是output,输出;h是head,头,头文件的意思。
    这里写图片描述

  • 然后在usart,c中编写字符写入函数,将格式化后的字符串依次写入到发送总线上

int fputc(int ch, FILE *f)
{
    while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET)
    {
    }
    USART_SendData(USART1, (uint8_t) ch);
    return ch;
}
  • 在主函数中调用printf函数,实现格式化输出
#include "stm32f10x.h"
#include "stm32f10x_gpio.h"
#include "timer.h"
#include "usart.h"
int main()
{
    SystemInit();                //初始化系统,系统时钟设定为72MHz
    systick_init(72);            //配置systick,中断时间设置为72000/72000000 = 1us
    led_gpio_init();
    usart_config();
    tx_stack_init();
    while(1)
    {
        printf("Hello World!\n%d\n\n", 12345);
        delay_ms(100);
    }
}

这里写图片描述

  • 再来一个

这里写图片描述


代码链接

回到顶部

### 回答1: stm32f103是一款32位的单片机芯片,常用于嵌入式系统物联网设备中。在stm32f103的串口通信功能中,通过串口可以实现与上位机通信串口通信协议是指不同设备之间传递数据所遵循的一种规定约定。对于stm32f103与上位机通信,常用的协议有UART、RS232、RS485等。 UART(通用异步收发传输)是一种常用的串口通信协议,在stm32f103中有多个串口接口可以用于UART通信。UART通过发送接收数据帧来实现通信。通常,上位机会发送一帧数据给stm32f103,然后接收单片机返回的数据。 RS232是一种常见的串口通信协议,使用基于电压的信号进行通信。RS232在物理层电气层面上定义了通信规范,可以在较长距离上进行通信。 RS485是一种多点通信串口通信协议,常用于多个设备之间的通信。RS485比RS232更适合多个设备之间的通信,可以在1200米的距离上进行通信。 当stm32f103与上位机进行通信时,通信双方需要事先约定好使用的通信协议,包括数据传输的格式、通信速率、数据位数、校验位、停止位等参数的设置。 总之,stm32f103通过串口上位机进行通信时,需要根据具体需求选择合适的串口通信协议,并在通信过程中遵循相应的协议规范参数设置。这样才能确保通信的稳定可靠。 ### 回答2: STM32F103是一款32位的ARM Cortex-M3处理器,具有很多功能丰富的外设模块,其中包括串口USART)模块,用于与上位机进行通信串口通信协议是一种用于在通信设备之间发送接收数据的规则集合。为使STM32F103与上位机进行通信,我们需要理解如何使用正确的串口通信协议。 首先,我们需要选择适当的串口通信模式参数设置。STM32F103的串口模块支持多种通信模式,包括UART、USART、LINIrDA。在选择模式时,需要考虑通信的要求上位机的支持能力。 然后,我们需要配置串口的参数,包括波特率、数据位、停止位校验位等。这些参数必须与上位机的配置相匹配,以确保数据可以正确地传输解析。 在STM32F103上编程时,我们可以使用相关的库函数或驱动程序来配置串口并实现通信。例如,在HAL库中,可以使用以下函数进行串口配置操作: 1. `HAL_UART_Init()`:用于初始化串口模块。 2. `HAL_UART_Transmit()`:用于向上位机发送数据。 3. `HAL_UART_Receive()`:用于接收上位机发送的数据。 此外,我们还需要定义一种上位机通信协议来组织数据的传输解析。例如,可以使用常见的协议,如ASCII、Modbus或自定义协议。其中,ASCII协议将数据以ASCII码的形式发送解析,而Modbus协议是一种通用的串行通信协议,支持多种数据类型功能码。自定义协议可以根据特定的需求来定义数据传输的格式解析规则。 最后,在STM32F103上编写完整的通信程序时,需要根据协议规范实现数据的解析处理逻辑,以实现与上位机的稳定通信。 总结来说,实现STM32F103与上位机通信涉及选择适当的串口模式参数,配置串口及相关库函数的调用,定义上位机通信协议以及编写数据处理逻辑。通过合理的设计编程,可以实现可靠的STM32F103与上位机通信。 ### 回答3: STM32F103是一款高性能的单片机,具有多个串口接口可以用于与上位机进行通信。在与上位机通信时,需要确定通信协议,以便双方能够正确地交换数据。 首先,STM32F103的串口通信协议可以选择使用UART(通用异步收发器)协议。UART协议是一种串行通信协议,通过一条数据线一条时钟线来实现数据的传输。在使用UART协议时,需注意以下几点: 1. 波特率:双方需要约定好相同的波特率,波特率决定了数据传输的速率。常用的波特率有9600、115200等。 2. 数据位:可以选择使用7位或8位数据位。一般情况下,8位数据位使用更为普遍。 3. 停止位:可以选择使用1位或2位停止位。一般情况下,使用1位停止位即可。 4. 校验位:可选择使用奇校验、偶校验或无校验。校验位用于检测数据传输过程中的错误。 5. 流控制:可以选择使用硬件流控制(如RTS/CTS)或软件流控制(如XON/XOFF)来控制数据的传输速度数据的流动。 上位机通常通过串口调试助手等工具与STM32F103进行通信。具体的通信协议可以根据需要进行约定,例如可以约定一条命令由特定的起始字节、命令内容校验位组成,从而实现双方的数据交换通信。 总之,STM32F103通过串口可以与上位机进行通信通信协议的选择以及具体通信规则需要双方协商确定。以上是关于STM32F103串口上位机通信协议的说明。
评论 27
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dwgan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值