【道生物联TKB-623评估板试用】基于串口透传的手写数字识别
本文介绍了道生物联TKB-623开发板结合 UART 串口透传实现手写数字识别与远距离传输的项目设计。
项目介绍
- 硬件连接:包括 TKB-623 连接单片机串口、发射和接收端定义等;
- 模式设置:设置 TKB-623 的发送端和接收端均为透传模式;
- 透传测试:连接硬件和 TKB-623,实现手写数字十六进制数据的串口透传;
- 数据解析:使用单片机结合 MNIST 库实现手写数字数据解析;
- 网页显示:网页端设计与数字解析结果显示等。

透传模式
透传模式是相对于 AT 指令的另一种模式。
- AT 指令模式是一种通信协议,设备之间通过发送 AT 指令来进行通信和控制;
- 透传模式的设备之间可以直接通过数据通道传输数据,无需发送和接收特定格式的 AT 指令。
- 透传模式是通过串口数据接收超时或超过最大包长来判断数据的结束,并开始发送数据。AT 指令模式是通过
\r\n 来判断指令的结束。
进入透传模式前需把设备配置成可以通讯的工作模式及射频发射接收的参数。
| 指令 |
响应 |
|---|
| AT+WORKMODE=<工作模式>,<超时时间>,<最大包长> |
AT_OK |
说明:
- <超时时间> 超时时间单位为毫秒,取值范围为 2~1000,默认 3
- <最大包长> 取值范围为 1~2048
详见:TK8620基于SDK2.0的AT指令使用说明 — 资料中心 文档 .
硬件连接
这里给出网页手写数字识别的串口透传硬件连接方案。
采用外加单片机解析 GPS 数据,TKB-623 将解析结果透传至接收端,接线如下
| TKB-623 (Receive) |
MCU |
Note |
|---|
| UART_TXD |
RXD (Pin9) |
Receive |
| UART_RXD |
TXD (Pin8) |
Transmit |
| 3V3_M |
VCC |
Power |
| GND |
GND |
Ground |
注意这里 TKB-623 接收端负责将透传信息发送至 MCU,并将识别结果透传回 TKB-623 发送端,因此TX和RX引脚均需要连接。
实物图

动态效果见顶部视频。
模式设置
- 固件默认工作模式为
21 ,即异步收发模式;
- 选择
透传模式 作为 TKB-623 的工作模式,转发网页发送的十六进制数据和单片机识别结果;
根据 AT 指令手册可知,AT+WORKMODE=81/82 可实现开启/关闭透传模式;

网页手写数字
设计网页手写数字面板设计,实现手写数字原始数据采集、转发和结果显示。
代码
电脑新建 index.html 文件,并添加如下代码
<!doctype html>
<html lang="zh-CN"><meta charset="utf-8"><title>手写数字识别</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<body style="margin:0;background:#0f172a;color:#e2e8f0;display:flex;flex-direction:column;align-items:center;padding:20px;font-family:system-ui">
<div style="background:rgba(30,41,59,.7);padding:20px;border-radius:12px;max-width:400px;width:100%;box-shadow:0 8px 32px rgba(2,8,20,.4)">
<h2 style="margin:0 0 15px;text-align:center;color:#38bdf8">? 手写数字识别</h2>
<canvas id=c width=280 height=280 style="border:2px solid #38bdf8;border-radius:8px;background:#000;cursor:crosshair;display:block;margin:0 auto"></canvas>
<div style="display:flex;gap:10px;margin-top:15px;flex-wrap:wrap;justify-content:center">
<button id=con>? 连接串口</button>
<button id=sen disabled>? 发送</button>
<button id=clr>?️ 清除</button>
<button id=inv>? 反相</button>
</div>
<div style="margin-top:12px;text-align:center">
<label>阈值<input type=range id=thr min=0 max=255 value=128 style="vertical-align:middle"><span id=thv>128</span></label>
<label><input type=checkbox id=auto>自动发送</label>
</div>
<div id=stat style="margin-top:12px;padding:10px;border-radius:6px;text-align:center;background:rgba(239,68,68,.2)">⚠️ 未连接</div>
<div id=res style="margin-top:12px;padding:10px;border-radius:6px;text-align:center;background:rgba(15,23,42,.6)">? 请写数字</div>
</div>
<script>
const c=document.getElementById('c'),x=c.getContext('2d'),
con=document.getElementById('con'),sen=document.getElementById('sen'),
clr=document.getElementById('clr'),inv=document.getElementById('inv'),
thr=document.getElementById('thr'),thv=document.getElementById('thv'),
auto=document.getElementById('auto'),
stat=document.getElementById('stat'),res=document.getElementById('res');
let port,reader,w,invFlag=0,threshold=128;
function initCanvas(){
x.fillStyle='#000';x.fillRect(0,0,280,280);
x.strokeStyle='#fff';x.lineWidth=12;x.lineCap='round';
}
initCanvas();
async function openPort(){
if(port){
reader&&reader.cancel(),await port.close(),port=null;
stat.style.background='rgba(239,68,68,.2)';stat.textContent='⚠️ 未连接';
con.textContent='? 连接串口';sen.disabled=1;
return;
}
port=await navigator.serial.requestPort();
await port.open({baudRate:115200});
reader=port.readable.getReader();
w=port.writable.getWriter();
stat.style.background='rgba(16,185,129,.2)';stat.textContent='✅ 已连接';
con.textContent='? 断开';sen.disabled=0;
readLoop();
}
async function readLoop(){
const dec=new TextDecoder();
let buf='';
try{while(1){
const {value,done}=await reader.read();
if(done)break;
buf+=dec.decode(value);
let line;
while((line=buf.indexOf('\\n'))>=0){
const msg=buf.slice(0,line).trim();buf=buf.slice(line+1);
if(msg.includes('RESULT')) res.innerHTML='? 识别结果: <b>'+msg.split(':')[1]+'</b>';
}
}}catch(e){port&&openPort()}
}
async function sendImage(){
if(!port){res.textContent='⚠️ 请先连接串口';return;}
const img=x.getImageData(0,0,280,280),d=img.data,bin=new Uint8Array(784);
for(let i=0;i<784;i++){
let v=0;
for(let dy=0;dy<10;dy++)for(let dx=0;dx<10;dx++){
const p=((i%28)*10+dx)*4+((i/28|0)*10+dy)*280*4;
v+=d[p]+d[p+1]+d[p+2];
}
bin[i]=v/300>threshold?255:0;
}
await w.write(bin);
res.textContent='? 已发送…';
}
function getPos(e){const r=c.getBoundingClientRect();return{x:e.clientX-r.left,y:e.clientY-r.top}}
c.onmousedown=e=>{x.beginPath();x.moveTo(getPos(e).x,getPos(e).y);c.onmousemove=e=>{x.lineTo(getPos(e).x,getPos(e).y);x.stroke()}};
c.onmouseup=c.onmouseout=()=>{c.onmousemove=null;auto.checked&&setTimeout(sendImage,300)};
c.ontouchstart=e=>c.onmousedown(e.touches[0]);
c.ontouchmove=e=>c.onmousemove(e.touches[0]);
c.ontouchend=c.onmouseup;
con.onclick=openPort;
sen.onclick=sendImage;
clr.onclick=()=>{initCanvas();res.textContent='? 画布已清除'};
inv.onclick=()=>{
const d=x.getImageData(0,0,280,280);
for(let i=0;i<d.data.length;i+=4)d.data[i]=255-d.data[i],d.data[i+1]=255-d.data[i+1],d.data[i+2]=255-d.data[i+2];
x.putImageData(d,0,0);invFlag^=1;x.strokeStyle=invFlag?'#000':'#fff';
};
thr.oninput=e=>{thv.textContent=threshold=+e.target.value;auto.checked&&sendImage()};
</script>
保存代码。
效果
使用浏览器加载 index.html 文件,界面如下

该网页设计的功能模块包括
- 串口连接
- 手写面板
- 发送和识别按钮
- 反相显示
- 波特率选择
- 数据格式选择
- 结果显示
透传测试
通过网页连接 TKB-623 发送端串口,以十六进制将手写数字发送至 TKB-623 接收端,完成透传测试。
波特率设置
这里使用 AT 指令设置串口通信波特率为 9600
» AT+BAUDRATE=9600
« AT_OK
效果
- 将 TKB-623 的发送端和接收端均通过 Type-C 数据线连接至电脑;
- 打开串口调试助手,配置串口参数,连接 TKB-623 接收端;
- 双击打开
index.html 网页,点击连接串口按钮,连接 TKB-623 发送端;

- 在面板手写数字,完成后点击发送识别按钮,此时接收端收到对应的十六进制数据,透传测试完成。
串口透传和识别
在网页手写数字完成的基础上,将 TKB-623 接收端连接单片机,将收到的十六进制手写数字数据通过串口传递至单片机,进而完成 MNIST 推理和识别,再将识别结果透传回网页端。
代码
在 Thonny IDE 中新建文件并添加如下代码
from machine import UART, Pin
import time
import json
import gc
class MNISTInference:
def __init__(self):
self.uart = UART(1, baudrate=9600, tx=Pin(8), rx=Pin(9))
self.uart.init(bits=8, parity=None, stop=1)
self.led = Pin(25, Pin.OUT)
self.receive_buffer = bytearray()
self.frame_start = b'\\xAA\\x55'
self.frame_end = b'\\x55\\xAA'
print("MNIST手写数字识别系统已启动")
print("等待串口数据...")
def receive_data(self):
"""接收并解析串口数据"""
if self.uart.any():
data = self.uart.read()
if data:
self.receive_buffer.extend(data)
start_pos = self.receive_buffer.find(self.frame_start)
if start_pos != -1:
remaining = self.receive_buffer[start_pos + 2:]
end_pos = remaining.find(self.frame_end)
if end_pos != -1:
frame_data = remaining[:end_pos]
if len(frame_data) >= 2:
data_length = (frame_data[0] << 8) | frame_data[1]
image_data = frame_data[2:-1]
received_checksum = frame_data[-1]
self.receive_buffer = bytearray()
calculated_checksum = self.calculate_checksum(image_data)
if calculated_checksum == received_checksum and len(image_data) == data_length:
return image_data
else:
print(f"校验和错误: 期望 {calculated_checksum}, 收到 {received_checksum}")
return None
def calculate_checksum(self, data):
"""计算校验和"""
checksum = 0
for byte in data:
checksum += byte
return checksum & 0xFF
def decode_hex_image(self, hex_data):
"""解码16进制图像数据"""
image_28x28 = []
for byte in hex_data:
for i in range(4):
pixel = (byte >> i) & 1
image_28x28.append(pixel)
if len(image_28x28) != 784:
print(f"图像尺寸错误: {len(image_28x28)}")
return None
return image_28x28
def simple_inference(self, image_data):
"""
简化的数字识别推理
在实际应用中,这里应该加载训练好的模型
"""
pixel_sum = sum(image_data)
center_of_mass = self.calculate_center_of_mass(image_data)
if pixel_sum < 50:
return 1
elif center_of_mass < 0.4:
return 0
elif pixel_sum > 200:
return 8
else:
if center_of_mass > 0.6:
return 9
elif pixel_sum < 100:
return 7
else:
return 3
def calculate_center_of_mass(self, image_data):
"""计算图像的质心"""
total_mass = 0
weighted_x = 0
for i, pixel in enumerate(image_data):
if pixel == 1:
x = i % 28
total_mass += 1
weighted_x += x
if total_mass == 0:
return 0.5
return weighted_x / total_mass / 28.0
def send_result(self, result):
"""发送识别结果"""
result_str = f"RESULT:{result}\\r\\n"
self.uart.write(result_str.encode())
print(f"识别结果已发送: {result}")
def blink_led(self, times=1):
"""LED闪烁"""
for _ in range(times):
self.led.on()
time.sleep(0.1)
self.led.off()
time.sleep(0.1)
def run(self):
"""主循环"""
while True:
try:
hex_data = self.receive_data()
if hex_data:
print(f"接收到图像数据,长度: {len(hex_data)}")
self.blink_led(2)
image_data = self.decode_hex_image(hex_data)
if image_data:
result = self.simple_inference(image_data)
self.send_result(result)
self.blink_led(result)
self.print_image_debug(image_data, result)
gc.collect()
time.sleep(0.01)
except Exception as e:
print(f"错误: {e}")
time.sleep(1)
def print_image_debug(self, image_data, result):
"""打印图像调试信息"""
print(f"\\n--- 识别到数字: {result} ---")
for y in range(0, 28, 4):
line = ""
for x in range(0, 28, 2):
has_pixel = False
for dy in range(4):
for dx in range(2):
idx = (y + dy) * 28 + (x + dx)
if idx < len(image_data) and image_data[idx] == 1:
has_pixel = True
break
if has_pixel:
break
line += "#" if has_pixel else "."
print(line)
print("-" * 20)
if __name__ == "__main__":
recognizer = MNISTInference()
recognizer.run()
保存代码。
效果
- 连接单片机与电脑,单片机串口与 TKB-623 接收端串口引脚连接;
- 网页端手写数字并发送,透传至单片机;
- 单片机推理并反馈识别结果至网页端;





同时终端打印调试信息,包括 28x28 二值化数字、识别结果上传等

- 可通过模型训练或使用预训练模型进一步提升识别准确率。
动态演示见底部视频。
总结
本文介绍了道生物联TKB-623开发板结合 UART 串口透传实现网页手写数字识别与远距离传输的项目设计,为相关产品在人工智能领域的开发设计和快速应用提供了参考。