自制Arduboy有一个包arduboy_homemade可以简化我们arduino配置工作，并扩展了不少功能。地址如下https://github.com/MrBlinky/Arduboy-homemade-package。安装这个包只要在Arduino IDE文件->首选项,把https://raw.githubusercontent.com/MrBlinky/Arduboy-homemade-package/master/package_arduboy_homemade_index.json 添加到“其它开发板管理器地址”。然后在开发板管理中下载包即可，当然也可以在github下载zip包后离线添加。在github主页，有一个表格Pin wiring table，里面详细说明了引脚链接,为使用方便，直接复制在这儿。Arduboy functionArduboy Leonardo/MicroDevelopmentKitProMicro 5V (standard wiring)ProMicro 5V (alternate wiring)OLED CS12 PORTD66 PORTD7GND/(inverted CART_CS)**1/TXO PORTD3*OLED DC4 PORTD44 PORTD44 PORTD44 PORTD4OLED RST6 PORTD712 PORTD66 PORTD72 PORTD1*SPI SCK15 PORTB115 PORTB115 PORTB 115 PORTB1SPI MOSI16 PORTB216 PORTB216 PORTB216 PORTB2RGB LED RED10 PORTB6_10 PORTB610 PORTB6RGB LED GREEN11 PORTB7_-3 PORTD0*RGB LED BLUE9 PORTB517 PORTB09 PORTB59 PORTB5RxLED17 PORTB0_17 PORTB017 PORTB0TxLED30 PORTD5_30 PORTD530 PORTD5BUTTON UPA0 PORTF78 PORTB4A0 PORTF7A0 PORTF7BUTTON RIGHTA1 PORTF65 PORTC6A1 PORTF6A1 PORTF6BUTTON LEFTA2 PORTF59 PORTB5A2 PORTF5A2 PORTF5BUTTON DOWNA3 PORTF410 PORTB6A3 PORTF4A3 PORTF4BUTTON A (left)7 PORTE6A0 PORTF77 PORTE67 PORTE6BUTTON B (right)8 PORTB4A1 PORTF68 PORTB48 PORTB4SPEAKER PIN 15 PORTC6A2 PORTF55 PORTC65 PORTC6SPEAKER PIN 213 PORTC7A3 PORTF4**GND6 PORTD7*CART_CS (org)0 PORTD2*-0 PORTD2*0 PORTD2*CART_CS (new)2 PORTD1*-2 PORTD1*-SPI MISO14 PORTB3*--14 PORTB3*OLED SDA4 PORTD4*-4 PORTD4*4 PORTD4*OLED SCL6 PORTD7*-6 PORTD7*1/TXO PORTD3*安装完arduboy_homemade之后，把开发板设置为homemade Arduboy,自制Arduboy最简单的就是使用Arduino Pro micro这个板子。在开发板设置里面可以设置为如下图所示。Based on里面有两个关于Pro micro的选项。一个是标准接线，一个是备用接线，分别对应上面表格里面最右边两列，按照表格接线即可。Bootloader选项，选择Cathy3K(start with menu),可以支持外挂Flash芯片，上传游戏合集包。Display选择SSD1306是兼容性最强的，这样设置烧录的Bootloader可以直接使用编译好的游戏和游戏合集包，使用Arduboy-uploader这个工具上传即可。而使用其它的显示屏则只能使用Arduino IDE编译游戏源码上传游戏。我这里测试SH1103 IIC支持不好，画面抖动厉害。Flash select这个选项是专门针对外挂Flash芯片的，就是在PD2（RX1）和PD1（2）中选择一个作为SPIFlash的片选（CS）引脚，不外挂Flash可以不用理它。使用外挂Flash芯片还有一个要注意的是，我们SPI显示屏的SCL和SDA是和Flash芯片共用的，这个时候显示屏的CS引脚接地（始终激活）就行不通了，表现为在游戏菜单选择界面无法正常显示，进入游戏后正常。直接用ATMEGA32U4芯片自制Arduboy的话一般是将显示屏片选引脚接芯片的PD6脚，但是Pro micro这个板子没有将这个引脚引出来。但是根据arduboy_homemade主页说明可以使用三级管和两个电阻组成非门电路，达到在Flash芯片的CS引脚激活时，关闭显示器CS引脚激活状态的目的。整个Arduboy接线如下。效果视频：https://www.bilibili.com/video/BV1Jz4y1A7qz/关于使用Arduino UNO给Pro micro烧录Bootloader接线如下。
淘宝上买的9.9包邮的GMG12864-03A单色LCD屏，ST7567驱动，12864分辨率，SPI接口，显示面积有33.2520.45mm,性价比还不错。但是这个屏u8g2库不支持，有一个U8G2_ST7567_OS12864_1_4W_SW_SPI，可以显示，但是直接使用显示上下颠倒，对比度有问题。好在店家提供了示例代码，按照示例代码把u8g2库改下就可以了。找到libraries\U8g2\src\clib\u8x8_d_st7567.c文件，打开找到屏幕初始化代码。static const uint8_t u8x8_st7567_os12864_init_seq[] = { U8X8_START_TRANSFER(), /* enable chip, delay is part of the transfer start */ U8X8_C(0x0e2), /* soft reset */ U8X8_C(0x0ae), /* display off */ U8X8_C(0x040), /* set display start line to 0 */ U8X8_C(0x0a1), /* ADC set to reverse */ U8X8_C(0x0c0), /* common output mode */ // Flipmode //U8X8_C(0x0a0), /* ADC set to reverse */ //U8X8_C(0x0c8), /* common output mode */ U8X8_C(0x0a6), /* display normal, bit val 0: LCD pixel off. */ U8X8_C(0x0a3), /* LCD bias 1/7 */ /* power on sequence from paxinstruments */ U8X8_C(0x028|4), /* all power control circuits on */ U8X8_DLY(50), U8X8_C(0x028|6), /* all power control circuits on */ U8X8_DLY(50), U8X8_C(0x028|7), /* all power control circuits on */ U8X8_DLY(50), U8X8_C(0x026), /* v0 voltage resistor ratio */ U8X8_CA(0x081, 50>>2), /* set contrast, contrast value*/ U8X8_C(0x0ae), /* display off */ U8X8_C(0x0a5), /* enter powersafe: all pixel on, issue 142 */ U8X8_END_TRANSFER(), /* disable chip */ U8X8_END() /* end of sequence */ };改成如下代码。static const uint8_t u8x8_st7567_os12864_init_seq[] = { U8X8_START_TRANSFER(), /* enable chip, delay is part of the transfer start */ //从这里开始修改 U8X8_C(0x0e2), U8X8_C(0x0a6), U8X8_C(0x0c8), U8X8_C(0x02f), U8X8_C(0x026), U8X8_C(0x0a2), U8X8_C(0x0a1), U8X8_C(0x0f8), U8X8_C(0x000), U8X8_C(0x081), U8X8_C(0x009), U8X8_C(0x0af), //到这里修改结束 U8X8_END_TRANSFER(), /* disable chip */ U8X8_END() /* end of sequence */ };改好库以后打开u8g2库的Hello World示例代码，编辑如下。#include <Arduino.h> #include <U8g2lib.h> #ifdef U8X8_HAVE_HW_SPI #include <SPI.h> #endif #ifdef U8X8_HAVE_HW_I2C #include <Wire.h> #endif //这里用的合宙esp32c3开发板，根据接线修改引脚定义 U8G2_ST7567_OS12864_1_4W_SW_SPI u8g2(U8G2_R0, /* clock=*/ 2, /* data=*/ 3, /* cs=*/ 7, /* dc=*/ 6, /* reset=*/ 11); void setup(void) { u8g2.begin(); u8g2.setContrast(20);//这里设置对比度，自行调整，直至显示效果最佳 } void loop(void) { u8g2.firstPage(); do { u8g2.setFont(u8g2_font_ncenB10_tr); u8g2.drawStr(0,24,"Hello World!"); } while ( u8g2.nextPage() ); }显示效果如下。
esp32c3的SD库在这里下载：https://github.com/nhatuan84/esp32-micro-sdcard显示屏库还是用Arduino_GFX:https://github.com/moononournation/Arduino_GFX/硬件链接如下：esp32c3 SD card ModuleGPIO02---->SCKGPIO03---->MOSIGPIO10---->MISOGPIO06---->CS3.3V------>3V3GND------->GNDesp32c3 1.8'TFTGPIO02---->SCLGPIO03---->SDAGPIO05---->DCGPIO07---->CSGPIO04---->RES3.3V------>VCCGND------->GND显示效果：#include <SPI.h> #include <mySD.h> #include <Arduino_GFX_Library.h> File bmpFile; unsigned short pic[0x5000]; // 128*160图像0x5000像素 byte aByte; // 1字节数据 Arduino_DataBus *bus = new Arduino_ESP32SPI(5 /* DC */, 7 /* CS */, 2 /* SCK */, 3 /* MOSI */); Arduino_GFX *gfx = new Arduino_ST7735(bus, 4 /* RST */, 2 /* 旋转屏幕180度 */, false); void setup() { Serial.begin(115200); //从SD卡读取图片 if (!SD.begin(6, 3, 10, 2)) { Serial.println("initialization SD card failed!"); return; } Serial.println("initialization SD card done."); bmpFile = SD.open("01.bmp", FILE_READ); if (bmpFile) { Serial.println("Reading 01.bmp..."); //显示图片 while (bmpFile.available()) { //跳过前0x36个字节的头 for (int i = 0; i < 0x36; i++) { Serial.print(bmpFile.read(), HEX); Serial.print(" "); } //把rgb24转换成rgb16 for (int i = 0; i < 0x5000; i++) { aByte = bmpFile.read(); pic[i] = (aByte * 32 / 256) << 11; aByte = bmpFile.read(); pic[i] |= (aByte * 64 / 256) << 5; aByte = bmpFile.read(); pic[i] |= (aByte * 32 / 256); } Serial.println("OK"); } bmpFile.close(); } else { Serial.println("error Reading 01.bmp"); } //显示图片 gfx->begin(); gfx->fillScreen(BLACK); gfx->draw16bitRGBBitmap(0, 0, pic, 128, 160); delay(1000); } void loop() { }
TFT屏型号GMT024-8p10p-SPI VER1.1，金逸晨的，2.4寸，分辨率240x320，参数还不错，淘宝价23.5元。试了TFT_eSPI.h，发现无法正常使用，所以又找了一个库，非常不错，地址：https://github.com/moononournation/Arduino_GFX/，能够适配的驱动芯片非常丰富，功能也不错。引脚配置按如下。/* TFT ESP-C3 GND--->GND VCC--->3.3V SCL--->IO02 SDA--->IO03 RESET->IO10 A0---->IO06 CS---->IO07 BL---->IO11 */使用代码如下：#include <Arduino_GFX_Library.h> Arduino_DataBus *bus = new Arduino_ESP32SPI(6 /* DC */,7 /* CS */,2 /* SCK */,3 /* MOSI */); Arduino_GFX *gfx = new Arduino_ST7789(bus,10 /* RST */,0,false,240,320); void setup(void) { gfx->begin(); gfx->fillScreen(BLUE); pinMode(11, OUTPUT);//背光 digitalWrite(11, HIGH); gfx->setCursor(0, 10); gfx->setTextColor(RED,YELLOW); gfx->setTextSize(2); gfx->println("Current milliseconds:"); delay(1000); } void loop() { gfx->setCursor(10, 70); gfx->print(millis()); delay(100); }
ESP32-C3(Arduino)驱动1.8寸128X160 ST7735S TFT显示屏
这个屏是真的便宜，和我手里的ESP32-C3开发板同一家淘宝店，同样9.9元包邮的价格，却有1.8寸的大小，128*160的分辨率，还是彩屏，这就够了。比那种差不多价格0.96寸的OLED单色屏良心多了。就是下面那货。如何来使用它，这个屏用的ST7735S驱动芯片，SPI接口，比较常见。这里介绍Arduino下两个库驱动它。Adafruit_ST7735.h下载地址：https://github.com/adafruit/Adafruit-ST7735-Library，把所有文件下载至librariesAdafruit_ST7735文件夹即可，当然也可以通过Arduino库管理器安装。这个库使用比较简单，引用Adafruit_GFX.h、Adafruit_ST7735.h这两个库文件就可以使用了，简单的代码如下：#include <Adafruit_GFX.h> #include <Adafruit_ST7735.h> #define TFT_SCLK 2 //SCL #define TFT_MOSI 3 //SDA #define TFT_RST 10 //RES #define TFT_DC 6 //DC #define TFT_CS 7 //CS //______TFT_BL 不接 Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_MOSI, TFT_SCLK, TFT_RST); void setup(void) { tft.initR(INITR_BLACKTAB); tft.fillScreen(ST7735_RED); tft.setTextColor(ST7735_YELLOW); tft.setTextSize(2); tft.setCursor(10,30); tft.print("Hello ST7735!"); } void loop(void) { }这个库虽然简单，但是功能也比较单薄，比如没有清屏的方法。TFT_eSPI.h这个库稍微麻烦一点，但是功能丰富。下载：https://github.com/Bodmer/TFT_eSPI，安装好库之后需要修改User_Setup.h配置文件。主要改下面几个地，都是按照这块屏幕设置。... //驱动选择 #define ST7735_DRIVER ... //选择宽高像素值 #define TFT_WIDTH 128 #define TFT_HEIGHT 160 ... #define ST7735_BLACKTAB ... #define TFT_INVERSION_OFF ... #define TFT_BACKLIGHT_ON HIGH ... //定义引脚 #define TFT_MOSI 3 #define TFT_SCLK 2 #define TFT_CS 7 // Chip select control pin #define TFT_DC 6 // Data Command control pin #define TFT_RST 10 // Reset pin (could connect to RST pin) ... //后面一般不用修改 ...下面是Arduino代码实例。#include <TFT_eSPI.h> TFT_eSPI tft =TFT_eSPI(); void setup(void) { tft.init(); tft.setRotation(0); tft.fillScreen(TFT_BLUE); tft.setCursor(20, 10); tft.setTextColor(TFT_WHITE); tft.setTextSize(2); tft.print("Hello ST7735!"); } void loop() { }
零零碎碎隔段时间捣鼓一下，历时几个月，终于把网络时钟制作齐了我想要的功能。外观看图。这个闹钟功能比较全面了，采用了D1 mini开发板（esp8266）、LCD1602显示屏、DS1302时钟芯片、DHT11温湿度模块、无源蜂鸣器等配件，Arduino开发，实现的功能列表如下。显示日期、星期、时间和温湿度，外带一个Wifi小图标。可以联网自动对时。断电不影响走时（DS1302）。可设置一路闹钟，通过web页面设置，响铃为两只老虎旋律。可web页面配置wifi，没有设置wifi时自动开启wifi热点，以供手机链接时钟进行配置。两个按钮，一个重启模块，一个清楚WiFi配置信息。下面是进行WiFi设置和闹钟设置的两个网页。程序代码如下：/* ---By www.yizu.org--- Connect D1 mini I2C_LCD 5V VCC GND GND D2 SDA D1 SCL D1 mini DS1302 3V3 VCC GND GND D6 CLK D7 DAT D8 RST */ #include <ESP8266WiFi.h> #include <EEPROM.h> #include <WiFiUdp.h> #include <NTPClient.h> #include <LiquidCrystal_I2C.h> #include <TimeLib.h> #include <Arduino.h> #include <DS1302.h> #include <dht11.h> #include <ESP8266WebServer.h> #include <FS.h> LiquidCrystal_I2C lcd(0x27, 16, 2); int32_t t_xd=0; //定义web服务 ESP8266WebServer server(80); //设置NTPClient #define NTP_OFFSET 60 * 60 * 8 // 时区偏移量 #define NTP_ADDRESS "ntp1.aliyun.com" // NTP服务器 WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP, NTP_ADDRESS, NTP_OFFSET); //设置时钟模块 DS1302 rtc(12,13,15); //设置校对时间间隔（单位秒） #define CALIBRATION_INTERVAL 60 unsigned long t_stamp=0; unsigned long t_stamp2=0; //设置蜂鸣器 #define BUZZER_PIN 14 //设置D5脚 #define ALARM_INTERVAL 300 //一个节拍的时间/毫秒 unsigned long b_stamp = 0; unsigned long b_stamp2 = 0; unsigned long duration = 0;//闹铃持续时间 boolean isAlarm=0;//蜂鸣器发声标记 uint8_t tonesTag=0; uint8_t tonesLength=33;//长度 //两只老虎音乐 char toneNotes[] = "cdeccdecefgefggagfecgagfecdgcdgc "; //'c', 'd', 'e', 'f', 'g', 'a', 'b', 'C' float beats[] = { 1,1,1,1,1,1,1,1,1,1,2,1,1,2,0.5,0.5,0.5,0.5,1,1,0.5,0.5,0.5,0.5,1,1,1,1,2,1,1,2,4 }; bool ringing=false; //闹钟设置 bool alarm_on=false; int alarm_hour=0; int alarm_min=0; //设置AP账号密码 const char* ssid = "myAP000"; const char* password = "12345678"; //设置温湿度传感器 #define DHT11PIN 2//定义dht11引脚 dht11 myDht; int tem=0;//温度 int hum=0;//湿度 //显示字符 #if defined(ARDUINO) && ARDUINO >= 100 #define printByte(args) write(args); #else #define printByte(args) print(args,BYTE); #endif //要显示的特殊字符 //uint8_t nian[8] = {0x08,0x0f,0x12,0x0f,0x0a,0x1f,0x02,0x02,};//年 //uint8_t yue[8] = {0x0f,0x09,0x0f,0x09,0x0f,0x09,0x0b,0x11,};//月 //uint8_t ri[8] = {0x1F,0x11,0x11,0x1F,0x11,0x11,0x1F,0x00,};//日 uint8_t dian[8] = {0x02,0x05,0x02,0x00,0x00,0x00,0x00,0x00,};//点 uint8_t dianC[8] ={0x00,0x17,0x08,0x08,0x08,0x08,0x07,0x00,};//温度单位 uint8_t wificon[8] ={0x00,0x0E,0x11,0x04,0x0A,0x00,0x04,0x00,};//WIFI链接 uint8_t wificon2[8]={0x00,0x11,0x0A,0x04,0x0A,0x11,0x00,0x00,};//断网 const char * days[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"} ; //连接WIFI void connectToWiFi() { //读取eeprom EEPROM.begin(68); //读取闹铃设置 if (EEPROM.read(66) == 1){ alarm_on = true; } else{ alarm_on = false; } alarm_hour = EEPROM.read(64); alarm_min = EEPROM.read(65); //读取闹铃设置结束 byte len=EEPROM.read(0); if(len>0&&len<30){ char ssid2[len+1]; for(int i=0;i<len;i++){ ssid2[i]=EEPROM.read(i+1); } ssid2[len]='\0'; Serial.printf("ssid:%s\n",ssid2); byte len2=EEPROM.read(32); if(len2>0&&len2<30){ char passwd2[len2+1]; for (int i = 0; i < len2; i++){ passwd2[i] = EEPROM.read(i + 32+1); } passwd2[len2]='\0'; EEPROM.end(); Serial.printf("password:%s\n",passwd2); //Serial.printf("password Lenth:%d\n",len2); WiFi.mode(WIFI_STA); WiFi.begin(ssid2,passwd2); Serial.println(""); Serial.print("Connected to WiFi at "); Serial.print(WiFi.localIP()); Serial.println(""); unsigned int count = 0; while ((count < 10) & (WiFi.status() != WL_CONNECTED)){ delay(500); Serial.print("."); count++; } }else{ WiFi.mode(WIFI_STA); WiFi.begin(ssid2); Serial.println(""); Serial.print("Connected to WiFi at "); Serial.print(WiFi.localIP()); Serial.println(""); unsigned int count = 0; while ((count < 10) & (WiFi.status() != WL_CONNECTED)){ delay(500); Serial.print("."); count++; } } }else{ WiFi.mode(WIFI_AP); WiFi.softAP(ssid, password); Serial.println(""); Serial.print("AP IP address:"); Serial.println(WiFi.softAPIP()); } } //显示WIFI连接状态 void showWifiStatus() { lcd.setCursor(11, 0); if(WiFi.status() == WL_CONNECTED) { lcd.write(byte(5)); } else { lcd.write(byte(6)); } } //校对时间 void correctionTime() { time_t tnow=now(); if(t_stamp==0||tnow-t_stamp>=5)//每10秒校对系统时钟 { Serial.println(""); Serial.print(rtc.getTimeStr()); Serial.print(" Correction the time."); if(WiFi.status() == WL_CONNECTED&&(t_stamp2==0||tnow-t_stamp2>=CALIBRATION_INTERVAL)) { if(timeClient.update()) { unsigned long t=timeClient.getEpochTime(); //设置系统时间 setTime(t); Serial.println(""); Serial.print("SysTime=NTP:"); Serial.printf("%02d:%02d:%02d",hour(t), minute(t), second(t)); //设置RTC时钟 if(rtc.peek(0)==88) { rtc.writeProtect(false); rtc.setDOW(weekday(t)+1); rtc.setTime(hour(t), minute(t), second(t)); rtc.setDate(day(t), month(t), year(t)); rtc.writeProtect(true); } } else { if(rtc.peek(0)==88) { //设置系统时间 Time t=rtc.getTime(); setTime(t.hour,t.min,t.sec,t.date,t.mon,t.year); Serial.println(""); Serial.print("SysTime=RTC:"); Serial.printf("%02d:%02d:%02d",t.hour,t.min,t.sec); } } t_stamp2=now(); } else { if(rtc.peek(0)==88) { //设置系统时间 Time t=rtc.getTime(); setTime(t.hour,t.min,t.sec,t.date,t.mon,t.year); Serial.println(""); Serial.print("SysTime=RTC:"); Serial.printf("%02d:%02d:%02d",t.hour,t.min,t.sec); } } t_stamp=now(); tempAndHun();//读取温度 } } void tempAndHun() { //-------------------------- Serial.println("\n"); int chk = myDht.read(DHT11PIN); Serial.print("Read sensor: "); switch (chk) { case DHTLIB_OK: Serial.println("OK"); break; case DHTLIB_ERROR_CHECKSUM: Serial.println("Checksum error"); break; case DHTLIB_ERROR_TIMEOUT: Serial.println("Time out error"); break; default: Serial.println("Unknown error"); break; } tem=myDht.temperature; hum=myDht.humidity; Serial.print("Humidity (%): "); Serial.println(hum); Serial.print("Temperature (oC): "); Serial.println(tem); } //音频频率 unsigned int toneFrequency(char note) { char names[] = { 'c', 'd', 'e', 'f', 'g', 'a', 'b', 'C' }; int tones[] = { 261, 294, 329, 349, 392, 440, 493, 523 }; for (int i = 0; i < 8; i++) { if (names[i] == note) { return tones[i]; } } } //驱动蜂鸣器 void alarm() { unsigned long currentMillis =millis(); if(b_stamp2==0) { b_stamp2=currentMillis; } if(currentMillis-b_stamp2<duration) { if(!isAlarm) { if(b_stamp==0) { tone(BUZZER_PIN,toneFrequency(toneNotes[tonesTag])); isAlarm=true; b_stamp=currentMillis; } else if(currentMillis-b_stamp>0.2*ALARM_INTERVAL) { if(toneNotes[tonesTag]!=' ') { tone(BUZZER_PIN,toneFrequency(toneNotes[tonesTag])); } isAlarm=true; b_stamp=currentMillis; } } else { if(currentMillis-b_stamp>beats[tonesTag]*ALARM_INTERVAL) { noTone(BUZZER_PIN); isAlarm=false; b_stamp=currentMillis; if(tonesTag<tonesLength-1) { tonesTag++; } else { tonesTag=0; } } } } else { b_stamp2=0; duration=0; noTone(BUZZER_PIN); } } void handleRouter() { String path = server.uri(); if(path=="/"){ path="/index.html"; } if (SPIFFS.exists(path)){ File file=SPIFFS.open(path,"r"); String contentType="text/html"; if(path.substring(path.lastIndexOf('.'))==".css"){ contentType="text/css"; } server.streamFile(file,contentType); file.close(); } else{ server.send(404, "text/plain", "File Not Found!\n\n"); } } void jsonRouter() { if(server.hasArg("nz")){ EEPROM.begin(68); // 4的倍数 String json = "{\"alarm\":\""; json+=EEPROM.read(66); json+="\",\"hour\":\""; json+=EEPROM.read(64); json+="\",\"min\":\""; json+=EEPROM.read(65); json+="\"}"; EEPROM.end(); server.send(200, "text/plain",json); } else{ server.send(404, "text/plain", "File Not Found!\n\n"); } } void postRouter() { EEPROM.begin(68);//4的倍数 if(server.hasArg("ssid")){ String ssid2 = server.arg("ssid"); Serial.printf("Write ssid:%s\n",ssid2.c_str()); EEPROM.write(0,ssid2.length()); for(int i=0;i<ssid2.length();i++){ EEPROM.write(i+1,ssid2[i]); } } if(server.hasArg("passwd")){ String passwd2 = server.arg("passwd"); passwd2.trim(); Serial.printf("Write password:%s\n",passwd2.c_str()); if(passwd2.length()>0){ EEPROM.write(32, passwd2.length()); for (int i = 0; i < passwd2.length(); i++) { EEPROM.write(i + 32 + 1, passwd2[i]); } }else{ EEPROM.write(32,0); } } //post?alarm=on&hour=03&min=02 if(server.hasArg("hour")){ duration==0; String h=server.arg("hour"); EEPROM.write(64,byte(h.toInt())); if (server.hasArg("min")){ String m = server.arg("min"); EEPROM.write(65, byte(m.toInt())); } if (server.hasArg("alarm")){ EEPROM.write(66, 1); } else{ EEPROM.write(66, 0); } } EEPROM.end(); server.send(200, "text/plain", "设置成功，即将重启时钟！\n\n"); delay(200); ESP.restart(); } void setup() { Serial.begin(115200); lcd.init();// 初始化lcd lcd.backlight(); lcd.setCursor(0, 0); lcd.print("Starting..."); lcd.setCursor(0, 1); lcd.print("Please wait."); //联网 connectToWiFi(); //初始化NTPClient timeClient.begin(); //开启时钟 if(rtc.peek(0)!=88) { lcd.clear(); lcd.setCursor(0, 0); lcd.print("System reseted!"); rtc.writeProtect(false); rtc.halt(false); rtc.poke(0,88);//写入启动标记 rtc.writeProtect(true); delay(1000); } //对时 correctionTime(); //读取温湿度 tempAndHun(); lcd.clear(); pinMode(BUZZER_PIN,OUTPUT); //开启web服务 SPIFFS.begin(); server.on("/",handleRouter); server.on("/post",postRouter); server.on("/json",jsonRouter); server.onNotFound(handleRouter); server.begin(); Serial.println("HTTP server started"); pinMode(16,INPUT); } void loop() { correctionTime(); // 打印到屏幕 //lcd.createChar(1, nian); //lcd.createChar(2, yue); //lcd.createChar(3, ri); lcd.createChar(4, dianC); lcd.createChar(5, wificon); lcd.createChar(6, wificon2); time_t t=now(); lcd.setCursor(0, 0); //lcd.printf("%04d",year(t)); //lcd.write(byte(1)); //lcd.printf("%02d",month(t)); //lcd.write(byte(2)); //lcd.printf("%02d",day(t)); //lcd.write(byte(3)); lcd.printf("%04d-%02d-%02d",year(t),month(t),day(t)); lcd.setCursor(9, 1); lcd.print(days[weekday(t)-1]); lcd.setCursor(0, 1); lcd.printf("%02d:%02d:%02d",hour(t),minute(t),second(t)); lcd.setCursor(13, 0); lcd.print(tem);//显示温度 lcd.write(byte(4)); lcd.setCursor(13, 1); lcd.print(hum);//显示湿度 lcd.print("%"); showWifiStatus(); server.handleClient(); //长按下按钮2，清除wifi设置 if(digitalRead(16)==LOW){ if(t_xd==0){ t_xd=millis(); duration=0; }else{ if(millis()-t_xd>2000){ EEPROM.begin(64); EEPROM.write(0, 0); EEPROM.end(); Serial.println("EEPROM is clear."); delay(200); ESP.reset(); } } }else{ if(t_xd!=0) t_xd=0; } //闹钟 if(hour(t)==alarm_hour&&minute(t)==alarm_min){ if(duration==0&&!ringing&&alarm_on){ duration = 50000;//设置持续时间 ringing=true; } } else{ ringing=false; } alarm(); }零敲碎打的，一点一点加上的功能，所以代码比较乱，但用也是一个比较完整的Arduino开发实例，用到的知识点还是比较多的，用来学习不错。完整代码打包下载。
本文在《合宙ESP32-C3开发板Arduino操作SPIFFS存储》基础上继续进行，关于合宙的这块板子介绍点这里。首先把index.html和style.css上传至EPS32-C3开发板，文件如下。index.html 设置WIFI 设置WIFI &copy 2022 yizu.org style.csshtml,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,dl,dt,dd,ol,nav ul,nav li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline;} article, aside, details, figcaption, figure,footer, header, hgroup, menu, nav, section {display: block;} ol,ul{list-style:none;margin:0px;padding:0px;} blockquote,q{quotes:none;} blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} table{border-collapse:collapse;border-spacing:0;} a{text-decoration:none;} .txt-rt{text-align:right;} .txt-lt{text-align:left;} .txt-center{text-align:center;} .float-rt{float:right;} .float-lt{float:left;} .clear{clear:both;} .pos-relative{position:relative;} .pos-absolute{position:absolute;} .vertical-base{ vertical-align:baseline;} .vertical-top{ vertical-align:top;} nav.vertical ul li{ display:block;} nav.horizontal ul li{ display: inline-block;} body{ font-family: 'Cabin', sans-serif !important; background:#d5d5d5 0px 0px; } .main{ width: 35%; margin: 8em auto 0; padding: 0em 4em 5em; } .main h1{ color: #7e8fad; text-align: center; font-size: 2.5em; margin: 0 0 1.5em; } .main input[type="text"],.main input[type="password"]{ outline: none; border: 1px solid rgb(254, 2, 2); background: #ffffff; border-radius: 5px; -webkit-border-radius: 5px; -moz-border-radius: 5px; -o-border-radius: 5px; -ms-border-radius: 5px; padding: 15px 10px; width: 96%; font-size: 1.3em; color: #7e8fad; text-align: center; font-family: 'Syncopate', sans-serif; } .main input[type="password"]{ margin:1.5em 0; } .main input[type="submit"]{ outline:none; border:1px solid #DBA9C8; background:#b47c9f; border-radius:5px; -webkit-border-radius:5px; -moz-border-radius:5px; -o-border-radius:5px; -ms-border-radius:5px; padding:15px 10px; width:100%; color:#fdbfe6; font-size:1.5em; font-family: 'Syncopate', sans-serif; cursor:pointer; } .main input[type="submit"]:hover{ background:#fed2ed; color:#b47c9f; border: 1px solid #FFF; } .footer{ text-align:center; padding:2em 0 0; } .footer p{ font-size:1em; color:#B47C9F; margin:0; } .footer p a{ color:#fff; } .footer p a:hover{ color:#B47C9F; } @media (max-width:1440px){ .main { width: 37%; } .main input[type="text"], .main input[type="password"] { width: 95.8%; } } @media (max-width: 1280px){ .main { width: 40%; } .main input[type="text"], .main input[type="password"] { width: 95.6%; } } @media (max-width: 1024px){ .main { width: 50%; margin: 5em auto 0; } } @media (max-width:768px){ .main { width: 60%; } .main input[type="text"], .main input[type="password"] { width: 95.2%; font-size: 1.1em; } } @media (max-width:736px){ .main { padding: 0em 4em 2em; } .footer { padding: 2em 0; } } @media (max-width:667px){ .main { width: 65%; margin: 3em auto 0; } .main h1 { font-size: 2.3em; margin: 0 0 1.2em; } .main input[type="text"], .main input[type="password"] { width: 94.5%; } } @media (max-width:640px){ .main input[type="submit"] { font-size: 1.3em; } } @media (max-width: 600px){ .main input[type="text"], .main input[type="password"] { width: 94.2%; } } @media (max-width:480px){ .main h1 { font-size: 2em; margin: 0 0 1em; } .main { padding: 0em 0em 2em; width: 85%; } } @media (max-width: 414px){ .main input[type="text"], .main input[type="password"] { width: 93.5%; } .footer p { width: 80%; margin: 0 auto; line-height: 1.8em; } } @media (max-width:320px){ .main { padding: 0; width: 90%; margin: 2em auto 0; } .main h1 { font-size: 1.7em; } .main input[type="text"], .main input[type="password"] { padding: 10px; font-size: 13px; width: 92.5%; } .main input[type="submit"] { font-size: 1em; padding: 10px; } .footer p { width: 90%; font-size: 15px; } }把以下代码编译上传开发板。#include <WiFi.h> #include <WiFiClient.h> #include <WebServer.h> #include <SPIFFS.h> #include <EEPROM.h> #include <TimeLib.h> const char* ssid = "myAP"; const char* password = "123456789"; WebServer server(80); const int led = 13; time_t resetTime=0; void handleRouter() { digitalWrite(led, 1); String path = server.uri(); if(path=="/"){ path="/index.html"; } if (SPIFFS.exists(path)){ File file=SPIFFS.open(path,"r"); String contentType="text/html"; if(path.substring(path.lastIndexOf('.'))==".css"){ contentType="text/css"; } server.streamFile(file,contentType); file.close(); } else{ server.send(404, "text/plain", "File Not Found!\n\n"); } digitalWrite(led, 0); } void postRouter() { digitalWrite(led, 1); EEPROM.begin(64);//4的倍数 if(server.hasArg("ssid")){ String ssid2 = server.arg("ssid"); Serial.printf("Write ssid:%s\n",ssid2); EEPROM.write(0,ssid2.length()); for(int i=0;i<ssid2.length();i++){ EEPROM.write(i+1,ssid2[i]); } } if(server.hasArg("passwd")){ String passwd2 = server.arg("passwd"); passwd2.trim(); Serial.printf("Write password:%s\n",passwd2); if(passwd2.length()>0){ EEPROM.write(32, passwd2.length()); for (int i = 0; i < passwd2.length(); i++) { EEPROM.write(i + 32 + 1, passwd2[i]); } }else{ EEPROM.write(32,0); } EEPROM.end(); } server.send(200, "text/plain", "设置WIFI成功，请按RST键重启WIFI！\n\n"); digitalWrite(led, 0); } void setup(void) { pinMode(led, OUTPUT); digitalWrite(led, 0); Serial.begin(115200); //开始操作EEPROM EEPROM.begin(64);//4的倍数 byte len=EEPROM.read(0); if(len>0&&len<30){ char ssid2[len+1]; for(int i=0;i<len;i++){ ssid2[i]=EEPROM.read(i+1); } ssid2[len]='\0'; Serial.printf("ssid:%s\n",ssid2); //Serial.printf("ssid Lenth:%d\n",len); byte len2=EEPROM.read(32); if(len2>0&&len2<30){ char passwd2[len2+1]; for (int i = 0; i < len2; i++){ passwd2[i] = EEPROM.read(i + 32+1); } passwd2[len2]='\0'; EEPROM.end(); Serial.printf("password:%s\n",passwd2); //Serial.printf("password Lenth:%d\n",len2); WiFi.begin(ssid2,passwd2); }else{ WiFi.begin(ssid2); } }else{ WiFi.softAP(ssid, password); Serial.println(""); Serial.print("AP IP address:"); Serial.println(WiFi.softAPIP()); } //开启web服务 SPIFFS.begin(); server.on("/",handleRouter); server.on("/post",postRouter); server.onNotFound(handleRouter); server.begin(); Serial.println("HTTP server started"); } void loop(void) { server.handleClient(); delay(2); //按boot键三秒,清除Wifi信息 if(digitalRead(9)==LOW&&resetTime==0){ resetTime=now(); digitalWrite(led, 1); } if(digitalRead(9)==LOW&&resetTime>0&&now()-resetTime>3){ resetTime=NULL; EEPROM.begin(64); EEPROM.write(0,0); EEPROM.end(); Serial.println("EEPROM is clear."); digitalWrite(led, 0); } }接通电源后，长按boot按键3秒以上，会清楚原来的WIFI配置信息，再重启板子，就会开启一个热点myAP，手机或者电脑连上这个热点，访问192.168.4.1，就可以进行WIFI设置，设置完再重启，就会自动连接刚刚设置的WIFI。
合宙的ESP32-C3开发板应该是全网最低价格的esp32开发板了吧，9.9包邮真是良心价，用来Arduino开发都是可以的。这篇文章主要讲明从零开始怎么把SPIFFS用起来，开发环境基于Windows。安装ESP32-C3的开发环境这个参照https://www.arduino.cn/thread-81194-1-1.html 这个帖子就行，其他方法都因为网络问题可能进行不下去。合宙这块板子为了增加可用io，牺牲了下载速度，利用2线SPI的方式操作flash。用Arduino开发时，需要把开发板设置中的FLASH MODE设置为DIO模式才能正常下载程序。其他与其他开发板基本没什么区别。向SPIFFS存储上传文件可以编写代码向SPIFFS存储写入文件，但是在有些场景没必要，也很麻烦。arduino上有插件可以干这个事，ESP32 Sketch Data Upload、ESP8266 Sketch Data Upload，这两个一个对应esp32,一个对应esp8266，虽然很方便，但是都不支持esp32-c3。这里介绍两个工具可以达成这一目的。一个是spiffsgen.py用来将上传的文件打包。一个是esptool,用来将打包的文件上传至esp32-c3。安装spiffsgen.py下载：https://github.com/espressif/esp-idf/blob/v4.4/components/spiffs/spiffsgen.py 一个文件，电脑有python环境就可以使用。安装esptool命令行执行以下命令：python -m pip install --upgrade pip pip install esptool pip install pyserial上传文件将要上传的文件存在data文件夹下，与spiffsgen.py放一起，命令行执行以下命令：python spiffsgen.py 0x170000 data aaa.bin esptool.py --chip esp32c3 --port COM3 --baud 115200 write_flash -z 0x290000 aaa.binaaa.bin是生成的文件，0x170000是生成文件大小，要与SPIFFS空间大小一致，0x290000是地址偏移量，与分区情况相关。这个可以在SDK中找到分区文件，我的是在C:UsersyournameAppDataLocalArduino15packagesesp32hardwareesp322.0.2toolspartitions ,里面有CSV文件，就是各种分区方案，找到与您设置项Partition Scheme对应的分区文件，打开里面可以找到SPIFFS大小和地址偏移。上传好了以后就可以在程序里使用了。我上传了两个文件index.html、style.css。开始在程序里面调用上传的文件#include <WiFi.h> #include <WiFiClient.h> #include <WebServer.h> #include <SPIFFS.h> const char* ssid = "myAP"; const char* password = "123456789"; WebServer server(80); const int led = 13; void handleRouter() { digitalWrite(led, 1); String path = server.uri(); if(path=="/"){ path="/index.html"; } if (SPIFFS.exists(path)){ File file=SPIFFS.open(path,"r"); String contentType="text/html"; if(path.substring(path.lastIndexOf('.'))==".css"){ contentType="text/css"; } server.streamFile(file,contentType); file.close(); } else{ server.send(404, "text/plain", "File Not Found!\n\n"); } digitalWrite(led, 0); } void setup(void) { pinMode(led, OUTPUT); digitalWrite(led, 0); Serial.begin(115200); WiFi.softAP(ssid, password); Serial.println(""); Serial.print("AP IP address:"); Serial.println(WiFi.softAPIP()); SPIFFS.begin(); server.on("/",handleRouter); server.onNotFound(handleRouter); server.begin(); Serial.println("HTTP server started"); } void loop(void) { server.handleClient(); delay(2); }esp32-c3将生成一个热点myap,设备连接这个热点，浏览器打开设备IP，就可以打开index.html页面了。