【STM32】 I2C 中断模式收发数据实战:从阻塞到中断的架构改造(AT24C02)


I2C 中断模式收发数据实战:从阻塞到中断的架构改造


工程流程与逻辑梳理

一、工程搭建流程(6 步走完)

第 1 步:CubeMX 配置(只配 4 处)

配置项 值 说明
I2C2 使能,PB10=SCL / PB11=SDA AT24C02 挂 I2C2
I2C 速度 Standard Mode(100kHz) 兼容性最稳,AT24C02 支持 400k 但不必须
USART1 使能,PA9/PA10,115200-8N1 结果打印
NVIC 勾选 I2C2 event interrupt 和 error interrupt ⚠️ 最容易漏------不勾,回调永远不触发
时钟 HSE 8MHz → PLL ×9 → 72MHz 串口波特率依赖正确时钟

第 2 步:生成 MDK 工程

生成后自动得到 MX_I2C2_Init()、MX_USART1_UART_Init()、MX_GPIO_Init() 等初始化骨架,以及 I2C2_EV_IRQHandler / I2C2_ER_IRQHandler 中断入口(CubeMX 已帮你写进 stm32f1xx_it.c)。


第 3 步:写 BSP 驱动层(两个文件)

文件 职责
BSP_AT24C02_IT.h 设备结构体 eeprom_t、状态枚举、操作类型、 回调函数指针、宏定义
BSP_AT24C02_IT.c Init / Ready / Read_Page_IT Write_Page_IT / Process + 重写 3 个 HAL 回调

第 4 步:main 业务(逻辑主线只有 3 行)

cpp 复制代码
初始化(HAL → 时钟 → GPIO/I2C2/USART1 → printf 重定向 → BSP_AT24C02_Init)
↓
发起写:BSP_AT24C02_Write_Page_IT(0, tx_buffer, 8, 回调)
↓
主循环:if (BSP_AT24C02_Process()) → 发起读回验证

第 5 步:编译烧录

SWD 下载后复位。

若串口没有任何输出先查:printf 重定向(MicroLIB)、USART1 时钟/引脚。


第 6 步:串口验证(115200,按顺序应看到 4 条日志)

cpp 复制代码
Init At24C02 SUCCESS                    ← 设备探测通过(IsDeviceReady 有 ACK)
Write Page success                      ← 写传输完成回调
等待设备就绪完成...                       ← Process 消费写完成状态
Read Page success! data: 0x11 0x22 ... ← 读回验证成功

二、运行逻辑(4 条链路)

链路 1:初始化链(上电 → 就绪)

cpp 复制代码
HAL_Init(SysTick 1ms)
→ SystemClock_Config(72MHz)
→ MX_GPIO_Init / MX_I2C2_Init / MX_USART1_UART_Init
→ BSP_Usart_Init(printf 重定向到串口)
→ BSP_AT24C02_Init:
   存句柄/地址(0x50<<1=0xA0)/页大小
   → HAL_I2C_IsDeviceReady(起始 + 地址+W,等 ACK)
   → 有 ACK → status=READY, is_init=true

逻辑要点:

任何 I2C 操作前必须先"探测"------设备可能没上电、地址不对、上拉缺失,先验明正身再干活。


链路 2:发起写(主循环侧,立即返回)

cpp 复制代码
Write_Page_IT(addr, data, len, cb)
├─ 关卡1 未初始化/参数空 → cb(false)
├─ 关卡2 跨页(page_offset+len>8)→ cb(false)
├─ 关卡3 status==BUSY(防重入)→ cb(false)
└─ 通过:status=BUSY,存 buffer/len/operator=WRITE/cb
        → HAL_I2C_Mem_Write_IT(...) 
        → HAL_OK 返回 true;非 OK → ERROR + cb(false)

逻辑要点:

函数只负责"检查 + 发起",不负责传输;

失败路径全部走 cb(false),调用方只认一个失败信号。


链路 3:后台传输 + 回调(中断侧)

cpp 复制代码
I2C2 EV 中断 → HAL_I2C_EV_IRQHandler → 按状态机推进:
起始 → 地址+W → ACK → MemAddress → ACK → 8 字节数据 → ACK → 停止
传输完成 → HAL_I2C_MemTxCpltCallback(__weak,被我们重写)
  ├─ 判断 Instance 是自己的 I2C2
  ├─ 判断 operator==WRITE
  ├─ status = SUCCESS
  └─ user_callback(true)(打印 "Write Page success")

(若中途出错)I2C2 ER 中断 → HAL_I2C_ER_IRQHandler
  → ErrorCallback → status=ERROR + user_callback(false)

逻辑要点:

回调运行在中断上下文,只做"置状态 + 通知",禁止阻塞(EEPROM 写周期约 5ms,绝不能在回调里等)。


链路 4:主循环调度(闭环推进)

cpp 复制代码
while(1):
  BSP_AT24C02_Process()
  ├─ 条件:operator==WRITE && status==SUCCESS
  ├─ 动作:BSP_AT24C02_Ready() 再探测一次 → 打印"等待设备就绪完成"
  │       → status 复位为 READY → return true
  └─ main 收到 true → 发起 BSP_AT24C02_Read_Page_IT(0, rx, 8, 回调)
      → 后台读回(重复起始 + 地址+R + 8 字节,最后 NACK)
      → MemRxCpltCallback → SUCCESS → 回调打印 rx_buffer 数据

逻辑要点:

写→读的"业务推进"全部由主循环驱动,中断只负责"传输完成信号"------这就是前后台架构的纪律:前台报信,后台办事。


三、数据流(硬件层面)

写方向:****tx_buffer → (BSP buffer) → HAL → I2C DR → 移位寄存器 → SDA 总线 → AT24C02 页缓冲 → 内部写周期(约5ms) → 存储单元

读方向:****存储单元 → 页缓冲 → SDA → 移位寄存器 → 比较器校验地址 → DR → HAL → rx_buffer → 回调打印


四、这套工程的 5 个设计决策

决策 为什么
状态机 + BUSY 状态 中断模式调用即返回,必须有"忙"标记防重入,否则并发发起会乱
status/operator_type 用 volatile 中断回调写、主循环读,防止编译器缓存旧值
回调分层(HAL 回调 → 用户回调) 驱动与业务解耦:驱动不写死"打印/存数据",用户可换回调
跨页检查放在发起前 AT24C02 页=8 字节,跨页会地址回绕覆盖页首
写成功后由 Process 触发读回 中断里不嵌套发起新传输,业务推进线性可控

工程知识

1. I2C 与 NVIC 联动:两个中断,各管一摊

STM32F103 的 I2C 中断在向量表里是分家的(这是很多人第一次接触会懵的地方):

中断 向量号 管什么
I2C1_EV_IRQn 31 事件中断:起始完成、地址匹配、TXE、RXNE、停止
I2C1_ER_IRQn 32 错误中断:NACK(AF)、总线错误(BERR)、仲裁丢失(ARLO)、溢出(OVR)
I2C2_EV_IRQn 33 I2C2 事件中断
I2C2_ER_IRQn 34 I2C2 错误中断

为什么事件和错误要分开?

  • 事件中断高频(每收发一个字节都可能触发),错误中断低频;
  • 分开后可以分别设置优先级------错误可以设得比事件更高,保证出问题时第一时间响应;
  • 调试时也清晰:进错中断就是出错了,不用在事件中断里翻标志。

对应到 I2C 的功能框图(参考手册 Figure),数据通路是:SDA ↔ 数据控制 → 移位寄存器 ↔ 数据寄存器(DR),移位结果送比较器与地址寄存器(OAR)比对判断是否被寻址;

SCL 由时钟控制(CCR) 产生;整个状态机由控制逻辑驱动,它读写 CR1/CR2(控制)和 SR1/SR2(状态),并向 NVIC 输出中断、向 DMA 输出请求。


2. 中断模式的 API:HAL_I2C_Mem_Read_IT / Write_IT

阻塞版本函数名去掉 _IT 就是中断版本,签名几乎一样:

cpp 复制代码
HAL_StatusTypeDef HAL_I2C_Mem_Read_IT(I2C_HandleTypeDef *hi2c,
                                      uint16_t DevAddress,
                                      uint16_t MemAddress,
                                      uint16_t MemAddSize,
                                      uint8_t  *pData,
                                      uint16_t Size);

HAL_StatusTypeDef HAL_I2C_Mem_Write_IT(I2C_HandleTypeDef *hi2c,
                                       uint16_t DevAddress,
                                       uint16_t MemAddress,
                                       uint16_t MemAddSize,
                                       uint8_t  *pData,
                                       uint16_t Size);

参数逐条说:

参数 含义 注意
hi2c I2C 外设句柄 指向已初始化的句柄
DevAddress 目标设备地址 必须传左移 1 位后的 8 位地址(见下方坑 1)
MemAddress 器件内部寄存器/存储地址 如 AT24C02 的字节地址 0~255
MemAddSize 内部地址位宽 I2C_MEMADD_SIZE_8BIT 或 16BIT
pData 数据缓冲区指针 收发都指向它
Size 字节数 单页场景 ≤ 页大小

返回值是一个 HAL_StatusTypeDef:

  • HAL_OK:操作成功启动(注意:不是传输完成);
  • HAL_BUSY:外设正在忙,本次调用没接单;
  • HAL_ERROR:参数无效或内部错误。

和阻塞版最大的三个区别:

  1. 非阻塞:函数发起后立即返回,实际传输在后台由中断服务程序完成;
  2. 回调通知:传输完成后进 HAL_I2C_MemRxCpltCallback(读完成)或 HAL_I2C_MemTxCpltCallback(写完成),你在代码里实现它;
  3. 专用性:这两个函数专用于带内部存储映射的器件(EEPROM、传感器),会自动在起始条件之后发送 MemAddress 字段------相比 HAL_I2C_Master_Transmit_IT 省去了手动拼地址的步骤。

3. 架构设计:设备对象 + 状态机 + 回调解耦

中断模式最怕什么?

乱。调用返回了,但传输还在飞,什么时候完、完了怎么知道、中途能不能再发起新操作------全靠一套明确的"协议"。课件里的解法很标准,三层结构:

3.1 设备结构体 eeprom_t:把全局状态装进一个对象

cpp 复制代码
typedef enum {
    EEPROM_IDLE = 0,   // 空闲
    EEPROM_READY,      // 就绪(检测通过)
    EEPROM_BUSY,       // 忙(传输进行中)
    EEPROM_SUCCESS,    // 成功(回调置位)
    EEPROM_ERROR       // 错误(错误回调置位)
} Status_t;

#define NONE_OPERATOR 0
#define READ_OPERATOR 1
#define WRITE_OPERATOR 2

typedef void (*user_callback_t)(bool);   // 用户回调:bool 表示成功/失败

typedef struct {
    I2C_HandleTypeDef *hi2c;             // I2C 句柄
    uint16_t dev_address;                // 从设备 8 位地址
    uint8_t  page_size;                  // 页大小
    bool     is_init;                    // 是否初始化成功

    volatile Status_t status;            // ★ 设备状态(中断改、主循环读)
    uint8_t *buffer;                     // 本次操作的缓冲区
    uint8_t  len;                        // 本次操作长度
    volatile uint8_t operator_type;      // 当前操作类型(读/写/无)
    user_callback_t user_callback;       // 用户回调函数指针
} eeprom_t;

每个字段为什么存在?

  • status + operator_type:中断回调和主循环之间靠它俩通信,被中断修改 → 必须 volatile(否则编译器可能把主循环里的读取优化到寄存器缓存,永远读到旧值);
  • buffer / len:中断回调需要知道"数据传到了哪块缓冲、一共多少字节",这些信息在发起调用的函数返回后就丢了,必须存进结构体;
  • user_callback:把"驱动层"和"业务层"解耦------驱动只负责传输,传完了调用户回调,用户想打印就打印、想存数据就存数据,驱动代码不用改。

3.2 状态机:防重入 + 统一出口

cpp 复制代码
IDLE → (Init 检测通过) → READY → (发起操作) → BUSY → (完成回调) → SUCCESS → (Process 消费) → READY
                                             BUSY → (错误回调) → ERROR
  • BUSY 的意义是防重入:上一次传输没结束,不允许再发起新操作,直接拒绝并回调 false;
  • SUCCESS 是"传输完成"的信号:注意它不代表 EEPROM 已经保存完毕(见第 5 节的坑 3);
  • ERROR 是统一失败出口:任何一步失败都走 ErrorCallback → 置 ERROR → 回调 false,调用方逻辑只认这一个信号。

4. 代码逐行解析

4.1 初始化:先"验明正身"

cpp 复制代码
bool BSP_AT24C02_Init(I2C_HandleTypeDef *hi2c)
{
    if (!hi2c)
        return false;

    at24c02.hi2c          = hi2c;
    at24c02.dev_address   = DEV_ADDRESS << 1;   // 0x50 << 1 = 0xA0(坑 1)
    at24c02.page_size     = PAGE_SIZE;          // 8
    at24c02.is_init       = false;

    at24c02.buffer        = NULL;
    at24c02.len           = 0;
    at24c02.user_callback = NULL;
    at24c02.status        = EEPROM_IDLE;
    at24c02.operator_type = NONE_OPERATOR;

    // 1. 产生起始
    // 2. 发送从设备地址 + W(0)
    // 3. 从机 ACK → 说明设备活着
    // 4. STOP
    if (HAL_OK == HAL_I2C_IsDeviceReady(at24c02.hi2c,
                                        at24c02.dev_address,
                                        SCAN_RETYIES, SCAN_TIME_MS))
    {
        at24c02.status  = EEPROM_READY;
        at24c02.is_init = true;
    }
    return at24c02.is_init;
}
  • HAL_I2C_IsDeviceReady 做的事:发起"起始 + 从机地址+W",等待 ACK。
  • 有 ACK = 从机在线;没有 = 不在(可能没上电、地址不对、上拉电阻缺失)。retries 和 timeout 是重试次数与单次等待超时。
  • 初始化失败直接返回 false,main 里会打印 Init At24C02 ERROR 并死循环,防止后面用未初始化的外设。

4.2 发起写:检查三重关卡

cpp 复制代码
bool BSP_AT24C02_Write_Page_IT(uint16_t start_address,
                               void *data_in,
                               uint8_t data_len,
                               user_callback_t cb)
{
    // 关卡 1:初始化、参数合法性
    if (!at24c02.is_init || !data_in || data_len == 0)
    {
        if (cb) cb(false);
        return false;
    }

    // 关卡 2:跨页检查(坑 2)
    uint16_t page_offset = start_address % at24c02.page_size;  // 页内偏移
    if (page_offset + data_len > at24c02.page_size)
    {
        if (cb) cb(false);
        return false;
    }

    // 关卡 3:忙检查(防重入)
    if (at24c02.status == EEPROM_BUSY)
    {
        if (cb) cb(false);
        return false;
    }

    // 记录本次操作上下文(回调要用)
    at24c02.status        = EEPROM_BUSY;
    at24c02.buffer        = data_in;
    at24c02.len           = data_len;
    at24c02.operator_type = WRITE_OPERATOR;
    at24c02.user_callback = cb;

    // 真正发起:非阻塞,立即返回
    HAL_StatusTypeDef state = HAL_I2C_Mem_Write_IT(at24c02.hi2c,
                                                   at24c02.dev_address,
                                                   start_address,
                                                   I2C_MEMADD_SIZE_8BIT,
                                                   data_in,
                                                   data_len);
    if (state != HAL_OK)
    {
        at24c02.status = EEPROM_ERROR;
        if (cb) cb(false);
        return false;
    }
    return true;
}

三个关卡的意义:

  • 跨页检查(关卡 2):AT24C02 页大小 8 字节,页编程只能在同一页内,跨页会地址回绕覆盖页首数据。page_offset = start_address % 8 算出页内偏移,偏移 + 长度 > 8 就是跨页,直接拒绝。多页数据怎么办?拆成多段、每段写一页(和 SPI Flash 跨页写同思路)。
  • 忙检查(关卡 3):status == EEPROM_BUSY 说明上一次传输还没结束,此时再发起,HAL 也会返回 HAL_BUSY------与其等 HAL 拒绝,不如我们自己先拦住,语义更清晰。
  • 统一失败出口:每个失败分支都 cb(false),调用方不用关心具体哪一步挂的。

4.3 三个回调:中断里只做"置状态 + 通知"

cpp 复制代码
// 写完成回调:走到这里,数据已经被 HAL 通过 I2C 发完了
void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
    if (hi2c->Instance != at24c02.hi2c->Instance)
        return;

    if (at24c02.operator_type == WRITE_OPERATOR)
    {
        // 注意:发送完毕 ≠ EEPROM 已经保存完毕!
        // 绝对不能在中断回调里等待设备就绪(坑 3)
        at24c02.status = EEPROM_SUCCESS;
        if (at24c02.user_callback)
        {
            at24c02.user_callback(true);
            at24c02.user_callback = NULL;   // 用后置空,防止重复调用
        }
    }
}

// 读完成回调
void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c)
{
    if (hi2c->Instance != at24c02.hi2c->Instance)
        return;

    if (at24c02.operator_type == READ_OPERATOR)
    {
        at24c02.status = EEPROM_SUCCESS;
        if (at24c02.user_callback)
        {
            at24c02.user_callback(true);
            at24c02.user_callback = NULL;
        }
    }
}

// 错误回调:传输过程中出错自动进来
void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c)
{
    if (hi2c->Instance != at24c02.hi2c->Instance)
        return;

    at24c02.status = EEPROM_ERROR;
    if (at24c02.user_callback)
    {
        at24c02.user_callback(false);
        at24c02.user_callback = NULL;
    }
}

三个要点:

  1. 先判 Instance:工程里可能同时用 I2C1/I2C2 甚至多个从机,回调是全局的,必须先确认"这次回调是不是我的 I2C 发起的",否则会误处理别人的设备;
  2. 只置状态 + 通知:回调运行在中断上下文,越短越好,绝不阻塞;
  3. user_callback 用完置 NULL:回调只该被触发一次,置空防止同一操作被重复通知(比如后续又误触发)。

4.4 主循环调度:Process 负责"消费状态"

cpp 复制代码
bool BSP_AT24C02_Process(void)
{
    // 写操作成功之后,把状态复位,让系统可以发起下一步
    if (at24c02.operator_type == WRITE_OPERATOR &&
        at24c02.status == EEPROM_SUCCESS)
    {
        BSP_AT24C02_Ready();                 // 再确认设备在线
        printf("等待设备就绪完成...\n");
        at24c02.status = EEPROM_READY;
        return true;
    }
    return false;
}

main 里的用法就是"写成功 → 读回来验证":

cpp 复制代码
BSP_AT24C02_Write_Page_IT(0, tx_buffer, 8, EEPROM_WriteCallBack);  // 先写 8 字节

while (1)
{
    if (BSP_AT24C02_Process())               // 写完成信号被消费
    {
        // 写成功了,读回来验证
        BSP_AT24C02_Read_Page_IT(0, rx_buffer, 8, EEPROM_ReadCallBack);
    }
}

为什么"读回"要放在 Process 里、而不是写回调里?

在回调里直接再发起一次读属于"中断里嵌套发起新传输",状态机要处理的情况急剧变多;放主循环按状态推进,逻辑线性、好调试、还方便中间插延时。


4.5 用户回调:业务代码只写一处

cpp 复制代码
void EEPROM_WriteCallBack(bool result)
{
    if (result) printf("Write Page success\n");
    else        printf("Write Page failed\n");
}

void EEPROM_ReadCallBack(bool result)
{
    if (result)
    {
        printf("Read Page success! data : \n");
        for (int i = 0; i < 8; i++)
            printf("0x%x ", rx_buffer[i]);
        printf("\n");
    }
    else
    {
        printf("Read Page failed\n");
    }
}

驱动层完全不关心"写完后是打印还是存数据库",它只负责把 bool result 传回来------这就是回调解耦的价值。


5. 三个必须记住的坑

坑 1:DevAddress 要左移一位

AT24C02 数据手册给的是 7 位地址 0x50,但 HAL 的 DevAddress 参数要的是8 位地址(第 0 位留给 R/W 方向)。所以:

复制代码
at24c02.dev_address = DEV_ADDRESS << 1;   // 0x50 << 1 = 0xA0

传成 0x50 会怎样?

I2C 总线上发出去的地址不对,从机不 ACK,IsDeviceReady 永远失败。


坑 2:跨页写入会覆盖页首

AT24C02 页 = 8 字节,页编程时地址只在页内递增,超过页尾就回绕到页首。

从地址 7 写 2 字节,第二个字节会写到地址 0。

所以发起前必须做 page_offset + data_len <= page_size 检查,多页数据要拆段写。


坑 3:回调里绝对不能等 EEPROM 就绪

这是最隐蔽的一个。HAL_I2C_MemTxCpltCallback 触发,只代表 I2C 总线上的传输完成了,而 EEPROM 内部还在把数据从页缓冲写入存储单元(AT24C02 写周期约 5ms)。

如果在中断回调里调 IsDeviceReady 忙等,5ms 里所有中断都被堵死------这是活生生的"中断里阻塞 = 系统失聪"反面教材。

正确姿势:置 SUCCESS,把"等写周期"交给主循环(Process 里再 Ready() 确认),或主循环延时后再发起读。


6. 阻塞 / 中断 / DMA:什么时候用哪个?

维度 阻塞模式 中断模式(本篇) DMA 模式
调用后主循环 被卡住 立即返回 立即返回
CPU 占用 高(忙等) 中(每字节一次事件中断) 低(搬运不占 CPU)
代码复杂度 最低 中(状态机 + 回调) 中高
典型场景 调试、单次小数据 命令交互、需要并发 大块数据(如整片读写)
EEPROM 页(8B) 场景 可以 推荐 收益小(数据太少)

一句话:要并发、要响应其他任务 → 中断模式;数据量大到中断开销不可接受 → DMA。


7. 总结

调用点 所在上下文 做什么 返回值 / 效果
main → Write_Page_IT 主循环 四道检查 + 存上下文 bool(是否发起成功)
Write_Page_IT → Mem_Write_IT 主循环 HAL 保存 xfer + 发起始 + 开中断 HAL_OK(立即返回)
硬件 → EV_IRQHandler 中断 逐标志推进传输 无
完成 → MemTxCpltCallback 中断 HAL 通知(__weak) 无
BSP 重写体 中断 判 Instance/operator + 置 SUCCESS 无
BSP → user_callback 中断 调用用户函数指针 无
user_callback → printf 中断 业务输出 无
cpp 复制代码
【T0】主循环
  ① main 调 BSP_AT24C02_Write_Page_IT(0, tx, 8, EEPROM_WriteCallBack)
  ② BSP 层把 EEPROM_WriteCallBack 的地址存进 at24c02.user_callback
  ③ BSP 层调 HAL_I2C_Mem_Write_IT → 库启动传输 → 返回 HAL_OK
  ④ main 继续 while 循环 ------ 此刻 user_callback 只被"登记",还没被"调用"

【T1】几百微秒后(中断上下文,和主循环是两条不同的执行线)
  ⑤ 硬件产生 I2C2 EV 中断 → 进入 ISR → HAL_I2C_EV_IRQHandler
  ⑥ 库在 ISR 里逐事件推进:写地址、写 8 字节数据、发停止条件
  ⑦ 传输收尾 → 库内部调 I2C_ITMasterCpltCallback(hi2c)
  ⑧ 该函数按 Mode 判断是 MEM 操作 → 调 HAL_I2C_MemTxCpltCallback(hi2c)
     ------ 注意:这一步调的其实是【你的重写体】(__weak 被覆盖)

【T2】你的重写体(仍在中断里)
  ⑨ 判断 hi2c->Instance == &hi2c2 ?是,认领
  ⑩ 判断 operator_type == WRITE_OPERATOR?是
  ⑪ status = EEPROM_SUCCESS        ← 主循环能"看见"完成
  ⑫ user_callback(true)  →  真正执行 EEPROM_WriteCallBack → printf
  ⑬ user_callback = NULL  ← 防止同一操作重复通知

【T3】中断退出,回到主循环
  ⑭ 主循环 BSP_AT24C02_Process 看到 status==SUCCESS → 触发读回

I2C 中断模式收发,本质是把"一次阻塞调用"拆成四个阶段:

  1. 发起(主循环):检查参数/跨页/忙 → _IT 函数启动 → 立即返回;
  2. 后台传输(I2C 事件中断):逐事件推进时序;
  3. 回调通知(HAL 回调 → 驱动回调 → 用户回调):只置状态 + 通知,不阻塞;
  4. 主循环调度(Process):轮询状态,复位就绪,发起下一步。

相关推荐
李永奉1 小时前
中科蓝讯SDK开发-一拖多烧录盒烧录使用教程
单片机·嵌入式硬件·物联网·智能手机·电脑
szxinmai主板定制专家2 小时前
FPGA高速IO+RK182X AI算力|RK3576+CODESYS工业自动化运动控制平台设计
运维·人工智能·嵌入式硬件·fpga开发·自动化·zynq
czhaii2 小时前
单片机&PLC PCB板
嵌入式硬件
星河单片机2 小时前
单片机成品方案:基于STM32单片机智能门禁系统
stm32·单片机·嵌入式硬件
jianqiang.xue2 小时前
【CStackGUI 实战】采集记录器:模拟采集 + 双曲线 + CSV 落盘 + 回放
单片机·嵌入式·cstackgui·c语言gui·可视化拖拽
iCxhust3 小时前
51单片机电力载波应用开发(3)载波模块点对点收发
网络·嵌入式硬件·51单片机
硅基手札3 小时前
【risc-v专栏03】CSR 速查与特权架构 M/S/U
单片机·架构·risc-v
单片机设计·······3 小时前
【dz-1162】汽车前玻璃雨刮器设计
c++·单片机·嵌入式硬件
尼喃3 小时前
USB口5V过压过流限流保护芯片:3A,6A的压降与热成像数据
嵌入式硬件