前言

参考文章

TI-芯片资料

TI-编译器

用于 MSP430 的 IAR 嵌入式工作台

硬件家园-软件

我的工程

阿里云盘-资料

GITHUB

项目需求

以超低功耗MSP430单片机为主控,设计一款太阳能路灯。 采用太阳能板,锂离子电池,OLED屏幕显示,配合一些外设,在学习产品设计的过程中,熟练的掌握MSP430单片机的应用

  1. 单片机型号

MSP430F149

  1. 太阳能板

光伏发电板, 18V, 10W/15W

  1. 电池

锂离子电池 - 电池座,单节/双节

  1. 显示

OLED屏幕,显示各种信息

4个LED灯

  1. 输出

大功率LED,恒流源驱动

一路5V/1A输出接口,扩展用

  1. 输入

2个按键, 开关机,触发OLED显示,调整灯亮度等

  1. 扩展

USB转TTL接口 - BSL程序烧录

RS-485接口(自动收发功能),打印信息,上位机监控或扩展用

预留GPIO口扩展

  1. 其他方面

低功耗设计

白天自动关闭大功率LED

晚上自动开启大功率LED

DEBUG调试接口

需求调整

  1. 充电电流由4A调整为3A

原因:太阳能电板选择10W/15W的,最大3A充电合适,充电电路成本也低些,同时可以满足学习要求

  1. 升压输出,由12V/1A调整为5V/1A

原因:升压至12V/1A,方案不通用,成本较高, 调整为5V/1A,选择通用方案,可以给其它实战板供电,同时可以满足学习要求

数据手册阅读

通过数据手册可知:

低电源电压范围:1.8V 至 3.6V

60KB + 256B 闪存, 2KB RAM

5 种省电模式

超低功耗: – 激活模式:280μA(在 1MHz 频率和 2.2V 电压 条件下) – 待机模式:1.6μA – 关闭模式(RAM 保持):0.1μA

可在不到 6μs 的时间内从待机模式唤醒

具有 7 个捕捉/比较及影子寄存器的 16 位 Timer_B • 具有 3 个捕捉/比较寄存器的 16 位 Timer_A

MSP430F14x 和 MSP430F14x1 器件上有两个 USART(USART0、USART1)

新建工程

上面两个版本通用

  • 新工程文件夹

新建一个文件夹,然后在里面新建两个文件夹分别是 appuser

app - 放置GPIO、UART、ADC等外设应用程序

user - 放置main,public等文件

  • 新建工程
  • 整理文件 - 将mian.c文件放入user文件夹

在编译器右键 main.c 选择 【remove】,然后在文件夹里重新把main.c 移到【user文件夹】里,然后在编译器里右键添加回去即可

编译一下

  • 设置

新建模板

main.h
cpp
#ifndef __MAIN_H
#define __MAIN_H
#include <msp430x14x.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include "public.h"
#include "sys_init.h"
#include "system.h"

#endif
main.c
cpp
/***************************************************************************
* File          : main.c
* Author        : Luckys.
* Date          : 2023-06-04
* description   : 主函数 
****************************************************************************/
#include <main.h>

int main( void )
{
  Hardware_Init.vSys_Init(); // 系统初始化
  // 主循环
  while(1)
  {
    // 主要任务
    if (System_Run == System.ucSystem_Status)
    {
      System.vRun();  // 系统运行
    }
    else
    {
      System.vStandBy();  // 系统待机
    }
  }
}
public.h
cpp
#ifndef __PUBLIC_H
#define __PUBLIC_H
#include <main.h>

// 数据类型重定义
typedef signed char        sint8_t;
typedef signed short int   sint16_t;
typedef signed long        sint32_t;

typedef unsigned char      uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned long      uint32_t;

// 定义枚举类型
typedef enum
{
  TRUE = (uint8_t)1,
  FALSE = (uint8_t)0,
}BOOL_t;

typedef struct
{
  void (*vDelay_ms)(uint16_t); // ms延时函数
}Public_t;

extern Public_t Public;

#endif
public.c
cpp
/***************************************************************************
* File          : public.c
* Author        : Luckys.
* Date          : 2023-06-04
* description   : 通用
****************************************************************************/

#include <main.h>


/*====================================静态函数声明区 BEGIN====================================*/
static void vDelay_ms(uint16_t);  // ms延时函数
/*====================================静态函数声明区   END====================================*/



Public_t Public = 
{
  vDelay_ms,
};


/*
* @function     : vDelay_ms
* @param        : ms -> 需要延时的时间
* @retval       : None
* @brief        : ms延时
*/
static void vDelay_ms(uint16_t ms)
{
  uint16_t i,j;
  
  for (i = 0; i < ms; i++)
  {
    for (j = 0; j < 1590; j++); // 示波器测量时间校准即可(使用LED翻转)
  }  
}
system.h
cpp
#ifndef __SYSTEM_H
#define __SYSTEM_H
#include <main.h>

// 系统状态宏定义
#define System_StandBy  (uint8_t)0
#define System_Run      (uint8_t)1
// 错误宏定义
#define ErrorCode_NoError (uint8_t)0
#define ErrorCode_CLK     (uint8_t)1

typedef struct
{
  uint8_t ucSystem_Status;  // 系统状态
  uint8_t ucSystem_ErrorCode; // 系统错误码
  
  void (*vRun)(void); // 系统运行
  void (*vStandBy)(void); // 系统待机
  void (*vError_Handle)(void);  // 错误处理
}System_t;


extern System_t System;

#endif
system.c
cpp
/***************************************************************************
 * File          : system.c
 * Author        : Luckys.
 * Date          : 2023-06-05
 * description   : 系统运行函数
****************************************************************************/

#include <main.h>

/*====================================静态函数声明区 BEGIN====================================*/  
static void vRun(void); // 系统运行
static void vStandBy(void); // 系统待机
static void vError_Handle(void);  // 错误处理
/*====================================静态函数声明区   END====================================*/

System_t System = 
{
  System_Run,
  ErrorCode_NoError,
  
  vRun,
  vStandBy,
  vError_Handle,
};

/*
* @function     : vRun
* @param        : None
* @retval       : None
* @brief        : 系统运行
*/
static void vRun(void)
{
  
}

/*
* @function     : vStandBy
* @param        : None
* @retval       : None
* @brief        : 系统待机
*/
static void vStandBy(void)
{
  
}

/*
* @function     : vError_Handle
* @param        : None
* @retval       : None
* @brief        : 错误处理
*/
static void vError_Handle(void)
{
  
}
sys_init.h
cpp
#ifndef __SYS_INIT_H
#define __SYS_INIT_H
#include <main.h>

typedef struct
{
  void (*vCLK_Init)(void);  // 时钟初始化
  void (*vGPIO_Init)(void); // 通用输入输出端口初始化
  void (*vIE_Init)(void); // 中断初始化
  void (*vPower_On_Indication)(void); // 上电指示
  void (*vSys_Init)(void); // 系统初始化
}Hardware_Init_t;

extern Hardware_Init_t Hardware_Init;

#endif
sys_init.c
cpp
/***************************************************************************
* File          : sys_init.c
* Author        : Luckys.
* Date          : 2023-06-04
* description   : 系统初始化  
****************************************************************************/

#include <main.h>

/*====================================静态函数声明区 BEGIN====================================*/
static void vCLK_Init(void);  // 时钟初始化
static void vGPIO_Init(void); // 通用输入输出端口初始化
static void vIE_Init(void); // 中断初始化
static void vPower_On_Indication(void); // 上电指示
static void vSys_Init(void); // 系统初始化
/*====================================静态函数声明区   END====================================*/

Hardware_Init_t Hardware_Init = 
{
  vCLK_Init,
  vGPIO_Init,
  vIE_Init,
  vPower_On_Indication,
  vSys_Init,
};

/*
* @function     : vCLK_Init
* @param        : None
* @retval       : None
* @brief        : 时钟初始化
*/
static void vCLK_Init(void)
{
  
}

/*
* @function     : vGPIO_Init
* @param        : None
* @retval       : None
* @brief        : 通用输入输出端口初始化
*/
static void vGPIO_Init(void)
{
  
}

/*
* @function     : vIE_Init
* @param        : None
* @retval       : None
* @brief        : 中断初始化
*/
static void vIE_Init(void)
{
  __enable_interrupt(); // 使能全局中断
}

/*
* @function     : vPower_On_Indication
* @param        : None
* @retval       : None
* @brief        : 上电指示
*/
static void vPower_On_Indication(void)
{
  
}

/*
* @function     : Sys_Init
* @param        : None
* @retval       : None
* @brief        : 系统初始化
*/
static void vSys_Init(void)
{
  WDTCTL = WDTPW + WDTHOLD; // 停止看门狗定时器以防止超时复位
}

时钟详解

  • 时钟源

LFXT1CLK:低频/高频振荡器,可用于低频32768Hz

XT2CLK:可选的高频振荡器

DCOCLK:具有RC型特性的内部数控振荡器(DCO)

  • 时钟信号

ACLK:辅助时钟 — 主要给低速外设使用

MCLK:主时钟 — CPU使用

SMCLK:副主时钟 — 主要给高速外设使用

上电复位后,MCU 和 SMCK 默认使用 DCOCLK时钟,频率为 800KHz,ACLK使用 LFXT1,频率为 32.768KHz

寄存器

  • 寄存器下面那行是默认值

  • 寄存器图空白处

该位可能被其他模块使用。 请参阅特定于设备的数据表。一般下面有注解

  • 时钟配置程序编写

步骤:

  1. 开启晶振
  2. 清除 OFIFG 标志
  3. 等待至少 50 us
  4. 测试 OFIFG,并重复步骤 1-4,直到 OFIFG 保持清零

sys_init.c
cpp
/*
* @function     : vCLK_Init
* @param        : None
* @retval       : None
* @brief        : 时钟初始化
*/
static void vCLK_Init(void)
{
  uint8_t i;
  uint8_t Error_Cnt = 0;
  
  // MCLK  -> 主系统时钟,CPU使用
  // SMCLK -> 子系统时钟,主要给高速外设使用
  // ACLK  -> 辅助系统,主要给低速外设使用
  
  // 系统复位后,时钟默认情况如下:
  // MCLK与SMCLK默认使用DCO,频率为800kHz;
  // ACLK使用LFXT1,频率为32.768KH;
  
  // 运行时钟
  // MCLK = SMCLK = XT2 = 8MHz
  // ACLK = LFXT1 = 32.768KHZ
  BCSCTL1 &= ~XT2OFF;  // 打开XT2高频晶体振荡器
  do
  {
    IFG1 &= ~OFIFG;     // 清除振荡器故障标志位
    for (i = 0; i < 100; i++);  // 至少等待50个周期--50us

    if(Error_Cnt++ >= 100)        // 超时退出
    {
      break;
    }
  }
  while (IFG1&OFIFG);   // 检测荡器故障标志位,为0时退出循环
  
  if (Error_Cnt < 100)  // 说明晶体正常,上面语句是正常退出
  {
    BCSCTL2 |= SELM_2 + SELS;   // MCLK和SMCLK选择XT2(可以使用|)
  }
  else
  {
    BCSCTL1 = XT2OFF;   // 关闭XT2高频晶体振荡器
    // DCO设置为最大值(全部置1),8MHz
    DCOCTL = DCO0 + DCO1 + DCO2;
    BCSCTL1 = RSEL0 + RSEL1 + RSEL2;
    // 系统错误处理
    System.ucSystem_ErrorCode = ErrorCode_CLK;
    System.vError_Handle();
  }
}

中断问题

430是默认关闭总中断的,而且中断优先级是固定,不能嵌套,如果想嵌套则需要在中断函数里打开总中断

当进入中断服务程序时,只要不在中断服务程序中再次开中断,则总中断是关闭的,此时后面到来的中断不管是比当前中断的优先级高还是低都不执行

若在中断中开了总中断,后来的中断同时有多个,则会按优先级来执行,即中断优先级只有在多个中断同时到来才起作用!中断服务不执行抢先原则

还有就是那些需要手动清除中断标志位的中断需要先清除中断标志位再开启总中断否则会造成有相同的中断不断嵌入,而导致堆栈溢出引起复位

cpp
#pragma vector = ADC12_VECTOR
__interrupt void ADC12_ISR(void)
{
    _EINT();
	……
}

GPIO手册阅读

  • 讲解

MSP430设备最多实现了6个数字I/O端口,P1-P6,每个端口都有 8 个输入输出引脚,每个输入/输出销都可单独配置为输入或输出方向,并且每个输入/输出线都可以单独读取或写入到

端口P1和P2具有 中断 功能。P1和P2 I/O线的每个中断可以单独启用并配置为提供在输入信号的上升边缘或下降边缘上的中断。 所有 P1 I/O 线都来自一个中断向量,所有 P2 I/O 线都来自不同的单个中断向量

数字输入/输出功能包括:

  1. 可独立编程的独立 I/O
  2. 任何输入或输出组合
  3. 可单独配置的 P1 和 P2 中断
  4. 独立的输入和输出数据寄存器
寄存器 名称 用途
PxIN 输入 Bit = 0: The input is low
Bit = 1: The input is high
PxOUT 输出 Bit = 0: The output is low
Bit = 1: The output is high
PxDIR 方向 Bit = 0: The port pin is switched to input direction
Bit = 1: The port pin is switched to output direction
PxSEL 复用 Bit = 0: I/O Function is selected for the pin
Bit = 1: Peripheral module function is selected for the pin
PxIFG 中断 Bit = 0: No interrupt is pending
Bit = 1: An interrupt is pending
PxIES 中断沿选择 Bit = 0: The PxIFGx flag is set with a low-to-high transition
Bit = 1: The PxIFGx flag is set with a high-to-low transition
PxIE 中断使能 Bit = 0: The interrupt is disabled
Bit = 1: The interrupt is enabled

MSP430管脚要么是GPIO,要么是复用,而且只有一个复用引脚

寄存器 名称 用途
P1IFG, P2IFG 中断标志位 Bit = 0: The input is low
Bit = 1: The input is high

未使用的 i o 引脚应配置为 i o 功能输出方向,并在印刷电路板上保持未连接以降低功耗 px 输出位的值无关紧要,因为该引脚未连接,请参阅系统章节重置终止未使用引脚的中断和操作模式

  • 程序编写
cpp
/*
* @function     : vGPIO_Init
* @param        : None
* @retval       : None
* @brief        : 通用输入输出端口初始化
*/
static void vGPIO_Init(void)
{
  // GPIO配置
  // 引脚复用PxSEL ->  0 - GPIO / 1 - 复用(外部中断禁用)
  // 方向PxDIR -> 0 - 输入 / 1 - 输出
  // 输入PxIN / 输出PxOUT
  
  // P1与P2中断
  // 标志位   P1IFG / P2IFG - 中断标志位(必须软件清除)
  // 触发选择 P1IES / P2IES - 0 - 上升沿   1 - 下降沿(写入时会影响PxIFG)
  // 中断启用 P1IE / P2IE 
  
  // 根据外设进行配置
  P1DIR = 0xC6;P1OUT = 0x80;
  P2DIR = 0xFF;P2OUT = 0x00;
  P3DIR = 0x7F;P3OUT = 0xC0;
  P4DIR = 0xFF;P4OUT = 0x00;
  P5DIR = 0xFF;P5OUT = 0x61;
  P6DIR = 0x9F;P6OUT = 0x70;  
}

LED

  • 硬件连接

  • 程序编写
cpp
/*
* @function     : vRun
* @param        : None
* @retval       : None
* @brief        : 系统运行
*/
static void vRun(void)
{
  // LED1间隔500ms闪烁
  Led.vLed_ON(LED1);
  Public.vDelay_ms(500);
  Led.vLed_OFF(LED1);
  Public.vDelay_ms(500);  
}

/*
* @function     : Sys_Init
* @param        : None
* @retval       : None
* @brief        : 系统初始化
*/
static void vSys_Init(void)
{
  WDTCTL = WDTPW + WDTHOLD; // 停止看门狗定时器以防止超时复位
  Hardware_Init.vCLK_Init();    // 时钟初始化
  Hardware_Init.vGPIO_Init();   // GPIO初始化
}
led.h
cpp
#ifndef __LED_H
#define __LED_H
#include <main.h>


// 定义枚举类型
typedef enum
{
  LED1 = (uint8_t)0x01,
  LED2 = (uint8_t)0x02,
  LED3 = (uint8_t)0x03,
  LED4 = (uint8_t)0x04,
}Led_Num_t;


typedef struct
{
  void (*vLed_ON)(Led_Num_t);     // 打开
  void (*vLed_OFF)(Led_Num_t);    // 关闭
  void (*vLed_Flip)(Led_Num_t);   // 翻转
}Led_t;

extern Led_t Led;

#endif
led.c
cpp
/***************************************************************************
 * File          : led.c
 * Author        : Luckys.
 * Date          : 2023-06-05
 * description   : LED
****************************************************************************/

#include <main.h>


/*====================================static function declaration area BEGIN====================================*/
static void vLed_ON(Led_Num_t);     // 打开
static void vLed_OFF(Led_Num_t);    // 关闭
static void vLed_Flip(Led_Num_t);   // 翻转

/*====================================static function declaration area   END====================================*/

Led_t Led = 
{
  vLed_ON,
  vLed_OFF,
  vLed_Flip,
};

/*
* @function     : vLed_ON
* @param        : Led_Num ->LED编号
* @retval       : None
* @brief        : LED打开
*/
static void vLed_ON(Led_Num_t Led_Num)
{
  switch(Led_Num)
  {
  case LED1:
    {
      P5OUT |= BIT1;    // P51置1
      break;
    }
  case LED2:
    {
      P5OUT |= BIT2;    // P52置1
      break;
    }
  case LED3:
    {
      P5OUT |= BIT3;    // P53置1
      break;
    }
  case LED4:
    {
      P5OUT |= BIT4;    // P54置1
      break;
    }
  default:
    {
      P5OUT &= (~(BIT1 + BIT2 + BIT3 + BIT4));
      break;    
    }
  }
}

/*
* @function     : vLed_OFF
* @param        : Led_Num ->LED编号
* @retval       : None
* @brief        : LED关闭
*/
static void vLed_OFF(Led_Num_t Led_Num)
{
  switch(Led_Num)
  {
  case LED1:
    {
      P5OUT &= (~BIT1);    // P51置0
      break;
    }
  case LED2:
    {
      P5OUT &= (~BIT2);    // P52置0
      break;
    }
  case LED3:
    {
      P5OUT &= (~BIT3);    // P53置0
      break;
    }
  case LED4:
    {
      P5OUT &= (~BIT4);    // P54置0
      break;
    }
  default:
    {
      P5OUT &= (~(BIT1 + BIT2 + BIT3 + BIT4));
      break;    
    }
  }
}

/*
* @function     : vLed_Flip
* @param        : Led_Num ->LED编号
* @retval       : None
* @brief        : LED翻转
*/
static void vLed_Flip(Led_Num_t Led_Num)
{
  switch(Led_Num)
  {
  case LED1:
    {
      P5OUT ^= BIT1;    // P51置反
      break;
    }
  case LED2:
    {
      P5OUT ^= BIT2;    // P52置反
      break;
    }
  case LED3:
    {
      P5OUT ^= BIT3;    // P53置反
      break;
    }
  case LED4:
    {
      P5OUT ^= BIT4;    // P54置反
      break;
    }
  default:
    {
      P5OUT &= (~(BIT1 + BIT2 + BIT3 + BIT4));
      break;    
    }
  }
}

定时器A+状态机

有3个TimerA,16位

定时器时钟可以来自 ACLK、SMCLK 或通过 TACLK 从外部获取或 INCLK,使用 TASSELx 位选择时钟源。 被选中的
时钟源可以直接传递给定时器或除以 2、4 或 8

MSP430的定时器模式分为 比较模式捕获模式

比较模式就是定时中断、计时等一些常规的功能

捕获模式是PWM输出,捕获外部信号,用于测量时间和脉冲数量等

MSP430默认为比较模式

  • 常用寄存器
寄存器 作用
TAxR 计数寄存器,记录当前定时器的计数值
TACCR0 记录一个比较值,类似STM32的重装载值ARR
TACCRn 通道n的比较值,这个值可以理解为PWM中高低电平的分割点,类似STM32里的pluse
CCTLx CCTL0 控制 TimerA 的捕获/比较通道 0 的工作模式和中断设置;CCTL1 控制捕获/比较通道 1,CCTL2 控制捕获/比较通道 2

TA1有两个定时器中断向量,用的是不同的比较匹配通道,也就是说一个定时器有两个中断,这里我们使用的中断向量是 TIMER1_A0_VECTOR,所以要配置 TACCR0 和TACCTL0

寄存器

模式

  • 上升模式

会产生两种中断:比较中断,溢出中断

  • 连续运行模式

  • 上/下模式

中断

两个中断向量与16位Timer_A模块相关联:

  1. TACCR0 CCIFG的TACCR0中断向量
  2. AIV中断向量和TAIFG的所有其他CCIFG标志

TACCR0CCIFG 标志具有 最高 的Timer_A中断优先级,并且具有一个专用的中断向量

  • 程序编写

定时器初始化的话首先选择时钟源,这里选择辅助时钟SMCLK,8分频后1us,然后定时5ms,打开中断,编写中断函数(注意格式)

中断向量可以在头文件最后那找

cpp
#pragma vector = TIMERA0_VECTOR // 定义一下中断向量
__interrupt void TimerA_isr(void)       // __interrupt 是中断关键字 后面是自定义函数名称
{
  ....
}
cpp
/*
* @function     : vRun
* @param        : None
* @retval       : None
* @brief        : 系统运行
*/
static void vRun(void)
{
  // 流水灯--状态机+定时器A
  if (TRUE == STA_Machine.ucSTA_Machine_Switch_Flag)
  {
    STA_Machine.ucSTA_Machine_Switch_Flag = FALSE;
    
    switch(STA_Machine.ucSTA_Machine_Status)
    {
    case STA1:
      {
        STA_Machine.vFun_STA1();
        STA_Machine.ucSTA_Machine_Status = STA2;
        break;
      }
    case STA2:
      {
        STA_Machine.vFun_STA2();
        STA_Machine.ucSTA_Machine_Status = STA3;        
        break;
      }
    case STA3:
      {
        STA_Machine.vFun_STA3();
        STA_Machine.ucSTA_Machine_Status = STA4;        
        break;
      }
    case STA4:
      {
        STA_Machine.vFun_STA4();
        STA_Machine.ucSTA_Machine_Status = STA5;        
        break;
      }
    case STA5:
      {
        STA_Machine.vFun_STA5();
        STA_Machine.ucSTA_Machine_Status = STA1;
        break;
      }
    default:
      {
        STA_Machine.ucSTA_Machine_Status = STA1;
        break;   
      }   
    }
  }
}

/*
* @function     : vIE_Init
* @param        : None
* @retval       : None
* @brief        : 中断初始化
*/
static void vIE_Init(void)
{
  __enable_interrupt(); // 使能全局中断
}

/*
* @function     : Sys_Init
* @param        : None
* @retval       : None
* @brief        : 系统初始化
*/
static void vSys_Init(void)
{
  WDTCTL = WDTPW + WDTHOLD; // 停止看门狗定时器以防止超时复位
  Hardware_Init.vCLK_Init();    // 时钟初始化
  Hardware_Init.vGPIO_Init();   // GPIO初始化
  TimerA.vTimerA_Init();
  Hardware_Init.vIE_Init();     // 中断初始化
}
timerA.h
cpp
#ifndef __TIMERA_H
#define __TIMERA_H
#include <main.h>


// 定义枚举类型
typedef enum
{
  TimerA_50ms = (uint16_t)10,
  TimerA_100ms = (uint16_t)20,
  TimerA_500ms = (uint16_t)100,
  TimerA_1s = (uint16_t)200,
}TimerA_Value_t;

typedef struct
{
  uint16_t volatile usMCU_Run_Timer;    // 系统运行时间
  
  void (*vTimerA_Init)(void);   // 定时器初始化
}TimerA_t;

extern TimerA_t TimerA;

#endif
timerA.c
cpp
/***************************************************************************
 * File          : timerA.c
 * Author        : Luckys.
 * Date          : 2023-06-05
 * description   : 定时器A 
****************************************************************************/

#include <main.h>

/*====================================static function declaration area BEGIN====================================*/
static void vTimerA_Init(void);
/*====================================static function declaration area   END====================================*/

TimerA_t TimerA = 
{
  0,
  vTimerA_Init,
};

/*
* @function     : vTimerA_Init
* @param        : None
* @retval       : None
* @brief        : 定时器A初始化
*/
static void vTimerA_Init(void)
{
  // 设置控制寄存器 -- SMCLK + 8分频 + 工作模式为向上计数
  TACTL = TASSEL_2 + ID_3 + MC_1;
  // 设置捕获/比较寄存器 -- 设置周期为5ms
  // 定时计算:
  // 定时器A时钟 = SMCLK/8 = 8000000Hz / 8 = 1000 000Hz
  // Fre = 1 / 0.005s = 200Hz
  // 1000 000Hz / 200Hz = 5000 - 1
  CCR0 = 5000 - 1;
  // 设置捕获/比较控制寄存器 - 默认比较模式, 开启CCIE中断
  CCTL0 = CCIE;
}

#pragma vector = TIMERA0_VECTOR // 定义一下中断向量
__interrupt void TimerA_isr(void)       // __interrupt 是中断关键字
{
  if (++TimerA.usMCU_Run_Timer >= TimerA_100ms)
  {
    TimerA.usMCU_Run_Timer = 0;
    // 置位流水灯状态机标志位
    STA_Machine.ucSTA_Machine_Switch_Flag = TRUE;
  }
}
sta_machine.h
cpp
#ifndef __STA_MACHINE_H
#define __STA_MACHINE_H
#include <main.h>

// 定义枚举类型
typedef enum
{
  STA1 = (uint8_t)0x01,
  STA2 = (uint8_t)0x02,
  STA3 = (uint8_t)0x03,
  STA4 = (uint8_t)0x04,
  STA5 = (uint8_t)0x05,
}STA_Machine_Status_t;

typedef struct
{
  STA_Machine_Status_t ucSTA_Machine_Status;    // 状态机状态
  uint8_t ucSTA_Machine_Switch_Flag;    // 状态机切换状态标志位
  
  void (*vFun_STA1)(void);
  void (*vFun_STA2)(void);
  void (*vFun_STA3)(void);
  void (*vFun_STA4)(void);
  void (*vFun_STA5)(void);
}STA_Machine_t;


extern STA_Machine_t STA_Machine;

#endif
sta_machine.c
cpp
/***************************************************************************
 * File          : sta_machine.c
 * Author        : Luckys.
 * Date          : 2023-06-05
 * description   : 状态机   
****************************************************************************/

#include <main.h>

/*====================================static function declaration area BEGIN====================================*/
static void vFun_STA1(void);
static void vFun_STA2(void);
static void vFun_STA3(void);
static void vFun_STA4(void);
static void vFun_STA5(void);
/*====================================static function declaration area   END====================================*/

STA_Machine_t STA_Machine = 
{
  STA1,
  FALSE,
  
  vFun_STA1,
  vFun_STA2,
  vFun_STA3,
  vFun_STA4,
  vFun_STA5,
};


/*
* @function     : vFun_STA1~5
* @param        : None
* @retval       : None
* @brief        : 状态函数
*/
static void vFun_STA1(void)
{
  Led.vLed_ON(LED1);
  Led.vLed_OFF(LED2);
  Led.vLed_OFF(LED3);
  Led.vLed_OFF(LED4);
}

static void vFun_STA2(void)
{
  Led.vLed_OFF(LED1);
  Led.vLed_ON(LED2);
  Led.vLed_OFF(LED3);
  Led.vLed_OFF(LED4);
}

static void vFun_STA3(void)
{
  Led.vLed_OFF(LED1);
  Led.vLed_OFF(LED2);
  Led.vLed_ON(LED3);
  Led.vLed_OFF(LED4);
}

static void vFun_STA4(void)
{
  Led.vLed_OFF(LED1);
  Led.vLed_OFF(LED2);
  Led.vLed_OFF(LED3);
  Led.vLed_ON(LED4);
}

static void vFun_STA5(void)
{
  Led.vLed_OFF(LED1);
  Led.vLed_OFF(LED2);
  Led.vLed_OFF(LED3);
  Led.vLed_OFF(LED4);
}

看门狗

第一次上电,WDT模块自动配置在看门狗中使用 DCOCLK 的初始 32毫秒 复位间隔模式。 用户必须在初始复位间隔到期之前设置或停止 WDT。

看门狗是 16位,受密码保护的,任何读写访问都必须使用字指令,并且写访问必须在上字节中包含写密码 05Ah。任何具有上字节中除05Ah以外的任何值的WDTCTL写入都是安全密钥冲突,并触发PUC系统重置。任何读取WDTCTL都会在上字节中读取 069h

WDT间隔应与 WDTCNTCL = 1 一起在一条指令中更改,以避免意外的立即PUC或中断。在更改时钟源之前,应停止WDT,以避免可能出现错误的时间间隔。

寄存器

  • 程序编写

WDT_ARST_250 是一个宏,它的替换体是 (WDTPW+WDTCNTCL+WDTSSEL+WDTIS0),这个已经是算好的时间在ACK 32K下是250ms

cpp
/*
* @function     : Sys_Init
* @param        : None
* @retval       : None
* @brief        : 系统初始化
*/
static void vSys_Init(void)
{
  WatchDog.vWatchDog_Init();    // 看门狗初始化
  Hardware_Init.vCLK_Init();    // 时钟初始化
  Hardware_Init.vGPIO_Init();   // GPIO初始化
  TimerA.vTimerA_Init();
  Hardware_Init.vIE_Init();     // 中断初始化
}

#pragma vector = TIMERA0_VECTOR // 定义一下中断向量
__interrupt void TimerA_isr(void)       // __interrupt 是中断关键字
{
  WatchDog.vWatchDog_Feed();    //喂狗
  
  if (++TimerA.usMCU_Run_Timer >= TimerA_100ms)
  {
    TimerA.usMCU_Run_Timer = 0;
    // 置位流水灯状态机标志位
    STA_Machine.ucSTA_Machine_Switch_Flag = TRUE;
  }
}
watch_dog.h
cpp
#ifndef __WATCH_DOG_H
#define __WATCH_DOG_H
#include <main.h>


typedef struct
{
  void (*vWatchDog_Init)(void);   // 看门狗初始化
  void (*vWatchDog_Feed)(void); // 喂狗
}WatchDog_t;


extern WatchDog_t WatchDog;

#endif
watch_dog.c
cpp
/***************************************************************************
 * File          : watch_dog.c
 * Author        : Luckys.
 * Date          : 2023-06-06
 * description   : 看门狗 
****************************************************************************/

#include <main.h>


/*====================================static function declaration area BEGIN====================================*/
static void vWatchDog_Init(void);
static void vWatchDog_Feed(void);
/*====================================static function declaration area   END====================================*/

WatchDog_t WatchDog = 
{
  vWatchDog_Init,
  vWatchDog_Feed,
};

/*
* @function     : vWatchDog_Init
* @param        : None
* @retval       : None
* @brief        : 看门狗初始化
*/
static void vWatchDog_Init(void)
{
  WDTCTL = WDTPW + WDTHOLD;     // 切换时钟源前先关闭看门狗
  // 时钟源选择ACLK,看门狗时间为250ms
  WDTCTL = WDT_ARST_250;
    
}

/*
* @function     : vWatchDog_Feed
* @param        : None
* @retval       : None
* @brief        : 喂狗
*/
static void vWatchDog_Feed(void)
{
  WDTCTL = WDT_ARST_250;
}

USART

USART通过 PUC 或设置 SWRST位 来重置,SWRST 默认已经是1了看寄存器可以知道

一般计算公式很复杂,所以直接看推荐值

寄存器

  • 硬件连接

  • 程序编写
cpp
/*
* @function     : Sys_Init
* @param        : None
* @retval       : None
* @brief        : 系统初始化
*/
static void vSys_Init(void)
{
  WatchDog.vWatchDog_Init();    // 看门狗初始化
  Hardware_Init.vCLK_Init();    // 时钟初始化
  Hardware_Init.vGPIO_Init();   // GPIO初始化
  TimerA.vTimerA_Init();
  Hardware_Init.vIE_Init();     // 中断初始化
  USART1.vUSART1_Init();        // 串口1初始化
  
  USART1.vUSART1_SendString("系统初始化完成\r\n");
  printf("PI = %.1f\r\n",3.14);
}
usart1.h
cpp
#ifndef __USART1_H
#define __USART1_H
#include <main.h>

typedef struct
{
  void (*vUSART1_Init)(void);   // 串口1初始化
  void (*vUSART1_SendArray)(uint8_t*, uint16_t);        // 发送数组
  void (*vUSART1_SendString)(uint8_t*); // 发送字符串
  
}USART1_t;


extern USART1_t USART1;

#endif
usart1.c
cpp
/***************************************************************************
 * File          : usart1.c
 * Author        : Luckys.
 * Date          : 2023-06-06
 * description   : 串口1  
****************************************************************************/

#include <main.h>

/*====================================static function declaration area BEGIN====================================*/
static void vUSART1_Init(void);   // 串口1初始化
static void vUSART1_SendArray(uint8_t*, uint16_t);        // 发送数组
static void vUSART1_SendString(uint8_t*); // 发送字符串
static void vUSART1_SendData(uint8_t);  // 发送字符
/*====================================static function declaration area   END====================================*/

USART1_t USART1 = 
{
  vUSART1_Init,
  vUSART1_SendArray,
  vUSART1_SendString,
};

/*
* @function     : vUSART1_Init
* @param        : None
* @retval       : None
* @brief        : 串口1初始化
*/
static void vUSART1_Init(void)
{
  P3SEL |= BIT6 + BIT7; // 开启复用引脚功能P36(TX) P3(RX)
  // 参数设置
  UCTL1 |= SWRST;       // 模块处于复位状态(默认已经置1,此行可要可不要)
  ME2 |= UTXE1 + URXE1; // 使能串口1发送和接收
  UCTL1 |= CHAR;        // 数据长度选择8位
  // 波特率设置 -- 手册查询可知:9600pcs 对应 00 03 4A
  UTCTL1 |= SSEL0;      // 配置ACLK
  UBR11 = 0x00; // UxBR1
  UBR01 = 0x03; // UxBR0
  UMCTL1 = 0x4A;        // UxMCTL
  UCTL1 &= ~SWRST;      // 把SWRST置0,启动模块
  // 如果需要中断可以设置
//  IE2 |= UTXIE1 + URXIE1;
}

/*
* @function     : vUSART1_SendArray
* @param        : p_Arr --> 要发送的数组 Arr_len --> 数据的长度
* @retval       : None
* @brief        : 发送数组
*/
static void vUSART1_SendArray(uint8_t* p_Arr, uint16_t Arr_len)
{
  uint16_t i;
  
  for (i = 0; i < Arr_len; i++)
  {
    vUSART1_SendData(*(p_Arr + i));
  }
}

/*
* @function     : vUSART1_SendString
* @param        : p_Str --> 要发送的字符串
* @retval       : None
* @brief        : 发送数组
*/
static void vUSART1_SendString(uint8_t* p_Str)
{
  while (*p_Str)
  {
    vUSART1_SendData(*(p_Str++));
  }
}

/*
* @function     : vUSART1_SendData
* @param        : ch --> 要发送的字符数据
* @retval       : None
* @brief        : 发送字符
*/
static void vUSART1_SendData(uint8_t ch)
{
  while (!(IFG2 & UTXIFG1));    // 等待为空才能发送
  TXBUF1 = ch;
}

// putchar函数 重定向
extern int putchar(int c)
{
  vUSART1_SendData((uint8_t)c);
  
  return c;
}

按键

  • 硬件连接

中断+延时

单击

cpp
/*
* @function     : vIE_Init
* @param        : None
* @retval       : None
* @brief        : 中断初始化
*/
static void vIE_Init(void)
{
  // KEY中断
  P1IES |= KEY1;        // 下降沿触发
  P1IFG &= (~KEY1);     // 清除标志位,避免误触发
  P1IE |= KEY1; // 使能KEY1中断
  
  __enable_interrupt(); // 使能全局中断
}
key.h
cpp
#ifndef __KEY_H
#define __KEY_H
#include <main.h>


// 按键引脚是P13
#define KEY1    BIT3

#endif
key.c
cpp
/***************************************************************************
 * File          : key.c
 * Author        : Luckys.
 * Date          : 2023-06-06
 * description   : 按键  
****************************************************************************/

#include <main.h>
   



// 按键中断函数
#pragma vector = PORT1_VECTOR
__interrupt void Key1_isr(void)
{
  static uint16_t Cnt = 0;
  
  // KEY1中断
  if (KEY1 == (P1IFG & KEY1))
  {
    // 延时消抖
    Public.vDelay_ms(5);
    if ((P1IN & KEY1) != KEY1)  // 检测输入是否为低电平
    {
      // 按键按下执行功能
      printf("KEY1 Down %u次\r\n",Cnt);
    }
    // 清除标志位
    P1IFG &= (~KEY1); 
  }
}

长按

在上面的基础上修改中断函数即可,这里注意中断优先级问题,要把喂狗的函数丢这里,不然长按导致单片机复位

key.c
cpp
// 按键中断函数
#pragma vector = PORT1_VECTOR
__interrupt void Key1_isr(void)
{
  uint8_t Delay_Cnt;
  
  // KEY1中断
  if (KEY1 == (P1IFG & KEY1))
  {
    // 延时消抖
    Public.vDelay_ms(5);
    if ((P1IN & KEY1) != KEY1)  // 检测输入是否为低电平
    {
      for (Delay_Cnt = 0; Delay_Cnt < 200; Delay_Cnt++)
      {
        WatchDog.vWatchDog_Feed();    //喂狗  
        Public.vDelay_ms(10);
        
        if (KEY1 == (P1IN & KEY1))      // 提前弹起则退出
        {
          break;
        }
      }
      if (Delay_Cnt >= 200)     // 长按
      {
        // 执行功能 ---切换状态
        if (System.ucSystem_Status == System_Run)
        {
          System.ucSystem_Status = System_StandBy;
          printf("系统处于待机状态\r\n");
        }
        else
        {
          System.ucSystem_Status = System_Run;
          printf("系统处于运行状态\r\n");
        }
      }
      else      // 单击
      {
        printf("单击\r\n");
      }
    }
    // 清除标志位
    P1IFG &= (~KEY1); 
  }
}

中断+状态机

cpp
// 定义枚举类型
typedef enum
{
  TimerA_10ms = (uint16_t)2,
  TimerA_50ms = (uint16_t)10,
  TimerA_100ms = (uint16_t)20,
  TimerA_200ms = (uint16_t)40,
  TimerA_500ms = (uint16_t)100,
  TimerA_1s = (uint16_t)200,
  TimerA_2s = (uint16_t)400,
}TimerA_Value_t;

#pragma vector = TIMERA0_VECTOR // 定义一下中断向量
__interrupt void TimerA_isr(void)       // __interrupt 是中断关键字
{
  WatchDog.vWatchDog_Feed();    //喂狗
  
  if (++TimerA.usMCU_Run_Timer >= TimerA_100ms)
  {
    TimerA.usMCU_Run_Timer = 0;
    // 置位流水灯状态机标志位
    STA_Machine.ucSTA_Machine_Switch_Flag = TRUE;
  }
  
  Key_STA_Machine.usKey_STA_Machine_Scan_Timer++;       // 按键状态扫描定时器
  Key_STA_Machine.usKey_Double_Click_Timer++;   // KEY1长按检测定时器
  Key_STA_Machine.usKey_Long_Click_Timer++;     // KEY1双击检测定时器
}

/*
* @function     : vRun
* @param        : None
* @retval       : None
* @brief        : 系统运行
*/
static void vRun(void)
{
  Key.Key_Detect();     // 按键扫描
  
  if (Key.Key_Flag == TRUE)
  {
    if (Key.Key_Click == TRUE)  // 单击
    {
      printf("单击动作\r\n");
    }
    if (Key.Key_Double_Click == TRUE)
    {
      printf("双击动作\r\n");
    }
    if (Key.Key_Long_Click == TRUE)
    {
      printf("长按动作\r\n");
    }
    // 清除KEY所有标志位
    Key.Key_Flag = FALSE;
    Key.Key_Click = FALSE;
    Key.Key_Double_Click = FALSE;
    Key.Key_Long_Click = FALSE;
  }
}
key.h
cpp
#ifndef __KEY_H
#define __KEY_H
#include <main.h>


// 按键引脚是P13
#define KEY1    BIT3
// 设定长按时间
#define Set_Long_TIME   TimerA_2s
// 设定双击时间
#define Set_Double_TIME TimerA_200ms

typedef struct
{
  uint8_t Key_Flag;     // 按键 标志位
  uint8_t Key_Click;    // 单击
  uint8_t Key_Double_Click;     // 双击
  uint8_t Key_Long_Click;       // 长按
  
  void (*Key_Detect)(void);     // 按键检测
}Key_t;

typedef enum
{
  STA1_KEY_UP = (uint8_t)0x01,  // 按键弹起
  STA2_KEY_D_SHAKE = (uint8_t)0x02,       // 按下抖动
  STA3_KEY_DOWN = (uint8_t)0x03,        // 按键按下
  STA4_KEY_U_SHAKE = (uint8_t)0x04,     // 弹起抖动
}Key_STA_Machine_Status_t;

typedef struct
{
  Key_STA_Machine_Status_t ucKey_STA_Machine_Status;    // 按键状态机状态
  uint16_t volatile usKey_STA_Machine_Scan_Timer;       // 状态机扫描定时器
  uint16_t volatile usKey_Double_Click_Timer;   // 双击定时器
  uint16_t volatile usKey_Long_Click_Timer;     // 长按定时器
}Key_STA_Machine_t;

extern Key_t Key;
extern Key_STA_Machine_t Key_STA_Machine;

#endif
key.c
cpp
/***************************************************************************
 * File          : key.c
 * Author        : Luckys.
 * Date          : 2023-06-06
 * description   : 按键  
****************************************************************************/

#include <main.h>
   
/*====================================variable definition declaration area BEGIN=================================*/
static uint8_t Key_Click_Status = FALSE;        // 单击状态缓存
/*====================================variable definition declaration area   END=================================*/

/*====================================static function declaration area BEGIN====================================*/
static void Key_Detect(void);   // 按键检测
/*====================================static function declaration area   END====================================*/

Key_t Key = 
{
  FALSE,
  FALSE,
  FALSE,
  FALSE,
  Key_Detect,
};

Key_STA_Machine_t Key_STA_Machine = 
{
  STA1_KEY_UP,
  0,
  0,
  0,
};


static void Key_Detect(void)
{
  if (Key_STA_Machine.usKey_STA_Machine_Scan_Timer >= TimerA_10ms)      // 间隔10ms扫描
  {
    switch(Key_STA_Machine.ucKey_STA_Machine_Status)    // 运行状态机检测-> 单击 双击 长按
    {
    case STA1_KEY_UP:   // 按键弹起
      {
        // 单击检测
        if ((P1IN & KEY1) == KEY1)      // 高电平
        {
          if (Key_Click_Status == TRUE) // 判断缓存,只有前面有单击,而且没检测到双击与长按 单击才有效
          {
            if (Key_STA_Machine.usKey_Double_Click_Timer >= Set_Double_TIME)    // 双击检测超时,单击有效
            {
              Key.Key_Flag = TRUE;
              Key.Key_Click = TRUE;
              Key_Click_Status = FALSE; // 清除单击缓存
            }
          }
        }
        else    // 检测到低电平 状态切换到按下抖动
        {
          Key_STA_Machine.ucKey_STA_Machine_Status = STA2_KEY_D_SHAKE;
          printf("事件通知:弹起状态 -> 按下抖动状态\r\n");
        }
        break;
      }
    case STA2_KEY_D_SHAKE:      // 按下抖动
      {
        if ((P1IN & KEY1) == KEY1)      // 检测到高电平,抖动引起,状态切换到弹起状态
        {
          Key_STA_Machine.ucKey_STA_Machine_Status = STA1_KEY_UP;
        }
        else    // 检测到低电平 确认按键按下,状态回到按下状态
        {
          Key_STA_Machine.ucKey_STA_Machine_Status = STA3_KEY_DOWN;
          Key_STA_Machine.usKey_Long_Click_Timer = 0;   // 长按检测定时器清0 开始计时
          printf("事件通知:按下抖动状态 -> 按下状态\r\n");
        }
        break;
      }
    case STA3_KEY_DOWN: // 按键按下
      {
        if ((P1IN & KEY1) == KEY1)      // 检测到高电平,抖动引起,状态切换到弹起抖动状态
        {
          Key_STA_Machine.ucKey_STA_Machine_Status = STA4_KEY_U_SHAKE;
          // 双击检测
          if (Key.Key_Long_Click == FALSE)
          {
            if (Key_Click_Status == TRUE)
            {
              Key.Key_Flag = TRUE;
              Key.Key_Double_Click = TRUE;
              Key_Click_Status = FALSE; // 清除单击缓存
            }
            else
            {
              Key_Click_Status = TRUE; // 单击缓存
              Key_STA_Machine.usKey_Double_Click_Timer = 0;     // 双击检测定时器清0 开始定时
            }
          }
          printf("事件通知:按下状态 -> 弹起抖动状态\r\n");
        }
        else    // 检测到低电平 按键保持按下 进行长按检测
        {
          if (Key.Key_Long_Click == FALSE)      // 长按检测
          {
            if (Key_STA_Machine.usKey_Long_Click_Timer >= Set_Long_TIME)
            {
              Key_STA_Machine.ucKey_STA_Machine_Status = STA4_KEY_U_SHAKE;      // 按键按下超过Set_Press_TIME,状态切换至STA4弹起抖动,避免反复检测长按
              Key.Key_Flag = TRUE;
              Key.Key_Long_Click = TRUE;
              Key_Click_Status = FALSE; // 清除单击缓存
              printf("事件通知:按下状态 -> 弹起抖动状态\r\n");
            }
          }
        }
        break;
      }
    case STA4_KEY_U_SHAKE:      // 弹起抖动
      {
        if ((P1IN & KEY1) == KEY1)      // 检测到高电平 回到弹起状态
        {
          Key_STA_Machine.ucKey_STA_Machine_Status = STA1_KEY_UP;
          printf("事件通知:弹起抖动状态 -> 弹起状态\r\n");
        }
        else    // 检测到低电平 抖动引起 按键保持弹起抖动状态
        {
          
        }
        break;
      }
    default:
      {
        Key_STA_Machine.ucKey_STA_Machine_Status = STA1_KEY_UP;
        break;
      }      
    }
    Key_STA_Machine.usKey_STA_Machine_Scan_Timer = 0;   // 扫描定时清0
  }
}

PWM调节1W灯

  • PWM相关

每个捕获/比较块包含一个输出单元(就是下图中的 CCR0(TA0),CCR1(TA1),CCR2(TA2))。 使用的输出单元生成输出信号,例如 PWM 信号。 每个输出单元有八个基于 EQU0EQUx 信号生成信号的工作模式。

输出模式由 OUTMODx 位定义并在表 11-2 中描述。 对于模式 0 以外的所有模式,OUTx 信号随着定时器时钟的上升沿而改变。输出模式 2、3、6 和 7 对输出单元 0 没有用,因为 EQUx = EQU0。(意思就是TA0只能输出固定频率的方波,频率是定时器周期的2倍,但是不能输出可调占空比的PWM)

使用TACCR0和TACCR1的示例图:

  • CN5611驱动IC

CN5611是一款工作于 2.7V~6V 的电流调制电路,恒定输出电流可达 800mA

注意PWM的频率

PWM为高时M1导通ISET相当于直接接地,LED无输出,PWM为低时M1截止ISET通过电阻上拉,LED输出,占空比和亮度是反比,可以设置PWM为低电平有效,占空比可亮度就对应了

  • 硬件连接

当 LED_EN 为1,NPN导通,PMOS的G极为低电平,PMOS导通

配置成模式6,看上面图即可,有效电平是高电平,但是LED的占空比是越大亮度越小,这个是需要注意的

然后占空比100%的话需要加1,不然使用示波器可以看到有尖峰波形

初始化时打开电源后需要延时5ms,不然上电的话会造成LED闪一下

cpp
/*
* @function     : Sys_Init
* @param        : None
* @retval       : None
* @brief        : 系统初始化
*/
static void vSys_Init(void)
{
  WatchDog.vWatchDog_Init();    // 看门狗初始化
  Hardware_Init.vCLK_Init();    // 时钟初始化
  Hardware_Init.vGPIO_Init();   // GPIO初始化
  TimerA.vTimerA_Init();
  Hardware_Init.vIE_Init();     // 中断初始化
  USART1.vUSART1_Init();        // 串口1初始化
  Pwm.vPWM_Init();      // PWM初始化
  USART1.vUSART1_SendString("系统初始化完成\r\n");
  printf("PI = %.1f\r\n",3.14);
}

/*
* @function     : vRun
* @param        : None
* @retval       : None
* @brief        : 系统运行
*/
static void vRun(void)
{
  static LED_Bright_t Value = LED_Bright_0;     // 初始档位为0
  
  Key.Key_Detect();     // 按键扫描
  
  if (Key.Key_Flag == TRUE)
  {
    if (Key.Key_Click == TRUE)  // 单击
    {
      printf("单击动作\r\n");
      switch(Value)
      {
      case LED_Bright_0:
        {
          Value = LED_Bright_1;
          break;
        }
      case LED_Bright_1:
        {
          Value = LED_Bright_2;
          break;
        }
      case LED_Bright_2:
        {
          Value = LED_Bright_3;
          break;
        }
      case LED_Bright_3:
        {
          Value = LED_Bright_4;
          break;
        }
      case LED_Bright_4:
        {
          Value = LED_Bright_5;
          break;
        }
      case LED_Bright_5:
        {
          Value = LED_Bright_0;
          break;
        }    
      }
      Pwm.vLED_Bright_Adjust(Value);    // 调整亮度
    }
    if (Key.Key_Double_Click == TRUE)
    {
      printf("双击动作\r\n");
      Pwm.vLED_Bright_Adjust(LED_Bright_5);    // 最亮
    }
    if (Key.Key_Long_Click == TRUE)
    {
      printf("长按动作\r\n");
      Pwm.vLED_Bright_Adjust(LED_Bright_0);    // 关闭
    }
    // 清除KEY所有标志位
    Key.Key_Flag = FALSE;
    Key.Key_Click = FALSE;
    Key.Key_Double_Click = FALSE;
    Key.Key_Long_Click = FALSE;
  }
}
pwm.h
cpp
#ifndef __PWM_H
#define __PWM_H
#include <main.h>

// 电源控制引脚P16
#define LED_POWER_EN_PIN        BIT6  
// PWM引脚P17
#define LED_PWM_PIN             BIT7

// 占空比
typedef enum
{
  PWM_Duty_0 = (uint16_t)0,     // 占空比为0%
  PWM_Duty_20 = (uint16_t)1000,     // 占空比为20%
  PWM_Duty_40 = (uint16_t)2000,     // 占空比为40%
  PWM_Duty_60 = (uint16_t)3000,     // 占空比为60%
  PWM_Duty_80 = (uint16_t)4000,     // 占空比为80%
  PWM_Duty_100 = (uint16_t)5001,     // 占空比为100%
}PWM_Duty_t;

// 大功率LED亮度
typedef enum
{
  LED_Bright_0,
  LED_Bright_1,
  LED_Bright_2,
  LED_Bright_3,
  LED_Bright_4,
  LED_Bright_5,
}LED_Bright_t;

// 大功率LED电源控制
typedef enum
{
  LED_Power_ON = (uint8_t)0,
  LED_Power_OFF = (uint8_t)1,
}LED_Power_t;

typedef struct
{
  PWM_Duty_t LED_Duty;  // PWM占空比
  
  void (*vPWM_Init)(void);      // PWM初始化
  void (*vLED_Power_Control)(LED_Power_t);     // LED电源控制
  void (*vLED_Bright_Adjust)(LED_Bright_t);      // 调整LED灯亮度
}Pwm_t;

extern Pwm_t Pwm;

#endif
pwm.c
cpp
/***************************************************************************
 * File          : pwm.c
 * Author        : Luckys.
 * Date          : 2023-06-07
 * description   : PWM输出
------------------
LED两个引脚初始化默认为高电平即关闭和灭状态
------------------
****************************************************************************/

#include <main.h>

/*====================================static function declaration area BEGIN====================================*/
static void vPWM_Init(void);      // PWM初始化
static void vLED_Power_Control(LED_Power_t);     // LED电源控制
static void vLED_Bright_Adjust(LED_Bright_t);      // 调整LED灯亮度
/*====================================static function declaration area   END====================================*/

Pwm_t Pwm = 
{
  PWM_Duty_100,
  vPWM_Init,
  vLED_Power_Control,
  vLED_Bright_Adjust,
};

/*
* @function     : vPWM_Init
* @param        : None
* @retval       : None
* @brief        : PWM初始化
*/
static void vPWM_Init(void)
{
  vLED_Power_Control(LED_Power_ON);     // 打开LED电源
  Public.vDelay_ms(5);  // 延时,等待LED驱动IC稳定工作
  CCTL2 |= OUTMOD_6;    // 选择模式6
  CCR2 = Pwm.LED_Duty;  // 初始占空比
  P1SEL |= LED_PWM_PIN; // 开启复用引脚功能
}

/*
* @function     : vLED_Power_Control
* @param        : None
* @retval       : None
* @brief        : LED电源控制
*/
static void vLED_Power_Control(LED_Power_t Value)
{
  if (Value == LED_Power_ON)
  {
    P1OUT |= LED_POWER_EN_PIN;  // 高电平是关闭
  }
  else
  {
    P1OUT &= (~LED_POWER_EN_PIN);  // 低电平是打开
  }
}

/*
* @function     : LED_Bright_Adjust
* @param        : None
* @retval       : None
* @brief        : 调整LED灯亮度
*/
static void vLED_Bright_Adjust(LED_Bright_t Value)
{
  // 占空比越大亮度越小
  switch(Value)
  {
  case LED_Bright_0:
    {
      Pwm.LED_Duty = PWM_Duty_100;
      break;
    }
  case LED_Bright_1:
    {
      Pwm.LED_Duty = PWM_Duty_80;
      break;
    }
  case LED_Bright_2:
    {
      Pwm.LED_Duty = PWM_Duty_60;
      break;
    }
  case LED_Bright_3:
    {
      Pwm.LED_Duty = PWM_Duty_40;
      break;
    }
  case LED_Bright_4:
    {
      Pwm.LED_Duty = PWM_Duty_20;
      break;
    }
  case LED_Bright_5:
    {
      Pwm.LED_Duty = PWM_Duty_0;
      break;
    }    
  }
  // 更新PWM占空比
  CCR2 = Pwm.LED_Duty;
}

ADC采集锂电池与太阳能板电压

  • ADC框图

A0~A7 分别对应引脚 P60~P67

计算采用 4095

寄存器

两个控制寄存器,一个中断标志寄存器,一个中断使能寄存器,中断查找寄存器,有16个内存,然后对应16个内存控制寄存器

需要注意的是 ENC 这个寄存器,需要置0才能修改其他寄存器,修改完再置1

  • 硬件连接

SW_BAT 引脚是控制MOS管的导通,电池电压BAT是等于 采集电压*2

  • 程序编写

单次转换内存寄存器 选择没有要求,选其中一个即可然后下面也对应即可

cpp
/*
* @function     : Sys_Init
* @param        : None
* @retval       : None
* @brief        : 系统初始化
*/
static void vSys_Init(void)
{
  WatchDog.vWatchDog_Init();    // 看门狗初始化
  Hardware_Init.vCLK_Init();    // 时钟初始化
  Hardware_Init.vGPIO_Init();   // GPIO初始化
  TimerA.vTimerA_Init();
  Hardware_Init.vIE_Init();     // 中断初始化
  USART1.vUSART1_Init();        // 串口1初始化
  Pwm.vPWM_Init();      // PWM初始化
  ADC.vADC_Init();      // ADC初始化
  USART1.vUSART1_SendString("系统初始化完成\r\n");
  printf("PI = %.1f\r\n",3.14);
}
adc.h
cpp
#ifndef __ADC_H
#define __ADC_H
#include <main.h>

// 打开/关闭(置1/0)BAT采集电路的MOS
#define BAT_MOS_OPEN    (P6OUT |= BIT4)
#define BAT_MOS_CLOSE   (P6OUT &= (~BIT4))

// 引脚定义 P65 P66
#define ADC_BAT_PIN     BIT5
#define ADC_VIN_PIN     BIT6


typedef enum
{
  INCH_BAT = INCH_5,    // 输入通道5
  INCH_VIN = INCH_6,    // 输入通道6
}ADC_CHANNEL_t;

typedef struct
{
  float fBAT_Voltage;   // 电池电压 初始化默认3.7V
  float fVIN_VOltage;   // 输入电压
  
  void (*vADC_Init)(void);      // ADC初始化
  void (*vADC_Get_BAT_Voltage)(void);       // 获取电池电压
  void (*vADC_Get_VIN_Voltage)(void);       // 获取输入电压
}ADC_t;

extern ADC_t ADC;

#endif
adc.c
cpp
/***************************************************************************
 * File          : adc.c
 * Author        : Luckys.
 * Date          : 2023-06-07
 * description   : ADC  
****************************************************************************/

#include <main.h>

/*====================================static function declaration area BEGIN====================================*/
static void vADC_Init(void);    // ADC初始化
static void vADC_Get_BAT_Voltage(void); // 获取电池电压
static void vADC_Get_VIN_Voltage(void); // 获取输入电压
/*====================================static function declaration area   END====================================*/

ADC_t ADC = 
{
  3.7,
    0,
  vADC_Init,
  vADC_Get_BAT_Voltage,
  vADC_Get_VIN_Voltage,
};

/*
* @function     : vADC_Init
* @param        : None
* @retval       : None
* @brief        : ADC初始化
*/
static void vADC_Init(void)
{
  P6SEL |= ADC_BAT_PIN + ADC_VIN_PIN;   // 开启复用
  // 配置ADC12 控制0寄存器--- 采样周期256 + 参考内部电压2.5V + 使能2.5V + 使能ADC12
  ADC12CTL0 = SHT0_8 + REF2_5V + REFON + ADC12ON;
}

/*
* @function     : vADC_Get_BAT_Voltage
* @param        : None
* @retval       : None
* @brief        : 获取电池电压
*/
static void vADC_Get_BAT_Voltage(void)
{
  // 打开MOS管
  BAT_MOS_OPEN;
  Public.vDelay_ms(1);
  printf("采集电池电压:\r\n");
  // 更改ADC寄存器前需要把ENC清0
  ADC12CTL0 &= (~ENC);
  // 脉冲采样模式 SHP = 1
  // 转换模式选择单通道,单次转换 CONSEQ = 00  
  // 单次转换内存寄存器选择5 
  // ADC12时钟默认选择ADC12OSC,大概5MHz, 不分频
  // 电压采样与保持时钟源选择软件控制 - ADC12SC
  ADC12CTL1 = CSTARTADD_5 + SHP + CONSEQ_0;
  // 参考电压VR+ = VREF+, VR- = VREF-
  // 选择输入通道5---跟硬件相关,因为BAT是连接A5的 
  ADC12MCTL5 = SREF_5 + INCH_BAT;
  // 使能转换
  ADC12CTL0 |= ENC;
  
  // 开始转换
  ADC12CTL0 |= ADC12SC;
  // 等待转换结束(标志位置1)--通过判断中断标志位尽管我们没有打开中断但是还是能通过它来查询是否转换完
  while ((ADC12IFG & ADC_BAT_PIN) != ADC_BAT_PIN);
  // 计算电压
  ADC.fBAT_Voltage = (ADC12MEM5 * 2.5) / 4095;
  ADC.fBAT_Voltage *= 2;
  // 打印
  printf("VBAT: %.2fV\r\n",ADC.fBAT_Voltage);
  // 关闭MOS管
  BAT_MOS_CLOSE;  
}

/*
* @function     : vADC_Get_VIN_Voltage
* @param        : None
* @retval       : None
* @brief        : 获取输入电压
*/
static void vADC_Get_VIN_Voltage(void)
{
  printf("采集太阳能电压:\r\n");
  // 更改ADC寄存器前需要把ENC清0
  ADC12CTL0 &= (~ENC);
  // 脉冲采样模式 SHP = 1
  // 转换模式选择单通道,单次转换 CONSEQ = 00  
  // 单次转换内存寄存器选择6 (可以是其他比如5,4,3,2,1,不是强制是6),但是CSTARTADD_6和CSTARTADD_6要匹配
  // ADC12时钟默认选择ADC12OSC,大概5MHz, 不分频
  // 电压采样与保持时钟源选择软件控制 - ADC12SC
  ADC12CTL1 = CSTARTADD_6 + SHP + CONSEQ_0;
  // 参考电压VR+ = VREF+, VR- = VREF-
  // 选择输入通道6---跟硬件相关,因为BAT是连接A5的 
  ADC12MCTL6 = SREF_5 + INCH_VIN;
  // 使能转换
  ADC12CTL0 |= ENC;
  
  // 开始转换
  ADC12CTL0 |= ADC12SC;
  // 等待转换结束(标志位置1)--通过判断中断标志位尽管我们没有打开中断但是还是能通过它来查询是否转换完
  while ((ADC12IFG & ADC_VIN_PIN) != ADC_VIN_PIN);
  // 计算电压
  ADC.fVIN_VOltage = (ADC12MEM6 * 2.5) / 4095;
  ADC.fVIN_VOltage *= 9.2;      // 最大输入 9.2*2.5V=23V
  // 打印
  printf("VIN: %.2fV\r\n",ADC.fVIN_VOltage);
}

OLED

写在 【协议/模块/算法/通信学习】文章里

电源及太阳能控制

  • 其他

LDO电路用到了 HT7333-3 稳压芯片,允许高达 30V 的输入电压,不同型号具有多种固定输出电压,范围从 2.1V 到 5.0V,这款型号的话是 3.3V

升压芯片使用 ME2159AM6G(推荐3.3V升压到5V),输出电压由一个电阻分压器从输出电压设定到FB。输出电压为

Vout=0.6×(1+R1R2)Vout=0.6\times(1+\frac{R1}{R2})

计算下面电路可以知道:0.6 x (1 + 80600/11000) ≈ 4.996V

  • 硬件连接

PMOS低电平导通

指示灯不会处于0 0 状态,可能是 01 10 11

  • 程序编写
cpp
/*
* @function     : vRun
* @param        : None
* @retval       : None
* @brief        : 系统运行
*/
static void vRun(void)
{
  uint16_t Temp_uint  = 0;
  uint16_t i;
  
  //采集电池与太阳能板电压
  ADC.vADC_Get_BAT_Voltage();
  ADC.vADC_Get_VIN_Voltage();
   
  //OLED显示太阳能板电压
  Temp_uint = (uint16_t)(ADC.fVIN_VOltage * 10);
  OLED.vOLED_Show_Char(2,64,Temp_uint / 100 + '0',ASCII_SIZE_16);
  OLED.vOLED_Show_Char(2,72,Temp_uint % 100 / 10 + '0',ASCII_SIZE_16);
  OLED.vOLED_Show_Char(2,80,'.',ASCII_SIZE_16);
  OLED.vOLED_Show_Char(2,88,Temp_uint % 10 + '0',ASCII_SIZE_16);
  OLED.vOLED_Show_Char(2,96,'V',ASCII_SIZE_16);
  
  //OLED显示电池电压
  Temp_uint = (uint16_t)(ADC.fBAT_Voltage * 10);
  OLED.vOLED_Show_Char(4,72,Temp_uint / 10 + '0',ASCII_SIZE_16);
  OLED.vOLED_Show_Char(4,80,'.',ASCII_SIZE_16);
  OLED.vOLED_Show_Char(4,88,Temp_uint % 10 + '0',ASCII_SIZE_16);
  OLED.vOLED_Show_Char(4,96,'V',ASCII_SIZE_16);
  
  // 充电指示
  Power.vCharging_Indicate();
  //间隔500ms采集一次
  for (i = 0; i < 500; i++)
  {
    Public.vDelay_ms(1);
    Key.Key_Detect();     // 按键扫描
    if (Key.Key_Flag == TRUE)
    {
      break;
    }
  }

  if (Key.Key_Flag == TRUE)
  {
    if (Key.Key_Click == TRUE)  // 单击
    {
      if (PWR_Status_ON == Power.Boost_5V_Status)       // 测试升压输出
      {
        Power.vBoost_5V_Control(PWR_OFF);
      }
      else
      {
        Power.vBoost_5V_Control(PWR_ON);
      }
      if (PWR_Status_ON == Power.Peripheral_3V3_Status)       // 测试控制外设3.3V电源
      {
        Power.vPeripheral_3V3_Control(PWR_OFF);
      }
      else
      {
        Power.vPeripheral_3V3_Control(PWR_ON);
      }      
    }
    // 清除KEY所有标志位
    Key.Key_Flag = FALSE;
    Key.Key_Click = FALSE;
  }
}
power.h
cpp
#ifndef __POWER_H
#define __POWER_H
#include <main.h>

typedef enum
{
  PWR_ON,
  PWR_OFF
}PWR_t;

typedef enum
{
  PWR_Status_ON,        // 打开
  PWR_Status_OFF,       // 关闭
}PWR_Status_t;

typedef enum
{
  Charge_Status_Just = (uint8_t)1,
  Charge_Status_Full = (uint8_t)2,
  Charge_Status_Stop = (uint8_t)3,
}Charge_Status_t;

typedef struct
{
    Charge_Status_t Charge_Status;       //充电状态
  PWR_Status_t Boost_5V_Status; // 升压电源状态
  PWR_Status_t Peripheral_3V3_Status;   // 外设3.3V电源状态
  
  void (*vBoost_5V_Control)(PWR_t);     // 升压电源控制
  void (*vPeripheral_3V3_Control)(PWR_t);       // 外设电源控制
  void (*vCharging_Indicate)(void);     // 充电指示
}Power_t;


extern Power_t Power;

#endif
power.c
cpp
/***************************************************************************
 * File          : power.c
 * Author        : Luckys.
 * Date          : 2023-06-09
 * description   : 电源  
****************************************************************************/


#include <main.h>

/*====================================static function declaration area BEGIN====================================*/
static void vBoost_5V_Control(PWR_t);     // 升压电源控制
static void vPeripheral_3V3_Control(PWR_t);       // 外设电源控制
static void vCharging_Indicate(void);     // 充电指示
/*====================================static function declaration area   END====================================*/


Power_t Power = 
{
  Charge_Status_Stop,
  PWR_Status_ON,
  PWR_Status_ON,
  vBoost_5V_Control,
  vPeripheral_3V3_Control,
  vCharging_Indicate,
};

/*
* @function     : vBoost_5V_Control
* @param        : PWR_Status -> 电源状态
* @retval       : None
* @brief        : 升压电源控制(P50管脚)
*/
static void vBoost_5V_Control(PWR_t PWR_Status)
{
  if (PWR_ON == PWR_Status)
  {
    P5OUT |= BIT0;      // 使能升压芯片--ME2159
    Power.Boost_5V_Status = PWR_Status_ON;
  }
  else
  {
    P5OUT &= (~BIT0);      // 失能升压芯片--ME2159
    Power.Boost_5V_Status = PWR_Status_OFF;    
  }
}

/*
* @function     : vPeripheral_3V3_Control
* @param        : None
* @retval       : None
* @brief        : 外设电源控制
*/
static void vPeripheral_3V3_Control(PWR_t PWR_Status)
{
  if (PWR_ON == PWR_Status)
  {
    P6OUT &= (~BIT3);   // 打开PMOS管
    Power.Peripheral_3V3_Status = PWR_Status_ON;
    P5DIR |= (BIT5 + BIT6);     // 恢复SDA SCL 管脚方向为输出
    OLED.vOLED_Init();  // OLED初始化
  }
  else
  {
    P6OUT |= BIT3;   // 关闭PMOS管
    Power.Peripheral_3V3_Status = PWR_Status_OFF;
    P5DIR &= (~(BIT5 + BIT6));  // 把SDA SCL 管脚设为输入
  }
}

/*
* @function     : vCharging_Indicate
* @param        : None
* @retval       : None
* @brief        : 升压电源控制
*/
static void vCharging_Indicate(void)
{
  //正在充电 - CHAG为低电平
  if((P1IN & BIT0) != BIT0)
  {
    OLED.vOLED_Show_CHN(6,64,"正");
    OLED.vOLED_Show_CHN(6,80,"在");
  }
  //完成充电 - DONE为低电平
  else if((P1IN & BIT5) != BIT5)
  {
    OLED.vOLED_Show_CHN(6,64,"完");
    OLED.vOLED_Show_CHN(6,80,"成");
  }
  //停止充电 - CHAG,DONE都为高电平,太阳能输入电压过低,CN3791没有工作
  else
  {
    OLED.vOLED_Show_CHN(6,64,"停");
    OLED.vOLED_Show_CHN(6,80,"止");
  }
}
oled.c
cpp
/*
* @function     : vOLED_Init
* @param        : None
* @retval       : None
* @brief        : OLED初始化
*/
static void vOLED_Init(void)
{
  Public.vDelay_ms(100);        // 上电延时
  vOLED_Write_CMD(0xAE);//--display off
  vOLED_Write_CMD(0x00);//---set low column address
  vOLED_Write_CMD(0x10);//---set high column address
  vOLED_Write_CMD(0x40);//--set start line address  
  vOLED_Write_CMD(0xB0);//--set page address
  vOLED_Write_CMD(0x81); // contract control
  vOLED_Write_CMD(0xFF);//--128   
  vOLED_Write_CMD(0xA1);//set segment remap 
  vOLED_Write_CMD(0xA6);//--normal / reverse
  vOLED_Write_CMD(0xA8);//--set multiplex ratio(1 to 64)
  vOLED_Write_CMD(0x3F);//--1/32 duty
  vOLED_Write_CMD(0xC8);//Com scan direction
  vOLED_Write_CMD(0xD3);//-set display offset
  vOLED_Write_CMD(0x00);//
  
  vOLED_Write_CMD(0xD5);//set osc division
  vOLED_Write_CMD(0x80);//
  
  vOLED_Write_CMD(0xD8);//set area color mode off
  vOLED_Write_CMD(0x05);//
  
  vOLED_Write_CMD(0xD9);//Set Pre-Charge Period
  vOLED_Write_CMD(0xF1);//
  
  vOLED_Write_CMD(0xDA);//set com pin configuartion
  vOLED_Write_CMD(0x12);//
  
  vOLED_Write_CMD(0xDB);//set Vcomh
  vOLED_Write_CMD(0x30);//
  
  vOLED_Write_CMD(0x8D);//set charge pump enable
  vOLED_Write_CMD(0x14);//
  
  vOLED_Write_CMD(0xAF);//--turn on oled panel
  
  //OLED清屏
  OLED.vOLED_Clear();                  
  //OLED屏幕初始显示
  OLED.vOLED_Show_CHN(0,8,"太");
  OLED.vOLED_Show_CHN(0,32,"阳");
  OLED.vOLED_Show_CHN(0,56,"能");
  OLED.vOLED_Show_CHN(0,80,"路");
  OLED.vOLED_Show_CHN(0,104,"灯");
  
  OLED.vOLED_Show_String(2,24,"VIN:",ASCII_SIZE_16);
  OLED.vOLED_Show_String(4,24,"BAT:",ASCII_SIZE_16);
  
  OLED.vOLED_Show_CHN(6,96,"充");
  OLED.vOLED_Show_CHN(6,112,"电");  
}

/*
* @function     : vOLED_Show_Char
* @param        : Page -> 页位置 Seg -> 段位置 ch -> 要显示的字符 ch_size -> 字体大小
* @retval       : None
* @brief        : OLED显示字符
*/
static void vOLED_Show_Char(uint8_t Page, uint8_t Seg, uint8_t ch, ASCII_Size_t ch_size)
{
  uint8_t ucIndex,i;
  if (PWR_Status_ON == Power.Peripheral_3V3_Status)
  {
    // ASCII 字符集数组索引,需要减去偏移量(' ' -> 空格对应的码值)
    ucIndex = ch - ' ';
    // 判断大小
    if (ASCII_SIZE_16 == ch_size)
    {
      // 设置字符上半部分
      vOLED_Set_Pos(Page,Seg);    
      // 写入字符上半部分数据
      for (i = 0; i < 8; i++)
      {
        vOLED_Write_Data(ucASCII_16x8[ucIndex][i]);
      }
      // 设置字符下半部分
      vOLED_Set_Pos(Page + 1,Seg);
      // 写入字符下半部分数据
      for (i = 0; i < 8; i++)
      {
        vOLED_Write_Data(ucASCII_16x8[ucIndex][i + 8]);
      }
    }    
  }
}

/*
* @function     : vOLED_Show_String
* @param        : Page -> 页位置 Seg -> 段位置 p_Str -> 要显示的字符串 ch_size -> 字体大小
* @retval       : None
* @brief        : OLED显示字符串
*/
static void vOLED_Show_String(uint8_t Page, uint8_t Seg, const char* p_Str, ASCII_Size_t ch_size)
{
  // 字符尺寸高度为 1-8时占1Page  9-16时占2Page 17-24时占3Page ....以此类推
  // 那怎么算出对应的Page是1,2,3.... 那直接加7除以8即可
  uint8_t ch_Pages = (ch_size + 7) / 8; // 字符占用的页数
  
  if (PWR_Status_ON == Power.Peripheral_3V3_Status)
  {
    while(*p_Str != '\0')
    {
      // 自动换行
      if ((Seg + ch_size / 2) > OLED_WIDTH)       // 如果 位置+字符宽度(16/2=8) 大于 屏幕宽度
      {
        Seg = 0;
        Page += ch_Pages;
        if (Page >= OLED_PAGE_MAX)
        {
          Page = 0;
        }
      }
      // 自动换页
      if ((Page + ch_Pages) > OLED_PAGE_MAX)
      {
        Seg = 0;
        Page = 0;
      }
      // 显示字符
      vOLED_Show_Char(Page,Seg,*p_Str,ch_size);
      // 更新字符
      p_Str++;
      // 更新显示位置
      Seg += ch_size / 2;
    }    
  }
}

/*
* @function     : vOLED_Show_CHN
* @param        : Page -> 页位置 Seg -> 段位置 p_Str -> 要显示的字符串
* @retval       : None
* @brief        : OLED显示汉字
*/
static void vOLED_Show_CHN(uint8_t Page, uint8_t Seg, const char* p_Str)
{
  uint16_t usCHN_Number;  // 字库中汉字数量
  uint16_t usIndex;     // 字库中的汉字索引
  uint8_t i;
  
  if (PWR_Status_ON == Power.Peripheral_3V3_Status)
  {
    // 统计汉字的位置
    usCHN_Number = sizeof(CHN_16x16) / sizeof(CHN_16x16_t);
    // 查找汉字的位置
    for (usIndex = 0; usIndex < usCHN_Number; usIndex++)
    {
      if ((CHN_16x16[usIndex].Index[0] == *p_Str) && (CHN_16x16[usIndex].Index[1] == *(p_Str + 1)))       // 因为一个汉字占两个字节
      {
        // 设置字符上半部分起始位置
        vOLED_Set_Pos(Page,Seg);   
        // 写入上半部分数据
        for (i = 0; i < 16; i++)
        {
          vOLED_Write_Data(CHN_16x16[usIndex].CHN_code[i]);
        }
        // 设置字符下半部分起始位置
        vOLED_Set_Pos(Page + 1,Seg);
        // 写入下半部分数据
        for (i = 0; i < 16; i++)
        {
          vOLED_Write_Data(CHN_16x16[usIndex].CHN_code[i + 16]);
        }
        break;    // 找到指针,退出循环
      }
    }    
  }
}
oled_font.h

字库在之前数组里添加以下:

cpp
{{"正"},{0x00,0x02,0x02,0xC2,0x02,0x02,0x02,0xFE,0x82,0x82,0x82,0x82,0x82,0x02,0x00,0x00,0x40,0x40,0x40,0x7F,0x40,0x40,0x40,0x7F,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x00}},/*"正",0*/
{{"在"},{0x08,0x08,0x88,0xC8,0x38,0x0C,0x0B,0x08,0x08,0xE8,0x08,0x08,0x08,0x08,0x08,0x00,0x02,0x01,0x00,0xFF,0x40,0x41,0x41,0x41,0x41,0x7F,0x41,0x41,0x41,0x41,0x40,0x00}},/*"在",1*/
{{"停"},{0x80,0x60,0xF8,0x07,0x00,0x04,0x74,0x54,0x55,0x56,0x54,0x54,0x74,0x04,0x00,0x00,0x00,0x00,0xFF,0x00,0x03,0x01,0x05,0x45,0x85,0x7D,0x05,0x05,0x05,0x01,0x03,0x00}},/*"停",2*/
{{"止"},{0x00,0x00,0x00,0xF0,0x00,0x00,0x00,0xFF,0x40,0x40,0x40,0x40,0x40,0x00,0x00,0x00,0x40,0x40,0x40,0x7F,0x40,0x40,0x40,0x7F,0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x00}},/*"止",3*/
{{"完"},{0x10,0x0C,0x04,0x24,0x24,0x24,0x25,0x26,0x24,0x24,0x24,0x24,0x04,0x14,0x0C,0x00,0x00,0x81,0x81,0x41,0x31,0x0F,0x01,0x01,0x01,0x7F,0x81,0x81,0x81,0xF1,0x00,0x00}},/*"完",4*/
{{"成"},{0x00,0x00,0xF8,0x88,0x88,0x88,0x88,0x08,0x08,0xFF,0x08,0x09,0x0A,0xC8,0x08,0x00,0x80,0x60,0x1F,0x00,0x10,0x20,0x1F,0x80,0x40,0x21,0x16,0x18,0x26,0x41,0xF8,0x00}},/*"成",5*/
{{"充"},{0x04,0x04,0x84,0xC4,0xA4,0x9C,0x85,0x86,0x84,0x84,0xA4,0xC4,0x84,0x04,0x04,0x00,0x00,0x80,0x80,0x40,0x30,0x0F,0x00,0x00,0x00,0x7F,0x80,0x80,0x81,0xF0,0x00,0x00}},/*"充",6*/
{{"电"},{0x00,0x00,0xF8,0x88,0x88,0x88,0x88,0xFF,0x88,0x88,0x88,0x88,0xF8,0x00,0x00,0x00,0x00,0x00,0x1F,0x08,0x08,0x08,0x08,0x7F,0x88,0x88,0x88,0x88,0x9F,0x80,0xF0,0x00}}
  • 问题

在关闭 Peripheral_3V3_EN 引脚后OLED依然工作,万用表测量后发现还有 2.6~2.8 V电压,所以说明是其他地方漏电了,经过原理图的理论分析,发现在OLED那也涉及到 Peripheral_3V3_EN 引脚,SDA和SCL引脚的输入是接3.3V的,所以如果这两个引脚处于输出模式会导致3.3V漏到 Peripheral_3V3_EN,所以需要在关闭时顺便把这两个引脚设置为输入模式防止漏电,但是这样的话烧进去按键按下后OLED只是闪了一下又显示了,说明代码还有其他地方把它设为输出了,经过检查发现是主循环里一直在执行OLED的显示函数调用到IIC的底层函数,所以我们需要在显示时判断一下如果是电源关闭状态则不显示,打开状态则显示,修改完烧写进去发现还是这样,经过阅读OLED手册(中景园的ZJY130S08Z0WG01)需要OLED初始化时最好上电延时100ms,但是问题还在,还需要上电后把OLED重新初始化一遍,然后顺便恢复IIC接口的设置,因为我们在关闭电源那把IIC的引脚设置为输入了

低功耗

  • 手册阅读

低功耗调试步骤:

  1. 代码框架

  2. 编写CPU进入低功耗,退出低功耗代码

  3. 关闭外设电源,LED灯等,测试电流

  4. 测试硬件,配置GPIO,测试电流

  5. 将待机电流设置为最低

LPM4模式

不需要新建文件

使用外部中断按键唤醒

system.c
cpp
static void StandBy_Enter_Config(void); // 进入待机配置
static void StandBy_Exit_Config(void);  // 退出待机配置

/*
* @function     : vRun
* @param        : None
* @retval       : None
* @brief        : 系统运行
*/
static void vRun(void)
{
  uint16_t Temp_uint  = 0;
  uint16_t i;
  
  //采集电池与太阳能板电压
  ADC.vADC_Get_BAT_Voltage();
  ADC.vADC_Get_VIN_Voltage();
   
  //OLED显示太阳能板电压
  Temp_uint = (uint16_t)(ADC.fVIN_VOltage * 10);
  OLED.vOLED_Show_Char(2,64,Temp_uint / 100 + '0',ASCII_SIZE_16);
  OLED.vOLED_Show_Char(2,72,Temp_uint % 100 / 10 + '0',ASCII_SIZE_16);
  OLED.vOLED_Show_Char(2,80,'.',ASCII_SIZE_16);
  OLED.vOLED_Show_Char(2,88,Temp_uint % 10 + '0',ASCII_SIZE_16);
  OLED.vOLED_Show_Char(2,96,'V',ASCII_SIZE_16);
  
  //OLED显示电池电压
  Temp_uint = (uint16_t)(ADC.fBAT_Voltage * 10);
  OLED.vOLED_Show_Char(4,72,Temp_uint / 10 + '0',ASCII_SIZE_16);
  OLED.vOLED_Show_Char(4,80,'.',ASCII_SIZE_16);
  OLED.vOLED_Show_Char(4,88,Temp_uint % 10 + '0',ASCII_SIZE_16);
  OLED.vOLED_Show_Char(4,96,'V',ASCII_SIZE_16);
  
  // 充电指示
  Power.vCharging_Indicate();
  //间隔500ms采集一次
  for (i = 0; i < 500; i++)
  {
    Public.vDelay_ms(1);
    Key.Key_Detect();     // 按键扫描
    if (Key.Key_Flag == TRUE)
    {
      break;
    }
  }

  if (Key.Key_Flag == TRUE)
  {
    if (Key.Key_Click == TRUE)  // 单击
    {
      printf("系统进入低功耗模式\r\n");
      System.ucSystem_Status = System_StandBy;
    }
    // 清除KEY所有标志位
    Key.Key_Flag = FALSE;
    Key.Key_Click = FALSE;
  }
}

/*
* @function     : vStandBy
* @param        : None
* @retval       : None
* @brief        : 系统待机
*/
static void vStandBy(void)
{
  StandBy_Enter_Config();       // 进入
  
  // 进入低功耗LPM4
  LPM4;
  StandBy_Exit_Config();        // 退出
}
/*
* @function     : StandBy_Enter_Config
* @param        : None
* @retval       : None
* @brief        : 进入待机配置
*/
static void StandBy_Enter_Config(void)
{
  // 关闭LED
  Led.vLed_OFF(LED1);
  Led.vLed_OFF(LED2);
  Led.vLed_OFF(LED3);
  Led.vLed_OFF(LED4);
  
  Power.vBoost_5V_Control(PWR_OFF);     // 关闭升压电源
  Power.vPeripheral_3V3_Control(PWR_OFF);       // 关闭3.3v外设电源
  Pwm.vLED_Power_Control(LED_Power_OFF);        // 关闭大功率LED电源
  BAT_MOS_CLOSE;        // 关闭VAT电压采样电路
  // ADC设置
  ADC12CTL0 &= (~ENC);
  ADC12CTL0 &= (~REFON);        // 关闭基准电压
}

/*
* @function     : StandBy_Exit_Config
* @param        : None
* @retval       : None
* @brief        : 退出待机配置
*/
static void StandBy_Exit_Config(void)
{
  Power.vBoost_5V_Control(PWR_ON);     // 恢复升压电源
  Power.vPeripheral_3V3_Control(PWR_ON);       // 恢复3.3v外设电源  
  BAT_MOS_OPEN; // 恢复
  Pwm.vLED_Power_Control(LED_Power_ON);        // 打开大功率LED电源
  ADC.vADC_Init();
  Public.vDelay_ms(10); // 延时10ms稳定电源
}

LPM3模式

通过定时器B自动唤醒

需要注意的是如果一直复位可能是看门狗的问题,因为喂狗是在定时器A中断里,定时器A使用是时钟是SMLK,所以需要关闭看门狗

  • 定时器B寄存器
  • 程序编写
cpp
/*
* @function     : Sys_Init
* @param        : None
* @retval       : None
* @brief        : 系统初始化
*/
static void vSys_Init(void)
{
  WatchDog.vWatchDog_Init();    // 看门狗初始化
  Hardware_Init.vCLK_Init();    // 时钟初始化
  Hardware_Init.vGPIO_Init();   // GPIO初始化
  TimerA.vTimerA_Init();        // 定时器A初始化
  Hardware_Init.vIE_Init();     // 中断初始化
  USART1.vUSART1_Init();        // 串口1初始化
  Pwm.vPWM_Init();      // PWM初始化
  ADC.vADC_Init();      // ADC初始化
  OLED.vOLED_Init();                //OLED初始化
  TimerB.vTimerB_Init();        // 定时器B初始化
  USART1.vUSART1_SendString("系统初始化完成\r\n");
  printf("PI = %.1f\r\n",3.14);
}

/*
* @function     : vStandBy
* @param        : None
* @retval       : None
* @brief        : 系统待机
*/
static void vStandBy(void)
{
  StandBy_Enter_Config();       // 进入
  
  // 进入低功耗LPM3
  WDTCTL = WDTPW + WDTHOLD;     // 关闭看门狗,避免待机时系统重启
  TBR = 0;      // 当前计数值清0保证固定时间唤醒
  LPM3;
  StandBy_Exit_Config();        // 退出
}

/*
* @function     : StandBy_Enter_Config
* @param        : None
* @retval       : None
* @brief        : 进入待机配置
*/
static void StandBy_Enter_Config(void)
{
  // 关闭LED
  Led.vLed_OFF(LED1);
  Led.vLed_OFF(LED2);
  Led.vLed_OFF(LED3);
  Led.vLed_OFF(LED4);
  
  Power.vBoost_5V_Control(PWR_OFF);     // 关闭升压电源
  Power.vPeripheral_3V3_Control(PWR_OFF);       // 关闭3.3v外设电源
  Pwm.vLED_Power_Control(LED_Power_OFF);        // 关闭大功率LED电源
  BAT_MOS_CLOSE;        // 关闭VAT电压采样电路
  // 管脚配置
  P3SEL &= (~BIT6);     // P36是串口1TX设置为GPIO口输出低电平
  P3OUT &= (~BIT6);
  // ADC设置
  ADC12CTL0 &= (~ENC);
  ADC12CTL0 &= (~REFON);        // 关闭基准电压
}

/*
* @function     : StandBy_Exit_Config
* @param        : None
* @retval       : None
* @brief        : 退出待机配置
*/
static void StandBy_Exit_Config(void)
{
  Power.vBoost_5V_Control(PWR_ON);     // 恢复升压电源
  Power.vPeripheral_3V3_Control(PWR_ON);       // 恢复3.3v外设电源  
  BAT_MOS_OPEN; // 恢复
  Pwm.vLED_Power_Control(LED_Power_ON);        // 打开大功率LED电源
  ADC.vADC_Init();
  // 管脚配置
  P3SEL |= BIT6;        // 恢复串口1TX
  Public.vDelay_ms(10); // 延时10ms稳定电源
}
timerB.h
cpp
#ifndef __TIMERB_H
#define __TIMERB_H
#include <main.h>

typedef struct
{
  void (*vTimerB_Init)(void);
}TimerB_t;

extern TimerB_t TimerB;

#endif
timerB.c
cpp
/***************************************************************************
 * File          : timerB.c
 * Author        : Luckys.
 * Date          : 2023-06-09
 * description   : 定时器B
****************************************************************************/

#include <main.h>

/*====================================static function declaration area BEGIN====================================*/
static void vTimerB_Init(void);
/*====================================static function declaration area   END====================================*/


TimerB_t TimerB = 
{
  vTimerB_Init,
};

/*
* @function     : vTimerB_Init
* @param        : None
* @retval       : None
* @brief        : 定时器初始化
*/
static void vTimerB_Init(void)
{
  // 设置控制寄存器 - ACLK + 8分频 + 向上计数模式
  TBCTL = TASSEL_1 + ID_3 + MC_1;
  // 设置捕获/ 比较寄存器 - 设置周期10s
  // 定时计算:
  // 定时器B时钟 = ACLK/8 = 32768Hz / 8 = 4096Hz
  // Fre = 1 / 10s = 0.1Hz
  // 4096Hz / 0.1Hz = 40960 - 1  
  TBCCR0 = 40960 - 1;
  // 设置捕获/比较控制寄存器 - 默认比较模式, 开启CCIE中断
  TBCCTL0 = CCIE;
}

// 定时器中断函数---10s
#pragma vector = TIMERB0_VECTOR
__interrupt void TimerB_isr(void)
{
  if (System_StandBy == System.ucSystem_Status)
  {
    System.ucSystem_Status = System_Run;
    LPM3_EXIT;  // 退出低功耗
  }
}

RS-485

写在 【协议/模块/算法/通信学习】文章里

整合代码

  • 流程图

注意回滞电压,回滞电压也称作死区电压或迟滞电压,是一段电压范围,在这个范围内输入信号不会被直接传递到输出端,而需要经过一定的处理才能得到输出结果。当输入信号经过一个比较器后输出高电平或低电平时,可能会出现瞬间抖动或杂波,导致输出电平不稳定。加上回滞电压限制电路可以防止这种情况的发生,提高电路的鲁棒性和可靠性。

所以在判断电压时可以加回滞电压,如:

cpp
//回滞电压0.4V
Temp_uchar = (uint8_t)(ADC.fVIN_Voltage*10);
if(Temp_uchar <= 12)
{
    PWM.LED_Brightness_Adjust(LED_Bright_5);
}
else if((Temp_uchar >= 8) && (Temp_uchar <= 22))
{
    PWM.LED_Brightness_Adjust(LED_Bright_4);
}
else if((Temp_uchar >= 18) && (Temp_uchar <= 32))
{
    PWM.LED_Brightness_Adjust(LED_Bright_3);
}
else if((Temp_uchar >= 28) && (Temp_uchar <= 33))
{
    PWM.LED_Brightness_Adjust(LED_Bright_2);
}
else if((Temp_uchar >= 37) && (Temp_uchar <= 42))
  
else if(Temp_uchar >= 38)
  PWM.LED_Brightness_Adjust(LED_Bright_0);

发布时需要改成release模式,编译如果报错头文件那就是头文件路径包含有问题检查一下,然后代码优化要跟debug下设置一样,不然切换了系统自动会把代码优化提到最高这样会出问题

问题

  • 长按复位按键导致MCU死机,原因是OLED那导致,OLED查手册可以知道最小工作电压是不能低于 1.8V

  • 1W灯出现闪灯问题,原因是MSP430单片机默认是输出模式上电,引脚输出不稳定,当出现低电平时导通MOS管就会出现闪灯,解决方法是把GPIO初始化放最开始,设置管脚为高电平,不需要初始化完时钟再初始化管脚