2024年寒假练 - 基于RP2040微控制器制作可编程PWM发生器
该项目使用了RP2040微控制器、MicroPython语言,实现了可编程PWM发生器的设计,它的主要功能为:基于RP2040微控制器、搭配240*240分辨率的LCD彩屏、两个轻触按键和一个拨轮;可同时生成3路PWM信号,每一路的重复频率和占空比都可独立调节;重复频率最高12MHz,最低120Hz,占空比调节精度最高为1/1000。。
标签
嵌入式系统
PWM
MicroPython
RP2040
参加活动
Glass
更新2024-04-02
华北电力大学
400

1 项目需求

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


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

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

2 需求分析

2.1 使用PIO编程,生成3路PWM信号

硬性要求使用RP2040的PIO编程,对树莓派官方在Github平台上开源的pico-micropython-examples-master中的pio示例进行学习,同时熟悉RP2040的可编程I/O,了解到RP2040有两个相同的PIO实例,每个实例有 4 个状态机,所以总共有 8 个状态机。所以生成3路PWM信号,我们需要将三个状态机绑定到三个不同的串口。

2.2 最高重复频率为12MHz的PWM信号,每一路的重复频率和占空比都可独立调节

最高重复频率要求12MHz,即对于状态机的执行周期有要求,通过对官方开源示例pio_pwm.py的学习,打算用相同的思路实现对占空比进行调节,代码如下。

@asm_pio(sideset_init=PIO.OUT_LOW)
def pwm_prog():
pull(noblock) .side(0)
mov(x, osr) # Keep most recent pull data stashed in X, for recycling by noblock
mov(y, isr) # ISR must be preloaded with PWM count max
label("pwmloop")
jmp(x_not_y, "skip")
nop() .side(1)
label("skip")
jmp(y_dec, "pwmloop")

可以看到,y的值影响pwm的重复频率,x的值影响pwm的占空比,注意到pio_pwm.py中PIOPWM类的初始化以及pio中执行语句组合的特点,实际上,官方给出的例子中,每两个执行指令构成占空比的调节精度,但由于前四条指令为必定执行的指令,所以需要对原有代码进行调整。又由于每两个执行指令构成占空比的调节精度,所以将RP2040超频到240MHz,以此提高状态机的频率,输出最高重复频率为12MHz,占空比分辨率为1/10的PWM信号。

2.3 重复频率可调

根据列表给出的分辨率和重复频率,以及PIO块中x,y为32位寄存器,所以x,y可以存储0~65535的任意数字,用暴力的解法,将12MHz、1.2MHz、120KHz,占空比分辨率分别为1/10、1/100、1/1000的pwm波输出,而由于占空比调节精度最高设定为1/1000,所以输出12KHz、1.2KHz和120Hz的pwm波时,可以适当降低状态机的频率。

2.4 按键、拨轮组合调节,LCD显示

对于后面的两个要求,就是适当组合之前产生的PWM信号的代码,加上一些按键中断和一些标志位,来记录PWM信号的变化,并产生相应输出。再做一个易于理解和操作的UI,便于人机交互;放一些趣味性的元素,使界面更加美观。上沿的两个按键,左边按键切换不同的通道,蓝色字体显示选中的通道;右边按键切换占空比和频率的调节,右侧的紫色‘-’显示选中的参数。拨轮,向左拨减小占空比数字或者切换频率,向右拨增大占空比数字或者切换频率,中间摁下对应通道切换输出和停止输出状态。

d0b100230f846148bc85e8d616d14a2.jpg

3 实现的方式

  • 使用PIO编程,设置不同的状态机频率,让IO输出不同频率和不同占空比的PWM信号;
  • 上沿的两个按键和一个拨轮可以切换通道,使能输出,改变PWM信号频率和占空比;
  • LCD显示输出通道,与带屏RP2040的CH0、CH1、CH2对应,显示PWM参数;
  • 按键拨轮配合LCD,修改和显示PWM参数。

4 功能框图

清新配色流程图标准样式模板.jpg

5 代码及说明

5.1 PWM信号

设置RP2040主频为240_000_000Hz。

machine.freq(240_000_000)

PIO块代码,pwm_prog_0为一般情况下,PIO绑定函数,设x,y寄存器数字大小为x,y,则低电平的执行周期个数为,高电平执行周期个数为,高电平占比为,低电平占比,一般情况下,x影响占空比,y影响精度,且由于jmp(x_not_y, "skip")和jmp(y_dec, "pwmloop")的存在,x必须小于等于y,先考虑1/10的分辨率,要使得分辨率为1/10,则y为7,x最大为7,占空比为8/10,如果只有pwm_prog_0函数,则无法输出占空比为9/10的PWM信号,所以设置了一个备用函数pwm_prog_1,以生成占空比为9/10的PWM信号。

同理,这两个函数适用于1/10,1/100,1/1000三种分辨率下,输出任意占空比的PWM信号。

@asm_pio(sideset_init=PIO.OUT_LOW)
def pwm_prog_0():
wrap_target()
pull(noblock) .side(0)
mov(x, osr) # Keep most recent pull data stashed in X, for recycling by noblock
mov(y, isr) # ISR must be preloaded with PWM count max
label("pwmloop")
jmp(x_not_y, "skip")
nop() .side(1)
label("skip")
jmp(y_dec, "pwmloop")
wrap()

@asm_pio(sideset_init=PIO.OUT_LOW)
def pwm_prog_1():
wrap_target()
pull(noblock) .side(1)
mov(x, osr) # Keep most recent pull data stashed in X, for recycling by noblock
mov(y, isr) # ISR must be preloaded with PWM count max
label("pwmloop")
jmp(x_not_y, "skip")
nop() .side(0)
label("skip")
jmp(y_dec, "pwmloop")
wrap()

PIOPWM类,共三个函数。

第一个函数_init_(),记录使用的状态机id、绑定的引脚、最大计数周期(即精度)、状态机频率(根据所需输出的PWM信号的频率设置),然后通过StateMachine获取状态机。

第二个函数set_pwm(),输入参数为设置占空比,这里为了方便起见,不设置百分比,而是直接输入0~1000中某个数字,至于不同频率的不同精度,pwm的最大值在其他地方加以限制。

第三个函数choose(),输入参数为状态机是否工作,1为状态机工作,0为状态机不工作。第三个函数中,还会对输入的PWM信号的占空比数字进行判断:若为0或最大值,就直接输出高低电平,而不使用PIO编程,增加任务复杂度;若为占空比范围内其他数字,先将和精度有关的(最大值-3)、和占空比有关的(占空比数字-1)两个数字分别储存到osr和isr寄存器,之后使用状态机执行响应函数(执行规则见5.1)。执行第二个函数时必定执行第三个函数,为了在改变PWM的占空比,同时保持状态机工作状态。

class PIOPWM:
def __init__(self, sm_id, pin, max_count, count_freq):
self._sm_id, self._pin, self._max_count, self._count_freq = sm_id, pin, max_count, count_freq
self.active_state = 0
self._pwm = 0
self._sm = StateMachine(self._sm_id, pwm_prog_0, freq=self._count_freq,
sideset_base=Pin(self._pin))

def set_pwm(self, value):
self._pwm = value
self.choose(self.active_state)

def choose(self, _value):
if _value:
if self._pwm == 0:
Pin(self._pin, Pin.OUT).value(0)
elif self._pwm == self._max_count:
Pin(self._pin, Pin.OUT).value(1)
elif self._pwm == self._max_count-1:
self._sm = StateMachine(self._sm_id, pwm_prog_1, freqself._count_freq,
sideset_base=Pin(self._pin))
self._sm.active(0)
self._sm.put(self._max_count-3)
self._sm.exec("pull()")
self._sm.exec("mov(isr, osr)")
self._sm.put(0)
self._sm.active(1)
else:
self._sm = StateMachine(self._sm_id, pwm_prog_0, freq=self._count_freq,
sideset_base=Pin(self._pin))
self._sm.active(0)
self._sm.put(self._max_count-3)
self._sm.exec("pull()")
self._sm.exec("mov(isr, osr)")
self._sm.put(self._pwm-1)
self._sm.active(1)
else:
self._sm.active(0)
self.active_state = _value

# Pin 20,21,22 on Pico boards
pwm_1 = PIOPWM(0, 20, max_count=10, count_freq=240_000_000)
pwm_2 = PIOPWM(2, 21, max_count=10, count_freq=240_000_000)
pwm_3 = PIOPWM(4, 22, max_count=10, count_freq=240_000_000)
pwm = [pwm_1, pwm_2, pwm_3]

标志位,channel_index表示当前选中的通道,freq_index、pwm_index、out_state三个数组分别存储三个通道的频率索引、pwm占空比数字、输出状态,vlist_index表示当前选中哪一行,即频率或者占空比,freq_list存储可选的PWM信号的频率。

freq_list = ['12M ', '1.2M', '120K', '12K ', '1.2K', '120 ']
channel_list = ['CH0', 'CH1', 'CH2']
freq_index = [0, 0, 0]
pwm_index = [0, 0, 0]
out_state = [0, 0, 0]
channel_index = 0
vlist_index = 0


5.2 LCD屏幕显示

屏幕颜色定义。

# Color definitions
BLACK = const(0x0000)
BLUE = const(0x001F)
RED = const(0xF800)
GREEN = const(0x07E0)
CYAN = const(0x07FF)
MAGENTA = const(0xF81F)
YELLOW = const(0xFFE0)
WHITE = const(0xFFFF)

屏幕初始化,sck脚为Pin2,mosi脚为Pin3,reset脚为Pin0,dc脚为Pin1,cs脚为Pin4。显示一张图片和一些基础信息(详见2.4)。

# 配置spi引脚
spi = SPI(id=0, baudrate=400000000, sck= Pin(2), mosi= Pin(3))
reset = Pin(0, Pin.OUT)
dc = Pin(1, Pin.OUT)
cs = Pin(4, Pin.OUT)
# 屏幕初始化
display = st7789.ST7789(
spi, 240, 240, reset, dc, cs
)

def display_init():
display.fill(st7789.WHITE)
display.vline(80, 0, 116, color=YELLOW)
display.vline(160, 0, 116, color=YELLOW)
display.hline(0, 116, 240, color=YELLOW)
image = open('./bin_2/'+'1'+'.bin', 'rb')
buf = image.read()
display.blit_buffer(buf, 120, 120, 120, 120)
image.close()
gc.collect()
display.text(font,'Glass*',0,208, color=WHITE, background=BLACK)

channel_show(),在主函数中循环执行的函数,即UI的显示,显示规则见2.4。配合5.1中“标志位”部分的内容进行理解。整体函数难度不高。

def channel_show():
global channel_index, vlist_index, freq_list, pwm_index, out_state
for i in range(0, 3):
display.text(font,freq_list[freq_index[i]],1+80*i,40, color=BLACK, background=WHITE)
display.text(font,str(pwm_index[i]),1+80*i,80, color=BLACK, background=WHITE)
for k in range(0, 4-len(str(pwm_index[i]))):
display.text(font,' ',48+80*i-16*k,80, color=WHITE, background=WHITE)
display.text(font,str(out_state[i]),60+80*i,0, color=BLACK, background=WHITE)
if i == channel_index:
display.text(font, channel_list[i],i*80+1,0, color=BLUE, background=WHITE)
else:
display.text(font, channel_list[i],i*80+1,0, color=BLACK, background=WHITE)
for j in range(0,2):
if j == vlist_index and i == channel_index:
display.text(font, '-', 80*channel_index+64, 40*vlist_index+40, color=MAGENTA, background=WHITE)
else:
display.text(font, ' ', 80*i+64, 40*j+40, color=WHITE, background=WHITE)
while True:
# gif_show()
channel_show()
time.sleep_ms(100)

gif_show(),功能是在屏幕上显示一个动态表情包的gif,通过读取flash中存储的已经从图像转换成的二进制文件,在LCD上进行显示。但由于主程序循环中较多的判断,导致画面不够流畅,所以注释掉。

pic_index = 0
def gif_show():
global pic_index
gc.collect()
image = open('./bin_2/'+str(pic_index)+'.bin', 'rb')
buf = image.read()
display.blit_buffer(buf, 120, 120, 120, 120)
image.close()
pic_index += 1
if pic_index > 3:
pic_index = 0


5.3 按键中断

配置与按键有关的Pin和中断函数绑定。

# 配置按键
key_Plus = Pin(5,Pin.IN,Pin.PULL_UP)
key_Minus = Pin(6,Pin.IN,Pin.PULL_UP)
key_L = Pin(7,Pin.IN,Pin.PULL_UP)
key_OK = Pin(8,Pin.IN,Pin.PULL_UP)
key_R = Pin(9,Pin.IN,Pin.PULL_UP)

# 按键中断
key_Plus.irq(key_Plus_interrupt, Pin.IRQ_FALLING)
key_Minus.irq(key_Minus_interrupt, Pin.IRQ_FALLING)
key_L.irq(key_L_interrupt, Pin.IRQ_FALLING)
key_OK.irq(key_OK_interrupt, Pin.IRQ_FALLING)
key_R.irq(key_R_interrupt, Pin.IRQ_FALLING)

按键中断事件函数,按键与LCD显示规则见2.4,其中变换频率和占空比时,注意修改对应通道下PWMPIO类中初始化下的状态机频率和占空比计数周期。

# 按键外部中断回调函数
def key_Plus_interrupt(key):
global channel_index
# 消除抖动
time.sleep_ms(100)
# 摁下左键
if not key.value():
channel_index += 1
if channel_index >= 3:
channel_index = 0
print("l")

def key_Minus_interrupt(key):
global vlist_index
# 消除抖动
time.sleep_ms(100)
# 摁下右键
if not key.value():
vlist_index += 1
if vlist_index >= 2:
vlist_index = 0
print("r")

def key_R_interrupt(key):
global pwm_index, channel_index, pwm, vlist_index, freq_index
# 消除抖动
time.sleep_ms(100)
# 再次判断按键是否被按下
if not key.value():
if vlist_index == 1:
pwm_index[channel_index] += 1
if freq_index[channel_index] == 0:
if pwm_index[channel_index] > 10:
pwm_index[channel_index] = 10
elif freq_index[channel_index] == 1:
if pwm_index[channel_index] > 100:
pwm_index[channel_index] = 100
else:
if pwm_index[channel_index] > 1000:
pwm_index[channel_index] = 1000
pwm[channel_index].set_pwm(pwm_index[channel_index])
elif vlist_index == 0:
freq_index[channel_index] += 1
if freq_index[channel_index] > 5:
freq_index[channel_index] = 5
if freq_index[channel_index] <= 2:
pwm[channel_index]._count_freq = 240_000_000
pwm[channel_index]._max_count = 10**(1+freq_index[channel_index])
elif freq_index[channel_index] == 3:
pwm[channel_index]._count_freq = 240_000_000
pwm[channel_index]._max_count = 1000
elif freq_index[channel_index] == 4:
pwm[channel_index]._count_freq = 2_400_000
pwm[channel_index]._max_count = 1000
elif freq_index[channel_index] == 5:
pwm[channel_index]._count_freq = 240_000
pwm[channel_index]._max_count = 1000
print("+")
def key_L_interrupt(key):
global pwm_index, channel_index, pwm, freq_index, vlist_index
# 消除抖动
time.sleep_ms(100)
# 再次判断按键是否被按下
if not key.value():
if vlist_index == 1:
pwm_index[channel_index] -= 1
if pwm_index[channel_index] < 0:
pwm_index[channel_index] = 0
pwm[channel_index].set_pwm(pwm_index[channel_index])
elif vlist_index == 0:
freq_index[channel_index] -= 1
if freq_index[channel_index] < 0:
freq_index[channel_index] = 0
if freq_index[channel_index] <= 2:
pwm[channel_index]._count_freq = 120_000_000
pwm[channel_index]._max_count = 10**(1+freq_index[channel_index])
elif freq_index[channel_index] == 3:
pwm[channel_index]._count_freq = 12_000_000
pwm[channel_index]._max_count = 1000
elif freq_index[channel_index] == 4:
pwm[channel_index]._count_freq = 1_200_000
pwm[channel_index]._max_count = 1000
elif freq_index[channel_index] == 5:
pwm[channel_index]._count_freq = 120_000
pwm[channel_index]._max_count = 1000
print("-")

def key_OK_interrupt(key):
global channel_index, out_state, pwm
# 消除抖动
time.sleep_ms(100)
# 再次判断按键是否被按下
if not key.value():
out_state[channel_index] = 1 - out_state[channel_index]
pwm[channel_index].choose(out_state[channel_index])
print('o')

6 演示视频



7 个人总结

该任务,总体来看,难度并不是很大,但是需要注意的细节比较多,也需要一定的经验,门槛较高。

突破的第一个难点,PIO编程,用适当的方式输出PWM信号,并且了解8种汇编指令,熟练的使用它们。这中间不仅仅只是软件学习那么简单,硬件学习需要在此基础上,不断地调试并且查看现象,然后调整代码,继续下一步工作。

突破的第二个难点,输出任务要求的PWM信号,思路可以有很多,像我采用的就是比较暴力的解法,超频,然后配合树莓派官方开源的例子,进行修改,转化为属于自己的代码。

突破的第三个难点,LCD显示,好在这个带屏RP2040到手,烧进可用MicroPython编写的uf2之后,flash内好像已经有硬禾学堂提供的st7789驱动库和两种大小不同的字库,可供直接使用。最后UI的编写费了一些功夫,但都是设计思路上的功夫,而不是底层思路,节省了大部分时间。

突破的第四个难点,按键、LCD显示和PWM信号输出,三者相互配合,顺畅的完成任务要求,这需要一步一步测试整个代码的子功能,然后再将这些子功能配合测试,最后则是用十二指神探测试输出的信号是否复合要求。

有快乐也有苦恼,第一次用MicroPython语言编写硬件,第一次用PIO类似汇编的功能,其特点也是非常鲜明,感受到了RP2040优秀的开发前景,也看到了许多前辈在各个平台上留下的开源项目,受益匪浅。

附件下载
st7789.py
st7789驱动库
vga1_16x32.py
16*32大小的字库(非中文)
main.py
主程序
1.bin
图片二进制文件
团队介绍
华北电力大学 电气与电子信息工程学院 电子信息工程 王亚楠
团队成员
Glass
和自己相比,有进步吗?
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号