HMI串口屏框架使用说明
设计目的
本HMI串口屏框架(下称“本框架”)是针对淘晶驰串口屏与STM32H7系列单片机开发的简单通信框架方案。
本框架采用初始化、回调、API封装三元模式开发,初始化整个通信系统之后,按下串口屏或者其他来自于单片机的中断事件可以调用经过封装API与串口屏进行通信。本框架部署简单、框架明晰,可以提供具有高度稳定性的人机交互解决方案实现路径。
文件组织
文件路径 | 备注 |
---|---|
UserCodes\hmi_callbacks.c | 用户编写的回调函数实现 |
UserCodes\hmi_callbacks.h | 用户编写的回调函数声明 |
UserCodes\hmi_sock.c | 与串口屏通信的API接口实现 |
UserCodes\hmi_sock.h | 与串口屏通信的API接口声明与相关常量、结构体 |
部署方法
工程生成与配置
在STM32CubeMX中对所要使用的串口配置如下(注意波特率与串口屏一致):
DMA按照如下的参数开启:
注意,一定要进行DMA的配置,本框架全部基于DMA进行通信。
调入文件
在Keil中加入相关的文件。我习惯将私有代码文件放在UserCodes
文件夹处,并且在Keil中新建相应的文件组。当然也可以放在别的地方。(这里图片的hmi_calbacks.c
实际应为hmi_callbacks.c
,刚开始的时候手残少打了一个l
)
还需要注意的是,需要添加头文件的路径。我的头文件也是在UserCodes
文件夹中。
在main.c中的部署工作
// Core/Src/main.c
// 此处省略了STM32CubeMX自动生成的相关内容,仅保留关于此框架的私有内容
int main(void)
{
...
/* USER CODE BEGIN 2 */
...
hmi_sock_init(&huart3, hmi_buffer);
...
/* USER CODE END 2 */
...
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
hmi_sock_loop_callback();
...
// 其他轮询任务
}
/* USER CODE END 3 */
}
需要注意的是,本框架的hmi_sock_rx_callback()
必须不能放在中断处理之内。如果在一次轮询时未接收到任何数据,则会跳过调取回调函数的过程,直接开始下一个轮询函数。
完成以上部署的步骤之后,在每一次从串口屏接收到4个字节的按键码之后,本框架将会自动判断案件的页码和按钮编号,调用相应的函数。
编写回调函数
编写格式
用户可以在hmi_calbacks.c
中编写案件回调函数。函数名其实没有什么特别的规定,但是建议遵循以下的模式:
// UserCodes/hmi_calbacks.c
void *hmi_callbacks_page1_button1(void);
需要注意的是,参数列表和返回值的定义必须与该形式保持一致,也就是没有参数且返回值为一个void
类型的指针,指向一个临时缓冲区用于返回数据(建议仅用于实现了malloc()
函数的系统)。
注册函数
用户完成回调函数的编写之后,需要在hmi_sock_init()
函数中对回调函数进行注册:
// UserCodes/hmi_sock.c
int hmi_sock_init(UART_HandleTypeDef *huart, unsigned char *hmi_buffer)
{
...
// Set button callbacks
page[0].buttons[0].button_callbacks = NULL;
page[0].buttons[1].button_callbacks = NULL;
page[0].buttons[2].button_callbacks = NULL;
page[0].buttons[3].button_callbacks = NULL;
page[0].buttons[4].button_callbacks = NULL;
page[1].buttons[0].button_callbacks = NULL;
page[1].buttons[1].button_callbacks = hmi_callbacks_page1_button1;
page[1].buttons[2].button_callbacks = hmi_callbacks_page1_button2;
page[1].buttons[3].button_callbacks = hmi_callbacks_page1_button3;
page[1].buttons[4].button_callbacks = NULL;
page[2].buttons[0].button_callbacks = NULL;
page[2].buttons[1].button_callbacks = NULL;
page[2].buttons[2].button_callbacks = NULL;
page[2].buttons[3].button_callbacks = NULL;
page[2].buttons[4].button_callbacks = NULL;
page[3].buttons[0].button_callbacks = NULL;
page[3].buttons[1].button_callbacks = NULL;
page[3].buttons[2].button_callbacks = NULL;
page[3].buttons[3].button_callbacks = NULL;
page[3].buttons[4].button_callbacks = NULL;
return HMI_RET_OK;
}
在第N页的第M个按钮的回调函数就是:
page[M].buttons[N].button_callbacks
API
需要注意的是,API都不能在中断处理中调用,否则会引起单片机死锁。
hmi_sock_init:初始化函数
// UserCodes/hmi_sock.c
/**
* @param huart UART port to communicate with HMI display.
* @param hmi_buffer HMI recive buffer, 4 bytes length.
*
* @return 0-succeed, other-failed.
*/
int hmi_sock_init(UART_HandleTypeDef *huart, unsigned char *hmi_buffer);
hmi_sock_get_txt:从文本框获取数据
// UserCodes/hmi_sock.c
/**
* @param id ID of txtboxes, etc t0, slt0.
* @param ret_buf Buffer to recive data from HMI Display.
* @param ret_max_len maximum length of buffer.
*/
int hmi_sock_get_txt(char *id, unsigned char *ret_buf, unsigned int ret_max_len);
这个函数可以从具有.txt
属性的控件中获取数据。
hmi_sock_set_txt:设置文本框的数据
// UserCodes/hmi_sock.c
/**
* @param id ID of txtboxes, etc t0, slt0.
* @param wr_buf Buffer to be written to HMI display.
* @param wr_len Length to write.
*/
int hmi_sock_set_txt(char *id, unsigned char *wr_buf, unsigned int wr_len);
这个函数向具有.txt
属性的控件写入数据。
实现细节
本框架的框图如下:
本框架的串口屏设计范例如下:
在这个范例中,当按键弹起后向单片机发出四个字节。前两个字节为按键事件的标识码,第三个字节为页码,第四个字节为案件编号。在单片机上,具体的代码实现如下:
// UserCodes/hmi_sock.c
int hmi_sock_rx_callback(void)
{
...
unsigned int page_num = _hmi_sock_buffer[2];
unsigned int button_num = _hmi_sock_buffer[3];
...
}
在获取到相应的页码和按键编号之后,则会根据page
数组中的内容调用相应的用户回调函数。page及其相关结构体的定义如下:
// UserCodes/hmi_sock.h
// Button callback structure
typedef struct
{
void *(*button_callbacks)(void);
} hmi_sock_button;
// Page structure
typedef struct
{
hmi_sock_button buttons[5];
} hmi_sock_page;
// UserCodes/hmi_sock.c
static hmi_sock_page page[4];
可见,在hmi_sock_button
结构体中定义了一个函数指针,其模板参数列表以及返回值为void *(*button_callbacks)(void);
。这个函数就是用户的回调函数;而在hmi_sock_page
结构体中定义了一个由hmi_sock_button
结构体为数据元素的长度为5的数组;在UserCodes/hmi_sock.c
中定义了数据元素为hmi_sock_page
的长度为4的数组。因此,在进行函数调用的时候,代码如下:
// UserCodes/hmi_sock.c
/**
* @return First byte of return buffer from the function to be called, or 0.
* @attention This function should not be called in interrupt callbacks.
*/
int hmi_sock_loop_callback(void)
{
if (hmi_rx_flag == 1)
{
unsigned int type = 0;
{
unsigned char *_type_ba = (unsigned char *)&type;
memcpy(_type_ba, _hmi_sock_buffer, 2);
}
unsigned int page_num = _hmi_sock_buffer[2];
unsigned int button_num = _hmi_sock_buffer[3];
__HMI_DEBUG_PRINT(6, "hmi_sock_rx_callback(): _hmi_sock_buffer: %02x, %02x, %02x, %02x\n", _hmi_sock_buffer[0], _hmi_sock_buffer[1], _hmi_sock_buffer[2], _hmi_sock_buffer[3]);
__HMI_DEBUG_PRINT(6, "hmi_sock_rx_callback(): type: %d, page_num: %d, button_num: %d\n", type, page_num, button_num);
if (page[page_num].buttons[button_num].button_callbacks != NULL)
{
int *ret_buf = page[page_num].buttons[button_num].button_callbacks();
if (ret_buf != NULL)
{
return ret_buf[0];
}
}
hmi_rx_flag = 0;
}
return 0;
}
关于调用的相关细节这里不过多赘述。需要注意的是返回值的问题:如果用户回调函数返回的是NULL
,hmi_sock_rx_callback()
则会向上一级函数返回0
;如果用户回调函数返回的不是NULL
,则会向上一级返回这个缓冲区的第一个int
类型4字节值。在具有malloc()
实现的嵌入式系统中,这个缓冲区可以在hmi_sock_rx_callback()
中继续处理,最后归还给系统;在不具有malloc()
实现的系统中,则不建议用户回调函数返回任何非NULL
的值。
API的实现
hmi_sock_get_txt:从文本框获取数据
这个函数的实现代码如下:
// UserCodes/hmi_sock.c
/**
* @param id ID of txtboxes, etc t0, slt0.
* @param ret_buf Buffer to recive data from HMI Display.
* @param ret_max_len maximum length of buffer.
*/
int hmi_sock_get_txt(char *id, unsigned char *ret_buf, unsigned int ret_max_len)
{
HAL_StatusTypeDef state;
__HMI_DEBUG_PRINT(6, "hmi_sock_get_txt(): Entered hmi_sock_get_txt()\n", 0);
state = HAL_UART_DMAStop(_hmi_huart);
if (state != HAL_OK)
{
return HMI_RET_DMA_ERROR;
__HMI_DEBUG_PRINT(0, "hmi_sock_get_txt(): Error: Stop DMA\n", 0);
}
__HMI_DEBUG_PRINT(6, "hmi_sock_get_txt(): Stopped DMA\n", 0);
char send_cmd[HMI_CMD_BUF_LEN] = {0};
sprintf(send_cmd, "prints %s.txt,0\xff\xff\xff", id);
__HMI_DEBUG_PRINT(6, "send_cmd: %s\n", send_cmd);
state = HAL_UART_Receive_DMA(_hmi_huart, ret_buf, ret_max_len);
if (state != HAL_OK)
{
__HMI_DEBUG_PRINT(0, "hmi_sock_get_txt(): Error: Start DMA\n", 0);
return HMI_RET_DMA_ERROR;
}
__HMI_DEBUG_PRINT(6, "hmi_sock_get_txt(): Started DMA\n", 0);
HAL_UART_Transmit(_hmi_huart, send_cmd, strlen(send_cmd), HAL_MAX_DELAY);
__HMI_DEBUG_PRINT(6, "hmi_sock_get_txt(): Sent CMD\n", 0);
while (1)
{
int cnt = __HAL_DMA_GET_COUNTER(_hmi_huart->hdmarx);
HAL_Delay(10);
if (cnt == __HAL_DMA_GET_COUNTER(_hmi_huart->hdmarx))
{
__HMI_DEBUG_PRINT(6, "cnt: %d\n", cnt);
break;
}
}
state = HAL_UART_DMAStop(_hmi_huart);
if (state != HAL_OK)
{
return HMI_RET_DMA_ERROR;
__HMI_DEBUG_PRINT(0, "Error: Stop DMA\n", 0);
}
__HMI_DEBUG_PRINT(6, "Stopped DMA\n", 0);
state = HAL_UART_Receive_DMA(_hmi_huart, _hmi_sock_buffer, HMI_SOCK_BUFFER_LEN);
if (state != HAL_OK)
{
__HMI_DEBUG_PRINT(0, "Error: Start DMA\n", 0);
return HMI_RET_DMA_ERROR;
}
__HMI_DEBUG_PRINT(6, "Started DMA\n", 0);
return 0;
}
这个函数的大致实现思路是,先关闭UART的接收DMA,然后再以最大缓冲区尺寸开启UART的接收DMA,然后发送获取控件.txt
属性数据的指令。串口屏将会向单片机返回这些数据,但是这些数据的长度是不定的。因此,还需要再接收的时候时刻紧盯DMA的接收长度。如果这个接收长度再10ms内都没有发生任何改变,就说明已经接收完毕了,数据也已经到达目标缓冲区。此时,再关闭UART的接收DMA,并再次以4字节长度开启,继续接收4字节的事件。
其实这个函数也不用怎么写注释了,变量的意义和各个操作的解释再调试内容的输出中已经说得很明确了。
hmi_sock_set_txt:设置文本框的数据
这个函数的实现如下:
// UserCodes/hmi_sock.c
/**
* @param id ID of txtboxes, etc t0, slt0.
* @param wr_buf Buffer to be written to HMI display.
* @param wr_len Length to write.
*/
int hmi_sock_set_txt(char *id, unsigned char *wr_buf, unsigned int wr_len)
{
char wr_buf_b[HMI_CMD_BUF_LEN / 2] = {0};
memcpy(wr_buf_b, wr_buf, wr_len);
unsigned char send_cmd[HMI_CMD_BUF_LEN] = {0};
sprintf(send_cmd, "%s.txt=\"%s\"\xff\xff\xff", id, wr_buf_b);
__HMI_DEBUG_PRINT(6, "send_cmd: %s\n", send_cmd);
HAL_UART_Transmit(_hmi_huart, send_cmd, strlen(send_cmd), HAL_MAX_DELAY);
__HMI_DEBUG_PRINT(6, "Sent CMD\n", 0);
return 0;
}
这个函数就很简单了,无非就是把字符串拼起来发出去,这里不做过多的解释。