分类 Learn 下的文章

前言

此篇文章为有关 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官网...

前言

虽然是考试月,并且一星期三门专业课,但是这个憨憨计组真的是越看越头疼,感觉再看下去要死在宿舍了。

在闲逛友链博客时,突然想在虚拟机里建个博客(虽然这两件事情之间并无关联),然后通过内网穿透进行访问,

这样等下次买了小主机后就有经验了,于是就有了此篇教程文章。

此次博客搬迁教程不涉及任何面板工具,从而降低功耗,适合建站小白观看。

此次迁移以 wordpress 为例,但 typecho 搬迁方法也和这个一样。

如果使用宝塔面板,可以直接使用宝塔的免费一键迁移功能,应用商店搜索即可。

 

1. 安装lamp环境(以Ubuntu为例)

  1. 更新软件包列表:打开终端(Terminal),执行以下命令以更新软件包列表:

    sudo apt update
    
  2. 安装Apache Web服务器:在终端中执行以下命令安装Apache:

    sudo apt install apache2
    
  3. 验证Apache是否安装成功:安装完成后,Apache服务会自动启动。在浏览器中输入服务器的IP地址或域名,如果看到默认的Apache欢迎页面,表示安装成功。

  4. 安装MySQL数据库服务器:在终端中执行以下命令安装MySQL服务器:

    sudo apt install mysql-server
    
  5. 安装PHP:在终端中执行以下命令安装PHP及常用扩展:

    sudo apt install php libapache2-mod-php php-mysql
    
  6. 验证PHP是否安装成功:创建一个简单的PHP文件以验证PHP是否正常工作。在终端中执行以下命令:


    1.   sudo vim /var/www/html/info.php
      
    2. 在打开的文件中,输入以下内容:

      <?php
      phpinfo();
      ?>
      
  7. 保存并关闭文件。然后,在浏览器中输入服务器的IP地址或域名,加上/info.php,如果看到PHP信息页面,表示安装成功。

2. 设置MySQL密码

  1. 打开终端(Terminal),以root用户身份登录或使用具有sudo权限的用户。

  2. 运行以下命令以连接到MySQL服务器:

    mysql -u root
    
  3. 进入MySQL命令行后,执行以下命令来更改root用户的密码(将new_password替换为您要设置的密码):

    ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'new_password';
    
  4. 刷新权限使更改生效:

    FLUSH PRIVILEGES;
    
  5. 退出MySQL命令行:

    exit
    

 

3. 迁移博客

  1. 在源服务器上将WordPress文件夹(即博客文件夹)打包为一个压缩文件(例如:wordpress.zip)。

    zip -r wordpress.zip /path/to/wordpress
    
  2. 将压缩文件通过scp命令拷贝到目标服务器上。(就是将数据库文件传送到目的服务器

    scp wordpress.zip user@目的ip:/path/to/destination_folder
    
  3. 在目标服务器上解压缩WordPress文件。

    unzip wordpress.zip
    
  4. 将WordPress文件夹移动到Web服务器的文档根目录

    sudo mv /path/to/destination_folder/wordpress /var/www/html/
    
  5. 授予适当的文件权限。

    sudo chown -R www-data:www-data /var/www/html/wordpress
    sudo chmod -R 755 /var/www/html/wordpress
    
  6. 连接到MySQL数据库,并创建一个新的数据库。

    mysql -u root -p
    CREATE DATABASE wordpress_database;
    
  7. 导入源服务器上的数据库备份文件(source_server_backup.sql)到新创建的数据库。

    mysql -u root -p wordpress_database < /path/to/source_server_backup.sql
    
  8. 在WordPress的wp-config.php文件中更新数据库连接信息,包括数据库名称、用户名和密码。

    vim wp-config.php
    define('DB_NAME', 'wordpress_database'); /* 数据库名 */
    define('DB_USER', 'root');		/* 用户名 */
    define('DB_PASSWORD', 'new_password');	/* 密码 */
    
  9. 检查Apache的配置文件(通常是/etc/apache2/sites-available目录下的000-default.conf.conf文件)是否正确设置了WordPress的虚拟主机,并且指定了正确的文档根目录和文件权限。

    VirtualHost 标签之间,DocumentRoot后面的改为你的博客绝对路径

    <VirtualHost *:80>
    	ServerAdmin webmaster@localhost
        DocumentRoot /var/www/html/liveout.cn
    </VirtualHost>
    
  10. 重启 Apache 以使配置生效:

    sudo systemctl restart apache2
    
  11. 打开网页浏览器,访问新服务器的地址,应该能够看到WordPress网站的动态页面。

  12. 如果跳转博客其他页面请求失败,则检查WordPress文件里是否有一个 .htaccess 文件,并且具有正确的重写规则。

    由于 .htaccess 文件是一个隐藏文件(文件名以点号开头),某些文件管理器默认情况下可能不会显示它。你需要在文件管理器中启用显示隐藏文件选项,或者通过命令行查看和编辑 .htaccess 文件。

    # BEGIN WordPress
    RewriteEngine On
    RewriteBase /
    RewriteRule ^index\.php$ - [L]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . /index.php [L]
    # END WordPress
    

     

 

--> 前言虽然是考试月,并且一星期三门专业课,但是这个憨憨计组真的是越看越头疼,感觉再看下去要死在宿舍了。在闲逛友链博客时,突然想在虚拟机里建个博客(虽然这两件事情之间并无关联),然后通过内网穿透进行访问,这样等下次买了小主机后就有经验了,于是就有了此篇教程文章。此次博客搬迁教程不涉及任何面板工具,从而降低功耗,适合建站小白观看。此次迁移以 wordpress 为例,但 typecho 搬迁方法也...

前言

基础算法模板 – Echo's blog (liveout.cn)


并查集

一共有 n个数,编号是 1∼n,最开始每个数各自在一个集合中。

现在要进行 m个操作,操作共有两种:

  1. M a b,将编号为 a和 b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. Q a b,询问编号为 和 b 的两个数是否在同一个集合中;
#include<iostream>
using namespace std;
const int N=100010;
int p[N];//定义多个集合

int find(int x)
{
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}

int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) p[i]=i;
    while(m--)
    {
        char op[2];
        int a,b;
        scanf("%s%d%d",op,&a,&b);
        if(*op=='M') p[find(a)]=find(b);
        else
        if(find(a)==find(b))
        printf("Yes\n");
        else
        printf("No\n");
    }
    return 0;
}

 

单调栈

给定一个长度为 N的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1−1

#include <iostream>
using namespace std;
const int N = 1e5+10;
int n;
int stk[N], tt;

int main()
{
    cin >> n;
    for (int i=0; i<n; i++)
    {
        int x;
        cin >> x;
        while (tt && stk[tt] >= x) tt--;
        
        if (tt) cout << stk[tt] << ' ';
        else cout << -1 << ' ';
        
        stk[++tt] = x;
    }
    return 0;
}

 

搜索

DFS(排列数字)

给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。

现在,请你按照字典序将所有的排列方法输出。

#include <iostream>
using namespace std;
const int N = 10;
int path[N];
int state[N];
int n;

void dfs(int u) 
{
	if (u>n)
	{
		for (int i=1; i<=n; i++)//数字填完了,输出
			cout << path[i] << " ";//输出方案
		cout << endl;	
	}	
	
	for (int i=1; i<=n; i++)//空位上可以选择的数字为:1 ~ n
	{
		if (!state[i]) //如果数字 i 没有被用过
		{
			path[u] = i;
			state[i] = 1;
			dfs(u+1);
			state[i] = 0;
		}
	}
}

int main()
{
	cin >> n;
	dfs(1);
	
	return 0;
}

 

BFS(迷宫)

给定一个 n×m的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。

最初,有一个人位于左上角 (1,1) 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。

请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。

数据保证 (1,1) 处和 (n,m)处的数字为 0,且一定至少存在一条通路。

#include <bits/stdc++.h>
using namespace std;

typedef pair<int, int> PII;
const int N = 110;
int n, m;
int g[N][N]; //迷宫 
int d[N][N]; //每一个点到起点距离 

int bfs()
{
	queue<PII> q;
	memset(d, -1, sizeof d);
	d[0][0] = 0;
	q.push({0,0}); //起点 
	
	int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
	while (q.size())
	{
		auto t = q.front();
		q.pop();
		for (int i=0; i<4; i++)
		{
			int x = t.first+dx[i], y = t.second+dy[i];
			if (x>=0 && x<n && y>=0 && y<m && g[x][y] == 0 && d[x][y]==-1)
			{
				d[x][y] = d[t.first][t.second]+1;
				q.push({x,y}); //下一个点	
			}
		}
	}
	return d[n-1][m-1];
}
 
int main()
{
	cin >> n >> m;
	for (int i=0; i<n; i++)
		for (int j=0; j<m; j++)
			cin >> g[i][j];
	cout << bfs() << endl;
	
	return 0;
}

 

BFS (图中点的层次)

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环。

所有边的长度都是 1,点的编号为 1∼n.

请你求出 1 号点到 n 号点的最短距离,如果从 1 号点无法走到 n 号点,输出 −1。

#include <bits/stdc++.h>
using namespace std;

const int N = 1e5+10;

int h[N], e[N], ne[N], idx;
int d[N]; //存储每个节点离起点的距离  d[1]=0
int n, m;


void add(int a, int b)
{
	e[idx] = b, ne[idx] = h[a], h[a] = idx ++;	
} 

int bfs()
{
	memset(d, -1, sizeof d);
	queue<int> q;
	d[1] = 0; //存储每个节点离起点的距离
	q.push(1);
	while (q.size())
	{
		int t = q.front();
		q.pop();
		
		for (int i=h[t]; i != -1; i=ne[i])
		{
			int j=e[i];
			if (d[j] == -1)
			{
				d[j] = d[t] + 1; //d[j]存储j节点离起点的距离,并标记为访问过
				q.push(j);
			}
		}
	}
	return d[n];
}

int main()
{
	cin >> n >> m;
	memset(h, -1, sizeof h);
	
	for (int i=0; i<m; i++)
	{
		int a, b;
		cin >> a >> b;
		add(a, b);
	}
	cout << bfs();
	return 0;
}

 

Dijkstra(优化版)

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n号点,则输出 −1。

#include <bits/stdc++.h>
using namespace std;

typedef pair<int, int> PII;
const int N = 1e6+10;

// 稀疏图用邻接表来存
int h[N], w[N], e[N], ne[N], idx;
int dist[N]; //1 号点到 n 号点的最短距离
bool st[N]; // 如果为true说明这个点的最短路径已经确定

int n, m;

void add(int x, int y, int c)
{
    // 有重边也不要紧,假设1->2有权重为2和3的边,再遍历到点1的时候2号点的距离会更新两次放入堆中
    // 这样堆中会有很多冗余的点,但是在弹出的时候还是会弹出最小值2+x(x为之前确定的最短路径),
    // 并标记st为true,所以下一次弹出3+x会continue不会向下执行。
	w[idx] = c;
	e[idx] = y; ne[idx] = h[x]; h[x] = idx ++;
}

int dijkstra()
{
	memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;
    
    //首先小根堆是根据距离来排的,所以有一个变量要是距离,
    // 其次在从堆中拿出来的时候要知道知道这个点是哪个点,不然怎么更新邻接点呢?所以第二个变量要存点
	priority_queue<PII, vector<PII>, greater<PII>> heap;
	heap.push({0, 1}); //距离+点  // 这个顺序不能倒,pair排序时是先根据first,再根据second,
	
	while (heap.size())
	{
		PII k = heap.top(); // 取不在集合S中距离最短的点
		heap.pop();
		int distance = k.first, ver = k.second;
		if (st[ver]) continue;
		st[ver] = true;
		
		for (int i=h[ver]; i != -1; i=ne[i])
		{
			int j = e[i]; // i只是个下标,e中在存的是i这个下标对应的点。
			if (dist[j] > dist[ver] + w[i])
			{
				dist[j] = dist[ver] + w[i];
				heap.push({dist[j], j});
			}
		}
	}
	if (dist[n] == 0x3f3f3f3f) return -1;
	else return dist[n];
}



int main()
{
	memset(h, -1, sizeof h);
	cin >> n >> m;
	
	while (m --)
	{
		int x, y, c;
		cin >> x >> y >> c;
		add(x, y, c);
	}
	cout << dijkstra() << endl;
	
	return 0;
}

 

Floyd求最短路

给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数。

再给定k个询问,每个询问包含两个整数x和y,表示查询从点x到点y的最短距离,如果路径不存在,则输出“impossible”。

数据保证图中不存在负权回路

#include <iostream>
using namespace std;

const int N = 210, M = 2e+10, INF = 1e9;

int n, m, k, x, y, z;
int d[N][N];

void floyd() {
    for(int k = 1; k <= n; k++)
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

int main() {
    cin >> n >> m >> k;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
            if(i == j) d[i][j] = 0;
            else d[i][j] = INF;
    while(m--) {
        cin >> x >> y >> z;
        d[x][y] = min(d[x][y], z);
        //注意保存最小的边
    }
    floyd();
    while(k--) {
        cin >> x >> y;
        if(d[x][y] > INF/2) puts("impossible");
        //由于有负权边存在所以约大过INF/2也很合理
        else cout << d[x][y] << endl;
    }
    return 0;
}

 

DP

01背包

有 N件物品和一个容量是 V的背包。每件物品只能使用一次

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

#include <iostream>
#include <algorithm>

using namespace std;

const int MAX = 1005;
int v[MAX];
int w[MAX];
int f[MAX];

int main()
{
    int n,m;
    cin >> n >> m;
    for (int i=1; i<=n; i++)
        cin >> v[i] >> w[i];
    
    for (int i=1; i<=n; i++)
        for (int j=m; j>=v[i]; j--)
        {
           f[j] = max(f[j],f[j-v[i]]+w[i]);
        }
    
    cout << f[m] << endl;
    return 0;
}

 

完全背包

有 N种物品和一个容量是 V 的背包,每种物品都有无限件可用

第 i 种物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main()
{
    cin >> n >> m;
    for (int i=1; i<=n; i++) cin >> v[i] >> w[i];
    
    for (int i=1; i<=n; i++)
        for (int j=v[i]; j<=m; j++)
            f[j] = max(f[j], f[j-v[i]]+w[i]);
            
    cout << f[m] << endl;
    
    return 0;
}

 

多重背包问题

有 N 种物品和一个容量是 V 的背包。

第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;

int n, m;
int v[N], w[N], s[N];
int f[N][N];

int main()
{
    cin >> n >> m;
    
    for (int i=1; i<=n; i++) cin >> v[i] >> w[i] >> s[i];
    
    for (int i=1; i<=n; i++)
        for (int j=0; j<=m; j++)
            for (int k=0; k<=s[i] && k*v[i] <= j; k++)
                f[i][j] = max(f[i][j], f[i-1][j-v[i]*k] + w[i]*k);
                
    cout << f[n][m] << endl;
    return 0;
}

 

线性DP(最长上升子序列)

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
int a[N];
int q[N];

int main()
{
    scanf("%d", &n);
    for (int i=0; i<n; i++) scanf("%d", &a[i]);
    
    int len = 0;
    for (int i=0; i<n; i++)
    {
        int l=0, r = len;
        while (l < r)
        {   //二分
            int mid  = l + r + 1 >> 1;
            if (q[mid] < a[i]) l = mid;
            else r = mid - 1;
        }
        len = max(len, r+1);
        q[r+1] = a[i];
    }
    printf("%d\n", len);
    
    return 0;
}

 

线性DP(最长公共子序列)

给定两个长度分别为 N 和 M 的字符串 A 和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少。

输入格式

第一行包含两个整数 N 和 M。

第二行包含一个长度为 N 的字符串,表示字符串 A。

第三行包含一个长度为 M 的字符串,表示字符串 B。

字符串均由小写字母构成。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N,M≤1000

输入样例

4 5
acbd
abedc

输出样例

3

代码

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];

int main()
{
    scanf("%d%d", &n, &m);
    scanf("%s%s", a+1, b+1);
    
    for (int i=1; i<=n; i++)
        for (int j=1; j<=m; j++)
        {
            f[i][j] = max(f[i-1][j], f[i][j-1]);
            if (a[i] == b[j]) f[i][j] = max(f[i][j], f[i-1][j-1]+1);
        }
    printf("%d\n",f[n][m]);
    
    return 0;   
}

 

区间DP(石子合并)

设有 N 堆石子排成一排,其编号为 1,2,3,…,N。

每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2 堆,代价为 4,得到 4 5 2, 又合并 1、2 堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;

如果第二步是先合并 2、3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。

问题是:找出一种合理的方法,使总的代价最小,输出最小代价。

模板

for (int len = 1; len <= n; len++) {         // 区间长度
    for (int i = 1; i + len - 1 <= n; i++) { // 枚举起点
        int j = i + len - 1;                 // 区间终点
        if (len == 1) {
            dp[i][j] = 初始值
            continue;
        }

        for (int k = i; k < j; k++) {        // 枚举分割点,构造状态转移方程
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + w[i][j]);
        }
    }
}

 

#include <iostream>
#include <cstring>

using namespace std;

const int N = 307;

int a[N], s[N];
int f[N][N];

int main() {
    int n;
    cin >> n;

    for (int i = 1; i <= n; i ++) {
        cin >> a[i];
        s[i] += s[i - 1] + a[i];
    }

    memset(f, 0x3f, sizeof f);
    // 区间 DP 枚举套路:长度+左端点 
    for (int len = 1; len <= n; len ++) { // len表示[i, j]的元素个数
        for (int i = 1; i + len - 1 <= n; i ++) {
            int j = i + len - 1; // 自动得到右端点
            if (len == 1) {
                f[i][j] = 0;  // 边界初始化
                continue;
            }

            for (int k = i; k <= j - 1; k ++) { // 必须满足k + 1 <= j
                f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
            }
        }
    }

    cout << f[1][n] << endl;


    return 0;
}

 

线性DP(数字矩阵和)

小蓝有一个 3 行 6 列的数字矩阵,矩阵中的每个数都是 0 到 9 之间的数字。现在小蓝想从这个矩阵的第一行第一列画一条折线到第 3 行 6 列,线只能沿水平向右走或竖直向下走,只能在有数字的地方拐弯。小蓝想知道,这样一条线经过的数字的和最大是多少。

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 35, M = 65;

int n = 30, m = 60;
char g[N][M];
int f[N][M];

int main()
{
for(int i = 1; i <= n; i ++) scanf("%s", g[i] + 1);

// cout << "?" << endl;

for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
{
f[i][j] = max(f[i - 1][j], f[i][j - 1]) + g[i][j] - '0';
}
cout << f[n][m] << endl;

return 0;
}

 

记忆化搜索(滑雪)

小蓝准备在一个空旷的场地里面滑行,这个场地的高度不一,小蓝用一个 n 行 m 列的矩阵来表示场地,矩阵中的数值表示场地的高度。
如果小蓝在某个位置,而他上、下、左、右中有一个位置的高度(严格)低于当前的高度,小蓝就可以滑过去,滑动距离为 1 。
如果小蓝在某个位置,而他上、下、左、右中所有位置的高度都大于等于当前的高度,小蓝的滑行就结束了。
小蓝不能滑出矩阵所表示的场地。
小蓝可以任意选择一个位置开始滑行,请问小蓝最多能滑行多远距离。

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 110;
int n, m;
int g[N][N], f[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1}; //dx上下 dy左右  

int dp(int x, int y) //dfs
{
	int &v = f[x][y];
	if (v != -1) return v;
	v = 1;
	for (int i=0; i<4; i++)
	{
		int a = x + dx[i], b = y + dy[i];
		if (a >= 1 && a <= n && b >= 1 && b <= m && g[x][y] > g[a][b])
			v = max(v, dp(a, b)+1); //dp(a, b)+1 表示从(a,b)到下一个点,中间加一
	}
	return v;
}

int main()
{
	cin >> n >> m;
	for (int i=1; i<=n; i++)
		for (int j=1; j<=m; j++)
			cin >> g[i][j];
			
	memset(f, -1, sizeof f);
	
	int res = 0;
	for (int i=1; i<=n; i++)
		for (int j=1; j<=m; j++)
			res = max(res, dp(i, j));
	cout << res;
}

 

数论

分解质因数

给定 n 个正整数 ai,判定每个数是否是质数。

#include <iostream>
#include <algorithm>

using namespace std;

bool is_prime(int x)
{
    if (x < 2) return false;
    for (int i=2; i<=x/i; i++)
    {
        if (x%i ==  0) return false;
    }
    return true;
}

int main()
{
    int n;
    cin >> n;
    while (n --)
    {
        int x;
        cin >> x;
        if (is_prime(x)) puts("Yes");
        else puts("No");
    }
    return 0;
}

 

判断闰年

#include<iostream>
using namespace std;

int main()
{
	int year;
	cin>>year;	//键盘中输入一个年份,保存到变量year中

	if((year%4 == 0 && year%100 != 0)|| year%400 == 0)//指定是否为闰年的判断条件
		cout<< year << "是闰年" << endl;	//条件成立则该年份是闰年
	else
		cout << year << "不是闰年" << endl;	//否则该年份不是闰年
	return 0;
}

 

进制转换(转换成10进制)

int convert(string s)
{
    //base为进制
	int sum = 0;
	for(int i = 0; i < s.size(); i ++)
  		sum = sum * base + s[i] - '0';
	return sum;
}

 

进制转换(10进制转换成 base 进制)

#include<bits/stdc++.h>
using namespace std;
int a,b;
char d[16]={'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
//0~16对应的编码

void jz(int k,int base)//将k转化为n进制数
{
    int r;
    r = k % base;//对n求余
    k = k / base;//除n
    if(k != 0)//k==0为边界条件
        jz(k, base);//递归
    cout << d[r];//输出对应编码
    return;
}
int main()
{
    cin>>a>>b;
    jz(a, b);
    return 0;
}

 

求最大公约数

#include <iostream>
#include <algorithm>
using namespace std;
int gcd(int a, int b)
{
    return b ? gcd(b, a%b) : a;
}
int main() 
{
    int a, b;
    cin >> a >> b;
    cout<< gcd(a, b);
} 

 

求公约数及个数

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

vector<int> get_divistors(int x) //求约数
{
    vector<int> res;
    for (int i=1; i<=x/i; i++)
        if (x % i == 0)
        {
            res.push_back(i);
            if (i != x/i) res.push_back(x/i);
        }
    sort(res.begin(), res.end());
    return res;
}

int main()
{
    int n;
    cin >> n;
    while (n --)
    {
        int x;
        cin >> x;
        auto res = get_divistors(x);
        for (auto x : res) cout << x << " "; //遍历存取约数的数组
        cout << endl;
        cout << res.size(); //约数个数
    }

    return 0;
}

 

--> 前言基础算法模板 – Echo's blog (liveout.cn)并查集一共有 n个数,编号是 1∼n,最开始每个数各自在一个集合中。现在要进行 m个操作,操作共有两种:M a b,将编号为 a和 b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;Q a b,询问编号为 和 b 的两个数是否在同一个集合中;#include<iostream>u...

前言

 

通常查看目录的完整路径需要 pwd 命令,但是我想要直接在终端左侧显示,即如下样子

[root@VM-12-5-centos /opt/C/test]$

 

具体操作

编辑 profile 文件

vim /etc/profile

 

最后一行插入 export PS1='[\u@\h $PWD]$'

//光标最后一行
shift + g 

//编辑模式
i

//输入此命令
export PS1='[\u@\h $PWD]$'

 

刷新环境变量

source /etc/profile

 

补充

显示绝对路径

export PS1='[\u@\h $PWD]$'

只列出最后一个目录

export PS1='[\u@\h \W]$'

显示完整工作目录,当前用户目录会以$代替

export PS1='[\u@\h \w]$' 

 

--> 前言 通常查看目录的完整路径需要 pwd 命令,但是我想要直接在终端左侧显示,即如下样子[root@VM-12-5-centos /opt/C/test]$ 具体操作编辑 profile 文件vim /etc/profile 最后一行插入 export PS1='[\u@\h $PWD]$'//光标最后一行shift + g //编辑模式i//输入此命令expo...