C语言相关的数电知识

一、电平特性

1.1、TTL 电路和 CMOS 电路的逻辑电平关系如下:

①VOH:逻辑电平 1 的输出电压。

②VOL:逻辑电平 0 的输出电压。

③VIH:逻辑电平 1 的输入电压。

④VIL:逻辑电平 0 的输入电压。

1.2、TTL 电平临界值:

①VOHmin=2.4V,VOLmax=0.4V。

②VIHmin=2.0V,VILmax=0.8V。

1.3、CMOS 电平临界值(假设电源电压为+5V):

①VOHmin=4.99V,VOLmax=0.01V。

②VIHmin=3.5V,VILmax=1.5V。

二 、二进制与十六进制

2.1、二进制

"逢二进一,借一当二"是二进制数的特点。

2.2、十六进制

"逢十六进一,借一 当十六",十进制的 0-15 表示成十六进制为 0~9、 A、B、C、D、E、F。

三 、二进制的逻辑运算

3.1、基本逻辑门电路图形符号

3.2、门电路基本描述

四、逻辑运算的实际应用(STM32的捕获/比较通道)与解读

左边的逻辑是:

CCR1的高低字节数据取反后给到capture_transfer的与门输入,然后还有一个输入是CC1S寄存器的第0和1字节经过或门输入,这两个要同时输出高,才会有输出;

IC1PS与CC1E要同时输出高,才会通过这个与门给到下一个或门,然后CC1G跟上一个输入进行二选一,其中一个给高就会通过这个或门,然后成为了下一个与门的输入,进行捕获输出

注意右边的逻辑也是如此分析,主要的是逻辑非的入和出的辨别,这个是逻辑非的入这个是逻辑非的出

五、数字电路时序图与协议总线时序图的解读

5.1、数字电路时序图

数字电路时序图(如前述触发器时序) 像是单个乐器(如小提琴)的乐谱,详细规定了每一个音符的时长、力度和精确的起止时刻。它关注的是内部如何精确运作

5.2、协议总线时序图

协议总线时序图 像是整个乐队(弦乐部、管乐部)的合奏总谱,它规定了不同乐器组(信号线)之间配合的节拍和先后顺序。它关注的是外部设备之间如何成功对话

5.3、 核心目的与关注点不同

特性维度 数字电路内部时序图 协议总线时序图
核心目的 描述一个电路模块(如触发器、计数器)内部的工作与稳定性条件。确保电路在电气上正确、可靠地翻转。 定义两个或以上独立设备之间进行数据交换的"语言规则"或"握手协议"。确保数据能被准确无误地识别和传输。
关注焦点 单个信号(CLK, D, Q)的波形细节 ,以及它们之间的精确时间关系 (如 T_su, T_h)。 多个相关信号(如 CLK, MOSI, MISO, CS)之间的协作顺序和相对时序**。
设计目标 满足物理电气特性,避免亚稳态,保证逻辑功能正确。 实现高效、可靠、可互操作的数据通信。
时间尺度 通常是纳秒级甚至皮秒级的绝对延迟。 通常是微秒级甚至毫秒级 的周期和延迟,更关注信号间的相对时序

5.4、 如何阅读和理解协议总线时序图

协议时序图是总线规范的灵魂。读懂了它,你就知道了如何编程控制硬件。我们以最常见的 SPI 总线模式0 的写时序为例。

5.4.1. 先看全局:有哪些信号线?

典型的SPI有四根线:

  • SCLK串行时钟,由主设备产生,是同步的基准。

  • MOSI主设备输出,从设备输入,用于传输数据。

  • MISO主设备输入,从设备输出,用于接收数据。

  • CS/SS片选信号,低电平有效,用于选中要通信的特定从设备。

5.4.2. 再看细节:信号的协作关系(时序图解析)

复制代码
             ┌───┐    ┌───┐    ┌───┐    ┌───┐    ┌───┐    ┌───┐
SCLK (CPOL=0)│   │    │   │    │   │    │   │    │   │    │   │
             └───┘    └───┘    └───┘    └───┘    └───┘    └───┘
             ↑    ↑    ↑    ↑    ↑    ↑    ↑    ↑    ↑    ↑    ↑
             │t1  │t2  │t3  │t4  │t5  │t6  │t7  │t8  │t9  │t10
             └────┴────┴────┴────┴────┴────┴────┴────┴────┴────►
            边沿1   边沿2   边沿3   边沿4   边沿5
            (采样)  (变化)  (采样)  (变化)  (采样)

CS/SS   ─────┐                               ┌─────────────────
(低有效)     │                               │
             └───────────────────────────────┘
             ↑                               ↑
           开始传输                         结束传输

MOSI    ────────┬─────┬─────┬─────┬─────┬─────┬───────────────
(主出)          │ D7  │ D6  │ D5  │ D4  │ ... │ (MSB First)
                └─────┴─────┴─────┴─────┴─────┘
                ↑           ↑           ↑
               在SCLK边沿1  在SCLK边沿3 在SCLK边沿5
               时刻已稳定   时刻已稳定   时刻已稳定
               (建立时间)  (建立时间)  (建立时间)

MISO    ────────────────────┬─────┬─────┬─────┬─────┬─────────
(主入)                      │ D7  │ D6  │ D5  │ D4  │ ...
                            └─────┴─────┴─────┴─────┘
                            ↑           ↑
                           在SCLK边沿2  在SCLK边沿4
                           后变化      后变化
                           (输出延迟) (输出延迟)

5.4.3、解读步骤:

5.4.3.1、找"帧"边界

首先看片选 CSCS 变低,表示一次通信事务的开始;CS 变高,表示事务结束。这框定了整个数据传输的"一帧"。

5.4.3.2、定"采样/变化"时刻

找到核心同步时钟 SCLK 。根据协议规定(这里是SPI模式0),在SCLK的上升沿(边沿1, 3, 5...)采样数据 ,在下降沿(边沿2, 4, 6...)切换数据 。这个规则就是协议的"相位 "和"极性"(CPHA, CPOL)。

5.4.3.3、循迹数据流

对于主设备发送(MOSI) :主设备必须在采样边沿(上升沿)之前t1 时间,就将要发送的位(如D7)稳定在MOSI线上。这个 t1 就是协议层面的建立时间。在上升沿之后,从设备内部的触发器会采样到该位。

对于主设备接收(MISO) :从设备在采样边沿(上升沿)之后t2 时间,才将它的数据位(如D7)放到MISO线上。这个 t2协议层面的输出延迟 。主设备会在下一个上升沿(边沿3) 去采样这条线上的数据。

5.4.3.4、关注关键参数

时序图旁边或下方一定会有一个参数表格,定义了所有时间要求的最小/最大值。例如:

  • t1 (SU): 数据建立时间,例如 > 20ns。
  • t2 (HO): 数据保持时间,例如 > 10ns。
  • t3 (V): 从设备输出有效延迟,例如 < 50ns。
  • 你的软件(驱动)必须配置主设备的SCLK周期足够长,以容纳所有这些延迟,满足最慢从设备的要求!

5.5、 两者的核心区别与内在联系

对比项 区别 内在联系
时钟角色 电路时序 : 全局同步,驱动所有内部状态更新。 协议时序 : 通常是点对点 的通信同步时钟(如SPI的SCLK),或隐含在数据变化中(如UART、I2C的起始位)。 协议中的同步时钟(如SCLK)在接收端,最终会作为其内部某个移位寄存器触发器的时钟输入
"建立/保持时间" 电路时序 : 是晶体管级的物理电气要求 ,必须绝对满足,否则电路失效。 协议时序 : 是系统级的逻辑约定。不满足会导致通信错误,但电路本身可能仍能工作。 协议中定义的 t_SUt_HO,其根本目的 就是为了保证接收端芯片内部触发器 的物理建立/保持时间得到满足。协议时序是电路时序在系统互联层面的体现和保障。
复杂度 电路时序 : 相对简单,信号少,关系直接。 协议时序: 更复杂,包含控制信号(如CS)、数据信号、方向切换、握手交互(如I2C的ACK)等。 复杂的协议时序(如USB、PCIe)最终都被分解为在底层由无数个遵循基本电路时序规则的触发器协同完成。
设计视角 芯片设计者/验证工程师的视角: "我这个触发器能不能可靠工作?" 系统工程师/驱动开发者的视角: "这两个设备能不能成功对话?"

5.6、如何根据协议时序图编写C代码程序

5.6.1、理解协议时序图

  • **确定信号线:**明确有哪些信号线,如时钟线、数据线、片选线等。
  • **确定时序参数:**建立时间、保持时间、时钟频率、数据有效窗口等。
  • **确定数据传输顺序:**数据位的顺序(MSB先还是LSB先)、数据采样边沿(上升沿或下降沿)等。

5.6.2、确定硬件连接

  • 根据硬件原理图,确定MCU的哪些GPIO引脚连接到这些信号线

5.6.3、GPIO初始化

  • 相关GPIO引脚配置为输出或输入,并初始化为空闲状态(例如,片选引脚空闲时为高电平)。

5.6.4、编写底层信号操作函数

5.6.4.1、通常需要编写如下基本函数:
  • 设置时钟线电平

  • 设置数据线电平(对于主机输出)

  • 读取数据线电平(对于主机输入

  • 设置片选线电平

5.6.4.2、注意点:

为了满足时序要求,可能需要在操作后加入短延时(使用空循环或调用微秒级延时函数)。

5.6.5、按照时序图实现数据传输函数

5.6.5.1、以SPI写数据为例:

a、拉低片选信号启动传输)

b、循环8次(每次传输一个字节):
根据时钟极性(CPOL)和相位(CPHA)设置时钟和数据的变化顺序
例如,对于SPI模式0(CPOL=0, CPHA=0):

  • 时钟上升沿采样数据,所以数据在时钟上升沿之前必须稳定。
  • 因此,先设置数据(根据要发送的位),然后拉高时钟(产生上升沿),然后拉低时钟(为下一个位做准备)。
    c、拉高片选信号结束传输)
    5.6.6、考虑时序参数
  • 如果协议对时序有严格要求(如建立时间、保持时间),则需要在操作之间插入延时
  • 延时可以通过空循环(nop指令)硬件定时器实现。

5.6.7、测试与调试

  • 使用逻辑分析仪示波器检查实际信号波形,确保与时序图一致

5.6.8、SPI示例,模拟SPI写一个字节(模式0,MSB先发送)

5.6.8.1、假设硬件连接:

CS -> P1.0

SCLK -> P1.1

MOSI -> P1.2

5.6.8.2、步骤:
5.6.8.2.1、初始化:

将CS、SCLK、MOSI配置为输出,并初始化为高电平(假设高电平为空闲)。

5.6.8.2.2、写字节函数
  • 拉低CS(开始传输

  • 循环8次(i从7到0):

    将SCLK拉低

    将数据字节的第i位(从最高位开始)输出到MOSI

    延时一小段时间(满足建立时间)

    将SCLK拉高(产生上升沿,从机在上升沿采样)

    延时一小段时间(保持时间)

    将SCLK拉低(为下一个位做准备)

  • 拉高CS(结束传输

注意:延时的时间根据具体器件的时序要求确定,可以通过调整循环次数或使用定时器来实现。

5.6.9、C语言代码

5.6.9.1、硬件接口定义
cpp 复制代码
// 1. 引脚宏定义(根据实际硬件连接)
#define SPI_CS_PIN     GPIO_PIN_4
#define SPI_CS_PORT    GPIOA
#define SPI_SCLK_PIN   GPIO_PIN_5
#define SPI_SCLK_PORT  GPIOA
#define SPI_MOSI_PIN   GPIO_PIN_6
#define SPI_MOSI_PORT  GPIOA
#define SPI_MISO_PIN   GPIO_PIN_7
#define SPI_MISO_PORT  GPIOA

// 2. 时序参数(根据数据手册,单位:ns)
#define SPI_T_SU_MIN   10   // 最小建立时间
#define SPI_T_HO_MIN   5    // 最小保持时间
#define SPI_T_CLK_HALF 25   // 半个时钟周期

// 3. 根据MCU时钟频率计算延时循环次数
#define DELAY_LOOP_COUNT(ns) ((ns) * (SystemCoreClock / 1000000) / 1000 / 4)
5.6.9.2、编写底层GPIO操作函数
cpp 复制代码
// 1. 基本GPIO操作(平台无关接口)
static void SPI_GPIO_WritePin(GPIO_TypeDef* port, uint16_t pin, uint8_t state) {
    if (state) {
        port->BSRR = pin;      // 置高
    } else {
        port->BRR = pin;       // 置低
    }
}

static uint8_t SPI_GPIO_ReadPin(GPIO_TypeDef* port, uint16_t pin) {
    return (port->IDR & pin) ? 1 : 0;
}

// 2. 纳秒级延时函数(使用空循环)
static void SPI_Delay_NS(uint32_t ns) {
    uint32_t loops = DELAY_LOOP_COUNT(ns);
    for (volatile uint32_t i = 0; i < loops; i++) {
        __NOP();  // 无操作指令
    }
}
5.6.9.3、实现协议核心函数

方法一:软件模拟SPI(模式0 - CPOL=0, CPHA=0)

cpp 复制代码
/**
  * @brief  SPI软件模拟写一个字节(模式0)
  * @param  data: 要发送的数据
  * @retval 接收到的数据
  */
uint8_t SPI_Soft_WriteByte(uint8_t data) {
    uint8_t i;
    uint8_t received = 0;
    
    // 拉低CS,开始传输
    SPI_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, 0);
    SPI_Delay_NS(SPI_T_SU_MIN);  // CS建立时间
    
    for (i = 0; i < 8; i++) {
        // 设置MOSI(在时钟边沿前稳定数据)
        if (data & 0x80) {  // MSB First
            SPI_GPIO_WritePin(SPI_MOSI_PORT, SPI_MOSI_PIN, 1);
        } else {
            SPI_GPIO_WritePin(SPI_MOSI_PORT, SPI_MOSI_PIN, 0);
        }
        data <<= 1;  // 左移,准备下一位
        
        SPI_Delay_NS(SPI_T_SU_MIN);  // 数据建立时间
        
        // 产生时钟上升沿(从机在此沿采样数据)
        SPI_GPIO_WritePin(SPI_SCLK_PORT, SPI_SCLK_PIN, 1);
        
        // 读取MISO(从机输出)
        received <<= 1;  // 左移接收数据
        if (SPI_GPIO_ReadPin(SPI_MISO_PORT, SPI_MISO_PIN)) {
            received |= 0x01;
        }
        
        SPI_Delay_NS(SPI_T_HO_MIN);  // 数据保持时间
        
        // 产生时钟下降沿,为下一位做准备
        SPI_GPIO_WritePin(SPI_SCLK_PORT, SPI_SCLK_PIN, 0);
        
        SPI_Delay_NS(SPI_T_CLK_HALF);
    }
    
    // 拉高CS,结束传输
    SPI_Delay_NS(SPI_T_HO_MIN);  // 最后一位保持时间
    SPI_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, 1);
    
    return received;
}

方法二:支持多种SPI模式的通用实现

cpp 复制代码
typedef enum {
    SPI_MODE_0 = 0,  // CPOL=0, CPHA=0
    SPI_MODE_1,      // CPOL=0, CPHA=1
    SPI_MODE_2,      // CPOL=1, CPHA=0
    SPI_MODE_3       // CPOL=1, CPHA=1
} SPI_ModeTypeDef;

typedef enum {
    SPI_BITORDER_MSB = 0,
    SPI_BITORDER_LSB
} SPI_BitOrderTypeDef;

typedef struct {
    SPI_ModeTypeDef mode;
    SPI_BitOrderTypeDef bitOrder;
    uint32_t clockSpeed;  // Hz
} SPI_ConfigTypeDef;

static SPI_ConfigTypeDef spi_config = {
    .mode = SPI_MODE_0,
    .bitOrder = SPI_BITORDER_MSB,
    .clockSpeed = 1000000  // 1MHz
};

/**
  * @brief  通用SPI传输函数
  * @param  tx_data: 发送数据缓冲区
  * @param  rx_data: 接收数据缓冲区
  * @param  size: 数据长度
  * @retval 传输状态
  */
uint8_t SPI_Transfer(const uint8_t *tx_data, uint8_t *rx_data, uint16_t size) {
    uint16_t i, j;
    uint8_t tx_byte, rx_byte;
    
    if (size == 0) return 0;
    
    // 根据模式设置初始时钟状态
    uint8_t clk_idle = (spi_config.mode == SPI_MODE_2 || spi_config.mode == SPI_MODE_3) ? 1 : 0;
    uint8_t clk_active = !clk_idle;
    uint8_t sample_edge = (spi_config.mode == SPI_MODE_0 || spi_config.mode == SPI_MODE_2) ? 0 : 1;
    
    // 设置初始时钟状态
    SPI_GPIO_WritePin(SPI_SCLK_PORT, SPI_SCLK_PIN, clk_idle);
    
    // 拉低CS
    SPI_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, 0);
    SPI_Delay_NS(SPI_T_SU_MIN);
    
    for (i = 0; i < size; i++) {
        tx_byte = tx_data ? tx_data[i] : 0xFF;  // 如果只读,发送0xFF
        rx_byte = 0;
        
        for (j = 0; j < 8; j++) {
            uint8_t bit_mask;
            
            // 确定要发送的位
            if (spi_config.bitOrder == SPI_BITORDER_MSB) {
                bit_mask = 0x80 >> j;  // 从最高位开始
            } else {
                bit_mask = 0x01 << j;  // 从最低位开始
            }
            
            // 设置数据位
            SPI_GPIO_WritePin(SPI_MOSI_PORT, SPI_MOSI_PIN, 
                            (tx_byte & bit_mask) ? 1 : 0);
            
            SPI_Delay_NS(SPI_T_SU_MIN);
            
            // 如果采样边沿是时钟变化的第一边沿
            if (sample_edge == 0) {
                // 先变化时钟,然后采样
                SPI_GPIO_WritePin(SPI_SCLK_PORT, SPI_SCLK_PIN, clk_active);
                
                // 读取数据
                if (SPI_GPIO_ReadPin(SPI_MISO_PORT, SPI_MISO_PIN)) {
                    if (spi_config.bitOrder == SPI_BITORDER_MSB) {
                        rx_byte |= (0x80 >> j);
                    } else {
                        rx_byte |= (0x01 << j);
                    }
                }
                
                SPI_Delay_NS(SPI_T_HO_MIN);
                SPI_GPIO_WritePin(SPI_SCLK_PORT, SPI_SCLK_PIN, clk_idle);
            } else {
                // 先采样,然后变化时钟
                // 读取数据(数据在上一时钟边沿已稳定)
                if (SPI_GPIO_ReadPin(SPI_MISO_PORT, SPI_MISO_PIN)) {
                    if (spi_config.bitOrder == SPI_BITORDER_MSB) {
                        rx_byte |= (0x80 >> j);
                    } else {
                        rx_byte |= (0x01 << j);
                    }
                }
                
                SPI_GPIO_WritePin(SPI_SCLK_PORT, SPI_SCLK_PIN, clk_active);
                SPI_Delay_NS(SPI_T_HO_MIN);
                SPI_GPIO_WritePin(SPI_SCLK_PORT, SPI_SCLK_PIN, clk_idle);
            }
            
            SPI_Delay_NS(SPI_T_CLK_HALF);
        }
        
        if (rx_data) {
            rx_data[i] = rx_byte;
        }
    }
    
    // 拉高CS
    SPI_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, 1);
    
    return 1;  // 成功
}
5.6.9.4、封装高层API
cpp 复制代码
// 1. 初始化函数
void SPI_Init(SPI_ConfigTypeDef *config) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    // 配置CS引脚为推挽输出
    GPIO_InitStruct.Pin = SPI_CS_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(SPI_CS_PORT, &GPIO_InitStruct);
    
    // 配置SCLK引脚为推挽输出
    GPIO_InitStruct.Pin = SPI_SCLK_PIN;
    HAL_GPIO_Init(SPI_SCLK_PORT, &GPIO_InitStruct);
    
    // 配置MOSI引脚为推挽输出
    GPIO_InitStruct.Pin = SPI_MOSI_PIN;
    HAL_GPIO_Init(SPI_MOSI_PORT, &GPIO_InitStruct);
    
    // 配置MISO引脚为上拉输入
    GPIO_InitStruct.Pin = SPI_MISO_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(SPI_MISO_PORT, &GPIO_InitStruct);
    
    // 设置初始状态
    SPI_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, 1);  // CS空闲高
    if (config) {
        spi_config = *config;
    }
}

// 2. 单字节读写
uint8_t SPI_ReadByte(void) {
    uint8_t rx_data;
    SPI_Transfer(NULL, &rx_data, 1);
    return rx_data;
}

void SPI_WriteByte(uint8_t data) {
    SPI_Transfer(&data, NULL, 1);
}

uint8_t SPI_ReadWriteByte(uint8_t tx_data) {
    uint8_t rx_data;
    SPI_Transfer(&tx_data, &rx_data, 1);
    return rx_data;
}

// 3. 多字节读写
void SPI_WriteBuffer(const uint8_t *buffer, uint16_t size) {
    SPI_Transfer(buffer, NULL, size);
}

void SPI_ReadBuffer(uint8_t *buffer, uint16_t size) {
    SPI_Transfer(NULL, buffer, size);
}

void SPI_TransferBuffer(const uint8_t *tx_buffer, uint8_t *rx_buffer, uint16_t size) {
    SPI_Transfer(tx_buffer, rx_buffer, size);
}
5.6.9.5、应用层示例
cpp 复制代码
// 示例1:读取SPI Flash设备ID
uint32_t SPI_FLASH_ReadID(void) {
    uint8_t tx_cmd[4] = {0x9F, 0x00, 0x00, 0x00};  // READ_ID命令
    uint8_t rx_data[3] = {0};
    
    SPI_Transfer(tx_cmd, rx_data, 4);  // 发送命令,接收ID
    
    return (rx_data[1] << 16) | (rx_data[2] << 8) | rx_data[3];
}

// 示例2:向SPI设备写入配置寄存器
void SPI_Device_WriteConfig(uint8_t reg_addr, uint8_t reg_value) {
    uint8_t tx_buffer[2];
    
    // 假设写入命令:第一位为0表示写,后跟7位地址
    tx_buffer[0] = reg_addr & 0x7F;  // 最高位清0表示写
    tx_buffer[1] = reg_value;
    
    SPI_Transfer(tx_buffer, NULL, 2);
}

// 示例3:从SPI设备读取数据块
void SPI_Device_ReadData(uint8_t cmd, uint8_t *buffer, uint16_t length) {
    uint8_t tx_cmd = cmd;
    
    SPI_Transfer(&tx_cmd, NULL, 1);      // 发送读命令
    SPI_Transfer(NULL, buffer, length);  // 读取数据
}
5.6.9.6、调试与验证
cpp 复制代码
// 1. 时序测试函数
void SPI_Timing_Test(void) {
    uint8_t test_pattern[8] = {0xAA, 0x55, 0xF0, 0x0F, 0x00, 0xFF, 0x33, 0xCC};
    uint8_t rx_buffer[8] = {0};
    
    printf("SPI时序测试开始...\n");
    
    // 测试1:回环测试(将MOSI与MISO短接)
    printf("测试1:回环测试\n");
    SPI_Transfer(test_pattern, rx_buffer, 8);
    
    for (int i = 0; i < 8; i++) {
        if (test_pattern[i] != rx_buffer[i]) {
            printf("错误:字节%d,发送0x%02X,接收0x%02X\n", 
                   i, test_pattern[i], rx_buffer[i]);
        }
    }
    
    // 测试2:时钟频率测试
    printf("测试2:时钟频率测试\n");
    uint32_t start_time = HAL_GetTick();
    
    for (int i = 0; i < 1000; i++) {
        SPI_ReadWriteByte(0xAA);
    }
    
    uint32_t end_time = HAL_GetTick();
    uint32_t elapsed = end_time - start_time;
    float freq = 1000.0 * 8.0 * 1000.0 / elapsed;  // 估算频率
    
    printf("传输1000字节耗时:%ld ms,估算频率:%.2f KHz\n", elapsed, freq);
    
    // 测试3:时序参数验证
    printf("测试3:时序参数验证\n");
    printf("建立时间t_SU:%d ns\n", SPI_T_SU_MIN);
    printf("保持时间t_HO:%d ns\n", SPI_T_HO_MIN);
    printf("时钟周期:%d ns\n", SPI_T_CLK_HALF * 2);
}

// 2. 信号波形捕获(通过逻辑分析仪)
// 在关键位置添加标记信号
void SPI_Debug_Pulse(void) {
    // 使用一个未用的GPIO引脚作为调试信号
    static uint8_t debug_state = 0;
    debug_state = !debug_state;
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, debug_state);  // 翻转LED或调试引脚
}
5.6.9.7、I2C协议实现要点
cpp 复制代码
// I2C特有:起始条件、停止条件、应答位
void I2C_Start(void) {
    SDA_HIGH(); DELAY();
    SCL_HIGH(); DELAY();
    SDA_LOW();  DELAY();  // 下降沿
    SCL_LOW();  DELAY();
}

uint8_t I2C_WriteByte(uint8_t data) {
    for (int i = 0; i < 8; i++) {
        (data & 0x80) ? SDA_HIGH() : SDA_LOW();
        data <<= 1;
        DELAY();
        SCL_HIGH(); DELAY();  // 时钟高电平期间数据稳定
        SCL_LOW();  DELAY();
    }
    
    // 读取ACK
    SDA_HIGH();  // 释放SDA
    DELAY();
    SCL_HIGH(); DELAY();
    uint8_t ack = !SDA_READ();  // ACK为低电平
    SCL_LOW();  DELAY();
    
    return ack;
}
5.6.9.8、UART协议实现要点
cpp 复制代码
// UART:异步,需要精确的波特率延时
void UART_SendByte(uint8_t data) {
    // 起始位
    TX_LOW();
    DELAY_BIT_TIME();
    
    // 数据位(LSB first)
    for (int i = 0; i < 8; i++) {
        (data & 0x01) ? TX_HIGH() : TX_LOW();
        data >>= 1;
        DELAY_BIT_TIME();
    }
    
    // 停止位
    TX_HIGH();
    DELAY_BIT_TIME();
}
5.6.9.9、1-Wire协议实现要点
cpp 复制代码
// 1-Wire:单总线,需要严格的时序控制
uint8_t OneWire_Reset(void) {
    uint8_t presence = 0;
    
    // 主机拉低480us以上
    DQ_LOW();
    DELAY_US(480);
    
    // 释放总线,等待15-60us
    DQ_HIGH();
    DELAY_US(60);
    
    // 读取从机响应
    presence = !DQ_READ();  // 从机拉低表示存在
    
    // 等待整个时隙结束
    DELAY_US(480);
    
    return presence;
}
5.6.9.10、性能优化
cpp 复制代码
// 使用DMA加速
void SPI_DMA_Transfer(uint8_t *tx_buf, uint8_t *rx_buf, uint16_t size) {
    // 配置DMA
    // 启动传输
    // 等待传输完成标志
}

// 使用硬件SPI外设(如果有)
void SPI_Hardware_Init(void) {
    hspi1.Instance = SPI1;
    hspi1.Init.Mode = SPI_MODE_MASTER;
    hspi1.Init.Direction = SPI_DIRECTION_2LINES;
    hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
    hspi1.Init.NSS = SPI_NSS_SOFT;
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2;
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
    HAL_SPI_Init(&hspi1);
}
5.6.9.11、可移植性优化
cpp 复制代码
// 抽象硬件层
typedef struct {
    void (*pin_write)(void *port, uint16_t pin, uint8_t state);
    uint8_t (*pin_read)(void *port, uint16_t pin);
    void (*delay_ns)(uint32_t ns);
    void (*delay_us)(uint32_t us);
} SPI_HW_Driver;

// 使用函数指针,便于移植
static SPI_HW_Driver spi_driver;

void SPI_SetDriver(SPI_HW_Driver *driver) {
    spi_driver = *driver;
}
5.6.9.12、错误处理与重试
cpp 复制代码
#define SPI_MAX_RETRIES 3

uint8_t SPI_Transfer_WithRetry(const uint8_t *tx_data, uint8_t *rx_data, 
                               uint16_t size, uint8_t retries) {
    uint8_t attempt;
    
    for (attempt = 0; attempt < retries; attempt++) {
        if (SPI_Transfer(tx_data, rx_data, size)) {
            // 验证数据(如果有验证机制)
            if (SPI_ValidateData(rx_data, size)) {
                return 1;  // 成功
            }
        }
        
        // 失败后延时重试
        DELAY_US(100);
        printf("SPI传输失败,第%d次重试\n", attempt + 1);
    }
    
    return 0;  // 所有重试都失败
}

5.7**、总结**

  • 数字电路时序(微观): 是规则本身的物理学基础,告诉你一个存储单元如何可靠地存储一个比特。

  • 协议总线时序(宏观): 是运用规则的通信约定,告诉你如何用一连串的比特(和对应的控制信号)在设备间传递有意义的信息。

当你阅读一份芯片数据手册中的总线时序图时,你正在以一位"系统指挥家"的视角,学习如何用精确的时间节拍,指挥各个设备(乐器)协同演奏出正确的数据乐章。而这份乐谱能演奏的前提,是每个乐器内部的机械结构(底层电路时序)本身是完好且精密的。

六、数字编码与译码器

6.1、数字编码基础框图

6.2、编码器详解框图

6.3、译码器详解(重点)

6.3.1、基本概念

译码器:将二进制代码转换为对应输出信号的组合逻辑电路(编码的逆过程)。

6.3.2、二进制译码器(3线-8线为例)

逻辑符号与功能

复制代码
         ┌─────────────┐
     A2 ─┤             ├─ Y7
     A1 ─┤             │
     A0 ─┤             ├─ Y6
         │    3-8      │
     E1 ─┤  译码器     ├─ Y5
     E2'─┤             │
     E3'─┤             ├─ Y4
         │             │
         │             ├─ Y3
         │             │
         │             ├─ Y2
         │             │
         │             ├─ Y1
         │             │
         └─────────────┘─ Y0

真值表(74LS138为例,输出低有效)

E1 E2' E3' A2 A1 A0 Y7' Y6' Y5' Y4' Y3' Y2' Y1' Y0'
X 1 X X X X 1 1 1 1 1 1 1 1
X X 1 X X X 1 1 1 1 1 1 1 1
0 X X X X X 1 1 1 1 1 1 1 1
1 0 0 0 0 0 1 1 1 1 1 1 1 0
1 0 0 0 0 1 1 1 1 1 1 1 0 1
... ... ... ... ... ... ... ... ... ... ... ... ... ...
1 0 0 1 1 1 0 1 1 1 1 1 1 1

逻辑表达式(每个输出对应一个最小项的非):

复制代码
Y0' = (A2'·A1'·A0')' = m0'
Y1' = (A2'·A1'·A0)'  = m1'
Y2' = (A2'·A1·A0')'  = m2'
...
Y7' = (A2·A1·A0)'    = m7'

内部结构示意图

复制代码
    A2 ─┬─\ │   ┌────┐
        │   ├───┤    │
    A1 ─┼─/ │   │    │
        │   ├───┤    │
    A0 ─┼─\ │   │ 8  │
        │   ├───┤输入│── Y0'
        ├─/ │   │与非│
        │   ├───┤门  │── Y1'
    E1 ─┼─\ │   │    │
        │   ├───┤    │── ...
E2'+E3'─┴─/ │   │    │
            └───┘    │── Y7'

6.3.3 、译码器应用

6.3.3.1、地址译码(微处理器系统)
复制代码
         ┌─────────┐   ┌──────┐
地址总线─┤3-8译码器├───┤CS RAM├── 存储器1
A15,A14, │         │   └──────┘
A13      ├─────────┼───┌──────┐
         │         │   │CS ROM├── 存储器2
         ├─────────┼───└──────┘
         │         │
         ├─────────┼───┌──────┐
         │         │   │CS I/O├── 外设接口
         └─────────┘   └──────┘
6.3.3.2、实现任意组合逻辑函数

利用译码器输出是最小项的特性:

例:实现 F(A,B,C) = Σm(1,3,5,7)

复制代码
    A ─┐
    B ─┤  3-8 ── m1' ─┐
    C ─┤ 译码 ── m3' ─┤
       │ 器   ── m5' ─┼──┤
       │      ── m7' ─┤  │
       └──────────────┘  └──┴── F
6.3.3.3、数码管显示译码器(七段译码器)
复制代码
         ┌─────────────┐
     D3 ─┤             ├─ a
     D2 ─┤             ├─ b
     D1 ─┤  BCD-七段   ├─ c
     D0 ─┤   译码器    ├─ d
         │             ├─ e
  LT' ───┤             ├─ f
  BI' ───┤             ├─ g
  RBI'───┤             │
         └─────────────┘
6.3.3.4、七段数码管段位图
复制代码
    a
   ───
f │   │ b
   ───  ← g
e │   │ c
   ───
    d
6.3.3.5、部分真值表(74LS48,共阴驱动)
D3 D2 D1 D0 显示 a b c d e f g
0 0 0 0 0 1 1 1 1 1 1 0
0 0 0 1 1 0 1 1 0 0 0 0
0 0 1 0 2 1 1 0 1 1 0 1
0 0 1 1 3 1 1 1 1 0 0 1
... ... ...
1 0 0 1 9 1 1 1 1 0 1 1
6.3.3.6、控制信号功能
  • LT'(灯测试):低电平时所有段亮

  • BI'(消隐):低电平时所有段灭

  • RBI'(动态灭零):高位数码为零时自动熄灭

6.4、编码器与译码器对比

特性 编码器 译码器
功能 输入→代码(压缩) 代码→输出(扩展)
输入数 2^n (或10等) n (二进制位数)
输出数 n (二进制位数) 2^n (或7段等)
典型芯片 74LS148 (8-3优先) 74LS138 (3-8译码)
使能控制 EI'(输入使能) E1, E2', E3'(组合使能)
有效电平 输入/输出常为低有效 输出常为低有效
级联扩展 通过EO'、EI'级联 通过使能端级联

6.5、高级应用与扩展框图

6.6、设计要点总结

  • 有效电平:明确输入输出是高有效还是低有效
  • 使能控制:合理使用使能端实现控制和级联
  • 优先级:编码器需考虑多输入时的优先级处理
  • 扩展性:预留扩展接口,便于系统升级
  • 延迟:注意编码/译码过程的传播延迟对系统速度的影响
  • 功耗:CMOS器件注意未用输入端的处理

七、计算器/定时器与状态机

7.1、计数器/定时器详解

计数器是时序逻辑电路最经典的应用之一,其本质是一个状态机,其状态按照一个固定的序列(递增、递减、或任意规律) 循环变化。

7.1.1、核心概念与分类

计数器对输入时钟脉冲的个数进行计数。每来一个有效时钟边沿,其输出状态(计数值)就改变一次。

7.1.1.1按触发方式
  • 同步计数器:所有触发器使用同一个时钟信号。速度快,无毛刺,是现代设计主流。

  • 异步计数器:前一级触发器的输出作为后一级的时钟。电路简单,但速度慢,存在累积延迟和毛刺。

7.1.1.2按计数方向
  • 加法计数器:状态递增(0,1,2,3...)

  • 减法计数器:状态递减(7,6,5,4...)

  • 可逆计数器:可通过控制信号选择加或减。

7.1.1.3按计数模值
  • 二进制计数器:模值为2^n(如4位二进制计数器,模16,计数0~15)。

  • 十进制/任意模值计数器:模值为非2^n(如模10计数器,计数0~9)。

7.1.2、原理与图解:以4位同步二进制加法计数器为例

电路核心思想:使用D触发器或JK触发器,并用当前状态的组合逻辑,来产生下一时刻(下一个时钟沿)应该翻转的触发器的控制信号

7.1.2.1状态转移表

目标是实现:0000 -> 0001 -> 0010 -> ... -> 1111 -> 0000

7.1.2.2关键发现(翻转规律)

观察二进制加法:

  • 最低位 (Q0)每个时钟脉冲都翻转一次

  • 次低位 (Q1)仅当 Q0=1 时,下一个时钟脉冲才翻转

  • 第三位 (Q2)仅当 Q0=1 且 Q1=1 (即 Q0·Q1 = 1) 时,下一个时钟脉冲才翻转

  • 最高位 (Q3)仅当 Q0=1 且 Q1=1 且 Q2=1 (即 Q0·Q1·Q2 = 1) 时,下一个时钟脉冲才翻转

这是一个 "逢n进1" 的规律。

7.1.2.3内部逻辑结构(使用T触发器或D触发器实现T功能)

一个T触发器在T=1时翻转,T=0时保持。根据上述规律,可以构造控制逻辑:

复制代码
            ┌───┐       ┌───┐       ┌───┐       ┌───┐
时钟CLK ────┤>  │       │>  │       │>  │       │>  │
            │ T │ Q0    │ T │ Q1    │ T │ Q2    │ T │ Q3
            └─┬─┘       └─┬─┘       └─┬─┘       └─┬─┘
              │ T0=1      │ T1=Q0     │ T2=Q0·Q1  │ T3=Q0·Q1·Q2
              └───────────┴───────────┴───────────┘
  • T0 恒为1 → Q0每个时钟都翻转。

  • T1 = Q0 → 只有当Q0为1(即上一个时钟Q0刚翻转为1)时,T1才为1,Q1才能在下一个时钟翻转。这实现了"Q0为1时,Q1准备翻转"。

  • T2 = Q0·Q1 → 只有当Q0和Q1同时为1时,T2才为1。

  • T3 = Q0·Q1·Q2 → 同理。

7.1.2.4波形图
复制代码
CLK  _┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌┐ ┌_
        ┘└┘└┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └┘ └
Q0   ___┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌
        └─┘   └─┘   └─┘   └─┘   └─┘   └─┘   └─┘
Q1   _______┐   ┌───────┐   ┌───────┐   ┌───────┐
            └───┘       └───┘       └───┘       └───
Q2   _______________┐       ┌───────────────┐
                    └───────┘               └───────
Q3   _______________________________┐       ┌───────
                                    └───────┘
计数  0   1   2   3   4   5   6   7   8   9  10  11 ...

7.1.3、在单片机中的运用

在单片机内部,计数器是一个极其重要的硬件外设,通常称为 Timer/Counter

7.1.3.1、基本结构
复制代码
┌─────────────────────────────────┐
│         单片机内部              │
│  ┌──────────────┐               │
│  │  预分频器    │  可编程分频   │
│  │ (Prescaler)  │◄──────────┐   │
│  └──────┬───────┘           │   │
│         │             系统时钟  │
│         ▼                   │   │
│  ┌──────────────┐           │   │
│  │ 计数器寄存器 │ 计数脉冲来源  │
│  │  (CNT)       │◄──────────┤   │
│  └──────┬───────┘ (可内部/外部) │
│         │                       │
│         ▼                       │
│  ┌──────────────┐               │
│  │自动重载寄存器│ 设定计数目标值│
│  │   (ARR)      │────────────┘  │
│  └──────────────┘               │
│         │                       │
│         └───► 比较/溢出中断     │
└─────────────────────────────────┘
7.1.3.2、核心应用
7.1.3.2.1、精确定时
  • 应用 :实现delay_ms()、任务调度、数据采样间隔。
  • 原理 :对稳定的系统时钟进行计数。例如,系统时钟72MHz,预分频72,则计数器每1µs加1。设置ARR=999,则每(999+1)*1µs = 1ms产生一次溢出中断。
7.1.3.2.2、脉冲计数
  • 原理:将外部引脚信号作为计数时钟源。
  • 应用:测量编码器转速、统计产品数量。
7.1.3.2.3、PWM(脉宽调制)输出
  • 原理 :计数器在0到ARR之间循环,同时设置一个比较寄存器CCRx。当CNT < CCRx时,输出高电平;CNT >= CCRx时,输出低电平。通过改变CCRx的值,即可改变输出方波的占空比
  • 应用控制LED亮度电机速度、生成模拟电压。
7.1.3.2.4、输入捕获
  • 原理 :当外部引脚发生跳变时,硬件自动锁存当前CNT的值。通过计算两次跳变间的计数值差,可精确测量脉冲宽度或周期
  • 应用 :测量超声波回波时间、解码红外遥控信号

7.2、状态机详解

状态机是描述和控制具有不同状态,并能根据输入在不同状态间转移的系统行为的数学模型和设计方法。

7.2.1、核心概念与模型

7.2.1.1、状态机三要素
  • 状态:系统所处的模式或阶段(如:空闲发送中等待应答)。
  • 事件/输入:触发状态转移的条件(如:按键按下数据到达定时器超时)。
  • 动作/输出:在进入某个状态、退出某个状态或转移过程中执行的操作(如:点亮LED发送数据包清除标志位)。

7.2.2、 两种经典模型

7.2.1.1、摩尔型状态机
  • 输出仅与当前状态有关

  • 像一个人,他的表情(输出)只取决于他当前的心情(状态)。

  • 电路实现相对简单,但输出可能比输入晚一个时钟周期。

7.2.1.2、米利型状态机
  • 输出当前状态当前输入都有关。

  • 像自动门,它的开关动作(输出)不仅取决于当前是开是关(状态),还取决于此刻是否有人靠近(输入)。

  • 输出响应更快,但对输入变化敏感,可能产生毛刺。

7.2.3、设计步骤与图解

以设计一个简单的 "按键消抖与检测" 状态机为例(检测一个按键的稳定按下)。

7.2.3.1、状态定义
  • IDLE: 空闲状态,按键未按下。

  • DEBOUNCE: 消抖状态,检测到边沿后等待一段时间。

  • PRESSED: 确认按下状态。

7.2.3.2、绘制状态转移图
复制代码
         +-------------------+
         |                   |
         |       IDLE        |<-----------------+
         | (Key=1, Out=0)    |                  |
         +---------+---------+                  |
                   | Key下降沿(检测到0)         |
                   | 启动20ms定时器             |
                   v                            |
         +-------------------+   定时器超时     |
         |                   |   (Key仍为0)     |
         |    DEBOUNCE       +------------------+
         | (等待20ms,Out=0) |                  |
         +---------+---------+                  |
                   | 定时器超时                 |
                   | (Key为1)                   |
                   v (误触发,返回)             |
         +-------------------+                  |
         |                   |                  |
         |     PRESSED       |  Key释放(上升沿) |
         | (Out=1, 触发事件) | 启动20ms定时器   |
         +-------------------+------------------+
                           定时器超时
                           (Key仍为1)
  • 圆圈:状态

  • 箭头:转移条件

  • 标注转移条件 / 执行的动作

7.2.3.3、状态编码与实现

用两个触发器可以表示最多4个状态。例如:

  • IDLE = 00

  • DEBOUNCE = 01

  • PRESSED = 10

然后根据状态图,列出状态转移表,写出每个触发器的次态方程,最后用组合逻辑和D触发器实现。

7.2.4、在单片机中的运用(软件状态机)

在单片机编程中,状态机 是最重要的编程思想之一,尤其适用于事件驱动、多任务协调的场景。

7.2.4.1、实现方式

通常用 switch-case 语句函数指针数组实现。

7.2.4.2、C语言示例(按键检测状态机)
cpp 复制代码
typedef enum {
    STATE_IDLE,
    STATE_DEBOUNCE,
    STATE_PRESSED
} KeyState_t;

KeyState_t keyState = STATE_IDLE;

void KeyScan_Task_10ms(void) { // 每10ms调用一次
    static uint8_t timer = 0;
    uint8_t keyCurrent = READ_KEY_PIN(); // 读取当前按键电平

    switch (keyState) {
        case STATE_IDLE:
            if (keyCurrent == 0) { // 检测到下降沿
                keyState = STATE_DEBOUNCE;
                timer = 2; // 需要20ms (2 * 10ms)
            }
            break;

        case STATE_DEBOUNCE:
            if (--timer == 0) {
                if (keyCurrent == 0) { // 20ms后仍为低,确认按下
                    keyState = STATE_PRESSED;
                    OnKeyPressed(); // 执行按键按下的动作
                } else {
                    keyState = STATE_IDLE; // 是抖动,返回
                }
            }
            break;

        case STATE_PRESSED:
            if (keyCurrent == 1) { // 检测到释放
                keyState = STATE_IDLE;
                OnKeyReleased(); // 执行释放动作
            }
            break;

        default:
            keyState = STATE_IDLE;
            break;
    }
}
7.2.4.3、经典应用场景
7.2.4.3.1、通信协议解析

UART、I2C、SPI等驱动层,状态包括起始位数据位校验位停止位

7.2.4.3.2、用户界面

菜单系统,每个菜单页是一个状态。

7.2.4.3.3、控制系统

如智能家居的"离家模式"、"回家模式"、"睡眠模式"。

7.2.4.3.4、任务调度

一个复杂任务分解为多个状态步骤(初始化 -> 等待数据 -> 处理数据 -> 发送结果)。

相关推荐
Scott9999HH3 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社4 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海4 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖5 小时前
JDK 26 新特性详解
java·开发语言
马优晨6 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区8 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大8 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai8 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜9 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa9 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio