04-HAL库UART配置及协议解析设计 要闻速递

面包芯语   2023-07-04 22:38:00

关注、星标公众号,直达精彩内容

源码地址:https://gitee.com/MR_Wyf/hal-cubemx-rt-thread/tree/master/hal_cubemx_rtNano_UART

或者关注公众号,后台回复“UART”,获取本章节源码


【资料图】

HAL库UART在cubemx中的配置

串口原理图

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

cubemx中引脚选择预配置

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

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

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

HAL库串口代码详解

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

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

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

/***@briefRxTransfercompletedcallback.*@paramhuartUARThandle.*@retvalNone*/__weakvoidHAL_UART_RxCpltCallback(UART_HandleTypeDef*huart){/*Preventunusedargument(s)compilationwarning*/UNUSED(huart);/*NOTE:Thisfunctionshouldnotbemodified,whenthecallbackisneeded,theHAL_UART_RxCpltCallbackcanbeimplementedintheuserfile.*/}

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

串口收发设计

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

1、串口接收:

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

/***@briefReceiveanamountofdataininterruptmode.*@noteWhenUARTparityisnotenabled(PCE=0),andWordLengthisconfiguredto9bits(M1-M0=01),*thereceiveddataishandledasasetofu16.Inthiscase,Sizemustindicatethenumber*ofu16availablethroughpData.*@paramhuartUARThandle.*@parampDataPointertodatabuffer(u8oru16dataelements).*@paramSizeAmountofdataelements(u8oru16)tobereceived.*@retvalHALstatus*/HAL_StatusTypeDefHAL_UART_Receive_IT(UART_HandleTypeDef*huart,uint8_t*pData,uint16_tSize);

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

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

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

HAL_UART_Receive_IT(&huart4,&rxdata,1);

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

voidHAL_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、数据帧接收完成判断

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

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

3、串口发送

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

/***@briefSendanamountofdatainblockingmode.*@noteWhenUARTparityisnotenabled(PCE=0),andWordLengthisconfiguredto9bits(M1-M0=01),*thesentdataishandledasasetofu16.Inthiscase,Sizemustindicatethenumber*ofu16providedthroughpData.*@noteWhenFIFOmodeisenabled,writingadataintheTDRregisteraddsone*datatotheTXFIFO.WriteoperationstotheTDRregisterareperformed*whenTXFNFflagisset.Fromhardwareperspective,TXFNFflagand*TXEaremappedonthesamebit-field.*@paramhuartUARThandle.*@parampDataPointertodatabuffer(u8oru16dataelements).*@paramSizeAmountofdataelements(u8oru16)tobesent.*@paramTimeoutTimeoutduration.*@retvalHALstatus*/HAL_StatusTypeDefHAL_UART_Transmit(UART_HandleTypeDef*huart,constuint8_t*pData,uint16_tSize,uint32_tTimeout);

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

数据接收:

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

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

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

串口回调设计:

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

voidHAL_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)/

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

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

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

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

typedefenum{STATUS_HEAD1=0,STATUS_HEAD2,STATUS_LEN,STATUS_HANDLE_PROCESS}frame_status_e;

然后封装数据解析函数:

/*协议架构*//**数据头(1字节)+数据长度(2字节,不包含数据头)+功能码+数据+校验码(CRC16-MODBUS)**/#definePROTOCOL_HEAD10x5A#definePROTOCOL_HEAD20xA5intprocess_frame(constuint8_tdata,constuint8_tchannel){uint16_tcrc=0;uint16_tlen=0;staticframe_status_eframe_status;staticuint16_tindex=0;/*timeoutresetthereceivestatus*/if(RT_EOK==rt_sem_take(sem_uart_timeout,RT_WAITING_NO)){index=0;frame_status=STATUS_HEAD1;}switch(frame_status){caseSTATUS_HEAD1:if(data==PROTOCOL_HEAD1){frame_status=STATUS_HEAD2;buffer[index++]=data;}else{frame_status=STATUS_HEAD1;index=0;}break;caseSTATUS_HEAD2:if(data==PROTOCOL_HEAD2){frame_status=STATUS_LEN;buffer[index++]=data;}else{frame_status=STATUS_HEAD1;index=0;}break;caseSTATUS_LEN:if(data>=0&&data<=MAX_DATA_LEN){frame_status=STATUS_HANDLE_PROCESS;buffer[index++]=data;}else{frame_status=STATUS_HEAD1;index=0;}break;caseSTATUS_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机制的方式,将我们的回调函数注册进去:

typedefvoid(*uart_dispatcher_func_t)(constuint32_t,constuint8_t*,constuint32_t);typedefstructuart_dispatcher_item{union{struct{uint8_tchannel;uint8_tcmd_id;};uint32_tmagic_number;};uart_dispatcher_func_tfunction;}uart_dispatcher_item_t;#defineUART_DISPATCHER_CALLBACK_REGISTER(ch,id,fn)staticconstuart_dispatcher_item_tuart_dis_table_##ch##_##id\__attribute__((section("uart_dispatcher_table"),__used__,aligned(sizeof(void*))))=\{.channel=ch,.cmd_id=id,.function=fn}intcall_reg_cb(uint8_t*frame,uint8_tdata_len,intchannel,uint8_tcmd_id);

回调函数:

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

voiddispatcher_on_02_callback(constuint32_tchannel,constuint8_t*data,constuint32_tdata_len){constchar*str="func02isrunning\r\n";uart_write((uint8_t*)str,rt_strlen(str),100);rt_kprintf("func02isrunning\r\n");}UART_DISPATCHER_CALLBACK_REGISTER(1,0x02,dispatcher_on_02_callback);voiddispatcher_on_03_callback(constuint32_tchannel,constuint8_t*data,constuint32_tdata_len){constchar*str="func03isrunning\r\n";uart_write((uint8_t*)str,rt_strlen(str),100);rt_kprintf("func03isrunning\r\n");}UART_DISPATCHER_CALLBACK_REGISTER(1,0x03,dispatcher_on_03_callback);voiddispatcher_on_04_callback(constuint32_tchannel,constuint8_t*data,constuint32_tdata_len){constchar*str="func04isrunning\r\n";uart_write((uint8_t*)str,rt_strlen(str),100);rt_kprintf("func04isrunning\r\n");}UART_DISPATCHER_CALLBACK_REGISTER(1,0x04,dispatcher_on_04_callback);voiddispatcher_on_05_callback(constuint32_tchannel,constuint8_t*data,constuint32_tdata_len){rt_kprintf("func05isrunning\r\n");}UART_DISPATCHER_CALLBACK_REGISTER(1,0x05,dispatcher_on_05_callback);voiddispatcher_on_06_callback(constuint32_tchannel,constuint8_t*data,constuint32_tdata_len){rt_kprintf("func06isrunning\r\n");}UART_DISPATCHER_CALLBACK_REGISTER(1,0x06,dispatcher_on_06_callback);

测试效果

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

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

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

相关新闻