STM32学习----RS232串口通讯

一、RS232相关概念

       RS ==Recommend Standard ==推荐标准;

        232==标识号,第232号;

        时间:1962年

        地点:美国

        人物:美国电子工业协会 == Electronic Industries Association ==(美国)电子工业协会

        事件:发布了一个串行通信的物理接口结合逻辑电平的规范文件,就是这个232号文件。

        

        现在关于串口通讯的叫法太多,什么RS232通讯、串口通讯、DB9通讯、UART通讯等等,其实这些称呼都跟这个额232号文件有些关系,随便怎么叫吧,理解就行。

        串行通讯,从字面意思理解就行,就是把数据串成一串发出去,比如一个字节8位,高位先发出去,那么发送顺序就是bit7--bit6--bit5--bit4--bit3--bit2--bit1--bit0。

        就某种单片机来说,比如STM32F103,它有好几个UART口,俗称串口,但是它的引脚高电平是3.3V,低电平是0V,正好满足TTL的电平标准,2.4V--5V表示逻辑1,0V--0.4V表示逻辑0。

        但是电压低了传输距离就比较短,为了传输远点,就把TTL电平转换成RS232电平,通过某种电平转换芯片,比如MAX232。RS232电平逻辑是-3V到-15V表示逻辑1,3V到15V表示逻辑0。这相当于把电平扩大,但是也只能传输十几米。

        人们为了传输更远,就用上了差分传输,比如RS422和RS485,用两根线的电压差来表示逻辑1和逻辑0,两根线的压差为2V至6V表示逻辑1,两线的压差为-2V至-6V表示逻辑0,这样就能传输1000米以上。

数据在一根线上传输,那什么时候是开始,什么时候是结束,每位数据的宽度是多少、数据有没有传输错误。那就需要约定一下,不是谁都像孙悟空一样有觉悟,头上敲三下就是让他凌晨3点过来,还是要讲清楚点好。

线顶一个空闲的状态,就是没传输数据的时候,传输线的电平逻辑是1,用单片机TTL的电平标准就是3.3V,高电平;

开始:发出1位逻辑0电平

结束:发出1位逻辑1电平

数据宽度:要定义每一位的宽度是多少,不然你发两位1我却认为是1位1,怎么办,发送和接收的双发要统一度量衡,才不会有误解。这个数据宽度就是用波特率的约定。

数据有没有传错:那就把收到的数据大家数一数,算一算,我给你发100块钱,我还告诉你是100张一块的,那你收到之后,要数一数,是不是100张,是不是100块,都对了,那就表示是我给你的。

传输一个字节数据的示意图

STM32F103单片机USART内部结构图

 这个图太大了,对新人实在是不友好,对老人也不友好,还是需要好好的梳理一下,总共就4块

1、串口引脚:发送和接收数据的外部引脚;

2、波特率发生器:产生波特率,就是为了控制数据每个bit的宽度;

3、发送与接收的控制单元:控制数据怎么发送,怎么接收;

4、发送与接收数据寄存器:单片机的发送的数据在内部怎么产生,接收的数据怎么吸收;

串口数据收发:

1、轮询收发

  发送数据

        1、声明一个UART_HandleTypeDef结构

        2、配置波特率、字符长度、停止位、校验位等

        3、UART 管脚配置:配置管脚位置和时钟

        4、UART初始化

        5、开始发送数据

       用STM32CubeMX生成代码,前面4步都自动生成好了,只需要在第五步调用发送函数就可以

以下是自动生成的配置代码

#include "usart.h"


//声明一个UART句柄结构体,应为使用串口1 ,结构体命名为huart1
UART_HandleTypeDef huart1;


//配置串口波特率、数据长度、停止位、校验位等
//调用HAL_UART_Init函数,再调用HAL_UART_MspInit函数,
//完成UART管脚和时钟的配置,以及串口初始化
void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }

}

//配置UART管脚位置和时钟
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(uartHandle->Instance==USART1)
  {

    __HAL_RCC_USART1_CLK_ENABLE();

    __HAL_RCC_GPIOA_CLK_ENABLE();

    //串口1使用GPIOA的,PIN9和PIN10
    GPIO_InitStruct.Pin = GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* 串口中断配置,轮询模式可以不用配置中断*/
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);

  }
}


//相当于还原,还原到配置之前的状态
void HAL_UART_MspDeInit(UART_HandleTypeDef* uartHandle)
{
  if(uartHandle->Instance==USART1)
  {

    __HAL_RCC_USART1_CLK_DISABLE();

    HAL_GPIO_DeInit(GPIOA, GPIO_PIN_9|GPIO_PIN_10);

    HAL_NVIC_DisableIRQ(USART1_IRQn);

  }
}

  在main文件中去完成串口数据发送

        串口发送使用函数

//huart:用哪个串口发送
//pData:被发送的数据指针
//Size:发送的数据个数
//Timeout:超时时间
HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)

        串口接收使用函数

//huart:用哪个串口接收
//pData:接收的数据存放位置
//Size:接收的数据个数
//Timeout:超时时间
HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)

收发实验放在一起比较好,本实验是单片机与PC之间进行数据传输:

PC向单片机发送5个数据 0xAA、 0x11 、0x22 、0x33 、0xBB

单片机每次接收5个数据,并判断第一个数据是不是AA,最后一个数据是不是BB;

如果判断正确,就把事先准备好的6个数据发出去;

还要把接收到的数据清空,等待下一次接收新数据。

#include "main.h"
#include "usart.h"
#include "gpio.h"

//将要发送的数据
uint8_t Tx_Buffer[6] = {0x5A, 0x01,0x02,0x03,0x04,0xA5};

//接收的数据存放位置
uint8_t Rx_Buffer[5] = {0};

void SystemClock_Config(void);

int main(void)
{

  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  MX_GPIO_Init();
  MX_USART1_UART_Init();

  while (1)
  {
    //开始接收数据
    if(HAL_UART_Receive(&huart1, Rx_Buffer, 5, 10))
    {
      //如果收到的第一个数据是AA,第五个数据是BB
      //就把要发送的数据发出去
      if((Rx_Buffer[0]==0xAA)&&(Rx_Buffer[4]==0xBB))
      {
        HAL_UART_Transmit(&huart1, Tx_Buffer, 6, 10);
        
        //也可以在收到数据的时候点个灯
        //当然,也可以根据其他的数据来做该做的事
        HAL_GPIO_TogglePin(GPIOA, LED0_Pin);
      }
      
      //把接收到的数据清空,等待下一次接收
      for(uint8_t i=0; i<5;i++)
      {
        Rx_Buffer[i] = 0;
      }
    }
  }
}

2、中断收发

//串口中断发送函数
//huart: 要发送数据的串口
//pData:要发送的数据指针
//Size:要发送的数据个数,
HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)


//串口中断接收函数
//huart: 要接收数据的串口
//pData:接收的数据存放位置
//Size:要接收的数据个数
HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

针对发送中断函数,把设置的Size个数据发送完之后,才执行中断,就可以执行回调函数了,一般数据发送完,我们也不用做什么事情,回调函数也懒得写,有需求的话就去写吧。

针对接收中断函数,也是接收到Size个数据之后才产生中断,,然后执行回调函数,可以在回调函数中做一些操作,比如判断数据的对错,以及收到数据要做的事情,一般是比较简短的事情,太复杂的事情就不要再回调函数里面做了,可以在回调函数中做个标记,然后再大循环中处理。

以下例程是中断收发数据

1、单片机设置接收中断使能,

2、等待PC发送5个数据0xAA、 0x11 、0x22 、0x33 、0xBB

3、收到5个数据后,串口接收中断触发

4、在接收回调函数中判断第一个数据和最后一个数据是否正确;

5、正确的话就用中断的方式把事先准备好的数据发给PC(本例程是准备6个数据);

6、把前面接收的数据清空,等待下一次接收(当然,接收的数据你想怎么处理就怎么处理)

7、在中断回调函数中,把接收中断重新使能,因为中断执行的过程中把中断使能关闭了。

中断代码

//将要发送的数据
extern uint8_t Tx_Buffer[6];

//接收的数据存放位置
extern uint8_t Rx_Buffer[5];
/**
  * @brief This function handles USART1 global interrupt.
  */
void USART1_IRQHandler(void)
{
  HAL_UART_IRQHandler(&huart1);
}


void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if(huart->Instance == USART1)
  {
    //只要串口接收中断产生了让灯改变一下亮灭的状态
    HAL_GPIO_TogglePin(GPIOA, LED0_Pin);
    
    //判断接收到的数据是否正确,本例程只判断首尾的数据
    if((Rx_Buffer[0]==0xAA)&&(Rx_Buffer[4]==0xBB))
    {
      //收到的数据正确,就把事先准备好的数据发出去
      HAL_UART_Transmit_IT(&huart1, Tx_Buffer, 5);
    }
    
    //把收到的数据清空,准备下一次接收
    for(uint8_t i=0; i<5;i++)
    {
      Rx_Buffer[i] = 0;
    }
  }
  
  //因为中断函数执行完之后,就把中断关闭了,需要重新开启中断
  HAL_UART_Receive_IT(&huart1, Rx_Buffer, 5);
}

main文件代码

#include "main.h"
#include "usart.h"
#include "gpio.h"

//将要发送的数据
uint8_t Tx_Buffer[6] = {0x5A, 0x01,0x02,0x03,0xA5};

//接收的数据存放位置
uint8_t Rx_Buffer[5] = {0};

void SystemClock_Config(void);

int main(void)
{

  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  MX_GPIO_Init();
  MX_USART1_UART_Init();
  
  //使能接收中断
  HAL_UART_Receive_IT(&huart1, Rx_Buffer, 5);

  while (1)
  { 
    //什么都不做
  }
}

中断跟捕猎有点像

        你设置好陷阱,等待猎物的到来,没有猎物的时候,陷阱是不会被触发的;当陷阱被触发之后,你就可以去把里面的猎物取出来了,猎物怎么处理是你的事,是放大自然,还是大孜然;当然了,此时你的陷阱已经被破坏了,如果你还要捕猎,那就把你的陷阱重新弄好。

        这种HAL库里面弄好的中断收发函数有个不好的地方,主要在串口接收,你要收到设定数量的数据才开始触发中断,比如设置收到5个数据才执行中断,第一次PC先给单片机发送4个数据,跟设定的5个数据相比,没达到触发中断的条件,那就继续等着。第二次PC接着又发送3个数据,这时候问题出现了,单片机怎么处理,当然是在第二次的数据还没发送完,只发送一个的时候,这时触发条件成熟了(达到5个数据),单片机开始中断,去处理数据。如果PC的两次发送时间间隔很久怎么办,凉拌,什么都不办了。这肯定是不行的。就像你的陷阱是想逮住5只猎物,但是只逮住了4只,然后再也没有猎物来,你就永远不能去取你的猎物,或者后面又来了3只,一共7只了,你拿走5只,还留两只等下次凑数,那这个猎人脑子肯定是有问题。

        解决这个问题的办法也是有,猎物逮一个收一个,就是来回跑有点累。代码演示一下。

1、每次接收到一个数据就中断;数据存在Rx_Data;

2、中断回调函数里面,把Rx_Data的数据转存到Rx_Buffer数组;

3、一共收到5个数据之后,就把收到的数据发出去,数据个数计数也清零,重新开始。

//串口收到的数据给Rx_Data
extern uint8_t Rx_Data;

//串口接收数据缓存,Rx_Data就是个二道贩子,最终数据还是放在Rx_Buffer
extern uint8_t Rx_Buffer[5];

//接收数据计数
uint8_t Rx_Count = 0;
/**
  * @brief This function handles USART1 global interrupt.
  */
void USART1_IRQHandler(void)
{
  HAL_UART_IRQHandler(&huart1);
}


void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{

  if(huart->Instance == USART1)
  {
    //只要串口接收中断产生了让灯改变一下亮灭的状态
    HAL_GPIO_TogglePin(GPIOA, LED0_Pin);
    
    //把串口收到的数据转存在Rx_Buffer里
    Rx_Buffer[Rx_Count++] = Rx_Data;
 
    //把收到的数据清空,准备下一次接收
    Rx_Data = 0;
    
    //判断是不是收到了5个数据
    if(Rx_Count ==5 )
    {
      //收到了5个数据,就把收到的数据发出去
      HAL_UART_Transmit_IT(&huart1, Rx_Buffer, 5);
      
      //收到5个数据之后,再重新计数
      Rx_Count = 0;
    }
  }
  
  //因为中断函数执行完之后,就把中断关闭了,需要重新开启中断
  HAL_UART_Receive_IT(&huart1, &Rx_Data, 1);
}

        上面每次收一个数据就中断的情况,在数据少的时候用用还可以,在大项目里面,数据多的话,最好还是别用,不然单片机就一直在中断了,不要干别的事情了。就好比你在干几个亿的项目,隔几秒钟就有人来汇报一下,你是什么心态。

        那肯定有解决方案的,为了解决大量的数据传送,不要想着轮询的方式了,轮询是最傻瓜的工作方式,在控制上想都别想,问题太多,简单项目用用还可以,比如打印信息。那可不可以等数据都发送完了,通讯线空下来的时候给我个中断,通知我来处理数据。就像你在跟客户讨论需求的时候,你不会客户提一个需求你就去干,再接等客户提下一个需求,再继续干,那人就废了,让客户一口气把需求提完,等客户安静下来,我再来处理那一堆的需求。STM32单片机就可以这样。


//huart:那个串口收数据
//pData:收到的数据存在哪里
//Size:收多少个数据
HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)

测试例程

1、PC给单片机发送10个数据,随便哪10个;

2、单片机通过空闲中断的方式接收;

3、接收到之后把数据拷贝出来;

4、把数据通过中断发出去

//将要发送的数据
extern uint8_t Tx_Buffer[6];

//接收的数据存放位置
extern uint8_t Rx_Buffer[20];

uint8_t RX_BUFFER[20];
/**
  * @brief This function handles USART1 global interrupt.
  */
void USART1_IRQHandler(void)
{
  HAL_UART_IRQHandler(&huart1);
}

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
  //if((huart->Instance == USART1)&&(Size==10))
  if(huart->Instance == USART1)
  {
    //只要串口接收中断产生了让灯改变一下亮灭的状态
    HAL_GPIO_TogglePin(GPIOA, LED0_Pin);
    
    //把串口收到的数据转到RX_BUFFER里
    memcpy(RX_BUFFER,Rx_Buffer,sizeof(Rx_Buffer) );
    //收到的数据正确,就把事先准备好的数据发出去
    HAL_UART_Transmit_IT(&huart1, RX_BUFFER, 10);

    
    //把收到的数据清空,准备下一次接收
    for(uint8_t i=0; i<20;i++)
    {
      Rx_Buffer[i] = 0;
    }
  }
  
  //因为中断函数执行完之后,就把中断关闭了,需要重新开启中断
  HAL_UARTEx_ReceiveToIdle_IT(&huart1, Rx_Buffer, 10);
}

还有DMA收发数据,放在DMA专题那边说。

  • 7
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值