本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目以STM32F103C8T6为核心控制器,构建了一个集环境温湿度采集与实时显示于一体的嵌入式系统。通过C语言编程实现对DHT11数字温湿度传感器的数据读取,并利用LCD显示屏直观展示温度和湿度信息。系统涵盖微控制器初始化、GPIO配置、时序控制通信协议解析及字符型LCD驱动等关键技术,适用于嵌入式开发初学者掌握传感器应用与硬件接口编程。项目使用STM32 HAL库或寄存器操作进行底层控制,结合STM32CubeIDE或Keil等开发工具完成编译调试,全面锻炼嵌入式系统设计与调试能力。

STM32F103C8T6微控制器架构与嵌入式开发理论基础

在物联网设备日益普及的今天,一个小小的温湿度计背后可能就藏着一颗STM32芯片。你有没有想过,为什么同样是单片机,有的只能点亮个LED,而有的却能稳定运行传感器网络、驱动显示屏甚至接入Wi-Fi?🤔 今天我们就来深挖一下那块“蓝色药丸”——STM32F103C8T6,看看它到底是怎么做到这一切的。

Cortex-M3内核:不只是72MHz那么简单

STM32F103C8T6的核心是ARM Cortex-M3,这可不是普通的8位MCU能比的。它采用的是32位RISC架构,主频高达72MHz,听起来很猛对吧?但真正让它强大的,其实是那些藏在手册第50页以后的东西。

比如它的 三级流水线结构 ,意味着CPU可以在执行当前指令的同时,预取下一条和解码再下一条。这就像是你在吃第一口饭的时候,筷子已经伸向第二口了,效率自然高。再加上Thumb-2指令集的支持,代码密度提升了近30%,同样的Flash空间能塞进更多功能。

// 示例:通过指针直接访问Cortex-M3的NVIC寄存器
#define NVIC_ISER0 *((volatile uint32_t*)0xE000E100) // 使能中断
NVIC_ISER0 |= (1 << 6); // 使能USART1中断(IRQ6)

看到这段代码没?我们直接操作内存地址修改NVIC(嵌套向量中断控制器)的值。这就是所谓“硬件可编程性”的体现——你可以像搭积木一样配置整个系统的响应逻辑。这种能力对于工业控制这类硬实时场景至关重要:想象一下机械臂正在高速运动,突然来了个急停信号,延迟哪怕多几个微秒都可能导致事故。而Cortex-M3最短12个时钟周期的中断响应时间,就是安全的保障 💪。

更别提还有MPU(内存保护单元)这种高级货色了。虽然在小项目里用不上,但在医疗或车载系统中,它可以防止某个任务误写其他区域的数据,相当于给内存加了个“防火墙”。

至于那64KB Flash和20KB SRAM,配合哈佛总线架构(指令与数据分开读取),让CPU不会因为取指令而耽误处理数据,效率提升显著。AHB/APB总线矩阵的设计也很巧妙,高频外设走AHB,低速的比如I²C走APB,互不干扰。


嵌入式系统分层模型:从裸机到RTOS的认知跃迁

说到嵌入式开发,很多人第一反应就是写个 while(1) 循环加一堆if判断。但这其实只是起点。真正的工程化开发讲究 分层设计 ,就像盖楼一样,一层打牢了才能往上建。

层级 功能职责 典型组件
硬件层 提供物理资源 MCU、传感器、电源
驱动层 寄存器配置与抽象 HAL库、LL驱动
中间件/OS层 任务调度与通信 FreeRTOS、FatFS
应用层 实现业务逻辑 温湿度监控主控程序

你看这个表格,是不是有点像计算机的操作系统结构?没错,现代嵌入式系统越来越趋向于“微型操作系统”模式。即使你现在还在用裸机开发,也应该有意识地按照这种结构组织代码。

举个例子,如果你把所有逻辑都堆在main函数里,某天客户说要加个串口上传功能,你就得翻遍几百行代码去找插入点;但如果你早就分好了“传感器采集模块”、“显示模块”、“通信模块”,那新功能只需要新增一个模块并注册到调度器就行,完全不影响原有逻辑。

而对于STM32F103C8T6这样的平台,即使是裸机程序,也可以玩出花来。常见的“轮询+中断”架构就能满足大多数硬实时需求。比如说按键检测用外部中断触发,避免轮询浪费CPU;定时任务用SysTick中断做节拍源;UART接收可以用DMA+空闲中断实现零拷贝……这些技巧组合起来,性能不输轻量级RTOS。

而且你知道吗?Cortex-M3支持多达256个中断源(包括16个内核异常),每个还能设置优先级,支持尾链技术和迟到处理。这意味着当高优先级中断到来时,低优先级的可以被打断,处理完再回来继续,极大减少了响应延迟。这在电机控制、无人机飞控等领域可是保命的功能!


开发环境搭建:别再手敲启动文件了!

还记得当年学51单片机时,为了点亮一个LED要先写一堆汇编配置时钟、栈指针、复位向量……现在?不存在的 😎。ST官方推出的STM32CubeIDE + CubeMX组合拳,直接让我们进入了“图形化编程”时代。

来,咱们走一遍实际流程:

  1. 安装STM32CubeIDE(基于Eclipse,免费!)
  2. 创建新项目,选中你的芯片型号STM32F103C8T6
  3. 打开CubeMX界面,点开Pinout视图
  4. 想让PC13接LED?直接点击那个引脚,选择GPIO_Output
  5. 要用串口调试?找到PA9/PA10,设为USART1_TX/RX
  6. 切到Clock Configuration,拉满PLL到72MHz
  7. 最后Generate Code,一键生成初始化代码

瞧,连 SystemClock_Config() 这种复杂的时钟树配置都给你写好了,再也不用手动算分频系数了。甚至连功耗估算都有!

int main(void) {
    HAL_Init();
    SystemClock_Config();        // 配置72MHz系统时钟
    MX_GPIO_Init();              // 初始化LED引脚
    while (1) {
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
        HAL_Delay(500);          // 500ms翻转一次PC13
    }
}

就这么几行,蓝灯就开始闪了 ✨。而且注意看,这里用了HAL库的API,跨平台兼容性超强。哪天你想升级到H7系列,大部分代码都不用改。

调试方面也超方便。只需要一个ST-Link V2(十几块钱包邮),两根线(SWCLK & SWDIO)接到板子上,就能实现:
- 单步调试
- 断点暂停
- 变量实时查看
- 内存快照分析

比起传统ISP烧录“改一次代码烧一次”的痛苦经历,简直是降维打击。

不过友情提示⚠️:第一次用记得安装驱动!否则识别不了仿真器。另外,如果板子供电不稳定,建议外部供电而不是靠ST-Link供电,不然容易炸JTAG。


外设驱动核心理论与GPIO控制实践

如果说MCU是一台电脑,那GPIO就是它的USB接口——什么设备都能插上去,关键看你会不会用。STM32F103C8T6提供了37个可编程GPIO引脚(PA0~PA15, PB0~PB15, PC13~PC15),分布在三个端口上,足够应付绝大多数应用场景。

但问题来了:同样是输出高低电平,为啥还要分推挽、开漏、上拉、下拉这么多模式?🤷‍♂️ 别急,下面我就带你一一拆解。

推挽 vs 开漏:不只是“能不能拉高”的区别

先来看最常见的两种输出模式:

推挽输出(Push-Pull)

这是最典型的输出方式。内部有两个MOS管——P-MOS负责拉高,N-MOS负责拉低。你要输出高电平,P-MOS导通;要低电平,N-MOS导通。两者永远不会同时工作。

优点很明显:驱动能力强,一般能达到±20mA,直接点亮LED毫无压力。而且上升下降沿陡峭,适合高速信号传输。

// 使用LL库配置PB5为推挽输出
LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_GPIOB); // 使能GPIOB时钟
LL_GPIO_SetPinMode(GPIOB, LL_GPIO_PIN_5, LL_GPIO_MODE_OUTPUT);
LL_GPIO_SetPinOutputType(GPIOB, LL_GPIO_PIN_5, LL_GPIO_OUTPUT_PUSHPULL);

⚠️ 注意:必须先开启时钟!否则你写的寄存器就像往断电的U盘里存文件——看似成功,实则无效。

开漏输出(Open-Drain)

只有N-MOS管,只能主动拉低,不能拉高。想要输出高电平怎么办?靠外加上拉电阻!

你可能会问:“这不是多此一举吗?”错!这正是I²C总线的灵魂所在 👑。

flowchart LR
    subgraph "I²C Bus Example"
        MCU[MCU SDA Pin] -->|Open Drain| BUS_LINE[(SDA Line)]
        SLAVE[Slave Device] -->|Open Drain| BUS_LINE
        RESISTOR[Pull-up Resistor] --> VCC[VDD]
        BUS_LINE --> RESISTOR
    end

看懂了吗?多个设备挂在同一根线上,谁都可以拉低,但没人能强制拉高。释放后由上拉电阻恢复高电平。这样就实现了“线与”逻辑:任何一个设备拉低,整条线就是低电平。这是I²C实现主从仲裁的基础机制。

所以记住一句话:
👉 普通数字输出用推挽
👉 共享总线通信用开漏


上下拉电阻:按键检测的“定海神针”

再说输入模式。如果你做过按键实验,肯定遇到过这种情况:按键没按下去,但程序偶尔会误判为按下。这就是典型的 浮空输入干扰 问题。

GPIO引脚如果不接任何东西,处于高阻态,就像一根天线,很容易拾取周围电磁噪声。解决办法很简单——启用内部上下拉电阻。

比如按键一端接地,另一端接PA0。这时候就应该启用 内部上拉电阻 ,让默认状态为高电平,按下后变为低电平。

// 配置 PA0 为上拉输入,用于按键检测
LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_GPIOA);
LL_GPIO_SetPinMode(GPIOA, LL_GPIO_PIN_0, LL_GPIO_MODE_INPUT);
LL_GPIO_SetPinPull(GPIOA, LL_GPIO_PIN_0, LL_GPIO_PULL_UP);

参数说明:
- LL_GPIO_PULL_UP : 启用约40kΩ的内部上拉
- LL_GPIO_PULL_DOWN : 启用内部下拉
- 不设置则为浮空输入,慎用!

当然,如果你担心内部电阻太弱(毕竟40kΩ不算小),也可以在外围电路加个更强的(比如10kΩ)。但从成本和布线简洁性考虑,大多数情况下片内电阻就够用了。


寄存器级初始化:从“会用”到“懂原理”的跨越

前面用LL库配置GPIO看起来很简单,但底层发生了什么?这才是高手和普通开发者拉开差距的地方。

时钟树关系图解

graph TD
    HSI[HSE/HSI 8MHz] --> PLL[PLL 倍频]
    PLL --> SYSCLK[System Clock 72MHz]
    SYSCLK --> AHB[Advanced High-performance Bus]
    AHB -->|Prescaler| APB2[APB2 Peripheral Clock]
    APB2 --> GPIOA
    APB2 --> GPIOB
    APB2 --> GPIOC

重点来了❗️:STM32F103C8T6的所有GPIO都在APB2总线上,频率默认等于SYSCLK(72MHz)。也就是说,你操作GPIO的速度理论上可达每秒七千多万次切换!当然实际受引脚负载和EMI限制,一般不超过几十MHz。

手动操作寄存器演示

// 手动写寄存器方式(理解原理用)
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;                    // 使能GPIOA时钟
GPIOA->CRL &= ~(0xF << (5*4));                         // 清除PA5配置位
GPIOA->CRL |= (0x2 << (5*4)) | (0x0 << ((5*4)+2));     // MODE=10, CNF=00 → 推挽输出

解释一下:
- RCC->APB2ENR 是时钟使能寄存器,置位第2位打开GPIOA时钟
- GPIOA->CRL 控制引脚0~7的模式,每4位一组
- PA5对应第5组(偏移量5×4)
- MODE[1:0]=10 表示输出模式,最大速度2MHz
- CNF[1:0]=00 表示通用推挽输出

虽然现在没人这么写了(太容易出错),但理解这个过程有助于你在调试时快速定位问题。比如发现某个引脚死活没反应,第一反应应该是:“我开时钟了吗?”


引脚复用与重映射:灵活应对PCB布局挑战

有时候你会发现:我想用USART1,但它默认TX在PA9,而我的PCB刚好把那个位置给了别的器件……怎么办?

答案是: 复用功能 重映射

STM32允许将某些外设功能“搬”到其他引脚上。例如USART2默认用PA2/PA3,但可以通过AFIO寄存器重映射到PD5/PD6(前提是封装支持)。

// 启用USART2重映射
LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_AFIO);
AFIO->MAPR |= AFIO_MAPR_USART2_REMAP;

当然,并非所有引脚都能重映射,具体要看数据手册里的《Alternate function mapping》表格。建议早期设计时就用STM32CubeMX规划好引脚分配,自动避让冲突。

还有一个常见误区:多个外设共用同一个引脚。千万别这么干!除非你能确保它们不会同时工作,否则轻则功能异常,重则烧毁IO口。宁可多费点PCB空间,也要保证资源独占。


HAL库实战:快速构建可靠系统

虽然LL库效率更高,但对于产品级项目,我还是推荐使用HAL库。原因只有一个: 开发效率 × 维护成本 = 正收益

STM32CubeMX生成初始化代码

再次安利这个神器!可视化配置引脚、时钟、外设,一键生成初始化代码。不仅减少人为错误,还能自动生成 .ioc 项目文件,团队协作无障碍。

生成的 gpio.c 长这样:

void MX_GPIO_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    /* GPIO Ports Clock Enable */
    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    /* Configure GPIO pin Output Level */
    HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_RESET);

    /* Configure GPIO pin : PA0 */
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* Configure GPIO pins : LD2_Pin */
    GPIO_InitStruct.Pin = LD2_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(LD2_GPIO_Port, &GPIO_InitStruct);
}

结构体 GPIO_InitTypeDef 是HAL库的核心配置对象,字段含义如下:
- .Pin : 支持按位或组合多个引脚(如 GPIO_PIN_0 | GPIO_PIN_1
- .Mode : 工作模式(INPUT, OUTPUT_PP, ALTERNATE, ANALOG等)
- .Pull : 上下拉配置
- .Speed : 输出速度等级(LOW/MEDIUM/HIGH)
- .Alternate : 复用功能编号(如GPIO_AF1_TIM1)

数字电平读写函数应用

有了HAL封装,读写变得极其简单:

// 写入高电平到LED引脚
HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin, GPIO_PIN_SET);

// 读取按键状态
if (HAL_GPIO_ReadPin(USER_BUTTON_GPIO_Port, USER_BUTTON_Pin) == GPIO_PIN_RESET) {
    HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
}

这些函数内部通过对ODR(输出数据寄存器)和IDR(输入数据寄存器)的原子操作实现,保证线程安全。即使在FreeRTOS环境下也能放心使用。


系统时钟与延时精度:别让“HAL_Delay(1)”害了你

很多初学者以为 HAL_Delay(1) 就是精确1ms,其实不然。它依赖于SysTick中断,而中断本身就有抖动。更重要的是,在某些场合你需要的是 微秒级 延时,比如驱动DHT11温湿度传感器。

时钟源选择策略

时钟源 频率 精度 功耗 适用场景
HSI 8 MHz ±1% ~ ±2% 较低 快速启动、无晶振场合
HSE 4–16 MHz ±10ppm 较高 高精度通信(USB、RTC)
PLL 可达72MHz 依赖输入源 中等 主系统时钟源

推荐配置: HSE 8MHz + PLL ×9 = 72MHz ,兼顾精度与性能。

微秒级延时实现方案

方案一:基于DWT(推荐)
__STATIC_INLINE void delay_us(uint32_t us) {
    uint32_t start = DWT->CYCCNT;
    uint32_t cycles = us * (SystemCoreClock / 1000000);
    while ((DWT->CYCCNT - start) < cycles);
}

// 使用前需启用
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

DWT是Cortex-M3内置的数据观察点单元, CYCCNT 是一个24位自由运行计数器,每秒计72M次,精度极高。只要不开调试功能,完全不影响正常运行。

方案二:基于SysTick(备用)
void delay_ms(uint32_t ms) {
    uint32_t tick_start = HAL_GetTick();
    while ((HAL_GetTick() - tick_start) < ms) {
        __WFI(); // Wait for Interrupt,降低功耗
    }
}

适用于毫秒级以上延时,且希望节能的场景。


外设驱动方法论:效率与可维护性的平衡艺术

最后聊聊开发哲学层面的问题:什么时候该用LL库?什么时候用HAL?要不要自己写寄存器?

特性 HAL 库 LL 库
抽象层级
可移植性 强(跨系列兼容) 弱(特定型号)
执行效率 相对较低(函数调用开销) 高(内联函数为主)
开发效率 高(API统一) 低(需熟悉寄存器)
适用场景 快速开发、产品级项目 性能敏感、资源受限系统

我的建议是:
中小型项目 :HAL + CubeMX 组合,快速迭代
高性能场景 (如PWM波形生成、ADC采样):局部使用LL优化关键路径
学习阶段 :先掌握HAL,再深入LL,最后尝试手动操作寄存器

记住一句话: 不要为了“炫技”而去写底层代码,但一定要有能力看懂它


DHT11温湿度传感与时序控制工程实现

终于到了激动人心的部分!我们要让STM32真正“感知世界”——通过DHT11采集温湿度数据。

但先泼盆冷水 ❄️:DHT11不是插上去就能读的“智能设备”。它的通信协议非常特殊,属于典型的“软件模拟时序”类传感器,成败全在于你对 时间的掌控

协议解析:时间即信号

DHT11采用单总线通信,仅用一根数据线完成双向交互。整个流程分为四步:

  1. 主机发送启动信号 :拉低至少18ms
  2. 等待DHT11响应 :20~40μs后,DHT11拉低80μs表示在线
  3. 进入数据传输阶段 :共40位,每位以低电平开始,高电平持续时间编码‘0’或‘1’
    - ‘0’:高电平约26–28μs
    - ‘1’:高电平约70μs
  4. 数据校验 :第5字节为前4字节之和
sequenceDiagram
    participant MCU
    participant DHT11

    MCU->>DHT11: 拉低 >18ms (启动信号)
    MCU->>DHT11: 拉高 20~40μs
    DHT11->>MCU: 拉低 ~80μs (响应开始)
    DHT11->>MCU: 拉高 ~80μs (响应结束)

    loop 40 bits
        DHT11->>MCU: 拉低 ~50μs (位起始)
        alt bit = '0'
            DHT11->>MCU: 高电平 ~26–28μs
        else bit = '1'
            DHT11->>MCU: 高电平 ~70μs
        end
    end

看出难点了吗?没有独立时钟线,所有信息都靠脉冲宽度传递。这就要求MCU必须精准控制每一个动作的时间窗口。


启动与响应检测:别被“HAL_Delay”坑了

新手常犯错误:用 HAL_Delay(18) 发启动信号。问题是这个函数最小单位是1ms,你怎么知道它到底延了多久?更何况中间还可能被中断打断。

正确做法是使用cycle级延时:

static void dht11_delay_us(uint32_t us) {
    uint32_t start = SysTick->VAL;
    uint32_t ticks = us * (SystemCoreClock / 1000000);
    while ((start - SysTick->VAL) < ticks);
}

然后完整握手流程如下:

uint8_t dht11_start(void) {
    // 设置为推挽输出,拉低18ms
    LL_GPIO_SetPinMode(DHT11_PORT, DHT11_PIN, LL_GPIO_MODE_OUTPUT);
    LL_GPIO_ResetOutputPin(DHT11_PORT, DHT11_PIN);
    dht11_delay_us(18000);

    // 拉高,等待20~40μs
    LL_GPIO_SetOutputPin(DHT11_PORT, DHT11_PIN);
    dht11_delay_us(30);

    // 切换为输入模式,等待响应
    LL_GPIO_SetPinMode(DHT11_PORT, DHT11_PIN, LL_GPIO_MODE_INPUT);

    uint32_t timeout = 0;
    while (LL_GPIO_ReadInputPortBit(DHT11_PORT, DHT11_PIN)) {
        if (++timeout > 100) return DHT11_ERR_TIMEOUT;
        dht11_delay_us(1);
    }

    timeout = 0;
    while (!LL_GPIO_ReadInputPortBit(DHT11_PORT, DHT11_PIN)) {
        if (++timeout > 100) return DHT11_ERR_TIMEOUT;
        dht11_delay_us(1);
    }

    return DHT11_OK;
}

这段代码实现了完整的握手验证,失败会返回错误码,便于后续重试或报警。


数据读取与校验:稳定性来自细节

接下来是40位数据的逐位读取:

uint8_t dht11_read_bit(void) {
    uint32_t start;

    // 等待低电平结束(进入高电平)
    while (!LL_GPIO_ReadInputPortBit(DHT11_PORT, DHT11_PIN));

    // 记录起始时间
    start = DWT->CYCCNT;

    // 等待高电平结束
    while (LL_GPIO_ReadInputPortBit(DHT11_PORT, DHT11_PIN));

    // 计算持续时间(us)
    uint32_t duration = (DWT->CYCCNT - start) / (SystemCoreClock / 1000000);

    return (duration > 50) ? 1 : 0;  // 大于50μs视为'1'
}

然后组装成字节:

uint8_t dht11_read_bytes(uint8_t *data) {
    for (int i = 0; i < 5; i++) {
        data[i] = 0;
        for (int j = 0; j < 8; j++) {
            data[i] <<= 1;
            data[i] |= dht11_read_bit();
        }
    }
    return DHT11_OK;
}

最后做校验:

uint8_t dht11_check_checksum(uint8_t *data) {
    uint8_t sum = data[0] + data[1] + data[2] + data[3];
    return (sum == data[4]) ? DHT11_OK : DHT11_ERR_CHECKSUM;
}

注意:DHT11的小数位恒为0,所以实际只用关心前两个字节。


错误处理与滤波策略:打造工业级可靠性

别忘了DHT11更新周期是2秒,连续读取间隔应≥1秒。否则可能读到未刷新的数据。

建议加入以下机制:
- 超时重试(最多3次)
- 滑动平均滤波
- 异常值剔除(如湿度>100%显然不对)

float last_temps[5] = {0};
int idx = 0;

float get_filtered_temp(float raw) {
    if (raw < 0 || raw > 80) return last_temps[idx]; // 过滤异常值
    last_temps[idx++] = raw;
    if (idx >= 5) idx = 0;
    float sum = 0;
    for (int i = 0; i < 5; i++) sum += last_temps[i];
    return sum / 5;
}

这样一来,即便个别采样出错,整体输出依然平稳。


LCD显示驱动与系统集成实战部署

终于要把数据显示出来了!我们选用经典的16x2字符型LCD,基于HD44780控制器。

HD44780工作原理:命令与数据的切换艺术

这块屏有两个关键引脚:
- RS :寄存器选择。RS=0发命令,RS=1发数据
- E :使能信号。上升沿锁存数据

虽然支持8位模式,但为了节省IO,通常使用 4位模式 ,只接D4-D7四根线,每个字节分两次发送。

典型初始化流程:

void lcd_init() {
    HAL_Delay(15);
    lcd_write_4bits(0x03);
    HAL_Delay(5);
    lcd_write_4bits(0x03);
    HAL_Delay(1);
    lcd_write_4bits(0x03);
    lcd_write_4bits(0x02);  // 进入4位模式

    lcd_send_cmd(0x28);     // 4位, 两行, 5x7点阵
    lcd_send_cmd(0x0C);     // 开显示,关光标
    lcd_send_cmd(0x06);     // 自动递增光标
    lcd_send_cmd(0x01);     // 清屏
    HAL_Delay(2);
}

这个“三次发0x03”的操作是为了同步LCD的状态机,千万不能省略!


动态刷新与模块化封装

封装常用函数:

void lcd_print(char *str) {
    for(int i=0; str[i]!='\0'; i++) {
        lcd_send_data(str[i]);
    }
}

void lcd_set_cursor(uint8_t row, uint8_t col) {
    uint8_t base[] = {0x80, 0xC0};
    lcd_send_cmd(base[row] + col);
}

void lcd_clear(void) {
    lcd_send_cmd(0x01);
    HAL_Delay(2);
}

然后就可以愉快地显示数据了:

lcd_clear();
lcd_set_cursor(0, 0);
lcd_print("Temp:");
lcd_set_cursor(0, 6);
char buf[10];
sprintf(buf, "%.1fC", temperature);
lcd_print(buf);

lcd_set_cursor(1, 0);
lcd_print("Humidity:");
lcd_set_cursor(1, 10);
sprintf(buf, "%.1f%%", humidity);
lcd_print(buf);

每隔1秒刷新一次,一个完整的温湿度监测终端就完成了 🎉!


总结与展望

回顾整个开发流程,我们经历了:
1. 从零搭建STM32开发环境
2. 深入理解GPIO各种模式的应用场景
3. 掌握微秒级时序控制技巧
4. 实现DHT11可靠数据采集
5. 构建LCD本地显示界面

这一套组合拳下来,不仅是技术的积累,更是思维方式的转变: 从“让灯亮”到“让系统稳”

未来还可以在此基础上扩展:
- 加入按键实现菜单交互
- 添加串口上传数据到PC或云平台
- 换成OLED实现图形化界面
- 引入FreeRTOS实现多任务调度

嵌入式的世界远不止于此,但每一次成功的点亮、每一次稳定的读取,都是通往更广阔天地的台阶。保持好奇,持续动手,你会发现:原来改变世界的起点,就在这一块小小的蓝色板子上 🌍✨。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:本项目以STM32F103C8T6为核心控制器,构建了一个集环境温湿度采集与实时显示于一体的嵌入式系统。通过C语言编程实现对DHT11数字温湿度传感器的数据读取,并利用LCD显示屏直观展示温度和湿度信息。系统涵盖微控制器初始化、GPIO配置、时序控制通信协议解析及字符型LCD驱动等关键技术,适用于嵌入式开发初学者掌握传感器应用与硬件接口编程。项目使用STM32 HAL库或寄存器操作进行底层控制,结合STM32CubeIDE或Keil等开发工具完成编译调试,全面锻炼嵌入式系统设计与调试能力。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐