MM32
RTC可用于周期性从低功耗模式下唤醒MCU,RTC可选三个时钟源:
- 低功耗 32.768kHz 外部低速晶振 (LSE):该时钟源提供了一个低功耗且精确的时间基准。
- 低功耗内部低速振荡器 (LSI):使用该时钟源,节省了一个 32.768kHz 晶振的成本,但是精度没有外部晶振的精度高。
- 外部高速晶振(HSE)128分频:在某些低功耗模式中HSE被关闭会导致RTC无时钟源。
为了用 RTC 闹钟事件将系统从停机模式下唤醒,必须进行如下操作:
- 配置外部中断线 17 为上升沿触发。
- 配置 RTC 使其可产生 RTC 闹钟事件。
如果要从待机模式中唤醒, 不必配置外部中断线 17。
MM32F013x有三种低功耗模式:睡眠模式(Sleep Mode)、停机模式(Stop Mode)和待机模式(Standby Mode),三种低功耗模式的对比如下表所示:
从上表中可以看出,当MCU工作在停机模式时,可以通过任一外部中断事件来唤醒。MM32F013x低功耗模式下的功耗列表如下图所示:
MM32F013x有22个外部中断源,其中EXTI 17对应的是RTC闹钟事件,所以结合RTC闹钟的功能,本文将重点介绍如何在MM32F013x上通过内部RTC模块的闹钟事件来唤醒处于停机模式下的MCU。
01、实现功能
通过内部RTC模块的闹钟事件(对应的是外部中断EXTI 17)来唤醒处于停机模式下的MCU。系统在进入停机模式时拉高GPIO端口,在恢复到正常运行状态时拉低GPIO端口。
02、配置顺序
1)使能PWR和BKP时钟:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
2)使能后备寄存器访问:
PWR_BackupAccessCmd(ENABLE);
3)配置RTC时钟源,使能RTC时钟,如果使用LSE,要打开LSE:
RCC_LSEConfig(RCC_LSE_ON); while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE);
4)设置RTC预分频系数:
RTC_SetPrescaler();
5)开启相关中断:
RTC_ITConfig(RTC_IT_ALR, ENABLE);
6)配置外部中断线:
EXTI_StructInit(&EXTI_InitStructure); EXTI_InitStructure.EXTI_Line = EXTI_Line17; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure);
7)配置中断服务函数:
NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn; NVIC_InitStructure.NVIC_IRQChannelPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);
8)部分操作要等待写操作完成和同步。
03、参考代码
3.1 RTC初始化配置:使用外部32.768kHz的晶振源
void RTC_Configure(void) { uint16_t BKP_Value = 0x5A5A; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; /* Enable PWR Clock */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); /* Enable Access To The RTC & Backup Registers */ PWR_BackupAccessCmd(ENABLE); if(BKP_ReadBackupRegister(BKP_DR1) != BKP_Value) { BKP_DeInit(); /* Enable LSE Clock Source */ RCC_LSEConfig(RCC_LSE_ON); /* Wait LSI Clock Source Ready */ while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); /* Config RTC Clock Source : LSE */ RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); /* Enable RTC Clock */ RCC_RTCCLKCmd(ENABLE); /* Wait For Synchronization */ RTC_WaitForSynchro(); /* Wait Until Last Write Operation On RTC REG Has Finished */ RTC_WaitForLastTask(); /* Set The RTC Prescaler Value */ RTC_SetPrescaler(32767); /* Wait Until Last Write Operation On RTC REG Has Finished */ RTC_WaitForLastTask(); /* Enable RTC Alarm Interrupt */ RTC_ITConfig(RTC_IT_ALR, ENABLE); /* Wait Until Last Write Operation On RTC REG Has Finished */ RTC_WaitForLastTask(); /* Exit From The RTC Configuration Mode */ RTC_ExitConfigMode(); BKP_WriteBackupRegister(BKP_DR1, BKP_Value); /* Wait Until Last Write Operation On RTC REG Has Finished */ RTC_WaitForLastTask(); } else { /* Wait For Synchronization */ RTC_WaitForSynchro(); /* Enable RTC Alarm Interrupt */ RTC_ITConfig(RTC_IT_ALR, ENABLE); /* Wait Until Last Write Operation On RTC REG Has Finished */ RTC_WaitForLastTask(); } /* Clear EXTI Line17 Flag */ EXTI_ClearITPendingBit(EXTI_Line17); /* Configure EXTI Line17(RTC Alarm) To Generate An Interrupt On Rising Edge */ EXTI_StructInit(&EXTI_InitStructure); EXTI_InitStructure.EXTI_Line = EXTI_Line17; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); /* Config RTC NVIC */ NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn; NVIC_InitStructure.NVIC_IRQChannelPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); }
3.2 RTC闹钟中断函数
void RTC_BKP_IRQHandler(void) { if(RTC_GetITStatus(RTC_IT_ALR) != RESET) { GPIO_WriteBit(LED_GPIO, LED_PIN, Bit_RESET); /* Clear EXTI Line17 Flag */ EXTI_ClearITPendingBit(EXTI_Line17); /* Check If The Wake-Up Flag Is Set */ if(PWR_GetFlagStatus(PWR_FLAG_WU) != RESET) { /* Clear Wake Up Flag */ PWR_ClearFlag(PWR_FLAG_WU); } /* Clear Alarm Flag */ RTC_ClearITPendingBit(RTC_IT_ALR); /* Wait Until Last Write Operation On RTC REG Has Finished */ RTC_WaitForLastTask(); } }
3.3 设置RTC闹钟时间,系统进入STOP模式
void RTC_EntryStopMode(void) { /* Wait till RTC Second event occurs */ RTC_ClearFlag(RTC_FLAG_SEC); while(RTC_GetFlagStatus(RTC_FLAG_SEC) == RESET); /* Set The RTC Alarm Value */ RTC_SetAlarm(RTC_GetCounter() + 3); /* Wait Until Last Write Operation On RTC REG Has Finished */ RTC_WaitForLastTask(); GPIO_WriteBit(LED_GPIO, LED_PIN, Bit_SET); /* Enter Stop Mode */ PWR_EnterSTOPMode(PWR_Regulator_ON, PWR_STOPEntry_WFI); }
04、运行结果
编译软件工程无误后下载代码,调用RTC_EntryStopMode()函数使系统进入到停机模式,等待RTC闹钟事件3S后唤醒停机模式下的MCU,唤醒后继续执行程序,通过观察GPIO的电平状态来查看运行结果:
用户可以结合上一篇万年历功能设置在某年某月某时某刻定时唤醒MCU功能,万年历的具体实方式可以参考上一篇《MM32F013x——万年历》。
转自:灵动微电子
MM32F013x内部的RTC是一个独立的定时器单元,它拥有一组连续计数的计数器,配置相应的寄存器参数,可以实现闹钟、秒中断、毫秒中断、MCU定时唤醒、万年历等功能。
主要特征
① 可编程的预分频系数:分频系数最高为 220
② 32 位的可编程计数器,用于较长时间段的测量
③ 2 个分离的时钟:用于 APB1 接口的 PCLK1 和 RTC 时钟 (RTC 时钟的频率必须小于PCLK1 时钟频率的四分之一以上)
④ 可以选择以下三种 RTC 的时钟源
– HSE 时钟除以 128
– LSE 振荡器时钟
– LSI 振荡器时钟
⑤ 2 个独立的复位类型
– APB1 接口由系统复位
– RTC 核心 (预分频器、闹钟、计数器和分频器) 只能由后备域复位
⑥ 3 个专门的屏蔽中断
– 闹钟中断,用来产生一个软件可编程的闹钟中断
– 秒 / 毫秒中断,用来产生一个可编程的周期性中断信号 (最长可达 1 秒)
– 溢出中断,指示内部可编程计数器溢出并返回为 0 的状态
本文将重点介绍如何在MM32F013x上通过内部RTC模块实现万年历的功能。
实现功能
通过修改RTC计数器的初始值来设置系统当前的时间和日期,使能RTC秒中断功能;在RTC产生秒中断后,通过获取当前RTC的计数值,将其转换为对应的年月日信息,再通过蔡勒公式计算出星期,将最终的结果通过串口的形式输出显示。
RTC模块的电源域处在VDD数字电源域,只要MCU供电就可以使用RTC,没有独立的VBAT供电引脚,所以无法使用纽扣电池类的应用。
参考代码
01、结构体定义及全局变量
typedef struct { uint16_t year; uint8_t month; uint8_t day; uint8_t week; uint8_t hour; uint8_t minute; uint8_t second; } CALENDAR_t; const uint8_t RTC_DayOfMonth[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 }; CALENDAR_t RTC_Calendar;
02、RTC初始化配置:使用外部32.768kHz的晶振源
void RTC_Configure(void) { uint16_t BKP_Value = 0x5A5A; NVIC_InitTypeDef NVIC_InitStructure; /* Enable PWR Clock */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); /* Enable WKUP pin */ PWR_WakeUpPinCmd(ENABLE); /* Enable Access To The RTC & Backup Registers */ PWR_BackupAccessCmd(ENABLE); if(BKP_ReadBackupRegister(BKP_DR1) != BKP_Value) { BKP_DeInit(); /* Enable LSE Clock Source */ RCC_LSEConfig(RCC_LSE_ON); /* Wait LSI Clock Source Ready */ while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); /* Config RTC Clock Source : LSE */ RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); /* Enable RTC Clock */ RCC_RTCCLKCmd(ENABLE); /* Wait For Synchronization */ RTC_WaitForSynchro(); /* Wait Until Last Write Operation On RTC REG Has Finished */ RTC_WaitForLastTask(); /* Set The RTC Prescaler Value */ RTC_SetPrescaler(32767); /* Wait Until Last Write Operation On RTC REG Has Finished */ RTC_WaitForLastTask(); /* Enable RTC Second Interrupt */ RTC_ITConfig(RTC_IT_SEC, ENABLE); /* Wait Until Last Write Operation On RTC REG Has Finished */ RTC_WaitForLastTask(); /* Exit From The RTC Configuration Mode */ RTC_ExitConfigMode(); BKP_WriteBackupRegister(BKP_DR1, BKP_Value); /* Wait Until Last Write Operation On RTC REG Has Finished */ RTC_WaitForLastTask(); /* Set initial time */ RTC_SetDateTime(2021, 1, 12, 14, 48, 0); } else { /* Wait For Synchronization */ RTC_WaitForSynchro(); /* Enable RTC Second Interrupt */ RTC_ITConfig(RTC_IT_SEC, ENABLE); /* Wait Until Last Write Operation On RTC REG Has Finished */ RTC_WaitForLastTask(); } /* Config RTC NVIC */ NVIC_InitStructure.NVIC_IRQChannel = RTC_IRQn; NVIC_InitStructure.NVIC_IRQChannelPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); }
03、RTC秒中断函数
void RTC_BKP_IRQHandler(void) { if(RTC_GetITStatus(RTC_IT_SEC) != RESET) { /* Update Date and Time */ RTC_UpdateCalendar(); /* Print current Date and Time */ RTC_PrintDateTime(); /* Clear Alarm Flag */ RTC_ClearITPendingBit(RTC_IT_SEC); /* Wait Until Last Write Operation On RTC REG Has Finished */ RTC_WaitForLastTask(); } }
04、将RTC计数值转换为日期信息
void RTC_UpdateCalendar(void) { static uint32_t PreTotalDay = 0; uint32_t TotalSecond = 0; uint32_t TotalDay = 0; uint16_t Year = 1970; uint8_t Month = 0; /* Get The RTC Counter Value */ TotalSecond = RTC_GetCounter(); TotalDay = TotalSecond / 86400; if(PreTotalDay != TotalDay) { PreTotalDay = TotalDay; while(TotalDay >= 365) { if(RTC_LeapYear(Year) == 1) { if(TotalDay >= 366) { TotalDay -= 366; } else { break; } } else { TotalDay -= 365; } Year++; } RTC_Calendar.year = Year; while(TotalDay >= 28) { if((Month == 1) && (RTC_LeapYear(RTC_Calendar.year) == 1)) { if(TotalDay >= 29) { TotalDay -= 29; } else { break; } } else { if(TotalDay >= RTC_DayOfMonth[Month]) { TotalDay -= RTC_DayOfMonth[Month]; } else { break; } } Month++; } RTC_Calendar.month = Month + 1; RTC_Calendar.day = TotalDay + 1; RTC_Calendar.week = RTC_GetWeek(RTC_Calendar.year, RTC_Calendar.month, RTC_Calendar.day); } RTC_Calendar.hour = (TotalSecond % 86400) / 3600; RTC_Calendar.minute = ((TotalSecond % 86400) % 3600) / 60; RTC_Calendar.second = ((TotalSecond % 86400) % 3600) % 60; }
05、将日期信息转换为RTC计数值
void RTC_SetDateTime(uint16_t Year, uint8_t Month, uint8_t Day, uint8_t Hour, uint8_t Min, uint8_t Sec) { uint32_t TotalSecond = 0; uint16_t y = 0; uint8_t m = 0; if((Year >= 1970) && (Year <= 2099)) { for(y = 1970; y < Year; y++) { if(RTC_LeapYear(y) == 1) { TotalSecond += 31622400; /* Total Seconds Of Leap Year */ } else { TotalSecond += 31536000; /* Total Seconds Of Normal Year */ } } for(m = 0; m < (Month - 1); m++) { TotalSecond += RTC_DayOfMonth[m] * 86400; /*Total Seconds Of Month */ if((RTC_LeapYear(Year) == 1) && (m == 1)) { TotalSecond += 86400; } } TotalSecond += (uint32_t)(Day - 1) * 86400; /* Total Seconds Of Day */ TotalSecond += (uint32_t)Hour * 3600; /* Total Seconds Of Hour */ TotalSecond += (uint32_t)Min * 60; /* Total Seconds Of Minute */ TotalSecond += Sec; /* Enable PWR Clock */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); /* Enable Access To The RTC & Backup Registers */ PWR_BackupAccessCmd(ENABLE); /* Set The RTC Counter Value */ RTC_SetCounter(TotalSecond); /* Wait Until Last Write Operation On RTC REG Has Finished */ RTC_WaitForLastTask(); RTC_UpdateCalendar(); } else { printf("\r\nError Date & Time!!!\r\n"); } }
06、RTC信息打印
void RTC_PrintDateTime(void) { printf("\r\n%04d-%02d-%02d", RTC_Calendar.year, RTC_Calendar.month, RTC_Calendar.day); switch(RTC_Calendar.week) { case 0 : printf(" SUN "); break; case 1 : printf(" MON "); break; case 2 : printf(" TUE "); break; case 3 : printf(" WED "); break; case 4 : printf(" THU "); break; case 5 : printf(" FRI "); break; case 6 : printf(" SAT "); break; default: break; } printf("%02d:%02d:%02d\r\n", RTC_Calendar.hour, RTC_Calendar.minute, RTC_Calendar.second); }
07、RTC功能函数:判断闰年、蔡勒公式计算星期
uint8_t RTC_LeapYear(uint16_t Year) { if( (((Year % 400) == 0) ) || /* Century Leap Year */ (((Year % 100) != 0) && ((Year % 4) == 0)) /* Normal Leay Year */ ) { return 1; } else { return 0; } } uint8_t RTC_GetWeek(uint16_t Year, uint8_t Month, uint8_t Day) { int w, c, y; /* Month 1 Or 2 of This Year Must Be As Last Month 13 Or 14 */ if((Month == 1) || (Month == 2)) { Month += 12; Year -= 1; } w = 0; /* Weekday */ c = Year / 100; /* Century */ y = Year % 100; /* Year */ w = y + (y / 4) + (c / 4) - (2 * c) + (26 * (Month + 1) / 10) + Day - 1; while(w < 0) w += 7; w %= 7; return w; }
运行结果
编译软件工程无误后下载代码,在串口终端工具中我们可以看到每间隔1秒钟,RTC产生一次中断,在中断中我们将当前的日期信息通过串口打印在显示终端软件上:
转自:灵动微电子
相信很多做变频设计或者电机控制领域的朋友们都熟悉一项重要技术——使用单电阻电流重构技术采样相电流,实现FOC控制。那采样相电流时就涉及到低调制区域,采样时间大于PWM分段矢量作用时间,此时就需要使用移相技术,就是在中央对齐互补模式下实现非对称PWM输出。
电机控制单电阻采样机制是在一个 PWM 波形内采集两相电流 ADC 数据,但某些扇区边界条件下只能获得一路电流 ADC 数据, 需要对 PWM 波形进行变形用于构造电流采样区域。
本文将重点介绍如何在MM32F013x上实现TIM1的硬件移相功能。
实现方式
目前MM32 MCU实现PWM 移相功能有多种实现方式,其中一种实现方式:设置TIM1和TIMx主从模式,开启TIMx的CCx中断,且开启对应的DMA请求通道去完成TIM1 CCRx值的改变,此外只需要在TIMx_CCx中断服务函数中改变TIM1 对应通道的CCR值即可在软件中实现动态修改移相角度功能,同样也就实现了PWM占空比在每半个周期内交替变化。
MM32F013x系列MCU中新增 TIM1的硬件移相功能,新增了PDER(通道x输出 PWM 移相使能位) 和 CCRxFALL(通道x在 PWM 中央对齐模式向下计数时的捕获/比较值)寄存器,允许 TIM1的5 个通道在硬件上完成输出 PWM 移相操作。开启 PDER 寄存器的 PWM移相使能,根据需要移动相位,配置 CCRxFALL 以及 CCRx,即可实现 PWM 输出可编程的移相波形,可左移或是右移。
相关寄存器
除了之前熟悉的TIM1 PWM输出相关的寄存器外,主要还需要关注以下新增寄存器。
定时器1的中央对齐模式
脉冲宽度调制模式可以产生一个由 TIMx_ARR 寄存器确定频率、由 TIMx_CCRx 寄存器确定占空比的信号。
当 TIMx_CR1 寄存器中的 CMS 位不为‘00’时为中央对齐模式 (所有其它的配置对 OCxREF/OCx 信号都有相同的作用)。根据不同的 CMS 位的设置,比较标志可以在计数器向上计数时被置 1、在计数器向下计数时被置 1、或在计数器向上和向下计数时被置‘1’。TIMx_CR1寄存器中的计数方向位 (DIR) 由硬件更新,不要用软件修改它。
中央对齐模式的图例可用下图来表示:
中央对齐有3种不同模式,可以通过软件设置TIM1_CR寄存器的CMS位来选择对应模式。3种模式中计数器的计数方式都是一样的,计数器从 0 开始计数到自动加载的值 (TIM1_ARR - 1),产生一个计数器溢出事件,然后向下计数到 1 并且产生一个计数器下溢事件,之后再循环从 0 开始重新计数。3种模式的不同之处在于,输出比较中断标志位被设置的时刻点均不一致,根据实际需求来进行选择。
使用中央对齐模式还有以下几点需要注意:
进入中央对齐模式时,使用当前的上/下计数配置;这就意味着计数器向上还是向下计数取决于 TIMx_CR1 寄存器中 DIR 位的当前值。此外,软件不能同时修改 DIR 和 CMS位。
不推荐当运行在中央对齐模式时改写计数器,因为会产生不可预知的结果。特别地:
– 如果写入计数器的值大于自动重加载的值 (TIMx_CNT > TIMx_ARR),则方向不会被更新
– 例如,如果计数器正在向上计数,它就会继续向上计数
– 如果将 0 或者 TIMx_ARR 的值写入计数器,方向被更新,但不产生更新事件UEV
使用中央对齐模式最保险的方法,就是在启动计数器之前产生一个软件更新 (设置 TIMx_EGR位中的 UG 位)。
软件实现步骤
了解完应用场景、实现原理以及涉及到的寄存器,下面介绍配置代码。
01、主程序初始化
以上为整个软件工程的入口主函数,默认主频为内部时钟HSI倍频到72M。初始化好了TIM1 前3个PWM输出通道所用引脚以及TIM1,并且开启TIM1一直输出PWM波形。
extern u32 SystemCoreClock; s32 main(void) { /* TIM1 PWM GPIO AF initial */ TIM1_GPIO_Init(); TIM1_PWM_Init(500 - 1, SystemCoreClock / 1000000 - 1); DELAY_Ms(100); TIM1_PWM_Shift_Test() ; }
02、TIM1初始化
void TIM1_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBENR_GPIOA, ENABLE); GPIO_PinAFConfig(GPIOA, GPIO_PinSource8, GPIO_AF_2); GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_2); GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_2); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7 ; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_SetBits(GPIOA,GPIO_Pin_7) ; }
以上代码完成了TIM1前3个PWM输出通道所用引脚的复用功能配置,对应的复用功能号需要根据芯片数据手册中的引脚功能定义表来获取到。
void TIM1_PWM_Shift_Init(u16 arr, u16 psc) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2ENR_TIM1, ENABLE); TIM_DeInit(TIM1) ; TIM_TimeBaseStructInit(&TIM_TimeBaseStructure); TIM_TimeBaseStructure.TIM_Period = arr; TIM_TimeBaseStructure.TIM_Prescaler = psc; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_RepetitionCounter = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_CenterAligned1; TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); TIM_OCStructInit(&TIM_OCInitStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStructure.TIM_Pulse = 250 ; TIM_OC1Init(TIM1, &TIM_OCInitStructure); TIM_OCInitStructure.TIM_Pulse = 250 ; TIM_OC2Init(TIM1, &TIM_OCInitStructure); TIM_OCInitStructure.TIM_Pulse = 250 ; TIM_OC3Init(TIM1, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_SelectOutputTrigger(TIM1,TIM_TRGOSource_Update); TIM_Cmd(TIM1, ENABLE); TIM_CtrlPWMOutputs(TIM1, ENABLE); }
以上配置接口程序为时基相关参数,调用时候只需要根据实际应用情况传入不同的参数即可改变PWM输出的频率。
计数模式设置为中央对齐模式1,输出比较模式设置为PWM1输出模式,输出有效极性设置为高电平。当计时器值小于比较器设定值时则对应通道输出有效高电平,当计时器值大于或等于比较器设定值时则对应通道输出无效低电平。
void TIM1_PWM_Shift_Test(void) { GPIO_ResetBits(GPIOA,GPIO_Pin_7) ; TIM_SetCCR2FALL(TIM1, 200); TIM_SetCCR3FALL(TIM1, 300); TIM_PWMShiftConfig(TIM1, TIM_PDER_CCR2SHIFTEN|TIM_PDER_CCR3SHIFTEN, ENABLE); }
TIM_PWMShiftConfig库函数实现开启或关闭对应通道输出PWM移相功能。TIM_SetCCRxFALL库函数实现设置对应通道在PWM中央对齐模式向下计数时的捕获/比较值。开启 PDER 寄存器的PWM移相使能后,根据需要移动相位,配置 CCRxFALL 以及 CCRx,即可实现PWM 输出可编程的移相波形,即可以左移亦或是右移。
测试验证
本次微课堂实验是基于eMiniboard MB-025硬件资源完成的,前述代码中,通过设置TIM1的时基为1KHz,且不开启OC1通道的移相功能,只使能OC2和OC3通道的允许输出PWM移相使能位,通过逻辑分析仪抓取TIM1 前3个PWM输出通道的波形,稳定的波形结果如下图所示:
粉红色表示通道1波形,深蓝色表示通道2波形,浅绿色表示通道3波形,黄色表示开始移相时刻点。图中看出,通道2较通道1往右移了18度的相位(最大只能移动180度,这里占用时长为500us,右移了50us为18度),通道2较通道1往左移了18度的相位(左移了50us为18度)。根据上述波形结果展示,TIM1 的硬件PWM移相功能可以配置相关寄存器的配置即可实现,操作简单。
转自:灵动微电子
在前面的章节中我们介绍了MM32 USB各种功能类型,也介绍了如何通过串口或者J-Link RTT方式实现shell辅助调试方式,但是其都需要依赖额外的工具,比如串口方式就需要USB转TTL,J-Link RTT需要使用J-Link下载器,所以希望有新的方法实现shell,本次我们介绍USB CDC的方式来实现shell功能。
本次我们采用MM32L373 MiniBoard作为测试开发板,验证USB CDC的方式来实现shell功能。
前面已经为大家讲解了shell的串口方法,其实原理一样,只是用MM32 USB枚举成串口设备替代USB转TTL,直接从USB获取数据到MCU,也不需要额外占用MCU的串口,节省资源和硬件,相关的代码都可以从之前的文章获取,本次只是融合两者,改变实现接口,具体代码参考如下:
对于CDC部分,其函数初始化配置及相关全局变量定义内容,代码如下:
#define USBD_POWER 0
#define USBD_MAX_PACKET0 64
#define USBD_DEVDESC_IDVENDOR 0x2F81 //0x0D28
#define USBD_DEVDESC_IDPRODUCT 0x0001 //0x0204
以上是定义的MM32 MCU CDC设备VID和PID,灵动微电子已经获得USB组织授权的VID和PID。当设备插入电脑上,可以查看到如上标识的CDC设备,如图1所示:
对于MM32 MCU的CDC功能来说,
在使用CDC功能之前先调用USB初始化函数来初始化USB协议栈。
int main(void)
{
// USB Device Initialization and connect
usbd_init();
usbd_connect(__TRUE);
while (!usbd_configured()) // Wait for USB Device to configure
{
}
while (1)
{
}
}
对于shell部分其函数初始化配置及相关全局变量定义内容,代码如下:
typedef struct
{
char *command; // shell命令提示符
char buffer[SHELL_COMMAND_MAX_LENGTH]; // shell命令缓冲buffer
unsigned short length; // shell命令长度大小
unsigned short cursor; // shell光标位置偏移
char *param[SHELL_PARAMETER_MAX_NUMBER]; // shell参数变量
char history[SHELL_HISTORY_MAX_NUMBER][SHELL_COMMAND_MAX_LENGTH]; // 历史记录区域
unsigned short historyCount; // 历史记录数量
short historyFlag; // 当前记录偏移位置
short historyOffset; // 历史记录偏移大小
SHELL_CommandTypeDef *commandBase; // 命令表基地址
unsigned short commandNumber; // 命令数量
int keyFuncBase; // 按键响应表基地址
unsigned short keyFuncNumber; // 按键响应数量
SHELL_InputMode status; // shell输入状态
unsigned char isActive; //是不是当前激活的shell
shellRead read; // shell读函数接口
shellWrite write; // shell写函数接口
}SHELL_TypeDef;
如上所示,为对象的定义接口,移植的步骤先定义一个shell对象,即:SHELL_TypeDef cdc_shell,然后实例化对象的操作接口,具体说明看注释,对于其中我们需要关注的是shell的读写接口。由于本次我们使用USB CDC接收和发送数据,所以我们只需要在USB CDC的函数中处理接收到的数据即可,我们使用shellHandler(&cdc_shell, EP2RXBuff[i]);来处理数据的交互,具体函数代码参考串口shell代码。
shell的发送接口,只需要把数据拷贝到buffer即可。
shell的读写接口移植到CDC上,代码如下:
void USBD_CDC_TASK(void)
{
uint8_t i, count;
NotifyOnStatusChange();
if (CDC_UART ->ISR & 0x08)
{
CDC_UART ->GCR &= ~(3 << 3);
CDC_UART ->GCR = 3 << 3;
UART_ClearITPendingBit(CDC_UART, UART_OVER_ERR);
}
// USB -> UART
if (EP2ReceiveFlag == 1)
{
EP2ReceiveFlag = 0;
for (i = 0; i < RxBufLen; i++)
shellHandler(&cdc_shell, EP2RXBuff[i]);
}
// UART -> USB
if (EP2TransferFlag == 1)
{
if (TxBufLen > 0)
{
while (USB->rEP2_CTRL & 0x80);
if (TxBufLen > 64)
{
UART_ReadData(EP2TXBuff, 64);
count = 64;
TxBufLen -= 64;
}
else
{
UART_ReadData(EP2TXBuff, TxBufLen);
count = TxBufLen;
TxBufLen = 0;
}
usb_buf_busy_flag = 1;
for (i = 0; i < count; i++)
{
USB->rEP2_FIFO = *(EP2TXBuff + i);
}
if ((USB ->rEP2_AVIL & 0x3f) == count)
{
USB->rEP2_CTRL = 0x80 | count;
}
else
{
USB->rTOP |= 1 << 3;
USB->rTOP &= ~(1 << 3);
}
USB->rEP2_CTRL = 0x80 | count;
if (0 == TxBufLen)
EP2TransferFlag = 0;
}
}
}
如上,我们就完成通过MM32 MCU的CDC实现shell调试功能,用串口助手打开虚拟串口,用CDC shell测试发送数据,结果如下:
以上就是MM32 MCU USB的CDC shell功能。
本文转自:灵动MM32MCU
前面我们介绍了新出USB设备类型WebUSB,其中使用MM32 MCU实现WebUSB功能。既然可以通过网页与USB设备通信,那是否可以做别的功能,比如USB-DFU,当然是可以的,我们通过网页进行DFU功能,即WebDFU功能。因此我们本节我们讲解如何在MM32 MCU实现WebDFU功能。
DFU是使用USB作为微控制器和编程工具之间的通信信道,通常是PC。在DFU类规格书说明中指出所有的DFU命令、状态和数据交换都需要通过端点0进行。命令集和基本协议都定义好的,但是上层协议(数据格式,错误信息等)是客户相关的。也就是说DFU类并没有定义数据传输格式(s19,16进制,纯2进制等等)
由于一个设备同时进行DFU操作和正常运行功能活动是不现实的,因此在DFU操作期间必须停止正常运行活动,这就意味着设备必须改变运行模式——也就是说我们在进行固件更新时比如打印机不再是打印机了,它是一个flash存储器编程器。但是支持DFU的设备不能自主改变模式,这需要接受外部(人或者主机操作系统)的干预。
对于DFU功能,其完成实现固件升级可以分为4个不同阶段。
01、枚举
设备把自身的一些特性告知主机,嵌入在设备正常运行描述符中的一个DFU类接口描述符和相关的函数符能够完成这个目的,并且能够为通过控制管道的类专用的请求提供目标。
02、DFU枚举
主机和设备同意开始固件升级,主机向设备发出USB复位,设备发出第二个描述符集合,并且为传输阶段做准备,这会是相应设备的运行时驱动无效,并使得DFU驱动不受其他目标为该设备通信妨碍,重编程设备的固件。
03、传输
主机将固件映像传输给设备,功能描述符中的参数用于确保非易失性存储器编程的块大小和时序的正确性。状态请求用于保持主机和设备之间的同步。
04、显示
一旦设备向主机报告重新编程完成,主机箱设备则发送usb复位,设备重枚举并执行升级后的固件。为了保证只有DFU驱动加载,有必要的在枚举DFU描述符集合改变id-product字段。
本节我们来讲解如何在MM32 MCU实现WebDFU设备功能,对于MM32 MCU来说,实现WebDFU只需要在之前程序基础上修改添加部分代码即可,按照开源的WebDFU协议加入功能。
本次我们采用MM32L373 miniboard作为测试开发板。为了方便大家使用MM32 MCU的WebDFU设备功能,我们重新封装好全部代码,用户不需要自己配置那些麻烦的描述符等参数,只需要知道用之前的单一设备函数即可。
软件资源如下:
对于MM32 MCU的WebDFU,我们可以配置WebDFU的参数。
#define USBD_DFU_DNLOAD_ENABLE 1 #define USBD_DFU_UPLOAD_ENABLE 0 #define USBD_DFU_STRDESC L"USB_DFU" #define USBD_DFU_XFERBUF_SIZE 1024 #define USBD_WEBUSB_VENDOR_CODE 0x21 #define USBD_WEBUSB_BASE_LANDING_URL "devanlai.github.io/webdfu/dfu-util/?vid=" #define USBD_WEBUSB_LANDING_URL CONCAT_MACRO_TO_STRING(USBD_WEBUSB_BASE_LANDING_URL, USBD_DEVDESC_IDVENDOR) #define USBD_WEBUSB_ORIGIN_URL "devanlai.github.io/" #define USBD_WEBUSB_IF_NUM USBD_DFU_IF_NUM
参数设置如上。当进行DFU升级时候,可以看到电脑上显示的设备名称为USB_DFU,就是配置的USBD_DFU_STRDESC参数。
在使用MM32 WebDFU功能之前先调用USB初始化函数来初始化USB协议栈。
int main(void) { // USB Device Initialization and connect usbd_init(); usbd_connect(__TRUE); while (!usbd_configured()) // Wait for USB Device to configure { } while (1) { …… } }
然后依然和之前一样只是在WebUSB基础上修改添加WebDFU相关参数函数接口即可,代码如下:
//DFU初始化 void usbd_dfu_init(void) { DFU_Reset(); current_write_addr = 0; } //USB DFU开始升级 BOOL USBD_DFU_StartUpgrade(void) { error_t err = flash_manager_init(target_device); current_write_addr = target_device.flash_start; switch (err) { case ERROR_SUCCESS: initialized = true; break; case ERROR_RESET: case ERROR_ALGO_DL: case ERROR_ALGO_DATA_SEQ: case ERROR_INIT: case ERROR_SECURITY_BITS: case ERROR_UNLOCK: DFU_SetStatus(DFU_STATUS_ERR_PROG); break; case ERROR_ERASE_SECTOR: case ERROR_ERASE_ALL: DFU_SetStatus(DFU_STATUS_ERR_ERASE); break; case ERROR_WRITE: DFU_SetStatus(DFU_STATUS_ERR_WRITE); break; case ERROR_FAILURE: case ERROR_INTERNAL: default: DFU_SetStatus(DFU_STATUS_ERR_UNKNOWN); break; } return (err == ERROR_SUCCESS) ? (__TRUE) : (__FALSE); } //复位目标 static bool reset_target(bool error_condition) { current_write_addr = 0; if (initialized) { error_t err = flash_manager_uninit(); switch (err) { case ERROR_SUCCESS: if (config_get_auto_rst()) { // Target is reset and run by the uninit } else if (!error_condition) { // Reset and run the target at the end of a successful upgrade target_set_state(RESET_RUN); } break; case ERROR_RESET: case ERROR_ALGO_DL: case ERROR_ALGO_DATA_SEQ: case ERROR_INIT: case ERROR_SECURITY_BITS: case ERROR_UNLOCK: DFU_SetStatus(DFU_STATUS_ERR_PROG); break; case ERROR_ERASE_SECTOR: case ERROR_ERASE_ALL: DFU_SetStatus(DFU_STATUS_ERR_ERASE); break; case ERROR_WRITE: DFU_SetStatus(DFU_STATUS_ERR_WRITE); break; case ERROR_FAILURE: case ERROR_INTERNAL: default: DFU_SetStatus(DFU_STATUS_ERR_UNKNOWN); break; } initialized = false; return (err == ERROR_SUCCESS); } return true; } //USB DFU结束升级 BOOL USBD_DFU_FinishUpgrade(void) { return reset_target(false) ? (__TRUE) : (__FALSE); } //USB DFU写数据 BOOL USBD_DFU_WriteBlock(const U8 *buffer, U16 blockSize) { error_t err = flash_manager_data(current_write_addr, (U8*)buffer, blockSize); switch (err) { case ERROR_SUCCESS: current_write_addr += blockSize; break; case ERROR_RESET: case ERROR_ALGO_DL: case ERROR_ALGO_DATA_SEQ: case ERROR_INIT: case ERROR_SECURITY_BITS: case ERROR_UNLOCK: DFU_SetStatus(DFU_STATUS_ERR_PROG); break; case ERROR_ERASE_SECTOR: case ERROR_ERASE_ALL: DFU_SetStatus(DFU_STATUS_ERR_ERASE); break; case ERROR_WRITE: DFU_SetStatus(DFU_STATUS_ERR_WRITE); break; case ERROR_FAILURE: case ERROR_INTERNAL: default: DFU_SetStatus(DFU_STATUS_ERR_UNKNOWN); break; } return (err == ERROR_SUCCESS); }
这样我们就完成MM32 MCU的WebDFU功能,将程序下载到板子中,USB插上电脑,电脑上会枚举出USB DFU。在USB DFU枚举成功后,我们需要检查是否真的可以被WebDFU网页识别。
打开https://devanlai.github.io/webdfu/dfu-util/通过该网页检测WebDFU工作状态,网页如下图所示:
通过点击Connect就可以像之前WebUSB设备一样弹出窗口选择我们的USB DFU设备,然后就可以在Vendor ID(hex)窗口看到序列号。可以通过网页上的Detach DFU和Download以及Upload进行DFU升级动作。
以上就是MM32 MCU USB的WebDFU功能,具体的WebDFU协议过程想详细了解看dapboot,大家可以自由发挥修改底层和上层的代码实现自己的WebDFU网页端和设备端。
本文转自:灵动MM32MCU
转眼间来到了2020年,新年伊始,小编将和大家一起学习使用MM32 MCU的USB功能。对于USB来说,主要应用是HID、CDC、MSC以及WINUSB等功能,此讲先介绍如何使用MM32 MCU的HID功能。
对于USB设备来说,其中有一大类就是HID设备,即Human Interface Devices,人机接口设备。这类设备包括鼠标、键盘等,其主要用于人与计算机进行交互。它是USB协议最早支持的一种设备类。HID设备可以作为低速、全速、高速设备用。由于HID设备要求用户输入能得到及时响应,所以其传输方式通常采用中断方式,而且无需安装驱动就能进行交互,简单方便。
在USB通信协议中,HID设备的定义放置在接口描述符中,USB的设备描述符和配置描述符中不包含HID设备的信息。所以对于某些特定的HID设备,我们可以定义多个接口,只要其中一个接口为HID设备类即可,在学习HID之前,先来复习一下USB协议的相关内容。
一、USB设备描述符-概述
当插入USB设备后,主机需要发送比较短的请求来确认设备的身份、类型、速度等信息,这个过程称之为枚举。
那什么是设备描述符呢?Descriptor即描述符,是一个完整的数据结构,可以通过C语言等编程实现,并存储在USB设备中,用于描述一个USB设备的所有属性,USB主机是通过一系列命令来要求设备发送这些信息的。
描述符的作用就是通过命令操作作来给主机传递信息,从而让主机知道设备具有什么功能、属于哪一类设备、要占用多少带宽、使用哪类传输方式及数据量的大小,只有主机确定了这些信息之后,设备才能真正开始工作。
USB有那些标准描述符呢?对于 USB来说有5种标准描述符:设备描述符、配置描述符、字符描述符、接口描述符、端点描述符 。
描述符之间有一定的关系,一个设备只有一个设备描述符,而一个设备描述符可以包含多个配置描述符,而一个配置描述符可以包含多个接口描述符,一个接口使用了几个端点,就有几个端点描述符。由此我们可以看出来,USB的描述符之间的关系是一层一层的,最上一层是设备描述符,下面是配置描述符,再下面是接口描述符,然后是端点描述符。在获取描述符时,先获取设备描述符,然后再获取配置描述符,根据配置描述符中的配置集合长度,一次将配置描述符、接口描述符、端点描述符一起一次读回。其中可能还会有获取设备序列号,厂商字符串,产品字符串等。
枚举的过程:
1、等待稳定:主机通过电平差检测到设备,等待100ms让设备电平趋于稳定;
2、首次复位:HUB发起复位,让设备进入初始的地址0模式;
3、首次查询设备描述符:GET_DESCRIPTOR 主机查询设备描述符,只要前8字节 ==> 80 06 01 00 00 00 12 00 ;
4、二次复位:在接收到设备描述符前8个字节后,再次重启设备;
5、设置地址:SET_ADDRESS 主机下发设置地址命令,设备获取新地址 ==> 00 05 01 00 00 00 00 00 ;
6、二次查询设备描述符:GET_DEVICE_DESCRPTOR获取整个18字节的设备描述符 ==> 80 06 01 00 00 00 12 00 ;
7、获取配置描述符:GET_CONFIGURATION 获取9字节配置描述符 ==> 80 06 02 00 00 00 09 00 ;
8、完成配置:SET_CONFIGURATION;
二、HID设备简述
2.1 HID设备的特点
交换的数据储存在称为报表(Report)的结构内,设备的固件必须支持HlD报表的格式。主机通过控制和中断传输中的传送和请求报表来传送和接收数据。报表的格式非常灵活。
每一笔事务可以携带小量或中量的数据。低速设备每一笔事务最大是8B ,全速设备每一笔事务最大是64B,高速设备每一笔事务最大是1024B,一个报表可以使用多笔事务。
设备可以在未预期的时间传送信息给主机,例如键盘的按键或是鼠标的移动。所以主机会定时轮询设备,以取得最新的数据。
HID 设备的最大传输速度有限制。主机可以保证低速的中断端点每10ms 内最多 1笔事务,每一秒最多是 800B,保证全速端点每1ms 一笔事务,每一秒最多是64000B,保证高速端点每125 us 三笔事务,每一秒最多是 24.576MB。
HID 设备没有保证的传输速率。如果设备是设置在 10ms 的时距,事务之间的时间可能等于或小于10ms。除非设备是设置在全速时在每个帧传输数据,或是在高速时在每个微帧传输数据。这个是最快的轮询速率,所以端点可以保证有正确的带宽可供使用。
HID 设备除了传送数据给主机外,它也会从主机接收数据。只要能够符合HlD 类别规范的设备都可以是HID 设备。设备除了HlD 接口之外,它可能同时还包含有其他的USB 接口。
2.2 HID设备的硬件要求
HID 接口必须要符合 Device Class Definition for Human interface Devices 规范内所定义的 HID 类别的需求。在此文件内描述了所需的描述符、传输的频率以及传输的类型等。为了符合规范,HID 接口的端点与描述符都必须符合数个要求。所有的 HID 传输都是使用默认控制管道或是一个中断管道,HID设备必须有一个中断输入端点来传送数据到主机,中断输出端点则不是必需的。Control管道用于接收和响应USB控制和类数据的请求,在由HID类驱动程序轮询时传输数据(使用Get_Reportrequest),从主机接收数据。
对于主机与设备之间所交换的数据,可以分成两种类型:低延迟的数据,必须尽快地到达目的;配置或其他的数据,没有严格时间限制的需求。中断管道是控制管道之外的另一种数据交换的方式,特别适合使用在接收端需要定时或是尽可能及时收到数据的时候。中断输入管道携带数据到主机,中断输出管道则是携带数据到设备。在总线忙的时候,控制管道可能会被延迟,而中断管道保证会有可得到的带宽。HID不需要一定有中断输出管道。如果没有中断输出管道,主机会在控制管道上使用HID 设备特有的 Set_Report 请求来传送所有的报表。
2.3 HID的程序要求
主机的驱动程序要与 HID 设备通信,其设备的固件必须符合如下几个需求,设备的描述符必须识别该设备包含有 HID 接口(描述符)。除了默认控制管道外,固件必须另外支持一个中断输入管道。固件必须包含一个报表描述符来定义要传送与接收的设备数据。如果要传送数据,固件必须支持 Get_Report 控制传输与中断输入传输。如果要接收数据,固件必须支持 Set_Report 控制传输与选择性的中断输出传输。所有的 HID 数据都必须使用定义过的报表格式来定义报表中数据的大小与内容。设备可以支持一个或多个报表。在固件中的一个报表描述符用来描述此报表,以及如何使用报表数据的信息。在每一个报表中的一个数值,定义此报表是一个输入(Input )、输出(Output )或是特征(Feature )报表。主机在输入报表中接收数据,在输出报表中传送数据,特征报表可以在任何方向传递。
三、HID 描述符
HID 设备除了支持 USB 设备的 5 种标准描述符之外,还支持 HID 设备特有的 3 种描述符。这些描述符是:1、USB 标准描述符:设备、配置、接口、端点和字符串描述符;2、HID 特有的描述符: HID 、报表(Report )和实体(Physical )描述符。从描述符的关联关系看, HID 描述符是关联于接口。所以如果一个 HID 设备有 2 个端点,设备不需要每个端点有一个 HID 描述符,具体参考如下代码:
设备描述符
struct _DEVICE_DEscriptOR_STRUCT
{
BYTE bLength; //设备描述符的字节数大小
BYTE bDescriptorType; //描述符类型编号,为0x01
WORD bcdUSB; //USB版本号
BYTE bDeviceClass; //USB分配的设备类代码,0x01~0xfe为标准设备类,0xff为厂商自定义类型,0x00不是在设备描述符中定义的,如HID
BYTE bDeviceSubClass; //USB分配的子类代码,同上,值由USB规定和分配的,HID设备此值为0
BYTE bDeviceProtocl; //USB分配的设备协议代码,同上HID设备此值为0
BYTE bMaxPacketSize0; //端点0的最大包的大小
WORD idVendor; //厂商编号
WORD idProduct; //产品编号
WORD bcdDevice; //设备出厂编号
BYTE iManufacturer; //描述厂商字符串的索引
BYTE iProduct; //描述产品字符串的索引
BYTE iSerialNumber; //描述设备序列号字符串的索引
BYTE bNumConfiguration; //可能的配置数量
}
配置描述符
struct _CONFIGURATION_DEscriptOR_STRUCT
{
BYTE bLength; //配置描述符的字节数大小
BYTE bDescriptorType; //描述符类型编号,为0x02
WORD wTotalLength; //配置所返回的所有数量的大小
BYTE bNumInterface; //此配置所支持的接口数量
BYTE bConfigurationVale; //Set_Configuration命令需要的参数值
BYTE iConfiguration; //描述该配置的字符串的索引值
BYTE bmAttribute; //供电模式的选择
BYTE MaxPower; //设备从总线提取的最大电流
}
字符描述符
struct _STRING_DEscriptOR_STRUCT
{
BYTE bLength; //字符串描述符的字节数大小
BYTE bDescriptorType; //描述符类型编号,为0x03
BYTE SomeDescriptor[36]; //UNICODE编码的字符串
}
接口描述符
struct _INTERFACE_DEscriptOR_STRUCT
{
BYTE bLength; //接口描述符的字节数大小
BYTE bDescriptorType; //描述符类型编号,为0x04
BYTE bInterfaceNunber; //接口的编号
BYTE bAlternateSetting; //备用的接口描述符编号
BYTE bNumEndpoints; //该接口使用端点数,不包括端点0
BYTE bInterfaceClass; //接口类型 HID设备此值为0x03
BYTE bInterfaceSubClass; //接口子类型 HID设备此值为0或者1
BYTE bInterfaceProtocol; //接口所遵循的协议
BYTE iInterface; //描述该接口的字符串索引值
}
端点描述符
struct _ENDPOIN_DEscriptOR_STRUCT
{
BYTE bLength; //端点描述符的字节数大小
BYTE bDescriptorType; //描述符类型编号,为0x05
BYTE bEndpointAddress; //端点地址及输入输出属性
BYTE bmAttribute; //端点的传输类型属性
WORD wMaxPacketSize; //端点收、发的最大包的大小
BYTE bInterval; //主机查询端点的时间间隔
}
四、MM32 MCU HID代码实现
本次我们采用MM32L373 miniboard作为测试开发板。为了方便大家使用MM32 MCU的HID功能,我们已经封装好全部代码,用户不需要自己配置以上的那些描述符等参数,只需要了解MM32 MCU HID的VID和PID以及如何处理HID的数据接收和发送即可。
软件资源如下:
以下为函数初始化配置及相关全局变量定义内容,代码如下:
#define USBD_POWER 0
#define USBD_MAX_PACKET0 64
#define USBD_DEVDESC_IDVENDOR 0x2F81
#define USBD_DEVDESC_IDPRODUCT 0x0001
以上是定义的MM32 MCU HID设备VID和PID,灵动微电子已经获得USB组织授权的VID和PID。当设备插入电脑上,可以查看到如上标识的HID设备,如图1所示:
对于MM32 MCU的HID功能来说,在使用HID功能之前先调用USB初始化函数来初始化USB协议栈。
int main(void)
{
// USB Device Initialization and connect
usbd_init();
usbd_connect(__TRUE);
while (!usbd_configured()) // Wait for USB Device to configure
{
}
while (1)
{
}
}
然后就是HID数据收发处理函数,USB数据处理函数如下:
static volatile uint8_t USB_ResponseIdle;
static HID_queue HID_Cmd_queue;
void hid_send_packet()
{
uint8_t *sbuf;
int slen;
if (HID_queue_get_send_buf(&HID_Cmd_queue, &sbuf, &slen))
{
if (slen > USBD_HID_OUTREPORT_MAX_SZ)
{
util_assert(0);
}
else
{
usbd_hid_get_report_trigger(0, sbuf, USBD_HID_OUTREPORT_MAX_SZ);
}
}
}
// USB HID Callback: when system initializes
void usbd_hid_init(void)
{
USB_ResponseIdle = 1;
HID_queue_init(&HID_Cmd_queue);
}
// USB HID Callback: when data needs to be prepared for the host
int usbd_hid_get_report(U8 rtype, U8 rid, U8 *buf, U8 req)
{
uint8_t *sbuf;
int slen;
switch (rtype)
{
case HID_REPORT_INPUT:
switch (req)
{
case USBD_HID_REQ_PERIOD_UPDATE:
break;
case USBD_HID_REQ_EP_CTRL:
case USBD_HID_REQ_EP_INT:
if (HID_queue_get_send_buf(&HID_Cmd_queue, &sbuf, &slen))
{
if (slen > USBD_HID_OUTREPORT_MAX_SZ)
{
util_assert(0);
}
else
{
memcpy(buf, sbuf, slen);
return (USBD_HID_OUTREPORT_MAX_SZ);
}
}
else if (req == USBD_HID_REQ_EP_INT)
{
USB_ResponseIdle = 1;
}
break;
}
break;
case HID_REPORT_FEATURE:
break;
}
return (0);
}
// USB HID override function return 1 if the activity is trivial or response is null
__attribute__((weak))
uint8_t usbd_hid_no_activity(U8 *buf)
{
return 0;
}
// USB HID Callback: when data is received from the host
void usbd_hid_set_report(U8 rtype, U8 rid, U8 *buf, int len, U8 req)
{
uint8_t *rbuf;
main_led_state_t led_next_state = MAIN_LED_FLASH;
switch (rtype)
{
case HID_REPORT_OUTPUT:
if (len == 0)
{
break;
}
if (buf[0] == ID_HID_TransferAbort)
{
HID_TransferAbort = 1;
break;
}
// execute and store to HID_queue
if (HID_queue_execute_buf(&HID_Cmd_queue, buf, len, &rbuf))
{
if (usbd_hid_no_activity(rbuf) == 1)
{
//revert HID LED to default if the response is null
led_next_state = MAIN_LED_DEF;
}
if (USB_ResponseIdle)
{
hid_send_packet();
USB_ResponseIdle = 0;
}
}
else
{
util_assert(0);
}
break;
case HID_REPORT_FEATURE:
break;
}
}
void HID_queue_init(HID_queue *queue)
{
queue->recv_idx = 0;
queue->send_idx = 0;
queue->free_count = FREE_COUNT_INIT;
queue->send_count = SEND_COUNT_INIT;
}
BOOL HID_queue_get_send_buf(HID_queue *queue, uint8_t **buf, int *len)
{
if (queue->send_count)
{
queue->send_count--;
*buf = queue->USB_Request[queue->send_idx];
*len = queue->resp_size[queue->send_idx];
queue->send_idx = (queue->send_idx + 1) % HID_PACKET_COUNT;
queue->free_count++;
return (__TRUE);
}
return (__FALSE);
}
BOOL HID_queue_execute_buf(HID_queue *queue, const uint8_t *reqbuf, int len, uint8_t **retbuf)
{
uint32_t rsize;
if (queue->free_count > 0)
{
if (len > HID_PACKET_SIZE)
{
len = HID_PACKET_SIZE;
}
queue->free_count--;
memcpy(queue->USB_Request[queue->recv_idx], reqbuf, len);
rsize = HID_ExecuteCommand(reqbuf, queue->USB_Request[queue->recv_idx]);
queue->resp_size[queue->recv_idx] = rsize & 0xFFFF; //get the response size
*retbuf = queue->USB_Request[queue->recv_idx];
queue->recv_idx = (queue->recv_idx + 1) % HID_PACKET_COUNT;
queue->send_count++;
return (__TRUE);
}
return (__FALSE);
}
如上,我们只需要实现修改如下HID_ExecuteCommand可处理我们收到的收据,并且填入我们发送数据出去队列即可发送出去。
本次我们使用HID工具V1.3.3测试我们的HID功能,打开软件如图2所示:
点击选择HID设备,选择我们MM32 MCU的HID设备,如图3:
点击连接,发送数据,可以看到图4结果,发送两次共128字节,收到两次数据,共128字节。
以上就是MM32 MCU USB的HID功能,下一节我们继续介绍MM32 MCU USB的WINUSB功能。
来源: 灵动MM32MCU
最近有部分刚接触MM32 MCU的用户朋友们碰到了MCU无法进行下载的情况,然后跟我们反馈芯片有问题,最后经过技术工程师跟进,其实都是用户程序使用错误或者操作不规范等原因造成的,并非芯片有问题,小编表示很无奈很无辜(T_T)。如果大家有碰到这种情况,请不要着急,我们今天这篇文章专门来讲解如何解决烧写失败的情况及有可能出现该类问题的原因。
MM32无法进行烧写原因有多种情况,我们从硬件和软件两个方面分析:
硬件原因:
1、使用的调试器不支持调试下载MM32 MCU,IAR/KEIL上仿真器选择/配置不正确,MM32 MCU已经获得Segger官方认证。在Segger官网J-Link驱动6.40以上版本支持MM32 MCU系列,MM32 MCU支持如MM32-Link、U-link、J-Link、DAP-Link等使用SWD/JTAG(M3)等方式对内核、外设、FLASH进行访问。
2、MM32 MCU最小系统不完整,复位电路设计错误、MCU供电不正常等也会出现无法识别设备Device等情况。MM32 MCU支持宽电压2.0-5.5v供电,在用户自己设计产品PCB时,建议用户预留VCC、SWDIO、SWCLK、GND、NRST五线下载口方式。大家在进行电路设计时请参考我们demo板进行设计,demo板资料在我们官网链接如下:http://www.mm32mcu.com/getfile.aspx?id=386。
3、SWD的两个引脚PA13、PA14引脚虚焊,外部硬件有上拉或下拉,与调试器连接不正确或未连通等情况。
软件原因:
1、程序下载错误,MM32MCU有F、L、SPIN、W、P五大系列,对于不同的MCU我们有不同的库、启动文件与例程,大家下载程序前请查看自己MCU版本型号是否与要下载的程序对应,例如MM3F031C6T6分q版和n版,两个型号由于时钟域配置不同,程序不能直接共用。MM32MCU选型表料在官网链接如下:http://www.mm32mcu.com/getfile.aspx?id=955,库和例程等资料在官网链接如下:http://www.mm32mcu.com/download.aspx?cid=2542。
2、MCU处于读/写保护状态,当MCU处于读保护状态时,FLASH空间处于写保护状态,此时无法使用SWD协议对内核、外设、FLASH进行访问,无法进行下载调试。
3、MCU处于低功耗状态,当MCU处于不同模式低功耗状态时,MCU的外设区域会处于断电状态,调试器识别MCU处于断电状态,无法进行下载调试。
4、SWD的两个引脚PA13、PA14被复用为其他功能或通用IO,PA13、PA14被配置为其他工作模式,此时PA13、PA14无法正常工作在SWD模式,调试器无法通过SWD协议进行下载调试。
5、程序下载过程中出错,由于下载速度和调试器版本特性等原因造成下载过程中出错,导致MCU进入HardFault状态(程序跑飞),此时无法进行下载调试。
下面以J-Link为例,列举两种具体表现形式与解决方案:
一、表现形式:下载报错No Cortex-M SW Device Found。此时调试器未识别到MCU(在keil中无法读到芯片ID),无法下载程序。
可能原因:硬件问题:1、2、3;软件问题:1、2、3、4、5;
解决方式:先检查硬件,确保使用的调试器支持仿真调试MM32MCU,查看MM32MCU最小系统是否完整、复位电路(建议电阻上拉100k,接100nF电容到地)与MCU供电是否正常,测量PA13、PA14引脚是否虚焊,有无上拉或下拉,确认与调试器连接正确并联通。
确认硬件无问题后,仿真方式选择SWD、速度设置建议在1M-10M以内,方法1:NRST脚拉低(按住复位键或短接复位电容,此时在KEIL中可读到ID),然后在IAR/KEIL中擦除程序(在keil的flash工具栏下的Erase操作),在IAR/KEIL出现进度条时释放NRST下拉电平,将芯片程序擦除后,重新上电就可以对MCU进行下载调试;方法2:将BOOT0接高,重新断电上电让MCU从SRAM启动,然后将芯片程序擦除,将BOOT0接低,重新上电就可以对MCU进行下载调试。
二、表现形式:下载报错Error:Flash Download failed – “Cortex - M0”。此时调试器能识别到MCU(在keil中可以读到芯片ID),可以擦除程序,无法下载程序。
可能原因:软件问题:1、5;
解决方式:方法1:调整下载速度,速度设置建议在1M-10M以内,可多调节几个速度试下,Download Fuction选择Erase Full Chip;方法2:如果还是无法下载则进行恢复出厂设置操作,资料在官网链接如下:http://www.mm32mcu.com/getfile.aspx?id=772。
下面介绍使用MM32-Link的解决方案:
如果手上有MM32-Link则可以直接使用MM32-Link+Program来进行恢复操作,MM32-Link支持包与文档资料链接如下:http://www.mm32mcu.com/getfile.aspx?id=963。
使用MM32-Link解决方式操作如下:
1、参照文档连接MM32-Link与MM32MCU,建议使用五线下载方式:VCC、SWDIO、SWCLK、GND、NRST。
2、打开MM32-Link Program,新建workspace,根据芯片型号新建project。
3、确保ICP Program模式下,左下角MM32-Link处圆点为绿色(不为绿色时检查MM32-Link是否连接电脑并正确识别设备,为黄色时双击左下角圆点),芯片连接处圆点为黄色或绿色(为灰色时检查硬件连接并手动对芯片进行一次复位操作,拉低NRST引脚)。
4、点击Erase Chip按钮,等待擦除完成即可重新下载。
以上方式下载均在我们官网例程上进行测试,如果有使用自己新建工程无法下载的情况,请检查自己工程配置与IDE版本,建议使用IAR版本在7.4以上、KEIL版本在5.13及以上,IAR与KEIL上新建MM32 MCU工程方式请参照官网应用文档,在KEIL下新建MM32 MCU工程官网链接如下:http://www.mm32mcu.com/getfile.aspx?id=341,在IAR下新建MM32 MCU工程官网链接如下:http://www.mm32mcu.com/getfile.aspx?id=396。
本文转自: 灵动微电MMCU(MindMotion-MMCU),转载此文目的在于传递更多信息,版权归原作者所有。
一、WWDG 简介
窗口看门狗通常被用来监测由外部干扰或不可预见的逻辑条件造成的应用程序背离正常的运行序列而产生的软件故障。除非递减计数器的值在T6 位变成 0前被刷新,看门狗电路在达到预置的时间周期时,会产生一个MCU 复位。在递减计数器达到窗口寄存器数值之前,如果 7位的递减计数器数值(在控制寄存器中)被刷新,那么也将产生一个MCU 复位。这表明递减计数器需要在一个有限的时间窗口中被刷新。
二、WWDG 主要特征
• 可编程的自由运行递减计数器
• 条件复位
- 当递减计数器的值小于 0x40,(若看门狗被启动)则产生复位。
- 当递减计数器在窗口外被重新装载,(若看门狗被启动)则产生复位。
• 如果启动了看门狗并且允许中断,当递减计数器等于0x40 时产生早期唤醒中断(EWI),它可以被用于重装载计数器以避免WWDG 复位。
三、WWDG功能描述
如果看门狗被启动(WWDG_CR寄存器中的WDGA 位被置1),并且当 7位(T[6:0])递减计数器从0x40 翻转到0x3F(T6位清零)时,则产生一个复位。如果软件在计数器值大于窗口寄存器中的数值时重新装载计数器,将产生一个复位。
应用程序在正常运行过程中必须定期地写入WWDG_CR 寄存器以防止MCU 发生复位。只有当计数器值小于窗口寄存器的值时,才能进行写操作。储存在WWDG_CR 寄存器中的数值必须在0xFF 和0xC0 之间:
1、启动看门狗
在系统复位后,看门狗总是处于关闭状态,设置WWDG_CR 寄存器的WDGA 位能够开启看门狗,随后它不能再被关闭,除非发生复位。
2、控制递减计数器
递减计数器处于自由运行状态,即使看门狗被禁止,递减计数器仍继续递减计数。当看门狗被启用时,T6 位必须被设置1,以防止立即产生一个复位。
T[5:0]位包含了看门狗产生复位之前的计时数目;复位前的延时时间在一个最小值和一个最大值之间变化,这是因为写入WWDG_CR寄存器时,预分频值是未知的。
配置寄存器(WWDG_CFR)中包含窗口的上限值:要避免产生复位,递减计数器必须在其值小于窗口寄存器的数值并且大于0x3F 时被重新装载,上图描述了窗口寄存器的工作过程。
另一个重装载计数器的方法是利用早期唤醒中断(EWI)。设置WWDG_CFR 寄存器中的WEI 位开启该中断。当递减计数器到达0x40 时,则产生此中断,相应的中断服务程序(ISR)可以用来加载计数器以防止WWDG 复位。
在WWDG_SR 寄存器中写0可以清除该中断。注:T6 位可以被用来产生一个软件复位(WDGA 位被置位,T6 位清零)
四、如何编写看门狗超时程序
下图显示了装载到看门狗计数器(CNT)中的 6 位计数值和看门狗的延迟时间之间的线性关系(以 mS为单位)。此图可用来做为快速计算的参考,而未将时间的偏差考虑在内。如果需要更高的精度,可以使用下图提供的计算公式。
当写入 WWDG_CR 寄存器时,始终置 T6 位为1以避免立即产生一个复位。
五、设置WWDG实验分析
• 实验内容简介
环境MM32L073PF Miniboard、MDK,设置WWDG,通过观察喂狗与不喂狗的复位现象。
• 实验代码分析
首先看main函数
先初始化一个串口,方便我们观察复位现象,然后通过函数Wwdg_reset_ON 进行设置WWDG。最后在循环里通过WWDG_SetCounter函数不停的喂狗。
在函数Wwdg_reset_ON中设置窗口值,和计数器的初值。
• 实验现象
1、当注释掉喂狗函数时,通过串口在不断打印可以看出,MCU一直在复位。
2、当不注释喂狗函数时,串口只打印一次,MCU没有复位。
来源:灵动微电子