标签 单片机 下的文章

前言

这篇文章是关于ESP32的一个小Demo,名称为 智能垃圾桶

涉及到的模块为 SG90舵机和HC-SR04超声波, 使用的编程烧录软件为 Arduino

一开始打算直接扔在ESP32基础教程 – Echo (liveout.cn)这篇文章里,不做过多介绍,因为这个小Demo只用到了舵机和超声波模块。

后来感觉模块虽少,但是涉及到的知识其实挺多的,有PWM、外部中断(硬件)、硬件定时器和二值信号量,所以就单独写了一篇文章。

如果想要再进化一下,可以尝试添加摄像头模块,进行深度学习,分别垃圾种类,这方面还没涉及到,就不多说了。

如果十分感兴趣,可以参考此篇文章:STEAM案例 | 《智能垃圾桶》项目的设计 - 知乎 (zhihu.com)

PS:这个Demo非本人原创,我加了些注释额外代码,并且整理优化成了此篇文章,相关链接会在文章结尾部分给出。

 

接线图

 

Demo流程

超声波模块每隔200ms发出一次信号进行测距,如果测量到的物体距离在范围内,则信号为 open_semaphore

舵机旋转打开盖子,板载灯变亮,串口打印相关信息。

当打开盖子时,记录打开时间,并启动计时器进行定时检测,即每隔500ms进行检测。

如果检测到盖子关闭时间超过了阈值,则重置打开时间,并设置二值信号量状态为关闭。

得到关闭 close_semaphore 信号后,舵机转动进行关盖。

 

 

 

代码部分

代码需要用到的库: ESP32Servo

主体部分

#include "sonar.h"
#include "cover.h"
#include "servo.h"
int ledPin = 2;//板载灯
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; // 自旋锁

// 打开盖子
void open_cover()
{
  bool shouldAct = false; // 开盖行为
  
  /*
     临界区是一段代码片段,用于在多任务环境下保护共享资源,以确保对资源的访问不会被并发任务中断或干扰。
     临界区的作用是提供一种互斥机制,使得同一时间只有一个任务可以访问共享资源,避免并发访问导致的数据竞争和不一致性。
  */
  portENTER_CRITICAL(&mux); // 进入临界区
  if (openTime == 0) // 打开时间为0时
    shouldAct = true;
  openTime = micros(); // 记录当前打开盖子的时间
  portEXIT_CRITICAL(&mux);  // 离开临界区

  if (shouldAct)
    servo.write(145); // 舵机旋转145,即开盖°
}

void close_cover()
{
  servo.write(0); // 舵机为0°,即关盖
}

void setup() {
  pinMode(ledPin,OUTPUT); //输出模式
  Serial.begin(115200);
  sonar_init(&mux); // 超声波模块初始化
  cover_detect_init(&mux); // 初始化盖子检测相关的设置
  servo_init(); // 舵机初始化
  close_cover(); // 刚开始为盖上盖子状态
}

void loop() {
  // 超声波有信号回来
  if (xSemaphoreTake(open_semaphore, 0) == pdTRUE) // 打开信号为true
  {
    Serial.println("open"); // 串口打印信息
    open_cover();
    digitalWrite(ledPin, HIGH); // 灯亮
  }

  if (xSemaphoreTake(close_semaphore, 0) == pdTRUE) // 关闭信号为true
  {
    Serial.println("close");
    close_cover();
    digitalWrite(ledPin, LOW); // 灯灭
  }
}

关盖处理

cover.h

#pragma once //预处理指令,用于确保头文件只被编译一次

#include <freertos/FreeRTOS.h>
#include <esp32-hal-timer.h>
#include <freertos/semphr.h>

extern volatile unsigned long openTime;
extern volatile SemaphoreHandle_t close_semaphore;

void cover_detect_init(portMUX_TYPE *mux);

cover.cpp

#include "cover.h"

hw_timer_t *cover_timer = NULL; // 定时器
static portMUX_TYPE *_mux = NULL;
volatile SemaphoreHandle_t close_semaphore; // 关盖信号量
volatile unsigned long openTime = 0; // 打开盖子的时间

// 中断服务程序ISR:检测盖子是否关闭
void IRAM_ATTR close_detect()
{
  portENTER_CRITICAL_ISR(_mux);
  auto now = micros();
  if (openTime != 0 && (now - openTime) >= 4000000) // 打开盖子时间大于等于4s则关闭
  { 
    openTime = 0;
    xSemaphoreGiveFromISR(close_semaphore, NULL);
  }
  portEXIT_CRITICAL_ISR(_mux);
}

void cover_detect_init(portMUX_TYPE *mux)
{
  _mux = mux;
  close_semaphore = xSemaphoreCreateBinary();

  // 检测到关闭部分,0.5秒检测一次
  cover_timer = timerBegin(2, 80, true);  // 初始化计时器2,分频系数80,使能中断
  timerAttachInterrupt(cover_timer, close_detect, true);  // 附加中断处理函数 close_detect 到计时器
  timerAlarmWrite(cover_timer, 500000, true);  // 设置计时器的定时时间为500000微秒(0.5秒),并使能重复触发
  timerAlarmEnable(cover_timer);  // 启动计时器
}

舵机模块

servo.h

#pragma
#include <ESP32Servo.h>

extern Servo servo;

void servo_init();

servo.cpp

#include "myservo.h"

// 舵机部分
Servo servo;
int minUs = 500;
int maxUs = 2500;
int servoPin = 13;

void servo_init()
{
  // 舵机
  ESP32PWM::allocateTimer(1);
  servo.setPeriodHertz(50);
  servo.attach(servoPin, minUs, maxUs);
}

超声波模块

sonar.h

#pragma once
#include <freertos/FreeRTOS.h>
#include <esp32-hal-timer.h>
#include <freertos/semphr.h>

extern volatile SemaphoreHandle_t open_semaphore; // 信号量
void sonar_init(portMUX_TYPE *mux);

sonar.cpp

#include "sonar.h"

volatile SemaphoreHandle_t open_semaphore; // 信号量

// 超声波测距部分
const int trigPin = 17; 
const int echoPin = 18;
int distance = 0;

static portMUX_TYPE *_mux = NULL;
hw_timer_t *sonar_timer = NULL; // 定时器
volatile unsigned long startTime = 0; // 发出超声波时间
volatile unsigned long endTime = 0; // 收到超声波时间

// 硬件定时器ISR
void IRAM_ATTR ping()
{
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(15);
  digitalWrite(trigPin, LOW);
}

// ECHO 引脚ISR
void IRAM_ATTR changeISR() 
{
  auto now = micros(); // 当前时间
  auto state = digitalRead(echoPin);

  portENTER_CRITICAL_ISR(_mux);
  if (state) // 高电平,即刚发出超声波
    startTime = now;
  else
    endTime = now;
// 变成低电平时表示已经收到回声
// 如果 < 10cm 就发信号开盖
  if (!state) {
    auto t = endTime - startTime;
    auto dis = t * 0.01715;
    if (dis <= 10)
    {
      xSemaphoreGiveFromISR(open_semaphore, NULL); // 给一个开盖信号量发送信号
    }
  }
  portEXIT_CRITICAL_ISR(_mux);
}

void sonar_init(portMUX_TYPE* mux)
{
  _mux = mux;
  pinMode(trigPin, OUTPUT);
  pinMode(echoPin, INPUT);
  open_semaphore = xSemaphoreCreateBinary();

  //测距定时器部分
  sonar_timer = timerBegin(0, 80, true);
  timerAttachInterrupt(sonar_timer, ping, true);
  timerAlarmWrite(sonar_timer, 200000, true); // 定时时间为 0.2s

  // echo引脚的中断
  attachInterrupt(digitalPinToInterrupt(echoPin), changeISR, CHANGE);

  // 开始周期测量
  timerAlarmEnable(sonar_timer);
}

相关链接

Demo教程视频:ESP32之智能垃圾桶制作讲解—哔哩哔哩_bilibili

GitHub仓库:https://github.com/PGwind/esp32project

ESP32代码记录:ESP32基础教程 – Echo (liveout.cn)

PS:写程序难免会有问题,而且国内的ChatGPT访问受限

这里就给个朋友搭建的链接吧:chuanwen智能

 

--> 前言这篇文章是关于ESP32的一个小Demo,名称为 智能垃圾桶涉及到的模块为 SG90舵机和HC-SR04超声波, 使用的编程烧录软件为 Arduino。一开始打算直接扔在ESP32基础教程 – Echo (liveout.cn)这篇文章里,不做过多介绍,因为这个小Demo只用到了舵机和超声波模块。后来感觉模块虽少,但是涉及到的知识其实挺多的,有PWM、外部中断(硬件)、硬件定时器和二值...

前言

此篇文章为有关 ESP32 的学习期间的代码记录,并且加上了自己的注释,非教学文章。

使用开发板全称ESP32 DEVKILTv1(devkitv1) ,搭载芯片为 ESP32D0WDQ6,使用软件为 Arduino

 

 

参考链接

如果是小白并且想要学习单片机相关知识,建议移步此篇文章:51单片机入门教程(上篇)(代码+个人理解) – Echo (liveout.cn)

此篇文章参考教程视频:小鱼创意的个人空间哔哩哔哩bilibili

GitHub代码样例链接:https://github.com/PGwind/ESP32code

开发板详细讲解:ESP32 DEVKILTv1(devkitv1)开发板全解析

 

1. 点亮LED

1.1 点亮第一个LED

int ledPin = 2; //定义引脚,一般为板载蓝色灯

void setup() {
  // put your setup code here, to run once:
  pinMode(ledPin,OUTPUT); //输出模式
}

void loop() {
  // put your main code here, to run repeatedly:
  digitalWrite(ledPin, HIGH); //引脚高电平,即等效于 digitalWrite(ledPin, 1);
}

1.2 LED闪烁

int ledPin = 2;

void setup() {
  pinMode(ledPin,OUTPUT);

}

void loop() {
  digitalWrite(ledPin, HIGH);
  delay(2000); //延迟
  digitalWrite(ledPin, LOW);
  delay(2000);
}

1.3 不同闪烁周期LED

int ledPin2 = 2; 
int ledStatus2 = 0;  //现在的状态
unsigned int prevTime2 = 0; //改变状态时的时间

int ledPin4 = 4;
int ledStatus4 = 0;  
unsigned int prevTime4 = 0;

void setup() {
  pinMode(ledPin2, OUTPUT);
  digitalWrite(ledPin2, HIGH);
  ledStatus2 = HIGH;
  prevTime2 = millis(); //millis(): 本程序已经运行的时间(ms) micros()微秒us

  pinMode(ledPin4, OUTPUT);
  digitalWrite(ledPin4, HIGH);
  ledStatus4 = HIGH;
  prevTime4 = millis(); //millis(): 本程序已经运行的时间(ms) micros()微秒us
}

void loop() {
  unsigned int now = millis(); //程序运行的时间

  if (now - prevTime2 > 3000) //上次改变状态后已经过了3s
  {
    int status  = ledStatus2 == HIGH ? LOW: HIGH;
    digitalWrite(ledPin2, status);
    ledStatus2 = status;
    prevTime2 = now;
  }

  if (now - prevTime4 > 1000) //上次改变状态后已经过了1s
  {
    int status  = ledStatus4 == HIGH ? LOW: HIGH;
    digitalWrite(ledPin4, status);
    ledStatus4 = status;
    prevTime4 = now;
  }
}

 

2. 按键

2.1 按键控制LED

int switchPin = 25; //按键所接GPIO口
int ledPin = 4; //LED接口
int ledStatus = 0; //LED目前状态

void setup() {
  pinMode(switchPin, INPUT_PULLUP);//INPUT_PULLUP上拉,低电平有效,检测到低电平表明按键已经按下
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, HIGH);
  ledStatus = HIGH;
}

void loop() {
  int val = digitalRead(switchPin); //读取开关引脚的电平状态
  if (val == LOW) //低电平有效
  {
    ledStatus = !ledStatus;
    digitalWrite(ledPin, ledStatus);
  }
}

2.2 软件消除抖

使用 RBD_ Button 库进行消抖,在 库管理 处进行安装

#include <RBD_Timer.h>
#include <RBD_Button.h>

int switchPin = 25;
int ledPin = 4;
int ledStatus = 0;

//创建一个可以消除拉动的按键对象
RBD::Button button(switchPin, INPUT_PULLUP);

void setup() {
  pinMode(ledPin, OUTPUT);
  button.setDebounceTimeout(20); // 消除抖动时间是20ms
}

void loop() {
  //检测按键按下去的事件(下降沿)
  if (button.onPressed()) //按键已经按下
  {
    ledStatus = !ledStatus;
    digitalWrite(ledPin, ledStatus);
  }
}

 

3. PWM

LED控制(LEDC)外围设备主要用于控制LED的强度,尽管它也可以用于生成PWM信号用于其他目的。它具有16个通道,可以生成独立的波形,这些波形可以用于驱动RGB LED器件。

3.1 LEDC(PWM)

void setup() {
  int ret = 0; //状态
  Serial.begin(115200);
  int ch0 = 0; //通道
  int gpio4 = 4; //引脚
  ret = ledcSetup(ch0, 5000, 12); //设置ledc通道0,频率5000HZ,精度12 

  delay(200);
  if (ret == 0)
    Serial.println("Error Setup");
  else 
    Serial.println("Success Setup");
    
  ledcAttachPin(gpio4, ch0); //设置引脚和通道
  ledcWrite(ch0, pow(2, 11)); //占空比50%   2^11 / 2^12 = 1/2 

}

void loop() {
  // put your main code here, to run repeatedly:

}

3.2 LED呼吸灯

每秒钟固定调整占空比 50 次。 T 为呼吸周期,光从灭到最亮经过半个周期T/2。

半个周期进行 50*T/2 调整占空比

count 表示占空比为 100% 时等分的格子

step 为每次调整时要加上的增量 step = count / (50 * T/2) = 2 * count / (50 * T)

3.2.1 使用 delay() ,呼吸周期偏长

/* 每秒钟固定调整占空比50次。T为呼吸周期,光从灭到最亮经过半个周期T/2。
   半个周期进行 50*T/2 调整占空比
   count表示占空比为100%时等分的格子
   step为每次调整时要加上的增量  step = count / (50 * T/2) = 2 * count / (50 * T)
*/

int gpio4 = 4;
int ch1 = 1; //ledc通道号
int duty = 0; //目前信号的占空比
int count = 0; //100%占空比时的格子
int step = 0; //占空比步进值(增量)
int breathTime = 3; //呼周期,单位s

void setup() {
  ledcSetup(ch1, 1000, 12); //建立ledc通道
  count = pow(2, 12); //计算占空比为100%时共几份
  step = 2 * count / (50 * breathTime); //计算一次增加多少格子
  ledcAttachPin(gpio4, ch1); //绑定 ch1 和 GPIO4
}

void loop() {
  ledcWrite(ch1, duty);
  duty += step;
  if (duty > count)
  {
    duty = count;
    step = -step; //step变为负数,duty递减
  }
  else if (duty < 0)
  {
    duty = 0;
    step = -step; //step变为正数,duty递增
  }
  delay(20); //阻塞20ms
}

3.2.2 使用 millis

int prevTime = 0;
int gpio4 = 4;
int ch1 = 1; //ledc通道号
int duty = 0; //目前信号的占空比
int count = 0; //100%占空比时的格子
int step = 0; //占空比步进值(增量)
int breathTime = 3; //呼周期,单位s

void setup() {
  ledcSetup(ch1, 1000, 12); //建立ledc通道
  count = pow(2, 12); //计算占空比为100%时共几份
  step = 2 * count / (50 * breathTime); //计算一次增加多少格子
  ledcAttachPin(gpio4, ch1); //绑定 ch1 和 GPIO4
}

void loop() {
  int now =  millis(); 
  if (now - prevTime >= 20) //上次改变状态后已经过了 20ms
  {
    ledcWrite(ch1, duty);
    duty += step;
    if (duty > count)
    {
      duty = count;
      step = -step;
    } 
    else if (duty < 0)
    {
      duty = 0;
      step = -step;
    }
    prevTime = now;
  }
}

 

4. 软件定时器

使用 AsyncTimer 库进行定时操作,在 库管理 处进行安装。

定时器主要模式:

  1. 等待多长时间触发一个事件
  2. 每个多久时间触发一个事件
  3. 到某个时间点触发一个事件

定时器类型:

  1. 硬件定时器:ESP32只有4个
  2. 软件定时器:精度低,数量多

4.1 单次定时任务

串口定时打印信息

#include <AsyncTimer.h>

AsyncTimer t; //定义一个定时器

void myfun()
{
  Serial.println("the second");
}
void setup() {
  Serial.begin(115200);
  delay(200);
  
  //setTimeout(回调函数, 超时时间(ms)),回调函数可以无参无返回值
  auto id = t.setTimeout([](){ //第一个单次定时任务:2s 打印 the first
    Serial.println("the first");
  }, 2000);
  Serial.print("First:");
  Serial.println(id);

  id = t.setTimeout(myfun, 4000); //第二个单次定时任务:4s 打印 the second
  Serial.print("Second:");
  Serial.println(id);
}

void loop() {
  t.handle(); //执行有关定时器的操作,精度与loop()函数里面操作时间有关
}
First:62510
Second:36048
the first
the second

4.2 周期定时任务

#include <AsyncTimer.h>

AsyncTimer t;

void myfun()
{
  Serial.println("the second");
}

void setup() {
  Serial.begin(115200);
  delay(200);
  
  //setInterval(回调函数, 超时时间(ms)),回调函数可以无参无返回值
  auto id = t.setInterval([](){ //第一个周期定时任务:每 2s 打印 the first
    Serial.println("the first");
  }, 2000);
  Serial.print("First:");
  Serial.println(id);

  id = t.setInterval(myfun, 4000); //第二个周期定时任务:每 4s 打印 the second
  Serial.print("Second:");
  Serial.println(id);
}

void loop() {
  t.handle();
}
the first
the first
the second

4.3 闪烁LED改造

LED灯刚启动以1s周期进行闪烁,按键按下去后在1s和3s的周期进行切换

// LED灯刚启动以1s周期进行闪烁,按键按下去后在1s和3s的周期进行切换
#include <RBD_Button.h>
#include <AsyncTimer.h>

int switchPin = 25; // 按钮
int ledPin = 4; // led
int ledStatus = HIGH;
int t = 1; // 闪烁周期

// 软件消抖
RBD::Button button(switchPin, INPUT_PULLUP);

AsyncTimer timer;
int taskId = 0;

void ChangeLedStatus() // 改变LED状态函数
{
  ledStatus = !ledStatus; // 状态取反
  digitalWrite(ledPin, ledStatus); // 改变状态
}

void setup() {
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin, HIGH); // 点亮
  button.setDebounceTimeout(20);
  // 创建周期任务
  taskId = timer.setInterval(ChangeLedStatus, t*1000);
}

void loop() {
  timer.handle();

  if (button.onPressed())
  {
    t = t == 1?3:1; // 周期定时时间为:1s或3s
    timer.changeDelay(taskId, t*1000);
  }
}

4.4 相关函数讲解

  1. 停止单个定时任务:

    cancel(intervalOrTimeoutId) ,intervalOrTimeoutid即定时任务的编号

  2. 停止多个定时任务:

    cancelAll(includeIntervals) ,参数默认值为true。

    true:取消所有定时任务 fasle:只取消单次定时任务

  3. 改变定时任务周期:

    changeDelay(intervalOrTimeoutId, delaylnMs) ,参数分别为定时任务的编号 和 新的超时时间(ms)

  4. 重置定时任务:

    reset(intervalOrTimeoutId),只能重置还没有停止的定时任务,重置完从0重新计时

  5. 额外延时一个定时任务:

    delay(intervalOrTimeoutId, delaylnMs) ,参数分别为定时任务的编号 和 额外延时时间(ms)

  6. 获取定时任务剩余时间:

    getRemaining(intervalOrTimeoutId) ,获取指定定时任务本轮还剩多久时间超时

    unsigned long remaining = getRemaining(timeoutId);
    

     

5. ADC模数转换

5.1 样例

void setup() {
  Serial.begin(115200);
  analogReadsolution(12); // 设置读取精度(位宽)

  //设置通道衰减值(不设置默认为11db)
  /*
  analogSetAttenuation(ADC_ATTEN_DB_11); // 设置所有通道
  analogSetPinAttenuation(2, ADC_ATTEN_DB_11); // 设置指定GPIO口的衰减值
  */
}

void loop() {
  int analogValue = analogRead(2); // 读取DAC值
  int analogVolts = analogReadMilliVolts(2); // 读取电压值(c)

  Serial.printf("ADC analog value = %d\n", analogValue);
  Serial.printf("ADC millivolts value = %d\n", analogVlots);

  delay(100);
}

5.2 电位器控制LED亮度

/* ADC + LEDC + 定时器(软件)
  通过更改定位器阻值控制LED亮度
*/
#include <AsyncTimer.h>

int pmPin = 32; // 电位器GPIO接口
int ledPin = 4; // LED
int ch0 = 0; // ledc通道

AsyncTimer timer;
int taskId = 0;

void ChangeLedLightness()
{
  int val = analogRead(pmPin);
  Serial.printf("%d:", val);

  auto vol = analogReadMilliVolts(pmPin);
  Serial.println(vol);

  int duty = val / 4095.0 * 1024;
  ledcWrite(ch0, duty);
}

void setup() {
  Serial.begin(115200);
  analogReadResolution(12); // 确定analogRead() 函数返回的值的分辨率(以位为单位)
  analogSetAttenuation(ADC_11db); // 设置所有通道衰减值

  ledcSetup(ch0, 1000, 10); // 设置ledc通道0,频率1000HZ,精度10
  ledcAttachPin(ledPin, ch0); 

  taskId = timer.setInterval(ChangeLedLightness, 20); //周期定时任务
}

void loop() {
  timer.handle();
}

 

6. I2C协议

6.1 I2C及Wire库使用

主机

// 主机Master
#include <Wire.h>

#define I2C_DEV_ADDR 0x55  // I2C设备地址

uint32_t i = 0;

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true); // 启用串口调试输出
  Wire.begin(); // 初始化I2C总线
}

void loop() {
  delay(5000);

  Wire.beginTransmission(I2C_DEV_ADDR); // 开始I2C传输
  Wire.printf("Hello World! %u", i++); // 向I2C设备发送数据
  uint8_t error = Wire.endTransmission(true); // 结束I2C传输并检查错误
  Serial.printf("endTransmission:%u\n", error);

  uint8_t bytesReceived = Wire.requestFrom(I2C_DEV_ADDR, 16); // 从I2C设备读取数据并返回接收到的字节数
  Serial.printf("requestFrom:%u\n", bytesReceived);
  if ((bool)bytesReceived) {
    uint8_t temp[bytesReceived];
    Wire.readBytes(temp, bytesReceived); // 读取接收到的字节
    log_print_buf(temp, bytesReceived); // 打印接收到的数据
  }
}

从机

// 从机Slave
#include "Wire.h"

#define I2C_DEV_ADDR 0x55

uint32_t i = 0;

/*
  onRequest()函数:用于处理主机的请求,在每次请求时,
  向主机发送递增的数据包计数,并打印调试信息。
*/
void onRequest(){
  Wire.print(i++);
  Wire.print("Packets.");
  Serial.println("onRequest");
}

//  onReceive()函数:用于处理主机发送的数据,在接收到数据时,打印接收到的数据内容和长度。
void onReceive(int len){
  Serial.printf("onReceived[%d]: ", len);
  while (Wire.available()){
    Serial.write(Wire.read());
  }
  Serial.println();
}

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Wire.onReceive(onReceive); // 注册接收回调函数
  Wire.onRequest(onRequest); // 注册请求回调函数
  Wire.begin((uint8_t)I2C_DEV_ADDR); // 初始化I2C从机

  // 如果是ESP系列芯片,可以使用slaveWrite函数发送初始消息
#if CONFIG_IDF_TARGET_ESP#@
  char message[64];
  snprintf(message, 64, "%u Packets.", i++);
  Wire.slaveWrite((uint8_t *)message, strlen(message));
#endif
}

void loop() {

}

6.2 ESP32双机通信

主机每秒2秒向从机发送递增的数字,

从机在收到主机的数据后LED闪烁0.5秒,并在收到的数字后加上OK字符发送给主机

主机收到从机发来的数据后打印在串口上

主机

主机程序使用了Wire库进行I2C通信。在setup函数中,初始化串口并加入I2C总线。在loop函数中,通过Wire.beginTransmissionWire.endTransmission向从机发送数字字符串,并通过Wire.requestFrom从从机接收数据。收到数据后,将其打印在串口上。

/* 
  主机每秒2秒向从机发送递增的数字,
  从机在收到主机的数据后LED闪烁0.5秒,并在收到的数字后加上OK字符发送给主机
  主机收到从机发来的数据后打印在串口上
*/

// 主机程序
#include <Wire.h>

int num = 1; // 发送给从机
int address = 33; // 从机地址

void setup() {
  Serial.begin(115200);

  if (Wire.begin()) // 主机加入I2C总线
    Serial.println("I2C Success");
  else 
    Serial.println("I2C Failed");
}

void loop() {
  char tmp[32];
  itoa(num++, tmp, 10); // 将数字转换成字符串

  Wire.beginTransmission(address);
  Wire.write(tmp); // 传输数字字符串
  int ret = Wire.endTransmission();
  if (ret != 0) // 判断状态
  {
    Serial.printf("Send failed:%d\r\n", ret);
    return;
  }

  delay(100); // 从机处理时间
    
  /*
  	Wire.requestFrom(address, quantity, stop);
  	requestFrom返回值代表了从机发来多少字节的数据,实际上是错误的,
  	返回值永远是等于你传进去的欲读取数据的数量值(quantity)
  	若 接收的数据量 > 从机发送的数据量,超出部分全部为 0x3f
  */
  int len = Wire.requestFrom(address, 32); // 发出请求,最多不超过32字节
  if (len > 0)
  {
    // 打印出来收到从机发来的数据
    Serial.print("Receive data size:");
    Serial.println(len);

    Wire.readBytes(tmp, 32);
    Serial.println(tmp);

    // 打印出收到数据的16进制值
    for (int i=0; i<32; i++)
    {
      Serial.printf("%2x, ", tmp[i]);
      if (i % 8 == 7)
        Serial.println();
    }
    Serial.println();
  }
  delay(1900);
}

从机

从机程序使用了Wire库进行I2C通信,并使用AsyncTimer库来控制LED闪烁。在onReceive函数中,当接收到数据时,将数据存储到缓冲区buf中,并让LED闪烁。在onRequest函数中,向主机发送带有"OK"字符的数据。

/* 
  主机每秒2秒向从机发送递增的数字,
  从机在收到主机的数据后LED闪烁0.5秒,并在收到的数字后加上OK字符发送给主机
  主机收到从机发来的数据后打印在串口上
*/

// 从机程序
#include <Wire.h>
#include <AsyncTimer.h>

char buf[32]; // 接受缓冲区
int ledPin = 4;

AsyncTimer timer;

void onReceive(int len) {
  // 接受数据,将数字存到缓冲区,并让led闪烁
  if (len > 0)
  {
    // 从I2C总线读取最多32个字节的数据,并将其存储到buf缓冲区中。函数返回实际读取到的字节数
    int sz = Wire.readBytes(buf, 32);
    if (sz > 0)
    {
      buf[sz] = 0;
      digitalWrite(ledPin, HIGH);

      // 注册定时事件,500ms后关闭led灯
      timer.setTimeout([](){
        digitalWrite(ledPin, LOW);
      }, 500);
    }
  }
}

void onRequest() {
   // 向主机发送数据
  strcat(buf, "OK"); // 拼接
  Wire.write(buf); // 发送缓冲区数据(包括"OK"字符)
  Wire.write(0);
}
void setup() {
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT); 
  Wire.onReceive(onReceive); // 注册接受事件
  Wire.onRequest(onRequest); // 注册发送事件
  Wire.begin(33);
}

void loop() { 
 timer.handle();
}

6.3 I2C操控1602LCD

需要下载 LiquidCrystal_I2C 库,地址为:https://github.com/mrkaleArduinoLib/LiquidCrystal_I2C

主要用到的文件为 LiquidCrystal_I2C.hLiquidCrystal_I2C.cpp 这两个文件

使用时移动到项目文件根目录并调用

#include <Arduino.h>
#include <Wire.h>
#include "LiquidCrystal_I2C.h"

LiquidCrystal_I2C lcd(0x27, 16, 2); // LiquidCrystal_I2C lcd(显示器地址, 行数, 列数);

void setup() {
  lcd.init(); // 初始化 LCD 显示器
  lcd.backlight(); // 打开背光
  lcd.print("Hello World!"); // 在第一行打印 "Hello World!"
  // lcd.setCursor(列号, 行号)
  lcd.setCursor(0, 1); // 设置光标位置为第二行第一列
  lcd.print("I am a fish, I am a fish, I am a fish."); // 在第二行打印 "I am a fish, I am a fish, I am a fish."

  // 将第二行的 "am" 改成大写 "AM"
  lcd.setCursor(2, 1); // 设置光标位置为第二行第三列
  lcd.write('A'); // 写入大写字母 'A'
  lcd.write('M'); // 写入大写字母 'M'

  lcd.clear(); // 清空显示器

  // 字幕不停向左滚动
  for (int i = 0; i < 100; i++) {
    lcd.scrollDisplayLeft(); // 向左滚动显示内容
    delay(1000); // 延迟1秒
  }
}

void loop() {

}

 

7. 外部中断(硬件)

中断服务程序要求:

  • 要尽量地短,减少执行时间
  • 不要使用 delay() 函数
  • 不要使用 Serial 打印
  • 和主程序共享的变量要加_上 volatile 关键字
  • 不要使用 millis() 函数,它的值将不会增长
  • 可以使用 micros 函数来获取时间
  • 外部中断最高频率手册没说,但达到几M是没有问题的

7.1 按键开关LED

IRAM_ATTR 是一个ESP32的特殊属性,用于指定函数在IRAM(内部RAM)中运行,而不是默认的闪存(Flash)中运行。在ESP32中,IRAM是位于处理器内部的高速随机访问存储器,执行速度更快。

使用 IRAM_ATTR 属性可以将函数加载到IRAM中,从而提高函数的执行速度和响应性能。在中断服务程序(ISR)中使用 IRAM_ATTR 属性可以确保ISR在最短的时间内得到执行,从而更及时地响应中断事件。

因此,IRAM_ATTR 修饰符常常用于将中断服务程序(ISR)函数加载到IRAM中,以提高性能。

const byte LED = 4;
const byte BUTTON = 25;

// ISR
IRAM_ATTR void switchPressed()
{
  // 按钮松开高电平亮,按钮按下低电平灭
  if (digitalRead(BUTTON) == HIGH)
    digitalWrite(LED, HIGH);
  else 
    digitalWrite(LED, LOW);
}

void setup() {
  pinMode(LED, OUTPUT);
  pinMode(BUTTON, INPUT_PULLUP);
  // 设置和执行ISR(中断服务程序)
  attachInterrupt(digitalPinToInterrupt(BUTTON), switchPressed, CHANGE);
}

void loop() {

}

7.2 简单PWM测量仪

临界区 是一段代码片段,用于在多任务环境下保护共享资源,以确保对资源的访问不会被并发任务中断或干扰。临界区的作用是提供一种互斥机制,使得同一时间只有一个任务可以访问共享资源,避免并发访问导致的数据竞争和不一致性。

#include "LiquidCrystal_I2C.h" // 包含 LiquidCrystal_I2C 库,用于LCD显示器
// 共享变量
volatile unsigned long raiseTime = 0; // 前一次上升沿时间
volatile unsigned long fallTime = 0; // 前一次下降沿时间
volatile double duty = 0; // 占空比
volatile double fre = 0; // 频率

int pwmPin = 27; // 信号输入接口

// 显示器初始化
LiquidCrystal_I2C lcd(0x27, 16, 2);

// 自旋锁
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;

// ISR:中断服务程序
void changeISR()
{
  auto now = micros();
  if (digitalRead(pwmPin)) // 现在是高
  {
    /*
     临界区是一段代码片段,用于在多任务环境下保护共享资源,以确保对资源的访问不会被并发任务中断或干扰。
     临界区的作用是提供一种互斥机制,使得同一时间只有一个任务可以访问共享资源,避免并发访问导致的数据竞争和不一致性。
    */
    portENTER_CRITICAL_ISR(&mux); // 进入临界区
    auto total = now - raiseTime; // 周期 us
    fre = 1e6 / (double)total; // 频率
    auto h = fallTime - raiseTime; // 脉宽
    duty = h / (double)total; // 占空比 = 脉宽 / 周期
    portEXIT_CRITICAL_ISR(&mux); // 离开临界区
    raiseTime = now;
  }
  else
  {
    fallTime = now;
  }
}

void setup() { 
 lcd.init(); // 初始化 LCD 显示器
  lcd.backlight(); // 打开背光
  lcd.setCursor(0, 0); // 设置光标位置为第一行第一列
  lcd.print("fre: "); // 在 LCD 上打印 "fre: "
  lcd.setCursor(0, 1); // 设置光标位置为第二行第一列
  lcd.print("duty: "); // 在 LCD 上打印 "duty: "
  pinMode(pwmPin, INPUT); // 将 pwmPin 设置为输入模式
  attachInterrupt(digitalPinToInterrupt(pwmPin), changeISR, CHANGE); // 注册中断服务程序来响应 pwmPin 引脚状态变化的事件
}

void loop() {
  delay(1000); // 延迟1秒

  portENTER_CRITICAL(&mux); // 进入临界区
  double f = fre; // 读取频率值
  double d = duty; // 读取占空比值
  portEXIT_CRITICAL(&mux); // 离开临界区

  lcd.setCursor(5, 0); // 设置光标位置为第一行第五列
  lcd.print(f); // 在 LCD 上打印频率值
  lcd.setCursor(6, 1); // 设置光标位置为第二行第六列
  lcd.print(d); // 在 LCD 上打印占空比值
} 

 

8. 硬件定时器及二值信号量

分频数越大,周期越长,频率越低。分频数最大是 65525

流程 :初始化 -> 绑定ISR -> 设置触发ISR的计数值 -> 启动定时器

硬件定时器流程

#include <esp32-hal-timer.h>

hw_timer_t *timer = NULL;

void IRAM_ATTR timerISR() {
  // 硬件定时器中断服务程序
}

void setup() {
  timer = timerBegin(0, 80, true); // 创建硬件定时器,使用定时器 0,预分频因子 80,设置为自动重载模式
  timerAttachInterrupt(timer, &timerISR, true); // 将定时器中断服务程序与硬件定时器关联
  timerAlarmWrite(timer, 1000000, true); // 设置定时器定时周期为 1 秒,自动重载,即周期循环
  timerAlarmEnable(timer); // 启用定时器定时中断"
  // timerEnd(timer); // 结束
}

void loop() {
  // 主循环代码
}

8.1 硬件定时器样例

每 1s 打印一次当前迭代数和时间

// 每 1s 打印一次当前迭代数和时间
#include <esp32-hal-timer.h>
volatile int count = 0;
volatile unsigned long tim = 0;

hw_timer_t *timer1 = NULL; // 1s 1次
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

// ISR
void IRAM_ATTR onTimer1() {
  portENTER_CRITICAL_ISR(&timerMux); // 进入临界区
  count ++;
  tim = micros();
  portEXIT_CRITICAL_ISR(&timerMux); // 离开临界区
}

void setup() {
  Serial.begin(115200);
  // 初始化定时器,80分频,1us计数一次
  timer1 = timerBegin(0, 80, true);
  // 附加中断
  timerAttachInterrupt(timer1, onTimer1, true);
  // 计数到 1000000(1s) 时触发中断
  timerAlarmWrite(timer1, 1000000, true);
  // 开启定时器
  timerAlarmEnable(timer1);
}

void loop() {
  portENTER_CRITICAL(&timerMux);
  auto c = count;
  auto t = tim;
  portEXIT_CRITICAL(&timerMux);

  Serial.println(c);
  Serial.println(t);
}

loop() 函数 中的 portENTER_CRITICAL(&timerMux) 会启用自旋锁,并且禁用掉了CPU的中断。

loop()函数执行速度很快,中断被屏蔽时间会非常长,外部如果有两个或以上中断进来无法及时检测到。

想要解决这个问题,这时候就需要 二值信号量了。

8.2 二值信号量

// 每 1s 打印一次当前迭代数和时间
#include <esp32-hal-timer.h>
volatile int count = 0;
volatile unsigned long tim = 0;
volatile SemaphoreHandle_t timerSemaphore; // 信号量

hw_timer_t *timer1 = NULL; // 1s 1次
portMUX_TYPE timerMux = portMUX_INITIALIZER_UNLOCKED;

// ISR
void IRAM_ATTR onTimer1() {
  portENTER_CRITICAL_ISR(&timerMux); // 进入临界区
  count ++;
  tim = micros();
  portEXIT_CRITICAL_ISR(&timerMux); // 离开临界区

  /*
    从中断服务程序(ISR)中给予一个二值信号量它会将二值信号量的计数值增加,
    并唤醒等待该信号量的任务。第二个参数为 NULL 表示不需要唤醒任何任务。
  */
  // 设置完共享变量后发送信号 
  xSemaphoreGiveFromISR(timerSemaphore, NULL);
}

void setup() {
  Serial.begin(115200);

  timerSemaphore = xSemaphoreCreateBinary(); // 创建一个二值信号量
  // 初始化定时器,80分频,1us计数一次
  timer1 = timerBegin(0, 80, true);
  // 附加中断
  timerAttachInterrupt(timer1, onTimer1, true);
  // 计数到 1000000(1s) 时触发中断
  timerAlarmWrite(timer1, 1000000, true);
  // 开启定时器
  timerAlarmEnable(timer1);
}

void loop() {
  if (xSemaphoreTake(timerSemaphore, 0) == pdTRUE)
  {
    portENTER_CRITICAL(&timerMux);
    auto c = count;
    auto t = tim;
    portEXIT_CRITICAL(&timerMux);

    Serial.println(c);
    Serial.println(t);
  }
  
}

 

9. 超声波测距

HC-SR04 模块测量错误的情况:

  1. 物体体积太小,无法反射超声波
  2. 物体在探头15°范围之外
  3. 物体表面材质是吸收超声波的,比如毛绒绒的物体
  4. 物体与探头的夹角不对

9.1 距离测量

const int trigPin = 4;
const int echoPin = 16;

void setup() {
  Serial.begin(115200);
  delay(200);
  pinMode(trigPin, OUTPUT);
  pinMode(echoPin, INPUT);
}

void loop() {
  // 在Trig引脚发送15us脉冲
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(15); // 15us
  digitalWrite(trigPin, LOW);

  // 读取Echo引脚脉冲时长
  auto t = pulseIn(echoPin, HIGH);
  double dis = t * 0.01715; // 单位:CM
  Serial.print(dis);
  Serial.println(" cm");

  delay(200);
}

此程序阻塞过长,下面将使用中断方式优化

9.2 距离测量(中断优化)

中断测距原理:

  • 外部中断(change) 附加到 ECHO 的引脚上
  • 使用硬件定时器每 500msTrigger 一个 15us 的脉冲 (1s测量2次)
  • 在上升沿中断的时候记当前时间 t1
  • 在下降沿中断的时候记当前时间 t2,并发 信号(Semaphore) 给任务
  • Loop函数在收到信号后获取 t2和t1 的值,并计算出距离
// 中断测距
/* - 将 外部中断(change) 附加到 ECHO 的引脚上
- 使用硬件定时器每 500ms 给 Trigger 一个 15us  的脉冲 (1s测量2次)
- 在上升沿中断的时候记当前时间 t1 
- 在下降沿中断的时候记当前时间 t2,并发 信号(Semaphore)`  给任务
- Loop函数在收到信号后获取 t2和t1 的值,并计算出距离
*/

const int trigPin = 4;
const int echoPin = 16;
double distance = 0; // 单位cm

hw_timer_t *timer1 = NULL; // 定时器
portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; // 自旋锁
volatile unsigned long startTime = 0; // 发出超声波时间
volatile unsigned long endTime = 0; // 收到超声波时间
volatile SemaphoreHandle_t semaphore; // 信号量

// 硬件定时器ISR
void IRAM_ATTR ping()
{
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(15);
  digitalWrite(trigPin, LOW);
}

// ECHO 引脚ISR
void IRAM_ATTR changeISR() 
{
  auto now = micros(); // 当前时间
  auto state = digitalRead(echoPin);

  portENTER_CRITICAL_ISR(&mux);
  if (state) // 高电平,即刚发出超声波
    startTime = now;
  else
    endTime = now;
  portEXIT_CRITICAL_ISR(&mux);

  // 变成低电平时表示已经收到回声
  if (!state)
    xSemaphoreGiveFromISR(semaphore, NULL); // 给一个信号量发送信号
}


void setup() {
  pinMode(trigPin, OUTPUT);
  pinMode(echoPin, INPUT);
  Serial.begin(115200);

  semaphore = xSemaphoreCreateBinary(); // 创建二值信号量

  // 定时器部分
  timer1 = timerBegin(0, 80, true);
  timerAttachInterrupt(timer1, ping, true);
  timerAlarmWrite(timer1, 500000, true); // 定时时间为 0.5s
 
  // echo引脚的中断
  attachInterrupt(digitalPinToInterrupt(echoPin), changeISR, CHANGE);

  // 开始周期测量
  timerAlarmEnable(timer1);

}

void loop() {
  if (xSemaphoreTake(semaphore, 0) == pdTRUE)
  {
    // 收到信号,准备工作
    portENTER_CRITICAL(&mux);
    auto t = endTime - startTime;
    portEXIT_CRITICAL(&mux);

    double dis = t * 0.01715;
    if (dis < 350)
    {   
        distance = dis;
        Serial.print("Distance: "); 
        Serial.print(distance, 1); // 小数点后1位
        Serial.println(" cm");
    }
  }
}

 

10. 舵机

10.1 Servo库操控舵机

库名称为 ESP32Servo

#include <ESP32Servo.h>

Servo servo1; // 定义对象
Servo servo2;

int minUs = 500; // 0°时的脉宽,单位us
int maxUs = 2500; // 180°时的脉宽,单位us

int servo1Pin = 15;
int servo2Pin = 16;
int pos = -1; // 舵机角度
bool up = true; // 计数方向

void setup() {
  ESP32PWM::allocateTimer(1); // 指定使用的硬件定时器

  servo1.setPeriodHertz(50); // 指定PWM的频率
  servo2.setPeriodHertz(50); // 指定PWM的频率

  servo1.attach(servo1Pin, minUs, maxUs);
  servo2.attach(servo2Pin, minUs, maxUs);

}

void loop() {
  if (pos == 181)
    up = false;
  else if (pos == -1)
    up = true;
  
  if (up)
    pos ++;
  else
    pos --;
  
  servo1.write(pos);
  servo2.write(180 - pos);

  //servo1.write(pos); // 转到指定的角度(0° - 180°)
  //servo1.detach(); // 不需要的时候将引脚和ledc分离

  delay(15);
}

10.2 智能垃圾桶

使用超声波测距配合舵机实现智能垃圾桶,因为懒得弄模型,所以垃圾桶开闭直接用串口打印信息。

其相关流程及代码部分见此篇文章:ESP32Demo:智能垃圾桶 – Echo (liveout.cn)

 

11. WiFi连接

#include<WiFi.h>
const char* ssid = "WiFi名称";
const char* password = "WiFi密码";
void setup() {
  //初始化串口
  Serial.begin(115200);
  delay(10);
    
  // 进行WiFi连接
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  //连接WIFI
  WiFi.begin(ssid, password);
  //等待WIFI连接成功
  while (WiFi.status() != WL_CONNECTED) { //WiFi.status()函数用于获取WiFi连接的状态
    //WL_CONNECTED,即连接状态
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");

}

void loop() {
    
}

 

 

--> 前言此篇文章为有关 ESP32 的学习期间的代码记录,并且加上了自己的注释,非教学文章。使用开发板全称ESP32 DEVKILTv1(devkitv1) ,搭载芯片为 ESP32D0WDQ6,使用软件为 Arduino 。  参考链接如果是小白并且想要学习单片机相关知识,建议移步此篇文章:51单片机入门教程(上篇)(代码+个人理解) – Echo (liveout.cn...

前言

心心念念了一个学期,最终还是全款买下一个香橙派,型号为 OrangePi 3 LTS。

本来想买树莓派的,可惜溢价太严重了,于是只好用香橙派来代替了。

因为经常折腾系统,所以肯定会多次重置,就写了一篇配置文章,留着以后重置后复制粘贴用,也就是备份了。

以下所有命令都是以Ubuntu系统为准

 

资料

Orange Pi 3 LTS地址:Orange Pi 3 LTS-Orange Pi官网-香橙派(Orange Pi)开发板,开源硬件,开源软件,开源芯片,电脑键盘

用户手册和原理图:用户手册和原理图_免费高速下载|百度网盘-分享无限制 (baidu.com)

 

烧录

材料:内存卡一个(8G起步)

工具软件:

  1. 烧录软件:balenaEtcher - Flash OS images to SD cards & USB drives
  2. 格式化内存卡软件:https://www.sdcard.org/downloads/formatter/eula_windows/SDCardFormatterv5_WinEN.zip

过程没啥好说的,直接看用户文档。先下载镜像,再格式化内存卡,然后通过烧录软件将镜像烧录进去。香橙派会自动选择加载内存卡里系统。

可参考好友文章:制作Linux启动盘 – Clif's Blog (new-epoch-meta.com)

配置

桌面服务

因为桌面服务功耗太高了,所以平时就关闭了,也可以直接下载服务器版本镜像使用

sudo orangepi-config

选择 System ,DesktopStop

关闭后记得重启生效

reboot

 

WiFi连接

没有最基本的网络,那么远程连接都不行,所以先进行网络连接

打开无线

nmcli radio wifi on

列出可用的 WiFi 网络

nmcli device wifi list

选择要连接的 WiFi 网络

nmcli device wifi connect SSID password PASSWORD

查看连接

nmcli con show

断开连接

nmcli con down yourSSID

重新连接

nmcli con up yourSSID

SSID 替换为要连接的 WiFi 网络的名称,将 PASSWORD 替换为该网络的密码。如果该网络不需要密码,则无需提供 password 部分。

请注意,使用 nmcli 连接 WiFi 需要您的系统上安装了 NetworkManager,并且您具有适当的权限来进行网络连接操作。

 

用户配置、SSH连接

系统初始用户名和密码都是 orangepi , 如果要更改root密码,使用 passwd 命令即可

passwd

通过命令查看ip地址,然后ssh连接香橙派

ifconfig

 

Vim配置

Vim 肯定是使用频率最高的东西之一了,所以得好好配置

vim /etc/vim/vimrc  #这里是全部用户都一样
syntax on                                                                                             set tabstop=4
set softtabstop=4
set shiftwidth=4
set autoindent
set cindent
set nu
set ruler
set cursorline
set cursorcolumn
set cuc cul

更加全局详细的配置:Vim基本配置 – Echo (liveout.cn)

 

.bashrc 配置

.bashrc文件位于Linux系统用户的主目录中。该文件是用户特定的bash shell配置文件

用于定义用户的环境变量、别名和其他定制内容

alias ls='ls --color=auto' #文件和文件夹设置不同的颜色
alias ll='ls -l'           # ll -> ls -l

最后别忘了运行 source ~/.bashrc 来使修改生效。

 

更换软件源

因为香橙派官方用的就是 清华源 ,所以就不换了。如果要更换,步骤如下

  1. 备份

    sudo cp /etc/apt/sources.list /etc/apt/sources.list.backup
    
  2. 打开配置文件

    sudo vim /etc/apt/sources.list
    
  3. 选择合适的源,这里以 阿里云 为例

    deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
    deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
    deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
    deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
    
  4. 更新软件源

    sudo apt update
    

     

UFW配置

安装

sudo apt install ufw

查看状态

sudo ufw status verbose

启动

sudo ufw enable

关闭

sudo ufw disable

开启/禁用相应端口或服务举例

sudo ufw allow 80 #允许外部访问80端口
sudo ufw delete allow 80 #禁止外部访问80端口
sudo ufw allow from 192.168.1.1 #允许此IP访问所有的本机端口
sudo ufw deny smtp #禁止外部访问smtp服务
sudo ufw delete allow smtp #删除上面建立的某条规则

#要拒绝所有的TCP流量从10.0.0.0/8 到192.168.0.1地址的22端口
sudo ufw deny proto tcp from 10.0.0.0/8 to 192.168.0.1 port 22

#可以允许所有RFC1918网络(局域网/无线局域网的)访问这个主机(/8,/16,/12是一种网络分级):
sudo ufw allow from 10.0.0.0/8
sudo ufw allow from 172.16.0.0/12
sudo ufw allow from 192.168.0.0/16

默认情况下,UFW 阻塞了所有进来的连接,并且允许所有出去的连接。这意味着任何人无法访问你的服务器,除非你打开端口。运行在服务器上的应用和服务可以访问外面的世界。

默认的策略定义在/etc/default/ufw文件中,并且可以通过使用sudo ufw default <policy> <chain>命令来修改。

 

Git配置

一般系统都装有Git,所以就记录下配置信息

  1. 配置用户信息

    git config --global user.name "Your Name"
    git config --global user.email "your.email@example.com"
    
  2. 配置文本编辑器

    git config --global core.editor "vim"
    
  3. 配置 Git 的颜色输出

    git config --global color.ui true
    

可参考好友文章:Ubuntu20.04下安装Docker – Clif's Blog (new-epoch-meta.com)

 

Docker安装

文档里有相关教程,这里我复制下来,方便粘贴

sudo apt updat
sudo apt-get install -y ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) \
signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \
https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io

检查 docker 的状态

systemctl status docker
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2020-08-24 10:29:22 UTC; 26min ago
Docs: https://docs.docker.com
Main PID: 3145 (dockerd)
Tasks: 15
CGroup: /system.slice/docker.service
└─3145 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.soc

测试 docker

$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
256ab8fe8778: Pull complete
Digest:
sha256:7f0a9f93b4aa3022c3a4c147a449ef11e0941a1fd0bf4a8e6c9408b2600777c5
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correct

设置 docker 仓库为国内源

vim /etc/docker/daemon,json
{
	"registry-mirrors": [ 		      
		"https://docker.mirrors.ustc.edu.cn"
 	]
}

重启 docker 服务

sudo systemctl restart docker

 

LAMP架构

买了这个,肯定得搭建一个网站玩玩,所以先配置好相关环境,之前已经写过了,直接看这篇文章

Wordpress/Typecho博客搬迁教程 – Echo (liveout.cn)

 

查看温度

毕竟长时间运行,温度得注意点,但是官方给的查看命令太长了,所以就写了个脚本

后来发现可以安装相关包,然后通过sensors命令查看,安装过程补充在脚本后面

CPU

#!/bin/bash

temp=$(cat /sys/class/thermal/thermal_zone0/temp)
temp=$((temp/1000))

# 定义颜色代码
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' 

if ((temp >= 70)); then
    echo -e "CPU温度: ${RED}${temp}°C${NC}"
else
    echo -e "CPU温度: ${GREEN}${temp}°C${NC}"
fi

GPU

#!/bin/bash

temp=$(cat /sys/class/thermal/thermal_zone1/temp)
temp=$((temp/1000))

# 定义颜色代码
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' 

if ((temp >= 70)); then
    echo -e "GPU温度: ${RED}${temp}°C${NC}"
else
    echo -e "GPU温度: ${GREEN}${temp}°C${NC}"
fi

顺便买了个散热套装

补充

才发现可以安装相应的软件包查看温度,命令如下

安装

sudo apt install lm-sensors

选择监控范围

sudo sensors-detect

查看温度

sensors

 

存储

Linux系统不像Windows可以自动识别外置存储设备,需要自己手动挂载,所以挂载硬盘肯定是必修课了。

下面是一些常用的磁盘管理命令,精简版。

du

disk usage ,用来展示磁盘使用量的统计信息

du -s

-s选项,是 --summarize 的缩写形式,其作用是对 du 的每一个给定参数计算其磁盘使用量,我们来看例子。

[roc@roclinux ruanjian]$ du -sh *
3.4M    curl-7.34.0.tar.gz
41M     soft
6.8M    wordpress-4.4.1.tar.gz

du -c

-c选项,是 --total 的缩写形式,它表示的是针对输出的各个对象来计算其磁盘使用量的总和。比如,我们想计算当前文件夹下所有后缀是 tar.gz 的文件的磁盘使用量总和,那么命令是这样的:

[roc@roclinux ruanjian]$ du -ch *.tar.gz
3.4M    curl-7.34.0.tar.gz
6.8M    wordpress-4.4.1.tar.gz
11M     总用量

当然,-c选项也可以计算文件和文件夹的混合求和:

[roc@roclinux ruanjian]$ du -ch curl-7.34.0.tar.gz soft
3.4M    curl-7.34.0.tar.gz
41M     soft
45M     总用量

 

df

disk free 命令用于显示目前在 Linux 系统上的文件系统磁盘使用情况统计。

df

# df 
Filesystem     1K-blocks    Used     Available Use% Mounted on 
/dev/sda6       29640780 4320704     23814388  16%     / 
udev             1536756       4     1536752    1%     /dev 
tmpfs             617620     888     616732     1%     /run 
none                5120       0     5120       0%     /run/lock 
none             1544044     156     1543888    1%     /run/shm 

df -h

-h选项,通过它可以产生可读的格式df命令的输出:

# df -h 
Filesystem      Size  Used   Avail Use% Mounted on 
/dev/sda6       29G   4.2G   23G   16%     / 
udev            1.5G  4.0K   1.5G   1%     /dev 
tmpfs           604M  892K   603M   1%     /run 
none            5.0M     0   5.0M   0%     /run/lock 
none            1.5G  156K   1.5G   1%     /run/shm 

 

lsblk

List block devices ,列出所有块设备

root@orangepi3-lts:/opt# lsblk
NAME         MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
loop0          7:0    0     4K  1 loop /snap/bare/5
loop1          7:1    0  68.5M  1 loop /snap/core22/807
loop2          7:2    0  91.7M  1 loop /snap/gtk-common-themes/1535
loop3          7:3    0  46.4M  1 loop /snap/snapd/19459
mmcblk2      179:0    0   7.3G  0 disk
├─mmcblk2p1  179:1    0     2G  0 part
├─mmcblk2p2  179:2    0    16M  0 part
├─mmcblk2p3  179:3    0    16M  0 part
├─mmcblk2p4  179:4    0    32M  0 part
├─mmcblk2p5  179:5    0     2G  0 part
├─mmcblk2p6  179:6    0   300M  0 part
├─mmcblk2p7  179:7    0    16M  0 part
├─mmcblk2p8  179:8    0    16M  0 part
├─mmcblk2p9  179:9    0    32M  0 part
├─mmcblk2p10 179:10   0     2G  0 part
├─mmcblk2p11 179:11   0    16M  0 part
├─mmcblk2p12 179:12   0    64M  0 part
├─mmcblk2p13 179:13   0     2M  0 part
├─mmcblk2p14 179:14   0    32M  0 part
├─mmcblk2p15 179:15   0    16M  0 part
└─mmcblk2p16 179:16   0   768M  0 part
mmcblk2boot0 179:32   0     4M  1 disk
mmcblk2boot1 179:64   0     4M  1 disk
mmcblk0      179:96   0 119.4G  0 disk
└─mmcblk0p1  179:97   0 118.2G  0 part /var/log.hdd
                                       /
zram0        252:0    0 992.4M  0 disk [SWAP]
zram1        252:1    0    50M  0 disk /var/log
zram2        252:2    0     0B  0 disk

 

mnt

mount 命令是经常会使用到的命令,它用于挂载Linux系统外的文件。

挂载设备的过程

  1. 获取设备名称

    fdisk -l
    
  2. 建立挂载点目录

    cd /mnt
    mkdir usb
    
  3. 挂载设备

    mount /dev/sdb1 /mnt/usb
    

    PS:若文件名因含有中文出现乱码,可用以下命令解决

    mount -o iocharset=cp936 /dev/sdb1 /mnt/usb
    

umount 卸载设备

umount /dev/sdb1
umount /mnt/usb

 

内网穿透

通过 frp 实现内网穿透,前提是有一台公网服务器,作为服务端使用

FRP内网穿透实践教程 - 知乎 (zhihu.com)

使用frp进行内网穿透 - 少数派 (sspai.com)

以上两篇结合着看就够了~

frp下载地址:https://github.com/fatedier/frp/releases

参考文档:概览 | frp (gofrp.org)

 

结尾

目前就这些了,等到有其他需要备份的信息再补上

--> 前言心心念念了一个学期,最终还是全款买下一个香橙派,型号为 OrangePi 3 LTS。本来想买树莓派的,可惜溢价太严重了,于是只好用香橙派来代替了。因为经常折腾系统,所以肯定会多次重置,就写了一篇配置文章,留着以后重置后复制粘贴用,也就是备份了。以下所有命令都是以Ubuntu系统为准 资料Orange Pi 3 LTS地址:Orange Pi 3 LTS-Orange Pi官网...

前言

必读

51单片机入门教程(上篇)(代码+个人理解) – Echo (liveout.cn)

GitHub仓库链接:https://github.com/PGwind/51code

这篇文章是记录我粗略学习51单片机的一些代码,我会加些个人理解以及注释在里面。

因为是囫囵吞枣式学习,所以质量不是很好,后期我会慢慢优化 ?

如果你想要学习单片机,可以观看下面的B站教程并配合本文档学习

本文章使用的51单片机是 普中STC89C52RC

教程

推荐B站视频: 【51单片机入门教程-2020版 程序全程纯手打 从零开始入门】 https://www.bilibili.com/video/BV1Mb411e7re/?share_source=copy_web&vd_source=55024add0415795a359bd7b29ca21142(应该都知道吧)。

资源

B站江科大资源 链接:https://pan.baidu.com/s/1dLED_1VqL66qYItLl5ic4A?pwd=1111 提取码:1111

普中 链接:https://pan.baidu.com/s/1dNCHm9lLMP8pe3rZu3ktZQ?pwd=1111 提取码:1111


9. LED点阵屏

原理

88LED

88LED引脚
88LED引脚

74HC595

74HC595是串行输入并行输出的移位寄存器,3根线输入串行数据,8根线输出比并行数据,多片级后,可输出16为、24为、32位等,常用于IO口扩展

9.1 LED点阵显示图形

#include <REGX52.H>
#include "Delay.h"
sbit RCK=P3^5;  //RCLK 上升沿锁存
sbit SCK=P3^6;  //SRCLK 上升沿移位
sbit SER=P3^4;  //SER

#define MATRIX_LED_PORT     P0

//Function Definition
/**
  * @brief 74HC595写入一个字节
  * @param  要写入的字节
  * @retval 无
  */


void _74HC595_WriteByte(unsigned char Byte) 
{
	unsigned char i;
	for (i=0;i<8;i++)  
	{
		SER=Byte&(0x80>>i);  //1000 0000 
	  SCK=1;
	  SCK=0;
	}
	RCK=1;
	RCK=0;
}

//Function Definition
/**
  * @brief LED点阵屏显示一列数据
  * @param  Column 要选择的列,范围:0~7,0在最左边
  * @param  Data 选择列显示的数据,高位在上,1位亮,0位灭
  * @retval 无          
  */

void MatrixLED_ShowColumn(unsigned char Column,Data)
{
	_74HC595_WriteByte(Data);  //Data==Byte
	MATRIX_LED_PORT=~(0x80>>Column); //0选中,1不选中
  Delay(1); //延迟
  MATRIX_LED_PORT=0xFF;  //全灭  消影
}


void main()
{
	SCK=0; //刚开始是高电平,所以要置0,使其为低电平
	RCK=0;
	
	while(1)
	{
		MatrixLED_ShowColumn(0,0x3C); //1亮0不亮
		MatrixLED_ShowColumn(1,0x42);
		MatrixLED_ShowColumn(2,0xA9);
		MatrixLED_ShowColumn(3,0x85);
		MatrixLED_ShowColumn(4,0x85);
		MatrixLED_ShowColumn(5,0xA9);
		MatrixLED_ShowColumn(6,0x42);
		MatrixLED_ShowColumn(7,0x3C);
	}
}

9.2 LED点阵显示动画

头文件

//MatrixLED.h
#ifndef __MATRIX_LED_H_
#define __MATRIX_LED_H_
void MatrixLED_Init();
void _74HC595_WriteByte(unsigned char Byte);
void MatrixLED_ShowColumn(unsigned char Column,Data);
#endif

//Delay.h
#ifndef __DELAY_H_
#define __DELAY_H_
void Delay(unsigned int xms);
#endif

函数

//MatrixLED.c
#include <REGX52.H>
#include "Delay.h"

sbit RCK=P3^5;  //RCLK 上升沿锁存
sbit SCK=P3^6;  //SRCLK 上升沿移位
sbit SER=P3^4;  //SER

#define MATRIX_LED_PORT     P0

//Function Definition
/**
  * @brief 74HC595写入一个字节
  * @param  要写入的字节
  * @retval 无
  */


void _74HC595_WriteByte(unsigned char Byte) 
{
	unsigned char i;
	for (i=0;i<8;i++)  
	{
		SER=Byte&(0x80>>i);  //1000 0000 
	  SCK=1;
	  SCK=0;
	}
	RCK=1;
	RCK=0;
}


//Function Definition
/**
  * @brief 点阵屏初始化
  * @param  无
  * @retval 无
  */
void MatrixLED_Init()
{
	SCK=0; //刚开始是高电平,所以要置0,使其为低电平
	RCK=0;
}

//Function Definition
/**
  * @brief LED点阵屏显示一列数据
  * @param  Column 要选择的列,范围:0~7,0在最左边
  * @param  Data 选择列显示的数据,高位在上,1位亮,0位灭
  * @retval 无          
  */

void MatrixLED_ShowColumn(unsigned char Column,Data)
{
	_74HC595_WriteByte(Data);  //Data==Byte
	MATRIX_LED_PORT=~(0x80>>Column); //0选中,1不选中
  Delay(1); //延迟
  MATRIX_LED_PORT=0xFF;  //全灭  消影
}

//Delay.c
//Function Definition
/**
  * @brief 延迟函数
  * @param  无
  * @retval 1ms
  */
void Delay(unsigned int xms)		//@11.0592MHz
{
	unsigned char i, j;
	while(xms)
	{
			i = 2;
			j = 199;
			do
			{
				while (--j);
			} while (--i);	
			xms--;
	}
} 

1. 通过流动实现动画

//main.c  通过流动实现动画
#include <REGX52.H>
#include "Delay.h"
#include "MatrixLED.h"

unsigned char Animation[]={0x00,0x00,0x00,0x81,0xFF,0x81,0x00,0x00,
	0x7E,0x02,0x02,0x00,0x3C,0x42,0x42,0x3C,
	0x00,0x30,0x0C,0x02,0x0C,0x30,0x00,0x3E,
	0x2A,0x2A,0x00,0x00,0x60,0x18,0x06,0x18,0x60,
	0x00
};

void main()
{
	unsigned char i,Offset=1,Count=0;  //Offset偏移量,即数组第几列 Count控制移动速度
	MatrixLED_Init();
	while(1)
	{
		for (i=0; i<8; i++)
		{
			MatrixLED_ShowColumn(i,Animation[i+Offset]);
		}
		Count++;
		if (Count>12)
		{
			Count=0;
			Offset++;
			if(Offset>24) //防止数组溢出
			{
				Offset=0;
			}	
		}
	}
}

2. 通过每次刷新(即每帧)实现动画

//main.c  通过每次刷新(即每帧)实现动画
#include <REGX52.H>
#include "Delay.h"
#include "MatrixLED.h"

unsigned char code Animation[]={
	0x00,0x0C,0x1C,0x21,0xDE,0xE0,0x3F,0x10,
	0x00,0x06,0x16,0x21,0xDE,0xE0,0x3F,0x10,
	0x00,0x03,0x13,0x21,0xDE,0xE0,0x3F,0x10,
	0x0C,0x0C,0x10,0x21,0xDE,0xE0,0x3F,0x10,
	0x00,0x0C,0x1C,0x21,0xDE,0xE0,0x3F,0x10,
}; //加code这些数据会放在flash里面,不然在RAM里。flash内存较大
//但是后面数组无法更改

void main()
{
	unsigned char i,Offset=0,Count=0;  //Offset偏移量,即数组第几列 Count控制变换速度
	MatrixLED_Init();
	while(1)
	{
		for (i=0; i<8; i++)
		{
			MatrixLED_ShowColumn(i,Animation[i+Offset]);
		}
		Count++;
		if (Count>15)
		{
			Count=0;
			Offset+=8;
			if(Offset>32) //防止数组溢出
			{
				Offset=0;
			}		
		}
	}
}


10. DS1302时钟

DS1302:低功耗实时时钟芯片,可以对年、月、日、周、时、分、秒进行计时,且具有闰年补偿等多种功能

RTC:实时时钟,是一种集成电路,通常称为时钟芯片(51单片机不带)

BCD码:用4位二进制数来表示1位十进制数 如:0001 0011 表示13,而 0001 1010 则不合法

原理

阅读厂家提供的DS1302中文手册

DS1302时钟

10.1 DS1302时钟

头文件

//DS1302.h
#ifndef __DS1302_H__
#define __DS1302_H__

//外部可调用时间数组,索引0~6分别为年、月、日、时、分、秒、星期
extern unsigned char DS1302_Time[];

void DS1302_Init(void);
void DS1302_WriteByte(unsigned char Command,Data);
unsigned char DS1302_ReadByte(unsigned char Command);
void DS1302_SetTime(void);
void DS1302_ReadTime(void);

#endif

函数

//DS1302.c
#include <REGX52.H>

//引脚定义
sbit DS1302_SCLK=P3^6;
sbit DS1302_IO=P3^4;
sbit DS1302_CE=P3^5;

//寄存器写入地址/指令定义
#define DS1302_SECOND		0x80
#define DS1302_MINUTE		0x82
#define DS1302_HOUR			0x84
#define DS1302_DATE			0x86
#define DS1302_MONTH		0x88
#define DS1302_DAY			0x8A
#define DS1302_YEAR			0x8C
#define DS1302_WP			0x8E

//时间数组,索引0~6分别为年、月、日、时、分、秒、星期
unsigned char DS1302_Time[]={19,11,16,12,59,55,6};

/**
  * @brief  DS1302初始化
  * @param  无
  * @retval 无
  */
void DS1302_Init(void)
{
	DS1302_CE=0;
	DS1302_SCLK=0;
}

/**
  * @brief  DS1302写一个字节
  * @param  Command 命令字/地址
  * @param  Data 要写入的数据
  * @retval 无
  */
void DS1302_WriteByte(unsigned char Command,Data)
{
	unsigned char i;
	DS1302_CE=1;
	for(i=0;i<8;i++)
	{
		DS1302_IO=Command&(0x01<<i);
		DS1302_SCLK=1;
		DS1302_SCLK=0;
	}
	for(i=0;i<8;i++)
	{
		DS1302_IO=Data&(0x01<<i);
		DS1302_SCLK=1;
		DS1302_SCLK=0;
	}
	DS1302_CE=0;
}

/**
  * @brief  DS1302读一个字节
  * @param  Command 命令字/地址
  * @retval 读出的数据
  */
unsigned char DS1302_ReadByte(unsigned char Command)
{
	unsigned char i,Data=0x00;
	Command|=0x01;	//将指令转换为读指令
	DS1302_CE=1;
	for(i=0;i<8;i++)
	{
		DS1302_IO=Command&(0x01<<i);
		DS1302_SCLK=0;
		DS1302_SCLK=1;
	}
	for(i=0;i<8;i++)
	{
		DS1302_SCLK=1;
		DS1302_SCLK=0;
		if(DS1302_IO){Data|=(0x01<<i);}
	}
	DS1302_CE=0;
	DS1302_IO=0;	//读取后将IO设置为0,否则读出的数据会出错
	return Data;
}

/**
  * @brief  DS1302设置时间,调用之后,DS1302_Time数组的数字会被设置到DS1302中
  * @param  无
  * @retval 无
  */
void DS1302_SetTime(void)
{
	DS1302_WriteByte(DS1302_WP,0x00);
	DS1302_WriteByte(DS1302_YEAR,DS1302_Time[0]/10*16+DS1302_Time[0]%10);//十进制转BCD码后写入
	DS1302_WriteByte(DS1302_MONTH,DS1302_Time[1]/10*16+DS1302_Time[1]%10);
	DS1302_WriteByte(DS1302_DATE,DS1302_Time[2]/10*16+DS1302_Time[2]%10);
	DS1302_WriteByte(DS1302_HOUR,DS1302_Time[3]/10*16+DS1302_Time[3]%10);
	DS1302_WriteByte(DS1302_MINUTE,DS1302_Time[4]/10*16+DS1302_Time[4]%10);
	DS1302_WriteByte(DS1302_SECOND,DS1302_Time[5]/10*16+DS1302_Time[5]%10);
	DS1302_WriteByte(DS1302_DAY,DS1302_Time[6]/10*16+DS1302_Time[6]%10);
	DS1302_WriteByte(DS1302_WP,0x80);
}

/**
  * @brief  DS1302读取时间,调用之后,DS1302中的数据会被读取到DS1302_Time数组中
  * @param  无
  * @retval 无
  */
void DS1302_ReadTime(void)
{
	unsigned char Temp;
	Temp=DS1302_ReadByte(DS1302_YEAR);
	DS1302_Time[0]=Temp/16*10+Temp%16;//BCD码转十进制后读取
	Temp=DS1302_ReadByte(DS1302_MONTH);
	DS1302_Time[1]=Temp/16*10+Temp%16;
	Temp=DS1302_ReadByte(DS1302_DATE);
	DS1302_Time[2]=Temp/16*10+Temp%16;
	Temp=DS1302_ReadByte(DS1302_HOUR);
	DS1302_Time[3]=Temp/16*10+Temp%16;
	Temp=DS1302_ReadByte(DS1302_MINUTE);
	DS1302_Time[4]=Temp/16*10+Temp%16;
	Temp=DS1302_ReadByte(DS1302_SECOND);
	DS1302_Time[5]=Temp/16*10+Temp%16;
	Temp=DS1302_ReadByte(DS1302_DAY);
	DS1302_Time[6]=Temp/16*10+Temp%16;
}
//main.c
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"

void main()
{
	LCD_Init();
	DS1302_Init();
	LCD_ShowString(1,1,"  -  -  ");//静态字符初始化显示
	LCD_ShowString(2,1,"  :  :  ");
	
	DS1302_SetTime();//设置时间
	
	while(1)
	{
		DS1302_ReadTime();//读取时间
		LCD_ShowNum(1,1,DS1302_Time[0],2);//显示年
		LCD_ShowNum(1,4,DS1302_Time[1],2);//显示月
		LCD_ShowNum(1,7,DS1302_Time[2],2);//显示日
		LCD_ShowNum(2,1,DS1302_Time[3],2);//显示时
		LCD_ShowNum(2,4,DS1302_Time[4],2);//显示分
		LCD_ShowNum(2,7,DS1302_Time[5],2);//显示秒
	}
}

10.2 DS1302可调时钟

#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
#include "Key.h"
#include "Timer0.h"

unsigned char KeyNum,MODE,TimeSetSelect,TimeSetFlashFlag;

void TimeShow(void)//时间显示功能
{
	DS1302_ReadTime();//读取时间
	LCD_ShowNum(1,1,DS1302_Time[0],2);//显示年
	LCD_ShowNum(1,4,DS1302_Time[1],2);//显示月
	LCD_ShowNum(1,7,DS1302_Time[2],2);//显示日
	LCD_ShowNum(2,1,DS1302_Time[3],2);//显示时
	LCD_ShowNum(2,4,DS1302_Time[4],2);//显示分
	LCD_ShowNum(2,7,DS1302_Time[5],2);//显示秒
}

void TimeSet(void)//时间设置功能
{
	if(KeyNum==2)//按键2按下
	{
		TimeSetSelect++;//设置选择位加1
		TimeSetSelect%=6;//越界清零
	}
	if(KeyNum==3)//按键3按下
	{
		DS1302_Time[TimeSetSelect]++;//时间设置位数值加1
		if(DS1302_Time[0]>99){DS1302_Time[0]=0;}//年越界判断
		if(DS1302_Time[1]>12){DS1302_Time[1]=1;}//月越界判断
		if( DS1302_Time[1]==1 || DS1302_Time[1]==3 || DS1302_Time[1]==5 || DS1302_Time[1]==7 || 
			DS1302_Time[1]==8 || DS1302_Time[1]==10 || DS1302_Time[1]==12)//日越界判断
		{
			if(DS1302_Time[2]>31){DS1302_Time[2]=1;}//大月
		}
		else if(DS1302_Time[1]==4 || DS1302_Time[1]==6 || DS1302_Time[1]==9 || DS1302_Time[1]==11)
		{
			if(DS1302_Time[2]>30){DS1302_Time[2]=1;}//小月
		}
		else if(DS1302_Time[1]==2)
		{
			if(DS1302_Time[0]%4==0)
			{
				if(DS1302_Time[2]>29){DS1302_Time[2]=1;}//闰年2月
			}
			else
			{
				if(DS1302_Time[2]>28){DS1302_Time[2]=1;}//平年2月
			}
		}
		if(DS1302_Time[3]>23){DS1302_Time[3]=0;}//时越界判断
		if(DS1302_Time[4]>59){DS1302_Time[4]=0;}//分越界判断
		if(DS1302_Time[5]>59){DS1302_Time[5]=0;}//秒越界判断
	}
	if(KeyNum==4)//按键3按下
	{
		DS1302_Time[TimeSetSelect]--;//时间设置位数值减1
		if(DS1302_Time[0]<0){DS1302_Time[0]=99;}//年越界判断
		if(DS1302_Time[1]<1){DS1302_Time[1]=12;}//月越界判断
		if( DS1302_Time[1]==1 || DS1302_Time[1]==3 || DS1302_Time[1]==5 || DS1302_Time[1]==7 || 
			DS1302_Time[1]==8 || DS1302_Time[1]==10 || DS1302_Time[1]==12)//日越界判断
		{
			if(DS1302_Time[2]<1){DS1302_Time[2]=31;}//大月
			if(DS1302_Time[2]>31){DS1302_Time[2]=1;}
		}
		else if(DS1302_Time[1]==4 || DS1302_Time[1]==6 || DS1302_Time[1]==9 || DS1302_Time[1]==11)
		{
			if(DS1302_Time[2]<1){DS1302_Time[2]=30;}//小月
			if(DS1302_Time[2]>30){DS1302_Time[2]=1;}
		}
		else if(DS1302_Time[1]==2)
		{
			if(DS1302_Time[0]%4==0)
			{
				if(DS1302_Time[2]<1){DS1302_Time[2]=29;}//闰年2月
				if(DS1302_Time[2]>29){DS1302_Time[2]=1;}
			}
			else
			{
				if(DS1302_Time[2]<1){DS1302_Time[2]=28;}//平年2月
				if(DS1302_Time[2]>28){DS1302_Time[2]=1;}
			}
		}
		if(DS1302_Time[3]<0){DS1302_Time[3]=23;}//时越界判断
		if(DS1302_Time[4]<0){DS1302_Time[4]=59;}//分越界判断
		if(DS1302_Time[5]<0){DS1302_Time[5]=59;}//秒越界判断
	}
	//更新显示,根据TimeSetSelect和TimeSetFlashFlag判断可完成闪烁功能
	if(TimeSetSelect==0 && TimeSetFlashFlag==1){LCD_ShowString(1,1,"  ");}
	else {LCD_ShowNum(1,1,DS1302_Time[0],2);}
	if(TimeSetSelect==1 && TimeSetFlashFlag==1){LCD_ShowString(1,4,"  ");}
	else {LCD_ShowNum(1,4,DS1302_Time[1],2);}
	if(TimeSetSelect==2 && TimeSetFlashFlag==1){LCD_ShowString(1,7,"  ");}
	else {LCD_ShowNum(1,7,DS1302_Time[2],2);}
	if(TimeSetSelect==3 && TimeSetFlashFlag==1){LCD_ShowString(2,1,"  ");}
	else {LCD_ShowNum(2,1,DS1302_Time[3],2);}
	if(TimeSetSelect==4 && TimeSetFlashFlag==1){LCD_ShowString(2,4,"  ");}
	else {LCD_ShowNum(2,4,DS1302_Time[4],2);}
	if(TimeSetSelect==5 && TimeSetFlashFlag==1){LCD_ShowString(2,7,"  ");}
	else {LCD_ShowNum(2,7,DS1302_Time[5],2);}
}

void main()
{
	LCD_Init();
	DS1302_Init();
	Timer0Init();
	LCD_ShowString(1,1,"  -  -  ");//静态字符初始化显示
	LCD_ShowString(2,1,"  :  :  ");
	
	DS1302_SetTime();//设置时间
	
	while(1)
	{
		KeyNum=Key();//读取键码
		if(KeyNum==1)//按键1按下
		{
			if(MODE==0){MODE=1;TimeSetSelect=0;}//功能切换
			else if(MODE==1){MODE=0;DS1302_SetTime();}
		}
		switch(MODE)//根据不同的功能执行不同的函数
		{
			case 0:TimeShow();break;
			case 1:TimeSet();break;
		}
	}
}

void Timer0_Routine() interrupt 1
{
	static unsigned int T0Count;
	TL0 = 0x18;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
	T0Count++;
	if(T0Count>=500)//每500ms进入一次
	{
		T0Count=0;
		TimeSetFlashFlag=!TimeSetFlashFlag;//闪烁标志位取反
	}
}


11. 蜂鸣器

原理

蜂鸣器

 C调音符与频率对照表

11.1 蜂鸣器播放提示音

头文件

//Buzzer.h
#ifndef __BUZZER_H__
#define __BUZZER_H__

void Buzzer_Time(unsigned int ms);

#endif

函数

//Buzzer.c
#include <REGX52.H>
#include <INTRINS.H>

//蜂鸣器端口:
sbit Buzzer=P2^5;

/**
  * @brief  蜂鸣器私有延时函数,延时500us
  * @param  无
  * @retval 无
  */
void Buzzer_Delay500us()		//@12.000MHz
{
	unsigned char i;

	_nop_();
	i = 247;
	while (--i);
}

/**
  * @brief  蜂鸣器发声
  * @param  ms 发声的时长,范围:0~32767
  * @retval 无
  */
void Buzzer_Time(unsigned int ms)
{
	unsigned int i;
	for(i=0;i<ms*2;i++)
	{
		Buzzer=!Buzzer;
		Buzzer_Delay500us();
	}
}
//mian.c
#include <REGX52.H>
#include "Delay.h"
#include "Key.h"
#include "Nixie.h"
#include "Buzzer.h"

sbit Buzzer=P2^5;

unsigned char KeyNum;
unsigned int i;

void main()
{
	 Nixie(1,0);
	while(1)
	{
		KeyNum=Key();
		if(KeyNum)
		{
			for(i=0; i<100; i++)
			{
				Buzzer=!Buzzer;
				Delay(1);
			}
			Buzzer_Time(100);
			Nixie(1,KeyNum);
		}
	}
}

11.2 蜂鸣器播放音乐

#include <REGX52.H>
#include "Delay.h"
#include "Timer0.h"

//蜂鸣器端口定义
sbit Buzzer=P2 ^5;

//播放速度,值为四分音符的时长(ms)
#define SPEED	500

//音符与索引对应表,P:休止符,L:低音,M:中音,H:高音,下划线:升半音符号#
#define P	0
#define L1	1
#define L1_	2
#define L2	3
#define L2_	4
#define L3	5
#define L4	6
#define L4_	7
#define L5	8
#define L5_	9
#define L6	10
#define L6_	11
#define L7	12
#define M1	13
#define M1_	14
#define M2	15
#define M2_	16
#define M3	17
#define M4	18
#define M4_	19
#define M5	20
#define M5_	21
#define M6	22
#define M6_	23
#define M7	24
#define H1	25
#define H1_	26
#define H2	27
#define H2_	28
#define H3	29
#define H4	30
#define H4_	31
#define H5	32
#define H5_	33
#define H6	34
#define H6_	35
#define H7	36

//索引与频率对照表
unsigned int FreqTable[]={
	0,
	63628,63731,63835,63928,64021,64103,64185,64260,64331,64400,64463,64528,
	64580,64633,64684,64732,64777,64820,64860,64898,64934,64968,65000,65030,
	65058,65085,65110,65134,65157,65178,65198,65217,65235,65252,65268,65283,
};

//乐谱
unsigned char code Music[]={
	//音符,时值,
	
	//1
	P,	4,
	P,	4,
	P,	4,
	M6,	2,
	M7,	2,
	
	H1,	4+2,
	M7,	2,
	H1,	4,
	H3,	4,
	
	M7,	4+4+4,
	M3,	2,
	M3,	2,
	
	//2
	M6,	4+2,
	M5,	2,
	M6, 4,
	H1,	4,
	
	M5,	4+4+4,
	M3,	4,
	
	M4,	4+2,
	M3,	2,
	M4,	4,
	H1,	4,
	
	//3
	M3,	4+4,
	P,	2,
	H1,	2,
	H1,	2,
	H1,	2,
	
	M7,	4+2,
	M4_,2,
	M4_,4,
	M7,	4,
	
	M7,	8,
	P,	4,
	M6,	2,
	M7,	2,
	
	//4
	H1,	4+2,
	M7,	2,
	H1,	4,
	H3,	4,
	
	M7,	4+4+4,
	M3,	2,
	M3,	2,
	
	M6,	4+2,
	M5,	2,
	M6, 4,
	H1,	4,
	
	//5
	M5,	4+4+4,
	M2,	2,
	M3,	2,
	
	M4,	4,
	H1,	2,
	M7,	2+2,
	H1,	2+4,
	
	H2,	2,
	H2,	2,
	H3,	2,
	H1,	2+4+4,
	
	//6
	H1,	2,
	M7,	2,
	M6,	2,
	M6,	2,
	M7,	4,
	M5_,4,
	
	
	M6,	4+4+4,
	H1,	2,
	H2,	2,
	
	H3,	4+2,
	H2,	2,
	H3,	4,
	H5,	4,
	
	//7
	H2,	4+4+4,
	M5,	2,
	M5,	2,
	
	H1,	4+2,
	M7,	2,
	H1,	4,
	H3,	4,
	
	H3,	4+4+4+4,
	
	//8
	M6,	2,
	M7,	2,
	H1,	4,
	M7,	4,
	H2,	2,
	H2,	2,
	
	H1,	4+2,
	M5,	2+4+4,
	
	H4,	4,
	H3,	4,
	H3,	4,
	H1,	4,
	
	//9
	H3,	4+4+4,
	H3,	4,
	
	H6,	4+4,
	H5,	4,
	H5,	4,
	
	H3,	2,
	H2,	2,
	H1,	4+4,
	P,	2,
	H1,	2,
	
	//10
	H2,	4,
	H1,	2,
	H2,	2,
	H2,	4,
	H5,	4,
	
	H3,	4+4+4,
	H3,	4,
	
	H6,	4+4,
	H5,	4+4,
	
	//11
	H3,	2,
	H2,	2,
	H1,	4+4,
	P,	2,
	H1,	2,
	
	H2,	4,
	H1,	2,
	H2,	2+4,
	M7,	4,
	
	M6,	4+4+4,
	P,	4,
	
	0xFF	//终止标志
};

unsigned char FreqSelect,MusicSelect;

void main()
{
	Timer0Init(); //定时器0初始化
	while(1)
	{
		if(Music[MusicSelect]!=0xFF)	//如果不是停止标志位
		{
			FreqSelect=Music[MusicSelect];	//选择音符对应的频率
			MusicSelect++;
			Delay(SPEED/4*Music[MusicSelect]);	//选择音符对应的时值
			MusicSelect++;
			TR0=0;
			Delay(5);	//音符间短暂停顿
			TR0=1;
		}
		else	//如果是停止标志位
		{
			TR0=0;
			while(1);
		}
	}
}

void Timer0_Routine() interrupt 1
{
	if(FreqTable[FreqSelect])	//如果不是休止符
	{
		/*取对应频率值的重装载值到定时器*/
		TL0 = FreqTable[FreqSelect]%256;		//设置定时初值
		TH0 = FreqTable[FreqSelect]/256;		//设置定时初值
		Buzzer=!Buzzer;	//翻转蜂鸣器IO口
	}
}


12. AT24C02(存储器)

原理

EEPROM

12.1 AT24C02数据存储

头文件

//12C.h
#ifndef __I2C_H_
#define __I2C_H_

void I2C_Start(void);
void I2C_Stop(void);
void I2C_SendByte(unsigned char Byte);
unsigned char I2C_ReceiveByte(void);
void I2C_SendAck(unsigned char AckBit);
unsigned char I2C_ReceiveAck(void);

#endif
//AT24C02.h
#ifndef __AT24C02_H_
#define __AT24C02_H_

void AT24C02_WriteByte(unsigned char WordAddress,Data);
unsigned char AT24C02_ReadByte(unsigned char WordAddress);

#endif

函数

//I2C.c
#include <REGX52.H>

sbit I2C_SCL=P2^1;
sbit I2C_SDA=P2^0;

//Function Definition
/**
  * @brief I2C开始
  * @param  无
  * @retval 无
  */
void I2C_Start(void)
{
	I2C_SDA=1; //高
	I2C_SCL=1;
 	I2C_SDA=0; //低
	I2C_SCL=0;
}

//Function Definition
/**
  * @brief I2C停止
  * @param  无
  * @retval 无
  */
void I2C_Stop(void)
{
	I2C_SDA=0; //低
	I2C_SCL=1; //高
	I2C_SDA=1; //高
}

//Function Definition
/**
  * @brief I2C发送一个字节
  * @param  Byte 要发送的字节
  * @retval 无
  */
void I2C_SendByte(unsigned char Byte)
{
	unsigned char i;
	for (i=0;i<8;i++)
	{
		I2C_SDA=Byte&(0x80>>i);
		I2C_SCL=1;
		I2C_SCL=0;
	}
	
}

//Function Definition
/**
  * @brief I2C接收一个字节
  * @param  无
  * @retval 接收到的一个字节数据 
  */
unsigned char I2C_ReceiveByte(void)
{
	unsigned char Byte,i;
	
	I2C_SDA=1;
	
	for (i=0;i<8;i++)
	{
		I2C_SCL=1;
		if(I2C_SDA) {Byte|=(0x80>>i);}
		I2C_SCL=0;
	}
	return Byte;
}

//Function Definition
/**
  * @brief  I2C发送应答位
  * @param  AckBit 应答位,0为应答,1为非应答
  * @retval 无
  */
void I2C_SendAck(unsigned char AckBit)
{
	I2C_SDA=AckBit;
	I2C_SCL=1;
	I2C_SCL=0;
}

//Function Definition
/**
  * @brief  I2C接收应答位
  * @param  无
  * @retval 接收到的应答位,  0为应答,1为非应答
  */
unsigned char I2C_ReceiveAck(void)
{
	unsigned char AckBit;
	I2C_SDA=1;
	I2C_SCL=1;
	AckBit=I2C_SDA;
	I2C_SCL=0;
	return AckBit;
}
//AT24C02.c
#include <REGX52.H>
#include "I2C.h"

#define AT24C02_ADDRESS  0xA0

//Function Definition
/**
  * @brief AT24C02写入一个字节
  * @param  WordAddress  要写入字节的地址   Data  要写入的数据
  * @retval 无
  */
void AT24C02_WriteByte(unsigned char WordAddress,Data)
{
	I2C_Start();
	I2C_SendByte(AT24C02_ADDRESS);
	I2C_ReceiveAck();
	I2C_SendByte(WordAddress);
	I2C_ReceiveAck();
	I2C_SendByte(Data);
	I2C_ReceiveAck();
	I2C_Stop();
}

//Function Definition
/**
  * @brief  AT24C02读取一个字节
  * @param   WordAddress  要读出字节的地址  
  * @retval  Data  要读出的数据
  */
unsigned char AT24C02_ReadByte(unsigned char WordAddress)
{
	unsigned char Data;
	I2C_Start();
	I2C_SendByte(AT24C02_ADDRESS);
	I2C_ReceiveAck();
	I2C_SendByte(WordAddress);
	I2C_ReceiveAck();
	I2C_Start();
	I2C_SendByte(AT24C02_ADDRESS|0x01);
	I2C_ReceiveAck();
	Data=I2C_ReceiveByte();
	I2C_SendAck(1);
	I2C_Stop();
	return Data;
}
//main.c
#include <REGX52.H>
#include "LCD1602.h"
#include "Key.h"
#include "AT24C02.h"
#include "Delay.h"

unsigned char KeyNum;
unsigned int Num;

void main()
{
	LCD_Init();
	LCD_ShowNum(1,1,Num,5);
	while(1)
	{
		KeyNum=Key();
		if(KeyNum==1)
		{
			Num++;
			LCD_ShowNum(1,1,Num,5);
		}
		
		if(KeyNum==2)
		{
			Num--;
			LCD_ShowNum(1,1,Num,5);
		}
		
		if(KeyNum==3)
		{
			AT24C02_WriteByte(0,Num%256); //低8位
			Delay(5);
			AT24C02_WriteByte(1,Num/256); //高8位
			Delay(5);
			LCD_ShowString(2,1,"Write Ok");
			Delay(1000);
			LCD_ShowString(2,1,"         ");
		}
		
		if(KeyNum==4)
		{
			Num=AT24C02_ReadByte(0); //o地址
			Num|=AT24C02_ReadByte(1)<<8; //1地址
			LCD_ShowNum(1,1,Num,5);
			LCD_ShowString(2,1,"Read OK");
			Delay(1000);
			LCD_ShowString(2,1,"         ");
		}
	}
}

12.2 秒表(定时器扫描按键数码管)

头文件

//Nixie.h
#ifndef __NIXIE_H__
#define __NIXIE_H__

void Nixie_SetBuf(unsigned char Location,Number);
void Nixie_Scan(unsigned char Location,Number);
void Nixie_Loop(void);

#endif
//Key.h
#ifndef __KEY_H__
#define __KEY_H__

unsigned char Key(void);
void Key_Loop(void);

#endif

函数

//Nixie.c
#include <REGX52.H>
#include "Delay.h"

//数码管显示缓存区
unsigned char Nixie_Buf[9]={0,10,10,10,10,10,10,10,10};

//数码管段码表
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x00,0x40};

/**
  * @brief  设置显示缓存区
  * @param  Location 要设置的位置,范围:1~8
  * @param  Number 要设置的数字,范围:段码表索引范围
  * @retval 无
  */
void Nixie_SetBuf(unsigned char Location,Number)
{
	Nixie_Buf[Location]=Number;
}

/**
  * @brief  数码管扫描显示
  * @param  Location 要显示的位置,范围:1~8
  * @param  Number 要显示的数字,范围:段码表索引范围
  * @retval 无
  */
void Nixie_Scan(unsigned char Location,Number)
{
	P0=0x00;				//段码清0,消影
	switch(Location)		//位码输出
	{
		case 1:P2_4=1;P2_3=1;P2_2=1;break;
		case 2:P2_4=1;P2_3=1;P2_2=0;break;
		case 3:P2_4=1;P2_3=0;P2_2=1;break;
		case 4:P2_4=1;P2_3=0;P2_2=0;break;
		case 5:P2_4=0;P2_3=1;P2_2=1;break;
		case 6:P2_4=0;P2_3=1;P2_2=0;break;
		case 7:P2_4=0;P2_3=0;P2_2=1;break;
		case 8:P2_4=0;P2_3=0;P2_2=0;break;
	}
	P0=NixieTable[Number];	//段码输出
}

/**
  * @brief  数码管驱动函数,在中断中调用
  * @param  无
  * @retval 无
  */
void Nixie_Loop(void)
{
	static unsigned char i=1;
	Nixie_Scan(i,Nixie_Buf[i]);
	i++;
	if(i>=9){i=1;}
}
//Key.c
#include <REGX52.H>
#include "Delay.h"

unsigned char Key_KeyNumber;

/**
  * @brief  获取按键键码
  * @param  无
  * @retval 按下按键的键码,范围:0,1~4,0表示无按键按下
  */
unsigned char Key(void)
{
	unsigned char Temp=0;
	Temp=Key_KeyNumber;
	Key_KeyNumber=0;
	return Temp;
}

/**
  * @brief  获取当前按键的状态,无消抖及松手检测
  * @param  无
  * @retval 按下按键的键码,范围:0,1~4,0表示无按键按下
  */
unsigned char Key_GetState()
{
	unsigned char KeyNumber=0;
	
	if(P3_1==0){KeyNumber=1;}
	if(P3_0==0){KeyNumber=2;}
	if(P3_2==0){KeyNumber=3;}
	if(P3_3==0){KeyNumber=4;}
	
	return KeyNumber;
}

/**
  * @brief  按键驱动函数,在中断中调用
  * @param  无
  * @retval 无
  */
void Key_Loop(void)
{
	static unsigned char NowState,LastState;
	LastState=NowState;				//按键状态更新
	NowState=Key_GetState();		//获取当前按键状态
	//如果上个时间点按键按下,这个时间点未按下,则是松手瞬间,以此避免消抖和松手检测
	if(LastState==1 && NowState==0)
	{
		Key_KeyNumber=1;
	}
	if(LastState==2 && NowState==0)
	{
		Key_KeyNumber=2;
	}
	if(LastState==3 && NowState==0)
	{
		Key_KeyNumber=3;
	}
	if(LastState==4 && NowState==0)
	{
		Key_KeyNumber=4;
	}
}
//main.c
#include <REGX52.H>
#include "Timer0.h"
#include "Key.h"
#include "Nixie.h"
#include "Delay.h"
#include "AT24C02.h"

unsigned char KeyNum;
unsigned char Min,Sec,MiniSec;
unsigned char RunFlag;

void main()
{
	Timer0_Init();
	while(1)
	{
		KeyNum=Key();
		if(KeyNum==1)			//K1按键按下
		{
			RunFlag=!RunFlag;	//启动标志位翻转
		}
		if(KeyNum==2)			//K2按键按下
		{
			Min=0;				//分秒清0
			Sec=0;
			MiniSec=0;
		}
		if(KeyNum==3)			//K3按键按下
		{
			AT24C02_WriteByte(0,Min);	//将分秒写入AT24C02
			Delay(5);
			AT24C02_WriteByte(1,Sec);
			Delay(5);
			AT24C02_WriteByte(2,MiniSec);
			Delay(5);
		}
		if(KeyNum==4)			//K4按键按下
		{
			Min=AT24C02_ReadByte(0);	//读出AT24C02数据
			Sec=AT24C02_ReadByte(1);
			MiniSec=AT24C02_ReadByte(2);
		}
		Nixie_SetBuf(1,Min/10);	//设置显示缓存,显示数据
		Nixie_SetBuf(2,Min%10);
		Nixie_SetBuf(3,11);
		Nixie_SetBuf(4,Sec/10);
		Nixie_SetBuf(5,Sec%10);
		Nixie_SetBuf(6,11);
		Nixie_SetBuf(7,MiniSec/10);
		Nixie_SetBuf(8,MiniSec%10);
	}
}

/**
  * @brief  秒表驱动函数,在中断中调用
  * @param  无
  * @retval 无
  */
void Sec_Loop(void)
{
	if(RunFlag)
	{
		MiniSec++;
		if(MiniSec>=100)
		{
			MiniSec=0;
			Sec++;
			if(Sec>=60)
			{
				Sec=0;
				Min++;
				if(Min>=60)
				{
					Min=0;
				}
			}
		}
	}
}

void Timer0_Routine() interrupt 1
{
	static unsigned int T0Count1,T0Count2,T0Count3;
	TL0 = 0x18;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
	T0Count1++;
	if(T0Count1>=20)
	{
		T0Count1=0;
		Key_Loop();	//20ms调用一次按键驱动函数
	}
	T0Count2++;
	if(T0Count2>=2)
	{
		T0Count2=0;
		Nixie_Loop();//2ms调用一次数码管驱动函数
	}
	T0Count3++;
	if(T0Count3>=10)
	{
		T0Count3=0;
		Sec_Loop();	//10ms调用一次数秒表驱动函数
	}
}


13. DS18B20温度传感器

数字温度传感器

原理

DS18B02

13.1 温度读取

头文件

//OneWire.h
#ifndef __ONEWIRE_H__
#define __ONEWIRE_H__

unsigned char OneWire_Init(void);
void OneWire_SendBit(unsigned char Bit);
unsigned char OneWire_ReceiveBit(void);
void OneWire_SendByte(unsigned char Byte);
unsigned char OneWire_ReceiveByte(void);

#endif
//DS18B02.h
#ifndef __DS18B20_H__
#define __DS18B20_H__

void DS18B20_ConvertT(void);
float DS18B20_ReadT(void);

#endif

函数

//OneWire.c
#include <REGX52.H>

//引脚定义
sbit OneWire_DQ=P3^7;

/**
  * @brief  单总线初始化
  * @param  无
  * @retval 从机响应位,0为响应,1为未响应
  */
unsigned char OneWire_Init(void)
{
	unsigned char i;
	unsigned char AckBit;
	OneWire_DQ=1;
	OneWire_DQ=0;
	i = 247;while (--i);		//Delay 500us
	OneWire_DQ=1;
	i = 32;while (--i);			//Delay 70us
	AckBit=OneWire_DQ;
	i = 247;while (--i);		//Delay 500us
	return AckBit;
}

/**
  * @brief  单总线发送一位
  * @param  Bit 要发送的位
  * @retval 无
  */
void OneWire_SendBit(unsigned char Bit)
{
	unsigned char i;
	OneWire_DQ=0;
	i = 4;while (--i);			//Delay 10us
	OneWire_DQ=Bit;
	i = 24;while (--i);			//Delay 50us
	OneWire_DQ=1;
}

/**
  * @brief  单总线接收一位
  * @param  无
  * @retval 读取的位
  */
unsigned char OneWire_ReceiveBit(void)
{
	unsigned char i;
	unsigned char Bit;
	OneWire_DQ=0;
	i = 2;while (--i);			//Delay 5us
	OneWire_DQ=1;
	i = 2;while (--i);			//Delay 5us
	Bit=OneWire_DQ;
	i = 24;while (--i);			//Delay 50us
	return Bit;
}

/**
  * @brief  单总线发送一个字节
  * @param  Byte 要发送的字节
  * @retval 无
  */
void OneWire_SendByte(unsigned char Byte)
{
	unsigned char i;
	for(i=0;i<8;i++)
	{
		OneWire_SendBit(Byte&(0x01<<i));
	}
}

/**
  * @brief  单总线接收一个字节
  * @param  无
  * @retval 接收的一个字节
  */
unsigned char OneWire_ReceiveByte(void)
{
	unsigned char i;
	unsigned char Byte=0x00;
	for(i=0;i<8;i++)
	{
		if(OneWire_ReceiveBit()){Byte|=(0x01<<i);}
	}
	return Byte;
}
//DS18B02.c
#include <REGX52.H>
#include "OneWire.h"

//DS18B20指令
#define DS18B20_SKIP_ROM          0xCC
#define DS18B20_CONVERT_T         0x44
#define DS18B20_READ_SCRATCHPAD 	0xBE

/**
  * @brief  DS18B20开始温度变换
  * @param  无
  * @retval 无
  */
void DS18B20_ConvertT(void)
{
	OneWire_Init();
	OneWire_SendByte(DS18B20_SKIP_ROM);
	OneWire_SendByte(DS18B20_CONVERT_T);
}

/**
  * @brief  DS18B20读取温度
  * @param  无
  * @retval 温度数值
  */
float DS18B20_ReadT(void)
{
	unsigned char TLSB,TMSB;
	int Temp;
	float T;
	OneWire_Init();
	OneWire_SendByte(DS18B20_SKIP_ROM);
	OneWire_SendByte(DS18B20_READ_SCRATCHPAD);
	TLSB=OneWire_ReceiveByte();
	TMSB=OneWire_ReceiveByte();
	Temp=(TMSB<<8)|TLSB;
	T=Temp/16.0;
	return T;
}
//main.c
#include <REGX52.H>
#include "LCD1602.h"
#include "DS18B20.h"
#include "Delay.h"

float T;

void main()
{
	DS18B20_ConvertT();		//上电先转换一次温度,防止第一次读数据错误
	Delay(1000);			//等待转换完成
	LCD_Init();
	LCD_ShowString(1,1,"Temperature:");
	while(1)
	{
		DS18B20_ConvertT();	//转换温度
		T=DS18B20_ReadT();	//读取温度
		if(T<0)				//如果温度小于0
		{
			LCD_ShowChar(2,1,'-');	//显示负号
			T=-T;			//将温度变为正数
		}
		else				//如果温度大于等于0
		{
			LCD_ShowChar(2,1,'+');	//显示正号
		}
		LCD_ShowNum(2,2,T,3);		//显示温度整数部分
		LCD_ShowChar(2,5,'.');		//显示小数点
		LCD_ShowNum(2,6,(unsigned long)(T*10000)%10000,4);//显示温度小数部分
	}
}

13.2 温度报警器

头文件

//Key.h
#ifndef __KEY_H__
#define __KEY_H__

unsigned char Key(void);
void Key_Loop(void);

#endif
//OneWire.h
#ifndef __DS18B20_H__
#define __DS18B20_H__

void DS18B20_ConvertT(void);
float DS18B20_ReadT(void);

#endif

函数

//Key.c
#include <REGX52.H>
#include "Delay.h"

unsigned char Key_KeyNumber;

/**
  * @brief  获取按键键码
  * @param  无
  * @retval 按下按键的键码,范围:0,1~4,0表示无按键按下
  */
unsigned char Key(void)
{
	unsigned char Temp=0;
	Temp=Key_KeyNumber;
	Key_KeyNumber=0;
	return Temp;
}

/**
  * @brief  获取当前按键的状态,无消抖及松手检测
  * @param  无
  * @retval 按下按键的键码,范围:0,1~4,0表示无按键按下
  */
unsigned char Key_GetState()
{
	unsigned char KeyNumber=0;
	
	if(P3_1==0){KeyNumber=1;}
	if(P3_0==0){KeyNumber=2;}
	if(P3_2==0){KeyNumber=3;}
	if(P3_3==0){KeyNumber=4;}
	
	return KeyNumber;
}

/**
  * @brief  按键驱动函数,在中断中调用
  * @param  无
  * @retval 无
  */
void Key_Loop(void)
{
	static unsigned char NowState,LastState;
	LastState=NowState;				//按键状态更新
	NowState=Key_GetState();		//获取当前按键状态
	//如果上个时间点按键按下,这个时间点未按下,则是松手瞬间,以此避免消抖和松手检测
	if(LastState==1 && NowState==0)
	{
		Key_KeyNumber=1;
	}
	if(LastState==2 && NowState==0)
	{
		Key_KeyNumber=2;
	}
	if(LastState==3 && NowState==0)
	{
		Key_KeyNumber=3;
	}
	if(LastState==4 && NowState==0)
	{
		Key_KeyNumber=4;
	}
}
//OneWire.c  函数改动部分(跳出中断)单总线不能中断
#include <REGX52.H>

//引脚定义
sbit OneWire_DQ=P3^7;

/**
  * @brief  单总线初始化
  * @param  无
  * @retval 从机响应位,0为响应,1为未响应
  */
unsigned char OneWire_Init(void)
{
	unsigned char i;
	unsigned char AckBit;
	EA=0; 					//关闭中断
	OneWire_DQ=1;
	OneWire_DQ=0;
	i = 247;while (--i);		//Delay 500us
	OneWire_DQ=1;
	i = 32;while (--i);			//Delay 70us
	AckBit=OneWire_DQ;
	i = 247;while (--i);		//Delay 500us
	EA=1; 					//打开中断
	return AckBit;
}
.
.
.
//main
#include <REGX52.H>
#include "LCD1602.h"
#include "DS18B20.h"
#include "Delay.h"
#include "AT24C02.h"
#include "Key.h"
#include "Timer0.h"
#include "Buzzer.h"

float T,TShow;
char TLow,THigh;
unsigned char KeyNum;

void main()
{
	DS18B20_ConvertT();		//上电先转换一次温度,防止第一次读数据错误
	Delay(1000);			//等待转换完成
	THigh=AT24C02_ReadByte(0);	//读取温度阈值数据
	TLow=AT24C02_ReadByte(1);
	if(THigh>125 || TLow<-55 || THigh<=TLow)
	{
		THigh=20;			//如果阈值非法,则设为默认值
		TLow=15;
	}
	LCD_Init();
	LCD_ShowString(1,1,"T:");
	LCD_ShowString(2,1,"TH:");
	LCD_ShowString(2,9,"TL:");
	LCD_ShowSignedNum(2,4,THigh,3);
	LCD_ShowSignedNum(2,12,TLow,3);
	Timer0_Init();
	
	while(1)
	{
		KeyNum=Key();
		
		/*温度读取及显示*/
		DS18B20_ConvertT();	//转换温度
		T=DS18B20_ReadT();	//读取温度
		if(T<0)				//如果温度小于0
		{
			LCD_ShowChar(1,3,'-');	//显示负号
			TShow=-T;		//将温度变为正数
		}
		else				//如果温度大于等于0
		{
			LCD_ShowChar(1,3,'+');	//显示正号
			TShow=T;
		}
		LCD_ShowNum(1,4,TShow,3);		//显示温度整数部分
		LCD_ShowChar(1,7,'.');		//显示小数点
		LCD_ShowNum(1,8,(unsigned long)(TShow*100)%100,2);//显示温度小数部分
		
		/*阈值判断及显示*/
		if(KeyNum)
		{
			if(KeyNum==1)	//K1按键,THigh自增
			{
				THigh++;
				if(THigh>125){THigh=125;}
			}
			if(KeyNum==2)	//K2按键,THigh自减
			{
				THigh--;
				if(THigh<=TLow){THigh++;}
			}
			if(KeyNum==3)	//K3按键,TLow自增
			{
				TLow++;
				if(TLow>=THigh){TLow--;}
			}
			if(KeyNum==4)	//K4按键,TLow自减
			{
				TLow--;
				if(TLow<-55){TLow=-55;}
			}
			LCD_ShowSignedNum(2,4,THigh,3);	//显示阈值数据
			LCD_ShowSignedNum(2,12,TLow,3);
			AT24C02_WriteByte(0,THigh);		//写入到At24C02中保存
			Delay(5);
			AT24C02_WriteByte(1,TLow);
			Delay(5);
		}
		if(T>THigh)			//越界判断
		{
			LCD_ShowString(1,13,"OV:H");
			Buzzer_Time(100); //报警器函数
		}
		else if(T<TLow)
		{
			LCD_ShowString(1,13,"OV:L");
			Buzzer_Time(100);
		}
		else
		{
			LCD_ShowString(1,13,"    ");
		}
	}
}

void Timer0_Routine() interrupt 1
{
	static unsigned int T0Count;
	TL0 = 0x18;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
	T0Count++;
	if(T0Count>=20)
	{
		T0Count=0;
		Key_Loop();	//每20ms调用一次按键驱动函数
	}
}


14. LCD1602

LCD与数码管引脚冲突

原理(图片)

代码

头文件

//LCD_1602.h
#ifndef __LCD1602_H__
#define __LCD1602_H__

void LCD_Init(void);
void LCD_ShowChar(unsigned char Line,unsigned char Column,unsigned char Char);
void LCD_ShowString(unsigned char Line,unsigned char Column,unsigned char *String);
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_WriteCommand(unsigned char Command);

#endif

函数

//LCD_1602.c
#include <REGX52.H>

//引脚定义
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_E=P2^7;
#define LCD_DataPort P0

/**
  * @brief  LCD1602延时函数,12MHz调用可延时1ms
  * @param  无
  * @retval 无
  */
void LCD_Delay()		//@12.000MHz 1ms
{
	unsigned char i, j;

	i = 2;
	j = 239;
	do
	{
		while (--j);
	} while (--i);
}

/**
  * @brief  LCD1602写命令
  * @param  Command 要写入的命令
  * @retval 无
  */
void LCD_WriteCommand(unsigned char Command)
{
	LCD_RS=0;
	LCD_RW=0;
	LCD_DataPort=Command;
	LCD_E=1;
	LCD_Delay();
	LCD_E=0;
	LCD_Delay();
}

/**
  * @brief  LCD1602写数据
  * @param  Data 要写入的数据
  * @retval 无
  */
void LCD_WriteData(unsigned char Data)
{
	LCD_RS=1;
	LCD_RW=0;
	LCD_DataPort=Data;
	LCD_E=1;
	LCD_Delay();
	LCD_E=0;
	LCD_Delay();
}

/**
  * @brief  LCD1602初始化函数
  * @param  无
  * @retval 无
  */
void LCD_Init(void)
{
	LCD_WriteCommand(0x38);
	LCD_WriteCommand(0x0C);
	LCD_WriteCommand(0x06);
	LCD_WriteCommand(0x01);
}

/**
  * @brief  LCD1602设置光标位置
  * @param  Line 行位置,范围:1~2
  * @param  Column 列位置,范围:1~16
  * @retval 无
  */
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{
	if(Line==1)
	{
		LCD_WriteCommand(0x80|(Column-1));
	}
	else
	{
		LCD_WriteCommand(0x80|(Column-1)+0x40);
	}
}

/**
  * @brief  在LCD1602指定位置上显示一个字符
  * @param  Line 行位置,范围:1~2
  * @param  Column 列位置,范围:1~16
  * @param  Char 要显示的字符
  * @retval 无
  */
void LCD_ShowChar(unsigned char Line,unsigned char Column,unsigned char Char)
{
	LCD_SetCursor(Line,Column);
	LCD_WriteData(Char);
}

/**
  * @brief  在LCD1602指定位置开始显示所给字符串
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  String 要显示的字符串
  * @retval 无
  */
void LCD_ShowString(unsigned char Line,unsigned char Column,unsigned char *String)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=0;String[i]!='\0';i++)
	{
		LCD_WriteData(String[i]);
	}
}

/**
  * @brief  返回值=X的Y次方
  */
int LCD_Pow(int X,int Y)
{
	unsigned char i;
	int Result=1;
	for(i=0;i<Y;i++)
	{
		Result*=X;
	}
	return Result;
}

/**
  * @brief  在LCD1602指定位置开始显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~65535
  * @param  Length 要显示数字的长度,范围:1~5
  * @retval 无
  */
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		LCD_WriteData('0'+Number/LCD_Pow(10,i-1)%10);
	}
}

/**
  * @brief  在LCD1602指定位置开始以有符号十进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:-32768~32767
  * @param  Length 要显示数字的长度,范围:1~5
  * @retval 无
  */
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{
	unsigned char i;
	unsigned int Number1;
	LCD_SetCursor(Line,Column);
	if(Number>=0)
	{
		LCD_WriteData('+');
		Number1=Number;
	}
	else
	{
		LCD_WriteData('-');
		Number1=-Number;
	}
	for(i=Length;i>0;i--)
	{
		LCD_WriteData('0'+Number1/LCD_Pow(10,i-1)%10);
	}
}

/**
  * @brief  在LCD1602指定位置开始以十六进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~0xFFFF
  * @param  Length 要显示数字的长度,范围:1~4
  * @retval 无
  */
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i;
	unsigned char SingleNumber;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		SingleNumber=Number/LCD_Pow(16,i-1)%16;
		if(SingleNumber<10)
		{
			LCD_WriteData('0'+SingleNumber);
		}
		else
		{
			LCD_WriteData('A'+SingleNumber-10);
		}
	}
}

/**
  * @brief  在LCD1602指定位置开始以二进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~1111 1111 1111 1111
  * @param  Length 要显示数字的长度,范围:1~16
  * @retval 无
  */
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		LCD_WriteData('0'+Number/LCD_Pow(2,i-1)%2);
	}
}
//main.c
#include <REGX52.H>
#include "LCD1602.h"
#include "Delay.h"

void main()
{
	LCD_Init();						//LCD初始化
	LCD_ShowChar(1,1,'A');			//在1行1列显示字符A
	LCD_ShowString(1,3,"Hello");	//在1行3列显示字符串Hello
	LCD_ShowNum(1,9,66,2);			//在1行9列显示数字66,长度为2
	LCD_ShowSignedNum(1,12,-88,2);	//在1行12列显示有符号数字-88,长度为2
	LCD_ShowHexNum(2,1,0xA5,2);		//在2行1列显示十六进制数字0xA5,长度为2
	LCD_ShowBinNum(2,4,0xA5,8);		//在2行4列显示二进制数字0xA5,长度为8
	LCD_ShowChar(2,13,0xDF);		//在2行13列显示编码为0xDF的字符 
	LCD_ShowChar(2,14,'C');			//在2行14列显示字符C
	LCD_ShowString(1,16,"Welcome to China!"); //字符往左移动
	while(1)
	{
		LCD_WriteCommand(0x18);
		Delay(500);
	}
}


15. 直流电机驱动(PWM)

原理

五线四项步进电机

15.1 LED呼吸灯

#include <REGX52.H>

sbit LED=P2^0;

void Delay(unsigned int t)
{
	while(t--);
}

void main()
{
	unsigned char Time,i;
	
	while(1)
	{
		for(Time=0;Time<100;Time++) //暗->亮
		{
			for(i=0;i<20;i++)
			{
				LED=0;
				Delay(Time);
				LED=1;
				Delay(100-Time);				
			}		
		}
		
		for(Time=100;Time>0;Time--) //亮->暗
		{
			for(i=0;i<20;i++)
			{
				LED=0;
				Delay(Time);
				LED=1;
				Delay(100-Time);				
			}		
		}
	}
}

15.2 直流电机调速

//Timer0.c
#include <REGX52.H>

/**
  * @brief  定时器0初始化,100us@12.000MHz
  * @param  无
  * @retval 无
  */
void Timer0_Init(void)
{
	TMOD &= 0xF0;		//设置定时器模式
	TMOD |= 0x01;		//设置定时器模式
	TL0 = 0x9C;		//设置定时初值
	TH0 = 0xFF;		//设置定时初值
	TF0 = 0;		//清除TF0标志
	TR0 = 1;		//定时器0开始计时
	ET0=1;
	EA=1;
	PT0=0;
}
//main.c
#include <REGX52.H>
#include "Delay.h"
#include "Key.h"
#include "Nixie.h"
#include "Timer0.h"

sbit Motor=P1^0;  //给1电机转

unsigned char Counter,Compare;
unsigned char KeyNum,Speed;

void main()
{
	Timer0_Init();
	Compare=90;
	while(1)
	{
		KeyNum=Key();
		if(KeyNum==1)
		{
			Speed++;
			Speed%=4;
			if(Speed==0) {Compare=0;} 
			if(Speed==1) {Compare=50;}
			if(Speed==2) {Compare=75;}
			if(Speed==3) {Compare=100;}
		}
		Nixie(1,Speed); //Speed越大,Compare越大,所占比也就越大
	}
}

void Timer0_Routine() interrupt 1
{
	TL0 = 0x9C;		//设置定时初值
	TH0 = 0xFF;		//设置定时初值
	Counter++;
	Counter%=100;
	if(Counter<Compare)
	{
		Motor=1;
	}
	else
	{
		Motor=0;
	}
}


16. AD/DA

原理

AD:模拟--数字转换,模拟信号变为计算机可操作的数字信号

DA:数字--模拟转换

ADC

DAC

AD模数转换

头文件

//XPT2046.h
#ifndef __XPT2046_H_
#define __XPT2046_H_
//8位
#define XPT2046_XP_8    0x9C
#define XPT2046_YP_8    0xDC
#define XPT2046_VBAT_8  0xAC
#define XPT2046_AUX_8   0xEC
//12位
#define XPT2046_XP_12    0x94
#define XPT2046_YP_12    0xD4
#define XPT2046_VBAT_12  0xA4
#define XPT2046_AUX_12   0xE4

unsigned int XPT2046_ReadAD(unsigned char Command);

#endif

函数

//XPT2046.c
#include <REGX52.H>
//引脚定义
sbit XPT2046_CS=P3^5;
sbit XPT2046_DCLK=P3^6;
sbit XPT2046_DIN=P3^4;
sbit XPT2046_DOUT=P3^7;

/**
  * @brief  ZPT2046读取AD值
  * @param  Command 命令字,范围:头文件内定义的宏,结尾的数字表示转换的位数
  * @retval AD转换后的数字量,范围:8位为0~255,12位为0~4095
  */
unsigned int XPT2046_ReadAD(unsigned char Command)
{
	unsigned  char i;
	unsigned int ADValue=0;
	XPT2046_DCLK=0;
	XPT2046_CS=0;
	
	for(i=0;i<8;i++)
	{
		XPT2046_DIN=Command&(0x80>>i);
		XPT2046_DCLK=1;
		XPT2046_DCLK=0;
	}

	for(i=0;i<16;i++)
	{
		
	  XPT2046_DCLK=1;
		XPT2046_DCLK=0;
		if(XPT2046_DOUT) {ADValue|=(0x8000>>i);}
	}
	XPT2046_CS=1;
	if(Command&0x08)
	{
		return ADValue>>8; //8位显示
	}
	else return ADValue>>4; //12位显示
}
//main.c
#include <REGX52.H>
#include "LCD1602.h"
#include "Delay.h"
#include "XPT2046.h"

unsigned int ADValue;

void main()
{
	LCD_Init();
	LCD_ShowString(1,1,"ADJ NTC RG");
	while(1)
	{
		ADValue= XPT2046_ReadAD(XPT2046_XP_8); //读取AIN0,可调电阻
		LCD_ShowNum(2,1,ADValue,3);
		ADValue= XPT2046_ReadAD(XPT2046_YP_8); //读取AIN1,热敏电阻
		LCD_ShowNum(2,5,ADValue,3);
		ADValue= XPT2046_ReadAD(XPT2046_VBAT_8);  //读取AIN2,光敏电阻
		LCD_ShowNum(2,9,ADValue,3);
		Delay(10);
		
	}
}

DA数模转换

//main.c 直流电机微改
#include <REGX52.H>
#include "Delay.h"
#include "Timer0.h"

sbit DA=P2^1;

unsigned char Counter,Compare;	//计数值和比较值,用于输出PWM
unsigned char i;

void main()
{
	Timer0_Init();
	while(1)
	{
		for(i=0;i<100;i++)
		{
			Compare=i;			//设置比较值,改变PWM占空比
			Delay(10);
		}
		for(i=100;i>0;i--)
		{
			Compare=i;			//设置比较值,改变PWM占空比
			Delay(10);
		}
	}
}

void Timer0_Routine() interrupt 1
{
	TL0 = 0x9C;		//设置定时初值
	TH0 = 0xFF;		//设置定时初值
	Counter++;
	Counter%=100;	//计数值变化范围限制在0~99
	if(Counter<Compare)	//计数值小于比较值
	{
		DA=1;		//输出1
	}
	else				//计数值大于比较值
	{
		DA=0;		//输出0
	}
}


17. 红外遥控

原理

红外接收模块

遥控器键码

遥控器

17.1 红外遥控

头文件

//Int0.h
#ifndef __INT0_H__
#define __INT0_H__

void Int0_Init(void);

#endif
//Timer0.h
#ifndef __TIMER0_H__
#define __TIMER0_H__

void Timer0_Init(void);
void Timer0_SetCounter(unsigned int Value);
unsigned int Timer0_GetCounter(void);
void Timer0_Run(unsigned char Flag);

#endif
//IR.h
#ifndef __IR_H__
#define __IR_H__

#define IR_POWER		0x45
#define IR_MODE			0x46
#define IR_MUTE			0x47
#define IR_START_STOP	0x44
#define IR_PREVIOUS		0x40
#define IR_NEXT			0x43
#define IR_EQ			0x07
#define IR_VOL_MINUS	0x15
#define IR_VOL_ADD		0x09
#define IR_0			0x16
#define IR_RPT			0x19
#define IR_USD			0x0D
#define IR_1			0x0C
#define IR_2			0x18
#define IR_3			0x5E
#define IR_4			0x08
#define IR_5			0x1C
#define IR_6			0x5A
#define IR_7			0x42
#define IR_8			0x52
#define IR_9			0x4A

void IR_Init(void);
unsigned char IR_GetDataFlag(void);
unsigned char IR_GetRepeatFlag(void);
unsigned char IR_GetAddress(void);
unsigned char IR_GetCommand(void);

#endif

函数

//Int0.c
#include <REGX52.H>

/**
  * @brief  外部中断0初始化
  * @param  无
  * @retval 无
  */
void Int0_Init(void)
{
	IT0=1;
	IE0=0;
	EX0=1;
	EA=1;
	PX0=1;
}

/*外部中断0中断函数模板
void Int0_Routine(void) interrupt 0
{
	
}
*/
//Timer0.c
#include <REGX52.H>

/**
  * @brief  定时器0初始化
  * @param  无
  * @retval 无
  */
void Timer0_Init(void)
{
	TMOD &= 0xF0;		//设置定时器模式
	TMOD |= 0x01;		//设置定时器模式
	TL0 = 0;		//设置定时初值
	TH0 = 0;		//设置定时初值
	TF0 = 0;		//清除TF0标志
	TR0 = 0;		//定时器0不计时
}

/**
  * @brief  定时器0设置计数器值
  * @param  Value,要设置的计数器值,范围:0~65535
  * @retval 无
  */
void Timer0_SetCounter(unsigned int Value)
{
	TH0=Value/256;
	TL0=Value%256;
}

/**
  * @brief  定时器0获取计数器值
  * @param  无
  * @retval 计数器值,范围:0~65535
  */
unsigned int Timer0_GetCounter(void)
{
	return (TH0<<8)|TL0;
}

/**
  * @brief  定时器0启动停止控制
  * @param  Flag 启动停止标志,1为启动,0为停止
  * @retval 无
  */
void Timer0_Run(unsigned char Flag)
{
	TR0=Flag;
}
//IR.c
#include <REGX52.H>
#include "Timer0.h"
#include "Int0.h"

unsigned int IR_Time;
unsigned char IR_State;

unsigned char IR_Data[4];
unsigned char IR_pData;

unsigned char IR_DataFlag;
unsigned char IR_RepeatFlag;
unsigned char IR_Address;
unsigned char IR_Command;

/**
  * @brief  红外遥控初始化
  * @param  无
  * @retval 无
  */
void IR_Init(void)
{
	Timer0_Init();
	Int0_Init();
}

/**
  * @brief  红外遥控获取收到数据帧标志位
  * @param  无
  * @retval 是否收到数据帧,1为收到,0为未收到
  */
unsigned char IR_GetDataFlag(void)
{
	if(IR_DataFlag)
	{
		IR_DataFlag=0;
		return 1;
	}
	return 0;
}

/**
  * @brief  红外遥控获取收到连发帧标志位
  * @param  无
  * @retval 是否收到连发帧,1为收到,0为未收到
  */
unsigned char IR_GetRepeatFlag(void)
{
	if(IR_RepeatFlag)
	{
		IR_RepeatFlag=0;
		return 1;
	}
	return 0;
}

/**
  * @brief  红外遥控获取收到的地址数据
  * @param  无
  * @retval 收到的地址数据
  */
unsigned char IR_GetAddress(void)
{
	return IR_Address;
}

/**
  * @brief  红外遥控获取收到的命令数据
  * @param  无
  * @retval 收到的命令数据
  */
unsigned char IR_GetCommand(void)
{
	return IR_Command;
}

//外部中断0中断函数,下降沿触发执行
void Int0_Routine(void) interrupt 0
{
	if(IR_State==0)				//状态0,空闲状态
	{
		Timer0_SetCounter(0);	//定时计数器清0
		Timer0_Run(1);			//定时器启动
		IR_State=1;				//置状态为1
	}
	else if(IR_State==1)		//状态1,等待Start信号或Repeat信号
	{
		IR_Time=Timer0_GetCounter();	//获取上一次中断到此次中断的时间
		Timer0_SetCounter(0);	//定时计数器清0
		//如果计时为13.5ms,则接收到了Start信号(判定值在12MHz晶振下为13500,在11.0592MHz晶振下为12442)
		if(IR_Time>12442-500 && IR_Time<12442+500)
		{
			IR_State=2;			//置状态为2
		}
		//如果计时为11.25ms,则接收到了Repeat信号(判定值在12MHz晶振下为11250,在11.0592MHz晶振下为10368)
		else if(IR_Time>10368-500 && IR_Time<10368+500)
		{
			IR_RepeatFlag=1;	//置收到连发帧标志位为1
			Timer0_Run(0);		//定时器停止
			IR_State=0;			//置状态为0
		}
		else					//接收出错
		{
			IR_State=1;			//置状态为1
		}
	}
	else if(IR_State==2)		//状态2,接收数据
	{
		IR_Time=Timer0_GetCounter();	//获取上一次中断到此次中断的时间
		Timer0_SetCounter(0);	//定时计数器清0
		//如果计时为1120us,则接收到了数据0(判定值在12MHz晶振下为1120,在11.0592MHz晶振下为1032)
		if(IR_Time>1032-500 && IR_Time<1032+500)
		{
			IR_Data[IR_pData/8]&=~(0x01<<(IR_pData%8));	//数据对应位清0
			IR_pData++;			//数据位置指针自增
		}
		//如果计时为2250us,则接收到了数据1(判定值在12MHz晶振下为2250,在11.0592MHz晶振下为2074)
		else if(IR_Time>2074-500 && IR_Time<2074+500)
		{
			IR_Data[IR_pData/8]|=(0x01<<(IR_pData%8));	//数据对应位置1
			IR_pData++;			//数据位置指针自增
		}
		else					//接收出错
		{
			IR_pData=0;			//数据位置指针清0
			IR_State=1;			//置状态为1
		}
		if(IR_pData>=32)		//如果接收到了32位数据
		{
			IR_pData=0;			//数据位置指针清0
			if((IR_Data[0]==~IR_Data[1]) && (IR_Data[2]==~IR_Data[3]))	//数据验证
			{
				IR_Address=IR_Data[0];	//转存数据
				IR_Command=IR_Data[2];
				IR_DataFlag=1;	//置收到连发帧标志位为1
			}
			Timer0_Run(0);		//定时器停止
			IR_State=0;			//置状态为0
		}
	}
}
//main.c
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "IR.h"

unsigned char Num;
unsigned char Address;
unsigned char Command;

void main()
{
	LCD_Init();
	LCD_ShowString(1,1,"ADDR  CMD  NUM");
	LCD_ShowString(2,1,"00    00   000");
	
	IR_Init();
	
	while(1)
	{
		if(IR_GetDataFlag() || IR_GetRepeatFlag())	//如果收到数据帧或者收到连发帧
		{
			Address=IR_GetAddress();		//获取遥控器地址码
			Command=IR_GetCommand();		//获取遥控器命令码
			
			LCD_ShowHexNum(2,1,Address,2);	//显示遥控器地址码
			LCD_ShowHexNum(2,7,Command,2);	//显示遥控器命令码
			
			if(Command==IR_VOL_MINUS)		//如果遥控器VOL-按键按下
			{
				Num--;						//Num自减
			}
			if(Command==IR_VOL_ADD)			//如果遥控器VOL+按键按下
			{
				Num++;						//Num自增
			}
			
			LCD_ShowNum(2,12,Num,3);		//显示Num
		}
	}
}

17.2 红外遥控电机调速

头文件

//Timer1.h
#ifndef __TIMER1_H__
#define __TIMER1___

void Timer1_Init(void);

#endif
//Motor.h
#ifndef __MOTOR_H__
#define __MOTOR_H__

void Motor_Init(void);
void Motor_SetSpeed(unsigned char Speed);

#endif
//Int0.h
#ifndef __INT0_H__
#define __INT0_H__

void Int0_Init(void);

#endif
//Timer0.h
#ifndef __TIMER0_H__
#define __TIMER0_H__

void Timer0_Init(void);
void Timer0_SetCounter(unsigned int Value);
unsigned int Timer0_GetCounter(void);
void Timer0_Run(unsigned char Flag);

#endif

函数

//Timer1.c
#include <REGX52.H>

/**
  * @brief  定时器1初始化,100us@12.000MHz
  * @param  无
  * @retval 无
  */
void Timer1_Init(void)
{
	TMOD &= 0x0F;		//设置定时器模式
	TMOD |= 0x10;		//设置定时器模式
	TL1 = 0x9C;		//设置定时初值
	TH1 = 0xFF;		//设置定时初值
	TF1 = 0;		//清除TF1标志
	TR1 = 1;		//定时器1开始计时
	ET1=1;
	EA=1;
	PT1=0;
}

/*定时器中断函数模板
void Timer1_Routine() interrupt 3
{
	static unsigned int T1Count;
	TL1 = 0x9C;		//设置定时初值
	TH1 = 0xFF;		//设置定时初值
	T1Count++;
	if(T1Count>=1000)
	{
		T1Count=0;
		
	}
}
*/
//Motor.c
#include <REGX52.H>
#include "Timer1.h"

//引脚定义
sbit Motor=P1^0;

unsigned char Counter,Compare;

/**
  * @brief  电机初始化
  * @param  无
  * @retval 无
  */
void Motor_Init(void)
{
	Timer1_Init();
}

/**
  * @brief  电机设置速度
  * @param  Speed 要设置的速度,范围0~100
  * @retval 无
  */
void Motor_SetSpeed(unsigned char Speed)
{
	Compare=Speed;
}

//定时器1中断函数
void Timer1_Routine() interrupt 3
{
	TL1 = 0x9C;		//设置定时初值
	TH1 = 0xFF;		//设置定时初值
	Counter++;
	Counter%=100;	//计数值变化范围限制在0~99
	if(Counter<Compare)	//计数值小于比较值
	{
		Motor=1;		//输出1
	}
	else				//计数值大于比较值
	{
		Motor=0;		//输出0
	}
}
//Int0.c
#include <REGX52.H>

/**
  * @brief  外部中断0初始化
  * @param  无
  * @retval 无
  */
void Int0_Init(void)
{
	IT0=1;
	IE0=0;
	EX0=1;
	EA=1;
	PX0=1;
}

/*外部中断0中断函数模板
void Int0_Routine(void) interrupt 0
{
	
}
*/
#include <REGX52.H>

/**
  * @brief  定时器0初始化
  * @param  无
  * @retval 无
  */
void Timer0_Init(void)
{
	TMOD &= 0xF0;		//设置定时器模式
	TMOD |= 0x01;		//设置定时器模式
	TL0 = 0;		//设置定时初值
	TH0 = 0;		//设置定时初值
	TF0 = 0;		//清除TF0标志
	TR0 = 0;		//定时器0不计时
}

/**
  * @brief  定时器0设置计数器值
  * @param  Value,要设置的计数器值,范围:0~65535
  * @retval 无
  */
void Timer0_SetCounter(unsigned int Value)
{
	TH0=Value/256;
	TL0=Value%256;
}

/**
  * @brief  定时器0获取计数器值
  * @param  无
  * @retval 计数器值,范围:0~65535
  */
unsigned int Timer0_GetCounter(void)
{
	return (TH0<<8)|TL0;


/**
  * @brief  定时器0启动停止控制
  * @param  Flag 启动停止标志,1为启动,0为停止
  * @retval 无
  */
void Timer0_Run(unsigned char Flag)
{
	TR0=Flag;
}
//main.c
#include <REGX52.H>
#include "Delay.h"
#include "Key.h"
#include "Nixie.h"
#include "Motor.h"
#include "IR.h"

unsigned char Command,Speed;

void main()
{
	Motor_Init();
	IR_Init();
	while(1)
	{
		if(IR_GetDataFlag())	//如果收到数据帧
		{
			Command=IR_GetCommand();		//获取遥控器命令码
			
			if(Command==IR_0){Speed=0;}		//根据遥控器命令码设置速度
			if(Command==IR_1){Speed=1;}
			if(Command==IR_2){Speed=2;}
			if(Command==IR_3){Speed=3;}
			
			if(Speed==0){Motor_SetSpeed(0);}	//速度输出
			if(Speed==1){Motor_SetSpeed(50);}
			if(Speed==2){Motor_SetSpeed(75);}
			if(Speed==3){Motor_SetSpeed(100);}
		}
		Nixie(1,Speed);						//数码管显示速度
	}
}

芜湖,本文章于 2022-12-14 完结散花~~~ 后续会持续优化本文

下面开启STM32的旅途?

--> [alert]本文章已完结,持续优化中[/alert]前言必读51单片机入门教程(上篇)(代码+个人理解) – Echo (liveout.cn)GitHub仓库链接:https://github.com/PGwind/51code这篇文章是记录我粗略学习51单片机的一些代码,我会加些个人理解以及注释在里面。因为是囫囵吞枣式学习,所以质量不是很好,后期我会慢慢优化 ?如果你想要学习单片机,可...

前言

必读

这篇文章是记录我粗略学习51单片机的一些代码,我会加些个人理解以及注释在里面。

因为是囫囵吞枣式学习,所以质量不是很好,后期我会慢慢优化 ?

如果你想要学习单片机,可以观看下面的B站教程并配合本文档学习

本文章使用的51单片机是 普中STC89C52RC

51单片机入门教程(下篇)(代码+个人理解) – Echo (liveout.cn)

GitHub仓库链接:https://github.com/PGwind/51code

教程

推荐B站视频: 【51单片机入门教程-2020版 程序全程纯手打 从零开始入门】 https://www.bilibili.com/video/BV1Mb411e7re/?share_source=copy_web&vd_source=55024add0415795a359bd7b29ca21142(应该都知道吧)。

资源

B站江科大资源 链接:https://pan.baidu.com/s/1dLED_1VqL66qYItLl5ic4A?pwd=1111 提取码:1111

普中 链接:https://pan.baidu.com/s/1dNCHm9lLMP8pe3rZu3ktZQ?pwd=1111 提取码:1111


1. 入门

原理: 单片机核心

cpu通过配置寄存器来控制驱动器,来控制硬件电路

寄存器:连接软硬件的媒介

单片机核心

数据类型

C51数据类型

2进制----16进制转换

16base


2. 点灯

原理

LED模块

2.1 点亮第一个LED

#include <REGX52.H> //右键添加头文件
void main()
{   //2进制转16进制,前缀为 0x 87654321 8个灯序号对应进制
    // 8个1分别为8个led,0则是负极点亮。最后一个0是D1,第一个是D8
    P2 = 0xFE; //1111 1110 点亮第1个led
    P2 = 0x7F; //0111 1111 点亮第8个led
    P2 = 0x5F; //0101 1111 led 6,8亮
    P2 = 0xAA; //1010 1010 led 1,3,5,7亮
    P2 = 0x55; //0101 0101 led 2,4,6,8亮
	while (1) //程序一直运行
	{
		
	}
}

2.2 LED闪烁

#include <REGX52.H>
#include <INTRINS.H>
void Delay500ms()		//@11.0592MHz 500ms延迟函数
{
	unsigned char i, j, k;
    
	_nop_(); //去掉nop不用加头文件#include <INTRINS.H>
	i = 4;
	j = 129;
	k = 119;
	do
	{
		do
		{
			while (--k);
		} while (--j);
	} while (--i);
}

void main()
{
	
	while(1)
	{
		P2 = 0xFE; 
		Delay500ms(); //延迟500ms
	    P2 = 0xFF;  
		Delay500ms();
	} 
}

2.3 LED流水灯

#include <REGX52.H>

void Delay500ms()		//@11.0592MHz 延迟500ms
{
	unsigned char i, j, k;
    
	i = 4;
	j = 129;
	k = 119;
	do
	{
		do
		{
			while (--k);
		} while (--j);
	} while (--i);
}

void main()
{
		while(1)
		{
			P2 = 0xFE; //1111 1110
			Delay500ms();
			P2 = 0xFD; //1111 1101
			Delay500ms();
			P2 = 0xFB; //1111 1011
			Delay500ms();
			P2 = 0xF7; //1111 0111
			Delay500ms();
			P2 = 0xEF; //1110 1111
			Delay500ms();
			P2 = 0xDF; //1101 1111
			Delay500ms();
			P2 = 0xBF; //1011 1111
			Delay500ms();
			P2 = 0x7F; //0111 1111
			Delay500ms();
		}
}

2.4 LED流水灯Plus

#include <REGX52.H>
void Delay1ms(unsigned int xms)		//@11.0592MHz
{ //xms 为延迟秒数
	unsigned char i, j;
  while(xms)
	{
		
		i = 2;
		j = 199;
		do
		{
			while (--j);
		} while (--i);
		xms--;
	}
}

void main()
{
	while(1)
		{
			P2 = 0xFE; //1111 1110
			Delay1ms(500); //延迟500ms
			P2 = 0xFD; //1111 1101
			Delay1ms(100); //100ms
			P2 = 0xFB; //1111 1011
			Delay1ms(1000); //1000ms
			P2 = 0xF7; //1111 0111
			Delay1ms(500);
			P2 = 0xEF; //1110 1111
			Delay1ms(200);
			P2 = 0xDF; //1101 1111
			Delay1ms(500);
			P2 = 0xBF; //1011 1111
			Delay1ms(900);
			P2 = 0x7F; //0111 1111
			Delay1ms(500);
		}
	Delay1ms(500);
}


3.独立按键控制LED

原理(高电平、低电平)

电平和电压是有差别的,高电平指的是与低电平相对的高电压,是电工程上的一种说法。

在逻辑电平中,保证逻辑门的输入为高电平时所允许的最小输入高电平,当输入电平高于输入高电压(Vih)时,则认为输入电平为高电平。

在电子和自动化控制中,分为模拟信号和数字信号,在数字逻辑电子电路中,数字信号是二进制的,即只有0信号和1信号。

高电平表示电压高的状态,记为1,一般规定高电平为3.5~5V
低电平表示电压低的状态, 记为0,一般规定低电平为0~0.25V

按键松开 高电平 1

按键按下 低电平 0

3.1 独立按键控制LED灯灭

#include <REGX52.H>
//P2_0就是单片机上面的一个端口,这个端口就是链接右边第一个led灯的
//等于左边七个LED直接不给信号了,只给右边第一个一个亮的信号
void main()
{
	//P2 = 0xFE;
	P2_0 = 1;
	while(1)
	{ // 按下亮,松开灭
		if(P3_1==0) //P3_1控制第一个按键,即P31
		{
			P2_0 = 0; //P2_0是第一个LED,0则亮
		}
		else
		{
			P2_0=1;
		}
        
        if (P3_0==0)
		{
			P2_1 = 0;
		}
        else
		{
			P2_1 = 1;
		}	
	}
}

3.2 独立按键控制LED状态

#include <REGX52.H>
void Delay1ms(unsigned int xms)		//@11.0592MHz
{
	unsigned char i, j;
	while(xms)
	{
			i = 2;
			j = 199;
			do
			{
				while (--j);
			} while (--i);	
			xms--;
	}
}

void main()
{  //按一次亮,再按一次灭
		while(1)
		{
				if(P3_1==0)
				{
					Delay1ms(20); //按下时消抖
					while(P3_1==0); //松开跳出while循环,不松开一直循环,所以灯不会有反应
					Delay1ms(20); //松开时消抖
					
					P2_0=~P2_0; //取反,灭变亮,亮变灭
				}
		}
}

3.3 独立按键控制LED显示二进制

#include <REGX52.H>
void Delay1ms(unsigned int xms)		//@11.0592MHz
{
	unsigned char i, j;
	while(xms)
	{
			i = 2;
			j = 199;
			do
			{
				while (--j);
			} while (--i);	
			xms--;
	}
}
void main()
{
	  unsigned char LEDNum = 0; 
		while(1)
		{
			if(P3_1==0)
			{
				Delay1ms(20);
				while(P3_1==0)
				Delay1ms(20);
				
				LEDNum++; //0000 0001
				P2 = ~LEDNum; //取反, 1111 1110,第一个LED灯亮
			}
		}
}

3.4 独立按键控制LED移位

0000 0001 -> 0000 0010 -> 0000 0100 ....

0x01<<0 0x01<<1 0x01<<2 ....

#include <REGX52.H>
void Delay1ms(unsigned int xms)		//@11.0592MHz
{
	unsigned char i, j;
	while(xms)
	{
			i = 2;
			j = 199;
			do
			{
				while (--j);
			} while (--i);	
			xms--;
	}
} 
void main()
{
	unsigned char LEDNum = 0;
	P2 = ~0x01; //LED初始化,不然第一次led1不亮
	while(1)
	{
        //按键1右移
		if(P3_1==0) //按键1按下
		{
			Delay1ms(20);
			while(P3_1==0);
			Delay1ms(20);
			
			LEDNum++; //右移
			if (LEDNum>=8)
				LEDNum=0; //重置
			
			P2=~(0x01<<LEDNum);
		}
		
        //按键2左移
		if(P3_0==0) //按键1按下
		{
			Delay1ms(20);
			while(P3_0==0);
			Delay1ms(20);
			
			if(LEDNum==0)
				LEDNum=7; //重置
			else
				LEDNum--; //左移
			
			P2=~(0x01<<LEDNum);
		}
	}
}



4.数码管

LED数码管:由多个发光二极管封装在一起组成“8”字型

输出扫描:显示第一位->显示第二位->显示第三位......,然后快速循环这个过程,最终实现所有数码管同时显示的效果

原理

138验码器

//这里灯的顺序排列我的理解有错误,还没仔细研究。我的单片机111时是Y0,即LED1亮
C(P24)  B(P23)    A(P22)  Y		 LED 8 -> 1
0(4)	0(2)	0(1)	Y7		0 1 1 1 1 1 1 1  LED8
0		0		 1   	 Y6		 1 0 1 1 1 1 1 1  LED7
1		0		 1		 Y2		 1 1 1 1 1 0 1 1  LED3
1		1		 1		 Y0		 1 1 1 1 1 1 1 0  LED1    

 

动态数码管

上面是共阴极,公共端为0则亮。如LED7(引脚9)为0,其他为1。即 1011 1111,则LED7亮,其他不亮。

下面是阴码。LED显示7,则b、c为1,其他0。即 0110 0000

4.1 静态数码管显示

点亮一个

#include <REGX52.H>

void main()
{
    //100,即4,LED4亮
	P2_4 = 1;
	P2_3 = 0;
	P2_2 = 0;
    //P07->P00  0000 0111 ,即0x07,
    //a、b、c为1,即a、b、c亮,显示为7
	P0 = 0x07;
	while(1)
	{
		
	}
}

模块化

#include <REGX52.H>
//显示数字的段码存储在数组中
//0~9 -> 0~9
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,
0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,0x00};

void Nixie(unsigned char Location,Number)
{   //Location为LED灯序号,Number为显示数字
	switch(Location)
	{
		case 1:P2_4=1;P2_3=1;P2_2=1;break;
		case 2:P2_4=1;P2_3=1;P2_2=0;break;
		case 3:P2_4=1;P2_3=0;P2_2=1;break;
		case 4:P2_4=1;P2_3=0;P2_2=0;break;
		case 5:P2_4=0;P2_3=1;P2_2=1;break;
		case 6:P2_4=0;P2_3=1;P2_2=0;break;
		case 7:P2_4=0;P2_3=0;P2_2=1;break;
		case 8:P2_4=0;P2_3=0;P2_2=0;break;
	}
	P0=NixieTable[Number];
}

void main()
{
	Nixie(6,2); //第6个LED显示2
	while(1)
	{
		
	}
}

4.2 动态数码管显示

个人理解:就像早期电影一样,通过视觉残留动态显示。实际上只能有一个LED灯亮

消影:位选 段选 清零 位选 段选 清零

利用延时来抵消人的视觉暂留现象达到消影

#include <REGX52.H>

void Delay1ms(unsigned int xms)		//@11.0592MHz
{
	unsigned char i, j;
	while(xms)
	{
			i = 2;
			j = 199;
			do
			{
				while (--j);
			} while (--i);	
			xms--;
	}
} 

unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,
0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,0x00};

void Nixie(unsigned char Location,Number)
{   
	switch(Location)
	{
		case 1:P2_4=1;P2_3=1;P2_2=1;break;
		case 2:P2_4=1;P2_3=1;P2_2=0;break;
		case 3:P2_4=1;P2_3=0;P2_2=1;break;
		case 4:P2_4=1;P2_3=0;P2_2=0;break;
		case 5:P2_4=0;P2_3=1;P2_2=1;break;
		case 6:P2_4=0;P2_3=1;P2_2=0;break;
		case 7:P2_4=0;P2_3=0;P2_2=1;break;
		case 8:P2_4=0;P2_3=0;P2_2=0;break;
	}
	P0=NixieTable[Number]; //段选
	Delay1ms(1); 
	P0=0x00; //清零
}

void main()
{

	while(1)
	{
			Nixie(1,1); 
			//Delay1ms(500);  //此延迟用于从第一个led1依次显示数字
			Nixie(2,2);
			//Delay1ms(500); 
			Nixie(3,3);
			//Delay1ms(500);
 			Nixie(4,4);
//			Delay1ms(500);
   		Nixie(5,5);
//   		Delay1ms(500);
			Nixie(6,6);
//			Delay1ms(500);
			Nixie(7,7);
//			Delay1ms(500);
			Nixie(8,8);
//			Delay1ms(500);
	}
}


5 .1模块化编程

视频

https://www.bilibili.com/video/BV1Mb411e7re?p=13&vd_source=a1234589a3616351986bc6d13bcbd8f8

代码

头文件

//Delay.h
#ifndef __DELAY_H_
#define __DELAY_H_
void Delay(unsigned int xms)		;
#endif
//Nixie.h
#ifndef __NIXIE_H___
#define __NIXIE_H__
void Nixie(unsigned char Location,Number);
#endif

函数

//main.c
#include <REGX52.H>
#include "Delay.h"
#include "Nixie.h"
void main()
{
	while(1)
	{
		Nixie(1,1);
		Nixie(2,2);
		Nixie(3,3);
	}
}

其他函数

//Delay.c
void Delay(unsigned int xms)		//@11.0592MHz
{
	unsigned char i, j;
	while(xms)
	{
			i = 2;
			j = 199;
			do
			{
				while (--j);
			} while (--i);	
			xms--;
	}
} 

//Nixe.c
#include <REGX52.H>
#include "Delay.h"

unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,
0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,0x00};

void Nixie(unsigned char Location,Number)
{   
	switch(Location)
	{
		case 1:P2_4=1;P2_3=1;P2_2=1;break;
		case 2:P2_4=1;P2_3=1;P2_2=0;break;
		case 3:P2_4=1;P2_3=0;P2_2=1;break;
		case 4:P2_4=1;P2_3=0;P2_2=0;break;
		case 5:P2_4=0;P2_3=1;P2_2=1;break;
		case 6:P2_4=0;P2_3=1;P2_2=0;break;
		case 7:P2_4=0;P2_3=0;P2_2=1;break;
		case 8:P2_4=0;P2_3=0;P2_2=0;break;
	}
	P0=NixieTable[Number]; 
	Delay(1); 
	P0=0x00; 
}

 


5.2 LCD1602调试工具

LCD与数码管引脚冲突

原理

LCD1602接口

LCD调试

代码

记得添加之前定义过的函数和头文件

//LCD1602.h  分别为显示各种进制数组以及字符
#ifndef __LCD1602_H__
#define __LCD1602_H__
void LCD_Init();
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char);
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String);
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length);
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length);
#endif
#include <REGX52.H>

//引脚配置:
sbit LCD_RS=P2^6;
sbit LCD_RW=P2^5;
sbit LCD_EN=P2^7;
#define LCD_DataPort P0

//函数定义:
/**
  * @brief  LCD1602延时函数,12MHz调用可延时1ms
  * @param  无
  * @retval 无
  */
void LCD_Delay()
{
	unsigned char i, j;

	i = 2;
	j = 239;
	do
	{
		while (--j);
	} while (--i);
}

/**
  * @brief  LCD1602写命令
  * @param  Command 要写入的命令
  * @retval 无
  */
void LCD_WriteCommand(unsigned char Command)
{
	LCD_RS=0;
	LCD_RW=0;
	LCD_DataPort=Command;
	LCD_EN=1;
	LCD_Delay();
	LCD_EN=0;
	LCD_Delay();
}

/**
  * @brief  LCD1602写数据
  * @param  Data 要写入的数据
  * @retval 无
  */
void LCD_WriteData(unsigned char Data)
{
	LCD_RS=1;
	LCD_RW=0;
	LCD_DataPort=Data;
	LCD_EN=1;
	LCD_Delay();
	LCD_EN=0;
	LCD_Delay();
}

/**
  * @brief  LCD1602设置光标位置
  * @param  Line 行位置,范围:1~2
  * @param  Column 列位置,范围:1~16
  * @retval 无
  */
void LCD_SetCursor(unsigned char Line,unsigned char Column)
{
	if(Line==1)
	{
		LCD_WriteCommand(0x80|(Column-1));
	}
	else if(Line==2)
	{
		LCD_WriteCommand(0x80|(Column-1+0x40));
	}
}

/**
  * @brief  LCD1602初始化函数
  * @param  无
  * @retval 无
  */
void LCD_Init()
{
	LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵
	LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关
	LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动
	LCD_WriteCommand(0x01);//光标复位,清屏
}

/**
  * @brief  在LCD1602指定位置上显示一个字符
  * @param  Line 行位置,范围:1~2
  * @param  Column 列位置,范围:1~16
  * @param  Char 要显示的字符
  * @retval 无
  */
void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char)
{
	LCD_SetCursor(Line,Column);
	LCD_WriteData(Char);
}

/**
  * @brief  在LCD1602指定位置开始显示所给字符串
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  String 要显示的字符串
  * @retval 无
  */
void LCD_ShowString(unsigned char Line,unsigned char Column,char *String)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=0;String[i]!='\0';i++)
	{
		LCD_WriteData(String[i]);
	}
}

/**
  * @brief  返回值=X的Y次方
  */
int LCD_Pow(int X,int Y)
{
	unsigned char i;
	int Result=1;
	for(i=0;i<Y;i++)
	{
		Result*=X;
	}
	return Result;
}

/**
  * @brief  在LCD1602指定位置开始显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~65535
  * @param  Length 要显示数字的长度,范围:1~5
  * @retval 无
  */
void LCD_ShowNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		LCD_WriteData(Number/LCD_Pow(10,i-1)%10+'0');
	}
}

/**
  * @brief  在LCD1602指定位置开始以有符号十进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:-32768~32767
  * @param  Length 要显示数字的长度,范围:1~5
  * @retval 无
  */
void LCD_ShowSignedNum(unsigned char Line,unsigned char Column,int Number,unsigned char Length)
{
	unsigned char i;
	unsigned int Number1;
	LCD_SetCursor(Line,Column);
	if(Number>=0)
	{
		LCD_WriteData('+');
		Number1=Number;
	}
	else
	{
		LCD_WriteData('-');
		Number1=-Number;
	}
	for(i=Length;i>0;i--)
	{
		LCD_WriteData(Number1/LCD_Pow(10,i-1)%10+'0');
	}
}

/**
  * @brief  在LCD1602指定位置开始以十六进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~0xFFFF
  * @param  Length 要显示数字的长度,范围:1~4
  * @retval 无
  */
void LCD_ShowHexNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i,SingleNumber;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		SingleNumber=Number/LCD_Pow(16,i-1)%16;
		if(SingleNumber<10)
		{
			LCD_WriteData(SingleNumber+'0');
		}
		else
		{
			LCD_WriteData(SingleNumber-10+'A');
		}
	}
}

/**
  * @brief  在LCD1602指定位置开始以二进制显示所给数字
  * @param  Line 起始行位置,范围:1~2
  * @param  Column 起始列位置,范围:1~16
  * @param  Number 要显示的数字,范围:0~1111 1111 1111 1111
  * @param  Length 要显示数字的长度,范围:1~16
  * @retval 无
  */
void LCD_ShowBinNum(unsigned char Line,unsigned char Column,unsigned int Number,unsigned char Length)
{
	unsigned char i;
	LCD_SetCursor(Line,Column);
	for(i=Length;i>0;i--)
	{
		LCD_WriteData(Number/LCD_Pow(2,i-1)%2+'0');
	}
}

 

//输出固定值
#include <REGX52.H>
#include "LCD1602.h"
void main()
{
	LCD_Init();
	LCD_ShowChar(1,1,'A');
	LCD_ShowString(1,3,"Hello");
	LCD_ShowNum(1,9,123,3);
	LCD_ShowSignedNum(1,13,-66,2);
	LCD_ShowHexNum(2,1,0xA8,2);
	LCD_ShowBinNum(2,4,0xAA,8);
	while(1)
	{
	}
}
//输出变量值
#include <REGX52.H>
#include "LCD1602.h"
int Result;
void main()
{
	LCD_Init();
	Result = 1+1;
	LCD_ShowNum(1,1,Result,3);
	while(1)
	{
	}
}
//输出变量值Plus 从0开始显示秒数
#include <REGX52.H>
#include "LCD1602.h"
#include "Delay.h"
int Result=0;
void main()
{
	LCD_Init();
	
	while(1)
	{
		Result++;
		Delay(1000); //1000ms == 1s
		LCD_ShowNum(1,1,Result,3);
	}
}

6. 矩阵键盘

输入扫描:读取第一行(列)->读取第二(列)->读取第三行(列).....,然后快速循环这个过程,最终实现所有按键同时检测的效果

原理

矩阵按键

6.1矩阵键盘

记得添加之前定义过的函数和头文件

头文件

//MatrixKey.h
#ifndef __MATRIXKEY_H_
#define __MATRIXKEY_H_
unsigned char MatrixKey();
#endif

函数

//main.c
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "MatrixKey.h"
unsigned char KeyNum;
void main()
{
	LCD_Init();
	LCD_ShowString(1,1,"Hello Echo"); //第一行显示字符串
	while(1)
	{
		KeyNum=MatrixKey();
		if (KeyNum) //如何没有if,按下一瞬间显示数字,但是松手会重置为0
		{
			LCD_ShowNum(2,1,KeyNum,2);//第2行显示KeyNum,即按下按键键码值
		}
	}
}                      
//MatrixKey.c
#include <REGX52.H>
#include "Delay.h"
//Function Definition:
/**
  * @brief 矩阵键盘读取按键码
  * @param  无
  * @retval KeyNumber 按下按键的键码值
  如果按键按下不放,程序会停留在此函数,松手一瞬间,返回按键键码。没有按键按下时返回0
  */

unsigned char MatrixKey()
{
	unsigned char KeyNumber=0;
	P1 = 0xFF; //初始化
	P1_3=0;  //第1列
	if(P1_7==0) {Delay(20);while(P1_7==0);Delay(20);KeyNumber=1;}//第1行
	if(P1_6==0) {Delay(20);while(P1_6==0);Delay(20);KeyNumber=5;}//第2行
	if(P1_5==0) {Delay(20);while(P1_5==0);Delay(20);KeyNumber=9;}//第3行
	if(P1_4==0) {Delay(20);while(P1_4==0);Delay(20);KeyNumber=13;}//第4行
	
	P1 = 0xFF;
	P1_2=0;  //第2列
	if(P1_7==0) {Delay(20);while(P1_7==0);Delay(20);KeyNumber=2;}
	if(P1_6==0) {Delay(20);while(P1_6==0);Delay(20);KeyNumber=6;}
	if(P1_5==0) {Delay(20);while(P1_5==0);Delay(20);KeyNumber=10;}
	if(P1_4==0) {Delay(20);while(P1_4==0);Delay(20);KeyNumber=14;}
	
	P1 = 0xFF;
	P1_1=0;  //第3列
	if(P1_7==0) {Delay(20);while(P1_7==0);Delay(20);KeyNumber=3;}
	if(P1_6==0) {Delay(20);while(P1_6==0);Delay(20);KeyNumber=7;}
	if(P1_5==0) {Delay(20);while(P1_5==0);Delay(20);KeyNumber=11;}
	if(P1_4==0) {Delay(20);while(P1_4==0);Delay(20);KeyNumber=15;}
	
	P1 = 0xFF;
	P1_0=0;  //第4列
	if(P1_7==0) {Delay(20);while(P1_7==0);Delay(20);KeyNumber=4;}
	if(P1_6==0) {Delay(20);while(P1_6==0);Delay(20);KeyNumber=8;}
	if(P1_5==0) {Delay(20);while(P1_5==0);Delay(20);KeyNumber=12;}
	if(P1_4==0) {Delay(20);while(P1_4==0);Delay(20);KeyNumber=16;}
	
	return KeyNumber; //返回按下按键键码值
}                    

6.2 密码锁

#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "MatrixKey.h"

//unsigned char没有符号位,因此能表示0~255,但是密码肯定不能这么短。
//B站视频教程是unsigned char类型,但是我输入时只能到256,改为int类型就行了
int KeyNum;
int Password,Count;

void main()
{
	LCD_Init();
	LCD_ShowString(1,1,"PassWord");
	while(1)
	{
		KeyNum=MatrixKey();
		if (KeyNum)
		{
			if(KeyNum<=10) //如果S1~S2按键按下,输入密码
            {
                 if(Count<4)
				Password*=10; //密码左移一位
				Password+=KeyNum%10; //获取一位密码
            }
             Count++; 。//计次加一
			LCD_ShowNum(2,1,Password,4); //更新显示
		}
        if(KeyNum==11)   //按下S11按键确认
		{
			if (Password==2345) //密码正确
			{
				LCD_ShowString(1,12,"OK"); //显示OK
                  Password=0; //密码清理
				Count=0;  //计数清理
                  LCD_ShowNum(2,1,Password,4); //更新显示
			}
            else
            {
                 LCD_ShowString(1,12,"ERROR");
				//Delay(2000); //延时2s,然后空格替代,不然OK后面会显示ROR
				Password=0;
				Count=0;
				LCD_ShowNum(2,1,Password,4);
				//LCD_ShowString(1,12,"     "); //空格替代
            }
		}
        if (KeyNum==12) //按键S12手动清零
		{
		   		Password=0; //密码清零
				Count=0;   //计次清零
            	 LCD_ShowString(1,12,"     "); //空格替代
				LCD_ShowNum(2,1,Password,4); //更新显示
                  
		}
	}
}                      


7. 定时器

原理

这章比较难,得多看几遍视频

视频教程:https://www.bilibili.com/video/BV1Mb411e7re/?p=17&spm_id_from=333.1007.top_right_bar_window_history.content.click&vd_source=a1234589a3616351986bc6d13bcbd8f8


定时器个数:3个(T0、T1、T2),T0和T1与传统的51单片机兼容,T2是此型号单片机增加的资源

SySclk:系统时钟,即晶振周期,本开发板上的晶振为11.0592MHz

时钟:给0定时器,给1计数器


基础理解代码:实现led1灯每1s闪烁一次

0~65535 每隔1us计数加一 总共定时时间65535us

给初值6435 离计数器65535 差1000us 所以计数时间为1ms

#include <REGX52.H>

//定时器0初始化
void Timer0_Init() //这个函数可以通过软件直接生成的!!!,但是没有中断,即最后三个
{
    //定时器
	//TMOD=0x01; //0000 0001  高四位  低四位
    TMOD=TMOD&0xF0; //把TMOD的低四位清0,高四位保持不变
    TMOD=TMOD|0x01; //把TMOD的最低位 置为1,高四位保持不变
	TF0=0; //TF0是进入中断程序后,硬件自动清零
	TR0=1; //定时器从0开始计时
    
    //TH0,TL0为二进制8位,单独可计256次   低8位计满256后高8位进1
    //设置定时初值
	TH0=64535/256; //高8位次数 
	TL0=64535%256; //低8位次数
    
    //中断
    ET0=1;
    EA=1;
    PT0=0; //设置优先级
}

//主程序
void main()
{
	Timer0_Init();
	while(1)
	{
		
	}
}

//中断服务程序B
unsigned int T0Count;
void Timer0_Routine() interrupt 1
{
	TH0=64535/256;
	TL0=64535%256; //重新赋值
	T0Count++; //每次加1us,1000次就是1s
	if (T0Count>=1000)
	{
		  T0Count=0; //重新赋值
		  P2_0=~P2_0; //每1s闪烁一次
	}

}

7.1 按键控制LED流水灯模式

头文件

//Delay.h
#ifndef __DELAY_H_
#define __DELAY_H_
void Delay(unsigned int xms);
#endif
//Key.h
#ifndef __KEY_H_
#define __KEY_H_
usigned char Key();
#endif

//Timer0.h
#ifndef __TIMER0_H_
#define __TIMER0_H_

void Timer0_Init();

#endif

函数

//Delay.c
//Function Definition
/**
  * @brief 延迟函数
  * @param  无
  * @retval 1ms
  */
void Delay(unsigned int xms)		//@11.0592MHz
{
	unsigned char i, j;
	while(xms)
	{
			i = 2;
			j = 199;
			do
			{
				while (--j);
			} while (--i);	
			xms--;
	}
} 
	
//Key.c
#include <REGX52.H>
#include "Delay.h"

//Function Definition
/**
  * @brief 获取独立按键键码
  * @param  无
  * @retval 按下按键的键码,范围0~4,无按键按下的返回值为0
  */

unsigned char Key()
{
	unsigned char KeyNum=0;
	
	if(P2_1==0) {Delay(20);while(P2_1==0);Delay(20);KeyNum=1;}
	if(P2_0==0) {Delay(20);while(P2_0==0);Delay(20);KeyNum=2;}
	if(P2_3==0) {Delay(20);while(P2_3==0);Delay(20);KeyNum=3;}
	if(P2_4==0) {Delay(20);while(P2_4==0);Delay(20);KeyNum=4;}
	
	return KeyNum;
}
//Timer0.c
#include <REGX52.H>

//Function Definition
/**
  * @brief 定时器0初始化
  * @param  无
  * @retval 1ms
  */
void Timer0_Init()	//1毫秒@12.000MHz	
{
	TMOD &= 0xF0;	//设置定时器模式	
	TMOD |= 0x01;	//设置定时器模式	
	TL0 = 0x66;		//设置定时初值
	TH0 = 0xFC;		//设置定时初值
	TF0 = 0;        //清除TF0标志 
	TR0 = 1;		//定时器0开始计时
	
	ET0=1;
	EA=1;
	PT0=0;
}

//mian.c (测试) 按键控制灯亮
#include <REGX52.H>
#include "Timer0.h"
#include "Key.h"

unsigned char KeyNum;

void main()
{
    Timer0_Init(); //定时器0初始化
	while(1)
	{
		KeyNum=Key();
		if (KeyNum)
		{
			if(KeyNum==1) P2_0=~P2_0; //按K1,led1亮,再按一次则熄灭
			if(KeyNum==2) P2_1=~P2_1;
			if(KeyNum==3) P2_2=~P2_2;
			if(KeyNum==4) P2_3=~P2_3;
		}
	}
}
//main.c  流水灯,按下K1改变方向
#include <REGX52.H>
#include "Timer0.h"
#include "Key.h"
#include "INTRINS.H"

unsigned char KeyNum,LEDMode;

void main()
{
	P2=0xFE;
    Timer0_Init(); //定时器0初始化
	while(1)
	{
		KeyNum=Key();
		if (KeyNum)
		{
			if(KeyNum==1) //按下K1,改变方向
			{
				LEDMode++;
				if(LEDMode>=2) LEDMode=0;
			}
			
		}
	}
}

void Timer0_Routine() interrupt 1
{
    static unsigned int T0Count;
	TL0 = 0x18; //重新赋值1us
	TH0 = 0xFC;
	T0Count++;
	if (T0Count>=500) //0.5s
	{
		  T0Count=0;
		  if(LEDMode==0)
			  P2=_crol_(P2,1);  //向左移位,并且循环
		  if(LEDMode==1)
			  P2=_cror_(P2,1);  //向右移位,并且循环
	}
}

7.2 定时器时钟

头文件之前都定义过了

#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "Timer0.h"

unsigned char Sec,Min,Hou;

void main()
{
	LCD_Init();
	Timer0_Init();
	
	LCD_ShowString(1,1,"Clock:");
	LCD_ShowString(2,1,"  :  :");
	
	while(1)
	{
        //LCD显示
		LCD_ShowNum(2,1,Hour,2);
		LCD_ShowNum(2,4,Min,2);
		LCD_ShowNum(2,7,Sec,2);
	}
}

void Timer0_Routine() interrupt 1
{
    static unsigned int T0Count;
	TL0 = 0x18; //重新赋值1us
	TH0 = 0xFC;
	T0Count++;
	if (T0Count>=1000) //1s
	{
	    T0Count=0;
		Sec++;  //每秒递增
		if(Sec>=60)
		{
			Sec=0;
			Min++;
			if(Min>=60)
			{
				Min=0;
				Hour++;
				if(Hour>=24)
				{
					Hour=0;
				}
			}
		}
	}
}


8. 串口

串口是一种应用十分广泛的通讯接口,成本低,容易使用,通讯线路简单,可以实现两个设备的互通性。

51单片机内部自带UART(通用异步收发器),可实现单片机的串口通信。串口接口是DB9接口

STC89C52有一个UART,STC89C52URAT有四种工作模式

原理

USBTTl

串口寄存器

数据显示模式

HEX模式/十六进制模式/二进制模式:以原始数据的形式显示

文本模式/字符模式:以原始数据编码后的形式显示

8.1 串口向电脑发送数据

头文件

//UART.h
#ifndef __UART_H_
#define __UART_H_
void UART_Init(); //初始化
void UART_SendByte(unsigned char Byte);//只发送,不接收	
#endif

函数

//UART.c
#include <REGX52.H>
//Function Definition
/**
  * @brief 串口初始化 4800bps@11.0592MHz
  * @param  无
  * @retval 无
  */

void UART_Init()  //初始化 4800bps@11.0592MHz
{
	SCON = 0x40; //0100 0000
	PCON |= 0x80; 
	
	TMOD &= 0x0F;	//设置定时器1模式  0000 1111
	TMOD |= 0x20;	//设置定时器1模式  0010 0000
	TL1 = 0xF4;		//设定定时初值
	TH1 = 0xF4;		//设定定时器重装值
	ET1 = 0;		//禁止定时器1中断
	TR1 = 1;		//启动定时器1

}

//Function Definition
/**
  * @brief 串口发送一个字节数据
  * @param  Byte 要发送一个字节数字
  * @retval 无
  */

void UART_SendByte(unsigned char Byte) //只发送,不接收 
{
	SBUF = Byte;
  while(TI==0); //发送成功跳出循环,TI变为1
	TI = 0;    //重置为0
}

//串口中断函数模板(用在下一个例子中)
//void UART_Routine() interrupt 4 //串口中断号为4
//{ //RI:接收中断请求标志位 
//	if(RI==1) //RI=1向主机请求中断响
//	{
//		RI=0;//应中断后必须用软件复位,即RI=0  
//	}
//}
//main.c
//每一秒发送一个字节
#include <REGX52.H>
#include "Delay.h"
#include "UART.h"

unsigned char Sec;

void main()
{
	UART_Init(); //初始化
	
	while(1)
	{
		UART_SendByte(Sec); //发送 16进制格式
		Sec++;
		Delay(1000);
	}
}

8.2 电脑通过串口控制LED

#include <REGX52.H>
#include "Delay.h"
#include "UART.h"

void main()
{
	UART_Init();
	
	while(1)
	{
		
	}
}

void UART_Routine() interrupt 4 //串口中断号为4
{ //RI:接收中断请求标志位 
	if(RI==1) //RI=1向主机请求中断响
	{
		P2=~SBUF; 
		UART_SendByte(SBUF);
		RI=0;//应中断后必须用软件复位,即RI=0  
	}
}


上篇已完结,将会持续优化

下篇链接:51单片机入门教程(下篇)(代码+个人理解) – Echo (liveout.cn)

--> [alert]本文章已完结,持续优化中[/alert]前言必读这篇文章是记录我粗略学习51单片机的一些代码,我会加些个人理解以及注释在里面。因为是囫囵吞枣式学习,所以质量不是很好,后期我会慢慢优化 ?如果你想要学习单片机,可以观看下面的B站教程并配合本文档学习本文章使用的51单片机是 普中STC89C52RC51单片机入门教程(下篇)(代码+个人理解) – Echo (liveout.cn)...