0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

深入浅出了解单调栈和单调队列

Q4MP_gh_c472c21 来源:袁厨的算法小屋 作者:袁厨 2021-02-02 10:18 次阅读

袁厨携袁记菜馆全体工作人员祝大家在新的一年,健健康康,开开心心。发量暴增,钱包超大。

哎,元旦假期结束了,又要继续搬砖了,我们接着做题吧,今天我们好好说说单调栈和单调队列。其实很容易理解,单调栈就是栈内单调递增或单调递减的栈,栈内元素是有序的,单调队列同样也是。

下面我们通过几个题目由浅入深,一点一点挖透他们吧!

a5d57fd4-64cf-11eb-8b86-12bb97331649.png

提纲

单调队列

剑指 Offer 59 - II. 队列的最大值

题目描述:

请定义一个队列并实现函数 max_value 得到队列里的最大值

若队列为空,pop_front 和 max_value 需要返回 -1

示例 1:

输入: ["MaxQueue","push_back","push_back","max_value","pop_front","max_value"]

[[],[1],[2],[],[],[]]

输出: [null,null,null,2,1,2]

示例 2:

输入:

["MaxQueue","pop_front","max_value"]

[[],[],[]]

输出: [null,-1,-1]

题目解析:

我们先来拆解下上面的示例 1

a615481c-64cf-11eb-8b86-12bb97331649.png

其实我觉得这个题目的重点在理解题意上面,可能刚开始刷题的同学,对题意理解不够透彻,做起来没有那么得心应手,通过上面的图片我们简单了解了一下题意,那我们应该怎么做才能实现上述要求呢?

下面我们来说一下双端队列。我们之前说过的队列,遵守先进先出的规则,双端队列则可以从队头出队,也可以从队尾出队,不用遵守先进先出的规则,我们先通过一个视频来简单了解下双端队列。

我们可以用双端队列做辅助队列,用辅助队列来保存当前队列的最大值。我们同时定义一个普通队列和一个双端单调队列。普通队列就正常执行入队,出队操作。max_value 操作则返回咱们的双端队列的队头即可。下面我们来看一下代码的具体执行过程吧。

我们来对视频进行解析

1.我们需要维护一个单调双端队列,上面的队列则执行正常操作,下面的队列队头元素则为上面队列的最大值

2.出队时,我们需要进行对比两个队列的队头元素是否相等,如果相等则同时出队,则出队后的双端队列的头部仍为上面队列中的最大值。

3.入队时,我们需要维持一个单调递减的双端队列,因为我们需要确保队头元素为最大值嘛。

题目代码:

a67690cc-64cf-11eb-8b86-12bb97331649.png

239.滑动窗口最大值

题目描述:

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

示例1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3 输出:[3,3,5,5,6,7]

题目解析:

题目让我们找出每个滑动窗口的最大值,那么题目具体含义是怎样呢?

a6c42648-64cf-11eb-8b86-12bb97331649.png

就是为了让我们输出每个窗口的最大值,那我们思考一下,我们一个数组一共有多少窗口呢?

比如我们这个例子中,我们的窗口长度为 3 ,数组长度为 8,我们的窗口每次移动一位,所以我们一共有 8 - (3 - 1)也就是 8 - 3 + 1。所以我们返回数组的长度是跟原数组长度和滑动窗口的长度有关的。

也就是 winlen = len(数组长度) - k(滑动窗口长度) + 1。下面我们来看一个视频,相信通过这个视频,大家一下就能搞懂啦。

下面我们对视频进行拆解。

1.先将我们第一个窗口的所有值按照规则存入单调双端队列中,单调队列里面的值为单调递减的。如果发现队尾元素小于要加入的元素,则将队尾元素出队,直到队尾元素大于等于新元素时,再让新元素入队,目的就是维护一个单调递减的队列。第一个窗口的所有值入队之后情况,如下图。是因为 3 要入队时,此时队中有 1 ,不能保证单调递减,所以需要 1 出队,然后 3 入队, -1 入队时,队中有 3 ,满足单调,所以 -1 可以入队。

a706abf8-64cf-11eb-8b86-12bb97331649.png

2.我们将第一个窗口的所有值,按照单调队列的规则入队之后,因为队列为单调递减,所以队头元素必为当前窗口的最大值,则将队头元素添加到数组中。

a7587316-64cf-11eb-8b86-12bb97331649.png

3.移动窗口,判断当前窗口前的元素是否和双端队列队头元素相等,如果相等则出队,此时滑动窗口的最大值发生改变了。

a7aca5b2-64cf-11eb-8b86-12bb97331649.png

4.继续然后按照规则进行入队,维护单调递减队列,这里和第一条规则一致。

5.每次将队头元素存到返回数组里。

6.返回数组

是不是一下就搞懂啦。你真帅,下面我们来看一下代码吧。

题目代码

a7e28f42-64cf-11eb-8b86-12bb97331649.png

单调栈

leetcode 155 最小栈

设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。

push(x) —— 将元素 x 推入栈中。

pop() —— 删除栈顶的元素。

top() —— 获取栈顶元素。

getMin() —— 检索栈中的最小元素。

输入:

["MinStack","push","push","push","getMin","pop","top","getMin"]

[[],[-2],[0],[-3],[],[],[],[]]

输出:

[null,null,null,null,-3,null,0,-2]

题目解析

感觉这个题目的难度就在读懂题意上面,读懂之后就没有什么难的了,我们在上面的滑动窗口的最大值已经进行了详细描述,其实这个题目和那个题目思路一致。

该题让我们设计一个栈,该栈具有的功能有,push,pop,top等操作,并且能够返回栈的最小值。比如此时栈中的元素为 5,1,2,3。我们执行 getMin() ,则能够返回 1。这块是这个题目的精髓所在,见下图, 这个题目也可以不利用辅助栈解决,但是不符合本文主题,所以在这里先不进行详细描述。大致思路为,把当前最小值用一个变量保存,需要入栈的值小于当前最小值时,先把当前最小值入栈,再将需要入栈的值入栈,并更新当前最小值。如果大于当前最小值,则直接入栈。getMin()函数则直接返回变量保存的值即可。下面我们来看一下我们借助辅助栈,如何解决这个题目吧。

a83e070a-64cf-11eb-8b86-12bb97331649.png

我们一起先通过一个视频先看一下具体解题思路,通过视频一定可以整懂的,我们注意观察栈 B 内的元素。

我们来对视频进行解析

1.我们执行入栈操作时,先观察需要入栈的元素是否小于栈 B 的栈顶元素,如果小于则两个栈都执行入栈操作。

2.栈 B 的栈顶元素则是栈 A 此时的最小值。则 getMin() 只需返回栈 B 的栈顶元素即可。

3.出栈时,需要进行对比,若栈 A 和栈 B 栈顶元素相同,则同时出栈,出栈后B 的栈顶保存的仍为此时栈 A 的最小元素

题目代码

a877d566-64cf-11eb-8b86-12bb97331649.png

leetcode 739 每日温度

题目描述:

请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。

示例1:

输入:temperatures = [73, 74, 75, 71, 69, 72, 76, 73]

输出:arr = [1, 1, 4, 2, 1, 1, 0, 0]

示例2:

输入:temperatures = [30,30,31,45,31,34,56]

输出:arr = [2,1,1,3,1,1,0]

题目解析

其实我们可以换种方式理解这个题目,比如我们 temperatures[0] = 30,则我们需要找到后面第一个比 30 大的数,也就是 31,31的下标为 2,30 的下标为 0 ,则我们的返回数组 arr[0] = 2。理解了题目之后我们来说一下解题思路。

遍历数组,数组中的值为待入栈元素,待入栈元素入栈时会先跟栈顶元素进行对比,如果小于等于该值则入栈,如果大于则将栈顶元素出栈,新的元素入栈。

例如栈顶为69,新的元素为72,则69出栈,72入栈。并赋值给 arr,69 的索引为4,72的索引为5,则 arr[4] = 5 - 4 = 1,这个题目用到的是单调栈的思想,下面我们来看一下视频解析。

注:栈中的括号内的值,代表索引对应的元素,我们的入栈的为索引值,为了便于理解将其对应的值写在了括号中

a8c1a42a-64cf-11eb-8b86-12bb97331649.png

leetcode 42 接雨水

这道接雨水也是一道特别经典的题目,一道必刷题目,我们也用单调栈来解决。下面我们来看一下题目吧

题目描述:

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例1:

输入:height =[0,1,0,2,1,0,1,3,2,1,2,1]

输出:6

示例2:

输入:height =[4,2,0,3,2,5]

输出:9

示例2:

输入:[4,3,2,0,1,1,5

输出:13

题目解析:

看了上面的示例刚开始刷题的同学可能有些懵逼,那我们结合图片来理解一下,我们就用示例3的例子进行举例,他的雨水到底代表的是什么。输入代表的是黄色箱子的个数,蓝色箱子代表雨水数量。缝隙之间可以装多少水

a90fa828-64cf-11eb-8b86-12bb97331649.png

说明:上面是由数组 [4,3,2,0,1,1,5]表示的高度图,在这种情况下,可以接 13个单位的雨水(见下图)。

上图则为我们的题目描述,是不是理解了呢?你也可以这样理解我们在地上放置了若干高度的黄色箱子,他们中间有空隙,然后我们想在他们里面插入若干蓝色箱子,并保证插入之后,这些箱子的左视图和右视图都不能看到蓝色箱子。 好啦题目我们已经理解了,下面我们来看一下接雨水问题到底该怎么做,其实原理也很简单,我们通过我们的例3来进行说明。 首先我们依次入栈4,3,2,0我们的数组前四个元素是符合单调栈规则的。但是我们的第五个1,是大于0的。那我们就需要0出栈1入栈。但是我们这样做是为了什么呢?有什么意义呢?别急我们来看下图。

a9500dfa-64cf-11eb-8b86-12bb97331649.png

上图我们的,4,3,2,0已经入栈了,我们的另一个元素为1,栈顶元素为0,栈顶下的元素为2。那么我们在这一层接到的雨水数量怎么算呢?2,0,1这三个元素可以接住的水为一个单位(见下图)这是我们第一层接到水的数量。 注:能接到水的情况,肯定是中间低两边高的情况

a9a1d252-64cf-11eb-8b86-12bb97331649.png

因为我们需要维护一个单调栈,所以我们则需要将0出栈1入栈,那么此时栈内元素为4,3,2,1。下一位元素为1,我们入栈,此时栈内元素为4,3,2,1,1。 下一元素为5,栈顶元素为1,栈顶的下一元素仍为1,则需要再下一个元素,为2,那我们求当前层接到的水的数量。 注:栈内保存的应是索引值,这里为了便于理解用了value值

a9f9ee38-64cf-11eb-8b86-12bb97331649.png

我们是通过2,1,1,5这四个元素求得第二层的接水数为1*3=3;1是因为min(2-1,5-1)=min(1,4)得来的,大家可以思考一下木桶效应。装水的多少,肯定是按最短的那个木板来的,所以高度为1,3的话是因为5的索引为6,2的索引为2,他们之间共有三个元素(3,4,5)也就是3个单位。所以为6-2-1=3。 将1出栈之后,我们栈顶元素就变成了2,下一元素变成了3,那么3,2,5这三个元素同样也可以接到水。

aa566686-64cf-11eb-8b86-12bb97331649.png

这是第三层的接水情况,能够接到4个单位的水,下面我们继续出栈2,那么我们的4,3,5仍然可以接到水啊。

aad0ff86-64cf-11eb-8b86-12bb97331649.png

这是我们第四层接水的情况,一共能够接到5个单位的水,那么我们总的接水数加起来,那就是1+3+4+5=13。你学会了吗?别急还有动图我们,我们再来深入理解一哈。

题目代码:

ab34aed2-64cf-11eb-8b86-12bb97331649.png

好啦,咱们的单调队列和单调栈的题目到这里就算总结完毕啦,希望对你能有一点点帮助。

原文标题:深入浅出搞通单调队列单调栈

文章出处:【微信公众号:嵌入式ARM】欢迎添加关注!文章转载请注明出处。

责任编辑:haq

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

原文标题:深入浅出搞通单调队列单调栈

文章出处:【微信号:gh_c472c2199c88,微信公众号:嵌入式微处理器】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    深入浅出RISC-V调试

    一、JTAG简介 目前RISC-V官方支持的调试方式是JTAG(Joint Test Action Group),而ARM支持的调试方式有JTAG和SWD(Serial Wire Debug)这两种。 JTAG是一种国际标准的调试方式(IEEE1149.1),而SWD是ARM开发的。标准JTAG采用四线方式,分别是TCK、TMS、TDI和TDO,有一个可选的TRST引脚。 ● TCK:测试时钟输入。 ● TMS:测试模式选择。 ● TDI:测试数据输入。 ● TDO:测试数据输出。 在调试时需要用到一个工具,比如JLink或者CMSIS-DAP,对于这个工具,在这里称为JTAG主机(JTAG host),而嵌入在芯片内部的JTAG称为JTAG从机(JTAG slave),需要注意的是上面这些信号的输入输出方向是对于JTAG从机来说的。下文中如无特别说明,JTAG都是指JTAG从机。 一个JTAG主机可以同时对多个JTAG从机进行调试,这通过JTAG扫描链(JTAG Scan Chain)完成,如图1所示。 图1 一个JTAG主机连接多个JTAG从机 JTAG内部有一个TAP(Test Access Port)控制器(或者说状态机),通过TCK和TMS信号来改变状态机的状态。这个状态机的核心是两路SCAN,分别是IR SCAN和DR SCAN,TAP状态机如图2所示。 图2 TAP状态机 箭头上的0或1表示的是TMS信号的电平。JTAG在每一个TCK信号的上升沿采样TMS信号和TDI信号,决定状态机的状态是否发生变化,在每一个TCK信号的下降沿输出TDO信号。可以看到,无论TAP目前处于哪一个状态,只要TMS保持高电平并持续5个TCK时钟,则TAP一定会回到Test-Logic-Reset状态。 JTAG内部有一个IR(instruction register)寄存器和多个DR(data register)寄存器,IR寄存器决定要访问的是哪一个DR寄存器。DR寄存器有IDCODE、BYPASS等。在Test-Logic-Reset状态下IR寄存器默认选择的是IDCODE这个DR寄存器。 JTAG主机通过IR SCAN设置IR寄存器的值,然后通过DR SCAN来读、写相应的DR寄存器。 二、RISC-V调试Spec 调试模块在CPU芯片设计里是最为不起眼的,但又是最为复杂的模块之一,大部分开源的处理器IP都没有调试模块。 下面的内容基于RISC-V debug spec 0.13版本。 目前RISC-V的官方调试上位机是openocd,调试工具可以是JLink或者CMSIS-DAP,RISC-V调试系统框架如图3所示。 图3 RISC-V调试系统框架 可以看到主要分为3个部分,分别是Debug Host,可以理解为PC;Debug Hardware,可以理解为JLink或者CMSIS-DAP这样的调试工具;第三部分就是嵌入在芯片内部的调试模块。在调试模块内部,与调试工具直接交互的是DTM模块,DTM模块通过DMI接口与DM模块交互。 1>DTM模块 在DTM模块里实现了一个TAP控制器(状态机),其中IR寄存器的长度最少为5位,当TAP控制器复位时,IR的值默认为5\'b00001,即选择的是IDCODE寄存器。DTM模块的寄存器(DR寄存器)定义如图4所示。 图4 DTM寄存器 其中红色框起来的寄存器是必须要实现的。下面简单介绍一下这几个寄存器。 ① IDCODE寄存器(0x01) 当TAP状态机复位时,IR寄存器的值默认为0x01,即选择的是IDCODE寄存器。IDCODE寄存器的每一位含义如图5所示。IDCODE是只读寄存器。 图5 IDCODE寄存器 ● Version:只读,版本号,可为任意值。 ● PartNumber:只读,可为任意值。 ● Manufld:只读,厂商号,遵循JEP106标准分配,实际中可为任意值,只要不与已分配的厂商号冲突即可。 ② DTM控制和状态寄存器(dtmcs,0x10) dtmcs寄存器的每一位含义如图6所示。 图6 dtmcs寄存器 ● dmihardreset:DTM模块硬复位,写1有效。 ● dmireset:清除出错,写1有效。 ● idle:只读,JTAG 主机在Run-Test-Idle状态停留的时钟周期数,0表示不需要进入Run-Test-Idle状态,1表示进入Run-Test-Idle状态后可以马上进入下一个状态,以此类推。 ● dmistat:只读,上一次操作的状态。0表示无出错,1或者2表示操作出错,3表示操作还未完成。 ● abits:只读,dmi寄存器中address域的大小(位数)。 ● version:只读,实现所对应的spec版本,0表示0.11版本,1表示0.13版本。 ③ DM模块接口访问寄存器(dmi,0x11) dmi寄存器的每一位含义如图7所示。 图7 dmi寄存器 ● address:可读可写,DM寄存器的长度(位数)。 ● data:可读可写,往DM寄存器读、写的数据,固定为32位。 ● op:可读可写,读或者写这个域时有不同的含义。当写这个域时,写0表示忽略address和data的值,相当于nop操作;写1表示从address指定的寄存器读数据;写2表示把data的数据写到address指定的寄存器。写3为保留值。当读这个域时,0表示上一个操作正确完成;1为保留值;2表示上一个操作失败,这个状态是会被记住的,因此需要往dtmcs寄存器的dmireset域写1才能清除这个状态。3表示上一个操作还未完成。 在Update-DR状态时,DTM开始执行op指定的操作。在Capture-DR状态时,DTM更新data域。 ④ BYPASS寄存器(0x1f) 只读,长度为1,值固定为0。 2>DM模块 从图3可知,DM模块访问RISC-V Core有两种方式,一种是通过abstract command,另一种是通过system bus。abstract command方式是必须要实现的,system bus的方式是可选的。 DM模块的寄存器都为32位,定义如图8所示。 图8 DM寄存器 下面介绍一下红色框起来这几个重要的寄存器。 ① data寄存器(data0-data11,0x04-0x0f) 这12个寄存器是用于abstract command的数据寄存器,长度为32位,可读可写。 ② DM控制寄存器(dmcontrol,0x10) dmcontrol寄存器的每一位含义如图9所示。 图9 dmcontrol寄存器 ● haltreq:只写,写1表示halt(暂停)当前hart(hart表示CPU核,存在多核的情况)。 ● resumereq:只能写1,写1表示resume(恢复)当前hart,即go。 ● hartreset:可读可写,写1表示复位DM模块,写0表示撤销复位,这是一个可选的位。 ● ackhavereset:只能写1,写1表示清除当前hart的havereset状态。 ● hasel:可读可写,0表示当前只有一个已经被选择了的hart,1表示当前可能有多个已经被选择了的hart。 ● hartsello:可读可写,当前选择的hart的低10位。1位表示一个hart。 ● hartselhi:可读可写,当前选择的hart的高10位。1位表示一个hart。如果只有一个hart,那么hasel的值为0,hartsello的值为1,hartselhi的值为0。 ● setresethaltreq:只能写1,写1表示当前选择的hart复位后处于harted状态。 ● clrresethaltreq:只能写1,写1表示清除setresethaltreq的值。 ● ndmreset:可读可写,写1表示复位整个系统,写0表示撤销复位。 ● dmactive:可读可写,写0表示复位DM模块,写1表示让DM模块正常工作。正常调试时,此位必须为1。 ③ DM状态寄存器(dmstatus,0x11) dmstatus寄存器是一个只读寄存器,每一位含义如图10所示。 图10 dmstatus寄存器 ● impebreak:1表示执行完progbuf的指令后自动插入一条ebreak指令,这样就可以节省一个progbuf。当progbufsize的值为1时,此值必须为1。 ● allhavereset:1表示当前选择的hart已经复位。 ● anyhavereset:1表示当前选择的hart至少有一个已经复位。 ● allresumeack:1表示当前选择的所有hart已经应答上一次的resume请求。 ● anyresumeack:1表示当前选择的hart至少有一个已经应答上一次的resume请求。 ● allnonexistent:1表示当前选择的hart不存在于当前平台。 ● anynonexistent:1表示至少有一个选择了的hart不存在于当前平台。 ● allunavail:1表示当前选择的hart都不可用。 ● anyunavail:1表示至少有一个选择了的hart不可用。 ● allrunning:1表示当前选择的hart都处于running状态。 ● anyrunning:1表示至少有一个选择了的hart处于running状态。 ● allhalted:1表示当前选择的hart都处于halted状态。 ● anyhalted:1表示至少有一个选择了的hart处于halted状态。 ● authenticated:0表示使用DM模块之前需要进行认证,1表示已经通过认证。 ● authbusy:0表示可以进行正常的认证,1表示认证处于忙状态。 ● hasresethaltreq:1表示DM模块支持复位后处于halted状态,0表示不支持。 ● confstrptrvalid:1表示confstrptr0~3寄存器保存了配置字符串的地址。 ● version:0表示DM模块不存在,1表示DM模块的版本为0.11,2表示DM模块的版本为0.13。 ④ abstract控制和状态寄存器(abstractcs,0x16) abstractcs寄存器定义如图11所示。 图11 abstractcs寄存器 ● progbufsize:只读,program buffer的个数,取值范围为0~16,每一个的大小为32位。 ● busy:只读,1表示abstract命令正在执行,当写command寄存器后该位应该马上被置位直到命令执行完成。 ● cmderr:可读、只能写1,cmderr的值仅当busy位为0时有效。0表示无错误,1表示正在操作command、abstractcs、data或者progbuf寄存器,2表示不支持当前命令,3表示执行命令时出现异常,4表示由于当前hart不可用,或者不是处于halted/running状态而不能被执行,5表示由于总线出错(对齐、访问大小、超时)导致的错误,7表示其他错误。写1清零cmderr。 ● datacount:只读,所实现的data寄存器的个数。 ⑤ abstract命令寄存器(command,0x17) 当写这个寄存器时,相应的操作就会被执行。command寄存器只能写,定义如图12所示。 图12 command寄存器 ● cmdtype:只写,命令类型,0为表示访问寄存器,1表示快速访问,2表示访问内存。 ● control:只写,不同的命令类型有不同的含义,说明如下。 当cmdtype为0时,control定义如图13所示。 图13 访问寄存器 ● cmdtype:值为0。 ● aarsize:2表示访问寄存器的最低32位,3表示访问寄存器的最低64位,4表示访问寄存器的最低128位。如果大于实际寄存器的大小则此次访问是失败的。 ● aarpostincrement:1表示成功访问寄存器后自动增加regno的值。 ● postexec:1表示执行progbuf里的内容(指令)。 ● transfer:0表示不执行write指定的操作,1表示执行write指定的操作。 ● write:0表示从指定的寄存器拷贝数据到arg0指定的data寄存器。1表示从arg0指定的data寄存器拷贝数据到指定的寄存器。 ● regno:要访问的寄存器。 综上,可知: Ⅰ. 当write=0,transfer=1时,从regno指定的寄存器拷贝数据到arg0对应的data寄存器。 Ⅱ. 当write=1,transfer=1时,从arg0对应的data寄存器拷贝数据到regno指定的寄存器。 Ⅲ. 当aarpostincrement=1时,将regno的值加1。 Ⅳ. 当postexec=1时,执行progbuf寄存器里的指令。 arg对应的data寄存器如图14所示。 图14 arg对应的data寄存器 即当访问的寄存器位数为32位时,arg0对应data0寄存器,arg1对应data1寄存器,arg2对应data2寄存器。 当cmdtype为1时,control定义如图15所示。 图15 快速访问 ● cmdtyte:值为1。 此命令会执行以下操作: 1)halt住当前hart。 2)执行progbuf寄存器里的指令。 3)resume当前hart。 当cmdtype为2时,control定义如图16所示。 图16 访问内存 ● cmdtype:值为2。 ● aamvirtual:0表示访问的是物理地址,1表示访问的是虚拟地址。 ● aamsize:0表示访问内存的低8位,1表示访问内存的低16位,2表示访问内存的低32位,3表示访问内存的低64位,4表示访问内存的低128位。 ● aampostincrement:1表示访问成功后,将arg1对应的data寄存器的值加上aamsize对应的字节数。 ● write:0表示从arg1指定的地址拷贝数据到arg0指定的data寄存器,1表示从arg0指定的data寄存器拷贝数据到arg1指定的地址。 ● target-specific:保留。 综上,可知: Ⅰ. 当write=0时,从arg1指定的地址拷贝数据到arg0指定的data寄存器。 Ⅱ. 当write=1时,从arg0指定的data寄存器拷贝数据到arg1指定的地址。 Ⅲ. 当aampostincrement=1时,增加arg1对应的data寄存器的值。 ⑥ 系统总线访问控制和状态寄存器(sbcs,0x38) sbcs寄存器定义如图17所示。 图17 sbcs寄存器 ● sbversion:只读,0表示system bus是2018.1.1之前的版本,1表示当前debug spec的版本,即0.13版本。 ● sbbusyerror:只读,写1清零,当debugger要进行system bus访问操作时,如果上一次的system bus访问还在进行中,此时会置位该位。 ● sbbusy:只读,1表示system bus正在忙。在进行system bus访问前必须确保该位为0。 ● sbreadonaddr:可读可写,1表示每次往sbaddress0寄存器写数据时,将会自动触发system bus从新的地址读取数据。 ● sbaccess:可读可写,访问的数据宽度,0表示8位,1表示16位,2表示32位,3表示64位,4表示128位。 ● sbautoincrement:可读可写,1表示每次system bus访问后自动将sbaddress的值加上sbaccess的大小(字节)。 ● sbreadondata:可读可写,1表示每次从sbdata0寄存器读数据后将自动触发system bus从新的地址读取数据。 ● sberror:可读,写1清零,0表示无错误,1表示超时,2表示访问地址错误,3表示地址对齐错误,4表示访问大小错误,7表示其他错误。 ● sbasize:只读,system bus地址宽度(位数),0表示不支持system bus访问。 ● sbaccess128:只读,1表示system bus支持128位访问。 ● sbaccess64:只读,1表示system bus支持64位访问。 ● sbaccess32:只读,1表示system bus支持32位访问。 ● sbaccess16:只读,1表示system bus支持16位访问。 ● sbaccess8:只读,1表示system bus支持8位访问。 ⑦ 系统总线地址0寄存器(sbaddress0,0x39) 可读可写,如果sbcs寄存器中的sbasize的值为0,那么此寄存器可以不用实现。 当写该寄存器时,会执行以下流程: Ⅰ. 设置sbcs.sbbusy的值为1。 Ⅱ. 从新的sbaddress地址读取数据。 Ⅲ. 如果读取成功并且sbcs.sbautoincrement的值为1,则增加sbaddress的值。 Ⅳ. 设置sbcs.sbbusy的值为0。 ⑧ 系统总线数据0寄存器(sbdata0,0x3c) 可读可写,如果sbcs寄存器中的所有sbaccessxx的值都为0,那么此寄存器可以不用实现。 当写该寄存器时,会执行以下流程: Ⅰ. 设置sbcs.sbbusy的值为1。 Ⅱ. 将sbdata的值写到sbaddress指定的地址。 Ⅲ. 如果写成功并且sbcs.sbautoincrement的值为1,则增加sbaddress的值。 Ⅳ. 设置sbcs.sbbusy的值为0。 当读该寄存器时,会执行以下流程: Ⅰ. 准备返回读取的数据。 Ⅱ. 设置sbcs.sbbusy的值为1。 Ⅲ. 如果sbcs.sbautoincrement的值为1,则增加sbaddress的值。 Ⅳ. 如果sbcs.sbreadondata的值为1,则开始下一次读操作。 Ⅴ. 设置sbcs.sbbusy的值为0。 三、RISC-V调试上位机分析 RISC-V官方支持的调试器上位机是openocd。openocd是地表最强大(没有之一)的开源调试上位机,支持各种target(ARM(M、A系列)、FPGA、RISC-V等),支持各种调试器(Jlink、CMSIS-DAP、FTDI等),支持JTAG和SWD接口。 这里不打算详细分析整个openocd的实现,只是重点关注针对RISC-V平台的初始化、读写寄存器和读写内存这几个流程。 1>openocd启动过程 openocd启动时需要通过-f参数制定一个cfg文件,比如: openocd.exe -f riscv.cfg riscv.cfg文件的内容如下: adapter_khz1000 reset_config srst_only adapter_nsrst_assert_width 100 interface cmsis-dap transport select jtag set _CHIPNAME riscv jtag newtap $_CHIPNAME cpu -irlen 5 -expected-id 0x1e200a6d set _TARGETNAME $_CHIPNAME.cpu target create $_TARGETNAME riscv -chain-position $_TARGETNAME ■ 第一行设置TCK的时钟为1000KHz。 ■ 第二行表示不支持通过TRST引脚复位,只支持TMS为高电平并持续5个TCK时钟这种方式的复位。 ■ 第三行是复位持续的延时。 ■ 第四行指定调试器为CMSIS-DAP。 ■ 第五行指定调试接口为JTAG。 ■ 第六行指定调试的target类型为riscv。 ■ 第七行指定生成一个IR寄存器长度为5位、IDCODE为0x1e200a6d的JTAG TAP。 ■ 第八、九行指定生成一个riscv target。 openocd启动时的主要流程如图18所示。 图18 openocd启动流程 下面重点关注一下examine target这个流程。 这里的target是指riscv,对于riscv,首先会读取dtmcontrol这个寄存器,因为openocd支持0.11和0.13版本的DTM,通过这个寄存器可以知道当前调试的DTM是哪一个版本。这里选择0.13版本来分析。通过读取dtmcontrol,还可以知道idle、abits这些参数。接下来会将dmcontrol这个寄存器的dmactive域写0后再写1来复位DM模块。接下来再读取dmstatus,判断version域是否为2。接下来还会读取sbcs和abstractcs寄存器,最后就是初始化每一个hart的寄存器。 2>read register过程 读寄存器时,先构建command寄存器的内容,首先将cmdtype的值设为0,aarsize的值设为2(寄存器的宽度为32位),transfer的值设为1,regno的值设为要读的寄存器的number,其他值设为0,然后写到command寄存器里。然后一直读取abstractcs寄存器,直到abstractcs寄存器的busy位为0或者超时。然后再判断abstractcs寄存器的cmderr的值是否为0,如果不为0则表示此次读取寄存器失败,如果为0则继续读取data0寄存器,这样就可以得到想要读的寄存器的值。 3>write register过程 写寄存器时,先将需要写的值写到data0寄存器,然后构建command寄存器的内容,首先将cmdtype的值设为0,aarsize的值设为2(寄存器的宽度为32位),transfer的值设为1,write的值设为1,regno的值设为要写的寄存器的number,其他值设为0,然后写到command寄存器里。然后一直读取abstractcs寄存器,直到abstractcs寄存器的busy位为0或者超时。然后再判断abstractcs寄存器的cmderr的值是否为0,如果不为0则表示此次写寄存器失败,如果为0则表示写寄存器成功。 4>read memory过程 如果progbufsize的值大于等于2,则会优先使用通过执行指令的方式来读取内存。这里不分析这种方式,而是分析使用system bus的方式。通过前面的分析可知,system bus有两个版本V0和V1,这里以V1版本来说明。 先将sbcs寄存器的sbreadonaddr的值设为1,sbaccess的值设为2(32位),然后将要读内存的地址写入sbaddress0寄存器。接着读sbdata0寄存器,最后读sbcs寄存器,如果其中的sbbusy、sberror和sbbusyerror都为0,则从sbdata0读取到的内容就是要读的内存的值。 5>write memory过程 和read memory类似,同样以V1版本来说明。 先将要写的内存地址写到sbaddress0寄存器,然后将要写的数据写到data0寄存器,最后读sbcs寄存器,如果其中的sbbusy、sberror和sbbusyerror都为0,则此次写内存成功。 四、RISC-V JTAG的实现 通过在STM32F103C8T6上实现(模拟)RISC-V调试标准,进一步加深对RISC-V JTAG调试的理解。 使用STM32的四个GPIO作为JTAG信号的四根线,其中TCK所在的引脚设为外部中断,即上升沿和下降沿触发方式,实现了可以通过openocd以RISC-V的调试标准来访问STM32的寄存器和内存。程序流程如图19所示。 图19 JTAG实现的程序流程 五、参考资料 1、在STM32上模拟RISC-V JTAG的实现:stm32_riscv_jtag_slave 2、一个从零开始写的易懂的RISC-V处理器核:tinyriscv
    发表于 11-28 22:00

    嵌入式环形队列与消息队列的实现原理

    嵌入式环形队列,也称为环形缓冲区或循环队列,是一种先进先出(FIFO)的数据结构,用于在固定大小的存储区域中高效地存储和访问数据。其主要特点包括固定大小的数组和两个指针(头指针和尾指针),分别指向队列的起始位置和结束位置。
    的头像 发表于 09-02 15:29 528次阅读

    深入浅出系列之代码可读性

    原创声明:该文章是个人在项目中亲历后的经验总结和分享,如有搬运需求请注明出处。 这是“深入浅出系列”文章的第一篇,主要记录和分享程序设计的一些思想和方法论,如果读者觉得所有受用,还请“一键三连
    的头像 发表于 08-09 16:00 265次阅读

    深入浅出谈TDR阻抗测试

    、脉宽、时序、抖动或噪声内容的任何事物都会影响整个系统的性能和可靠性。为保证信号完整性,必须了解和控制信号经过的传输环境的阻抗。阻抗不匹配和不连续会导致反射,增加系
    的头像 发表于 06-06 08:28 5788次阅读
    <b class='flag-5'>深入浅出</b>谈TDR阻抗测试

    深入浅出带你搞懂-MOSFET栅极电阻

    一、MOSFET简介MOSFET是金属(metal)—氧化物(oxide)—半导体(semiconductor)场效应晶体管,属于电压控制电流型元件,是开关电路中的基本元件,其栅极(G极)内阻极高。以N沟道增强型为例,其结构为在一块浓度较低的P型硅上扩散两个浓度较高的N型区作为漏极和源极,半导体表面覆盖二氧化硅绝缘层并引出一个电极作为栅极。由于mos管本身的
    的头像 发表于 05-09 08:10 2.3w次阅读
    <b class='flag-5'>深入浅出</b>带你搞懂-MOSFET栅极电阻

    【大语言模型:原理与工程实践】探索《大语言模型原理与工程实践》

    的未来发展方向进行了展望,包括跨领域、跨模态和自动提示生成能力方向,为读者提供了对未来技术发展的深刻见解。《大语言模型原理与工程实践》是一本内容丰富、深入浅出的技术书籍。它不仅为读者提供了大语言模型
    发表于 04-30 15:35

    深入浅出带你了解磁共振成像(MRI)基本原理

    磁共振成像技术原本称为核磁共振成像。很多人听到“核磁”,第1反应是这个对人体有害吗,因为名称中不是有“核”吗。其实,此处的”核“指”原子核“确实不假,但磁共振成像只与原子核的磁场相关,与原子核聚变、裂变等的能量放射并无关系。因此,磁共振成像其实是利用人体组织中某种原子核的核磁共振现象,将所得射频信号经过计算机处理,重构出人体某一层面的图像的诊断技术。
    的头像 发表于 04-03 17:04 961次阅读
    <b class='flag-5'>深入浅出</b>带你<b class='flag-5'>了解</b>磁共振成像(MRI)基本原理

    怎么理解负频率呢?射频人眼中的负频率

    说实话,我对负频率这个概念,也是有点凌乱。不过,最近不是正在看“深入浅出通信原理”嘛,看了一些相关概念。
    的头像 发表于 03-05 16:10 3240次阅读
    怎么理解负频率呢?射频人眼中的负频率

    TC3xx的HSM中有没有单调计数器?

    你好, 我看到 OPTIGA 有单调计数器,但我在 TC3xx 的 HSM 中确实找不到单调计数器。 能否确认TC3xx的HSM中没有单调计数器?
    发表于 03-05 07:56

    深入浅出理解三极管

    原文来自原创书籍《硬件设计指南 从器件认知到手机基带设计》: 本小节介绍下三极管的特性,清晰易懂,使用通俗的水流模型加强对三极管的原理记忆,一定比课堂上讲的要形象的多,各位同学要学会类比的方法来加深记忆(比如在介绍相对论中引力扭曲时空的概念时,国外科学家们就用生活中的漩涡,或者在弹性膜中间的重球,来类比星体引力对时空的影响,这样会大大简化我们学习、理解和记忆的过程,这种学习方法被称为类比学习法)。 我们
    的头像 发表于 02-23 08:41 692次阅读
    <b class='flag-5'>深入浅出</b>理解三极管

    深入浅出了解高边驱动在汽车应用中的挑战

    随着汽车电子技术发展,电动化,轻量化与智能化需求带动了车规级高边驱动(High-side Driver, HSD)在车身负载驱动中的大规模应用。
    的头像 发表于 01-23 10:05 5405次阅读
    <b class='flag-5'>深入浅出了解</b>高边驱动在汽车应用中的挑战

    【年度精选】2023年度top5榜单——电机控制资料

    推荐理由: 这是一份关于PID闭环控制算法的解析资料,内容深入浅出,易于理解。通过这份资料,你可以全面了解PID控制算法的工作原理、参数调整技巧以及在实际应用中的注意事项。无论你是初学者还是有一定经验
    发表于 01-16 14:34

    深入浅出理解PagedAttention CUDA实现

    vLLM 中,LLM 推理的 prefill 阶段 attention 计算使用第三方库 xformers 的优化实现,decoding 阶段 attention 计算则使用项目编译 CUDA 代码实现。
    的头像 发表于 01-09 11:43 1893次阅读
    <b class='flag-5'>深入浅出</b>理解PagedAttention CUDA实现

    labview 队列最前端插入的应用

    起到很多作用。本文将详细介绍LabVIEW队列的应用,特别是在最前端插入数据的情况下。 首先,让我们了解LabVIEW队列的基本概念。队列是一种数据结构,允许在一端插入元素,并在另一端
    的头像 发表于 01-08 11:45 1192次阅读

    简析控制系统的稳定性判据

    上一篇视频,我们已经对控制系统分析的关键 —— 传递函数进行了深入浅出的介绍(点我穿越回上一期内容)。
    的头像 发表于 01-03 12:37 3362次阅读
    简析控制系统的稳定性判据