本节内容介绍
- 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功能码,也欢迎小伙伴们提交代码进来,一起学习!!!