04-HAL库UART配置及协议解析设计

news2024/11/25 8:10:59

本节内容介绍

  • 1、HAL库UART 在cubemx中的配置及注意事项;
  • 2、HAL库UART详解与结构介绍;
  • 3、实现简单地UART数据收发;

源码地址:

HAL库UART在cubemx中的配置

串口原理图

串口1咱们已经用作rtt的print使用了,所以使用另外一组串口来进行串口的教程,这里一定要注意下,alios的这个板子原理图是有点问题的,标注的是串口3PA2和PA3,实际上小飞哥调了好久,最后万用表量引脚才发现是原理图标注错误,实际上是UART4,PA0和PA1

cubemx中引脚选择预配置

选择PA0、PA1,配置为串口模式,波特率什么的见图示:

开启中断,优先级可以根据自己的需求配置,本次不使用DMA,所以DMA就先不进行配置了

配置是非常简单的,就不多啰嗦了,配置完直接生成代码就OK了

HAL库串口代码详解

cubemx里面配置了一大堆,生成的应用代码主要在初始化中:

关于串口的接口是很多的,本次主要使用3个接口,发送、接收和接收回调

HAL库数据接收的设计思想是底层配置完成后,暴露给用户的是一组回调函数,用户不用关心底层实现,只需要关注应用层逻辑即可,回调函数是定义为_weak属性的接口,用户可以在应用层实现

/**
  * @brief  Rx Transfer completed callback.
  * @param  huart UART handle.
  * @retval None
  */
__weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  /* Prevent unused argument(s) compilation warning */
  UNUSED(huart);

  /* NOTE : This function should not be modified, when the callback is needed,
            the HAL_UART_RxCpltCallback can be implemented in the user file.
   */
}

发送也有对应的callback,我们只需要在callback处理我们的逻辑即可。

串口收发设计

教程不玩虚的,本章节小飞哥从实际应用出发,通过解析协议数据,顺便讲解uart的收发设计。

1、串口接收:

先来看看HAL库串口接收的接口函数,这就是使用库函数的好处,底层实现不用关心,只要会用接口就行了

/**
  * @brief Receive an amount of data in interrupt mode.
  * @note   When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
  *         the received data is handled as a set of u16. In this case, Size must indicate the number
  *         of u16 available through pData.
  * @param huart UART handle.
  * @param pData Pointer to data buffer (u8 or u16 data elements).
  * @param Size  Amount of data elements (u8 or u16) to be received.
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

如何使用这个接口接收数据呢?

从接口描述可以看到,第1个参数是我们的串口号,第2个参数数我们用于接收数据的buffer,第3个参数是数据长度,即要接受的数据量,这里我们每次仅接收一个数据即进入逻辑处理

每次取一个数据,放到rxdata的变量中

    HAL_UART_Receive_IT(&huart4, &rxdata, 1);

HAL库所有的串口是共享一个回调函数的,那么如何区分数据是来自哪一个串口的?这个逻辑可以在应用实现,区分不同的串口号,根据对应的串口号实现对应的逻辑即可

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{

  if (huart->Instance == UART4)
  {
    //rt_sem_release(sem_uart_rec);
    embedded_set_uart_rec_flag(RT_TRUE);
    embedded_set_uart_timeout_cnt(0);
    HAL_UART_Receive_IT(&huart4, &rxdata, 1);
		mb_process_frame(rxdata,CHANNEL_MODBUS);
  }
}

2、数据帧接收完成判断

通讯基本上都是不定长数据的接收,一般对于一个完整的通讯帧来说,是有长度字段的,分以下几种接收完成判断方式

  • 特殊数据格式,比如结束符,像正点原子串口教程的“回车、换行(0x0D,0x0A)”

  • 数据长度,适用已知数据长度的数据帧,根据接收到的数据长度跟数据帧里面的长度是否一致,判断接受是否完成

  • 超时判断,定时器设计一个超时机制,一定时间内没有数据进来即认为数据传输结束

  • 空闲中断,串口是有个空闲中断的,这个实现类似于超时机制

  • 也可以从软件设计实现,比如设计一个队列,取数据即可,队列中没数据即认为数据接受完成

方式有很多,本章节主要使用数据长度和定时器超时两种方式来讲解

3、串口发送

串口发送比较简单,先来看看发送接口函数,类似接收函数,只需要把我们的数据放进发送buffer,启动发送即可

/**
  * @brief Send an amount of data in blocking mode.
  * @note   When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
  *         the sent data is handled as a set of u16. In this case, Size must indicate the number
  *         of u16 provided through pData.
  * @note When FIFO mode is enabled, writing a data in the TDR register adds one
  *       data to the TXFIFO. Write operations to the TDR register are performed
  *       when TXFNF flag is set. From hardware perspective, TXFNF flag and
  *       TXE are mapped on the same bit-field.
  * @param huart   UART handle.
  * @param pData   Pointer to data buffer (u8 or u16 data elements).
  * @param Size    Amount of data elements (u8 or u16) to be sent.
  * @param Timeout Timeout duration.
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout);

数据接收及协议帧解析设计

数据接收:

基于数据长度和超时时间完成数据帧发送完成的判断:

定时器中断回调设计,实现逻辑为,当收到串口数据时,开始计时,超过100ms无数据进来,认为数据帧结束,同时释放数据接收完成的信号量,接收到接受完成的信号量之后,重置一些数据,为下一次接收做好准备

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim->Instance == TIM15)
  {
    // if(RT_EOK==rt_sem_take(sem_uart_rec,RT_WAITING_NO))
    // {
    if (embedded_get_uart_rec_flag())
    {
      /*100ms超时无数据接收*/
      if (embedded_get_uart_timeout_cnt() > 9)
      {
        embedded_set_uart_rec_flag(RT_FALSE);

        rt_sem_release(sem_uart_timeout);
      }
    }

    // }
  }
}

串口回调设计:

串口回调要实现的逻辑比较简单,主要是数据接收、解析:


void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == UART4)
  {
    //rt_sem_release(sem_uart_rec);
    embedded_set_uart_rec_flag(RT_TRUE);
    embedded_set_uart_timeout_cnt(0);
    HAL_UART_Receive_IT(&huart4, &rxdata, 1);
		process_frame(rxdata,CHANNEL_UART4);
  }
}

/协议架构/

/数据头(2字节)+数据长度(2字节,不包含数据头)+功能码+数据+校验码(CRC16-MODBUS)/

我们采用这个协议框架来解析数据,数据解析可以设计成一个简单的状态机,根据每一步决定下一步做什么

比如针对上面的协议,我们就可以分几步设计:

  • 1、解析数据头1;
  • 2、解析数据头2;
  • 3、解析数据长度;
  • 4、接收数据;
  • 5、校验数据CRC;
  • 6、调用命令回调函数;

把握好这个步骤,设计其实非常简单

先来定义一个简单的枚举,表示每一个状态:

typedef enum
{
    STATUS_HEAD1 = 0,
    STATUS_HEAD2,
    STATUS_LEN,
    STATUS_HANDLE_PROCESS
} frame_status_e;

然后封装数据解析函数:

/*协议架构*/

/**数据头(1字节)+数据长度(2字节,不包含数据头)+功能码+数据+校验码(CRC16-MODBUS)**/

#define PROTOCOL_HEAD1 0x5A
#define PROTOCOL_HEAD2 0xA5

int process_frame(const uint8_t data, const uint8_t channel)
{
    uint16_t crc = 0;
    uint16_t len = 0;

    static frame_status_e frame_status;
    static uint16_t index = 0;

    /*timeout reset the receive status*/
    if (RT_EOK == rt_sem_take(sem_uart_timeout, RT_WAITING_NO))
    {
        index = 0;
        frame_status = STATUS_HEAD1;
    }
    switch (frame_status)
    {
    case STATUS_HEAD1:
        if (data == PROTOCOL_HEAD1)
        {
            frame_status = STATUS_HEAD2;
            buffer[index++] = data;
        }
        else
        {
            frame_status = STATUS_HEAD1;
            index = 0;
        }
        break;
    case STATUS_HEAD2:
        if (data == PROTOCOL_HEAD2)
        {
            frame_status = STATUS_LEN;
            buffer[index++] = data;
        }
        else
        {
            frame_status = STATUS_HEAD1;
            index = 0;
        }
        break;
    case STATUS_LEN:
        if (data >= 0 && data <= MAX_DATA_LEN)
        {
            frame_status = STATUS_HANDLE_PROCESS;
            buffer[index++] = data;
        }
        else
        {
            frame_status = STATUS_HEAD1;
            index = 0;
        }
        break;
    case STATUS_HANDLE_PROCESS:
        buffer[index++] = data;
        len = buffer[LEN_POS];
        if (index - 3 == len)
        {
            crc = embedded_mbcrc16(buffer, index - 2);
            if (crc == (buffer[index - 1] | buffer[index - 2] << 8))
            {
                call_reg_cb(buffer, index, channel, buffer[CMD_POS]);
            }
            index = 0;
            frame_status = STATUS_HEAD1;
        }

        break;

    default:
        frame_status = STATUS_HEAD1;
        index = 0;
    }
}

对用的功能函数:

我们采用 attribute at机制的方式,将我们的回调函数注册进去:

typedef void (*uart_dispatcher_func_t)(const uint32_t, const uint8_t *, const uint32_t);
typedef struct uart_dispatcher_item
{
    union
    {
        struct
        {
            uint8_t channel;
            uint8_t cmd_id;
        };

        uint32_t magic_number;
    };

    uart_dispatcher_func_t function;

} uart_dispatcher_item_t;

#define UART_DISPATCHER_CALLBACK_REGISTER(ch, id, fn) static const uart_dispatcher_item_t uart_dis_table_##ch##_##id                         \
                                                   __attribute__((section("uart_dispatcher_table"), __used__, aligned(sizeof(void *)))) = \
                                                          {.channel = ch, .cmd_id = id, .function = fn}
int call_reg_cb(uint8_t *frame, uint8_t data_len, int channel, uint8_t cmd_id);

回调函数:

这样设计可以把驱动层,协议解析层和应用层完全分开,用户只需要注册相关的命令,实现回调即可,完全不用关心底层实现

void dispatcher_on_02_callback(const uint32_t channel, const uint8_t *data, const uint32_t data_len)
{
	const char *str = "func02 is running\r\n";
	uart_write((uint8_t *)str, rt_strlen(str), 100);
	rt_kprintf("func02 is running\r\n");
}
UART_DISPATCHER_CALLBACK_REGISTER(1, 0x02, dispatcher_on_02_callback);

void dispatcher_on_03_callback(const uint32_t channel, const uint8_t *data, const uint32_t data_len)
{
	const char *str = "func03 is running\r\n";
	uart_write((uint8_t *)str, rt_strlen(str), 100);
	rt_kprintf("func03 is running\r\n");
}
UART_DISPATCHER_CALLBACK_REGISTER(1, 0x03, dispatcher_on_03_callback);

void dispatcher_on_04_callback(const uint32_t channel, const uint8_t *data, const uint32_t data_len)
{
	const char *str = "func04 is running\r\n";
	uart_write((uint8_t *)str, rt_strlen(str), 100);
	rt_kprintf("func04 is running\r\n");
}
UART_DISPATCHER_CALLBACK_REGISTER(1, 0x04, dispatcher_on_04_callback);

void dispatcher_on_05_callback(const uint32_t channel, const uint8_t *data, const uint32_t data_len)
{
	rt_kprintf("func05 is running\r\n");
}
UART_DISPATCHER_CALLBACK_REGISTER(1, 0x05, dispatcher_on_05_callback);

void dispatcher_on_06_callback(const uint32_t channel, const uint8_t *data, const uint32_t data_len)
{
	rt_kprintf("func06 is running\r\n");
}
UART_DISPATCHER_CALLBACK_REGISTER(1, 0x06, dispatcher_on_06_callback);

测试效果

通过上面的回调函数注册,我们来测试下是不是达到预期情况:

测试是完全OK的,本次的教程到这里就结束了,代码会上传到gitee上,里面有很多不错的设计,小伙伴们可以自行下载下来看看

彩蛋: 目前固件是支持MODBUS的,对MODBUS感兴趣的小伙伴也可以下载下来看看,目前支持了03、06、16功能码,也欢迎小伙伴们提交代码进来,一起学习!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/718731.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

android studio 4.0以上隐藏调用方法参数名提示

引入&#xff1a; android studio在编辑代码的时候&#xff0c;调用函数时会接口处会自动提示参数名&#xff0c;方便代码书写时对传参命名的规范性。 可以如果代码是魂效过的&#xff0c;那会适得其反&#xff0c;l,l1,l2,i,i1,i2这样的参数名提醒反而会混淆视听。 这时候可…

图书馆机器人的应用,科技助力新趋势

随着科技的发展和智能化时代的到来&#xff0c;图书馆越来越多地引入了机器人作为服务和管理的工具。图书馆机器人可以轻松地完成多种任务&#xff0c;包括为用户提供导航服务、管理借还书、整理图书、清扫图书馆等。 首先&#xff0c;图书馆机器人可以为用户提供导航服务。在庞…

讲讲仿真软件的文件导入

仿真软件识别导入的设计文档是有区别的&#xff0c;实际的使用经历&#xff0c;ADS只是用于搭建Channel通道仿真&#xff0c;那本文以Cadence的Sigrity和Ansys的SIwave为例&#xff0c;讲讲仿真软件的文件导入。 先以Sigrity为例&#xff0c;打开软件&#xff0c;File 菜单Open…

记一次 JVM 参数调整导致 ShardingSphere-Proxy 性能下降的问题排查过程

问题现象 在性能测试中&#xff0c;分别对两个版本差异间隔一天的 ShardingSphere-Proxy 做性能测试&#xff0c;发现版本更新的 Proxy 比旧的 Proxy 在 TPC-C 场景下峰值 tpmC 下降了 7% 左右。 排查过程 在性能测试期间&#xff0c;使用 async-profiler 分别对两个进程进行…

根据aop实现自定义缓存注解

根据aop实现自定义缓存注解 自定义注解 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.concurrent.TimeUnit;/*** author: yanche…

ESP32通过Arduino导出编译bin文件并进行量产烧录

ESP32通过Arduino导出编译bin文件并进行量产烧录 文章目录 ESP32通过Arduino导出编译bin文件并进行量产烧录Arduino导出编译的bin文件方法一&#xff1a;通过Arduino IDE提供的工具导出编译文件方法二&#xff1a;到编译生成的临时文件目录进行查找 弄清楚Arduino的编译烧录过程…

Unity3D:工具栏

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D工具链 3D工具集&#xff1a; NSDT简石数字孪生 工具栏 在 Unity Editor 顶部可以看到工具栏。 工具栏不是窗口&#xff0c;是 Unity 界面中唯一无法重新排列的部分。 有关场景视图中的其他工具&#xff0c;请参阅叠加。 工具栏…

记录一次使用thinkphp5分页器获取数据

// 输出当前页 $nowPage $data->currentPage(); // 输出总条数 $total $data->total(); // 输出当前页条数 $listRows $data->listRows();db(tablename)->where("id > 0")->paginate(10,true,[page>4]); //每页显示10条记录&#xff0c;且打…

C#(五十一)之特性

特性是用于为程序元素添加额外信息的一种机制。比如记录文件修改时间、提示某方法已经过期等。方法、变量、属性、类、接口、结构体以及程序及都是程序元素 Obsolete第二个参数设置为true,调用此方法会产生警告并引起编译器报错 百度了一下C#还有其他的特性以及自定义特性&am…

基于ssm实现图书商城(spring+springmvc+mybatis)

一、项目功能 前台 图书基本展示,包括推荐图书展示和类图书类型展示.推荐图书包括条幅推荐,热销推荐和新品推荐.按照图书类型展示商品.图书详细信息展示.图书加入购物车.修改购物车内图书信息,例如数量等.用户登录.用户注册.修改个人信息,包括密码和收获信息.购物车付款.用户…

前置声明、源文件include、编译链接顺序问题

TestB.h (前置声明&#xff0c;无需在源文件include)重点&#xff1a; 1.前置声明用在指针变量使用&#xff0c;无需在头文件或源文件include 2.继承或者普通变量在头文件使用的时候(除非所有的编译顺序都正确&#xff0c;才能在源文件include)&#xff0c;最好不要在源文件i…

汇编语言基础--内中断

在8086CPU实模式下有如下内存布局&#xff1a; 我们看到在000-3FF的位置是放着中断向量表。 里面放的其实是4个字节的地址&#xff08;地址处放着对应的中断处理函数&#xff09;。我们知道在8086实模式下&#xff0c;是通过cs:ip来找到要执行的指令。cs是2个字节&#xff0c;i…

chat2DB使用教程

1. chat2DB简介 1-1. 简介 ​ chat2DB是一款有开源免费的多数据库客户端工具&#xff0c;支持windows、mac本地安装&#xff0c;也支持服务器端部署&#xff0c;web网页访问。和传统的数据库客户端软件Navicat、DBeaver 相比Chat2DB集成了AIGC的能力&#xff0c;能够将自然语…

Squid 缓存代理--反向代理

Squid 缓存代理–反向代理 反向代理&#xff1a;如果Squid反向代理服务器中缓存了该请求的资源&#xff0c;则将该请求的资源直接返回给客户端&#xff1a;否则反向代理服务器将向后台的WEB服务器请求资源&#xff0c;然后将请求的应答返回给客户端&#xff0c;同时也将应答缓…

VMware 虚拟磁盘格式

1.如果必须用FT &#xff0c; 只能选eagerzeroedthick 2.如果追求最佳应用性能不考虑空间占用 &#xff0c; 选eagerzeroedthick 3.如果希望最大成都的利用空间&#xff0c;并且对磁盘的增长是可控的&#xff0c;可以选thin格式 4.如果不希望空间的过量分配(oversubsribe)造…

OpenStack组件的基本使用

OpenStack组件的基本使用 Keystone命令行的方式项目用户角色 图形化界面方式项目用户角色 Glance命令行操作镜像上传下载管理镜像开放镜像权限转换镜像格式 图形化界面 Nova和Neutron命令行的方式实例类型密钥对安全组创建网络创建云主机启动关闭云主机 图形化界面创建实例类型…

【数据结构与算法】二叉树中从每个叶子结点到根结点的路径

题目 Qestion: 输出二叉树中从每个叶子结点到根结点的路径 数据结构与定义 #include <stdio.h> #include <stdlib.h>typedef struct TreeNode {int val;struct TreeNode *left;struct TreeNode *right; } TreeNode;二叉树形状 核心代码 void LeafToRoot(TreeNod…

[网络安全提高篇] 一二一.恶意软件动态分析Cape沙箱Report报告的API序列批量提取详解

终于忙完初稿,开心地写一篇博客。 “网络安全提高班”新的100篇文章即将开启,包括Web渗透、内网渗透、靶场搭建、CVE复现、攻击溯源、实战及CTF总结,它将更加聚焦,更加深入,也是作者的慢慢成长史。换专业确实挺难的,Web渗透也是块硬骨头,但我也试试,看看自己未来四年究…

HIve中的查询语句

文章目录 Hive中的查询语句1. 基础语法2. 基本查询&#xff08;Select…From&#xff09;2.1 数据准备&#xff08;0&#xff09;原始数据&#xff08;1&#xff09;创建部门表&#xff08;2&#xff09;创建员工表&#xff08;3&#xff09;导入数据 2.2 全表和特定列查询1&am…

【对象存储】那些事

最近在某个项目中使用了对象存储。以前看过一个新闻&#xff1a;某公司的对象存储被盗刷&#xff0c;一夜之间账户欠费几十万&#xff01;我们这点小买卖可经不起这么折腾&#xff01;所以下功夫研究了下&#xff0c;防患于未然。 说到防盗刷&#xff0c;我们还得了解对象存储是…