左左右学堂

STM32入门与实战文稿汇编

47期课程文稿,配合207张1080P视频画面阅读。点击图片可查看原图。

第01期 初识stm32

观看原视频

hello,小伙伴们,大家好。这里是左左右。今天给大家带了一整套stm32入门加实战的教程。首先,这一节我们要知道什么是单片机?单片机其实就是一种集成电路芯片!它就像是一个微型的计算机系统,把中央处理器CPU、随机存储器RAM、只读存储器ROM,还有好多I/O口、中断系统、定时器/计数器等功能,都集成到了一块小小的硅片上,从而构成的一个小而完善的微型计算机系统也就是我们俗称的MCU。

单片机可以说是麻雀虽小五脏俱全。这些 名词对于刚接触单片机的小伙伴们可能有些陌生。在后面的课程中我会为大家一一讲解。单片机的应用场景随处可见。就像在我们家用电器里,比如,冰箱、电视、洗衣机,都有单片机的身影。它控制着电器的运行,让我们的生活变得更加方便。在工业控制领域,单片机也是大显身手。

比如PLC控制系统、传感器数据采集系统,都离不开它。还有汽车电子里,像发动机控制、车窗控制、安全气囊,也都是单片机在发挥着作用,让我们的出行更加安全舒适。

另外,在医疗设备、通信设备、智能设备、智能家居、无人机等领域的应用也十分广泛。接下来说一下我们后面课程要讲的这款芯片。stm32f103c8t6,它是由ST公司也就是意法半导体推出的STM32系列芯片之一。

那么stm32f103c8t6这一长串数字加字母代表什么呢?其中ST代表的就是ST公司,M代表MCU。32代表的是基于ARM核心的32位微控制器。F代表通用类型,103代表增强型,C代表引脚数为48。8代表闪存容量为64k,T代表封装形式为LQFP。6代表适用温度为-40℃~85℃。

视频画面 01:48.0
视频画面 01:48.0 · 1080P 原图 · 查看对应视频

关于这些知识,大家有一定的了解就可以了。目前大多数的教程都是以入门为主。没有配套的实战项目,而很多的开源的单片机项目呢,由于难易程度的不同,导致很多只学了入门的同学很难上手,所以,我精心为大家准备了这个。关于教程的说明有以下几点:1.学习本教程需要什么基础?几乎是零基础。

对于教程中遇到的c语音、相关软硬件知识都会有讲解。2.教程收费吗?本套教程是完全免费的,大家的一键三连就相当于学费了。教程的更新频率会在一周更新一到两节。每节课程控制在20分钟左右。其中学习套件是需要大家进行购买的。3.课程的学习路线。总体就是我们前面提到的基础加实战。

视频画面 02:52.0
视频画面 02:52.0 · 1080P 原图 · 查看对应视频
视频画面 09:48.0
视频画面 09:48.0 · 1080P 原图 · 查看对应视频

其中每个知识点我们都会有了解到熟悉再到掌握的过程。了解的过程就是观看视频。熟悉就是听完课以后跟着动手做的过程。掌握就是我在每个知识点讲完之后都会有相应的习题。4.课程相关资料的获取。关注我的个人公众号:左左右学堂。相关资料我会陆续在上面更新的,好了,今天的视频就到这里了。

大家的一键三连是我更新下去的最大动力。

第02期 stm32cudeide的安装

观看原视频

helllo,小伙伴们,大家好。这里是左左右。今天我们讲stm32cubeide这款软件的安装。在安装之前先说一下我为什么选择这款软件?1.集成化的开发环境。stm32cubeide内置图形化配置工具(STM32CubeMX)。支持通过可视化界面配置引脚、时钟、外设等。

视频画面 00:31.8
视频画面 00:31.8 · 1080P 原图 · 查看对应视频

自动生成初始化代码,极大简化硬件配置流程。学过keil5的同学应该知道。初始化的过程是非常痛苦的,不利于新手的学习。当然,如果大家感兴趣。我也会单独推出keil5的课程来供大家学习。2.免费。官方提供完整功能,无需付费许可证,适合个人开发者和企业,STM32CubeIDE 是ST公司,也就是意法半导体推出的官方集成开发环境(IDE)。

专为STM32微控制器设计。学过前面课程的同学应该还记得。stm32这款单片机也是由ST公司推出的。3.跨平台。同时,支持 Windows、Linux 和 macOS。4.内置HAL库。HAL库相对于标准库有诸多优势,比如可跨芯片系列移植代码。HAL库由官方持续更新,而标准库已停止更新。

视频画面 01:21.3
视频画面 01:21.3 · 1080P 原图 · 查看对应视频

好了,下面讲一下这款软件是怎么安装的吧。首先,我们打开搜索引擎,输入stm32cubeide官网。找到带有意法半导体字样的网址。找到Windows版本点击下载,但是国内连接ST官网下载速度很慢。由于要录这期视频,我就把之前的CubeIDE这款软件卸载了。结果昨天晚上下载一晚上都失败了。

直接导致我这期视频晚出了一天。为了避免大家在单片机学习道路的第一关就被却退了。我讲网盘链接放到了我的个人公众号了。请大家自行下载。下面,我们将下载好的安装包解压。双击打开安装程序,跟着我的视频一步一步点击就可以了。这里我建议将软件的安装位置不要放到C盘。

视频画面 02:10.7
视频画面 02:10.7 · 1080P 原图 · 查看对应视频

下面我们双击打开程序。这里是我们将来程序的存放位置,也不建议放到C盘。同时,要注意安装路径和程序的存放路径中都不能有中文。好的,到这里我们的软件就安装好。好了,今天的课程就到这里了,大家点点关注。你们的一键三连是我更新下去的最大动力。

视频画面 03:10.1
视频画面 03:10.1 · 1080P 原图 · 查看对应视频

第03期 点亮一盏LED小灯

观看原视频

helllo,小伙伴们,大家好。这里是左左右。今天带大家点亮第一盏LED小灯。根据我们初中学过的知识,点亮一盏灯需要具备什么条件呢?首先,肯定要有灯吧。我们再添加一个电源。用导线将他两连在一起。这时我们的小灯就会被点亮了。

好的,带着这一思路我们进入下面的课程。现在我们打开我为大家准备的,这块学习板的电路原理图。没拿到的小伙伴们可以扫码关注我的个人公众号获取。我们找到LED灯的电路图 将他单独拿出来。这里的LED_1代表什么呢?这时我们打开另外一张原理图。从中不难发现LED_1就是单片机的PA15引脚。

记住PA15这个引脚,呆会要考。那么,什么是引脚呢?引脚就是单片机芯片周围这些银色的金属条。每个金属条官方都有命名。比如PA1,PB3,PC10等等,现在,我们要在单片机上找到哪个引脚呢?对 PA15。这里 我们假设这个引脚就是PA15。将PA15与LED_1相连。

视频画面 01:30.5
视频画面 01:30.5 · 1080P 原图 · 查看对应视频

下面,我们将stm32也供上电。stm32的正常工作电压就是3.3V。现在,我们感觉是不是离LED灯的点亮就剩一步之遥了?那么PA15有什么功能呢?PA15是GPIO口,翻译过来就是通用输入输出口。它在推挽输出模式下。既可以输出3.3V的高电平,也可以输出0V的低电平。

那么,它是怎么实现的呢,具体原理我们下节课再讲?现在我们可以这样理解。当PA15与电源正极相连时,输出3.3V的高电平。与电源负极相连时,输出0V的低电平。在低电平状态下,灯是不是就被点亮了?讲了这么多,是不是我们将PA15引脚设置为推完输出模式?输出低电平,LED小灯就被点亮了?

视频画面 02:29.4
视频画面 02:29.4 · 1080P 原图 · 查看对应视频

下面我们来验证这个结论是否正确?双击 打开软件。新建项目。在这里我们输入我们单片机的型号 stm32f103c8t6。我们选择第一个 点击收藏 点击finish。我们设置项目名称为3.1_LED 点击finish。现在我们看到的就是STM32的示意图。

视频画面 03:08.7
视频画面 03:08.7 · 1080P 原图 · 查看对应视频

找到PA15引脚 设置为GPIO Output。点击这里 找到GPIO。GPIO output level 就是高低电平的选择。现在默认是低电平我们不用改。GPIO modo 就是GPIO的输出模式的选择。outout push pull就是推挽输出 我们也不用改。

视频画面 04:46.8
视频画面 04:46.8 · 1080P 原图 · 查看对应视频

然后,点击SYS。选择DEBUG模式为serial wire。然后,我们点击保存。之后代码就自动生成了。下面我们拿出我为大家准备的这块学习板。先把STLINK和学习板连在一起。然后,将我们的USB延长线的一端与电脑连接。另一端与STLINK相连。如果之前买过最小系统板的同学也可以按我的电路图进行连线。

好的 我们回到软件 点击绿色小三角。小灯就被点亮了。是不是很简单?这节课我们在没有输入代码的情况下,点亮了一盏LED小灯。是不是单片机的学习也没有想象中的那么难?通过这一节的学习。我们要明白单片机的学习一定要懂原理。懂原理了你才会知道自己要做什么,要怎么做?不论是硬件还是软件都是你实现自己的想法的工具而已。

视频画面 05:45.7
视频画面 05:45.7 · 1080P 原图 · 查看对应视频

不懂原理的学习是很艰难的,也是违背常理的。所以,不要只是盲目的去学软件的使用和代码的编写。那样的学习到最后只会是竹篮打水一场空。在接下来的学习中,我会将这一思想贯穿整套教程。这节课听明白的同学可以把听明白了三个字打到公屏上了。好了,今天的课程就到这里了,您的一键三连是我坚持更新下去的最大动力 我们下节课见。

第04期 一节课讲明白GPIO口

观看原视频

hello,小伙伴们,大家好。这里是左左右。今天通过这节课的学习让大家学明白什么是GPIO口。通过上一节课的学习,大家对GPIO应该有了一定的了解。GPIO就是通用输入输出口。

下面我们打开STM32中文手册的第106页。这张图就是IO口的基本结构图。没有的小伙伴可以关注我的个人公众号领取。这里我们把这张图单独拿出来。这里的IO引脚就是我们单片机上一根根银色的金属条。其余部分指的就是我们stm32单片机的芯片内部了。这里的VDD和VSS分别代表3.3V和0V。

当我们的输入电压大于3.3V时,电流就会从VDD流出。当我们的输入电压是负压,小于0V时,电流就会沿着VSS从IO引脚流出。所以,只有输入电压在0V和3.3V之间。我们的电流才会流入芯片内部。对我们的芯片起到一个保护的作用。既然是IO口,那么它肯定包含输入和输出两种模式。

大家已经听过上节课的讲解。那么,我就从大家熟悉的输出模式开始讲起吧。输出模式由输出驱动器和输出寄存器两部分构成。我们先讲(输出驱动器)。这里我们将P-MOS和N-MOS可以理解为两个开关。当P-MOS闭合时,从VDD通过IO引脚向外输出3.3V的高电平。当N-MOS闭合时,从VSS通过IO引脚向外输出0V的低电平。

当在推挽输出模式下,我们对P-MOS和N-MOS的开合有绝对的控制权。就是说IO口是输出3.3V的高电平 还是0V的低电平。完全是由我们说了算的,在开漏模式下P-MOS相当于彻底断开了,我们只能从VSS通过IO引脚向外输出0V的低电平。这种模式下是有特殊用途的,留个悬念,我们在后面的课程揭晓。

视频画面 02:15.6
视频画面 02:15.6 · 1080P 原图 · 查看对应视频

那么P-MOS和N-MOS的开合是靠什么来控制的呢?就是前面的输出寄存器。这里假设我们的需求是将PA5输出3.3V的高电平。要想输出3.3V的高电平。我们必须首先把输出模式设置为推挽模式。由于我们的STM32单片机是32位的,而不管是PA还是PB引脚都只有16个。所以,我们将输出寄存器的后16位保留。

前16位和我们的PA引脚一一对应,这时通过编写程序控制位设置寄存器。来改变输出数据寄存器中PA5对应的值。通过输出控制将PA5对应的P-MOS管闭合。电流沿VDD从PA5对应的IO口输出3.3V高电平。我们可以把输出模式分为4种。当走上面这条路时分为推挽输出和开漏输出两种模式,当走下面这条路时分为复用推挽输出和复用开漏输出,一共加起来就是4种模式。

下面我们接着讲输入模式。他和输出模式一样。也是包括输入驱动器和输入寄存器这两大块。在输入驱动器这里我们可以看到两个开关。当上面这个开关闭合时 此时默认输入高电平 称为上拉输入模式,当下面这个开关闭合时 此时默认输入低电平 称为下拉输入模式,那这里的这个斯密特触发器起到一个什么作用呢?

视频画面 04:19.3
视频画面 04:19.3 · 1080P 原图 · 查看对应视频

外部电压输入时难免会有波动。这时斯密特触发器设置了一高一低两个阈值电压。当输入电压高于高的那个阈值电压时 输入高电平,当输入电压低于低的那个阈值电压时 输入低电平,经过斯密特触发器形成一个稳定的方波信号。当上下两个开关都处于打开状态时 输入电压不确定 称为浮空输入,当电路处浮空输入模式下,输入电压极易受到外部线路的干扰。

前面讲到的上拉和下拉模式都属于弱上拉和弱下拉。这是因为在VSS和VDD上都串联了一个阻值很大的电阻。在模拟输入模式下,VDD、VSS和施密特触发器的这三个开关都处于失效状态。电流从IO引脚直接到片上外设。由于模拟输入我们需要的到的真实的输入电压值,如果经过斯密特触发器,IO口的输入电压值就失真了。

模拟输入的使用情况。我也会在后面的课程中为大家一一讲解的。通过刚才的学习我们不难得出。输入模式分为上拉、下拉、浮空、模拟四种模式。好了 我们趁热打铁开始今天的案例。

首先,我们打开STM32cubeide这款软件。点击收藏。找到我们上节课收藏好的stm32。点击next。这里我们新建一个项目。并给他命名。第一步我们仍然是点击SYS。选择Debug模式为 serial wire。这时我们设置PA15引脚为GPIO_output。这时我们设置默认输出低电平。

输出模式选择为推挽输出。我们点击保存。这时代码就自动生成了。我们找到main.c的文件。这里的main函数,就是我们程序执行的入口,也就是我们程序开始的地方。

下面是一些初识化的函数。下面我们按下ctrl按键用鼠标点击GPIO_init这个函数。我们就跳转到定义这个函数的地方了。这个函数里的代码是和我们前面的设置是一一对应的。我们在前面设置好了 代码就自动生成了。是不是很方便?

好的 我们回到主函数。这里我们看到一些绿色的注释。

/* 和 */之间的代码是不执行的  起到一个解释代码的作用

方便自己或者其他人维护代码时。一眼就能看出这段代码的作用。接着,我们可以看到,user code begin和user code end两行注释。我们自己写的代码必须放到这两行注释之间。否则在自动生成代码时。不在这两行注释之间的代码会被删掉。

视频画面 06:22.9
视频画面 06:22.9 · 1080P 原图 · 查看对应视频

那么,我们的程序就白写了。上节课我们讲到想要把学习板上的小灯点亮。只需要设置PA15引脚为低电平就可以了。那么,我们看代码是怎么实现的?前面我们刚讲HAL_GPIO_WritePin这个函数时。就是设置PA15引脚为低电平。我们将他复制下来,将他放到WHILE(1)的大括号里。

视频画面 09:48.9
视频画面 09:48.9 · 1080P 原图 · 查看对应视频
视频画面 11:11.4
视频画面 11:11.4 · 1080P 原图 · 查看对应视频

这时我的小灯就被点亮。下面我们给他一个延时,让小灯亮一会。我们输入HAL_DEL。然后,同时按下键盘上的ALT键和/。我们的代码就被自动补全了。这里我们输入500ms也就是0.5秒。还有每行代码后的分号是必不可少的,那么,我们如何实现小灯的熄灭呢?我们还是打开LED小灯的电路原理图。

这时我们将PA15引脚输出3.3V的高电平。小灯两边没有电压差。是不是小灯就熄灭了?前面我讲过 只有在推挽输出模式下。我们对GPIO输出低电平还是高电平才有绝对的控制权。这就是我们前面为什么要把PA15设置为推挽输出的原因?好了,我们接着复制上面这行代码,将RESET改为SET就实现了将PA15输出高电平。

然后,我们接着给一个500ms的延时。还有这个while(1)代表什么呢?它代表下面这个大括号里的代码由第一行开始执行到最后一行。然后,再从第一行开始执行到最后一行。反复无限循环。以我们这段代码为例。第一行执行将PA15设置为低电平,小灯被点亮。第二行等待500ms。第三行执行将PA15设置为高电平,小灯熄灭。

第四行等待500ms。接着,在重新执行第一行代码。这样就是实现了LED小灯亮0.5秒灭0.5秒的效果。小灯就会这样一闪一闪的无限循环下去。好了,拿出我们的学习板和电脑连接。手里之前买过最小系统板的小伙伴们可以按这个图接线。接着,我们点击运行就能实现视频开始LED小灯闪烁的效果了。

可能最近大家也发现我更新的速度并没有之前那么快了。一方面是因为自己制作一期视频用时很长。需要写稿。写稿的目的是以尽量精简语言来提高大家学习的效率。再加上制作动画、添加字幕。一期十几分钟的视频制作时间都在十个小时以上。由于白天还有自己的本职工作要做,每天晚上都会抽出两三个小时去做视频。

另一方面是因为之前发布的视频。播放量和互动量都没有预想的那么好。所以,我把更多的时间投入到了的视频的打磨上。值得欣慰的是最近没有更新的日子里。视频的播放量和关注还是在慢慢上涨的,我始终坚信 好的视频迟早都会得到大家的认可。好了 今天的视频就到这里了。制作不易。您的关注、收藏和点赞是我更新下去的最大动力。

下集我们讲一下C语音的基础。下集见。

第05期 VScode安装与c语言环境搭建

观看原视频

helllo,小伙伴们,大家好。这里是左左右。今天通过这节课的学习让大家学会c语言开发环境的搭建。听过上节课的小伙伴们。如果没有c语言基础会觉得懵懵懂懂的,所以,我在这里给大家临阵磨枪,恶补一下c语音的基础。讲课之前我们先把c语言的环境安装好。

首先,我们先打开桌面上叫c语言安装的这个文件夹。这里有一个压缩包 先将他解压。然后,将解压完的文件剪切到c盘任意文件夹。注意文件夹不能有中文和空格。接下来我们找到bin这个文件将他的路径复制一下。第二步 我们来配置环境变量。点击此电脑图标 右键 属性。找到高级系统设置 单击 点击环境变量。

视频画面 01:01.5
视频画面 01:01.5 · 1080P 原图 · 查看对应视频

找到系统路径中path这个变量 点击编辑。点击新建 将我们刚才复制的路径粘贴到这里。下面我们安装vscode这个软件 按照视频一步步安装就行。比较简单 安装完成后我们双击打开vscode。然后,点击扩展 我们先把c和c++这个拓展库安装了。接着,安装第二个扩展库 coderunner。

视频画面 01:26.2
视频画面 01:26.2 · 1080P 原图 · 查看对应视频
视频画面 01:50.9
视频画面 01:50.9 · 1080P 原图 · 查看对应视频

最后,我们输入zh找到中文简体这个汉化插件。我们看这里的使用方法 按下ctrl shift和 p。输入display 找到configure display language。找到简体中文 点击restart。

视频画面 02:52.8
视频画面 02:52.8 · 1080P 原图 · 查看对应视频

接下来我们在桌面创建一个c语言的文件夹。这里我们随便命名啊,我把他命名为test。在vscode中点击打开文件夹,找到我们刚才新建的文件夹。点击选择文件夹。右键新建文件创建一个test.c的文件。到这里我们的c语音环境就安装好了。下节课我们讲讲c语言的函数和结构体。您的点赞、关注、收藏是我更新下去的最大动力。

视频画面 03:54.6
视频画面 03:54.6 · 1080P 原图 · 查看对应视频

我们下集见。

第06期 八分钟学会c语言函数

观看原视频

helllo,小伙伴们,大家好。这里是左左右。这节课我们接着讲C语言基础。这里打开我们上节课在vscode里新建的c文件。我们新建一个main函数,作为程序的入口,也就是程序开始的地方。这个我们前面的课程是提到过的,我们输出一个 Hello VScode。然后,我们点击RUN code。

这时在程序的下方就输出了hello vscode。我们按下键盘上的CTRL按键点击printf。发现并没有跳转到定义printf这个函数的地方。说明我们编写的程序还是存在问题的,这时我们要添加stdio.h这个头文件。现在按下CTRL点击printf发现程序可以正常跳转了。

视频画面 00:59.6
视频画面 00:59.6 · 1080P 原图 · 查看对应视频

好的,接着往下讲。假如已知一组圆的直径数据分别是98,122,46,72。求出他们的半径。我们定义一个变量a让它等于98除以2。这里的int就代表整数型。用同样的方法定义变量b,c,d。这时我们输出a的值试一试。在每次点击运行前记得要CTRL+S保存一下。在输出窗口发现程序报错了,这是为什么呢?

因为我们没有按printf这个函数的规矩来办事。有点太为所欲为了。我们要用%d来代替a。继续点运行。发现程序输出49 没有问题。趁热打铁我们把b,c,d也输出了。虽然输出的值是没有问题,但是,也不是特别理想。我们在printf中再加一些文字说明。保存后点击运行。是不是清楚明了多了?

视频画面 02:34.7
视频画面 02:34.7 · 1080P 原图 · 查看对应视频

每次算半径都要除以2是不是很麻烦?这里我们就要引入函数的概念了。照猫画虎我们按照main函数写一个函数。我们定义一个整数型的r 让它等于二分之一d。在主函数中调用get_r这个函数,把其他先屏蔽了。选中要屏蔽的代码。

视频画面 03:46.1
视频画面 03:46.1 · 1080P 原图 · 查看对应视频

同时,按下键盘上的ctrl+/。这样选中的代码就被屏蔽了。我们单独输出变量a。程序又一次报错了。实在是太为所欲为了。我们回来改Get_R这个函数。在这个小括号里我们要定义一个整数型的d。这样我们的变量a调用这个函数时。就能把98这个是传到d。相当于d等于98,把98带入get_r这个函数内部后。

r就等于49。这个函数还没有写完。我们不能再为所欲为了。加一行代码。

return r

把我们算出来的r返回。这时函数前面的int要与r数据类型一样。这里我们定义的r就是int类型。所以,函数前面这个int就不用改了。再点运行。代码就能正常执行了。如果我们定义的函数,里面有几十行代码。

视频画面 05:21.3
视频画面 05:21.3 · 1080P 原图 · 查看对应视频

那么,我们就节省了很多写重复代码的时间。所以,函数就具有可重复使用,方便代码维护,增强代码可读性等优点。是不是发明函数的这个人真是个天才啊?大家可能也发现了,我的课程都是讲的比较基础的。其实,任何一门知识,基础的知识点往往是使用频率最高的。这就像大家经常用到的word,excel等。

我们平时用到功能可能都不及软件全部功能的十分之一。如果我讲函数讲两个小时,你可能都睡着了,就算认真听,每个知识点都是讲两三个小时。任何人都很难坚持下去的,所以,我的教程就是让大家不停的去强化各个基础的知识点。然后,把各个知识点串起来在你大脑中形成一套认知体系。就像今天写的这个函数,你突然有一天发现别人是这样写的。

视频画面 06:32.6
视频画面 06:32.6 · 1080P 原图 · 查看对应视频

直接将d/2返回。程序一样能正常运行。你第一次遇到会觉得读不懂这段代码吗?不会吧。你只会觉得这样写真好,我以后也这样写。数据类型我们只讲了int代表整数型。其他都没讲,其实大同小异。遇到了再讲,吃多嚼不烂。所以,我们要通过不断强化基础知识点。先把这套认知体系建立起来再去学一些深入的知识。

就算你忘了。也可以在deepseek里找到答案。你能让一个三岁孩子去deepseek提编程相关的问题吗?所以,认知很重要。在现在这种知识井喷式发展时代,我觉得我这种方法是能最快速度提升知识广度的一种方法。好了 这节课就到这里了。自己去写,去琢磨,才能事半功倍,一定要动起手来敲代码。

凌晨一点还在这里码字,创作不易。您的点赞,收藏加关注是我更新下去的最大动力。我们下期见。

第07期 七分钟学会c语言结构体

观看原视频

helllo,小伙伴们,大家好。这里是左左右。这节课我们接着讲C语言基础。在这里假设你是一名高中老师,这学期新转来几个学生。如果让大家使用excel是很简单的,如果使用c语言该如何去做呢?

首先,我们要定义一个学生的结构体。这里的struct关键词就是来声明这个结构体类型的。结构体中包括int类型的学号。char类型的姓名,int类型的年龄。在前面的课程中我们讲过,int代表整数型。这里的char代表字符型。我们在需要定义字符串类型的变量时。就用char来定义。

视频画面 00:30.8
视频画面 00:30.8 · 1080P 原图 · 查看对应视频

*号代表指针。指针也是c语言中一个非常重要的概念。我们在后面的课程为大家详细讲解。这里不影响我们对结构体的学习。接下来我们在主函数中定义一个变量名为st1的学生结构体。这里的struct Student和我们的数据类型int是一样的。也可以理解为一种数据类型。st1就是一个变量。

视频画面 01:46.2
视频画面 01:46.2 · 1080P 原图 · 查看对应视频
视频画面 04:16.9
视频画面 04:16.9 · 1080P 原图 · 查看对应视频
视频画面 04:54.6
视频画面 04:54.6 · 1080P 原图 · 查看对应视频

这里的点我们可以理解为汉字“的”。就是st1的id是2025001。st1的姓名是强哥。st1的年龄是17岁。那会的强哥还是未成年。照猫画虎把学生2的参数也输入进去。接着,我们先将st1的数据打印出来。来验证一下我的程序编写是否处在问题?

视频画面 03:01.6
视频画面 03:01.6 · 1080P 原图 · 查看对应视频

好的 我们运行一下。运行前一定要记得Ctrl+s保存一下。没有问题。输出的是我们想要的内容。接下来我们把st1和st2的所有信息都用printf打印出来。这里要切记。我们C语言所以用到的标点符号都要是英文的。点击运行。打印出来的信息没有问题。只不过将所有信息都打印在一行不太好。

我们在学生2的前面加一个换行符。反斜杠n。再点击运行。这样就清晰多了。其实,这里还是可以简化一下。我们在struct前面加个关键字typedef。这时我们使用这个结构体时。就可以把struct省去了。我们将student放在大括号的后面。这里我们可以把它理解为两部分。前面这部分是在定义结构体的参数。

后面这个是将我们定义好的结构体命名为student。我们继续点击运行。程序仍能正常输出。没有问题。下面我们打开之前cubeide自动生成的结构体。它这里定义结构体的方法是不是和我们一模一样?

好的 今天的课程就到这里。您的点赞关注和收藏是我持续更新下去的最大动力。我们下期见。

第08期 如何用按键控制LED灯的亮灭

观看原视频

helllo,小伙伴们,大家好。这里是左左右。这节课我们讲一下如何通过一个按键实现led灯的亮灭?学过之前课程的小伙伴们可能就要说了。在之前的线路上安装一个开关不就可以了吗?可是加装按钮开关只能实现按下按钮LED灯亮。抬起按钮LED灯灭的功能。我们总不能为了让LED灯亮着用手指一直按着开关按钮吧。

视频画面 00:32.5
视频画面 00:32.5 · 1080P 原图 · 查看对应视频

还记的我们之前讲过GPIO口不止有输出功能,还有输入功能。这里我们随便选一个IO口设置为输入模式。将我们的开关一端与GPIO口相连,另一端接地,或接3.3V电源。当另一端接地时,我们将输入模式设置为上拉输入。在按键没有按下时,我们单片机读到的电平是高电平1。之前我们讲过不管是上拉还是下拉。

由于接了大阻值的电阻都是弱上拉和弱下拉,当我们按下按钮时,接地的这条线就被导通了。形成一个强下拉。单片机读到的电平是低电平0。同理,当另一端接3.3V电源时。我们将输入模式设置为下拉输入。按钮没有按下时,单片机读到的电平是低电平0,按钮按下时,单片机读到的电平是高电平1,这时,我们可以通过不断读取IO口的电平来判断按钮是否按下?

然后,通过判断电平的高低来控制LED灯的亮灭是不是就可以?原理部分是不是还是很简单的?既然原理听明白了,我们就开始实操吧。我们打开cubeide,新建一个工程。第一步我们仍然是先设置debug模式为serial wire。找到我们的原理图。从中不难发现按钮一端是与PC13相连的。

视频画面 01:54.6
视频画面 01:54.6 · 1080P 原图 · 查看对应视频

LED灯是与PA15相连的,在cubeide中我们将PC13设置为输入模式。PA15设置为输出模式。在原理图中我们也可以找到按钮的另一端是与地相连的。所以,我们要将PC13设置为上拉输入模式,这样设置的目的,就是为了在按钮按下前提供一个反向的电平信号。PA15设置为推挽输出模式。

视频画面 03:16.7
视频画面 03:16.7 · 1080P 原图 · 查看对应视频

设置完成之后我们点击保存,让代码自动生成。接着,我们找到main.c文件中主函数的while循环。在这里我们一直读取pc13号引脚的变化。当读到pc13号引脚为低电平时,说明我们的按钮此时就被按下了。我们先写一个判断。if hal_gpio_read alt+/ 代码自动补全。

这里设置为GPIOC,引脚号为13。我们按下Ctrl鼠标单击进入定义这个函数的地方。有点c语言基础的小伙伴们不难看出。这个函数的返回值是GPIO_PIN_SET和GPIO_PIN_RESET。这也是我们的老朋友了 代表高低电平。为了验证我们的想法,按下Ctrl鼠标单击GPIO_PinState。

在这里也可以看到确实是这两个返回值。实在看不懂的同学可以把整段函数复制下来。打开我们的vscode安装这两个插件。安装完成后在对话框这里粘贴我们刚才复制的函数。点击回车。然后,他就可以帮我们了解这段函数。我们在这里可以看到这段函数的作用。是帮我们读取GPIO引脚的状态。

视频画面 04:38.8
视频画面 04:38.8 · 1080P 原图 · 查看对应视频

往下滑可以看到返回值。下面还有使用示例。大家可以详细看一下。是不是我们又发现自学的一大利器?我们回到ide。当识别到按键被按下,我们就将pa15号引脚的电平反转。是不是就可以了?可是我按下时会有一小段停顿。我们将这个停顿while循环中。到这里我们还没有结束。因为按键刚按下时,会有一个抖动。

视频画面 06:00.9
视频画面 06:00.9 · 1080P 原图 · 查看对应视频

我们将这段抖动加个10ms的延时过滤掉。过滤掉后再次判断按键状态。这样就可以了。只要我们明白原理了,写代码的过程就是实现原理的过程。不断地去修改代码 总会实现我们想要的功能的。我们点击运行 程序没有问题。可以正常运行。我讲最小系统板的接线图也放到这里了。有最小系统板的同学可以这个图接线。

好的 今天的课程就到这里了。您的一键三连和关注是我持续更新下去的最大动力。我们下期见。

第09期 一节课讲明白c语言中的数据类型

观看原视频

helllo,小伙伴们,大家好。这里是左左右。这节课我们接着讲c语言基础。在讲数据类型前先讲一下内存的概念,这里假设这一块就是我们的内存。我们把内存分为很多个小格子。一个小格子中只能存储1位二进制数。它里面可以是0,也可以是1。在计算机中把它命名为bit,它是内存中最小的存储单元。

8个bit组成1byte,也就是一个字节。1024byte组成1KB。以此类推1024kb组成1MB,1024MB组成1GB,1024GB组成1TB。那么,一个字节最多表示多少个数呢?我们先算一个我们熟悉的,两位十进制最多能表示多少个数呢?这个很简单吧,0-99共100个数,也就是10的二次方。

视频画面 01:12.8
视频画面 01:12.8 · 1080P 原图 · 查看对应视频

以此类推,一个字节最多能表示2的8次方个数,也就是0-255 共256个数。其中最小值就是八个0对应十进制0,最大值是8个1 ,对应10进制255。接着,我们在主函数中打印char类型的数据所占的内存空间。这里用到sizeof,输出值为1。因为sizeof的返回值为字节数。

视频画面 01:53.3
视频画面 01:53.3 · 1080P 原图 · 查看对应视频

所以char类型的数据所占内存空间为1个字节。这里有人肯定就要问了,字符'a'是怎么来的?你刚才不是说一个字节只能代表0到255的数吗?这里我们就要引出asc码表了。从中不难发现,在asc码表中。字符'a'的asc码值十进制为97,16进制为61。这里其实不难理解。相当于人和电脑之间有个翻译官,当我们输入字符'a'时,这个翻译官就会将它翻译成97,交给计算机中的内存。

对应二进制是01100001。当我们想要输出变量b时,如果我们要的是字符型,这个翻译官就把字符a给咱们。如果我们要的是整数型,这个翻译官就直接把97给咱们。

视频画面 03:14.4
视频画面 03:14.4 · 1080P 原图 · 查看对应视频

好的我们写代码来验证一下,这里的%c就代表字符型。%d就代表整数型。是不是输出结果和我刚才讲的一模一样?既然我们输入的是字符,但在电脑中存的就是一个二进制的整数。

那么,我们直接输入字符对应的asc码值是不是也可以啊?好的,我们来验证自己的想法。是不是输出的结果也是一样的,这里十进制转二进制电脑自动完成?不用我们操心。这里我们顺便讲一下16进制和2进制相互转换的方法。这个很重要,一定要学会。要学会16进制和2进制相互转换,首先一定要记住4个数字。

那就是8421。接下来我们将01100001转换十六进制数。首先,我们将01100001分为两组,4位为一组。每一位分别与8421这四个数相乘。再将每一组的数相加,第一组为1,第二组为6。于是就得到16进制数为61。

视频画面 04:35.4
视频画面 04:35.4 · 1080P 原图 · 查看对应视频

接下来我们将61转换为二进制数。首先,我们将二进制分为两组,4位为一组。十六进制中的1,我们8421中正好有1。与1对应的二进制填入1,其他填0。十六进制中的6,我们8421中4加2等于6。与4和2对应的二进制填入1,其他填0。就得出二进制数01100001。我们打开计算器来验证一下,是不是没有问题?

视频画面 05:15.9
视频画面 05:15.9 · 1080P 原图 · 查看对应视频

照猫画虎,我们将其他数据类型也用sizeof打印出来。这里大家大概记一下就行。现在我们假如c语言的发展过程中根本就没有数据类型这一说法。当我们定义一个变量时,计算机也不知道你要输入多大的数。是不是只能把8个字节的空间都给你啊,当你实际赋值时?你只给了个整数0。这样是不是就造成大量内存空间的一个浪费啊?

所以,定义数据类型是为了增强内存了利用率。既然数据类型占用的空间是有限的,那我们的变量值也是有限的。具体的数据类型的范围我们当作课后作业留给大家去百度查一下吧。查完也不用去记忆,现用现查就行。

好的 今天的课程就到这里了。您的一键三连和关注是我持续更新下去的最大动力。

第10期 一节课讲明白c语音中的数组

观看原视频

helllo,小伙伴们,大家好。这里是左左右。这期视频我们接着讲c语言基础。听过之前视频的小伙伴们应该都熟悉这个表格。在之前的视频中,我们用结构体输出了其中部分内容。这里我要求大家把他们的身高按顺序打印出来。

好的,开始写代码。现在来看这么写是不是也没什么问题?假如这里我要求大家打印100个人的身高,这样写是不是就显得我们的代码过于冗长了?于是就引出数组。这里我们定义一个int类型的数组,数组名为height。数组的大小为3,也就是可以存放3个int类型的变量。数组成员用大括号括起来,成员之间用逗号隔开。

视频画面 01:27.5
视频画面 01:27.5 · 1080P 原图 · 查看对应视频

接着,我们将数组成员依次打印出来。是不是输出的仍然是我们想要的答案?这里有几点需要注意一下,在数组中,成员的下标是从0开始的,这个其实也不难理解。不管是二进制,十进制,还是十六进制都是从零开始的,数组成员下标不能把0跳过去吧,还有要注意的是这里的方括号,在定义数组时,方括号里的值代表成员数,在使用时,方括号里的值代表成员序号。

视频画面 02:17.8
视频画面 02:17.8 · 1080P 原图 · 查看对应视频

接下来我们把数组的其他几种写法都是写一下。最后,一种写法为什么报错了呢?听过上期视频的同学应该知道。我们定义数据类型的过程就是向内存索要空间的过程。我们最后一种写法内存都不知道你要和他要多少空间。这种写法就属于你是要了,但是你却没说要多少,内存该怎么给你?再回来看前几种写法,是不是内存都知道你要多少空间?

视频画面 03:58.4
视频画面 03:58.4 · 1080P 原图 · 查看对应视频

结合上期视频,大家是不是有豁然开朗的感觉?只要懂得这一原理,接下来我们是不是可以随心所欲的写了,你也别管它有几种写法了?武功的最高境界不就是无招胜有招吗?这时内存肯定要问了,要了5个成员的空间,为什么只填了三个?你就说“那你别管”。当然,这些都是心里话,他不问你也别回。

接着,我们把数组所占空间打印一下。是不是没有问题,一个int类型的变量占用4个字节,数组的大小为5,所以数组所占空间为20个字节。接着,我们把他们的血型也用数组打印出来。是不是也没有问题?我们在讲C语言基础第一期视频是不是打印了一个“hello vscode”?这个就相当于我们前面定义了一个char类型的数组,然后把他打印出来。

视频画面 06:29.4
视频画面 06:29.4 · 1080P 原图 · 查看对应视频

输出没有问题。那么,现在这两种写法有什么区别呢?我们打印出来看一眼。目前来看是不是也没什么问题?有的编译器第一行的输出是会出问题的,接下来我们把两个数组所占空间分别打印出来。这里第二种写法为什么会多一个字节呢?这是因为多出来的一个字节放\0来作为结束符。打印输出时,当识别到\0时就打印结束了。

这里的\0在asc码表中对应值为0,也就是内存给空出来8位全部为0的二进制空间作为结束位。我们来验证一下,是不是输出没有问题?字符型的数组我们在C语言中叫做字符串。在以后的代码编写过程我们按第二种双引号的写法写就行。

好的 今天的视频就到这里了。您的一键三连和关注是我持续更新下去的最大动力。

第11期 一节课讲明白usart通信

观看原视频

helllo,小伙伴们,大家好。这里是左左右。这期视频我们讲USART串口通信。这里假设单片机要将一个字节的数据发送到另一台设备。那么,怎样才能做到呢?

首先,我们将两台设备的引脚用导线连在一起。这里将他们的一端命名为发送端,另一端为接收端。这里的难点是怎么将一个字节的数据通过导线传输到另一台设备?我们先建立一个直角坐标系。横坐标代表时间,单位为秒。纵坐标代表高低电平。学过前面视频的同学都知道引脚是可以输出高低电平的。这里我们从发送端在间隔一秒的时间交替输出高低电平。

在直角坐标系随着时间的推移,是不是就形成了这样的一个方波信号?我们将高电平代表1 低电平代表0。接收端只要将收到的高低电平转换为二进制的1和0。放入其内存中是不是就实现了数据的传输?回到刚才的画面中来演示一下。理解起来会更加容易。因为USART通信协议是低位先行。

首先,要读取第一位0,向外输出低电平。同时,单片机中的8位二进制数向右移一位。这时串口屏读到低电平,将0放入串口屏内存中的最高位。一秒后再次读取单片机中的8位二进制数,此时的最低位已经是1。向外输出高电平,串口屏读到高电平,在将1放入最高位时先将之前的0向右移一位,之后将读到的高电平1放入最高位。

视频画面 01:37.1
视频画面 01:37.1 · 1080P 原图 · 查看对应视频

以此类推,8秒正好就将1个字节的数从单片机传送到串口屏了。这里为什么两个设备的时间间隔都是1秒呢?假设发送端是1秒读一次最低位,而接收端是0.5秒读一次接收到的高低电平会怎么样呢?这时第一位的低电平会被接收端读到两次,也就会填入内存两个0。这时读到的数据就是一个错误的数据。

所以,要求输入和输出要一样的时间间隔。这里的1秒是方便大家理解。现实单片机的传输速度是很快的,这里就要引入波特率这个概念了。常用的波特率有9600bit/s和115200bit/秒等等,也就是每秒传输9600或者115200个bit。所以,这个速度还是很快的,在单片机中真实的USART通信过程,比这个还是要复杂一点点的,要想发送一个字节的数据,还需要有起始位,终止位,奇偶校验位。

我们仍然以发送一个字节的数10101010为例。由于是低位先行,随时间推移依次向外发出是01010101,那么,什么是是奇偶校验呢?假如采用奇校验,现在数据中1的个数为偶数,此时要将我们奇偶校验位置1,1的个数达到奇数。当接收端接收到的数据中1的个数为偶数时,说明数据传输发生错误,这就是奇校验,偶校验指的是要将数据中1的个数设置为偶数,原理是一样的。

视频画面 03:30.6
视频画面 03:30.6 · 1080P 原图 · 查看对应视频

这就是奇偶校验。起始位为0,终止位为1,这里的奇偶校验位是可有可无的。单片机将这样的一组数据称为1帧数据,当有奇偶校验位时这组数据的个数为11,没有奇偶校验位时数据个数为10。我们接着画之前的方波图。默认空闲状态为高电平,起始位0将高电平拉低。作为数据开始传输的标志,我们接着画。

视频画面 04:27.3
视频画面 04:27.3 · 1080P 原图 · 查看对应视频

到终止位时我们将电平拉高,这是因为要恢复到空闲状态下的高电平。为下次数据传输做好准备。这个我们画好的方波图在数据通信中叫做时序图,它很重要。我们是不是不知不觉也将他画出来了?这个很重要,一定要会画,还有值得注意的是,这里的数据帧是由stm32内部集成的硬件外设自动生成的。

接收端会将收到的这帧数据中的起始位,终止位,奇偶校验位去掉。将真正想要的数据存起来,这个过程也是自动完成的。我们只要在软件中设置好就可以了,还有就是这里的发送端在单片机中是叫TX引脚,而这里的接收端叫RX引脚。发送端的这个寄存器叫做发送移位寄存器。接收端的这个寄存器叫做接收移位寄存器。

usart通信是全双工的,也就是说串口屏也是可以给单片机发送数据的。这里我再加一根导线就可以了。这时导线的一端要与串口屏的TX引脚相连。另一端与单片机的RX引脚相连。R 就是receive(收到)的首字母,知道这个就很好记了,如果自己接线的时候要注意,千万不要接错了。我讲上半部分就行了,也就是单片机向串口屏发送数据这一部分。

下半部分与上半部分正好相反就不需要讲了。如果我们想要连续发送多字节的数据,这时就要引入两个寄存器和两个标志位了,发送数据寄存器、TXE标志位。接收数据寄存器,RXNE标志位。当我们输出多字节数据时,此时TXE为1,表示发送数据寄存器为空。单片机CPU将第一个字节的数据放到输出数据寄存器。

视频画面 06:20.8
视频画面 06:20.8 · 1080P 原图 · 查看对应视频
视频画面 08:14.3
视频画面 08:14.3 · 1080P 原图 · 查看对应视频

同时TXE标志位置0,发送数据寄存器会将数据发送到发送移位寄存器,TXE置1。CPU会将第二个字节的数放到输出数据寄存器。TXE标志位同时置0,当发送移位寄存器的数发送完成后,发送数据寄存器会将第二个字节的数发送到发送移位寄存器。TXE标志位置1。CPU会将第三个字节的数放到输出数据寄存器。

TXE标志位同时置0,以此类推 一直到数据发送完成。这里的TXE标志位的作用。就是告诉CPU是否可以往发送数据寄存器中发送数据?起到一个“喊话”的作用。当TXE为1时表示可以。为0时表示不可以,否则会将现有数据覆盖。接收端也是一样的原理。当接收移位寄存器接收到一个字节的数据后,将接收到的数据发送到接收数据寄存器。

RXNE标志位置1。接收数据寄存器将数据发送到串口屏的CPU。RXNE标志位置零,以此类推,一直到数据接收完成。usart全称是通用同步/异步串行接收/发送器。在使用usart通信协议时。如果单片机只需要发送数据或者只需要接收数据时,我们只要接一根线就可以了。其实usart通信就像我和你打电话。

这里的听筒就相当于RX引脚,话筒相当于是TX引脚。我的声音可以通过我的话筒传送到你的听筒再到你的耳朵里。你的声音也可以通过你的话筒传输到我的听筒再到我的耳朵里。我们可以一边说一边听 也可以同时说同时听。还可以只说不听 或者只听不说。之前好多学过单片机的小伙伴一遇到通信协议就发怵。

这期视频听下来是不是觉得通信协议也没有那么可怕?这些都是人类创造的,也是符合人类的思维的。多看几遍,多思考,不难。好的,今天的视频就到这里了,您的点赞收藏和关注是我更新下去的最大动力。我们一起努力,下期见。

第12期 初识串口屏

观看原视频

helllo,小伙伴们,大家好。这里是左左右。这期视频我们讲串口屏。录这期视频之前我一直在纠结这套学习板是选择OLED屏还是串口屏。最终还是选择了串口屏。我给大家对比一下两者的区别,顺便说说串口屏有什么优势。先说OLED屏,优点是简单,易上手,目前大多数教程都是用的这种屏。

它的缺点是功能单一,只有显示功能,而串口屏呢,优点是不止有显示功能,还有触摸功能,而且串口屏有模拟器,就是在没有串口屏实物的情况下也是可以调试的。还有就是串口屏有良好的人机交互能力,不管是自己diy一些小玩意。还是工作中的实战项目,人机交互是很重要的。大家可以想象一下,你的手机没有触摸功能得多难受。

串口屏和OLED屏的区别就是,老年机和智能机的区别。串口屏的缺点是上手有难度,但不大。前面说了那么多优点就是希望大家能学下去,真正掌握串口屏。串口屏使用的通信协议就是USART串口通信。这也就是前面一期视频为什么要讲USART串口通信的原因?这期视频我们要实现。通过串口屏上的按钮来控制学习板上LED灯的亮灭。

就是在串口屏中按钮按下时。向单片机发送一个固定的字符串。当单片机接收到这个字符串就反转LED灯的电平,从而实现LED灯的亮灭。带着这一原理开始我们下面的讲解。

首先,我们打开串口屏官方自带的这款软件。安装包在我的资料包中。关于资料包的获取方式在第一期的视频中有过讲解,这里就不赘述了。首先,我们要新建一个工程。点击文件下拉菜单中的新建。然后,起个文件名,点击保存。接着,选择对应串口屏的型号,点击ok,选择设备显示方向为横屏。这里用到的字符编码是gb2312。

gb2312是字符编码名称,属简体中文编码的一种。和ASC码类似。接着,在工具箱中单击按钮这个图标。这里的按下事件就表示按钮按下时执行的动作。这里的prints就是向单片机发送数据。prints后面第一个参数代表要发送的数据。第二个参数代表要发送数据长度。填入0就表示自动识别长度,一般我们都填0。

点击编译。这里报错了,显示字库ID无效。我们就新建一个字库。点击工具下拉菜单中的字库制作。这里我们选择思源黑体,字库名称填一个自己能分辨的名称就可以了。点击生成字库,然后保存。立即加入字库这里选择是,我们把按钮改个名称,输入按钮,点击回车。点击调试,这里跳出来的就是串口屏的模拟器。

这里的模拟器返回数据就是按钮按下时,串口屏向外发送的数据。31在asc码表中是字符'1'对应的十六进制数。点击S将模拟器返回数据改为字符串类型。是不是输出就是字符'1'了?这里已经完成带有按钮功能的串口屏制作了。

视频画面 04:15.9
视频画面 04:15.9 · 1080P 原图 · 查看对应视频

接下来打开cubedie来实现单片机stm32的相关功能。我们将之前写好的按键控制LED小灯复制一个出来。将他项目名称改一下。这个项目还不能直接用。要将.ioc的文件名改成和项目名称一样。然后,把这两个debug文件删掉。这个项目就可以直接使用了。接着,打开这个.ioc文件,将PA9和PA10这两个引脚分别设置成usart1_tx和usart1_rx。

接着,将USART1设置为异步模式。这个大写A开头的单词就是USART中的A,代表异步模式。由于我们很少用到同步模式,常用的就是UART串口通信,将波特率设置为9600,数据长度为8位二进制数。这里的数据长度包含奇偶校验位。奇偶校验位设置为空,停止位是一个二进制数。

视频画面 05:25.6
视频画面 05:25.6 · 1080P 原图 · 查看对应视频

好的,我们点击保存,进入main.c文件。我们定义一个char类型的数组,用来存储串口屏发送过来的数据。数组成员个数为1,初始化为空。我们把之前写好的代码屏蔽掉。这里用hal_uart_receive用来接收串口屏发过来的数据。这个函数的第一个参数要求是指针类型的。所以,前面要加一个取地址符。

视频画面 07:44.9
视频画面 07:44.9 · 1080P 原图 · 查看对应视频

huart1是uart1的句柄。句柄你可以理解为是一个内存块。它包含了UART1的所有配置参数(如波特率、数据位长度、停止位等)。指针就是指向这个数据块的,第二个参数就是代表接收串口屏发送过来的变量名。这里我们用刚才定义好的receive_data。第三个参数代表要接收字符串的长度。

前面定义的receive_data字符串长度为1,所以填入1。第四个参数代表接收等待的时间。我们填入的hal_max_delay代表接收不到就一直停在这里等。

接下来写个判断,当接收到的字符第0位是字符'1'时。代表按键被按下,我们反转PA15引脚的电平信号。之后我们将字符串的第0位置0,这里只要不是字符'1,就行。点击编译发现有一个警告,按下Ctrl单击找到定义这个函数的地方。可以看得出第二个参数的数据类型是uint8_t的指针。

uint8_t是指无符号整数类,占用空间为8个bit。我们知道char类型也是占用8个bit。我们把receive强转为uint8_t的指针类型就行了。我们要知道stm32输出的是3.3V的高电平和0V的低电平。分别代表1和0,而电脑usb口输出的差分信号,具体什么是差分信号?

感兴趣的小伙伴们,可以去查查,这个不是我们本期视频的重点。那么,要想通过stm32与电脑上的串口屏模拟器进行通信。就要将高低电平信号转换为差分信号,这里就要引入usb转ttl这个模块了。我们的学习板是内置这个模块的,使用学习板的小伙伴们只需要安装ch340这个驱动就可以了,使用最小系统板的小伙伴们要准备usb转ttl这个模块。

并下载安装对应的驱动。接下来我们将电脑通过usb数据线和单片机连在一起。我将学习板和最小系统板的接线图放在这里供大家连线。接着,我们右击电脑这里的开始选择设备管理器。在端口这里找到ch340对应com3,回到串口屏模拟器,选择用户MCU输入。波特率选择9600,串口号选择刚才的COM3,你们根据自己的电脑选择对应的串口号,点击开始。

这时我们点击按钮是不是就实现了LED小灯的亮灭了?这里我们只使用了USART通信里的一根线,也就是串口屏向stm32发送数据。另外,一条stm32向串口屏发送数据的线还没使用呢?该如何使用呢?我们回到cubeide编写代码将stm32接收到的数据'1'再发回到串口屏。点击运行。

是不是发现串口屏发送出去的数据又被单片机发送回来了?这样就实现了USART的双向通信了。接下来我们将做好的串口屏界面下载到实物。然后,与学习板相连。是不是也能实现我们想要的功能?

好的,今天的视频就到这里。您的点赞,收藏和关注是我更新下去的最大动力。我们下期见。

第13期 一节课学会Proteus

观看原视频

helllo,小伙伴们,大家好。这里是左左右。这期视频我们讲proteus这款仿真软件。所有资料的获取方式看我第一期视频,资料包我会持续更新的。后面就不重复说了。

首先,双击图标点开仿真软件。我们要通过仿真来实现我们前面视频讲过的按钮控制LED的亮灭。点击新建工程改个工程名,下面一直点next就可以了。我们点这里的元件模式,然后点击字母是p的按钮。在里面找到我们用的这款stm32f103c8单片机。点击确定就可以把它放置到我们的画布上了。

这里滚动鼠标滑轮可以实现画布的放大和缩小。单击鼠标中键可以拖动画布。单击鼠标左键退出拖动模式。我们接着放置元器件,输入LED找到白色LED灯放入画布。这里点击小键盘上的减号可以实现元器件的旋转。同样的方法放置电阻,将电阻阻值设为220。阻值太大会导致LED小灯点不亮。接着,点击这里的终端模式,点击power放置一个3.3V的正极。

视频画面 01:14.8
视频画面 01:14.8 · 1080P 原图 · 查看对应视频
视频画面 01:56.7
视频画面 01:56.7 · 1080P 原图 · 查看对应视频

用导线将他们连在一起。用同样的方法搭建按钮这一部分。我们知道灯是与PA15号引脚连在一起的。按钮是与PC13号引脚连在一起的,这里相同的名称就代表他们是连在一起的。这里的NRST引脚低电平复位。我们给他加个高电平。仿真软件这一块暂时就到这里了。回到桌面,打开cubeide。

由于仿真软件需要.hex的编译文件,cubeide默认状态下是不生成这个文件的。我们来设置一下。找到我们之前做的按键控制LED灯亮灭的项目。右击,找到最下面这个单击打开。按照视频一步步去操作。然后,选中8.1这个项目,点击编译。在debug这个文件夹中就找到.hex这个文件了,回到仿真软件,选中stm32这个芯片右击。

点击编辑属性。这里的文件路径就选择刚才生成的.hex文件。接着,点击左下角这里的运行。仿真软件就可以正常运行了。接下来我们通过仿真实现串口屏模拟器控制LED灯亮灭。回到桌面双击打开创建虚拟端口的图标。点击这里 再点击这里。因为我前面创建了com8和com9。这里就不重复创建了。

视频画面 04:02.4
视频画面 04:02.4 · 1080P 原图 · 查看对应视频

选择完com口之后,点击对钩就可以了。右击开始找到设备管理器。如果能看到我们刚才创建的两个com口就算成功了。回到cubeide使用同样的方法生成.hex文件。这些都是前面视频已经完成的内容。看不懂的小伙伴可以回去看前面的视频。找到串口的虚拟元器件,TX和RX分别与PA9和PA10相连。

视频画面 05:26.2
视频画面 05:26.2 · 1080P 原图 · 查看对应视频

这里串口选择COM8,波特率设置为9600。把编辑属性里的路径改一下。打开串口屏模拟器点击调试。这里的串口号选择COM9 点击开始。是不是就实现我们想要的功能了?到目前为止,已经讲了三种实现stm32功能的方法。学习板、最小系统板、仿真软件。这下没有理由不去动手实操了吧。

后面每期视频我会将做好的仿真文件放在资料包供大家调试。就不会详细再去讲解这款软件了。我还是会把精力放到stm32的讲解上。好的 今天的视频就到这里。您的点赞、收藏、和关注是我更新下去的最大动力。我们下期见。

第14期 一节课学会c语言判断

观看原视频

helllo,小伙伴们,大家好。这里是左左右。这期视频我们讲c语言中的选择语句if和switch。这里先把我们之前写的main函数写一下。这里用整数1到7分别代表星期一到星期日。我们判断哪天休息。这里if小括号加大括号的写法要刻在脑子里。不要管别人怎么写,你就这么写?小括号里的条件只要满足,就执行大括号里的内容。

视频画面 02:03.6
视频画面 02:03.6 · 1080P 原图 · 查看对应视频

这里一个等于号代表赋值,就是把整数1赋值到变量today中,而两个等号代表判断。就是判断today这个变量里的值是不是等于6或者等于7?这里的竖杠表表示“或”。就是竖杠前后的条件只要满足一个。小括号里的条件就为"真",就会执行大括号里的内容。接着,我们把哪天上班也写出来。

视频画面 00:59.8
视频画面 00:59.8 · 1080P 原图 · 查看对应视频

除了休息就是上班的时候。这里我们只需要else加个大括号就可以了。上点难度,我们判断输入的整数具体代表那一天?运行一下,是不是没有问题?那有没有更简单的写法呢?答案是有的,就是我们的swich选择语句。switch和if写法差不多。只不过switch小括号里填入的是变量名。

视频画面 03:07.4
视频画面 03:07.4 · 1080P 原图 · 查看对应视频

case后面的是判断变量里的值,和双等于类似。点击运行,是不是和我们想象中的不太一样?这就是switch中的穿透,就是只要一个条件满足。后面的条件不管是否满足,都按满足去执行?穿透是有自己的应用场景的只不过在这个例子中我们用不到。这里就要加一个break的关键词了,表示只要条件满足就跳出。

视频画面 03:39.3
视频画面 03:39.3 · 1080P 原图 · 查看对应视频

这里的default表示switch中的所有条件都不满足就执行default后面的内容。这里的default不是必须要写的,是用来处理意外的,建议写上。接着,我们用switch去写之前用if判断写的例子。就是判断哪天休息,哪天上班的例子。这里就用到穿透。点击运行,是不是也没有问题?

视频画面 04:11.2
视频画面 04:11.2 · 1080P 原图 · 查看对应视频

这里还可以简化一些,让代码的可读性更强。好的 今天的视频就到这里。您的点赞、收藏、和关注是我更新下去的最大动力。我们下期见。

第15期 一节课讲明白c语言中的循环(上)

观看原视频

helllo,小伙伴们,大家好。这里是左左右。这期视频我们讲c语言基础中的循环语句。首先,来写一下我们的main函数,之前函数那一期视频我们讲过。函数前面的数据类型要和返回值的数据类型保持一致。由于main函数前面有个int,这里在main函数的结尾给它返回一个整数0。

这就是main函数的一个标准的写法,以后大家都按这个去写。接着,定义一个字符串,还是我们熟悉的hello vscode。这里要求将这个字符串中的每一个字符单独打印出来。并把其对应的asc码值同时打印出来。这个字符串一共有12个字符,空格也属于一个字符。按我们之前学过的内容是不是要这要写?

视频画面 05:06.7
视频画面 05:06.7 · 1080P 原图 · 查看对应视频

点击运行,是不是没有问题?这样写看上去有点太冗长了,那有没有更简单的写法呢?答案是有的,这里就引入我们这期视频要讲解的主要内容了。就是我们在视频开头提到的c语言的循环语句。在讲c语言的循环语句前,先讲一下vscode是如何实现程序调试的?调试在我们c语言的编写过程中是十分重要的。

视频画面 01:32.4
视频画面 01:32.4 · 1080P 原图 · 查看对应视频

它可以看到我们程序每一步的执行过程。好的,我们来设置一下。首先,点击这个图标。点击运行和调试。接着,点击创建launch.json文件这里。然后,点GDB这个,接着点击GDB启动这个选项。这时文件就自动生成了。这里有两个地方需要改一下。第一个地方是program这里,这里要填入的是工作区路径,也就是我们创建的test.c文件所在的路径。

第二个就是下面这个路径,就是前面视频配置环境变量那个路径。后面加一个gdb.exe。这里把反斜杠都写成两个,起到一个转义的作用,不然会报错。调试需要配置的文件我们就写好了,我也会把写好的json文件放到我们的资料包。供大家来参考。回到.c文件接着来写我们的循环语句。

首先,定义一个整数i,初始值设为0。先用whlie循环来写,这里的i++和 i=i+1等价。while小括号里的条件只要满足,大括号里的代码就会一直循环执行下去。这就像我们听音乐时设置的单曲循环。歌曲从第一个句歌词开始播放,一直到最后一句歌词。播放完成后接着从第一句歌词开始播放。

视频画面 03:19.5
视频画面 03:19.5 · 1080P 原图 · 查看对应视频

一直循环下去,除非手动停止。只不过在c语言中不是一句句的歌词,而是一行行的代码。接着,我们在序号前面点击这个小红点,给程序添加一个断点。断点的意思就是程序执行到这里就停下来,进入调试模式。接着,点这个绿色的小三角。在调试界面可以看到我们前面定义的字符串str和整数i。这里显示的字符串成员数是13,前面我们数的不是12吗?

我们点开看一下,是不是发现最后一位是asc码值为0的结束符?是不是和我们前面讲的数组那一期视频对上了?接着,我们点击单步调试,程序就开始一步一步执行了。从中我们可以看出,当i等于12时,while里的条件就不满足了。就跳出while循环了。接着,我们讲一下break和continue这两个关键字。

先说break,while中遇到break就跳出循环了,而continue只是跳过当次循环后面的代码,从头开始执行。break这就像单曲循环听烦了,直接关掉了,而continue只是听了一半后面的不想听了,从头开始再听。这样说大家可能听的云里雾里的,我们看代码。现在如果我们只想输出hello中的每个字符和对应的asc码值。

视频画面 06:00.3
视频画面 06:00.3 · 1080P 原图 · 查看对应视频

这是只需要加个判断就可以了,当数组成员为空格时。用break关键字跳出while循环。点击运行,是不是结果和我们想的一样?如果我们不想输出字符串中‘o’该怎么办呢?这里只需将判断里的空格换成字符o,将break换成continue就可以了。点击运行,是不是出问题了,程序卡死了?

视频画面 06:53.8
视频画面 06:53.8 · 1080P 原图 · 查看对应视频

我们点击停止,来调试代码。调试过程中我们不难发现,当判断成立时,执行到continue后面的代码就被跳过了。i++不会被执行,while小括号里的条件一直不会被满足。就进入一个死循环了。为了能得到我们想要的输出结果,只能将i++放到continue的前面。由于第一次循环时i就等于1了 ,而数组元素的索引是从零开始的。

所以,打印时要打印i-1。运行一下,是不是没有问题?这里还有一点要注意,在c语言中整数0代表假,所有非零的整数代表真,我们熟悉的while(1)中1为真,表示条件一直满足。除非遇到break才会跳出,这在c语言中也叫死循环。这期视频要比前几期c语言基础稍有难度。大家一定要动手写代码,听视频和自己写完全是两回事。

在自己写的过程中遇到问题边改边调试,从中能学到很多。好的,今天的视频就到这里。您的点赞、收藏、和关注是我更新下去的最大动力。我们下期见。

第16期 一节课讲明白c语言中的循环(下)

观看原视频

helllo,小伙伴们,大家好。这里是左左右。这期视频我们接着讲c语言基础中的循环语句。先来讲for循环。还是用上期视频用到的例子。它和whlie循环类似,也是小括号加大括号的结构。只不过for循环里小括号的内容要复杂些。它把while循环里的一些关键参数进行了整合。接着,我们来写for循环里的三要素:第一个是初始化,也就是i=0。

第二个是条件判断,i<12。第三个是迭代操作,就是我们熟悉的i++。点击运行输出是不是没有问题?接着,我们讲dowhile循环。这个循环属于先上车后检票的一种模式。就是说第一次循环不管条件是否满足,都要执行一次。第二次循环才开始判断while小括号里的条件。我们来写一下。

视频画面 01:06.2
视频画面 01:06.2 · 1080P 原图 · 查看对应视频

这里还有一点要注意。就是while小括号后面的分号不要忘了写。当i<12时,和刚才的for循环的结果是一样的。当i<0时,我们仍然可以输出字符h。就是说尽管while小括号里的条件不满足,do大括号里的代码还是执行了一遍。这里需要强调的是,break和continue两个关键字在这两种循环里也同样适用。

视频画面 02:08.1
视频画面 02:08.1 · 1080P 原图 · 查看对应视频
视频画面 02:39.0
视频画面 02:39.0 · 1080P 原图 · 查看对应视频

同时do while循环在日常代码编写过程中使用频率很低。如果认真听上期视频,这期视频是很容易理解的。好的,今天的视频就到这里。您的点赞、收藏、和关注是我持续更新下去的最大动力。我们下期见。

第17期 一节课将明白EXTI外部中断

观看原视频

helllo,小伙伴们,大家好。这里是左左右。这期视频我们讲EXTI外部中断。首先,讲一下什么是中断。中断是一种硬件机制,允许单片机在执行主程序的过程中,暂停当前任务,优先响应和处理某些紧急或特定事件,处理完成后自动恢复原任务继续执行。举个简单的例子,让大家理解起来更容易些。

假设你今天要休息,计划一整天都用来刷我的视频,中途你女朋友打来电话你总要接吧。这时你就暂停视频去接女朋友电话了。接电话就是一个中断,刷我的视频就是主程序中的任务。当你接完电话你还要从刚才视频暂停的地方继续刷吧,就相当于是处理完中断回到主程序继续执行原来的任务。中断的种类有很多,这么多的中断总有个轻重缓急。

所以,引出中断优先级这个概念。当有多个中断源同时申请中断时优先级决定了哪个中断会被优先处理,优先级中又包含了抢占优先级,和响应优先级。还是刚才的例子,这里的你假如是一个海王。在接女朋友电话过程中还有许多女性朋友打来电话,这里用甲乙丙来代替她们。在挂断女朋友电话后,她们在你心里总有个排名吧。

视频画面 01:11.2
视频画面 01:11.2 · 1080P 原图 · 查看对应视频

你就按这个排名按顺序给她们回了电话。这里的排名就是响应优先级。数字越小代表优先等级越高,还有一种情况是在你和女朋友打电话过程中,这时打来的电话中有一位比你女朋友更加重要。还没等你女朋友说完你就迅速挂断电话。接了这位在你看来更重要的电话,这时她的抢占优先级就高于你的女朋友。

视频画面 01:50.7
视频画面 01:50.7 · 1080P 原图 · 查看对应视频

当和她聊完,再回去给你女朋友打电话继续刚才没结束的对话。这就是中断的嵌套。这时抢占优先级和响应优先级就并不难理解了。“抢占”这个词更加强势。抢占优先级更高的是可以打断正在进行的中断,而“响应”这个词就有点随遇而安的感觉了。所以,它只能优先排队,而不能打断中断。假如你属于王中王级别的,有特别多的女性朋友。

关于他们的抢占优先级和响应优先级排名你也记不住。这时就产生了一个叫NVIC的小助手来管理中断。该挂断谁的电话,该优先回复谁的电话这些都由NVIC来做。你只管聊天就行。NVIC的中断优先级由优先级寄存器的四位决定。四位可以分为两组,分别代表抢占优先级和响应优先级。按之前我们学过的8421,四位二进制可以代表0-15。

视频画面 02:49.9
视频画面 02:49.9 · 1080P 原图 · 查看对应视频

三位二进制可以代表0-7,二位二进制可以代表0-3。一位二进制可以代表0-1。结合之前的视频,这个动画应该是很容易理解的。NVIC一共分为5组,这时里面的取值就一目了然了。为了让大家对抢占优先级和响应优先级有一个更准确的认知,我们还是用刚才这个例子,用NVIC分组2来对抢占优先级和响应优先级进行分组,也就是这两个优先级的取值只能是0到3。

值越小优先级就越高。抢占优先级(Preemption Priority)和响应优先级(Subpriority)的分组(Priority Grouping)通常建议只配置一次,虽然技术上可以实现修改,但不建议怎么做。

接下来我们将女朋友和甲乙丙来分配优先级。因为女朋友这个中断是先进来的,这时其他中断还没触发。接着,甲这个中断过来了,由于它的抢占优先级没有女朋友高。只能在后面排队。然后,是乙这个中断过来了,由于它的抢占优先级也没有女朋友高。也只能在后面排队。乙的抢占优先级和甲的相同,但是响应优先级要比甲高,虽然来得晚。

视频画面 04:28.6
视频画面 04:28.6 · 1080P 原图 · 查看对应视频

但排队还是排在甲的前面。最后,是丙的中断过来了,由于它的抢占优先级比女朋友高。它是可以打断女朋友这个中断的,所以,这几个中断的先后顺序就是这样的。其实,这里的海王就是我们的cpu。刷视频是我们主程序中的任务,而这些甲乙丙就是各种类型的中断。不知道这样讲是否有助于大家的理解?

我们接着讲EXTI(Extern Interrupt)外部中断,它是stm32众多中断之一,相当于stm32众多女性朋友之一。EXTI是指当指定GPIO口产生电平变化时,EXTI向NVIC申请中断。这里的电平变化是指中断触发的方式:包括上升沿/下降沿/双边沿/软件触发。

视频画面 05:27.8
视频画面 05:27.8 · 1080P 原图 · 查看对应视频

学过之前视频的小伙伴知道高低电平的变化是可以形成方波信号的。这里低电平转变为高电平就是上升沿。同理这里就是下降沿,上升沿和下降沿同时检测就是双边沿。软件触发不是我们这期视频的重点, 我们不做讲解。这里有一点需要注意。PA0,PB0,PC0只可以选择其中的一个作为中断引脚,也就是对应EXTI0这个中断。

就是说选择了PA0作为中断引脚就不能选PB0或者PC0了。同理PA1,PB1,PC1只可以选择其中的一个作为中断引脚,也就是EXTI1这个中断。以此类推。中断的触发方式包括中断响应和事件响应。这里的中断响应是通向我们的cpu的,而事件响应是通向其他外设,事件响应不是我们这期视频的重点。

大家有个了解就可以了。好的 今天的视频就到这里了。如果觉得视频讲的还不错就赏个赞吧。您的点赞、收藏、和关注是我持续更新下去的最大动力。我们下期见。

第18期 一节课讲明白c语言头文件

观看原视频

helllo,小伙伴们,大家好。这里是左左右。这期视频我们讲一下c语言中的头文件。讲视频前先设置一下我们的vscode。否则后面引用我们自己写的头文件会报错。大家改成我这样就可以了,很简单。打开test.c文件。这里我们假设已知圆的直径是10,求出圆的半径和面积。前面函数那期视频我们写过求半径的函数。

视频画面 00:47.8
视频画面 00:47.8 · 1080P 原图 · 查看对应视频
视频画面 01:41.5
视频画面 01:41.5 · 1080P 原图 · 查看对应视频
视频画面 04:40.4
视频画面 04:40.4 · 1080P 原图 · 查看对应视频

轻车熟路的再写上一遍。接着,写求面积的函数。由于求出的面积是有小数点的,所以,返回值我们用float这个数据类型。这里的Get_s函数定义时调用了前面定义好的Get_r函数。这就是C语言中函数的嵌套调用。我们接着往下写。点击运行是不是没有问题?只不过面积的小数有点多。我们在f前面加个.2就可以输出两位小数了。

目前来看这样写貌似也没有什么大问题。可是现实中完整的项目,我们不可能只有这几个函数。可能会有成百上千的函数。如果所有函数都写在test.c这一个文件中,我们到时候找main函数都费劲了。这样写很不利于代码的阅读。现实中我们写代码会将函数进行分类。然后,放到不同的c文件中。

我们尝试的做一下。先创建一个get_r.c的文件将get_r这个函数放进去。是不是报错了,这是因为test.c在调用get_r这个函数时迷路了?这时就引入头文件这个概念,头文件就起到一个导航的作用。我们接着尝试的去做一下。先创建一个get_r.h的文件。在这个文件中我们要声明函数。

视频画面 02:35.2
视频画面 02:35.2 · 1080P 原图 · 查看对应视频

就是把get_r.c中函数名都写到这里。这里函数的声明其实起到一个目录的作用。就是告诉引用这个头文件的地方。其对应的.c文件中有哪些函数。当然,正常的项目中,每个.c文件中都会定义很多同类型的函数。所以,对应.h文件中声明的函数也会有很多。回到test.c这个文件中把刚才写好的.h文件包含进去。

点击运行是不是就不报错了?同理,我们将get_s这个函数放到另外一个.c文件中。由于get_s这个函数中调用了get_r,所以,为了防止迷路,我们也要将get_r这个头文件包含进来。点击运行是不是也没有问题?其实,这样写是存在隐患的,我们在test.c和get_s.c中都包含了get_r这个头文件。

视频画面 04:22.5
视频画面 04:22.5 · 1080P 原图 · 查看对应视频

这样就会导致get_r.c中的函数被定义了两次,也就是重定义了。这里没有报错可能是因为vscode内部进行了处理。那该如何避免这种重定义呢?我们在所有.h文件中加入这3行代码。这里必须用大写而且要和对应.h文件名相同。这是头文件中的固定写法。意思就是如果没有定义这个头文件才去定义。

这就保证了同一项目中同一头文件只会被定义一次。接着,我们把所有头文件都这样写。点击运行,结果也是没有问题的,其实,使用头文件的写法是由诸多好处的。比如,你在其他项目中也要用到get_r这个函数,你只需要把get_r.和.h这两个文件复制过去就能用了。如果其他人也要用到这个函数,你把这两个文件给他发过去是不是也是可以直接使用了?

还有我们熟悉的printf这个函数,就是包含了stdio.h这个头文件就可以使用了。好的 今天的视频就到这里。您的点赞、收藏、和关注是我更新下去的最大动力。我们下期见。

第19期 一节课讲明白旋转编码器计次

观看原视频

helllo,小伙伴们,大家好。这里是左左右。这期视频我们要通过使用exti外部中断来实现编码器的计次功能。首先,打开串口屏的上位机来快速的做一下屏幕相关功能。记一下这里的字符编码是GB2312,后面会用到。现阶段我们不追求美观,只为实现功能。老样子,先创建一个字库。点击工具箱中的按钮,将按钮命名为本期视频的序号。

在按钮的按下事件中,输入 page page1,代表跳转到page1。接着,新建页面并打开。点击工具箱中的文本,将其命名为计数器。点击工具箱中的按钮,将其命名为返回。在按钮的按下事件中,输入 page page0,代表跳转到page0。点击调试来看一下,就是要实现这样一个功能。

接着,来讲一下旋转编码器的工作原理。首先,打开我们原理图中的旋转编码器。编码器在旋转过程中会通过AB引脚向外输出方波信号。通过原理图不难发现,旋转编码器的AB引脚分别和stm32的PA0和PA1相连。旋转编码器旋转一下,不管是A引脚还是B引脚都会形成一个周期的方波信号,也就是一高一低两个电平。

视频画面 02:17.7
视频画面 02:17.7 · 1080P 原图 · 查看对应视频

这时我们只需选择PA0和PA1中任意一个引脚作为EXTI外部中断。选择上升沿或者下降沿来触发中断。每次触发中断,计数器加1,这样就能实现计数的功能了。那么,如何判断正反转呢,其实AB引脚产生的方波信号是有相位差的?当旋转编码器正转或者反转时,B产生的方波信号会比A提前1/4个周期,或延迟1/4个周期。

这里的正转和反转其实是相对的,我们将其中一种命名正转,另外一种就命名为反转。这时我们将PA0和PA1中两个引脚作为EXTI外部中断,选择下降沿来触发中断,当EXTI0进入中断时,PA1输入高电平,计数器加 1,代表旋转编码器正转。当EXTI1进入中断时,PA0输入高电平,计数器减 1,代表旋转编码器反转。

原理这部分弄明白了,就打开我们的cubeide开始写代码吧。老规矩,把之前串口屏那期视频的项目复制一份出来。打开ioc文件,将PA0和PA1分别设置为EXTI0和EXTI1。接着,打开GPIO,在GPIO mode下拉菜单中可以看到中断响应和事件响应。这就是我们中断那期视频中讲的EXTI外部中断的响应方式。

视频画面 04:23.4
视频画面 04:23.4 · 1080P 原图 · 查看对应视频

这里的上升沿、下降沿、上升/下降沿。就是我们中断那期视频中讲的EXTI外部中断的触发方式。我们选择响应方式为中断响应,触发方式为下降沿。接着,点击NVIC,点击中断分组的下拉菜单。是不是和我们中断那期视频也是一模一样的?这里选择4位抢占优先级和0位响应优先级这个分组。接着,来设置一下他们的抢占优先级的值。

点击保存。打开main.c这个文件。随着项目越来越复杂,main.c中的函数越来越多了,听过上期视频的小伙伴们应该一下子就想到解决办法了。这里.c和.h文件在cubeide中只需要设置一下就可以自动生成了。接着,顺便把字符编码也设置成GB2312。打开自动生成的.c和.h文件。

是不是和我们上期视频自己写的头文件一模一样的?打开这个it.c文件。在里面找到EXTI0_IRQHandler和EXTI1_IRQHandler这两个函数,前面视频我们讲过,中断是一种硬件机制,也就是说EXTI0中断被触发后,硬件会让程序自动跳转到EXTI0_IRQHandler这个函数中。

视频画面 06:29.1
视频画面 06:29.1 · 1080P 原图 · 查看对应视频
视频画面 09:16.6
视频画面 09:16.6 · 1080P 原图 · 查看对应视频

所以,里面写我们想要实现的功能。这个函数是检查外部中断(EXTI)标志位的状态。这里判断如果EXTI0的外部中断标志位为真就执行大括号里的内容。接着,在这里写入我们刚才判断旋转方向的逻辑。

首先,判断PA1是不是高电平?如果是高电平,就是正转。我们执行count++。由于count这个变量还没有定义,我们在前面定义一下,大家看到int16_t是不是傻眼了?之前讲c语言数据类型可没讲过。可是在讲数据类型那一期视频我们讲过。定义数据类型的过程就是和内存要空间的过程。

这样可以有效提高内存空间的利用率。在c语言中int占32个bit,而stm32的内存是很小的,有时候要定义的数很小,要32个bit有点太浪费空间了。于是在就有了int8_t,int16_t,int32_t等等。其中int8_t占用8个比特的空间。可以代表2的8次方数,也就是0-255。

由于int8_t中也有负数,所以,通常用于存储范围在-128到127之间的整数。同理可以推出int16_t,int32_t。这里用到sprintf这个函数。所以,我们要包含stdio.h这个标准库。这里的sprintf的意思是将双引号里的内容赋值给str,而%d就代表count,和我们之前常用的printf类似。

由于%d代表的count是整数型,我们给他加个双引号转换成文本型。如果这里直接使用双引号,编译器就会误以为前面这个双引号是一部分。后面这个双引号是另一部分,就会报错。所以,我们要在引号前加个反斜杠,\"也是一个转义字符,他还是代表双引号。但是,不会被编译器误判。这里的\x后面代表十六进制数,这里是ff我们可以直接判断这是是16进制数。

如果是10呢,编译器也不知道你写的是二进制数,还是十进制数。又或者是16进制数。这里的\x也是一个转义字符。3个\xff是串口屏的结束符。它与uart那期视频讲的帧数据中结束符作用是一样的。假设这里的count是整数1,sprintf这个函数相当于完成了这样一行代码。

接下来写我们熟悉的发送数据到串口屏的函数。这个字符串对于串口屏来说。就是设置page1下的控件t0显示为变量count中的值。这里的strlen返回的是str这个字符串中字符的个数。想要用这个strlen就要包含string.h这个标准库。接着,往下看他这里自动生成的这个库函数代表什么意思?

这个的库函数就代表清除中断标志位,如果没有这行代码程序会一直卡在中断里,返回到刚才的位置。照猫画虎,把EXTI1_IRQHandler里的内容也补全了。这里的huart1在这个文件中并没有定义。为什么可以直接用呢?这是因为它在usart.c这个文件中定义过了。但是,就算是在这里定义过了也不可以跨文件使用。

要想跨文件使用就要在usart.h文件中将定义的huart1前面加个extern的关键字。这里cubeide已经帮我们写好了。这里的判断其实是一个双保险的写法。既然能进入EXTI1_IRQHandler这个函数。就说明产生了EXTI1的外部中断。

那么EXTI1的外部中断标志位就肯定为真。所以,这个判断就永远为真。这样写还有另外一个原因。假如我们用到的是PA11和PA12这两个引脚为外部中断引脚,点击保存自动生成代码。这时我们会发现EXTI10-15写在一个函数中了。刚才那个判断就有他的用武之地了。我们可以通过这个判断得知是哪个引脚触发了中断。

视频画面 11:22.3
视频画面 11:22.3 · 1080P 原图 · 查看对应视频

好的,我们把刚才设置的引脚恢复原状态。接着,我们回到主程序,这里也不能让主程序闲着,让它每秒钟执行一次i++的操作,也就是说程序正常状态下会在主程序中循环执行i++的操作。只有外部中断被触发了,才会由硬件自动跳转到对应的中断函数。

好的,我们来运行试一下。报了一些错,我们查看原因并解决。写代码的过程就是边调试边改的一个过程,大家要有耐心。动起手来,不动手永远学不会。连接好我们的串口屏虚拟机和仿真就可以实现这样的功能。大家可以观察一下示波器里的波形和我讲的是否一致?

同时,我把最小系统板的接线图也放到这里供大家参考。好的,今天的视频就到这里了。您的关注,点赞和收藏是我持续更新下去的最大动力。我们下期见。

第20期 一节课讲明白usart中断

观看原视频

helllo,小伙伴们,大家好。这里是左左右。前面中断那期视频我们讲过,stm32的中断种类有很多。其中USART串口通信也是支持中断功能的。打开cubeide,把之前串口屏那期视频的项目复制一份出来。前面讲USART串口通信那期,我们并没有使用其中断功能,而是直接将接收和发送函数放在主程序中的while循环中,而按下串口屏中的按钮是一个随机且短暂动作。

当stm32接收不到串口屏发送过来的数据时,接收函数只能停在那里一直等。这样主程序就不能执行其他任务了,就会造成主程序的阻塞。所以,开启USART串口通信的中断功能就可以完美的解决这个问题。

视频画面 01:38.4
视频画面 01:38.4 · 1080P 原图 · 查看对应视频

好的,我们来实操一下。打开ioc文件,找到NVIC。开启USART1的中断,并设置抢占优先级。同时,设置一下让它自动生成单独的.c和.h文件。打开main.c文件,将之前写好的UART接收数据的函数写到while循环外面。这时只要在原来函数后面加一个下划线IT就能使用USART的中断功能了。

接着,打开it.c这个文件找到我们熟悉的USART1_IRQHandler,也就是USART1发生中断时由硬件自动跳转到这个函数。这里和EXTI外部中断的跳转函数还是有一定区别的。由于数据的接收是有个过程的,我们直接把程序写在这里就可能会发生数据还没接收完成。你就直接开始判断接收到的内容了。

所以,这里我们就要引入回调函数这个概念了。它的核心作用是,让开发者自定义特定事件发生时的处理逻辑。这里我们就要写一个数据接收完成的回调函数,也就是说数据一旦接收完成,就自动执行我们写好的这个回调函数。其实,这个函数cubeide已经自动生成了。我们只需要重写一下就可以了。

视频画面 03:33.5
视频画面 03:33.5 · 1080P 原图 · 查看对应视频

按下ctrl,鼠标单击这个函数就跳到定义这个函数的地方。在这里我们找到这个函数,其中cplt就是complete的缩写。前面的下划线weak代表这是一个弱函数。就是说这个函数的允许重写。我们将它复制到main.c这个文件中,删掉前面的下划线weak。这时我们只需要将进入中断并完成数据接收任务后。

将要执行的代码写到这里就可以了。接着,在函数末尾加上前面的中断函数。这是因为如果不加这行代码,中断执行一次就结束了。所以,我们要在末尾重新开启中断。由于这个回调函数用到receive data这个变量,我们要把main函数中定义的receive data放到外面,让它成为一个全局变量,这样main.c中的其他函数才能使用它。

同时,主程序中while循环也不能让他闲着。还是让它每隔一秒执行i++的操作。好的,我们点击编译,是不是没有问题?如果前面视频听明白 了,这期应该是很容易理解的。由于这期视频相对容易,我们在这里补充一个知识点,其实,我们一直在cubeide中写的代码都是用串口屏的模拟器进行通信的。

那么,接下来讲一下如何将?串口屏上位机做好的UI界面下载到串口屏实物。并通过stm32与串口屏实物连接进行通信。由于串口屏模拟器和stm32通信用到的是uasrt1,而串口屏模实物和stm32通信用到的是uasrt2。所以,我们要在cubeide中设置一下。接着,回到main.c将之前用到的huart1全部改成huart2。

视频画面 05:28.7
视频画面 05:28.7 · 1080P 原图 · 查看对应视频
视频画面 06:26.3
视频画面 06:26.3 · 1080P 原图 · 查看对应视频

一共三处需要修改。接着,我们点击运行将程序下载到stm32中。接着,将学习板按视频中这样连接。点击下载就可以下载到串口屏实物了。然后,将串口屏与学习板另一个接口连在一起。就能实现串口屏实物与stm32通信了。接着,我将最小系统板接线图也放到这里供大家参考。回到刚才的cubeide中,大家有没有思考过这样一个问题:前面我们将3个huart1改成huart2。

如果这里我们有100个huart1需要修改,我们是不是也要改100遍?万一改的过程中还漏掉一两个该怎么办呢?其实,这里是有其他解决办法的,这时我们就要引入一个新的概念叫宏定义。他的写法是这样的,这里的ss_huart就是huart2的别名。就像高启强人们又叫他强哥是一个道理。

视频画面 08:21.4
视频画面 08:21.4 · 1080P 原图 · 查看对应视频

接着,我们将刚才那三个huart2都改成ss_huart。这样写的好处是,当我们要用串口屏模拟器和stm32通信时,就不用将文件中的所有huart2改回huart1了。我们只需要将宏定义这一个huart2改成huart1就行了。这样写是不是节约了很多时间,而且不还不容易出错?

点击编译试一下,是不是也没有问题?好的,今天的视频就到这里了。您的关注,点赞和收藏是我持续更新下去的最大动力。我们下期见。

第21期 一节课讲明白usart接收不定长数据

观看原视频

helllo,小伙伴们,大家好。这里是左左右。这期视频我们通过使用串口屏制作的串口助手。来实现不定长数据的接收。还是老样子,打开cubeide,将上期视频做好的项目复制一份下来。回忆下前面usart通信那期视频。大家有没有这样一个疑问?发送数据寄存器中的值是哪来的?同理,接收数据寄存器中的值去了哪里?

其实,发送数据寄存器中值是由cpu从内存中搬运而来,而接收数据寄存器中的值也是由cpu搬运到内存中去了。如果我们在某一个项目中要频繁的通过usart接收和发送数据,那么cpu是不是就会很忙碌?这时cpu就没时间去处理其他数据了。这里你可以把stm32理解成一家公司,而cpu就是这家公司的老板。

这就相当于让老板去搬砖。这样的公司还能有发展吗?所以,这种事要交给像我这样的牛马来做。那么,在stm32中有没有这样的牛马呢?答案是肯定的,它就是DMA。它是数据的搬运工,工作勤勤恳恳,替老板分忧。回到cubeide来设置一下。打开.ioc文件中的usart1,接着打开DMA Settings点击ADD。

添加接收和发送的DMA通道就可以了。接着,往下看,这里的Mode可选项有Normal和Circular两种模式。其中Circular通过字面意思不难猜出它是循环模式。他的特点的是循环往复,永不停止。这种模式我会在后面讲ADC数据采集时用到,而Normal模式DMA会在传输完成指定数据后停止。

视频画面 03:07.4
视频画面 03:07.4 · 1080P 原图 · 查看对应视频

正是我们USART通信要用到的模式。所以,这里我们选择Normal模式。由于我们将接收到的数据要存入连续的内存缓冲区,所以,地址递增这里就选择Memory。

另外,一个英文是外设的意思。这里用不到我们放到后面的视频讲解。好的 DMA的设置就到这里了。接着,回到main.c文件。这里只需要将uart接收和发送中断函数中的IT改为DMA就可以了。点击编译看一下,是不是也没有问题?前面我们接收的数据长度都是固定的,下面我们讲一下如何接收不定长数据?

视频画面 03:57.5
视频画面 03:57.5 · 1080P 原图 · 查看对应视频

先把之前的字符串定义的长一点。接收不定长数据我们用到的是HAL_UARTEx_ReceiveToIdle_DMA这个函数。空闲中断(IDLE)是 UART 通信中用于检测数据帧结束的核心硬件机制。特别适用于通信协议不固定或数据包长度变化的场景。这里有一点需要注意,这里的size不是接收到数据的实际长度,而是最大可接收数据的长度,这里的值需小于等于receive_data的字符串长度。

视频画面 06:27.8
视频画面 06:27.8 · 1080P 原图 · 查看对应视频

为了方便,我们直接用sizeof就可以了。这个函数的回调函数是这个RxEventCallback。我们找一下并重写。这里先判断一下进来的中断是不是uart1?接着,把从串口屏接收到的数据再发回串口屏。这里的size就用传参进来的Size,也就是接收到数据的实际长度。

视频画面 05:37.7
视频画面 05:37.7 · 1080P 原图 · 查看对应视频

最后,把DMA的接收中断再重新开启一下。点击编译,没有报错。点击运行把写好的代码下载到stm32中,打开串口屏模拟器。是不是就可以实现串口助手的功能了?使用它我们是不是随时可以找到任何字符对应16进制的asc码值了?我们输入a就可以得到对应16进制的asc码值。我们将maic.c中的huart1改为huart2。

并将串口助手下载到串口屏,输入hello。是不是stm32就实现不定长数据的接收了?我将做好的串口助手放到资料包了,免费提供给大家。需要的小伙伴们可以自行去取。由于这个串口助手涉及的串口屏知识太多,我们将在以后单独做一期视频讲解,还有就串口助手这个项目放到仿真软件达不到预期的效果。

所以,就没在这里给大家演示。仿真难免会有问题的,以后能实现的我都会给大家演示。没有演示的就说明仿真软件实现不了。好的,今天的视频就到这里了。您的关注,点赞和收藏是我持续更新下去的最大动力。

第22期 一节课讲明白C语言中的指针(上)

观看原视频

hello,小伙伴们,大家好。这里是左左右。这期视频给大家讲解一下c语言中的指针。学过c语言的小伙伴们都知道。指针算是c语言中的拦路虎了。很多人学到这里就开始犯糊涂了。之前视频中我们讲过内存的最小单位是比特(Bit)。8个bit组成一个字节。

那么cpu是怎么在庞大的内存中找到我们存入的数据呢?其实,每个字节都有它的地址。cpu就是通过地址找到我们存进去的值的。这就像你点外卖。外卖员是通过你填写的地址来找到你是一个道理的,而接下来要讲的指针,它里面存放的就是地址。打开vscode来试一下。

首先,定义一个int类型的变量a,将十六进制的数12345678赋值给变量a。接着,定义一个指向int类型的指针p1。这里的int *它是一个整体。* 代表指针。int代表我们定义的指针是指向int型的数据类型的。接着,我们把a的地址取出来存放到定义好的指针p1中。这里取地址我们用“与“(&)这个符号。

视频画面 01:19.4
视频画面 01:19.4 · 1080P 原图 · 查看对应视频
视频画面 05:04.2
视频画面 05:04.2 · 1080P 原图 · 查看对应视频

这行代码的意思就是将变量a的地址取出来放到指针p1中。刚才我们讲过内存每个字节都是有对应的地址的。这个地址在内存中是独一无二的,内存本来就是存放数据的容器。这里你可以这样理解。内存中的字节相当于是杯子,而每个杯子外面都印有编码。这个编码就是地址。这里我们定义了int类型的变量a。

由于int是四个字节的,相当于内存给了你四个杯子。至于杯子里要放什么那是你的事。你可以放水或牛奶又或者是咖啡或饮料。内存将来可以通过杯子外面的印的编码就知道杯子里装的是什么了。

视频画面 02:04.4
视频画面 02:04.4 · 1080P 原图 · 查看对应视频

接下来我们通过a的地址找到存在里面的值并打印出来。到这里大家是不是已经开始糊涂了?其实,这里的 * 是取内容的意思。就是把存放在指针p1对应地址中的值给取出来。因为p1里存放的是a的地址。所以,点击运行。这里就输出16进制的12345678。这种类似的我们之前数组那期视频是遇到过的。

比如,我们定义一个数组。当前面有数据类型时,方括号里的值就代表定义数组的成员个数。而前面没有数据类型时,方括号里的值就代表数组的成员序号,而*前有数据类型就是定义指针。*前没有数据类型就是取出指针对应地址的值。用相同的方法来定义一个char类型的变量。

同时,我们把指针中的值和占用多少内存打印出来看一下。这里的结果都是8。就是说不管你定义的是int*还是char*的指针,指针占用的内存空间都是8个字节的,因为指针是用来存放地址的,说明地址是8个字节的,那为什么地址是8个字节的呢?因为我的电脑是64位的,这里的位指的是比特(bit),也就是8字节。

视频画面 03:34.3
视频画面 03:34.3 · 1080P 原图 · 查看对应视频

同理,如果你的电脑是32位的,地址大小就是4个字节的。接着,详细讲解一下每行代码的执行过程过程。前面我们一直在强调。定义数据类型就是向内存要空间的过程。这里我们定义了int a。系统就分配给我们4个字节的空间。并将16进制的12345678放到系统给我们分配的空间中去。

接着,定义指针p1。系统给我们分配8个字节的空间。接着,看下一行代码。前面我们讲过内存中的每个字节都有其对应的地址。int数据类型是4个字节的,也就是有4个地址,而指针只能存储一个地址。所以,指针p1里存储的是int a的第一个地址,也就是首地址。这里有一个要点,大家一定要明白。

地址是内存中字节的编号,它是一直存在的。就像刻在内存中一样。他不会因为指针的存在而存在。也不会因为指针的消失而消失,而我们定义指针只不过是把拿到的地址存在这个指针里而已。这就像你家的地址是真是存在的,而外卖员只不过是把你的地址存在他手机里是一个道理。就像水杯外印的编号,不会因为你把水喝掉而消失。

这里你可以把指针理解为一种特殊的数据类型。它是专门用来存储地址的,指针的其他功能我会接着讲的。这行代码就是将p1对应地址中的值取出来。刚才我们讲过指针存放的是首地址,而int a是四个字节的,它是跨越4个地址的。指针是怎么将四个地址中的值同时取出来的呢?这时我们定义指针时前面的int就起到关键作用了。

视频画面 05:49.1
视频画面 05:49.1 · 1080P 原图 · 查看对应视频

这里的int就是告诉指针在取值时要从四个相邻地址去取值。这里只是我自己的个人想法。我们来验证一下这个想法,我们将这里的int改为char。我们都知道char类型是一个字节的,我们来输出看一下。这里虽然报错,但它还是输出了78。因为我们前面定义指针时,用的是char这个数据类型。

就是说这时的指针认为它取一个字节地址中的内容就可以了。接着,把char改为short。short是两个字节的,这是我们打印输出看一下。这个结果是我们意料之中的对不对?这时的指针认为它把和首地址相邻的一个地址的内容取出来就可以了。只有我们理解到这一步,你才能彻底理解为什么要这样定义指针?

要不然你总是糊里糊涂的,我将这节课的主要内容总结到这里。大家一定要去记,去理解,这期视频对以后指针的学习至关重要。由于指针内容比较多,我会分三到四期给大家讲解,好的,今天的视频就到这里了。您的关注,点赞和收藏是我持续更新下去的最大动力。我们下期见。

接着,我们定义一个int类型的数组、接着定义指针p3。数组是比较特殊的,数组名本身就是指针。所以,这里不需要&这个符号。接着,我们把指针里的内容打印出来看一下。他输出的是数组中的第一个数。

那么,第二个数该如何输出呢?只需要指针加一就可以了。打印是不是就输出第二个数了?这里我们还发现指针的另一个功能就是自动跳转的功能。指针加1就可以输出数组中的第二个数。接着,通过动画来演示一下这段代码的执行过程。同理我们把int换成char和short。是不是指针的跳转就发生错误了?

因为数组名本身就是指针。我们前面这种做法是只是方便大家学习。正常写这部分就不要了,属于多此一举。这里我们有没有发现这里的输出和我们前面数组视频讲过的。通过下标输出是一模一样的,我们来写的试一下。是不是说明这两种写法是等效的?

第23期 一节课讲明白C语言中的指针(中①)

观看原视频

helllo,小伙伴们,大家好。这里是左左右。这期视频给大家讲解一下c语言中的指针和数组。大家首先来思考这样一个问题:如何通过指针来分别输出数组中的各个元素?我来给大家写一下。看和你想的一样不?

首先,打开vscode来定义一个short类型的数组。接着,定义指针变量p1。然后,把arr数组中第一个元素的首地址取出来。这时我们取指针变量p1中的内容就得到数组中第一个元素的值。

同时,将指针变量p1内存储的地址和占用内存大小分别打印出来。同理,我们将数组中剩下的元素也都打印出来。点击运行,打印输出是不是没有问题?回过头来,将每行代码仔细分析一下。

首先,来看第一行代码。由于数据类型short是两个字节的,这里我们定义的数组成员数是3个。所以,内存给我们分配了6个字节的空间。同时,把值放入到内存中。接着,下一行代码我们定义了指针p1。向内存要了8个字节的空间用来存放地址。紧接着将arr数组中第一个元素的首地址存入p1。

这时打印输出的第一个值就是指针p1中对应地址的值。指针说白了就是通过庙来找和尚。第二个值是指针变量p1里存放的地址。第三个是指针占用内存的大小,也就是8个字节。同理,p2,p3都是一样的,在这里就不赘述了。

那么,这里有没有简单的写法呢 答案肯定是有的。我们一般也不这样写 前面这样写是方便大家理解。这里一长串可以用数组名直接来代替。打印输出来看一下,是不是也没有问题?写到这里很多老师会说因为p1是指针。p1等于arr,所以数组名也是指针。在这里你可以把它理解为指针,但这样的说法是不准确的。

视频画面 03:12.9
视频画面 03:12.9 · 1080P 原图 · 查看对应视频

这个很好推翻的,我们把arr用sizeof打印出来。如果arr是指针,这里打印输出应该是8才对。但这里输出的并不是指针的大小,而是数组的大小。short的数据类型是2个字节的,成员数是3。所以,输出结果是6。通过这里就可以证明数组名并不等同于指针。

接下来这里可以用arr+1来代替。这里用arr+2代替。打印输出来看一下是不是也没有问题?通过这里我们发现指针是可以执行加减运算的。同时,也得出指针的另一个功能。就是跳转功能。当arr+1时,指针跳转到数组第二个成员的首地址。同理。当arr+2时,指针跳转到数组第三个成员的首地址。

这里的加1、加2其实代表的是指针跳转的步数。加1代表跳转1步,加2代表跳转两步。这里指针每次跳转是跳转了2个字节。为什么不是1个字节呢?这是因为我们定义数组时前面数据类型是short,而short就是两个字节的,我们把指针每次跳转的字节数称之为步幅。定义数组时前面的数据就决定了步幅的大小。

视频画面 04:33.4
视频画面 04:33.4 · 1080P 原图 · 查看对应视频

我们来验证一下,将定义数组时前面的short改为int。调试输出可以看出,每次指针跳转的步幅就变成4了。学到这里就可以总结出指针的3大功能了。一、指针是一种特殊的变量,它是用来存放首地址的。二、指针具有指向内存空间功能。指向内存的大小是由定义指针时前面的数据类型决定的。

三、指针具有跳转功能。跳转步数是由指针执行加减运算时的值决定的。步幅是由定义数组时的数据类型决定的,这里我们还可以用for循环将数组成员遍历出来。这里的数组成员数我们可以通过,数组占用空间的大小除以成员占用空间的大小来算出来。调试输出来看一下,是不是和之前一样?这里也可以得出这样一个结论。

*arr和arr[0]等效,*(arr+1)和arr[1]等效。*(arr+2)和arr[2]等效。接下来,出一道思考题,这里的打印输出的值等于多少。大家可以通过弹幕的方式把答案发出来。

同时,我也会将答案放到评论区。这期视频如果听不懂的小伙伴们可以先把上期视频听一遍。下期视频会更新指针与数组名的区别与联系。好的,今天的视频就到这里了。您的关注,点赞和收藏是我持续更新下去的最大动力。我们下期见。

这里的p1等于arr和1等于1没啥区别。属于多此一举,我们直接用arr就可以。那么,数组名和指针到底有什么区别和联系呢?为了方便大家学习,我列了这样一个表格,指针的本质我们是知道的,指针是一种特殊变量,他是用来存储地址的。数组名是一种标识符,代表一块连续内存。说白了数组名就是数组的名称而已。

只不过c语言赋予了它一些特殊的功能而已。指针内存占用只和电脑位数有关,64位占8字节。32位占4字节,而数组名是没有独立存储空间的,数组名只是给名称而已。内存是没有单独开辟空间给数组名的,sizeof这里指针返回的是指针本身的大小,而数组名返回的是数组本身的大小。这个刚才讲过的,大家应该一看就明白了。

之前我们讲过内存中的每个字节都是有地址的。指针是占了8个字节的内存的,所以,我们给指针取地址得到的就是指针的首地址,而给数组名取地址得到的是首元素的首地址。可赋值性这里指针本身就是变量。它里面的值当然是可以改的,而数组名是不能重新赋值的,访问元素和指针运算这里刚才是讲过的,大家看一下就可以了。

最后,一个要给大家详细讲一下。

第24期 一节课讲明白c语言指针(中②)

观看原视频

hello,小伙伴们,大家好。这里是左左右。这期视频接着给大家讲C语言基础中指针这一部分。讲这期视频前先问大家这样一个问题:指针和数组名是否等价?这时好多小伙伴们就要说了。数组名就是指针,我们老师就是这样讲的。也一直这样用的,肯定没有问题。但是,如果听过上期视频的小伙们就知道。

数组名就是指针这样的说法是不准确的,那么,数组名和指针之间到底有什么关系呢?通过这期视频就让大家彻底弄明白。首先,打开vscode的来写这样一段代码。这里%p就是专门来输出指针地址的,调试输出来看一下。这里虽然报错了。但可以看到三个输出的结果是一模一样的。看到这里,对于刚学习C语言或者C语言基础比较薄弱的小伙伴们来说。

视频画面 00:35.8
视频画面 00:35.8 · 1080P 原图 · 查看对应视频

是不是心态都崩了?对于听过我前两期视频的小伙伴们很容易判断出这里第一个和第三个是等效的。他们都是指向数组中第一个元素的首地址的。那么,给数组名取地址为什么输出的也是这个地址值呢?他们三个是不是等效的?带着这个疑问我们再来写这样一段代码。调试输出来看一下。这里的指针变量p1和p2是我们熟悉的。

这里的p3代表什么什么呢?而且它也报错了。接下来我们详细分析一下这段代码。首先,我们定义int类型变量a,并赋值。同理定义char类型变量b,并赋值。定义short类型的数组,并赋值。接着,定义指针变量p1,通过取地址符(&)把int a的首地址存入指针p1中,而且指针p1是指向整个变量a占用的内存空间的。

同理定义指针变量p2。这时的指针p2存放的就是char b的首地址,而且指针p2是指向整个变量b占用的内存空间的。同理定义指针变量p3。这时的指针p3存放的就是arr的首地址,而且指针p3是指向整个数组arr占用的内存空间的。这时我们返回来看这里定义指针变量p3时,用的是short *。

视频画面 02:58.5
视频画面 02:58.5 · 1080P 原图 · 查看对应视频

它是指向短整型(short)的指针和前面定义的int *一样。它不能代表指向数组的指针。那我们应该如何定义指向数组的指针呢?这时我们就要引出一个专业名词了 它就是数组指针,也就是指向数组的指针。他的固定写法是这样的(数据类型 (*指针变量名)[数组长度];)。所以,照猫画虎 这里的指针p3要这样定义。

视频画面 03:46.1
视频画面 03:46.1 · 1080P 原图 · 查看对应视频

点击运行,是不是就没有报错了?接着,回到我们刚开始写的第一段代码。按照数组指针的写法把p2改一下。点击运行,是不是就没有报错了?通过这部分内容的学习。我们是不是发现这时的数组名arr和变量名a,和b都是一样的?数组名只不过是数组的标识符而已。只不过C语言赋予数组名一部分指针的功能而已。

这个也从另一个方向验证了数组名并不等于指针。结合前两期视频我们得出这样一个结论。当给数组名取地址时 它存放的是数组的首地址,它指向的是整个数组。当把数组名单独拿出来时, 它存放的是数组中第一个元素的首地址。它指向的是数组中的第一个元素。它与数组首元素取地址等效。所以,他两输出的结果虽然相同,但指向是大不相同的。

接着,我们写这样一段代码。这样写是不是看起来有点太啰嗦了?那有没有更高效的写法呢?答案肯定是有的,我们可以将所有指针放到一个数组。如果我们这样来定义就与普通数组的定义方法一样了。无法作区分了,所以要在int后面加个*号。代表我们定义数组成员是指针类型的,我们将这样的数组称之为指针数组。

我们要能把前面讲的数组指针区分出来。其中数组指针的重点是指针二字,他代表指向数组的指针,而指针数组的重点是数组二字,他代表由指针组成的数组。讲到这里我将数组名和指针的区别与联系。总结了一张表格在这里供大家参考。其中我把这一排单独拿出来给大家讲一下。我们写这样一段大家熟悉的代码。

点击运行,没有任何问题。这时我们将指针p+1改为p++。点击运行,也没有问题。但当我们将数组名arr+1改为arr++。点击运行,就报错了,说明在C语言中数组名++ 的写法是不合法的。大家不要这样写。总结这个表格的目的就是为了大家在以后遇到数组名和指针的相关问题解决不了时。

返回来看一下,找找灵感,还有这里最后一行,知识点比较多,我们将在下期视频给大家展开来讲解。老样子,习题时间。当我把这里的注释取消时,输出的结果是什么?大家可以通过弹幕的方式把答案发出来。

同时,我也会将答案放到评论区。这期视频如果听不懂的小伙伴们可以先前面两期视频听一遍。好的,今天的视频就到这里了。您的关注,点赞和收藏是我持续更新下去的最大动力。我们下期见。

第25期 一节课讲明白c语言指针(中③)

观看原视频

hello,小伙伴们,大家好。这里是左左右。这期视频接着给大家讲C语言基础中指针这一部分。首先,我来写一段大家熟悉的代码。这时使用%s就可以将完整的字符串打印出来了。我们返回来看一下这段代码。

那么,这两行代码是不是可以合并为一行呢?答案是可以的,打印输出,是不是结果和之前是一样的?接着,我们返回来看一下这行代码。它的等号一边是指针,而另一边是字符串。两边怎么会等效呢?其实c语言内部在运行到这行代码时。默认指针仍然存放的是字符串中第一个元素的首地址。我们打印输出来验证一下我的说法。

这时输出的结果是不是就是数组中的第一个元素?这时有的小伙伴就要问了。第二种写法比第一种明显要简单明了。那么,我们以后能不能就按第二种写法来写呢?答案是不可以的,他两还是有区别的,这时假设我们要将字符串arr1中的第一个元素改为f。这时打印输出是不是也没有问题?我们要将第二种写法中的第一个元素改为f。

这时打印输出是不是就出问题了?这是因为第二种写法c语言默认会将字符串放到内存的只读区域,也就是说这时的字符串hello vscode它是只读的,是不允许被更改的。为了防止被更改,我们可以在前面加一个const 的关键词。这个关键词的作用就是用来声明对象是不可以被修改。这时我们打印输出是不是有报错提示了?

视频画面 00:34.7
视频画面 00:34.7 · 1080P 原图 · 查看对应视频
视频画面 02:51.2
视频画面 02:51.2 · 1080P 原图 · 查看对应视频

我们在讲结构体那一期视频时挖了一个指针的坑。现在我们就把之前挖的坑给填上。我们返回来看当时的那段代码是不是就明白我为什么要定义成指针类型的了?那么,我们把这里的指针定义成字符串类型的可以吗?当然,是可以的,点击运行发现报错了,这是因为数组名是不可以直接赋值的。我们回到刚才的代码给数组名直接赋值,是不是报了相同的错误?

这时我们就要用到strcpy这个函数了。同时,我们要将string.h的头文件引用过来。点击运行是不是就没有问题了?同理我们给结构体赋值。点击运行是不是也没有问题了?这时name中的字符串是可以更改的,这里我们将强哥改为左左右。打印输出是不是也没有问题?这时如果我们要输入的学生姓名是这样的呢(胡柏·布里恩·沃夫斯里积士丁可辛贝格多夫)?

这时就超出定义字符串时向内存要的十字节的空间。这时点击运行,程序就出问题。那我们将定义字符串时向内存要的空间多一点不就行了吗?但当我们输入短的字符串时,就造成大量空间的浪费。这时我们刚才指针的写法就凸显出它的优点了。不管名字长或短,输出都没有问题,也就是说指针的写法,内存空间是自动分配的。

不用我们操心。这时我们将st1的姓名再改回强哥。调试输出是不是也没有问题?这时有的小伙伴就要疑惑了。刚才不是说数组名不是不能直接赋值吗?这里为什么没有报错?其实,在结构体中我们定义的name它是一个指针,而非数组名。指针一开始指向的内存只读区中这一长串字符。后来指向的是强哥这个字符串,也就是说指针中储存的地址值发生变化了。

指针本身就是一种变量,所以它里面的地址值是可以被重新赋值的。今天这期视频就解释了上期视频这个表格中最后一行的内容了。好的习题时间。大家可以通过弹幕的方式把答案发出来。

同时,我也会将答案放到评论区。这期视频如果听不懂的小伙伴们可以先前面三期视频听一遍。好的,今天的视频就到这里了。您的关注,点赞和收藏是我持续更新下去的最大动力。我们下期见。

typedef struct{
    int id;
    char *name;
    int age;

}Student;
    Student st1;
    st1.id = 2025001;

st1.name = "高启强";

    st1.age = 17;
    printf("学生1的学号是:%d,学生1的姓名是:%s,学生1的年龄是:%d",st1.id,st1.name,st1.age);
视频画面 04:22.1
视频画面 04:22.1 · 1080P 原图 · 查看对应视频
视频画面 05:53.1
视频画面 05:53.1 · 1080P 原图 · 查看对应视频

第26期 一节课讲明白c语言指针(中④)

观看原视频

hello,小伙伴们,大家好。这里是左左右。这期视频接着给大家讲C语言基础中指针这一部分。在函数那期视频我们写过这样一个Get_R的函数。前面我们讲过给数组名取地址得到的是数组指针。

那么,如果我们给函数名取地址是不是也可以得到函数的指针呢?通过代码来验证一下。点击运行。这时就将函数指针的地址打印出来了。那么,我们应该如何定义一个函数指针呢?也就是要用一个什么样的容器来存放函数指针呢?我们将之前定义好的函数复制下来,将函数名Get_R改为括号*p,将传参过来的变量名去掉。

这里我们就将函数指针定义好了。这时的变量p就是指向函数Get_R的指针。同时,函数名取地址和函数名是等效的,调试输出来看一下,是不是输出结果一模一样?那么,函数指针该怎么使用呢?

视频画面 01:33.5
视频画面 01:33.5 · 1080P 原图 · 查看对应视频

首先,将函数指针解引用得到函数本身,接着将需要的参数传进来。打印输出是不是没有问题?其实,指针函数前的*在c语言中是允许被省略的。打印输出是不是也没有问题?按照这样的逻辑,给结构体名取地址是不是得到的就是结构体指针?我们通过代码来验证一下。

视频画面 02:27.8
视频画面 02:27.8 · 1080P 原图 · 查看对应视频

首先,我们定义一个结构体变量并通过大括号的形式给结构体成员赋值。接着,打印输出。就将结构体指针的地址打印出来了。接着,要用一个什么样的容器来存放结构体指针呢?它和int等基础数据类型指针的定义类似。前面放的是结构体名。

那么,如何通过结构体指针来给结构体成员赋值呢?这里我们要用结构体指针加减号,大于号,加结构体成员名的形式。减号和大于号组成一个箭头,代表指针指向结构体成员。这时打印输出看一下结果,是不是和我们想的一模一样?通过这里我们可以得出这样一个结论。通过结构体名访问或修改结构体成员要用“.”的形式,而通过结构体指针访问或修改结构体成员要用“->”的形式。

这里假设我们有这样一种需求:通过只调用一个函数实现打印输出“hello vscode”或“hello stm32cubeide”或“hello 左左右”。也就是说hello是不变的,后面的内容是可变的。按我们之前学过的内容,我们只能这样写。这时只能输出一种结果,也就是hello后面的内容是不受控的。

这时我们将前面的一个函数拆分为两个。那么,我们该如何将这两个函数关联起来呢?我们刚才讲的函数指针就派上用场了。这时我们将Print_vs这个函数以函数指针的形式传参到Print_h中。再在Print_h中以函数指针的形式来调用Print_vs这个函数。接着,我们在main函数中将Print_vs以函数指针的形式传参到Print_h中。

视频画面 06:05.1
视频画面 06:05.1 · 1080P 原图 · 查看对应视频

点击运行,是不是输出没有问题?这段代码我们还可以简化一下。前面我们说过函数名和函数名取地址等效。我们将这里的取地址符可以去掉。我们可以用typedef。用一个简单的名称来替代这里的函数指针,也就是说这时的函数指针和CallbackFunc等效。接着,我们将这里的函数指针改为CallbackFunc。

同时,前面我们说过函数指针前的*是可以省略掉的。所以,这里我们可以写成这样。点击运行,是不是输出没有问题?接着,我们将剩下几个回调函数也照猫画虎的写一下。点击运行,这时我们是不是就可以随心所欲的输出我们想要的结果了?

那么,回调函数有什么作用呢?如果是我们自己写的回调函数,是不是可以通过不同的回调函数,实现不同的功能。如果我们调用别人的回调函数我们不用考虑Print_h里这一部分有多么复杂,现实中hal库源码中这一部分写的也是十分复杂。我这样简单的写是为了大家能够理解回调函数的实现原理。

视频画面 07:53.7
视频画面 07:53.7 · 1080P 原图 · 查看对应视频

我们在调用时,不用考虑这部分有多么复杂。我们只需要重写回调函数这一部分就可以了。这就是回调函数的现实意义。我们将之前stm3cubeide中写好的代码再拿出来看一下。当时我们使用这个函数HAL_UART_Receive_IT时,只需与知道他是usart中断接收数据的一个函数。

具体他是怎么接收的过程我们不需要知道?我们只需要重写HAL_UART_RxCpltCallback这个回调函数来实现我们想要的功能就可以了。好的,今天的视频就到这里了。您的关注,点赞和收藏是我持续更新下去的最大动力。我们下期见。

第27期 一节课讲明白C语言中的指针(下①)

观看原视频

hello,小伙伴们,大家好。这里是左左右。通过前面几期视频的学习。大家心中可能都有这样一个疑惑。就是总感觉指针有一种多此一举的感觉。那么,这期视频我们就通过指针应用的实际案例来帮大家解解心中的疑惑。假如现在有这样一种需求:需要通过调用函数将两个整形变量中的值互换。这里首先定义两个变量。

视频画面 00:33.8
视频画面 00:33.8 · 1080P 原图 · 查看对应视频
视频画面 05:39.6
视频画面 05:39.6 · 1080P 原图 · 查看对应视频

接着,我们来定义一个函数。那么,如何将两个变量中的值互换呢?这就好比两个杯中分别装有牛奶和可乐。那么,如何将牛奶和可乐互换呢?这时我们需要找到一个空杯子,将牛奶杯中的牛奶倒入空杯。接着,将可乐杯中的可乐倒入原来装牛奶的杯中。

最后,将空杯中的牛奶倒入原来装可乐的杯中。这样就完成了牛奶与可乐的互换了。所以,照猫画虎来写一下这个函数。首先,定义一个变量名为空的变量,将变量a中的值赋值到这个变量。接着,将变量b中的值赋值到变量a。

最后,将空变量中的值赋值到变量b。这样就实现了变量a和b中值的互换。我们调用写好的函数,调试输出看一下结果。这时输出的结果和我们预想的并不一样。变量x和y中值未能实现互换。这是为什么呢?带着这样的疑问重新来写一下刚才的函数。我们将之前的变量a和b改为指针p1和p2。接着,将指针中地址对应的值进行互换。

视频画面 02:01.2
视频画面 02:01.2 · 1080P 原图 · 查看对应视频

这时我们再次调用写好的函数,调试输出看一下结果。这时输出的结果和我们预想的一模一样。变量x和y中值实现互换。那么,这两个函数到底有什么样本质的区别呢?通过前面几期指针的学习,我想大家已经有了一个大概的思路了。接着,我们通过大家熟悉的动画来分析一下这两个函数的。

视频画面 03:28.6
视频画面 03:28.6 · 1080P 原图 · 查看对应视频

首先,在内存中存入5和10这两个值。这里我们将之前的动画做了简化。接着,调用swap这个函数。这里看上去虽然传入的是x,y这两个变量。其实,在c语言中,这时传入的只是xy这两个变量中的值。c语言会新建两个变量来存放5和10这两个值。这时互换的只是新建的这两个变量中的值。

与x,y这两个变量无关,而且随着函数调用的结束。这两个新建的变量也会消失。这两个变量就是我们常说的局部变量。空口无凭。我们来验证一下,首先将变量x和y的地址打印输出一下。接着,把swap函数中的a和b的地址打印输出来看一下。这时上下两行的输出结果是不一样的,这也就验证了调用swap这个函数传入的参数是变量中的值。

如果传入是变量x和y本身,那么,上下两行的输出结果就会一模一样的。接下来分析一下带指针的这个函数。调用这个函数是将x的地址传入到指针p1。y的地址传入到指针p2。接着,我们通过解引用p1和p2。直接操作变量xy中的值,也就是我们常说的通过庙来找和尚。这时我们不止通过庙找到了和尚,还把庙里的和尚给换了。

随着函数调用的结束虽然指针p1和p2也会消失。但这时指针已经完成 了他们的使命。已经把变量x和y中值进行了互换。是不是通过这个案例,我们对指针的应用有了更加深入的了解,希望大家有所收获?

好的,今天的视频就到这里了。您的关注,点赞和收藏是我持续更新下去的最大动力。我们下期见。

第28期 一节课讲明白c语言指针(下②)

观看原视频

hello,小伙伴们,大家好。这里是左左右。这期视频我们接着讲指针的应用。前面视频我们写过一个通过直径计算半径的函数。快速的来写一下。打印输出来看一下结果是不是没有问题?接着,我们用指针的形式来重新写一下这个函数。打印输出来看一下结果是不是也没有问题?通过上期视频的学习我们知道第一种函数的写法。

视频画面 01:58.8
视频画面 01:58.8 · 1080P 原图 · 查看对应视频

它是新建了一个局部变量用来存放变量d1中的值,也就是说在函数中我们改变变量的值也只是改变局部变量中的值。对d1本身是没有任何影响的,接着,我们写代码来验证一下。点击运行。d1的值是没有发生任何变化。接着,我们来看第二种写法。通过上期视频的学习我们知道指针是通过地址来直接操作变量d1中的值。

我们写代码来验证一下。点击运行。d1的值是不是发生了改变?第二种写法省去了再次定义一个新的局部变量,而是通过指针的形式直接操作变量d1。这样就提高了代码执行的效率。但是,第二种写法存在一个什么样的问题呢?由于它是通过地址来直接操作变量d1的,他会导致d1本身的值发生变化。

当其他函数使用d1时,这时d1中的值就是20了,而不是10了,这样就很混乱了。为了防止这种情况的发生,我们经常在指针前面加一个const的关键词。这时在函数中改变指针对应地址中的值时就会报错。通过前面几期视频的学习我们知道。函数是可以通过return来产生一个返回值的。

视频画面 04:07.1
视频画面 04:07.1 · 1080P 原图 · 查看对应视频

但是,如果我们需要多个返回值应该怎么办呢?假设我们现在有这样一个需求,通过调用函数返回圆的半径,周长和面积。首先,我们在主函数中定义变量c和s分别代表周长和面积并初始化。由于周长和面积是存在小数点的,所以,数据类型选择float。接着,我们把浮点型变量c和s的地址传入函数。

视频画面 04:49.8
视频画面 04:49.8 · 1080P 原图 · 查看对应视频
视频画面 06:15.3
视频画面 06:15.3 · 1080P 原图 · 查看对应视频

由于传参传入的是地址,所以,我们在函数这里定义指针cp和sp。接着,给指针cp对应地址中的值赋值,也就是给变量c赋值。然后,给指针sp对应地址中的值赋值,也就是给变量s赋值。这里我们就通过指针的形式直接改变了变量c和s中值。也就实现了一个函数有多个返回值的效果了。点击运行。

输出的正是我们想要的结果。通过这个案例我们得到这样一个结论。就是说我们不想改变变量本身的值时。就在前面加一个const的关键词。当想要改变的变量本身的值时,就不需要const这个关键词。c语言基础的讲解暂时也就告一段落了。后面如果有大的c语言知识点。我仍然也会单独拿出来给大家进行讲解。

好的,今天的视频就到这里了。您的关注,点赞和收藏是我持续更新下去的最大动力。我们下期见。

第29期 如何通过串口助手实现printf函数

观看原视频

hello,小伙伴们,大家好。这里是左左右。在前面的视频中我们会频繁使用printf这个函数。它对我们调试和验证代码起到至关重要的作用。那么,在stm32中结合学习板和串口屏也是可以实现这样的功能的。

首先,我们打开串口屏软件。点击新建。这里随便起个名字就叫调试输出吧。接着,选择串口屏型号。导入我们之前创建好的字库。接着,导入两张图片,一张是清晰的,一张是模糊的。这是我用PS做的两张图,我将它们放到了我们的免费资料包中。对PS感兴趣的小伙伴们也可以自己去做。之后我们将主界面改为这张清晰的图片。

接下来我们创建一个按钮。和图标大小相同。点击新建好的按钮。在按钮的属性栏中将按钮上的文字删掉,在这里选择切图,分别在picc和picc2中放入清晰和模糊的图片。这样能达到一个什么样的效果呢?当按钮被按下时,按钮这一部分就会切换成模糊的图片。待会在调试过程中我们再来看实际的效果。

接着,点击这里的加号来新增一个页面。放入一个文本控件和两个按钮控件。其中一个按钮控件来实现清空数据的功能。另一个按钮控件来实现返回的功能。接着,我们将文本控件放大一些。这个文本控件就是用来显示我们调试输出内容的。在返回按钮的弹起事件中让它跳回page0,同理在调试这个图标按钮的弹起事件中让它跳转到page1。

随后在清空数据按钮的弹起事件中让它实现清空文本t0中的数据,也就是让t0中的内容为空,所以这里输入t0.txt等于空。这里有两点需要注意。等号两边不能有空格,末尾不能有分号。点击编译。这里0错误和0警告,说明我们前面的操作没有任何问题的。接着,点击调试。这里按下调试按钮,调试图标就变模糊了。

视频画面 03:25.2
视频画面 03:25.2 · 1080P 原图 · 查看对应视频

同时,在放开按钮的瞬间跳转到了page1。点击清空数据,文本框中的数据就被清空了。点击返回就回到了主界面。好的,我们结束调试对文本控件t0做一些优化。我们把文本位置改到左上方,把默认的显示内容清空,把文本最大显示数量改为1000,让它能够显示足够多的数据。串口屏制作就暂时到这里了。

接着,打开stm32cubeide。新建一个工程并命名。接下来。第一步SYS这里设置成这样。第二步设置生成单独的.c和.h文件。第三步这里要选择GB2312。第四步设置USART1这里为异步模式。波特率设置为9600。USART2也是同样的方法来设置。这些前面视频都讲过的,带大家再熟悉一下。

点击保存代码就自动生成了。接着,引用这两个常用的头文件。同时,给huart1起个别名。学过前面视频的小伙伴们都知道。在vscode这个软件中使用printf这个函数时。打印输出的结果会显示在输出窗口。我们现在想要实现的功能是打印输出的结果显示到串口屏,也就是需要实现printf这个函数的重定向。

视频画面 05:59.7
视频画面 05:59.7 · 1080P 原图 · 查看对应视频

这时我们就要重写_write这个函数。这个函数写法是固定的,在这里 调用熟悉的hal_uart_tramsmit这个函数,从而实现将printf中的内容输出到串口屏。也就实现了printf这个函数的重定向。接着,调用setvbuf这个函数。来设置无缓冲输出。这是因为在嵌入式系统中。

调试结果会先放入缓冲区。当遇到\n才会从缓冲区输出到串口屏。但是,由于串口屏不能识别\n。所以,就通过调用这个函数来实现无缓冲输出。按串口屏手册中写的是可以识别\n的,可能是和串口屏的版本有关系了。这里就不去纠结了。直接调用这个函数就行。接着,我们在while循环中调用printf这个函数。

这里的page1.t0.txt就是向串口屏t0这个文本控件中写入内容。三个xff是串口屏的结束符。这时连接我们的学习板或者最小系统板。点击运行。打开串口屏模拟器。点击用户mcu输入。点击开始。点击调试。这时t0这个控件输出的这就是我们想要的结果。

接下来我们将上期视频在vscode中写好的代码复制到cubeide中。接着,我们把之前printf中的内容复制到刚才写好的printf函数中。我们来看这里报错了。这是因为cudeide默认情况下printf是不能输出浮点数的。根据这里的报错提示来设置一下。接着,在这里加一个1秒的延时。

点击运行,打开串口屏模拟器。输出结果是不是和上期视频一模一样?这里的print是不是使用起来还是没有我们之前那么顺手?所以,我把它封装一个新的printf函数,就把他命名为my_printf。这个新封装的函数当做一个工具来用。不是我们学习的重点,不用关心具体的实现过程。

如果对c语言特别感兴趣的小伙伴们可以自行研究一下。我在里面我做了详细的注释。同时,引入这样一个头文件。最后,我们在while循环中调用封装好的输出函数。点击运行,打开串口屏模拟器。输出的结果是不是也没有问题?通过这期视频的学习。在以后的stm32的学习过程中,我们也可以随心所欲的调用my_printf这个函数。

来调试和验证我们写好的程序了。好的,今天的视频就到这里了。您的关注,点赞和收藏是我持续更新下去的最大动力。我们下期见。

#include<stdarg.h>
void my_printf(const char *format, ...)
{
    static char buffer[512];
    char temp_buffer[256];  // 用于存放格式化后的字符串
    va_list args;

    // 固定前缀和后缀
    const char prefix[] = "page1.t0.txt+=\"";
    const char suffix[] = "\\r\"\xff\xff\xff";

    // 格式化用户输入的内容
    va_start(args, format);
    int len = vsnprintf(temp_buffer, sizeof(temp_buffer), format, args);
    va_end(args);

    if (len < 0) {
        // 格式化错误
        return;
    }

    // 检查总长度是否超出缓冲区
    int total_len = strlen(prefix) + len + strlen(suffix);
    if (total_len >= sizeof(buffer)) {
        // 超出缓冲区,截断处理
        len = sizeof(buffer) - strlen(prefix) - strlen(suffix) - 1;
        if (len < 0) {
            len = 0;
        }
        temp_buffer[len] = '\0';
    }

    // 组合字符串
    strcpy(buffer, prefix);
    strcat(buffer, temp_buffer);
    strcat(buffer, suffix);

    // 发送
    HAL_UART_Transmit(&ss_huart, (uint8_t*)buffer, strlen(buffer), HAL_MAX_DELAY);
}
视频画面 11:08.7
视频画面 11:08.7 · 1080P 原图 · 查看对应视频

第30期 一节课讲明白STM32寄存器(上)

观看原视频

hello,小伙伴们,大家好。这里是左左右。大家有没有思考过这样一个问题?我们是如何通过几行简单的代码就实现了对单片机上LED小灯亮灭的控制呢?这些代码在我看来无异于是直接写了一句把灯打开。只不过这里的代码是英文的而已。

那么,代码与灯之间到底经历了什么?这里我们就不得不提到一个关键的概念了。那就是寄存器。那么,什么是寄存器呢?说白了寄存器就是单片机内存空间中一块特殊的区域。我们通过修改寄存器中特定位置中的值。当寄存器特定位置中的值发生改变时,led小灯就实现了亮灭的功能。

同时,我们也可以读取寄存器特定位置中的值。来判断小灯此时的状态是亮还是灭。这里所说的特定位置就是地址。说专业点就是通过改变寄存器中特定地址中的值来来输出高低电平,从而实现小灯的亮灭,也就是前面我们说的通过庙来找和尚或者换和尚。所以,在stm32中不管是标准库还是hal库的源代码,都大量使用了指针。

这也是我为什么在前面连续几期视频对指针的概念和使用方法做了详细讲解?当然,实际情况要比这里所说的复杂一些,但基本的实现原理就是这样。接着,我们打开stm32中文手册。手册对于芯片来说就是一份详细的使用说明书。所以,阅读手册是每一个嵌入式学习者的基本功。大家一定要仔细阅读手册。

接着,我们找到2.3存储器映像中GPIO端口A(28页)。这里的地址区间就是寄存器分配给GPIOA来使用的。接着,跳转到8.5。横的这一行这里的0-31代表它占用了32位比特的地址,也就是四个字节。和我们前面讲过的int型数据类型占用的地址是一样的。竖的这一列指的是偏移量。

视频画面 01:55.4
视频画面 01:55.4 · 1080P 原图 · 查看对应视频

那么,它是相对于哪个地址来偏移的呢?这里的偏移量是相对于GPIOA的基地址而言的。如果是GPIOB就是以这个基地址为偏移的。其他也是以此类推。接着,来看这里的偏移量。相邻偏移量的差值都是4。就是前面这里每一组寄存器地址是占用了4个字节的空间。接着,来看这里的ODR这个寄存器。

这里的值就是控制GPIO引脚输出高低电平的。假设我们要控制GPIOA,那么,这里的16位比特,分别控制PA0到PA15。如果我们要控制的是PA15这个引脚,就要改变这里的值。当这里的值是0时,PA15就向外输出低电平,小灯点亮。当这里的值是1时,PA15就向外输出高电平,小灯熄灭。

接着,我们打开stm32cubeIDE来验证一下。首先,定义一个指针将基地址加偏移量放入其中。之前我们说过指针不仅有存放地址的功能还有指向的功能,而这里我们只是传入一个地址,并无指向的功能。所以,所以我们要把这里的地址强转为指针的形式。接着,我们给这个指针指向的地址赋值,也就是给GPIOA中ODR这个寄存器赋值。

通过前面视频的学习我们很容易就把这里的值转化为16进制的数。接着,点击运行。也同样实现了小灯的闪烁功能。接着,我们点击Debug这个按钮,然后打开SFR视图。这里就能实时看到STM32所有外设寄存器的值。找到GPIOA中ODR寄存器。接着,点击F6让代码一步一步向下执行。

视频画面 03:59.5
视频画面 03:59.5 · 1080P 原图 · 查看对应视频
视频画面 04:40.9
视频画面 04:40.9 · 1080P 原图 · 查看对应视频

然后,我们就能看到ODR15中的值。找到GPIOA下的ODR寄存器。点击F6让代码单步执行。就能看到ODR15位的值随着代码执行在0和1之间变化。同时,小灯也在闪烁。我们甚至可以直接在调试界面点击ODR15位的值来手动修改它,小灯也会随之立刻亮灭。其实,寄存器的存在就像一个翻译官一样。

视频画面 05:22.3
视频画面 05:22.3 · 1080P 原图 · 查看对应视频

我们通过向寄存器写值的操作,来告诉单片机我们想让它做什么操作。比如:让某个引脚输出高电平,也可以通过读取寄存器中的值,来"听懂"单片机向我们"传达"的信息。比如:某个引脚当前的电压状态,有了它的存在,我们才能与单片机进行无障碍沟通。虽然在实际开发中,我们很少直接使用寄存器来编写程序。

但理解寄存器的工作原理,对我们深入理解STM32是至关重要的。也是我们读懂STM32 HAL库或标准库源码的关键。如果你把我前面讲指针的视频都弄明白了,学习这期内容应该有一种豁然开朗的感觉。其实,这里的写法还是有一些瑕疵的,下期视频我会完善这里的代码,并分析hal库源码。

如果大家对这些底层源码分析的内容反响比较好,我后续也会穿插做更多这样的深度解析。同时,我也把这期视频的要点整理成了文档。供大家查漏补缺。之前视频的文档我也会陆续补齐。

同时,我也把这期视频的内容做成了文档的形式供大家查漏补缺。前面视频的文档内容我也会陆陆续续补齐。好的,今天的视频就到这里了。为了能让大家高效、易懂的学习,每期视频我都倾注了大量的时间和心血在里面。您的关注,点赞和收藏是我持续更新下去的最大动力。我们下期见。

那么,我们该如何改变其中某一位中的值呢?stm32已经替我想好了。接着,我们来看BSRR这个寄存器。这个寄存器分为低16位和高16位。这里也是分别控制PA0到PA15。

typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;
#define GPIOA                           ((GPIO_TypeDef *)GPIOA_BASE)
#define GPIO_PIN_15                ((uint16_t)0x8000)
#define GPIOA_BASE                 (APB2PERIPH_BASE + 0x00000800UL)
#define APB2PERIPH_BASE       (PERIPH_BASE +          0x00010000UL)
#define PERIPH_BASE                                                      0x40000000UL
uint32_t *GPIOA_ODR = (uint32_t*)0x40010800 + 0x0000000C;
*GPIOA_ODR = 0x00008000;
HAL_Delay(500);
*GPIOA_ODR = 0x00000000;
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, GPIO_PIN_RESET);
HAL_Delay(500);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, GPIO_PIN_SET);
HAL_Delay(500);

这里我们打开实现LED小灯闪烁的那期视频的程序。来看看hal库具体是怎么操作寄存器的?打开HAL_GPIO_WritePin这个函数。这两行代码作用是判断我们输入的这两个参数是否合法?

下期视频 改代码 讲bssr寄存器 看输出数据寄存器图片 读源码。

uint32_t    *GPIOA_ODR = (uint32_t*)(0x40010800 + 0x0000000C);
	 *GPIOA_ODR = 0x00008000;
	  HAL_Delay(500);
	  *GPIOA_ODR = 0x00000000;
	  HAL_Delay(500);

也同样实现了小灯的闪烁功能。

第31期 一节课讲明白STM32寄存器(下)

观看原视频

hello,小伙伴们,大家好。这里是左左右。在上期视频中我们通过修改ODR这个寄存器中的值也同样实现了小灯闪烁的功能。这样的写法在这个案例中是没什么问题的。但他其实是存在一定的隐患的,我们在给寄存器中PA15,这一位,置1和置0时。也同时把PA0到PA15的位也置零了。

我们在做一个完整的项目时。前面的代码可能已经对PA0到PA15中的某些引脚置1了。我们在这时将他们全部置0。程序不就乱套了?那么,有没有什么办法将PA15这一位单独置零或者置1呢?答案肯定是有的,首先,我们来尝试一下将PA15这一位单独置1。其实,也很简单。我们都知道1与任意值做或运算都等于1,而0与任意值做或运算都等于这个值本身。

视频画面 00:39.3
视频画面 00:39.3 · 1080P 原图 · 查看对应视频

基于这一原理。我们将GPIOA ODR这个指针指向的值与这个数做或运算。这样我们就实现了将PA15这一位单独置1,而不影响其他位的值。在代码中我们要将它转换为16进制的数。

那么,每次都要将2进制的数转换为16进制的数。是不是很容易出错?那么,有什么更简单的方法呢?其实,我们只要将16进制的0x1向左移动15位就可以了。接下来我们要将PA15这一位单独置0。我们都知道0与任意值做与运算都等于0,而1与任意值做与运算都等于这个值本身。这时将GPIO A ODR这个指针指向的值与这个数做与运算。

我们就实现了将PA15这一位单独置0,而不影响其他位的值。我们要将这个2进制的数转换为16进制数是不是更容易出错啊?其实,我们只要将另外一个2进制数取反就等价与这个2进制的数。这么看是不是就很眼熟了?我们只要将16进制的0x1向左移动15位就可以了。在STM 3 2 GPIO寄存器中除了ODR寄存器还有一个非常重要的寄存器,那就是BSRR寄存器。

这是它的偏移量。一会会用到,STM 3 2将32比特的BSRR寄存器分为高16位和低16位。高16位是用来清除ODR中0到15位的值。命名为BR位 这里的R就是RESET。设置为1会将ODR对应的位的值置为0,设置为0不影响,而低16位是用来设置ODR中0到15位的值。

命名为BS位 这里的S就是SET。设置为1会将ODR对应的位的值置为1,设置为0不影响,也就是说我们只需通过在BSSR这个寄存器中置1的操作。就可以实现ODR对应位置1或置0的操作。同样我们通过改变BSRR这个寄存器中的值也可以间接实现对小灯亮灭的控制。

视频画面 03:23.2
视频画面 03:23.2 · 1080P 原图 · 查看对应视频

那么,当高16位和低16位都同时置1该怎么办?在文档中也有说明,当高16位和低16位都同时置1时,BS位起作用。接下来我们打开STM 32 cube IDE来试一下。点击运行,小灯同样实现了亮灭的功能。接着,我们通过改变BSSR这个寄存器中值来实现小灯的亮灭。点击运行,是不是也没有问题?

接下来我们看看hal库具体是怎么操作寄存器的?我们按下Ctrl点击HAL_GPIO_WritePin这个函数。接着,顺藤摸瓜找到和它同名的.c文件。这两行代码作用是判断我们输入的这两个参数是否合法?不用管他。在这个函数中我们传入的PinState的值是GPIO_PIN_RESET。

所以,程序会跳转到这里。当我们看到这个符号,是不是就瞬间想到结构体指针了?这是因为HAL_GPIO_WritePin这个函数传入的第一个参数就是结构体指针。我们这里传入的GPIOA。所以GPIOA就是就是指向GPIO_TypeDef这个结构体的指针。指针的两个功能分别是指向功能和存放地址的功能。

指向的功能在传参时已经赋予了。接着,来找一下GPIOA中存放的地址。顺藤摸瓜我们可以找到GPIOA中存放的就是手册中GPIOA的基地址0x40010800。接着,我们找到定义GPIO_TypeDef这个结构体的地方。由于GPIOA的地址是0x40010800,由此就可推出ODR和BSRR的地址。

视频画面 07:01.7
视频画面 07:01.7 · 1080P 原图 · 查看对应视频

是不是很眼熟?这不就是手册中ODR和BSRR寄存器的地址嘛。接着,找到GPIO_PIN_15对应的值。此时的1处于BSRR寄存器低16位的最高位。将它左移16位。对应BSRR寄存器高16位的最高位置1。此时ODR PA15的值置0。所以hal库源码做的事和我们前面操作寄存器是一模一样的。

那么 ODR和 BSSR到底是什么呢?他们对应的就是前面讲过的位设置清除寄存器。和输出数据寄存器。这里有两条路可以控制IO口输出高低电平。一条是通过改变BSRR中值来改变ODR。另一条就是直接改变ODR中的值。和输出数据寄存器。这就将前面视频的知识串在一起了。通过这两期视频我们完成了ODR和 BSSR两个寄存器的讲解。

同时,也完成了HAL_GPIO_WritePin这个函数的源码讲解。其他内容我也会在后面的HAL库源码解析的课程中陆续讲解到的。好的,今天的视频就到这里了。为了能让大家高效、易懂的学习,每期视频我都倾注了大量的时间和心血在里面。您的关注,点赞和收藏是我持续更新下去的最大动力。

我们下期见。

我们在给寄存器中P A 15,这一位,置1和,置0时。设置为1,会,将ODR对应的位的值,置为0。设置为1,会,将ODR对应的位的值,置为1。

那么,我们该如何改变其中某一位中的值呢?stm32已经替我想好了。接着,我们来看BSRR这个寄存器。这个寄存器分为低16位和高16位。这里也是分别控制PA0到PA15。

typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;
#define GPIOA                           ((GPIO_TypeDef *)GPIOA_BASE)
#define GPIO_PIN_15                ((uint16_t)0x8000)
#define GPIOA_BASE                 (APB2PERIPH_BASE + 0x00000800UL)
#define APB2PERIPH_BASE       (PERIPH_BASE +          0x00010000UL)
#define PERIPH_BASE                                                      0x40000000UL
uint32_t *GPIOA_ODR = (uint32_t*)0x40010800 + 0x0000000C;
*GPIOA_ODR = 0x00008000;
HAL_Delay(500);
*GPIOA_ODR = 0x00000000;
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, GPIO_PIN_RESET);
HAL_Delay(500);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, GPIO_PIN_SET);
HAL_Delay(500);
视频画面 04:17.8
视频画面 04:17.8 · 1080P 原图 · 查看对应视频
视频画面 06:07.1
视频画面 06:07.1 · 1080P 原图 · 查看对应视频

这里我们打开实现LED小灯闪烁的那期视频的程序。来看看hal库具体是怎么操作寄存器的?打开HAL_GPIO_WritePin这个函数。这两行代码作用是判断我们输入的这两个参数是否合法?

下期视频 改代码 讲bssr寄存器 看输出数据寄存器图片 读源码。

uint32_t    *GPIOA_ODR = (uint32_t*)(0x40010800 + 0x0000000C);
	 *GPIOA_ODR = 0x00008000;
	  HAL_Delay(500);
	  *GPIOA_ODR = 0x00000000;
	  HAL_Delay(500);
uint32_t *GPIOA_ODR = (uint32_t*)(0x40010800 + 0x0000000C);
*GPIOA_ODR = *GPIOA_ODR | 0x1 << 15;
*GPIOA_ODR |= 0x1 << 15;
 HAL_Delay(500);
*GPIOA_ODR = *GPIOA_ODR & ~(0x1 << 15);
*GPIOA_ODR &= ~(0x1 << 15);
HAL_Delay(500);

第32期 一节课讲明白stm32内部架构

观看原视频

hello,小伙伴们,大家好。这里是左左右。你是否好奇?指尖大小的这块黑色塑料片。如何成为无数智能设备的大脑?今天,我们将深入STM32F103C8T6的微观宇宙。解锁这颗传奇芯片的内部奥秘。STM32F103C8T6这颗芯片内部有四个驱动单元。他们分别是Cortex M3内核、DCode总线、系统总线、通用DMA1和通用DMA2。

视频画面 02:26.9
视频画面 02:26.9 · 1080P 原图 · 查看对应视频

有四个被动单元。他们分别是内部SRAM、 内部闪存存储器Flash、FSMC。AHB到APB的桥和它连接所有的APB设备。那么,什么是驱动单元?什么又是被动单元呢?驱动单元是指有能力主动发起读写操作的模块,而被动单元只能被动地响应来自主动单元的读写请求。这里的Cortex M3内核就是stm32单片机的CPU,也就是单片机的大脑。

视频画面 01:37.8
视频画面 01:37.8 · 1080P 原图 · 查看对应视频

DMA是我们比较熟悉的,它有许多别名。比如,专业牛马、数据搬运工、CPU小助手等等,这里的Flash相当于电脑的硬盘,用来存储我们编译好的代码。它是非易失性存储器,也就是断电数据不丢失。SRAM相当于点电脑的内存。它属于易失性存储器,数据在断电瞬间即丢失。它是程序的运行空间。

主要用于程序运行时的高速数据存取,读写极快。FMSC用来连接外部的Flash和SRAM。SDIO是用来高速读写SD卡的,这里的ICOode总线是用来读取指令的,也就是读取我们编译好的程序的,而DCOode总线是用来传输数据的,System总线是处理所有不属于I-Code和D-Code专职范围的访问。

比如,片上外设、通过FSMC连接的其他外部存储器等等,这里的APB2总线和APB1总线分别通过桥接1和桥接2连接到了AHB总线。与APB2和APB1连接的都是一些片上外设。这里的GPIO和USART都是我们熟悉的。APB1和APB2的主要区别在于。APB1操作速度上限是36MHz,APB2操作速度上限是72MHz。

视频画面 02:51.4
视频画面 02:51.4 · 1080P 原图 · 查看对应视频
视频画面 03:15.9
视频画面 03:15.9 · 1080P 原图 · 查看对应视频

APB2主要连接的是GPIO、ADC、高级定时器等对速度要求高的外设。APB1主要连接的是I2C、SPI、USART等大部分通用通讯接口。其他一些片上外设我也会在接下来的视频中陆续讲到的。这里有这么多总线其实就是不同的分工而已。不要被密密麻麻的总线吓到,这里的总线矩阵就相当于是个集散中心。

关于stm32的系统架构我们只要有一个宏观的感受就可以了。不用细究。好的,今天的视频就到这里了。您的关注,点赞和收藏是我持续更新下去的最大动力。我们下期见。

我们在给寄存器中P A 15,这一位,置1和,置0时。设置为1,会,将ODR对应的位的值,置为0。设置为1,会,将ODR对应的位的值,置为1。

那么,我们该如何改变其中某一位中的值呢?stm32已经替我想好了。接着,我们来看BSRR这个寄存器。这个寄存器分为低16位和高16位。这里也是分别控制PA0到PA15。

typedef struct
{
  __IO uint32_t CRL;
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;
#define GPIOA                           ((GPIO_TypeDef *)GPIOA_BASE)
#define GPIO_PIN_15                ((uint16_t)0x8000)
#define GPIOA_BASE                 (APB2PERIPH_BASE + 0x00000800UL)
#define APB2PERIPH_BASE       (PERIPH_BASE +          0x00010000UL)
#define PERIPH_BASE                                                      0x40000000UL
uint32_t *GPIOA_ODR = (uint32_t*)0x40010800 + 0x0000000C;
*GPIOA_ODR = 0x00008000;
HAL_Delay(500);
*GPIOA_ODR = 0x00000000;
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, GPIO_PIN_RESET);
HAL_Delay(500);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_15, GPIO_PIN_SET);
HAL_Delay(500);

这里我们打开实现LED小灯闪烁的那期视频的程序。来看看hal库具体是怎么操作寄存器的?打开HAL_GPIO_WritePin这个函数。这两行代码作用是判断我们输入的这两个参数是否合法?

下期视频 改代码 讲bssr寄存器 看输出数据寄存器图片 读源码。

uint32_t    *GPIOA_ODR = (uint32_t*)(0x40010800 + 0x0000000C);
	 *GPIOA_ODR = 0x00008000;
	  HAL_Delay(500);
	  *GPIOA_ODR = 0x00000000;
	  HAL_Delay(500);
uint32_t *GPIOA_ODR = (uint32_t*)(0x40010800 + 0x0000000C);
*GPIOA_ODR = *GPIOA_ODR | 0x1 << 15;
*GPIOA_ODR |= 0x1 << 15;
 HAL_Delay(500);
*GPIOA_ODR = *GPIOA_ODR & ~(0x1 << 15);
*GPIOA_ODR &= ~(0x1 << 15);
HAL_Delay(500);

第33期 一节课讲明白STM32时钟系统

观看原视频

Hello,小伙伴们,大家好。这里是左左右。这期视频我们来聊一聊STM32F103系列的时钟系统。在STM32程序编写过程中,我们经常会用到HAL_Delay这个函数。

那么STM32是如何实现准确计时的呢?在这里我先问大家这样一个问题:如果我让大家在5秒钟后在弹幕上打1,你们会怎么做?大家肯定会看着手机或者手表去倒计时5秒。如果身边没有任何计时设备,该怎么办呢?我们还有最简单最原始的办法。那就是数数。STM32单片机就是通过计数的方法来实现计时的。

在STM32单片机内部有两个时钟源,分别是频率为40KHz低速内部时钟LSI。和频率为8MHz高速内部时钟HSI。他们可以产生固定频率的脉冲信号。脉冲信号每通过一个周期计数加一。其中低速内部时钟主要是为单片机实时时钟系统,即RTC提供时钟源的。如日历、闹钟、时间戳等,而高速内部时钟主要是给片上外设提供时钟的。

如我们前面学到的GPIO和USART等。周期稳定的脉冲信号才能实现稳定的计数,才能保证计时的准确性。由于内部时钟是采用的RC震荡电路,它的精度并不好,所以,我们一般不用。我们会在单片机外部接两个晶振。分别为8Mhz的高速外时钟,即HSE。和32.768khz的低速外部时钟,即LSE。

由于晶振内部的石英晶体拥有一个非常精确的固有谐振频率,所以,外部晶振的精度极高。这里的RTC时钟源可以是多个中的一个。它可以是40Khz的低速内部时钟。也可以是32.768KHz的低速外部时钟。也可以是高速外部时钟的128分频。这里的分频就是做除法运算。当然,低速内部时钟也并不是一无是处。

它单独给看门狗提供时钟源。这里的看门狗可以把它想象成一个必须在规定时间内被“喂食”的“电子看门狗”。如果主程序因为意外,如程序跑飞、陷入死循环、外部干扰等,而未能按时喂狗,这只狗就会“叫醒”或“重启”整个系统,让系统从一个可知的初始状态重新开始运行。后面的课程我们还会详细讲解看门狗的使用。

视频画面 02:52.4
视频画面 02:52.4 · 1080P 原图 · 查看对应视频

这里有一个基础的认知就可以了。上期视频我们讲过APB1总线的最高时钟频率是36Mhz。APB2总线的最高时钟频率是72Mhz。此时我们外部晶振才8Mhz。

那么,如何才能让APB2达到72Mhz呢?这里就要提到PLL锁相环了。它的作用就是倍频。8Mhz的频率经过PLL锁相环乘9就达到72Mhz了。这里的72Mhz分两路。一路经USB分频器到USB。另一路到系统时钟SYSCLK。这里的SYSCLK的时钟源也有很多。它还可以是外部高速时钟不经过锁相环直接到SYSCLK系统时钟。

也可以是8MHz的内部高速时钟。当然,内部高速时钟也并不是一无是处。它可以给FLITFCLK提供时钟。FLITFCLK的核心作用是为某些必须在所有低功耗模式下都能持续工作的关键模块,提供一个独立、稳定、且始终开启的时钟源。简单来说,它是STM32内部的一个 “永不停息的脉搏”。

8MHz的内部高速时钟也可经PLL锁相环到系统时钟。这里的CSS是时钟安全系统。当HSE失效时能启动HSI,并产生中断,我们可以在中断中编写代码。如实现记录日志、切换模式、尝试恢复等功能。SYSCLK经AHB分频器到HCLK。这里的HCLK就是AHB总线的是时钟。HCLK又产生很多分支。

这里经APB1分频器2分频到达最大的36Mhz。APB1定时器经2倍频达到72Mhz。接着,经APB2分频器1分频到达最大的72Mhz。这里的APB2定时器也是72Mhz。ADC经6倍频达到12Mhz。就是说当APB1外设达到最大36Mhz。APB2外设达到最大72Mhz时。

不管是APB1还是APB2总线上的定时器都是72Mhz,这条分支是AHB总线,内核,内存,DMA提供时钟源的。接着,看这里的FCLK。它的作用就是提供CPU内核的工作时钟。这个分支是滴答计时器。这里是1分频也可以是8分频。这里可以看到STM32的时钟系统非常复杂。它有很多的分支。

所以,又被称作时钟树。那么STM32的时钟系统为什么要做的这么复杂呢?一会儿倍频一会分频的,其实,它是为了满足单片机内部的不同需求。那有人肯定就要问了,直接都弄成72Mhz不就完事了嘛。这样做会造成,功耗的浪费。浪费电不就是浪费钱么。我们打开STM32CubeIDE来看一下。

这里的40khz就是低速内部时钟。8Mhz就是高速内部时钟。这时的外部时钟都是灰色的,我们需要通过这里来设置一下。我们直接将HCLK设置为72Mhz。APB1总线上的外设就达到36Mhz的最快速度l。APB2总线上的外设就达到72Mhz的最快速度了。其实,这期视频我们只要记住高速外部时钟的这两条线路。

和低速外部时钟的这一条线路就可以了。其他用到我还会带大家回来看这个图的,吃多嚼不烂,还有这里为什么是灰色呢?我们只需要打开master clock output这个选项就可以了。这时PA8这个引脚就变绿了。这时的PA8就能当做一根时钟线向单片机外部的其他设备提供时钟源了。

这里再给大家补充两个小的知识点。第一个知识点事72Mhz的频率到底有多快?72M等于72乘以10的6次方。我们都知道频率和周期是互为倒数的关系。这样单片机在使用8M外部晶振的情况下。最快就可以实现72分之一微秒记一次数。是不是很快啊?

另外,一个知识点就是32.768这个数感觉很别扭还难记。低速外部时钟为什么是32.768k赫兹?这是因为2的15次方正好就是32768这个数,也就是32786被2分频15次之后就变成1赫兹了,也就是1次/秒。前面我们说了低速时钟用于日历、闹钟、时间戳等功能。它们最基本的单位不就是秒嘛。

如果大家对hal_delay这个函数的实现过程感兴趣也可以给我留言。感兴趣的小伙伴们多的话,我们下期视频就看看hal_delay源码是如何实现延时的供能的?

好的,今天的视频就到这里了。您的关注,点赞和收藏是我持续更新下去的最大动力。我们下期见。

Hello,小伙伴们,大家好。这里是左左右。这期视频我们来聊一聊STM32F103系列的时钟系统。在STM32程序编写过程中,我们会经常用到hal delay这个函数。

那么STM32是究竟是如何实现准确计时的呢?在这里我先问大家这样一个问题:如果我让大家在5秒钟后在弹幕上打1,你们会怎么做?你们肯定会看着手机或者手表去倒计时5秒。如果身边没有任何计时设备,该怎么办呢?我们还有最简单最原始的办法。那就是数数。STM32单片机就是通过计数的方法来实现计时的。

在STM32单片机内部有两个时钟源,分别是频率为40K赫兹低速内部时钟LSI。和频率为8兆赫兹高速内部时钟HSI。他们可以产生固定频率的脉冲信号。脉冲信号每通过一个周期计数加一。其中低速内部时钟主要是为单片机实时时钟系统RTC提供时钟的。如日历、闹钟、时间戳等,而高速内部时钟主要是给片上外设提供时钟的。

如我们前面学到的GPIO和USART等。周期稳定的脉冲信号才能实现稳定的计数。才能保证计时的准确性。由于内部时钟是采用的RC震荡电路它的精度并不好,所以,我们通常不把它们作为主时钟源。我们一般会在单片机外部接两个晶振。分别为8兆赫兹的高速外部时钟HSE。和32.768 K赫兹的低速外部时钟LSE。

由于晶振内部的石英晶体拥有非常精确的固有谐振频率,所以,外部时钟的精度极高。这里的RTC时钟源可以是三选一。它可以是40K赫兹的低速内部时钟。也可以是32.768K赫兹的低速外部时钟。还可以是高速外部时钟的128分频。这里的分频。简单理解就是做除法运算。当然,低速内部时钟也并不是一无是处。

它单独给看门狗提供时钟源。这里的看门狗可以把它想象成一个必须在规定时间内被“喂食”的“电子看门狗”。如果主程序因为意外,如程序跑飞、陷入死循环、外部干扰等,而未能按时喂狗。这只狗就会“叫醒”或“重启”整个系统,让系统从一个可知的初始状态重新开始运行。后面的课程我们还会详细讲解看门狗的使用。

这里有一个基础的认知就可以了。上期视频我们讲过APB1总线的最高时钟频率是36兆赫兹。APB2总线的最高时钟频率是72兆赫兹。此时我们外部晶振才8兆赫兹。

那么,如何才能让APB2达到72兆赫兹呢?这里就要请出PLL锁相环了。它的核心作用就是倍频。8兆赫兹的频率经过PLL锁相环乘9就达到72兆赫兹了。这里的72兆赫兹信号分两路。一路经USB预分频器到USB。另一路则通向系统时钟SYSCLK。这里的SYSCLK的时钟源有多种选择。

它还可以是外部高速时钟不经过锁相环直接到SYSCLK系统时钟。也可以是8兆赫兹的内部高速时钟。当然,内部高速时钟也并不是一无是处。它可以给FLITFCLK提供时钟。FLITFCLK的核心作用是为某些必须在所有低功耗模式下都能持续工作的关键模块。提供一个独立、稳定、且始终开启的时钟源。

简单来说。它是STM32内部的一个 “永不停息的脉搏”,这里的CSS是时钟安全系统。当HSE失效时,它能自动切换到 HSI 并产生中断。我们可以在中断中编写代码。如实现记录日志、切换模式、尝试恢复等功能。这为高可靠性应用提供了保障。SYSCLK经AHB预分频器到HCLK。

这里的HCLK就是AHB总线的时钟。HCLK又产生很多分支。一路经APB1预分频器2分频到达最大的36兆赫兹。这里有一个关键机制。只有当 APB 预分频系数大于1时。比如,这里的2分频,连接到该总线上的定时器时钟。才会在 PCLK1 的基础上被自动倍频乘以2,从而达到最高 72兆赫兹。

另一路经APB2预分频器1分频到达最大的72兆赫兹。这里的APB2定时器也是72兆赫兹。ADC经6倍频达到12兆赫兹。就是说当APB1外设达到最大36兆赫兹。APB2外设达到最大72兆赫兹时。不管是APB1还是APB2总线上的定时器都是72兆赫兹,这条分支是为AHB总线,内核,内存,DMA提供时钟源的。

视频画面 05:06.1
视频画面 05:06.1 · 1080P 原图 · 查看对应视频
视频画面 05:32.8
视频画面 05:32.8 · 1080P 原图 · 查看对应视频

接着,看这里的FCLK。它的作用就是提供CPU内核的工作时钟。这个分支是滴答计时器。这里是1分频也可以是8分频。我们熟悉的hal_delay这个函数就是靠滴答定时器来实现的。这里可以看到STM32的时钟系统非常复杂。它有很多的分支。因此也被形象地称为“时钟树”。

那么STM32的时钟系统为什么要做得这么复杂呢?一会儿倍频一会儿分频的,这都是为了精确满足单片机内部不同模块对时钟速度和功耗的多样化需求。那有人肯定就要问了。直接都弄成72兆赫兹不就完事了嘛。这样做确实简单。但会造成巨大的功耗浪费。对于嵌入式设备。尤其是电池供电的产品来说。

功耗就是续航。省电就是省钱。下面,我们打开S T M 3 2 Cube IDE来直观感受一下。这里的40 k 赫兹就是低速内部时钟。8兆赫兹就是高速内部时钟。这时的外部时钟都是灰色的,我们需要在这里激活它们。我们直接将HCLK设置为72兆赫兹。APB1总线上的外设就达到36兆赫兹的最快速度了。

APB2总线上的外设就达到72兆赫兹的最快速度了。大家只要重点记住 高速外部时钟HSE的主干路径,以及低速外部时钟 LSE 通往 RTC 的这条路径就够了。其他的分支,我们会在后续用到时再回来看这张图。毕竟贪多嚼不烂,还有这里为什么是灰色呢?我们只需要打开master clock output这个选项就可以了。

视频画面 07:19.8
视频画面 07:19.8 · 1080P 原图 · 查看对应视频

这时PA8这个引脚就变绿了。这时的PA8就能当做一根时钟线向单片机外部的其他设备提供时钟源了。这里再给大家补充两个小的知识点。第一个知识点是72兆赫兹的频率到底有多快?72兆等于72乘以10的6次方。我们都知道频率和周期是互为倒数的关系。这样单片机在使用8兆外部晶振的情况下。

最快就可以实现72分之一微秒记一次数。是不是很快啊?另外,一个知识点就是32.768这个数感觉很别扭还难记。低速外部时钟为什么是32.768 k赫兹?这是因为2的15次方正好就是32768这个数,也就是32786被2分频15次之后就变成1赫兹了,也就是1次每秒。前面我们说了低速时钟用于日历、闹钟、时间戳等功能。

它们最基本的单位不就是秒嘛。如果大家对hal_delay函数的底层实现过程感兴趣也可以在评论区留言。感兴趣的小伙伴们多的话。我们下期视频就一起剖析一下hal_delay的源码。看看它是如何实现延时功能的?

好的,今天的视频就到这里了。您的关注、点赞和收藏是我持续更新下去的最大动力。我们下期见。

第34期 一节课讲明白STM32滴答定时器

观看原视频

Hello,小伙伴们,大家好。这里是左左右。上回咱们说到,HAL_Delay那个延时函数。其实,就是靠芯片内部一个叫滴答定时器的(SysTick)来实现的。今天这期视频,咱们就把它彻底拆开。看看它到底是怎么工作的?顺便亲手写一个自己的延时函数。

首先,我们翻开Cortex-M3内核的编程手册第150页。从这里看出,SysTick本质上是一个 24位的向下计数器。说白了就是倒数,也就是倒计时。翻到第154页可以看到滴答定时器一共有四个核心寄存器。学寄存器我们抓住三个要点就行。分别是基地址,偏移量、和位含义。基地址和偏移量在手册中一眼就可以找到。

接着,我们往上翻来找一下每个寄存器的位含义。首先,来看这里的STK_CTRL寄存器。它的的全称是 SysTick Control and Status Register。通常译为SysTick 控制与状态寄存器。STK_CTRL寄存器别看它32位。只有4位起作用,第0位是计数的使能位,也就是CTRL这个寄存器的总开关,所以这一位是一定要置1的。

视频画面 01:31.4
视频画面 01:31.4 · 1080P 原图 · 查看对应视频

位 1 设置SysTick中断使能。置0代表禁用中断。置1代表当倒数到0时进入中断。位 2是时钟源选择。这个在上期视频是提到过的,置0,是系统时钟的8分频。置1代表时钟源直接就是系统主时钟,不分频。位16 COUNTFLAG:计数完成标志位,这是一个状态标志位。如果定时器已经完成了一轮计数,减到零了,这一位就会被硬件自动置1。

你可以通过读它来查询定时是否完成?接着,来看STK_LOAD这个寄存器。它是一个24位的值。RELOAD值可以是0x00000001至0x00FFFFFF范围内的任意值。这里放的就是要倒数到0的那个数STK_VAL全称SysTick Current Value Register。

视频画面 02:50.8
视频画面 02:50.8 · 1080P 原图 · 查看对应视频

中文译为SysTick 当前值寄存器。它实时记录被倒计时的当前值。STK_CALIB全称SysTick calibration value register。通常译为SysTick校准值寄存器。这里我们可以看到前面三个寄存器这里都是rw,也就是可读可写,而这个寄存器只有一个r,也就是这个寄存器是只读的,它是芯片出厂时就写好的,告诉你一些硬件特性。

比如,在标准参考时钟下,实现1毫秒定时需要多少计数值。位31称作无参考时钟标志位(NOREF),当读取到NOREF=0时,代表SysTick可以选择使用处理器时钟 (AHB) 也可以使用,外部参考时钟 (HCLK/8) ,而当读取到NOREF=1时,代表SysTick只能使用处理器时钟 (AHB),由于我们这里的时钟源是NOREF=0的这种情况,所以当我们读取这一位时肯定是0,位30被称作校准偏差标志位,它是用来指示TENMS值的精确度的,SKEW = 0时,TENMS值精确 ,SKEW = 1时TENMS值不精确。

视频画面 04:10.3
视频画面 04:10.3 · 1080P 原图 · 查看对应视频

stm32f103系列在出厂校准时,SysTick校准值是基于内部RC振荡器测量的,也就是HSI,HSI本身的精度并不高,所以这一位是1。位0:到位23存储的是1ms校准值,它这里显示的在外部参考时钟(HCLK/8)下,产生1ms定时所需的计数值,以HCLK最高72Mhz为例。

TENMS = (72,000,000 / 8) × 0.001。= 9,000,000 × 0.001。= 90,00。= 0x2328 (十六进制)。STK_CALIB这个寄存器使用频率并不高,所以我们只做了解就可以了,光说不练假把式。咱们来实战配置一下寄存器,让SysTick精确地每1毫秒完成一次倒数,这里我们默认HCLK位72M赫兹的最大频率,前面我们说过72兆赫兹相当于72分之一微秒就记一次数,那么1毫秒需要记72000个数,但是这里的零也算一个数,比如我们要记10个数,记0到9就可以了,所以这里的72000要减去一个1,这时的计数值就是1毫秒。

视频画面 05:56.2
视频画面 05:56.2 · 1080P 原图 · 查看对应视频

所以,重载值寄存器这里就存入71999这个数,CLKSOURCE这里我们选的是最快的72M赫兹,不需要分频,所以这里置1,TICKINT这里置0,先不触发中断,纯计时。ENABLE这里置1,倒计时开启,STK_VAL中的值从71,999开始递减,当倒数到0时,COUNTFLAG这是一个状态标志位就会自动置1,紧接着STK_LOAD中值会自动装进STK_VAL,COUNTFLAG这是一个状态标志位就会自动置1,这时就进入下一轮1毫秒的计时了,以此类推永不停息,接着我们使用寄存器在stm32cubeide中快速的来写一个毫秒级的延时函数,同时我们点击调试,可以打开寄存器,观察一下里面的值是不是和我们前面所讲一样?

视频画面 06:49.1
视频画面 06:49.1 · 1080P 原图 · 查看对应视频

Hello,小伙伴们,大家好。这里是左左右。回咱们说到,HAL_Delay这个延时函数。其实,就是靠芯片内部一个叫滴答定时器的来实现的。今天这期视频。咱们就把它彻底拆开。看看它到底是怎么工作的?顺便亲手写一个自己的延时函数。

首先,我们翻开 Cortex M3 内核的编程手册第150页。从这里看出,SysTick本质上是一个 24位的向下计数器。说白了就是倒数,也就是倒计时。翻到第154页可以看到滴答定时器一共有四个核心寄存器。学寄存器我们抓住三个要点就行。分别是基地址,偏移量、和位含义。基地址和偏移量在手册中一眼就可以找到。

接着,我们往上翻。来找一下每个寄存器的位含义。首先,来看这里的STK_CTRL寄存器。它的全称是 SysTick Control and Status Register。通常译为SysTick 控制与状态寄存器。STK_CTRL寄存器别看它32位。只有4位起作用。第0位是计数的使能位。

它是计数器的总开关。想让计数器工作。这一位必须置1。位1设置SysTick中断使能。置0代表禁用中断。置1代表当倒数到0时进入中断。位 2是时钟源选择。这个在上期视频是提到过的,置0,是HCLK的8分频。置1代表时钟源直接就是HCLK,不分频。位16 COUNTFLAG。

计数完成标志位。这是一个状态标志位。如果定时器已经完成了一轮计数,减到零了,这一位就会被硬件自动置1。你可以通过读它来查询定时是否完成?接着,来看 S T K L O A D这个寄存器。它的全称是 SysTick reload value register。通常译为SysTick 崇装载值寄存器。

它是一个24位的值。RELOAD值可以是0x00000001至0x00FFFFFF范围内的任意值。这里放的就是要倒数到0的那个数。STK_VAL全称 SysTick Current Value Register。中文译为SysTick 当前值寄存器。它实时记录被倒计时的当前值,STK_CALIB全称SysTick calibration value register。

通常译为SysTick校准值寄存器。这里我们可以看到前面三个寄存器这里都是r w,也就是可读可写,而这个寄存器只有一个r,也就是这个寄存器是只读的,它是芯片出厂时就写好的,告诉你一些硬件特性。比如,在标准参考时钟下,实现1毫秒定时需要多少计数值。位31称作无参考时钟标志位(N O R E F)。

当读取到N O R E F = 0 时,代表SysTick可以选择使用处理器时钟 (A H B) 也可以使用。外部参考时钟 (H C L K除以8),而当读取到N O R E F = 1 时。代表SysTick只能使用处理器时钟 (A H B)。由于我们这里的时钟源是N O R E F = 0 的这种情况,所以,当我们读取这一位时肯定是0。

位30被称作校准偏差标志位。它是用来指示TENMS值的精确度的,S K E W = 0时,T E N M S 值精确。S K E W = 1时T E N M S 值不精确。S t m 3 2 f 1 0 3 系列在出厂校准时。SysTick校准值是基于内部 R C 振荡器测量的,也就是H S I,H S I本身的精度并不高。

所以,位0到位23存储的是1毫秒校准值。它这里显示的是在外部参考时钟H C L K除以8下。产生1毫秒定时所需的计数值。以HCLK最高72兆赫兹为例。TENMS等于9000。STK_CALIB这个寄存器使用频率并不高。所以,我们只做了解就可以了。光说不练假把式。咱们来实战配置一下寄存器,让SysTick精确地每1毫秒完成一次倒数。

这里我们默认H C L K为72兆赫兹的最大频率。前面我们说过72兆赫兹相当于72分之一微秒就记一次数。那么1毫秒需要记72000个数。但是,这里的零也算一个数。比如,我们要记10个数,记0到9就可以了。所以,这里的72000要减去一个1。这时的计数值就是1毫秒。所以,重载值寄存器这里就存入71999这个数。

CLKSOURCE这里我们选的是最快的72兆赫兹。不需要分频,所以这里置1。TICKI NT这里置0,先不触发中断。纯计时,ENABLE这里置1,倒计时开启。STK_VAL中的值从71999开始递减。当倒数到0时,COUNTFLAG这一状态标志位就会自动置1。紧接着STKLOAD中值会自动装进STKVAL。

COUNTFLAG这一状态标志位就会自动置0。这时就进入下一轮1毫秒的计时了。以此类推永不停息。接着,我们使用寄存器在 s t m32 cubeide中快速的来写一个毫秒级的延时函数。

同时,我们点击调试。可以打开寄存器。观察一下里面的值是不是和我们前面所讲一样?好的,今天的视频就到这里了。下期视频我们来看一下HAL_Delay这个函数是如何实现延时的?您的关注、点赞和收藏是我持续更新下去的最大动力。我们下期见。

第35期 HAL_Delay源码深度解析

观看原视频

Hello,小伙伴们,大家好,这里是左左右。上一期视频,我们通过直接配置寄存器。实现了一个毫秒级的延时函数。它的核心逻辑很简单。程序执行到延时函数时。通过配置滴答计时器实现1毫秒的延时。通过循环累积多个1毫秒,从而达到所需的延时长度。

那么,有没有其他实现延时的方法呢?当然,有。我们可以换一种思路。在刚进入延时函数时,记录一个“开始时间点”,每隔一段时间看一下表。记录一下当前时间。当“当前时间”减去“开始时间”等于我们需要的“延时长度”时就退出函数。实际上,HAL 库中的 HAL_Delay 函数。采用的就是这种“查询时间差”的方法。

今天,我们就一起来深入探索一下它的源码实现过程。在STM32CubeIDE中,按下Ctrl键并单击 HAL_Delay 函数,就跳转到定义这个函数的地方了。在这里我们可以看到,函数一开始就用变量 tickstart 记录了调用时刻的“时间戳”,也就是“开始时间”。然后,将我们要延时的毫秒数 delay 赋值给变量 wait。

视频画面 01:24.3
视频画面 01:24.3 · 1080P 原图 · 查看对应视频

紧接着有一个 if 判断:如果延时值 wait 小于一个延时最大值。则会将 wait 自增一个数,这个数就是1,这稍后解释。最关键的是while循环,其条件正是。当前时间减去开始时间小于需要等待的时间,只要条件成立,程序就在这里循环。模拟“等待”的过程,这也就是我们前面说的“看表”动作。

循环中调用了 HAL GetTick() 来获取“当前时间”。它返回的是一个名为 uwTick 的全局变量。到这里貌似到了一个死胡同了。但是,靠前面知识的积累。直觉告诉我们这个uwTick肯定是一个每隔1毫秒递增一个数。这样才能起到计时作用。带着这个疑问。我们从程序启动的地方开始寻找答案。在 main 函数之前。

系统会调用 HAL_ Init() 进行初始化。在这里,我们一眼就能看到带有“Tick”字样的函数——HAL_InitTick。点进去看看。它内部调用了 HAL SYSTICK_Config,这里传入的值是 8000。然而,在跟踪代码时我们发现。这里不就是上期视频讲到的滴答定时器的相关寄存器吗?

视频画面 02:36.6
视频画面 02:36.6 · 1080P 原图 · 查看对应视频

我们来看看它是如何配置的,这里配置滴答定时器重装载寄存器值为7999。按理说我们这里设置的是72兆赫兹。重装载寄存器值为71999才对。我们打印输出来看一下SystemCoreClock这个变量的值。结果是72000000。没有问题,这说明后面的代码将SystemCoreClock这个变量重新赋值了。

视频画面 03:24.8
视频画面 03:24.8 · 1080P 原图 · 查看对应视频

接着,往下看,当前值寄存器设置为0。控制与状态寄存器配置成这样,这里的配置开启了中断。那么,SystemCoreClock 这个决定重装载值的关键变量。是在何时被正确设置为72000000的呢?

接下来我们返回main.c来找找SystemCoreClock变量是什么时候被重新赋值的?前面这些都是在配置时钟树。这里的每行都是和时钟树那期视频是可以一一对应的。大家一定要自己去看,看不懂的地方可以在评论区留言。接着,打开HAL_RCC_ClockConfig这个函数看看。

在这里我们终于找到了SystemCoreClock这个全局变量被更新的地方了。由于这里源代码涉及到RCC寄存器的相关知识,我们暂时不做讲解,这里直接告诉大家答案。结果就是72000000 ,其实RCC寄存器就是用来配置时钟树的。

最后,SysTick中断服务函数在哪里?它在 stm32f1xx_it.c 文件中,it 即 interrupt 的缩写。找到 SysTick Handler 函数。我们发现它内部调用了 HAL IncTick()。这里我们终于找到了uwTick这个变量了。太不容易了,这个函数的内容极其简单,使全局变量 uwTick 加1。

也就是滴答定时器每隔1毫秒进入中断使uwTick加1。这时uwTick就变成了一个每隔1毫秒自动加1的变量了。这时他不就是一个精确的钟表吗?也就是我们前面提到的“看表”中的“表”。至此,谜底全部揭晓:1.系统启动后,时钟被配置为72兆赫兹。System Core Clock 被更新为72000000。

视频画面 05:25.4
视频画面 05:25.4 · 1080P 原图 · 查看对应视频

2.SysTick定时器被配置为每72000个时钟周期即1毫秒中断一次。3.每次SysTick中断,uwTick 这个“软件时钟”就加1。HAL_Delay 函数通过对比当前的 uwTick 和进入函数时记录的 uwTick。实现精准的毫秒级延时。前面提到 wait 这里为什么要加1呢?

这里假设没有这段代码,我们需要1毫秒的延时。我们都知道当“systic当前值寄存器”中的值从71999降为0时。uwtick中的值才会加一。假设tickstart取值时systic当前值寄存器中的值正好为1,当这个寄存器中的值为0时,uwtick加1。条件满足,跳出延时函数。

视频画面 06:13.6
视频画面 06:13.6 · 1080P 原图 · 查看对应视频

时间上真实的延时值为72000分之1毫秒。几乎就是没有延时的,这里wait加1就是为了避免这种极端情况的存在。这时有的小伙伴就要说了。这样做不就有了1毫秒的误差吗?其实,从前面的动画也可以看出tickstar在读取uwtick时本来就是有1毫秒的误差的,而且uwtick取值是一个向下取整的过程。

加1对它几乎是没有任何影响的。我为什么要带着大家如此细致地解读HAL库源码呢?首先,这能帮助我们穿透抽象层。理解本质。每次调用HAL库函数,如HAL_Delay。你脑海中不再只是一行神秘的代码,而是一幅清晰的画面。时钟树如何工作,SysTick如何计时?一个全局变量如何被中断服务程序默默更新?

这种深刻的理解,是单纯调用HAL库函数无法获得的。其次,这是学习优秀编程思维的绝佳途径。HAL库的代码结构、模块化设计、对硬件抽象的方式。都值得我们学习。坚持阅读源码。就像与优秀的工程师对话。能极大地提升我们的编程内功。还有一点也是最重要的一点。我也会根据大家的反馈调整视频内容。

如果大家对源码解析这样的课程不感兴趣,我也会减少这类视频的更新,大家可以把自己的想法打到公屏上。“读书百遍,其义自见。”。学习嵌入式开发也是如此,不要被暂时的困难吓倒。通过积累每一次这样的源码探索。你会发现自己对系统的掌控力越来越强。希望我的坚持分享,能换来你的坚持学习。

好的,今天的源码探秘之旅就到这里。如果觉得有收获,一定要关注、点赞、收藏,评论。你的支持是我这样的小博主持续更新下去的最大动力。我们下期再见!

第36期 GPIO寄存器详解

观看原视频

Hello,小伙伴们,大家好,这里是左左右。前面视频我们讲过 GPIO相关寄存器ODR和BSRR。今天我们将把剩余的GPIO核心寄存器全部讲完。首先,来看端口配置低寄存器GPIOx_CRL。它是一个32位寄存器,被划分为8组,每组4位。分别对应PIN 0到PIN7,也就是端口的低8位。

视频画面 00:31.1
视频画面 00:31.1 · 1080P 原图 · 查看对应视频

这也是它名称中带“低”字的原因。每一组又分为两部分,MODE位和CNF位。各占2比特,它们组合起来。共同决定一个引脚的工作模式与特性。其中MODE位是00时表示的IO端口为输入模式。01表示输出模式,最大速度10兆赫兹。10表示输出模式,最大速度2兆赫兹。11表示输出模式,最大速度50兆赫兹。

CNF位含义取决于MODE位设置。当MODE位设置为输入模式时,00表示模拟输入模式,01表示浮空输入模式。10表示上拉或下拉输入模式。11位为保留位,没有实际作用。当MODE位设置位输出模式时,00表示通用推挽输出模式,01表示通用开漏输出模式。10表示复用推挽输出模式,11表示复用开漏输出模式。

接下来是端口配置高寄存器GPIOx_CRH。位含义和GPIOx_CRL是一模一样的。只不过分别对应PIN8到PIN15高8位引脚。然后,是端口输入数据寄存器GPIOx_IDR。它的低16位分别对应PIN0到PIN15的输入电平状态。高16位为保留位。由于此时的引脚是用来读取stm32芯片外部输入的高低电平信号的,所以,它是只读的,接着,是端口位清除寄存器GPIOx_BRR。

视频画面 01:47.5
视频画面 01:47.5 · 1080P 原图 · 查看对应视频

这里的低16位是用来给PIN0到PIN15置零的。它与我们前面讲过的GPIOx_BSRR中的高16位的作用是一样的。这里就不赘述了。最后,是端口配置锁定寄存器GPIOx_LCKR。它的位0到位15 分别对应PIN0到PIN15。将某一位或多位设置为1。即表示选中该引脚,准备对其进行配置锁定。

它的位16,LCKK,是锁定键。当它被成功置1后,之前被选中的引脚。即LCK中为1的位的配置才会被真正锁定,无法再被软件修改,而且位16锁定操作有一个严格的写入序列。需要先写1,再写0,最后再写1,最后一步就是读1,也就是读取位16有没有被置1,如果读取结果为1。就说明位16被成功锁定了。

这个寄存器的作用是防止一些已配置好的关键引脚。如复位引脚、晶振引脚等。在程序后续运行中被意外修改,提高了系统的稳定性和安全性。到这里GPIO相关寄存器就全部讲完了。我在这里总结了一个表格给大家。接着,我来出两道小的题目来考考大家对知识点的掌握情况。第一题,如何通过配置寄存器将PA15引脚配置为上拉输入模式?

视频画面 04:20.4
视频画面 04:20.4 · 1080P 原图 · 查看对应视频

首先,定义指针,这里设置为10为上拉或下拉输入模式。他也没说上拉怎么设置?我们打开手册108页可以看到当设置为输入模式时。这里是断开的,此时stm32赋予ODR寄存器另外一个功能。当设置输出数据数据寄存器对应为1,就是上拉输入,反之设置为0为下拉输入。接着,来配置O D R寄存器。

视频画面 03:42.2
视频画面 03:42.2 · 1080P 原图 · 查看对应视频

好的,这样就设置完成了。第二题,将P B 3引脚配置为通用推挽输入模式,最大速度50兆赫兹。并锁定该引脚相关配置。我们一起来写一下。其实HAL库已经为我们写好了端口配置的函数了。我们打开stm32cubeIDE读一下它的源码。把找到线索汇总一下,这样看来当我们有了一定的知识储备。

视频画面 04:58.6
视频画面 04:58.6 · 1080P 原图 · 查看对应视频

HAL源码读起来也并不是那么困难了,而是变成水到渠成的一件事了,好的,今天的视频就到这里。如果觉得有收获,一定要关注、点赞、收藏,评论。你的支持是我这样的小博主持续更新下去的最大动力。期待下期视频再见!

第37期 CLion安装

观看原视频

hello,小伙伴们,大家好,这里是左左右。前面视频我们一直在用STM32CubeIDE这款软件来教大家学习STM32单片机开发。通过“点一点”,“选一选”的方式,代码就自动帮我们生成了。这对新手入门来说是十分友好的,但是,随着学习的深入。我们不可避免的要去阅读别人写好的代码。

我们自己也会使用不同的软件去做嵌入式的开发。这样前面学到的知识就有些力不从心了。所以,这期视频我要教大家一款新的软件。它就是Clion。这个也是很多嵌入式爱好者的首选编程软件下面就讲讲怎么安装?需要下载三个安装包。分别是STM32CubeMX,STM32CubeCLT和Clion。

视频画面 00:34.9
视频画面 00:34.9 · 1080P 原图 · 查看对应视频
视频画面 05:09.6
视频画面 05:09.6 · 1080P 原图 · 查看对应视频

在安装STM32CubeMX和STM32CubeCLT过程中是需要账号。如果一直跟着我视频学的小伙伴们肯定已经有账号了,没有的申请一个就行。接着,将三个安装包按视频顺序安装就可以了。没有什么难点。 打开Clion点击新建项目。在嵌入式这里找到STM32CubeMX,点击。

视频画面 02:06.5
视频画面 02:06.5 · 1080P 原图 · 查看对应视频
视频画面 03:38.1
视频画面 03:38.1 · 1080P 原图 · 查看对应视频

在这里我们可以看到STM32CubeMX。STM32CubeCLT后面都是有对钩的。说明我们的安装是没问题的,接着,来看这里提示目录不存在。这是因为我们还没正确的导入STM32项目呢?接着,点击启动STM32CubeMX。点击这里选择芯片型号。输入我们熟悉的STM32F103C8T6。

这里记得收藏一下,下次就好找了。接着,把这些常规的设置设置一下。这里和STM32CubeIDE的设置是一模一样的。第一步SYS这里Debug选择serial wire。第二步RCC这里选择外部晶振。第三步HCLK设置为72兆赫兹。第四步生成单独的.c和.h文件。这里有两点需要注意,把这里改成Cmake。

这里项目存储位置设置一下。我是在D盘直接建了个名为STM32CubeMX的文件夹。把刚才的路径复制到Clion。这时刚才那个提示就没有了。点击下一步,点击确定。这里我们可以看到免费试用29天。其实Clion现在非商用的话是完全免费。就是可以一直使用的,点击这里注册一下就可以了。

视频画面 05:55.4
视频画面 05:55.4 · 1080P 原图 · 查看对应视频

接下来我们通过Clion顺便实现小灯的闪烁案例。首先,打开gpio.c写一个MY_GPIO_Init的函数。在这里我要将PA15引脚设置为推挽输出的模式。

首先,开启GPIO的时钟。这里的代码提示功能是不是很智能?按下tab键代码就自动补全了。接着,定义一个GPIO初始化结构体。选择15号引脚,设置推挽输出。设置无上下拉,设置最高速度50兆赫兹。初始化PA15,接着把写好的函数复制到gpio.h中。之后在main函数中调用我们写好的函数,在while中实现闪烁功能。

接着,点击这里的绿色小三角程序就下载到板子里了。小灯就开始闪烁了。好的,今天的视频就到这里。如果觉得有收获,一定要关注、点赞、收藏,评论。你的支持是我这样的小博主持续更新下去的最大动力。期待下期视频再见!

第38期 HAL_GPIO_Init()源码深度解析

观看原视频

hello,小伙伴们,大家好,这里是左左右。上期视频我们使用CLion写了一个MY_GPIO_Init()的函数。将PA15引脚设置为推挽输出模式。我们先自己操作寄存器来试一下。

首先,定义一个32位指针指向GPIOA的CRH寄存器。接着,将MODE15设置为11,将CNF15设置为00。接着,我们就一起来看看HAL库源码是如何将PA15引脚设置为推挽输出模式的。

视频画面 00:33.5
视频画面 00:33.5 · 1080P 原图 · 查看对应视频

首先,我们新建了一个结构体变量。变量名为GPIO_InitStruct。我们打开GPIO_InitTypeDef这个结构体看一眼。里面一共就4个结构体成员。接着,我们给结构体成员赋值。这里HAL库都是使用的宏定义。方便程序的阅读和编写。找到他们对应的真实值并记录。接着,来看HAL_GPIO_Init()这个函数。

这里传入的第一个参数GPIOA是我们很熟悉的。GPIOA就是指向GPIO_TypeDef结构体的指针,而且GPIOA结构体指针中存放的地址就是GPIOA的基地址,而刚才定义的结构体变量以指针的形式传入HAL_GPIO_Init()函数作为第二个参数。这样做的目的是HAL_GPIO_Init()函数内部可以通过结构体指针的形式对结构体成员进行读取和赋值。

接着,进入HAL_GPIO_Init()函数内部看看。从代码数量来看,这个函数的代码行数是挺多的。接着,我们从代码结构来看看代码的复杂程度。首先,函数一进来先定义了一些变量。接着,就进入了while循环。while循环中只有一个if判断语句。如果条件不满足就执行position++。

if判断语句中又嵌套了switch选择语句和if判断语句,而这个判断语句是关于EXTI外部中断的,大概率和我们是无关的。这样来看这个函数本身并不复杂。接着,我们从头来看这个函数。此时while循环中GPIO_Init->Pin中的值就是GPIO_PIN_15。对应二进制数就是0x8000(即第15位为1)。

视频画面 02:42.3
视频画面 02:42.3 · 1080P 原图 · 查看对应视频

同理将while循环中的条件和变量ioposition、iocurrent用二进制数表示出来。随着while循环的执行,position递增。当position等于15时if判断条件成立,进入switch选择语句。由于我们设置的是推挽输出模式,switch选择语句会跳到这一行。

给config赋值。然后,就跳出switch选择语句了。这里的 ? :是 C 语言中的条件运算符,也叫三元运算符。这个也很简单,当问号前的条件满足就返回冒号后的第一个值。条件不满足就返回冒号后的第二个值。由于这里的iocurrent中的值(0x8000)大于GPIO_PIN_8(0x0100),所以,条件不成立。

返回冒号后的第二个值。此时configregister变量就是指向GPIOA的CRH寄存器的指针。registeroffset变量中的值是28,也就是对应MODE15和CNF15在CRH寄存器中的偏移量。按下CTRL点击MODIFY_REG这个函数。

视频画面 04:08.2
视频画面 04:08.2 · 1080P 原图 · 查看对应视频

首先,来看WRITE_REG。很简单就是将后面的值赋值到前面的变量,而READ_REG就是这个值本身。接着,来看MODIFY_REG。它执行的操作是先将第一个参数与第二个参数的取反值做按位与(即清零操作)。再与第三个参数做按位或(即置1操作)。之后将结果赋值到第一个参数。

这里先通过清零操作将GPIOA_CRH寄存器中MODE15、CNF15对应的位清零。接着,通过置位操作将GPIOA_CRH寄存器设置为推挽输出模式,也就是我们自己写的第二行、第三行代码的效果。通过对HAL库源码的学习,我们发现HAL还是很严谨的。在对GPIOA_CRH寄存器设置推挽输出模式前先进行了清零的操作。

视频画面 04:51.2
视频画面 04:51.2 · 1080P 原图 · 查看对应视频

现学现用。我们也对自己写的代码进行一个清零的操作。同时CLion这个软件也拥有强大的调试功能。点击这里的行号就会出现一个小红点,也就是我们所熟悉的断点。接着,点击这里的绿色小甲壳虫。点击键盘上的F7。代码就开始单行执行了。在这里我们还可以看到变量中的值。接着,点击这里的外设。

我这里是显示寄存器的,你们第一次打开会提示加载svd文件。接着,打开STM32CubeMX。点击Help下Docs and Resources找到System View Description并下载。下载完成后找到STM32F103.svd文件。把它复制到你能找到的地方。

视频画面 06:17.1
视频画面 06:17.1 · 1080P 原图 · 查看对应视频

返回CLion加载STM32F103.svd文件。找到GPIOA_CRH寄存器就可以看到MODE15和CNF15位的值了。接着,回到HAL_GPIO_Init()函数。

下面这个if条件(EXTI配置部分)不成立。position执行加加操作。此时的position值为16。while条件不成立。跳出while循环。HAL_GPIO_Init()函数到此结束。

好的,今天的视频就到这里。如果觉得有收获,一定要关注、点赞、收藏、评论。你的支持是我这样的小博主持续更新下去的最大动力。期待下期视频再见!

第39期 CLion实现按键控制LED小灯亮灭

观看原视频

hello,小伙伴们,大家好,这里是左左右,在前面的第8期视频中,我们用STM32CubeIDE通过对I O口的简单配置,和几行简单的代码,实现了按钮控制L E D灯亮灭的一个小案例,这期视频我们就使用Clion来实现相同的功能,与STM32CubeIDE开发的不同之处在于,我们要用自己写代码来配置I O口,通过原理图不难发现,按钮一端与PC13相连,另一端接地接地,当按钮被按下时,PC 13对应 I D R寄存器被置0,当按钮抬起时,PC13引脚受外部干扰,输入电平不确定,这时我们要将 PC 13引脚设置为上拉输入模式,提供一个相反的电平信号,接着我们就通过可以读取 PC 13对应 I D R寄存器中的值来判断,按钮是抬起还是按下状态,接着来看原理图,LED1一端与PA 15相连接,另一端接3.3V,当我们把PA 15对应 O D R寄存器被置0时,PA15向外输出0V的低电平,小灯两端形成电压差,小灯被点亮,当我们把PA 15对应ODR寄存器被置1时,PA 15向外输出3.3V的高电平,小灯两端无电压差,小灯熄灭,这时我们通过判断 PC 13对应 I D R寄存器中值的变化,来操作PA15对应 ODR寄存器中的值,就实现了按键控制小灯亮灭的功能了,好多小伙伴们会问,为什么这里是PC 13和 PA 15这两个引脚,其他引脚不可以吗?

这是因为我们的电路板就是这样设计的,在设计电路板前要先绘制原理图,规划s t m 3 2芯片各个引脚要与什么样的元器件连接,实现什么样的功能,接下来才是绘制电路板,当电路板生产完成后,原理图就成了这块电路板的说明书了,因为直接看电路板是看不出来具体引脚的连接方式的。

接着打开我们上期视频建好的CLion项目,将它复制一份出来,打开项目文件删掉文件名为.idea的文件夹,这个文件夹包含了旧项目的所有CLion专属配置,在新项目中使用会导致冲突。紧接着打开Clion,点击新建项目,位置这里输入我们刚才的路径,点击继续,接着将之前写好的代码全部删掉,这时这个项目就是个模板项目了,同样我们打开这个模板项目的所在位置,删除.idea的文件夹,之后每次我们在创建新项目时,把它复制一份出来就可以了,接着打开复制好的项目,首先采用寄存器的写法来实现按键控制 L E D小灯亮灭的案例,这里为了方便大家的理解,我就直接就用二进制数了,首先将 PC 13引脚设置为上拉输入模式,这里我们定义指针指向 GPIOC_IDR寄存器地址,这是因为待会我们要读取这个寄存器中值来判断按键是否被按下,接着将PA15设置为推挽输出模式,然后在whlie循环中判断按键是否被按下,当 PC13对应 IDR寄存器值为0时,说明按键被按下了,由于按键刚被按下时,会有发生抖动,这时我们加一个10毫秒的延时将这段抖动延时过去,接着继续判断按键状态如果被按下,就翻转 PA15电平信号,也就是翻转PA15对应ODR寄存器中的值,这里我们要增加一个新的知识点,前面我们讲过位的“或”运算和“与”运算,这里我们讲一个新的位运算符,那就是位的“异或”运算,符号是这样,也很简单,两个值相同就为零,不相同为1,当 PA15对应 ODR中对应值为1时,与这个值做异或运算,结果为0,当 PA15对应 ODR中对应值为0时,与这个值做“异或”运算,结果为1,而且对其他位没有任何影响,是不是很巧妙的实现了电平翻转效果,接着等待按键被抬起,就跳出while循环,虽然我们在按下按键到抬起按键的时间很短暂,但程序的执行速度远大于这个时间,所以这里加个while循环做等待,最后就是用Hell库实现相同的功能,这个就很简单了。

视频画面 03:02.2
视频画面 03:02.2 · 1080P 原图 · 查看对应视频
视频画面 04:39.5
视频画面 04:39.5 · 1080P 原图 · 查看对应视频
视频画面 05:28.1
视频画面 05:28.1 · 1080P 原图 · 查看对应视频
视频画面 06:16.7
视频画面 06:16.7 · 1080P 原图 · 查看对应视频

最后强调一点我手写的代码大家一定要自己手动写一遍,而且自己写的过程肯定会出问题,有问题解决问题才能真正掌握对应的知识点,,好的,今天的视频就到这里,如果觉得有收获,一定要关注、点赞、收藏,评论,你的支持是我这样的小博主持续更新下去的最大动力,期待下期视频再见!

第40期 HAL_GPIO_ReadPin()、HAL_GPIO_TogglePin()、HAL_GPIO_LockPin()源码详解

观看原视频

40、HAL_GPIO_ReadPin()、HAL_GPIO_TogglePin()、HAL_GPIO_LockPin()源码详解。

视频画面 06:48.4
视频画面 06:48.4 · 1080P 原图 · 查看对应视频

hello,小伙伴们,大家好,这里是左左右。在前面视频中我给大家讲解了GPIO所有相关寄存器。接着,我们打开stm32f1xx_hal_gpio.c这个文件。这里的HAL_GPIO_Init()和HAL_GPIO_WritePin()。这两个函数的源代码我们是做过深度解析的,而这里的HAL_GPIO_DeInit()、HAL_GPIO_EXTI_IRQHandler()、HAL_GPIO_Callback()。

视频画面 00:35.3
视频画面 00:35.3 · 1080P 原图 · 查看对应视频

这三个函数是和EXTI外部中断相关的。所以,我们放到后面的视频讲解。这时GPIO相关函数就剩下。HAL_GPIO_ReadPin()、HAL_GPIO_TogglePin()、HAL_GPIO_LockPin()这三个函数了。这期视频我们就把这三个函数的源代码一网打尽。

在讲他们之前我先穿插的讲一个C语言的知识点,那就是枚举类型。在实际应用中,有些变量的取值范围是有限的,就比如说人的性别只有男和女两种。不可能会有第三种性别了。再比如说投出去的硬币落地后。只有正面和反面。当然,你要说可以是立着的。也可以把它加进去,还有比如三原色有红、绿、蓝。

现实中这种情况是数不胜数的,当然,在写程序时,你可以用0代表男性,1代表女性。但是,这时只有写程序的人才知道0,1代表什么。这样代码的可读性就会变得很差。这时就引出枚举这个概念了。

首先,我们定义一个性别的枚举,把鼠标移动到枚举成员这里。我们可以发现male默认为0,female默认为1,也就是说枚举成员默认情况下是从0开始依次递增的。接着,我们定义一个枚举变量。打印输出来看一下。结果就是0和1。和我们预想的一模一样。接着,来看枚举成员占用内存空间大小。

打印输出结果为4。也就可以推断枚举成员是int类型。为了简化枚举变量的定义,我们可以在这里加个typedef。然后,把枚举名称放到后面。这时定义枚举变量就可以不用加enum了。这个方法在定义结构体时也用到了。听过前面视频的小伙伴应该并不陌生。当然,我们也可以将这里的默认值设置为其它值。

视频画面 02:55.2
视频画面 02:55.2 · 1080P 原图 · 查看对应视频

这时female值也是递增1。当然,我们也可以根据自己的实际需求定义female的值。好的,关于枚举类型的介绍就到这里了。接着,一起来看一下上期视频写好的代码。按下Ctrl跳转到定义HAL_GPIO_ReadPin()这个函数的地方。这里首先定义了一个枚举类型的变量。GPIO_PinState就是枚举类型。

视频画面 04:28.5
视频画面 04:28.5 · 1080P 原图 · 查看对应视频

代表GPIO的引脚状态。GPIO_PIN_RESET代表低电平。对应值为0。GPIO_PIN_SET代表高电平。对应值为1。当PC13对应IDR寄存器值为1时,这里运算结果是一个非零的值。if判断条件成立。返回高电平。当PC13对应IDR寄存器值为0时,这里运算结果为0。

视频画面 05:15.1
视频画面 05:15.1 · 1080P 原图 · 查看对应视频

if判断条件不成立。返回低电平。这里写法常常用来判断寄存器中某一位中的值是否为零?大家一定要记住。接着,来看HAL_GPIO_TogglePin()这个函数。这里首先定义了一个名为odr的变量。接着,把GPIOA中odr寄存器中的值赋值到odr变量。接着,来看这行代码。

虽然看起来挺复杂,但是,我们知道它肯定是要实现电平的翻转功能。首先,我们假设此时PA15为高电平。来看一下这里的运算结果。我们将运算结果放入GPIOA BSRR寄存器。这时PA15就实现了电平翻转的目的了。这里我们假设PA15为高电平。你们也可以试试假设PA15为低电平。

是不是也能实现电平翻转的功能?接下来我们再来看看HAL_GPIO_LockPin()这个函数。这里假设我们要锁定的IO口还是PA15。首先,定义变量tmp。它存放的值其实就是1左移16位。接着,来看这里的SET BIT。它就是两者做或运算。

接下来就是将GPIOA LCKR寄存器中位16。进行置1、置0、置1、和读1的操作了。这个和我们前面讲的是一模一样的,接下来判断位16是否被置1?当条件成立,说明PA15就被锁了。到目前为止我们将GPIO相关寄存器和源码全部学习完了。我也希望大家能有所收获。如果有收获,大家可以把“有收获”三个字打到弹幕上。

今天的视频就到这里。下期视频我们就又开始新的章节了。一定要关注、点赞、收藏,评论。您的支持是我这样的小博主持续更新下去的最大动力。期待下期视频再见。

第41期 EXTI外部中断寄存器详解(上)

观看原视频

Hello,小伙伴们,大家好,这里是左左右。这期视频我们来讲一下EXTI外部中断相关寄存器。我们都知道EXTI外部中断只是中断中的一种。如果之前你对中断的概念还有点模糊,没关系,我们先用几分钟快速梳理一下。假设没有中断,主程序会从头到尾顺序执行,但是,如果我们在程序的编写过程中加入多个中断。

并且按照NVIC分组2进行分组,也就是抢占优先级和响应优先级各占两位。当中断1被触发时,主程序直接就被打断了。当 中断2被触发时,我们都知道不管是抢占优先级还是响应优先级。都是值越小,优先等级越高。此时中断2的抢占优先级中的值大于中断1。所以,中断2的抢占优先级要低于中断1。

视频画面 00:27.7
视频画面 00:27.7 · 1080P 原图 · 查看对应视频

它只能在这里排队等候。接着,中断3被触发。它的抢占优先级中的值小于中断1。中断1就被打断了。接着,中断4被触发。它的抢占优先级和响应优先级和中断2一模一样。所以,它也只能排队等候。但是,该如何判断他们执行的先后顺序呢?这时就要打开stm32手册。找到中断向量表。在这个表中我们可以看到,灰色这部分被称为内核中断。

用来处理单片机异常。剩下的这些中断就是内核外中断了。这里每个中断都有一个固定的位置序号。当两个中断软件优先级相同时,谁排在前面,谁就先执行。这里的DMA位置序号小于USART。所以,当DMA和USART抢占优先级和响应优先级相等时。DMA中断要排到USART中断前面。随着时间的推移,所有中断都被执行完毕了。

视频画面 02:01.8
视频画面 02:01.8 · 1080P 原图 · 查看对应视频

那么,谁在背后管理这些打断和排队的逻辑。它就是NVIC,嵌套向量中断控制器。也被称为中断管理员,实际上,stm32芯片可以说是由两部分组成。分别是Cortex M3内核和片上外设。NVIC就住在这个内核里。前面中断向量控制表中的灰色部分的中断也在Cortex m3内核。

其他中断就在片上外设了。那么GPIO是如何变成中断引脚的?GPIO本身只能输入输出。我们需要借助 AFIO这个引脚复用选择器。把引脚连接到EXTI上。由于在实际项目中,我们不可能把所有引脚都设置为EXTI外部中断。为了节省资源,单片机将所有引脚分为16组,每组包括PA0到PG0,PA1到PG1。

一直到PA15到PG15,而且每组只能选择一个引脚作为EXTI外部中断,也就是说外部引最多只有16个通道到EXTI外部中断。与EXTI外部中断相连的还有。PVD电源检测,RTC闹钟,USB检测,以太网唤醒与EXTI相连。共计20个中断通道到NVIC。接着,我们来看一下EXTI相关寄存器。

视频画面 03:04.5
视频画面 03:04.5 · 1080P 原图 · 查看对应视频

凭着前面知识的积累。我们可以推断出EXTI相关寄存器。肯定包括上升沿触发选择寄存器和下降沿触发选择寄存器。当上升沿触发选择寄存器被激活时,上升沿信号会触发中断。当下降沿触发选择寄存器被激活时,下降沿信号会触发中断。当上升沿触发选择寄存器和下降沿触发选择寄存器被激活时,双边沿信号会触发中断。

视频画面 03:35.9
视频画面 03:35.9 · 1080P 原图 · 查看对应视频

这里假设上升沿触发选择寄存器被激活。当stm32引脚输入上升沿信号时,通过边沿检测电路检测到上升沿信号。中断被触发,移交到NVIC中断控制器。等待进入主程序。但实际情况要比我们想象的要复杂一些。EXTI相关寄存器还包括软件中断事件寄存器。请求挂起寄存器,中断屏蔽寄存器。

他们通过外设接口与APB总线相连。软件中断事件寄存器和边沿检测电路与或门相连。这里的或门就是二选一。或门的输出结果经请求挂起寄存器和中断屏蔽寄存器经“与门“到达NVIC。这里的与门是只要有一个条件不满足。结果就不满足。这里的或门结果有两条路可选。

视频画面 04:38.6
视频画面 04:38.6 · 1080P 原图 · 查看对应视频

另外,一条路是与事件屏蔽寄存器经与门到脉冲发生器。产生脉冲信号,也就是产生中断事件。这个脉冲信号可以作为其他片上外设的触发源。所以,EXTI的整个工作流程就是,引脚信号 或 软件触发 到 挂起寄存器置位。如果屏蔽寄存器允许,NVIC 响应。如果屏蔽寄存器关闭,信号就只能走事件通道。

今天的视频就到这里。一定要关注、点赞、收藏,评论。您的支持是我这样的小博主持续更新下去的最大动力。期待下期视频再见!

第42期 EXTI外部中断寄存器详解(下)

观看原视频

Hello,小伙伴们,大家好,这里是左左右。上期视频我们讲解了EXTI外部中断寄存器的基本功能。这期视频我们就来看看每个寄存器具体是怎么设置的。但在讲寄存器之前。我们要知道中断信号是如何从stm32引脚进入外部中断的。前面我们讲过GPIO口的基本结构图。当IO口被设置为输入模式时,这里有一条线路是这样的,中断信号经斯密特触发器到到复用功能输入。

视频画面 00:28.8
视频画面 00:28.8 · 1080P 原图 · 查看对应视频

信号经过AFIO这个引脚复用选择器后。才到达EXTI外设。那AFIO起到什么作用呢?AFIO相当于一个“信号调度员”。它负责告诉引脚去和哪个片上外设相连。控制信号通路的“开关”。理解了这一点。后面的寄存器配置就顺理成章了。这时的引脚就不再是一个普通的引脚了。当然,这里的普通引脚指的就是只具有通用输入输出功能的引脚。

所以,当我们要将指定引脚配置为EXTI外部中断引脚时。要分三步走。第一步我们要将这个引脚配置为输入模式。这个我们在前面分多期视频讲过。对大家来说肯定是小菜一碟。第二步就要配置AFIO寄存器了。在手册的第28页我们可以看到,AFIO的基地址是0x40010000。

同时,把EXTI的基地址也记一下。待会我们会用到,在手册里,AFIO中与EXTI相关的寄存器叫AFIO_EXTICR1到AFIO_EXTICR4。以AFIO_EXTICR1为例。它的低16位分成4组,每组4位。分别对应EXTI0到EXTI3。假设我们要把PC2作为EXTI2的中断源,只需要在AFIO_EXTICR1中找到EXTI2对应的那4位。

设置为0010。就代表选择GPIOC。这里的x就是和EXTI后面的序号一致。寄存器AFIO_EXTICR1到AFIO_EXTICR4的位含义都是一模一样的。只是对应不同EXTI通道而已。第三步就要配置EXTI相关寄存器了。

视频画面 02:09.9
视频画面 02:09.9 · 1080P 原图 · 查看对应视频

首先,来看中断屏蔽寄存器。这里的位0到位15对应EXTI的16个通道。位16到位19对应这4个中断。置0时,对应通道中断请求被屏蔽。置1时,对应通道中断请求被放开。事件屏蔽寄存器(EXTI_EMR)和中断屏蔽寄存器(EXTI_IMR) 位含义是一模一样的。只不过它针对的是事件屏蔽。

上升沿触发选择寄存器(EXTI_RTSR)。置0时:禁止输入线上的上升沿触发。置1时:允许输入线上的上升沿触发(中断和事件)。下降沿触发选择寄存器(EXTI_FTSR)。置0时:禁止输入线上的下降沿触发。置1时:允许输入线上的下降沿触发(中断和事件)。软件中断事件寄存器(EXTI_SWIER)。

视频画面 03:17.3
视频画面 03:17.3 · 1080P 原图 · 查看对应视频

这个也很简单,当我们把其中某一位置1时。对应通道就会产生一个中断信号。挂起寄存器(EXTI_PR)。当EXTI中断被触发,挂起寄存器(EXTI_PR) 对应位就被硬件自动置1了。在该位中写入’1’可以清除它。

接下来就出几道思考题吧。第一题:如何通过寄存器配置将PC13引脚设置为下降沿触发中断?首先,设置PC13为上拉输入模式。这个以前讲过很多次了,这里就不赘述了。接着,设置AFIO寄存器,将PC13引脚设置为EXTI外部中断引脚。

视频画面 04:58.3
视频画面 04:58.3 · 1080P 原图 · 查看对应视频

最后,就是设置EXTI相关寄存器了。配置下降沿触发选择寄存器对应引脚为1。然后,将中断屏蔽寄存器置1。第二题:如何通过寄存器配置将PA15设置为下降沿事件触发?

首先,设置PA15为上拉输入模式。接着,设置AFIO寄存器,将PA15引脚设置为EXTI外部中断引脚。最后,就是设置EXTI相关寄存器了。配置下降沿触发选择寄存器对应引脚为1。然后,将事件屏蔽寄存器置1。今天的视频就到这里。一定要关注、点赞、收藏,评论。您的支持是我这样的小博主持续更新下去的最大动力。

期待下期视频再见!

第43期 EXTI外部中断实例解析

观看原视频

Hello,小伙伴们,大家好,这里是左左右。这期视频我们会通过一个非常经典的按键控制LED小灯的例子。彻底搞懂中断的工作流程以及在实际开发中的正确姿势。在之前的视频中,我们写过一个按键控制LED亮灭的代码。当时,我们把按键检测的逻辑直接放在了main函数的。while循环里面。

大概是这样的,循环里不断读取按键IO口的电平。如果判断按键被按下,就翻转LED的状态。这种方式虽然能工作。但存在一个很明显的缺陷。主程序被阻塞了。大家可以想一下。按下按键这个动作是随机的,而且不是持续发生的,但是,我们的程序却要在while循环里一刻不停地去读按键。那他还怎么去干别的活呢?

这就是所谓的“轮询”方式。它会占用CPU的全部时间。导致其他任务无法及时执行。说通俗点。主程序什么都干不了了。只能守在那里等待按键。那有没有更好的办法呢?当然,有!既然按键动作是随机发生的,我们就应该让它“主动通知”CPU,而不是让CPU去“主动问”。这就是中断的典型应用场景,把按键检测放到中断里面。

CPU平时可以安心处理其他事情。一旦按键被按下。硬件就会自动触发中断。暂时打断当前任务。去执行我们预先写好的中断处理程序。执行完再回来继续原来的工作。这样一来,主程序就不会被阻塞了。好,理论说完了。我们打开STM32CubeIDE。

首先,将引脚设置为外部中断触发。由于我们的按钮是与地相连的,所以,我们将P C 13引脚设置为下降沿触发。输入模式为上拉输入。同时,在 N V I C中将中断使能。点击保存代码就自动生成了。实际看一下中断在底层到底是怎么被调用的?我们都知道。中断是由硬件自动调用的,那它到底调用了哪个函数呢?

视频画面 02:42.3
视频画面 02:42.3 · 1080P 原图 · 查看对应视频

我们可以打开启动文件startup_stm32f103c8tx.s。这个文件是用汇编写的,里面定义了各种中断向量。仔细看,你会看到一大堆以IRQHandler结尾的函数名。这些就是硬件发生中断时自动调用的入口函数。比如,我们这次要用到的按键。它接在哪个IO口呢?我们这里用的是PC13。

视频画面 04:51.2
视频画面 04:51.2 · 1080P 原图 · 查看对应视频

那对应的外部中断通道就是EXTI13。它属于EXTI15_10这一组。所以,我们就能看到EXTI15_10_IRQHandler这个函数。当按键按下触发中断后,硬件就会自动跳转到这个函数。我们点进去看它的实现过程。发现这个函数里调用了HAL_GPIO_EXTI_IRQHandler。

视频画面 03:25.3
视频画面 03:25.3 · 1080P 原图 · 查看对应视频

并且传入了对应的GPIO引脚。再继续往下追。在HAL_GPIO_EXTI_IRQHandler这个函数内部,它又会调用一个叫做HAL_GPIO_EXTI_Callback的函数。注意看,这个回调函数前面有一个__weak关键字。在C语言中表示“弱定义”,什么意思呢?就是说?

如果你自己没有写这个函数,编译器就会用这个默认的,什么都没做的弱函数。如果你自己写了一个同名的函数,那编译器就会用你的版本。覆盖掉弱定义。HAL库把中断的处理流程固定好了。但真正要做的事情。留给我们去实现。所以,结论很简单。我们想要在中断发生时执行的自定义代码都应该写在HAL_GPIO_EXTI_Callback这个函数里面。

我们将之前写好的代码复制到这里就可以了。点击运行这时我们发现按下按钮并不能实现小灯的亮灭功能。这是为什么呢?这时我们将HAL_Delay这个延时函数更换为我们之前自己写的my_delay延时函数。点击运行,程序正常执行了。我想此时大家心中应该已经有答案了。HAL_Delay和my_delay这两个函数的区别就是HAL_Delay使用了滴答定时器这个中断。

视频画面 05:34.1
视频画面 05:34.1 · 1080P 原图 · 查看对应视频

EXTI本身就是中断。它打断了主程序,而HAL_Delay中也包含中断,也就是要实现中断的嵌套。这就要求滴答定时器的抢占优先级要高于EXTI。打开NVIC来看一下。这里的滴答定时器抢占优先级确实低于EXTI。这里我们只要保证滴答定时器抢占优先级确实高于EXTI就可以了。

点击运行,程序是不是也能正常执行了?由于这里是EXTI15-10的中断,这里也可能是除PC13外的其他引脚触发的中断。谨慎起见我们在这里加个if判断。确保是PC13触发的中断才去执行我们这段代码。其实,这段代码还是存在一定问题的,这里加入HAL_Delay的延时其实并不合适。

虽然10毫秒对于我们来说是一瞬间的事,但是,对于单片机来说是很漫长的,中断讲究“短且快”。所以,在中断里使用延时是并不合适的,但是,在这个案例中使用是没有问题的,随着视频内容的推进,我们会讲更合适的方法来处理这个问题的。我只是想通过这个案例让大家熟悉中断和中断嵌套的相关知识点。

好的,今天的视频就到这里。下期视频我们将使用CLion使用寄存器的方式完成相同的案例。一定要关注、点赞、收藏,评论。您的支持是我这样的小博主持续更新下去的最大动力。期待下期视频再见!

找出这篇stm32f103c8t6单片机口播文案存在的错误,,形成一篇2500字左右的完整口播文案。

首先,将引脚设置为外部中断触发,由于我们的按钮是与地相连的,所以我们将P C 13引脚设置为下降沿触发,输入模式为上拉输入,同时在 N V I C中将中断使能,点击保存代码就自动生成了,

第44期 如何使用寄存器实现EXTI外部中断

观看原视频

44。Hello,小伙伴们,大家好,这里是左左右。这期视频我们就通过配置EXTI相关寄存器。实现按键控制LED亮灭的功能。我们不依赖任何IDE的图形化配置和代码的自动生成功能。直接操作寄存器。这样做虽然有一定难度。但是,能让你真正理解单片机的中断机制和底层原理。为了方便大家理解,我画了一个思维导图。

重点关注三个部分。LED、按键、外部中断、下面我们就按这个思路一步步写代码。首先,来看LED。其一端与PA15相连,一端接3.3V高电平。LED初始化需要两步。第一步开启GPIOA时钟。第二步设置PA15为推挽输出模式。接着,来看按键。它的一端与PC13相连,另一端与地相连。

由于这里要用到PC13引脚的EXTI外部中断的复用功能,设置会相对复杂。第一步,开启GPIOC和AFIO的时钟。第二步,配置PC13引脚。由于按键被按下时,PC13输入低电平,所以,我们要将PC13设置为上拉输入模式。第三步配置AFIO。第四步配置EXTI。由于按键被按下时,PC13输入低电平。

视频画面 06:06.3
视频画面 06:06.3 · 1080P 原图 · 查看对应视频

所以 将EXTI13设置为下降沿触发。这里的第二步到第四步就是我们前面讲的三步走。最后,一步就是设置NVIC了。这里需要设置EXTI和SysTick两个中断。需要注意的是要设置SysTick的抢占优先级高于EXTI。接着,来看EXTI外部中断。这里只需要将LED翻转电平的逻辑放到这里就可以了。

接下来我们就按这个思维导图一步一步来实现就可以了。首先,将我们做好的模版文件复制一个出来。点击新建项目。位置这里将刚才复制好的新文件路径复制过来。我们来看一下。这里的项目名称虽然更改过来了。但是,这里还没有更改过来。有的小伙伴就说了我有强迫症,看着别扭。那我们就将它也改过来。

首先,打开项目对应文件夹。找到CMakeLists.txt文件。找到Set the project name将后面的内容改为我们想要的项目名称就可以了。接着,我们新建两个.c和.h文件。这里提示此文件不属于任何项目。这是因为我们新建的这4个文件。系统并不知道他们归属于哪个项目。

我们点击这里的修正,将他们放入对应的项目中就可以了。接着,将这两个.h文件里的内容补齐了。之后打开led.c文件写一个LED初始化的函数。首先,我们要导入HAL库的头文件。这里可以看到头文件显示为灰色。这是因为我们没有调用里面的任何函数。后面只要调用任意头文件里的函数。

这里就会变亮了。第一步开启GPIOA时钟。第二步设置PA15为推挽输出模式。之前我们一直都是自己定义一个指针。存放着我们要操作寄存器的地址。通过解引用的形式来改变对应寄存器中的值。但是,通过前面那么多期HAL库源码深度解析的视频。我们都知道HAL库源码本质也是在操作寄存器,而且往往都是通过结构体指针的形式来改变相应寄存器中的值。

那么,我们直接用hal库写好的结构体。来操作寄存器就省去了自己定义指针的环节了。我们来试着写一下。接着,打开key.c文件写一个KEY初始化的函数。第一步,开启GPIOC和AFIO的时钟。第二步,配置PC13引脚为上拉输入模式。第三步配置AFIO。第四步配置EXTI,将EXTI13设置为下降沿触发。

视频画面 04:47.6
视频画面 04:47.6 · 1080P 原图 · 查看对应视频

接着,开启中断。这最后一步就是设置NVIC了。首先,设置分组。接着,打开定义这个函数的地方。这里的分组和我们前面讲的一模一样。我们将其设置为分组4。接着,设置EXTI15到10的抢占优先级和响应优先级,使能中断。同理设置SySTick的抢占优先级和响应优先级,使能中断。

同时,保证SysTick的抢占优先级高于EXTI。最后,一步设置外部中断。这里外部中断函数是固定的,我们把它复制过来。当PC13中断被触发是,EXTI_PR寄存器中13位被硬件自动置1。所以,首先要判断这里的值是否为1?如果为1说明PC13 EXTI外部中断被触发了,接着,通过写1来清除位13。

视频画面 08:43.8
视频画面 08:43.8 · 1080P 原图 · 查看对应视频

这里就可以执行PA15引脚翻转的动作来实现LED灯亮灭的功能了。其实,这里的逻辑也很容易理解。就比如我们去那些需要购票旅游景点。进园前会有一个检票口。检票口其实有两个目的,第一个目的就是看你有没有票。如果没有票你肯定是不能进去的,对应就是EXTI_PR位中的值是否为1?

为1就可以进入中断。第二个目的就是如果你有票。对方会在上面做标记。证明你已经来过了。如果不在票上做标记,你不就可以买一张票天天来玩吗?对应就是通过写1来清除对应位。如果这1位不被清除,程序不就被卡在中断里一直执行下去了吗?接着,在main点c这个文件中加入我们写好的头文件。

视频画面 10:02.5
视频画面 10:02.5 · 1080P 原图 · 查看对应视频

在main函数中调用我们写好的函数。此时这个项目正常情况会一直在while循环中执行。尽管现在这里是空的,只有当按键被按下时才会去执行中断函数中的任务。中断执行完成后继续在while循环中执行。这时点击运行。程序功能是不是被正常实现了?

接下来我们就用HAL库来实现相同的功能。这里的模式我们选择下降沿触发中断。其他和我们之前讲过的是一模一样的,此时配置寄存器时的第三步和第四步就可以省略了,也就是说HAL库已经帮我们配置好AFIO。EXTI的相关寄存器了。点击运行。程序也可以正常执行了。这期视频我们完整地实现了寄存器版本和HAL库版本的按键中断控制LED。

如果你跟着做一遍,相信你对EXTI、NVIC、GPIO和AFIO的理解会上升一个台阶。好的,今天的视频就到这里。下期视频我们将深度解析HAL库中关于EXTI的相关函数。一定要关注、点赞、收藏,评论。您的支持是我这样的小博主持续更新下去的最大动力。期待下期视频再见!

这里为什么是数组的形式呢?这是因为WXTICR这四个寄存器的位含义是一模一样的。所以HAL库将他们放到了一个数组里。

第45期 HAL_DeInit()源码深度解析

观看原视频

Hello,小伙伴们,大家好,这里是左左右。前面视频我们把hal_gpio.c这个文件中。关于GPIO 初始化、配置相关的函数都细细拆解完了。从这期开始。咱们继续啃这个文件里剩下的源码。今天的主角就是 HAL_GPIO_DeInit 这个函数。老规矩,咱们先找一个最直观的例子,把函数用起来,再跳进源码去读。

首先,我们打开按钮控制LED小灯亮灭的案例。在while循环中加入HAL_GPIO_DeInit(GPIOC,GPIO_Pin_13)这个函数,接着,跳转到定义这个函数的地方去看看。一进来你就会发现。它的整体结构其实和前面讲过的HAL_GPIO_Init()是十分相似的。

首先,定义变量。接着,检测传入参数的合法性。接下来就是一个while循环了。在循环里藏着两个 if 判断,当条件成立时进入判断语句,条件不成立时。执行position++的操作。观察完整体结构。咱们就从头开始。一行一行地把这个函数读懂。这里一共定义了5个变量。分别是position,iocurrent,tmp,registeroffset。

视频画面 01:29.8
视频画面 01:29.8 · 1080P 原图 · 查看对应视频

还有32位的指针configregister。由于这里要做位运算,所以,我们将GPIO_Pin_13转换为二进制数。随着position++,while循环中的条件持续向右移动。此时iocurrent中的值一直等于零。只有当position等于13时。iocurrent中的值才是一个非零的数。

视频画面 05:49.3
视频画面 05:49.3 · 1080P 原图 · 查看对应视频
视频画面 06:41.2
视频画面 06:41.2 · 1080P 原图 · 查看对应视频

if判断条件成立。接着,将这里的position也就是13转换为二进制数。将其右移两位。对应十进制数为3。此时tmp中存放的就是寄存器AFIO EXTICR4中的值。通过这里的运算就将EXTI13中的值提取出来了,也就是0010。接着,来看GPIO_GET_INDEX。

视频画面 03:13.6
视频画面 03:13.6 · 1080P 原图 · 查看对应视频

由于这里我们传入的是GPIOC,所以,返回值为2,将它转换为二进制数。左移4位。接着,来看CLEAR_BIT()这个函数。这里就是我们熟悉的将寄存器中某一位实现置零的操作。通过这几行代码就将EXTI IMR寄存器、EXTI EMR寄存器。EXTI FTSR寄存器、EXTI RTSR寄存器位13的值全部置零了。

接着,给变量tmp赋值。接着AFIO EXTICR4寄存器中EXTI13这四位就被置零了。这三行代码是大家熟悉的了。指针configregister存放的就是GPIOC CRH寄存器的地址了。registeroffset存放的是PC13对应的偏移值。MODIFY_REG这个函数就同时实现了复位和置位的操作。

此时可以发现PC13对应MODE位和CNF位中的值都是复位后的值。通过对HAL_GPIO_DeInit()这个函数的源码解析。我们也能够知道它要实现的主要功能就是关闭对应引脚的EXTI外部中断功能。将对应AFIO和EXTI寄存器中对应引脚全部复位。

同时,将对应引脚的输入输出模式设置为复位值,也就是浮空输入模式。这里的复位值指的就是我们不对引脚做任何设置。单片机上电后的初始状态,接着点击运行来验证一下。这里可以看到按键在第一次被按下时。小灯电平实现了翻转功能。后来再按按键就不起作用了。这是因为只有在第一次PC13引脚是具有外部中断的功能的。

视频画面 04:57.4
视频画面 04:57.4 · 1080P 原图 · 查看对应视频

之后PC13引脚就被复位了。也就失去了外部中断的功能了。所以,再按按键就不管用了。接下来就出几道思考题来考考大家吧。第一题:变量position在这个函数中的作用是什么?随着while循环的执行,position从零开始递增。position其实在遍历引脚号。用于指示当前正在处理哪一个引脚。

只有当对应引脚值为1时。判断条件才会成立。程序才会执行判断语句中的代码。它的值会用于计算 AFIO 的 EXTICR 数组索引和内部 4 位偏移值。也会通过移位运算定位到 GPIO 配置寄存器CRL或CRH中的对应位域。第二题,这里的iocurrent中的值和传入参数GPIO_Pin一模一样。

是不是有些多余?这里这样写有什么优点呢?这个函数其实可以同时处理多个引脚的,这里假设要同时处理PC13、PC14、PC15。在函数中我们只需要用"|"这个符号隔开就可以了。这时随着while循环的执行。PC13、PC14、PC15就会被执行相同的操作了。在前面的视频中,如果我们要将IMR位15置零,我们会在这里写入0b1 << 15。

这里的运算结果和iocurrent是一模一样的。这个固定的值也被称作掩码。英文常用mask。第三题,这里的position-8左移两位的结果为什么正好是传入引脚对应MODE位和CNF位的偏移值?这里首先假设将十进制数1。左移2位。相当于在原来的基础上乘以10的二次方。同理将二进制数左移2位。

相当于在原基础上乘以2的2次方,也就是4。由于在stm32中引脚0到7使用的是CRL寄存器,引脚8到15使用的是CRH寄存器,也就是说在CRH寄存器中。序号是从8开始的,所以,要用引脚号减8,而每个引脚占4位。所以(position - 8) * 4得到的正好是该引脚在CRH寄存器中的偏移值。

学而不思则罔,思而不学则殆。今天的源码看似不长。但里面藏着寄存器设计、位运算、多引脚遍历和 HAL 库的统一接口这么多知识点。只有多想、多练,才能真正变成自己的东西。好了,今天的视频就到这里。下期视频,我会继续围绕 EXTI 相关源码做更深入的解析。带你摸清外部中断从初始化到触发响应的完整链路。

一定要关注、点赞、收藏、评论。你们的支持是我这样的小博主持续更新下去的最大动力。期待下期视频再见。

第46期 外部中断HAL库源码深度解析(完结篇)

观看原视频

Hello,小伙伴们,大家好,这里是左左右。这期视频我们来看看当PC13被设置为下降沿触发中断时。HAL_GPIO_Init()内部做了些什么?这里当position等于13时,if判断条件成立。由于我们将PC13设置为上拉输入模式,所以,这里的if判断成立。变量config中的值为0b1000。

GPIOC BSRR寄存器中BSR位13被置1,也就是GPIOC ODR寄存器位13被置1了。接着,通过这里的MODIFY_REG函数。PC13就被设置为上拉输入模式了。一直往下看,这里判断条件成立。这里开启了AFIO的时钟。这也就意味着我们前面写的那段代码中。开启AFIO时钟这一行是可以省略的,接着,将AFIO EXTICR4寄存器中的值赋值到变量tmp中。

视频画面 00:49.1
视频画面 00:49.1 · 1080P 原图 · 查看对应视频

通过这几行代码就将PC13引脚设置为外部中断模式了。由于我们前面设置的是下降沿触发中断,所以,第一个if判断不成立。EXTI RTSR寄存器中位13被置零。第二个if判断成立。EXTI FTSR寄存器中位13被置1。第三个if判断不成立。EXTI EMR寄存器中位13被置零。

视频画面 01:38.6
视频画面 01:38.6 · 1080P 原图 · 查看对应视频

第四个if判断成立。EXTI IMR寄存器中位13被置1。通过这个函数就将BSRR、ODR、AFIO、EXTI寄存器相关位都设置完成了。HAL库源码的操作和前面讲过的寄存器设置是一模一样的。接着,来看HAL_GPIO_EXTI_IRQHandler()这个函数。这里就是我们熟悉的验票操作。

视频画面 02:28.1
视频画面 02:28.1 · 1080P 原图 · 查看对应视频
视频画面 03:42.3
视频画面 03:42.3 · 1080P 原图 · 查看对应视频

第一步看有没有票。就是判断EXTI PR寄存器位13是否为1?第二步就是在票上做标记。就是向EXTI PR寄存器位13写1。这时位13就被清除了。接着,就调用了HAL_GPIO_EXTI_Callback这个函数。前面视频我们是自己写的“验票”逻辑。其实,这里HAL已经帮我们写好了。

视频画面 02:52.8
视频画面 02:52.8 · 1080P 原图 · 查看对应视频

我们直接调用HAL_GPIO_EXTI_IRQHandler()这个函数就可以了。接着,重写HAL_GPIO_EXTI_Callback这个回调函数。将电平的翻转逻辑放到里面就可以了。为了谨慎起见,还是要判断一下传入的引脚号是否为13?点击运行,程序正常执行。通过这段时间的学习,我们将GPIO.C这个文件中源码全部都解读完了。

随着学习的推进,源码的解读对小伙伴们来说也并不是那么难了。关于外部中断的学习也暂时告一段落了。下棋视频我们就要开启新的章节了。好了,今天的视频就到这里。一定要关注、点赞、收藏、评论,你们的支持是我这样的小博主持续更新下去的最大动力。期待下期视频再见。

第47期 什么是USART串口通信(上)

观看原视频

Hello,小伙伴们,大家好。这里是左左右。好久不见。这期视频我们来聊一聊什么是 USART 串口通信协议。那么什么是通信协议呢?这就好比两个人打电话,一个人说英语,一个人说汉语,互相都听不懂对方在说什么。而“通信协议”就是电子元器件之间沟通的“语言”,只有使用相同的通信协议,电子元器件之间才能实现有效的数据传输。

那么,什么又是串口通信呢?串口的全称是”串行通信接口”,与“串口”相对应的就是“并口”,全称为“并行通信接口”。这里我们假设要将一个字节的数据由单片机A发送到单片机B,在使用并行通信接口时,8个bit的数据一下子就传送完了,而在串行通信模式下,8个bit的数据需要一个bit一个bit的传送,这时串口和并口的优缺点一下子就看明白了,并口的优点是传送速度快,缺点是占用引脚多,而且由于多根数据线紧挨在一起,容易产生串扰,且每一位到达时间不完全一致(信号歪斜),距离越长问题越严重。

视频画面 00:50.7
视频画面 00:50.7 · 1080P 原图 · 查看对应视频

所以并口的传输距离很短,所以并口通信距离通常不超过两三米。串口的优点是:引脚占用少,线少,彼此间干扰小,可以加屏蔽和差分驱动(RS-232、RS-485),轻松传输几十米到上千米。唯一的缺点就是相对于“并行”速度较慢,但是串行已经可以满足我们大多数设计需求了,在这里大家有没有这样一个疑问?

视频画面 01:16.5
视频画面 01:16.5 · 1080P 原图 · 查看对应视频

这里的0和1是如何从单片机A发送到单片机B的?我们都知道IO口是可以输出高低电平的,单片机A将0转换为低电平发送到单片机B,单片机B将接收到的低电平信号转换为0,这样就实现了数据的传输。同理,1就是高电平。为了保证数据传输的准确,就要保证发送和接收保持相同的节奏,也就是说,单片机A发送一次数据,单片机B接收一次数据。

这里假设单片机A发送一次数据,而单片机B接收两次数据,这时接收到的数据就会变成这样,也就是多收到数据了。同理假设单片机A发送两次数据,单片机B接收一次数据,这时接收到的数据就会变成这样,也就是漏收到数据了。所以保证发送和接收保持相同的节奏是非常重要的。这里的节奏有个专业名词就是波特率,它指的是每秒信号状态变化的次数,单位是波特(Baud),常用的波特率有9600、115200等等。

视频画面 02:33.8
视频画面 02:33.8 · 1080P 原图 · 查看对应视频

USART串口通信协议的全称是Universal Synchronous Asynchronous Receiver Transmitter‌,中文译作通用同步/异步收发器。这里一定要能区分S和A分别代表什么意思。这里的S,也就是Synchronous,中文译作“同步”。这里的A,也就是Asynchronous,中文译作“异步”。

视频画面 02:59.6
视频画面 02:59.6 · 1080P 原图 · 查看对应视频

如果实在记不住看到S就要想到same这个单词,也就是“相同”的意思。

这样就记住了S代表同步,A代表异步了。USART 支持的模式包括:UART异步全双工 、同步模式、单线半双工、多处理器通信、IrDA SIR ENDEC、LIN 模式、智能卡模式、硬件流控制。其中UART异步全双工模式占 90% 以上的应用场景,也是我们接下来主要讲的一种模式。异步 + 硬件流控制的模式是在数据量较大时、需要可靠传输时(比如连接 4G 模块、高速 WiFi 模组),会启用 RTS/CTS 硬件流控。

它可以自动暂停对方发送,防止接收缓冲区溢出。本质上还是异步模式,只是增加了两个硬件握手引脚。多处理器模式(一主多从常用)在 RS-485 多机通信 中非常经典,至于其他模式,使用频率不高,大家先做了解就好。

视频画面 04:04.1
视频画面 04:04.1 · 1080P 原图 · 查看对应视频

其实,在之前的视频里,为了帮助大家快速上手串口屏,我们已经讲过 USART 串口通信的一些基础内容。如果这期视频听起来有不太明白的地方,欢迎回去翻一翻之前那几期。好了,今天的视频就到这里。一定要关注、点赞、收藏、评论,你们的支持是我这样的小博主持续更新下去的最大动力。期待下期视频再见。