CircuitPython I2C与HID实战:从TSL2591传感器到键盘鼠标模拟
1. 项目概述与核心价值如果你正在玩转像Adafruit ItsyBitsy、Metro这类小巧但功能强大的CircuitPython开发板并且想让它们不仅仅是运行几行简单的脚本而是真正地与外部世界“对话”——比如读取一个高精度的环境传感器数据或者干脆把你的硬件项目变成一个能控制电脑的键盘或鼠标——那么这篇文章就是为你准备的。我花了相当多的时间在I2C通信和HID设备模拟这两个核心主题上它们可以说是连接物理世界与数字世界、让硬件项目“活”起来的关键桥梁。今天我就以TSL2591高动态范围数字光传感器为例带你从最基础的连线开始一步步实现I2C设备扫描、数据读取并最终将开发板打造成一个能模拟键盘输入和鼠标操作的人机交互设备。无论你是想制作一个自动记录光照数据的环境监测站还是一个通过物理按钮触发复杂快捷键的宏键盘这里面的思路和代码都能直接拿来用。2. I2C通信基础与硬件连接2.1 I2C协议核心原理浅析在动手接线之前花两分钟理解I2CInter-Integrated Circuit到底是怎么工作的能让你在后续调试时事半功倍。你可以把它想象成一个老师主设备和一群学生从设备在教室里的问答环节。老师手里握着唯一的哨子SCL时钟线他吹一下哨子就代表可以开始或结束一次提问。数据线SDA就像是传递纸条的通道。每个学生从设备都有一个唯一的学号7位I2C地址。当老师想找某个学生时他会先吹哨示意开始然后对着全班喊出这个学号。只有学号匹配的学生才会起立应答并通过纸条通道传递答案数据。其他学生则保持安静。这就是I2C最基本的“主-从”通信模式。它的两大优势在于节省引脚和支持多设备。传统的并行通信可能需要8根、16根数据线而I2C只用两根线SCL和SDA就能搞定这对于引脚资源紧张的微控制器比如我们用的这些小巧开发板来说简直是福音。同时理论上你可以在同一条总线上挂载上百个设备受限于地址空间和总线电容通过地址来区分它们这使得扩展多个传感器变得非常整洁。2.2 TSL2591传感器与开发板接线实战我们今天的“学生”是TSL2591一个非常灵敏的数字光传感器它能同时感知可见光和红外光计算出精确的照度值Lux。它的“学号”I2C地址通常是0x29。现在我们需要把它和“老师”我们的CircuitPython开发板连接起来。接线是硬件项目的第一步也是最容易出错的一步。务必在断电状态下操作。以下是针对不同型号Adafruit开发板的接线方法核心就四根线电源、地、时钟线SCL、数据线SDA。对于 ItsyBitsy M0 Express 和 ItsyBitsy M4 Express开发板 USB 接口-传感器 VIN给传感器供电。注意这里是用开发板的USB电压通常是5V直接给传感器的VIN引脚供电。开发板 G-传感器 GND共地为电路提供参考零点。开发板 SCL-传感器 SCL连接I2C时钟线。开发板 SDA-传感器 SDA连接I2C数据线。对于 Metro M0 Express 和 Metro M4 Express开发板 5V-传感器 VINMetro板上有明确的5V引脚。开发板 GND-传感器 GND接地。开发板 SCL-传感器 SCL时钟线连接。开发板 SDA-传感器 SDA数据线连接。注意很多传感器包括TSL2591也支持3.3V逻辑电平。如果你看到传感器上既有VIN也有3Vo引脚并且你的开发板逻辑电平是3.3V像ItsyBitsy、Metro的I/O引脚都是3.3V那么将传感器连接到3.3V电源和地也是完全可以的有时甚至更安全。但遵循官方指南连接VIN到5V通常能保证传感器工作在最稳定的状态。接好线后先别急着写代码。肉眼检查一遍所有杜邦线是否插紧有没有虚接或短路比如相邻引脚不小心碰在一起。硬件连接是后续所有软件工作的基石这里马虎不得。3. CircuitPython I2C设备扫描与寻址3.1 编写I2C扫描程序硬件连接妥当后第一件事不是读数据而是“点名”。我们需要确认开发板是否真的“看”到了传感器并且知道它的地址。这就是I2C扫描。下面这个脚本是你在CircuitPython里进行I2C调试的“瑞士军刀”务必保存好。# SPDX-FileCopyrightText: 2017 Limor Fried for Adafruit Industries # SPDX-License-Identifier: MIT CircuitPython I2C Device Address Scan import time import board # 使用板子默认的I2C总线大多数开发板 i2c board.I2C() # 这会自动使用 board.SCL 和 board.SDA 引脚 # 如果你使用的是板载的STEMMA QT连接器可以取消下面这行的注释 # i2c board.STEMMA_I2C() # 如果需要手动指定引脚创建I2C总线例如使用非标引脚可以这样 # import busio # i2c busio.I2C(board.SCL1, board.SDA1) # 适用于QT Py RP2040等有额外I2C端口的板子 # 尝试锁定I2C总线。锁定后只有当前代码可以访问总线防止冲突。 while not i2c.try_lock(): pass try: while True: # 执行扫描并将找到的地址转换为十六进制格式显示 found_addresses [hex(addr) for addr in i2c.scan()] print(I2C addresses found:, found_addresses) time.sleep(2) # 每2秒扫描一次 finally: # 无论是否出错最终都要解锁I2C总线这是一个好习惯 i2c.unlock()将这段代码保存为你的code.py然后通过串行控制台如Mu编辑器、Thonny或screen/putty查看输出。如果一切正常你应该会看到类似I2C addresses found: [0x29]的输出。这说明总线通信正常并且成功找到了地址为0x29的设备。实操心得如果扫描结果为空列表[]首先别慌按以下步骤排查复查接线这是最常见的问题。确保SCL接SCLSDA接SDA电源和地没有接反或接错位置。检查电源确认传感器供电正常VIN有电压。可以用万用表量一下。检查上拉电阻I2C总线需要上拉电阻通常4.7kΩ到10kΩ将SDA和SCL线拉到高电平。幸运的是绝大多数Adafruit的CircuitPython开发板如ItsyBitsy, Metro已经在板载的I2C引脚上内置了上拉电阻。但如果你是自己用像RP2040 Pi Pico这样的裸芯片搭建电路或者使用了非标I2C引脚就必须在SDA和SCL线上分别外接上拉电阻到3.3V。尝试手动解锁有时I2C总线会卡在锁定状态。你可以在REPL串行控制台中手动输入以下命令解锁import board board.I2C().unlock()降低时钟频率在极少数情况下传感器可能不支持默认的100kHz高速模式。你可以在初始化I2C时指定一个更低的频率例如i2c busio.I2C(board.SCL, board.SDA, frequency10000)10kHz。3.2 理解I2C初始化的“单例模式”代码中i2c board.I2C()这一行值得深入说一下。在CircuitPython中board.I2C()是一个“单例”Singleton设计模式的实现。这意味着无论你在代码的哪个地方、调用多少次board.I2C()它返回的都是同一个I2C总线对象。这避免了资源冲突确保整个程序里只有一个实体在控制那组物理引脚。这是一种非常简洁和安全的设计你不需要担心重复初始化导致的问题。4. 读取TSL2591传感器数据4.1 安装依赖库与项目代码确认传感器在线后我们就可以开始读取光照数据了。CircuitPython的强大之处在于其丰富的“驱动库”生态系统。对于TSL2591Adafruit提供了专门的adafruit_tsl2591库它封装了所有复杂的寄存器操作让我们用几行简单的代码就能获取专业级的光照数据。最方便的方法是下载“项目包”Project Bundle。这通常是一个zip文件里面包含了所有必要的库文件放在lib文件夹和写好的示例代码code.py。你只需要解压并将整个内容复制到你的CIRCUITPYU盘根目录即可。对于手动安装你需要将adafruit_tsl2591.mpy和它的依赖库adafruit_bus_device复制到CIRCUITPY驱动器的lib文件夹中。4.2 数据读取代码解析以下是读取TSL2591数据的核心代码我添加了详细的注释# SPDX-FileCopyrightText: 2017 Limor Fried for Adafruit Industries # SPDX-License-Identifier: MIT CircuitPython Essentials I2C sensor example using TSL2591 import time import board import adafruit_tsl2591 # 导入TSL2591专用库 # 初始化I2C总线 i2c board.I2C() # 可选先扫描一次确认传感器存在调试用 while not i2c.try_lock(): pass print(I2C addresses found:, [hex(addr) for addr in i2c.scan()]) i2c.unlock() # 扫描完记得解锁因为后续库会重新锁定 # 创建传感器对象。这是最关键的一步库会通过这个对象与传感器通信。 # 将我们初始化的i2c总线对象传递给它。 tsl adafruit_tsl2591.TSL2591(i2c) # 现在你可以通过这个tsl对象访问传感器的各种属性和方法了。 print(Sensor initialized. Starting to read data...) while True: # 直接读取照度值单位Lux。库已经帮我们完成了所有光电转换和计算。 lux_value tsl.lux print(fLux: {lux_value:.2f}) # 格式化输出保留两位小数 # 除了lux你还可以读取原始的红外光和全光谱光值用于更高级的应用 # ir tsl.infrared # full_spectrum tsl.visible # print(fIR: {ir}, Full: {full_spectrum}) time.sleep(1.0) # 每秒读取一次将代码上传到板子并打开串口监视器你应该能看到不断输出的Lux值。用手盖住传感器数值会骤降用手电筒照射它数值会飙升。这直观地证明了你的I2C通信和数据读取链路完全畅通。注意事项TSL2591是一个高灵敏度传感器它的增益和积分时间是可以配置的这直接影响测量范围和精度。默认设置适合大多数室内环境。如果你在非常暗或非常亮的环境下使用可能需要调整。你可以通过tsl.gain adafruit_tsl2591.GAIN_LOW或tsl.integration_time adafruit_tsl2591.INTEGRATIONTIME_100MS等属性进行调整。具体可查阅adafruit_tsl2591库的文档。不当的设置可能导致读数溢出显示为None或精度不足。5. CircuitPython HID设备模拟实战5.1 键盘模拟从物理按键到键盘输入让开发板模拟成键盘意味着你可以通过连接一个简单的按钮或触摸传感器来触发键盘上的任意按键甚至组合键实现物理快捷键、宏命令等功能。这在自动化测试、辅助输入设备制作上非常有用。核心库与对象实现HID键盘功能需要三个库usb_hid、adafruit_hid.keyboard和adafruit_hid.keyboard_layout_us或其他语言布局。Keyboard对象用于发送单个键码如Keycode.A而KeyboardLayoutUS对象用于处理字符串输入如直接打出“Hello World”它会自动将字符转换为一系列键击。代码实战与解析下面这个例子使用两个引脚A1和A2连接按钮或直接接地触发分别模拟按下“A”键和输入字符串“Hello World!”。import time import board import digitalio import usb_hid from adafruit_hid.keyboard import Keyboard from adafruit_hid.keyboard_layout_us import KeyboardLayoutUS from adafruit_hid.keycode import Keycode # --- 配置部分 --- # 1. 定义用于触发按键的引脚 keypress_pins [board.A1, board.A2] # 2. 定义每个引脚触发时对应的动作Keycode对象或字符串 keys_pressed [Keycode.A, Hello World!\n] # \n 代表回车键 # 3. 定义需要同时按下的修饰键如Shift, Ctrl这里用Shift实现大写A control_key Keycode.SHIFT # --- 初始化 --- time.sleep(1) # 重要给主机一点时间识别HID设备避免竞争条件 keyboard Keyboard(usb_hid.devices) keyboard_layout KeyboardLayoutUS(keyboard) # 使用美式键盘布局 # 初始化引脚为输入模式并启用内部上拉电阻默认高电平按下按钮时接地变为低电平 key_pin_array [] for pin in keypress_pins: key_pin digitalio.DigitalInOut(pin) key_pin.direction digitalio.Direction.INPUT key_pin.pull digitalio.Pull.UP # 启用内部上拉 key_pin_array.append(key_pin) # 初始化板载LED作为状态指示 led digitalio.DigitalInOut(board.LED) led.direction digitalio.Direction.OUTPUT print(Waiting for key pin...) # --- 主循环 --- while True: for key_pin in key_pin_array: if not key_pin.value: # 检测引脚是否被拉低按钮按下 key_index key_pin_array.index(key_pin) print(fPin #{key_index} is grounded.) led.value True # 点亮LED表示正在处理 # 等待按钮释放引脚恢复高电平实现按下-释放一次触发 while not key_pin.value: pass # 执行按键动作 key_action keys_pressed[key_index] if isinstance(key_action, str): # 如果是字符串使用键盘布局对象“敲出”整个字符串 keyboard_layout.write(key_action) else: # 如果是Keycode如Keycode.A则配合修饰键按下 keyboard.press(control_key, key_action) # 同时按下Shift和A keyboard.release_all() # 释放所有按键这一步至关重要 led.value False # 熄灭LED time.sleep(0.01) # 短暂延迟降低CPU占用关键点解析与避坑指南time.sleep(1)在创建Keyboard对象前等待一秒。这是为了确保计算机的USB主机栈有足够时间识别并枚举你的开发板作为一个HID设备。省略这一步可能导致前几次按键无效。上拉电阻 (pull digitalio.Pull.UP)将引脚配置为内部上拉这样当按钮未按下时引脚通过电阻连接到3.3V读取为高电平True或1。当按钮按下引脚直接接地读取为低电平False或0。这是一种非常常见的按钮检测电路无需外部电阻。keyboard.release_all()这是整个键盘模拟中最容易忘记但后果最严重的一步。keyboard.press()相当于用手指按下了键但如果你不“抬起手指”即release电脑会认为这个键一直被按着导致“按键粘滞”。务必在每次press操作后尽快调用release_all()。对于组合键如CtrlC也是先press(Keycode.CONTROL, Keycode.C)再release_all()。非美式键盘布局如果你需要模拟其他语言的键盘如德语、法语可以从adafruit_hid库中导入相应的布局类例如KeyboardLayoutDE。5.2 鼠标模拟用摇杆控制光标将开发板模拟成鼠标结合一个模拟摇杆Joystick你可以制作自定义的游戏控制器、演示笔或者无障碍辅助设备。硬件连接需要一个双轴模拟摇杆通常带有按钮。连接方式如下摇杆VCC- 开发板3.3V摇杆GND- 开发板GND摇杆X轴输出- 开发板A0(模拟输入)摇杆Y轴输出- 开发板A1(模拟输入)摇杆选择按钮SW- 开发板A2(数字输入内部上拉)代码实战与解析鼠标模拟的核心是将摇杆的模拟电压值0-3.3V映射为光标的移动速度和方向。import time import analogio import board import digitalio import usb_hid from adafruit_hid.mouse import Mouse mouse Mouse(usb_hid.devices) # 初始化摇杆引脚 x_axis analogio.AnalogIn(board.A0) y_axis analogio.AnalogIn(board.A1) select_button digitalio.DigitalInOut(board.A2) select_button.direction digitalio.Direction.INPUT select_button.pull digitalio.Pull.UP # 按钮未按下时为高电平 # 摇杆电压范围校准可能需要根据你的具体硬件微调 # 摇杆居中时电压通常在VCC/2左右即约1.65V pot_min 0.00 # 理论上最小值 pot_max 3.29 # 理论上最大值接近3.3V step (pot_max - pot_min) / 20.0 # 将整个电压范围划分为20个“步进” def get_voltage(pin): 将ADC读取的原始值0-65535转换为电压值0-3.3V return (pin.value * 3.3) / 65536 def steps(axis_voltage): 将电压值映射到0-20的整数步进中心点约为10 return round((axis_voltage - pot_min) / step) while True: # 读取当前电压 x_voltage get_voltage(x_axis) y_voltage get_voltage(y_axis) # 处理按钮点击内部上拉按下时为低电平False if not select_button.value: mouse.click(Mouse.LEFT_BUTTON) time.sleep(0.2) # 简单防抖防止误触发多次点击 # 处理X轴移动左右 x_step steps(x_voltage) if x_step 11: # 轻微向右偏 mouse.move(x1) # 向右移动1像素 elif x_step 9: # 轻微向左偏 mouse.move(x-1) # 向左移动1像素 elif x_step 19: # 大幅度向右偏 mouse.move(x8) # 快速向右移动 elif x_step 1: # 大幅度向左偏 mouse.move(x-8) # 快速向左移动 # 处理Y轴移动上下注意屏幕坐标系Y轴向下为正 y_step steps(y_voltage) if y_step 11: # 轻微向下偏摇杆通常向上推电压降低 mouse.move(y1) # 向下移动1像素 elif y_step 9: # 轻微向上偏 mouse.move(y-1) # 向上移动1像素 elif y_step 19: # 大幅度向下偏 mouse.move(y8) # 快速向下移动 elif y_step 1: # 大幅度向上偏 mouse.move(y-8) # 快速向上移动 time.sleep(0.01) # 主循环延迟代码逻辑与调优电压映射get_voltage函数将ADC的16位原始值0-65535转换为实际的电压值。steps函数则将连续的电压值离散化为0-20的整数中心位置摇杆松开大约是10。这样设计是为了简化后续的逻辑判断。移动逻辑代码设置了两个移动阈值。当摇杆轻微偏离中心步进值9或11时鼠标以慢速1像素/次移动当摇杆推到极限步进值0或19时鼠标快速移动8像素/次。这种“慢-快”两档速度提升了操控体验。方向注意在屏幕坐标系中Y轴正方向是向下的。所以代码中y_step值大电压高对应mouse.move(y1)向下移动这符合直觉摇杆向前推通常电压降低让光标向上走向后拉让光标向下走。如果你觉得方向反了可以交换y1和y-1的条件。校准pot_min和pot_max是理论值。实际中摇杆可能无法达到完整的0V和3.3V。你可以通过串口打印x_voltage和y_voltage的实时值观察摇杆在四个极限位置时的读数并用这些实际值替换理论值让映射更精确。6. 高级话题与深度优化6.1 探索更多I2C引脚的可能性大多数CircuitPython兼容的板子如SAMD21, SAMD51, nRF52840的I2C引脚并不固定于标有“SDA”/“SCL”的那一对。你可以使用其他引脚通过“软件I2C”bitbang功能来实现。Adafruit提供了一个非常实用的脚本来扫描板子上所有可能的引脚组合找出哪些可以用于硬件I2C。运行这个脚本你会得到一个长长的列表里面是所有可用的SCL和SDA引脚对。这对于当你需要多个I2C总线或者默认引脚被其他功能占用时特别有用。6.2 调整I2C时钟速度默认的I2C时钟速度通常是100kHz对绝大多数传感器如TSL2591来说绰绰有余。但在某些特定场景下你可能需要调整它超长导线如果传感器离主控板很远导线电容会导致信号边沿变缓降低通信速度。此时可以调低频率如10kHz以提高稳定性。高速设备某些设备支持快速模式400kHz甚至高速模式1MHz以上。在确认设备和布线都支持的情况下提高时钟速度可以加快数据读取。在CircuitPython中如果你使用busio.I2C()手动创建总线对象可以在初始化时指定frequency参数例如i2c busio.I2C(scl_pin, sda_pin, frequency400000)。但请注意使用board.I2C()这个单例时无法直接指定频率它使用板子预设的默认频率。6.3 系统监控与数据持久化你的CircuitPython板子还能做一些很酷的“内省”和记录工作。读取CPU温度几乎所有现代微控制器内部都有一个温度传感器。在REPL中两行代码就能读到它import microcontroller print(microcontroller.cpu.temperature) # 单位是摄氏度这个温度反映的是芯片内核的温度通常比环境温度高一些但在没有专用温度传感器时可以用来估算环境温度或监控芯片是否过热。使用存储模块进行数据记录你想让板子脱离电脑独立运行并把传感器数据比如刚才读到的温度和光照记录下来吗这就需要用到storage模块。核心思路是在boot.py文件中通过一个物理开关或跳线的状态来决定CIRCUITPY驱动器是对电脑可写方便你改代码还是对CircuitPython程序可写方便程序记录数据到文件。一个典型的boot.py如下import board import digitalio import storage # 使用一个数字引脚如D2连接一个开关或跳线到GND write_pin digitalio.DigitalInOut(board.D2) write_pin.direction digitalio.Direction.INPUT write_pin.pull digitalio.Pull.UP # 内部上拉默认高电平 # 如果引脚连接到GND值为False则允许CircuitPython写入文件系统 # 如果引脚悬空或为高电平值为True则文件系统对电脑可写对CircuitPython只读 storage.remount(/, readonlywrite_pin.value)设置好后当引脚接地时你可以在code.py中用普通的Python文件操作如open(data.txt, a).write(...)来记录数据。当你需要把数据拷贝出来时只需让引脚断开接地恢复高电平重新插拔板子CIRCUITPY就会重新以对电脑可写的模式挂载你就能像操作U盘一样复制data.txt文件了。重要警告切勿在电脑正在访问CIRCUITPY驱动器比如你在用编辑器打开上面的文件时让CircuitPython尝试写入它。这极有可能导致文件系统损坏丢失所有数据。boot.py机制正是为了防止这种同时访问的情况发生。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2613753.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!