0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

【试用报告】RP2040上的MicroPython环境中多线程编程

电子发烧友论坛 来源:未知 2023-04-18 09:15 次阅读

双核介绍

BPI-Pico-RP2040官方介绍如下:



其核心是RP2040,采用的是ARM Cortex M0+ CPU内核,运行频率高达 133 MHz。


比一般使用Cortex M0+的MCU更强大的是,RP2040使用了双核ARM Cortex M0+,既然是双核的,那么我们就可以在BPI-Pico-RP2040运行多线程程序了,更好的挖掘出其潜力来。


多线程了解

关于什么是多线程,本文不讲,大家可以自行查找资料详细了解。


为了更方便的进行测试,本次所有的实例,都是在python环境中进行的。


经过了解,circuitpython还不支持多线程,而micropython则已经提供支持。


不过micropython中的多线程还是实验性质的支持,这从官方文档中可以了解:MicroPython libraries » _thread – multithreading support



micropython官方为RP2040提供的最新固件为v1.19.1,其已提供对多线程的支持。


因为micropython的多线程基于CPython中的_thread模块,所以可以从Python官方文档了解其具体用法:_thread --- 底层多线程 API


如果是开始使用多线程,那么先关注如下的调用,等熟悉了以后,再深入学习其他的:

  • _thread.start_new_thread(function, args[, kwargs]):开启一个新线程

  • _thread.allocate_lock():返回一个新的锁对象

  • lock.acquire(blocking=True, timeout=- 1):申请获得锁

  • lock.release():释放锁


本文中所有的实例代码,都可以从以下地址获取:

Pico(RP2040)上的MicroPython环境中多线程编程https://gitee.com/honestqiao/multithread_in_micropython_on_pico


基础多线程

首先,用一个简单的micropython程序,来控制板载的LED不同时间点亮和熄灭

# file: multicore_test01.py
import machine
import _thread
import utime


led = machine.Pin(25, machine.Pin.OUT)
led.off()


key = 0
start_time = 0
def run_on_core1():
global start_time
while start_time == 0:
pass


while True:
utime.sleep_ms(300)
print((utime.ticks_us()-start_time)//100000, "led on")
led.on()
utime.sleep_ms(700)

def run_on_core0():
global start_time
start_time = utime.ticks_us()
while True:
utime.sleep_ms(700)
print((utime.ticks_us()-start_time)//100000, "led off")
led.off()
utime.sleep_ms(300)


_thread.start_new_thread(run_on_core1, ( ))
run_on_core0()

(左右移动查看全部内容)


在RP2040的micropython环境中,程序默认在core0上运行,使用_thread.start_new_thread()启动新的线程后,将会在core1上运行。


上面的程序运行后,具体输出结果如下:



在run_on_core1中,先延时300ms,然后点亮led,再延时700ms,然后继续循环


在run_on_core0中,先延时700ms,然后熄灭led,再延时300ms,然后继续循环


从以上的输出可以看到,点亮和熄灭led,都对应到了对应的时间点。


也许有人会说,这有啥用,我不用多线程,也完全可以在对应的时间点点亮和熄灭LED,用多线程岂不是多此一举。


上面的例子,是一个基础的多线程演示,其只是在两个线程中,控制同一个LED,所以会觉得意义不大。如果我们的程序要同时做两件不同的事情,那么每件事情在一个core上运行,互不干扰,就很重要的,在后面会有这样的实例展示。


确认双线程

在不同的开发板上,对多线程的支持,也是有差异的。


RP2040上的micropython,只能跑两个线程,每个线程占用1个core,多了就会出错。


我们可以用下面的程序进行验证:

# file: multicore_test02.py


import machine
import _thread
import utime


def thread_1():
while True:
print("thread_1")
utime.sleep_ms(1000)


def thread_2():
while True:
print("thread_2")
utime.sleep_ms(1000)


_thread.start_new_thread(thread_1, ( ))
_thread.start_new_thread(thread_2, ( ))


while True:
print("main")
utime.sleep_ms(1000)

(左右移动查看全部内容)


运行上面的程序后,将会出现如下的错误信息



其原因在于,主程序本身,使用了core0,而使用_thread.start_new_thread()创建一个线程时,会自动的使用core1,第二次调用_thread.start_new_thread()再次创建一个线程时,无法再使用core1,所以就会出错。


在core1上运行的子线程,需要使用_thread.start_new_thread()创建,所以其运行的需要使用一个函数进行调用作为入口。


而程序的主线程,运行在core0上,可以直接在程序主流程中写运行逻辑,也可以写一个函数调用,效果是一样的。


后续的实例中,我们将使用run_on_core0()和run_on_core1()来区分在core0、core1的所运行的线程。



线程间交互


全局变量

通常时候,让两个线程,分别做各自独立的事情,可以运行的很好。


但有的时候,我们可能还需要两个之间,能够有一些交流。


最简单的方法,就是使用一个全局变量,然后两个线程之间,都调用这个全局变量即可。


下面用一个简单的程序进行演示:

# file: multicore_test03.py
import machine
import _thread
import utime


led = machine.Pin(25, machine.Pin.OUT)
led.off()


status = 0
def run_on_core1():
global status
while True:
if status:
led.on()
else:
led.off()
utime.sleep_ms(100)

def run_on_core0():
global status
while True:
status = 1 if not status else 0
utime.sleep_ms(1000)


_thread.start_new_thread(run_on_core1, ( ))
run_on_core0()

(左右移动查看全部内容)


在上面的程序中,core0上的程序,每隔1秒,将status取反一次。core1上的程序,则根据status的值,来点亮或者熄灭LED。


线程锁

上面这个程序比较简单,处理起来的速度很快,所以这么实用,不会有什么问题。


如果我们有一个程序需要两个线程进行配合,例如主线程进行数据采集分析,而子线程进行数据的呈现,就有可能会出现问题了。


我们看一看下面的程序:

# file: multicore_test04.py
import machine
import _thread
import utime


led = machine.Pin(25, machine.Pin.OUT)
led.off()


status = 0
data = []
def run_on_core1():
global status, data
while True:
if status:
led.on()
else:
led.off()
str_data = ''.join(data)
print("str_data: len=%d content=%s" % (len(str_data), str_data))
utime.sleep_ms(1000)

def run_on_core0():
global status, data
while True:
status = 1 if not status else 0
data = []
for i in range(100):
data.append(str(status))
utime.sleep_ms(10)
utime.sleep_ms(1000)


_thread.start_new_thread(run_on_core1, ( ))
run_on_core0()

(左右移动查看全部内容)


在core0的主线程中,根据status的值,将data设置为100个0或者1;而在core1的子线程中,则将其值合并为字符串输出出来,输出的同时,显示字符串的长度。


运行上面的程序后,实际输出结果如下:



按说,其长度,要么是空,要么是100,可是实际结果却会出现不为100的情况呢?


这是因为,core0上的主线程在操作data,core1的子线程也在操作data,两者都是在同时进行的,而多个控制线程之间是共享全局数据空间,那么就会出现,core0上的主线程处理数据处理到到一半了,core1的子线程已经开始操作了,这样就会出现问题,数据不完整了。


显然,这种情况,是我们所不期望的。那要解决这种情况,可以用一个全局变量作为标志,主线程告诉子线程是否处理完成了,一旦处理完成了,子线程就可以开始处理了。


但线程调用库本身,有更好的办法,那就是锁。


我们先看下面的程序:

# file: multicore_test05.py
import machine
import _thread
import utime


led = machine.Pin(25, machine.Pin.OUT)
led.off()


status = 0
data = []
def run_on_core1():
global status, data
while True:
if status:
led.on()
else:
led.off()
lock.acquire()
str_data = ''.join(data)
print("str_data: len=%d content=%s" % (len(str_data), str_data))
lock.release()
utime.sleep_ms(1000)

def run_on_core0():
global status, data
while True:
status = 1 if not status else 0
lock.acquire()
data = []
for i in range(100):
data.append(str(status))
utime.sleep_ms(10)
lock.release()
utime.sleep_ms(1000)


lock = _thread.allocate_lock()
_thread.start_new_thread(run_on_core1, ( ))
run_on_core0()

(左右移动查看全部内容)


在上面的程序中,启动线程之前,使用 _thread.allocate_lock() 来获取一个新的锁,然后在core0的主线程中,处理数据前,使用 lock.acquire() 获得锁,处理完成后,再使用lock.release()释放锁。


一但一个线程获得锁,那么其他线程想要获得该锁时,只能等待直到这个锁被释放,也就是不能同时获得,这在python中叫做互斥锁。


因而,在core1的子线程,要输出数据的时候,也使用同样的机制来获得和释放锁。


最终,data改变时,其他地方需要等待改变完成。data输出时,其他地方也需要等待输出完成。从而确保了任何时刻,对只有一个地方操作改数据。


运行上面的程序,就能得到理想的输出了:



运行中启动线程

前面演示的程序,都是在主线程中,启动了子线程,然后并行运行。


在实际使用中,还可以在主线程中,按需启动子线程。

我们先看下面的程序:

# file: multicore_test06.py
import machine
import _thread
import utime


def run_on_core1():
print("[core1] run thread")
utime.sleep_ms(100)


def run_on_core0():
while True:
print("[core0] start thread:")
_thread.start_new_thread(run_on_core1, ( ))
utime.sleep_ms(1000)

run_on_core0()

(左右移动查看全部内容)


在上面的程序中,core0上运行的主线程,会每过1秒启动一个子线程。子线程在core1上运行完以后,会自动退出。


运行后,输出如下:



需要特别注意的是,如果子线程还没有退出,那么再次启动,将会出现错误。


例如我们修改上面的程序的延时如下:

# file: multicore_test07.py
import machine
import _thread
import utime


def run_on_core1():
print("[core1] run thread")
utime.sleep_ms(1000)


def run_on_core0():
while True:
print("[core0] start thread:")
_thread.start_new_thread(run_on_core1, ( ))
utime.sleep_ms(100)

run_on_core0()

(左右移动查看全部内容)


运行后,就会出错:

[core0] start thread:
[core1] run thread
[core0] start thread:
Traceback (most recent call last):
File "", line 17, in
File "", line 14, in run_on_core0
OSError: core1 in use


其原因就在于,子线程还没有结束,主线程又再次启动主线程了。


这在多线程编程中,是需要特别注意的问题。


要解决这个问题,可以使用前面主线程和子线程交互中的方法,例如使用一个全局变量表示子线程是否运行完成,或者使用锁。


下面是一个使用锁的程序:

# file: multicore_test08.py
import machine
import _thread
import utime


def run_on_core1():
lock.acquire()
print("[core1] run thread")
utime.sleep_ms(1000)
lock.release()


def run_on_core0():
while True:
print("[core0] start thread:")
lock.acquire()
_thread.start_new_thread(run_on_core1, ( ))
lock.release()
utime.sleep_ms(100)


lock = _thread.allocate_lock()
run_on_core0()

(左右移动查看全部内容)


运行后,输出如下:

[core0] start thread:
[core1] run thread
[core0] start thread:
[core1] run thread
[core0] start thread:
[core1] run thread
[core0] start thread:
[core1] run thread



多线程的实例


双线程做pwm和ws2812b

下面,再用一段稍微复杂一点点的程序,演示多线程的使用。

# file: multicore_test09.py
import machine
import _thread
import utime
from ws2812 import WS2812


led = machine.Pin(25, machine.Pin.OUT)
led.off()


BLACK = (0, 0, 0)
RED = (255, 0, 0)
YELLOW = (255, 150, 0)
GREEN = (0, 255, 0)
CYAN = (0, 255, 255)
BLUE = (0, 0, 255)
PURPLE = (180, 0, 255)
WHITE = (255, 255, 255)
COLORS = (BLACK, RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE)


ws = WS2812(3, 1) #WS2812(pin_num,led_count)
ws.pixels_fill(BLACK)
ws.pixels_show()


def run_on_core1():
while True:
for color in COLORS:
ws.pixels_fill(color)
ws.pixels_show()
utime.sleep_ms(200)

def run_on_core0():
duty = 0
step = 1
count = 0
while True:
led.on()
utime.sleep_ms(duty)
led.off()
utime.sleep_ms(10-duty)

count = count + 1
if count>10:
count = 0
duty = duty + step
if duty >= 10:
step = -1
if duty <= 0 :
step = 1


_thread.start_new_thread(run_on_core1, ( ))
run_on_core0()

(左右移动查看全部内容)


在上面的这段程序中,我们会在core0上运行的主线程中,控制GPIO25的输出占空比,从而让板载LED产生类似呼吸灯的效果。同时,还会在core1上运行的子线程中,控制板载WS2812B灯珠变色。


双线程播放Bad Apple

最后,我们再用经典的Bad Apple,作为这篇文章的结尾。

# file: multicore_test10.py
from machine import SPI,Pin
from ssd1306 import SSD1306_SPI
import framebuf
import _thread
import utime


spi = SPI(1, 100000, mosi=Pin(11), sck=Pin(10))
display = SSD1306_SPI(128, 64, spi, Pin(9),Pin(8), Pin(1))


def run_on_core1():
global fbuf
while True:
if not fbuf == None:
display.fill(0)

lock.acquire()
display.blit(fbuf,19,0)
fbuf = None
lock.release()

display.show()


utime.sleep_ms(100)


def run_on_core0():
global fbuf
while True:
for i in range(1,139):
dirt = 'BAD_APPLE/' + str(i) + '.pbm'
print(i, dirt)
with open(dirt,'rb') as f :
f.readline()
f.readline()
data = bytearray(f.read())

lock.acquire()
fbuf = framebuf.FrameBuffer(data,88,64,framebuf.MONO_HLSB)
lock.release()

utime.sleep_ms(100)


fbuf = None
lock = _thread.allocate_lock()
_thread.start_new_thread(run_on_core1, ( ))
run_on_core0()

(左右移动查看全部内容)


上面的代码,使用core0上运行的主线程,来从pbm文件中读取需要呈现的图片数据,而在core1上运行的子线程中,则使用读取到的数据输出到OLED进行显示。


因为受限于Pico内置存储的限制,并没有存储完整的Bad Apple数据,所以只播放了部分。如果感兴趣,可以将数据放置到SD卡上,主线程读取数据,子线程显示数据,一样丝滑流畅。



后记


多线程是个让人有爱又恨的东西,用好了能有大作用,但是用不好可能会出现莫名其妙的问题,需要好好钻研。本文只是一些较为基础的研究,还比较浅显,对于gc等方面,都尚未涉及,感兴趣的读者可以进一步深入了解。


在钻研的过程中,参考了不少资料,对所有资料的贡献者表示感谢。以下为参考到的部分资料列表:

  • 树莓派Pico迷你开发板MicroPython多线程编程实践

  • Multithreaded on Raspberry Pi Pico (MicroPython)

  • Raspberry Pi Pico Dual Core Programming

  • Multi Thread Coding on the Raspberry Pi Pico in Micropython

  • pico-micropython-examples

  • Raspberrypi Pico MicroPython Cookbook

  • MicroPython类库 » _thread --- 线程

  • ESP32上驱动OLED屏幕播放你想要的视频



声明本文由电子发烧友社区发布,转载请注明以上来源。如需社区合作及入群交流,请添加微信EEFans0806,或者发邮箱liuyong@huaqiu.com。


更多热点文章阅读

  • 龙芯架构首款面向嵌入式应用的开发板,2K500开发应用实例

  • 基于32位RISC-V设计的互联型微控制器,沁恒微CH32V307开发样例

  • RK3568!四核64位ARMv8.2A架构,汇聚编译源码及实战样例

  • 尺寸仅有21mm*51mm,板边采用邮票孔设计,合宙 Air105 核心板开发总结

  • 基于32位RISC-V高集成SoC,ADP-Corvette-T1开发板样例及源码!


原文标题:【试用报告】RP2040上的MicroPython环境中多线程编程

文章出处:【微信公众号:电子发烧友论坛】欢迎添加关注!文章转载请注明出处。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

原文标题:【试用报告】RP2040上的MicroPython环境中多线程编程

文章出处:【微信号:gh_9b9470648b3c,微信公众号:电子发烧友论坛】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    Python多线程和多进程的区别

    Python作为一种高级编程语言,提供了多种并发编程的方式,其中多线程与多进程是最常见的两种方式之一。在本文中,我们将探讨Python多线程
    的头像 发表于 10-23 11:48 203次阅读
    Python<b class='flag-5'>中</b><b class='flag-5'>多线程</b>和多进程的区别

    我用了3分钟,从零实现了单片机的点灯开发!

    近日,小熊派悄悄的上线了一款新的Pico板,基于树莓派RP2040芯片的:BearPi-Pico RP2040 树莓派? 对,你没看错!树莓派不仅是一家优秀的开源硬件品牌,更是一家优秀的芯片公司
    发表于 09-27 15:51

    龙芯2K0300蜂鸟板试用报告

    龙芯2K0300蜂鸟板试用报告 一、试用环境 操作系统和框架:Linux+QT5.15 交叉编译工具链
    发表于 09-13 18:00

    ubuntu上交叉编译rp2040

    连接到rp2040 ,所以使用另一块pico作为调试器,需要给pico调试器下载固件,也就是picoprobe 地址 https://github.com/Wiz-IO
    发表于 08-27 08:00

    树莓派Pico 2发布,搭载RP2350双核RISC-V和Arm Cortex-M33微控制器!

    RP2040 一样轻松超频到 250MHz 官方声称 RP2350 与 RP2040 在软件和硬件都是向前兼容的(RP2350 兼容
    发表于 08-13 10:07

    pico-ice:RP2040 plus Lattice iCE40UP5K FPGA 开发板 介绍

    RP2040 提供的 FPGA 时钟,可在 SW 控制下轻松对 FPGA 时钟进行编程 RP2040 可以对 FPGA 进行编程,还提供直通 UART 功能 通过 SPI 与 FPG
    发表于 06-28 15:45

    java实现多线程的几种方式

    Java实现多线程的几种方式 多线程是指程序包含了两个或以上的线程,每个线程都可以并行执行不同的任务或操作。Java
    的头像 发表于 03-14 16:55 542次阅读

    基于树莓派RP2040的解魔方机器人,7秒还原三阶魔方

    地望着你,是时候亮出工程师的魔法神器了。今天特别分享@爱跑步的小何大佬的开源佳作——三阶魔方还原机器人。三阶魔方还原机器人-开源分享-这是一款基于树莓派RP2040单片机设
    的头像 发表于 01-13 08:04 1674次阅读
    基于树莓派<b class='flag-5'>RP2040</b>的解魔方机器人,7秒还原三阶魔方

    基于树莓派RP2040单片机设计的三阶魔方还原机器人

    这是一款基于树莓派RP2040单片机设计的三阶魔方还原机器人,控制和魔方求解都使用单片机完成。对于随机打乱的三阶魔方,平均还原步骤数在21步左右。
    的头像 发表于 01-12 13:37 1280次阅读
    基于树莓派<b class='flag-5'>RP2040</b>单片机设计的三阶魔方还原机器人

    mfc多线程编程实例

    (图形用户界面)应用程序的开发。在这篇文章,我们将重点介绍MFC多线程编程多线程编程
    的头像 发表于 12-01 14:29 1361次阅读

    多线程如何保证数据的同步

    多线程编程是一种并发编程的方法,意味着程序同时运行多个线程,每个线程可独立执行不同的任务,共享
    的头像 发表于 11-17 14:22 1079次阅读

    RP2040和Raspberry Pi的区别

    作为 Raspberry Pi 基金会推出的首款微控制器产品,RP2040 标志着以单板计算机 (SBC) 而闻名的 Raspberry Pi 基金会的新方向。RP2040 将 Raspberry
    的头像 发表于 11-16 17:37 865次阅读
    <b class='flag-5'>RP2040</b>和Raspberry Pi的区别

    Linux系统多线程和多进程的运行效率

    关于多进程和多线程,教科书上最经典的一句话是“进程是资源分配的最小单位,线程是CPU调度的最小单位”,这句话应付考试基本够了,但如果在工作遇到类似的选择问题,那就没有这么简单了,选
    的头像 发表于 11-10 10:54 1273次阅读
    Linux系统<b class='flag-5'>上</b><b class='flag-5'>多线程</b>和多进程的运行效率

    LabVIEW多线程编程解析 LabVIEW的VI优先级和并行循环等相关知识

    软件开发过程总会遇到需要多线程同步运行的情况,尤其是一些复杂的测试系统和大型项目,仅靠单线程运行的程序是远远无法满足用户需求的,甚至可以说在复杂测试系统的软件开发
    的头像 发表于 11-10 10:20 1w次阅读
    LabVIEW<b class='flag-5'>多线程</b><b class='flag-5'>编程</b>解析 LabVIEW的VI优先级和并行循环等相关知识

    在树莓派Pico RP2040怎样使用MicroPython呢?

    RP2040 是一款由树莓派公司设计的 32 位双核 ARM Cortex-M0+ 微控制器芯片,于 2021 年 1 月发布,作为树莓派 Pico 开发板的核心部件。
    的头像 发表于 11-08 14:29 2996次阅读
    在树莓派Pico <b class='flag-5'>RP2040</b><b class='flag-5'>上</b>怎样使用<b class='flag-5'>MicroPython</b>呢?