一、 项目描述
1.1 项目介绍
行人按钮是一种可以用来触发交通信号灯的按钮,通常位于红绿灯控制器旁边或者路口附近。行人按下按钮后,信号灯会变成行人绿灯,使行人可以安全地穿过道路。在一些地区,如果没有行人按下按钮,交通信号灯可能不会变成行人绿灯,这就需要行人等待更长时间。
本项目旨在利用Step Pico的控制功能结合上扩展板上的相关元件,模拟真实情况下的带有行人按钮的交通灯工作流。
1.2 设计思路
基本构成:由三色led模拟红黄绿灯,k1按钮充当行人按钮,蜂鸣器作为警报装置。当按钮未触发时,三色灯定时切换;当k1被触发,由于触发的持续时间很短,要求某处需要持续记录当前触发事件,因此可以利用多线程技术,在主线程循环交通灯的同时,快速检测k1的触发状态,当检测到触发后,将信号量置为真。此时,主线程在每个循环开始会先检测信号量,若为真,进入行人优先模式,此时信号灯为绿,蜂鸣器启动。
在此基础上,由于日常生活中有很多交通灯整合了带有行人形象的标志灯,本项目也同样模拟了这个功能。通过编写转换模块,把png等格式的图片转码为可被扩展板上oled显示屏直接读取的字节流格式,并根据当前交通灯的状态同步切换显示内容。
1.3 框图和软件流程图
逻辑框图
软件流程图
1.4 硬件介绍
Pico麻雀虽小,五脏俱全。Pico是一款由英国树莓派基金会开发的微控制器板,它采用了Arm Cortex-M0+处理器,并搭载了2MB的闪存和264KB的SRAM,还集成了各种常用的硬件接口和控制器,如GPIO、SPI、I2C、PWM、ADC等。Pico的引脚布局与Arduino Uno兼容,可与许多Arduino扩展板兼容,,其可以通过C/C++以及MicroPython编程来学习嵌入式系统的工作原理和应用。
利用micropython SDK, 像我这样只有python开发经验的嵌入式小白也能通过查阅文档很快上手开发。由硬禾课堂提供的例程封装了扩展板的很多功能部件,如oled,led和button。在此基础上,我也封装了蜂鸣器的调用文件buzzer.py。
1.5 实现的功能及图片展示
交通灯默认以绿黄红色交替切换,且oled上显示的图像也根据交通灯的颜色变化。
绿 黄
红 行人优先
二、 主要代码片段及说明
2.1 循环主体和优先模式的逻辑
循环主体基本构成为:灯亮-屏显内容切换-延时-灯灭,值得注意的是,在黄灯亮起时,为实现行人标志闪烁),使用了poweroff/poweron对oled进行两轮断电加电(而不是清空图片内容),原因在于,断电后,oled的帧缓冲区仍会保存断电前的图片内容,可减少对图片的读写次数,提高性能。
If语句缩进代码块为优先模式的判断逻辑:如果信号量button_pressed=True, 表明检测到行人触发行人按钮,此时会在循环开始之前进入优先模式,蜂鸣器启动,oled图片切换为停车标志。结束后将信号量重置为False。
while True:
if button_pressed == True:
# 优先模式
g.on()
show_img(data[2])
buzzer(5)
g.off()
button_pressed = False
#绿灯
g.on()
show_img(data[0])
utime.sleep(5)
g.off()
# 黄灯
y.on()
for _ in range(2):
oled.poweroff()
utime.sleep(0.5)
oled.poweron()
utime.sleep(0.5)
y.off()
# 红灯
r.on()
show_img(data[1])
utime.sleep(5)
r.off()
2.2 查询行人按键事件的线程
通过全局变量向主线程传递信息。功能很简单,即,当检测到k1被按下时,置button_pressed为True。需要注意的一点是,与python中的global相同,在函数体内部使用时,为与局部变量相区分,要先添加语句 global <变量名>。
# PV信号量,与button_reader_thread通信
global button_pressed
button_pressed = False
# 每0.01s读取k1的状态,将信号量置为True
def button_reader_thread():
global button_pressed
while True:
if k1.value() == 1:
button_pressed = True
之后,利用micropython多线程库_thread启动该线程。由于不需要参数传递,start_new_thread的第二个参数为空元组。
# 启动该线程
_thread.start_new_thread(button_reader_thread,())
2.3 OLED显示屏的图像显示
Oled屏读取数据的格式时bytearray, 因此在下载到需要的图像之后,需要对其进行转换。
转换脚本接受参数:图片名,宽,高
from io import BytesIO
from PIL import Image
import sys
import pathlib
import os
THRESHOLD = 200
if len(sys.argv) > 1:
path_to_image = str(sys.argv[1])
x = int(sys.argv[2])
y = int(sys.argv[3])
filename = path_to_image.split('.')[0]
im = Image.open(path_to_image)
im = im.resize((x,y)).convert('1')
# Define a threshold value to remove the point noise
threshold_value = THRESHOLD
# Apply the threshold using the point method
def threshold(pixel):
if pixel < threshold_value:
return 0
else:
return 255
im = im.point(threshold)
pathlib.Path(os.path.join('out', filename)).mkdir(parents=True, exist_ok=True)
buf = BytesIO()
im.save(buf, 'ppm')
im.save(f'./out/{filename}/{filename}-{x}x{y}.png')
byte_im = buf.getvalue()
temp = len(str(x) + ' ' + str(y)) + 4
with open(f'./out/{filename}/{filename}-{x}x{y}.txt', 'w') as f:
f.write(str(byte_im[temp::]))
# print(byte_im[temp::])
print('file converted successfully')
else:
print("please specify the location of image i.e img2bytearray.py /path/to/image width heigh")
转换流程:通过PIL先对图像进行预处理(缩放,二值化),然后以ppm格式存储,最后通过python内置io.BytesIO将其转为比特流。
转换完成后,缩放后的图片(png格式)和比特流文件(txt格式)存入以图片名命名的文件夹下,方便后续加载。
硬禾课堂提供的例程封装了oled的接口,但是为了项目内方便调用,在其上再次进行封装。
def load_img(img_name):
img_root = img_name
files = os.listdir(img_root)
txt_files = [file for file in files if file.endswith('.txt')]
txt_file = txt_files[0]
_, wxh, _, offset_redundant = txt_file.split('-')
offset = offset_redundant.split('.')[0]
width, height = wxh.split('x')
byte_path = img_root + '/' + txt_file
with open(byte_path, 'r') as f:
contents = f.read()
bytes = eval(contents)
return (bytearray(bytes), int(width), int(height), int(offset))
def show_img(data):
TH, width, height, offset = data
fb = framebuf.FrameBuffer(TH, width, height, framebuf.MONO_HLSB)
oled.fill(0)
oled.blit(fb, offset, 0)
oled.show()
Load_img: 根据上文所述图片名从txt文档中加载比特流,同时,文件名中还标记了图片宽高和位移。为下一步的绘制提供基本信息。
Show_img: 承接load_img返回的数据,底层调用oled模块将图片绘制到oled屏幕上。
交通灯的切换很考验实时性,而图片加载等io操作往往就是性能瓶颈,为避免出现延后等问题,图片在初始化阶段即加载完成。
# 一次性读入图像, 后续循环直接读取data
data = [load_img('walk'), load_img('stand'), load_img('stop_sign')]
# 初始化交通灯
r.off()
g.off()
y.off()
之后的循环内直接读取缓存中的图片:
while True:
if button_pressed == True:
g.on()
show_img(data[2]) # 此处切换图片
buzzer(5)
g.off()
button_pressed = False
g.on()
show_img(data[0]) # 此处切换图片
utime.sleep(5)
g.off()
y.on()
for _ in range(2):
oled.poweroff()
utime.sleep(0.5)
oled.poweron()
utime.sleep(0.5)
y.off()
r.on()
show_img(data[1]) # 此处切换图片
utime.sleep(5)
r.off()
三、 遇到的主要难题及解决方法
问题一:彩色图片经转换后,出现大量噪点,使得图片信息损失严重。
解决:之前的图片预处理过程是:先二值化,再进行放缩。而在第一步彩色像素会被转化成黑白混杂的像素块,此时放缩,局部采样就会出现斑点。因此,我先进行放缩,再二值化。有效的提高了图片质量。
问题二:考虑到图片io的性能制约,我一开始时打算再创建一个线程处理oled显示。而micropython多线程库处尚处于实验阶段,arm处理器的每个核只能运行一个线程,所以总共只能运行两个线程。
解决:如上文所述,我改进了图片加载的流程,有效的降低了图片加载时延。
四、 未来的计划或建议
经过这次的锻炼,我对硬件开发这个领域再次产生了浓厚的兴趣(上次是数电考试成绩出分之前)。下次我想试一下FPGA(虽然感觉会很难)。
有一点小建议是,虽然平时看文档也不少,但是涉及到硬件的datasheet让我这个半吊子还是晕头转向,希望可以有课程教大家如何从中有效检索信息。
五、 总结
之前说到嵌入式总想到的是单片机,经过这次上手体验Pico,我意识到小小的MCU也有巨大的潜力。