[{"permalink":"https://zhr0529.cn/posts/stm32第一部分学习/","title":"STM32第一部分学习","content":"快速上手STM32外设 一、跑马灯 c int main(void) { delay init(); LED Init(); // 需要初始化用到的模块（delay，LED） while(1) { GPIO ResetBits(GPIOC,GPIO Pin 13); delay ms(100); GPIO SetBits(GPIOC,GPIO Pin 13); delay ms(100); } } GPIO ResetBits置零（低电平），GPIO SetBits置一（高电平） 控制LED亮灭 go to definition到LED Init内部一下 c RCC APB2PeriphClockCmd(RCC APB2Periph GPIOC, ENABLE); // 使能总线时钟 GPIO InitStructure.GPIO Pin GPIO Pin 13; // 初始化PC13引脚 GPIO InitStructure.GPIO Mode GPIO Mode Out PP; // 设置推挽输出 GPIO InitStructure.GPIO Speed GPIO Speed 50MHz; // 设置IO口速度GPIO Speed 50MHz（一般默认50MHz） GPIO Init(GPIOC, &GPIO InitStructure); // 把配置好的变量输入进去 以上是对GPIO InitTypeDef结构体的初始化设置 在led.c页面，在页面空白处右键，选择“toggle header”，进入led.h 可写一个宏定义： define LED PCout（13） PCout(13) 1; 这句就完全等于GPIO SetBits(GPIOC,GPIO Pin 13); PCout(13) 0; 这句就完全等于GPIO ResetBits(GPIOC,GPIO Pin 13); 这样宏定义之后，控制PC13的电平就可以写得更简便 头文件中 c ifndef LED H define LED H … … endif 这一段是固定的，照着写即可。 二、按键 c int main(void) { u8 a; delay init(); LED Init(); KEY Init(); //按键初始化 while(1) { a KEY Scan(0); if(a 1) { LED 0; } else if(a 2) { LED 1; } } } 变量a被KEY Scan函数的返回值赋了个值，如果是a 1就让灯亮，a 2灯灭，逻辑分析结束。 key.c: ![[屏幕截图 2025 03 30 170039.png 500]] 使能了PB4和PB5两个GPIO口，但GPIO模式配置成了GPIO Mode IPU 上拉输入模式 上拉输入的IO口在外部接这个IO口的电路（外部输入电路）为 低电平 时，IO口可以测到明确的 低电平 上拉输入的IO口在外部接这个IO口的电路（外部输入电路）为 高电平 时，IO口可以测到明确的 高电平 上拉输入的IO口在外部接这个IO口的电路（外部输入电路）为 高阻态 时，IO口可以测到明确的 高电平 key.h: ![[屏幕截图 2025 03 30 170719.png 500]] GPIO ReadInputDataBit( ) 这个函数就是检测IO口电平的核心函数，比如检测B4口的电平是高还是低： if(GPIO ReadInputDataBit(GPIOB,GPIO Pin 4) 1)就可以简化为： if(KEY1 1) 并且GPIO ReadInputDataBit函数还能进一步简化： 就像第一节的PCout(13)一样： int a; a PBin(4); 这个a如果为1那PB4就是高电平，0就是低电平。 KEY Scan 函数 c u8 KEY Scan(u8 mode) { static u8 key up 1; if(mode) key up 1; // mode 0，不支持长按 if(key up && (KEY1 0 KEY2 0)) //这句就在判断是否有任何按键被按下：没 有按键被按下前，变量key up一直都是1，所以符合条件 { delay ms(10); //消抖延时 if(KEY1 0) { key up 0; return KEY1 PRESS; } else if(KEY2 0) { key up 0; return KEY2 PRESS; } } if(KEY1 1 && KEY2 1) { key up 1; } return 0; } 三、如何自己添加代码文件 创建相应模块文件夹，编写.h文件和.c文件代码（可复制粘贴） 配置编译器：点界面上方三色块，添加.c源文件； 点魔术棒 C/C++ Include Paths，添加.h头文件 在main函数添加新的头文件（ include \"key.h\"） 四、定时器中断 c int main(void) { NVIC PriorityGroupConfig(NVIC PriorityGroup 2); delay init(); LED Init(); TIM3 Init(20000 1,720 1); while(1) { delay ms(100); } } STM32的每个中断都有自己的抢占优先级和子优先级 NVIC PriorityGroupConfig(NVIC PriorityGroup 2); 设置中断优先级分组为分组2 定时器： TIM2，TIM3，TIM4这三个是通用定时器，TIM1是高级定时器。 可以认为定时器内部有个计数器，初始值是0，每过1ms，计数器+1，一直过了99ms，这时，定时器的计数器的值是99；紧接着到第100ms，计数器数到了100，但是计数器的值不是100，而是清零，然后继续从0开始数， 同时触发定时器中断 。 tim.c: ![[屏幕截图 2025 03 30 225527.png 575]] TIM3 IRQHandler函数 —— 中断服务函数 （函数名是不能改的，可以在里面运行各种代码） TIM TimeBaseInitTypeDef TIM TimeBaseStructure; NVIC InitTypeDef NVIC InitStructure; 上面这两句分别是 定时器配置结构体 和 中断配置结构体 TIM GetITStatus函数，获取定时器相关中断的状态。 当一个中断状态是RESET时证明这个中断 还没 准备好触发； 当一个中断状态是SET时证明这个中断 已经 准备好触发； 自动重装载值arr & 预分频系数psc psc 作用： 对总线时钟的频率进行分频，分频之后的频率就是定时器计数频率 STM32F103C8T6最小系统板的STM32的系统总线时钟的频率是 72Mhz ，也就是72,000,000 Hz， 这个数值要记住！ 定时器的psc参数对系统总线时钟频率分频，psc 720 – 1，那意思就是720分频，也就是分频后的频率是之前的1/720，如下： \\frac{{72000000Hz}}{{720}} 100000H 分频后的频率是100000Hz，也就是100KHz，也就是定时器3的计数器的计数频率是100KHz。 100KHz也就是一个周期0.00001秒，也就是定时器3的计数器每0.00001秒记一个数。 psc 720 – 1， 1是内部寄存器规定的：0是不分频，1是二分频，2是三分频，依次类推 arr 作用：规定 “数到多少就触发定时器更新中断” 例：规定让计数器从0数到100，1ms数一次。 这个“100”就是arr，arr 100。 所以自动重装载值arr的作用就是这个的计数器要记到多少才能进一次中断 arr 20000，那就是计数器计两万次才进一次中断，也就是0.00001 20000 0.2秒进一次中断。 ![[屏幕截图 2025 03 30 231312.png 575]] 定时器的计数器随着时间计数，然后计数器达到arr值（TIMx ARR）时，触发定时器更新中断，然后计数器清零继续重新计数。 五、STM32标准库 STM32F10x FWLib 文件夹即为STM32标准库的库函数 “stm32f10x ”这个前缀，代表这套库函数可用于STM32F1的所有系列 “stm32f10x gpio.c” “stm32f10x gpio.c”就是操作gpio相关的库函数，比如之前用的： ![[屏幕截图 2025 03 31 230921.png 625]] 这些凡是操作GPIO的函数全是。 因为我们go to definition以上的任意一个函数，都会跳转到“stm32f10x gpio.c”里去。 “stm32f10x rcc.c” ![[屏幕截图 2025 03 31 231027.png]] 这句就是它的函数，RCC分配外设时钟。 “stm32f10x usart.c” 串口相关的库函数 “misc.c” Misc是Miscellaneous（各种各样的）的缩写，杂项文件，里面主要装的是NVIC相关文件。也就是中断优先级分配相关，以后做项目几乎必用中断，所以提前加。 以 用到单片机的什么外设就加什么库 的原则，这样可以节省单片机的内存。 六、PWM控制舵机 PWM: 新的参数： TIMx CCRx CCR寄存器的全称是CCR(Capture/Compare Register)的缩写，即：捕获/比较寄存器。 ![[屏幕截图 2025 03 31 232005.png 575]] PWM的频率与arr和psc有关 CNT CCR时，PWM是低电平 CCR寄存器有比较的功能, CCR寄存器确定了PWM的占空比 。占空比英文是“duty”： duty \\frac{{TIM\\ CCRx}}{{TIM\\ ARR}} PWM的频率由arr和psc决定； PWM的占空比由CCR决定 舵机： 一般的舵机（特指模拟舵机）有三根线，VCC，GND和SIG（signal信号） 分别是电源线，地线和信号线，信号线需要接具有定时器PWM输出通道的引脚（ 复用 ） 代码： ![[屏幕截图 2025 04 01 143327.png 500]] TIM1 PWM Init： 先看TIM1 PWM Init： 两个形参是arr自动重装载值和psc预分频系数 舵机的PWM周期必须为20ms（50Hz），所以先配置arr和psc这两个参数，让PWM的周期正好是20ms。 设置自动重装载值arr 20000，预分频系数psc 72 则定时器自动装载一次的周期为：1/(72000000/72) 20000 0.02s 20ms，符合要求。注意这里，arr设置得越大，PWM占空比的精度越高，因为： duty \\frac{{TIM\\ CCRx}}{{TIM\\ ARR}} ![[屏幕截图 2025 04 01 143943.png 550]]![[屏幕截图 2025 04 01 143951 2.png 550]] 比定时器中断少了中断服务函数TIMx IRQHandler，也没有了NVIC的相关配置。main函数内也少了中断优先级分组，因为生成PWM用不到定时器中断。 多了新结构体TIM OCInitTypeDef TIM OCInitStructure TIM OCInitTypeDef 是 定时器输出通道配置结构体 ，带OC字样的都是其相关的配置，OC（Output Channel），输出通道。 看代码，首先： ![[屏幕截图 2025 04 01 144711.png 625]] 三个结构体：GPIO配置、定时器配置、定时器输出通道配置。 两个RCC外设时钟使能：使能TIM1和GPIOA的外设。TIM1和GPIOA都在APB2总线上 接着相应的IO使能： ![[屏幕截图 2025 04 01 144943.png 600]] A8 A11，TIM1的四个PWM通道。 GPIO Mode AF PP 复用推挽输出模式 推挽输出和复用推挽输出的不同在于： 推挽输出是GPIO自己通过函数控制电平高低，比如PCout(13) 1; 复用推挽输出是GPIO把电平高低的控制权给了外设，这里就是给了TIM1，GPIO不管了，电平高还是低让TIM1自己去控制。 定时器的参数配置 输出通道参数配置 TIM OCMode，配置为PWM模式1：TIM OCMode PWM1 PWM模式1：递增计数时，当TIMx CNT（计数值） 有效电平 ，否则为 无效电平 。 PWM模式2：递增计数时，当TIMx CNT（计数值） 无效电平 ，否则为 有效电平 。 ![[屏幕截图 2025 04 02 134915.png 525]] 输出极性高： 有效电平为高电平 输出极性低： 有效电平为低电平 实际输出的电平是由输出极性和电平是否有效共同决定的 TIM OutputState是输出模式是否开启，开启才能输出PWM，填TIM OutputState Enable。 TIM Pulse，意思是TIM CCRx的初始值，一般都统一设为0。 然后四句就是输出通道1 4的配置参数上传函数。 ![[屏幕截图 2025 04 02 135424.png 575]] 以上是可以不管的，写上即可 最后一句 TIM Cmd(TIM1, ENABLE) ,开启定时器 TIM SetComparex ![[屏幕截图 2025 04 02 140019.png 575]] TIM SetCompare1(TIM1,500)的意思就是设置定时器1的通道1的CCR值为500。 TIM SetCompare2(TIM1,500)的意思就是设置定时器1的通道2的CCR值为500。 占空比公式： duty \\frac{{TIM\\ CCRx}}{{TIM\\ ARR}} 所以TIM SetCompare1(TIM1,500);这句话的作用就是把TIM1 CH1的占空比改为：500/20000 2.5%，对应舵机的0°位置； 同理，后面TIM SetCompare1(TIM1,2500)就是把占空比改为： 2500/20000 12.5%，对应舵机的180°位置 2025.4.2，还是没能明白占空比与舵机角度之间的对应关系，明白了回来还愿 七、仿真&实物debug 看教程即可。 八、OLED显示 先看代码： ![[屏幕截图 2025 04 02 162732.png 600]] OLED Clear 作用：清屏 OLED ShowString（x，y，内容，字体大小），xy代表坐标值 x的范围（0,127） y的范围： 如果字体高度为8，行数的范围是0到7。 如果文字高度为16，那只能塞下四行，对应的行数就只有0,2,4,6这四个值。 内容：注意（u8 ） 字体大小：8 / 16 OLED Init函数 ![[屏幕截图 2025 04 02 163421.png 625]] PA6，PA7接口可以随意更换：GPIO初始化换成任意其他的两个IO后，在oled.h更改宏定义 sprintf函数 利用sprintf可以轻松处理一个的字符串， 让一些变量值插入字符串 。 c sprintf(text,\"you have %d yuan!\", your money); 此函数的作用就是将your money变量的值插入字符串，并且将加工好的字符串赋值给text变量，text[20]是一个字符串。 之后就可以使用OLED屏幕显示信息了 九、红外寻迹传感器 红外传感器原理： 红外发射管不断发射红外线，红外接收管不断接收是否有红外线反射回来。 当红外线没有被反射回来，红外接收管关断，此时模块的输出端为 低电平 。 当红外线被反射回来，红外接收管导通，此时对应的输出端就是 高电平 。 即： 如果某个传感器检测到 白色 ，那就输出 高电平“1” ； 如果某个传感器检测到 黑色 ，那就输出 低电平“0” ； 看代码： ![[屏幕截图 2025 04 02 231445.png 525]] extern： 外部调用 ，定义一个 全局变量 不在任何一个函数里，添加一句：extern int track error; track Init: ![[屏幕截图 2025 04 02 231924.png 650]] 初始化了五个GPIO， 浮空输入 ，用于检测循迹传感器的OUT1 OUT5。 浮空输入完全取决于外部输入信号，外部输入是1那就是1，外部输入是0就是0，外部输入是高阻态那就电平不确定。 track.h: ![[屏幕截图 2025 04 02 232117.png 600]] GPIO ReadInputDataBit的作用就是读取一个IO口电平的高低，高电平即1，低电平即0 define OUT1 GPIO ReadInputDataBit(GPIOA,GPIO Pin 4) define OUT1 PAin(4) 以上两句是功能完全一样的，两种不同写法。 十、超声波测距 & 定时器计时 1. 会使用超声波测距 2. 会使用定时器进行计时 3. 提升C语言算法功底 HC SR04 VCC： 电源线，3 5.5V宽电压输入 GND：地线，连接单片机的接地（GND） TRIG： 触发 控制信号输入 ECHO： 回响 信号输出 工作原理： (1)TRIG口 触发测距，给TRIG口最少 10us 的高电平。 (2)模块内部自动发送 8 个 40khz 的方波，自动检测是否有声波返回；（这一步我们了解即可，这是超声波传感器内部在干的事，无需我们操作） (3)若有信号返回，经过模块内部运算后，通过 ECHO口 输出一个高电平， 高电平持续的时间就是超声波从发射到返回的时间。 测量距离 (高电平时间 声速)/2 所以只需要： 1. 给TRIG口一个10us以上的高电平，去触发测量任务。 把随便一个IO口搞成推挽输出，接到TRIG上去，然后电平置高，delay us(11)，延时十微秒以上即可 2. 传感器的测量任务进行完之后，ECHO会发出一段时间的高电平，这段高电平的持续时间经过换算就能得到待测距离。 需要测量高电平持续时间 1. 测量高电平： 使用 GPIO ReadInputDataBit 函数不断检测ECHO口电平，一旦电平变化就开始计时。 2. 测量高电平的持续时间 在高电平到来瞬间打开定时器开始计数，等到高电平结束关闭定时器。 CNT清零的同时会进定时器更新中断，我们可以在定时器中断里记录，每进一次定时器中断，记录清零次数+1。 再就是把ARR的值设置得尽量大一点，降低定时器更新中断的触发频率，比如arr 10和arr 65535，如果arr 10，程序会不停进入定时器更新中断以至于大量资源被占用，main函数while(1)的东西可能都执行不了了。 看代码： main: ![[屏幕截图 2025 04 03 232633.png 600]] 选用定时器4。 psc 0 1 1，也就是计数频率是72MHz，不分频，为了让计数器的计数精度足够高 arr 65535，arr能取到的最大值就是65535。 HC SR04 Init: ![[屏幕截图 2025 04 03 232459.png 650]] Trig口推挽，echo口浮空 置低trig口，保持低电平，因为trig置高是开始测距的信号 Senor Using ![[屏幕截图 2025 04 08 223418.png 625]] 第一句 extern int overcount 来自TIM4的中断服务函数，代表TIM4 CNT清零的次数。 while1 中： 前三句用于操作trig口触发超声波开始测距 while(GPIO ReadInputDataBit(GPIOA,GPIO Pin 12) RESET); 只要PA12是低电平，循环就一直进行 也就是PA12（ECHO）一旦变成高电平，马上跳出这个循环 然后给相关变量清零： overcount 0; TIM4 CNT 0 （给TIM4的计数器清零） 再TIM Cmd(TIM4,ENABLE);开启定时器，定时器计数器就会开始递增，CNT到最大值清零并触发中断，中断内通过overcount记录清零次数。 再看后半段程序： 变量i记录第几次测距，测五次取平均值（ 均值滤波 ） while(GPIO ReadInputDataBit(GPIOA,GPIO Pin 12) SET); 一旦检测到echo口变低，说明返回的超声波已经接收到了，就跳出循环 TIM Cmd(TIM4,DISABLE);//关闭定时器，计数器也会停止计数。 使用TIM GetCounter函数得到定时器4计数器目前的值，这句代码等同于：tim TIM4 CNT; length计算公式： tim+overcount 65536就是定时器这段时间总共计了多少个数 高电平时间 总计数值 计数周期 (tim+overcount 65536) ( 1/72000000) 十一、直流有刷电机控制 L298N L298N芯片内部集成有两个 H桥电机驱动电路 ![[屏幕截图 2025 04 10 081014.png 500]] 中间这个圆圈代表 直流电机 。 已知 S8050 型号的三极管是 NPN型三极管 ， S8550 型号的三极管是 PNP型三极管 。 (对于NPN型三极管，IN端给高电平开关导通；给低电平开关关断。PNP型三极管完全相反) 分析： 当IN1 1，IN2 0时： Q1,Q4导通，Q2,Q3关断，电流流向如下图，定义此时电机正转： ![[屏幕截图 2025 04 10 081425.png 500]] 当IN1 0，IN2 1时：电机电流与上图方向相反，所以电机反转。 ![[屏幕截图 2025 04 10 081530.png 500]] 控制转速： 把IN1和IN2换成两路PWM控制 比如当IN1占空比是0，IN2占空比是100%时，电机全速转 但是当IN1占空比是0，IN2占空比是50%时，电机以全速的1/2转 L298N引脚 ![[屏幕截图 2025 04 10 081913.png 500]] ‘’通道A使能”、“通道B使能”、“板载5V使能”三个跳线帽都不能拔掉 电机A/B通道无法使能的话，对应通道的电机就不会转; 板载5V跳线帽拔掉，5V输出就没有了 用12V锂电池供电 L298N电路 ![[屏幕截图 2025 04 10 082354.png 500]] 右边蓝色的八个二极管作用： 四个钳位电路，将连接电机输出所有通道的电压值钳位在0 Vss（0 12V之间） 因为电机是一种特别容易产生干扰的电气设备， 电动机反过来就是发电机 ，如果不通电，手摇电机，就会产生不小的 反电动势 ；如果发生电机急停等，电路电压就会产生很大的尖峰等干扰，电压一下会波动高于12V，如果不做钳位，可能就会对L2998N芯片等产生损害。 代码分析： ![[屏幕截图 2025 04 10 082656.png 500]] 使输出PWM赫兹为24kHz（芯片推荐的PWM频率） 注意不同的电机有不同的额定电压 额定电压是12V的电机，用12V电池供电，那PWM占空比就可以开到100%，让电机两端电压在12V。 但是如果电机额定电压是8V，大于8V会烧坏电机。所以如果还是使用12V电池供电，PWM控制占空比最大只能开到66.6%。 十二、电机编码器测速 编码器 首先我们要知道， 电机是电机，编码器是编码器 ，这两个东西是各自独立的 13线霍尔款编码器原理如下图： ![[屏幕截图 2025 04 10 083844.png 500]] 1. 上图这个刻度盘安装在电机转轴上，随着电机一起旋转； 2. 刻度盘上有一条条等间距的缝隙； 3. 右边是个小红外灯（发光元件），一直向左边的光敏元件发射激光，但是刻度盘会把光挡住，激光只能穿过刻度盘的一条条缝隙射给光敏元件，如下图： ![[屏幕截图 2025 04 10 084120.png 500]] 刻度盘一直转，光敏元件就能得到 一个循环被照和不被照 的 类似PWM的信号 ，得到一个如下图A相那样的波： ![[屏幕截图 2025 04 10 084245.png 500]] 如果说刻度盘上共有25个缝隙。那么我们可以统计一段时间内 A波形所有高电平的数量 来得出刻度盘一共转了多少个 缝隙 ，也就是多少个 刻度 。 并且能算出刻度盘转到了什么位置 。 比如我们测得，10秒内，高电平共来了67个，那么也就是这个电机十秒转了67/25 2.68转，也就是十秒内的平均转速是0.268r/s 这样我们就实现编码器测速了。 但是这样有个问题 ，不知道盘转的方向，正转反转都是这样一个波！而且如果电机就来回正反转打哆嗦，这样始终就是同一个缝隙来回被光穿过，这样好像也能得到A波，这咋办呢？ 解决方法就是把刻度盘的 内圈 再刻上一圈与原缝隙错开90°的缝隙，然后再拿一个光敏元件和发光元件！如下图：![[屏幕截图 2025 04 10 084644.png 500]]这样就会得到如下两条锯齿波： ![[屏幕截图 2025 04 10 084733.png 500]] A和B两组波形相位差是90º，可以按照下图判断出旋转方向： ![[屏幕截图 2025 04 10 084827.png 500]] 并且再进一步：通过观察上面AB相的波我们发现，圆盘向某个方向一直旋转，AB两相就是这四个状态周而复始。 ![[屏幕截图 2025 04 10 085148.png 500]] 由于该编码器为13线，也就是线数为13ppr，也就是刻度盘上每相都有13个 缝隙 ； 电机每转一圈，A或B相的脉冲数都是13；也就是电机转一圈A相和B相就分别有13个脉冲。 而这个电机的减速器减速比为1：30： 也就是电机转30圈，输出转轴转一圈；编码器轴是连接在里面的电机轴上的，不是接在输出转轴上的。 综合 编码器线数 和 减速器减速比 ，得出最外面的电机输出转轴转1圈，里面的霍尔编码器A或B相会有 13 90 390 个脉冲 。 看代码： ![[屏幕截图 2025 04 10 085540.png 525]] 使用了三个定时器 先看TIM4 ENCODE Init： 编码器正交解码用的定时器4。 多了一个IC结构体。（input channel， 输出通道）![[屏幕截图 2025 04 10 085754.png 500]]![[屏幕截图 2025 04 10 085759.png 750]] 以上代码只需要复制黏贴就好了，除了第一句函数的第一个参数 第一句是一个非常重要的库函数： 编码器模式设置函数 。 TIM EncoderMode TI1代表仅TI1计数； TIM EncoderMode TI2代表仅TI2计数； TIM EncoderMode TI12代表TI1 TI2都计数。 TI1就是输入通道1，就是PB6，就是编码器A相； TI2就是输入通道2，就是PB7，就是编码器B相。 这个参数的意思具体看下面这两个图：![[屏幕截图 2025 04 10 090236.png 550]] 我们使用的模式就是TI12都计数，也就是：![[屏幕截图 2025 04 10 090418.png 275]]观察上面三个图可知，只有正确的连续四个AB状态才会成功进行下一次计数， 一旦有一个毛刺，发现顺序不对了就退回到上次顺序正确时候的计数值。 —————————————————————————————————————————— 再来看定时器的配置：![[屏幕截图 2025 04 10 090609.png 575]] 为了保证最高的采样精度，psc不分频，72MHz。 arr的值是综合了编码器线数、减速器减速比和通道采样方式的一个值： 13ppr的编码器，1:30的减速器下，电机外转轴转一圈A或B相会有390个脉冲，但并不是A或B相来一个脉冲计数器计一次数，我们设置了TI12都计数，那就是每个AB相的正交脉冲组合都算作一次计数： 四个组合都算作计一次数，也就是全部的arr需要390 4 1560。 最后得出 电机输出转轴转一圈会有1560个状态 ，定时器的计数器计1560个数。 定时器4的中断服务函数： ![[屏幕截图 2025 04 10 090908.png 550]] 每当更新中断（计数器溢出）来临，要先判断定时器是向上溢出还是向下溢出： “TIM4 CR1 4&0x01”这一堆就是调用了定时器4相关寄存器的相应位， 结果 0就是向上溢出，结果 1就是向下溢出。 向上溢出就是计数器计数到了1557、1558、1559，然后清零，0。 向下溢出就是4,3,2,1,0，然后CNT arr，1559,1558,1557。 定时器3的中断服务函数： ![[屏幕截图 2025 04 10 091205.png 700]] speed就是速度，单位是转每秒 TIM4 CNT 的值就是定时器4计数器当前时刻的值，范围是0 1559。 Last num变量存着上次采样时TIM4 CNT的值，last count存着上次采样的圈数，这次的和上次的一减，就是△X，△X/△t就是速度，△t是通过定时器3的psc和arr决定的，我们配置的是0.1秒，△t 0.1s。 用到的三个定时器都是用作了不同的功能： 定时器1用于生成PWM控制电机。 定时器3用于多线程任务，计算电机转速。 定时器4用于编码器正交解码。 十三、电机PID闭环控制 先看代码： ![[屏幕截图 2025 04 23 172219 1.png 550]] 定时器1：生成PWM 定时器2：生成0.1s的定时器中断PID计算 定时器3：生成0.1s的定时器中断电机编码器测速 定时器4：正交解码 PID算法的原理和用法 PID（Proportional Integral Derivative）（比例 积分 微分） 算法是一种常用的反馈控制算法，广泛应用于自动控制系统中。它通过测量系统的误差（目标值与实际值之间的差异）来调整控制器的输出，以使系统的响应满足特定的要求。 比例项P PID（位置式）： u P e + I \\int e + D \\mathop e\\limits^ \\bullet P、I、D三个参数是确定不变的：也就是在自动控制中，这三个参数最初设定是多少就永远是多少； 上式中，u就是output，输出值； e就是error，误差值，作为输入。 比例控制P越大，接近目标值的速度越快，但是提升控制速度的代价是 超调 和 震荡 会随着P的增大而增大；P过大会使系统失稳。 ![[屏幕截图 2025 04 23 174358.png 425]] 微分项D 这时候就要引入D了，微分： u P e + D \\mathop e\\limits^ \\bullet e上加一个点代表是e的微分。微分 离散化 之后就是e e last，e是这一次的误差，e last是上一次的误差，于是公式就变成了： u P e + D (e {e {last}}) P e + D \\Delta D项相当于一个阻尼项，可以让控制更加平缓，不会有过猛过冲的情况，同理，D项最重要的作用就是消除超调量![[屏幕截图 2025 04 23 174422.png 625]] 积分项I u P e + I \\int e + D \\mathop e\\limits^ \\bullet 积分项I就是专门来消除静态误差的，加入积分项，PID就全了。 跟前面一样，把公式离散化： u P e + I \\sum e + D \\Delta e 但是积分项I过大也会引起超调增大以及失稳，而且I过大会导致系统滞后，也就是调节效果慢半拍，降低系统快速性。 位置式PID和增量式PID 连续的PID公式： u P e + I \\int e + D \\mathop e\\limits^ \\bullet 将其离散化，得到离散位置式PID公式： u(k) P e(k) + I \\sum\\limits {i 1}^k {e(i)} + D [e(k) e(k 1) K代表第k次PID调节，u(k)就是本次的输出值。 增量式PID： 增量的意思就是本次输出量和上次输出量的差值，比如第k次PID： u(k) P e(k) + I \\sum\\limits {i 1}^k {e(i)} + D [e(k) e(k 1) 那上次就是k 1次PID： u(k 1) P e(k 1) + I \\sum\\limits {i 1}^{k 1} {e(i)} + D [e(k 1) e(k 2) 两式一减就是增量式： \\Delta u(k) u(k) u(k 1) \\Delta u(k) P [e(k) e(k 1)] + I e(k) + D [e(k) 2 e(k 1) + e(k 2) △u(k)是输出值的增量， 也就是上一次的输出值加上△u(k)就是这一次的输出值。 两种PID的应用场合： （1） 位置式PID控制器 适用于需要快速响应和精确控制的场景。比如，在电机控制中，需要控制电机的转速或位置，使其能够快速响应并准确控制，这时可以使用位置式PID控制器。又如，在温度控制中，需要控制温度的精度和稳定性，这时也可以使用位置式PID控制器，同时位置式PID中，I项是会产生滞后，增加准确性的作用，为了系统的快速性，有可能I项直接不用，只用PD控制。 （2） 增量式PID控制器 适用于需要较强稳定性和对测量噪声较高的场景。比如，在机器人控制中，需要控制机器人的运动轨迹，这时可以使用增量式PID控制器，因为机器人在运动过程中，往往受到各种干扰，如摩擦力、惯性力等，增量式PID控制器对这些干扰有较强的稳定性；又如，在液位控制中，需要控制液位的高度和稳定性，这时也可以使用增量式PID控制器，因为在液位测量中，测量信号通常存在一定的噪声和波动，增量式PID控制器可以较好地抵抗这些干扰。 位置式PID代码： C include \"pid.h\" static float err last 0; //上次的误差 static float err 0; //本次的误差 static float Integral err 0; //累计的误差 static float P 0;// //P static float I 0;// //I static float D 0;// //D float a m 0.0; //输出值 float pid control(float speed) { err (aim v now v); //获得本次误差 Integral err + err; //获得累加误差 a m P err + I Integral err + D (err err last); //位置式PID公式 a m a m 10000?a m: 10000; return a m; //输出a m } a m a m 输出限幅 很重要 a m a m 10000?a m: 10000; 增量式PID代码： C include \"pid.h\" static float err last last 0; //上上次的误差 static float err last 0; //上次的误差 static float err 0; //本次的误差 static float P 0; //参数P static float I 0; //参数I static float D 0; //参数D static float off cal 0; float a m 0.0; //输出值 float pid control(float speed) { err last last err last; //更新上上次的误差 err last err; //更新上次的误差 err (aim v now v); //更新本次误差 off cal P (err err last) + I err + D (err + err last last 2 err last); //增量式PID的公式 a m+ off cal; //得到输出值 a m a m 10000?a m: 10000; return a m; //输出a m } 电机速度闭环控制及调参方法 1. 把P、I、D三个参数都调成0 2. 先调P，P从0开始缓慢增大，比如先让P 0.1，观察速度变化，一旦电机一上电，速度超过了目标值，比如2.5r/s，说明有超调了，这时候快速性已经没问题了。 3. 然后慢慢增大D，增加阻尼，减小超调，稍微增大D，观察速度变化，让电机上电后快速稳定在2r/s，即参数调试完成。 4. 速度闭环控制中，积分项I的作用不大，因为发现并没有静态误差，就没有调积分项。 十四、Ozone变量可视化 看教程即可。 十五、I2C底层通信 [[SPI、UART、RS232、RS485、IIC 通信总线协议]] i2c.c： ![[屏幕截图 2025 04 24 175723.png 575]] 引脚的初始化，两个推挽输出。SCL是时钟信号，STM32是主机，负责发出时钟信号，所以IO模式使用推挽输出。 i2c.h： ![[屏幕截图 2025 04 24 180116.png 600]] 直接控制了STM32的寄存器，没有任何库函数 位操作 STM32F1xx的中文参考手册: ![[屏幕截图 2025 04 24 180512.png 650]] 32位寄存器，管GPIO低八位的模式，比如是GPIOA，那就管着GPIOA.0 – GPIOA.7这八个脚。 & ^ 这六个属于 位运算符 ， 分别代表“与运算”，“或运算”，“左移”，“右移”，“按位取反”，“按位异或”。 GPIOA CRL& 0XFF0FFFFF： 0XFF0FFFFF 十六进制转二进制为： “1111 1111 0000 1111 1111 1111 1111 1111” GPIOA CRL& 0XFF0FFFFF也就是GPIOA CRL GPIOA CRL&0XFF0FFFFF 对比上图，也就是 把GPIOA CRL寄存器的23 20位全部置0 ，也就是让5号口的所有配置全部置0 GPIOA CRL (u32)8 1000对应着上/下拉输入。 具体是上拉还是下拉，用PAout(5)这个函数决定 上拉输入的IO口在外部接IO口的电路（外部输入电路）为低电平时，IO口可以测到明确的低电平； 上拉输入的IO口在外部接IO口的电路（外部输入电路）为高电平时，IO口可以测到明确的高电平； 上拉输入的IO口在外部接IO口的电路（外部输入电路）为高阻态时，IO口可以测到明确的高电平； 下拉输入的IO口在外部接IO口的电路（外部输入电路）为低电平时，IO口可以测到明确的低电平； 下拉输入的IO口在外部接IO口的电路（外部输入电路）为高电平时，IO口可以测到明确的高电平； 下拉输入的IO口在外部接IO口的电路（外部输入电路）为高阻态时，IO口可以测到明确的低电平； IIC通信函数： 1. 开始位 2. 结束位 3. 主机回应从机ack 4. 主机回应从机nack 5. 主机等待从机回应 6. 主机发送一个字节 7. 主机接收一个字节 一、开始位 ![[屏幕截图 2025 04 24 182557.png 500]] 开始位：SCL保持高电平，SDA由高电平变为低电平后，延时( 4.7us)，SCL变为低电平。为啥说了要大于4.7us，但是要延时2us呢？ 因为代码运行也需要一定时间，加上代码运行时间即可大于4.7us。 二、停止位 ![[屏幕截图 2025 04 24 182806.png 575]] 停止信号：SCL保持高电平。SDA由低电平变为高电平。 IIC信号在数据传输过程中，当SCL 1高电平时，数据线SDA必须保持稳定状态，不允许有电平跳变，只有在时钟线上的信号为低电平期间，数据线上的高电平或低电平状态才允许变化。 SCL 1时 数据线SDA的任何电平变换会看做是总线的起始信号或者停止信号。 三、应答信号和不应答信号 ![[屏幕截图 2025 04 24 183156.png 600]] 应答信号：主机SCL拉高，读取从机SDA的电平，为低电平表示产生应答。应答信号为低电平时，规定为有效应答位（ACK，简称应答位），表示接收器已经成功地接收了该字节； 应答信号为高电平时，规定为非应答位（NACK），一般表示接收器接收该字节没有成功。 四、等待从机应答 ![[屏幕截图 2025 04 24 183406 1.png 375]] 五、主机发送一个字节 ![[屏幕截图 2025 04 24 183516.png 450]] 六、主机读取一个字节 ![[屏幕截图 2025 04 24 183712.png 500]] 十六、逻辑分析仪 看教程即可。 十七、MPU6050（陀螺仪/加速度计） 注意！使用MPU6050时需要水平放置！不要把芯片竖着放，会导致芯片内部的陀螺仪初始化不成功！ MPU6050的本质是一个IIC通信的传感器模块，也就是一个传感器： MPU6050 陀螺仪+加速度计+数字运动处理器DMP（digital motion processor） 陀螺仪可以测量以空间坐标系x、y、z轴为转轴的三轴角速度；加速度计可以测量x、y、z三轴方向的加速度；3轴+3轴 6轴，所以是六轴运动处理传感器。 ![[屏幕截图 2025 04 26 142914.png 575]] 只需要接VCC、GND、SCL、SDA四个脚即可 传感器的作用： 获取当前的三轴角速度和三轴加速度。 看代码： ![[屏幕截图 2025 04 26 143245.png 550]] IIC通信接口的初始化，随便两个口就行 MPU6050初始化，DMP初始化，不需要看，只需要会用 只需要在合适的地方添加Read DMP函数即可，为读出：三轴姿态角，三轴角速度，三轴加速度 三轴姿态角就是Pitch、Roll、Yaw，俯仰角，横摆角，航向角 三轴角速度就是gyro[0]、gyro[1]、gyro[2] 三轴加速度就是accel[0]、accel [1]、accel [2] 每次调用Read DMP函数，角速度和加速度的相关变量就自动赋值了，我们引用下面这个头文件之后直接用就行 ![[屏幕截图 2025 04 26 144016.png 600]] 十八、完成平衡小车 放手去做！","summary":"STM32 外设学习笔记：GPIO、定时器、串口、ADC 与 DMA。"},{"permalink":"https://zhr0529.cn/posts/linux学习笔记-2025/","title":"【理论】Linux学习笔记","content":"Linux基础命令 Linux的目录结构 image 20221027214128453 / ，根目录是最顶级的目录了 Linux只有一个顶级目录： / 路径描述的层次关系同样适用 / 来表示 /home/itheima/a.txt，表示根目录下的home文件夹内有itheima文件夹，内有a.txt ls命令 功能：列出文件夹信息 语法： ls [ l h a] [参数] 参数：被查看的文件夹，不提供参数，表示查看当前工作目录 l，以列表形式查看 h，配合 l，以更加人性化的方式显示文件大小 a，显示隐藏文件 隐藏文件、文件夹 在Linux中以 . 开头的，均是隐藏的。 默认不显示出来，需要 a 选项才可查看到。 pwd命令 功能：展示当前工作目录 语法： pwd cd命令 功能：切换工作目录 语法： cd [目标目录] 参数：目标目录，要切换去的地方，不提供默认切换到 当前登录用户HOME目录 HOME目录 每一个用户在Linux系统中都有自己的专属工作目录，称之为HOME目录。 普通用户的HOME目录，默认在： /home/用户名 root用户的HOME目录，在： /root FinalShell登陆终端后，默认的工作目录就是用户的HOME目录 相对路径、绝对路径 相对路径， 非 / 开头的称之为相对路径 相对路径表示以 当前目录 作为起点，去描述路径，如 test/a.txt ，表示当前工作目录内的test文件夹内的a.txt文件 绝对路径， 以 / 开头的称之为绝对路径 绝对路径从 根 开始描述路径 特殊路径符 . ，表示当前，比如./a.txt，表示当前文件夹内的 a.txt 文件 .. ，表示上级目录，比如 ../ 表示上级目录， ../../ 表示上级的上级目录 ，表示用户的HOME目录，比如 cd ，即可切回用户HOME目录 mkdir命令 功能：创建文件夹 语法： mkdir [ p] 参数 参数：被创建文件夹的路径 选项： p，可选，表示创建前置路径 touch命令 功能：创建文件 语法： touch 参数 参数：被创建的文件路径 cat命令 功能：查看文件内容 语法： cat 参数 参数：被查看的文件路径 more命令 功能：查看文件，可以支持翻页查看 语法： more 参数 参数：被查看的文件路径 在查看过程中： 空格 键翻页 q 退出查看 cp命令 功能：复制文件、文件夹 语法： cp [ r] 参数1 参数2 参数1，被复制的 参数2，要复制去的地方 选项： r，可选，复制文件夹使用 示例： cp a.txt b.txt，复制当前目录下a.txt为b.txt cp a.txt test/，复制当前目录a.txt到test文件夹内 cp r test test2，复制文件夹test到当前文件夹内为test2存在 mv命令 功能：移动文件、文件夹 语法： mv 参数1 参数2 参数1：被移动的 参数2：要移动去的地方，参数2如果不存在，则会进行改名 rm命令 功能：删除文件、文件夹 语法： rm [ r f] 参数...参数 参数：支持多个，每一个表示被删除的，空格进行分隔 选项： r，删除文件夹使用 选项： f，强制删除，不会给出确认提示，一般root用户会用到 rm命令很危险，一定要注意，特别是切换到root用户的时候。 which命令 功能：查看命令的程序本体文件路径 语法： which 参数 参数：被查看的命令 find命令 功能：搜索文件 语法1按文件名搜索： find 路径 name 参数 路径，搜索的起始路径 参数，搜索的关键字，支持通配符 ， 比如： test表示搜索任意以test结尾的文件 grep命令 功能：过滤关键字 语法： grep [ n] 关键字 文件路径 选项 n，可选，表示在结果中显示匹配的行的行号。 参数，关键字，必填，表示过滤的关键字，带有空格或其它特殊符号，建议使用””将关键字包围起来 参数，文件路径，必填，表示要过滤内容的文件路径，可作为内容输入端口 参数文件路径，可以作为管道符的输入 wc命令 功能：统计 语法： wc [ c m l w] 文件路径 选项， c，统计bytes数量 选项， m，统计字符数量 选项， l，统计行数 选项， w，统计单词数量 参数，文件路径，被统计的文件，可作为内容输入端口 参数文件路径，可作为管道符的输入 管道符 写法： 功能：将符号左边的结果，作为符号右边的输入 示例： cat a.txt grep itheima ，将cat a.txt的结果，作为grep命令的输入，用来过滤 itheima 关键字 可以支持嵌套： cat a.txt grep itheima grep itcast echo命令 功能：输出内容 语法： echo 参数 参数：被输出的内容 反引号 功能：被两个反引号包围的内容，会作为命令执行 示例： echo \\ pwd\\ ，会输出当前工作目录 tail命令 功能：查看文件尾部内容 语法： tail [ f] 参数 参数：被查看的文件 选项： f，持续跟踪文件修改 head命令 功能：查看文件头部内容 语法： head [ n] 参数 参数：被查看的文件 选项： n，查看的行数 重定向符 功能：将符号左边的结果，输出到右边指定的文件中去 ，表示覆盖输出 ，表示追加输出 vi编辑器 命令模式快捷键 image 20221027215841573 image 20221027215846581 image 20221027215849668 底线命令快捷键 image 20221027215858967 命令的选项 我们学习的一系列Linux命令，它们所拥有的选项都是非常多的。 比如，简单的ls命令就有： a A b c C d D f F g G h H i I k l L m n N o p q Q r R s S t T u U v w x X 1等选项，可以发现选项是极其多的。 课程中， 并不会将全部的选项都进行讲解，否则，一个ls命令就可能讲解2小时之久。 课程中，会对常见的选项进行讲解， 足够满足绝大多数的学习、工作场景。 查看命令的帮助 可以通过： 命令 help 查看命令的帮助手册 image 20221027220005610 查看命令的详细手册 可以通过： man 命令 查看某命令的详细手册 image 20221027220009949 Linux常用操作 软件安装 CentOS系统使用： yum [install remove search] [ y] 软件名称 install 安装 remove 卸载 search 搜索 y，自动确认 Ubuntu系统使用 apt [install remove search] [ y] 软件名称 install 安装 remove 卸载 search 搜索 y，自动确认 yum 和 apt 均需要root权限 systemctl 功能：控制系统服务的启动关闭等 语法： systemctl start stop restart disable enable status 服务名 start，启动 stop，停止 status，查看状态 disable，关闭开机自启 enable，开启开机自启 restart，重启 软链接 功能：创建文件、文件夹软链接（快捷方式） 语法： ln s 参数1 参数2 参数1：被链接的 参数2：要链接去的地方（快捷方式的名称和存放位置） 日期 语法： date [ d] [+格式化字符串] d 按照给定的字符串显示日期，一般用于日期计算 格式化字符串：通过特定的字符串标记，来控制显示的日期格式 %Y 年%y 年份后两位数字 (00..99) %m 月份 (01..12) %d 日 (01..31) %H 小时 (00..23) %M 分钟 (00..59) %S 秒 (00..60) %s 自 1970 01 01 00:00:00 UTC 到现在的秒数 示例： 按照2022 01 01的格式显示日期 image 20221027220514640 按照2022 01 01 10:00:00的格式显示日期 image 20221027220525625 d选项日期计算 image 20221027220429831 支持的时间标记为： image 20221027220449312 时区 修改时区为中国时区 image 20221027220554654 ntp 功能：同步时间 安装： yum install y ntp 启动管理： systemctl start stop restart status disable enable ntpd 手动校准时间： ntpdate u ntp.aliyun.com ip地址 格式：a.b.c.d abcd为0 255的数字 特殊IP： 127.0.0.1，表示本机 0.0.0.0 可以表示本机 也可以表示任意IP（看使用场景） 查看ip： ifconfig 主机名 功能：Linux系统的名称 查看： hostname 设置： hostnamectl set hostname 主机名 配置VMware固定IP 1. 修改VMware网络，参阅PPT，图太多 2. 设置Linux内部固定IP 修改文件： /etc/sysconfig/network scripts/ifcfg ens33 示例文件内容： shell TYPE \"Ethernet\" PROXY METHOD \"none\" BROWSER ONLY \"no\" BOOTPROTO \"static\" 改为static，固定IP DEFROUTE \"yes\" IPV4 FAILURE FATAL \"no\" IPV6INIT \"yes\" IPV6 AUTOCONF \"yes\" IPV6 DEFROUTE \"yes\" IPV6 FAILURE FATAL \"no\" IPV6 ADDR GEN MODE \"stable privacy\" NAME \"ens33\" UUID \"1b0011cb 0d2e 4eaa 8a11 af7d50ebc876\" DEVICE \"ens33\" ONBOOT \"yes\" IPADDR \"192.168.88.131\" IP地址，自己设置，要匹配网络范围 NETMASK \"255.255.255.0\" 子网掩码，固定写法255.255.255.0 GATEWAY \"192.168.88.2\" 网关，要和VMware中配置的一致 DNS1 \"192.168.88.2\" DNS1服务器，和网关一致即可 ps命令 功能：查看进程信息 语法： ps ef ，查看全部进程信息，可以搭配grep做过滤： ps ef grep xxx kill命令 image 20221027221303037 nmap命令 image 20221027221241123 netstat命令 功能：查看端口占用 用法： netstat anp grep xxx ping命令 测试网络是否联通 语法： ping [ c num] 参数 image 20221027221129782 wget命令 image 20221027221148964 curl命令 image 20221027221201079 image 20221027221210518 top命令 功能：查看主机运行状态 语法： top ，查看基础信息 可用选项： image 20221027221340729 交互式模式中，可用快捷键： image 20221027221354137 df命令 查看磁盘占用 image 20221027221413787 iostat命令 查看CPU、磁盘的相关信息 image 20221027221439990 image 20221027221514237 sar命令 查看网络统计 image 20221027221545822 环境变量 临时设置：export 变量名 变量值 永久设置： 针对用户，设置用户HOME目录内： .bashrc 文件 针对全局，设置 /etc/profile PATH变量 记录了执行程序的搜索路径 可以将自定义路径加入PATH内，实现自定义命令在任意地方均可执行的效果 $符号 可以取出指定的环境变量的值 语法： $变量名 示例： echo $PATH ，输出PATH环境变量的值 echo ${PATH}ABC ，输出PATH环境变量的值以及ABC 如果变量名和其它内容混淆在一起，可以使用${} 压缩解压 压缩 tar zcvf 压缩包 被压缩1...被压缩2...被压缩N z表示使用gzip，可以不写 zip [ r] 参数1 参数2 参数N image 20221027221906247 解压 tar zxvf 被解压的文件 C 要解压去的地方 z表示使用gzip，可以省略 C，可以省略，指定要解压去的地方，不写解压到当前目录 unzip [ d] 参数 image 20221027221939899 su命令 切换用户 语法： su [ ] [用户] image 20221027222021619 sudo命令 image 20221027222035337 比如： shell itheima ALL (ALL) NOPASSWD: ALL 在visudo内配置如上内容，可以让itheima用户，无需密码直接使用 sudo chmod命令 修改文件、文件夹权限 语法： chmod [ R] 权限 参数 权限，要设置的权限，比如755，表示： rwxr xr x image 20221027222157276 参数，被修改的文件、文件夹 选项 R，设置文件夹和其内部全部内容一样生效 chown命令 修改文件、文件夹所属用户、组 语法： chown [ R] [用户][:][用户组] 文件或文件夹 image 20221027222326192 用户组管理 image 20221027222354498 用户管理 image 20221027222407618 genenv命令 getenv group ，查看系统全部的用户组 image 20221027222446514 getenv passwd ，查看系统全部的用户 image 20221027222512274 env命令 查看系统全部的环境变量 语法： env","summary":"Linux 命令、权限、网络与进程管理。"},{"permalink":"https://zhr0529.cn/posts/51单片机学习笔记/","title":"【理论】51单片机学习笔记（AI8051U，32bit）","content":"在最开始的 c define MAIN Fosc 24000UL 定义主频为24.000MHz 在main函数中，开头可添加如下代码 c EAXFR 1; // 使能扩展的特殊寄存器 WTST 0x00; // 设置代码等待时间为0，CPU 执行程序的速度最快 CKCON 0x00; // 设置外部总线速度最快 以使得程序运行速度最快 点亮一个LED c sbit LED1 P0 ^ 0; sbit LED2 P0 ^ 1; LED1的对应引脚是P00 LED2的对应引脚是P01 以上代码一般添加在最开头（main函数之前） 用来定义引脚 c P0M1 0x00; P0M0 0x00； 将P0端口的所有引脚都设置为 推挽输出 模式 因为要使用P0引脚，以上代码暂且理解为初始化 （目前这么理解 2025.3.29，知道具体为什么来还愿） c LED1 0; LED2 1; LED1输出低电平，LED2输出高电平 通过输出高低电平控制LED亮灭 编程实现LED闪烁&LED流水灯&数码管显示 LED闪烁 c LED1 ON; delay ms(500); LED1 OFF; delay ms(500); 很简单，在LED亮灭间穿插延时就好了 LED流水灯&数码管显示 c u8 code LED Index[] {0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80}; u8 code SEG Index[] {0x3F,0x06,0x5B,0x4F,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; LED数组的作用，通过十六进制转二进制可以得到LED阵列的状态。如0x08就代表00001000，可得出LED状态 SEG数组的作用，通过不同的十六进制可让数码管显示不同的字符，如0x3F代表1，0x06代表2 c void blink 3(void) { static u8 j; P2 LED Index[j]; j++; if(j 8) j 0; delay ms(500); } void blink 4(void) { static u8 k; P0 SEG Index[k]; k++; if(k 9) k 0; delay ms(500); } 顺着数组做for循环，通过前面定义好的数组就可以实现LED流水灯或数码管计数了 I/O输入与输出控制按键 按键检测代码 c while(1) { delay ms(20); // 延时20毫秒 if(KEY1 0) // 检测按键KEY1是否被按下 { if(i 3) i 0; P2 LED[i]; // 将P2端口的输出设置为LED数组的第i个元素 i++; while(KEY1 0); // 等待按键释放（按键防抖或按键长按处理） } } 注意需要添加消抖延迟函数 & 等待按键释放的while函数 延时代码 c void delay ms(u16 ms) { u16 i; do{ i MAIN Fosc / 6000; while( i); }while( ms); } 例：需要延时200ms → delay ms(200) 定时器中断 需要去ISP软件中选择“定时器计算器”工具，生成所需要的定时器初始化函数与定时器执行函数。 例：主频为24.000MHz进行500毫秒中断的定时器函数 c void Timer0 Init(void) { TM0PS 0x0F; AUXR & 0x7F; TMOD & 0xF0; TL0 0xDC; TH0 0x0B; TF0 0; TR0 1; ET0 1; EA 1； } 注意！ 一定要有EA 1定时器中断才能正常开启。 需要在main函数中进行初始化： Timer0 Init(); 定时器执行函数 c void Timer0 Isr(void) interrupt 1 { //在这里编写需要执行的代码 } 定时器的周期调用 c u16 Count ms[3] {0, 0, 0}; u8 state1 1, state2 1, state3 1, state4 1; void Timer0 Isr(void) interrupt 1 { u8 i; for(i 0; i 300) { Count ms[0] 0; state1 !state1; P20 state1; P23 !state1; } if(Count ms[1] 600) { Count ms[1] 0; state2 !state2; P21 state2; } if(Count ms[2] 900) { Count ms[2] 0; state3 !state3; P22 state3; } if(state1 0 && state2 0 && state3 0) P24 0; else P24 1; } 定时器中断的进阶用法： 通过定义三个不同阈值的计数器，定时器每中断一次，计数器加一。 实现在300ms，600ms，900ms时分别中断执行不同任务 数码管与定时器 任务：按下KEY1，数码管开始计时(如00:00:00)， 按下KEY2，数码管显示年月日(如2024:12:11)， 按下KEY3，数码管计时暂停，再按一下KEY3又恢复计时 按下KEY4，数码管计时清零。 c while(1) { delay ms(20); if(P31 0){ state 1; while(P30 0); } if(P30 0){ state 0; while(P31 0); } if(P32 0){ if(state 1){ state 2; }else if(state 2){ state 1; } while(P32 0); } if(P33 0){ miao fen shi 0; while(P33 0); } switch(state){ case 0:seg date();break; case 1:seg count();break; case 2:seg count pause();break; } } 在while1中持续检测按键情况，哪个按键被按下则执行相对应的任务 数码管显示日期 c void seg(u8 loc, u16 num) { switch(loc) { case 1:P24 1;P23 1;P22 1;break; case 2:P24 1;P23 1;P22 0;break; case 3:P24 1;P23 0;P22 1;break; case 4:P24 1;P23 0;P22 0;break; case 5:P24 0;P23 1;P22 1;break; case 6:P24 0;P23 1;P22 0;break; case 7:P24 0;P23 0;P22 1;break; case 8:P24 0;P23 0;P22 0;break; } P0 SEG NUM[num]; delay ms(1); //延时的目的是为了让数码管有足够的时间显示当前的数字，避免显示闪烁 P0 0x00; //在显示下一个数字之前，先将当前的显示清除，避免串扰 } // 该函数用于在不同的数码管位置显示不同的数字，每个case对应一个数码管的位置 void seg date(void) { seg(1,2);seg(2,0);seg(3,2);seg(4,5); seg(5,0);seg(6,1);seg(7,0);seg(8,1); } 如何用数码管显示20250101 数码管显示计数器 c void Timer0 Isr(void) interrupt 1 { if(state 1){ miao ++; if( miao 59 ) { miao 0; fen++; if( fen 59 ) { fen 0; shi ++; if( shi 23 ) shi 0; } } } } 利用定时器中断进行时分秒计数 c void seg count pause(void) { seg(1,shi/10);seg(2,shi%10);seg(3,SEG NUM[16]);seg(4,fen/10); seg(5,fen%10);seg(6,SEG NUM[16]);seg(7,miao/10);seg(8,miao%10); } 将计数器显示在数码管上 PWM 任务：PWM驱动电机，按键K1按下，电机转速提高(一共三档)，按键K2按下，电机转速减慢，按键K3按下，电机停止 c while(1) { delay ms(20); if(P31 0) { Speed++; if(Speed 3) Speed 3; while(P31 0); } if(P30 0) { Speed ; if(Speed 一个普通的按键检测代码，按下对应按键执行对应速度并在数码管显示 通过定时器中断来控制电机的占空比（PWM功能） 先上定时器中断执行代码： c void Timer0 Isr(void) interrupt 1 { TL0 0x38; TH0 0xFF; counter++; counter% 100; if(counter 逐步分析： c TL0 0x38; TH0 0xFF; TL0 和 TH0 是定时器0的低8位和高8位寄存器。 这两行代码的作用是重新装载定时器0的初始值 0x38 和 0xFF 是定时器的初始值，具体数值取决于单片机的时钟频率和定时器的模式。这些值决定了定时器溢出的时间间隔 每当定时器溢出时，就会触发中断，执行这个中断服务例程。 c counter++; counter % 100; 用于记录定时器中断的次数，当等于100时清零 PWM逻辑 c if (counter 占空比阈值 这段代码的作用是根据计数器的值和占空比阈值来控制电机的状态： 如果 counter 小于 Compare ，则将 motor 设置为1（表示电机开启或输出高电平）。 如果 counter 大于或等于 Compare ，则将 motor 设置为0（表示电机关闭或输出低电平）。 通过调整 Compare 的值，可以改变PWM的占空比，从而控制电机的速度或功率 PWM原理 占空比 ：占空比是指在一个周期内，高电平时间与总周期时间的比例。例如，如果占空比为50%，则在一个周期内，高电平和低电平各占一半时间。 在这段代码中， counter 的范围是0到99，总共100个计数值。 Compare 的值决定了高电平的持续时间。例如： 如果 Compare 50，则高电平持续50个计数值，占空比为50%。 如果 Compare 20，则高电平持续20个计数值，占空比为20%。","summary":"AI8051U 32bit 单片机学习笔记。"},{"permalink":"https://zhr0529.cn/posts/旅游攻略合集/","title":"（杂）旅游攻略合集","content":"此帖为记录近年自己旅游有意识的进行规划的旅游路线，给时光以生命。也为会来浏览我个人博客的人们一些旅游路线参考。 天津 时间：2023.6.29 7.1 day 1 中午到达，晚上去德云社看了相声，200r/人，相当不错 day 2 乘坐环城巴士游览，路线：天津之眼——大悲禅院——金刚桥——三岔河口——古文化街——官银号——鼓楼商业街——南市食品街——北安桥——津门津塔——天津规划展览馆——意大利风情街——大沽桥——解放桥——世纪钟广场——津湾广场——天津站——袁世凯故居——冯国璋故居——玻璃桥——李叔同故居——海河亲水平台——狮子林桥——望海楼教堂——引滦入津纪念碑——天津之眼 然后晚上去打球，感受天津强度😝 day 3 中午去滨江道步行街，其实感觉每个地方的步行街都大同小异。去天津狗不理包子总店吃了午饭，相当一般。 天津景点之一瓷房子，没什么好看的。晚上坐了天津之眼摩天轮，不愧是第一大水上摩天轮，确实不错。 广州 澳门 香港 时间：2023.7.8 7.13 day 1 下午到达，晚上：广州塔 海心桥 在广州塔下的公园听小年轻唱粤语歌 day 2 参观越秀王墓，没什么好看的除了下去墓葬实地内部参观有意思 晚上逛了逛商场 day 3 前往澳门，从珠海的横琴口岸乘坐酒店的发财车入住金沙度假村。晚上澳门的叔叔请我们吃了葡萄牙菜。晚上坐轻轨游览： 澳门银河酒店 永利皇宫 新濠天地 摩铂斯 伦敦人 其实来澳门参观这些富丽堂皇的酒店建筑就很有意思 day 4 去酒店的游泳池玩了一圈 接着去参观巴黎人酒店的埃菲尔铁塔 威尼斯人 新濠影汇 乘坐永利皇宫喷泉缆车 坐酒店的港澳快线途经港珠澳大桥前往香港 day 5 香港迪士尼一日游。买了优速通一天刷完🥰🥰 我想如果我是小时候来肯定会很开心，长大了来玩也很开心 day 6 在香港市区玩了一天，早上出门就遇到了传说中的富贵雪糕车，但好像没有很好吃。去维多利亚港看了江景 铜锣湾 中环 坐天星小轮回到江对岸 坐香港巴士到故宫文化博物馆附近的江滨道坐着看维多利亚港夜景 南京一日游 时间：2023.12.2 租了一辆电动车游览：梧桐大道 明孝陵 中山陵 美龄宫 晚上坐地铁去逛了秦淮河和南京夫子庙，人山人海。 没预约到南京大屠杀死难者纪念馆很可惜 泉州 时间：2024.1.22 2024.1.23 day 1 清源山老君岩 天后宫 浔埔古镇（都是贝壳建筑的小镇，来这才知道这里是簪花真正的起源地） 郑成功雕像 钟楼 开元寺（一条路上都是好吃的😍） day 2 清净寺 关岳庙 洛阳桥 苏延玉故居 泉州海运文化博物馆 崇武古城 南山广化寺 绍兴一日游 时间：2024.4.4 鲁迅故里 百草园 三味书屋 沈氏园（人山人海） 兰亭 这个可以，人很少环境优美。就是离市区比较远 上海（鸣谢hkl，lsh） 时间：2024.6.7 6.8 day 1 入住徐家汇 去静安寺，顺便看了亚洲最大的Apple店，好像也没多大 南京路步行街（下午茶吃了莱莱小笼，神中神，除了贵没有任何缺点） 和平饭店 外滩 坐船到陆家嘴 到上海中心楼顶看了稳定器灯光秀（。） 花园饭店吃晚饭 最后又到外滩边散步，来上海就是要city walk对吧🤭 day 2 武康大楼 宋庆龄故居 上海中心翠园吃午饭 四行仓库 中共一大旧址&会址纪念馆 武汉 时间：2024.8.1 8.4 day 1 古德寺（寺庙偏欧洲中世纪风格很好看，很多鸽子很可爱） 解放公园 （没什么好逛的） 黎黄陂路（商业步行街） 咸安坊 江汉路步行街（人山人海） day 2 湖北省博物馆（大家都约到了门票，赞），主要是看编钟和越王勾践剑 武商梦时代吃午饭 宝通寺 首义公园&博物馆（比较有革命意义） 黄鹤楼（晚上门票加钱的灯光秀是👎） 户部巷（很多美食） day 3 归元寺（就像每个地方都有的普通寺庙） 古琴台（伯牙绝弦的地方，挺有意义的） 爬龟山 晴川阁 长江观景第一台 day 4 东湖风景区（可惜没坐到滑道） 武汉大学 凌波门东湖观景点 光谷广场 霞浦 时间：2024.8.10 8.12 day 1 三都澳旅游景区。这个没啥意思 入住东壁村民宿 day 2 三点起来看巴黎奥运会男篮决赛 五点去北竹看日出，结果当天雾很大没看成 睡回笼觉 下午坐快艇前往四礵列岛。不愧是人间塞尔达，确实美啊，有点像澳大利亚的十二使徒。就是海岛上没有信号，且风大太阳大，没有防护措施，还是比较危险的。 day 3 前往福鼎 爬太姥山，很多怪石头还是挺有意思的 昆明 大理 丽江 香格里拉 时间：2024.8.31 9.8 day 1 先去云牵一线品尝了一下云南招牌过桥米线😍好吃量大不贵，人均吃撑40r 海埂大坝 海埂公园 西山索道 俯瞰滇池 龙门索道下山 昆明老街吃晚饭 途中偶遇暴雨，拼尽全力无法战胜 day 2 翠湖公园 国立西南联合大学旧址 怪兽大厦 前往大理 大理古城 打车到才村码头看到了洱海日落，绝美 晚上回大理古城喝酒 （大理苦荞啤酒很好喝） day 3 崇圣寺三塔 租车环洱海自驾路线：磻溪S湾 廊桥 喜洲古镇 海舌公园 双廊古镇 鹿鹅山遗址 小普陀 day 4 理想邦 洱海公园 苍洱大道 团山公园 出发丽江 洱海是真的太漂亮了，水清澈且蓝，天气好的时候很无敌，以后有机会想来这里长住一段时间 day 5 玉龙雪山一日游🤗：乘坐大索道到山顶4680石碑 牦牛坪 蓝月谷 晚上回到丽江古城 不得不吐槽丽江的天气，我们过去的时候正好是雨季。一会儿雨一会儿晴，山上全是雾什么都看不到 day 6 一早出发香格里拉，民宿老板帮我联系了当地旅游车。沿途游览：上虎跳峡 中虎跳 哈巴雪山石碑 白水台 到达香格里拉 晚上吃了顿牦牛肉火锅自助有点太顶了 游览独克宗古城：龟山公园 转经筒 大佛寺 day 7 一早驱车两小时前往无底湖（真的美，雪山中的湖泊像一块翡翠） 大经幡 大白塔 day 8 松赞林寺 帕纳海（小羊羔太可爱了） 坐高铁返回丽江乘坐飞机","summary":"天津、广州、澳门、香港等地的旅行路线备忘。"},{"permalink":"https://zhr0529.cn/posts/随想/","title":"随想","content":"迎着阳光盛大逃亡。最后留在记忆深处的总是那些虚无缥缈的东西，往事总在回忆中被赋予寓意。","summary":"往事总在回忆中被赋予寓意。"},{"permalink":"https://zhr0529.cn/posts/arm-cortex-m3学习笔记/","title":"【理论】《ARM Cortex-M3 权威指南》学习笔记","content":"《ARM cortex M3权威指南》学习笔记 学习目的 是时候把单片机开发学得再深入一些了。我希望自己之后的开发能深入到内核和汇编层面，自然要多补充一些理论基础。好的","summary":"《ARM Cortex-M3 权威指南》阅读笔记。"},{"permalink":"https://zhr0529.cn/posts/个人博客开发日记/","title":"【前端】个人博客开发日记 - 基于 Hexo + Butterfly","content":"个人博客开发日记 一、致谢 感谢安知鱼大佬，让我熟悉前端开发流程并使用框架快速搭建了博客，让我也体验了网页设计程序员的快乐 安知鱼：Butterfly重装日记 糖果屋教程贴 二、博客开发和前端学习暂时告一段落 博客还有很多很多可以优化的地方，但我毕竟不是这个方向的，能琢磨成这样已经非常满意了。后面我会多写博客，等以后有机会再优化网站。 网站主题配置文件 文章编写源代码文件 关于网址域名更改回https://Fad1ngWind.github.io 11.21号晚上，本来兴致勃勃的回到家里打开电脑想更新一下博客，结果发现网页访问不了了。各种问题排查才发现是没备案导致的，好家伙一研究才发现备案这么麻烦😢费时费力填了一大堆相关信息不说过不过还要看工作人员脸色，最后还要花几百块买个ICP备案服务码才肯给你搞。于是乎我决定先换回这个基于Github直接推送的域名，暂时先弃用之前斥巨资购买的两年zhr0529.cn的域名，望周知。😊","summary":"第一版个人博客的搭建过程，基于 Hexo + Butterfly。"},{"permalink":"https://zhr0529.cn/posts/日记-第一篇/","title":"【日记】","content":"Our Story Begins. 一、我的个人网站的诞生 c include \"stdio.h\" void main{ printf(\"hello world!\") while(1) { delay ms(10); } } 一切的开始都是在刚上大学的那个迷茫的秋天。 一路走来 2024.11 降转后 大一上 遇到了慧眼识珠的伯乐，随队参加了机器人大赛，收获了人生中第一个国一。参加了信息学院组织的竞赛班，开始自学电路，同时开始接触51单片机开发。我搭建了自己的个人博客网站。 2024.8 大一暑假 自学一年c语言，以不低的排名转专业到机器人工程感知与控制，暑假开始自学Linux系统基础操作指令。同时开始接触Stm32，能为单片机与各类电子器件做基础的嵌入式开发。暑假在计算机公司实习，职位是云网运维工程师，上班就是在机房修电脑，蹲在服务器前等着bios系统启动，以及坐在大屏幕前看镜像监控。实习期间上班摸鱼学了画PCB电路板，这一关键的技能点在后来为我带来了许多机会。 2024.5 大一下 室友们一起给我过了生日。 2024.3 大一下 寒假在新东方兼职当助教，初次体验上班的无奈。经济学学科真的很抽象，我并不喜欢这种在纸上计算建模、对着图像曲线理论分析虚无缥缈的学科，再加上一直对金融股市有着不好的印象，我决定转专业。 2023.11 大一上 面对未来无数未知的分岔路虽然时常会怀念高中的小镇做题家生活，但是大学也蛮精彩。遇到了很多很好的朋友们，期中考高数考了93分选择分流到计算机＋金融的高新专业量化金融。或许未来我会坐在屏幕，成为赛博华尔街之狼？ 2023.8 高考完的暑假 暑假两个月考完了驾照，自学了一点Python，通关了许多曾经想玩的游戏，去了杭州、上海、天津、广州、澳门、香港。录取结果出了，浙江工业大学吗...嗯，虽然最后没有去一个211或者双一流，但是在杭州，好像还不错。","summary":"关于大学、专业选择和个人网站的第一篇日记。"},{"permalink":"https://zhr0529.cn/posts/理论linux学习笔记/","title":"【理论】Linux学习笔记","content":"Linux 基础命令、目录结构、文件操作、用户权限与常用指令学习笔记。","summary":"Linux 基础命令、目录结构、文件操作与权限管理。"}]