
STM32F103C8T6内核与主频ARM Cortex-M3 内核最高 72 MHz存储空间64 KB Flash程序存储20 KB SRAM运行内存工作电压2.0V ~ 3.6V通常由板载稳压芯片将5V转换为3.3V供电GPIO引脚37个快速I/O端口大多可容忍5V输入通信接口3组USART、2组SPI、2组I2C、1组USB 2.0全速、1组CAN总线定时器3个通用定时器1个高级定时器模拟外设2个12位ADC模数转换多达16个输入通道接线图OLEDKeil的字符编码解析与源文件实际的UTF-8编码不匹配导致中文字符串在编译器眼中被“切断”或“乱码化”源文件OLED_Data.c是UTF-8编码汉字占3个字节Keil 默认按GBK/ANSI编码汉字占2个字节去解析文件编译器按2个字节切分3个字节的中文导致字节错位告诉编译器“不要把字符串里的中文字符当做多字节字符去特殊处理请直接按原始字节流处理。”LED通过LED_Init函数开启GPIOC端口时钟并将PC13引脚配置为推挽输出模式同时默认输出高电平使灯处于熄灭状态TIM这个板子只有4个定时器TIM1提供1ms的定时中断TIM1 是高级定时器物理上就接在 APB2 总线上。APB2 上挂载的都是高速和核心外设比如GPIO端口高级定时器通信接口。默认72MHZTIM_TimeBaseInitStructure.TIM_Period 1000 - 1; TIM_TimeBaseInitStructure.TIM_Prescaler 72 - 1;1.TIM_Prescaler 72 - 1;预分频器 PSC作用降频。系统输入的时钟是72MHz每秒振荡72,000,000次。公式实际计数频率 系统时钟 / (PSC 1)。计算72,000,000 / (72 - 1 1) 72,000,000 / 72 1,000,000 Hz (即 1MHz)。物理意义设置完之后定时器内部的计数器每1微秒1us增加1。(注意代码里写的是72 - 1因为硬件是从0开始计数的)2.TIM_Period 1000 - 1;自动重装载值 ARR作用设定计数目标上限。公式定时器从0开始计数数到 ARR 的值时就会发生溢出产生中断或更新事件然后清零重新开始。计算因为计数频率是 1MHz每微秒加1数到 1000-1999 也就意味着数了1000次。物理意义计时总时长 1微秒 × 1000 1000微秒1毫秒1ms。(注意同样因为从0开始1000 - 1实际上代表了1000个数)TIM2驱动两个电机旋转TIM3 TIM4读取两个直流电机旋转的速度和位置key对应端口配置非阻塞式按键定时轮询 状态机 位掩码事件标志1. 定时轮询后台“心跳”如何不阻塞按键检测并非一直运行而是依托于系统定时器比如你之前配置的1ms定时器中断。每次中断只执行一次Key_Tick()函数执行完就立刻退出绝不占用CPU等待。利用时间消抖代码内部设置Count变量每调用20次即20ms才真正去读取一次引脚。这不仅充分利用了时间刻度还完美替代了传统的软件延时消抖。因为机械按键按下时会有5-10ms的抖动20ms足够让电平稳定下来2. 状态机驱动复杂逻辑的“大脑”分步处理用一个变量S来记录每个按键当前处于什么阶段例如等待按下、短按释放、双击等待、长按触发等。每次轮询只根据当前状态和时间戳做一次简单的判断和转移。区分长短事件通过内部的Time变量配合递减运算精确地划分出单击、双击、长按和连发。这些判断不需要CPU卡在这里死等时间而是“每次来看一眼时间到了没”。3. 标志位与“边沿检测”机制前台“事件通知”事件打包检测到按下、松开、长按等事件时通过位运算|把对应的标志位置1存放在Key_Flag[]数组中。边沿提取通过对比当前状态CurrState和上一次状态PrevState精准捕捉到“刚刚按下”和“刚刚松开”的瞬间而不是单纯的“现在是按下状态”。主动读取并清零主程序通过Key_Check()函数查询标志位。查询成功后利用位清除运算将标志位归零实现“一次性事件”如单击、双击只触发一次的效果防止重复执行。MPU6050问题为了精准读出数据把这个函数放在1ms定时中断里如果函数执行太慢会严重拖慢定时中断的执行如果定时函数执行超过1ms定时不准确像这样写如果中断里函数执行时间超过了定时的时间会发生中断重叠。MPU6050 通信耗时MPU6050是通过 I2C 总线通信的。I2C 速度较慢标准模式100kHz快速模式400kHz。读取6个数据AX, AY, AZ, GX, GY, GZ需要发送多次起始信号、寄存器地址读取多个字节这个过程通常需要几百微秒到1毫秒。后果你的定时器每1ms触发一次中断但读取MPU6050可能就要占用接近1ms。这会导致中断还没执行完下一次中断又来了。系统会陷入疯狂进中断的循环抢占CPU资源。定时时间到后定时器会发一个中断位为1中断标志位交给中断控制器控制器时刻观察比对如果中断标志位为1且这时没有优先级更高的中断来抢占那中断函数被自动调用。所以中断重叠的情况下不会形成中断嵌套因为优先级相等。如果在函数执行完清标志位第二个中断会被忽略如果直接清标志位中断函数会反复快速执行不给主程序执行时间。先判断一下是否重叠在中断退出之前再次判断标志位如果还是1那说明中断重叠了。优化方案把延迟去掉对与72M主频的单片机来说还可以工作如果是更高主频的单片机还是需要延时以免超过i2c协议要求的最大速率此时中断不重叠但是中断函数占用时间太久用多字节读取MPU6050内部有自增的地址指针可以读出连续的一片寄存器优化到了300多us电机驱动TB6612可以控制两个独立的直流电机A 通道和 B 通道逻辑电平接3.3v驱动电平接7.4vM1左轮电机M2右轮电机最外层的两个引脚接电机驱动中间是编码器接口TB6612 的控制方式非常简洁电平决定方向PWM 决定速度。以通道 A 为例只要使能引脚 STBY 为高电平通过改变 AIN1 和 AIN2 的高低电平组合就能控制电机状态。AIN1 电平AIN2 电平PWMA 状态电机状态接在 AO1/AO2说明高 (1)低 (0)有效 (如50%)正转顺时针旋转低 (0)高 (1)有效 (如50%)反转逆时针旋转高 (1)高 (1)任意短制动电机两端短接迅速停止低 (0)低 (0)任意停止电机两端悬空自由滑行tim2复用PWM频率是20khz在 STM32 内部系统时钟SYSCLK是 72MHz。这个 72MHz 直接喂给了APB2高速总线。为了省电和降低电磁干扰EMISTM32 设计了一个APB1 预分频器。它把 72MHz 的时钟除以 2变成了36MHz的时钟专门喂给 APB1 总线上比较低速的外设如 I2C、USART2 等。为了照顾定时器STM32 的硬件设计者规定了一条规则如果 APB1 的预分频系数不等于 1比如除以了 2那么定时器TIMx的时钟源会自动乘以 2所以tim2时钟仍旧是72mhz控制左右电机void Motor_SetPWM(uint8_t n, int16_t Duty) { if (n 1) { if (Duty 0) { GPIO_ResetBits(GPIOB, GPIO_Pin_13); GPIO_SetBits(GPIOB, GPIO_Pin_12); PWM_SetCompare1(Duty); } else { GPIO_SetBits(GPIOB, GPIO_Pin_13); GPIO_ResetBits(GPIOB, GPIO_Pin_12); PWM_SetCompare1(-Duty); } } else if (n 2) { if (Duty 0) { GPIO_SetBits(GPIOB, GPIO_Pin_15); GPIO_ResetBits(GPIOB, GPIO_Pin_14); PWM_SetCompare2(Duty); } else { GPIO_ResetBits(GPIOB, GPIO_Pin_15); GPIO_SetBits(GPIOB, GPIO_Pin_14); PWM_SetCompare2(-Duty); } } }STM32 编码器接口Encoder Interface驱动#include stm32f10x.h // Device header void Encoder_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; TIM_TimeBaseInitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInitStructure.TIM_Period 65536 - 1; //ARR TIM_TimeBaseInitStructure.TIM_Prescaler 1 - 1; //PSC TIM_TimeBaseInitStructure.TIM_RepetitionCounter 0; TIM_TimeBaseInit(TIM3, TIM_TimeBaseInitStructure); TIM_ICInitTypeDef TIM_ICInitStructure; TIM_ICStructInit(TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICFilter 0xF; TIM_ICInit(TIM3, TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_2; TIM_ICInitStructure.TIM_ICFilter 0xF; TIM_ICInit(TIM3, TIM_ICInitStructure); TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising); TIM_Cmd(TIM3, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); TIM_TimeBaseInitStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInitStructure.TIM_Period 65536 - 1; //ARR TIM_TimeBaseInitStructure.TIM_Prescaler 1 - 1; //PSC TIM_TimeBaseInitStructure.TIM_RepetitionCounter 0; TIM_TimeBaseInit(TIM4, TIM_TimeBaseInitStructure); TIM_ICStructInit(TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICFilter 0xF; TIM_ICInit(TIM4, TIM_ICInitStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_2; TIM_ICInitStructure.TIM_ICFilter 0xF; TIM_ICInit(TIM4, TIM_ICInitStructure); TIM_EncoderInterfaceConfig(TIM4, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Falling); TIM_Cmd(TIM4, ENABLE); } int16_t Encoder_Get(uint8_t n) { int16_t Temp; if (n 1) { Temp TIM_GetCounter(TIM3); TIM_SetCounter(TIM3, 0); return Temp; } else if (n 2) { Temp TIM_GetCounter(TIM4); TIM_SetCounter(TIM4, 0); return Temp; } return 0; }双定时器配置TIM3挂在 APB1 总线上RCC_APB1Periph_TIM3占用PA6 和 PA7引脚。TIM4也是挂在 APB1 总线上RCC_APB1Periph_TIM4占用PB6 和 PB7引脚。这两个引脚例如 PA6、PA7就是电机编码器输出的A相和B相脉冲信号线。配置为上拉输入配置定时器为什么 ARR 是 65535因为编码器模式下定时器相当于一个“计数器”。我们需要它尽可能数得多所以把自动重装载值ARR设为最大16位无符号的最大值65535。这样电机转一圈产生的脉冲只要不超过65535就不会发生溢出虽然溢出也可以通过中断处理但设为最大最省事。为什么 PSC 是 01-1编码器模式要求必须不分频直接让编码器的脉冲信号作为计数时钟。配置输入捕获通道 (TIM_ICInit) - 滤波cTIM_ICStructInit(TIM_ICInitStructure); // 初始化结构体为默认值 TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICFilter 0xF; // 开启数字滤波 TIM_ICInit(TIM3, TIM_ICInitStructure); // ... 同理配置通道2TIM_ICFilter 0xF这是非常关键的一步。编码器信号在电机转动时会产生抖动毛刺如果不加滤波计数器会乱跳。设置0xF表示开启最强烈的数字滤波即连续采样到N次相同的电平才算有效能有效防止因机械抖动造成的误计数。配置编码器接口模式判断电机是正转还是反转单靠一个光电管是不够的。所以编码器有两个光电管A相通道1和B相通道2。这两个光电管在物理位置上错开了一定的角度通常是90度。正转时A相总是先变成高电平B相稍微“慢半拍”才变成高电平即A相超前B相90度。反转时情况颠倒B相变成高电平A相慢半拍即B相超前A相90度。这就是“正交信号”。TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, ...);TI12模式让定时器内部的硬件逻辑时刻盯着 A 相和 B 相。效果因为A相和B相在物理上是错开90度相位的这就是“正交”当电机正转时A相领先B相90度反转时B相领先A相90度。硬件自动判断开启TI12模式后单片机内部的硬件逻辑会自动检测“谁先跳变”。如果是A相先变计数器加1如果是B相先变计数器减1。这样电机一转计数器就变成了一个有正有负的“速度累加器”你根本不用写复杂的代码去判断方向硬件全帮你干了关于极性配置如果两轮极性相同小车会原地打转Rising, RisingTIM3左轮意思是在A相TI1的上升沿和B相TI2的上升沿都去进行计数。实际效果当电机向前转时A相先跳变上升沿硬件检测到这个顺序计数器就会加1。所以左轮向前转读数总是正的。Rising, FallingTIM4右轮意思是在A相TI1的上升沿和B相TI2的下降沿都去进行计数。实际效果这相当于给B相信号加了一个“180度反相”。因为右轮是镜像安装的原本它向前转时B相会先跳变导致计数器减1变成负数。但是我们把B相的触发条件改成了“下降沿”这导致硬件判断的“谁先跳变”的顺序反过来了。结果就是右轮向前转计数器也变成了加1正数。编码器测量得到的速度值编码器电路中有两个霍尔元件当磁铁N/S级靠近时可以输出高或低电平编码电机常用的磁铁级数是22级磁铁旋转一圈AB相各输出11个脉冲一个跳变沿驱动cnt计数一次总共44个数读取编码器的增量值先除以44再除以读取编码器的时间间隔就是磁铁旋转的速度值单位是转每秒上面测的值除以减速比就是最终测量得到减速电机输出轴的速度串口通信CH340void Serial_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate 9600; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Tx | USART_Mode_Rx; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_Init(USART1, USART_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_Init(NVIC_InitStructure); USART_Cmd(USART1, ENABLE); }