【得捷Follow me第3期】使用Xiao开发板给他过一个难忘的生日
本帖最后由 大波丁 于 2023-12-17 21:58 编辑<p>有幸入围本次的【<a href="https://www.eeworld.com.cn/huodong/digikey_follow_me/">Follow Me第三期</a>】活动,从<a href="https://www.digikey.cn/">得捷DigiKey</a>采购了以下物料:</p>
<table>
<thead>
<tr>
<th>物料</th>
<th>介绍</th>
<th>链接</th>
</tr>
</thead>
<tbody>
<tr>
<td>XiaoESP32C3核心板</td>
<td>深圳SeedStudio(矽递)公司设计开发的一款小尺寸ESP32C3核心开发板</td>
<td><a href="https://www.digikey.cn/zh/products/detail/seeed-technology-co-ltd/113991054/16652880">购买链接</a></td>
</tr>
<tr>
<td>Xiao扩展板</td>
<td>可适配XiaoESP32C3的扩展版,含0.96寸OLED屏幕、蜂鸣器、SD卡槽、RTC芯片等外设和接口</td>
<td><a href="https://www.digikey.cn/zh/products/detail/seeed-technology-co-ltd/103030356/13572081">购买链接</a></td>
</tr>
<tr>
<td>XiaoAHT20模块</td>
<td>适配Xiao扩展板IIC接口端子的温湿度传感器</td>
<td><a href="https://www.digikey.cn/zh/products/detail/seeed-technology-co-ltd/101990644/11681294">购买链接</a></td>
</tr>
<tr>
<td>Xiao光电模块</td>
<td>适配Xiao扩展板模拟量接口端子的环境光传感器,光电二极管加运放</td>
<td><a href="https://www.digikey.cn/zh/products/detail/seeed-technology-co-ltd/101020132/6558656">购买链接</a></td>
</tr>
</tbody>
</table>
<p>任务的基本要求为采用MicroPython的方式对ESP32C3进行开发,并且使用到板载的各项外设,包括屏幕、蜂鸣器、WIFI、温湿度传感器、光线传感器等;任务的拓展要求为综合实践,使用以上外设,选择或者自行设计一个综合性的应用加以实现。</p>
<p>着手完成任务的时候离活动截止时间已经不多了,正值儿子过生日,就想着给他设计一个电子生日贺卡,在他幼小的心中埋下些关于电子、编程等理工科的种子,或许将来也会如我一般对这些产生好奇和热爱。</p>
<h1 id="h1-u6F14u793Au89C6u9891"><a name="演示视频"></a>演示视频</h1>
<p>本次任务采用迭代形式开发,以至于完成到最后一项综合实践环节时,前面各单元模块的独立视频以无法呈现。只能在视频里针对这些单独模块的使用简单介绍一下,MCU上电后会连接指定WIFI,通过授时服务器的HTTP接口获取当前时间,采用JSON解析,针对东八区的时区自行补8个小时计算本地时间,利用所得时间的初始值创建硬件定时器继续走时;接着使用MicroPython的线程能力创建了<strong>两个线程</strong>,第一个线程负责显示,第二个线程负责音乐播放:<strong>显示线程</strong>会综合时间日期、AHT20采集到的温度在OLED上绘制各项数值,同时在屏幕主体部分用中文显示生日贺词。还留下一个彩蛋,当检测到环境光数值不足500时,会将生日贺词使用跑马灯的效果滚动起来,想象着儿子关上灯吹蜡烛时应该能够发现这个彩蛋。<strong>音乐播放线程</strong>会检测扩展版上的用户按键,但按下后会切换音乐播放的开关,以播放《生日快乐歌》。</p>
<p> </p>
<p><iframe allowfullscreen="true" frameborder="0" height="450" src="https://training.eeworld.com.cn/shareOpenCourseAPI?isauto=true&lessonid=38685" style="background:#eee;margin-bottom:10px;" width="700"></iframe><br />
</p>
<h1 id="h1-u9879u76EEu603Bu7ED3u548Cu7ECFu9A8C"><a name="项目总结和经验"></a>项目总结和经验</h1>
<h2 id="h2-u8FDEu63A5u793Au610Fu56FE"><a name="连接示意图"></a>连接示意图</h2>
<p>先来总览一下这次XiaoESP32核心板和扩展板的连接示意图,网上有核心板的原理图,但扩展板的原理图一直没找到,只能找到以下连接示意图:</p>
<div style="text-align: center;"></div>
<p> </p>
<div style="text-align: center;"></div>
<h2 id="h2-u73AFu5883u642Du5EFA"><a name="环境搭建"></a>环境搭建</h2>
<p>这次是我第一次使用MicroPython进行ESP32的开发,之前一直使用C语言在CLion下进行ESP-IDF的开发,相比而言,MicroPython的开发环境确实搭建起来非常简单:</p>
<h3 id="h3-u4E0Bu8F7D"><a name="下载"></a>下载</h3>
<p>整个环境搭建只要下载以下两个内容,一个是固件库,一个是esptool命令行工具:</p>
<ul>
<li>MicroPython固件库:<a href="https://micropython.org/download/ESP32_GENERIC_C3/">https://micropython.org/download/ESP32_GENERIC_C3</a></li>
<li>esptool:<a href="https://github.com/espressif/esptool/releases">https://github.com/espressif/esptool/releases</a></li>
</ul>
<h3 id="h3-u70E7u5F55"><a name="烧录"></a>烧录</h3>
<p>使用TypeC连接核心板和电脑,正常情况下计算机的设备管理器中应该能观察到一个新的串口设备,这是出厂固件所提供的,但是我在电脑上却一直没能观察到串口设备。后来辗转了多次,才发现使用了一根没有通讯能力的TypeC线(用这根线连接电脑和安卓手机也只能充电,无法传输数据),换了成了原装的TypeC线,顺利在电脑上观察到了新的串口了。</p>
<p>有了串口端口号,即可通过esptool工具将ESP32C3上最新的MicroPython固件烧录进去,执行脚本为:</p>
<pre>
</pre>
<ol>
<li>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</li>
</ol>
<p>上述脚本参数中,COM5是我的串口端口号,ESP32_GENERIC_C3-20231005-v1.21.0.bin是下载回来的最新的固件库,将它放到esptool同一路径即可。</p>
<p>命令执行需要一段时间,直到命令行提示硬件已成功复位,完成烧录。</p>
<h3 id="h3-upycraft"><a name="uPyCraft"></a>uPyCraft</h3>
<p>根据本次活动的直播课程,使用uPyCraft这款工具进行ESP32C3的MicroPython开发,这是个非常轻量级的小工具,可阅读<a href="https://randomnerdtutorials.com/install-upycraft-ide-windows-pc-instructions/">这篇文章</a>进行下载和安装。安装结束后,打开后的画面是这样的:</p>
<div style="text-align: center;"></div>
<p>操作步骤如下:</p>
<ol>
<li>打开uPyCraft</li>
<li>Tools中选择正确的串口</li>
<li>workSpace选定本地工作目录</li>
<li>新建py脚本,比如hello.py</li>
<li>File中刷新工作区</li>
<li>右边的烧录,观察串口输出</li>
<li>microPython依次执行device中的boot.py和main.py</li>
<li>代码中如果出现while True:这类死循环,需要RESET后观察,如果此时串口介入,会STOP程序</li>
<li>开发过程中如果发生任何异常,可按下扩展板上的RESET尝试解决</li>
</ol>
<p>接下来,即可阅读MicroPython的<a href="https://docs.micropython.org/en/latest/index.html">官方手册</a>,踏上ESP32C3的MicroPython之旅了。</p>
<h2 id="h2-gpio"><a name="GPIO"></a>GPIO</h2>
<p>任何MCU的起点都是点灯,也就是GPIO的控制,根据<a href="https://docs.micropython.org/en/latest/esp32/quickref.html#pins-and-gpio">Pins and GPIO</a>的介绍,我们来看看python是如何控制ESP32的GPIO的:</p>
<pre>
<code class="language-python">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</code></pre>
<p>因为我平时不用Python,再加上一直以来我对Python极自由主义(反骨)的语法设计持保留看法,所以这次我这次几乎是面对一个新的语言,相信也会有一些跟我一样的python初学者,我们来利用这次使用MicropPython的机会全新地理解一下Python。</p>
<ul>
<li>from, import 关键字,引入库,machine是个库,Pin是这个库中的某个类。可以引入整个库比如这里可以写import machine,那么下文就可以用machine.Pin来使用Pin类。也可以只引入库中某个类(或函数),比如from machine import Pin,那么下文可以直接使用Pin类。整体上from和import两个关键字的设计,Python采用了与JavaScript类似的方式,一切看上去非常和谐。</li>
<li>初始化对象,p0 = Pin(0, Pin.OUT),这种初始化类对象的方式,调用了Pin类的构造函数,其设计思想来自于C++。Python是弱类型语言,定义变量时并不需要事先定义对象,也不用声明其类型,=右侧的表达式自动给予变量暂时的类型(弱类型语言可在程序执行过程中动态更改对象类型)。</li>
<li>动态入参和默认参数特性,可以看到Python在调用构造函数进行p2, p4, p5, p6等对象的构造时,灵活调用了不同数量和类型的入参,它的背后需要语言具备动态入参和默认参数两种特性:<strong>传递则修改,不传递则默认</strong>。同时还用具名参数(如代码中的value=1)来突破入参顺序的桎梏,这些特性借鉴了JavaScript,又出于其右了。</li>
<li>解析性语言最大的弊端就是IDE不友好,像这些代码的使用,IDE是完全无法提供足够的智能提示,只能依靠一边写代码,一边查阅文档,至少uPyCraft这个开发工具无法提供有效的编程辅助。</li>
</ul>
<p>以上,是我作为一名Python的初学者,在观察了GPIO相关的MicroPython代码的一些心得体会,后续随着这个项目的深入,还将遇到更多更丰富的Python用法,在这篇文档里我会一一加以阐述并附带一些个人的看法,权当笑耳。</p>
<p>以上代码除了语言层面的理解,还有涉及了一些嵌入式开发的基本知识,比如GPIO的输入输出模式,上下拉设置等,这里不再赘述。通过对这段代码的阅读,我们可以很容易写一个关于扩展板上用户按键的检测逻辑,作为后面音乐播放的开关,类似于这样:</p>
<pre>
<code class="language-python">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:
# 按顺序播放音节</code></pre>
<p>在给出上述代码的同时,我们又接触到了Python的几种语法:</p>
<ul>
<li>def关键字, Python<strong>特立独行</strong>地使用了这个关键字来定义函数,主流语言定义函数的时候,大多数都不会浪费关键字,比如int add(int a, int b),当然,Python为了实现弱类型和动态传参,确实需要一个关键字来定义函数。它选择了与javascript中funciton关键字不同的def,以此来提高程序员认知的存储空间,并故作聪明地提高其与javascript的差异性。</li>
<li>:,Python再一次颠覆了所有其他语言的语法规则,使用:和缩进,而不是{}来形成代码块,只能说其作者天生的反骨,太有个性了,必须与众不同,才能彰显优越。</li>
<li>and or not: 在if的逻辑判断中,python再次展现出它的特立独行和浑身反骨,使用SQL中and、or、not来表达条件与、或、非,非常佩服,可能作者特别钟情和擅长写SQL语句进行增删改查。</li>
</ul>
<h2 id="h2-oled"><a name="OLED"></a>OLED</h2>
<p>在熟悉了MicroPython控制ESP32C3的GPIO后,接下来我们开始学习如何驱动扩展板上的0.96寸128x64的OLED屏幕。这块屏幕对我来说非常熟悉,我们公司有几款量产产品上确实在用这种屏幕,由中景园供货,尺寸、像素、驱动都与这块扩展板上的屏幕一致。只不过我们公司一般用SPI与SSD1306驱动芯片进行通信,而这个扩展板上使用的I2C搭建的电路。</p>
<p>得益于Python几乎疯狂的生态系统,我们可以轻松找到官方为我们提供的基于MicroPython适配ESP32的SSD1306驱动库,可以从点击<a href="https://github.com/IcingTomato/micropython_xiao_esp32c3/blob/master/drivers/display/ssd1306.py">ssd1306.py</a>下载或复制。在uPyCraft中编辑和保存后,点击Tools中的Download按钮,便将这个驱动文件传送到了真实MCU中的存储空间了。</p>
<div style="text-align: center;"></div>
<p>到这里其实应该理解整个MicroPython的开发机制了,我们使用一个固件程序刷入了MCU,这个固件程序启用了USB虚拟串口,还启用了一段Flash区域构造了存放python脚本的文件系统,uPyCraft工具即通过这个虚拟串口对MCU进行调试、以及上传下载我们编写的python脚本。在uPyCraft的device中的文件即为MCU Flash中的程序,经过引导,将首先执行其中的boot.py,然后执行main.py,于是我们可以将应用逻辑写入main.py中:</p>
<pre>
<code class="language-python">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()</code></pre>
<p>即可在OLED屏幕上绘制文字hello world!。关于ssd1306.py,还有如下API:</p>
<pre>
<code class="language-python">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</code></pre>
<p>值得一提的是,这个ssd1302.py基础驱动的功能还是稍显简陋,甚至无法调整英文字体和字号,更不用奢望中文的显示了,跟我之前使用的<a href="https://github.com/olikraus/u8g2">U8G2</a>库差距甚多,奈何U8G2的作者似乎表示没有移植到MicroPython的计划。</p>
<h3 id="h3-u4E2Du6587u663Eu793A"><a name="中文显示"></a>中文显示</h3>
<p>儿子还小,给他的生日贺词不太适合全英文的,参考<a href="https://blog.csdn.net/weixin_42880082/article/details/126540660">这篇博客</a>,研究了一下如何实现中文的显示。</p>
<p>首先使用<a href="https://github.com/fishjump/PCtoLCD2002_exe">PCtoLCD2002</a>取模软件对“祝丁天晨生日快乐”8个字进行取模,建议使用宋体,比雅黑和其他字体在点阵屏上的表现要好,软件可以实时预览显示效果。注意取模配置如图所示:</p>
<div style="text-align: center;"></div>
<p>软件将逐字生成它们的点阵数据。为了方便后续使用Python的UTF-8字符串直接显示,还需要进行每个字的UTF-8转码,使用<a href="http://www.mytju.com/classcode/tools/encode_utf8.asp">UTF8转二进制</a>在线转码工具进行字符转码,转码结果:</p>
<table>
<thead>
<tr>
<th>UTF-8字符</th>
<th>十六进制表达</th>
</tr>
</thead>
<tbody>
<tr>
<td>祝</td>
<td>0xE7A59D</td>
</tr>
<tr>
<td>丁</td>
<td>0xE4B881</td>
</tr>
<tr>
<td>天</td>
<td>0xE5A4A9</td>
</tr>
<tr>
<td>晨</td>
<td>0xE699A8</td>
</tr>
<tr>
<td>生</td>
<td>0xE7949F</td>
</tr>
<tr>
<td>日</td>
<td>0xE697A5</td>
</tr>
<tr>
<td>快</td>
<td>0xE5BFAB</td>
</tr>
<tr>
<td>乐</td>
<td>0xE4B990</td>
</tr>
</tbody>
</table>
<p>最终使用以下代码将中文字符显示在屏幕上:</p>
<p></p>
<pre>
<code class="language-python">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 << 16
code |= data_code << 8
code |= data_code
byte_data = fonts
for y in range(0, 16):
a_ = bin(byte_data).replace('0b', '')
while len(a_) < 8:
a_ = '0'+ a_
b_ = bin(byte_data).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_))
oled.pixel(x_axis + offset_ + x +8, y+y_axis, int(b_))
offset_ += 16
chinese('祝丁天晨生日快乐', 0, 10)</code></pre>
<div style="text-align: center;"> </div>
<p>这段代码中我们第一次接触到了Python的循环写法:</p>
<ul>
<li>for循环的语法中规中矩,可见javascript的影子,配合in关键字可对集合数据进行迭代,配合range关键字可对范围数据迭代。值得一提的是,无论javascript还是Golang,都在使用range这类关键字扩展for循环的能力的同时,对标准C系的for init; condition; action{}语法结构实现兼容,而Python拒绝兼容这种最基础的循环写法,反骨的风格还是很浓的。</li>
</ul>
<h2 id="h2-pwm"><a name="PWM"></a>PWM</h2>
<p>根据任务书,下面一个学习的内容是驱动扩展板上的蜂鸣器,我们使用硬件PWM来实现音乐的播放。</p>
<p>首先,根据<a href="https://docs.micropython.org/en/latest/esp32/quickref.html#pwm-pulse-width-modulation">MicroPython-PWM官方文档</a>我们熟悉一下最基本的PWM用法:</p>
<pre>
<code class="language-python">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</code></pre>
<p>有了之前Python语言的基础,看这段示例应该不会太困难,但还是出现了2个关于运算符的知识点:</p>
<ul>
<li>**运算符:Python另辟蹊径,采用了Ruby和Perl中指数运算的语法,而没有使用更为常用的^运算符,导致在阅读2**16*3//4这行表达式时,一度怀疑人生。怎么说呢,Python的作者在圣诞夜创作这门语言时,一定无时无刻地在提醒自己,要<strong>雨露均沾</strong>地向其他所有语言致敬,佩服。</li>
<li>//运算符:如果说**指数运算符是Python作者在向Ruby和Perl两门语言致敬,那么//表示整除可以算是Python独一无二,开天辟地,醍醐灌顶地首创了。其他语言整数/整数还等于整数,到了Python这里,全球首创,整数/整数=浮点,整数//整数才能等于整数,再次佩服得五体投地。</li>
</ul>
<h3 id="h3-u97F3u4E50u57FAu7840"><a name="音乐基础"></a>音乐基础</h3>
<h4 id="h4-u97F3u8C03u548Cu97F3u91CF"><a name="音调和音量"></a>音调和音量</h4>
<p>众所周知,声音是由震动产生的波,利用空气作为介质传播到人耳的。振动的频率决定了音调的高低,震动的振幅决定了音量的大小,因此PWM脉冲波作用于蜂鸣器后,就可以通过PWM的频率和占空比分别控制声音的音调和大小。当然,手上这块扩展板蜂鸣器的功率有限,即使占空比调整到很高其声音也非常微弱,需要离近了才能勉强听见。</p>
<p>通过搜索引擎搜索,查找到每种音调对应的频率:</p>
<table>
<thead>
<tr>
<th>音调</th>
<th>Hz</th>
<th>音调</th>
<th>Hz</th>
<th>音调</th>
<th>Hz</th>
</tr>
</thead>
<tbody>
<tr>
<td>低音1</td>
<td>262</td>
<td>中音1</td>
<td>523</td>
<td>高音1</td>
<td>1046</td>
</tr>
<tr>
<td>低音1#</td>
<td>277</td>
<td>中音1#</td>
<td>554</td>
<td>高音1#</td>
<td>1109</td>
</tr>
<tr>
<td>低音2</td>
<td>294</td>
<td>中音2</td>
<td>587</td>
<td>高音2</td>
<td>1175</td>
</tr>
<tr>
<td>低音2#</td>
<td>311</td>
<td>中音2#</td>
<td>622</td>
<td>高音2#</td>
<td>1245</td>
</tr>
<tr>
<td>低音3</td>
<td>330</td>
<td>中音3</td>
<td>659</td>
<td>高音3</td>
<td>1318</td>
</tr>
<tr>
<td>低音4</td>
<td>349</td>
<td>中音4</td>
<td>698</td>
<td>高音4</td>
<td>1397</td>
</tr>
<tr>
<td>低音4#</td>
<td>370</td>
<td>中音4#</td>
<td>740</td>
<td>高音4#</td>
<td>1480</td>
</tr>
<tr>
<td>低音5</td>
<td>392</td>
<td>中音5</td>
<td>784</td>
<td>高音5</td>
<td>1568</td>
</tr>
<tr>
<td>低音5#</td>
<td>415</td>
<td>中音5#</td>
<td>831</td>
<td>高音5#</td>
<td>1661</td>
</tr>
<tr>
<td>低音6</td>
<td>440</td>
<td>中音6</td>
<td>880</td>
<td>高音6</td>
<td>1760</td>
</tr>
<tr>
<td>低音6#</td>
<td>466</td>
<td>中音6#</td>
<td>932</td>
<td>高音6#</td>
<td>1865</td>
</tr>
<tr>
<td>低音7</td>
<td>494</td>
<td>中音7</td>
<td>988</td>
<td>高音7</td>
<td>1976</td>
</tr>
</tbody>
</table>
<p> </p>
<p>查找《Happy Birthday》这首歌的简谱:</p>
<p> </p>
<div style="text-align: center;"></div>
<p> </p>
<p>在这份简谱里,我们看到所有的音符都是由不加点的中音和几个头上带点的高音组成的,因此需要在程序中用到上表格中的中音和高音频率。</p>
<h4 id="h4-u8282u62CD"><a name="节拍"></a>节拍</h4>
<p>对于这份简谱,我们还需要了解节拍的概念,乐谱的顶部有这样的符号:$1=C\frac{3}{4}$,表示这首歌是C大调,4分音符为一拍,每小节有3拍。我们大可不用管它具体的含义,只需要了解节拍是代表每个音持续长短的含义,节拍是一个相对值,可以动态定义1拍等于1s,也可以定义1拍等于500ms,但是乐谱中每个音符的持续时间的比例是约定好的,比如这首歌第1小节:0 0 <u>5 5</u>, 前面两个0各占一拍,后面两个5,利用下划线表示每个音符半拍,所以这一小节共计3拍,可以看到这份简谱中每一小节都是3拍。</p>
<h4 id="h4-u8FDEu97F3u7B26"><a name="连音符"></a>连音符</h4>
<p>这份乐谱的第3行第1段,在7和6的头顶上有一个连音符,代表7转向6时没有停顿,直接转音的。这同时意味着,除此以外的音符和音符之间,都是有间隙的,同样,间隙的长短需要我们程序里进行动态设置,反复尝试以求得一个比较好的播放效果。</p>
<h4 id="h4-u540Cu97F3u7B26"><a name="同音符"></a>同音符</h4>
<p>乐谱中还出现了一些-符号,这代表延续上一个音符,一般来说是延续一半的音长,但我们可以不用理会,直接延长同等长度即可。</p>
<p>综上,理解一份乐谱,我们对每一个音符需要建立<strong>音调</strong>、<strong>音长</strong>、<strong>间隙</strong>的三个概念,抽象为数据结构后,我们可以很容易编写以下代码,实现整首歌曲的播放:</p>
<pre>
<code class="language-python">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 = [ # 三维数组,分别为音调、音长、跟下一个音符之间是否存在间隙
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
,
]
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)
buzzer.duty(800)
time.sleep_ms(int(music))
if music == 1:
buzzer.duty(0)
i += 1
if i == len(music) - 1:
i = 0
time.sleep_ms(MUSIC_DELAY)</code></pre>
<p>这份代码,首先使用常量定义了各音调的频率表格,然后利用数据结构定义了乐谱中每一个音符的数据信息,接着初始化了相关外设,最后在循环中依次播放每个音符,加入btn作为音乐播放开关控制。随着我们对Python语言各项语法和关键字的熟悉,这份代码也没有什么新的知识点出现了。</p>
<h2 id="h2-wifi"><a name="WIFI"></a>WIFI</h2>
<p>ESP32最让人着迷的功能肯定是其强大的无线通信能力,使用MicroPython固件可以非常方便地调用网络库进行WIFI的连接,并执行更多丰富的网络动作,比如HTTP请求,MQTT推送和订阅等。查阅<a href="https://docs.micropython.org/en/latest/esp32/quickref.html#networking">MicroPython-Networking官方文档</a>可以看到以下示例代码:</p>
<pre>
<code class="language-python">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</code></pre>
<p>上面的代码只有基本的WIFI连接和扫描等动作,连接网络后,我们需要进行HTTP请求,此时需要用到urequests库。有些奇怪的是,MicroPython官网并没有提及任何关于urequests包的内容,但是在<a href="https://wiki.seeedstudio.com/XIAO_ESP32C3_MicroPython/#demo-3-connect-to-wi-fi">XIAO开发板入门教程-WIFI</a>以及<a href="https://training.eeworld.com.cn/video/38591">EEWORLD第三期视频教程</a>都不约而同地使用到这个库,我查了一下,urequests这个库目前在PyPi中处于第三方库的形式提供下载: <a href="https://pypi.org/project/micropython-urequests/">https://pypi.org/project/micropython-urequests/</a> 。或许是MicroPython在文档中并未提及该库的情况下,其发布的固件库中包含了该第三方库,个中原因无从得知。</p>
<p>WIFI部分我设计的逻辑是:</p>
<ul>
<li>通过网络http请求获得当前时间</li>
<li>通过解析json获得响应报文中的准确时间戳</li>
<li>使用ESP32硬件定时器每秒递增这个时间戳</li>
<li>通过本地时间转化进一步获得北京东八区的时间</li>
<li>定期将本地时间日期绘制到OLED屏幕的指定位置</li>
</ul>
<p>在实现以上逻辑的过程中,需要解决以下问题:</p>
<h3 id="h3-u7F51u7EDCu8BF7u6C42"><a name="网络请求"></a>网络请求</h3>
<p>之前提到,使用urequests库的urequests.get(url)方法可以方便地进行HTTP请求。通过搜索引擎搜索,得知苏宁提供一个稳定免费的授时服务: <a href="https://f.m.suning.com/api/ct.do">https://f.m.suning.com/api/ct.do</a> ,返回格式是json,解析其中的currentTime字段即可获得UTC标准时间戳。</p>
<h3 id="h3-json-"><a name="JSON解析"></a>JSON解析</h3>
<p>MicroPython内置了json支持,参考文档:<a href="https://docs.micropython.org/en/latest/genrst/modules.html#json">MicroPython的JSON模块</a>,这里再次表示对urequests库的疑惑,json库在MicroPython的文档中都有专门章节介绍到,而urequests库却只字未提。</p>
<h3 id="h3-u65F6u95F4u8F6Cu5316"><a name="时间转化"></a>时间转化</h3>
<p>通过上述请求,我们可以获得由<a href="https://f.m.suning.com/api/ct.do">苏宁</a>授时的国际UTC标准时间戳(毫秒级),这个时间戳是不含时区信息的,换言之就是全世界都使用这个时间戳作为国际时间,因此我们要在获得北京时间的话,还得补上东八区的8个小时。同时,为了适配micropython中的time包进行本地时间转化,我们还得将毫秒级的时间戳首先整除1000,获得秒级的UTC时间戳。</p>
<p>MicroPython的time包提供了时间日期转化函数localtime和gmtime,参见文档:<a href="https://docs.micropython.org/en/latest/library/time.html#time.gmtime">MicroPython的time模块</a>,文档中还有这么一段说明:</p>
<blockquote>
<p><strong>Time Epoch</strong>: 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).</p>
</blockquote>
<p>这句话是说,对于localtime函数,国际上普遍使用1970年1月1日零点作为时间的初始数据,但有些嵌入式设备使用的是从2000年1月1日起开始计算时间,为了避免误解,建议使用gmtime函数,直接获得纪元年份。</p>
<h3 id="h3-u786Cu4EF6u5B9Au65F6u5668"><a name="硬件定时器"></a>硬件定时器</h3>
<p>参考<a href="https://docs.micropython.org/en/latest/esp32/quickref.html#timers">MicroPython-Timer定时器官方文档</a>, 可观察到使用Python调用硬件定时器的基础用法:</p>
<pre>
<code class="language-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))</code></pre>
<p>这里首次出现了Python一个非常重要的知识点:</p>
<ul>
<li>lambda表达式:这是一种匿名函数的传递,现在的高级语言几乎都以“函数是一等公民”为设计准则,让函数可以入参传播。其实早在C语言时期,就可以通过函数指针随意传递函数,这些高级语言也真没有必要引以为傲。Python这次选用了lambda这个关键字来表达匿名函数的传递,是足够让我震撼的,虽然此时我已经被它的反骨和特立独行搞麻木了,但我还是怀疑作者是不是生怕别人不知道他懂lambda。一种匿名函数的传递而已,叫它闭包也好,lambda表达式也好,箭头函数不够用吗,非得拽词增加一个关键字。</li>
</ul>
<h3 id="h3-u5F02u5E38u5904u7406"><a name="异常处理"></a>异常处理</h3>
<p>在进行网络模块的设计时,将要面对一些运行时的不确定因素,因此这里需要熟悉一下Python的异常处理机制:</p>
<ul>
<li>异常处理:Python的异常处理使用try、finally关键字致敬了C系、JAVA系语言,使用raise致敬了Ruby,使用except标榜了自己,非常符合它一贯的雨露均沾和天生反骨的气质。我非常困惑的是为什么全世界都在用catch捕获异常,它老人家偏要用except,额外增加程序员的记忆量,是except这个单词更加优秀吗。</li>
</ul>
<p>解决了以上所有问题以后,编写以下代码实现上电后的网络授时、定时器走时和UI显示:</p>
<pre>
<code class="language-python">import nework
import urequests
import time
ssid = 'UFO'
password = 'newflypig'
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(ssid, password)
max_wait = 15
while max_wait > 0:
if wlan.isconnected():
break
max_wait -= 1
print('wait connect')
time.sleep(1)
if not wlan.isconnected():
print('connect fail')
else:
print('network connected')
try:
r = urequests.get('https://f.m.suning.com/api/ct.do')
data = r.json()
except Exception as e:
print(e)
else:
timeUTC = int(int(data['currentTime']) / 1000) + 8 * 3600 # 补上东八区的时差
# init Timer
tim0 = Timer(0)
tim0.init(period=1000, mode=Timer.PERIODIC, callback=lambda t:timeUTC += 1)
while True:
tm = time.gmtime(timeUTC)
oled.fill_rect(64, 0, 128, 10, 0)
oled.text('%02d:%02d:%02d' % (tm, tm, tm), 64, 0)
oled.fill_rect(48, 56, 128, 64, 0)
oled.text('%04d-%02d-%02d' % (tm - 30, tm, tm + 1), 48, 56)
finally:
r.close()</code></pre>
<h2 id="h2-aht20-"><a name="AHT20 温湿度传感器"></a>AHT20 温湿度传感器</h2>
<p>AHT20几乎是最常见的I2C传感器之一了,也几乎是各大开发板的必备器件,有了之前I2C学习的经验,这一部分几乎可以一带而过。首先,下载一个MicroPython平台上,现成的AHTX的驱动库: <a href="https://github.com/targetblank/micropython_ahtx0/blob/master/ahtx0.py">https://github.com/targetblank/micropython_ahtx0/blob/master/ahtx0.py</a> , 将该文件通过uPyCraft下载到设备中,接着编写以下测试代码即可</p>
<pre>
<code class="language-python">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%}")</code></pre>
<p>观察上述代码,我们表面上是调用了aht20.temperature是一个变量,但是我们细想就会得知,除非aht20模块内部维护了一个周期性的线程,在不停地更新其内部temperature变量(即使这样,在访问和更新时还要加锁保护),否则temperature变量将永不会改变。这里就遇到Python中一个鸡肋的语法糖,叫做<a href="https://github.com/property" title="@property">@property </a>装饰器。</p>
<ul>
<li><a href="https://github.com/property" title="@property">@property </a>装饰器:其设计思想可能来自于JAVA Bean的Getter/Setter,又有点像来自于Vue中的数据绑定。总之就是让你看上去访问的是一个变量,其实访问的是一个函数。形式上,让用户调用时多加一个括号转成显式函数调用就行了,纯纯的语法糖。</li>
</ul>
<p>观察下面代码,可以更加方便理解Pyton中这类用法:</p>
<pre>
<code class="language-python">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</code></pre>
<h2 id="h2-u5149u7EBFu4F20u611Fu5668"><a name="光线传感器"></a>光线传感器</h2>
<p>XIAO开发板附带的光线传感器模块,使用的是一颗光电二极管和一个运放构成的模拟量电路,因此可以通过MicroPython的ADC外设轻松获取模拟量值,从而评估环境光强弱。</p>
<p>参考<a href="https://docs.micropython.org/en/latest/esp32/quickref.html#adc-analog-to-digital-conversion">MicroPython-ADC官方文档</a>,有以下示例:</p>
<pre>
<code class="language-python">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</code></pre>
<p>根据原理图,参考上述示例,编写以下代码,以获取环境光数值:</p>
<pre>
<code class="language-python">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)</code></pre>
<p>由暗到亮调节周围环境光的亮度,可以观察模拟量采集到的数据差不多在0 - 5000左右,达到2000以上时环境光的亮度已经相当高了。</p>
<h2 id="h2-u7EFCu5408u5B9Eu9A8C"><a name="综合实验"></a>综合实验</h2>
<p>综合实验部分,我打算利用上述所学的所有内容,综合给儿子设计一个电子贺卡,基本需求如下:</p>
<ul>
<li>上电后使用网络通信获取当前时间</li>
<li>启动硬件定时器作为后续的计时工具</li>
<li>屏幕左上方显示当前温度</li>
<li>屏幕右上方显示时分秒</li>
<li>屏幕中间部分显示生日贺词</li>
<li>屏幕右下方显示日期</li>
<li>按下用户按键开始播放生日快乐歌,再次按下停止</li>
<li>当感应到周围环境光变弱时(关灯吹蜡烛时),将生日贺词进行走马灯滚动</li>
</ul>
<p>为了完成以上业务逻辑,靠单个while True似乎有些困难,于是开始研究MicroPython的多线程</p>
<h3 id="h3-thread"><a name="Thread"></a>Thread</h3>
<p>熟悉RTOS的朋友面对以上业务逻辑时,从外设资源的区分角度,一定希望能将系统拆分成两个线程,一个负责显示,一个负责音乐播放。于是开始探索Python的多线程编程,在<a href="https://docs.micropython.org/en/latest/library/_thread.html">MicroPython多线程</a>官方文档中,表示目前MicroPython可以高度实验性提供_thread底层线程支持。值得注意的是,在标准Python环境下,一般使用threading包来处理多线程任务,而_thread是Python底层非面向用户的包;而在MicroPython中,暂时只能使用_thread包直接使用。</p>
<p>继而查询_thread的使用方法,在<a href="https://docs.python.org/3/library/_thread.html">Python-thread官方文档</a>,中,有关于_thread库的使用文档,主要是_thread.start_new_thread(_function_, _args_[, _kwargs_])函数,初始化并启动一个线程。</p>
<h3 id="h3-u9879u76EEu7ED3u6784"><a name="项目结构"></a>项目结构</h3>
<p>至此,整个项目的结构就清晰了,上电后首先处理网络任务,获得时间并初始化定时器,然后兵分两路,一路负责显示,一路负责音乐播放:显示线程的while循环中,需要关心温度的采集,环境光的判断,UTC时间戳的变化;音乐播放线程的while中需要关心用户按键的触动,以及逐个音符的播放。配合几个驱动文件,整个项目的文档结构如图所示:</p>
<div style="text-align: center;"></div>
<p>最后还有一个小小的Python语法知识:</p>
<ul>
<li>Tuple:中文翻译成元组,Python中用两个圆括号包住多个由逗号分隔的数据,称为tuple,它们形成了一个<strong>有序</strong>、<strong>不可改变</strong>的数据集合。“不可改变的数据集合”这种数据类型在其他语言里是没有的,Python属于典型地“无中生有一个痛点并解决这个痛点”。在上述代码中,线程启动函数start_new_thread第二个参数的类型就是tuple,将线程函数所要的参数传递进去,(timeUTC)这种单个元素的tuple会被Python自动降级为单数据,所以如果不加逗号,是会报入参类型错误的,必须写成(timeUTC,)这种奇丑无比的写法。</li>
</ul>
<h2 id="h2-u603Bu7ED3"><a name="总结"></a>总结</h2>
<p>最终,实现了本次活动的所有作业,同时对Python语言的语法再次复习了一遍,也对Python有了更深入的理解。</p>
<p> </p>
<p>整个项目从组装硬件到最后完成共计三天左右,不得不说MicroPython平台的高效便捷。</p>
<p> </p>
<p>但项目最后跑起来后也逐渐发现性能问题似乎已经到了临界点,因为在开发过程中发现线程中所用的sleep_us函数出现了严重的拖延,尤其是当光线变暗,中文的贺词开始跑马灯滚动起来的时候,拖延变得更加厉害,歌曲的播放速度也会变慢许多。可见MicroPython平台,以及Python脚本的解析需要消耗大量算力,以至于在这一颗160MHz主频的MCU上,跑两个线程,驱动一个0.96寸的单色OLED屏幕都显得捉襟见肘。</p>
<p> </p>
<p>同时MicroPython平台虽然生态已经很棒了,但不得不说针对0.96寸单色OLED显示屏,有一个重要的开源库U8G2尚未支持,显示中文变得较为困难。</p>
<p> </p>
<p>窥其一斑,可见目前MCU领域还是C语言为王,其余的花拳绣腿只能在一定程度上加快开发,如果想发挥硬件的全部性能,或者最大程度地获得嵌入式开源库的支持,又或者想摆脱依赖深入底层,还是建议使用传统的C语言和RTOS进行产品级开发。</p>
<p> </p>
<p>最后感谢EEWorld社区,给我这次机会,熟悉和进一步了解了MicroPython平台和Python,希望下次活动能够有机会继续参与。</p>
<p> </p>
<div style="text-align: center;"></div>
<p>作业源代码:<a href="https://download.eeworld.com.cn/detail/%E5%A4%A7%E6%B3%A2%E4%B8%81/630300" target="_blank">https://download.eeworld.com.cn/detail/%E5%A4%A7%E6%B3%A2%E4%B8%81/630300</a></p>
<p>谢谢分享</p>
页:
[1]