STM32F103C8T6最小系统板实践:将文本分割结果通过串口屏显示
本文介绍了如何在星图GPU平台上自动化部署BERT文本分割-中文-通用领域镜像,实现高效的文本智能处理。该镜像能够精准识别并分割中文新闻等文本的结构,提取标题、首段等关键信息。处理后的结构化数据可轻松应用于嵌入式系统,例如驱动STM32开发板在串口屏上实现新闻摘要的滚动显示。
STM32F103C8T6最小系统板实践:将文本分割结果通过串口屏显示
最近在捣鼓一个挺有意思的小项目,想试试能不能把云端AI处理的结果,实时显示在一块小小的嵌入式屏幕上。手头正好有一块经典的STM32F103C8T6最小系统板,还有一块串口TFT屏,于是就有了这个想法:用AI模型把一篇新闻文章的关键部分切出来,比如标题、首段这些,然后通过网络发到开发板上,最后让屏幕滚动显示出来。
听起来像是把云端智能和本地硬件给串起来了,对吧?整个过程确实涉及了好几个环节,从调用AI模型、网络传输,再到嵌入式端的驱动和显示,算是一个小小的全链路实践。做完之后感觉挺有成就感的,尤其是看到屏幕上流畅地滚动出处理好的文字时。如果你也对这种软硬件结合的项目感兴趣,或者手头有类似的板子和屏幕想玩点新花样,那接下来的内容或许能给你一些参考。
1. 项目整体思路与核心环节
这个项目的目标很明确,就是让一块小小的开发板,能接收并显示经过云端智能处理后的文本信息。整个流程可以拆解成三个主要环节,像接力赛一样,一环扣一环。
首先是在云端,我们需要一个能理解文章结构、并精准分割出关键部分的AI模型。这里我选择了一个基于BERT的文本分割模型,它特别擅长理解上下文,能准确地把一篇文章的标题、首段、正文小节给识别出来。这一步相当于给原始文本做了一次“智能摘要提取”。
然后,这些被提取出来的文本片段,需要从云端“飞”到我的本地开发环境。我通过一个简单的网络服务(比如用Python的Flask框架快速搭一个)来接收AI模型的处理结果,并提供一个接口,让我的STM32开发板能够请求到这些数据。考虑到开发板的网络处理能力和资源,通信协议要尽量轻量。
最后,也是最“硬核”的一步,就是在STM32F103C8T6这块核心板上完成接收、解析和显示。开发板通过串口或者某种网络模块(比如ESP8266)拿到数据后,需要驱动那块串口TFT屏,把文字按照我们想要的格式(比如滚动显示)展示出来。这里涉及到嵌入式端的串口通信、屏幕驱动指令的发送、以及显示缓冲区的管理等细节。
2. 云端AI文本分割处理
要让机器理解一篇文章哪里是标题,哪里是段落,并不像我们人眼一看那么简单。我选择使用BERT模型来完成这个任务,主要是看中了它在理解语义和上下文方面的强大能力。不过,我们不需要从头训练一个模型,那样太耗时了。更实用的方法是利用已经预训练好的模型,进行微调或者直接使用一些现成的工具库。
我在这里用的是transformers这个强大的库。思路是,将一篇完整的新闻文本输入给模型,让模型为文本中的每个句子或子句打上标签,比如“标题”、“首段”、“正文”、“结尾”等。这本质上是一个序列标注任务。对于BERT来说,我们可以在其输出的每个token的表示上接一个分类层,来判断这个token所属的片段类型。
下面是一段简化的示例代码,展示了如何使用预训练模型进行推理:
from transformers import AutoTokenizer, AutoModelForTokenClassification
import torch
# 加载预训练的文本分割模型(这里以假设的模型名为例,实际需根据任务选择或微调)
model_name = "your-pretrained-segmentation-model"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForTokenClassification.from_pretrained(model_name)
def segment_news(text):
"""
对输入的新闻文本进行分割,返回结构化的结果。
"""
# 对文本进行编码
inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=512)
# 模型推理
with torch.no_grad():
outputs = model(**inputs)
# 获取预测的标签ID
predictions = torch.argmax(outputs.logits, dim=-1)[0].tolist()
# 将token IDs转换回标签
labels = [model.config.id2label[pred] for pred in predictions]
# 将token和标签对齐,并合并连续的相同标签片段
tokens = tokenizer.convert_ids_to_tokens(inputs['input_ids'][0])
segmented_parts = []
current_label = None
current_text = ""
for token, label in zip(tokens, labels):
if token in [tokenizer.cls_token, tokenizer.sep_token, tokenizer.pad_token]:
continue
# 处理子词标记(如 ##ing)
if token.startswith("##"):
current_text += token[2:]
else:
if current_label and label == current_label:
current_text += " " + token
else:
if current_text:
segmented_parts.append({"label": current_label, "text": current_text.strip()})
current_text = token
current_label = label
if current_text:
segmented_parts.append({"label": current_label, "text": current_text.strip()})
# 过滤和整理,提取我们关心的部分,如标题和首段
result = {}
for part in segmented_parts:
if part['label'] == 'TITLE':
result['title'] = part['text']
elif part['label'] == 'FIRST_PARAGRAPH':
result['first_paragraph'] = part['text']
# 可以继续添加其他需要的部分,如正文关键句等
return result
# 示例:处理一篇新闻
news_article = """
人工智能助力嵌入式开发。
近年来,随着边缘计算的发展,在资源受限的设备上运行轻量级AI模型成为可能。STM32等MCU平台也开始集成AI加速功能,为开发者打开了新的大门。
...
"""
segmented_result = segment_news(news_article)
print(segmented_result)
# 期望输出类似: {'title': '人工智能助力嵌入式开发', 'first_paragraph': '近年来,随着边缘计算的发展...成为可能。'}
这段代码跑起来后,你就能得到一篇新闻里最核心的标题和首段内容了。当然,实际应用中你可能需要根据自己的数据微调模型,或者使用专门针对新闻结构分割训练好的模型,这样效果会更精准。
得到结构化的文本后,我们需要把它封装起来,通过网络提供给STM32开发板。一个简单快速的方法是使用Flask搭建一个轻量级的Web API:
from flask import Flask, jsonify
import json
app = Flask(__name__)
# 假设我们有一个处理好的结果
processed_news_data = {
"title": "人工智能助力嵌入式开发",
"first_paragraph": "近年来,随着边缘计算的发展,在资源受限的设备上运行轻量级AI模型成为可能。",
"update_time": "2023-10-27"
}
@app.route('/get_news_segment', methods=['GET'])
def get_news_segment():
"""提供文本分割结果的API接口"""
return jsonify(processed_news_data)
if __name__ == '__main__':
# 在本地运行,端口可自定义
app.run(host='0.0.0.0', port=5000, debug=False)
这样,当STM32开发板需要获取数据时,只需要向这个地址(例如 http://你的电脑IP:5000/get_news_segment)发起一个HTTP GET请求,就能拿到JSON格式的文本数据了。这一步的关键是确保你的开发板和运行这个Flask服务的电脑在同一个局域网内。
3. STM32端的数据接收与解析
数据已经从云端“生产”出来了,并且有了一个获取数据的“窗口”。接下来,我们要让STM32F103C8T6这块小板子学会去“窗口”拿数据,并且能看懂拿回来的东西。这里通常有两种思路:一种是让STM32直接通过Wi-Fi模块(如ESP8266)接入网络,发起HTTP请求;另一种是让STM32通过串口连接一个更强大的主机(如树莓派),由主机负责网络通信,再把数据转发给STM32。为了简化并使项目聚焦于显示,我这里采用第二种方式,即假设STM32通过串口从主机接收已经获取好的JSON数据。
首先,我们需要在STM32的工程中配置好串口。以使用HAL库为例,我们初始化一个串口(比如USART1)用于接收数据。
// 串口初始化代码片段 (基于STM32CubeMX生成)
UART_HandleTypeDef huart1;
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_USART1_UART_Init(void);
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
// 开启串口接收中断
HAL_UART_Receive_IT(&huart1, &rx_buffer, 1);
while (1) {
// 主循环,处理数据解析和显示
if (data_received_flag) {
parse_received_data();
data_received_flag = 0;
}
// 其他任务...
}
}
// 串口接收中断回调函数
uint8_t rx_buffer;
uint8_t uart_rx_data[512];
uint16_t uart_rx_index = 0;
uint8_t data_received_flag = 0;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart->Instance == USART1.Instance) {
// 简单的以换行符作为一帧数据结束的标志
if (rx_buffer != '\n') {
uart_rx_data[uart_rx_index++] = rx_buffer;
if (uart_rx_index >= 512) uart_rx_index = 0; // 防止溢出
} else {
uart_rx_data[uart_rx_index] = '\0'; // 字符串结束符
uart_rx_index = 0;
data_received_flag = 1; // 设置标志位,通知主循环
}
// 重新开启接收中断
HAL_UART_Receive_IT(&huart1, &rx_buffer, 1);
}
}
在上面的代码中,STM32通过中断方式不断接收串口数据,当遇到换行符 \n 时,就认为一帧数据接收完成,并设置一个标志位。主循环检测到这个标志位后,就会调用 parse_received_data() 函数来解析数据。
接下来就是解析环节。我们假设从串口收到的是一个JSON字符串,比如 {"title":"AI News","first_paragraph":"Hello World."}。在资源有限的MCU上解析完整的JSON库可能有点吃力,但因为我们预期数据格式固定且简单,完全可以自己写一个轻量级的解析器,或者只提取关键字段。
// 一个非常简易的、针对固定格式JSON的解析函数示例
typedef struct {
char title[64];
char first_paragraph[256];
} NewsSegment;
NewsSegment current_news;
void parse_received_data(void) {
char *json_str = (char*)uart_rx_data;
// 查找 "title": 字段
char *title_start = strstr(json_str, "\"title\":\"");
if (title_start) {
title_start += 9; // 跳过 "\"title\":\""
char *title_end = strchr(title_start, '\"');
if (title_end) {
int len = title_end - title_start;
strncpy(current_news.title, title_start, len);
current_news.title[len] = '\0';
}
}
// 查找 "first_paragraph": 字段
char *para_start = strstr(json_str, "\"first_paragraph\":\"");
if (para_start) {
para_start += 19; // 跳过 "\"first_paragraph\":\""
char *para_end = strchr(para_start, '\"');
if (para_end) {
int len = para_end - para_start;
strncpy(current_news.first_paragraph, para_start, len);
current_news.first_paragraph[len] = '\0';
}
}
// 解析完成后,可以设置另一个标志位,触发显示更新
display_update_flag = 1;
}
这个解析函数非常直接,就是通过字符串查找定位我们需要的字段内容。解析成功后,数据就被存储在了 current_news 这个结构体变量中,等待被显示到屏幕上。
4. 驱动串口TFT屏进行滚动显示
数据已经准备就绪,最后一步就是让它们在屏幕上“动”起来。我使用的是一块常见的串口TFT屏,它通常通过UART接收特定的指令集来控制显示,比如清屏、设置光标、打印字符串、设置字体颜色等。不同的屏幕厂商指令可能略有不同,但原理相通。
首先,我们需要根据屏幕的指令手册,编写底层的发送函数。假设屏幕的指令格式很简单,比如打印字符串就是直接发送字符串本身,而控制指令则以特殊的转义字符开头。
// 向串口屏发送字符串(假设USART1连接屏幕)
void tft_send_string(char *str) {
HAL_UART_Transmit(&huart1, (uint8_t*)str, strlen(str), HAL_MAX_DELAY);
}
// 发送清屏指令(假设指令是 "\x1B[2J")
void tft_clear_screen(void) {
char cmd[] = "\x1B[2J";
tft_send_string(cmd);
}
// 设置光标位置(假设指令是 "\x1B[%d;%dH",行,列)
void tft_set_cursor(uint8_t row, uint8_t col) {
char cmd[16];
sprintf(cmd, "\x1B[%d;%dH", row, col);
tft_send_string(cmd);
}
有了这些基础函数,我们就可以构思滚动显示的效果了。一个简单的思路是:将屏幕的显示区域看作一个固定高度的窗口,我们要显示的文本(标题+首段)是一个更长的内容。通过定时改变文本在窗口中的起始显示位置,就能实现滚动。
我们可以定义一个缓冲区来存放要显示的全部文本,并维护一个“滚动索引”。
char display_buffer[512]; // 存储待显示的全部文本
int scroll_index = 0; // 当前滚动起始位置
int total_length = 0; // 文本总长度
int screen_width = 20; // 假设屏幕每行显示20个字符
int screen_height = 10; // 假设屏幕显示10行
void prepare_display_buffer(void) {
// 将标题和段落组合成一个长字符串,中间加些分隔符
sprintf(display_buffer, ">> %s <<\n\n%s", current_news.title, current_news.first_paragraph);
total_length = strlen(display_buffer);
}
void update_scrolling_display(void) {
tft_clear_screen();
tft_set_cursor(1, 1); // 光标回到左上角
// 从 scroll_index 开始,截取能填满屏幕的内容
for (int line = 0; line < screen_height; line++) {
int start_pos = scroll_index + line * screen_width;
if (start_pos >= total_length) {
// 如果已经显示到文本末尾,可以重新开始或停止
// 这里简单处理为显示空白行
tft_send_string("\n");
continue;
}
// 临时缓冲区,用于存放一行文本
char line_buf[screen_width + 1];
int copy_len = (total_length - start_pos) > screen_width ? screen_width : (total_length - start_pos);
strncpy(line_buf, &display_buffer[start_pos], copy_len);
line_buf[copy_len] = '\0';
tft_send_string(line_buf);
tft_send_string("\n"); // 换行
}
// 更新滚动索引,实现滚动
scroll_index++;
// 如果滚动到末尾,则回到开头
if (scroll_index > total_length + screen_height * screen_width) {
scroll_index = 0;
}
}
最后,我们需要一个定时器来驱动这个滚动更新。可以在主循环中用一个简单的延时,或者配置一个硬件定时器中断,每隔几百毫秒调用一次 update_scrolling_display() 函数,并更新 scroll_index。同时,要确保只有在解析到新数据(display_update_flag 被设置)时,才调用 prepare_display_buffer() 来刷新显示内容。
// 在主循环或定时器中断中
if (display_update_flag) {
prepare_display_buffer();
scroll_index = 0; // 重置滚动位置
display_update_flag = 0;
}
// 每隔一定时间(如300ms)滚动一次
if (HAL_GetTick() - last_scroll_time > 300) {
update_scrolling_display();
last_scroll_time = HAL_GetTick();
}
当所有这些代码都烧录进STM32F103C8T6,并且硬件连接正确后,你就能看到串口屏上开始滚动显示从云端获取并分割好的新闻标题和首段了。整个过程从AI智能处理开始,到硬件动态显示结束,形成了一个完整的闭环。
5. 实践总结与思考
折腾完这个项目,感觉就像搭了一座小小的桥,连接了云端的智能和本地硬件的感知。STM32F103C8T6这块经典的最小系统板,虽然资源有限,但处理这样的文本接收、解析和驱动串口屏显示的任务,还是绰绰有余的。关键在于把任务拆解清楚,每个环节做好数据格式的约定和错误处理。
实际做下来,有几个地方的体会比较深。一是网络数据的可靠性,无论是STM32直接联网还是通过主机中转,都要考虑网络不稳定或数据格式错误的情况,在代码里加一些超时重试和简单校验会稳妥很多。二是显示效果的优化,简单的逐字滚动虽然实现了功能,但观感上可能有点生硬,如果屏幕指令支持,可以尝试更平滑的滚动方式,或者加入一些过渡效果。三是扩展性,现在只显示了标题和首段,其实完全可以显示更多信息,比如关键词、情感倾向等,这取决于云端AI模型能提供什么。
这个项目更像是一个起点,展示了这种软硬件结合的可能性。你可以把它想象成一个极简的“信息显示屏”,未来可以接入不同的数据源,比如天气预报、股票行情、甚至是实时翻译的结果,显示在不同的屏幕上。对于初学者来说,跟着走一遍这个流程,对理解嵌入式开发中数据流的概念、串口通信、以及如何与上层应用交互,都挺有帮助的。如果遇到问题,多查查屏幕的指令手册、STM32的HAL库文档,大部分都能解决。动手试试,看到屏幕亮起并滚动出文字的那一刻,还是挺有乐趣的。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐
所有评论(0)