電波時計を合わせるためのプログラム(スケッチ)入力
やっとここまで来ました。
では、Arduino – ESP32 を使って、JJY信号を発信させて、電波時計を合わせるプログラム(スケッチ)を入力していきます。
2年前のこちらの記事を変更して、以下のようにしました。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)
#include <WiFi.h>
#include <WiFiUdp.h>
#include <TimeLib.h> //Arduino time library ver1.5-
const char* ssid = "xxxxxxxx"; //ご自分のルーターのSSIDに書き換えてください
const char* password = "xxxxxxxx"; //ご自分のルーターのパスワードに書き換えてください
const uint8_t gpio_high = 17;
const uint8_t gpio_low = 16;
uint8_t min_now, hour_now, year_now, weekday_now;
uint16_t day_total;
uint8_t parity1, parity2; //パリティ定義
//-----------NTP server Get 関連初期化 --------------------------
unsigned int localPort = 2390;
IPAddress timeServer;
const char* ntpServerName = "time.windows.com";
const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
byte timeZone = 9; //Tokyo
WiFiUDP Udp;
uint32_t get_ntp_last_time = 0;
//*****************セットアップ******************************
void setup() {
Serial.begin(115200);
delay(10);
Serial.println();
Serial.print(F("Connecting to "));
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.println(F("WiFi connected"));
delay(1000);
Serial.println(WiFi.localIP());
delay(10);
pinMode(gpio_high, OUTPUT);
pinMode(gpio_low, OUTPUT);
WiFi.hostByName(ntpServerName, timeServer); //サーバーネームをIPアドレスに変換
Udp.begin(localPort);
setTime( getNtpTime() ); //NTPサーバーから時刻を取得し、ESP32内蔵時計にセットする
delay(2000); //安定するまで待つ
setTime( now() + 1 ); //標準時とキッチリ合わせるため、1秒早める。
get_ntp_last_time = millis();
TaskHandle_t th; //マルチタスクハンドル定義
//マルチタスク実行
xTaskCreatePinnedToCore(Task1, "Task1", 4096, NULL, 5, &th, 0);
}
//************* メインループ ****************************************
void loop() {
if((millis() - get_ntp_last_time) > 1800000){ //30分毎にNTPサーバーから時刻取得
setTime( getNtpTime() );
getNowTime(); //現在時刻表示
get_ntp_last_time = millis();
}
}
//************* マルチタスク ****************************************
void Task1(void *pvParameters){
uint32_t last_time = 0;
if(second() == 0){
delay(1500); //ゼロ秒の場合は次のゼロ秒までやり過ごすために、1.5秒待つ。
}
while(second() != 0){ //ゼロ秒になるまで待つ
delay(1); //ウォッチドッグタイマを動作させるために必要
}
Serial.println();
for(;;){
last_time = millis();
getNowTime(); //現在時刻初期化
sendMerker(); //M マーカー送信
sendMinuteTimeCode(); //タイムコード(分)送信
sendMerker(); //P1 ポジションマーカー送信
sendHourTimeCode(); //タイムコード(時)送信
sendMerker(); //P2 ポジションマーカー送信
sendDayTotalParityTimeCode(); //タイムコード(通算日)送信
sendMerker(); //P4 ポジションマーカー送信
sendYearTimeCode(); //タイムコード(年)送信
sendMerker(); //P5 ポジションマーカー送信
sendWeekdayUruuTimeCode(); //タイムコード(曜日、うるう秒)送信
sendMerker(); //P0 ポジションマーカー送信
Serial.printf("\r\n[Total = %ld ms]\r\n", millis() - last_time);
while(second() != 0){ //もし、時間がズレた場合、ゼロ秒になるまで時間調整する
Serial.print('.');
delay(1); //ウォッチドッグタイマを動作させるために必要
}
}
}
//******** マーカーおよびポジションマーカー送信 ************************
void sendMerker(){
Serial.print(".M.");
oscillator(gpio_high, 200);
oscillator(gpio_low, 800);
delay(1); //ウォッチドッグタイマを動作させるために必要
}
//******** タイムコード(分)送信 **************
void sendMinuteTimeCode(){
uint8_t min10 = (uint8_t)floor((double)min_now / 10.0);
uint8_t min01 = min_now % 10;
uint8_t m[7] = {0}; //分のパリティ parity2 計算用配列初期化
//---------10の位-------------
m[6] = bitRead( min10, 2 );
Serial.print( m[6] );
oscillatorTimeCodeBit( m[6] );
m[5] = bitRead( min10, 1 );
Serial.print( m[5] );
oscillatorTimeCodeBit( m[5] );
m[4] = bitRead( min10, 0 );
Serial.print( m[4] );
oscillatorTimeCodeBit( m[4] );
Serial.print('0');
oscillatorTimeCodeBit(0);
//---------1の位--------------
m[3] = bitRead( min01, 3 );
Serial.print( m[3] );
oscillatorTimeCodeBit( m[3] );
m[2] = bitRead( min01, 2 );
Serial.print( m[2] );
oscillatorTimeCodeBit( m[2] );
m[1] = bitRead( min01, 1 );
Serial.print( m[1] );
oscillatorTimeCodeBit( m[1] );
m[0] = bitRead( min01, 0 );
Serial.print( m[0] );
oscillatorTimeCodeBit( m[0] );
//-------分パリティ計算-------
parity2 = (m[6] + m[5] + m[4] + m[3] + m[2] + m[1] + m[0]) % 2;
}
//******** タイムコード(時)送信 *************
void sendHourTimeCode(){
uint8_t hour10 = (uint8_t)floor((double)hour_now / 10.0);
uint8_t hour01 = hour_now % 10;
uint8_t h[6] = {0}; //時のパリティ parity1 計算用配列初期化
Serial.print('0');
oscillatorTimeCodeBit(0);
Serial.print('0');
oscillatorTimeCodeBit(0);
//---------10の位-------------
h[5] = bitRead( hour10, 1 );
Serial.print( h[5] );
oscillatorTimeCodeBit( h[5] );
h[4] = bitRead( hour10, 0 );
Serial.print( h[4] );
oscillatorTimeCodeBit( h[4] );
Serial.print('0');
oscillatorTimeCodeBit(0);
//---------1の位-------------
h[3] = bitRead( hour01, 3 );
Serial.print( h[3] );
oscillatorTimeCodeBit( h[3] );
h[2] = bitRead( hour01, 2 );
Serial.print( h[2] );
oscillatorTimeCodeBit( h[2] );
h[1] = bitRead( hour01, 1 );
Serial.print( h[1] );
oscillatorTimeCodeBit( h[1] );
h[0] = bitRead( hour01, 0 );
Serial.print( h[0] );
oscillatorTimeCodeBit( h[0] );
//-------分パリティ計算-------
parity1 = (h[5] + h[4] + h[3] + h[2] + h[1] + h[0]) % 2;
}
//******** タイムコード(その年の1月1日からの通算日)送信 ***********
void sendDayTotalParityTimeCode(){
uint8_t t_day100 = (uint8_t)floor((double)day_total / 100.0);
uint8_t t_day10 = ((uint8_t)(floor((double)day_total / 10.0))) % 10;
uint8_t t_day01 = day_total % 10;
uint8_t b;
Serial.print('0');
oscillatorTimeCodeBit(0);
Serial.print('0');
oscillatorTimeCodeBit(0);
//---------100の位-------------
b = bitRead( t_day100, 1 );
Serial.print( b );
oscillatorTimeCodeBit( b );
b = bitRead( t_day100, 0 );
Serial.print( b );
oscillatorTimeCodeBit( b );
Serial.print('0');
oscillatorTimeCodeBit(0);
//---------10の位-------------
b = bitRead( t_day10, 3 );
Serial.print( b );
oscillatorTimeCodeBit( b );
b = bitRead( t_day10, 2 );
Serial.print( b );
oscillatorTimeCodeBit( b );
b = bitRead( t_day10, 1 );
Serial.print( b );
oscillatorTimeCodeBit( b );
b = bitRead( t_day10, 0 );
Serial.print( b );
oscillatorTimeCodeBit( b );
sendMerker(); //P3 ポジションマーカー
//---------1の位-------------
b = bitRead( t_day01, 3 );
Serial.print( b );
oscillatorTimeCodeBit( b );
b = bitRead( t_day01, 2 );
Serial.print( b );
oscillatorTimeCodeBit( b );
b = bitRead( t_day01, 1 );
Serial.print( b );
oscillatorTimeCodeBit( b );
b = bitRead( t_day01, 0 );
Serial.print( b );
oscillatorTimeCodeBit( b );
Serial.print('0');
oscillatorTimeCodeBit(0);
Serial.print('0');
oscillatorTimeCodeBit(0);
//---------パリティビット---------
Serial.print(parity1);
oscillatorTimeCodeBit(parity1);
Serial.print(parity2);
oscillatorTimeCodeBit(parity2);
//---------予備ビット-------------
Serial.print('0');
oscillatorTimeCodeBit(0); //SU1 予備ビット
}
//**********タイムコード(年)下2桁送信**********************
void sendYearTimeCode(){
uint8_t year10 = (uint8_t)(floor((double)year_now / 10.0));
uint8_t year01 = year_now % 10;
uint8_t b;
//---------予備ビット---------
Serial.print('0');
oscillatorTimeCodeBit(0); //SU2 予備ビット
//---------10の位-------------
b = bitRead( year10, 3 );
Serial.print( b );
oscillatorTimeCodeBit( b );
b = bitRead( year10, 2 );
Serial.print( b );
oscillatorTimeCodeBit( b );
b = bitRead( year10, 1 );
Serial.print( b );
oscillatorTimeCodeBit( b );
b = bitRead( year10, 0 );
Serial.print( b );
oscillatorTimeCodeBit( b );
//---------1の位-------------
b = bitRead( year01, 3 );
Serial.print( b );
oscillatorTimeCodeBit( b );
b = bitRead( year01, 2 );
Serial.print( b );
oscillatorTimeCodeBit( b );
b = bitRead( year01, 1 );
Serial.print( b );
oscillatorTimeCodeBit( b );
b = bitRead( year01, 0 );
Serial.print( b );
oscillatorTimeCodeBit( b );
}
//********** タイムコード(曜日、うるう秒)送信 ************
void sendWeekdayUruuTimeCode(){
uint8_t b;
//---------曜日-------------
b = bitRead( weekday_now, 2 );
Serial.print( b );
oscillatorTimeCodeBit( b );
b = bitRead( weekday_now, 1 );
Serial.print( b );
oscillatorTimeCodeBit( b );
b = bitRead( weekday_now, 0 );
Serial.print( b );
oscillatorTimeCodeBit( b );
//---------うるう秒---------
Serial.print('0');
oscillatorTimeCodeBit(0); //LS1:うるう秒
Serial.print('0');
oscillatorTimeCodeBit(0); //LS2:うるう秒
//--------------------------
Serial.print('0');
oscillatorTimeCodeBit(0);
Serial.print('0');
oscillatorTimeCodeBit(0);
Serial.print('0');
oscillatorTimeCodeBit(0);
Serial.print('0');
oscillatorTimeCodeBit(0);
}
//********** タイムコードビット送信 *****************
void oscillatorTimeCodeBit(uint8_t b){
if( b ){ //2進数の1
oscillator(gpio_high, 500);
oscillator(gpio_low, 500);
delay(1); //ウォッチドッグタイマを動作させるために必要
}else{ //2進数の0
oscillator(gpio_high, 800);
oscillator(gpio_low, 200);
delay(1); //ウォッチドッグタイマを動作させるために必要
}
}
//*********** 40kHz High レベル発信******************
void oscillator(const uint8_t pin, uint16_t time_osc){
uint32_t last_time = millis();
for(;;){
long startCount;
startCount = getCpuCcount();
digitalWrite(pin, HIGH);
while(getCpuCcount() - startCount < 2960) NOP();
startCount = getCpuCcount();
digitalWrite(pin, LOW);
while (getCpuCcount() - startCount < 2700) NOP();
if(millis() - last_time == time_osc) return;
}
}
//*********CPUクロックでカウントする*****************
static inline unsigned getCpuCcount(void){
unsigned r;
asm volatile ("rsr %0, ccount" : "=r"(r));
return r;
}
//*********** その年の1月1日からの通算日計算 ******
uint16_t calcDayTotal(){
//Arduino Timeライブラリ関数を使う
tmElements_t tm = {0, 0, 0, 0, 1, 1, (uint8_t)(year()-1970)};
time_t t = makeTime(tm);
return (now() - t) / SECS_PER_DAY + 1;
}
//*********** 現在時刻代入 ************************
void getNowTime(){
min_now = minute();
hour_now = hour();
year_now = year() - 2000;
if(year_now > 100) year_now = 0; //有り得ない数値は0にする
weekday_now = weekday() - 1;
day_total = calcDayTotal();
Serial.printf("\r\nYear=%d / TotalDay=%d / Weekday=%d / %02d:%02d\r\n", year_now, day_total, weekday_now, hour_now, min_now);
}
//********* NTP time GET ***************************
time_t getNtpTime(){
while (Udp.parsePacket() > 0) ;
Serial.println("Transmit NTP Request");
sendNtpPacket(timeServer);
uint32_t beginWait = millis();
while (millis() - beginWait < 1500) {
int size = Udp.parsePacket();
if (size >= NTP_PACKET_SIZE) {
Serial.println("Receive NTP Response");
Udp.read(packetBuffer, NTP_PACKET_SIZE);
unsigned long secsSince1900;
// convert four bytes starting at location 40 to a long integer
secsSince1900 = (unsigned long)packetBuffer[40] << 24;
secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
secsSince1900 |= (unsigned long)packetBuffer[43];
return secsSince1900 - 2208988800UL + timeZone * 3600UL;
}
}
Serial.println("No NTP Response :-(");
return 0; // return 0 if unable to get the time
}
//******** Send NTP Packet *************************
void sendNtpPacket(IPAddress &address){
memset(packetBuffer, 0, NTP_PACKET_SIZE);
packetBuffer[0] = 0b11100011; // LI, Version, Mode
packetBuffer[1] = 0; // Stratum, or type of clock
packetBuffer[2] = 6; // Polling Interval
packetBuffer[3] = 0xEC; // Peer Clock Precision
packetBuffer[12] = 49;
packetBuffer[13] = 0x4E;
packetBuffer[14] = 49;
packetBuffer[15] = 52;
Udp.beginPacket(address, 123); //NTP requests are to port 123
Udp.write(packetBuffer, NTP_PACKET_SIZE);
Udp.endPacket();
}
5-6行目でご自分のルーターの SSID とパスワードに書き換えてください。
先に紹介したように、以前のこちらの記事のコメント欄で、Kat-Kaiさんに教えていただいた、マイクロセコンド単位でパルス幅を変えられる関数を入れています。
2年前のこちらの記事と殆ど同じ構成ですが、JJY のHIGHレベル信号関数とLOWレベル関数を同じ関数で統合しています。
メインloop関数内で、ネット上の NTPサーバーから30分毎に時刻を取得して、マルチタスクループ内で、JJY信号を GPIO から発信させています。
重要なのは、マルチタスクループ内では、約1秒くらいの間に最低1ms以上の休止時間を設けねばなりません。
ESP32 のウォッチドックタイマを動作させるためです。
ですから、109-114行と、318-328行のところに delay を入れて、トータルで 60000ms に近づくようにしています。
要するに、JJY のLOWレベル信号が終わる時に 1ms の休止時間を設けています。
LOWレベル信号は無くても電波時計は受信できるので、影響の少ない LOWレベル信号に delay を入れています。
オシロスコープで波形を眺めていると、休止時間がわかると思います。
40kHzの信号内で、1ms の休止時間っていうのは結構長いですね。
以下の記事も合わせて参照してください。
(2019/04/04)
https://www.mgo-tec.com/blog-entry-ledc-pwm-arduino-esp32.html
コンパイル書き込み実行
では、Arduino IDE でコンパイル書き込み実行させてみて下さい。
そして、インターネットにつながった Wi-Fi ルーターを起動して、シリアルモニターを 115200 bps で起動しておいてください。
しばらくして、標準時刻がゼロ秒になったところで、LED が点滅し始めると思います。
シリアルモニターには以下のように表示されます。
Total 時間が 60000 ms になるように、スケッチ上の oscillator 関数の時間や、ウォッチドッグタイマ動作用の delay を調整してください。
その他調整方法は、以前の記事を参照してください。
アンテナ用ワイヤーと電波時計の設置方法
今回は、以前の記事とは別の設置方法を紹介します。
何重にも巻いたループアンテナは作りません。
要するに、電波は 10㎝ も飛びませんが、電流の流れているコードワイヤー単線のみで近くに電波時計が置いておけば、しっかり受信してくれます。
交流なので、電流の方向は図の方向ではなく、40kHzで交互になりますが、流れる電流の方向を意識してワイヤーを配置すると良いと思います。
下図の様に逆方向の電流同士のワイヤーを近づけてしまうと、電流磁界が相殺されて電波が飛ばないので気を付けてください。
このように電流の逆方向の電路をピッタリ付けることによって、放射ノイズを減らすことになります。
その部分を撚ってツイストさせると、さらに電波が飛びづらくなります。
それがツイストペアーと言われているやつです。
LANケーブルなどの高周波ケーブルの中は、ツイストペアー線だらけですね。
また、大きい円でコイル状に巻くと、今度は同じ方向の電流が集まるので、その付近の電波は多くなると思われます。
以前の記事はそれを使っています。
これはいろいろと実験してみて下さい。
いずれにしても、パッシブパーツだけの構成なので、大して電波は飛びません。
コイル状に巻かなくても、単線だけで、ワイヤーに電波時計を近づければ結構十分受信してくれますよ。
というわけで、ここまで電波送信回路が何となく分かって来たので、いつかアマチュア無線免許でも取って、ダイポールアンテナとかで電波発信してみたいという欲が出て来てしまいますね。
いつになることやら・・・。
まとめ
どうでしょうか?
AD9833 のようなサイン波発生チップを使わずとも、パッシブパーツだけで構成した LC 共振回路だけで、十分キレイなサイン波形ができることが分かりました。
いままでインダクターの挙動が大の苦手だったのですが、これで好きなパーツになりましたね。
私個人的なLC共振回路の発見は、一旦、スイッチで瞬間的に電圧をかけてやれば、その後電圧をかけ続けているのに、勝手に自然とサイン波振動してしまうことです。
これに気付いて共振回路の利用方法をひらめいたことが大きな収穫でした。
後は、電波発信機にしたければ、この振動を維持して増幅すればいいだけのことです。
でも、電圧だけ増幅しても飛ばないので、電波を出すには電流を流すことということもわかりました。
このことが実際に体感で理解できたので、グランドループノイズなどの対策もイメージできますし、モーターやソレノイドなどのノイズ対策もイメージできそうな気がして、なんでも作れそうな気がしました。
それにしても、コイルって改めてスゴイですね・・・。
ということで、自己満足ですが、以前指摘された共振回路のリベンジを果たすことができたのではないかと思います。
これで心置きなく次の工作やプログラミングに進めそうです。
因みに、何か間違えていたらコメント投稿欄等でご連絡いただけると助かります。
何しろド素人ですから。
今回はここまでです。
そろそろディープラーニング勉強しないと・・・。
ではまた・・・。
コメント
mgo-tecさま始めまして。
あなたさまのHP楽しく拝見させていただいております。
此処に有る電波時計のサーバーを作ってみました、動作はするのですがシリアルモニタのTotal 時間が 60000 msの所が上 60008 ms 下が 59994 ms と一寸バラツキが大きいように思っております。
後は、特に問題なく時間もピッタリと会っております、ここだけの問題ですが気にしなければ特に問題はないのですが製作上何か何かまずいところがあったのでしょうか、ちょっと気になっております。
Amazonの1,250円の安いwaves ESP32を使ってせいでしょうか?。
てるさん
はじめまして。
記事をご覧いただき、そして実際に作って頂きありがとうございます。
別の記事で同じようなコメント投稿があったのですが、こちらで回答させていただきます。
かなり昔に作った記事ですので、うろ覚えの記憶をたどってお答えしますが、トータル60000msのバラつきは必ず出ますので、ハードの不良ではありません。
そもそも、この記事のプログラムでは、パルスの精度が悪く、CPUクロックカウントも誤差があり、しかもESP32のシステム関連の割り込みなどがあって、60000msピッタリとはいきません。
ですから、しばらく動かしていていて、時刻と大幅にズレて来た場合、現行時刻の00秒に合わせるために1分少々動作がストップするようにプログラミングしています。
なお、この記事内でも案内していますが、LEDCライブラリを使って出力パルスの精度を大幅に上げることができます。
それは、この記事を作ってしばらく後でわかったことです。
以下の記事を参照してみてください。
●https://www.mgo-tec.com/blog-entry-ledc-pwm-arduino-esp32.html
●https://www.mgo-tec.com/blog-entry-m5stack-jjy-watch-yhaoo-news.html
mgo-tecさま、ご返答ありがとうございました。
コメントをする時に、コメント送信ボタンを押すと、パ!と全て消えてしまうので送れていないと勘違いて何度か同じコメントを送ってしまったらしいです、お詫び申し上げます。
LEDCライブラリを使って出力パルスの精度を上げてみたいと思っております、ありがとうございました。
そうなんですよね~。
WordPressのブログでは、コメント投稿送信はそうなってしまうんです。
LEDCライブラリはお勧めですよ~。
(^^)
それと
3番目のセラミックコンデンサの乗数が1μFのが0.01μになっておりました。
先に連絡すればよかったのですが、遅れて申し訳ありません。
おー!
この記事は3年半以上前の記事ですが、気付きませんでした。
早速修正しました~。
ご連絡、ありがとうございました。
m(_ _)m
お忙しい中大変申し訳ありませんが又質問させていただきたくお願いいたします。
時計サーバーですが、良くしようと色々といじくりまわしておりましたが、
「task_wdt: Task watchdog got triggered. The following tasks did not reset the watchdog in time:」と出て途切れ途切れになってしましました、サーバーの役目をしなく成ってしまいました、何が原因なのかわかりません、このような症状出たことありませんでしょうか。
暇なときにでも、わかる範囲で宜しいのでお教えいただければと思っております。
本当に暇な時で宜しいです。
私もウォッチドックタイマに関しては詳しくないのですが、当ブログで何回か扱ったので検索して参照してみてください。
例えば、以下の記事を参照してみてください。
https://www.mgo-tec.com/blog-entry-trouble-shooting-esp32-wroom.html/4
ウォッチドッグタイマの自動リセット?が機能しなかったかもです。
ご回答ありがとうございます。
>WiFiClientSecure ライブラリを使って
とありましたので、プログラムには全く駄目な僕ですが、スケッチを下記のようにしてみました
1://#include
2:#include //安定化させる、てるが追加
3:#include
4:#include //Arduino time library ver1.5-
今のところ非常に安定しています。
もうこれ以上は、いじらない様にしたいと思います。
ありがとうございました。
http://monsirochou.blog.fc2.com/
コードがよくわかりませんが、安定しているのならそれが一番良いと思います。
おはようございます。
includenoの中身が記入されていないみたいですが、この様に送らておりましたか?。
実は、コメント投稿欄では半角の括弧<>以降は削除されてしまうんです。
一般的なブログの場合、多くがそうだと思われます。
私の場合、WordPressでブログを作成しているので、仕方のないところです。
括弧だけ全角で投稿してみてください。
例えばこんな感じです。
#include<ESP32_lib.h>
思いつきで失礼します。
パッシブ部品を望まれるのでしたら電波時計に使われるバーアンテナ水平置き+コンデンサーのタンク回路は実験としていかがでしょうか。
受信も送信も共振回路は似たようなものですから。
偏波が合うのでよく受信してくれますし、送信器としてはいい物ではありませんが、コンパクトで偏波が合うので本体だけで上下か前後方向にバッチリ指向性が付くんじゃないかと想像しています。
通りがかりのゆにっこぷらす さん
記事をご覧いただき、誠にありがとうございます。
この記事は2019年1月に書いたもので、すっかり忘れてしまっていました。
そういえば、当時はX(旧Twitter)でバーアンテナを使った方法を教えてもらった記憶があります。
試したことは無いのですが、そんなに良いのなら試してみたいですね。
今は電子工作から離れてしまっているので、復帰したらチャレンジしたいと思います。
情報ありがとうございま~す!
m(_ _)m