不得不说,关于 *(unsigned long *) 和 (unsigned long)
@辛昕:
然而我很悲伤地再次郑重声明:我木有错!具体请看我回复 水果君 那难得一见的长回帖的回帖!!
这事情居然还可以从遥远的 2月2号 刚下班放假那天开始。
那天,水群里,水果君弄了个帖子,说让我去回,我看了一下,看到一个挺新鲜的东西,觉得有点意思,但看到下面的讨论我就觉得很晕。
当时稀里糊涂在群里和他们胡说八道了一番,废了很多劲才发现问题的关键。
可参见:http://bbs.eeworld.com.cn/thread-483541-1-1.html
帖子很短,基本都是代码,我贴过来........
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这个问题。
而这里提供了一个 非常直接简单的方法:
*(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程序员都马上会领悟到这一点。
这就很重要。
为什么,因为,在类似的环境下,我就会搞懵。
比如说,最开始主楼贴 的那个图。
那是 人民币君发的。
我一开始因为直接联想到 这个放假前的讨论,因此我想都没想,就直接说,这两个效果是一样的。
然而,果真是一样吗?呵呵,那还真不是。
比如说
写到这里,我就懵了,看出问题了没?
一个是那个数其实是地址值,要去操作那个地址上的内容
另一个,压根就只是一个常数。
这两个操作的出来的结果和影响完全不一样。
虽然这可以解释为看走眼,糊涂,当然你也可以认为我经常犯懵,但是我个人的经验是——不要盲目自信你不会犯懵。
因为一旦犯懵的时候,你可能要付出两三天或者两三个加班的夜晚去解决问题。
试问值得不值得?
而这也是我和水果君争论的核心所在:
虽然我的写法复杂一点,但是,我觉得我可以很明确的告诉别人这就是一个地址。
正如我发帖子或者在q群里讨论,总是不厌其烦,说的大白菜一样。
而不是假设你和我一样知道一些东西。
这样虽然我可以省事很多,少说很多话,打少很多字,但是却极有可能产生沟通的误解,因为双方很可能
不是建立在相同的已知上。
这也是我为什么非常厌恶和吐槽一些半导体厂商提供的文档的理由。
因为他们说的根本不是人话,不是站在开发者使用者的角度去说事。
似乎他们很希望和你分享他们设计芯片和外设原理一样,却不知道这样把我们搞的是一塌糊涂。
我们只不过想学会如何配置和使用,它们却罗罗嗦嗦说另一个方面的问题。
好了,就此结束。
@cruelfox:
我差点被你绕晕了。
(uint32) GPIOx 还是写 *(uint32_t*)&GPIOx
我倾向于前者,或者C++写 static_cast<uint32_t>(GPIOx)
因为 GPIOx 已经是一个地址了,地址值的比较么,直接当作32位整型(这里隐含32-bit平台前提)比较就是了。写成后面那种形式,这是何苦呢,呃,还要取一个地址……如果参数是寄存器传递的话,地址是哪儿呢?
就好比说有有符号的数要转换成无符号的
int x=-1;
unsigned int y;
是写成 y=x; 呢,还是 y=(unsigned int)x; 呢,还是 y=*(unsigned int*)&x; 呢?
除了第一个写法会有编译警告,都是一样的效果。
@michael_llh:
感谢版主非常详细的解释了这个问题,以前看32的时候还真没有认真注意这个细节的地方,有时候人真的会“自以为是”,很多东西都觉得懂了,其实还是似懂非懂,在往里问一点就搞不清楚。也确实说明我们应该认真细心的学,脚踏实地一个一个搞清楚才是正确的学习方法。
@lcofjp:
我来对号入座了,我就是文中提到的水果君。
首先来讨论一个我认为很有意思的问题,就是这两个强制类型转换:
*(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 *)
.....
都木有任何关系
@samos2011:
(uint32) GPIOx 还是 *(uint32_t*)&GPIOx
如果GPIOx是形参,无论哪种写法,得出的汇编都一样,都是直接取R0,
如果GPIOx 是全局变量,汇编结果也是一样,都是取GPIOx变量的内容
如果GPIOx变量存放到0x10001000,直接取0x10001000里面的内容
编译器非常聪明,认为对指针变量X取地址A再取A里面的内容和直接取X里面的内容是一样的!
@lcofjp:
虽然效果一样,但是明显给阅读程序带来了困难,一般人看到这个表达式都要思考一下才敢下决定。
如我在7楼所说,如果换一种类型,例如:
(uint16_t) GPIOx 和 *(uint16_t*)&GPIOx 未必一样,或许在小端机器上同样被优化,但是大端机器上肯定就不同了。
很显然,@辛昕(别名教主)对我的反驳是很无力的
在@freebsder叔叔的蛊惑下,一直在找我的漏洞,我也觉得代码这东西没有绝对的对与错,虽然那种写法看起来很复杂并且高大上,一个语句展现了好几个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之争,程序员的理论就是运行结果没错那就都不是大问题,不说了
忘了总结了:
虽然看似老婆都是别人家的好,但是代码并不是别人写的就一定好,即使你是一个新手,也不要随便拿来主义随便借鉴,包括我写的代码,很多都比较烂,写的并不严谨,只是为了应付能用就行(能保证在我的应用环境中不出错就行),因为把代码写完美确实是一件很不容易的事,很费时间和精力。
@samos2011:
有以下代码:
char xx;
void GPIO_DeInit1(GPIO4_TypeDef* GPIOx) {
xx = (char)GPIOx;
}
void GPIO_DeInit2(GPIO4_TypeDef* GPIOx) {
xx = *(char*)&GPIOx;
}
在32位机测试结果如下:(ARMCC编译器,优化级别-O0,-O1,-O2,-O3都一样)
xx = (char)GPIOx;
0x10000558 4919 LDR r1,[pc,#100] ; @0x100005C0
0x1000055A 7008 STRB r0,[r1,#0x00]
前者虽然效果非常好,但编译器给出警告: warning: #767-D: conversion from pointer to smaller integer
xx = *(char*)&GPIOx;
0x1000055E B501 PUSH {r0,lr}
0x10000560 4668 MOV r0,sp
0x10000562 4917 LDR r1,[pc,#92] ; @0x100005C0
0x10000564 7800 LDRB r0,[r0,#0x00]
0x10000566 7008 STRB r0,[r1,#0x00]
0x10000568 BD08 POP {r3,pc}
最终结果还是一样
当把char修改为int型,编译器才能对这句xx = *(char*)&GPIOx优化
xx = (int)GPIOx;
0x10000516 4919 LDR r1,[pc,#100] ; @0x1000057C
0x10000518 6048 STR r0,[r1,#0x04]
xx = *(int*)&GPIOx;
0x1000051C 4917 LDR r1,[pc,#92] ; @0x1000057C
0x1000051E 6048 STR r0,[r1,#0x04]
对于这个编译器警告,我猜想,编译器认为不应该把指针变量(即内存地址)强制转换为小于这个地址宽度的值,地址就应该当地址用,不要破坏地址
通过你们的争论,让我对这两个写法产生了好奇,至于用那种,没有警告没有错误就行!
我们都不是研究编译器的,也没有在那个编译器手册中看到这方面的规定
在32位机下 xx = *(long long*)&GPIOx; 同样给出警告 warning: C4487E: read from variable 'GPIOx' with offset out of bounds
也许ARMCC默认了,只是是将指针变量转换为整型,就默认允许,否则就是非常规应用,就给出警告!
@huaiqiao:
我原来其实对这个“去指针”运算没有想的很清楚,那会儿看了各位哥哥们的说法,自己参考了下别人的,在Codeblocks写了个小程序验证了下。
根据C语言语法switch()的括号里面必须是常量表达式。这样以来就让我更加理解了。(我的小程序后面的b = *(int *)p,其实就相当于“赋值”运算)。
感谢各位哥哥啦。
@samos2011:
再次总结一下昨天的测试结果,有以下代码,ARMCC编译器
short x;
// p无论是什么类型的指针或是不是指针都不影响汇编代码
void f1(short* p) {
x = (short)p; // 前者
}
void f2(short* p) {
x = *(short*)&p; // 后者
}
前者当p为指针时只有在类型为int时编译器才不会警告 当 (short)p (char)p 都会警告
后者都没有警告,
虽然汇编的最终结果一样,但如果非要这样用,且不能接受编译器警告,为了消除警告,恐怕要写成这样: (short)(int)p; 当p为形参时汇编结果非常高效:
0x10000516 4919 LDR r1,[pc,#100] ; @0x1000057C
0x10000518 8048 STRH r0,[r1,#0x02]
(short)p同样也是上面的汇编结果,只不过编译器警告!
后者写法虽然复杂,但没有任何错误,对一个变量取地址,再通知编译器按新的方式读取里面的内容,无论这个变量是什么类型,是不是指针,得出的汇编代码也都一样!
由于取地址是个伪操作,意思可以简化为,(编译器:请将这个内存地址的内容按新的方式对待,并读取来),只不过汇编代码显得有些复杂.可能是编译器优化得不够狠!
前提条件是,编程者要知道这个地址里面的东西是什么!
基于这个前提,再看前者写法,意思为(编译器,将这个变量的内容读出来,按新的内容对待,)于是,编译器发现变量是指针,里面的内容是地址,而你却明显不当地址用,直接警告你,后者写法则绕过了编译器的这个判断.
论性能,我喜欢前者,但为了哄编译器,不得不写成这样(short)(int)p