HID文档PDF下载大全

 2023-10-25  3891  1

MAX7219级联

Arduino-ESP8266之MAX7219 8位数码管级联的实现 [复制链接]
duocool电梯直达1# 发表于 2018-5-18 16:26:58 | 只看该作者 回帖奖励    先上最终效果,感觉还是可以的,不过要是想完美,得像卖得挺火的那种木壳钟一样,平常像块木头,显示的时候才透出来,不过没找到合适的外壳。
    最近想用arduino-esp8266网上抓点东西下来玩,先试了试OLED模块,网上0.9寸的多,怕小买了1.3寸的,倒是搞定了显示,不过感觉还是小,不够用。

     还有种办法是用电子纸模块,淘宝上也有现成的,不过2,3百的价钱感觉性价比不高。后来决定用下面这种Max7219 8段数码管模块


这种模块从5块多到10多块,样子都差不多,都宣称三线驱动,支持级联,有例程。有焊的也有没焊接口的,我省事买了7块多的焊了的,后的发觉要级联还是得焊,还不如买没焊接口的,自己接线还能紧凑点。

   然后就是填级联这个坑了。最省力的应该是用LedControl这个库了,论坛里也有用这个库max7219 级联LED点阵的例子。不过不管怎么试,都是报错,好象这个库是不支持esp8266的。问卖家要到例程,arduino的例程只有单个模块的,这个样子,没用什么库,感觉就是别的单片机的c代码直接移值过来的。
/*******************************************************************************       
* Software Author:        HQ       
* Creation Date:        2015-2-10
* Software History:        2015-3-10
* Version:              2.0
* Sales address:       http://qifeidz.taobao.com/
********************************************************************************/
//模块引脚定义
int CLK = 2;
int CS = 1;
int DIN = 0; //这里定义了那三个脚

void setup() {
  // put your setup code here, to run once:
  pinMode(CLK,OUTPUT);
  pinMode(CS,OUTPUT);
  pinMode(DIN,OUTPUT); //让三个脚都是输出状态
}

void loop() {
  // put your main code here, to run repeatedly:
   Delay_xms(50);
   Init_MAX7219();
   Delay_xms(2000);
   Write_Max7219(0x0f, 0x00);       //显示测试:1;测试结束,正常显示:0
   Write_Max7219(1,8);
   Write_Max7219(2,7);
   Write_Max7219(3,6);
   Write_Max7219(4,5);
   Write_Max7219(5,4);
   Write_Max7219(6,3);
   Write_Max7219(7,2);
   Write_Max7219(8,1);
   while(1);
}
void Delay_xms(unsigned int x)
{
  unsigned int i,j;
  for(i=0;i<x;i++)
  for(j=0;j<112;j++);
}
//——————————————–
//功能:向MAX7219(U3)写入字节
//入口参数:DATA
//出口参数:无
//说明:
void Write_Max7219_byte(unsigned char DATA)         
{
    unsigned char i;   
    digitalWrite(CS,LOW);               
    for(i=8;i>=1;i–)
    {                  
      digitalWrite(CLK,LOW);   
      if(DATA&0X80)
           digitalWrite(DIN,HIGH);
      else
           digitalWrite(DIN,LOW);
      DATA<<=1;
      digitalWrite(CLK,HIGH);
     }                                 
}
//——————————————-
//功能:向MAX7219写入数据
//入口参数:address、dat
//出口参数:无
//说明:
void Write_Max7219(unsigned char address,unsigned char dat)
{
   digitalWrite(CS,LOW);
   Write_Max7219_byte(address);           //写入地址,即数码管编号
   Write_Max7219_byte(dat);               //写入数据,即数码管显示数字
   digitalWrite(CS,HIGH);                        
}

void Init_MAX7219(void)
{
   Write_Max7219(0x09, 0xff);       //译码方式:BCD码
   Write_Max7219(0x0a, 0x03);       //亮度
   Write_Max7219(0x0b, 0x07);       //扫描界限;4个数码管显示
   Write_Max7219(0x0c, 0x01);       //掉电模式:0,普通模式:1
   Write_Max7219(0x0f, 0x01);       //显示测试:1;测试结束,正常显示:0
}复制代码

试了下,可以用。级联怎么解决呢,网上反复找,本坛里有篇写的http://www.geek-workshop.com/for … p;highlight=max7219控制led点阵的,试着小改用了用,显示还是乱的,不行。又搜到这个贴http://lib.csdn.net/article/embeddeddevelopment/63594 MAX7219多级联串行控制多个点阵/数码管的详解  用的是c语言,都不是写给arduino用的,不过我一看注释那么熟,有些部分简直和卖家给的例程一模一样,可以肯定他们都有一个共同的祖先。仔细研究后,发现级联的关键是写后面的模块就得把它前面的模块写空。像这样/*第二片MAX7219的写入数据*/
void Write_Max7219_2(uchar add2,uchar dat2)
{
        Max7219_pinCS=0;
        Write_Max7219_byte(add2);
        Write_Max7219_byte(dat2);
        Max7219_pinCLK=1;
        Write_Max7219_byte(0x00);  //片1写入空
        Write_Max7219_byte(0x00);
        Max7219_pinCS=1;
}
/*第三片MAX7219的写入数据*/
void Write_Max7219_3(uchar add3,uchar dat3)
{
        Max7219_pinCS=0;
        Write_Max7219_byte(add3);
        Write_Max7219_byte(dat3);
        Max7219_pinCLK=1;
        Write_Max7219_byte(0x00); //片1写入空
        Write_Max7219_byte(0x00);
        Write_Max7219_byte(0x00); //片2写入空
        Write_Max7219_byte(0x00);
        Max7219_pinCS=1;
}
复制代码


我的代码如下: 定义了一个总的片数,然后用循环把初始化数码管模块和写每片的数据都统一起来了,这样不管是写一片,还是8片,代码都是一样的长度(好象max7219最多就支持8片),还自定义了一个DP参数,解决了卖家例程没写的小数点有无的问题。卖家例程是在loop中初始化模块的,这样显示感觉有闪烁,我移到setup中初始化,闪烁就没了
//模块引脚定义
int CLK = D6;
int CS = D7;
int DIN = D8; //这里定义了那三个脚
int PIECENUM = 4;//数码管片数

void setup() {
  // put your setup code here, to run once:
  pinMode(CLK, OUTPUT);
  pinMode(CS, OUTPUT);
  pinMode(DIN, OUTPUT); //让三个脚都是输出状态
  Delay_xms(50);
  Init_MAX7219(PIECENUM);
  Delay_xms(2000);
}

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

  Write_Max7219(1, 0x0f, 0x00, 0);     //显示测试:1;测试结束,正常显示:0

  for (int i = 1; i <= 5; i++) {
    Write_Mynum(1, i, i + 1, 1);
  }
  //Write_Mynum(1, 7, ‘_’, 0);
// Write_Mynum(1, 8, ‘_’, 0);
  for (int i = 1; i <= 8; i++) {
    Write_Mynum(2, i, 2, 0);
  }

  for (int i = 1; i <= 8; i++) {
    Write_Mynum(3, i, 3, 0);
  }

  for (int i = 1; i <= 8; i++) {
    Write_Mynum(4, i, 8 – i, 1);
  }

  while (1);
}


void Delay_xms(unsigned int x)
{
  unsigned int i, j;
  for (i = 0; i < x; i++)
    for (j = 0; j < 112; j++);
}

//切换地址,方便写
void Write_Mynum(int pnum,  unsigned char address, unsigned char dat , int dp) {
  Write_Max7219(pnum, 9 – address, dat, dp);
}

//——————————————–
//功能:向MAX7219写入字节
//入口参数:DATA,dp显示小数点与否
void Write_Max7219_byte(unsigned char DATA, int dp)
{
  unsigned char i;
  digitalWrite(CS, LOW);
  for (i = 8; i >= 1; i–)
  {
    digitalWrite(CLK, LOW);
    if (i == 8 && dp == 1)
      digitalWrite(DIN, HIGH);
    else {
      if (DATA & 0X80)
        digitalWrite(DIN, HIGH);
      else
        digitalWrite(DIN, LOW);
    }
    DATA <<= 1;
    digitalWrite(CLK, HIGH);
  }
}

//——————————————-
//功能:向MAX7219写入数据
//入口参数:pnum数码管片序号,address,dat,dp显示小数点与否
void Write_Max7219(int pnum, unsigned char address, unsigned char dat, int dp)
{
  digitalWrite(CS, LOW);
  Write_Max7219_byte(address, 0);          //写入地址,即数码管编号
  Write_Max7219_byte(dat, dp);              //写入数据,即数码管显示数字
  if (pnum > 1) {
    digitalWrite(CLK, HIGH);
    for (int i = 1; i < pnum; i++) {
      Write_Max7219_byte(0X00, 0);
      Write_Max7219_byte(0X00, 0);
    }
  }
  digitalWrite(CS, HIGH);
}


//Max7219初始化
void Init_MAX7219(int pienum)
{
  for (int i = 1 ; i <= pienum ; i++) {
    Write_Max7219(i, 0x09, 0xff, 0);     //译码方式:BCD码
    Write_Max7219(i, 0x0a, 0x03, 0);     //参数3:亮度
    Write_Max7219(i, 0x0b, 0x07, 0);     //扫描界限;参数3:8个数码管显示
    Write_Max7219(i, 0x0c, 0x01, 0);     //掉电模式:参数3:0,普通模式:1
    Write_Max7219(i, 0x0f, 0x01, 0);     //显示测试:参数3:1;测试结束,正常显示:0
  }
}
复制代码

STM32之FLASH驱动

本文介绍如何使用STM32标准外设库驱动FLASH,本例程驱动的FLASH为W25Q64。

本文适合对单片机及C语言有一定基础的开发人员阅读,MCU使用STM32F103VE系列。

1. FLASH简介

FLASH存储器又称为闪存,为可重复擦写的存储器,容量比EEPROM大的多。

FLASH在写入数据时只能把1改成0,而0无法直接改成1,因此要写入数据时,必须先执行擦除操作,而一次擦除操作无法仅擦除一个字节,必须将一整块区域的数据全部改成1。因此FLASH操作的特性是擦除时必须一次擦除一整块区域;写入时可以按字节或按块写入;读取则不受限制,可以读取一个字节和任意多个字节。

FLASH可分为NOR FLASH和NAND FLASH,两者特性有所区别,NOR FLASH读取速度快、可以按字节读写,但容量相同的情况下价格较高,而NAND FLASH读取速度较慢,只能按块为单位读写,但容量相同的情况下价格较低。一般NOR FLASH适用于存储程序代码,NAND FLASH适用于存储大数据量存储。

2. 常用FLASH

一般常用的NOR FLASH为Winbond公司的W25Qxx系列,常用容量从16M到256Mbit不等,换算成字节为2M到32MBytes,可以根据项目需求和价格综合考虑选型。

3.FLASH操作说明

以W25Q64举例,W25Q64容量为64Mbit,即8MByte,地址范围0~0x800000,3个字节即可表示,因此地址长度为3字节。

W25Q64共分为128个Block,每个Block为64Kbytes,每个Block又分为16个Sector,每个Sector为4Kbytes,每个Sector又可分为16个Pages,每个Page有256个字节,每次擦除时至少需要擦除一整个Sector,写入时则可以单字节写入,也可以写入多个字节,但最多写入一个Page,即256个字节。

3.1. 设备ID

W25Q64厂商号为0xEF,FLASH型号为0x4017,可以读取这些信息判定FLASH是否正常。

3.2. 指令

该表中的第一列为指令名,第二列为指令编码,第三至第 N 列的具体内容根据指令的不同而有不同的含义。其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应,不带括号的则为主机向 FLASH 传输。表中“A0~A23”指 FLASH 芯片内部存储器组织的地址;“M0~M7”为厂商号(MANUFACTURER ID);“ID0-ID15”为 FLASH 芯片的ID;“dummy”指该处可为任意数据;“D0~D7”为 FLASH 内部存储矩阵的内容。

3.3. 读取

使用读取命令(指令编码为03h),发送了指令编码及要读的起始地址后,FLASH 芯片就会按地址递增的方式返回内部存储的数据,读取的数据量没有限制,只要没有停止通讯,FLASH 芯片就会一直返回数据。

3.4. 写使能、写禁用和状态读取

在向 FLASH 写入数据或者擦除前,首先要使能写操作,通过发送“Write Enable”命令使FLASH可写,写入或者擦除完毕之后,FLASH自动进入写禁用状态,无需单独发送写禁用命令,当FLASH为写禁用状态时,任何写入或擦除操作无效,这样可避免误写入或误擦除。

由于FLASH写入数据需要消耗一定的时间,并不是在总线通讯结束的一瞬间完成的,所以在写操作后需要确认 FLASH 芯片“空闲”时才能进行再次写入。为了表示自己的工作状态,FLASH 芯片定义了一个状态寄存器,这个状态寄存器的第 0 位为“BUSY”,当这个位为“1”时,表明 FLASH芯片处于忙碌状态,它可能正在进行“擦除”或“数据写入”的操作。利用指令表中的“Read Status Register”指令可以获取 FLASH 状态寄存器的内容,只要向 FLASH 芯片发送了读状态寄存器的指令,FLASH 芯片就会持续向主机返回最新的状态寄存器内容,直到收到 SPI通讯的停止信号。因此可以通过查看该位,直到该位为0时,即可对FLASH进行擦除或者写操作。如果刚写完数据就执行读操作,也需要等待。

3.5. 擦除

FLASH写入数据之前需要先擦除,擦除可分为扇区擦除(Sector Erase)、块擦除(Block Erase)和整片擦除(Chip Erase)。指令编码分别为20h、D8h,而整片擦除支持2个命令,即2个命令均可使用,为C7h和60h。要实现擦除操作时先发送指令编码,扇区擦除和块擦除需要继续发送要擦除区域的地址,而整片擦除无需发送地址。要执行擦除操作之前需要确保FLASH处于写使能状态,可通过发送写使能命令实现。

3.6. 写入

使用页写入命令(指令编码为02h),先发送指令编码,然后发送要写的起始地址,然后继续发送要写入的内容,一次写入操作最多写入256字节数据。 进行写入之前需要确保FLASH处于写使能状态,可通过发送写使能命令实现。如果想要一次写入超过256字节,那么就需要对页写入命令进行封装。

 

完整代码(仅自己编写的部分)

复制代码
  1 #include "flash.h" 
2 #include "delay.h"
3 #include <stdio.h>
4
5 #define FLASH_PAGE_SIZE 256 //W25Q64每页256个字节
6
7 #define W25X_WriteEnable 0x06
8 #define W25X_WriteDisable 0x04
9 #define W25X_ReadStatusReg 0x05
10 #define W25X_WriteStatusReg 0x01
11 #define W25X_ReadData 0x03
12 #define W25X_FastReadData 0x0B
13 #define W25X_FastReadDual 0x3B
14 #define W25X_PageProgram 0x02
15 #define W25X_BlockErase 0xD8
16 #define W25X_SectorErase 0x20
17 #define W25X_ChipErase 0xC7
18 #define W25X_PowerDown 0xB9
19 #define W25X_ReleasePowerDown 0xAB
20 #define W25X_DeviceID 0xAB
21 #define W25X_ManufactDeviceID 0x90
22 #define W25X_JedecDeviceID 0x9F
23
24 /* WIP(busy)标志,FLASH内部正在写入 */
25 #define WIP_Flag 0x01
26
27 //初始化FLASH接口
28 void FLASH_Init(void)
29 {
30 SPI_IoInit();
31 }
32
33 //查看W25Q64是否空闲
34 //返回值: 1,FLASH忙,无法读写
35 // 0,FLASH空闲,可以读写
36 //注意执行完此函数后,FLASH已取消选中,如果要写入,必须重新选中
37 uint8_t FLASH_WaitReady(void)
38 {
39 uint32_t i = 0;
40 uint8_t ret = 1;
41 uint8_t status = 0;
42
43 SPI_CS_0;
44
45 SPI_WriteByte(W25X_ReadStatusReg);
46
47 for(i = 0; i < 1000; i++){
48 status = SPI_ReadByte();
49 if((status & WIP_Flag) == RESET){
50 ret = 0;
51 break;
52 }
53 delay_ms(10);
54 }
55
56 SPI_CS_1;
57
58 return ret;
59 }
60
61 /*
62 FLASH擦除、写入数据完毕后会自动禁用写使能,因此无需再执行写禁用操作
63 注意执行此函数前,必须先选中FLASH
64 */
65 void FLASH_WriteEnable(void)
66 {
67 /* 发送写使能命令*/
68 SPI_WriteByte(W25X_WriteEnable);
69 }
70
71 uint32_t FLASH_ReadJedecID(void)
72 {
73 uint32_t temp, temp0, temp1, temp2;
74
75 SPI_CS_0;
76
77 /* 发送JEDEC指令,读取ID */
78 SPI_WriteByte(W25X_JedecDeviceID);
79
80 temp0 = SPI_ReadByte();
81 temp1 = SPI_ReadByte();
82 temp2 = SPI_ReadByte();
83
84 /*把数据组合起来,作为函数的返回值*/
85 temp = (temp0 << 16) | (temp1 << 8) | temp2;
86
87 SPI_CS_1;
88
89 return temp;
90 }
91
92 uint8_t FLASH_SectorErase(uint32_t addr)
93 {
94 /* 判断FLASH是否可写,如果不可写,直接返回错误 */
95 if(FLASH_WaitReady()){
96 return 1;
97 }
98
99 SPI_CS_0;
100
101 /* 发送FLASH写使能命令 */
102 FLASH_WriteEnable();
103
104 /* 发送扇区擦除指令*/
105 SPI_WriteByte(W25X_SectorErase);
106 /*发送擦除扇区地址的高位*/
107 SPI_WriteByte((addr & 0xFF0000) >> 16);
108 /* 发送擦除扇区地址的中位 */
109 SPI_WriteByte((addr & 0xFF00) >> 8);
110 /* 发送擦除扇区地址的低位 */
111 SPI_WriteByte(addr & 0xFF);
112 /* 发送FLASH写禁用命令 */
113 // FLASH_WriteDisable();
114
115 SPI_CS_1;
116
117 if(FLASH_WaitReady()){
118 return 2;
119 }
120
121 return 0;
122 }
123
124 uint8_t FLASH_ChipErase(void)
125 {
126 /* 判断FLASH是否可写,如果不可写,直接返回错误 */
127 if(FLASH_WaitReady()){
128 return 1;
129 }
130
131 SPI_CS_0;
132
133 /* 发送FLASH写使能命令 */
134 FLASH_WriteEnable();
135
136 /* 发送扇区擦除指令*/
137 SPI_WriteByte(W25X_ChipErase);
138
139 SPI_CS_1;
140
141 if(FLASH_WaitReady()){
142 return 2;
143 }
144
145 return 0;
146 }
147
148 //在W25Q64里面的指定地址开始读出指定个数的数据
149 //addr: 开始读数的地址
150 //pBuffer: 需要读取数据的指针
151 //numToRead:要读出数据的个数
152 //返回值: 1,读取失败
153 // 0,读取成功
154 uint8_t FLASH_Read(uint32_t addr, uint8_t *pBuffer, uint32_t numToRead)
155 {
156 SPI_CS_0;
157
158 SPI_WriteByte(W25X_ReadData);
159
160 /* 发送 读 地址高位 */
161 SPI_WriteByte((addr & 0xFF0000) >> 16);
162 /* 发送 读 地址中位 */
163 SPI_WriteByte((addr & 0xFF00) >> 8);
164 /* 发送 读 地址低位 */
165 SPI_WriteByte(addr & 0xFF);
166
167 /* 读取数据 */
168 while (numToRead--) /* while there is data to be read */
169 {
170 /* 读取一个字节*/
171 *pBuffer++ = SPI_ReadByte();
172 }
173
174 SPI_CS_1;
175
176 return 0;
177 }
178
179 //在W25Q64指定地址读出一个数据
180 //addr: 开始读数的地址
181 //pReadData:需要读取数据的指针
182 //返回值: 1,读取失败
183 // 0,读取成功
184 uint8_t FLASH_ByteRead(uint32_t addr, uint8_t * pReadData)
185 {
186 SPI_CS_0;
187
188 SPI_WriteByte(W25X_ReadData);
189
190 /* 发送 读 地址高位 */
191 SPI_WriteByte((addr & 0xFF0000) >> 16);
192 /* 发送 读 地址中位 */
193 SPI_WriteByte((addr & 0xFF00) >> 8);
194 /* 发送 读 地址低位 */
195 SPI_WriteByte(addr & 0xFF);
196
197 /* 读取一个字节*/
198 *pReadData = SPI_ReadByte();
199
200 SPI_CS_1;
201
202 return 0;
203 }
204
205 //在W25Q64指定地址写入一个数据
206 //addr: 写入数据的目的地址
207 //dataToWrite: 要写入的数据
208 //返回值: 1,写入失败
209 // 0,写入成功
210 uint8_t FLASH_ByteWrite(uint32_t addr, uint8_t dataToWrite)
211 {
212 /* 判断FLASH是否可写,如果不可写,直接返回错误 */
213 if(FLASH_WaitReady()){
214 return 1;
215 }
216
217 SPI_CS_0;
218
219 /* 发送FLASH写使能命令 */
220 FLASH_WriteEnable();
221
222 /* 写页写指令*/
223 SPI_WriteByte(W25X_PageProgram);
224 /*发送写地址的高位*/
225 SPI_WriteByte((addr & 0xFF0000) >> 16);
226 /*发送写地址的中位*/
227 SPI_WriteByte((addr & 0xFF00) >> 8);
228 /*发送写地址的低位*/
229 SPI_WriteByte(addr & 0xFF);
230
231 /* 发送当前要写入的字节数据 */
232 SPI_WriteByte(dataToWrite);
233
234 SPI_CS_1;
235
236 if(FLASH_WaitReady()){
237 return 2;
238 }
239
240 return 0;
241 }
242
243 uint8_t FLASH_PageWrite(uint32_t addr, uint8_t *pBuffer, uint32_t numToWrite)
244 {
245 /* 判断FLASH是否可写,如果不可写,直接返回错误 */
246 if(FLASH_WaitReady()){
247 return 1;
248 }
249
250 SPI_CS_0;
251
252 /* 发送FLASH写使能命令 */
253 FLASH_WriteEnable();
254
255 /* 写页写指令*/
256 SPI_WriteByte(W25X_PageProgram);
257 /*发送写地址的高位*/
258 SPI_WriteByte((addr & 0xFF0000) >> 16);
259 /*发送写地址的中位*/
260 SPI_WriteByte((addr & 0xFF00) >> 8);
261 /*发送写地址的低位*/
262 SPI_WriteByte(addr & 0xFF);
263
264 /* 写入数据*/
265 while(numToWrite--)
266 {
267 /* 发送当前要写入的字节数据 */
268 SPI_WriteByte(*pBuffer++);
269 }
270
271 SPI_CS_1;
272
273 if(FLASH_WaitReady()){
274 return 2;
275 }
276
277 return 0;
278 }
279
280 /*
281 根据要写入的地址、长度、页大小计算如何分页
282 输入参数:addr: 写入起始地址
283 len: 写入数据长度
284 pageSize:每页存储的数据,对于W25Q64来说,该值为256
285 要写入参数:pFirstPageLen: 首页要写入的字节
286 pLastPageLen: 尾页要写入的字节
287 pPageNum: 总共要写入的页数
288 */
289 void FLASH_GetWritePages(uint32_t addr, uint32_t len, uint32_t pageSize,
290 uint32_t * pFirstPageLen, uint32_t * pLastPageLen, uint32_t * pPageNum)
291 {
292 uint32_t firstPageOffset; //首页偏移
293 uint32_t otherLen; //去除首页之后剩余长度
294 uint32_t otherPageNum; //去除首页之后剩余整数页数量
295
296 firstPageOffset = addr % pageSize;
297 *pFirstPageLen = pageSize - firstPageOffset;
298
299 if(len < *pFirstPageLen){
300 *pFirstPageLen = len;
301 }
302
303 otherLen = len - *pFirstPageLen;
304 otherPageNum = otherLen / pageSize;
305 *pLastPageLen = otherLen % pageSize;
306
307 *pPageNum = otherPageNum + 1;
308
309 if(*pLastPageLen){
310 (*pPageNum)++;
311 }
312 }
313
314
315 //在W25Q64里面的指定地址开始写入指定个数的数据
316 //addr: 开始读数的地址
317 //pBuffer: 需要读取数据的指针
318 //NumToWrite:要写入数据的个数
319 //返回值: 1,读取失败
320 // 0,读取成功
321 uint8_t FLASH_Write(uint32_t addr, uint8_t *pBuffer, uint32_t numToWrite)
322 {
323 uint32_t i;
324 uint32_t firstPageLen, lastPageLen, pageNum;
325
326 FLASH_GetWritePages(addr, numToWrite, FLASH_PAGE_SIZE,
327 &firstPageLen, &lastPageLen, &pageNum);
328
329 printf("addr:%#x, numToWrite:%d, firstPageLen:%d, lastPageLen:%d, pageNum:%d\n",
330 addr, numToWrite, firstPageLen, lastPageLen, pageNum);
331
332 for(i = 0; i < pageNum; i++)
333 {
334 if(i == 0){ //首页写入长度为firstPageLen
335 if(FLASH_PageWrite(addr, pBuffer, firstPageLen)){
336 goto write_fail;
337 }
338 addr += firstPageLen;
339 pBuffer += firstPageLen;
340 }else if(i == pageNum - 1){ //尾页写入长度为lastPageLen
341 if(FLASH_PageWrite(addr, pBuffer, lastPageLen)){
342 goto write_fail;
343 }
344 addr += lastPageLen;
345 pBuffer += lastPageLen;
346 }else{ //除首页和尾页外写入长度为FLASH_PAGE_SIZE
347 if(FLASH_PageWrite(addr, pBuffer, FLASH_PAGE_SIZE)){
348 goto write_fail;
349 }
350 addr += FLASH_PAGE_SIZE;
351 pBuffer += FLASH_PAGE_SIZE;
352 }
353 }
354
355 return 0;
356
357 write_fail:
358 return 1;
359 }

本文转载自“https://www.cnblogs.com/greatpumpkin/p/13747892.html”

STM32掉电保存解决方案

【干货】STM32通过ADC模拟看门狗实现掉电保存

原创 嵌入式技术开发2022-11-17 08:00747浏览0评论0点赞

7nm国产芯片成功上车,EDA如何赋能—西门子EDA对话芯擎科技50万+ eCAD和mCAD模型, 150+常用格式可供选择1.前言很多时候我们需要将程序中的一些参数、数据等存储在EEPROM或者Flash中,达到掉电保存的目的。但有些情况下,程序需要频繁的修改这些参数,如果每次修改参数都进行一次保存,那将大大降低存储器的寿命。尤其是单片机内部Flash,以STM32F030K6T6为例,擦写寿命只有1000次。当然,这是最小值,实际可能比这个多,但也是有风险。因此,最好的办法就是在程序运行中不进行保存操作,只在断电时保存一次。掉电保存的关键是怎样检测掉电瞬间,方法有很多种:1.通过外部电路检测电源,触发IO中断。2.通过单片机的PVD(可编程电压检测器) 中断检测。3.通过ADC看门狗中断检测。不管哪种方式,一般都是通过中断来实现,主要是为了快速响应。今天主要介绍第三种方式,通过ADC看门狗实现掉电保存。2.硬件设计

2.1掉电时间掉电保存的前提是断电后电源电压是缓慢下降的,这样才有足够的时间去检测掉电并保存数据。因此,电源上必须有个大电容,保证电源断开后能继续给单片机供电。具体需要维持多长时间,要看存储器的擦写周期。以STM32F030K6T6的内部存储器为例,擦除一页需要30ms,写入一个16位数据需要53.5us。根据实际需要擦除和写入的数据多少来计算至少需要多少时间。还需要关注一个参数,编程电压。在用示波器测量掉电时的波形时,测量出从断电瞬间到电压降低到2.4V时的时间,该时间大于总的数据擦写的时间即可。当然要留有一定裕量。如果时间不够,就要加大电容了。2.2ADC检测       ADC检测掉电的方式有两种,一种是通过某个通道直接采集电源电压(或者分压后采集),另一种是采集内部参考电压Vrefint来判断电源电压。第一种方式很好理解,采样值就代表电源电压,可以直接去触发ADC的看门狗中断。第二种方式由于内部参考电压是不变的,STM32F030是1.23V,有一定误差。当电源电压变化时,ADC采集的参考电压会发生变化,因此也可以通过这个变化触发看门狗中断。这里有个前提,即单片机的VREF引脚或AVDD引脚就是要检测的电源电压。3.软件设计首先打开STM32CubeMx,配置一下ADC,如下。首先需要使能Vrefint Channel,如果需要其它通道也可以使能。其次需要使能ADC的看门狗,看门狗通道选择Vrefint,设置一下高/低门限值,使能看门狗中断模式,同时ADC的中断也要打开。这里的高/低门限是指,当ADC的采样值大于高门限或小于低门限时,ADC的看门狗中断将被触发。如果是用于掉电检测,只要关心高门限就行。正常时ADC采样值=1.23*4096/3.3,大约是1526左右,由于Vrefint和电源电压都有误差,所以只是个大概。如果我们将掉电电压检测值设为3.1V,那对应的ADC看门狗的高门限值应为1.23*4096/3.1,约1625左右。生成代码后,在初始化完成启动ADC采样,如下:

uint32_t adc_buf;
HAL_ADCEx_Calibration_Start(&hadc);HAL_Delay(100);HAL_ADC_Start_DMA(&hadc,(uint32_t*)&adc_buf,1);然后再ADC的中断中添加保存数据的程序即。

void ADC1_IRQHandler(void){  /* USER CODE BEGIN ADC1_IRQn 0 */ /* USER CODE END ADC1_IRQn 0 */ HAL_ADC_IRQHandler(&hadc); /* USER CODE BEGIN ADC1_IRQn 1 */ Save_Param(); //保存参数 while(1); /* USER CODE END ADC1_IRQn 1 */}这里有两点需要注意:一是在中断中先关闭功耗较大的外设,比如液晶背光、数码管等。使断电时电源电压下降不至于太快。二是在保存数据后关闭看门狗中断,或者直接死循环(因为已经断电,也不需要执行其它程序了)。这样做主要是为了防止电压下降的太慢,多次触发看门狗中断,导致最后一次写入错误。

推荐阅读:STM32使用HAL库驱动W5500
开源自己做的4.3寸触摸屏,SWM32单片机+LVGL串口接收不定长数据的几种方法
四位数显表头设计

   欢迎关注公众号”嵌入式技术开发”,大家可以后台给我留言沟通交流。如果觉得该公众号对你有所帮助,也欢迎推荐分享给其他人。

免责声明: 该内容由专栏作者授权发布或作者转载,目的在于传递更多信息,并不代表本网赞同其观点,本站亦不保证或承诺内容真实性等。若内容或图片侵犯您的权益,请及时联系本站删除。侵权投诉联系:  [email protected]

Petit FAT文件系统相关备忘

[笔记]SD卡相关资料

 ESD静电放电模块

  • 我知道的flash分为两种NOR flash和NAND flash,NOR falsh容量一般为1~16M用于单片机代码存储,NAND flash最小的是8M最大的现在听说有90G还可能更大,一般用在大容量存储方面。
  • VGA芯片AD8367是AD公司推出的
  • 镁光(Micron)身为世界第二大内存颗粒制造商,OWD22-D9LCQ  做DDR3用的。
  • 三星的FLASH芯片NAND 容量的K9F1G08U00,配置STM32 ARM芯片用。

来源:http://blog.sina.com.cn/s/blog_4ada01d8010006h7.html

来源:DDR内存条比较http://wenku.baidu.com/link?url=WgIvJUndrCDTj9gvCHnFlJv2PtHEkefYFko8twldRiT7THj2e-t5pbCq8rZDwaWIWCwC50jYEiquIh2zGd00wcrnDOQ8QgeW6xCtM93U5Wu

bandwidth – 硬件带宽,单位为Hz(赫兹)。单位时间中线路(传输系统)中电信号的最大振荡频率,超过此频率硬件将无法保证信号传输的正确性。

data rate – 数据传输率,单位为bps(bits per second),单位传输的二进制位的数目。

throughtput – 吞吐量,单位为bps(bits per second),数据通过网络的传输速率。

 吞吐量是通过频率和带宽算出来的,带宽和频率。比如PCI,IDE,AGP,USB。

计算公式:数据带宽=时钟频率×数据总线位数/8(单位:byte/s)

内存带宽计算公式:带宽=内存核心频率×内存总线位数×倍增系数。

DDR3预读取位数8bit需要4个时钟周期完成,所以DDR3的I/O时钟频率是存储单元核心频率的4倍,由于是上下沿都在传输数据,所以实际有效的数据传输频率达到核心频率的8倍。 

内存标准核心频率,I/O 频率,有效传输频率,单通道带宽,双通道带宽。。

内存条有三种不同的频率指标,它们分别是核心频率、时钟频率和有效数据传输频率。核心频率即为内存Cell阵列(Memory Cell Array)的工作频率,它是内存的真实运行频率;时钟频率即I/O Buffer(输入/输出缓存)的传输频率(预读取位数8位);而有效数据传输频率则是指数据传送的频率(时钟的上升沿和下降沿都采集数据)。Cell和Bank。数据总线位宽一直是64bit。

DDR3-800内存有效数据传输频率为800MHz,其I/O频率为400MHz,核心频率只有100MHz。

DDR3-1600有效数据传输频率为1600MHZ,I/O时钟频率800MHZ,核心频率是200MHZ。

AGP总线带宽:
AGP1x总线带宽=66MHZ*32bit/8=264MB/s
AGP8x总线带宽=66MHZ*32bit/8=2.1GB/s
PCI总线带宽:PCI带宽=33MH*32bit/8=133MB/S
32bit @ 33MHz 数据吞吐量为33MHZ*32bit/8=132MB/S

USB 3.0 最大传输速率5Gbps, 向下兼容USB 1.0/1.1/2.0

Mbps和MBps的区别:数据在传输过程中是以二进制位的形式,用bit来表示。在衡量储存容量时,用byte来表示。
Mbps(Million bits per second) – 每秒xx百万位
MBps(Million bytes per second) – 每秒xx百万字节

低速USB的时钟频率是1.5MHz
全速USB的时钟频率是12MHz
高速USB的时钟频率是480MHz
USB传输又分四种:控制、中断、成组和同步(Control,Interrupt,BulkandIsochronous)。
不同的传输速度下不同的传输方式有不同的理论传输速度,不能笼统地说。
不管哪种传输速度,同步传输的理论传输速度最快,控制传输的理论传输速度最慢。
USB系统要保留10%的带宽
usb1.1  12Mb/s(12Mbps/8=1.5MBps),极限速度是1MB/s,接口效率为1MB/1.5MB*100%=66.7%
usb2.0  480Mb/s(480Mbps/8=60MBps),极限速度约40MB/s,接口效率为40MB/60MB*100%=66.7%
移动硬盘USB2.0写速度需要在20MB/s左右~.

PCI 总线位宽是 32位,总线频率 33 MHz,每时钟传输 1 组数据,它的带宽为 127.2 MB/s,即 1017.6 Mbps。
●PCI 2.1 总线位宽是 64位,总线频率 66 MHz,每时钟传输 1 组数据,它的带宽为 508.6 MB/s,即 4068.8 Mbps。
●AGP 总线位宽是 32位,总线频率 66 MHz,每时钟传输 1 组数据,它的带宽为 254.3 MB/s,即 2034.4 Mbps。
●AGP Pro 总线位宽是 32位,总线频率 66 MHz,每时钟传输 1 组数据,它的带宽为 254.3 MB/s,即 2034.4 Mbps。

AGP Pro 是 AGP 的改进型,它使工作站级主板也能利用 AGP 的加速性能,降低了 AGP 所需的电压供应,并没有什么太大的改变。
●AGP 2X 总线位宽是 32位,总线频率 66 MHz,每时钟传输 2 组数据,它的带宽为 508.6 MB/s,即 4068.8 Mbps。
●AGP 4X 总线位宽是 32位,总线频率 66 MHz,每时钟传输 4 组数据,它的带宽为 1017.3 MB/s,即 8138.4 Mbps。
●AGP 8X 总线位宽是 32位,总线频率 66 MHz,每时钟传输 8 组数据,它的带宽为 2034.6 MB/s,即 16276.8 Mbps。

图片存储在带文件系统的SD卡中如何解出RGB:http://www.blogjava.net/georgehill/articles/6549.html

BMP文件格式。

Altera固化到Flash的文件是JIC,而ISE固化到Flash的文件是MCS

来源:http://wenku.baidu.com/link?url=0Ju7D8JGRJpDBQN_z_FYAgx8aUq1HELen1GVJvzSL30iY2B5VmzQlDsrbOkI-FnbGw0-ZV6RjXhhZ2s5Mr2EKP2E1a_QSJ3EMcNuS1yG8KG SPI模式下,SD卡在时钟上升沿读数据。

来源:http://wenku.baidu.com/view/af0de2e74afe04a1b071de49.html?re=view

STM32学习笔记之SD卡V2.0协议初始化,解释得很具体

http://blog.sina.com.cn/s/blog_4f09c0b50101636h.html 每个命令的位介绍。

来源:http://wenku.baidu.com/link?url=w7enVFMgall62cMzaxEyPGq10gwdyZkE_UBZHyUim4kp_U5I97FhUSHob-bHHqqh44-0kHP3hf7W94S5ZyXnmQIQJfSSC0h6UgiSmNL7w7_

v2.0版SD卡协议中命令CMD8的使用详解

SDHC是“High Capacity SD Memory Card”的缩写,即“高容量SD存储卡”,这里的HC指的是High Capacity,高容量。2006年5月,SD协会发布了最新版的SD 2.0的系统规范,在其中规定SDHC是符合新的规范,且容量大于2GB小于等于32GB的SD卡。另外SDHC至少要符合Class 2的速度等级,并且在卡片上必须有SDHC标志和速度等级标志。

SD卡最大支持2GB容量,SDHC 最大支持32GB容量,SDXC 最大支持2TB(2048GB)容量

来源:http://bbs.ednchina.com/BLOG_ARTICLE_269804.HTM

 在单片机读写SD卡后,如果需要查看读写的效果,可以利用winhex软件在计算机端直接观察到SD卡中的数据内容。

        今天用PIC单片机将SD卡的第一个扇区数据读取出来后,通过串口发送到PC上观察,发现读取的数据与在PC上用winhex软件查看的第一个扇区的数据不一致,老是以为单片机端得读取程序有问题,修改了很多次也没有效果,后上网发现winhex软件在打开磁盘的对话框时,有两种打开方式,一种是逻辑驱动器,另一种是物理驱动器,如果希望看到与单片机读取扇区一致的数据,需以物理驱动器方式打开SD卡,此时看到的第一扇区就与单片机所选择的扇区一致了,可以很直观的看到SD卡中的对应数据了,不过要注意,前面的扇区最好不要随便通过单片机写入数据,防止破坏了SD卡上的文件系统,导致无法再次在PC机上打开SD卡了。

来源:http://wenku.baidu.com/link?url=oC1-1QghHU5_tfoBdnprae3_PFlfbz-ceJ5jGXoOh_irHmwg_0mEYkj37JFePpoHsLcWFrOvrPrdEYjaP3IDDPjPyMOiMgAX9rfNRK4qujW

SD卡命令解读。

来源:http://dontium.blog.163.com/blog/static/342952722009419114113217/

sd卡分为mmc卡,sd v1.0,sd v2.0三个版本

cmd0是{0x40,0x00,0x00,0x00,0x00,0x95}  –0x01
cmd1是{0x41,0x00,0x00,0x00,0x00,0xff}    –0x01
cmd24是{0x58,0x00,0x00,0x00,0x00,0xff}  –0x01
cmd16是{0x51,0x00,0x00,0x00,0x00,0xff}   –0x01

cmd17是{0x51,0x00,0x00,0x00,0x00,0xff}  –0x01

cmd8是{0x48,0x00,0x00,0x01,0xaa,0x87}   –0x01

cmd55是{0x77,0x00,0x00,0x00,0x00,0xff}   –0x01

acmd41是{0x69,0x40,0x00,0x00,0x00,0xff}  –0x00

如果是sd v1.0版本,直接发CMD55+ACMD41

如果是sd v2.0版本,先发cmd8再发CMD55+ACMD41

1 . 命令变量

大容量SD卡,存储器访问命令的32位变量是对块寻址的存储器访问(是决定块的块变量)。块的固定大小为512字节。而标准容量的SD卡,32位变量是对字节寻址,块长度由CMD16命令决定。

5、读写超时检查

读:对标准容量的SD卡,读超时的时间设定为大于典型读出时间的100倍,或者设置为100mS。卡参数的读时间为:CSD中的TAACT NSAC参数的两倍。

写:对标准容量的SD卡,写超时的时间设定为大于典型编程时间的100倍,或者设置为250mS。卡参数的写时间为:CSD中的R2W_FACTOR

对于大容量卡,CSD中的参数为因定值,因此最好使用>100mS作为读超时,>250mS作为写超时。

五、关于命令索引

“命令索引”在SD协议中并没有明确指出,但综合参考三星程序及网上文章,认为这种说法是正确的:“命令索引”中的数字就是其“索引值”。对于ACMD类的命令,可以看作为“复合命令”,即在执行时,前面先执行CMD55,然后再执行“去掉ACMDn前的‘A’的命令”

来源:http://www.doc88.com/p-348627971214.html  单片机用SPI模式控制SD卡读写

4G以上的SD不能用SPI模式吗?

容量与SPI总线无关,2G是个分水岭,2G以上是SD2.0协议,与2G以下的卡驱动不兼容。很多老设备(俺家的佳能A530相机就是一例)用不了2G以上的卡,也就是这个原因。

#define ACMD41  41      //命令41,应返回0x00
#define CMD55   55      //命令55,应返回0x01

看了2.0的协议,在网上查了sdhc的初始化方法,到目前已经把csd读出来了,下一步该弄读写了。

SD卡2G及以下是按字节寻址的,更大的是SD2.0协议,但还要读取OCR数据,判断是SD2.0还是SD2.0HC卡,只有SD2.0HC卡才是按扇区寻址的,所以卡的初始化时先要读取卡的类型,这点要特别注意。这个好像SD2.0卡也是按扇区寻址的吧?SD1.X才是字节寻址吧。

SD卡简介:

SD卡的技术规范经过几次升级,与最初版本已有很大不同,本文基于Ver 3.01讨论

从容量上分

 容量命名 简称 
 0G<容量<2GStandard Capacity SD Memory Card  SDSC或SD
 2G<容量<32GHigh Capacity SD Memory Card SDHC
 32G<容量<2TExtended Capacity SD Memory Card SDXC

http://blog.sina.com.cn/s/blog_4f09c0b50101636h.html

来源:http://servers.pconline.com.cn/skills/0712/1193752.html

WinHex工具对FAT16磁盘进行分析,簇、扇区、容量关系。

 4G 及4G以上sd卡不能用SPI读写吗?

数据块长度515个字节,起始标志字节为0xFE,实际数据512字节,2字节CRC

CRC使用CRC-16算法,不计算时可以设置为0xFF

写操作之后要等待非忙信号,忙信号使MISO为低电位,接收数据一直为0

来源:http://wenku.baidu.com/view/953298fcaef8941ea76e053c.html

单片机读写SD卡的代码。。。

来源:http://wenku.baidu.com/link?url=8xXHUPr2ekFj6xzqfWI5Mu2q7iiszatEic-cikFaEOEnmd_8vg9oLtjhFk3qSCzDK2x-rTGjiFHc4QBeJ3RHIEg51nz4xBIVlN3rxpIR2E3  带文件系统的专用SD卡 的读写操作。不需要了解FAT的复杂结构。

一旦我们找到了我们要写入文件的起始位置(它一般表示为一个扇区号),那我们就可以在这个起始扇区的下一个扇区写入数据了。

CMD17的Address是32位的,最后9位是Block地址,前面的是Sector地址.如果Block=512 Byte的话,Block的值只能是0,Block!=0,Response会报错.sector是物理地址.欢迎砸砖! 振南电子,那里有SD的菜鸟教程。

来源:http://blog.163.com/zhaojun_xf/blog/static/30050580201151410635516/

winhex读取的数据是逻辑0扇区,而SD卡读取的数据是物理0扇区,肯定不一样。图上的winhex读取的数据应该是DBR,SD卡读取的数据是MBR。

SD卡地址第一个数据物理地址初始值 用winhex怎么查?

首先点“工具”点“打开磁盘”选择你要看的磁盘,U盘也能看,点“位置”点“转到偏移”然后再输入“0”点“OK”就可以了
告诉你快捷方式吧 打开软件后按F9选择你要看的磁盘,然后alt+g或者ctrl+g。然后再输入“0”点“OK”就可以了。

offset=扇区*512,扇区是512B。WINHEX显示的就是实际的起始字节而不是扇区

在OpenCore上下载的SD SPI模式代码是按照下面的流程来做的,可以参考下哈。。。

代码参考C语言:来源:http://wenku.baidu.com/link?url=kn2cP2vSMRlFaO3fwOi-xN188IQe47s6TO37qcqTjndipGSFSQT4akXb0_v2ZjZ51BkA6Jd14BI2G8OFt_NAmNoAUy41cQsN50ENcfheuCm

来源:http://www.cnblogs.com/zyqgold/archive/2012/01/02/2310340.html

对SD卡的控制流程

1、SD卡的SPI工作模式

SD 卡在上电初期自动进入SD 总线模式,在此模式下向 SD 卡发送复位命令CMD0 。如果SD卡在接收复位命令过程中CS低电平有效,则进入SPI模式,否则工作在SD 总线模式。

下边是插入SD卡,并初始化为SPI模式的流程图:(至于CMD××究竟是什么样的命令,如下所示

复制代码
  1 /* 命令响应定义 define command's response */
  2 #define R1 1
  3 #define R1B 2
  4 #define R2 3
  5 #define R3 4
  6 
  7 /**********************************************
  8 
  9      SD卡SPI模式下命令集
 10 
 11 **********************************************/
 12 
 13 /******************************** 基本命令集 Basic command set **************************/
 14 /* 复位SD 卡 Reset cards to idle state */
 15 #define CMD0 0
 16 #define CMD0_R R1
 17 
 18 /* 读OCR寄存器 Read the OCR (MMC mode, do not use for SD cards) */
 19 #define CMD1 1
 20 #define CMD1_R R1
 21 
 22 /* 读CSD寄存器 Card sends the CSD */
 23 #define CMD9 9
 24 #define CMD9_R R1
 25 
 26 /* 读CID寄存器 Card sends CID */
 27 #define CMD10 10
 28 #define CMD10_R R1
 29 
 30 /* 停止读多块时的数据传输 Stop a multiple block (stream) read/write operation */
 31 #define CMD12 12
 32 #define CMD12_R R1B
 33 
 34 /* 读 Card_Status 寄存器 Get the addressed card's status register */
 35 #define CMD13 13
 36 #define CMD13_R R2
 37 
 38 /***************************** 块读命令集 Block read commands **************************/
 39 
 40 /* 设置块的长度 Set the block length */
 41 #define CMD16 16
 42 #define CMD16_R R1
 43 
 44 /* 读单块 Read a single block */
 45 #define CMD17 17
 46 #define CMD17_R R1
 47 
 48 /* 读多块,直至主机发送CMD12为止 Read multiple blocks until a CMD12 */
 49 #define CMD18 18
 50 #define CMD18_R R1
 51 
 52 /***************************** 块写命令集 Block write commands *************************/
 53 /* 写单块 Write a block of the size selected with CMD16 */
 54 #define CMD24 24
 55 #define CMD24_R R1
 56 
 57 /* 写多块 Multiple block write until a CMD12 */
 58 #define CMD25 25
 59 #define CMD25_R R1
 60 
 61 /* 写CSD寄存器 Program the programmable bits of the CSD */
 62 #define CMD27 27
 63 #define CMD27_R R1
 64 
 65 /***************************** 写保护 Write protection *****************************/
 66 /* Set the write protection bit of the addressed group */
 67 #define CMD28 28
 68 #define CMD28_R R1B
 69 
 70 /* Clear the write protection bit of the addressed group */
 71 #define CMD29 29
 72 #define CMD29_R R1B
 73 
 74 /* Ask the card for the status of the write protection bits */
 75 #define CMD30 30
 76 #define CMD30_R R1
 77 
 78 /***************************** 擦除命令 Erase commands *******************************/
 79 /* 设置擦除块的起始地址(只用于SD卡) Set the address of the first write block to be erased(only for SD) */
 80 #define CMD32 32
 81 #define CMD32_R R1
 82 
 83 /* 设置擦除块的终止地址(只用于SD卡) Set the address of the last write block to be erased(only for SD) */
 84 #define CMD33 33
 85 #define CMD33_R R1
 86 
 87 /* 设置擦除块的起始地址(只用于MMC卡) Set the address of the first write block to be erased(only for MMC) */
 88 #define CMD35 35
 89 #define CMD35_R R1
 90 
 91 /* 设置擦除块的终止地址(只用于MMC卡) Set the address of the last write block to be erased(only for MMC) */
 92 #define CMD36 36
 93 #define CMD36_R R1
 94 
 95 /* 擦除所选择的块 Erase the selected write blocks */
 96 #define CMD38 38
 97 #define CMD38_R R1B
 98 
 99 /***************************** 锁卡命令 Lock Card commands ***************************/
100 /* 设置/复位密码或上锁/解锁卡 Set/reset the password or lock/unlock the card */
101 #define CMD42 42
102 #define CMD42_R    R1B
103 /* Commands from 42 to 54, not defined here */
104 
105 /***************************** 应用命令 Application-specific commands ****************/
106 /* 禁止下一个命令为应用命令  Flag that the next command is application-specific */
107 #define CMD55 55
108 #define CMD55_R R1
109 
110 /* 应用命令的通用I/O  General purpose I/O for application-specific commands */
111 #define CMD56 56
112 #define CMD56_R R1
113 
114 /* 读OCR寄存器  Read the OCR (SPI mode only) */
115 #define CMD58 58
116 #define CMD58_R R3
117 
118 /* 使能或禁止 CRC Turn CRC on or off */
119 #define CMD59 59
120 #define CMD59_R R1
121 
122 /***************************** 应用命令 Application-specific commands ***************/
123 /* 获取 SD Status寄存器 Get the SD card's status */
124 #define ACMD13 13
125 #define ACMD13_R R2
126 
127 /* 得到已写入卡中的块的个数 Get the number of written write blocks (Minus errors ) */
128 #define ACMD22 22
129 #define ACMD22_R R1
130 
131 /* 在写之前,设置预先擦除的块的个数 Set the number of write blocks to be pre-erased before writing */
132 #define ACMD23 23
133 #define ACMD23_R R1
134 
135 /* 读取OCR寄存器 Get the card's OCR (SD mode) */
136 #define ACMD41 41
137 #define ACMD41_R R1
138 
139 /* 连接/断开CD/DATA[3]引脚上的上拉电阻 Connect or disconnect the 50kOhm internal pull-up on CD/DAT[3] */
140 #define ACMD42 42
141 #define ACMD42_R R1
142 
143 /* 读取SCR寄存器 Get the SD configuration register */
144 #define ACMD51 51
145 #define ACMD51_R R1
复制代码

张亚峰 SD卡的C语言实现:http://www.cnblogs.com/yuphone/archive/2011/04/19/2021549.html

现在我急需利用SD卡作为一个数据存储设备, 仅用于 数据写入和读取。但是并不知道SD卡的扇区地址的范围是什么,也就是说我想利用SD卡指令的COM17((单块读指令)和COM24(多块读指令)对SD卡进行纯读写,因为要读写多个扇区,而指令后面加地址参数的取值范围和格式我并不太清楚。希望各位高手帮我解答这个问题。

另外,SD卡只用来存取数值的话,有必要上系统吗?我的想法是只要知道扇区的地址,逐个读写就行了,不知道这个想法对不对。

单片机型号:AVR mega16 单片机
SD卡型号:1G TF卡(已利用卡套转为SD卡)

 CMD17的Address是32位的,最后9位是Block地址,前面的是Sector地址.如果Block=512 Byte的话,Block的值只能是0,Block!=0,Response会报错.

MMC_write_Blocks:
1,初始化,进入SPI模式,
2,发CMD25命令,
3,连读2字节,好象读1字节也可以,
4,发start(发一字节0xfc到sd卡),
5,发一个数据块到sd卡,
6,发2字节0xff到sd卡,当作CRC16,
7,读忙信息,直到不忙,
8,如果所有数据没有发完,跳到4,
9,结束,发end(发一字节0xfb到sd卡),
10,发CMD12命令。

SMMC_read_Blocks:
1,初始化,进入SPI模式,
2,发CMD18命令,
3,循环读到0xfe字节,
4,读1个数据块到sd卡,
5,读2字节CRC16,
6,如果所有数据没有读完,跳到3,
7,结束,发CMD12命令。

SD百科资料:http://baike.baidu.com/link?url=tz_JG2S6yNTDWR701WxF4sv7BPXEzy5RtwO-Z2Nc4mcC-OdEK8RV_l16DFY1EBFJ

MiniSD卡的设计初始是为逐渐开始普及的拍照手机而作,通过附赠的SD转接卡还可当做一般SD卡使用。

支持传输模式

SD卡共支持三种传输模式:SPI模式(独立序列输入和序列输出),1位SD模式 (独立指令和数据通道,独有的传输格式), 4位SD模式 (使用额外的针脚以及某些重新设置的针脚。支持四位宽的并行传输)。

低速卡通常支持 0~400 千比特/秒 数据传输率,采用SPI 和1位SD传输模式。 高速卡支持 0 ~ 100 兆比特/秒数据传输率,采用4位SD传输模式; 支持0–25 兆比特/秒 ,采用SPI和 1位SD模式。

因应SD卡的标准容量上限只有4GB,不足以应付日益上升的容量需求,联盟制定了新的SDHC标准。SDHC卡的外型跟普通的SD卡完全相同,而容量的下限为4GB,预料年内可推出高达32GB的SDHC卡。

技术论坛:http://www.amobbs.com/forum-1029-1.html

拜托你写这跟没写没多大区别,搞硬件的这些不理解就不叫搞硬件的。关键是这些器件如何操作,如何控制,怎样写时序,这才是重点。

http://www.amobbs.com/thread-4676153-1-1.html

 首先SD一般有两种接口协议,SPI和SDIO 如果你单片机没有SDIO那就用SPI好了,SPI用硬件的或者软件模拟都可以,然后调用SPI发送和接收函数写SD的驱动,写完之后SD卡就可以类似于EEPROM来用了,但是注意,这时候的SD卡是没有“文件”这个概念的,EEPROM也没有嘛,如果想读取SD中的文件那还需要移植文件系统,比如fatfs,移植成功后你就可以用类似于fopen之类的函数啦~驱动方面可以参考原子哥 《STM32不完全》手册的SD卡操作的相关内容,文件系统移植嘛,单片机强一点推荐用fatfs,弱一点的用Petit FAT,曾经在STC 1T的51单片机上使用Petit FAT文件系统读取bmp图像并在彩屏上显示,一分钟刷了一幅图,罪孽啊!!实在无聊

来源:SD卡的SPI模式的初始化顺序http://jinyong314.blog.163.com/blog/static/301657422010530112349686/

来源:基于FPGA的SD卡硬件控制器开发(SPI模式)

 http://zsl666.blog.163.com/blog/static/17626761520114308512108/

来源:http://forum.eepw.com.cn/thread/91070/1 我也有同样的问题。。。

http://www.amobbs.com/forum.php?mod=viewthread&action=printable&tid=4676153  用FPGA直接读取SD卡扇区数据

能不能把SD卡作为一个“大容量的可以按字节读写的数据存储器”。就是没有任何文件系统,直接像eeprom那样,按地址进行读写数据?谢谢!

SD卡规范和FAT文件格式规范是非常复杂,如果在项目中要单独来写这两个规范的非常费时和费力,而其非常占用系统资源;现在的便携仪采集的数据种类越来越多,数据量越来越大,而其大部分要求在计算机上备份数据或者后期用计算机处理数据;而SD卡以其容量大,速度快,接口简单,加之配套的读卡器便宜而发展迅速;

首先SD一般有两种接口协议,SPI和SDIO 如果你单片机没有SDIO那就用SPI好了,SPI用硬件的或者软件模拟都可以,然后调用SPI发送和接收函数写SD的驱动,写完之后SD卡就可以类似于EEPROM来用了,但是注意,这时候的SD卡是没有“文件”这个概念的,EEPROM也没有嘛,如果想读取SD中的文件那还需要移植文件系统,比如fatfs,移植成功后你就可以用类似于fopen之类的函数啦~驱动方面可以参考原子哥 《STM32不完全》手册的SD卡操作的相关内容,文件系统移植嘛,单片机强一点推荐用fatfs,弱一点的用Petit FAT,曾经在STC 1T的51单片机上使用Petit FAT文件系统读取bmp图像并在彩屏上显示,一分钟刷了一幅图,罪孽啊!!实在无聊

你要了解SD卡资料,文件系统,SPI通信的相关内容。

http://bbs.ednchina.com/BLOG_ARTICLE_2059372.HTM基于FPGA的bmp图片显示

1、最近编写了几个工厂需要的test pattern,用于UHD120和UHD60的pannel上,其中UHD120需要做半分屏处理,存储一行。

2、最近写了UHD120/60/30缩放到FHD120/60/30的算法,采用的算法是双线性算法,相邻4点取平均得到的。需要进行行的存储。注意DPRAM的使用技巧,读写控制逻辑的实现。

3、FHD120/60/30经过FRC处理后 VbyOne的实现是个技术难点。

I2C去拉动的方法,值得学习。。。

复制代码
 1 // local wires and regs
 2 reg sdaDeb;
 3 reg sclDeb;
 4 reg [`DEB_I2C_LEN-1:0] sdaPipe;
 5 reg [`DEB_I2C_LEN-1:0] sclPipe;
 6 
 7 reg [`SCL_DEL_LEN-1:0] sclDelayed;
 8 reg [`SDA_DEL_LEN-1:0] sdaDelayed;
 9 reg [1:0] startStopDetState;
10 wire clearStartStopDet;
11 wire sdaOut;
12 wire sdaIn;
13 wire [7:0] regAddr;
14 wire [7:0] dataToRegIF;
15 wire writeEn;
16 wire [7:0] dataFromRegIF;
17 reg [1:0] rstPipe;
18 wire rstSyncToClk;
19 reg startEdgeDet;
20 
21 assign sdaEn = sdaOut;
22 assign sda = (sdaOut == 1'b0) ? 1'b0 : 1'bz;
23 assign sdaIn = sda;
24 
25 // sync rst rsing edge to clk
26 always @(posedge clk) begin
27   if (rst == 1'b1)
28     rstPipe <= 2'b11;
29   else
30     rstPipe <= {rstPipe[0], 1'b0};
31 end
32 
33 assign rstSyncToClk = rstPipe[1];
34 
35 // debounce sda and scl
36 always @(posedge clk) begin
37   if (rstSyncToClk == 1'b1) begin
38     sdaPipe <= {`DEB_I2C_LEN{1'b1}};
39     sdaDeb <= 1'b1;
40     sclPipe <= {`DEB_I2C_LEN{1'b1}};
41     sclDeb <= 1'b1;
42   end
43   else begin
44     sdaPipe <= {sdaPipe[`DEB_I2C_LEN-2:0], sdaIn};
45     sclPipe <= {sclPipe[`DEB_I2C_LEN-2:0], scl};
46     if (&sclPipe[`DEB_I2C_LEN-1:1] == 1'b1)
47       sclDeb <= 1'b1;
48     else if (|sclPipe[`DEB_I2C_LEN-1:1] == 1'b0)
49       sclDeb <= 1'b0;
50     if (&sdaPipe[`DEB_I2C_LEN-1:1] == 1'b1)
51       sdaDeb <= 1'b1;
52     else if (|sdaPipe[`DEB_I2C_LEN-1:1] == 1'b0)
53       sdaDeb <= 1'b0;
54   end
55 end
56 
57 
58 // delay scl and sda
59 // sclDelayed is used as a delayed sampling clock
60 // sdaDelayed is only used for start stop detection
61 // Because sda hold time from scl falling is 0nS
62 // sda must be delayed with respect to scl to avoid incorrect
63 // detection of start/stop at scl falling edge. 
64 always @(posedge clk) begin
65   if (rstSyncToClk == 1'b1) begin
66     sclDelayed <= {`SCL_DEL_LEN{1'b1}};
67     sdaDelayed <= {`SDA_DEL_LEN{1'b1}};
68   end
69   else begin
70     sclDelayed <= {sclDelayed[`SCL_DEL_LEN-2:0], sclDeb};
71     sdaDelayed <= {sdaDelayed[`SDA_DEL_LEN-2:0], sdaDeb};
72   end
73 end
74 
75 // start stop detection
76 always @(posedge clk) begin
77   if (rstSyncToClk == 1'b1) begin
78     startStopDetState <= `NULL_DET;
79     startEdgeDet <= 1'b0;
80   end
81   else begin
82     if (&sclDelayed == 1'b1 && sdaDelayed[`SDA_DEL_LEN-2] == 1'b0 && sdaDelayed[`SDA_DEL_LEN-1] == 1'b1)
83       startEdgeDet <= 1'b1;
84     else
85       startEdgeDet <= 1'b0;
86     if (clearStartStopDet == 1'b1)
87       startStopDetState <= `NULL_DET;
88     else if (&sclDelayed == 1'b1) begin
89       if (sdaDelayed[`SDA_DEL_LEN-2] == 1'b1 && sdaDelayed[`SDA_DEL_LEN-1] == 1'b0) 
90         startStopDetState <= `STOP_DET;
91       else if (sdaDelayed[`SDA_DEL_LEN-2] == 1'b0 && sdaDelayed[`SDA_DEL_LEN-1] == 1'b1)
92         startStopDetState <= `START_DET;
93     end
94   end
95 end
复制代码

标签: SD

sprintf用法详解

简介: printf可能是许多程序员在开始学习C语言时接触到的第二个函数(我猜第一个是main),说起来,自然是老朋友了,可是,你对这个老朋友了解多吗?你对它的那个孪生兄弟sprintf了解多吗?在将各种类型的数据构造成字符串时,sprintf的强大功能很少会让你失望。

printf可能是许多程序员在开始学习C语言时接触到的第二个函数(我猜第一个是main),说起来,自然是老朋友了,可是,你对这个老朋友了解多吗?你对它的那个孪生兄弟sprintf了解多吗?在将各种类型的数据构造成字符串时,sprintf的强大功能很少会让你失望。
由于sprintf跟printf在用法上几乎一样,只是打印的目的地不同而已,前者打印到字符串中,后者则直接在命令行上输出。这也导致sprintf比printf有用得多。所以本文着重介绍sprintf,有时也穿插着用用pritnf。sprintf是个变参函数,定义如下:int sprintf( char *buffer, const char *format [, argument] … );除了前两个参数类型固定外,后面可以接任意多个参数。而它的精华,显然就在第二个参数:格式化字符串上。printf和sprintf都使用格式化字符串来指定串的格式,在格式串内部使用一些以“%”开头的格式说明符(format specifications)来占据一个位置,在后边的变参列表中提供相应的变量,最终函数就会用相应位置的变量来替代那个说明符,产生一个调用者想要的字符串。

1.      格式化数字字符串sprintf最常见的应用之一莫过于把整数打印到字符串中,所以,spritnf在大多数场合可以替代itoa。如:

//把整数123打印成一个字符串保存在s中。

sprintf(s, “%d”, 123);   //产生”123″
可以指定宽度,不足的左边补空格:

sprintf(s, “%8d%8d”, 123, 4567); //产生:”    123    4567″
当然也可以左对齐:

sprintf(s, “%-8d%8d”, 123, 4567); //产生:”123         4567″
也可以按照16进制打印:

sprintf(s, “%8x”, 4567); //小写16进制,宽度占8个位置,右对齐

sprintf(s, “%-8X”, 4568); //大写16进制,宽度占8个位置,左对齐
这样,一个整数的16进制字符串就很容易得到,但我们在打印16进制内容时,通常想要一种左边补0的等宽格式,那该怎么做呢?很简单,在表示宽度的数字前面加个0就可以了。

sprintf(s, “%08X”, 4567); //产生:”000011D7″
上面以”%d”进行的10进制打印同样也可以使用这种左边补0的方式。这里要注意一个符号扩展的问题:比如,假如我们想打印短整数(short)-1的内存16进制表示形式,在Win32平台上,一个short型占2个字节,所以我们自然希望用4个16进制数字来打印它:

short si = -1;

sprintf(s, “%04X”, si);
产生“FFFFFFFF”,怎么回事?因为spritnf是个变参函数,除了前面两个参数之外,后面的参数都不是类型安全的,函数更没有办法仅仅通过一个“%X”就能得知当初函数调用前参数压栈时被压进来的到底是个4字节的整数还是个2字节的短整数,所以采取了统一4字节的处理方式,导致参数压栈时做了符号扩展,扩展成了32位的整数-1,打印时4个位置不够了,就把32位整数-1的8位16进制都打印出来了。如果你想看si的本来面目,那么就应该让编译器做0扩展而不是符号扩展(扩展时二进制左边补0而不是补符号位):

sprintf(s, “%04X”, (unsigned short)si);
就可以了。或者:

unsigned short si = -1;

sprintf(s, “%04X”, si);
sprintf和printf还可以按8进制打印整数字符串,使用”%o”。注意8进制和16进制都不会打印出负数,都是无符号的,实际上也就是变量的内部编码的直接的16进制或8进制表示。

2.      控制浮点数打印格式浮点数的打印和格式控制是sprintf的又一大常用功能,浮点数使用格式符”%f”控制,默认保留小数点后6位数字,比如:

sprintf(s, “%f”, 3.1415926);    //产生”3.141593″
但有时我们希望自己控制打印的宽度和小数位数,这时就应该使用:”%m.nf”格式,其中m表示打印的宽度,n表示小数点后的位数。比如:

sprintf(s, “%10.3f”, 3.1415626);   //产生:”     3.142″

sprintf(s, “%-10.3f”, 3.1415626); //产生:”3.142     “

sprintf(s, “%.3f”, 3.1415626); //不指定总宽度,产生:”3.142″
注意一个问题,你猜

int i = 100;

sprintf(s, “%.2f”, i);
会打出什么东东来?“100.00”?对吗?自己试试就知道了,同时也试试下面这个:

sprintf(s, “%.2f”, (double)i);
第一个打出来的肯定不是正确结果,原因跟前面提到的一样,参数压栈时调用者并不知道跟i相对应的格式控制符是个”%f”。而函数执行时函数本身则并不知道当年被压入栈里的是个整数,于是可怜的保存整数i的那4个字节就被不由分说地强行作为浮点数格式来解释了,整个乱套了。不过,如果有人有兴趣使用手工编码一个浮点数,那么倒可以使用这种方法来检验一下你手工编排的结果是否正确。J

字符/Ascii码对照我们知道,在C/C++语言中,char也是一种普通的scalable类型,除了字长之外,它与short,int,long这些类型没有本质区别,只不过被大家习惯用来表示字符和字符串而已。(或许当年该把这个类型叫做“byte”,然后现在就可以根据实际情况,使用byte或short来把char通过typedef定义出来,这样更合适些)于是,使用”%d”或者”%x”打印一个字符,便能得出它的10进制或16进制的ASCII码;反过来,使用”%c”打印一个整数,便可以看到它所对应的ASCII字符。以下程序段把所有可见字符的ASCII码对照表打印到屏幕上(这里采用printf,注意”#”与”%X”合用时自动为16进制数增加”0X”前缀):

for(int i = 32; i < 127; i++) {

    printf(“[ %c ]: %3d 0x%#04X\n”, i, i, i);

}

3.      连接字符串sprintf的格式控制串中既然可以插入各种东西,并最终把它们“连成一串”,自然也就能够连接字符串,从而在许多场合可以替代strcat,但sprintf能够一次连接多个字符串(自然也可以同时在它们中间插入别的内容,总之非常灵活)。比如:

char* who = “I”;

char* whom = “CSDN”;

sprintf(s, “%s love %s.”, who, whom); //产生:”I love CSDN. “
strcat只能连接字符串(一段以’\0’结尾的字符数组或叫做字符缓冲,null-terminated-string),但有时我们有两段字符缓冲区,他们并不是以’\0’结尾。比如许多从第三方库函数中返回的字符数组,从硬件或者网络传输中读进来的字符流,它们未必每一段字符序列后面都有个相应的’\0’来结尾。如果直接连接,不管是sprintf还是strcat肯定会导致非法内存操作,而strncat也至少要求第一个参数是个null-terminated-string,那该怎么办呢?我们自然会想起前面介绍打印整数和浮点数时可以指定宽度,字符串也一样的。比如:

char a1[] = {‘A’, ‘B’, ‘C’, ‘D’, ‘E’, ‘F’, ‘G’};

char a2[] = {‘H’, ‘I’, ‘J’, ‘K’, ‘L’, ‘M’, ‘N’};
如果:

sprintf(s, “%s%s”, a1, a2); //Don’t do that!
十有八九要出问题了。是否可以改成:

sprintf(s, “%7s%7s”, a1, a2);
也没好到哪儿去,正确的应该是:

sprintf(s, “%.7s%.7s”, a1, a2);//产生:”ABCDEFGHIJKLMN”
这可以类比打印浮点数的”%m.nf”,在”%m.ns”中,m表示占用宽度(字符串长度不足时补空格,超出了则按照实际宽度打印),n才表示从相应的字符串中最多取用的字符数。通常在打印字符串时m没什么大用,还是点号后面的n用的多。自然,也可以前后都只取部分字符:

sprintf(s, “%.6s%.5s”, a1, a2);//产生:”ABCDEFHIJKL”
在许多时候,我们或许还希望这些格式控制符中用以指定长度信息的数字是动态的,而不是静态指定的,因为许多时候,程序要到运行时才会清楚到底需要取字符数组中的几个字符,这种动态的宽度/精度设置功能在sprintf的实现中也被考虑到了,sprintf采用”*”来占用一个本来需要一个指定宽度或精度的常数数字的位置,同样,而实际的宽度或精度就可以和其它被打印的变量一样被提供出来,于是,上面的例子可以变成:

sprintf(s, “%.*s%.*s”, 7, a1, 7, a2);
或者:

sprintf(s, “%.*s%.*s”, sizeof(a1), a1, sizeof(a2), a2);
实际上,前面介绍的打印字符、整数、浮点数等都可以动态指定那些常量值,比如:

sprintf(s, “%-*d”, 4, ‘A’); //产生”65 “

sprintf(s, “%#0*X”, 8, 128);    //产生”0X000080″,”#”产生0X

sprintf(s, “%*.*f”, 10, 2, 3.1415926); //产生”      3.14″

4.      打印地址信息有时调试程序时,我们可能想查看某些变量或者成员的地址,由于地址或者指针也不过是个32位的数,你完全可以使用打印无符号整数的”%u”把他们打印出来:

sprintf(s, “%u”, &i);
不过通常人们还是喜欢使用16进制而不是10进制来显示一个地址:

sprintf(s, “%08X”, &i);
然而,这些都是间接的方法,对于地址打印,sprintf 提供了专门的”%p”:

sprintf(s, “%p”, &i);

我觉得它实际上就相当于:

sprintf(s, “%0*x”, 2 * sizeof(void *), &i);

5.      利用sprintf的返回值较少有人注意printf/sprintf函数的返回值,但有时它却是有用的,spritnf返回了本次函数调用最终打印到字符缓冲区中的字符数目。也就是说每当一次sprinf调用结束以后,你无须再调用一次strlen便已经知道了结果字符串的长度。如:

int len = sprintf(s, “%d”, i);
对于正整数来说,len便等于整数i的10进制位数。下面的是个完整的例子,产生10个[0, 100)之间的随机数,并将他们打印到一个字符数组s中,以逗号分隔开。

#include <stdio.h>

#include <time.h>

#include <stdlib.h>

int main() {

    srand(time(0));

    char s[64];

    int offset = 0;

    for(int i = 0; i < 10; i++) {

       offset += sprintf(s + offset, “%d,”, rand() % 100);

    }

    s[offset – 1] = ‘\n’;//将最后一个逗号换成换行符。

    printf(s);

    return 0;

}
设想当你从数据库中取出一条记录,然后希望把他们的各个字段按照某种规则连接成一个字符串时,就可以使用这种方法,从理论上讲,他应该比不断的strcat效率高,因为strcat每次调用都需要先找到最后的那个’\0’的位置,而在上面给出的例子中,我们每次都利用sprintf返回值把这个位置直接记下来了。

6.      使用sprintf的常见问题sprintf是个变参函数,使用时经常出问题,而且只要出问题通常就是能导致程序崩溃的内存访问错误,但好在由sprintf误用导致的问题虽然严重,却很容易找出,无非就是那么几种情况,通常用眼睛再把出错的代码多看几眼就看出来了。

?         缓冲区溢出第一个参数的长度太短了,没的说,给个大点的地方吧。当然也可能是后面的参数的问题,建议变参对应一定要细心,而打印字符串时,尽量使用”%.ns”的形式指定最大字符数。

?         忘记了第一个参数低级得不能再低级问题,用printf用得太惯了。//偶就常犯。:。(

?         变参对应出问题通常是忘记了提供对应某个格式符的变参,导致以后的参数统统错位,检查检查吧。尤其是对应”*”的那些参数,都提供了吗?不要把一个整数对应一个”%s”,编译器会觉得你欺她太甚了(编译器是obj和exe的妈妈,应该是个女的,:P)。

7.      strftimesprintf还有个不错的表妹:strftime,专门用于格式化时间字符串的,用法跟她表哥很像,也是一大堆格式控制符,只是毕竟小姑娘家心细,她还要调用者指定缓冲区的最大长度,可能是为了在出现问题时可以推卸责任吧。这里举个例子:

time_t t = time(0);

//产生”YYYY-MM-DD hh:mm:ss”格式的字符串。

char s[32];

strftime(s, sizeof(s), “%Y-%m-%d %H:%M:%S”, localtime(&t));
sprintf在MFC中也能找到他的知音:CString::Format,strftime在MFC中自然也有她的同道:CTime::Format,这一对由于从面向对象哪里得到了赞助,用以写出的代码更觉优雅。

8.      后记本文介绍的所有这些功能,在MSDN中都可以很容易地查到,笔者只是根据自己的使用经验,结合一些例子,把一些常用的,有用的,而可能为许多初学者所不知的用法介绍了一点,希望大家不要笑话,也希望大家批评指正。有人认为这种带变参的函数会引起各种问题,因而不提倡使用。但笔者本人每每还是抵挡不了它们强大功能的诱惑,在实际工作中一直在使用。实际上,C#.NET从开始就支持变参,刚发布不久的Java5.0也支持变参了。感谢ericzhangali(另一个空间)仔细审阅了全稿,纠正了很多小错误,并提出了一些建议。也感谢laomai(老迈)阅读了全稿并给出了增删一些内容的建议。

①获取System时间: void GetSystemTime(LPSYSTEMTIME lpSystemTime); 下面是例子:
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
void main() {
SYSTEMTIME st; //定义存放时间的结构体
char strTime[256];
int n=”0″;
GetSystemTime(&st);
n = sprintf(strTime,”Year:\t%d\n”,st.wYear);
n += sprintf(strTime+n,”Month:\t%d\n”,st.wMonth);
n += sprintf(strTime+n,”Day:\t%d\n”,st.wDay);
n += sprintf(strTime+n,”Date:\t%d\n”,st.wDayOfWeek);
n += sprintf(strTime+n,”Hour:\t%d\n”,st.wHour);
n += sprintf(strTime+n,”Minute:\t%d\n”,st.wMinute);
n += sprintf(strTime+n,”Second:\t%d\n”,st.wSecond);
n += sprintf(strTime+n,”MilliSecond:\t%d\n”,st.wMilliseconds);

printf(“%s”,strTime);
system(“pause”);
}
******************************************

 参量表是需要输出的一系列参数, 其个数必须与格式化字符串所说明的输出
参数个数一样多, 各参数之间用”,”分开, 且顺序一一对应,  否则将会出现意想
不到的错误。
    1. 格式化规定符
    Turbo C2.0提供的格式化规定符如下:
━━━━━━━━━━━━━━━━━━━━━━━━━━
   符号                  作用
──────────────────────────
    %d              十进制有符号整数
    %u              十进制无符号整数
    %f              浮点数
    %s              字符串
    %c              单个字符
    %p              指针的值
    %e              指数形式的浮点数
    %x, %X          无符号以十六进制表示的整数
    %0              无符号以八进制表示的整数
    %g              自动选择合适的表示法
━━━━━━━━━━━━━━━━━━━━━━━━━━
    说明:
    (1). 可以在”%”和字母之间插进数字表示最大场宽。
     例如:  %3d   表示输出3位整型数, 不够3位右对齐。
            %9.2f 表示输出场宽为9的浮点数, 其中小数位为2, 整数位为6,
                  小数点占一位, 不够9位右对齐。
            %8s   表示输出8个字符的字符串, 不够8个字符右对齐。
    如果字符串的长度、或整型数位数超过说明的场宽, 将按其实际长度输出。
但对浮点数, 若整数部分位数超过了说明的整数位宽度, 将按实际整数位输出;
若小数部分位数超过了说明的小数位宽度, 则按说明的宽度以四舍五入输出。
    另外, 若想在输出值前加一些0, 就应在场宽项前加个0。
    例如:   %04d  表示在输出一个小于4位的数值时, 将在前面补0使其总宽度
为4位。
    如果用浮点数表示字符或整型量的输出格式, 小数点后的数字代表最大宽度,
小数点前的数字代表最小宽度。
    例如: %6.9s 表示显示一个长度不小于6且不大于9的字符串。若大于9,  则
第9个字符以后的内容将被删除。
    (2). 可以在”%”和字母之间加小写字母l, 表示输出的是长型数。
    例如:   %ld   表示输出long整数
            %lf   表示输出double浮点数
    (3). 可以控制输出左对齐或右对齐, 即在”%”和字母之间加入一个”-” 号可
说明输出为左对齐, 否则为右对齐。
    例如:   %-7d  表示输出7位整数左对齐
            %-10s 表示输出10个字符左对齐
    2. 一些特殊规定字符
━━━━━━━━━━━━━━━━━━━━━━━━━━
    字符                           作用
──────────────────────────
     \n                   换行
     \f                   清屏并换页
     \r                   回车
     \t                   Tab符
     \xhh                 表示一个ASCII码用16进表示,
                          其中hh是1到2个16进制数
━━━━━━━━━━━━━━━━━━━━━━━━━━

版权声明:本文来自开发者社区 > 云计算 > 文章,如侵犯了你的版权请联系我删除。

单片机测量PWM占空比的三种方法

cathy — 周四, 11/19/2020 – 13:57

PWM(Pulse Width Modulation),一般指脉冲宽度调节,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,广泛应用在从测量、通信到功率控制与变换的许多领域中,比如LED亮度调节、电机转速控制等。

而在某些特殊应用中,我们也需要通过测量输入PWM的占空比,来实现不同的输出控制,这就需要使用到PWM占空比的测量方法。这里介绍三种不同的测量方法:阻塞方式、中断方式以及定时器捕获功能。

1. 阻塞方式

MCU阻塞方式测量PWM占空比的原理比较简单,也只需要使用到一个普通的IO端口(设置为输入模式,对于51而言那就是一个普通的双向口)。具体实现流程为:

  • 等待上升沿到来,然后开启定时器,开始计时;
  • 等待下降沿到来,记录下定时器的计数值,即得到PWM的高电平时间H
  • 同时,清零定时器,重新开始计数;
  • 等待上升沿到来,记录下定时器的计数值,即得到PWM的低电平时间L
  • 计算得出占空比:duty = H / (H + L);

阻塞方式原理简单,而且只需要MCU有一个定时器的资源即可实现;但采集时阻塞CPU运行,阻塞的时间和输入PWM的周期相关,只适用于实时性较低的系统。

另外,上述流程中存在着一个严重的BUG,即当输入的PWM占空比为0%或者100%时,程序会被一直阻塞,等待上升沿/下降沿的到来。所以解决方法是,在等待上升沿/下降沿的过程中,实时提取定时器的值,一旦定时时间超过1个周期的限定(一般可定义为2-3个周期时间),即退出等待,并根据端口电平判断此时占空比为0%(低电平)或100%(高电平)。

示例代码,仅供参考:

//获取PWM输入脚的电平
#define PWM_IN()   xxxxxx
//定义超时时间(如2-3倍PWM周期)
#define T1_TIMEOUT  xxxxxx

uint8_t PWM_Analyse(void)
{
    uint8_t duty = 0xFF;
    uint16_t pwm_H = 0;
    uint16_t pwm_L = 0;

    if (PWM_IN())   //初始为高电平,则开始等待低电平
    {
        TH1 = 0;
        while (PWM_IN()) //等待下降沿
        {
            if (TH1 >= T1_TIMEOUT)  //下降沿没有到来,判定为100%占空比
            {
                duty = 100;
                return duty;
            }
        }

        TH1 = 0;
        TL1 = 0;
        while (!PWM_IN()) //等待上升沿
        {
            if (TH1 >= T1_TIMEOUT)  //上升沿没有到来,判定为0%占空比
            {
                duty = 0;
                return duty;
            }
        }
        pwm_L = (TH1 << 8) | TL1;

        TH1 = 0;
        TL1 = 0;
        while (PWM_IN()) //等待下降沿
        {
            if (TH1 >= T1_TIMEOUT)  //下降沿没有到来,判定为100%占空比
            {
                duty = 100;
                return duty;
            }
        }
        pwm_H = (TH1 << 8) | TL1;

        duty = pwm_H * 100 / (pwm_H + pwm_L);
        return duty;
    }
    else    //当前为低电平,则开始等待高电平
    {
        TH1 = 0;
        while (!PWM_IN()) //等待上升沿
        {
            if (TH1 >= T1_TIMEOUT)  //上升沿没有到来,判定为0%占空比
            {
                duty = 0;
                return duty;
            }
        }

        TH1 = 0;
        TL1 = 0;
        while (PWM_IN()) //等待下降沿
        {
            if (TH1 >= T1_TIMEOUT)  //下降沿没有到来,判定为100%占空比
            {
                duty = 100;
                return duty;
            }
        }
        pwm_H = (TH1 << 8) | TL1;

        TH1 = 0;
        TL1 = 0;
        while (!PWM_IN()) //等待上升沿
        {
            if (TH1 >= T1_TIMEOUT)  //上升沿没有到来,判定为0%占空比
            {
                duty = 0;
                return duty;
            }
        }
        pwm_L = (TH1 << 8) | TL1;

        duty = pwm_H * 100 / (pwm_H + pwm_L);
        return duty;
    }

    return 0xFF;
}

2. 中断方式

中断方式的PWM采集原理与阻塞方式相同,只是将判定移动至外部中断中。开启MCU端口的外部中断(上升沿和下降沿中断);如果MCU外部中断触发不支持上升和下降沿中断,则先开启上升沿中断,在中断处理中切换中断触发条件。

处理方法:在中断处理函数中,根据当前电平状态,记录下定时器的值,并清零定时器的值,重新开始下一轮计时。

0%和100%的处理:设定一个定时递增的变量,同时在外部中断中执行清零操作。若该变量超过一定值(说明外部中断有较长时间没有触发),则判定为0%或100%。

uint16_t pwm_H = 0;
uint16_t pwm_L = 0;
uint16_t pwm_time_out = 0;
void EXT1_ISR(void) interrupt EXTI1_VECTOR
{
    if (PWM_IN())
    {
        pwm_L = (TH1 << 8) | TL1;    //记录低电平时间
        TH1 = 0;
        TL1 = 0;
    }
    else
    {
        pwm_H = (TH1 << 8) | TL1;    //记录高电平时间
        TH1 = 0;
        TL1 = 0;
    }

    //该变量定时递增(如1ms递增1),在外部中断中清零
    //在主程序中判断,超过一定值时认为PWM占空比为0%或100%
    pwm_time_out = 0;

    return;
}

注:使用中断方式,则占空比计算不建议放在中断中处理;同时,为了保证占空比的准确性,可以连续2-3次计算结果一致时,再确定当前占空比的结果。

3. MCU捕获方式

采用捕获方式的前提是MCU支持捕获功能。当前部分厂家推出的51内核单片机,会包含一个定时器2,其拥有捕获功能;或者采用32位单片机,一般都带有捕获功能。捕获的原理很简单,当上升沿或下降沿来临时,MCU硬件将定时器/计数器的值保存在一个影子寄存器中,并产生捕获中断。

通过固定每次上升/下降沿的计数器值,相减即可分别得出高电平值和低电平值,从而计算出占空比。“”

下面以某颗51内核的MCU为例,提供示例代码:

unsigned int pwm_fall = 0, pwm_rise = 0;

volatile unsigned int pwm_H;
volatile unsigned int pwm_L;

volatile unsigned char pwm_time_out;
//------------------------------------------------------------
void T2_interrupt(void) interrupt 5          //定时器2中断;
{

    if (CCCON & 0x02) //CC1中断标志位
    {
        CCCON  &= 0xFD; //清除中断标志

        if (PWM_IN())   //上升沿触发
        {
            pwm_rise = CC1;     //获取捕获寄存器中的值
            pwm_L = pwm_rise - pwm_fall;
        }
        else
        {
            pwm_fall = CC1;     //获取捕获寄存器中的值
            pwm_H = pwm_fall - pwm_rise;
        }

        //该变量定时递增(如1ms递增1),在外部中断中清零
        //在主程序中判断,超过一定值时认为PWM占空比为0%或100%
        pwm_time_out = 0;
    }
}

注: pwm_rise/pwm_fall/pwm_L/pwm_H都必须使用无符号数,否则相减时可能得到错误的值。

总结

方式一:任何单片机都可以实现,但是阻塞方式会使系统的实时性变差;
方式二:在使用时,需要保证外部中断的最高优先级,不可以被其他中断打断,以保证其准确性;
方式三:的稳定性和准确性都较高,但是需要MCU硬件支持。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/crazy_kismet/article/details/102756206

本文转载自:CSDN博客(博主:freeze chen)
免责声明:本文为转载文章,转载此文目的在于传递更多信息,版权归原作者所有。本文所用视频、图片、文字如涉及作品版权问题,请联系小编进行处理(联系邮箱:[email protected])。

卡尔曼滤波算法

1. 卡尔曼滤波器的介绍

为了可以更加容易的理解卡尔曼滤波器,这里会应用形象的描述方法来讲解,而不是像大多数参考书那样罗列一大堆的数学公式和数学符号。但是,他的5条公式是其核心内容。结合现代的计算机,其实卡尔曼的程序相当的简单,只要你理解了他的那5条公式。

在介绍他的5条公式之前,先让我们来根据下面的例子一步一步的探索。

假设我们要研究的对象是一个房间的温度。根据你的经验判断,这个房间的温度是恒定的,也就是下一分钟的温度等于现在这一分钟的温度(假设我们用一分钟来做时间单位)。假设你对你的经验不是100%的相信,可能会有上下偏差几度。我们把这些偏差看成是高斯白噪声(White Gaussian Noise),也就是这些偏差跟前后时间是没有关系的而且符合高斯分配(Gaussian Distribution)。另外,我们在房间里放一个温度计,但是这个温度计也不准确的,测量值会比实际值偏差。我们也把这些偏差看成是高斯白噪声。

好了,现在对于某一分钟我们有两个有关于该房间的温度值:你根据经验的预测值(系统的预测值)和温度计的值(测量值)。下面我们要用这两个值结合他们各自的噪声来估算出房间的实际温度值。

假如我们要估算k时刻的是实际温度值。首先你要根据k-1时刻的温度值,来预测k时刻的温度。因为你相信温度是恒定的,所以你会得到k时刻的温度预测值是跟k-1时刻一样的,假设是23度,同时该值的高斯噪声的偏差是5度(5是这样得到的:如果k-1时刻估算出的最优温度值的偏差是3,你对自己预测的不确定度是4度,他们平方相加再开方,就是5)。然后,你从温度计那里得到了k时刻的温度值,假设是25度,同时该值的偏差是4度。

由于我们用于估算k时刻的实际温度有两个温度值,分别是23度和25度。究竟实际温度是多少呢?相信自己还是相信温度计呢?究竟相信谁多一点,我们可以用他们的covariance来判断。因为Kg^2=5^2/(5^2+4^2),所以Kg=0.78,我们可以估算出k时刻的实际温度值是:23+0.78*(25-23)=24.56度。可以看出,因为温度计的covariance比较小(比较相信温度计),所以估算出的最优温度值偏向温度计的值。

现在我们已经得到k时刻的最优温度值了,下一步就是要进入k+1时刻,进行新的最优估算。到现在为止,好像还没看到什么自回归的东西出现。对了,在进入k+1时刻之前,我们还要算出k时刻那个最优值(24.56度)的偏差。算法如下:((1-Kg)*5^2)^0.5=2.35。这里的5就是上面的k时刻你预测的那个23度温度值的偏差,得出的2.35就是进入k+1时刻以后k时刻估算出的最优温度值的偏差(对应于上面的3)。

就是这样,卡尔曼滤波器就不断的把covariance递归,从而估算出最优的温度值。他运行的很快,而且它只保留了上一时刻的covariance。上面的Kg,就是卡尔曼增益(Kalman Gain)。他可以随不同的时刻而改变他自己的值,是不是很神奇!

下面就要言归正传,讨论真正工程系统上的卡尔曼。

2. 卡尔曼滤波器算法
在这一部分,我们就来描述源于Dr Kalman 的卡尔曼滤波器。下面的描述,会涉及一些基本的概念知识,包括概率(Probability),随即变量(Random Variable),高斯或正态分配(Gaussian Distribution)还有State-space Model等等。但对于卡尔曼滤波器的详细证明,这里不能一一描述。

首先,我们先要引入一个离散控制过程的系统。该系统可用一个线性随机微分方程(Linear Stochastic Difference equation)来描述:
X(k)=A X(k-1)+B U(k)+W(k)
再加上系统的测量值:
Z(k)=H X(k)+V(k)
上两式子中,X(k)是k时刻的系统状态,U(k)是k时刻对系统的控制量。A和B是系统参数,对于多模型系统,他们为矩阵。Z(k)是k时刻的测量值,H是测量系统的参数,对于多测量系统,H为矩阵。W(k)和V(k)分别表示过程和测量的噪声。他们被假设成高斯白噪声(White Gaussian Noise),他们的covariance 分别是Q,R(这里我们假设他们不随系统状态变化而变化)。

对于满足上面的条件(线性随机微分系统,过程和测量都是高斯白噪声),卡尔曼滤波器是最优的信息处理器。下面我们来用他们结合他们的covariances 来估算系统的最优化输出(类似上一节那个温度的例子)。

首先我们要利用系统的过程模型,来预测下一状态的系统。假设现在的系统状态是k,根据系统的模型,可以基于系统的上一状态而预测出现在状态:
X(k|k-1)=A X(k-1|k-1)+B U(k) ……….. (1)
式(1)中,X(k|k-1)是利用上一状态预测的结果,X(k-1|k-1)是上一状态最优的结果,U(k)为现在状态的控制量,如果没有控制量,它可以为0。

到现在为止,我们的系统结果已经更新了,可是,对应于X(k|k-1)的covariance还没更新。我们用P表示covariance:
P(k|k-1)=A P(k-1|k-1) A’+Q ……… (2)
式(2)中,P(k|k-1)是X(k|k-1)对应的covariance,P(k-1|k-1)是X(k-1|k-1)对应的covariance,A’表示A的转置矩阵,Q是系统过程的covariance。式子1,2就是卡尔曼滤波器5个公式当中的前两个,也就是对系统的预测。

现在我们有了现在状态的预测结果,然后我们再收集现在状态的测量值。结合预测值和测量值,我们可以得到现在状态(k)的最优化估算值X(k|k):
X(k|k)= X(k|k-1)+Kg(k) (Z(k)-H X(k|k-1)) ……… (3)
其中Kg为卡尔曼增益(Kalman Gain):
Kg(k)= P(k|k-1) H’ / (H P(k|k-1) H’ + R) ……… (4)

到现在为止,我们已经得到了k状态下最优的估算值X(k|k)。但是为了要另卡尔曼滤波器不断的运行下去直到系统过程结束,我们还要更新k状态下X(k|k)的covariance:
P(k|k)=(I-Kg(k) H)P(k|k-1) ……… (5)
其中I 为1的矩阵,对于单模型单测量,I=1。当系统进入k+1状态时,P(k|k)就是式子(2)的P(k-1|k-1)。这样,算法就可以自回归的运算下去。

卡尔曼滤波器的原理基本描述了,式子1,2,3,4和5就是他的5 个基本公式。根据这5个公式,可以很容易的实现计算机的程序。

下面,我会用程序举一个实际运行的例子。。。

3. 简单例子

这里我们结合第二第三节,举一个非常简单的例子来说明卡尔曼滤波器的工作过程。所举的例子是进一步描述第二节的例子,而且还会配以程序模拟结果。

根据第二节的描述,把房间看成一个系统,然后对这个系统建模。当然,我们见的模型不需要非常地精确。我们所知道的这个房间的温度是跟前一时刻的温度相同的,所以A=1。没有控制量,所以U(k)=0。因此得出:
X(k|k-1)=X(k-1|k-1) ……….. (6)
式子(2)可以改成:
P(k|k-1)=P(k-1|k-1) +Q ……… (7)

因为测量的值是温度计的,跟温度直接对应,所以H=1。式子3,4,5可以改成以下:
X(k|k)= X(k|k-1)+Kg(k) (Z(k)-X(k|k-1)) ……… (8)
Kg(k)= P(k|k-1) / (P(k|k-1) + R) ……… (9)
P(k|k)=(1-Kg(k))P(k|k-1) ……… (10)

现在我们模拟一组测量值作为输入。假设房间的真实温度为25度,我模拟了200个测量值,这些测量值的平均值为25度,但是加入了标准偏差为几度的高斯白噪声(在图中为蓝线)。

为了令卡尔曼滤波器开始工作,我们需要告诉卡尔曼两个零时刻的初始值,是X(0|0)和P(0|0)。他们的值不用太在意,随便给一个就可以了,因为随着卡尔曼的工作,X会逐渐的收敛。但是对于P,一般不要取0,因为这样可能会令卡尔曼完全相信你给定的X(0|0)是系统最优的,从而使算法不能收敛。我选了X(0|0)=1度,P(0|0)=10。

该系统的真实温度为25度,图中用黑线表示。图中红线是卡尔曼滤波器输出的最优化结果(该结果在算法中设置了Q=1e-6,R=1e-1)。

  • clear
  • N=200;
  • w(1)=0;
  • w=randn(1,N)
  • x(1)=0;
  • a=1;
  • for k=2:N;
  • x(k)=a*x(k-1)+w(k-1);
  • end
  •  
  •  
  • V=randn(1,N);
  • q1=std(V);
  • Rvv=q1.^2;
  • q2=std(x);
  • Rxx=q2.^2;
  • q3=std(w);
  • Rww=q3.^2;
  • c=0.2;
  • Y=c*x+V;
  •  
  • p(1)=0;
  • s(1)=0;
  • for t=2:N;
  • p1(t)=a.^2*p(t-1)+Rww;
  • b(t)=c*p1(t)/(c.^2*p1(t)+Rvv);
  • s(t)=a*s(t-1)+b(t)*(Y(t)-a*c*s(t-1));
  • p(t)=p1(t)-c*b(t)*p1(t);
  • end
  •  
  • t=1:N;
  • plot(t,s,’r’,t,Y,’g’,t,x,’b’);
  •  

关于iptables过滤关键词网站的原理和方法解读

服务器上有时候难免做些限制,比如说一个游戏加速器可能会禁止用来网站访问,但是问题来了,我们并不能简单的禁止一个80或者443来达到目的,因为有些游戏会从网站上去获取一些信息,比如现在最火的绝地求生大逃杀,吃个鸡却要先从一个网页进行登录,所以禁止80/443端口的方案我们可以直接否掉,奶牛的方法是用iptables进行关键词和网站过滤。

iptables过滤关键词网站的原理解读

INPUT,OUTPUT,FORWARD选哪个?

使用iptables过滤关键词和网站,我们需要对进出服务器的流量都进行过滤,当然也有人建议在传输层过滤,那奶牛就来谈谈自己的理解。首先INPUT,OUTPUT,FORWARD三个分别对应进、出、传输。如果我们在FORWARD过滤,应该可以达到预期效果,但是如果我们的服务器是用作转发的呢?比如一个请求发送过来,我们允许INPUT进入,然后我们的转发服务器会先将请求转发出去,再接收转发内容,最后在转发回程的过程中过滤拦截,这样子,服务器的流量带宽资源就会被浪费,也许你会说在接收到请求之后关键词和网站就已经过滤了,其实不然,很多关键词是包含在请求获得的内容中的,所以不可能在转发之前就过滤掉,我们需要的结果是在转发之前过滤掉。那么我们就用INPUT进行限制?通过实测,发现单单用INPUT也不能过滤干净,奶牛的理解是INPUT不会拒绝请求的结果,所以我们还需要和OUTPUT配合使用。

DROP,REJECT选哪个?

奶牛选择的是DROP。我们说说DROP和REJECT的区别,当我们使用REJECT的时候,如果拒绝请求,在iptables中输入

iptables -L -nv

可以看到拒绝的请求会reject-withicmp-port-unreachable,也就是会通过一个icmp包来告诉目标请求被拒绝了,这个资源也是属于浪费的,如果我们直接用DROP丢弃,则不会有这个回包。

iptables过滤关键词网站的方法

首先备份好我们的iptables规则,避免操作失误。备份和还原的命令如下:

iptables-save > iptables_origin_rules
iptables-restore > iptables_origin_rules

然后我们的规则这样写:

iptables -A INPUT -m string --string "xxx.com" --algo bm --to 65535 -j DROP
iptables -A OUTPUT -m string --string "xxx.com" --algo bm --to 65535 -j DROP

其中的xxx.com就是关键词,可以写网站域名,也可以写关键词,但是关键词一定要想好,否则可能会导致很多东西都无法访问的。如果规则写错了,可以通过命令删除规则:

iptables -D INPUT -m string --string "xxx.com" --algo bm --to 65535 -j DROP
iptables -D OUTPUT -m string --string "xxx.com" --algo bm --to 65535 -j DROP

过滤需谨慎,特别是一些国cdn、公共库、ssl、dns等一定要谨慎处理,否则可能会导致很多服务无法访问严重后果。

本文抄作业,来自关于iptables过滤关键词网站的原理和方法解读 | 奶牛博客 (nenew.net)谢谢!