使用HAL库开发STM32(基于F4):UART进阶使用

目的

在前面文章 《使用HAL库开发STM32(基于F4):UART基础使用》 中介绍的UART的基础使用,基础使用非常简单,不过在实际应用过程中仅基础方法可能不是那么方便,还需要编写更多代码来完善使用。这篇文章将对常见的数据发送接收处理方式做个演示。

注1:在STM32开发时因为默认分配的堆内存不大,我个人比起使用malloc或是new方法申请内存,更多的喜欢把数据放在静态区域;(这样编译的时候也可以看到内存占用情况)
注2:本文中有些功能使用C++作为演示,实际使用中也可以改为纯C代码实现;

发送处理

存在的问题

前面文章中讲到我们通常使用非阻塞方式来收发数据,这里就产生了一个问题,如下代码:

void fun(void)
{
    uint8_t data[256] = {0};
    // TODO
    HAL_UART_Transmit_DMA(&huart1, data, 256); //将data数组内容通过UART发送
}

int main(void)
{
    Init();
    fun();
    while (1)
    {
    }
}

上面代码中fun函数里声明了一个数组,然后通过UART以非阻塞的方式进行发送,在调用发送函数后紧接着会立即退出fun函数,dara数组内存会被释放,但这个时候发送还在进行,这里就有可能发生发生数据不对或是程序跑飞等问题。
此外还有一个问题是同一个串口如果以非阻塞方式发送数据,在数据还未发送完的时候再次调用发送函数就会出错。

解决方法

对于第一个问题解决方法很简单,把data声明放到外面就成:

uint8_t data[256] = {0};
void fun(void)
{
	// TODO
    HAL_UART_Transmit_DMA(&huart1, data, 256); //将data数组内容通过UART发送
}

或者用动态申请的方式:

uint8_t *data;
void fun(void)
{
    data = (uint8_t*)malloc(256); //申请内存
    // TODO
    HAL_UART_Transmit_DMA(&huart1, data, 256);
}

void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
    if(huart == &huart1)
    {
        free(data); //发送完成后释放内存
    }
}

对于第二个问题解决方法也不麻烦,通过观察可以知道HAL库的串口发送函数传入参数除了串口对象以外还有数据地址和长度,只要把数据地址和长度保存到下来,然后一个一发送即可,可以参考下节。

个人常用处理方式

下面是我个人对于串口发送常用的处理方式:
在这里插入图片描述
lib_fakeheap代码如下:

#ifndef LIB_FAKEHEAP_H_
#define LIB_FAKEHEAP_H_

#include "main.h"

class LibFakeHeap {
public:
	LibFakeHeap(uint8_t *buf, size_t size);
	~LibFakeHeap(void);
	uint8_t *get(size_t size);
private:
	uint8_t *_buf;
	size_t _size;
	size_t _index;
};

#endif /* LIB_FAKEHEAP_H_ */
#include "lib_fakeheap.h"

LibFakeHeap::LibFakeHeap(uint8_t *buf, size_t size) :
		_buf(buf), _size(size), _index(0) {
}

LibFakeHeap::~LibFakeHeap(void) {
}

uint8_t *LibFakeHeap::get(size_t size) {
	if ((size == 0) || (size > _size)) {
		return nullptr;
	}
	if ((_index + size) > _size) {
		_index = size;
		return _buf;
	}
	uint8_t *tmp = _buf + _index;
	_index = (_index + size) % _size;
	return tmp;
}

代码非常简单,功能上就是一开始声明个大点的静态数组,然后使用的时候动态分配。
这个方式和malloc或是new差不多,好处是用完不用释放,缺点是所占用的内存无法它用。另外这个代码使用是基于一个前提的——单位时间内需要发送的数据最大数量是能预估的。
在使用时需要根据业务功能来估计声明的静态数组的大小,最好是单位时间内最大需求的两倍。

lib_uart发送部分代码如下:

#ifndef LIB_UART_H_
#define LIB_UART_H_

#include "main.h"

typedef struct {
	uint8_t *data;
	uint16_t size;
} LibUartTxInfo;

class LibUartTx {
public:
	LibUartTx(UART_HandleTypeDef *uart, LibUartTxInfo *queue, size_t queuesize);
	~LibUartTx(void);
	bool write(uint8_t *data, uint16_t size);
	void dmaTcHandle(UART_HandleTypeDef *uart);

private:
	UART_HandleTypeDef *_uart;
	LibUartTxInfo *_queue;
	size_t _queuesize;
	size_t _queuefront;
	size_t _queuerear;
	bool _sending;
};

#endif /* LIB_UART_H_ */
#include "lib_uart.h"

LibUartTx::LibUartTx(UART_HandleTypeDef *uart, LibUartTxInfo *queue, size_t queuesize) :
		_uart(uart), _queue(queue), _queuesize(queuesize), _queuefront(0), _queuerear(0), _sending(false) {
}

LibUartTx::~LibUartTx(void) {
}

bool LibUartTx::write(uint8_t *data, uint16_t size) {
	if ((_queuerear + 1) % _queuesize == _queuefront) {
		return false;
	}
	_queue[_queuerear].data = data;
	_queue[_queuerear].size = size;
	_queuerear = (_queuerear + 1) % _queuesize;
	if (!_sending) {
		_sending = true;
		HAL_UART_Transmit_DMA(_uart, _queue[_queuefront].data, _queue[_queuefront].size);
		_queuefront = (_queuefront + 1) % _queuesize;
	}
	return true;
}

void LibUartTx::dmaTcHandle(UART_HandleTypeDef *uart) {
	if (uart != _uart) {
		return;
	}
	if (_queuerear == _queuefront) {
		_sending = false;
		return;
	}
	HAL_UART_Transmit_DMA(_uart, _queue[_queuefront].data, _queue[_queuefront].size);
	_queuefront = (_queuefront + 1) % _queuesize;
}

上面代码思路其实就是把待发送数据的地址和长度放到一个队列里,当没有进行发送或发送完成时判断下队列内容,如果队列不为空则再次启动发送。

数据接收与解析

和发送相比UART接收到真正使用更加麻烦点,因为接收的时候会有更多不确定性,数据长度不定、数据传输出错等等各种问题。一般的串口通讯中会制定一些带有校验功能的协议,只有接收到符合协议的数据才进行响应。一般的来说数据接收可以按下面方式处理:
在这里插入图片描述

数据接收

下面是数据接收的演示:
在这里插入图片描述
上图中串口配置了中断和DMA功能,其中DMA接收部分用了循环接收方式。在 stm32f4xx_it.cpp 文件的 void USART1_IRQHandler(void) 函数中添加了空闲中断相关处理。上图中每次串口接收完成数据后会触发空闲中断,在空闲中断中调用 fun 函数把 uartrxbuf 当前的数据发回上位机。在这里的 fun 函数其实就是下文的数据解析函数,只不过这里没有进行解析而已。

lib_uart接收部分代码如下:

#ifndef LIB_UART_H_
#define LIB_UART_H_

#include "main.h"

class LibUartRx {
public:
	LibUartRx(UART_HandleTypeDef *uart, DMA_HandleTypeDef *dma, uint8_t *buf, size_t bufsize, void (*dataParse)(size_t rear));
	~LibUartRx(void);
	void listen(void);
	void uartIdleHandle(void);

private:
	UART_HandleTypeDef *_uart;
	DMA_HandleTypeDef *_dma;
	uint8_t *_buf;
	size_t _bufsize;
	void (*_dataParse)(size_t rear);
};

#endif /* LIB_UART_H_ */
#include "lib_uart.h"

LibUartRx::LibUartRx(UART_HandleTypeDef *uart, DMA_HandleTypeDef *dma, uint8_t *buf, size_t bufsize, void (*dataParse)(size_t rear)) :
		_uart(uart), _dma(dma), _buf(buf), _bufsize(bufsize), _dataParse(dataParse) {
}

LibUartRx::~LibUartRx(void) {
}

void LibUartRx::listen(void) {
	__HAL_UART_CLEAR_IDLEFLAG(_uart);
	__HAL_UART_ENABLE_IT(_uart, UART_IT_IDLE);
	HAL_UART_Receive_DMA(_uart, _buf, _bufsize);
}

void LibUartRx::uartIdleHandle(void) {
	if (__HAL_UART_GET_FLAG(_uart, UART_FLAG_IDLE)) {
		__HAL_UART_CLEAR_IDLEFLAG(_uart);
		_dataParse(_bufsize - __HAL_DMA_GET_COUNTER(_dma));
	}
}

数据解析

数据解析需要根据具体业务进行,比如我有如下通讯协议:
在这里插入图片描述
编写相应的解析函数来执行操作,先看下面演示:
在这里插入图片描述
上面演示中注册了两条指令,mcu在收到相应指令后进行了应答,如果收到无法解析为指令的数据就会滤过(演示中忘记示范了)。
lib_naisu_protocol部分代码如下:

#ifndef LIB_NAISU_PROTOCOL_H_
#define LIB_NAISU_PROTOCOL_H_

#include "main.h"

/*
 format:
 STX CMD ADDR LEN-H(data) LEN-L(data) DATA(if have) BCC
 */

#define LIB_NAISU_PROTOCOL_MAX_SIZE 16

typedef struct {
	void (*callback)(uint16_t size, uint8_t addr);
	uint8_t stx;
	uint8_t cmd;
} LibNaisuProtocolInfo;

class LibNaisuProtocol {
public:
	LibNaisuProtocol(uint8_t *buf, uint16_t bufsize);
	~LibNaisuProtocol(void);
	bool add(void (*callback)(uint16_t size, uint8_t addr), uint8_t stx, uint8_t cmd);
	void parse(size_t rear);
	uint8_t read(uint16_t offset);
	bool read(uint8_t *dest, uint16_t offset, uint16_t size);
	uint8_t calBcc(uint8_t *data, uint16_t size);

private:
	LibNaisuProtocolInfo info[LIB_NAISU_PROTOCOL_MAX_SIZE];
	size_t _attachsize;
	uint8_t *_buf;
	uint16_t _bufsize;
	uint16_t _front;
	uint16_t _rear;
	uint16_t _size;
	uint16_t _cmdsize;
};

#endif /* LIB_NAISU_PROTOCOL_H_ */
#include "lib_naisu_protocol.h"

LibNaisuProtocol::LibNaisuProtocol(uint8_t *buf, uint16_t bufsize) :
		_attachsize(0), _buf(buf), _bufsize(bufsize), _front(0), _rear(0), _size(0) {
}

LibNaisuProtocol::~LibNaisuProtocol(void) {
}

bool LibNaisuProtocol::add(void (*callback)(uint16_t size, uint8_t addr), uint8_t stx, uint8_t cmd) {
	if (_attachsize >= LIB_NAISU_PROTOCOL_MAX_SIZE) {
		return false;
	}
	info[_attachsize].callback = callback;
	info[_attachsize].stx = stx;
	info[_attachsize].cmd = cmd;
	_attachsize++;
	return true;
}

void LibNaisuProtocol::parse(size_t rear) {
	_rear = rear;
	_size = (_rear + _bufsize - _front) % _bufsize;
	loop: if (_size < 6) {
		return;
	}
	for (size_t i = 0; i < _attachsize; i++) {
		if ((_buf[_front] == info[i].stx) && (_buf[(_front + 1) % _bufsize] == info[i].cmd)) {
			uint16_t cmdsize = ((_buf[(_front + 3) % _bufsize] << 8) | _buf[(_front + 4) % _bufsize]) + 6;
			if (cmdsize <= _size) {
				uint8_t bcc = 0;
				if ((_front + cmdsize - 1) <= _bufsize) {
					bcc = calBcc(&_buf[_front], cmdsize - 1);
				} else {
					bcc = calBcc(&_buf[_front], _bufsize - _front);
					bcc = bcc ^ calBcc(_buf, cmdsize - 1 - (_bufsize - _front));
				}
				if (_buf[(_front + cmdsize - 1) % _bufsize] == bcc) {
					_cmdsize = cmdsize;
					info[i].callback(_cmdsize, _buf[(_front + 2) % _bufsize]);
					_cmdsize = 0;
					_front = (_front + cmdsize) % _bufsize;
					_size -= cmdsize;
					goto loop;
				} else {
					_front = (_front + 1) % _bufsize;
					_size--;
					goto loop;
				}
			} else if (cmdsize < _bufsize) {
				return;
			} else { // cmdsize >= _bufsize
				_front = (_front + 1) % _bufsize;
				_size--;
				goto loop;
			}
		}
	}
	_front = (_front + 1) % _bufsize;
	_size--;
	goto loop;
}

uint8_t LibNaisuProtocol::read(uint16_t offset) {
	if (offset >= _cmdsize) {
		return 0;
	}
	return _buf[(_front + offset) % _bufsize];
}

bool LibNaisuProtocol::read(uint8_t *dest, uint16_t offset, uint16_t size) {
	if ((offset + size) > _cmdsize) {
		return false;
	}
	uint16_t front = (_front + offset) % _bufsize;
	if ((front + size) <= _bufsize) {
		uint8_t *tmp = &_buf[front];
		for (uint16_t i = 0; i < size; i++) {
			dest[i] = tmp[i];
		}
	} else {
		uint8_t *tmp = &_buf[front];
		uint16_t hfsize = _bufsize - front;
		for (uint16_t i = 0; i < hfsize; i++) {
			dest[i] = tmp[i];
		}
		tmp = &dest[hfsize];
		hfsize = size - hfsize;
		for (uint16_t i = 0; i < hfsize; i++) {
			tmp[i] = _buf[i];
		}
	}
	return true;
}

uint8_t LibNaisuProtocol::calBcc(uint8_t *data, uint16_t size) {
	uint8_t bcc = data[0];
	for (uint16_t i = 1; i < size; i++) {
		bcc = bcc ^ data[i];
	}
	return bcc;
}

总结

串口是蛮常用的功能,为了使使用时更顺手花时间整点工具还是值得的。这篇文章主要是提供了一种思路,上面代码中也还有很多可以调整优化的地方。

完整的包含上文中数据发送与接收解析的例程可以从下面链接下载:
https://download.csdn.net/download/Naisu_kun/12043656

发布了66 篇原创文章 · 获赞 176 · 访问量 22万+

猜你喜欢

转载自blog.csdn.net/Naisu_kun/article/details/103455638