24 KiB
硬件视觉调度时序图
概述
本文档描述了硬件视觉调度系统的完整时序流程,展示了从调度车托料完成到最终调度至立锯的全过程。
系统架构
| 项目 | 技术栈 | 角色 | 说明 |
|---|---|---|---|
go-auto |
Go | TCP客户端(上位机) | 硬件控制中枢,通过PLC控制下位机,通过TCP与视觉算法通信 |
LogMax3D |
C# | TCP服务端(视觉算法) | 视觉算法服务,监听9000端口,处理3D扫描、建模、切割优化 |
通信协议
- 协议: TCP长连接,JSON行协议(
\n分隔) - 连接方向: Go客户端 → C#服务端(上位机主动连接视觉算法)
- 消息格式: 每行一个完整JSON对象,支持单行和多行格式化JSON自动重组
- 连接确认: 连接后C#立即推送
connection_ack确认 - 错误标识: 使用
error_id字段标识错误,空字符串表示成功
上位机控制时序图-视觉部分
sequenceDiagram
autonumber
participant 调度车
participant 上位机Go
participant 下位机PLC
participant 视觉算法C#
box rgb(173, 216, 230) 硬件调度系统
participant 调度车
end
box rgb(255, 218, 185) 控制系统
participant 上位机Go
participant 下位机PLC
end
box rgb(152, 251, 152) 算法系统
participant 视觉算法C#
end
Note over 调度车,视觉算法C#: 系统初始化阶段
调度车->>上位机Go: 托料完成(M42上升沿)
上位机Go->>上位机Go: 生成WoodID、推送LED面板
Note over 调度车,视觉算法C#: 视觉定位阶段(对中拍照)
上位机Go->>下位机PLC: M809触发 + D2130=2630验证
上位机Go->>上位机Go: 打开红外测距仪、等待读数稳定
上位机Go->>下位机PLC: 写入D2152/D2154调整坐标、M119调整拍照位
上位机Go->>下位机PLC: M810就位 → 关闭红外、M130触发坐标调整
上位机Go->>上位机Go: M120就位 → 监控激光传感器读数
上位机Go->>视觉算法C#: center_photo_taking(wood_id+左右相机坐标)
视觉算法C#-->>上位机Go: ACK: center_scanning
视觉算法C#->>视觉算法C#: 异步拍照+端面中心计算
视觉算法C#->>上位机Go: center_photo_result(顶针目标坐标+直径)
上位机Go->>上位机Go: 设置原木直径、触发托料装置升高
上位机Go->>下位机PLC: 写入D2024/D2026/D2028/D2030顶针坐标
上位机Go->>下位机PLC: 写入D2168/D2170轨道目标坐标
上位机Go->>下位机PLC: M49启动顶料
Note over 调度车,视觉算法C#: 视觉扫描阶段
上位机Go->>上位机Go: M50上升沿 + D2130<1600 + D2042>=2300
上位机Go->>视觉算法C#: start_vision_algorithm(wood_id)
视觉算法C#-->>上位机Go: ACK: vision_scanning
视觉算法C#->>视觉算法C#: 异步启动相机采集
视觉算法C#->>上位机Go: capture_ready(status=ready)
上位机Go->>下位机PLC: 写入M103启动扫描轨道
上位机Go->>上位机Go: 启动M104监控(等待工单推送触发)
下位机PLC->>下位机PLC: 扫描轨道运行、编码器触发采集
Note over 调度车,视觉算法C#: 工单推送与算法处理阶段
上位机Go->>上位机Go: M104上升沿 + M42=true
上位机Go->>上位机Go: 读取D2134计算原木长度
上位机Go->>视觉算法C#: order_info(工单信息+规格+长度)
视觉算法C#-->>上位机Go: ACK: algorithm_coordinates_run(status=processing)
视觉算法C#->>视觉算法C#: 停止采集→建模→顶针裁剪→多规格切割优化
视觉算法C#->>上位机Go: algorithm_coordinates(切割坐标+旋转角度+材积)
视觉算法C#->>上位机Go: board_split_yield_result(板材拆分出材率)
Note over 调度车,视觉算法C#: 旋转与调度执行阶段
上位机Go->>上位机Go: 保存视觉数据到数据库+推送LED面板
上位机Go->>下位机PLC: 写入D2038旋转角度
上位机Go->>下位机PLC: M45执行旋转
上位机Go->>下位机PLC: 写入D2136原木直径
上位机Go->>上位机Go: M46上升沿检测(旋转完成)
上位机Go->>上位机Go: 异步计算A/B组夹臂坐标(D2106-D2110/D2162-D2166)
上位机Go->>上位机Go: 触发立锯调度(M807=true)
调度车->>调度车: 取料并调度至立锯
流程说明
1. 初始阶段
- 调度车托料完成: M42上升沿检测,生成WoodID,推送当天原木数量至LED面板
2. 视觉定位阶段(对中拍照)
- M809触发: 升降车D2130在2630±15mm拍照位时,启动红外测距仪
- 红外测距调整: 等待红外读数稳定,计算调整坐标写入D2152/D2154,M119调整拍照位
- M810就位信号: 关闭红外测距仪,写入M130触发坐标调整
- M120就位信号: 监控激光传感器读数,左右均在(850,1000]mm范围内时,推送
center_photo_taking - 异步对中计算: C#端异步执行图漾深度相机拍照和端面中心计算
- 推送对中结果: C#推送
center_photo_result,包含视觉顶针机构目标坐标(mm)和直径(mm) - 写入顶针坐标: Go端将坐标写入D2024/D2026/D2028/D2030,轨道坐标写入D2168/D2170,M49启动顶料
3. 视觉扫描阶段
- M50上升沿检测: 同时验证D2130<1600(升降车安全位)且D2042>=2300(调度车就位)
- D2138方向判断: D2138>-2250为左扫描(left),<=-2250为右扫描(right)
- 推送扫描标识: Go发送
start_vision_algorithm,C#异步启动4台相机持续采集 - capture_ready信号: C#相机就绪后推送就绪信号,Go收到后写入M103启动扫描轨道
4. 工单推送与算法处理阶段
- M104上升沿: 扫描结束触发,同时验证M42=true
- 读取原木长度: 从D2134寄存器读取长度值,减去315mm得到实际原木长度
- 推送工单信息: Go发送
order_info,包含工单规格、副规格、锯路厚度等参数 - 异步算法处理: C#端依次执行:停止采集→等待帧就绪→点云融合建模→Y轴归一化→顶针裁剪→多规格切割优化
- 推送算法结果: C#推送
algorithm_coordinates(切割坐标+旋转角度+材积+出材率) - 推送板材拆分: C#异步推送
board_split_yield_result(板材拆分出材率)
5. 旋转与调度执行阶段
- 数据持久化: Go保存视觉算法数据到数据库(坐标JSON、旋转角度、材积、径级),推送出材率到LED面板
- 写入旋转角度: D2038写入算法角度值(回读验证,最多重试5次)
- 执行旋转: M45写入true执行物理旋转
- 写入原木直径: D2136写入原木直径值
- 旋转完成检测: M46上升沿检测(旋转完成),并行计算A/B组夹臂伺服坐标
- 触发立锯调度: M807=true时触发立锯调度逻辑
关键特性
- 异步推送机制: C#端收到请求后立即返回ACK,算法计算完成后异步推送结果(
center_photo_result/algorithm_coordinates) - capture_ready握手: C#端相机就绪后推送就绪信号,Go端收到后才写入M103启动扫描轨道,避免时序错误
- 多规格对比优化: 算法对比主规格与副规格,选出材率最高的切割方案
- 顶针裁剪: 根据扫描方向(left/right)从点云两端裁剪固定长度,移除机械顶针和过渡区点云
- Y轴归一化: scan_start="right"时翻转Y轴,确保Y=0始终对应同一物理端
- 寄存器回读验证: 关键寄存器(D2038角度值、D2024-D2030顶针坐标)写入后立即回读验证
- 材积基准统一: TotalVolume使用middle Y截面R计算,与AvailableVolume同源,消除出材率偏高
注意事项
- 每个阶段的执行都依赖于前一阶段的成功完成
- C#端TCP服务监听
0.0.0.0:9000,支持多客户端连接 - 视觉算法的准确性直接影响整个调度流程的效果
- Go与C#之间的TCP长连接支持自动重连(默认10秒间隔)
- 红外测距仪读数稳定判断:连续10次相邻读数差值<0.5mm
- 激光传感器有效范围:左/右均在(850, 1000]mm
- 原木长度 = D2134寄存器值 - 315mm(固定偏移)
- 顶针裁剪量根据扫描方向不同:left扫描(低Y端215mm+高Y端172mm),right扫描(低Y端170mm+高Y端209mm)
TCP视觉算法数据接口
接口概述
上位机(go-auto)作为TCP客户端,连接视觉算法服务端(LogMax3D, 监听9000端口),通过JSON行协议(\n分隔)进行双向通信。
通信模型:
- Go → C#: 上位机发送请求(
center_photo_taking、start_vision_algorithm、order_info) - C# → Go: 算法推送异步结果(
center_photo_result、capture_ready、algorithm_coordinates、board_split_yield_result)
通用消息格式
请求格式(Go → C#)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"action": "order_info",
"data": { ... },
"timestamp": 1762158534297
}
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | string | 是 | 请求唯一标识符(UUID) |
| action | string | 是 | 动作类型 |
| data | object | 否 | 请求数据,根据不同action有不同结构 |
| timestamp | number | 是 | 请求时间戳(毫秒) |
响应格式(C# → Go)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"action": "algorithm_coordinates",
"data": { ... },
"timestamp": 1762308230194,
"error_id": ""
}
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | string | 是 | 对应请求的ID |
| action | string | 是 | 动作类型 |
| data | object | 否 | 响应数据 |
| timestamp | number | 是 | 时间戳(毫秒) |
| error_id | string | 否 | 错误码:空字符串=成功,非空=错误标识 |
支持的动作类型
1. center_photo_taking - 视觉取中拍照命令(Go → C#)
功能说明: 上位机下发视觉取中拍照命令,触发C#端图漾深度相机拍照并计算原木端面中心坐标。
触发条件: M120上升沿 && 激光传感器读数左/右均在(850,1000]mm
调用方法: visionClient.SendRequestWithRetry(request) → C# HandleCenterPhotoTaking()
请求示例:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"action": "center_photo_taking",
"data": {
"wood_id": "WOOD_20251103_162602_004_2ace914e",
"left": {
"x": 55050,
"y": 32849,
"z": 0,
"d": 910.5
},
"right": {
"x": 55050,
"y": 32849,
"z": 0,
"d": 912.3
}
},
"timestamp": 1762158362326
}
data参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| wood_id | string | 是 | 原木唯一标识ID |
| left | object | 是 | 左相机在机械坐标系下的绝对位置 |
| left.x | number | 是 | 左相机X坐标(PLC D2016) |
| left.y | number | 是 | 左相机Y坐标(PLC D2018) |
| left.z | number | 是 | 左相机Z坐标(PLC D2126) |
| left.d | number | 否 | 左侧激光传感器读数(mm) |
| right | object | 是 | 右相机在机械坐标系下的绝对位置 |
| right.x | number | 是 | 右相机X坐标(PLC D2020) |
| right.y | number | 是 | 右相机Y坐标(PLC D2022) |
| right.z | number | 是 | 右相机Z坐标(PLC D2128) |
| right.d | number | 否 | 右侧激光传感器读数(mm) |
即时响应(ACK):
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"action": "center_scanning",
"data": {},
"timestamp": 1762158362400,
"error_id": ""
}
2. center_photo_result - 视觉对中结果推送(C# → Go)
功能说明: C#端异步完成拍照和端面中心计算后,主动推送视觉顶针机构目标坐标给上位机。
调用方法: C# SendCenteringResult() → Go handleCenterPhotoResult()
请求示例:
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"action": "center_photo_result",
"data": {
"left": {
"x": 509.34,
"y": 1330.28,
"z": 450.0,
"diameter": 345.6
},
"right": {
"x": 512.15,
"y": 1325.40,
"z": 455.0,
"diameter": 340.2
},
"x_diameter": 348.5,
"y_diameter": 342.1,
"wood_id": "WOOD_20251103_162602_004_2ace914e",
"reg_timestamp": 1762158362326,
"recv_timestamp": 1762308226907,
"send_timestamp": 1762308230194
},
"timestamp": 1762308230194,
"error_id": ""
}
data参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| left | object | 是 | 左端面中心点结果 |
| left.x | number | 是 | 左视觉顶针机构目标X坐标(mm) = 相机位置X + RT变换偏移X |
| left.y | number | 是 | 左视觉顶针机构目标Y坐标(mm) = 相机位置Y + RT变换偏移Y |
| left.z | number | 是 | 相机到原木端面距离(mm) |
| left.diameter | number | 否 | 左端面直径(mm) = (X径+Y径)/2 |
| right | object | 是 | 右端面中心点结果 |
| right.x | number | 是 | 右视觉顶针机构目标X坐标(mm) |
| right.y | number | 是 | 右视觉顶针机构目标Y坐标(mm) |
| right.z | number | 是 | 相机到原木端面距离(mm) |
| right.diameter | number | 否 | 右端面直径(mm) |
| x_diameter | number | 否 | X方向直径(mm) |
| y_diameter | number | 否 | Y方向直径(mm) |
| wood_id | string | 是 | 原木标识 |
| reg_timestamp | number | 否 | 请求时间戳(来自上位机原始请求) |
| recv_timestamp | number | 否 | 视觉系统接收时间戳 |
| send_timestamp | number | 否 | 视觉系统发送结果时间戳 |
Go端处理逻辑:
- 直径单位转换:
diameter / 10(mm → cm) - 设置全局原木直径、X轴直径、Y轴直径
- 触发托料装置升高
- 调用
WriteVisionPinCoordinate()写入顶针坐标至D2024/D2026/D2028/D2030 - 计算轨道目标坐标写入D2168/D2170
- 写入M49启动顶料
3. start_vision_algorithm - 开始视觉扫描(Go → C#)
功能说明: 上位机通知视觉算法开始3D扫描,C#端异步启动4台相机进入持续采集模式。
触发条件: M50上升沿 && D2130<1600 && D2042>=2300
调用方法: visionClient.SendRequestWithRetry(request) → C# HandleStartVision()
请求示例:
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"action": "start_vision_algorithm",
"data": {
"wood_id": "WOOD_20251103_162602_004_2ace914e"
},
"timestamp": 1762158400000
}
data参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| wood_id | string | 是 | 原木唯一标识ID |
即时响应(ACK):
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"action": "vision_scanning",
"data": {
"status": "scanning"
},
"timestamp": 1762158400100,
"error_id": ""
}
异步响应(相机就绪后推送):
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"action": "capture_ready",
"data": {
"status": "ready"
},
"timestamp": 1762158401500,
"error_id": ""
}
说明:
capture_ready的status字段可能为ready(成功)或error(失败)。Go端收到ready后写入M103启动扫描轨道,收到error则不写入M103。
Go端capture_ready处理逻辑:
- 检查
error_id和status字段 - 读取
pendingScanStart(D2138方向判断结果) - 写入M103=true启动扫描轨道
- 启动M104监控goroutine,等待工单推送触发
4. order_info - 工单信息下发(Go → C#)
功能说明: 上位机下发工单信息,代表扫描结束。C#端停止采集并执行建模+切割优化算法。
触发条件: M104上升沿 && M42=true
调用方法: visionClient.SendRequestWithRetry(request) → C# HandleOrderInfo()
请求示例:
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"action": "order_info",
"data": {
"wood_id": "WOOD_20251103_162602_004_2ace914e",
"order_sn": "SO20251101000002",
"order_spec": "38*89*3.98",
"sub_spec": ["38*89*3.98", "37*66*4.1"],
"has_bark": true,
"grade": "等级A",
"start_distance": 115,
"end_distance": 115,
"length": 3785.0,
"saw_thickness": 1.8,
"scan_start": "left"
},
"timestamp": 1762158534297
}
data参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| wood_id | string | 是 | 原木唯一标识ID |
| order_sn | string | 是 | 工单编号 |
| order_spec | string | 是 | 工单主规格(格式:厚宽长),如"38893.98" |
| sub_spec | []string | 否 | 副规格列表,算法对比主规格与各副规格选出材率最高方案 |
| has_bark | bool | 否 | 是否带树皮(默认true) |
| grade | string | 否 | 原木等级(如"等级A") |
| start_distance | number | 否 | 起始距离(mm):侧面相机至原木端面距离(常量115mm) |
| end_distance | number | 否 | 结束距离(mm):侧面相机至原木端面距离(常量115mm) |
| length | number | 否 | 原木长度(mm):D2134寄存器值 - 315 |
| saw_thickness | number | 否 | 锯片厚度(mm),默认1.8 |
| scan_start | string | 否 | 初始扫描方向:"left"或"right"(基于D2138判断) |
即时响应(ACK):
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"action": "algorithm_coordinates_run",
"data": {
"status": "processing"
},
"timestamp": 1762158534400,
"error_id": ""
}
说明:
order_info代表扫描结束,C#收到后立即响应ACK,然后异步执行:停止采集→等待帧就绪→点云融合→建模→顶针裁剪→多规格切割优化→推送结果。
5. algorithm_coordinates - 算法切割坐标结果(C# → Go)
功能说明: C#端切割优化算法计算完成后,主动推送最优切割方案给上位机。
调用方法: C# TcpServer.SendToClientAsync() → Go handleAlgorithmCoordinates()
请求示例:
{
"id": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"action": "algorithm_coordinates",
"data": {
"left": [120.5, 80.3, 40.1],
"right": [15.2, 55.8, 96.4, 137.0],
"rotation_angle": 15.5,
"available_volume": 0.85,
"total_volume": 1.205,
"yield_rate": 70.5,
"wood_id": "WOOD_20251103_162602_004_2ace914e"
},
"timestamp": 1762158540000,
"error_id": ""
}
data参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| left | []number | 是 | 左侧锯片坐标数组(mm),距中心距离,1-15个元素 |
| right | []number | 是 | 右侧锯片坐标数组(mm),距中心距离,1-15个元素 |
| rotation_angle | number | 是 | 旋转角度(度) |
| available_volume | number | 是 | 可用材积(m³) |
| total_volume | number | 否 | 整根原木材积(m³),基于middle Y截面R计算 |
| yield_rate | number | 否 | 出材率(%),如70.5表示70.5% |
| wood_id | string | 否 | 原木ID |
Go端处理逻辑:
- 验证
error_id为空 - 解析
left/right数组(长度1-15)并转换为float32 - 保存视觉数据到数据库(坐标JSON、旋转角度、材积、径级)
- 推送出材率到LED面板
- 写入D2038旋转角度(回读验证,最多重试5次,容差0.01度)
- 写入M45=true执行旋转
- 写入D2136原木直径
- 启动M46上升沿监控(旋转完成后触发立锯调度)
- 异步计算A/B组夹臂伺服坐标
6. board_split_yield_result - 板材拆分出材率(C# → Go)
功能说明: C#端异步完成板材拆分计算后,推送出材率结果给上位机。此消息在algorithm_coordinates之后异步推送,不阻塞主流程。
调用方法: C# BoardSplitYieldService.RunAsync() → Go handleBoardSplitYieldResult()
请求示例:
{
"id": "",
"action": "board_split_yield_result",
"data": {
"status": "completed",
"wood_id": "WOOD_20251103_162602_004_2ace914e",
"yield_rate": 68.5,
"n_boards": 6,
"n_blocks": 42,
"block_volume": 0.583,
"available_volume": 0.85,
"ply_file": "data/modeling/WOOD_20251103_162602_004_2ace914e/board_split.ply",
"png_file": "data/modeling/WOOD_20251103_162602_004_2ace914e/board_split.png"
},
"timestamp": 1762158545000,
"error_id": ""
}
data参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| status | string | 是 | 计算状态:"completed"=完成,"error"=错误 |
| wood_id | string | 是 | 原木唯一标识ID |
| yield_rate | number | 是 | 板材拆分出材率(%) |
| n_boards | number | 是 | 板材数量 |
| n_blocks | number | 是 | 木方数量 |
| block_volume | number | 是 | 木方总体积(m³) |
| available_volume | number | 是 | 可用材积(m³) |
| ply_file | string | 否 | PLY点云可视化文件路径 |
| png_file | string | 否 | PNG截面图文件路径 |
7. connection_ack - 连接确认(C# → Go)
功能说明: TCP连接建立后,C#端立即推送连接确认消息。
请求示例:
{
"id": "",
"action": "connection_ack",
"data": null,
"timestamp": 1762158000000,
"error_id": ""
}
错误处理
错误响应格式
当请求处理失败时,C#端通过error_id字段标识错误:
{
"id": "req001",
"action": "algorithm_coordinates",
"data": {},
"timestamp": 1705123456789,
"error_id": "err_algorithm"
}
常见错误码
| error_id | 说明 | 触发场景 |
|---|---|---|
"" (空字符串) |
成功 | 正常处理 |
err_unknown_action |
未知动作类型 | action不在支持列表中 |
err_empty_data |
请求data为空 | order_info缺少data |
err_parse_order |
解析OrderData失败 | JSON格式错误 |
err_algorithm |
算法计算异常 | 切割优化过程异常 |
err_no_data |
未采集到有效数据 | 相机未采集到帧数据 |
err_no_model |
无原木3D模型 | 未收到start_vision_algorithm |
err_start_capture |
启动采集失败 | capture_ready推送error状态 |
err003 |
对中计算异常 | CenteringService计算过程异常 |
err004 |
对中服务未初始化 | 图漾相机未连接或未启用 |
完整调用链路时序
Go(上位机) C#(视觉算法)
| |
| ── center_photo_taking ──────────────────► | 对中拍照请求
| ◄── center_scanning (ACK) ─────────────── | 立即响应
| | 异步: 拍照+计算
| ◄── center_photo_result ────────────────── | 推送对中坐标
| |
| ── start_vision_algorithm ───────────────► | 开始扫描请求
| ◄── vision_scanning (ACK) ─────────────── | 立即响应
| | 异步: 启动相机采集
| ◄── capture_ready ──────────────────────── | 相机就绪信号
| (写入M103启动扫描轨道) |
| |
| ── order_info ───────────────────────────► | 工单信息(扫描结束)
| ◄── algorithm_coordinates_run (ACK) ────── | 立即响应
| | 异步: 停止采集→建模→切割优化
| ◄── algorithm_coordinates ──────────────── | 推送切割坐标+材积
| ◄── board_split_yield_result ───────────── | 推送板材拆分出材率
| |
接口使用注意事项
- 数据格式:所有请求为JSON行协议,每条消息以
\n结尾 - 坐标精度:坐标值支持float64精度,Go端内部转换为float32写入PLC
- TCP长连接:Go端使用带自动重连的TCP客户端,断线自动重连(默认10秒间隔)
- 异步推送:C#端收到请求后立即返回ACK,计算完成后异步推送结果,Go端通过注册的
asyncHandler处理 - 请求ID:使用UUID生成唯一请求ID,便于追踪和调试
- 超时处理:Go端
SendRequestWithRetry支持重试机制(可配置重试次数和间隔) - 线程安全:C#端使用SemaphoreSlim保证StreamWriter写入线程安全
- 连接清理:C#端每60秒自动清理已断开的客户端连接,防止僵尸连接积累
- 直径单位:C#推送的直径单位为mm,Go端接收后除以10转换为cm存储
- 扫描方向:scan_start由D2138寄存器判断,>-2250为"left",<=-2250为"right"