搜索
bottom↓
回复: 195

【原创】MCU程序设计高手进阶(连载)

  [复制链接]

出0入0汤圆

发表于 2013-6-26 18:53:03 | 显示全部楼层 |阅读模式
本帖最后由 yrloy 于 2013-6-26 21:32 编辑

既然敢用如此猖狂的标题,就请各位看官先息怒,一点点看下去。

本人称不上高手,对电路及程序设计有一定心得,也许真正是顶尖高手或自认为是顶尖高手的人根本看不起(或者根本看不懂 ),这样的人请笑笑走开。
而本帖欢迎以下几类朋友,希望你们看到些许有用的知识之时,留下你们的心得,已共大家共同学习交流:

1、针对技术大牛,本帖也会弥补你某些程序设计上的盲点,或者让你感觉相见恨晚。
2、多年MCU程序设计开发者,虽然可以完成大部分工作,但难以更进一步的朋友,也许本帖会开拓你的视野。
3、刚刚初学的朋友,也许某些技巧会让你云里雾里,但是未来的某一天,本帖的内容很可能成为你脑中的火花。

这两年在论坛受益匪浅,这是对论坛的报答,和大家学习交流一些心得技巧。
也希望高手多多拍砖,让大家和我多多学习。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

本文会以实际需求为背景的问答形式,阐述一些MCU程序设计上的技巧与心得。
每周进行一次连载,前后无任何关联,而且每次内容会很短,但信息量很大。希望大家深入讨论并指正错误。

文章会做两个假定:
一是针对读者,要有扎实的C语言功底,至少有一种MCU设计经验,有一定工程项目经验。
二是本文不针对任何一种硬件平台,只做基本假设。可以是任意一种8位、16位、32位单核MCU,512字节以上RAM,有该体系的C语言编译器(最好支持C99的编译器或GCC编译器)。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

正文

一、MCU上的无锁原子读操作

原子读操作是在MCU并发编程中常用的操作,简单举个例子来阐述问题:

我们使用RTOS或裸机状态编程时,必然需要一个全局时钟基准,通常是在一个定时器中断中累加实现,简化代码如下:

static unsigned long volatile __jiffies = 0;    /* 全局时钟基准节拍累加器 */                     

ISR_TIMER()    /* 定时中断服务函数 */
{
    ++__jiffies;
    /* 其它代码...: */
}

对于其中的__jiffies变量,就是全局时间基准,程序中其它地方都会对其进行原子读操作来判断时间,典型的接口实现如下:

unsigned long get_jiffies(void)
{
    unsigned long tmp;

    CLOCK_IRQ_DIS();    /* 关定时中断 */
    tmp = __jiffies;
    CLOCK_IRQ_EN();    /* 开定时中断 */

    return tmp;
}

请注意,其中关于对中断的开关是对该定时中断中所有代码会带来影响。如果在RTOS中,关中断的时间是一种重要性能指标,决定了整个系统的中断快速响应能力。



这其中的意义我相信大家都明白,在此问各位一个问题,以上面代码为例,如何完成一个无需开关中断的原子读操作,保证读__jiffies变量的原子性?
(提示:考虑代码要跨8位、16位或32位单核MCU所有平台)




九点公布答案及讲解,再此期间,欢迎各位MCU程序设计高手解答。



----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------



11楼答案及下一道题


出0入0汤圆

 楼主| 发表于 2013-6-26 19:45:05 | 显示全部楼层
哎,估计不会因为太难(或太简单?)才没有人来一起讨论,毕竟还是有高手的
根本原因还是现在浮躁的研发人员,不会参与这类互动性学习讨论,只想现成的而不思考,更不懂回馈。
算了

出0入0汤圆

发表于 2013-6-26 19:59:19 | 显示全部楼层
好久没写程序了,随便写一个答案吧,抛砖引玉,两种情况:
1.如果读取操作一条汇编指令就完成了的话,直接一读应该就可以了。
2.一条汇编完不成的情况,读两次,两次的值相等则完成了读操作,不相等的话再读取比较。(严格上来讲这个方法不是原子操作,但是起到了相同的效果)

出0入0汤圆

发表于 2013-6-26 20:18:24 | 显示全部楼层
想到两个办法
1。增加一个全局可单周口期操作的BOOL变量,读定时值前测试这个变量的值,为1则清0,用完再写回1。(好像又不行,要有测试清零的指令才可以)
2。读定时值先读取低字节,再读高字节(这样也有失败的风险)
3。好像最好的方法就是关中断了

出0入0汤圆

发表于 2013-6-26 20:19:05 | 显示全部楼层
等楼主公布答案

出0入0汤圆

发表于 2013-6-26 20:19:31 | 显示全部楼层
个人见解,读操作没必要关中断.

出110入93汤圆

发表于 2013-6-26 20:36:20 | 显示全部楼层
不是高手,新手。

可不可以在ISR_TIMER()中,复制一份到另一个全局变量供读取。
让__jiffies只在ISR_TIMER()可读写。

出0入0汤圆

发表于 2013-6-26 21:06:13 | 显示全部楼层
搬个凳子进来学习!~                          

出0入0汤圆

发表于 2013-6-26 21:07:48 | 显示全部楼层
先占个位

出0入0汤圆

 楼主| 发表于 2013-6-26 21:22:41 | 显示全部楼层
本帖最后由 yrloy 于 2013-6-26 21:45 编辑

立刻有朋友在没时间测试的情况下就答出正解,真是好感觉
感谢参与讨论的人,这会使大家一起进步

答案非常简单,我逆着说,先说说一些情况为什么不行。
在此假设一个最艰难的架构,8位机(AVR、51等等),其上只有8位单字节数据的读写是单指令原子的,其中unsigned long型在这样的架构下是32位8字节。

根据各位朋友提出情况,进行说明:

1、有朋友认为读操作没必要关中断.

这个显然不可能,当你读了32位变量任何一个字节的时候,剩下的7个字节都可能改变。

2、认为在中断函数建立数据拷贝

这个理由同上,无论如何复制,都难以避免读的瞬间数据被破坏

3、建立单字节原子锁

该体系必须支持测试清零指令,而且就算支持。如果中断里发现锁被占有了,那这个周期还能进行+1操作么?无论是用变量缓存还是丢弃,所记时间都不准了。


四楼的 lbc___  的朋友正解,功力深厚,好感觉

实现如下:
unsigned long get_jiffies(void)
{
    unsigned long tmp;

    do {
        tmp = __jiffies;
    } while(tmp != __jiffies);

    return tmp
}

简单的大家可能都不相信,可以满足任何MCU架构完成如上对__jiffies变量的操作(必须单核),大家可以仔细想想。
无锁单读单写队列是MCU上经常用的,对中断通信接口的缓冲非常方便可靠。以此为基础,可跨平台实现。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

二、unsigned char、unsigned int、unsigned long的使用(解放思维,拒绝权威误导)

大家不知道注意没注意到我上面对 __jiffies的定义使用的unsigned long,有很多所谓“高手”会来指导我这个菜鸟说:“应该自己定义类型或使用C99的<stdint.h>中的uin32_t,这样才能跨平台,下次注意啊!”
我也相信很多人(包括我)看过所谓“高手指南”或者"教科书",说ANSI C没有规定类型的具体位宽,要跨平台设计,要多用类型定义或使用<stdint.h>等类似方法。也许最初提出这些的老外初衷是正确的,但最终已经被淘汰的规则还被高手当成定律来教育新手,很搞笑。

首先肯定,ANSI C真的没规定类型的具体位宽,在说一个背景:
最初linux内核是从93年(没记错?!)被移植到其它平台的,当时大量的类型定义被使用,典型的u8\__u8\u16\__u16\u32\__u32。
而今天,内核中除了处理通信协议、外设驱动(固定位宽)以及很特殊需要隐藏的类型,你不会看到这套定义(u64除外,公布答案时解释),只有C标准原生定义!
linux可是跨平台的,而且是底层程序密布的庞然大物,绝大多数代码必须支持数十个架构。




大家家说说为什么会这么做,原因是什么?而且既然如此,如何假设个体变量的长度?如何来写跨平台代码?




明天找时间公布答案,大家先想想,希望大家多多讨论。

出0入0汤圆

发表于 2013-6-26 22:33:21 | 显示全部楼层
LZ说的什么,看来我还是没入门,

出0入0汤圆

发表于 2013-6-26 22:36:16 | 显示全部楼层
菜鸟围观

出0入0汤圆

发表于 2013-6-26 22:55:14 来自手机 | 显示全部楼层
学习了…
来自:amoBBS 阿莫电子论坛 Windows Phone 7 客户端

出0入0汤圆

发表于 2013-6-26 22:58:51 | 显示全部楼层
yrloy 发表于 2013-6-26 21:22
立刻有朋友在没时间测试的情况下就答出正解,真是好感觉。
感谢参与讨论的人,这会使大家一起进步 ...

很感谢楼主带来这么一个小品 希望可以坚持下去啊

第一个题目可能是我没明白题意

保证原子性 就是说读取数值的过程中没有被中断打断 或者这个数值没有被修改 对吗

出0入0汤圆

发表于 2013-6-26 23:02:38 | 显示全部楼层
第二个问题 还是等楼主吧 完全没有想法 一直以为自定义是很好的

出0入0汤圆

发表于 2013-6-26 23:12:20 来自手机 | 显示全部楼层
学习学习

出0入0汤圆

发表于 2013-6-26 23:21:02 | 显示全部楼层
进来学习一下。软件方面不是很熟练。。

出0入0汤圆

发表于 2013-6-26 23:33:14 | 显示全部楼层
学习了,很好....

出0入0汤圆

发表于 2013-6-26 23:40:24 | 显示全部楼层
楼主的教程不错,实际经验出发。决定关注到底,哈哈。
第一个我是这样理解,不关中断也能够正确读取完整的数据。对不?

出0入0汤圆

发表于 2013-6-27 00:22:36 | 显示全部楼层
哇,怎么热闹在上课啊,菜鸟搬个板凳来听课

出0入0汤圆

发表于 2013-6-27 00:55:57 | 显示全部楼层
我来听课!

出0入0汤圆

发表于 2013-6-27 00:57:33 | 显示全部楼层
一直搞windows下的开发, 玩单片机也有日子了还真没去注意同步这个事... 好在做的一些小玩意没出问题 不过出问题也可能是早晚的事.
谢谢楼主 受教了.

用avr的话8位就无所谓了, 8位以上就悲催了 按上面例子来的话 如果无锁定处理
16位 先读低位再读高位 读低位时 为0x01ff 读高位时如果中断了一次就成了0x0200 如果碰巧最终结果是 0x02ff 反过来先读高再读低 0x0100....

出0入0汤圆

发表于 2013-6-27 01:01:42 来自手机 | 显示全部楼层
楼上没看11楼讲解。。

出0入0汤圆

发表于 2013-6-27 01:18:28 来自手机 | 显示全部楼层
只能支持,LZ继续

出0入0汤圆

发表于 2013-6-27 01:51:30 来自手机 | 显示全部楼层
楼主位的题目在没看答案情况下猜到了,原子操作是从傻孩子那里学习到的,但从没想过无锁这个问题,平时使用大部分都是使用带锁的。至于跨平台数据类型,基本上都是使用<stdint.h>

出0入0汤圆

发表于 2013-6-27 02:44:49 | 显示全部楼层
占位 学习。。。

出0入0汤圆

发表于 2013-6-27 03:18:12 | 显示全部楼层
基础不到位,我说下第一题的理解,楼主帮我看看我理解的对不对行不?

题目的意思应该是,读取变量的时候可能会有中断触发,要求在不关闭中断的情况下完整地读取那个变量的值。

答案就是通过一个do……while……的循环在读取之后再进行一次对比,防止读取的过程中改变。

如果这样的话,是存在可能循环相当大的一个次数的

出0入0汤圆

发表于 2013-6-27 06:40:15 来自手机 | 显示全部楼层
讨论贴,支持!

出0入0汤圆

发表于 2013-6-27 07:03:27 | 显示全部楼层
楼主继续。
顶。
写一个稳定可靠地程序,确实要考虑很多东西。

出0入0汤圆

发表于 2013-6-27 07:25:05 来自手机 | 显示全部楼层
看上去不错

出0入0汤圆

发表于 2013-6-27 08:01:14 | 显示全部楼层
学习学习了~~

出0入0汤圆

发表于 2013-6-27 08:35:33 | 显示全部楼层
第一个已经知道了。
第二个看楼主讲解了,为啥呢?我还真的见见过任何资料有谈及。

出0入0汤圆

发表于 2013-6-27 08:41:38 | 显示全部楼层
等听课,楼主的第一题看懂了,第二题不觉明历,恩恩

出0入0汤圆

发表于 2013-6-27 08:51:48 | 显示全部楼层
感觉不错,等待

出0入0汤圆

发表于 2013-6-27 08:58:35 | 显示全部楼层
支持 多学习一点

出0入0汤圆

发表于 2013-6-27 09:09:24 | 显示全部楼层
采用信号量。裸机下采用全局变量,初始化为1,一旦资源被访问,该变量减1,所有的其他地方想要访问该资源的,都必须先判断该信号量是否可用。

出0入0汤圆

发表于 2013-6-27 09:31:46 | 显示全部楼层
想到一个方法,不知可否,在中断中复制tmp = __jiffies;并把对temp的操作加锁,貌似也可以实现

出0入0汤圆

发表于 2013-6-27 09:36:28 | 显示全部楼层
yrloy 发表于 2013-6-26 21:22
立刻有朋友在没时间测试的情况下就答出正解,真是好感觉。
感谢参与讨论的人,这会使大家一起进步 ...

讲的太复杂,简单一句话

原子操作 = 汇编单指令

出0入0汤圆

发表于 2013-6-27 10:01:21 | 显示全部楼层
真是不错,一直迷惑于这样的问题,学了一招,谢谢

出0入0汤圆

发表于 2013-6-27 10:03:59 | 显示全部楼层
敢问楼主 __jiffies 在各种条件判断语句中怎么处理?

出0入0汤圆

发表于 2013-6-27 10:04:54 | 显示全部楼层
yrloy 发表于 2013-6-26 21:22
立刻有朋友在没时间测试的情况下就答出正解,真是好感觉。
感谢参与讨论的人,这会使大家一起进步 ...

第一个题的原子操作,在中断中的变量用volatile修饰,是不是也可以?

出0入0汤圆

 楼主| 发表于 2013-6-27 10:05:52 | 显示全部楼层
xiaoziwen 发表于 2013-6-26 22:58
很感谢楼主带来这么一个小品 希望可以坚持下去啊

第一个题目可能是我没明白题意

谢谢支持,只要大家能参与进来,我就尽全力写下去。

原子性操作严格上来说指不可分割的操作,能对数据进行一次完整的读或写
jiffies的随时都可能被改变,如何无锁(不加锁或关中断)把它完整读出来是第一题的意思。

绝非楼主把简单的事情说复杂,而是想讲解清楚。做过的朋友一定知道,这样的错误发生了几乎是不可能定位的(无法重现),无数次才会出现一次。

出0入0汤圆

 楼主| 发表于 2013-6-27 10:08:54 | 显示全部楼层
Stone_up 发表于 2013-6-26 23:40
楼主的教程不错,实际经验出发。决定关注到底,哈哈。
第一个我是这样理解,不关中断也能够正确读取完整的 ...

就是这个意思。

绝非楼主把简单的事情说复杂,而是想一步步讲让大家独立思考。
这样的错误发生了,几乎是不可能定位的(无法重现)。

出0入0汤圆

 楼主| 发表于 2013-6-27 10:15:40 | 显示全部楼层
goolloo 发表于 2013-6-27 03:18
基础不到位,我说下第一题的理解,楼主帮我看看我理解的对不对行不?

题目的意思应该是,读取变量的时候可 ...

就如君所想。

但循环不会多,就算所有指令间都发生了中断,总共也就几次
原则上讲,99.999/100情况不会循环,剩下情况最多循环1次,因为若程序在几条指令间就发生两次中断,可能有些设计问题。

出0入0汤圆

 楼主| 发表于 2013-6-27 10:17:14 | 显示全部楼层
chenxujiaoyang 发表于 2013-6-27 09:09
采用信号量。裸机下采用全局变量,初始化为1,一旦资源被访问,该变量减1,所有的其他地方想要访问该资源的 ...

敢问如何保证信号量的原子性?

出0入0汤圆

 楼主| 发表于 2013-6-27 10:19:41 | 显示全部楼层
wangyeqing333 发表于 2013-6-27 09:31
想到一个方法,不知可否,在中断中复制tmp = __jiffies;并把对temp的操作加锁,貌似也可以实现 ...

其实这跟楼上问题相同,信号量(锁)的实现本身就要保证原子性,如果不关中断,架构就必须有同步指令或类似测试清零指令,然后嵌入汇编来写。对否?

出0入0汤圆

 楼主| 发表于 2013-6-27 10:20:49 | 显示全部楼层
wxty 发表于 2013-6-27 10:03
敢问楼主 __jiffies 在各种条件判断语句中怎么处理?

unsigned long get_jiffies(void)才是要使用的接口,正常用即可。

出0入0汤圆

 楼主| 发表于 2013-6-27 10:28:50 | 显示全部楼层
本帖最后由 yrloy 于 2013-6-27 10:35 编辑
spy2008 发表于 2013-6-27 10:04
第一个题的原子操作,在中断中的变量用volatile修饰,是不是也可以?


如上文我的__jiffies变量已经使用volatile修饰了,但这其实仅仅是防止编译器的优化
很多人都把volatile当作原子量来使用,其实这是误区

引用linux/Documentation的volatile-considered文件的原文来回答这个问题,虽然里面讲的是多线程的环境,但其实是一样的:


以下为正文
---------------------------------------------------------------------

为什么不应该使用“volatile”类型
------------------------------

C程序员通常认为volatile表示某个变量可以在当前执行的线程之外被改变;因此,在内核
中用到共享数据结构时,常常会有C程序员喜欢使用volatile这类变量。换句话说,他们经
常会把volatile类型看成某种简易的原子变量,当然它们不是。在内核中使用volatile几
乎总是错误的;本文档将解释为什么这样。

理解volatile的关键是知道它的目的是用来消除优化,实际上很少有人真正需要这样的应
用。在内核中,程序员必须防止意外的并发访问破坏共享的数据结构,这其实是一个完全
不同的任务。用来防止意外并发访问的保护措施,可以更加高效的避免大多数优化相关的
问题。

像volatile一样,内核提供了很多原语来保证并发访问时的数据安全(自旋锁, 互斥量,内
存屏障等等),同样可以防止意外的优化。如果可以正确使用这些内核原语,那么就没有
必要再使用volatile。如果仍然必须使用volatile,那么几乎可以肯定在代码的某处有一
个bug。在正确设计的内核代码中,volatile能带来的仅仅是使事情变慢。

思考一下这段典型的内核代码:

    spin_lock(&the_lock);
    do_something_on(&shared_data);
    do_something_else_with(&shared_data);
    spin_unlock(&the_lock);

如果所有的代码都遵循加锁规则,当持有the_lock的时候,不可能意外的改变shared_data的
值。任何可能访问该数据的其他代码都会在这个锁上等待。自旋锁原语跟内存屏障一样—— 它
们显式的用来书写成这样 —— 意味着数据访问不会跨越它们而被优化。所以本来编译器认为
它知道在shared_data里面将有什么,但是因为spin_lock()调用跟内存屏障一样,会强制编
译器忘记它所知道的一切。那么在访问这些数据时不会有优化的问题。

如果shared_data被声名为volatile,锁操作将仍然是必须的。就算我们知道没有其他人正在
使用它,编译器也将被阻止优化对临界区内shared_data的访问。在锁有效的同时,
shared_data不是volatile的。在处理共享数据的时候,适当的锁操作可以不再需要
volatile —— 并且是有潜在危害的。

volatile的存储类型最初是为那些内存映射的I/O寄存器而定义。在内核里,寄存器访问也应
该被锁保护,但是人们也不希望编译器“优化”临界区内的寄存器访问。内核里I/O的内存访问
是通过访问函数完成的;不赞成通过指针对I/O内存的直接访问,并且不是在所有体系架构上
都能工作。那些访问函数正是为了防止意外优化而写的,因此,再说一次,volatile类型不
是必需的。

另一种引起用户可能使用volatile的情况是当处理器正忙着等待一个变量的值。正确执行一
个忙等待的方法是:

    while (my_variable != what_i_want)
        cpu_relax();

cpu_relax()调用会降低CPU的能量消耗或者让位于超线程双处理器;它也作为内存屏障一样出
现,所以,再一次,volatile不是必需的。当然,忙等待一开始就是一种反常规的做法。

在内核中,一些稀少的情况下volatile仍然是有意义的:

  - 在一些体系架构的系统上,允许直接的I/0内存访问,那么前面提到的访问函数可以使用
    volatile。基本上,每一个访问函数调用它自己都是一个小的临界区域并且保证了按照
    程序员期望的那样发生访问操作。

  - 某些会改变内存的内联汇编代码虽然没有什么其他明显的附作用,但是有被GCC删除的可
    能性。在汇编声明中加上volatile关键字可以防止这种删除操作。

  - Jiffies变量是一种特殊情况,虽然每次引用它的时候都可以有不同的值,但读jiffies
    变量时不需要任何特殊的加锁保护。所以jiffies变量可以使用volatile,但是不赞成
    其他跟jiffies相同类型变量使用volatile。Jiffies被认为是一种“愚蠢的遗留物"
    (Linus的话)因为解决这个问题比保持现状要麻烦的多。

(楼主注:Linus的话是指直接引用jiffies变量,而非如本帖例子中的接口引用,我已经避免了这个问题)

  - 由于某些I/0设备可能会修改连续一致的内存,所以有时,指向连续一致内存的数据结构
    的指针需要正确的使用volatile。网络适配器使用的环状缓存区正是这类情形的一个例
    子,其中适配器用改变指针来表示哪些描述符已经处理过了。

对于大多代码,上述几种可以使用volatile的情况都不适用。所以,使用volatile是一种
bug并且需要对这样的代码额外仔细检查。那些试图使用volatile的开发人员需要退一步想想
他们真正想实现的是什么。

出0入0汤圆

发表于 2013-6-27 10:38:23 | 显示全部楼层
本帖最后由 wxty 于 2013-6-27 10:50 编辑
yrloy 发表于 2013-6-27 10:20
unsigned long get_jiffies(void)才是要使用的接口,正常用即可。


如果jiffies这种变量很多,程序中多处且各种使用,都这么调用对于资源有限的MCU来说也是个问题,需综合考量。

楼主继续讲解,关注中。。。

出0入0汤圆

 楼主| 发表于 2013-6-27 10:47:03 | 显示全部楼层
本帖最后由 yrloy 于 2013-6-27 10:50 编辑
wxty 发表于 2013-6-27 10:38
如果jiffies这种变量很多,程序中多处且各种使用,都这么调用对于资源有限的MCU来说。。。 ...


你好,这个地方要看个人理解,很多人赞成你的观点,也有很多反对(我也是)。

我最初写代码很极端,计较指令级的得失,务必把代码优化到极致。
但几年后我变了,只做算法复杂度的优化、架构的优化,细节放心交给编译器来管。
我当初的极端优化,总是在我把代码复用到其它平台时产生各种问题,反而要重写,耽误时间和系统稳定性。

所以现在代码方针,第一是健壮,第二是高可复用,第三是可读性
然而不代表我写的代码运行慢,其实我们优化的,通常都不是程序主要消耗时间的地方。

出0入0汤圆

发表于 2013-6-27 10:59:25 | 显示全部楼层
好贴呀,好好学习

出0入0汤圆

发表于 2013-6-27 11:12:01 | 显示全部楼层
需要再仔细理解

出0入0汤圆

发表于 2013-6-27 12:05:20 | 显示全部楼层
有所收获,谢谢楼主分享,希望能看到更多,,

出0入0汤圆

发表于 2013-6-27 12:46:02 | 显示全部楼层
mark

出0入0汤圆

发表于 2013-6-27 12:59:08 | 显示全部楼层
占座学习

出0入0汤圆

发表于 2013-6-27 13:04:21 | 显示全部楼层
楼主的总结很好

出0入0汤圆

发表于 2013-6-27 13:07:06 | 显示全部楼层
chenxujiaoyang 发表于 2013-6-27 09:09
采用信号量。裸机下采用全局变量,初始化为1,一旦资源被访问,该变量减1,所有的其他地方想要访问该资源的 ...

同意。
semaphore主要用于资源数量大于1的时候,例如表示有限的并行缓冲区大小之类,这里用一个mutex就够用了。
提议大家可以一起努力封装一个轻量的用于51等MCU的多线程库,可以定义一个bit表示mutex互斥锁,当然前提是mutex的访问代码必须要是atomic的。

出0入0汤圆

发表于 2013-6-27 13:18:58 | 显示全部楼层
本帖最后由 shuxmpx123 于 2013-6-27 13:20 编辑

楼主厉害,能否举个例子,说明 这样做的必要性?
我指的是这个:
unsigned long get_jiffies(void)
{
    unsigned long tmp;

    do {
        tmp = __jiffies;
    } while(tmp != __jiffies);

    return tmp
}

出0入0汤圆

发表于 2013-6-27 13:27:57 | 显示全部楼层
受益中,谢谢

出0入0汤圆

发表于 2013-6-27 13:40:06 | 显示全部楼层
期待爆料!!!!!!

出0入0汤圆

发表于 2013-6-27 14:02:36 | 显示全部楼层
学习一下

出0入0汤圆

发表于 2013-6-27 14:03:10 | 显示全部楼层
继续支持楼主。                     

出0入0汤圆

 楼主| 发表于 2013-6-27 14:11:23 | 显示全部楼层
shuxmpx123 发表于 2013-6-27 13:18
楼主厉害,能否举个例子,说明 这样做的必要性?
我指的是这个:
unsigned long get_jiffies(void)

如例子中的情形有很多

再比如,串口中断接收编程,最恰当的方式是所有接收的数据都被压入队列缓冲,然后程序中从队列读再处理
队列的队头、队尾就会遇到并发访问问题,此方法正适合。

当然,关中断不是不好,但有缺点:
1、降低系统的实时响应能力
2、无法跨平台,我们并不会熟悉所有平台,移植的隐形风险不小。看看下面典型伪码:

    关中断();
    读取操作();
    开中断();

如果中断之前就是关的,这样代码就致命了,因此你肯定要这样实现:

    保存中断标志,关中断();
    读取操作();
    恢复中断标志();

这样的开关中断方式每个平台都不同,在移植时麻烦容易出错不说,其实完全可以避免。

---------------------------------------------------------------------------------------------------------------------------------------------

借楼说明,希望大家多讨论问题二,这个答案网上、书上很难有提及,但涉及很多思考方法,大家可以开拓思路说说自己想法
我的也不是标准答案,晚上和大家说说我的一些想法,请大家指正!



出0入0汤圆

发表于 2013-6-27 14:16:02 | 显示全部楼层
学习一下,诶观众

出0入0汤圆

发表于 2013-6-27 14:48:52 | 显示全部楼层
有深度,围观一下!

出0入0汤圆

发表于 2013-6-27 15:05:29 | 显示全部楼层
楼主挖的坑什么时候填上,坐等中···

出0入0汤圆

 楼主| 发表于 2013-6-27 15:11:54 | 显示全部楼层
macaroni 发表于 2013-6-27 15:05
楼主挖的坑什么时候填上,坐等中···

您好,谢谢你支持!
问题二还没有人发表想法,不是卖关子,但直接“给鱼”不是本帖的目的呀。
初定今晚八点左右。

出0入0汤圆

发表于 2013-6-27 16:38:10 | 显示全部楼层
yrloy 发表于 2013-6-27 15:11
您好,谢谢你支持!
问题二还没有人发表想法,不是卖关子,但直接“给鱼”不是本帖的目的呀。
初定今晚八 ...

假使想要申请一个32bit的机器无关的变量。

void* p = (void*)malloc(4);

出0入0汤圆

发表于 2013-6-27 17:10:47 | 显示全部楼层
chinabn 发表于 2013-6-27 09:36
讲的太复杂,简单一句话

原子操作 = 汇编单指令

看了4楼,11楼,及您的解释,豁然开朗,感谢无私的朋友们

出0入0汤圆

 楼主| 发表于 2013-6-27 18:34:11 | 显示全部楼层
Johnwoo 发表于 2013-6-27 13:07
同意。
semaphore主要用于资源数量大于1的时候,例如表示有限的并行缓冲区大小之类,这里用一个mutex就够 ...

其实几年前,我写过基于AVR的调度器应用于产品里,帖子还在:

【原创】最高效率使用单片机,放弃程序中的延时函数
【原创OS】执行效率与裸机无异的实时内核Z1/OS

现在回头看看,我当时虽然思维活跃,但是水平太弱,而且做过的项目太少,想法过于天真。
当时想写个OS简化项目里的程序设计,但对于51\AVR这类单片机,其实并不实用
调度器消耗的时间及内存远比项目做的事情多,而且潜伏BUG多,产品里好几个月之后才偶尔出现。

推荐小规模控制器上使用Protothreads库(协程库,可以理解为线性化编程的状态机)
大规模处理器上直接移植Linux(基本上只要有MMU的芯片都不用自己移植了)

出0入0汤圆

发表于 2013-6-27 18:45:03 | 显示全部楼层
占座, 听LZ讲课, 第一个问题, 已让我受益了!

出0入0汤圆

发表于 2013-6-27 20:18:19 来自手机 | 显示全部楼层
来长长见识

出0入0汤圆

发表于 2013-6-27 20:32:54 | 显示全部楼层
LZ讲的第一个问题很好,我的理解是其实在MCU中只有主循环一个进程,那么开一个中断就相当于多一个进程,当两个进程同事操作一个变量的时候就有这个问题了,这时候原子操作就发挥作用了,处理的方法就是楼主提到的两种。其实在这两个或多个进程中还有一个值得注意的问题就是函数的重入问题,这个也是要特别注意的。楼主的第二个问题还不知道,不过看样子是跟跨平台有关吧,呵呵。非常喜欢楼主的讲解方式,希望能多讲讲....

出0入0汤圆

发表于 2013-6-27 20:33:10 | 显示全部楼层
yrloy 发表于 2013-6-27 18:34
其实几年前,我写过基于AVR的调度器应用于产品里,帖子还在:

【原创】最高效率使用单片机,放弃程序中 ...

unsigned long get_jiffies(void)
{
    unsigned long tmp;

    do {
        tmp = __jiffies;
    } while(tmp != __jiffies);

    return tmp
}  

如果时基中断非常快,这里会不会死循环呢?

出0入0汤圆

 楼主| 发表于 2013-6-27 20:39:28 | 显示全部楼层
本帖最后由 yrloy 于 2013-6-27 20:53 编辑
jacobson 发表于 2013-6-27 20:32
LZ讲的第一个问题很好,我的理解是其实在MCU中只有主循环一个进程,那么开一个中断就相当于多一个进程,当 ...


谢谢支持

不过请问,把接口变量都放在栈上(都是局部变量或参数),重入又如何?

出0入0汤圆

 楼主| 发表于 2013-6-27 20:49:58 | 显示全部楼层
本帖最后由 yrloy 于 2013-6-27 20:52 编辑
spy2008 发表于 2013-6-27 20:33
unsigned long get_jiffies(void)
{
    unsigned long tmp;


这几句话在32位平台上也就3-4个周期,8位平台也就10多个周期

以32M为论,间隔时间t = 1 / 32000000 * 10 = 313ns,除非设计有误,否则不太现实,

出0入0汤圆

发表于 2013-6-27 20:57:11 | 显示全部楼层
厉害 学习了  。。。

出0入0汤圆

发表于 2013-6-27 21:22:53 | 显示全部楼层
yrloy 发表于 2013-6-27 20:39
谢谢支持

不过请问,把接口变量都放在栈上(都是局部变量或参数),重入又如何? ...

被重入的函数即使都声明为局部变量,重入后仍有变量被修改的风险,最好是声明成可重入函数或宏。 是否理解有误?请楼主指点一二

出0入0汤圆

 楼主| 发表于 2013-6-27 21:35:01 | 显示全部楼层
本帖最后由 yrloy 于 2013-6-27 22:43 编辑

感谢大家对第一题的肯定与支持,但大家对第二题的积极性很小,先说简单的一部分原因,缩小论点,希望大家理解后深入讨论。

早期的编程规则是推荐多用类型定义的,后来实践中遇到问题,所以调整为截然相反的结果。
Linux内核现在编程规范中对typedef关键字恶评出于很多原因,先说两点:

1、对结构体和指针使用typedef是一个错误。当你在代码里看到:
vps_t a;
这代表什么意思呢?
相反,如果是这样
struct virtual_container *a;
你就知道“a”是什么了。

2、无缘由的包装类型只会使简单事情变复杂。
绝大多数人隐藏类型的原因只是少写几个字
另一部分人无法判断整型值范围,所以基本全部定义一遍
如果一个变量的范围在合理内,就不要隐藏它的类型。让使用者不知真实类型,赋值时型别转换会引发不必要的问题(比如溢出、截断)。

Linux内核推荐在如下情况使用typedef(摘自内核文档中CodingStyle文件):
--------------------------------------------------------------------------------------------------------------------------------------
原文如下:
很多人认为typedef“能提高可读性”。实际不是这样的。它们只在下列情况下有用:

(a) 完全不透明的对象(这种情况下要主动使用typedef来隐藏这个对象实际上是什么)。

     例如:“pte_t”等不透明对象,你只能用合适的访问函数来访问它们。

     注意!不透明性和“访问函数”本身是不好的。我们使用pte_t等类型的原因在于真的是
     完全没有任何共用的可访问信息。

(b) 清楚的整数类型,如此,这层抽象就可以帮助消除到底是“int”还是“long”的混淆。

     u8/u16/u32是完全没有问题的typedef,不过它们更符合类别(d)而不是这里。

   (楼主注:这部分就是指一些通信协议或者定长外设驱动的编写,它们当然必须定长)

     再次注意!要这样做,必须事出有因。如果某个变量是“unsigned long“,那么没有必要

        typedef unsigned long myflags_t;

     不过如果有一个明确的原因,比如它在某种情况下可能会是一个“unsigned int”而在
     其他情况下可能为“unsigned long”,那么就不要犹豫,请务必使用typedef。

--------------------------------------------------------------------------------------------------------------------------------------


说了这么多类型隐藏的问题。回头来问大家,ANSI C没规定类型的宽度,那如果不隐藏类型,如何判断你所用类型的宽度?如何跨平台的使用这些类型?
缩小了论点,希望大家可以独立思考出结果,明天进行完整说明。







出0入0汤圆

 楼主| 发表于 2013-6-27 21:39:02 | 显示全部楼层
本帖最后由 yrloy 于 2013-6-27 22:47 编辑
jacobson 发表于 2013-6-27 21:22
被重入的函数即使都声明为局部变量,重入后仍有变量被修改的风险,最好是声明成可重入函数或宏。 是否理 ...


说一个复杂环境,比如__jiffies变量在中断里记时间,同时还有十个线程在运行,又同时调用了下面代码的函数(如果是类似KEIL的编译器,要加入关键字reentrant):

unsigned long get_jiffies(void)
{
    unsigned long tmp;

    do {
        tmp = __jiffies;
    } while(tmp != __jiffies);

    return tmp
}  

这都不会产生任何问题,因为变量tmp在栈上,没有一个任务会读到不完整的__jiffies变量,你仔细想想,我们明天讨论下

出0入0汤圆

发表于 2013-6-27 22:00:00 | 显示全部楼层
楼主讲的很好..我一定持续关注..

出0入0汤圆

发表于 2013-6-27 22:11:53 | 显示全部楼层
spy2008 发表于 2013-6-27 20:33
unsigned long get_jiffies(void)
{
    unsigned long tmp;

时基中断非常快,这里即使没有,程序也根本走不动,反复就这一个函数。

出0入0汤圆

发表于 2013-6-27 22:14:49 | 显示全部楼层
jacobson 发表于 2013-6-27 21:22
被重入的函数即使都声明为局部变量,重入后仍有变量被修改的风险,最好是声明成可重入函数或宏。 是否理 ...

局部变量每次都会有一个新的。

出0入0汤圆

发表于 2013-6-28 08:33:57 来自手机 | 显示全部楼层
channe 发表于 2013-6-27 22:14 局部变量每次都会有一个新的。

在c51中函数被重入后运行的并不是函数的副本,局部变量仍有被修改的风险。

出0入0汤圆

发表于 2013-6-28 08:42:45 来自手机 | 显示全部楼层
yrloy 发表于 2013-6-27 21:39 说一个复杂环境,比如__jiffies变量在中断里记时间,同时还有十个线程在运行,又同时调用了下面代码的函 ...

这个函数这样读当然不会有问题,它是用时间换正确率,即使这个函数被重入也没问题,可以通过多次读做校正,但换个函数可能就不一样了。

出0入0汤圆

 楼主| 发表于 2013-6-28 08:51:52 | 显示全部楼层
本帖最后由 yrloy 于 2013-6-28 08:56 编辑
jacobson 发表于 2013-6-28 08:42
这个函数这样读当然不会有问题,它是用时间换正确率,即使这个函数被重入也没问题,可以通过多次读做校正 ...


跟你说的完全不是一个意思。

简单点,求累加和

int sum(int n)
{
    int i, sum = 0;

    for (i = 0; i < n ; ++n)
        sum += n;

    return sum;
}

这个函数也是可重入的,毫无问题,多少个线程同时进入都行
因为参数和局部变量在栈上,所有的变量每次地址都是新的(在栈上)。
C51是因为它不符合标准C,但也提供了关键字解决,后来的编译器都没这样的事了。

出0入0汤圆

发表于 2013-6-28 09:56:23 | 显示全部楼层
yrloy 发表于 2013-6-27 21:35
感谢大家对第一题的肯定与支持,但大家对第二题的积极性很小,先说简单的一部分原因,缩小论点,希望大家理 ...

菜鸟弱弱的说下          先用sizeof测量类型长度        然后再定义吗        比如8位平台      len=sizeof(int)        然后再typedef           期待楼主的答案!!!    希望能坚持下去          谢谢了      楼主加油!!!   

出0入0汤圆

发表于 2013-6-28 09:57:41 | 显示全部楼层
关注……

出0入0汤圆

发表于 2013-6-28 10:01:56 | 显示全部楼层
yrloy 发表于 2013-6-27 20:39
谢谢支持

不过请问,把接口变量都放在栈上(都是局部变量或参数),重入又如何? ...

函数的重入是个值得研究的命题,
因为把接口变量都放在栈上(都是局部变量或参数) 这样对变量是OK了, 但有些时候, 硬件资源却不允许.

出0入0汤圆

 楼主| 发表于 2013-6-28 10:43:05 | 显示全部楼层
如您所说,硬件资源确实如此。
这个问题在任何系统或框架上都一样,一定时间段内,加锁独占获取硬件资源、使用、然后释放。

出0入0汤圆

 楼主| 发表于 2013-6-28 10:48:50 | 显示全部楼层
leijiayou 发表于 2013-6-28 09:56
菜鸟弱弱的说下          先用sizeof测量类型长度        然后再定义吗        比如8位平台      len=siz ...

您好,

这类方式其实跟直接包含C99的<stdint.h>文件是一样的。
换个角度想想,如果真的这样,为何ANSI C不把变量类型直接制定为定长的?

出0入0汤圆

发表于 2013-6-28 12:05:43 | 显示全部楼层
yrloy 发表于 2013-6-28 10:48
您好,

这类方式其实跟直接包含C99的文件是一样的。

C语言没有明确规定它们所占内存的字节数,只要求long型数据长度不短于int型,short型不长于int型。具体是多少是实现由计算机系统自行决定;    您前面提出的两个问题:1,如何判断所用类型宽度      这个应该要在确定在什么平台之后才能判断吧     比如8位机   32位机     unsigned long肯定不同              2,如何跨平台使用这些类型           看你前面说的应该用C的原生态定义   比如unsigned long        这个不明白是什么意思?   比如我在32位上有一个数据为unsigned int = 0x22222222;(4个字节)        但是如果在8位,这个数据肯定不对了          你的第二个题目还没有具体太看懂。。。  

出0入0汤圆

发表于 2013-6-28 12:48:26 | 显示全部楼层
我是来学习的。

出0入0汤圆

发表于 2013-6-28 13:55:14 | 显示全部楼层
做个标记。

出0入0汤圆

 楼主| 发表于 2013-6-28 13:57:20 | 显示全部楼层
leijiayou 发表于 2013-6-28 12:05
C语言没有明确规定它们所占内存的字节数,只要求long型数据长度不短于int型,short型不长于int型。具体是 ...

您好,您问题已经理解对了

ANSI C确实没定位宽,然而本帖还推荐使用原生类型,怎么用?如何用?

出0入0汤圆

发表于 2013-6-28 14:56:38 | 显示全部楼层
从可移植性来说,我觉得u8,u16等这些让人一看就明白是怎么回事。楼主说的其它隐含的类型确实不可取。。

出0入0汤圆

发表于 2013-6-28 17:08:51 | 显示全部楼层
学习学习~期待更多~

出0入0汤圆

发表于 2013-6-28 17:52:35 | 显示全部楼层
yrloy 发表于 2013-6-28 13:57
您好,您问题已经理解对了

ANSI C确实没定位宽,然而本帖还推荐使用原生类型,怎么用?如何用? ...

这个问题真心不错,可惜超过我说了解的范围。。。   惭愧         看了下网上的资料和在群里面讨论了下,都没什么大的收获,只有期待楼主的答案了!       接下来说说看法吧       首先 跨平台是不是有一个前提的吧      比如多少位到多少位可以用(如16位   32位   64位)????    第二,跨平台数据的储存怎么保证(同样int不同平台分配内存大小是不一样的)??(既然推荐用原生定义,应该不用再继续判断长度然后再定义了吧)     第三,跨平台的代码具体用什么类型的变量是没法由sizeof来测的??  只能靠C语言标准保证??        个人感觉跨平台数据存储是个难点。。。。     期待您更精彩的解答,谢谢!        希望楼主能坚持下去 ,要不也来个你不知道的100个程序设计问题大讨论。。。   

出0入0汤圆

发表于 2013-6-28 18:12:44 | 显示全部楼层
努力学习中

出0入0汤圆

 楼主| 发表于 2013-6-28 19:12:53 | 显示全部楼层
本帖最后由 yrloy 于 2013-6-28 19:30 编辑

第二题做个了结吧,让大家久等了,这不是标准答案,大家完全可以有自己的理解

ANSI C虽然规定了变量的最小长度,但没有规定具体的标准长度,它们可以根据编译器的实现而变化。
C语言的标准数据类型长度随体系结构变化这一特性不断引起争议。好的一面是标准数据类型可以充分利用不同体系结构变化的字长而无需明确定义长度。不好的一面是在编程时不能对标准的C数据类型进行大小的假定。

其实各类型的大小是可以预知的,已知条件已定(long long类型及64位变量容后讨论):

1、ANSI C标准规定char <=  short <= int <= long,short短整型不得少于16位
2、各体系结构编译器至少要提供8、16、32三种宽度变量,虽然这点标准里没有明确写出,但1989年以后的编译器绝无意外。

根据这两个条件,玩个逻辑推理游戏,则不管在8\16\32还是64位体系下,都可以得出如下结论:

1、char一定是8位
2、short一定是16位
3、int至少16位
4、long至少32位
5、如果int为16位,则long必然为32位

上面五点是绝无意外的
而且大家都知道,ANSI C 让所有默认计算中类型都默认为int
这点最初意图是让编译器定义的int尽量符合体系结构中运行最快的变量,但如下架构没有符合:

1、8位架构上,int是16位,由于上述的规则第3点,自然无法把int实现为8位(但编译器一般也提供一个非标准的编译参数,可以让8位机将int作为8位使用,不过相信没人用)。
2、64位架构上,int是32位,一部分原因是由于必然要提供一个32位类型,另一部分原因是制定标准是一帮纯软件编程的人(只在32位或64位PC上编程),int是32位的习惯都到骨子里了,反而long型在PC机上是随体系变化的变量类型。

大家已知听了以上分析,在说最终结论前,在说一说大家在类型使用上的一个重大误区

能用短类型就用短的类型,能用char绝不要用int,因为它快,它占用内存还小(不知道最初是哪个半吊子说这话误导的人 )!
这个论点完全错误,其中一方面看下面的例子:

    unsigned char i;
    for (i = 0; i < 100; ++i) {
        /* 代码 */
    }

    int i;
    for (i = 0; i < 100; ++i) {
        /* 代码 */
    }

上面两段代码任何平台都没有区别(除非强制关闭所有优化),无论速度还是大小。
反而第二种方式的代码更安全,我们要学会相信编译器。

另一方面:
短类型不一定快,32位平台,32、16、8的处理速度是一样的(甚至有些操作短类型可能更慢,比如带符号移位)
16位平台下16位及8位处理速度一致,只有8位平台下8位会比16位快,但绝大多数情况编译器会替你把使用范围一定小于255的16位变量优化成8位。

再说大小,局部变量和参数都是在栈或者寄存器中,它们不会占用什么独立内存,使用完就释放了。比如栈上内存的分配,无非是进入函数时,执行SP -= n操作(SP是栈顶指针,n是临时分配的内存数量)。



最后,说明一下跨平台程序(8/16/32/64)中变量类型的使用具体方法及注意事项:

1、#define定义常数时,如果数值大于65535,一定要加UL或L(编译器默认把所有数值看位int,而int至少为16位)
例如:#define VAR 70000UL

2、常规如果变量范围小于65535,能用int类型就使用int,安全且不会影响执行速度。
例如:
    int i;
    for (i = 0; i < 100; ++i) {
        /* 代码 */
    }

3、大于65535的值使用long型即可,long型至少32位。
例如:本帖的时钟累加器,显然要至少32位
static unsigned long volatile __jiffies = 0;


4、编写操作外部设备的驱动或通信协议包时,应该包含<stdint.h>头文件,使用里面定义的标准类型。
例如:
#include <stdint.h>
struct usb_config_descriptor { /* USB配置描述符包定义 */
        uint8_t  bLength;
        uint8_t  bDescriptorType;

        uint16_t wTotalLength;
        uint8_t   bNumInterfaces;
        uint8_t   bConfigurationValue;
        uint8_t   iConfiguration;
        uint8_t   bmAttributes;
        uint8_t   bMaxPower;
} __attribute__ ((packed));

5、大规模计算或者查表类数组时,如果是char或short则直接使用,是int或long时则使用步骤4的方式。
例如:
static unsigned short crc16table[256];
static uint32_t crc32table[BE_TABLE_SIZE];

6、参考本帖80楼所述的Linux拒绝typedef的理由,除非万不得已,否则没必要隐藏数据的类型。



这个系列至少可以写十几期,大家还可以随时提出更多的话题,但目的是大家都一起讨论进来,否则就难以持续下去。
感谢各位!

回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-5-6 08:14

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表