串口通信协议

本章我们来给大家介绍如何使用DMA+空闲中断的方式来与stm32进行通讯.通过本章的学习大家可以掌握如下知识点:

  1. 理解DMA
  2. 掌握DMA的通信
  3. 掌握空闲中断
  4. 掌握串口的高效传输处理
  5. 掌握自定义串口通信协议

DMA

在这张图中Cortex-M3是核心,DMA,FALSH,SRAM,APB1,APB2这些相当于是芯片的外设.

DMA: 直接存储器存取(Direct Memory Access)

SRAM: 静态随机存取存储器,用于存放程序运行时的变量

FLASH: 用来存储程序代码

APB1: 负责DA,USB,SPI,I2C,CAN,串口2345,普通TIM ,APB1支持低速状态下的工作

APB2: 负责AD,I/O,高级TIM,串口1. APB2支持高速状态下的工作。

这里我们主要给大家介绍DMA的作用.在这个里面,Cortext-m3是核心芯片,它主要负责整个程序的核心逻辑,而DMA是干嘛用的呢?

举个我们生活的例子来说, 我每天都专心于写bug,简直程序于代码无法自拔.

如果这个是否还要我去收快递,发快递,我觉得我真的忙不过来.

任何事物都有它存在的意义,这个时候DMA就应运而生了.DMA相当于是数据的搬运工,它负责收发数据.怎么收发呢? 我们需要在Cortex-m3和DMA之间建立一个缓冲区,Cortex-m3要发数据的话,可以把数据先放到缓冲区中,Cortex-m3要读数据的话,可以直接去缓冲区中获取即可.

这个缓冲区非常类似于我们黑马程序员的快递收发室,而DMA就类似于快递员,它专门负责收发,当我们需要发快递的时候,我们把数据放到收发室,当我们要取数据的时候,我们去收发室取就可以了.

至于什么时候,能停下手中的活,中断,去取数据,就要看快递员给我们发的取件短信啦.

好啦,下面我们就先来配置一下DMA功能

  • 首先我们点击add 按照如下图添加一个读的功能

  • 点击add添加一个发的功能

  • 再次检查中断是否已经开启

空闲中断

空闲中断指的是数据帧在接受的过程中,如果一个字节的时间内,没有接收到新的数据,就会产生一次空闲中断.

要想使用空闲中断,我们先要开启它

1
 __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

然后调用DMA的方式进行数据的接收

1
 HAL_UART_Receive_DMA(&huart1, (uint8_t*)uart_rx_buff, BUFFER_SIZE);

我们还需要在stm32f1xx_it.c 文件中的void USART1_IRQHandler(void)函数中执行如下逻辑:

  1. 当前是否产生了空闲中断
  2. 计算获取到的数据帧长度
  3. 清除空闲中断的标志,防止多次进入空闲中断处理函数
  4. 停止DMA传输功能
  5. 处理本次接收的数据
  6. 处理完成之后重新初始化缓冲区
  7. 重新启动DMA功能
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
void common_uart_idle_handle(UART_HandleTypeDef* huart1){

    if(USART1 == huart1->Instance)
    {   // 判断是否是空闲中断
        if (RESET != __HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) {
            // 计算接收到的数据长度
            uint32_t data_length = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
            __HAL_UART_CLEAR_IDLEFLAG(huart1);
            // 停止本次DMA传输
            HAL_UART_DMAStop(huart1);
            // 将数据丢给外部去处理
            while (HAL_UART_Receive_DMA(huart1, uart_rx_buff, BUFFER_SIZE) != HAL_OK) {
                huart1->RxState = HAL_UART_STATE_READY;
                __HAL_UNLOCK(huart1);
            }

            // 数据处理的回调
            common_uart_idle_callback(uart_rx_buff,data_length);

        }
    }
}

示例代码

导入common_uart.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//
// Created by Kaijun on 2020/6/8.
//

#ifndef HEIMAROBOT4WD_COMMON_UART_H
#define HEIMAROBOT4WD_COMMON_UART_H

#ifdef __cplusplus
extern "C" {
#endif
#include "usart.h"
#include "stm32f1xx_hal.h"


extern DMA_HandleTypeDef hdma_usart1_rx;
extern UART_HandleTypeDef huart1;


void common_uart_init();

void common_uart_idle_handle(UART_HandleTypeDef *huart1);

void common_uart_send(uint8_t *data, uint16_t size);

__weak void common_uart_idle_callback(uint8_t receive_buf[],uint16_t receive_len);


#ifdef __cplusplus
}
#endif

#endif //HEIMAROBOT4WD_COMMON_UART_H

导入common_uart.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
//
// Created by Kaijun on 2020/6/8.
//
#include "common_uart.h"
#include <string.h>
#include <stdio.h>
#include "stm32f1xx_hal.h"

// 定义缓冲区的大小
const uint32_t BUFFER_SIZE = 255;
// 声明缓冲区
static uint8_t uart_rx_buff[255];
/**
 * 串口的初始化操作
 */
void common_uart_init(){
    // 开启DMA接收数据
    HAL_UART_Receive_DMA(&huart1, (uint8_t*)uart_rx_buff, BUFFER_SIZE);
    // 开启空闲中断处理
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
    // 开启通信异常处理
    __HAL_UART_ENABLE_IT(&huart1,UART_IT_ERR);
}

/**
 * 需要将这个函数放到stm32f1xx_it.c 文件中的void USART1_IRQHandler(void)函数中调用
 * @param huart1
 */
void common_uart_idle_handle(UART_HandleTypeDef* huart1){

    if(USART1 == huart1->Instance)
    {   // 判断是否是空闲中断
        if (RESET != __HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) {
            // 计算接收到的数据长度
            uint32_t data_length = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
            __HAL_UART_CLEAR_IDLEFLAG(huart1);
            // 停止本次DMA传输
            HAL_UART_DMAStop(huart1);
            // 将数据丢给外部去处理
            while (HAL_UART_Receive_DMA(huart1, uart_rx_buff, BUFFER_SIZE) != HAL_OK) {
                huart1->RxState = HAL_UART_STATE_READY;
                __HAL_UNLOCK(huart1);
            }

            // 数据处理的回调
            common_uart_idle_callback(uart_rx_buff,data_length);
        }
    }
}

/**
 * 将数据使用DMA的方式发送出去
 * @param data
 * @param size
 */
void common_uart_send(uint8_t* data ,uint16_t size){
    // 直到DMA空闲,才进行数据的发送
    while (HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX_RX);

    while (HAL_UART_Transmit_DMA(&huart1, data, size) != HAL_OK);
}


/* 中断错误处理函数,在此处理overrun错误 */
void HAL_UART_ErrorCallback1(UART_HandleTypeDef *huart)
{
    if(__HAL_UART_GET_FLAG(huart,UART_FLAG_PE) != RESET)
    {
        __HAL_UART_CLEAR_OREFLAG(huart);
    }

    if(__HAL_UART_GET_FLAG(huart,UART_FLAG_FE) != RESET)
    {
        __HAL_UART_CLEAR_FEFLAG(huart);
    }
    if(__HAL_UART_GET_FLAG(huart,UART_FLAG_NE) != RESET)
    {
        __HAL_UART_CLEAR_NEFLAG(huart);
    }
    if(__HAL_UART_GET_FLAG(huart,UART_FLAG_ORE) != RESET)
    {
        __HAL_UART_CLEAR_OREFLAG(huart);
    }

    common_uart_init();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// 定义缓冲区的大小
const uint32_t BUFFER_SIZE = 255;
// 声明缓冲区
static uint8_t uart_rx_buff[255];
/**
 * 串口的初始化操作
 */
void common_uart_init(){
    // 开启DMA接收数据
    HAL_UART_Receive_DMA(&huart1, (uint8_t*)uart_rx_buff, BUFFER_SIZE);
    // 开启空闲中断处理
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
    // 开启通信异常处理
    __HAL_UART_ENABLE_IT(&huart1,UART_IT_ERR);
}

/**
 * 需要将这个函数放到stm32f1xx_it.c 文件中的void USART1_IRQHandler(void)函数中调用
 * @param huart1
 */
void common_uart_idle_handle(UART_HandleTypeDef* huart1){

    if(USART1 == huart1->Instance)
    {   // 判断是否是空闲中断
        if(RESET != __HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE))
        {
            // 计算接收到的数据长度
            uint8_t data_length  = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
            // 清除空闲中断标志(否则会一直不断进入中断)
            __HAL_UART_CLEAR_IDLEFLAG(huart1);
            // 停止本次DMA传输
            HAL_UART_DMAStop(huart1);

            // 将数据丢给外部去处理
            common_uart_idle_callback(uart_rx_buff,data_length);

            // 清零接收缓冲区
            memset(uart_rx_buff,0,data_length);
            // 重启开始DMA传输 每次255字节数据
            HAL_UART_Receive_DMA(huart1, (uint8_t*)uart_rx_buff, BUFFER_SIZE);
        }
    }
}

/**
 * 将数据使用DMA的方式发送出去
 * @param data
 * @param size
 */
void common_uart_send(uint8_t* data ,uint16_t size){
    // 直到DMA空闲,才进行数据的发送
    while (HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX_RX){}
    HAL_UART_Transmit_DMA(&huart1,data, size);
}


/* 中断错误处理函数,在此处理overrun错误 */
void HAL_UART_ErrorCallback1(UART_HandleTypeDef *huart)
{
    if(__HAL_UART_GET_FLAG(huart,UART_FLAG_PE) != RESET)
    {
        __HAL_UART_CLEAR_OREFLAG(huart);
    }

    if(__HAL_UART_GET_FLAG(huart,UART_FLAG_FE) != RESET)
    {
        __HAL_UART_CLEAR_FEFLAG(huart);
    }
    if(__HAL_UART_GET_FLAG(huart,UART_FLAG_NE) != RESET)
    {
        __HAL_UART_CLEAR_NEFLAG(huart);
    }
    if(__HAL_UART_GET_FLAG(huart,UART_FLAG_ORE) != RESET)
    {
        __HAL_UART_CLEAR_OREFLAG(huart);
    }

    common_uart_init();
}

自定义协议

我们需要将下位机STM32中的一些信息发送给其它设备使用, 例如,温度,3轴加速度,3轴角速度,3轴地磁传感器,以及小车速度与线速度等等信息发送出去.这么多的信息我们以怎样的一种格式对外进行发送呢?

这就需要我们来指定一种协议,或者理解为一种格式.

例如下面这段代码,就是我所设计的一个协议, 它由两个帧头用于找到协议的开始, 有协议的类型,有协议的类型,以及我们想对外发布的数据.

协议的制定完全由我们自己决定, 你希望告诉别人下位机哪些信息,就对外发送哪些信息就好了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#define FLAG_HEAD0  0xce
#define FLAG_HEAD1  0xfa
#define FLAG_TAIL   0xaa

#pragma pack(1) // 数据按1字节对齐
typedef struct TxProtocol{
    uint8_t head0;
    uint8_t head1;
    uint8_t type;
    uint8_t len;
    short temprature;

    short ax;
    short ay;
    short az;

    short gx;
    short gy;
    short gz;

    short mx;
    short my;
    short mz;

    short v;
    short w;
};

声明完成之后,我们只需要往这个里面填充数据,然后调用已封装的API发送即可

1
2
3
4
5
truct TxProtocol protocol;
protocol.head0 = FLAG_HEAD0;
protocol.head1 = FLAG_HEAD1;
...............
common_uart_send((uint8_t*)&protocol, sizeof(protocol))

解析协议

在这一小节,我们主要来学习如何解析串口发送过来的数据协议.

关于数据协议解析的步骤如下:

  1. 读取数据缓冲区和接收到的数据长度
  2. 找到数据的起始标志位
  3. 因为当前情况下,我们是固定长度的协议,所以直接从起始标志位向后读取协议长度数据
  4. 判断数据结尾标志位是否正确
  5. 如果上述都没有问题,根据协议类型做出相应的处理

示例代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void common_uart_idle_callback(uint8_t receive_buf[],uint16_t receive_len){

    uint8_t i = 0;

    while(i < receive_len-2){
        // 找到head0 + head1
        if(receive_buf[i] == FLAG_HEAD0){
            if(receive_buf[i+1] == FLAG_HEAD1){
                // 说明匹配到了帧头
                if(receive_buf[i+2] == 0x04){
                    //类型匹配成功
                    //判断数据数据长度是否足够
                    if(i + receive_buf[i+3] < receive_len){
                        uint8_t v_low = receive_buf[i+4];
                        uint8_t v_high = receive_buf[i+5];
                        short v = v_high<<8|v_low;
                        uint8_t w_low = receive_buf[i+6];
                        uint8_t w_high = receive_buf[i+7];
                        short w = w_high<<8|w_low;
                        HAL_GPIO_TogglePin(GPIOE,GPIO_PIN_10);
                    }
                }
            }
        }
        i++;
    }
}*

windows收发自定义协议

为了进行数据的收发,我们首先来学习一下python是如何处理C语言结构数据的. 这里有个比较重要的模块struct,它包含个我们常用的API:

  1. struct.pack(); 对数据进行打包 . 返回包装的结果
  2. struct.unpack(); 对数据进行拆包. 返回的是一个元组,即使只有一个数据也是元组

他们的工作原理其实是先将数据读取为字符串, 然后利用格式化的方式, 将数据转成python中的结构数据

下面我们给出short类型转换的示例代码:

1
2
3
4
5
6
temp = struct.unpack('h', bytearray([0xde ,0x0e]))[0]
print(temp)
print(0x0e<<8|0xde);

temp = bytearray(struct.pack('h',int(1.02*1000)));
print(temp)