搜索
bottom↓
回复: 11

急求马老师。关于您给出的spi设计实例中putchar函数中((SPSR & 0x80) == 0)判断

[复制链接]

出0入0汤圆

发表于 2011-9-7 22:12:26 | 显示全部楼层 |阅读模式
最近在学习spi,用的是您在05年在论坛上 仿照 uart的中断+缓冲的方法给出的例子。我自己用的mega48+4m晶振(spi是16分频),但是遇到了很奇怪的问题,纠结了一整天。
在应用定义的putchar函数时 必须对其中 SPDR=c 这句进行数次重复 或者在前面加入延时(在这句之后加入没作用),否则不能进入spi的中断。(刚才测试发现在加入延时的情况下会发送两个数,奇怪了)
在修改后正常使用,但是因为在这纠结了一天 所以非常想问明白这是为什么?在其他avr芯片上也会这样吗?非常感谢
=================================
最近的发现在1楼


==================





原始代码的网址http://www.ourdev.cn/machao_spi_design.html
并将原文附在下面:
串行接口SPI接口应用设计

 作者:马潮老师 / 整理:armok / 2005-01-17/ www.OurAVR.com

      使用的同步串行三线SPI接口,可以方便的连接采用SPI通信协议的外围或另一片AVR单片机,实现在短距离内的高速同步通信。ATmega128的 SPI采用硬件方式实现面向字节的全双工3线同步通信,支持主机、从机和2种不同极性的SPI时序,通信速率有7种选择,主机方式的最高速率为1/2系统时钟,从机方式最高速率为1/4系统时钟。

      ATmega128单片机内部的SPI接口也被用于程序存储器和数据E2PROM的编程下载和上传。但特别需要注意的是,此时SPI的MOSI和 MISO接口不再对应PB2、PB3引脚,而是转换到PE0、PE1引脚上(PDI、PDO),其详见第二章中关于程序存储器的串行编程和校验部分的内容。

      ATmega128的SPI为硬件接口和传输完成中断申请,所以使用SPI传输数据的有效方法是采用中断方式+数据缓存器的设计方法。在对SPI初始化时,应注意以下几点:

        .正确选择和设置主机或从机,以及工作模式(极性),数据传输率;

        .注意传送字节的顺序,是低位优先(LSB First)还是高位优先(MSB Frist);

        .正确设置MOSI和MISO接口的输入输出方向,输入引脚使用上拉电阻,可以节省总线上的吊高电阻。

       下面一段是SPI主机方式连续发送(接收)字节的例程:


        #define SIZE 100
        unsigned char SPI_rx_buff[SIZE];
        unsigned char SPI_tx_buff[SIZE];
        unsigned char rx_wr_index,rx_rd_index,rx_counter,rx_buffer_overflow;
        unsigned char tx_wr_index,tx_rd_index,tx_counter;

        #pragma interrupt_handler spi_stc_isr:18
        void spi_stc_isr(void)
        {
          SPI_rx_buff[rx_wr_index] = SPDR; //从ISP口读出收到的字节
          if (++rx_wr_index == SIZE) rx_wr_index = 0; //放入接收缓冲区,并调整队列指针
          if (++rx_counter == SIZE)
            {
              rx_counter = 0;
              rx_buffer_overflow = 1;
            }
          if (tx_counter) //如果发送缓冲区中有待发的数据
            {
              --tx_counter;
              SPDR = SPI_tx_buff[tx_rd_index]; //发送一个字节数据,并调整指针
              if (++tx_rd_index == SIZE) tx_rd_index = 0;
            }
        }

        unsigned char getSPIchar(void)
        {
          unsigned char data;
          while (rx_counter == 0); //无接收数据,等待
          data = SPI_rx_buff[rx_rd_index]; //从接收缓冲区取出一个SPI收到的数据
          if (++rx_rd_index == SIZE) rx_rd_index = 0; //调整指针
          CLI();
          --rx_counter;
            SEI();
            return data;
        }

        void putSPIchar(char c)
        {
          while (tx_counter == SIZE);//发送缓冲区满,等待
          CLI();
          if (tx_counter || ((SPSR & 0x80) == 0))//发送缓冲区已中有待发数据
            { //或SPI正在发送数据时
              SPI_tx_buffer[tx_wr_index] = c; //将数据放入发送缓冲区排队
              if (++tx_wr_index == SIZE) tx_wr_index = 0; //调整指针
              ++tx_counter;
            }
          else
            SPDR = c; //发送缓冲区中空且SPI口空闲,直接放入SPDR由SIP口发送
        SEI();
        }

        void spi_init(void)
        {
          unsigned chat temp;
          DDRB |= 0x080; //MISO=input and MOSI,SCK,SS = output
          PORTB |= 0x80; //MISO上拉电阻有效
          SPCR = 0xD5; //SPI允许,主机模式,MSB,允许SPI中断,极性方式01,1/16系统时钟速率
          SPSR = 0x00;
          temp = SPSR;
          temp = SPDR; //清空SPI,和中断标志,使SPI空闲
        }

        void main(void)
        {
          unsigned char I;
          CLI(); //关中断
          spi_init(); //初始化SPI接口
          SEI(); //开中断
          while()
            {
              putSPIchat(i); //发送一个字节
              i++;
              getSPIchar(); //接收一个字节(第一个字节为空字节)
              ………
            }
        }

      这个典型的SPI例程比较简单,主程序中首先对ATmega128的硬件SPI进行初始化。在初始化过程中,将PORTB的MOSI、SCLK和SS 引脚作为输出,同时将MISO作为输入引脚,并打开上拉电阻。接着对SPI的寄存器进行初始化设置,并空读一次SPSR、SPDR寄存器(读SPSR后再对SPDR操作将自动清零SPI中断标志自动清零),使ISP空闲等待发送数据。

      AVR的SPI由一个16位的循环移位寄存器构成,当数据从主机方移出时,从机的数据同时也被移入,因此SPI的发送和接收在一个中断服务中完成。在 SPI中断服务程序中,先从SPDR中读一个接收的字节存入接收数据缓冲器中,再从发送数据缓冲器取出一个字节写入SPDR中,由ISP发送到从机。数据一旦写入SPDR,ISP硬件开始发送数据。下一次ISP中断时,表示发送完成,并同时收到一个数据。类似本章介绍的USART接口的使用,程序中 putSPIchar()和getSPIchar()为应用程序的底层接口函数(SPI驱动程序是SPI中断服务程序),同时也使用了两个数据缓冲器,分别构成循环队列。这种程序设计的思路,不但程序的结构性完整,同时也适当的解决了高速MCU和低速串口之间的矛盾,实现程序中任务的并行运行,提高了 MCU的运行效率。

      本例程是通过SPI批量输出、输入数据的示例,用户可以使用一片ATmega128,将其MOSI和MISO两个引脚连接起来,构成一个ISP接口自发自收的系统,对程序进行演示验证。需要注意,实际接收到的字节为上一次中断时发出的数据,即第一个收到的字节是空字节。

      读懂和了解程序的处理思想,读者可以根据需要对程序进行改动,适合实际系统的使用。如在实际应用中外接的从机是一片SPI接口的温度芯片,协议规程为:主机先要连续发送3个字节的命令,然后从机才返回一个字节的数据。那么用户程序可以先循环调用putSPIchar()函数4次,将3个字节的命令和一个字节的空数据发送到从机,然后等待一段时间,或处理一些其它的操作后,再循环调用getSPIchar()函数4次,从接收数据缓冲器中连续读取4个字节,放弃前3个空字节,第4个字节即为从机的返回数据了。

阿莫论坛20周年了!感谢大家的支持与爱护!!

一只鸟敢站在脆弱的枝条上歇脚,它依仗的不是枝条不会断,而是自己有翅膀,会飞。

出0入0汤圆

 楼主| 发表于 2011-9-8 09:37:45 | 显示全部楼层
最新的发现 似乎问题出在 这句判断上  if (tx_counter || ((SPSR & 0x80) == 0)) 如果去掉后面的((SPSR & 0x80) == 0)条件 可以正常使用。
分析后认为 是初始化时将spif即中断标志这位置0,于是这个判断永远将数放在缓冲区,不触发中断,这样因为没有一开始的一个触发 永远进不了中断 。
似乎是这样

出0入0汤圆

发表于 2011-9-8 10:36:37 | 显示全部楼层
这个05的一个初稿,后来出书时已经纠正了。
建议你购买我最新第二版的教程,里面有改过的例子。

出0入0汤圆

 楼主| 发表于 2011-9-8 10:51:19 | 显示全部楼层
回复【2楼】machao  
-----------------------------------------------------------------------

哦 我本来是买了第一版的书 但是放在学校了,现在见不着。
我想了半天 还是希望您现在能指点下,怎么改这个判断 既能保证正常的中断 还得保证不会在spdr中有数的时候误写入。
因为还得在此基础上做2401通讯 所以比较着急,希望能稍微指点 谢谢您。

出0入0汤圆

 楼主| 发表于 2011-9-9 18:18:12 | 显示全部楼层
回复【2楼】machao  
-----------------------------------------------------------------------

终于解决了
一开始仅仅是把那个去掉 但是不能连续输入数 而且字符串也不成。然后尝试自己设置一个标志位 在spdr复制时就判断为1 在中断里 tx_counter没数就判断为0  于是终于解决了这两个问题。
顺带一说 以前有个帖子说数组当参数不能正常调用 说是在ram里 但我这次是因为这个判断造成的 现在用着不错 。
对了 非常感谢  ps:节日快乐

出0入0汤圆

发表于 2011-9-10 00:08:50 | 显示全部楼层
解决了就好,下面我贴上的是正式书中的代码例子,与你解决的处理思想类似的,采用了一个位变量:SPI_free,而不使用SPSR中的状态标志位,

===============================================================================

15.3.2典型SPI底层驱动+中间层软件结构示例

    同样我们也可以采用底层中断驱动+中间层的结构,把SPI接口部分相对的独立出来。下面一段是SPI主机方式连续发送(接收)字节的例程:

#define SIZE 100
unsigned char SPI_rx_buff[SIZE];
unsigned char SPI_tx_buff[SIZE];
unsigned char rx_wr_index,rx_rd_index,rx_counter,rx_buffer_overflow;
unsigned char tx_wr_index,tx_rd_index,tx_counter;
unsigned char SPI_free;

interrupt [SPI_STC] void spi_isr(void)                // SPI 完成中断服务
{
    SPI_rx_buff[rx_wr_index] = SPDR;                        // 从SPI口读出收到的字节放入接收缓冲区
    if (tx_counter)                                // 如果发送缓冲区中有待发的数据
    {
        SPDR = SPI_tx_buff[tx_rd_index];                // 发送1字节数据,
        --tx_counter;                                // 待发送数据个数减1
        if (++tx_rd_index == SIZE) tx_rd_index = 0; // 调整发送缓冲区队列指针
    }
    else SPI_free = 1;                                // 无待发送数据,置SPI空闲

    if (++rx_wr_index == SIZE) rx_wr_index = 0;        // 调整接收缓冲区队列指针
    if (++rx_counter == SIZE)
    {
         rx_counter = 0;
         rx_buffer_overflow = 1;                                  // 接收数据溢出
    }
}

unsigned char getSPIchar(void)
{
    unsigned char data;
    while (rx_counter == 0);                        //无接收数据,等待(死循环!)
    data = SPI_rx_buff[rx_rd_index];                        //从接收缓冲区取出一个SPI收到的数据
    if (++rx_rd_index == SIZE) rx_rd_index = 0;        //调整指针
    #asm("cli")
    --rx_counter;
    #asm("sei")
    return data;
}

void putSPIchar(unsigned char c)
{
    while (tx_counter == SIZE);                        // 发送缓冲区满,等待
    #asm("cli")
    if (SPI_free)
    {
        SPDR = c;                                        // SPI口空闲,直接放入SPDR由SPI口发送
        SPI_free = 0;                                // 置SPI忙
    }
    else
    {                                                                             
        SPI_tx_buffer[tx_wr_index] = c;                // 将数据放入发送缓冲区排队
        if (++tx_wr_index == SIZE) tx_wr_index = 0;        //调整指针
        ++tx_counter;
    }
    #asm("sei")
}

void spi_init(void)
{
    unsigned char temp;
    DDRB |= 0xB0;                // MISO为输入方式,MOSI,SCK和~SS为输出方式
    PORTB |= 0x40;                // MISO上拉电阻有效
    SPCR = 0xD5;                // SPI允许,主机模式,MSB方式,允许SPI中断,极性方式01,1/16系统时钟频率
    SPSR = 0x00;
    temp = SPSR;
    temp = SPDR;                // 清除SPI中断标志位,使SPI空闲
    SPI_free = 1;         // 置SPI空闲
}

void main(void)
{
    unsigned char i;
    #asm("cli")        // 关中断
    spi_init();             // 初始化SPI接口
    #asm("sei")        // 使能中断
    while()
    {
        putSPIchar(i);                // 通过SPI发送1字节
        i++;
        getSPIchar();                        // 读取SPI接收的字节
        ………
    }
}

    在这个典型的SPI例程中,主程序中首先对SPI进行初始化,将PORTB的MOSI、SCLK和SS引脚作为输出,同时将MISO作为输入引脚,并打开上拉电阻。接着对SPI的寄存器进行初始化设置,并空读一次SPSR、SPDR寄存器(读SPSR后再对SPDR操作将自动清零SPI中断标志自动清零),使ISP空闲等待发送数据。
    在SPI总线上,主机和从机构成一个相当于16位的循环移位寄存器构成(图15-2),当数据从主机方移出时,从机的数据同时也被移入,因此AVR的SPI数据发送和接收是在一个中断服务中完成。在SPI中断服务程序中,先从SPDR中读取一个由SPI串入接收到的字节,并存入接收数据缓冲队列中,再从发送数据缓冲队列中取出一个字节写入SPDR中,由ISP发送到从机。数据一旦写入SPDR,ISP硬件开始发送数据。等到ISP中断时,表示本次发送完成,并同时收到一个数据。类似第14章中的USART接口使用,程序中putSPIchar()和getSPIchar()为应用程序的中间层接口函数(SPI底层驱动程序为SPI的中断服务程序),分别使用了两组数据缓冲器构成循环队列。这种程序设计的思路,不但程序的结构性完整,同时也比较好的解决了高速MCU和低速串口之间的矛盾,便于实现程序中任务的并行执行,提高了MCU的运行效率。
    本例程是一个通过ISP总线批量输出、输入数据的示例,用户可以使用一片ATmega16,将其MOSI和MISO两个引脚连接起来,构成一个ISP接口自发自收的系统,对程序进行演示验证。此时需要注意,本次中断中接收到的字节,实际为在上次中断中发出的数据。
    在读懂及了解程序的基本处理思想和方法后,读者可以根据实际的需要对程序进行改动,以适合应用系统的使用。如在实际应用中,外接的从机是一片兼容SPI接口的温度芯片,控制时序为:主机先要连续发送3个字节的命令,然后从机才返回一个字节的数据。那么用户程序可以循环调用putSPIchar()函数4次,连续地将3个字节的命令和一个字节的空数据发送到从机,然后从接收数据缓冲器队列中连续读取4个字节,放弃掉前3个字节,第4个字节即为从机的返回数据了。
================================================================================

简单分析一下原因:
   SPSR寄存器的最高位SPIF是SPI中断标志,它在SPI接口发送(接收)完成后才会被接口的硬件置为“1”。

   仔细想过可以知道,SPIF为0和1都不能准确的判断出SPI口是否在发送数据,因为SPI不工作时候以及SPI正在发送数据,但没有发送完成,SPIF都是“0”。而SPIF为1时,也不说明SPI是否在工作(可能不工作了,也可能正在发送第2个数据),只是说明前一个字节发送完成了,同时也收到了一个数据,可以读了。

   因此判断语句:   if (tx_counter || ((SPSR & 0x80) == 0))  后面对SPSR的条件判断显然是不正确的。

出0入0汤圆

 楼主| 发表于 2011-9-10 08:50:41 | 显示全部楼层
回复【5楼】machao  
-----------------------------------------------------------------------

恩 谢谢您的例子 我也是这么想的 光靠spif是不能正确判断的 ;
还有个体会就是 我感觉编程里最精髓的是思想 或者叫 算法 :想这种缓冲+中断的 想法 屡试不爽 spi uart 都能用。
另外再次印证您的书是我目前看到的最好的avr书 讲解明白 而且有 比较全的例子 非常喜欢

出0入0汤圆

发表于 2011-9-10 20:14:35 | 显示全部楼层
回复【6楼】rockethuojian
回复【5楼】machao   
-----------------------------------------------------------------------
恩 谢谢您的例子 我也是这么想的 光靠spif是不能正确判断的 ;
还有个体会就是 我感觉编程里最精髓的是思想 或者叫 算法 :想这种缓冲+中断的 想法 屡试不爽 spi uart 都能用。
另外再次印证您的书是我目前看到的最好的avr书 讲解明白 而且有 比较全的例子 非常喜欢
-----------------------------------------------------------------------

你涉及到了本质的东西:“算法”。

首先算法是建立在一定的基础上的,需要一定的资源支持,比如重要的就包括内存的大小。

    “缓冲+中断”实际是一种典型的结构,在大型计算机的软件涉及中经常使用。但在早期的51教科书中比较少出现(就是现在的许多书籍,如果是剪刀剪出的所谓新书,也没有),因为标准51架构的MCU内存只有128个字节,要开缓冲就比较奢侈了。
     
     但新型的MCU一般在片内集成了比较多的RAM,这就为在MCU中采用“缓冲+中断”建立了必要的基础。另外新型的MCU的工作频率都有比较大的提高,采用这样的算法结构,可以更好的发挥MCU本身的效率。

     在硬件性能的提高的同时,我们软件设计能力和方法也要提高。现在普遍的情况为,使用了好的硬件,比如32位的系统,STM8等,新的51架构的MCU,但还是采用老的51思路编写代码。更可怕的是,许多人还认识不到这个问题。

出0入0汤圆

 楼主| 发表于 2011-9-11 14:46:18 | 显示全部楼层
回复【7楼】machao  
-----------------------------------------------------------------------

在此受教了。对于这个基础倒是没有关心过,因为是直接从avr开始的,本身资源就相当丰富了。51只是在选修课上听了少许,也没有在上面实现什么功能。而avr是因为自己做的一个项目吧 从9月1号一直到现在 从uart spi lcd1602 icl7135 nrf24l01 真是痛苦不堪 现在虽然还没成功 但是想来进步非常大。

那关于这种算法的养成与学习, 是只能不断从实践中积累吗 有没有详细介绍这种东西的书籍或者了解的其他方法呢?还有就是我现在有两个大方向可以选,是在现有的avr的基础上 不断地吃透其硬件,软件(其中就指“算法”),还是像我一位研究生的师兄说的 模块了解的差不多 就转投stm32(或者就是其他更高级的芯片) 呢

出0入0汤圆

发表于 2011-9-24 12:06:01 | 显示全部楼层
受教了,谢谢马老师和大家讨论。

出0入0汤圆

发表于 2012-1-16 21:17:50 | 显示全部楼层
马老师的贴使人受益匪浅,谢谢!


另,回复7楼的,建议在AVR一些重要模块试成后向研究生那样转投新技术比较划算。

出0入0汤圆

发表于 2012-1-16 21:33:21 | 显示全部楼层
"最新的发现 似乎问题出在 这句判断上  if (tx_counter || ((SPSR & 0x80) == 0)) 如果去掉后面的((SPSR & 0x80) == 0)条件 可以正常使用。 "

logically, (SPSR & 0x80) == 0 is wrong: the SPIF = 0 does not uniquely identify a state of the spi module. SPIF = 1 does.

so if you wish to test, you will have to test SPIF to 1 or other status bits to determine if the spi module is busy.

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

本版积分规则

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

GMT+8, 2024-3-29 06:44

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

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