串口通信协议
本章我们来给大家介绍如何使用DMA+空闲中断的方式来与stm32进行通讯.通过本章的学习大家可以掌握如下知识点:
- 理解DMA
- 掌握DMA的通信
- 掌握空闲中断
- 掌握串口的高效传输处理
- 掌握自定义串口通信协议
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功能
空闲中断
空闲中断指的是数据帧在接受的过程中,如果一个字节的时间内,没有接收到新的数据,就会产生一次空闲中断.
要想使用空闲中断,我们先要开启它
| __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
|
然后调用DMA的方式进行数据的接收
| HAL_UART_Receive_DMA(&huart1, (uint8_t*)uart_rx_buff, BUFFER_SIZE);
|
我们还需要在stm32f1xx_it.c 文件中的void USART1_IRQHandler(void)函数中执行如下逻辑:
- 当前是否产生了空闲中断
- 计算获取到的数据帧长度
- 清除空闲中断的标志,防止多次进入空闲中断处理函数
- 停止DMA传输功能
- 处理本次接收的数据
- 处理完成之后重新初始化缓冲区
- 重新启动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发送即可
| truct TxProtocol protocol;
protocol.head0 = FLAG_HEAD0;
protocol.head1 = FLAG_HEAD1;
...............
common_uart_send((uint8_t*)&protocol, sizeof(protocol))
|
解析协议
在这一小节,我们主要来学习如何解析串口发送过来的数据协议.
关于数据协议解析的步骤如下:
- 读取数据缓冲区和接收到的数据长度
- 找到数据的起始标志位
- 因为当前情况下,我们是固定长度的协议,所以直接从起始标志位向后读取协议长度数据
- 判断数据结尾标志位是否正确
- 如果上述都没有问题,根据协议类型做出相应的处理
示例代码
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:
- struct.pack(); 对数据进行打包 . 返回包装的结果
- struct.unpack(); 对数据进行拆包. 返回的是一个元组,即使只有一个数据也是元组
他们的工作原理其实是先将数据读取为字符串, 然后利用格式化的方式, 将数据转成python中的结构数据
下面我们给出short类型转换的示例代码:
| 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)
|