室内检测有无人系统完整设计方案
[资料下载](https://wwapn.lanzoul.com/b01d71wsba
密码:1aw4)
1. 系统目标
本终端实现室内有无人检测,基于STM32F103C8T6单片机,并通过 LCD1602 本地显示和串口 JSON 上报完成状态输出。具体功能如下:
- 通过1 路 PIR人体红外传感器(LD2402或AM312)检测室内是否有人。
- PIR 检测到人体(高电平)时立即点亮 LED,并在无人持续5 秒后熄灭 LED。
- LCD1602 实时显示房间有人/无人状态与 LED 状态。
- USART1(调试)与 USART2(上报)以 JSON 格式同步输出状态数据。
- 业务逻辑相互独立,通过共享全局变量传递状态。
2. 硬件组成与引脚分配
2.1 完整引脚表
根据现有代码,引脚和外设的分配严格遵循下表(PIR 为下拉输入):
| 引脚 | 功能 | 配置方式 | 说明 |
|---|---|---|---|
| PA0 | PIR1 | GPIO InputPull-down | 人体红外输入,高电平=检测到人 |
| PB8 | LED | GPIO Push-Pull Output | LED 指示灯(高电平亮) |
| PB0 | LCD_D4 | GPIO Push-Pull Output | LCD1602 数据位 4 |
| PB1 | LCD_D5 | GPIO Push-Pull Output | LCD1602 数据位 5 |
| PB10 | LCD_D6 | GPIO Push-Pull Output | LCD1602 数据位 6 |
| PB11 | LCD_D7 | GPIO Push-Pull Output | LCD1602 数据位 7 |
| PB12 | LCD_RS | GPIO Push-Pull Output | LCD1602 寄存器选择 |
| PB13 | LCD_EN | GPIO Push-Pull Output | LCD1602 使能脉冲 |
| PA9 | USART1_TX | 复用推挽输出 | 调试串口(接 Virtual Terminal) |
| PA10 | USART1_RX | 浮空输入 | 调试串口接收 |
| PA2 | USART2_TX | 复用推挽输出 | JSON 状态上报(接 COMPIM) |
| PA3 | USART2_RX | 浮空输入 | 上报接收 |
注:LCD1602 采用 4-bit 并行接口模式。其 RW 引脚直接接地(GND),处于只写模式,不占用 MCU 引脚。
3. STM32CubeMX 配置参数
3.1 时钟配置 (HSI)
- 内置高速时钟 HSI:8 MHz
- 系统主频 (SYSCLK):64 MHz(经 PLL 16倍频)
3.2 外设配置参数
- TIM2:预分频系数(Prescaler)=6399,自动重装载值(Period)=9999。开启全局中断(产生 1 Hz 定时节拍)。
- USART1 & USART2:波特率9600,8位数据位,1位停止位,无校验。
4. 软件架构与库函数实现
4.1 核心 LCD1602 标准显示库
代码集成了自适应 4 位总线的 51 移植版高级显示函数,移除了原方案中未使用的函数(如原方案的LCD_Print,统一改回代码实际使用的命名):
| 函数原型 | 作用说明 |
|---|---|
void LCD_Init(void); | 初始化 LCD(含 4 位总线软件复位唤醒序列) |
void LCD_WriteCommand(unsigned char Command); | 写入控制命令(0x01清屏时自动追加 3ms 延迟) |
void LCD_WriteData(unsigned char Data); | 写入字符数据 |
void LCD_ShowString(unsigned char Line, unsigned char Column, unsigned char *String); | 在指定行(1-2)和列(1-16)显示字符串 |
4.2 三模块独立运行调度逻辑
在main.c的主循环中,系统通过各自的触发机制独立运行,避免互相嵌套阻塞:
- 模块1:PIR 与 LED 控制
- 持续轮询 PA0 的状态。
- 触发有人时,立即点亮 PB8,并清除定时器计数。
- 无人时由 TIM2 中断服务函数
HAL_TIM_PeriodElapsedCallback负责每秒累加,满 5 秒自动关灯。
- 模块2:LCD 本地显示刷新
- 基于软件时间戳节拍,固定每300 ms调用一次
LCD_Task_Refresh()刷新屏幕。 - 刷新内容通过长字符串后补空格的方式,自动覆盖旧字符残余,避免频繁调用清屏导致闪烁。
- 基于软件时间戳节拍,固定每300 ms调用一次
- 模块3:串口 JSON 数据上报
- 采用状态变化事件触发机制。
- 仅在检测到
presence或led_state发生改变时,触发Send_Status_JSON()发送数据,大大减少串口拥堵。
5. JSON 数据上报格式
当状态改变时,USART1 和 USART2 会同时发送一行合规的标准 JSON 字符串:
JSON
{"id":"T5","presence":0,"led":0}id:终端唯一标识,固定为"T5"。presence:房间有人状态(1: 有人,0: 无人)。led:当前灯光状态(1: 开启,0: 关闭)。
proteus仿真
硬件接线
面包板效果
main.c代码(STM32CubeMX基于HAL库)
/* USER CODE BEGIN Header */ /** ****************************************************************************** * @file : main.c * @brief : 终端5 室内有无人检测系统(4位总线 + 51经典标准显示库移植版) * 全部业务逻辑与高级显示库函数集成于本文件 USER CODE 区域 * * 引脚分配: * PA0 —— PIR1(GPIO 输入,下拉) * PB8 —— LED(GPIO 推挽输出) * PB0 / PB1 —— LCD D4 / D5 * PB10 / PB11 —— LCD D6 / D7 * PB12 —— LCD RS * PB13 —— LCD EN * PA9 / PA10 —— USART1 TX/RX(调试 Virtual Terminal) * PA2 / PA3 —— USART2 TX/RX(上报 COMPIM) ****************************************************************************** */ /* USER CODE END Header */ /* Includes ------------------------------------------------------------------*/ #include "main.h" /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ #include <stdio.h> #include <string.h> /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ /* USER CODE BEGIN PTD */ /* USER CODE END PTD */ /* Private define ------------------------------------------------------------*/ /* USER CODE BEGIN PD */ /* ── PIR / LED ── */ #define DELAY_OFF_SECONDS 5U /* 无人持续多少秒后关灯 */ /* ── LCD1602 引脚(全部在 GPIOB) ── */ #define LCD_D4_PIN GPIO_PIN_0 #define LCD_D5_PIN GPIO_PIN_1 #define LCD_D6_PIN GPIO_PIN_10 #define LCD_D7_PIN GPIO_PIN_11 #define LCD_RS_PIN GPIO_PIN_12 #define LCD_EN_PIN GPIO_PIN_13 #define LCD_GPIO_PORT GPIOB /* ── LED 引脚 ── */ #define LED_PIN GPIO_PIN_8 #define LED_GPIO_PORT GPIOB /* ── PIR 引脚(仅保留PA0下拉) ── */ #define PIR1_PIN GPIO_PIN_0 #define PIR_GPIO_PORT GPIOA /* ── 刷新节拍 ── */ #define LCD_REFRESH_MS 300U /* USER CODE END PD */ /* Private macro -------------------------------------------------------------*/ /* USER CODE BEGIN PM */ /* USER CODE END PM */ /* Private variables ---------------------------------------------------------*/ TIM_HandleTypeDef htim2; UART_HandleTypeDef huart1; UART_HandleTypeDef huart2; /* USER CODE BEGIN PV */ /* ── 系统状态变量 ── */ static volatile uint8_t presence = 0; /* 1=有人 0=无人 */ static volatile uint8_t led_state = 0; /* 1=亮 0=灭 */ static volatile uint32_t no_person_cnt = 0; /* 无人持续秒计数 */ static volatile uint8_t counting_down = 0; /* 是否正在延时关灯 */ /* 记录上一次上报的状态,用于检测变化 */ static uint8_t last_reported_presence = 0; static uint8_t last_reported_led = 0; /* USER CODE END PV */ /* Private function prototypes -----------------------------------------------*/ void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_TIM2_Init(void); static void MX_USART1_UART_Init(void); static void MX_USART2_UART_Init(void); /* USER CODE BEGIN PFP */ /* ── LCD1602 移植标准库函数声明 ── */ static void Delay_us(uint32_t us); static void LCD_SendNibble(uint8_t nibble); static void LCD_SendByte(uint8_t byte, uint8_t rs); void LCD_Init(void); void LCD_WriteCommand(unsigned char Command); void LCD_WriteData(unsigned char Data); void LCD_SetCursor(unsigned char Line, unsigned char Column); void LCD_ShowChar(unsigned char Line, unsigned char Column, unsigned char Char); void LCD_ShowString(unsigned char Line, unsigned char Column, unsigned char *String); void LCD_ShowNum(unsigned char Line, unsigned char Column, unsigned int Number, unsigned char Length); void LCD_ShowSignedNum(unsigned char Line, unsigned char Column, int Number, unsigned char Length); void LCD_ShowHexNum(unsigned char Line, unsigned char Column, unsigned int Number, unsigned char Length); void LCD_ShowBinNum(unsigned char Line, unsigned char Column, unsigned int Number, unsigned char Length); int LCD_Pow(int X, int Y); /* ── 业务功能函数 ── */ static void LCD_Task_Refresh(void); static void Send_Status_JSON(void); /* USER CODE END PFP */ /* Private user code ---------------------------------------------------------*/ /* USER CODE BEGIN 0 */ /** * @brief 微秒级精确延迟(基于当前主频估计,规避 HAL_Delay 毫秒级阻塞) */ static void Delay_us(uint32_t us) { uint32_t delay = us * 8; while(delay--) { __NOP(); } } /* ============================================================ * 1. LCD1602 底层硬件驱动(4位并行模式) * ============================================================ */ static void LCD_SendNibble(uint8_t nibble) { HAL_GPIO_WritePin(LCD_GPIO_PORT, LCD_D4_PIN, (nibble & 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_GPIO_PORT, LCD_D5_PIN, (nibble & 0x02) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_GPIO_PORT, LCD_D6_PIN, (nibble & 0x04) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(LCD_GPIO_PORT, LCD_D7_PIN, (nibble & 0x08) ? GPIO_PIN_SET : GPIO_PIN_RESET); Delay_us(2); // 地址建立时间 HAL_GPIO_WritePin(LCD_GPIO_PORT, LCD_EN_PIN, GPIO_PIN_SET); Delay_us(5); // 使能脉冲宽度 HAL_GPIO_WritePin(LCD_GPIO_PORT, LCD_EN_PIN, GPIO_PIN_RESET); // 下降沿锁存 Delay_us(2); // 保持时间 } static void LCD_SendByte(uint8_t byte, uint8_t rs) { HAL_GPIO_WritePin(LCD_GPIO_PORT, LCD_RS_PIN, rs ? GPIO_PIN_SET : GPIO_PIN_RESET); // 4位总线分高低半字节发送 LCD_SendNibble(byte >> 4); LCD_SendNibble(byte & 0x0F); Delay_us(80); // 内部执行处理延迟 } /* ============================================================ * 2. 移植自 51 参考库的高级显示函数(针对 STM32 4位总线自适应) * ============================================================ */ void LCD_WriteCommand(unsigned char Command) { LCD_SendByte(Command, 0); if (Command == 0x01) { HAL_Delay(3); // 清屏需要额外长延迟 } } void LCD_WriteData(unsigned char Data) { LCD_SendByte(Data, 1); } void LCD_Init(void) { HAL_Delay(50); // 上电稳定等待 HAL_GPIO_WritePin(LCD_GPIO_PORT, LCD_RS_PIN, GPIO_PIN_RESET); // ── 4位并行总线软件复位唤醒序列 ── LCD_SendNibble(0x03); HAL_Delay(5); LCD_SendNibble(0x03); HAL_Delay(1); LCD_SendNibble(0x03); HAL_Delay(1); LCD_SendNibble(0x02); HAL_Delay(1); // 切换进入 4-bit 模式 // ── 基础参数初始化配置 ── LCD_WriteCommand(0x28); // 4位总线,两行显示,5x7点阵 LCD_WriteCommand(0x0C); // 显示开,光标关,闪烁关 LCD_WriteCommand(0x06); // 写字符后光标自动加1 LCD_WriteCommand(0x01); // 清屏 } void LCD_SetCursor(unsigned char Line, unsigned char Column) { if(Line == 1) { LCD_WriteCommand(0x80 | (Column - 1)); } else { LCD_WriteCommand(0x80 | (Column - 1) + 0x40); } } void LCD_ShowChar(unsigned char Line, unsigned char Column, unsigned char Char) { LCD_SetCursor(Line, Column); LCD_WriteData(Char); } void LCD_ShowString(unsigned char Line, unsigned char Column, unsigned char *String) { unsigned char i; LCD_SetCursor(Line, Column); for(i = 0; String[i] != '\0'; i++) { LCD_WriteData(String[i]); } } int LCD_Pow(int X, int Y) { unsigned char i; int Result = 1; for(i = 0; i < Y; i++) { Result *= X; } return Result; } void LCD_ShowNum(unsigned char Line, unsigned char Column, unsigned int Number, unsigned char Length) { unsigned char i; LCD_SetCursor(Line, Column); for(i = Length; i > 0; i--) { LCD_WriteData('0' + Number / LCD_Pow(10, i - 1) % 10); } } void LCD_ShowSignedNum(unsigned char Line, unsigned char Column, int Number, unsigned char Length) { unsigned char i; unsigned int Number1; LCD_SetCursor(Line, Column); if(Number >= 0) { LCD_WriteData('+'); Number1 = Number; } else { LCD_WriteData('-'); Number1 = -Number; } for(i = Length; i > 0; i--) { LCD_WriteData('0' + Number1 / LCD_Pow(10, i - 1) % 10); } } void LCD_ShowHexNum(unsigned char Line, unsigned char Column, unsigned int Number, unsigned char Length) { unsigned char i; unsigned char SingleNumber; LCD_SetCursor(Line, Column); for(i = Length; i > 0; i--) { SingleNumber = Number / LCD_Pow(16, i - 1) % 16; if(SingleNumber < 10) { LCD_WriteData('0' + SingleNumber); } else { LCD_WriteData('A' + SingleNumber - 10); } } } void LCD_ShowBinNum(unsigned char Line, unsigned char Column, unsigned int Number, unsigned char Length) { unsigned char i; LCD_SetCursor(Line, Column); for(i = Length; i > 0; i--) { LCD_WriteData('0' + Number / LCD_Pow(2, i - 1) % 2); } } /* ============================================================ * 3. 业务刷新与串口上报 * ============================================================ */ static void LCD_Task_Refresh(void) { // 利用高层 ShowString 接口,自动覆盖残余字符,更加简洁整齐 if (presence) LCD_ShowString(1, 1, "Room:Person "); else LCD_ShowString(1, 1, "Room:Empty "); if (led_state) LCD_ShowString(2, 1, "LED :ON "); else LCD_ShowString(2, 1, "LED :OFF "); } static void Send_Status_JSON(void) { char buf[48]; int len; len = snprintf(buf, sizeof(buf), "{\"id\":\"T5\",\"presence\":%d,\"led\":%d}\r\n", (int)presence, (int)led_state); HAL_UART_Transmit(&huart1, (uint8_t *)buf, (uint16_t)len, 100); HAL_UART_Transmit(&huart2, (uint8_t *)buf, (uint16_t)len, 100); } /* USER CODE END 0 */ /** * @brief The application entry point. * @retval int */ int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM2_Init(); MX_USART1_UART_Init(); MX_USART2_UART_Init(); /* USER CODE BEGIN 2 */ HAL_TIM_Base_Start_IT(&htim2); LCD_Init(); // 使用新标准的库接口显示欢迎界面 LCD_ShowString(1, 1, " Terminal 5 "); LCD_ShowString(2, 1, " Initializing"); HAL_Delay(1000); LCD_WriteCommand(0x01); // 清屏 /* 上电发送一次初始状态 */ Send_Status_JSON(); last_reported_presence = presence; last_reported_led = led_state; uint32_t lcd_last_tick = HAL_GetTick(); /* USER CODE END 2 */ while (1) { /* USER CODE BEGIN 3 */ /* ── 模块1:单引脚 PIR 检测 (PA0 下拉) + LED 控制 ── */ GPIO_PinState pir1 = HAL_GPIO_ReadPin(PIR_GPIO_PORT, PIR1_PIN); if (pir1 == GPIO_PIN_SET) { presence = 1; led_state = 1; no_person_cnt = 0; counting_down = 0; HAL_GPIO_WritePin(LED_GPIO_PORT, LED_PIN, GPIO_PIN_SET); } else { presence = 0; if (led_state == 1 && counting_down == 0) { counting_down = 1; no_person_cnt = 0; } } /* ── 模块2:LCD1602 刷新 ── */ if ((HAL_GetTick() - lcd_last_tick) >= LCD_REFRESH_MS) { lcd_last_tick = HAL_GetTick(); LCD_Task_Refresh(); } /* ── 模块3:USART 上报 ── */ if (presence != last_reported_presence || led_state != last_reported_led) { last_reported_presence = presence; last_reported_led = led_state; Send_Status_JSON(); } /* USER CODE END 3 */ } } /** * @brief System Clock Configuration */ void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI; RCC_OscInitStruct.HSIState = RCC_HSI_ON; RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI_DIV2; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL16; if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) { Error_Handler(); } RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1; if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) { Error_Handler(); } } static void MX_TIM2_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; htim2.Instance = TIM2; htim2.Init.Prescaler = 6399; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 9999; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; if (HAL_TIM_Base_Init(&htim2) != HAL_OK) { Error_Handler(); } sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; if (HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig) != HAL_OK) { Error_Handler(); } sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig) != HAL_OK) { Error_Handler(); } /* 配置并开启 TIM2 的 NVIC 中断 */ HAL_NVIC_SetPriority(TIM2_IRQn, 2, 0); HAL_NVIC_EnableIRQ(TIM2_IRQn); } static void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 9600; 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; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } } static void MX_USART2_UART_Init(void) { huart2.Instance = USART2; huart2.Init.BaudRate = 9600; huart2.Init.WordLength = UART_WORDLENGTH_8B; huart2.Init.StopBits = UART_STOPBITS_1; huart2.Init.Parity = UART_PARITY_NONE; huart2.Init.Mode = UART_MODE_TX_RX; huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart2.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart2) != HAL_OK) { Error_Handler(); } } static void MX_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_8, GPIO_PIN_RESET); /* PA0:PIR 输入,配置为下拉模式 */ GPIO_InitStruct.Pin = PIR1_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLDOWN; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); /* PB0/PB1/PB10/PB11(PB12/PB13/PB8) 高频推挽输出 */ GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_8; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); } /* USER CODE BEGIN 4 */ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance != TIM2) return; if (counting_down) { no_person_cnt++; if (no_person_cnt >= DELAY_OFF_SECONDS) { led_state = 0; counting_down = 0; no_person_cnt = 0; HAL_GPIO_WritePin(LED_GPIO_PORT, LED_PIN, GPIO_PIN_RESET); } } } /* USER CODE END 4 */ void Error_Handler(void) { __disable_irq(); while (1) {} } #ifdef USE_FULL_ASSERT void assert_failed(uint8_t *file, uint32_t line) { } #endif /* USE_FULL_ASSERT */ ``