ESP8266 MP3制作——关于SelectionList从源码中改代码的一次经历

SelectionList修改源码(这可能为扩展引脚提供一个新思路)

原因

上一篇文章已经提到了SelectionList的神奇功能,然后在setup中有这个一段代码:

u8g2.begin(/* menu_select_pin= */ 5, /* menu_next_pin= */ 4, /* menu_prev_pin= */ 2, /* menu_up_pin= */ U8X8_PIN_NONE, /* menu_down_pin= */ U8X8_PIN_NONE, /* menu_home_pin= */ 3);
  • 可以定义6个引脚来完成这个功能,后续我查了一下源码,发现menu_next_pin和menu_down_pin的功能是一样的,而menu_prev_pin和menu_up_pin的功能是一样的,也就是说我如果要得到这里面全部的功能,至少需要4个引脚,但是我数了一下我的Nodemcu,只剩2个引脚可以使用,这可就难办了。
  • 我第一个想到的是扩展引脚,之前好像听说过一个芯片叫74HC595,能扩展io口,买回来才知道它只能扩展o口(output)不能扩展i口(input)。而且如果要把引脚和这个功能关联起来,这个引脚肯定得是能叫得上名字的,也就是自带的引脚,很显然不能用扩展引脚。
  • 接着我想能不能先看看它的映射关系是怎么样的(即是说这个库是怎么把引脚和功能关联的),**找到这种关联的关系,然后植入自己想要的代码,就可以把想要的功能继承过来了。**至于input口不够的问题,我突然也找到了一个解决办法:用nodemcu仅有的模拟引脚接收信号,不同的按钮接不同的电阻,读出来的模拟值就不同,通过这个来判断按下的是哪个键即可,至于具体电阻值等参数,自己算好然后测试就可以了。

找代码

(注:如果你使用的是新版本的arduino开发工具,要找哪个函数的源代码直接右键然后跳转到定义即可)
先观察代码:

  current_selection = u8g2.userInterfaceSelectionList(
    "Cloud Types",
    current_selection, 
    string_list);

  if ( current_selection == 0 ) {
    u8g2.userInterfaceMessage(
	"Nothing selected.", 
	"",
	"",
	" ok ");
  } else {
    u8g2.userInterfaceMessage(
	"Selection:", 
	u8x8_GetStringLineStart(current_selection-1, string_list ),
	"",
	" ok \n cancel ");
  }
  • 这个是放在loop里面的,userInterfaceSelectionList是个阻塞函数,只有当其中一个项被选中才会返回,然后弹出一个确认界面,这就是上述代码的大致功能。

  • 很显然,我们如果想要在显示SelectionList的时候控制menu_next_pin等功能,就得查看userInterfaceSelectionList的源码

  • 在这个文件的最上方引入了这个文件

    #include <U8g2lib.h>
    

    所以我们进入这个文件(文档\Arduino\libraries\U8g2\src\U8g2lib.h),用查找功能查找
    在这里插入图片描述
    发现它其实是调用了u8g2_UserInterfaceSelectionList这个函数,在文件的最上方看头文件:

    #ifndef U8G2LIB_HH
    #define U8G2LIB_HH
    
    #ifdef ARDUINO
    #include <Arduino.h>
    #include <Print.h>
    #endif
    
    #include "U8x8lib.h"
    
    #include "clib/u8g2.h"
    

    只有后面两个比较像,而函数名是u8g2开头的,下看最后一个文件
    在这里插入图片描述
    果然找到了,不知道是只有这个库写得这么清楚还是,他直接标出这个函数的定义的.c文件的位置,所以点卡这个文件,找到这个函数即可
    在这里插入图片描述
    好啦,找到之后剩下的代码分析部分等明天再来补充,未完待续。。。


从这里开始

好久没写博客了,突然想起来我还有这篇没写完,继续补齐

分析源码

这个是我找到的库的源代码
里面有我阅读源码的注释

uint8_t u8g2_UserInterfaceSelectionList(u8g2_t *u8g2, const char *title, uint8_t start_pos, const char *sl)
{
  //一些数据处理,比如说绘画位置的确定
  u8sl_t u8sl;
  u8g2_uint_t yy;

  uint8_t event;

  u8g2_uint_t line_height = u8g2_GetAscent(u8g2) - u8g2_GetDescent(u8g2)+MY_BORDER_SIZE;

  uint8_t title_lines = u8x8_GetStringLineCnt(title);
  uint8_t display_lines;

  
  if ( start_pos > 0 )	/* issue 112 */
    start_pos--;		/* issue 112 */


  if ( title_lines > 0 )
  {
	display_lines = (u8g2_GetDisplayHeight(u8g2)-3) / line_height;
	u8sl.visible = display_lines;
	u8sl.visible -= title_lines;
  }
  else
  {
	display_lines = u8g2_GetDisplayHeight(u8g2) / line_height;
	u8sl.visible = display_lines;
  }

  u8sl.total = u8x8_GetStringLineCnt(sl);
  u8sl.first_pos = 0;
  u8sl.current_pos = start_pos;

  if ( u8sl.current_pos >= u8sl.total )
    u8sl.current_pos = u8sl.total-1;
  if ( u8sl.first_pos+u8sl.visible <= u8sl.current_pos )
    u8sl.first_pos = u8sl.current_pos-u8sl.visible+1;

  u8g2_SetFontPosBaseline(u8g2);
  //上面都是处理,这里处理完了,开始绘画
  for(;;)
  {
  	  //进入第一个循环,正常情况下这个for是不会退出的
  	  //可以看到for后面没有return语句,说明写代码的人根本不希望这个函数return?!!接着往下看
      u8g2_FirstPage(u8g2);
      do
      {
        yy = u8g2_GetAscent(u8g2);
        if ( title_lines > 0 )
        {
          yy += u8g2_DrawUTF8Lines(u8g2, 0, yy, u8g2_GetDisplayWidth(u8g2), line_height, title);
		
	  u8g2_DrawHLine(u8g2, 0, yy-line_height- u8g2_GetDescent(u8g2) + 1, u8g2_GetDisplayWidth(u8g2));
		
	  yy += 3;
        }
        u8g2_DrawSelectionList(u8g2, &u8sl, yy, sl);
      } while( u8g2_NextPage(u8g2) );
      //这一段很明显就是真正在绘制窗口
      
#ifdef U8G2_REF_MAN_PIC
      return 0;
#endif

	  //既然绘制完了,那么下面这段代码在干什么呢?
	  //学过win32或者qt的小伙伴不难发现,这是事件处理,触发这些事件应该是更底层完成的事情,与我们无关
	  //但是可以发现,只需要我们的写代码过程中,除了我们的按键按下,将其对应到下面的代码即可
	  //对了,也许有读者会奇怪这里for一直卡着,那事件处理是怎么发生的呢
	  //换句话说,我们怎么在这个循环过程中执行我们自己的代码呢?
	  //当然是用中断了,我觉得这个库应该也是通过中断实现这个功能的(或者是定时器扫描,总之不是多线程)
      for(;;)
      {
        event = u8x8_GetMenuEvent(u8g2_GetU8x8(u8g2));
        if ( event == U8X8_MSG_GPIO_MENU_SELECT )
          return u8sl.current_pos+1;		/* +1, issue 112 */
        else if ( event == U8X8_MSG_GPIO_MENU_HOME )
          return 0;				/* issue 112: return 0 instead of start_pos */
        else if ( event == U8X8_MSG_GPIO_MENU_NEXT || event == U8X8_MSG_GPIO_MENU_DOWN )
        {
          u8sl_Next(&u8sl);
          break;
        }
        else if ( event == U8X8_MSG_GPIO_MENU_PREV || event == U8X8_MSG_GPIO_MENU_UP )
        {
          u8sl_Prev(&u8sl);
          break;
        }
      }
  }
}

看了上面的代码和注释,想必该怎么做就显而易见了,那就是通过io口的中断处理按键信息,在按键按下时执行源码中对应的操作,这样子就可以将源码的功能扩展了

具体处理过程

可以发现这个函数使用的大多是局部变量,event也不例外,所以要在这个函数外部实现上述功能,有以下几个思路:

  • 定义全局变量,然后在这个函数中处理这个全局变量,在函数外部就可以修改这个全局变量(变量名字越长,越有个性越好,不容易跟源代码的变量相撞)
  • 修改参数,传入指针或者引用,修改值能够引起函数内发生对应的操作
    思路2虽然听起来不错,这也是我最开始的思路,但是需要注意以下几个问题:
    - 参数必须放在最后
    - 参数必须有初始值
    - 要想最后一层的函数能接收到参数,要从最外层的函数改起(但愿你能听懂我在说什么)
    这样子才能保证源码的其他部分能照常运行

但是出了个大问题:arduino的c++版本似乎不支持默认参数(不知道是不是我哪里写错了,反正我看完报错信息是这么认为的)
所以的决定使用全局变量,在看下面的代码之前,建议先去了解extern的用法:

我在u8g2.h中引入了这两个变量(名字够长吧):

extern uint8_t my_bool_changed_for_selectionList; // 判断是否有按钮按下(事件触发),不用bool是因为我发现arduino好像也不支持bool
extern uint8_t my_event_for_selectionList;	//具体事件

在处理对应事件的代码中,我加入了这段:

	   for(;;)
	   {
	   		//从这里开始添加代码
		   if(my_bool_changed_for_selectionList == 1)
		   { 
				event = my_event_for_selectionList;
				my_bool_changed_for_selectionList = 0; 
				  if ( event == U8X8_MSG_GPIO_MENU_SELECT )
			          return u8sl.current_pos+1;		/* +1, issue 112 */
			        else if ( event == U8X8_MSG_GPIO_MENU_HOME )
			          return 0;				/* issue 112: return 0 instead of start_pos */
			        else if ( event == U8X8_MSG_GPIO_MENU_NEXT || event == U8X8_MSG_GPIO_MENU_DOWN )
			        {
			          u8sl_Next(&u8sl);
			          break;
			        }
			        else if ( event == U8X8_MSG_GPIO_MENU_PREV || event == U8X8_MSG_GPIO_MENU_UP )
			        {
			          u8sl_Prev(&u8sl);
			          break;
			        }
			event = 0;
	 	 }
	 	 //代码添加结束
	     event = u8x8_GetMenuEvent(u8g2_GetU8x8(u8g2));
	     if ( event == U8X8_MSG_GPIO_MENU_SELECT )
	       return u8sl.current_pos+1;		/* +1, issue 112 */
	     else if ( event == U8X8_MSG_GPIO_MENU_HOME )
	       return 0;				/* issue 112: return 0 instead of start_pos */
	     else if ( event == U8X8_MSG_GPIO_MENU_NEXT || event == U8X8_MSG_GPIO_MENU_DOWN )
	     {
	       u8sl_Next(&u8sl);
	       break;
	     }
	     else if ( event == U8X8_MSG_GPIO_MENU_PREV || event == U8X8_MSG_GPIO_MENU_UP )
	     {
	       u8sl_Prev(&u8sl);
	       break;
	     }
	   }

接下来是处理按钮按下的伪代码

	interupt function()
		... //消抖操作
		if(按钮"select"按下)
			my_event_for_selectionList = U8X8_MSG_GPIO_MENU_SELECT;
    	    my_bool_changed_for_selectionList = 1;

测试之后发现没问题,而且这个办法同样适用于u8g2.userInterfaceMessage这个函数,有兴趣的可以自己试一下


内容就这么多,难免有些啰嗦,望见谅,如果有大佬发现有哪些做得不够,还望不吝指出

esp8266 语音播放 //Priorities of the reader and the decoder thread. Higher = higher prio. #define PRIO_READER 11 #define PRIO_MAD 1 //The mp3 read buffer size. 2106 bytes should be enough for up to 48KHz mp3s according to the sox sources. Used by libmad. #define READBUFSZ (2106) static char readBuf[READBUFSZ]; static long bufUnderrunCt; //Reformat the 16-bit mono sample to a format we can send to I2S. static int sampToI2s(short s) { //We can send a 32-bit sample to the I2S subsystem and the DAC will neatly split it up in 2 //16-bit analog values, one for left and one for right. //Duplicate 16-bit sample to both the L and R channel int samp=s; samp=(samp)&0xffff; samp=(samp<65535) samp=65535; if (samp>11]; err=(samp&0x7ff); //Save rounding error. return samp; } //2nd order delta-sigma DAC //See http://www.beis.de/Elektronik/DeltaSigma/DeltaSigma.html for a nice explanation static int sampToI2sDeltaSigma(short s) { int x; int val=0; int w; static int i1v=0, i2v=0; static int outReg=0; for (x=0; x<32; x++) { val<0) w-=32767; else w+=32767; //Difference 1 w+=i1v; i1v=w; //Integrator 1 if (outReg>0) w-=32767; else w+=32767; //Difference 2 w+=i2v; i2v=w; //Integrator 2 outReg=w; //register if (w>0) val|=1; //comparator } return val; } //Calculate the number of samples that we add or delete. Added samples means a slightly lower //playback rate, deleted samples means we increase playout speed a bit. This returns an //8.24 fixed-point number int recalcAddDelSamp(int oldVal) { int ret; long prevUdr=0; static int cnt; int i; static int minFifoFill=0; i=spiRamFifoFill(); if (i<minFifoFill) minFifoFill=i; //Do the rest of the calculations plusminus every 100mS (assuming a sample rate of 44KHz) cnt++; if (cnt<1500) return oldVal; cnt=0; if (spiRamFifoLen()<10*1024) { //The FIFO is very small. We can't do calculations on how much it's filled on average, so another //algorithm is called for. int tgt=1600; //we want an average of this amount of bytes as the average minimum buffer fill //Calculate underruns this cycle int udr=spiRamGetUnderrunCt()-prevUdr; //If we have underruns, the minimum buffer fill has been lower than 0. if (udr!=0) minFifoFill=-1; //If we're below our target decrease playback speed, and vice-versa. ret=oldVal+((minFifoFill-tgt)*ADD_DEL_BUFFPERSAMP_NOSPIRAM); prevUdr+=udr; minFifoFill=9999; } else { //We have a larger FIFO; we can adjust according to the FIFO fill rate. int tgt=spiRamFifoLen()/2; ret=(spiRamFifoFill()-tgt)*ADD_DEL_BUFFPERSAMP; } return ret; } //This routine is called by the NXP modifications of libmad. It passes us (for the mono synth) //32 16-bit samples. void render_sample_block(short *short_sample_buff, int no_samples) { //Signed 16.16 fixed point number: the amount of samples we need to add or delete //in every 32-sample static int sampAddDel=0; //Remainder of sampAddDel cumulatives static int sampErr=0; int i; int samp; #ifdef ADD_DEL_SAMPLES sampAddDel=recalcAddDelSamp(sampAddDel); #endif sampErr+=sampAddDel; for (i=0; i(1<<24)) { sampErr-=(1<<24); //...and don't output an i2s sample } else if (sampErr<-(1<<24)) { sampErr+=(1<bufend-stream->next_frame; memmove(readBuf, stream->next_frame, rem); while (rem<sizeof(readBuf)) { n=(sizeof(readBuf)-rem); //Calculate amount of bytes we need to fill buffer. i=spiRamFifoFill(); if (i<n) n=i; //If the fifo can give us less, only take that amount if (n==0) { //Can't take anything? //Wait until there is enough data in the buffer. This only happens when the data feed //rate is too low, and shouldn't normally be needed! // printf("Buf uflow, need %d bytes.\n", sizeof(readBuf)-rem); bufUnderrunCt++; //We both silence the output as well as wait a while by pushing silent samples into the i2s system. //This waits for about 200mS for (n=0; nerror, mad_stream_errorstr(stream)); return MAD_FLOW_CONTINUE; } //This is the main mp3 decoding task. It will grab data from the input buffer FIFO in the SPI ram and //output it to the I2S port. void ICACHE_FLASH_ATTR tskmad(void *pvParameters) { int r; struct mad_stream *stream; struct mad_frame *frame; struct mad_synth *synth; //Allocate structs needed for mp3 decoding stream=malloc(sizeof(struct mad_stream)); frame=malloc(sizeof(struct mad_frame)); synth=malloc(sizeof(struct mad_synth)); if (stream==NULL) { printf("MAD: malloc(stream) failed\n"); return; } if (synth==NULL) { printf("MAD: malloc(synth) failed\n"); return; } if (frame==NULL) { printf("MAD: malloc(frame) failed\n"); return; } //Initialize I2S i2sInit(); bufUnderrunCt=0; printf("MAD: Decoder start.\n"); //Initialize mp3 parts mad_stream_init(stream); mad_frame_init(frame); mad_synth_init(synth); while(1) { input(stream); //calls mad_stream_buffer internally while(1) { r=mad_frame_decode(frame, stream); if (r==-1) { if (!MAD_RECOVERABLE(stream->error)) { //We're most likely out of buffer and need to call input() again break; } error(NULL, stream, frame); continue; } mad_synth_frame(synth, frame); } } } int getIpForHost(const char *host, struct sockaddr_in *ip) { struct hostent *he; struct in_addr **addr_list; he=gethostbyname(host); if (he==NULL) return 0; addr_list=(struct in_addr **)he->h_addr_list; if (addr_list[0]==NULL) return 0; ip->sin_family=AF_INET; memcpy(&ip->sin_addr, addr_list[0], sizeof(ip->sin_addr)); return 1; } //Open a connection to a webserver and request an URL. Yes, this possibly is one of the worst ways to do this, //but RAM is at a premium here, and this works for most of the cases. int ICACHE_FLASH_ATTR openConn(const char *streamHost, const char *streamPath) { int n, i; while(1) { struct sockaddr_in remote_ip; bzero(&remote_ip, sizeof(struct sockaddr_in)); if (!getIpForHost(streamHost, &remote_ip)) { vTaskDelay(1000/portTICK_RATE_MS); continue; } int sock=socket(PF_INET, SOCK_STREAM, 0); if (sock==-1) { continue; } remote_ip.sin_port = htons(streamPort); printf("Connecting to server %s...\n", ipaddr_ntoa((const ip_addr_t*)&remote_ip.sin_addr.s_addr)); if (connect(sock, (struct sockaddr *)(&remote_ip), sizeof(struct sockaddr))!=00) { close(sock); printf("Conn err.\n"); vTaskDelay(1000/portTICK_RATE_MS); continue; } //Cobble together HTTP request write(sock, "GET ", 4); write(sock, streamPath, strlen(streamPath)); write(sock, " HTTP/1.0\r\nHost: ", 17); write(sock, streamHost, strlen(streamHost)); write(sock, "\r\n\r\n", 4); //We ignore the headers that the server sends back... it's pretty dirty in general to do that, //but it works here because the MP3 decoder skips it because it isn't valid MP3 data. return sock; } } //Reader task. This will try to read data from a TCP socket into the SPI fifo buffer. void ICACHE_FLASH_ATTR tskreader(void *pvParameters) { int madRunning=0; char wbuf[64]; int n, l, inBuf; int t; int fd; int c=0; while(1) { fd=openConn(streamHost, streamPath); printf("Reading into SPI RAM FIFO...\n"); do { n=read(fd, wbuf, sizeof(wbuf)); if (n>0) spiRamFifoWrite(wbuf, n); c+=n; if ((!madRunning) && (spiRamFifoFree()0); close(fd); printf("Connection closed.\n"); } } //Simple task to connect to an access point, initialize i2s and fire up the reader task. void ICACHE_FLASH_ATTR tskconnect(void *pvParameters) { //Wait a few secs for the stack to settle down vTaskDelay(3000/portTICK_RATE_MS); //Go to station mode wifi_station_disconnect(); if (wifi_get_opmode() != STATION_MODE) { wifi_set_opmode(STATION_MODE); } //Connect to the defined access point. struct station_config *config=malloc(sizeof(struct station_config)); memset(config, 0x00, sizeof(struct station_config)); sprintf(config->ssid, AP_NAME); sprintf(config->password, AP_PASS); wifi_station_set_config(config); wifi_station_connect(); free(config); //Fire up the reader task. The reader task will fire up the MP3 decoder as soon //as it has read enough MP3 data. if (xTaskCreate(tskreader, "tskreader", 230, NULL, PRIO_READER, NULL)!=pdPASS) printf("Error creating reader task!\n"); //We're done. Delete this task. vTaskDelete(NULL); } //We need this to tell the OS we're running at a higher clock frequency. extern void os_update_cpu_frequency(int mhz); void ICACHE_FLASH_ATTR user_init(void) { //Tell hardware to run at 160MHz instead of 80MHz //This actually is not needed in normal situations... the hardware is quick enough to do //MP3 decoding at 80MHz. It, however, seems to help with receiving data over long and/or unstable //links, so you may want to turn it on. Also, the delta-sigma code seems to need a bit more speed //than the other solutions to keep up with the output samples, so it's also enabled there. #if defined(DELTA_SIGMA_HACK) SET_PERI_REG_MASK(0x3ff00014, BIT(0)); os_update_cpu_frequency(160); #endif //Set the UART to 115200 baud UART_SetBaudrate(0, 115200); //Initialize the SPI RAM chip communications and see if it actually retains some bytes. If it //doesn't, warn user. if (!spiRamFifoInit()) { printf("\n\nSPI RAM chip fail!\n"); while(1); } printf("\n\nHardware initialized. Waiting for network.\n"); xTaskCreate(tskconnect, "tskconnect", 200, NULL, 3, NULL); }
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值