从零开始:用Keil打造一个看得见温度的STM32监控系统
你有没有过这样的经历?
接好了电路,烧录了程序,MCU也在跑——可就是不知道传感器到底“读到了什么”。尤其是初学嵌入式时,面对一串串跳动的AD值,心里直打鼓:“这真的是当前温度吗?我是不是哪里接错了?”
别急。今天我们就来解决这个问题。
我们不讲大道理,也不堆术语,就手把手带你做一个能‘说话’的温度监控系统:芯片实时采集环境温度,通过串口发到电脑上,你在串口助手里就能清清楚楚看到“当前温度:24.6°C”——就像给单片机装上了嘴。
整个项目基于Keil μVision + STM32F103C8T6(蓝丸板)实现,全程使用标准外设库(Standard Peripheral Library),适合刚入门嵌入式开发的同学一步步跟着做。你会发现,原来“感知世界”的第一步,并没有想象中那么难。
为什么选Keil?它真适合新手吗?
市面上做嵌入式的IDE不少,VS Code搭PlatformIO、STM32CubeIDE、IAR……那为什么还要从Keil开始?
因为——它够稳、够全、够直观。
Keil MDK(Microcontroller Development Kit)虽然不是免费的,但它对ARM Cortex-M系列的支持堪称教科书级别。特别是对于STM32F1这类经典芯片,Keil自带完整的设备支持包(Device Family Pack),你新建工程时选个型号,启动文件、寄存器定义、中断向量表全都自动配好,连时钟初始化都能帮你生成模板代码。
更重要的是,它的调试体验非常友好:
- 可以直接看变量实时变化;
- 能进汇编层单步执行;
- 支持外设寄存器视图,比如打开ADC页面,你能一眼看到
DR数据寄存器里的值是不是更新了; - 配合ST-Link下载器,几分钟就能把程序烧进去并开始调试。
所以如果你是第一次接触裸机编程,Keil是一个让你少踩坑、多看到结果的选择。
提示:学生可以申请Keil免费许可证(容量限制32KB),足够跑完本项目。
硬件怎么搭?只需要三个元件
我们的目标很简单:把环境温度变成数字,再打印出来。
为此,硬件部分极其精简:
- 主控:STM32F103C8T6 最小系统板(俗称“蓝丸”)
- 传感器:NTC热敏电阻(如10kΩ @25°C)
- 分压电阻:固定10kΩ电阻一只
- 下载工具:ST-Link V2 或兼容仿真器
连接方式也超级简单:
NTC引脚1 ──┬── 接VCC(3.3V) ├── 接PA0(ADC通道0) └── 接10kΩ下拉电阻到GND也就是说,NTC和固定电阻组成一个分压网络,中间节点接到STM32的PA0脚,这个脚要配置为模拟输入模式。
为什么不用DS18B20或I²C传感器?
因为我们要练的是最核心的能力——如何让MCU真正理解模拟世界。而这一切的起点,就是ADC。
ADC采集:让单片机“读懂”电压
STM32F103内置了一个12位ADC,意味着它可以将0~3.3V之间的电压量化成0到4095共4096个等级。精度约为0.8mV/LSB,用来测温度绰绰有余。
但关键问题来了:
怎么让这个数字变成“摄氏度”?
第一步:获取原始AD值
我们先来看最关键的初始化代码:
void ADC_Temp_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; // 使能GPIOA和ADC1时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); // 配置PA0为模拟输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 注意!必须设为AIN GPIO_Init(GPIOA, &GPIO_InitStructure); // ADC基本配置 ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = DISABLE; ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 1; ADC_Init(ADC1, &ADC_InitStructure); // 设置通道0采样时间(越长越准) ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); // 开启ADC并校准 ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while(ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while(ADC_GetCalibrationStatus(ADC1)); }这里有几个新手容易忽略的点:
GPIO_Mode_AIN必须设置,否则ADC无法正确采样;- 校准步骤不能省,尤其是在冷启动时,ADC内部偏移会影响精度;
- 采样时间选55.5周期以上,给外部RC电路足够的充电时间,避免读数偏低。
接着写一个读取函数:
uint16_t Read_ADC_Value(void) { ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件触发 while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 等待转换完成 return ADC_GetConversionValue(ADC1); // 读取结果 }现在你已经拿到了一个0~4095之间的数值。下一步才是重点:把它翻译成温度。
温度计算:别被非线性吓住
NTC的阻值随温度呈指数变化,理想情况下应该用Steinhart-Hart公式:
$$
\frac{1}{T} = A + B \cdot \ln(R) + C \cdot (\ln(R))^3
$$
听上去很复杂?其实我们可以简化处理。
先根据分压原理算出NTC的实际阻值:
float voltage = (adc_raw_value / 4095.0f) * 3.3f; float r_ntc = 10.0f * voltage / (3.3f - voltage); // 单位kΩ假设你的参考电阻是10kΩ,室温25°C时NTC也是10kΩ,那你可以在两个已知点进行线性拟合:
| 温度(°C) | NTC阻值(kΩ) |
|---|---|
| 0 | ~33 |
| 25 | 10 |
| 50 | ~4.5 |
于是你可以粗略估算:
temperature = 25.0f + (10.0f - r_ntc) * 3.0f; // 每kΩ对应约±3°C当然这不是高精度方案,但对于教学和原型验证完全够用。等你掌握了流程后,完全可以换成查表法或多项式拟合来提升准确性。
为了减少波动,建议连续采样16次取平均:
uint32_t sum = 0; for(int i = 0; i < 16; i++) { sum += Read_ADC_Value(); Delay_ms(5); // 小延时稳定信号 } adc_raw_value = sum >> 4; // 右移代替除法,提高效率串口输出:让数据“开口说话”
有了温度值还不算完——得让人看得见才行。
我们使用USART1,TX接PA9,配置为115200波特率,异步通信模式。初始化如下:
void USART1_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // PA9 复用推挽输出(TX) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure); // PA10 浮空输入(RX) GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置串口参数 USART_InitStructure.USART_BaudRate = 115200; USART_InitStructure.USART_WordLength = USART_WordLength_8b; USART_InitStructure.USART_StopBits = USART_StopBits_1; USART_InitStructure.USART_Parity = USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_Init(USART1, &USART_InitStructure); USART_Cmd(USART1, ENABLE); }发送函数也很直观:
void USART_SendChar(USART_TypeDef* USARTx, char ch) { while (!USART_GetFlagStatus(USARTx, USART_FLAG_TXE)); USART_SendData(USARTx, ch); } void USART_SendString(USART_TypeDef* USARTx, const char* str) { while (*str) { USART_SendChar(USARTx, *str++); } }最后在主循环中整合所有逻辑:
int main(void) { SystemInit(); // 系统时钟初始化(默认72MHz) ADC_Temp_Init(); USART1_Init(); while (1) { float temp = Calculate_Temperature(); // 包含滤波与换算 char buffer[32]; sprintf(buffer, "Temp: %.1f°C\r\n", temp); USART_SendString(USART1, buffer); Delay_ms(1000); // 每秒刷新一次 } }打开XCOM或SSCOM之类的串口助手,选择正确的COM口和波特率,你就会看到一行行温度数据不断刷出:
Temp: 24.3°C Temp: 24.5°C Temp: 24.4°C那一刻你会觉得:我真的让机器感知到了这个世界。
常见问题与避坑指南
别以为一切顺利。我在第一次调试时也遇到一堆问题,总结几个典型的“坑”:
❌ AD值总是接近0或4095?
- 检查PA0是否误设为普通输入或输出;
- 查线路是否有虚焊,NTC是否接反;
- 测一下实际分压点电压是否在合理范围(比如25°C应在1.65V左右);
❌ 温度跳变剧烈?
- 加大采样次数(如32次)并加入滑动平均滤波;
- 检查电源是否干净,可用万用表测VDD是否存在纹波;
- PCB布线上,模拟走线尽量短,远离SWD、时钟线等高频路径;
❌ 串口收不到任何数据?
- 确认TX/RX是否接反;
- 检查USART时钟是否开启(RCC配置);
- 使用示波器观察PA9是否有波形输出;
❌ Keil提示“no target connected”?
- ST-Link驱动是否安装成功?
- SWDIO/SWCLK是否接触良好?
- 是否误开启了JTAG调试导致引脚冲突?
这些问题看似琐碎,但正是它们决定了你是“调通了”,还是“一直卡着”。
这个项目教会了我们什么?
表面上看,这只是个简单的温度显示系统。但背后藏着嵌入式开发的核心脉络:
- 从物理信号到数字世界的桥梁—— ADC 是你理解传感器的第一道关卡;
- 数据不是终点,表达才是—— 把AD值转成字符串发出去,完成了信息传递的闭环;
- 模块化思维养成—— 初始化、采集、处理、输出,每个环节独立又协同;
- 调试能力比编码更重要—— 学会看寄存器、查手册、用串口“问”单片机状态,是你成长为工程师的关键跃迁。
更进一步地,你可以在这个基础上加料:
- 加个OLED屏幕,本地显示;
- 设定阈值,超温点亮LED或蜂鸣报警;
- 用定时器+中断实现精准周期采样,释放CPU;
- 接ESP8266,把温度上传到手机或云平台。
但所有这些扩展的前提,是你亲手跑通了第一个“看得见”的系统。
写在最后:动手,是最好的老师
很多人学嵌入式,看书看视频看了几个月,始终不敢点“Download”按钮。怕烧芯片,怕接错线,怕程序跑不起来。
可我想说:错误不可怕,沉默才可怕。
当你第一次看到串口助手里跳出“Temp: 24.6°C”,那种成就感,远胜于背下十页数据手册。
所以别等了。
插上你的蓝丸板,打开Keil,新建工程,照着上面的代码敲一遍。哪怕只改一个字母,也要让它属于你自己。
你会发现,那个曾经遥远的“智能世界”,其实就藏在这一个个AD值的背后。
如果你在实现过程中遇到了具体问题(比如编译报错、下载失败、数据异常),欢迎留言交流,我们一起排查。毕竟,每一个老手,都曾是个连PA0都找不到的新手。