STM32F103C8T6 · HAL库开发 · 1-Wire单总线通信
DS18B20是Dallas/Maxim公司推出的单总线数字温度传感器。其内部集成了温度传感电路、模数转换器和数字接口逻辑。温度测量基于带隙温度传感器原理:芯片内部有一个温度敏感振荡器和一个温度不敏感振荡器,通过计数两个振荡器的周期差值来获取温度数据,并将结果以16位数字量(二进制补码形式)存储在暂存器(Scratchpad)中。
DS18B20采用Dallas 1-Wire单总线协议,仅需一根数据线(DQ)即可实现双向通信。主机(STM32)通过严格的时序控制,在数据线上产生复位脉冲、写入命令字节和读取数据字节。芯片支持9~12位可编程分辨率,12位模式下精度达0.0625°C,转换时间约750ms。每个DS18B20出厂时拥有唯一的64位ROM序列号,支持单总线上挂载多个传感器。内置CRC8校验机制(多项式X⁸+X⁵+X⁴+1),确保数据传输的可靠性。
仅需一根数据线(DQ)即可完成双向数据传输,大幅节省MCU引脚资源和布线成本,支持长距离通信。
12位分辨率下精度0.0625°C,测温范围-55°C~+125°C,在-10°C~+85°C范围内最大误差仅±0.5°C。
暂存器第9字节为前8字节的CRC校验码,通信时自动校验数据完整性,有效避免传输错误导致的误读。
每颗芯片拥有唯一64位ROM ID,单条总线上可挂载多个DS18B20,通过ROM命令逐一寻址,适合分布式测温。
| 引脚号 | 引脚名称 | 功能描述 | STM32连接 |
|---|---|---|---|
| 1 | GND | 地线,电源负极 | GND |
| 2 | DQ | 单总线数据通信引脚(开漏模式,需上拉电阻) | PA0 |
| 3 | VDD | 电源正极(2.5V~5.5V) | 3.3V |
配置STM32F103C8T6系统时钟为72MHz。使用外部高速晶振(HSE 8MHz),通过PLL倍频9倍得到72MHz系统时钟。AHB=72MHz,APB1=36MHz,APB2=72MHz,Flash等待周期2。
/* HSE外部晶振 8MHz → PLL×9 → SYSCLK 72MHz */ void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9; // 8MHz × 9 = 72MHz HAL_RCC_OscConfig(&RCC_OscInitStruct); RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; // HCLK = 72MHz RCC_ClkInitStruct.APB1CLKDivider= RCC_HCLK_DIV2; // PCLK1 = 36MHz RCC_ClkInitStruct.APB2CLKDivider= RCC_HCLK_DIV1; // PCLK2 = 72MHz HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2); }
DS18B20数据引脚DQ连接STM32的PA0。配置为开漏输出模式(GPIO_MODE_OUTPUT_OD),启用内部上拉电阻,高速模式。开漏模式是1-Wire协议的要求——主机和从机都可以拉低总线,释放时由上拉电阻恢复高电平,实现"线与"逻辑。
#define DQ_Pin GPIO_PIN_0 // PA0 #define DQ_GPIO_Port GPIOA void Ds18b20_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; DQ_GPIO_CLK_ENABLE(); // 使能GPIOA时钟 GPIO_InitStruct.Pin = DQ_Pin; // PA0 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出(1-Wire必需) GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速 HAL_GPIO_Init(DQ_GPIO_Port, &GPIO_InitStruct); }
1-Wire通信的起始步骤。主机拉低DQ线750μs产生复位脉冲,然后释放总线。DS18B20检测到复位脉冲后,在15~60μs内拉低总线60~240μs产生存在脉冲(Presence Pulse)。主机通过检测此脉冲确认从机在线。
void DS_rest(void) { HAL_GPIO_WritePin(DQ_GPIO_Port, DQ_Pin, GPIO_PIN_SET); // 释放总线 delay_us(1); HAL_GPIO_WritePin(DQ_GPIO_Port, DQ_Pin, GPIO_PIN_RESET); // 拉低750us(复位脉冲) delay_us(750); HAL_GPIO_WritePin(DQ_GPIO_Port, DQ_Pin, GPIO_PIN_SET); // 释放总线,等待存在脉冲 delay_us(100); while(HAL_GPIO_ReadPin(DQ_GPIO_Port, DQ_Pin)) // 等待DS18B20拉低(存在脉冲) { delay_us(1); ds_cnt++; if(ds_cnt==200) { ds_cnt=0; break; } // 超时退出(200us) } delay_us(300); }
向DS18B20写入一个字节,LSB优先(低位先发)。每位时序:拉低总线2μs启动写时隙,然后将数据位写入DQ,保持30μs。写"1"时释放总线(由上拉拉高),写"0"时保持低电平。
void write_byte(uint8_t dat) { unsigned char i; for(i=0; i<8; i++) { HAL_GPIO_WritePin(DQ_GPIO_Port, DQ_Pin, GPIO_PIN_SET); // 释放 delay_us(1); HAL_GPIO_WritePin(DQ_GPIO_Port, DQ_Pin, GPIO_PIN_RESET); // 拉低2us(启动写时隙) delay_us(2); HAL_GPIO_WritePin(DQ_GPIO_Port, DQ_Pin, (GPIO_PinState)(dat&0x01)); // 写入数据位 delay_us(30); // 保持30us dat = dat >> 1; // 右移,LSB优先 } }
从DS18B20读取一个字节,LSB优先。每位时序:主机拉低总线2μs启动读时隙,然后释放总线2μs,接着读取DQ引脚电平。DS18B20在此时隙内将数据位驱动到总线上。读取后保持30μs完成时序。
unsigned char read_byte(void) { unsigned char i, dat, tmp=0; for(i=0; i<8; i++) { HAL_GPIO_WritePin(DQ_GPIO_Port, DQ_Pin, GPIO_PIN_SET); // 释放 delay_us(1); HAL_GPIO_WritePin(DQ_GPIO_Port, DQ_Pin, GPIO_PIN_RESET); // 拉低2us(启动读时隙) delay_us(2); HAL_GPIO_WritePin(DQ_GPIO_Port, DQ_Pin, GPIO_PIN_SET); // 释放,等待数据 delay_us(2); dat = HAL_GPIO_ReadPin(DQ_GPIO_Port, DQ_Pin); // 读取DQ电平 tmp |= (dat<<i); // 组装,LSB优先 delay_us(30); } return tmp; }
完整的温度读取流程:①复位 → Skip ROM(0xCC) → Convert T(0x44),触发温度转换;②复位 → Skip ROM(0xCC) → Read Scratchpad(0xBE),读取暂存器前2字节温度数据。温度原始值为16位补码,乘以0.0625得到实际温度。
unsigned int Ds18b20_Read_Temp(void) { unsigned char i, j; unsigned int tem = 0; /* 第一步: 触发温度转换 */ DS_rest(); // 复位脉冲 write_byte(0xCC); // Skip ROM (跳过ROM寻址) write_byte(0x44); // Convert T (启动温度转换) /* 第二步: 读取暂存器温度数据 */ DS_rest(); // 再次复位 write_byte(0xCC); // Skip ROM write_byte(0xBE); // Read Scratchpad (读暂存器) i = read_byte(); // 温度低字节 LSB (data[0]) j = read_byte(); // 温度高字节 MSB (data[1]) tem |= j & 0x0F; tem <<= 8; tem |= i; tem = (tem * 0.0625); // 原始值 × 0.0625 = 实际温度 return tem; }
读取完整9字节暂存器后,对前8字节进行CRC8校验(多项式0x8C,即X⁸+X⁵+X⁴+1)。校验通过后,从前2字节提取16位温度原始值,计算实际温度并放大10倍返回(便于OLED显示1位小数)。
/* CRC8校验函数 — 多项式: X^8+X^5+X^4+1 (0x8C) */ uint8_t DS18B20_Check_CRC(uint8_t *data, uint8_t len) { uint8_t crc = 0; for (uint8_t i = 0; i < len; i++) { crc ^= data[i]; // 逐字节异或 for (uint8_t j = 0; j < 8; j++) { if (crc & 0x01) crc = (crc >> 1) ^ 0x8C; // 多项式异或 else crc >>= 1; } } return crc; } /* 读取温度数据并进行CRC校验 */ int16_t DS18B20_Read_Temp_With_CRC(void) { uint8_t data[9]; // 9字节暂存器 Ds18b20_Read_Temp(); // 触发温度转换 DS18B20_Read_Scratchpad(data, 9); // 读取9字节数据 uint8_t crc_calc = DS18B20_Check_CRC(data, 8); // 计算前8字节CRC if (crc_calc == data[8]) { // CRC校验通过 int16_t temp = ((data[1]<<8)|data[0]) * 10 / 16.0; return temp; // 返回温度×10 } else { return 0x7FFF; // 校验失败,返回错误值 } }
| 实际温度 | 原始值(Hex) | 原始值(Dec) | 返回值(×10) | OLED显示 |
|---|---|---|---|---|
| -55°C | 0xFC90 | -880 | -550 | -55.0℃ |
| -10°C | 0xFF5E | -160 | -100 | -10.0℃ |
| 0°C | 0x0000 | 0 | 0 | 0.0℃ |
| 25.0°C | 0x0190 | 400 | 250 | 25.0℃ |
| 25.5°C | 0x019A | 410 | 256 | 25.6℃ |
| 85°C | 0x0550 | 1360 | 850 | 85.0℃ |
| 125°C | 0x07D0 | 2000 | 1250 | 125.0℃ |
主循环每500ms执行一次温度读取和OLED刷新。使用HAL_GetTick()获取系统滴答定时器计数值,实现非阻塞式定时。读取温度后调用OLED_Clear()清屏,OLED_CenterPrintf()居中显示温度,最后OLED_Update()刷新显存到屏幕。
while (1) { if(HAL_GetTick() - Time_Now >= 500) // 500ms更新一次 { temp = DS18B20_Read_Temp_With_CRC(); // 读取温度(CRC校验) OLED_Clear(); // 清屏 OLED_CenterPrintf(24, "温度:%.1f℃", temp/10.0); // 居中显示温度 OLED_Update(); // 更新OLED显示 Time_Now = HAL_GetTick(); // 更新时间戳 } }
拖动滑块模拟环境温度变化,点击"开始演示"后系统将模拟STM32每500ms读取一次DS18B20温度的完整过程。左侧展示1-Wire通信协议步骤和DQ数据线脉冲,右侧OLED屏幕实时显示温度值,温度计随温度变化颜色和高度,数据面板显示原始值、计算结果和CRC校验状态。