FPGA实现CNN卷积层:高效窗口生成模块设计与验证

news2025/6/3 20:56:48

我最近在从事一项很有意思的项目,我想在PFGA上部署CNN并实现手写图片的识别。而本篇文章,是我迈出的第一步。具体代码已发布在github上

模块介绍

卷积神经网络(CNN)可以分为卷积层、池化层、激活层、全链接层结构,本篇要实现的,就是CNN的卷积层中的window窗。

在卷积过程中,最复杂的就是卷积运算,也就是Filter和图片(输入)相乘然后在相加的这一步骤。

img

我此处的构想就是将其卷积这个步骤进行拆分:加窗、载入权重、卷积运算。因而对应3个模块,而此处实现的就是加窗这个模块。而他主要负责的功能就是:提取输入图片中的数据,生成对应的窗口。 如上图所示,对x[:,:,0]图片进行窗口提起,提取的第一个窗口(左上角第一个)就是

[ 0 0 0 0 0 1 0 0 1 ] \begin{bmatrix}0&0&0\\0&0&1\\0&0&1\end{bmatrix} 000000011

代码

  1. 可配置参数、输入和输出定义

STRIDE为窗口滑动的步长,KERNEL_SIZE对应输入卷积核的大小,PADDING 为补充的长度

pixel_in 为输出的图片数据,frame_start 为图片开始输入的标志,pixel_valid为输入有效标志

window_out是图片展成一维的窗口数据

module window #(
    parameter DATA_WIDTH = 16,             // Width of each pixel data
    parameter IMG_WIDTH = 32,             // Width of input image
    parameter IMG_HEIGHT = 32,            // Height of input image
    parameter KERNEL_SIZE = 3,            // Size of convolution window (square)
    parameter STRIDE = 1,                 // Stride of convolution
    parameter PADDING = (KERNEL_SIZE - 1) / 2  // Padding size calculated for SAME mode
)
(
    input wire clk,                       // Clock signal
    input wire rst_n,                     // Active low reset
    input wire [DATA_WIDTH-1:0] pixel_in, // Input pixel data
    input wire pixel_valid,               // Input pixel valid signal
    input wire frame_start,               // Start of new frame signal
    
    output reg [KERNEL_SIZE*KERNEL_SIZE*DATA_WIDTH-1:0] window_out, // Flattened window output
    output reg window_valid              // Window data valid
);
  1. 内部信号定义
  • 输入的图片数据是一个一个输入的,用x_pos和y_pos 来记录当前pixel位于图片中的位置

  • 窗口在图片上滑动,用x_window,y_window用来判断窗口目前的位置

  • line_Buffer缓存输入的数据,同时进行padding操作, 形成数据窗口,而window_buffer 在line_buffer上进行滑动,形成窗口

  • 状态机,分为三个状态 IDLE, LOAD,PROCESS, 分别对应空闲,载入(开始载入数据),处理(形成window)

// Internal signals
reg [5:0] x_pos, y_pos;                  // Current input pixel position
reg [5:0] x_window, y_window;            // Window center position
reg [DATA_WIDTH-1:0] line_buffer [0:KERNEL_SIZE][0:IMG_WIDTH+2*PADDING-1]; // Line buffer
reg [DATA_WIDTH-1:0] window_buffer [0:KERNEL_SIZE-1][0:KERNEL_SIZE-1]; // Window buffer
reg signed [6:0] src_y, src_x;           // Temporary variables for coordinate calculation

// State machine
reg [1:0] current_state, next_state;
localparam IDLE = 2'b00, LOAD = 2'b01, PROCESS = 2'b10;

// Loop variables
integer i, j, k;
  1. 状态的赋值以及跳转
  • 当接收到frame_start信号(图片开始输入),状态从空闲进入到LOAD状态;

  • 当目前的图片数据可以已经足够,可以用来生成稳定的输出窗口时,进入到PROCESS状态

  • 当目前滑窗口提取完对应数据窗口后,回到IDLE状态

注:y_pos从0到KERNEL_SIZE-1时,已经有了KERNEL_SIZE行数据了,可以进入窗口数据提取阶段;实际上可以更早进入,因为存在Padding。当y_pos=KERNEL_SIZE-Padding-1的时候,就可以进入了

// FSM state transitions
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) 
        current_state <= IDLE;
    else 
        current_state <= next_state;
end

always @(*) begin
    case (current_state)
        IDLE:    next_state = frame_start ? LOAD : IDLE;
        LOAD:    next_state = (y_pos >= KERNEL_SIZE-1) ? PROCESS : LOAD;
        PROCESS: next_state = (y_window >= IMG_HEIGHT && x_window == 0) ? IDLE : PROCESS;
        default: next_state = IDLE;
    endcase
end
  1. 状态执行

推荐使用拆分的方法,把一个状态执行的大always块,分成很多子always块。

a. 输入图片数据位置捕获

  • 当前状态为IDLE,图片即将开始输入时,将定位信号复原

  • 当前状态不为IDLE, 同时输入有效,那么坐标根据情况自增

// Input pixel position tracking
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        x_pos <= 0;
        y_pos <= 0;
    end else if (current_state == IDLE && frame_start) begin
        x_pos <= 0;
        y_pos <= 0;
    end else if (pixel_valid && current_state != IDLE) begin
        if (x_pos == IMG_WIDTH-1) begin
            x_pos <= 0;
            y_pos <= y_pos + 1;
        end else begin
            x_pos <= x_pos + 1;
        end
    end
end

b. Line_Buffer 的缓冲

  • 每次开启新的一行的数据,对Line_Buffer 全部复位
  • 然后对对应的位置进行实际数据的填充
// Line buffer management
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        for (i = 0; i <= KERNEL_SIZE; i = i + 1)
            for (j = 0; j < IMG_WIDTH + 2*PADDING; j = j + 1)
                line_buffer[i][j] <= 0;
    end else if (pixel_valid && current_state != IDLE) begin
                if (x_pos == 0) begin
            // Clear the line buffer row at the start of each new line
            for (k = 0; k < IMG_WIDTH + 2*PADDING; k = k + 1)
                        line_buffer[y_pos % (KERNEL_SIZE + 1)][k] <= 0;
        end
        line_buffer[y_pos % (KERNEL_SIZE + 1)][x_pos + PADDING] <= pixel_in;
    end
end

c .Window position tracking

  • 复位、一帧图片的开始或即将进入PROCESS状态,对window记位进行复位
  • 当前状态位PROCESS状态,同时没有超过当前图片的高度时,对window的位置进行对应的变化
// Window position tracking
always @(posedge clk or negedge rst_n) begin
    if (!rst_n || frame_start || (current_state == LOAD && next_state == PROCESS)) begin
        x_window <= 0;
        y_window <= 0;
    end else if (current_state == PROCESS && y_window < IMG_HEIGHT) begin
        if (x_window + STRIDE >= IMG_WIDTH) begin
                    x_window <= 0;
                    y_window <= y_window + STRIDE;
                end else begin
                    x_window <= x_window + STRIDE;
                end
    end
end

d. window_buffer的处理

// Window generation and output
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        window_valid <= 0;
        for (i = 0; i < KERNEL_SIZE; i = i + 1)
            for (j = 0; j < KERNEL_SIZE; j = j + 1)
                window_buffer[i][j] <= 0;
    end else begin
        window_valid <= 0; // Default
        
        if (current_state == PROCESS && 
            x_window < IMG_WIDTH && 
            y_window < IMG_HEIGHT && 
            y_window + (KERNEL_SIZE>>1) <= y_pos) begin
            // Generate window
            for (i = 0; i < KERNEL_SIZE; i = i + 1) begin
                for (j = 0; j < KERNEL_SIZE; j = j + 1) begin
                    src_y = y_window + i - (KERNEL_SIZE>>1);
                    src_x = x_window + j - (KERNEL_SIZE>>1);
                    
                    if (src_y >= 0 && src_y < IMG_HEIGHT && 
                        src_x >= 0 && src_x < IMG_WIDTH) begin
                        window_buffer[i][j] <= line_buffer[src_y % (KERNEL_SIZE + 1)][src_x + PADDING];
                        end else begin
                        window_buffer[i][j] <= 0; // Padding
                        end
                    end
                end
                window_valid <= 1;
            end 
    end
end

当window坐标没有超过图片大小,确保可以生成窗口时,获取生成。对KERNEL_SIZE>>1,等价于KERNEL_SIZE/2,表示中心位置的偏移量

e.g.

[0,0] [0,1] [0,2]    [-1,-1] [-1, 0] [-1,+1]
[1,0] [1,1] [1,2] -> [ 0,-1] [ 0, 0] [ 0,+1]  <- (1,1)是中心
[2,0] [2,1] [2,2]    [+1,-1] [+1, 0] [+1,+1]

这样就可以将卷积索引转换为相对于中心的坐标,这样可以用于判断是否越界,从而进行padding补充

以KERNEL_SIZE=3为例

卷积核位置src坐标计算结果取值
0,0scr_y=0+0-1=-1越界padding
0,1src_y=0+0-1=-1越界padding
0,2src_y=0+0-1=-1越界padding
1,0src_x=0+0-1=-1越界padding
1,1src_y=0,src_x=0有效原图[0,0]
1,2src_y=0,src_x=1有效原图[0,1]
2,0src_x=0+0-1=-1越界padding
2,1src_y=1,src_x=0有效原图[1,0]
2,2scr_y=1,src_x=1有效原图[1,1]

e. 数据窗口的展平

将原本二维的的数据(宽为KERNEL_SIZE, 高为KERNEL_SIZE, 位宽为DATA_WIDTH)的数据,按照从罪小位排在最高位的顺序,压缩成一维的数据

// Flatten window buffer for output
always @(*) begin
    for (i = 0; i < KERNEL_SIZE; i = i + 1) begin
        for (j = 0; j < KERNEL_SIZE; j = j + 1) begin
            window_out[(KERNEL_SIZE*KERNEL_SIZE-(i*KERNEL_SIZE+j))*DATA_WIDTH-1 -: DATA_WIDTH] = window_buffer[i][j];
        end
    end
end

endmodule

测试

`timescale 1ns / 1ps

module window_tb();

    // 测试用参数 - 使用小尺寸便于观察
    parameter DATA_WIDTH = 8;
    parameter IMG_WIDTH = 32;
    parameter IMG_HEIGHT = 32;
    parameter KERNEL_SIZE = 3;
    parameter STRIDE = 1;
    parameter PADDING = (KERNEL_SIZE - 1) / 2;
    
    // 测试信号
    reg clk;
    reg rst_n;
    reg [DATA_WIDTH-1:0] pixel_in;
    reg pixel_valid;
    reg frame_start;
    
    wire [KERNEL_SIZE*KERNEL_SIZE*DATA_WIDTH-1:0] window_out;
    wire window_valid;
    
    // 实例化被测模块
    window #(
        .DATA_WIDTH(DATA_WIDTH),
        .IMG_WIDTH(IMG_WIDTH),
        .IMG_HEIGHT(IMG_HEIGHT),
        .KERNEL_SIZE(KERNEL_SIZE),
        .STRIDE(STRIDE),
        .PADDING(PADDING)
    ) dut (
        .clk(clk),
        .rst_n(rst_n),
        .pixel_in(pixel_in),
        .pixel_valid(pixel_valid),
        .frame_start(frame_start),
        .window_out(window_out),
        .window_valid(window_valid)
    );
    
    // 时钟生成
    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end
    
    // 测试数据 - 5x5图像
    reg [DATA_WIDTH-1:0] test_image [0:IMG_HEIGHT-1][0:IMG_WIDTH-1];
    
    // 窗口计数器
    integer window_count = 0;
    
    // 初始化测试图像
    
      task reset_test_image;
        integer i, j;
        begin
            for(i = 0; i < IMG_HEIGHT; i = i + 1) begin
                for(j = 0; j < IMG_WIDTH; j = j + 1) begin
                    test_image[i][j] =0;
                end
            end
        end
    endtask
    
    task init_test_image;
        integer i, j;
        begin
            for(i = 0; i < IMG_HEIGHT; i = i + 1) begin
                for(j = 0; j < IMG_WIDTH; j = j + 1) begin
                    test_image[i][j] = i * IMG_WIDTH + j + 1;
                end
            end
        end
    endtask
    
    // 显示测试图像
    task display_test_image;
        integer i, j;
        begin
            $display("\n=== 4x4 Test Image ===");
            for(i = 0; i < IMG_HEIGHT; i = i + 1) begin
                $write("Row %0d: ", i);
                for(j = 0; j < IMG_WIDTH; j = j + 1) begin
                    $write("%3d ", test_image[i][j]);
                end
                $display("");
            end
            $display("======================\n");
        end
    endtask
    
    // 发送一帧图像数据
    task send_frame;
        integer i, j;
        begin
            $display("Sending 4x4 frame...");
            init_test_image();
            display_test_image();
            
            // 发送frame_start信号
            @(posedge clk);
            frame_start = 1;
            @(posedge clk);
            frame_start = 0;
            
            // 逐像素发送数据
            for(i = 0; i < IMG_HEIGHT; i = i + 1) begin
                for(j = 0; j < IMG_WIDTH; j = j + 1) begin
                    @(posedge clk);
                    pixel_in = test_image[i][j];
                    pixel_valid = 1;
                    $display("Sending pixel[%0d][%0d] = %0d at time %0t", i, j, pixel_in, $time);
                end
            end
            
            @(posedge clk);
            pixel_valid = 0;
            $display("All pixels sent at time %0t", $time);
        end
    endtask
    
    // 主测试序列
    initial begin
        $display("========================================");
        $display("Window Test - Focus on Last Window");
        $display("IMG_SIZE: %0dx%0d, KERNEL: %0dx%0d", IMG_WIDTH, IMG_HEIGHT, KERNEL_SIZE, KERNEL_SIZE);
        $display("Expected windows: %0d", IMG_WIDTH * IMG_HEIGHT);
        $display("========================================");
        
        // 初始化信号
        rst_n = 0;
        pixel_in = 0;
        pixel_valid = 0;
        frame_start = 0;
        
        reset_test_image();
        // 复位序列
        repeat(5) @(posedge clk);
        rst_n = 1;
        repeat(3) @(posedge clk);
        
        // 发送测试帧
        send_frame();
        
        // 等待所有窗口输出
        repeat(50) @(posedge clk);
        
        $display("\n========================================");
        $display("Test Summary:");
        $display("Total Windows Generated: %0d", window_count);
        $display("Expected Windows: %0d", IMG_WIDTH * IMG_HEIGHT);
        if(window_count == IMG_WIDTH * IMG_HEIGHT) begin
            $display("SUCCESS: All windows generated!");
        end else begin
            $display("FAILURE: Missing windows!");
        end
        $display("========================================");
        
        $finish;
    end
    
    // 窗口监控
    always @(posedge clk) begin
        if(window_valid) begin
            window_count = window_count + 1;
            $display("Window %0d: pos(%0d,%0d) at time %0t", 
                     window_count, dut.x_window, dut.y_window, $time);
            
            // 显示窗口内容
            $write("Window content: ");
            $write("[%0d %0d %0d] ", 
                   window_out[71:64], window_out[63:56], window_out[55:48]);
            $write("[%0d %0d %0d] ", 
                   window_out[47:40], window_out[39:32], window_out[31:24]);
            $write("[%0d %0d %0d]", 
                   window_out[23:16], window_out[15:8], window_out[7:0]);
            $display("");
        end
    end
    
    // 状态机监控
    reg [1:0] prev_state = 2'b00;
    always @(posedge clk) begin
        if(dut.current_state != prev_state) begin
            case(dut.current_state)
                2'b00: $display("Time %0t: State -> IDLE", $time);
                2'b01: $display("Time %0t: State -> LOAD", $time);
                2'b10: $display("Time %0t: State -> PROCESS", $time);
                default: $display("Time %0t: State -> UNKNOWN(%0d)", $time, dut.current_state);
            endcase
            prev_state = dut.current_state;
        end
    end
    
    
    // 波形转储
    initial begin
        $dumpfile("window_tb.vcd");
        $dumpvars(0, window_tb);
        
        // 限制仿真时间
        #2000;
        $display("ERROR: Simulation timeout!");
        $finish;
    end

endmodule 

结果

输入数据

在这里插入图片描述

Row 0: 1 2 3 4 5
Row 1: 6 7 8 9 10
Row 2: 11 12 13 14 15
Row 3: 16 17 18 19 20

Line_Buffer 缓冲数据
在这里插入图片描述

Window_Buffer输出数据

在这里插入图片描述

valid为高,window_buffer开始提取line_buffer数据,同时输出展平的window_out;

在这里插入图片描述

window_buffer提取完毕,valid拉低

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

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

相关文章

LeetCode 3068.最大节点价值之和:脑筋急转弯+动态规划(O(1)空间)

【LetMeFly】3068.最大节点价值之和&#xff1a;脑筋急转弯动态规划&#xff08;O(1)空间&#xff09; 力扣题目链接&#xff1a;https://leetcode.cn/problems/find-the-maximum-sum-of-node-values/ 给你一棵 n 个节点的 无向 树&#xff0c;节点从 0 到 n - 1 编号。树以长…

BLIP-2

目录 摘要 Abstract BLIP-2 模型框架 预训练策略 模型优势 应用场景 实验 代码 总结 摘要 BLIP-2 是一种基于冻结的图像编码器和大型语言模型的高效视觉语言预训练模型&#xff0c;由 Salesforce 研究团队提出。它在 BLIP 的基础上进一步优化&#xff0c;通过轻量级…

支持向量机(SVM)例题

对于图中所示的线性可分的20个样本数据&#xff0c;利用支持向量机进行预测分类&#xff0c;有三个支持向量 A ( 0 , 2 ) A\left(0, 2\right) A(0,2)、 B ( 2 , 0 ) B\left(2, 0\right) B(2,0) 和 C ( − 1 , − 1 ) C\left(-1, -1\right) C(−1,−1)。 求支持向量机分类器的线…

SQL中各个子句的执行顺序

select、from、 join、where、order by、group by、having、limit 解释 1) FROM (确定数据源) 查询的执行首先从FROM子句开始&#xff0c;确定数据的来源(表、视图、连接等)。 2) JOIN (如果有JOIN操作) 在FROM子句之后&#xff0c;SQL引擎会执行连接操作(JOIN)&#xff0c…

本地部署消息代理软件 RabbitMQ 并实现外部访问( Windows 版本 )

RabbitMQ 是由 Erlang 语言开发的 消息中间件&#xff0c;是一种应用程序之间的通信方法。支持多种编程和语言和协议发展&#xff0c;用于实现分布式系统的可靠消息传递和异步通信等方面。 本文将详细介绍如何在 Windows 系统本地部署 RabbitMQ 并结合路由侠实现外网访问本…

基于微信小程序的垃圾分类系统

博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;熟悉各种主流语言&#xff0c;精通java、python、php、爬虫、web开发&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过上千套毕业设计程序&#xff0c;没有什么华丽的语言&#xff0…

流媒体基础解析:视频清晰度的关键因素

在视频处理的过程中&#xff0c;编码解码及码率是影响视频清晰度的关键因素。今天&#xff0c;我们将深入探讨这些概念&#xff0c;并解析它们如何共同作用于视频质量。 编码解码概述 编码&#xff0c;简单来说&#xff0c;就是压缩。视频编码的目的是将原始视频数据压缩成较…

grid网格布局

使用flex布局的痛点 如果使用justify-content: space-between;让子元素两端对齐&#xff0c;自动分配中间间距&#xff0c;假设一行4个&#xff0c;如果每一行都是4的倍数那没任何问题&#xff0c;但如果最后一行是2、3个的时候就会出现下面的状况&#xff1a; /* flex布局 两…

Vehicle HAL(2)--Vehicle HAL 的启动

目录 1. VehicleService-main 函数分析 2. 构建EmulatedVehicleHal 2.1 EmulatedVehicleHal::EmulatedVehicleHal(xxx) 2.2 EmulatedVehicleHal::initStaticConfig() 2.3 EmulatedVehicleHal::onPropertyValue() 3. 构建VehicleEmulator 4. 构建VehicleHalManager (1)初…

【C语言】详解 指针

前言&#xff1a; 在学习指针前&#xff0c;通过比喻的方法&#xff0c;让大家知道指针的作用。 想象一下&#xff0c;你在一栋巨大的图书馆里找一本书。如果没有书架编号和目录&#xff0c;这几乎是不可能完成的任务。 在 C 语言中&#xff0c;指针就像是图书馆的索引系统&…

RabbitMQ仲裁队列高可用架构解析

#作者&#xff1a;闫乾苓 文章目录 概述工作原理1.节点之间的交互2.消息复制3.共识机制4.选举领导者5.消息持久化6.自动故障转移 集群环境节点管理仲裁队列增加集群节点重新平衡仲裁队列leader所在节点仲裁队列减少集群节点 副本管理add_member 在给定节点上添加仲裁队列成员&…

Apache Kafka 实现原理深度解析:生产、存储与消费全流程

Apache Kafka 实现原理深度解析&#xff1a;生产、存储与消费全流程 引言 Apache Kafka 作为分布式流处理平台的核心&#xff0c;其高吞吐、低延迟、持久化存储的设计使其成为现代数据管道的事实标准。本文将从消息生产、持久化存储、消息消费三个阶段拆解 Kafka 的核心实现原…

Python 训练营打卡 Day 41

简单CNN 一、数据预处理 在图像数据预处理环节&#xff0c;为提升数据多样性&#xff0c;可采用数据增强&#xff08;数据增广&#xff09;策略。该策略通常不改变单次训练的样本总数&#xff0c;而是通过对现有图像进行多样化变换&#xff0c;使每次训练输入的样本呈现更丰富…

leetcode付费题 353. 贪吃蛇游戏解题思路

贪吃蛇游戏试玩:https://patorjk.com/games/snake/ 问题描述 设计一个贪吃蛇游戏,要求实现以下功能: 初始化游戏:给定网格宽度、高度和食物位置序列移动操作:根据指令(上、下、左、右)移动蛇头规则: 蛇头碰到边界或自身身体时游戏结束(返回-1)吃到食物时蛇身长度增加…

CCPC dongbei 2025 I

题目链接&#xff1a;https://codeforces.com/gym/105924 题目背景&#xff1a; 给定一个二分图&#xff0c;左图编号 1 ~ n&#xff0c;右图 n 1 ~ 2n&#xff0c;左图的每个城市都会与右图的某个城市犯冲&#xff08;每个城市都只与一个城市犯冲&#xff09;&#xff0c;除…

系统性学习C语言-第十三讲-深入理解指针(3)

系统性学习C语言-第十三讲-深入理解指针&#xff08;3&#xff09; 1. 数组名的理解2. 使用指针访问数组3. ⼀维数组传参的本质4. 冒泡排序5. ⼆级指针 6. 指针数组7. 指针数组模拟二维数组 1. 数组名的理解 在上⼀个章节我们在使用指针访问数组的内容时&#xff0c;有这样的代…

贪心算法实战篇2

文章目录 前言序列问题摆动序列单调递增的数字 贪心解决股票问题买卖股票的最佳时机II 两个维度权衡问题分发糖果根据身高重建队列 前言 今天继续带大家进行贪心算法的实战篇2&#xff0c;本章注意来解答一些运用贪心算法的中等的问题&#xff0c;大家好好体会&#xff0c;怎么…

Java 大视界 -- Java 大数据机器学习模型在元宇宙虚拟场景智能交互中的关键技术(239)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

高速串行接口

1.网口设计方案 上图中给出了两种网口设计方案&#xff0c;最上面是传统设计方式&#xff0c;下面是利用GT作为PHY层的设计&#xff0c;然后FPGA中设计协议层和MAC层。 2.SRIO SRIO的本地操作和远程操作 3.其他高速接口 srio rapid io aurora8b10b aurora64b66b pcie s…

学习STC51单片机23(芯片为STC89C52RCRC)

每日一言 成功的路上从不拥挤&#xff0c;因为坚持的人不多&#xff0c;你要做那个例外。 通过单片机发指令给ESP8266进行通信 通信原理(也是接线原理) 代码如下 代码解释一下&#xff0c;因为我们的指令是字符数组&#xff08;c语言没有字符串的概念&#xff09;&#xff0c;…