(转)涨姿势,关于类似*(uint32_t*)&GPIOx这样形式的讨论

看了看下面大神的讨论,有些茅厕顿开的感觉,特此搬砖过来

void GPIO_DeInit(GPIO_TypeDef* GPIOx)
{
  /* Check the parameters */
  assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
  
  switch (*(uint32_t*)&GPIOx)
  {
    case GPIOA_BASE:
      RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOA, ENABLE);
      RCC_APB2PeriphResetCmd(RCC_APB2Periph_GPIOA, DISABLE);
      break;.....
//不明白switch行中GPIOx为什么要取址,GPIOx本来不就是地址么?
#define GPIOA    ((GPIO_TypeDef*)GPIOA_BASE)
//求解释

首先,从 GPIOx 说起

这是ST库里的经典映射手法。
具体就是

和PC上的程序不一样,在STM32上,可以通过

*(int *)0x08001000 = 34;

在地址为 0x80010000上写入 34 这个内容。
——这里不考虑什么 FLASH RAM之类的问题。

所以,ST库映射寄存器的典型手法就是

比如说 GPIOA的寄存器地址,如果是从 0x20001000开始存放什么 ODR IDR之类的。
因为 结构体成员在内存上也是按序排放的,所以,它就把

ODR IDR等等 寄存器 按顺序 定义成 GPIO 这个结构体。

形如

typedef GPIO
{
    odr;
    idr;
}

这一部分具体可以去看 stm32fxxx.h,我就不多说了。

最后,GPIOA GPIOB GPIOC都会有一个 GPIOA_Base GPIOB_Base
这个基地址,指的就是 每个port端口 寄存器的 起始地址。ABCDEFGI口各自按顺序排好。
所以只要找到头,再借助这个结构体,就可以直接通过

GPIOA->ODR这样的写法非常简单直观的 寻址到 GPIOA的ODR地址。

非常形象,非常生动。
而且很简洁,完全的利用了C语法本身的特性。

是以,从我个人的角度看,这是一个非常不错的 映射手法。

这基本也是那天晚上我语无伦次 发语音说的重点。

现在,先来回答原来那个帖子的问题。

*(uint32_t*)&GPIOx

这句话到底是什么意思?

其实问题还是在上一个帖子里提到的 GPIOx的定义里

#define GPIOA    ((GPIO_TypeDef*)GPIOA_BASE)

其实我为什么第一反应会觉得这个东西写的挺新鲜,因为以前我曾小小纠结过如何通过一个函数,让函数自己区分 GPIOA GPIOB这个问题。
而这里提供了一个 非常直接简单的方法:


```c
*(uint32_t*)&GPIOx

对这种较复杂的表达式或者宏,解决的思路很简单,就是一步一步展开。但这个过于简单,而且这个话题也太口水了,我就直接带过去不罗嗦了,罗嗦了你们还以为是我无知大惊小怪…(多怨啊我,我只是一个喜欢 详细解释的好版主)
GPIOx 是传递进来的形参,它的可能值就是 GPIOA GPIOB之类的
那也就是

((GPIO_TypeDef*)GPIOA_BASE)

GPIOA_BASE是一个数值,代表的是 GPIOA的寄存器的起始地址

*(uint32_t*)&GPIOx

这个操作,等于,把 GPIOA_BASE 这个最初宏定义的数值,就是说,这是一个常数。

所以这个时候,就可以很方便的使用 switch-case结构了
因为case后面跟的只能是常数,而不能是变量或者其他任何数值。

这就是这个问题的所有答案

水果君在那个帖子里认为,其实这个地方是多此一举,可以直接写成

(uint32)GPIOx

说实在的,这个,和 那个显得很麻烦的写法

*(uint32_t*)&GPIOx

效果确实是一样的。

只是,我强调 后面这种写法,我的理由在于:

可读性。

看到前者,你不会联想到 GPIOx是一个地址,而看到后者,稍微有点经验的C程序员都马上会领悟到这一点。
这就很重要。
为什么,因为,在类似的环境下,我就会搞懵。
比如说,最开始主楼贴 的那个图。

那是 人民币君发的。

我一开始因为直接联想到 这个放假前的讨论,因此我想都没想,就直接说,这两个效果是一样的。

然而,果真是一样吗?呵呵,那还真不是。
比如说

{
*(int *)0x80001000 = 34;
(int)0x80001000
while(1);
}

写到这里,我就懵逼了,看出问题了没?
一个是那个数其实是地址值,要去操作那个地址上的内容
另一个,压根就只是一个常数。

这两个操作的出来的结果和影响完全不一样。

虽然这可以解释为看走眼,糊涂,当然你也可以认为我经常犯懵,但是我个人的经验是——
不要盲目自信你不会犯懵。

因为一旦犯懵的时候,你可能要付出两三天或者两三个加班的夜晚去解决问题。
试问值得不值得?

而这也是我和水果君争论的核心所在:
虽然我的写法复杂一点,但是,我觉得我可以很明确的告诉别人这就是一个地址。

正如我发帖子或者在q群里讨论,总是不厌其烦,说的大白菜一样。
而不是假设你和我一样知道一些东西。

这样虽然我可以省事很多,少说很多话,打少很多字,但是却极有可能产生沟通的误解,因为双方很可能
不是建立在相同的已知上。

这也是我为什么非常厌恶和吐槽一些半导体厂商提供的文档的理由。
因为他们说的根本不是人话,不是站在开发者使用者的角度去说事。

似乎他们很希望和你分享他们设计芯片和外设原理一样,却不知道这样把我们搞的是一塌糊涂。
我们只不过想学会如何配置和使用,它们却罗罗嗦嗦说另一个方面的问题。

好了,就此结束。


我来对号入座了,我就是文中提到的水果君。
首先来讨论一个我认为很有意思的问题,就是这两个强制类型转换:
(uint32_t)&AAA 是否等价于 (uint32_t)AAA ?(假设我们不知道AAA的类型,变量还是常量)
为了便于讨论,我们假设变量uint32_t X=(uint32_t)&AAA, Y=(uint32_t)AAA;
乍一看,好像是有点等价的意思,但是仔细想想,又不是那么回事,这还取决于AAA的类型。
(1).现在假设AAA是2字节short型=0x1234;
那么X的结果是强制从AAA的地址中取走4字节,其中2字节未知:
X=0xXXXX1234 (小端情况下)
Y的结果就比较确定,编译器帮他把高位填0, Y=0x00001234;
(2).假设AAA是64位long long类型=0x1234;前面的0我就不写了。
那么X还是取走4个字节,根据大小端而异,可能是高4字节,也可能是低四字节。
Y只是简单的舍弃了高四字节,结果比较确定。
(3). 假设AAA是个数组,这个情况比较特殊,数组名本身就是数组的首地址,取地址后还是相同的值:
因此X会取出数组中的元素
而Y却还是一个地址值。
(4). 一个不靠谱的假设AAA是个结构体
那么X可以取到结构体的成员
而Y的写法就直接报错了,不允许强制类型转换。
(5). AAA是uint32_t类型,那么没什么问题,X与Y等价。

由此可见:
X写法确实是无条件强制转换,基本是无所不能转,但是如果类型不匹配就很容出错了。
Y写法是有限制的编译器参与的半智能转换,因为编译器知道源类型与目标类型,会帮忙参与转换,或者类型转换不太靠谱的话,直接报错。

由此可以得出结论,在使用强制类型转换的时候,必须具有可行性,同时也必须清楚转换后的结果,也就是说,程序员(写程序的人)必须清清楚楚地知道源类型和目标类型到底是什么,否则还是去读书深造的好。

现在我来说说为什么我觉得uint32_t X=(uint32_t)&GPIOx有脱裤子放屁的嫌疑(从阅读程序的角度来说)。
首先有个变量GPIOx,是个指针,也就是个地址1,此地址上面存放的类型是GPIO_TypeDef。
然后对这个地址1取了个地址2,那么这个地址2的类型是GPIO_TypeDef **
现在对地址2进行强制类型转换,转成uint32_t*,也就是间接说明,不再把地址1当做地址(指针)看待,而是作为一个uint32_t类型。
然后在地址2中的uint32_t数据取出来,完毕。牛逼的程序员也看出来了,这就是拐外抹角的把GPIOx转成uint32_t类型。
一般刚入门的程序员看了会不会很懵逼?

好,说的有点乱,我们按教主的思路重新捋一下:
我们假设不知道GPIOx到底是个什么东西,就当做是一个不知道类型的普通变量。
引用:“——————————————————————————————————
水果君在那个帖子里认为,其实这个地方是多此一举,可以直接写成
(uint32)GPIOx
说实在的,这个,和 那个显得很麻烦的写法
(uint32_t)&GPIOx
效果确实是一样的。
只是,我强调 后面这种写法,我的理由在于:
可读性。
看到前者,你不会联想到 GPIOx是一个地址,而看到后者,稍微有点经验的C程序员都马上会领悟到这一点。
————————————————————————————————引用结束”
看看我对脱裤子的理解:
首先一个变量GPIOx,不知道其类型
然后对此变量取了个地址,此地址类型也未知。
然后对这个地址进行强制转换成uint32_t类型的一个地址(此处影射GPIOx是个uint32_t类型)
最后,从这个地址中取出了一个uint32_t类型的变量,完成了最终这个语句的使命。
这样理解,也根本看不出GPIOx有地址的意思(只是明确了变量的地址是个具体类型的地址)。只是拐了个弯,把GPIOx强制转成uint32_t类型而已。

然而,把一个地址(指针)转成一个整形数,就很常见不过了。uint32_t Y=(uint32_t)GPIOx。

由此,可以看到两个转换的最终区别:脱裤子那种,是强制转换的变量地址的类型,间接对数据类型进行转换,而直接转换就是直接对变量进行转换。

看到你回这么长的贴,我也是挺感动的,然而很不好意思的告诉你。
虽然你没错,但我还是要比你更正确…

很简单,看好了。

在32位机器下,你是对的,你还是更简洁的。
然而如果这种写法,在16位机或者64位机 等非32位机下就会出错。
why?
很简单,,非32位机的 地址非4字节,而是 16位机的2字节,64位机的8字节。
这种情况下,对应的指针字长也就成了 2字节 8字节。

于是结果已经很明显。
你每次都uint32去强转地址,问题是,你强转的是一个地址…那也就是说。
对16位机,你多转了后面未知的2字节,对64位机,你少转了后面需要的4字节。
所以,必然是错的。

而原来那种看起来复杂的写法呢?
木有错,为毛?
因为,,uint32_t * 也是一个指针,或者地址(指针或地址随你叫吧)
因为在同一机器下,任何类型指针的字长都是一样的。
所以这种情况下,我读到的地址值永远不会少或者多。

这个问题意味着。
在你的写法里,你需要去假设指针字长,比如uint32,但这永远只能对一种机器字长适应。
而那个复杂的写法,则无此需求,不需要作任何假设,也就不会受限于任何机器字长的限制。

事实上,我写成

*(uint8_t *)
*(uint16_t *)

都木有任何关系

(uint32) GPIOx 还是 (uint32_t)&GPIOx
如果GPIOx是形参,无论哪种写法,得出的汇编都一样,都是直接取R0,

如果GPIOx 是全局变量,汇编结果也是一样,都是取GPIOx变量的内容
如果GPIOx变量存放到0x10001000,直接取0x10001000里面的内容

编译器非常聪明,认为对指针变量X取地址A再取A里面的内容和直接取X里面的内容是一样的!

我也觉得代码这东西没有绝对的对与错,虽然那种写法看起来很复杂并且高大上,一个语句展现了好几个C语言的知识点,而且结果正确,一点毛病没有,只是让人读起来需要有个停顿思考的时间。

C语言在程序移植这里确实存在许多诟病,在不同硬件平台上,数据类型的长度并不统一。
例如通常int在16位机为2字节,32位机为4字节,指针也一样,根据机型有2字节,4字节或者8字节的长度,记得还有3字节的……因为硬件平台种类实在是太多了,五花八门。
为了应付这类问题,C99标准出台了更具体的类型,如int16_t,uint32_t这样的具体长度类型,使程序在不同硬件平台更容易移植。但是……。
遗憾的是,这些类型中没有指针,我觉得因为指针也没法具体化。因此,教主提出了他的问题:在不同平台上如何用整型来表示指针?
其实这问题的根源完全来自于switch语句,因为在switch中只能使用整型数据类型,不可以是指针,如果非要去比较指针,那么只能转成整型。因此,如果把指针转为整型成了讨论的重点,但是很明显,教主的结论是错误的,他的写法并不能实现他要的结果。
大家都应该知道,指针是有类型的,解引用的时候会得到相应的类型:
(uint32_t)xxx 结果将是uint32_t
(int16_t)xxx 结果就是int16_t
根本得不到他所想要的与平台相关的数据类型。
因此在C语言中,想要得到指针所对应的整型类型,只能通过手动指定,例如微软就是这么干的
#ifdef _WIN64
typedef __int64 intptr_t;
#else
typedef int intptr_t;
#endif
这样intptr_t就可以确保能保存指针类型。
但是可惜这只是某些厂家这么干,ST的库中并没有这样的类型,否则这事就好办了(当然了,ST目前可能也没考虑推出64位的单片机)
我不知道*(uint32_t*)&GPIOx这样的代码是否是出自ST的标准库,我没有去考证,如果真这样写的话他们自己可能也看着别扭,所以我看到st的某个版本库里面看到的是直接比较指针,当然了,就不能使用switch语句了,而是if语句,像这样:
if (GPIOx == GPIOA) xxx_statement 。反正我觉得这样写是最直观最易读的了,给他们点个赞!

c语言的争议太多了,就像#define与typedef之争,#define与const之争,程序员的理论就是运行结果没错那就都不是大问题,不说了,累,继续搬砖了。

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值