デジカメ弐号機 第4回 ストリーミング
2022.11.18
YouTubeでポイントを説明しています。画像をクリックすると再生できます。
第4回では、ArduCAM カメラモジュールを用いたストリーミング画像を、第3回で紹介したディスプレイに表示します。
■ArduCAM Mini 2MP カメラモジュール
ArduCam Mini 2MP は、オムニビジョン OV2640 2Mピクセルレンズを搭載したカメラモジュールです。
3Mb(384KB)のフレームバッファを搭載しています。レンズマウントはM12マウントで交換も可能です。
【主な仕様】
| 電源電圧: | DC5V |
| 消費電流: | ノーマル70mA、ローパワーモード20mA |
| フレームバッファ: | 384KB |
| 出力解像度: |
1600×1200、800×600、
640×480、320×240、
352×288、176×144 |
| 出力形式: | RAW、YUV、RGB、JPEG |
| 基板サイズ: | 34×24mm |
| サイズ: | 40×24×33mm(厚みはレンズに依存) |
ArduCamカメラモジュールは、秋月電子通商で購入しました。
ArduCAM Mini 2MP ピン配置
ArduCAM とディスプレイ、マイコンボードとの接続は下記のようになります。
ST7789 | - | QT Py ESP32S2 | - | ArduCam |
| | A0 | - | CS |
D/C | - | A1 | | |
TCS | - | A2 | | |
MOSI | - | MOSI | - | MOSI |
| | MISO | - | MISO |
SCK | - | SCK | - | SCLK |
GND | - | GND | - | GND |
Vin | - | 3.3V | - | +5V |
| | SDA | - | SDA |
| | SCL | - | SCL |
前面、カメラモジュール側からの配線の様子です。
背面、ディスプレイ側からの配線の様子です。
■プログラミング
ソースコードのビルドには、PlatformIOを使用しています。
Arduino開発環境構築 PlatformIO
ビルドに必要なファイル
ビルドに必要なライブラリに関しては、設定ファイル(platform.ini)の lib_deps = に指定してもよいのですが、GitHubからライブラリをダウンロード、展開して、必要なファイルをソースと同じディレクトリに置いてビルドしています。
Arduino/ArduCAM
┣ ArduCAM.cpp
┣ ArduCAM.h
┣ memorysaver.h
┣ ov2640_regs.h
┗ <examples>
┗ <mini>
┗ <ArduCAM_Mini_Video_Streaming>
┗ ArduCAM_Mini_Video_Streaming.ino
adafruit/Adafruit_BusIO
┣ Adafruit_I2CDevice.cpp
┗ Adafruit_I2CDevice.h
$ vi src/memorysaver.h
ファイルを開くと、デフォルトで OV5642_MINI_5MP_PLUS が定義されているので、コメントアウトして、OV2640_MINI_2MP を有効にします。
//Step 1: select the hardware platform, only one at a time
//#define OV2640_MINI_2MP
#define OV5642_MINI_5MP_PLUS
↓
#define OV2640_MINI_2MP
//#define OV5642_MINI_5MP_PLUS
デジカメ初号機では、サンプルコード ArduCAM_Mini_Capture2SD.ino を編集して、撮影画像をJPEG形式で保存していました。
弐号機では、撮影画像をBMP形式で保存するので、ArduCAM_Mini_Video_Streaming.ino を用います。
キャプチャーもストリーミングも、連続撮影していることに変わりないのですが、2つのサンプルコードの大きな違いは下記の箇所です。
ArduCAM_Mini_Capture2SD.ino 抜粋
myCAM.set_format(JPEG);
~~~~
while ( length-- ) {
temp = SPI.transfer(0x00);
if ( (temp == 0xD9) && (temp_last == 0xFF) ) {
buf[i++] = temp; //save the last 0XD9
myCAM.CS_HIGH();
outFile.write(buf, i);
outFile.close();
} else if ((temp == 0xD8) & (temp_last == 0xFF)) {
buf[i++] = temp_last;
buf[i++] = temp;
}
}
上記のJPEG保存用のサンプルコードでは、予めJPEG画像形式に変換された状態で、画像情報が提供されてしまいます。
これをディスプレイに表示可能なRGB形式に変換するのは、かなり複雑なコードを追加する必要があります。
ArduCAM_Mini_Video_Streaming.ino 抜粋・編集
color565 = (uint16_t*)malloc(240*240*2);
myCAM.set_format(BMP);
uint16_t *imgpt;
~~~~
SPI.transfer(0x00);
char VH, VL;
int y = 0, x = 0;
int col = 0;
imgpt = color565;
for (y=0; y<240; y++) {
imgpt += 240;
for (x=0; x<320; x++) {
VH = SPI.transfer(0x00);
VL = SPI.transfer(0x00);
if ((x>39)&&(x<280)) {
imgpt--;
*imgpt = VH|(VL<<8);
}
delayMicroseconds(12);
}
imgpt += 240;
}
Arduino では、RAMが小さいため、画像データをメモリー上にバッファリングすることはできませんが、
Adafruit QT Py ESP32-S2 は、2MバイトのPSRAM を実装しているため、
画像の一次保管場所として、メモリー空間を確保することができます。
color565 = (uint16_t*)malloc(240*240*2);
ArduCam で画像形式をBMPに設定すると、color565(赤:5ビット、緑:6ビット、青:5ビット)の
2バイトの画像データとして取得することができます。
この上位1バイトと下位1バイトをメモリー上に保存します。
*imgpt = VH|(VL<<8);
ArduCam から得られた画像の表示用ディスプレイとして、縦横240ピクセルのLCDを使用しているので、
横長の画像の中心部分の幅240ピクセル分を切り取って使います。
if ((x>39)&&(x<280)) {
また、撮影画像が左右反転していたので、再度反転させて正常な画像に戻しています。
imgpt += 240;
保存した画像を表示する部分は、前回第3回 DISPLAYのSPI制御で説明していますが、下記のようになります。
void show() {
spi_lock(LOCK_ST7789);
drawRGBBitmap(0,0,color565,240,240);
spi_lock(LOCK_NONE);
}
SPI クロック速度
ArduCAM SPIインターフェイスでは、SPI MODE0 固定です。SCLK は最大 8MHzになります。
SPISettings settingsArduCam(8000000, MSBFIRST, SPI_MODE0);
SPISettings settingsST7789(24000000, MSBFIRST, SPI_MODE3);
下記のコードでは、サンプルソースの余計なコードを削除、ロジックの追加などを行っています。
$ vi src/ArduCAM_Mini_Video_Streaming.ino
#include <Wire.h>
#include <SPI.h>
#include <ArduCAM.h>
#include "memorysaver.h"
#define SPI_SCK SCK
#define SPI_MISO MISO
#define SPI_MOSI MOSI
#define SPI_RST -1
#define SPI_DC A1
#define SPI_ARDUCAM_CS A0
#define SPI_TFT_CS A2
ArduCAM myCAM( OV2640, SPI_ARDUCAM_CS );
typedef struct {
uint32_t Width;
uint32_t Height;
} IMAGE_INFO;
SPISettings settingsArduCam(4000000, MSBFIRST, SPI_MODE0);
SPISettings settingsST7789(24000000, MSBFIRST, SPI_MODE3);
#define LOCK_NONE 0
#define LOCK_ARDUCAM 1
#define LOCK_ST7789 3
void spi_lock(int lock) {
static int status = -1;
if (lock==LOCK_NONE) {
SPI.endTransaction();
status = -1;
return;
}
if (lock!=status) {
SPI.endTransaction();
if (lock==LOCK_ARDUCAM) {
SPI.beginTransaction(settingsArduCam);
} else if (lock==LOCK_ST7789) {
SPI.beginTransaction(settingsST7789);
}
status = lock;
}
}
#define TFT_WIDTH 240
#define TFT_HEIGHT 240
uint16_t *color565;
#define HBYTE(u) ((u >> 8) & 0xFF)
#define LBYTE(u) (u & 0xFF)
// TFTにコマンドを送信
void tftSendCommand(uint8_t command) {
digitalWrite(SPI_TFT_CS, LOW);
digitalWrite(SPI_DC, LOW); // Command mode
SPI.transfer(command);
}
// TFTにコマンド+1バイトデータを送信
void tftSendCommand1(uint8_t command, uint8_t data1) {
digitalWrite(SPI_TFT_CS, LOW);
digitalWrite(SPI_DC, LOW); // Command mode
SPI.transfer(command);
digitalWrite(SPI_DC, HIGH); // Data mode
SPI.transfer(data1);
digitalWrite(SPI_TFT_CS, HIGH);
}
// TFTにコマンド+2バイトデータを送信
void tftSendCommand2(uint8_t command, uint8_t data1, uint8_t data2) {
digitalWrite(SPI_TFT_CS, LOW);
digitalWrite(SPI_DC, LOW); // Command mode
SPI.transfer(command);
digitalWrite(SPI_DC, HIGH); // Data mode
SPI.transfer(data1);
SPI.transfer(data2);
digitalWrite(SPI_TFT_CS, HIGH);
}
// TFTにコマンド+3バイトデータを送信
void tftSendCommand3(uint8_t command, uint8_t data1, uint8_t data2, uint8_t data3) {
digitalWrite(SPI_TFT_CS, LOW);
digitalWrite(SPI_DC, LOW); // Command mode
SPI.transfer(command);
digitalWrite(SPI_DC, HIGH); // Data mode
SPI.transfer(data1);
SPI.transfer(data2);
SPI.transfer(data3);
digitalWrite(SPI_TFT_CS, HIGH);
}
// TFTにコマンド+4バイトデータを送信
void tftSendCommand4(uint8_t command, uint8_t data1, uint8_t data2, uint8_t data3, uint8_t data4) {
digitalWrite(SPI_TFT_CS, LOW);
digitalWrite(SPI_DC, LOW); // Command mode
SPI.transfer(command);
digitalWrite(SPI_DC, HIGH); // Data mode
SPI.transfer(data1);
SPI.transfer(data2);
SPI.transfer(data3);
SPI.transfer(data4);
digitalWrite(SPI_TFT_CS, HIGH);
}
void drawRGBBitmap(uint16_t x, uint16_t y, uint16_t *rgbs, uint16_t width, uint16_t height) {
uint8_t h, l;
width--;
h = (uint8_t)(width>>8);
l = (uint8_t)(width&0x00ff);
tftSendCommand4(0x2A,x,y,h,l); // Colmun Address
height--;
h = (uint8_t)(height>>8);
l = (uint8_t)(height&0x00ff);
tftSendCommand4(0x2B,x,y,h,l); // Row Address
digitalWrite(SPI_TFT_CS, LOW);
digitalWrite(SPI_DC, LOW); // Command mode
SPI.transfer(0x2C);
digitalWrite(SPI_DC, HIGH); // Data mode
SPI.transfer(rgbs, width*height*2);
digitalWrite(SPI_TFT_CS, HIGH);
digitalWrite(SPI_DC, LOW); // Command mode
}
// 表示開始ライン設定
void dispStartLine(uint16_t y) {
uint8_t yH = (y >> 8) & 0xFF ;
uint8_t yL = y & 0xFF ;
digitalWrite(SPI_TFT_CS, LOW);
digitalWrite(SPI_DC, LOW); // Command mode
SPI.transfer(0x37);
digitalWrite(SPI_DC, HIGH); // Data mode
SPI.transfer(yH);
SPI.transfer(yL);
digitalWrite(SPI_TFT_CS, HIGH);
}
void init_tft() {
spi_lock(LOCK_ST7789);
// --- HARD Ware Reset
if (SPI_RST >= 0) {
digitalWrite(SPI_RST, HIGH);
delay(500); // VDD goes high at start, pause for 500 ms
digitalWrite(SPI_RST, LOW); // Bring reset low
delay(100); // Wait 100 ms
digitalWrite(SPI_RST, HIGH); // Bring out of reset
delay(500); // Wait 500 ms, more then 120 ms
}
// --- SOFT Ware Reset
tftSendCommand(0x01) ; // SOFTWARE RESET
delay(50);
// --- Initial Comands
tftSendCommand(0x28) ; // Display OFF
delay(500);
tftSendCommand(0x11) ; // Sleep Out
delay(500);
tftSendCommand1(0x3A,0x05) ; // 16Bit Pixel Mode
delay(10);
tftSendCommand1(0x36,B00000000) ; // MX MY MV ML RGB MH x x:縦向き1
tftSendCommand2(0xB6,0x15,0x02) ; // フレームレート設定
tftSendCommand(0x13) ; // NomalDisplayMode
tftSendCommand(0x21) ; // Display Inversion Off
tftSendCommand(0x29) ; // Display ON
tftSendCommand1(0x36,B11000000) ; // MX MY MV ML RGB MH x x:縦向き2
dispStartLine(80);
spi_lock(LOCK_NONE);
}
void show() {
spi_lock(LOCK_ST7789);
drawRGBBitmap(0,0,color565,240,240);
spi_lock(LOCK_NONE);
}
void capture() {
// put your main code here, to run repeatedly:
uint8_t temp = 0xff, temp_last = 0;
bool is_header = false;
uint16_t *imgpt;
temp = 0xff;
myCAM.flush_fifo();
myCAM.clear_fifo_flag();
myCAM.start_capture();
spi_lock(LOCK_ARDUCAM);
while(1) {
if (myCAM.get_bit(ARDUCHIP_TRIG, CAP_DONE_MASK)) {
delay(50);
uint8_t temp, temp_last;
uint32_t length = 0;
length = myCAM.read_fifo_length();
if (length >= MAX_FIFO_SIZE ) {
myCAM.clear_fifo_flag();
spi_lock(LOCK_NONE);
return;
}
if (length == 0 ) { //0 kb
myCAM.clear_fifo_flag();
spi_lock(LOCK_NONE);
return;
}
myCAM.CS_LOW();
myCAM.set_fifo_burst();//Set fifo burst mode
SPI.transfer(0x00);
char VH, VL;
int y = 0, x = 0;
int col = 0;
imgpt = color565;
for (y=0; y<240; y++) {
imgpt += 240;
for (x=0; x<320; x++) {
VH = SPI.transfer(0x00);
VL = SPI.transfer(0x00);
if ((x>39)&&(x<280)) {
imgpt--;
*imgpt = VH|(VL<<8);
}
delayMicroseconds(12);
}
imgpt += 240;
}
myCAM.CS_HIGH();
//Clear the capture done flag
myCAM.clear_fifo_flag();
break;
} else {
delay(100);
}
}
spi_lock(LOCK_NONE);
}
void init_ArduCam() {
uint8_t vid, pid;
uint8_t temp;
Serial1.println(F("ACK CMD ArduCAM Start! END"));
//Reset the CPLD
myCAM.write_reg(0x07, 0x80);
delay(100);
myCAM.write_reg(0x07, 0x00);
delay(100);
while(1){
//Check if the ArduCAM SPI bus is OK
myCAM.write_reg(ARDUCHIP_TEST1, 0x55);
temp = myCAM.read_reg(ARDUCHIP_TEST1);
if (temp != 0x55){
Serial1.println(F("ACK CMD SPI interface Error! END"));
delay(1000);continue;
}else{
Serial1.println(F("ACK CMD SPI interface OK. END"));break;
}
}
while(1){
//Check if the camera module type is OV2640
myCAM.wrSensorReg8_8(0xff, 0x01);
myCAM.rdSensorReg8_8(OV2640_CHIPID_HIGH, &vid);
myCAM.rdSensorReg8_8(OV2640_CHIPID_LOW, &pid);
if ((vid != 0x26 ) && (( pid != 0x41 ) || ( pid != 0x42 ))){
Serial1.println(F("ACK CMD Can't find OV2640 module! END"));
delay(1000);continue;
} else {
Serial1.println(F("ACK CMD OV2640 detected. END"));break;
}
}
myCAM.OV2640_set_JPEG_size(OV2640_320x240); delay(1000);
myCAM.set_format(BMP);
myCAM.InitCAM();
myCAM.wrSensorReg16_8(0x3818, 0x81);
myCAM.wrSensorReg16_8(0x3621, 0xA7);
}
void setup() {
color565 = (uint16_t*)malloc(240*240*2);
if (!color565) {
while(1);
}
Wire.begin();
// set the CS as an output:
pinMode(SPI_ARDUCAM_CS,OUTPUT);
pinMode(SPI_TFT_CS, OUTPUT);
pinMode(SPI_DC, OUTPUT);
digitalWrite(SPI_ARDUCAM_CS, HIGH);
digitalWrite(SPI_TFT_CS, HIGH);
SPI.begin();
init_ArduCam();
init_tft();
}
void loop() {
capture();
show();
}
■自作デジカメの特性を把握する
デジカメ初号機と同様、弐号機も処理速度の観点から、動いている被写体の撮影は想定していません。
ですが、自作デジカメの特性を理解しておくことは重要です。
試しにストリーミング撮影において、ゆっくりと動いているものを撮影するとどうなるか確認してみました。
ArduCamの特性評価のためにのみ購入した「おだてブタ ポチッとな! クリップケース」です。
「豚もおだてりゃ木に登る」とは、日本語のことわざの一つ。普段は無能な者でも、おだててその気にさせると期待以上の成果を出すことがあるという譬え。不可能なことの譬えとして「豚の木登り」ということわざもある。 ウィキペディア
撮影した画像の表示にはタイムラグがあり、撮影間隔も1秒以上ありますが、ゆっくり動くものであれば撮影画像がぶれることはありませんでした。
■参考文献
・Arducam Chip
・SDカード速度比較
・ESP32 の SPI_MODE が修正
|
Raspberry Pi(ラズベリー パイ)は、ARMプロセッサを搭載したシングルボードコンピュータ。イギリスのラズベリーパイ財団によって開発されている。
Arduinoで学ぶ組込みシステム入門(第2版)
●Arduinoを使って組込みシステム開発を理解する
・ハードウェアやソフトウェアなどの基礎知識/
・設計から実装までを系統的に説明するモデルベース開発/
・Arduinoを用いた実際の開発例
最新 使える! MATLAB 第3版
◆◆すぐに「使える!」 全ページフルカラー!◆◆
・MATLAB R2022bに対応し、解説もより詳しく!/
・コマンド・スクリプトの例が豊富で、動かして学べる!/
・超基本から解説。これから使いはじめる人にぴったり!/
・全編フルカラー、スクリーンショットも豊富!
Amazon Web Services基礎からのネットワーク&サーバー構築改訂4版
1.システム構築をインフラから始めるには/
2.ネットワークを構築する/
3.サーバーを構築する/
4.Webサーバーソフトをインストールする/
5.HTTPの動きを確認する/
6.プライベートサブネットを構築する/
7.NATを構築する/
8.DBを用いたブログシステムの構築/
9.TCP/IPによる通信の仕組みを理解する
C言語は第二の母国語: 独学学生時代から企業内IT職人時代に培った、独立のための技術とノウハウ 平田豊著
学生時代から独学でプログラミングをはじめ、企業内でデバイスドライバを開発し、そして独立後もたくさんのアプリケーション開発や技術書制作に携わってきた著者。その筆者が大事に使い続ける「C言語」の“昔と今”について、気づいたことや役立つ知識、使ってきたツールなどについて、これまで記してきたことを整理してまとめました。
本書では、現役プログラマーだけでなく、これからプログラミングを学ぶ学生などにも有益な情報やノウハウを、筆者の経験を元に紹介しています。
1冊ですべて身につくJavaScript入門講座
・最初の一歩が踏み出せる! 初心者に寄り添うやさしい解説
・最新の技術が身につく! 今のJavaScriptの書き方・使い方
・絶対に知っておきたい! アニメーションとイベントの知識
・プログラミングの基本から実装方法まですべて学べる
図解! Git & GitHubのツボとコツがゼッタイにわかる本
ソフトウェア開発では欠かすことのできないGit、GitHub。
これからGit、GitHubを使いたいという入門者の方でも、実際に手を動かしながら使い方を学べます。
C自作の鉄則!2023 (日経BPパソコンベストムック)
メーカー製のパソコンはスペックが中途半端で、自分が本当に欲しい機種がない――。そう思っている人には、ぜひ自作パソコンをお薦めします。自作パソコンのパーツは進化が速く、しかも驚くほど種類が豊富。価格も性能も、幅広く用意されているため、満足度100%の“自分だけの1台”を手に入れることができます。
Interface 2023年6月号
特集:第1部 フィルタ設計 基礎の基礎/
第2部 係数アプリや波形観測アプリで合点!FIR&IIRフィルタ作り/
第3部 配布プリント基板で体験!マイコンで動くフィルタ作り
日経Linux 2023年5月号
【特集 1】 AI時代の最強フリーソフト ~ 25のやりたいを実現!
【特集 2】 AWS、Azureのうまみを無料で体感!面倒なことはクラウドに任せよう
【特集 3】 新しいRaspberry Pi Cameraで遊んでみよう
【特集 4】 Linuxで旧型PCを復活! 1kg切るモバイルPCを「ChromeOS Flex」でChromebook化
ラズパイマガジン2022年秋号
特集:5大人気ボード 電子工作超入門
「半導体不足で在庫が不足し、電子工作のボードがなかなか買えない…」。そんな今にふさわしい特集を企画しました。5種の人気ボードにすべて対応した電子工作の入門特集です。「GPIO」や「I2C」を使った電子パーツの制御方法は、どのボードでも同じです。手に入れられたボードを使って、今こそ電子工作を始めましょう。
地方で稼ぐ! ITエンジニアのすすめ
学歴、理系の知識、専門スキル……全部なくてもITエンジニアになれる!
地方でも高収入でやりがいをもって働ける!ITエンジニアの魅力を一挙大公開
Raspberry Piのはじめ方2022
本書は、ラズパイやPicoの買い方やインストール、初期設定といった基本から、サーバー、電子工作、IoT、AIといったラズパイならではの活用方法まで、1冊でお届けします。
ラズパイをこれから始める方向けに、全36ページの入門マンガ「女子高生とラズベリーパイ」も巻末に掲載。これを読むだけでラズパイがどんなものなのか、すぐに分かって触れるようになります。
ハッカーの学校 IoTハッキングの教科書
生活にとけこみ、家電機器を便利にするIoT技術。
Webカメラなど、便利の裏側に潜むセキュリティの危険性をハッキングで検証。
専門家がパケットキャプチャからハードウェアハッキングまで、その攻撃と防御を徹底解説。
本書は2018年7月に刊行された「ハッカーの学校IoTハッキングの教科書」に一部修正を加えた第2版です。
攻撃手法を学んで防御せよ! 押さえておくべきIoTハッキング
本書は、経済産業省から2021年4月にリリースされた、IoTセキュリティを対象とした『機器のサイバーセキュリティ確保のためのセキュリティ検証の手引き』の『別冊2 機器メーカに向けた脅威分析及びセキュリティ検証の解説書』をもとに、IoT機器の開発者や品質保証の担当者が、攻撃者の視点に立ってセキュリティ検証を実践するための手法を、事例とともに詳細に解説しました。
ポチらせる文章術
販売サイト・ネット広告・メルマガ・ブログ・ホームページ・SNS…
全WEB媒体で効果バツグン!
カリスマコピーライターが教える「見てもらう」「買ってもらう」「共感してもらう」すべてに効くネット文章術
プログラマーは世界をどう見ているのか 西村博之著
イーロン・マスク(テスラ)、ジェフ・べゾス(Amazon)、ラリー・ペイジ(Google)…etc.
世界のトップはなぜプログラマーなのか?
ニーア オートマタ PLAY ARTS改 <ヨルハ 二号 B型 DX版> PVC製 塗装済み可動フィギュア
「NieR:Automata」より、ヨルハ二号B型こと2BがPLAY ARTS改に新たに登場!
高級感の感じられるコスチュームや髪の質感、洗練されたボディバランス、細かなデティールに至るまでこだわり抜かれた逸品。
DX版には通常版のラインナップに加え2Bの随行支援ユニット ポッド042などをはじめ“純白の美しい太刀"白の約定やエフェクトパーツ、自爆モードを再現できる換装用ボディパーツ、シーンに合わせて変えられる顔パーツ2種も付属する豪華な仕様に。
作中のあらゆるシーンを再現することが可能なファン必見の一品となっている。
Newtonライト2.0 ベイズ統計
ベイズ統計は,結果から原因を推定する統計学です。AIや医療などの幅広い分野で応用されています。その基礎となるのは18世紀に考えだされた「ベイズの定理」です。
この本では,ベイズ統計学のきほんをやさしく紹介していきます。
白光(HAKKO) ダイヤル式温度制御はんだ吸取器 ハンディタイプ FR301-81
無水エタノールP 500mlx2個パック(掃除)
ケイバ(KEIBA) マイクロニッパー MN-A04
サンハヤト SAD-101 ニューブレッドボード
白光(HAKKO) HEXSOL 巻はんだ 精密プリント基板用 150g FS402-02
[Amazon限定ブランド]【指定第2類医薬品】PHARMA CHOICE 解熱鎮痛薬 解熱鎮痛錠IP 100錠
|