拓冰建站拓冰建站
首页 / 资讯中心 / 正文

树莓派 Pico PIO 模拟 SPI 全解析:原理、时序与代码实战

PIO 实现 SPI 通信这里我们需要使用KingST逻辑分析仪连接**风雅一号板-通用兼容扩展板**上的长排针树莓派 Pico 使用引脚情况如下表所示实物整体连接效果如下1.使用 PIO 实现 SPI 通信协议这里我们以CPOL时钟极性和CPHA时钟相位均为 0 的 SPI 通信为例讲解如何使用PIO实现SPI通信协议首先我们先回忆一下SPI通信的基本要点当CPOL 0和CPHA 0时时钟信号在没有数据传输时保持低电平而数据在时钟信号的上升沿被采样即数据被发送到接收设备时时钟信号从低电平跳变到高电平通信流程可以简化为当不传输数据时SCLK时钟线保持低电平。在每个时钟周期数据在MOSI主输出从输入或MISO主输入从输出线上传输数据信号在下降沿变化上升沿读取。数据的传输是根据时钟同步的每传输一个数据位时钟就变化一次。这里我们参考官方例程对其进行修改代码如下# Python env : MicroPython v1.23.0 # -*- coding: utf-8 -*- # Time : 2024/11/6 下午4:26 # Author : 李清水 # File : pio_spi.py # Description : PIO类实验在PIO程序中实现SPI协议通信 # 参考代码https://github.com/raspberrypi/pico-micropython-examples/blob/master/pio/pio_spi.py # 导入相关模块 # 导入硬件相关模块 from machine import Pin # 导入RP2040相关的模块 from rp2 import PIO, StateMachine, asm_pio # 全局变量 # 功能函数 # 使用asm_pio装饰器定义一个 PIO 程序 # OSR移位寄存器的方向为左移使能自动推出和自动加载移位计数阈值均为8 # 用于侧集操作的两个引脚初始化为低电平和高电平用于输出的引脚初始化为低电平 asm_pio(out_shiftdir0, autopullTrue, pull_thresh8, autopushTrue, push_thresh8, sideset_init(PIO.OUT_LOW, PIO.OUT_HIGH), out_initPIO.OUT_LOW) def spi_cpha0() - None: ​ PIO 实现的 SPI 协议CPHA0\CPOL0 ​ 该 PIO 程序实现了 SPI 协议的 CPHA0\CPOL0 模式支持 8 位数据传输。 ​ 通过侧集引脚控制 SCK 和 MOSI同时读取 MISO 数据。 ​ Args: ​ None ​ Returns: ​ None ​ ​ ​# 设置寄存器 x 为 6这里 x 寄存器用来控制每个字节的位数倒计数 # 设置为 6 表示需要发送 7 次时钟循环x的值从6递减到0 set(x, 6) # 用于定义循环开始位置 wrap_target() # 当OSR输出移位计数器达到其阈值时即8个数据位则从TX FIFO队列中提取一个字节数据到输出移位寄存器 # 如果TX FIFO为空则等待数据填充 # 设置侧边位side-set的时钟线 SCK 为高电平[1] 表示等待一个周期 pull(ifempty) .side(0x2) [1] # 定义标签 bitloop用于表示一个字节数据的位循环发送和接收每一位 label(bitloop) # 从输出移位寄存器 OUT 中取出 1 位并通过 pins 输出即数据输出到 MOSI 引脚 # 设置 SCK 低电平表明数据在时钟的下降沿输出 out(pins, 1) .side(0x0) [1] # 从 MISO 引脚读取1位数据并存入输入ISR移位寄存器 # 将 SCK 设置为高电平在时钟的上升沿采样数据 in_(pins, 1) .side(0x1) # 判断x是否减至零如果 x 未减到零跳转到 bitloop 标签继续传输下一个位 # 之后对 x 寄存器进行递减操作同时保持时钟为高电平 jmp(x_dec, bitloop) .side(0x1) # 再次输出 1 位数据到 MOSI 引脚将 SCK 设置为低电平确保数据发送完成 # 总共进行8次数据收发即一个字节数据传输完成 out(pins, 1) .side(0x0) # 重置 x 寄存器为 6为下一字节传输做准备 set(x, 6) .side(0x0) # 从 MISO 引脚读取1位数据并存入输入ISR移位寄存器 # 将 SCK 设置为高电平在时钟的上升沿采样数据 in_(pins, 1) .side(0x1) # 如果 OSR 不为空尚未达到其阈值则跳转到 bitloop继续传输下一个字节 jmp(not_osre, bitloop) .side(0x1) # 无操作同时设置 SCK 为低电平形成CS结束的延迟表示一字节数据传输结束 nop() .side(0x0) [1] # 用于定义循环结束位置 wrap() # 自定义类 # 使用RP2040的PIO可编程I/O系统实现的自定义SPI驱动类 class PIOSPI: ​ PIOSPI 类用于通过 PIO 实现 SPI 通信。 ​ 该类封装了基于 RP2040 PIO 的 SPI 通信功能支持阻塞式读写操作。 ​ 通过 PIO 状态机实现 SPI 协议适用于需要高效、低延迟的 SPI 通信场景。 ​ Attributes: ​ _sm (StateMachine): PIO 状态机实例用于实现 SPI 协议。 ​ _cs (Pin): CS 引脚实例用于控制 SPI 设备的片选信号。 ​ Methods: ​ __init__(self, sm_id, pin_mosi, pin_sck, pin_misoNone, pin_csNone, cphaFalse, cpolFalse, freq1000000): ​ 初始化 PIOSPI 类实例。 ​ write(self, wdata: list[int]) - None: ​ 阻塞式写入数据到 SPI 设备。 ​ read(self, n: int) - list[int]: ​ 阻塞式从 SPI 设备读取数据。 ​ write_read(self, wdata: list[int]) - list[int]: ​ 阻塞式写入并读取 SPI 设备数据。 ​ ​ ​def __init__(self, sm_id: int, pin_mosi: int, pin_sck: int, pin_miso: int None, pin_cs: int None, cpha: bool False, cpol: bool False, freq: int 1000000): ​ 初始化 PIOSPI 类实例。 ​ Args: ​ sm_id (int): 状态机编号。 ​ pin_mosi (int): MOSI 引脚编号。 ​ pin_sck (int): SCK 引脚编号。 ​ pin_miso (int): MISO 引脚编号可选。 ​ pin_cs (int): CS 引脚编号可选。 ​ cpha (bool): 时钟相位默认为 False。 ​ cpol (bool): 时钟极性默认为 False。 ​ freq (int): 时钟频率默认为 1000000 Hz。 ​ Raises: ​ AssertionError: 如果 cpol 或 cpha 不为 False。 ​ ​ ​# 确保仅使用 CPHA0 和 CPOL0不支持反相时钟信号或其他相位 assert(not(cpol or cpha)) # 判断pin_mosi编号和pin_sck编号是否相邻并且pin_mosi编号小于pin_sck编号 if not (pin_mosi pin_sck - 1 or pin_mosi pin_sck 1): raise AssertionError(pin_mosi must be adjacent to pin_sck) # 创建了一个状态机对象 _sm,并将其激活状态机程序为 spi_cpha0,频率为 4*freq self._sm StateMachine(sm_id, spi_cpha0, freq4*freq, sideset_basePin(pin_sck), out_basePin(pin_mosi), in_basePin(pin_miso)) # 初始化 CS 引脚如果提供 self._cs Pin(pin_cs, Pin.OUT) if pin_cs is not None else None if self._cs: # 初始状态为高电平未选中 self._cs.value(1) # 激活状态机 self._sm.active(1) def write(self, wdata: list[int]) - None: ​ 阻塞式写入数据到 SPI 设备。 ​ Args: ​ wdata (list[int]): 要写入的数据列表每个元素为 8 位数据。 ​ Returns: ​ None ​ ​ ​# 拉低 CS 引脚 if self._cs: self._cs.value(0) # 将每个字节左移 24 位后放入状态机的输出 FIFO for b in wdata: self._sm.put(b 24) # 拉高 CS 引脚 if self._cs: self._cs.value(1) def read(self, n: int) - list[int]: ​ 阻塞式从 SPI 设备读取数据。 ​ Args: ​ n (int): 需要读取的数据字节数。 ​ Returns: ​ list[int]: 读取的数据列表每个元素为 8 位数据。 ​ ​ ​# 拉低 CS 引脚 if self._cs: self._cs.value(0) data [] # 清空RX FIFO self._sm.restart() for i in range(n): # 取数据的前16位 data.append(self._sm.get() 0xff) # 拉高 CS 引脚 if self._cs: self._cs.value(1) return data def write_read(self, wdata: list[int]) - list[int]: ​ 阻塞式写入并读取 SPI 设备数据。 ​ Args: ​ wdata (list[int]): 要写入的数据列表每个元素为 8 位数据。 ​ Returns: ​ list[int]: 读取的数据列表每个元素为 8 位数据。 ​ ​ ​# 拉低 CS 引脚 if self._cs: self._cs.value(0) rdata [] # 清空RX FIFO self._sm.restart() for b in wdata: # 将每个字节左移 24 位后放入状态机的输出 FIFO self._sm.put(b 24) # 取数据的前16位 rdata.append(self._sm.get() 0xff) # 拉高 CS 引脚 if self._cs: self._cs.value(1) return rdata # 初始化配置 # 主程序 该代码中首先使用装饰器asm_pio来定义程序的基本配置包括设定OSR移位方向为左移使能自动推出和自动加载其移位计数阈值为 8表示每个数据字节包含 8 个数据位PIO会等待 8 个时钟周期后才移位一次初始化用于SPI时钟和其他操作的引脚同时MOSI和SCK引脚都初始化为低电平。接着在PIO汇编程序中我们首先使用set(x, 6)设置寄存器x的初始值为 6这表示每次需要进行 7 次时钟循环每次发送 1 位数据这里的x寄存器控制每次循环的次数。然后我们定义SPI协议数据传输过程首先通过wrap_target()和wrap()包裹了整个数据传输过程表示每次传输一个字节后可以继续传输下一个字节​拉取数据​pull(ifempty)表示从TX FIFO队列中拉取数据如果 FIFO 为空则会等待直到数据填充并且通过side(0x2)使 SCK 信号被设置为高电平表示开始发送数据​发送和接收数据的位循环​数据的传输使用bitloop标签来实现循环发送和接收每一位​输出数据位​使用out(pins, 1)从输出移位寄存器OSR输出 1 位数据到MOSI引脚并通过side(0x0)将时钟线SCK设置为低电平表示在时钟的下降沿进行数据发送​读取数据位​使用in_(pins, 1)从 MISO 引脚读取 1 位数据到输入移位寄存器ISR并通过side(0x1)将SCK设置为高电平表示在时钟的上升沿采样数据​跳转判断​通过jmp(x_dec, bitloop)指令将x寄存器递减 1 并继续循环直到所有数据位都传输完成​完成一个字节传输​当传输一个字节的 8 个位时设置x为 6 并开始新的字节传输通过out(pins, 1)输出最后一位数据并设置SCK为低电平表示完成数据的发送​检查是否继续​jmp(not_osre, bitloop)表示如果输出移位寄存器OSR未达到其阈值 8且FIFO队列仍有数据则继续循环进行数据传输​无操作等待延迟​nop().side(0x0)用于在传输完成后等待一定时间形成CS信号的延迟标志着一个字节的传输结束整体时序图如下所示简单来说我们主要通过如下步骤通过 PIO 实现 SPI 通信在每个时钟周期数据的各个位通过移位寄存器从输出移位寄存器OSR移到MOSI引脚在时钟的下降沿被发送而数据在时钟的上升沿被采样。通过x寄存器控制每个字节的数据位传输次数每次传输完一个字节后程序会准备好下一字节的数据。使用side()指令控制 SPI 时钟SCK引脚的电平变化保证数据发送和采样的时序符合 CPHA0, CPOL0 的要求。接着我们在自定义的PIOSPI类中封装了使用 PIO 状态机实现 SPI 通信的功能并提供了如下方法​初始化​init()​​方法​初始化 PIO 状态机指定sm_id状态机编号、pin_mosi数据输出引脚、pin_miso数据输入引脚、pin_sck时钟引脚等参数并设置频率为4*freq来适应 PIO 指令的时钟周期每个数据位级操作占用一个时钟周期接着激活状态机准备传输数据。​写入​write()​​方法​使用self._sm.put()方法将每个字节左移 24 位对齐到最高位后传入 TX FIFO该方法为阻塞式方法需要等待所有数据发送完成。​读取​read()​​方法​使用self._sm.get()逐字节读取 RX FIFO 中的数据通过 0xff截取最低 8 位确保数据在 1 字节范围内同样为阻塞式方法。​读写​write_read()​​方法​同时发送和接收数据使用put发送每个字节同时调用get接收对应的响应数据。这里需要注意的是由于 PIO 汇编程序中涉及到侧集操作相关设置要求​**pin_mosi​编号和​pin_sck​编号相邻并且​pin_mosi​编号小于​pin_sck​编号。**2.SPI 数据收发回环测试以下代码可以在我们提供的资料包中elegance-devkit v1\Demo\36 PIO_SPI文件夹找到。在主程序中我们通过自定义的PIOSPI类向外部设备发送数据主程序每秒发送一组字节数据并且设置MOSI引脚和MISO引脚相同从而进行数据收发回环测试示例代码如下所示# Python env : MicroPython v1.23.0 # -*- coding: utf-8 -*- # Time : 2024/11/6 下午6:41 # Author : 李清水 # File : main.py # Description : 使用PIO模拟的SPI协议进行数据收发回环测试 # 导入相关模块 # 导入时间相关模块 import time # 导入PIO模拟SPI协议相关模块 from pio_spi import PIOSPI # 全局变量 # 待发送的字节列表 tx_list [0, 1, 2, 3, 4, 5, 6, 7] # 功能函数 # 自定义类 # 初始化配置 # 延时等待设备初始化 time.sleep(3) # 打印调试信息 print(FreakStudio : Using PIO to implement the SPI protocol) # 初始化SPI类设置波特率、极性、相位、时钟引脚、数据引脚 # 设置pin_mosi和pin_miso为GP10pin_sck为GP11pin_cs为GP12CPHA为FalseCPOL为False波特率为1000000 # 设置MISO和MOSI引脚相同进行收发回环测试 spi PIOSPI(sm_id0, pin_mosi10, pin_sck11, pin_miso10, pin_cs 12,cphaFalse, cpolFalse, freq1000000) # 主程序 # 使用PIO模拟SPI协议收发数据 while True: # 发送并读取数据 data spi.write_read(tx_list) # 打印调试信息 print(FreakStudio : SPI data received : {}.format(data)) # 等待1秒 time.sleep(1)烧录代码前我们需要在KingST逻辑分析软件中添加一个SPI协议解析器进行如下设置接着烧录代码打开终端并打开逻辑分析仪上位机点击启动单次采样输出如下所示可以看到我们成功使用 PIO 模拟 SPI 协议发送了数据时钟频率和程序预设的通信频率一致CS片选端时序与设置的基本相同拉低发送开始、拉高时表示发送完成数据收发周期为 1s 左右打开终端使用 mpremote 工具连接树莓派 Pico可以看到自定义的 PIOSPI 类可以完成数据接收
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门