ESP32 I2S音频总线学习笔记(四): INMP441采集音频并实时播放

简介

前面两期文章我们介绍了I2S的读取和写入,一个是通过INMP441麦克风模块采集音频,一个是通过PCM5102A模块播放音频,那如果我们将两者结合起来,将麦克风采集到的音频通过PCM5102A播放,是不是就可以做一个扩音器了呢,本篇将介绍一个INMP441采集音频并实时播放的应用。

往期相关文章:

ESP32 I2S音频总线学习笔记(一):初识I2S通信与配置基础

ESP32 I2S音频总线学习笔记(二):I2S读取INMP441音频数据

ESP32 I2S音频总线学习笔记(三):I2S音频输出

主要硬件

INMP441全向麦克风模块:
在这里插入图片描述
PCM5102A 立体声DAC模块 :在这里插入图片描述

硬件接线

ESP32和麦克风INMP441:

ESP32INMP441
D13SCK
D12WS
D14SD
3.3VVDD
GNDGND

ESP32和PCM5102A:

ESP32PCM5102A
-VCC
3.3V3.3V
GNDGND
GNDFLT、DMP、SCL (这里SCL悬空可能会有干扰,所以接地)
D27BCK
D25DIN
D26LCK
GNDFMT
3.3VXMT

软件实现

前面两篇文章我们详细介绍了I2S读取和I2S输出的初始化步骤,所以本篇我们就不过多介绍了。我们的目的是实现INMP441采集音频并通过PCM5102A实时播放(可替换为其他DAC模块如MAX98357),使用的协议是I2S,所以首先要分别配置麦克风I2S初始化音频播放模块I2S初始化,前者我们起名为setupI2SMic( ) ,后者起名为setupI2SDac( );因为我们是循环采集音频,所以音频处理逻辑部分放在loop( )函数,首先搭建起整体框架,注意必不可少的是包含I2S驱动头文件:

#include <driver/i2s.h>void setup() {Serial.begin(115200);setupI2SMic();setupI2SDac();
}void loop()
{/*音频处理逻辑部分...待补充*/
}

搭建完整体框架后,我们再来完善setupI2SMic( )和setupI2SDac( )里面的内容。

INMP441读取 I2S初始化

setupI2SMic( )里面的I2S初始化步骤如何配置,可参考往期第二篇文章。这里因为使用到了两个I2S,一个用于麦克风采集,一个用于播放音频,所以我们将I2S0用于麦克风,I2S1用于音频的实时播放。

// 配置 I2S0 用于麦克风采集
#define I2S_MIC_NUM    I2S_NUM_0
#define I2S_MIC_BCK 13
#define I2S_MIC_WS  12
#define I2S_MIC_SD  14#define SAMPLE_RATE    44100void setupI2SMic() {i2s_config_t mic_config = {.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),.sample_rate = SAMPLE_RATE,.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // 单声道.communication_format = I2S_COMM_FORMAT_I2S,.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,.dma_buf_count = 8,  // 增加 DMA 缓冲区数量.dma_buf_len = BUFFER_SIZE};i2s_pin_config_t mic_pin_config = {.bck_io_num = I2S_MIC_BCK,.ws_io_num = I2S_MIC_WS,.data_out_num = -1,.data_in_num = I2S_MIC_SD};i2s_driver_install(I2S_MIC_NUM, &mic_config, 0, NULL);i2s_set_pin(I2S_MIC_NUM, &mic_pin_config);
}

PCM5102A输出 I2S初始化

// 配置 I2S1 用于 DAC 输出
#define I2S_DAC_NUM    I2S_NUM_1
#define I2S_DAC_BCK 27
#define I2S_DAC_WS  26
#define I2S_DAC_DIN  25void setupI2SDac() {i2s_config_t dac_config = {.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),.sample_rate = SAMPLE_RATE,.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, // 立体声.communication_format = I2S_COMM_FORMAT_I2S,.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,.dma_buf_count = 8,.dma_buf_len = BUFFER_SIZE};i2s_pin_config_t dac_pin_config = {.bck_io_num = I2S_DAC_BCK,.ws_io_num = I2S_DAC_WS,.data_out_num = I2S_DAC_DIN,.data_in_num = -1};i2s_driver_install(I2S_DAC_NUM, &dac_config, 0, NULL);i2s_set_pin(I2S_DAC_NUM, &dac_pin_config);
}

音频处理逻辑部分

音频处理逻辑部分主要步骤是从麦克风采集数据,然后播放音频数据,需要用到前面讲过的两个函数:esp_err_t i2s_read(i2s_port_t i2s_num, void *dest, size_t size, size_t *bytes_read, TickType_t ticks_to_wait);esp_err_t i2s_write(i2s_port_t i2s_num, const void *src, size_t size, size_t *bytes_written, TickType_t ticks_to_wait);
除此之外,因为麦克风是单声道的,音频播放模块是双声道的,需要将单声道转化为双声道音频;并且这里测试发现如果不对采集到的声音进行放大,声音是比较小的,所以还需进行音频增益处理。其处理逻辑如下:

void loop(){/*音频处理逻辑部分 *///      从麦克风采集数据//     增益处理,放大音量并限制范围//     单声道转立体声//     播放音频数据
}

首先定义两个音频缓冲区,buffer 用于单声道输入,stereo_buffer 用于立体声输出。BUFFER_SIZE定义为单声道缓冲区大小1024,每次读取1024个样本,双声道缓存区的样本数是单声道缓存区样本数的两倍,所以为BUFFER_SIZE * 2。

#define BUFFER_SIZE    1024int16_t buffer[BUFFER_SIZE];          // 单声道缓冲区
int16_t stereo_buffer[BUFFER_SIZE * 2]; // 立体声缓冲区

然后从麦克风采集数据:

i2s_read(I2S_MIC_NUM, buffer, BUFFER_SIZE * sizeof(int16_t), &bytesRead, portMAX_DELAY);

将采集到的音频数据进行增益处理,这里实测增益因子20~50比较合适,太大了采集到的音频声音会大,但是近距离说话的时候会容易出现破音,对远处的采集声音较好。溢出保护是因为16 位音频范围为 -32768 到 32767,放大后若超出此范围就会导致失真。

 for (int i = 0; i < BUFFER_SIZE; i++) {buffer[i] = buffer[i] * 20;  // 增益因子为20,可以根据需要调整// 增加溢出保护if (buffer[i] > 32767) buffer[i] = 32767;if (buffer[i] < -32768) buffer[i] = -32768;}

放大音频数据后将单声道音频数据转化为立体声 音频数据,单声道只有一个声道的数据,立体声需要左右两个声道,这里将单声道样本复制到左右声道。

 for (int i = 0; i < BUFFER_SIZE; i++) {stereo_buffer[2 * i] = buffer[i];      // 左声道stereo_buffer[2 * i + 1] = buffer[i]; // 右声道}

通过 I2S1 接口将立体声数据发送到PCM5102A播放。

i2s_write(I2S_DAC_NUM, stereo_buffer, sizeof(stereo_buffer), &bytesWritten, portMAX_DELAY);

音频处理逻辑部分的代码如下,这个步骤可以总结为:1. 采集单声道音频 → 2. 放大音量并限制范围 → 3. 转换为立体声 → 4. 播放

void loop() {int16_t buffer[BUFFER_SIZE];          // 单声道缓冲区int16_t stereo_buffer[BUFFER_SIZE * 2]; // 立体声缓冲区size_t bytesRead, bytesWritten;// 从麦克风采集数据i2s_read(I2S_MIC_NUM, buffer, BUFFER_SIZE * sizeof(int16_t), &bytesRead, portMAX_DELAY);// 增益处理,放大音量并限制范围for (int i = 0; i < BUFFER_SIZE; i++) {buffer[i] = buffer[i] * 20;  // 增益因子为20,可以根据需要调整// 增加溢出保护if (buffer[i] > 32767) buffer[i] = 32767;if (buffer[i] < -32768) buffer[i] = -32768;}// 单声道转立体声for (int i = 0; i < BUFFER_SIZE; i++) {stereo_buffer[2 * i] = buffer[i];      // 左声道stereo_buffer[2 * i + 1] = buffer[i]; // 右声道}// 播放音频数据i2s_write(I2S_DAC_NUM, stereo_buffer, sizeof(stereo_buffer), &bytesWritten, portMAX_DELAY);
}

全部整合后的代码如下:

#include <driver/i2s.h>// 配置 I2S0 用于麦克风采集
#define I2S_MIC_NUM    I2S_NUM_0
#define I2S_MIC_BCK 13
#define I2S_MIC_WS  12
#define I2S_MIC_SD  14// 配置 I2S1 用于 DAC 输出
#define I2S_DAC_NUM    I2S_NUM_1
#define I2S_DAC_BCK 27
#define I2S_DAC_WS  26
#define I2S_DAC_DIN  25#define SAMPLE_RATE    44100
#define BUFFER_SIZE    1024void setupI2SMic() {i2s_config_t mic_config = {.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),.sample_rate = SAMPLE_RATE,.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // 单声道.communication_format = I2S_COMM_FORMAT_I2S,.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,.dma_buf_count = 8,  // 增加 DMA 缓冲区数量.dma_buf_len = BUFFER_SIZE};i2s_pin_config_t mic_pin_config = {.bck_io_num = I2S_MIC_BCK,.ws_io_num = I2S_MIC_WS,.data_out_num = -1,.data_in_num = I2S_MIC_SD};i2s_driver_install(I2S_MIC_NUM, &mic_config, 0, NULL);i2s_set_pin(I2S_MIC_NUM, &mic_pin_config);
}void setupI2SDac() {i2s_config_t dac_config = {.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),.sample_rate = SAMPLE_RATE,.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, // 立体声.communication_format = I2S_COMM_FORMAT_I2S,.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,.dma_buf_count = 8,.dma_buf_len = BUFFER_SIZE};i2s_pin_config_t dac_pin_config = {.bck_io_num = I2S_DAC_BCK,.ws_io_num = I2S_DAC_WS,.data_out_num = I2S_DAC_DIN,.data_in_num = -1};i2s_driver_install(I2S_DAC_NUM, &dac_config, 0, NULL);i2s_set_pin(I2S_DAC_NUM, &dac_pin_config);
}void setup() {Serial.begin(115200);setupI2SMic();setupI2SDac();
}void loop() {int16_t buffer[BUFFER_SIZE];          // 单声道缓冲区int16_t stereo_buffer[BUFFER_SIZE * 2]; // 立体声缓冲区size_t bytesRead, bytesWritten;// 从麦克风采集数据i2s_read(I2S_MIC_NUM, buffer, BUFFER_SIZE * sizeof(int16_t), &bytesRead, portMAX_DELAY);// 增益处理,放大音量并限制范围for (int i = 0; i < BUFFER_SIZE; i++) {buffer[i] = buffer[i] * 20;  // 增益因子为20,可以根据需要调整// 增加溢出保护if (buffer[i] > 32767) buffer[i] = 32767;if (buffer[i] < -32768) buffer[i] = -32768;// }// 单声道转立体声for (int i = 0; i < BUFFER_SIZE; i++) {stereo_buffer[2 * i] = buffer[i];      // 左声道stereo_buffer[2 * i + 1] = buffer[i]; // 右声道}// 播放音频数据i2s_write(I2S_DAC_NUM, stereo_buffer, sizeof(stereo_buffer), &bytesWritten, portMAX_DELAY);
}

功能扩展

添加两个按钮,用于控制实时播放的音量。主要实现思路是通过按钮去控制增益因子的增加或减少。

首先创建一个数组用来存储增益因子,这里我们选择放大的倍数为20, 30, 40, 50, 60, 70, 80可选,然后numGains用于计算元素个数,方便我们后续的判断。并设置默认增益为20。

// 增益因子数组
const int gainFactors[] = {20, 30, 40, 50, 60, 70, 80};
const int numGains = sizeof(gainFactors) / sizeof(gainFactors[0]);
int currentGainIndex = 0;  // 默认增益为 20

整体思路是:如果按键1按下,currentGainIndex++, 如果按键2按下,currentGainIndex- -

考虑到按键抖动的情况,代码如下:

// 按键引脚
#define BUTTON_UP_PIN 33   // 增益增加按键(GPIO 33)
#define BUTTON_DOWN_PIN 32 // 增益减少按键(GPIO 32)// 按键去抖变量
unsigned long lastUpDebounceTime = 0;
unsigned long lastDownDebounceTime = 0;
const unsigned long debounceDelay = 50;void KeyUp() {// 检查增益增加按键bool upReading= digitalRead(BUTTON_UP_PIN);if (upReading!= lastUpButtonState) {lastUpDebounceTime = millis();}if ((millis() - lastUpDebounceTime) > debounceDelay) {if (upReading== LOW && currentGainIndex < numGains - 1) {  // 按下且未达最大增益currentGainIndex++;Serial.println("增益切换至: " + String(gainFactors[currentGainIndex]));delay(200);  // 防止快速切换}}lastUpButtonState = upReading;
}void KeyDn() {// 检查增益减少按键bool downReading = digitalRead(BUTTON_DOWN_PIN);if (downReading != lastDownButtonState) {lastDownDebounceTime = millis();}if ((millis() - lastDownDebounceTime) > debounceDelay) {if (downReading == LOW && currentGainIndex > 0) {  // 按下且未达最小增益currentGainIndex--;Serial.println("增益切换至: " + String(gainFactors[currentGainIndex]));delay(200);  // 防止快速切换}}lastDownButtonState = downReading;}

增加增益因子和减少增益因子的去抖机制相同,以增加为例,机械按键在按下或释放的时候会有产生短暂的电平抖动,通过去抖机制,等待一段时间(这里的debounceDelay)可以确保状态稳定,避免误判为多次按键事件。millis() 这个函数会返回程序启动后经过的毫秒数。lastUpDebounceTime = millis(); 标记按键状态变化的时间点,millis() - lastUpDebounceTime是当前时间与按键状态变化时间的差值,表示从上次状态变化以来经过的毫秒数, 用于判断按键状态是否稳定足够长时间,如果时间差值大于 debounceDelay,说明按键状态已经稳定,其效果类似于delay(50) (但是一个是阻塞等待,一个是非阻塞等待)。

主要代码是这两个, 其中numGains 是增益因子数组元素个数,因为数组是从零开始的,所以numGains个数减1就是表示数组最大索引数, 如果没达到最大索引,即还未达最大增益,currentGainIndex++。

 if (upReading== LOW && currentGainIndex < numGains - 1) {  // 按下且未达最大增益currentGainIndex++;Serial.println("增益切换至: " + String(gainFactors[currentGainIndex]));delay(200);  // 防止快速切换}

currentGainIndex > 0表示还未到最小索引,所以currentGainIndex- -

if (downReading == LOW && currentGainIndex > 0) {  // 按下且未达最小增益currentGainIndex--;Serial.println("增益切换至: " + String(gainFactors[currentGainIndex]));delay(200);  // 防止快速切换}

按键是否按下是通过判断upReading和downReading是否变为LOW实现。

为了方便控制音量,这里我还尝试了使用旋转编码器来控制音量,主要代码如下:

#include <ESP32Encoder.h>
// 编码器引脚
#define MODE_DT_PIN  32  // A 相
#define MODE_CLK_PIN 35  // B 相#define MODE_STEP    2   // 每 2 个计数触发ESP32Encoder modeEncoder;void KeyUp() {if (currentGainIndex < numGains - 1) {currentGainIndex++;Serial.println("增益切换至: " + String(gainFactors[currentGainIndex]));}
}void KeyDn() {if (currentGainIndex > 0) {currentGainIndex--;Serial.println("增益切换至: " + String(gainFactors[currentGainIndex]));}
}/*------EC11 控制函数------*/
void EC11_Control() {static int lastModeCount = 0;static unsigned long lastRotateTime = 0;int currentModeCount = modeEncoder.getCount();if (abs(currentModeCount - lastModeCount) >= MODE_STEP) {lastRotateTime = millis();if (currentModeCount > lastModeCount) {KeyUp(); // 顺时针增加增益Serial.println("向下");} else {KeyDn(); // 逆时针减少增益Serial.println("向上");}modeEncoder.setCount(0); // 重置计数lastModeCount = 0;}// 长时间无操作时输出停止if (millis() - lastRotateTime > 1000) {//Serial.println("停止旋转");lastRotateTime = millis(); }
}

理解了按键控制增益因子的原理,这里也是一样的。我们在EC11_Control()调用 KeyUp()和KeyDn()这两个函数。采用编码器的时候还需要注意在setup()函数里面添加初始化编码器的相关代码。

void setup() {
// 初始化编码器pinMode(MODE_CLK_PIN, INPUT_PULLUP);pinMode(MODE_DT_PIN, INPUT_PULLUP);modeEncoder.attachHalfQuad(MODE_CLK_PIN, MODE_DT_PIN);//配置编码器为半四分之一模式,只计数部分状态变化(每步约 1-2 个计数)modeEncoder.setFilter(50); // 滤波值modeEncoder.setCount(0);}

现象

ESP32驱动inmp441采集音频并实时播放

注意事项

  1. 音频输出可以用耳机去接收听到声音,也可以使用AUX线外接功放板。PCM5102A板子上也有L/R左右声道接口,也可以杜邦线接到其他功放上(比如上一期的TDA2030A功放模块)。
  2. INM441是全向麦克风模块,把扬声器和麦克风放在一起使用很容易引起啸叫,有耳机的话基本不会有这个问题。
  3. 根据前面啸叫问题,增益不宜调太大,一是容易引起啸叫,二是靠近说话容易引起破音,增益大了对远距离采集声音较好。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/pingmian/84341.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

冯诺依曼架构是什么?

冯诺依曼架构是什么&#xff1f; 冯诺依曼架构&#xff08;Von Neumann Architecture&#xff09;是现代计算机的基础设计框架&#xff0c;由数学家约翰冯诺依曼&#xff08;John von Neumann&#xff09;及其团队在1945年提出。其核心思想是通过统一存储程序与数据&#xff0…

【持续更新】linux网络编程试题

问题1 请简要说明TCP/IP协议栈的四层结构&#xff0c;并分别举出每一层出现的典型协议或应用。 答案 应用层&#xff1a;ping,telnet,dns 传输层&#xff1a;tcp,udp 网络层&#xff1a;ip,icmp 数据链路层&#xff1a;arp,rarp 问题2 下列协议或应用分别属于TCP/IP协议…

椭圆曲线密码学(ECC)

一、ECC算法概述 椭圆曲线密码学&#xff08;Elliptic Curve Cryptography&#xff09;是基于椭圆曲线数学理论的公钥密码系统&#xff0c;由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA&#xff0c;ECC在相同安全强度下密钥更短&#xff08;256位ECC ≈ 3072位RSA…

【JVM】- 内存结构

引言 JVM&#xff1a;Java Virtual Machine 定义&#xff1a;Java虚拟机&#xff0c;Java二进制字节码的运行环境好处&#xff1a; 一次编写&#xff0c;到处运行自动内存管理&#xff0c;垃圾回收的功能数组下标越界检查&#xff08;会抛异常&#xff0c;不会覆盖到其他代码…

React 基础入门笔记

一、JSX语法规则 1. 定义虚拟DOM时&#xff0c;不要写引号 2.标签中混入JS表达式时要用 {} &#xff08;1&#xff09;.JS表达式与JS语句&#xff08;代码&#xff09;的区别 &#xff08;2&#xff09;.使用案例 3.样式的类名指定不要用class&#xff0c;要用className 4.内…

Linux链表操作全解析

Linux C语言链表深度解析与实战技巧 一、链表基础概念与内核链表优势1.1 为什么使用链表&#xff1f;1.2 Linux 内核链表与用户态链表的区别 二、内核链表结构与宏解析常用宏/函数 三、内核链表的优点四、用户态链表示例五、双向循环链表在内核中的实现优势5.1 插入效率5.2 安全…

SQL进阶之旅 Day 19:统计信息与优化器提示

【SQL进阶之旅 Day 19】统计信息与优化器提示 文章简述 在数据库性能调优中&#xff0c;统计信息和优化器提示是两个至关重要的工具。统计信息帮助数据库优化器评估查询成本并选择最佳执行计划&#xff0c;而优化器提示则允许开发人员对优化器的行为进行微调。本文深入探讨了…

安宝特方案丨船舶智造AR+AI+作业标准化管理系统解决方案(维保)

船舶维保管理现状&#xff1a;设备维保主要由维修人员负责&#xff0c;根据设备运行状况和维护计划进行定期保养和故障维修。维修人员凭借经验判断设备故障原因&#xff0c;制定维修方案。 一、痛点与需求 1 Arbigtec 人工经验限制维修效率&#xff1a; 复杂设备故障的诊断和…

MFC内存泄露

1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…

基于区块链的供应链溯源系统:构建与实践

前言 在当今全球化的经济环境中&#xff0c;供应链的复杂性不断增加&#xff0c;商品从原材料采购到最终交付给消费者的过程涉及多个环节和众多参与者。如何确保供应链的透明度、可追溯性和安全性&#xff0c;成为企业和消费者关注的焦点。区块链技术以其去中心化、不可篡改和透…

Web攻防-SQL注入数据格式参数类型JSONXML编码加密符号闭合

知识点&#xff1a; 1、Web攻防-SQL注入-参数类型&参数格式 2、Web攻防-SQL注入-XML&JSON&BASE64等 3、Web攻防-SQL注入-数字字符搜索等符号绕过 案例说明&#xff1a; 在应用中&#xff0c;存在参数值为数字&#xff0c;字符时&#xff0c;符号的介入&#xff0c…

探秘鸿蒙 HarmonyOS NEXT:实战用 CodeGenie 构建鸿蒙应用页面

在开发鸿蒙应用时&#xff0c;你是否也曾为一个页面的布局反复调整&#xff1f;是否还在为查 API、写模板代码而浪费大量时间&#xff1f;今天带大家实战体验一下鸿蒙官方的 AI 编程助手——CodeGenie&#xff08;代码精灵&#xff09; &#xff0c;如何从 0 到 1 快速构建一个…

DBAPI如何优雅的获取单条数据

API如何优雅的获取单条数据 案例一 对于查询类API&#xff0c;查询的是单条数据&#xff0c;比如根据主键ID查询用户信息&#xff0c;sql如下&#xff1a; select id, name, age from user where id #{id}API默认返回的数据格式是多条的&#xff0c;如下&#xff1a; {&qu…

使用Whisper本地部署实现香港版粤语+英语混合语音转文字方案

今天要一个非常好的朋友有个工作&#xff0c;就是要把医院医生诊断的说话记录转成文字&#xff0c;之前都是她本人一句一句的听&#xff0c;然后记录下来的&#xff0c;我想通过ai 来解决这个问题。 她的需求如下&#xff1a; 不能把数据传到网上&#xff0c;隐私问题所以需要…

案例分享--汽车制动卡钳DIC测量

制动系统是汽车的主要组成部分&#xff0c;是汽车的主要安全部件之一。随着车辆性能的不断提高&#xff0c;车速不断提升&#xff0c;对车辆的制动系统也随之提出了更高要求&#xff0c;因此了解车辆制动系统中每个部件的动态行为成为了制动系统优化的主要途径&#xff0c;同时…

保姆级教程:在无网络无显卡的Windows电脑的vscode本地部署deepseek

文章目录 1 前言2 部署流程2.1 准备工作2.2 Ollama2.2.1 使用有网络的电脑下载Ollama2.2.2 安装Ollama&#xff08;有网络的电脑&#xff09;2.2.3 安装Ollama&#xff08;无网络的电脑&#xff09;2.2.4 安装验证2.2.5 修改大模型安装位置2.2.6 下载Deepseek模型 2.3 将deepse…

【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)

服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…

VB.net复制Ntag213卡写入UID

本示例使用的发卡器&#xff1a;https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …

SQL SERVER 数据库迁移的三种方法!

要将SQL Server从研发环境的把数据库结构(不含数据)迁移至生产环境,可通过以下几种方法实现。以下是具体操作步骤及适用场景: ⚙️ 一、使用SSMS图形界面生成结构脚本(推荐新手) 通过SQL Server Management Studio的生成脚本向导,仅导出数据库架构: ​​连接测试库​​…

C# 快速检测 PDF 是否加密,并验证正确密码

引言&#xff1a;为什么需要检测PDF加密状态&#xff1f; 在批量文档处理系统&#xff08;如 OCR 文字识别、内容提取、格式转换&#xff09;中&#xff0c;加密 PDF 无法直接操作。检测加密状态可提前筛选文件&#xff0c;避免流程因密码验证失败而中断。 本文使用 Free Spire…