切换到宽版
爱科技/爱创意/爱折腾/爱极致;技术知识分享平台,点击进入新版数码之家网站
  • 3049阅读
  • 4回复

[其他]零基础学习STM8开发笔记(四) [复制链接]

上一主题 下一主题
离线fox69
 

发帖
176
M币
1174
专家
7
粉丝
29
只看楼主 倒序阅读 我要置顶 楼主  发表于: 2017-07-29
  接上篇笔记:http://bbs.mydigit.cn/read.php?tid=2176276
  之前测试各种核心板的功能没有增加任何外设,这次添加旋转编码器和8x8的LED矩阵进行试验。


  旋转编码器需要使用2个IO口,就选D4、D5。

  8x8 LED可以使用SPI的通讯方式,SPI需要时钟CLK,再加1(单线)或2(两线)个IO口,STM8带有SPI功能,其C5可复用为CLK,C6是STM8做主设备时的输出线,我们使用单线通讯,不把C7当SPI端口用,而把C7作为普通的IO口作为LED模块的使能信号。




  我们先不接8X8LED,只连接旋转编码器进行试验。

  旋转编码器的简单原理(下一段摘自网络):


  增量式旋转编码器通过内部两个光敏接受管转化其角度码盘的时序和相位关系,得到其角度码盘角度位移量增加(正方向)或减少(负方向)。A,B两点对应两个光敏接受管,A,B两点间距为 S2 ,角度码盘的光栅间距分别为S0和S1。当角度码盘以某个速度匀速转动时,那么可知输出波形图中的S0:S1:S2比值与实际图的S0:S1:S2比值相同,同理角度码盘以其他的速度匀速转动时,输出波形图中的S0:S1:S2比值与实际图的S0:S1:S2比值仍相同。如果角度码盘做变速运动,把它看成为多个运动周期(在下面定义)的组合,那么每个运动周期中输出波形图中的S0:S1:S2比值与实际图的S0:S1:S2比值仍相同。

  对我们简单的应用来说,只考虑旋转方向,不必计算转速等情况,所以关键就是判断顺时针或逆时针方向。单片机D4口连接了编码器的CLK(以下也称为A),D5口连接了编码器DT(以下称为B),当A脉冲上升沿来到时,在一个较短的时间T内,顺时针旋转时A=1,B=0;逆时针旋转时A=1, B=1。






  我们把D4口设为脉冲上升沿触发,并允许D4发出中断,因此A脉冲的上升沿将引起D4口中断,在中断处理程序中判断D4、D5口的状态就能得出旋转方向。
  EXTI_CR1_PDIS= 0x01;   这句设置D端口上升沿触发外部中断。
  PD_CR2_C24 =1;         这句允许端口D4引发中断
  为增强处理的可靠性,排除偶然的尖峰干扰,我们在处理程序中加上软件滤波,具体方法是:循环读取一个端口的电平n次,如果80%时间这个端口都是1(其余20%应该是干扰造成的误差),我们就认为端口是1,否则是0。n的次数越多,滤波的稳定性越高。但从上图能看到,判断旋转方向的窗口限制在T时间内,而T的长短与旋转的速度有关。经过多次试验,取n=50可以比较可靠地适应普通快或慢速的旋转。中断处理的代码如下:


__interrupt void GPIOD_IRQ(void)        
{
    int CountA = 0;    // A线高电平次数计数      
    int CountB = 0;    // B线高电平次数计数
    char A = 0;       // A线电平(0或1)
    char B = 0;       // B线电平(0或1)
    int change = 0;    // 变化方向1=顺时针方向;-1=逆时针方向

    for(int i = 0; i < 50; i++)   // 进行50次软件滤波,积累采集到的高电平次数
    {
       if(ROTARY_A) CountA ++;    // ROTARY_A是在外部进行的宏定义,代表A信号来自的端口,这里是B4
       if(ROTARY_B) CountB ++;   // ROTARY_B是在外部进行的宏定义,代表B信号来自的端口,这里是B5
    }

    if(CountA > 40) A = 1;  // A信号高电平超过80%,A = 1
    if(CountB > 40) B = 1;

    if(A ==1 && B == 0)  change = 1;   // 认为是顺时针
    else if(A ==1 && B == 1)  change = -1; // 认为是逆时针

    if(change != 0){
       // 确认旋转,这里省略了代码,可在此处加减LED的亮度级别
    }
}




  下面我们连接上8x8 LED试验对其驱动。

  这个8 x 8 的LED利用MAX7219驱动,支持SPI协议,利用3根数据线与主机通讯,主机通过数据线写7219内部的0至15号寄存器就可实现各种功能。
  7219的每次通讯通过Din口串行接收2个字节(CLK端口收到一个脉冲Din接收一位,共16个脉冲接收完,在这个过程中CS要保持低电平,接收完CS应由主机拉高,表示传输完成),第一个字节指明要写的7219寄存器号,第二个字节是要写的值。以两个字节一组,接收多个指令。如发送0x0F, 0x01表示对F寄存器写1,Max7219进入测试模式,所有LED全部点亮。
  其中1-8号寄存器代表8根扫描线中的一条。


  例如1号寄存器中1个字节的8位每位对应从上到下的每个LED像素点,对1号寄存器发送数据0x44,如上图对应二进制01000100中1的两个LED点就会点亮。
  STM8自带SPI协议的通讯,简化了编程,SPI通讯需要的三条数据线连接至端口C5、C6、C7,因为其中两条分别进行数据收发,而7219只用于接收指令,我们可以通过设置使其只使用C5、C6两条线,C7作为通用端口连接到7219的CS(CS并不是SPI协议中的要求的,因为7219是两字节一组传输,所以需要CS作为每组的起始和终止信号,多个LED点阵串联时CS起到选通特定LED点阵模块的作用。虽然CS是低电平有效,但不能固定地接地,最后还要一个上升沿来锁存数据)。另外7219的端口是悬浮的,没有上拉电阻,我们还需要设置C5 - C7为推挽输出模式。

  以下是SPI发送一个字节的过程:


  先向SPI_DR寄存器发送一个字节,这时SPI_SR的TXE位=0,表示缓冲器非空,SPI_DR寄存器自动将数据送入串行发送器,同时将SPI_SR的TXE位置1,表示缓冲器空,可以接受下一个字节,串行发送器得到数据后SPI_SR_BSY=1表示数据正传输中,MOSI端口依据CLK时钟逐个发送8个数据位,发送完毕后SPI_SR_BSY=0,表示发送总线空闲。

  STM8发送1个2字节命令的时序如下:






  代码如下:Address是7219寄存器号,ch是发送数据
void SPISend(char Address, char ch)
{
    CS = 1;
    __no_operation();
    CS = 0;
    __no_operation();
    SPI_DR = Address;     // 发送缓冲
    while(!SPI_SR_TXE);  // 缓冲已空?
    SPI_DR = ch;         // 发送缓冲
    while(!SPI_SR_TXE); // 缓冲已空?
    while(SPI_SR_BSY);   // SPI总线全部发送完毕?
  CS = 1;
    __no_operation();
    __no_operation();
}

  在使用8*8 LED之前先要进行一下初始化:

#define CS   PC_ODR_ODR7
void SPIInit()
{
    CS = 1;
    PC_DDR_DDR7 = 1;   // C7 为输出模式
    PC_CR1_C15 = 1;     // C5推挽输出
    PC_CR1_C16 = 1;     // C6推挽输出
    PC_CR1_C17 = 1;     // C7推挽输出
    SPI_CR1_BR = 0x1 ;  //  2M/ 4   4分频波特率,速率稍低一点可靠性更高
    SPI_CR2_SSM = 1;
    SPI_CR2_SSI = 1;
    SPI_CR1_MSTR = 1;   //STM8做SPI做主设备
    SPI_CR1_SPE = 1;     //SPI允许工作
    SPI_CR2_BDM = 1;    //只使用单线双工模式,SPI只使用C5和C6两个端口
    SPI_CR2_BDOE = 1;   //SPI只用输出模式

    SPISend(0xC, 0x1);   // 设置正常工作状态
    SPISend(0xF, 0x0);   // 设置正常工作状态
    SPISend(0x9, 0x0);   // 非解码模式
    SPISend(0xB, 0x7);   // 设置8段全用
    SPISend(0xA, 0x1);   // 设置亮度1级
}

  能点亮LED像素后,还需要把字符或数字转换成数据发送显示,我们可以用8X8LED显示两个数字,事先将0-9的数字编成LED的点阵码:
const char SymCode[] = {
    0,0x3E,0x22,0x3E,     //0
    0,0x04,0x3E,0x00,     //1
    0,0x32,0x2A,0x26,     //2
    0,0x22,0x2A,0x36,     //3
    0,0x18,0x14,0x3E,     //4
    0,0x2E,0x2A,0x3A,     //5
    0,0x3C,0x2A,0x3A,     //6
    0,0x32,0x0A,0x06,      //7
    0,0x3E,0x2A,0x3E,     //8
    0,0x2E,0x2A,0x1E,     //9
    0,0,0,0
} ;

  SymCode编码是固定的,无需加载到内存中,对于IAR的C,加上const描述符,其定义的变量就留在FLASH中。编码中,一个数字由4个字节组成,使用LED的4个列,第n个数字从4*n个字节开始。
  显示一个字符的函数:
// 显示单个字符,idx=字符序号,X起始列 最小=0,最大7
void DisAChar(int idx, int X)
{
char const * p =SymCode + 4 * idx;
    X++;
    SPISend(X++, *p++);
    SPISend(X++, *p++);
    SPISend(X++, *p++);
    SPISend(X, *p);
}
  显示一个两位数的函数:
void DisANum(int Num)
{
    char n1 = Num / 10;
    if(n1 == 0) DisAChar(10, 0);  // 高位是0不显示
    else DisAChar(n1, 0);

    n1 = Num % 10;
    DisAChar(n1, 4);
}

  还有一个要实现的功能是要把调光的亮度级别保存在EEPROM中,有种简单的方法是用__eeprom描述符可把变量直接定义在EEPROM中,不过我的编译没有通过,自己写了读写EEPROMd的函数,读函数:
unsigned char ReadEEPROM(int idx)
{
    unsigned char *p;
    p = (unsigned char *)(0x4000 + idx); // 指针p指向芯片内部的EEPROM第一个单元
    return *p;
}

  写函数:
void WriteEEPROM(int idx, unsigned char ch)
{
    unsigned char *p;
    p = (unsigned char *)(0x4000 + idx); // 指针p指向芯片内部的EEPROM第一个单元
    // 对数据EEPROM进行解锁
    do{
        FLASH_DUKR = 0xae; // 写入第一个密钥
        FLASH_DUKR = 0x56; // 写入第二个密钥
    } while((FLASH_IAPSR & 0x08) == 0); // 若解锁未成功,则重新再来
    *p = ch; // 写入第一个字节
    while((FLASH_IAPSR & 0x04) == 0); // 等待写操作成功
}

  完整的代码 main.cpp:
#include<intrinsics.h>
#include<iostm8s103f3.h>

#define ROTARY_APD_IDR_IDR4
#define ROTARY_BPD_IDR_IDR5
void WriteEEPROM(int idx, unsigned char ch);
unsigned char ReadEEPROM(int idx);
void DisANum(int Num);
void SetLight();
void SPIInit();

int LedLightTemp =0;  
int LedLight;
int TimerCount;

//初始化定时器过程
void InitTimer()
{
   TIM2_PSCR = 0;           // Timer2的预分频1
   TIM2_ARRH = 0x07;        // Timer2的自动重载寄存器高位
   TIM2_ARRL = 0xD0;        // Timer2的自动重载寄存器低位    两个合起来是 2000

   TIM2_CCER1_CC2P = 1;     // low level
   TIM2_CCER1_CC2E = 1;     // OC2

   TIM2_CCMR2_OC2PE = 1;   // 通道2预装载使能
   TIM2_CCMR2_OC2M = 0x6;   //110 PWM模式1

   TIM2_CR1_CEN = 1;

   TIM1_PSCRH = 0x03;       // Timer1的预分频寄存器值高位, 与低位合起来值是999,即Timer1的输入时钟分频1000
   TIM1_PSCRL = 0xE7;       // Timer1第预分频寄存器值低位

   TIM1_ARRH = 0x7;        // Timer1的自动重载寄存器高位
   TIM1_ARRL = 0xD0;        // Timer1的自动重载寄存器低位  2000 ,总时间1秒

   TIM1_IER_UIE = 1;       // 设置Timer1中断允许

   TIM1_CR1_ARPE = 1;       // 设置Timer1为自动重载模式
   TIM1_CR1_CEN = 1;       // 设置Timer1启动计时
}
// 主程序入口
int main()
{
   LedLight = ReadEEPROM(0);   // 从EEPROM读入亮度值
   LedLightTemp = LedLight;

   InitTimer();    // 初始化定时器
   SPIInit();   // 初始化SPI 和 8*8 LED
   SetLight();  // 设置LED亮度
   DisANum(LedLight - 2);   // 显示亮度数字

   // 初始化旋转编码器用的端口
   EXTI_CR1_PDIS = 0x01; // GPIO_D端口上升沿触发
   PD_CR2_C24 = 1;        // GPIO_D端口D4允许中断
   __enable_interrupt();   // 开系统中断  可用

   while(1)
   {
      __no_operation();
   }
}

// 写EEPROM
void WriteEEPROM(int idx, unsigned char ch)
{
    unsigned char *p;
    p = (unsigned char *)(0x4000 + idx); // 指针p指向芯片内部的EEPROM第一个单元
   // 对数据EEPROM进行解锁
    do{
        FLASH_DUKR = 0xae; // 写入第一个密钥
        FLASH_DUKR = 0x56; // 写入第二个密钥
    } while((FLASH_IAPSR & 0x08) == 0); // 若解锁未成功,则重新再来

    *p = ch; // 写入第一个字节
    while((FLASH_IAPSR & 0x04) == 0); // 等待写操作成功
}

// 读EEPROM
unsigned char ReadEEPROM(int idx)
{
    unsigned char *p;
    p = (unsigned char *)(0x4000 + idx); // 指针p指向芯片内部的EEPROM第一个单元
    return *p;
}

// 设置亮度
void SetLight()
{
    int CCR2 = LedLightTemp * LedLightTemp /5;   // 0 - 2000, 亮度变化10级
   // CCR2 = 2000 - CCR2;    // 不用这一句,PWM是低电平有效,即占空比0时保持高电平;若是高电平则取消此句注释(例如PT4115)
    TIM2_CCR2H = (CCR2 >> 8);       // 低电平保持时间
    TIM2_CCR2L = CCR2 & 0xFF;
}


#pragma vector =EXTI3_vector    //设置D端口第中断
__interrupt void GPIOD_IRQ(void)       // 旋转编码器D4中断程序,执行旋转编码器改变占空比过程
{
    int CountA = 0;
    int CountB = 0;

    char A = 0;
    char B = 0;

    int change = 0;

    for(int i = 0; i < 50; i++)   // 进行软件滤波
    {
       if(ROTARY_A) CountA ++;
       if(ROTARY_B) CountB ++;
    }

    if(CountA > 40) A = 1;
    if(CountB > 40) B = 1;

    if(A ==1 && B == 0)  change = 1;
    else if(A ==1 && B == 1) change =-1;

    if(change != 0){
        LedLightTemp += change;

        if(LedLightTemp > 100) LedLightTemp= 100;
        else if(LedLightTemp <2) LedLightTemp =2;
        SetLight();
        DisANum(LedLightTemp - 2);
    }
}

#pragma vector = TIM1_OVR_UIF_vector    //设置Timer1的中断向量地址,其值为 0xD  
__interrupt void TIM1_IRQ(void)         // Timer1 的中断程序
{
    TIM1_SR1_UIF = 0;
    TimerCount ++;
    if(TimerCount > 10)   // 每10秒保存一下亮度级别
    {
      TimerCount = 0;
      if(LedLightTemp != LedLight)
      {
          LedLight = LedLightTemp;
          WriteEEPROM(0, (unsignedchar)LedLight);
      }
    }
}

  8*8 LED 的代码写在8x8LED.cpp里(File->New->File,建立新文件,保存为“8x8LED.cpp”,然后在项目文件列表中右击项目使用Add把文件加入项目中)。

#include<intrinsics.h>
#include<iostm8s103f3.h>

void SPISend(char Address, char ch);
void DisAChar(int idx, int StartCol);
void DisANum(int Num);

#define CS   PC_ODR_ODR7

const char SymCode[] = {
    0,0x3E,0x22,0x3E,     //0
    0,0x04,0x3E,0x00,     //1
    0,0x32,0x2A,0x26,     //2
    0,0x22,0x2A,0x36,     //3
    0,0x18,0x14,0x3E,     //4
    0,0x2E,0x2A,0x3A,     //5
    0,0x3C,0x2A,0x3A,     //6
    0,0x32,0x0A,0x06,     //7
    0,0x3E,0x2A,0x3E,     //8
    0,0x2E,0x2A,0x1E,     //9
    0,0,0,0
} ;

void SPIInit()
{
    CS = 1;
    PC_DDR_DDR7 = 1;    // C7 为输出模式

    PC_CR1_C15 = 1;     // C5推挽输出
    PC_CR1_C16 = 1;     // C6推挽输出
    PC_CR1_C17 = 1;     // C7推挽输出

    SPI_CR1_BR = 0x1 ;  //  2M/ 4   4分频波特率
    SPI_CR2_SSM = 1;
    SPI_CR2_SSI = 1;
    SPI_CR1_MSTR = 1;   //SPI主模式
    SPI_CR1_SPE = 1;    //SPI允许

    SPI_CR2_BDM = 1;    //只使用单线双工模式
    SPI_CR2_BDOE = 1;   //输出模式

    SPISend(0xC, 0x1);   // 设置正常工作状态
    SPISend(0xF, 0x0);   // 设置正常工作状态
    SPISend(0x9, 0x0);   // 非解码模式
    SPISend(0xB, 0x7);   // 设置8段全用
    SPISend(0xA, 0x1);   // 设置亮度1级
}
//向Max7219发送一个命令数据
void SPISend(char Address, char ch)
{

    CS = 1;
    __no_operation();
    CS = 0;
    __no_operation();

    SPI_DR = Address;     // 发送缓冲
    while(!SPI_SR_TXE);  // 缓冲已空?
    SPI_DR = ch;         // 发送缓冲
    while(!SPI_SR_TXE);  // 缓冲已空?

    while(SPI_SR_BSY);   // SPI总线全部发送完毕?

    CS = 1;
    __no_operation();
    __no_operation();
}

// 显示单个字符,idx=字符序号,X起始列 最小=0
void DisAChar(int idx, int X)
{
char const * p =SymCode + 4 * idx;

    X++;
    SPISend(X++, *p++);
    SPISend(X++, *p++);
    SPISend(X++, *p++);
    SPISend(X, *p);

}
//显示两位数字
void DisANum(int Num)
{
    char n1 = Num / 10;
    if(n1 == 0) DisAChar(10, 0);
    else DisAChar(n1, 0);

    n1 = Num % 10;
    DisAChar(n1, 4);
}

  写EEPROM比较麻烦,我们不想每旋转一次编码器都保存亮度值,因此在main.cpp里使用了Timer1,每10秒钟判断一次是否已经保存了新亮度值,若没保存则写入EEPROM
  亮度值可由2-100变化,调整占空比的定时器值从02000(占空比0-100%),所以要将亮度对应到此定时器值。由于占空比控制亮度方式对人眼的感觉是非线性的,在低亮度范围变化比较剧烈,所以考虑变换方式也用非线性的2次函数进行校正:   int CCR2 = LedLightTemp * LedLightTemp / 5;
LedLightTemp的最大值是100,其平方值/5= 2000,而2的平方/5= 0,因小于2的值都是0,所以亮度的最低值定为2。
  本程序的PWM是低电平有效,如占空比是10%时,低电平在一个PWM周期中占10%。如果要反过来用作高电平有效,只要将上句改成:int CCR2 = 2000 - LedLightTemp * LedLightTemp / 5;








  下一步就是组装最终的电源、LED驱动和LED灯,用输出控制LED驱动进行调光。

  待续



[ 此帖被fox69在2017-07-30 17:25重新编辑 ]
本文内容包含图片或附件,获取更多资讯,请 登录 后查看;或者 注册 成为会员获得更多权限
本帖最近打赏记录:共4条打赏M币+20
离线阿甲

发帖
2449
M币
9491
专家
12
粉丝
54
只看该作者 1楼 发表于: 2017-07-29
请登录后查看
离线wlhcq
发帖
31
M币
-479
专家
0
粉丝
4
只看该作者 2楼 发表于: 2017-07-30
请登录后查看
离线钟义亭

发帖
658
M币
749
专家
3
粉丝
26
只看该作者 3楼 发表于: 2017-07-30
请登录后查看
本文内容包含图片或附件,获取更多资讯,请 登录 后查看;或者 注册 成为会员获得更多权限
离线钟义亭

发帖
658
M币
749
专家
3
粉丝
26
只看该作者 4楼 发表于: 2017-07-30
请登录后查看
离线gopher

发帖
201
M币
1854
专家
1
粉丝
12
只看该作者 5楼 发表于: 2017-07-30
请登录后查看
快速回复
限80 字节
如果您提交过一次失败了,可以用”恢复数据”来恢复帖子内容
 
上一个 下一个