使用HAL库开发STM32:UART进阶使用

目的

在前面文章 《使用HAL库开发STM32: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 函数其实就是下文的数据解析函数,只不过这里没有进行解析而已。
上图演示中用的是循环接收,但后面的演示中用的是普通接收方式,因为HAL库和循环接收的思路逻辑有点冲突。下面的代码是上面演示中用到的代码,实际使用中因为逻辑上的冲突后面有些改动,最终代码可以参考本文后边给出的链接。

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));
	}
}

数据解析

数据解析需要根据具体业务进行,比如拿常见的Modbus-Rtu协议说明:
在这里插入图片描述
编写相应的解析函数来执行操作,先看下面演示(下图的演示是修复一些问题后的代码演示,具体代码可以在下面的链接中找到):
在这里插入图片描述
在这里插入图片描述
上面演示中注册了两条指令,mcu在收到相应指令后进行了应答,如果收到无法解析为指令的数据就会滤过(演示中忘记示范了)。

上面的库代码和例程演示可以在我的GitHub项目中找到:
https://github.com/NaisuXu/STM32-tool-library-based-on-HAL-and-LL

对于HAL库的吐槽

HAL库设计了一套模式,让用户可以用上各个功能,但同时也带来了一些问题。比如你只能按着它的思路来使用,不然就会可能出现各种问题,下面就是串口使用中出现的一些问题:

  • 一般情况下串口接收用DMA循环接收是非常好的一种方式,但在HAL库下就不太好用,只要一出现异常它就把串口和DMA全部关了(比如你用不匹配的波特率给串口发数据就必定出现帧错误),这在以前的STD库里面是不会有这样的情况的(错了就错了出现问题时的几个异常数据并不是啥大问题,反正接到的数据还要解析处理的,HAL库倒好干脆串口都给你关了,反应过度)。HAL库的这种设计模式有时候反而把简单的问题弄复杂了。
  • 第二的我遇到的问题是我使用空闲中断加DMA接收数据,在空闲中断中处理数据后重启串口DMA接收等待下次数据处理。就这点操作,逻辑上问题不大,在STM32F070F6P6上运行毫无问题,但同样的代码在STM32F405RGT6上就不正常工作了,空闲中断中 重启串口DMA接收 这个操作经常失败,这就比较尴尬了,想好好用还得多处理一下。

总结

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

  • 9
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Naisu Xu

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

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

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

打赏作者

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

抵扣说明:

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

余额充值