2024年寒假练 - 基于带显示屏的RP2040调试平台设计可编程PWM发生器
该项目使用了Thonny软件、MicroPython语言,实现了带显示屏的、基于RP2040的多功能嵌入式编程学习、硬件调试平台的可编程PWM发生器的设计,它的主要功能为:使用PIO编程,生成3路最高重复频率为12MHz的PWM信号,且每一路的频率和占空比都可独立调节。
标签
嵌入式系统
MicroPython
RP2040
2024寒假在家一起练
PWM发生器
pei
更新2024-03-29
福州大学
370

项目需求

基本要求:

  • 使用RP2040的PIO编程,生成3路最高重复频率为12MHz的PWM信号,每一路的重复频率和占空比都可独立调节
  • 重复频率越高,占空比分辨率降低(主时钟120MHz为例)

重复频率

占空比调节精度(分辨率)

12MHz

1/10

1.2MHz

1/100

120KHz

1/1000

12KHz

1/10,000

1.2KHz

1/100,000

120Hz

1/1000,000

占空比的调节精度最高设定为1/1000

  • 使用按键和拨轮组合调节输出输出频率、占空比,并由按键控制每一路PWM信号的输出
  • 能在LCD上显示基础信息如当前使用引脚示意、引脚相应的PWM参数

需求分析

  1. 考虑性能要求最高的情况,即PWM波的最高重复频率为12MHz且分辨率达到1/10,也就是输出PWM中每个“点”的频率不低于120MHz,假如一周期输出一点,最高频率为125MHz的PIO状态机可以满足需求(即不需要超频),而在取120MHz时,表格中要求的其他频率对应的分辨率也能正好满足。
  2. 由于状态机和寄存器裕度充足,3路PWM中每一路的重复频率和占空比可独立调节的要求可轻松通过代码的重复完成。
  3. 项目要求占空比的调节精度最高设定为1/1000,但为了精度调整逻辑的完整性,本项目仍为使用者提供了低频输出时的极限分辨率。
  4. 在按键和拨轮组合操作的实现上,使用平面化的设计,所有可修改项都可在UI的初始层级被选中,目的是用简单的操作高效地完成交互。
  5. 在LCD上显示的引脚示意图则通过形象的字符组成,并利用彩色显示增强可读性。

实现方式及代码说明

一、LCD显示

屏幕显示驱动部分使用板卡自带的st7789.py,字库同样基于自带的vga1_16x32.py和vga2_8x8.py,标题部分使用前者,其余UI使用后者。由于本次的PWM发生器人机交互需求较为简单,且得益于RP2040不错的性能,UI实现可以仅依靠判断的嵌套,并且实际体验流畅跟手。以下对各部分进行说明:

  1. 彩色显示:屏幕采用565编码方式,可通过st7789驱动程序中的“color565”函数将红色,绿色和蓝色值(0~255)转换为16位565编码:
st7789.color565(225,0,255)   # 紫色

在屏幕显示中加入彩色的目的是用颜色标记对应端口,使操作更加便利。

  1. 显示逻辑:在本项目中,屏幕的作用是显示选中的内容和体现其数值的更新(频率档位切换和占空比调节),选择高亮形式(字体颜色与底色取反)体现选中的参数时,每次执行的屏幕更新实际上都可分为两类:一是更新当前高亮参数的数值;二是取消当前高亮参数,并将下一个选中的参数高亮。前者仅需将对应位置新值传给屏幕即可,后者则需要先将原高亮位置复原,再高亮一处新的地方。由于可选位置数量多,为每一个位置更新编程不现实,因此可以选择定义一个屏幕恢复函数“recover”,作用是在每一次更新前将所有参数重置为无高亮状态,接下来即可只针对新的高亮处编程,而无需考虑上一个时刻的高亮状态。
recover() # 将所有参数恢复至未高亮状态
if(x_pos): # 高亮占空比
display.text(font1, str(int(D_gear[y_pos]/(10 ** (6-x_pos))%10)), x[x_pos], y[y_pos], BLACK, WHITE)
else: # 高亮频率
display.text(font1, freq_disp[freq_CH[y_pos]], x[x_pos], y[y_pos], BLACK, WHITE)

二、按键交互

本项目按键交互全部采用在主循环中循环判断的方式实现。当选中了一个可以更改的状态(频率或占空比),对应位置会被高亮,此时按下左上方的按钮,选中的频率会切换到下一档,而占空比对应位数会加一;左右拨动右上方的按钮可在同一行左右移动,而居中按下可切换到下一个PWM通道。同时,选中不同通道时,被选中的通道会在示意图中以叉表现出来。在合适的延时下,按键操作顺畅且代码结构清晰,方便阅读(往右拨动按钮的代码如下,其他按钮对应代码的结构相同)。

if(btn_R.value() == 0):
utime.sleep(debounce)
if(btn_R.value() == 0):
……​

三、PIO实现

由PIO生成PWM波的过程主要由两个部分组成:

  1. PIO汇编程序和状态机的初始化

首先,选择侧置引脚作为PWM的输出,将PWM波中正脉冲所占的时间存入x寄存器,将PWM波中负脉冲所占的时间存入y寄存器,再分别对x和y自减,就可以完成PWM波的生成,但这里有一个问题,就是当在120MHz频率下选择输出12MHz的PWM波时,装载x和y寄存器的指令会强制占用一部分负脉冲时间,导致无法输出70以上的占空比。

# 12MHz下,占空比为10%~70%可用(不可避免地有三个负脉冲)
@asm_pio(sideset_init=PIO.OUT_LOW)
def pwm_1_7():
mov(x, osr) # 正装载
mov(y, isr) # 负装载
label("P")
jmp(x_dec, "P") .side(1) # 自减同时侧置1
label("N")
jmp(y_dec, "N") .side(0) # 自减同时侧置0

为了解决这个问题,我们可以再写一段对称的指令,在这段指令中,装载寄存器的时间是强制的正脉冲时间,也就是说它不能输出30以下的占空比。此时,利用这两段代码的互补,就可以输出全占空比的PWM波了。

# 12MHz下,占空比为30%~90%可用(不可避免地有三个正脉冲)
@asm_pio(sideset_init=PIO.OUT_LOW)
def pwm_3_9():
mov(x, osr) .side(1) # 正装载同时侧置1
mov(y, isr) # 负装载
label("P")
jmp(x_dec, "P") # 自减
label("N")
jmp(y_dec, "N") .side(0) # 自减同时侧置0

状态机频率为120MHz,当输出12MHz时,每一周期对引脚进行十次输出,占空比精度为1/10;而输出1.2MHz时,即可进行每一周期一百次输出,占空比精度为1/100……以此类推,当频率越低,可输出的占空比精度就越高,与项目需求相对应。

# 配置状态机
sm0 = StateMachine(0, pwm_1_7, 120_000_000, sideset_base=Pin(20))
sm09 = StateMachine(1, pwm_3_9, 120_000_000, sideset_base=Pin(20))
sm1 = StateMachine(2, pwm_1_7, 120_000_000, sideset_base=Pin(21))
sm19 = StateMachine(3, pwm_3_9, 120_000_000, sideset_base=Pin(21))
sm2 = StateMachine(4, pwm_1_7, 120_000_000, sideset_base=Pin(22))
sm29 = StateMachine(5, pwm_3_9, 120_000_000, sideset_base=Pin(22))
  1. pwm_set函数:对频率以及占空比进行更改

在编写好“mov(x, osr)”和“mov(y, isr)”之后,只需要向osr和isr中传入数值,PIO就会自动将它们的值不断拷贝到x和y寄存器中,供PWM生成中的自减过程计数使用,因此,改变osr和isr中的值,对应的PWM波频率和占空比就能得到修改。如前述PWM波生成代码占空比“死区”的需要,当占空比越限,使用的状态机在smX和smX9之间切换(X为0、1或2),在实际使用中就可以覆盖所有的可调节占空比。

## PIO配置
def pwm_set():
# 正、负脉冲点数计算
PV = int(D_gear[y_pos]/10000 * freq_gear[freq_CH[y_pos]])
NV = int(100.0 * freq_gear[freq_CH[y_pos]] - PV)
# CH0
if(y_pos == 0):
if(D_gear[y_pos] > (2 * (10 ** (5 - freq_CH[y_pos])))):
sm0.active(0)
#sm09.active(0)
sm09.put(NV-1)
sm09.exec("pull()")
sm09.exec("mov(isr, osr)")
sm09.put(PV-3)
sm09.exec("pull()")
sm09.active(1)
else:
#sm0.active(0)
sm09.active(0)
sm0.put(NV-3)
sm0.exec("pull()")
sm0.exec("mov(isr, osr)")
sm0.put(PV-1)
sm0.exec("pull()")
sm0.active(1)
# CH1
elif(y_pos == 1):
if(D_gear[y_pos] > (2 * (10 ** (5 - freq_CH[y_pos])))):
sm1.active(0)
#sm19.active(0)
sm19.put(NV-1)
sm19.exec("pull()")
sm19.exec("mov(isr, osr)")
sm19.put(PV-3)
sm19.exec("pull()")
sm19.active(1)
else:
#sm1.active(0)
sm19.active(0)
sm1.put(NV-3)
sm1.exec("pull()")
sm1.exec("mov(isr, osr)")
sm1.put(PV-1)
sm1.exec("pull()")
sm1.active(1)
# CH2
elif(y_pos == 2):
if(D_gear[y_pos] > (2 * (10 ** (5 - freq_CH[y_pos])))):
sm2.active(0)
#sm29.active(0)
sm29.put(NV-1)
sm29.exec("pull()")
sm29.exec("mov(isr, osr)")
sm29.put(PV-3)
sm29.exec("pull()")
sm29.active(1)
else:
#sm2.active(0)
sm29.active(0)
sm2.put(NV-3)
sm2.exec("pull()")
sm2.exec("mov(isr, osr)")
sm2.put(PV-1)
sm2.exec("pull()")
sm2.active(1)

功能框图

如图所示:

效果展示

在本项目中,输出端使用了地和0、1、2,也就是紫色、棕色、白色三路通道。UI的主体是一个更改配置和状态显示一体的表格,左侧的CH0、CH1、CH2代表了三行分别控制的三个PWM输出,上侧的Freq和Duty表示了左右两列分别为频率和占空比,下方的引脚示意图代表了右侧的输出口,示意图的凸起右侧输出口的镂空位置,即GND的位置在右上第一个,CH0、1、2则依次向左排开,同时示意图颜色与导线颜色的对应可以帮助我们更快地辨认输出位置。

image.png

image.png

image.png

image.png

演示视频

见:【2024寒假一起练 - 基于RP2040的PWM发生器】

代码附件

见文末附件处(含所使用Micropython固件版本和代码工程文件)。

参考资料

  1. RaspberryPi官方MicroPython文档
  2. RaspberryPi官方数据手册
  3. 硬禾学堂板卡介绍+题目解析直播
  4. DigiKey:Shawn的RP2040教程
  5. Github上的官方MicroPython例程包


附件下载
RPI_PICO-20240222-v1.22.2.uf2
项目使用的MicroPython固件版本
RP2040_PIO_PWM.zip
工程文件:在Thonny中打开即可运行
团队介绍
福州大学 电气工程及其自动化专业 何祥培
团队成员
pei
福州大学 电气工程及其自动化专业
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号