最新文章
全部归档
教程
40 分钟
遗传算法与神经网络:用进化训练贪吃蛇
当梯度下降陷入局部最优解时,不妨试试另一条路:让神经网络像生物一样“进化”。本文介绍如何用遗传算法(选择、交叉、变异)训练一个贪吃蛇智能体,附完整 Python 源码与训练反思。
阅读全文
开发板
13 分钟
DIY—ESP32云台(持续更新中)
为满足业余射电天文观测需求,我用 ESP32 与 ULN2003 驱动电路 DIY 了一台低成本云台,充当天线”赤道仪”。本文记录从步进电机驱动原理研究到云台成品的完整过程,持续更新中。
阅读全文
开发板
278 分钟
开发板研究笔记—ESP32特别篇:ESP32-2432S028
ESP32-2432S028R 开发板在创客社区中被称为“Cheap Yellow Display”或简称 CYD。这款开发板的主芯片是 ESP32-WROOM-32 模组,自带 2.8 英寸 TFT 触摸屏 LCD microSD 卡接口 RGB 指示灯 LDR(光敏电阻器) 供电、串口通信电路 下面是它的背面图以及相关部分的说明: 屏幕显示驱动相关引脚 SPI Pin GPIO MISO GPIO 12 MOSI GPIO 13 SCKL GPIO 14 CS GPIO 15 DC GPIO 2 RST -1 Backlight Pin GPIO 21 屏幕触屏驱动相关引脚 SPI Pin GPIO IRQ GPIO 36 MOSI GPIO 32 MISO GPIO 39 CLK GPIO 25 CS GPIO 33 RGB灯珠相关引脚 RGB LED GPIO Red LED GPIO 4 Green LED GPIO 16 Blue LED GPIO 17 MicroSD卡相关引脚 MicroSD card SPI GPIO MISO GPIO 19 MOSI GPIO 23 SCK GPIO 18 CS GPIO 5 LDR GPIO 34 Speaker GPIO 26 BOOT Button GPIO 0 通过图示和表格可以看出,ESP32开发板的许多引脚已被占用。因此,若要使用这块板子进行开发,特别是涉及引脚需求较多的外设时,需慎重考虑是否适合选择此板。接下来我们通过一些程序来检验这个板子的可用性。 1.RGB灯珠RGB,这三个字母分别表示红(red)、绿(green)、蓝(blue),这三种颜色构成了光的三原色,可以说我们能见到的任何一种颜色都由它们混合而成。所以我们只需要分别控制这三种光的强度,就可以控制它最终显示出来的颜色了。 因为这个RGB灯珠内部没有集成特殊芯片(这类芯片可以用最少的引脚来控制尽可能多的led),所以我们还得一个一个的去控制每一个LED。下面,我们通过一个代码来让这个RGB灯珠循环渐变彩虹: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566# 导入必要的模块from machine import Pin, PWMimport time# 定义RGB灯珠的引脚red_pin = Pin(4, Pin.OUT)green_pin = Pin(16, Pin.OUT)blue_pin = Pin(17, Pin.OUT)# 创建PWM对象来控制亮度pwm_red = PWM(red_pin)pwm_green = PWM(green_pin)pwm_blue = PWM(blue_pin)# 设置PWM频率为1000Hzpwm_red.freq(1000)pwm_green.freq(1000)pwm_blue.freq(1000)# 颜色轮函数def wheel(pos): """生成颜色轮上的颜色。""" if pos < 0 or pos > 255: r = g = b = 0 elif pos < 85: r = int(pos * 3) g = int(255 - pos * 3) b = 0 elif pos < 170: pos -= 85 r = int(255 - pos * 3) g = 0 b = int(pos * 3) else: pos -= 170 r = 0 g = int(pos * 3) b = int(255 - pos * 3) return (r, g, b)# 主程序入口def main(): pos = 0 # 初始化位置变量 try: while True: # 无限循环 color = wheel(pos) print("Setting color:", color) # 打印颜色值 # 将RGB值映射到PWM占空比范围(0-1023) pwm_red.duty(int(color[0] * 1023 / 255)) pwm_green.duty(int(color[1] * 1023 / 255)) pwm_blue.duty(int(color[2] * 1023 / 255)) # 更新位置变量以实现颜色轮的效果 pos = (pos + 1) % 256 time.sleep(0.03) # 可以根据需要调整这个值 except KeyboardInterrupt: pwm_red.deinit() pwm_green.deinit() pwm_blue.deinit() print("Color cycle stopped.")# 运行主程序main() 需要注意的是,RGB LED的引脚是反向逻辑的,也就是说它们是低电平有效。这意味着, HIGH = OFF 和 LOW = ON: 1234567891011121314151617from machine import Pinimport osfrom time import sleep# RGB LED at the backred_led = Pin(4, Pin.OUT)green_led = Pin(16, Pin.OUT)blue_led = Pin(17, Pin.OUT)# 开启red_led.off()green_led.off()blue_led.off()sleep(2)# 关闭red_led.on()green_led.on()blue_led.on() 2.MicroSD卡在ESP32-CYD上,设计者非常贴心地预留了microSD卡的卡槽。这一设计考虑到了用户未来可能面临的内存不足问题。特别是当你需要加载CYD自带屏幕的两个驱动库(显示与触屏),或者安装其他第三方库时,内存需求会不断增加。因此,拥有一个SD卡作为外部扩展内存是一个十分明智的选择。 为了能够让ESP32设备读取到SD卡的内容,我们需要一个驱动库(sdcard.py): 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296"""MicroPython driver for SD cards using SPI bus.Requires an SPI bus and a CS pin. Provides readblocks and writeblocksmethods so the device can be mounted as a filesystem.Example usage on pyboard: import pyb, sdcard, os sd = sdcard.SDCard(pyb.SPI(1), pyb.Pin.board.X5) pyb.mount(sd, '/sd2') os.listdir('/')Example usage on ESP8266: import machine, sdcard, os sd = sdcard.SDCard(machine.SPI(1), machine.Pin(15)) os.mount(sd, '/sd') os.listdir('/')"""from micropython import constimport time_CMD_TIMEOUT = const(100)_R1_IDLE_STATE = const(1 << 0)R1_ERASE_RESET = const(1 << 1)_R1_ILLEGAL_COMMAND = const(1 << 2)R1_COM_CRC_ERROR = const(1 << 3)R1_ERASE_SEQUENCE_ERROR = const(1 << 4)R1_ADDRESS_ERROR = const(1 << 5)R1_PARAMETER_ERROR = const(1 << 6)_TOKEN_CMD25 = const(0xFC)_TOKEN_STOP_TRAN = const(0xFD)_TOKEN_DATA = const(0xFE)class SDCard: def __init__(self, spi, cs, baudrate=1320000): self.spi = spi self.cs = cs self.cmdbuf = bytearray(6) self.dummybuf = bytearray(512) self.tokenbuf = bytearray(1) for i in range(512): self.dummybuf[i] = 0xFF self.dummybuf_memoryview = memoryview(self.dummybuf) # initialise the card self.init_card(baudrate) def init_spi(self, baudrate): try: master = self.spi.MASTER except AttributeError: # on ESP8266 self.spi.init(baudrate=baudrate, phase=0, polarity=0) else: # on pyboard self.spi.init(master, baudrate=baudrate, phase=0, polarity=0) def init_card(self, baudrate): # init CS pin self.cs.init(self.cs.OUT, value=1) # init SPI bus; use low data rate for initialisation self.init_spi(100000) # clock card at least 100 cycles with cs high for i in range(16): self.spi.write(b"\xff") # CMD0: init card; should return _R1_IDLE_STATE (allow 5 attempts) for _ in range(5): if self.cmd(0, 0, 0x95) == _R1_IDLE_STATE: break else: raise OSError("no SD card") # CMD8: determine card version r = self.cmd(8, 0x01AA, 0x87, 4) if r == _R1_IDLE_STATE: self.init_card_v2() elif r == (_R1_IDLE_STATE | _R1_ILLEGAL_COMMAND): self.init_card_v1() else: raise OSError("couldn't determine SD card version") # get the number of sectors # CMD9: response R2 (R1 byte + 16-byte block read) if self.cmd(9, 0, 0, 0, False) != 0: raise OSError("no response from SD card") csd = bytearray(16) self.readinto(csd) if csd[0] & 0xC0 == 0x40: # CSD version 2.0 self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 1024 elif csd[0] & 0xC0 == 0x00: # CSD version 1.0 (old, <=2GB) c_size = (csd[6] & 0b11) << 10 | csd[7] << 2 | csd[8] >> 6 c_size_mult = (csd[9] & 0b11) << 1 | csd[10] >> 7 read_bl_len = csd[5] & 0b1111 capacity = (c_size + 1) * (2 ** (c_size_mult + 2)) * (2**read_bl_len) self.sectors = capacity // 512 else: raise OSError("SD card CSD format not supported") # print('sectors', self.sectors) # CMD16: set block length to 512 bytes if self.cmd(16, 512, 0) != 0: raise OSError("can't set 512 block size") # set to high data rate now that it's initialised self.init_spi(baudrate) def init_card_v1(self): for i in range(_CMD_TIMEOUT): self.cmd(55, 0, 0) if self.cmd(41, 0, 0) == 0: # SDSC card, uses byte addressing in read/write/erase commands self.cdv = 512 # print("[SDCard] v1 card") return raise OSError("timeout waiting for v1 card") def init_card_v2(self): for i in range(_CMD_TIMEOUT): time.sleep_ms(50) self.cmd(58, 0, 0, 4) self.cmd(55, 0, 0) if self.cmd(41, 0x40000000, 0) == 0: self.cmd(58, 0, 0, -4) # 4-byte response, negative means keep the first byte ocr = self.tokenbuf[0] # get first byte of response, which is OCR if not ocr & 0x40: # SDSC card, uses byte addressing in read/write/erase commands self.cdv = 512 else: # SDHC/SDXC card, uses block addressing in read/write/erase commands self.cdv = 1 # print("[SDCard] v2 card") return raise OSError("timeout waiting for v2 card") def cmd(self, cmd, arg, crc, final=0, release=True, skip1=False): self.cs(0) # create and send the command buf = self.cmdbuf buf[0] = 0x40 | cmd buf[1] = arg >> 24 buf[2] = arg >> 16 buf[3] = arg >> 8 buf[4] = arg buf[5] = crc self.spi.write(buf) if skip1: self.spi.readinto(self.tokenbuf, 0xFF) # wait for the response (response[7] == 0) for i in range(_CMD_TIMEOUT): self.spi.readinto(self.tokenbuf, 0xFF) response = self.tokenbuf[0] if not (response & 0x80): # this could be a big-endian integer that we are getting here # if final<0 then store the first byte to tokenbuf and discard the rest if final < 0: self.spi.readinto(self.tokenbuf, 0xFF) final = -1 - final for j in range(final): self.spi.write(b"\xff") if release: self.cs(1) self.spi.write(b"\xff") return response # timeout self.cs(1) self.spi.write(b"\xff") return -1 def readinto(self, buf): self.cs(0) # read until start byte (0xff) for i in range(_CMD_TIMEOUT): self.spi.readinto(self.tokenbuf, 0xFF) if self.tokenbuf[0] == _TOKEN_DATA: break time.sleep_ms(1) else: self.cs(1) raise OSError("timeout waiting for response") # read data mv = self.dummybuf_memoryview if len(buf) != len(mv): mv = mv[: len(buf)] self.spi.write_readinto(mv, buf) # read checksum self.spi.write(b"\xff") self.spi.write(b"\xff") self.cs(1) self.spi.write(b"\xff") def write(self, token, buf): self.cs(0) # send: start of block, data, checksum self.spi.read(1, token) self.spi.write(buf) self.spi.write(b"\xff") self.spi.write(b"\xff") # check the response if (self.spi.read(1, 0xFF)[0] & 0x1F) != 0x05: self.cs(1) self.spi.write(b"\xff") return # wait for write to finish while self.spi.read(1, 0xFF)[0] == 0: pass self.cs(1) self.spi.write(b"\xff") def write_token(self, token): self.cs(0) self.spi.read(1, token) self.spi.write(b"\xff") # wait for write to finish while self.spi.read(1, 0xFF)[0] == 0x00: pass self.cs(1) self.spi.write(b"\xff") def readblocks(self, block_num, buf): nblocks = len(buf) // 512 assert nblocks and not len(buf) % 512, "Buffer length is invalid" if nblocks == 1: # CMD17: set read address for single block if self.cmd(17, block_num * self.cdv, 0, release=False) != 0: # release the card self.cs(1) raise OSError(5) # EIO # receive the data and release card self.readinto(buf) else: # CMD18: set read address for multiple blocks if self.cmd(18, block_num * self.cdv, 0, release=False) != 0: # release the card self.cs(1) raise OSError(5) # EIO offset = 0 mv = memoryview(buf) while nblocks: # receive the data and release card self.readinto(mv[offset : offset + 512]) offset += 512 nblocks -= 1 if self.cmd(12, 0, 0xFF, skip1=True): raise OSError(5) # EIO def writeblocks(self, block_num, buf): nblocks, err = divmod(len(buf), 512) assert nblocks and not err, "Buffer length is invalid" if nblocks == 1: # CMD24: set write address for single block if self.cmd(24, block_num * self.cdv, 0) != 0: raise OSError(5) # EIO # send the data self.write(_TOKEN_DATA, buf) else: # CMD25: set write address for first block if self.cmd(25, block_num * self.cdv, 0) != 0: raise OSError(5) # EIO # send the data offset = 0 mv = memoryview(buf) while nblocks: self.write(_TOKEN_CMD25, mv[offset : offset + 512]) offset += 512 nblocks -= 1 self.write_token(_TOKEN_STOP_TRAN) def ioctl(self, op, arg): if op == 4: # get number of blocks return self.sectors if op == 5: # get block size in bytes return 512 接下来我们可以通过代码对sd卡进行读写操作: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657import os from machine import Pin, SoftSPIfrom sdcard import SDCard# 接线说明:# MISO -> GPIO 19# MOSI -> GPIO 23# CLK -> GPIO 18# CS -> GPIO 5spisd = SoftSPI(-1, miso=Pin(19), mosi=Pin(23), sck=Pin(18))sd = SDCard(spisd, Pin(5))# os.listdir() --> 返回当前目录下的所有文件和目录的列表print('未挂载SD之前:', os.listdir()) print("-------------------------------")# 创建一个虚拟文件系统,用于挂载SD卡vfs = os.VfsFat(sd) # 挂载SD卡到虚拟文件系统:将虚拟文件系统挂载到/sd目录。之后,对/sd目录的访问将实际访问SD卡。os.mount(vfs, '/sd') print('挂载SD之后:', os.listdir())# 对SD卡进行操作try: # 切换到SD卡目录 os.chdir('/sd') print('SD卡中的文件:', os.listdir()) # 写入文件到SD卡 with open("/sd/test.txt", "w") as f: for i in range(1, 101): f.write(str(i) + "\n") print("-------------------------------") print("已经将1~100写入到SD卡中的test.txt文件") print("-------------------------------") # 从SD卡读取文件并显示内容 """ 在运行之前,请确保你的sd卡内 包含一个名为url.txt的文件, 且里面写入了些文本 """ with open("/sd/url.txt", "r") as f: content = f.readlines() print("SD卡中url.txt文件的内容:") print("-------------------------------") for line in content: print(line.strip()) print("-------------------------------")except Exception as e: print("\n发生错误!", e)finally: os.umount('/sd') print("SD卡已卸载") 3.点亮屏幕3.1显示ESP32-CYD上自带了一个2.8 英寸(320x240) TFT 触摸屏 (LCD),TFT LCD 屏幕通过控制液晶分子的排列来改变光的透过率,从而实现图像的显示。这块屏幕的驱动芯片为ili9341,ILI9341 控制器负责将来自微控制器的显示数据转换为控制液晶分子的信号。当微控制器向 ILI9341 发送显示数据时,ILI9341 会根据这些数据控制 LCD 屏幕上的像素点,使其呈现出相应的颜色和亮度。 micropython驱动库(ili9341.py): 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074"""ILI9341 LCD/Touch module."""from time import sleepfrom math import cos, sin, pi, radiansfrom sys import implementationfrom framebuf import FrameBuffer, RGB565 # type: ignorefrom micropython import const # type: ignoredef color565(r, g, b): """Return RGB565 color value. Args: r (int): Red value. g (int): Green value. b (int): Blue value. """ return (r & 0xf8) << 8 | (g & 0xfc) << 3 | b >> 3class Display(object): """Serial interface for 16-bit color (5-6-5 RGB) IL9341 display. Note: All coordinates are zero based. """ # Command constants from ILI9341 datasheet NOP = const(0x00) # No-op SWRESET = const(0x01) # Software reset RDDID = const(0x04) # Read display ID info RDDST = const(0x09) # Read display status SLPIN = const(0x10) # Enter sleep mode SLPOUT = const(0x11) # Exit sleep mode PTLON = const(0x12) # Partial mode on NORON = const(0x13) # Normal display mode on RDMODE = const(0x0A) # Read display power mode RDMADCTL = const(0x0B) # Read display MADCTL RDPIXFMT = const(0x0C) # Read display pixel format RDIMGFMT = const(0x0D) # Read display image format RDSELFDIAG = const(0x0F) # Read display self-diagnostic INVOFF = const(0x20) # Display inversion off INVON = const(0x21) # Display inversion on GAMMASET = const(0x26) # Gamma set DISPLAY_OFF = const(0x28) # Display off DISPLAY_ON = const(0x29) # Display on SET_COLUMN = const(0x2A) # Column address set SET_PAGE = const(0x2B) # Page address set WRITE_RAM = const(0x2C) # Memory write READ_RAM = const(0x2E) # Memory read PTLAR = const(0x30) # Partial area VSCRDEF = const(0x33) # Vertical scrolling definition MADCTL = const(0x36) # Memory access control VSCRSADD = const(0x37) # Vertical scrolling start address PIXFMT = const(0x3A) # COLMOD: Pixel format set WRITE_DISPLAY_BRIGHTNESS = const(0x51) # Brightness hardware dependent! READ_DISPLAY_BRIGHTNESS = const(0x52) WRITE_CTRL_DISPLAY = const(0x53) READ_CTRL_DISPLAY = const(0x54) WRITE_CABC = const(0x55) # Write Content Adaptive Brightness Control READ_CABC = const(0x56) # Read Content Adaptive Brightness Control WRITE_CABC_MINIMUM = const(0x5E) # Write CABC Minimum Brightness READ_CABC_MINIMUM = const(0x5F) # Read CABC Minimum Brightness FRMCTR1 = const(0xB1) # Frame rate control (In normal mode/full colors) FRMCTR2 = const(0xB2) # Frame rate control (In idle mode/8 colors) FRMCTR3 = const(0xB3) # Frame rate control (In partial mode/full colors) INVCTR = const(0xB4) # Display inversion control DFUNCTR = const(0xB6) # Display function control PWCTR1 = const(0xC0) # Power control 1 PWCTR2 = const(0xC1) # Power control 2 PWCTRA = const(0xCB) # Power control A PWCTRB = const(0xCF) # Power control B VMCTR1 = const(0xC5) # VCOM control 1 VMCTR2 = const(0xC7) # VCOM control 2 RDID1 = const(0xDA) # Read ID 1 RDID2 = const(0xDB) # Read ID 2 RDID3 = const(0xDC) # Read ID 3 RDID4 = const(0xDD) # Read ID 4 GMCTRP1 = const(0xE0) # Positive gamma correction GMCTRN1 = const(0xE1) # Negative gamma correction DTCA = const(0xE8) # Driver timing control A DTCB = const(0xEA) # Driver timing control B POSC = const(0xED) # Power on sequence control ENABLE3G = const(0xF2) # Enable 3 gamma control PUMPRC = const(0xF7) # Pump ratio control MIRROR_ROTATE = { # MADCTL configurations for rotation and mirroring (False, 0): 0x80, # 1000 0000 (False, 90): 0xE0, # 1110 0000 (False, 180): 0x40, # 0100 0000 (False, 270): 0x20, # 0010 0000 (True, 0): 0xC0, # 1100 0000 (True, 90): 0x60, # 0110 0000 (True, 180): 0x00, # 0000 0000 (True, 270): 0xA0 # 1010 0000 } def __init__(self, spi, cs, dc, rst, width=240, height=320, rotation=0, mirror=False, bgr=True, gamma=True): """Initialize OLED. Args: spi (Class Spi): SPI interface for OLED cs (Class Pin): Chip select pin dc (Class Pin): Data/Command pin rst (Class Pin): Reset pin width (Optional int): Screen width (default 240) height (Optional int): Screen height (default 320) rotation (Optional int): Rotation must be 0 default, 90. 180 or 270 mirror (Optional bool): Mirror display (default False) bgr (Optional bool): Swaps red and blue colors (default True) gamma (Optional bool): Custom gamma correction (default True) """ self.spi = spi self.cs = cs self.dc = dc self.rst = rst self.width = width self.height = height if (mirror, rotation) not in self.MIRROR_ROTATE: raise ValueError('Rotation must be 0, 90, 180 or 270.') else: self.rotation = self.MIRROR_ROTATE[mirror, rotation] if bgr: # Set BGR bit self.rotation |= 0b00001000 # Initialize GPIO pins and set implementation specific methods if implementation.name == 'circuitpython': self.cs.switch_to_output(value=True) self.dc.switch_to_output(value=False) self.rst.switch_to_output(value=True) self.reset = self.reset_cpy self.write_cmd = self.write_cmd_cpy self.write_data = self.write_data_cpy else: self.cs.init(self.cs.OUT, value=1) self.dc.init(self.dc.OUT, value=0) self.rst.init(self.rst.OUT, value=1) self.reset = self.reset_mpy self.write_cmd = self.write_cmd_mpy self.write_data = self.write_data_mpy self.reset() # Send initialization commands self.write_cmd(self.SWRESET) # Software reset sleep(.1) self.write_cmd(self.PWCTRB, 0x00, 0xC1, 0x30) # Pwr ctrl B self.write_cmd(self.POSC, 0x64, 0x03, 0x12, 0x81) # Pwr on seq. ctrl self.write_cmd(self.DTCA, 0x85, 0x00, 0x78) # Driver timing ctrl A self.write_cmd(self.PWCTRA, 0x39, 0x2C, 0x00, 0x34, 0x02) # Pwr ctrl A self.write_cmd(self.PUMPRC, 0x20) # Pump ratio control self.write_cmd(self.DTCB, 0x00, 0x00) # Driver timing ctrl B self.write_cmd(self.PWCTR1, 0x23) # Pwr ctrl 1 self.write_cmd(self.PWCTR2, 0x10) # Pwr ctrl 2 self.write_cmd(self.VMCTR1, 0x3E, 0x28) # VCOM ctrl 1 self.write_cmd(self.VMCTR2, 0x86) # VCOM ctrl 2 self.write_cmd(self.MADCTL, self.rotation) # Memory access ctrl self.write_cmd(self.VSCRSADD, 0x00) # Vertical scrolling start address self.write_cmd(self.PIXFMT, 0x55) # COLMOD: Pixel format self.write_cmd(self.FRMCTR1, 0x00, 0x18) # Frame rate ctrl self.write_cmd(self.DFUNCTR, 0x08, 0x82, 0x27) self.write_cmd(self.ENABLE3G, 0x00) # Enable 3 gamma ctrl self.write_cmd(self.GAMMASET, 0x01) # Gamma curve selected if gamma: # Use custom gamma correction values self.write_cmd(self.GMCTRP1, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00) self.write_cmd(self.GMCTRN1, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F) self.write_cmd(self.SLPOUT) # Exit sleep sleep(.1) self.write_cmd(self.DISPLAY_ON) # Display on sleep(.1) self.clear() def block(self, x0, y0, x1, y1, data): """Write a block of data to display. Args: x0 (int): Starting X position. y0 (int): Starting Y position. x1 (int): Ending X position. y1 (int): Ending Y position. data (bytes): Data buffer to write. """ self.write_cmd(self.SET_COLUMN, x0 >> 8, x0 & 0xff, x1 >> 8, x1 & 0xff) self.write_cmd(self.SET_PAGE, y0 >> 8, y0 & 0xff, y1 >> 8, y1 & 0xff) self.write_cmd(self.WRITE_RAM) self.write_data(data) def cleanup(self): """Clean up resources.""" self.clear() self.display_off() self.spi.deinit() print('display off') def clear(self, color=0, hlines=8): """Clear display. Args: color (Optional int): RGB565 color value (Default: 0 = Black). hlines (Optional int): # of horizontal lines per chunk (Default: 8) Note: hlines was introduced to deal with memory allocation on some boards. Smaller values allocate less memory but take longer to execute. hlines must be a factor of the display height. For example, for a 240 pixel height, valid values for hline would be 1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 16, 20, 24, 30, 40, etc. Higher values may result in memory allocation errors. """ w = self.width h = self.height assert hlines > 0 and h % hlines == 0, ( "hlines must be a non-zero factor of height.") # Clear display if color: line = color.to_bytes(2, 'big') * (w * hlines) else: line = bytearray(w * 2 * hlines) for y in range(0, h, hlines): self.block(0, y, w - 1, y + hlines - 1, line) def display_off(self): """Turn display off.""" self.write_cmd(self.DISPLAY_OFF) def display_on(self): """Turn display on.""" self.write_cmd(self.DISPLAY_ON) def draw_circle(self, x0, y0, r, color): """Draw a circle. Args: x0 (int): X coordinate of center point. y0 (int): Y coordinate of center point. r (int): Radius. color (int): RGB565 color value. """ f = 1 - r dx = 1 dy = -r - r x = 0 y = r self.draw_pixel(x0, y0 + r, color) self.draw_pixel(x0, y0 - r, color) self.draw_pixel(x0 + r, y0, color) self.draw_pixel(x0 - r, y0, color) while x < y: if f >= 0: y -= 1 dy += 2 f += dy x += 1 dx += 2 f += dx self.draw_pixel(x0 + x, y0 + y, color) self.draw_pixel(x0 - x, y0 + y, color) self.draw_pixel(x0 + x, y0 - y, color) self.draw_pixel(x0 - x, y0 - y, color) self.draw_pixel(x0 + y, y0 + x, color) self.draw_pixel(x0 - y, y0 + x, color) self.draw_pixel(x0 + y, y0 - x, color) self.draw_pixel(x0 - y, y0 - x, color) def draw_ellipse(self, x0, y0, a, b, color): """Draw an ellipse. Args: x0, y0 (int): Coordinates of center point. a (int): Semi axis horizontal. b (int): Semi axis vertical. color (int): RGB565 color value. Note: The center point is the center of the x0,y0 pixel. Since pixels are not divisible, the axes are integer rounded up to complete on a full pixel. Therefore the major and minor axes are increased by 1. """ a2 = a * a b2 = b * b twoa2 = a2 + a2 twob2 = b2 + b2 x = 0 y = b px = 0 py = twoa2 * y # Plot initial points self.draw_pixel(x0 + x, y0 + y, color) self.draw_pixel(x0 - x, y0 + y, color) self.draw_pixel(x0 + x, y0 - y, color) self.draw_pixel(x0 - x, y0 - y, color) # Region 1 p = round(b2 - (a2 * b) + (0.25 * a2)) while px < py: x += 1 px += twob2 if p < 0: p += b2 + px else: y -= 1 py -= twoa2 p += b2 + px - py self.draw_pixel(x0 + x, y0 + y, color) self.draw_pixel(x0 - x, y0 + y, color) self.draw_pixel(x0 + x, y0 - y, color) self.draw_pixel(x0 - x, y0 - y, color) # Region 2 p = round(b2 * (x + 0.5) * (x + 0.5) + a2 * (y - 1) * (y - 1) - a2 * b2) while y > 0: y -= 1 py -= twoa2 if p > 0: p += a2 - py else: x += 1 px += twob2 p += a2 - py + px self.draw_pixel(x0 + x, y0 + y, color) self.draw_pixel(x0 - x, y0 + y, color) self.draw_pixel(x0 + x, y0 - y, color) self.draw_pixel(x0 - x, y0 - y, color) def draw_hline(self, x, y, w, color): """Draw a horizontal line. Args: x (int): Starting X position. y (int): Starting Y position. w (int): Width of line. color (int): RGB565 color value. """ if self.is_off_grid(x, y, x + w - 1, y): return line = color.to_bytes(2, 'big') * w self.block(x, y, x + w - 1, y, line) def draw_image(self, path, x=0, y=0, w=320, h=240): """Draw image from flash. Args: path (string): Image file path. x (int): X coordinate of image left. Default is 0. y (int): Y coordinate of image top. Default is 0. w (int): Width of image. Default is 320. h (int): Height of image. Default is 240. """ x2 = x + w - 1 y2 = y + h - 1 if self.is_off_grid(x, y, x2, y2): return with open(path, "rb") as f: chunk_height = 1024 // w chunk_count, remainder = divmod(h, chunk_height) chunk_size = chunk_height * w * 2 chunk_y = y if chunk_count: for c in range(0, chunk_count): buf = f.read(chunk_size) self.block(x, chunk_y, x2, chunk_y + chunk_height - 1, buf) chunk_y += chunk_height if remainder: buf = f.read(remainder * w * 2) self.block(x, chunk_y, x2, chunk_y + remainder - 1, buf) def draw_letter(self, x, y, letter, font, color, background=0, landscape=False, rotate_180=False): """Draw a letter. Args: x (int): Starting X position. y (int): Starting Y position. letter (string): Letter to draw. font (XglcdFont object): Font. color (int): RGB565 color value. background (int): RGB565 background color (default: black) landscape (bool): Orientation (default: False = portrait) rotate_180 (bool): Rotate text by 180 degrees """ buf, w, h = font.get_letter(letter, color, background, landscape) if rotate_180: # Manually rotate the buffer by 180 degrees # ensure bytes pairs for each pixel retain color565 new_buf = bytearray(len(buf)) num_pixels = len(buf) // 2 for i in range(num_pixels): # The index for the new buffer's byte pair new_idx = (num_pixels - 1 - i) * 2 # The index for the original buffer's byte pair old_idx = i * 2 # Swap the pixels new_buf[new_idx], new_buf[new_idx + 1] = buf[old_idx], buf[old_idx + 1] buf = new_buf # Check for errors (Font could be missing specified letter) if w == 0: return w, h if landscape: y -= w if self.is_off_grid(x, y, x + h - 1, y + w - 1): return 0, 0 self.block(x, y, x + h - 1, y + w - 1, buf) else: if self.is_off_grid(x, y, x + w - 1, y + h - 1): return 0, 0 self.block(x, y, x + w - 1, y + h - 1, buf) return w, h def draw_line(self, x1, y1, x2, y2, color): """Draw a line using Bresenham's algorithm. Args: x1, y1 (int): Starting coordinates of the line x2, y2 (int): Ending coordinates of the line color (int): RGB565 color value. """ # Check for horizontal line if y1 == y2: if x1 > x2: x1, x2 = x2, x1 self.draw_hline(x1, y1, x2 - x1 + 1, color) return # Check for vertical line if x1 == x2: if y1 > y2: y1, y2 = y2, y1 self.draw_vline(x1, y1, y2 - y1 + 1, color) return # Confirm coordinates in boundary if self.is_off_grid(min(x1, x2), min(y1, y2), max(x1, x2), max(y1, y2)): return # Changes in x, y dx = x2 - x1 dy = y2 - y1 # Determine how steep the line is is_steep = abs(dy) > abs(dx) # Rotate line if is_steep: x1, y1 = y1, x1 x2, y2 = y2, x2 # Swap start and end points if necessary if x1 > x2: x1, x2 = x2, x1 y1, y2 = y2, y1 # Recalculate differentials dx = x2 - x1 dy = y2 - y1 # Calculate error error = dx >> 1 ystep = 1 if y1 < y2 else -1 y = y1 for x in range(x1, x2 + 1): # Had to reverse HW ???? if not is_steep: self.draw_pixel(x, y, color) else: self.draw_pixel(y, x, color) error -= abs(dy) if error < 0: y += ystep error += dx def draw_lines(self, coords, color): """Draw multiple lines. Args: coords ([[int, int],...]): Line coordinate X, Y pairs color (int): RGB565 color value. """ # Starting point x1, y1 = coords[0] # Iterate through coordinates for i in range(1, len(coords)): x2, y2 = coords[i] self.draw_line(x1, y1, x2, y2, color) x1, y1 = x2, y2 def draw_pixel(self, x, y, color): """Draw a single pixel. Args: x (int): X position. y (int): Y position. color (int): RGB565 color value. """ if self.is_off_grid(x, y, x, y): return self.block(x, y, x, y, color.to_bytes(2, 'big')) def draw_polygon(self, sides, x0, y0, r, color, rotate=0): """Draw an n-sided regular polygon. Args: sides (int): Number of polygon sides. x0, y0 (int): Coordinates of center point. r (int): Radius. color (int): RGB565 color value. rotate (Optional float): Rotation in degrees relative to origin. Note: The center point is the center of the x0,y0 pixel. Since pixels are not divisible, the radius is integer rounded up to complete on a full pixel. Therefore diameter = 2 x r + 1. """ coords = [] theta = radians(rotate) n = sides + 1 for s in range(n): t = 2.0 * pi * s / sides + theta coords.append([int(r * cos(t) + x0), int(r * sin(t) + y0)]) # Cast to python float first to fix rounding errors self.draw_lines(coords, color=color) def draw_rectangle(self, x, y, w, h, color): """Draw a rectangle. Args: x (int): Starting X position. y (int): Starting Y position. w (int): Width of rectangle. h (int): Height of rectangle. color (int): RGB565 color value. """ x2 = x + w - 1 y2 = y + h - 1 self.draw_hline(x, y, w, color) self.draw_hline(x, y2, w, color) self.draw_vline(x, y, h, color) self.draw_vline(x2, y, h, color) def draw_sprite(self, buf, x, y, w, h): """Draw a sprite (optimized for horizontal drawing). Args: buf (bytearray): Buffer to draw. x (int): Starting X position. y (int): Starting Y position. w (int): Width of drawing. h (int): Height of drawing. """ x2 = x + w - 1 y2 = y + h - 1 if self.is_off_grid(x, y, x2, y2): return self.block(x, y, x2, y2, buf) def draw_text(self, x, y, text, font, color, background=0, landscape=False, rotate_180=False, spacing=1): """Draw text. Args: x (int): Starting X position y (int): Starting Y position text (string): Text to draw font (XglcdFont object): Font color (int): RGB565 color value background (int): RGB565 background color (default: black) landscape (bool): Orientation (default: False = portrait) rotate_180 (bool): Rotate text by 180 degrees spacing (int): Pixels between letters (default: 1) """ iterable_text = reversed(text) if rotate_180 else text for letter in iterable_text: # Get letter array and letter dimensions w, h = self.draw_letter(x, y, letter, font, color, background, landscape, rotate_180) # Stop on error if w == 0 or h == 0: print('Invalid width {0} or height {1}'.format(w, h)) return if landscape: # Fill in spacing if spacing: self.fill_hrect(x, y - w - spacing, h, spacing, background) # Position y for next letter y -= (w + spacing) else: # Fill in spacing if spacing: self.fill_hrect(x + w, y, spacing, h, background) # Position x for next letter x += (w + spacing) # # Fill in spacing # if spacing: # self.fill_vrect(x + w, y, spacing, h, background) # # Position x for next letter # x += w + spacing def draw_text8x8(self, x, y, text, color, background=0, rotate=0): """Draw text using built-in MicroPython 8x8 bit font. Args: x (int): Starting X position. y (int): Starting Y position. text (string): Text to draw. color (int): RGB565 color value. background (int): RGB565 background color (default: black). rotate(int): 0, 90, 180, 270 """ w = len(text) * 8 h = 8 # Confirm coordinates in boundary if self.is_off_grid(x, y, x + 7, y + 7): return buf = bytearray(w * 16) fbuf = FrameBuffer(buf, w, h, RGB565) if background != 0: # Swap background color bytes to correct for framebuf endianness b_color = ((background & 0xFF) << 8) | ((background & 0xFF00) >> 8) fbuf.fill(b_color) # Swap text color bytes to correct for framebuf endianness t_color = ((color & 0xFF) << 8) | ((color & 0xFF00) >> 8) fbuf.text(text, 0, 0, t_color) if rotate == 0: self.block(x, y, x + w - 1, y + (h - 1), buf) elif rotate == 90: buf2 = bytearray(w * 16) fbuf2 = FrameBuffer(buf2, h, w, RGB565) for y1 in range(h): for x1 in range(w): fbuf2.pixel(y1, x1, fbuf.pixel(x1, (h - 1) - y1)) self.block(x, y, x + (h - 1), y + w - 1, buf2) elif rotate == 180: buf2 = bytearray(w * 16) fbuf2 = FrameBuffer(buf2, w, h, RGB565) for y1 in range(h): for x1 in range(w): fbuf2.pixel(x1, y1, fbuf.pixel((w - 1) - x1, (h - 1) - y1)) self.block(x, y, x + w - 1, y + (h - 1), buf2) elif rotate == 270: buf2 = bytearray(w * 16) fbuf2 = FrameBuffer(buf2, h, w, RGB565) for y1 in range(h): for x1 in range(w): fbuf2.pixel(y1, x1, fbuf.pixel((w - 1) - x1, y1)) self.block(x, y, x + (h - 1), y + w - 1, buf2) def draw_vline(self, x, y, h, color): """Draw a vertical line. Args: x (int): Starting X position. y (int): Starting Y position. h (int): Height of line. color (int): RGB565 color value. """ # Confirm coordinates in boundary if self.is_off_grid(x, y, x, y + h - 1): return line = color.to_bytes(2, 'big') * h self.block(x, y, x, y + h - 1, line) def fill_circle(self, x0, y0, r, color): """Draw a filled circle. Args: x0 (int): X coordinate of center point. y0 (int): Y coordinate of center point. r (int): Radius. color (int): RGB565 color value. """ f = 1 - r dx = 1 dy = -r - r x = 0 y = r self.draw_vline(x0, y0 - r, 2 * r + 1, color) while x < y: if f >= 0: y -= 1 dy += 2 f += dy x += 1 dx += 2 f += dx self.draw_vline(x0 + x, y0 - y, 2 * y + 1, color) self.draw_vline(x0 - x, y0 - y, 2 * y + 1, color) self.draw_vline(x0 - y, y0 - x, 2 * x + 1, color) self.draw_vline(x0 + y, y0 - x, 2 * x + 1, color) def fill_ellipse(self, x0, y0, a, b, color): """Draw a filled ellipse. Args: x0, y0 (int): Coordinates of center point. a (int): Semi axis horizontal. b (int): Semi axis vertical. color (int): RGB565 color value. Note: The center point is the center of the x0,y0 pixel. Since pixels are not divisible, the axes are integer rounded up to complete on a full pixel. Therefore the major and minor axes are increased by 1. """ a2 = a * a b2 = b * b twoa2 = a2 + a2 twob2 = b2 + b2 x = 0 y = b px = 0 py = twoa2 * y # Plot initial points self.draw_line(x0, y0 - y, x0, y0 + y, color) # Region 1 p = round(b2 - (a2 * b) + (0.25 * a2)) while px < py: x += 1 px += twob2 if p < 0: p += b2 + px else: y -= 1 py -= twoa2 p += b2 + px - py self.draw_line(x0 + x, y0 - y, x0 + x, y0 + y, color) self.draw_line(x0 - x, y0 - y, x0 - x, y0 + y, color) # Region 2 p = round(b2 * (x + 0.5) * (x + 0.5) + a2 * (y - 1) * (y - 1) - a2 * b2) while y > 0: y -= 1 py -= twoa2 if p > 0: p += a2 - py else: x += 1 px += twob2 p += a2 - py + px self.draw_line(x0 + x, y0 - y, x0 + x, y0 + y, color) self.draw_line(x0 - x, y0 - y, x0 - x, y0 + y, color) def fill_hrect(self, x, y, w, h, color): """Draw a filled rectangle (optimized for horizontal drawing). Args: x (int): Starting X position. y (int): Starting Y position. w (int): Width of rectangle. h (int): Height of rectangle. color (int): RGB565 color value. """ if self.is_off_grid(x, y, x + w - 1, y + h - 1): return chunk_height = 1024 // w chunk_count, remainder = divmod(h, chunk_height) chunk_size = chunk_height * w chunk_y = y if chunk_count: buf = color.to_bytes(2, 'big') * chunk_size for c in range(0, chunk_count): self.block(x, chunk_y, x + w - 1, chunk_y + chunk_height - 1, buf) chunk_y += chunk_height if remainder: buf = color.to_bytes(2, 'big') * remainder * w self.block(x, chunk_y, x + w - 1, chunk_y + remainder - 1, buf) def fill_rectangle(self, x, y, w, h, color): """Draw a filled rectangle. Args: x (int): Starting X position. y (int): Starting Y position. w (int): Width of rectangle. h (int): Height of rectangle. color (int): RGB565 color value. """ if self.is_off_grid(x, y, x + w - 1, y + h - 1): return if w > h: self.fill_hrect(x, y, w, h, color) else: self.fill_vrect(x, y, w, h, color) def fill_polygon(self, sides, x0, y0, r, color, rotate=0): """Draw a filled n-sided regular polygon. Args: sides (int): Number of polygon sides. x0, y0 (int): Coordinates of center point. r (int): Radius. color (int): RGB565 color value. rotate (Optional float): Rotation in degrees relative to origin. Note: The center point is the center of the x0,y0 pixel. Since pixels are not divisible, the radius is integer rounded up to complete on a full pixel. Therefore diameter = 2 x r + 1. """ # Determine side coordinates coords = [] theta = radians(rotate) n = sides + 1 for s in range(n): t = 2.0 * pi * s / sides + theta coords.append([int(r * cos(t) + x0), int(r * sin(t) + y0)]) # Starting point x1, y1 = coords[0] # Minimum Maximum X dict xdict = {y1: [x1, x1]} # Iterate through coordinates for row in coords[1:]: x2, y2 = row xprev, yprev = x2, y2 # Calculate perimeter # Check for horizontal side if y1 == y2: if x1 > x2: x1, x2 = x2, x1 if y1 in xdict: xdict[y1] = [min(x1, xdict[y1][0]), max(x2, xdict[y1][1])] else: xdict[y1] = [x1, x2] x1, y1 = xprev, yprev continue # Non horizontal side # Changes in x, y dx = x2 - x1 dy = y2 - y1 # Determine how steep the line is is_steep = abs(dy) > abs(dx) # Rotate line if is_steep: x1, y1 = y1, x1 x2, y2 = y2, x2 # Swap start and end points if necessary if x1 > x2: x1, x2 = x2, x1 y1, y2 = y2, y1 # Recalculate differentials dx = x2 - x1 dy = y2 - y1 # Calculate error error = dx >> 1 ystep = 1 if y1 < y2 else -1 y = y1 # Calcualte minimum and maximum x values for x in range(x1, x2 + 1): if is_steep: if x in xdict: xdict[x] = [min(y, xdict[x][0]), max(y, xdict[x][1])] else: xdict[x] = [y, y] else: if y in xdict: xdict[y] = [min(x, xdict[y][0]), max(x, xdict[y][1])] else: xdict[y] = [x, x] error -= abs(dy) if error < 0: y += ystep error += dx x1, y1 = xprev, yprev # Fill polygon for y, x in xdict.items(): self.draw_hline(x[0], y, x[1] - x[0] + 2, color) def fill_vrect(self, x, y, w, h, color): """Draw a filled rectangle (optimized for vertical drawing). Args: x (int): Starting X position. y (int): Starting Y position. w (int): Width of rectangle. h (int): Height of rectangle. color (int): RGB565 color value. """ if self.is_off_grid(x, y, x + w - 1, y + h - 1): return chunk_width = 1024 // h chunk_count, remainder = divmod(w, chunk_width) chunk_size = chunk_width * h chunk_x = x if chunk_count: buf = color.to_bytes(2, 'big') * chunk_size for c in range(0, chunk_count): self.block(chunk_x, y, chunk_x + chunk_width - 1, y + h - 1, buf) chunk_x += chunk_width if remainder: buf = color.to_bytes(2, 'big') * remainder * h self.block(chunk_x, y, chunk_x + remainder - 1, y + h - 1, buf) def invert(self, enable=True): """Enables or disables inversion of display colors. Args: enable (Optional bool): True=enable, False=disable """ if enable: self.write_cmd(self.INVON) else: self.write_cmd(self.INVOFF) def is_off_grid(self, xmin, ymin, xmax, ymax): """Check if coordinates extend past display boundaries. Args: xmin (int): Minimum horizontal pixel. ymin (int): Minimum vertical pixel. xmax (int): Maximum horizontal pixel. ymax (int): Maximum vertical pixel. Returns: boolean: False = Coordinates OK, True = Error. """ if xmin < 0: print('x-coordinate: {0} below minimum of 0.'.format(xmin)) return True if ymin < 0: print('y-coordinate: {0} below minimum of 0.'.format(ymin)) return True if xmax >= self.width: print('x-coordinate: {0} above maximum of {1}.'.format( xmax, self.width - 1)) return True if ymax >= self.height: print('y-coordinate: {0} above maximum of {1}.'.format( ymax, self.height - 1)) return True return False def load_sprite(self, path, w, h): """Load sprite image. Args: path (string): Image file path. w (int): Width of image. h (int): Height of image. Notes: w x h cannot exceed 2048 on boards w/o PSRAM """ buf_size = w * h * 2 with open(path, "rb") as f: return f.read(buf_size) def reset_cpy(self): """Perform reset: Low=initialization, High=normal operation. Notes: CircuitPython implemntation """ self.rst.value = False sleep(.05) self.rst.value = True sleep(.05) def reset_mpy(self): """Perform reset: Low=initialization, High=normal operation. Notes: MicroPython implemntation """ self.rst(0) sleep(.05) self.rst(1) sleep(.05) def scroll(self, y): """Scroll display vertically. Args: y (int): Number of pixels to scroll display. """ self.write_cmd(self.VSCRSADD, y >> 8, y & 0xFF) def set_scroll(self, top, bottom): """Set the height of the top and bottom scroll margins. Args: top (int): Height of top scroll margin bottom (int): Height of bottom scroll margin """ if top + bottom <= self.height: middle = self.height - (top + bottom) self.write_cmd(self.VSCRDEF, top >> 8, top & 0xFF, middle >> 8, middle & 0xFF, bottom >> 8, bottom & 0xFF) def sleep(self, enable=True): """Enters or exits sleep mode. Args: enable (bool): True (default)=Enter sleep mode, False=Exit sleep """ if enable: self.write_cmd(self.SLPIN) else: self.write_cmd(self.SLPOUT) def write_cmd_mpy(self, command, *args): """Write command to OLED (MicroPython). Args: command (byte): ILI9341 command code. *args (optional bytes): Data to transmit. """ self.dc(0) self.cs(0) self.spi.write(bytearray([command])) self.cs(1) # Handle any passed data if len(args) > 0: self.write_data(bytearray(args)) def write_cmd_cpy(self, command, *args): """Write command to OLED (CircuitPython). Args: command (byte): ILI9341 command code. *args (optional bytes): Data to transmit. """ self.dc.value = False self.cs.value = False # Confirm SPI locked before writing while not self.spi.try_lock(): pass self.spi.write(bytearray([command])) self.spi.unlock() self.cs.value = True # Handle any passed data if len(args) > 0: self.write_data(bytearray(args)) def write_data_mpy(self, data): """Write data to OLED (MicroPython). Args: data (bytes): Data to transmit. """ self.dc(1) self.cs(0) self.spi.write(data) self.cs(1) def write_data_cpy(self, data): """Write data to OLED (CircuitPython). Args: data (bytes): Data to transmit. """ self.dc.value = True self.cs.value = False # Confirm SPI locked before writing while not self.spi.try_lock(): pass self.spi.write(data) self.spi.unlock() self.cs.value = True 3.1.1 绘制文本要绘制文本的话,我们还需要加装一个用于渲染字符的字体库(xglcd_font.py): 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169"""XGLCD Font Utility."""from math import ceil, floorclass XglcdFont(object): """Font data in X-GLCD format. Attributes: letters: A bytearray of letters (columns consist of bytes) width: Maximum pixel width of font height: Pixel height of font start_letter: ASCII number of first letter height_bytes: How many bytes comprises letter height Note: Font files can be generated with the free version of MikroElektronika GLCD Font Creator: www.mikroe.com/glcd-font-creator The font file must be in X-GLCD 'C' format. To save text files from this font creator program in Win7 or higher you must use XP compatibility mode or you can just use the clipboard. """ # Dict to tranlate bitwise values to byte position BIT_POS = {1: 0, 2: 2, 4: 4, 8: 6, 16: 8, 32: 10, 64: 12, 128: 14, 256: 16} def __init__(self, path, width, height, start_letter=32, letter_count=96): """Constructor for X-GLCD Font object. Args: path (string): Full path of font file width (int): Maximum width in pixels of each letter height (int): Height in pixels of each letter start_letter (int): First ACII letter. Default is 32. letter_count (int): Total number of letters. Default is 96. """ self.width = width self.height = max(height, 8) self.start_letter = start_letter self.letter_count = letter_count self.bytes_per_letter = (floor( (self.height - 1) / 8) + 1) * self.width + 1 self.__load_xglcd_font(path) def __load_xglcd_font(self, path): """Load X-GLCD font data from text file. Args: path (string): Full path of font file. """ bytes_per_letter = self.bytes_per_letter # Buffer to hold letter byte values self.letters = bytearray(bytes_per_letter * self.letter_count) mv = memoryview(self.letters) offset = 0 with open(path, 'r') as f: for line in f: # Skip lines that do not start with hex values line = line.strip() if len(line) == 0 or line[0:2] != '0x': continue # Remove comments comment = line.find('//') if comment != -1: line = line[0:comment].strip() # Remove trailing commas if line.endswith(','): line = line[0:len(line) - 1] # Convert hex strings to bytearray and insert in to letters mv[offset: offset + bytes_per_letter] = bytearray( int(b, 16) for b in line.split(',')) offset += bytes_per_letter def lit_bits(self, n): """Return positions of 1 bits only.""" while n: b = n & (~n+1) yield self.BIT_POS[b] n ^= b def get_letter(self, letter, color, background=0, landscape=False): """Convert letter byte data to pixels. Args: letter (string): Letter to return (must exist within font). color (int): RGB565 color value. background (int): RGB565 background color (default: black). landscape (bool): Orientation (default: False = portrait) Returns: (bytearray): Pixel data. (int, int): Letter width and height. """ # Get index of letter letter_ord = ord(letter) - self.start_letter # Confirm font contains letter if letter_ord >= self.letter_count: print('Font does not contain character: ' + letter) return b'', 0, 0 bytes_per_letter = self.bytes_per_letter offset = letter_ord * bytes_per_letter mv = memoryview(self.letters[offset:offset + bytes_per_letter]) # Get width of letter (specified by first byte) letter_width = mv[0] letter_height = self.height # Get size in bytes of specified letter letter_size = letter_height * letter_width # Create buffer (double size to accommodate 16 bit colors) if background: buf = bytearray(background.to_bytes(2, 'big') * letter_size) else: buf = bytearray(letter_size * 2) msb, lsb = color.to_bytes(2, 'big') if landscape: # Populate buffer in order for landscape pos = (letter_size * 2) - (letter_height * 2) lh = letter_height # Loop through letter byte data and convert to pixel data for b in mv[1:]: # Process only colored bits for bit in self.lit_bits(b): buf[bit + pos] = msb buf[bit + pos + 1] = lsb if lh > 8: # Increment position by double byte pos += 16 lh -= 8 else: # Descrease position to start of previous column pos -= (letter_height * 4) - (lh * 2) lh = letter_height else: # Populate buffer in order for portrait col = 0 # Set column to first column bytes_per_letter = ceil(letter_height / 8) letter_byte = 0 # Loop through letter byte data and convert to pixel data for b in mv[1:]: # Process only colored bits segment_size = letter_byte * letter_width * 16 for bit in self.lit_bits(b): pos = (bit * letter_width) + (col * 2) + segment_size buf[pos] = msb pos = (bit * letter_width) + (col * 2) + 1 + segment_size buf[pos] = lsb letter_byte += 1 if letter_byte + 1 > bytes_per_letter: col += 1 letter_byte = 0 return buf, letter_width, letter_height def measure_text(self, text, spacing=1): """Measure length of text string in pixels. Args: text (string): Text string to measure spacing (optional int): Pixel spacing between letters. Default: 1. Returns: int: length of text """ length = 0 for letter in text: # Get index of letter letter_ord = ord(letter) - self.start_letter offset = letter_ord * self.bytes_per_letter # Add length of letter and spacing length += self.letters[offset] + spacing return length 接下来我们来尝试在屏幕上绘制文本: 123456789101112131415161718192021222324252627282930313233343536from machine import Pin, SPI, ADC, idleimport osfrom time import sleepfrom ili9341 import Display, color565from xglcd_font import XglcdFont# 初始化 SPI 总线和显示器display_spi = SPI(1, baudrate=60000000, sck=Pin(14), mosi=Pin(13))display = Display(display_spi, dc=Pin(2), cs=Pin(15), rst=Pin(15), width=320, height=240, rotation=90)# 绘制文本def draw_text(): # 颜色 white_color = color565(255, 255, 255) black_color = color565(0, 0, 0) # 设置背光 backlight = Pin(21, Pin.OUT) backlight.on() # 清除屏幕 display.clear(black_color) # 绘制文本(用法: draw_text8x8(x, y, 'text', fg_color, bg_color, rotate)) display.draw_text8x8(0, 0, 'ESP32 says hello!', white_color, black_color, 0) # 从(0,0)开始,白色字体,黑色背景,旋转角度0# 捕获异常并打印错误信息try: draw_text()except Exception as e: print('Error occured: ', e)except KeyboardInterrupt: print('Program Interrupted by the user') display.cleanup() 我们可以利用display.draw_text8x8(x, y, ‘text’, fg_color, bg_color, rotate)绘制文本,x代表x坐标、y代表有坐标、’text’表示要显示的文本、fg_color指的是文本颜色、bg_color代表的是绘制文本部分背景的颜色、rotate代表角度。 通过上述代码可以在屏幕显示文字,但这文字似乎看起来太小了,且不太符合我们平时的审美,因此,我们可以加入一个字体库来自定义字体(Unispace12x24.c): 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105//Font Generated by MikroElektronika GLCD Font Creator 1.2.0.0//MikroElektronika 2011 //http://www.mikroe.com //GLCD FontName : Unispace12x24//GLCD FontSize : 12 x 24 (Fixed Width)const unsigned short Unispace12x24[] = { 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x7F, 0x07, 0xF8, 0x7F, 0x07, 0xF8, 0x7F, 0x07, 0xF8, 0x7F, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char ! 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char " 0x0B, 0x00, 0x00, 0x00, 0x80, 0x61, 0x00, 0x80, 0x61, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x80, 0x61, 0x00, 0x80, 0x61, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x80, 0x61, 0x00, 0x80, 0x61, 0x00, 0x00, 0x00, 0x00, // Code for char # 0x0C, 0x00, 0x00, 0x00, 0xE0, 0x07, 0x01, 0xF0, 0x0F, 0x01, 0x70, 0x0E, 0x01, 0x30, 0x0C, 0x01, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x30, 0x0C, 0x01, 0x30, 0x0C, 0x01, 0x30, 0xFC, 0x01, 0x30, 0xFC, 0x01, 0x00, 0x70, 0x00, // Code for char $ 0x0C, 0x00, 0x00, 0x00, 0xF8, 0x03, 0x00, 0x08, 0x02, 0x00, 0x08, 0x02, 0x06, 0xF8, 0xC3, 0x07, 0xE0, 0xF8, 0x00, 0x80, 0x1F, 0x00, 0xF0, 0xF1, 0x07, 0x38, 0x30, 0x04, 0x00, 0x10, 0x04, 0x00, 0xF0, 0x07, 0x00, 0xE0, 0x03, // Code for char % 0x0B, 0x00, 0x00, 0x00, 0xE0, 0xF9, 0x01, 0xF8, 0xFF, 0x03, 0xF8, 0xFF, 0x07, 0x18, 0x06, 0x06, 0x18, 0x06, 0x06, 0x18, 0x06, 0x06, 0x18, 0xFE, 0x07, 0x18, 0xFE, 0x07, 0x00, 0x06, 0x06, 0x00, 0x06, 0x06, 0x00, 0x00, 0x00, // Code for char & 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char ' 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x3F, 0xFE, 0xFF, 0x7F, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char ( 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x07, 0x00, 0xE0, 0xFE, 0xFF, 0x7F, 0xFC, 0xFF, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char ) 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x90, 0x01, 0x00, 0xB8, 0x01, 0x00, 0xF0, 0x07, 0x00, 0xE0, 0x07, 0x00, 0xF8, 0x01, 0x00, 0xD8, 0x03, 0x00, 0x60, 0x07, 0x00, 0x60, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char * 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0xC0, 0x7F, 0x00, 0xC0, 0x7F, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, // Code for char + 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char , 0x0B, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, // Code for char - 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x07, 0x00, 0x00, 0x07, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char . 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x07, 0x00, 0xC0, 0x03, 0x00, 0xF0, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x0F, 0x00, 0xC0, 0x03, 0x00, 0xE0, 0x01, 0x00, 0x78, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char / 0x0B, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x01, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x18, 0xE0, 0x07, 0x18, 0xFC, 0x06, 0x98, 0x1F, 0x06, 0xF8, 0x03, 0x06, 0xF8, 0x00, 0x07, 0xF8, 0xFF, 0x07, 0xF0, 0xFF, 0x03, 0x00, 0x00, 0x00, // Code for char 0 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, // Code for char 1 0x0B, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x18, 0xFC, 0x07, 0x18, 0xFC, 0x07, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0xF8, 0x0F, 0x06, 0xF8, 0x0F, 0x06, 0xF0, 0x07, 0x06, 0x00, 0x00, 0x00, // Code for char 2 0x0B, 0x00, 0x00, 0x00, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0xF8, 0xFF, 0x07, 0xF0, 0xFF, 0x07, 0xE0, 0xF3, 0x03, 0x00, 0x00, 0x00, // Code for char 3 0x0B, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x00, 0x00, 0xFC, 0x00, 0x00, 0xDF, 0x00, 0xC0, 0xC7, 0x00, 0xF0, 0xC1, 0x00, 0x78, 0xC0, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, // Code for char 4 0x0B, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x06, 0xF8, 0x07, 0x06, 0xF8, 0x07, 0x06, 0x18, 0x06, 0x06, 0x18, 0x06, 0x06, 0x18, 0x06, 0x06, 0x18, 0x06, 0x06, 0x18, 0xFE, 0x07, 0x18, 0xFE, 0x07, 0x00, 0xFC, 0x01, 0x00, 0x00, 0x00, // Code for char 5 0x0B, 0x00, 0x00, 0x00, 0xF0, 0xFF, 0x03, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x18, 0x06, 0x06, 0x18, 0x06, 0x06, 0x18, 0x06, 0x06, 0x18, 0x06, 0x06, 0x18, 0xFE, 0x07, 0x00, 0xFE, 0x07, 0x00, 0xFC, 0x03, 0x00, 0x00, 0x00, // Code for char 6 0x0C, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x04, 0x18, 0x00, 0x07, 0x18, 0xE0, 0x07, 0x18, 0xF8, 0x03, 0x18, 0xFF, 0x00, 0xD8, 0x1F, 0x00, 0xF8, 0x07, 0x00, 0xF8, 0x00, 0x00, 0x38, 0x00, 0x00, 0x08, 0x00, 0x00, // Code for char 7 0x0B, 0x00, 0x00, 0x00, 0xE0, 0xFB, 0x03, 0xF0, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF0, 0xFB, 0x03, 0x00, 0x00, 0x00, // Code for char 8 0x0B, 0x00, 0x00, 0x00, 0xF0, 0x07, 0x00, 0xF8, 0x0F, 0x06, 0xF8, 0x0F, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF0, 0xFF, 0x03, 0x00, 0x00, 0x00, // Code for char 9 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x07, 0x00, 0x0E, 0x07, 0x00, 0x0E, 0x07, 0x00, 0x0E, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char : 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x7E, 0x00, 0x0E, 0x7E, 0x00, 0x0E, 0x7E, 0x00, 0x0E, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char ; 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x1F, 0x00, 0x80, 0x39, 0x00, 0xC0, 0x71, 0x00, 0xE0, 0xE0, 0x00, 0x70, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char < 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x19, 0x00, 0x80, 0x19, 0x00, 0x80, 0x19, 0x00, 0x80, 0x19, 0x00, 0x80, 0x19, 0x00, 0x80, 0x19, 0x00, 0x80, 0x19, 0x00, 0x80, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char = 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0xC0, 0x01, 0x60, 0xE0, 0x00, 0xC0, 0x70, 0x00, 0x80, 0x31, 0x00, 0x00, 0x1B, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char > 0x0B, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x30, 0x07, 0x18, 0x7C, 0x07, 0x18, 0x1C, 0x07, 0x18, 0x0C, 0x07, 0x18, 0x0C, 0x00, 0xF8, 0x0F, 0x00, 0xF8, 0x0F, 0x00, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, // Code for char ? 0x0B, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x3F, 0xC0, 0xFF, 0x3F, 0xC0, 0x00, 0x30, 0xC0, 0xF8, 0x31, 0xC0, 0xFC, 0x33, 0xC0, 0x0C, 0x33, 0xC0, 0xFC, 0x33, 0xC0, 0x00, 0x33, 0xC0, 0xFF, 0x33, 0xC0, 0xFF, 0x33, 0x00, 0x00, 0x00, // Code for char @ 0x0C, 0x00, 0x00, 0x00, 0x00, 0x80, 0x07, 0x00, 0xFC, 0x07, 0xC0, 0xFF, 0x07, 0xF8, 0xFF, 0x00, 0xF8, 0xC0, 0x00, 0x78, 0xC0, 0x00, 0xF8, 0xDF, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0xFE, 0x07, 0x00, 0xC0, 0x07, 0x00, 0x00, 0x04, // Code for char A 0x0B, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xE0, 0xF3, 0x03, 0x00, 0x00, 0x00, // Code for char B 0x0B, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x00, 0xF0, 0xFF, 0x03, 0xF8, 0xFF, 0x07, 0x38, 0x00, 0x07, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x00, 0x00, 0x00, // Code for char C 0x0B, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF0, 0xFF, 0x03, 0x00, 0x00, 0x00, // Code for char D 0x0B, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x00, 0x00, 0x00, // Code for char E 0x0B, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x18, 0x0C, 0x00, 0x18, 0x0C, 0x00, 0x18, 0x0C, 0x00, 0x18, 0x0C, 0x00, 0x18, 0x0C, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char F 0x0B, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x01, 0xF0, 0xFF, 0x03, 0xF8, 0xFF, 0x07, 0x38, 0x00, 0x07, 0x18, 0x00, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0xFC, 0x07, 0x18, 0xFC, 0x07, 0x18, 0xFC, 0x07, 0x00, 0x00, 0x00, // Code for char G 0x0B, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x00, 0x0C, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0C, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x00, 0x00, 0x00, // Code for char H 0x0B, 0x00, 0x00, 0x00, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x00, 0x00, 0x00, // Code for char I 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x03, 0xF8, 0xFF, 0x01, 0x00, 0x00, 0x00, // Code for char J 0x0B, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x00, 0x0E, 0x00, 0x80, 0x3F, 0x00, 0xC0, 0xFF, 0x00, 0xF0, 0xF1, 0x03, 0x78, 0xE0, 0x07, 0x38, 0x80, 0x07, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, // Code for char K 0x0B, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, // Code for char L 0x0B, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0x07, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0xF0, 0x07, 0x00, 0xE0, 0x07, 0x80, 0xFF, 0x03, 0xF8, 0x0F, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x00, 0x00, 0x00, // Code for char M 0x0B, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0x01, 0x00, 0xF0, 0x07, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xF0, 0x03, 0x00, 0xC0, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x00, 0x00, 0x00, // Code for char N 0x0B, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x01, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF0, 0xFF, 0x03, 0x00, 0x00, 0x00, // Code for char O 0x0B, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0xF8, 0x1F, 0x00, 0xF8, 0x1F, 0x00, 0xE0, 0x0F, 0x00, 0x00, 0x00, 0x00, // Code for char P 0x0B, 0x00, 0x00, 0x00, 0xE0, 0xFF, 0x01, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x18, 0x00, 0x06, 0x18, 0x00, 0x06, 0x18, 0x00, 0x1E, 0x18, 0x00, 0x1E, 0xF8, 0xFF, 0x17, 0xF8, 0xFF, 0x07, 0xF0, 0xFF, 0x03, 0x00, 0x00, 0x00, // Code for char Q 0x0B, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x18, 0x0C, 0x00, 0x18, 0x0C, 0x00, 0x18, 0x0C, 0x00, 0x18, 0x0C, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF0, 0xF3, 0x07, 0x00, 0x00, 0x00, // Code for char R 0x0B, 0x00, 0x00, 0x00, 0xE0, 0x03, 0x06, 0xF0, 0x07, 0x06, 0xF8, 0x0F, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0x0C, 0x06, 0x18, 0xFC, 0x07, 0x18, 0xFC, 0x07, 0x18, 0xF8, 0x03, 0x00, 0x00, 0x00, // Code for char S 0x0B, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char T 0x0B, 0x00, 0x00, 0x00, 0xF8, 0xFF, 0x01, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x03, 0x00, 0x00, 0x00, // Code for char U 0x0C, 0x00, 0x00, 0x00, 0x78, 0x00, 0x00, 0xF8, 0x0F, 0x00, 0xF8, 0xFF, 0x00, 0x00, 0xFF, 0x07, 0x00, 0xC0, 0x07, 0x00, 0x80, 0x07, 0x00, 0xFE, 0x07, 0xF0, 0xFF, 0x00, 0xF8, 0x0F, 0x00, 0xF8, 0x00, 0x00, 0x08, 0x00, 0x00, // Code for char V 0x0C, 0x38, 0x00, 0x00, 0xF8, 0xFF, 0x00, 0xF8, 0xFF, 0x07, 0x00, 0xFE, 0x07, 0x00, 0xE0, 0x07, 0x80, 0xFF, 0x00, 0x80, 0x3F, 0x00, 0x00, 0xF8, 0x07, 0x00, 0xE0, 0x07, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x03, 0xF8, 0x01, 0x00, // Code for char W 0x0C, 0x00, 0x00, 0x00, 0x18, 0x00, 0x06, 0x78, 0x80, 0x07, 0xF8, 0xE1, 0x07, 0xF0, 0xFF, 0x01, 0x80, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0xC0, 0xFF, 0x01, 0xF0, 0xF3, 0x07, 0xF8, 0xC0, 0x07, 0x38, 0x00, 0x07, 0x08, 0x00, 0x04, // Code for char X 0x0C, 0x08, 0x00, 0x00, 0x38, 0x00, 0x00, 0xF8, 0x00, 0x00, 0xF8, 0x03, 0x00, 0xE0, 0xFF, 0x07, 0x00, 0xFF, 0x07, 0x00, 0xFE, 0x07, 0xC0, 0xFF, 0x07, 0xF8, 0x07, 0x00, 0xF8, 0x00, 0x00, 0x38, 0x00, 0x00, 0x08, 0x00, 0x00, // Code for char Y 0x0B, 0x00, 0x00, 0x00, 0x18, 0x00, 0x06, 0x18, 0x80, 0x07, 0x18, 0xE0, 0x07, 0x18, 0xF8, 0x07, 0x18, 0x7E, 0x06, 0x18, 0x3F, 0x06, 0xD8, 0x0F, 0x06, 0xF8, 0x03, 0x06, 0xF8, 0x00, 0x06, 0x38, 0x00, 0x06, 0x00, 0x00, 0x00, // Code for char Z 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x03, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char [ 0x0B, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x38, 0x00, 0x00, 0xF0, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x3C, 0x00, 0x00, 0xF0, 0x00, 0x00, 0xC0, 0x03, 0x00, 0x80, 0x07, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, // Code for char BackSlash 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char ] 0x0C, 0x00, 0x02, 0x00, 0x00, 0x03, 0x00, 0x80, 0x01, 0x00, 0xC0, 0x00, 0x00, 0x60, 0x00, 0x00, 0x38, 0x00, 0x00, 0x18, 0x00, 0x00, 0x70, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x80, 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x02, 0x00, // Code for char ^ 0x0C, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, 0x00, 0x00, 0xE0, // Code for char _ 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00, 0x18, 0x00, 0x00, 0x10, 0x00, 0x00, 0x10, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char ` 0x0B, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x03, 0xC0, 0xF8, 0x07, 0xC0, 0xF8, 0x07, 0xC0, 0x18, 0x06, 0xC0, 0x18, 0x06, 0xC0, 0x18, 0x06, 0xC0, 0x18, 0x06, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0x80, 0xFF, 0x07, 0x00, 0x00, 0x00, // Code for char a 0x0B, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x07, 0xFC, 0xFF, 0x07, 0xFC, 0xFF, 0x07, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0x80, 0xFF, 0x03, 0x00, 0x00, 0x00, // Code for char b 0x0B, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x01, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0xC0, 0x00, 0x07, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0x00, 0x00, 0x00, // Code for char c 0x0B, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x03, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0xC0, 0x00, 0x07, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xFC, 0xFF, 0x07, 0xFC, 0xFF, 0x07, 0xFC, 0xFF, 0x07, 0x00, 0x00, 0x00, // Code for char d 0x0B, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x03, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0xC0, 0x30, 0x06, 0xC0, 0x30, 0x06, 0xC0, 0x30, 0x06, 0xC0, 0x30, 0x06, 0xC0, 0x39, 0x06, 0xC0, 0x3F, 0x06, 0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, // Code for char e 0x0B, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x07, 0xFC, 0xFF, 0x07, 0xFC, 0xFF, 0x07, 0xCC, 0x00, 0x00, 0xCC, 0x00, 0x00, 0xCC, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char f 0x0B, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x03, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0xC7, 0xC0, 0x00, 0xC6, 0xC0, 0x00, 0xC6, 0xC0, 0x00, 0xC6, 0xC0, 0x00, 0xC6, 0xC0, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xC0, 0xFF, 0x7F, 0x00, 0x00, 0x00, // Code for char g 0x0B, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x07, 0xFC, 0xFF, 0x07, 0xFC, 0xFF, 0x07, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0x80, 0xFF, 0x07, 0x00, 0x00, 0x00, // Code for char h 0x0B, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xCC, 0x00, 0x06, 0xCC, 0xFF, 0x07, 0xCC, 0xFF, 0x07, 0xCC, 0xFF, 0x07, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, // Code for char i 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0xC0, 0x00, 0xC0, 0xC0, 0x00, 0xC0, 0xC0, 0x00, 0xC0, 0xC0, 0x00, 0xE0, 0xCC, 0xFF, 0xFF, 0xCC, 0xFF, 0xFF, 0xCC, 0xFF, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char j 0x0C, 0x00, 0x00, 0x00, 0xFC, 0xFF, 0x07, 0xFC, 0xFF, 0x07, 0xFC, 0xFF, 0x07, 0x00, 0x7C, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xEF, 0x01, 0xC0, 0xC7, 0x03, 0xC0, 0x81, 0x07, 0xC0, 0x00, 0x06, 0x40, 0x00, 0x04, 0x00, 0x00, 0x04, // Code for char k 0x0B, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x06, 0x0C, 0x00, 0x06, 0x0C, 0x00, 0x06, 0x0C, 0x00, 0x06, 0xFC, 0xFF, 0x07, 0xFC, 0xFF, 0x07, 0xFC, 0xFF, 0x07, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, // Code for char l 0x0B, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0xC0, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0x80, 0xFF, 0x07, 0x00, 0x00, 0x00, // Code for char m 0x0B, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0x80, 0xFF, 0x07, 0x00, 0x00, 0x00, // Code for char n 0x0C, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x03, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0x80, 0xFF, 0x03, 0x00, 0xFE, 0x00, // Code for char o 0x0B, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0x80, 0xFF, 0x03, 0x00, 0x00, 0x00, // Code for char p 0x0B, 0x00, 0x00, 0x00, 0x80, 0xFF, 0x03, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0xC0, 0xFF, 0xFF, 0x00, 0x00, 0x00, // Code for char q 0x0B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0x80, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0xC0, 0x01, 0x00, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char r 0x0C, 0x00, 0x00, 0x00, 0x80, 0x0F, 0x06, 0xC0, 0x1F, 0x06, 0xC0, 0x1D, 0x06, 0xC0, 0x18, 0x06, 0xC0, 0x18, 0x06, 0xC0, 0x18, 0x06, 0xC0, 0x18, 0x06, 0xC0, 0x18, 0x06, 0xC0, 0xF8, 0x07, 0xC0, 0xF0, 0x03, 0x00, 0xC0, 0x00, // Code for char s 0x0B, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0xF8, 0xFF, 0x03, 0xF8, 0xFF, 0x07, 0xF8, 0xFF, 0x07, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char t 0x0B, 0x00, 0x00, 0x00, 0xC0, 0xFF, 0x03, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0x00, 0x00, 0x06, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x07, 0x00, 0x00, 0x00, // Code for char u 0x0C, 0x40, 0x00, 0x00, 0xC0, 0x03, 0x00, 0xC0, 0x1F, 0x00, 0xC0, 0xFF, 0x01, 0x00, 0xFC, 0x07, 0x00, 0xC0, 0x07, 0x00, 0x80, 0x07, 0x00, 0xF8, 0x07, 0x80, 0xFF, 0x03, 0xC0, 0x3F, 0x00, 0xC0, 0x07, 0x00, 0x40, 0x00, 0x00, // Code for char v 0x0C, 0xC0, 0x00, 0x00, 0xC0, 0xFF, 0x01, 0xC0, 0xFF, 0x07, 0x00, 0xF0, 0x07, 0x00, 0xF0, 0x07, 0x00, 0xFE, 0x01, 0x00, 0xFE, 0x00, 0x00, 0xF8, 0x07, 0x00, 0xC0, 0x07, 0xC0, 0xFF, 0x07, 0xC0, 0xFF, 0x03, 0xC0, 0x03, 0x00, // Code for char w 0x0C, 0x40, 0x00, 0x04, 0xC0, 0x00, 0x06, 0xC0, 0x01, 0x07, 0xC0, 0xC7, 0x07, 0x00, 0xFF, 0x01, 0x00, 0xFE, 0x00, 0x00, 0x7C, 0x00, 0x00, 0xFF, 0x01, 0x80, 0xC7, 0x07, 0xC0, 0x81, 0x07, 0xC0, 0x00, 0x06, 0x40, 0x00, 0x04, // Code for char x 0x0C, 0x40, 0x00, 0x00, 0xC0, 0x01, 0x00, 0xC0, 0x0F, 0x00, 0xC0, 0x7F, 0xC0, 0x00, 0xFE, 0xF3, 0x00, 0xF0, 0xFF, 0x00, 0xC0, 0x1F, 0x00, 0xF0, 0x07, 0x00, 0xFF, 0x00, 0xC0, 0x1F, 0x00, 0xC0, 0x03, 0x00, 0x40, 0x00, 0x00, // Code for char y 0x0B, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x06, 0xC0, 0x00, 0x07, 0xC0, 0xC0, 0x07, 0xC0, 0xE0, 0x07, 0xC0, 0xF8, 0x06, 0xC0, 0x7E, 0x06, 0xC0, 0x1F, 0x06, 0xC0, 0x0F, 0x06, 0xC0, 0x03, 0x06, 0xC0, 0x00, 0x06, 0x00, 0x00, 0x00, // Code for char z 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x18, 0x00, 0xF8, 0xFF, 0x1F, 0xFE, 0xEF, 0x7F, 0xFE, 0xC3, 0x7F, 0x03, 0x00, 0xC0, 0x01, 0x00, 0x80, 0x01, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char { 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char | 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x80, 0x01, 0x00, 0x80, 0x03, 0x00, 0xC0, 0xFE, 0xC3, 0xFF, 0xFE, 0xE7, 0x7F, 0xFC, 0xFF, 0x3F, 0x00, 0x18, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char } 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x06, 0x00, 0x00, 0x02, 0x00, 0x00, 0x06, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Code for char ~ 0x05, 0x80, 0xFF, 0x03, 0x80, 0xFF, 0x03, 0x80, 0x00, 0x02, 0x80, 0xFF, 0x03, 0x80, 0xFF, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // Code for char }; 接下来我们要在原先的draw_text()函数中小小的修改一下,首先加载这个字体库:unispace_font = XglcdFont(‘Unispace12x24.c’, 12, 24),如果你是将这个字体库放入到一个文件夹内的话,可以修改成unispace_font = XglcdFont(‘font/Unispace12x24.c’, 12, 24)。 然后修改绘制文本的代码:display.draw_text(0, 10, ‘ESP32 says hello!’, unispace_font, white_color, black_color),你会发现在原先的代码上只是加入了字体参数“unispace_font”。需要注意的是,加载字体库要优先于绘制文本(说白了就是先加载后绘制)。运行后会发现加载了字体的文本比原先的要大一些,这就说明我们自定义的文本生效了。 3.1.2 绘制图形绘制图形不像绘制文本那样复杂,并不需要加装字符渲染库和字体库,ili9341库自带绘制图形的功能,我们只需要在调用时添加参数就行。以下是个绘制图形的示例代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109from machine import Pin, SPI, ADC, idle,SoftSPIimport osfrom time import sleepfrom ili9341 import Display, color565from xglcd_font import XglcdFontdisplay_spi = SPI(1, baudrate=60000000, sck=Pin(14), mosi=Pin(13))display = Display(display_spi, dc=Pin(2), cs=Pin(15), rst=Pin(15), width=320, height=240, rotation=90)print('Display height: ' + str(display.height))print('Display width: ' + str(display.width))white_color = color565(255, 255, 255) # whiteblack_color = color565(0, 0, 0) # Blackbacklight = Pin(21, Pin.OUT)backlight.on()display.clear(black_color)def draw_shapes(): # 在(10, 40)位置画一条长度为70的水平线,颜色为紫色 display.draw_hline(10, 40, 70, color565(255, 0, 255)) sleep(1) # 在(10, 0)位置画一条长度为40的垂直线,颜色为青色 display.draw_vline(10, 0, 40, color565(0, 255, 255)) sleep(1) # 在(23, 50)位置填充一个宽度为30,高度为75的矩形,颜色为白色 display.fill_hrect(23, 50, 30, 75, color565(255, 255, 255)) sleep(1) # 在(0, 0)位置画一条长度为100的水平线,颜色为红色 display.draw_hline(0, 0, 100, color565(255, 0, 0)) sleep(1) # 在(50, 0)和(64, 40)之间画一条线,颜色为黄色 display.draw_line(50, 0, 64, 40, color565(255, 255, 0)) sleep(2) # 清除显示内容 display.clear() # 绘制一个由多个点组成的折线,颜色为青色 coords = [[0, 63], [78, 80], [122, 92], [50, 50], [78, 15], [0, 63]] display.draw_lines(coords, color565(0, 255, 255)) sleep(1) # 清除显示内容 display.clear() # 填充一个多边形,颜色为绿色 display.fill_polygon(7, 120, 120, 100, color565(0, 255, 0)) sleep(1) # 填充一个矩形,颜色为红色 display.fill_rectangle(0, 0, 15, 227, color565(255, 0, 0)) sleep(1) # 清除显示内容 display.clear() # 填充一个矩形,颜色为蓝色 display.fill_rectangle(0, 0, 163, 163, color565(128, 128, 255)) sleep(1) # 画一个矩形框,颜色为紫色 display.draw_rectangle(0, 64, 163, 163, color565(255, 0, 255)) sleep(1) # 填充一个矩形,颜色为品红色 display.fill_rectangle(64, 0, 163, 163, color565(128, 0, 255)) sleep(1) # 画一个旋转15度的多边形,颜色为蓝色 display.draw_polygon(3, 120, 110, 30, color565(0, 64, 255), rotate=15) sleep(3) # 清除显示内容 display.clear() # 填充一个圆,颜色为绿色 display.fill_circle(132, 132, 70, color565(0, 255, 0)) sleep(1) # 画一个圆,颜色为蓝色 display.draw_circle(132, 96, 70, color565(0, 0, 255)) sleep(1) # 填充一个椭圆,颜色为红色 display.fill_ellipse(96, 96, 30, 16, color565(255, 0, 0)) sleep(1) # 画一个椭圆,颜色为黄色 display.draw_ellipse(96, 85, 16, 30, color565(255, 255, 0)) sleep(5) # 清理显示资源 display.cleanup()try: draw_shapes() except Exception as e: print('Error occured: ', e)except KeyboardInterrupt: print('Program Interrupted by the user') finally: display.clear() 3.1.3 绘制图片如何在这块屏幕上绘制一个图片(或者照片),巧的是,ili9341库自带了一个绘制图像的功能。但是要利用这个功能绘制图像的话,这个图像必须转换为.raw类型。 接下来我们通过一个小工具来将平时我们常见的png、jpg类型的图片转换为raw(img2rgb565.py): 1234567891011121314151617181920212223242526272829303132333435363738394041# -*- coding: utf-8 -*-"""Utility to convert images to raw RGB565 format.Usage: python img2rgb565.py <your_image> <your_image> is the full path to the image file you want to convert."""from PIL import Imagefrom struct import packfrom os import pathimport sysdef error(msg): """Display error and exit.""" print (msg) sys.exit(-1)def write_bin(f, pixel_list): """Save image in RGB565 format.""" for pix in pixel_list: r = (pix[0] >> 3) & 0x1F g = (pix[1] >> 2) & 0x3F b = (pix[2] >> 3) & 0x1F f.write(pack('>H', (r << 11) + (g << 5) + b))if __name__ == '__main__': args = sys.argv if len(args) != 2: error('Please specify input file: ./img2rgb565.py test.png') in_path = args[1] if not path.exists(in_path): error('File Not Found: ' + in_path) filename, ext = path.splitext(in_path) out_path = filename + '.raw' img = Image.open(in_path).convert('RGB') pixels = list(img.getdata()) with open(out_path, 'wb') as f: write_bin(f, pixels) print('Saved: ' + out_path) 在正式使用工具之前,请确保你电脑安装了python,并且安装好了该程序需要的第三方库。接下来,将你想要转换的图片置于程序同级目录下。然后打开命令提示符,输入python img2rgb565.py xxx.png回车后就能得到转换后的图片了。 接下来将这个raw类型的图片文件上传至esp32,然后利用 12def load_image(): display.draw_image('xxx.raw', 0, 0, 128, 128) 在屏幕(0,0)处显示此图像。需要注意的是,128,128指的是图片的尺寸,需要根据实际情况填写,如果你选择的图片是100x200,那么你应该填入100,200,填入其它参数会导致图像绘制失败。同时也别让图像超过屏幕的分辨率,不然也会导致图像绘制失败。 3.2 触摸屏电阻式触摸屏通过在屏幕表面覆盖一层电阻膜来实现触摸功能。当手指或触摸笔按压屏幕时,会改变电阻膜的电阻值,从而产生一个电信号。XPT2046 控制器负责检测这个电信号,并将其转换为触摸点的坐标信息。然后,微控制器可以通过读取这些坐标信息来确定触摸位置,并据此执行相应的操作。 ESP32-CYD的屏幕自带触摸的功能,它采用了xpt2046芯片作为触屏驱动,因此我们需要选择一个合适的驱动库(xpt2046.py): 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135"""XPT2046 Touch module."""from time import sleepfrom micropython import constclass Touch(object): """Serial interface for XPT2046 Touch Screen Controller.""" # Command constants from ILI9341 datasheet GET_X = const(0b11010000) # X position GET_Y = const(0b10010000) # Y position GET_Z1 = const(0b10110000) # Z1 position GET_Z2 = const(0b11000000) # Z2 position GET_TEMP0 = const(0b10000000) # Temperature 0 GET_TEMP1 = const(0b11110000) # Temperature 1 GET_BATTERY = const(0b10100000) # Battery monitor GET_AUX = const(0b11100000) # Auxiliary input to ADC def __init__(self, spi, cs, int_pin=None, int_handler=None, width=240, height=320, x_min=100, x_max=1962, y_min=100, y_max=1900): """Initialize touch screen controller. Args: spi (Class Spi): SPI interface for OLED cs (Class Pin): Chip select pin int_pin (Class Pin): Touch controller interrupt pin int_handler (function): Handler for screen interrupt width (int): Width of LCD screen height (int): Height of LCD screen x_min (int): Minimum x coordinate x_max (int): Maximum x coordinate y_min (int): Minimum Y coordinate y_max (int): Maximum Y coordinate """ self.spi = spi self.cs = cs self.cs.init(self.cs.OUT, value=1) self.rx_buf = bytearray(3) # Receive buffer self.tx_buf = bytearray(3) # Transmit buffer self.width = width self.height = height # Set calibration self.x_min = x_min self.x_max = x_max self.y_min = y_min self.y_max = y_max self.x_multiplier = width / (x_max - x_min) self.x_add = x_min * -self.x_multiplier self.y_multiplier = height / (y_max - y_min) self.y_add = y_min * -self.y_multiplier if int_pin is not None: self.int_pin = int_pin self.int_pin.init(int_pin.IN) self.int_handler = int_handler self.int_locked = False int_pin.irq(trigger=int_pin.IRQ_FALLING | int_pin.IRQ_RISING, handler=self.int_press) def get_touch(self): """Take multiple samples to get accurate touch reading.""" timeout = 2 # set timeout to 2 seconds confidence = 5 buff = [[0, 0] for x in range(confidence)] buf_length = confidence # Require a confidence of 5 good samples buffptr = 0 # Track current buffer position nsamples = 0 # Count samples while timeout > 0: if nsamples == buf_length: meanx = sum([c[0] for c in buff]) // buf_length meany = sum([c[1] for c in buff]) // buf_length dev = sum([(c[0] - meanx)**2 + (c[1] - meany)**2 for c in buff]) / buf_length if dev <= 50: # Deviation should be under margin of 50 return self.normalize(meanx, meany) # get a new value sample = self.raw_touch() # get a touch if sample is None: nsamples = 0 # Invalidate buff else: buff[buffptr] = sample # put in buff buffptr = (buffptr + 1) % buf_length # Incr, until rollover nsamples = min(nsamples + 1, buf_length) # Incr. until max sleep(.05) timeout -= .05 return None def int_press(self, pin): """Send X,Y values to passed interrupt handler.""" if not pin.value() and not self.int_locked: self.int_locked = True # Lock Interrupt buff = self.raw_touch() if buff is not None: x, y = self.normalize(*buff) self.int_handler(x, y) sleep(.1) # Debounce falling edge elif pin.value() and self.int_locked: sleep(.1) # Debounce rising edge self.int_locked = False # Unlock interrupt def normalize(self, x, y): """Normalize mean X,Y values to match LCD screen.""" x = int(self.x_multiplier * x + self.x_add) y = int(self.y_multiplier * y + self.y_add) return x, y def raw_touch(self): """Read raw X,Y touch values. Returns: tuple(int, int): X, Y """ x = self.send_command(self.GET_X) y = self.send_command(self.GET_Y) if self.x_min <= x <= self.x_max and self.y_min <= y <= self.y_max: return (x, y) else: return None def send_command(self, command): """Write command to XT2046 (MicroPython). Args: command (byte): XT2046 command code. Returns: int: 12 bit response """ self.tx_buf[0] = command self.cs(0) self.spi.write_readinto(self.tx_buf, self.rx_buf) self.cs(1) return (self.rx_buf[1] << 4) | (self.rx_buf[2] >> 4) 接下来通过代码来使用触摸屏的功能: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152from machine import Pin, SPI, ADC, idleimport osfrom time import sleepfrom ili9341 import Display, color565from xpt2046 import Touchfrom xglcd_font import XglcdFontdisplay_spi = SPI(1, baudrate=60000000, sck=Pin(14), mosi=Pin(13))display = Display(display_spi, dc=Pin(2), cs=Pin(15), rst=Pin(15), width=320, height=240, rotation=90)print('Display height: ' + str(display.height))print('Display width: ' + str(display.width))white_color = color565(255, 255, 255) black_color = color565(0, 0, 0) backlight = Pin(21, Pin.OUT)backlight.on()display.clear(black_color)# 显示的中心位置显示文本font_size = 8text_msg = 'Touch screen to test'x_center = int((display.width-len(text_msg)*font_size)/2)y_center = int(((display.height)/2)-(font_size/2))display.draw_text8x8(x_center, y_center,text_msg, white_color, black_color, 0)# 初始化触摸屏touchscreen_spi = SPI(2, baudrate=1000000, sck=Pin(25), mosi=Pin(32), miso=Pin(39))# 定义触摸屏按下事件处理函数def touchscreen_press(x, y): display.clear(black_color) text_touch_coordinates = "Touch: X = " + str(x) + " | Y = " + str(y) x_center = int((display.width-len(text_touch_coordinates)*font_size)/2) display.draw_text8x8(x_center, y_center, text_touch_coordinates, white_color, black_color, 0) print("Touch: X = " + str(x) + " | Y = " + str(y))# 检测触摸touchscreen = Touch(touchscreen_spi, cs=Pin(33), int_pin=Pin(36), int_handler=touchscreen_press)try: while True: # 等待触摸屏按下事件 touchscreen.get_touch() except Exception as e: print('Error occured: ', e)except KeyboardInterrupt: print('Program Interrupted by the user') finally: display.cleanup() touchscreen = Touch(touchscreen_spi, cs=Pin(33), int_pin=Pin(36), int_handler=touchscreen_press)将在检测到触摸时运行。该库将自动传递x和y坐标分配给该函数。在我们的例子中,它将调用touchscreen_press函数。当在屏幕上检测到触摸时,touchscreen_press函数将运行,屏幕就会实时显示我们触屏的坐标。我们也可以执行任何其他任务。 4.LDRLDR(Light Dependent Resistor,光敏电阻)是一种能够根据光照强度改变其电阻值的传感器。其工作原理基于半导体材料的光电效应。它广泛应用于各种光控电路中,如自动照明系统、光敏报警器等。 我们同样可以通过代码来获取传感器参数: 123456789101112131415from machine import Pin, ADCimport osfrom time import sleeptry: # Run the event loop indefinitely while True: # Read light sensor lightsensor = ADC(34, atten=ADC.ATTN_0DB) print('LDR value: ' + str(lightsensor.read_uv())) sleep(1)except Exception as e: print('Error occured: ', e)except KeyboardInterrupt: print('Program Interrupted by the user') 5.合理扩容实际上,当你将所有上述代码部署到ESP32-CYD时,你会发现随着项目的进行,内存资源会变得越来越紧张。为了有效缓解这一问题,我们可以采取一种策略:将驱动库、图片、字体等辅助文件存储到SD卡中。这样一来,就能为主程序腾出ESP32上宝贵的存储空间,确保系统的稳定运行。 接下来,请将驱动库、图片、字体等辅助文件都转存至SD卡中,然后将其插入进ESP32中,同时在ESP32设备上,删除之前转存的所有辅助文件,以释放存储空间,只留下boot.py、sdcard.py和main.py(或者你的主程序)。 在ESP32设备的启动或重启流程中,boot.py扮演着至关重要的角色。每当ESP32启动或重启时,系统会自动执行boot.py文件一次。这使得boot.py成为初始化ESP32设备的关键代码文件。在boot.py中,通常包含了一些基础配置和初始化操作,如设置WiFi连接、挂载SD卡、配置引脚模式等。 根据这一原理,我们可以在boot.py中写以下代码,以初始化SD卡: 12345678from machine import Pin, SPI, SoftSPIimport osfrom sdcard import SDCardspisd = SoftSPI(-1, miso=Pin(19), mosi=Pin(23), sck=Pin(18))sd = SDCard(spisd, Pin(5))vfs = os.VfsFat(sd) os.mount(vfs, '/sd')os.chdir('/sd/libs') 需要注意的是,sd卡目录下的路径视情况而定,比如,在我的SD卡中,我将所有的驱动库都放入了一个名为libs的文件夹中。 在挂载了SD卡后,绘制文本、绘制图像都和之前的代码一样,因为我们已经在boot.py中初始化了有关驱动库的部分,不过对于图片或者是字体库等其它辅助文件,我们也要通过代码初始化。比如绘制一个图片: 123456def load_image(): # 切换到sd卡目录下 os.chdir('/sd') # 使用draw_image函数加载图片# 同时请注意图片的路径,注意是相对于sd卡的路径 display.draw_image('/sd/img/xxx.raw', 0, 0, 150, 110) 结合上述的所有代码,我们可以写出以下代码(大杂烩): 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374from machine import Pin, SPI, ADC, idle,SoftSPIimport osfrom time import sleep# 导入ili9341和xglcd_font模块还有xpt2046模块from ili9341 import Display, color565from xglcd_font import XglcdFontfrom xpt2046 import Touch# 初始化显示屏display_spi = SPI(1, baudrate=60000000, sck=Pin(14), mosi=Pin(13))display = Display(display_spi, dc=Pin(2), cs=Pin(15), rst=Pin(15), width=320, height=240, rotation=90)# 图片加载函数def load_image(): # 因为挂载了sd卡,所以需要先切换到sd卡目录下 os.chdir('/sd') # 然后使用draw_image函数加载图片,同时请注意图片的路径,注意是相对于sd卡的路径 display.draw_image('/sd/img/logo.raw', 80, 0, 150, 110)# 绘制图形def draw_shape(): white_color = color565(255, 255, 255) # white black_color = color565(0, 0, 0) # Black backlight = Pin(21, Pin.OUT) backlight.on() display.fill_rectangle(0, 0, 15, 240, color565(255, 0, 0)) display.draw_hline(85, 130, 150, color565(255, 151, 0)) display.fill_circle(60, 120, 10, color565(0, 0, 255))# 绘制文字def draw_text(): white_color = color565(255, 255, 255) black_color = color565(0, 0, 0) backlight = Pin(21, Pin.OUT) backlight.on() display.clear(white_color) print('Loading Unispace font...') # 挂载sd卡后,也要注意字体库的路径 unispace_font = XglcdFont('/sd/font/Unispace12x24.c', 12, 24) display.draw_text(20, 210, 'blog.goldenapplepie.xyz', unispace_font, black_color, white_color) font_size_w = unispace_font.width font_size_h = unispace_font.height text_msg = 'GoldenApplePie' x_center = int((display.width-len(text_msg)*font_size_w)/2) y_center = int(((display.height)/2)-(font_size_h/2)) display.draw_text(x_center, y_center, text_msg, unispace_font, black_color, white_color) display.draw_text(display.width-font_size_h, display.height-font_size_w, ' Pie Dream Studio', unispace_font, black_color, white_color, landscape=True)# 触摸屏测试def touch(): def touchscreen_press(x, y): print("Touch detected at", x, y) touchscreen_spi = SPI(2, baudrate=1000000, sck=Pin(25), mosi=Pin(32), miso=Pin(39)) touchscreen = Touch(touchscreen_spi, cs=Pin(33), int_pin=Pin(36), int_handler=touchscreen_press)try: #os.chdir('/sd') draw_text() draw_shape() load_image() while True: touch() passexcept Exception as e: print('Error occured: ', e)except KeyboardInterrupt: print('Program Interrupted by the user') display.cleanup()# 程序结束时,卸载SD卡finally: os.umount('/sd') 又或者,我们还可以通过ESP32的联网的功能,搭建一个http服务端,且将服务端的文件置于sd卡内: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970import networkimport socketimport timeimport machineimport os# 配置网络参数ssid = ''password = ''# 连接到WiFi网络sta_if = network.WLAN(network.STA_IF)sta_if.active(True)sta_if.connect(ssid, password)print('网络已连接,IP地址:', sta_if.ifconfig()[0])# 创建socketaddr = ('', 80) # 监听所有可用的网络接口上的80端口s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.bind(addr)s.listen(1)def http_response(client_socket, status_line, headers, content=""): # 发送HTTP状态行 client_socket.sendall(b'HTTP/1.1 ' + status_line.encode() + b'\r\n') # 遍历headers列表,逐个发送header for header in headers: client_socket.sendall(header.encode() + b'\r\n') # 发送一个空行,表示headers部分结束 client_socket.sendall(b'\r\n') # 发送响应内容 client_socket.sendall(content.encode())def serve_file(path): try: with open(path, 'r') as f: return f.read() except FileNotFoundError: return "404 Not Found"try: while True: print('等待连接...') client_socket, addr = s.accept() print('连接自:', addr) request = client_socket.recv(1024) print('收到请求:', request.decode()) # 简单的请求处理(支持GET /) if request.startswith(b'GET / HTTP/1.1'): status_line = '200 OK' headers = [ 'Content-Type: text/html', 'Connection: close' ] # 读取SD卡上的index.html文件内容 html_file_path = '/sd/web/index.html' content = serve_file(html_file_path) http_response(client_socket, status_line, headers, content) client_socket.close()except KeyboardInterrupt: s.close() print('服务器已关闭')finally: # 卸载SD卡 os.umount('/sd') 你需要注意的是html_file_path = ‘/sd/web/index.html’部分,需要根据具体的路径修改。对于ssid与password,也应根据具体情况修改。 LVGLLVGL是一个免费开源的轻量级嵌入式图形库,专为资源受限的硬件设计,仅需64KB Flash和16KB RAM即可运行,提供按钮、图表、动画等丰富组件及抗锯齿等视觉效果,支持多平台、多输入设备,并配备模拟器和可视化工具,帮助开发者快速构建智能穿戴、工业控制等领域的交互界面,其MIT开源协议和活跃社区进一步降低了开发门槛。 在这篇内容中,我们将从 Arduino 的角度探讨 LVGL 的运用方式。选择开发语言时,C/C++(Arduino 主流方案)能直接操作硬件寄存器,精准分配内存与外设资源,尤其适合资源受限的嵌入式场景;而 MicroPython 虽简化了编程门槛,但因动态类型和解释执行的特性,对硬件的控制粒度较弱,且运行时开销较大。若追求高性能、低功耗或复杂图形渲染(如动画、多屏适配),C/C++ 是更可靠的选择;若需快速验证原型或开发简单交互界面,MicroPython 则能提升开发效率。 小插曲:一开始我想用 MicroPython 快速试试 LVGL 的功能,结果发现要把原本用 C/C++ 写的 LVGL 移植到 MicroPython 上特别麻烦。为了做这个测试,我还专门给电脑装了 Ubuntu 系统(因为需要 Linux 环境),又从网上下载了一堆编译工具来配置。结果遇到各种问题:工具版本对不上、编译参数调来调去——光是解决程序报错就花了好几天,好不容易编译生成了能用的文件并导进 ESP32 开发板,结果显示屏驱动又不兼容,界面根本显示不出来。折腾半天没解决,只能先暂停,等以后找到更简单的方法再试。 主要问题是 MicroPython 和 C/C++ 的工作方式差别太大:MicroPython 是边解释边执行,而 C/C++ 需要提前编译好,所以得用工具把 LVGL 的 C 代码转换成 MicroPython 能用的模块。这个过程中,编译工具的版本(比如 CMake、GCC)和依赖库(比如图形库、Python 扩展)得完全匹配,而 ESP32 开发板的特殊要求(比如存储空间分配、接口定义)又让调试变得更复杂。最后才发现直接移植可能不太现实,还是先用稳定的 C/C++ 方案更靠谱。 不过,你想试试我当时最后一次编译生成的lvgl-micropython的话,你可以直接点击链接下载,但我事先声明,此固件(也是针对当前文章所介绍的设备编译的,日期为:2025/6/11)虽然能够正常导入LVGL(import lvgl),但在显示驱动上可能出现了点问题(正如前面所说)。最后附上参考的链接:https://github.com/lvgl-micropython/lvgl_micropython,如果你愿意尝试自行编译优化,或者发现了新的解决方案,非常欢迎和我分享经验! 回到正题,打开的你的arduino,并且进行相关配置(和esp32一样),然后安装好lvgl、TFT_eSPI、XPT2046_Touchscreen库。接下来你需要为TFT库和LVGL库分别更新一个配置文件,找到你的项目文件夹,然后找到libraries文件夹,然后再找到TFT_eSPI文件夹并找到User_Setup.h文件,这个就是该库的配置文件,你可以在里面配置驱动类型、加载的字体、使用的引脚以及 SPI 控制方法等。如果你不知道怎么配置,可以直接下载此文件然后替换掉原先的就行。对于LVGL,你需要回到libraries文件夹,并在里面添加一个lv_conf.h文件,它是LVGL图形库的配置文件,用于设置和调整图形库的各种参数。这些参数包括颜色设置、标准库包装器设置、硬件抽象层(HAL)设置、操作系统支持、渲染配置、功能配置等。如果你不知道怎么填写和修改,也可以直接下载此文件至目标文件夹内。至此,所有准备工作都完成了。 接下来上传并编译下面的代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248// 引入必要的库文件#include <lvgl.h> // LVGL图形库核心头文件#include <TFT_eSPI.h> // TFT屏幕驱动库#include <XPT2046_Touchscreen.h> // 触摸屏驱动库// 触摸屏引脚定义(根据实际硬件连接修改)#define XPT2046_IRQ 36 // 触摸中断引脚#define XPT2046_MOSI 32 // SPI数据输出#define XPT2046_MISO 39 // SPI数据输入#define XPT2046_CLK 25 // SPI时钟引脚#define XPT2046_CS 33 // 触摸芯片片选// 显示屏引脚定义(需与TFT_eSPI的User_Setup.h配置一致)#define TFT_MISO 12 // SPI数据输入(部分屏幕可能不用)#define TFT_MOSI 13 // SPI数据输出#define TFT_SCLK 14 // SPI时钟引脚#define TFT_CS 15 // 屏幕片选#define TFT_DC 2 // 数据/命令控制引脚#define TFT_RST -1 // 复位引脚(-1表示未连接)#define TFT_BL 21 // 背光控制引脚// 初始化触摸屏SPI对象(使用VSPI通道)SPIClass touchscreenSPI = SPIClass(VSPI);// 创建触摸屏对象,传入CS和IRQ引脚XPT2046_Touchscreen touchscreen(XPT2046_CS, XPT2046_IRQ);// 息屏相关变量unsigned long lastTouchTime = 0; // 记录最后触摸时间const unsigned long idleTimeout = 25000; // 25秒无操作后息屏// 屏幕分辨率定义#define SCREEN_WIDTH 240#define SCREEN_HEIGHT 320// 触摸坐标变量int x, y, z; // z表示触摸压力值// LVGL绘图缓冲区配置(根据屏幕大小动态计算)#define DRAW_BUF_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8))uint32_t draw_buf[DRAW_BUF_SIZE / 4]; // 缓冲区数组// LVGL日志打印回调函数void log_print(lv_log_level_t level, const char * buf) { LV_UNUSED(level); // 忽略日志级别参数 Serial.println(buf); // 通过串口输出日志 Serial.flush(); // 确保日志立即发送}// 触摸屏读取回调函数(LVGL定时调用)void touchscreen_read(lv_indev_t * indev, lv_indev_data_t * data) { // 检测触摸状态 if(touchscreen.tirqTouched() && touchscreen.touched()) { TS_Point p = touchscreen.getPoint(); // 获取原始触摸坐标 // 将原始坐标映射到屏幕分辨率(需根据实际屏幕校准) x = map(p.x, 200, 3700, 0, SCREEN_WIDTH); y = map(p.y, 240, 3800, 0, SCREEN_HEIGHT); z = p.z; // 保存压力值 // 设置LVGL触摸数据 data->state = LV_INDEV_STATE_PRESSED; data->point.x = x; data->point.y = y; // 调试信息输出 Serial.print("X = "); Serial.print(x); Serial.print(" | Y = "); Serial.print(y); Serial.print(" | Pressure = "); Serial.println(z); lastTouchTime = millis(); // 更新最后触摸时间 } else { data->state = LV_INDEV_STATE_RELEASED; // 触摸释放状态 }}// 按钮1点击计数器int btn1_count = 0;// 按钮1事件处理函数static void event_handler_btn1(lv_event_t * e) { lv_event_code_t code = lv_event_get_code(e); if(code == LV_EVENT_CLICKED) { btn1_count++; LV_LOG_USER("Button clicked %d", (int)btn1_count); // 记录点击次数 }}// 按钮2(切换开关)事件处理static void event_handler_btn2(lv_event_t * e) { lv_event_code_t code = lv_event_get_code(e); lv_obj_t * obj = (lv_obj_t*) lv_event_get_target(e); if(code == LV_EVENT_VALUE_CHANGED) { LV_UNUSED(obj); LV_LOG_USER("Toggled %s", lv_obj_has_state(obj, LV_STATE_CHECKED) ? "on" : "off"); }}// 滑动条标签指针(用于更新显示值)static lv_obj_t * slider_label;// 滑动条事件回调static void slider_event_callback(lv_event_t * e) { lv_obj_t * slider = (lv_obj_t*) lv_event_get_target(e); char buf[8]; // 格式化显示百分比 lv_snprintf(buf, sizeof(buf), "%d%%", (int)lv_slider_get_value(slider)); lv_label_set_text(slider_label, buf); // 更新标签文本 lv_obj_align_to(slider_label, slider, LV_ALIGN_OUT_BOTTOM_MID, 0, 10); LV_LOG_USER("Slider changed to %d%%", (int)lv_slider_get_value(slider));}// 下拉菜单事件处理static void dropdown_event_handler(lv_event_t * e) { lv_obj_t * dropdown = (lv_obj_t *)lv_event_get_target(e); char buf[32]; lv_dropdown_get_selected_str(dropdown, buf, sizeof(buf)); LV_LOG_USER("Selected option: %s", buf);}// 复选框事件处理static void checkbox_event_handler(lv_event_t * e) { lv_obj_t * checkbox = static_cast<lv_obj_t*>(lv_event_get_target(e)); bool checked = lv_obj_has_state(checkbox, LV_STATE_CHECKED); LV_LOG_USER("Checkbox state: %s", checked ? "Checked" : "Unchecked");}// 创建主界面GUIvoid lv_create_main_gui(void) { // 创建欢迎文本标签 lv_obj_t * text_label = lv_label_create(lv_screen_active()); lv_label_set_long_mode(text_label, LV_LABEL_LONG_WRAP); // 自动换行 lv_label_set_text(text_label, "Welcome to LVGL GUI!"); lv_obj_set_width(text_label, 150); // 设置宽度触发换行 lv_obj_set_style_text_align(text_label, LV_TEXT_ALIGN_CENTER, 0); lv_obj_align(text_label, LV_ALIGN_CENTER, 0, -90); // 屏幕上方 lv_obj_t * btn_label; // 临时按钮标签指针 // 创建按钮1(普通按钮) lv_obj_t * btn1 = lv_button_create(lv_screen_active()); lv_obj_add_event_cb(btn1, event_handler_btn1, LV_EVENT_ALL, NULL); lv_obj_align(btn1, LV_ALIGN_CENTER, 0, -50); // 屏幕中上方 lv_obj_remove_flag(btn1, LV_OBJ_FLAG_PRESS_LOCK); // 允许重复触发 btn_label = lv_label_create(btn1); lv_label_set_text(btn_label, "Click Me"); lv_obj_center(btn_label); // 创建按钮2(切换开关) lv_obj_t * btn2 = lv_button_create(lv_screen_active()); lv_obj_add_event_cb(btn2, event_handler_btn2, LV_EVENT_ALL, NULL); lv_obj_align(btn2, LV_ALIGN_CENTER, 0, 10); // 屏幕中央 lv_obj_add_flag(btn2, LV_OBJ_FLAG_CHECKABLE); // 设置为可切换状态 lv_obj_set_height(btn2, LV_SIZE_CONTENT); // 高度自适应内容 btn_label = lv_label_create(btn2); lv_label_set_text(btn_label, "Toggle"); lv_obj_center(btn_label); // 创建滑动条 lv_obj_t * slider = lv_slider_create(lv_screen_active()); lv_obj_align(slider, LV_ALIGN_CENTER, 0, 60); // 屏幕中下方 lv_obj_add_event_cb(slider, slider_event_callback, LV_EVENT_VALUE_CHANGED, NULL); lv_slider_set_range(slider, 0, 100); // 设置范围0-100 lv_obj_set_style_anim_duration(slider, 2000, 0); // 动画持续时间 // 创建滑动条百分比标签 slider_label = lv_label_create(lv_screen_active()); lv_label_set_text(slider_label, "0%"); lv_obj_align_to(slider_label, slider, LV_ALIGN_OUT_BOTTOM_MID, 0, 10); // 创建下拉菜单 lv_obj_t * dropdown = lv_dropdown_create(lv_screen_active()); lv_obj_align(dropdown, LV_ALIGN_CENTER, 0, 120); // 屏幕更下方 lv_obj_set_size(dropdown, 150, 40); lv_dropdown_set_options(dropdown, "Option 1\nOption 2\nOption 3"); lv_obj_add_event_cb(dropdown, dropdown_event_handler, LV_EVENT_VALUE_CHANGED, NULL); // 创建复选框 lv_obj_t * checkbox = lv_checkbox_create(lv_screen_active()); lv_obj_align(checkbox, LV_ALIGN_CENTER, 0, 160); // 屏幕底部 lv_checkbox_set_text(checkbox, "Enable Feature"); lv_obj_add_event_cb(checkbox, checkbox_event_handler, LV_EVENT_ALL, NULL);}// 关闭显示屏(息屏)void lv_disp_off() { digitalWrite(TFT_BL, LOW); // 关闭背光}// 开启显示屏void lv_disp_on() { digitalWrite(TFT_BL, HIGH); // 开启背光}void setup() { // 初始化背光控制引脚 pinMode(TFT_BL, OUTPUT); digitalWrite(TFT_BL, HIGH); // 默认开启背光 // 输出LVGL版本信息 String LVGL_Arduino = String("LVGL Library Version: ") + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch(); Serial.begin(115200); Serial.println(LVGL_Arduino); // 初始化LVGL核心 lv_init(); lv_log_register_print_cb(log_print); // 注册日志回调 // 初始化触摸屏SPI通信 touchscreenSPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS); touchscreen.begin(touchscreenSPI); touchscreen.setRotation(2); // 设置旋转方向(根据实际屏幕调整) // 初始化显示屏 TFT_eSPI tft = TFT_eSPI(SCREEN_WIDTH, SCREEN_HEIGHT); tft.begin(); tft.setRotation(3); // 设置旋转方向(需与LVGL配置一致) // 创建LVGL显示设备 lv_display_t * disp; disp = lv_tft_espi_create(SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf)); lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270); // 设置旋转 // 创建输入设备(触摸屏) lv_indev_t * indev = lv_indev_create(); lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); lv_indev_set_read_cb(indev, touchscreen_read); // 注册触摸读取回调 // 创建主界面 lv_create_main_gui();}void loop() { // LVGL任务处理(必须周期性调用) lv_task_handler(); lv_tick_inc(5); // 增加系统时钟(5ms) delay(5); // 短暂延时 // 息屏逻辑处理 unsigned long currentTime = millis(); if (currentTime - lastTouchTime > idleTimeout) { lv_disp_off(); // 超时息屏 } else { lv_disp_on(); // 有操作时保持亮屏 }} 上传编译成功后(要花很长时间),你能看到屏幕上有相关的组件显示,你可以试着交互一下,如果没报错说明运行成功。 接着我们进阶一下,ESP32-2432S028上有一个RGB灯珠,我们可以通过屏幕上的界面中的相关组件来控制这个RGB灯珠。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197#include <lvgl.h>#include <TFT_eSPI.h>#include <XPT2046_Touchscreen.h>// 触摸屏引脚定义#define XPT2046_IRQ 36#define XPT2046_MOSI 32#define XPT2046_MISO 39#define XPT2046_CLK 25#define XPT2046_CS 33// 显示屏引脚定义#define TFT_MISO 12#define TFT_MOSI 13#define TFT_SCLK 14#define TFT_CS 15#define TFT_DC 2#define TFT_RST -1 // 未连接#define TFT_BL 21 // 背光控制// RGB LED引脚定义const int redPin = 4;const int greenPin = 16;const int bluePin = 17;SPIClass touchscreenSPI = SPIClass(VSPI);XPT2046_Touchscreen touchscreen(XPT2046_CS, XPT2046_IRQ);unsigned long lastTouchTime = 0;const unsigned long idleTimeout = 25000; // 25秒无操作息屏#define SCREEN_WIDTH 240#define SCREEN_HEIGHT 320int x, y, z;#define DRAW_BUF_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8))uint32_t draw_buf[DRAW_BUF_SIZE / 4];void log_print(lv_log_level_t level, const char * buf) { LV_UNUSED(level); Serial.println(buf); Serial.flush();}void touchscreen_read(lv_indev_t * indev, lv_indev_data_t * data) { if(touchscreen.tirqTouched() && touchscreen.touched()) { TS_Point p = touchscreen.getPoint(); // 根据实际屏幕旋转调整坐标映射 x = map(p.x, 200, 3700, 0, SCREEN_WIDTH); y = map(p.y, 240, 3800, 0, SCREEN_HEIGHT); z = p.z; data->state = LV_INDEV_STATE_PRESSED; data->point.x = x; data->point.y = y; Serial.print("X = "); Serial.print(x); Serial.print(" | Y = "); Serial.print(y); Serial.print(" | Pressure = "); Serial.print(z); Serial.println(); lastTouchTime = millis(); } else { data->state = LV_INDEV_STATE_RELEASED; }}// RGB滑条相关变量static lv_obj_t * r_slider;static lv_obj_t * g_slider;static lv_obj_t * b_slider;static lv_obj_t * rgb_label;// RGB值存储变量static uint8_t r_value = 0;static uint8_t g_value = 0;static uint8_t b_value = 0;// RGB滑条事件回调函数static void rgb_slider_event_callback(lv_event_t * e) { lv_obj_t * slider = (lv_obj_t*) lv_event_get_target(e); // 更新RGB值 if (slider == r_slider) { r_value = lv_slider_get_value(slider); } else if (slider == g_slider) { g_value = lv_slider_get_value(slider); } else if (slider == b_slider) { b_value = lv_slider_get_value(slider); } // 设置RGB LED的颜色 analogWrite(redPin, r_value); analogWrite(greenPin, g_value); analogWrite(bluePin, b_value); // 更新标签文本 char buf[32]; lv_snprintf(buf, sizeof(buf), "RGB: (%d, %d, %d)", r_value, g_value, b_value); lv_label_set_text(rgb_label, buf);}// 创建RGB控制GUIvoid lv_create_rgb_gui(void) { // 创建RGB标签 rgb_label = lv_label_create(lv_screen_active()); lv_label_set_text(rgb_label, "RGB: (0, 0, 0)"); lv_obj_align(rgb_label, LV_ALIGN_TOP_MID, 0, 10); // 使用 LV_ALIGN_TOP_MID // 创建红色滑条 r_slider = lv_slider_create(lv_screen_active()); lv_obj_set_size(r_slider, 200, 30); lv_obj_align(r_slider, LV_ALIGN_TOP_MID, 0, 50); // 使用 LV_ALIGN_TOP_MID lv_slider_set_range(r_slider, 0, 255); lv_obj_add_event_cb(r_slider, rgb_slider_event_callback, LV_EVENT_VALUE_CHANGED, NULL); // 创建绿色滑条 g_slider = lv_slider_create(lv_screen_active()); lv_obj_set_size(g_slider, 200, 30); lv_obj_align(g_slider, LV_ALIGN_TOP_MID, 0, 120); // 使用 LV_ALIGN_TOP_MID lv_slider_set_range(g_slider, 0, 255); lv_obj_add_event_cb(g_slider, rgb_slider_event_callback, LV_EVENT_VALUE_CHANGED, NULL); // 创建蓝色滑条 b_slider = lv_slider_create(lv_screen_active()); lv_obj_set_size(b_slider, 200, 30); lv_obj_align(b_slider, LV_ALIGN_TOP_MID, 0, 190); // 使用 LV_ALIGN_TOP_MID lv_slider_set_range(b_slider, 0, 255); lv_obj_add_event_cb(b_slider, rgb_slider_event_callback, LV_EVENT_VALUE_CHANGED, NULL);}void lv_disp_off() { // 关闭背光 digitalWrite(TFT_BL, LOW);}void lv_disp_on() { // 开启背光 digitalWrite(TFT_BL, HIGH);}void setup() { // 初始化背光引脚 pinMode(TFT_BL, OUTPUT); digitalWrite(TFT_BL, HIGH); // 默认开启背光 // 初始化RGB LED引脚 pinMode(redPin, OUTPUT); pinMode(greenPin, OUTPUT); pinMode(bluePin, OUTPUT); String LVGL_Arduino = String("LVGL Library Version: ") + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch(); Serial.begin(115200); Serial.println(LVGL_Arduino); lv_init(); lv_log_register_print_cb(log_print); // 初始化触摸屏SPI touchscreenSPI.begin(XPT2046_CLK, XPT2046_MISO, XPT2046_MOSI, XPT2046_CS); touchscreen.begin(touchscreenSPI); touchscreen.setRotation(2); // 根据实际屏幕方向调整 // 初始化显示屏 TFT_eSPI tft = TFT_eSPI(SCREEN_WIDTH, SCREEN_HEIGHT); tft.begin(); tft.setRotation(3); // 根据实际屏幕方向调整 lv_display_t * disp; disp = lv_tft_espi_create(SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf)); lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270); lv_indev_t * indev = lv_indev_create(); lv_indev_set_type(indev, LV_INDEV_TYPE_POINTER); lv_indev_set_read_cb(indev, touchscreen_read); lv_create_rgb_gui();}void loop() { lv_task_handler(); lv_tick_inc(5); delay(5); unsigned long currentTime = millis(); if (currentTime - lastTouchTime > idleTimeout) { // 执行息屏操作 lv_disp_off(); } else { // 如果有触摸操作,保持屏幕亮起 lv_disp_on(); }} 程序运行后,你可以在屏幕上看到三个滑条组件,它们分别控制着RGB灯珠的红、绿、蓝三种颜色的比例,通过调节滑条,你可以让RGB灯珠显示不同的颜色。 接下来,我们尝试让屏幕显示一张图片。首先你得要准备一张合适大小的图片,然后打开LVGL官网提供的一个图像转换工具,将图片上传,选择ARGB8888颜色格式,最后点击转换,你将会得到一个.c文件,将其打开,找到const LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST……一行,你会在后面的{}内发现有一个由超多十六进制字符。接着,你需要在你的这个项目的文件夹里创建一个名为image.h的文件,并在里面加入以下代码(里面有些地方需要你手动替换或修改): 12345678910111213141516171819202122232425262728293031323334353637#ifdef __has_include #if __has_include("lvgl.h") #ifndef LV_LVGL_H_INCLUDE_SIMPLE #define LV_LVGL_H_INCLUDE_SIMPLE #endif #endif#endif#if defined(LV_LVGL_H_INCLUDE_SIMPLE) #include "lvgl.h"#else #include "lvgl/lvgl.h"#endif#ifndef LV_ATTRIBUTE_MEM_ALIGN#define LV_ATTRIBUTE_MEM_ALIGN#endif#ifndef LV_ATTRIBUTE_IMAGE_GAP#define LV_ATTRIBUTE_IMAGE_GAP#endifconst LV_ATTRIBUTE_MEM_ALIGN LV_ATTRIBUTE_LARGE_CONST LV_ATTRIBUTE_IMAGE_GAP uint8_t gap_map[] = { <添加十六进制字符> };const lv_image_dsc_t gap = { .header = { .magic = LV_IMAGE_HEADER_MAGIC, .cf = LV_COLOR_FORMAT_ARGB8888, .flags = 0, .w = 320, // 宽度 .h = 240, // 高度 //.stride = 120, .reserved_2 = 0 }, .data_size = sizeof(gap_map), .data = gap_map, .reserved = NULL}; 保存后,在你的项目代码里写入: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950// 引入库文件#include <lvgl.h> // LVGL图形库#include <TFT_eSPI.h> // TFT显示屏驱动#include "image.h" // 图片资源头文件// 屏幕参数#define SCREEN_WIDTH 240 // 屏幕宽度#define SCREEN_HEIGHT 320 // 屏幕高度// 绘图缓冲区(屏幕大小的1/10)#define DRAW_BUF_SIZE (SCREEN_WIDTH * SCREEN_HEIGHT / 10 * (LV_COLOR_DEPTH / 8))uint32_t draw_buf[DRAW_BUF_SIZE / 4]; // 分配缓冲区// 日志打印函数(调试用)void log_print(lv_log_level_t level, const char * buf) { LV_UNUSED(level); Serial.println(buf); Serial.flush();}// 显示图片(测试用)void draw_image(void) { LV_IMAGE_DECLARE(gap); // 声明图片资源 lv_obj_t * img1 = lv_image_create(lv_screen_active()); // 创建图片对象 lv_image_set_src(img1, &gap); // 设置图片源 lv_obj_align(img1, LV_ALIGN_CENTER, 0, 0); // 居中对齐}void setup() { // 初始化串口并打印LVGL版本 Serial.begin(115200); Serial.println(String("LVGL Version: ") + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch()); // 初始化LVGL lv_init(); lv_log_register_print_cb(log_print); // 注册日志回调 // 初始化显示屏 lv_display_t * disp = lv_tft_espi_create(SCREEN_WIDTH, SCREEN_HEIGHT, draw_buf, sizeof(draw_buf)); lv_display_set_rotation(disp, LV_DISPLAY_ROTATION_270); // 旋转屏幕 draw_image(); // 显示测试图片}void loop() { lv_task_handler(); // 处理LVGL任务 lv_tick_inc(5); // 更新系统时间(5ms) delay(5); // 延时匹配} 编译上传后,应该就能看到图片了。如果出现问题,你的从转换格式是否正确、图片的宽高是否匹配、代码其它地方是否被修改入手。 未完待续……
阅读全文
开发板
348 分钟
开发板研究笔记—ESP32篇
前言继ESP8266后我又整了一块ESP32的板子。通过查阅资料可以发现,ESP32和ESP8266都是流行的物联网(IoT)微控制器,它们共同的特点在于都支持Wi-Fi通信,能够连接到无线网络进行数据传输。两者都拥有丰富的开源资源,支持开发者进行二次开发和定制,广泛应用于物联网、智能家居、嵌入式系统等领域。且都支持C/C++和MicroPython语言。 ESP32以其高性能著称,采用双核处理器,具备强大的计算能力和多任务处理能力。它集成了丰富的外设接口和更多的GPIO引脚,支持多种通信协议(包括蓝牙),并具备低功耗设计和较高的安全性。然而,相比ESP8266,ESP32的成本较高,不过俗话说的好,一分钱一分货,面对这种情况得按需选择。 可以说,ESP32就是ESP8266的升级版。那么在此文中,我会继续分享一些ESP32的有趣项目,当然还是得叠个甲:本人并非专业人士,接下来的内容仅供参考。而且毕竟是笔记,难免有copy的部分。 一、开发环境搭建基于之前对 ESP8266 的研究经验,接下来我打算分别用 MicroPython 和 C/C++ 这两种语言来开发 ESP32 项目。听说 ESP32 还能支持 Lua、JavaScript 等其他编程语言,不过我对它们了解不多,所以还是以 MicroPython 和 C 语言为主进行开发。言归正传,和开发 ESP8266 时一样,咱们首先要安装一个能用来编写程序的集成开发环境(IDE)。 1.MicroPython可支持MicroPython开发的IDE有Mu Editor、uPyCraft IDE、Thonny IDE以及MicroIDE等。这里依然推荐Thonny,因为它操作十分简单,容易上手。当然,使用其它工具也是可以的,甚至是Visual Studio Code,只不过你需要额外加装合适的插件才行(如RT-Thread、Pymakr)。 以Thonny为例,选择合适的版本下载后按照正常安装流程安装软件。接着下载适配ESP32的MicroPython,它是一个后缀为bin的文件。同时你还得下载并安装ESP32的串口通信的驱动程序,以便计算机能够识别和读写ESP32(不过一般情况下,电脑系统都会自带)。到这里,前期准备工作就完成了。 接下来是配置IDE,打开Thonny,在顶栏菜单找到运行—配置解释器,在打开的窗口配置好ESP32解释器和端口,接下来点击安装MicroPython。安装完成出现Done的字样后说明安装成功。 刷新完成后会出现以下界面(如果没有显示文件列表,则需要在视图中勾选“文件”): 到这里,整个开发环境已经搭建完毕,点击新建,创建一个新的文件。 2.C/C++对于C/C++,其实乐鑫官方就给出了一个开发的工具框架:ESP-IDF,但它配置起来有点复杂,所以这边推荐新手友好的Arduino IDE,不过你需要注意,因为ESP-IDF是官方的工具,所以它能够很快适配 ESP32 的新特性和功能更新,但Arduino就很难保证同步了。 接下来我们正式进入Arduino的安装环节。首先,找一个适合你电脑系统的版本下载下来。下载好之后,就按照平常安装软件的步骤,一步步操作就行,没什么特别的难度。 安装好 Arduino IDE (写当前内容时的版本为2.3.6)后,双击打开它,进入配置环节。软件界面打开后,在菜单栏里找到“文件”,再点击“首选项”。接下来,你会看到一个设置页面,在“项目文件夹地址”这一栏,要填入一个你觉得合适的文件夹路径。这个文件夹可重要啦,以后你所有的项目都会存放在这里。它就像是一个“大仓库”,里面会包含各种项目相关的内容,比如各种库文件、配置文件,还有你编写的主程序代码等等;接着在“开发板管理地址”填入以下内容:https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json 它是 Arduino IDE 获取 ESP32 开发板支持包的关键资源定位符。点击确定完成这一步的操作。 接下来找到工具->开发板:“……”->开发板管理器(或者左侧竖栏从上往下数第二个图标),搜索“esp32”,找到esp32 by Espressif Systems安装。安装完成后,将你的ESP32开发板连接至电脑,然后在顶栏第二栏菜单中选择你的端口和开发板,在本例中选择的是COM5-ESP32 Dev Module。 至此,ArduinoIDE安装并配置完毕。 二、了解开发板的结构 如图是我自己购买的ESP-WROOM-32开发板的实物图,可以看到它的结构和搭载ESP8266的NodeMCU大差不差。但仍然有些细节要注意。首先,你会发现我的ESP32上搭载的串口硬件与之前的ESP8266不一样,这是因为我的ESP32采用的是CH9102X串口芯片,而之前ESP8266的是CH340,理论来说CH9102X要比CH340传输速度会更快,所以搭载CH9102X串口芯片的板子要比搭载CH340的要贵一点。 ESP32主芯片(其实应该称其为ESP32模组,因为已经封装起来了)里就藏有ESP32芯片、Flash芯片和一些基础的外围电路(由晶振、电容、电阻等构成);再往外看,就是一些我们能看得到的电路,这些电路分为电源稳压、数据传输、硬件保护等部分。 顺便注意一下,下面的文本里基本都是基于我购买的这款来写的,同时请注意接下来说的“ESP32”均默认代表ESP-WROOM-32(特别说明除外)。 ESP32的硬件设施及特点: 双核CPU:ESP32采用双核Xtensa 32-bit LX6 CPU,每个核心均可独立控制或上电,时钟频率可调范围从80 MHz到240 MHz,提供强大的处理能力。 低功耗设计:ESP32具备低功耗模式,包括深度睡眠模式,其睡眠电流小于5µA,非常适合电池供电的设备。此外,它还配备了一个低功耗协处理器,可以在CPU断电时监测外设状态或模拟量变化。 无线通信 Wi-Fi:支持802.11 b/g/n协议,最大传输速率可达150 Mbps,通过路由器可直接连接互联网,支持STA/AP/STA+AP模式。 蓝牙:集成传统蓝牙和低功耗蓝牙BLE,可用于连接手机、耳机、广播BLE Beacon等。 外设接口 存储接口:SD卡接口、高速SDIO/SPI。 通信接口:UART(通用异步收发传输器)、I2S(用于音频数据传输的总线标准)、I2C(两线式串行总线)、SPI(串行外设接口)、以太网接口等。 电源接口:通常包括USB供电接口和排针供电接口。USB供电接口可以通过USB线连接到电脑或其他USB电源设备,为开发板提供5V电源。排针供电接口则可以通过外接电源模块为开发板提供更高电压的电源 GPIO(通用输入输出)引脚:ESP32开发板提供了大量的GPIO引脚,这些引脚可以配置为输入或输出模式,用于连接各种外部设备和传感器。 ADC(模数转换器)和DAC(数模转换器):ESP32开发板内置ADC和DAC模块,可以将模拟信号转换为数字信号或将数字信号转换为模拟信号,从而支持模拟信号的采集和输出。 PWM(脉冲宽度调制):ESP32开发板支持PWM输出,可以通过调整PWM信号的占空比来控制外部设备的运行状态。 按钮①EN按钮(复位按钮) 作用:EN按钮主要用于复位ESP32模块。当按下此按钮时,ESP32模块会进行复位操作,重启其运行的系统或程序。这对于在开发过程中需要重置设备到初始状态或解决一些运行时出现的问题非常有用。 应用场景:在调试程序时,如果程序出现死循环或不可预料的错误,可以通过按下EN按钮来重启ESP32,从而恢复到初始状态,便于重新加载程序或进行进一步的调试。在需要重新启动设备以应用新的设置或配置时,也可以使用EN按钮进行复位操作。 ②BOOT按钮(下载/启动配置按钮) 作用:BOOT按钮在ESP32的启动和固件下载过程中起着关键作用。通过特定的操作(如同时按下BOOT和EN按钮),可以将ESP32置于特定的启动模式(如固件下载模式),从而允许通过串口等方式向ESP32下载新的固件或程序。 应用场景:在开发新程序或更新现有程序时,需要先将ESP32置于固件下载模式。这通常通过同时按下BOOT和EN按钮(有时还需要保持BOOT按钮按下直到开始下载)来实现。在需要恢复ESP32到出厂设置或刷写底层固件时,也可能需要进入固件下载模式。 硬件内存 ROM: 448 KB(用于启动和运行) SRAM:520 KB(用于数据和指令) RTC 快速 SRAM:8 KB(用于从深度睡眠模式启动 RTC 期间的数据存储和主 CPU) RTC 慢速 SRAM:8KB(用于深度睡眠模式下的协处理器访问) eFuse:1 Kb(其中 256 位用于系统(MAC 地址和芯片配置),其余 768 位保留用于客户应用,包括 Flash-Encryption 和 Chip-ID) 嵌入式闪存:通过 ESP32-D2WD 和 ESP32-PICO-D4 上的 IO16、IO17、SD_CMD、SD_CLK、SD_DATA_0 和 SD_DATA_1 内部连接的闪存。0 MiB(ESP32-D0WDQ6、ESP32-D0WD 和 ESP32-S0WD 芯片);2 MiB (ESP32-D2WD 芯片);4 MiB(ESP32-PICO-D4 SiP 模组) 接口概览 18 个模数转换器 (ADC) 通道 3 个 SPI 接口 3 个 UART 接口 2 个 I2C 接口 16 个 PWM 输出通道 2 个数模转换器 (DAC) 2 个 I2S 接口 10 个电容式感应 GPIO 1、仅输入引脚GPIO 34 ~ GPIO 39 没有内部上拉或下拉电阻,它们只能用作输入,不能用作输出。 2、SPI闪存GPIO 6 ~ GPIO 11 连接到 ESP-WROOM-32 芯片上的集成 SPI 闪存,不建议用于其他用途。 3、电容式触摸 IOESP32 具有 10 个内置电容式触摸传感器。它们可以感知任何带有电荷的东西的变化,可以检测用手指触摸GPIO时引起的变化。这些引脚可以集成到电容焊盘中取代机械按钮,也可用于 ESP32 从深度睡眠中唤醒。 T0 (GPIO 4)、T1 (GPIO 0)、T2 (GPIO 2)、T3 (GPIO 15)、T4 (GPIO 13)、T5 (GPIO 12)、T6 (GPIO 14)、T7 (GPIO 27)、T8 (GPIO 33)、T9 (GPIO 32) 4、ADCESP32 具有 18 x 12 位 ADC 输入通道,12位分辨率可以获得从 0 到 4095 的模拟读数,其中 0 对应 0V,4095 对应 3.3V。还可以在代码和ADC范围上设置通道的分辨率。 注意:ADC 引脚没有线性行为;使用 Wi-Fi 时不能使用 ADC2 引脚。 ADC1_CH0 (GPIO 36)ADC1_CH1 (GPIO 37)ADC1_CH2 (GPIO 38)ADC1_CH3 (GPIO 39)ADC1_CH4 (GPIO 32)ADC1_CH5 (GPIO 33)ADC1_CH6 (GPIO 34)ADC1_CH7 (GPIO 35)ADC2_CH0 (GPIO 4)ADC2_CH1 (GPIO 0)ADC2_CH2 (GPIO 2)ADC2_CH3 (GPIO 15)ADC2_CH4 (GPIO 13)ADC2_CH5 (GPIO 12)ADC2_CH6 (GPIO 14)ADC2_CH7 (GPIO 27)ADC2_CH8 (GPIO 25)ADC2_CH9 (GPIO 26) 5、DACESP32 上有 2 x 8 位 DAC 通道,用于将数字信号转换为模拟电压信号输出。 DAC1 (GPIO25)、DAC2 (GPIO26) 6、RTC GPIO当 ESP32 处于深度睡眠状态时,可以使用路由到 RTC 低功耗子系统的 GPIO。当超低功耗 (ULP) 协处理器运行时,这些 RTC GPIO 可用于将 ESP32 从深度睡眠中唤醒。 RTC_GPIO0 (GPIO36)RTC_GPIO3 (GPIO39)RTC_GPIO4 (GPIO34)RTC_GPIO5 (GPIO35)RTC_GPIO6 (GPIO25)RTC_GPIO7 (GPIO26)RTC_GPIO8 (GPIO33)RTC_GPIO9 (GPIO32)RTC_GPIO10 (GPIO4)RTC_GPIO11 (GPIO0)RTC_GPIO12 (GPIO2)RTC_GPIO13 (GPIO15)RTC_GPIO14 (GPIO13)RTC_GPIO15 (GPIO12)RTC_GPIO16 (GPIO14)RTC_GPIO17 (GPIO27) 7、PWMESP32 LED PWM 控制器具有 16 个独立通道,可配置为生成具有不同属性的 PWM 信号。所有可以用作输出的引脚都可以用作PWM引脚(GPIO 34至39不能产生PWM)。 设置PWM信号,需要定义信号频率、占空比、PWM通道、要输出信号的 GPIO。 8、I2CESP32 有两个 I2C 通道,任何引脚都可以设置为 SDA 或 SCL。ESP32 与 Arduino IDE 配合使用时默认的 I2C 引脚为:GPIO 21 (SDA)、GPIO 22 (SCL)。 如果想使用其他引脚,调用:Wire.begin(SDA, SCL); 9、SPI默认 SPI 的引脚映射为: SPI MOSI MISO CLK CS VSPI GPIO 23 GPIO 19 GPIO 18 GPIO 5 HSPI GPIO 13 GPIO 12 GPIO 14 GPIO 15 10、StrappingStrapping 引脚用于将 ESP32 置于引导加载程序或闪烁模式。大多数内置 USB/串口的开发板上,无需担心这些引脚的状态。但如果外围设备连接到这些引脚,则在代码烧录、刷新固件或复位开发板时可能会遇到问题。 GPIO 0(必须为低电平才能进入启动模式)GPIO 2(启动期间必须为浮动或低电平)GPIO 4GPIO 5(启动期间必须为高电平)GPIO 12(启动期间必须为低电平)GPIO 15(启动期间必须为高电平) 12、高电平 Boot 引脚一些 GPIO 在启动或复位时将其状态改为高电平或输出 PWM,如果外设连接到这些 GPIO,在 ESP32 复位或启动时可能出现问题。 GPIO 1GPIO 3GPIO 5GPIO 6 ~ GPIO 11(集成 SPI 闪存)GPIO 14GPIO 15 13、EN引脚EN 是 3.3V 稳压的使能引脚。使用被拉起,接地禁用稳压。 接口参考图 术语解释:ADC(模拟数字转换器,Analog-to-Digital Converter):ADC是一种将模拟信号(如温度、压力、声音等连续变化的物理量)转换为数字信号(离散的二进制数)的电路或设备。它广泛应用于各种需要数字化模拟信号的场合,如数据采集、信号处理等。 DAC(数字模拟转换器,Digital-to-Analog Converter):DAC与ADC相反,它将数字信号转换为模拟信号。DAC常用于音频、视频和其他模拟信号重建的场合,如数字音频播放、数字信号处理后的模拟输出等。 UART(通用异步收发传输器,Universal Asynchronous Receiver/Transmitter):UART是一种用于串行通信的接口标准,它允许设备之间以异步方式传输数据。UART通信通常用于低速、短距离的数据传输,如微控制器与其他外设之间的通信。 I2C(两线式串行总线,Inter-Integrated Circuit):I2C是一种用于连接低速外设的串行通信协议,它使用两根线(数据线SDA和时钟线SCL)进行数据传输。I2C具有简单、高效的特点,广泛应用于微控制器与各种传感器、存储器等外设之间的通信。 I2S(集成音频接口,Inter-IC Sound):I2S是一种用于音频数据传输的串行通信协议,它支持高速、双向、多通道的数据传输。I2S常用于数字音频设备之间的连接,如数字音频播放器、音频解码器等。 SPI(串行外设接口,Serial Peripheral Interface):SPI是一种高速、全双工的同步串行通信 协议,它使用四根线(MISO、MOSI、SCK、CS)进行数据传输。SPI广泛应用于微控制器与各种外设(如存储器、显示器、传感器等)之间的通信。 TCP(传输控制协议,Transmission Control Protocol):TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP在IP协议的基础上提供了面向连接的、可靠的传输服务,它广泛应用于互联网中的各种数据传输场景。 UDP(用户数据报协议,User Datagram Protocol):UDP是一种无连接的、不可靠的、基于报文的传输层通信协议。UDP在IP协议的基础上提供了简单的、面向报文的传输服务,它通常用于对实时性要求较高、但对可靠性要求不高的数据传输场景,如视频流、语音通信等。 对于http和mqtt在后面会有介绍,因此在此不多做赘述。 拓:ESP32系列ESP32芯片截止到2024年11月有5个系列: ESP32由ESP32-P 系列、ESP32-S 系列、ESP32-C 系列、ESP32-H 系列、ESP32 系列构成。其中,ESP32-P目前只有ESP32-P4;ESP32-H只有ESP32-H2;ESP32-S分为S3和S2两个小系列;ESP32-C系列分为C6、C5、C3、C2、C61五个小系列。不同系列的芯片它的各个功能和参数不同,你可以根据的你的项目需求选择合适的型号。比如如果你想整一些低成本,简单轻便的项目,你可以选择ESP32-C系列;而如果你要在ESP32开发有关人工智能的项目,那么首先推荐ESP32-S3,因为ESP32-S3在普通ESP32芯片的基础上增添了向量指令以加速神经网络计算和信号处理,是AIoT项目的不二之选…… 有时你可以看到ESP32型号上会带有WROOM或WROVER的表示,这代表的是ESP32模组的官方封装方式,而不同封装方式又有不同的参数系列,如ESP32-WROOM-32D(左图)、ESP32-WROOM-32U(右图),由图可见,它们虽然使用的芯片一样,封装也大差不差,但仍存在一些细节,不难发现,ESP32-WROOM-32D有一个板载天线,而ESP32-WROOM-32U没有但ESP32-WROOM-32U有一个天线座子,可供连接外部天线。 虽然二者都往外扩展了天线,但如果你要以此类封装来设计一个完整的PCB,那可需要看情况了,因为为了防止外部电路的信号影响ESP32-WROOM-32D的板载天线,你需要在设计PCB的时候将有板载天线的区域挖空,不过这个涉及ESP32的硬件设计,更多信息可以查阅官方给的硬件设计文档。 下面的表格就汇总了各个系列的主要参数: ESP32系列 ESP32芯片 芯片核数 WIFI 蓝牙 单核时钟 封装 引脚数量 RAM/ROM FLASH(MB) PSRAM ESP32-P4 ESP32-P4NRW16 双/单 - - QFN10X10 104引脚 128 KB HP ROM,16 KB LP ROM,768 KB HP L2MEM,32 KB LP SRAM,8 KB TCM N/A 16 ESP32-P4NRW32 双/单 - - QFN10X10 104引脚 128 KB HP ROM,16 KB LP ROM,768 KB HP L2MEM,32 KB LP SRAM,8 KB TCM N/A 32 ESP32-S3系列 ESP32-S3 双 √ √ 240 QFN7X7 56引脚 32 KB ROM,512 KB SRAM, 16 KB RTC SRAM N/A N/A ESP32-S3R2 双 √ √ 240 QFN7X7 56引脚 32 KB ROM,512 KB SRAM, 16 KB RTC SRAM N/A 2 ESP32-S3R8 双 √ √ 240 QFN7X7 56引脚 32 KB ROM,512 KB SRAM, 16 KB RTC SRAM N/A 8 ESP32-S3R8V 双 √ √ 240 QFN7X7 56引脚 32 KB ROM,512 KB SRAM, 16 KB RTC SRAM N/A 8 ESP32-S3FN8 双 √ √ 240 QFN7X7 56引脚 32 KB ROM,512 KB SRAM, 16 KB RTC SRAM 8 N/A ESP32-S3FH4R2 双 √ √ 240 QFN7X7 56引脚 32 KB ROM,512 KB SRAM, 16 KB RTC SRAM 4 2 ESP32-S3-PICO-1 ESP32-S3-PICO-1-N8R2 双 √ √ 240 LGA 7x7 56引脚 32 KB ROM,512 KB SRAM, 16 KB RTC SRAM 8 2 Quad ESP32-S3-PICO-1-N8R8 双 √ √ 240 LGA 7x7 56引脚 32 KB ROM,512 KB SRAM, 16 KB RTC SRAM Quad 8 Octal ESP32-S2 ESP32-S2 单 √ - 240 QFN7X7 56引脚 128 KB ROM, 320 KB SRAM, 16 KB RTC SRAM N/A N/A ESP32-S2R2 单 √ - 240 QFN7X7 56 128 KB ROM, 320 KB SRAM, 16 KB RTC SRAM N/A 2 ESP32-S2F ESP32-S2FH2 单 √ - 240 QFN7X7 56 128 KB ROM, 320 KB SRAM, 16 KB RTC SRAM 2 N/A ESP32-S2FH4 单 √ - 240 QFN7X7 56 128 KB ROM, 320 KB SRAM, 16 KB RTC SRAM 4 N/A ESP32-S2FN4R2 单 √ - 240 QFN7X7 56 128 KB ROM, 320 KB SRAM, 16 KB RTC SRAM 4 2 ESP32-C5 ESP32-C5N 单 √ √ 160 QFN6X6 48引脚 320 KB ROM, 384 KB HP SRAM, 16 KB LP SRAM - N/A ESP32-C5NR4 单 √ √ 160 QFN6X6 48引脚 320 KB ROM, 384 KB HP SRAM, 16 KB LP SRAM - 4 ESP32-C3 ESP32-C3 单 √ - 160 QFN5X5 32引脚 400 KB RAM, 384 KB ROM, 8 KB RTC SRAM N/A N/A ESP32-C3FH4 单 √ √ 160 QFN5X5 32引脚 400 KB RAM, 384 KB ROM, 8 KB RTC SRAM 4 ESP32-C3FH4X 单 √ √ 160 QFN5X5 32引脚 400 KB RAM, 384 KB ROM, 8 KB RTC SRAM 4 ESP32-C3FN4 (EOL) 单 √ √ 160 QFN5X5 32引脚 400 KB RAM, 384 KB ROM, 8 KB RTC SRAM 4 ESP8685 ESP8685H4 单 √ - 160 QFN4X4 28引脚 32 KB ROM,512 KB SRAM, 16 KB RTC SRAM 4 N/A ESP32-C2 ESP8684H2 单 √ √ 120 QFN4X4 24引脚 576 KB ROM, 272 KB SRAM 2 N/A ESP8684H4 单 √ √ 120 QFN4X4 24引脚 576 KB ROM, 272 KB SRAM 4 N/A ESP32-H ESP32-H2 单 - √ 96 QFN4X4 32引脚 320 KB SRAM, 128 KB ROM, 4 KB LP Memor√ 2or4 N/A ESP32 ESP32-D0WD-V3 双 √ √ 80-240 QFN5X5 48引脚 520 KB SRAM 448 KB ROM 16 KB RTC SRAM N/A N/A ESP32-D0WDR2-V3 双 √ √ 80-240 QFN5X5 48引脚 520 KB SRAM 448 KB ROM 16 KB RTC SRAM N/A 2 ESP32-U4WDH 双 √ √ 80-240 QFN5X5 48 520 KB SRAM 448 KB ROM 16 KB RTC SRAM 4 N/A ESP32-PICO-V3 双 √ √ 80-240 LGA 7X7 48 448 KB ROM 520 KB SRAM 16 KB RTC SRAM 4 N/A ESP32-PICO-V3-02 双 √ √ 80-240 LGA 7X7 48 448 KB ROM 520 KB SRAM 16 KB RTC SRAM 8 2 ESP32-PICO-D4 双 √ √ 80-240 LGA 7X7 48 448 KB ROM 520 KB SRAM 16 KB RTC SRAM 4 N/A ESP32-D0WD推荐使用升级版本 →ESP32-D0WD-V3 / ESP32-U4WDH 双 √ √ 80-240 QFN5X5 48 520 KB SRAM 448 KB ROM 16 KB RTC SRAM N/A N/A ESP32-D0WDQ6-V3推荐使用升级版本 →ESP32-D0WD-V3 / ESP32-U4WDH 双 √ √ 80-240 QFN6X6 48 520 KB SRAM 448 KB ROM 16 KB RTC SRAM N/A N/A ESP32-D0WDQ6推荐使用升级版本 →ESP32-D0WD-V3 / ESP32-U4WDH 双 √ S 520 KB SRAM 448 KB ROM 16 KB RTC SRAM 80-240 QFN6X6 48 520 KB SRAM 448 KB ROM 16 KB RTC SRAM N/A N/A ESP32-S0WD推荐使用升级版本 →ESP32-D0WD-V3 / ESP32-U4WDH 单 √ S 520 KB SRAM 448 KB ROM 16 KB RTC SRAM 80-240 QFN5X5 48 520 KB SRAM 448 KB ROM 16 KB RTC SRAM N/A N/A 表格仅供参考,部分数据待求证 更多相关内容,你可以查看相关文章,对于ESP32各个系列的信息汇总,你可以直接查看乐鑫官方的文档(在官方网站找“硬件”菜单就可以查询了)。 项目开发——MicroPython篇开机自动运行程序在ESP8266的研究笔记中我曾提及了boot.py文件用于初始化的功能,但在后续查阅资料的过程中,我发现除了初始化自动生成的boot.py还有一个(需要我们自行创建)名为main.py的文件。 ESP32开机自动运行程序有两个,一个是boot.py另一个是main.py。在MicroPython的运行环境中,boot.py和main.py是两个关键文件,它们在ESP32启动时的执行顺序和作用不同。 1. boot.py作用**:** boot.py是开发板在每次启动时(包括从深度睡眠中唤醒后)自动执行的文件。它主要用于设置一些基本的初始化参数,比如连接到Wi-Fi网络、设置系统时间、初始化硬件等。通过在boot.py中编写代码,可以确保每次设备启动时都能自动执行这些初始化操作,无需用户手动干预。其主要作用包括: 初始化设置:进行设备的基本初始化设置,例如配置引脚、电源管理、时钟设置等。 网络配置:配置WiFi或其他网络连接,设置静态IP地址等。 外设初始化:初始化外设,如I2C、SPI、UART等。 日志设置:设置调试信息输出方式,是否启用REPL(Read-Eval-Print Loop)等。 特点: 通常只包含一次性的设置代码,比如网络连接、硬件初始化等。 可以在其中调用其他模块或函数来完成初始化任务。 一旦执行完毕,boot.py就会结束,然后开发板会寻找并执行main.py文件。 2. main.py作用 main.py是开发板在boot.py执行完毕后自动执行的文件。它通常包含应用程序的主要逻辑代码,比如处理用户输入、控制GPIO引脚、读取传感器数据等。main.py是用户编写自定义代码的主要场所,用于实现具体的业务逻辑和功能。其主要作用包括: 应用逻辑:执行主要功能代码,如传感器数据采集、控制输出设备、数据处理等。 任务调度:管理和调度不同任务的执行。 事件处理:响应用户输入或外部事件(如定时器中断、网络请求等)。 特点 包含应用程序的主体逻辑代码。 可以是一个循环执行的程序,用于持续监听事件、处理数据等。 可以调用外部库或模块来扩展功能。 如果main.py不存在,开发板在boot.py执行完毕后可能会进入REPL模式(如果启用了的话),允许用户通过串行接口直接输入Python代码进行测试和调试。 总结图表 boot.py main.py 作用 设备启动时自动执行的初始化脚本 初始化后执行的应用程序主体逻辑 内容 一次性设置代码,如网络连接、硬件初始化等 应用程序的主体逻辑代码,如控制 GPIO、读取传感器等 特点 执行后结束,不持续运行 可循环执行,持续监听事件、处理数据等 执行时机 设备每次启动时(包括从深度睡眠中唤醒后) boot.py 执行完毕后 程序示例在前文我们也说过,能在ESP8266上运行的程序基本都能在ESP32上运行。所以之前在ESP8266研究笔记中出现的示例代码咱可以直接复制到ESP32上使用。对于micropython的使用,你可以参考MicroPython官方给出的文档。 点灯程序由于ESP32上蓝色LED的引脚连接被调整(即接线互换),因此需要对原先编写的代码进行相应修改。 12345678910from machine import Pinimport timeled = Pin(2,Pin.OUT)while True: led.value(0) print("灭") time.sleep(1) led.value(1) print("亮") time.sleep(1) 1.**导入必要的库:** from machine import Pin: 从machine模块中导入Pin类。machine模块是MicroPython的一部分,用于控制硬件,如GPIO引脚、I2C、SPI等。这里我们主要使用Pin类来操作GPIO引脚。 import time: 导入Python的time模块,以便使用其中的sleep函数来控制程序中的延时。 2.**配置LED引脚:** led = Pin(2, Pin.OUT): 创建一个Pin对象led,并将其与设备的GPIO引脚2相关联。Pin.OUT表示该引脚被配置为输出模式,即用于向外部设备(如LED灯)发送信号。 3.**设置循环以控制LED闪烁:** while True:使用一个无限循环来控制LED的闪烁。这个循环会一直执行,直到设备被断电或程序被强制停止。 3.1**控制LED熄灭:** led.value(0): 将led引脚的电平设置为0(低电平)。对于大多数LED电路,低电平意味着LED熄灭。 print("灭"): 在控制台上打印“灭”,以表明LED当前的状态。 time.sleep(1): 使程序暂停执行1秒。这个延时使得LED的闪烁可以被人类肉眼观察到。 3.2**控制LED点亮:** led.value(1): 将led引脚的电平设置为1(高电平)。对于大多数LED电路,高电平意味着LED点亮。 print("亮"): 在控制台上打印“亮”,以表明LED当前的状态。 time.sleep(1): 使程序再次暂停执行1秒,然后回到循环的开始,继续控制LED的熄灭。 如果你能看见板子上LED闪烁,则说明运行成功。同时,你利用面包板、杜邦线等材料,将另一个LED接入D2引脚(正极)、负极连接GND引脚,你会发现这个LED也会跟着同步闪烁,因为这个LED和板子上的LED是同一个引脚上的。将正负极对调(记得更换接线至3.3v引脚),你可以发现当板子led亮时,外接的LED灭,反之,板子上led灭时,外接的led亮。 我们已经学会了如何使用代码轻松地控制ESP32上的LED灯。那么,接下来一个自然的问题是:如何扩展这种控制,让ESP32能够响应外部硬件(如按钮)的操作,从而实现对已连接设备的更灵活控制呢? 要实现这一目标,我们需要编写代码来检测外部硬件(如按钮)的状态,并根据该状态来控制LED的亮灭。以下是一个示例代码,展示了如何通过ESP32的GPIO引脚读取按钮状态,并根据按钮的按下与否来控制LED的亮灭: 1234567891011121314151617181920212223242526272829303132import machineimport time# 设置LED引脚为输出模式led_pin = 2led = machine.Pin(led_pin, machine.Pin.OUT)# 设置按键引脚为输入模式,并启用内部上拉电阻button_pin = 0button = machine.Pin(button_pin, machine.Pin.IN, machine.Pin.PULL_UP)# 初始状态:LED熄灭led.value(0)try: while True: # 检查按键状态 if not button.value(): # 按键被按下,value()返回False print("按键被按下,点亮LED") led.value(1) # 点亮LED # 等待一段时间以消抖(可选) time.sleep(0.2) else: print("按键未按下,熄灭LED") led.value(0) # 熄灭LED # 可选:在循环中添加一些延迟,以减少CPU占用 time.sleep(0.1)except KeyboardInterrupt: # 当用户按下Ctrl+C时,执行清理工作 led.value(0) # 确保LED熄灭 print("程序已中断,LED已熄灭") 运行后,你只需要在GPIO4与GND之间连接一个按钮就行,按下去的一瞬间,ESP32上的LED会被点亮。接下来我们升级一下这段代码,让按键按下去的时候led(常)亮,再按一次,则熄灭: 12345678910111213141516171819202122232425262728293031import machineimport timeled_pin = 2led = machine.Pin(led_pin, machine.Pin.OUT)button_pin = 4button = machine.Pin(button_pin, machine.Pin.IN, machine.Pin.PULL_UP)# 初始状态:LED熄灭led_state = 0 # 0表示LED熄灭,1表示LED点亮try: while True: # 检查按键状态 if not button.value(): print("按键被按下,切换LED状态") led_state = 1 - led_state # 切换LED状态 if led_state == 1: led.value(1) print("LED已点亮") else: led.value(0) print("LED已熄灭") time.sleep(0.2) time.sleep(0.1)except KeyboardInterrupt: # 当用户按下Ctrl+C时,执行清理工作 led.value(0) # 确保LED熄灭 print("程序已中断,LED已熄灭") 呼吸灯呼吸灯是指灯光在相关电子元件的控制之下完成由亮到暗的逐渐变化,感觉好像是人在呼吸。对于ESP32开发板来说,我们可以利用上面的PWM实现这一功能。 PWM(Pulse Width Modulation)简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,广泛应用在测量、通信、工控等方面。对于PWM,你需要知道这些概念: PWM频率:指1秒钟内信号从高电平到低电平再回到高电平的次数,即一个周期内信号变化的次数。单位通常为Hz(赫兹)。 PWM周期:PWM周期是信号完成一个完整的高电平到低电平再回到高电平所需的时间。它与频率是倒数关系,即T = 1/f,其中T是周期,f是频率。 占空比:占空比是一个脉冲周期内,高电平的时间与整个周期时间的比例,通常以百分比或小数表示。计算公式:占空比(%) = (高电平时间/周期时间) × 100%。 下面的代码正好可以实现呼吸灯的效果: 1234567891011121314151617181920# 导入必要的库from machine import Pin, PWM import utime led_pin = Pin(2, Pin.OUT) # 创建Pin对象并赋值 pwm = PWM(led_pin, freq=500) # 创建PWM对象并赋值,设置频率为500 def breath_led(): while True: # 模拟吸气(亮度增加) for duty in range(0, 1024, 32): # 逐渐增加PWM信号的占空比 pwm.duty(duty) # 调用以设置PWM信号的当前占空比 utime.sleep_ms(40) # 设置延迟时间 # 模拟呼气(亮度减少) for duty in range(1023, -1, -32): pwm.duty(duty) utime.sleep_ms(40) breath_led() 首先导入了必要的库,并尝试将GPIO引脚2配置为输出模式,然后创建了一个PWM对象来控制该引脚。定义了一个breath_led函数,该函数包含一个无限循环,用于通过逐渐增加(吸气)然后错误地尝试减少到负数(呼气)PWM占空比来模拟LED的亮度变化。在吸气阶段,占空比从0增加到1023,每次增加32,并在每次设置占空比后暂停40毫秒以产生平滑效果。 温馨提示:准备运行第二个程序时,记得先关闭前一个程序的运行,接着按下EN按钮以刷新,然后再写入第二个程序,这样更不容易出错。 HTTP服务器HTTP(HyperText Transfer Protocol),即超文本传输协议,是互联网上应用最为广泛的一种网络协议。HTTP是万维网(World Wide Web)交换信息的基础,它允许将超文本标记语言(HTML)文档、图片、音频、视频等超文本数据从Web服务器传送到Web浏览器。 HTTP服务器是一种运行在服务器上的软件,它负责接收来自客户端(如Web浏览器)的请求,解析这些请求,然后返回对应的网页资源(如HTML文档、图片等)作为响应。简而言之,HTTP服务器就是提供网站内容给用户的服务器。 利用下面的代码可以让你的ESP32开发板运行一个简单的http服务器: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162import network import socket # 配置网络参数 ssid = '' password = '' # 连接到WiFi网络 sta_if = network.WLAN(network.STA_IF) sta_if.active(True) sta_if.connect(ssid, password) while not sta_if.isconnected(): pass print('网络已连接,IP地址:', sta_if.ifconfig()[0]) # 创建socket addr = ('', 80)s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(addr) s.listen(1) def http_response(client_socket, status_line, headers, content=""): client_socket.sendall(b'HTTP/1.1 ' + status_line.encode() + b'\r\n') for header in headers: client_socket.sendall(header.encode() + b'\r\n') client_socket.sendall(b'\r\n') client_socket.sendall(content.encode()) try: while True: print('等待连接...') client_socket, addr = s.accept() print('连接自:', addr) request = client_socket.recv(1024) print('收到请求:', request.decode()) # 简单的请求处理(仅支持GET /) if request.startswith(b'GET / HTTP/1.1'): status_line = '200 OK' headers = [ 'Content-Type: text/html', 'Connection: close' ] content = '''<!DOCTYPE html> <html> <head> <title>ESP32 HTTP Server</title> </head> <body> <h1>Hello from ESP32!</h1> </body> </html> ''' http_response(client_socket, status_line, headers, content) client_socket.close() except KeyboardInterrupt: s.close() print('服务器已关闭') 这段代码首先导入了network和socket模块,用于配置和建立网络连接。它设置了WiFi网络的SSID和密码,并尝试连接到该网络。一旦连接成功,它会打印出设备的IP地址。接下来,它创建了一个socket对象,监听所有网络接口上的80端口(HTTP的标准端口),并准备接受客户端的连接。定义了一个http_response函数,用于向客户端发送HTTP响应。在主循环中,服务器会不断等待客户端的连接,一旦有连接请求,它会接收并打印出客户端的请求。如果请求是GET /(即请求根目录),服务器会发送一个包含简单HTML页面的HTTP 200 OK响应。如果用户在运行服务器时按下Ctrl+C,服务器会捕获这个信号,关闭socket并打印出“服务器已关闭”。你可以根据具体情况修改html部分以满足你的需要。 当你看到控制台输出“网络已连接,IP地址:”192.168.32.45等待连接…”的提示时,说明程序运行成功并且成功联网。需要注意的是由于当前版本的ESP32不能连接5G路由器,不过我们可以将自己的手机开热点让ESP32链接。在同一网络下,任何设备只要在浏览器输入IP就能正常访问,当你看到网页出现Hello from ESP32!,说明连接成功。 升级一下上述代码,我们添加一个指示灯的功能:当正在连接网络时,LED闪烁;当成功连上网络时,LED保持常亮。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879import network import socket import time import machine # 配置网络参数 ssid = '' password = '' # LED连接的GPIO引脚编号led_pin = 2 # 设置LED引脚为输出模式led = machine.Pin(led_pin, machine.Pin.OUT)# 连接到WiFi网络 sta_if = network.WLAN(network.STA_IF) sta_if.active(True) sta_if.connect(ssid, password) while not sta_if.isconnected(): led.on() # 打开LED time.sleep(0.5) # 等待0.5秒 led.off() # 关闭LED time.sleep(0.5) # 等待0.5秒 led.on() # 打开LEDprint('网络已连接,IP地址:', sta_if.ifconfig()[0]) # 创建socket addr = ('', 80) # 监听所有可用的网络接口上的80端口 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(addr) s.listen(1) def http_response(client_socket, status_line, headers, content=""): # 发送HTTP状态行 client_socket.sendall(b'HTTP/1.1 ' + status_line.encode() + b'\r\n') # 遍历headers列表,逐个发送header for header in headers: client_socket.sendall(header.encode() + b'\r\n') # 发送一个空行,表示headers部分结束 client_socket.sendall(b'\r\n') # 发送响应内容 client_socket.sendall(content.encode()) try: while True: print('等待连接...') client_socket, addr = s.accept() print('连接自:', addr) request = client_socket.recv(1024) print('收到请求:', request.decode()) # 简单的请求处理(仅支持GET /) if request.startswith(b'GET / HTTP/1.1'): status_line = '200 OK' headers = [ 'Content-Type: text/html', 'Connection: close' ] content = '''<!DOCTYPE html> <html> <head> <title>ESP32 HTTP Server</title> </head> <body> <h1>Hello from ESP32!</h1> </body> </html> ''' http_response(client_socket, status_line, headers, content) client_socket.close() except KeyboardInterrupt: s.close() led.off() print('服务器已关闭') 我们还可以利用HTTP服务器处理函数http_response根据客户端发送的HTTP请求来更改LED的状态,并返回相应的HTTP响应。从而实现网页远程控制LED。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586import network import socket import machine # 配置LED引脚 led = machine.Pin(2, machine.Pin.OUT) # 配置网络参数 ssid = '' password = '' # 连接到WiFi网络 sta_if = network.WLAN(network.STA_IF) sta_if.active(True) sta_if.connect(ssid, password) # 等待网络连接 while not sta_if.isconnected(): pass print('网络已连接,IP地址:', sta_if.ifconfig()[0]) # HTML页面内容 HTML_CONTENT = """ <!DOCTYPE html> <html> <head> <title>LED Control</title> <script> function toggleLED(state) { var xhr = new XMLHttpRequest(); xhr.open('GET', '/' + state, true); xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { document.getElementById('ledStatus').innerText = xhr.responseText; } }; xhr.send(); } </script> </head> <body> <h1>LED Control</h1> <button onclick="toggleLED('on')">Turn LED On</button> <button onclick="toggleLED('off')">Turn LED Off</button> <p id="ledStatus">LED is OFF</p> </body> </html> """ # HTTP服务器处理函数 def http_response(client_socket, request): request = request.decode() if request.startswith('GET /on HTTP/1.1'): led.value(1) # 打开LED response = 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nLED is ON' elif request.startswith('GET /off HTTP/1.1'): led.value(0) # 关闭LED response = 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nLED is OFF' elif request.startswith('GET / HTTP/1.1'): response = 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n' + HTML_CONTENT else: response = 'HTTP/1.1 404 Not Found\r\n\r\nNot Found' client_socket.sendall(response.encode()) client_socket.close() # 创建socket并监听 addr = ('', 80) # 监听所有传入连接,端口80 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(addr) s.listen(1) print('\nHTTP服务器启动,等待连接...') try: while True: client_socket, addr = s.accept() print('\n连接自:', addr) request = client_socket.recv(4096) # 增加缓冲区大小以容纳整个HTTP请求 if request: http_response(client_socket, request) except KeyboardInterrupt: s.close() print('\n\n\n\n服务器已关闭') 在这一节的最后,我会分享一个有趣的网页井字棋小游戏,在经典井字棋玩法的基础上,我额外加入了新的机制:当下到第四步棋时,会清除第一步的棋,以此类推,丰富了井字棋的玩法。同时我也设计了网页的样式,让网页看起来更美观(发热警告x)。 参考代码 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168import network import socket import time import machine # 配置网络参数 ssid = '' password = '' # LED连接的GPIO引脚编号led_pin = 2 # 设置LED引脚为输出模式led = machine.Pin(led_pin, machine.Pin.OUT)# 连接到WiFi网络 sta_if = network.WLAN(network.STA_IF) sta_if.active(True) sta_if.connect(ssid, password) while not sta_if.isconnected(): led.on() # 打开LED time.sleep(0.5) # 等待0.5秒 led.off() # 关闭LED time.sleep(0.5) # 等待0.5秒 led.on() # 打开LEDprint('网络已连接,IP地址:', sta_if.ifconfig()[0]) # 创建socket addr = ('', 80) # 监听所有可用的网络接口上的80端口 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(addr) s.listen(1) def http_response(client_socket, status_line, headers, content=""): # 发送HTTP状态行 client_socket.sendall(b'HTTP/1.1 ' + status_line.encode() + b'\r\n') # 遍历headers列表,逐个发送header for header in headers: client_socket.sendall(header.encode() + b'\r\n') # 发送一个空行,表示headers部分结束 client_socket.sendall(b'\r\n') # 发送响应内容 client_socket.sendall(content.encode()) try: while True: print('等待连接...') client_socket, addr = s.accept() print('连接自:', addr) request = client_socket.recv(1024) print('收到请求:', request.decode()) # 简单的请求处理(仅支持GET /) if request.startswith(b'GET / HTTP/1.1'): status_line = '200 OK' headers = [ 'Content-Type: text/html', 'Connection: close' ] content = '''<!DOCTYPE html><html lang='en'> <head> <meta charset='UTF-8'> <meta name='viewport' content='width=device-width, initial-scale=1.0'> <title>Tic-Tac-Toe Game with Removal</title> <style> body { display: flex; justify-content: center; align-items: center; height: 100vh; background-color: #f0f0f0; margin: 0; font-family: Arial, sans-serif; } .game-container { background-color: #c5ffc4; border: 2px solid #ff9a67; padding: 20px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);text-align: center; } .board { display: grid; grid-template-columns: repeat(3, 100px); grid-template-rows: repeat(3, 100px); gap: 10px; margin-top: 20px; } .cell { width: 100px; height: 100px; background-color: #c2ffff; display: flex; align-items: center; justify-content: center; font-size: 60px; border: 2px solid #ffa5e1; cursor: pointer; } h1 { margin-top: 0;color: rgb(92, 56, 255); } button {margin-top: 20px;padding: 10px 20px;font-size: 16px;cursor: pointer;background-color: #ffbf48;color: #ff5e5e;font-weight: bold;border: none;border-radius: 5px;} button:hover {background-color: #ffac12;color: #ff4545; } #reset-alert {display: none;position: fixed;top: 20px;left: 50%;transform: translateX(-50%);padding: 10px 20px;background-color: #f9f2e7;color: #ad8305;border: 1px \solid #ffd700;border-radius: 5px;z-index: 1000;} </style> </head><body> <div class='game-container'> <h1>Tic-Tac-Toe Game</h1> <div class='board' id='board'></div> <button onclick='resetGame()'>Reset Game</button> <div id='reset-alert'>Game has been reset!</div> </div> <script> const board = document.getElementById('board'); const cells = []; const resetAlert = document.getElementById('reset-alert'); let currentPlayer = 'X'; let gameActive = true; let playerMoves = { X: [], O: [] }; function createBoard() { for (let i = 0; i < 9; i++) { const cell = document.createElement('div'); cell.classList.add('cell'); cell.addEventListener('click', () => placeStone(cell)); board.appendChild(cell); cells.push(cell); } } function placeStone(cell) { if (cell.textContent === '' && gameActive) { cell.textContent = currentPlayer; cell.classList.add(currentPlayer); playerMoves[currentPlayer].push(cell); if (playerMoves[currentPlayer].length > 3) { removeStone(playerMoves[currentPlayer].shift()); } checkWin(cell); currentPlayer = currentPlayer === 'X' ? 'O' : 'X'; } } function removeStone(cell) { cell.textContent = ''; cell.classList.remove('x', 'o'); } function checkWin(cell) { const index = cells.indexOf(cell); const row = Math.floor(index / 3); const col = index % 3; if (cells[row * 3].textContent === currentPlayer && cells[row * 3 + 1].textContent === currentPlayer && cells[row * 3 + 2].textContent === currentPlayer) { gameActive = false; alert(`${currentPlayer} wins!`); return; } if (cells[col].textContent === currentPlayer && cells[col + 3].textContent === currentPlayer && cells[col + 6].textContent === currentPlayer) { gameActive = false; alert(`${currentPlayer} wins!`); return; } if (cells[0].textContent === currentPlayer && cells[4].textContent === currentPlayer && cells[8].textContent === currentPlayer) { gameActive = false; alert(`${currentPlayer} wins!`); return; } if (cells[2].textContent === currentPlayer && cells[4].textContent === currentPlayer && cells[6].textContent === currentPlayer) { gameActive = false; alert(`${currentPlayer} wins!`); return; } } function resetGame() { cells.forEach(cell => { cell.textContent = ''; cell.classList.remove('x', 'o'); }); playerMoves.X = []; playerMoves.O = []; currentPlayer = 'X'; gameActive = true; resetAlert.style.display = 'block'; setTimeout(() => resetAlert.style.display = 'none', 2000); } createBoard(); </script> </body> </html> ''' http_response(client_socket, status_line, headers, content) client_socket.close() except KeyboardInterrupt: s.close() led.off() print('服务器已关闭') ***注:在连接ESP32设备至WiFi时,请注意该设备的版本目前不支持5G频段,推荐选用2.4G频段或手机热点。若需查找2.4G频段的WiFi账号和密码,请按以下步骤操作: 找到路由器本体,注意查看其上的标签,上面印有管理员的用户名和密码。2.接着,在同一网络环境下,使用浏览器访问路由器的管理地址(通常也在路由器标签上)。3.在弹出的登录界面,输入之前记录的管理员用户名和密码进行登录。4.登录成功后,进入路由器设置页面,我们就可以在其中找到并查看包括2.4G频段在内的各频段WiFi的账号和密码信息。*** ESP32-WIFI我们可以利用代码设置ESP32为WiFi接入点模式(Access Point, AP),将ESP32当作一个网络发射端。以下是基础代码: 123456789101112import networkdef wifi_ap_start(ssid, password): wlan = network.WLAN(network.AP_IF) if not wlan.active(True): print("Could not activate AP mode") return wlan.config(essid=ssid, password=password) print("Access Point started with SSID: %s" % ssid)wifi_ap_start('ESP32_WIFI', '12345678') 这段代码首先导入了network模块,然后定义了一个名为wifi_ap_start的函数,该函数接受两个参数:ssid(服务集标识符,即WiFi网络的名称)和password(WiFi网络的密码)。 在函数内部,它首先通过network.WLAN(network.AP_IF)创建了一个WLAN对象,并指定为接入点接口。接着,它使用wlan.active(True)尝试激活WiFi接入点模式。如果激活失败,则打印错误信息并返回。 如果接入点模式成功激活,则使用wlan.config(essid=ssid, password=password)来配置WiFi网络的名称(SSID)和密码。最后,它打印一条消息,表明接入点已成功启动,并显示了配置的SSID。 在代码的最后部分,调用了wifi_ap_start函数,并传入了’ESP32_WIFI’作为SSID和’12345678’作为密码,以启动一个名为’ESP32_WIFI’的WiFi接入点。 简单来说这段代码用于在ESP32上启动一个名为’ESP32_WIFI’的WiFi接入点,并设置其密码为’12345678’。你可以在网络搜索中找到名为’ESP32_WIFI’的WiFi并连接。 我们将其升级一下,使ESP32无需连接任何网络热点,直接作为发射端来提供网页服务。以下是示例代码(结合了前面的网页控制LED程序): 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788import networkimport usocket as socketfrom machine import Pin# 设置LED引脚LED_PIN = 2led = Pin(LED_PIN, Pin.OUT)# 设置WiFi接入点的SSID和密码SSID = 'ESP32_WIFI'PASSWORD = '12345678'# 简单的HTML内容HTML_CONTENT = """<!DOCTYPE html><html><head> <title>ESP32 WiFi AP</title></head><body> <h1>Welcome to ESP32 WiFi AP</h1> <p>LED is currently: <span id="ledStatus">OFF</span></p> <button onclick="toggleLED('on')">Turn LED On</button> <button onclick="toggleLED('off')">Turn LED Off</button> <script> function toggleLED(state) { var xhr = new XMLHttpRequest(); xhr.open('GET', '/' + state, true); xhr.send(); xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { document.getElementById('ledStatus').innerText = xhr.responseText.toUpperCase(); } }; } </script></body></html>"""def wifi_ap_start(ssid, password): wlan = network.WLAN(network.AP_IF) # 使用 AP_IF 而不是 MODE_AP if not wlan.active(True): print("Could not activate AP mode") return wlan.config(essid=ssid, password=password) print("Access Point started with SSID: %s" % ssid) # 等待连接 while not wlan.isconnected(): pass print('Network config:', wlan.ifconfig())def http_response(client_socket, request): request = request.decode() if request.startswith('GET /on HTTP/1.1'): led.value(1) # 打开LED response = 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nLED is ON' elif request.startswith('GET /off HTTP/1.1'): led.value(0) # 关闭LED response = 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nLED is OFF' elif request.startswith('GET / HTTP/1.1'): response = 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n' + HTML_CONTENT else: response = 'HTTP/1.1 404 Not Found\r\n\r\nNot Found' client_socket.sendall(response.encode()) client_socket.close()# 启动WiFi APwifi_ap_start(SSID, PASSWORD)# 创建HTTP服务器addr = ('', 80) # 监听所有传入连接,端口80s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.bind(addr)s.listen(5)print('HTTP server started')while True: client_socket, addr = s.accept() print('Client connected from', addr) client_request = client_socket.recv(1024) if client_request: http_response(client_socket, client_request) 点亮屏幕如何点亮一块屏幕?首先得要将屏幕与设备正确的连接起来,硬件接口就像是设备和屏幕之间的桥梁。ESP32用GPIO引脚连上屏幕,并通过SPI或I2C协议来传送数据。这些协议能确保数据快速、准确地从ESP32传到屏幕上。 接着就要进行数据传输。ESP32要组织好每个像素的颜色和亮度信息,也就是像素数据,然后按照屏幕的分辨率和颜色格式发送出去。它还会在内存里准备一个区域,叫帧缓冲区,用来存一整幅图像的数据。ESP32不断更新这个区域的内容,并发送给屏幕,这样我们就能看到动态的图像了。 最后,运用屏幕显示技术让图像出现在屏幕上。LCD屏幕是通过控制液晶分子来改变光的透过率,而OLED屏幕则是用电场让有机材料发光。不管是哪种屏幕,都需要ESP32发送驱动信号来控制每个像素的显示状态。同时,ESP32还要按照屏幕的刷新率持续发送新的图像数据,这样屏幕上的图像才能保持流畅。 0.96英寸OLED屏幕 如左图是一块0.96英寸(指的是对角线尺寸)OLED(有机发光二极管)屏幕,分辨率为128x64像素。通过观察你可以看到在这一块OLED屏幕上,有GND、VDD、SCK、SDA四个引脚。GND引脚用于接地,确保电路稳定;VDD引脚提供工作电压;SCK引脚作为串行时钟,在数据传输过程中,SCK引脚会按照一定的频率产生时钟信号,控制数据在SDA引脚上的传输速度。;SDA引脚传输数据,在数据传输过程中,SDA引脚会根据SCK引脚提供的时钟信号,逐位地传输数据。 接线环节:GND-GND,VDD-3.3v,SCK-D4,SDA-D5.你可以根据需要变更SCK与SDA的引脚,同时别忘记要在代码上进行修改。 程序环节:在写正式程序时,我们需要导入一个ssd1306.py库,用于驱动: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164#MicroPython SSD1306 OLED driver, I2C and SPI interfaces created by Adafruitimport timeimport framebuf# register definitionsSET_CONTRAST = const(0x81)SET_ENTIRE_ON = const(0xa4)SET_NORM_INV = const(0xa6)SET_DISP = const(0xae)SET_MEM_ADDR = const(0x20)SET_COL_ADDR = const(0x21)SET_PAGE_ADDR = const(0x22)SET_DISP_START_LINE = const(0x40)SET_SEG_REMAP = const(0xa0)SET_MUX_RATIO = const(0xa8)SET_COM_OUT_DIR = const(0xc0)SET_DISP_OFFSET = const(0xd3)SET_COM_PIN_CFG = const(0xda)SET_DISP_CLK_DIV = const(0xd5)SET_PRECHARGE = const(0xd9)SET_VCOM_DESEL = const(0xdb)SET_CHARGE_PUMP = const(0x8d)class SSD1306: def __init__(self, width, height, external_vcc): self.width = width self.height = height self.external_vcc = external_vcc self.pages = self.height // 8 # Note the subclass must initialize self.framebuf to a framebuffer. # This is necessary because the underlying data buffer is different # between I2C and SPI implementations (I2C needs an extra byte). self.poweron() self.init_display() def init_display(self): for cmd in ( SET_DISP | 0x00, # off # address setting SET_MEM_ADDR, 0x00, # horizontal # resolution and layout SET_DISP_START_LINE | 0x00, SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0 SET_MUX_RATIO, self.height - 1, SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0 SET_DISP_OFFSET, 0x00, SET_COM_PIN_CFG, 0x02 if self.height == 32 else 0x12, # timing and driving scheme SET_DISP_CLK_DIV, 0x80, SET_PRECHARGE, 0x22 if self.external_vcc else 0xf1, SET_VCOM_DESEL, 0x30, # 0.83*Vcc # display SET_CONTRAST, 0xff, # maximum SET_ENTIRE_ON, # output follows RAM contents SET_NORM_INV, # not inverted # charge pump SET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14, SET_DISP | 0x01): # on self.write_cmd(cmd) self.fill(0) self.show() def poweroff(self): self.write_cmd(SET_DISP | 0x00) def contrast(self, contrast): self.write_cmd(SET_CONTRAST) self.write_cmd(contrast) def invert(self, invert): self.write_cmd(SET_NORM_INV | (invert & 1)) def show(self): x0 = 0 x1 = self.width - 1 if self.width == 64: # displays with width of 64 pixels are shifted by 32 x0 += 32 x1 += 32 self.write_cmd(SET_COL_ADDR) self.write_cmd(x0) self.write_cmd(x1) self.write_cmd(SET_PAGE_ADDR) self.write_cmd(0) self.write_cmd(self.pages - 1) self.write_framebuf() def fill(self, col): self.framebuf.fill(col) def pixel(self, x, y, col): self.framebuf.pixel(x, y, col) def scroll(self, dx, dy): self.framebuf.scroll(dx, dy) def text(self, string, x, y, col=1): self.framebuf.text(string, x, y, col)class SSD1306_I2C(SSD1306): def __init__(self, width, height, i2c, addr=0x3c, external_vcc=False): self.i2c = i2c self.addr = addr self.temp = bytearray(2) # Add an extra byte to the data buffer to hold an I2C data/command byte # to use hardware-compatible I2C transactions. A memoryview of the # buffer is used to mask this byte from the framebuffer operations # (without a major memory hit as memoryview doesn't copy to a separate # buffer). self.buffer = bytearray(((height // 8) * width) + 1) self.buffer[0] = 0x40 # Set first byte of data buffer to Co=0, D/C=1 self.framebuf = framebuf.FrameBuffer1(memoryview(self.buffer)[1:], width, height) super().__init__(width, height, external_vcc) def write_cmd(self, cmd): self.temp[0] = 0x80 # Co=1, D/C#=0 self.temp[1] = cmd self.i2c.writeto(self.addr, self.temp) def write_framebuf(self): # Blast out the frame buffer using a single I2C transaction to support # hardware I2C interfaces. self.i2c.writeto(self.addr, self.buffer) def poweron(self): passclass SSD1306_SPI(SSD1306): def __init__(self, width, height, spi, dc, res, cs, external_vcc=False): self.rate = 10 * 1024 * 1024 dc.init(dc.OUT, value=0) res.init(res.OUT, value=0) cs.init(cs.OUT, value=1) self.spi = spi self.dc = dc self.res = res self.cs = cs self.buffer = bytearray((height // 8) * width) self.framebuf = framebuf.FrameBuffer1(self.buffer, width, height) super().__init__(width, height, external_vcc) def write_cmd(self, cmd): self.spi.init(baudrate=self.rate, polarity=0, phase=0) self.cs.high() self.dc.low() self.cs.low() self.spi.write(bytearray([cmd])) self.cs.high() def write_framebuf(self): self.spi.init(baudrate=self.rate, polarity=0, phase=0) self.cs.high() self.dc.high() self.cs.low() self.spi.write(self.buffer) self.cs.high() def poweron(self): self.res.high() time.sleep_ms(1) self.res.low() time.sleep_ms(10) self.res.high() 然后你就可以创建一个新文件用于编写主体程序。示例如下: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253from machine import Pin, I2C#OLED=....i2c = I2C(scl=Pin(4), sda=Pin(5))from ssd1306 import SSD1306_I2C OLED= SSD1306_I2C(128, 64, i2c)#fonts=....fonts= { 0xe59bbe: # 图 [0x80,0xBF,0xA0,0xAF,0xA8,0xAB,0xAA,0xAB,0xA8,0xAF,0xA0,0xBF,0x80,0xFF,0x00,0x00, 0x00,0xFE,0x02,0xFA,0x0A,0xEA,0x2A,0xAA,0x2A,0xEA,0x0A,0xFA,0x02,0xFE,0x00,0x00], 0xe5bc80: # 开 [0x00,0x00,0x00,0x00,0x00,0x3F,0x08,0x08,0x7F,0x08,0x10,0x10,0x60,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x00,0xE0,0x40,0x40,0xF8,0x40,0x40,0x40,0x40,0x00,0x00,0x00], 0xe58f91: # 发 [0x00,0x00,0x00,0x00,0x24,0x44,0x7F,0x08,0x0F,0x14,0x22,0x43,0x9C,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x40,0x20,0xF8,0x00,0xE0,0x20,0x40,0x80,0x70,0x00,0x00,0x00], 0xe69dbf: # 板 [0x00,0x00,0x00,0x00,0x23,0x7C,0x24,0x27,0x7D,0xA4,0x24,0x24,0x2B,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0xF0,0x00,0x00,0xF0,0x10,0xA0,0x40,0xA0,0x10,0x00,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_ += 16OLED.text('Hello!', 0, 0)OLED.text('ESP32', 0, 15)chinese('开发板',40,10)chinese('图',90,10)OLED.show() 如果屏幕上能正常显示下面的内容,则说明运行成功: Hello! ESP32开发板 [图像] 对于OLED.text(‘<文本>’, <x>, <y>),<文本>填入英文或者英文符号,否则会在屏幕上出现乱码;不过我们有方法显示中文甚至一个特殊符号。 首先,你需要利用这个网站查询汉字所对应的16进制编码。在这个示例中,只有“开发板”三个字是中文,所以我们通过查询可以得到它们的16进制编码:e5bc80(开)、e58f91(发)、e69dbf(板),这些编码对应的是fonts={}中的自定义显示的内容的前缀。 接下来是生成对应的图案,也就是fonts={}中方括号对应的内容。我们需要图像设计软件将一个文字或者图案转换为这一系列的编码。打开这里所提供的字库设计软件,首先按照图示进行配置: 在主界面选择模式—字符模式,同时你可以将字体修改成微软雅黑,当然设置字体看你个人选择,随后在下面的输入框内输入文字,点击生成字模,这样你就可以看到这三个字对应的图案数据。将其复制进font={}内。 1234567fonts= { 0x<文字16进制编码>: [<图案编码数据>], 0x<文字16进制编码>: [<图案编码数据>], …… } 最后你就可以在代码中通过添加chinese('<文字>',<x>,<y>)进行显示。 TFT1.3屏幕TFT(Thin Film Transistor)指的是该屏幕的材质(薄膜晶体管),1.3指的是屏幕的尺寸(1.3英寸)。这类液晶显示屏分辨率通常为240x240像素,与0.96OLED屏幕不同的是,TFT屏幕是可以显示丰富的彩色的,适用于显示高质量图像。下图是我购买的一块TFT1.3屏幕: VCC:电源正极,提供3.3V或5V工作电压。GND:地线,提供电路的公共参考电位点。SDA:数据线,用于传输像素数据到屏幕。SCL:时钟线,提供时钟信号以同步数据的传输。RES:复位引脚,用于重置屏幕到初始状态。DC:数据/命令选择引脚,指示当前传输的是数据还是命令。BLK:背光控制引脚,用于控制屏幕的背光亮度。 要想让这一块屏幕亮起来并显示点什么,可比前面的OLED复杂多了,首先整个程序分为3个部分:st7789驱动库(用于配合st7789显示屏控制芯片)、字体库(用于显示基础的英文字母,如果可以,你还可以加入中文,但是需要特殊方式转化)、主程序(让屏幕显示出内容) 首先请通过链接直接安装好st7789驱动库和字体库(因为这俩玩意代码太多了,直接copy过来直接帮我水了快5000字了),然后在创建一个新的程序用来写主程序: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051import timefrom machine import Pin, SoftSPIimport st7789 as st7789import vga2_bold_16x32 as fontdef main1(): # 创建SoftSPI对象(SCK对应SCL,MOSI对应SDA) spi = SoftSPI(baudrate=60000000, polarity=1, phase=0, sck=Pin(18), mosi=Pin(23), miso=Pin(19)) # 创建ST7789对象 tft = st7789.ST7789( spi, 240, 240, #(reset就是RST) reset=Pin(15, Pin.OUT), dc=Pin(2, Pin.OUT), cs=Pin(4, Pin.OUT), backlight=Pin(21, Pin.OUT), rotation=0) '''定义颜色''' red = st7789.color565(255, 0, 0) blue = st7789.color565(0, 0, 255) white = st7789.color565(255, 255, 255) blue_background = st7789.color565(0, 255, 128) # 清屏 # 清除屏幕内容 tft.fill(0) # 在屏幕上显示"hello world",颜色为红色 tft.text(font, "Hello World", 30, 50, red) time.sleep(1) # 等待1秒 # 再次清除屏幕内容 tft.fill(0) tft.text(font, "A TFT1.3 Screen", 0, 50, blue) time.sleep(1) # 清除屏幕内容,并设置背景色为蓝色 tft.fill(blue_background) time.sleep(1) tft.fill(0) white = st7789.color565(255, 255, 255) time.sleep(1)main1() 成功运行后,会先显示Hello World,接着显示A TFT1.3 Screen,然后会将整个屏幕填充为淡绿色,最后清屏。 未完待续…. 蓝牙通信ESP32的蓝牙功能是其强大的无线通信能力之一,广泛应用于各种物联网场景中,通过蓝牙连接,ESP32可以实现与其他设备的无线通信和数据交互,为各种应用场景提供便捷和高效的解决方案。ESP32支持双模蓝牙,即同时支持经典蓝牙(Bluetooth Classic)和蓝牙低功耗(Bluetooth Low Energy,简称BLE)。 我们可以通过代码让ESP32开发板成为一个蓝牙服务端。以下是示例代码: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990# 导入模块from machine import Pinfrom machine import Timerfrom time import sleep_msimport bluetooth# 定义全局变量BLE_MSG = ""class ESP32_BLE(): def __init__(self, name): self.led = Pin(2, Pin.OUT) self.timer1 = Timer(0) self.name = name self.ble = bluetooth.BLE() self.ble.active(True) self.ble.config(gap_name=name) self.disconnected() self.ble.irq(self.ble_irq) self.register() self.advertiser() def connected(self): self.led.value(1) self.timer1.deinit() def disconnected(self): self.timer1.init(period=1000, mode=Timer.PERIODIC, callback=lambda t: self.led.value(not self.led.value())) #self.led.value(0) def ble_irq(self, event, data): global BLE_MSG #_IRQ_CENTRAL_CONNECT 蓝牙终端链接了此设备 if event == 1: self.connected() #_IRQ_CENTRAL_DISCONNECT 蓝牙终端断开此设备 elif event == 2: self.advertiser() self.disconnected() #_IRQ_GATTS_WRITE 蓝牙终端向ESP32发送数据,接收数据处理 elif event == 3: buffer = self.ble.gatts_read(self.rx) BLE_MSG = buffer.decode('UTF-8').strip() print("接收到其他蓝牙终端发来的数据:",BLE_MSG) def register(self): service_uuid = '6E400001-B5A3-F393-E0A9-E50E24DCCA9E' reader_uuid = '6E400002-B5A3-F393-E0A9-E50E24DCCA9E' sender_uuid = '6E400003-B5A3-F393-E0A9-E50E24DCCA9E' services = ( ( bluetooth.UUID(service_uuid), ( (bluetooth.UUID(sender_uuid), bluetooth.FLAG_NOTIFY), (bluetooth.UUID(reader_uuid), bluetooth.FLAG_WRITE), ) ), ) ((self.tx, self.rx,), ) = self.ble.gatts_register_services(services) def send(self, data): self.ble.gatts_notify(0, self.tx, data + '\n') def advertiser(self): name = bytes(self.name, 'UTF-8') adv_data = bytearray('\x02\x01\x02', 'UTF-8') + bytearray((len(name) + 1, 0x09), 'UTF-8') + name self.ble.gap_advertise(100, adv_data) print(adv_data) print("\r\n")def main(BLE_NAME): global BLE_MSG #调用BLE 设置ESP32被发现的蓝牙名称 名称为主程序传参传入 ble = ESP32_BLE(BLE_NAME) #蓝牙指示灯(板载蓝色LED),当ESP32设备未被连接,则周期闪烁;若被连接,则常亮 led = Pin(2, Pin.OUT) while True: #使用测试数据需要符合 r#hello if BLE_MSG.split("#")[0] == 'r': #打印获取到的数据 print(BLE_MSG) # 清空接收的数据 BLE_MSG = "" sleep_ms(100)if __name__ == "__main__": main("ESP32") 成功运行代码后,手机端使用蓝牙调试助手连接蓝牙设备(名称为:ESP32),未连接成功时LED指示灯闪烁,连接成功后LED指示灯常亮,此时写入数据如:r#hello,ESP32接收到后会在Shell终端中输出对应的数据。如何使用蓝牙调试助手:打开程序,点击左上角三个横杠打开左侧菜单,点击Devices,在Bluetooth LE中点击右上角的SCAN扫码附近的蓝牙设备(记得把蓝牙打开),出现ESP32时,点击连接。蓝牙调试控制台出现Connected则说明连接成功。你可以在下面的输入框输入任意文字,在IDE控制台中就会接收到相应的文字。 MQTTMQTT(Message Queuing Telemetry Transport)是一种轻量级的消息传输协议,专为小型设备、低带宽、高延迟或不可靠的网络环境设计。它广泛应用于物联网(IoT)中,以实现设备间的通信和数据交换。 MQTT分为客户端、服务端。每个客户端可以接收数据(订阅)、发送数据(发布),每个消息都由主题+消息内容组成。假设我们有4个客户端(a、b、c、d)和1个服务端,这4个客户端都已经通过网络连接上了服务端。假如客户端a向服务器订阅了一个主题为abc的信息,那么当服务器接收到来自任何一个客户端(b/c/d)的消息时,只要这个消息的主题是abc,那么服务器就会将这个消息发送给客户端a同理,如果除了客户端a之外还有其它的客户端(b/c)也订阅了abc这个主题,那么服务器也会同时将这个消息送给客户端b、客户端c。 在本小节,我会尝试创建一个MQTT服务器,并且利用ESP32连接,实现PC、ESP32消息互通。首先,我们要创建一个MQTT服务器,你可以选择在自己的电脑本地创建,也可以利用云资源进行部署。在这里我用云资源演示。 我推荐EMQX,打开网站注册/登录账号后,选择一个合适的产品进行云资源部署,这里以专有版云服务为例,按照指引部署即可,实在不行可以查看他们站内的文档,非常详细。当你创建完一个MQTT云服务器后,请切换到该服务器的控制台界面,找到“连接信息”,这一栏包含了连接地址和各种端口。为了测试,你需要在控制台内添加几个客户端认证用户。 接下来为了测试连接情况,我们需要创建一系列的客户端(至少2个),分别在ESP32和电脑上运行。电脑上你可以选择利用EMQX内的客户端资源,EMQX站内提供了各种编程语言的客户端开发教程,你也可以选择使用现成的MQTT客户端工具:MQTTX。MQTTX提供了一个可视化的操作界面,让MQTT调试变得更简单便捷。如果要MQTT服务端,我推荐GMQT。 下载并安装好MQTTX后,打开程序在出现的窗口找到“General”一栏开始信息填写。Name只是一个区分名,对MQTT的连接影响不是很大;Client ID是你的自定义名称,它用于在MQTT消息中区分不同用户,类似聊天昵称;Host部分则需要在后边一栏填入刚刚在MQTT服务器控制台记下的连接地址;Port代表的是连接端口,默认为1883。对于其它内容和细节,你可以查阅相关文档。 填入完信息后,点击右上角的Connect,当提示Connected时说明连接成功。这时候你需要在左边找到New Subscription并点击它以订阅一个话题。这个话题你可以类比成一个聊天群的群名(或群号),我们可以通过这个聊天群名进行消息发送和接收。订阅就相当于加群,接下来你可以在不同的客户端上用这个“群”来聊天了。 现在我们来到ESP32上,利用代码在ESP32上创建一个MQTT客户端,首先你得要有一个名为umqtt.py的前置库: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210import socketimport structfrom binascii import hexlifyclass MQTTException(Exception): passclass MQTTClient: def __init__( self, client_id, server, port=0, user=None, password=None, keepalive=0, ssl=None, ): if port == 0: port = 8883 if ssl else 1883 self.client_id = client_id self.sock = None self.server = server self.port = port self.ssl = ssl self.pid = 0 self.cb = None self.user = user self.pswd = password self.keepalive = keepalive self.lw_topic = None self.lw_msg = None self.lw_qos = 0 self.lw_retain = False def _send_str(self, s): self.sock.write(struct.pack("!H", len(s))) self.sock.write(s) def _recv_len(self): n = 0 sh = 0 while 1: b = self.sock.read(1)[0] n |= (b & 0x7F) << sh if not b & 0x80: return n sh += 7 def set_callback(self, f): self.cb = f def set_last_will(self, topic, msg, retain=False, qos=0): assert 0 <= qos <= 2 assert topic self.lw_topic = topic self.lw_msg = msg self.lw_qos = qos self.lw_retain = retain def connect(self, clean_session=True): self.sock = socket.socket() addr = socket.getaddrinfo(self.server, self.port)[0][-1] self.sock.connect(addr) if self.ssl: self.sock = self.ssl.wrap_socket(self.sock, server_hostname=self.server) premsg = bytearray(b"\x10\0\0\0\0\0") msg = bytearray(b"\x04MQTT\x04\x02\0\0") sz = 10 + 2 + len(self.client_id) msg[6] = clean_session << 1 if self.user: sz += 2 + len(self.user) + 2 + len(self.pswd) msg[6] |= 0xC0 if self.keepalive: assert self.keepalive < 65536 msg[7] |= self.keepalive >> 8 msg[8] |= self.keepalive & 0x00FF if self.lw_topic: sz += 2 + len(self.lw_topic) + 2 + len(self.lw_msg) msg[6] |= 0x4 | (self.lw_qos & 0x1) << 3 | (self.lw_qos & 0x2) << 3 msg[6] |= self.lw_retain << 5 i = 1 while sz > 0x7F: premsg[i] = (sz & 0x7F) | 0x80 sz >>= 7 i += 1 premsg[i] = sz self.sock.write(premsg, i + 2) self.sock.write(msg) # print(hex(len(msg)), hexlify(msg, ":")) self._send_str(self.client_id) if self.lw_topic: self._send_str(self.lw_topic) self._send_str(self.lw_msg) if self.user: self._send_str(self.user) self._send_str(self.pswd) resp = self.sock.read(4) assert resp[0] == 0x20 and resp[1] == 0x02 if resp[3] != 0: raise MQTTException(resp[3]) return resp[2] & 1 def disconnect(self): self.sock.write(b"\xe0\0") self.sock.close() def ping(self): self.sock.write(b"\xc0\0") def publish(self, topic, msg, retain=False, qos=0): pkt = bytearray(b"\x30\0\0\0") pkt[0] |= qos << 1 | retain sz = 2 + len(topic) + len(msg) if qos > 0: sz += 2 assert sz < 2097152 i = 1 while sz > 0x7F: pkt[i] = (sz & 0x7F) | 0x80 sz >>= 7 i += 1 pkt[i] = sz # print(hex(len(pkt)), hexlify(pkt, ":")) self.sock.write(pkt, i + 1) self._send_str(topic) if qos > 0: self.pid += 1 pid = self.pid struct.pack_into("!H", pkt, 0, pid) self.sock.write(pkt, 2) self.sock.write(msg) if qos == 1: while 1: op = self.wait_msg() if op == 0x40: sz = self.sock.read(1) assert sz == b"\x02" rcv_pid = self.sock.read(2) rcv_pid = rcv_pid[0] << 8 | rcv_pid[1] if pid == rcv_pid: return elif qos == 2: assert 0 def subscribe(self, topic, qos=0): assert self.cb is not None, "Subscribe callback is not set" pkt = bytearray(b"\x82\0\0\0") self.pid += 1 struct.pack_into("!BH", pkt, 1, 2 + 2 + len(topic) + 1, self.pid) # print(hex(len(pkt)), hexlify(pkt, ":")) self.sock.write(pkt) self._send_str(topic) self.sock.write(qos.to_bytes(1, "little")) while 1: op = self.wait_msg() if op == 0x90: resp = self.sock.read(4) # print(resp) assert resp[1] == pkt[2] and resp[2] == pkt[3] if resp[3] == 0x80: raise MQTTException(resp[3]) return # Wait for a single incoming MQTT message and process it. # Subscribed messages are delivered to a callback previously # set by .set_callback() method. Other (internal) MQTT # messages processed internally. def wait_msg(self): res = self.sock.read(1) self.sock.setblocking(True) if res is None: return None if res == b"": raise OSError(-1) if res == b"\xd0": # PINGRESP sz = self.sock.read(1)[0] assert sz == 0 return None op = res[0] if op & 0xF0 != 0x30: return op sz = self._recv_len() topic_len = self.sock.read(2) topic_len = (topic_len[0] << 8) | topic_len[1] topic = self.sock.read(topic_len) sz -= topic_len + 2 if op & 6: pid = self.sock.read(2) pid = pid[0] << 8 | pid[1] sz -= 2 msg = self.sock.read(sz) self.cb(topic, msg) if op & 6 == 2: pkt = bytearray(b"\x40\x02\0\0") struct.pack_into("!H", pkt, 2, pid) self.sock.write(pkt) elif op & 6 == 4: assert 0 return op # Checks whether a pending message from server is available. # If not, returns immediately with None. Otherwise, does # the same processing as wait_msg. def check_msg(self): self.sock.setblocking(False) return self.wait_msg() 参考EMQX的文档,可以跟着教程创建一个名为wifi.py的前置库,用于网络连接: 12345678910111213import networkimport timedef connect(): ssid = 'NAME OF YOUR WIFI NETWORK' password = 'PASSWORD OF YOUR WIFI NETWORK' wlan = network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(ssid, password) while wlan.isconnected() == False: print('Waiting for connection...') time.sleep(1) print('Connected on {ip}'.format(ip = wlan.ifconfig()[0])) 到这里,所有准备工作已经做完了,接下来在ESP32上创建一个MQTT客户端: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273import jsonimport randomimport sslimport timeimport wififrom umqtt import MQTTClientSERVER = "" # 连接地址PORT = 1883 # 连接端口CLIENT_ID = 'micropython-client-{id}'.format(id=random.getrandbits(8)) # 客户端标识USERNAME = '' # 用户名/账号PASSWORD = '' # 用户秘密TOPIC = "" # 订阅的话题def on_message(topic, msg): # 从主题接收到的消息为 {payload},主题是 {topic} print("Received '{payload}' from topic '{topic}'\n".format( # 解码消息内容为字符串 payload = msg.decode(), # 解码主题内容为字符串 topic = topic.decode()))def connect(): # 创建一个MQTT客户端对象 client = MQTTClient(CLIENT_ID, SERVER, PORT, USERNAME, PASSWORD) # 连接MQTT服务器 client.connect() # 打印连接成功的消息 print('Connected to MQTT Broker "{server}"'.format(server = SERVER)) # 返回已连接的MQTT客户端对象 return clientdef subscribe(client): # 设置回调函数为on_message client.set_callback(on_message) # 订阅主题TOPIC client.subscribe(TOPIC)def loop_publish(client): # 初始化消息计数为0 msg_count = 0 # 无限循环 while True: # 创建一个字典,包含键'msg'和对应的值msg_count msg_dict = { 'msg': msg_count } # 将字典转换为JSON格式的字符串 msg = json.dumps(msg_dict) # 使用client的publish方法发布消息到指定主题 result = client.publish(TOPIC, msg) # 打印发送的消息和主题 print("Send '{msg}' to topic '{topic}'".format(msg=msg, topic=TOPIC)) # 等待接收消息 client.wait_msg() # 消息计数加1 msg_count += 1 # 等待1秒 time.sleep(1)def run(): # 连接WiFi wifi.connect() # 建立客户端连接 client = connect() # 订阅消息 subscribe(client) # 循环发布消息 loop_publish(client)if __name__ == "__main__": run() 上述代码运行成功后,会周期性地发布消息。接下来我们根据这个代码的原理创建一个类似于聊天机器人的程序,并让它在ESP32上运行。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495import jsonimport randomimport timeimport machineimport wififrom umqtt import MQTTClientSERVER = "" # 连接地址PORT = 1883 # 连接端口CLIENT_ID = 'micropython-client-{id}'.format(id=random.getrandbits(8)) # 客户端标识USERNAME = '' # 用户名/账号PASSWORD = '' # 用户秘密TOPIC = "" # 订阅的话题led = machine.Pin(2, machine.Pin.OUT) # 初始化LED引脚client = Nonedef on_message(topic, msg): global client # 打印从指定主题接收到的消息 print("Received '{payload}' from topic '{topic}'".format(payload=msg.decode(), topic=topic.decode())) # 将接收到的消息解码为字符串 message = msg.decode() # 初始化响应字符串 response = "" # 根据接收到的消息内容执行相应的操作 if message == '你好': # 如果消息内容为'你好',则设置响应字符串为'Hello,I'm ESP32.' response = "Hello,I'm ESP32." elif message == '打开LED': # 如果消息内容为'打开LED',则调用led_on()函数打开LED灯,并设置响应字符串为'LED is ON!'(下面的部分以此类推) led_on() response = 'LED is ON!' elif message == '关闭LED': led_off() response = 'LED is OFF!' elif message == 'LED闪烁': led_blink() response = 'LED is Blinking!' # 如果response不为空,则发送MQTT消息 if response: # 如果response不为空,则发送MQTT消息 # 调用publish()函数发送MQTT消息 publish(client, response)def connect(): client = MQTTClient(CLIENT_ID, SERVER, PORT, USERNAME, PASSWORD) client.connect() print('Connected to MQTT Broker "{server}"'.format(server=SERVER)) publish(client, "ESP32: Online Mode.") return clientdef subscribe(client): client.set_callback(on_message) client.subscribe(TOPIC)def publish(client, message): #msg = json.dumps({'esp32_msg': message}) result = client.publish(TOPIC, message) print("Sent '{msg}' to topic '{topic}'".format(msg=message, topic=TOPIC))# 点亮LED的函数def led_on(): led.value(1) print('LED is on')# 关闭LED的函数def led_off(): led.value(0) print('LED is off')# LED闪烁的函数def led_blink(): print('LED is blinking') for _ in range(5): led.value(1) time.sleep(0.5) led.value(0) time.sleep(0.5)def run(): global client wifi.connect() client = connect() subscribe(client) while True: try: client.wait_msg() # 持续监听MQTT消息 except OSError as e: print("An OSError occurred: {}".format(e)) # 尝试重新连接 client.disconnect() time.sleep(5) # 等待一段时间后重试 client = connect() subscribe(client)if __name__ == "__main__": run() 成功运行后,ESP32会根据你发送的关键词消息进行相应的操作,比如你发送打开LED时,开发板上的LED就会点亮,同时会在控制台和MQTT服务器上发送相应的信息提示。 传感器系列数字温度传感器(DS18B20):DS18B20是一款常用的高精度的单总线数字温度测量芯片。具有体积小,硬件开销低,抗干扰能力强,精度高的特点。其测温范围为-55℃到+125℃,在-10℃到+85℃范围内误差为±0.4°。供电电压在2.5V~5.5V之间。 DS18B20返回的16位二进制数代表此刻探测的温度值,其高五位代表正负。如果高五位全部为1,则代表返回的温度值为负值。如果高五位全部为0,则代表返回的温度值为正值。后面的11位数据代表温度的绝对值,将其转换为十进制数值之后,再乘以0.0625即可获得此时的温度值。 将传感器正负极分别连接至ESP32的VIN与GND引脚,将DQ连接至任意一个GPIO引脚,比如D13。接下来就是写代码获取传感器的数据就行。不过micropython自带一个DS18B20库,并有相对应的API(来自文档),所以我们可以直接利用这个库写程序。下面是示例代码: 1234567891011121314151617181920212223242526272829303132from machine import Pinimport onewire, ds18x20import time# 初始化DS18X20温度传感器ds_pin = Pin(13)ds_sensor = ds18x20.DS18X20(onewire.OneWire(ds_pin))def read_ds_sensor(): """ 读取DS18X20温度传感器的温度值。 返回: float: 返回检测到的温度值,四舍五入到小数点后两位。 如果没有检测到有效的温度,则返回None。 """ roms = ds_sensor.scan() # 扫描设备 print('发现设备:', roms) ds_sensor.convert_temp() # 开始温度转换 for rom in roms: temp = ds_sensor.read_temp(rom) # 读取温度 if isinstance(temp, float): # 确保读取到的是有效的浮点数温度值 return round(temp, 2) # 返回四舍五入后的温度值 # 如果没有检测到有效的温度,则返回None# 无限循环,每秒读取并打印温度值while True: temp = read_ds_sensor() # 读取温度值 if temp is not None: # 如果读取到了有效的温度值,则打印出来 print(temp) else: # 如果没有读取到有效的温度值,则打印提示信息 print("未检测到有效的温度值。") time.sleep(1) # 每次读取后暂停1秒 运行后会看到控制台每隔1s发送一次温度数据,如果程序运行的同时你将传感器断开,程序会马上报错终止。接下来我们做进一步的升级:比如我将温度数据利用一个网页来显示,将上面的代码与http服务器的代码相结合就是我们想要的了。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465import machineimport onewire, ds18x20import timeimport networkimport usocket# 初始化DS18X20温度传感器ds_pin = machine.Pin(13)ds_sensor = ds18x20.DS18X20(onewire.OneWire(ds_pin))# 读取DS18X20温度传感器的温度值def read_ds_sensor(): roms = ds_sensor.scan() ds_sensor.convert_temp() for rom in roms: temp = ds_sensor.read_temp(rom) if isinstance(temp, float): return round(temp, 2) return None# 设置ESP32为AP模式def setup_ap_mode(ssid, password): wlan = network.WLAN(network.AP_IF) wlan.active(True) wlan.config(essid=ssid, password=password) while not wlan.active(): pass print('AP模式已启动,SSID:', ssid)# Web服务器处理函数def web_server(port=80): s = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM) s.bind(('', port)) s.listen(5) print('Web服务器已启动,端口:', port) while True: conn, addr = s.accept() print('客户端连接:', addr) request = conn.recv(1024) response = create_http_response(read_ds_sensor()) conn.sendall(response) conn.close()# 创建HTTP响应def create_http_response(temp): if temp is not None: http_header = b"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" html_body = f"<html><body><h1>当前温度: {temp}°C</h1></body></html>" response = http_header + html_body.encode() else: http_header = b"HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/html\r\n\r\n" html_body = "<html><body><h1>温度读取错误</h1></body></html>" response = http_header + html_body.encode() return response# 主程序def main(): ssid = 'ESP32-Temperature' password = 'password123' setup_ap_mode(ssid, password) web_server(8080)if __name__ == "__main__": main() 首先配置ESP32为无线接入点(AP)模式,并设置特定的SSID和密码,以便其他设备可以连接到这个WiFi网络。然后,它启动一个Web服务器,监听特定端口上的连接请求。当有客户端连接到服务器时,代码会读取温度传感器的当前值,并创建一个HTTP响应,将温度信息以HTML格式发送给客户端。 当你的设备连接上一个名为“ESP32-Temperature”的网络时,在浏览器输入地址192.168.4.1:8080后就能在出现的网页上看到温度传感器的数据了。由于此段代码比较简单,想要更新网页上的温度数据还得不断的刷新网页。 温湿度传感器(DHT11): DHT11是一款有已校准数字信号输出的温湿度传感器。 其精度湿度±5%RH, 温度±2℃,量程湿度595%RH, 温度-20+60℃。它采用了单线制串行接口,这使得系统集成变得非常简单和快捷。DHT11的每个传感器都在非常精确的湿度校验室中进行校准,并将校准系数以程序的形式存储在OTP内存中,使得传感器在检测信号的处理过程中能够调用这些校准系数。 图片展示的就是一个DHT11传感器的模块,只有三个引脚的它可以很方便的与开发板连接。与前面的DS18B20一样,连接的时候正极对VIN,负极对GND,数据输出(中间的那个)对任意一个GPIO引脚(本例用D13)。很巧的是,micropython自带一个dht库用于驱动DHT11 传感器,示例代码如下: 1234567891011121314151617181920212223242526272829from machine import Pinimport dhtimport time# 设置DHT11传感器连接的GPIO引脚dht_pin = Pin(15, Pin.IN)# 初始化DHT11传感器对象dht11 = dht.DHT11(dht_pin)while True: try: # 读取传感器数据 dht11.measure() # 获取温度和湿度值 temp = dht11.temperature() hum = dht11.humidity() # 打印温度和湿度到控制台 print("Temperature: %.0f °C" % temp) print("Humidity: %.0f %%" % hum) except OSError as e: # 如果读取失败,打印错误信息 print("Failed to read sensor, try again!") print(e) # 等待一段时间再次读取,这里设置为1秒 time.sleep(1) 接下来我们继续拓展这个代码。在前文中我们利用OLED来显示文字,那么我们可以将这串代码升级成可在OLED屏幕上实时显示读数。对于屏幕的接线:GND-GND,VDD-3.3v,SCK-D4,SDA-D5;DHT11传感器的接线:+对VIN,-对GND(换一个接),数据输出接D13。连接后将下列代码导入进开发板。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798from machine import Pin, I2Cfrom ssd1306 import SSD1306_I2C import dhtimport time# 设置DHT11传感器连接的GPIO引脚dht_pin = Pin(13, Pin.IN)# 初始化DHT11传感器对象dht11 = dht.DHT11(dht_pin)#OLED=....i2c = I2C(scl=Pin(4), sda=Pin(5))OLED= SSD1306_I2C(128, 64, i2c)#fonts=....fonts= { 0xe6b8a9: # 温 [0x00,0x23,0x12,0x12,0x83,0x42,0x42,0x13,0x10,0x27,0xE4,0x24,0x24,0x24,0x2F,0x00, 0x00,0xF8,0x08,0x08,0xF8,0x08,0x08,0xF8,0x00,0xFC,0xA4,0xA4,0xA4,0xA4,0xFE,0x00], 0xe5baa6: # 度 [0x01,0x00,0x3F,0x22,0x22,0x3F,0x22,0x22,0x23,0x20,0x2F,0x24,0x42,0x41,0x86,0x38, 0x00,0x80,0xFE,0x20,0x20,0xFC,0x20,0x20,0xE0,0x00,0xF0,0x10,0x20,0xC0,0x30,0x0E], 0xe6b9bf: # 湿 [0x00,0x27,0x14,0x14,0x87,0x44,0x44,0x17,0x11,0x21,0xE9,0x25,0x23,0x21,0x2F,0x00, 0x00,0xF8,0x08,0x08,0xF8,0x08,0x08,0xF8,0x20,0x20,0x24,0x28,0x30,0x20,0xFE,0x00], 0xe28483: # ℃ [0x00,0x00,0x00,0x38,0x2B,0x14,0x08,0x18,0x18,0x18,0x18,0x08,0x0C,0x07,0x00,0x00, 0x00,0x00,0x00,0x08,0xF8,0x18,0x08,0x00,0x00,0x00,0x00,0x08,0x18,0xE0,0x00,0x00], 0xe59bbe: # % [0x00,0x00,0x1E,0x12,0x12,0x1E,0x00,0x00,0x01,0x03,0x06,0x0C,0x18,0x00,0x00,0x00, 0x00,0x00,0x04,0x0C,0x18,0x30,0x60,0xC0,0x80,0x3C,0x24,0x24,0x3C,0x00,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# 主循环,不断读取传感器数据并更新OLED显示while True: try: # 读取传感器数据 dht11.measure() temp = dht11.temperature() hum = dht11.humidity() # 清空OLED屏幕 OLED.fill(0) OLED.text("-----------------",0,0) OLED.text("|Sensor:DHT11 |",0,5) OLED.text("-----------------",0,10) # 在OLED上显示温度和湿度 chinese('温度', 25, 20) OLED.text('%.0f' % temp, 65, 25) # 显示温度值 chinese('℃', 85, 20) # 显示温度单位 chinese('湿度', 25, 40) OLED.text('%.0f' % hum, 65, 45) # 显示湿度值 chinese('图', 85, 42) # 显示湿度单位 # 更新OLED屏幕显示 OLED.show() except OSError as e: # 如果读取失败,打印错误信息(可选:也可以在OLED上显示错误信息) OLED.fill(0) # 清空屏幕 OLED.text("[ERROR]",0,5) OLED.text("Failed to read ", 0, 15) # 显示消息 OLED.text("sensor!",0,25) OLED.show() # 更新屏幕显示 print("Failed to read sensor, try again!") print(e) # 等待一段时间再次读取,这里设置为1秒 time.sleep(1) 关于正负极接线也不是强制要求要分开连接,屏幕和传感器的正负极都可以连接至ESP32上的同一VCC和GND上。其实你还可以通过外部电源对二者供电,只要这个电源能够驱动传感器或者屏幕,一般在3.3v~5v。但是,需要注意的是,虽然传感器和ESP32使用不同的电源,但它们的地线(GND)应该是共用的,以确保它们之间的电平参考是一致的。如果不这样做,可能会导致数据传输不稳定或其他不可预测的问题。最后只需将传输数据的引脚与ESP32连接就行(如DHT11的数据输出,OLED屏幕的SCK和SDA)。 高精度温湿度传感器(AHT20) AHT20是一款性能优越、易于集成的温湿度传感器,相比于DHT11,AHT20的精度更高,量程更广,稳定性高而且体积小,可作为DHT11的替代。它能够满足多种高精度应用场景的需求。其小巧的体积和低功耗特点使其非常适合在空间受限和电池供电的设备中使用。 AHT20基于热敏电阻和电容式湿度传感器的原理进行工作。热敏电阻能够感应温度的变化,而电容式湿度传感器则能够感应空气中的湿度。当温度变化时,热敏电阻的阻值会发生变化,从而引起电压的变化;而当湿度变化时,电容式湿度传感器的电容值也会发生变化,从而引起频率的变化。这两个信号经过AHT20内部的电路处理后,通过I2C接口输出数字信号。 接下来我们要利用micropython代码写一个能读取AHT20数据的程序。首先,你需要在ESP32上下载一个能够驱动并识别AHT20的库(axtx0.py): 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118# The MIT License (MIT)## Copyright (c) 2020 Kattni Rembor for Adafruit Industries# Copyright (c) 2020 Andreas Bühl## Permission is hereby granted, free of charge, to any person obtaining a copy# of this software and associated documentation files (the "Software"), to deal# in the Software without restriction, including without limitation the rights# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell# copies of the Software, and to permit persons to whom the Software is# furnished to do so, subject to the following conditions:## The above copyright notice and this permission notice shall be included in# all copies or substantial portions of the Software.## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN# THE SOFTWARE."""MicroPython driver for the AHT10 and AHT20 Humidity and Temperature SensorAuthor(s): Andreas Bühl, Kattni Rembor""" import utimefrom micropython import const class AHT10: """Interface library for AHT10/AHT20 temperature+humidity sensors""" AHTX0_I2CADDR_DEFAULT = const(0x38) # Default I2C address AHTX0_CMD_INITIALIZE = 0xE1 # Initialization command AHTX0_CMD_TRIGGER = const(0xAC) # Trigger reading command AHTX0_CMD_SOFTRESET = const(0xBA) # Soft reset command AHTX0_STATUS_BUSY = const(0x80) # Status bit for busy AHTX0_STATUS_CALIBRATED = const(0x08) # Status bit for calibrated def __init__(self, i2c, address=AHTX0_I2CADDR_DEFAULT): utime.sleep_ms(20) # 20ms delay to wake up self._i2c = i2c self._address = address self._buf = bytearray(6) self.reset() if not self.initialize(): raise RuntimeError("Could not initialize") self._temp = None self._humidity = None def reset(self): """Perform a soft-reset of the AHT""" self._buf[0] = self.AHTX0_CMD_SOFTRESET self._i2c.writeto(self._address, self._buf[0:1]) utime.sleep_ms(20) # 20ms delay to wake up def initialize(self): """Ask the sensor to self-initialize. Returns True on success, False otherwise""" self._buf[0] = self.AHTX0_CMD_INITIALIZE self._buf[1] = 0x08 self._buf[2] = 0x00 self._i2c.writeto(self._address, self._buf[0:3]) self._wait_for_idle() if not self.status & self.AHTX0_STATUS_CALIBRATED: return False return True @property def status(self): """The status byte initially returned from the sensor, see datasheet for details""" self._read_to_buffer() return self._buf[0] @property def relative_humidity(self): """The measured relative humidity in percent.""" self._perform_measurement() self._humidity = ( (self._buf[1] << 12) | (self._buf[2] << 4) | (self._buf[3] >> 4) ) self._humidity = (self._humidity * 100) / 0x100000 return self._humidity @property def temperature(self): """The measured temperature in degrees Celcius.""" self._perform_measurement() self._temp = ((self._buf[3] & 0xF) << 16) | (self._buf[4] << 8) | self._buf[5] self._temp = ((self._temp * 200.0) / 0x100000) - 50 return self._temp def _read_to_buffer(self): """Read sensor data to buffer""" self._i2c.readfrom_into(self._address, self._buf) def _trigger_measurement(self): """Internal function for triggering the AHT to read temp/humidity""" self._buf[0] = self.AHTX0_CMD_TRIGGER self._buf[1] = 0x33 self._buf[2] = 0x00 self._i2c.writeto(self._address, self._buf[0:3]) def _wait_for_idle(self): """Wait until sensor can receive a new command""" while self.status & self.AHTX0_STATUS_BUSY: utime.sleep_ms(5) def _perform_measurement(self): """Trigger measurement and write result to buffer""" self._trigger_measurement() self._wait_for_idle() self._read_to_buffer() class AHT20(AHT10): AHTX0_CMD_INITIALIZE = 0xBE # Calibration command 然后就可以使用相关函数进行数据的读取了,按照下面代码的指示连接好传感器(一定一定一定不要接错,尤其是正负极,如果接错的话可能会直接把传感器烧坏)。如果能在控制台看到相关输出的数据就说明运行成功: 12345678910111213141516171819202122232425from machine import Pin,SPI,I2C,PWMfrom ahtx0 import AHT20import time # 初始化i2c = I2C(1,scl = Pin(25),sda = Pin(26),freq = 400_000)aht = AHT20(i2c) def ahtxx(): # 获取温度 temp = aht.temperature # 获取湿度 humi = aht.relative_humidity print("温度= %.2f ℃"%(temp)) print("湿度= %.2f %s" %(humi,'%')) time.sleep(0.1) def main(): while True: ahtxx() if __name__ == "__main__": main() 温湿度+气压海拔传感器(AHT20+BMP280) BMP280是一款由博世公司推出的高精度、小体积、超低功耗的数字气压传感器。它可以测量环境温度和大气压强,广泛应用于各种需要精确测量气压和温度的场合。BMP280的工作原理主要基于压阻式传感器技术。传感器内部有一个微小的压阻传感器和一个温度传感器。当气压或温度发生变化时,这些传感器会产生相应的电信号。 具体来说,压阻传感器由一系列微小的电阻组成,当气压变化时,这些电阻会随之发生变化。通过测量这些电阻的变化,BMP280可以精确地计算出气压的变化。同时,温度传感器用于测量环境温度,以便对气压测量进行温度补偿,从而提高测量精度。 市面上有一款将AHT20+BMP280集成在一起的传感器,咱们可以直接使用4个引脚来控制它们。 在前面AHT20的基础上,我们还需要添加一个BMP280的库(bmp280.py): 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101from micropython import constfrom machine import I2C class BMP280(): def __init__(self, i2c,address = None): self.i2c = i2c self.address = address self.tb = bytearray(1) self.rb = bytearray(1) self.dig_T1 = self.get2Reg(0x88) self.dig_T2 = self.short(self.get2Reg(0x8A)) self.dig_T3 = self.short(self.get2Reg(0x8C)) self.dig_P1 = self.get2Reg(0x8E) self.dig_P2 = self.short(self.get2Reg(0x90)) self.dig_P3 = self.short(self.get2Reg(0x92)) self.dig_P4 = self.short(self.get2Reg(0x94)) self.dig_P5 = self.short(self.get2Reg(0x96)) self.dig_P6 = self.short(self.get2Reg(0x98)) self.dig_P7 = self.short(self.get2Reg(0x9A)) self.dig_P8 = self.short(self.get2Reg(0x9C)) self.dig_P9 = self.short(self.get2Reg(0x9E)) self.mode = 3 self.osrs_p = 3 self.osrs_t = 1 self.setReg(0xF4, 0x2F) self.setReg(0xF5, 0x0C) self.filter = 3 self.T = 0 self.P = 0 self.version = '1.0' def short(self, dat): if dat > 32767: return dat - 65536 else: return dat # set reg def setReg(self, reg, dat): self.tb[0] = dat self.i2c.writeto_mem(self.address, reg, self.tb) # get reg def getReg(self, reg): self.i2c.readfrom_mem_into(self.address, reg, self.rb) return self.rb[0] # get two reg def get2Reg(self, reg): return self.getReg(reg) + self.getReg(reg+1) * 256 def get(self): adc_T = (self.getReg(0xFA)<<12) + (self.getReg(0xFB)<<4) + (self.getReg(0xFC)>>4) var1 = (((adc_T>>3)-(self.dig_T1<<1))*self.dig_T2)>>11 var2 = (((((adc_T>>4)-self.dig_T1)*((adc_T>>4) - self.dig_T1))>>12)*self.dig_T3)>>14 t = var1+var2 self.T = ((t * 5 + 128) >> 8)/100 var1 = (t>>1) - 64000 var2 = (((var1>>2) * (var1>>2)) >> 11 ) * self.dig_P6 var2 = var2 + ((var1*self.dig_P5)<<1) var2 = (var2>>2)+(self.dig_P4<<16) var1 = (((self.dig_P3*((var1>>2)*(var1>>2))>>13)>>3) + (((self.dig_P2) * var1)>>1))>>18 var1 = ((32768+var1)*self.dig_P1)>>15 if var1 == 0: return # avoid exception caused by division by zero adc_P = (self.getReg(0xF7)<<12) + (self.getReg(0xF8)<<4) + (self.getReg(0xF9)>>4) p=((1048576-adc_P)-(var2>>12))*3125 if p < 0x80000000: p = (p << 1) // var1 else: p = (p // var1) * 2 var1 = (self.dig_P9 * (((p>>3)*(p>>3))>>13))>>12 var2 = (((p>>2)) * self.dig_P8)>>13 self.P = p + ((var1 + var2 + self.dig_P7) >> 4) return [self.T, self.P] # get Temperature in Celsius def getTemp(self): self.get() return self.T # get Pressure in Pa def getPress(self): self.get() return self.P # Calculating absolute altitude def getAltitude(self): return 44330*(1-(self.getPress()/101325)**(1/5.255)) # sleep mode def poweroff(self): self.setReg(0xF4, 0) # normal mode def poweron(self): self.setReg(0xF4, 0x2F) 接着编写传感器运行的主程序: 1234567891011121314151617181920212223242526272829303132333435363738394041# AHT20+BMP280温度+湿度+气压+海拔测量from machine import Pin, I2Cfrom ahtx0 import AHT20from bmp280 import BMP280import timei2c = I2C(0, scl=Pin(4), sda=Pin(5), freq=800_000)bmp = BMP280(i2c, 0x77)aht = AHT20(i2c, 0x38)def display(): try: """BMP280压力传感器信息""" temp = bmp.getTemp() press = bmp.getPress() altitude = bmp.getAltitude() print("===============================") print(f"压力: {press:.2f} Pa") print(f"BMP280温度: {temp:.2f} ℃") #海拔测量待验证,仅供参考 #print(f"海拔: {altitude:.2f} m") """AHT20传感器信息""" humidity = aht.relative_humidity temperature = aht.temperature print(f"AHT20湿度: {humidity:.2f} %") print(f"AHT20温度: {temperature:.2f} ℃") print("===============================") except RuntimeError: print("传感器异常!")def main(): for i in i2c.scan(): print("%#x" % i) while True: display() time.sleep(2) if __name__ == "__main__": main() 这样你就可以一次性获取温度、湿度、气压的值了。需要注意的是,虽然我们可以根据这几个数据来计算海拔高度,但这个海拔高度会随着各种环境因素的影响而影响,因此可能不准确,所以将其注释掉了。 超声波测距(HC-SR04) HC-SR04是一个超声波测距模块,通过发出超声波然后接收超声波,利用这个往返时间算出距离的模块。 首先该模块有四个口VCC、Gnd、Trig、Echo。在模块工作的时候,先要给Trig口拉高电平至少10us,然后Echo口会返回一个高电平信号,同时发射超声波。在HC-SR04接收到发出去反射回来的声波后,会给一个低电平。至此,一次检测就完成了。我们可以用定时器在发射声波时开始计数,等到接收声波后,停止计数。这样我们就有了时间。但是这个是往返时间,我们要除2,声音的传播速度时340m/s(这里暂时不考虑其他因素的影响)。有了时间和速度,路程就可以算出来了。接下来就是用代码实现这个过程了: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051import machinefrom machine import Pin, I2Cimport utime#from ssd1306 import SSD1306_I2Cdef getDistance(trigger, echo): # 将触发引脚拉低,然后暂停2微秒,以确保其未激活 trigger.value(0) utime.sleep_us(2) # 在将触发器引脚拉低之前,将触发器引脚拉高5微秒 # 这将从超声波传感器发送一个短脉冲,然后关闭该脉冲 trigger.value(1) utime.sleep_us(5) trigger.value(0) # 创建一个while循环以检查回波引脚。 # 如果没有收到回波脉冲,则更新变量,使其包含以微秒为单位的时间戳。 while echo.value() == 0: signal_off = utime.ticks_us() # 若收到了回波则开始另一个while循环,不断将当前时间戳(以微秒为单位)存储到signal_on变量中直至回波结束 while echo.value() == 1: signal_on = utime.ticks_us() # 计算从超声波脉冲发出到返回的总时间 time_passed = signal_on - signal_off # 将总时间乘以声速(343.2m/s,即0.0343cm/μs),该方程的乘积除以2,得到目标距离(厘米)。 distance = (time_passed * 0.0343) / 2 return distance# 创建触发信号管脚对象trigger = Pin(13, Pin.OUT)# 创建返回信号管脚对象echo = Pin(12, Pin.IN)# 初始化OLED显示屏#i2c = I2C(scl=Pin(4), sda=Pin(5))#OLED= SSD1306_I2C(128, 64, i2c)# 循环不断测量距离while True: distance = getDistance(trigger, echo) # 打印距离到控制台 print("Distance:", distance, "CM") # 将距离显示在OLED上 #OLED.fill(0) #OLED.text("Distance: ", 0, 0) #OLED.text('{:.2f}'.format(distance), 70, 0) #OLED.text(" CM", 90, 0) #OLED.show() utime.sleep(0.08) 上述代码你可以将有关OLED屏幕的部分的代码注释给去除,这样的话就可以在OLED屏幕上实时显示距离了。需要注意的是,超声波测距传感器虽然可以让我们很轻松的通过代码精确测量大范围距离,但也存在些缺陷。由于是基于超声波测距的原理,传感器很容易受到环境温度(声速随温度变化)、测量角度(传感器有一个相对狭窄的测量束角,通常约为15度,这意味着它可能无法检测到与其不正对的物体)、噪声(其他声源可能干扰传感器的信号)、表面特性敏感性(某些类型的表面,如吸声材料,可能吸收超声波信号,导致测量不准确)等因素的影响。 光照强度传感器(BH1750) 光照强度是一种物理术语,指单位面积上所接受可见光的光通量。简称照度 ,单位勒克斯(Lux或lx)。用于指示光照的强弱和物体表面积被照明程度的量。 BH1750是一种数字式光照强度传感器,其工作原理基于光敏电阻和数字转换器。传感器内部包含一个光敏电阻,其电阻值随着环境光照强度的变化而变化。BH1750还内置了一个模拟电路,主要用于将光敏电阻的变化转换为电压信号。此外,BH1750还内置了一个12位的模数转换器(ADC),用于将模拟电路输出的电压信号转换为数字信号。 接下来就是写代码环节。要使用BH1750,首先得装一个库(bh1750.py): 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061"""bh1750.pyMicropython BH1750 ambient light sensor driver."""from utime import sleep_msclass BH1750(): """Micropython BH1750 ambient light sensor driver.""" PWR_OFF = 0x00 PWR_ON = 0x01 RESET = 0x07 # modes CONT_LOWRES = 0x13 CONT_HIRES_1 = 0x10 CONT_HIRES_2 = 0x11 ONCE_HIRES_1 = 0x20 ONCE_HIRES_2 = 0x21 ONCE_LOWRES = 0x23 # default addr=0x23 if addr pin floating or pulled to ground # addr=0x5c if addr pin pulled high def __init__(self, bus, addr=0x23): self.bus = bus self.addr = addr self.off() self.reset() def off(self): """Turn sensor off.""" self.set_mode(self.PWR_OFF) def on(self): """Turn sensor on.""" self.set_mode(self.PWR_ON) def reset(self): """Reset sensor, turn on first if required.""" self.on() self.set_mode(self.RESET) def set_mode(self, mode): """Set sensor mode.""" self.mode = mode self.bus.writeto(self.addr, bytes([self.mode])) def luminance(self, mode): """Sample luminance (in lux), using specified sensor mode.""" # continuous modes if mode & 0x10 and mode != self.mode: self.set_mode(mode) # one shot modes if mode & 0x20: self.set_mode(mode) # earlier measurements return previous reading sleep_ms(24 if mode in (0x13, 0x23) else 180) data = self.bus.readfrom(self.addr, 2) factor = 2.0 if mode in (0x11, 0x21) else 1.0 return (data[0]<<8 | data[1]) / (1.2 * factor) 接着调用这个库,写主程序(基本就是导入库-定引脚-取读数这个流程): 1234567891011121314151617181920"""bh1750光强传感器"""from machine import Pin, SoftI2Cfrom bh1750 import BH1750import time# Initialize I2C communication (ESP32)i2c = SoftI2C(scl=Pin(4), sda=Pin(5), freq=400000)# Initialize I2C communication (ESP8266)# i2c = SoftI2C(scl=Pin(4), sda=Pin(5), freq=400000)# Create BH1750 objectlight_sensor = BH1750(bus=i2c, addr=0x23)while True: time.sleep(1) lux = light_sensor.luminance(BH1750.CONT_HIRES_1) print("Luminance: {:.2f} lux".format(lux)) 运动传感器(MPU6050) MPU6050是一款集成3轴陀螺仪和3轴加速度计的6轴运动处理传感器,支持±2g至±16g的加速度量程和最高±2000°/秒的陀螺仪量程。它通过I2C接口与微控制器通信,可广泛应用于姿态检测和运动追踪等消费电子产品中。加速度计根据牛顿第二定律(F=ma)工作,通过测量物体受力来计算加速度。在MPU6050中,它用小块质量和弹簧来测加速度。物体加速时,质量块因惯性会移动,这移动被转成电信号。 陀螺仪则基于角动量守恒,测物体旋转速度来得知其姿态。当物体旋转时,振荡器产生与旋转速度相关的力,然后将这种力转化为电信号。 加速度计在静止时准确,可测倾斜角度,但运动时就不准了。而陀螺仪测的是旋转速度,积分后可得角度,静止时可能因噪音有误差,但运动时仍准确。也就是说,加速度计具有静态稳定性,陀螺仪具有动态稳定性。两者各有优缺点,结合使用,互补滤波,就能更稳定地知道物体的姿态了。 引脚功能: VCC:电源正极,为模块提供工作所需的电能。 GND:电源负极,接地,确保电路的正常工作。 SCL:I2C通信的时钟线,用于同步数据传输的速率。 SDA:I2C通信的数据线,用于在主设备和从设备之间传输数据。 INT:中断引脚,用于输出中断信号,当MPU6050检测到特定事件时,可以通过该引脚输出中断信号。 AD0:地址选择引脚,用于设置I2C从设备的地址,控制MPU6050的I2C地址。 XDA/XCL:用于外接电磁传感器,如磁力计,组成九轴传感器,提供额外的数据输入以提高数据稳定性。 接下来我们利用MPU6050和0.96英寸OLED屏幕,来整一个实时获取传感器各轴加速度的值和角速度的值。在写主程序之前,我们需要为ESP32加装一个可用于处理分析MPU6050传回数据的库(mpu6050.py): 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788import machineclass accel(): def __init__(self, i2c, gr,addr=0x68): self.iic = i2c self.addr = addr # 陀螺仪量程设置: # 寄存器地址 写入数据 量程 # 0x1b 0x00 ±250°/s # 0x1b 0x08 ±500°/s # 0x1b 0x10 ±1000°/s # 0x1b 0x18 ±2000°/s # 加速度计量程设置: # 寄存器地址 写入数据 量程 # 0x1c 0x00 ±2G # 0x1c 0x08 ±4G # 0x1c 0x10 ±8G # 0x1c 0x18 ±16G #设置加速度倍率 self.temp = bytearray(2) self.temp[0]=0x1C self.temp[1]=8*gr self.iic.writeto(self.addr, self.temp) #a = self.iic.readfrom_mem(self.addr, 0x1C, 1) #print(a) #向0x6B寄存器写入0 #表示开始数据传输 self.temp[0] = 0x6B # Co=1, D/C#=0 self.temp[1] = 0 self.iic.writeto(self.addr, self.temp) def set_gyro_range(self, gr): """ 设置陀螺仪量程。 参数gr: 0: ±250°/s 1: ±500°/s 2: ±1000°/s 3: ±2000°/s """ self.temp = bytearray(2) self.temp[0] = 0x1B # 陀螺仪量程设置寄存器地址 if gr == 0: self.temp[1] = 0x00 # ±250°/s elif gr == 1: self.temp[1] = 0x08 # ±500°/s elif gr == 2: self.temp[1] = 0x10 # ±1000°/s elif gr == 3: self.temp[1] = 0x18 # ±2000°/s else: raise ValueError("Invalid gyro range value. Use 0, 1, 2, or 3.") self.iic.writeto(self.addr, self.temp) def get_raw_values(self): a = self.iic.readfrom_mem(self.addr, 0x3B, 14) return a def get_ints(self): b = self.get_raw_values() c = [] for i in b: c.append(i) return c def bytes_toint(self, firstbyte, secondbyte): if not firstbyte & 0x80: return firstbyte << 8 | secondbyte return - (((firstbyte ^ 255) << 8) | (secondbyte ^ 255) + 1) def get_values(self): raw_ints = self.get_raw_values() vals = {} vals["AcX"] = self.bytes_toint(raw_ints[0], raw_ints[1]) vals["AcY"] = self.bytes_toint(raw_ints[2], raw_ints[3]) vals["AcZ"] = self.bytes_toint(raw_ints[4], raw_ints[5]) vals["Tmp"] = self.bytes_toint(raw_ints[6], raw_ints[7]) / 340.00 + 36.53 vals["GyX"] = self.bytes_toint(raw_ints[8], raw_ints[9]) vals["GyY"] = self.bytes_toint(raw_ints[10], raw_ints[11]) vals["GyZ"] = self.bytes_toint(raw_ints[12], raw_ints[13]) return vals # returned in range of Int16 # -32768 to 32767 def val_test(self): # ONLY FOR TESTING! Also, fast reading sometimes crashes IIC from time import sleep while 1: print(self.get_values()) sleep(0.05) 接下来就根据这个库来写一段代码,请根据下面的代码正确连接好OLED屏幕和MPU6050传感器: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354from machine import Pin, I2Cimport utimefrom ssd1306 import SSD1306_I2Cimport mpu6050 # 初始化I2C0接口,用于与OLED屏幕通信sda = Pin(5)scl = Pin(4)i2c0 = I2C(0, sda=sda, scl=scl, freq=400000)oled = SSD1306_I2C(128, 64, i2c0)# 初始化I2C1接口,用于与MPU6050传感器通信(只用到了SCL和SDA引脚)sda1 = Pin(27)scl1 = Pin(14)i2c1 = I2C(1, sda=sda1, scl=scl1, freq=400000) # 注意修改了I2C的频率以匹配常见设置# 设置MPU6050的加速度计和陀螺仪量程,并获取对应的灵敏度accel_range_mapping = {0: 16384.0, 1: 8192.0, 2: 4096.0, 3: 2048.0} # 加速度计量程映射gyro_range_mapping = {0: 131.0, 1: 65.5, 2: 32.8, 3: 16.4} # 陀螺仪量程映射accel_range = 0 # 加速度计量程设置,可以根据需要修改gyro_range = 0 # 陀螺仪量程设置,可以根据需要修改mpu = mpu6050.accel(i2c1, accel_range)mpu.set_gyro_range(gyro_range)accel_scale = accel_range_mapping[accel_range] # 根据量程获取加速度计灵敏度gyro_scale = gyro_range_mapping[gyro_range] # 根据量程获取陀螺仪灵敏度# 无限循环,不断读取传感器数据并更新OLED屏幕显示while True: values = mpu.get_values() # 读取MPU6050的当前值 # 计算加速度和角速度,使用动态获取的灵敏度进行转换 acX = values['AcX'] / accel_scale acY = values['AcY'] / accel_scale acZ = values['AcZ'] / accel_scale gyX = values['GyX'] / gyro_scale gyY = values['GyY'] / gyro_scale gyZ = values['GyZ'] / gyro_scale # 清空OLED屏幕的内容 oled.fill(0) # 在OLED屏幕上显示加速度计和陀螺仪的当前值 oled.text("AccX: {:.2f}g".format(acX), 0, 0) oled.text("AccY: {:.2f}g".format(acY), 0, 10) oled.text("AccZ: {:.2f}g".format(acZ), 0, 20) oled.text("GyroX: {:.2f}dps".format(gyX), 0, 30) oled.text("GyroY: {:.2f}dps".format(gyY), 0, 40) oled.text("GyroZ: {:.2f}dps".format(gyZ), 0, 50) oled.show() utime.sleep(0.05) # 延时以降低更新频率,可以根据需要调整 运行成功后OLED屏幕会实时显示3个方向上的加速度值和角速度值。需要注意的是,如果运动太过于剧烈而超过了传感器的量程,可能会导致程序报错。 很好,我们已经可以通过代码获取加速度和角速度的值了,那么我们不妨假设一下,如果将此类设备通过蓝牙与计算机连接,通过运动传感参数来控制计算机游戏中人物的行为,将会带来怎样全新的游戏体验呢? 想象一下,你手中握着一个小巧的设备,它不断捕捉着你的动作,并将这些数据实时传输给计算机。在游戏中,你不再仅仅是通过键盘和鼠标来操控角色,而是可以通过自己的实际动作来让角色做出各种复杂的动作和反应,增加玩家的沉浸感。 颜色传感器(TCS3472) TCS3672颜色传感器是一种用于测量环境颜色信息的设备。它通常包含一个或多个LED光源和一个光电探测器。当LED光源照射到物体表面时,物体会根据自身的颜色特性反射出特定波长的光线。这些反射光线随后被TCS3672中的光电探测器接收,并转换成电信号。这些电信号随后被送到内部的电路进行处理(放大、滤波、数字化等处理),通过特定的颜色识别算法(如RGB算法、HSV算法等)对物体的颜色进行准确识别和分析。 TCS3672颜色传感器可以输出RGB(红、绿、蓝)三个通道的颜色数据,同时还兼备了色温、色度、光照强度的测量。色温是光源颜色温度的一个指标,用来描述光源的冷暖特性。色温的单位是开尔文(K),低色温的光源(如2700K左右)被认为是暖色调,产生温暖、舒适的感觉,常用于家庭照明和营造温馨氛围。高色温的光源(如6500K以上)被认为是冷色调,产生清凉、明亮的感觉,常用于需要清晰视觉和专注的场合,如办公室和教室。 色度则用来描述光源或物体颜色的饱和度或纯度。在颜色学中,色度通常与色相和饱和度一起使用,用于区分和描述不同的颜色。色度高的颜色表示颜色更加纯净、饱和,而色度低的颜色则显得柔和、不饱和。 接下来我们就利用TCS3472传感器来获取测试物体的颜色,并尝试将这个颜色代入进一个RGB灯珠,也就是说,测试物体是什么颜色的,RGB灯珠就显示什么颜色。 首先,我们需要加装一个TCS3572的驱动库(tcs35725.py): 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157from machine import I2Cimport timeimport ustruct const = lambda x:x _COMMAND_BIT = const(0x80)_REGISTER_ENABLE = const(0x00)_REGISTER_ATIME = const(0x01)_REGISTER_AILT = const(0x04)_REGISTER_AIHT = const(0x06)_REGISTER_ID = const(0x12)_REGISTER_APERS = const(0x0c)_REGISTER_CONTROL = const(0x0f)_REGISTER_SENSORID = const(0x12)_REGISTER_STATUS = const(0x13)_REGISTER_CDATA = const(0x14)_REGISTER_RDATA = const(0x16)_REGISTER_GDATA = const(0x18)_REGISTER_BDATA = const(0x1a)_ENABLE_AIEN = const(0x10)_ENABLE_WEN = const(0x08)_ENABLE_AEN = const(0x02)_ENABLE_PON = const(0x01) _GAINS = (1, 4, 16, 60)_CYCLES = (0, 1, 2, 3, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60) class TCS34725: def __init__(self, i2c, address=None): self.i2c = i2c self.address = address self._active = False self.integration_time(2.4) sensor_id = self.i2c.scan()[0] if sensor_id not in (0x29,0x10): raise RuntimeError("wrong sensor id 0x{:x}".format(sensor_id)) def _register8(self, register, value=None): register |= _COMMAND_BIT if value is None: return self.i2c.readfrom_mem(self.address, register, 1)[0] data = ustruct.pack('<B', value) self.i2c.writeto_mem(self.address, register, data) def _register16(self, register, value=None): register |= _COMMAND_BIT if value is None: data = self.i2c.readfrom_mem(self.address, register, 2) return ustruct.unpack('<H', data)[0] data = ustruct.pack('<H', value) self.i2c.writeto_mem(self.address, register, data) def active(self, value=None): if value is None: return self._active value = bool(value) if self._active == value: return self._active = value enable = self._register8(_REGISTER_ENABLE) if value: self._register8(_REGISTER_ENABLE, enable | _ENABLE_PON) time.sleep_ms(3) self._register8(_REGISTER_ENABLE, enable | _ENABLE_PON | _ENABLE_AEN) else: self._register8(_REGISTER_ENABLE, enable & ~(_ENABLE_PON | _ENABLE_AEN)) def sensor_id(self): return self.i2c.scan()[0] def integration_time(self, value=None): if value is None: return self._integration_time value = min(614.4, max(2.4, value)) cycles = int(value / 2.4) self._integration_time = cycles * 2.4 return self._register8(_REGISTER_ATIME, 256 - cycles) def gain(self, value): if value is None: return _GAINS[self._register8(_REGISTER_CONTROL)] if value not in _GAINS: raise ValueError("gain must be 1, 4, 16 or 60") return self._register8(_REGISTER_CONTROL, _GAINS.index(value)) def _valid(self): return bool(self._register8(_REGISTER_STATUS) & 0x01) def read(self, raw=False): was_active = self.active() self.active(True) while not self._valid(): time.sleep_ms(int(self._integration_time + 0.9)) data = tuple(self._register16(register) for register in ( _REGISTER_RDATA, _REGISTER_GDATA, _REGISTER_BDATA, _REGISTER_CDATA, )) self.active(was_active) if raw: return data return self._temperature_and_lux(data) def _temperature_and_lux(self, data): r, g, b, c = data x = -0.14282 * r + 1.54924 * g + -0.95641 * b y = -0.32466 * r + 1.57837 * g + -0.73191 * b z = -0.68202 * r + 0.77073 * g + 0.56332 * b d = x + y + z n = (x / d - 0.3320) / (0.1858 - y / d) cct = 449.0 * n**3 + 3525.0 * n**2 + 6823.3 * n + 5520.33 return cct, y def threshold(self, cycles=None, min_value=None, max_value=None): if cycles is None and min_value is None and max_value is None: min_value = self._register16(_REGISTER_AILT) max_value = self._register16(_REGISTER_AILT) if self._register8(_REGISTER_ENABLE) & _ENABLE_AIEN: cycles = _CYCLES[self._register8(_REGISTER_APERS) & 0x0f] else: cycles = -1 return cycles, min_value, max_value if min_value is not None: self._register16(_REGISTER_AILT, min_value) if max_value is not None: self._register16(_REGISTER_AIHT, max_value) if cycles is not None: enable = self._register8(_REGISTER_ENABLE) if cycles == -1: self._register8(_REGISTER_ENABLE, enable & ~(_ENABLE_AIEN)) else: self._register8(_REGISTER_ENABLE, enable | _ENABLE_AIEN) if cycles not in _CYCLES: raise ValueError("invalid persistence cycles") self._register8(_REGISTER_APERS, _CYCLES.index(cycles)) def interrupt(self, value=None): if value is None: return bool(self._register8(_REGISTER_STATUS) & _ENABLE_AIEN) if value: raise ValueError("interrupt can only be cleared") self.i2c.writeto(self.address, b'\xe6') def html_rgb(data): r, g, b, c = data red = pow((int((r/c) * 256) / 255), 2.5) * 255 green = pow((int((g/c) * 256) / 255), 2.5) * 255 blue = pow((int((b/c) * 256) / 255), 2.5) * 255 return red, green, blue def html_hex(data): r, g, b = html_rgb(data) return "{0:02x}{1:02x}{2:02x}".format(int(r),int(g),int(b)) 然后再写一段代码用于获取传感器数据: 1234567891011121314151617181920212223242526272829303132333435363738394041424344import neopixelfrom machine import Pin, I2Cfrom tcs34725 import TCS34725 import time# 创建I2C对象i2c = I2C(0, scl=Pin(5), sda=Pin(4), freq=400_000)# 创建TCS34725颜色识别模块对象tcs = TCS34725(i2c, 0x29)# 初始化NeoPixelLED_PIN = 48 LED_NUM = 30 LED = neopixel.NeoPixel(pin=Pin(LED_PIN), n=LED_NUM, timing=1)# 创建RGB颜色计算函数def get_rgb(): try: red, green, blue, colour = tcs.read(True) # 读取颜色值 if colour != 0: # Red = int((red / colour) * 255) Green = int((green / colour) * 255) Blue = int((blue / colour) * 255) print('Red:', Red, 'Green:', Green, 'Blue:', Blue) return Red, Green, Blue # 返回RGB值 else: return 0, 0, 0 except: print("数据异常!") raise# 主程序入口def main(): while True: red, green, blue = get_rgb() for i in range(LED_NUM): LED[i] = (red, green, blue) LED.write() time.sleep(0.1) # 运行主程序main() 运行程序后,你可以随意选取一个颜色鲜艳的物体,直接将其放置在传感器前进行识别。如果一切顺利,RGB灯珠将尝试展示与物体相对应的颜色。然而,需要注意的是,RGB灯珠所显示的颜色可能会比物体的实际颜色略淡且更亮,这是由于我们的代码仅获取了物体的RGB值,而忽略了色温、色度以及光照强度等其他因素。此外,由于这段代码的限制,RGB灯珠对物体颜色的响应并非完全实时,存在一定的延迟。 Ps:其实写这段代码时我是利用ESP32-S3-DevKitC-1来测试的,这块板子上自带一个RGB灯珠,且被安排到了48号引脚,所以代码中的LED_PIN 设置为了48。 电流/电压传感器(INA219) INA219是一款零漂移双向电流/功率监测计。该器件可以监测分流器电压降和总线电源电压,转换次数和滤波选项可以通过编程设定。可编程校准值与内部乘法器相结合,支持直接读取电流值(单位:安培A)。通过附加乘法寄存器可计算功率(单位:瓦W)。 INA219经常用作电子设备的状态检测,比如在必要的设备里植入这样的传感器,可以让我们方便的获取设备运行的信息(电流、功率等)。接下来我们尝试利用代码来获取电压与电流值。 在主程序开始之前,你需要在ESP32上下载好INA219的驱动库。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413# The MIT License (MIT)## Copyright (c) 2017 Dean Miller for Adafruit Industries## Permission is hereby granted, free of charge, to any person obtaining a copy# of this software and associated documentation files (the "Software"), to deal# in the Software without restriction, including without limitation the rights# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell# copies of the Software, and to permit persons to whom the Software is# furnished to do so, subject to the following conditions:## The above copyright notice and this permission notice shall be included in# all copies or substantial portions of the Software.## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN# THE SOFTWARE."""`adafruit_ina219`====================================================CircuitPython/MicroPython driver for the INA219 current sensor.* Author(s): Dean Miller"""from micropython import const# from adafruit_bus_device.i2c_device import I2CDevice__version__ = "0.0.0-auto.0"__repo__ = "https://github.com/robert-hh/INA219.git"# Bits# pylint: disable=bad-whitespace_READ = const(0x01)# Config Register (R/W)_REG_CONFIG = const(0x00)_CONFIG_RESET = const(0x8000) # Reset Bit_CONFIG_BVOLTAGERANGE_MASK = const(0x2000) # Bus Voltage Range Mask_CONFIG_BVOLTAGERANGE_16V = const(0x0000) # 0-16V Range_CONFIG_BVOLTAGERANGE_32V = const(0x2000) # 0-32V Range_CONFIG_GAIN_MASK = const(0x1800) # Gain Mask_CONFIG_GAIN_1_40MV = const(0x0000) # Gain 1, 40mV Range_CONFIG_GAIN_2_80MV = const(0x0800) # Gain 2, 80mV Range_CONFIG_GAIN_4_160MV = const(0x1000) # Gain 4, 160mV Range_CONFIG_GAIN_8_320MV = const(0x1800) # Gain 8, 320mV Range_CONFIG_BADCRES_MASK = const(0x0780) # Bus ADC Resolution Mask_CONFIG_BADCRES_9BIT = const(0x0080) # 9-bit bus res = 0..511_CONFIG_BADCRES_10BIT = const(0x0100) # 10-bit bus res = 0..1023_CONFIG_BADCRES_11BIT = const(0x0200) # 11-bit bus res = 0..2047_CONFIG_BADCRES_12BIT = const(0x0400) # 12-bit bus res = 0..4097_CONFIG_SADCRES_MASK = const(0x0078) # Shunt ADC Res. & Avg. Mask_CONFIG_SADCRES_9BIT_1S_84US = const(0x0000) # 1 x 9-bit shunt sample_CONFIG_SADCRES_10BIT_1S_148US = const(0x0008) # 1 x 10-bit shunt sample_CONFIG_SADCRES_11BIT_1S_276US = const(0x0010) # 1 x 11-bit shunt sample_CONFIG_SADCRES_12BIT_1S_532US = const(0x0018) # 1 x 12-bit shunt sample_CONFIG_SADCRES_12BIT_2S_1060US = const(0x0048) # 2 x 12-bit sample average_CONFIG_SADCRES_12BIT_4S_2130US = const(0x0050) # 4 x 12-bit sample average_CONFIG_SADCRES_12BIT_8S_4260US = const(0x0058) # 8 x 12-bit sample average_CONFIG_SADCRES_12BIT_16S_8510US = const(0x0060) # 16 x 12-bit sample average_CONFIG_SADCRES_12BIT_32S_17MS = const(0x0068) # 32 x 12-bit sample average_CONFIG_SADCRES_12BIT_64S_34MS = const(0x0070) # 64 x 12-bit sample average_CONFIG_SADCRES_12BIT_128S_69MS = const(0x0078) # 128 x 12-bit sample average_CONFIG_MODE_MASK = const(0x0007) # Operating Mode Mask_CONFIG_MODE_POWERDOWN = const(0x0000)_CONFIG_MODE_SVOLT_TRIGGERED = const(0x0001)_CONFIG_MODE_BVOLT_TRIGGERED = const(0x0002)_CONFIG_MODE_SANDBVOLT_TRIGGERED = const(0x0003)_CONFIG_MODE_ADCOFF = const(0x0004)_CONFIG_MODE_SVOLT_CONTINUOUS = const(0x0005)_CONFIG_MODE_BVOLT_CONTINUOUS = const(0x0006)_CONFIG_MODE_SANDBVOLT_CONTINUOUS = const(0x0007)# SHUNT VOLTAGE REGISTER (R)_REG_SHUNTVOLTAGE = const(0x01)# BUS VOLTAGE REGISTER (R)_REG_BUSVOLTAGE = const(0x02)# POWER REGISTER (R)_REG_POWER = const(0x03)# CURRENT REGISTER (R)_REG_CURRENT = const(0x04)# CALIBRATION REGISTER (R/W)_REG_CALIBRATION = const(0x05)# pylint: enable=bad-whitespacedef _to_signed(num): if num > 0x7FFF: num -= 0x10000 return numclass INA219: """Driver for the INA219 current sensor""" def __init__(self, i2c_device, addr=0x40): self.i2c_device = i2c_device self.i2c_addr = addr self.buf = bytearray(2) # Multiplier in mA used to determine current from raw reading self._current_lsb = 0 # Multiplier in W used to determine power from raw reading self._power_lsb = 0 # Set chip to known config values to start self._cal_value = 4096 self.set_calibration_32V_2A() def _write_register(self, reg, value): self.buf[0] = (value >> 8) & 0xFF self.buf[1] = value & 0xFF self.i2c_device.writeto_mem(self.i2c_addr, reg, self.buf) def _read_register(self, reg): self.i2c_device.readfrom_mem_into(self.i2c_addr, reg & 0xff, self.buf) value = (self.buf[0] << 8) | (self.buf[1]) return value @property def shunt_voltage(self): """The shunt voltage (between V+ and V-) in Volts (so +-.327V)""" value = _to_signed(self._read_register(_REG_SHUNTVOLTAGE)) # The least signficant bit is 10uV which is 0.00001 volts return value * 0.00001 @property def bus_voltage(self): """The bus voltage (between V- and GND) in Volts""" raw_voltage = self._read_register(_REG_BUSVOLTAGE) # Shift to the right 3 to drop CNVR and OVF and multiply by LSB # Each least signficant bit is 4mV voltage_mv = _to_signed(raw_voltage >> 3) * 4 return voltage_mv * 0.001 @property def current(self): """The current through the shunt resistor in milliamps.""" # Sometimes a sharp load will reset the INA219, which will # reset the cal register, meaning CURRENT and POWER will # not be available ... athis by always setting a cal # value even if it's an unfortunate extra step self._write_register(_REG_CALIBRATION, self._cal_value) # Now we can safely read the CURRENT register! raw_current = _to_signed(self._read_register(_REG_CURRENT)) return raw_current * self._current_lsb def set_calibration_32V_2A(self): # pylint: disable=invalid-name """Configures to INA219 to be able to measure up to 32V and 2A of current. Counter overflow occurs at 3.2A. ..note :: These calculations assume a 0.1 shunt ohm resistor""" # By default we use a pretty huge range for the input voltage, # which probably isn't the most appropriate choice for system # that don't use a lot of power. But all of the calculations # are shown below if you want to change the settings. You will # also need to change any relevant register settings, such as # setting the VBUS_MAX to 16V instead of 32V, etc. # VBUS_MAX = 32V (Assumes 32V, can also be set to 16V) # VSHUNT_MAX = 0.32 (Assumes Gain 8, 320mV, can also be # 0.16, 0.08, 0.04) # RSHUNT = 0.1 (Resistor value in ohms) # 1. Determine max possible current # MaxPossible_I = VSHUNT_MAX / RSHUNT # MaxPossible_I = 3.2A # 2. Determine max expected current # MaxExpected_I = 2.0A # 3. Calculate possible range of LSBs (Min = 15-bit, Max = 12-bit) # MinimumLSB = MaxExpected_I/32767 # MinimumLSB = 0.000061 (61uA per bit) # MaximumLSB = MaxExpected_I/4096 # MaximumLSB = 0,000488 (488uA per bit) # 4. Choose an LSB between the min and max values # (Preferrably a roundish number close to MinLSB) # CurrentLSB = 0.0001 (100uA per bit) self._current_lsb = .1 # Current LSB = 100uA per bit # 5. Compute the calibration register # Cal = trunc (0.04096 / (Current_LSB * RSHUNT)) # Cal = 4096 (0x1000) self._cal_value = 4096 # 6. Calculate the power LSB # PowerLSB = 20 * CurrentLSB # PowerLSB = 0.002 (2mW per bit) self._power_lsb = .002 # Power LSB = 2mW per bit # 7. Compute the maximum current and shunt voltage values before # overflow # # Max_Current = Current_LSB * 32767 # Max_Current = 3.2767A before overflow # # If Max_Current > Max_Possible_I then # Max_Current_Before_Overflow = MaxPossible_I # Else # Max_Current_Before_Overflow = Max_Current # End If # # Max_ShuntVoltage = Max_Current_Before_Overflow * RSHUNT # Max_ShuntVoltage = 0.32V # # If Max_ShuntVoltage >= VSHUNT_MAX # Max_ShuntVoltage_Before_Overflow = VSHUNT_MAX # Else # Max_ShuntVoltage_Before_Overflow = Max_ShuntVoltage # End If # 8. Compute the Maximum Power # MaximumPower = Max_Current_Before_Overflow * VBUS_MAX # MaximumPower = 3.2 * 32V # MaximumPower = 102.4W # Set Calibration register to 'Cal' calculated above self._write_register(_REG_CALIBRATION, self._cal_value) # Set Config register to take into account the settings above config = (_CONFIG_BVOLTAGERANGE_32V | _CONFIG_GAIN_8_320MV | _CONFIG_BADCRES_12BIT | _CONFIG_SADCRES_12BIT_1S_532US | _CONFIG_MODE_SANDBVOLT_CONTINUOUS) self._write_register(_REG_CONFIG, config) def set_calibration_32V_1A(self): # pylint: disable=invalid-name """Configures to INA219 to be able to measure up to 32V and 1A of current. Counter overflow occurs at 1.3A. .. note:: These calculations assume a 0.1 ohm shunt resistor.""" # By default we use a pretty huge range for the input voltage, # which probably isn't the most appropriate choice for system # that don't use a lot of power. But all of the calculations # are shown below if you want to change the settings. You will # also need to change any relevant register settings, such as # setting the VBUS_MAX to 16V instead of 32V, etc. # VBUS_MAX = 32V (Assumes 32V, can also be set to 16V) # VSHUNT_MAX = 0.32 (Assumes Gain 8, 320mV, can also be # 0.16, 0.08, 0.04) # RSHUNT = 0.1 (Resistor value in ohms) # 1. Determine max possible current # MaxPossible_I = VSHUNT_MAX / RSHUNT # MaxPossible_I = 3.2A # 2. Determine max expected current # MaxExpected_I = 1.0A # 3. Calculate possible range of LSBs (Min = 15-bit, Max = 12-bit) # MinimumLSB = MaxExpected_I/32767 # MinimumLSB = 0.0000305 (30.5uA per bit) # MaximumLSB = MaxExpected_I/4096 # MaximumLSB = 0.000244 (244uA per bit) # 4. Choose an LSB between the min and max values # (Preferrably a roundish number close to MinLSB) # CurrentLSB = 0.0000400 (40uA per bit) self._current_lsb = 0.04 # In milliamps # 5. Compute the calibration register # Cal = trunc (0.04096 / (Current_LSB * RSHUNT)) # Cal = 10240 (0x2800) self._cal_value = 10240 # 6. Calculate the power LSB # PowerLSB = 20 * CurrentLSB # PowerLSB = 0.0008 (800uW per bit) self._power_lsb = 0.0008 # 7. Compute the maximum current and shunt voltage values before # overflow # # Max_Current = Current_LSB * 32767 # Max_Current = 1.31068A before overflow # # If Max_Current > Max_Possible_I then # Max_Current_Before_Overflow = MaxPossible_I # Else # Max_Current_Before_Overflow = Max_Current # End If # # ... In this case, we're good though since Max_Current is less than # MaxPossible_I # # Max_ShuntVoltage = Max_Current_Before_Overflow * RSHUNT # Max_ShuntVoltage = 0.131068V # # If Max_ShuntVoltage >= VSHUNT_MAX # Max_ShuntVoltage_Before_Overflow = VSHUNT_MAX # Else # Max_ShuntVoltage_Before_Overflow = Max_ShuntVoltage # End If # 8. Compute the Maximum Power # MaximumPower = Max_Current_Before_Overflow * VBUS_MAX # MaximumPower = 1.31068 * 32V # MaximumPower = 41.94176W # Set Calibration register to 'Cal' calculated above self._write_register(_REG_CALIBRATION, self._cal_value) # Set Config register to take into account the settings above config = (_CONFIG_BVOLTAGERANGE_32V | _CONFIG_GAIN_8_320MV | _CONFIG_BADCRES_12BIT | _CONFIG_SADCRES_12BIT_1S_532US | _CONFIG_MODE_SANDBVOLT_CONTINUOUS) self._write_register(_REG_CONFIG, config) def set_calibration_16V_400mA(self): # pylint: disable=invalid-name """Configures to INA219 to be able to measure up to 16V and 400mA of current. Counter overflow occurs at 1.6A. .. note:: These calculations assume a 0.1 ohm shunt resistor.""" # Calibration which uses the highest precision for # current measurement (0.1mA), at the expense of # only supporting 16V at 400mA max. # VBUS_MAX = 16V # VSHUNT_MAX = 0.04 (Assumes Gain 1, 40mV) # RSHUNT = 0.1 (Resistor value in ohms) # 1. Determine max possible current # MaxPossible_I = VSHUNT_MAX / RSHUNT # MaxPossible_I = 0.4A # 2. Determine max expected current # MaxExpected_I = 0.4A # 3. Calculate possible range of LSBs (Min = 15-bit, Max = 12-bit) # MinimumLSB = MaxExpected_I/32767 # MinimumLSB = 0.0000122 (12uA per bit) # MaximumLSB = MaxExpected_I/4096 # MaximumLSB = 0.0000977 (98uA per bit) # 4. Choose an LSB between the min and max values # (Preferrably a roundish number close to MinLSB) # CurrentLSB = 0.00005 (50uA per bit) self._current_lsb = 0.05 # in milliamps # 5. Compute the calibration register # Cal = trunc (0.04096 / (Current_LSB * RSHUNT)) # Cal = 8192 (0x2000) self._cal_value = 8192 # 6. Calculate the power LSB # PowerLSB = 20 * CurrentLSB # PowerLSB = 0.001 (1mW per bit) self._power_lsb = 0.001 # 7. Compute the maximum current and shunt voltage values before # overflow # # Max_Current = Current_LSB * 32767 # Max_Current = 1.63835A before overflow # # If Max_Current > Max_Possible_I then # Max_Current_Before_Overflow = MaxPossible_I # Else # Max_Current_Before_Overflow = Max_Current # End If # # Max_Current_Before_Overflow = MaxPossible_I # Max_Current_Before_Overflow = 0.4 # # Max_ShuntVoltage = Max_Current_Before_Overflow * RSHUNT # Max_ShuntVoltage = 0.04V # # If Max_ShuntVoltage >= VSHUNT_MAX # Max_ShuntVoltage_Before_Overflow = VSHUNT_MAX # Else # Max_ShuntVoltage_Before_Overflow = Max_ShuntVoltage # End If # # Max_ShuntVoltage_Before_Overflow = VSHUNT_MAX # Max_ShuntVoltage_Before_Overflow = 0.04V # 8. Compute the Maximum Power # MaximumPower = Max_Current_Before_Overflow * VBUS_MAX # MaximumPower = 0.4 * 16V # MaximumPower = 6.4W # Set Calibration register to 'Cal' calculated above self._write_register(_REG_CALIBRATION, self._cal_value) # Set Config register to take into account the settings above config = (_CONFIG_BVOLTAGERANGE_16V | _CONFIG_GAIN_1_40MV | _CONFIG_BADCRES_12BIT | _CONFIG_SADCRES_12BIT_1S_532US | _CONFIG_MODE_SANDBVOLT_CONTINUOUS) self._write_register(_REG_CONFIG, config) 接下来就是主程序: 123456789101112131415161718192021# 从machine模块导入Pin和I2C类from machine import Pin, I2Cimport timeimport ina219# 初始化I2C总线,使用引脚5作为SCL(时钟线),引脚4作为SDA(数据线)i = I2C(0, scl=Pin(5), sda=Pin(4))print("I2C Bus Scan: ", i.scan(), "\n")# 创建INA219传感器对象,连接到I2C总线sensor = ina219.INA219(i)# 设置校准参数,适用于16V和400mA的测量范围sensor.set_calibration_16V_400mA()while True: print("\n") # 返回的电流单位为毫安 print("Current / mA: %8.3f" % (sensor.current)) # 返回的分流电压单位为伏特 print("Shunt voltage / mV: %8.3f" % (sensor.shunt_voltage * 1000)) time.sleep(1) 在运行之前,你首先要搭建好测试电路。测试电路的搭建思路非常简单,你只需要将ina219的两个输入(测试)端口与待测负载并联,负载的电源既可以接入ESP32也可以接入一个其它电源(视情况而定)。按照如图的方式进行搭建,搭建完毕后就可以运行ESP32的代码了,如果能正常输出电压电流的值就说明没问题。 在此需要补充说明一点,当你运行程序后,所获取的电压和电流值实际上是针对INA219上的一个大号R100贴片电阻(其阻值为0.1欧姆)进行测量的结果。为便于理解,我们可以将整个电路简化为左图所示,其中将ESP32部分移除,将INA219视为电阻R2,而负载则视为R1。这样,通过程序测量得到的数据,就相当于使用万用表直接在R2(即INA219内部的分流电阻)上进行测量所得到的结果。 然后你可以利用欧姆定律和测量值计算出R1(不过会有误差,误差来着电路与电源以及其它你猜不到的地方),或者利用公式计算出功率。 摇杆传感器 摇杆一般在航模、电玩、遥控车、云台等设备上应用广泛,为操作者提供了直观且灵活的控制方式。很多带有屏幕的设备,如游戏机、便携式控制台等,也经常使用摇杆作为菜单选择的输入控制,使用户能够方便快捷地浏览和选择选项。 双轴按键摇杆是其中一种常见类型,主要由两个电位器和一个按键开关精巧组合而成。这两个电位器随着摇杆在X轴和Y轴上的扭转角度,分别输出对应的电压值,从而实现对这两个方向上的精确控制。而在Z轴方向上,当用户按下摇杆时,则会触发一个轻触按键,通常用于确认选择或执行特定功能。简单来说,摇杆其实就是一组特殊的电位器与按键开关的集成体,通过结合特定的机械机构,形成了一种能够同时检测二维位移和按键触发的新型传感器。我们可以利用ADC接口读取X、Y轴电位器的模拟信号,并通过数字输入引脚监测按键状态,从而实现了对设备操作的精准控制。下面是一个简单的示例代码: 1234567891011import timefrom machine import Pin, ADC# 定义摇杆的引脚ps2_x = ADC(Pin(15), atten=ADC.ATTN_11DB)ps2_y = ADC(Pin(2), atten=ADC.ATTN_11DB)ps2_button = Pin(4, Pin.IN, Pin.PULL_UP)while True: print(f'x:{ps2_x.read()} y:{ps2_y.read()} z:{ps2_button.value()}') time.sleep(0.1) 你在具体实验的时候会发现,x、y的值即使你不动摇杆,它也会进行一定幅度的变化,这个问题我们在后面会提及。 传感器总结篇在传感器系列的章节中,我们充分利用了多种能够测量生活中常见物理量的传感器来进行实验。那么,你是否曾想过亲手制作一个传感器,并为它编写相应的代码呢? 要想自制传感器,第一步我们需要深入了解模数转换(Analog-to-Digital Conversion, ADC)的概念。但首先我们需要介绍两个名词:模拟信号和数字信号。 模拟信号就像是自然界中的声音、光线、温度等连续变化的量。想象一下你听音乐时,音乐的声音是连续不断变化的,有高有低,有强有弱,这种连续变化的信号就是模拟信号。模拟信号可以取任意值,它的变化是平滑的,没有固定的台阶或级别。在电子设备中,模拟信号也很常见,比如麦克风捕捉到的声音信号、摄像头捕捉到的图像信号等,这些都是模拟信号。模拟信号的优点是能够非常精细地描述现实世界的变化,但缺点是容易受到干扰,且在传输和处理过程中可能会失真。 数字信号则与模拟信号截然不同,它是离散的,只有有限个可能的取值。你可以把数字信号想象成一系列的开关,每个开关只有“开”和“关”两种状态,对应着数字信号中的“1”和“0”。在计算机和电子设备中,数字信号非常普遍,因为计算机只能理解和处理离散的数字信息。数字信号的优点是抗干扰能力强,传输和处理过程中不容易失真,而且便于存储和加密。但缺点是,由于数字信号是离散的,所以在描述连续变化的量时,可能会有一定的精度损失。不过,通过提高采样率和量化位数,数字信号也能够非常接近地模拟出连续变化的效果。 回顾之前的章节,你会发现那些传感器大多依赖于模数转换技术,将模拟信号转换为数字信号,从而使得ESP32能够轻松处理和分析这些数据。模数转换作为传感器与数字系统之间的关键纽带,确保了微控制器能够准确读取并响应来自传感器的信息。 幸运的是,ESP32内置了ADC引脚,这极大地简化了传感器与微控制器的连接过程。接下来,我们将通过一个简单的电位器实验来亲身体验模数转换的魅力。首先,你需要准备一个电位器,电位器一般有3个引脚(两个固定端和一个滑动端),利用面包板和杜邦线将电位器的两个固定端与ESP32的3.3v与GND连接,滑动端与D34连接,这样我们就搭建好了一个最简单的测试电路。 接下来是写代码: 12345678910111213141516171819202122232425262728from machine import Pin, ADC, UARTimport utime# 初始化模拟输入adc_pin = Pin(34, Pin.IN) # GPIO34(ADC)adc = ADC(adc_pin)adc.atten(ADC.ATTN_11DB) # 设置 ADC 衰减为 11dB,适用于 0-3.3V 输入范围adc.width(ADC.WIDTH_12BIT) # 设置 ADC 分辨率为 12 位# 初始化串口(可选,用于调试。注意:如果使用 ESP32 的默认 UART(UART0),则可能需要通过其他方式查看输出)uart = UART(2, baudrate=115200, tx=17, rx=16) def read_potentiometer(): """读取电位器的 ADC 值""" return adc.read_u16() # 读取 ADC 值,返回整数def main(): """主函数,用于持续读取并发送电位器的 ADC 值""" while True: adc_value = read_potentiometer() # 使用 UART 发送数据 uart.write(f"ADC Value: {adc_value}\n".encode('utf-8')) # 使用print函数打印数据到串口 print("ADC Value:", adc_value) utime.sleep(0.1) # 延迟以避免输出过快if __name__ == "__main__": main() 在代码运行的同时旋转电位器,你会发现控制台输出ADC数值的变化,这就是最简单的用硬件来控制软件参数的例子。但是,你可能会观察到,ADC数值的变化并不像我们想象的那样平滑。有时,即使你不旋转电位器,数值也会有一定的波动。这种现象主要是由于电子噪声、电源波动以及ADC转换过程中的量化误差等因素导致的。所以我们需要为其添加滤波,这个滤波可以是从硬件层面的(如为传感器电路加入合适的滤波电路),也可以是软件层面的(如使用滤波算法)。 音频放大器(MAX98357A) MAX98357A 是一款由 Maxim Integrated(美信集成产品公司)生产的立体声D类音频功率放大器 IC。这款放大器专为便携式音频设备设计,如智能手机、平板电脑和笔记本电脑等,能够提供高效、高质量的音频放大功能。 音频放大器在音频系统中至关重要,它负责将微弱的音频信号放大到足以驱动扬声器或耳机的水平。这种放大不仅确保了声音的清晰度和响亮度,还满足了扬声器对功率的需求。 高质量的音频放大器能提升音质,减少失真和噪声,同时提供多种功能如音量控制、音调调节等,以满足不同环境和需求。在音频系统设计中,音频放大器是确保系统稳定性和性能的关键组件,它使得音频输出能够适应各种应用场景。 而关于音频就不得不介绍一下I2S了,I2S(Inter-IC Sound)是一种串行音频总线接口标准,用于连接数字音频设备,如音频编解码器、数字音频处理器(DSP)等。它主要用于传输未压缩的数字音频数据,如PCM(脉冲编码调制)数据。I2S接口由飞利浦半导体(现为恩智浦半导体)在1980年代开发,并逐渐成为数字音频领域的标准接口之一。接下来,我们通过代码来播放一首曲子。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162from machine import I2S from machine import Pin """GPIO13 -- DINGPIO12 --- BCLKGPIO14 -- LRCGND -- GND5V或3.3V -- VCC""" # 初始化引脚定义sck_pin = Pin(12) # 串行时钟输出ws_pin = Pin(14) # 字时钟sd_pin = Pin(13) # 串行数据输出 """sck 是串行时钟线的引脚对象ws 是单词选择行的引脚对象sd 是串行数据线的引脚对象mode 指定接收或发送bits 指定样本大小(位),16 或 32format 指定通道格式,STEREO(左右声道) 或 MONO(单声道)rate 指定音频采样率(样本/秒)ibuf 指定内部缓冲区长度(字节)""" # 初始化i2saudio_out = I2S(1, sck=sck_pin, ws=ws_pin, sd=sd_pin, mode=I2S.TX, bits=16, format=I2S.MONO, rate=16000, ibuf=20000) wavtempfile = "test.wav"with open(wavtempfile,'rb') as f: # 跳过文件的开头的44个字节,直到数据段的第1个字节 pos = f.seek(44) # 用于减少while循环中堆分配的内存视图 wav_samples = bytearray(1024) wav_samples_mv = memoryview(wav_samples) print("开始播放音频...") #并将其写入I2S DAC while True: try: num_read = f.readinto(wav_samples_mv) # WAV文件结束 if num_read == 0: break # 直到所有样本都写入I2S外围设备 num_written = 0 while num_written < num_read: num_written += audio_out.write(wav_samples_mv[num_written:num_read]) except Exception as ret: print("产生异常...", ret) break 需要注意的是,WAV音频文件通常体积较大,尤其是较长的音频文件。ESP32的存储空间有限,可能无法容纳完整的音频文件,除非考虑使用SD卡进行扩展存储。不过,还有一种方法是通过访问音频文件的URL来获取音频数据,从而避免直接存储大文件的问题。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677from machine import I2Sfrom machine import Pinimport urequestsimport networkimport time """GPIO12 --- BCLKGPIO14 --- LRCGPIO13 --- DINGND --- GND5V --- VCC""" # 初始化引脚定义sck_pin = Pin(12) # 串行时钟输出ws_pin = Pin(14) # 字时钟sd_pin = Pin(13) # 串行数据输出 """sck 是串行时钟线的引脚对象ws 是单词选择行的引脚对象sd 是串行数据线的引脚对象mode 指定接收或发送bits 指定样本大小(位),16 或 32format 指定通道格式,STEREO(左右声道) 或 MONO(单声道)rate 指定音频采样率(样本/秒),数值越大播放速度越快ibuf 指定内部缓冲区长度(字节)""" # 初始化i2saudio_out = I2S(1, sck=sck_pin, ws=ws_pin, sd=sd_pin, mode=I2S.TX, bits=16, format=I2S.MONO, rate=44100, ibuf=20000) # 连接网络函数def do_connect(): """链接WIFI网络""" wlan = network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): print('connecting to network...') wlan.connect('GAP', 'nahidaaa') # WIFI名字密码 i = 1 while not wlan.isconnected(): print("正在链接中...{}".format(i)) i += 1 time.sleep(1) print('network config:', wlan.ifconfig()) # 连接网络do_connect() # 注意不要用https,要用httpresponse = urequests.get("http://............../xxxx.wav", stream=True)response.raw.read(44) # 跳过开头的44字节音频文件头信息 print("开始播放音频...") #并将其写入I2S DACwhile True: try: content_byte = response.raw.read(1024) # 每次读取1024个字节 # 判断WAV文件是否结束 if len(content_byte) == 0: break # 调用I2S对象播放音频 audio_out.write(content_byte) except Exception as ret: print("程序产生异常...", ret) audio_out.deinit() break audio_out.deinit() # 音乐播放完毕后,退出 注:通过上述代码播放出的音乐效果并不是很理想 未完待续…… 步进电机步进电机,又称脉冲电机,基于电磁铁原理,是一种可自由回转且通过气隙磁导变化产生电磁转矩的装置。它能够将电脉冲信号转化为角位移或线位移,但不能直接连接到直流或交流电源上,需要专用的驱动电路。简单来说,步进电机能旋转特定的角度。 步进电机的原理大概就是通过脉冲信号控制电机内线圈的磁性,由于其特殊的结构,可以驱动转子旋转一定角度。具体原理可以去搜索相关科普视频。 在本节示例中,我选用型号为28BYJ-48的步进电机与相关驱动电路(ULN2003芯片)进行试验。我们可以通过电机的型号获取到以下参数:该步进电机的直径为28mm,B代表步进电机,Y代表永磁,J代表带减速箱,48表示可以以四拍和八拍运行。同时,这款步进电机内部的齿轮比为1:64,在全步进模式下,转一圈需要64步,即每步转动5.625°;在半步进模式下,转一圈需要128步,即每步转动2.8125°。因为这个是带减速齿轮的,所以最后在输出轴上的步进角是5.625/64=0.08789度(因为减速比是1/64)。 相数,指的是产生对N,S极磁场的激励线圈对数;拍数,指的是完成一个磁场周期变化所需的脉冲数或导电状态,或者指步进电机转过一个齿距角所需脉冲数。步进电机有一个技术参数:空载启动频率,即步进电机在空载情况下能够正常启动的脉冲频率,如果脉冲频率高于该值,电机不能正常启动,可能发生丢步或堵转。在有负载的情况下,启动频率应更低。如果要使电机达到高速转动,脉冲频率应该有加速过程,即启动频率较低,然后按一定加速度升到所希望的高频(电机转速从低速升到高速)。 简单解释一下步进电机的原理:如图所示,ABCD为定子,上面绕有线圈,为四相,与之相对应的对面四个定子上面也有线圈,相对应的两个定子之间线圈是相互连接形成一个绕组。 单四拍模式:当B相导通,对0的吸引力最大。接下来B断开,C导通,1和C相之间夹角最小被吸引过去,被吸引过去之前2和D相之间夹角为1和C相之间夹角的2倍,1被吸引到C以后,2和D之间最近,此时0和A之间的夹角为2和D之间的2倍,接下来C断开,D导通,2被吸引到D,此时0距离A最近D断开A导通,0被吸引到A相,至此一个周期完成。 双拍工作模式:每次给两个线圈通电,通过改变通电的线圈从而使步进电机转动 五线四相步进电机:在双拍工作方式下,线圈的通电方式依次是:AB、BC、CD、DA 即单拍工作方式下,线圈的通电方式依次是:A、B、C、D 单双拍(八拍工作方式):单双拍工作方式就是单拍工作方式和双拍工作方式交替进行。 五线四相步进电机:A、AB、B、BC、C、CD、D、DA。 驱动电路:ULN2003X芯片是一种高耐压、大电流驱动阵列,由7个NPN达林顿管组成。每个达林顿管都能承受高达500mA的电流,并具有内置基极电阻,适合与TTL和CMOS电路接口。当输入为高电平时,输出为低电平,反之则输出高电平,因此它可用于反向驱动继电器等负载。此外,ULN2003X芯片还具有内部抑制二极管,可以提供反向保护,使系统更加稳定可靠。该芯片被广泛应用于各种需要高电压和大电流输出的场合,如步进电机驱动、继电器驱动、LED显示驱动等。特别是在步进电机驱动中,ULN2003X芯片能够依次对步进电机的各个相位施加电流,从而实现对步进电机的精确控制。 准备好上面的东西后,我们就可以开始写代码了。将ESP32与驱动电路连接(15-IN1,2-IN2,4-IN3,16-IN4,VIN-5v正极,GND-5v负极),再将28BYJ-48步进电机与驱动电路连接,然后将下面的代码上传至ESP32运行: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647from machine import Pinimport time# 电机驱动引脚a = Pin(15, Pin.OUT)b = Pin(2, Pin.OUT)c = Pin(4, Pin.OUT)d = Pin(16, Pin.OUT)# 初始化引脚状态a.value(0)b.value(0)c.value(0)d.value(0)# 延时时间(毫秒)delay_time_ms = 2# 定义电机步序steps = [ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]# 电机一圈的步数full_steps = 512# 计算角度对应的步数angle = 30 steps_for_angle = int((angle / 360) * full_steps)# 旋转电机特定角度for _ in range(steps_for_angle): for step in steps: a.value(step[0]) b.value(step[1]) c.value(step[2]) d.value(step[3]) time.sleep_ms(delay_time_ms)# 停止电机,将所有引脚设置为低电平a.value(0)b.value(0)c.value(0)d.value(0) 通过上述代码,你可以获取这些重要的参数:delay_time_ms 代表延迟时间,指的是步进电机“走了一步”后“走下一步”的时间间隔。这个参数经过我的测试,设置在2~6ms较佳,超过这个范围,电机可能会出现大幅度振动,或者不动。同时这个延迟时间也决定了电机的转速,间隔越长,转的越慢。full_steps 代表电机转动一圈的步数,这边设置为512最佳,设置为其它的可能会出现多转或少转的情况(针对这个示例)。angle 代表需要旋转的角度,你可以自定义它的参数。 需要注意的是,因为full_step设置为了512,代表电机一圈被细分为512个基本步数(这个参数因电机类型、驱动电路、算法而异),因此它的精度会很高,但是,它的转矩就会变小,不适合承受大质量的负载。 矩阵键盘众所周知,我们可以通过按键开关等电子元件来作为单片机的输入设备,但如果一个按键占用单片机的一个引脚的话,对于需要大量按键的应用场景,单片机的引脚资源将很快耗尽。为了解决这一问题,矩阵键盘应运而生。下面是4x4矩阵键盘的原理图: 矩阵键盘通过行列扫描的方式,极大地减少了所需引脚的数量。以4x4矩阵键盘为例,仅需8个引脚(4行4列)即可实现16个按键的输入。我们可以利用微控制器的GPIO引脚,通过行输出高电平、列检测高电平的方式,来判断哪个按键被按下。代码如下: 123456789101112131415161718192021222324252627282930313233343536373839from machine import Pinimport time# 创建行的对象row1 = Pin(7, Pin.OUT)row2 = Pin(6, Pin.OUT)row3 = Pin(5, Pin.OUT)row4 = Pin(4, Pin.OUT)row_list = [row1, row2, row3, row4] # 将创建的行对象放到list里面# 创建列的对象col1 = Pin(15, Pin.IN, Pin.PULL_DOWN)col2 = Pin(16, Pin.IN, Pin.PULL_DOWN)col3 = Pin(17, Pin.IN, Pin.PULL_DOWN)col4 = Pin(18, Pin.IN, Pin.PULL_DOWN)col_list = [col1, col2, col3, col4] # 将创建的列对象放到list里面# 键盘矩阵表,后面用来判断按下的是哪个按钮names = [ ["1", "2", "3", "A"], ["4", "5", "6", "B"], ["7", "8", "9", "C"], ["*", "0", "#", "D"]]while True: for i, row in enumerate(row_list): # 遍历序号和对应的值 # 目的:只让某一行通电,其他的行都是0 for temp in row_list: # 遍历行对象 temp.value(0) # 给每一个行对象赋值 row.value(1) time.sleep_ms(10) # 键盘通电后,延迟一小会 for j, col in enumerate(col_list): # 遍历序号和对应的值 if col.value() == 1: # 给每一个列对象赋值 print("按键: {} 被按下".format(names[i][j])) # print(row1.value(), row2.value(), row3.value(), row4.value()) # 打印出每行的值 # print(col1.value(), col2.value(), col3.value(), col4.value()) # 打印出每列的值 # print("-" * 30) time.sleep(0.1) 具体原理如下:首先通过创建四个行引脚和四个列引脚,行引脚配置为输出模式,用于发送电信号;列引脚配置为输入模式,并启用下拉电阻,确保在没有按键按下时,列引脚保持低电平状态。同时,定义了一个二维列表names,用于存储键盘矩阵中每个按键对应的字符。接下来我们只需要逐行扫描键盘矩阵。对于每一行,它首先将所有行引脚设置为低电平,确保只有当前行发送高电平信号。然后,将当前行引脚设置为高电平,并稍作延迟,以稳定信号。接着,代码遍历所有列引脚,检查是否有列引脚接收到了高电平信号。如果检测到某列引脚为高电平,说明该列对应的按键被按下,此时根据当前行和列的索引,在names列表中查找并打印出对应的按键字符。 完成一轮行扫描后,代码会稍作延迟,然后开始下一轮扫描。这样,通过不断循环扫描键盘矩阵,程序能够实时检测并响应按键操作。 扩充IO ESP32的引脚资源颇为有限,在实际应用中,时常会面临需要占用大量引脚来连接某一特定模块的困境。不过值得庆幸的是,我们可借助另一类模块来实现引脚扩充,它便是PCF8574。 PCF8574一种常见的I/O扩展芯片,用于将微控制器的少量GPIO引脚扩展为更多的GPIO接口。它采用I2C总线(串行通信协议)进行与微处理器的通信。PCF8574芯片具有8个并行输入/输出引脚(即I/O口),可以根据需要配置为输入或输出模式。每个I/O口可以提供高电平或低电平的逻辑状态,用于控制外部设备或接口。 PCF8574 借助 I²C 总线协议,靠 SDA 和 SCL 这两根线与微控制器“交流”。它内部有 8 个能双向工作的 I/O 寄存器,用来存储数据并实现控制。硬件方面,它设置了三个地址引脚,能让多个设备一起使用。当它作为输出端时,寄存器的状态会直接决定引脚的电平高低;作为输入端时,会把引脚的电平情况读取并存到寄存器里,方便微控制器读取。另外,它采用开漏输出结构,当外接上拉电阻才能输出高电平,最终实现为微控制器扩展 I/O 资源的目的。 接下来,我们来尝试使用这个模块来扩充ESP32的引脚。首先你需要载入一个PCF8574的驱动库: pcf8574.py 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758# SPDX-FileCopyrightText: 2019 Mike Causer <https://github.com/mcauser># SPDX-License-Identifier: MIT"""MicroPython PCF8574 8-Bit I2C I/O Expander with Interrupthttps://github.com/mcauser/micropython-pcf8574"""__version__ = "1.1.0"class PCF8574: def __init__(self, i2c, address=0x20): self._i2c = i2c self._address = address self._port = bytearray(1) def check(self): if self._i2c.scan().count(self._address) == 0: raise OSError(f"PCF8574 not found at I2C address {self._address:#x}") return True @property def port(self): self._read() return self._port[0] @port.setter def port(self, value): self._port[0] = value & 0xFF self._write() def pin(self, pin, value=None): pin = self._validate_pin(pin) if value is None: self._read() return (self._port[0] >> pin) & 1 if value: self._port[0] |= 1 << (pin) else: self._port[0] &= ~(1 << (pin)) self._write() def toggle(self, pin): pin = self._validate_pin(pin) self._port[0] ^= 1 << (pin) self._write() def _validate_pin(self, pin): # pin valid range 0..7 if not 0 <= pin <= 7: raise ValueError(f"Invalid pin {pin}. Use 0-7.") return pin def _read(self): self._i2c.readfrom_into(self._address, self._port) def _write(self): self._i2c.writeto(self._address, self._port) 接着,我们根据这个驱动库的使用方法写一个主程序用于验证模块的功能。 未完待续…… 扩充内存 如果要在ESP32上运行一个小网站的话,其内置内存可能不足以支撑需求。不过,ESP32具备良好的扩展能力,允许我们通过外接SD卡或TF卡来扩充其存储空间,从而满足网站运行的需求。 SD卡(Secure Digital Memory Card)和TF卡(又称为microSD卡)都是广泛使用的便携式存储介质,它们主要用于扩展电子设备(如智能手机、平板电脑、数码相机、音乐播放器以及某些嵌入式设备等)的存储容量。 对于ESP32来说,要为其扩容,就得增加一个Micro SD读取模块,如左图所示,这是市面上常用的读卡模块。这个模块有GND、VCC、MISO、MOSI、SCK、CS六个引脚,通过这些引脚,我们可以将SD卡与ESP32进行通信。 第一步,先在ESP32上下载好驱动库(sdcard.py): 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296"""MicroPython driver for SD cards using SPI bus.Requires an SPI bus and a CS pin. Provides readblocks and writeblocksmethods so the device can be mounted as a filesystem.Example usage on pyboard: import pyb, sdcard, os sd = sdcard.SDCard(pyb.SPI(1), pyb.Pin.board.X5) pyb.mount(sd, '/sd2') os.listdir('/')Example usage on ESP8266: import machine, sdcard, os sd = sdcard.SDCard(machine.SPI(1), machine.Pin(15)) os.mount(sd, '/sd') os.listdir('/')"""from micropython import constimport time_CMD_TIMEOUT = const(100)_R1_IDLE_STATE = const(1 << 0)R1_ERASE_RESET = const(1 << 1)_R1_ILLEGAL_COMMAND = const(1 << 2)R1_COM_CRC_ERROR = const(1 << 3)R1_ERASE_SEQUENCE_ERROR = const(1 << 4)R1_ADDRESS_ERROR = const(1 << 5)R1_PARAMETER_ERROR = const(1 << 6)_TOKEN_CMD25 = const(0xFC)_TOKEN_STOP_TRAN = const(0xFD)_TOKEN_DATA = const(0xFE)class SDCard: def __init__(self, spi, cs, baudrate=1320000): self.spi = spi self.cs = cs self.cmdbuf = bytearray(6) self.dummybuf = bytearray(512) self.tokenbuf = bytearray(1) for i in range(512): self.dummybuf[i] = 0xFF self.dummybuf_memoryview = memoryview(self.dummybuf) # initialise the card self.init_card(baudrate) def init_spi(self, baudrate): try: master = self.spi.MASTER except AttributeError: # on ESP8266 self.spi.init(baudrate=baudrate, phase=0, polarity=0) else: # on pyboard self.spi.init(master, baudrate=baudrate, phase=0, polarity=0) def init_card(self, baudrate): # init CS pin self.cs.init(self.cs.OUT, value=1) # init SPI bus; use low data rate for initialisation self.init_spi(100000) # clock card at least 100 cycles with cs high for i in range(16): self.spi.write(b"\xff") # CMD0: init card; should return _R1_IDLE_STATE (allow 5 attempts) for _ in range(5): if self.cmd(0, 0, 0x95) == _R1_IDLE_STATE: break else: raise OSError("no SD card") # CMD8: determine card version r = self.cmd(8, 0x01AA, 0x87, 4) if r == _R1_IDLE_STATE: self.init_card_v2() elif r == (_R1_IDLE_STATE | _R1_ILLEGAL_COMMAND): self.init_card_v1() else: raise OSError("couldn't determine SD card version") # get the number of sectors # CMD9: response R2 (R1 byte + 16-byte block read) if self.cmd(9, 0, 0, 0, False) != 0: raise OSError("no response from SD card") csd = bytearray(16) self.readinto(csd) if csd[0] & 0xC0 == 0x40: # CSD version 2.0 self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 1024 elif csd[0] & 0xC0 == 0x00: # CSD version 1.0 (old, <=2GB) c_size = (csd[6] & 0b11) << 10 | csd[7] << 2 | csd[8] >> 6 c_size_mult = (csd[9] & 0b11) << 1 | csd[10] >> 7 read_bl_len = csd[5] & 0b1111 capacity = (c_size + 1) * (2 ** (c_size_mult + 2)) * (2**read_bl_len) self.sectors = capacity // 512 else: raise OSError("SD card CSD format not supported") # print('sectors', self.sectors) # CMD16: set block length to 512 bytes if self.cmd(16, 512, 0) != 0: raise OSError("can't set 512 block size") # set to high data rate now that it's initialised self.init_spi(baudrate) def init_card_v1(self): for i in range(_CMD_TIMEOUT): self.cmd(55, 0, 0) if self.cmd(41, 0, 0) == 0: # SDSC card, uses byte addressing in read/write/erase commands self.cdv = 512 # print("[SDCard] v1 card") return raise OSError("timeout waiting for v1 card") def init_card_v2(self): for i in range(_CMD_TIMEOUT): time.sleep_ms(50) self.cmd(58, 0, 0, 4) self.cmd(55, 0, 0) if self.cmd(41, 0x40000000, 0) == 0: self.cmd(58, 0, 0, -4) # 4-byte response, negative means keep the first byte ocr = self.tokenbuf[0] # get first byte of response, which is OCR if not ocr & 0x40: # SDSC card, uses byte addressing in read/write/erase commands self.cdv = 512 else: # SDHC/SDXC card, uses block addressing in read/write/erase commands self.cdv = 1 # print("[SDCard] v2 card") return raise OSError("timeout waiting for v2 card") def cmd(self, cmd, arg, crc, final=0, release=True, skip1=False): self.cs(0) # create and send the command buf = self.cmdbuf buf[0] = 0x40 | cmd buf[1] = arg >> 24 buf[2] = arg >> 16 buf[3] = arg >> 8 buf[4] = arg buf[5] = crc self.spi.write(buf) if skip1: self.spi.readinto(self.tokenbuf, 0xFF) # wait for the response (response[7] == 0) for i in range(_CMD_TIMEOUT): self.spi.readinto(self.tokenbuf, 0xFF) response = self.tokenbuf[0] if not (response & 0x80): # this could be a big-endian integer that we are getting here # if final<0 then store the first byte to tokenbuf and discard the rest if final < 0: self.spi.readinto(self.tokenbuf, 0xFF) final = -1 - final for j in range(final): self.spi.write(b"\xff") if release: self.cs(1) self.spi.write(b"\xff") return response # timeout self.cs(1) self.spi.write(b"\xff") return -1 def readinto(self, buf): self.cs(0) # read until start byte (0xff) for i in range(_CMD_TIMEOUT): self.spi.readinto(self.tokenbuf, 0xFF) if self.tokenbuf[0] == _TOKEN_DATA: break time.sleep_ms(1) else: self.cs(1) raise OSError("timeout waiting for response") # read data mv = self.dummybuf_memoryview if len(buf) != len(mv): mv = mv[: len(buf)] self.spi.write_readinto(mv, buf) # read checksum self.spi.write(b"\xff") self.spi.write(b"\xff") self.cs(1) self.spi.write(b"\xff") def write(self, token, buf): self.cs(0) # send: start of block, data, checksum self.spi.read(1, token) self.spi.write(buf) self.spi.write(b"\xff") self.spi.write(b"\xff") # check the response if (self.spi.read(1, 0xFF)[0] & 0x1F) != 0x05: self.cs(1) self.spi.write(b"\xff") return # wait for write to finish while self.spi.read(1, 0xFF)[0] == 0: pass self.cs(1) self.spi.write(b"\xff") def write_token(self, token): self.cs(0) self.spi.read(1, token) self.spi.write(b"\xff") # wait for write to finish while self.spi.read(1, 0xFF)[0] == 0x00: pass self.cs(1) self.spi.write(b"\xff") def readblocks(self, block_num, buf): nblocks = len(buf) // 512 assert nblocks and not len(buf) % 512, "Buffer length is invalid" if nblocks == 1: # CMD17: set read address for single block if self.cmd(17, block_num * self.cdv, 0, release=False) != 0: # release the card self.cs(1) raise OSError(5) # EIO # receive the data and release card self.readinto(buf) else: # CMD18: set read address for multiple blocks if self.cmd(18, block_num * self.cdv, 0, release=False) != 0: # release the card self.cs(1) raise OSError(5) # EIO offset = 0 mv = memoryview(buf) while nblocks: # receive the data and release card self.readinto(mv[offset : offset + 512]) offset += 512 nblocks -= 1 if self.cmd(12, 0, 0xFF, skip1=True): raise OSError(5) # EIO def writeblocks(self, block_num, buf): nblocks, err = divmod(len(buf), 512) assert nblocks and not err, "Buffer length is invalid" if nblocks == 1: # CMD24: set write address for single block if self.cmd(24, block_num * self.cdv, 0) != 0: raise OSError(5) # EIO # send the data self.write(_TOKEN_DATA, buf) else: # CMD25: set write address for first block if self.cmd(25, block_num * self.cdv, 0) != 0: raise OSError(5) # EIO # send the data offset = 0 mv = memoryview(buf) while nblocks: self.write(_TOKEN_CMD25, mv[offset : offset + 512]) offset += 512 nblocks -= 1 self.write_token(_TOKEN_STOP_TRAN) def ioctl(self, op, arg): if op == 4: # get number of blocks return self.sectors if op == 5: # get block size in bytes return 512 然后再创建一个新的文件,在这个文件里我们尝试用代码来读写 microSD卡: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152import os # 用于文件系统操作,如挂载、卸载、列出目录内容等from machine import Pin, SoftSPIfrom sdcard import SDCard# 接线说明:# MISO -> GPIO 19# MOSI -> GPIO 23# CLK -> GPIO 18# CS -> GPIO 5spisd = SoftSPI(-1, miso=Pin(19), mosi=Pin(23), sck=Pin(18))sd = SDCard(spisd, Pin(5))# os.listdir() --> 返回当前目录下的所有文件和目录的列表print('未挂载SD之前:', os.listdir()) print("-------------------------------")# 创建一个虚拟文件系统,用于挂载SD卡vfs = os.VfsFat(sd) # 挂载SD卡到虚拟文件系统:将虚拟文件系统挂载到/sd目录。之后,对/sd目录的访问将实际访问SD卡。os.mount(vfs, '/sd') print('挂载SD之后:', os.listdir())# 对SD卡进行操作try: # 切换到SD卡目录 os.chdir('/sd') print('SD卡中的文件:', os.listdir()) # 写入文件到SD卡 with open("/sd/test.txt", "w") as f: for i in range(1, 101): f.write(str(i) + "\n") print("-------------------------------") print("已经将1~100写入到SD卡中的test.txt文件") print("-------------------------------") # 从SD卡读取文件并显示内容 with open("/sd/led.py", "r") as f: content = f.readlines() print("SD卡中led.py文件的内容:") print("-------------------------------") for line in content: print(line.strip()) print("-------------------------------")except Exception as e: print("\n发生错误!", e)finally: os.umount('/sd') print("SD卡已卸载") 在将microSD卡装载前,我其实早已在卡内下载了一个名为led.py的文件(这个文件其实就是程序示例中点灯程序的代码文件),这里作为了读取文件的示例。 未完待续…… 项目开发——Arduino篇接下来,我们将正式开启 Arduino 开发的相关内容讲解。在打开 Arduino IDE 后,你会看到两个关键函数,即 setup() 和 loop()。这两个函数的功能与 MicroPython 中的 boot.py 与 main.py 类似。其中,setup() 函数通常用于设备的初始化操作,例如配置引脚模式、初始化串口通信等;而 loop() 函数则用于实现设备的循环程序功能,程序会在此函数中不断重复执行特定任务。 点灯在IDE里复制以下代码,并编译上传。如果你能看见板子上LED闪烁,则说明运行成功。 12345678910111213141516171819void setup() { // 初始化串口通信,用于打印调试信息 Serial.begin(115200); // 设置引脚 2 为输出模式 pinMode(2, OUTPUT);}void loop() { // 关闭 LED(假设低电平点亮 LED) digitalWrite(2, LOW); Serial.println("灭"); delay(1000); // 延时 1 秒 // 打开 LED(假设高电平关闭 LED) digitalWrite(2, HIGH); Serial.println("亮"); delay(1000); // 延时 1 秒} 呼吸灯123456789101112131415161718192021222324// 宏定义 GPIO 输出引脚#define LED_PIN 2void setup() { // 配置 GPIO 输出引脚 pinMode(LED_PIN, OUTPUT);}void loop() { // 实现渐亮效果 for (int i = 0; i < 256; i++) { // 设置亮度模拟值 analogWrite(LED_PIN, i); // 延时 10ms delay(10); } // 实现渐灭效果 for (int i = 255; i >= 0; i--) { // 设置亮度模拟值 analogWrite(LED_PIN, i); // 延时 10ms delay(10); }} WIFI1234567891011121314151617181920212223242526272829#include <WiFi.h>// 定义SSID与密码const char * ssid = "xxxxx"; const char * password = "xxxxxx"; void setup() { Serial.begin(9600); // 断开之前的连接 WiFi.disconnect(true); // 连接 Wi-Fi WiFi.begin(ssid, password); Serial.print("正在连接 Wi-Fi"); // 检测是否链接成功 while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("连接成功"); Serial.print("IP address: "); Serial.println(WiFi.localIP());}void loop() {} 未完待续…… 参考资料 ESP-WROOM-32开发板笔记-CSDN博客 乐鑫科技 it项目网-ESP32篇 MicroPython官方文档 Random Nerd Tutorials ESP-WROOM-32 技术规格书 MicroPython+ESP32实现蓝牙通信-CSDN博客 esp32蓝牙开发教程 esp32蓝牙使用_laojean的技术博客_51CTO博客 使用 ESP32 + MicroPython 连接 | EMQX Platform 文档 DS18B20温度传感器原理详解及例程代码-CSDN博客 DHT11详细介绍(内含51和STM32代码)-CSDN博客 HC-SR04超级简单教程(快速入门)-CSDN博客 MPU6050 6轴姿态传感器的分析与使用(一)-CSDN博客 esp32家族 esp32产品_killads的技术博客_51CTO博客 ESP32的芯片有几种_esp32各个型号区别-CSDN博客 下班后实验室的个人空间-下班后实验室个人主页-哔哩哔哩视频 (bilibili.com) 步进电机28BYJ-48的驱动(arduino,STM32平台),最全的驱动详细原理,驱动电路分析,驱动代码解释_步进电机28by代码-CSDN博客 芯片简介之PCF8574-CSDN博客 详解PCF8574 I/O扩展芯片I2C驱动原理与实现-开发者社区-阿里云 附件:金苹果派の开发板研究
阅读全文
计算机
11 分钟
人工智能硬件探秘:从CPU到类脑芯片
从数据中心到边缘设备,AI 的每一次运算都离不开硬件的支撑。本文带你认识 CPU、GPU、TPU、NPU、FPGA 与类脑芯片的工作原理、适用场景与取舍,并展望量子计算、DNA 计算等前沿方向。
阅读全文
计算机
128 分钟
初识人工智能——神经网络&强化学习
从 AlphaGO 到斯坦福小镇,人工智能正以前所未有的速度渗透各个领域。本文带你初识 AI:它如何让机器”能看、能听、能说、能想”,并聚焦神经网络与强化学习的核心思想。
阅读全文Godot从安装到卸载——Godot学习笔记
游戏引擎就像一座”虚拟世界工厂”。本篇从游戏引擎概念讲起,带你认识开源轻量的 Godot:节点树架构、类 Python 的 GDScript 语法,以及从官网/Steam 安装到上手开发的完整学习笔记。
阅读全文基于ESP32的Minecraft服务器状态查询摆件
为免去频繁查看 MC 服务器状态的繁琐,我用一块 ESP32 加 0.96 英寸 OLED 屏(SSD1306)做了个桌面状态摆件,约 20~25 元成本即可实时显示服务器离线状态与在线玩家数,本文附完整 MicroPython 代码。
阅读全文
开发板
77 分钟
开发板研究笔记—ESP8266篇(停更)
前言最近我购买了一款内置ESP8266的NodeMCU开发板,这标志着我首次涉足开发板领域。开发板巧妙融合了软件与硬件技术,恰逢我对计算机编程及电子电路设计抱有浓厚兴趣,虽仅初窥门径。因此,此番购板实则是将我的两大爱好融汇贯通的一次尝试。鉴于初获此板时,我对其操作一无所知,我特撰研究笔记一篇,以期记录学习心得。 NodeMCU是一块物联网(IOT)开发板,能连WiFi,用起来跟Arduino差不多。你可以用Arduino的软件来编它的程序。ESP8266是块小芯片,NodeMCU就是围绕这块芯片做的板子。NodeMCU上有两排小针脚,直接连着ESP8266的引脚,做实验很方便。板上还有USB接口和电压转换的小电路,你只用一根USB线就能控制它。 特别说明:此文是本人的研究笔记,并非专业教程。若文中存在学术疏漏,恳请不吝指正,望各位大佬能温和以待,雅正为盼。 一、开发环境搭建在踏入物联网编程的奇妙旅程之前,首先是要安装好必要的开发环境,这是编写程序代码不可或缺的基石。第一步便是下载Arduino IDE,作为我们编写代码的主战场,同时,为了支持ESP8266这一强大的物联网芯片,我们还需要额外下载ESP8266资源(通常也是以exe文件的形式提供)。完成这些下载与安装后,接下来就可以打开Arduino IDE,并按照以下图示进行配置,为接下来的项目开发做好充分准备。 附:Arduino介绍 Arduino是一款开源电子原型平台,集成了硬件(如Arduino板)和软件(Arduino IDE),以简单易用的方式让初学者和非专业人士能够快速上手进行电子项目的设计和原型制作。它支持C/C++编程,通过一系列引脚连接各种传感器、执行器等外部设备,实现环境感知和反馈控制,广泛应用于教育、项目开发、艺术创作等领域。Arduino以其低成本、灵活性、强大的社区支持和丰富的资源而受到全球创客和爱好者的喜爱。 二、了解开发板结构 如左图,这块板子将CH340串口芯片(图片中的款式)和FLASH闪存芯片(25Q32)与ESP8266集成在了一起,并且预留了MicroUSB(有的厂商会焊接type-c接口)接口,也就是说,我们可以直接利用MicroUSB数据线直接将板子与电脑连接进行数据传输。而这个完整的组合就是NodeMCU。 接口:如图所示(注:仅供参考): ESP8266+NodeMCU的各个接口具有多种作用,这些接口主要包括电源接口、GPIO引脚、串行端口(UART)、I2C接口、SPI接口、ADC接口、PWM引脚以及控制引脚等。以下是对这些接口作用的详细解释: 1. 电源接口VIN引脚:如果有5V稳压源,VIN引脚可用于直接为ESP8266及其外设供电。 3.3V引脚:这些是板载稳压器的输出,可用于为外部组件供电。 GND引脚:为ESP8266 NodeMCU开发板的接地引脚。 注:NodeMCU开发板引脚的输入输出电压的限制是3.3V。如果向引脚施加3.3V以上的电压就有可能对芯片电路造成损伤。最大输出电流是12mA。 2. GPIO引脚ESP8266 NodeMCU有17个GPIO引脚(GPIO0~GPIO16),这些引脚可以通过编程方式分配给各种功能,如I2C、I2S、UART、PWM、红外遥控、LED灯和按钮等。 需要注意的是,GPIO6~GPIO11引脚通常被用于连接开发板的闪存,因此在实验电路中应避免使用这些引脚进行通用输入输出。 GPIO0-15引脚配有内置上拉电阻,而GPIO16引脚配有内置下拉电阻。 开发板引脚的输入输出电压限制是3.3V,向引脚施加高于3.6V的电压可能会对芯片电路造成损坏。 在NodeMCU上有类似于D1、D2之类的编号,这些引脚编号指的是开发板引脚名,跟ESP8266的引脚并不是一一对应(并不是D1对应GPIO1),比如,对于digitalWrite(4, HIGH);,我们操作的引脚是:GPIO4引脚。不是D4引脚。(该操作也可以写成:digitalWrite(D2, HIGH);)也就是说,当直接使用数字的时候,操作的引脚是GPIO引脚,而ESP8266引脚与NodeMCU引脚对应关系可以利用上面的图片查看。 3. 串行端口(UART)ESP8266有两个硬件串行端口(UART):UART0和UART1。 UART0使用GPIO1(TX0)和GPIO3(RX0)引脚,负责与电脑进行通信,但也用于其他操作时可能导致程序无法烧录成功。 UART1使用GPIO2(TX1)引脚发送数据,但由于GPIO8被用于连接闪存芯片,所以无法接收数据。 4. I2C接口ESP8266只有软件模拟的I2C端口,没有硬件的I2C端口。因此,可以使用任意的两个GPIO引脚通过软件模拟I2C通讯。I2C引脚用于连接项目中的各种I2C传感器和外设,支持I2C Master和I2C Slave。 5. SPI接口ESP8266在从模式和主模式下具有两个SPI接口(SPI和HSPI)。SPI接口包括GPIO14(CLK)、GPIO12(MISO)、GPIO13(MOSI)和GPIO15(CS/SS)等引脚。 6. ADC接口ESP8266只有一个模拟输入引脚,该引脚通过模拟-数字转换将引脚上的模拟电压数值转化为数字量。 此引脚可以读取的模拟电压值为0–1.0V,但请注意,将ESP8266芯片模拟输入引脚连接在1.0V以上电压可能会损坏ESP8266芯片。 幸运的是,NodeMCU开发板配有降压电路,因此可以用NodeMCU开发板的模拟输入引脚读取0-3.3V的模拟电压信号。 7. PWM引脚ESP8266 NodeMCU开发板具有4个脉冲宽度调制(PWM)通道,PWM输出可以通过编程方式实现,并用于驱动数字电机和LED等。 8. 控制引脚控制引脚包括芯片使能引脚(EN)、复位引脚(RST)和WAKE引脚等。 EN引脚:当EN引脚拉高时,ESP8266芯片被启用;当拉低时,芯片以最小功率工作。 RST引脚:RST引脚用于复位ESP8266芯片。 按钮:1.Reset(RST)键Reset键,通常也被称为复位键,其主要功能是将ESP8266模块重置到其初始状态。具体来说,Reset键的功能包括: 重启设备:按下Reset键会使ESP8266模块重新启动,这有助于在程序运行出错或需要重新开始运行时快速恢复设备到初始状态。 深度睡眠唤醒:在特定设置中,Reset键还可以作为ESP8266深度睡眠模式的唤醒源。通过将Reset键连接到具有唤醒功能的GPIO引脚(如GPIO 16),在深度睡眠模式结束时,GPIO 16会发送低电平信号到Reset键,从而唤醒ESP8266模块。 2.Flash键Flash键通常与ESP8266的固件更新或编程相关,其主要功能包括: 进入下载模式:在ESP8266上电前或复位时,按住Flash键可以使设备进入下载模式(也称为烧录模式)。在这个模式下,可以通过编程工具(如Arduino IDE配合ESP8266插件)将新的固件或程序烧录到ESP8266的Flash存储器中。这对于更新固件、修复错误或安装新功能非常有用。 固件更新:配合烧录软件,Flash键使得用户可以轻松更新ESP8266的固件。这对于保持设备的安全性、兼容性和功能更新至关重要。 三、项目开发【Arduino】第一个项目:点亮一个LED利用Arduino IDE创建一个新的工程项目,如果想偷懒可以看看IDE自带示例内的项目,这里选用一个名为Blink的项目进行演示和测试: 文件打开后,进行开发板的匹配(前文已说),可以看到有以下代码(注释已替换): 12345678910111213141516171819202122232425// 定义setup()函数,该函数在Arduino板通电或重置后自动运行一次 void setup() { // 使用pinMode()函数将内置LED引脚设置为输出模式 // LED_BUILTIN是一个预定义的宏,它代表了开发板上内置LED的引脚编号 // OUTPUT是一个常量,表示该引脚将用作输出引脚 pinMode(LED_BUILTIN, OUTPUT); } // 定义loop()函数,该函数在setup()函数执行完毕后不断循环执行 void loop() { // 使用digitalWrite()函数将内置LED引脚设置为低电平(LOW) // 这通常意味着LED会熄灭(具体取决于LED的连接方式,但在这个上下文中,它通常是熄灭) digitalWrite(LED_BUILTIN, LOW); // 使用delay()函数暂停程序执行1000毫秒(即1秒) // 这意味着LED将保持熄灭状态1秒钟 delay(1000); // 再次使用digitalWrite()函数,但这次是将内置LED引脚设置为高电平(HIGH) // 这通常意味着LED会点亮(同样,具体取决于LED的连接方式) digitalWrite(LED_BUILTIN, HIGH); // 再次使用delay()函数暂停程序执行2000毫秒(即2秒) // 这意味着LED将保持点亮状态2秒钟 delay(2000); } 这段代码实现了一个非常简单的LED闪烁效果。在setup()函数中,它首先通过pinMode()函数将内置LED引脚配置为输出模式,以便能够控制LED的亮灭。然后,在loop()函数中,它通过一个无限循环不断地将LED引脚设置为低电平(熄灭LED),等待1秒,然后将其设置为高电平(点亮LED),再等待2秒。这个过程会不断重复,导致LED以1秒熄灭、2秒点亮的模式闪烁。 接下来点击IDE内的“上传”,将程序传入进板子内。观察输出,如果出现以下提示则说明写入成功。若发现模块上的LED进行了规律性闪烁说明程序运行成功: . Variables and constants in RAM (global, static), used 28104 / 80192 bytes (35%)║ SEGMENT BYTES DESCRIPTION╠══ DATA 1496 initialized variables╠══ RODATA 920 constants╚══ BSS 25688 zeroed variables. Instruction RAM (IRAM_ATTR, ICACHE_RAM_ATTR), used 59667 / 65536 bytes (91%)║ SEGMENT BYTES DESCRIPTION╠══ ICACHE 32768 reserved space for flash instruction cache╚══ IRAM 26899 code in IRAM. Code in flash (default, ICACHE_FLASH_ATTR), used 232100 / 1048576 bytes (22%)║ SEGMENT BYTES DESCRIPTION╚══ IROM 232100 code in flashesptool.py v3.0Serial port COM5Connecting….Chip is ESP8266EXFeatures: WiFiCrystal is 26MHzMAC: e0:98:06:07:e0:f6Uploading stub…Running stub…Stub running…Configuring flash size…Auto-detected Flash size: 4MBCompressed 265568 bytes to 195684…Writing at 0x00000000… (8 %)Writing at 0x00004000… (16 %)Writing at 0x00008000… (25 %)Writing at 0x0000c000… (33 %)Writing at 0x00010000… (41 %)Writing at 0x00014000… (50 %)Writing at 0x00018000… (58 %)Writing at 0x0001c000… (66 %)Writing at 0x00020000… (75 %)Writing at 0x00024000… (83 %)Writing at 0x00028000… (91 %)Writing at 0x0002c000… (100 %)Wrote 265568 bytes (195684 compressed) at 0x00000000 in 17.2 seconds (effective 123.5 kbit/s)…Hash of data verified. Leaving…Hard resetting via RTS pin… 拓:分析过程 1. 内存使用情况 . Variables and constants in RAM (global, static), used 28104 / 80192 bytes (35%) 这行显示了ESP8266的RAM(随机存取存储器)中全局和静态变量及常量的使用情况。总共有80192字节的RAM,其中28104字节被用于这些变量和常量,占用了35%的RAM。 接下来,详细列出了不同类型的内存使用情况: DATA:1496字节,用于已初始化的变量。 RODATA:920字节,用于只读数据(常量)。 BSS:25688字节,用于未初始化的变量(这些变量在程序开始时会被自动清零)。 2. 指令RAM(IRAM)使用情况 . Instruction RAM (IRAM_ATTR, ICACHE_RAM_ATTR), used 59667 / 65536 bytes (91%) 这行显示了ESP8266的指令RAM(IRAM)的使用情况。IRAM用于存储需要快速访问的代码。总共有65536字节的IRAM,其中59667字节被使用,占用了91%的IRAM。 接下来,详细列出了IRAM的使用情况: ICACHE:32768字节,这是为闪存指令缓存保留的空间,实际上并不直接存储代码,而是用作缓存。 IRAM:26899字节,这是实际存储在IRAM中的代码量。 3. 闪存中的代码(Flash) . Code in flash (default, ICACHE_FLASH_ATTR), used 232100 / 1048576 bytes (22%) 这行显示了ESP8266的闪存中存储的代码量。总共有1048576字节(即1MB)的闪存空间用于代码存储,其中232100字节被使用,占用了22%的闪存空间。 接下来的IROM段表示了存储在闪存中的代码量,即232100字节。 4. esptool.py 固件烧写过程 esptool.py v3.0Serial port COM5Connecting….Chip is ESP8266EXFeatures: WiFiCrystal is 26MHzMAC: e0:98:06:07:e0:f6…Writing at 0x0002c000… (100%)Wrote 265568 bytes (195684 compressed) at 0x00000000 in 17.2 seconds (effective 123.5 kbit/s)…Hash of data verified.Leaving…Hard resetting via RTS pin… 这部分输出来自于使用esptool.py(ESP8266的固件烧写工具)将固件烧写到ESP8266设备的过程。 esptool.py v3.0:表示使用的esptool.py版本是3.0。 Serial port COM5:表示ESP8266设备通过COM5端口与计算机连接。 接下来的几行显示了设备的识别信息,包括芯片型号(ESP8266EX)、功能(WiFi)、晶振频率(26MHz)和MAC地址。烧写过程中,固件被分成多个块写入到设备的闪存中,每写入一个块都会显示当前的进度百分比。烧写完成后,工具会验证数据的哈希值以确保固件正确写入,并通过RTS引脚执行硬复位以重启设备。 第二个项目:连接网络我们可以充分利用ESP8266的WIFI模块进行网络连接,并利用开发板搭建一个简易的http服务器。示例代码: 12345678910111213141516171819202122232425262728293031323334353637#include <ESP8266WiFi.h> #include <ESP8266WebServer.h> const char* ssid = "abcd"; const char* password = "12345678"; ESP8266WebServer server(80); // 创建Web服务器对象,监听80端口 void handleRoot() { server.send(200, "text/plain", "This is an http server developed based on ESP8266+NodeMCU. More information:https://blog.goldenapplepie.xyz/"); } void setup() { //server主体 Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.println("WiFi connected"); Serial.print("IP address: "); Serial.println(WiFi.localIP()); server.on("/", handleRoot); // 设置根路径的处理函数 server.begin(); // 启动服务器 Serial.println("HTTP server started"); } void loop() { //server server.handleClient(); // 处理客户端请求 } 设备运行该程序时,会不断闪烁LED(同时输出窗口提示“….”),表示正在联网中,当成功联网后LED保持常亮(同时输出窗口出现WiFi connected等提示)。接下来就可以利用IP(在输出窗口可以查询)在浏览器访问这个网页了。需要注意的是,我们只能利用局域网连接这个板子,所以得保证两端必须在同一网络下。 在网页上能看到“This is an http server developed based on ESP8266+NodeMCU. More information:https://blog.goldenapplepie.xyz/”一行字则说明一切正常。 我们可以继续修改html部分给网页升级(这将涉及html+css+js的编写): 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980#include <ESP8266WiFi.h>#include <ESP8266WebServer.h>const char *ssid = "";const char *password = "";ESP8266WebServer server(80);// 假设LED_BUILTIN是内置的LED引脚编号,通常NodeMCU上是D4(GPIO2)#define LED_BUILTIN 2bool isWiFiConnected = false;void handleRoot(){ String html = "<html>" "<head>" "<title>ESP8266 HTTP Server</title>" "<style>" "body { font-family: Arial, sans-serif; }" "h2 { color: blue; }" "a {color: #fad17e;text-decoration: none;}" "</style>" "</head>" "<body>" "<h2>Welcome to my ESP8266 HTTP Server</h1>" "<p>This is an http server developed based on ESP8266+NodeMCU.</p>" "<p><a href='https://blog.goldenapplepie.xyz/'>GoldenApplePie's blog</a></p>" "<script>" "function showAlert() {" " alert('yee');" "}" "</script>" "<button onclick='showAlert()'>Click Me</button>" "</body>" "</html>"; server.send(200, "text/html", html);}void setup(){ pinMode(LED_BUILTIN, OUTPUT); Serial.begin(115200); // 开始连接WiFi,并在连接过程中让LED闪烁 WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); // 切换LED状态以指示正在连接 digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); } // WiFi连接成功后,设置LED为常亮 digitalWrite(LED_BUILTIN, LOW); isWiFiConnected = true; Serial.println(""); Serial.println("WiFi connected"); Serial.print("IP address: "); Serial.println(WiFi.localIP()); server.on("/", handleRoot); server.begin(); Serial.println("HTTP server started");}void loop(){ if (!isWiFiConnected) { } // 处理客户端请求 server.handleClient();} 在handleRoot()部分,我们可以利用前端代码进一步升级网页内容。由于没有加装本地文件读取的库,所以只能利用将html代码逐行转换为字符串,让其作为上传内容使用。设备运行后你可以在网页上看到: 第三个项目:使用MQTT随着物联网技术的迅猛发展,一个由海量设备互联互通的庞大生态系统应运而生,这一过程中,通信协议作为核心要素,其重要性不言而喻。面对设备接入量的激增及其对网络带宽、时延、协议兼容性及平台服务架构的严苛要求,物联网协议亟需解决一系列关键通信难题。在此背景下,MQTT(Message Queuing Telemetry Transport)协议应运而生,它以其轻量级、高效能、高可靠性、支持海量连接及提供安全双向通信等显著优势,经过时间的验证与技术的迭代,已成为物联网领域的标准通信协议,广泛应用于车联网、工业物联网、智能家居、智慧交通等多个前沿领域,推动着物联网生态的持续繁荣与发展。 MQTT分为客户端、服务端。每个客户端可以接收数据(订阅)、发送数据(发布),每个消息都由主题+消息内容组成。假设我们有4个客户端(a、b、c、d)和1个服务端,这4个客户端都已经通过网络连接上了服务端。假如客户端a向服务器订阅了一个主题为abc的信息,那么当服务器接收到来自任何一个客户端(b/c/d)的消息时,只要这个消息的主题是abc,那么服务器就会将这个消息发送给客户端a同理,如果除了客户端a之外还有其它的客户端(b/c)也订阅了abc这个主题,那么服务器也会同时将这个消息送给客户端b、客户端c。 在本小节,我会尝试创建一个MQTT服务器,并且利用ESP8266连接,实现PC、ESP8266消息互通。首先,我们要创建一个MQTT服务器,你可以选择在自己的电脑本地创建,也可以利用云资源进行部署。在这里我用云资源演示。 我推荐EMQX,打开网站注册/登录账号后,选择一个合适的产品进行云资源部署,这里以专有版云服务为例,按照指引部署即可,实在不行可以查看他们站内的文档,非常详细。当你创建完一个MQTT云服务器后,请切换到该服务器的控制台界面,找到“连接信息”,这一栏包含了连接地址和各种端口。为了测试,你需要在控制台内添加几个客户端认证用户。 接下来为了测试连接情况,我们需要创建一系列的客户端(至少2个),分别在ESP8266和电脑上运行。电脑上你可以选择利用EMQX内的客户端资源,EMQX站内提供了各种编程语言的客户端开发教程,你也可以选择使用现成的MQTT客户端工具:MQTTX。MQTTX提供了一个可视化的操作界面,让MQTT调试变得更简单便捷。 下载并安装好MQTTX后,打开程序在出现的窗口找到“General”一栏开始信息填写。Name只是一个区分名,对MQTT的连接影响不是很大;Client ID是你的自定义名称,它用于在MQTT消息中区分不同用户,类似聊天昵称;Host部分则需要在后边一栏填入刚刚在MQTT服务器控制台记下的连接地址;Port代表的是连接端口,默认为1883。对于其它内容和细节,你可以查阅相关文档。 填入完信息后,点击右上角的Connect,当提示Connected时说明连接成功。这时候你需要在左边找到New Subscription并点击它以订阅一个话题。这个话题你可以类比成一个聊天群的群名(或群号),我们可以通过这个聊天群名进行消息发送和接收。订阅就相当于加群,接下来你可以在不同的客户端上用这个“群”来聊天了。 让我们把视线换到ESP8266上,接下来我们利用代码在ESP8266上创建一个MQTT客户端。代码示例(请安装PubSubClient库): 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889#include <ESP8266WiFi.h>#include <PubSubClient.h>const int ledPin = 2; // WiFi 设置const char *ssid = ""; // WiFi账 const char *password = ""; // WiFi密// MQTT 设置const char *mqtt_broker = ""; // MQTT服务器连接地址const char *mqtt_topic = ""; //话题const char *mqtt_username = ""; // 客户端用户名const char *mqtt_password = ""; // 客户端用户密码const int mqtt_port = 1883; // MQTT 端口 (TCP)WiFiClient espClient;PubSubClient mqtt_client(espClient);void connectToWiFi();void connectToMQTTBroker();void mqttCallback(char *topic, byte *payload, unsigned int length);void blinkLED(); // LED亮or不亮?void setup() { pinMode(ledPin, OUTPUT); Serial.begin(115200); connectToWiFi(); mqtt_client.setServer(mqtt_broker, mqtt_port); mqtt_client.setCallback(mqttCallback); connectToMQTTBroker();}// 网络连接void connectToWiFi() { WiFi.begin(ssid, password); Serial.print("Connecting to WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(500); blinkLED(); Serial.print("."); } digitalWrite(ledPin, HIGH); Serial.println("\nConnected to the WiFi network");}// LED闪烁void blinkLED() { digitalWrite(ledPin, HIGH); delay(500); digitalWrite(ledPin, LOW); delay(500);}// 连接MQTT服务器void connectToMQTTBroker() { while (!mqtt_client.connected()) { String client_id = "MQTT/ESP8266"; Serial.printf("Connecting to MQTT Broker as %s.....\n", client_id.c_str()); if (mqtt_client.connect(client_id.c_str(), mqtt_username, mqtt_password)) { Serial.println("Connected to MQTT broker"); mqtt_client.subscribe(mqtt_topic); // Publish message upon successful connection mqtt_client.publish(mqtt_topic, "ESP8266:Hello,I'm ESP8266!"); // 发送的消息 } else { Serial.print("Failed to connect to MQTT broker, rc="); Serial.print(mqtt_client.state()); Serial.println(" try again in 5 seconds"); delay(5000); } }}// 响应MQTTvoid mqttCallback(char *topic, byte *payload, unsigned int length) { Serial.print("Message received on topic: "); Serial.println(topic); Serial.print("Message:"); for (unsigned int i = 0; i < length; i++) { Serial.print((char) payload[i]); } Serial.println(); Serial.println("-----------------------");}void loop() { if (!mqtt_client.connected()) { connectToMQTTBroker(); } mqtt_client.loop();} 将代码上传至ESP8266并成功运行后,你可以在MQTTX和Adruino控制台上同时看到消息内容(ESP8266:Hello,I’m ESP8266!)。 接下来我们给这个程序继续加点花样。我们可以将ESP8266当成一个聊天机器人的客户端,当聊天机器人识别到其他用户发出消息的关键词后会做出回应。比如当用户发送“你好”时,ESP8266会发送“你好呀~”。代码示例: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109#include <ESP8266WiFi.h>#include <PubSubClient.h>const int ledPin = 2; // WiFi settingsconst char *ssid = ""; const char *password = ""; // MQTT Broker settingsconst char *mqtt_broker = ""; const char *mqtt_topic = ""; const char *mqtt_username = ""; const char *mqtt_password = ""; const int mqtt_port = 1883; // MQTT port (TCP)// 关键词和回复const char* KEYWORD_HELLO = "你好";const char* REPLY_HELLO = "你好呀~";// 用于跟踪是否已经回复过的标志bool hasReplied = false;WiFiClient espClient;PubSubClient mqtt_client(espClient);void connectToWiFi();void connectToMQTTBroker();void mqttCallback(char *topic, byte *payload, unsigned int length);void blinkLED(); // 新增函数,用于LED闪烁void setup() { pinMode(ledPin, OUTPUT); // 设置LED引脚为输出模式 Serial.begin(115200); connectToWiFi(); mqtt_client.setServer(mqtt_broker, mqtt_port); mqtt_client.setCallback(mqttCallback); connectToMQTTBroker();}void connectToWiFi() { WiFi.begin(ssid, password); Serial.print("Connecting to WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(500); blinkLED(); // 在连接WiFi过程中,LED闪烁 Serial.print("."); } digitalWrite(ledPin, HIGH); Serial.println("\nConnected to the WiFi network");}void blinkLED() { // 新增函数,实现LED闪烁效果 digitalWrite(ledPin, HIGH); delay(500); digitalWrite(ledPin, LOW); delay(500);}void connectToMQTTBroker() { while (!mqtt_client.connected()) { String client_id = "MQTT/ESP8266"; Serial.printf("Connecting to MQTT Broker as %s.....\n", client_id.c_str()); if (mqtt_client.connect(client_id.c_str(), mqtt_username, mqtt_password)) { Serial.println("Connected to MQTT broker"); mqtt_client.subscribe(mqtt_topic); // Publish message upon successful connection mqtt_client.publish(mqtt_topic, "ESP8266:聊天机器人已上线~"); } else { Serial.print("Failed to connect to MQTT broker, rc="); Serial.print(mqtt_client.state()); Serial.println(" try again in 5 seconds"); delay(5000); } } hasReplied = false;}void mqttCallback(char* topic, byte* payload, unsigned int length) { // 将收到的消息转换为字符串 char message[length + 1]; strncpy(message, (char*)payload, length); message[length] = '\0'; // 确保字符串以null结尾 // 检查关键词并回复 if (strstr(message, KEYWORD_HELLO) != NULL && !hasReplied) { // 发送回复消息,只发送一次 mqtt_client.publish(mqtt_topic, REPLY_HELLO); // 设置标志,表示已经回复过 hasReplied = true; } // 打印收到的消息(可选,用于调试) Serial.print("Received message: "); Serial.println(message);}void loop() { if (!mqtt_client.connected()) { connectToMQTTBroker(); // 尝试重新连接MQTT代理 } mqtt_client.loop(); // 处理MQTT消息 // 每10秒自动重置hasReplied标志,以确保ESP8266能够再次对"你好"消息作出回应 static unsigned long lastResetTime = 0; unsigned long currentTime = millis(); if (currentTime - lastResetTime > 2000) { hasReplied = false; lastResetTime = currentTime; // 更新上次重置时间 }} 在上面的代码上继续修改,当我发送“打开LED”时,开发板上的LED亮;当我发送“关闭LED”时,开发板上的LED灭;当我发送“LED闪烁”时,开发板上的LED开启闪烁模式,代码示例如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140#include <ESP8266WiFi.h>#include <PubSubClient.h>const int ledPin = 2; // WiFi settingsconst char *ssid = ""; const char *password = ""; // MQTT Broker settingsconst char *mqtt_broker = ""; const char *mqtt_topic = ""; const char *mqtt_username = ""; const char *mqtt_password = ""; const int mqtt_port = 1883; // MQTT port (TCP)// 关键词和回复const char* KEYWORD_HELLO = "你好";const char* REPLY_HELLO = "你好呀~";const char* KEYWORD_OPEN_LED = "打开LED";const char* KEYWORD_CLOSE_LED = "关闭LED";const char* KEYWORD_BLINK_LED = "LED闪烁";// 用于跟踪是否已经回复过的标志bool hasReplied = false;bool ledState = false; // false表示LED关闭,true表示LED打开bool isBlinking = false; // 表示LED是否处于闪烁状态WiFiClient espClient;PubSubClient mqtt_client(espClient);void connectToWiFi();void connectToMQTTBroker();void mqttCallback(char *topic, byte *payload, unsigned int length);void blinkLED(); // 新增函数,用于LED闪烁void setup() { pinMode(ledPin, OUTPUT); // 设置LED引脚为输出模式 Serial.begin(115200); connectToWiFi(); mqtt_client.setServer(mqtt_broker, mqtt_port); mqtt_client.setCallback(mqttCallback); connectToMQTTBroker();}void connectToWiFi() { WiFi.begin(ssid, password); Serial.print("Connecting to WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(500); blinkLED(); // 在连接WiFi过程中,LED闪烁 Serial.print("."); } digitalWrite(ledPin, HIGH); Serial.println("\nConnected to the WiFi network");}void blinkLED() { // 新增函数,实现LED闪烁效果 digitalWrite(ledPin, HIGH); delay(500); digitalWrite(ledPin, LOW); delay(500);}void blinkLED1() { if (isBlinking) { digitalWrite(ledPin, !digitalRead(ledPin)); // 切换LED状态 }}void connectToMQTTBroker() { while (!mqtt_client.connected()) { String client_id = "MQTT/ESP8266"; Serial.printf("Connecting to MQTT Broker as %s.....\n", client_id.c_str()); if (mqtt_client.connect(client_id.c_str(), mqtt_username, mqtt_password)) { Serial.println("Connected to MQTT broker"); mqtt_client.subscribe(mqtt_topic); // Publish message upon successful connection mqtt_client.publish(mqtt_topic, "ESP8266:聊天机器人已上线~"); } else { Serial.print("Failed to connect to MQTT broker, rc="); Serial.print(mqtt_client.state()); Serial.println(" try again in 5 seconds"); delay(5000); } } hasReplied = false;}void mqttCallback(char* topic, byte* payload, unsigned int length) { // 将收到的消息转换为字符串 char message[length + 1]; strncpy(message, (char*)payload, length); message[length] = '\0'; // 确保字符串以null结尾 // 检查关键词并回复 if (strstr(message, KEYWORD_HELLO) != NULL && !hasReplied) { // 发送回复消息,只发送一次 mqtt_client.publish(mqtt_topic, REPLY_HELLO); // 设置标志,表示已经回复过 hasReplied = true; } // 检查并处理LED控制关键词 if (strstr(message, KEYWORD_OPEN_LED) != NULL) { digitalWrite(ledPin, LOW); // 打开LED ledState = true; isBlinking = false; // 停止闪烁 } else if (strstr(message, KEYWORD_CLOSE_LED) != NULL) { digitalWrite(ledPin, HIGH); // 关闭LED ledState = false; isBlinking = false; // 停止闪烁 } else if (strstr(message, KEYWORD_BLINK_LED) != NULL) { isBlinking = true; // 开始闪烁 } // 打印收到的消息(可选,用于调试) Serial.print("Received message: "); Serial.println(message);}void loop() { if (!mqtt_client.connected()) { connectToMQTTBroker(); // 尝试重新连接MQTT代理 } mqtt_client.loop(); // 处理MQTT消息 // 每10秒自动重置hasReplied标志,以确保ESP8266能够再次对"你好"消息作出回应 static unsigned long lastResetTime = 0; unsigned long currentTime = millis(); if (currentTime - lastResetTime > 2000) { hasReplied = false; lastResetTime = currentTime; // 更新上次重置时间 } // 如果LED处于闪烁状态,则每隔500ms切换一次状态 if (isBlinking) { static unsigned long lastBlinkTime = 0; unsigned long currentTime = millis(); if (currentTime - lastBlinkTime > 500) { blinkLED1(); lastBlinkTime = currentTime; // 更新上次闪烁时间 } }} 你可能发现了上面的代码中,关键词触发都涉及到一个状态切换的过程,这是为了防止当你发送关键词消息后,开发板会持续循环的发送消息。 未完待续……. 【MircoPython】在上面的例子中,你会发现我们都是用Arduino结合C/C++语言编写程序的,其实ESP8266还能支持python(MicroPython)。 首先同样要安装IDE:推荐使用Thonny IDE,安装正常步骤安装后打开,并连接你的板子。 然后进行配置。先下载一个MicroPython的烧录驱动:在MicroPython官网可以找到的,点击download,找到ESP8266的驱动,找到下面的Releases选择合适的版本下载后,将其保存至一个目录内。 接下来在IDE在顶栏找:运行—配置解释器,点开后出现一个窗口。接下来先点击“Install or update MicroPython”,Port选择你板子连接电脑的端口(可以通过设备管理器查看),下面的Firmware则需填入刚刚下载驱动程序的所在路径。最后点击安装,等待安装结束。 然后回到第一个窗口,将端口或WebREPL也换成你板子连接电脑的端口,点击好的。然后刷新一下,当下方控制台出现“>>>”时(python标准的窗口),说明成功烧录。 到这里,你已经将烧录驱动给安装完毕了,整个IDE初始化的样子是这样的: boot.py是开发板启动时自动运行的主文件。开发板一通电,就会运行boot.py里的程序。为了避免出错,比如不小心写了让程序无限循环的代码,我们最好不在boot.py里直接编程。因为如果程序卡住了,连重启(按reset键)也没用,因为重启后还是会运行同样的程序。所以,我们要在别的文件里写程序,并给程序加一个“停止按钮”,比如通过按某个外置按钮(已经通过引脚连接)来让程序安全停止,这样就不怕程序出错了。 点灯程序1234567891011121314# 导入必要的库from machine import Pinimport time# 定义引脚led = Pin(2,Pin.OUT)# 设置循环while True: led.value(0) # 设置电平 print("亮") # 控制台输出 time.sleep(1) # 间隔时间 led.value(1) print("灭") time.sleep(1) 上面的代码是一个点灯实例。点击保存后它会出现提示窗口,根据需要选择操作(当然选择MicroPython设备)。然后写入后(你可以选择覆盖boot文件,也可以重新命名新文件保存)在点击运行即可。 如果你能看见板子上LED闪烁,则说明运行成功。同时,你利用面包板、杜邦线等材料,将另一个LED接入D4引脚(负极)、正极连接3v,你会发现这个LED也会闪烁,因为这个LED和板子上的LED是同一个引脚上的。将正负极对调(记得更换接线至GND引脚),你可以发现当板子led亮时,外接的LED灭,反之,板子上led灭时,外接的led亮。 呼吸灯呼吸灯是利用了开发板的PWM功能,PWM(Pulse Width Modulation)简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,广泛应用在测量、通信、工控等方面。我们可以利用PWM控制LED渐变闪烁,而这也被称为呼吸灯,下面是一个简单的示例代码,当你看到LED渐变闪烁时说明运行成功: 1234567891011121314151617181920212223from machine import Pin, PWM import utime # 初始化GPIO引脚 led_pin = Pin(2, Pin.OUT) # 初始化PWM对象,频率设置为1000Hz,占空比初始化为0(即LED关闭) pwm = PWM(led_pin, freq=500) # 呼吸灯效果函数 def breath_led(): while True: # 模拟吸气(亮度增加) for duty in range(0, 1024, 32): # 逐渐增加占空比 pwm.duty(duty) utime.sleep_ms(30) # 模拟呼气(亮度减少) for duty in range(1023, -1, -32): # 逐渐减少占空比 pwm.duty(duty) utime.sleep_ms(30) # 调用函数开始呼吸灯效果 breath_led() http服务器和Arduino一样,我们也可以利用micropython让开发板联网。接下来我们来编写基于python的http服务器程序,直接放上代码: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162import network import socket # 配置网络参数 ssid = '' password = '' # 连接到WiFi网络 sta_if = network.WLAN(network.STA_IF) sta_if.active(True) sta_if.connect(ssid, password) while not sta_if.isconnected(): pass print('网络已连接,IP地址:', sta_if.ifconfig()[0]) # 创建socket addr = ('', 80) # 监听所有可用的网络接口上的80端口 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(addr) s.listen(1) def http_response(client_socket, status_line, headers, content=""): client_socket.sendall(b'HTTP/1.1 ' + status_line.encode() + b'\r\n') for header in headers: client_socket.sendall(header.encode() + b'\r\n') client_socket.sendall(b'\r\n') client_socket.sendall(content.encode()) try: while True: print('等待连接...') client_socket, addr = s.accept() print('连接自:', addr) request = client_socket.recv(1024) print('收到请求:', request.decode()) # 简单的请求处理(仅支持GET /) if request.startswith(b'GET / HTTP/1.1'): status_line = '200 OK' headers = [ 'Content-Type: text/html', 'Connection: close' ] content = '''<!DOCTYPE html> <html> <head> <title>ESP8266 HTTP Server</title> </head> <body> <h1>Hello from ESP8266!</h1> </body> </html> ''' http_response(client_socket, status_line, headers, content) client_socket.close() except KeyboardInterrupt: s.close() print('服务器已关闭') 当看到控制台输出了IP后复制打开,当你看到网页有“Hello from ESP8266!”则说明运行成功: 结合上面的LED程序,我们可以写一个在网页上控制LED开关的程序,结构大差不差,我们主要对前端代码和后续的http请求进行了功能扩充: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586import network import socket import machine # 配置LED引脚 led = machine.Pin(2, machine.Pin.OUT) # 配置网络参数 ssid = 'GAP' password = 'nahidaaa' # 连接到WiFi网络 sta_if = network.WLAN(network.STA_IF) sta_if.active(True) sta_if.connect(ssid, password) # 等待网络连接 while not sta_if.isconnected(): pass print('网络已连接,IP地址:', sta_if.ifconfig()[0]) # HTML页面内容 HTML_CONTENT = """ <!DOCTYPE html> <html> <head> <title>LED Control</title> <script> function toggleLED(state) { var xhr = new XMLHttpRequest(); xhr.open('GET', '/' + state, true); xhr.onreadystatechange = function () { if (xhr.readyState == 4 && xhr.status == 200) { document.getElementById('ledStatus').innerText = xhr.responseText; } }; xhr.send(); } </script> </head> <body> <h1>LED Control</h1> <button onclick="toggleLED('on')">Turn LED On</button> <button onclick="toggleLED('off')">Turn LED Off</button> <p id="ledStatus">LED is OFF</p> </body> </html> """ # HTTP服务器处理函数 def http_response(client_socket, request): request = request.decode() if request.startswith('GET /on HTTP/1.1'): led.value(0) # 打开LED response = 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nLED is ON' elif request.startswith('GET /off HTTP/1.1'): led.value(1) # 关闭LED response = 'HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nLED is OFF' elif request.startswith('GET / HTTP/1.1'): response = 'HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n' + HTML_CONTENT else: response = 'HTTP/1.1 404 Not Found\r\n\r\nNot Found' client_socket.sendall(response.encode()) client_socket.close() # 创建socket并监听 addr = ('', 80) # 监听所有传入连接,端口80 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(addr) s.listen(1) print('\nHTTP服务器启动,等待连接...') try: while True: client_socket, addr = s.accept() print('\n连接自:', addr) request = client_socket.recv(4096) # 增加缓冲区大小以容纳整个HTTP请求 if request: http_response(client_socket, request) except KeyboardInterrupt: s.close() print('\n\n\n\n服务器已关闭') 温馨提示:若你在运行第二个程序时发现板子还是在运行前一个程序,记得点击RES刷新,然后再开启运行 参考资料 【物联网】基础篇 ESP8266-NodeMCU学习一_nodemcu和esp8266是什么关系 it项目网【官网】-配套软件+硬件,教程视频,通俗易懂 ESP8266+Micorpython快速入门_哔哩哔哩_bilibili Thonny MicroPython (Python) IDE ESP32 和 ESP8266 入门 一文速通ESP8266NodeMCU(基于ArduinoIDE)——万字爆肝含示例代码 2024 MQTT 协议入门教程 | EMQ (emqx.com) 附:参考文件(内涵代码文件和安装程序等)
阅读全文