标签 嵌入式开发 下的文章

大家好,我是良许。

今天咱们来聊聊 51 单片机。

作为嵌入式开发领域的"老前辈",51 单片机陪伴了无数工程师走过了学习和工作的岁月。

虽然现在 STM32、ESP32 等新一代单片机层出不穷,但 51 单片机依然在某些场景下发挥着不可替代的作用。

那么,51 单片机到底有哪些优缺点呢?

今天我就从实际开发的角度,给大家详细分析一下。

1. 51 单片机的主要优点

1.1 学习门槛低,上手快

51 单片机最大的优点就是简单易学。

它的指令集只有 111 条,相比 ARM Cortex-M 系列动辄上百条指令,学习负担要轻很多。

对于刚入门的同学来说,不需要掌握太多复杂的概念就能开始写程序。

我记得当年读大学的时候,第一次接触单片机就是从 51 开始的。

那时候用 Keil C51 编译器,写个流水灯程序也就几十行代码,调试起来也很直观。

这种"所见即所得"的学习体验,让我很快就建立了对嵌入式开发的信心。

#include <reg51.h>
​
void delay(unsigned int ms) {
    unsigned int i? j;
    for(i = 0; i < ms; i++)
        for(j = 0; j < 120; j++);
}
​
void main() {
    unsigned char led = 0xFE;  // 初始状态:P0.0点亮
    
    while(1) {
        P0 = led;              // 输出到P0口
        delay(500);            // 延时500ms
        led = (led << 1) | 0x01;  // 左移一位
        if(led == 0xFF)        // 全灭后重新开始
            led = 0xFE;
    }
}

这段流水灯代码非常简单,即使是零基础的同学看几遍也能理解。

这就是 51 单片机的魅力所在——它不会让你在一开始就被复杂的寄存器配置、时钟树、中断向量表等概念搞晕。

1.2 资料丰富,社区成熟

51 单片机诞生于 1980 年代,经过几十年的发展,相关的学习资料、开发工具、例程代码可以说是铺天盖地。

无论你遇到什么问题,基本上都能在网上找到解决方案。

这对于自学者来说是非常友好的。

我在做嵌入式开发的这些年里,经常会在一些论坛、贴吧看到关于 51 单片机的讨论。

即使是十几年前的帖子,里面的技术方案现在依然适用。

这种技术的延续性和稳定性,是很多新兴平台无法比拟的。

而且,51 单片机的开发板、仿真器价格都非常便宜。

一套完整的学习套件可能只需要几十块钱,这对于学生党来说非常友好。

我当年买的第一块 51 开发板才 35 块钱,上面集成了 LED、数码管、按键、蜂鸣器等常用外设,足够完成大部分基础实验了。

1.3 成本低廉,适合批量生产

在商业应用中,成本控制是非常重要的考量因素。

51 单片机的价格通常在几毛钱到几块钱之间,这对于需要大批量生产的产品来说是个巨大的优势。

比如说,一些简单的家电控制器、玩具、小家电等产品,功能需求并不复杂,用 51 单片机完全可以满足。

我之前接触过一个做电动车仪表盘的项目,客户最终选择了 STC89C52 作为主控芯片,原因就是成本低、供货稳定。

这个项目每年的出货量在几十万台,单片机成本每降低 1 毛钱,一年就能省下好几万。

1.4 功耗较低,适合电池供电场景

51 单片机的功耗相对较低,特别是国产的 STC 系列,在休眠模式下电流可以降到微安级别。

这使得它非常适合一些需要电池供电的场景,比如遥控器、无线传感器节点等。

#include <STC89C5xRC.h>
​
void enter_power_down() {
    EA = 0;           // 关闭总中断
    PCON |= 0x02;     // 进入掉电模式
    _nop_();
    _nop_();
}
​
void main() {
    // 初始化配置
    P1 = 0xFF;        // 设置P1口为高电平
    
    while(1) {
        // 执行一些任务
        // ...
        
        // 进入低功耗模式
        enter_power_down();
        
        // 被外部中断唤醒后继续执行
    }
}

通过合理的电源管理,51 单片机可以在电池供电的情况下工作很长时间。

我曾经做过一个无线温度采集器的项目,使用两节 AA 电池,通过让单片机大部分时间处于休眠状态,只在需要采集数据时唤醒,最终实现了一年以上的续航时间。

1.5 结构简单,便于理解底层原理

51 单片机的内部结构相对简单,包括 CPU、RAM、ROM、定时器、串口等基本模块。

这种简单的架构非常适合用来学习计算机组成原理和嵌入式系统的基本概念。

通过学习 51 单片机,你可以清楚地了解到程序是如何在硬件上运行的,寄存器是如何控制外设的,中断机制是如何工作的。

这些底层知识对于后续学习更复杂的 ARM、RISC-V 等架构都有很大帮助。

2. 51 单片机的主要缺点

2.1 性能有限,处理能力较弱

51 单片机的主频通常在 12MHz 到 40MHz 之间,即使是增强型的 STC15 系列,主频也不过 30MHz 左右。

这个性能在今天看来确实比较弱。

如果你的项目需要进行复杂的数学运算、图像处理、或者需要运行操作系统,51 单片机就力不从心了。

我在实际工作中遇到过这样的情况:客户要求在产品上增加一个 FFT(快速傅里叶变换)算法来分析音频信号。

原本使用的是 STC89C52,结果发现计算一次 FFT 需要好几秒钟,完全无法满足实时性要求。

最后不得不更换为 STM32F103,问题才得以解决。

而且,51 单片机是 8 位架构,处理 16 位或 32 位数据时需要多次操作,效率很低。

比如做一个简单的 32 位加法:

// 51单片机处理32位加法需要分步进行
unsigned long add32(unsigned long a, unsigned long b) {
    unsigned long result;
    unsigned char *pa = (unsigned char *)&a;
    unsigned char *pb = (unsigned char *)&b;
    unsigned char *pr = (unsigned char *)&result;
    unsigned char carry = 0;
    
    // 需要逐字节相加,并处理进位
    pr[0] = pa[0] + pb[0];
    carry = (pr[0] < pa[0]) ? 1 : 0;
    
    pr[1] = pa[1] + pb[1] + carry;
    carry = (pr[1] < pa[1]) ? 1 : 0;
    
    pr[2] = pa[2] + pb[2] + carry;
    carry = (pr[2] < pa[2]) ? 1 : 0;
    
    pr[3] = pa[3] + pb[3] + carry;
    
    return result;
}

而在 32 位的 STM32 上,这只需要一条指令就能完成。

这种性能差距在处理大量数据时会非常明显。

2.2 存储空间小,难以支持复杂应用

经典的 51 单片机内部 RAM 只有 128 字节,即使是增强型的也不过 512 字节到 4KB。

这点内存在现在看来实在是太小了。

如果你的程序需要处理较大的数组、缓冲区,或者需要实现复杂的数据结构,51 单片机就会捉襟见肘。

我记得有一次做一个数据采集项目,需要缓存 1000 个采样点的数据。

每个采样点是 2 字节的整数,总共需要 2KB 的 RAM。

这对于 51 单片机来说几乎是不可能完成的任务。

虽然可以通过外扩 RAM 来解决,但这会增加硬件成本和设计复杂度。

程序存储空间方面,虽然现在的 51 单片机 Flash 可以做到 64KB 甚至更大,但相比 STM32 动辄几百 KB、上 MB 的 Flash,还是显得捉襟见肘。

如果你的项目需要存储大量的字库、图片资源、或者需要实现 OTA 升级功能,51 单片机就很难胜任了。

2.3 外设功能单一,扩展性差

51 单片机的片上外设比较简单,通常只有定时器、串口、外部中断等基本功能。

如果你需要使用 SPI、I2C、CAN、USB 等现代通信接口,就需要通过软件模拟或者外接专用芯片来实现。

软件模拟的方式虽然可行,但会占用大量的 CPU 时间,而且时序控制不够精确。

比如用 51 单片机模拟 I2C 通信:

#include <reg51.h>
​
sbit SDA = P1^0;
sbit SCL = P1^1;
​
void i2c_delay() {
    unsigned char i = 5;
    while(i--);
}
​
void i2c_start() {
    SDA = 1;
    SCL = 1;
    i2c_delay();
    SDA = 0;
    i2c_delay();
    SCL = 0;
}
​
void i2c_stop() {
    SDA = 0;
    SCL = 1;
    i2c_delay();
    SDA = 1;
    i2c_delay();
}
​
void i2c_write_byte(unsigned char dat) {
    unsigned char i;
    for(i = 0; i < 8; i++) {
        SDA = (dat & 0x80) ? 1 : 0;
        dat <<= 1;
        i2c_delay();
        SCL = 1;
        i2c_delay();
        SCL = 0;
    }
}

这种软件模拟的方式不仅代码冗长,而且在高速通信时容易出现时序问题。

而 STM32 的硬件 I2C 外设只需要简单配置几个寄存器,就能实现稳定可靠的通信,还支持 DMA 传输,完全不占用 CPU 时间。

2.4 开发工具相对落后

51 单片机的主流开发工具是 Keil C51,虽然功能还算完善,但相比现代的 IDE(比如 STM32CubeIDE、VS Code 等),在代码提示、调试功能、版本控制集成等方面都显得比较落后。

而且,51 单片机的仿真调试功能比较有限。

很多时候我们只能通过串口打印信息来调试程序,或者使用 LED 闪烁来判断程序运行状态。

这种原始的调试方式效率很低,特别是在排查复杂问题时,往往需要花费大量时间。

相比之下,STM32 可以使用 ST-Link 进行在线调试,支持断点、单步执行、变量监视等功能,大大提高了开发效率。

我现在做项目基本都是用 STM32,配合 HAL 库和 CubeMX 图形化配置工具,开发效率比用 51 单片机高了不知道多少倍。

2.5 生态系统相对封闭

51 单片机虽然资料很多,但大多是一些基础的例程和教程,缺乏成熟的软件框架和中间件支持。

如果你想实现一些复杂的功能,比如文件系统、网络协议栈、图形界面等,基本上需要从零开始写,或者移植其他平台的代码,工作量非常大。

而像 STM32 这样的平台,有 ST 官方提供的 HAL 库、LL 库,还有大量的第三方库和开源项目可以直接使用。

比如 FreeRTOS、LwIP、FatFS、emWin 等成熟的软件组件,可以大大缩短开发周期。

3. 51 单片机的适用场景

说了这么多优缺点,那么 51 单片机到底适合用在什么场景呢?

根据我的经验,以下几种情况可以考虑使用 51 单片机:

3.1 教学和学习

对于刚入门的学生来说,51 单片机是非常好的学习平台。

它能让你快速建立对嵌入式系统的认知,理解程序是如何控制硬件的。

而且学习成本低,不需要购买昂贵的开发工具。

3.2 简单的控制应用

如果你的项目只是做一些简单的逻辑控制,比如 LED 控制、继电器开关、简单的传感器读取等,51 单片机完全可以胜任。

而且成本低廉,适合大批量生产。

3.3 对功耗敏感的应用

在一些需要电池供电、对功耗要求严格的场景,51 单片机(特别是 STC 系列)的低功耗特性可以发挥优势。

3.4 对实时性要求不高的应用

如果你的应用不需要复杂的运算,不需要处理大量数据,对响应时间要求不高,51 单片机是个经济实惠的选择。

4. 总结

51 单片机作为嵌入式领域的经典产品,有着学习门槛低、成本低廉、资料丰富等优点,非常适合入门学习和简单应用。

但它的性能有限、存储空间小、外设功能单一等缺点,也限制了它在现代复杂应用中的使用。

对于初学者来说,我建议先从 51 单片机入手,打好基础,理解嵌入式系统的基本概念。

等掌握了基本原理后,再学习 STM32 等更强大的平台,这样的学习路径会比较平滑。

而对于实际项目开发,则需要根据具体需求来选择合适的平台,不能盲目追求新技术,也不能固守老平台。

我自己的经历就是最好的例证:从 51 单片机起步,逐步过渡到 STM32,再到现在做 Linux 应用开发。

每个阶段的学习都为下一阶段打下了基础。

技术在不断进步,但基本原理是相通的。

希望这篇文章能帮助大家更好地理解 51 单片机,在学习和工作中做出正确的技术选择。

更多编程学习资源

大家好,我是良许。

在嵌入式开发中,IIC(I2C)总线可以说是最常用的通信协议之一了。

无论是读取传感器数据、控制EEPROM存储器,还是与各种外设进行通信,IIC总线都扮演着重要角色。

但很多初学者在使用IIC时,往往只关注软件层面的时序和协议,却忽略了硬件层面的关键设计。

今天我就来聊聊IIC总线硬件部分的两个核心要点:开漏输出和上拉电阻。

理解了这两点,你才能真正掌握IIC总线的精髓。

1. IIC总线的基本结构

在深入讲解之前,我们先简单回顾一下IIC总线的基本构成。

IIC总线只需要两根信号线就能实现多主机、多从机之间的通信,这两根线分别是:

  • SCL(Serial Clock):时钟线,由主机产生时钟信号
  • SDA(Serial Data):数据线,用于主从设备之间的数据传输

一条IIC总线上可以挂载多个设备,每个设备都有唯一的地址。

这种简洁的设计让IIC总线在嵌入式系统中广受欢迎,特别是在PCB布线空间有限的场景下。

但问题来了:多个设备共用同一根数据线和时钟线,它们是如何避免冲突的呢?这就要说到IIC总线硬件设计的核心机制了。

2. 开漏输出:IIC总线的灵魂

2.1 什么是开漏输出

开漏输出(Open-Drain)是IIC总线最核心的硬件特性。

要理解开漏输出,我们先来看看常见的GPIO输出模式。

在普通的推挽输出(Push-Pull)模式下,GPIO引脚可以主动输出高电平(通过上管导通)或低电平(通过下管导通)。

这种模式下,引脚能够提供较强的驱动能力,但有个致命问题:如果两个推挽输出的引脚连接在一起,一个输出高电平,另一个输出低电平,就会造成短路,可能烧毁芯片。

而开漏输出则不同,它的内部结构只有一个下拉的NMOS管,没有上拉的PMOS管。这意味着:

  • 当GPIO输出低电平时,NMOS管导通,引脚被拉到地(GND),呈现低电平
  • 当GPIO输出高电平时,NMOS管截止,引脚呈现高阻态(既不输出高也不输出低)

这种"只能拉低,不能拉高"的特性,正是开漏输出的精髓所在。

2.2 开漏输出的优势

你可能会问:只能拉低不能拉高,这不是很鸡肋吗?恰恰相反,这正是IIC总线能够实现多设备共享总线的关键。

第一个优势:线与逻辑

多个开漏输出连接在同一根线上时,会形成"线与"(Wired-AND)逻辑。

只要有任何一个设备输出低电平,整条总线就是低电平;只有当所有设备都输出高阻态时,总线才能被上拉电阻拉到高电平。

这种特性在IIC总线中至关重要。

比如在多主机系统中,如果两个主机同时发送数据产生冲突,通过检测总线电平,主机可以发现冲突并进行仲裁。

发送"1"的主机如果检测到总线为"0",就知道有其他主机在发送数据,会主动放弃总线控制权。

第二个优势:电平转换

开漏输出配合上拉电阻,可以轻松实现不同电压域之间的电平转换。

比如一个3.3V的MCU和一个5V的传感器通信,只需要将上拉电阻接到5V电源,就能实现电平匹配。

3.3V的MCU输出低电平时可以将总线拉低,输出高阻态时总线被上拉到5V,这个5V电平不会损坏MCU(因为MCU引脚是高阻态,没有电流流入)。

第三个优势:避免总线冲突

在推挽输出模式下,如果两个设备同时驱动总线,一个输出高一个输出低,就会造成短路。

而开漏输出永远不会主动输出高电平,最多只是高阻态,因此不会产生短路风险。

2.3 STM32中的开漏配置

在STM32中配置IIC引脚为开漏输出非常简单。

使用HAL库的话,代码如下:

void MX_I2C1_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    /* 使能GPIOB时钟 */
    __HAL_RCC_GPIOB_CLK_ENABLE();
    
    /* 配置IIC引脚:PB6(SCL), PB7(SDA) */
    GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;  // 复用开漏输出
    GPIO_InitStruct.Pull = GPIO_NOPULL;      // 不使用内部上下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF4_I2C1;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    
    /* 配置IIC外设 */
    hi2c1.Instance = I2C1;
    hi2c1.Init.ClockSpeed = 100000;  // 100kHz标准速率
    hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
    hi2c1.Init.OwnAddress1 = 0;
    hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
    hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
    HAL_I2C_Init(&hi2c1);
}

注意代码中的 GPIO_MODE_AF_OD,这就是配置为复用功能的开漏输出模式。

同时 GPIO_NOPULL 表示不使用芯片内部的上下拉电阻,因为我们需要外部上拉电阻。

3. 上拉电阻:开漏输出的最佳拍档

3.1 为什么需要上拉电阻

前面提到,开漏输出只能拉低电平,不能主动输出高电平。

那么高电平从哪里来呢?答案就是上拉电阻。

上拉电阻一端连接到电源(通常是VCC),另一端连接到IIC总线。

当所有设备的开漏输出都处于高阻态时,上拉电阻会将总线"拉"到高电平。

当任何一个设备输出低电平时,由于低电平的驱动能力远强于上拉电阻,总线会被拉到低电平。

可以把上拉电阻想象成一根弹簧,总是试图把总线拉到高电平。

而开漏输出就像一只手,需要的时候可以把总线按下去(拉低),松开手(高阻态)时弹簧就会把总线弹回高电平。

3.2 上拉电阻的阻值选择

上拉电阻的阻值选择是个技术活,选大了选小了都不行。

阻值太小的问题:

如果上拉电阻太小(比如1kΩ),虽然可以提供很强的上拉能力,但会带来两个问题:

  1. 功耗增加。当总线被拉低时,会有较大的电流从VCC经过上拉电阻流向GND,计算公式为I=VCC/Rpullup。以3.3V系统为例,1kΩ电阻会产生3.3mA的电流,在低功耗应用中这是不可接受的。
  2. 增加驱动负担。开漏输出需要吸收更大的电流才能将总线拉低,可能超出芯片的驱动能力。

阻值太大的问题:

如果上拉电阻太大(比如100kΩ),上拉能力会变弱,带来的问题是:

  1. 上升沿变慢。总线电容(包括走线电容、引脚电容等)需要通过上拉电阻充电才能从低电平变为高电平。阻值越大,充电时间越长,上升沿越慢。时间常数可以用 τ=R×C 计算。
  2. 抗干扰能力下降。较弱的上拉能力使得总线更容易受到外部干扰的影响。

合适的阻值范围:

一般来说,IIC总线的上拉电阻推荐范围是:

  • 标准速率(100kHz):4.7kΩ ~ 10kΩ
  • 快速模式(400kHz):2.2kΩ ~ 4.7kΩ
  • 高速模式(3.4MHz):需要更精确的计算,通常在1kΩ左右

最常用的值是4.7kΩ,这是一个经过实践检验的经验值,在大多数应用场景下都能良好工作。

3.3 上拉电阻的计算方法

如果你想精确计算上拉电阻的阻值,可以使用以下公式。首先需要确定总线电容 Cbus,它包括:

  • 走线电容(约10pF/cm)
  • 每个设备的引脚电容(数据手册会标明,通常5~10pF)
  • 其他寄生电容

假设IIC总线时钟频率为 fSCL,上升时间要求为tr

(标准模式下最大1000ns,快速模式下最大300ns),则上拉电阻的最大值为:

同时,为了保证足够的驱动能力,上拉电阻的最小值需要满足:

其中 VOL(max) 是输出低电平的最大值(通常0.4V),IOL是开漏输出的最大吸收电流(查阅芯片手册)。

举个实际例子,假设:

  • 总线电容 Cbus=100pF
  • 上升时间要求 tr=1000ns(标准模式)
  • 电源电压 VCC=3.3V
  • 最大吸收电流 IOL=3mA

则:

因此上拉电阻应该选择在1kΩ到11.8kΩ之间,选择4.7kΩ是非常合适的。

3.4 多个上拉电阻并联的情况

在实际应用中,有时候会遇到多个模块都带有上拉电阻的情况。

比如你的主板上有上拉电阻,外接的传感器模块上也有上拉电阻。

这时候多个电阻会并联,等效电阻会变小。

两个电阻并联的等效电阻计算公式为:

比如两个4.7kΩ的电阻并联,等效电阻为:

这个值仍然在合理范围内,但如果并联的电阻太多,等效电阻可能会过小,导致功耗增加。

因此在设计时,建议只在主板上放置上拉电阻,外接模块上不要再加上拉电阻。

如果模块已经有上拉电阻,可以考虑用0欧电阻或跳线帽来选择性地启用。

4. 实际应用中的注意事项

4.1 上拉电阻的位置

上拉电阻应该尽量靠近主控芯片放置,而不是分散在各个从设备附近。

这样可以减少总线的寄生电容,提高信号质量。

在多层PCB中,建议将IIC走线放在内层,并在下方铺设完整的地平面,以减少干扰。

4.2 长距离传输的考虑

IIC总线本来是为板级通信设计的,传输距离通常在几厘米到几十厘米之间。

如果需要长距离传输(超过1米),需要特别注意:

  1. 降低通信速率,比如从400kHz降到100kHz甚至更低
  2. 使用更小的上拉电阻(但不要小于最小值)
  3. 考虑使用IIC总线扩展芯片或差分信号方案
  4. 增加滤波电容,提高抗干扰能力

4.3 调试技巧

在调试IIC通信问题时,可以用示波器观察SCL和SDA信号。正常情况下应该看到:

  1. 高电平接近VCC,低电平接近0V
  2. 上升沿呈指数曲线(RC充电曲线),下降沿陡峭
  3. 没有明显的振铃或过冲

如果上升沿太慢,说明上拉电阻太大或总线电容太大;如果有振铃,可能需要增加串联电阻或并联电容进行阻尼。

4.4 软件模拟IIC的配置

有时候我们需要用GPIO模拟IIC(比如硬件IIC引脚被占用了),这时候也要配置为开漏输出。

示例代码如下:

/* 初始化模拟IIC的GPIO */
void Soft_I2C_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    __HAL_RCC_GPIOB_CLK_ENABLE();
    
    /* 配置SCL和SDA为开漏输出 */
    GPIO_InitStruct.Pin = I2C_SCL_PIN | I2C_SDA_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;  // 开漏输出
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(I2C_GPIO_PORT, &GPIO_InitStruct);
    
    /* 初始状态设为高电平(实际是高阻态) */
    HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_SET);
    HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_SET);
}

/* 读取SDA电平 */
uint8_t I2C_SDA_Read(void)
{
    return HAL_GPIO_ReadPin(I2C_GPIO_PORT, I2C_SDA_PIN);
}

/* 设置SDA为低电平 */
void I2C_SDA_Low(void)
{
    HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_RESET);
}

/* 设置SDA为高电平(高阻态) */
void I2C_SDA_High(void)
{
    HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_SET);
}

注意在读取SDA电平时,要先将SDA设为高阻态(输出高电平),然后再读取引脚状态。

这样才能正确读取从设备发送的应答信号。

5. 总结

IIC总线的硬件设计看似简单,实则蕴含着精妙的设计思想。

开漏输出和上拉电阻这两个关键点,共同构成了IIC总线多设备共享、双向通信的基础。

开漏输出提供了"线与"逻辑,使得多个设备可以安全地共享同一根总线,避免了总线冲突的风险。

而上拉电阻则为开漏输出提供了高电平,同时还能实现电平转换、限制电流等功能。

两者配合,才能让IIC总线稳定可靠地工作。

在实际应用中,正确选择上拉电阻的阻值、合理布局PCB、注意信号完整性,都是保证IIC通信质量的关键。

希望通过今天的讲解,能让大家对IIC总线有更深入的理解,在以后的项目中少走弯路。

如果你在使用IIC总线时遇到通信不稳定、速率上不去等问题,不妨从硬件层面入手,检查一下是不是开漏输出配置不对,或者上拉电阻选择不合适。

很多时候,硬件问题比软件问题更隐蔽,但一旦找到根源,解决起来反而更简单。

更多编程学习资源

达林顿管的基础知识

大家好,我是良许。

在嵌入式开发中,我们经常需要驱动各种负载,比如继电器、电机、LED灯带等。

这些负载往往需要较大的电流,而单片机的IO口输出能力有限,这时候就需要用到功率放大电路。

达林顿管(Darlington Transistor)就是一种非常实用的功率放大器件,它能够提供极高的电流增益,让我们用很小的基极电流就能控制很大的负载电流。

今天我就来详细聊聊达林顿管的相关知识。

1. 什么是达林顿管

1.1 达林顿管的结构

达林顿管,又称达林顿晶体管或复合管,是由两个或多个三极管按照特定方式连接而成的复合器件。

最常见的是由两个NPN型或PNP型三极管组成。

其基本连接方式是:第一个三极管(称为驱动管)的发射极直接连接到第二个三极管(称为输出管)的基极,而两个三极管的集电极连接在一起作为复合管的集电极。

这种连接方式使得第一个三极管的输出电流成为第二个三极管的输入电流,从而实现了电流的二次放大。

如果第一个三极管的电流增益是β1,第二个三极管的电流增益是β2,那么整个达林顿管的总电流增益约为β1×β2,通常可以达到几百甚至上千。

1.2 达林顿管的符号

在电路图中,达林顿管有专门的符号表示。

对于NPN型达林顿管,符号看起来像一个普通的NPN三极管,但在内部会画出两个三极管的连接关系。

有些封装好的达林顿管芯片,比如ULN2003、TIP120等,在电路图中可能直接用一个三角形加箭头表示,并标注型号。

1.3 常见的达林顿管型号

在实际应用中,常见的达林顿管型号包括:

  • TIP120/TIP121/TIP122:NPN型达林顿管,最大电流5A,常用于中等功率场合
  • TIP125/TIP126/TIP127:PNP型达林顿管,与TIP120系列互补
  • ULN2003/ULN2803:集成了7路/8路达林顿管阵列的芯片,内置续流二极管,特别适合驱动继电器、步进电机等感性负载
  • BD681/BD682:大功率达林顿管,最大电流可达4A

2. 达林顿管的工作原理

2.1 电流放大过程

达林顿管的核心优势在于其超高的电流放大能力。

让我们详细分析一下电流是如何被放大的。

假设我们有一个由Q1和Q2组成的NPN型达林顿管,当基极B输入一个微小的电流Ib时,这个电流首先流入Q1的基极。

根据三极管的放大原理,Q1的集电极电流Ic1=β1×Ib,发射极电流Ie1=(β1+1)×Ib。

由于Q1的发射极连接到Q2的基极,因此Ie1就成为了Q2的基极电流。

Q2再次进行电流放大,其集电极电流Ic2=β2×Ie1=β2×(β1+1)×Ib。

最终,达林顿管的总集电极电流Ic=Ic1+Ic2≈β1×β2×Ib(当β1和β2都远大于1时)。

这就是达林顿管能够实现超高电流增益的原因。

2.2 导通压降

达林顿管有一个需要注意的特点,就是它的基极-发射极导通压降(Vbe)比普通三极管要高。

普通三极管的Vbe约为0.7V,而达林顿管的Vbe约为1.4V(两个三极管的Vbe相加)。

这意味着在设计电路时,我们需要确保基极电压至少比发射极高1.4V以上,达林顿管才能可靠导通。

同样,集电极-发射极的饱和压降(Vce(sat))也会比普通三极管略高,通常在0.9V到2V之间。

2.3 开关速度

由于达林顿管是两级放大,其开关速度相对较慢。

这是因为关断时需要等待两个三极管的存储电荷都消散完毕。

因此,达林顿管不太适合用于高频开关场合,更适合用于低频或直流驱动应用。

3. 达林顿管的典型应用

3.1 驱动继电器

继电器是嵌入式系统中常用的执行器件,但其线圈电流通常在几十到上百毫安,远超单片机IO口的驱动能力。

使用达林顿管可以轻松解决这个问题。

以STM32驱动继电器为例,我们可以使用TIP120达林顿管。

电路连接方式是:STM32的GPIO通过一个限流电阻(比如10kΩ)连接到TIP120的基极,继电器线圈一端接电源正极,另一端接TIP120的集电极,发射极接地。

继电器线圈两端还需要并联一个续流二极管(如1N4007),防止关断时的反向电动势损坏达林顿管。

下面是一个简单的HAL库代码示例:

// 初始化GPIO
void Relay_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    // 使能GPIOA时钟
    __HAL_RCC_GPIOA_CLK_ENABLE();
    
    // 配置PA5为输出模式
    GPIO_InitStruct.Pin = GPIO_PIN_5;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // 初始状态设为低电平(继电器关闭)
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
}

// 控制继电器开关
void Relay_Control(uint8_t state)
{
    if(state == 1)
    {
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);  // 继电器吸合
    }
    else
    {
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET); // 继电器释放
    }
}

3.2 驱动直流电机

直流电机的启动电流可能达到几安培,这时候单个达林顿管可能不够用,我们可以使用更大功率的型号,或者采用H桥电路实现正反转控制。

对于简单的单向电机控制,可以使用TIP122这样的大功率达林顿管。

电路连接与继电器类似,但需要注意散热问题。

当电流较大时,达林顿管会产生较多热量,需要加装散热片。

// PWM控制电机转速
void Motor_Init(void)
{
    TIM_HandleTypeDef htim2;
    TIM_OC_InitTypeDef sConfigOC = {0};
    
    // 配置定时器2用于PWM输出
    __HAL_RCC_TIM2_CLK_ENABLE();
    
    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 72-1;  // 假设系统时钟72MHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 1000-1;   // PWM频率约1kHz
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_PWM_Init(&htim2);
    
    // 配置PWM通道
    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 0;  // 初始占空比0%
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
    HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
    
    // 启动PWM输出
    HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
}

// 设置电机转速(0-100)
void Motor_SetSpeed(uint8_t speed)
{
    if(speed > 100) speed = 100;
    
    uint32_t pulse = (speed * 1000) / 100;
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, pulse);
}

3.3 驱动LED灯带

对于需要驱动多路LED的场合,ULN2003是一个非常好的选择。

这款芯片内部集成了7路达林顿管,每路可以驱动最大500mA的电流,并且内置了续流二极管,使用非常方便。

// ULN2003驱动LED灯带示例
void LED_Array_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    __HAL_RCC_GPIOB_CLK_ENABLE();
    
    // 配置PB0-PB6共7个引脚连接到ULN2003的输入端
    GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | 
                          GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}

// 控制LED显示模式(流水灯效果)
void LED_WaterLight(void)
{
    uint8_t pattern = 0x01;
    
    for(int i = 0; i < 7; i++)
    {
        GPIOB->ODR = (GPIOB->ODR & 0xFF80) | pattern;
        pattern <<= 1;
        HAL_Delay(100);
    }
}

4. 使用达林顿管的注意事项

4.1 基极限流电阻的选择

虽然达林顿管的电流增益很高,但我们仍然需要在基极串联一个限流电阻,防止基极电流过大损坏单片机IO口或达林顿管本身。

限流电阻的计算公式为:

其中,VGPIO是单片机IO口的输出电压(通常为3.3V或5V),VBE是达林顿管的基极-发射极导通电压(约1.4V),Ib是期望的基极电流。

例如,如果我们要驱动一个100mA的负载,达林顿管的电流增益为1000,那么需要的基极电流为:Ib=100mA/1000=0.1mA

如果GPIO输出3.3V,则限流电阻为:

实际应用中可以选择标准阻值20kΩ,或者为了留有余量选择10kΩ。

4.2 散热问题

达林顿管在工作时会产生功耗,功耗主要来自于集电极-发射极的压降和流过的电流。功耗计算公式为:

当功耗较大时,必须考虑散热问题。

一般来说,当功耗超过1W时,就应该考虑加装散热片。

散热片的选择需要根据达林顿管的热阻和环境温度来计算。

4.3 感性负载的保护

当驱动继电器、电机等感性负载时,必须在负载两端并联续流二极管。

这是因为感性负载在断电瞬间会产生很高的反向电动势,可能达到几十甚至上百伏特,足以击穿达林顿管。

续流二极管的选择要求:反向耐压至少是电源电压的2倍以上,正向电流应大于负载的工作电流。

常用的续流二极管有1N4007(耐压1000V,电流1A)、1N5819(肖特基二极管,压降小,速度快)等。

4.4 开关速度限制

由于达林顿管的开关速度较慢,不适合用于高频PWM控制。

如果需要高频开关,建议使用MOSFET代替。

一般来说,达林顿管的PWM频率最好不要超过10kHz,否则可能出现开关损耗增大、发热严重等问题。

5. 达林顿管与MOSFET的对比

在实际应用中,达林顿管和MOSFET都可以用作开关器件,但它们各有特点。

达林顿管的优势在于:驱动简单,只需要很小的基极电流就能控制大电流;价格便宜;对静电不敏感。

缺点是:导通压降较大(通常1-2V),开关速度慢,不适合高频应用。

MOSFET的优势在于:导通电阻很小(可以低至几毫欧),开关速度快,适合高频PWM;几乎不需要驱动电流(只需要充放电栅极电容)。

缺点是:需要足够的栅极电压才能完全导通(通常需要10V以上),对静电敏感,价格相对较高。

在嵌入式开发中,如果是低频开关、对效率要求不高的场合,达林顿管是很好的选择;如果是高频PWM、对效率要求高的场合,MOSFET更合适。

6. 总结

达林顿管作为一种经典的功率放大器件,在嵌入式系统中有着广泛的应用。

它的超高电流增益使得我们可以用单片机的微弱输出轻松驱动大功率负载。

虽然在高频和高效率场合逐渐被MOSFET取代,但在低频、简单的驱动电路中,达林顿管仍然是性价比很高的选择。

掌握达林顿管的工作原理和使用方法,对于嵌入式工程师来说是一项基本技能。

希望通过这篇文章,大家能够对达林顿管有更深入的了解,并能在实际项目中灵活运用。

更多编程学习资源

大家好,我是良许。

今天我们来聊一聊 C 语言中最让初学者头疼,却又最强大的特性——指针。

作为一名从事嵌入式开发多年的程序员,我深知指针在底层编程中的重要性。

无论是操作硬件寄存器、管理动态内存,还是实现高效的数据结构,指针都扮演着不可或缺的角色。

1. 什么是指针

1.1 指针的本质

指针其实就是一个变量,只不过这个变量存储的不是普通的数值,而是内存地址。

我们可以把内存想象成一排排的房间,每个房间都有一个门牌号(地址),而指针就是记录这个门牌号的本子。

通过这个门牌号,我们可以找到对应的房间,进而访问或修改房间里的内容。

在嵌入式开发中,这个概念尤为重要。比如 STM32 的 GPIO 端口,其实就是通过固定的内存地址来访问的。

当我们要点亮一个 LED 灯时,本质上就是通过指针操作特定地址的寄存器。

1.2 为什么需要指针

指针的存在主要解决了以下几个问题:

第一,高效传递数据。

当我们需要在函数之间传递大型数据结构时,如果直接传递整个结构体,会产生大量的复制开销。

而使用指针,只需要传递一个地址(通常是 4 字节或 8 字节),效率大大提升。

第二,动态内存管理。

在嵌入式系统中,内存资源往往非常有限。

通过指针和动态内存分配,我们可以在程序运行时根据实际需要申请和释放内存,提高内存利用率。

第三,直接操作硬件。

在嵌入式开发中,我们经常需要直接访问硬件寄存器。

这些寄存器都有固定的物理地址,必须通过指针来访问。

2. 指针的基本使用

2.1 指针的声明和初始化

声明一个指针变量的语法是在类型名后面加上星号(*)。例如:

int *p;        // 声明一个指向整型的指针
char *str;     // 声明一个指向字符的指针
float *fp;     // 声明一个指向浮点数的指针

需要注意的是,刚声明的指针是野指针,它指向一个不确定的地址,使用前必须初始化。

我们可以用取地址符(&)来获取变量的地址:

int num = 100;
int *p = &num;  // p指向num的地址
​
printf("num的值: %d\n", num);
printf("num的地址: %p\n", &num);
printf("p存储的地址: %p\n", p);
printf("p指向的值: %d\n", *p);

这段代码会输出 num 的值、num 的地址、指针 p 存储的地址(与 num 的地址相同),以及通过指针 p 访问到的值(也是 100)。

2.2 指针的解引用

解引用就是通过指针访问它所指向的内存中的值。

使用星号(*)操作符可以实现解引用:

int a = 50;
int *ptr = &a;
​
printf("a的值: %d\n", a);        // 输出50
printf("*ptr的值: %d\n", *ptr);  // 输出50
​
*ptr = 80;  // 通过指针修改a的值
printf("修改后a的值: %d\n", a);  // 输出80

在这个例子中,我们通过指针 ptr 修改了变量 a 的值。

这在函数参数传递中非常有用,可以实现真正的"传址调用"。

2.3 指针与函数

在 C 语言中,函数参数默认是值传递,也就是说函数内部对参数的修改不会影响外部变量。

但通过指针,我们可以实现传址调用:

void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}
​
int main(void) {
    int x = 10, y = 20;
    printf("交换前: x=%d, y=%d\n", x, y);
    
    swap(&x, &y);
    printf("交换后: x=%d, y=%d\n", x, y);
    
    return 0;
}

这个经典的交换函数例子展示了指针的威力。

通过传递变量的地址,函数内部可以直接修改外部变量的值。

3. 指针的进阶应用

3.1 指针与数组

数组名本身就是一个指针常量,指向数组的首元素。

这是 C 语言中一个非常重要的概念:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;  // 等价于 int *p = &arr[0];
​
printf("arr[0] = %d\n", arr[0]);    // 输出1
printf("*p = %d\n", *p);            // 输出1
printf("*(p+1) = %d\n", *(p+1));    // 输出2
printf("p[2] = %d\n", p[2]);        // 输出3

指针可以进行算术运算。

当指针加 1 时,实际上是移动了一个所指类型的大小。

比如 int 类型占 4 字节,那么 p+1 实际上是地址增加 4。

在嵌入式开发中,这个特性经常用于遍历数据缓冲区:

uint8_t buffer[256];
uint8_t *ptr = buffer;
​
// 通过指针遍历整个缓冲区
for(int i = 0; i < 256; i++) {
    *ptr = i;  // 写入数据
    ptr++;     // 指针移动到下一个位置
}

3.2 指针与字符串

在 C 语言中,字符串实际上就是字符数组,而字符串的操作大量使用指针:

char str[] = "Hello";
char *p = str;
​
while(*p != '\0') {
    printf("%c", *p);
    p++;
}
printf("\n");

这段代码通过指针遍历字符串并逐个打印字符。

在实际开发中,我们经常需要处理字符串,比如解析串口接收到的 AT 指令:

void parse_at_command(char *cmd) {
    if(strncmp(cmd, "AT+", 3) == 0) {
        char *param = cmd + 3;  // 指针偏移到参数部分
        printf("收到AT指令,参数: %s\n", param);
    }
}

3.3 多级指针

指针本身也是变量,也有自己的地址,因此可以有指向指针的指针,称为多级指针:

int num = 100;
int *p = &num;      // 一级指针
int **pp = &p;      // 二级指针

printf("num = %d\n", num);
printf("*p = %d\n", *p);
printf("**pp = %d\n", **pp);

**pp = 200;  // 通过二级指针修改num的值
printf("修改后num = %d\n", num);

多级指针在动态二维数组、函数指针数组等场景中很常见。

在嵌入式开发中,有时需要动态管理设备列表,就会用到二级指针。

4. 指针在嵌入式中的实战应用

4.1 操作硬件寄存器

在 STM32 开发中,我们经常需要直接操作寄存器。

这些寄存器都有固定的物理地址,必须通过指针访问:

// 定义GPIO端口的基地址
#define GPIOA_BASE    0x40020000U
#define GPIOA_MODER   (*(volatile uint32_t *)(GPIOA_BASE + 0x00))
#define GPIOA_ODR     (*(volatile uint32_t *)(GPIOA_BASE + 0x14))

// 配置PA5为输出模式
void led_init(void) {
    // 使能GPIOA时钟
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;

    // 配置PA5为输出模式
    GPIOA_MODER &= ~(3U << (5 * 2));  // 清除原配置
    GPIOA_MODER |= (1U << (5 * 2));   // 设置为输出
}

// 点亮LED
void led_on(void) {
    GPIOA_ODR |= (1U << 5);
}

// 熄灭LED
void led_off(void) {
    GPIOA_ODR &= ~(1U << 5);
}

这里的 volatile 关键字非常重要,它告诉编译器这个变量可能被外部因素改变,不要对其进行优化。

在访问硬件寄存器时必须使用 volatile 修饰。

4.2 DMA 数据传输

在使用 STM32 的 DMA 功能时,我们需要指定源地址和目标地址,这都是通过指针实现的:

uint8_t tx_buffer[128];
uint8_t rx_buffer[128];

void dma_uart_init(void) {
    // 配置DMA
    hdma_usart1_tx.Instance = DMA2_Stream7;
    hdma_usart1_tx.Init.Channel = DMA_CHANNEL_4;
    hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;

    HAL_DMA_Init(&hdma_usart1_tx);
}

void send_data_via_dma(void) {
    // 通过DMA发送数据,传递缓冲区指针
    HAL_UART_Transmit_DMA(&huart1, tx_buffer, sizeof(tx_buffer));
}

4.3 动态内存管理

在嵌入式系统中,虽然要谨慎使用动态内存,但在某些场景下确实需要:

#include <stdlib.h>

typedef struct {
    uint8_t id;
    uint16_t data;
    uint32_t timestamp;
} sensor_data_t;

sensor_data_t* create_sensor_data(uint8_t id) {
    sensor_data_t *data = (sensor_data_t*)malloc(sizeof(sensor_data_t));
    if(data != NULL) {
        data->id = id;
        data->data = 0;
        data->timestamp = HAL_GetTick();
    }
    return data;
}

void process_sensor(void) {
    sensor_data_t *sensor = create_sensor_data(1);
    if(sensor != NULL) {
        // 处理传感器数据
        sensor->data = read_sensor();

        // 使用完毕后释放内存
        free(sensor);
    }
}

需要注意的是,在嵌入式系统中使用动态内存要特别小心,因为频繁的 malloc 和 free 可能导致内存碎片,影响系统稳定性。

5. 指针使用的注意事项

5.1 野指针问题

野指针是指向未知内存区域的指针,使用野指针会导致程序崩溃或产生不可预测的行为:

int *p;  // 野指针,未初始化
*p = 10; // 危险!可能导致程序崩溃

// 正确做法
int *p = NULL;  // 初始化为NULL
if(p != NULL) {
    *p = 10;
}

在使用指针前,一定要确保它已经被正确初始化。

养成将指针初始化为 NULL 的习惯,并在使用前检查是否为 NULL。

5.2 内存泄漏

动态分配的内存如果忘记释放,就会造成内存泄漏:

void memory_leak_example(void) {
    int *p = (int*)malloc(sizeof(int) * 100);
    // 使用p
    // 忘记调用free(p),造成内存泄漏
}

// 正确做法
void correct_example(void) {
    int *p = (int*)malloc(sizeof(int) * 100);
    if(p != NULL) {
        // 使用p
        free(p);
        p = NULL;  // 释放后置为NULL
    }
}

5.3 悬空指针

当指针指向的内存被释放后,如果继续使用该指针,就会产生悬空指针问题:

int *p = (int*)malloc(sizeof(int));
*p = 100;
free(p);
// p现在是悬空指针
*p = 200;  // 危险!访问已释放的内存

// 正确做法
free(p);
p = NULL;  // 释放后立即置为NULL

6. 总结

指针是 C 语言的精髓,也是嵌入式开发的基石。

虽然初学时可能觉得难以理解,但只要多加练习,理解其本质(就是内存地址),就能逐渐掌握。

在我多年的嵌入式开发经验中,指针无处不在:从操作硬件寄存器到管理数据结构,从函数参数传递到实现复杂算法,都离不开指针。

掌握指针不仅能让你写出更高效的代码,还能帮助你深入理解计算机的工作原理。

特别是在嵌入式领域,对指针的熟练运用直接关系到能否写出高质量的底层代码。

希望这篇文章能帮助大家更好地理解和使用 C 语言的指针,在嵌入式开发的道路上走得更远。

第六十四章 LVGL 综合例程

本章,简单的介绍一下DNESP32S3开发板的 LVGL 综合例程。需要说明一下的是:本例程是一个不完整的例程。因为该例程只是实现一个基于 LVGL 的 GUI 界面,里面的 APP
基本没有实现功能,所以这只是给大家参考的 GUI demo。
实现这样简单的 GUI demo 原因如下:
1, 板载的2.4寸TFTLCD并未具备触摸条件,所以设计UI时受到很大的制约。
2, 想做出一个 LVGL 综合例程给大家参考,但时间比较赶。
3, 要实现一个不错的 LVGL 综合例程,要花费不少精力。
4, 要考虑板载资源,兼容性等。
5, 工程师们手头的事情比较多,等后续空闲些再规划。
大家可以把自己期待的 LVGL 界面、功能等,通过各种渠道跟我们沟通,比如:B 站视频评论区,销售客户/技术支持等。后续有时间,我们会把大家的建议都考虑上去的。最后,敬请大家心怀一个小小的期待,期待正点原子的 LVGL 综合例程,感谢大家的支持!!!
本章将分为如下 2 个小节:

64.1 LVGL 综合例程注意事项
64.2 LVGL 综合例程界面展示

64.1 LVGL 综合例程注意事项

注意事项如下:
1,DNESP32S3开发板的LVGL综合例程只支持正点原子的2.4寸 TFTLCD屏。其它屏幕会出现图标显示异常。
2,所用的LVGL版本是V8.2。
3,需要准备一张TF卡,将A盘资料的SD卡根目录文件复制到TF卡根目录当中,SD卡根目录文件如下图所示。

图64.1.1 拷贝资料到TF卡当中
图64.1.2展示的是LVGL例程界面所用到的 bin 文件。LVGL 综合例程会将这些bin文件拷贝到16MB Flash分区表的storage子分区表备份,方便GUI界面读取。如果直接从TF卡中读取,速度会比较慢,影响 GUI 的流畅性。

图 64.1.2 LVGL例程界面所用到的bin文件

64.2 LVGL 综合例程界面展示


图64.2.1 GUI主界面和视频播放器界面

图64.2.2 图片浏览界面和计算机界面
由于DNESP32S3开发板的2.4寸TFTLCD显示屏未具备触摸条件,所以作者只能实现一些简单的APP应用。

大家好,我是良许

说到三极管,可能很多刚入门的朋友会觉得这个名字有点陌生,但如果你接触过电子电路或者嵌入式开发,那你一定见过它的身影。

三极管可以说是电子世界里最基础、最重要的元器件之一,几乎所有的电子设备里都能找到它的踪迹。

今天咱们就来聊聊三极管到底是什么,它有什么用,以及在实际开发中我们该怎么使用它。

1. 三极管的基本概念

1.1 三极管是什么

三极管,全称叫做"半导体三极管",英文名是 Transistor,有时候也叫做晶体管。

从名字就能看出来,它有三个电极,这也是"三极管"名字的由来。

这三个电极分别叫做:基极(Base,简称 B)、集电极(Collector,简称 C)和发射极(Emitter,简称 E)。

三极管本质上是一种半导体器件,它是由两个 PN 结组成的。根据这两个 PN 结的排列方式不同,三极管可以分为 NPN 型和 PNP 型两种。

NPN 型就是中间是 P 型半导体,两边是 N 型半导体;PNP 型则相反,中间是 N 型半导体,两边是 P 型半导体。

在实际应用中,NPN 型三极管使用得更多一些。

1.2 三极管的工作原理

三极管最神奇的地方在于,它可以用一个很小的电流去控制一个很大的电流。

具体来说,就是通过控制基极和发射极之间的电流(基极电流,记作IB​),来控制集电极和发射极之间的电流(集电极电流,记作IC)。

这个过程就像是用一个小水龙头去控制一个大水龙头的开关一样。

这里有一个很重要的参数,叫做电流放大倍数,用希腊字母β(贝塔)来表示。这个β值表示的是集电极电流和基极电流的比值,也就是:

$$
\beta = \frac{I_C}{I_B}
$$

一般来说,普通三极管的β值在几十到几百之间。

比如说,如果一个三极管的β值是 100,那么当基极电流是 1mA 的时候,集电极电流就可以达到 100mA。这就是三极管的放大作用。

1.3 三极管的三种工作状态

三极管在电路中有三种基本的工作状态:截止状态、放大状态和饱和状态。

截止状态:当基极电流为零或者很小的时候,三极管就处于截止状态。

这时候集电极电流也基本为零,三极管相当于一个断开的开关。

放大状态:当基极电流在一个合适的范围内时,三极管就工作在放大状态。这时候集电极电流和基极电流成正比关系,也就是IC​=β×IB。

这个状态主要用于模拟电路中的信号放大。

饱和状态:当基极电流足够大的时候,三极管就进入了饱和状态。这时候集电极电流不再随基极电流的增加而增加,三极管相当于一个闭合的开关。

在数字电路中,我们经常让三极管工作在饱和状态或截止状态,用来实现开关功能。

2. 三极管的实际应用

2.1 三极管作为开关使用

在嵌入式开发中,我们最常用三极管来做的事情就是当开关用。

比如说,STM32 的 GPIO 口输出电流一般只有几十毫安,如果我们要驱动一个需要几百毫安电流的负载(比如继电器、电机等),直接用 GPIO 口是不行的,这时候就需要用三极管来做电流放大。

举个具体的例子,假设我们要用 STM32 控制一个 12V 的继电器,这个继电器的线圈电流是 100mA。

我们可以这样设计电路:用 STM32 的 GPIO 口控制三极管的基极,三极管的集电极接继电器线圈,发射极接地。

当 GPIO 口输出高电平时,三极管导通,继电器得电工作;当 GPIO 口输出低电平时,三极管截止,继电器断电。

下面是一个简单的 HAL 库代码示例:

// 初始化GPIO
void Relay_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    // 使能GPIOA时钟
    __HAL_RCC_GPIOA_CLK_ENABLE();
    
    // 配置PA5为输出模式
    GPIO_InitStruct.Pin = GPIO_PIN_5;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;  // 推挽输出
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // 初始状态设为低电平,继电器断电
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
}
​
// 控制继电器开
void Relay_On(void)
{
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
}
​
// 控制继电器关
void Relay_Off(void)
{
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);
}

在这个应用中,我们需要注意几个关键点:首先是基极电阻的选择。

基极电阻太小会导致基极电流过大,可能损坏 GPIO 口;基极电阻太大则可能导致三极管无法完全导通。

一般来说,我们可以这样计算:假设 GPIO 口输出电压是 3.3V,三极管的 BE 结压降约 0.7V,我们希望基极电流是 1mA,那么基极电阻应该是:

$$
R_B = \frac{3.3V - 0.7V}{1mA} = 2.6k\Omega
$$

实际应用中可以选择标准阻值 2.7kΩ 或 3kΩ。

2.2 三极管的限流保护

在使用三极管驱动感性负载(如继电器、电机)时,还需要注意一个问题:当三极管突然截止时,感性负载会产生反向电动势,这个电压可能会很高,足以击穿三极管。

所以我们通常会在负载两端并联一个续流二极管,用来释放这个反向电动势。

电路设计时,续流二极管的负极接电源正极,正极接三极管的集电极。

当三极管截止时,感性负载产生的反向电流就会通过这个二极管形成回路,从而保护三极管。

2.3 三极管在模拟电路中的应用

除了做开关,三极管在模拟电路中还可以用来做信号放大。比如在音频电路中,我们可以用三极管来放大麦克风采集到的微弱音频信号。

不过在嵌入式系统中,我们更多的是使用集成运放芯片来做信号放大,因为运放的性能更稳定,使用也更方便。

但了解三极管的放大原理还是很有必要的,因为很多集成电路的内部其实就是由大量的三极管组成的。

比如我们常用的 LM358 运放,内部就包含了几十个三极管。

3. 三极管选型和使用注意事项

3.1 如何选择合适的三极管

在实际项目中选择三极管时,我们需要关注以下几个参数:

最大集电极电流ICM:这个参数表示三极管能够承受的最大电流。选择时要留有余量,一般选择实际工作电流的 2-3 倍。比如你的负载电流是 100mA,那就选择ICM至少 300mA 的三极管。

最大集电极-发射极电压VCEO:这个参数表示三极管能够承受的最大电压。

同样要留有余量,如果你的电路工作电压是 12V,建议选择VCEO 至少 20V 以上的三极管。

电流放大倍数β:这个参数越大,说明三极管的放大能力越强,需要的基极电流就越小。一般选择β值在 100 以上的三极管就够用了。

功耗:三极管在工作时会发热,特别是在驱动大电流负载时。要根据实际功耗选择合适封装的三极管,必要时还要加散热片。功耗可以用公式P=VCE​×IC​ 来估算,其中VCE是集电极-发射极之间的电压降。

常用的小功率三极管有 S8050(NPN 型)、S8550(PNP 型)、2N3904(NPN 型)、2N3906(PNP 型)等。中功率三极管有 TIP41(NPN 型)、TIP42(PNP 型)等。这些型号在市场上都很容易买到,价格也便宜。

3.2 使用三极管的常见错误

在实际使用中,新手经常会犯一些错误,这里总结几个常见的:

忘记加基极电阻:有些朋友直接把 GPIO 口连到三极管基极,这样会导致基极电流过大,可能烧坏 GPIO 口或三极管。一定要记得加基极电阻。

三极管极性接反:NPN 型和 PNP 型三极管的接法是不一样的,如果接反了,电路就不会工作。使用前一定要查清楚三极管的管脚定义。

不加续流二极管:驱动感性负载时如果不加续流二极管,三极管很容易被反向电动势击穿。这是一个很容易被忽视但又很重要的保护措施。

工作状态选择不当:如果是做开关使用,一定要让三极管工作在饱和状态或截止状态,不要工作在放大区,否则三极管会发热严重,甚至烧毁。

4. 总结

三极管虽然是一个很基础的元器件,但它的作用却非常重要。

在嵌入式开发中,我们经常需要用三极管来扩展单片机的驱动能力,实现对各种负载的控制。

掌握三极管的基本原理和使用方法,是每一个嵌入式工程师的必备技能。

从我自己的经验来看,刚开始接触三极管的时候,确实会觉得有点抽象,特别是那些什么 PN 结、载流子之类的概念。

但其实在实际应用中,我们不需要深究那么多理论,只要记住几个关键点就行:三极管可以用小电流控制大电流,做开关用时要工作在饱和或截止状态,驱动感性负载要加续流二极管。

把这些基本原则掌握了,在实际项目中就能游刃有余了。

希望这篇文章能帮助大家更好地理解和使用三极管。

如果你在实际使用中遇到什么问题,欢迎留言交流。

电子技术这东西,理论固然重要,但更重要的是多动手实践,在实践中积累经验。加油!

大家好,我是良许。

在嵌入式开发中,串口通信是我们最常用的通信方式之一。

但很多初学者经常会被 UART、RS232、RS485 这几个概念搞混,不清楚它们之间到底有什么区别和联系。

今天我就来详细聊聊这三者的区别,帮助大家彻底理解这些概念。

1. 基本概念解析

1.1 UART 是什么

UART(Universal Asynchronous Receiver/Transmitter)的中文名称是通用异步收发器,它本质上是一种通信协议和硬件电路

UART 定义了数据如何在设备之间进行串行传输,包括数据格式、传输速率、起始位、停止位等。

简单来说,UART 是一种逻辑层面的协议标准。

它规定了数据帧的格式,比如一个标准的 UART 数据帧通常包含:1 个起始位(低电平)、5 到 8 个数据位、可选的校验位、1 到 2 个停止位(高电平)。

在我们的 STM32 单片机中,UART 就是芯片内部集成的一个硬件模块,负责将并行数据转换为串行数据发送出去,或者将接收到的串行数据转换为并行数据。

UART 通信只需要两根线:TX(发送)和 RX(接收),再加上一根地线 GND。

1.2 RS232 是什么

RS232 是由美国电子工业协会(EIA)制定的一种物理层标准,全称是 EIA-RS-232。

它定义了数据终端设备(DTE)与数据通信设备(DCE)之间的物理接口标准,包括电气特性、机械特性、功能特性等。

RS232 最重要的特点是它的电平标准:逻辑 1(MARK)的电压范围是-15V 到-3V,逻辑 0(SPACE)的电压范围是 +3V 到 +15V。

注意,这个电平标准和我们单片机的 TTL 电平(0V 和 3.3V 或 5V)是完全不同的。

RS232 通常使用 DB9 或 DB25 接口,最大传输距离约为 15 米,最大传输速率一般不超过 20kbps(理论上可以更高,但实际应用中受限于线缆长度和质量)。

1.3 RS485 是什么

RS485 同样是一种物理层标准,它是 RS232 的改进版本。

RS485 采用差分信号传输方式,使用两根线(A 和 B)来传输数据,通过两根线之间的电压差来表示逻辑 0 和 1。

RS485 的主要优势包括:传输距离可达 1200 米,传输速率可达 10Mbps(短距离下),支持多点通信(最多可以连接 128 个设备),抗干扰能力强。由于采用差分信号,RS485 在工业环境中的应用非常广泛。

2. 三者之间的关系

理解了基本概念后,我们来看看它们之间的关系。

简单来说:

UART 是协议层,RS232 和 RS485 是物理层。

这就好比我们说话时,UART 定义了"说什么"(语言规则),而 RS232 和 RS485 定义了"怎么说"(声音的大小、传播方式)。

一个完整的串口通信系统,既需要 UART 协议来组织数据,也需要 RS232 或 RS485 这样的物理层标准来实际传输数据。

在实际应用中,我们的单片机 UART 输出的是 TTL 电平信号(比如 0V 和 3.3V),如果要通过 RS232 接口通信,就需要使用电平转换芯片(如 MAX232)将 TTL 电平转换为 RS232 电平;如果要通过 RS485 通信,就需要使用 RS485 收发器芯片(如 MAX485)进行转换。

3. 详细对比分析

3.1 电气特性对比

从电气特性来看,三者有明显的区别:

UART(TTL 电平):逻辑 1 通常是 3.3V 或 5V,逻辑 0 是 0V。这是单片机内部直接使用的电平标准,驱动能力弱,抗干扰能力差,只适合板级通信。

RS232:采用负逻辑,逻辑 1 是-3V 到-15V,逻辑 0 是 +3V 到 +15V。这种较大的电压摆幅提供了一定的抗干扰能力,但功耗相对较高。RS232 是单端信号传输,容易受到共模干扰的影响。

RS485:采用差分信号传输,两根线之间的电压差大于 +200mV 表示逻辑 1,小于-200mV 表示逻辑 0。差分传输的最大优势是抗共模干扰能力强,即使两根线同时受到相同的干扰,只要它们之间的电压差保持不变,就不会影响数据传输。

3.2 传输距离和速率对比

在实际应用中,传输距离和速率是我们选择通信方式的重要考虑因素:

UART(TTL 电平):传输距离非常有限,一般不超过 1 米,速率可以很高,但受限于线缆和驱动能力。在 PCB 板上的芯片间通信非常合适。

RS232:标准规定最大传输距离为 15 米,但在实际应用中,如果降低波特率,可以达到更远的距离。比如在 9600bps 的速率下,可以传输 30 米甚至更远。但随着距离增加,信号衰减和干扰会导致通信质量下降。

RS485:这是三者中传输距离最远的,标准距离可达 1200 米。而且 RS485 的传输速率和距离是可以权衡的:短距离下可以达到 10Mbps,而在最大距离 1200 米时,速率通常限制在 100kbps 左右。

3.3 通信方式对比

从通信拓扑结构来看:

UART/RS232:只支持点对点通信,即一个发送端对应一个接收端。如果需要连接多个设备,就需要多个串口,或者使用串口服务器等设备。

RS485:支持多点通信(也叫总线型通信),可以在同一条总线上连接多达 128 个设备(理论值,实际应用中需要考虑负载能力)。这使得 RS485 在工业控制系统中非常受欢迎,可以大大减少布线成本。

另外,RS485 支持半双工和全双工两种模式。半双工模式只需要两根线(A 和 B),但同一时刻只能有一个设备发送数据;全双工模式需要四根线,可以同时收发数据。

4. 实际应用场景

4.1 UART 的典型应用

在嵌入式开发中,UART 最常见的应用场景包括:

  1. 单片机与 PC 之间的调试通信,通过 USB 转 TTL 模块连接。
  2. 单片机与各种传感器模块的通信,比如 GPS 模块、蓝牙模块、WiFi 模块等。
  3. 单片机之间的短距离通信。

下面是一个 STM32 使用 HAL 库进行 UART 通信的简单示例:

// UART初始化
UART_HandleTypeDef huart1;
​
void MX_USART1_UART_Init(void)
{
    huart1.Instance = USART1;
    huart1.Init.BaudRate = 115200;
    huart1.Init.WordLength = UART_WORDLENGTH_8B;
    huart1.Init.StopBits = UART_STOPBITS_1;
    huart1.Init.Parity = UART_PARITY_NONE;
    huart1.Init.Mode = UART_MODE_TX_RX;
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    
    if (HAL_UART_Init(&huart1) != HAL_OK)
    {
        Error_Handler();
    }
}
​
// 发送数据
uint8_t txData[] = "Hello UART!\r\n";
HAL_UART_Transmit(&huart1, txData, sizeof(txData)-1, 1000);
​
// 接收数据
uint8_t rxData[100];
HAL_UART_Receive(&huart1, rxData, 10, 1000);

4.2 RS232 的典型应用

RS232 虽然是比较老的标准,但在很多场合仍然在使用:

  1. 工业设备的配置和调试接口,很多老设备都配备 RS232 接口。
  2. 一些专业设备如示波器、频谱分析仪的通信接口。
  3. PLC(可编程逻辑控制器)的编程和监控接口。

在使用 RS232 时,我们需要在单片机的 UART 和 RS232 接口之间加入电平转换芯片。

以 MAX232 为例,它可以将 TTL 电平转换为 RS232 电平,反之亦然。

电路连接非常简单,只需要几个外围电容即可。

4.3 RS485 的典型应用

RS485 在工业自动化领域应用极为广泛:

  1. 工业现场的传感器网络,比如温度、压力、流量等传感器的数据采集。
  2. 楼宇自动化系统,如门禁、照明、空调控制等。
  3. 智能电网的抄表系统。
  4. 工业机器人的控制系统。

使用 RS485 时,需要注意以下几点:

  1. 总线两端需要加 120 欧姆的终端电阻,以消除信号反射。
  2. 在没有数据传输时,需要将总线拉到确定的电平状态,通常使用上拉和下拉电阻。
  3. 在多主机通信时,需要设计好通信协议,避免总线冲突。

下面是一个使用 MAX485 进行 RS485 通信的示例代码:

// 定义RS485方向控制引脚
#define RS485_DE_GPIO_Port GPIOA
#define RS485_DE_Pin GPIO_PIN_8
​
// 设置为发送模式
void RS485_TX_Mode(void)
{
    HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_SET);
    HAL_Delay(1); // 等待芯片切换
}
​
// 设置为接收模式
void RS485_RX_Mode(void)
{
    HAL_GPIO_WritePin(RS485_DE_GPIO_Port, RS485_DE_Pin, GPIO_PIN_RESET);
    HAL_Delay(1);
}
​
// 发送数据
void RS485_SendData(uint8_t *data, uint16_t len)
{
    RS485_TX_Mode();
    HAL_UART_Transmit(&huart1, data, len, 1000);
    RS485_RX_Mode();
}
​
// 接收数据
void RS485_ReceiveData(uint8_t *data, uint16_t len)
{
    RS485_RX_Mode();
    HAL_UART_Receive(&huart1, data, len, 1000);
}

5. 如何选择合适的通信方式

在实际项目中,我们应该如何选择呢?可以参考以下原则:

  1. 短距离板级通信:直接使用 UART 的 TTL 电平即可,简单、成本低、速度快。比如单片机与传感器模块之间的通信。
  2. 中等距离点对点通信:如果距离在几米到十几米之间,并且只需要连接两个设备,可以选择 RS232。虽然 RS232 比较老,但它的兼容性很好,很多设备都支持。
  3. 长距离或多设备通信:如果传输距离超过 15 米,或者需要连接多个设备,那么 RS485 是最佳选择。特别是在工业环境中,RS485 的抗干扰能力和多点通信能力使它成为首选。
  4. 高速短距离通信:如果需要高速传输且距离不远,可以考虑使用 LVDS(低压差分信号)等其他技术。
  5. 无线通信需求:如果布线困难或需要移动通信,可以考虑使用蓝牙、WiFi、LoRa 等无线通信方式。

6. 总结

通过以上的详细分析,我们可以清楚地看到 UART、RS232、RS485 之间的区别和联系:

UART 是一种通信协议和硬件模块,定义了数据的组织方式;RS232 和 RS485 则是物理层标准,定义了信号的电气特性和传输方式。

它们不是互相替代的关系,而是协同工作的关系。

在实际应用中,我们通常是在单片机的 UART 基础上,根据具体需求选择合适的物理层标准。

如果是短距离通信,直接使用 UART 的 TTL 电平;如果需要更远的传输距离或更强的抗干扰能力,就通过电平转换芯片将 TTL 电平转换为 RS232 或 RS485 电平。

理解这些概念对于我们进行嵌入式系统设计非常重要,可以帮助我们在不同的应用场景中选择最合适的通信方式,设计出稳定可靠的系统。

希望这篇文章能够帮助大家彻底搞清楚这三者的区别,在以后的项目中能够灵活运用。