STM32F103C8T6温室监测:传感器驱动、数据滤波与系统调度实战
2026/9/16 22:22:50 网站建设 项目流程

简介:这是一套以STM32F103C8T6为核心的温室环境监测系统完整设计资料,面向电子信息、计算机等专业学生,适用于毕业设计、期末大作业及嵌入式项目实践。系统完成空气温湿度、土壤湿度和光照强度的采集、处理与显示,结构涵盖传感器模块、主控电路与数据显示单元。压缩包共280个文件,大小9.12MB,核心包含C/H源码、Keil工程文件、hex固件、markdown设计文档及若干图片,另含较多编译中间与备份文件,可满足源码研读、程序烧录与论文撰写需求。现有56人学习下载,资料内涉及定时器、ADC、I2C、USART等标准外设驱动,附带的详细文档阐述了系统架构、数据处理逻辑与模块功能,能帮助理解ARM Cortex-M3内核在数据采集监控中的实际应用。项目经学术审核,完整度高、可操作性强,适合作为课程设计和毕业设计的参考范本。

1. 小封装的引脚账本:为什么温室监测选 C8T6 而不是大板子

温室环境监测在课程设计和毕设里出镜率很高,但拆开看,硬件链路并不复杂:环境量经传感器变成电信号,MCU 采集、换算、显示,必要时送串口上位机。这套基于 STM32F103C8T6 的工程把温湿度、光照、土壤湿度三路传感器和 OLED 显示集成到一颗 LQFP48 封装的小芯片上,并配套完整源码和设计文档。C8T6 只有 20KB RAM、64KB Flash,单看资源表寒酸,但单总线、I2C、ADC、USART 这四类外设恰好能错开,不用为加外设换大板。资源适合两类人:一类是第一次做多传感器汇总的嵌入式课设学生,另一类是准备答辩前想补足“为什么引脚这么分”细节的应届生。

2. GPIO/ADC/单总线/共享I2C:C8T6 外设资源分配与初始化

2.1 先数引脚,再决定外设能不能共存

STM32F103C8T6 是 48 脚封装,除去电源、地、BOOT、SWD 调试所需的 PA13/PA14 和 NRST,实际可用的 GPIO 有三四十个。这个资源里需要占用的接口是:DHT11 用一根 GPIO 做单总线,BH1750 和 OLED 共用一条 I2C 总线的两根线,土壤湿度探头用 ADC 的模拟输入,再加上 USART1 的 TX/RX 用于向串口助手打印数据,合计只用 7 个引脚。这正是 C8T6 比 ZET6 这类 144 脚大芯片更适合课程设计的原因:PCB 布线简单、板子便宜、出问题也好查。

常见做法是最小系统板加传感器模块直连,本工程的标准外设库代码也是按这套接线写的。值得注意的是资源包里的stm32f10x_*.c属于标准外设库(SPL),不是 HAL 库,初始化和事件判断方式与CubeMX生成的代码有明显差异,下文所有代码都按标准外设库展开。

2.2 引脚分配和库函数初始化

我一般按下面的分配表接线,这套分配的原则是:单总线独占一个 IO,I2C 两根线固定给 PB6/PB7,ADC 通道避开 I2C 引脚,串口留给 PA9/PA10。

外设引脚模式说明
DHT11PB0开漏输出 / 上拉输入单总线,空闲必须为高电平
BH1750 SCLPB6复用开漏I2C1 时钟
BH1750 SDAPB7复用开漏I2C1 数据
OLED SCL/SDAPB6/PB7复用开漏与 BH1750 共用 I2C1
土壤湿度探头PA0模拟输入ADC1_IN0
备用土壤探头PA1模拟输入ADC1_IN1
USART1 TX/RXPA9/PA10复用推挽 / 浮空输入打印调试信息
SWDPA13/PA14默认不要初始化为普通 IO

初始化代码首先要处理 GPIO 模式,这一步比主频配置更容易翻车。DHT11 数据脚先配置为开漏输出并拉高,进入读时序时再切输入;I2C 引脚必须用GPIO_Mode_AF_OD,不能照搬点灯的推挽输出。

void Gpio_Init(void) { GPIO_InitTypeDef gpio; // DHT11 数据脚:PB0,先置高,开漏输出 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_StructInit(&gpio); gpio.GPIO_Pin = GPIO_Pin_0; gpio.GPIO_Mode = GPIO_Mode_Out_OD; gpio.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOB, &gpio); GPIO_SetBits(GPIOB, GPIO_Pin_0); }

开漏模式保证总线空闲时被外部上拉电阻拉到 3.3V,也方便后面把同一根线切到输入。GPIO_Speed_2MHz对单总线足够,DHT11 时序里最短的高电平也有 26us,2MHz 摆率不会把边沿弄缓。

I2C1 的初始化要同时配 GPIO 和 I2C 外设,关键参数是时钟速率和应答模式:

void I2C1_Init(void) { GPIO_InitTypeDef gpio; I2C_InitTypeDef i2c; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); gpio.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode = GPIO_Mode_AF_OD; // I2C 必须复用开漏 gpio.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOB, &gpio); I2C_StructInit(&i2c); i2c.I2C_ClockSpeed = 100000; // 100kHz,标准模式 i2c.I2C_Ack = I2C_Ack_Enable; i2c.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; I2C_Init(I2C1, &i2c); I2C_Cmd(I2C1, ENABLE); }

这里有个容易忽略的点:OLED 模块和 BH1750 模块放在同一条 I2C 总线上时,每个模块上通常都自带一组上拉电阻。多组上拉并联后等效阻值变小,总线负载变大,上升沿变缓。稳妥做法是只保留一组约 4.7k 的上拉到 3.3V,其余模块的上拉焊掉,总线速率保持 100kHz 不要上 400kHz。

土壤湿度探头接 ADC,初始化时要把 PA0/PA1 配成模拟输入。标准外设库的校准流程也要走一遍:

void ADC1_Init_Soil(void) { GPIO_InitTypeDef gpio; ADC_InitTypeDef adc; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); // ADC 时钟 12MHz gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; gpio.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入 GPIO_Init(GPIOA, &gpio); ADC_StructInit(&adc); adc.ADC_Mode = ADC_Mode_Independent; adc.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发 ADC_Init(ADC1, &adc); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); }

RCC_PCLK2_Div6把 72MHz 的 PCLK2 分频到 12MHz,这是该芯片 ADC 的推荐上限。ADC_ExternalTrigConv_None表示每次读取由软件启动转换。校准两次 while 是标准外设库的固定流程,漏掉后首次采样可能偏差较大,但不会完全读不出来。单次采样代码里,采样时间要选长一点:

uint16_t ADC_ReadOnce(uint8_t ch) { ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); return ADC_GetConversionValue(ADC1); }

土壤湿度探头是典型的高内阻源,采样时间太短会导致内部采样电容没充满,读数跳动剧烈。用ADC_SampleTime_239Cycles5(239.5 周期)是实打实的经验值,配 12MHz ADC 时钟即约 20us 采样窗口。

2.3 引脚冲突排查:JTAG 占用和 BOOT 配置

C8T6 上 PA13、PA14 同时连在 SWD 和 JTAG 上,代码里如果把这两个引脚初始化为普通 GPIO,下一次就无法仿真下载,只能按住复位键抢时间下载,或者用串口 ISP 擦除。这个资源没用到 JTAG,只留 SWD,标准外设库中如果需要释放 PA15、PB3、PB4 做普通 IO,可以调用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE),但要在程序能正常烧录的前提下改,否则一次误配置就可能进死循环。

另一个容易误判的问题是 BOOT0。BOOT0 必须通过 10k 电阻下拉到 GND,否则芯片上电后停在系统存储器里的 ISP 引导程序里,主程序不跑,OLED 全黑,DHT11 无应答,串口发出 0x7F。这类问题在毕设联调时经常被误判成传感器损坏,排查顺序应该是供电、复位、BOOT、时钟、外设初始化,最后再看传感器接线。

3. 传感器驱动拆解:DHT11 单总线与 BH1750 的 I2C 时序

3.1 DHT11 单总线:40 个数据位的时序边界

DHT11 一次完整通信包括主机起始信号、从机应答和 40 位数据:8 位湿度整数、8 位湿度小数、8 位温度整数、8 位温度小数、8 位校验和。时序上,主机先把总线拉低至少 18ms,再释放;传感器随后把总线拉低 80us 应答,再拉高 80us,之后每一位数据都以 50us 低电平开始。

阶段电平时间说明
主机起始20ms必须大于 18ms
主机释放20~40us把总线交还给传感器
从机应答80us随后拉高 80us
数据位 0低 50us + 高 26~28us约 78us高电平宽度小于 50us
数据位 1低 50us + 高 70us约 120us高电平宽度大于 50us

判断 0 和 1 的关键不是测量绝对时间,而是在 50us 低电平结束后,用延时采样高电平宽度。下面的读取函数先等每个位的低电平结束,延时 40us 后再读引脚:

uint8_t DHT11_ReadByte(void) { uint8_t byte = 0; for (uint8_t i = 0; i < 8; i++) { while (DHT11_PIN_IN() == RESET); // 跳过 50us 低电平 DelayUs(40); // 在高电平中间采样 byte <<= 1; if (DHT11_PIN_IN() != RESET) { // 高电平 > 40us 视为 1 byte |= 0x01; while (DHT11_PIN_IN() != RESET); // 等本数据位结束 } } return byte; }

40us 阈值选在 26us 和 70us 中间,留了足够裕量。注意DelayUs必须用 SysTick 或定时器实现,不能靠空循环嵌套,不同编译优化等级下空循环延时长度差别很大。我在调试中见过代码开-O0一切正常,开到-O2后 DHT11 全部读到 0xFF,就是延时被编译器优化掉了。

完整读取还要处理应答和校验和:

uint8_t DHT11_Read(float *humi, float *temp) { uint8_t data[5] = {0}; DHT11_PIN_OUT_1(); DelayUs(30); DHT11_PIN_OUT_0(); // 起始信号 DelayMs(20); DHT11_PIN_OUT_1(); // 释放总线 DelayUs(40); if (DHT11_PIN_IN() == RESET) { // 传感器应答低脉冲 uint8_t timeout = 100; while (DHT11_PIN_IN() == RESET && --timeout); if (timeout == 0) return 0; timeout = 100; while (DHT11_PIN_IN() != RESET && --timeout); // 跳过应答高电平 if (timeout == 0) return 0; for (uint8_t i = 0; i < 5; i++) data[i] = DHT11_ReadByte(); // 校验:前四个字节相加,低 8 位等于校验位 uint8_t sum = data[0] + data[1] + data[2] + data[3]; if ((sum & 0xFF) == data[4]) { *humi = data[0] + data[1] * 0.1f; *temp = data[2] + data[3] * 0.1f; return 1; } } return 0; }

注意:DHT11 两次读取间隔必须大于 1 秒,否则传感器内部测量未完成,返回结果不稳定。

超时处理在这里很关键。单总线协议没有时钟线,如果传感器不应答,while会一直卡住,所以每个等待点都要加 timeout 计数。这套驱动里用递减计数简单可靠,如果项目里多个地方都要等待,可以统一封装成Wait_Timeout(condition, count)

3.2 BH1750:共享 I2C 总线上的一次长读取

BH1750 的 I2C 地址固定为 0x46(写)/0x47(读),命令寄存器按操作码区分。这个资源里推荐用连续 H-分辨率模式(0x10),分辨率 1lx,测量时间约 120ms,适合温室里从日出前到正午的动态范围。很多移植示例漏了一个比例因子:读到的 16 位原始值除以 1.2 才是真正的照度 lx。

命令作用
Power Down0x00进入待机
Power On0x01上电唤醒
Continuous H-Resolution0x101lx 分辨率,连续测量
One Time H-Resolution0x20单次测量后自动断电
Reset0x07复位内部寄存器

用标准外设库操作 I2C1 时,事件链必须严格按 START、地址、数据、STOP 的顺序走:

uint8_t BH1750_ReadLux(float *lux) { uint8_t buf[2]; uint16_t raw; I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, 0x46, I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, 0x10); // 连续 H 分辨率模式 while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1, ENABLE); DelayMs(180); // 等转换完成 I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, 0x47, I2C_Direction_Receiver); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); for (uint8_t i = 0; i < 2; i++) { if (i == 0) { while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); buf[0] = I2C_ReceiveData(I2C1); } else { I2C_AcknowledgeConfig(I2C1, DISABLE); // 最后一字节回 NACK while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); buf[1] = I2C_ReceiveData(I2C1); I2C_GenerateSTOP(I2C1, ENABLE); } } I2C_AcknowledgeConfig(I2C1, ENABLE); raw = ((uint16_t)buf[0] << 8) | buf[1]; *lux = (float)raw / 1.2f; return 1; }

这段代码有两个细节。第一,连续模式下每次读取都要重新发一次测量命令,否则读到的是上一次结果;第二,接收最后一个字节前要把 ACK 关掉,从机收到 NACK 后才知道主机不要更多数据了。STOP 之后再恢复 ACK,不然下一次通信的事件状态会被残留标志污染。

OLED 也用同一路 I2C 时,设备地址不同(SSD1306 通常是 0x78 写地址),总线仲裁没问题。但两个模块共用时,如果 OLED 初始化时拉低总线时间过长,BH1750 的测量命令可能被干扰。我一般会先初始化 OLED,再初始化 BH1750,两条 I2C 事务之间至少间隔 50ms。

3.3 长线传输对 I2C 时序的影响

BH1750 模块引线超过 30cm 后,SCL/SDA 的上升沿会明显变缓。100kHz 标准模式下位周期 10us,如果上升沿超过 1us,器件采样点容易错。常见做法是把外部上拉从 4.7k 换成 2.2k,并把延时适当拉长;不要试图把总线提到 400kHz,除非后端接的是示波器验证过的短线。OLED 与 BH1750 同总线时,OLED 的初始化报文很长,初始化完成后加 50ms 延时再开始读光照,可以避免连续 I2C 操作导致总线忙。

4. 滑动滤波与分段标定:把 ADC 裸值变成可读的温室数据

4.1 去毛刺:中位值平均滤波

土壤湿度探头在湿润环境下等效阻抗低,干燥时阻抗高,ADC 输入信号本身带工频干扰。直接读原始值,同一个泥盆连续采样可能波动 ±200 LSB。单纯取平均值能压低噪声,但对突发的开关电源尖峰效果不好。我常使用中位值平均滤波:连续采样 8 次,去掉一个最大值和一个最小值,剩下的 6 个取平均。

#define FILTER_N 8 uint16_t ADC_Filter(uint8_t ch) { uint16_t buf[FILTER_N]; uint32_t sum = 0; uint16_t min, max; for (uint8_t i = 0; i < FILTER_N; i++) { buf[i] = ADC_ReadOnce(ch); DelayMs(2); // 采样间隔大于转换时间即可 } min = buf[0]; max = buf[0]; for (uint8_t i = 0; i < FILTER_N; i++) { if (buf[i] < min) min = buf[i]; if (buf[i] > max) max = buf[i]; sum += buf[i]; } return (uint16_t)((sum - min - max) / (FILTER_N - 2)); }

8 次采样里去掉极值,剩下的算术平均能同时压制白噪声和脉冲毛刺。2ms 的采样间隔对应约 500Hz 采样率,对土壤湿度这种秒级变化完全够用。如果原始信号漂移严重,不要只加大 N,响应滞后会更明显,先检查探头的供电是否稳定、分压电阻是否匹配,再考虑滤波强度。

4.2 土壤湿度标定:两点定标比公式更重要

土壤湿度模块的输出电压随含水率变化,换算不能只套线性公式,必须做两点标定。常见做法是:把探头完全暴露在空气中测一个 ADC 值记为dry_adc,再插入水中测一个wet_adc。换算公式根据输出电压的方向决定,电压随湿度升高而降低的模块使用下面这段:

float Soil_GetPercent(uint16_t adc_value) { if (adc_value >= dry_adc) return 0.0f; // 比空气还干,按 0% 处理 if (adc_value <= wet_adc) return 100.0f; // 完全浸水,按饱和处理 return (float)(dry_adc - adc_value) * 100.0f / (float)(dry_adc - wet_adc); }

标定有两点必须注意。第一,干湿两端要等探头通电预热 30 秒再采样,否则探头表面初始极化状态不稳定,读出的两个标定点没意义。第二,dry_adcwet_adc不是定死的 4095 和 0,不同厂家模块的分压电阻不同,同一个模块浸水深度不同结果差异也很大。把标定点定义成宏,换探头时只改两行。如果用的是电容式土壤湿度模块,输出随湿度升高而升高,公式里的减号要反过来。

这里还要提醒一个系统性误差:STM32F103 的 VREF+ 一般直接接 VDDA,如果板上的 3.3V 由 USB 5V 经 LDO 降压得到,LDO 的输出误差会直接影响 ADC 满量程。所以标定要在实际供电电压下进行,不要在实验室可调电源下调完再拿到 USB 供电上跑,否则标定结果全部偏移。

4.3 OLED 显示与串口打印:数据出口的统一

现场看数据用 OLED,记录数据走串口。SSD1306 的驱动代码这里不展开,重点看数据格式化方式。OLED 两行分别显示温湿度和光照/土壤,串口每一帧打印所有关键量:

char line[32]; sprintf(line, "T:%.1fC H:%.0f%%", temp, humi); OLED_ShowString(0, 0, line); sprintf(line, "Lux:%.0f Soil:%.0f%%", lux, soilPercent); OLED_ShowString(0, 2, line); printf("T=%.1f C H=%.0f%% Lux=%.0f Soil=%.0f%%\r\n", temp, humi, lux, soilPercent);

sprintf处理浮点会在 Keil MDK 下引入较大的浮点库支持代码,C8T6 虽然 64KB Flash,但标准外设库加 OLED 字库后空间并不宽裕。如果编译报 Flash 不足,把%.1f换成整数运算,比如temp的整数部分和小数部分分别取:

int16_t t_int = (int16_t)temp; uint8_t t_dec = (uint8_t)((temp - t_int) * 10); sprintf(line, "T:%d.%dC H:%d%%", t_int, t_dec, (int)humi);

用这种方式能省下几 KB。串口重定向在 MDK 里需要重写fputc并关闭半主机模式,更简单的做法是直接用USART_SendData封装自己的打印函数,输出长度可控,也不依赖微库选项。

5. 前后台调度、看门狗与 CRC16:让监测系统连续跑 72 小时

5.1 主循环里的三级任务调度

很多初版代码会把三个传感器读一遍、OLED 刷一遍,DHT11 卡 20ms,BH1750 等待 180ms,ADC 滤波循环又是十几 ms,一个循环周期超过 200ms。这个系统没有外部中断需求,用前后台调度最合适:SysTick 产生 1ms 心跳,主循环里用“到时间就执行”的非阻塞标志驱动任务。

volatile uint32_t g_tick = 0; void SysTick_Handler(void) { g_tick++; } uint8_t Timeout_Reached(uint32_t *last, uint32_t period) { if (g_tick - *last >= period) { *last = g_tick; return 1; } return 0; } int main(void) { // 时钟、GPIO、I2C、ADC、OLED、串口初始化... uint32_t t_dht = 0, t_lux = 0, t_soil = 0, t_ui = 0; while (1) { if (Timeout_Reached(&t_dht, 2000)) DHT11_Read(&humi, &temp); if (Timeout_Reached(&t_lux, 2000)) BH1750_ReadLux(&lux); if (Timeout_Reached(&t_soil, 1000)) soil = ADC_Filter(ADC_CH_SOIL); if (Timeout_Reached(&t_ui, 500)) OLED_Update(humi, temp, lux, soil); } }

g_tick是 32 位无符号数,g_tick - *last在计数器溢出时按无符号回绕计算,依然正确,不需要额外处理 49 天清零问题。各任务周期独立,DHT11 读失败时下一轮 2 秒后再试,不会阻塞光照和土壤数据的更新。

任务周期最大执行时间失败策略
DHT11 读取2000ms约 25ms下一周期重试,显示--.--
BH1750 读取2000ms约 180ms 等待保留旧值,最多容忍 2 个周期
土壤 ADC 滤波1000ms约 20ms用上次滤波结果
OLED 刷新500ms约 10ms不阻塞主循环

周期拆开还有一个好处:DHT11 需要满足“两次读取间隔大于 1 秒”,主循环调度天然保证 2 秒间隔,不用在驱动代码里额外做时间判断。如果后面要加按键或报警,只需再挂一路Timeout_Reached,不会影响已有任务。

5.2 独立看门狗:死循环和时序卡死的兜底

温室监测往往要长时间上电,程序在某个while等待事件时卡死是常见故障。STM32F103 的 IWDG 由独立的 40kHz LSI 时钟驱动,一旦启动无法由软件停止,非常适合这种无人值守场景:

void IWDG_Init(void) { IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); IWDG_SetPrescaler(IWDG_Prescaler_64); // LSI 40kHz / 64 = 625Hz IWDG_SetReload(1250); // 1250 / 625 = 2 秒 IWDG_ReloadCounter(); IWDG_Enable(); }

喂狗点放在主循环末尾,不要放在传感器读取函数内部。如果 DHT11 的while等待把所在任务卡住,而看门狗喂狗恰好也在同一调用链里,看门狗就失效了。放在主循环末尾可以保证即使某个传感器故障,只要调度器还活着,系统就不会复位,故障状态的现场也能保留。

验证看门狗是否生效有个危险但有效的方法:把 DHT11 数据线拔掉,让读取函数超时返回,程序应继续运行;如果人为进入一个死循环,2 秒后芯片自动复位,OLED 重新初始化,串口打印复位标志RCC_GetFlagStatus(RCC_FLAG_IWDGRST)可以确认复位源来自看门狗。

5.3 串口数据帧的 CRC16:上位机接收前加一道保险

这套系统如果只做本地显示,CRC 可有可无;一旦要接上位机或 4G 模块,建议在串口帧末尾加 CRC16。温室环境数据会被拿去控制风机、卷帘,一个错字节可能导致执行机构误动作。CRC16-MODBUS 多项式 0x8005,初值 0xFFFF,代码量小,适合在 MCU 上跑:

uint16_t CRC16_Modbus(uint8_t *buf, uint16_t len) { uint16_t crc = 0xFFFF; for (uint16_t i = 0; i < len; i++) { crc ^= buf[i]; for (uint8_t j = 0; j < 8; j++) { if (crc & 0x0001) { crc = (crc >> 1) ^ 0xA001; // 0x8005 的反射多项式 } else { crc >>= 1; } } } return crc; }

0xA0010x8005按位反转后的值,这是 Modbus 协议对 CRC 的低字节优先处理方式。发送时先发送 CRC 低字节,再发高字节,这个字节序和很多上位机示例不一致,是联调失败的高发区。接收端收到整帧后重新计算 CRC,与帧尾比对,不一致就丢弃整帧,不做部分处理。

6. 用逻辑分析仪和发布脚本把交付物做“干净”

DHT11 这类单总线器件,时序是否对不能只看读数。用 24MHz 采样率的逻辑分析仪抓 PB0 波形,按“20ms 低电平 → 40us 高 → 80us 低 → 80us 高 → 数据位”的顺序核对。波形验证步骤:

1. 逻辑分析仪 CH0 接 PB0,GND 与板子共地 2. 采样率设 2MHz,触发方式选下降沿,预触发 10% 3. 上电后触发一次,测量第一段低电平宽度,应约 20ms 4. 展开第一个数据位,高电平 26~28us 为 0,70us 左右为 1

2MHz 采样率对应 0.5us 分辨率,足够区分 26us 和 70us 的高电平。如果分析仪显示所有高电平均在 30us 左右,说明数据被判定为全 0,需要查DelayUs实现;如果起始低电平只有 10ms,DHT11 大概率不应答。BH1750 则抓 I2C 的 SCL/SDA,重点看 STOP 后是否有残留 SCL 脉冲。

工程发布前,资源里自带的keilkilll.bat这类脚本值得保留。它负责删除ListingsObjects里的中间编译文件,以及.uvguix.*界面布局文件。给别人发源码压缩包前执行一次,工程体积能从几十 MB 压到几百 KB,.axf.hex按需保留。

文件/目录是否保留理由
Template.uvprojx保留Keil 工程入口
Template.uvguix.*删除本机窗口布局,换机器会重建
Listings/Objects/删除编译中间产物
用户.c/.h保留核心驱动源码
.axf/.hex保留 hexaxf 是调试产物,不需要留给别人

移植到另一块最小系统板时,检查三个点:BOOT0 是否下拉到 GND;PA13/PA14 有没有被 GPIO 初始化占用;I2C 总线上是否只剩一组上拉。这套程序的核心是驱动文件之间耦合很小,从蓝丸板换到自制板主要改stm32f10x_conf.h中的外设使能和引脚宏。第一次移植建议保留原引脚的传感器接线,只改时钟和外设初始化,等数据通路恢复正常后再动管脚映射,这样能把接线问题和代码改动量分开定位,出现异常时不用同时怀疑两个变量。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询