2023年7月

前言

此篇文章为有关 ESP32 的学习期间的代码记录,并且加上了自己的注释,非教学文章。

使用开发板全称ESP32 DEVKILTv1(devkitv1) ,搭载芯片为 ESP32D0WDQ6,使用软件为 Arduino

 

 

参考链接

如果是小白并且想要学习单片机相关知识,建议移步此篇文章:51单片机入门教程(上篇)(代码+个人理解) – Echo (liveout.cn)

此篇文章参考教程视频:小鱼创意的个人空间哔哩哔哩bilibili

GitHub代码样例链接:https://github.com/PGwind/ESP32code

开发板详细讲解:ESP32 DEVKILTv1(devkitv1)开发板全解析

 

1. 点亮LED

1.1 点亮第一个LED

int ledPin = 2; //定义引脚,一般为板载蓝色灯

void setup() {
  // put your setup code here, to run once:
  pinMode(ledPin,OUTPUT); //输出模式
}

void loop() {
  // put your main code here, to run repeatedly:
  digitalWrite(ledPin, HIGH); //引脚高电平,即等效于 digitalWrite(ledPin, 1);
}

1.2 LED闪烁

int ledPin = 2;

void setup() {
  pinMode(ledPin,OUTPUT);

}

void loop() {
  digitalWrite(ledPin, HIGH);
  delay(2000); //延迟
  digitalWrite(ledPin, LOW);
  delay(2000);
}

1.3 不同闪烁周期LED

int ledPin2 = 2; 
int ledStatus2 = 0;  //现在的状态
unsigned int prevTime2 = 0; //改变状态时的时间

int ledPin4 = 4;
int ledStatus4 = 0;  
unsigned int prevTime4 = 0;

void setup() {
  pinMode(ledPin2, OUTPUT);
  digitalWrite(ledPin2, HIGH);
  ledStatus2 = HIGH;
  prevTime2 = millis(); //millis(): 本程序已经运行的时间(ms) micros()微秒us

  pinMode(ledPin4, OUTPUT);
  digitalWrite(ledPin4, HIGH);
  ledStatus4 = HIGH;
  prevTime4 = millis(); //millis(): 本程序已经运行的时间(ms) micros()微秒us
}

void loop() {
  unsigned int now = millis(); //程序运行的时间

  if (now - prevTime2 > 3000) //上次改变状态后已经过了3s
  {
    int status  = ledStatus2 == HIGH ? LOW: HIGH;
    digitalWrite(ledPin2, status);
    ledStatus2 = status;
    prevTime2 = now;
  }

  if (now - prevTime4 > 1000) //上次改变状态后已经过了1s
  {
    int status  = ledStatus4 == HIGH ? LOW: HIGH;
    digitalWrite(ledPin4, status);
    ledStatus4 = status;
    prevTime4 = now;
  }
}

 

2. 按键

2.1 按键控制LED

int switchPin = 25; //按键所接GPIO口
int ledPin = 4; //LED接口
int ledStatus = 0; //LED目前状态

void setup() {
  pinMode(switchPin, INPUT_PULLUP);//INPUT_PULLUP上拉,低电平有效,检测到低电平表明按键已经按下
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, HIGH);
  ledStatus = HIGH;
}

void loop() {
  int val = digitalRead(switchPin); //读取开关引脚的电平状态
  if (val == LOW) //低电平有效
  {
    ledStatus = !ledStatus;
    digitalWrite(ledPin, ledStatus);
  }
}

2.2 软件消除抖

使用 RBD_ Button 库进行消抖,在 库管理 处进行安装

#include <RBD_Timer.h>
#include <RBD_Button.h>

int switchPin = 25;
int ledPin = 4;
int ledStatus = 0;

//创建一个可以消除拉动的按键对象
RBD::Button button(switchPin, INPUT_PULLUP);

void setup() {
  pinMode(ledPin, OUTPUT);
  button.setDebounceTimeout(20); // 消除抖动时间是20ms
}

void loop() {
  //检测按键按下去的事件(下降沿)
  if (button.onPressed()) //按键已经按下
  {
    ledStatus = !ledStatus;
    digitalWrite(ledPin, ledStatus);
  }
}

 

3. PWM

LED控制(LEDC)外围设备主要用于控制LED的强度,尽管它也可以用于生成PWM信号用于其他目的。它具有16个通道,可以生成独立的波形,这些波形可以用于驱动RGB LED器件。

3.1 LEDC(PWM)

void setup() {
  int ret = 0; //状态
  Serial.begin(115200);
  int ch0 = 0; //通道
  int gpio4 = 4; //引脚
  ret = ledcSetup(ch0, 5000, 12); //设置ledc通道0,频率5000HZ,精度12 

  delay(200);
  if (ret == 0)
    Serial.println("Error Setup");
  else 
    Serial.println("Success Setup");
    
  ledcAttachPin(gpio4, ch0); //设置引脚和通道
  ledcWrite(ch0, pow(2, 11)); //占空比50%   2^11 / 2^12 = 1/2 

}

void loop() {
  // put your main code here, to run repeatedly:

}

3.2 LED呼吸灯

每秒钟固定调整占空比 50 次。 T 为呼吸周期,光从灭到最亮经过半个周期T/2。

半个周期进行 50*T/2 调整占空比

count 表示占空比为 100% 时等分的格子

step 为每次调整时要加上的增量 step = count / (50 * T/2) = 2 * count / (50 * T)

3.2.1 使用 delay() ,呼吸周期偏长

/* 每秒钟固定调整占空比50次。T为呼吸周期,光从灭到最亮经过半个周期T/2。
   半个周期进行 50*T/2 调整占空比
   count表示占空比为100%时等分的格子
   step为每次调整时要加上的增量  step = count / (50 * T/2) = 2 * count / (50 * T)
*/

int gpio4 = 4;
int ch1 = 1; //ledc通道号
int duty = 0; //目前信号的占空比
int count = 0; //100%占空比时的格子
int step = 0; //占空比步进值(增量)
int breathTime = 3; //呼周期,单位s

void setup() {
  ledcSetup(ch1, 1000, 12); //建立ledc通道
  count = pow(2, 12); //计算占空比为100%时共几份
  step = 2 * count / (50 * breathTime); //计算一次增加多少格子
  ledcAttachPin(gpio4, ch1); //绑定 ch1 和 GPIO4
}

void loop() {
  ledcWrite(ch1, duty);
  duty += step;
  if (duty > count)
  {
    duty = count;
    step = -step; //step变为负数,duty递减
  }
  else if (duty < 0)
  {
    duty = 0;
    step = -step; //step变为正数,duty递增
  }
  delay(20); //阻塞20ms
}

3.2.2 使用 millis

int prevTime = 0;
int gpio4 = 4;
int ch1 = 1; //ledc通道号
int duty = 0; //目前信号的占空比
int count = 0; //100%占空比时的格子
int step = 0; //占空比步进值(增量)
int breathTime = 3; //呼周期,单位s

void setup() {
  ledcSetup(ch1, 1000, 12); //建立ledc通道
  count = pow(2, 12); //计算占空比为100%时共几份
  step = 2 * count / (50 * breathTime); //计算一次增加多少格子
  ledcAttachPin(gpio4, ch1); //绑定 ch1 和 GPIO4
}

void loop() {
  int now =  millis(); 
  if (now - prevTime >= 20) //上次改变状态后已经过了 20ms
  {
    ledcWrite(ch1, duty);
    duty += step;
    if (duty > count)
    {
      duty = count;
      step = -step;
    } 
    else if (duty < 0)
    {
      duty = 0;
      step = -step;
    }
    prevTime = now;
  }
}

 

4. 软件定时器

使用 AsyncTimer 库进行定时操作,在 库管理 处进行安装。

定时器主要模式:

  1. 等待多长时间触发一个事件
  2. 每个多久时间触发一个事件
  3. 到某个时间点触发一个事件

定时器类型:

  1. 硬件定时器:ESP32只有4个
  2. 软件定时器:精度低,数量多

4.1 单次定时任务

串口定时打印信息

#include <AsyncTimer.h>

AsyncTimer t; //定义一个定时器

void myfun()
{
  Serial.println("the second");
}
void setup() {
  Serial.begin(115200);
  delay(200);
  
  //setTimeout(回调函数, 超时时间(ms)),回调函数可以无参无返回值
  auto id = t.setTimeout([](){ //第一个单次定时任务:2s 打印 the first
    Serial.println("the first");
  }, 2000);
  Serial.print("First:");
  Serial.println(id);

  id = t.setTimeout(myfun, 4000); //第二个单次定时任务:4s 打印 the second
  Serial.print("Second:");
  Serial.println(id);
}

void loop() {
  t.handle(); //执行有关定时器的操作,精度与loop()函数里面操作时间有关
}
First:62510
Second:36048
the first
the second

4.2 周期定时任务

#include <AsyncTimer.h>

AsyncTimer t;

void myfun()
{
  Serial.println("the second");
}

void setup() {
  Serial.begin(115200);
  delay(200);
  
  //setInterval(回调函数, 超时时间(ms)),回调函数可以无参无返回值
  auto id = t.setInterval([](){ //第一个周期定时任务:每 2s 打印 the first
    Serial.println("the first");
  }, 2000);
  Serial.print("First:");
  Serial.println(id);

  id = t.setInterval(myfun, 4000); //第二个周期定时任务:每 4s 打印 the second
  Serial.print("Second:");
  Serial.println(id);
}

void loop() {
  t.handle();
}
the first
the first
the second

4.3 闪烁LED改造

LED灯刚启动以1s周期进行闪烁,按键按下去后在1s和3s的周期进行切换

// LED灯刚启动以1s周期进行闪烁,按键按下去后在1s和3s的周期进行切换
#include <RBD_Button.h>
#include <AsyncTimer.h>

int switchPin = 25; // 按钮
int ledPin = 4; // led
int ledStatus = HIGH;
int t = 1; // 闪烁周期

// 软件消抖
RBD::Button button(switchPin, INPUT_PULLUP);

AsyncTimer timer;
int taskId = 0;

void ChangeLedStatus() // 改变LED状态函数
{
  ledStatus = !ledStatus; // 状态取反
  digitalWrite(ledPin, ledStatus); // 改变状态
}

void setup() {
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, HIGH); // 点亮
  button.setDebounceTimeout(20);
  // 创建周期任务
  taskId = timer.setInterval(ChangeLedStatus, t*1000);
}

void loop() {
  timer.handle();

  if (button.onPressed())
  {
    t = t == 1?3:1; // 周期定时时间为:1s或3s
    timer.changeDelay(taskId, t*1000);
  }
}

4.4 相关函数讲解

  1. 停止单个定时任务:

    cancel(intervalOrTimeoutId) ,intervalOrTimeoutid即定时任务的编号

  2. 停止多个定时任务:

    cancelAll(includeIntervals) ,参数默认值为true。

    true:取消所有定时任务 fasle:只取消单次定时任务

  3. 改变定时任务周期:

    changeDelay(intervalOrTimeoutId, delaylnMs) ,参数分别为定时任务的编号 和 新的超时时间(ms)

  4. 重置定时任务:

    reset(intervalOrTimeoutId),只能重置还没有停止的定时任务,重置完从0重新计时

  5. 额外延时一个定时任务:

    delay(intervalOrTimeoutId, delaylnMs) ,参数分别为定时任务的编号 和 额外延时时间(ms)

  6. 获取定时任务剩余时间:

    getRemaining(intervalOrTimeoutId) ,获取指定定时任务本轮还剩多久时间超时

    unsigned long remaining = getRemaining(timeoutId);
    

     

5. ADC模数转换

5.1 样例

void setup() {
  Serial.begin(115200);
  analogReadsolution(12); // 设置读取精度(位宽)

  //设置通道衰减值(不设置默认为11db)
  /*
  analogSetAttenuation(ADC_ATTEN_DB_11); // 设置所有通道
  analogSetPinAttenuation(2, ADC_ATTEN_DB_11); // 设置指定GPIO口的衰减值
  */
}

void loop() {
  int analogValue = analogRead(2); // 读取DAC值
  int analogVolts = analogReadMilliVolts(2); // 读取电压值(c)

  Serial.printf("ADC analog value = %d\n", analogValue);
  Serial.printf("ADC millivolts value = %d\n", analogVlots);

  delay(100);
}

5.2 电位器控制LED亮度

/* ADC + LEDC + 定时器(软件)
  通过更改定位器阻值控制LED亮度
*/
#include <AsyncTimer.h>

int pmPin = 32; // 电位器GPIO接口
int ledPin = 4; // LED
int ch0 = 0; // ledc通道

AsyncTimer timer;
int taskId = 0;

void ChangeLedLightness()
{
  int val = analogRead(pmPin);
  Serial.printf("%d:", val);

  auto vol = analogReadMilliVolts(pmPin);
  Serial.println(vol);

  int duty = val / 4095.0 * 1024;
  ledcWrite(ch0, duty);
}

void setup() {
  Serial.begin(115200);
  analogReadResolution(12); // 确定analogRead() 函数返回的值的分辨率(以位为单位)
  analogSetAttenuation(ADC_11db); // 设置所有通道衰减值

  ledcSetup(ch0, 1000, 10); // 设置ledc通道0,频率1000HZ,精度10
  ledcAttachPin(ledPin, ch0); 

  taskId = timer.setInterval(ChangeLedLightness, 20); //周期定时任务
}

void loop() {
  timer.handle();
}

 

6. I2C协议

6.1 I2C及Wire库使用

主机

// 主机Master
#include <Wire.h>

#define I2C_DEV_ADDR 0x55  // I2C设备地址

uint32_t i = 0;

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true); // 启用串口调试输出
  Wire.begin(); // 初始化I2C总线
}

void loop() {
  delay(5000);

  Wire.beginTransmission(I2C_DEV_ADDR); // 开始I2C传输
  Wire.printf("Hello World! %u", i++); // 向I2C设备发送数据
  uint8_t error = Wire.endTransmission(true); // 结束I2C传输并检查错误
  Serial.printf("endTransmission:%u\n", error);

  uint8_t bytesReceived = Wire.requestFrom(I2C_DEV_ADDR, 16); // 从I2C设备读取数据并返回接收到的字节数
  Serial.printf("requestFrom:%u\n", bytesReceived);
  if ((bool)bytesReceived) {
    uint8_t temp[bytesReceived];
    Wire.readBytes(temp, bytesReceived); // 读取接收到的字节
    log_print_buf(temp, bytesReceived); // 打印接收到的数据
  }
}

从机

// 从机Slave
#include "Wire.h"

#define I2C_DEV_ADDR 0x55

uint32_t i = 0;

/*
  onRequest()函数:用于处理主机的请求,在每次请求时,
  向主机发送递增的数据包计数,并打印调试信息。
*/
void onRequest(){
  Wire.print(i++);
  Wire.print("Packets.");
  Serial.println("onRequest");
}

//  onReceive()函数:用于处理主机发送的数据,在接收到数据时,打印接收到的数据内容和长度。
void onReceive(int len){
  Serial.printf("onReceived[%d]: ", len);
  while (Wire.available()){
    Serial.write(Wire.read());
  }
  Serial.println();
}

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Wire.onReceive(onReceive); // 注册接收回调函数
  Wire.onRequest(onRequest); // 注册请求回调函数
  Wire.begin((uint8_t)I2C_DEV_ADDR); // 初始化I2C从机

  // 如果是ESP系列芯片,可以使用slaveWrite函数发送初始消息
#if CONFIG_IDF_TARGET_ESP#@
  char message[64];
  snprintf(message, 64, "%u Packets.", i++);
  Wire.slaveWrite((uint8_t *)message, strlen(message));
#endif
}

void loop() {

}

6.2 ESP32双机通信

主机每秒2秒向从机发送递增的数字,

从机在收到主机的数据后LED闪烁0.5秒,并在收到的数字后加上OK字符发送给主机

主机收到从机发来的数据后打印在串口上

主机

主机程序使用了Wire库进行I2C通信。在setup函数中,初始化串口并加入I2C总线。在loop函数中,通过Wire.beginTransmissionWire.endTransmission向从机发送数字字符串,并通过Wire.requestFrom从从机接收数据。收到数据后,将其打印在串口上。

/* 
  主机每秒2秒向从机发送递增的数字,
  从机在收到主机的数据后LED闪烁0.5秒,并在收到的数字后加上OK字符发送给主机
  主机收到从机发来的数据后打印在串口上
*/

// 主机程序
#include <Wire.h>

int num = 1; // 发送给从机
int address = 33; // 从机地址

void setup() {
  Serial.begin(115200);

  if (Wire.begin()) // 主机加入I2C总线
    Serial.println("I2C Success");
  else 
    Serial.println("I2C Failed");
}

void loop() {
  char tmp[32];
  itoa(num++, tmp, 10); // 将数字转换成字符串

  Wire.beginTransmission(address);
  Wire.write(tmp); // 传输数字字符串
  int ret = Wire.endTransmission();
  if (ret != 0) // 判断状态
  {
    Serial.printf("Send failed:%d\r\n", ret);
    return;
  }

  delay(100); // 从机处理时间
    
  /*
  	Wire.requestFrom(address, quantity, stop);
  	requestFrom返回值代表了从机发来多少字节的数据,实际上是错误的,
  	返回值永远是等于你传进去的欲读取数据的数量值(quantity)
  	若 接收的数据量 > 从机发送的数据量,超出部分全部为 0x3f
  */
  int len = Wire.requestFrom(address, 32); // 发出请求,最多不超过32字节
  if (len > 0)
  {
    // 打印出来收到从机发来的数据
    Serial.print("Receive data size:");
    Serial.println(len);

    Wire.readBytes(tmp, 32);
    Serial.println(tmp);

    // 打印出收到数据的16进制值
    for (int i=0; i<32; i++)
    {
      Serial.printf("%2x, ", tmp[i]);
      if (i % 8 == 7)
        Serial.println();
    }
    Serial.println();
  }
  delay(1900);
}

从机

从机程序使用了Wire库进行I2C通信,并使用AsyncTimer库来控制LED闪烁。在onReceive函数中,当接收到数据时,将数据存储到缓冲区buf中,并让LED闪烁。在onRequest函数中,向主机发送带有"OK"字符的数据。

/* 
  主机每秒2秒向从机发送递增的数字,
  从机在收到主机的数据后LED闪烁0.5秒,并在收到的数字后加上OK字符发送给主机
  主机收到从机发来的数据后打印在串口上
*/

// 从机程序
#include <Wire.h>
#include <AsyncTimer.h>

char buf[32]; // 接受缓冲区
int ledPin = 4;

AsyncTimer timer;

void onReceive(int len) {
  // 接受数据,将数字存到缓冲区,并让led闪烁
  if (len > 0)
  {
    // 从I2C总线读取最多32个字节的数据,并将其存储到buf缓冲区中。函数返回实际读取到的字节数
    int sz = Wire.readBytes(buf, 32);
    if (sz > 0)
    {
      buf[sz] = 0;
      digitalWrite(ledPin, HIGH);

      // 注册定时事件,500ms后关闭led灯
      timer.setTimeout([](){
        digitalWrite(ledPin, LOW);
      }, 500);
    }
  }
}

void onRequest() {
   // 向主机发送数据
  strcat(buf, "OK"); // 拼接
  Wire.write(buf); // 发送缓冲区数据(包括"OK"字符)
  Wire.write(0);
}
void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT); 
  Wire.onReceive(onReceive); // 注册接受事件
  Wire.onRequest(onRequest); // 注册发送事件
  Wire.begin(33);
}

void loop() { 
 timer.handle();
}

6.3 I2C操控1602LCD

需要下载 LiquidCrystal_I2C 库,地址为:https://github.com/mrkaleArduinoLib/LiquidCrystal_I2C

主要用到的文件为 LiquidCrystal_I2C.hLiquidCrystal_I2C.cpp 这两个文件

使用时移动到项目文件根目录并调用

#include <Arduino.h>
#include <Wire.h>
#include "LiquidCrystal_I2C.h"

LiquidCrystal_I2C lcd(0x27, 16, 2); // LiquidCrystal_I2C lcd(显示器地址, 行数, 列数);

void setup() {
  lcd.init(); // 初始化 LCD 显示器
  lcd.backlight(); // 打开背光
  lcd.print("Hello World!"); // 在第一行打印 "Hello World!"
  // lcd.setCursor(列号, 行号)
  lcd.setCursor(0, 1); // 设置光标位置为第二行第一列
  lcd.print("I am a fish, I am a fish, I am a fish."); // 在第二行打印 "I am a fish, I am a fish, I am a fish."

  // 将第二行的 "am" 改成大写 "AM"
  lcd.setCursor(2, 1); // 设置光标位置为第二行第三列
  lcd.write('A'); // 写入大写字母 'A'
  lcd.write('M'); // 写入大写字母 'M'

  lcd.clear(); // 清空显示器

  // 字幕不停向左滚动
  for (int i = 0; i < 100; i++) {
    lcd.scrollDisplayLeft(); // 向左滚动显示内容
    delay(1000); // 延迟1秒
  }
}

void loop() {

}

 

7. 外部中断(硬件)

中断服务程序要求:

  • 要尽量地短,减少执行时间
  • 不要使用 delay() 函数
  • 不要使用 Serial 打印
  • 和主程序共享的变量要加_上 volatile 关键字
  • 不要使用 millis() 函数,它的值将不会增长
  • 可以使用 micros 函数来获取时间
  • 外部中断最高频率手册没说,但达到几M是没有问题的

7.1 按键开关LED

IRAM_ATTR 是一个ESP32的特殊属性,用于指定函数在IRAM(内部RAM)中运行,而不是默认的闪存(Flash)中运行。在ESP32中,IRAM是位于处理器内部的高速随机访问存储器,执行速度更快。

使用 IRAM_ATTR 属性可以将函数加载到IRAM中,从而提高函数的执行速度和响应性能。在中断服务程序(ISR)中使用 IRAM_ATTR 属性可以确保ISR在最短的时间内得到执行,从而更及时地响应中断事件。

因此,IRAM_ATTR 修饰符常常用于将中断服务程序(ISR)函数加载到IRAM中,以提高性能。

const byte LED = 4;
const byte BUTTON = 25;

// ISR
IRAM_ATTR void switchPressed()
{
  // 按钮松开高电平亮,按钮按下低电平灭
  if (digitalRead(BUTTON) == HIGH)
    digitalWrite(LED, HIGH);
  else 
    digitalWrite(LED, LOW);
}

void setup() {
  pinMode(LED, OUTPUT);
  pinMode(BUTTON, INPUT_PULLUP);
  // 设置和执行ISR(中断服务程序)
  attachInterrupt(digitalPinToInterrupt(BUTTON), switchPressed, CHANGE);
}

void loop() {

}

7.2 简单PWM测量仪

临界区 是一段代码片段,用于在多任务环境下保护共享资源,以确保对资源的访问不会被并发任务中断或干扰。临界区的作用是提供一种互斥机制,使得同一时间只有一个任务可以访问共享资源,避免并发访问导致的数据竞争和不一致性。

#include "LiquidCrystal_I2C.h" // 包含 LiquidCrystal_I2C 库,用于LCD显示器
// 共享变量
volatile unsigned long raiseTime = 0; // 前一次上升沿时间
volatile unsigned long fallTime = 0; // 前一次下降沿时间
volatile double duty = 0; // 占空比
volatile double fre = 0; // 频率

int pwmPin = 27; // 信号输入接口

// 显示器初始化
LiquidCrystal_I2C lcd(0x27, 16, 2);

// 自旋锁
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;

// ISR:中断服务程序
void changeISR()
{
  auto now = micros();
  if (digitalRead(pwmPin)) // 现在是高
  {
    /*
     临界区是一段代码片段,用于在多任务环境下保护共享资源,以确保对资源的访问不会被并发任务中断或干扰。
     临界区的作用是提供一种互斥机制,使得同一时间只有一个任务可以访问共享资源,避免并发访问导致的数据竞争和不一致性。
    */
    portENTER_CRITICAL_ISR(&mux); // 进入临界区
    auto total = now - raiseTime; // 周期 us
    fre = 1e6 / (double)total; // 频率
    auto h = fallTime - raiseTime; // 脉宽
    duty = h / (double)total; // 占空比 = 脉宽 / 周期
    portEXIT_CRITICAL_ISR(&mux); // 离开临界区
    raiseTime = now;
  }
  else
  {
    fallTime = now;
  }
}

void setup() { 
 lcd.init(); // 初始化 LCD 显示器
  lcd.backlight(); // 打开背光
  lcd.setCursor(0, 0); // 设置光标位置为第一行第一列
  lcd.print("fre: "); // 在 LCD 上打印 "fre: "
  lcd.setCursor(0, 1); // 设置光标位置为第二行第一列
  lcd.print("duty: "); // 在 LCD 上打印 "duty: "
  pinMode(pwmPin, INPUT); // 将 pwmPin 设置为输入模式
  attachInterrupt(digitalPinToInterrupt(pwmPin), changeISR, CHANGE); // 注册中断服务程序来响应 pwmPin 引脚状态变化的事件
}

void loop() {
  delay(1000); // 延迟1秒

  portENTER_CRITICAL(&mux); // 进入临界区
  double f = fre; // 读取频率值
  double d = duty; // 读取占空比值
  portEXIT_CRITICAL(&mux); // 离开临界区

  lcd.setCursor(5, 0); // 设置光标位置为第一行第五列
  lcd.print(f); // 在 LCD 上打印频率值
  lcd.setCursor(6, 1); // 设置光标位置为第二行第六列
  lcd.print(d); // 在 LCD 上打印占空比值
} 

 

8. 硬件定时器及二值信号量

分频数越大,周期越长,频率越低。分频数最大是 65525

流程 :初始化 -> 绑定ISR -> 设置触发ISR的计数值 -> 启动定时器

硬件定时器流程

#include <esp32-hal-timer.h>

hw_timer_t *timer = NULL;

void IRAM_ATTR timerISR() {
  // 硬件定时器中断服务程序
}

void setup() {
  timer = timerBegin(0, 80, true); // 创建硬件定时器,使用定时器 0,预分频因子 80,设置为自动重载模式
  timerAttachInterrupt(timer, &timerISR, true); // 将定时器中断服务程序与硬件定时器关联
  timerAlarmWrite(timer, 1000000, true); // 设置定时器定时周期为 1 秒,自动重载,即周期循环
  timerAlarmEnable(timer); // 启用定时器定时中断"
  // timerEnd(timer); // 结束
}

void loop() {
  // 主循环代码
}

8.1 硬件定时器样例

每 1s 打印一次当前迭代数和时间

// 每 1s 打印一次当前迭代数和时间
#include <esp32-hal-timer.h>
volatile int count = 0;
volatile unsigned long tim = 0;

hw_timer_t *timer1 = NULL; // 1s 1次
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

// ISR
void IRAM_ATTR onTimer1() {
  portENTER_CRITICAL_ISR(&timerMux); // 进入临界区
  count ++;
  tim = micros();
  portEXIT_CRITICAL_ISR(&timerMux); // 离开临界区
}

void setup() {
  Serial.begin(115200);
  // 初始化定时器,80分频,1us计数一次
  timer1 = timerBegin(0, 80, true);
  // 附加中断
  timerAttachInterrupt(timer1, onTimer1, true);
  // 计数到 1000000(1s) 时触发中断
  timerAlarmWrite(timer1, 1000000, true);
  // 开启定时器
  timerAlarmEnable(timer1);
}

void loop() {
  portENTER_CRITICAL(&timerMux);
  auto c = count;
  auto t = tim;
  portEXIT_CRITICAL(&timerMux);

  Serial.println(c);
  Serial.println(t);
}

loop() 函数 中的 portENTER_CRITICAL(&timerMux) 会启用自旋锁,并且禁用掉了CPU的中断。

loop()函数执行速度很快,中断被屏蔽时间会非常长,外部如果有两个或以上中断进来无法及时检测到。

想要解决这个问题,这时候就需要 二值信号量了。

8.2 二值信号量

// 每 1s 打印一次当前迭代数和时间
#include <esp32-hal-timer.h>
volatile int count = 0;
volatile unsigned long tim = 0;
volatile SemaphoreHandle_t timerSemaphore; // 信号量

hw_timer_t *timer1 = NULL; // 1s 1次
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

// ISR
void IRAM_ATTR onTimer1() {
  portENTER_CRITICAL_ISR(&timerMux); // 进入临界区
  count ++;
  tim = micros();
  portEXIT_CRITICAL_ISR(&timerMux); // 离开临界区

  /*
    从中断服务程序(ISR)中给予一个二值信号量它会将二值信号量的计数值增加,
    并唤醒等待该信号量的任务。第二个参数为 NULL 表示不需要唤醒任何任务。
  */
  // 设置完共享变量后发送信号 
  xSemaphoreGiveFromISR(timerSemaphore, NULL);
}

void setup() {
  Serial.begin(115200);

  timerSemaphore = xSemaphoreCreateBinary(); // 创建一个二值信号量
  // 初始化定时器,80分频,1us计数一次
  timer1 = timerBegin(0, 80, true);
  // 附加中断
  timerAttachInterrupt(timer1, onTimer1, true);
  // 计数到 1000000(1s) 时触发中断
  timerAlarmWrite(timer1, 1000000, true);
  // 开启定时器
  timerAlarmEnable(timer1);
}

void loop() {
  if (xSemaphoreTake(timerSemaphore, 0) == pdTRUE)
  {
    portENTER_CRITICAL(&timerMux);
    auto c = count;
    auto t = tim;
    portEXIT_CRITICAL(&timerMux);

    Serial.println(c);
    Serial.println(t);
  }
  
}

 

9. 超声波测距

HC-SR04 模块测量错误的情况:

  1. 物体体积太小,无法反射超声波
  2. 物体在探头15°范围之外
  3. 物体表面材质是吸收超声波的,比如毛绒绒的物体
  4. 物体与探头的夹角不对

9.1 距离测量

const int trigPin = 4;
const int echoPin = 16;

void setup() {
  Serial.begin(115200);
  delay(200);
  pinMode(trigPin, OUTPUT);
  pinMode(echoPin, INPUT);
}

void loop() {
  // 在Trig引脚发送15us脉冲
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(15); // 15us
  digitalWrite(trigPin, LOW);

  // 读取Echo引脚脉冲时长
  auto t = pulseIn(echoPin, HIGH);
  double dis = t * 0.01715; // 单位:CM
  Serial.print(dis);
  Serial.println(" cm");

  delay(200);
}

此程序阻塞过长,下面将使用中断方式优化

9.2 距离测量(中断优化)

中断测距原理:

  • 外部中断(change) 附加到 ECHO 的引脚上
  • 使用硬件定时器每 500msTrigger 一个 15us 的脉冲 (1s测量2次)
  • 在上升沿中断的时候记当前时间 t1
  • 在下降沿中断的时候记当前时间 t2,并发 信号(Semaphore) 给任务
  • Loop函数在收到信号后获取 t2和t1 的值,并计算出距离
// 中断测距
/* - 将 外部中断(change) 附加到 ECHO 的引脚上
- 使用硬件定时器每 500ms 给 Trigger 一个 15us  的脉冲 (1s测量2次)
- 在上升沿中断的时候记当前时间 t1 
- 在下降沿中断的时候记当前时间 t2,并发 信号(Semaphore)`  给任务
- Loop函数在收到信号后获取 t2和t1 的值,并计算出距离
*/

const int trigPin = 4;
const int echoPin = 16;
double distance = 0; // 单位cm

hw_timer_t *timer1 = NULL; // 定时器
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; // 自旋锁
volatile unsigned long startTime = 0; // 发出超声波时间
volatile unsigned long endTime = 0; // 收到超声波时间
volatile SemaphoreHandle_t semaphore; // 信号量

// 硬件定时器ISR
void IRAM_ATTR ping()
{
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(15);
  digitalWrite(trigPin, LOW);
}

// ECHO 引脚ISR
void IRAM_ATTR changeISR() 
{
  auto now = micros(); // 当前时间
  auto state = digitalRead(echoPin);

  portENTER_CRITICAL_ISR(&mux);
  if (state) // 高电平,即刚发出超声波
    startTime = now;
  else
    endTime = now;
  portEXIT_CRITICAL_ISR(&mux);

  // 变成低电平时表示已经收到回声
  if (!state)
    xSemaphoreGiveFromISR(semaphore, NULL); // 给一个信号量发送信号
}


void setup() {
  pinMode(trigPin, OUTPUT);
  pinMode(echoPin, INPUT);
  Serial.begin(115200);

  semaphore = xSemaphoreCreateBinary(); // 创建二值信号量

  // 定时器部分
  timer1 = timerBegin(0, 80, true);
  timerAttachInterrupt(timer1, ping, true);
  timerAlarmWrite(timer1, 500000, true); // 定时时间为 0.5s
 
  // echo引脚的中断
  attachInterrupt(digitalPinToInterrupt(echoPin), changeISR, CHANGE);

  // 开始周期测量
  timerAlarmEnable(timer1);

}

void loop() {
  if (xSemaphoreTake(semaphore, 0) == pdTRUE)
  {
    // 收到信号,准备工作
    portENTER_CRITICAL(&mux);
    auto t = endTime - startTime;
    portEXIT_CRITICAL(&mux);

    double dis = t * 0.01715;
    if (dis < 350)
    {   
        distance = dis;
        Serial.print("Distance: "); 
        Serial.print(distance, 1); // 小数点后1位
        Serial.println(" cm");
    }
  }
}

 

10. 舵机

10.1 Servo库操控舵机

库名称为 ESP32Servo

#include <ESP32Servo.h>

Servo servo1; // 定义对象
Servo servo2;

int minUs = 500; // 0°时的脉宽,单位us
int maxUs = 2500; // 180°时的脉宽,单位us

int servo1Pin = 15;
int servo2Pin = 16;
int pos = -1; // 舵机角度
bool up = true; // 计数方向

void setup() {
  ESP32PWM::allocateTimer(1); // 指定使用的硬件定时器

  servo1.setPeriodHertz(50); // 指定PWM的频率
  servo2.setPeriodHertz(50); // 指定PWM的频率

  servo1.attach(servo1Pin, minUs, maxUs);
  servo2.attach(servo2Pin, minUs, maxUs);

}

void loop() {
  if (pos == 181)
    up = false;
  else if (pos == -1)
    up = true;
  
  if (up)
    pos ++;
  else
    pos --;
  
  servo1.write(pos);
  servo2.write(180 - pos);

  //servo1.write(pos); // 转到指定的角度(0° - 180°)
  //servo1.detach(); // 不需要的时候将引脚和ledc分离

  delay(15);
}

10.2 智能垃圾桶

使用超声波测距配合舵机实现智能垃圾桶,因为懒得弄模型,所以垃圾桶开闭直接用串口打印信息。

其相关流程及代码部分见此篇文章:ESP32Demo:智能垃圾桶 – Echo (liveout.cn)

 

11. WiFi连接

#include<WiFi.h>
const char* ssid = "WiFi名称";
const char* password = "WiFi密码";
void setup() {
  //初始化串口
  Serial.begin(115200);
  delay(10);
    
  // 进行WiFi连接
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  //连接WIFI
  WiFi.begin(ssid, password);
  //等待WIFI连接成功
  while (WiFi.status() != WL_CONNECTED) { //WiFi.status()函数用于获取WiFi连接的状态
    //WL_CONNECTED,即连接状态
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");

}

void loop() {
    
}

 

 

--> 前言此篇文章为有关 ESP32 的学习期间的代码记录,并且加上了自己的注释,非教学文章。使用开发板全称ESP32 DEVKILTv1(devkitv1) ,搭载芯片为 ESP32D0WDQ6,使用软件为 Arduino 。  参考链接如果是小白并且想要学习单片机相关知识,建议移步此篇文章:51单片机入门教程(上篇)(代码+个人理解) – Echo (liveout.cn...

前言

心心念念了一个学期,最终还是全款买下一个香橙派,型号为 OrangePi 3 LTS。

本来想买树莓派的,可惜溢价太严重了,于是只好用香橙派来代替了。

因为经常折腾系统,所以肯定会多次重置,就写了一篇配置文章,留着以后重置后复制粘贴用,也就是备份了。

以下所有命令都是以Ubuntu系统为准

 

资料

Orange Pi 3 LTS地址:Orange Pi 3 LTS-Orange Pi官网-香橙派(Orange Pi)开发板,开源硬件,开源软件,开源芯片,电脑键盘

用户手册和原理图:用户手册和原理图_免费高速下载|百度网盘-分享无限制 (baidu.com)

 

烧录

材料:内存卡一个(8G起步)

工具软件:

  1. 烧录软件:balenaEtcher - Flash OS images to SD cards & USB drives
  2. 格式化内存卡软件:https://www.sdcard.org/downloads/formatter/eula_windows/SDCardFormatterv5_WinEN.zip

过程没啥好说的,直接看用户文档。先下载镜像,再格式化内存卡,然后通过烧录软件将镜像烧录进去。香橙派会自动选择加载内存卡里系统。

可参考好友文章:制作Linux启动盘 – Clif's Blog (new-epoch-meta.com)

配置

桌面服务

因为桌面服务功耗太高了,所以平时就关闭了,也可以直接下载服务器版本镜像使用

sudo orangepi-config

选择 System ,DesktopStop

关闭后记得重启生效

reboot

 

WiFi连接

没有最基本的网络,那么远程连接都不行,所以先进行网络连接

打开无线

nmcli radio wifi on

列出可用的 WiFi 网络

nmcli device wifi list

选择要连接的 WiFi 网络

nmcli device wifi connect SSID password PASSWORD

查看连接

nmcli con show

断开连接

nmcli con down yourSSID

重新连接

nmcli con up yourSSID

SSID 替换为要连接的 WiFi 网络的名称,将 PASSWORD 替换为该网络的密码。如果该网络不需要密码,则无需提供 password 部分。

请注意,使用 nmcli 连接 WiFi 需要您的系统上安装了 NetworkManager,并且您具有适当的权限来进行网络连接操作。

 

用户配置、SSH连接

系统初始用户名和密码都是 orangepi , 如果要更改root密码,使用 passwd 命令即可

passwd

通过命令查看ip地址,然后ssh连接香橙派

ifconfig

 

Vim配置

Vim 肯定是使用频率最高的东西之一了,所以得好好配置

vim /etc/vim/vimrc  #这里是全部用户都一样
syntax on                                                                                             set tabstop=4
set softtabstop=4
set shiftwidth=4
set autoindent
set cindent
set nu
set ruler
set cursorline
set cursorcolumn
set cuc cul

更加全局详细的配置:Vim基本配置 – Echo (liveout.cn)

 

.bashrc 配置

.bashrc文件位于Linux系统用户的主目录中。该文件是用户特定的bash shell配置文件

用于定义用户的环境变量、别名和其他定制内容

alias ls='ls --color=auto' #文件和文件夹设置不同的颜色
alias ll='ls -l'           # ll -> ls -l

最后别忘了运行 source ~/.bashrc 来使修改生效。

 

更换软件源

因为香橙派官方用的就是 清华源 ,所以就不换了。如果要更换,步骤如下

  1. 备份

    sudo cp /etc/apt/sources.list /etc/apt/sources.list.backup
    
  2. 打开配置文件

    sudo vim /etc/apt/sources.list
    
  3. 选择合适的源,这里以 阿里云 为例

    deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
    deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
    deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
    deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
    
  4. 更新软件源

    sudo apt update
    

     

UFW配置

安装

sudo apt install ufw

查看状态

sudo ufw status verbose

启动

sudo ufw enable

关闭

sudo ufw disable

开启/禁用相应端口或服务举例

sudo ufw allow 80 #允许外部访问80端口
sudo ufw delete allow 80 #禁止外部访问80端口
sudo ufw allow from 192.168.1.1 #允许此IP访问所有的本机端口
sudo ufw deny smtp #禁止外部访问smtp服务
sudo ufw delete allow smtp #删除上面建立的某条规则

#要拒绝所有的TCP流量从10.0.0.0/8 到192.168.0.1地址的22端口
sudo ufw deny proto tcp from 10.0.0.0/8 to 192.168.0.1 port 22

#可以允许所有RFC1918网络(局域网/无线局域网的)访问这个主机(/8,/16,/12是一种网络分级):
sudo ufw allow from 10.0.0.0/8
sudo ufw allow from 172.16.0.0/12
sudo ufw allow from 192.168.0.0/16

默认情况下,UFW 阻塞了所有进来的连接,并且允许所有出去的连接。这意味着任何人无法访问你的服务器,除非你打开端口。运行在服务器上的应用和服务可以访问外面的世界。

默认的策略定义在/etc/default/ufw文件中,并且可以通过使用sudo ufw default <policy> <chain>命令来修改。

 

Git配置

一般系统都装有Git,所以就记录下配置信息

  1. 配置用户信息

    git config --global user.name "Your Name"
    git config --global user.email "your.email@example.com"
    
  2. 配置文本编辑器

    git config --global core.editor "vim"
    
  3. 配置 Git 的颜色输出

    git config --global color.ui true
    

可参考好友文章:Ubuntu20.04下安装Docker – Clif's Blog (new-epoch-meta.com)

 

Docker安装

文档里有相关教程,这里我复制下来,方便粘贴

sudo apt updat
sudo apt-get install -y ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) \
signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \
https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io

检查 docker 的状态

systemctl status docker
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2020-08-24 10:29:22 UTC; 26min ago
Docs: https://docs.docker.com
Main PID: 3145 (dockerd)
Tasks: 15
CGroup: /system.slice/docker.service
└─3145 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.soc

测试 docker

$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
256ab8fe8778: Pull complete
Digest:
sha256:7f0a9f93b4aa3022c3a4c147a449ef11e0941a1fd0bf4a8e6c9408b2600777c5
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correct

设置 docker 仓库为国内源

vim /etc/docker/daemon,json
{
	"registry-mirrors": [ 		      
		"https://docker.mirrors.ustc.edu.cn"
 	]
}

重启 docker 服务

sudo systemctl restart docker

 

LAMP架构

买了这个,肯定得搭建一个网站玩玩,所以先配置好相关环境,之前已经写过了,直接看这篇文章

Wordpress/Typecho博客搬迁教程 – Echo (liveout.cn)

 

查看温度

毕竟长时间运行,温度得注意点,但是官方给的查看命令太长了,所以就写了个脚本

后来发现可以安装相关包,然后通过sensors命令查看,安装过程补充在脚本后面

CPU

#!/bin/bash

temp=$(cat /sys/class/thermal/thermal_zone0/temp)
temp=$((temp/1000))

# 定义颜色代码
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' 

if ((temp >= 70)); then
    echo -e "CPU温度: ${RED}${temp}°C${NC}"
else
    echo -e "CPU温度: ${GREEN}${temp}°C${NC}"
fi

GPU

#!/bin/bash

temp=$(cat /sys/class/thermal/thermal_zone1/temp)
temp=$((temp/1000))

# 定义颜色代码
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' 

if ((temp >= 70)); then
    echo -e "GPU温度: ${RED}${temp}°C${NC}"
else
    echo -e "GPU温度: ${GREEN}${temp}°C${NC}"
fi

顺便买了个散热套装

补充

才发现可以安装相应的软件包查看温度,命令如下

安装

sudo apt install lm-sensors

选择监控范围

sudo sensors-detect

查看温度

sensors

 

存储

Linux系统不像Windows可以自动识别外置存储设备,需要自己手动挂载,所以挂载硬盘肯定是必修课了。

下面是一些常用的磁盘管理命令,精简版。

du

disk usage ,用来展示磁盘使用量的统计信息

du -s

-s选项,是 --summarize 的缩写形式,其作用是对 du 的每一个给定参数计算其磁盘使用量,我们来看例子。

[roc@roclinux ruanjian]$ du -sh *
3.4M    curl-7.34.0.tar.gz
41M     soft
6.8M    wordpress-4.4.1.tar.gz

du -c

-c选项,是 --total 的缩写形式,它表示的是针对输出的各个对象来计算其磁盘使用量的总和。比如,我们想计算当前文件夹下所有后缀是 tar.gz 的文件的磁盘使用量总和,那么命令是这样的:

[roc@roclinux ruanjian]$ du -ch *.tar.gz
3.4M    curl-7.34.0.tar.gz
6.8M    wordpress-4.4.1.tar.gz
11M     总用量

当然,-c选项也可以计算文件和文件夹的混合求和:

[roc@roclinux ruanjian]$ du -ch curl-7.34.0.tar.gz soft
3.4M    curl-7.34.0.tar.gz
41M     soft
45M     总用量

 

df

disk free 命令用于显示目前在 Linux 系统上的文件系统磁盘使用情况统计。

df

# df 
Filesystem     1K-blocks    Used     Available Use% Mounted on 
/dev/sda6       29640780 4320704     23814388  16%     / 
udev             1536756       4     1536752    1%     /dev 
tmpfs             617620     888     616732     1%     /run 
none                5120       0     5120       0%     /run/lock 
none             1544044     156     1543888    1%     /run/shm 

df -h

-h选项,通过它可以产生可读的格式df命令的输出:

# df -h 
Filesystem      Size  Used   Avail Use% Mounted on 
/dev/sda6       29G   4.2G   23G   16%     / 
udev            1.5G  4.0K   1.5G   1%     /dev 
tmpfs           604M  892K   603M   1%     /run 
none            5.0M     0   5.0M   0%     /run/lock 
none            1.5G  156K   1.5G   1%     /run/shm 

 

lsblk

List block devices ,列出所有块设备

root@orangepi3-lts:/opt# lsblk
NAME         MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
loop0          7:0    0     4K  1 loop /snap/bare/5
loop1          7:1    0  68.5M  1 loop /snap/core22/807
loop2          7:2    0  91.7M  1 loop /snap/gtk-common-themes/1535
loop3          7:3    0  46.4M  1 loop /snap/snapd/19459
mmcblk2      179:0    0   7.3G  0 disk
├─mmcblk2p1  179:1    0     2G  0 part
├─mmcblk2p2  179:2    0    16M  0 part
├─mmcblk2p3  179:3    0    16M  0 part
├─mmcblk2p4  179:4    0    32M  0 part
├─mmcblk2p5  179:5    0     2G  0 part
├─mmcblk2p6  179:6    0   300M  0 part
├─mmcblk2p7  179:7    0    16M  0 part
├─mmcblk2p8  179:8    0    16M  0 part
├─mmcblk2p9  179:9    0    32M  0 part
├─mmcblk2p10 179:10   0     2G  0 part
├─mmcblk2p11 179:11   0    16M  0 part
├─mmcblk2p12 179:12   0    64M  0 part
├─mmcblk2p13 179:13   0     2M  0 part
├─mmcblk2p14 179:14   0    32M  0 part
├─mmcblk2p15 179:15   0    16M  0 part
└─mmcblk2p16 179:16   0   768M  0 part
mmcblk2boot0 179:32   0     4M  1 disk
mmcblk2boot1 179:64   0     4M  1 disk
mmcblk0      179:96   0 119.4G  0 disk
└─mmcblk0p1  179:97   0 118.2G  0 part /var/log.hdd
                                       /
zram0        252:0    0 992.4M  0 disk [SWAP]
zram1        252:1    0    50M  0 disk /var/log
zram2        252:2    0     0B  0 disk

 

mnt

mount 命令是经常会使用到的命令,它用于挂载Linux系统外的文件。

挂载设备的过程

  1. 获取设备名称

    fdisk -l
    
  2. 建立挂载点目录

    cd /mnt
    mkdir usb
    
  3. 挂载设备

    mount /dev/sdb1 /mnt/usb
    

    PS:若文件名因含有中文出现乱码,可用以下命令解决

    mount -o iocharset=cp936 /dev/sdb1 /mnt/usb
    

umount 卸载设备

umount /dev/sdb1
umount /mnt/usb

 

内网穿透

通过 frp 实现内网穿透,前提是有一台公网服务器,作为服务端使用

FRP内网穿透实践教程 - 知乎 (zhihu.com)

使用frp进行内网穿透 - 少数派 (sspai.com)

以上两篇结合着看就够了~

frp下载地址:https://github.com/fatedier/frp/releases

参考文档:概览 | frp (gofrp.org)

 

结尾

目前就这些了,等到有其他需要备份的信息再补上

--> 前言心心念念了一个学期,最终还是全款买下一个香橙派,型号为 OrangePi 3 LTS。本来想买树莓派的,可惜溢价太严重了,于是只好用香橙派来代替了。因为经常折腾系统,所以肯定会多次重置,就写了一篇配置文章,留着以后重置后复制粘贴用,也就是备份了。以下所有命令都是以Ubuntu系统为准 资料Orange Pi 3 LTS地址:Orange Pi 3 LTS-Orange Pi官网...

人生小事

每次写生活类的文章时,都不知道第一个文章目录如何起名,像起笔、缘起啥的都给我借用过了。对于写这种记录生活的流水账文章,我特别喜欢带入自己的感受,努力将平时所感寄托于文字中。一般这种文章比教程类容易却又难写。

教程类文章我只需要读者(或者我自己)能够看懂以及跟着操作就行,讲究条理清晰,所以目录一般都是主要步骤。但是生活类文章我却得努力让它具有我自己的特点,使读者读到这种文字,就会想到:哦~原来是那个博主。emm,有一点过于看重自己了,估计也没几个人会读一个陌生人的流水账,哈哈。

好了,综上所述,这篇文章的第一个目录就叫人生小事了,毕竟文章都是关于大二下这一学期的小结。

本来没打算写的,可是发现博客已经好久没有更新文章了,所以就硬凑下吧。这应该算是博客中第二篇属于生活类的文章,我也打算努力向生活类博客过渡(毕竟没啥技术,还喜欢看别人的生活博客,hh)至于年终总结写啥,就暂且不管了 ,说不定直接鸽了~

莫思身外无穷事,且尽生前有限杯

疫情后的南京

12月7号,国务院发布《关于进一步优化落实新冠肺炎疫情防控措施的通知》,按国家卫健委要求,我国境内除一些特殊场所外,其他场所出入不再需要核酸阴性报告,国务院联防联控机制发布的新十条措施中提到全国各地不再查验健康码,行程码。

12月7日,就在我提前放假的第二天,解封了。整个寒假期间都是关于阳了的新闻,遇到熟人的第一句从“你吃了没?”变成了“你阳了没?”。就这样,大二下开始了。随着大多数人都阳康后,“大学生特种兵旅游”也火了起来。室友在两天之中爬了个泰山,而我则是去了趟六朝古都——南京。

尽管去之前已经了解到了南京的旅游人数很多,然而到了后,还是被惊讶住了,可能这就是被疫情偷走三年时光的人们的报复吧。

吐槽一句,下了高铁才发现南京南站是真的大呀,每次坐地铁出来的出口都不一样,没办法,谁让我是路痴,害。

在去秦淮河的路上,随手拍了一张(毕竟人太多了),基本是跟着人群走,路上还遇到了好几个导游团。此时看到这张图片,还是不由得感慨那疫情三年。

下面再来几张旅游图,都是随便拍的,就当是来过的标记了

顺便吐槽下,上面的梅花糕感觉不是很好吃,不过挺好看的,哈哈

南京博物院!!!这个强烈推荐去看看,但是一定要记得提前在公众号预约。里面给我映像最深的就是民国馆以及排了三小时的队,只为了收集印章。里面还有个纪念品店,可以购买明信片等周边物品,防止你像我一样用一个大白纸收集印章,哈哈。

除了这几处外,还去了其他一些地方,可惜时间有限,还剩下许多景点没有机会去,下次一定。

对了,南京的地铁声音灰常好听,怪不得抖音上这么火,但是,真的几乎没有座位坐,233

徐州烧烤

淄博烧烤在那段时间也火了起来,可惜没有机会去,但是徐州烧烤就在旁边,这我就得加点篇幅介绍下了。

徐州烧烤是徐州饮食文化的体现,烧烤最重要的食材就是羊。 1986年,徐州境内出土的一块汉画像石上就有烧烤的画像。

最接近现代形式的烧烤,首次出现在汉代画像石上。徐州作为中国汉画像石的起源地之一,有着丰富的汉画资料。肉串烤炉加蘸料,灵魂烧烤“三件套”,这样的画面,在徐州出土的汉画像石中频频出现。

不论是汉画像石的图像材料,还是出土的烧烤器具文物,或是彭祖文化对徐州的影响,都可以表明徐州是中国烧烤的发源地之一。

综上所述,可以说徐州是烧烤的发源地了,至于到底怎么样则可以看这篇文章:

https://zhuanlan.zhihu.com/p/553838007

毕竟一千个哈姆雷特有一千个胃口,所以如果你喜欢烧烤或者美食,一定要亲自来尝试下。

徐州除了烧烤,还有个湖,俗称云龙湖,至于举办的彭城风华表演,只能说太贵了,没去看过。

当然了,徐州作为九州之一,有超过6000年的文明史和2600年的建城史,是两汉文化的发源地,同时是帝王之乡,所以徐州博物馆一定要去看看,尤其是镇馆之宝——金缕玉衣。(记得提前预约)

emm,再贴一个图

复苏的电影市场

这个学期一共就看了四个电影,其中两个为动漫,一个公路片,一个牛马片,这里就短暂评价下。

两部动漫分别为《铃芽之旅》和《蜘蛛侠:纵横宇宙》。其中我觉得蜘蛛侠的更值得一看,可能因为我是男孩子,又或者因为我对新海诚的期待太高了,谁让他的《秒速五厘米》和《言叶之庭》那么好看,还特地买了周边

下面就是多图预警了

剩下的公路片就是《人生路不熟了》,感觉还是不错的,期间笑点挺足。

至于牛马片——《龙马精神》,emm,我只能一句牛马了,但是毕竟是米粉节活动免费看的,也不多说啥了。

对了,最新出的《消失的她》以及《八角笼》听说都挺好看的,有时间一定去看看,先立个flag。

走在小路上

下面就是日常生活中的点点滴滴了,再次贴几张图片吧。

可惜传输的图片压缩了画质,我也懒得调了,不然夜空图应该会很好看。

五一期间久违的没有了疫情,放了七天假,于是回了趟家,顺便拍了张傻猫咪和夜晚。

长时间走在宿舍食堂以及教室的三点一线上,顿时有种梦回高中的感觉,虽然没有那时候那么紧张,并且有着许多娱乐设备,然而精神内耗并不会就此消失。所以,看看风景,不只是治愈眼睛,也是治愈心灵。

最终BOSS

每个时代的人有着每个时代的使命,同样的,每个年龄的人有着每个年龄的迷茫。高中生可能在迷茫着高考,大学生则是期末考试。没错,这很大学生。

考试的前几天,终于舍得迈开步子,去了图书馆进行紧急救援。晚上顺手拍了张照片(毕竟一学期才来一次),感觉还是很不错的。

在这抢救期间,感觉脑子回到了高中,就像一台年久失修的跑车,尽管很难启动,但是一旦跑起来,速度还是可以的。可惜,时间回不到高中,不然按照今年高考的卷度,我现在可能在工地打灰了。突然想起一个著名等式

清澈的愚蠢 + “你人还怪好嘞” = 大学生 < 高中生

嗯,流水账算是记完了,果然和预想的一样,全程下来如流水,毕竟文学天赋在这呢。嗯,一点天赋都没有。

不过能够水完一篇文章已经很不错了,高考后再也没有认真写过文章了,也从未想过大学会写这些。

按照正常的作文套路,最后该升华主题了,那么就老套路,再来一首苏轼的名句

回首向来萧瑟处,归去,也无风雨也无晴。

突然想起来开学就大三了,真快啊~

到底是谁给我按下了加速键呢。

--> 人生小事每次写生活类的文章时,都不知道第一个文章目录如何起名,像起笔、缘起啥的都给我借用过了。对于写这种记录生活的流水账文章,我特别喜欢带入自己的感受,努力将平时所感寄托于文字中。一般这种文章比教程类容易却又难写。教程类文章我只需要读者(或者我自己)能够看懂以及跟着操作就行,讲究条理清晰,所以目录一般都是主要步骤。但是生活类文章我却得努力让它具有我自己的特点,使读者读到这种文字,就会想到:哦...