云计算百科
云计算领域专业知识百科平台

基于 STM32F103 标准库的多模式传感器终端(ADC 三通道 + 滑动平均 + IWDG)

        本文是个人的STM32 标准库的自学记录,代码参考了 ST 官方固件库例程、网上开源项目(江协科技为主)以及 AI 辅助的讨论,所有代码仅供学习交流,也非常希望各位大佬能指点一二,感谢各位前辈的开源分享。

  • 目录

    一、项目概况:

    二、系统架构与基础外设

     2.1 主循环与模块划分

    2.2 PWM调光

    三、ADC 三通道与滑动平均滤波

    3.1 ADC 三通道采集

    3.2 滑动平均滤波

    3.3 电位器(模式1)

    3.4 热敏电阻(模式2)

    3.5 光敏电阻(模式3)

    四、按键、串口日志与看门狗

    4.1 按键

    4.2 串口日志

    4.3 看门狗

    五、功能演示

    六、踩坑记录与总结


    一、项目概况:

        本次项目使用的是STM32F103C8T6 + 标准库外设库,做了一个能采样三种模拟量,三种模式分别使用其中的一路数据、带看门狗的采集系统。

要使用模块有:

传感器 对应的功能
电位器 调节PWM占比,控制LED的亮度(对应模式1)
热敏电阻 温度超阈值(比如 >30°C)蜂鸣器报警(对应模式2)
光敏电阻 光照低于阈值,自动点亮 LED(对应模式3)
有源蜂鸣器 报警响应
按键 切换模式
LED x 3

PWM调光对象

蜂鸣器指示(温度报警时灭)

夜灯(光照低于阈值时亮)

接线图如下图所示:

本项目开发环境为:

开发环境 型号
开发板 野火_STM32F103C8T6核心板
固件库 标准外设库SPL V3.5.0版本
工具链 STM32CubeIDE + GCC

二、系统架构与基础外设

 2.1 主循环与模块划分

        main.c主函数与与TIM3定时器中断服务函数具体代码如下:

#include <stdint.h>
#include "stm32f10x.h"
#include "stm32f10x_conf.h"
#include "stdio.h"

#include "LED.h"
#include "Timer.h"
#include "KEY.h"
#include "Delay.h"
#include "ADC.h"
#include "UART.h"
#include "IWDG.h"

#define FEATURE_COUNT3//模式总数
#define NEXT_FEATURE(Mode_Num + 1)//切换下一个模式
#define FIRST_FEATURE1//返回第一个模式
#define KEY_SCAN_MS 10//按键扫描周期
#defineLOG_REFRESH_MS 2000//日志刷新周期
#define RISING_EDGE1//按下按钮产生上升沿标志位

#if !defined(__SOFT_FP__) && defined(__ARM_FP)
#warning "FPU is not initialized, but the project is compiling for an FPU. Please initialize the FPU before use."
#endif

const char *IWDG_String = "\\n系统卡死,正在重启\\n";//存放系统卡死输出字符
ADC_Value Read_Value;//存放三个传感器输出数值
volatile uint8_t Log_Refresh_SW = 1;//初始化日志刷新标志位
static uint8_t Key_Status; //按键事件状态
static uint8_t Mode_Num = 1;//当前模式,初值 1

int main(void)
{

//初始化对应外设
LED_Init();
Timer_Init();
Key_Init();
ADC_INIT();
UART_Init();
IWDG_Init();

/*判断看门狗重启标志位,看是不是看门狗重启*/
if(RCC_GetFlagStatus(RCC_FLAG_IWDGRST) == SET)
{
UART_SendString((uint8_t*)IWDG_String);
RCC_ClearFlag();//清空标志位
}

while(1)
{
IWDG_ReloadCounter();//喂狗
IWDG_Test();//按键(PC13)阻塞程序,验证看门狗功能是否在工作

//一圈只采一次,后续复用,避免滤波窗口被重复推进
Key_Status = Key_GetKeyNum();
Read_Value.Pot = Pot_GetValue();
Read_Value.Temp =Temp_GetValue();
Read_Value.Light = Light_GetValue();

if(Key_Status == RISING_EDGE)//当检测到按键处于上升沿时
Mode_Num = (Mode_Num < FEATURE_COUNT) ? NEXT_FEATURE : FIRST_FEATURE;

//模式选择
switch(Mode_Num)
{
/*电位器调光模式*/
case Pot_Mode:GPIO_SetBits(GPIOA,GPIO_Pin_3);//关闭光敏夜灯模式的LED
TIM_SetCompare2(TIM2, Read_Value.Pot);//控制对应LED的PWM占空比来控制亮度
break;
/*温度报警模式*/
case Temp_Mode:if(Read_Value.Temp >= TEMP_WARNING_VALUE)//判断是否高于预设值
GPIO_SetBits(GPIOA,GPIO_Pin_2);//蜂鸣器响
else
GPIO_ResetBits(GPIOA,GPIO_Pin_2);//蜂鸣器不响
break;
/*光敏夜灯模式*/
case Light_Mode:GPIO_ResetBits(GPIOA,GPIO_Pin_2);//切出温度报警模式时关闭蜂鸣器
if(Read_Value.Light < MINIMUM_BRIGHT_VALUE)//判断亮度是否低于预设值
GPIO_ResetBits(GPIOA,GPIO_Pin_3);//开灯
else
GPIO_SetBits(GPIOA,GPIO_Pin_3);//关灯
break;
}

UART_Log_Display(&Log_Refresh_SW, Mode_Num, &Read_Value);//串口日志显示系统的状态与信息
}
}

/* 1ms 中断:按键扫描 + 日志刷新标志 */
void TIM3_IRQHandler(void)
{
static uint16_t con_1 = 0;//按键扫描周期计时
static uint16_t con_2 = 0;//串口日志刷新周期计时
if(TIM_GetITStatus(TIM3, TIM_IT_Update) == SET)
{
con_1++;
con_2++;
if(con_1 >= KEY_SCAN_MS)
{
Key_Scan();
con_1 = 0;
}

if(con_2 >= LOG_REFRESH_MS)
{
Log_Refresh_SW = 1;//置串口日志刷新标志
con_2 = 0;
}

}
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);//清空定时器中断标志位
}

main.c主函数流程图如图下所示:

模块职责表,如下表所示:

文件 职责
LED.c PWM 输出 + 三个GPIO
ADC.c 三通道采样、滑动平均、数值换算
KEY.c 按键扫描与按键事件
Timer.c 1ms时基
UART.c 状态日志
IWDG.c 看门狗

三个设计点:

1.喂狗为什么放在循环的开头:         喂狗放在主循环开头,保证每轮循环都刷新计数器,喂狗周期 = 循环周期;                               2.一圈采样一次传感器数值:         之前写的时候case采集一次、日志显示采集一次,采集次数过多滑动滤波失效了,所以放在switch判断语句之前只采样一次;  3.日志为什么用标志位驱动:         之前使用延时阻塞来控制串口日志的刷新频率,但导致了传感器采样频率太低,由于使用的是滑动滤波的原因,滑动窗口要 8 次新采样才能完全更新导致滤波后的数值就要等数十秒才能跟上实际变化,改成定时器中断置标志位后,采样不再被串口发送阻塞。


2.2 PWM调光

要求:本项目的模式 1 是电位器调节LED的亮度,模式该代码如下:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);

TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Period = 1000 – 1;
TIM_TimeBaseInitStruct.TIM_Prescaler = 72 – 1;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct);

TIM_OCInitStruct.TIM_OCIdleState = TIM_OCIdleState_Set;
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStruct.TIM_OCNIdleState = TIM_OCNIdleState_Set;
TIM_OCInitStruct.TIM_OCNPolarity = TIM_OCNPolarity_High;
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStruct.TIM_OutputNState = TIM_OutputNState_Disable;
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStruct.TIM_Pulse = 50;

TIM_OC2Init(TIM2, &TIM_OCInitStruct);
TIM_Cmd(TIM2, ENABLE);

配置过程遇到的坑:         坑一:配置 TIM2 的 RCC 时钟时,把总线选成了 APB2,编译直接错,后面看了一下STM32的数据手册才得知TIM2是挂在APB1总线上的。

//RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM2, ENABLE); //错误配置: error: 'RCC_APB2Periph_TIM2' undeclared
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); //正确配置
/* TIM1、TIM8 挂 APB2;TIM2~TIM7 挂 APB1 */

        坑二:使用TIM_SetCompare1函数调节LED的亮度,调了半天发现亮度不变,后面翻资料后确认后才得知,TIM2_CH2要使TIM_SetCompare2来进行调节,TIM_SetCompare1 改的是 CH1 的 CCR,而 PA1 输出的是 CH2的波形,两者不相干。

//TIM_SetCompare1(TIM2, Read_Value.Pot); //错误配置
TIM_SetCompare2(TIM2, Read_Value.Pot); //正确配置

三、ADC 三通道与滑动平均滤波

3.1 ADC 三通道采集

        采集三通道数据,引脚分别是:PA4(电位器)、PA5(热敏)、PA6(光敏),配置程序如下:

void ADC_INIT(void)
{
ADC_InitTypeDef ADC_InitStruct;
GPIO_InitTypeDef GPIO_InitStruct;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

//ADC 时钟 = 72MHz / 6 = 12MHz(F103 上限 14MHz)
RCC_ADCCLKConfig(RCC_PCLK2_Div6);

GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;

/*PA4接电位器、PA5接热敏传感器、PA6接光敏传感器*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);

ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;
ADC_InitStruct.ADC_ScanConvMode = DISABLE;
ADC_InitStruct.ADC_ContinuousConvMode = DISABLE;
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStruct.ADC_NbrOfChannel = 1;
ADC_Init(ADC1, &ADC_InitStruct);

ADC_Cmd(ADC1, ENABLE);

/*ADC校准过程*/
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1) == SET);//等待复位完成
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1) == SET);//等待校准完成

/* 滑动窗口长度 8,预填充避免开机前 7 次被 0 拉偏 */
for(uint8_t i = 0; i < 8; i++)
{
Pot_GetValue();
Temp_GetValue();
Light_GetValue();
}
}

三个配置选择: 1. ADC 时钟取 12MHz —— ADC 时钟上限 14MHz,72÷6 是够用又不超的取法;

2. 单通道轮询而非扫描模式 —— 三路数据后续要分别换算(查表、归一化), 轮询更直观采样频率也远高于需求;

3. 初始化末尾预填充 8 次 —— 滑动窗口长度是 8,不预填充的话开机前 7 次 窗口里残留 0,输出被拉偏;

3.2 滑动平均滤波

        由于ADC原始数值会因为电源噪声、参考电压波动或者采样时刻的不同而导致有抖动,为了输出数据稳定且不牺牲太多响应速度,采用了滑动平均滤波,代码如下:

/**
* @brief 滑动平均滤波函数
* @param Filter_value:对应的采样数组指针,Value:更新的数据
* @retval 输入值的 8 点平均值
* @note 使每个传感器输出的值更稳定
*/
uint16_t Moving_Average(uint16_t *Filter_value, uint16_t Value)
{
int8_t i;
uint16_tsum = 0;

for(i = 0; i < 7; i++)
{
//正序搬:往低地址搬必须从前往后
Filter_value[i] = Filter_value[i + 1];
}

Filter_value[7] = Value;

for(i = 0; i < 8; i++)
{
sum += Filter_value[i];
}

return (sum / 8);
}

 代码的工作原理:

        数组会储存最近8次的数值,把前 7 个元素整体左移一格,最久的第 1 位的数值就会被挤掉,新值会放在数值第 8 位,每次调用的流程就是:7次搬运 -> 8次求和 -> 1次除法。

3.3 电位器(模式1)

        模式1是使用电位器来调节PA1引脚上的LED的亮度,读取PA4引脚上的ADC值,然后映射对应的0 ~ 999 的占空比值,详细程序如下:

/**
* @brief 读取电位器的AD值转换成PWM输出
* @param 无
* @retval 0 ~ 999
*/
uint16_t Pot_GetValue(void)
{
uint16_t AD_Value, led_duty;
static uint16_t pot_buf[8];//滑动采样数值

AD_Value = ADC_GetValue(ADC_Channel_4);

//转换成PWM:范围是0 ~ 999
led_duty = (float)(4095 – AD_Value) / 4095.0 * 999.0;//先转 float,否则整数除法只出 0 或 999

return Moving_Average(pot_buf, led_duty);
}

转换公式有两点注意

第一点:为什么要 4095 – AD_Value?

答:LED为低电平亮,CCR越大越暗,所以要把ADC值反过来映射。

 

第二点:为什么计算(4095 – AD_Value)前要进行强制 float 转换?

答:如果不先执行 float 强制转换,会按照 int 类型来计算会忽略小数点后面的数,结果就只有两个数值 0 或 999;

3.4 热敏电阻(模式2)

        模式2需要环境温度,模块上的热敏电阻与10KΩ电阻分压,ADC 读到的是分压点电压,需要先求出热敏电阻的阻值,再使用二分查表法与插值公式来推算环境温度,具体程序如下:

/**
* @brief 读取热敏传感器的AD值转换成温度输出
* @param 无
* @retval 0 ~ 60(单位:℃)
*/
uint16_t Temp_GetValue(void)
{
uint16_t AD_Value, Temp;
static uint16_ttemp_buf[8];//滑动采样数值
float Vo, Ro;

AD_Value = ADC_GetValue(ADC_Channel_5);
Vo = (float) AD_Value / 4095 * VDD_VOLTAGE;//将AD值转换成输入电压
Ro = (Vo * R_DIVIDER) / (VDD_VOLTAGE – Vo);//计算出热敏电阻的阻值

Temp = Scan_NTC(Ro);//查表 + 插值计算出具体温度

return Moving_Average(temp_buf, Temp);
}

本程序的三个关键点:

1. 为什么用查表,不用线性公式?

答:热敏电阻是负温度系数,阻值-温度是指数关系,不是直线;

                

例如说:25℃阻值为:10KΩ、0℃阻值为:32KΩ、  -25℃阻值为:117KΩ

 -> 所以用线性公式套,中间段误差很大。

2. 表从哪来?

答:南京时恒 MF52A103F3950 规格书的中心值,0 ~ 60℃共61个点,每 1 ℃ 一个点,温度为数组下标,阻值为数组里的参数。

 3. 怎么查?

 答:使用二分查表法与插值公式来反推出实际温度数值。代码如下:

float Scan_NTC(float Ro)
{
uint8_t Left = 0, Right = NTC_LEN – 1, Middle;
float Temp;

/*钳位判断数值是否超出量程*/
if(Ro <= ntc_R_kohm[Right]) return 60.0;//低于表的阻值下限直接输出温度最大值 60℃
if(Ro >= ntc_R_kohm[Left])return 0.0;//高于表的阻值上限直接输出温度最小值 0℃

/*二分查表法,交叉退出循环*/
while(Left <= Right)
{
Middle = (Left + Right) / 2;

if(ntc_R_kohm[Middle] == Ro) return Middle;
else if(Ro > ntc_R_kohm[Middle]) Right = Middle – 1;
else if(Ro < ntc_R_kohm[Middle]) Left= Middle + 1;
}

//从 Right(低温侧)起插值
Temp = Right + (Left – Right) * (Ro – ntc_R_kohm[Right]) / (ntc_R_kohm[Left] – ntc_R_kohm[Right]);

return Temp;
}

3.5 光敏电阻(模式3)

        模式 3 需要获取环境亮度,来判断光照强度,且低于阈值就点灯。代码如下:

/**
* @brief 读取光敏传感器的AD值转换成亮度百分比
* @param 无
* @retval 0 ~ 100(单位:%)
*/
uint16_t Light_GetValue(void)
{
uint16_t AD_Value, Lux;
static uint16_tlight_buf[8];//滑动采样数值

AD_Value = ADC_GetValue(ADC_Channel_6);

/*钳位判断数值是否超出量程*/
if(AD_Value > DARK_VALUE) AD_Value = DARK_VALUE;//过暗就按照最暗量程来计算
if(AD_Value < BRIGHT_VALUE)AD_Value = BRIGHT_VALUE;//过亮就按照最亮量程来计算

/*AD值转换亮度百分比*/
Lux = 100 – ((float)AD_Value – BRIGHT_VALUE) / (DARK_VALUE – BRIGHT_VALUE) * 100;

return Moving_Average(light_buf, Lux);
}

本程序的两个关键点:

        

1.为什么不使用查表,而是使用照度百分比?

答:这里和热敏电阻不一样的地方在于,GL5516数据手册只给了两个点,10Lux 阻值为5 ~10KΩ、暗阻值为0.5MΩ,没有完整的阻值与照度表,查表做不了,所以使用了百分比,来表示明暗程度。

注意:因为光敏的阻值-照度是对数关系(γ=0.5),这个百分比不等于照度百分比,只是相对明暗程度。不过对于"低于阈值点灯"够用。

2.钳位的作用。

答:Moving_Average 里的 sum 是 uint16 类型。AD 超出预设值时公式会算出负数, 赋给 uint16_t 后会回绕成 6 万多,sum 累加时就溢出了,所以先钳住 AD 值再算。


四、按键、串口日志与看门狗

4.1 按键

        使用了两个按键实现的功能为:模式切换(PA0)、看门狗功能验证(PC13),10ms扫面一次按键(PA0),检测上升沿事件。代码如下:

/**
* @brief 扫描按键
* @param 无
* @retval 无
* @note 函数运行在定时器中断函数,每10ms运行一次
* @note 检测到上升沿时置 1
*/
void Key_Scan(void)
{
static uint8_t prev_state = 0, this_state = 0;//储存上一次扫描与本次扫描的状态
prev_state = this_state;
this_state = Key_GetValue();

//上一次是低电平、本次是高电平,判断为上升沿,Key_Num 置 1
if(prev_state == 0 && this_state == 1) Key_Num = 1;

}

/**
* @brief 读取按键状态
* @param 无
* @retval 0 = 无,1 = 有按键事件
* @note 运行在主循环,非阻塞判断按键事件
*/
uint8_t Key_GetKeyNum(void)
{
uint8_t Value;
__disable_irq();//关中断服务,防止读清之间被中断插入导致丢事件
Value = Key_Num;
Key_Num = 0;
__enable_irq();//打开中断服务
return Value;
}

本程序的两个关键点:

        

1.为什么是10ms扫描一次?

答:TIM3中断服务函数里计时,跨过机械按键的抖动窗口(5~20ms),以确保读值稳定。

2.读清分离的作用

答:确保按键事件只会消费一次,读取时关中断,避免"读"和"清"之间被中断插入, 导致事件丢失。

4.2 串口日志

        串口1输出系统的状态日志,波特率为115200,TX为PA9(串口只输出,不输入,所以不配RX),每 2 秒打印一次日志,内容为:三个通道的数值、当前模式。具体代码如下:

/**
* @brief 系统串口日志
* @param Refresh:刷新数据标志位
* @param Mode_Num:系统正在运行的模式
* @param ADC_Struct :传感器数据结构体地址
* @retval 无
* @note 标志位驱动:*Refresh 非 0 才输出,发送完自动清零
*/
void UART_Log_Display(volatile uint8_t *Refresh, uint8_t Mode_Num, ADC_Value *ADC_Struct)
{
static uint8_t Log_String[268];
static uint8_t Mode_Name[64];
uint8_t *p = Log_String;

if(*Refresh == REFRESH_OFF)return;

switch(Mode_Num)
{
case Pot_Mode: sprintf ((char*)Mode_Name, "电位器调光"); break;
case Temp_Mode: sprintf ((char*)Mode_Name, "温度报警(报警条件 >= %d℃)",TEMP_WARNING_VALUE); break;
case Light_Mode:sprintf ((char*)Mode_Name, "光敏夜灯(开灯条件 <= %d%%)", MINIMUM_BRIGHT_VALUE); break;
}

p += sprintf ((char*)p, "\\n/*****系统状态日志*****/\\r\\n");
p += sprintf ((char*)p, "-> 电位器控制PWM占空比:%d/999\\r\\n", ADC_Struct->Pot);
p += sprintf ((char*)p, "-> 热敏电阻检测温度为:%d℃\\r\\n", ADC_Struct->Temp);
p += sprintf ((char*)p, "-> 光敏电阻检测光照强度:%d%%\\r\\n\\n",ADC_Struct->Light);

p += sprintf ((char*)p, "-> 现在是模式为:%s \\r\\n",Mode_Name);
UART_SendString((uint8_t*)Log_String);
*Refresh = REFRESH_OFF;
}

本程序的两个关键点:

        

1. 为什么用标志位驱动,不直接延时?

答:如果在主函数使用 Delay_ms(2000); 阻塞延时的话,会影响滑动平均滤波到达目标数值要等上数十秒,所使用 TIM3 定时中断服务计时,每 2 秒刷新标志置1,日志刷新一次在将刷新标志置 0。 

2. 为什么用游标  p += ?

                

答:sprintf 函数每次会从缓冲区开头写,如果四次都使用 Log_String 写入,就会导致前三次都会被覆盖,只显示最后一次,用指针后推才能显示多行日志。

4.3 看门狗

        IWDG 用于程序跑飞时自动复位。用内部 40kHz LSI,分频 16、重载 2499,标称超时 1 秒。代码如下:

/**
* @brief 独立看门狗初始化
* @param 无
* @retval 无
* @note 超过 1 秒没进行“喂狗”操作看门狗激活,系统重启
*/
void IWDG_Init(void)
{
IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);
IWDG_SetPrescaler(IWDG_Prescaler_16);// LSI = 40000 Hz ,LSI / 16 = 2500Hz
IWDG_SetReload(2499);
IWDG_ReloadCounter();
IWDG_Enable();

}

/**
* @brief 看门狗功能验证测试
* @param 无
* @retval 无
* @note 使用阻塞扫描按键事件
* @note需要按住测试按键(PC13)超过一秒以上
*/
void IWDG_Test(void)
{
const char *P_String = "\\n->独立看门狗功能测试,请按测试按钮的时间超过1秒以上…….\\n";
if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_13) == SET)
{
UART_SendString((uint8_t*)P_String);//串口打印测试条件
Delay_ms(15);
while(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_13) == SET);//阻塞等待按键释放,此期间不喂狗
Delay_ms(15);
}
}

本程序的三个关键点:

        

1. 超时怎么算?

        答:LSI 40kHz ÷ 16 = 2500Hz,即每格 0.4ms。数 2500 格(Reload 填 2499)= 1 秒。

2. 喂狗位置?

        答:放在主循环开头,保证每轮都刷新计数器。

3. 怎么验证复位?

        答:按住 PC13 超过 1 秒,程序阻塞不喂狗 →  看门狗复位。复位后用 RCC_FLAG_IWDGRST 判断复位来源,打印提示。注意:判断完要 RCC_ClearFlag(),否则下次正常上电也会误报。


五、功能演示

功能演示视频


六、踩坑记录与总结

整个项目做下来,最花时间的不是写代码,是找"为什么不对"。

几个印象比较深的坑:

  • TIM2 挂 APB1,不挂 APB2 —— 时钟使能写错总线,编译直接报错
  • PA1 是 TIM2_CH2 —— 用 TIM_SetCompare1 调了半天亮度不变,因为改的是 CH1 的寄存器
  • 整数除法的强转位置 —— (float) 放在第一个操作数上,否则除法先按整数算,占空比只有 0 或 999
  • 忙等延时的系数靠标定 —— 7200 在 -O0 下跑出 3 倍时间,最后实测标定成 6600

几条可复用的经验:

  • 引脚号、通道号、总线号必须配对,选错是静默失效,最难查
  • 注意整数除法和无符号类型——不报错、不崩溃,但结果全错
  • 看门狗复位要有证据,读 RCC_FLAG_IWDGRST 确认,不能靠感觉
  • 基于 STM32F103 标准库的多模式传感器终端项目源码https://github.com/GDchan/stm32-multi-mode-sensor

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 基于 STM32F103 标准库的多模式传感器终端(ADC 三通道 + 滑动平均 + IWDG)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!