ESP32--S3 基础实验6-8

接上集,咱们继续来学习esp32s3基础实验部分,这一部分需要我们了解大量函数使用需要反复记忆,可能遇到看了一遍只能记得有个什么样的函数可以实现相关功能但是却不知道具体是哪个函数,这样一来容易记忆错乱,所以需要我们这一块反复记忆,反复理解相关代码为什么这样写,其中内在的逻辑思维是什么样子的,这是之后的一个重要步骤,加油!

6.步进电机实验

6.1实验目的

使用按键控制28BYJ48步进电机正反转和加减速。

6.2实验介绍

6.3硬件介绍

6.4代码介绍

cpp 复制代码
main.ino
* 实验名称:步进电机实验
 * 
 * 接线说明:电机模块-->ESP32 IO
 *         (IN1-IN4)-->(9,10,11,12)
 *         
 *         电机模块输出-->28BYJ-48步进电机
 *         (5V)-->红线
 *         (O1)-->依次排序
 *         
 *         按键模块-->ESP32 IO
 *         (K1-K4)-->(1,38,39,40)
 * 
 * 实验现象:程序下载成功后,当按下KEY1键可调节电机旋转方向;当按下KEY2键,电机加速;
            当按下KEY3键,电机减速;
 * 
 * 注意事项:
 * 
 */

#include "public.h"
#include "key.h"
#include "step_motor.h"


//定义全局变量
u8 g_key=0;
u8 g_dir=0;//默认逆时针方向
u8 g_speed=STEPMOTOR_MAXSPEED;//默认最大速度旋转
u8 g_step=0;

void setup() {
  key_init();
  step_motor_init();
}

void loop() {
  g_key=key_scan(0);
  if(g_key==KEY1_PRESS)//换向
  {
    g_dir=!g_dir;    
  }
  else if(g_key==KEY2_PRESS)//加速
  {
    if(g_speed>STEPMOTOR_MAXSPEED)
      g_speed-=1;     
  }
  else if(g_key==KEY3_PRESS)//减速
  {
    if(g_speed<STEPMOTOR_MINSPEED)
      g_speed+=1;     
  }
  step_motor_28BYJ48_send_pulse(g_step++,g_dir);
  if(g_step==8)g_step=0;    
  delay(g_speed);
}
cpp 复制代码
key.cpp
#include "key.h"


//端口初始化
void key_init(void)
{
  pinMode(key1_pin, INPUT_PULLUP);//设置引脚为输入上拉模式
  pinMode(key2_pin, INPUT_PULLUP);
  pinMode(key3_pin, INPUT_PULLUP);
  pinMode(key4_pin, INPUT_PULLUP);
}

/*******************************************************************************
* 函 数 名       : key_scan
* 函数功能        : 检测独立按键是否按下,按下则返回对应键值
* 输    入       : mode=0:单次扫描按键
                  mode=1:连续扫描按键
* 输    出       : KEY1_PRESS:K1按下
                  KEY2_PRESS:K2按下
                  KEY3_PRESS:K3按下
                  KEY4_PRESS:K4按下
                  KEY_UNPRESS:未有按键按下
*******************************************************************************/
u8 key_scan(u8 mode)
{
  static u8 key=1;

  if(mode)key=1;//连续扫描按键
  if(key==1&&(digitalRead(key1_pin)==0||digitalRead(key2_pin)==0||digitalRead(key3_pin)==0||digitalRead(key4_pin)==0))//任意按键按下
  {
    delay(10);//消抖
    key=0;
    if(digitalRead(key1_pin)==0)
      return KEY1_PRESS;
    else if(digitalRead(key2_pin)==0)
      return KEY2_PRESS;
    else if(digitalRead(key3_pin)==0)
      return KEY3_PRESS;
    else if(digitalRead(key4_pin)==0)
      return KEY4_PRESS;  
  }
  else if(digitalRead(key1_pin)==1&&digitalRead(key2_pin)==1&&digitalRead(key3_pin)==1&&digitalRead(key4_pin)==1) //无按键按下
  {
    key=1;      
  }
  return KEY_UNPRESS;   
}
cpp 复制代码
key.h
#ifndef _key_H
#define _key_H

#include "public.h"

//定义按键控制管脚
#define key1_pin  1
#define key2_pin  38
#define key3_pin  39
#define key4_pin  40

//使用宏定义独立按键按下的键值
#define KEY1_PRESS  1
#define KEY2_PRESS  2
#define KEY3_PRESS  3
#define KEY4_PRESS  4
#define KEY_UNPRESS 0 

//函数声明
void key_init(void);
u8 key_scan(u8 mode);

#endif
cpp 复制代码
set_motor.cpp
#include "step_motor.h"

//端口初始化
void step_motor_init(void)
{
  pinMode(ina_pin, OUTPUT);//设置引脚为输出模式
  pinMode(inb_pin, OUTPUT);
  pinMode(inc_pin, OUTPUT);
  pinMode(ind_pin, OUTPUT);
}

/*******************************************************************************
* 函 数 名       : step_motor_28BYJ48_send_pulse
* 函数功能       : 输出一个数据给ULN2003从而实现向步进电机发送一个脉冲
* 输    入       : step:指定步进序号,可选值0~7
                  dir:方向选择,1:顺时针,0:逆时针
* 输    出       : 无
*******************************************************************************/
void step_motor_28BYJ48_send_pulse(u8 step,u8 dir)
{
  u8 temp=step;
  
  if(dir==0)  //如果为逆时针旋转
    temp=7-step;//调换节拍信号
  switch(temp)//8个节拍控制:A->AB->B->BC->C->CD->D->DA
  {
    case 0: digitalWrite(ina_pin,1);digitalWrite(inb_pin,0);digitalWrite(inc_pin,0);digitalWrite(ind_pin,0);break;
    case 1: digitalWrite(ina_pin,1);digitalWrite(inb_pin,1);digitalWrite(inc_pin,0);digitalWrite(ind_pin,0);break;
    case 2: digitalWrite(ina_pin,0);digitalWrite(inb_pin,1);digitalWrite(inc_pin,0);digitalWrite(ind_pin,0);break;
    case 3: digitalWrite(ina_pin,0);digitalWrite(inb_pin,1);digitalWrite(inc_pin,1);digitalWrite(ind_pin,0);break;
    case 4: digitalWrite(ina_pin,0);digitalWrite(inb_pin,0);digitalWrite(inc_pin,1);digitalWrite(ind_pin,0);break;
    case 5: digitalWrite(ina_pin,0);digitalWrite(inb_pin,0);digitalWrite(inc_pin,1);digitalWrite(ind_pin,1);break;
    case 6: digitalWrite(ina_pin,0);digitalWrite(inb_pin,0);digitalWrite(inc_pin,0);digitalWrite(ind_pin,1);break;
    case 7: digitalWrite(ina_pin,1);digitalWrite(inb_pin,0);digitalWrite(inc_pin,0);digitalWrite(ind_pin,1);break;
    default: digitalWrite(ina_pin,0);digitalWrite(inb_pin,0);digitalWrite(inc_pin,0);digitalWrite(ind_pin,0);break;//停止相序 
  }     
}
cpp 复制代码
set_motor.h
#ifndef _step_motor_H
#define _step_motor_H

#include "public.h"

//定义步进电机控制管脚
#define ina_pin   9
#define inb_pin   10
#define inc_pin   11
#define ind_pin   12

// 定义步进电机速度,值越小,速度越快
// 最小不能小于1
#define STEPMOTOR_MAXSPEED        1  
#define STEPMOTOR_MINSPEED        5 


//函数声明
void step_motor_init(void);
void step_motor_28BYJ48_send_pulse(u8 step,u8 dir);

#endif

7.外部中断实验

7.1实验目的

使用外部中断功能实现按键控制LED亮灭。

7.2实验介绍

ESP32的外部中断有上升沿、下降沿、低电平、高电平触发模式。上升沿和下降沿触发如下:

7.3Arduino函数使用

7.3.1attachInterrupt()函数
cpp 复制代码
void attachInterrupt(uint8_t interruptPin, void (*userFunction)(void), uint8_t mode)

它需要三个参数:

  1. interruptPin (中断引脚号)

    • 指定哪个引脚用来触发中断。注意 :不是所有Arduino引脚都支持中断功能。对于最常见的Uno/Nano,只有引脚 2 ​ 和 3​ 支持。
  2. userFunction (用户函数)

    • 当中断被触发时,你想要自动执行的函数。这个函数必须没有参数,也不返回任何值 ​ (void)。

    • 例如:void myInterruptHandler() { ... }

  3. mode (触发模式)

    • 这是图中详细解释的部分,决定了在什么电平变化条件下触发中断。共有五种模式:

总的来说,attachInterrupt()是实现Arduino快速响应外部异步事件的标准方法。你只需根据图中所示的模式选择合适的触发条件,并遵循"快速设置标志,主循环处理"的最佳实践,就能让程序高效、可靠地处理如按键、编码器、传感器脉冲等实时信号。

7.3.2detachInterrupt()函数

detachInterrupt()是 attachInterrupt()的配对函数,用于取消中断的绑定,让指定引脚不再触发中断。

cpp 复制代码
void detachInterrupt(uint8_t interruptPin)

7.4连线情况

7.5代码介绍

cpp 复制代码
 main.ino
* 实验名称:外部中断实验
 * 
 * 接线说明:按键模块-->ESP32 IO
 *         (K1-K4)-->(1,38,39,40)
 *         
 *         LED模块-->ESP32 IO
 *         (D1)-->(3)
 * 
 * 实验现象:程序下载成功后,操作K1-K4键控制D1指示灯亮灭;
 * 
 * 注意事项:
 * 
 */

#include "public.h"
#include "exti.h"


//定义LED控制引脚
#define led1_pin  3

//定义全局变量

void setup() {
  pinMode(led1_pin, OUTPUT);//设置引脚为输出模式
  exti_init();
}

void loop() {
  digitalWrite(led1_pin,led1_sta);
}
cpp 复制代码
exti.cpp
#include "exti.h"

volatile u8 led1_sta=0;

//端口初始化
void exti_init(void)
{
  pinMode(key1_pin, INPUT_PULLUP);//设置引脚为输入上拉模式
  pinMode(key2_pin, INPUT_PULLUP);
  pinMode(key3_pin, INPUT_PULLUP);
  pinMode(key4_pin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(key1_pin), key1_isr, FALLING);//设置下降沿触发
  attachInterrupt(digitalPinToInterrupt(key2_pin), key2_isr, FALLING);
  attachInterrupt(digitalPinToInterrupt(key3_pin), key3_isr, FALLING);
  attachInterrupt(digitalPinToInterrupt(key4_pin), key4_isr, FALLING);
}

void key1_isr(void)
{
  delay(10);
  led1_sta=!led1_sta;
}

void key2_isr(void)
{
  delay(10);
  led1_sta=!led1_sta;
}

void key3_isr(void)
{
  delay(10);
  led1_sta=!led1_sta;
}

void key4_isr(void)
{
  delay(10);
  led1_sta=!led1_sta;
}
cpp 复制代码
exti.h
#ifndef _exti_H
#define _exti_H

#include "public.h"

//定义按键控制管脚
#define key1_pin  1
#define key2_pin  38
#define key3_pin  39
#define key4_pin  40

//变量声明
extern volatile u8 led1_sta;

//函数声明
void exti_init(void);
void key1_isr(void);
void key2_isr(void);
void key3_isr(void);
void key4_isr(void);
#endif
cpp 复制代码
public.h
#ifndef _public_H
#define _public_H

#include "Arduino.h"

//类型重定义
typedef unsigned char u8;
typedef unsigned int u16;

#endif

8.定时器中断实验

8.1实验目的

通过定时器让 LED 周期性每秒闪烁 1 次。

8.2定时器配置步骤

①选择定时器(两组四个)

②配置合适分频系数

③绑定中断函数

④配置报警计数器保护值

⑤开启报警

8.3Arduino函数使用

8.3.1timerBegin() 函数
cpp 复制代码
hw_timer_t * timerBegin(uint8_t num, uint16_t divider, bool countUp)
cpp 复制代码
hw_timer_t* tim1 = NULL;
tim1 = timerBegin(0, 80, true); // 80MHz, ESP32主频80MHz

参数解读:

  • 0:使用定时器0

  • 80:分频系数为80

  • true:采用向上计数模式

定时器频率计算

重要公式

复制代码
定时器频率 = 主频 / 分频系数

以ESP32主频80MHz为例:

  • 分频系数80时:80,000,000 Hz ÷ 80 = 1,000,000 Hz(1MHz)

  • 这意味着定时器每**1微秒(µs)**​ 计数一次

通过合理使用timerBegin()函数,你可以在ESP32上实现精确的定时控制,适用于数据采集、PWM生成、实时控制等各种需要精确时间基准的应用场景。

8.3.2timerEnd()函数
cpp 复制代码
void timerEnd(hw_timer_t *timer)
功能说明

timerEnd()函数用于停止并释放ESP32硬件定时器资源。当定时器不再需要时调用此函数,可以:

  1. 停止定时器计数

  2. 禁用定时器中断

  3. 释放定时器硬件资源,使其可供其他任务使用

使用上述两个函数的流程代码

cpp 复制代码
#include "driver/timer.h"

hw_timer_t *myTimer = NULL;  // 定时器指针

void setup() {
  // 1. 创建并配置定时器
  myTimer = timerBegin(0, 80, true);  // 使用定时器0
  timerAttachInterrupt(myTimer, &timerISR, true);
  timerAlarmWrite(myTimer, 1000000, true);  // 1秒间隔
  timerAlarmEnable(myTimer);
  
  Serial.println("定时器已启动");
}

void loop() {
  // 主循环逻辑
  
  // 当需要停止定时器时
  if (someCondition) {
    stopTimer();
  }
}

// 停止定时器的函数
void stopTimer() {
  if (myTimer != NULL) {
    // 2. 停止并释放定时器
    timerEnd(myTimer);
    myTimer = NULL;  // 重要:将指针置为NULL,避免野指针
    
    Serial.println("定时器已停止并释放");
  }
}

// 定时器中断服务函数
void IRAM_ATTR timerISR() {
  // 定时器中断处理
}
8.3.3 timerAttachInterrupt()函数 --附加中断处理函数
cpp 复制代码
void timerAttachInterrupt(hw_timer_t *timer, void (*fn)(void), bool edge)
cpp 复制代码
调用示例
// 1. 首先创建定时器
hw_timer_t *tim1 = timerBegin(0, 80, true);

// 2. 定义中断处理函数(需简短)
void IRAM_ATTR tim1Interrupt() {
  // 中断处理代码
  digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
}

// 3. 附加中断函数到定时器
timerAttachInterrupt(tim1, tim1Interrupt, true);
重要特性
  • 中断函数必须简短,避免使用delay()或阻塞代码

  • 建议使用IRAM_ATTR属性确保函数在RAM中执行

  • 边沿触发(true)通常比电平触发(false)更可靠

8.3.4 timerDetachInterrupt() - 分离中断处理函数
cpp 复制代码
void timerDetachInterrupt(hw_timer_t *timer)
功能说明
  • 解除中断函数与定时器的绑定

  • 定时器继续运行,但不再触发中断

  • 用于临时禁用中断而不停止定时器计数

8.3.5 timerAlarmWrite() - 设置定时器报警值
cpp 复制代码
void timerAlarmWrite(hw_timer_t *timer, uint64_t alarm_value, bool autoreload)
cpp 复制代码
调用示例
// 设置tim1每100,000计数触发一次,并自动重载
timerAlarmWrite(tim1, 100000, true);
参数 autoreload参数详解
  • autoreload = true :定时器达到报警值后自动重置计数,周期性地触发中断
cpp 复制代码
// 周期定时器,每100ms触发一次
timerAlarmWrite(timer, 100000, true);  // 自动重载,无限循环

autoreload = false:定时器达到报警值后停止计数,只触发一次中断

cpp 复制代码
// 单次定时器,100ms后触发一次,然后停止
timerAlarmWrite(timer, 100000, false); // 单次触发
cpp 复制代码
完整使用代码
#include "driver/timer.h"

hw_timer_t *tim1 = NULL;
volatile bool interruptFlag = false;

// 中断服务函数 - 必须简短
void IRAM_ATTR tim1Interrupt() {
  interruptFlag = true;  // 只设置标志,快速退出
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_BUILTIN, OUTPUT);
  
  // 步骤1:创建定时器(定时器0,分频80,向上计数,1MHz频率)
  tim1 = timerBegin(0, 80, true);
  
  // 步骤2:附加中断函数(边沿触发)
  timerAttachInterrupt(tim1, tim1Interrupt, true);
  
  // 步骤3:设置报警值(100ms间隔,自动重载)
  timerAlarmWrite(tim1, 100000, true);  // 100,000微秒 = 100毫秒
  
  // 步骤4:启用定时器报警
  timerAlarmEnable(tim1);
  
  Serial.println("定时器配置完成,100ms间隔中断");
}

void loop() {
  if (interruptFlag) {
    interruptFlag = false;
    
    // 在主循环中处理中断事件
    static int counter = 0;
    counter++;
    
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));
    
    // 每10次中断(1秒)打印一次
    if (counter % 10 == 0) {
      Serial.println("1秒过去了");
    }
  }
  
  // 主循环其他任务
  // ...
}

// 如果需要修改定时器参数
void changeTimerInterval(uint64_t newInterval) {
  // 先停止定时器
  timerAlarmDisable(tim1);
  
  // 修改报警值
  timerAlarmWrite(tim1, newInterval, true);
  
  // 重新启用
  timerAlarmEnable(tim1);
  
  Serial.print("定时器间隔修改为: ");
  Serial.print(newInterval);
  Serial.println(" 微秒");
}

// 临时禁用中断
void pauseInterrupts() {
  timerDetachInterrupt(tim1);
  Serial.println("中断已禁用");
}

// 重新启用中断
void resumeInterrupts() {
  timerAttachInterrupt(tim1, tim1Interrupt, true);
  Serial.println("中断已启用");
}
8.3.6 timerAlarmEnable() - 启用定时器报警
cpp 复制代码
void timerAlarmEnable(hw_timer_t *timer)
功能说明
  • 启动定时器的报警功能,使定时器开始计数并在达到设定值时触发中断

  • 必须在调用timerAlarmWrite()设置报警值后使用

  • 是定时器开始工作的最后一步

cpp 复制代码
使用示例
hw_timer_t *tim1 = NULL;

void setup() {
  // 1. 创建定时器
  tim1 = timerBegin(0, 80, true);
  
  // 2. 附加中断函数
  timerAttachInterrupt(tim1, &timerISR, true);
  
  // 3. 设置报警值(1秒间隔)
  timerAlarmWrite(tim1, 1000000, true);
  
  // 4. 启用定时器报警 - 定时器开始工作!
  timerAlarmEnable(tim1);
  
  Serial.println("定时器已启动,每秒触发一次中断");
}
8.3.7 timerAlarmDisable() - 禁用定时器报警
cpp 复制代码
void timerAlarmDisable(hw_timer_t *timer)
功能说明
  • 暂停定时器的报警功能,定时器停止计数和触发中断

  • 不释放定时器资源,可以重新启用

  • 用于临时暂停定时器而不完全销毁它

cpp 复制代码
使用示例
// 暂停定时器
timerAlarmDisable(tim1);
Serial.println("定时器已暂停");

// ... 执行不需要定时器的任务 ...

// 恢复定时器
timerAlarmEnable(tim1);
Serial.println("定时器已恢复");
与timeend()的区别
cpp 复制代码
// timerAlarmDisable() - 临时暂停
timerAlarmDisable(tim1);  // 只是暂停,配置保留
timerAlarmEnable(tim1);   // 可以立即恢复

// timerEnd() - 完全释放
timerEnd(tim1);           // 释放所有资源
tim1 = NULL;
// 需要重新调用timerBegin()才能再次使用
8.3.8 timerAlarmEnabled() - 检查报警状态
cpp 复制代码
bool timerAlarmEnabled(hw_timer_t *timer)
功能说明
  • 查询定时器报警功能的当前状态

  • 返回true表示报警已启用,false表示已禁用

  • 用于状态检查条件判断

cpp 复制代码
使用示例
// 检查定时器状态
if (timerAlarmEnabled(tim1)) {
  Serial.println("定时器正在运行");
} else {
  Serial.println("定时器已停止");
}

// 安全地切换状态
void toggleTimer(hw_timer_t *timer) {
  if (timerAlarmEnabled(timer)) {
    timerAlarmDisable(timer);
    Serial.println("已停止定时器");
  } else {
    timerAlarmEnable(timer);
    Serial.println("已启动定时器");
  }
}

总结

8.4接线情况

8.5代码介绍

cpp 复制代码
main.ino
* 实验名称:定时器中断实验
 * 
 * 接线说明:LED模块-->ESP32 IO
 *         (D1)-->(3)
 * 
 * 实验现象:程序下载成功后,D1指示灯间隔0.5s状态翻转
 * 
 * 注意事项:
 * 
 */

#include "public.h"
#include "led.h"
#include "time.h"


//定义全局变量


void setup() {
  led_init();
  time0_init(500000);//定时500ms
  
}

void loop() {
  
}
cpp 复制代码
led.cpp
#include "led.h"

//led初始化
void led_init(void)
{
  pinMode(led1_pin, OUTPUT);//设置引脚为输出模式
}
cpp 复制代码
led.h
#ifndef _led_H
#define _led_H

#include "public.h"

//LED管脚定义
#define led1_pin  3

//函数声明
void led_init(void);

#endif
cpp 复制代码
public.h
#ifndef _public_H
#define _public_H

#include "Arduino.h"

//类型重定义
typedef unsigned char u8;
typedef unsigned int u16;
typedef unsigned long u32;

#endif
cpp 复制代码
time.cpp
#include "time.h"
#include "led.h"

hw_timer_t *timer0 = NULL;

//定时器初始化
//per:定时时间,单位us
void time0_init(u32 per)
{
  /* timerBegin:初始化定时器指针
    第一个参数:设置定时器0(一共有四个定时器0、1、2、3)
    第二个参数:80分频(设置APB时钟,ESP32主频80MHz),80则时间单位为1Mhz即1us,1000000us即1s。
    第三个参数:计数方式,true向上计数 false向下计数
 */
  timer0 = timerBegin(0, 80, true);
  /* timerAlarmWrite:配置报警计数器保护值(就是设置时间)
     第一个参数:指向已初始化定时器的指针
     第二个参数:定时时间,这里为500000us  意思为0.5s进入一次中断
     第三个参数:是否重载,false定时器中断触发一次  true:死循环
  */
  timerAlarmWrite(timer0, per, true);
  /* timerAttachInterrupt:绑定定时器
     第一个参数:指向已初始化定时器的指针
     第二个参数:中断服务器函数
     第三个参数:true边沿触发,false电平触发
  */
  timerAttachInterrupt(timer0, &time0_isr, true); 
  timerAlarmEnable(timer0);//启用定时器
  //timerDetachInterrupt(timer0);//关闭定时器
  
}

//定时器中断函数
void time0_isr(void)
{
  static u8 led_sta=0; 
  led_sta=!led_sta;
  digitalWrite(led1_pin,led_sta);
}
cpp 复制代码
time.h
#ifndef _time_H
#define _time_H

#include "public.h"

//变量声明
extern hw_timer_t *timer0;

//函数声明
void time0_init(u32 per);
void time0_isr(void);

#endif
相关推荐
帅次8 小时前
系统分析师-信息物理系统分析与设计
stm32·单片机·嵌入式硬件·mcu·物联网·iot·rtdbs
澜莲Alice9 小时前
STM32 MPLAB X IDE 软件安装-玩转单片机-英文版沉浸式安装
stm32·单片机·嵌入式硬件
良许Linux9 小时前
IIC总线的硬件部分的两个关键点:开漏输出+上拉电阻
单片机·嵌入式硬件
✎ ﹏梦醒͜ღ҉繁华落℘9 小时前
单片机基础知识 -- ADC分辨率
单片机·嵌入式硬件
雾削木10 小时前
树莓派部署 HomeAssistant 教程
stm32·单片机·嵌入式硬件
Q_219327645510 小时前
基于单片机的破壁机自动控制系统设计
单片机·嵌入式硬件
我是一棵无人问荆的小草10 小时前
stm32f103芯片多个IO配置成外部中断
stm32·单片机·嵌入式硬件
wjykp11 小时前
ESP32xxx烧录
stm32·单片机·嵌入式硬件
早起huo杯黑咖啡12 小时前
【NOR Flash】关于芯片的高耐久性分区的编程/擦除周期和最小保留时间的数据
单片机·嵌入式硬件
来可电子CAN青年12 小时前
《工业级 CAN 环网冗余架构设计与光纤长距离传输实践》
经验分享·笔记·单片机·物联网·网络协议