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

2023-11-18

目的

在前面文章 《使用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接收 这个操作经常失败,这就比较尴尬了,想好好用还得多处理一下。

总结

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

使用HAL库开发STM32:UART进阶使用 的相关文章

随机推荐

  • 面试顺序问题:用数学建模优化生产与服务运作中的管理问题

    例题 有 4 名同学到一家公司参加三个阶段的面试 公司要求每个同学都必须首先 找公司秘书初试 然后到部门主管处复试 最后到经理处参加面试 并且不允许插队 即 在任何一个阶段 4 名同学的顺序是一样的 由于 4 名同学的专业背景不同 所以每人
  • Spring知识体系

    Spring知识体系 一 基本介绍 1 Spring全家桶 Spring SpringMVC Spring Boot Spring Cloud 其中Spring是其他的基础 2 Spring 出现的目的是为了解决企业级开发的难度 减轻对项目
  • ARouter在kotlin中使用,传递Int Long Double类型的参数空指针异常

    ARouter在kotlin中使用 传递Int Long Double类型的参数由A activity传递到 B activity 在B Activity接收时出现空指针异常 java lang NullPointerException A
  • clickhouse insert的数据结构

    clickhouse insert大致分为两部分 1 sql语句部分 insert into table f1 f2 2 数据部分 数据部分又分为3部分 头 数据 尾 数据头 数据块1 数据块2 数据块N 数据尾 从中可以看出 每个inse
  • Mybatis对数据的增删改查

    文章目录 创建sql的映射文件 增加 插入数据 修改 删除 查找 向数据库参数传递 简单参数 多个参数 传入对象 使用map传递 Mybatis的基本增删改查总的代码演示 创建sql的映射文件
  • 在 esp32 上运行 lvgl + freetype

    前言 最近有个需求 如何在 esp32 上运行 lvgl freetype 这个想法的难点是 freetype 的环境搭建 我想将其做得非常简单 最好的办法是做成组件来使用 所以我将 freetype 的相关依赖做成了 esp idf 组件
  • JS基础 预编译 与 AO对象

    预编译发生在函数执行的前一刻 预编译在函数执行的前一刻完成 function pre a console log a var a 100 console log a console log b var b function console
  • 将代码上传到指定仓库的步骤(git指令)

    1 git status 查看状态 2 git branch 查看目前所在的分支 3 git add 将内容从工作目录添加到暂存区 有的时候git add 不起作用 这时可以用 git add A 4 git commit m v1 更新代
  • 如何在CSDN博客添加友情链接

    如何在CSDN博客添加友情链接 每次看到别人的CSDN博客左侧都能够加上友情链接 我发现没有直接制定的 所以经过研究 我发现可以这样来添加友情链接 首先 进入博客设置 找到博客栏目 在里面新建一个栏目 名字随便 我这叫BLOG推荐 然后把下
  • 分布式锁工具 Redisson,贼香!!

    一 Redisson概述 二 分布式锁 三 Redisson分布式锁 四 RLock 五 公平锁 六 总结 一 Redisson概述 什么是Redisson Redisson是一个在Redis的基础上实现的Java驻内存数据网格 In Me
  • 攻防世界 web篇(一)

    攻防世界 web篇 一 inget fileclude easyupload fileinclude very easy sql 攻防世界 是一群信息安全大咖共同研究的答题 竞赛 以游戏方式结合的一款新型学习平台 融入多种场景在线题型 集实
  • 渗透测试——报错注入

    1 报错注入原理 由于后台没有对数据库的信息做过滤 会输出到前台显示 那么我们就可以通过制造报错函数 将查询语句带入到数据库中 以报错信息显示出来 2 报错注入漏洞产生的条件 1 参数用户可控 前端传入的参数内容由用户控制 2 参数带入数据
  • ArcGISMapsSDK for UnrealEngine_Beta2_00

    ArcGISMapsSDK for UnrealEngine Beta2 00 Prepare 1 Esri Community 2 All Communities 3 ArcGIS Maps SDK for Unreal Engine 4
  • Swing可视化设计:在IntelliJ IDEA中安装JFormDesigner教程

    Swing可视化设计 在IntelliJ IDEA中安装JFormDesigner教程 前言 最近课设大多需要gui设计 排除自学的情况 大部分同学都只接触过swing设计gui swing可视化插件将大大提高工作效率 这里提高破解版使用方
  • 数据结构与算法【Java】02---链表

    前言 数据 data 结构 structure 是一门 研究组织数据方式的学科 有了编程语言也就有了数据结构 学好数据结构才可以编写出更加漂亮 更加有效率的代码 要学习好数据结构就要多多考虑如何将生活中遇到的问题 用程序去实现解决 程序 数
  • 微信小程序申请 wx.getLocation 接口 审核一直不通过

    项目需要通过微信的 getLocation 获取本地的位置信息 经纬度 但是提交很多次审核都不通过 最后通过写了个项目里用不到的 导航功能 截图录屏才通过了审核 以下申请文案及配图仅供参考 因当前业务涉及就近医院挂号取号业务 需获取用户地理
  • python 使用pip install 手动安装本地包的方法

    Installing pystan manually fixed the issue otherwise it would just hang forever GitHub git clone https github com facebo
  • 【js中的单元测试】【30秒快速入门】

    什么是单元测试 测试是一种验证我们代码是否可以按预期工作的方法 换种说法就是写些代码来验证一段代码的正确性 被测试的对象可以是样式 功能 流程 组件等 单元测试是对软件中最小可测试单元进行检测和验证 单元测试能有效的提升工作效率 1 能监测
  • 常见swap()函数实现和细节讲解

    前言说明 swap 函数的作用是进行交换传入的两个值 本文都以整形int举例说明 且用C语言描述 常见的swap的实现方式有三种 格外一个空间的临时存放发 无格外空间的位运算异或法 无额外空间的加减法 主程序框架 include
  • 使用HAL库开发STM32:UART进阶使用

    文章目录 目的 发送处理 存在的问题 解决方法 个人常用处理方式 数据接收与解析 数据接收 数据解析 对于HAL库的吐槽 总结 目的 在前面文章 使用HAL库开发STM32 UART基础使用 中介绍的UART的基础使用 基础使用非常简单 不