关于STM32F051C8T6某项目总述(一)
1. 总述目录
- 项目简介
- 主控总述
- 模块总述
- TM1640
- R25_F3950
- VS1838B
- AIP650
- W25Q16
- BUZZER
- HEATER
- MOTOR
- TDA7492
- W25Q16
- TDA7492
- KEY
- 其他
- 硬件总述
- 差分运放
- 电流稳定
- 温控开关
- …
2. 总述内容
项目简介
1. 编写时间
2023年10月7日
2. 需求说明
本产品主要功能有加热,振动,电流刺激三种功能。其中加热功能和振动功能由PWM波控制驱动,而电流刺激由DAC控制驱动。产品有显示、控制、报警等各项功能。
报警由MCU和硬件控制,主要报警内容如下:
- MCU监控加热温度,计时,硬件接口是否插入,电流是否过大等
- 硬件(温控开关)主要对温度进行监控,温度过高自动断开电路
- 计时为0时,报警提示工作结束。
主控总述
代码控制详解
- MCU主控芯片型号为STM32F051C8T6
- 用户交互通过数码管显示,按键控制。数码管显示芯片型号为TM1640和AIP650,按键控制芯片为AIP650。
- 部分交互通过电路单刀单掷按键交互。
- 项目使用的是库函数。如果想了解寄存器,需查阅库函数源码。
引脚说明
本项目主控封装的引脚总数是48引脚,其中包含了TIM1、TIM2、TIM3、TIM15、TIM16和TIM17。其中TIM1用于电机振动,TIM2用于VS1838B红外接收头,TIM3用于加热器,TIM16用于计时器,TIM17用于DAC。
模块应用说明
-
-
问题简介
PWM输出方波用于控制电机震动速度,加热器加热速度,是基于定时器实现的。在输出PWM波时,出现了很多问题。
-
首先面临着定时器选择问题。
因为ST芯片定时器的引脚是固定的所以在使用引脚的输入输出时必须事先定好各引脚要用的功能。GPIO还是特定复用功能。本产品在使用加热器PWM波定时器时,采用了一个定时器输出两路PWM波。配置方法如下:
void Heater_GPIO_Init(void) { GPIO_InitTypeDef GPIO_CFG; RCC_AHBPeriphClockCmd(HEATER_RCC_CLOCK, ENABLE); GPIO_CFG.GPIO_Pin = HEATER_GPIO_PIN_INTRA | HEATER_GPIO_PIN_BELLY; GPIO_CFG.GPIO_Speed = GPIO_Speed_50MHz; GPIO_CFG.GPIO_Mode = GPIO_Mode_AF; GPIO_CFG.GPIO_OType = GPIO_OType_PP; GPIO_CFG.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_PinAFConfig(HEATER_GPIO_PORT, GPIO_PinSource0, GPIO_AF_1); GPIO_PinAFConfig(HEATER_GPIO_PORT, GPIO_PinSource1, GPIO_AF_1); GPIO_Init(HEATER_GPIO_PORT, &GPIO_CFG); } void Heater_Tim_Init(void) { TIM_TimeBaseInitTypeDef TIM_CFG; TIM_OCInitTypeDef TIM_OCInitStruct = {0}; // 输出比较结构体 NVIC_InitTypeDef NVIC_CFG; // 使能 TIM3 时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_DeInit(TIM3); TIM_CFG.TIM_Period = 479; TIM_CFG.TIM_ClockDivision = TIM_CKD_DIV1; TIM_CFG.TIM_Prescaler = 99; // 分频系数,使 TIM2 计数器时钟为 1MHz TIM_CFG.TIM_CounterMode = TIM_CounterMode_Up; TIM_CFG.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM3, &TIM_CFG); TIM_OCStructInit(&TIM_OCInitStruct); // 配置输出比较结构体的参数 TIM_OCInitStruct.TIM_Pulse = 0xFFF; // 配置CCR寄存器,控制占空比 TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; // 配置输出比较模式 TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_Low; // 控制输出的极性 TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; // 输出使能 TIM_OC3Init(TIM3, &TIM_OCInitStruct); TIM_OC4Init(TIM3, &TIM_OCInitStruct); TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable); // 让捕获/比较1寄存器 预装载功能使能 同时配置CC1通道为输出 TIM_OC4PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM3, ENABLE); // TIM_ARRPreloadConfig(TIM1, ENABLE); NVIC_CFG.NVIC_IRQChannel = TIM3_IRQn; NVIC_CFG.NVIC_IRQChannelCmd = ENABLE; NVIC_CFG.NVIC_IRQChannelPriority = 3; NVIC_Init(&NVIC_CFG); TIM_ClearFlag(TIM3, TIM_IT_CC3 | TIM_IT_CC4); TIM_ITConfig(TIM3, TIM_IT_CC3 | TIM_IT_CC4, ENABLE); TIM_CtrlPWMOutputs(TIM3, ENABLE); TIM_Cmd(TIM3, ENABLE); }
-
紧接着配置成功了但周期不对,此时需要对分频和周期进行配置。计算方法如下:
PWM频率:FREQ=CK_PSC/(PSC+1)/(ARR+1)
其中CK_PSC为时钟源主频频率,假如配置输出时钟源是外部高速时钟源,那么CK_PSC的时钟源就是48MHZ(STM32F103ZET6是72MHZ),PSC是指预分频系数,预分频系数可以把它理解成时钟源主频细分,预分频系数PSC越大,所能计时的时间片中断越细致。ARR名为预装载寄存器可以理解为阈值。我们要想控制电平反转必定需要给定时间用来确定该什么时间进行翻转。当计数器寄存器CNT的值到达预装载寄存器里的值就进行电平反转,这便是ARR的作用。PSC的作用可以理解为CNT增加的频率,如果PSC为0就表示主分频频率不变,CNT将以每秒48000000次进行计数,如果我们给PSC增加一个0就代表主分频少一个0,计时次数速率变慢,计时就可以更细致。
-
由于通用定时器不够所以用到了高级定时器TIM1,高级定时器的用法和通用定时器的用法略有不同。
高级定时器相较于通用定时器,高级定时器较为复杂,要想把高级定时器当作通用定时器使用,需要进行额外的配置,否则要么不起作用,要么频率不对。以下代码仅仅是把高级定时器当做通用定时器使用。
TIM_TimeBaseInitTypeDef TIM_CFG; TIM_OCInitTypeDef TIM_OCInitStruct = {0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); TIM_DeInit(TIM1); // 重新将Timer设置为缺省值 TIM_CFG.TIM_ClockDivision = TIM_CKD_DIV1; // 48M TIM_CFG.TIM_Prescaler = 99; // 48k 1Tick=1ms TIM_CFG.TIM_CounterMode = TIM_CounterMode_Up; TIM_CFG.TIM_Period = 479; // 1ms周期 TIM_CFG.TIM_RepetitionCounter = 0; TIM1->CR1 |= 0x21; TIM_TimeBaseInit(TIM1, &TIM_CFG); // 输出比较结构体 TIM_OCStructInit(&TIM_OCInitStruct); // 配置输出比较结构体的参数 TIM_OCInitStruct.TIM_Pulse = 0xfff; // 配置CCR寄存器,控制占空比 TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; // 配置输出比较模式 TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; // 控制输出的极性 TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Disable; // 主定時器输出失能 TIM_OCInitStruct.TIM_OutputNState = TIM_OutputNState_Enable; // 补位定时器使能 TIM_OCInitStruct.TIM_OCNPolarity = TIM_OCNPolarity_High;//配置比较寄存器输出极性,当计数器的值等于或超过比较值时,通道的输出信号将为高电平 TIM_OCInitStruct.TIM_OCIdleState = TIM_OCIdleState_Reset;//配置比较寄存器空闲状态输出极性,当定时器不在输出比较状态时,通道的输出信号将保持复位 TIM_OCInitStruct.TIM_OCNIdleState = TIM_OCNIdleState_Set;//表示当定时器不处于输出比较状态时,与该通道关联的输出引脚(OCN引脚)将被设置为高电平(即空闲状态为高电平)。 TIM_OC1Init(TIM1, &TIM_OCInitStruct); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); // 让捕获/比较1寄存器 预装载功能使能 同时配置CC1通道为输出 TIM_ARRPreloadConfig(TIM1, ENABLE); // TIM_SetCompare1(TIM17, 500); TIM_CtrlPWMOutputs(TIM1, ENABLE); // 确定让TIM1输入PWM // TIM_Cmd(TIM1, ENABLE); // 使能TIM1
其中主定时器使能和失能配置是看引脚,因为高级定时器有PWM波补间输出分别是两个通道分布在两个引脚上。故要看你要使用的是哪个引脚,具体高级定时器使用说明请查阅文档,这里只配置高级定时器的的普通pwm波输出。
-
-
使用心得
PWM波主要用于控制占空比来控制设备电流和功率。占空比又分为正占空比和负占空比,两者的区别只是计算方法不同。PWM波可以用于电机,灯光等各种需要控制功率的负载。PWM波是方波,故它的电压是固定的,其所控制的不是电压值而是电流值。MCU的PWM输出需要用到定时器。也就是说如果不加外置芯片靠MCU输出PWM那么其需要用到定时器功能。
-
-
-
问题简介
DAC整体不是方波,而是一种以小方波的形式模拟正弦波。它允许通过改变电压值来稳定电流值。也就是说DAC的作用是为了数字信号模拟模拟信号(个人理解为微积分,也许不对)。本产品中由于项目需求,DAC输出需要与定时器配合。定时器通过定时中断,对DAC输出值进行配置。
-
在开始配置时DAC生成的波形不对,会有溢出现象,突然的电平跳出现象,原因是超出了DAC输出的最大值。因为DAC的最大输出值为0xFFF,即4095。因此当我们配置的值超出此值时,电平信号会突然增高,我们在使用时需要提前算好其最大值。以下是DAC的配置:
void DAC_GPIO_Config(void){ GPIO_InitTypeDef GPIO_CFG; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA,ENABLE); GPIO_CFG.GPIO_Pin = DAC_GPIO_PIN; GPIO_CFG.GPIO_Mode = GPIO_Mode_AN; GPIO_CFG.GPIO_OType = GPIO_OType_PP; GPIO_CFG.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_Init(DAC_GPIO_PORT,&GPIO_CFG); } void DAC_Config(void){ DAC_InitTypeDef DAC_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC,ENABLE); TIM_DeInit(TIM17); // 重新将Timer设置为缺省值 DAC_InitStructure.DAC_Trigger = DAC_Trigger_Software; DAC_InitStructure.DAC_WaveGeneration = DAC_WaveGeneration_None; DAC_InitStructure.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bits11_0; DAC_InitStructure.DAC_OutputBuffer = DAC_OutputBuffer_Enable; DAC_Init(DAC_Channel_1, &DAC_InitStructure); DAC_Cmd(DAC_Channel_1, ENABLE); } void DAC_TIM_Config(void){ NVIC_InitTypeDef NVIC_CFG; TIM_TimeBaseInitTypeDef TIM_CFG ={0}; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM17,ENABLE); TIM_CFG.TIM_ClockDivision = 0; TIM_CFG.TIM_CounterMode = TIM_CounterMode_Up; TIM_CFG.TIM_Prescaler = 47; TIM_CFG.TIM_Period = 499; //TIM_CFG.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM17,&TIM_CFG); TIM_ClearFlag(TIM17, TIM_FLAG_Update); // 防止一上电进入中断 // 使能定时器3更新中断 TIM_ITConfig(TIM17, TIM_IT_Update, ENABLE); // 配置中断优先级 NVIC_CFG.NVIC_IRQChannel = TIM17_IRQn; NVIC_CFG.NVIC_IRQChannelPriority = 2; NVIC_CFG.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_CFG); TIM_Cmd(TIM17,ENABLE); }
-
-
-
- 由于芯片硬件I2C一直没有配置成功,所以使用的是软件I2C,较为简单,不多赘述。时序不对,若有ACK回复一定是软件问题。
-
- 同I2C一样SPI硬件也没有配置成功,所以使用的是软件SPI,不多赘述。问题大同小异,时序不对,若有ACK回复一定是软件问题
-
-
问题简介
-
本产品串口通信只是用于DEBUG调试,一遍过,配置没出什么问题以下是配置代码:
void USART1_GPIO_Configuration(void){ GPIO_InitTypeDef GPIO_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_GPIOA,ENABLE); GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_1); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; GPIO_Init(GPIOA, &GPIO_InitStructure); } void USART1_Configuration(void){ USART_InitTypeDef USART_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE); 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_Rx | USART_Mode_Tx; USART_Init(USART1, &USART_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); }
-
虽然配置没什么问题但输出有乱码,而且格式不对,输出进行格式封装:
void USART1_SendByte(uint8_t Data){ while((USART1->ISR & USART_FLAG_TXE) == RESET){ } USART1->TDR = (Data & (uint16_t)0x01FF); } void USART1_SendNByte(uint8_t *pData, uint16_t Length){ while(Length--) { USART1_SendByte(*pData); pData++; } } void USART1_Printf(uint8_t *String){ while((*String) != '\0') { USART1_SendByte(*String); String++; } USART1_SendByte(0x0A); } /* 作用 : 用于多路串口重定义 * 传入参数 : baseAddress : 即USART1,要打印的串口地址,UARTx_BASE,x可为0,1,2,3,4,5,6,7 format : 需要打印的东西 ... : 如果是打印字符,输入%c。有符号数字,%d。用法与printf一样 * 返回值 : 无 */ void USART_Std_Printf(const char *format, ...) { char* string; va_list parameter; va_start(parameter, format); while(*format != '\0'){ if(*format == '\\'){ switch(*(++format)){ /*换行*/ case 'n': USART_SendData(USART1, (unsigned char)0x10); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) != SET); break; /*回车*/ case 'r': USART_SendData(USART1, (unsigned char)0x13); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) != SET); break; /*双引号*/ case '\"': USART_SendData(USART1, (unsigned char)0x34); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) != SET); break; /*单引号*/ case '\'': USART_SendData(USART1, (unsigned char)0x39); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) != SET); break; } }else if(*format == '%'){ switch(*(++format)){ /* %号 */ case '%': USART_SendData(USART1, *format); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) != SET); break; /* %s */ case 's': string = va_arg(parameter, char*); while(*string != '\0'){ USART_SendData(USART1, *string); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) != SET); string++; } break; /*打印十进制整数*/ case 'd': string = DecimalToString(va_arg(parameter, int)); while(*string != '\0'){ USART_SendData(USART1, *string); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) != SET); string++; } break; } }else{ USART_SendData(USART1, *format); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) != SET); } format++; } va_end(parameter); } char* DecimalToString(int num){ int i, length, flag=0; char temp[50], reserve[50]; char* s; if(num==0){ temp[0] = '0'; temp[1] = '\0'; s = temp; return s; } if(num<0){ num *= -1; flag = 1; reserve[0] = '-'; temp[0] = '-'; } length = flag?1:0; while(num!=0){ temp[length] = (char)(num%10)+'0'; num /= 10; length++; } if(!flag){ for(i=0;i<length;i++){ reserve[i] = temp[length-i-1]; } }else{ for(i=1;i<length;i++){ reserve[i] = temp[length-i]; } } reserve[i] = '\0'; s = reserve; return s; }
-
-
-
-
延时采用的是SysTick 定时器,从而进行延时操作,以下是配置代码:
volatile uint32_t delay_counter = 0; void delay_ms(uint16_t nms) { SysTick_Config(SystemCoreClock / 1000); // 打开 SysTick 定时器,设定为每毫秒触发一次中断 delay_counter = nms; while (delay_counter) ; } void delay_us(uint32_t nus) { SysTick_Config(SystemCoreClock / 1000000); // 打开 SysTick 定时器,设定为每毫秒触发一次中断 delay_counter = nus; while (delay_counter) ; } void SysTick_Handler(void) { if (delay_counter > 0) { delay_counter--; } }
-
-
- 独立看门狗是为了保证程序的正常运行,在初始化看门狗后,在主函数里进行喂狗,当主函数卡死在某一步时,由于没有及时喂狗,所以会进行软件复位操作。以下是配置代码:
// 初始化独立看门狗 IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); // 允许对独立看门狗寄存器的写操作 IWDG_SetPrescaler(IWDG_Prescaler_256); // 设置预分频器,将输入频率除以256 IWDG_SetReload(1024); // 设置重装载寄存器的值,可根据需要调整 IWDG_ReloadCounter(); // 更新值至重装载寄存器(喂狗) // 启动独立看门狗 IWDG_Enable();
-
定时器是本项目的核心,基本功能都是需要定时器的功能作为基础的。在使用定时器的过程中,会出现各种问题,最基础的问题就是时钟频率的配置问题。定时器的的基础原理就是依靠计数器,当计数达到0或是达到TIM_Period的值就会产生中断信号。问题是计数器的值多长时间减1,这就需要进行计算了。首先时钟树的作用就是为了提供可靠的时钟源。以保证信号的同步。要想进行获得精准的时间计数,仅依靠外部时钟源的48MHZ是不能满足特定需求的,那么TIM_ClockDivision 的作用便是调整计时精度,分频越小每一个Tick上的计时越小,精度越高。TIM_Prescaler表示当主时钟源信号振动了多少次之后,计数器计时加1(减1)。那么1Tick计时时间公式如下(时间单位是s):
1Tick_time = 1/(主频时钟源/TIM_ClockDivision /TIM_Prescaler)
那有了1Tick(指多长时间计数器加1或减1)的时长,就需要配置多少Tick触发中断函数,TIM_Period 的作用就是当计数器CNT的值达到了TIM_Period 就触发一次中断。以下是1s时间一次的中断函数配置方法:
void TIM16_Init(void) { TIM_TimeBaseInitTypeDef TIM_CFG; NVIC_InitTypeDef NVIC_CFG; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM16, ENABLE); TIM_DeInit(TIM16); // 重新将Timer设置为缺省值 TIM_InternalClockConfig(TIM16); // 采用内部时钟给TIM16提供时钟源 TIM_ARRPreloadConfig(TIM16, DISABLE); TIM_CFG.TIM_ClockDivision = 0; // 48M TIM_CFG.TIM_Prescaler = 47999; // 48k 1Tick=1ms TIM_CFG.TIM_CounterMode = TIM_CounterMode_Up; TIM_CFG.TIM_Period = 999; // 1s周期 // TIM_CFG.TIM_RepetitionCounter = 10; TIM_TimeBaseInit(TIM16, &TIM_CFG); TIM_ClearFlag(TIM16, TIM_FLAG_Update); // 防止一上电进入中断 // 使能定时器3更新中断 TIM_ITConfig(TIM16, TIM_IT_Update, ENABLE); // 配置中断优先级 NVIC_CFG.NVIC_IRQChannel = TIM16_IRQn; NVIC_CFG.NVIC_IRQChannelPriority = 2; NVIC_CFG.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_CFG); }
以上是对项目使用芯片功能和协议的总结,后面会更新模块内容总结。
自行转载即可,转载请注明出处。