【得捷Follow me第3期】使用Xiao开发板给他过一个难忘的生日
[复制链接]
本帖最后由 大波丁 于 2023-12-17 21:58 编辑
有幸入围本次的【Follow Me第三期】活动,从得捷DigiKey采购了以下物料:
物料 |
介绍 |
链接 |
XiaoESP32C3核心板 |
深圳SeedStudio(矽递)公司设计开发的一款小尺寸ESP32C3核心开发板 |
购买链接 |
Xiao扩展板 |
可适配XiaoESP32C3的扩展版,含0.96寸OLED屏幕、蜂鸣器、SD卡槽、RTC芯片等外设和接口 |
购买链接 |
XiaoAHT20模块 |
适配Xiao扩展板IIC接口端子的温湿度传感器 |
购买链接 |
Xiao光电模块 |
适配Xiao扩展板模拟量接口端子的环境光传感器,光电二极管加运放 |
购买链接 |
任务的基本要求为采用MicroPython的方式对ESP32C3进行开发,并且使用到板载的各项外设,包括屏幕、蜂鸣器、WIFI、温湿度传感器、光线传感器等;任务的拓展要求为综合实践,使用以上外设,选择或者自行设计一个综合性的应用加以实现。
着手完成任务的时候离活动截止时间已经不多了,正值儿子过生日,就想着给他设计一个电子生日贺卡,在他幼小的心中埋下些关于电子、编程等理工科的种子,或许将来也会如我一般对这些产生好奇和热爱。
2023-12-17 21:26 上传
环境搭建
这次是我第一次使用MicroPython进行ESP32的开发,之前一直使用C语言在CLion下进行ESP-IDF的开发,相比而言,MicroPython的开发环境确实搭建起来非常简单:
下载
整个环境搭建只要下载以下两个内容,一个是固件库,一个是esptool命令行工具:
烧录
使用TypeC连接核心板和电脑,正常情况下计算机的设备管理器中应该能观察到一个新的串口设备,这是出厂固件所提供的,但是我在电脑上却一直没能观察到串口设备。后来辗转了多次,才发现使用了一根没有通讯能力的TypeC线(用这根线连接电脑和安卓手机也只能充电,无法传输数据),换了成了原装的TypeC线,顺利在电脑上观察到了新的串口了。
有了串口端口号,即可通过esptool工具将ESP32C3上最新的MicroPython固件烧录进去,执行脚本为:
- esptool.exe --chip esp32c3 --port COM5 --baud 921600 --before default_reset --after hard_reset --no-stub write_flash --flash_mode dio --flash_freq 80m 0x0 ESP32_GENERIC_C3-20231005-v1.21.0.bin
上述脚本参数中,COM5是我的串口端口号,ESP32_GENERIC_C3-20231005-v1.21.0.bin是下载回来的最新的固件库,将它放到esptool同一路径即可。
命令执行需要一段时间,直到命令行提示硬件已成功复位,完成烧录。
uPyCraft
根据本次活动的直播课程,使用uPyCraft这款工具进行ESP32C3的MicroPython开发,这是个非常轻量级的小工具,可阅读这篇文章进行下载和安装。安装结束后,打开后的画面是这样的:
操作步骤如下:
- 打开uPyCraft
- Tools中选择正确的串口
- workSpace选定本地工作目录
- 新建py脚本,比如hello.py
- File中刷新工作区
- 右边的烧录,观察串口输出
- microPython依次执行device中的boot.py和main.py
- 代码中如果出现while True:这类死循环,需要RESET后观察,如果此时串口介入,会STOP程序
- 开发过程中如果发生任何异常,可按下扩展板上的RESET尝试解决
接下来,即可阅读MicroPython的官方手册,踏上ESP32C3的MicroPython之旅了。
GPIO
任何MCU的起点都是点灯,也就是GPIO的控制,根据Pins and GPIO的介绍,我们来看看python是如何控制ESP32的GPIO的:
from machine import Pin
p0 = Pin(0, Pin.OUT) # create output pin on GPIO0
p0.on() # set pin to "on" (high) level
p0.off() # set pin to "off" (low) level
p0.value(1) # set pin to on/high
p2 = Pin(2, Pin.IN) # create input pin on GPIO2
print(p2.value()) # get value, 0 or 1
p4 = Pin(4, Pin.IN, Pin.PULL_UP) # enable internal pull-up resistor
p5 = Pin(5, Pin.OUT, value=1) # set pin high on creation
p6 = Pin(6, Pin.OUT, drive=Pin.DRIVE_3) # set maximum drive strength
因为我平时不用Python,再加上一直以来我对Python极自由主义(反骨)的语法设计持保留看法,所以这次我这次几乎是面对一个新的语言,相信也会有一些跟我一样的python初学者,我们来利用这次使用MicropPython的机会全新地理解一下Python。
- from, import 关键字,引入库,machine是个库,Pin是这个库中的某个类。可以引入整个库比如这里可以写import machine,那么下文就可以用machine.Pin来使用Pin类。也可以只引入库中某个类(或函数),比如from machine import Pin,那么下文可以直接使用Pin类。整体上from和import两个关键字的设计,Python采用了与JavaScript类似的方式,一切看上去非常和谐。
- 初始化对象,p0 = Pin(0, Pin.OUT),这种初始化类对象的方式,调用了Pin类的构造函数,其设计思想来自于C++。Python是弱类型语言,定义变量时并不需要事先定义对象,也不用声明其类型,=右侧的表达式自动给予变量暂时的类型(弱类型语言可在程序执行过程中动态更改对象类型)。
- 动态入参和默认参数特性,可以看到Python在调用构造函数进行p2, p4, p5, p6等对象的构造时,灵活调用了不同数量和类型的入参,它的背后需要语言具备动态入参和默认参数两种特性:传递则修改,不传递则默认。同时还用具名参数(如代码中的value=1)来突破入参顺序的桎梏,这些特性借鉴了JavaScript,又出于其右了。
- 解析性语言最大的弊端就是IDE不友好,像这些代码的使用,IDE是完全无法提供足够的智能提示,只能依靠一边写代码,一边查阅文档,至少uPyCraft这个开发工具无法提供有效的编程辅助。
以上,是我作为一名Python的初学者,在观察了GPIO相关的MicroPython代码的一些心得体会,后续随着这个项目的深入,还将遇到更多更丰富的Python用法,在这篇文档里我会一一加以阐述并附带一些个人的看法,权当笑耳。
以上代码除了语言层面的理解,还有涉及了一些嵌入式开发的基本知识,比如GPIO的输入输出模式,上下拉设置等,这里不再赘述。通过对这段代码的阅读,我们可以很容易写一个关于扩展板上用户按键的检测逻辑,作为后面音乐播放的开关,类似于这样:
def play():
btn = Pin(3, Pin.IN, Pin.PULL_UP)
lstBtnValue = btn.value()
musicSwitch = False
while True:
btnValue = btn.value()
if lstBtnValue == 1 and btnValue == 0:
musicSwitch = not musicSwitch
lstBtnValue = btnValue
if not musicSwitch:
# 不用播放,空等一个周期
else:
# 按顺序播放音节
在给出上述代码的同时,我们又接触到了Python的几种语法:
- def关键字, Python特立独行地使用了这个关键字来定义函数,主流语言定义函数的时候,大多数都不会浪费关键字,比如int add(int a, int b),当然,Python为了实现弱类型和动态传参,确实需要一个关键字来定义函数。它选择了与javascript中funciton关键字不同的def,以此来提高程序员认知的存储空间,并故作聪明地提高其与javascript的差异性。
- :,Python再一次颠覆了所有其他语言的语法规则,使用:和缩进,而不是{}来形成代码块,只能说其作者天生的反骨,太有个性了,必须与众不同,才能彰显优越。
- and or not: 在if的逻辑判断中,python再次展现出它的特立独行和浑身反骨,使用SQL中and、or、not来表达条件与、或、非,非常佩服,可能作者特别钟情和擅长写SQL语句进行增删改查。
下载或复制。在uPyCraft中编辑和保存后,点击Tools中的Download按钮,便将这个驱动文件传送到了真实MCU中的存储空间了。
到这里其实应该理解整个MicroPython的开发机制了,我们使用一个固件程序刷入了MCU,这个固件程序启用了USB虚拟串口,还启用了一段Flash区域构造了存放python脚本的文件系统,uPyCraft工具即通过这个虚拟串口对MCU进行调试、以及上传下载我们编写的python脚本。在uPyCraft的device中的文件即为MCU Flash中的程序,经过引导,将首先执行其中的boot.py,然后执行main.py,于是我们可以将应用逻辑写入main.py中:
from machine import Pin, SoftI2C
import ssd1306
print("hello world2")
i2c = SoftI2C(scl=Pin(7), sda=Pin(6))
oled_width = 128
oled_height = 64
oled = ssd1306.SSD1306_I2C(oled_width, oled_height, i2c)
str1 = "hello world!"
oled.fill(0)
oled.text(str1, 0, 15)
oled.show()
即可在OLED屏幕上绘制文字hello world!。关于ssd1306.py,还有如下API:
display.poweroff() # power off the display, pixels persist in memory
display.poweron() # power on the display, pixels redrawn
display.contrast(0) # dim
display.contrast(255) # bright
display.invert(1) # display inverted
display.invert(0) # display normal
display.rotate(True) # rotate 180 degrees
display.rotate(False) # rotate 0 degrees
display.show() # write the contents of the FrameBuffer to display memory
display.fill(0) # fill entire screen with colour=0
display.pixel(0, 10) # get pixel at x=0, y=10
display.pixel(0, 10, 1) # set pixel at x=0, y=10 to colour=1
display.hline(0, 8, 4, 1) # draw horizontal line x=0, y=8, width=4, colour=1
display.vline(0, 8, 4, 1) # draw vertical line x=0, y=8, height=4, colour=1
display.line(0, 0, 127, 63, 1) # draw a line from 0,0 to 127,63
display.rect(10, 10, 107, 43, 1) # draw a rectangle outline 10,10 to 117,53, colour=1
display.fill_rect(10, 10, 107, 43, 1) # draw a solid rectangle 10,10 to 117,53, colour=1
display.text('Hello World', 0, 0, 1) # draw some text at x=0, y=0, colour=1
display.scroll(20, 0) # scroll 20 pixels to the right
值得一提的是,这个ssd1302.py基础驱动的功能还是稍显简陋,甚至无法调整英文字体和字号,更不用奢望中文的显示了,跟我之前使用的 库差距甚多,奈何U8G2的作者似乎表示没有移植到MicroPython的计划。
中文显示
儿子还小,给他的生日贺词不太适合全英文的,参考这篇博客,研究了一下如何实现中文的显示。
首先使用 取模软件对“祝丁天晨生日快乐”8个字进行取模,建议使用宋体,比雅黑和其他字体在点阵屏上的表现要好,软件可以实时预览显示效果。注意取模配置如图所示:
软件将逐字生成它们的点阵数据。为了方便后续使用Python的UTF-8字符串直接显示,还需要进行每个字的UTF-8转码,使用UTF8转二进制在线转码工具进行字符转码,转码结果:
UTF-8字符 |
十六进制表达 |
祝 |
0xE7A59D |
丁 |
0xE4B881 |
天 |
0xE5A4A9 |
晨 |
0xE699A8 |
生 |
0xE7949F |
日 |
0xE697A5 |
快 |
0xE5BFAB |
乐 |
0xE4B990 |
最终使用以下代码将中文字符显示在屏幕上:
fonts= {
0xE7A59D:
[0x20,0x13,0x12,0xFA,0x0A,0x12,0x13,0x38,0x54,0x94,0x10,0x11,0x11,0x12,0x14,0x18,
0x00,0xFC,0x04,0x04,0x04,0x04,0xFC,0x90,0x90,0x90,0x90,0x12,0x12,0x12,0x0E,0x00], # "祝"
0xE4B881:
[0x00,0x7F,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x05,0x02,
0x00,0xFC,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00], # "丁"
0xE5A4A9:
[0x00,0x3F,0x01,0x01,0x01,0x01,0xFF,0x01,0x02,0x02,0x04,0x04,0x08,0x10,0x20,0xC0,
0x00,0xF8,0x00,0x00,0x00,0x00,0xFE,0x00,0x80,0x80,0x40,0x40,0x20,0x10,0x08,0x06], # "天"
0xE699A8:
[0x1F,0x10,0x1F,0x10,0x1F,0x00,0x3F,0x20,0x2F,0x20,0x3F,0x29,0x28,0x48,0x4A,0x8C,
0xF0,0x10,0xF0,0x10,0xF0,0x00,0xFC,0x00,0xF8,0x00,0xFE,0x08,0x90,0x60,0x18,0x06], # "晨"
0xE7949F:
[0x01,0x11,0x11,0x11,0x3F,0x21,0x41,0x81,0x01,0x3F,0x01,0x01,0x01,0x01,0xFF,0x00,
0x00,0x00,0x00,0x00,0xFC,0x00,0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,0xFE,0x00], # "生"
0xE697A5:
[0x00,0x1F,0x10,0x10,0x10,0x10,0x10,0x1F,0x10,0x10,0x10,0x10,0x10,0x10,0x1F,0x10,
0x00,0xF0,0x10,0x10,0x10,0x10,0x10,0xF0,0x10,0x10,0x10,0x10,0x10,0x10,0xF0,0x10], # "日"
0xE5BFAB:
[0x10,0x10,0x10,0x13,0x18,0x54,0x50,0x50,0x97,0x10,0x10,0x10,0x11,0x11,0x12,0x14,
0x40,0x40,0x40,0xF8,0x48,0x48,0x48,0x48,0xFE,0x40,0xA0,0xA0,0x10,0x10,0x08,0x06], #"快"
0xE4B990:
[0x00,0x00,0x1F,0x10,0x11,0x21,0x21,0x3F,0x01,0x09,0x09,0x11,0x21,0x41,0x05,0x02,
0x20,0xF0,0x00,0x00,0x00,0x00,0x00,0xFC,0x00,0x20,0x10,0x08,0x04,0x04,0x00,0x00], #"乐"
}
#汉字显示遍历
def chinese(ch_str, x_axis, y_axis):
offset_ = 0
for k in ch_str:
code = 0x00 # 将中文转成16进制编码
data_code = k.encode("utf-8")
code |= data_code[0] << 16
code |= data_code[1] << 8
code |= data_code[2]
byte_data = fonts[code]
for y in range(0, 16):
a_ = bin(byte_data[y]).replace('0b', '')
while len(a_) < 8:
a_ = '0'+ a_
b_ = bin(byte_data[y+16]).replace('0b', '')
while len(b_) < 8:
b_ = '0'+ b_
for x in range(0, 8):
oled.pixel(x_axis + offset_ + x, y+y_axis, int(a_[x]))
oled.pixel(x_axis + offset_ + x +8, y+y_axis, int(b_[x]))
offset_ += 16
chinese('祝丁天晨生日快乐', 0, 10)
这段代码中我们第一次接触到了Python的循环写法:
- for循环的语法中规中矩,可见javascript的影子,配合in关键字可对集合数据进行迭代,配合range关键字可对范围数据迭代。值得一提的是,无论javascript还是Golang,都在使用range这类关键字扩展for循环的能力的同时,对标准C系的for init; condition; action{}语法结构实现兼容,而Python拒绝兼容这种最基础的循环写法,反骨的风格还是很浓的。
PWM
根据任务书,下面一个学习的内容是驱动扩展板上的蜂鸣器,我们使用硬件PWM来实现音乐的播放。
首先,根据MicroPython-PWM官方文档我们熟悉一下最基本的PWM用法:
from machine import Pin, PWM
pwm0 = PWM(Pin(0), freq=5000, duty_u16=32768) # create PWM object from a pin
freq = pwm0.freq() # get current frequency
pwm0.freq(1000) # set PWM frequency from 1Hz to 40MHz
duty = pwm0.duty() # get current duty cycle, range 0-1023 (default 512, 50%)
pwm0.duty(256) # set duty cycle from 0 to 1023 as a ratio duty/1023, (now 25%)
duty_u16 = pwm0.duty_u16() # get current duty cycle, range 0-65535
pwm0.duty_u16(2**16*3//4) # set duty cycle from 0 to 65535 as a ratio duty_u16/65535, (now 75%)
duty_ns = pwm0.duty_ns() # get current pulse width in ns
pwm0.duty_ns(250_000) # set pulse width in nanoseconds from 0 to 1_000_000_000/freq, (now 25%)
pwm0.deinit() # turn off PWM on the pin
pwm2 = PWM(Pin(2), freq=20000, duty=512) # create and configure in one go
print(pwm2) # view PWM settings
有了之前Python语言的基础,看这段示例应该不会太困难,但还是出现了2个关于运算符的知识点:
- **运算符:Python另辟蹊径,采用了Ruby和Perl中指数运算的语法,而没有使用更为常用的^运算符,导致在阅读2**16*3//4这行表达式时,一度怀疑人生。怎么说呢,Python的作者在圣诞夜创作这门语言时,一定无时无刻地在提醒自己,要雨露均沾地向其他所有语言致敬,佩服。
- //运算符:如果说**指数运算符是Python作者在向Ruby和Perl两门语言致敬,那么//表示整除可以算是Python独一无二,开天辟地,醍醐灌顶地首创了。其他语言整数/整数还等于整数,到了Python这里,全球首创,整数/整数=浮点,整数//整数才能等于整数,再次佩服得五体投地。
2023-12-17 21:32 上传
在这份简谱里,我们看到所有的音符都是由不加点的中音和几个头上带点的高音组成的,因此需要在程序中用到上表格中的中音和高音频率。
节拍
对于这份简谱,我们还需要了解节拍的概念,乐谱的顶部有这样的符号:$1=C\frac{3}{4}$,表示这首歌是C大调,4分音符为一拍,每小节有3拍。我们大可不用管它具体的含义,只需要了解节拍是代表每个音持续长短的含义,节拍是一个相对值,可以动态定义1拍等于1s,也可以定义1拍等于500ms,但是乐谱中每个音符的持续时间的比例是约定好的,比如这首歌第1小节:0 0 5 5, 前面两个0各占一拍,后面两个5,利用下划线表示每个音符半拍,所以这一小节共计3拍,可以看到这份简谱中每一小节都是3拍。
连音符
这份乐谱的第3行第1段,在7和6的头顶上有一个连音符,代表7转向6时没有停顿,直接转音的。这同时意味着,除此以外的音符和音符之间,都是有间隙的,同样,间隙的长短需要我们程序里进行动态设置,反复尝试以求得一个比较好的播放效果。
同音符
乐谱中还出现了一些-符号,这代表延续上一个音符,一般来说是延续一半的音长,但我们可以不用理会,直接延长同等长度即可。
综上,理解一份乐谱,我们对每一个音符需要建立音调、音长、间隙的三个概念,抽象为数据结构后,我们可以很容易编写以下代码,实现整首歌曲的播放:
from micropython import const
import time
from machine import Pin, PWM
MUSIC_M1 = const(523)
MUSIC_M2 = const(587)
MUSIC_M3 = const(659)
MUSIC_M4 = const(698)
MUSIC_M5 = const(784)
MUSIC_M6 = const(880)
MUSIC_M7 = const(988)
MUSIC_H1 = const(1046)
MUSIC_H2 = const(1175)
MUSIC_H3 = const(1318)
MUSIC_H4 = const(1397)
MUSIC_H5 = const(1568)
MUSIC_H6 = const(1760)
MUSIC_H7 = const(1976)
MUSIC_P = 600 # 自由定义 1拍 = 600ms
MUSIC_DELAY = 50 # 自由定义,音符之间的间隙50ms
music = [ # 三维数组,分别为音调、音长、跟下一个音符之间是否存在间隙
[MUSIC_M5, 0.5 * MUSIC_P, 1],
[MUSIC_M5, 0.5 * MUSIC_P, 1],
[MUSIC_M6, MUSIC_P, 1],
[MUSIC_M5, MUSIC_P, 1],
[MUSIC_H1, MUSIC_P, 1],
[MUSIC_M7, 2 * MUSIC_P, 1],
[MUSIC_M5, 0.5 * MUSIC_P, 1],
[MUSIC_M5, 0.5 * MUSIC_P, 1],
[MUSIC_M6, MUSIC_P, 1],
[MUSIC_M5, MUSIC_P, 1],
[MUSIC_H2, MUSIC_P, 1],
[MUSIC_H1, 2 * MUSIC_P, 1],
[MUSIC_M5, 0.5 * MUSIC_P, 1],
[MUSIC_M5, 0.5 * MUSIC_P, 1],
[MUSIC_H5, MUSIC_P, 1],
[MUSIC_H3, MUSIC_P, 1],
[MUSIC_H1, MUSIC_P, 1],
[MUSIC_M7, MUSIC_P, 0],
[MUSIC_M6, MUSIC_P, 1],
[MUSIC_H4, 0.5 * MUSIC_P, 1],
[MUSIC_H4, 0.5 * MUSIC_P, 1],
[MUSIC_H3, MUSIC_P, 1],
[MUSIC_H1, MUSIC_P, 1],
[MUSIC_H2, MUSIC_P, 1],
[MUSIC_H1, 3 * MUSIC_P, 1],
]
def play():
buzzer_pin = Pin(5, Pin.OUT)
buzzer = PWM(buzzer_pin)
btn = Pin(3, Pin.IN, Pin.PULL_UP)
lstBtnValue = btn.value()
musicSwitch = False
i = 0
while True:
btnValue = btn.value()
if lstBtnValue == 1 and btnValue == 0:
musicSwitch = not musicSwitch
lstBtnValue = btnValue
if not musicSwitch:
i = 0
buzzer.duty(0)
else:
buzzer.freq(music[i][0])
buzzer.duty(800)
time.sleep_ms(int(music[i][1]))
if music[i][2] == 1:
buzzer.duty(0)
i += 1
if i == len(music) - 1:
i = 0
time.sleep_ms(MUSIC_DELAY)
这份代码,首先使用常量定义了各音调的频率表格,然后利用数据结构定义了乐谱中每一个音符的数据信息,接着初始化了相关外设,最后在循环中依次播放每个音符,加入btn作为音乐播放开关控制。随着我们对Python语言各项语法和关键字的熟悉,这份代码也没有什么新的知识点出现了。
WIFI
ESP32最让人着迷的功能肯定是其强大的无线通信能力,使用MicroPython固件可以非常方便地调用网络库进行WIFI的连接,并执行更多丰富的网络动作,比如HTTP请求,MQTT推送和订阅等。查阅MicroPython-Networking官方文档可以看到以下示例代码:
import network
wlan = network.WLAN(network.STA_IF) # create station interface
wlan.active(True) # activate the interface
wlan.scan() # scan for access points
wlan.isconnected() # check if the station is connected to an AP
wlan.connect('ssid', 'key') # connect to an AP
wlan.config('mac') # get the interface's MAC address
wlan.ifconfig() # get the interface's IP/netmask/gw/DNS addresses
ap = network.WLAN(network.AP_IF) # create access-point interface
ap.config(ssid='ESP-AP') # set the SSID of the access point
ap.config(max_clients=10) # set how many clients can connect to the network
ap.active(True) # activate the interface
上面的代码只有基本的WIFI连接和扫描等动作,连接网络后,我们需要进行HTTP请求,此时需要用到urequests库。有些奇怪的是,MicroPython官网并没有提及任何关于urequests包的内容,但是在XIAO开发板入门教程-WIFI以及EEWORLD第三期视频教程都不约而同地使用到这个库,我查了一下,urequests这个库目前在PyPi中处于第三方库的形式提供下载: https://pypi.org/project/micropython-urequests/ 。或许是MicroPython在文档中并未提及该库的情况下,其发布的固件库中包含了该第三方库,个中原因无从得知。
WIFI部分我设计的逻辑是:
- 通过网络http请求获得当前时间
- 通过解析json获得响应报文中的准确时间戳
- 使用ESP32硬件定时器每秒递增这个时间戳
- 通过本地时间转化进一步获得北京东八区的时间
- 定期将本地时间日期绘制到OLED屏幕的指定位置
在实现以上逻辑的过程中,需要解决以下问题:
网络请求
之前提到,使用urequests库的urequests.get(url)方法可以方便地进行HTTP请求。通过搜索引擎搜索,得知苏宁提供一个稳定免费的授时服务: https://f.m.suning.com/api/ct.do ,返回格式是json,解析其中的currentTime字段即可获得UTC标准时间戳。
JSON解析
MicroPython内置了json支持,参考文档:MicroPython的JSON模块,这里再次表示对urequests库的疑惑,json库在MicroPython的文档中都有专门章节介绍到,而urequests库却只字未提。
时间转化
通过上述请求,我们可以获得由苏宁授时的国际UTC标准时间戳(毫秒级),这个时间戳是不含时区信息的,换言之就是全世界都使用这个时间戳作为国际时间,因此我们要在获得北京时间的话,还得补上东八区的8个小时。同时,为了适配micropython中的time包进行本地时间转化,我们还得将毫秒级的时间戳首先整除1000,获得秒级的UTC时间戳。
MicroPython的time包提供了时间日期转化函数localtime和gmtime,参见文档:MicroPython的time模块,文档中还有这么一段说明:
Time Epoch: Unix port uses standard for POSIX systems epoch of 1970-01-01 00:00:00 UTC. However, some embedded ports use epoch of 2000-01-01 00:00:00 UTC. Epoch year may be determined with gmtime(0)[0].
这句话是说,对于localtime函数,国际上普遍使用1970年1月1日零点作为时间的初始数据,但有些嵌入式设备使用的是从2000年1月1日起开始计算时间,为了避免误解,建议使用gmtime函数,直接获得纪元年份。
硬件定时器
参考MicroPython-Timer定时器官方文档, 可观察到使用Python调用硬件定时器的基础用法:
from machine import Timer
tim0 = Timer(0)
tim0.init(period=5000, mode=Timer.ONE_SHOT, callback=lambda t:print(0))
tim1 = Timer(1)
tim1.init(period=2000, mode=Timer.PERIODIC, callback=lambda t:print(1))
这里首次出现了Python一个非常重要的知识点:
- lambda表达式:这是一种匿名函数的传递,现在的高级语言几乎都以“函数是一等公民”为设计准则,让函数可以入参传播。其实早在C语言时期,就可以通过函数指针随意传递函数,这些高级语言也真没有必要引以为傲。Python这次选用了lambda这个关键字来表达匿名函数的传递,是足够让我震撼的,虽然此时我已经被它的反骨和特立独行搞麻木了,但我还是怀疑作者是不是生怕别人不知道他懂lambda。一种匿名函数的传递而已,叫它闭包也好,lambda表达式也好,箭头函数不够用吗,非得拽词增加一个关键字。
, 将该文件通过uPyCraft下载到设备中,接着编写以下测试代码即可
from machine import Pin, SoftI2C
i2c = SoftI2C(scl=Pin(7), sda=Pin(6))
aht20 = AHT20(i2c)
try:
t = aht20.temperature
h = aht20.relative_humidity
except:
print("aht20 read error")
else:
print(f"t: {t:0.1f}")
print(f"h: {h:h%}")
观察上述代码,我们表面上是调用了aht20.temperature是一个变量,但是我们细想就会得知,除非aht20模块内部维护了一个周期性的线程,在不停地更新其内部temperature变量(即使这样,在访问和更新时还要加锁保护),否则temperature变量将永不会改变。这里就遇到Python中一个鸡肋的语法糖,叫做 装饰器。
- 装饰器:其设计思想可能来自于JAVA Bean的Getter/Setter,又有点像来自于Vue中的数据绑定。总之就是让你看上去访问的是一个变量,其实访问的是一个函数。形式上,让用户调用时多加一个括号转成显式函数调用就行了,纯纯的语法糖。
观察下面代码,可以更加方便理解Pyton中这类用法:
class Student(object):
@property
def birth(self):
return self._birth
@birth.setter
def birth(self, value):
self._birth = value
@property
def age(self):
return 2015 - self._birth
光线传感器
XIAO开发板附带的光线传感器模块,使用的是一颗光电二极管和一个运放构成的模拟量电路,因此可以通过MicroPython的ADC外设轻松获取模拟量值,从而评估环境光强弱。
参考MicroPython-ADC官方文档,有以下示例:
from machine import ADC
adc = ADC(pin) # create an ADC object acting on a pin
val = adc.read_u16() # read a raw analog value in the range 0-65535
val = adc.read_uv() # read an analog value in microvolts
根据原理图,参考上述示例,编写以下代码,以获取环境光数值:
from machine import ADC
import time
# init ADC
adc = ADC(2, atten=ADC.ATTN_11DB)
while Ture:
light = adc.read()
print(light)
time.sleep(1)
由暗到亮调节周围环境光的亮度,可以观察模拟量采集到的数据差不多在0 - 5000左右,达到2000以上时环境光的亮度已经相当高了。
综合实验
综合实验部分,我打算利用上述所学的所有内容,综合给儿子设计一个电子贺卡,基本需求如下:
- 上电后使用网络通信获取当前时间
- 启动硬件定时器作为后续的计时工具
- 屏幕左上方显示当前温度
- 屏幕右上方显示时分秒
- 屏幕中间部分显示生日贺词
- 屏幕右下方显示日期
- 按下用户按键开始播放生日快乐歌,再次按下停止
- 当感应到周围环境光变弱时(关灯吹蜡烛时),将生日贺词进行走马灯滚动
为了完成以上业务逻辑,靠单个while True似乎有些困难,于是开始研究MicroPython的多线程
Thread
熟悉RTOS的朋友面对以上业务逻辑时,从外设资源的区分角度,一定希望能将系统拆分成两个线程,一个负责显示,一个负责音乐播放。于是开始探索Python的多线程编程,在MicroPython多线程官方文档中,表示目前MicroPython可以高度实验性提供_thread底层线程支持。值得注意的是,在标准Python环境下,一般使用threading包来处理多线程任务,而_thread是Python底层非面向用户的包;而在MicroPython中,暂时只能使用_thread包直接使用。
继而查询_thread的使用方法,在Python-thread官方文档,中,有关于_thread库的使用文档,主要是_thread.start_new_thread(_function_, _args_[, _kwargs_])函数,初始化并启动一个线程。
2023-12-17 21:36 上传
最后还有一个小小的Python语法知识:
- Tuple:中文翻译成元组,Python中用两个圆括号包住多个由逗号分隔的数据,称为tuple,它们形成了一个有序、不可改变的数据集合。“不可改变的数据集合”这种数据类型在其他语言里是没有的,Python属于典型地“无中生有一个痛点并解决这个痛点”。在上述代码中,线程启动函数start_new_thread第二个参数的类型就是tuple,将线程函数所要的参数传递进去,(timeUTC)这种单个元素的tuple会被Python自动降级为单数据,所以如果不加逗号,是会报入参类型错误的,必须写成(timeUTC,)这种奇丑无比的写法。
2023-12-17 21:38 上传
作业源代码:https://download.eeworld.com.cn/detail/%E5%A4%A7%E6%B3%A2%E4%B8%81/630300
|