在智能硬件项目中,触摸屏经常承担"输入"和"显示"两个角色。电子画板、设备配置面板、手写签名、交互式控制台、工业设备调试界面,都需要把手指触摸的位置转换成程序能够处理的数据,再通过屏幕反馈成可见图形。对于 Python 硬件编程入门而言,触摸屏画图实验很适合作为图形交互的起点,因为它能直观看到坐标读取、状态判断、图像绘制和屏幕刷新之间的关系。
本实验基于 CanMV K210 开发板实现一个简单的触摸屏画图 Demo。程序会初始化 LCD 显示屏和 I2C 触摸屏,手指按下或移动时,屏幕上会绘制红色触摸点和白色连续轨迹。按下 BOOT_KEY 后,程序会清空画布并重新绘制顶部状态栏。整个实验不是简单显示一张图片,而是让代码持续读取触摸状态,把真实手势转化成屏幕上的线条。
| 学习目标 | 说明 |
|---|---|
| 理解图形交互 | 认识触摸输入、图像绘制和 LCD 显示之间的关系 |
| 掌握触摸坐标读取 | 通过 I2C 触摸屏读取触摸状态、X 坐标和 Y 坐标 |
| 学会 LCD 画布绘制 | 使用 image.Image() 创建画布,并绘制文字、圆点和线条 |
| 理解轨迹绘制逻辑 | 通过保存上一帧坐标,把离散触摸点连接成连续轨迹 |
| 掌握按键清屏 | 使用 BOOT_KEY 作为清空画布的输入按钮 |
| 建立交互项目思路 | 为触摸菜单、参数面板、手写输入和硬件控制界面打基础 |
这个实验的重点不只是"能在屏幕上画线",而是建立触摸屏交互的基本流程:触摸屏负责输入坐标,程序负责判断状态和绘制图形,LCD 负责显示结果,BOOT_KEY 负责触发清屏操作。后续的触摸菜单、屏幕按钮、参数调节界面和 AI 识别结果标注,都可以复用这套结构。
文章目录
理论基础
触摸屏实验同时包含输入和输出两个方向。输入来自触摸屏,程序需要读取手指是否按下、是否移动,以及当前触摸坐标;输出来自 LCD,程序需要把点、线、文字和背景画到图像画布上,再刷新到屏幕显示。这个过程比普通 LED 或按键实验更接近真实的人机交互系统。
LCD 本身主要负责显示,不能直接理解触摸动作。触摸屏则通过 I2C 总线向 K210 提供触摸状态和坐标数据。程序读取到坐标后,并不是直接把坐标显示出来,而是根据状态判断是否需要绘制点或线。当触摸状态为按下或移动时,程序会在当前位置画一个红色小点;如果已经存在上一帧坐标,就把上一帧坐标和当前坐标连成白色线段,从而形成连续轨迹。
BOOT_KEY 在本实验中作为清屏按钮使用。它不参与触摸轨迹绘制,只负责触发 clear_canvas()。按键按下后,程序会清空当前画布,然后重新绘制顶部提示栏。这个设计让触摸屏负责绘图输入,BOOT_KEY 负责功能控制,LCD 负责统一显示,形成一个完整的交互闭环。
手指触摸屏幕
按下 / 移动 / 松开
I2C 触摸屏
读取状态与坐标
CanMV K210
IO30=SCL / IO31=SDA
ts.read()
返回 status, x, y
状态判断
PRESS / MOVE / IDLE
image 画布绘制
点 / 线 / 顶部提示栏
lcd.display(img)
刷新到 LCD
BOOT_KEY
IO16
GPIO1 输入检测
clear_canvas()
清空画布并重绘提示栏
这张流程图展示的是触摸屏画图实验的控制链路。触摸屏通过 I2C 把状态和坐标传给 K210,程序根据坐标在 image 画布上绘制图形,再通过 LCD 显示出来。BOOT_KEY 作为额外输入,负责触发清屏逻辑。整个实验体现了"输入事件 -> 程序处理 -> 图形反馈"的交互过程。
轨迹绘制的关键是"上一点"和"当前点"。如果每次触摸只画一个圆点,手指快速移动时屏幕上会出现一串分散的点,轨迹不够连续。代码中使用 last_x、last_y 和 has_last_point 记录上一帧触摸位置,当新的触摸点到来时,程序用 draw_line() 把两个点连接起来。这样手指移动时,屏幕上就会形成连续线条。
硬件设施
本实验围绕 LCD 显示屏、I2C 触摸屏和 BOOT_KEY 按键展开。LCD 负责显示画布和触摸轨迹,触摸屏通过 I2C 总线向 K210 提供触摸状态与坐标数据,BOOT_KEY 作为清空画布的输入按钮。软件侧主要使用 lcd、image、touchscreen、machine.I2C、maix.GPIO 和 fpioa_manager.fm,分别负责显示刷新、图像绘制、触摸读取、I2C 通信、按键输入和引脚功能映射。
实验接线和运行效果可以通过下面的图片建立整体印象。触摸屏负责输入坐标,LCD 负责显示轨迹,BOOT_KEY 用于清空画布。检查硬件时重点关注触摸屏 I2C 通信、LCD 显示是否正常,以及 BOOT_KEY 是否能被程序读取。

| 硬件 / 软件 | 作用 | 说明 |
|---|---|---|
| CanMV K210 开发板 | 实验运行平台 | 负责执行 Python 程序,连接 LCD、触摸屏和 BOOT_KEY |
| LCD 显示屏 | 图像输出设备 | 显示顶部提示信息、触摸点和连续轨迹 |
| I2C 触摸屏 | 坐标输入设备 | 通过触摸状态、X 坐标和 Y 坐标反馈手指操作 |
| BOOT_KEY | 清空画布按钮 | 按下后触发 clear_canvas(),用于重新开始绘制 |
| IO30 | I2C 时钟线 | 根据代码和注释推导,对应触摸屏 SCL |
| IO31 | I2C 数据线 | 根据代码和注释推导,对应触摸屏 SDA |
| IO16 | 按键输入引脚 | 根据代码推导,注册为 GPIO1 后读取 BOOT_KEY 状态 |
lcd |
屏幕控制模块 | 用于初始化屏幕、清屏和显示图像 |
image |
图像绘制模块 | 创建画布,并绘制矩形、文字、圆点和线条 |
touchscreen |
触摸屏模块 | 初始化触摸屏并读取触摸状态和坐标 |
machine.I2C |
I2C 通信模块 | 创建触摸屏通信总线 |
maix.GPIO |
GPIO 控制模块 | 用于读取 BOOT_KEY 的按下状态 |
fpioa_manager.fm |
引脚映射模块 | 将物理引脚 IO16 注册为 GPIO1 |
接线关系可以从代码中的 I2C 配置、按键注册和注释说明中推导出来。触摸屏通过 I2C 与 K210 通信,SCL 对应 IO30,SDA 对应 IO31。BOOT_KEY 使用 IO16,并通过 fm.register(CLEAR_KEY_PIN, CLEAR_KEY_GPIO, force=True) 注册为 GPIO1。LCD 的具体硬件连接没有在代码中展开,程序通过 lcd.init() 和 lcd.display(img) 直接调用显示接口。
| 物理引脚 / 接口 | 功能引脚 / 总线 | 代码变量 | 对应硬件 | 说明 |
|---|---|---|---|---|
| IO30 | I2C SCL | I2C_SCL_PIN |
触摸屏 SCL | 用作触摸屏 I2C 时钟线 |
| IO31 | I2C SDA | I2C_SDA_PIN |
触摸屏 SDA | 用作触摸屏 I2C 数据线 |
| IO16 | GPIO1 | btn_clear |
BOOT_KEY | 按下后用于清空画布 |
| LCD 显示接口 | LCD 控制接口 | lcd、img |
LCD 屏幕 | 通过 LCD 模块接口完成显示 |
| I2C0 | 触摸通信总线 | TOUCH_I2C_ID |
触摸屏通信 | 用于读取触摸状态和坐标 |
| VCC / GND | 供电与地线 | 无 | LCD 与触摸屏 | 保证屏幕和触摸通信正常工作 |
物理引脚可以理解成开发板连接真实硬件的入口,功能引脚则是程序对这些入口赋予的角色。IO30 和 IO31 被用于 I2C 通信,负责让 K210 与触摸屏交换坐标数据;IO16 被注册成 GPIO1,负责读取按键是否被按下;LCD 显示部分由 lcd 模块统一管理,程序只需要把绘制好的 img 画布刷新到屏幕上。
| 实验现象 | 正常表现 | 异常提示 |
|---|---|---|
| 程序启动 | LCD 显示顶部提示栏,画布区域为空 | 无显示时检查 LCD 初始化和程序运行状态 |
| 手指按下 | 触摸位置出现红色小点 | 无触摸响应时检查 I2C 触摸屏初始化 |
| 手指移动 | 屏幕显示白色连续轨迹 | 轨迹断续时检查刷新频率和触摸采样速度 |
| 手指松开 | 轨迹停止延伸,下一次触摸重新记录起点 | 松开后仍乱画时检查触摸状态读取 |
| 按下 BOOT_KEY | 画布清空,顶部提示栏保留 | 无法清屏时检查 IO16 和按键电平 |
| 顶部区域无法画线 | y 坐标小于等于 30 时不绘制轨迹 | 这是为了保留状态栏,不属于故障 |
软件代码
本实验代码围绕触摸屏画图功能展开。程序先配置 LCD 尺寸、I2C 引脚、清屏按键、颜色参数和防抖时间,再分别初始化 LCD、触摸屏、BOOT_KEY 和画布。主循环中不断读取触摸状态,触摸移动时绘制连续线条,按键按下时清空画布,并持续刷新 LCD 显示。
| 软件环境 | 作用 | 检查重点 |
|---|---|---|
| CanMV IDE | 编辑、运行和调试 K210 程序 | 能识别开发板串口,并能正常运行基础测试程序 |
| CanMV 固件 | 提供 lcd、image、touchscreen、machine.I2C 等模块 |
固件环境需要支持 LCD 和触摸屏相关接口 |
| LCD 显示模块 | 显示画布和轨迹 | 程序启动后能看到顶部提示栏 |
| I2C 触摸屏模块 | 返回触摸状态和坐标 | 手指按下或移动时坐标会变化 |
| BOOT_KEY | 清空画布输入 | 按下后能触发清屏 |
| 串口终端 | 查看触摸状态变化 | 状态变化时能看到 Touch Status 输出 |
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
CanMV K210 触摸屏画图实验 Demo
功能说明:
1. 初始化 LCD 显示屏
2. 初始化 I2C 触摸屏
3. 手指按下或移动时,在屏幕上绘制连续轨迹
4. 按下 BOOT_KEY 清空画布
5. 屏幕顶部显示简单状态提示
接线说明:
触摸屏通常通过 I2C 与 K210 通信
SCL -> IO30
SDA -> IO31
BOOT_KEY -> IO16
"""
import time
import lcd
import image
import touchscreen as ts
from machine import I2C
from maix import GPIO
from fpioa_manager import fm
# =========================
# 硬件配置区
# =========================
LCD_WIDTH = 320
LCD_HEIGHT = 240
I2C_SCL_PIN = 30
I2C_SDA_PIN = 31
CLEAR_KEY_PIN = 16
CLEAR_KEY_GPIO = fm.fpioa.GPIO1
TOUCH_I2C_ID = I2C.I2C0
TOUCH_I2C_FREQ = 400000
PEN_COLOR = (255, 255, 255)
TEXT_COLOR = (0, 255, 0)
POINT_COLOR = (255, 0, 0)
BG_COLOR = (0, 0, 0)
CLEAR_DEBOUNCE_MS = 300
# =========================
# 全局状态
# =========================
img = None
btn_clear = None
last_x = 0
last_y = 0
last_status = ts.STATUS_IDLE
has_last_point = False
last_clear_time = 0
# =========================
# 初始化函数
# =========================
def init_lcd():
"""初始化 LCD 显示屏"""
lcd.init()
lcd.clear()
def init_touchscreen():
"""初始化触摸屏"""
i2c = I2C(
TOUCH_I2C_ID,
freq=TOUCH_I2C_FREQ,
scl=I2C_SCL_PIN,
sda=I2C_SDA_PIN
)
ts.init(i2c)
# 首次使用触摸屏位置不准确时,可以取消下面这一行注释进行校准
# ts.calibrate()
def init_clear_button():
"""初始化清空按钮"""
global btn_clear
fm.register(CLEAR_KEY_PIN, CLEAR_KEY_GPIO, force=True)
btn_clear = GPIO(GPIO.GPIO1, GPIO.IN)
def create_canvas():
"""创建画布"""
global img
img = image.Image(size=(LCD_WIDTH, LCD_HEIGHT))
clear_canvas()
def init_hardware():
"""统一初始化硬件"""
init_lcd()
init_touchscreen()
init_clear_button()
create_canvas()
# =========================
# 画布函数
# =========================
def draw_header():
"""绘制顶部提示信息"""
img.draw_rectangle(0, 0, LCD_WIDTH, 28, color=(20, 20, 20), fill=True)
img.draw_string(8, 8, "Touch Draw Demo", color=TEXT_COLOR, scale=1)
img.draw_string(180, 8, "BOOT: Clear", color=TEXT_COLOR, scale=1)
def clear_canvas():
"""清空画布"""
global has_last_point
img.clear()
draw_header()
has_last_point = False
lcd.display(img)
def draw_touch_point(x, y):
"""绘制当前触摸点"""
if y > 30:
img.draw_circle(x, y, 2, color=POINT_COLOR, fill=True)
def draw_touch_line(x1, y1, x2, y2):
"""绘制触摸轨迹线"""
if y1 > 30 and y2 > 30:
img.draw_line((x1, y1, x2, y2), color=PEN_COLOR)
# =========================
# 触摸处理函数
# =========================
def is_touching(status):
"""判断当前是否处于触摸状态"""
return status == ts.STATUS_PRESS or status == ts.STATUS_MOVE
def handle_touch():
"""读取触摸数据并绘制轨迹"""
global last_x, last_y, last_status, has_last_point
status, x, y = ts.read()
if is_touching(status):
if has_last_point:
draw_touch_line(last_x, last_y, x, y)
else:
has_last_point = True
draw_touch_point(x, y)
last_x = x
last_y = y
else:
has_last_point = False
if status != last_status:
print("Touch Status:", status, "X:", x, "Y:", y)
last_status = status
# =========================
# 按键处理函数
# =========================
def handle_clear_button():
"""检测 BOOT_KEY 是否被按下,按下后清空画布"""
global last_clear_time
now = time.ticks_ms()
if btn_clear.value() == 0:
if time.ticks_diff(now, last_clear_time) > CLEAR_DEBOUNCE_MS:
print("Canvas cleared")
clear_canvas()
last_clear_time = now
# =========================
# 主循环
# =========================
def loop():
"""主循环"""
while True:
handle_touch()
handle_clear_button()
lcd.display(img)
time.sleep_ms(10)
# =========================
# 程序入口
# =========================
if __name__ == '__main__':
try:
init_hardware()
loop()
except KeyboardInterrupt:
lcd.clear()
print("Program stopped")
这段程序可以分成硬件配置、全局状态、初始化函数、画布函数、触摸处理函数、按键处理函数和主循环。硬件配置区集中保存 LCD 尺寸、I2C 引脚、清屏按键、颜色参数和防抖时间;初始化函数负责准备 LCD、触摸屏、BOOT_KEY 和画布;画布函数负责绘制顶部提示栏、清空画面、绘制点和线;触摸处理函数负责读取坐标并更新轨迹;按键处理函数负责检测清屏操作;主循环负责持续刷新。
代码中所有图形都先绘制到 img 画布中,再通过 lcd.display(img) 显示到屏幕。这样做的好处是绘制逻辑比较集中,后续增加文字、按钮、边框或状态提示时,都可以继续在同一张画布上操作。顶部提示栏通过 draw_header() 绘制,画图区域通过 y > 30 限制,避免手指在顶部区域绘图时覆盖提示文字。
| 函数名 | 功能 | 对应现象 |
|---|---|---|
init_lcd() |
初始化 LCD 显示屏 | 屏幕进入可显示状态,并清理旧画面 |
init_touchscreen() |
初始化 I2C 触摸屏 | 触摸屏可以返回触摸状态和坐标 |
init_clear_button() |
初始化 BOOT_KEY | 按键可以被程序读取,用于清空画布 |
create_canvas() |
创建图像画布 | 生成一张 320×240 的绘图区域 |
init_hardware() |
统一初始化硬件 | 程序启动后完成屏幕、触摸和按键准备 |
draw_header() |
绘制顶部提示栏 | 屏幕顶部显示 Demo 名称和清屏提示 |
clear_canvas() |
清空画布 | 轨迹被清除,顶部提示栏重新出现 |
draw_touch_point() |
绘制触摸点 | 手指触摸位置出现红色小点 |
draw_touch_line() |
绘制触摸轨迹 | 手指移动路径形成白色连续线条 |
is_touching() |
判断触摸状态 | 区分按下、移动和空闲状态 |
handle_touch() |
读取触摸并绘图 | 手指按下或移动时,屏幕显示轨迹 |
handle_clear_button() |
检测清屏按键 | 按下 BOOT_KEY 后清空画布 |
loop() |
持续执行主循环 | 程序不断读取触摸、检测按键并刷新屏幕 |
handle_touch() 是轨迹绘制的核心。ts.read() 返回触摸状态、X 坐标和 Y 坐标,当状态为按下或移动时,程序会判断是否已经有上一个触摸点。如果有,就把上一点和当前点连成线;如果没有,就从当前点开始记录。手指松开后,has_last_point 会被重置,下一次触摸会重新开始新的轨迹,避免不同笔画之间被错误连线。
handle_clear_button() 使用了按键防抖逻辑。BOOT_KEY 按下时,程序先判断距离上一次清屏是否超过 CLEAR_DEBOUNCE_MS。只有超过 300 毫秒,才执行清屏操作。这样可以减少机械按键抖动造成的重复清屏。主循环每 10 毫秒运行一次,既能保持较好的触摸响应速度,也能避免程序无意义地高速空转。
扩展应用
触摸屏画图实验同时涉及显示、I2C 通信、触摸坐标、按键输入和刷新频率。排查时需要把问题拆开看,屏幕不显示优先检查 LCD 初始化,触摸无反应优先检查 I2C 和触摸屏初始化,清屏无效优先检查 BOOT_KEY 引脚和电平逻辑。
| 问题现象 | 可能原因 | 处理思路 |
|---|---|---|
| LCD 没有显示内容 | LCD 初始化失败、屏幕连接异常、程序没有运行 | 检查 lcd.init() 是否正常执行,确认程序已经进入主循环 |
| 触摸没有轨迹 | I2C 引脚不匹配、触摸屏没有初始化成功、触摸模块异常 | 核对 IO30 和 IO31 是否对应 SCL、SDA,检查 ts.init(i2c) 是否报错 |
| 轨迹位置偏移 | 触摸屏校准数据不准确 | 取消 ts.calibrate() 注释进行校准,再重新测试坐标位置 |
| 顶部区域无法画线 | 代码限制 y > 30 才允许绘图 |
这是为了保留顶部提示栏,属于程序设计逻辑 |
| 按下 BOOT_KEY 没有清屏 | IO16 映射不正确、按键电平逻辑不匹配、按键没有接通 | 检查 fm.register() 和 GPIO.GPIO1 是否一致,观察 btn_clear.value() 的返回值 |
| 按键触发多次清屏 | 按键抖动或防抖时间过短 | 适当增大 CLEAR_DEBOUNCE_MS,例如从 300 调整到 500 |
| 画线不连续 | 循环刷新太慢、触摸采样间隔过大、手指移动速度太快 | 适当减小 time.sleep_ms(10),或降低手指移动速度进行测试 |
| 画面刷新有闪烁感 | LCD 刷新频率与绘图频率不匹配 | 保持画布绘制后统一刷新,避免在多个函数里频繁刷新屏幕 |
| 串口持续输出较少 | 代码只在触摸状态变化时打印状态 | 这是正常设计,持续移动时主要观察 LCD 轨迹变化 |
触摸屏画图实验的价值不只在于做一个简易电子画板,更重要的是建立图形交互思维。程序从触摸屏读取坐标,再把坐标转换成图形、线条和状态变化,这个过程可以迁移到很多实际项目中。设备参数设置、手写签名、交互菜单、坐标调试、轨迹采集和屏幕控制面板,本质上都可以建立在同样的触摸读取与图像绘制逻辑之上。
| 应用场景 | 实现思路 | 可扩展能力 |
|---|---|---|
| 电子画板 | 继续使用触摸坐标绘制点和线 | 可增加画笔颜色、线宽、橡皮擦和保存图片功能 |
| 手写签名采集 | 将触摸轨迹记录为坐标序列或图像 | 可扩展为签名确认、轨迹存档和身份验证实验 |
| 设备触控面板 | 把屏幕区域划分为不同按钮 | 可扩展菜单切换、参数设置和模式选择 |
| 触摸坐标调试 | 在屏幕上显示触摸点位置 | 可用于校准屏幕、检查坐标方向和识别触摸区域 |
| 简易白板工具 | 在 LCD 上绘制文字、线条和提示信息 | 可扩展为教学演示、会议草图或调试标记工具 |
| 轨迹采集实验 | 保存手指移动路径的坐标变化 | 可扩展为手势识别、运动轨迹分析和交互实验 |
| 智能硬件控制入口 | 使用触摸区域触发不同动作 | 后续可扩展蜂鸣器、LED、电机或传感器联动 |
| AI 识别结果标注 | 在摄像头画面上叠加触摸标记 | 可扩展为目标选择、区域标注和交互式识别 |
从工程结构看,当前代码已经具备较好的扩展基础。硬件初始化被单独封装,画布操作和触摸处理相互独立,清屏按键也被拆成单独函数。新增功能时,可以围绕 handle_touch() 增加触摸区域判断,也可以围绕 draw_touch_line() 扩展画笔样式,还可以在 clear_canvas() 中加入更多界面元素。后续接入 LED、蜂鸣器、电机或传感器时,触摸屏就可以从"画图工具"升级成"硬件控制入口"。
总结
本实验通过 CanMV K210 完成了一个触摸屏画图 Demo,核心能力包括 LCD 初始化、I2C 触摸屏通信、GPIO 按键输入、图像画布创建、坐标读取、状态判断、线条绘制、屏幕刷新和按键防抖。代码把真实触摸动作转换成坐标数据,再通过 image 模块绘制成可见轨迹,完整展示了 Python 程序处理硬件交互的过程。
这类实验非常适合作为图形化硬件交互入门案例。触摸状态对应程序分支,坐标变化对应屏幕图形,按键输入对应画布清空,主循环负责把这些动作持续串联起来。理解这套流程后,后续可以继续扩展到触摸菜单、参数配置界面、手势识别、传感器数据显示、摄像头图像标注和 AI 识别交互等方向。触摸屏不只是显示模块,而是智能硬件项目中非常重要的人机交互入口。